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

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


  • 首页

  • 归档

  • 搜索

Google鼓励的13条代码审查标准 【建议收藏】

发表于 2020-09-05

如何在代码审查方面表现出色

在本文中,我们将简要介绍13种代码审查标准,这些标准可以极大地帮助改善软件的运行状况并保持开发人员满意。

顾名思义,

代码审查
是一个过程,其中一个或多个开发人员审查或筛选另一位开发者(作者)编写的代码,以确保:

  • 代码没有任何错误或问题。
  • 符合所有质量要求和标准。
  • 代码执行了预期的测试。
  • 合并后,它将使代码库的运行状况保持更好。

这就是为什么代码审查是软件开发的关键部分的原因。代码审阅者充当代码库管理员,负责确定代码是否处于要成为代码库的一部分并进入生产的状态。

Google以其卓越的技术而著称,它们具有有效的代码审查标准,这些标准似乎突出了审查代码时要记住的一些要点。在Google,代码审查的主要目的是确保Google代码库的整体代码运行状况随着时间的推移而不断改善。

这是您在查看更改列表时要记住的事项列表。

审查标准

1.该代码改善了系统的整体运行状况

每个更改列表(Pull Request)都会改善系统的整体运行状况。想法是,由于进行了如此小的改进,每次合并后,软件或代码库的运行状况都会得到改善。

2.快速的代码审查,响应和反馈

首先,不要延迟推送(合并)更好的代码。不要指望代码是完美的。如果它的状况可以改善系统的整体运行状况,则请推送。

“这里的关键是没有’完美’的代码,只有更好的代码。”

如果您不在一项重点任务的中间,那么请在代码完成后立即进行检查;但是,一个工作日是响应拉取请求所需的最长时间。预计变更列表将在一天之内获得多轮的部分/完整代码审查。

3.在代码审查期间进行教育和启发

通过尽可能共享知识和经验,在代码审查期间提供指导。

4.审查代码时遵循标准

始终牢记,编码标准此类文档是代码审查期间的绝对权威。例如,要在制表符和空格之间保持一致性,可以引用编码约定。

5.解决代码审查冲突

通过遵循样式指南和编码标准文档中商定的最佳实践,并寻求其他在产品领域具有更多知识和经验的人的建议,来解决冲突。根据严重性,处理冲突有所不同。

如果您的评论是可选的或次要的,请在评论中进行说明,然后由作者决定是解决还是忽略它们。作为代码审阅者,您至少可以建议在没有样式指南或编码标准的情况下,更改列表(请求)与其余代码库保持一致。

6.演示UI更改是代码审查的一部分

如果更改列表(Pull Request)更改了用户界面,则除了代码查看之外,还必须进行演示以确保外观上的所有外观均符合预期并与模拟匹配。

对于前端变更列表(Pull Request),始终建议进行演示/演练,或确保变更列表还包括必要的UI自动化测试,以验证添加/更新的功能。

7.确保代码审查伴随所有测试

除非紧急情况,否则拉取请求(更改列表)应伴随所有必要的测试,例如单元,集成,端到端等。

紧急情况可能是需要尽快修复的错误或安全漏洞,以后可以添加测试。在这种情况下,请确保创建了适当的问题,并确保有人在完成热修复或部署后立即拥有所有权才能完成。

没有足够的理由跳过测试。如果由于时间限制,某些目标有无法实现的风险,那么解决方案不是跳过测试,而是要对可交付成果进行范围界定。

8.专注时,不要打扰自己进行代码审查

如果您正处于重点工作中,请不要打扰自己,因为这可能需要很长时间才能恢复正常。换句话说,打断专注的开发人员所付出的代价比让开发人员等待代码审查要高得多。在计划的休息时间(例如午餐,咖啡等)之后进行代码检查。

期望并非总是在同一天完成并合并整个代码审查过程。重要的是迅速给作者一些反馈。例如,您可能无法进行完整的检查,但是您可以快速指出一些可以研究的内容。这将极大地减少代码审查期间的挫败感。

9.复习一切,不要做任何假设

查看分配给您检查的每一行代码。不要对人工编写的类和方法做任何假设,并且应该确保您了解代码在做什么。

确保了解您正在检查的代码。如果没有,请进行澄清或要求代码演练/解释。如果您有部分代码不具备审阅的资格,请确保还有其他合格的开发人员可以审阅代码的那些部分。

10.回顾代码时要顾全大局

从更广泛的背景来看变化通常是有帮助的。例如,更改了文件,并添加了四行代码。不要只查看四行代码;相反,请考虑查看整个文件并检查新添加的内容。它们会降低现有代码的质量,还是会使现有功能成为重构的候选对象?

如果不在函数/方法或类的上下文中检查此类简单的添加项,则随着时间的流逝,您将继承一个类,该类是不可维护的,超级复杂的,难以测试的,无法完成的所有工作,并且难以扩展或重构。

请记住,随着时间的推移,很少的改进加起来就可以产生具有最少数量缺陷的优质产品,同样,随着时间的流逝,轻微的代码降级或技术负担也会加重并导致产品难以维护和扩展。

11.认可并鼓励代码评审期间的良好工作

如果您在变更列表中看到了一些不错的东西,请别忘了喊出作者的出色作品并鼓励他们。这是我个人以前从未做过的事情。代码审查的目的不仅应该是发现错误,还应该鼓励和指导开发人员所做的出色工作。

12.在代码审查中要谨慎,尊重,友善和清晰

至关重要的是,在代码审阅期间,您要善良,清晰,礼貌和尊重,同时也要对作者非常清楚和乐于助人。查看代码时,请确保对代码而不是开发人员做出评论。

13.解释您的代码审查注释,并牢记范围

每当代码审阅意见提出替代方法或进行标记时,至关重要的是要解释原因并根据您的知识和经验提供示例,以帮助开发人员了解您的建议将如何帮助提高代码质量。

当建议修复或更改时,请在如何指导作者修复代码方面找到适当的平衡。例如,我很欣赏指导,解释,一些提示或建议,而不是整个解决方案。

本文转载自: 掘金

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

springBoot整合spring security+JW

发表于 2020-09-04

写在前面

在前一篇文章《springBoot整合spring security实现权限管理(单体应用版)–筑基初期》当中,我们介绍了springBoot整合spring security单体应用版,在这篇文章当中,我将介绍springBoot整合spring secury+JWT实现单点登录与权限管理。

本文涉及的权限管理模型是基于资源的动态权限管理。数据库设计的表有 user 、role、user_role、permission、role_permission。

单点登录当中,关于访问者信息的存储有多种解决方案。如将其以key-value的形式存储于redis数据库中,访问者令牌中存放key。校验用户身份时,凭借访问者令牌中的key去redis中找value,没找到则返回“令牌已过期”,让访问者去(重新)认证。本文中的demo,是将访问者信息加密后存于token中返回给访问者,访问者携带令牌去访问服务时,服务提供者直接解密校验token即可。两种实现各有优缺点。大家也可以尝试着将本文中的demo的访问者信息存储改造成存在redis中的方式。文末提供完整的代码及sql脚本下载地址。

在进入正式步骤之前,我们需要了解以下知识点。

单点登录SSO

单点登录也称分布式认证,指的是在有多个系统的项目中,用户经过一次认证,即可访问该项目下彼此相互信任的系统。

单点登录流程

给大家画了个流程图

![](F:\01_Java全栈工程师学习&笔记&资料\22ssm综合练习和svn  adminlte  springSecurity的使用\springboot整合spring security(分布式版).assets/01_单点登录-1599194921207.png)

关于JWT

jwt,全称JSON Web Token,是一款出色的分布式身份校验方案。

jwt由三个部分组成

  1. 头部:主要设置一些规范信息,签名部分的编码格式就在头部中声明。
  2. 有效载荷:token中存放有效信息的部分,比如用户名,用户角色,过期时间等,但不适合放诸如密码等敏感数据,会造成泄露。
  3. 签名:将头部与载荷分别采用base64编码后,用“.”相连,再加入盐,最后使用头部声明的编码类型进行编码,就得到了签名。

jwt生成的Token安全性分析

想要使得token不被伪造,就要确保签名不被篡改。然而,其签名的头部和有效载荷使用base64编码,这与明文无异。因此,我们只能在盐上做手脚了。我们对盐进行非对称加密后,在将token发放给用户。

RSA非对称加密

  1. 基本原理:同时生成两把密钥:私钥和公钥,私钥隐秘保存,公钥可以下发给信任客户端 。
* 公钥加密:只有私钥才能解密,一般公钥具有多个拷贝
* 私钥加密:只有公钥才能解密,一般私钥只有一份
  1. 优缺点:
* 优点:安全、难以破解
* 缺点:耗时,但是为了安全,这是可以接受的

SpringSecurity+JWT+RSA分布式认证思路分析

通过之前的学习,我们知道了spring security主要是基于过滤器链来做认证的,因此,如何打造我们的单点登录,突破口就在于spring security中的认证过滤器。

用户认证

在分布式项目当中,现在大多数都是前后端分离架构设计的,因此,我们需要能够接收POST请求的认证参数,而不是传统的表单提交。因此,我们需要修改修
改UsernamePasswordAuthenticationFilter过滤器中attemptAuthentication方法,让其能够接收请求体。

关于spring security的认证流程分析,大家可以参考我上一篇文章《Spring Security认证流程分析–练气后期》。

另外,默认情况下,successfulAuthentication 方法在通过认证后,直接将认证信息放到服务器的session当中就ok了。而我们分布式应用当中,前后端分离,禁用了session。因此,我们需要在认证通过后生成token(载荷内具有验证用户身份必要的信息)返回给用户。

身份校验

默认情况下,BasicAuthenticationFilter过滤器中doFilterInternal方法校验用户是否登录,就是看session中是否有用户信息。在分布式应用当中,我们要修改为,验证用户携带的token是否合法,并解析出用户信息,交给SpringSecurity,以便于后续的授权功能可以正常使用。

实现步骤

(默认大家一已经创建好了数据库)

第一步:创建一个springBoot的project

这个父工程主要做依赖的版本管理。

其pom.xml文件如下

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
110
111
112
xml复制代码<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<modules>
<module>common</module>
</modules>

<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.3.3.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<packaging>pom</packaging>
<groupId>pers.lbf</groupId>
<artifactId>springboot-springSecurity-jwt-rsa</artifactId>
<version>1.0.0-SNAPSHOT</version>

<properties>
<java.version>1.8</java.version>
<jwt.version>0.10.7</jwt.version>
<jackson.version>2.11.2</jackson.version>
<springboot.version>2.3.3.RELEASE</springboot.version>
<mybatis.version>2.1.3</mybatis.version>
<mysql.version>8.0.12</mysql.version>
<joda.version>2.10.5</joda.version>
<springSecurity.version>5.3.4.RELEASE</springSecurity.version>
<common.version>1.0.0-SNAPSHOT</common.version>
</properties>

