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

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


  • 首页

  • 归档

  • 搜索

【Terraform】一文了解特点、生命周期、基础语法

发表于 2021-10-15

小知识,大挑战!本文正在参与“程序员必备小知识”创作活动。

本文同时参与 「掘力星计划」 ,赢取创作大礼包,挑战创作激励金。

本文重点讲Terraform的特点和生命周期,以及基础语法。

一、显著特点

Terraform作为基础设施即代码的先驱者,有很多领先的思想,下面是它的一些主要特点。

  • 基于IaC(基础设施即代码,Infrastructure as Code)的设计,可以将基础设施以一种领域特定语言(HasiCorp的HCL/DCL)描述出来,消除了在基础设施自动化时描述语义上的歧义,同时减轻了人为因素造成的不确影响。
  • Terraform 在执行编排动作前,会生成一份可读性良好的执行计划,关键基础设施的变更可以得到充分审查,保证了基础设施的可靠性
  • 基于 DAG(有向无环图,Directed Acyclic Graph)描述资源与资源之间的关系,由于 DAG 良好的拓扑性质,当资源属性与资源关系发生改变时,变更动作将被充分并行地执行

二、生命周期

img

根据示例图,左侧的部分是 Terraform 本身提供的能力,右侧是由云厂商提供的能力。图上一共包含三个主要的线程,包括Terraform 核心进程、Provider 进程、Provisioner 进程。三个线程的能力分别如下:

  • Terraform 核心进程:负责资源定义文件,构建有向无环图,管理状态存储;
  • Provider 进程:即提供资源编排能力的进程,包括由云厂商实现的能力(比如 UCloud),和应用程序提供的能力(比如 TLS)等;
  • •Provisioner 进程:即提供资源编排后处理操作的进程,比如执行 Shell 命令,上传文件等;

三、基础语法规则

3.1 类型

原始类型(三种类型可隐式转换)

  • string

其代表一组Unicode字符串,例如:”hello”。

  • number

其代表数字,可以为整数,也可以为小数

  • bool

逻辑判断,true or false

复杂类型

集合类型

  • list(连续集合)
  • map(字典类型)
  • set(集合类型,代表一组不重复的值)

结构化类型

  • object(名称+属性)
+ 格式:object({age=number, name=string})
+ 例子:object({age=number,name=string}),赋值{ age=18, name="john" }
  • tuple(类似于list,但每个元素都可具有独立的类型)

特殊类型

  • any(占位符,可以用于赋值任何类型)
  • null(无类型,表示数据缺失)

3.2 配置语法

参数赋值

将一个值赋给一个特定的名称,例子:image_id=”abc123”

块

说明:一个块是包含一组其它内容的容器,块可以嵌套

  • 一个块类型可以规定任意多个标签,也可以没有标签
  • 内置了顶级块,可独立定义在配置文件中,例如resource, variable, output, data

img

注释

说明:注释掉不解释执行

  • 单行注释,其后的内容为注释,默认优先使用这个!
  • // 单行注释,其后的内容为注释
  • /* 和 */,多行注释,可以注释多行

编码格式

  • 配置文件必须始终使用UTF-8编码
  • 分隔符必须使用ASCII符号
  • 其他标识符、注释以及字符串字面量均可使用非ASCII字符

标识符

说明:参数名、块类型名以及其他Terraform规范中定义的结构的名称

  • 合法的标识符可以包含字母、数字、下划线(_)以及减号(-)
  • 标识符首字母不可以为数字

本文转载自: 掘金

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

微服务SpringCloud项目(九):初步整合gatewa

发表于 2021-10-15

小知识,大挑战!本文正在参与「程序员必备小知识」创作活动

本文已参与 「掘力星计划」 ,赢取创作大礼包,挑战创作激励金。

📖前言

1
复制代码心态好了,就没那么累了。心情好了,所见皆是明媚风景。

“一时解决不了的问题,那就利用这个契机,看清自己的局限性,对自己进行一场拨乱反正。”正如老话所说,一念放下,万般自在。如果你正被烦心事扰乱心神,不妨学会断舍离。断掉胡思乱想,社区垃圾情绪,离开负面能量。心态好了,就没那么累了。心情好了,所见皆是明媚风景。

🚓摘要


Spring Cloud Gateway为 SpringBoot 应用提供了 API 网关支持,具有强大的智能路由与过滤器功能,本文将对其用法进行详细介绍。

SpringCloudGateway 是 SpringCloud 新推出的网关框架,比较于上一代 Zuul,功能和性能有很大的提升。Zuul1.x 采用的是阻塞多线程方式,也就是一个线程处理一个连接请求,高并发情况下性能较差,即使是 Zuul2.x 虽然做到了非阻塞,但是面对连续跳票,看起来 Zuul 要被抛弃了。取而代之的是SpringCloudGateway,SpringCloudGateway 是基于 Webflux ,是一个非阻塞异步的框架,性能上有很大提升,而且包含了 Zuul 的所有功能,可以从 Zuul 无缝切换到 SpringCloudGateway


1. Gateway 简介

Gateway是在 Spring生态系统 之上构建的 API 网关服务,基于 Spring 5,Spring Boot 2和 Project Reactor 等技术。Gateway 旨在提供一种简单而有效的方式来对API 进行路由,以及提供一些强大的过滤器功能, 例如:熔断、限流、重试 等。

Spring Cloud Gateway 具有如下特性:

  • 基于 Spring Framework 5, Project Reactor 和 Spring Boot 2.0 进行构建;
  • 动态路由:能够匹配任何请求属性;
  • 可以对路由指定 Predicate(断言)和 Filter(过滤器);
  • 集成 Hystrix 的断路器功能;
  • 集成 Spring Cloud 服务发现功能;
  • 易于编写的 Predicate(断言)和 Filter(过滤器);
  • 请求限流功能;
  • 支持路径重写。

2. 相关概念

  • Route(路由):路由是构建网关的基本模块,它由ID,目标URI,一系列的断言和过滤器组成,如果断言为true则匹配该路由;
  • Predicate(断言):指的是 Java 8 的 Function Predicate。 输入类型是 Spring 框架中的 ServerWebExchange 。 这使开发人员可以匹配HTTP请求中的所有内容,例如请求头或请求参数。如果请求与断言相匹配,则进行路由;
  • Filter(过滤器):指的是Spring框架中 GatewayFilter 的实例,使用过滤器,可以在请求被路由前后对请求进行修改。

3. 引入 gateway 依赖

在 pom.xml 中添加相关依赖(引入 SpringCloudGateway 需要的 POM ,记得引入 actuator 组件,否则服务发现中心会认为服务不在线,导致网关无法路由到服务)

1
2
3
4
xml复制代码<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>

下面是我用到的依赖

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
xml复制代码<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>DreamChardonnayCloud</artifactId>
<groupId>com.cyj.dream</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<groupId>com.cyj.dream.gateway</groupId>
<artifactId>dream-gateway</artifactId>
<version>1.0-SNAPSHOT</version>
<name>dream-gateway</name>
<packaging>jar</packaging>
<description>网关</description>

<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
</properties>

<dependencies>

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

<dependency>
<groupId>de.codecentric</groupId>
<artifactId>spring-boot-admin-starter-client</artifactId>
<version>${admin-server.version}</version>
</dependency>

<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>

<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>

<!-- SpringBoot 监控客户端 -->
<dependency>
<groupId>de.codecentric</groupId>
<artifactId>spring-boot-admin-starter-client</artifactId>
<version>${spring-boot-admin.version}</version>
</dependency>

<!-- 引入gateway -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
<version>${gateway.version}</version>
</dependency>

<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-loadbalancer</artifactId>
<version>${gateway.version}</version>
</dependency>

<!-- 引入数据库密码加密 -->
<dependency>
<groupId>com.github.ulisesbocchio</groupId>
<artifactId>jasypt-spring-boot-starter</artifactId>
<version>${jasypt.version}</version>
</dependency>

<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
<version>${common-pool.version}</version>
</dependency>

<!-- 引入redis数据库依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
<!-- <exclusions>
<exclusions>
<exclusion>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
</exclusion>
<exclusion>
<groupId>io.lettuce</groupId>
<artifactId>lettuce-core</artifactId>
</exclusion>
</exclusions> -->
</dependency>

<!-- 引入验证码 -->
<dependency>
<groupId>com.cyj.dream.captcha</groupId>
<artifactId>dream-common-captcha</artifactId>
<version>1.0-SNAPSHOT</version>
<exclusions>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</exclusion>
</exclusions>
</dependency>

<!-- 引入核心core -->
<dependency>
<groupId>com.cyj.dream.core</groupId>
<artifactId>dream-common-core</artifactId>
<version>1.0-SNAPSHOT</version>
<!-- https://blog.csdn.net/qq_41686190/article/details/107280990 -->
<exclusions>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</exclusion>
</exclusions>
</dependency>

<dependency>
<groupId>com.cyj.dream.swagger</groupId>
<artifactId>dream-swagger</artifactId>
<version>1.0-SNAPSHOT</version>
<exclusions>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</exclusion>
</exclusions>
</dependency>

</dependencies>

<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<executions>
<execution>
<goals>
<goal>repackage</goal><!--可以把依赖的包都打包到生成的Jar包中 -->
</goals>
<!--可以生成不含依赖包的不可执行Jar包 -->
<configuration>
<classifier>exec</classifier>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>


</project>

4. 启动类如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
java复制代码package com.cyj.dream.gateway;

import cn.hutool.core.date.DateUtil;
import com.cyj.dream.swagger.annotation.EnableDreamSwagger2;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.WebApplicationType;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;

/**
* @Description: 网关gateway启动类
* @BelongsProject: DreamChardonnay
* @BelongsPackage: com.cyj.dream.gateway
* @Author: ChenYongJia
* @CreateTime: 2021-09-27
* @Email: chen87647213@163.com
* @Version: 1.0
*/
@Slf4j
@EnableDreamSwagger2
@EnableDiscoveryClient
@SpringBootApplication(exclude={DataSourceAutoConfiguration.class})
public class DreamGatewayApplication {

/**
* 项目启动方法
*
* @param args the input arguments
* @date 2021-9-26
* @author Sunny Chen
*/
public static void main(String[] args) {
log.info("梦享云--网关gateway开始启动ing!======>{}", DateUtil.now());
SpringApplication application = new SpringApplication(DreamGatewayApplication.class);
// 该设置方式
application.setWebApplicationType(WebApplicationType.REACTIVE);
application.run(args);
log.info("梦享云--网关gateway启动成功ing.......!======>{}", DateUtil.now());
}

}

5. yml 配置

这里我只展示关于 gateway 的配置其他配置请自行处理

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
yaml复制代码spring:
cloud:
gateway:
globalcors:
cors-configurations:
'[/**]':
allow-credentials: true
allowed-originPatterns: "*"
allowed-methods: "*"
allowed-headers: "*"
add-to-simple-url-handler-mapping: true
locator:
enabled: true
routes:
# 认证中心
- id: dream-auth
uri: lb://dream-auth
predicates:
- Path=/auth/**
filters:
# 验证码处理
- ValidateCodeGatewayFilter
# 前端密码解密
# - PasswordDecoderFilter
# 代码生成模块
- id: dream-codegen
uri: lb://dream-codegen
predicates:
- Path=/dsconf/**
# 文件管理模块
- id: dream-file-management
uri: lb://dream-file-management
predicates:
- Path=/file/**
# springboot2.X版本需要如下配置,设置大文件处理的大小
servlet:
multipart:
# 1GB
max-file-size: 1024MB
max-request-size: 1024MB
# 允许覆盖bean定义
main:
allow-bean-definition-overriding: true
# redis 配置
redis:
# redis服务地址
host:
# Redis服务器连接端口
port:
# Redis服务器连接密码(默认为空)
password:
# 使用的库
database: 0
lettuce:
pool:
# 连接池最大连接数(使用负值表示没有限制)
max-active: 300
# 连接池最大阻塞等待时间(使用负值表示没有限制)
max-wait: 300ms
# 连接池中的最大空闲连接
max-idle: 16
# 连接池中的最小空闲连接
min-idle: 8
# 连接超时时间(毫秒)
timeout: 60000

gateway:
encode-key: 'thanks,dreamChardonnay'
ignore-clients:
# - test2
allow-paths:
- /dreamAuth/**
- /auth/**
- /oauth/**
- /login
- /v2/**
- /allowFile/**
- /sse/**
- /form/**

6. 路由配置信息

我在路由里加了图片验证操作,各位可以自行查找添加进来

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
java复制代码/**
* @Description: 路由配置信息
* @BelongsProject: DreamChardonnay
* @BelongsPackage: com.cyj.dream.gateway.config
* @Author: ChenYongJia
* @CreateTime: 2021-09-27 12:50
* @Email: chen87647213@163.com
* @Version: 1.0
*/
@Slf4j
@Configuration(proxyBeanMethods = false)
@RequiredArgsConstructor
public class RouterFunctionConfiguration {

private final ImageCodeHandler imageCodeHandler;

@Bean
public RouterFunction routerFunction() {
return RouterFunctions.route(
RequestPredicates.path("/code").and(RequestPredicates.accept(MediaType.TEXT_PLAIN)), imageCodeHandler);
}

}

