开发者博客 – IT技术 尽在开发者博客

开发者博客 – 科技是第一生产力


  • 首页

  • 归档

  • 搜索

选择困难症必看!云服务器如何选择操作系统,Windows和L

发表于 2020-07-08

在购买云服务器时,会有一个必选的配置,就是操作系统的选择,如何选择操作系统?操作系统选择错了怎么办?这是不少用户会遇到的问题,今天我们就来教大家如何选择操作系统,以及操作系统选择错了,该怎么切换。

Windows操作系统和Linux操作系统有何区别?

Windows操作系统:需支付版权费用,(华为云已购买正版版权,在华为云购买云服务器的用户安装系统时无需额外付费),界面化的操作系统对用户使用习惯来说可能更容易上手;目前华为云提供的版本有2008版、2012版、2016版和2019版,并有英文和中文版的区分。

Linux操作系统:分为商业公司维护的商业版本和开源社区维护的免费发行版本,常用的Ubuntu、CentOS、Debian属于免费发行版,而Redhat和SUSE需要收费,有界面化和命令行两种操作,可分为Debian系、Redhat系以及其他自由的发布版本,当前华为云提供了CentOS、Ubuntu、EulerOS、Debian、OpenSUSE、Fedora、CoreOS、openEuler以及other等9种可供选择的操作系统。

Windows系统和Linux系统哪个更好,应该怎么选择?

其实不存在哪个好哪个不好,两个操作系统各有特点,只是说哪个操作系统更适合你,适合你的业务,在选择时,可从如下角度去选择。

Ø 根据业务需求来选(网站采用哪种开发语言?网站的数据库类型?)

如果开发语言为ASP、.NET、MFC、C#,数据库为ACCESS、SQL Server,请选择Windows;如果需要运行Microsoft软件,则只能选择Windows;

如果开发语言为WAP,数据库为MySQL、SQLite,请选择Linux;

如果开发语言为HTML、C、JAVA、PHP等,两种操作系统都支持,随心选吧!

Ø 如果你选择好了系统,这里我们再来讲讲版本如何选择?

Windows:版本选择的建议是版本越高越好,win2003和win2008都已经停止了安全更新,许多云厂商也会逐步下线这两个版本,因此不建议选择,,win2012、win2016、win2019网络优化和系统兼容性比较好,兼具开放性,可伸缩性、安全性、高性能、操作简单。另外,在选择32位还是64位时,区别在内存的大小,32位最大只可支持到4GB内存,如果要使用高于4GB的内存或者以后有扩充内存到4GB以上,选择64位操作系统。至于语言,中文还是英文,请根据自身使用习惯来选择。

Linux:常用的发行版是CentOS、Ubuntu、Debian,当前,绝大多数互联网公司选择CentOS, CentOS更侧重服务器领域,并且无版权约束。

华为云提供哪些版本的操作系统?

Windows系统,华为云在用户购买云服务器时提供了最多16个版本

Linux系统,华为云提供了9个常用的Linux操作系统,且每个系统又有不同的版本。选择非常丰富!

如果选错了云服务器的操作系统,怎么切换?

这里为大家整理了详细的切换指导,

1、 登录管理控制台

2、 单击管理控制台左上角的,选择区域和项目。

3、 选择计算>弹性云服务器。

4、 在待切换操作系统的弹性云服务器的“操作”列下,单击“更多>镜像/磁盘>切换操作系统”。

请注意:切换操作系统前请先将云服务器关机,或根据页面提示勾选“系统自动关机后切换操作系统”。

5、 根据需求选择需要更换的弹性云服务器规格,包括“镜像类型”和“镜像”。

6、 设置登录方式,如果待切换操作系统的弹性云服务器是使用密钥登录方式创建的,此时可以更换新密钥。

7、 单击“确定”

8、 在“切换云服务器操作系统”页面,确认切换的操作系统规格无误后,阅读并勾选“我已经阅读并同意《华为弹性云服务器服务协议》”,单击“提交申请”。

请注意:提交切换操作系统的申请后,弹性云服务器的状态变为“切换中”,当该状态消失后,表示切换结束。

说明:切换操作系统过程中,会创建一台临时弹性云服务器,切换操作系统结束后会自动删除。

看完这些,知道该怎么选择云服务器操作系统了吧!另外,教大家一个省钱小秘籍,如果你是华为云的新用户,是可以免费领云服务器的,最长6个月的使用期,还可以关注官网的促销活动。

点击关注,第一时间了解华为云新鲜技术~

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

做前后端分离项目前,劝你先了解 OAuth20 的四种授权

发表于 2020-07-08

本文收录在个人博客:www.chengxy-nds.top,技术资源共享,一起进步

上周我的自研开源项目开始破土动工了,《开源项目迈出第一步,10 选 1?页面模板成了第一个绊脚石
》
,密谋很久才付诸行动,做这个的初衷就是不想让自己太安稳,技术这条路不进步就等于后退,必须要逼着自己学习。

项目偏向于技术实践,因此不会做太多的业务堆砌,业务代码还是在公司学习比较好。现在正在做技术的选型与储备,像比较主流的,项目前后端分离、微服务、Springboot、Springcloud 等都会应用到项目中,其实很多技术我也不会,也是在反复的查阅资料求证,探索的过程技术提升真的要比工作中快很多,毕竟主动与被动学习是有本质区别的。

这几天打算先把项目的前后端分离架构搭建完成,既然是前后端分离项目就免不了做鉴权, 所以 oauth2.0 是一个我们不得不了解的知识点。


一、OAuth2.0 为何物

OAuth 简单理解就是一种授权机制,它是在客户端和资源所有者之间的授权层,用来分离两种不同的角色。在资源所有者同意并向客户端颁发令牌后,客户端携带令牌可以访问资源所有者的资源。

OAuth2.0 是OAuth 协议的一个版本,有2.0版本那就有1.0版本,有意思的是OAuth2.0 却不向下兼容OAuth1.0 ,相当于废弃了1.0版本。

举个小栗子解释一下什么是 OAuth 授权?

在家肝文章饿了定了一个外卖,外卖小哥30秒火速到达了我家楼下,奈何有门禁进不来,可以输入密码进入,但出于安全的考虑我并不想告诉他密码。

此时外卖小哥看到门禁有一个高级按钮“一键获取授权”,只要我这边同意,他会获取到一个有效期 2小时的令牌(token)正常出入。

在这里插入图片描述

在这里插入图片描述

令牌(token)和 密码 的作用虽然相似都可以进入系统,但还有点不同。token 拥有权限范围,有时效性的,到期自动失效,而且无效修改。

二、OAuth2.0 授权方式

OAuth2.0 的授权简单理解其实就是获取令牌(token)的过程,OAuth 协议定义了四种获得令牌的授权方式(authorization grant )如下:

  • 授权码(authorization-code)
  • 隐藏式(implicit)
  • 密码式(password):
  • 客户端凭证(client credentials)

但值得注意的是,不管我们使用哪一种授权方式,在三方应用申请令牌之前,都必须在系统中去申请身份唯一标识:客户端 ID(client ID)和 客户端密钥(client secret)。这样做可以保证 token 不被恶意使用。

下面我们会分析每种授权方式的原理,在进入正题前,先了解 OAuth2.0 授权过程中几个重要的参数:

  • response_type:code 表示要求返回授权码,token 表示直接返回令牌
  • client_id:客户端身份标识
  • client_secret:客户端密钥
  • redirect_uri:重定向地址
  • scope:表示授权的范围,read只读权限,all读写权限
  • grant_type:表示授权的方式,AUTHORIZATION_CODE(授权码)、password(密码)、client_credentials(凭证式)、refresh_token 更新令牌
  • state:应用程序传递的一个随机数,用来防止CSRF攻击。

1、授权码

OAuth2.0四种授权中授权码方式是最为复杂,但也是安全系数最高的,比较常用的一种方式。这种方式适用于兼具前后端的Web项目,因为有些项目只有后端或只有前端,并不适用授权码模式。

下图我们以用WX登录掘金为例,详细看一下授权码方式的整体流程。

在这里插入图片描述

在这里插入图片描述

用户选择WX登录掘金,掘金会向WX发起授权请求,接下来 WX询问用户是否同意授权(常见的弹窗授权)。response_type 为 code 要求返回授权码,scope 参数表示本次授权范围为只读权限,redirect_uri 重定向地址。

1
2
3
4
5
复制代码https://wx.com/oauth/authorize?
response_type=code&
client_id=CLIENT_ID&
redirect_uri=http://juejin.im/callback&
scope=read

用户同意授权后,WX 根据 redirect_uri重定向并带上授权码。

1
复制代码http://juejin.im/callback?code=AUTHORIZATION_CODE

当掘金拿到授权码(code)时,带授权码和密匙等参数向WX申请令牌。grant_type表示本次授权为授权码方式 authorization_code ,获取令牌要带上客户端密匙 client_secret,和上一步得到的授权码 code。

1
2
3
4
5
6
复制代码https://wx.com/oauth/token?
client_id=CLIENT_ID&
client_secret=CLIENT_SECRET&
grant_type=authorization_code&
code=AUTHORIZATION_CODE&
redirect_uri=http://juejin.im/callback

最后 WX 收到请求后向 redirect_uri 地址发送 JSON 数据,其中的access_token 就是令牌。

1
2
3
4
5
6
7
8
复制代码 {    
"access_token":"ACCESS_TOKEN",
"token_type":"bearer",
"expires_in":2592000,
"refresh_token":"REFRESH_TOKEN",
"scope":"read",
......
}

2、隐藏式

上边提到有一些Web应用是没有后端的, 属于纯前端应用,无法用上边的授权码模式。令牌的申请与存储都需要在前端完成,跳过了授权码这一步。

前端应用直接获取 token,response_type 设置为 token,要求直接返回令牌,跳过授权码,WX授权通过后重定向到指定 redirect_uri 。

1
2
3
4
5
复制代码https://wx.com/oauth/authorize?
response_type=token&
client_id=CLIENT_ID&
redirect_uri=http://juejin.im/callback&
scope=read

3、密码式

密码模式比较好理解,用户在掘金直接输入自己的WX用户名和密码,掘金拿着信息直接去WX申请令牌,请求响应的 JSON结果中返回 token。grant_type 为 password 表示密码式授权。

1
2
3
4
5
复制代码https://wx.com/token?
grant_type=password&
username=USERNAME&
password=PASSWORD&
client_id=CLIENT_ID

这种授权方式缺点是显而易见的,非常的危险,如果采取此方式授权,该应用一定是可以高度信任的。

4、凭证式

凭证式和密码式很相似,主要适用于那些没有前端的命令行应用,可以用最简单的方式获取令牌,在请求响应的 JSON 结果中返回 token。

grant_type 为 client_credentials 表示凭证式授权,client_id 和 client_secret 用来识别身份。

1
2
3
4
复制代码https://wx.com/token?
grant_type=client_credentials&
client_id=CLIENT_ID&
client_secret=CLIENT_SECRET

三、令牌的使用与更新

1、令牌怎么用?

拿到令牌可以调用 WX API 请求数据了,那令牌该怎么用呢?

每个到达WX的请求都必须带上 token,将 token 放在 http 请求头部的一个Authorization字段里。

如果使用postman 模拟请求,要在Authorization -> Bearer Token 放入 token,注意:低版本postman 没有这个选项。

在这里插入图片描述

在这里插入图片描述

2、令牌过期怎么办?

token是有时效性的,一旦过期就需要重新获取,但是重走一遍授权流程,不仅麻烦而且用户体验也不好,那如何让更新令牌变得优雅一点呢?

一般在颁发令牌时会一次发两个令牌,一个令牌用来请求API,另一个负责更新令牌 refresh_token。grant_type 为 refresh_token 请求为更新令牌,参数 refresh_token 是用于更新令牌的令牌。

1
2
3
4
5
复制代码https://wx.com/oauth/token?
grant_type=refresh_token&
client_id=CLIENT_ID&
client_secret=CLIENT_SECRET&
refresh_token=REFRESH_TOKEN

总结

OAuth2.0 授权其实并不是很难,只不过授权流程稍显麻烦,逻辑有些绕,OAuth2.0它是面试经常会被问到的知识点,还是应该多了解一下。下一篇实战 OAuth2.0四种授权,敬请期待,欢迎关注哦~


原创不易,燃烧秀发输出内容,如果有一丢丢收获,点个赞鼓励一下吧!