<dependencyManagement>
<dependencies>
<dependency>
<groupId>pers.lbf</groupId>
<artifactId>common</artifactId>
<version>${common.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
<version>${springboot.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>${springboot.version}</version>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>${mybatis.version}</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>${mysql.version}</version>
</dependency>

<!--jwt所需jar包-->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>${jwt.version}</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>${jwt.version}</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<version>${jwt.version}</version>
<scope>runtime</scope>
</dependency>
<!-- 处理日期-->
<dependency>
<groupId>joda-time</groupId>
<artifactId>joda-time</artifactId>
<version>${joda.version}</version>
</dependency>
<!--处理json工具包-->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>${jackson.version}</version>
</dependency>
<!--日志包-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-logging</artifactId>
<version>${springboot.version}</version>
</dependency>
<!--测试包-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<version>${springboot.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-test</artifactId>
<version>${springSecurity.version}</version>
</dependency>
</dependencies>
</dependencyManagement>


</project>

第二步:创建三个子模块

其中,common模块作为公共模块存在,提供基础服务,包括token的生成、rsa加密密钥的生成与使用、Json序列化与反序列化。

authentication-service模块提供单点登录服务(用户认证及授权)。

product-service模块模拟一个子系统。它主要负责提供接口调用和校验用户身份。

创建common模块模块

#####修改pom.xml,添加jwt、json等依赖

pom.xml

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
xml复制代码<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>springboot-springSecurity-jwt-rsa</artifactId>
<groupId>pers.lbf</groupId>
<version>1.0.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>

<artifactId>common</artifactId>

<dependencies>
<!--jwt所需jar包-->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>joda-time</groupId>
<artifactId>joda-time</artifactId>
</dependency>
<!--处理json工具包-->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
<!--日志包-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-logging</artifactId>
</dependency>
<!--测试包-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
</dependency>

</dependencies>


</project>
创建一个JSON工具类
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
java复制代码**json工具类
* @author 赖柄沣 bingfengdev@aliyun.com
* @version 1.0
* @date 2020/9/2 22:28
*/
public class JsonUtils {

public static final ObjectMapper MAPPER = new ObjectMapper();
private static final Logger logger = LoggerFactory.getLogger(JsonUtils.class);


private JsonUtils() {

}

public static String toString(Object obj) {
if (obj == null) {
return null;
}
if (obj.getClass() == String.class) {
return (String) obj;
}
try {
return MAPPER.writeValueAsString(obj);
} catch (JsonProcessingException e) {
logger.error("json序列化出错:" + obj, e);
return null;
}
}

public static <T> T toBean(String json, Class<T> tClass) {
try {
return MAPPER.readValue(json, tClass);
} catch (IOException e) {
logger.error("json解析出错:" + json, e);
return null;
}
}

public static <E> List<E> toList(String json, Class<E> eClass) {
try {
return MAPPER.readValue(json, MAPPER.getTypeFactory().constructCollectionType(List.class, eClass));
} catch (IOException e) {
logger.error("json解析出错:" + json, e);
return null;
}
}

public static <K, V> Map<K, V> toMap(String json, Class<K> kClass, Class<V> vClass) {
try {
return MAPPER.readValue(json, MAPPER.getTypeFactory().constructMapType(Map.class, kClass, vClass));
} catch (IOException e) {
logger.error("json解析出错:" + json, e);
return null;
}
}

public static <T> T nativeRead(String json, TypeReference<T> type) {
try {
return MAPPER.readValue(json, type);
} catch (IOException e) {
logger.error("json解析出错:" + json, e);
return null;
}
}
}
创建RSA加密工具类,并生成公钥和密钥文件

​ RsaUtils.java

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
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
java复制代码/**RSA非对称加密工具类
* @author 赖柄沣 bingfengdev@aliyun.com
* @version 1.0
* @date 2020/9/2 22:27
*/
public class RsaUtils {

private static final int DEFAULT_KEY_SIZE = 2048;

/**从文件中读取公钥
* @author 赖柄沣 bingfengdev@aliyun.com
* @date 2020-09-04 13:10:15
* @param filename 公钥保存路径,相对于classpath
* @return java.security.PublicKey 公钥对象
* @throws Exception
* @version 1.0
*/
public static PublicKey getPublicKey(String filename) throws Exception {

byte[] bytes = readFile(filename);
return getPublicKey(bytes);
}


/**从文件中读取密钥
* @author 赖柄沣 bingfengdev@aliyun.com
* @date 2020-09-04 13:12:01
* @param filename 私钥保存路径,相对于classpath
* @return java.security.PrivateKey 私钥对象
* @throws Exception
* @version 1.0
*/
public static PrivateKey getPrivateKey(String filename) throws Exception {
byte[] bytes = readFile(filename);
return getPrivateKey(bytes);

}

/**
* @author 赖柄沣 bingfengdev@aliyun.com
* @date 2020-09-04 13:12:59
* @param bytes 公钥的字节形式
* @return java.security.PublicKey 公钥对象
* @throws Exception
* @version 1.0
*/
private static PublicKey getPublicKey(byte[] bytes) throws Exception {
bytes = Base64.getDecoder().decode(bytes);
X509EncodedKeySpec spec = new X509EncodedKeySpec(bytes);
KeyFactory factory = KeyFactory.getInstance("RSA");
return factory.generatePublic(spec);

}


/**获取密钥
* @author 赖柄沣 bingfengdev@aliyun.com
* @date 2020-09-04 13:14:02
* @param bytes 私钥的字节形式
* @return java.security.PrivateKey
* @throws Exception
* @version 1.0
*/
private static PrivateKey getPrivateKey(byte[] bytes) throws InvalidKeySpecException, NoSuchAlgorithmException {
bytes = Base64.getDecoder().decode(bytes);
PKCS8EncodedKeySpec spec = new PKCS8EncodedKeySpec(bytes);
KeyFactory factory = KeyFactory.getInstance("RSA");
return factory.generatePrivate(spec);

}

/**
* 根据密文,生存rsa公钥和私钥,并写入指定文件
*@author 赖柄沣 bingfengdev@aliyun.com
*@date 2020-09-04 13:14:02
* @param publicKeyFilename 公钥文件路径
* @param privateKeyFilename 私钥文件路径
* @param secret 生成密钥的密文
*/
public static void generateKey(String publicKeyFilename, String privateKeyFilename, String secret, int keySize) throws Exception {
KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA");
SecureRandom secureRandom = new SecureRandom(secret.getBytes());
keyPairGenerator.initialize(Math.max(keySize, DEFAULT_KEY_SIZE), secureRandom);
KeyPair keyPair = keyPairGenerator.genKeyPair();
// 获取公钥并写出
byte[] publicKeyBytes = keyPair.getPublic().getEncoded();
publicKeyBytes = Base64.getEncoder().encode(publicKeyBytes);
writeFile(publicKeyFilename, publicKeyBytes);
// 获取私钥并写出
byte[] privateKeyBytes = keyPair.getPrivate().getEncoded();
privateKeyBytes = Base64.getEncoder().encode(privateKeyBytes);
writeFile(privateKeyFilename, privateKeyBytes);
}

/**读文件
* @author 赖柄沣 bingfengdev@aliyun.com
* @date 2020-09-04 13:15:37
* @param fileName
* @return byte[]
* @throws
* @version 1.0
*/
private static byte[] readFile(String fileName) throws Exception {
return Files.readAllBytes(new File(fileName).toPath());

}

/**写文件
* @author 赖柄沣 bingfengdev@aliyun.com
* @date 2020-09-04 13:16:01
* @param destPath
* @param bytes
* @return void
* @throws
* @version 1.0
*/
private static void writeFile(String destPath, byte[] bytes) throws IOException {
File dest = new File(destPath);
if (!dest.exists()) {
dest.createNewFile();
}
Files.write(dest.toPath(), bytes);

}

/**构造器私有化
* @author 赖柄沣 bingfengdev@aliyun.com
* @date 2020-09-04 13:16:29
* @param
* @return
* @throws
* @version 1.0
*/
private RsaUtils() {

}


}

生成私钥和公钥两个文件

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
java复制代码/**
* @author 赖柄沣 bingfengdev@aliyun.com
* @version 1.0
* @date 2020/9/3 10:28
*/

public class RsaTest {
private String publicFile = "D:\\Desktop\\rsa_key.pub";
private String privateFile = "D:\\Desktop\\rsa_key";


/**生成公钥和私钥
* @author 赖柄沣 bingfengdev@aliyun.com
* @date 2020-09-03 10:32:16
* @throws Exception
* @version 1.0
*/
@Test
public void generateKey() throws Exception{

RsaUtils.generateKey(publicFile,privateFile,"Java开发实践",2048);

}

}

私钥文件一定要保护好!!!

私钥文件一定要保护好!!!

私钥文件一定要保护好!!!

(重要的事情说三遍!!!)

1
shell复制代码##### 创建token有效载荷实体类和JWT工具类
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
java复制代码/**为了方便后期获取token中的用户信息,
* 将token中载荷部分单独封装成一个对象
* @author 赖柄沣 bingfengdev@aliyun.com
* @version 1.0
* @date 2020/9/2 22:24
*/
public class Payload<T> implements Serializable {

/**
* token id
*/
private String id;

/**
* 用户信息(用户名、角色...)
*/
private T userInfo;

/**
* 令牌过期时间
*/
private Date expiration;

getter。。。
setter。。。
}

JwtUtils

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
java复制代码/**token工具类
* @author 赖柄沣 bingfengdev@aliyun.com
* @version 1.0
* @date 2020/9/2 22:28
*/
public class JwtUtils {

private static final String JWT_PAYLOAD_USER_KEY = "user";

/**
* 私钥加密token
*
* @param userInfo 载荷中的数据
* @param privateKey 私钥
* @param expire 过期时间,单位分钟
* @return JWT
*/
public static String generateTokenExpireInMinutes(Object userInfo, PrivateKey privateKey, int expire) {
return Jwts.builder()
.claim(JWT_PAYLOAD_USER_KEY, JsonUtils.toString(userInfo))
.setId(createJTI())
.setExpiration(DateTime.now().plusMinutes(expire).toDate())
.signWith(privateKey, SignatureAlgorithm.RS256)
.compact();
}

/**
* 私钥加密token
*
* @param userInfo 载荷中的数据
* @param privateKey 私钥
* @param expire 过期时间,单位秒
* @return JWT
*/
public static String generateTokenExpireInSeconds(Object userInfo, PrivateKey privateKey, int expire) {
return Jwts.builder()
.claim(JWT_PAYLOAD_USER_KEY, JsonUtils.toString(userInfo))
.setId(createJTI())
.setExpiration(DateTime.now().plusSeconds(expire).toDate())
.signWith(privateKey, SignatureAlgorithm.RS256)
.compact();
}

/**
* 公钥解析token
*
* @param token 用户请求中的token
* @param publicKey 公钥
* @return Jws<Claims>
*/
private static Jws<Claims> parserToken(String token, PublicKey publicKey) throws ExpiredJwtException {
return Jwts.parser().setSigningKey(publicKey).parseClaimsJws(token);
}

private static String createJTI() {
return new String(Base64.getEncoder().encode(UUID.randomUUID().toString().getBytes()));
}

/**
* 获取token中的用户信息
*
* @param token 用户请求中的令牌
* @param publicKey 公钥
* @return 用户信息
*/
public static <T> Payload<T> getInfoFromToken(String token, PublicKey publicKey, Class<T> userType) throws ExpiredJwtException {
Jws<Claims> claimsJws = parserToken(token, publicKey);
Claims body = claimsJws.getBody();
Payload<T> claims = new Payload<>();
claims.setId(body.getId());
claims.setUserInfo(JsonUtils.toBean(body.get(JWT_PAYLOAD_USER_KEY).toString(), userType));
claims.setExpiration(body.getExpiration());
return claims;
}

/**
* 获取token中的载荷信息
*
* @param token 用户请求中的令牌
* @param publicKey 公钥
* @return 用户信息
*/
public static <T> Payload<T> getInfoFromToken(String token, PublicKey publicKey) {
Jws<Claims> claimsJws = parserToken(token, publicKey);
Claims body = claimsJws.getBody();
Payload<T> claims = new Payload<>();
claims.setId(body.getId());
claims.setExpiration(body.getExpiration());
return claims;
}

private JwtUtils() {

}
}

写完common模块后,将其打包安装,后面的两个服务都需要引用。

创建认证服务模块authentication-service

认证服务模块的关键点在于自定义用户认证过滤器和用户校验过滤器,并将其加载到spring security的过滤器链中,替代掉默认的。

1
shell复制代码##### 修改pom.xml文件,添加相关依赖

pom.xml

​

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
xml复制代码<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>pers.lbf</groupId>
<artifactId>springboot-springSecurity-jwt-rsa</artifactId>
<version>1.0.0-SNAPSHOT</version>
</parent>
<artifactId>authentication-service</artifactId>
<version>1.0.0-SNAPSHOT</version>
<name>authentication-service</name>
<description>Demo project for Spring Boot</description>

<properties>
<java.version>1.8</java.version>
</properties>

<dependencies>
<dependency>
<groupId>pers.lbf</groupId>
<artifactId>common</artifactId>
</dependency>
<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.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
</dependency>

<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>

<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>

</project>

这个模块添加的依赖主要是springBoot整合spring security的相关依赖以及数据库相关的依赖,当然还有我们的common模块。

修改application.yml文件

这一步主要是设置数据库连接的信息以及公钥、私钥的位置信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
yaml复制代码server:
port: 8081
spring:
datasource:
url: jdbc:mysql://127.0.0.1:3306/security_authority?useSSL=false&serverTimezone=GMT
username: root
password: root1997
driver-class-name: com.mysql.cj.jdbc.Driver
mybatis:
configuration:
map-underscore-to-camel-case: true
logging:
level:
pers.lbf: debug
lbf:
key:
publicKeyPath: 你的公钥路径
privateKeyPath: 你的私钥路径
配置解析公钥和私钥
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
java复制代码**解析公钥和私钥的配置类
* @author 赖柄沣 bingfengdev@aliyun.com
* @version 1.0
* @date 2020/9/3 10:42
*/
@ConfigurationProperties(prefix = "lbf.key")
@ConstructorBinding
public class AuthServerRsaKeyProperties {

private String publicKeyPath;
private String privateKeyPath;

private PublicKey publicKey;
private PrivateKey privateKey;


/**加载文件当中的公钥、私钥
* 被@PostConstruct修饰的方法会在服务器加载Servlet的时候运行,
* 并且只会被服务器执行一次。PostConstruct在构造函数之后执行,
* init()方法之前执行。
* @author 赖柄沣 bingfengdev@aliyun.com
* @date 2020-09-03 12:07:35
* @throws Exception e
* @version 1.0
*/
@PostConstruct
public void loadKey() throws Exception {
publicKey = RsaUtils.getPublicKey(publicKeyPath);
privateKey = RsaUtils.getPrivateKey(privateKeyPath);

}

public String getPublicKeyPath() {
return publicKeyPath;
}

public void setPublicKeyPath(String publicKeyPath) {
this.publicKeyPath = publicKeyPath;
}

public String getPrivateKeyPath() {
return privateKeyPath;
}

public void setPrivateKeyPath(String privateKeyPath) {
this.privateKeyPath = privateKeyPath;
}

public PublicKey getPublicKey() {
return publicKey;
}

public void setPublicKey(PublicKey publicKey) {
this.publicKey = publicKey;
}

public PrivateKey getPrivateKey() {
return privateKey;
}

public void setPrivateKey(PrivateKey privateKey) {
this.privateKey = privateKey;
}
}
修改启动类,添加token加密解析的配置和mapper扫描
1
2
3
4
5
6
7
8
9
10
11
12
java复制代码/**
* @author Ferryman
*/
@SpringBootApplication
@MapperScan(value = "pers.lbf.ssjr.authenticationservice.dao")
@EnableConfigurationProperties(AuthServerRsaKeyProperties.class)
public class AuthenticationServiceApplication {
public static void main(String[] args) {
SpringApplication.run(AuthenticationServiceApplication.class, args);
}

}
创建用户登录对象UserLoginVO

我们将用户登录的请求参数封装到一个实体类当中,而不使用与数据库表对应的UserTO。

1
2
3
4
5
6
7
8
9
10
11
12
13
Java复制代码/**用户登录请求参数对象
* @author 赖柄沣 bingfengdev@aliyun.com
* @version 1.0
* @date 2020/9/3 16:16
*/
public class UserLoginVo implements Serializable {

private String username;
private String password;

getter。。。
settter。。。
}
创建用户凭证对象UserAuthVO

这个对象主要用于存储访问者认证成功后,其在token中的信息。这里我们是不存储密码等敏感数据的。

1
2
3
4
5
6
7
8
9
10
11
12
13
java复制代码/**用户凭证对象
* @author 赖柄沣 bingfengdev@aliyun.com
* @version 1.0
* @date 2020/9/3 16:20
*/
public class UserAuthVO implements Serializable {

private String username;
private List<SimpleGrantedAuthority> authorities;

getter。。。
setter。。。
}
创建自定义认证过滤器
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
110
111
112
113
114
115
Java复制代码/**自定义认证过滤器
* @author 赖柄沣 bingfengdev@aliyun.com
* @version 1.0
* @date 2020/9/3 12:11
*/
public class TokenLoginFilter extends UsernamePasswordAuthenticationFilter {

/**
* 认证管理器
*/

private AuthenticationManager authenticationManager;

private AuthServerRsaKeyProperties prop;

/**构造注入
* @author 赖柄沣 bingfengdev@aliyun.com
* @date 2020-09-03 12:17:54
* @param authenticationManager spring security的认证管理器
* @param prop 公钥 私钥 配置类
* @version 1.0
*/
public TokenLoginFilter(AuthenticationManager authenticationManager, AuthServerRsaKeyProperties prop) {
this.authenticationManager = authenticationManager;
this.prop = prop;

}


/**接收并解析用户凭证,并返回json数据
* @author 赖柄沣 bingfengdev@aliyun.com
* @date 2020-09-03 12:19:29
* @param request req
* @param response resp
* @return Authentication
* @version 1.0
*/
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response){

//判断请求是否为POST,禁用GET请求提交数据
if (!"POST".equals(request.getMethod())) {
throw new AuthenticationServiceException(
"只支持POST请求方式");
}


//将json数据转换为java bean对象
try {
UserLoginVo user = new ObjectMapper().readValue(request.getInputStream(), UserLoginVo.class);

if (user.getUsername()==null){
user.setUsername("");
}

if (user.getPassword() == null) {
user.setPassword("");
}
user.getUsername().trim();
//将用户信息交给spring security做认证操作
return authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(
user.getUsername(),
user.getPassword()));
}catch (Exception e) {

throw new RuntimeException(e);
}

}

/**这个方法会在验证成功时被调用
*用户登录成功后,生成token,并且返回json数据给前端
* @author 赖柄沣 bingfengdev@aliyun.com
* @date 2020-09-03 13:00:23
* @param request
* @param response
* @param chain
* @param authResult
* @version 1.0
*/
@Override
protected void successfulAuthentication(HttpServletRequest request,
HttpServletResponse response, FilterChain chain, Authentication authResult) {
//获取当前登录对象
UserAuthVO user = new UserAuthVO();
user.setUsername(authResult.getName());
user.setAuthorities((List<SimpleGrantedAuthority>) authResult.getAuthorities());

//使用jwt创建一个token,私钥加密
String token = JwtUtils.generateTokenExpireInMinutes(user,prop.getPrivateKey(),15);

//返回token
response.addHeader("Authorization","Bearer"+token);

//登录成功返回json数据提示
try {
//生成消息
Map<String, Object> map = new HashMap<>();
map.put("code",HttpServletResponse.SC_OK);
map.put("msg","登录成功");
//响应数据
response.setContentType("application/json;charset=utf-8");
response.setStatus(HttpServletResponse.SC_OK);
PrintWriter writer = response.getWriter();
writer.write(new ObjectMapper().writeValueAsString(map));
writer.flush();
writer.close();
}catch (Exception e) {
throw new RuntimeException(e);
}
}


}

到了这一步,你或许会开始觉得难以理解,这需要你稍微了解spring security的认证流程。可以阅读我之前的文章《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
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
java复制代码/**自定义身份验证器
* @author 赖柄沣 bingfengdev@aliyun.com
* @version 1.0
* @date 2020/9/3 15:02
*/
public class TokenVerifyFilter extends BasicAuthenticationFilter {

private AuthServerRsaKeyProperties prop;

public TokenVerifyFilter(AuthenticationManager authenticationManager, AuthServerRsaKeyProperties prop) {
super(authenticationManager);
this.prop = prop;
}

/**过滤请求
* @author 赖柄沣 bingfengdev@aliyun.com
* @date 2020-09-03 15:07:27
* @param request
* @param response
* @param chain
* @version 1.0
*/
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response, FilterChain chain) throws ServletException, IOException, AuthenticationException,ExpiredJwtException {

//判断请求体的头中是否包含Authorization
String authorization = request.getHeader("Authorization");
//Authorization中是否包含Bearer,不包含直接返回
if (authorization==null||!authorization.startsWith("Bearer")){
chain.doFilter(request, response);
return;
}

UsernamePasswordAuthenticationToken token;
try {
//解析jwt生成的token,获取权限
token = getAuthentication(authorization);

}catch (ExpiredJwtException e){
// e.printStackTrace();
chain.doFilter(request, response);
return;
}

//获取后,将Authentication写入SecurityContextHolder中供后序使用
SecurityContextHolder.getContext().setAuthentication(token);
chain.doFilter(request, response);


}



/**对jwt生成的token进行解析
* @author 赖柄沣 bingfengdev@aliyun.com
* @date 2020-09-03 15:21:04
* @param authorization auth
* @return org.springframework.security.authentication.UsernamePasswordAuthenticationToken
* @throws
* @version 1.0
*/
public UsernamePasswordAuthenticationToken getAuthentication(String authorization) throws ExpiredJwtException{

if (authorization == null) {
return null;
}

Payload<UserAuthVO> payload;

//从token中获取有效载荷
payload = JwtUtils.getInfoFromToken(authorization.replace("Bearer", ""), prop.getPublicKey(), UserAuthVO.class);



//获取当前访问对象
UserAuthVO userInfo = payload.getUserInfo();
if (userInfo == null){
return null;
}

//将当前访问对象及其权限封装称spring security可识别的token
UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(userInfo,null,userInfo.getAuthorities());
return token;
}
}
编写spring security的配置类

这一步主要是是完成对spring security的配置。唯一和单体版应用集成spring’security不同的是,在这一步需要加入我们自定义的用户认证和用户校验的过滤器,还有就是禁用session。

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
java复制代码/**spring security配置类
* @author 赖柄沣 bingfengdev@aliyun.com
* @version 1.0
* @date 2020/9/3 15:41
*/
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SpringSecurityConfig extends WebSecurityConfigurerAdapter {

@Autowired
private UserDetailsService userService;

@Autowired
private AuthServerRsaKeyProperties properties;

@Bean
public BCryptPasswordEncoder myPasswordEncoder(){
return new BCryptPasswordEncoder();
}


/**配置自定义过滤器
* @author 赖柄沣 bingfengdev@aliyun.com
* @date 2020-09-03 15:53:45
* @param http
* @version 1.0
*/
@Override
protected void configure(HttpSecurity http) throws Exception {

//禁用跨域保护,取代它的是jwt
http.csrf().disable();

//允许匿名访问的方法
http.authorizeRequests().antMatchers("/login").anonymous();
//其他需要鉴权
//.anyRequest().authenticated();

//添加认证过滤器
http.addFilter(new TokenLoginFilter(authenticationManager(),properties));

//添加验证过滤器
http.addFilter(new TokenVerifyFilter(authenticationManager(),properties));


//禁用session,前后端分离是无状态的
http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);


}



/**配置密码加密策略
* @author 赖柄沣 bingfengdev@aliyun.com
* @date 2020-09-03 15:50:46
* @param authenticationManagerBuilder
* @version 1.0
*/
@Override
protected void configure(AuthenticationManagerBuilder authenticationManagerBuilder) throws Exception {

authenticationManagerBuilder.userDetailsService(userService).passwordEncoder(myPasswordEncoder());
}

@Override
public void configure(WebSecurity webSecurity) throws Exception{
//忽略静态资源
webSecurity.ignoring().antMatchers("/assents/**","/login.html");
}

}
添加对GrantedAuthority类型的自定义反序列化工具

因为我们的权限信息是加密存储于token中的,因此要对authorities进行序列化与反序列化,然后由于jackson并不支持对其进行反序列化,因此需要我们自己去做。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
java复制代码**
* @author 赖柄沣 bingfengdev@aliyun.com
* @version 1.0
* @date 2020/9/3 22:42
*/
public class CustomAuthorityDeserializer extends JsonDeserializer {

@Override
public Object deserialize(JsonParser jp, DeserializationContext ctxt) throws IOException {
ObjectMapper mapper = (ObjectMapper) jp.getCodec();
JsonNode jsonNode = mapper.readTree(jp);
List<GrantedAuthority> grantedAuthorities = new LinkedList<>();

Iterator<JsonNode> elements = jsonNode.elements();
while (elements.hasNext()) {
JsonNode next = elements.next();
JsonNode authority = next.get("authority");
grantedAuthorities.add(new SimpleGrantedAuthority(authority.asText()));
}
return grantedAuthorities;
}

}

在UserAuthVO上标记

1
2
3
4
5
6
7
8
9
10
11
12
13
14
java复制代码/**用户凭证对象
* @author 赖柄沣 bingfengdev@aliyun.com
* @version 1.0
* @date 2020/9/3 16:20
*/
public class UserAuthVO implements Serializable {

@JsonDeserialize(using = CustomAuthorityDeserializer.class)
public void setAuthorities(List<SimpleGrantedAuthority> authorities) {
this.authorities = authorities;
}

//省略了其他无关的代码
}
实现UserDetailsService接口

实现loadUserByUsername方法,修改认证信息获取方式为:从数据库中获取权限信息。

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
java复制代码/**
* @author 赖柄沣 bingfengdev@aliyun.com
* @version 1.0
* @date 2020/8/28 22:16
*/
@Service("userService")
public class UserServiceImpl implements UserDetailsService {

@Autowired
private IUserDao userDao;
@Autowired
private IRoleDao roleDao;
@Autowired
private IPermissonDao permissonDao;


@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
if (username == null){
return null;
}

UserDO user = userDao.findByName(username);

List<RoleDO> roleList = roleDao.findByUserId(user.getId());

List<SimpleGrantedAuthority> list = new ArrayList<> ();
for (RoleDO roleDO : roleList) {
List<PermissionDO> permissionListItems = permissonDao.findByRoleId(roleDO.getId());
for (PermissionDO permissionDO : permissionListItems) {
list.add(new SimpleGrantedAuthority(permissionDO.getPermissionUrl()));
}
}
user.setAuthorityList(list);
return user;
}
}

**提示:**关于用户、角色、权限的数据库操作及其实体类到这里就省略了,不影响大家理解,当然,文末提供了完整的代码下载地址。

自定义401和403异常处理

Spring Security 中的异常主要分为两大类:一类是认证异常,另一类是授权相关的异常。并且,其抛出异常的地方是在过滤器链中,如果你使用@ControllerAdvice是没有办法处理的。

当然,像spring security这么优秀的框架,当然考虑到了这个问题。

spring security当中的HttpSecurity提供的exceptionHandling() 方法用来提供异常处理。该方法构造出 ExceptionHandlingConfigurer异常处理配置类。

然后该类呢有提供了两个接口用于我们自定义异常处理:

  • AuthenticationEntryPoint 该类用来统一处理 AuthenticationException异常(403异常)
  • AccessDeniedHandler 该类用来统一处理 AccessDeniedException异常(401异常)
    ​

MyAuthenticationEntryPoint.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
Java复制代码/**401异常处理
* @author 赖柄沣 bingfengdev@aliyun.com
* @version 1.0
* @date 2020/9/3 22:08
*/
public class MyAuthenticationEntryPoint implements AuthenticationEntryPoint {

@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
response.setContentType("application/json;charset=utf-8");

response.setStatus(200);
Map<String, Object> map = new HashMap<>();
map.put("code", HttpServletResponse.SC_UNAUTHORIZED);
map.put("msg","令牌已过期请重新登录");

ServletOutputStream out = response.getOutputStream();
String s = new ObjectMapper().writeValueAsString(map);
byte[] bytes = s.getBytes();
out.write(bytes);
}
}

MyAccessDeniedHandler.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
java复制代码/**403异常处理
* @author 赖柄沣 bingfengdev@aliyun.com
* @version 1.0
* @date 2020/9/3 22:11
*/
public class MyAccessDeniedHandler implements AccessDeniedHandler {

@Override
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
response.setContentType("application/json;charset=utf-8");
response.setStatus(200);
Map<String, Object> map = new HashMap<>();
map.put("code", HttpServletResponse.SC_FORBIDDEN);
map.put("msg","未授权访问此资源,如有需要请联系管理员授权");
ServletOutputStream out = response.getOutputStream();
String s = new ObjectMapper().writeValueAsString(map);
byte[] bytes = s.getBytes();
out.write(bytes);
}
}

将这两个类添加到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
Java复制代码/**spring security配置类
* @author 赖柄沣 bingfengdev@aliyun.com
* @version 1.0
* @date 2020/9/3 15:41
*/
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SpringSecurityConfig extends WebSecurityConfigurerAdapter {

@Autowired
private UserDetailsService userService;

@Autowired
private AuthServerRsaKeyProperties properties;

@Bean
public BCryptPasswordEncoder myPasswordEncoder(){
return new BCryptPasswordEncoder();
}


/**配置自定义过滤器
* @author 赖柄沣 bingfengdev@aliyun.com
* @date 2020-09-03 15:53:45
* @param http
* @version 1.0
*/
@Override
protected void configure(HttpSecurity http) throws Exception {

//其他代码。。。

//添加自定义异常处理
http.exceptionHandling().authenticationEntryPoint(new MyAuthenticationEntryPoint());
http.exceptionHandling().accessDeniedHandler(new MyAccessDeniedHandler());

//其他代码1


}
}

到这一步大家就可以运行启动类先进行测试一下。在本文当中就先将product-service模块也实现了再集中测试

创建子系统模块product-service

修改pom.xml文件

这一步和我们创建认证服务时相差无几。

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
xml复制代码<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>pers.lbf</groupId>
<artifactId>springboot-springSecurity-jwt-rsa</artifactId>
<version>1.0.0-SNAPSHOT</version>
</parent>

<artifactId>product-service</artifactId>
<version>1.0.0-SNAPSHOT</version>
<name>product-service</name>
<description>Demo project for Spring Boot</description>