7. 路由限流

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
java复制代码/**
* @Description: 路由限流
* @BelongsProject: DreamChardonnay
* @BelongsPackage: com.cyj.dream.gateway.config
* @Author: ChenYongJia
* @CreateTime: 2021-09-27
* @Email: chen87647213@163.com
* @Version: 1.0
*/
@Configuration(proxyBeanMethods = false)
public class RateLimiterConfiguration {

@Bean(value = "remoteAddrKeyResolver")
public KeyResolver remoteAddrKeyResolver() {
return exchange -> Mono.just(exchange.getRequest().getRemoteAddress().getAddress().getHostAddress());
}

}

8. 网关配置

GatewayConfigProperties.java 网关配置文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
java复制代码/**
* @Description: 网关配置文件
* @BelongsProject: DreamChardonnay
* @BelongsPackage: com.cyj.dream.gateway.config
* @Author: ChenYongJia
* @CreateTime: 2021-09-27
* @Email: chen87647213@163.com
* @Version: 1.0
*/
@Data
@RefreshScope
@ConfigurationProperties("gateway")
public class GatewayConfigProperties {

/**
* 网关解密登录前端密码 秘钥 {@link PasswordDecoderFilter}
*/
public String encodeKey;

/**
* 权限放行的地址
*/
private List<String> allowPaths;

}

GatewayConfiguration.java 网关配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
java复制代码/**
* @Description: 网关配置
* @BelongsProject: DreamChardonnay
* @BelongsPackage: com.cyj.dream.gateway.config
* @Author: ChenYongJia
* @CreateTime: 2021-09-27
* @Email: chen87647213@163.com
* @Version: 1.0
*/
@Configuration(proxyBeanMethods = false)
@EnableConfigurationProperties(GatewayConfigProperties.class)
public class GatewayConfiguration {

@Bean
public PasswordDecoderFilter passwordDecoderFilter(GatewayConfigProperties configProperties) {
return new PasswordDecoderFilter(configProperties);
}

@Bean
public GlobalExceptionHandler globalExceptionHandler(ObjectMapper objectMapper) {
return new GlobalExceptionHandler(objectMapper);
}

}

8. filter拦截器配置

PasswordDecoderFilter 密码相关

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
java复制代码/**
* @Description: 密码解密过滤器
* @BelongsProject: DreamChardonnay
* @BelongsPackage: com.cyj.dream.gateway.filter
* @Author: ChenYongJia
* @CreateTime: 2021-09-27
* @Email: chen87647213@163.com
* @Version: 1.0
*/
@Slf4j
@RequiredArgsConstructor
public class PasswordDecoderFilter extends AbstractGatewayFilterFactory {

private static final String PASSWORD = "password";

private static final String QRCODE = "QRCode";

private static final String KEY_ALGORITHM = "AES";

private final GatewayConfigProperties configProperties;

private static String decryptAES(String data, String pass) {
AES aes = new AES(Mode.CBC, Padding.NoPadding, new SecretKeySpec(pass.getBytes(), KEY_ALGORITHM),
new IvParameterSpec(pass.getBytes()));
byte[] result = aes.decrypt(Base64.decode(data.getBytes(StandardCharsets.UTF_8)));
return new String(result, StandardCharsets.UTF_8);
}

@Override
public GatewayFilter apply(Object config) {
return (exchange, chain) -> {
ServerHttpRequest request = exchange.getRequest();

// 不是登录请求,直接向下执行
if (!StrUtil.containsAnyIgnoreCase(request.getURI().getPath(), SecurityConstants.OAUTH_TOKEN_URL)) {
return chain.filter(exchange);
}

URI uri = exchange.getRequest().getURI();
String queryParam = uri.getRawQuery();
Map<String, String> paramMap = HttpUtil.decodeParamMap(queryParam, CharsetUtil.CHARSET_UTF_8);
//解析密码
String password = paramMap.get(PASSWORD);
if (StrUtil.isNotBlank(password)) {
try {
password = decryptAES(password, configProperties.getEncodeKey());
}
catch (Exception e) {
log.error("密码解密失败:{}", password);
return Mono.error(e);
}
paramMap.put(PASSWORD, password.trim());
}
//解析QRCode
String QRCode= paramMap.get(QRCODE);
if (StrUtil.isNotBlank(QRCode)) {
try {
QRCode = decryptAES(QRCode, configProperties.getEncodeKey());
}
catch (Exception e) {
log.error("QRCode解密失败:{}", QRCode);
return Mono.error(e);
}
paramMap.put(QRCODE, QRCode.trim());
}

URI newUri = UriComponentsBuilder.fromUri(uri).replaceQuery(HttpUtil.toParams(paramMap)).build(true)
.toUri();

ServerHttpRequest newRequest = exchange.getRequest().mutate().uri(newUri).build();
return chain.filter(exchange.mutate().request(newRequest).build());
};
}

}

全局拦截器,作用所有的微服务

  • 对请求头中参数进行处理 from 参数进行清洗
  • 重写StripPrefix = 1,支持全局
  • 支持swagger添加X-Forwarded-Prefix header (F SR2 已经支持,不需要自己维护)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
java复制代码package com.cyj.dream.gateway.filter;

import com.cyj.dream.core.constant.SecurityConstants;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.util.StringUtils;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;

import java.util.Arrays;
import java.util.stream.Collectors;

import static org.springframework.cloud.gateway.support.ServerWebExchangeUtils.GATEWAY_REQUEST_URL_ATTR;
import static org.springframework.cloud.gateway.support.ServerWebExchangeUtils.addOriginalRequestUrl;

/**
* @Description: 全局拦截器,作用所有的微服务
* @BelongsProject: DreamChardonnay
* @BelongsPackage: com.cyj.dream.gateway.filter
* @Author: ChenYongJia
* @CreateTime: 2021-09-27
* @Email: chen87647213@163.com
* @Version: 1.0
*/
public class RequestGlobalFilter implements GlobalFilter, Ordered {

/**
* Process the Web request and (optionally) delegate to the next {@code WebFilter}
* through the given {@link GatewayFilterChain}.
* @param exchange the current server exchange
* @param chain provides a way to delegate to the next filter
* @return {@code Mono<Void>} to indicate when request processing is complete
*/
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
// 1. 清洗请求头中from 参数
ServerHttpRequest request = exchange.getRequest().mutate()
.headers(httpHeaders -> httpHeaders.remove(SecurityConstants.FROM)).build();

// 2. 重写StripPrefix
addOriginalRequestUrl(exchange, request.getURI());
String rawPath = request.getURI().getRawPath();
String newPath = "/" + Arrays.stream(StringUtils.tokenizeToStringArray(rawPath, "/")).skip(1L)
.collect(Collectors.joining("/"));
ServerHttpRequest newRequest = request.mutate().path(newPath).build();
exchange.getAttributes().put(GATEWAY_REQUEST_URL_ATTR, newRequest.getURI());

return chain.filter(exchange.mutate().request(newRequest.mutate().build()).build());
}

@Override
public int getOrder() {
return -1000;
}
}

token 验证过滤器

注意如果线上使用看下代码把本地注解注掉

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
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
java复制代码/**
* @Description: token验证过滤器
* @BelongsProject: DreamChardonnay
* @BelongsPackage: com.cyj.dream.gateway.filter
* @Author: ChenYongJia
* @CreateTime: 2021-09-27
* @Email: chen87647213@163.com
* @Version: 1.0
*/
@Component
@RefreshScope
public class TokenFilter implements GlobalFilter, Ordered {
@Resource
private RedisTemplate redisTemplate;
@Autowired
private ObjectMapper objectMapper;
@Resource
private GatewayConfigProperties gatewayConfigProperties;
/**
* 可以用于验证用户登录状态,设置某部分请求信息等。
*/
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
ServerHttpRequest request = exchange.getRequest();
ServerHttpResponse response = exchange.getResponse();
String apiPath=request.getPath().toString();

// 如果不是GET请求,判断是否已经登录,token是否过期
// 获取cookie中的token令牌
HttpHeaders headers = exchange.getRequest().getHeaders();
// List<HttpCookie> cookies = multiValueMap.get("token");

if ( headers.size() != 0) {
String token = headers.getFirst("Authorization");
//token 不为空 放行
/*if (StrUtil.isNotBlank(token)) {
return chain.filter(exchange);
}else{
if(this.isAllow(apiPath)){
return chain.filter(exchange);
}
}*/
// 本地不需要token
return chain.filter(exchange);
}
return response.writeWith(Mono.create(monoSink -> {
try {
byte[] bytes = objectMapper.writeValueAsBytes(ResponseUtil.error("登录过期请先登录"));
DataBuffer dataBuffer = response.bufferFactory().wrap(bytes);

monoSink.success(dataBuffer);
} catch (JsonProcessingException jsonProcessingException) {
monoSink.error(jsonProcessingException);
}
}));
}

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

/**
* 正则匹配地址
*
* @param path
* @return
*/
private boolean isAllow(String path) {
Pattern pattern = null;
String orginalUrl = path.split("[?]")[0];
for (String regex : getRegexList()) {
pattern = Pattern.compile(regex, Pattern.CASE_INSENSITIVE);
if (pattern.matcher(orginalUrl).matches()) {
return true;
}
}
return false;
}

/**
* 获取正则匹配规则list
*
* @return
*/
private List<String> getRegexList() {
//第一次加载这个,以及配置修改的时候加载这个(这个还没法实现,只能每次都加载)
List<String> regexList=null;
Object regexListJson = redisTemplate.opsForValue().get("regexList");
if (regexListJson == null) {
regexList = new ArrayList<String>();
}else{
regexList = JSON.parseObject((String)regexListJson,new TypeReference<ArrayList<String>>() {
});
return regexList;
}
for (String url : gatewayConfigProperties.getAllowPaths()) {
StringBuilder regex = new StringBuilder("\\S*").append(url.replace("/**", "\\S*")).append("\\S*");
regexList.add(regex.toString());
}
redisTemplate.opsForValue().set("regexList",JSON.toJSONString(regexList));
return regexList;
}

}

9. 处理器

网关异常通用处理器,只作用在 webflux 环境下 , 优先级低于{@link ResponseStatusExceptionHandler}执行

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
java复制代码/**
* @Description: 网关异常通用处理器,只作用在webflux 环境下 , 优先级低于 {@link ResponseStatusExceptionHandler} 执行
* @BelongsProject: DreamChardonnay
* @BelongsPackage: com.cyj.dream.gateway.handler
* @Author: ChenYongJia
* @CreateTime: 2021-09-27
* @Email: chen87647213@163.com
* @Version: 1.0
*/
@Slf4j
@Order(-1)
@RequiredArgsConstructor
public class GlobalExceptionHandler implements ErrorWebExceptionHandler {

private final ObjectMapper objectMapper;

@Override
public Mono<Void> handle(ServerWebExchange exchange, Throwable ex) {
ServerHttpResponse response = exchange.getResponse();

if (response.isCommitted()) {
return Mono.error(ex);
}

// header set
response.getHeaders().setContentType(MediaType.APPLICATION_JSON);
if (ex instanceof ResponseStatusException) {
response.setStatusCode(((ResponseStatusException) ex).getStatus());
}

return response.writeWith(Mono.fromSupplier(() -> {
DataBufferFactory bufferFactory = response.bufferFactory();
try {
return bufferFactory.wrap(objectMapper.writeValueAsBytes(ResponseUtil.error(ex.getMessage())));
}
catch (JsonProcessingException e) {
log.error("Error writing response", ex);
return bufferFactory.wrap(new byte[0]);
}
}));
}

}

10. 简单说下–结果我就不验证了

这次的代码,涉及到了比较多的引入,我就没有一一放出来给到大家,如果想看源码就等等我发到 git 吧,实际使用需要你去组合借鉴,切忌复制粘贴哦!!!!!!


PS:最近输出的文章以实用耐造为主,希望对你有所帮助而不是长篇大论全是理论实战弱鸡,最后感谢大家耐心观看完毕,留个点赞收藏是您对我最大的鼓励!


🎉总结:

  • 更多参考精彩博文请看这里:《陈永佳的博客》
  • 喜欢博主的小伙伴可以加个关注、点个赞哦,持续更新嘿嘿!

本文转载自: 掘金

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

这几种Java异常处理方法,你会吗?

发表于 2021-10-15

​​摘要:我们在软件开发的过程中,任何语言的开发过程中都离不开异常处理。

本文分享自华为云社区《Java异常处理学习总结》,作者: zekelove 。

我们在软件开发的过程中,任何语言的开发过程中都离不开异常处理。如果不处理异常,将会导致软件异常中断,崩溃,退出,严重影响用户的使用和体验。如果合理的应用异常处理那将会减少软件出现的错误,可以友好的提示用户,提升用户的体验。

异常是什么

异常是程序中的一些错误,但并不是所有的错误都是异常,并且错误有时候是可以避免的。如:用户输入非法数据,做除法运算的时候除数为0,语句少写分号,打开的文件不存在,内存溢出,网络中断等。

异常处理机制能让程序在异常发生时,按照代码的预先设定的异常处理逻辑,针对性地处理异常,让程序尽最大可能恢复正常并继续执行,且保持代码的清晰。

异常类型

Java中所有的异常类是从 java.lang.Exception 类继承的子类。Exception 类是 Throwable 类的子类。除了Exception类外,Throwable还有一个子类Error 。

