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

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


  • 首页

  • 归档

  • 搜索

如何玩转Golang的插件功能

发表于 2020-12-06

背景

在我做 C 语言开发的时候,为了让程序有更好的扩展性,通常选择将需要扩展的功能实现为插件,通过加载 so 文件的方式导入插件中的函数。当我学 Golang 的时候,很希望能有这样的插件功能。终于,Golang 在 1.8 版本的时候支持了插件功能。于是,第一时间尝鲜,并写了个开源库来支持热更新插件,代码地址在文末。

环境

系统: linux (别问为什么,因为 windows 下 Golang不支持动态库)
Golang 版本: 1.5 以上支持动态库,1.8 以上支持 plugin

插件代码

插件代码跟普通的 Golang 模块代码没啥差别,主要是 package 必须是 main。下面是一段简易的插件代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
go复制代码//testplugin.go
package main

import (
"fmt"
)

func init() {
fmt.Println("world")
//我们还可以做其他更高阶的事情,比如 platform.RegisterPlugin({"func": Hello}) 之类的,向插件平台自动注册该插件的函数
}

func Hello() {
fmt.Println("hello")
}

init 函数的目的是在插件模块加载的时候自动执行一些我们要做的事情,比如:自动将方法和类型注册到插件平台、输出插件信息等等。

Hello 函数则是我们需要在调用方显式查找的 symbol

编译命令

1
shell复制代码go build -buildmode=plugin testplugin.go

编译完后我们可以看到当前目录下有一个 testplugin.so 文件
我们也可以通过类似如下命令来生成不同版本的插件

1
shell复制代码go build -o testplugin_v1.so -buildmode=plugin testplugin.go

如果要想更好的控制插件的版本,想做更酷的事情,比如:热更新插件。那么可以采用自动注册的方式,新版本的插件加载上来后,自动注册插件版本号,插件平台里优先使用新版本的方法。

使用

使用方需要引入 plugin 这个包

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
go复制代码//main.go
package main

import (
"plugin"
)

func main() {
p, err := plugin.Open("testplugin.so")
if err != nil {
panic(err)
}
f, err := p.Lookup("Hello")
if err != nil {
panic(err)
}
f.(func())()
}

输出

1
2
3
shell复制代码$ go run main.go 
world
hello

我们可以看到,我们只显式调用了插件中的 Hello 方法,打印 hello 这个字符串,但是在调用 Hello 之前,已经输出了 world,这个正是插件 init 函数做的事情。

总结

Golang 支持插件使得 Golang 程序的扩展性上升到另一个台阶,可以用来做更酷的事情,如:利用插件做服务的热更新

代码:github.com/letiantech/…

推荐文章

如何用Go打造千万级流量秒杀系统

扫码关注公众号

在这里插入图片描述

本文转载自: 掘金

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

揭露 mybatis日志不为人知的秘密

发表于 2020-12-06

引言

我们在使用mybatis时,如果出现sql问题,一般会把mybatis配置文件中的logging.level参数改成debug,这样就能在日志中看到某个mapper最终执行sql、入参和影响数据行数。我们拿到sql和入参,手动拼接成完整的sql,然后将该sql在数据库中执行一下,就基本能定位到问题原因。

mybatis的日志功能使用起来还是非常方便的,大家有没有想过它是如何设计的呢?

从logging目录开始

我们先看一下mybatis的logging目录,该目录的功能决定了mybatis使用什么日志工具打印日志。

logging目录结构如下:

file

它里面除了jdbc目录,还包含了7个子目录,每一个子目录代表一种日志打印工具,目前支持6种日志打印工具和1种非日志打印工具。我们用一张图来总结一下
file

除了上面的7种日志工具之外,它还抽象出一个Log接口,所有的日志打印工具必须实现该接口,后面可以面向接口编程。

定义了LogException异常,该异常是日志功能的专属异常,如果你有看过mybatis其他源码的话,不难发现,其他功能也定义专属异常,比如:DataSourceException等,这是mybatis的惯用手法,主要是为了将异常细粒度的划分,以便更快定位问题。

此外,它还定义了LogFactory日志工厂,以便于屏蔽日志工具实例的创建细节,让用户使用起来更简单。

如果是你该如何设计这个功能?

我们按照上面目录结构的介绍其实已经有一些思路:

  1. 定义一个Log接口,以便于统一抽象日志功能,这7种日志功能都实现Log接口,并且重写日志打印方法。
  2. 定义一个LogFactory日志工厂,它会根据我们项目中引入的某个日志打印工具jar包,创建一个具体的日志打印工具实例。

看起来,不错。但是,再仔细想想,LogFactory中如何判断项目中引入了某个日志打印工具jar包才创建相应的实例呢?我们第一个想到的可能是用if…else判断不就行了,再想想感觉用if…else不好,7种条件判断太多了,并非优雅的编程。这时候,你会想一些避免太长if…else判断的方法,当然如果你看过我之前写的文章《实战|如何消除又臭又长的if…else判断更优雅的编程?》,可能已经学到了几招,但是mybatis却用了一个新的办法。

mybatis是如何设计这个功能的?

  1. 从Log接口开始
    file
    它里面抽象了日志打印的5种方法和2种判断方法。
  2. 再分析LogFactory的代码
    file
    它里面定义了一个静态的构造器logConstructor,没有用if…else判断,在static代码块中调用了6个tryImplementation方法,该方法会启动一个执行任务去调用了useXXXLogging方法,创建日志打印工具实例。

file
当然tryImplementation方法在执行前会判断构造器logConstructor为空才允许执行任务中的run方法。下一步看看useXXXLogging方法:
file
看到这里,聪明的你可能会有这样的疑问,从上图可以看出mybatis定义了8种useXXXLogging方法,但是在前面的static静态代码块中却只调用了6种,这是为什么?

对比后发现:useCustomLogging 和 useStdOutLogging 前面是没调用的。useStdOutLogging它里面使用了StdOutImpl类
file
该类其实就是通过JDK自带的System类的方法打印日志的,无需引入额外的jar包,所以不参与static代码块中的判断。

而useCustomLogging方法需要传入一个实现了Log接口的类,如果mybatis默认提供的6种日志打印工具不满足要求,以便于用户自己扩展。

而这个方法是在Configuration类中调用的,如果用户有自定义logImpl参数的话。

file

file
具体是在XMLConfigBuilder类的settingsElement方法中调用
file
再回到前面LogFactory的setImplementation方法

file
它会先找到实现了Log接口的类的构造器,返回将该构造器赋值给全局的logConstructor。

这样一来,就可以通过getLog方法获取到Log实例。
file

然后在业务代码中通过下面这种方式获取Log对象,调用它的方法打印日志了。
file

梳理一下LogFactory的流程:

  • 在static代码块中根据逐个引入日志打印工具jar包中的日志类,先判断如果全局变量logConstructor为空,则加载并获取相应的构造器,如果可以获取到则赋值给全局变量logConstructor。
  • 如果全局变量logConstructor不为空,则不继续获取构造器。
  • 根据getLog方法获取Log实例
  • 通过Log实例的具体日志方法打印日志
    在这里还分享一个知识点,如果某个工具类里面都是静态方法,那么要把该工具类的构造方法定义成private的,防止被疑问调用,LogFactory就是这么做的。
    file

适配器模式

日志模块除了使用工厂模式之外,还是有了适配器模式。

适配器模式会将所需要适配的类转换成调用者能够使用的目标接口

涉及以下几个角色:

  • 目标接口( Target )
  • 需要适配的类( Adaptee )
  • 适配器( Adapter)
    file
    mybatis是怎么用适配器模式的?
    file
    上图中标红的类对应的是Adapter角色,Log是Target角色。
    file
    而LogFactory就是Adaptee,它里面的getLog方法里面包含是需要适配的对象。

sql执行日志打印原理

从上面已经能够确定使用哪种日志打印工具,但在sql执行的过程中是如何打印日志的呢?这就需要进一步分析logging目录下的jdbc目录了。

file
看看这几个类的关系图:
file
ConnectionLogger、PreparedStatementLogger、ResultSetLogger和StatementLogger都继承了BaseJdbcLogger类,并且实现了InvocationHandler接口。从类名非常直观的看出,这4种类对应的数据库jdbc功能。

file
它们实现了InvocationHandler接口意味着它用到了动态代理,真正起作用的是invoke方法,我们以ConnectionLogger为例:
file

如果调用了prepareStatement方法,则会打印debug日志。
file

上图中传入的original参数里面包含了\n\t等分隔符,需要将分隔符替换成空格,拼接成一行sql。

最终会在日志中打印sql、入参和影响行数:
file
上图中的sql语句是在ConnectionLogger类中打印的

那么入参和影响行数呢?

入参在PreparedStatementLogger类中打印的
file
影响行数在ResultSetLogger类中打印的
file

大家需要注意的一个地方是:sql、入参和影响行数只打印了debug级别的日志,其他级别并没打印。所以需要在mybatis配置文件中的logging.level参数配置成debug,才能打印日志。

彩蛋

不知道大家有没有发现这样一个问题:

在LogFactory的代码中定义了很多匿名的任务执行器

file
但是在实际调用时,却没有在线程中执行,而是直接调用的,这是为什么?

答案是为了保证顺序执行,如果所有的日志工具jar包都有,加载优先级是:slf4j 》commonsLog 》log4j2 》log4j 》jdkLog 》NoLog

还有个问题,顺序执行就可以了,为什么要把匿名内部类定义成Runnable的呢?

这里非常有迷惑性,因为它没创建Thread类,并不会多线程执行。我个人认为,这里是mybatis的开发者的一种偷懒,不然需要定义一个新类代替这种执行任务的含义,还不如就用已有的。

最后说一句(求关注,别白嫖我)

求关注,如果这篇文章对您有所帮助,或者有所启发的话,帮忙扫描下发二维码关注一下,坚持原创不易,您的支持是我坚持下来最大的动力。

求一键三连:点赞、转发、在看。在公众号中回复:面试、代码神器、开发手册、时间管理有超赞的粉丝福利,另外回复:加群,可以跟很多BAT大厂的前辈交流和学习。

本文转载自: 掘金

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

《SpringBoot源码分析》ConditionalOn

发表于 2020-12-06

由示例引出本文的主角