<properties>
<java.version>1.8</java.version>
</properties>

<dependencies>
<dependency>
<groupId>pers.lbf</groupId>
<artifactId>common</artifactId>
</dependency>
<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.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
</dependency>

<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>

<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>

</project>
修改application.yml配置文件

这里主要是配置数据库信息和加入公钥的地址信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
yaml复制代码server:
port: 8082
spring:
datasource:
url: jdbc:mysql://127.0.0.1:3306/security_authority?useSSL=false&serverTimezone=GMT
username: root
password: root1997
driver-class-name: com.mysql.cj.jdbc.Driver
mybatis:
configuration:
map-underscore-to-camel-case: true
logging:
level:
pers.lbf: debug
lbf:
key:
publicKeyPath: 你的公钥地址
创建读取公钥的配置类
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
Java复制代码/**读取公钥配置类
* @author 赖柄沣 bingfengdev@aliyun.com
* @version 1.0
* @date 2020/9/4 10:05
*/
@ConfigurationProperties(prefix = "lbf.key")
@ConstructorBinding
public class ProductRsaKeyProperties {

private String publicKeyPath;
private PublicKey publicKey;

@PostConstruct
public void loadKey() throws Exception {
publicKey = RsaUtils.getPublicKey(publicKeyPath);
}

@Override
public String toString() {
return "ProductRsaKeyProperties{" +
"pubKeyPath='" + publicKeyPath + '\'' +
", publicKey=" + publicKey +
'}';
}

public String getPublicKeyPath() {
return publicKeyPath;
}

public void setPublicKeyPath(String publicKeyPath) {
this.publicKeyPath = publicKeyPath;
}

public PublicKey getPublicKey() {
return publicKey;
}

public void setPublicKey(PublicKey publicKey) {
this.publicKey = publicKey;
}
}
修改启动类

这一步和创建认证服务器时一样,如要是加入公钥配置和mapper扫描

1
2
3
4
5
6
7
8
9
10
11
12
13
java复制代码/**
* @author Ferryman
*/
@SpringBootApplication
@MapperScan(basePackages = "pers.lbf.ssjr.productservice.dao")
@EnableConfigurationProperties(ProductRsaKeyProperties.class)
public class ProductServiceApplication {

public static void main(String[] args) {
SpringApplication.run(ProductServiceApplication.class, args);
}

}
复制

这一步主要是将UserAuthVo、自定义校验器、自定义异常处理器和自定义反序列化器从认证服务模块复制过来。(之所以不放入到公共模块common中是因为。不想直接在common模块中引入springBoot整合spring security的依赖)

创建子模块spring security配置类

这里也只需要在认证服务模块的配置上修改即可,去掉自定义认证过滤器的内容。资源模块只负责校验,不做认证。

创建一个测试接口
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
java复制代码/**
* @author 赖柄沣 bingfengdev@aliyun.com
* @version 1.0
* @date 2020/8/27 20:02
*/
@RestController
@RequestMapping("/product")
public class ProductController {


@GetMapping("/get")
@PreAuthorize("hasAuthority('product:get')")
public String get() {
return "产品信息接口调用成功!";
}
}

第三步:启动项目,进行测试

登录(认证)操作

登录成功返回消息提示

![](F:\01_Java全栈工程师学习&笔记&资料\22ssm综合练习和svn  adminlte  springSecurity的使用\springboot整合spring security(分布式版).assets/02_登录成功(1).png)

并且可以在请求头中看到token

![](F:\01_Java全栈工程师学习&笔记&资料\22ssm综合练习和svn  adminlte  springSecurity的使用\springboot整合spring security(分布式版).assets/02_登录成功(2).png)

登陆失败提示”用户名或密码错误”

![](F:\01_Java全栈工程师学习&笔记&资料\22ssm综合练习和svn  adminlte  springSecurity的使用\springboot整合spring security(分布式版).assets/04_登录失败.png)

访问资源

携带令牌访问资源,且具备权限、令牌未过期

![](F:\01_Java全栈工程师学习&笔记&资料\22ssm综合练习和svn  adminlte  springSecurity的使用\springboot整合spring security(分布式版).assets/05_携带toekn访问资源.png)

携带token访问资源。但是没有权限

![](F:\01_Java全栈工程师学习&笔记&资料\22ssm综合练习和svn  adminlte  springSecurity的使用\springboot整合spring security(分布式版).assets/07_未授权的访问.png)

未携带token访问(未登录、未经过认证)

![](F:\01_Java全栈工程师学习&笔记&资料\22ssm综合练习和svn  adminlte  springSecurity的使用\springboot整合spring security(分布式版).assets/06_未携带toekn访问资源.png)

携带过期令牌访问资源

![](F:\01_Java全栈工程师学习&笔记&资料\22ssm综合练习和svn  adminlte  springSecurity的使用\springboot整合spring security(分布式版).assets/08_令牌过期.png)

写在最后

springBoot整合security实现权限管理与认证分布式版(前后端分离版)的的核心在于三个问题

  1. 禁用了session,用户信息保存在哪?
  2. 如何实现对访问者的认证,或者说是根据token去认证访问者?
  3. 如何实现对访问者的校验,或者说是根据token去校验访问者身份?

基本上我们解决了上面三个问题之后,springBoot整合spring security实现前后端分离(分布式)场景下的权限管理与认证问题我们就可以说是基本解决了。

**代码以及sql脚本下载方式:**微信搜索关注公众号【Java开发实践】,回复20200904即可得到下载链接。

本文转载自: 掘金

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

从零搭建高逼格的监控报警系统 前言 Prometheus A

发表于 2020-09-04

如果觉得文章有用或写得好,还请在左边点个赞哦!

本文已收录到个人博客:geekvic.top ,欢迎来撩!

前言

业务中是否经常遇到服务器负载过高问题,或者经常碰到后台服务挂掉,却没有自动提醒功能,因此搭建一套监控报警系统势在必行。

Prometheus目前在开源社区相当活跃,在GitHub上拥有两万多Star,是当前最流行的监控系统,相比Zabbix,定制灵活度更高,而且Prometheus在云环境、容器支持这块优势明显。

Prometheus

简介

Prometheus是一套开源的监控&报警&时间序列数据库的组合,基于应用的metrics来进行监控的开源工具。

prometheus.png

下载&安装

  • 下载地址:prometheus.io/download/
  • 解压:tar zxvf prometheus-2.12.0.linux-amd64.tar.gz
  • 编辑: prometheus.yml,其中包括全局、alertmanager、告警规则、监控job配置,具体内容如下。
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
xml复制代码# my global config
global:
scrape_interval: 15s # Set the scrape interval to every 15 seconds. Default is every 1 minute.
evaluation_interval: 15s # Evaluate rules every 15 seconds. The default is every 1 minute.
# scrape_timeout is set to the global default (10s).

# Alertmanager configuration
alerting:
alertmanagers:
- static_configs:
- targets:
- 192.168.88.69:9093

# Load rules once and periodically evaluate them according to the global 'evaluation_interval'.
rule_files:
- "test_rules.yml"
# - "second_rules.yml"

# A scrape configuration containing exactly one endpoint to scrape:
# Here it's Prometheus itself.
scrape_configs:
# The job name is added as a label `job=<job_name>` to any timeseries scraped from this config.
- job_name: 'prometheus'

# metrics_path defaults to '/metrics'
# scheme defaults to 'http'.

static_configs:
- targets: ['192.168.88.69:9090']

- job_name: 'monitor'
scrape_interval: 5s
metrics_path: '/actuator/prometheus'
static_configs:
- targets: ['192.168.88.69:8008']

- job_name: 'node-exporter'
static_configs:
- targets: ['192.168.88.69:9100']
  • 启动:./prometheus &
  • 验证安装:访问地址:http://192.168.88.69:9090/targets

Spring Boot集成Prometheus

配置pom文件

1
2
3
4
5
6
7
8
9
xml复制代码<!--监控-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-registry-prometheus</artifactId>
</dependency>

配置yml

1
2
3
4
5
6
7
8
9
10
11
12
13
yaml复制代码server:
port: 8008
spring:
application:
name: monitor
management:
endpoints:
web:
exposure:
include: '*'
metrics:
tags:
application: ${spring.application.name}

添加配置类

1
2
3
4
5
6
7
java复制代码@Configuration
public class MeterRegistryConfig {
@Bean
MeterRegistryCustomizer<MeterRegistry> configurer(@Value("${spring.application.name}") String applicationName) {
return (registry) -> registry.config().commonTags("application", applicationName);
}
}

AlertManager

简介

Alertmanager 对收到的告警信息进行处理,包括去重,降噪,分组,策略路由告警通知。

配置

修改alertmanager.yml,当前配置的是邮箱告警,当然还支持企业微信、钉钉等,内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
yaml复制代码global:
resolve_timeout: 5m
smtp_smarthost: 'smtp.mxhichina.com:25' # smtp地址
smtp_from: 'test@163.com' # 发送邮箱地址
smtp_auth_username: 'test@163.com' # 邮箱用户
smtp_auth_password: '123456' # 邮箱密码

route:
group_by: ["instance"] # 分组名
group_wait: 10s # 当收到告警的时候,等待十秒看是否还有告警,如果有就一起发出去
igroup_interval: 10s # 发送警告间隔时间
repeat_interval: 1h # 重复报警的间隔时间
receiver: mail # 全局报警组,这个参数是必选的,和下面报警组名要相同

receivers:
- name: 'mail' # 报警组名
email_configs:
- to: 'receiver@163.com' # 收件人邮箱
headers: {Subject: "告警测试邮件"}

启动

命令:./alertmanager & ,端口号:9093

Grafana

简介

Grafana是一款用Go语言开发的开源数据可视化工具,可以做数据监控和数据统计,带有告警功能。

配置

  1. 解压grafana-6.3.5.linux-amd64.tar.gz,启动 ./grafana-server &,访问地址http://192.168.88.69:3000
  2. 配置Data Sources
    Kafana.png
  3. 安装exporter,如要监控服务器的运行状态,需要安装node_exporter,并启动项目,端口号:9100,并在prometheus里配置节点,并重启prometheus。
  4. 导入模板,可以在Grafana官网找下,地址:grafana.com/grafana/das…。

node_exporter.png

左手敲键盘,右手投篮,一个爱好篮球的码农~

同步个人博客:geekvic.top/post/c79551…

声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!

本文转载自: 掘金

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

5000字 24张图带你彻底理解Java中的21种锁

发表于 2020-09-03

本篇主要内容如下:

本篇主要内容

本篇文章已收纳到我的Java在线文档、 Github

我的SpringCloud实战项目持续更新中

帮你总结好的锁:

序号 锁名称 应用
1 乐观锁 CAS
2 悲观锁 synchronized、vector、hashtable
3 自旋锁 CAS
4 可重入锁 synchronized、Reentrantlock、Lock
5 读写锁 ReentrantReadWriteLock,CopyOnWriteArrayList、CopyOnWriteArraySet
6 公平锁 Reentrantlock(true)
7 非公平锁 synchronized、reentrantlock(false)
8 共享锁 ReentrantReadWriteLock中读锁
9 独占锁 synchronized、vector、hashtable、ReentrantReadWriteLock中写锁
10 重量级锁 synchronized
11 轻量级锁 锁优化技术
12 偏向锁 锁优化技术
13 分段锁 concurrentHashMap
14 互斥锁 synchronized
15 同步锁 synchronized
16 死锁 相互请求对方的资源
17 锁粗化 锁优化技术
18 锁消除 锁优化技术

1、乐观锁

乐观锁

乐观锁是一种乐观思想,假定当前环境是读多写少,遇到并发写的概率比较低,读数据时认为别的线程不会正在进行修改(所以没有上锁)。写数据时,判断当前 与期望值是否相同,如果相同则进行更新(更新期间加锁,保证是原子性的)。

Java中的乐观锁: CAS,比较并替换,比较当前值(主内存中的值),与预期值(当前线程中的值,主内存中值的一份拷贝)是否一样,一样则更新,否则继续进行CAS操作。

如上图所示,可以同时进行读操作,读的时候其他线程不能进行写操作。

2、悲观锁

悲观锁

悲观锁是一种悲观思想,即认为写多读少,遇到并发写的可能性高,每次去拿数据的时候都认为其他线程会修改,所以每次读写数据都会认为其他线程会修改,所以每次读写数据时都会上锁。其他线程想要读写这个数据时,会被这个线程block,直到这个线程释放锁然后其他线程获取到锁。

Java中的悲观锁: synchronized修饰的方法和方法块、ReentrantLock。

如上图所示,只能有一个线程进行读操作或者写操作,其他线程的读写操作均不能进行。

3、自旋锁

mark

自旋锁是一种技术: 为了让线程等待,我们只须让线程执行一个忙循环(自旋)。

现在绝大多数的个人电脑和服务器都是多路(核)处理器系统,如果物理机器有一个以上的处理器或者处理器核心,能让两个或以上的线程同时并行执行,就可以让后面请求锁的那个线程“稍等一会”,但不放弃处理器的执行时间,看看持有锁的线程是否很快就会释放锁。

自旋锁的优点: 避免了线程切换的开销。挂起线程和恢复线程的操作都需要转入内核态中完成,这些操作给Java虚拟机的并发性能带来了很大的压力。

自旋锁的缺点: 占用处理器的时间,如果占用的时间很长,会白白消耗处理器资源,而不会做任何有价值的工作,带来性能的浪费。因此自旋等待的时间必须有一定的限度,如果自旋超过了限定的次数仍然没有成功获得锁,就应当使用传统的方式去挂起线程。

**自旋次数默认值:**10次,可以使用参数-XX:PreBlockSpin来自行更改。

自适应自旋: 自适应意味着自旋的时间不再是固定的,而是由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定的。有了自适应自旋,随着程序运行时间的增长及性能监控信息的不断完善,虚拟机对程序锁的状态预测就会越来越精准。

Java中的自旋锁: CAS操作中的比较操作失败后的自旋等待。

4、可重入锁(递归锁)

可重入锁

可重入锁是一种技术: 任意线程在获取到锁之后能够再次获取该锁而不会被锁所阻塞。

可重入锁的原理: 通过组合自定义同步器来实现锁的获取与释放。

  • 再次获取锁:识别获取锁的线程是否为当前占据锁的线程,如果是,则再次成功获取。获取锁后,进行计数自增,
  • 释放锁:释放锁时,进行计数自减。

Java中的可重入锁: ReentrantLock、synchronized修饰的方法或代码段。

可重入锁的作用: 避免死锁。

面试题1: 可重入锁如果加了两把,但是只释放了一把会出现什么问题?

答:程序卡死,线程不能出来,也就是说我们申请了几把锁,就需要释放几把锁。

面试题2: 如果只加了一把锁,释放两次会出现什么问题?

答:会报错,java.lang.IllegalMonitorStateException。

5、读写锁

读写锁是一种技术: 通过ReentrantReadWriteLock类来实现。为了提高性能, Java 提供了读写锁,在读的地方使用读锁,在写的地方使用写锁,灵活控制,如果没有写锁的情况下,读是无阻塞的,在一定程度上提高了程序的执行效率。 读写锁分为读锁和写锁,多个读锁不互斥,读锁与写锁互斥,这是由 jvm 自己控制的。

读锁: 允许多个线程获取读锁,同时访问同一个资源。

读锁

写锁: 只允许一个线程获取写锁,不允许同时访问同一个资源。

写锁

如何使用:

1
2
3
4
5
java复制代码/**
* 创建一个读写锁
* 它是一个读写融为一体的锁,在使用的时候,需要转换
*/
private ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();

获取读锁和释放读锁

1
2
3
4
5
java复制代码// 获取读锁
rwLock.readLock().lock();

// 释放读锁
rwLock.readLock().unlock();

获取写锁和释放写锁

1
2
3
4
5
java复制代码// 创建一个写锁
rwLock.writeLock().lock();

// 写锁 释放
rwLock.writeLock().unlock();

Java中的读写锁:ReentrantReadWriteLock

6、公平锁

公平锁

公平锁是一种思想: 多个线程按照申请锁的顺序来获取锁。在并发环境中,每个线程会先查看此锁维护的等待队列,如果当前等待队列为空,则占有锁,如果等待队列不为空,则加入到等待队列的末尾,按照FIFO的原则从队列中拿到线程,然后占有锁。

7、非公平锁

非公平锁

非公平锁是一种思想: 线程尝试获取锁,如果获取不到,则再采用公平锁的方式。多个线程获取锁的顺序,不是按照先到先得的顺序,有可能后申请锁的线程比先申请的线程优先获取锁。

优点: 非公平锁的性能高于公平锁。

缺点: 有可能造成线程饥饿(某个线程很长一段时间获取不到锁)

**Java中的非公平锁:**synchronized是非公平锁,ReentrantLock通过构造函数指定该锁是公平的还是非公平的,默认是非公平的。

8、共享锁

共享锁

共享锁是一种思想: 可以有多个线程获取读锁,以共享的方式持有锁。和乐观锁、读写锁同义。

Java中用到的共享锁: ReentrantReadWriteLock。

9、独占锁

独占锁

独占锁是一种思想: 只能有一个线程获取锁,以独占的方式持有锁。和悲观锁、互斥锁同义。

Java中用到的独占锁: synchronized,ReentrantLock

10、重量级锁

重量级锁

重量级锁是一种称谓: synchronized是通过对象内部的一个叫做监视器锁(monitor)来实现的,监视器锁本身依赖底层的操作系统的 Mutex Lock来实现。操作系统实现线程的切换需要从用户态切换到核心态,成本非常高。这种依赖于操作系统 Mutex Lock来实现的锁称为重量级锁。为了优化synchonized,引入了轻量级锁,偏向锁。

Java中的重量级锁: synchronized

11、轻量级锁

轻量级锁

轻量级锁是JDK6时加入的一种锁优化机制: 轻量级锁是在无竞争的情况下使用CAS操作去消除同步使用的互斥量。轻量级是相对于使用操作系统互斥量来实现的重量级锁而言的。轻量级锁在没有多线程竞争的前提下,减少传统的重量级锁使用操作系统互斥量产生的性能消耗。如果出现两条以上的线程争用同一个锁的情况,那轻量级锁将不会有效,必须膨胀为重量级锁。

优点: 如果没有竞争,通过CAS操作成功避免了使用互斥量的开销。

缺点: 如果存在竞争,除了互斥量本身的开销外,还额外产生了CAS操作的开销,因此在有竞争的情况下,轻量级锁比传统的重量级锁更慢。

12、偏向锁

偏向锁

偏向锁是JDK6时加入的一种锁优化机制: 在无竞争的情况下把整个同步都消除掉,连CAS操作都不去做了。偏是指偏心,它的意思是这个锁会偏向于第一个获得它的线程,如果在接下来的执行过程中,该锁一直没有被其他的线程获取,则持有偏向锁的线程将永远不需要再进行同步。持有偏向锁的线程以后每次进入这个锁相关的同步块时,虚拟机都可以不再进行任何同步操作(例如加锁、解锁及对Mark Word的更新操作等)。

优点: 把整个同步都消除掉,连CAS操作都不去做了,优于轻量级锁。

缺点: 如果程序中大多数的锁都总是被多个不同的线程访问,那偏向锁就是多余的。

13、分段锁

分段锁

分段锁是一种机制: 最好的例子来说明分段锁是ConcurrentHashMap。

**ConcurrentHashMap原理:**它内部细分了若干个小的 HashMap,称之为段(Segment)。 默认情况下一个 ConcurrentHashMap 被进一步细分为 16 个段,既就是锁的并发度。如果需要在 ConcurrentHashMap 添加一项key-value,并不是将整个 HashMap 加锁,而是首先根据 hashcode 得到该key-value应该存放在哪个段中,然后对该段加锁,并完成 put 操作。在多线程环境中,如果多个线程同时进行put操作,只要被加入的key-value不存放在同一个段中,则线程间可以做到真正的并行。

**线程安全:**ConcurrentHashMap 是一个 Segment 数组, Segment 通过继承ReentrantLock 来进行加锁,所以每次需要加锁的操作锁住的是一个 segment,这样只要保证每个 Segment 是线程安全的,也就实现了全局的线程安全

14、互斥锁

互斥锁

互斥锁与悲观锁、独占锁同义,表示某个资源只能被一个线程访问,其他线程不能访问。

  • 读-读互斥
  • 读-写互斥
  • 写-读互斥
  • 写-写互斥

Java中的同步锁: synchronized

15、同步锁

同步锁

同步锁与互斥锁同义,表示并发执行的多个线程,在同一时间内只允许一个线程访问共享数据。

Java中的同步锁: synchronized

16、死锁

死锁