检查性异常:用户错误或问题引起的异常,这是程序员无法预见的。除了Error 和RuntimeException的其它异常。

运行时异常: 运行时异常是可能被程序员避免的异常。Error 和 RuntimeException 以及他们的子类。

错误: 错误不是异常,而是脱离程序员控制的问题。通常的做法是通知用户并中止程序的执行。

常见异常

输入输出异常:IOException

算术异常类:ArithmeticExecption

空指针异常类:NullPointerException

类型强制转换异常:ClassCastException

文件未找到异常:FileNotFoundException

数组下标越界异常:ArrayIndexOutOfBoundsException

数组负下标异常:NegativeArrayException

文件已结束异常:EOFException

字符串转换为数字异常:NumberFormatException

方法未找到异常:NoSuchMethodException

操作数据库异常:SQLException

常用异常方法

getMessage:获取异常的详细信息。

toString:使用getMessage()的结果返回类的串级名字。

printStackTrace:错误输出流。

异常处理

对检查异常处理的方式:使用try…catch…finally 语句块处理;在函数定义中使用throws 声明。

语法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
vbnet复制代码try{
// try 块中放可能发生异常的代码。
}catch(ArithmeticException arithexception){
// 1.每一个 catch 块用于捕获并处理一个特定的异常,或者异常类型的子类。
// 2.catch 后面括号中定义了异常类型和异常参数。
// 3.在 catch 块中可以使用这个块的异常参数来获取异常的相关信息。
// 4.如果 try 块中发生的异常在所有catch中都没捕获到,则去执行finally,然后到这个函数的外部 caller 中去匹配异常处理器。
// 5.如果 try 中没有发生异常,则所有的catch块都不执行。
}catch(Exception exception){
// 捕获异常
}finally{
// 1.finally 块是可选的。
// 2.无论异常是否发生,finally 都会执行。
// 3.一个 try 至少要有一个 catch 块,或者至少要有一个 finally 块。
// 4.finally 块主要做一些关闭清理工作,如关闭文件,关闭数据库连接等。
}

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
csharp复制代码package com.exception;
public class test {
public static void main(String[] args) {
// TODO 自动生成的方法存根
compute();
}

public static void compute() {
try {
double num = 10 / 0;
System.out.println(num);
}catch(ArithmeticException exp){
System.out.println(exp.getMessage().toString());
}finally {
System.out.println("执行finally块");
}
}
}

关键字 throws/throw

如果一个方法没有捕获到一个检查性异常,那么该方法必须使用 throws 关键字来声明,throws 关键字放在定义方法的尾部。

也可以使用 throw 关键字抛出一个异常,无论它是新实例化的还是刚捕获到的。

throws 语法:

修饰符返回值类型 方法名(参数) throws 异常类名1, 异常类名2, … { 代码块 }

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
java复制代码package com.exception;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.InputStream;

public class test {

public static void main(String[] args) throws FileNotFoundException {
// TODO 自动生成的方法存根
readFile();
}

public static void readFile() throws FileNotFoundException {
InputStream file = new FileInputStream("E:/test.txt");
}
}

throw 语法:

throw new xxxException();

其中 xxxException 必须是 Exception 的派生类。

注意:throw 出的是1个异常对象,所以 new 不能省略。

自定义异常

除了JDK定义好的异常类外,我们可以根据自身业务情况自定义异常处理类。

编写自定义异常类注意点:

1.所有异常都必须是 Throwable 的子类。

2.如果写一个检查性异常类,则需要继承 Exception 类。

3.如果写一个运行时异常类,则需要继承 RuntimeException 类。

语法:修饰符 class 类名 extends Exception {代码块 }

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
java复制代码package com.exception;
/**
* 自定义异常类
* 登录失败异常信息类
*/
public class LoginFailException extends RuntimeException {
public LoginFailException() {
super();
}
public LoginFailException(String message) {
super(message);
}
public void printStackTrace(){
//overwrite
super.printStackTrace();
System.out.printf("This is a LoginFailException excetpion\n");
}
}
public class test {

public static void main(String[] args){
// TODO 自动生成的方法存根
Boolean isLogin = false;
if(!isLogin) {
// 登录失败,抛出异常
throw new LoginFailException("Login is fail");
}
}
}

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

本文转载自: 掘金

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

看动画学算法之 平衡二叉搜索树AVL Tree 简介 AVL

发表于 2021-10-15

本文已参与 「掘力星计划」 ,赢取创作大礼包,挑战创作激励金

「欢迎在评论区讨论,掘金官方将在掘力星计划活动结束后,在评论区抽送100份掘金周边,抽奖详情见活动文章」

简介

平衡二叉搜索树是一种特殊的二叉搜索树。为什么会有平衡二叉搜索树呢?

考虑一下二叉搜索树的特殊情况,如果一个二叉搜索树所有的节点都是右节点,那么这个二叉搜索树将会退化成为链表。从而导致搜索的时间复杂度变为O(n),其中n是二叉搜索树的节点个数。

而平衡二叉搜索树正是为了解决这个问题而产生的,它通过限制树的高度,从而将时间复杂度降低为O(logn)。

AVL的特性

在讨论AVL的特性之前,我们先介绍一个概念叫做平衡因子,平衡因子表示的是左子树和右子树的高度差。

如果平衡因子=0,表示这是一个完全平衡二叉树。

如果平衡因子=1,那么这棵树就是平衡二叉树AVL。

也就是是说AVL的平衡因子不能够大于1。

先看一个AVL的例子:

总结一下,AVL首先是一个二叉搜索树,然后又是一个二叉平衡树。

AVL的构建

有了AVL的特性之后,我们看下AVL是怎么构建的。

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

//根节点
Node root;

class Node {
int data; //节点的数据
int height; //节点的高度
Node left;
Node right;

public Node(int data) {
this.data = data;
left = right = null;
}
}

同样的,AVL也是由各个节点构成的,每个节点拥有data,left和right几个属性。

因为是二叉平衡树,节点是否平衡还跟节点的高度有关,所以我们还需要定义一个height作为节点的高度。

在来两个辅助的方法,一个是获取给定的节点高度:

1
2
3
4
5
6
java复制代码//获取给定节点的高度
int height(Node node) {
if (node == null)
return 0;
return node.height;
}

和获取平衡因子:

1
2
3
4
5
6
java复制代码//获取平衡因子
int getBalance(Node node) {
if (node == null)
return 0;
return height(node.left) - height(node.right);
}

AVL的搜索

AVL的搜索和二叉搜索树的搜索方式是一致的。

先看一个直观的例子,怎么在AVL中搜索到7这个节点:

搜索的基本步骤是:

  1. 从根节点15出发,比较根节点和搜索值的大小
  2. 如果搜索值小于节点值,那么递归搜索左侧树
  3. 如果搜索值大于节点值,那么递归搜索右侧树
  4. 如果节点匹配,则直接返回即可。

相应的java代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
java复制代码//搜索方法,默认从根节点搜索
public Node search(int data){
return search(root,data);
}

//递归搜索节点
private Node search(Node node, int data)
{
// 如果节点匹配,则返回节点
if (node==null || node.data==data)
return node;

// 节点数据大于要搜索的数据,则继续搜索左边节点
if (node.data > data)
return search(node.left, data);

// 如果节点数据小于要搜素的数据,则继续搜索右边节点
return search(node.right, data);
}

AVL的插入

AVL的插入和BST的插入是一样的,不过插入之后有可能会导致树不再平衡,所以我们需要做一个再平衡的步骤。

看一个直观的动画:

插入的逻辑是这样的:

  1. 从根节点出发,比较节点数据和要插入的数据
  2. 如果要插入的数据小于节点数据,则递归左子树插入
  3. 如果要插入的数据大于节点数据,则递归右子树插入
  4. 如果根节点为空,则插入当前数据作为根节点

插入数据之后,我们需要做再平衡。

再平衡的逻辑是这样的:

  1. 从插入的节点向上找出第一个未平衡的节点,这个节点我们记为z
  2. 对z为根节点的子树进行旋转,得到一个平衡树。

根据以z为根节点的树的不同,我们有四种旋转方式:

  • left-left:

如果是left left的树,那么进行一次右旋就够了。

右旋的步骤是怎么样的呢?

  1. 找到z节点的左节点y
  2. 将y作为旋转后的根节点
  3. z作为y的右节点
  4. y的右节点作为z的左节点
  5. 更新z的高度

相应的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
java复制代码Node rightRotate(Node node) {
Node x = node.left;
Node y = x.right;

// 右旋
x.right = node;
node.left = y;

// 更新node和x的高度
node.height = max(height(node.left), height(node.right)) + 1;
x.height = max(height(x.left), height(x.right)) + 1;

// 返回新的x节点
return x;
}
  • right-right:

如果是right-right形式的树,需要经过一次左旋:

左旋的步骤正好和右旋的步骤相反:

  1. 找到z节点的右节点y
  2. 将y作为旋转后的根节点
  3. z作为y的左节点
  4. y的左节点作为z的右节点
  5. 更新z的高度

相应的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
java复制代码//左旋
Node leftRotate(Node node) {
Node x = node.right;
Node y = x.left;

//左旋操作
x.left = node;
node.right = y;

// 更新node和x的高度
node.height = max(height(node.left), height(node.right)) + 1;
x.height = max(height(x.left), height(x.right)) + 1;

// 返回新的x节点
return x;
}
  • left-right:

如果是left right的情况,需要先进行一次左旋将树转变成left left格式,然后再进行一次右旋,得到最终结果。

  • right-left:

如果是right left格式,需要先进行一次右旋,转换成为right right格式,然后再进行一次左旋即可。

现在问题来了,怎么判断一个树到底是哪种格式呢?我们可以通过获取平衡因子和新插入的数据比较来判断:

  1. 如果balance>1,那么我们在Left Left或者left Right的情况,这时候我们需要比较新插入的data和node.left.data的大小

如果data < node.left.data,表示是left left的情况,只需要一次右旋即可

如果data > node.left.data,表示是left right的情况,则需要将node.left进行一次左旋,然后将node进行一次右旋
2. 如果balance<-1,那么我们在Right Right或者Right Left的情况,这时候我们需要比较新插入的data和node.right.data的大小
如果data > node.right.data,表示是Right Right的情况,只需要一次左旋即可

如果data < node.left.data,表示是Right left的情况,则需要将node.right进行一次右旋,然后将node进行一次左旋

插入节点的最终代码如下:

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
java复制代码//插入新节点,从root开始
public void insert(int data){
root=insert(root, data);
}

//遍历插入新节点
Node insert(Node node, int data) {

//先按照普通的BST方法插入节点
if (node == null)
return (new Node(data));

if (data < node.data)
node.left = insert(node.left, data);
else if (data > node.data)
node.right = insert(node.right, data);
else
return node;

//更新节点的高度
node.height = max(height(node.left), height(node.right)) + 1;

//判断节点是否平衡
int balance = getBalance(node);

//节点不平衡有四种情况
//1.如果balance>1,那么我们在Left Left或者left Right的情况,这时候我们需要比较新插入的data和node.left.data的大小
//如果data < node.left.data,表示是left left的情况,只需要一次右旋即可
//如果data > node.left.data,表示是left right的情况,则需要将node.left进行一次左旋,然后将node进行一次右旋
//2.如果balance<-1,那么我们在Right Right或者Right Left的情况,这时候我们需要比较新插入的data和node.right.data的大小
//如果data > node.right.data,表示是Right Right的情况,只需要一次左旋即可
//如果data < node.left.data,表示是Right left的情况,则需要将node.right进行一次右旋,然后将node进行一次左旋

//left left
if (balance > 1 && data < node.left.data)
return rightRotate(node);

// Right Right
if (balance < -1 && data > node.right.data)
return leftRotate(node);

// Left Right
if (balance > 1 && data > node.left.data) {
node.left = leftRotate(node.left);
return rightRotate(node);
}

// Right Left
if (balance < -1 && data < node.right.data) {
node.right = rightRotate(node.right);
return leftRotate(node);
}

//返回插入后的节点
return node;
}

AVL的删除

AVL的删除和插入类似。

首先按照普通的BST删除,然后也需要做再平衡。

看一个直观的动画:

删除之后,节点再平衡也有4种情况:

  1. 如果balance>1,那么我们在Left Left或者left Right的情况,这时候我们需要比较左节点的平衡因子

如果左节点的平衡因子>=0,表示是left left的情况,只需要一次右旋即可

如果左节点的平衡因<0,表示是left right的情况,则需要将node.left进行一次左旋,然后将node进行一次右旋
2. 如果balance<-1,那么我们在Right Right或者Right Left的情况,这时候我们需要比较右节点的平衡因子

如果右节点的平衡因子<=0,表示是Right Right的情况,只需要一次左旋即可

如果右节点的平衡因子>0,表示是Right left的情况,则需要将node.right进行一次右旋,然后将node进行一次左旋

