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

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


  • 首页

  • 归档

  • 搜索

SpringBoot基础系列之AOP结合SpEL实现日志输出

发表于 2021-06-12

【SpringBoot 基础系列】AOP结合SpEL实现日志输出的注意事项一二

使用 AOP 来打印日志大家一把都很熟悉了,最近在使用的过程中,发现了几个有意思的问题,一个是 SpEL 的解析,一个是参数的 JSON 格式输出

I. 项目环境

1. 项目依赖

本项目借助SpringBoot 2.2.1.RELEASE + maven 3.5.3 + IDEA进行开发

开一个 web 服务用于测试

1
2
3
4
5
6
xml复制代码<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
</dependencies>

II. AOP & SpEL

关于 AOP 与 SpEL 的知识点,之前都有过专门的介绍,这里做一个聚合,一个非常简单的日志输出切面,在需要打印日志的方法上,添加注解@Log,这个注解中定义一个key,作为日志输出的标记;key 支持 SpEL 表达式

1. AOP 切面

注解定义

1
2
3
4
5
java复制代码@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Log {
String key();
}

切面逻辑

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复制代码@Slf4j
@Aspect
@Component
public class AopAspect implements ApplicationContextAware {
private ExpressionParser parser = new SpelExpressionParser();
private ParameterNameDiscoverer parameterNameDiscoverer = new DefaultParameterNameDiscoverer();

@Around("@annotation(logAno)")
public Object around(ProceedingJoinPoint joinPoint, Log logAno) throws Throwable {
long start = System.currentTimeMillis();
String key = loadKey(logAno.key(), joinPoint);
try {
return joinPoint.proceed();
} finally {
log.info("key: {}, args: {}, cost: {}", key,
JSONObject.toJSONString(joinPoint.getArgs()),
System.currentTimeMillis() - start);
}
}

private String loadKey(String key, ProceedingJoinPoint joinPoint) {
if (key == null) {
return key;
}

StandardEvaluationContext context = new StandardEvaluationContext();

context.setBeanResolver(new BeanFactoryResolver(applicationContext));
String[] params = parameterNameDiscoverer.getParameterNames(((MethodSignature) joinPoint.getSignature()).getMethod());
Object[] args = joinPoint.getArgs();
for (int i = 0; i < args.length; i++) {
context.setVariable(params[i], args[i]);
}

return parser.parseExpression(key).getValue(context, String.class);
}

private ApplicationContext applicationContext;

@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
this.applicationContext = applicationContext;
}
}

上面这个逻辑比较简单,和大家熟知的使用姿势没有太大的区别

2. StandardEvaluationContext 安全问题

关于StandardEvaluationContext的注入问题,有兴趣的可以查询一下相关文章;对于安全校验较高的,要求只能使用SimpleEvaluationContext,使用它的话,SpEL 的能力就被限制了

如加一个测试

1
2
3
4
5
6
7
8
java复制代码@Data
@Accessors(chain = true)
public class DemoDo {

private String name;

private Integer age;
}

服务类

1
2
3
4
5
6
7
8
java复制代码@Service
public class HelloService {

@Log(key = "#demo.getName()")
public String say(DemoDo demo, String prefix) {
return prefix + ":" + demo;
}
}

为了验证SimpleEvaluationContext,我们修改一下上面的loadKeys方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
java复制代码private String loadKey(String key, ProceedingJoinPoint joinPoint) {
if (key == null) {
return key;
}

SimpleEvaluationContext context = new SimpleEvaluationContext.Builder().build();
String[] params = parameterNameDiscoverer.getParameterNames(((MethodSignature) joinPoint.getSignature()).getMethod());
Object[] args = joinPoint.getArgs();
for (int i = 0; i < args.length; i++) {
context.setVariable(params[i], args[i]);
}

return parser.parseExpression(key).getValue(context, String.class);
}

启动测试

1
2
3
4
5
6
7
8
9
10
11
java复制代码@SpringBootApplication
public class Application {

public Application(HelloService helloService) {
helloService.say(new DemoDo().setName("一灰灰blog").setAge(18), "welcome");
}

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

直接提示方法找不到!!!

3. gson 序列化问题

上面的 case 中,使用的 FastJson 对传参进行序列化,接下来我们采用 Gson 来做序列化

1
2
3
4
xml复制代码<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
</dependency>

然后新增一个特殊的方法

1
2
3
4
5
6
7
8
9
10
11
12
java复制代码@Service
public class HelloService {
/**
* 字面量,注意用单引号包裹起来
* @param key
* @return
*/
@Log(key = "'yihuihuiblog'")
public String hello(String key, HelloService helloService) {
return key + "_" + helloService.say(new DemoDo().setName(key).setAge(10), "prefix");
}
}

注意上面方法的第二个参数,非常有意思的是,传参是自己的实例;再次执行

1
2
3
4
5
6
java复制代码public Application(HelloService helloService) {
helloService.say(new DemoDo().setName("一灰灰blog").setAge(18), "welcome");

String ans = helloService.hello("一灰灰", helloService);
System.out.println(ans);
}

直接抛了异常

这就很尴尬了,一个输出日志的辅助工具,因为序列化直接导致接口不可用,这就不优雅了;而我们作为日志输出的切面,又是没有办法控制这个传参的,没办法要求使用的参数,一定能序列化,这里需要额外注意 (比较好的方式就是简单对象都实现 toString,然后输出 toString 的结果;而不是 json 串)

4. 小结

虽然上面一大串的内容,总结下来,也就两点

  • SpEL 若采用的是SimpleEvaluationContext,那么注意 spel 的功能是减弱的,一些特性不支持
  • 若将方法参数 json 序列化输出,那么需要注意某些类在序列化的过程中,可能会抛异常

(看到这里的小伙伴,不妨点个赞,顺手关注下微信公众号”一灰灰 blog“,我的公众号已经寂寞的长草了 😭)

III. 不能错过的源码和相关知识点

0. 项目

  • 工程:github.com/liuyueyi/sp…
  • 源码: github.com/liuyueyi/sp…

AOP 系列博文

  • SpringBoot 基础系列 AOP 无法拦截接口上注解场景兼容
  • SpringBoot 基础系列实现一个简单的分布式定时任务(应用篇)
  • SpringBoot 基础篇 AOP 之拦截优先级详解
  • SpringBoot 应用篇之 AOP 实现日志功能
  • SpringBoot 基础篇 AOP 之高级使用技能
  • SpringBoot 基础篇 AOP 之基本使用姿势小结

1. 一灰灰 Blog

尽信书则不如,以上内容,纯属一家之言,因个人能力有限,难免有疏漏和错误之处,如发现 bug 或者有更好的建议,欢迎批评指正,不吝感激

下面一灰灰的个人博客,记录所有学习和工作中的博文,欢迎大家前去逛逛

  • 一灰灰 Blog 个人博客 blog.hhui.top
  • 一灰灰 Blog-Spring 专题博客 spring.hhui.top

一灰灰blog

本文转载自: 掘金

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

规则引擎调研与思考(一)

发表于 2021-06-12

1. 规则引擎简述

世界万事万物皆有规则

说起规则引擎, 相信很多小伙伴对于规则引擎产生了很多疑问. 它是什么? 它能做啥? 应该怎么做? 希望通过阅读下面的内容能给你一些启发.

首先规则引擎是什么,我们来看下百度百科是怎么定义的

规则引擎由推理引擎发展而来,是一种嵌入在应用程序中的组件,实现了将业务决策从应用程序代码中分离出来,并使用预定义的语义模块编写业务决策。接受数据输入,解释业务规则,并根据业务规则做出业务决策。

上面说的很清晰, 总结一句话规则引擎做的事情就是 录入特定判断逻辑, 通过输入的数据进行决策

规则引擎这么好? 我们的业务适合引入规则引擎吗?

首先我们有个基本的优缺点分析:

规则引擎带来的优点:

  1. 高灵活性

高灵活性带来的直接效果是缩短开发到上线周期, 热更新修复bug
2. 天生组件化

简化复杂的业务逻辑, 缩减业务代码量, 易于业务逻辑管理.

规则引擎带来的缺点:

  1. 引入了额外服务依赖

对于一些对稳定性、正确性要求极高的场景, 前期不建议引入 (需要提供完善的权限控制和规则单元测试能力)
2. 前期增加产品、技术学习成本

产品需要具有一定抽象思维, 需求文档中给出系统易变部分进行抽象处理

研发需要学习部分规则语法, 并了解系统实现和约束
3. 并不能依赖规则热更新满足所有业务判定场景

所以规则引擎并不是万能, 在熟悉规则引擎的具体能力前提下, 根据具体所在的业务场景, 来判断引入后是否可达到效益最高

2. 规则引擎选择

规则引擎/指标 drools gengine
上手难度 有一定门槛 易
运行方式 仅支持顺序型 支持顺序/并行/N-M等模式
开发语言 java golang
社区活跃度 高 一般

基于本人水平, 此处选择了更易学习的 gengine 作为研究对象 (虽然规则引擎有不同的运行模式和语言, 但对于我们理解本质并没什么区别)

  • gengine是一款基于golang和AST(抽象语法树)开发的规则引擎, gengine支持的语法是一种自定义的DSL
  • gengine于2020年7月由哔哩哔哩(bilibili.com)授权开源
  • gengine现已应用于B站风控系统、流量投放系统、AB测试、推荐平台系统等多个业务场景
  • 你也可以将gengine应用于golang应用的任何需要规则或指标支持的业务场景

gengine 规则引擎使用流程

支持部分规则模式

运行模式 方法名 含义
顺序型 ExecuteSelectedRulesWithControlAndStopTag 按规则优先级执行(从上往下)-耗时为所有规则执行时间累加
并发型 ExecuteSelectedRulesConcurrent 所有规则并发执行(执行时间为执行时间最长的-考虑池限制)
混合模式 ExecuteSelectedRulesMixModel 先执行一个优先级最高的规则,然后并发执行剩下的所有规则
N-M ExecuteNConcurrentMConcurrent 前N个规则并发执行, M个规则也并发执行/ 前N个规则顺序执行, M个规则并发执行
  1. 制定规则中依赖的方法(硬编码)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
golang复制代码type ruleResponse struct {
Code int
Data map[string]interface{}
}

func success(code int) ruleResponse {
return ruleResponse{
Code: code,
Data: make(map[string]interface{}),
}
}

func (rule ruleResponse) AddDataParam(name string, value interface{}) {
rule.Data[name] = value
}

规则中是不支持自定义函数和结构体的, 当需要返回非int、字符串、bool值的时候, 需要我们外部注入对应实现方法, 供规则内调用 (例如自定义结构体、自定义复杂规则验证、获取订单课程等方法)
2. 声明规则

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
golang复制代码rule1 := `

rule "case0" "case1测试用例" salience 0 //后执行
begin
Print(flag-2)
successObj := success(0)
successObj.AddDataParam("name0","test 000 value")
return successObj
end

rule "case1" "case1测试用例" salience 1 //后执行
begin
a = 8
if a < 1 {
println("a < 1")
} else if a >= 1 && a <6 {
println("a >= 1 && a <6")
} else if a >= 6 && a < 7 {
println("a >= 6 && a < 7")
} else if a >= 7 && a < 10 {
println("a >=7 && a < 10")
} else {
println("a > 10")
}
end

rule "case2" "case2测试用例" salience 2 //先执行
begin

if flag>0 {
Print(flag+3)
stag.StopTag = true
successObj := success(222)
successObj.AddDataParam("name2","test value")
return successObj
}

end
`

上图中声明了规则, Print、success是我们外部注入的方法, 我们将在下一步制定.

rule表示一段新的规则开始 , 第一个为规则名(返回结果时候为对应key), 第二个为描述, 第三个 salience 表示规则的优先级

规则的优先级数字越大优先级越高(当使用 AsGivenSortedName 方法时, 会忽略掉规则内优先级作用 )
3. 注入规则内依赖方法, 初始化规则池

1
2
3
4
5
6
7
8
9
10
11
12
golang复制代码apis := make(map[string]interface{})
apis["success"] = success
apis["Print"] = fmt.Println

var poolMinLen int64 = 50
var poolMaxLen int64 = 100
pool, err := engine.NewGenginePool(poolMinLen, poolMaxLen, engine.SortModel, rule1, apis)

if err != nil {
fmt.Println(err.Error())
return
}

第一个参数 poolMinLen 表示初始化池最小50

第二个参数 poolMaxLen 表示初始化池最大100

第三个参数为设置执行模式(分为顺序执行、并发执行等), 只有调用 ExecuteSelectedRules 和 ExecuteSelectedRulesConcurrent 有效 (后续可做规则模式选择)

第四个参数是我们配置的规则

第五个是我们注入的变量值、方法等
4. 调用执行规则

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
golang复制代码data := make(map[string]interface{})
data["flag"] = 2

stag := &engine.Stag{StopTag: false}
data["stag"] = stag //显示将标记注入进去

//_ = pool.RemoveRules([]string{"case1"}) //移除规则
/*_ = pool.UpdatePooledRulesIncremental(` //新增规则
rule "case66" "case66测试用例" salience 3
begin
Print("case66")
return false
end
`)*/

_, resp := pool.ExecuteSelectedRulesWithControlAndStopTag(data, true, stag, []string{"case1", "case66", "case2", "case0"})
fmt.Println(resp)
//output:
//5
//map[case2:{222 map[name2:test value]}]

业务调用方变量注入, StopTag 声明为可中断, 执行选定的规则

3. 业务系统应用

层级架构图

image.png

业务场景通过业务编号(标识)调用规则接入层, 根据运行模式运行关联的规则.

接入层负责根据业务编号拿到具体规则进行运行, 同时接入层负责收集业务执行的异常与结果

最底层规则信息管理部分, 负责规则信息数据的维护与整理

业务接入模块分工流程

image.png

一、业务请求侧流程步骤:

  1. 业务方请求 (参数为 业务编号+业务数据) 执行规则
  2. 规则执行获取业务编号对应的规则列表, 根据此业务的运行模式(顺序型、并行等) 执行规则
  3. 执行完成后返回具体执行结果供业务使用

二、规则管理侧能力:

