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

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


  • 首页

  • 归档

  • 搜索

Spring Cloud GateWay 解决跨域问题并兼容

发表于 2021-06-02

Spring Cloud GateWay 解决跨域问题并兼容IE & 重复 Request Headers处理方法

一、Spring Cloud GateWay解决跨域问题并兼容IE

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
js复制代码@Configuration
public class GlobalCorsConfig {
private static final String MAX_AGE = "86400L";

@Bean
public WebFilter corsFilter() {

return (ServerWebExchange ctx, WebFilterChain chain) -> {
ServerHttpRequest request = ctx.getRequest();
if (CorsUtils.isCorsRequest(request)) {
HttpHeaders requestHeaders = request.getHeaders();
ServerHttpResponse response = ctx.getResponse();
HttpMethod requestMethod = requestHeaders.getAccessControlRequestMethod();
HttpHeaders headers = response.getHeaders();

//允许所有域名进行跨域调用
headers.add(HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN, requestHeaders.getOrigin());
if (requestMethod != null) {//适配IE
//放行全部原始头信息
headers.add(HttpHeaders.ACCESS_CONTROL_ALLOW_HEADERS, requestHeaders.getAccessControlRequestHeaders().toString().replace("[", "").replace("]", ""));
//允许所有请求方法跨域调用
headers.add(HttpHeaders.ACCESS_CONTROL_ALLOW_METHODS, requestMethod.name());
}
//允许跨域发送cookie
headers.add(HttpHeaders.ACCESS_CONTROL_ALLOW_CREDENTIALS, "true");
//获取除[Cache-Control,Content-Language,Content-Type,Expires,Last-Modified,Pragma]其他全部字段
headers.add(HttpHeaders.ACCESS_CONTROL_EXPOSE_HEADERS, "*");
//请求有效期
headers.add(HttpHeaders.ACCESS_CONTROL_MAX_AGE, MAX_AGE);

if (request.getMethod() == HttpMethod.OPTIONS) {
response.setStatusCode(HttpStatus.OK);
return Mono.empty();
}
}
return chain.filter(ctx);
};
}

}

二、Gateway处理重复Request Headers的方法

2.1 跨域完成后,出现重复header,报出以下错误:

1
js复制代码origin xxxx has been blocked by CORS policy: The 'Access-Control-Allow-Origin' header contains multiple values 'xxxx, xxxx', but only one is allowed.The 'Access-Control-Allow-Origin' header contains multiple values 'xxxx,xxxx', but only one is allowed.

2.2 处理方法:

方法一:

由于我用的Spring Cloud是较高的版本Hoxton.SR9,这个版本中有 DedupeResponseHeaderGatewayFilterFactory,贴出源码。

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
js复制代码import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
import org.springframework.cloud.gateway.filter.GatewayFilter;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.factory.AbstractGatewayFilterFactory.NameConfig;
import org.springframework.cloud.gateway.support.GatewayToStringStyler;
import org.springframework.http.HttpHeaders;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;

public class DedupeResponseHeaderGatewayFilterFactory extends AbstractGatewayFilterFactory<DedupeResponseHeaderGatewayFilterFactory.Config> {
private static final String STRATEGY_KEY = "strategy";

public DedupeResponseHeaderGatewayFilterFactory() {
super(DedupeResponseHeaderGatewayFilterFactory.Config.class);
}

public List<String> shortcutFieldOrder() {
return Arrays.asList("name", "strategy");
}

public GatewayFilter apply(DedupeResponseHeaderGatewayFilterFactory.Config config) {
return new GatewayFilter() {
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
return chain.filter(exchange).then(Mono.fromRunnable(() -> {
DedupeResponseHeaderGatewayFilterFactory.this.dedupe(exchange.getResponse().getHeaders(), config);
}));
}

public String toString() {
return GatewayToStringStyler.filterToStringCreator(DedupeResponseHeaderGatewayFilterFactory.this).append(config.getName(), config.getStrategy()).toString();
}
};
}

void dedupe(HttpHeaders headers, DedupeResponseHeaderGatewayFilterFactory.Config config) {
String names = config.getName();
DedupeResponseHeaderGatewayFilterFactory.Strategy strategy = config.getStrategy();
if (headers != null && names != null && strategy != null) {
String[] var5 = names.split(" ");
int var6 = var5.length;

for(int var7 = 0; var7 < var6; ++var7) {
String name = var5[var7];
this.dedupe(headers, name.trim(), strategy);
}

}
}

private void dedupe(HttpHeaders headers, String name, DedupeResponseHeaderGatewayFilterFactory.Strategy strategy) {
List<String> values = headers.get(name);
if (values != null && values.size() > 1) {
//过滤过程
switch(strategy) {
case RETAIN_FIRST:
headers.set(name, (String)values.get(0));
break;
case RETAIN_LAST:
headers.set(name, (String)values.get(values.size() - 1));
break;
case RETAIN_UNIQUE:
headers.put(name, (List)values.stream().distinct().collect(Collectors.toList()));
}

}
}

public static class Config extends NameConfig {
private DedupeResponseHeaderGatewayFilterFactory.Strategy strategy;

public Config() {
//默认给定过滤规则=RETAIN_FIRST
this.strategy = DedupeResponseHeaderGatewayFilterFactory.Strategy.RETAIN_FIRST;
}

public DedupeResponseHeaderGatewayFilterFactory.Strategy getStrategy() {
return this.strategy;
}

public DedupeResponseHeaderGatewayFilterFactory.Config setStrategy(DedupeResponseHeaderGatewayFilterFactory.Strategy strategy) {
this.strategy = strategy;
return this;
}
}

public static enum Strategy {
//过滤规则
//[RETAIN_FIRST|RETAIN_UNIQUE|RETAIN_LAST]
RETAIN_FIRST,
RETAIN_LAST,
RETAIN_UNIQUE;

private Strategy() {
}
}
}

由源码中可以看出DedupeResponseHeader的过滤规则为RETAIN_FIRST|RETAIN_UNIQUE|RETAIN_LAST。而且在DedupeResponseHeaderGatewayFilterFactory初始化时已经给定了默认的过滤规则为RETAIN_FIRST。

1
2
3
js复制代码RETAIN_FIRST=过滤取第一个值
RETAIN_LAST=过滤取最后一个值
RETAIN_UNIQUE=过滤取唯一的值

DedupeResponseHeader可以配置三种规则中任一种规则,过滤规则和过滤参数以逗号,分割。

1
js复制代码spring.cloud.gateway.default-filters[0]=DedupeResponseHeader=A B C D,[RETAIN_FIRST|RETAIN_UNIQUE|RETAIN_LAST]

或在配置中,也可以省略过滤规则,DedupeResponseHeaderGatewayFilterFactory会自动给定RETAIN_FIRST为默认过滤规则。

1
js复制代码spring.cloud.gateway.default-filters[0]=DedupeResponseHeader=A B C D

yml配置如下:

1
2
3
4
5
6
7
8
9
10
11
12
js复制代码spring:
application:
name: gateway-server2
cloud:
gateway:
discovery:
locator:
# 是否与服务发现组件进行结合,通过 serviceId 转发到具体服务实例。
enabled: true # 是否开启基于服务发现的路由规则
lower-case-service-id: true # 是否将服务名称转小写
#gateway跨域后 Header重复过滤,过滤规则[RETAIN_FIRST|RETAIN_UNIQUE|RETAIN_LAST]
default-filters[0]: DedupeResponseHeader=Access-Control-Allow-Origin Access-Control-Allow-Credentials Access-Control-Expose-Headers Access-Control-Allow-Methods Access-Control-Allow-Headers Content-Type Vary,RETAIN_FIRST

方法二:

直接继承GlobalFilter, Ordered复写 filter() 过滤exchange.getResponse().getHeaders()中的headers属性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
js复制代码@Component
public class CorsResponseHeaderFilter implements GlobalFilter, Ordered {

@Override
public int getOrder() {
// 指定位于 NettyWriteResponseFilter 处理完响应体后移除重复 CORS 响应头
return NettyWriteResponseFilter.WRITE_RESPONSE_FILTER_ORDER + 1;
}

@Override
@SuppressWarnings("serial")
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
return chain.filter(exchange).then(Mono.defer(() -> {
exchange.getResponse().getHeaders().entrySet().stream()
.filter(kv -> (kv.getValue() != null && kv.getValue().size() > 1))
.filter(kv -> (kv.getKey().equals(HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN)
|| kv.getKey().equals(HttpHeaders.ACCESS_CONTROL_ALLOW_CREDENTIALS)))
.forEach(kv -> kv.setValue(new ArrayList<String>() {{
add(kv.getValue().get(0));
}}));
return chain.filter(exchange);
}));
}
}

三、GateWay全局过滤器向request header中添加参数

由于在项目中从nginx请求到gateway,需要将获取到的权鉴数据存到httpRequest请求体中,供下游逻辑服务使用。

废话不多说,直接上代码。

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
js复制代码@Component
@Slf4j
public class AccessFilter implements GlobalFilter, Ordered {

@Autowired
private RedisTemplate redisTemplate;

@Autowired
private RestTemplate restTemplate;

//authMap
private JSONObject jsonMap;

@Autowired
private CustomConfiguration customConfiguration;

private static boolean isContains(String container, String[] regx) {
for (int i = 0; i < regx.length; i++) {
if (container.indexOf(regx[i]) != -1) {
return true;
}
}
return false;
}


@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {

ServerHttpRequest request = exchange.getRequest();
ServerHttpResponse response = exchange.getResponse();

String includes = "/public;/auth-authentication;/admin/auth-license/addCmsFiles";
String[] excludeList = customConfiguration.getExcludeUrl().split(";");
if (isContains(request.getURI().toString(), excludeList)) {
return chain.filter(exchange);
}

//请求体内容重写
ServerHttpRequest httpRequest = requestHeadersRePut(request, jsonMap);
ServerWebExchange newExchage = exchange.mutate().request(httpRequest).build();
return chain.filter(newExchage);
}

/**
* auth参数存入
* request header
*
* @param request
* @param authMap
* @return
*/
private ServerHttpRequest requestHeadersRePut(ServerHttpRequest request, JSONObject authMap) {
ServerHttpRequest httpRequest;
Map<String, String> map = new HashMap<>();

Consumer<HttpHeaders> httpHeaders = httpHeader -> {
httpHeader.set("userId", authMap.getString("userId"));
httpHeader.set("departmentId", authMap.getString("departmentId"));
try {
httpHeader.set("realName", URLEncoder.encode(authMap.getString("realName"), "UTF-8"));
} catch (UnsupportedEncodingException e) {
log.info("{} ===>putRequestHeaders, exceptiopn: {}", this.getClass().getSimpleName(), e.toString());
e.printStackTrace();
}
httpHeader.set("username", authMap.getString("username"));
httpHeader.set("authorization", authMap.getString("authorization"));
};
httpRequest = request.mutate().headers(httpHeaders).build();
return httpRequest;
}

@Override
public int getOrder() {
return 0;
}
}

本文转载自: 掘金

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

从零学 Go 语言:第一个 Golang 程序

发表于 2021-06-02

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

前文回顾

上一篇文章,我们介绍了 Go 语言的一些特性以及环境的安装。具体可以参见:juejin.cn/user/349170…

Golang 简单、高效、并发的特性吸引了众多开发人员加入到 Golang 开发的大家庭中,目前已经涌现大量通过 Golang 原生开发的大型开源项目, 并在软件行业中发挥重要作用,其中包括 Docker、Kubernetes、etcd 等。

hello Go

本文我们将编写我们的第一个 Golang 程序,正式成为一名 Golang 开发者。相信大多数读者的第一个可运行的程序都是简单的 “Hello World” 输出,这代表了我辈程序员对计算机世界无尽的探索热情和激情。同时大多数读者也会对这个简单的 HelloGo 小程序表示不屑,因为它并不能体现太多的语法和语言特性。

因此我们决定稍微提高一下第一个 Golang 程序的编码难度,HelloGo.go 将会是一个简单的命令行聊天机器人,它将展示部分 Golang 特性,让读者们对 Golang 语言有一个大致的了解。即使第一次没有读懂代码也并没有关系,随着知识点的逐渐展开与深入,相信再回头时读者能够轻易读懂以下代码。

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
go复制代码// 每一个可执行的 golang 程序必定具备一个 main 包,并在该 main 包下具有执行函数 main 的 go 文件
package main

// HelloGo.go
// 基于图灵 API 一个简单的聊天机器人

// 引入相关依赖
import (
"bytes"
"encoding/json"
"fmt"
"io/ioutil"
"math/rand"
"net/http"
)

// 请求体结构体
type requestBody struct {
Key string `json:"key"`
Info string `json:"info"`
UserId string `json:"userid"`
}


// 结果体结构体
type responseBody struct {
Code int `json:"code"`
Text string `json:"text"`
List []string `json:"list"`
Url string `json:"url"`
}

// 请求机器人
func process(inputChan <-chan string, userid string) {
for{
// 从通道中接受输入
input := <- inputChan
if input == "EOF"{
break
}
// 构建请求体
reqData := &requestBody{
Key: "792bcf45156d488c92e9d11da494b085",
Info : input,
UserId: userid,
}
// 转义为 json
byteData,_ := json.Marshal(&reqData)
// 请求聊天机器人接口
req, err := http.NewRequest("POST",
"http://www.tuling123.com/openapi/api",
bytes.NewReader(byteData))

req.Header.Set("Content-Type", "application/json;charset=UTF-8")

client := http.Client{}
resp, err := client.Do(req)

if err != nil {
fmt.Println("Network Error!")
}else {
// 将结果从 json 中解析并输出到命令行
body, _ := ioutil.ReadAll(resp.Body)
var respData responseBody
json.Unmarshal(body, &respData)
fmt.Println("AI: " + respData.Text)

}
resp.Body.Close()
}
}