相应的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
java复制代码Node delete(Node node, int data)
{
//Step 1. 普通BST节点删除
// 如果节点为空,直接返回
if (node == null)
return node;

// 如果值小于当前节点,那么继续左节点删除
if (data < node.data)
node.left = delete(node.left, data);

//如果值大于当前节点,那么继续右节点删除
else if (data > node.data)
node.right = delete(node.right, data);

//如果值相同,那么就是要删除的节点
else
{
// 如果是单边节点的情况
if ((node.left == null) || (node.right == null))
{
Node temp = null;
if (temp == node.left)
temp = node.right;
else
temp = node.left;

//没有子节点的情况
if (temp == null)
{
node = null;
}
else // 单边节点的情况
node = temp;
}
else
{ //非单边节点的情况
//拿到右侧节点的最小值
Node temp = minValueNode(node.right);
//将最小值作为当前的节点值
node.data = temp.data;
// 将该值从右侧节点删除
node.right = delete(node.right, temp.data);
}
}

// 如果节点为空,直接返回
if (node == null)
return node;

// step 2: 更新当前节点的高度
node.height = max(height(node.left), height(node.right)) + 1;

// step 3: 获取当前节点的平衡因子
int balance = getBalance(node);

// 如果节点不再平衡,那么有4种情况
//1.如果balance>1,那么我们在Left Left或者left Right的情况,这时候我们需要比较左节点的平衡因子
//如果左节点的平衡因子>=0,表示是left left的情况,只需要一次右旋即可
//如果左节点的平衡因<0,表示是left right的情况,则需要将node.left进行一次左旋,然后将node进行一次右旋
//2.如果balance<-1,那么我们在Right Right或者Right Left的情况,这时候我们需要比较右节点的平衡因子
//如果右节点的平衡因子<=0,表示是Right Right的情况,只需要一次左旋即可
//如果右节点的平衡因子>0,表示是Right left的情况,则需要将node.right进行一次右旋,然后将node进行一次左旋
// Left Left Case
if (balance > 1 && getBalance(node.left) >= 0)
return rightRotate(node);

// Left Right Case
if (balance > 1 && getBalance(node.left) < 0)
{
node.left = leftRotate(node.left);
return rightRotate(node);
}

// Right Right Case
if (balance < -1 && getBalance(node.right) <= 0)
return leftRotate(node);

// Right Left Case
if (balance < -1 && getBalance(node.right) > 0)
{
node.right = rightRotate(node.right);
return leftRotate(node);
}
return node;
}

本文的代码地址:

learn-algorithm

本文收录于 www.flydean.com/11-algorith…

最通俗的解读,最深刻的干货,最简洁的教程,众多你不知道的小技巧等你来发现!

欢迎关注我的公众号:「程序那些事」,懂技术,更懂你!

本文转载自: 掘金

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

一位拖延症患者程序员的自我救赎! 写在前面 四碗鸡汤 没有灵

发表于 2021-10-15

写在前面

关于时间管理、如何做计划、如何提高执行力…等等相关的话题其实很早之前我就想写了,但一直拖着迟迟没有动笔。但是这次我想了又想,不能再拖了。

因为网上经常会看到类似的问题:不少小伙伴说,有时候总感觉过得不太充实,执行力上不来,做的任务计划也感觉没灵魂,很难坚持得下去,当然我也很有体会。

所以这篇文章,我准备来聊一聊这方面个人的一些思考、想法、以及实践的心得和习惯,内容比较主观,供大家参考。另外也期望借此话题,大家能够一起交流看看,说不定有其他新的灵感产生。

其实很多博主在讲时间管理,包括怎么做计划时,上来就是分享自己用了哪些软件,怎么在上面做计划。但是咱这次内容重点不在这个,我觉得用什么软件不是关键,关键是自己怎么思考的,有没有找到问题的痛点到底在哪。

这方面花点时间想清楚,对于后期学习和工作的正向工作流形成,应该都是非常有帮助的。

本文在GitHub开源仓库「编程之路」 github.com/rd2coding/R… 中已经收录,里面有我整理的6大编程方向(岗位)的自学路线+知识点大梳理、面试考点、我的简历、几本硬核pdf笔记,以及我的程序员生活和感悟,欢迎star。


四碗鸡汤

开始之前,有几个大的原则,我觉得有必要先聊一聊。

  • 首先坚定的信念必须要有!

这话怎么说呢?

因为我自己也反复体验过那种做了计划而没有完成,立了Flag但却迟迟没有兑现的那种纠结、困扰、甚至是自愧。所以也曾一度自暴自弃,在时间管理和任务安排上一蹶不振,天天脚踩着西瓜皮往前滑,滑到哪里算哪里。

因此什么手段啊、工具啊、方法啥的咱都先不说,首先内心深处一定要告诉自己并坚信自己会成为一个时间管理和任务规划大师,高效做事,快乐生活,慢慢成为一个执行力很强的人!

所以信念必须要有。

  • 任务计划(ToDo List)一定要坚持做

很多人会抱怨说,感觉自己所做的任务计划(ToDo List)没有灵魂。因为很少能够坚持完成,这样反而会给自己带来心理负担,后来干脆就放弃了。

要知道,凡事预则立,不预则废!

在我看来,无论如何,任务计划还是要坚持做!做计划和不做计划肯定是有差别的,只不过我觉得这个和做计划的方式方法、习惯、工具、以及是否得到正向的反馈等等多方面因素都是有关系的,下文我也会详细阐述一下我的个人方法心得,这也是本文的重点!

  • 持续激励和正反馈尤为重要

我们都知道,在平时的工作学习中,联调成功了是成就感、Bug解决了是成就感、技术难点攻克了是成就感、功能稳定上线了是成就感…这些都会成为我们生活和工作中的甜蜜点,同时激励着我们朝着更好的目标前进。

正向激励是我们能够获得持续前进的源动力!

同理,当我们的时间管理和任务计划在获得扎实执行,并得到正向反馈时,这也是一种莫大的成就感,往往就是这种成就感会推动着我们愈发镞砺和精进,一旦正反馈形成,很多事情就好办了!

  • 复盘和回味是不可或缺的

因为再严密的计划和执行也都有可能被现实变化所扰乱,所以阶段性地复盘总结有利于我们把握进度、纠正偏差。

到达终点的路线固然要摸清,但更要弄清楚的是,我们在路上的当下位置。


所以大道理就先聊这么多,接下来聊聊个人在平时学习与工作中的一些实际操作经验和感想。

我先来抛砖引玉,欢迎大家分享出各自的经验心得,一起交流。


没有灵魂的ToDo List!

不知道大家有没有这种感觉,那就是:

  • 做计划时信誓旦旦
  • 执行计划时拖延拉胯
  • 验收计划时自责内疚……

久而久之,总会感觉自己做的计划毫无灵魂,一声叹息之后便慢慢放弃了。

实不相瞒,其实这种心理循环我也多次经历过,痛定思痛后我分析了几点可能的原因:

  • 做计划的仪式感有待加强
  • 做计划的粒度有待商榷
  • 做计划的轻重缓急没有分开、长短线任务没有加以区别
  • 计划的执行得不到保证
  • 缺乏正反馈和正向激励
  • 缺少复盘和总结
  • …

下面聊一聊我的一些方法和习惯。


如何做计划(ToDo List)

我觉得「关于如何做计划」这一块颇有学问,我自己也在不断摸索和改进中。

我先聊一聊我自己在做ToDo List时的个人习惯,很多都是在工作后跟着公司和团队的做法学的。

1、首先在做计划时,我习惯于将任务拆分细化,拆分成一个个切实可执行的任务点,执行的单位时间控制在1-2天为宜,而一天之内的任务点安排在3~5个就可以了。我觉得这样更便于督促自己拿出信心来完成,从而形成短期的正向激励和成就感反馈。

因为我们是人而不是神,强行给每一天安排太多任务,一旦完不成,反而会让人有一种内心上的愧疚和焦虑,这不一定是一件好事。

2、其次在做计划的时候,我会利用到一种叫做「池子」的概念。

做技术的我们都知道,池子这玩意儿它是个好东西呀,比如天天打交道的:常量池、线程池、数据库连接池、内存池、网络连接池… 等等。

为了让做计划这个事情仪式感拉满,我自己也搞了一个任务看板,在看板上搞了7个池子,逻辑效果大概类似于这样:

下面我来详细解释一下。

  • 任务计划池

这个池子里面放了我几乎未来一段时间里想做的任何事情和任何可能的任务点,不计大小、不论优先级、也不管任务粒度划分是否合理,只要有想法甚至任何凌空闪现的idea,我就会及时地丢进去。

这么做原因很简单。

因为我们往往没有办法在列计划的时候就能立即确定当下的这一条任务划分就一定是合理的,所以只要是未来可能会做的任务点,我都会把它丢进「计划池」里。

所以这样来看,「任务计划池」更像是一个收藏夹,甚至是备忘录,里面记录着自己任何的想法和未来可能要做的事。

  • 任务就绪池

「就绪池」里放的任务全部都是从上面的「计划池」里筛选出来等待执行的确定任务,因此丢到这个池子里的任务就等待着接下来的执行和完成了。

从「任务计划池」里筛选任务到「任务就绪池」里时就要反复问自己:

  • 这个任务重要吗?
  • 这个任务紧急吗?
  • 这个任务确定要在这一周做吗?
  • 这个任务一定能出结果吗?
  • 这个任务需要切分吗?
  • …等等。

所以下面这个四象限图在任务筛选和切分时则可以参考一下。

我个人会定期(我是每晚睡前花十多分钟)筛选和切分「计划池」里优先级较高,或者较容易实现,能快速产生短期正反馈的任务,放到「任务就绪池」。

  • 任务执行池

顾名思义,拖到这个池子里面的任务都是正在执行的任务点。

注意我这里的个人习惯是只排一天之内的执行任务,且对于个人来说,一天之内要执行的任务点安排在3~5个就足够了,强行拖太多过来做不完也没意义,反而会引起内心的焦虑和愧疚。

  • 任务验收池

这个也好理解,每天做完的任务可以当即拖到此处。

注意,之所以称这个为「任务验收池」,是因为拖到这里的任务本身虽然完成了,但后续的一些复盘、总结、对比、输出、记录…等等一些工作不一定做了,所以增加这样一个缓冲的池子。

比如今天写了某一个模块的项目代码,代码的确是写完了,功能也实现了,但总结工作是否做了?遇到了什么bug记录没?调试踩了哪些坑?下个迭代还有哪些地方可能需要优化?等等。

这么做目的很简单,就是为了让自己对于已经做完的任务,能增加一个简单的复盘和回味的环节,这样也能增加成就感。因为不管是学习还是工作,有时候真的不在乎快,多回头看一看想一想其实很有必要。还是那句话:有时候慢一点才能更快!

所以我个人一般也是每天晚上,会把验收池里的任务过一遍,该写备注的写备注,该做笔记的做笔记,该写博客的写博客。确定完成后再把该任务拖到下面要说的「任务完成池」里。

所以对于我来说,「验收池」里的任务可能是会有堆积的,一般来讲,我会争取一周内完全清一次。

  • 任务完成池

这个就特别好理解了,拖到此处的任务都是执行完毕的。

每天最爽的事情就是把完成的任务卡片依次拖到这个池子里来,所以某一程度上来说,这就是一种最直接的正向激励!

  • 任务阻塞池

「阻塞池」里面专门存放那些执行周期较长,或者优先级不高但还算比较重要,或者是一些老大难的历史遗留任务。

这类任务的共同特点就是:一般难以在一两天,甚至一两周内就能完成,持续的时间比较久,而且可能会有一些不确定因素会影响着它的执行,大部分都是长线任务。

对于我个人来说,一般就是一些持续时间比较长的学习任务会放在这里面,比如系统地学完某个知识点,或者花一个月搞定某个开源项目等等。

不过实不相瞒,我的阻塞池里的有几个任务都放进去都快半年了,感觉都快要烂尾了。。。

  • 任务酱油池

这个池子里面放的任务列表比较特殊,我要好好来解释一下。

我们都知道,虽然我们每天会把主要的时间和精力放在工作和学习上,但是再勤恳的打工人,难免也会有着属于自己的打酱油时间(再通俗一点说就是摸鱼时间、划水时间。。)。

比如:工作前后的间隙、开会前后的间隙、发呆的时间、刷手机的时间…这类时间的特点一般是:比较零碎、时长不固定、频次不固定、可控性不高…其实这类时间如果好好利用,能完成不少事情的。

所以特别针对于这样一个情况,在安排任务时,其实可以把一些边角零碎的事情,统统丢到这个池子里来。一旦有打酱油的时间,可以扫一眼这个池子里的任务,看有没有随手可以完成的。

比如:查一下某个资料或文章、写一个简单的总结回顾提纲、看一下某方面的最新资讯、扫一眼大盘的情况…

所以总而言之,「酱油池」里的任务,主要就是用来打发打酱油的时间,目的也是为了最大化地利用碎片时间,这么说应该就好理解了。

  • 小 结

所以最后来小小总结一下!

这样一来,每天我只要重点保证「执行池」里的条目全部清空,那就说明完成了一天的任务。而每周只要保证「任务验收池」里的条目全部清空,那就说明完成了一周的任务!

这样心理上就会很充实,人也很畅快。

完成之后,重新再从「任务计划池」里筛选和切分任务,并适当关注一下「任务阻塞池」里的长线任务,如果有必要,就将其加入到「就绪池」中,等待被执行。

如此循环往复,将任务计划和执行,变成一种能看到积极反馈的正向工作流,这样习惯就能慢慢形成!而习惯一旦形成,很多事情就好办了。


如何执行任务

既然任务已经计划好了,接下来就要落实到具体的执行层面了。