整理了几百本各类技术电子书,送给小伙伴们。关注公号回复【666】自行领取。和一些小伙伴们建了一个技术交流群,一起探讨技术、分享技术资料,旨在共同学习进步,如果感兴趣就加入我们吧!

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

OAuth20系列之客户端模式实践教程(五)

发表于 2020-07-07

@TOC
OAuth2.0系列博客:

  • OAuth2.0系列之基本概念和运作流程(一)
  • OAuth2.0系列之授权码模式实践教程(二)
  • OAuth2.0系列之简化模式实践教程(三)
  • OAuth2.0系列之密码模式实践教程(四)
  • OAuth2.0系列之客户端模式实践教程(五)
  • OAuth2.0系列之信息数据库存储教程(六)
  • OAuth2.0系列之信息Redis存储教程(七)
  • OAuth2.0系列之JWT令牌实践教程(八)
  • OAuth2.0系列之集成JWT实现单点登录

1、客户端模式简介

1.1 前言简介

在上一篇文章中我们学习了OAuth2的一些基本概念,对OAuth2有了基本的认识,接着学习OAuth2.0授权模式中的客户端模式

ps:OAuth2.0的授权模式可以分为:

  • 授权码模式(authorization code)
  • 简化模式(implicit)
  • 密码模式(resource owner password credentials)
  • 客户端模式(client credentials)

客户端模式(client credentials):客户端模式(client credentials)适用于没有前端的命令行应用,即在命令行下请求令牌

1.2 授权流程图

官网图片:

在这里插入图片描述

  • (A)客户端提供client_id等信息给授权服务器授权服务器身份验证
  • (B)授权通过,返回acceptToken给客户端

从调接口方面,简单来说:

  • 第一步: 获取token
    http://localhost:8888/oauth/token?client_id=cms&client_secret=secret&grant_type=client_credentials&scope=all
  • 第二步:拿到acceptToken之后,就可以直接访问资源

http://localhost:8084/api/userinfo?access_token=${accept_token}

2、例子实践

2.1 实验环境准备

  • IntelliJ IDEA
  • Maven3.+版本
    新建SpringBoot Initializer项目,可以命名password
    在这里插入图片描述

在这里插入图片描述

主要是想引入:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
复制代码 <dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- Spring Cloud Oauth2-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-oauth2</artifactId>
</dependency>
<!-- Spring Cloud Security-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-security</artifactId>
</dependency>

2.2 OAuth2.0角色

前面的学习,我们知道了OAuth2.0主要包括如下角色,下面通过代码例子加深对理论的理解

  • 资源所有者(Resource Owner)
  • 用户代理(User Agent)
  • 客户端(Client)
  • 授权服务器(Authorization Server)
  • 资源服务器(Resource Server)

生产环境、资源服务器和授权服务器一般是分开的,不过学习的可以放在一起

定义资源服务器,用注解@EnableResourceServer;
定义授权服务器,用注解@EnableAuthorizationServer;

2.3 OAuth2.0配置类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
复制代码package com.example.oauth2.clientcredentials.config;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.oauth2.config.annotation.configurers.ClientDetailsServiceConfigurer;
import org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerConfigurerAdapter;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableAuthorizationServer;
import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerEndpointsConfigurer;
import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerSecurityConfigurer;
import org.springframework.security.oauth2.provider.token.TokenStore;
import org.springframework.security.oauth2.provider.token.store.InMemoryTokenStore;


/**
* <pre>
* OAuth2.0配置类
* </pre>
*
* <pre>
* @author mazq
* 修改记录
* 修改后版本: 修改人: 修改日期: 2020/06/11 11:00 修改内容:
* </pre>
*/
@Configuration
//开启授权服务
@EnableAuthorizationServer
public class OAuth2Config extends AuthorizationServerConfigurerAdapter {

@Autowired
private AuthenticationManager authenticationManager;

private static final String CLIENT_ID = "cms";
private static final String SECRET_CHAR_SEQUENCE = "{noop}secret";
private static final String SCOPE_READ = "read";
private static final String SCOPE_WRITE = "write";
private static final String TRUST = "trust";
private static final String USER ="user";
private static final String ALL = "all";
private static final int ACCESS_TOKEN_VALIDITY_SECONDS = 2*60;
private static final int FREFRESH_TOKEN_VALIDITY_SECONDS = 2*60;
// 密码模式授权模式
private static final String GRANT_TYPE_PASSWORD = "password";
//授权码模式
private static final String AUTHORIZATION_CODE = "authorization_code";
//refresh token模式
private static final String REFRESH_TOKEN = "refresh_token";
//简化授权模式
private static final String IMPLICIT = "implicit";
//客户端模式
private static final String CLIENT_CREDENTIALS="client_credentials";
//指定哪些资源是需要授权验证的
private static final String RESOURCE_ID = "resource_id";

@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients
// 使用内存存储
.inMemory()
//标记客户端id
.withClient(CLIENT_ID)
//客户端安全码
.secret(SECRET_CHAR_SEQUENCE)
//为true 直接自动授权成功返回code
.autoApprove(true)
.redirectUris("http://127.0.0.1:8084/cms/login") //重定向uri
//允许授权范围
.scopes(ALL)
//token 时间秒
.accessTokenValiditySeconds(ACCESS_TOKEN_VALIDITY_SECONDS)
//刷新token 时间 秒
.refreshTokenValiditySeconds(FREFRESH_TOKEN_VALIDITY_SECONDS)
//允许授权类型
.authorizedGrantTypes(CLIENT_CREDENTIALS );
}

@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
// 使用内存保存生成的token
endpoints.authenticationManager(authenticationManager).tokenStore(memoryTokenStore());
}

/**
* 认证服务器的安全配置
*
* @param security
* @throws Exception
*/
@Override
public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
security
//.realm(RESOURCE_ID)
// 开启/oauth/token_key验证端口认证权限访问
.tokenKeyAccess("isAuthenticated()")
// 开启/oauth/check_token验证端口认证权限访问
.checkTokenAccess("isAuthenticated()")
//允许表单认证
.allowFormAuthenticationForClients();
}

@Bean
public TokenStore memoryTokenStore() {
// 最基本的InMemoryTokenStore生成token
return new InMemoryTokenStore();
}

}

2.4 Security配置类

为了测试,可以进行简单的SpringSecurity

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
复制代码package com.example.oauth2.clientcredentials.config;


import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;

/**
* <pre>
* SpringSecurity配置类
* </pre>
*
* <pre>
* @author mazq
* 修改记录
* 修改后版本: 修改人: 修改日期: 2020/06/11 11:23 修改内容:
* </pre>
*/
@Configuration
@EnableWebSecurity
@Order(1)
public class SecurityConfig extends WebSecurityConfigurerAdapter {

@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}

@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception { //auth.inMemoryAuthentication()
auth.inMemoryAuthentication()
.withUser("nicky")
.password("{noop}123")
.roles("admin");
}

@Override
public void configure(WebSecurity web) throws Exception {
//解决静态资源被拦截的问题
web.ignoring().antMatchers("/asserts/**");
web.ignoring().antMatchers("/favicon.ico");
}

@Override
protected void configure(HttpSecurity http) throws Exception {
http // 配置登录页并允许访问
//.formLogin().permitAll()
// 配置Basic登录
//.and().httpBasic()
// 配置登出页面
.logout().logoutUrl("/logout").logoutSuccessUrl("/")
// 配置允许访问的链接
.and().authorizeRequests().antMatchers("/oauth/**", "/login/**", "/logout/**","/api/**").permitAll()
// 其余所有请求全部需要鉴权认证
.anyRequest().authenticated()
// 关闭跨域保护;
.and().csrf().disable();
}

}

2.5 功能简单测试

接口测试,要用POST方式,在postman测试,response_type参数传client_credentials:

http://localhost:8888/oauth/token?client_id=cms&client_secret=secret&grant_type=client_credentials&scope=all

在这里插入图片描述

注意配置一下请求头的授权参数,username即client_id,password即client_secret

在这里插入图片描述

代码方式请求,可以进行如下封装,即进行base64加密

1
2
3
4
5
复制代码HttpHeaders headers = new HttpHeaders();
byte[] key = (clientId+":"+clientSecret).getBytes();
String authKey = new String(Base64.encodeBase64(key));
LOG.info("Authorization:{}","Basic "+authKey);
headers.add("Authorization","Basic "+authKey);

拿到token直接去调业务接口:
http://localhost:8888/api/userinfo?access_token=61b113f3-f1e2-473e-a6d7-a0264bfdfa8d

例子代码下载:code download

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

面试官:说说Kafka处理请求的全流程 Reactor模式

发表于 2020-07-07

大家好,我是 yes。

这是我的第三篇Kafka源码分析文章,前两篇讲了日志段的读写和二分算法在kafka索引上的应用

今天来讲讲 Kafka Broker端处理请求的全流程,剖析下底层的网络通信是如何实现的、Reactor在kafka上的应用。

再说说社区为何在2.3版本将请求类型划分成两大类,又是如何实现两类请求处理的优先级。
#叨叨
不过在进入今天主题之前我想先叨叨几句,就源码这个事儿,不同人有不同的看法。

有些人听到源码这两个词就被吓到了,这么多代码怎么看。奔进去就像无头苍蝇,一路断点跟下来,跳来跳去,算了拜拜了您嘞。

而有些人觉得源码有啥用,看了和没看一样,看了也用不上。

其实上面两种想法我都有过,哈哈哈。那为什么我会开始看Kafka源码呢?

其实就是我有个同事在自学go,然后想用go写个消息队列,在画架构图的时候就来问我,这消息队列好像有点东西啊,消息收发,元数据管理,消息如何持久一堆问题过来,我直呼顶不住。

这市面上Kafka、RocketMQ都是现成的方案,于是乎我就看起了源码。

所以促使我看源码的初始动力,竟然是为了在同事前面装逼!!

我是先看了RocketMQ,因为毕竟是Java写的,而Kafka Broker都是scala写的。

梳理了一波RocketMQ之后,我又想看看Kafka是怎么做的,于是乎我又看起了Kafka。

在源码分析之前我先总结性的说了说Kafka底层的通信模型。应对面试官询问Kafka请求全过程已经够了。

Reactor模式

在扯到Kafka之前我们先来说说Reactor模式,基本上只要是底层的高性能网络通信就离不开Reactor模式。像Netty、Redis都是使用Reactor模式。

像我们以前刚学网络编程的时候以下代码可是非常的熟悉,新来一个请求,要么在当前线程直接处理了,要么新起一个线程处理。

在早期这样的编程是没问题的,但是随着互联网的快速发展,单线程处理不过来,也不能充分的利用计算机资源。

而每个请求都新起一个线程去处理,资源的要求就太高了,并且创建线程也是一个重操作。

说到这有人想到了,那搞个线程池不就完事了嘛,还要啥Reactor。

池化技术确实能缓解资源的问题,但是池子是有限的,池子里的一个线程不还是得候着某个连接,等待指示嘛。现在的互联网时代早已突破C10K了。

因此引入的IO多路复用,由一个线程来监视一堆连接,同步等待一个或多个IO事件的到来,然后将事件分发给对应的Handler处理,这就叫Reactor模式。

网络通信模型的发展如下

单线程 => 多线程 => 线程池 => Reactor模型

Kafka所采用的Reactor模型如下

图来自Doug Lea大神的 Scalable IO in Java

Kafka Broker 网络通信模型

简单来说就是,Broker 中有个Acceptor(mainReactor)监听新连接的到来,与新连接建连之后轮询选择一个Processor(subReactor)管理这个连接。

而Processor会监听其管理的连接,当事件到达之后,读取封装成Request,并将Request放入共享请求队列中。

然后IO线程池不断的从该队列中取出请求,执行真正的处理。处理完之后将响应发送到对应的Processor的响应队列中,然后由Processor将Response返还给客户端。

每个listener只有一个Acceptor线程,因为它只是作为新连接建连再分发,没有过多的逻辑,很轻量,一个足矣。

Processor 在Kafka中称之为网络线程,默认网络线程池有3个线程,对应的参数是num.network.threads。并且可以根据实际的业务动态增减。

还有个 IO 线程池,即KafkaRequestHandlerPool,执行真正的处理,对应的参数是num.io.threads,默认值是 8。IO线程处理完之后会将Response放入对应的Processor中,由Processor将响应返还给客户端。

可以看到网络线程和IO线程之间利用的经典的生产者 - 消费者模式,不论是用于处理Request的共享请求队列,还是IO处理完返回的Response。