**死锁是一种现象:**如线程A持有资源x,线程B持有资源y,线程A等待线程B释放资源y,线程B等待线程A释放资源x,两个线程都不释放自己持有的资源,则两个线程都获取不到对方的资源,就会造成死锁。

Java中的死锁不能自行打破,所以线程死锁后,线程不能进行响应。所以一定要注意程序的并发场景,避免造成死锁。

17、锁粗化

锁粗化

锁粗化是一种优化技术: 如果一系列的连续操作都对同一个对象反复加锁和解锁,甚至加锁操作都是出现在循环体体之中,就算真的没有线程竞争,频繁地进行互斥同步操作将会导致不必要的性能损耗,所以就采取了一种方案:把加锁的范围扩展(粗化)到整个操作序列的外部,这样加锁解锁的频率就会大大降低,从而减少了性能损耗。

18、锁消除

锁消除

锁消除是一种优化技术: 就是把锁干掉。当Java虚拟机运行时发现有些共享数据不会被线程竞争时就可以进行锁消除。

那如何判断共享数据不会被线程竞争?

利用逃逸分析技术:分析对象的作用域,如果对象在A方法中定义后,被作为参数传递到B方法中,则称为方法逃逸;如果被其他线程访问,则称为线程逃逸。

在堆上的某个数据不会逃逸出去被其他线程访问到,就可以把它当作栈上数据对待,认为它是线程私有的,同步加锁就不需要了。

19、synchronized

synchronized

synchronized是Java中的关键字:用来修饰方法、对象实例。属于独占锁、悲观锁、可重入锁、非公平锁。

  • 1.作用于实例方法时,锁住的是对象的实例(this);
  • 2.当作用于静态方法时,锁住的是 Class类,相当于类的一个全局锁,
    会锁所有调用该方法的线程;
  • 3.synchronized 作用于一个非 NULL的对象实例时,锁住的是所有以该对象为锁的代码块。 它有多个队列,当多个线程一起访问某个对象监视器的时候,对象监视器会将这些线程存储在不同的容器中。

每个对象都有个 monitor 对象, 加锁就是在竞争 monitor 对象,代码块加锁是在代码块前后分别加上 monitorenter 和 monitorexit 指令来实现的,方法加锁是通过一个标记位来判断的。

20、Lock和synchronized的区别

自动挡和手动挡的区别

Lock: 是Java中的接口,可重入锁、悲观锁、独占锁、互斥锁、同步锁。

  • 1.Lock需要手动获取锁和释放锁。就好比自动挡和手动挡的区别
  • 2.Lock 是一个接口,而 synchronized 是 Java 中的关键字, synchronized 是内置的语言实现。
  • 3.synchronized 在发生异常时,会自动释放线程占有的锁,因此不会导致死锁现象发生;而 Lock 在发生异常时,如果没有主动通过 unLock()去释放锁,则很可能造成死锁现象,因此使用 Lock 时需要在 finally 块中释放锁。
  • 4.Lock 可以让等待锁的线程响应中断,而 synchronized 却不行,使用 synchronized 时,等待的线程会一直等待下去,不能够响应中断。
  • 5.通过 Lock 可以知道有没有成功获取锁,而 synchronized 却无法办到。
  • 6.Lock 可以通过实现读写锁提高多个线程进行读操作的效率。

synchronized的优势:

  • 足够清晰简单,只需要基础的同步功能时,用synchronized。
  • Lock应该确保在finally块中释放锁。如果使用synchronized,JVM确保即使出现异常,锁也能被自动释放。
  • 使用Lock时,Java虚拟机很难得知哪些锁对象是由特定线程锁持有的。

21、ReentrantLock 和synchronized的区别

Lock、ReentrantLock、shnchronzied

ReentrantLock是Java中的类 : 继承了Lock类,可重入锁、悲观锁、独占锁、互斥锁、同步锁。

划重点

相同点:

  • 1.主要解决共享变量如何安全访问的问题
  • 2.都是可重入锁,也叫做递归锁,同一线程可以多次获得同一个锁,
  • 3.保证了线程安全的两大特性:可见性、原子性。

不同点:

  • 1.ReentrantLock 就像手动汽车,需要显示的调用lock和unlock方法, synchronized 隐式获得释放锁。
  • 2.ReentrantLock 可响应中断, synchronized 是不可以响应中断的,ReentrantLock 为处理锁的不可用性提供了更高的灵活性
  • 3.ReentrantLock 是 API 级别的, synchronized 是 JVM 级别的
  • 4.ReentrantLock 可以实现公平锁、非公平锁,默认非公平锁,synchronized 是非公平锁,且不可更改。
  • 5.ReentrantLock 通过 Condition 可以绑定多个条件

彩蛋: 讲了那么多锁,都跟阻塞相关,宝宝想听阻塞呀!

我是悟空,一只努力变强的码农!我要变身超级赛亚人啦!

我的资料

你好,我是悟空哥,7年项目开发经验,全栈工程师,开发组长,超喜欢图解编程底层原理。正在编写两本PDF,分别是 1、Spring Cloud实战项目(佳必过),2、Java并发必知必会。我还手写了2个小程序,Java刷题小程序,PMP刷题小程序,点击我的公众号菜单打开!另外有111本架构师资料以及1000道Java面试题,都整理成了PDF,可以关注公众号 悟空聊架构 回复 悟空 领取优质资料。

**转发->在看->点赞->收藏->评论!!!**是对我最大的支持!

《Java并发必知必会》系列:

1.反制面试官 | 14张原理图 | 再也不怕被问 volatile!

2.程序员深夜惨遭老婆鄙视,原因竟是CAS原理太简单?

3.用积木讲解ABA原理 | 老婆居然又听懂了!

4.全网最细 | 21张图带你领略集合的线程不安全

本文转载自: 掘金

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

跳跃表的原理和实现(Java) 一、高效查找算法 二、跳跃表

发表于 2020-09-02

一、高效查找算法

我们在实际开发中经常会有在一堆数据中查找一个指定数据的需求,而常用的支持高效查找算法的实现方式有以下几种:

  1. 有序数组:这种方式的存储结构,优点是支持数据的随机访问,并且可以采用二分查找算法降低查找操作的复杂度。缺点同样很明显,插入和删除数据时,为了保持元素的有序性,需要进行大量的移动数据的操作。
  2. 二叉查找树:如果需要一个既支持高效的二分查找算法,又能快速的进行插入和删除操作的数据结构,那首先就是二叉查找树莫属了。缺点是在某些极端情况下,二叉查找树有可能变成一个线性链表。
  3. 平衡二叉树:二叉树表示不服,于是基于二叉查找树的优点,对其缺点进行改进,引入了平衡的概念。根据平衡算法的不同,具体实现有AVL树、B树(B-Tree)、B+树(B+Tree)、红黑树 等等。但是平衡二叉树的实现多数比较复杂,较难理解。
  4. 跳跃表:同样支持对数据进行高效的查找,插入和删除数据操作也比较简单,最重要的就是实现比较平衡二叉树真是轻量几个数量级。缺点就是存在一定数据冗余。

二、跳跃表

跳跃表(SkipList)是一种可以替代平衡树的数据结构。跳跃表让已排序的数据分布在多层次的链表结构中,默认是将 Key 值升序排列的,以 0-1 的随机值决定一个数据是否能够攀升到高层次的链表中。它通过容许一定的数据冗余,达到 “以空间换时间” 的目的。

跳跃表的效率和 AVL 相媲美,查找、添加、插入、删除操作都能够在 O(LogN) 的复杂度内完成。讲了那么多,下面就直接进入主题,详细的看一看跳跃表是怎么实现的。

三、跳跃表的实现

上面这张图就是一个跳跃表的实例,先说一下跳跃表的构造特征:

  • 一个跳跃表应该有若干个层(Level)链表组成;
  • 跳跃表中最底层的链表包含所有数据; 每一层链表中的数据都是有序的;
  • 如果一个元素 X 出现在第i层,那么编号比 i 小的层都包含元素 X;
  • 第 i 层的元素通过一个指针指向下一层拥有相同值的元素;
  • 在每一层中,-∞ 和 +∞ 两个元素都出现(分别表示 INT_MIN 和 INT_MAX);
  • 头指针(head)指向最高一层的第一个元素;

1、定义链表中节点的模型

image.png

Java 代码实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
java复制代码public class SkipListEntry<T> {
// data
public Integer key;
public T value;

// links
public SkipListEntry up;
public SkipListEntry down;
public SkipListEntry left;
public SkipListEntry right;

// constructor
public SkipListEntry(Integer key, T value) {
this.key = key;
this.value = value;
}

// methods...
}

可以看到节点模型主要分为2个部分。

data 部分包含具体的存储数据,这里为了不引入其他杂乱的问题,使用 Integer 作为 key 的类型,Object 作为value 的类型。

links 部分包含4个指针,分别是 up、down、left、right,单从名字上就能够明白它们的作用。

2、跳跃表本身的模型

1
2
3
4
5
6
7
8
9
10
11
12
13
arduino复制代码public class SkipList {
// 节点数量
public int n;
// 节点最大层数
public int h;

// 第一个节点
SkipListEntry head;
// 最后一个节点
SkipListEntry tail;

public Random r;
}

Note: Random 类的实例对象 r 用来决定新添加的节点是否能够向更高一层的链表攀升。

3、初始化跳跃表的实例

构造函数将初始化一个空的跳跃表看起来像下面这样:
image.png

构造函数的 Java 代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
ini复制代码public SkipList() {
// 创建 head 节点
this.head = new SkipListEntry(Integer.MIN_VALUE, null);
// 创建 tail 节点
this.tail = new SkipListEntry(Integer.MAX_VALUE, null);

// 将 head 节点的右指针指向 tail 节点
this.head.right = tail;
// 将 tail 节点的左指针指向 head 节点
this.tail.left = head;

this.h = 0;
this.n = 0;
this.r = new Random();
}

4、基本操作

跳跃表需要实现查找、插入、移除这些基本操作:

  • get(Integer key) : 根据 key 值查找某个元素
  • put(Integer key, Object value) :插入一个新的元素,元素已存在时为修改操作
  • remove(Integer key): 根据 key 值删除某个元素

虽然看似是 3 个不同的操作,但是究其本质,要实现这 3 个操作,都得先找到某个元素或是定位到一个元素,好在下一个位子插入新元素。那么,我们就先把这个 findEntry 的方法实现吧。

image.png

上面的图示使用紫色的箭头画出了在一个 SkipList 中查找 key 值 50 的过程。简述如下:

  1. 从 head 出发,因为 head 指向最顶层(top level)链表的开始节点,相当于从顶层开始查找;
  2. 移动到当前节点的右指针(right)指向的节点,直到右节点的 key 值大于要查找的 key 值时停止;
  3. 如果还有更低层次的链表,则移动到当前节点的下一层节点(down),如果已经处于最底层,则退出;

重复第 2 步和第 3 步,直到查找到 key 值所在的节点,或者不存在而退出查找;

Java 代码实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
java复制代码private SkipListEntry findEntry(Integer key) {
// 从head头节点开始查找
SkipListEntry p = head;

while (true) {
// 从左向右查找,直到右节点的key值大于要查找的key值
while (p.right.key <= key) {
p = p.right;
}

// 如果有更低层的节点,则向低层移动
if (p.down != null) {
p = p.down;
} else {
break;
}
}

// 返回p,!注意这里p的key值是小于等于传入key的值的(p.key <= key)
return p;
}

注意以下几点:

  1. 如果传入的 key 值在跳跃表中存在,则 findEntry 返回该对象的底层节点;
  2. 如果传入的 key 值在跳跃表中不存在,则 findEntry 返回跳跃表中 key 值小于 key,并且 key 值相差最小的底层节点;

示例,在跳跃表中查找 key=42 的元素节点,将返回 key=39 的节点。如下图所示:
image.png

基于 findEntry 方法,我们就能很容易的实现前面所说的一些操作了。

get方法

1
2
3
4
5
6
7
8
9
java复制代码public Object get(Integer key) {
SkipListEntry p = findEntry(key);

if (p.key.equals(key)) {
return p.value;
} else {
return null;
}
}

put方法

一些需要注意的步骤:

  1. 如果 put 的 key 值在跳跃表中存在,则进行修改操作;
  2. 如果 put 的 key 值在跳跃表中不存在,则需要进行新增节点的操作,并且需要由 random 随机数决定新加入的节点的高度(最大level);
  3. 当新添加的节点高度达到跳跃表的最大 level,需要添加一个空白层(除了-oo 和 +oo 没有别的节点)

下面我们一步一步的通过图示看一下插入节点的过程:

第一步,查找适合插入的位子

第二步,在查找到的p节点后面插入新增的节点q

image.png

第三步,重复下面的操作,使用随机数决定新增节点的高度

  1. 从p节点开始,向左移动,直到找到含有更高level节点的节点;
  2. 将p指针向上移动一个level;
  3. 创建一个和q节点data一样的节点,插入位子在跳跃表中p的右方和q的上方;
  4. 直到随机数不满足向上攀升的条件为止;

图示如下:

只要随机数满足条件,key=42 的节点就会一直向上攀升,直到它的 level 等于跳跃表的高度(height)。这个时候我们需要在跳跃表的最顶层添加一个空白层,同时跳跃表的 height+1,以满足下一次新增节点的操作。

Java代码实现如下:

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
java复制代码public Object put(Integer key, Object value) {

SkipListEntry p, q;
int i = 0;

// 查找适合插入的位子
p = findEntry(key);

// 如果跳跃表中存在含有key值的节点,则进行value的修改操作即可完成
if (p.key.equals(key)) {
Object oldValue = p.value;
p.value = value;
return oldValue;
}

// 如果跳跃表中不存在含有key值的节点,则进行新增操作
q = new SkipListEntry(key, value);
q.left = p;
q.right = p.right;
p.right.left = q;
p.right = q;

// 再使用随机数决定是否要向更高level攀升
while (r.nextDouble() < 0.5) {

// 如果新元素的级别已经达到跳跃表的最大高度,则新建空白层
if (i >= h) {
addEmptyLevel();
}

// 从p向左扫描含有高层节点的节点
while (p.up == null) {
p = p.left;
}
p = p.up;

// 新增和q指针指向的节点含有相同key值的节点对象
// 这里需要注意的是除底层节点之外的节点对象是不需要value值的
SkipListEntry z = new SkipListEntry(key, null);

z.left = p;
z.right = p.right;
p.right.left = z;
p.right = z;

z.down = q;
q.up = z;

q = z;
i = i + 1;
}

n = n + 1;

// 返回null,没有旧节点的value值
return null;
}

remove方法

删除节点的操作相对 put 就比较简单了,首先查找到包含 key 值的节点,将节点从链表中移除,接着如果有更高 level 的节点,则 repeat 这个操作即可。
Java代码实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
java复制代码public Object remove(Integer key) {
SkipListEntry p, q;

p = findEntry(key);

if (!p.key.equals(key)) {
return null;
}

Object oldValue = p.value;
while (p != null) {
q = p.up;
p.left.right = p.right;
p.right.left = p.left;
p = q;
}
return oldValue;
}

跳跃表的原理和实现到这里就结束了。

还有需要说明的一点是:跳跃表每次运行的结果是不一样的,这就是为什么说跳跃表是属于随机化数据结构。(Random的存在导致的)

四、跳跃表在Java中的应用

  • ConcurrentSkipListMap:在功能上对应HashTable、HashMap、TreeMap;
  • ConcurrentSkipListSet : 在功能上对应HashSet;

确切的说,SkipList 更像 Java 中的 TreeMap ,TreeMap 基于红黑树(一种自平衡二叉查找树)实现的,时间复杂度平均能达到 O(log n)。

HashMap 是基于散列表实现的,查找时间复杂度平均能达到 O(1)。ConcurrentSkipListMap 是基于跳跃表实现的,查找时间复杂度平均能达到 O(log n)。

ConcurrentSkipListMap 具有 SkipList 的性质 ,并且适用于大规模数据的并发访问。多个线程可以安全地并发执行插入、移除、更新和访问操作。与其他有锁机制的数据结构在巨大的压力下相比有优势。

TreeMap 插入数据时平衡树采用严格的旋转操作(比如平衡二叉树有左旋右旋)来保证平衡,因此 SkipList 比较容易实现,而且相比平衡树有着较高的运行效率。

本文转载自: 掘金

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

Java 带你理解 ServiceLoader 的原理与

发表于 2020-09-02

前言

  • ServiceLoader是Java提供的一套**SPI(Service Provider Interface,常译:服务发现)**框架,用于实现服务提供方与服务使用方解耦
  • 在这篇文章里,我将带你理解ServiceLoader的原理与设计思想,希望能帮上忙。请点赞,你的点赞和关注真的对我非常重要!

目录


  1. SPI 简介

  • 定义
  • 一个服务的注册与发现机制*
  • 作用
    通过解耦服务提供者与服务使用者,帮助实现模块化、组件化


  1. ServiceLoader 使用步骤

我们直接使用JDBC的例子,帮助各位建立起对ServiceLoader 的基本了解,具体如下:

我们都知道JDBC编程有五大基本步骤:

    1. 执行数据库驱动类加载(非必须):Class.forName("com.mysql.jdbc.driver")
    1. 连接数据库:DriverManager.getConnection(url, user, password)
    1. 创建SQL语句:Connection#.creatstatement();
    1. 执行SQL语句并处理结果集:Statement#executeQuery()
    1. 释放资源:ResultSet#close()、Statement#close()与Connection#close()

操作数据库需要使用厂商提供的数据库驱动程序,直接使用厂商的驱动耦合太强了,更推荐的方法是使用DriveManager管理类:

步骤1:定义服务接口

JDBC抽象出一个服务接口,数据库驱动实现类统一实现这个接口:

1
2
3
4
5
6
7
java复制代码public interface Driver {
// 创建数据库连接
Connection connect(String url, java.util.Properties info)
throws SQLException;

// 省略其他方法...
}

步骤2:实现服务接口

服务提供者(数据库厂商)提供一个或多个实现这个服务的类(驱动实现类),具体如下:

  • mysql:com.mysql.cj.jdbc.Driver.java
1
2
3
4
5
6
7
8
9
10
11
scala复制代码public class Driver extends NonRegisteringDriver implements java.sql.Driver {
static {
try {
// 注册驱动
java.sql.DriverManager.registerDriver(new Driver());
} catch (SQLException E) {
throw new RuntimeException("Can't register driver!");
}
}
// 省略...
}
  • oracle:oracle.jdbc.driver.OracleDriver.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
java复制代码public class OracleDriver implements Driver {
private static OracleDriver defaultDriver = null;
static {
try {
if (defaultDriver == null) {
//1. 单例
defaultDriver = new OracleDriver();
// 注册驱动
DriverManager.registerDriver(defaultDriver);
}
} catch (RuntimeException localRuntimeException) {
;
} catch (SQLException localSQLException) {
;
}
}

// 省略...
}

步骤3:注册实现类到配置文件

在java的同级目录中新建目录resources/META-INF/services,新建一个配置文件java.sql.Driver(文件名为服务接口的全限定名),文件中每一行是实现类的全限定名,例如:

1
复制代码com.mysql.cj.jdbc.Driver

我们可以解压mysql-connector-java-8.0.19.jar包,找到对应的META-INF文件夹。

步骤4:加载服务

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
csharp复制代码// DriverManager.java
static {
loadInitialDrivers();
}

private static void loadInitialDrivers() {
// 省略次要代码...
AccessController.doPrivileged(new PrivilegedAction<Void>() {
public Void run() {
// 使用ServiceLoader遍历实现类
ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
// 获得迭代器
Iterator<Driver> driversIterator = loadedDrivers.iterator();
// 迭代
try{
while(driversIterator.hasNext()) {
driversIterator.next();
// 疑问:为什么没有任何处理?
}
} catch(Throwable t) {
// Do nothing
}
return null;
}
});
// 省略次要代码...
}

可以看到,DriverManager的静态代码块调用loadInitialDrivers (),方法内部通过ServiceLoader提供的迭代器Iterator<Driver> 遍历了所有驱动实现类,但是为什么在迭代里没有任何操作呢?

1
2
3
4
scss复制代码while(driversIterator.hasNext()) {
driversIterator.next();
// 疑问:为什么没有任何处理?
}

在下一节,我们深入ServiceLoader的源码来解答这个问题。


  1. ServiceLoader 源码解析

# 提示

ServiceLoader中有一些源码使用了安全检测,如AccessController.doPrivileged(),在以下代码摘要中省略

  • 工厂方法
    ServiceLoader提供了三个静态泛型工厂方法,内部最终将调用ServiceLoader.load(Class,ClassLoader),具体如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
java复制代码// 1.
public static <S> ServiceLoader<S> loadInstalled(Class<S> service) {
// 使用双亲委派模型中最顶层的ClassLoader
ClassLoader cl = ClassLoader.getSystemClassLoader();
ClassLoader prev = null;
while (cl != null) {
prev = cl;
cl = cl.getParent();
}
return ServiceLoader.load(service, prev);
}