不同人有不同的心得和技巧,怎么确保效果真的得看个人自己的执行力了。有些方法可能在别人那里有效,到我们自己这里说不定就扑街了,这都是有可能的。

接下来聊几点自己实践下来的一些小心得,希望能对大家有用,也欢迎大家踊跃分享出自己的实践心得!

目光集中、一鼓作气

一天的时间其实很短,多数时候导致我们焦虑空虚的一个主要的原因就是因为没有提前做好确定的任务计划。漫无目的,东一榔头西一棒子的做事方式肯定是不可取的。

上面在聊做计划问题时也已经说过,只要我们每天都保证「任务执行池」里的条目全部清空,那就说明完成了一天的任务,这样就会很充实。

所以只要目光集中在每天要做的确定任务上,优先保证其完成,这样就保住了基本盘,内心就会非常踏实!工作学习就会有奔头!

避免频繁地任务切换

大家都学过计算机,我们都知道像线程的切换或者任务的中断带来的开销成本是不可小觑的!

同理,生活、学习、工作中的频繁任务切换也会带来很多的“性能损失”。

而想要避免这一点,更深次要求是在做当日ToDo List计划时必须要合理安排任务。比如同类的任务可以放在一起,次要的、机械性的任务放在边角,有难度挑战性的任务放在大区间内,等等。

举个例子。

比如像拿快递、回邮件、回信息等这类机械重复性任务可以安排在某个固定的时间边角;而写代码、做需求、研究开源项目等等这类挑战性稍大一些的任务可以放在完整高效的时间区间里;而那些像读书、写作、剪视频等个人提升和兴趣时间可以放在一起,安排在自己卸下包袱的轻松时间里。

番茄工作法

番茄工作法我想大家应该都听过,是一种思想非常朴素的时间管理疗法。

基本思想就是将时间切割成一个个番茄时间片,25分钟工作+5分钟休息,四个番茄时间片一过(2小时),可以进行一次较长时间的休息,当然它的其他施行小细节还有好多,具体可以看下百科。

对于这个方法,我个人在施行时,有些情况下觉得还是有指导价值的,但有些情况下是真的很难玩起来。

比方说一个任务如果需要的时间并不长,这时候强行套用番茄工作法反而可能会让效率降低。

再比如,在一些时间容易被打断的零碎时间场景下,这种方法效果也会大打折扣。试想一下,一会领导叫你过去汇个报讨个论,一会产品临时召集开个会,一会儿要等一个重要电话,一会儿又要出去隔壁团队沟通个需求… 时间一旦被打碎,番茄工作法的效果就大打折扣,这样反而会有心理负担,而且毫无成就感。

我个人觉得,在面对那些有相对完整成块的时间段里,这种方法是有效的,因为这种情形下不容易被打断。比如今天是周末,今天下午我就在家准备用一个相对完整的时间块(≥2小时)准备用来重构XXX模块的代码,或者阅读XXX技术书籍,这时候番茄工作法用起来还是蛮舒服的。有时候甚至有一种忘了时间的感觉,25分钟瞬间就到了。事情做完了,成就感也满满。

所以,我现在愈发感觉,能用上番茄工作法真的是一件很幸福的事情!这说明你有大把美好的完整时间块去做事。

时间统计和定期复盘

除了番茄时间工作法很有名之外,还有一个叫做「柳比歇夫时间统计法」不知道大家听过没。

它是前苏联昆虫学家、哲学家、数学家柳比歇夫56年如一日对个人时间进行定量统计管理而得名的一种方法。

总体思想倒也挺朴素:在完成任务时,尽量精确地记录下起始时间和持续时间,并做好分类(现在有很多App都能帮我们完成这个功能),完事形成完整的时间账簿,并进行事后的总结分析,然后用于指导并改善对于时间的把控及任务的完成。

这说明了,我们在做任务计划和时间管理时,定期去复盘和总结自己的时间花销和任务完成情况非常有必要,通过实际情况和预想情况的不断对比,慢慢就能构建出自己对于后续任务计划和时间管理的感知度和宽容度。

打造正向激励和正反馈

其实上面在聊「如何做计划」时就一直在强调短期的正向激励和正向反馈对于时间管理和任务执行的重要性,为此也做了很多可以提升成就感和看到正反馈的措施,这些在上文都已经说过了,这里就不再赘述了。

deadline+硬肝

就像看过很多道理依然过不好这一生一样。有时候当各种时间管理方法、任务安排技巧都打不过拖延症的时候,深夜硬肝则成了我自己的独家解药。

比如写文章的时候就是,本来定的今天必须做完,但是由于各种原因,到晚上12点了还滞留在那里。好呗,这时候洗把脸,看一看镜子里的自己,憋说了,今晚怒肝到凌晨,不完成绝不睡觉…


总 结

好了,这篇文章聊了很多。

到现在为止,我依然觉得在时间管理和任务规划上一个非常非常重要的坚持动力那就是:一定要想办法来持续获得正向反馈和正向激励,从而形成正向的工作流!

一旦正反馈形成,很多事情就好办了,执行力也就能慢慢上来。

所以大家如果围绕着这个思路,来改变一下现在的学习和工作,或许是有效的。

当然了,别人说的不一定有用,别人的方法在自己这里也不一定完全有效,所以最后建议大家还是,尽早摸索出一套,适合于自己的时间管理和任务规划的方法论,并且扎实地执行下去,坚持半年,相信就会大不一样。

希望我们都能慢慢变得越来越好。

本文已被GitHub开源仓库「编程之路」 github.com/rd2coding/R… 收录,里面有我整理的6大编程方向(岗位)的自学路线+知识点大梳理、面试考点、我的简历、几本硬核pdf笔记,以及我的程序员生活和感悟,部分内容如下↓↓↓,欢迎star。

本文转载自: 掘金

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

Dubbo、Zookeeper(三) Dubbo、Zooke

发表于 2021-10-14

Dubbo、Zookeeper

一、软件架构的演进过程

1、单体架构

​ 全部功能都集中在一个项目内。

优点

​ 架构简单,前期开发成本低、开发周期短,适合小型项目。

缺点

①复杂性高

②技术债务逐渐上升,难以修改维护

③部署速度逐渐变慢

④扩展能力受限制,无法按照伸缩

⑤阻碍新技术创新

总结

  • 全部功能集成在一个工程中,对于大型项目不易开发、扩展和维护
  • 技术栈受限,只能使用一种语言开发
  • 系统性能扩展只能通过扩展集群节点,成本高

2.垂直架构

​ 按照业务切割,形成小的单体项目。主要有表现层、业务逻辑层,数据访问层组成的MVC架构,整个项目打包放在一个tomcat里面。适合于访问量小,用户数不多的业务。

优点

技术栈可扩展(不同的系统可以用不同的编程语言编写)。

缺点

① 这是一个大而全的项目,项目的部署效率很低,代码全量编译和部署一次发布需要很长时间,更重要的是 如果某个功能出错有问题,所有的功能都需要再重新打包编译,部署效率极低。

② 团队协作难度高,如多人使用 git 很可能在同一个功能上,多人同时进行了修改,作为一个大而全的项目,可能个人只是需要开发其中一个小的模块的需求,却需要导入整个项目全量的代码。

总结

  • 功能集中在一个项目中,不利于开发、扩展、维护。
  • 系统扩张只能通过集群的方式。
  • 项目之间功能冗余、数据冗余、耦合性强。

3.SOA架构

1
复制代码将重复功能或模块抽取成组件的形式,对外提供服务,在项目与服务之间使用ESB(企业服务总线)的形式作为通信的桥梁。

​ SOA 全称为 Service-Oriented Architecture,即面向服务的架构。它可以根据需求通过网络对松散耦合的粗粒度应用组件(服务)进行分布式部署、组合和使用。一个服务通常以独立的形式存在于操作系统进程中。

​ 站在功能的角度,把业务逻辑抽象成可复用的服务,通过服务的编排实现业务的快速再生,目的:把原先固有的业务功能转变为通用的业务服务,实现业务逻辑的快速复用。

ESB

简单 来说 ESB 就是一根管道,用来连接各个服务节点。为了集 成不同系统,不同协议的服务,ESB 做了消息的转化解释和路由工作,让不同的服务互联互通;

优点

  • 重复功能或模块抽取为服务,提高开发效率。
  • 可重用性高。
  • 可维护性高。

缺点

  • 各系统之间业务不同,很难确认功能或模块是重复的。
  • 抽取服务的粒度大。
  • 系统和服务之间耦合度高。

4.微服务架构

​ 其实和 SOA 架构类似,微服务是在 SOA 上做的升华,微服务架构强调的一个重点是“业务需要彻底的组件化和服务化”,原有的单个业务系统会拆分为多个可以独立开发、设计、运行的小应用。这些小应用之间通过服务完成交互和集成。

优点

  • 服务拆分粒度更细,有利于提高开发效率。
  • 可以针对不同服务制定对应的优化方案。
  • 适用于互联网时代,产品迭代周期更短。

缺点

  • 粒度太细导致服务太多,维护成本高。
  • 分布式系统开发的技术成本高,对团队的挑战大。

二、Dubbo

1.简介

​ Apache Dubbo是一款高性能的Java RPC框架。其前身是阿里巴巴公司开源的一个高性能、轻量级的开源Java RPC框架,可以和Spring框架无缝集成。

RPC

​ RPC全称为remote procedure call,即远程过程调用。比如两台服务器A和B,A服务器上部署一个应用,B服务器上部署一个应用,A服务器上的应用想调用B服务器上的应用提供的方法,由于两个应用不在一个内存空间,不能直接调用,所以需要通过网络来表达调用的语义和传达调用的数据。

​ 需要注意的是RPC并不是一个具体的技术,而是指整个网络远程调用过程。

​ Dubbo提供了三大核心能力:面向接口的远程方法调用,智能容错和负载均衡,以及服务自动注册和发现。

​ 通俗一点说,就是一般程序员对于本地的过程调用很熟悉,那么我们把RPC作成和本地调用完全类似,那么就更容易被接收,使用起来毫无障碍。

2.架构

官方提供的架构图。

dubbo-1.png
角色说明

节点 角色名称
Provider 暴漏服务的服务提供方
Consumer 调用远程服务的服务消费方
Registry 服务注册与发现的注册中心
Container 服务运行容器
Monitor 统计服务的调用次数和调用时间的监控中心

虚线都是异步访问,实线都是同步访问。

蓝色虚线:在启动时完成的功能。

红色虚线、实线:都是程序运行中执行的功能。

调用关系说明

  • 服务容器负责启动,加载,运行服务提供者。
  • 服务提供者在启动时,向注册中心注册自己提供的服务。
  • 服务消费者在启动时,向注册中心订阅自己所需的服务。
  • 注册中心返回服务提供者地址列表给消费者,如果有变更,注册中心将基于长连接推送变更数据给消费者。
  • 服务消费者,从提供者地址列表中,基于负载均衡算法,选一台提供者进行调用,如果调用失败,再零选一台调用。
  • 服务消费者和提供者,在内存中累计调用次数和调用时间,定时每分钟发送一次统计数据到监控中心。

三、服务注册中心Zookeeper

​ 通过Dubbo架构图可以看到,Registry(服务注册中心)起了关键作用。Dubbo官方推荐使用Zookeeper作为服务注册中心。

1.介绍

​ Zookeeper 是 Apache Hadoop 的子项目,是一个树型的目录服务,支持变更推送,适合作为 Dubbo 服务的注册中心,工业强度较高,可用于生产环境,并推荐使用 。

2.工作流程说明

  • 服务提供者启动时,向/dubbo/com.foo.BarService/providers 目录下写入自己的URL地址。
  • 服务消费者(Consumer)启动时:订阅 /dubbo/com.foo.BarService/providers目录下的提供者URL地址。并向 /dubbo/com.foo.BarService/consumers目录下写入自己的URL地址
  • 监控中心(Monitor)启动时:订阅 /dubbo/com.foo.BarService目录下的所有提供者和消费者URL地址。

3.Zookeeper使用

双击运行zkServer.cmd即可。

1.需要保证运行环境中正确安装了Java运行环境

2.闪退。在zkEnv.cmd文件末尾添加pause,这样出错不会退出,会提示错误信息。方便找到原因。

3.zoo.cfg文件中需要修改dataDir属性值,在上级目录下添加data目录(../data)

四、Dubbo快速入门

​ Dubbo作为一个RPC框架,其最核心的功能就是要实现跨网络的远程调用。本小节就是要创建两个应用,一个作为服务的提供方,一个作为服务的消费方。通过Dubbo来实现服务消费方远程调用服务提供方的方法。

​ 项目结构为dubbodemo工程下,一个dubbodemo_provider项目,一个dubbodemo_consumer项目,均为war。

1.服务方开发

pom

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
xml复制代码<packaging>war</packaging>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target>
<spring.version>5.0.5.RELEASE</spring.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-jdbc</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aspects</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-jms</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context-support</artifactId>
<version>${spring.version}</version>
</dependency>
<!-- dubbo相关 -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>dubbo</artifactId>
<version>2.6.0</version>
</dependency>
<dependency>
<groupId>org.apache.zookeeper</groupId>
<artifactId>zookeeper</artifactId>
<version>3.4.7</version>
</dependency>
<dependency>
<groupId>com.github.sgroschupf</groupId>
<artifactId>zkclient</artifactId>
<version>0.1</version>
</dependency>
<dependency>
<groupId>javassist</groupId>
<artifactId>javassist</artifactId>
<version>3.12.1.GA</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.47</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>2.3.2</version>
<configuration>
<source>1.8</source>
<target>1.8</target>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.tomcat.maven</groupId>
<artifactId>tomcat7-maven-plugin</artifactId>
<version>2.2</version>
<configuration>
<!-- 指定端口 -->
<port>8081</port>
<!-- 请求路径 -->
<path>/</path>
</configuration>
</plugin>
</plugins>
</build>