  1. 规则录入、修改、删除
  2. 规则合并创建业务规则包, 生成业务编号供业务使用
  3. 运行模式/规范输入输出

4. 总结

随着业务的快速发展, 代码的生命周期越来越短, 项目慢慢发展成为恐怖的“巨兽”, 吞噬着研发同学的耐心, 叫苦不迭却难以破局, 上面讲了这么多, 我们怎么来判断是否适合引入规则引擎呢?

首先达成共识, 领导及产研同事是认可当下值得去做的, 可以解决掉我们目前发现的痛点 (发现痛点可以先通过 最简单的方式先找出那些类似 if else多分支决策场景, 根据历史改动及业务需求来进行判断)

当然最好的话, 你对代码所服务的业务具有很好的预见能力.

参考资料

www.bstek.com/#videos

rencalo770.github.io/gengine_doc…

github.com/bilibili/ge…

本文转载自: 掘金

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

《蹲坑也能进大厂》多线程这几道基础面试题,80%小伙伴第一题

发表于 2021-06-12

这是我参与更文挑战的第 2 天,活动详情查看:更文挑战

作者:花哥

微信公众号:花哥编程

前言

某一天清晨(9:00),装逼界俩泰斗早早到了公司。

狗剩:龙Gie,去不去蹲坑,趁现在没人,去占个好位置,然后给你看个好宝贝。

我:臭不要脸,大清早就想着蹲坑划水,我平是怎么教育你的,能不能向我学习。

20190424072290_dxKYFl.gif

我:真香…..

狗剩子:花Gie,听说隔壁组的毛孩离职进了那个姓阿的公司,这尼玛简直太气人了。

我:你还别不服气,人家肚里真的有货,不信我问你几个问题,看看你能不能答上来?

正文

我:你用过多线程吗?创建线程的方式有几种?

这种问题还拿出来问,你是来侮辱俺的智商么,创建线程的典型方式有两种,分别是实现Runnable接口和继承Thread类,此外还可以通过线程池、定时器、匿名内部类等来进行创建。

我:还有其他的吗?

心中嘀咕:这特么话问的想给我下套啊。啊..那个..有的。

虽然说了这么多创建线程的方式,但我们查看源码就会发现,其实本质上只有一种,那就是通过新建Thread类来创建线程,并最终通过start方法来新建线程,只是run方法的实现有两种。

  • 第一种继承Thread类是重写父类run方法
  • 而第二种实现Runnable是对接口的run方法进行实现,然后将runnable实例传递给Thread类。

1)新建Thread类,run方法实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
java复制代码//实现方式
class ThreadTest extends Thread{
@Override
public void run() {
System.out.println("我爱杰伦");
}
}
Thread thread = new Thread(new ThreadTest());
thread.start();

//线程最终调用target.run()
private Runnable target;
@Override
public void run() {
if (target != null) {
target.run();
}
}

2) 实现Runnable,run方法实现

1
2
3
4
5
6
7
java复制代码Thread thread = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("大家好,我叫狗剩子");
}
});
thread.start();

至于线程池、定时器等工具类本质上也是上述的一种实现。

小样,是不是偷看过花Gie的笔记了,那你知道继承Thread和实现Runnable接口哪种更好?

实现Runnable接口更好,主要原因有以下几个:

  • Java是单继承,如果继承Thread类后,会限制扩展性。
  • 实现Runnable接口将具体的任务(run方法)和创建线程(Thread类)分开实现,这样可以使同一个任务类传递给不同的线程,任务类不负责创建线程等工作,两者各司其职,从而实现解耦。
  • 使用继承Thread类创建线程,每次新建任务,只能创建一个新的线程,即使该任务只是打印一行日志,也要完成线程的创建销毁等过程,造成资源的严重浪费。而Runnable和线程池就能避免这种浪费。

那你知道线程有哪几种状态?它的生命周期是什么吗?

花Gie ,你把手伸过来,我给你看一样东西你就全懂了。

20160608363327_irvZSt.jpg

这啥,你踏马….!

不好意思…拿错了,把我早餐大包子拿出来了,在这…是这个

image.png

这个图是我见过画的最好的图,我们可以看到Java线程共计包含六个生命周期。

  • 新建(New):创建一个线程对象。
  1. JVM为其分配内存,并初始化其成员变量的值;
    1. 该状态下线程也不会得到调度。
  • 就绪状态(Runnable):当线程对象调用start方法之后,就会进入就绪状态,需要注意的是线程获取到时间片后依旧会处以Runnable状态。
  1. JVM创建方法调用栈和程序计数器;
    1. 该状态的线程一直处于线程就绪队列(尽管采用的是队列形式,事实上,把它成为可运行池而不是可运行队列。因为CPU的调度不一定是按照先进先出的顺序来调度的),线程并没有开始运行;
    2. 线程并不是说执行了start()方法就立即执行,需要等待系统为其分配CPU时间片。
  • 阻塞状态(Blocked):线程试图获取synchronized方法/代码块,但monitor锁被其他线程占用;处于运行状态的线程,遇到某些情况会让出自己的CPU执行权,进入阻塞状态。
  • 计时等待(TIMED_WAITING):线程调用sleep方法,主动进入休眠,在达到一定时间后它们会自动唤醒,当休眠结束进入就绪状态等待获取CPU执行权;
  • 等待(WAITING):线程调用持有锁的wait方法,处于这种状态的线程不会被分配CPU,它们要等待其他线程调用notify,来将其唤醒,否则会处于无限期等待的状态。
  • 死亡(Terminated):线程执行完成或者抛出异常,线程资源被回收。

线程进入死亡状态3种方法:

1. run()或call()方法执行完成,线程正常结束;
2. 线程抛出一个未捕获的异常;
3. 直接调用该线程的stop()方法——容易导致死锁,不推荐使用。

敲黑板:下图是Java官方文档,Java线程没有Running状态,其Runnable包含了操作系统中ready和running状态。

image.png

哟,小伙子有点东西啊,上面你说线程调用start()方法最终调用的是run()方法,

那为什么我们不直接调用run()方法呢?

因为start()是用来启动线程,run()方法只是执行线程运行时的代码,如果直接调用run()方法,也仅仅是调用一个普通的方法而已,和线程的生命周期是没有关系的,还有我们需要注意start()方法调用第二次会报运行时异常。

那你知道sleep和wait/notify的区别是什么吗?

这个也休想难倒咱家。

  • 相同:
1. 他们都可以让线程进入阻塞状态。
2. 他们都可以响应中断Thread.interrupt。
  • 不同点
1. sleep() 来自 Thread,wait() 来自 Object。
2. sleep() 不释放monitor锁,但wait() 会释放。
3. sleep() 时间到会自动恢复,wait() 可以使用 notify()/notifyAll()直接唤醒。
4. wait方法需要在同步方法中执行,而sleep不需要。

你知道yeid和sleep有什么区别吗?

好家伙,还好我准备的充足,yield的作用是让出自己的时间片,线程依旧处于Runnable状态,依然有可能被再次调度,而sleep被调用后会进入阻塞状态,在等待时间内不会被线程调度器调用,

回答的还可以,都快赶上我了,那还有一个问题,守护线程和普通线程的区别你有了解吗?

20180606218906_HsZFpy.gif

握草,午饭时间到了,不和你说了,今天暂且到这,等老衲补充一下营养,明天再会会你。

总结

以上就是今天介绍的多线程基础知识,这块知识点比较多,这里花Gie也会分多个篇幅来介绍,我敢向狗剩子保证,只要你认真跟好花Gie的这一系列,面试必能小虐面试官,如果不能…那我把狗剩子送给你。

万丈高楼平地起,花Gie认为只有打好基础,认真修炼内功,才能走的更轻松,也更远。

点关注,防走丢

以上就是本期全部内容,如有纰漏之处,请留言指教,非常感谢。我是花GieGie ,有问题大家随时留言讨论 ,我们下期见🦮。

文章持续更新,可以微信搜一搜「 花哥编程 」第一时间阅读,后续会持续更新Java面试和各类知识点,有兴趣的小伙伴欢迎关注,一起学习,一起哈🐮🥃:

原创不易,你怎忍心白嫖,如果你觉得这篇文章对你有点用的话,感谢老铁为本文点个赞、评论或转发一下,因为这将是我输出更多优质文章的动力,感谢!

本文转载自: 掘金

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

盘点 SpringBoot Application配置的

发表于 2021-06-11

首先分享之前的所有文章 , 欢迎点赞收藏转发三连下次一定 >>>> 😜😜😜

文章合集 : 🎁 juejin.cn/post/694164…

Github : 👉 github.com/black-ant

CASE 备份 : 👉 gitee.com/antblack/ca…

一. 前言

文章目的 :

  • 梳理 Applicaiton 的加载方式
  • 梳理 Profile 的处理

Spring-enviroment.jpg

二 . 扫描的触发

启动的源头任然是SpringApplication#run , 回顾之前的一篇源码 , 在 SpringApplication 中 ,会执行一段代码 :

ConfigurableEnvironment environment = prepareEnvironment(listeners, applicationArguments);

而一切的起点就是那里 , SpringBoot 通过这一句加载所有的环境信息 :

2.1 SpringApplication # prepareEnvironment

在该环节中 , 对 Environment 进行操作的处理 , 其中包括几个主要的操作 :

  1. ConfigurableEnvironment 的生成
  2. configureEnvironment 细粒度处理
  3. 对 configurationProperties 属性进行处理
  4. 发布 listener 处理执行不同类型配置文件的处理
  5. 将 environment 绑定到 SpringApplication
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
java复制代码C- SpringApplication # prepareEnvironment
private ConfigurableEnvironment prepareEnvironment(SpringApplicationRunListeners listeners,
ApplicationArguments applicationArguments) {
// 内部通过 WebApplicationType 生成不同的 Environment (可以set 自己的 Environment)
ConfigurableEnvironment environment = getOrCreateEnvironment();
// 重写此方法以完全控制环境自定义,或者重写上述方法之一以分别对属性源或概要文件进行细粒度控制。
configureEnvironment(environment, applicationArguments.getSourceArgs());
// 对 configurationProperties 属性进行处理
ConfigurationPropertySources.attach(environment);
// 发布 listener 处理
listeners.environmentPrepared(environment);
// 将 environment 绑定到 SpringApplication
bindToSpringApplication(environment); -> M1_25
if (!this.isCustomEnvironment) {
environment = new EnvironmentConverter(getClassLoader()).convertEnvironmentIfNecessary(environment,deduceEnvironmentClass());
}
ConfigurationPropertySources.attach(environment);
return environment;
}

2.1.1 ConfigurableEnvironment 的生成

此处会生成一个 StandardServletEnvironment , 他是 ConfigurableEnvironment 的实现类 , 该对象中存在2个方法 , 分别配置了多个 Source 对象

  • customizePropertySources(MutablePropertySources propertySources)
  • initPropertySources(ServletContext servletContext,ServletConfig servletConfig)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
java复制代码
// Servlet上下文初始化参数属性源名称
public static final String SERVLET_CONTEXT_PROPERTY_SOURCE_NAME = "servletContextInitParams";
// Servlet config init parameters属性源名称
public static final String SERVLET_CONFIG_PROPERTY_SOURCE_NAME = "servletConfigInitParams";

// 加载系统属性
public static final String SYSTEM_ENVIRONMENT_PROPERTY_SOURCE_NAME = "systemEnvironment";
public static final String SYSTEM_PROPERTIES_PROPERTY_SOURCE_NAME = "systemProperties";

protected void customizePropertySources(MutablePropertySources propertySources) {

// 这2步主要添加2个空对象
propertySources.addLast(new StubPropertySource(SERVLET_CONFIG_PROPERTY_SOURCE_NAME));
propertySources.addLast(new StubPropertySource(SERVLET_CONTEXT_PROPERTY_SOURCE_NAME));


if (JndiLocatorDelegate.isDefaultJndiEnvironmentAvailable()) {
propertySources.addLast(new JndiPropertySource(JNDI_PROPERTY_SOURCE_NAME));
}

// 伪代码 , 来自于父类 , 此处用于加载系统属性 -> PRO:0001
propertySources.addLast(
new PropertiesPropertySource(SYSTEM_PROPERTIES_PROPERTY_SOURCE_NAME, getSystemProperties()));
propertySources.addLast(
new SystemEnvironmentPropertySource(SYSTEM_ENVIRONMENT_PROPERTY_SOURCE_NAME, getSystemEnvironment()));
}

public void initPropertySources(@Nullable ServletContext servletContext, @Nullable ServletConfig servletConfig) {
WebApplicationContextUtils.initServletPropertySources(getPropertySources(), servletContext, servletConfig);
}


// PS : 这里的多个Sources 将会用于优先级处理
SERVLET_CONFIG_PROPERTY_SOURCE_NAME > SERVLET_CONTEXT_PROPERTY_SOURCE_NAME > JNDI_PROPERTY_SOURCE_NAME


// [PRO:0001] 系统属性的获取方式 ?
return (Map) System.getProperties();

[PRO:0001] 系统属性大概样式 >>>
system_config.jpg

其中最主要的就是第 4 步 , 该步骤中扫描不同的配置处理类

补充 : getOrCreateEnvironment 的获取

1
2
3
4
5
6
7
8
9
10
11
12
13
java复制代码private ConfigurableEnvironment getOrCreateEnvironment() {
if (this.environment != null) {
return this.environment;
}
switch (this.webApplicationType) {
case SERVLET:
return new StandardServletEnvironment();
case REACTIVE:
return new StandardReactiveWebEnvironment();
default:
return new StandardEnvironment();
}
}

2.2 发布 listener 处理执行不同类型配置文件的处理

前面主要是获取 SystemSource , 还没有正式开始 , 我们从 prepareEnvironment 第四步开始看 , 来看看后面怎么处理 :

Step 1 : 发布 Listener , 推动 Enviroment 处理

这里不需要深入太多 , 标准的 Listener 发布方式 , 这里主要发布的是 ApplicationEnvironmentPreparedEvent

1
2
3
4
5
6
7
java复制代码C- SpringApplicationRunListeners
void environmentPrepared(ConfigurableEnvironment environment) {
for (SpringApplicationRunListener listener : this.listeners) {
// PS : 此处使用的 Listener 为 ApplicationEnvironmentPreparedEvent
listener.environmentPrepared(environment);
}
}

Step 2 :ConfigFileApplicationListener 处理该事件