首先新建两个Pojo,分别是People和Company

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复制代码
/**
* @description: People
* @Author MRyan
* @Date 2020/12/5 14:20
* @Version 1.0
*/
@Data
@ToString
@NoArgsConstructor
@AllArgsConstructor
public class People {
/**
* 姓名
*/
private String name;
/**
* 年龄
*/
private Integer age;
/**
* 所在公司
*/
private Company city;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
java复制代码
/**
* @description: Company
* @Author MRyan
* @Date 2020/12/5 14:20
* @Version 1.0
*/
@Data
@ToString
@AllArgsConstructor
@NoArgsConstructor
public class Company {
/**
* 公司名称
*/
private String companyName;
/**
* 公司所在城市
*/
private String companyCity;
}

有了这两个Pojo我们可以开始搞事情了
定义一个ConditionText类,将Company作为bean注入IOC容器中并返回对象,并同样创建People作为bean依赖Company。

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复制代码/**
* @description: 《SpringBoot源码分析》ConditionText
* @Author MRyan
* @Date 2020/12/5 14:21
* @Version 1.0
*/
@Configuration
public class ConditionText {

@Bean
public Company loadCompany() {
Company company = new Company();
company.setCompanyName("Google");
return company;
}


@Bean
public People people(Company company) {
company.setCompanyCity("USA");
return new People("MRyan", 21, company);
}

}

然后我们在测试类中注入People并输出people信息

1
2
3
4
5
6
7
8
9
10
11
12
13
java复制代码@Slf4j
@SpringBootTest
class DemoApplicationTests {

@Autowired(required = false)
private People people;

@Test
void contextLoads() {
System.out.println("people = " + people);
}

}

在这里插入图片描述
发现正常输出没有毛病,也符合实际开发的需求。
那么问题来了,如果上面的Company没有注入成功,会出现什么事情
(将Company注释掉模拟没有注入成功的场景)

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

/**
* @description: 《SpringBoot源码分析》ConditionText
* @Author MRyan
* @Date 2020/12/5 14:21
* @Version 1.0
*/
@Configuration
public class ConditionText {

/* @Bean
public Company loadCompany() {
Company company = new Company();
company.setCompanyName("Google");
return company;
}*/


@Bean
public People people(Company company) {
company.setCompanyCity("USA");
return new People("MRyan", 21, company);
}

}

在这里插入图片描述
在这里插入图片描述
启动直接空指针爆红了,这显然不是我们想要的结果,我们是要当Company已经注入成功那么实例化People,如果没有注入成功那么不实例化People。
那么我们该怎么做呢?
本文的重点来了:
@ConditionalOnBean注解的作用

将上述测试代码修改如下:

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
java复制代码/**
* @description: 《SpringBoot源码分析》ConditionText
* @Author MRyan
* @Date 2020/12/5 14:21
* @Version 1.0
*/
@Configuration
public class ConditionText {

/* @Bean
public Company loadCompany() {
Company company = new Company();
company.setCompanyName("Google");
return company;
}*/

/***
*这里加了ConditionalOnBean注解,就代表如果Company存在才实例化people
*/
@ConditionalOnBean(name = "Company")
@Bean
public People people(Company company) {
company.setCompanyCity("USA");
return new People("MRyan", 21, company);
}

}

运行测试,发现这次没爆红,而且People如我们所愿没有实例化
在这里插入图片描述
ConditionalOnBean的作用是什么,它是怎么实现的呢?

注解ConditionalOnBean是什么

源码如下:

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
java复制代码
/**
* {@link Conditional @Conditional} that only matches when beans meeting all the specified
* requirements are already contained in the {@link BeanFactory}. All the requirements
* must be met for the condition to match, but they do not have to be met by the same
* bean.
* <p>
* When placed on a {@code @Bean} method, the bean class defaults to the return type of
* the factory method:
*
* <pre class="code">
* &#064;Configuration
* public class MyAutoConfiguration {
*
* &#064;ConditionalOnBean
* &#064;Bean
* public MyService myService() {
* ...
* }
*
* }</pre>
* <p>
* In the sample above the condition will match if a bean of type {@code MyService} is
* already contained in the {@link BeanFactory}.
* <p>
* The condition can only match the bean definitions that have been processed by the
* application context so far and, as such, it is strongly recommended to use this
* condition on auto-configuration classes only. If a candidate bean may be created by
* another auto-configuration, make sure that the one using this condition runs after.
*
* @author Phillip Webb
* @since 1.0.0
*/
@Target({ ElementType.TYPE, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Conditional(OnBeanCondition.class)
//当给定的在bean存在时,则实例化当前Bean
public @interface ConditionalOnBean {

/**
* The class types of beans that should be checked. The condition matches when beans
* of all classes specified are contained in the {@link BeanFactory}.
* @return the class types of beans to check
*/
//需要作为条件的类的Class对象数组
Class<?>[] value() default {};

/**
* The class type names of beans that should be checked. The condition matches when
* beans of all classes specified are contained in the {@link BeanFactory}.
* @return the class type names of beans to check
*/
//需要作为条件的类的Name,Class.getName()
String[] type() default {};

/**
* The annotation type decorating a bean that should be checked. The condition matches
* when all of the annotations specified are defined on beans in the
* {@link BeanFactory}.
* @return the class-level annotation types to check
*/
//(用指定注解修饰的bean)条件所需的注解类
Class<? extends Annotation>[] annotation() default {};

/**
* The names of beans to check. The condition matches when all of the bean names
* specified are contained in the {@link BeanFactory}.
* @return the names of beans to check
*/
//spring容器中bean的名字
String[] name() default {};

/**
* Strategy to decide if the application context hierarchy (parent contexts) should be
* considered.
* @return the search strategy
*/
//搜索容器层级,当前容器,父容器
SearchStrategy search() default SearchStrategy.ALL;

/**
* Additional classes that may contain the specified bean types within their generic
* parameters. For example, an annotation declaring {@code value=Name.class} and
* {@code parameterizedContainer=NameRegistration.class} would detect both
* {@code Name} and {@code NameRegistration<Name>}.
* @return the container types
* @since 2.1.0
*/
//可能在其泛型参数中包含指定bean类型的其他类
Class<?>[] parameterizedContainer() default {};

}

其中我们看@Conditional(OnBeanCondition.class)是Spring4新提供的注解,它的作用是按照一定的条件进行判断,满足条件的才给容器注册Bean(有关于这个注解会另起一篇文章分析)

而@ConditionalOnBean作用是当给定的在bean存在时,则实例化当前Bean
需要注意配合上@Autowired(required = false)使用 required=false 的意思就是允许当前的Bean对象为null。

其实类似@ConditionalOnBean有很多注解
在这里插入图片描述
例如:

1
2
3
4
less复制代码@ConditionalOnBean         //	当给定的在bean存在时,则实例化当前Bean
@ConditionalOnMissingBean // 当给定的在bean不存在时,则实例化当前Bean
@ConditionalOnClass // 当给定的类名在类路径上存在,则实例化当前Bean
@ConditionalOnMissingClass // 当给定的类名在类路径上不存在,则实例化当前Bean

原理大致相同。

本文转载自: 掘金

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

逻辑思维题

发表于 2020-12-06

⭐️ 本文已收录到 AndroidFamily,技术和职场问题,请关注公众号 [彭旭锐] 和 BaguTree Pro 知识星球提问。

在计算机面试中,偶尔会遇到逻辑类题目。由于题目花样百出,准备难度较大,题海战术可能不是推荐的策略。同时,我认为回答问题的思路比答案本身更加重要。在这个专栏里,我将精选一些经典的逻辑思维题,希望能帮助你找到解题思路 / 技巧。

本文是逻辑思维系列的第 4 篇文章,完整文章目录请移步到文章末尾~

前言

大家好,我是小彭。

在计算机面试中,逻辑类题目是规模以上互联网公司的必考题。由于题目花样百出,准备难度较大,题海战术可能不是推荐的做法。在这个系列里,我将精选十道非常经典的逻辑题,希望能帮助你找到解题思路 / 技巧。如果能帮上忙,请务必点赞加关注,这真的对我非常重要。


  1. 题目描述

A 与 B 好奇问 C 的年龄,爱卖关子的 C 给出了以下 11 个数字,C 的年龄就是其中的一个:35、36、38、42、45、46、51、55、57、61、6235、36、38、42、45、46、51、55、57、61、6235、36、38、42、45、46、51、55、57、61、62,并且分别把年龄的十位数告诉给 A,把个位数告诉给 B。此时,A 和 B 发生以下对话:

A:我不知道 C 的年龄,我知道你不知道。

B:原本我不知道的,现在我知道了。

A:现在我也知道了。

那么,请问 C 的年龄是?


  1. 解题关键

  • 1、我知道你不知道的含义: 这句话的含义是:根据我的现有信息,可以知道你未取得命题的充分条件。更通俗的说法是,我不知道你现在是什么情况,反正一定不是那个可以推断出结果的状态;
  • 2、“唯一性” 隐含的充分条件: 有 36、46、57 这三个数,假设已知目标数的个位数是 7 ,那么很明显这个数就是 57 了(因为只有唯一个数字个位是 7)。

  1. 题解

  • 首先我们观察 11 个数字: (35、36、38、42、45、46、51、55、57、61、62)(35、36、38、42、45、46、51、55、57、61、62)(35、36、38、42、45、46、51、55、57、61、62)

十位数有 3、4、5、6 这几种可能,都会匹配不唯一个数字。

个位数有 1、2、5、6、7、8 这几种可能,其中 1、2、5、6 会匹配不唯一个数字,而 7、8 会分别匹配 57、38 两个数字。

  • A:我不知道 C 的年龄,我知道你不知道。

首先,A 说 “不知道 C 的年龄”,说明十位数是不唯一的。由于 十位数 3、4、5、6 都对应多个数,目前无法排除任何数字;

随后 A 又说 “我知道你不知道”,in other words,“根据我的现有信息,可以知道你未取得命题的充分条件”,in other words,“我知道你手上的个位数一定不是 7 或 8 ”。

为什么 A 敢断言:“你手上的个位数一定不是 7 和 8” 呢?一定是 A 手中的十位数不是 3,也不是 5 的时候。只有这样才能确定最终答案不是 3_ 或 5_ ,也就不可能是 38、57 两个数字。

  • 此时,观察剩下以下数字: (42、45、46、61、62)(42、45、46、61、62)(42、45、46、61、62)

个位数有 1、2、5、6 这几种可能,其中 个位数 2 会匹配多个数字,而 个位数 1、5、6 会分别匹配 61、45、46 三个数字。

  • B:原本我不知道的,现在我知道了。

此时,B 说 “我知道了”,说明达到唯一性充分条件,那么年龄个位数一定不是 2 。

  • 此时,观察剩下以下数字: (61、45、46)(61、45、46)(61、45、46)

十位数有 4_ 和6_ 这两种可能,其中 十位数 4 会匹配 2 个数字,而 十位数 6 会唯一匹配 61 这个数字。

  • A:现在我也知道了。

此时,A 说 “我也知道了”,说明达到唯一性充分条件,那么年龄是 61。


推荐阅读

逻辑思维系列往期回顾:

  • #1 25 匹马 5 条赛道,最快需要几轮求出前 3 名?
  • #2 舞会上有多少顶黑帽?
  • #3 至少要几个砝码,可以称出 1g ~ 40g 重量
  • #4 我知道你不知道,我到底知不知道

⭐️ 永远相信美好的事情即将发生,欢迎加入小彭的 Android 交流社群~

本文转载自: 掘金

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

基础篇:异步编程不会?我教你啊!CompletableFut

发表于 2020-12-05

前言

以前需要异步执行一个任务时,一般是用Thread或者线程池Executor去创建。如果需要返回值,则是调用Executor.submit获取Future。但是多个线程存在依赖组合,我们又能怎么办?可使用同步组件CountDownLatch、CyclicBarrier等;其实有简单的方法,就是用CompeletableFuture

  • 线程任务的创建
  • 线程任务的串行执行
  • 线程任务的并行执行
  • 处理任务结果和异常
  • 多任务的简单组合
  • 取消执行线程任务
  • 任务结果的获取和完成与否判断

关注公众号,一起交流,微信搜一搜: 潜行前行

1 创建异步线程任务

根据supplier创建CompletableFuture任务

1
2
3
4
java复制代码//使用内置线程ForkJoinPool.commonPool(),根据supplier构建执行任务
public static <U> CompletableFuture<U> supplyAsync(Supplier<U> supplier)
//指定自定义线程,根据supplier构建执行任务
public static <U> CompletableFuture<U> supplyAsync(Supplier<U> supplier, Executor executor)

根据runnable创建CompletableFuture任务

1
2
3
4
java复制代码//使用内置线程ForkJoinPool.commonPool(),根据runnable构建执行任务
public static CompletableFuture<Void> runAsync(Runnable runnable)
//指定自定义线程,根据runnable构建执行任务
public static CompletableFuture<Void> runAsync(Runnable runnable, Executor executor)
  • 使用示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
java复制代码ExecutorService executor = Executors.newSingleThreadExecutor();
CompletableFuture<Void> rFuture = CompletableFuture
.runAsync(() -> System.out.println("hello siting"), executor);
//supplyAsync的使用
CompletableFuture<String> future = CompletableFuture
.supplyAsync(() -> {
System.out.print("hello ");
return "siting";
}, executor);

//阻塞等待,runAsync 的future 无返回值,输出null
System.out.println(rFuture.join());
//阻塞等待
String name = future.join();
System.out.println(name);
executor.shutdown(); // 线程池需要关闭
--------输出结果--------
hello siting
null
hello siting

常量值作为CompletableFuture返回

1
2
java复制代码//有时候是需要构建一个常量的CompletableFuture
public static <U> CompletableFuture<U> completedFuture(U value)

2 线程串行执行

任务完成则运行action,不关心上一个任务的结果,无返回值

1
2
3
4
java复制代码public CompletableFuture<Void> thenRun(Runnable action)
public CompletableFuture<Void> thenRunAsync(Runnable action)
//action用指定线程池执行
public CompletableFuture<Void> thenRunAsync(Runnable action, Executor executor)
  • 使用示例
1
2
3
4
5
6
java复制代码CompletableFuture<Void> future = CompletableFuture
.supplyAsync(() -> "hello siting", executor)
.thenRunAsync(() -> System.out.println("OK"), executor);
executor.shutdown();
--------输出结果--------
OK

任务完成则运行action,依赖上一个任务的结果,无返回值

1
2
3
4
java复制代码public CompletableFuture<Void> thenAccept(Consumer<? super T> action)
public CompletableFuture<Void> thenAcceptAsync(Consumer<? super T> action)
//action用指定线程池执行
public CompletableFuture<Void> thenAcceptAsync(Consumer<? super T> action, Executor executor)
  • 使用示例
1
2
3
4
5
6
7
java复制代码ExecutorService executor = Executors.newSingleThreadExecutor();
CompletableFuture<Void> future = CompletableFuture
.supplyAsync(() -> "hello siting", executor)
.thenAcceptAsync(System.out::println, executor);
executor.shutdown();
--------输出结果--------
hello siting

任务完成则运行fn,依赖上一个任务的结果,有返回值

1
2
3
4
java复制代码public <U> CompletableFuture<U> thenApply(Function<? super T,? extends U> fn)
public <U> CompletableFuture<U> thenApplyAsync(Function<? super T,? extends U> fn)
//fn用指定线程池执行
public <U> CompletableFuture<U> thenApplyAsync(Function<? super T,? extends U> fn, Executor executor)
  • 使用示例
1
2
3
4
5
6
7
8
9
10
11
java复制代码ExecutorService executor = Executors.newSingleThreadExecutor();
CompletableFuture<String> future = CompletableFuture
.supplyAsync(() -> "hello world", executor)
.thenApplyAsync(data -> {
System.out.println(data); return "OK";
}, executor);
System.out.println(future.join());
executor.shutdown();
--------输出结果--------
hello world
OK

thenCompose - 任务完成则运行fn,依赖上一个任务的结果,有返回值

  • 类似thenApply(区别是thenCompose的返回值是CompletionStage,thenApply则是返回 U),提供该方法为了和其他CompletableFuture任务更好地配套组合使用
1
2
3
4
java复制代码public <U> CompletableFuture<U> thenCompose(Function<? super T, ? extends CompletionStage<U>> fn) 
public <U> CompletableFuture<U> thenComposeAsync(Function<? super T, ? extends CompletionStage<U>> fn)
public <U> CompletableFuture<U> thenComposeAsync(Function<? super T, ? extends CompletionStage<U>> fn,
Executor executor)
  • 使用示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
java复制代码//第一个异步任务,常量任务
CompletableFuture<String> f = CompletableFuture.completedFuture("OK");
//第二个异步任务
ExecutorService executor = Executors.newSingleThreadExecutor();
CompletableFuture<String> future = CompletableFuture
.supplyAsync(() -> "hello world", executor)
.thenComposeAsync(data -> {
System.out.println(data); return f; //使用第一个任务作为返回
}, executor);
System.out.println(future.join());
executor.shutdown();
--------输出结果--------
hello world
OK

3 线程并行执行

两个CompletableFuture并行执行完,然后执行action,不依赖上两个任务的结果,无返回值

1
2
3
java复制代码public CompletableFuture<Void> runAfterBoth(CompletionStage<?> other, Runnable action)
public CompletableFuture<Void> runAfterBothAsync(CompletionStage<?> other, Runnable action)
public CompletableFuture<Void> runAfterBothAsync(CompletionStage<?> other, Runnable action, Executor executor)
  • 使用示例
1
2
3
4
5
6
7
8
9
10
11
java复制代码//第一个异步任务,常量任务
CompletableFuture<String> first = CompletableFuture.completedFuture("hello world");
ExecutorService executor = Executors.newSingleThreadExecutor();
CompletableFuture<Void> future = CompletableFuture
//第二个异步任务
.supplyAsync(() -> "hello siting", executor)
// () -> System.out.println("OK") 是第三个任务
.runAfterBothAsync(first, () -> System.out.println("OK"), executor);
executor.shutdown();
--------输出结果--------
OK

两个CompletableFuture并行执行完,然后执行action,依赖上两个任务的结果,无返回值

1
2
3
4
5
6
7
8
9
java复制代码//调用方任务和other并行完成后执行action,action再依赖消费两个任务的结果,无返回值
public <U> CompletableFuture<Void> thenAcceptBoth(CompletionStage<? extends U> other,
BiConsumer<? super T, ? super U> action)
//两个任务异步完成,fn再依赖消费两个任务的结果,无返回值,使用默认线程池
public <U> CompletableFuture<Void> thenAcceptBothAsync(CompletionStage<? extends U> other,
BiConsumer<? super T, ? super U> action)
//两个任务异步完成,fn(用指定线程池执行)再依赖消费两个任务的结果,无返回值
public <U> CompletableFuture<Void> thenAcceptBothAsync(CompletionStage<? extends U> other,
BiConsumer<? super T, ? super U> action, Executor executor)
  • 使用示例
1
2
3
4
5
6
7
8
9
10
11
java复制代码//第一个异步任务,常量任务
CompletableFuture<String> first = CompletableFuture.completedFuture("hello world");
ExecutorService executor = Executors.newSingleThreadExecutor();
CompletableFuture<Void> future = CompletableFuture
//第二个异步任务
.supplyAsync(() -> "hello siting", executor)
// (w, s) -> System.out.println(s) 是第三个任务
.thenAcceptBothAsync(first, (s, w) -> System.out.println(s), executor);
executor.shutdown();
--------输出结果--------
hello siting

两个CompletableFuture并行执行完,然后执行fn,依赖上两个任务的结果,有返回值

1
2
3
4
5
6
7
8
9
java复制代码//调用方任务和other并行完成后,执行fn,fn再依赖消费两个任务的结果,有返回值
public <U,V> CompletableFuture<V> thenCombine(CompletionStage<? extends U> other,
BiFunction<? super T,? super U,? extends V> fn)
//两个任务异步完成,fn再依赖消费两个任务的结果,有返回值,使用默认线程池
public <U,V> CompletableFuture<V> thenCombineAsync(CompletionStage<? extends U> other,
BiFunction<? super T,? super U,? extends V> fn)
//两个任务异步完成,fn(用指定线程池执行)再依赖消费两个任务的结果,有返回值
public <U,V> CompletableFuture<V> thenCombineAsync(CompletionStage<? extends U> other,
BiFunction<? super T,? super U,? extends V> fn, Executor executor)
  • 使用示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
java复制代码//第一个异步任务,常量任务
CompletableFuture<String> first = CompletableFuture.completedFuture("hello world");
ExecutorService executor = Executors.newSingleThreadExecutor();
CompletableFuture<String> future = CompletableFuture
//第二个异步任务
.supplyAsync(() -> "hello siting", executor)
// (w, s) -> System.out.println(s) 是第三个任务
.thenCombineAsync(first, (s, w) -> {
System.out.println(s);
return "OK";
}, executor);
System.out.println(future.join());
executor.shutdown();
--------输出结果--------
hello siting
OK

4 线程并行执行,谁先执行完则谁触发下一任务(二者选其最快)

上一个任务或者other任务完成, 运行action,不依赖前一任务的结果,无返回值

1
2
3
4
5
java复制代码public CompletableFuture<Void> runAfterEither(CompletionStage<?> other, Runnable action)   
public CompletableFuture<Void> runAfterEitherAsync(CompletionStage<?> other, Runnable action)
//action用指定线程池执行
public CompletableFuture<Void> runAfterEitherAsync(CompletionStage<?> other,
Runnable action, Executor executor)
  • 使用示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
java复制代码//第一个异步任务,休眠1秒,保证最晚执行晚
CompletableFuture<String> first = CompletableFuture.supplyAsync(()->{
try{ Thread.sleep(1000); }catch (Exception e){}
System.out.println("hello world");
return "hello world";
});
ExecutorService executor = Executors.newSingleThreadExecutor();
CompletableFuture<Void> future = CompletableFuture
//第二个异步任务
.supplyAsync(() ->{
System.out.println("hello siting");
return "hello siting";
} , executor)
//() -> System.out.println("OK") 是第三个任务
.runAfterEitherAsync(first, () -> System.out.println("OK") , executor);
executor.shutdown();
--------输出结果--------
hello siting
OK

上一个任务或者other任务完成, 运行action,依赖最先完成任务的结果,无返回值

1
2
3
4
5
6
7
java复制代码public CompletableFuture<Void> acceptEither(CompletionStage<? extends T> other,
Consumer<? super T> action)
public CompletableFuture<Void> acceptEitherAsync(CompletionStage<? extends T> other,
Consumer<? super T> action, Executor executor)
//action用指定线程池执行
public CompletableFuture<Void> acceptEitherAsync(CompletionStage<? extends T> other,
Consumer<? super T> action, Executor executor)
  • 使用示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
java复制代码//第一个异步任务,休眠1秒,保证最晚执行晚
CompletableFuture<String> first = CompletableFuture.supplyAsync(()->{
try{ Thread.sleep(1000); }catch (Exception e){}
return "hello world";
});
ExecutorService executor = Executors.newSingleThreadExecutor();
CompletableFuture<Void> future = CompletableFuture
//第二个异步任务
.supplyAsync(() -> "hello siting", executor)
// data -> System.out.println(data) 是第三个任务
.acceptEitherAsync(first, data -> System.out.println(data) , executor);
executor.shutdown();
--------输出结果--------
hello siting

上一个任务或者other任务完成, 运行fn,依赖最先完成任务的结果,有返回值

1
2
3
4
5
6
7
java复制代码public <U> CompletableFuture<U> applyToEither(CompletionStage<? extends T> other,
Function<? super T, U> fn)
public <U> CompletableFuture<U> applyToEitherAsync(CompletionStage<? extends T> other,
Function<? super T, U> fn)
//fn用指定线程池执行
public <U> CompletableFuture<U> applyToEitherAsync(CompletionStage<? extends T> other,
Function<? super T, U> fn, Executor executor)
  • 使用示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
java复制代码//第一个异步任务,休眠1秒,保证最晚执行晚
CompletableFuture<String> first = CompletableFuture.supplyAsync(()->{
try{ Thread.sleep(1000); }catch (Exception e){}
return "hello world";
});
ExecutorService executor = Executors.newSingleThreadExecutor();
CompletableFuture<String> future = CompletableFuture
//第二个异步任务
.supplyAsync(() -> "hello siting", executor)
// data -> System.out.println(data) 是第三个任务
.applyToEitherAsync(first, data -> {
System.out.println(data);
return "OK";
} , executor);
System.out.println(future);
executor.shutdown();
--------输出结果--------
hello siting
OK

5 处理任务结果或者异常

exceptionally-处理异常

1
java复制代码public CompletableFuture<T> exceptionally(Function<Throwable, ? extends T> fn)
  • 如果之前的处理环节有异常问题,则会触发exceptionally的调用相当于 try…catch
  • 使用示例
1
2
3
4
5
6
7
8
9
10
11
12
java复制代码CompletableFuture<Integer> first = CompletableFuture
.supplyAsync(() -> {
if (true) {
throw new RuntimeException("main error!");
}
return "hello world";
})
.thenApply(data -> 1)
.exceptionally(e -> {
e.printStackTrace(); // 异常捕捉处理,前面两个处理环节的日常都能捕获
return 0;
});

handle-任务完成或者异常时运行fn,返回值为fn的返回

  • 相比exceptionally而言,即可处理上一环节的异常也可以处理其正常返回值
1
2
3
4
java复制代码public <U> CompletableFuture<U> handle(BiFunction<? super T, Throwable, ? extends U> fn) 
public <U> CompletableFuture<U> handleAsync(BiFunction<? super T, Throwable, ? extends U> fn)
public <U> CompletableFuture<U> handleAsync(BiFunction<? super T, Throwable, ? extends U> fn,
Executor executor)
  • 使用示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
java复制代码CompletableFuture<Integer> first = CompletableFuture
.supplyAsync(() -> {
if (true) { throw new RuntimeException("main error!"); }
return "hello world";
})
.thenApply(data -> 1)
.handleAsync((data,e) -> {
e.printStackTrace(); // 异常捕捉处理
return data;
});
System.out.println(first.join());
--------输出结果--------
java.util.concurrent.CompletionException: java.lang.RuntimeException: main error!
... 5 more
null

whenComplete-任务完成或者异常时运行action,有返回值

  • whenComplete与handle的区别在于,它不参与返回结果的处理,把它当成监听器即可
  • 即使异常被处理,在CompletableFuture外层,异常也会再次复现
  • 使用whenCompleteAsync时,返回结果则需要考虑多线程操作问题,毕竟会出现两个线程同时操作一个结果
1
2
3
4
java复制代码public CompletableFuture<T> whenComplete(BiConsumer<? super T, ? super Throwable> action) 
public CompletableFuture<T> whenCompleteAsync(BiConsumer<? super T, ? super Throwable> action)
public CompletableFuture<T> whenCompleteAsync(BiConsumer<? super T, ? super Throwable> action,
Executor executor)
  • 使用示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
java复制代码CompletableFuture<AtomicBoolean> first = CompletableFuture
.supplyAsync(() -> {
if (true) { throw new RuntimeException("main error!"); }
return "hello world";
})
.thenApply(data -> new AtomicBoolean(false))
.whenCompleteAsync((data,e) -> {
//异常捕捉处理, 但是异常还是会在外层复现
System.out.println(e.getMessage());
});
first.join();
--------输出结果--------
java.lang.RuntimeException: main error!
Exception in thread "main" java.util.concurrent.CompletionException: java.lang.RuntimeException: main error!
... 5 more

6 多个任务的简单组合

1
2
java复制代码public static CompletableFuture<Void> allOf(CompletableFuture<?>... cfs)
public static CompletableFuture<Object> anyOf(CompletableFuture<?>... cfs)


  • 使用示例
1
2
3
4
5
6
7
8
9
10
java复制代码 CompletableFuture<Void> future = CompletableFuture
.allOf(CompletableFuture.completedFuture("A"),
CompletableFuture.completedFuture("B"));
//全部任务都需要执行完
future.join();
CompletableFuture<Object> future2 = CompletableFuture
.anyOf(CompletableFuture.completedFuture("C"),
CompletableFuture.completedFuture("D"));
//其中一个任务行完即可
future2.join();

8 取消执行线程任务

1
2
3
4
java复制代码// mayInterruptIfRunning 无影响;如果任务未完成,则返回异常
public boolean cancel(boolean mayInterruptIfRunning)
//任务是否取消
public boolean isCancelled()
  • 使用示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
java复制代码CompletableFuture<Integer> future = CompletableFuture
.supplyAsync(() -> {
try { Thread.sleep(1000); } catch (Exception e) { }
return "hello world";
})
.thenApply(data -> 1);

System.out.println("任务取消前:" + future.isCancelled());
// 如果任务未完成,则返回异常,需要对使用exceptionally,handle 对结果处理
future.cancel(true);
System.out.println("任务取消后:" + future.isCancelled());
future = future.exceptionally(e -> {
e.printStackTrace();
return 0;
});
System.out.println(future.join());
--------输出结果--------
任务取消前:false
任务取消后:true
java.util.concurrent.CancellationException
at java.util.concurrent.CompletableFuture.cancel(CompletableFuture.java:2276)
at Test.main(Test.java:25)
0

9 任务的获取和完成与否判断

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
java复制代码// 任务是否执行完成
public boolean isDone()
//阻塞等待 获取返回值
public T join()
// 阻塞等待 获取返回值,区别是get需要返回受检异常
public T get()
//等待阻塞一段时间,并获取返回值
public T get(long timeout, TimeUnit unit)
//未完成则返回指定value
public T getNow(T valueIfAbsent)
//未完成,使用value作为任务执行的结果,任务结束。需要future.get获取
public boolean complete(T value)
//未完成,则是异常调用,返回异常结果,任务结束
public boolean completeExceptionally(Throwable ex)
//判断任务是否因发生异常结束的
public boolean isCompletedExceptionally()
//强制地将返回值设置为value,无论该之前任务是否完成;类似complete
public void obtrudeValue(T value)
//强制地让异常抛出,异常返回,无论该之前任务是否完成;类似completeExceptionally
public void obtrudeException(Throwable ex)
  • 使用示例
1
2
3
4
5
6
7
8
9
10
11
12
13
java复制代码CompletableFuture<Integer> future = CompletableFuture
.supplyAsync(() -> {
try { Thread.sleep(1000); } catch (Exception e) { }
return "hello world";
})
.thenApply(data -> 1);

System.out.println("任务完成前:" + future.isDone());
future.complete(10);
System.out.println("任务完成后:" + future.join());
--------输出结果--------
任务完成前:false
任务完成后:10

欢迎指正文中错误

本文转载自: 掘金

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

面试官:说说Integer缓存范围

发表于 2020-12-05

本文主要大致思路为:

不管从工作中还是面试,这篇文章都应该好好看完,本人认为是非常有用的。

案例

Integer是基本类型int的封装类。平时不管是入坑多年的小伙伴还在入坑路上的小伙伴,都应该知道的使用频率是相当高。

下面模仿订单支付,做了一个订单支付状态枚举类PayStatusEnum

1
2
3
4
5
6
7
8
9
10
11
12
13
14
java复制代码    public class IntegerDemo {
public static void main(String[] args) {
Integer a = new Integer(8);
Integer b = Integer.valueOf(8);
Integer c = 8;
System.out.println(a.equals(b));
System.out.println(a.equals(c));
System.out.println(b.equals(c));

System.out.println(a == b);
System.out.println(a == c);
System.out.println(b == c);
}
}

结果输出什么?

把上面代码中的8改成128后,又输出什么?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
java复制代码    public class IntegerDemo {
public static void main(String[] args) {
Integer a = new Integer(128);
Integer b = Integer.valueOf(128);
Integer c = 128;
System.out.println(a.equals(b));
System.out.println(a.equals(c));
System.out.println(b.equals(c));

System.out.println(a == b);
System.out.println(a == c);
System.out.println(b == c);
}
}

答案慢慢道来。

解析案例

Integer整体阅览

构造方法

1
2
3
4
java复制代码    private final int value;
public Integer(int value) {
this.value = value;
}

太简单了,没什么可讲的。

valueOf()方法

1
2
3
4
5
6
7
8
9
10
11
12
13
java复制代码    public static Integer valueOf(String s) throws NumberFormatException {
return Integer.valueOf(parseInt(s, 10));
}
//@HotSpotIntrinsicCandidate 这个注解是JDK9才引入的
//HotSpot 虚拟机将对标注了@HotSpotIntrinsicCandidate注解的方法的调用,
//替换为直接使用基于特定 CPU 指令的高效实现。这些方法我们便称之为 intrinsic。
public static Integer valueOf(int i) {
//如果i在low和high之间就使用缓存
if (i >= IntegerCache.low && i <= IntegerCache.high){
return IntegerCache.cache[i + (-IntegerCache.low)];
}
return new Integer(i);
}

上面valueOf()方法中用到了IntegerCache,下面我们来聊聊。

IntegerCache

下面是IntegerCache源码和部分注释:

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
java复制代码        /**
* Cache to support the object identity semantics of autoboxing for values between
* -128 and 127 (inclusive) as required by JLS.
* JLS协议要求缓存在-128到127之间(包含边界值)
*
* The cache is initialized on first usage.
* 程序第一次使用Integer的时候
* The size of the cache may be controlled by the {@code -XX:AutoBoxCacheMax=<size>} option.
* JVM 的启动参数 -XX:AutoBoxCacheMax=size 修改最大值
* During VM initialization, java.lang.Integer.IntegerCache.high property
* may be set and saved in the private system properties in the
* sun.misc.VM class.
* 可以通过系统属性来获得:-Djava.lang.Integer.IntegerCache.high=<size>
*/
private static class IntegerCache {
static final int low = -128;
static final int high;
//使用数组来缓存常量池
static final Integer cache[];

static {
// high value may be configured by property
//最大值是可以配置的
int h = 127;
String integerCacheHighPropValue =
sun.misc.VM.getSavedProperty("java.lang.Integer.IntegerCache.high");
//如果有配置-XX:AutoBoxCacheMax=<size>
if (integerCacheHighPropValue != null) {
try {
int i = parseInt(integerCacheHighPropValue);
//和127进行比较,谁大用谁
i = Math.max(i, 127);
// Maximum array size is Integer.MAX_VALUE
//再比较,获取最小值
h = Math.min(i, Integer.MAX_VALUE - (-low) -1);
} catch( NumberFormatException nfe) {
// If the property cannot be parsed into an int, ignore it.
// 如果该值配置错误则忽略该参数配置的值,使用默认范围-128~127
}
}
high = h;

cache = new Integer[(high - low) + 1];
int j = low;
// 缓存通过for循环来实现,创建范围内的整数对象并存储到cache数组中
// 程序第一次使用Integer的时候需要一定的额外时间来初始化该缓存
for(int k = 0; k < cache.length; k++){
cache[k] = new Integer(j++);
}
//无论如何,缓存的最大值肯定是大于等于127
assert IntegerCache.high >= 127;
}
//私有的构造方法,因为所有的属性均属于类常量
private IntegerCache() {}
}

整个静态块逻辑为:

那么,如何设置java.lang.Integer.IntegerCache.high的值呢?

The size of the cache may be controlled by the {@code -XX:AutoBoxCacheMax=} option.

注释中已经说清楚,可以使用-XX:AutoBoxCacheMax=设置。

写个demo来debug看看

1
2
3
4
5
6
7
8
java复制代码    public class IntegerDemo {
public static void main(String[] args) {
Integer a = 8;
Integer b =Integer.valueOf(8);
System.out.println(a.equals(b));
System.out.println(a == b);
}
}

设置-XX:AutoBoxCacheMax=100

开始debug

看看high的值

是127,那就对了,因为上面

设置-XX:AutoBoxCacheMax=130

开启debug模式

注意:low=-128是不会变的,整个缓存初始化过程并没有对low进行修改,再说low是常量。

-XX:AutoBoxCacheMax最大能设置成多大?

因为Integer的最大值是2147483647 ,所以我们这里使用这个值试试,

开始debug,直接报OOM了

为什么会OOM呢?

如果-XX:AutoBoxCacheMax没有设置值,那么对应数组是这样的。

equals()方法

上面的案例中有equals方法,这里把这个方法也拿出来聊聊

1
2
3
4
5
6
7
8
9
java复制代码    private final int value;
public boolean equals(Object obj) {
if (obj instanceof Integer) {
//这里比较的是两个int类型的值
return value == ((Integer)obj).intValue();
}
//obj不是Integer类型直接返回false
return false;
}

回到上面的案例中

当我们使用equals方法比较两个对象是否相等的时候其实就是比较他们的value值。

所以不管是128还是8,equals后肯定都是true。

当引用类型使用==进行比较的时候,此时比较的是两个引用的对象的地址,是不是同一个。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
java复制代码    public class IntegerDemo {
public static void main(String[] args) {
Integer a = new Integer(8);
Integer b = Integer.valueOf(8);
Integer c = 8;
System.out.println(a.equals(b));
System.out.println(a.equals(c));
System.out.println(b.equals(c));

System.out.println(a == b);
System.out.println(a == c);
System.out.println(b == c);
}
}

我们看看器class文件中的字节码;

本地变量表

每个本地变量赋值的过程

这里我们可以得出一个结论:

Integer c =8;就是Integer c = Integer.valueOf(8);

上面Integer b = Integer.valueOf(8);,那就说明变量b和c都是使用Integer.valueOf()获取到的。

valueOf方法中

-XX:AutoBoxCacheMax不设置

上面关于IntegerCache的low和high已经进行了说明,low永远是-128,所以当我们没有设置

-XX:AutoBoxCacheMax 的值的时候,这时候 high=127。

当Integer.valueOf(8);的时候,就会进入上面代码中的if中。然后从IntegerCache中的数组cache中获取。

但是IntegerCache中的cache数组是个常量数组。

言外之意,就是一旦给这个数组cache赋值后,就不会变了。

Integer b = Integer.valueOf(8);和Integer c=8;

b和c不就是都指向同一个引用地址吗?

所以 b==c为true;

但是Integer b=Integer.valueOf(128);

此时就不进入if中,而是直接new一个Integer对象

所以此时

Integer b=Innteger.valueOf(128) ;和Integer c = 128;

都会各自new 一个Integer对象,

此时的b==c为false。

这里也就是网上很多文章也就说到这里,就是比较当前int i 这个i是不是在-128到127范围之内。

-XX:AutoBoxCacheMax设置为130

如果我们把-XX:AutoBoxCacheMax设置为130。那么上面

Integer b=Innteger.valueOf(128) ;和Integer c = 128;

也会进入if条件中。

最后b==c为true。

如何避坑

Integer是基本类型int的封装类,那么在平常使用的时候需要注意几点:

1,如果使用Integer,注意Integer的默认是null,容易引起空指针异常NullPointerException。

2,如果使用int类型,注意int类型的初始值是0,很多设计某某状态时,很喜欢用0作为某个状态,这里要小心使用。

3,另外从内存使用层面来讲,int是基本数据类型,只占用4个字节,Integer是一个对象,当表示一个值时Integer占用的内存空间要高于int类型,从节省内存空间考虑,建议使用int类型(建议了解一下Java对象内存布局)。

4,Integer使用的时候,直接赋值,Integer c = 8,不要new Integer(8)。因为直接赋值就是Integer.valueOf方法使用缓存,没必要每次都new一个新对象,有效提高内存使用。

5,如果系统中大量重复的使用比127大的数,建议JVM启动的时候为-XX:AutoBoxCacheMax=size 适当的大小,提升内存使用效率(但是也不能太大,上面我们已经演示了可能出现OOM)。

面试题

面试题1

1
2
3
4
5
java复制代码    Integer num1 = new Integer(10);
Integer num2 = new Integer(10);

System.out.println(num1.equals(num2));
System.out.println(num1 == num2);

面试题2

1
2
3
4
java复制代码    Integer num3 = 100;
Integer num4 = 100;
System.out.println(num3.equals(num4));
System.out.println(num3 == num4);

面试题3

1
2
3
4
java复制代码    Integer num5 = 1000;
Integer num6 = 1000;
System.out.println(num5.equals(num6));
System.out.println(num5 == num6);

把上面看完了,在回头来看看这种面试题,还难吗?

如果在面试中遇到面试题3,可以适当反问一下面试官是否有对缓存范围进行调整,或许某些面试官都会懵逼。

赤裸裸的吊打面试官。

总结

1.引用类型的==是比较对应的引用地址。

2.Integer中使用的默认缓存是-128到127。但是可以在JVM启动的时候进行设置。

3.Integer b=Integer.valueOf(8);和Integer b=8;是一样的效果。

4.Integer.valueOf()方式,比较是否在缓存范围之内,在就直接从缓存中获取,不在new一个Integer对象。

5.每次使用new来创建Integer对象,是用不到IntegerCache缓存的。

6.Integer中的使用缓存的方式也可以理解为享元模式。

本文转载自: 掘金

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

拜托,别再让我优化大事务了,我的头都要裂开了

发表于 2020-12-05

前言

最近有个网友问了我一个问题:系统中大事务问题要如何处理?

正好前段时间我在公司处理过这个问题,我们当时由于项目初期时间比较紧张,为了快速完成业务功能,忽略了系统部分性能问题。项目顺利上线后,专门抽了一个迭代的时间去解决大事务问题,目前已经优化完成,并且顺利上线。现给大家总结了一下,我们当时使用的一些解决办法,以便大家被相同问题困扰时,可以参考一下。

大事务引发的问题

在分享解决办法之前,先看看系统中如果出现大事务可能会引发哪些问题
file

从上图可以看出如果系统中出现大事务时,问题还不小,所以我们在实际项目开发中应该尽量避免大事务的情况。如果我们已有系统中存在大事务问题,该如何解决呢?

解决办法

少用@Transactional注解

大家在实际项目开发中,我们在业务方法加上@Transactional注解开启事务功能,这是非常普遍的做法,它被称为声明式事务。
部分代码如下:

1
2
3
4
java复制代码@Transactional(rollbackFor=Exception.class)
public void save(User user) {
doSameThing...
}

然而,我要说的第一条是:少用@Transactional注解。
为什么?

我们知道@Transactional注解是通过spring的aop起作用的,但是如果使用不当,事务功能可能会失效。如果恰巧你经验不足,这种问题不太好排查。至于事务哪些情况下会失效,可以参考我之前写的《spring事务的这10种坑,你稍不注意可能就会踩中!!!》这篇文章。
@Transactional注解一般加在某个业务方法上,会导致整个业务方法都在同一个事务中,粒度太粗,不好控制事务范围,是出现大事务问题的最常见的原因。

那我们该怎么办呢?

可以使用编程式事务,在spring项目中使用TransactionTemplate类的对象,手动执行事务。
部分代码如下:

1
2
3
4
5
6
7
8
9
10
11
typescript复制代码   @Autowired
private TransactionTemplate transactionTemplate;

...

public void save(final User user) {
transactionTemplate.execute((status) => {
doSameThing...
return Boolean.TRUE;
})
}

从上面的代码中可以看出,使用TransactionTemplate的编程式事务功能自己灵活控制事务的范围,是避免大事务问题的首选办法。

当然,我说少使用@Transactional注解开启事务,并不是说一定不能用它,如果项目中有些业务逻辑比较简单,而且不经常变动,使用@Transactional注解开启事务开启事务也无妨,因为它更简单,开发效率更高,但是千万要小心事务失效的问题。

将查询(select)方法放到事务外

如果出现大事务,可以将查询(select)方法放到事务外,也是比较常用的做法,因为一般情况下这类方法是不需要事务的。

比如出现如下代码:

1
2
3
4
5
6
7
scss复制代码@Transactional(rollbackFor=Exception.class)
public void save(User user) {
queryData1();
queryData2();
addData1();
updateData2();
}

可以将queryData1和queryData2两个查询方法放在事务外执行,将真正需要事务执行的代码才放到事务中,比如:addData1和updateData2方法,这样就能有效的减少事务的粒度。
如果使用TransactionTemplate的编程式事务这里就非常好修改。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
scss复制代码   @Autowired
private TransactionTemplate transactionTemplate;

...

public void save(final User user) {
queryData1();
queryData2();
transactionTemplate.execute((status) => {
addData1();
updateData2();
return Boolean.TRUE;
})
}

但是如果你实在还是想用@Transactional注解,该怎么拆分呢?

1
2
3
4
5
6
7
8
9
10
11
scss复制代码public void save(User user) {
queryData1();
queryData2();
doSave();
}

@Transactional(rollbackFor=Exception.class)
public void doSave(User user) {
addData1();
updateData2();
}

这个例子是非常经典的错误,这种直接方法调用的做法事务不会生效,给正在坑中的朋友提个醒。因为@Transactional注解的声明式事务是通过spring aop起作用的,而spring aop需要生成代理对象,直接方法调用使用的还是原始对象,所以事务不会生效。

有没有办法解决这个问题呢?

1.新加一个Service方法

这个方法非常简单,只需要新加一个Service方法,把@Transactional注解加到新Service方法上,把需要事务执行的代码移到新方法中。具体代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
scss复制代码@Servcie
publicclass ServiceA {
@Autowired
prvate ServiceB serviceB;

public void save(User user) {
queryData1();
queryData2();
serviceB.doSave(user);
}
}

@Servcie
publicclass ServiceB {

@Transactional(rollbackFor=Exception.class)
public void doSave(User user) {
addData1();
updateData2();
}

}

2.在该Service类中注入自己

如果不想再新加一个Service类,在该Service类中注入自己也是一种选择。具体代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
scss复制代码@Servcie
publicclass ServiceA {
@Autowired
prvate ServiceA serviceA;

public void save(User user) {
queryData1();
queryData2();
serviceA.doSave(user);
}

@Transactional(rollbackFor=Exception.class)
public void doSave(User user) {
addData1();
updateData2();
}
}

可能有些人可能会有这样的疑问:这种做法会不会出现循环依赖问题?

其实spring ioc内部的三级缓存保证了它,不会出现循环依赖问题。如果你想进一步了解循环依赖问题,可以看看我之前文章《spring解决循环依赖为什么要用三级缓存?》。

3.在该Service类中使用AopContext.currentProxy()获取代理对象

上面的方法2确实可以解决问题,但是代码看起来并不直观,还可以通过在该Service类中使用AOPProxy获取代理对象,实现相同的功能。具体代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
scss复制代码@Servcie
publicclass ServiceA {

public void save(User user) {
queryData1();
queryData2();
((ServiceA)AopContext.currentProxy()).doSave(user);
}

@Transactional(rollbackFor=Exception.class)
public void doSave(User user) {
addData1();
updateData2();
}
}

事务中避免远程调用

我们在接口中调用其他系统的接口是不能避免的,由于网络不稳定,这种远程调的响应时间可能比较长,如果远程调用的代码放在某个事物中,这个事物就可能是大事务。当然,远程调用不仅仅是指调用接口,还有包括:发MQ消息,或者连接redis、mongodb保存数据等。

1
2
3
4
5
scss复制代码@Transactional(rollbackFor=Exception.class)
public void save(User user) {
callRemoteApi();
addData1();
}

远程调用的代码可能耗时较长,切记一定要放在事务之外。

1
2
3
4
5
6
7
8
9
10
11
12
typescript复制代码   @Autowired
private TransactionTemplate transactionTemplate;

...

public void save(final User user) {
callRemoteApi();
transactionTemplate.execute((status) => {
addData1();
return Boolean.TRUE;
})
}

有些朋友可能会问,远程调用的代码不放在事务中如何保证数据一致性呢?这就需要建立:重试+补偿机制,达到数据最终一致性了。

事务中避免一次性处理太多数据

如果一个事务中需要处理的数据太多,也会造成大事务问题。比如为了操作方便,你可能会一次批量更新1000条数据,这样会导致大量数据锁等待,特别在高并发的系统中问题尤为明显。

解决办法是分页处理,1000条数据,分50页,一次只处理20条数据,这样可以大大减少大事务的出现。

非事务执行

在使用事务之前,我们都应该思考一下,是不是所有的数据库操作都需要在事务中执行?

1
2
3
4
5
6
7
8
9
10
11
12
13
scss复制代码   @Autowired
private TransactionTemplate transactionTemplate;

...

public void save(final User user) {
transactionTemplate.execute((status) => {
addData();
addLog();
updateCount();
return Boolean.TRUE;
})
}

上面的例子中,其实addLog增加操作日志方法 和 updateCount更新统计数量方法,是可以不在事务中执行的,因为操作日志和统计数量这种业务允许少量数据不一致的情况。

1
2
3
4
5
6
7
8
9
10
11
12
13
scss复制代码   @Autowired
private TransactionTemplate transactionTemplate;

...

public void save(final User user) {
transactionTemplate.execute((status) => {
addData();
return Boolean.TRUE;
})
addLog();
updateCount();
}

当然大事务中要鉴别出哪些方法可以非事务执行,其实没那么容易,需要对整个业务梳理一遍,才能找出最合理的答案。

异步处理

还有一点也非常重要,是不是事务中的所有方法都需要同步执行?我们都知道,方法同步执行需要等待方法返回,如果一个事务中同步执行的方法太多了,势必会造成等待时间过长,出现大事务问题。

看看下面这个列子:

1
2
3
4
5
6
7
8
9
10
11
12
typescript复制代码   @Autowired
private TransactionTemplate transactionTemplate;

...

public void save(final User user) {
transactionTemplate.execute((status) => {
order();
delivery();
return Boolean.TRUE;
})
}

order方法用于下单,delivery方法用于发货,是不是下单后就一定要马上发货呢?

答案是否定的。

这里发货功能其实可以走mq异步处理逻辑。

1
2
3
4
5
6
7
8
9
10
11
12
typescript复制代码   @Autowired
private TransactionTemplate transactionTemplate;

...

public void save(final User user) {
transactionTemplate.execute((status) => {
order();
return Boolean.TRUE;
})
sendMq();
}

总结

本人从网友的一个问题出发,结合自己实际的工作经验分享了处理大事务的6种办法:

  • 少用@Transactional注解
  • 将查询(select)方法放到事务外
  • 事务中避免远程调用
  • 事务中避免一次性处理太多数据
  • 非事务执行
  • 异步处理

最后说一句(求关注,不要白嫖我)

如果这篇文章对您有所帮助,或者有所启发的话,帮忙扫描下发二维码关注一下,或者点赞、转发、在看。在公众号中回复:面试、代码神器、开发手册、时间管理有超赞的粉丝福利,另外回复:加群,可以跟很多大厂的前辈交流和学习。

file

本文转载自: 掘金

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

【推荐】mysql优化工具(值得一用)

发表于 2020-12-04

前言

今天逛github时,发现了这款对 SQL 进行优化和改写的自动化工具sora。感觉挺不错的,就下载学习了一下。这个工具支持的功能比较多,可以作为我们日常开发中的一款辅助工具,现在我就把它推荐给你们~~~

github传送门:github.com/XiaoMi/soar

背景

在我们日常开发中,优化SQL总是我们日常开发任务之一。例行 SQL 优化,不仅可以提升程序性能,还能够降低线上故障的概率。

目前常用的 SQL 优化方式包括但不限于:业务层优化、SQL逻辑优化、索引优化等。其中索引优化通常通过调整索引或新增索引从而达到 SQL 优化的目的。索引优化往往可以在短时间内产生非常巨大的效果。如果能够将索引优化转化成工具化、标准化的流程,减少人工介入的工作量,无疑会大大提高我们的工作效率。

SOAR(SQL Optimizer And Rewriter) 是一个对 SQL 进行优化和改写的自动化工具。 由小米人工智能与云平台的数据库团队开发与维护。

与业内其他优秀产品对比如下:

SOAR sqlcheck pt-query-advisor SQL Advisor Inception sqlautoreview
启发式建议 ✔️ ✔️ ✔️ ❌ ✔️ ✔️
索引建议 ✔️ ❌ ❌ ✔️ ❌ ✔️
查询重写 ✔️ ❌ ❌ ❌ ❌ ❌
执行计划展示 ✔️ ❌ ❌ ❌ ❌ ❌
Profiling ✔️ ❌ ❌ ❌ ❌ ❌
Trace ✔️ ❌ ❌ ❌ ❌ ❌
SQL在线执行 ❌ ❌ ❌ ❌ ✔️ ❌
数据备份 ❌ ❌ ❌ ❌ ✔️ ❌

从上图可以看出,支持的功能丰富,其功能特点如下:

  • 跨平台支持(支持 Linux, Mac 环境,Windows 环境理论上也支持,不过未全面测试)
  • 目前只支持 MySQL 语法族协议的 SQL 优化
  • 支持基于启发式算法的语句优化
  • 支持复杂查询的多列索引优化(UPDATE, INSERT, DELETE, SELECT)
  • 支持 EXPLAIN 信息丰富解读
  • 支持 SQL 指纹、压缩和美化
  • 支持同一张表多条 ALTER 请求合并
  • 支持自定义规则的 SQL 改写

就介绍这么多吧,既然是SQL优化工具,光说是没有用的,我们还是先用起来看看效果吧。

安装

这里有两种安装方式,如下:

    1. 下载二进制安装包
1
2
shell复制代码$ wget https://github.com/XiaoMi/soar/releases/download/0.11.0/soar.linux-amd64 -O soar
chmod a+x soar

这里建议直接下载最新版,要不会有bug。

下载好的二进制文件添加到环境变量中即可(不会的谷歌一下吧,这里就不讲了)。

测试一下:

1
2
3
4
5
6
7
8
9
10
11
12
shell复制代码$ echo 'select * from user' | soar.darwin-amd64(根据你自己的二进制文件名来输入)
# Query: AC4262B5AF150CB5

★ ★ ★ ☆ ☆ 75分

​```sql

SELECT
*
FROM
USER
​

最外层 SELECT 未指定 WHERE 条件

  • Item: CLA.001

  • Severity: L4

  • Content: SELECT 语句没有 WHERE 子句,可能检查比预期更多的行(全表扫描)。对于 SELECT COUNT(*) 类型的请求如果不要求精度,建议使用 SHOW TABLE STATUS 或 EXPLAIN 替代。

不建议使用 SELECT * 类型查询

  • Item: COL.001

  • Severity: L1

  • Content: 当表结构变更时,使用 * 通配符选择所有列将导致查询的含义和行为会发生更改,可能导致查询返回更多的数据。

1
2
3
4
5

* 2. 源码安装


依赖环境:

go复制代码1. Go 1.10+
2. git

1
2
3
4
5
6
7
8
9
10
11

高级依赖(仅面向开发人员)


* [mysql](https://dev.mysql.com/doc/refman/8.0/en/mysql.html) 客户端版本需要与容器中MySQL版本相同,避免出现由于认证原因导致无法连接问题
* [docker](https://docs.docker.com/engine/reference/commandline/cli/) MySQL Server测试容器管理
* [govendor](https://github.com/kardianos/govendor) Go包管理
* [retool](https://github.com/twitchtv/retool) 依赖外部代码质量静态检查工具二进制文件管理


生成二进制文件:

go复制代码go get -d github.com/XiaoMi/soar
cd ${GOPATH}/src/github.com/XiaoMi/soar && make

1
2
3
4
5
6
7
8
9
10
11
12

生成的二进制文件与上面一样,直接放入环境变量即可,这里我没有尝试,靠你们自己踩坑了呦~~~


简单使用
----


### 0. 前置准备


准备一个`table`,如下:

sql复制代码CREATE TABLE users (
id bigint(20) unsigned NOT NULL AUTO_INCREMENT,
username varchar(64) NOT NULL DEFAULT ‘’,
nickname varchar(255) DEFAULT ‘’,
password varchar(256) NOT NULL DEFAULT ‘’,
salt varchar(48) NOT NULL DEFAULT ‘’,
avatar varchar(128) DEFAULT NULL,
uptime datetime DEFAULT NULL,
PRIMARY KEY (id),
UNIQUE KEY username (username)
) ENGINE=InnoDB AUTO_INCREMENT=11 DEFAULT CHARSET=utf8mb4

1
2

### 1. 直接输入sql语句(不运行)

shell复制代码$ echo “select * from users” | soar.darwin-amd64
$ # Query: 30AFCB1E1344BEBD

★ ★ ★ ☆ ☆ 75分

​

1
2
3
4
5
6

SELECT
*
FROM
users
​

最外层 SELECT 未指定 WHERE 条件

  • Item: CLA.001

  • Severity: L4

  • Content: SELECT 语句没有 WHERE 子句,可能检查比预期更多的行(全表扫描)。对于 SELECT COUNT(*) 类型的请求如果不要求精度,建议使用 SHOW TABLE STATUS 或 EXPLAIN 替代。

不建议使用 SELECT * 类型查询

  • Item: COL.001

  • Severity: L1

  • Content: 当表结构变更时,使用 * 通配符选择所有列将导致查询的含义和行为会发生更改,可能导致查询返回更多的数据。

1
2
3
4
5
6
7
8

现在是完全根据SQL语句进行分析的,因为没有连接到`mysql`。可以看到,给出的报告也很详细,但是只是空壳子,仅凭`SQL`语句给出的分析并不是准确的,所以我们开始接下来的应用。


### 2. 连接`mysql`生成`EXPLAIN`分析报告


我们可以在配置文件中配置好`mysql`相关的配置,操作如下:

go复制代码vi soar.yaml

yaml format config file

online-dsn:
addr: 127.0.0.1:3306
schema: asong
user: root
password: root1997
disable: false

test-dsn:
addr: 127.0.0.1:3306
schema: asong
user: root
password: root1997
disable: false

1
2

配置好了,我们来实践一下子吧:

shell复制代码$ echo “SELECT id,username,nickname,password,salt,avatar,uptime FROM users WHERE username = ‘asong1111’” | soar.darwin-amd64 -test-dsn=”root:root1997@127.0.0.1:3306/asong” -allow-online-as-test -log-output=soar.log
$ # Query: D12A420193AD1674

★ ★ ★ ★ ★ 100分

​

1
2
3
4
5
6
7
8

SELECT
id, username, nickname, PASSWORD, salt, avatar, uptime
FROM
users
WHERE
username = 'asong1111'
​

Explain信息

id select_type table partitions type possible_keys key key_len ref rows filtered scalability Extra
1 SIMPLE users NULL const username username 258 const 1 ☠️ 100.00% ☠️ O(n) NULL

Explain信息解读

SelectType信息解读

  • SIMPLE: 简单SELECT(不使用UNION或子查询等).

Type信息解读

  • const: const用于使用常数值比较PRIMARY KEY时, 当查询的表仅有一行时, 使用system. 例:SELECT * FROM tbl WHERE col = 1.
1
2
3
4
5
6
7
8

这回结果中多了EXPLAIN信息分析报告。这对于刚开始入门的小伙伴们是友好的,因为我们对`Explain`解析的字段并不熟悉,有了它我们可以完美的分析`SQL`中的问题,是不是很棒。


### 3. 语法检查


`soar`工具不仅仅可以进行`sql`语句分析,还可以进行对`sql`语法进行检查,找出其中的问题,来看个例子:

shell复制代码$ echo “selec * from users” | soar.darwin-amd64 -only-syntax-check
At SQL 1 : line 1 column 5 near “selec * from users” (total length 18)

1
2
3
4
5
6
7
8

这里`select`关键字少了一个`t`,运行该指令帮助我们一下就定位了问题,当我们的`sql`语句很长时,就可以使用该指令来辅助我们检查`SQL`语句是否正确。


### 4. SQL美化


我们日常开发时,经常会看其他人写的代码,因为水平不一样,所以有些`SQL`语句会写的很乱,所以这个工具就派上用场了,我们可以把我们的`SQL`语句变得漂亮一些,更容易我们理解哦。

shell复制代码$ echo “SELECT id,username,nickname,password,salt,avatar,uptime FROM users WHERE username = ‘asong1111’” | soar.darwin-amd64 -report-type=pretty

SELECT
id, username, nickname, PASSWORD, salt, avatar, uptime
FROM
users
WHERE
username = ‘asong1111’;


这样看起来是不是更直观了呢~~。


结尾
--


因为我也才是刚使用这个工具,更多的玩法我还没有发现,以后补充。更多玩法可以自己研究一下,github传送门:[github.com/XiaoMi/soar…](https://github.com/XiaoMi/soar%E3%80%82%E5%AE%98%E6%96%B9%E6%96%87%E6%A1%A3%E5%85%B6%E5%AE%9E%E5%BE%88%E7%B2%97%E7%B3%99%EF%BC%8C%E6%9B%B4%E5%A4%9A%E6%96%B9%E6%B3%95%E8%A7%A3%E9%94%81%E8%BF%98%E8%A6%81%E9%9D%A0%E8%87%AA%E5%B7%B1%E7%A0%94%E7%A9%B6%EF%BC%8C%E6%AF%95%E7%AB%9F%E6%BA%90%E7%A0%81%E5%B7%B2%E7%BB%8F%E7%BB%99%E6%88%91%E4%BB%AC%E4%BA%86%EF%BC%8C%E5%AF%B9%E4%BA%8E%E5%AD%A6%E4%B9%A0%60go%60%E4%B9%9F%E6%9C%89%E4%B8%80%E5%AE%9A%E5%B8%AE%E5%8A%A9%EF%BC%8C%E5%BD%93%E4%BD%9C%E4%B8%80%E4%B8%AA%E5%B0%8F%E9%A1%B9%E7%9B%AE%E6%85%A2%E6%85%A2%E4%BC%98%E5%8C%96%E5%B2%82%E4%B8%8D%E6%98%AF%E6%9B%B4%E5%A5%BD%E5%91%A2%EF%BD%9E%EF%BD%9E%E3%80%82)


**好啦,这一篇文章到这就结束了,我们下期见~~。希望对你们有用,又不对的地方欢迎指出,可添加我的golang交流群,我们一起学习交流。**


**结尾给大家发一个小福利吧,最近我在看[微服务架构设计模式]这一本书,讲的很好,自己也收集了一本PDF,有需要的小伙可以到自行下载。获取方式:关注公众号:[Golang梦工厂],后台回复:[微服务],即可获取。**


**我翻译了一份GIN中文文档,会定期进行维护,有需要的小伙伴后台回复[gin]即可下载。**


**翻译了一份Machinery中文文档,会定期进行维护,有需要的小伙伴们后台回复[machinery]即可获取。**


**我是asong,一名普普通通的程序猿,让gi我一起慢慢变强吧。我自己建了一个`golang`交流群,有需要的小伙伴加我`vx`,我拉你入群。欢迎各位的关注,我们下期见~~~**


![](https://gitee.com/songjianzaina/juejin_p8/raw/master/img/e3920b33cdca91623d82db33c70bfd0ffffe3b27e4a43e0f34530e455bc125fd)


推荐往期文章:


* [machinery-go异步任务队列](https://mp.weixin.qq.com/s/4QG69Qh1q7_i0lJdxKXWyg)
* [十张动图带你搞懂排序算法(附go实现代码)](https://mp.weixin.qq.com/s/rZBsoKuS-ORvV3kML39jKw)
* [Go语言相关书籍推荐(从入门到放弃)](https://mp.weixin.qq.com/s/PaTPwRjG5dFMnOSbOlKcQA)
* [go参数传递类型](https://mp.weixin.qq.com/s/JHbFh2GhoKewlemq7iI59Q)
* [手把手教姐姐写消息队列](https://mp.weixin.qq.com/s/0MykGst1e2pgnXXUjojvhQ)
* [常见面试题之缓存雪崩、缓存穿透、缓存击穿](https://mp.weixin.qq.com/s?__biz=MzIzMDU0MTA3Nw==&mid=2247483988&idx=1&sn=3bd52650907867d65f1c4d5c3cff8f13&chksm=e8b0902edfc71938f7d7a29246d7278ac48e6c104ba27c684e12e840892252b0823de94b94c1&token=1558933779&lang=zh_CN#rd)
* [详解Context包,看这一篇就够了!!!](https://mp.weixin.qq.com/s/JKMHUpwXzLoSzWt_ElptFg)
* [go-ElasticSearch入门看这一篇就够了(一)](https://mp.weixin.qq.com/s/mV2hnfctQuRLRKpPPT9XRw)
* [面试官:go中for-range使用过吗?这几个问题你能解释一下原因吗](https://mp.weixin.qq.com/s/G7z80u83LTgLyfHgzgrd9g)
* [学会wire依赖注入、cron定时任务其实就这么简单!](https://mp.weixin.qq.com/s/qmbCmwZGmqKIZDlNs_a3Vw)
* [听说你还不会jwt和swagger-饭我都不吃了带着实践项目我就来了](https://mp.weixin.qq.com/s/z-PGZE84STccvfkf8ehTgA)
* [掌握这些Go语言特性,你的水平将提高N个档次(二)](https://mp.weixin.qq.com/s/7yyo83SzgQbEB7QWGY7k-w)


**本文转载自:** [掘金](https://juejin.cn/post/6902399174080692238)

*[开发者博客 – 和开发相关的 这里全都有](https://dev.newban.cn/)*

「面试」拿到小红书的意向书

发表于 2020-12-04

上一篇给大家分享了B站的面试之旅,大家的反响还不错,居然催更了,小手不禁颤抖。所以决定把剩下的这些公司给安排明白了。这不,今天就看看小红书服务端/后台面了啥,不为别的,就想遇到漂亮的HR小姐姐,开工。

大纲

大纲

一面


一面面试官看着二十七八岁,文质彬彬,这哪里是写代码的,头发都飘起来了好么。上来就干项目,由于大家的项目都不太一样,所以对于项目部分我就说说我面试的时候经常遇到的问题

  • 描述下项目

一口是吃不了胖子的,描述之前先憋着气掂量掂量自己所说的东西能不能唬住自己,然后唬住面试官。

  • 项目中担任的角色

对于大多数的我们而言,就是开发的角色,同样的道理,角色对应相应的职务,阐述自己做的内容能引面试官上钩,拉钩上吊一百年不许变。

  • 在项目遇到什么困难

这三个问题,是不是可以拎着脚趾拇都可以想出来,除非不是你做的,哈哈哈哈哈。不慌,不是我们做的也不怕,我们必须知道有个网站叫做Github,大牛这么多,自己不是大牛,难道不会学学人家麦。Clone下来,搭建环境跑起来,开始调试修改,通过将模块拆分,进一步修改,这不就是你的项目吗,当然我不怎么建议大家这么操作啦。

项目被问的差不多了,开始怼基础知识,基础知识老四套,计算机网络,数据库,操作系统,数据结构(来吧,时刻准备着,真没吹牛逼)

我看你简历中写着网络流量的还原,你应该对计算机网络比较熟悉?(注意哈,简历上写上去的东西,自己心里一定要有点B数),那我们说说计算机网络

  • 说说计算机网络中TCP的三次握手吧

首先 Client 给 Server 发送一个SYN包,Server 接收到 SYN 回复 SYN+ACK,然后客户端回复 ACK 表示收到。

你这样回答肯定是不会让面试官满意的,那就加点配料,不放佐料的菜怎么香?那就详细的安排一下

首先客户端的协议栈向服务端发送SYN包,同时告诉服务端当前发送的序列号是X,此时客户端进入 SYNC_SENT状态

服务端的协议栈收到这个包以后,使用 ACK 应答,此时应答的值为 X+1,表示对 SYN 包 J 的确认,同时服务端也发送一个SYN包,告诉客户端当前我的发送序列号是Y,此时服务端进入SYNC_RCVD状态

客户端协议栈收到 ACK 以后,应用程序通过connect调用表示服务端的单向连接成功,此时状态为ESTABLISHED,同时客户端协议栈对服务器端的 SYN 进行应答,此时数据为Y+1

服务端收到客户端的应答包,通过accept阻塞调用返回,此时服务端到客户单的单向连接也建立成功,服务器将进入ESTABLISHED状态

这样是不是稍微有B格一点呢,而且还比较形象,当然为了加深大家对这个过程的印象,我再举个例子

***第一次握手:***小蓝给某女娃告白,说我喜欢你,然后我傻乎乎的等着回应

第二次握手:女生看我这颜值,秒回,自然就答应我啊,并回复我也喜欢你拉

第三次握手:我收到女生的回应说:“那晚上去吃火锅,看电影,理疗”

就这样在一起啦,那么后续是啥样呢?是不是得往下看看什么是四次挥手了(渣男石锤),非也,还在热恋期呢,专一的好吗。面试官会继续问你三次握手

面试官说:“那我问你,如果客户端发送的SYN丢失了或者其他原因导致Server无法处理,是什么原因?

这个场景非常常见,没有万无一失。在TCP的可靠传输中,如果SYN包在传输的过程中丢失,此时Client段会触发重传机制,但是也不是无脑的一直重传过去,重传的次数是受限制的,可以通过 tcp_syn_retries 这个配置项来决定。如果此时 tcp_syn_retries 的配置为3,那么其过程如下

TCP重传
当 Client 发送 SYN 后,如果过了1s还没有收到 Server 的回应,那么进行第一次的重传。如果经过了2s没有收到Sever的响应进行第二次的重传,一直重传tcp_syn_retries次。这里的重传三次,意味着当第一次发送SYN后,需要等待(1 +2 +4 +8)秒,如果还是没有响应,connect就会通过ETIMEOUT的错误返回。

说说四次挥手吧,哎,卑微的蓝蓝

第一次挥手:女生觉得和这个男生不太合适,但是是个好人,决定提出分手,等待男生回应

第二次挥手:这男生吧,也是会玩儿,直接说:”分就分“

第三次挥手:过了一段时间,男生觉得好没得面子:”我一个大老爷们,应该是我提出分手啊”,于是给女生说:我们分手吧

第四次挥手:女生看到这个消息,你是「憨批」还是「神经病」?

TIMEWAIT了解哈,过多的TIMEWAIT怎么办,什么原因造成的?

回答问题的方法无外乎即是什么,为什么会出现以及可以解决的方案

在TCP的四次挥手过程中,发起连接断开的一方会进入TIME_WAIT的状态。通常一个TCP连接通过对外开发端口的方式提供服务,在高并发的情况下,每个连接占用一个端口,但是端口是有限的以致于可能导致端口耗尽,所以就会出现’”服务时而好时而坏的情况“。

如下图所示的TCP四次挥手,TCP连接准备终止的时候会发送FIN报文,主机2进入CLOSE_WAIT状态并发送ACK应答。主机1会在TIMEWAIT停留2MSL的时间。

为什么不直接进入CLOSE转态,而是需要先等待2MSL,这段时间在干啥?

第一个原因是为了确保最后的ACK能够正常接收,从而有效的正常关闭。怎么理解,科学家们在设计TCP的时候,假设TCP】 报文会出错从而开始重传,如果主机1的报文没有传输成功,那么主机2就会重发FIN报文,此时主机1没有维护TIME_WAIT状态,就会失去上下文从而恢复RST,导致服被动关闭一方出现错误。

四次挥手
第二个原因是让旧链接的重复分节在网络中自然消失。

一次网络通信可能经过无数个路由器,交换机,不知道到底会是哪个环节出问题。我们为了标识一个连接,通过四元组的方式[源IP,源端口,目的IP,目的端口]。假设此时两个连接A,B。A连接在中途中断了,此时重新创建B连接,这个B连接的四元组和A连接一样,如果A连接经过一段时间到达了目的地,那么B连接很有可能被认为是A连接的一部分,这样就会造成混乱。所以TCP设置了这样一个机制,让两个方向的分组都被丢弃。

那么TIME_WAIT有哪些危害?

过多的连接势必造成内存资源的浪费

对端口的占用。可开启的端口也就32768~61000

有没有对TCP进行优化过

开玩笑,这东西复习过,尽管问,锤子不怕。优化的点很多,随便提一点,让后比较深的描述下这个过程就行比如调整哪些参数在某些特定的条件下会最优

我们应该都知道半连接,即收到SYN以后没有回复SYN+ACK的连接,那么Server每收到新的SYN包,都会创建一个半连接,然后将这个半连接加入到半连接的队列(syn queue)中,syn queue的长度又是有限的,可以通过tcp_max_syn_backlog进行配置,当队列中积压的半连接数超过了配置的值,新的SYN包就会被抛弃。对于服务器而言,可能瞬间多了很多新的连接,所以通过调大该值,以防止SYN包被丢弃而导致Client收不到SYN+ACK。

就这样是不是就可以让面试官感觉,这小伙子有点东西。那怎么配置呢

配置syn queue

你以为面试官是傻子?当然不是,万一面试官问你:半连接积压较多,还有其他的原因?

哈哈哈,这说明面试官上钩了哇,来,我们看看还有啥原因

还有可能是因为恶意的Client在进行SYN Flood攻击。

SYN Flood攻击是个啥过程?

首先Client以较高频率发送SYN包,且这个SYN包的源IP不停的更换,对于Server来说,这是新的链接,就会给它分配一个半连接

Server的SYN+ACK会根据之前的SYN包找IP,发现不是原来的IP,所以无法收到Client的ACK包,从而导致无法正确的建立连接,自然就让Server的半连接队列耗尽,无法响应正常的SYN包

那有没有什么方案解决这个问题?

那必须,毕竟面试嘛,需要让面试官问我们知道的内容。在Linux内核中引入了SYN Cookies机制,那看看这个机制是啥意思

首先Server收到SYN包,不分配资源保存Client的信息,而是根据SYN计算出Cookie值,然后将Cookie记录到SYN ACK并发送出去

如果是正常的情况,这个Cookies值会伴随着Client的ACK报文带回来

Server会根据这个Cookies检查ACK包的合法性,合法则创建连接

那么开启SYN Cookies的方法?

SYN Cookies

SYN Cookies

网络问到这就差不多了,挺好的,完全按照我的套路出牌。开始怼我操作系统

  • 说下什么是大页内存

我擦,我差点没反应过来,”大爷内存”,不过确实牛逼,大页内存,记住了,是大页内存。

我们知道操作系统堆内存的管理采用多级页表和分页进行管理,操作系统给每个页的默认大小是4KB。假设当前进程使用的内存比较大为1GB,那么此时在页表中会占用1GB/4KB=26211个页表项,但是系统的TLB可容乃的页表项远远小于这个数量。所以当多个内存密集型应用访问内存的时候,就会导致过多的TLB没有命中,因此在特定的情况下会需要减少未命中次数,一个可行的办法即是增大每个页的尺寸。

操作系统默认支持的大页为2MB,当使用1GB内存的时候,页表将占用512页表项,大大的提高TLB命中率从而提高性能。另外需要注意的是,大页内存分配的是物理内存,所以不会有换出磁盘的操作,所以没有缺页中断,也就不会引入访问磁盘的时延。

行,差不多时间了,写个简单代码吧,实现一个无重复字符的最长子串

思路:使用滑动窗口保证每个窗口的字母都是唯一的

  • 使用 vector m 来记录一个字母如果后面出现重复时,i 应该调整到的新位置
  • 所以每次更新的时候都会保存 j + 1 ,即字母后面的位置
  • j 表示子串的最后一个字母,计算子串长度为 j - i + 1

无重复字符的最长子串


二面


一面感觉还不错,果不其然二面来了,HR小姐姐打电话通知周三二面,行,对于从来不迟到的暖蓝,肯定守时。拿着咖啡,等到2:30,至于为什么拿着茶,这是我的习惯,面试前喝杯茶等待面试官的捧击(面试官其实大部分很温柔的啦)。

可耐,面试官到点了居然还没来,等不及的我打电话给HR,HR说不好意思,得等几分钟,行,对这甜美的声音我忍了,可是等了十分钟都没音信,我下午还有个笔试,无奈给HR说,我下午还有事儿,要不改天面?

不知道什么情况,直接说,我马上给你换个面试官,我擦,还有这种事儿,我这乡卡卡的娃儿有这种的待遇?是我一面表现的太太突出?不会吧,反正小红书我爱了。

“staty with me”响起,这正是我的手机铃声。。

“您好”

“你好,请问是XX?”

“嗯嗯,你好面试官”

“我是你的二面面试官,先自我介绍吧”

我叫小蓝,来自XX大学,本科XX,硕士XXX,期间做了XX,谢谢面试官。自我介绍不用那么花里胡哨,挑重点说,不会在意你本科谈了几次恋爱,也不会在意你XXXX,简单明了完事,开始二面

  • 应该学过C的吧,用C实现多态怎么个思路

至于这个题,我还是比较惊讶的,怎么突然问到了C,想了想可能还是考虑对于面向对象中多态,继承等的理解。

多态无外乎就是编译时多态和运行时多态,编译时多态理解为重载,运行时多态理解为重写。那么要实现重载,需要用到c中的宏,V_ARGS。

c实现重载
理解上面的方法,实现多态就更轻松了

c实现多态

感觉没啥问的,先写个代码,二路归并

哈哈,让我想起了歌词”来左边跟我花个龙,在你右边画一道彩虹”(脑补画面)

停!!这是我之前说过的常考算法之一,中心思想即分治,可通过递归一直拆分,递归的结束条件即不可再分,即分为1个的时候就停止。从第一个开始时将每一个模块当作一个已经排序好的数组,有如双指针,在两个数组头设立指针,进行值的比较,然后插入到新数组中,上代码咯

归并排序

归并排序

倒排索引了解不?

假设我这里有几十本文档,每个文档题目不一样,如果我给你文档的题目,你可能很快就可以找到相应的文档。但是如果我让你找论文中包含”暖”和“蓝”这两个字,你可能直接给我”两儿巴“。因为多半很难很快就找出来。从稍微专业的角度来分,前一种是正排索引,后一个是倒排索引。

我们先看简单的正排索引。此时给每个文档一个唯一ID,然后使用哈希表将文档的ID作为键,将文档内容作为键对应的值。这样我们就可以在O(1)的时间代价完成key的检索。这也正是正排索引

正排索引
这里遍历哈希表的时间代价为O(n)。每遍历一个文档都需要遍历每个字符判断是否包含两字。假设每个文档的平均长度为k,那么遍历一个文档的时间代价为O(K)。

有没有什么优化的方法?

其实以上就是两种方案,一种是根据题目找到内容,另一种是根据关键字查找题目。这完全相反的方案,那我们反着试试

我们将关键字当做key,将包含这个关键字的文档的列表当做存储的内容。同样建立一个哈希表,在O(1)的时间我就可以找到包含该关键字的文档列表。这种根据内容或者字段反过来的索引结构即倒排索引。

如何创建倒排索引?

  • 首先给文档编个号表示唯一表示,然后排序遍历文档
  • 解析每个文档的关键字并生成<关键字,文档ID,关键字位置>。这里的关键字位置主要是为了检索的时候显示关键字前后信息
  • 将关键字key插入哈希表。如果哈希表已存在这个key,就在对应的posting list中追加节点,记录文档ID。如果哈希表没有响应的key则插入该key并创建posting list和对应的节点
  • 重复2 3步处理完所以文档

创建倒排索引

如果要查询同时包含”暖”“蓝”两个key怎么办?

顺藤摸瓜啦,分别用两个key去倒排索引中检索,这样使用的两个不同list:A和B。A中的文档都包含了”暖”字,B中的文档都包含了”蓝”字。如果文档即出现”暖”也出现”蓝”,是不是就正好包含了两个字?所以只需要找到AB公共元素即可

如何找到AB两个链表的公共元素?希望小伙伴们思考下,经常在手撕算法中被问到

  • 首先使用两个指针P1 P2分别指向有序链表AB的第一个元素
  • 然后对比两个指针所指节点是否相同,这可能出现三种情况
    • 两者id相同则是公共元素,直接归并即可,然后P1 P2后移
    • p1元素小于p2元素,p1后裔,指向A链表的下一个元素
    • p1元素大于p2元素,p2后裔,指向B链表中下一个元素
  • 重复第二步 直到p1和p2移动到链表尾

链表公共元素

链表公共元素

你说使用过kafka,那么使用消息队列的时候如何保证只消费一次?

首先引入kafka等消息队列是为了对峰值写流量做削峰填谷,对不同系统做解耦合。

举个例子,我们开发了一个电商系统,其中一个功能是当用户购买了A产品5份就送一个红包从而鼓励用户消费。但是如果在消息传递的过程中丢失了,用户很可能会因为没有收到红包而不开心,甚至取消订单,在这里如何保证消息被消费到且一次?

我们先看看这个消息写入消息队列会有几个阶段,首先有消息从生产者写入消息到队列,消息存储在消息队列,消息被消费者消费这个阶段,任何一个阶段都有可能丢失,我们分别查看这几个阶段
丢失的三种可能

第一个阶段:消息生产

消息的生产通常会是业务服务器,业务服务器和独立部署的消息队列服务器通过内网通信,很可能因为网络抖动导致消息的丢失,这样可以采用消息重传的机制保证消息的送达。但是容易出现重复消费的情况,意思收到两个红包,用户开心了,但是。。。

第二个阶段:在队列中丢失

kafka为了减少消息存储对磁盘的随机IO,采用的异步刷盘的方式将消息存储在磁盘中。

我看你简历上打过acm,说说你的策略或者经历吧

哈哈哈,终于到我正儿八经吹水的时候了。低调,才是最牛逼的炫耀

写个验证邮箱的正则

当时没有写出来,确实记不住,每次都是用的时候才去查,谁知道面试的时候遇见谁呢,手撕KMP?这里给大家个答案,后续我详细安排一篇正则的套路

实现验证邮箱的正则

实现验证邮箱的正则

了解内存映射?说说,尽量说

既然是尽量说,就不客气了。从什么是内存到如何查看服务器内存,最后怎么能够更好地用好内存来答就完事

首先内存作为存储系统和应用程序的指令,数据等。在Linux中,管理内存使用到了内存映射。平时我们经常说的内存容量,主要指的是物理内存,也叫做主存。只有内核才能直接访问,那么问题来了,进城如果要访问内存怎么办呢?

Linux内核为每个进程提供了一个虚拟地址空间且空间地址连续,这样的话,进程访问虚拟内存将非常的方便。

虚拟地址又分为内核空间和用户空间,不同字长的处理器地址范围也不同。我们下面分别看看32位和64位的虚拟地址空间

内核空间与用户空间

内核空间与用户空间

从这个图很明显的看出32位系统中内核空间1G,而64位的内核空间与用户空间都是128T。

内存映射即虚拟内存地址映射到物理内存地址,完了顺利完成映射,需要给每个进程维护一张页表记录两者的关系。
虚拟地址到物理地址的转化

这样,如果进程访问的虚拟地址不在则通过缺页异常进入内核空间分配物理内存,更新进程页表,最终返回用户空间。

说到虚拟内存又不得不说说用户空间的各个段

用户空间各个段

用户空间各个段

忍不住悄悄咪咪问了下HR,二面面试官对我的评价,基础和code的能力不错,项目讲述的不清楚

  • 我自己可能没有把项目更本质的东西理解清楚
  • 从事的不同的方向,有些专业术语的理解的不同)

三面

三面面试官,真的不能用”秃”来描述了,就感觉我的眼睛被闪了一分钟,怎么说,面嘛

线程的锁有哪些,我说到了读写锁打断我了,问我读写锁会有什么些问题,无非就是写锁饥饿问题,我说没看过内核源码,然后如果让我来实现,我怎么来避免

分布式Hash表,当进行扩容的时候(会花费很长的时间),我说P肯定一定要保证的,CA只能选其一,但是我们可以使用弱一致性来保证其可用性

多个随机Request请求,然后不同的请求有不同的权重,进行随机抽样,要求权重大更可能被抽到

有了解过RPC?

RPC翻译过来为远程过程调用。帮助我们屏蔽网络细节,实现调用远程方法就跟调用本地一样的体验。举个例子,如果没有桥,我们要过河只好划船,绕道等方式,如果有桥,我们就像在路面行走一样自如到目的地。

RPC的通信流程是怎样的?

刚才说RPC屏蔽了网络细节,也就是意味着它处理好了网络部分,它为了保证可靠性,默认采用TCP传输,网络传输的数据是二进制,但是调用所请求的参数是对象,所以需要将对象转换为二进制,这就需要用到序列化技术

服务提供方接收到数据以后,并不知道哪里是结尾,所以需要一些边界条件来标识请求的数据哪里是开始,哪里是结束,就像高速路上各种指路牌引领我们前进的方向。这种格式的约定叫做“协议”

根据协议规定的格式,就可以正确的提取出相应的请求,根据请求的类型和序列化的类型,将二进制消息体逆向还原为请求对象,这叫做反序列化

服务提供方通过反序列化的对象找到对应的实现类完成整正的调用,这样就是一次rcp的调用。画个图加深下印象

RPC过程

RPC过程

其他问的一些问题感觉在前面的面试问过了就没有写在这部分内容了,还问了几个数据库的问题,很常规的了,之前的文章写过,篇幅太长,看着累,要不先三连,我们下期再见?么么哒

5 总结

请记下以下几点:

  • 公司招你去是干活了,不会因为你怎么怎么的而降低对你的要求标准。
  • 工具上面写代码和手撕代码完全不一样。
  • 珍惜每一次面试机会并学会复盘。
  • 对于应届生主要考察的还是计算机基础知识的掌握,项目要求没有那么高,是自己做的就使劲抠细节,做测试,只有这样,才知道会遇到什么问题,遇到什么难点,如何解决的。从而可以侃侃而谈了。
  • 非科班也不要怕,怕了你就输了!一定要多尝试。

img

img


我是小蓝,一个专为大家分享面试经验的蓝人。如果觉得文章不错或者对你有点帮助,感谢分享给你的朋友,也可在下方给小蓝给个在看,这对小蓝非常重要,谢谢你们,下期再会。

本文转载自: 掘金

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

Spring Security 学习之原理篇

发表于 2020-12-04

接上一篇,我们已经熟悉了Spring Security的基本使用,并且实操了几个范例。

本文我们将在这几个范例的基础上进行调试,并深入研究其在源码层面上的实现原理。只有知其然,知其所以然,才能在Spring Security的使用上更加得心应手。

本文主要参考了官方文档9-11章节,在文档基础上进行丰富、拓展和提炼。

Spring Security初始化

SecurityAutoConfiguration

按照Spring Boot Starter的套路,一定有一个Configuration提供一些默认的Bean注入。而在Spring Security中,SecurityAutoConfiguration承担着该角色:

1
2
3
4
5
6
7
8
9
10
11
12
13
java复制代码@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(DefaultAuthenticationEventPublisher.class)
@EnableConfigurationProperties(SecurityProperties.class)
@Import({ SpringBootWebSecurityConfiguration.class, WebSecurityEnablerConfiguration.class,
SecurityDataConfiguration.class })
public class SecurityAutoConfiguration {
@Bean
@ConditionalOnMissingBean(AuthenticationEventPublisher.class)
public DefaultAuthenticationEventPublisher authenticationEventPublisher(ApplicationEventPublisher publisher) {
return new DefaultAuthenticationEventPublisher(publisher);
}

}

重点关注导入了WebSecurityEnablerConfiguration.class :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
java复制代码/**
* If there is a bean of type WebSecurityConfigurerAdapter, this adds the
* {@link EnableWebSecurity @EnableWebSecurity} annotation. This will make sure that the
* annotation is present with default security auto-configuration and also if the user
* adds custom security and forgets to add the annotation. If
* {@link EnableWebSecurity @EnableWebSecurity} has already been added or if a bean with
* name {@value BeanIds#SPRING_SECURITY_FILTER_CHAIN} has been configured by the user,
* this will back-off.
*
* @author Madhura Bhave
* @since 2.0.0
*/
@Configuration(proxyBeanMethods = false)
@ConditionalOnBean(WebSecurityConfigurerAdapter.class)
@ConditionalOnMissingBean(name = BeanIds.SPRING_SECURITY_FILTER_CHAIN)
@ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.SERVLET)
@EnableWebSecurity
public class WebSecurityEnablerConfiguration {
}

从注释可以看出,WebSecurityEnablerConfiguration的意义是注入一个名为”springSecurityFilterChain”的bean。但如果用户已经指定了同名的bean,则这里就不注入。

再来关注下注解 @EnableWebSecurity

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
java复制代码/**
* Add this annotation to an {@code @Configuration} class to have the Spring Security
* configuration defined in any {@link WebSecurityConfigurer} or more likely by extending
* the {@link WebSecurityConfigurerAdapter} base class and overriding individual methods:
*
* <pre class="code">
* &#064;Configuration
* &#064;EnableWebSecurity
* public class MyWebSecurityConfiguration extends WebSecurityConfigurerAdapter {
*
* &#064;Override
* public void configure(WebSecurity web) throws Exception {
* web.ignoring()
* // Spring Security should completely ignore URLs starting with /resources/
* .antMatchers(&quot;/resources/**&quot;);
* }
*
* &#064;Override
* protected void configure(HttpSecurity http) throws Exception {
* http.authorizeRequests().antMatchers(&quot;/public/**&quot;).permitAll().anyRequest()
* .hasRole(&quot;USER&quot;).and()
* // Possibly more configuration ...
* .formLogin() // enable form based log in
* // set permitAll for all URLs associated with Form Login
* .permitAll();
* }
*
* &#064;Override
* protected void configure(AuthenticationManagerBuilder auth) throws Exception {
* auth
* // enable in memory based authentication with a user named &quot;user&quot; and &quot;admin&quot;
* .inMemoryAuthentication().withUser(&quot;user&quot;).password(&quot;password&quot;).roles(&quot;USER&quot;)
* .and().withUser(&quot;admin&quot;).password(&quot;password&quot;).roles(&quot;USER&quot;, &quot;ADMIN&quot;);
* }
*
* // Possibly more overridden methods ...
* }
* </pre>
*
* @see WebSecurityConfigurer
* @see WebSecurityConfigurerAdapter
*
* @author Rob Winch
* @since 3.2
*/
@Retention(value = java.lang.annotation.RetentionPolicy.RUNTIME)
@Target(value = { java.lang.annotation.ElementType.TYPE })
@Documented
@Import({ WebSecurityConfiguration.class,
SpringWebMvcImportSelector.class,
OAuth2ImportSelector.class })
@EnableGlobalAuthentication
@Configuration
public @interface EnableWebSecurity {

/**
* Controls debugging support for Spring Security. Default is false.
* @return if true, enables debug support with Spring Security
*/
boolean debug() default false;
}

注释里讲得很清楚,这个注解@EnableWebSecurity是为了向实现WebSecurityConfigurer或者是继承 WebSecurityConfigurerAdapter的实例暴露Spring Security的配置API入口。API入口分为三类:

1
2
3
4
5
java复制代码public void configure(WebSecurity web) throws Exception {//...}

protected void configure(HttpSecurity http) throws Exception {//..}

protected void configure(AuthenticationManagerBuilder auth) throws Exception {// ...}

所以用户只要通过实现WebSecurityConfigurer接口(或者继承WebSecurityConfigurerAdapter)覆盖configure方法中的若干个,即可实现自定义登录安全逻辑。正如我们在上一篇文章所写的第一个样例:

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
java复制代码public class GeneralSecurityConfiguration extends WebSecurityConfigurerAdapter {

private static final String[] BY_PASS_URLS = {"/styles/**", "/views/**", "/img/**", "/i18n/**", "/health"};

@Override
protected void configure(HttpSecurity http) throws Exception {
// 关闭跨站伪造攻击检查
http.csrf().disable();

// 设置X-Frame-Options: SAMEORIGIN
http.headers().frameOptions().sameOrigin();

// 部分访问路径进行权限控制
http.authorizeRequests()
.antMatchers(BY_PASS_URLS).permitAll()
.antMatchers("/**").authenticated();
// 自定义登录页面
http.formLogin()
.loginPage("/login")
.defaultSuccessUrl("/index", true)
.permitAll()
.failureUrl("/login").and();

// 自定义登出页面
http.logout()
.logoutUrl("/logout")
.invalidateHttpSession(true)
.clearAuthentication(true)
.logoutSuccessUrl("/login");

// 自定义异常跳转页面
http.exceptionHandling()
.authenticationEntryPoint(new LoginUrlAuthenticationEntryPoint("/login"))
.and()
.exceptionHandling().accessDeniedHandler(new AccessDeniedHandlerImpl());

// 配置remember-me
rememberMe(http);
}

protected void rememberMe(HttpSecurity http) throws Exception {
http.rememberMe().rememberMeServices(new NullRememberMeServices());
}
}

EnableWebSecurity

我们主要关注EnableWebSecurity注解注入了WebSecurityConfiguration.class以及引入了另外一个注解@EnableGlobalAuthentication。我们逐个来看看都做了哪些操作。

先来看看WebSecurityConfiguration的核心逻辑:

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
java复制代码public class WebSecurityConfiguration implements ImportAware, BeanClassLoaderAware {
private WebSecurity webSecurity;

@Bean(name = AbstractSecurityWebApplicationInitializer.DEFAULT_FILTER_NAME)
public Filter springSecurityFilterChain() throws Exception {
boolean hasConfigurers = webSecurityConfigurers != null
&& !webSecurityConfigurers.isEmpty();
if (!hasConfigurers) {
WebSecurityConfigurerAdapter adapter = objectObjectPostProcessor
.postProcess(new WebSecurityConfigurerAdapter() {
});
webSecurity.apply(adapter);
}
return webSecurity.build();
}

@Autowired(required = false)
public void setFilterChainProxySecurityConfigurer(
ObjectPostProcessor<Object> objectPostProcessor,
@Value("#{@autowiredWebSecurityConfigurersIgnoreParents.getWebSecurityConfigurers()}") List<SecurityConfigurer<Filter, WebSecurity>> webSecurityConfigurers)
throws Exception {
webSecurity = objectPostProcessor
.postProcess(new WebSecurity(objectPostProcessor));
if (debugEnabled != null) {
webSecurity.debug(debugEnabled);
}

webSecurityConfigurers.sort(AnnotationAwareOrderComparator.INSTANCE);

Integer previousOrder = null;
Object previousConfig = null;
for (SecurityConfigurer<Filter, WebSecurity> config : webSecurityConfigurers) {
Integer order = AnnotationAwareOrderComparator.lookupOrder(config);
if (previousOrder != null && previousOrder.equals(order)) {
throw new IllegalStateException(
"@Order on WebSecurityConfigurers must be unique. Order of "
+ order + " was already used on " + previousConfig + ", so it cannot be used on "
+ config + " too.");
}
previousOrder = order;
previousConfig = config;
}
for (SecurityConfigurer<Filter, WebSecurity> webSecurityConfigurer : webSecurityConfigurers) {
webSecurity.apply(webSecurityConfigurer);
}
this.webSecurityConfigurers = webSecurityConfigurers;
}
// ...
}

以上删减了大部分代码,只关心重要的两件事情:

  • 第一件事,在方法setFilterChainProxySecurityConfigurer上注入用户自定义的WebSecurityConfigurers,并新建了一个WebSecurity,并将用户的configurers放进了WebSecurity中。这个步骤是通过@Autowire注入的,要比内部的@Bean的优先级更高。
  • 第二件事,调用WebSecurity的build方法,生成并注入了一个名为”springSecurityFilterChain”的bean。这个bean是Spring Security的核心逻辑,后文会详细分析;

可以从build方法debug进去看看WebSecurity注入用户自定义配置的过程,这个不在本文的讨论范围之内,有时间再写一篇有关文章。

DelegatingFilterProxy

Spring Security会自动注册一个DelegatingFilterProxy到Servlet的过滤链中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
java复制代码@Configuration(proxyBeanMethods = false)
@ConditionalOnWebApplication(type = Type.SERVLET)
@EnableConfigurationProperties(SecurityProperties.class)
@ConditionalOnClass({ AbstractSecurityWebApplicationInitializer.class, SessionCreationPolicy.class })
@AutoConfigureAfter(SecurityAutoConfiguration.class)
public class SecurityFilterAutoConfiguration {

private static final String DEFAULT_FILTER_NAME = AbstractSecurityWebApplicationInitializer.DEFAULT_FILTER_NAME;

@Bean
@ConditionalOnBean(name = DEFAULT_FILTER_NAME)
public DelegatingFilterProxyRegistrationBean securityFilterChainRegistration(
SecurityProperties securityProperties) {
DelegatingFilterProxyRegistrationBean registration = new DelegatingFilterProxyRegistrationBean(
DEFAULT_FILTER_NAME);
registration.setOrder(securityProperties.getFilter().getOrder());
registration.setDispatcherTypes(getDispatcherTypes(securityProperties));
return registration;
}

// ...
}

DelegatingFilterProxyRegistrationBean,顾名思义,就是为了注册DelegatingFilterProxy而存在的。getFilter 方法用来获取需要注册过滤器,方法里新建了一个DelegatingFilterProxy对象,入参包含了this.targetBeanName,也就是”springSecurityFilterChain” :

1
2
3
4
5
6
7
8
9
10
11
java复制代码@Override
public DelegatingFilterProxy getFilter() {
return new DelegatingFilterProxy(this.targetBeanName, getWebApplicationContext()) {

@Override
protected void initFilterBean() throws ServletException {
// Don't initialize filter bean on init()
}

};
}

注册完毕后的过滤链是这样子的:

delegatingfilterproxy.png

DelegatingFilterProxy 实现了Filter接口,并且也是一个代理类。所以它会将doFilter方法的调用透传给内部的被代理对象 Filter delegate 。此外,还能实现被代理对象的懒加载:

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复制代码@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain)
throws ServletException, IOException {

// Lazily initialize the delegate if necessary.
Filter delegateToUse = this.delegate;
if (delegateToUse == null) {
// 懒加载
synchronized (this.delegateMonitor) {
delegateToUse = this.delegate;
if (delegateToUse == null) {
WebApplicationContext wac = findWebApplicationContext();
if (wac == null) {
throw new IllegalStateException("No WebApplicationContext found: " +
"no ContextLoaderListener or DispatcherServlet registered?");
}
delegateToUse = initDelegate(wac);
}
this.delegate = delegateToUse;
}
}

// Let the delegate perform the actual doFilter operation.
invokeDelegate(delegateToUse, request, response, filterChain);
}

FilterChainProxy && Securityfilterchain

DelegatingFilterProxy内部的被代理对象delegate其实是一个FilterChainProxy。流程图可以更新为:

filterchainproxy.png

但为什么delegate不是SecurityFilterChain?那是因为WebSecurity在doBuild中,又给它包了一层代理:

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
java复制代码@Override
protected Filter performBuild() throws Exception {
Assert.state(
!securityFilterChainBuilders.isEmpty(),
() -> "At least one SecurityBuilder<? extends SecurityFilterChain> needs to be specified. "
+ "Typically this done by adding a @Configuration that extends WebSecurityConfigurerAdapter. "
+ "More advanced users can invoke "
+ WebSecurity.class.getSimpleName()
+ ".addSecurityFilterChainBuilder directly");
int chainSize = ignoredRequests.size() + securityFilterChainBuilders.size();
List<SecurityFilterChain> securityFilterChains = new ArrayList<>(
chainSize);
for (RequestMatcher ignoredRequest : ignoredRequests) {
securityFilterChains.add(new DefaultSecurityFilterChain(ignoredRequest));
}
for (SecurityBuilder<? extends SecurityFilterChain> securityFilterChainBuilder : securityFilterChainBuilders) {
securityFilterChains.add(securityFilterChainBuilder.build());
}
// 对securityFilterChains链表进行封装
FilterChainProxy filterChainProxy = new FilterChainProxy(securityFilterChains);
if (httpFirewall != null) {
filterChainProxy.setFirewall(httpFirewall);
}
filterChainProxy.afterPropertiesSet();

Filter result = filterChainProxy;
if (debugEnabled) {
logger.warn("\n\n"
+ "********************************************************************\n"
+ "********** Security debugging is enabled. *************\n"
+ "********** This may include sensitive information. *************\n"
+ "********** Do not use in a production system! *************\n"
+ "********************************************************************\n\n");
result = new DebugFilter(filterChainProxy);
}
postBuildAction.run();
return result;
}

值得注意的是FilterChainProxy也是一个特殊的Filter,而且可以看出它是支持对多个securityFilterChain进行代理(详见下一节)!!!FilterChainProxy#doFilterInternal里会按顺序找到第一个满足条件的securityFilterChain,并构建一个VirtualFilterChain:

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复制代码private void doFilterInternal(ServletRequest request, ServletResponse response,
FilterChain chain) throws IOException, ServletException {
// ...
List<Filter> filters = getFilters(fwRequest);
// ...
VirtualFilterChain vfc = new VirtualFilterChain(fwRequest, chain, filters);
vfc.doFilter(fwRequest, fwResponse);
}

/**
* Returns the first filter chain matching the supplied URL.
*
* @param request the request to match
* @return an ordered array of Filters defining the filter chain
*/
private List<Filter> getFilters(HttpServletRequest request) {
for (SecurityFilterChain chain : filterChains) {
if (chain.matches(request)) {
// 返回第一个满足条件的chain内部所有的过滤器
return chain.getFilters();
}
}
return null;
}

而在VirtualFilterChain内部会按顺序触发该securityFilterChain内部的所有过滤器:

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复制代码private static class VirtualFilterChain implements FilterChain {
// 保存最开始的sevlet过滤链
private final FilterChain originalChain;
// securityFilterChain内部的所有过滤器
private final List<Filter> additionalFilters;
@Override
public void doFilter(ServletRequest request, ServletResponse response)
throws IOException, ServletException {
if (currentPosition == size) {
// securityFilterChain内部所有过滤器都已经处理完毕
if (logger.isDebugEnabled()) {
logger.debug(UrlUtils.buildRequestUrl(firewalledRequest)
+ " reached end of additional filter chain; proceeding with original chain");
}

// Deactivate path stripping as we exit the security filter chain
this.firewalledRequest.reset();
// 返回继续处理最开始的sevlet过滤器
originalChain.doFilter(request, response);
}
else {
// 递增处理下一个过滤器
currentPosition++;

Filter nextFilter = additionalFilters.get(currentPosition - 1);

if (logger.isDebugEnabled()) {
logger.debug(UrlUtils.buildRequestUrl(firewalledRequest)
+ " at position " + currentPosition + " of " + size
+ " in additional filter chain; firing Filter: '"
+ nextFilter.getClass().getSimpleName() + "'");
}
// 触发下一个过滤器
nextFilter.doFilter(request, response, this);
}
}
}

所以,流程图更新为:

securityfilterchain.png

为什么使用FilterChainProxy?主要是有三点好处:

  • 使用它作为过滤链的起点,可以方便排查故障的时候,将它作为debug入口。
  • 使用它做一些公共操作。比如清空线程内的SecurityContext,避免内存泄漏;又比如使用HttpFirewall过滤部分类型的攻击请求。
  • 使用它可以支持多个securityFilterChain,不同的securityFilterChain匹配不同的URL。这样可以提供更多的灵活性。如下图所示:

multi-securityfilterchain.png

Security Filters

SecurityFilterChain 内部会包含多个Security Filters。这些 Security Filters都是用户通过configure注册,并通过WebSecurity的build过程生成的。

常见的一些Filter顺序如下:

  • ChannelProcessingFilter
  • ConcurrentSessionFilter
  • WebAsyncManagerIntegrationFilter
  • SecurityContextPersistenceFilter
  • HeaderWriterFilter
  • CorsFilter
  • CsrfFilter
  • LogoutFilter
  • OAuth2AuthorizationRequestRedirectFilter
  • Saml2WebSsoAuthenticationRequestFilter
  • X509AuthenticationFilter
  • AbstractPreAuthenticatedProcessingFilter
  • CasAuthenticationFilter
  • OAuth2LoginAuthenticationFilter
  • Saml2WebSsoAuthenticationFilter
  • UsernamePasswordAuthenticationFilter
  • ConcurrentSessionFilter
  • OpenIDAuthenticationFilter
  • DefaultLoginPageGeneratingFilter
  • DefaultLogoutPageGeneratingFilter
  • DigestAuthenticationFilter
  • BearerTokenAuthenticationFilter
  • BasicAuthenticationFilter
  • RequestCacheAwareFilter
  • SecurityContextHolderAwareRequestFilter
  • JaasApiIntegrationFilter
  • RememberMeAuthenticationFilter
  • AnonymousAuthenticationFilter
  • OAuth2AuthorizationCodeGrantFilter
  • SessionManagementFilter
  • ExceptionTranslationFilter
  • FilterSecurityInterceptor
  • SwitchUserFilter

具体会用到哪些Filter,需要结合业务分析。本文只分析其中最关键的几个Filter(已加粗显示)。

例如在表单登录例子里,用到的filter如下图:

chain-of-form-log in.png

ExceptionTranslationFilter

ExceptionTranslationFilter是非常重要的过滤器之一,它主要功能是负责异常拦截,而且核心是对AccessDeniedException 和 AuthenticationException 捕获处理。

比如,所有第一次访问 xxxx.com/index.html 的未登录用户,都应该被重定向到登录页 xxxx.com/login.html 进行鉴定。

这个功能点实现就需要依赖ExceptionTranslationFilter对AuthenticationException 的处理。我们来看源码:

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
java复制代码private void handleSpringSecurityException(HttpServletRequest request,
HttpServletResponse response, FilterChain chain, RuntimeException exception)
throws IOException, ServletException {
if (exception instanceof AuthenticationException) {
// 处理AuthenticationException(未鉴定异常)
logger.debug(
"Authentication exception occurred; redirecting to authentication entry point",
exception);
// 跳转到鉴定逻辑(一般是重定向到鉴定页面)
sendStartAuthentication(request, response, chain,
(AuthenticationException) exception);
}
else if (exception instanceof AccessDeniedException) {
// 处理AccessDeniedException(访问拒绝异常)
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (authenticationTrustResolver.isAnonymous(authentication) || authenticationTrustResolver.isRememberMe(authentication)) {
logger.debug(
"Access is denied (user is " + (authenticationTrustResolver.isAnonymous(authentication) ? "anonymous" : "not fully authenticated") + "); redirecting to authentication entry point",
exception);
// 如果是默认的匿名用户,则尝试跳转到鉴定逻辑(一般是重定向到鉴定页面)
sendStartAuthentication(
request,
response,
chain,
new InsufficientAuthenticationException(
messages.getMessage(
"ExceptionTranslationFilter.insufficientAuthentication",
"Full authentication is required to access this resource")));
}
else {
logger.debug(
"Access is denied (user is not anonymous); delegating to AccessDeniedHandler",
exception);
// 如果非默认的匿名用户(也就是已经登录过的用户),则跳转到访问拒绝逻辑,
// 因为该用户的确没有权限访问该资源。
accessDeniedHandler.handle(request, response,
(AccessDeniedException) exception);
}
}
}

这段代码的逻辑非常清晰,注释已经清楚这里不再赘言。而sendStartAuthentication方法再展开看看是这样的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
java复制代码protected void sendStartAuthentication(HttpServletRequest request,
HttpServletResponse response, FilterChain chain,
AuthenticationException reason) throws ServletException, IOException {
// SEC-112: Clear the SecurityContextHolder's Authentication, as the
// existing Authentication is no longer considered valid
// 清空已有的鉴定信息,这是一个防御性编程
SecurityContextHolder.getContext().setAuthentication(null);
// 缓存这次请求的上下文,因为准备进入鉴定逻辑,可能要跳转页面,
// 而等鉴定完了还要继续处理这请求
requestCache.saveRequest(request, response);
// 调用鉴定逻辑
logger.debug("Calling Authentication entry point.");
authenticationEntryPoint.commence(request, response, reason);
}

用一张图总结以上流程:

exceptiontranslationfilter.png

因此,用户可以自定义自己的异常跳转页面,例如:

1
2
3
4
5
java复制代码// 自定义异常跳转页面
http.exceptionHandling()
.authenticationEntryPoint(new LoginUrlAuthenticationEntryPoint("/login"))
.and()
.exceptionHandling().accessDeniedHandler(new AccessDeniedHandlerImpl());

UsernamePasswordAuthenticationFilter

UsernamePasswordAuthenticationFilter 是专门用来处理登录请求的过滤器。默认情况下,它只处理POST /login请求:

1
2
3
java复制代码public UsernamePasswordAuthenticationFilter() {
super(new AntPathRequestMatcher("/login", "POST"));
}

请求过滤是在父类实现的:

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
java复制代码public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
throws IOException, ServletException {

HttpServletRequest request = (HttpServletRequest) req;
HttpServletResponse response = (HttpServletResponse) res;
// 过滤请求,默认只处理 POST /login 请求
if (!requiresAuthentication(request, response)) {
chain.doFilter(request, response);

return;
}

Authentication authResult;

try {
// 身份鉴定逻辑
authResult = attemptAuthentication(request, response);
if (authResult == null) {
// return immediately as subclass has indicated that it hasn't completed
// authentication
return;
}
sessionStrategy.onAuthentication(authResult, request, response);
}
catch (InternalAuthenticationServiceException failed) {
// 身份鉴定失败逻辑1
logger.error(
"An internal error occurred while trying to authenticate the user.",
failed);
unsuccessfulAuthentication(request, response, failed);

return;
}
catch (AuthenticationException failed) {
// 身份鉴定失败逻辑2
unsuccessfulAuthentication(request, response, failed);

return;
}

// 以下为身份鉴定成功后逻辑
if (continueChainBeforeSuccessfulAuthentication) {
chain.doFilter(request, response);
}

successfulAuthentication(request, response, chain, authResult);
}

protected boolean requiresAuthentication(HttpServletRequest request,
HttpServletResponse response) {
// 如果是登录请求,则返回true。默认是POST /login
return requiresAuthenticationRequestMatcher.matches(request);
}

UsernamePasswordAuthenticationFilter 实现了父类的身份鉴定逻辑抽象方法:

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复制代码public Authentication attemptAuthentication(HttpServletRequest request,
HttpServletResponse response) throws AuthenticationException {
if (postOnly && !request.getMethod().equals("POST")) {
throw new AuthenticationServiceException(
"Authentication method not supported: " + request.getMethod());
}
// 从请求中取出username和password
String username = obtainUsername(request);
String password = obtainPassword(request);

if (username == null) {
username = "";
}

if (password == null) {
password = "";
}

username = username.trim();
// 构建一个token实例
UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(
username, password);

// Allow subclasses to set the "details" property
setDetails(request, authRequest);
// 交给AuthenticationManager进行身份鉴定
return this.getAuthenticationManager().authenticate(authRequest);
}

可见,UsernamePasswordAuthenticationFilter只是个框架,真正的鉴定逻辑都是在AuthenticationManager中。

再回到父类看看鉴定成功后的处理逻辑,因为这逻辑为后文埋了伏笔:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
java复制代码protected void successfulAuthentication(HttpServletRequest request,
HttpServletResponse response, FilterChain chain, Authentication authResult)
throws IOException, ServletException {
// 鉴定成功保存结果到线程上下文空间内
SecurityContextHolder.getContext().setAuthentication(authResult);
// 调用"记住我"服务保存用户鉴定信息
rememberMeServices.loginSuccess(request, response, authResult);

// Fire event
if (this.eventPublisher != null) {
eventPublisher.publishEvent(new InteractiveAuthenticationSuccessEvent(
authResult, this.getClass()));
}
// 鉴定成功处理逻辑
successHandler.onAuthenticationSuccess(request, response, authResult);
}

注意这里在上下文保存了鉴定结果。

RememberMeAuthenticationFilter

上边我们着重强调上下文空间会保存用户的鉴定信息,但是当用户关闭浏览器之后一段时间,session会自动超时销毁。那么上下文空间保存的鉴定信息自然丢失,此时用户就需要重新登录鉴定。

所以,常见一些安全风险不大的网页,会提供一个”记住我”的按钮。只要用户在一定时间内(比如24h)重新打开该网站,都为用户自动登录。RememberMeAuthenticationFilter的存在正是为了实现这个功能。

既然RememberMeAuthenticationFilter是个Filter,那么按照惯例,我们直接来看其doFilter方法:

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
java复制代码public class RememberMeAuthenticationFilter extends GenericFilterBean implements
ApplicationEventPublisherAware {
private AuthenticationManager authenticationManager;
private RememberMeServices rememberMeServices;

public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) req;
HttpServletResponse response = (HttpServletResponse) res;

if (SecurityContextHolder.getContext().getAuthentication() == null) {
// 如果请求里没有身份鉴定信息,则尝试通过"记住我"自动登录
Authentication rememberMeAuth = rememberMeServices.autoLogin(request,
response);

if (rememberMeAuth != null) {
// "记住我"里边获取到了用户鉴定信息
try {
// 通过authenticationManager确定鉴定信息是否有效
rememberMeAuth = authenticationManager.authenticate(rememberMeAuth);

// 鉴定信息存进上下文空间
SecurityContextHolder.getContext().setAuthentication(rememberMeAuth);

onSuccessfulAuthentication(request, response, rememberMeAuth);
// ...

if (successHandler != null) {
successHandler.onAuthenticationSuccess(request, response,
rememberMeAuth);

return;
}

}
catch (AuthenticationException authenticationException) {
// 鉴定异常,则清除"记住我"里的token信息
rememberMeServices.loginFail(request, response);

onUnsuccessfulAuthentication(request, response,
authenticationException);
}
}

chain.doFilter(request, response);
}
else {
chain.doFilter(request, response);
}
}
}

从注释可以看出,doFilter里也是非常骨架的代码。核心的逻辑需要看看AuthenticationManager和RemeberMeService,这部分内容放在后文。请继续往下。

鉴定

SecurityContextHolder && SecurityContext

SecurityContextHolder是Spring Security的核心模型,用来保存通过鉴定的用户信息。

securitycontextholder.png

默认情况下,用户信息保存在线程空间内(ThreadLocal)。可以通过以下方法保存和取出用户信息:

1
2
3
4
java复制代码// 取出鉴定信息
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
// 保存鉴定信息
SecurityContextHolder.getContext().setAuthentication(authenticaiton);

Authentication

Authentication内部包含是三部分:

  • principal : 保存用户信息,当通过用户和密码进行鉴定的时候,通常是一个UserDetails实例;
  • credentials :保存密码,通常当用户通过鉴定后,该字段会被清空以免密码泄露;
  • authorities :保存用户权限,通常是角色或者范围等较高抽象等级的权限,GrantedAuthority的实现类型;

Authentication有两个应用情景:

  • 表示还没被鉴定的用户的信息。它作为AuthenticationManager的入参,这时候isAuthenticated()方法返回false;
  • 表示已经被鉴定的用户信息。可以通过SecurityContext获取到当前用户的鉴定信息。

GrantedAuthority

GrantedAuthority是抽象程度较高的权限,一般是角色或者范围等级别。比方说可以是 ROLE_ADMINISTRATOR 或者 ROLE_HR_SUPERVISOR。这种级别的权限后续可以应用到URL授权或者方法访问授权上。但不建议用于更细粒度的权限控制。

AuthenticationManager && ProviderManager

AuthenticationManager是Spring Security的身份鉴定入口。通常的流程是Spring Security的Filter调用AuthenticationManager的API进行身份鉴定后获得一个用户鉴定信息(Authentication),然后将该 Authentication保存在线程空间(SecurityContextHolder)中。

而ProviderManager是最常用的一个AuthenticationManager实现。它将委托若干AuthenticationProvider进行身份鉴定。每一个AuthenticationProvider都有机会对该用户信息鉴定,如果鉴定不通过则交给下一个Provider。如果所有的Provider都不支持该Authentication的鉴定,则会抛出一个ProviderNotFoundException。

providermanager.png

实际中每一个AuthenticationProvider只处理特定类型的authentication。例如,UsernamePasswordAuthenticationToken只能够鉴定用户密码类型的信息:

1
2
3
4
java复制代码public boolean supports(Class<?> authentication) {
return (UsernamePasswordAuthenticationToken.class
.isAssignableFrom(authentication));
}

这样子的设计使得我们可以通过定制不同的AuthenticationProvider来支持多种鉴定方式。

ProviderManager还支持指定一个可选的父AuthenticationManager,以应对所有的Provider都不支持该Authentication的鉴定的情景。父AuthenticationManager通常也是一个ProviderManager。

providermanager-parent.png

代码中也清晰体现了以上逻辑:

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
java复制代码public Authentication authenticate(Authentication authentication)
throws AuthenticationException {
Class<? extends Authentication> toTest = authentication.getClass();
// ...
// 遍历所有的provider
for (AuthenticationProvider provider : getProviders()) {
if (!provider.supports(toTest)) {
// 如果该provider不支持该类型鉴定信息,则跳过
continue;
}

try {
result = provider.authenticate(authentication);

if (result != null) {
// 鉴定通过则终止循环
copyDetails(authentication, result);
break;
}
}
catch (AccountStatusException | InternalAuthenticationServiceException e) {
prepareException(e, authentication);
// SEC-546: Avoid polling additional providers if auth failure is due to
// invalid account status
throw e;
} catch (AuthenticationException e) {
lastException = e;
}
}

if (result == null && parent != null) {
// result为null,意味着所有的provider都不支持鉴定,需要使用parent来鉴定,作为兜底。
try {
result = parentResult = parent.authenticate(authentication);
}
catch (ProviderNotFoundException e) {
// ignore as we will throw below if no other exception occurred prior to
// calling parent and the parent
// may throw ProviderNotFound even though a provider in the child already
// handled the request
}
catch (AuthenticationException e) {
lastException = parentException = e;
}
}

if (result != null) {
if (eraseCredentialsAfterAuthentication
&& (result instanceof CredentialsContainer)) {
// 鉴定成功,尝试擦除用户密码信息
((CredentialsContainer) result).eraseCredentials();
}

// If the parent AuthenticationManager was attempted and successful than it will publish an AuthenticationSuccessEvent
// This check prevents a duplicate AuthenticationSuccessEvent if the parent AuthenticationManager already published it
if (parentResult == null) {
eventPublisher.publishAuthenticationSuccess(result);
}
return result;
}

// Parent was null, or didn't authenticate (or throw an exception).

if (lastException == null) {
lastException = new ProviderNotFoundException(messages.getMessage(
"ProviderManager.providerNotFound",
new Object[] { toTest.getName() },
"No AuthenticationProvider found for {0}"));
}

// If the parent AuthenticationManager was attempted and failed than it will publish an AbstractAuthenticationFailureEvent
// This check prevents a duplicate AbstractAuthenticationFailureEvent if the parent AuthenticationManager already published it
if (parentException == null) {
prepareException(lastException, authentication);
}

throw lastException;
}

另外多个ProviderManager共享同一个父AuthenticationManager也是可行的,常常用于多个 SecurityFilterChain 对象 拥有部分公共的鉴定逻辑的情景。

providermanagers-parent.png

AuthenticationProvider

如上所言,多个AuthenticationProvider会被注入到ProviderManager。但是单个AuthenticationProvider只能处理特定类型的鉴定信息。比如,DaoAuthenticationProvider支持包含用户名密码的鉴定信息,而JwtAuthenticationProvider 支持包含JWT token的鉴定信息。

AuthenticationEntryPoint

上文我们已经多次看见AuthenticationEntryPoint,一般它的作用是当发现来自未鉴定用户请求的时候进行重定向到登录页。

AbstractAuthenticationProcessingFilter

前文在介绍UsernamePasswordAuthenticationFilter的时候,其实已经介绍过AbstractAuthenticationProcessingFilter。它其实是用来鉴定用户身份信息的一个抽象Filter。用户可以继承该抽象基类,实现自定义的身份鉴定Filter,比如:UsernamePasswordAuthenticationFilter。通过分析该抽象基类源码,可以得到以下执行流程图:

abstractauthenticationprocessingfilter.png

总结流程如下:

  • 1.当用户提交身份鉴定信息,AbstractAuthenticationProcessingFilter会从HttpServletRequest提供的信息构造一个Authentication。这个步骤是在 attemptAuthentication 抽象方法中完成的,也就是由AbstractAuthenticationProcessingFilter的实现类来实现。例如,UsernamePasswordAuthenticationFilter 构造了一个包含用户名和密码的UsernamePasswordAuthenticationToken作为身份鉴定信息;
  • 2.接下来这个Authentication会被传递到AuthenticationManager进行身份鉴定;
  • 3.如果鉴定失败:
    • 清空 SecurityContextHolder;
    • 调用RememberMeServices.loginFail;如果没有注册RememberMeServices,则忽略;
    • 调用AuthenticationFailureHandler;
  • 4.如果鉴定成功:
    • 通知SessionAuthenticationStrategy有一个新的登录请求;
    • 保存Authentication到SecurityContextHolder。稍后SecurityContextPersistenceFilter会保存SecurityContext到HttpSession中;
    • 调用RememberMeServices.loginSuccess;如果没有注册RememberMeServices,则忽略;
    • ApplicationEventPublisher发布一个InteractiveAuthenticationSuccessEvent事件。

从代码里也能一看端倪:

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
java复制代码public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) req;
HttpServletResponse response = (HttpServletResponse) res;

if (!requiresAuthentication(request, response)) {
chain.doFilter(request, response);

return;
}

Authentication authResult;

try {
authResult = attemptAuthentication(request, response);
if (authResult == null) {
// return immediately as subclass has indicated that it hasn't completed
// authentication
return;
}
sessionStrategy.onAuthentication(authResult, request, response);
}
catch (InternalAuthenticationServiceException failed) {
logger.error(
"An internal error occurred while trying to authenticate the user.",
failed);
unsuccessfulAuthentication(request, response, failed);

return;
}
catch (AuthenticationException failed) {
// Authentication failed
unsuccessfulAuthentication(request, response, failed);

return;
}

// Authentication success
if (continueChainBeforeSuccessfulAuthentication) {
chain.doFilter(request, response);
}

successfulAuthentication(request, response, chain, authResult);
}

UserDetails

UserDetails与Authentication的内部结构非常相似,读者需要注意区分。上文提到Authentication的两处应用场景,而UserDetails则只有唯一的应用场景,它是由 UserDetailsService返回的数据结构,携带着用户信息,包括用户名、密码(一般是加密后)、权限角色等。

UserDetailsService

先来看看接口定义:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
java复制代码/**
* Core interface which loads user-specific data.
* <p>
* It is used throughout the framework as a user DAO and is the strategy used by the
* {@link org.springframework.security.authentication.dao.DaoAuthenticationProvider
* DaoAuthenticationProvider}.
*
* <p>
* The interface requires only one read-only method, which simplifies support for new
* data-access strategies.
*
* @see org.springframework.security.authentication.dao.DaoAuthenticationProvider
* @see UserDetails
*
* @author Ben Alex
*/
public interface UserDetailsService {
// ~ Methods
// ========================================================================================================

/**
* Locates the user based on the username. In the actual implementation, the search
* may possibly be case sensitive, or case insensitive depending on how the
* implementation instance is configured. In this case, the <code>UserDetails</code>
* object that comes back may have a username that is of a different case than what
* was actually requested..
*
* @param username the username identifying the user whose data is required.
*
* @return a fully populated user record (never <code>null</code>)
*
* @throws UsernameNotFoundException if the user could not be found or the user has no
* GrantedAuthority
*/
UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
}

从注释可以看出,这个接口主要是提供给DaoAuthenticationProvider加载用户信息的,返回数据结构正是上文的UserDetails。当然这个接口还应用在了AbstractRememberMeServices等地方。

用户可以自定义自己UserDetailsService:

1
2
3
4
java复制代码@Bean
CustomUserDetailsService customUserDetailsService() {
return new CustomUserDetailsService();
}

PasswordEncoder

PasswordEncoder是用来对密码进行加解密的接口。Spring Security提供了非常多实现,可以看看 PasswordEncoderFactories类。

特别注意的是,DelegatingPasswordEncoder还支持根据密文前缀来动态选择加解密算法。

DaoAuthenticationProvider

如果用户没有指定AuthenticationProvider的话,Spring Security默认会以DaoAuthenticationProvider作为兜底。

DaoAuthenticationProvider使用UserDetailsService以及UserDetailsService来鉴定用户的身份信息。流程图大概如下:

daoauthenticationprovider.png

  • 1.Authentication Filter (比如UsernamePasswordAuthenticationFilter)构造一个
    包含用户名和密码的UsernamePasswordAuthenticationToken,并传递给AuthenticationManager(通常是ProviderManager实现);
  • 2.ProviderManager会使用AuthenticationProvider列表去鉴定用户身份,列表里就可以有DaoAuthenticationProvider;
  • 3.DaoAuthenticationProvider 通过 UserDetailsService 加载用户信息 UserDetails;
  • 4.DaoAuthenticationProvider使用 PasswordEncoder 验证传入的用户身份信息和UserDetails是否吻合;
  • 5.当身份鉴定成功,会返回一个包含UserDetails和Authorities的UsernamePasswordAuthenticationToken实例。最后这个token会在Authentication Filter里被保存到SecurityContextHolder中。

Basic Authentication

Spring Securtiy原生支持Basic HTTP Authentication。
这种认证方式不常用,而且分析思路和上文一致,因此,可以参考下官方文档10.10.2. Basic Authentication

Digest Authentication

同上,不常用而且分析思路一致,详见官方文档 10.10.3. Digest Authentication

LDAP Authentication

LDAP的接入例子我们在上一篇《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
java复制代码@Profile("ldap")
@Component
public class SecurityLdapConfiguration extends GeneralSecurityConfiguration {

@Bean
public LdapContextSource ldapContextSource(LdapProperties ldapProperties) {
LdapContextSource contextSource = new LdapContextSource();
contextSource.setUrl(ldapProperties.getUrl());
contextSource.setUserDn(ldapProperties.getUserDn());
contextSource.setPassword(ldapProperties.getPassword());
contextSource.setPooled(false);
contextSource.afterPropertiesSet();
return contextSource;
}

@Bean
public BindAuthenticator authenticator(BaseLdapPathContextSource contextSource, LdapProperties ldapProperties) {
String searchBase = ldapProperties.getSearchBase();
String filter = ldapProperties.getSearchFilter();
FilterBasedLdapUserSearch search =
new FilterBasedLdapUserSearch(searchBase, filter, contextSource);
BindAuthenticator authenticator = new BindAuthenticator(contextSource);
authenticator.setUserSearch(search);
return authenticator;
}

@Bean
public LdapAuthenticationProvider authenticationProvider(LdapAuthenticator authenticator) {
return new LdapAuthenticationProvider(authenticator);
}
}

与之前不一样的是,这里LDAP需要自定义一个 LdapAuthenticationProvider ,用来取代兜底的DaoAuthenticationProvider。

这给我们一个启发,可以通过自定义AuthenticationProvider,实现更加丰富多样的身份鉴定逻辑。

RememberServices

RememberServices用来实现”记住我”功能,原理是从用户请求的Cookie中解析token信息,然后和本地缓存的token(或者根据用户密码生成的token)作比较,比较成功则为用户自动登录。

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
java复制代码@Override
public final Authentication autoLogin(HttpServletRequest request,
HttpServletResponse response) {
String rememberMeCookie = extractRememberMeCookie(request);

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

if (rememberMeCookie.length() == 0) {
logger.debug("Cookie was empty");
cancelCookie(request, response);
return null;
}

UserDetails user = null;

try {
// 从cookie信息解析出token信息
String[] cookieTokens = decodeCookie(rememberMeCookie);
// 根据cookieTokens执行自动登录
user = processAutoLoginCookie(cookieTokens, request, response);
userDetailsChecker.check(user);

logger.debug("Remember-me cookie accepted");

return createSuccessfulAuthentication(request, user);
}
catch (CookieTheftException cte) {
cancelCookie(request, response);
throw cte;
}
catch (UsernameNotFoundException noUser) {
logger.debug("Remember-me login was valid but corresponding user not found.",
noUser);
}
catch (InvalidCookieException invalidCookie) {
logger.debug("Invalid remember-me cookie: " + invalidCookie.getMessage());
}
catch (AccountStatusException statusInvalid) {
logger.debug("Invalid UserDetails: " + statusInvalid.getMessage());
}
catch (RememberMeAuthenticationException e) {
logger.debug(e.getMessage());
}

cancelCookie(request, response);
return null;
}

Spring Security提供了两个常用实现:PersistentTokenBasedRememberMeServices 和 TokenBasedRememberMeServices。前者支持将cookieToken持久化,而后者则是从UserDetailsService获取用户信息并生成cookieToken。

总结

本文详细介绍了Spring Security实现原理。

首先,谈到Spring Security对Sevlet Filter的应用,并在Filter的基础上拓展出DelegatingFilterProxy、FilterChainProxy、SecurityFilterChain以及Security Filters。我们在实际生产中遇到这种责任链模式的时候,也可以参考这个丰富且灵活的案例。

然后,我们又深入了解了Spring Security的鉴定模块原理,掌握了各个内部组件的实现细节。

通过深入学习原理,我们可以大大提高对Spring Security的掌握程度,真正做到融会贯通、得心应手。

参考资料

  • 官方文档9-11章节:非常清晰的文档;
  • 杜瓦尔的博客:我的个人博客地址,可以去看看更多内容;

本文转载自: 掘金

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

1…758759760…956

开发者博客

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