func main() {

var input string
fmt.Println("Enter 'EOF' to shut down: ")
// 创建通道
channel := make(chan string)
// main 结束时关闭通道
defer close(channel)
// 启动 goroutine 运行机器人回答线程
go process(channel, string(rand.Int63()))

for {
// 从命令行中读取输入
fmt.Scanf("%s", &input)
// 将输入放到通道中
channel <- input
// 结束程序
if input == "EOF"{
fmt.Println("Bye!")
break
}

}

}

在上述这段长长的 HelloGo 程序中,我们通过 import 关键字引入了诸多的依赖包。在 Golang 中,主要通过 import 引入外部依赖。

可以注意到代码位于 main 包下,Golang 中规定可执行程序必须具备 main 包,具备可以执行函数 main 的 go 文件必须位于该包下。而且 Golang 中的代码通过换行符分割,不需要在每行代码后加上 ; 等结束符。

我们还定义了两个结构体,和两个函数。两个结构体分别代表请求体和结果体的 JSON 格式。process 函数执行了从通道中获取输入消息并发送到聊天机器人 API,从而获取返回结果的逻辑。main 函数启动了这个程序,从命令行中等待输入,并把输入放入到通道中,同时通过 goroutine 启动了一个新的线程执行 process 函数。通道可以理解为 main 函数线程和 process 函数线程信息传递的工具。

小结

本文较为简单,主要介绍了使用 Go 语言编写一个简单的程序。我们的聊天机器人的逻辑很简单,即从命令行中读取用户输入,然后调用远程聊天机器人的 API 进行分析,使用 API 中返回的结果反馈给用户。

下面的文章我们将会介绍相关的 GO 语言编译工具。

本文转载自: 掘金

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

Nacos源码之一-配置自动更新

发表于 2021-06-02

Nacos 是阿里巴巴开源的集分布式配置中心、分布式注册中心为一体的分布式解决方案。

它的优点:

  1. 提供命令空间,方便管理不同环境的配置;
  2. 提供web界面,方便管理配置和服务;
  3. 支持配置版本管理,回滚;
  4. 支持服务管理,手动上线、下线服务。

等等其他优点。

1 如何使用 Nacos 自动更新配置

1.1 配置自动更新的两种方式

第一种方式

  1. 属性使用@Value注解
  2. 类使用@RefreshScope 注解
1
2
3
4
5
6
7
java复制代码@RefreshScope
@RequestMapping("config")
public class ConfigController {

@Value("${useLocalCache:false}")
private boolean useLocalCache;
}

第二种方式

  1. 使用@NacosValue注解,自动更新配置成true
1
2
3
4
5
6
7
8
9
10
11
12
13
java复制代码@Controller
@RequestMapping("config")
public class ConfigController {

@NacosValue(value = "${useLocalCache:false}", autoRefreshed = true)
private boolean useLocalCache;

@RequestMapping(value = "/get", method = GET)
@ResponseBody
public boolean get() {
return useLocalCache;
}
}

2 Nacos 配置更新源码分析

先上图

Nacos配置更新流程图

具体步骤

1、第一步(更新数据库)比较简单,就是判断是新增配置还是修改配置,然后修改或者新增数据库响应信息(根据使用的数据库是内嵌derby还是mysql使用不同的实现)。

2、比较复杂的是通知(更新文件系统中的文件)

2.1 通过发布订阅模式,发布配置变更事件

2.2 订阅者接收到消息后,调用controller请求(communication/dataChange)

2.3 这个controller请求,启动一个异步任务,这个异步任务更新配置数据到指定的配置文件(nacos目录下)

之后通过发布-订阅模式通知其他服务,其他服务接收到通知之后,更新配置。

PS:

1、controller 请求http://ip:port/nacos/v1/cs/communication/dataChange?dataId=example&group=DEFAULT_GROUP

2、配置存储有两个地方,一个是在文件系统中,另一个是配置的数据库中(可能是内嵌的derby数据库和MySQL数据库)

3、一些配置基本信息,比如配置文件的 MD5 值、dataId、groupName等,会保存在 ConcurrentHashMap 存储的缓存的

源码1: 添加异步任务更新配置文件代码

1
2
3
4
5
6
7
8
9
java复制代码/**
* Add DumpTask to TaskManager, it will execute asynchronously.
*/
public void dump(String dataId, String group, String tenant, long lastModified, String handleIp, boolean isBeta) {
String groupKey = GroupKey2.getKey(dataId, group, tenant);
String taskKey = String.join("+", dataId, group, tenant, String.valueOf(isBeta));
dumpTaskMgr.addTask(taskKey, new DumpTask(groupKey, lastModified, handleIp, isBeta));
DUMP_LOG.info("[dump-task] add task. groupKey={}, taskKey={}", groupKey, taskKey);
}

然后通过persistService读取数据库中的相应记录

源码2: 读取数据库中的最新配置数据

1
java复制代码ConfigInfo cf = persistService.findConfigInfo(dataId, group, tenant);

将读取到的内容,写入到本地文件nacos/distribution/target/nacos-server-1.4.2/nacos/data/config-data/${GROUP_NAME}/${dataId}中,并更新配置文件的md5值。

源码3: 保存配置文件到文件目录中,并更新文件的 MD5 值

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复制代码/**
* Save config file and update md5 value in cache.
*
* @param dataId dataId string value.
* @param group group string value.
* @param tenant tenant string value.
* @param content content string value.
* @param lastModifiedTs lastModifiedTs.
* @param type file type.
* @return dumpChange success or not.
*/
public static boolean dump(String dataId, String group, String tenant, String content, long lastModifiedTs,
String type) {
String groupKey = GroupKey2.getKey(dataId, group, tenant);
CacheItem ci = makeSure(groupKey);
ci.setType(type);
final int lockResult = tryWriteLock(groupKey);
assert (lockResult != 0);

if (lockResult < 0) {
DUMP_LOG.warn("[dump-error] write lock failed. {}", groupKey);
return false;
}

try {
final String md5 = MD5Utils.md5Hex(content, Constants.ENCODE);

if (md5.equals(ConfigCacheService.getContentMd5(groupKey))) {
DUMP_LOG.warn("[dump-ignore] ignore to save cache file. groupKey={}, md5={}, lastModifiedOld={}, "
+ "lastModifiedNew={}", groupKey, md5, ConfigCacheService.getLastModifiedTs(groupKey),
lastModifiedTs);
} else if (!PropertyUtil.isDirectRead()) {
// 保存数据到文件中
DiskUtil.saveToDisk(dataId, group, tenant, content);
}
updateMd5(groupKey, md5, lastModifiedTs);
return true;
} catch (IOException ioe) {
DUMP_LOG.error("[dump-exception] save disk error. " + groupKey + ", " + ioe.toString(), ioe);
if (ioe.getMessage() != null) {
String errMsg = ioe.getMessage();
if (NO_SPACE_CN.equals(errMsg) || NO_SPACE_EN.equals(errMsg) || errMsg.contains(DISK_QUATA_CN) || errMsg
.contains(DISK_QUATA_EN)) {
// Protect from disk full.
FATAL_LOG.error("磁盘满自杀退出", ioe);
System.exit(0);
}
}
return false;
} finally {
releaseWriteLock(groupKey);
}
}

/**
* Save configuration information to disk.
*/
public static void saveToDisk(String dataId, String group, String tenant, String content) throws IOException {
File targetFile = targetFile(dataId, group, tenant);
FileUtils.writeStringToFile(targetFile, content, Constants.ENCODE);
}

3 源码亮点

3.1 发布订阅模式

发布订阅模式使用于一个事件发生需要通知多个订阅者的场景。实际开发中,也能会有不同的名字,大家要能够识别,比如:Subject-Observer(经典例子)、Publisher-Subscriber(Nacos)、Producer-Consumer(Kafka)、Dispatcher-Listener。

发布变更事件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
java复制代码/**
* Request publisher publish event Publishers load lazily, calling publisher.
*
* @param eventType class Instances type of the event type.
* @param event event instance.
*/
private static boolean publishEvent(final Class<? extends Event> eventType, final Event event) {
if (ClassUtils.isAssignableFrom(SlowEvent.class, eventType)) {
return INSTANCE.sharePublisher.publish(event);
}

final String topic = ClassUtils.getCanonicalName(eventType);

EventPublisher publisher = INSTANCE.publisherMap.get(topic);
if (publisher != null) {
return publisher.publish(event);
}
LOGGER.warn("There are no [{}] publishers for this event, please register", topic);
return false;
}

循环订阅者列表,通知订阅者。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
java复制代码/**
* Receive and notifySubscriber to process the event.
*
* @param event {@link Event}.
*/
void receiveEvent(Event event) {
final long currentEventSequence = event.sequence();

// Notification single event listener
for (Subscriber subscriber : subscribers) {
// Whether to ignore expiration events
if (subscriber.ignoreExpireEvent() && lastEventSequence > currentEventSequence) {
LOGGER.debug("[NotifyCenter] the {} is unacceptable to this subscriber, because had expire",
event.getClass());
continue;
}

// Because unifying smartSubscriber and subscriber, so here need to think of compatibility.
// Remove original judge part of codes.
notifySubscriber(subscriber, event);
}
}

通过线程池处理订阅者任务,因为这里订阅者需要调用HTTP请求来处理更新,所以通过线程池可以解耦。

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复制代码@Override
public void notifySubscriber(final Subscriber subscriber, final Event event) {

LOGGER.debug("[NotifyCenter] the {} will received by {}", event, subscriber);

final Runnable job = new Runnable() {
@Override
public void run() {
subscriber.onEvent(event);
}
};

final Executor executor = subscriber.executor();

if (executor != null) {
executor.execute(job);
} else {
try {
job.run();
} catch (Throwable e) {
LOGGER.error("Event callback exception : {}", e);
}
}
}

3.2 任务管理器

这里存在竞争资源—单个配置文件,也就是确定Group下的dataId文件或者数据库记录。

  1. 失败重试
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
java复制代码/**
* process tasks in execute engine.
*/
protected void processTasks() {
Collection<Object> keys = getAllTaskKeys();
for (Object taskKey : keys) {
AbstractDelayTask task = removeTask(taskKey);
if (null == task) {
continue;
}
NacosTaskProcessor processor = getProcessor(taskKey);
if (null == processor) {
getEngineLog().error("processor not found for task, so discarded. " + task);
continue;
}
try {
// ReAdd task if process failed
if (!processor.process(task)) {
retryFailedTask(taskKey, task);
}
} catch (Throwable e) {
getEngineLog().error("Nacos task execute error : " + e.toString(), e);
retryFailedTask(taskKey, task);
}
}
}

3.3. 添加任务处理并发

使用重入锁ReentrantLock

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
java复制代码protected final ReentrantLock lock = new ReentrantLock();

@Override
public void addTask(Object key, AbstractDelayTask newTask) {
lock.lock();
try {
AbstractDelayTask existTask = tasks.get(key);
if (null != existTask) {
newTask.merge(existTask);
}
tasks.put(key, newTask);
} finally {
lock.unlock();
}
}
  1. 缓存任务处理器对象,需要的时候直接通过本地缓存获取
1
java复制代码private final ConcurrentHashMap<Object, NacosTaskProcessor> taskProcessors = new ConcurrentHashMap<Object, NacosTaskProcessor>();

3.4 一个接口多个实现,根据条件选择

如果要实现多种不同数据库的操作的时候,Condition是非常有用的,比如这里要支持内嵌的derby数据库,也要支持MySQL,通过Condition可以根据需要使用不同的数据库实现。

传统企业里面有些业务系统需要支持多种不同的数据库,不同客户现场可能会使用不同的数据库,通过这种方式可以减少定制、减少到现场由于客户数据库不同而临时进行定制开发。