这样的好处是什么?生产者和消费者之间解耦了,可以对生产者或者消费者做独立的变更和扩展。并且可以平衡两者的处理能力,例如消费不过来了,我多加些IO线程。

如果你看过其他中间件源码,你会发现生产者-消费者模式真的是太常见了,所以面试题经常会有手写一波生产者-消费者。

源码级别剖析网络通信模型

Kafka 网络通信组件主要由两大部分构成:SocketServer 和 KafkaRequestHandlerPool。

##SocketServer

可以看出SocketServer旗下管理着,Acceptor 线程、Processor 线程和 RequestChannel等对象。
data-plane和control-plane稍后再做分析,先看看RequestChannel是什么。

RequestChannel

关键的属性和方法都已经在下面代码中注释了,可以看出这个对象主要就是管理Processor和作为传输Request和Response的中转站。
##Acceptor
接下来我们再看看Acceptor

可以看到它继承了AbstractServerThread,接下来再看看它run些啥

再来看看accept(key) 做了啥

很简单,标准selector的处理,获取准备就绪事件,调用serverSocketChannel.accept()得到socketChannel,将socketChannel交给通过轮询选择出来的Processor,之后由它来处理IO事件。
##Processor
接下来我们再看看Processor,相对而言比Acceptor 复杂一些。

先来看看三个关键的成员

再来看看主要的处理逻辑。

可以看到Processor主要是将底层读事件IO数据封装成Request存入队列中,然后将IO线程塞入的Response,返还给客户端,并处理Response 的回调逻辑。

#KafkaRequestHandlerPool

IO线程池,实际处理请求的线程。

再来看看IO线程都干了些啥

很简单,核心就是不断的从requestChannel拿请求,然后调用handle处理请求。

handle方法是位于KafkaApis类中,可以理解为通过switch,根据请求头里面不同的apikey调用不同的handle来处理请求。

我们再举例看下较为简单的处理LIST_OFFSETS的过程,即handleListOffsetRequest,来完成一个请求的闭环。

我用红色箭头标示了调用链。表明处理完请求之后是塞给对应的Processor的。

最后再来个更详细的总览图,把源码分析到的类基本上都对应的加上去了。

#请求处理优先级
上面提到的data-plane和control-plane是时候揭开面纱了。这两个对应的就是数据类请求和控制类请求。

为什么需要分两类请求呢?直接在请求里面用key标明请求是要读写数据啊还是更新元数据不就行了吗?

简单点的说比如我们想删除某个topic,我们肯定是想这个topic马上被删除的,而此时producer还一直往这个topic写数据,那这个情况可能是我们的删除请求排在第N个…等前面的写入请求处理好了才轮到删除的请求。实际上前面哪些往这个topic写入的请求都是没用的,平白的消耗资源。

再或者说进行Preferred Leader选举时候,producer将ack设置为all时候,老leader还在等着follower写完数据向他报告呢,谁知follower已经成为了新leader,而通知它leader已经变更的请求由于被一堆数据类型请求堵着呢,老leader就傻傻的在等着,直到超时。

就是为了解决这种情况,社区将请求分为两类。

那如何让控制类的请求优先被处理?优先队列?

社区采取的是两套Listener,即数据类型一个listener,控制类一个listener。

对应的就是我们上面讲的网络通信模型,在kafka中有两套! kafka通过两套监听变相的实现了请求优先级,毕竟数据类型请求肯定很多,控制类肯定少,这样看来控制类肯定比大部分数据类型先被处理!

迂回战术啊。

控制类的和数据类区别就在于,就一个Porcessor线程,并且请求队列写死的长度为20。

最后

看源码主要就是得耐心,耐心跟下去。然后再跳出来看。你会发现不过如此,哈哈哈。

我是yes,一个在互联网摸爬滚打且莫得感情的工具人。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

2 个 Flask练手项目,毕设、私活不用愁

发表于 2020-07-07

一个知识点听别人讲能吸收20%, 跟着别人做能吸收60%, 教别人做大概能吸收80%。

这个数字是我编的,但道理大概是这么回事。当你能教别人怎么做的时候,其实你已经是大神了。

很多初学者都会遇到入门后不知道下一步干什么的问题,这就给你们准备3个Flask相关的练手项目,学完之后,可直接应用在真实项目中,接私活、做毕业设计都不是问题。

这些项目是从0到1手把手教你搭建完整项目,里面还涉及到一些最佳实践经验。

1、Flask 超级教程

《The Flask Mega-Tutorial》 是狗书作者写的Flask教程,从0到1手把手教你完成搭建一个生产级别的项目。即时你是小白也可以完全毫无压力的跟着做。

传送门:blog.miguelgrinberg.com/post/the-fl…

中文版PDF下载链接: pan.baidu.com/s/1z7sUMtgd… 提取码: 48ef

2、Flask之旅

《Flask 之旅》 是一个旨在帮助你理解Flask项目最佳实践是什么样,应该遵循什么规则。

因为Flask非常灵活,以至于一百个项目可能有一百种风格,如果大家都能按照约定的方式来组织代码时,项目协助将变得简单很多。

传送门:exploreflask.com/en/latest/

中文版PDF下载地址:pan.baidu.com/s/1z7sUMtgd… 提取码: 48ef

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Android属性动画,看完这篇够用了吧

发表于 2020-07-06

随着APP的开发周期演进,APP不再满足基础的功能保障,需要有较好视觉体验和交互操作。那么动画效果是必不可少的,动画有帧动画,补间动画,属性动画等等。

本文通过一些简单常见的动画效果,和大家重温属性动画的相关知识点。旨在通过全文,全面掌握属性动画~如果看完本文,还需要查阅其他文章,说明本文总结得还不够好,欢迎留言补充。

续集1:Android矢量图动画:每人送一辆掘金牌小黄车

续集2:Android过渡动画,让APP更富有生机

一、属性动画概览

顾名思义,通过控制对象的属性,来实现动画效果。官方定义:定义一个随着时间 (注:停个顿)更改任何对象属性的动画,无论其是否绘制到屏幕上。

可以控制对象什么属性呢?什么属性都可以,理论是通过set和get某个属性来达到动画效果。例如常用下面一些属性来实现View对象的一些动画效果。

  • 位移:translationX、translationY、translationZ
  • 透明度:alpha,透明度全透明到不透明:0f->1f
  • 旋转:rotation,旋转一圈:0f->360f
  • 缩放:水平缩放scaleX,垂直缩放scaleY

简单的效果图:

二、基本使用

简单介绍View对象几个属性动画的使用。

1、位移属性动画

效果图:

先看一下布局代码的实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
复制代码<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/colorPrimary">

<LinearLayout
android:id="@+id/llAddAccount"
android:layout_width="wrap_content"
android:layout_height="35dp"
android:layout_alignParentRight="true"
android:layout_marginTop="100dp"
android:layout_marginRight="-70dp"//将现有视图藏在屏幕的右边
android:background="@drawable/bg_10_10_fff">

<ImageView
android:id="@+id/ivMakeNote"
android:layout_width="35dp"
android:layout_height="30dp"
android:layout_gravity="center_vertical"
android:paddingLeft="2dp"
android:paddingTop="2dp"
android:paddingBottom="2dp"
android:src="@mipmap/ic_account_add" />

<TextView
android:id="@+id/tvAddAccount"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:gravity="center_vertical"
android:paddingRight="12dp"
android:text="添加账户"
android:textColor="@color/colorPrimary"
android:textSize="14sp" />
</LinearLayout>
</RelativeLayout>

上面只是简单实现了布局,下面看看属性动画代码的实现:

1
2
3
4
复制代码llAddAccount.setOnClickListener {
val objectAnimation =ObjectAnimator.ofFloat(llAddAccount, "translationX", 0f, -70f)
objectAnimation.start()
}

到这里,我们才真正看到属性动画的影子。通过ObjectAnimator的工厂方法ofFloat我们得到一个ObjectAnimator对象,并通过该对象的start()方法,开启动画效果。

ofFloat()方法的第一个参数为要实现动画效果的View,例如这里整体效果的LinearLayout;第二个参数为属性名,也就是前面所说的:translationX,translationY,alpha,rotation,scaleX,scaleY等,这里要实现的是水平平移效果,所以我们采用了translationX;第三参数为可变长参数,第一个值为动画开始的位置,第二个值为结束值得位置,如果数组大于3位数,那么前者将是后者的起始位置。

注意事项:如果可变长参数只有一个值,那么ObjectAnimator的工厂方法会将值作为动画结束值,此时属性必须拥有初始化值和getXXX方法。

translationX和translationY这里涉及到的位移都是相对自身位置而言。例如 View在点A(x,y)要移动到点B(x1,y1),那么ofFloat()方法的可变长参数,第一个值应该0f,第二个值应该x1-x。

XML布局实现:

在res/animator文件夹下,创建animator_translation.xml文件,内容如下:

1
2
3
4
5
6
复制代码<objectAnimator xmlns:android="http://schemas.android.com/apk/res/android"
android:propertyName="translationX"
android:valueFrom="0dp"
android:valueTo="-70dp"
android:valueType="floatType"
/>

在代码上调用:

1
2
3
4
5
复制代码llAddAccount.setOnClickListener {
val objectAnimation =AnimatorInflater.loadAnimator(this,R.animator.animator_translation)
objectAnimation.setTarget(llAddAccount)
objectAnimation.start()
}

2、透明属性动画

透明度属性动画比较简单,即控制View的可见度实现视觉差动画效果。这里展示效果是从不透明到透明,再到不透明。

代码如下:

1
2
3
4
5
复制代码tvText.setOnClickListener {
val objectAnimation =ObjectAnimator.ofFloat(tvText, "alpha", 1f,0f,1f)
objectAnimation.duration=3000
objectAnimation.start()
}

ofFloat()方法将属性名换成了透明度alpha,并且可变长参数增加到了3个。给ObjectAnimator对象的duration属性设置了动画展示时间3秒,默认情况下300毫秒。

3、缩放属性动画

缩放可以通过控制scaleX和scaleY分别在X轴和Y轴上进行缩放,如下图在X轴中进行两次两倍缩放。

代码如下:

1
2
3
4
5
6
7
复制代码tvText.setOnClickListener {
val objectAnimation =ObjectAnimator.ofFloat(tvText, "scaleX", 1f,2f)
objectAnimation.duration=3000
objectAnimation.repeatCount=2
objectAnimation.repeatMode=ValueAnimator.REVERSE
objectAnimation.start()
}

ofFloat()方法传入参数属性为scaleX和scaleY时,动态参数表示缩放的倍数。设置ObjectAnimator对象的repeatCount属性来控制动画执行的次数,设置为ValueAnimator.INFINITE表示无限循环播放动画;通过repeatMode属性设置动画重复执行的效果,取值为:ValueAnimator.RESTART和ValueAnimator.REVERSE。

ValueAnimator.RESTART效果:(即每次都重头开始)

ValueAnimator.REVERSE效果:(即和上一次效果反着来)

4、旋转属性动画

旋转动画也比较简单,将一个View进行顺时针或逆时针旋转。

代码如下:

1
2
3
4
5
6
复制代码tvText.setOnClickListener {
val objectAnimation =
ObjectAnimator.ofFloat(tvText, "rotation", 0f,180f,0f)
objectAnimation.duration=3000
objectAnimation.start()
}

ofFloat()方法的可变长参数,如果后者的值大于前者,那么顺时针旋转,小于前者,则逆时针旋转。

三、AnimatorSet

如果想要一个动画结束后播放另外一个动画,或者同时播放,可以通过AnimatorSet来编排。

1
2
3
4
5
6
7
8
9
10
11
复制代码val aAnimator=ObjectAnimator.ofInt(1)
val bAnimator=ObjectAnimator.ofInt(1)
val cAnimator=ObjectAnimator.ofInt(1)
val dAnimator=ObjectAnimator.ofInt(1)

AnimatorSet().apply {
play(aAnimator).before(bAnimator)//a 在b之前播放
play(bAnimator).with(cAnimator)//b和c同时播放动画效果
play(dAnimator).after(cAnimator)//d 在c播放结束之后播放
start()
}

或者

1
2
3
4
5
6
7
8
9
复制代码AnimatorSet().apply {
playSequentially(aAnimator,bAnimator,cAnimator,dAnimator) //顺序播放
start()
}

AnimatorSet().apply {
playTogether(animator,bAnimator,cAnimator,dAnimator) //同时播放
start()
}

另有:

1
2
3
4
复制代码AnimatorSet ().apply {
play(aAnimator).after(1000) //1秒后播放a动画
start()
}