核心的处理方式就是 EnvironmentPostProcessor 的循环 , 后面所有的操作均在其中完成 :

1
2
3
4
5
6
7
8
9
JAVA复制代码private void onApplicationEnvironmentPreparedEvent(ApplicationEnvironmentPreparedEvent event) {
List<EnvironmentPostProcessor> postProcessors = loadPostProcessors();
postProcessors.add(this);
AnnotationAwareOrderComparator.sort(postProcessors);
// 可以看到 , 此处还是通过 Processors 进行处理 -> PS:0001
for (EnvironmentPostProcessor postProcessor : postProcessors) {
postProcessor.postProcessEnvironment(event.getEnvironment(), event.getSpringApplication());
}
}

三 . postProcessors 的处理

前面看了 , 通过调用 postProcessor.postProcessEnvironment 来实现不同的环境加载 , 可以看到 , 此处主要有5个 postProcessors :

PS:0001 ConfigFileApplicationListener 中 postProcessors 有哪些

config_process.jpg

1
2
3
4
5
java复制代码- SystemEnvironmentPropertySourceEnvironmentPostProcessor
- SpringApplicationJsonEnvironmentPostProcessor
- CloudFoundryVcapEnvironmentPostProcessor
- ConfigFileApplicationListener(没错 , 他本身也是个 EnvironmentPostProcessor)
- DebugAgentEnvironmentPostProcessor

3.1 SystemEnvironmentPropertySourceEnvironmentPostProcessor

该类主要是对 systemEnvironment 进行处理 , 前面看到了 , 实际上前面已经拿到 SystemSource , 此处是对这些配置的二次处理 :

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复制代码public void postProcessEnvironment(
ConfigurableEnvironment environment, SpringApplication application) {
// >>> systemEnvironment
String sourceName = StandardEnvironment.SYSTEM_ENVIRONMENT_PROPERTY_SOURCE_NAME;
// 从 environment 中获取 systemEnvironment 的所有属性
PropertySource<?> propertySource = environment.getPropertySources().get(sourceName);
if (propertySource != null) {
replacePropertySource(environment, sourceName, propertySource);
}
}

// 此处主要是把 SystemEnvironmentPropertySource 封装成了 OriginAwareSystemEnvironmentPropertySource
private void replacePropertySource(ConfigurableEnvironment environment, String sourceName,
PropertySource<?> propertySource) {
Map<String, Object> originalSource = (Map<String, Object>) propertySource.getSource();
SystemEnvironmentPropertySource source
= new OriginAwareSystemEnvironmentPropertySource(sourceName,originalSource);
environment.getPropertySources().replace(sourceName, source);
}

[Pro] OriginAwareSystemEnvironmentPropertySource 是什么 ?

// OriginAwareSystemEnvironmentPropertySource 是SystemEnvironmentPropertySource 的子类,
// 提供了获取Origin的方法,即返回SystemEnvironmentOrigin对象

SystemEnvironmentOrigin 提供对原始属性名的访问

3.2 SpringApplicationJsonEnvironmentPostProcessor

该类的主要使用形式是在启动 jar 包时使用 , 这种方式的优先级最高:

java -jar xxx.jar --spring.application.json={\"username\":\"ant-black\"}

image.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
java复制代码// 从 spring.application.json或等价的SPRING_APPLICATION_JSON中解析JSON ,
// 并将其作为映射属性源添加到环境中

public void postProcessEnvironment(
ConfigurableEnvironment environment, SpringApplication application) {
MutablePropertySources propertySources = environment.getPropertySources();
propertySources.stream().map(JsonPropertyValue::get).filter(Objects::nonNull).findFirst()
.ifPresent((v) -> processJson(environment, v));
}

private void processJson(ConfigurableEnvironment environment, JsonPropertyValue propertyValue) {
JsonParser parser = JsonParserFactory.getJsonParser();
// 使用 JsonParser 将 JSON 解析为 Map 集合
Map<String, Object> map = parser.parseMap(propertyValue.getJson());
if (!map.isEmpty()) {
addJsonPropertySource(environment, new JsonPropertySource(propertyValue, flatten(map)));
}
}

// 这里可以看到 , spring.application.json 添加的优先级是最高的
private void addJsonPropertySource(ConfigurableEnvironment environment, PropertySource<?> source) {
MutablePropertySources sources = environment.getPropertySources();
String name = findPropertySource(sources);
if (sources.contains(name)) {
// 添加优先级立即高于命名相对属性源的给定属性源对象
sources.addBefore(name, source);
} else {
sources.addFirst(source);
}
}

3.3 CloudFoundryVcapEnvironmentPostProcessor

一个环境 postprocessor,它知道在现有环境中在哪里找到VCAP(也就是云计算)元数据。它解析VCAP_APPLICATION和VCAP_SERVICES元数据,并将其转储Environment。

这一块比较玄妙 , 首先要知道 VCAP 是什么 :

  • IBM Cloud 中有一个概念 : Cloud Foundry 应用程序
  • 在 Cloud Foundry 运行的应用程序通过存储在一个名为 VCAP services 的环境变量中的凭证获得对绑定服务实例的访问
  • VCAP_APPLICATION 是指云计算的应用单体元数据
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
java复制代码public void postProcessEnvironment(ConfigurableEnvironment environment, SpringApplication application) {
if (CloudPlatform.CLOUD_FOUNDRY.isActive(environment)) {
Properties properties = new Properties();
JsonParser jsonParser = JsonParserFactory.getJsonParser();
addWithPrefix(properties, getPropertiesFromApplication(environment, jsonParser), "vcap.application.");
addWithPrefix(properties, getPropertiesFromServices(environment, jsonParser), "vcap.services.");
MutablePropertySources propertySources = environment.getPropertySources();
if (propertySources.contains(CommandLinePropertySource.COMMAND_LINE_PROPERTY_SOURCE_NAME)) {
propertySources.addAfter(CommandLinePropertySource.COMMAND_LINE_PROPERTY_SOURCE_NAME,
new PropertiesPropertySource("vcap", properties));
}
else {
propertySources.addFirst(new PropertiesPropertySource("vcap", properties));
}
}
}

简单来说 , 这个类主要是为了云计算的环境准备的 , 因为没有太多涉及 , 此处就不深入了

3.4 ConfigFileApplicationListener

这一块应该就是最核心的一块了 , 这里会对 Application 文件进行扫描处理 :

Step 1 : ConfigFileApplicationListener 的入口

由于 ConfigFileApplicationListener 本身继承了 EnvironmentPostProcessor , 其本身也会加载配置环境 :

1
2
3
4
5
java复制代码// 将配置文件属性源添加到指定的环境
protected void addPropertySources(ConfigurableEnvironment environment, ResourceLoader resourceLoader) {
RandomValuePropertySource.addToEnvironment(environment);
new Loader(environment, resourceLoader).load();
}

Step 2 : 构建 Loader 对象

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复制代码// 来看一看 loader 对象 , 该对象会用于处理属性 Document 并且扫描
Loader(ConfigurableEnvironment environment, ResourceLoader resourceLoader) {
//
this.environment = environment;
// 解析器 , 用于解析占位符
this.placeholdersResolver = new PropertySourcesPlaceholdersResolver(this.environment);
// 资源加载器
this.resourceLoader = (resourceLoader != null) ? resourceLoader : new DefaultResourceLoader();
// 属性加载器 , 这里是从 SpringFactories 中加载
this.propertySourceLoaders = SpringFactoriesLoader.loadFactories(PropertySourceLoader.class,
getClass().getClassLoader());
}


// 我们省略其中的一些流程 , 直接关注主流程 :
void load() {
FilteredPropertySource.apply(this.environment, DEFAULT_PROPERTIES, LOAD_FILTERED_PROPERTY,
(defaultProperties) -> {
this.profiles = new LinkedList<>();
this.processedProfiles = new LinkedList<>();
this.activatedProfiles = false;
this.loaded = new LinkedHashMap<>();
initializeProfiles();
// 处理 Profile
while (!this.profiles.isEmpty()) {
Profile profile = this.profiles.poll();
if (isDefaultProfile(profile)) {
addProfileToEnvironment(profile.getName());
}
load(profile, this::getPositiveProfileFilter,
addToLoaded(MutablePropertySources::addLast, false));
this.processedProfiles.add(profile);
}
load(null, this::getNegativeProfileFilter, addToLoaded(MutablePropertySources::addFirst, true));
addLoadedPropertySources();
applyActiveProfiles(defaultProperties);
});
}

Step 3 : 扫描所有的路径 , 发起处理

此处通过 getSearchLocations 获得所有的根路径 , 然后依次对根路径下的文件进行扫描

PS :因为处理的顺序不同 , 更靠后的优先级更高

1
2
3
4
5
6
7
8
9
10
java复制代码private void load(Profile profile, DocumentFilterFactory filterFactory, DocumentConsumer consumer) {
// 获取所有的路径 , 并且对路径进行扫描
getSearchLocations().forEach((location) -> {
boolean isFolder = location.endsWith("/");
// 注意 , 这里 SearchName 是获取所有扫描的文件名 , 可以通过 spring.config.name 配置
// 默认名称 application
Set<String> names = isFolder ? getSearchNames() : NO_SEARCH_NAMES;
names.forEach((name) -> load(location, name, profile, filterFactory, consumer));
});
}

这里主要有四个默认路径 :

  • file:./config/
  • file:./
  • classpath:/config/
  • classpath:/

Step 4 : 对路径进行依次处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
java复制代码private void load(String location, String name, Profile profile, DocumentFilterFactory filterFactory,
DocumentConsumer consumer) {
if (!StringUtils.hasText(name)) {
for (PropertySourceLoader loader : this.propertySourceLoaders) {
if (canLoadFileExtension(loader, location)) {
load(loader, location, profile, filterFactory.getDocumentFilter(profile), consumer);
return;
}
}
throw new IllegalStateException(".....");
}
Set<String> processed = new HashSet<>();
// PropertySourceLoader -> PS:0002
for (PropertySourceLoader loader : this.propertySourceLoaders) {
// 此处获取额外的属性 , 此处为 yml 和 yaml
for (String fileExtension : loader.getFileExtensions()) {
if (processed.add(fileExtension)) {
// 此处拿到的值为 classpath:/application
loadForFileExtension(loader, location + name, "." + fileExtension, profile, filterFactory,consumer);
}
}
}
}

PS:0002 PropertySourceLoader 的类型

此处提供了 2 种 PropertySourceLoader ,分别是 PropertiesPropertySourceLoader (.properties) 和 YamlPropertySourceLoader (支持 .yml .yaml’)

Step 5 : loadForFileExtension 处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
java复制代码private void loadForFileExtension(PropertySourceLoader loader, String prefix, String fileExtension,
Profile profile, DocumentFilterFactory filterFactory, DocumentConsumer consumer) {
DocumentFilter defaultFilter = filterFactory.getDocumentFilter(null);
DocumentFilter profileFilter = filterFactory.getDocumentFilter(profile);
if (profile != null) {
// 尝试配置文件中特定配置文件和配置文件段(gh-340)
String profileSpecificFile = prefix + "-" + profile + fileExtension;
load(loader, profileSpecificFile, profile, defaultFilter, consumer);
load(loader, profileSpecificFile, profile, profileFilter, consumer);
// 尝试分析我们已经处理过的文件中的特定部分
for (Profile processedProfile : this.processedProfiles) {
if (processedProfile != null) {
String previouslyLoaded = prefix + "-" + processedProfile + fileExtension;
load(loader, previouslyLoaded, profile, profileFilter, consumer);
}
}
}
// Also try the profile-specific section (if any) of the normal file
load(loader, prefix + fileExtension, profile, profileFilter, consumer);
}

Step 6 : 对 Application 文件进行读取

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
java复制代码private void load(PropertySourceLoader loader, String location, Profile profile, DocumentFilter filter,
DocumentConsumer consumer) {
try {
Resource resource = this.resourceLoader.getResource(location);
if (resource == null || !resource.exists()) {
// PS : 当依次对四个路径进行处理的时候 , 如果不存在会由此逻辑返回
return;
}
if (!StringUtils.hasText(StringUtils.getFilenameExtension(resource.getFilename()))) {
return;
}
String name = "applicationConfig: [" + location + "]";
// 通过 Document 加载文档 , 此处为 applicationConfig: [classpath:/application.yml]
List<Document> documents = loadDocuments(loader, name, resource);
if (CollectionUtils.isEmpty(documents)) {
// PS : 如果文档为空 ,会从此逻辑返回
return;
}
List<Document> loaded = new ArrayList<>();
for (Document document : documents) {
if (filter.match(document)) {
// 处理 Profiles 文件
addActiveProfiles(document.getActiveProfiles());
addIncludedProfiles(document.getIncludeProfiles());
loaded.add(document);
}
}
Collections.reverse(loaded);
if (!loaded.isEmpty()) {
loaded.forEach((document) -> consumer.accept(profile, document));
}
}catch (Exception ex) {
throw new IllegalStateException("Failed to load property source from location '" + location + "'", ex);
}
}

Step 7 : 调用 loader 流程发起加载

该逻辑就是对Document 的处理了 , 也不用太深入

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
java复制代码
private List<Document> loadDocuments(PropertySourceLoader loader, String name, Resource resource)
throws IOException {
DocumentsCacheKey cacheKey = new DocumentsCacheKey(loader, resource);
List<Document> documents = this.loadDocumentsCache.get(cacheKey);
if (documents == null) {
// 此处 loader 为 org.springframework.boot.env.YamlPropertySourceLoader
List<PropertySource<?>> loaded = loader.load(name, resource);
documents = asDocuments(loaded);
this.loadDocumentsCache.put(cacheKey, documents);
}
return documents;
}


// PS : 此处暂时就可以不用关注了 , 后续主要是Loader 的加载逻辑 , 可以参考之前的文档 :
I- PropertySourceLoader
C- YamlPropertySourceLoader
C- PropertiesPropertySourceLoader

3.5 DebugAgentEnvironmentPostProcessor

作用 : 启用反应堆调试代理

开启 : 调试代理默认是启用的,除非 spring.reactor.debug-agent.enabled 配置属性设置为false

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
JAVA复制代码// 这是个特殊的类 , 目的是为了快速加载
private static final String DEBUGAGENT_ENABLED_CONFIG_KEY = "spring.reactor.debug-agent.enabled";


public void postProcessEnvironment(ConfigurableEnvironment environment, SpringApplication application) {
if (ClassUtils.isPresent(REACTOR_DEBUGAGENT_CLASS, null)) {
Boolean agentEnabled = environment.getProperty(DEBUGAGENT_ENABLED_CONFIG_KEY, Boolean.class);
if (agentEnabled != Boolean.FALSE) {
try {
Class<?> debugAgent = Class.forName(REACTOR_DEBUGAGENT_CLASS);
// 调用init 方法初始化
debugAgent.getMethod("init").invoke(null);
} catch (Exception ex) {
throw new RuntimeException("Failed to init Reactor's debug agent");
}
}
}
}

这个类的主要目的就是为了初始化 REACTOR_DEBUGAGENT_CLASS 类 , 而不是为了加载配置 , 这种方式更加快 (学到了!!!!)

四 . 知识点

4.1 Profile 的处理

Step 1 : 当第一步扫描 Document 的时候 , 会从其中获得 profile 属性 , 并且标注从来

image.png

Step 2 : 处理 Profiles

起因是上文 Step 6 中调用的 addActiveProfiles 和 addIncludedProfiles

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
java复制代码void addActiveProfiles(Set<Profile> profiles) {
if (profiles.isEmpty()) {
return;
}
if (this.activatedProfiles) {
return;
}
this.profiles.addAll(profiles);
this.activatedProfiles = true;
removeUnprocessedDefaultProfiles();
}

// 此处设置了 ConfigFileApplicationListener 的 Profiles 属性
private void addIncludedProfiles(Set<Profile> includeProfiles) {
LinkedList<Profile> existingProfiles = new LinkedList<>(this.profiles);
this.profiles.clear();
this.profiles.addAll(includeProfiles);
this.profiles.removeAll(this.processedProfiles);
this.profiles.addAll(existingProfiles);
}

// PS:此处设置完成后 ,Profiles 就会由 default 变成 test

到这里还没完 , 还要做相关的处理 :

Step 3: 反复处理 Profiles

回顾上文 Step 5 : loadForFileExtension 处理 中 , 会发现这样一个处理

1
2
3
4
5
6
7
8
java复制代码private void loadForFileExtension(PropertySourceLoader loader, String prefix, String fileExtension,
// ........ 省略
String profileSpecificFile = prefix + "-" + profile + fileExtension;
load(loader, profileSpecificFile, profile, defaultFilter, consumer);
// 核心 , profile 再次处理
load(loader, profileSpecificFile, profile, profileFilter, consumer);
//.........
}

没错 , 这里就是把 profile传进去原样再扫描一遍 , 而值就是上一步设置的 value

你以为这就完了吗 ? 这就像盗梦空间里面一样 , 你以为你醒了 ,其实还在一个梦里 , 整个逻辑中涉及多个循环处理 , 而 Profile 的循环开启是在第一层

1
2
3
4
5
6
7
8
9
10
11
java复制代码// 当第一遍大循环执行玩抽 , profiles 中就已经有了新的 profiles , 此处会开启第二个大循环
// 此处使用 Deque<Profile> profiles , 以弹出的方式获取对象
while (!this.profiles.isEmpty()) {
Profile profile = this.profiles.poll();
if (isDefaultProfile(profile)) {
addProfileToEnvironment(profile.getName());
}
load(profile, this::getPositiveProfileFilter,
addToLoaded(MutablePropertySources::addLast, false));
this.processedProfiles.add(profile);
}

补充一下循环体系 :

  • Loader # load : while 循环 profile
  • ConfigFileApplicationListener # load(1) : foreach location
  • ConfigFileApplicationListener # load(1) : foreach names (PS : 这个循环在上个循环的内部)
  • ConfigFileApplicationListener # load(2) : for 循环 propertySourceLoaders
  • ConfigFileApplicationListener # load(2) : for 循环 FileExtensions

load(1) : load(Profile profile, DocumentFilterFactory filterFactory, DocumentConsumer consumer)

load(2) : load(String location, String name, Profile profile, DocumentFilterFactory filterFactory,
DocumentConsumer consumer)

4.2 具体的加载顺序

Step 1 : 后缀文件的加载顺序

从 ConfigFileApplicationListener # load 方法种 , 我们可以看到 , 加载顺序依次是 :

  • properties
  • xml (没错 , 他会试图加载 xml 文件)
  • yml
  • yaml

总结

本来以为篇幅不会太长, 结构又写了这么多 , 所以将属性的转换放在下一篇将 ,欢迎点赞收藏

修改记录

  • V20210612 : 添加 >> 四 . 知识点
  • V20210804 : 添加结构图

本文转载自: 掘金

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

状态机引擎实现调研 背景 状态机简介 技术选型对比

发表于 2021-06-11

今天来分享了一下,前段时间状态机引擎实现调研结果

背景

  • 1.很多文章都分享过状态管理的相关方案,方案和具体业务相关性强,可以给我们提供通用的设计思路与方法论,很难提供一些通用工具组件出来
  • 2.本次调研的主要目的是,提供实现状态引擎可以共用的工具组件,来提高状态管理相关的开发效率,避免重复造轮子
  • 3.本次主要调研了业界常用的、开源的、社区文档说明较多的两种方案
  • 4.spring-statemachine是spring官方提供的状态机实现
  • 5.squirrel-foundation是一个开源的轻量级的状态机实现(马蜂窝交易系统、美团ERP研发中心方案使用过该方案)

状态机简介

状态机概念

状态机是一种用来进行对象行为建模的工具,描述对象在生命周期内所经历的状态变化

特点

  • 有限状态机一般都有以下特点:
    可以用状态来描述事物,并且任一时刻,事物总是处于一种状态事物拥有的状态总数是有限的通过触发事物的某些行为,可以导致事物从一种状态过渡到另一种状态事物状态变化是有规则的,A状态可以变换到B,B可以变换到C,A却不一定能变换到C同一种行为,可以将事物从多种状态变成同种状态,但是不能从同种状态变成多种状态。

适用场景

适用与对象有一个明确的生命周期,并且在生命周期的状态变迁中存在不同的触发条件以及达到状态需要执行的动作。将所有的状态、事件、动作都抽离出来,对复杂的状态迁移逻辑进行统一管理,来取代冗长的 if else 判断,明确的状态条件、原子的响应动作、事件驱动迁移目标状态,使系统更加易于维护和管理

状态机要素

状态机可归纳为4个要素:

即现态、事件、动作、次态。“现态”和“条件”是因,“动作”和“次态”是果。详解如下:

现态

现态:是指当前所处的状态

事件

事件:又称为“条件”。当一个条件被满足,将会触发一个动作,或者执行一次状态的迁移

动作

动作:条件满足后执行的动作。动作执行完毕后,可以迁移到新的状态,也可以仍旧保持原状态。动作不是必需的,当条件满足后,也可以不执行任何动作,直接迁移到新状态

次态

次态:条件满足后要迁往的新状态。“次态”是相对于“现态”而言的,“次态”一旦被激活,就转变成新的“现态”了

技术选型对比

spring statemachine

spring-statemachine是spring官方提供的状态机实现

Star 892

开源地址如下: github.com/spring-proj…

官方文档如下: projects.spring.io/spring-stat…

优点

轻量级,可以快速接入(已经接入spring框架的应用)内置redis等持久化手段可以通过对接zookpeer实现分布式状态管理模型比较复杂支持较为负责多层的状态模型配置支持使用注解,使用方便,支持基于spring aop上的扩展spring推出,持续迭代

缺点

对于一些没有接入spring框架的应用接入比较困难相关使用文档等较少模型较为负责,接入学习成本相对较高监听器、aop等组件模块都使用了spring框架中的模块,在上面做改造较难

squirrel-foundation

squirrel-foundation一个开源产品 Star 1.4k

开源地址如下: github.com/hekailiang/…

官方文档如下: hekailiang.github.io/squirrel/

优点

squirrel的实现更为轻量,设计思路也比较清晰,文档以及测试用例也很丰富squirrel关注的人比较大,目前github star,1.4k,贡献采坑问题和问题场景也比较丰富不依赖于spring,没有接入spring框架的应用也可以接入使用支持使用注解,支持自定义切点内部实现不依赖于spring,可以在在上面扩展改造

缺点

切点粒度比较粗,要使用细粒度的切点需要自己实现模型相比较spring statemachine较为简单,实现复杂的多层状态模型需要在此基础上开发实现

选择squirrel-foundation

  • 这里选择squirrel-foundation原因如下:
    量级比较轻,扩展和维护相对而言比较容易不强行依赖于spring,没有接入spring框架的应用也可以接入使用,用spring也很方便入点丰富,支持状态进入、状态完成、异常等节点的监听,使转换过程留有足够的切入点支持使用注解定义状态转移,使用方便开源项目关注多,文档与相关资料多

核心数据流

状态机.png

State

State:定义出来业务中所有的状态

Event

Event:定义出来造成状态变更的事件

Context

Context:上下文对象,一般存储业务实体等信息(如活动状态机中存储活动对象campaign)

Condition

Condition: 条件,当触发事件时,可以对context对象中的业务实体进行一些条件判断,是否达到设置的要求

Action

Action: 要执行的动作,目前有三类动作,exit从某个状态退出时要执行的动作,transition从一个状态变更到另一个状态要执行的动作,entry进入一个状态要执行的动作

Listener

Listener: 监听器,监听整个状态变更的过程

核心处理流程

借用了网上文档的中图片,觉得这个图描述的比较清楚
状态机核心处理流程.png

状态正常迁移

TransitionBegin–(exit->transition->entry)–>TransitionComplete–>TransitionEnd

状态迁移异常

TransitionBegin–(exit->transition->entry)–>TransitionException–>TransitionEnd

状态迁移事件拒绝

TransitionBegin–>TransitionDeclined–>TransitionEnd

接入demo

pom文件
1
2
3
4
5
xml复制代码<dependency>
<groupId>org.squirrelframework</groupId>
<artifactId>squirrel-foundation</artifactId>
<version>0.3.8</version>
</dependency>
定义状态State
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复制代码public enum LinkStates {

LINK_INIT_STATE(1,"初始状态"),
LINK_NEW_STATE(2,"未发布状态"),
LINK_RUN_STATE(3,"执行种状态"),
LINK_DELETE_STATE(1,"已删除状态"),
LINK_PAUSE_STATE(4,"暂停状态"),
LINK_END_STATE(5,"结束状态");

private int state;
private String name;

public int getState() {
return state;
}

public String getName() {
return name;
}

private LinkStates(int state, String name) {
this.state = state;
this.name = name;
}
}
定义事件Event
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
java复制代码public enum LinkStatesEvent {

LINK_BUILD_EVENT(1,"新建操作","新建操作状态为2",0,2),
LINK_RELEASE_EVENT(2,"发布操作","发布操作,状态从2变为3",2,3),
LINK_PAUSE_EVENT(3,"暂停操作","暂停操作,状态从3变为4",3,4),
LINK_CONTINUE_EVENT(4,"继续操作","继续操作,状态从4变为3",4,3),
LINK_END1_EVENT(5,"执行中结束操作","结束操作,状态从3变为5",3,5),
LINK_END2_EVENT(6,"暂停中结束操作","结束操作,状态从4变为5",4,5),
LINK_DELETE_EVENT(7,"删除操作","删除操作,状态从2变为1",2,1);


private int opType;
private String name;
private String msg;
private int from;
private int to;

private LinkStatesEvent(int opType, String name, String msg, int from, int to) {
this.opType = opType;
this.name = name;
this.msg = msg;
this.from = from;
this.to = to;
}

public int getOpType() {
return opType;
}

public String getName() {
return name;
}

public String getMsg() {
return msg;
}

public int getFrom() {
return from;
}

public int getTo() {
return to;
}
}
定义上下文Context
1
2
3
4
5
6
7
8
9
10
11
12
13
java复制代码public class MyContext {

//这里使用自己业务对象 活动对象
private CampaignInfo campaignInfo;

public CampaignInfo getCampaignInfo() {
return campaignInfo;
}

public void setCampaignInfo(CampaignInfo campaignInfo) {
this.campaignInfo = campaignInfo;
}
}
定义状态机,要执行的动作可以定义到状态机对象中

基类为: StateMachine<T, S, E, C>需要把定义好的状态、事件、上下文对象传入

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
java复制代码public class MyStateMachine extends AbstractStateMachine<MyStateMachine, LinkStates, LinkStatesEvent, MyContext> {


/**
* 动作1:transition动作 从 状态 from 变动到 to状态,触发事件并满足条件后执行
* @param from 现态
* @param to 次态
* @param event 事件
* @param context 上下文对象
*/
public void transition(LinkStates from, LinkStates to, LinkStatesEvent event, MyContext context) {
System.out.println("transition() 方法执行了。。。。。。。。。。。。。 from:" + from + ", to:" + to + ", " +
"event:" + event + ", context:" + context.getCampaignInfo());
}

/**
* 动作1:exit动作 从 状态 from 退出后,触发事件并满足条件后执行
* @param from 现态
* @param to 次态
* @param event 事件
* @param context 上下文对象
*/
protected void exitFrom(LinkStates from, LinkStates to, LinkStatesEvent event, MyContext context){
System.out.println("exitFrom() 方法执行了。。。。。。。。。。。。。 from:" + from + ", to:" + to + ", " +
"event:" + event + ", context:" + context.getCampaignInfo());
}

/**
* 动作1:entry动作 进入to状态后,触发事件并满足条件后执行
* @param from 现态
* @param to 次态
* @param event 事件
* @param context 上下文对象
*/
protected void entryTo(LinkStates from, LinkStates to, LinkStatesEvent event, MyContext context){
System.out.println("entryTo() 方法执行了。。。。。。。。。。。。。 from:" + from + ", to:" + to + ", " +
"event:" + event + ", context:" + context.getCampaignInfo());
}

}
定义监听器
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
java复制代码public class MyStateListener  {


@OnTransitionBegin
public void transitionBegin(LinkStatesEvent event) {
System.out.println("转换开始执行.." + event);
}


@OnTransitionBegin(when = "context.num == 20 || event.getName().equals(\"新建操作\")")
public void begins(LinkStates from, LinkStates to, LinkStatesEvent event, MyContext context) {
System.out.println("begins 执行了, from:" + from + ", to:" + to + ", event:" + event + ", context:" + context.getCampaignInfo());
}

@OnTransitionEnd
public void transitionEnd() {
System.out.println("转换结束执行..");
}

@OnTransitionComplete
public void transitionComplete(LinkStates from, LinkStates to, LinkStatesEvent event, MyContext context) {
System.out.println("转换成功执行..");
}

@OnTransitionDecline
public void transitionDeclined(LinkStates from, LinkStatesEvent event, MyContext context) {
System.out.println("转换拒绝执行..");
}

@OnBeforeActionExecuted
public void onBeforeActionExecuted(LinkStates sourceState, LinkStates targetState,
LinkStatesEvent event, MyContext context, int[] mOfN, Action action) {
System.out.println("状态机内方法动作执行之前..");
}

@OnAfterActionExecuted
public void onAfterActionExecuted(LinkStates sourceState, LinkStates targetState,
LinkStatesEvent event, MyContext context, int[] mOfN, Action action) {
System.out.println("状态机内方法动作执行之后..");
}

@OnActionExecException
public void onActionExecException(Action action, TransitionException e) {
System.out.println("转换异常执行..");
}

}

这里使用了注解定义了监听器

@OnTransitionBegin

转换开始执行

@OnTransitionBegin(when = “context.campaignInfo.state == 1 || event.name.equals(“新建操作”)”)

这里使用了条件判断,判断条件单独说

@OnTransitionEnd

转换结束执行

@OnTransitionComplete

转换成功执行

@OnTransitionDecline

转换拒绝执行

@OnBeforeActionExecuted

状态机内动作执行之前

@OnAfterActionExecuted

状态机内方法动作执行之后

@OnActionExecException

转换异常执行

测试代码,API构建状态机
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复制代码public class TestState {


public static void main(String[] args) {
StateMachineBuilder<MyStateMachine, LinkStates, LinkStatesEvent, MyContext> builder =
StateMachineBuilderFactory.create(MyStateMachine.class, LinkStates.class, LinkStatesEvent.class, MyContext.class);


//设置现态为 1,初始状态, 次态为2,未发布状态
builder.externalTransition().from(LinkStates.LINK_INIT_STATE).to(LinkStates.LINK_NEW_STATE)
//造成转化的事件为 新建操作,状态从1变为2
.on(LinkStatesEvent.LINK_BUILD_EVENT)
//设置条件 context条件不为空 并且 getCampaignInfo.getState得结果是1的
.whenMvel("myCondition:::(context!=null && context.getCampaignInfo().getState() == 1)")
//设置动作 设置方法名 满足条件后 调用 transition方法
.callMethod("transition");


MyStateMachine machine = builder.newStateMachine(LinkStates.LINK_INIT_STATE);
//设置监听器
machine.addDeclarativeListener(new MyStateListener());
//开启
machine.start();

System.out.println("当前状态为" + machine.getCurrentState());
System.out.println("模拟业务操作");

//设置上下文对象
MyContext context = new MyContext();
CampaignInfo campaignInfo = new CampaignInfo();
campaignInfo.setState(1);
context.setCampaignInfo(campaignInfo);

//发送事件
machine.fire(LinkStatesEvent.LINK_BUILD_EVENT, context);

System.out.println("现在状态为 " + machine.getCurrentState());

}
}
测试结果:

测试结果.png

注解定义状态机

也提供了注解的定义方式

1
2
3
4
5
6
7
8
java复制代码@States({
@State(name="A", entryCallMethod="entryStateA", exitCallMethod="exitStateA"),
@State(name="B", entryCallMethod="entryStateB", exitCallMethod="exitStateB")
})
@Transitions({
@Transit(from="A", to="B", on="GoToB", callMethod="stateAToStateBOnGotoB"),
@Transit(from="A", to="A", on="WithinA", callMethod="stateAToStateAOnWithinA", type= TransitionType.INTERNAL)
})

States 注解,定了进入状态要执行的方法,退出状态要执行的方法Transitions 注解,定义状态变更的事件和要执行的方式注解标记到状态机的实现类上

条件判断表达式说明

注解和api创建方式都会使用到条件判断表达式表达式中可以使用 event、context 两个对象中的属性进行判断,但是要实现属性的getcontext.getCampaignInfo().getState() == 1 || event.getName().equals(“新建操作”),使用对象.get方法可以获取到具体的值context.campaignInfo().state() == 1 || event.name.equals(“新建操作”),使用属性名也可以获取具体的属性值

本文转载自: 掘金

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

史上最全jdk版本新特性大全

发表于 2021-06-11

前言

在本文中,我将描述自第8版以来Java最重要且对开发人员友好的功能。为什么会有这样的主意?在Web上,您可以找到许多文章,其中包含每种Java版本的新功能列表。但是,由于缺少文章,因此无法简要概述自第8版以来最重要的更改。好的,但是为什么是第8版?令人惊讶的是,它仍然是最常用的Java版本。即使我们已经到了Java 16发行版的前夕果。如您所见,超过46%的响应者仍在生产中使用Java 8。相比之下,只有不到10%的响应者使用Java 12或更高版本。

java版本使用占比

那接下来咋们从JDK8到JDK15,给大家介绍新的JDK提供给咋们的新特性!

JDK8

  1. Lambda表达式

最直接作用就是减少代码,代码直接减少50%+,显得非常简洁

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
vbnet复制代码 //使用java匿名内部类
Comparator<Integer> cpt = new Comparator<Integer>() {
@Override
public int compare(Integer o1, Integer o2) {
return Integer.compare(o1,o2);
}
};

TreeSet<Integer> set = new TreeSet<>(cpt);

System.out.println("=========================");

//使用JDK8 lambda表达式
Comparator<Integer> cpt2 = (x,y) -> Integer.compare(x,y);
TreeSet<Integer> set2 = new TreeSet<>(cpt2);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
csharp复制代码// java7中  筛选产品为nike的
public List<Product> filterProductByColor(List<Product> list){
List<Product> prods = new ArrayList<>();
for (Product product : list){
if ("nike".equals(product.getName())){
prods.add(product);
}
}
return prods;
}

// 使用 lambda
public List<Product> filterProductByPrice(List<Product> list){
return list.stream().filter(p->"nike".equals(p.getName())).collect(Collectors.toList());
}
  1. 函数式接口

位于java.util.function包下,下面介绍最常用的几个

  • Predicate

接收一个值返回boolean

1
ini复制代码  Predicate p = t->true;
  • Supplier

无接受参数返回一个值

1
ini复制代码Supplier<T> s = () -> new T();
  • Consumer

接受一个参数无返回值

1
ini复制代码Consumer<String> c = c -> System.out.println(s);
  • Function<T,R>

接受参数T 返回参数R

1
vbnet复制代码Function<Long,String> f = c -> String.valueof(c);
  • 其他还有一些 BiFunction,BiConsumer,DoubleSupplier等大家有兴趣自己去阅读下源码
  1. 方法引用

  • 静态引用:
    格式:Class::static_method
1
2
3
ini复制代码List<String> list = Arrays.asList("a","b","c");
list.forEach(str -> System.out.print(str));
list.forEach(System.out::print);
  • 构造器调用
    构造器方法引用格式:Class::new,调用默认构造器
1
2
3
4
5
6
7
8
9
10
arduino复制代码List<String> list = Arrays.asList("a","b","c");
List<Test> list.stream().map(Test::new).collect(Collectors.toList());

public class Test{
private final String desc;

public Test(String desc){
this.desc=desc;
}
}
  • 方法调用
    格式:instance::method
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
arduino复制代码List<String> list = Arrays.asList("a","b","c");
Test test = new Test();
List<String> list.stream().map(test::toAdd).collect(Collectors.toList());

public class Test{
private final String desc;

public Test(String desc){
this.desc=desc;
}

public String toAdd(String desc){
return desc+"add";
}
}
  1. Stream API

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
scss复制代码// 使用jdk1.8中的Stream API进行集合的操作
@Test
public void test(){

// 循环过滤元素
proList.stream()
.fliter((p) -> "红色".equals(p.getColor()))
.forEach(System.out::println);

// map处理元素然后再循环遍历
proList.stream()
.map(Product::getName)
.forEach(System.out::println);

// map处理元素转换成一个List
proList.stream()
.map(Product::getName)
.collect(Collectors.toList());
}
  1. 接口中的默认方法和静态方法

1
2
3
4
5
6
7
8
9
10
11
csharp复制代码public interface ProtocolAdaptor {

ProtocolAdaptor INSTANCE = DynamicLoader.findFirst(ProtocolAdaptor.class).orElse(null);


default ProtocolAdaptor proxy() {
return (ProtocolAdaptor) Proxy.newProxyInstance(ProtocolAdaptor.class.getClassLoader(),
new Class[]{ProtocolAdaptor.class},
(proxy, method, args) -> intercept(method, args));
}
}
  1. Optional

用于处理对象空指针异常:

1
2
3
4
typescript复制代码  public String getDesc(Test test){
return Optional.ofNullable(test)
.map(Test::getDesc).else("");
}

JDK9

  • 收集工厂方法

借助Java 9的一项新功能,即集合工厂方法,您可以轻松地使用预定义的数据创建不可变的集合。您只需要在特定集合类型上使用of方法。

1
2
ini复制代码List<String> fruits = List.of("apple", "banana", "orange");
Map<Integer, String> numbers = Map.of(1, "one", 2,"two", 3, "three");

在Java 9之前,您可以使用Collections,但这绝对是一种更复杂的方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
typescript复制代码public List<String> fruits() {
List<String> fruitsTmp = new ArrayList<>();
fruitsTmp.add("apple");
fruitsTmp.add("banana");
fruitsTmp.add("orange");
return Collections.unmodifiableList(fruitsTmp);
}

public Map<Integer, String> numbers() {
Map<Integer, String> numbersTmp = new HashMap<>();
numbersTmp.put(1, "one");
numbersTmp.put(2, "two");
numbersTmp.put(3, "three");
return Collections.unmodifiableMap(numbersTmp);
}

同样,仅从ArrayList对象表创建即可使用Arrays.asList(…)method。

1
2
3
4
typescript复制代码public List<String> fruitsFromArray() {
String[] fruitsArray = {"apple", "banana", "orange"};
return Arrays.asList(fruitsArray);
}
  • 接口中的私有方法

从Java 8开始,您可以在接口内部使用公共默认方法。但是仅从Java 9开始,由于接口中的私有方法,您将能够充分利用此功能。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
csharp复制代码ublic interface ExampleInterface {

private void printMsg(String methodName) {
System.out.println("Calling interface");
System.out.println("Interface method: " + methodName);
}

default void method1() {
printMsg("method1");
}

default void method2() {
printMsg("method2");
}
}

JDK10

从Java 9和Java 10开始,有几种用于Optional的有用方法。其中最有趣的两个是orElseThrow和ifPresentOrElse。如果没有值,则使用该orElseThrow方法抛出NoSuchElementException。否则,它返回一个值。

1
2
3
4
ini复制代码public Person getPersonById(Long id) {
Optional<Person> personOpt = repository.findById(id);
return personOpt.orElseThrow();
}

因此,您可以避免将带参数的if语句与isPresentmethod一起使用。

1
2
3
4
5
6
7
vbnet复制代码public Person getPersonByIdOldWay(Long id) {
Optional<Person> personOpt = repository.findById(id);
if (personOpt.isPresent())
return personOpt.get();
else
throw new NoSuchElementException();
}

第二种有趣的方法是ifPresentOrElse。如果存在一个值,它将使用该值执行给定的操作。否则,它将执行给定的基于空的操作。

1
2
3
4
5
6
7
csharp复制代码public void printPersonById(Long id) {
Optional<Person> personOpt = repository.findById(id);
personOpt.ifPresentOrElse(
System.out::println,
() -> System.out.println("Person not found")
);
}

在Java 8中,我们可以if-else直接与isPresent方法一起使用。

1
2
3
4
5
6
7
csharp复制代码public void printPersonByIdOldWay(Long id) {
Optional<Person> personOpt = repository.findById(id);
if (personOpt.isPresent())
System.out.println(personOpt.get());
else
System.out.println("Person not found");
}

JDK 10 && JDK 11

从Java 10开始,您可以声明没有其类型的局部变量。您只需要定义var关键字而不是类型。从Java 11开始,您还可以将其与lambda表达式一起使用,如下所示。

1
2
3
4
typescript复制代码public String sumOfString() {
BiFunction<String, String, String> func = (var x, var y) -> x + y;
return func.apply("abc", "efg");
}

JDK 12

使用Switch表达式,您可以定义多个case标签并使用箭头返回值。此功能自JDK 12起可用。它使Switch表达式真正更易于访问。

1
2
3
4
5
6
7
arduino复制代码  public String newMultiSwitch(int day) {
return switch (day) {
case 1, 2, 3, 4, 5 -> "workday";
case 6, 7 -> "weekend";
default -> "invalid";
};
}

对于低于12的Java,相同的示例要复杂得多。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
arduino复制代码public String oldMultiSwitch(int day) {
switch (day) {
case 1:
case 2:
case 3:
case 4:
case 5:
return "workday";
case 6:
case 7:
return "weekend";
default:
return "invalid";
}
}

JDK 13

文本块是多行字符串文字,它避免使用转义序列,并以可预测的方式自动设置字符串格式。它还使开发人员可以控制字符串的格式。从Java 13开始,文本块可用作预览功能。它们以三个双引号(”””)开头。让我们看看我们如何轻松地创建和格式化JSON消息。

1
2
3
4
5
6
7
8
typescript复制代码    public String getNewPrettyPrintJson() {
return """
{
"firstName": "Piotr",
"lastName": "Mińkowski"
}
""";
}

创建Java 13之前的相同JSON字符串要复杂得多。

1
2
3
4
5
6
swift复制代码   public String getOldPrettyPrintJson() {
return "{\n" +
" \"firstName\": \"Piotr\",\n" +
" \"lastName\": \"Mińkowski\"\n" +
"}";
}

JDK14

使用Records,您可以定义不可变的纯数据类(仅限getter)。它会自动创建toString,equals和hashCode方法。实际上,您只需要定义如下所示的字段即可。

1
arduino复制代码public record Person(String name, int age) {}

具有类似功能的类如record包含字段,构造函数,getter和实施toString,equals以及hashCode方法。

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
typescript复制代码public class PersonOld {

private final String name;
private final int age;

public PersonOld(String name, int age) {
this.name = name;
this.age = age;
}

public String getName() {
return name;
}

public int getAge() {
return age;
}

@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
PersonOld personOld = (PersonOld) o;
return age == personOld.age && name.equals(personOld.name);
}

@Override
public int hashCode() {
return Objects.hash(name, age);
}

@Override
public String toString() {
return "PersonOld{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}

}

JDK15

使用密封类功能,您可以限制超类的使用。使用new关键字,sealed您可以定义哪些其他类或接口可以扩展或实现当前类。

1
kotlin复制代码public abstract sealed class Pet permits Cat, Dog {}

允许的子类必须定义一个修饰符。如果您不想允许任何其他扩展名,则需要使用final关键字。

1
scala复制代码public final class Cat extends Pet {}

另一方面,您可以打开扩展类。在这种情况下,应使用non-sealed修饰符。

1
scala复制代码public non-sealed class Dog extends Pet {}

当然,下面的可见声明是不允许的。

1
scala复制代码public final class Tiger extends Pet {}

结束

🔍 微信公众号coding途中,扫码加入我们交流群!

点赞是认可,在看是支持

本文转载自: 掘金

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

不使用第三个变量,怎么交换两个变量的值? 才华横溢+玉树临风

发表于 2021-06-11

微信公众号:moon聊技术

关注选择“ 星标 ”, 重磅干货,第一 时间送达!

[如果你觉得文章对你有帮助,欢迎关注,在看,点赞,转发]


才华横溢+玉树临风 VS 腰缠万贯

大家好,我是1,我长的非常帅又很有才华

我有一个好兄弟,它叫8,这家伙豪气的不得了,是一个富二代

但是由于我的才华和英俊的外表,他还是认我做了大哥

我们都很羡慕对方,我想过有钱的日子,但是他想变成我这样有才华且帅的男人

总的来说,我想变成他,他想变成我!

于是,我们开始翻阅各种武学宝典,功夫不负有心人!终于让我找到了!—->偷天换日!!

《偷天换日》第一重异或大法

这本武功秘籍中居然有一式名曰异或大法,可使两人互换!!!!

激动的我立马就翻阅起来!

以下就是秘籍介绍了


先和大家简单普及下小知识,计算机的世界是二进制的,只有01这两个数字,

  • 1在计算机中可以用1标识
  • 5在计算机的世界里,其实是101

异或是什么?

就是相同为0,不同为1

我们举个例子,如图


1001 和 1100 的异或结果为 0101

看到这里,恭喜你!异或大法你已经有所小成了!

《偷天换日》第二重双修大法

双修????

咳咳咳,不要乱想,是需要两人同时修炼,要配合!把脑子里的脏东西踢掉!

但是双修大法需要一个人牺牲一个

我来吧!!!怎么能让兄弟吃亏!

第二重??只需要对兄弟使出第一式就好了,这么简单吗?


于是乎变我成了变态???

《偷天换日》第三重偷天换日

书中简介:你不会白牺牲的!现在需要第二个人来拯救你了!

快!对它使用异或大法!


兄弟:我变了我变了!!!!!我变成帅比了!!!哈哈哈哈!!!!!

我上去就是给他一拳:我还人不人鬼不鬼的,你笑个P,快看看怎么办!

兄弟露出了姨母笑:我看下我看下,哈哈哈哈…
书中说到,你对我再用下异或大法就好了!来吧!!!

我二话不说,又是一招异或大法打了过去


我哭了:我变成土豪了,哈哈哈哈哈哈..

恭喜你,神功大成!

代码演示

小说看完了,代码也得码一下

1
2
3
4
5
复制代码   int handsome = 1;
   int rich = 5;
   handsome = handsome ^ rich;
   rich = handsome ^ rich;
   handsome = rich ^ handsome;


看到这里,你神功大成了吗?

本文转载自: 掘金

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

【ShardingSphere】springboot整合sh

发表于 2021-06-11

上两篇文章讲到了两种分库分表的方式,这两种方式可以归结为一种类型,都是通过配置的形式来分片数据。本文我们继续讲解一种新的方式来分片数据,除了配置的形式外,shardingjdbc还支持通过代码来自定义规则。


自定义规则

之前我们实现了id取模和按日期分库分表,这里我们为了展示技术,还是继续按照日期分表,不过这里通过代码来自定义。在开始写代码之前,我们先将分库分表规则定义好。
这里我们建立两个库ds0,ds1。每个库建立表t_order2021、t_order2022两个表,语句如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
sql复制代码CREATE TABLE `t_order2021` (
`id` bigint(32) NOT NULL,
`user_id` int(11) DEFAULT NULL,
`order_id` int(11) DEFAULT NULL,
`cloumn` varchar(45) DEFAULT NULL,
`day_date` char(8) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE `t_order2022` (
`id` bigint(32) NOT NULL,
`user_id` int(11) DEFAULT NULL,
`order_id` int(11) DEFAULT NULL,
`cloumn` varchar(45) DEFAULT NULL,
`day_date` char(8) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

搭建工程

基本环境准备好了,我们就可以开始我们的工程搭建了。这里搭建一个springboot工程,然后整合mybatis和shardingjdbc。具体依赖如下:

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
xml复制代码<properties>
<java.version>1.8</java.version>
<sharding-sphere.version>4.1.1</sharding-sphere.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>

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

<dependency>
<groupId>org.apache.shardingsphere</groupId>
<artifactId>sharding-jdbc-spring-boot-starter</artifactId>
<version>${sharding-sphere.version}</version>
</dependency>

<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<scope>provided</scope>
</dependency>

<!-- https://mvnrepository.com/artifact/org.apache.commons/commons-lang3 -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.12.0</version>
</dependency>

<!-- https://mvnrepository.com/artifact/com.zaxxer/HikariCP -->
<dependency>
<groupId>com.zaxxer</groupId>
<artifactId>HikariCP</artifactId>
<version>4.0.3</version>
</dependency>

<!-- https://mvnrepository.com/artifact/mysql/mysql-connector-java -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.25</version>
</dependency>

<!-- https://mvnrepository.com/artifact/org.mybatis/mybatis -->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.1.4</version>
</dependency>
</dependencies>

上手sharding配置

添加mybatis和shardingjdbc的配置

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
properties复制代码server.port=10080

spring.shardingsphere.datasource.names=ds0,ds1

# 配置第一个数据库
spring.shardingsphere.datasource.ds0.type=com.zaxxer.hikari.HikariDataSource
spring.shardingsphere.datasource.ds0.driver-class-name=com.mysql.jdbc.Driver
spring.shardingsphere.datasource.ds0.jdbc-url=jdbc:mysql://localhost:3306/ds0
spring.shardingsphere.datasource.ds0.username=root
spring.shardingsphere.datasource.ds0.password=root

# 配置第二个数据库
spring.shardingsphere.datasource.ds1.type=com.zaxxer.hikari.HikariDataSource
spring.shardingsphere.datasource.ds1.driver-class-name=com.mysql.jdbc.Driver
spring.shardingsphere.datasource.ds1.jdbc-url=jdbc:mysql://localhost:3306/ds1
spring.shardingsphere.datasource.ds1.username=root
spring.shardingsphere.datasource.ds1.password=root

# 配置t_order表的分库策略
spring.shardingsphere.sharding.tables.t_order.database-strategy.standard.sharding-column=id
# 自定义分库策略
spring.shardingsphere.sharding.tables.t_order.database-strategy.standard.precise-algorithm-class-name=com.example.test.config.MyDbPreciseShardingAlgorithm

# 配置t_order的分表策略
spring.shardingsphere.sharding.tables.t_order.actual-data-nodes=ds$->{0..1}.t_order$->{2021..2022}
spring.shardingsphere.sharding.tables.t_order.table-strategy.inline.sharding-column=day_date
# 自定义分表策略
spring.shardingsphere.sharding.tables.t_order.table-strategy.standard.precise-algorithm-class-name=com.example.test.config.MyTablePreciseShardingAlgorithm

# 添加t_order表的id生成策略
spring.shardingsphere.sharding.tables.t_order.key-generator.column=id
spring.shardingsphere.sharding.tables.t_order.key-generator.type=SNOWFLAKE

# 打开sql输出日志
spring.shardingsphere.props.sql.show=true

# mybatis配置
mybatis.mapper-locations=classpath:mapping/*.xml
mybatis.type-aliases-package=com.example.test.po

# 配置日志级别
logging.level.com.echo.shardingjdbc.dao=DEBUG

启动类上添加mybatis的mapper扫描配置@MapperScan(“com.example.test.dao”)

在以上配置中,我们定义了自定义配置的类路径,接下来我们会去编写这两个自定义配置的内容。

编写自定义规则类

在文章开头我们就已经定义了规则,现在我们来实现这个规则。根据我们的规则我们可以选择精确分片算法来实现,具体代码如下:

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复制代码package com.example.test.config;

import lombok.extern.slf4j.Slf4j;
import org.apache.shardingsphere.api.sharding.standard.PreciseShardingAlgorithm;
import org.apache.shardingsphere.api.sharding.standard.PreciseShardingValue;

import java.util.Collection;

/**
* 自定义分库规则类
* @author echo
* @date 2021/6/10 0010 上午 10:09
*/
@Slf4j
public class MyDbPreciseShardingAlgorithm implements PreciseShardingAlgorithm<Long> {

/**
* 分片策略
*
* @param availableTargetNames 所有的数据源
* @param shardingValue SQL执行时传入的分片值
* @return 返回
*/
@Override
public String doSharding(Collection<String> availableTargetNames, PreciseShardingValue<Long> shardingValue) {
// 真实节点
availableTargetNames.forEach(item -> log.info("actual node db:{}", item));

log.info("logic table name:{},rout column:{}", shardingValue.getLogicTableName(), shardingValue.getColumnName());

//精确分片
log.info("column value:{}", shardingValue.getValue());

for (String each : availableTargetNames) {
Long value = shardingValue.getValue();
if (("ds" + value % 2).equals(each)) {
return each;
}
}

return null;
}

}
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
java复制代码package com.example.test.config;

import lombok.extern.slf4j.Slf4j;
import org.apache.shardingsphere.api.sharding.standard.PreciseShardingAlgorithm;
import org.apache.shardingsphere.api.sharding.standard.PreciseShardingValue;

import java.util.Collection;

/**
* 自定义分表规则类
*
* @author echo
* @date 2021/6/10 0010 上午 10:09
*/
@Slf4j
public class MyTablePreciseShardingAlgorithm implements PreciseShardingAlgorithm<String> {

@Override
public String doSharding(Collection<String> availableTargetNames, PreciseShardingValue<String> shardingValue) {
// 真实节点
availableTargetNames.forEach(item -> log.info("actual node table:{}", item));

log.info("logic table name:{},rout column:{}", shardingValue.getLogicTableName(), shardingValue.getColumnName());

// 精确分片
log.info("column value:{}", shardingValue.getValue());

for (String each : availableTargetNames) {
if (("t_order" + shardingValue.getValue()).equals(each)) return each;
}

return null;
}

}

上测试代码

按照之前文章的套路,我们写点测试代码,代码如下:

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

import com.example.test.po.TOrder;
import com.example.test.service.TOrderService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

import java.util.List;

/**
* @author echo
* @date 2021/6/3 0003 下午 16:37
*/
@RestController
@RequestMapping("/order")
public class TOrderController {

@Autowired
private TOrderService tOrderService;

@PostMapping("/save")
public String save(@RequestBody TOrder tOrder) {
tOrderService.save(tOrder);
return "success";
}

@PostMapping("/delete")
public String delete(@RequestParam(value = "id") Long id) {
tOrderService.delete(id);
return "success";
}

@PostMapping("/update")
public int update(@RequestBody TOrder tOrder) {
return tOrderService.update(tOrder);
}

@GetMapping("/getList")
public List<TOrder> getList() {
return tOrderService.getList();
}

}

public interface TOrderService {
void save(TOrder tOrder);

void delete(Long id);

int update(TOrder tOrder);

List<TOrder> getList();
}

@Service
public class TOrderServiceImpl implements TOrderService {

@Autowired
private TOrderDao tOrderDao;

@Override
public void save(TOrder tOrder) {
tOrderDao.insert(tOrder);
}

@Override
public void delete(Long id) {
tOrderDao.delete(id);
}

@Override
public int update(TOrder tOrder) {
return tOrderDao.update(tOrder);
}

@Override
public List<TOrder> getList() {
return tOrderDao.getList();
}

}

public interface TOrderDao {

void insert(TOrder tOrder);

List<TOrder> getList();

void delete(Long id);

int update(TOrder tOrder);
}

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.example.test.dao.TOrderDao">
<resultMap id="BaseResultMap" type="com.example.test.po.TOrder">
<id column="id" jdbcType="BIGINT" property="id"/>
<result column="user_id" jdbcType="INTEGER" property="userId"/>
<result column="order_id" jdbcType="INTEGER" property="orderId"/>
<result column="cloumn" jdbcType="VARCHAR" property="cloumn"/>
<result column="day_date" jdbcType="CHAR" property="dayDate"/>
</resultMap>
<sql id="Base_Column_List">
id, user_id, order_id, cloumn, day_date
</sql>

<insert id="insert" parameterType="com.example.test.po.TOrder">
insert into t_order (user_id, order_id, cloumn, day_date) value (#{userId}, #{orderId}, #{cloumn}, #{dayDate})
</insert>

<select id="getList" resultMap="BaseResultMap">
select
<include refid="Base_Column_List"/>
from t_order
</select>

<delete id="delete" parameterType="java.lang.Long">
delete from t_order
where id = #{id,jdbcType=BIGINT}
</delete>

<update id="update" parameterType="com.example.test.po.TOrder">
update t_order
set
cloumn = #{cloumn,jdbcType=VARCHAR},
order_id = #{orderId,jdbcType=INTEGER},
user_id = #{userId,jdbcType=INTEGER}
where id = #{id,jdbcType=BIGINT}
</update>
</mapper>

完成之后我们可以测试一下

调用接口http://localhost:3306/order/save,我们会发现,我们的数据根据我们既定的规则进入了相应的表
在这里插入图片描述

总结

  • 在配置的时候,版本问题会对配置造成一定的影响,所以如果配置相应内容的话, 要注意版本信息对应的官网配置规则
  • 不同规则对应的配置规则不一,比如这里用的精确分片算法,需要找到对应的精确分片算法的配置内容,不然不会生效

本文转载自: 掘金

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

面试官:请说下volatile的实现原理

发表于 2021-06-11

volatile是面试中的高频问题,这篇文章能带你详细了解volatile的面试相关问题

推荐阅读:

  • MySQL数据库高频面试题
  • 计算机网络高频面试题最新版
  • Java集合高频面试题最新版
  • 并发编程高频面试题
  • 面试官:请用五种方法实现多线程交替打印问题
  • 深度剖析synchronized实现原理

文章目录

image.png

volatile的作用是什么?

volatile是一个轻量级的synchronized,一般作用与变量,在多处理器开发的过程中保证了内存的可见性。相比于synchronized关键字,volatile关键字的执行成本更低,效率更高。

volatile的特性有哪些?

并发编程的三大特性为可见性、有序性和原子性。通常来讲volatile可以保证可见性和有序性。

  • 可见性:volatile可以保证不同线程对共享变量进行操作时的可见性。即当一个线程修改了共享变量时,另一个线程可以读取到共享变量被修改后的值。
  • 有序性:volatile会通过禁止指令重排序进而保证有序性。
  • 原子性:对于单个的volatile修饰的变量的读写是可以保证原子性的,但对于i++这种复合操作并不能保证原子性。这句话的意思基本上就是说volatile不具备原子性了。

Java内存的可见性问题

Java的内存模型如下图所示。

在这里插入图片描述

这里的本地内存并不是真实存在的,只是Java内存模型的一个抽象概念,它包含了控制器、运算器、缓存等。同时Java内存模型规定,线程对共享变量的操作必须在自己的本地内存中进行,不能直接在主内存中操作共享变量。这种内存模型会出现什么问题呢?,

  1. 线程A获取到共享变量X的值,此时本地内存A中没有X的值,所以加载主内存中的X值并缓存到本地内存A中,线程A修改X的值为1,并将X的值刷新到主内存中,这时主内存及本地内存A中的X的值都为1。
  2. 线程B需要获取共享变量X的值,此时本地内存B中没有X的值,加载主内存中的X值并缓存到本地内存B中,此时X的值为1。线程B修改X的值为2,并刷新到主内存中,此时主内存及本地内存B中的X值为2,本地内存A中的X值为1。
  3. 线程A再次获取共享变量X的值,此时本地内存中存在X的值,所以直接从本地内存中A获取到了X为1的值,但此时主内存中X的值为2,到此出现了所谓内存不可见的问题。

该问题Java内存模型是通过synchronized关键字和volatile关键字就可以解决。

为什么代码会重排序?

计算机在执行程序的过程中,编译器和处理器通常会对指令进行重排序,这样做的目的是为了提高性能。具体可以看下面这个例子。

1
2
3
4
5
6
7
java复制代码int a = 1;
int b = 2;
int a1 = a;
int b1 = b;
int a2 = a + a;
int b2 = b + b;
......

像这段代码,不断地交替读取a和b,会导致寄存器频繁交替存储a和b,使得代码性能下降,可对其进入如下重排序。

1
2
3
4
5
6
7
java复制代码int a = 1;
int b = 2;
int a1 = a;
int a2 = a + a;
int b1 = b;
int b2 = b + b;
......

按照这样地顺序执行代码便可以避免交替读取a和b,这就是重排序地意义。

指令重排序一般分为编译器优化重排、指令并行重拍和内存系统重排三种。

  • 编译器优化重排:编译器在不改变单线程程序语义的情况下,可以对语句的执行顺序进行重新排序。
  • 指令并行重排:现代处理器多采用指令级并行技术来将多条指令重叠执行。对于不存在数据依赖的程序,处理器可以对机器指令的执行顺序进行重新排列。
  • 内存系统重排:因为处理器使用缓存和读/写缓冲区,使得加载(load)和存储(store)看上去像是在乱序执行。

注:简单解释下数据依赖性:如果两个操作访问了同一个变量,并且这两个操作有一个是写操作,这两个操作之间就会存在数据依赖性,例如:

1
2
java复制代码a = 1;
b = a;

如果对这两个操作的执行顺序进行重排序的话,那么结果就会出现问题。

其实,这三种指令重排说明了一个问题,就是指令重排在单线程下可以提高代码的性能,但在多线程下可以会出现一些问题。

重排序会引发什么问题?

前面已经说过了,在单线程程序中,重排序并不会影响程序的运行结果,而在多线程场景下就不一定了。可以看下面这个经典的例子,该示例出自《Java并发编程的艺术》。

1
2
3
4
5
6
7
8
9
10
11
12
13
java复制代码class ReorderExample{
int a = 0;
boolean flag = false;
public void writer(){
a = 1; // 操作1
flag = true; // 操作2
}
public void reader(){
if(flag){ // 操作3
int i = a + a; // 操作4
}
}
}

假设线程1先执行writer()方法,随后线程2执行reader()方法,最后程序一定会得到正确的结果吗?

答案是不一定的,如果代码按照下图的执行顺序执行代码则会出现问题。

在这里插入图片描述

操作1和操作2进行了重排序,线程1先执行flag=true,然后线程2执行操作3和操作4,线程2执行操作4时不能正确读取到a的值,导致最终程序运行结果出问题。这也说明了在多线程代码中,重排序会破坏多线程程序的语义。

as-if-serial规则和happens-before规则的区别?

区别:

  • as-if-serial定义:无论编译器和处理器如何进行重排序,单线程程序的执行结果不会改变。
  • happens-before定义:一个操作happens-before另一个操作,表示第一个的操作结果对第二个操作可见,并且第一个操作的执行顺序也在第二个操作之前。但这并不意味着Java虚拟机必须按照这个顺序来执行程序。如果重排序的后的执行结果与按happens-before关系执行的结果一致,Java虚拟机也会允许重排序的发生。
  • happens-before关系保证了同步的多线程程序的执行结果不被改变,as-if-serial保证了单线程内程序的执行结果不被改变。

相同点:happens-before和as-if-serial的作用都是在不改变程序执行结果的前提下,提高程序执行的并行度。

voliatile的实现原理?

前面已经讲述volatile具备可见性和有序性两大特性,所以volatile的实现原理也是围绕如何实现可见性和有序性展开的。

volatile实现内存可见性原理

导致内存不可见的主要原因就是Java内存模型中的本地内存和主内存之间的值不一致所导致,例如上面所说线程A访问自己本地内存A的X值时,但此时主内存的X值已经被线程B所修改,所以线程A所访问到的值是一个脏数据。那如何解决这种问题呢?

volatile可以保证内存可见性的关键是volatile的读/写实现了缓存一致性,缓存一致性的主要内容为:

  • 每个处理器会通过嗅探总线上的数据来查看自己的数据是否过期,一旦处理器发现自己缓存对应的内存地址被修改,就会将当前处理器的缓存设为无效状态。此时,如果处理器需要获取这个数据需重新从主内存将其读取到本地内存。
  • 当处理器写数据时,如果发现操作的是共享变量,会通知其他处理器将该变量的缓存设为无效状态。

那缓存一致性是如何实现的呢?可以发现通过volatile修饰的变量,生成汇编指令时会比普通的变量多出一个Lock指令,这个Lock指令就是volatile关键字可以保证内存可见性的关键,它主要有两个作用:

  • 将当前处理器缓存的数据刷新到主内存。
  • 刷新到主内存时会使得其他处理器缓存的该内存地址的数据无效。

volatile实现有序性原理

前面提到重排序可以提高代码的执行效率,但在多线程程序中可以导致程序的运行结果不正确,那volatile是如何解决这一问题的呢?

为了实现volatile的内存语义,编译器在生成字节码时会通过插入内存屏障来禁止指令重排序。

内存屏障:内存屏障是一种CPU指令,它的作用是对该指令前和指令后的一些操作产生一定的约束,保证一些操作按顺序执行。

Java虚拟机插入内存屏障的策略

Java内存模型把内存屏障分为4类,如下表所示:

屏障类型 指令示例 说明
LoadLoad Barriers Load1;LoadLoad;Load2 保证Load1数据的读取先于Load2及后续所有读取指令的执行
StoreStore Barriers Store1;StoreStore;Store2 保证Store1数据刷新到主内存先于Store2及后续所有存储指令
LoadStore Barriers Load1;LoadStore;Store2 保证Load1数据的读取先于Store2及后续的所有存储指令刷新到主内存
StoreLoad Barriers Store1;StoreLoad;Load2 保证Store1数据刷新到主内存先于Load2及后续所有读取指令的执行

注:StoreLoad Barriers同时具备其他三个屏障的作用,它会使得该屏障之前的所有内存访问指令完成之后,才会执行该屏障之后的内存访问命令。

Java内存模型对编译器指定的volatile重排序规则为:

  • 当第一个操作是volatile读时,无论第二个操作是什么都不能进行重排序。
  • 当第二个操作是volatile写时,无论第一个操作是什么都不能进行重排序。
  • 当第一个操作是volatile写,第二个操作为volatile读时,不能进行重排序。

根据volatile重排序规则,Java内存模型采取的是保守的屏障插入策略,volatile写是在前面和后面分别插入内存屏障,volatile读是在后面插入两个内存屏障,具体如下:

  • volatile读:在每个volatile读后面分别插入LoadLoad屏障及LoadStore屏障(根据volatile重排序规则第一条),如下图所示

在这里插入图片描述

​ LoadLoad屏障的作用:禁止上面的所有普通读操作和上面的volatile读操作进行重排序。

​ LoadStore屏障的作用:禁止下面的普通写和上面的volatile读进行重排序。

  • volatile写:在每个volatile写前面插入一个StoreStore屏障(为满足volatile重排序规则第二条),在每个volatile写后面插入一个StoreLoad屏障(为满足volatile重排序规则第三条),如下图所示

在这里插入图片描述

StoreStore屏障的作用:禁止上面的普通写和下面的volatile写重排序

StoreLoad屏障的作用:防止上面的volatile写与下面可能出现的volatile读/写重排序。

编译器对内存屏障插入策略的优化

因为Java内存模型所采用的屏障插入策略比较保守,所以在实际的执行过程中,只要不改变volatile读/写的内存语义,编译器通常会省略一些不必要的内存屏障。

代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
java复制代码public class VolatileBarrierDemo{
int a;
volatile int b = 1;
volatile int c = 2;

public void test(){
int i = b; //volatile读
int j = c; //volatile读
a = i + j; //普通写

}
}

指令序列示意图如下:

在这里插入图片描述

从上图可以看出,通过指令优化一共省略了两个内存屏障(虚线表示),省略第一个内存屏障LoadStore的原因是最后的普通写不可能越过第二个volatile读,省略第二个内存屏障LoadLoad的原因是下面没有涉及到普通读的操作。

volatile能使一个非原子操作变成一个原子操作吗?

volatile只能保证可见性和有序性,但可以保证64位的long型和double型变量的原子性。

对于32位的虚拟机来说,每次原子读写都是32位的,会将long和double型变量拆分成两个32位的操作来执行,这样long和double型变量的读写就不能保证原子性了,而通过volatile修饰的long和double型变量则可以保证其原子性。

volatile、synchronized的区别?

  • volatile主要是保证内存的可见性,即变量在寄存器中的内存是不确定的,需要从主存中读取。synchronized主要是解决多个线程访问资源的同步性。
  • volatile作用于变量,synchronized作用于代码块或者方法。
  • volatile仅可以保证数据的可见性,不能保证数据的原子性。synchronized可以保证数据的可见性和原子性。
  • volatile不会造成线程的阻塞,synchronized会造成线程的阻塞。

微信搜索公众号路人zhang,回复面试手册,领取更多高频面试题PDF版及更多面试资料。

本文转载自: 掘金

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

我不服!这开源项目居然才888个星!?

发表于 2021-06-11

你好呀,我是why。

是的,我又发现了一个宝藏,又迫不及待的想分享给大家。

这个宝藏是一个开源项目,或者叫做一本开源的书。

让我意难平的是,这本写的如此具有学习潜力和指导意义的开源书,目前才 887 个 Star。

赶得早不如赶得巧,我就是第 888 个点 Star 的人,听起来就很有缘分的样子:

https://icyfenix.cn/


先看一下我看了之后整理的思维导图吧:


看不清没关系,文末会给你领取链接的。

看完之后我个人的一个感受是:

关于分布式架构方向,写的很多很全。而且都是笔者一路走过来的经验之谈,浓缩在了文章里面。

对的起首页的这一句口号:构建可靠的大型分布式系统。

谁写的?

那么这个开源书的作者是谁呢?

周志明。

是的,就是你想到的那个写了 JVM 的书的男人。


其实你仔细看周佬写的自我介绍,很有小细节。

程序员、研究员、作者、布道师这四个职业,排在第一的是最没有噱头的“程序员”一职。

而在程序员里面,给自己的描述是:

一名兼职一些管理与研究工作的程序员。

其实关于这个点,我看过周佬的一些公开场合的自我介绍,都是说自己是一个“兼职一些管理工作的程序员”,给自己这样的人设标签。

他在自己写的《程序员之路》一文中解释过这个标签。

他想要透过这个标签表达的是对于一枚程序员,以后是想要发展为一个架构师还是研发管理者,都不要轻易地离开技术领域的一线前沿,离开技术、放弃编码的决定很可能会像你高考之后放下的数学、生物、地理等知识那样,一旦放手,毕生就很难有机会再重新捡起来。

当你放下代码的时间越长,久而久之,你对代码、技术、产品状态与团队研发状态的理解,渐渐的会和团队成员产生了偏差错位,丧失了细节上给予指导的能力,丧失了专业问题上提出接地气解决方案的能力,只能在无法短期难以校验对错的大战略方向提意见。

在会议、流程及团队管理措施上下功夫,在职业经理人式的宣讲与汇报上寻找存在感。

此刻,你便从团队的导师变成了管理者,最终你与团队的关系,从携手并肩奋斗的伙伴,完全演变成只能靠公司制度与管理职位的权力来维系雇佣关系。

我能理解周佬说的现象,其实是一个非常普遍的现象。甚至有的朋友走上管理岗位的目标就是不再写代码了,基于当前的市场和行业现状,这样的选择是无可厚非的。

只是,有没有那么一点点可能,不要完全抛弃代码。这也需要是你和团队之间的最行之有效的纽带。

就像《代码整洁之道》一书中说的:

软件架构师本身就是最好的程序员,他们会一直编写代码,虽然可能不会像其他程序员输出的代码量那样多,但是只有持续地编程,才能确保他们遇见其他程序员所面对的问题,体会其他程序员心中的感受,因此如果不编程,他们亦将无法胜任软件架构这项工作。

这本《凤凰架构》,周佬自己对它的定位是这样的:


给开发人员整理的关于软件架构方面的技能地图,同时系统的梳理自己的知识,并配备了对应技术方案的演示程序。

真的是一件利人利己的事情。

我本来的想法是先带着大家囫囵吞枣的走一圈这一本书,主要起到一个介绍的作用。

但是越写越不得劲的感觉。因为即使我写的这么卖力认真,都没有体现出这本书的价值的千分之一。

你得自己去读,你才知道我没有骗你:

这真的是个宝藏啊!

所以我决定换个思路,告诉大家这里面有什么就行了,其实就是书中的探索起步一小节。

探索起步


这是探索起步的更新日志部分,可以看到周佬对于该项目一直在进行维护新内容:


而对于已有的内容,其中的错别字、不通顺的地方、含义不清的地方,他也在抽时间修改,最近的一次修改就是 6 月 6 日,昨天,上周日:


所以,相比于其他的大部分网上的文章来说,会更加实时、系统、优质一点。

全书分为五大部分和两个篇外,而为了让你快速定位到合适自己的部分,周佬也细心的介绍了每一部分对应的读者类型。

  • 引导篇 探索起步:这部分面向于准备对文档介绍的内容亲身实践的探索者。
  • 第一部分 演进中的架构:这部分适合所有开发者,但尤其推荐刚刚从单体架构向微服务架构转型的开发者去阅读。
  • 第二部分 架构师的视角:这部分讨论与风格无关的架构知识,适合所有技术架构师、系统设计、开发人员。
  • 第三部分 分布式的基石:这部分面向于使用分布式架构的开发人员。
  • 第四部分 不可变基础设施:这部分面向于基础设施运维人员、技术平台的开发者。
  • 第五部分 技术方法论:这部分面向于在企业中能对重要技术决策进行拍板的决策者。
  • 篇外 随笔文章:这部分无特定读者对象,内容是笔者日常文章的整理。
  • 篇外 附录:这部分面向刚开始接触云原生环境的设计者、开发者。

其中第一部分的我读完之后做的思维导图如下:


可以大家对于其中的后微服务时代和无服务时代稍微有点陌生,但是我换个英文名称,大家就应该是非常熟悉了。

后微服务时代其实就是云原生时代,Cloud Native。

而无服务其实就是 Serverless。

但是需要注意的是,周佬把 Serverless 排在了 Cloud Native 之后,其实它们两者并没有继承替代关系。不要因为周老对于两者的书写顺序产生了“无服务就会比微服务更加先进”的错误想法。

周佬对于这两者之间的关系描述是这样的:

  • 如果说微服务架构是分布式系统这条路当前所能做到的极致,那无服务架构,也许就是“不分布式”的云端系统这条路的起点。

第二部分的思维导图如下:


这一部分主要聊了我们做分布式服务时,一定会涉及到的问题,比如:远程服务调用(RPC)、分布式事务的处理、多级分流、架构安全。

我个人认为这一部分是干货满满的。

其中访问远程服务,对 RPC 和 REST 从各自的起源开始进行了一个详尽的描述:


事务处理小节,大家可以看看“共享事务”的概念,其实我发现有一部分号称是微服务架构的项目,走向了“共享事务”的路线。

我认为这是一种伪分布式。

透明多级分流系统从客户端到网络在到服务端的拆析,这是一种上帝视角的描述,而这一部分的主题就是“架构师的视角”。

架构师就应该是从这样的一个比较统筹规划的角度去看待系统,不必进入到具体系统的细枝末节中去:


第三部分分布式的基石:


共识算法、服务发现、流量治理、网络通信、监控预警共同构成了分布式的基石。

可以说如果是一个分布式的服务,都能找到上面的这些关键词的影子。

有些是应用系统自己做的。

有些是开源框架就帮你搞定了,你甚至不知道它们的存在。

但是上面的诸如流量治理和监控预警(可观察性)不是一个分布式服务一开始搭建时所必须的。

大多数情况下,刚刚搭建好的分布式都处于一个蛮荒状态。

随着时间推进和业务的发展,会慢慢补充上流量治理和监控预警。

也就是说如果想要分布式服务发展的监控可控,那么这些东西都是应该有的。

“基石”一词,像是手术刀一样精准。

来到了第四部分,不可变基础设施:


到这里我们就要从微服务走向云原生了。

在这一章,周佬以容器、编排系统和服务网格的发展为主线,介绍虚拟化容器与服务网格是如何模糊掉软件与硬件之间的界限,如何在基础设施与通讯层面上帮助微服务隐藏复杂性,解决原本只能由程序员通过软件编程来解决的分布式问题。

接下来的“技术方法论”属于微服务避坑指南,从目的、前提、边界、治理四个角度去阐述如何更好的使用微服务。

最后一部分是“随笔文章”:


其中的《云原生时代,Java 的危与机》和《程序员之路》这两篇文章,建议大家反复观看。

前者是技术方向的,后者是软技能方向的。

读完《Java 的危与机》,我感受到的是一场关于 Java 的自我革命已经悄然开始了。

Java 并不是一个优秀的开发语言,这一点我是非常承认且确定的。但是 Java 有一个庞大的用户群体和异常丰富的生态,这是它的护城河。所以短时间内还倒不下来。

但是大风起于青萍之末。风雨欲来,而包括我在内的很多人都浑然不知。

在文章里面,周佬有这样的一段话:

Java 支持提前编译最大的困难在于它是一门动态链接的语言,它假设程序的代码空间是开放的(Open World),允许在程序的任何时候通过类加载器去加载新的类,作为程序的一部分运行。要进行提前编译,就必须放弃这部分动态性,假设程序的代码空间是封闭的(Closed World),所有要运行的代码都必须在编译期全部可知。这一点不仅仅影响到了类加载器的正常运作,除了无法再动态加载外,反射(通过反射可以调用在编译期不可知的方法)、动态代理、字节码生成库(如 CGLib)等一切会运行时产生新代码的功能都不再可用,如果将这些基础能力直接抽离掉,Helloworld 还是能跑起来,但 Spring 肯定跑不起来,Hibernate 也跑不起来,大部分的生产力工具都跑不起来,整个 Java 生态中绝大多数上层建筑都会轰然崩塌。

“整个 Java 生态中绝大多数上层建筑都会轰然崩塌。”

所以,Java 的这次变革属于要釜底抽薪。

读完《Java 的危与机》之后,你再去看《Graal VM》一文,你就明白了:为什么说 Graal VM 的成功与否,与 Java 的前途命运息息相关。

而这场变革已然悄悄开始,比如说一个小点:

大多数运行期对字节码的生成和修改操作,在 Graal VM 看来都是无法接受的。

但是比如 CGLIB 就是通过运行时产生字节码(生成代理类的子类)来做动态代理的。

这是目前的主流形式。

现在因为Graal VM 支持不了,所以必须由和框架一起来共同解决。

因此自 Spring Framework 5.2 起,@Configuration 注解中加入了一个新的 proxyBeanMethods 参数,设置为 false 则可避免 Spring 对与非接口类型的 Bean 进行代理。

同样地,对应在 Spring Boot 2.2 中,@SpringBootApplication 注解也增加了 proxyBeanMethods 参数,通常采用 Graal VM 去构建的 Spring Boot 本地应用都需要设置该参数。

我最喜欢的还是技术演示工程部分,直接把项目 Demo 都给你准备好了,开箱即用:


而且周佬写的这个开源项目有个特点,引用的部分会给出具体的官方的地址。严谨又权威,比如写到项目中用到的技术组件的时候:


一个问答


在项目里面,我还发现了一个问答。

问题和回答都非常的好,搬运过来给大家看看。

评论区:https://icyfenix.cn/methodology/forward-msa/prerequest.html

问题如下:

周大哥,看到了您说的马太效应。再联想到之前您讲的软件涅槃,而完善的微服务体系允许服务有涅槃的过程,有强大的容错能力。微服务发展又如此迅猛,觉得马太效应真的不远。

我不禁对最需要掌握的技能进行了思考,并产生了更强的焦虑感。

我是一名有七年工作经验的java开发工程师,28岁,目前在一家北京的传统信息软件技术公司,工资相对计算机行业偏低。

局限在java语言来说,jvm调优与并发编程等比较高阶的能力,是不是就很不关键了?

jvm我读了您写的《深入理解Java虚拟机:JVM高级特性与最佳实践》的第二版与第三版,由于工作中鲜有机会实践,只停留在一些理论理解,而缺失实践,理论知识也会淡忘。

并发编程读过《Java并发编程实战》,对并发编程有些了解,也有一些实践,一般水平。

微服务公司并没有用起来,实践经验也缺少。远程调用、分布式事务、注册中心、配置中心、熔断、限流等知识,通过看视频跟您的这个文档有一些了解。

java基础知识,经过这些年的磨练,是挺扎实了,spring能熟练使用。

常用设计模式有了解,也理解的比较到位。

我不想沦为螺丝钉。

我应该提升自己的哪些能力呢?

这些年只是做到了胜任分配给自己的工作。

现在发现自己缺少前瞻性思考,缺少对自己职业生涯的把控。

我现在想把握自己的职业生涯,请周大哥给一些指导。

我会通过招聘市场去挖掘市场需求,做整理,进行思考。

但是迫不及待的想跟您述说一下,请您不吝赐教,希望我的请求不是很唐突。

这个问题其实是很具有普遍性的:学了没地方实践,慢慢就忘记了。理论学了一大堆,聊起来可以谈笑风生,但是就是没有实际使用过。自己就是一颗螺丝钉。

周佬的回复如下:

写这文章不是为了贩卖焦虑,我也没有能力指导别人的职业生涯,但针对“应该提升自己的哪些能力”这类问题,我以前被问过很多次,这里可以重复一下。

我的建议就两个:

  • 不要轻视不直接产生实践价值的知识;
  • 不要对陷入已经被你熟练掌握的技术中不能自拔。

为了便于你理解,我做一个很土的比喻,把程序员提升自己类比成武侠中的练功,软件中的技能其实有很明显的“内力”和“武功”之分,譬如你提到的Java虚拟机,这类知识不仅是你在工作中鲜有机会实践,我也是差不多的。

大学计算机课程中,以“原理”二字结尾的课程,譬如计算机原理、操作系统原理、编译原理、数据库原理,等等,对绝大多数人而言,都不太会去设计处理器逻辑电路、设计程序语言和编译器,开发操作系统内核。

这些都有很经典的书:编译原理的龙书,计算机体系结构和程序运行的CSAPP,分布式与数据库原理的DDIA、操作系统原理的MOS,等等。这些书系统严谨全面,但可读性并不优秀,在B站/Coursera刷这些书作者们的公开课翻译视频也许是更好的方法。

这些技能能够辅助你去思考和分析问题,但是很难直接为你解决生产中的问题,以实践价值,就是以工作中是否有机会用到来衡量它们的作用是不合适的。

但这些课程之所以会是必修,是因为学习它们,能够为一名程序员的知识框架构筑好基础。

这话听起来很教条,可是当你一旦建立了相对完备稳固的知识框架,发现遇到的新知识、新技术,能够很自然地安放在已有知识体系的某个位置上,能够清楚感知到语言、技术、框架的设计意图和目标,甚至能共鸣到设计者当时所想,就会产生一种理所当然的感觉。

这样你接受新知识的认知负荷就会比别人更低,掌握起来更快速,理解起来更深刻。

我在这文档开篇中所说的,写这部文档是以整理自己的知识框架为目的,并非场面话,这点的确就是程序员如何学习新知识的关键,在知识快速迭代IT业界,这也是决定一名程序员能力上限有多高的根本因素。

相对的,那些具体的、用来解决生产中问题的技术和方法,譬如你提到的Spring、设计模式,我将其类比为“武功”。

这当然也是重要的,只有内力没有武功无法行走江湖,空有一身理论,但写不出代码来(包括那些只定大方向的架构师、设计者),我认为不肯定是合格的程序员,也很难指望能成为一名出色的技术领导(难以服众)。

但是具体的“武功”应该是能够快速捡起的,也能快速“忘掉”的,就是避免将一件事情做熟了,就一直陷在这件事情里面,避免拿到一把好的锤子,就看着一切问题都像是钉子。

很多程序员都抱怨,自己是CRUD Boy,自己在业务逻辑中打滚,没有机会接触底层的或者前瞻性的技术,所以自己技术难以提升。

这里当然有客观原因的存在,但往往也是受到了主观原因放大。

程序员其实与旧时代的手工技艺者差不多,骨子里就有天生的技术崇拜,你写的代码比别人的优雅健壮高性能,你杀BUG比别人快速干净利索,就会受到大家的认可。

很自然地,更多偏向技术偏向深层次的工作就会落到你这里,至少你会有话语权,有选择做哪些事情的权利,是否要一直在围绕着你最熟悉的业务去打滚是由你决定的。

学习武艺成为“武林高手”,是成为大BOSS之后才不必长期面对虾兵蟹将的纠缠。

学习一门具体的技术,也是为了用它解决好问题,然后把它忘掉,去掌握那些更深层次的、更前沿,而且自己还不会的技术。

最后还有个追问和回答如下:


不知道大家看到这个回答后的感受是怎么样的。

至少对我而言,振聋发聩。特别是这两点:

  • 不要轻视不直接产生实践价值的知识;
  • 不要对陷入已经被你熟练掌握的技术中不能自拔。

已经放入手机标签中,时常提醒自己。

与君共勉。

本文转载自: 掘金

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

1…645646647…956

开发者博客

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