// 2.
public static <S> ServiceLoader<S> load(Class<S> service) {
// 使用线程上下文类加载器
ClassLoader cl = Thread.currentThread().getContextClassLoader();
return ServiceLoader.load(service, cl);
}

// 3.
public static <S> ServiceLoader<S> load(Class<S> service, ClassLoader loader){
return new ServiceLoader<>(service, loader);
}

可以看到,三个方法仅在传入的ClassLoader参数有区别,若还不了解ClassLoader,请务必阅读[《Java | 带你理解 ClassLoader 的原理与设计思想》](Editting…)

  • 构造方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
ini复制代码private final Class<S> service;
private LinkedHashMap<String,S> providers = new LinkedHashMap<>();

private ServiceLoader(Class<S> svc, ClassLoader cl) {
service = Objects.requireNonNull(svc, "Service interface cannot be null");
loader = (cl == null) ? ClassLoader.getSystemClassLoader() : cl;
reload();
}

public void reload() {
// 清空 providers
providers.clear();
// 实例化 LazyIterator
lookupIterator = new LazyIterator(service, loader);
}

可以看到,ServiceLoader的构造器中创建了LazyIterator迭代器的实例,这是一个“懒加载”的迭代器。那么这个迭代器在哪里使用的呢?继续往下看~

  • 外部迭代器
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
typescript复制代码private LazyIterator lookupIterator;

// 返回一个新的迭代器,包装了providers和lookupIterator
public Iterator<S> iterator() {
return new Iterator<S>() {

Iterator<Map.Entry<String,S>> knownProviders
= providers.entrySet().iterator();

public boolean hasNext() {
// 优先从knownProviders取
if (knownProviders.hasNext())
return true;
return lookupIterator.hasNext();
}

public S next() {
// 优先从knownProviders取
if (knownProviders.hasNext())
return knownProviders.next().getValue();
return lookupIterator.next();
}

public void remove() {
throw new UnsupportedOperationException();
}

};
}

可以看到,ServiceLoader里有一个泛型方法Iterator<S> iterator(),它包装了providers集合迭代器和lookupIterator两个迭代器,迭代过程中优先从providers获取元素。

为什么要优先从providers集合中取元素呢?阅读源码发现,LazyIterator#next()会将每轮迭代中取到的元素put到providers集合中,providers其实是LazyIterator的内存缓存。

  • 内部迭代器
# 提示

以下代码摘要中省略了源码中的try-catch

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
ini复制代码// ServiceLoader.java

private static final String PREFIX = "META-INF/services/";

private class LazyIteratorimplements Iterator<S> {

Class<S> service;
ClassLoader loader;
Enumeration<URL> configs = null;
Iterator<String> pending = null;
String nextName = null;

private LazyIterator(Class<S> service, ClassLoader loader) {
this.service = service;
this.loader = loader;
}

private boolean hasNextService() {
if (nextName != null) {
return true;
}
if (configs == null) {
// configs 未初始化才执行
// 配置文件:META-INF/services/服务接口的全限定名
String fullName = PREFIX + service.getName();
if (loader == null)
configs = ClassLoader.getSystemResources(fullName);
else
configs = loader.getResources(fullName);
}

while ((pending == null) || !pending.hasNext()) {
if (!configs.hasMoreElements()) {
return false;
}
// 分析点1:解析配置文件资源
pending = parse(service, configs.nextElement());
}
// nextName:下一个实现类的全限定名
nextName = pending.next();
return true;
}

private S nextService() {
if (!hasNextService()) throw new NoSuchElementException();
String cn = nextName;
nextName = null;
// 1. 使用类加载器loader加载
Class<?> c = Class.forName(cn, false, loader);
if (!service.isAssignableFrom(c)) {
ClassCastException cce = new ClassCastException(service.getCanonicalName() + " is not assignable from " + c.getCanonicalName());
fail(service, "Provider " + cn + " not a subtype", cce);
}
// 2. 根据Class实例化服务实现类
S p = service.cast(c.newInstance());
// 3. 服务实现类缓存到 providers
providers.put(cn, p);
return p;
}

public boolean hasNext() {
return hasNextService();
}

public S next() {
return nextService();
}

public void remove() {
throw new UnsupportedOperationException();
}
}
// 分析点1:解析配置文件资源,实现类的全限定名列表迭代器
private Iterator<String> parse(Class<?> service, URL u) throws ServiceConfigurationError {
// 使用 UTF-8 编码输入配置文件资源
InputStream in = u.openStream();
BufferedReader r = new BufferedReader(new InputStreamReader(in, "utf-8"));
ArrayList<String> names = new ArrayList<>();
int lc = 1;
while ((lc = parseLine(service, u, r, lc, names)) >= 0);
return names.iterator();
}
  1. ServiceLoader 要点

理解ServiceLoader源码之后,我们总结要点如下:

  • 约束
+ 服务实现类必须实现服务接口`S`,参见源码:`if (!service.isAssignableFrom(c))`
+ 服务实现类需包含无参的构造器,`ServiceLoader`将通过该无参的构造器来创建服务实现者的实例,参见源码:`S p = service.cast(c.newInstance());`
+ 配置文件需要使用`UTF-8`编码,参见源码:`new BufferedReader(new InputStreamReader(in, "utf-8"));`
  • 懒加载
    ServiceLoader使用“懒加载”的方式创建服务实现类实例,只有在迭代器推进的时候才会创建实例,参见源码:nextService()
  • 内存缓存
    ServiceLoader使用LinkedHashMap缓存创建的服务实现类实例,LinkedHashMap在二次迭代时会按照Map#put执行顺序遍历
  • 服务实现的选择
    当存在多个提供者时,服务消费者模块不一定要全部使用,而是需要根据某些特性筛选一种最佳实现。ServiceLoader的机制只能在遍历整个迭代器的过程中,从发现的实现类中决策出一个最佳实现,例如使用Charset.forName(String)获得Charset实现类:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
java复制代码// 服务接口
public abstract class CharsetProvider {
public abstract Charset charsetForName(String charsetName);
// 省略其他方法...
}

// Charset.java
public static Charset forName(String charsetName) {
// 以下只摘要与ServiceLoader有关的逻辑
ServiceLoader<CharsetProvider> sl = ServiceLoader.load(CharsetProvider.class, cl);
Iterator<CharsetProvider> i = sl.iterator();
for (Iterator<CharsetProvider> i = providers(); i.hasNext();) {
CharsetProvider cp = i.next();
// 满足匹配条件,return
Charset cs = cp.charsetForName(charsetName);
if (cs != null)
return cs;
}
}
  • ServiceLoader没有提供服务的注销机制
    服务实现类实例被创建后,它的垃圾回收的行为与Java中的其他对象一样,只有这个对象没有到GC Root的强引用,才能作为垃圾回收。

  1. 问题回归

现在我们回到阅读DriverManager源码提出的疑问:

1
2
3
4
scss复制代码while(driversIterator.hasNext()) {
driversIterator.next();
// 疑问:为什么没有任何处理?
}

为什么next()操作既不取得服务实现类对象,后续也没有任何处理呢?我们再回去看下LazyIterator#next()的源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
ini复制代码// ServiceLoader.java

// next() 直接调用 nextService()
private S nextService() {
if (!hasNextService()) throw new NoSuchElementException();
String cn = nextName;
nextName = null;
// 1. 使用类加载器loader加载
Class<?> c = Class.forName(cn, false, loader);
if (!service.isAssignableFrom(c)) {
ClassCastException cce = new ClassCastException(service.getCanonicalName() + " is not assignable from " + c.getCanonicalName());
fail(service, "Provider " + cn + " not a subtype", cce);
}
// 2. 根据Class实例化服务实现类
S p = service.cast(c.newInstance());
// 3. 服务实现类缓存到 providers
providers.put(cn, p);
return p;
}
    1. 使用类加载器loader加载:Class<?> c = Class.forName(cn, false, loader);
      这里传参使用false,类加载器将执行加载 -> 链接,不会执行初始化
    1. 根据 Class 实例化服务实现类
      由于创建类实例前一定会保证类加载完成,因此这里类加载器隐式执行了初始化,这就包括了类的静态代码块执行

回过头看com.mysql.cj.jdbc.Driver和oracle.jdbc.driver.OracleDriver源码,我们都发现了类似的静态代码块:

1
2
3
4
5
6
7
8
php复制代码static {
try {
// 注册驱动
java.sql.DriverManager.registerDriver(new Driver());
} catch (SQLException E) {
throw new RuntimeException("Can't register driver!");
}
}

可以看到,它们都调用了DriverManager#registerDriver注册了一个服务实现类实例,保存在CopyOnWriteArrayList中,后续获取数据库连接时是从这个列表中获取数据库驱动。现在,你理解了吗?


  1. 总结

    1. ServiceLoader基于 SPI 思想,可以实现服务提供方与服务使用方解耦,是模块化、组件化的一种实现方式
    1. ServiceLoader是一个相对简易的框架,往往只在Java源码中使用,为了满足复杂业务的需要,一般会使用提供SPI功能的第三方框架,例如后台的Dubbo、客户端的ARouter与WMRouter等

在后面的文章中,我将与你探讨ARouter与WMRouter的源码实现,欢迎关注彭旭锐的博客。


参考资料

  • WMRouter Github —— meituan
  • ARouter Github —— alibaba
  • ServiceLoader —— Android Developers
  • 《美团猫眼电影android模块化实战–可能是最详细的模块化实战》 —— 陈文超happylion 著
  • 《WMRouter:美团外卖Android开源路由框架》 —— 子健 渊博 云驰 (美团技术团队)著
  • 《Android组件化方案及组件消息总线modular-event实战》 —— 海亮 (美团技术团队)著

推荐阅读

  • Android | 带你理解 NativeAllocationRegistry 的原理与设计思想
  • Android | 一文带你全面了解 AspectJ 框架
  • Android | 使用 AspectJ 限制按钮快速点击
  • Android | 这是一份详细的 EventBus 使用教程
  • 开发者 | 浅析App社交分享的5种形式
  • 开发者 | 那些令人“奔溃”的 UI 验收
  • 计算机组成原理 | Unicode 和 UTF-8是什么关系?
  • 计算机组成原理 | 为什么浮点数运算不精确?(阿里笔试)

感谢喜欢!你的点赞是对我最大的鼓励!欢迎关注彭旭锐的Github!

本文转载自: 掘金

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

MySQL 使用索引提高查询性能

发表于 2020-09-02

下面所有内容都是以 InnoDB 为例,要说为什么的话,因为咱只用过这个存储引擎。。。

什么是索引

MySQL 中索引是一种数据结构(InnoDB 是 B+ 树),为了能够更高效的找到想要的数据,从而提高查询的性能而存在。就像字典的目录一样,想要从字典中查找某个 字/词 都可以在目录中根据拼音或部首找到指定的位置。

既然索引是以数据结构的形式存在,那么它也必然会占用空间。索引是有序存储在磁盘中的,在添加、更新数据时,都可能会涉及到索引的排序以及树的分裂、合并等维护操作。所以索引虽然查询效率高,但是也不能滥用哦~

索引的类型

为了应对各种使用场景,MySQL 提供很多种类型的索引,常用的有如下几种:

  • 主键索引:如果指定了主键字段,MySQL 会自动创建该类型索引,一张表中只能有一个主键索引,且主键索引字段值必须唯一
  • 唯一索引:使用该索引的字段必须具备唯一性,它会强制要求该字段的值必须是唯一的
  • 普通索引:为单个字段创建的普通索引,其值可以重复,没有什么特别的限制
  • 联合索引:多个字段绑定在一起创建的普通索引

这里需要明白,上面的几种特性是可能存在交叉的,比如唯一索引也可以指定多个字段来做索引(联合索引)

如何使用索引

要想通过索引来提高查询的效率,首先需要了解索引的工作方式,大概了解 MySQL 是如何通过索引来提高查询性能的,而不管是什么类型的索引,最后都需要跟 聚簇索引 打交道。

聚簇索引

大家也许听过 索引即数据 这个说法,InnoDB 中就是通过聚簇索引来实现的,聚簇索引实际上并不是一种索引类型,而是一种组织存储数据的方式,它将索引和数据存储在一起。InnoDB中一张表有且仅有一个聚簇索引,它默认使用主键来创建 主键 -> 数据 的索引关系,如果没有定义主键的话,将会使用一个唯一非空的字段或是一个隐式的主键来创建聚簇索引。之所以这么做是因为数据需要有序的存储在磁盘中,这也是为什么 MySQL 会推荐使用 自增 的 int/bigint 类型字段来做主键,自增数值类型天生就已经排序,有利于聚簇索引的维护。

之前接手过一个老项目,其中所有表都是使用 UUID 随机字符串来作为主键,说实话不是很能懂这样做的意义-_-

由于一张表中只有一个聚簇索引,理所当然的,其他类型索引都可以称之为非聚簇索引。他们和聚簇索引的区别就是聚簇索引是索引即数据,而非聚簇索引中存储的则是 索引值 -> 主键 格式。查询时通过索引值获取到主键值,最后通过主键从聚簇索引中拿到真正的数据本身。

普通索引

普通索引是理解起来最简单的一种索引,通常用于简单的查询条件和排序字段,可以通过如下语句创建普通索引:

t_user 表

id name age
1 张三 18
2 李四 20
3 王二麻子 22
1
go复制代码ALTER TABLE `t_user` ADD INDEX `age_idx` (`age`);

创建 age 字段索引后,所用此字段作为 WHERE 条件的查询语句将会通过该索引提高查询效率:

1
sql复制代码SELECT * FROM `t_user` WHERE `age` = 18;

如果使用 age 字段来做排序的话,也会通过索引来省去排序操作(索引本身是有序的)

1
sql复制代码SELECT * FROM `t_user` WHERE `age` > 18 ORDER BY `age` DESC;

联合索引

联合索引和普通索引差不多,区别在于联合索引主要用于多条件检索,如下表:

t_user 表

id name age type
1 张三 18 1
2 李四 20 2
3 王二麻子 22 3

当经常使用如下查询来获取数据时,可以使用联合索引来提高查询效率:

1
go复制代码SELECT * FROM `t_user` WHERE `age` > 18 AND `type` = 1;

为查询条件字段添加索引:

1
go复制代码ALTER TABLE `t_user` ADD INDEX `type_age_idx` (`type`, `age`);

该语句会为 t_user 表创建两个索引:type、(type, age)

覆盖索引

覆盖索引并不是一种索引类型,倒不如说是一种为了提高查询效率的索引使用技巧。前面说过,使用非聚簇索引的查询首先会通过索引找到主键,然后通过主键从聚簇索引中拿到真正的数据本身,这个过程一般称之为 回表

回表是为了通过主键拿到完整的数据,而如果我们使用的索引已经包含了查询需要的所有字段,则第一次索引查询就能拿到查询结果,可以避免回表查询,这种情况就是 覆盖索引 指索引内容覆盖了查询需要的所有字段。当数据量较大时,这种优化方案尤其重要,一般来说索引数据的量级会比真实数据的量级小很多,覆盖索引能够大大的减少从磁盘加载的数据量。

如下索引和查询将不会回表:

1
2
go复制代码ALTER TABLE `t_user` ADD INDEX `type_age_name_idx` (`type`, `age`, `name`);
SELECT `name`, `type`, `age` FROM `t_user` WHERE `type` = 1;

为什么索引失效了?

并不是说创建了索引,查询就一定会使用索引,MySQL 会自行判断当前查询是否适合使用某个索引,某些情况下,MySQL 会认为使用索引查询的效率不如全表扫描,从而放弃使用索引。

索引字段通用规则

在未满足以下情况时,MySQL 将无法命中索引:

  • 不能在索引字段上使用计算函数、类型转换等操作
  • 索引字段需要尽量保证 NOT NULL
  • LIKE 不能以通配符(%)开头,如:%XXX
  • 尽量使用粒度较细、重复度较低的字段作为索引

多表关联字段索引

多表关联字段索引需要注意以下几点:

  • 字段类型必须一致
  • 字符集必须一致

最左原则

联合索引查询时将使用 最左原则 匹配索引,即从最左边的索引开始匹配,如上面的例子:

1
go复制代码ALTER TABLE `t_user` ADD INDEX `type_age_idx` (`type`, `age`);

该语句会为 t_user 表创建两个索引:type、(type, age)

如下查询将会命中索引:

1
2
3
sql复制代码SELECT * FROM `t_user` WHERE `type` = 1;
SELECT * FROM `t_user` WHERE `age` > 18 AND `type` = 1;
SELECT * FROM `t_user` WHERE `type` = 1 AND `age` > 18;

合理使用覆盖索引

当查询索引数据过多时,由于需要回表,MySQL 可能会认为全表扫描的效率更高,从而放弃使用索引。此时合理的使用覆盖索引,避免回表,将会使索引重新命中。当然还得特别注意,需要根据实际业务来建立合适且不冗余的索引。

本文首发于我的博客,欢迎来我的博客玩耍,也欢迎友链~

渣渣的粗浅理解,如有错误,还望指正

本文转载自: 掘金

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

Java后端学习路线

发表于 2020-09-02

点赞再看,养成习惯,微信搜索【三太子敖丙】关注这个互联网苟且偷生的工具人。

本文 GitHub github.com/JavaFamily 已收录,有一线大厂面试完整考点、资料以及我的系列文章。

前言

自学/学习路线这样的一期我想写很久了,因为一直想写的全一点硬核一点所以拖到了现在,我相信这一期对不管是还在学校还是已经工作的同学都有所帮助,不管是前端还是后端我都墙裂建议大家看完,因为这样会让你对你所工作的互联网领域相关技术栈有个初步的了解。

你们也知道敖丙我是个创作鬼才,常规的切入点也不是我的风格,我毕业后主要接触的都是电商领域,所以这一期我把目前所了解的技术栈加上之前电商系统的经验臆想了一个完整的电商系统,大家会看到很多熟悉的技术栈我相信也会看到自己未接触过的技术栈,我也会对每个技术栈的主要技术点提一下,至于细节就只能大家在我历史和未来的文章去看了。

这期可谓是呕心沥血之作,不要白嫖喲。

正文

我先介绍一下前端

前端

我读者群体是以后端为主的,如果有大学还没开始学习的小伙伴,这个时候我想已经是满屏幕的问号了,为啥我们后端程序员还要去学习前端呢?我只能告诉你,傻瓜,肤浅。

如果是已经大学毕业的程序员我相信每一个后端程序员都会简单的前端,甚至很多后端对目前前端最新技术也都是了解的,我们可不能闭门造车,谁告诉你后端就不学点前端了?而且你了解前端在之后工作联调过程中或许会有更好的思路对你的工作是有所帮助的。

我们上网最先接触到的肯定不是后端的一系列东西,而是移动端和前端网页,各种花里胡哨的样式不是我们要去了解的,但是网页的基本语言以及布局从0到1这个过程是我们应该去了解的,大家看到的花里胡哨的网页布局、链接、文字、图片、事件等,都是一个个的标签、class样式以及js事件而已。

技术背后的思想其实是互通的,所以作为后端以前端作为我们程序员学习的切入点是完全OK的(只是针对还未入门萌新猿),我相信在各位的大学前端基础课程也都是有安排的,而且不管是上学还是以后毕业我相信各位以后一定会接触些许前端的。

在大学一般都是用项目去锻炼技术的,那在项目里面很可能就是你一个人从前端到后端都是自己写的,我在大学就是这样的,现在工作了我们很多内容系统简单的前端也都是我们自己去开发的,因为为了简单的页面和逻辑去浪费前端的资源是没有很大必要的。

在这里我列举了我目前觉得比较简单和我们后端可以了解的技术栈,都是比较基础和我觉得比较必须的。

HTML、CSS、JS、Ajax我觉得是必须掌握的点,看着简单其实深究或者去操作的话还是有很多东西的,其他作为扩展有兴趣可以了解,反正入门简单,只是精通很难很难。

在这一层不光有这些还有Http协议和Servlet,request、response、cookie、session这些也会伴随你整个技术生涯,理解他们对后面的你肯定有不少好处。

扩展:前端技术我觉得VUE、React大家都可以尝试去用用,他们目前支持很多即插即用的插件会帮助你更便捷的开发出漂亮的网页。

Tip:我这里最后删除了JSP相关的技术,我个人觉得没必要学了,很多公司除了老项目之外,新项目都不会使用那些技术了。

前端在我看来比后端难,技术迭代比较快,知识好像也没特定的体系,所以面试大厂的前端很多朋友都说难,不是技术多难,而是知识多且复杂,找不到一个完整的体系,相比之下后端明朗很多,我后面就开始继续往下讲了。

网关层:

互联网发展到现在,涌现了很多互联网公司,技术更新迭代了很多个版本,从早期的单机时代,到现在超大规模的互联网时代,几亿人参与的春运,几千亿成交规模的双十一,无数互联网前辈的造就了现在互联网的辉煌。

微服务,分布式,负载均衡、云原生等我们经常提到的这些名词都是这些技术在场景背后支撑。

单机顶不住,我们就多找点服务器,但是怎么将流量均匀的打到这些服务器上呢?

负载均衡,LVS

我们机器都是IP访问的,但是我们上网都是访问域名就好了,那怎么通过我们申请的域名去请求到服务器呢?

DNS

大家刷的抖音,B站,快手等等视频服务商,是怎么保证同时为全国的用户提供快速的体验?

CDN

我们这么多系统和服务,还有这么多中间件的调度怎么去管理调度等等?

zk

这么多的服务器,怎么对外统一访问呢,就可能需要知道反向代理的服务器。

Nginx

这一层做了反向负载、服务路由、服务治理、流量管理、安全隔离、服务容错等等都做了,大家公司的内外网隔离也是这一层做的。

我之前还接触过一些比较有意思的项目,所有对外的接口都是加密的,几十个服务会经过网关解密,找到真的路由再去请求。

这一层的知识点其实也不少,你往后面学会发现分布式事务,分布式锁,还有很多中间件都离不开这一层的Zookeeper,接下来就是整个学习体系最复杂的部分了,服务端。

服务层:

这一层有点东西了,算是整个框架的核心,如果你跟敖丙一样以后都是从事后端开发的话,我们基本上整个技术生涯,大部分时间都在跟这一层的技术栈打交道了,各种琳琅满目的中间件,计算机基础知识,Linux操作,算法数据结构,架构框架,研发工具等等。

我想在看这个文章的各位,计算机基础肯定都是学过的吧,如果大学的时候没好好学,我觉得还是有必要再看看的。

为什么我们网页能保证安全可靠的传输,你可能会了解到HTTP,HTTPS,TCP协议,什么三次握手,四次挥手,中间人攻击等。

还有进程、线程、协程,内存屏障,指令乱序,分支预测,CPU亲和性等等,在之后的编程生涯,如果你能掌握这些东西,会让你在遇到很多问题的时候瞬间get到点,而不是像个无头苍蝇一样乱撞(然而敖丙还做得不够,所以最近也是在恶补操作系统和网路相关的知识)。

了解这些计算机知识后,你就需要接触编程语言了,大学的C语言基础会让你学什么语言入门都会快点,嵌入式实习结束后我选择了面向对象的JAVA,但是也不知道为啥现在还没对象。

JAVA的基础也一样重要,面向对象(包括类、对象、方法、继承、封装、抽象、 多态、消息解析等),常见API,数据结构,集合框架,设计模式(包括创建型、结构型、行为型),多线程和并发,I/O流,Stream,网络编程你都需要了解。

代码会写了,你就要开始学习一些能帮助你把系统变得更加规范的框架,SSM可以会让你的开发更加便捷,结构层次更加分明。

写代码的时候你会发现你大学用的Eclipse在公司看不到了,你跟大家一样去用了IDEA,第一天这是什么玩意,一周后,真香,但是这玩意收费有点贵,那免费的VSCode真的就是不错的选择了。

代码写的时候你会接触代码的仓库管理工具maven、Gradle,提交代码的时候会去学习项目版本管理工具Git。

代码提交之后,发布之后你会发现很多东西需要自己去服务器亲自排查,那Linux的知识点就可以在里面灵活运用了,通过跳板机访问服务器查看进程,查看文件,各种Vim操作指令等等。

当你自己研发系统发布时你发现很多命令其实可以写成一个脚本一键执行就好了,那Shell会让你事半功倍的。

系统层面的优化很多时候会很有限,你可能会尝试从算法,或者优化数据结构去优化,你看到了HashMap的源码,想去了解红黑树,然后在算法网上看到了二叉树搜索树和各种常见的算法问题,刷多了,你也能总结出精华所在,什么贪心,分治,动态规划等。

这么多个服务,你发现HTTP请求已经开始有点不满足你的需求了,你想开发更便捷,像访问本地服务一样访问远程服务,所以我们去了解了Dubbo,Spring cloud等。

了解Dubbo的过程中,你发现了RPC的精华所在,所以你去接触到了高性能的NIO框架,Netty。

代码写好了,服务也能通信了,但是你发现你的代码链路好长,都耦合在一起了,所以你接触了消息队列,这种异步的处理方式,真香。

他还可以帮你在突发流量的时候用队列做缓冲,但是你发现分布式的情况,事务就不好管理了,你就了解到了分布式事务,什么两段式,三段式,TCC,XA,阿里云的全局事务服务GTS等等。

业务场景使用的多的时候你会想去了解RocketMQ,他也自带了分布式事务的解决方案,但是他并不适合超大数据量的场景,这个时候Kafka就会进入你的视线中。

我上面提到过zk,像Dubbo、Kafka等中间件都是用它做注册中心的(后续kafka会把zk去掉)很多技术栈最后都组成了一个知识体系,你先了解了体系中的每一员,你才能把它们联系起来。

服务的交互都从进程内通信变成了远程通信,所以性能必然会受到一些影响。

此外由于很多不确定性的因素,例如网络拥塞、Server 端服务器宕机、挖掘机铲断机房光纤等等,需要许多额外的功能和措施才能保证微服务流畅稳定的工作。

Spring Cloud 中就有 Hystrix 熔断器、Ribbon客户端负载均衡器、Eureka注册中心等等都是用来解决这些问题的微服务组件。

你感觉学习得差不多了,你发现各大论坛博客出现了一些前沿技术,比如容器化、云原生,你可能就会去了解像**Docker,Kubernetes(K8s)**等技术,你会发现他们给企业级应用提供了怎样的便捷。

微服务之所以能够快速发展,很重要的一个原因就是:容器化技术的发展和容器管理系统的成熟。

这一层的东西呢其实远远不止这些的,我不过多赘述,写多了像个劝退师一样,但是大家也不用慌,大部分的技术都是慢慢接触了,工作中慢慢去了解,去深入的。

这里呢还是想说我经常提到的那句话,你知道的越多,你不知道的越多,所有领域都是这样,一旦你深入了解了这个技术细节,衍生出来的新知识点和他的弊端会让你发现自己的无知,但学到自己不会的不断去进步会让你在学习的道路上走更远的。

好啦我们继续沿着图往下看,那再往下是啥呢?

数据层:

数据库可能是整个系统中最值钱的部分了,今年呢也发生了微盟程序员删库跑路的操作,删库跑路其实是我们在网上最常用的笑话,但是这个笑话背后我们应该得到的思考就是,数据是整个企业最重要最核心的东西,我现在在公司的大数据团队对此深有体会。

如果大家对大数据感兴趣我想我后面也可以找机会单独出一期大数据技术栈相关的专题。

数据库基本的事务隔离级别,索引,SQL,主被同步,读写分离等都可能是你学的时候要了解到的。

不要把鸡蛋放一个篮子的道理大家应该都知道,那分库的意义就很明显了,然后你会发现时间久了表的数据大了,就会想到去接触分表,什么TDDL、Sharding-JDBC、DRDS这些插件都会接触到。

你发现流量大的时候,或者热点数据打到数据库还是有点顶不住,压力太大了,那非关系型数据库就进场了,Redis当然是首选,但是memcache也有各自的应用场景。

Redis使用后,真香,真快,但是你会开始担心最开始提到的安全问题,这玩意快是因为在内存中操作,那断点了数据丢了怎么办?你就开始阅读官方文档,了解RDB,AOF这些持久化机制,线上用的时候还会遇到缓存雪崩击穿、穿透等等问题。

单机不满足你就用了,他的集群模式,用了集群可能也担心集群的健康状态,所以就得去了解哨兵,他的主从同步,时间久了Key多了,就得了解内存淘汰机制……

老板让你最最小的代价去设计每日签到和UV、PV统计你就会接触到:位图和HyperLogLog,高速的过滤你就会考虑到:布隆过滤器 (Bloom Filter) ,附近的人就会使用到:GeoHash 他的大容量存储有问题,你可能需要去了解Pika….

其实远远没完,每个的点我都点到为止,但是其实要深究每个点都要学很久,我们接着往下看。

实时/离线数仓/大数据

等你把几种关系型非关系型数据库的知识点,整理清楚后,你会发现数据还是大啊,而且数据的场景越来越多多样化了,那大数据的各种中间件你就得了解了。

你会发现很多场景,不需要实时的数据,比如你查你的支付宝去年的,上个月的账单,这些都是不会变化的数据,没必要实时,那你可能会接触像ODPS这样的中间件去做数据的离线分析。

然后你可能会接触Hadoop系列相关的东西,比如于Hadoop(HDFS)的一个数据仓库工具Hive,是建立在 Hadoop 文件系统之上的分布式面向列的数据库HBase 。

写多的场景,适合做一些简单查询,用他们又有点大材小用,那Cassandra就再合适不过了。

离线的数据分析没办法满足一些实时的常见,类似风控,那Flink你也得略知一二,他的窗口思想还是很有意思。

数据接触完了,计算引擎Spark你是不是也不能放过……

算法/机器学习/人工智能:

数据是整个电商系统乃至于我们整个互联网最值钱的部分不是随便说说的,但是如何发挥他们的价值,数据放在数据库是无法发挥他应有的价值的,算法在最近10年越来越受到大家的重视,机器学习、深度学习、人工智能、自动驾驶等领域也频频爆出天价offer的新闻,所以算法我觉得也有机会也是可以了解一下的。

不知道大家用搜索引擎或者购物网站使用过以图搜图功能没,这就是算法的图像搜索功能,我们在搜索栏输入对应关键词之后算法同学会通过自然语言处理,然后再落到推荐系统给出最好的搜索结果,以及大家看到的热搜,默认搜索的推荐都是通过算法算出针对你个人最优的推荐,你最最感兴趣的推荐。

就比如我最近在B站看了《龙王赘婿》相关的视频,我的默认搜索推荐就出现了《画网赘婿》的默认搜索推荐,这就是根据近期热点和你个人喜好算出来的,大家可以进去刷新试试。

国内人口基数这么大,那相对来说垃圾内容应该更多才对,但是大家几乎可以一直浏览到绿色健康的网络环境,这得益于风控,算法同学也会用风控去对涉黄,涉政等内容做一个甄别。

你要知道你的每一个行为在进入app开始就会被分析,最后给你打上一个个的标签,算法算出你最喜欢的内容投喂给你,你没发现抖音你越看内容越和你的胃口么?淘宝你越逛推荐的商品你越想买么?

这都得益于大数据和算法的结合,不断完善不同的训练模型,投喂给用户他最喜欢的内容,很多训练模型甚至以小时维度的更新频率在更新。

用户数据对内对外还有差别,因为很多平台是不会给你完整的数据的,但是算法同学会尽可能的捕捉用户的每一个潜在特性,然后去给你投喂最适合你的广告。

看到这里大家可能会担心自己的数据安全了,其实每个公司都会有自己最基本的职业操守,正常公司都是不会去出卖自己用户的任何数据的,但是市面上也存在销售用户数据的黑色产业。

生在这个大数据的年代是一件好事,技术是两面性也是我一直强调的,这样的技术会让你的所有信息透明,这个时候我们就要尽可能的注重保护我们自己的数据隐私安全,不要贪图小便宜去到处填写自己的真实信息,手机号,身份证号码等,你永远都不知道你数据的价值,以及他们可能把你的数据用在什么地方。

算法这里我提到过搜索引擎,我打算单独讲一下,因为在技术侧还算有可圈可点之处。

搜索引擎:

传统关系型数据库和NoSQL非关系型数据都没办法解决一些问题,比如我们在百度,淘宝搜索东西的时候,往往都是几个关键字在一起一起搜索东西的,在数据库除非把几次的结果做交集,不然很难去实现。

那全文检索引擎就诞生了,解决了搜索的问题,你得思考怎么把数据库的东西实时同步到ES中去,那你可能会思考到logstash去定时跑脚本同步,又或者去接触伪装成一台MySQL从服务的Canal,他会去订阅MySQL主服务的binlog,然后自己解析了去操作Es中的数据。

这些都搞定了,那可视化的后台查询又怎么解决呢?Kibana,他他是一个可视化的平台,甚至对Es集群的健康管理都做了可视化,很多公司的日志查询系统都是用它做的。

学习路线

以上就是整个系统所有的技术栈了,这个时候大家再看一下我开头的电商项目图大家是不是会觉得更有感觉了?是不是发现好像是那么回事,也大概知道了很多技术栈在一个系统里面的地位了?

技术路线路线图呢就用我之前的图其实就够了,不一定要严格按照这个去学习,只是给大家一个参考。

资料/学习网站

JavaFamily:由一个在互联网苟且偷生的男人维护的GitHub

B站 网址:www.bilibili.com

中国大学MOOC 网址:<www.icourse163.org>

IMOOC 网址:<www.imooc.com>

极客时间 网址:time.geekbang.org

极客学院 网址:<www.jikexueyuan.com>

网易云课堂 网址:study.163.com

百度/谷歌 网址:<www.baidu.com> <www.google.com>

知乎 网址:www.zhihu.com

GitHub 网址:github.com

我要自学网 网址:<www.51zxw.net>

w3school、菜鸟教程 网址:<www.w3school.com.cn> <www.runoob.com>

豆瓣、微信读书、当当 网址:<www.douban.com> weread.qq.com book.dangdang.com

CSDN 网址www.csdn.net

掘金 网址 juejin.cn

博客园 网址:www.cnblogs.com

思否(segmentfault) 网址:segmentfault.com

stackoverflow 网址:stackoverflow.com

开源中国 网址:<www.oschina.net>

V2ex 网址:<www.v2ex.com>

infoQ 网址:<www.infoq.cn>

有道词典 网址:<www.youdao.com>

印象笔记 网址:<www.yinxiang.com>

有道云、石墨文档 网址:note.youdao.com shimo.im

ProcessOn 、xmind 网址:www.processon.com <www.xmind.cn>

鸠摩搜索 网址:<www.jiumodiary.com>

脚本之家 网址:<www.jb51.net/books>

牛客网 校招 网址:<www.nowcoder.com>

LeetCode、lintcode 网址:leetcode-cn.com <www.lintcode.com>

数据结构模拟 网址:<www.cs.usfca.edu>

BOSS、拉钩 网址:<www.zhipin.com> <www.lagou.com>

掘金:juejin.cn

絮叨

如果你想去一家不错的公司,但是目前的硬实力又不到,我觉得还是有必要去努力一下的,技术能力的高低能决定你走多远,平台的高低,能决定你的高度。

如果你通过努力成功进入到了心仪的公司,一定不要懈怠放松,职场成长和新技术学习一样,不进则退。

丙丙发现在工作中发现我身边的人真的就是实力越强的越努力,最高级的自律,享受孤独(周末的歪哥)。

总结

我提到的技术栈你想全部了解,我觉得初步了解可能几个月就够了,这里的了解仅限于你知道它,知道他是干嘛的,知道怎么去使用它,并不是说深入了解他的底层原理,了解他的常见问题,熟悉问题的解决方案等等。

你想做到后者,基本上只能靠时间上的日积月累,或者不断的去尝试积累经验,也没什么速成的东西,欲速则不达大家也是知道的。

技术这条路,说实话很枯燥,很辛苦,但是待遇也会高于其他一些基础岗位。

所实话我大学学这个就是为了兴趣,我从小对电子,对计算机都比较热爱,但是现在打磨得,现在就是为了钱吧,是不是很现实?若家境殷实,谁愿颠沛流离。

但是至少丙丙因为做软件,改变了家庭的窘境,自己日子也向小康一步步迈过去,不经一番寒彻骨,怎得梅花扑鼻香?

说做程序员改变了我和我家人的一生可能夸张了,但是我总有一种下班辈子会因为我选择走这条路而改变的错觉。

我是敖丙,一个在互联网苟且偷生的工具人。

创作不易,本期硬核,不想被白嫖,各位的**「三连」**就是丙丙创作的最大动力,我们下次见!

文章持续更新,可以微信搜索「 三太子敖丙 」第一时间阅读,回复【资料】有我准备的一线大厂面试资料和简历模板,本文 GitHub github.com/JavaFamily 已经收录,有大厂面试完整考点,欢迎Star。

本文转载自: 掘金

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

「Java 路线」 方法调用的本质(含重载与重写区别) 前

发表于 2020-09-01

点赞关注,不再迷路,你的支持对我意义重大!

🔥 Hi,我是丑丑。本文 GitHub · Android-NoteBook 已收录,这里有 Android 进阶成长路线笔记 & 博客,欢迎跟着彭丑丑一起成长。(联系方式在 GitHub)

前言

  • 对于习惯使用面向对象开发的工程师们来说,重载 & 重写 这两个概念应该不会陌生了。在中 / 低级别面试中,也常常会考察面试者对它们的理解(隐约记得当年在校招面试时遇到过);
  • 网上大多数资料 & 面经对这两个概念的阐述,多数仅停留在讨论两者在 表现上 的差异,让读者去被动地接受知识。在这篇文章里,我将更有深度地理解重载 & 重写的原理,应深入理解Java 虚拟机执行引擎是如何进行方法调用的。请点赞,你的点赞和关注真的对我非常重要!