四、ViewPropertyAnimator

如果只是针对View对象的特定属性同时播放动画,我们也可以采用ViewPropertyAnimator。

例如:

1
复制代码 tvText.animate().translationX(100f).translationY(100f).start()

支持属性:

  • translationX、translationY、translationZ
  • x、y、z
  • alpha
  • scaleX、scaleY

注意到ViewPropertyAnimator对象具有property(Float)和propertyBy(Float)方法,其中property(Float)是指属性变化多少(可以理解一次有效),而propertyBy(Float)每次变化多少(可以理解多次有效)。

举例说明:

translationX

1
2
3
4
5
6
复制代码tvText.setOnClickListener {
val animator = tvText.animate()
animator.duration=1000
animator.translationX(100f)//点击一次会向右偏移,再点击没效果
animator.start()
}

translationXBy

1
2
3
4
5
6
复制代码tvText.setOnClickListener {
val animator = tvText.animate()
animator.duration=1000
animator.translationXBy(100f)//每次点击都会向右偏移
animator.start()
}

五、ValueAnimator与ObjectAnimator

ValueAnimator作为ObjectAnimator的父类,主要动态计算目标对象属性的值,然后设置给对象属性,达到动画效果,而ObjectAnimator则在ValueAnimator的基础上极大地简化对目标对象的属性值的计算和添加效果,融合了 ValueAnimator 的计时引擎和值计算以及为目标对象的命名属性添加动画效果这一功能。

举个栗子,通过ValueAnimator的工厂方法ofFloat、ofInt、ofArgb、ofObject来实现动画效果:

代码如下:

1
2
3
4
5
6
7
8
9
10
复制代码    //ValueAnimator实现
tvText.setOnClickListener {
val valueAnimator = ValueAnimator.ofFloat(0f, 180f)
valueAnimator.addUpdateListener {
tvText.rotationY = it.animatedValue as Float //手动赋值
}
valueAnimator.start()
}
//ObjectAnimator实现
ObjectAnimator.ofFloat(tvText, "rotationY", 0f, 180f).apply { start() }

从上面代码可以看出,使用ValueAnimator实现动画,需要手动赋值给目标对象tvText的rotationY,而ObjectAnimator则是自动赋值,不需要手动赋值就可以达到效果。

动画过程可以通过AnimatorUpdateListener和AnimatorListener来监听。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
复制代码 ObjectAnimator.ofFloat(tvText, "translationX", 0f, 780f, 0f).apply {
duration=3000//完成动画所需要时间
repeatCount=ValueAnimator.INFINITE //重复次数:无限循环
repeatMode=ValueAnimator.RESTART //重复模式:重头开始
addUpdateListener { //监听值变化
tvText.translationX= it.animatedValue as Float
}
addListener(object:Animator.AnimatorListener{
override fun onAnimationRepeat(animation: Animator?) {
//动画重复
}

override fun onAnimationEnd(animation: Animator?) {
//动画结束
}

override fun onAnimationCancel(animation: Animator?) {
//动画取消
}

override fun onAnimationStart(animation: Animator?) {
//动画开始
}
})
}

动画可调用start()方法开始,也可调用cancel()方法取消。

那么,要正确使属性动画实现动画效果,那么目标对象应该注意什么?

  • 属性必须具有 set<PropertyName>() 形式的 setter 函数(采用驼峰式大小写形式),例如,如果属性名称为 text,则需要使用 setText() 方法。
  • 如果ObjectAnimator的一个工厂方法中仅为 values... 参数指定了一个值,那么该参数需要提供初始值和getPropertyName()方法。
  • 属性的初始值和结束值之间必须保持类型相同。
  • 可能需要在UpdateListener对象中调用invalidate() 方法,来刷新属性作用后的效果。

六、XML实现

本文一开始介绍位移属性动画时,有提到通过XML文件来实现动画效果,在这里进一步细讲。

在res/animator文件夹下,创建animator_translation.xml文件。XML文件有四个标签可用,要注意到propertyValuesHolder标签的Android 版本适配。

1
2
3
4
5
6
7
复制代码<?xml version="1.0" encoding="utf-8"?>
<set> =>AnimatorSet
<animator/> =>ValueAnimator
<objectAnimator> =>ObjectAnimator
<propertyValuesHolder/> =>PropertyValuesHolder
</objectAnimator>
</set>

set标签对应代码的AnimatorSet,只有一个属性可以设置:android:ordering,取值:同时播放together、顺序播放sequentially。

animator标签对应代码的ValueAnimator,可以设置如下属性:

  • android:duration:动画时长
  • android:valueType:属性类型,intType、floatType、colorType、pathType
  • android:valueFrom:属性初始值
  • android:valueTo:属性结束值
  • android:repeatCount:重复次数
  • android:repeatMode:重复模式
  • android:interpolator:插值器,可看下一节默认插值器。
  • android:startOffset:延迟,对应startOffset()延迟多少毫秒执行

示例:

1
2
3
4
5
6
7
8
9
10
复制代码    <animator
android:duration="1000"
android:valueType="floatType"
android:valueFrom="0f"
android:valueTo="100f"
android:repeatCount="infinite"
android:repeatMode="restart"
android:interpolator="@android:interpolator/linear"
android:startOffset="100"
/>