web.xml

配置监听器,加载配置文件。

1
2
3
4
5
6
7
8
9
10
11
12
13
xml复制代码<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd"
version="3.1">
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:applicationContext*.xml</param-value>
</context-param>
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
</web-app>

HelloService.class

1
2
3
java复制代码public interface HelloService {
public String sayHello(String name);
}

实现类需要使用dubbo的注解@Service,有时会因为导错包而服务发布失败。

1
2
3
4
5
6
7
8
java复制代码import com.alibaba.dubbo.config.annotation.Service;
@Service
public class HelloServiceImpl implements HelloService {
@Override
public String sayHello(String name) {
return "hello " + name;
}
}

在src/main/resources下创建 applicationContext-service.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
xml复制代码<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:p="http://www.springframework.org/schema/p"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:dubbo="http://code.alibabatech.com/schema/dubbo"
xmlns:mvc="http://www.springframework.org/schema/mvc"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/mvc
http://www.springframework.org/schema/mvc/spring-mvc.xsd
http://code.alibabatech.com/schema/dubbo
http://code.alibabatech.com/schema/dubbo/dubbo.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd">
<!-- 当前应用名称,用于注册中心计算应用间依赖关系,注意:消费者和提供者应用名不要一样 -->
<dubbo:application name="dubbodemo_provider" />
<!-- 连接服务注册中心zookeeper ip为zookeeper所在服务器的ip地址-->
<dubbo:registry address="zookeeper://127.0.0.1:2181"/>
<!-- 注册 协议和port 端口默认是20880 -->
<dubbo:protocol name="dubbo" port="20881"></dubbo:protocol>
<!-- 扫描指定包,加入@Service注解的类会被发布为服务 -->
<dubbo:annotation package="com.dyy.service.impl" />
</beans>

这个文件要被导入到web.xml中才能生效。

启动服务

maven->项目->plugins->tomcat7->右键单击tomcat7:run选择运行当前项目

2.消费方开发

pom

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
xml复制代码
<packaging>war</packaging>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target>
<spring.version>5.0.5.RELEASE</spring.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-jdbc</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aspects</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-jms</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context-support</artifactId>
<version>${spring.version}</version>
</dependency>
<!-- dubbo相关 -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>dubbo</artifactId>
<version>2.6.0</version>
</dependency>
<dependency>
<groupId>org.apache.zookeeper</groupId>
<artifactId>zookeeper</artifactId>
<version>3.4.7</version>
</dependency>
<dependency>
<groupId>com.github.sgroschupf</groupId>
<artifactId>zkclient</artifactId>
<version>0.1</version>
</dependency>
<dependency>
<groupId>javassist</groupId>
<artifactId>javassist</artifactId>
<version>3.12.1.GA</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.47</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>2.3.2</version>
<configuration>
<source>1.8</source>
<target>1.8</target>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.tomcat.maven</groupId>
<artifactId>tomcat7-maven-plugin</artifactId>
<version>2.2</version>
<configuration>
<!-- 指定端口 -->
<port>8082</port>
<!-- 请求路径 -->
<path>/</path>
</configuration>
</plugin>
</plugins>
</build>

web.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
xml复制代码<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd"
version="3.1">
<servlet>
<servlet-name>springmvc</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<!-- 指定加载的配置文件 ,通过参数contextConfigLocation加载 -->
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:applicationContext-web.xml</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>springmvc</servlet-name>
<url-pattern>/</url-pattern>
</servlet-mapping>
</web-app>

将服务提供者工程中的HelloService接口复制到当前工程

1
2
3
java复制代码public interface HelloService {
public String sayHello(String name);
}

编写Controller

注意:Controller中注入HelloService使用的是Dubbo提供的@Reference注解

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
java复制代码@Controller
@RequestMapping("/demo")
public class HelloController {
@Reference
private HelloService helloService;

@RequestMapping("/hello")
@ResponseBody
public String getName(String name){
//远程调用
String result = helloService.sayHello(name);
System.out.println(result);
return result;
}
}

在src/main/resources下创建applicationContext-web.xml

需要配置到web.xml中才能生效

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
xml复制代码<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:p="http://www.springframework.org/schema/p"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:dubbo="http://code.alibabatech.com/schema/dubbo"
xmlns:mvc="http://www.springframework.org/schema/mvc"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/mvc
http://www.springframework.org/schema/mvc/spring-mvc.xsd
http://code.alibabatech.com/schema/dubbo
http://code.alibabatech.com/schema/dubbo/dubbo.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd">

<!-- 当前应用名称,用于注册中心计算应用间依赖关系,注意:消费者和提供者应用名不要一样 -->
<dubbo:application name="dubbodemo-consumer" />
<!-- 连接服务注册中心zookeeper ip为zookeeper所在服务器的ip地址-->
<dubbo:registry address="zookeeper://127.0.0.1:2181"/>
<!-- 扫描的方式暴露接口 -->
<dubbo:annotation package="com.dyy.controller" />
<!-- 运行dubbo不检查提供者是否提前开启 -->
<!-- <dubbo:consumer check="false"></dubbo:consumer> -->
</beans>

问题

1.在消费者工程中只是引用了HelloService接口,并没有提供实现类,Dubbo是如何做到远程调用的?

答:Dubbo底层是基于代理技术为HelloService接口创建代理对象,远程调用时通过此代理对象完成的。

2.上面的Dubbo入门案例中我们使用Zookeeper作为服务注册中心,服务提供者需要将自己的服务信息注册到Zookeeper,服务消费者需要从Zookeeper订阅自己所需要的服务,此时Zookeeper服务就变得非常重要了,那如何防止Zookeeper单点故障呢?

答:Zookeeper支持集群模式,可以配置Zookeeper集群来达到Zookeeper服务的高可用,防止出现单点故障。

代码重构

思路

​ 将HelloService接口从服务提供者工程(dubbodemo_provider)复制到服务消费者工程(dubbodemo_consumer)中,这种做法显然不好,不利于后期维护。更好的方式是单独创建一个maven工程。将接口创建在这个maven工程中。需要依赖这个接口的工程只需要在自己工程的pom.xml文件中引入maven坐标即可。

执行

① 创建项目:dubbodemo_interface

② 把 项目dubbodemo_consumer 和 项目dubbodemo_provider当中的 接口 HelloService 拷贝到dubbodemo_interface工程里面

③ 删除工程dubbodemo_consumer 和 工程dubbodemo_provider当中的 接口 HelloService

dubbodemo_consumer 工程和dubbodemo_provider添加pom文件的依赖

五、Dubbo管理控制台

​ 在开发时,需要知道Zookeeper上注册哪些服务,有哪些消费者来消费这些服务。可以通过部署一个管理中心来实现。

​ 这个管理中心就是一个web应用,部署到tomcat即可。

1.如何使用

将的dubbo-admin-2.6.0.war文件复制到tomcat的webapps目录下;

启动tomcat,war会自动解压。

在解压后的文件中找到dubbo.properties文件,将address改为当前使用的Zookeeper的IP地址和端口号。

1
2
3
ini复制代码dubbo.registry.address=zookeeper://192.168.6.100:2181
dubbo.admin.root.password=root
dubbo.admin.guest.password=guest

访问http://localhost:8080/dubbo-admin-2.6.0/ ,输入用户名(root)和密码(root),切换简体中文

在这里可以看到启动了的提供者工程和服务者工程。

2.duboo的属性配置

  1. 包扫描

【提供者】

1
ini复制代码<dubbo:annotation package="com.dyy.service" />

或

1
2
ini复制代码<bean id="helloService" class="com.dyy.service.impl.HelloServiceImpl" />
<dubbo:service interface="com.dyy.api.HelloService" ref="helloService" />

【消费者】

1
2
xml复制代码<!-- 生成远程服务代理,可以和本地bean一样使用helloService -->
<dubbo:reference id="helloService" interface="com.dyy.api.HelloService" />
  1. 协议

一般在服务提供者乙方配置,可以使用的协议名称和端口号。

其中dubbo支持的协议有**【dubbo、rmi、hessian、http、webservice、rest、redis】等**

推荐使用dubbo

​ dubbo协议采用单一长连接和NIO异步通讯,适合于小数据量大并发的服务调用,以及服务消费者机器数远大于服务提供者机器数的情况。不适合传送大数据流量的服务,比如传视频,传文件等,除非请求量很低。

​ 也可以在同一个工程中配置多个协议,不同服务使用不同协议。比如:

1
2
3
4
5
6
7
8
9
10
> java复制代码<!-- 多协议配置 -->
> <dubbo:protocol name="dubbo" port="20880" />
> <dubbo:protocol name="rmi" port="1099" />
> <!-- 使用dubbo协议暴露服务 -->
> <dubbo:service interface="com.dyy.service.HelloService" ref="helloService" protocol="dubbo" />
> <!-- 使用rmi协议暴露服务 -->
> <dubbo:service interface="com.dyy.service.DemoService" ref="demoService" protocol="rmi" />
>
>
>
1
ini复制代码<dubbo:protocol name="dubbo" port="20880"/>
  1. 启动时检查
1
xml复制代码<dubbo:consumer check="false"/>

该配置需要配置在服务消费者乙方,如果不配置默认check值为true。dubbo缺省会在启动时检查依赖的服务是否可用,不可用时会抛出异常,阻止spring初始化完成,以便上线时,能及早发现问题。可以通过将check值改为false来关闭检查。

建议在开发阶段将check设为false,生产环境下改为true
4. 负载均衡

负载均衡(load balance):其实就是将请求分摊到多个操作单元上执行,从而共同完成工作任务。

在集群负载均衡时,dubbo提供了多种均衡册罗(包括随机、轮询、最少活跃调用数、一致性hash),缺省是random随机调用。

* 随机算法(默认)RandomLoadBalance
* 轮询算法RoundRobinLoadBalance
* 最小活跃数LeastActiveLoadBalance
* 一致性hash算法ConsistentHashLoadBalance**配置负载均衡策略,既可以在服务提供者配置,也可以在服务消费者配置。**

【提供者】

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
less复制代码@Controller
@RequestMapping("/demo")
public class HelloController {
//在服务消费者一方配置负载均衡策略
@Reference(check = false,loadbalance = "random")
private HelloService helloService;

@RequestMapping("/hello")
@ResponseBody
public String getName(String name){
//远程调用
String result = helloService.sayHello(name);
System.out.println(result);
return result;
}
}

【消费者】

1
2
3
4
5
6
7
typescript复制代码//在服务提供者一方配置负载均衡
@Service(loadbalance = "random")
public class HelloServiceImpl implements HelloService {
public String sayHello(String name) {
return "hello " + name;
}
}

可以通过启动多个服务提供者来观察Dubbo负载均衡的效果。

注意:因为我们是在一台机器上启动多个服务提供者,所以需要修改tomcat的端口号和dubbo服务的端口号来防止端口冲突。

在实际生产环境中,多个服务提供者是分别部署在不同的机器上,所以不存在端口冲突问题。

六、解决dubbo无法发布被事务代理的Service问题

问题描述:

​ 如果我们在服务提供者类上加入@Transactional事务控制注解后,服务就发布不成功了。

原因:

​ 事务控制的底层原理是为服务提供者类创建代理对象, 而默认情况下Spring是基于JDK动态代理方式创建代理对象,而此代理对象的完整类名是com.sun.proxy.$Proxy42(最后两位数字不是固定的),导致dubbo在发布服务前进行包匹配时无法完成匹配,而没有进行服务的发布。

开启注解事务支持

1
xml复制代码<tx:annotation-driven transaction-manager="transactionManager"/>

在类上添加事务注解

1
css复制代码@Transactional

解决方案

  1. 修改applicationContext-service.xml配置文件,开启事务注解支持时指定proxy-target-class属性,值为true。作用是使用cglib代理方式为Service类创建代理对象,添加如下配置
1
2
xml复制代码<!--开启事务控制的注解支持-->
<tx:annotation-driven transaction-manager="transactionManager" proxy-target-class="true"/>
  1. 修改HelloServiceImpl类,在Service注解中加入interfaceClass属性,值为HelloService.class,作用是指定服务的接口类型
1
2
3
4
5
6
7
8
9
10
java复制代码import com.alibaba.dubbo.config.annotation.Service;
import org.springframework.transaction.annotation.Transactional;
@Service(interfaceClass = HelloService.class)
@Transactional
public class HelloServiceImpl implements HelloService {
@Override
public String sayHello(String name) {
return "8086 hello " + name;
}
}

本文转载自: 掘金

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

使用 Go 语言开发一款跨平台桌面应用(伪) 实现思路 如何

发表于 2021-10-14

最近在学习 Go 语言,语法看得差不多了,想着找点什么项目做做,正好我一直想做一个「局域网PC与手机互传文件,且不想借助微信/QQ等骚扰软件」的软件,于是就用 Go 来做了,最终截图如下:

image.png

image.png

image.png

功能很简单:

  1. PC 上传文字或文件后创建二维码,提供给手机浏览器扫码下载
  2. 手机在浏览器上传文字或文件后自动用 websocket 通知给 PC 端,弹出下载提示

源码在此,这里主要说一下实现思路。

实现思路

用 Loca 创建窗口

我了解到 Go 的如下库可以实现窗口:

  1. lorca - 调用系统现有的 Chrome/Edge 实现简单的窗口,UI 通过 HTML/CSS/JS 实现
  2. webview - 比 lorca 功能更强,实现 UI 的思路差不多
  3. fyne - 使用 Canvas 绘制的 UI 框架,性能不错
  4. qt - 更复杂更强大的 UI 框架

我随便挑了个最简单的 Lorca 就开始了。

用 HTML/CSS/JS 制作 UI

我用 React + ReactRouter 来实现页面结构,文件上传和对话框是自己用原生 JS 写的,UI 细节用 CSS3 来做,没有依赖其他 UI 组件库。

Lorca 的主要功能就是展示我写的 index.html。

用 gin 实现后台接口

index.html 中的 JS 用到了五个接口,我使用 gin 来实现:

1
2
3
4
5
vbnet复制代码router.GET("/uploads/:path", controllers.UploadsController)              
router.GET("/api/v1/addresses", controllers.AddressesController)
router.GET("/api/v1/qrcodes", controllers.QrcodesController)
router.POST("/api/v1/files", controllers.FilesController)
router.POST("/api/v1/texts", controllers.TextsController)

其中的二维码生成我用的是 go-qrcode。

用 gorilla/websocket 实现手机通知 PC

这个库提供了一个聊天室的例子,稍微改一下就能为我所用了。

整体思路

总得来说:

  1. 用 Lorca 搞出一个窗口
  2. 用 HTML 制作界面,用 JS 调用后台接口
  3. 用 Gin 实现后台接口
  4. 上传的文件都放到 uploads 文件夹中

共 400 行 Go 代码,700 行 JS 代码。

如何使用

目前我只测试了 Windows 系统,能正常运行。理论上 macOS 和 Linux 也能运行,但我并没有测试。

你可以在 releases 页面 下载可执行文件,也可以自行编译源代码得到可执行文件。

Windows 用户须知

Windows 用户需要在防火墙的入站规则中运行 27149 端口的连接,否则此软件无法被手机访问。

自行编译

1
2
3
4
5
bash复制代码git clone git@github.com:FrankFang/synk.git
cd synk
cd server/frontend; yarn build; cd -
./scripts/build_for_mac.sh
./scripts/build_for_win.sh

我的收获

  1. 知道如何用 Go 写一个发布订阅(使用 websocket 的时候看到别人的源码了)
  2. 知道 ch2 <- ch1 与 ch2 <- (<-ch1) 的区别了,前者一般是写错了
  3. 知道了 Go 应用代码引入本地包的方法

本文转载自: 掘金

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

别再用 kill -9 了,这才是微服务上下线的正确姿势!

发表于 2021-10-14

对于微服务来说,服务的优雅上下线是必要的。

就上线来说,如果组件或者容器没有启动成功,就不应该对外暴露服务,对于下线来说,如果机器已经停机了,就应该保证服务已下线,如此可避免上游流量进入不健康的机器。

优雅下线

基础下线(Spring/SpringBoot/内置容器)

首先JVM本身是支持通过shutdownHook的方式优雅停机的。

1
2
3
4
5
6
scss复制代码Runtime.getRuntime().addShutdownHook(new Thread() {
@Override
public void run() {
close();
}
});

此方式支持在以下几种场景优雅停机:

1.程序正常退出
2.使用System.exit()
3.终端使用Ctrl+C
4.使用Kill pid干掉进程

那么如果你偏偏要kill -9 程序肯定是不知所措的。

而在Springboot中,其实已经帮你实现好了一个shutdownHook,支持响应Ctrl+c或者kill -15 TERM信号。

随便启动一个应用,然后Ctrl+c一下,观察日志就可知, 它在 AnnotationConfigEmbeddedWebApplicationContext 这个类中打印出了疑似Closing…的日志,真正的实现逻辑在其父类 AbstractApplicationContext 中(这个其实是spring中的类,意味着什么呢,在spring中就支持了对优雅停机的扩展)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
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
kotlin复制代码public void registerShutdownHook() {
if (this.shutdownHook == null) {
this.shutdownHook = new Thread() {
public void run() {
synchronized(AbstractApplicationContext.this.startupShutdownMonitor) {
AbstractApplicationContext.this.doClose();
}
}
};
Runtime.getRuntime().addShutdownHook(this.shutdownHook);
}

}

public void destroy() {
this.close();
}

public void close() {
Object var1 = this.startupShutdownMonitor;
synchronized(this.startupShutdownMonitor) {
this.doClose();
if (this.shutdownHook != null) {
try {
Runtime.getRuntime().removeShutdownHook(this.shutdownHook);
} catch (IllegalStateException var4) {
;
}
}

}
}

protected void doClose() {
if (this.active.get() && this.closed.compareAndSet(false, true)) {
if (this.logger.isInfoEnabled()) {
this.logger.info("Closing " + this);
}

LiveBeansView.unregisterApplicationContext(this);

try {
this.publishEvent((ApplicationEvent)(new ContextClosedEvent(this)));
} catch (Throwable var3) {
this.logger.warn("Exception thrown from ApplicationListener handling ContextClosedEvent", var3);
}

if (this.lifecycleProcessor != null) {
try {
this.lifecycleProcessor.onClose();
} catch (Throwable var2) {
this.logger.warn("Exception thrown from LifecycleProcessor on context close", var2);
}
}

this.destroyBeans();
this.closeBeanFactory();
this.onClose();
this.active.set(false);
}

}

我们能对它做些什么呢,其实很明显,在doClose方法中它发布了一个ContextClosedEvent的方法,不就是给我们扩展用的么。

于是我们可以写个监听器监听ContextClosedEvent,在发生事件的时候做下线逻辑,对微服务来说即是从注册中心中注销掉服务。

1
2
3
4
5
6
7
8
9
10
typescript复制代码@Component
public class GracefulShutdownListener implements ApplicationListener<ContextClosedEvent> {

@Override
public void onApplicationEvent(ContextClosedEvent contextClosedEvent){
//注销逻辑
zookeeperRegistry.unregister(mCurrentServiceURL);
...
}
}

可能会有疑问的是,微服务中一般来说,注销服务往往是优雅下线的第一步,接着才会执行停机操作,那么这个时候流量进来怎么办呢?

个人会建议是,在注销服务之后就可开启请求挡板拒绝流量了,通过微服务框架本身的故障转移功能去处理被拒绝的流量即可。

Docker中的下线

好有人说了,我用docker部署服务,支不支持优雅下线。

那来看看docker的一些停止命令都会干些啥:

一般来说,正常人可能会用docker stop或者docker kill 命令去关闭容器(当然如果上一步注册了USR2自定义信息,可能会通过docker exec kill -12去关闭)。

对于docker stop来说,它会发一个SIGTERM(kill -15 term信息)给容器的PID1进程,并且默认会等待10s,再发送一个SIGKILL(kill -9 信息)给PID1。

那么很明显,docker stop允许程序有个默认10s的反应时间去做一下优雅停机的操作,程序只要能对kill -15 信号做些反应就好了,如上一步描述。那么这是比较良好的方式。

当然如果shutdownHook方法执行了个50s,那肯定不优雅了。可以通过docker stop -t 加上等待时间。

外置容器的shutdown脚本(Jetty)

如果非要用外置容器方式部署(个人认为浪费资源并提升复杂度)。那么能不能优雅停机呢。

可以当然也是可以的,这里有两种方式:

首先RPC框架本身提供优雅上下线接口,以供调用来结束整个应用的生命周期,并且提供扩展点供开发者自定义服务下线自身的停机逻辑。同时调用该接口的操作会封装成一个preStop操作固化在jetty或者其他容器的shutdown脚本中,保证在容器停止之前先调用下线接口结束掉整个应用的生命周期。shutdown脚本中执行类发起下线服务 -> 关闭端口 -> 检查下线服务直至完成 -> 关闭容器的流程。

而更简单的另一种方法是直接在脚本中加入kill -15命令。
在这里插入图片描述
优雅上线

优雅上线相对来说可能会更加困难一些,因为没有什么默认的实现方式,但是总之呢,一个原则就是确保端口存在之后才上线服务。

springboot内置容器优雅上线

这个就很简单了,并且业界在应用层面的优雅上线均是在内置容器的前提下实现的,并且还可以配合一些列健康检查做文章。

参看sofa-boot的健康检查的源码,它会在程序启动的时候先对springboot的组件做一些健康检查,然后再对它自己搞得sofa的一些中间件做健康检查,整个健康检查的流程完毕之后(sofaboot 目前是没法对自身应用层面做健康检查的,它有写相关接口,但是写死了port is ready…)才会暴露服务或者说优雅上线,那么它健康检查的时机是什么时候呢:

1
2
3
4
5
6
7
8
scss复制代码@Override
public void onApplicationEvent(ContextRefreshedEvent event) {
healthCheckerProcessor.init();
healthIndicatorProcessor.init();
afterHealthCheckCallbackProcessor.init();
publishBeforeHealthCheckEvent();
readinessHealthCheck();
}

可以看到它是监听了ContextRefreshedEvent这个事件。在内置容器模式中,内置容器模式的start方法是在refreshContext方法中,方法执行完成之后发布一个ContextRefreshedEvent事件,也就是说在监听到这个事件的时候,内置容器必然是启动成功了的。

但ContextRefreshedEvent这个事件,在一些特定场景中由于种种原因,ContextRefreshedEvent会被监听到多次,没有办法保证当前是最后一次event,从而正确执行优雅上线逻辑。

在springboot中还有一个更加靠后的事件,叫做ApplicationReadyEvent,它的发布藏在了afterRefresh还要后面的那一句listeners.finished(context, null)中,完完全全可以保证内置容器 端口已经存在了,所以我们可以监听这个事件去做优雅上线的逻辑,甚至可以把中间件相关的健康检查集成在这里。

1
2
3
4
5
6
7
8
9
typescript复制代码@Component
public class GracefulStartupListener implements ApplicationListener<ApplicationReadyEvent> {
@Override
public void onApplicationEvent(ApplicationReadyEvent applicationReadyEvent){
//注册逻辑 优雅上线
apiRegister.register(urls);
...
}
}

外置容器(Jetty)优雅上线

目前大多数应用的部署模式不管是jetty部署模式还是docker部署模式(同样使用jetty镜像),本质上用的都是外置容器。那么这个情况就比较困难了,至少在应用层面无法观察到外部容器的运行状态,并且容器本身没有提供什么hook给你实现。

那么和优雅上线一样,需要RPC框架提供优雅上线接口来初始化整个应用的生命周期,并且提供扩展点给开发者供执行自定义的上线逻辑(上报版本探测信息等)。同样将调用这个接口封装成一个postStart操作,固化在jetty等外置容器的startup脚本中,保证应用在容器启动之后在上线。容器执行类似启动容器 -> 健康检查 -> 上线服务逻辑 -> 健康上线服务直至完成 的流程。
在这里插入图片描述
作者:fredalxin
地址:fredal.xin/graceful-so…

本文转载自: 掘金

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

SrpingBoot+WebMagic+MyBaties实现

发表于 2021-10-14

WebMagic是一个开源爬虫框架,本项目通过在SpringBoot项目中使用WebMagic去抓取数据,最后使用MyBatis将数据入库。

本项目代码地址:ArticleCrawler: SrpingBoot+WebMagic+MyBaties实现爬虫和数据入库 (gitee.com)

创建数据库:

本示例中库名为article,表名为cms_content,表中包含contentId、title、date三个字段。