1
2
3
java复制代码@Conditional(value = ConditionOnEmbeddedStorage.class)
@Component
public class EmbeddedStoragePersistServiceImpl implements PersistService {
1
2
3
java复制代码@Conditional(value = ConditionOnExternalStorage.class)
@Component
public class ExternalStoragePersistServiceImpl implements PersistService {

如果本文对你有帮助,欢迎关注我的公众号 【哥妞】 ,带你深入 JAVA 的世界~

本文转载自: 掘金

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

难受,生产 Go timerAfter 内存泄露之痛!

发表于 2021-06-02

微信搜索【脑子进煎鱼了】关注这一只爆肝煎鱼。本文 GitHub github.com/eddycjy/blo… 已收录,有我的系列文章、资料和开源 Go 图书。

大家好,我是煎鱼。

前几天分享了一篇 Go timer 源码解析的文章《难以驾驭的 Go timer,一文带你参透计时器的奥秘》。

在评论区有小伙伴提到了经典的 timer.After 泄露问题,希望我能聊聊,这是一个不能不知的一个大 “坑”。

今天这篇文章煎鱼就带大家来研讨一下这个问题。

timer.After

今天是男主角是Go 标准库 time 所提供的 After 方法。函数签名如下:

1
golang复制代码func After(d Duration) <-chan Time

该方法可以在一定时间(根据所传入的 Duration)后主动返回 time.Time 类型的 channel 消息。

在常见的场景下,我们会基于此方法做一些计时器相关的功能开发,例子如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
golang复制代码func main() {
ch := make(chan string)
go func() {
time.Sleep(time.Second * 3)
ch <- "脑子进煎鱼了"
}()

select {
case _ = <-ch:
case <-time.After(time.Second * 1):
fmt.Println("煎鱼出去了,超时了!!!")
}
}

在运行 1 秒钟后,输出结果:

1
复制代码煎鱼出去了,超时了!!!

上述程序在在运行 1 秒钟后将触发 time.After 方法的定时消息返回,输出了超时的结果。

坑在哪里

从例子来看似乎非常正常,也没什么 “坑” 的样子。难道是 timer.After 方法的虚晃一枪?

我们再看一个不像是有问题例子,这在 Go 工程中经常能看见,只是大家都没怎么关注。

代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
golang复制代码func main() {
ch := make(chan int, 10)
go func() {
in := 1
for {
in++
ch <- in
}
}()

for {
select {
case _ = <-ch:
// do something...
continue
case <-time.After(3 * time.Minute):
fmt.Printf("现在是:%d,我脑子进煎鱼了!", time.Now().Unix())
}
}
}

在上述代码中,我们构造了一个 for+select+channel 的一个经典的处理模式。

同时在 select+case 中调用了 time.After 方法做超时控制,避免在 channel 等待时阻塞过久,引发其他问题。

看上去都没什么问题,但是细心一看。在运行了一段时间后,粗暴的利用 top 命令一看:

运行了一会后,10+GB

我的 Go 工程的内存占用竟然已经达到了 10+GB 之高,并且还在持续增长,非常可怕。

在所设置的超时时间到达后,Go 工程的内存占用似乎一时半会也没有要回退下去的样子,这,到底发生了什么事?

为什么

抱着一脸懵逼的煎鱼,我默默的掏出我早已埋好的 PProf,这是 Go 语言中最强的性能分析剖析工具,在我出版的 《Go 语言编程之旅》特意有花量章节的篇幅大面积将讲解过。

在 Go 语言中,PProf 是用于可视化和分析性能分析数据的工具,PProf 以 profile.proto 读取分析样本的集合,并生成报告以可视化并帮助分析数据(支持文本和图形报告)。

我们直接用 go tool pprof 分析 Go 工程中函数内存申请情况,如下图:

PProf

从图来分析,可以发现是不断地在调用 time.After,从而导致计时器 time.NerTimer 的不断创建和内存申请。

这就非常奇怪了,因为我们的 Go 工程里只有几行代码与 time 相关联:

1
2
3
4
5
6
7
8
9
10
golang复制代码func main() {
...
for {
select {
...
case <-time.After(3 * time.Minute):
fmt.Printf("现在是:%d,我脑子进煎鱼了!", time.Now().Unix())
}
}
}

由于 Demo 足够的小,我们相信这就是问题代码,但原因是什么呢?

原因在于 for+select,再加上 time.After 的组合会导致内存泄露。因为 for在循环时,就会调用都 select 语句,因此在每次进行 select 时,都会重新初始化一个全新的计时器(Timer)。

我们这个计时器,是在 3 分钟后才会被触发去执行某些事,但重点在于计时器激活后,却又发现和 select 之间没有引用关系了,因此很合理的也就被 GC 给清理掉了,因为没有人需要 “我” 了。

要命的还在后头,被抛弃的 time.After 的定时任务还是在时间堆中等待触发,在定时任务未到期之前,是不会被 GC 清除的。

但很可惜,他 “永远” 不会到期了,也就是为什么我们的 Go 工程内存会不断飙高,其实是 time.After 产生的内存孤儿们导致了泄露。

解决办法

既然我们知道了问题的根因代码是不断的重复创建 time.After,又没法完整的走完释放的闭环,那解决办法也就有了。

改进后的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
golang复制代码func main() {
timer := time.NewTimer(3 * time.Minute)
defer timer.Stop()

...
for {
select {
...
case <-timer.C:
fmt.Printf("现在是:%d,我脑子进煎鱼了!", time.Now().Unix())
}
}
}

经过一段时间的摸鱼后,再使用 PProf 进行采集和查看:

PProf

Go 进程的各项指标正常,完好的解决了这个内存泄露的问题。

总结

在今天这篇文章中,我们介绍了标准库 time 的基本常规使用,同时针对 Go 小伙伴所提出的 time.After 方法的使用不当,所导致的内存泄露进行了重现和问题解析。

其根因就在于 Go 语言时间堆的处理机制和常规 for+select+time.After 组合的下意识写法所导致的泄露。

突然想起我有一个朋友在公司里有看到过类似的代码,在生产踩过这个坑,半夜被告警抓起来…

不知道你在日常工作中有没有遇到过相似的问题呢,欢迎留言区评论和交流。

若有任何疑问欢迎评论区反馈和交流,最好的关系是互相成就,各位的点赞就是煎鱼创作的最大动力,感谢支持。

文章持续更新,可以微信搜【脑子进煎鱼了】阅读,回复【000】有我准备的一线大厂面试算法题解和资料;本文 GitHub github.com/eddycjy/blo… 已收录,欢迎 Star 催更。

本文转载自: 掘金

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

MAMP PRO for MAC 安装redis、memca

发表于 2021-06-02

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

首先说明:

基本所有的添加扩展都是这两步:

编译PHP源码,生成 redis.so 扩展文件,并将扩展文件放在扩展的文件夹下

修改php.ini, 即在php.ini 中添加一行:extensions = redis.so;

Tips:我的环境是php7.1.32

准备:

1.pecl下载:php pecl扩展下载链接 可以到这里面下载所需的扩展,下面的流程基本一致了!

2.PHP7可以在 php版本所在的bin目录下执行 ./pecl install memcached/./pecl install redis (可能会有遇到报错,下面会讲怎么处理)

3.还可以到git下载redis git clone github.com/nicolasff/p… (一个名为 phpredis 的文件夹)

【 Redis 】

1.打开自己php版本所在目录 cd /Applications/MAMP/bin/php/php7.1.32

2.我这里用git的方式安装,你可以直接用 ./pecl install redis 这个很简单,编译好你开启php.ini就好(可能会报错,下面会讲解)

2.1 下载redis git clone github.com/nicolasff/p… (一个名为 phpredis 的文件夹)

2.2 cd phpredis

2.3 执行以下代码

1
2
3
4
5
bash复制代码/Applications/MAMP/bin/php/php7.1.32/bin/phpize

./configure --with-php-config=/Applications/MAMP/bin/php/php7.1.32/bin/php-config

make
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
bash复制代码ERROR

执行第一句可能出现以下问题:

Configuring for:

PHP Api Version: 20041225

Zend Module Api No: 20060613

Zend Extension Api No: 220060519

Cannot find autoconf. Please check your autoconf installation and the $PHP_AUTOCONF environment variable is set correctly and then rerun this script.

说明:这是缺少autoconf

解决办法: 使用brew安装

执行brew install autoconf

`(没有安装brew的话就执行下面的语句安装:/bin/zsh -c "$(curl -fsSL https://gitee.com/cunkai/HomebrewCN/raw/master/Homebrew.sh)")`

下载可以重新回去执行上面提供的操作执行编译了。

2.4 编译成功,会在phpredis/modules下生成了redis.so文件,则把这个redis.so 放到/Applications/MAMP/bin/php/php7.1.32/lib/php/extensions/no-debug-non-zts-20160303(最后这个文件夹的名字可能不一样)

3.修改php.ini,重启MAMP。 就可以在phpinfo中看到redis了。

修改方法:

1.点击菜单 –> File –> Edit Template –> PHP –> PHP 7.1.32 php.ini

7114239-dd9a3678623e4338.png

2.找到extension 扎堆的地方,加上 extension=redis.so;

走到这里redis就安装成功了,打开phpinfo查看redis扩展就好!

【Memcached】(这是今天的重头戏 因为安装他真的遇到很多问题,我看了很多文章才安装好,就都整理到这里了)

(这里的安装方式有两种我以下班压缩包自己编译的方式为例子,./pecl的方式我会发配置的方式出来)

1.下载memcached扩展包

2.1 把压缩包复制到php目录下解压并打开包

1
bash复制代码 cd  /Applications/MAMP/bin/php/php7.1.32/memcached-3.1.5/memcached-3.1.5

2.2 — 编译完成, 老三步,执行编译代码(基本的步骤跟安装redis差不多,这里就不多说了,重复的步骤就参考上面的把。)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
bash复制代码/Applications/MAMP/bin/php/php7.1.32/bin/phpize

./configure --with-php-config=/Applications/MAMP/bin/php/php7.1.32/bin/php-config

make

make install

(理想的情况是什么问题都没有直接编译成功,然后到 /Applications/MAMP/bin/php/php7.1.32/memcached-3.1.5/memcached-3.1.5/modules 目录复制memcached.so文件到/Applications/MAMP/bin/php/php7.1.32/lib/php/extensions/no-debug-non-zts-20160303目录,但是不出意外的话肯定会出现各种问题,所以下载带着大家解决)

修改php.ini的配置

[memcached]
extension=memcached.so

执行第二句可能出现以下问题:

问题1:缺少 pkg-config

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
arduino复制代码出现以下错误提示,是表明你的mac缺少了pkg-config ,那没办法,竟然却了那就只能装了。

checking for pkg-config... no

pkg-config not found

configure: error: Please reinstall the pkg-config distribution

解决方法(下面这个执行的时间可能会比较久):

下载mac最新版pkg-config解压,地址:https://pkg-config.freedesktop.org/releases/

我下载的是https://pkg-config.freedesktop.org/releases/pkg-config-0.29.2.tar.gz,好像这个很久没有更新了,可以用跟我一样的!

终端cd到解压文件夹下:cd pkg-config-0.29.2

运行配置文件进行系统配置:./configure --with-internal-glib

编译pkgconfig:make

安装包自检测: make check

安装: make install

(到这里都没有提示什么错误的话,pkg-config就装完了)

问题2:缺少 zlib

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
lua复制代码看到这个错误提示就对了,我们的道路一波三折

checking for zlib location... configure: error: memcached support requires ZLIB. Use --with-zlib-dir=<DIR> to specify the prefix where ZLIB headers and library are located

解决办法:

直接用brew安装:brew install zlib

有些朋友反馈,安装了还是出现上面的错误,主要出现这种情况的朋友使用 (./pecl install memcached ) 这种方式安装的,这里我也顺便教大家怎么处理。

首先查看一下自己zlib安装的目录是否存在 一般是:/usr/local/opt/zlib

1.可以在询问zlib目录的时候把上面的路径复制上去

2.可以执行第一句的时候加上 --with-zlib-dir=/usr/local/opt/zlib

3.跟我一样下载扩展包安装的话是会自动识别的不需要指定了

(到这里zlib的问题就解决了)

问题3:缺少 libmemcached

1
2
3
4
5
6
7
8
9
10
11
12
13
vbnet复制代码一波三折的我们又见面了,我们先来看看下面的错误提示

checking for libmemcached location... configure: error: memcached support requires libmemcached. Use --with-libmemcached-dir=<DIR> to specify the prefix where libmemcached headers and library are locatedERROR: `/private/tmp/pear/temp/memcached/configure --with-php-config=/Applications/MAMP/bin/php/php7.1.32/bin/php-config --with-libmemcached-dir=no --with-zlib-dir=no --with-system-fastlz=no --enable-memcached-igbinary=no --enable-memcached-msgpack=no --enable-memcached-json=no --enable-memcached-protocol=no --enable-memcached-sasl=yes --enable-memcached-session=yes' failed

发现问题了吗?上面生成的编译代码 --with-libmemcached-dir=no 是no的这样当然会报错咯。

竟然发现问题了,那我们就好处理了,看看自己系统安装了libmemcached没有,目录跟上面提到的zlib是差不多的,没有的话我们就安装一下吧。

安装libmemcached: brew install libmemcached

这里跟上面zlib的处理方法是一样的,参考上面处理吧。

(这个问题解决完就可以开开心心的编译了)

本文安装教程到此结束,希望对你有帮助!

本文转载自: 掘金

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

【spring源码】spring中bean的创建流程,三级缓

发表于 2021-06-02

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

前面的文章主要写了spring源码工厂后置处理器所干的事情,即spring工程的扫描,解析,得到beanDefinition以及所有的beanName,还有注解@Import的原理,以及引入ImportSelector和ImportBeanDefinitionRegistrar的应用,有兴趣的可以看看前面的文章。

这篇文章开始写bean的实例化过程,读源码比较枯燥,相关的文章也比较枯燥,我尽量写简单明了点。

1.概述

bean创建过程.png

上面的图大概画了整个bean的创建流程,spring在创建bean之前首先从缓存池中去getBean,当从缓存池中获取不到bean时,才会去创建bean。了解spring源码的同学知道,spring中有三级缓存:

  • 一级缓存:singletonObjects —> 存放完整bean的单例池
  • 二级缓存:earlySingletonObjects —> 存放非完整bean的单例池,存放早期暴露的bean
  • 三级缓存:singletonFactories —> 存放可以创建bean的factory,这里的factory主要是去创建bean的代理对象

在创建bean的过程中,会通过bean后置处理去拿到bean的构造方法,对bean进行创建,然后对bean进行包装代理,如果bean不需要进行包装代理,就会返回一个早期暴露的对象,并放入二级缓存中,如果需要进行包装代理,就会返回一个factory,并放入三级缓存中。

然后会组装bean,这个过程就是去装配bean的属性,当bean装配完成后,bean就算是一个完整的bean了,最后会把这个完整的bean放入单例池中,即一级缓存中。

这就是bean创建的整个过程,这里概述了一下,也是想读到这篇文章的同学心里有一个大概,后面不至于晕乎乎的,下面就进入源码阅读阶段吧。

2.从缓存池中getBean

我们直接看doGetBean()这个方法,方法路径:AbstractBeanFactory#doGetBean(),在该方法最开始的时候就是去从单例池获取bean:

缓存getBean.png

接下来就点进这个方法看看是怎么从缓存中获取bean的:

getBean.png

可以看到代码中,首先是从一级缓存中去拿bean,当拿不到bean时,且当前bean正在被创建,就会从二级缓存中去获取,当二级拿不到时,从三级缓存中获取factory对象,如果拿到了factory对象,会从factory中去获取bean,最后将该bean放入二级缓存中。

由于spring工程在启动时,缓存里肯定没有bean对象,于是就需要去创建bean,接下来看bean是如何创建的。

3.bean的创建

还是doGetBean()方法,看一下最重要的代码片段:

getSingleton2.png

这里有两个方法,先看方法1,即createBean(beanName, mbd, args);点进这个方法去看看,这里省略了一些不重要的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
java复制代码protected Object createBean(String beanName, RootBeanDefinition mbd, @Nullable Object[] args)
throws BeanCreationException {

//... ...

// Prepare method overrides.
try {
/**
* 处理lookup-Method 和replace-Method,统称为overrides
*/
mbdToUse.prepareMethodOverrides();
}
//... ...

try {
// Give BeanPostProcessors a chance to return a proxy instead of the target bean instance.
/**
* 里面有个后置处理器,如果这个后置处理器能返回一个bean,就直接返回了,不再进行后面的装配,不理会里面的依赖
* InstantiationAwareBeanPostProcessor
*/
Object bean = resolveBeforeInstantiation(beanName, mbdToUse);
if (bean != null) {
return bean;
}
}
//... ...

try {
/**
* 创建对象
*/
Object beanInstance = doCreateBean(beanName, mbdToUse, args);
if (logger.isTraceEnabled()) {
logger.trace("Finished creating instance of bean '" + beanName + "'");
}
return beanInstance;
}
//... ...
}

可以看到这段代码片段里,首先是去处理overrides,然后去调用一个bean后置处理(InstantiationAwareBeanPostProcessor)的方法,如果该方法返回了一个对象,则返回该对象,不进行后面的bean创建流程。

接下来看看创建对象这个方法:Object beanInstance = doCreateBean(beanName, mbdToUse, args);

在这个方法里,我们一部分一部分的来看,第一部分就是去创建bean对象:

createBean1.png

来看看创建包装对象的部分,点进这个方法中:instanceWrapper = createBeanInstance(beanName, mbd, args); 依然省略一部分不重要的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
java复制代码protected BeanWrapper createBeanInstance(String beanName, RootBeanDefinition mbd, @Nullable Object[] args) {
//... ...
/**
* supplier的方式实例化对象
*/
Supplier<?> instanceSupplier = mbd.getInstanceSupplier();
if (instanceSupplier != null) {
return obtainFromSupplier(instanceSupplier, beanName);
}

/**
* 如果 FactoryMethodName 不为空,通过 instantiateUsingFactoryMethod() 去实例化对象
* 通过xml去指定 FactoryMethodName
* @Bean 当是static的时候,相当于是给对象配置了一个 FactoryMethodName
* 不是static的时候, 是uniqueFactoryMethodName
* 具体看ConfigurationClassPostProcessor
*/
if (mbd.getFactoryMethodName() != null) {
return instantiateUsingFactoryMethod(beanName, mbd, args);
}

// Shortcut when re-creating the same bean...
/**
* 从Spring的原始注释中,可以知道这是一个Shortcut(快捷方式),什么意思呢?
* 当多次构建同一bean时,可以使用这个 Shortcut
* 也就是不需要再次推断使用哪种方式构造bean
*/
boolean resolved = false;
boolean autowireNecessary = false;
if (args == null) {
synchronized (mbd.constructorArgumentLock) {
if (mbd.resolvedConstructorOrFactoryMethod != null) {
resolved = true;
autowireNecessary = mbd.constructorArgumentsResolved;
}
}
}
if (resolved) {
if (autowireNecessary) {
return autowireConstructor(beanName, mbd, null, null);
}
else {
return instantiateBean(beanName, mbd);
}
}

// Candidate constructors for autowiring?
/**
* 由后置处理器(SmartInstantiationAwareBeanPostProcessor)决定 返回 有参数的 构造方法
* 如果只有一个无参构造方法,这里返回为 null
*/
Constructor<?>[] ctors = determineConstructorsFromBeanPostProcessors(beanClass, beanName);
/**
* 自动装配模型 != 自动装配技术
* 自动装配模型 默认为 0
*/
if (ctors != null || mbd.getResolvedAutowireMode() == AUTOWIRE_CONSTRUCTOR ||
mbd.hasConstructorArgumentValues() || !ObjectUtils.isEmpty(args)) {
return autowireConstructor(beanName, mbd, ctors, args);
}

// Preferred constructors for default construction?
ctors = mbd.getPreferredConstructors();
if (ctors != null) {
return autowireConstructor(beanName, mbd, ctors, null);
}

// No special handling: simply use no-arg constructor.
/**
* 通过默认的无参构造方法进行初始化
*/
return instantiateBean(beanName, mbd);
}

该方法主要是讲如何去创建bean,首先是通过supplier的方式实例化对象,其次是通过FactoryMethodName的方式去实例化对象(这种方式好像没用过,有大神知道吗),再然后是通过bean后置处理器去找到bean的构造方法,如果bean中定义了有参构造方法,就返回有参构造方法,determineConstructorsFromBeanPostProcessors(beanClass, beanName);这里面有一些逻辑,感兴趣的同学可以自己去看一看,如果没有找到有参构造方法,就返回默认的无参构造方法。到这里就返回了一个BeanWrapper对象。

4.代理bean,放入三级缓存

前面bean已经被实例化出来了,因为spring中有些对象需要被代理,来看看这段代码:

addSingleton.png

在getEarlyBeanReference(beanName, mbd, bean));这个方法里,实际上就是去执行bean后置处理器中的方法,对bean进行代理:

1
2
java复制代码Object proxy = createProxy(
bean.getClass(), beanName, specificInterceptors, new SingletonTargetSource(bean));

最后会调用 ProxyFactory#getProxy 方法:

代理.png

这里是不是就是我们熟悉的JDK代理和Cglib代理~

然后看 addSingletonFactory() 这个方法:

1
2
3
4
5
6
7
8
9
10
java复制代码protected void addSingletonFactory(String beanName, ObjectFactory<?> singletonFactory) {
Assert.notNull(singletonFactory, "Singleton factory must not be null");
synchronized (this.singletonObjects) {
if (!this.singletonObjects.containsKey(beanName)) {
this.singletonFactories.put(beanName, singletonFactory);
this.earlySingletonObjects.remove(beanName);
this.registeredSingletons.add(beanName);
}
}
}

这里就是将factory对象放入三级缓存中。

5.组装bean,循环依赖问题

这里就不贴代码了,感兴趣的同学下来再研究吧。这里就是对bean的属性进行填充,比如A依赖B,这里就是去填充B.

这里涉及到一个很经典的问题,就是spring中的循环依赖问题,spring到底是怎么解决循环依赖问题的呢?

我在读spring源码后,画了一张图,来清晰的表示:

循环依赖.png

假设A和B相互依赖,当创建A到了组装这一步骤时,需要去组装B,这个时候就会去getB,但是get不到,就会去创建B,当创建B到组装这一步骤时,需要去组装A,而A这个bean已经创建过了,并且在缓存中,可以得到A,这个时候B就组装完成了,而当B组装完成了,相应的A也组装完成。

最后A和B都是一个完整的bean,然后被放入一级缓存中,即单例池中。

到这里bean的整个创建过程和spring的循环依赖问题就讲完了,希望看到这篇文章的同学有所收获。

本文转载自: 掘金

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

12 OptaPlanner快速开始

发表于 2021-06-02

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

内容概要

本章主要是首次入门学习OptaPlanner的一个Demo,类似于Java的HelloWorld。对于文章中“红底黑字”的内容,大家先牢记此概念,后续篇章会详细讲解。

OptaPlanner介绍

OptaPlanner是一个轻量级的、可嵌入的约束满足引擎,可求解规划问题,它巧妙的跟分数相结合,用分数来对比每一个解决方案,最高分作为最优解决方案。OptaPlanner本身支持多种算法,可扩展性和使用性都很高,社区环境在国外相对活跃,版本迭代稳定。多说无益,下面给大家看下视频介绍,可以很直观的了解OptaPlanner(字幕是我自己翻译的,不准确请谅解)

OptaPlanner视频介绍

你将要解决什么问题?

咱们今天来优化一个问题,为高中的学生和教师优化一个学校的时间表。

image.png

我们将使用OptaPlanner算法自动将课程分配到对应的教室、教室,并且要遵守硬约束、软约束的规则,如下:
硬约束

  • 一个房间在同一时间最多可以有一节课。
  • 一个老师在同一时间最多可以教一节课。
  • 一个学生在同一时间最多可以上一节课。
  • 软约束*
  • 一个老师喜欢在一个房间里教每一节课。
  • 一个老师喜欢教连续的课程,不喜欢课程之间有空隙。
    从数学上讲,学校的时间安排是一个NP-hard问题。简单地使用穷举算法来迭代所有可能的组合,对于一个很小的数据集,即使在超级计算机上也需要数百万年。幸运的是,像OptaPlanner这样的人工智能约束解算器拥有先进的算法,可以在合理的时间内提供一个接近最优的解决方案。

准备工作

JDK、MAVEN及编辑器:

  • JDK 8 or later
  • Maven 3.2+ or Gradle4+
  • An IDE, such as IntelliJ IDEA, VSCode or Eclipse

工程创建和Maven配置

使用idea初始化一个应用:

  • Spring Web (spring-boot-starter-web)
    MAVEN配置:
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
xml复制代码<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>{version-org-spring-framework-boot}</version>
</parent>

<groupId>com.example</groupId>
<artifactId>constraint-solving-ai-optaplanner</artifactId>
<version>0.1.0-SNAPSHOT</version>
<name>Constraint Solving AI with OptaPlanner</name>
<description>A Spring Boot OptaPlanner example to generate a school timetable.</description>

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

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

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

<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.optaplanner</groupId>
<artifactId>optaplanner-spring-boot-starter</artifactId>
<version>{project-version}</version>
</dependency>
</dependencies>
</dependencyManagement>

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

对问题进行业务建模

我们的目标是将每节课分配到一个时间段和一个房间。所以需要创建课程。如下类图:

image.png

Timeslot

时间段类表示上课的时间段,例如,星期一10:30 - 11:30或星期二13:30 - 14:30。为了简单起见,所有的时间段都有相同的时间长度,在午餐或其他休息时间没有时间段。
一个时间段没有日期,因为高中的课程表只是每周重复一次。因此,没有必要添加日期属性。

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

private DayOfWeek dayOfWeek;
private LocalTime startTime;
private LocalTime endTime;

private Timeslot() {
}
public Timeslot(DayOfWeek dayOfWeek, LocalTime startTime, LocalTime endTime) {
this.dayOfWeek = dayOfWeek;
this.startTime = startTime;
this.endTime = endTime;
}
@Override
public String toString() {
return dayOfWeek + " " + startTime.toString();
}
// ********************************
// Getters and setters
// ********************************

public DayOfWeek getDayOfWeek() {
return dayOfWeek;
}
public LocalTime getStartTime() {
return startTime;
}
public LocalTime getEndTime() {
return endTime;
}
}

因为在求解过程中Timeslot对象不会发生变化,所以Timeslot被称为Problem Fact(问题事实)。这样的类不需要任何OptaPlanner的注解。

注意:toString()方法使输出保持简短,所以更容易阅读OptaPlanner的DEBUG或TRACE日志。

Room

房间代表授课的地点,例如,房间A或房间B。为简单起见,所有房间都没有容量限制,它们可以容纳所有课程。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
java复制代码public class Room {

private String name;

private Room() {
}
public Room(String name) {
this.name = name;
}
@Override
public String toString() {
return name;
}
// ********************************
// Getters and setters
// ********************************

public String getName() {
return name;
}
}

Room对象在求解过程中不会改变,所以Room也是一个Problem Fact(问题事实)。

Lesson

在由Lesson类代表的一堂课中,教师向一组学生教授一个科目,例如,九年级的A.Turing教授的数学或十年级的M.Curie教授的化学。如果一个科目每周由同一个老师对同一个学生组进行多次授课,那么就会有多个Lesson实例,这些实例只能通过id来区分。例如,9年级的学生每周有6节数学课。

在求解过程中,OptaPlanner会改变Lesson类的timeSlot和room字段,以将每节课分配到一个时间段和一个房间。因为OptaPlanner改变了这些字段,所以Lesson是一个 Planning Entity(规划实体)。

image.png
一个课程的timeslot 和room 字段在初始化时是空的。OptaPlanner在求解过程中会改变这些字段的值。这些字段被称为Planning Variable(计划变量)。为了让OptaPlanner识别它们,timeslot 和room 字段都需要一个@PlanningVariable注解。它们的包含类Lesson,需要一个@PlanningEntity注解。

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
java复制代码@PlanningEntity
public class Lesson {

private Long id;

private String subject;
private String teacher;
private String studentGroup;

@PlanningVariable(valueRangeProviderRefs = "timeslotRange")
private Timeslot timeslot;

@PlanningVariable(valueRangeProviderRefs = "roomRange")
private Room room;

private Lesson() {
}

public Lesson(Long id, String subject, String teacher, String studentGroup) {
this.id = id;
this.subject = subject;
this.teacher = teacher;
this.studentGroup = studentGroup;
}

@Override
public String toString() {
return subject + "(" + id + ")";
}

// ********************************
// Getters and setters
// ********************************

public Long getId() {
return id;
}

public String getSubject() {
return subject;
}

public String getTeacher() {
return teacher;
}

public String getStudentGroup() {
return studentGroup;
}

public Timeslot getTimeslot() {
return timeslot;
}

public void setTimeslot(Timeslot timeslot) {
this.timeslot = timeslot;
}

public Room getRoom() {
return room;
}

public void setRoom(Room room) {
this.room = room;
}

}

定义约束条件并计算得分

分数代表一个解决方案的质量,越高越好。OptaPlanner寻找最优解决方案,也就是在可用时间内找到的得分最高的解决方案,它可能是最优解决方案。
因为这个用例有硬约束和软约束,所以用HardSoftScore类来表示分数。

  • 硬约束不能被打破。比如说,一个房间在同一时间最多可以有一节课。
  • 软约束不应该被打破。比如说,一个教师更喜欢在一个房间里上课。
    硬约束与其他硬约束相加权。软约束也要加权,与其他软约束相比,不管它们各自的权重如何,硬约束总是大于软约束,两者是不同层级的。

创建一个TimeTableConstraintProvider.java 类来执行增量分数计算。它使用OptaPlanner的ConstraintStream API,其灵感来自于Java 8 Streams和SQL。

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 TimeTableConstraintProvider implements ConstraintProvider {

@Override
public Constraint[] defineConstraints(ConstraintFactory constraintFactory) {
return new Constraint[]{
// Hard constraints
roomConflict(constraintFactory),
teacherConflict(constraintFactory),
studentGroupConflict(constraintFactory),
// Soft constraints are only implemented in the "complete" implementation
};
}

private Constraint roomConflict(ConstraintFactory constraintFactory) {
// 一个房间最多可以同时容纳一节课。

// 选择一个课程...
return constraintFactory.from(Lesson.class)
// ......并与另一课程配对......
.join(Lesson.class,
// ... 在同一时间段内 ...
Joiners.equal(Lesson::getTimeslot),
// ...在同一个房间里...
Joiners.equal(Lesson::getRoom),
// ...... 而且这一对是唯一的(不同的id,没有反向的对)。
Joiners.lessThan(Lesson::getId))
//然后用一个硬权重来惩罚每一对。
.penalize("Room conflict", HardSoftScore.ONE_HARD);
}

private Constraint teacherConflict(ConstraintFactory constraintFactory) {
// 一个教师在同一时间最多可以教一门课。
return constraintFactory.from(Lesson.class)
.join(Lesson.class,
Joiners.equal(Lesson::getTimeslot),
Joiners.equal(Lesson::getTeacher),
Joiners.lessThan(Lesson::getId))
.penalize("Teacher conflict", HardSoftScore.ONE_HARD);
}

private Constraint studentGroupConflict(ConstraintFactory constraintFactory) {
// 一个学生在同一时间最多只能上一节课。
return constraintFactory.from(Lesson.class)
.join(Lesson.class,
Joiners.equal(Lesson::getTimeslot),
Joiners.equal(Lesson::getStudentGroup),
Joiners.lessThan(Lesson::getId))
.penalize("Student group conflict", HardSoftScore.ONE_HARD);
}

}

创建PlanningSolution类

创建TimeTable类包装了一个单一数据集的所有Timeslot、Room和Lesson实例。此外,因为它包含了所有的课程,每个课程都有一个特定的规划变量状态,所以它是一个规划解决方案,它有一个分数。

可以把Timeable类理解成,OptaPlanner在求解过程中操作数据的入口,所有的常量数据及变量修改、分数计算都是通过这个类进行的。分数如下:

  • 如果课程仍未分配,那么它就是一个未初始化的解决方案,例如,一个得分为-4init/0hard/0soft的解决方案。
  • 如果它破坏了硬约束,那么它就是一个不可行的解决方案,例如,一个得分为-2hard/3soft的解决方案。
  • 如果它遵守了所有的硬约束,那么它就是一个可行的解决方案,例如,一个得分为0hard/7soft的解决方案。
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复制代码@PlanningSolution
public class TimeTable {

@ValueRangeProvider(id = "timeslotRange")
@ProblemFactCollectionProperty
private List<Timeslot> timeslotList;
@ValueRangeProvider(id = "roomRange")
@ProblemFactCollectionProperty
private List<Room> roomList;
@PlanningEntityCollectionProperty
private List<Lesson> lessonList;
@PlanningScore
private HardSoftScore score;

private TimeTable() {
}
public TimeTable(List<Timeslot> timeslotList, List<Room> roomList,
List<Lesson> lessonList) {
this.timeslotList = timeslotList;
this.roomList = roomList;
this.lessonList = lessonList;
}
// ********************************
// Getters and setters
// ********************************
public List<Timeslot> getTimeslotList() {
return timeslotList;
}
public List<Room> getRoomList() {
return roomList;
}
public List<Lesson> getLessonList() {
return lessonList;
}
public HardSoftScore getScore() {
return score;
}
}

TimeTable类有一个@PlanningSolution注解,所以OptaPlanner知道这个类包含所有的输入和输出数据。
具体来说,这个类是问题的输入:

  • timeslotList字段
    • 这是一个problem facts(问题事实)的列表,因为它们在解题过程中不会改变。
  • roomList字段
    • 这是一个problem facts(问题事实)的列表,因为它们在解题过程中不会发生变化。
  • lessonList字段
  • 这是一个planning entities(计划实体)的列表,因为它们在解题过程中会改变。
  • lesson
    • timeslot和room 字段的值通常还是空的,所以没有分配。它们是planning variables(规划变量)。
    • 其他字段,如subject, teacher ,studentGroup,都被填入。这些字段是problem properties(问题属性)。
      当然,这个类也是解决方案的输出。
  • lessonList字段,每个Lesson实例在解决后都有非空的timeslot 和room房间字段
  • score 分数字段,表示输出解决方案的质量,例如,0hard/-5soft

创建求解业务类

现在我们把所有的东西放在一起,创建一个REST服务。但是在REST线程上解决规划问题会导致HTTP超时问题。因此,Spring Boot启动器注入了一个SolverManager实例,它在一个单独的线程池中运行求解器,可以并行解决多个数据集。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
java复制代码@RestController
@RequestMapping("/timeTable")public class TimeTableController {

@Autowired
private SolverManager<TimeTable, UUID> solverManager;

@PostMapping("/solve")
public TimeTable solve(@RequestBody TimeTable problem) {
UUID problemId = UUID.randomUUID();
// 提交问题开始求解
SolverJob<TimeTable, UUID> solverJob = solverManager.solve(problemId, problem);
TimeTable solution;
try {
// 等待求解结束
solution = solverJob.getFinalBestSolution();
} catch (InterruptedException | ExecutionException e) {
throw new IllegalStateException("Solving failed.", e);
}
return solution;
}
}

为了简单起见,这个实现会等待求解器完成,这仍然会导致HTTP超时。完整的实现可以更优雅地避免HTTP超时。

设置终止时间

如果没有终止设置或终止事件,解算器会永远运行。为了避免这种情况,将求解时间限制在5秒之内。这足够短,可以避免HTTP超时。

Create the src/main/resources/application.properties file:

1
js复制代码optaplanner.solver.termination.spent-limit=5s

启动程序

通过SpringBoot Application类启动即可。

1
2
3
4
5
6
java复制代码@SpringBootApplication
public class TimeTableSpringBootApp {
public static void main(String[] args) {
SpringApplication.run(TimeTableSpringBootApp.class, args);
}
}

尝试求解

启动服务后,我们通过PostMan来进行访问。

URL:http://localhost:8080/timeTable/solve

求解数据JSON:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
json复制代码{
"timeslotList": [
{
"dayOfWeek": "MONDAY",
"startTime": "08:30:00",
"endTime": "09:30:00"
},
{
"dayOfWeek": "MONDAY",
"startTime": "09:30:00",
"endTime": "10:30:00"
}
],
"roomList": [
{
"name": "Room A"
},
{
"name": "Room B"
}
],
"lessonList": [
{
"id": 1,
"subject": "Math",
"teacher": "A. Turing",
"studentGroup": "9th grade"
},
{
"id": 2,
"subject": "Chemistry",
"teacher": "M. Curie",
"studentGroup": "9th grade"
},
{
"id": 3,
"subject": "French",
"teacher": "M. Curie",
"studentGroup": "10th grade"
},
{
"id": 4,
"subject": "History",
"teacher": "I. Jones",
"studentGroup": "10th grade"
}
]
}

大约5秒钟后,根据application.properties中定义的终止花费时间,该服务会返回一个输出。

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
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
json复制代码{
"timeslotList": [
{
"dayOfWeek": "MONDAY",
"startTime": "08:30:00",
"endTime": "09:30:00"
},
{
"dayOfWeek": "MONDAY",
"startTime": "09:30:00",
"endTime": "10:30:00"
}
],
"roomList": [
{
"name": "Room A"
},
{
"name": "Room B"
}
],
"lessonList": [
{
"id": 1,
"subject": "Math",
"teacher": "A. Turing",
"studentGroup": "9th grade",
"timeslot": {
"dayOfWeek": "MONDAY",
"startTime": "08:30:00",
"endTime": "09:30:00"
},
"room": {
"name": "Room A"
}
},
{
"id": 2,
"subject": "Chemistry",
"teacher": "M. Curie",
"studentGroup": "9th grade",
"timeslot": {
"dayOfWeek": "MONDAY",
"startTime": "09:30:00",
"endTime": "10:30:00"
},
"room": {
"name": "Room A"
}
},
{
"id": 3,
"subject": "French",
"teacher": "M. Curie",
"studentGroup": "10th grade",
"timeslot": {
"dayOfWeek": "MONDAY",
"startTime": "08:30:00",
"endTime": "09:30:00"
},
"room": {
"name": "Room B"
}
},
{
"id": 4,
"subject": "History",
"teacher": "I. Jones",
"studentGroup": "10th grade",
"timeslot": {
"dayOfWeek": "MONDAY",
"startTime": "09:30:00",
"endTime": "10:30:00"
},
"room": {
"name": "Room B"
}
}
],
"score": "0hard/0soft"
}

可以看出,程序将所有四节课分配给两个时间段中的一个和两个房间中的一个。还注意到,它符合所有的硬约束。例如,M. Curie’s 的两节课是在不同的时间段。

在服务器端,信息日志显示了OptaPlanner在这五秒钟内做了什么。

1
2
3
4
js复制代码... Solving started: time spent (33), best score (-8init/0hard/0soft), environment mode (REPRODUCIBLE), random (JDK with seed 0).
... Construction Heuristic phase (0) ended: time spent (73), best score (0hard/0soft), score calculation speed (459/sec), step total (4).
... Local Search phase (1) ended: time spent (5000), best score (0hard/0soft), score calculation speed (28949/sec), step total (28398).
... Solving ended: time spent (5000), best score (0hard/0soft), score calculation speed (28524/sec), phase total (2), environment mode (REPRODUCIBLE).

日志配置

我们在ConstraintProvider中添加约束条件时,请留意信息日志中的得分计算速度,在解出相同的时间后,评估对性能的影响。

1
js复制代码... Solving ended: ..., score calculation speed (29455/sec), ...

要了解OptaPlanner如何在内部求解问题,在application.properties文件中或用-D系统属性改变日志记录。

1
js复制代码logging.level.org.optaplanner=debug

使用调试日志来显示每一个步骤:

1
2
3
4
js复制代码 ... Solving started: time spent (67), best score (-20init/0hard/0soft), environment mode (REPRODUCIBLE), random (JDK with seed 0).
... CH step (0), time spent (128), score (-18init/0hard/0soft), selected move count (15), picked move ([Math(101) {null -> Room A}, Math(101) {null -> MONDAY 08:30}]).
... CH step (1), time spent (145), score (-16init/0hard/0soft), selected move count (15), picked move ([Physics(102) {null -> Room A}, Physics(102) {null -> MONDAY 09:30}]).
...

总结

给我们自己点个赞!我们刚刚用OptaPlanner开发了一个Spring应用程序。

本文章代码地址:gitee.com/yqsoftware/…

作业

通过这篇文章,我们已经能构建出一个简单的求解程序,文章中我们提到的硬约束/软约束,只有硬约束的实现,大家可以尝试给它加上软约束条件,例如:

软约束:M. Curie老师喜欢上下午课。

结束语

此次大家已经可以构建一个简单的求解程序,下一篇章我来学习各种的例子,加深对OptaPlanner的理解,再最后我们再进行系统的学习各个深层的应用。

最后编写不易,麻烦给个小小的赞,关注我,大家一起来学习~

本文转载自: 掘金

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

抖音春晚活动背后的 Service Mesh 流量治理技术

发表于 2021-06-02

本文整理自火山引擎开发者社区 Meetup 的同名演讲,主要介绍了抖音春晚红包大规模流量场景下的 Service Mesh 流量治理技术。

背景与挑战

2021 年的央视春晚红包项目留给业务研发同学的时间非常少,他们需要在有限的时间内完成相关代码的开发测试以及上线。

整个项目涉及到不同的技术团队,自然也会涉及众多的微服务。这些微服务有各自的语言技术栈,包括 Go,C++,Java,Python,Node 等,同时又运行在非常复杂的环境中,比如容器、虚拟机、物理机等。这些微服务在整个抖音春晚活动的不同阶段,可能又需要使用不同的流量治理策略来保证稳定性。

因此基础架构就需要为这些来自不同团队、用不同语言编写的微服务提供统一的流量治理能力。

传统微服务架构的应对

说到微服务,我们先来看一下传统的微服务架构是怎么解决这些问题的。随着企业组织的不断发展,产品的业务逻辑日渐复杂,为了提升产品的迭代效率,互联网软件的后端架构逐渐从单体的大服务演化成了分布式微服务。分布式架构相对于单体架构,其稳定性和可观测性要差一些。

为了提升这些点,我们就需要在微服务框架上实现很多功能。例如:

  • 微服务需要通过相互调用来完成原先单体大服务所实现的功能,这其中就涉及到相关的网络通信,以及网络通信带来的请求的序列化、响应的反序列化。
  • 服务间的相互调用涉及服务发现。
  • 分布式的架构可能需要不同的流量治理策略来保证服务之间相互调用的稳定性。
  • 微服务架构下还需要提升可观测性能力,包括日志、监控、Tracing 等。

通过实现以上这些功能,微服务架构也能解决前面提到的一些问题。但是微服务本身又存在一些问题:

  • 在多语言的微服务框架上实现多种功能,涉及的开发和运维成本非常高;
  • 微服务框架上一些新 Feature 的交付或者版本召回,需要业务研发同学配合进行相关的改动和发布上线,会造成微服务框架的版本长期割裂不受控的现象。

那我们怎么去解决这些问题呢?在软件工程的领域有这样一句话:任何问题都可以通过增加一个中间层去解决。而针对我们前面的问题,业界已经给出了答案,这个中间层就是 Service Mesh(服务网格)。

自研 Service Mesh 实现

下面就给大家介绍一下火山引擎自研 Service Mesh 的实现。先看下面这张架构图。

图中蓝色矩形的 Proxy 节点是 Service Mesh 的数据面,它是一个单独的进程,和运行着业务逻辑的 Service 进程部署在同样的运行环境(同一个容器或同一台机器)中。由这个 Proxy 进程来代理流经 Service 进程的所有流量,前面提到的需要在微服务框架上实现的服务发现、流量治理策略等功能就都可以由这个数据面进程完成。

图中的绿色矩形是 Service Mesh 的控制面。我们需要执行的路由流量、治理策略是由这个控制面决定的。它是一个部署在远端的服务,由它和数据面进程下发一些流量治理的规则,然后由数据面进程去执行。

同时我们也可以看到数据面和控制面是与业务无关的,其发布升级相对独立,不需要通知业务研发同学。

基于这样的架构就可以解决前文提到的一些问题:

  • 我们不需要把微服务框架众多的功能在每种语言上都实现一遍,只需要在 Service Mesh 的数据面进程中实现即可;
  • 同时由数据面进程屏蔽各种复杂的运行环境,Service 进程只需要和数据面进程通讯即可;
  • 各种灵活多变的流量治理策略也都可以由 Service Mesh 的进程控制面服务进行定制。

Service Mesh 流量治理技术

接下来给大家介绍我们的 Service Mesh 实现具体提供了哪些流量治理技术来保障微服务在面对抖音春晚活动的流量洪峰时能够有一个比较稳定的表现。

首先介绍一下流量治理的核心:

  • 路由:流量从一个微服务实体出发,可能需要进行一些服务发现或者通过一些规则流到下一个微服务。这个过程可以衍生出很多流量治理能力。
  • 安全:流量在不同的微服务之间流转时,需要通过身份认证、授权、加密等方式来保障流量内容是安全、真实、可信的。
  • 控制:在面对不同的场景时,用动态调整治理策略来保障微服务的稳定性。
  • 可观测性:这是比较重要的一点,我们需要对流量的状态加以记录、追踪,并配合预警系统及时发现并解决问题。

以上的四个核心方面配合具体的流量治理策略,可以提升微服务的稳定性,保障流量内容的安全,提升业务同学的研发效率,同时在面对黑天鹅事件的时候也可以提升整体的容灾能力。

下面我们继续来看一下 Service Mesh 技术具体都提供了哪些流量治理策略来保障微服务的稳定性。

稳定性策略——熔断

首先是熔断。在微服务架构中,单点故障是一种常态。当出现单点故障的时候,如何保障整体的成功率是熔断需要解决的问题。

熔断可以从客户端的视角出发,记录从服务发出的流量请求到达下游中每一个节点的成功率。当请求达到下游的成功率低于某一阈值,我们就会对这个节点进行熔断处理,使得流量请求不再打到故障节点上。

当故障节点恢复的时候,我们也需要一定的策略去进行熔断后的恢复。比如可以尝试在一个时间周期内发送一些流量打到这个故障节点,如果该节点仍然不能提供服务,就继续熔断;如果能够提供服务了,就逐渐加大流量,直到恢复正常水平。通过熔断策略,可以容忍微服务架构中个别节点的不可用,并防止进一步恶化带来的雪崩效应。

稳定性策略——限流

另外一个治理策略是限流。限流是基于这样的一个事实:Server 在过载状态下,其请求处理的成功率会降低。比如一个 Server 节点正常情况下能够处理 2000 QPS,在过载情况下(假设达到 3000 QPS),这个 Server 就只能处理 1000 QPS 甚至更低。限流可以主动 drop 一些流量,使得 Server 本身不会过载,防止雪崩效应。

稳定性策略——降级

当 Server 节点进一步过载,就需要使用降级策略。降级一般有两种场景:

  • 一种是按照比例丢弃流量。比如从 A 服务发出到 B 服务的流量,可以按照一定的比例(20% 甚至更高)丢弃。
  • 另外一种是旁路依赖的降级。假设 A 服务需要依赖 B、C、D 3 个服务,D 是旁路,可以把旁路依赖 D 的流量掐掉,使得释放的资源可以用于核心路径的计算,防止进一步过载。

稳定性策略——动态过载保护

熔断、限流、降级都是针对错误发生时的治理策略,其实最好的策略是防患于未然,也就是接下来要介绍的动态过载保护。

前面提到了限流策略很难确定阈值,一般是通过压测去观测一个节点能够承载的 QPS,但是这个上限量级可能会由于运行环境的不同,在不同节点上的表现也不同。动态过载保护就是基于这样一个事实:资源规格相同的服务节点,处理能力不一定相同。

如何实现动态过载保护?它分为三个部分:过载检测,过载处理,过载恢复。其中最关键的是如何判断一个 Server 节点是否过载。

上图中的 Ingress Proxy 是 Service Mesh 的数据面进程,它会代理流量并发往 Server 进程。图中的 T3 可以理解为从 Proxy 进程收到请求到 Server 处理完请求后返回的时间。这个时间是否可以用来判断过载?答案是不能,因为 Server 有可能依赖于其他节点。有可能是其他节点的处理时间变长了,导致 Server 的处理时间变长,这时 T3 并不能反映 Server 是处于过载的状态。

图中 T2 代表的是数据面进程把请求转发到 Server 后,Server 真正处理到它的时间间隔。T2 能否反映过载的状态?答案是可以的。为什么可以?举一个例子,假设 Server 的运行环境是一个 4 核 8g 的实例,这就决定了该 Server 最多只能同时处理 4 个请求。如果把 100 个请求打到该 Server,剩余的 96 个请求就会处于 pending 的状态。当 pending 的时间过长,我们就可以认为是过载了。

检测到 Server 过载之后应当如何进行处理?针对过载处理也有很多策略,我们采用的策略是根据请求的优先级主动 drop 低优的请求,以此来缓解 Server 过载的情况。当 drop 了一些流量后 Server 恢复了正常水平,我们就需要进行相应的过载恢复,使得 QPS 能够达到正常状态。

这个过程是如何体现动态性的?过载检测是一个实时的过程,它有一定的时间周期。在每一个周期内,当检测到 Server 是过载的状态,就可以慢慢根据一定比例 drop 一些低优请求。在下一个时间周期,如果检测到 Server 已经恢复了,又会慢慢调小 drop 的比例,使 Server 逐渐恢复。

动态过载保护的效果是非常明显的:它可以保证服务在大流量高压的情况下不会崩溃,该策略也广泛地应用于抖音春晚红包项目中的一些大服务。

稳定性策略——负载均衡

接下来我们看一下负载均衡策略。假设有一个服务 A 发出的流量要达到下游服务 B,A 和 B 都有一万个节点,我们如何保障从 A 出发的流量达到 B 中都是均衡的?做法其实有很多,比较常用的是随机轮询、加权虚机、加权轮询,这些策略其实看名字就能知道是什么意思了。

另一种比较常见的策略是一致性哈希。哈希是指根据请求的一些特征使得请求一定会路由到下游中的相同节点,将请求和节点建立起映射关系。一致性哈希策略主要应用于缓存敏感型服务,可以大大提升缓存的命中率,同时提升 Server 性能,降低超时的错误率。当服务中有一些新加入的节点,或者有一些节点不可用了,哈希的一致性可以尽可能少地影响已经建立起的映射关系。

还有很多其他的负载均衡策略,在生产场景中的应用范围并不是很广泛,这里不再赘述。

稳定性策略——节点分片

面对抖音春晚红包这种超大流量规模的场景,还有一个比较有用的策略是节点分片。节点分片基于这样一个事实:节点多的微服务,其长连接的复用率是非常低的。因为微服务一般是通过 TCP 协议进行通信,需要先建立起 TCP 连接,流量流转在 TCP 连接上。我们会尽可能地复用一个连接去发请求搜响应,以避免因频繁地进行连接、关闭连接造成的额外开销。

当节点规模非常大的时候,比如说 Service A 和 Service B 都有 1 万个节点,它们就需要维持非常多的长连接。为避免维持这么多长连接,通常会设置一个 idle timeout 的时间,当一个连接在一定的间隔内没有流量经过的时候,这个连接就会被关掉。在服务节点规模非常大的场景下,长连接退化成的短连接,会使得每一个请求都需要建立连接才能进行通讯。它带来的影响是:

  • 连接超时带来的错误。
  • 性能会有所降低。

解决这个问题可以使用节点分片的策略。实际上我们在抖音春晚红包的场景中也是非常广泛地使用了这个策略。这个策略对节点数较多的服务进行节点分片,然后建立起一种映射关系,使得如下图中所示的 A 服务的分片 0 发出的流量一定能到达 service B 的分片 0。

这样就可以大大提升长连接的复用率。对于原先 1000010000 的对应关系,现在就变成了一个常态的关系,比如 100100。我们通过节点分片的策略大大提升了长连接的复用率,降低了连接超时带来的错误,并且提升了微服务的性能。

效率策略

前面提到的限流、熔断、降级、动态过载保护、节点分片都是提升微服务稳定性相关的策略,还会有一些与效率相关的策略。

我们先介绍一下泳道和染色分流的概念。

上图中所示的某个功能可能涉及到 a、b、c、d、e、f 六个微服务。泳道可以对这些流量进行隔离,每一个泳道内完整地拥有这六个微服务,它们可以完整的完成一个功能。

染色分流是指根据某些规则使得流量打到不同的泳道,然后借此来完成一些功能,这些功能主要包括:

  • Feature 调试:在线上的开发测试过程中,可以把个人发出的一些请求打到自己设置的泳道并进行 Feature 调试。
  • 故障演练:在抖音春晚活动的一些服务开发完成之后,需要进行演练以对应对不同的故障。这时我们就可以把压测流量通过一些规则引流到故障演练的泳道上。
  • 流量录制回放:把某种规则下的流量录制下来,然后进行相关回放,主要用于 bug 调试或在某些黑产场景下发现问题。

安全策略

安全策略也是流量治理的重要环节。我们主要提供三种安全策略:

  • 授权:授权是指限定某一个服务能够被哪些服务调用。
  • 鉴权:当一个服务接收到流量时,需要鉴定流量来源的真实性。
  • 双向加密(mTLS) :为了防止流量内容被窥探、篡改或被攻击,需要使用双向加密。

通过以上的这些策略,我们提供了可靠的身份认证,安全地传输加密,还可以防止传输的流量内容被篡改或攻击。

春晚红包场景落地

通过前面提到的各种策略,我们可以大大提升微服务的稳定性以及业务研发的效率。但是当我们落地这一套架构的时候也会遇到一些挑战,最主要的挑战是性能问题。我们知道,通过增加一个中间层,虽然提升了扩展性和灵活性,但同时也必然有一些额外的开销,这个开销就是性能。在没有 Service Mesh 时,微服务框架的主要开销来自于序列化与反序列化、网络通讯、服务发现以及流量治理策略。使用了 Service Mesh 之后,会多出两种开销:

协议解析

对于数据面进程代理的流量,需要对流量的协议进行一定的解析才能知道它从哪来到哪去。但是协议解析本身的开销非常高,所以我们通过增加一个 header (key 和 value 的集合) 可以把流量的来源等服务元信息放到这个 header 里,这样只需要解析一两百字节的内容就可以完成相关的路由。

进程间通讯

数据面进程会代理业务进程的流量,通常是通过 iptables 的方式进行。这种方案的 overhead 非常高,所以我们采用了进程间通讯的方式,通过和微服务框架约定一个 unix domain socket 地址或者一个本地的端口,然后进行相关的流量劫持。虽然这种方式相对于 iptables 会有一些性能提升,它本身也存在的额外的一些开销。

我们是如何降低进程间通讯开销的呢?在传统的进程间通讯里,比如像 unix domain socket 或者本地的端口,会涉及到传输的内容在用户态到内核态的拷贝。比如请求转发给数据面进程会涉及到请求在用户态和内核态之间拷贝,数据面进程读出来的时候又会涉及内核态到用户态的拷贝,那么一来一回就会涉及到多达 4 次的内存拷贝。

我们的解决方案是通过共享内存来完成的。共享内存是 Linux 下最高性能的一种进程间通讯方式,但是它没有相关的通知机制。当我们把请求放到共享内存之后,另外一个进程并不知道有请求放了进来。所以我们需要引入一些事件通知的机制,让数据面进程知道。我们通过 unix domain socket 完成了这样一个过程,它的效果是可以减少内存的拷贝开销。同时我们在共享内存中引用了一个队列,这个队列可以批量收割 IO,从而减少了系统的调用。它起到的效果也是非常明显的,在抖音春晚活动的一些风控场景下,性能可以提高 24%。

完成这些优化之后,要去落地的阻力就没那么大了。

总结

本次分享主要为大家介绍了 Service Mesh 技术能够提供哪些流量治理能力来保证微服务的稳定和安全。主要包括三个核心点:

  • 稳定:面对瞬时亿级 QPS 的流量洪峰, 通过 Service Mesh 提供的流量治理技术,保证微服务的稳定性。
  • 安全:通过 Service Mesh 提供的安全策略,保证服务之间的流量是安全可信的。
  • 高效:春晚活动涉及众多不同编程语言编写的微服务,Service Mesh 天然为这些微服务提供了统一的流量治理能力,提升了开发人员的研发效率。

Q&A

Q:共享内存中的 IPC 通信为什么能够减少系统调用?

A:当客户端进程把一个请求放到共享内存中之后,我们需要通知 Server 进程进行处理,会有一个唤醒的操作,每次唤醒意味着一个系统调用。当 Server 还没有被唤醒的时候,或者它正在处理请求时,下一个请求到来了,就不需要再执行相同的唤醒操作,这样就使得在请求密集型的场景下我们不需要去频繁的唤醒,从而起到降低系统调用的效果。

Q:自研 Service Mesh 实现是纯自研还是基于 Istio 等社区产品?如果是自研使用的是 Go 还是 Java 语言?数据面用的是 Envoy 么?流量劫持用的 iptables 么?

A:

  1. 数据面是基于 Envoy 进行二次开发的,语言使用 C++。
  2. 流量劫持用与微服务框架约定好的的 uds 或者本地端口,不用 iptables。
  3. Ingess Proxy 和业务进程部署在同样的运行环境里,发布升级不需要重启容器。

本文转载自: 掘金

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

一个 println 竟然比 volatile 还好使?

发表于 2021-06-02

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

先点赞再看,养成好习惯

前两天一个小伙伴突然找我求助,说准备换个坑,最近在系统学习多线程知识,但遇到了一个刷新认知的问题……

小伙伴:Effective JAVA 里的并发章节里,有一段关于可见性的描述。下面这段代码会出现死循环,这个我能理解,JMM 内存模型嘛,JMM 不保证 stopRequested 的修改能被及时的观测到。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
java复制代码static boolean stopRequested = false;

public static void main(String[] args) throws InterruptedException {

Thread backgroundThread = new Thread(() -> {
int i = 0;
while (!stopRequested) {
i++;
}
}) ;
backgroundThread.start();
TimeUnit.MICROSECONDS.sleep(10);
stopRequested = true ;
}

但奇怪的是在我加了一行打印之后,就不会出现死循环了!难道我一行 println 能比 volatile 还好使啊?这俩也没关系啊

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
java复制代码static boolean stopRequested = false;

public static void main(String[] args) throws InterruptedException {

Thread backgroundThread = new Thread(() -> {
int i = 0;
while (!stopRequested) {

// 加上一行打印,循环就能退出了!
System.out.println(i++);
}
}) ;
backgroundThread.start();
TimeUnit.MICROSECONDS.sleep(10);
stopRequested = true ;
}

我:小伙子八股文背的挺熟啊,JMM 张口就来。
​

我:这个……其实是 JIT 干的好事,导致你的循环无法退出。JMM 只是一个逻辑上的内存模型,内部有些机制是和 JIT 有关的

比如你第一个例子里,你用-Xint禁用 JIT,就可以退出死循环了,不信你试试?

小伙伴:卧槽,真的可以,加上 -Xint 循环就退出了,好神奇!JIT 是个啥啊?还能有这种功效?

image.png

JIT(Just-in-Time) 的优化

众所周知,JAVA 为了实现跨平台,增加了一层 JVM,不同平台的 JVM 负责解释执行字节码文件。虽然有一层解释会影响效率,但好处是跨平台,字节码文件是平台无关的。
image.png
在 JAVA 1.2 之后,增加了 即时编译(Just-in-Time Compilation,简称 JIT) 的机制,在运行时可以将执行次数较多的热点代码编译为机器码,这样就不需要 JVM 再解释一遍了,可以直接执行,增加运行效率。
image.png
​

但 JIT 编译器在编译字节码时,可不仅仅是简单的直接将字节码翻译成机器码,它在编译的同时还会做很多优化,比如循环展开、方法内联等等……
​

这个问题出现的原因,就是因为 JIT 编译器的优化技术之一 - 表达式提升(expression hoisting) 导致的。

表达式提升(expression hoisting)

先来看个例子,在这个 hoisting 方法中,for 循环里每次都会定义一个变量 y,然后通过将 x*y 的结果存储在一个 result 变量中,然后使用这个变量进行各种操作

1
2
3
4
5
6
7
8
9
java复制代码public void hoisting(int x) {
for (int i = 0; i < 1000; i = i + 1) {
// 循环不变的计算
int y = 654;
int result = x * y;

// ...... 基于这个 result 变量的各种操作
}
}

但是这个例子里,result 的结果是固定的,并不会跟着循环而更新。所以完全可以将 result 的计算提取到循环之外,这样就不用每次计算了。JIT 分析后会对这段代码进行优化,进行表达式提升的操作:

1
2
3
4
5
6
7
8
java复制代码public void hoisting(int x) {
int y = 654;
int result = x * y;

for (int i = 0; i < 1000; i = i + 1) {
// ...... 基于这个 result 变量的各种操作
}
}

这样一来,result 不用每次计算了,而且也完全不影响执行结果,大大提升了执行效率。

注意,编译器更喜欢局部变量,而不是静态变量或者成员变量;因为静态变量是“逃逸在外的”,多个线程都可以访问到,而局部变量是线程私有的,不会被其他线程访问和修改。
​

编译器在处理静态变量/成员变量时,会比较保守,不会轻易优化。
​

像你问题里的这个例子中,stopRequested就是个静态变量,编译器本不应该对其进行优化处理;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
java复制代码static boolean stopRequested = false;// 静态变量

public static void main(String[] args) throws InterruptedException {

Thread backgroundThread = new Thread(() -> {
int i = 0;
while (!stopRequested) {
// leaf method
i++;
}
}) ;
backgroundThread.start();
TimeUnit.MICROSECONDS.sleep(10);
stopRequested = true ;
}

但由于你这个循环是个 leaf method,即没有调用任何方法,所以在循环之中不会有其他线程会观测到stopRequested值的变化。那么编译器就冒进的进行了表达式提升的操作,将stopRequested提升到表达式之外,作为循环不变量(loop invariant)处理:

1
2
3
4
5
6
java复制代码int i = 0;

boolean hoistedStopRequested = stopRequested;// 将stopRequested 提升为局部变量
while (!hoistedStopRequested) {
i++;
}

这样一来,最后将 stopRequested赋值为 true 的操作,影响不了提升的hoistedStopRequested的值,自然就无法影响循环的执行了,最终导致无法退出。
​

至于你增加了 println 之后,循环就可以退出的问题。是因为你这行 println 代码影响了编译器的优化。println 方法由于最终会调用 FileOutputStream.writeBytes 这个 native 方法,所以无法被内联优化(inling)。而未被内敛的方法调用从编译器的角度看是一个“full memory kill”,也就是说 副作用不明 、必须对内存的读写操作做保守处理。
​

在这个例子里,下一轮循环的 stopRequested 读取操作按顺序要发生在上一轮循环的 println 之后。这里“保守处理”为:就算上一轮我已经读取了 stopRequested 的值,由于经过了一个副作用不明的地方,再到下一次访问就必须重新读取了。
​

所以在你增加了 prinltln 之后,JIT 由于要保守处理,重新读取,自然就不能做上面的表达式提升优化了。
​

以上对表达式提升的解释,总结摘抄自 R大的知乎回答。R大,行走的 JVM Wiki!
​

我:“这下明白了吧,这都是 JIT 干的好事,你要是禁用 JIT 就没这问题了”

小伙伴:“卧槽🐂🍺,一个简单的 for 循环也太多机制了,没想到 JIT 这么智能,也没想到 R 大这么🐂🍺”

小伙伴:“那 JIT 一定很多优化机制吧,除了这个表达式提升还有啥?”

我:我也不是搞编译器的……哪了解这么多,就知道一些常用的,简单给你说说吧

表达式下沉(expression sinking)

和表达式提升类似的,还有个表达式下沉的优化,比如下面这段代码:

1
2
3
4
5
6
7
8
9
java复制代码public void sinking(int i) {
int result = 543 * i;

if (i % 2 == 0) {
// 使用 result 值的一些逻辑代码
} else {
// 一些不使用 result 的值的逻辑代码
}
}

由于在 else 分支里,并没有使用 result 的值,可每次不管什么分支都会先计算 result,这就没必要了。JIT 会把 result 的计算表达式移动到 if 分支里,这样就避免了每次对 result 的计算,这个操作就叫表达式下沉:

1
2
3
4
5
6
7
8
java复制代码public void sinking(int i) {
if (i % 2 == 0) {
int result = 543 * i;
// 使用 result 值的一些逻辑代码
} else {
// 一些不使用 result 的值的逻辑代码
}
}

JIT 还有那些常见优化?

除了上面介绍的表达式提升/表达式下沉以外,还有一些常见的编译器优化机制。

循环展开(Loop unwinding/loop unrolling)

下面这个 for 循环,一共要循环 10w 次,每次都需要检查条件。

1
2
3
java复制代码for (int i = 0; i < 100000; i++) {
delete(i);
}

在编译器的优化后,会删除一定的循环次数,从而降低索引递增和条件检查操作而引起的开销:

1
2
3
4
5
6
7
java复制代码for (int i = 0; i < 20000; i+=5) {
delete(i);
delete(i + 1);
delete(i + 2);
delete(i + 3);
delete(i + 4);
}

除了循环展开,循环还有一些优化机制,比如循环剥离、循环交换、循环分裂、循环合并……

内联优化(Inling)

JVM 的方法调用是个栈的模型,每次方法调用都需要一个压栈(push)和出栈(pop)的操作,编译器也会对调用模型进行优化,将一些方法的调用进行内联。
​

内联就是抽取要调用的方法体代码,到当前方法中直接执行,这样就可以避免一次压栈出栈的操作,提升执行效率。比如下面这个方法:

1
2
3
4
5
6
7
8
9
10
11
java复制代码public  void inline(){
int a = 5;
int b = 10;
int c = calculate(a, b);

// 使用 c 处理……
}

public int calculate(int a, int b){
return a + b;
}

在编译器内联优化后,会将 calculate 的方法体抽取到 inline 方法中,直接执行,而不用进行方法调用:

1
2
3
4
5
6
7
java复制代码public  void inline(){
int a = 5;
int b = 10;
int c = a + b;

// 使用 c 处理……
}

不过这个内联优化是有一些限制的,比如 native 的方法就不能内联优化

提前置空

来先看一个例子,在这个例子中 was finalized! 会在 done.之前输出,这个也是因为 JIT 的优化导致的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
java复制代码class A {
// 对象被回收前,会触发 finalize
@Override protected void finalize() {
System.out.println(this + " was finalized!");
}

public static void main(String[] args) throws InterruptedException {
A a = new A();
System.out.println("Created " + a);
for (int i = 0; i < 1_000_000_000; i++) {
if (i % 1_000_00 == 0)
System.gc();
}
System.out.println("done.");
}
}

//打印结果
Created A@1be6f5c3
A@1be6f5c3 was finalized!//finalize方法输出
done.

从例子中可以看到,如果 a 在循环完成后已经不再使用了,则会出现先执行finalize的情况;虽然从对象作用域来说,方法没有执行完,栈帧并没有出栈,但是还是会被提前执行。
​

这就是因为 JIT 认为 a 对象在循环内和循环后都不会在使用,所以提前给它置空了,帮助 GC 回收;如果禁用 JIT,那就不会出现这个问题……
​

这个提前回收的机制,还是有点风险的,在某些场景下会引起 BUG,比如《一个JDK线程池BUG引发的GC机制思考》

HotSpot VM JIT 的各种优化项

上面只是介绍了几个简单常用的编译优化机制,JVM JIT 更多的优化机制可以参考下面这个图。这是 OpenJDK 文档中提供的一个 pdf 材料,里面列出了 HotSpot JVM 的各种优化机制,相当多……
image.png

如何避免因 JIT 导致的问题?

小伙伴:“JIT 这么多优化机制,很容易出问题啊,我平时写代码要怎么避开这些呢”

​

平时在编码的时候,不用刻意的去关心 JIT 的优化,就比如上面那个 println 问题,JMM 本来就不保证修改对其他线程可见,如果按照规范去加锁或者用 volatile 修饰,根本就不会有这种问题。
​

而那个提前置空导致的问题,出现的几率也很低,只要你规范写代码基本不会遇到的。

我:所以,这不是 JIT 的锅,是你的……

小伙伴:“懂了,你这是说我菜,说我代码写的屎啊……”

总结

在日常编码过程中,不用刻意的猜测 JIT 的优化机制,JVM 也不会完整的告诉你所有的优化。而且这种东西不同版本效果不一样,就算搞明白了一个机制,可能到下个版本就完全不一样了。
​

所以,如果不是搞编译器开发的话,JIT 相关的编译知识,作为一个知识储备就好。

也不用去猜测 JIT 到底会怎么优化你的代码,你(可能)猜不准……

参考

  • JSR-133 Java Memory Model and Thread Specification 1.0 Proposed Final Draft
  • Oracle JVM Just-in-Time Compiler (JIT)
  • JVM JIT-compiler overview - Vladimir Ivanov HotSpot JVM Compiler Oracle Corp.
  • JVM JIT optimization techniques - part 2
  • The Java platform - WikiBook
  • R 大的知乎百科

原创不易,禁止未授权的转载。如果我的文章对您有帮助,就请点赞/收藏/关注鼓励支持一下吧❤❤❤❤❤❤

一点补充

可能部分读者大佬们会认为是 sync 导致的问题,下面是稍加改造后的 sync 例子,结果是仍然无法退出死循环……

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 class HoistingTest {
static boolean stopRequested = false;

public static void main(String[] args) throws InterruptedException {
Thread backgroundThread = new Thread(() -> {
int i = 0;
while (!stopRequested) {

// 加上一行打印,循环就能退出了!
// System.out.println(i++);
new HoistingTest().test();
}
}) ;
backgroundThread.start();
TimeUnit.SECONDS.sleep(5);
stopRequested = true ;
}

Object lock = new Object();

private void test(){

synchronized (lock){}
}
}

再升级下,把 test 方法,也加上 sync,结果还是无法退出死循环……

1
2
3
4
5
6
java复制代码Object lock = new Object();

private synchronized void test(){

synchronized (lock){}
}

但我只是想说,这个问题的关键是 jit 的优化导致的问题。jit 的优化机制,也是 jmm 的一部分,jmm 只是规范, 而 jit 是 vm 实现中的一个机制,它也会遵循 jmm 的规范。

不过 jmm 并没有说 sync 会影响 jit 之类的,可就算 sync 会影响那又怎么样呢……并不是关键点

结合 R大 的解释,编译器对静态变量更敏感,如果把上面的 lock 对象修改成 static 的,循环又可以退出了……

那如果不加 static ,把 sync 换成 unsafe.pageSize()呢?结果是循环还是可以退出……

所以,本文的重点是描述 jit 的影响,而不是各种会影响 jit 的动作。影响 jit 的可能性会非常多,而且不同的vm甚至不同的版本表现都会有所不同,我们并不需要去摸清这个机制,也没法摸清(毕竟不是做编译器的,就是是做编译器,也不一定是 HotSpot……)

本文转载自: 掘金

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

Spring 优雅注册 Bean 的方式

发表于 2021-06-01

1、Spring 注册 Bean

这篇先说明用法,下篇分析以下场景是如何将 Bean 注册进 IOC容器的。

1.1、使用 @Bean 注解

这种用法在项目中是非常常见的,基本上是必有。我们来看下用法:

1
2
3
4
5
6
7
8
9
typescript复制代码@Configuration
public class TestConfig {

@Bean
public TestBean testBean(){
return new TestBean();
}
public static class TestBean{}
}

这样一个 Bean 就注册进 IOC 容器了,Bean 的名称默认是方法名,并且是不会转换大小写的,也就是假如你的方法名是 TestBean() ,那么 Bean 的名称就是 TestBean 。当然我们也可以使用 name 或者 value 指定 Bean 的名称,比如 @Bean(value = "testBean"),如果二者同时存在则会报错。

我们来看下其他属性:

autowireCandidate:默认值是 true 。如果设置为 false 的话,那么通过 byType 的方式获取 Bean 就会报错,当然我们可以使用 Resource 注解获取。

initMethod:在 Bean 实例化后调用的初始化方法,值是 Bean 类中的方法名。

destroyMethod:在 Bean 要销毁时调用的清理方法,值是 Bean 类中的方法名。

@Bean 注解只能定义在 @Configuration 类下吗? NO NO NO,它可以定义在任意能被 IOC 扫描的注解下,比如 @Component注解,至于区别,下篇再讲。

1.2、使用 @ComponentScan 注解组件扫描

先讲普通用法:

1
2
3
4
less复制代码@ComponentScan(basePackages = "com.rookie.spring.source.run.component")
@Configuration
public class TestConfig {
}

使用 @ComponentScan 组件扫描方式,它会扫描指定包下(包括子包)下的所有类,只要包含了 @Component、@Configuration 等 Spring 的声明注解,就会将 Bean 加入到 IOC 容器中。

深度用法:

ComponentScan 注解中有两个这样的属性:includeFilters 与 excludeFilters,前一个是只包含规则,后一个是排除包含规则,他们的值是一个 @Filter 注解的形式,Filter 中的 type 有 5 中类型,分别如下。

1、ANNOTATION

第一种是以注解的形式包含或不包含,比如:

1
2
3
less复制代码@ComponentScan(basePackages = "com.rookie.spring.source.run.component",
includeFilters = @ComponentScan.Filter(type = FilterType.ANNOTATION,classes = Configuration.class),
useDefaultFilters = false)

这里边要配置useDefaultFilters = false 禁用默认规则,因为默认规则是扫描所有,配只包含就没用了。这里的意思只扫描 Configuration 注解。

2、ASSIGNABLE_TYPE

这种是包含我们给定的类型,不管是给定的类型和子类都会被包含进 IOC 容器。

1
2
3
4
5
6
7
8
9
10
11
12
less复制代码public class TestBean1 extends TestBean {
}
public class TestBean {
}

@ComponentScan(basePackages = "com.rookie.spring.source.run.component",
includeFilters = {@ComponentScan.Filter(type = FilterType.ANNOTATION,classes = Configuration.class),
@ComponentScan.Filter(type = FilterType.ASSIGNABLE_TYPE,classes = TestBean.class)},
useDefaultFilters = false)
@Configuration
public class TestConfig {
}

然后我们发现 testBean 注册进去了,为什么我们不标注 @Component 这样的注解实例也会被注册进 IOC 呢?因为 ComponentScan 会扫描包下所有文件,只要符合我们定义的过滤规则,它就会将 Bean 注册进 IOC 容器中。

3、ASPECTJ

ASPECTJ 是使用 aspectj 表达式

4、REGEX

REGEX 是使用正则表达式

5、CUSTOM

这种呢就是我们 SpringBootApplication 注解用到的方式了,我来解释一下具体规则:这种方式是可以自己自定义扫描规则,它接受一个实现 TypeFilter 接口的类。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
java复制代码public class MyTypeFilter implements TypeFilter {
/**
*
* @param metadataReader 当前类的信息
* @param metadataReaderFactory 可以获取其他类的信息
* @return 匹配结果
* @throws IOException 异常
*/
@Override
public boolean match(MetadataReader metadataReader, MetadataReaderFactory metadataReaderFactory) throws IOException {
// 获取当前扫描类信息
ClassMetadata classMetadata = metadataReader.getClassMetadata();
return classMetadata.getClassName().contains("com.rookie.spring.source.run.component.TestBean");

}
}

@ComponentScan(basePackages = "com.rookie.spring.source.run.component",
includeFilters = {@ComponentScan.Filter(type = FilterType.CUSTOM,classes = MyTypeFilter.class)},
useDefaultFilters = false)
@Configuration
public class TestConfig {
}

当它扫描类的时候扫描到了 TestBean,然后符合了我的匹配规则(也就是返回true)就注册进去了。

1.3、使用 @Import 注解

下面的例子中,我们直接看 Spring 源码的实现比较具有代表性一点。

我们点进 @EnableTransactionManagement 注解中,发现了这个 @Import(TransactionManagementConfigurationSelector.class),它的作用就是将类导入,类会被注册进 IOC 容器中。

这个注解放置的位置要是 Spring 能扫描到的地方,不然 Spring 也不会主动去解析这个注解。

如果我们自己要使用注解的话,我们可以做个类似于 EnableTransactionManagement 的功能插拔式导入配置类,这样就可以实现动态开启一些 Bean 了。

1.4、实现 ImportSelector 接口

1
2
3
4
5
6
7
8
9
vbnet复制代码public interface ImportSelector {

/**
* Select and return the names of which class(es) should be imported based on
* the {@link AnnotationMetadata} of the importing @{@link Configuration} class.
*/
String[] selectImports(AnnotationMetadata importingClassMetadata);

}

我们还是来看下 TransactionManagementConfigurationSelector 这个类,看下它的继承关系发现它间接性的实现了 ImportSelector 接口,主要看它实现的这个方法:

1
2
3
4
5
6
7
8
9
10
11
12
typescript复制代码@Override
protected String[] selectImports(AdviceMode adviceMode) {
switch (adviceMode) {
case PROXY:
return new String[] {AutoProxyRegistrar.class.getName(),
ProxyTransactionManagementConfiguration.class.getName()};
case ASPECTJ:
return new String[] {determineTransactionAspectClass()};
default:
return null;
}
}

这个方法的作用就是根据你返回的类全限定名(org.springframework.context.annotation.AutoProxyRegistrar)数组来创建 Bean 。

实现了 ImportSelector 的类也是需要使用 @Import 导入。

1.5、实现 ImportBeanDefinitionRegistrar 接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
java复制代码public interface ImportBeanDefinitionRegistrar {

/**
* Register bean definitions as necessary based on the given annotation metadata of
* the importing {@code @Configuration} class.
* <p>Note that {@link BeanDefinitionRegistryPostProcessor} types may <em>not</em> be
* registered here, due to lifecycle constraints related to {@code @Configuration}
* class processing.
* @param importingClassMetadata annotation metadata of the importing class
* @param registry current bean definition registry
*/
void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry);

}

这个我们来看下 @MapperScan (org.mybatis.spring.annotation)导入的 MapperScannerRegistrar 发现它实现了 ImportBeanDefinitionRegistrar:

1
2
3
4
5
6
7
csharp复制代码public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
AnnotationAttributes mapperScanAttrs = AnnotationAttributes.fromMap(importingClassMetadata.getAnnotationAttributes(MapperScan.class.getName()));
if (mapperScanAttrs != null) {
this.registerBeanDefinitions(mapperScanAttrs, registry);
}

}

它的作用是拿到 BeanDefinitionRegistry Bean 的定义信息,然后往里面加 BeanDefinition 就会将相应的对象注册进去,它更深入的就不说了,实际上就是解析下注解属性,然后扫描相应的包下的类注册 Bean。我们自己搞个简单的。

1
arduino复制代码registry.registerBeanDefinition("testBean",new RootBeanDefinition(TestBean.class));

这样就注册了一个 Bean 名称是 testBean 类型是 TestBean 类型的 Bean 了。

如果注册的是一个有参构造器呢?那就这样:

1
2
3
ini复制代码BeanDefinitionBuilder beanDefinitionBuilder = BeanDefinitionBuilder.rootBeanDefinition(TestBean.class);
beanDefinitionBuilder.addConstructorArgValue(1);
registry.registerBeanDefinition("testBean",beanDefinitionBuilder.getBeanDefinition());

addConstructorArgValue 根据构造器参数的顺序去添加。

实现了 ImportBeanDefinitionRegistrar 的类也是需要使用 @Import 导入。

1.6、实现 FactoryBean 接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
java复制代码public class MyFactoryBean implements FactoryBean<TestBean> {

@Override
public TestBean getObject() throws Exception {
return new TestBean();
}

@Override
public Class<?> getObjectType() {
return TestBean.class;
}

@Override
public boolean isSingleton() {
return true;
}
}

@Import(MyFactoryBean.class)
@Configuration
public class TestConfig {
}

然后 TestBean 就注册进去了,打印的时候我们发现 Bean 的名称是 MyFactoryBean 的全限定名,但是它的类型是 TestBean 类型的,如果想要获取 MyFactoryBean 类型的 Bean 的话,通过 Bean 名称为 &myFactoryBean 就能获取到。

1.7、使用 spring.factories 配置

在我们的Spring Boot项目中,一般都是只扫描主类下的所有类,然后将一些被特定注解标注的类加载到IOC容器,但是如果我们将包分离,我们又如何更加方便的将其他包的类加载进来呢? spring boot提供了一种类似于Java的SPI(服务发现)机制spring.factories,只要在resources目录下创建META-INF文件夹,再创建 spring.factories文件,然后再里面配置

1
2
ini复制代码org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.jylcloud.jyl.common.commondcs.config.RedissonManagerConfig

这样在导入当前包的就会自动扫描spring.factories文件,解析后将里面的一些类加载到IOC容器中。具体的实现代码在spring-core的SpringFactoriesLoader类中。

1.8、使用 @Component、@Repository、@Service、@Controller、@RestController

这些就不讲了。

2、总结

就不总结了,看着用。还有其他注册 Bean 的方式放置在其他地方讲。

原文地址

本文转载自: 掘金

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

1…655656657…956

开发者博客

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