首先,尝试写出以下程序的输出:

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
typescript复制代码public class Base {
public static void funcStatic(String str){
System.out.println("Base - funcStatic - String");
}
public static void funcStatic(Object obj){
System.out.println("Base - funcStatic - Object");
}
public void func(String str){
System.out.println("Base - func - String");
}
public void func(Object obj){
System.out.println("Base - func - Object");
}
}
public class Child extends Base {
public static void funcStatic(String str){
System.out.println("Child - funcStatic - String");
}
public static void funcStatic(Object obj){
System.out.println("Child - funcStatic - Object");
}
@Override
public void func(String str){
System.out.println("Child - func - String");
}
@Override
public void func(Object obj){
System.out.println("Child - func - Object");
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
ini复制代码public class Test{
public static void main(String[] args){
Object obj = new Object();
Object str = new String();

Base base = new Base();
Base child1 = new Child();
Child child2 = new Child();

base.funcStatic(obj); // 正常编程中不应该用实例去调用静态方法
child1.funcStatic(obj);
child2.funcStatic(obj);

base.func(str);
child1.func(str);
child2.func(str);
}
}

程序输出:

1
2
3
4
5
6
7
css复制代码Base - funcStatic - Object
Base - funcStatic - Object
Child - funcStatic - Object

Base - func - Object
Child - func - Object
Child - func - Object

程序输出是否与你的预期一致呢?遇到困难了吗,相信这篇文章一定能帮到你…

延伸文章

  • 对于Java编译过程不了解,请阅读:《Java | 聊一聊编译过程(编译前端 & 编译后端)》
  • 对于Class 文件 & 符号引用不了解,请阅读:《Java | 请概述一下 Class 文件的结构》
  • 对于类加载的流程不太了解,请阅读:《Java | 谈谈你对类加载过程的理解》

目录


  1. 静态类型 & 实际类型

每一个变量都有两种类型:静态类型(Static Type) & 实际类型(Actual Type)。例如下面代码中,Base为变量base的静态类型,Child为实际类型:

1
ini复制代码Base base = new Child();

两者的具体区别如下:

  • 静态类型:引用变量的类型,在编译期确定,无法改变
  • 实际类型:实例对象的类型,在编译期无法确定,需在运行期确定,可以改变

这里先谈到这里,后文会从字节码的角度理解继续讨论两个类型。


  1. 方法调用的本质

这一节,我们来讨论Java中方法调用的本质。我们知道,Java前端编译的产物是字节码,与C/C++不同,前端编译过程中并没有链接步骤,字节码中所有的方法调用都是使用符号引用。举个例子:

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
scala复制代码- 源码:

public class Child extends Base {

@Override
void func() {
}

void test1(){
func();
}

void test2(){
super.func();
}
}

- 字节码(javap -c Child.class):

Compiled from "Child.java"
public class com.Child extends com.Base {
// 构造函数,默认调用父类构造函数
public com.Child();
Code:
0: aload_0
1: invokespecial #1 // Method com/Base."<init>":()V
4: return

void func();
Code:
0: return

void test1();
Code:
0: aload_0
// invokevirtual 调用实例方法
1: invokevirtual #2 // Method func:()V
4: return

void test2();
Code:
0: aload_0
// invokespecial 调用静态方法
1: invokespecial #3 // Method com/Base.func:()V
4: return
}

上面的字节码中,invokespecial和invokevirtual都是方法调用的字节码指令,具体细节下文会详细解释。后面的#1 #2 #3表示符号引用在常量池中的索引号,根据这个索引号检索常量表,可以查到最终表示的是一个字符串字面量,例如func:()V,这个就是方法的符号引用。

为了方便理解字节码,javap反编译的字节码已经在注释中提示了最终表示的值,例如Method func:()V。

符号引用(Symbolic References)是一个用来无歧义地标识一个实体(例如方法/字段)的字符串,在运行期它会翻译为直接引用(Direct Reference)。对于方法来说,就是方法的入口地址。

下图描述了方法符号引用的基本格式:

方法的符号引用

这个符号引用包含了变量的静态类型(如果是变量的静态类型与本类相同,不需要指明)、简单方法名以及描述符(参数顺序、参数类型和方法返回值)。通过这个符号引用,Java虚拟机就可以翻译出该方法的直接引用。但是,同一个符号引用,运行时翻译出来的直接引用可能是不同的,为什么会这样呢?

  • 小结:
  • 1. 方法调用的本质是根据方法的符号引用确定方法的直接引用(入口地址)*

  1. 从符号引用到直接引用

为什么同一个符号引用,运行时翻译出来的直接引用可能是不同的? 这与使用的方法调用指令的处理过程有关,Java字节码的方法调用指令一共有以下 5 种:

五种方法调用指令

其中,根据调用方法的版本是否在编译期可以确定,(注意:只是版本,而不是入口地址,入口地址只能在运行时确定)可以将方法调用划分为静态解析 & 动态分派两种。

# 误区(重要)#

《深入理解Java虚拟机》中将方法调用分为解析、静态分派、动态分派三种,又根据宗量的数量引入了静态多分派,动态单分派的概念。这些概念事实上过于字典化,也很容易让读者误认为静态分派与动态分派是非此即彼的互斥关系。事实上,一个方法可以同时重写与重载 ,重载 & 重写是方法调用的两个阶段,而不是两个种类。

下面,我将介绍Java中方法选择的三个步骤:

3.1 步骤1:生成符号引用(编译时)

上一节我们提到过方法符号引用的基本格式,分为三个部分:

  • 变量的静态类型:
    类的全限定名中将.替换为/,例如java.lang.Object对应java/lang/Object
  • 简单名称:
    方法的名称,例如Object#toString()的简单名称为:toString
  • 描述符:
    方法的参数列表和返回值,例如Object#toString()的描述符为()LJava/lang/String;

描述符的规则不是本文重点,这里便不再赘述了,若不了解可阅读延伸文章。这里我们用两段程序验证上述规则,这两段程序中我们考虑了重载 & 重写、静态 & 实例两个维度的因素:

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
java复制代码程序一(重载 & 重写)

public class Base {
public void func() {}
public void func(int i){}
}

public class Child extends Base {
@Override
public void func() {}
@Override
public void func(int i){}
}

public class Test{
public static void main(String[] args){
Base base1 = new Base();
Base child1 = new Child();
Child child2 = new Child();

base1.func(); // invokevirtual com.Base.func:():V
child1.func(); // invokevirtual com.Base.func:():V
child2.func(); // invokevirtual com.Child.func:():V

base1.func(1); // invokevirtual com.Base.func:(I):V
child1.func(1); // invokevirtual com.Base.func:(I):V
child2.func(1); // invokevirtual com.Child.func:(I):V
}
}

可以看到,符号引用中的类名确实是变量的静态类型,而不是变量的实际类型;方法名不用多说,方法描述符则选择重载方法中最合适的一个方法。这个例程很容易判断重载方法选择结果,具体选择规则其实更为复杂。

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
java复制代码程序二(静态 & 实例)

public class Base {
public static void func() {}
public void func(int i){}
}

public class Child extends Base {
public static void func() {}
@Override
public void func(int i){}
}

public class Test{
public static void main(String[] args){
Base base1 = new Base();
Base child1 = new Child();
Child child2 = new Child();

符号引用与程序一相同,仅指令不同

base1.func(); // invokestatic com.Base.func:():V
child1.func(); // invokestatic com.Base.func:():V
child2.func(); // invokestatic com.Child.func:():V

base1.func(1); // invokevirtual com.Base.func:(I):V
child1.func(1); // invokevirtual com.Base.func:(I):V
child2.func(1); // invokevirtual com.Child.func:(I):V
}
}

可以看到,static对符号引用没有影响,仅影响使用的指令(静态方法调用使用invokestatic)。而通过对象实例去调用静态方法是javac的语法糖,编译时会转换为使用变量的静态类型固化到符号引用中。

  • 小结:
  • 1. 方法的符号引用在编译期确定,并固化到字节码中方法调用指令的参数中*

2. 是否有static修饰对符号引用没有影响,仅影响使用的字节码指令,对象实例去调用静态方法是javac的语法糖

3.2 步骤二:解析(类加载时)

为什么静态方法、私有实例方法、实例构造器、父类方法以及final修饰这五种方法(对应的关键字: static、private、<init>、super、final)可以在编译期确定版本呢?因为无论运行时加载多少个类,这些方法都保证唯一的版本:

方法 原因
static 相同签名的子类方法会隐藏父类方法
private 只在本类可见
<init> 由编译器生成,源码无法编写
super Java是单继承,只有一个父类
final 禁止被重写

既然可以确定方法的版本,虚拟机在处理invokestatic、invokespecial、invokevirtual(final)时,就可以提前将符号引用转换为直接引用,不必延迟到方法调用时确定,具体来说,是在类加载的解析阶段完成转换的。

invokestatic 指令
  • 1)类加载解析阶段:根据符号引用中类名(如下例中java/lang/String变量的静态类型中),在对应的类中找到简单名称与描述符相符合的方法,如果找到则将符号引用转换为直接引用;否则,按照继承关系从下往上依次在各个父类中搜索
  • 2)调用阶段:符号引用已经转换为直接引用;调用invokestatic不需要将对象加载到操作数栈,只需要将所需要的参数入栈就可以执行invokestatic指令。例如:
1
2
3
4
5
6
7
rust复制代码源码:
String str = String.valueOf("1")

字节码:
0: iconst_1
1: invokestatic #2 // Method java/lang/String.valueOf:(I)Ljava/lang/String;
4: astore_1
invokespecial 指令
  • 1)类加载解析阶段:同invokestatic,也是从符号引用中的静态类型开始查找
  • 2)调用阶段:同invokestatic,符号引用已经转换为直接引用;、父类方法、私有实例方法这3种情况都是属于实例方法,所以调用invokespecial指令需要将对象加载到操作数栈。例如:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
yaml复制代码1、源码(实例构造器):
String str = new String();

字节码:
0: new #2 // class java/lang/String
3: dup
4: invokespecial #3 // Method java/lang/String."<init>":()V
7: astore_1
--------------------------------------------------------------------
2、源码(父类方法):
super.func();

字节码:
0: aload_0
1: invokespecial #2 // Method com/Base.func:()V
--------------------------------------------------------------------
3、源码(私有方法):
funcPrivate();

字节码:
0: aload_0
1: invokespecial #2 // Method funPrivate:()V

3.3 步骤三:动态分派(类使用时)

动态分派分为invokevitrual、invokeinterface 与 invokedynamic,其中动态调用invokedynamic是 JDK 1.7 新增的指令,我们单独在另一篇中解析。有些同学可能会觉得方法不重写不就只有一个版本了吗?这个想法忽略了Java动态链接的特性,Java可以从任何途径加载一个class,除非解析的 5 种的情况外,无法保证方法不被重写。

invokevirtual指令

虚拟机为每个类生成虚方法表vtable(virtual method table)的结构,类中声明的方法的入口地址会按固定顺序存放在虚方法表中;虚方法表还会继承父类的虚方法表,顺序与父类保持一致,子类新增的方法按顺序添加到虚方法末尾(这以Java单继承为前提);若子类重写父类方法,则重写方法位置的入口地址修改为子类实现;

  • 1)类加载解析阶段: 解析类的继承关系,生成类的虚方法表 (包含了这个类型所有方法的入口地址)。举个例子,有Class B继承与Class A,并重写了A中的方法:

Object是所有类的父类,所有每个类的虚方法表头部都会包含Object的虚方法表。另外,B重写了A#printMe(),所以对应位置的入口地址方法被修改为B重写方法的入口地址。

需要注意的是,被final、static或private修饰的方法不会出现在虚方法表中,因为这些方法无法被继承重写。

  • 2)调用阶段(动态分派): 解析阶段生成虚方法表后,每个方法在虚方法表中的索引是固定的,这是不会随着实际类型变化影响的。调用方法时,首先根据变量的实际类型获得对应的虚方法表(包含了这个类型所有方法的入口地址),然后根据索引找到方法的入口地址。
invokeinterface指令

接口方法的选择行为与类方法的选择行为略有区别,主要原因是Java接口是支持多继承的,就没办法像虚方法表那样直接继承父类的虚方法表。虚拟机提供了itable(interface method table)来支持多接口,itable由偏移量表offset table与方法表method table两部分组成。

当需要调用某个接口方法时,虚拟机会在offset table查找对应的method table,随后在该method table上查找方法。

3.4 性能对比

  • invokestatic & invokespecial可以直接调用方法入口地址,最快
  • invokevirtual通过编号在vtable中查找方法,次之
  • invokeinterface现在offset table中查找method table的偏移位置,随后在method table中查找接口方法的实现

  1. 总结

  • 方法调用的本质是从符号引用转换到直接引用(方法入口地址)的过程,一共需要经过(编译时)生成符号引用、(类加载时)解析、(调用时)动态分派三个步骤
  • invokestatic & invokespecial指令在(类加载时)解析时根据静态类型完成转换
  • invokevirtual & invokeinterface在(调用时)根据实际类型,查找vtable & itable完成转换
  • 重载其实是编译器的语法特性与多态无关,对编译时符号引用生成有影响,在运行时已经没有影响了;重写是多态的基础,虚拟机通过vtable & itable来支持虚方法的方法选择。

参考资料

  • 《深入理解Java虚拟机(第3版本)》(第8章)—— 周志明 著
  • 《深入理解Android:Java虚拟机 ART》(第2章) —— 邓凡平 著
  • 《深入理解 JVM 字节码》(第2、3章)—— 张亚 著

创作不易,你的「三连」是丑丑最大的动力,我们下次见!

本文转载自: 掘金

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

逐行解读Spring(六)- FactoryBean的亿点点

发表于 2020-09-01

创作不易,转载请篇首注明 作者:掘金@小希子 + 来源链接~

如果想了解更多Spring源码知识,点击前往其余逐行解析Spring系列

一、前言

Spring的IOC部分已经差不多讲完了,下一篇会开始讲AOP部分的源码。本篇博文主要是分享一个小甜点给同学们,讲一下FactoryBean这个接口。

这个接口我们日常开发中使用的不多,更多的是第三方框架接入Spring的时候会使用。不过由于这个接口跟我们IOC中承载主要逻辑的BeanFactory长的比较像,所以面试的时候面试官偶尔也会问问这两种有什么区别。

要我说,这两者区别可大了去了,因为他们基本没啥关联,完全是两个东西。

二、FactoryBean的作用

在讲原理之前,我们还是简单的讲一下FactoryBean接口的作用。我们首先看一下FactoryBean的定义:

1
2
3
4
5
6
7
8
9
10
11
12
java复制代码public interface FactoryBean<T> {
@Nullable
T getObject() throws Exception;

@Nullable
Class<?> getObjectType();
// FactoryBean#getObject返回的bean实例是否是单例的
// 如果是单例,那么FactoryBean#getObject将只会被调用一次
default boolean isSingleton() {
return true;
}
}

然后我们写一个类实现一下这个接口:

1
2
3
4
5
6
7
8
9
10
11
12
13
java复制代码public class SubBean {
}
@Service
public class FactoryBeanDemo implements FactoryBean<SubBean> {
@Override
public SubBean getObject() throws Exception {
return new SubBean();
}
@Override
public Class<?> getObjectType() {
return SubBean.class;
}
}

我们启动Spring打印一下这个factoryBeanDemo:

1
2
3
4
5
java复制代码public void test() {
applicationContext = new AnnotationConfigApplicationContext("com.xiaoxizi.spring");
Object subBean = applicationContext.getBean("factoryBeanDemo");
System.out.println(subBean);
}

输出:

1
shell复制代码com.xiaoxizi.spring.factoryBean.SubBean@3e0e1046

可以看到,我们通过getBean("factoryBeanDemo")拿到的居然是FactoryB的实例,而不是我们@Service注解标记的FactoryBeanDemo的实例。

这就是FactoryBean接口的用途啦,当我们向spring注册一个FactoryBean时,通过beanName获取到的将是FactoryBean#getObject方法返回的subBean(我们使用subBean来表示factoryBean#getObject的返回对象)实例,而且注意看FactoryBean#isSingleton方法,说明我们也是可以指定getObject方法获取的实例是单例的还是多例的。

那么,在这种情况,我们还能获取到FactoryBeanDemo的实例么?当然也是可以的,只不过我们需要稍微做一点改变:

1
2
3
4
5
6
7
8
java复制代码public void test() {
applicationContext = new AnnotationConfigApplicationContext("com.xiaoxizi.spring");
Object subBean = applicationContext.getBean("factoryBeanDemo");
System.out.println(subBean);

Object factoryBeanDemo = applicationContext.getBean("&factoryBeanDemo");
System.out.println(factoryBeanDemo);
}

输出:

1
2
shell复制代码com.xiaoxizi.spring.factoryBean.SubBean@3e0e1046
com.xiaoxizi.spring.factoryBean.FactoryBeanDemo@24c1b2d2

也就是说,正常通过beanName从Spring容器中取的话,是只能取到subBean实例的,但是如果在beanName前面加上&符号,使用&beanName从Spring容器中获取,才能获取到FactoryBean实例本身。

三、源码解析

那么Spring是如何支撑FactoryBean的功能的呢?我们还是一起跟源码看一下。我们之前讲bean的生命周期的时候,有讲到单例的bean都是在Spring容器启动的时候就初始化的,那么对于FactoryBean实例,它的FactoryBean#getObject方法也会在Spring容器启动的时候就初始化嘛?subBean实例又储存在哪里呢?带着这些疑问,我们来看一下获取bean的核心逻辑AbstractBeanFactory#doGetBean方法:

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
java复制代码protected <T> T doGetBean(final String name, @Nullable final Class<T> requiredType,
@Nullable final Object[] args, boolean typeCheckOnly) {
// 转换一下需要获取的beanName
final String beanName = transformedBeanName(name);
Object bean;

// 直接从一级缓存获取单例对象
Object sharedInstance = getSingleton(beanName);
if (sharedInstance != null && args == null) {
// 获取最终返回的bean实例,FactoryBean的主要处理逻辑在这里
bean = getObjectForBeanInstance(sharedInstance, name, beanName, null);
} else {
// skip...
if (mbd.isSingleton()) {
// spring容器启动的时候会走到这个分支
// 触发当前bean的初始化流程
sharedInstance = getSingleton(beanName, () -> {
try {
return createBean(beanName, mbd, args);
}
catch (BeansException ex) {
destroySingleton(beanName);
throw ex;
}
});
// 初始化单例bean之后,拿到这个bean对象,最终也会调用这个方法
// 获取最终返回的bean实例,FactoryBean的主要处理逻辑在这里
// 需要注意的是,bean实例初始化之后调用的时候,多传了一个BeanDefinition参数
bean = getObjectForBeanInstance(sharedInstance, name, beanName, mbd);
}
else if (mbd.isPrototype()) {
// 多例的bean
Object prototypeInstance = null;
try {
beforePrototypeCreation(beanName);
prototypeInstance = createBean(beanName, mbd, args);
}
finally {
afterPrototypeCreation(beanName);
}
// 多例的时候也会调用
// 需要注意的是,bean实例初始化之后调用的时候,多传了一个BeanDefinition参数
bean = getObjectForBeanInstance(prototypeInstance, name, beanName, mbd);
}else {
// 其他自定义scope
String scopeName = mbd.getScope();
final Scope scope = this.scopes.get(scopeName);
if (scope == null) {
throw new IllegalStateException(..);
}
try {
Object scopedInstance = scope.get(beanName, () -> {
beforePrototypeCreation(beanName);
try {
return createBean(beanName, mbd, args);
}
finally {
afterPrototypeCreation(beanName);
}
});
// 最后也是需要调用这个方法的
// 需要注意的是,bean实例初始化之后调用的时候,多传了一个BeanDefinition参数
bean = getObjectForBeanInstance(scopedInstance, name, beanName, mbd);
}
catch (IllegalStateException ex) {
throw new BeanCreationException(...);
}
}
}
}
}

我们可以看到,不管是初始化还是

1. transformedBeanName处理&符号

刚刚在测试的时候,我们有看到,使用getBean("&factoryBeanDemo")是可以获取到factoryBean的实例的,那么对于这个&符号,spring是在transformedBeanName中做初步处理的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
java复制代码// AbstractBeanFactory#transformedBeanName
protected String transformedBeanName(String name) {
// canonicalName 主要是通过别名找beanName的逻辑,逻辑也简单,不过我们不关注
// 就不看了,而且别名其实用的很少
// 主要看一下 BeanFactoryUtils.transformedBeanName
return canonicalName(BeanFactoryUtils.transformedBeanName(name));
}
// BeanFactoryUtils#transformedBeanName
public static String transformedBeanName(String name) {
// 先说一下,这个BeanFactory.FACTORY_BEAN_PREFIX常量就是 & 符号
if (!name.startsWith(BeanFactory.FACTORY_BEAN_PREFIX)) {
// 如果不是以 & 符号开头,那就直接返回了
return name;
}
return transformedBeanNameCache.computeIfAbsent(name, beanName -> {
// 把name的所有前置的&符号全部干掉
// 比如 &&&factoryBean --> factoryBean
do {
beanName = beanName.substring(BeanFactory.FACTORY_BEAN_PREFIX.length());
}
while (beanName.startsWith(BeanFactory.FACTORY_BEAN_PREFIX));
return beanName;
});
}

也就是说,对于我们getBean("&factoryBeanDemo")的调用,经过transformedBeanName(name)这一步之后,返回的beanName就是"factoryBeanDemo"了。

2. getObjectForBeanInstance获取最终需要返回的bean实例

不管是调用getBean时,是触发创建初始化bean流程(单例容器初始化/多例每次调用都会创建bean实例),还是直接从一级缓存获取到单例实例最终都需要使用获取到的bean实例调用getObjectForBeanInstance获取最终需要返回的bean,而我们的FactoryBean的逻辑就是在这个地方处理的:

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
java复制代码// 需要注意的是,这里传入了name和beanName两个值
// name是transformedBeanName之前的原始值,也就是我们调用getBean方法时传入的
// beanName就是转换后的啦,正常情况下(name没有前置的&标记),这两是一样的
// 如果mbd不为空,说明bean对象刚刚初始化完
protected Object getObjectForBeanInstance(
Object beanInstance, String name, String beanName, @Nullable RootBeanDefinition mbd) {
if (BeanFactoryUtils.isFactoryDereference(name)) {
// 如果name是带有&前缀的,说明我们是想获取factoryBean实例
// 而不是获取factoryBean#getObject返回的实例
if (beanInstance instanceof NullBean) {
return beanInstance;
}
// 判断一下,如果你通过&xxx来获取bean实例,那你获取到的bean实例必须实现FactoryBean接口
// 这种判断主要是杜绝意料之外的事情发生,比较beanName是用户指定的
// 要是用户指定一个bean名称是&xxx但是实际上是不实现FactoryBean是不允许的
// 启动就会报错
if (!(beanInstance instanceof FactoryBean)) {
throw new BeanIsNotAFactoryException(beanName, beanInstance.getClass());
}
if (mbd != null) {
mbd.isFactoryBean = true;
}
// 这里就直接把factoryBean实例返回出去了
// 这就是我们getBean("&factoryBeanDemo")获取到factoryBean实例的原因
return beanInstance;
}

// 能走到这里,其实说明name是一个正常的非&开头的name了
if (!(beanInstance instanceof FactoryBean)) {
// 这个时候,如果获取到的bean实例没有实现FactoryBean接口,
// 是不需要特殊处理的,直接返回就行了
// 对于正常的bean(没实现FactoryBean的),都是往这里返回的
return beanInstance;
}

Object object = null;
if (mbd != null) {
// 如果mbd不为空,说明bean对象(FactoryBean)刚刚初始化完
mbd.isFactoryBean = true;
}
else {
// 不是bean对象(FactoryBean)刚刚初始化完,直接从缓存获取
object = getCachedObjectForFactoryBean(beanName);
}
if (object == null) {
// 如果缓存中没有这个factoryBean对应的subBean
// 或者是factoryBean刚初始化完的时候
FactoryBean<?> factory = (FactoryBean<?>) beanInstance;
if (mbd == null && containsBeanDefinition(beanName)) {
mbd = getMergedLocalBeanDefinition(beanName);
}
boolean synthetic = (mbd != null && mbd.isSynthetic());
// 从factoryBean获取subBean并且返回
object = getObjectFromFactoryBean(factory, beanName, !synthetic);
}
// 这里返回了subBean
return object;
}