1
2
3
4
5
6
mysql复制代码CREATE TABLE `cms_content` (
`contentId` varchar(40) NOT NULL COMMENT '内容ID',
`title` varchar(150) NOT NULL COMMENT '标题',
`date` varchar(150) NOT NULL COMMENT '发布日期',
PRIMARY KEY (`contentId`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='CMS内容表';

新建SpringBoot项目:

1、配置依赖pom.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
xml复制代码<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.5.5</version>
<relativePath/>
</parent>
<groupId>com.example</groupId>
<artifactId>Article</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>Article</name>
<description>Article</description>
<properties>
<java.version>1.8</java.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.test.skip>true</maven.test.skip>
<maven.compiler.plugin.version>3.8.1</maven.compiler.plugin.version>
<maven.resources.plugin.version>3.1.0</maven.resources.plugin.version>

<mysql.connector.version>5.1.47</mysql.connector.version>
<druid.spring.boot.starter.version>1.1.17</druid.spring.boot.starter.version>
<mybatis.spring.boot.starter.version>1.3.4</mybatis.spring.boot.starter.version>
<fastjson.version>1.2.58</fastjson.version>
<commons.lang3.version>3.9</commons.lang3.version>
<joda.time.version>2.10.2</joda.time.version>
<webmagic.core.version>0.7.5</webmagic.core.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.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>

<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>${mysql.connector.version}</version>
</dependency>

<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>${druid.spring.boot.starter.version}</version>
</dependency>

<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>${mybatis.spring.boot.starter.version}</version>
</dependency>

<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>${fastjson.version}</version>
</dependency>

<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>${commons.lang3.version}</version>
</dependency>

<dependency>
<groupId>joda-time</groupId>
<artifactId>joda-time</artifactId>
<version>${joda.time.version}</version>
</dependency>

<dependency>
<groupId>us.codecraft</groupId>
<artifactId>webmagic-core</artifactId>
<version>${webmagic.core.version}</version>
<exclusions>
<exclusion>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
</exclusion>
</exclusions>
</dependency>

</dependencies>

<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>${maven.compiler.plugin.version}</version>
<configuration>
<source>${java.version}</source>
<target>${java.version}</target>
<encoding>${project.build.sourceEncoding}</encoding>
</configuration>
</plugin>

<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-resources-plugin</artifactId>
<version>${maven.resources.plugin.version}</version>
<configuration>
<encoding>${project.build.sourceEncoding}</encoding>
</configuration>
</plugin>

<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<fork>true</fork>
<addResources>true</addResources>
</configuration>
<executions>
<execution>
<goals>
<goal>repackage</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>

<repositories>
<repository>
<id>public</id>
<name>aliyun nexus</name>
<url>http://maven.aliyun.com/nexus/content/groups/public/</url>
<releases>
<enabled>true</enabled>
</releases>
</repository>
</repositories>

<pluginRepositories>
<pluginRepository>
<id>public</id>
<name>aliyun nexus</name>
<url>http://maven.aliyun.com/nexus/content/groups/public/</url>
<releases>
<enabled>true</enabled>
</releases>
<snapshots>
<enabled>false</enabled>
</snapshots>
</pluginRepository>
</pluginRepositories>

</project>

2、创建CmsContentPO.java

数据实体,和表中3个字段对应。

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
java复制代码package site.exciter.article.model;

public class CmsContentPO {
private String contentId;

private String title;

private String date;

public String getContentId() {
return contentId;
}

public void setContentId(String contentId) {
this.contentId = contentId;
}

public String getTitle() {
return title;
}

public void setTitle(String title) {
this.title = title;
}

public String getDate() {
return date;
}

public void setDate(String date) {
this.date = date;
}
}

3、创建CrawlerMapper.java

1
2
3
4
5
6
7
8
9
java复制代码package site.exciter.article.dao;

import org.apache.ibatis.annotations.Mapper;
import site.exciter.article.model.CmsContentPO;

@Mapper
public interface CrawlerMapper {
int addCmsContent(CmsContentPO record);
}

4、配置映射文件CrawlerMapper.xml

在resources下新建mapper文件夹,在mapper下创建CrawlerMapper.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
xml复制代码<?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="site.exciter.article.dao.CrawlerMapper">

<insert id="addCmsContent" parameterType="site.exciter.article.model.CmsContentPO">
insert into cms_content (contentId,
title,
date)
values (#{contentId,jdbcType=VARCHAR},
#{title,jdbcType=VARCHAR},
#{date,jdbcType=VARCHAR})
</insert>
</mapper>

5、配置application.properties

配置数据库和mybatis映射关系。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
bash复制代码# mysql
spring.datasource.name=mysql
spring.datasource.type=com.alibaba.druid.pool.DruidDataSource
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
spring.datasource.url=jdbc:mysql://10.201.61.184:3306/article?useUnicode=true&characterEncoding=utf8&useSSL=false&allowMultiQueries=true
spring.datasource.username=root
spring.datasource.password=root

# druid
spring.datasource.druid.initial-size=5
spring.datasource.druid.min-idle=5
spring.datasource.druid.max-active=10
spring.datasource.druid.max-wait=60000
spring.datasource.druid.validation-query=SELECT 1 FROM DUAL
spring.datasource.druid.test-on-borrow=false
spring.datasource.druid.test-on-return=false
spring.datasource.druid.test-while-idle=true
spring.datasource.druid.time-between-eviction-runs-millis=60000
spring.datasource.druid.min-evictable-idle-time-millis=300000
spring.datasource.druid.max-evictable-idle-time-millis=600000

# mybatis
mybatis.mapperLocations=classpath:mapper/CrawlerMapper.xml

6、创建ArticlePageProcessor.java

解析html的逻辑。

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
java复制代码package site.exciter.article;

import org.springframework.stereotype.Component;
import us.codecraft.webmagic.Page;
import us.codecraft.webmagic.Site;
import us.codecraft.webmagic.processor.PageProcessor;
import us.codecraft.webmagic.selector.Selectable;

@Component
public class ArticlePageProcessor implements PageProcessor {

private Site site = Site.me().setRetryTimes(3).setSleepTime(1000);

@Override
public void process(Page page) {
String detail_urls_Xpath = "//*[@class='postTitle']/a[@class='postTitle2']/@href";
String next_page_xpath = "//*[@id='nav_next_page']/a/@href";
String next_page_css = "#homepage_top_pager > div:nth-child(1) > a:nth-child(7)";
String title_xpath = "//h1[@class='postTitle']/a/span/text()";
String date_xpath = "//span[@id='post-date']/text()";
page.putField("title", page.getHtml().xpath(title_xpath).toString());
if (page.getResultItems().get("title") == null) {
page.setSkip(true);
}
page.putField("date", page.getHtml().xpath(date_xpath).toString());

if (page.getHtml().xpath(detail_urls_Xpath).match()) {
Selectable detailUrls = page.getHtml().xpath(detail_urls_Xpath);
page.addTargetRequests(detailUrls.all());
}

if (page.getHtml().xpath(next_page_xpath).match()) {
Selectable nextPageUrl = page.getHtml().xpath(next_page_xpath);
page.addTargetRequests(nextPageUrl.all());

} else if (page.getHtml().css(next_page_css).match()) {
Selectable nextPageUrl = page.getHtml().css(next_page_css).links();
page.addTargetRequests(nextPageUrl.all());
}
}

@Override
public Site getSite() {
return site;
}
}

7、创建ArticlePipeline.java

处理数据的持久化。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
java复制代码package site.exciter.article;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import site.exciter.article.model.CmsContentPO;
import site.exciter.article.dao.CrawlerMapper;
import us.codecraft.webmagic.ResultItems;
import us.codecraft.webmagic.Task;
import us.codecraft.webmagic.pipeline.Pipeline;

import java.util.UUID;

@Component
public class ArticlePipeline implements Pipeline {

private static final Logger LOGGER = LoggerFactory.getLogger(ArticlePipeline.class);

@Autowired
private CrawlerMapper crawlerMapper;

public void process(ResultItems resultItems, Task task) {
String title = resultItems.get("title");
String date = resultItems.get("date");

CmsContentPO contentPO = new CmsContentPO();
contentPO.setContentId(UUID.randomUUID().toString());
contentPO.setTitle(title);
contentPO.setDate(date);

try {
boolean success = crawlerMapper.addCmsContent(contentPO) > 0;
LOGGER.info("保存成功:{}", title);
} catch (Exception ex) {
LOGGER.error("保存失败", ex);
}
}
}

8、创建ArticleTask.java

执行抓取任务。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
java复制代码package site.exciter.article;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import us.codecraft.webmagic.Spider;

import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;

@Component
public class ArticleTask {
private static final Logger LOGGER = LoggerFactory.getLogger(ArticlePipeline.class);

@Autowired
private ArticlePipeline articlePipeline;

@Autowired
private ArticlePageProcessor articlePageProcessor;

private ScheduledExecutorService timer = Executors.newSingleThreadScheduledExecutor();

public void crawl() {
// 定时任务,每10分钟爬取一次
timer.scheduleWithFixedDelay(() -> {
Thread.currentThread().setName("ArticleCrawlerThread");

try {
Spider.create(articlePageProcessor)
.addUrl("http://www.cnblogs.com/dick159/default.html?page=2")
// 抓取到的数据存数据库
.addPipeline(articlePipeline)
// 开启5个线程抓取
.thread(5)
// 异步启动爬虫
.start();
} catch (Exception ex) {
LOGGER.error("定时抓取数据线程执行异常", ex);
}
}, 0, 10, TimeUnit.MINUTES);
}
}

9、修改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
java复制代码package site.exciter.article;

import org.mybatis.spring.annotation.MapperScan;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
@MapperScan(basePackages = "site.exciter.article.interface")
public class ArticleApplication implements CommandLineRunner {

@Autowired
private ArticleTask articleTask;

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

@Override
public void run(String... args) throws Exception {
articleTask.crawl();
}
}

10、执行application,开始抓数据并入库

本文转载自: 掘金

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

springcloud alibaba企业落地实战:getw

发表于 2021-10-14

小知识,大挑战!本文正在参与“程序员必备小知识”创作活动。

在spring cloud netflix时使用zuul作为官网,但是随着zuul2.0的多次跳票和getway异军突起。getway已经是一款主流的产品了,同时springcloud alibaba官网的推荐网关也是getway,所以在选型时不需要犹豫。干就完了。同时getway使用了webflux,较第一代网关更优秀。

1.为什么使用网关

使用网关后,对于系统相当有了一个统一的入口,例如你有100个微服务系统,现在只需要对外暴露网关的地址即可。同时所有的请求都是通过网关的分发,所以很方便的在网关上对请求拦截,重而实现反向代理,鉴权,流量控制,熔断,日志监控等。

2.实现

getway基础实现很简单。鉴权等功能会在以后章节中发布。

1.添加pom.xml

1
2
3
4
5
6
7
8
xml复制代码        <dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>

注意:getway使用的是webflux,不要引入spring-boot-starter-web

2.修改启动类

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

public static void main(String[] args) {
//去除nacos日志
System.setProperty("nacos.logging.default.config.enabled", "false");
SpringApplication.run(GetwayApplication.class, args);
}

}

3.bootstrap.yml

getway也需要注册到nacos中。也需要将namespace和group对应。

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
yaml复制代码server:
port: 6001
spring:
application:
name: gateway-server
cloud:
nacos:
discovery:
# 服务注册地址
server-addr: 192.168.xx.xx:8848
#命名空间
namespace: b80f0aa4-3af2-a6e3-c6fda24c2bc0
#分组
group: xxx
config:
# 配置中心地址
server-addr: 192.168.xx.xx:8848
# 配置文件格式
file-extension: yml
#命名空间
namespace: b80f0aa4-3af2-a6e3-c6fda24c2bc0
#分组
group: xxx
gateway:
routes:
# 路由标识(id:标识,具有唯一性)
- id: auth
# 目标服务地址(uri:地址,请求转发后的地址) 此处是nacos中服务名称
uri: lb://auth-server
# 路由条件(predicates:断言,匹配 HTTP 请求内容)
predicates:
## 当路径为/auth/xx/xx的请求都会转发到auth-server (也就是请求system-resource/auth/xx/xx)
- Path=/auth/**
- id: systemresource
uri: lb://system-resource
predicates:
- Path=/system/**

楼主因为在小公司,所以只使用该规则就满足了业务要求,如果有其他规则使用,请自行百度,这里不具体介绍。

注意:

  1. getway只能获取同namespace和group中的服务。
  2. websocket接口与普通接口用以上配置就可以同时接受,不用额外配置路由。

3.配置oauth2的跨域

spring cloud ouath2 + getway跨域是有特别的配置的(与普通跨域不同)需要特别注意。

1.修改bootstrap.yml

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
yaml复制代码spring:
cloud:
gateway:
globalcors:
add-to-simple-url-handler-mapping: true
corsConfigurations:
'[/**]':
# 支持跨域访问的来源 也就是前台地址 可以配置多个 方法如下
allowedOrigins:
- "http://192.168.xx.xx:8080"
- "http://192.168.xx.xx:8080"
# 切记 allowCredentials 配置 为true时,allowedOrigins不能为 *
allowCredentials: true
maxAge: 86400
# 支持的方法 * 代表所有
allowedMethods: "*"
allowedHeaders: "*"
exposedHeaders: "setToken"
routes:
# 路由标识(id:标识,具有唯一性) 截取请求
- id: auth
# 目标服务地址(uri:地址,请求转发后的地址)
uri: lb://auth
# 路由条件(predicates:断言,匹配 HTTP 请求内容)
predicates:
## 转发地址格式为 uri/archive,/str 部分会被下面的过滤器给截取掉
- Path=/auth/**

2.添加GatewayConfig

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
kotlin复制代码import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.cloud.context.config.annotation.RefreshScope;
import org.springframework.cloud.gateway.config.GlobalCorsProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.web.cors.reactive.CorsWebFilter;
import org.springframework.web.cors.reactive.UrlBasedCorsConfigurationSource;
import org.springframework.web.util.pattern.PathPatternParser;

@Configuration
@EnableConfigurationProperties(GlobalCorsProperties.class)
public class GatewayConfig {

/**
* 配置全局解决cors跨域问题
*
* @return
*/
@Order(Ordered.HIGHEST_PRECEDENCE)
@RefreshScope
@Bean
public CorsWebFilter corsWebFilter(GlobalCorsProperties globalCorsProperties){
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(new PathPatternParser());
globalCorsProperties.getCorsConfigurations().forEach((k,v) -> source.registerCorsConfiguration(k, v));
return new CorsWebFilter(source);
}
}

注意如果网关中添加了跨域配置,业务服务就不要添加了,否则就会报错!

本文转载自: 掘金

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

1…490491492…956

开发者博客

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