objectAnimator属性对应代码ObjectAnimator,由于继承自ValueAnimator,所以属性相对多了` android:propertyName。

七、估值器与插值器

看到这里,不知道小伙伴们有没有这个疑问,属性动画是如何计算属性的值的?

这份工作由类型估值器TypeEvaluator与时间插值器TimeInterpolator来完成的。

插值器:根据时间流逝的百分比计算出当前属性值改变的百分比。

估值器:根据当前属性改变的百分比来计算改变后的属性值。

从它两的已定义,也可以看出它们之间的协调关系,先由插值器根据时间流逝的百分比计算出目标对象的属性改变的百分比,再由估值器根据插值器计算出来的属性改变的百分比计算出目标对象属性对应类型的值。

从估值器和插值器可以看出属性动画的工作原理,下面看看官方对工作原理的解析:

更多的原理可以看看链接

估值器

SDK中默认带有的估值器有:
IntEvaluator、FloatEvaluator、ArgbEvaluator,他们分别对应前面我们调用 ValueAnimator对象所有对应的ofInt、ofFloat、ofArgb函数的估值器,分别用在Int类型,Float,颜色值类型之间计算。而ofObject函数则对应我们自定义类型的属性计算。

当估值器的类型不满足需求,就需要自定义类型估值器。例如我们要实现下面效果:

这个效果可以通过AnimatorSet来实现,但我们这里采用自定义TypeEvaluator来实现TextView从屏幕左上角斜线滑到屏幕右下角。

  1. 定义Point类,我们操作的对象。
1
复制代码data class Point(var x: Float, var y: Float)
  1. 定义PointEvaluator估值器,继承自TypeEvaluator,泛型参数为Point类型。通过实现evaluate()方法,可以实现很多复制的动画效果,我们这里实现上面简单算法。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
复制代码class PointEvaluator : TypeEvaluator<Point> {
/**
* 根据插值器计算出当前对象的属性的百分比fraction,估算去属性当前具体的值
* @param fraction 属性改变的百分比
* @param startValue 对象开始的位置,例如这里点坐标开始位置:屏幕左上角位置
* @param endValue 对象结束的位置,例如这里点坐标结束的位置:屏幕右下角位置
*/
override fun evaluate(fraction: Float, startValue: Point?, endValue: Point?): Point {
if (startValue == null || endValue == null) {
return Point(0f, 0f)
}

return Point(
fraction * (endValue.x - startValue.x),
fraction * (endValue.y - startValue.y)
)
}
}
  1. 使用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
复制代码       val animator= ValueAnimator.ofObject(
PointEvaluator(),
Point(0f, 0f),//动画开始属性值
Point(
ScreenUtils.getScreenWidth(this).toFloat(),
ScreenUtils.getScreenHeight(this).toFloat()
)//动画结束值
)

animator.addUpdateListener {//手动更新TextView的x和y 属性
val point = it.animatedValue as Point
tvText.x = point.x
tvText.y = point.y
logError("point:${point}")
}
animator.duration = 5000

btnStart.setOnClickListener {
animator.start()
}

一个简单的自定义估值器就算完成了。数学学的好,任何复杂效果都不是问题。

插值器

TypeEvaluator对象的evaluate()方法的fraction参数就是插值器计算得来,SDK中默认的时间插值器有:

  • LinearInterpolator 线性(匀速)
  • AccelerateInterpolator 持续加速
  • DecelerateInterpolator 持续减速
  • AccelerateDecelerateInterpolator 先加速后减速
  • OvershootInterpolator 结束时回弹一下
  • AnticipateInterpolator 开始回拉一下
  • BounceInterpolator 结束时Q弹一下
  • CycleInterpolator 来回循环

看看效果:

LinearInterpolator

AccelerateInterpolator

DecelerateInterpolator

AccelerateDecelerateInterpolator

OvershootInterpolator

AnticipateInterpolator

BounceInterpolator

CycleInterpolator

正常情况下,默认的插值器已经够用,如果自己数学厉害,想显摆一下,也是通过实现TimeInterpolator接口的getInterpolation()自定义的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
复制代码/**
* A time interpolator defines the rate of change of an animation. This allows animations
* to have non-linear motion, such as acceleration and deceleration.
*/
public interface TimeInterpolator {

/**
* Maps a value representing the elapsed fraction of an animation to a value that represents
* the interpolated fraction. This interpolated value is then multiplied by the change in
* value of an animation to derive the animated value at the current elapsed animation time.
*
* @param input A value between 0 and 1.0 indicating our current point
* in the animation where 0 represents the start and 1.0 represents
* the end
* @return The interpolation value. This value can be more than 1.0 for
* interpolators which overshoot their targets, or less than 0 for
* interpolators that undershoot their targets.
*/
float getInterpolation(float input);
}

八、Keyframe

要控制动画速率的变化,就得去自定义插值器或估值器,对我这种数学渣渣来说,简直比上天一样难的。

所以渣渣们可以考虑用关键帧Keyframe对象来实现。Keyframe让我们可以指定某个属性百分比时对象的属性值。

1
2
3
4
5
6
7
8
9
10
复制代码    tvText.setOnClickListener {
val start = Keyframe.ofFloat(0f, 0f)
val middle = Keyframe.ofFloat(0.3f, 400f)
val end = Keyframe.ofFloat(1f, 700f)
val holder=PropertyValuesHolder.ofKeyframe("translationX",start,middle,end)
ObjectAnimator.ofPropertyValuesHolder(tvText,holder).apply {
duration=2000
start()
}
}

上面代码分别定义了三个关键帧,分别在属性百分比为0f、0.3f、1f对应的translationX的值。

动画效果:

可以看到效果先快后慢。
Keyframe同样支持ofFloat、ofInt、ofObject。使用关键帧,至少需要有两个关键帧,不然坐等奔溃吧。PropertyValuesHolder对象是用来保存动画过程所操作的属性和对应的值。

九、总结

通过ObjectAnimator的工厂方法可以快速实现一个属性动画,但默认的属性动画不满足自己的需求是,可以通过ValueAnimator对象来定义自己的属性,注意属性的要求。可以通过AnimatorSet来实现属性组合播放效果。

动画的原理是通过时间插值器与类型估值器配置使用,控制对象的属性来实现动画效果。

续集:Android矢量图动画:每人送一辆掘金牌小黄车

我的GitHub

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

SpringSecurity+JWT认证流程解析 掘金新

发表于 2020-07-06

纸上得来终觉浅,觉知此事要躬行。

楔子

本文适合: 对Spring Security有一点了解或者跑过简单demo但是对整体运行流程不明白的同学,对SpringSecurity有兴趣的也可以当作你们的入门教程,示例代码中也有很多注释。

本文代码: 码云地址 GitHub地址

大家在做系统的时候,一般做的第一个模块就是认证与授权模块,因为这是一个系统的入口,也是一个系统最重要最基础的一环,在认证与授权服务设计搭建好了之后,剩下的模块才得以安全访问。

市面上一般做认证授权的框架就是shiro和Spring Security,也有大部分公司选择自己研制。出于之前看过很多Spring Security的入门教程,但都觉得讲的不是太好,所以我这两天在自己鼓捣Spring Security的时候萌生了分享一下的想法,希望可以帮助到有兴趣的人。

Spring Security框架我们主要用它就是解决一个认证授权功能,所以我的文章主要会分为两部分:

  • 第一部分认证(本篇)
  • 第二部分授权(放在下一篇)

我会为大家用一个Spring Security + JWT + 缓存的一个demo来展现我要讲的东西,毕竟脑子的东西要体现在具体事物上才可以更直观的让大家去了解去认识。

学习一件新事物的时候,我推荐使用自顶向下的学习方法,这样可以更好的认识新事物,而不是盲人摸象。

注:只涉及到用户认证授权不涉及oauth2之类的第三方授权。

  1. 📖SpringSecurity的工作流程

想上手 Spring Security 一定要先了解它的工作流程,因为它不像工具包一样,拿来即用,必须要对它有一定的了解,再根据它的用法进行自定义操作。

我们可以先来看看它的工作流程:

在Spring Security的官方文档上有这么一句话:

Spring Security’s web infrastructure is based entirely on standard servlet filters.

Spring Security 的web基础是Filters。

这句话展示了Spring Security的设计思想:即通过一层层的Filters来对web请求做处理。

放到真实的Spring Security中,用文字表述的话可以这样说:

一个web请求会经过一条过滤器链,在经过过滤器链的过程中会完成认证与授权,如果中间发现这条请求未认证或者未授权,会根据被保护API的权限去抛出异常,然后由异常处理器去处理这些异常。

用图片表述的话可以这样画,这是我在百度找到的一张图片:

image.png

如上图,一个请求想要访问到API就会以从左到右的形式经过蓝线框框里面的过滤器,其中绿色部分是我们本篇主要讲的负责认证的过滤器,蓝色部分负责异常处理,橙色部分则是负责授权。

图中的这两个绿色过滤器我们今天不会去说,因为这是Spring Security对form表单认证和Basic认证内置的两个Filter,而我们的demo是JWT认证方式所以用不上。

如果你用过Spring Security就应该知道配置中有两个叫formLogin和httpBasic的配置项,在配置中打开了它俩就对应着打开了上面的过滤器。

image.png

  • formLogin对应着你form表单认证方式,即UsernamePasswordAuthenticationFilter。
  • httpBasic对应着Basic认证方式,即BasicAuthenticationFilter。

换言之,你配置了这两种认证方式,过滤器链中才会加入它们,否则它们是不会被加到过滤器链中去的。

因为Spring Security自带的过滤器中是没有针对JWT这种认证方式的,所以我们的demo中会写一个JWT的认证过滤器,然后放在绿色的位置进行认证工作。

  1. 📝SpringSecurity的重要概念

知道了Spring Security的大致工作流程之后,我们还需要知道一些非常重要的概念也可以说是组件:

  • SecurityContext:上下文对象,Authentication对象会放在里面。
  • SecurityContextHolder:用于拿到上下文对象的静态工具类。
  • Authentication:认证接口,定义了认证对象的数据形式。
  • AuthenticationManager:用于校验Authentication,返回一个认证完成后的Authentication对象。

1.SecurityContext

上下文对象,认证后的数据就放在这里面,接口定义如下:

1
2
3
4
5
6
7
复制代码public interface SecurityContext extends Serializable {
// 获取Authentication对象
Authentication getAuthentication();

// 放入Authentication对象
void setAuthentication(Authentication authentication);
}

这个接口里面只有两个方法,其主要作用就是get or set Authentication。

2. SecurityContextHolder

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
复制代码public class SecurityContextHolder {

public static void clearContext() {
strategy.clearContext();
}

public static SecurityContext getContext() {
return strategy.getContext();
}

public static void setContext(SecurityContext context) {
strategy.setContext(context);
}

}

可以说是SecurityContext的工具类,用于get or set or clear SecurityContext,默认会把数据都存储到当前线程中。

3. Authentication

1
2
3
4
5
6
7
8
9
复制代码public interface Authentication extends Principal, Serializable {

Collection<? extends GrantedAuthority> getAuthorities();
Object getCredentials();
Object getDetails();
Object getPrincipal();
boolean isAuthenticated();
void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException;
}

这几个方法效果如下:

  • getAuthorities: 获取用户权限,一般情况下获取到的是用户的角色信息。
  • getCredentials: 获取证明用户认证的信息,通常情况下获取到的是密码等信息。
  • getDetails: 获取用户的额外信息,(这部分信息可以是我们的用户表中的信息)。
  • getPrincipal: 获取用户身份信息,在未认证的情况下获取到的是用户名,在已认证的情况下获取到的是 UserDetails。
  • isAuthenticated: 获取当前 Authentication 是否已认证。
  • setAuthenticated: 设置当前 Authentication 是否已认证(true or false)。

Authentication只是定义了一种在SpringSecurity进行认证过的数据的数据形式应该是怎么样的,要有权限,要有密码,要有身份信息,要有额外信息。

4. AuthenticationManager

1
2
3
4
5
复制代码public interface AuthenticationManager {
// 认证方法
Authentication authenticate(Authentication authentication)
throws AuthenticationException;
}

AuthenticationManager定义了一个认证方法,它将一个未认证的Authentication传入,返回一个已认证的Authentication,默认使用的实现类为:ProviderManager。

接下来大家可以构思一下如何将这四个部分,串联起来,构成Spring Security进行认证的流程:

  1. 👉先是一个请求带着身份信息进来

  2. 👉经过AuthenticationManager的认证,

  3. 👉再通过SecurityContextHolder获取SecurityContext,

  4. 👉最后将认证后的信息放入到SecurityContext。

  1. 📃代码前的准备工作

真正开始讲诉我们的认证代码之前,我们首先需要导入必要的依赖,数据库相关的依赖可以自行选择什么JDBC框架,我这里用的是国人二次开发的myabtis-plus。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
复制代码    <dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>

<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.0</version>
</dependency>

<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.3.0</version>
</dependency>

<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.47</version>
</dependency>

接着,我们需要定义几个必须的组件。

由于我用的Spring-Boot是2.X所以必须要我们自己定义一个加密器:

1. 定义加密器Bean

1
2
3
4
复制代码 @Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}

这个Bean是不必可少的,Spring Security在认证操作时会使用我们定义的这个加密器,如果没有则会出现异常。

2. 定义AuthenticationManager

1
2
3
4
复制代码@Bean
public AuthenticationManager authenticationManager() throws Exception {
return super.authenticationManager();
}

这里将Spring Security自带的authenticationManager声明成Bean,声明它的作用是用它帮我们进行认证操作,调用这个Bean的authenticate方法会由Spring Security自动帮我们做认证。

3. 实现UserDetailsService

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
复制代码public class CustomUserDetailsService implements UserDetailsService {
@Autowired
private UserService userService;
@Autowired
private RoleInfoService roleInfoService;
@Override
public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
log.debug("开始登陆验证,用户名为: {}",s);

// 根据用户名验证用户
QueryWrapper<UserInfo> queryWrapper = new QueryWrapper<>();
queryWrapper.lambda().eq(UserInfo::getLoginAccount,s);
UserInfo userInfo = userService.getOne(queryWrapper);
if (userInfo == null) {
throw new UsernameNotFoundException("用户名不存在,登陆失败。");
}

// 构建UserDetail对象
UserDetail userDetail = new UserDetail();
userDetail.setUserInfo(userInfo);
List<RoleInfo> roleInfoList = roleInfoService.listRoleByUserId(userInfo.getUserId());
userDetail.setRoleInfoList(roleInfoList);
return userDetail;
}
}

实现UserDetailsService的抽象方法并返回一个UserDetails对象,认证过程中SpringSecurity会调用这个方法访问数据库进行对用户的搜索,逻辑什么都可以自定义,无论是从数据库中还是从缓存中,但是我们需要将我们查询出来的用户信息和权限信息组装成一个UserDetails返回。

UserDetails 也是一个定义了数据形式的接口,用于保存我们从数据库中查出来的数据,其功能主要是验证账号状态和获取权限,具体实现可以查阅我仓库的代码。

4. TokenUtil

由于我们是JWT的认证模式,所以我们也需要一个帮我们操作Token的工具类,一般来说它具有以下三个方法就够了:

  • 创建token
  • 验证token
  • 反解析token中的信息

在下文我的代码里面,JwtProvider充当了Token工具类的角色,具体实现可以查阅我仓库的代码。

  1. ✍代码中的具体实现

有了前面的讲解之后,大家应该都知道用SpringSecurity做JWT认证需要我们自己写一个过滤器来做JWT的校验,然后将这个过滤器放到绿色部分。

在我们编写这个过滤器之前,我们还需要进行一个认证操作,因为我们要先访问认证接口拿到token,才能把token放到请求头上,进行接下来请求。

如果你不太明白,不要紧,先接着往下看我会在这节结束再次梳理一下。

1. 认证方法

访问一个系统,一般最先访问的是认证方法,这里我写了最简略的认证需要的几个步骤,因为实际系统中我们还要写登录记录啊,前台密码解密啊这些操作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
复制代码@Override
public ApiResult login(String loginAccount, String password) {
// 1 创建UsernamePasswordAuthenticationToken
UsernamePasswordAuthenticationToken usernameAuthentication = new UsernamePasswordAuthenticationToken(loginAccount, password);
// 2 认证
Authentication authentication = this.authenticationManager.authenticate(usernameAuthentication);
// 3 保存认证信息
SecurityContextHolder.getContext().setAuthentication(authentication);
// 4 生成自定义token
UserDetail userDetail = (UserDetail) authentication.getPrincipal();
AccessToken accessToken = jwtProvider.createToken((UserDetails) authentication.getPrincipal());

// 5 放入缓存
caffeineCache.put(CacheName.USER, userDetail.getUsername(), userDetail);
return ApiResult.ok(accessToken);
}

这里一共五个步骤,大概只有前四步是比较陌生的:

  1. 传入用户名和密码创建了一个UsernamePasswordAuthenticationToken对象,这是我们前面说过的Authentication的实现类,传入用户名和密码做构造参数,这个对象就是我们创建出来的未认证的Authentication对象。
  2. 使用我们先前已经声明过的Bean-authenticationManager调用它的authenticate方法进行认证,返回一个认证完成的Authentication对象。
  3. 认证完成没有出现异常,就会走到第三步,使用SecurityContextHolder获取SecurityContext之后,将认证完成之后的Authentication对象,放入上下文对象。
  4. 从Authentication对象中拿到我们的UserDetails对象,之前我们说过,认证后的Authentication对象调用它的getPrincipal()方法就可以拿到我们先前数据库查询后组装出来的UserDetails对象,然后创建token。
  5. 把UserDetails对象放入缓存中,方便后面过滤器使用。

这样的话就算完成了,感觉上很简单,因为主要认证操作都会由authenticationManager.authenticate()帮我们完成。


接下来我们可以看看源码,从中窥得Spring Security是如何帮我们做这个认证的(省略了一部分):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
复制代码// AbstractUserDetailsAuthenticationProvider

public Authentication authenticate(Authentication authentication){

// 校验未认证的Authentication对象里面有没有用户名
String username = (authentication.getPrincipal() == null) ? "NONE_PROVIDED"
: authentication.getName();

boolean cacheWasUsed = true;
// 从缓存中去查用户名为XXX的对象
UserDetails user = this.userCache.getUserFromCache(username);

// 如果没有就进入到这个方法
if (user == null) {
cacheWasUsed = false;

try {
// 调用我们重写UserDetailsService的loadUserByUsername方法
// 拿到我们自己组装好的UserDetails对象
user = retrieveUser(username,
(UsernamePasswordAuthenticationToken) authentication);
}
catch (UsernameNotFoundException notFound) {
logger.debug("User '" + username + "' not found");

if (hideUserNotFoundExceptions) {
throw new BadCredentialsException(messages.getMessage(
"AbstractUserDetailsAuthenticationProvider.badCredentials",
"Bad credentials"));
}
else {
throw notFound;
}
}

Assert.notNull(user,
"retrieveUser returned null - a violation of the interface contract");
}

try {
// 校验账号是否禁用
preAuthenticationChecks.check(user);
// 校验数据库查出来的密码,和我们传入的密码是否一致
additionalAuthenticationChecks(user,
(UsernamePasswordAuthenticationToken) authentication);
}


}

看了源码之后你会发现和我们平常写的一样,其主要逻辑也是查数据库然后对比密码。

登录之后效果如下:

image.png

我们返回token之后,下次请求其他API的时候就要在请求头中带上这个token,都按照JWT的标准来做就可以。

2. JWT过滤器

有了token之后,我们要把过滤器放在过滤器链中,用于解析token,因为我们没有session,所以我们每次去辨别这是哪个用户的请求的时候,都是根据请求中的token来解析出来当前是哪个用户。

所以我们需要一个过滤器去拦截所有请求,前文我们也说过,这个过滤器我们会放在绿色部分用来替代UsernamePasswordAuthenticationFilter,所以我们新建一个JwtAuthenticationTokenFilter,然后将它注册为Bean,并在编写配置文件的时候需要加上这个:

1
2
3
4
5
6
7
8
9
10
复制代码@Bean
public JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter() {
return new JwtAuthenticationTokenFilter();
}

@Override
protected void configure(HttpSecurity http) throws Exception {
http.addFilterBefore(jwtAuthenticationTokenFilter(),
UsernamePasswordAuthenticationFilter.class);
}

addFilterBefore的语义是添加一个Filter到XXXFilter之前,放在这里就是把JwtAuthenticationTokenFilter放在UsernamePasswordAuthenticationFilter之前,因为filter的执行也是有顺序的,我们必须要把我们的filter放在过滤器链中绿色的部分才会起到自动认证的效果。

接下来我们可以看看JwtAuthenticationTokenFilter的具体实现了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
复制代码@Override
protected void doFilterInternal(@NotNull HttpServletRequest request,
@NotNull HttpServletResponse response,
@NotNull FilterChain chain) throws ServletException, IOException {
log.info("JWT过滤器通过校验请求头token进行自动登录...");

// 拿到Authorization请求头内的信息
String authToken = jwtProvider.getToken(request);

// 判断一下内容是否为空且是否为(Bearer )开头
if (StrUtil.isNotEmpty(authToken) && authToken.startsWith(jwtProperties.getTokenPrefix())) {
// 去掉token前缀(Bearer ),拿到真实token
authToken = authToken.substring(jwtProperties.getTokenPrefix().length());

// 拿到token里面的登录账号
String loginAccount = jwtProvider.getSubjectFromToken(authToken);

if (StrUtil.isNotEmpty(loginAccount) && SecurityContextHolder.getContext().getAuthentication() == null) {
// 缓存里查询用户,不存在需要重新登陆。
UserDetail userDetails = caffeineCache.get(CacheName.USER, loginAccount, UserDetail.class);

// 拿到用户信息后验证用户信息与token
if (userDetails != null && jwtProvider.validateToken(authToken, userDetails)) {

// 组装authentication对象,构造参数是Principal Credentials 与 Authorities
// 后面的拦截器里面会用到 grantedAuthorities 方法
UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails, userDetails.getPassword(), userDetails.getAuthorities());

// 将authentication信息放入到上下文对象中
SecurityContextHolder.getContext().setAuthentication(authentication);

log.info("JWT过滤器通过校验请求头token自动登录成功, user : {}", userDetails.getUsername());
}
}
}

chain.doFilter(request, response);
}

代码里步骤虽然说的很详细了,但是可能因为代码过长不利于阅读,我还是简单说说,也可以直接去仓库查看源码:

  1. 拿到Authorization请求头对应的token信息
  2. 去掉token的头部(Bearer )
  3. 解析token,拿到我们放在里面的登陆账号
  4. 因为我们之前登陆过,所以我们直接从缓存里面拿我们的UserDetail信息即可
  5. 查看是否UserDetail为null,以及查看token是否过期,UserDetail用户名与token中的是否一直。
  6. 组装一个authentication对象,把它放在上下文对象中,这样后面的过滤器看到我们上下文对象中有authentication对象,就相当于我们已经认证过了。

这样的话,每一个带有正确token的请求进来之后,都会找到它的账号信息,并放在上下文对象中,我们可以使用SecurityContextHolder很方便的拿到上下文对象中的Authentication对象。

完成之后,启动我们的demo,可以看到过滤器链中有以下过滤器,其中我们自定义的是第5个:

image.png

🐱‍🏍就酱,我们登录完了之后获取到的账号信息与角色信息我们都会放到缓存中,当带着token的请求来到时,我们就把它从缓存中拿出来,再次放到上下文对象中去。

结合认证方法,我们的逻辑链就变成了:

登录👉拿到token👉请求带上token👉JWT过滤器拦截👉校验token👉将从缓存中查出来的对象放到上下文中

这样之后,我们认证的逻辑就算完成了。

  1. 💡代码优化

认证和JWT过滤器完成后,这个JWT的项目其实就可以跑起来了,可以实现我们想要的效果,如果想让程序更健壮,我们还需要再加一些辅助功能,让代码更友好。

1. 认证失败处理器

image.png

当用户未登录或者token解析失败时会触发这个处理器,返回一个非法访问的结果。

image.png

2. 权限不足处理器

image.png

当用户本身权限不满足所访问API需要的权限时,触发这个处理器,返回一个权限不足的结果。

image.png

3. 退出方法

image.png

用户退出一般就是清除掉上下文对象和缓存就行了,你也可以做一下附加操作,这两步是必须的。

4. token刷新

image.png

JWT的项目token刷新也是必不可少的,这里刷新token的主要方法放在了token工具类里面,刷新完了把缓存重载一遍就行了,因为缓存是有有效期的,重新put可以重置失效时间。

后记

这篇文我从上周日就开始构思了,为了能讲的老妪能解,修修改改了几遍才发出来。

Spring Security的上手的确有点难度,在我第一次去了解它的时候看的是尚硅谷的教程,那个视频的讲师拿它和Thymeleaf结合,这就导致网上也有很多博客去讲Spring Security的时候也是这种方式,而没有去关注前后端分离。

也有教程做过滤器的时候是直接继承UsernamePasswordAuthenticationFilter,这样的方法也是可行的,不过我们了解了整体的运行流程之后你就知道没必要这样做,不需要去继承XXX,只要写个过滤器然后放在那个位置就可以了。

好了,认证篇结束后,下篇就是动态鉴权了,这是我在掘金的第一篇文,我的第一次知识输出,希望大家持续关注。

你们的每个点赞收藏与评论都是对我知识输出的莫大肯定,如果有文中有什么错误或者疑点或者对我的指教都可以在评论区下方留言,一起讨论。

我是耳朵,一个一直想做知识输出的人,下期见。

本文代码:码云地址 GitHub地址

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

搞懂Spring事务失效的8大原因,轻轻松松面试过关 1、数

发表于 2020-07-06

前几天发了一篇文章,里面有一个关于事务失效的问题:

用 Spring 的 @大辉哥注解控制事务有哪些不生效的场景?

其中有个热心粉丝留言分享了下,我觉得总结得有点经验,给置顶了:

但是我觉得还是总结得不够全,今天我再总结一下,再延着这位粉丝的总结再补充完善一下,不用说,我肯定也不见得总结全,但希望可以帮忙有需要的人。

1、数据库引擎不支持事务

这里以 MySQL 为例,其 MyISAM 引擎是不支持事务操作的,InnoDB 才是支持事务的引擎,一般要支持事务都会使用 InnoDB。

根据 MySQL 的官方文档:

https://dev.mysql.com/doc/refman/5.5/en/storage-engine-setting.html

从 MySQL 5.5.5 开始的默认存储引擎是:InnoDB,之前默认的都是:MyISAM,所以这点要值得注意,底层引擎不支持事务再怎么搞都是白搭。

2、没有被 Spring 管理

如下面例子所示:

如果此时把 @Service 注解注释掉,这个类就不会被加载成一个 Bean,那这个类就不会被 Spring 管理了,事务自然就失效了。

3、方法不是 public 的

以下来自 Spring 官方文档:

When using proxies, you should apply the @Transactional annotation only to methods with public visibility. If you do annotate protected, private or package-visible methods with the @Transactional annotation, no error is raised, but the annotated method does not exhibit the configured transactional settings. Consider the use of AspectJ (see below) if you need to annotate non-public methods.

大概意思就是 @Transactional 只能用于 public 的方法上,否则事务不会失效,如果要用在非 public 方法上,可以开启 AspectJ 代理模式。

4、自身调用问题

来看两个示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
复制代码//@Service

publicclassOrderServiceImplimplementsOrderService{

@Transactional

publicvoidupdateOrder(Orderorder){

try{

//updateorder

}catch{

thrownewException("更新错误");

}

}

}

update方法上面没有加 @Transactional 注解,调用有 @Transactional 注解的 updateOrder 方法,updateOrder 方法上的事务管用吗?

再来看下面这个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
复制代码@Service
publicclassOrderServiceImplimplementsOrderService{

@Transactional
publicvoidupdate(Orderorder){
updateOrder(order);
}

@Transactional(propagation=Propagation.REQUIRES_NEW)
publicvoidupdateOrder(Orderorder){
//updateorder
}

}

这次在 update 方法上加了 @Transactional,updateOrder 加了 REQUIRES_NEW 新开启一个事务,那么新开的事务管用么?

这两个例子的答案是:不管用!

因为它们发生了自身调用,就调该类自己的方法,而没有经过 Spring 的代理类,默认只有在外部调用事务才会生效,这也是老生常谈的经典问题了。

这个的解决方案之一就是在的类中注入自己,用注入的对象再调用另外一个方法,这个不太优雅,另外一个可行的方案可以参考《Spring 如何在一个事务中开启另一个事务?》这篇文章。

5、数据源没有配置事务管理器

1
2
3
4
复制代码@Bean
publicPlatformTransactionManagertransactionManager(DataSourcedataSource){
returnnewDataSourceTransactionManager(dataSource);
}

如上面所示,当前数据源若没有配置事务管理器,那也是白搭!

6、不支持事务

来看下面这个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
复制代码@Service
publicclassOrderServiceImplimplementsOrderService{

@Transactional
publicvoidupdate(Orderorder){
updateOrder(order);
}

@Transactional(propagation=Propagation.NOT_SUPPORTED)
publicvoidupdateOrder(Orderorder){
//updateorder
}

}

Propagation.NOT_SUPPORTED: 表示不以事务运行,当前若存在事务则挂起

都主动不支持以事务方式运行了,那事务生效也是白搭!

7、异常被吃了

这个也是出现比较多的场景:

1
2
3
4
5
6
7
8
9
10
11
12
13
复制代码//@Service
publicclassOrderServiceImplimplementsOrderService{

@Transactional
publicvoidupdateOrder(Orderorder){
try{
//updateorder
}catch{

}
}

}

把异常吃了,然后又不抛出来,事务怎么回滚吧!

8、异常类型错误

上面的例子再抛出一个异常:

1
2
3
4
5
6
7
8
9
10
11
12
13
复制代码//@Service
publicclassOrderServiceImplimplementsOrderService{

@Transactional
publicvoidupdateOrder(Orderorder){
try{
//updateorder
}catch{
thrownewException("更新错误");
}
}

}

这样事务也是不生效的,因为默认回滚的是:RuntimeException,如果你想触发其他异常的回滚,需要在注解上配置一下,如:

1
复制代码@Transactional(rollbackFor=Exception.class)

这个配置仅限于 Throwable 异常类及其子类。

总结

本文总结了八种事务失效的场景,其实发生最多就是自身调用、异常被吃、异常抛出类型不对这三个了。

也像文章开头说的那样,本文不一定总结得全,只是总结常见的事务失效的场景,即使如此,这 8 点已经足以帮你面试轻轻松松过,如果你还知道其他场景也欢迎留言分享。

喜欢就来个小关注吧,私信小编微即可免费获取相关资料哦

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

全方面分析 Hilt 和 Koin 性能

发表于 2020-07-06

前言

Hilt 系列文章

  • Jetpack 成员 Hilt 实践(一)启程过坑记
  • Jetpack 成员 Hilt 结合 App Startup(二)进阶篇
  • Jetpack 成员 Hilt 与 Dagger 区别 (三) 落地篇

Koin、Dagger、Hilt 目前都是非常流行的库,面对这么多层出不穷的新技术,我们该做如何选择,是一直困扰我们的一个问题,之前我分析过 Koin 和 Dagger 的性能对比,Hilt 与 Dagger 的不同之处,可以点击下方链接前往查看。

  • 放弃 Dagger 拥抱 Koin
  • Jetpack 新成员 Hilt 与 Dagger 大不同(三)落地篇

这是 Hilt 系列的第四篇,主要来分析 Hilt 和 Koin 的性能,如果你之前对 Hilt 和 Koin 不了解也没有关系,对阅读本文没有什么影响,接下来将会从以下几个方面来分析 Hilt 和 Koin 不同之处。

  • 依赖注入的优点?
  • Koin 为什么可以做到无代码生成、无反射?
  • AndroidStudio 支持 Hilt 和 Koin 在关联代码间进行导航吗?
  • Hilt 和 Koin 谁的编译速度更快?以及为什么?
  • 比较 Hilt 和 Koin 代码行数?
  • Hilt 和 Koin 在使用上的区别,谁上手最快?

依赖注入的优点

Koin 是为 Kotlin 开发者提供的一个实用型轻量级依赖注入框架,采用纯 Kotlin 语言编写而成,仅使用功能解析,无代理、无代码生成、无反射。

Hilt 是在 Dagger 基础上进行开发的,减少了在项目中进行手动依赖,Hilt 集成了 Jetpack 库和 Android 框架类,并删除了大部分模板代码,让开发者只需要关注如何进行绑定,同时 Hilt 也继承了 Dagger 优点,编译时正确性、运行时性能、并且得到了 Android Studio 的支持。

Hilt、Dagger、Koin 等等都是依赖注入库,依赖注入是面向对象设计中最好的架构模式之一,使用依赖注入库有以下优点:

  • 依赖注入库会自动释放不再使用的对象,减少资源的过度使用。
  • 在指定范围内,可重用依赖项和创建的实例,提高代码的可重用性,减少了很多模板代码。
  • 代码变得更具可读性。
  • 易于构建对象。
  • 编写低耦合代码,更容易测试。

Hilt VS Koin

接下来将从 AndroidStudio 基础支持、项目结构、代码行数、编译时间、使用上的不同,这几个方面对 Hilt 和 Koin 进行全方面的分析。

Android Studio 强大的基础支持

Android Studio >= 4.1 的版本,在编辑器和代码行号之间,增加了一个新的 “间距图标”,可以在 Dagger 的关联代码间进行导航,包括依赖项的生产者、消费者、组件、子组件以及模块。

Hilt 是在 Dagger 基础上进行开发的,所以 Hilt 自然也拥有了 Dagger 的优点,在 Android Studio >= 4.1 版本上也支持在 Hilt 的关联代码间进行导航,如下图所示。

hilt

PS: 我用的版本是 Android Studio 4.1 Canary 10,命名和图标在不同版本上会有差异。

有了 Android Studio 支持,在 Android 应用中 Dagger 和 Hilt 在关联代码间进行导航是如此简单。

这两个图标的意思如下:

  • 左边(向上箭头)的图标: 提供类型的地方 (即依赖项来自何处)
  • 右边的图标: 类型被当作依赖项使用的地方

遗憾的是 Koin 不支持,其实 Koin 并不需要这个功能,Koin 并不像 Hilt 注入代码那么分散,而且 Koin 注入关系很明确,可以很方便的定位到与它相关联的代码,并且 Koin 提供的 Debug 工具,可以打印出其构建过程,帮助我们分析。

Koin 构建过程

而 Hilt 不一样的是 Hilt 采用注解的方式,在使用 Hilt 的项目中,如果想要弄清楚其依赖项来自 @Inject 修饰的构造器、@Binds 或者 @Provides 修饰的方法?还是限定符?不是一件容易的事,尤其在一个大型复杂的的项目中,想要弄清楚它们之间的依赖关系是非常困难的,而 Android Studio >= 4.1 的版本,增加的 “间距图标”,帮助我们解决了这个问题。

比较 Hilt 和 Koin 项目结构

为了能够正确比较这两种方式,新建了两个项目 Project-Hilt 和 Project-Koin, 分别用 Hilt 和 Koin 去实现,Project-Hilt 和 Project-Koin 两个项目的依赖库版本管理统一用 Composing builds 的方式(关于 Composing builds 请参考这篇文章 再见吧 buildSrc, 拥抱 Composing builds 提升 Android 编译速度),除了它们本身的依赖库,其他的依赖库都是相同的,如下图所示:

项目 Project-Hilt 和 Project-Koin 都分别实现了 Room 和 Retrofit 进行数据库和网络访问,统一在 Repository 里面进行处理,它们的依赖注入都放在了 di 下面,这应该是一个小型 App 基础架构,如下图所示:

如上图所示,这里需要关注 di 包下的类,Project-Hilt 和 Project-Koin 分别注入了 Room、Retrofit 和 Repository,以 Hilt 注入的方式至少需要三个文件才能完成,但是如果使用 Koin 的方式只需要一个文件就可以完成,后面我会进行详细的分析。

比较 Koin 和 Hilt 代码行数

项目 Project-Hilt 和 Project-Koin 除了它们本身的依赖之外,其他的依赖都是相同的。

我使用 Statistic 工具来进行代码行数的统计,反复对比了项目编译前和编译后,它们的结果如下所示:

代码行数 Hilt Koin
编译之前 2414 2414
编译之后 149608 138405

正如你所见 Hilt 生成的代码多于 Koin,随着项目越来越复杂,生成的代码量会越来越多。

比较 Koin 和 Hilt 编译时间

为了保证测试的准确性,每次编译之前我都会先 clean 然后才会 rebuild,反复的进行了三次这样的操作,它们的结果如下所示。

第一次编译结果:

1
2
3
4
5
6
7
yaml复制代码Hilt:
BUILD SUCCESSFUL in 28s
27 actionable tasks: 27 executed

Koin:
BUILD SUCCESSFUL in 17s
27 actionable tasks: 27 executed

第二次编译结果:

1
2
3
4
5
6
7
yaml复制代码Hilt:
BUILD SUCCESSFUL in 22s
27 actionable tasks: 27 executed

Koin:
BUILD SUCCESSFUL in 15s
27 actionable tasks: 27 executed

第三编译结果:

1
2
3
4
5
6
7
yaml复制代码Hilt:
BUILD SUCCESSFUL in 35s
27 actionable tasks: 27 executed

Koin:
BUILD SUCCESSFUL in 18s
27 actionable tasks: 27 executed

每次的编译时间肯定是不一样的,速度取决于你的电脑的环境,不管执行多少次,结果如上所示 Hilt 编译时间总是大于 Koin,这个结果告诉我们,如果在一个非常大型的项目,这个代价是非常昂贵。

为什么 Hilt 编译时间总是大于 Koin

因为在 Koin 中不需要使用注解,也不需要用 kapt,这意味着没有额外的代码生成,所有的代码都是 Kotlin 原始代码,所以说 Hilt 编译时间总是大于 Koin,从这个角度上同时也解释了,为什么会说 Koin 仅使用功能解析,无额外代码生成。

Koin 和 Hilt 使用上的不同

为了节省篇幅,这里只会列出部分代码,具体详细使用参考我之前写的 Hilt 入门三部曲,包含了 Hilt 所有的用法以及实战案例。

  • Jetpack 新成员 Hilt 实践(一)启程过坑记:介绍了 Hilt 的常用注解、以及在实践过程中遇到的一些坑,Hilt 如何与 Android 框架类进行绑定,以及他们的生命周期。
  • Jetpack 新成员 Hilt 实践之 App Startup(二)进阶篇:分析注解的区别、限定符和作用域注解的使用,以及如何在 ViewModel、App Startup、ContentProvider 中使用等等。
  • Jetpack 新成员 Hilt 与 Dagger 大不同(三)落地篇:Hilt 与 Dagger 不同之处,以及在多模块中局限性以及使用。

在项目中使用 Hilt

如果我们需要在项目中使用 Hilt,我们需要添加 Hilt 插件和依赖库,首先在 project 的 build.gradle 添加以下依赖。

1
2
3
4
5
6
7
arduino复制代码buildscript {
...
dependencies {
...
classpath 'com.google.dagger:hilt-android-gradle-plugin:2.28-alpha'
}
}

然后在 App 模块中的 build.gradle 文件中添加以下代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
arduino复制代码...
apply plugin: 'kotlin-kapt'
apply plugin: 'dagger.hilt.android.plugin'

android {
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}

// For Kotlin projects
kotlinOptions {
jvmTarget = JavaVersion.VERSION_1_8.toString()
}
}

dependencies {
implementation "com.google.dagger:hilt-android:2.28-alpha"
kapt "com.google.dagger:hilt-android-compiler:2.28-alpha"
}

注意: 这里有一个坑,对于 Kotlin 项目,需要添加 kotlinOptions,这是 Google 文档 Dependency injection with Hilt 中没有提到的,否则使用 ViewModel 会编译不过。

完成以上步骤就可以在项目中使用 Hilt 了,所有使用 Hilt 的 App 必须包含一个使用 @HiltAndroidApp 注解的 Application,这是依赖注入容器的入口。

1
2
3
4
5
6
7
8
9
10
11
kotlin复制代码@HiltAndroidApp
class HiltApplication : Application() {
/**
* 1. 所有使用 Hilt 的 App 必须包含一个使用 @HiltAndroidApp 注解的 Application
* 2. @HiltAndroidApp 将会触发 Hilt 代码的生成,包括用作应用程序依赖项容器的基类
* 3. 生成的 Hilt 组件依附于 Application 的生命周期,它也是 App 的父组件,提供其他组件访问的依赖
* 4. 在 Application 中设置好 @HiltAndroidApp 之后,就可以使用 Hilt 提供的组件了,
* Hilt 提供的 @AndroidEntryPoint 注解用于提供 Android 类的依赖(Activity、Fragment、View、Service、BroadcastReceiver)等等
* Application 使用 @HiltAndroidApp 注解
*/
}

@HiltAndroidApp 注解将会触发 Hilt 代码的生成,用作应用程序依赖项容器的基类,这下我们就可以在 di 包下注入 Room、Retrofit 和 Repository,其中 Room 和 Retrofit 比较简单,这里我们看一下 如何注入 Repository, Repository 有一个子类 TasksRepository,代码如下所示。

1
2
3
4
kotlin复制代码class TasksRepository @Inject constructor(
private val localDataSource: DataSource,
private val remoteDataSource: DataSource
) : Repository

TasksRepository 的构造函数包含了 localDataSource 和 remoteDataSource,需要构建这两个 DataSource 才能完成 TasksRepository 注入,代码如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
less复制代码@Module
@InstallIn(ApplicationComponent::class)
object QualifierModule {

// 为每个声明的限定符,提供对应的类型实例,和 @Binds 或者 @Provides 一起使用
@Qualifier
// @Retention 定义了注解的生命周期,对应三个值(SOURCE、BINARY、RUNTIME)
// AnnotationRetention.SOURCE:仅编译期,不存储在二进制输出中。
// AnnotationRetention.BINARY:存储在二进制输出中,但对反射不可见。
// AnnotationRetention.RUNTIME:存储在二进制输出中,对反射可见。
@Retention(AnnotationRetention.RUNTIME)
annotation class RemoteTasksDataSource // 注解的名字,后面直接使用它

@Qualifier
@Retention(AnnotationRetention.RUNTIME)
annotation class LocalTasksDataSource

@Singleton
@RemoteTasksDataSource
@Provides
fun provideTasksRemoteDataSource(): DataSource { // 返回值相同
return RemoteDataSource() // 不同的实现
}

@Singleton
@LocalTasksDataSource
@Provides
fun provideTaskLocalDataSource(appDatabase: AppDataBase): DataSource { // 返回值相同
return LocalDataSource(appDatabase.personDao()) // 不同的实现
}

@Singleton
@Provides
fun provideTasksRepository(
@LocalTasksDataSource localDataSource: DataSource,
@RemoteTasksDataSource remoteDataSource: DataSource
): Repository {
return TasksRepository(
localDataSource,
remoteDataSource
)
}
}

这只是 Repository 注入代码,当然这并不是全部,还有 Room、Retrofit、Activity、Fragment、ViewModel 等等需要注入,随着项目越来越复杂,多模块化的拆分,还有更多的事情需要去做。

Hilt 和 Dagger 比起来虽然简单很多,但是 Hilt 相比于 Koin,其入门的门槛还是很高的,尤其是 Hilt 的注解,需要了解其每个注解的含义才能正确的使用,避免资源的浪费,但是对于注解的爱好者来说,可能更偏向于使用 Hilt,接下来我们来看一下如何在项目中使用 Koin。

在项目中使用 Koin

如果要在项目中使用 Koin,需要在项目中添加 Koin 的依赖,我们只需要在 App 模块中的 build.gradle 文件中添加以下代码。

1
2
复制代码implementation “org.koin:koin-core:2.1.5”
implementation “org.koin:koin-androidx-viewmodel:2.1.5”

如果需要在项目中使用 Koin 进行依赖注入,需要在 Application 或者其他的地方进行初始化。

1
2
3
4
5
6
7
8
9
10
11
kotlin复制代码class KoinApplication : Application() {

override fun onCreate() {
super.onCreate()
startKoin {
AndroidLogger(Level.DEBUG)
androidContext(this@KoinApplication)
modules(appModule)
}
}
}

当初始化完成之后,就可以在项目中使用 Koin 了,首先我们来看一下如何在项目中注入 Repository, Repository 有一个子类 TasksRepository,代码和上文介绍的一样,需要在其构造函数构造 localDataSource 和 remoteDataSource 两个 DataSource。

1
2
3
4
kotlin复制代码class TasksRepository @Inject constructor(
private val localDataSource: DataSource,
private val remoteDataSource: DataSource
) : Repository

那么在 Koin 中如何注入呢,很简单,只需要几行代码就可以完成。

1
2
3
4
5
6
7
scss复制代码val repoModule = module {
single { LocalDataSource(get()) }
single { RemoteDataSource() }
single { TasksRepository(get(), get()) }
}
// 添加所有需要在 Application 中进行初始化的 module
val appModule = listOf(repoModule)

和上面 Hilt 长长的代码比起来,Koin 是不是简单很多,那么 Room、Retrofit、ViewModel 如何注入呢,也很简单,代码如下所示。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
scss复制代码// 注入 ViewModel
val viewModele = module {
viewModel { MainViewModel(get()) }
}

// 注入 Room
val localModule = module {
single { AppDataBase.initDataBase(androidApplication()) }
single { get<AppDataBase>().personDao() }
}

// 注入 Retrofit
val remodeModule = module {
single { GitHubService.createRetrofit() }
single { get<Retrofit>().create(GitHubService::class.java) }
}

// 添加所有需要在 Application 中进行初始化的 module
val appModule = listOf(viewModele, localModule, remodeModule)

上面 Koin 的代码严格意义上讲,其实不太规范,在这里只是为了和 Hilt 进行更好的对比。

到这里是不是感觉 Hilt 相比于 Koin 是不是简单很多,在阅读 Hilt 文档的时候花了好几天时间才消化,而 Koin 只需要花很短的时间。

我们在看一下使用 Hilt 和 Koin 完成 Room、Retrofit、Repository 和 ViewModel 等等全部的依赖注入需要多少行代码。

依赖注入框架 Hilt Koin
代码行数 122 42

正如你所见依赖注入部分的代码 Hilt 多于 Koin,示例中只是一个基本的项目架构,实际的项目往往比这要复杂的很多,所需要的代码也更多,也越来越复杂。

不仅仅如此而已,根据 Koin 文档介绍,Koin 不需要用到反射,那么无反射 Koin 是如何实现的呢,因为 Koin 基于 kotlin 基础上进行开发的,使用了 kotlin 强大的语法糖(例如 Inline、Reified 等等)和函数式编程,来看一个简单的例子。

1
2
3
4
5
6
7
8
9
kotlin复制代码inline fun <reified T : ViewModel> Module.viewModel(
qualifier: Qualifier? = null,
override: Boolean = false,
noinline definition: Definition<T>
): BeanDefinition<T> {
val beanDefinition = factory(qualifier, override, definition)
beanDefinition.setIsViewModel()
return beanDefinition
}

内联函数支持具体化的类型参数,使用 reified 修饰符来限定类型参数,可以在函数内部访问它,由于函数是内联的,所以不需要反射。

但是在另一方面 Koin 相比于 Hilt 错误提示不够友好,Hilt 是基于 Dagger 基础上进行开发的,所以 Hilt 自然也拥有了 Dagger 的优点,编译时正确性,对于一个大型项目来说,这是一个非常严重的问题,因为我们更喜欢编译错误而不是运行时错误。

总结

我们总共从以下几个方面对 Hilt 和 Koin 进行全方面的分析:

  • AndroidStudio 支持 Hilt 在关联代码间进行导航,支持在 @Inject 修饰的构造器、@Binds 或者 @Provides 修饰的方法、限定符之间进行跳转。
  • 项目结构:完成 Hilt 的依赖注入需要的文件往往多于 Koin。
  • 代码行数:使用 Statistic 工具来进行代码统计,反复对比了项目编译前和编译后,Hilt 生成的代码多于 Koin,随着项目越来越复杂,生成的代码量会越来越多。
代码行数 Hilt Koin
编译之前 2414 2414
编译之后 149608 138405
* 编译时间:Hilt 编译时间总是大于 Koin,这个结果告诉我们,如果是在一个非常大型的项目,这个代价是非常昂贵。
1
2
3
4
5
6
7
yaml复制代码Hilt:
BUILD SUCCESSFUL in 35s
27 actionable tasks: 27 executed

Koin:
BUILD SUCCESSFUL in 18s
27 actionable tasks: 27 executed
  • 使用上对比:Hilt 使用起来要比 Koin 麻烦很多,其入门门槛高于 Koin,在阅读 Hilt 文档的时候花了好几天时间才消化,而 Koin 只需要花很短的时间,依赖注入部分的代码 Hilt 多于 Koin,在一个更大更复杂的项目中所需要的代码也更多,也越来越复杂。
依赖注入框架 Hilt Koin
代码行数 122 42

为什么 Hilt 编译时间总是大于 Koin?

因为在 Koin 中不需要使用注解,也不需要 kapt,这意味着没有额外的代码生成,所有的代码都是 Kotlin 原始代码,所以说 Hilt 编译时间总是大于 Koin,从这个角度上同时也解释了,为什么会说 Koin 仅使用功能解析,无额外代码生成。

为什么 Koin 不需要用到反射?

因为 Koin 基于 kotlin 基础上进行开发的,使用了 kotlin 强大的语法糖(例如 Inline、Reified 等等)和函数式编程,来看一个简单的例子。

1
2
3
4
5
6
7
8
9
kotlin复制代码inline fun <reified T : ViewModel> Module.viewModel(
qualifier: Qualifier? = null,
override: Boolean = false,
noinline definition: Definition<T>
): BeanDefinition<T> {
val beanDefinition = factory(qualifier, override, definition)
beanDefinition.setIsViewModel()
return beanDefinition
}

内联函数支持具体化的类型参数,使用 reified 修饰符来限定类型参数,可以在函数内部访问它,由于函数是内联的,所以不需要反射。

正在建立一个最全、最新的 AndroidX Jetpack 相关组件的实战项目 以及 相关组件原理分析文章,目前已经包含了 App Startup、Paging3、Hilt 等等,正在逐渐增加其他 Jetpack 新成员,仓库持续更新,可以前去查看:AndroidX-Jetpack-Practice, 如果这个仓库对你有帮助,请仓库右上角帮我点个赞。

结语

公众号开通了:ByteCode , 欢迎小伙伴们前去查看 Jetpack ,Kotlin ,Android 10 系列源码,译文,LeetCode / 剑指 Offer / 国内外大厂算法题 等等一系列文章,如果对你有帮助,请帮我点个 star,感谢!!!,欢迎一起来学习,在技术的道路上一起前进。

算法

由于 LeetCode 的题库庞大,每个分类都能筛选出数百道题,由于每个人的精力有限,不可能刷完所有题目,因此我按照经典类型题目去分类、和题目的难易程度去排序。

  • 数据结构: 数组、栈、队列、字符串、链表、树……
  • 算法: 查找算法、搜索算法、位运算、排序、数学、……

每道题目都会用 Java 和 kotlin 去实现,并且每道题目都有解题思路、时间复杂度和空间复杂度,如果你同我一样喜欢算法、LeetCode,可以关注我 GitHub 上的 LeetCode 题解:Leetcode-Solutions-with-Java-And-Kotlin,一起来学习,期待与你一起成长。

Android 10 源码系列

正在写一系列的 Android 10 源码分析的文章,了解系统源码,不仅有助于分析问题,在面试过程中,对我们也是非常有帮助的,如果你同我一样喜欢研究 Android 源码,可以关注我 GitHub 上的 Android10-Source-Analysis,文章都会同步到这个仓库。

  • 0xA01 Android 10 源码分析:APK 是如何生成的
  • 0xA02 Android 10 源码分析:APK 的安装流程
  • 0xA03 Android 10 源码分析:APK 加载流程之资源加载
  • 0xA04 Android 10 源码分析:APK 加载流程之资源加载(二)
  • 0xA05 Android 10 源码分析:Dialog 加载绘制流程以及在 Kotlin、DataBinding 中的使用
  • 0xA06 Android 10 源码分析:WindowManager 视图绑定以及体系结构
  • 0xA07 Android 10 源码分析:Window 的类型 以及 三维视图层级分析
  • 更多……

Android 应用系列

  • 如何在项目中封装 Kotlin + Android Databinding
  • 再见吧 buildSrc, 拥抱 Composing builds 提升 Android 编译速度
  • 为数不多的人知道的 Kotlin 技巧以及 原理解析
  • Jetpack 最新成员 AndroidX App Startup 实践以及原理分析
  • Jetpack 成员 Paging3 实践以及源码分析(一)
  • Jetpack 新成员 Paging3 网络实践及原理分析(二)
  • Jetpack 新成员 Hilt 实践(一)启程过坑记
  • Jetpack 新成员 Hilt 实践之 App Startup(二)进阶篇
  • Jetpack 新成员 Hilt 与 Dagger 大不同(三)落地篇

精选译文

目前正在整理和翻译一系列精选国外的技术文章,不仅仅是翻译,很多优秀的英文技术文章提供了很好思路和方法,每篇文章都会有译者思考部分,对原文的更加深入的解读,可以关注我 GitHub 上的 Technical-Article-Translation,文章都会同步到这个仓库。

  • [译][Google工程师] 刚刚发布了 Fragment 的新特性 “Fragment 间传递数据的新方式” 以及源码分析
  • [译][Google工程师] 详解 FragmentFactory 如何优雅使用 Koin 以及部分源码分析
  • [译][2.4K Start] 放弃 Dagger 拥抱 Koin
  • [译][5k+] Kotlin 的性能优化那些事
  • [译] 解密 RxJava 的异常处理机制
  • [译][1.4K+ Star] Kotlin 新秀 Coil VS Glide and Picasso
  • 更多……

工具系列

  • 为数不多的人知道的 AndroidStudio 快捷键(一)
  • 为数不多的人知道的 AndroidStudio 快捷键(二)
  • 关于 adb 命令你所需要知道的
  • 10分钟入门 Shell 脚本编程
  • 基于 Smali 文件 Android Studio 动态调试 APP
  • 解决在 Android Studio 3.2 找不到 Android Device Monitor 工具

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

IDEA 不为人知的 5 个骚技巧!真香!

发表于 2020-07-06

工欲善其事,必先利其器,磊哥最近发现了几个特别棒的 IDEA“骚”技巧,已经迫不及待的想要分享给你了,快上车…

1.快速补全行末分号

img

使用快捷键 Shfit + Ctrl + Enter 轻松实现。

2.自带的 HTTP 请求工具

IDEA 自带了 HTTP 的测试工具,这个功能隐藏的有点深。

这下可以卸载掉 Postman 了(我信你个鬼,你个糟老头…),如下图所示:

img

使用快捷键 Shift + Ctrl + A,然后搜索 “rest client”,输入回车打开 HTTP 请求测试页面。

3.粘贴板历史记录

俗话说的好,程序员都是面向 CV 编程(Ctrl+C 复制、Ctrl+V 粘贴),那怎么能不知道这个神奇的功能呢?

只需要使用快捷键 Shitf + Ctrl + V 就打开粘贴板的历史记录了,话说这个快捷键磊哥最熟了呢,如下图所示:

img

4.神奇的 Language Injection

我们将 String 转换为 JSON 格式非常的麻烦,需要各种转义,而 IDEA 为我们提供了 Language Injection,可以轻松的将字符串转换为 JSON,如下图所示:

img

PS:妈妈再也不用担心我转换字符串了。

Language Injection 也可以支持正则表达式,甚至支持简单的正则表达式的测试能力:

img

5.秒查字节码

这是一个超牛的功能,磊哥最近才发现的。

从此可以告别传统的 javac 生成字节码,再用 javap -c xxx 查看字节码的方式了,IDEA 支持直接查看字节码,只能说相见恨晚,如下图所示:

img

最后

你还知道哪些更“骚”的技巧吗?欢迎评论区留言补充。

参考 & 鸣谢

www.jianshu.com/p/364b94a66…

关注公众号「Java中文社群」订阅更多精彩。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

1…796797798…956

开发者博客

9558 日志
1953 标签
RSS
© 2025 开发者博客
本站总访问量次
由 Hexo 强力驱动
|
主题 — NexT.Muse v5.1.4
0%