可以看到,如果a实例是一个factoryBean的话,当我们调用getBean("a")时,是会创建a实例并触发它的factoryBean#getObject获取到subBean实例并返回的;而如果是使用getBean("&a"),则只会实例化a实例并返回factoryBean本身。

2.1. getCachedObjectForFactoryBean从缓存获取subBean

可以看到,当调用getObjectForBeanInstance方法的最后一个参数BeanDefinition为空的时候,代表factoryBean实例是已经创建好了,这个时候会通过getCachedObjectForFactoryBean方法尝试直接从缓存中获取subBean对象,这个方法的逻辑很简单:

1
2
3
4
5
6
java复制代码// 当前类是 FactoryBeanRegistrySupport
private final Map<String, Object> factoryBeanObjectCache = new ConcurrentHashMap<>(16);
protected Object getCachedObjectForFactoryBean(String beanName) {
// 直接从缓存中拿了
return this.factoryBeanObjectCache.get(beanName);
}

如果缓存中有subBean实例,就直接返回这个实例,如果没有,则还会继续走下面的获取subBean的逻辑。

2.2. getObjectFromFactoryBean从factoryBean获取subBean

假设缓存中还没有subBean实例,那么肯最终都会走到getObjectFromFactoryBean方法,来获取一个subBean对象:

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
java复制代码protected Object getObjectFromFactoryBean(FactoryBean<?> factory, String beanName, boolean shouldPostProcess) {
// 注意这个isSingleton是FactoryBean#isSingleton
// 也就是说factoryBean是单例-containsSingleton(beanName),
// 且subBean也定义为单例时,才会把subBean缓存起来
if (factory.isSingleton() && containsSingleton(beanName)) {
synchronized (getSingletonMutex()) {
// 加锁
// 先从缓存拿一次
Object object = this.factoryBeanObjectCache.get(beanName);
if (object == null) {
// 确保缓存没有,才创建一个
// 这里就是简单的调用FactoryBean#getObject了,就不往下跟了
object = doGetObjectFromFactoryBean(factory, beanName);
// Only post-process and store if not put there already during getObject() call above
// (e.g. because of circular reference processing triggered by custom getBean calls)
Object alreadyThere = this.factoryBeanObjectCache.get(beanName);
if (alreadyThere != null) {
// 再从缓存中获取一遍,如果缓存中存在对象了,则把当前对象覆盖
// 并且会跳过subBean的beanPostProcessor调用流程
// 这里其实是用来解决循环依赖问题的
// 同学们可以思考一下,什么场景下,会走到这个分支呢?
object = alreadyThere;
}
else {
// 正常流程是走这里,到这里我们已经拿到subBean实例了
if (shouldPostProcess) {
// 如果当前subBean已经在创建中了,那就直接返回了。
// 其实就是判断在不在singletonsCurrentlyInCreation这个容器里
if (isSingletonCurrentlyInCreation(beanName)) {
return object;
}
// 把当前beanName加入singletonsCurrentlyInCreation容器(set)
// 如果加入不进去会报循环依赖错误,同学们应该要眼熟这个容器了才对
beforeSingletonCreation(beanName);
try {
// 调用beanPostProcessor,由于subBean的初始化/销毁等生命周期
// 都是由factoryBean自行管理的,所以这里就是调用了bean完全实例化之后的
// postProcessAfterInitialization方法
// AOP切面就是在这个埋点里做的
object = postProcessObjectFromFactoryBean(object, beanName);
}
catch (Throwable ex) {
throw new BeanCreationException(...);
}
finally {
// 从singletonsCurrentlyInCreation容器删除
afterSingletonCreation(beanName);
}
}
if (containsSingleton(beanName)) {
// 最后放入缓存
this.factoryBeanObjectCache.put(beanName, object);
}
}
}
return object;
}
}
else {
// 非单例就直接创建一个了
Object object = doGetObjectFromFactoryBean(factory, beanName);
if (shouldPostProcess) {
try {
// 调用BeanPostProcessor
object = postProcessObjectFromFactoryBean(object, beanName);
}
catch (Throwable ex) {
throw new BeanCreationException(...);
}
}
// 返回
return object;
}
}

我们简单看一下这个postProcessObjectFromFactoryBean:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
java复制代码protected Object postProcessObjectFromFactoryBean(Object object, String beanName) {
return applyBeanPostProcessorsAfterInitialization(object, beanName);
}

// 其实这个方法就是bean初始化流程中,initializeBean方法里,bean完全初始化完之后调用的埋点方法
// 由于subBean把整个生命周期(初始化、依赖注入)交由factoryBean处理了(即用户自定义)
// 所以它只需要再调用这个埋点就行
public Object applyBeanPostProcessorsAfterInitialization(Object existingBean, String beanName)
throws BeansException {

Object result = existingBean;
for (BeanPostProcessor processor : getBeanPostProcessors()) {
Object current = processor.postProcessAfterInitialization(result, beanName);
if (current == null) {
return result;
}
result = current;
}
return result;
}

可以看到,其实最终调用到的applyBeanPostProcessorsAfterInitialization方法就是bean初始化流程中,initializeBean方法里,bean完全初始化完之后调用的埋点方法。aop也是在这个埋点做操作的,所以我们的subBean也是能使用aop的功能的。

3. subBean的初始化时机

我们已经了解了getBean中对factoryBean的处理逻辑,简单的来讲,其实就是针对传入的name是否有&前缀,来走不同的分支逻辑。

那么现在又有一个问题了,单例的subBean对象,到底是在什么时候创建并且被spring管理起来的呢?

我们知道,如果subBean缓存factoryBeanObjectCache中没有对于的subBean,那么直接调用getBean("factoryBeanDemo")肯定是会创建一个subBean的,现在我想说的是,我们普通的单例bean会在spring容器启动的时候就初始化,单例的subBean也会在这个时候初始化么?要清楚这个问题,我们还是要直接去看源码,这个时候就需要看spring容器启动时,初始化所有单例bean的逻辑了:

如果对bean的生命周期级spring容器启动流程不熟悉,《逐行解读Spring(四) - 万字长文讲透bean生命周期(上)(下)》

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
java复制代码// DefaultListableBeanFactory类中
public void preInstantiateSingletons() throws BeansException {
// 获取所以的beanName
List<String> beanNames = new ArrayList<>(this.beanDefinitionNames);

for (String beanName : beanNames) {
// 循环逐一处理
RootBeanDefinition bd = getMergedLocalBeanDefinition(beanName);
if (!bd.isAbstract() && bd.isSingleton() && !bd.isLazyInit()) {
// 单例的非抽象非懒加载的才需要实例化
if (isFactoryBean(beanName)) {
// 这里主要是通过beanDefinition中的信息,判断一下是否是factoryBean
// 如果是factoryBean,将会在beanName前面加上一个&符合再调用getBean
// 也就是说这个getBean是不会初始化subBean实例的
Object bean = getBean(FACTORY_BEAN_PREFIX + beanName);
if (bean instanceof FactoryBean) {
// 拿到bean的实例之后,就可以通过bean实例使用instanceof进行二次确认了
final FactoryBean<?> factory = (FactoryBean<?>) bean;
// 可以看到这里出现了一个SmartFactoryBean接口,且有一个isEagerInit方法
// 如果isEagerInit方法返回true,spring就认为这个subBean是需要提前初始化
boolean isEagerInit = (factory instanceof SmartFactoryBean &&
((SmartFactoryBean<?>) factory).isEagerInit());
if (isEagerInit) {
// 这个时候使用原始的beanName再调用一次getBean
// 这里就会触发subBean的初始化流程了
getBean(beanName);
}
}
}
else {
// 普通的bean直接走这里
getBean(beanName);
}
}
}
// 跳过
}

可以看到,对于我们的普通的subBean,在spring容器启动的时候,是不会主动去初始化的,而只会初始化factoryBean对象。除非我们的factoryBean实现了FactoryBean的子接口SmartFactoryBean并表明该subBean需要提前初始化。

也简单看一下SmartFactoryBean接口的定义:

1
2
3
4
5
6
7
8
9
10
java复制代码public interface SmartFactoryBean<T> extends FactoryBean<T> {
// 跟FactoryBean#isSingleton()差不多,但是用处稍微有点不一样
default boolean isPrototype() {
return false;
}
// 这个方法表明是否需要提前初始化
default boolean isEagerInit() {
return false;
}
}

4. subBean的循环依赖问题

我们之前讲循环依赖的时候,都是基于两个普通的bean来讲解的,而循环依赖现象是指spring在进行单例bean的依赖注入时,出现A->B,B->A的问题。

同学们可能会说,subBean的依赖注入都不归spring管理了,怎么还能出现循环依赖问题的?

首先需要明确一点的是,循环依赖其实跟spring没有关系的,只要出现了A->B,B->A的情况,我们就认为A、B实例出现了循环依赖。而spring只是在它的管理的范围内,巧妙的使用了三级缓存/@Lazy解决了循环依赖而已。

而由于factoryBean实例本身就是由spring容器管理的,那么我们做以下操作,也是合理的:

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
java复制代码@Getter
@ToString
@AllArgsConstructor
public class SubBean {
private A a;
}

@Component
public class A {
@Autowired
private SubBean subBean;
}

@Service
public class FactoryBeanDemo implements FactoryBean<SubBean>, BeanFactoryAware {
private BeanFactory beanFactory;

@Override
public void setBeanFactory(final BeanFactory beanFactory) throws BeansException {
this.beanFactory = beanFactory;
}

@Override
public SubBean getObject() throws Exception {
final A bean = this.beanFactory.getBean(A.class);
return new SubBean(bean);
}
}

我们factoryBean通过BeanFactoryAware接口拿到beanFactory实例,并且在工厂方法getObject获取subBean的流程中使用beanFactory.getBean(A.class)从spring容器中获取a实例,而a实例又是依赖subBean实例的…

有同学可能会觉得我在难为spring,为什么要强行用这么复杂的结构,来构建一个循环依赖呢?

可是a实例和subBean最终不都是由spring管理么?它不应该解决这个问题么?

当然是可以解决的,不过这个地方要分两种情况讨论。

接下来的讨论会涉及到spring三级循环依赖的原理,不清楚的同学可以去《逐行解读Spring(五)- 没人比我更懂循环依赖!》了解相关知识。

3.1. 先初始化a实例

对于先初始化a实例的场景,其实spring原有的三级缓存设计就可以很好的解决这个问题。同学们可以回想一下,我们在创建a实例之后,尚未进行依赖注入subBean之前,就把a实例暴露到缓存了。而注入subBean的时候,会触发FactoryBean#getObject方法,最终会调用到我们自己写的beanFactory.getBean(A.class)的逻辑,从缓存中获取到暴露到缓存的a实例。

那么按这个流程下来,其实整体是没问题的,spring的三级缓存的设计已经很好的解决了这种循环依赖的问题。

我们还是简单的看一下流程图:
factoryBean循环依赖问题-先初始化A

3.2. 先初始化subBean实例

刚刚讲subBean的初始化时机时,其实有讲过,正常的subBean的初始化是一种类似于懒加载的方式,也就是说它是不会在a初始化化之前触发初始化的。但是有时候我们的项目中,实例的依赖关系可能不是这么清晰的。

假设我们有一个c实例,它依赖subBean实例,而subBean实例又和a实例循环依赖。那如果c实例先于a实例初始化,就会出现subBean实例先于a实例初始化的情况了。由于我们的subBean是没有多级缓存的机制来解决循环依赖问题,那么这个时候,整个初始化流程就变成了:

factoryBean循环依赖问题-先初始化subBean

可以看到,如果没有特殊处理的话,尽管由于我们的普通bean有三级缓存的设计,不会出现完全无法解决的级联创建实例问题。但是,也会导致我们的factoryBean#getObject被调用两次,生成两个subBean对象,且最终factoryBeanObjectCache缓存中的subBean1对象与a实例中注入的subBean2对象不是同一个。

那么这个情况应该如何解决呢?有同学可能会说,使用多级缓存呀,和普通的bean一个思路就可以了。

但是,多级缓存的思路,其实主要就是在bean实例创建之后,依赖注入之前,将bean实例暴露到缓存中,进而解决循环依赖的问题。然而,我们刚刚举例中,实际是在factoryBean#getObject获取subBean实例的过程中进行了依赖注入(虽然是我们手动的调用beanFactory.getBean获取的依赖),这个情况其实有点类似于构造器注入依赖了,构造器循环依赖用多级缓存的思想也解决不了哇。那么对于两个subBean实例的问题,spring是怎么解决的呢?spring通过短短几行代码,就解决了这个问题:

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
java复制代码protected Object getObjectFromFactoryBean(FactoryBean<?> factory, String beanName, boolean shouldPostProcess) {
// 注意这个isSingleton是FactoryBean#isSingleton
// 也就是说factoryBean是单例-containsSingleton(beanName),
// 且subBean也定义为单例时,才会把subBean缓存起来
if (factory.isSingleton() && containsSingleton(beanName)) {
synchronized (getSingletonMutex()) {
// 加锁
// 先从缓存拿一次
Object object = this.factoryBeanObjectCache.get(beanName);
if (object == null) {
// 确保缓存没有,才创建一个
// 这里就是简单的调用FactoryBean#getObject了,就不往下跟了
object = doGetObjectFromFactoryBean(factory, beanName);
Object alreadyThere = this.factoryBeanObjectCache.get(beanName);
if (alreadyThere != null) {
// 再从缓存中获取一遍,如果缓存中存在对象了,则把当前对象覆盖
// 并且会跳过subBean的beanPostProcessor调用流程
// 这里其实是用来解决循环依赖问题的
object = alreadyThere;
}
else {
// 跳过调用beanPostProcessor的逻辑
this.factoryBeanObjectCache.put(beanName, object);
}
}
return object;
}
}
// 跳过
}

这里再factoryBean#getObject方法获取到subBean1之后,再次从factoryBeanObjectCache获取了一遍subBean实例,如果获取到了subBean2,其实就代表出现我们举例的那种循环依赖了,导致缓存中已经有subBean实例了。此时会把subBean2赋值给object并且返回出去,subBean1就直接丢弃掉了,也不会放入缓存。这样就巧妙的解决了两个subBean的问题啦~

factoryBean循环依赖问题-解决方案

3.3. 无法解决的循环依赖问题

刚刚我们有聊到,factoryBean#getObject中使用beanFactory#getBean进行依赖注入,本质上相当于是构造器注入。

而上一篇讲循环缓存的时候,我们也有讲过,正常情况下来讲,构造器循环依赖是无法解决的:

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
java复制代码@Getter
@ToString
@AllArgsConstructor
public class SubBean {
private A a;
}

@Component
public class A {
public A(final SubBean subBean) {
this.subBean = subBean;
}
private SubBean subBean;
}

@Service
public class FactoryBeanDemo implements FactoryBean<SubBean>, BeanFactoryAware {
private BeanFactory beanFactory;

@Override
public void setBeanFactory(final BeanFactory beanFactory) throws BeansException {
this.beanFactory = beanFactory;
}

@Override
public SubBean getObject() throws Exception {
final A bean = this.beanFactory.getBean(A.class);
return new SubBean(bean);
}
}

我们启动一下:

1
2
3
4
5
java复制代码public void test() {
applicationContext = new AnnotationConfigApplicationContext("com.xiaoxizi.spring");
Object subBean = applicationContext.getBean("factoryBeanDemo");
System.out.println(subBean);
}

肯定是会直接报错的:

1
shell复制代码Caused by: org.springframework.beans.factory.BeanCurrentlyInCreationException: Error creating bean with name 'a': Requested bean is currently in creation: Is there an unresolvable circular reference?

当然我们还是可以使用@Lazy解决这个问题:

1
2
3
4
5
6
7
java复制代码@Component
public class A {
public A(@Lazy final SubBean subBean) {
this.subBean = subBean;
}
private SubBean subBean;
}

这种情况下,spring是能正常运行的,因为我们使用@Lazy切断了循环依赖链。

那么接下来我要说的是,正真完全无法解决的循环依赖问题:

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
java复制代码@AllArgsConstructor
public class SubBeanA {
private SubBeanB b;
}

@AllArgsConstructor
public class SubBeanB {
private SubBeanA a;
}

@Service
public class FactoryBeanA implements FactoryBean<SubBeanA>, BeanFactoryAware {
private BeanFactory beanFactory;

@Override
public void setBeanFactory(final BeanFactory beanFactory) throws BeansException {
this.beanFactory = beanFactory;
}

@Override
public SubBeanA getObject() throws Exception {
final SubBeanB bean = (SubBeanB)this.beanFactory.getBean("factoryBeanB");
return new SubBeanA(bean);
}
}

@Service
public class FactoryBeanB implements FactoryBean<SubBeanB>, BeanFactoryAware {
private BeanFactory beanFactory;

@Override
public void setBeanFactory(final BeanFactory beanFactory) throws BeansException {
this.beanFactory = beanFactory;
}

@Override
public SubBeanB getObject() throws Exception {
final SubBeanA bean = (SubBeanA)this.beanFactory.getBean("factoryBeanA");
return new SubBeanB(bean);
}
}

这种情况下启动会直接栈溢出的,连BeanCurrentlyInCreationException异常都不会有。主要原因是spring是在调用完factoryBean#getObject之后再使用singletonsCurrentlyInCreation容器进行循环依赖检测的,而这种循环依赖,其实是在疯狂的调用factoryBeanA#getObject -> factoryBeanB#getObject -> factoryBeanA#getObject -> ... 了,直接导致了栈溢出。

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
java复制代码protected Object getObjectFromFactoryBean(FactoryBean<?> factory, String beanName, boolean shouldPostProcess) {
if (factory.isSingleton() && containsSingleton(beanName)) {
synchronized (getSingletonMutex()) {
// 在这个地方就栈溢出了
Object object = this.factoryBeanObjectCache.get(beanName);
if (object == null) {
Object alreadyThere = this.factoryBeanObjectCache.get(beanName);
if (alreadyThere != null) {
object = alreadyThere;
}
else {
// 正常要走这里
if (shouldPostProcess) {
// 在这里才做循环依赖检测
if (isSingletonCurrentlyInCreation(beanName)) {
return object;
}
// 在这里才做循环依赖检测
beforeSingletonCreation(beanName);
try {
object = postProcessObjectFromFactoryBean(object, beanName);
}
catch (Throwable ex) {
throw new BeanCreationException(...);
}
finally {
afterSingletonCreation(beanName);
}
}
if (containsSingleton(beanName)) {
this.factoryBeanObjectCache.put(beanName, object);
}
}
}
return object;
}
}
// 跳过
}

所以,同学们千万不要写刚刚示例中这种代码呀,一定一定会被开除哒~

四、小结

本篇博文主要讲了一下spring中的FactoryBean接口。这个接口其实是spring对工厂模式的一种支持。

通过阅读源码,我们知道了:

  1. 单例的factoryBean对象本身会在spring容器启动时主动初始化。而subBean的初始化则是在第一次需要获取时才会触发。
  2. 如果factoryBean对象实现的接口是SmartFactoryBean且isEagerInit方法返回true,那么subBean对象也会在spring容器启动的时候主动初始化。
  3. 如果bean注册的时候,beanName对应的bean实例是一个factoryBean,那么我们通过getBean(beanName)获取到的对象将会是subBean对象;如果要获取工厂对象factoryBean,需要使用getBean("&" + beanName).
  4. 单例的subBean也会缓存在spring容器中,具体的容器是FactoryBeanRegistrySupport#factoryBeanObjectCache,一个Map<beanName, subBean实例>。
  5. spring的三级缓存设计解决了大部分循环依赖问题,而对与subBean与普通bean的循环依赖导致可能出现两个subBean对象的问题,spring采用多重检查的方式,丢弃掉其中一个无用的subBean,保留已被其他bean注入的那个subBean实例。
  6. 两个不同的subBean的获取逻辑factoryBean#getObject中的相互循环依赖是无法解决的,因为这种注入对spring来讲有点类似于构造器注入,也就是说这种循环依赖是构造器循环依赖,而且无法使用@Lazy强行切断,所以一定不要写这种代码。

创作不易,转载请篇首注明 作者:掘金@小希子 + 来源链接~

如果想了解更多Spring源码知识,点击前往其余逐行解析Spring系列

٩(* ఠO ఠ)=3⁼³₌₃⁼³₌₃⁼³₌₃嘟啦啦啦啦。。。

这里是新人博主小希子,大佬们都看到这了,左上角点个赞再走吧~~

本文转载自: 掘金

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

1…783784785…956

开发者博客

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