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

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


  • 首页

  • 归档

  • 搜索

SpringBoot事件监听机制,并实现业务解耦 本章目标

发表于 2021-11-25

ApplicationEvent以及Listener是Spring为我们提供的一个事件监听、订阅的实现,内部实现原理是观察者设计模式,设计初衷也是为了系统业务逻辑之间的解耦,提高可扩展性以及可维护性。事件发布者并不需要考虑谁去监听,监听具体的实现内容是什么,发布者的工作只是为了发布事件而已。

applicationContext.publishEvent:相当于生产者

Event:相当于事件,在里面可以携带参数,该参数可以类比body,相当于消息体

Listener:相当于消费者

所以整体就是一个单机版的mq,在单机中也可以使用这种方式进行解耦和异步处理

本章目标

我们本章在SpringBoot平台上通过ApplicationEvents以及Listener来完成简单的注册事件流程。

构建项目

我们本章只是简单的讲解如何使用ApplicationEvent以及Listener来完成业务逻辑的解耦,不涉及到数据交互所以依赖需要引入的也比较少,项目pom.xml配置文件如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
xml复制代码<dependencies>
<!--web-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!--lombok-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.16.16</version>
</dependency>
<!--test-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>

Event事件

1
2
3
4
5
6
7
8
9
10
java复制代码@Getter
public class UserRegisterEvent extends ApplicationEvent {

private User user;

public UserRegisterEvent(Object source,User user) {
super(source);
this.user = user;
}
}

我们自定义事件UserRegisterEvent继承了ApplicationEvent,继承后必须重载构造函数,构造函数的参数可以任意指定,其中source参数指的是发生事件的对象,一般我们在发布事件时使用的是this关键字代替本类对象,而user参数是我们自定义的注册用户对象,该对象可以在监听内被获取。

在Spring内部中有多种方式实现监听如:@EventListener注解、实现ApplicationListener泛型接口、实现SmartApplicationListener接口等,我们下面来讲解下这三种方式分别如何实现。

创建User

我们简单创建一个用户实体,并添加两个字段:用户名、密码。实体代码如下所示:

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

private String username;

private String password;

}

创建UserController

创建一个@RestController控制器,对应添加一个注册方法简单实现,代码如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
kotlin复制代码@RestController
public class UserController {

@Autowired
UserService userService;

@GetMapping("/register")
public void registerUser() {
User user = new User();
user.setUsername("小白");
user.setPassword("123");
userService.registerUser(user);
}

}

创建UserService

UserService内添加一个注册方法,该方法只是实现注册事件发布功能,代码如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
java复制代码@Slf4j
@Service
public class UserService {

@Autowired
ApplicationContext applicationContext;

@Autowired
private AsyncService asyncService;

public void registerUser(User user) {
log.info("执行业务代码!");
applicationContext.publishEvent(new UserRegisterEvent(this, user));

applicationContext.publishEvent(new UserMailEvent(this, user));

asyncService.testAsync(user);
}
}

事件发布是由ApplicationContext对象管控的,我们发布事件前需要注入ApplicationContext对象调用publishEvent方法完成事件发布。

@EventListener实现监听

注解方式比较简单,并不需要实现任何接口,具体代码实现如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
java复制代码@Component
@Slf4j
public class AnnotationRegisterListener {

@Async
@EventListener(UserRegisterEvent.class)
public void register(UserRegisterEvent userRegisterEvent) {
User user = userRegisterEvent.getUser();
log.info("监听注册,{}", user);
}

@Async
@EventListener(UserMailEvent.class)
public void sendMail(UserMailEvent userMailEvent) {
User user = userMailEvent.getUser();
log.info("监听发送邮件,{}", user);
}

}

我们只需要让我们的监听类被Spring所管理即可,在我们用户注册监听实现方法上添加@EventListener注解,该注解会根据方法内配置的事件完成监听。下面我们启动项目来测试下我们事件发布时是否被监听者所感知。

ApplicationListener实现监听

这种方式也是Spring之前比较常用的监听事件方式,在实现ApplicationListener接口时需要将监听事件作为泛型传递,监听实现代码如下所示:

1
2
3
4
5
6
7
8
9
10
java复制代码@Slf4j
@Component
public class RegisterApplicationListener implements ApplicationListener<UserRegisterEvent> {

@Async
@Override
public void onApplicationEvent(UserRegisterEvent userRegisterEvent) {
log.info("ApplicationListener,{}", userRegisterEvent.getUser());
}
}

我们看到了控制台打印了我们监听内输出用户信息,事件发布后就不会考虑具体哪个监听去处理业务,甚至可以存在多个监听同时需要处理业务逻辑。

我们在注册时如果不仅仅是记录注册信息到数据库,还需要发送邮件通知用户,当然我们可以创建多个监听同时监听UserRegisterEvent事件,接下来我们先来实现这个需求。

邮件通知监听

我们使用注解的方式来完成邮件发送监听实现,代码如下所示:

1
2
3
4
5
6
7
8
9
10
java复制代码@Getter
public class UserMailEvent extends ApplicationEvent {

private User user;

public UserMailEvent(Object source,User user) {
super(source);
this.user = user;
}
}

我们看到控制台输出的内容感到比较疑惑,我注册时用户信息写入数据库应该在发送邮件前面,为什么没有在第一步执行呢?
好了,证明了一点,事件监听是无序的,监听到的事件先后顺序完全随机出现的。我们接下来使用SmartApplicationListener实现监听方式来实现该逻辑。

SmartApplicationListener实现有序监听

我们对注册用户以及发送邮件的监听重新编写,注册用户写入数据库监听代码如下所示:

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
typescript复制代码@Component
public class UserRegisterListener implements SmartApplicationListener
{
/**
* 该方法返回true&supportsSourceType同样返回true时,才会调用该监听内的onApplicationEvent方法
* @param aClass 接收到的监听事件类型
* @return
*/
@Override
public boolean supportsEventType(Class<? extends ApplicationEvent> aClass) {
//只有UserRegisterEvent监听类型才会执行下面逻辑
return aClass == UserRegisterEvent.class;
}

/**
* 该方法返回true&supportsEventType同样返回true时,才会调用该监听内的onApplicationEvent方法
* @param aClass
* @return
*/
@Override
public boolean supportsSourceType(Class<?> aClass) {
//只有在UserService内发布的UserRegisterEvent事件时才会执行下面逻辑
return aClass == UserService.class;
}

/**
* supportsEventType & supportsSourceType 两个方法返回true时调用该方法执行业务逻辑
* @param applicationEvent 具体监听实例,这里是UserRegisterEvent
*/
@Override
public void onApplicationEvent(ApplicationEvent applicationEvent) {

//转换事件类型
UserRegisterEvent userRegisterEvent = (UserRegisterEvent) applicationEvent;
//获取注册用户对象信息
UserBean user = userRegisterEvent.getUser();
//.../完成注册业务逻辑
System.out.println("注册信息,用户名:"+user.getName()+",密码:"+user.getPassword());
}

/**
* 同步情况下监听执行的顺序
* @return
*/
@Override
public int getOrder() {
return 0;
}
}

SmartApplicationListener接口继承了全局监听ApplicationListener,并且泛型对象使用的ApplicationEvent来作为全局监听,可以理解为使用SmartApplicationListener作为监听父接口的实现,监听所有事件发布。

既然是监听所有的事件发布,那么SmartApplicationListener接口添加了两个方法supportsEventType、supportsSourceType来作为区分是否是我们监听的事件,只有这两个方法同时返回true时才会执行onApplicationEvent方法。

可以看到除了上面的方法,还提供了一个getOrder方法,这个方法就可以解决执行监听的顺序问题,return的数值越小证明优先级越高,执行顺序越靠前。

注册成功发送邮件通知监听代码如下所示:

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
typescript复制代码@Component
public class UserRegisterSendMailListener implements SmartApplicationListener
{
/**
* 该方法返回true&supportsSourceType同样返回true时,才会调用该监听内的onApplicationEvent方法
* @param aClass 接收到的监听事件类型
* @return
*/
@Override
public boolean supportsEventType(Class<? extends ApplicationEvent> aClass) {
//只有UserRegisterEvent监听类型才会执行下面逻辑
return aClass == UserRegisterEvent.class;
}

/**
* 该方法返回true&supportsEventType同样返回true时,才会调用该监听内的onApplicationEvent方法
* @param aClass
* @return
*/
@Override
public boolean supportsSourceType(Class<?> aClass) {
//只有在UserService内发布的UserRegisterEvent事件时才会执行下面逻辑
return aClass == UserService.class;
}

/**
* supportsEventType & supportsSourceType 两个方法返回true时调用该方法执行业务逻辑
* @param applicationEvent 具体监听实例,这里是UserRegisterEvent
*/
@Override
public void onApplicationEvent(ApplicationEvent applicationEvent) {
//转换事件类型
UserRegisterEvent userRegisterEvent = (UserRegisterEvent) applicationEvent;
//获取注册用户对象信息
UserBean user = userRegisterEvent.getUser();
System.out.println("用户:"+user.getName()+",注册成功,发送邮件通知。");
}

/**
* 同步情况下监听执行的顺序
* @return
*/
@Override
public int getOrder() {
return 1;
}
}

如果说我们不希望在执行监听时等待监听业务逻辑耗时,发布监听后立即要对接口或者界面做出反映,我们该怎么做呢?

使用@Async实现异步监听

@Aysnc其实是Spring内的一个组件,可以完成对类内单个或者多个方法实现异步调用,这样可以大大的节省等待耗时。内部实现机制是线程池任务ThreadPoolTaskExecutor,通过线程池来对配置@Async的方法或者类做出执行动作。

线程任务池配置

我们创建一个ListenerAsyncConfiguration,并且使用@EnableAsync注解开启支持异步处理,具体代码如下所示:

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复制代码@Configuration
@EnableAsync
public class ListenerAsyncConfiguration implements AsyncConfigurer
{
/**
* 获取异步线程池执行对象
* @return
*/
@Override
public Executor getAsyncExecutor() {
//使用Spring内置线程池任务对象
ThreadPoolTaskExecutor taskExecutor = new ThreadPoolTaskExecutor();
//设置线程池参数
taskExecutor.setCorePoolSize(5);
taskExecutor.setMaxPoolSize(10);
taskExecutor.setQueueCapacity(25);
taskExecutor.initialize();
return taskExecutor;
}

@Override
public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() {
return null;
}
}

我们自定义的监听异步配置类实现了AsyncConfigurer接口并且实现内getAsyncExecutor方法以提供线程任务池对象的获取。
我们只需要在异步方法上添加@Async注解就可以实现方法的异步调用,为了证明这一点,我们在发送邮件onApplicationEvent方法内添加线程阻塞3秒,修改后的代码如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
java复制代码/*** supportsEventType & supportsSourceType 两个方法返回true时调用该方法执行业务逻辑
* @param applicationEvent 具体监听实例,这里是UserRegisterEvent
*/
@Override
@Async
public void onApplicationEvent(ApplicationEvent applicationEvent) {
try {
Thread.sleep(3000);//静静的沉睡3秒钟
}catch (Exception e)
{
e.printStackTrace();
}
//转换事件类型
UserRegisterEvent userRegisterEvent = (UserRegisterEvent) applicationEvent;
//获取注册用户对象信息
UserBean user = userRegisterEvent.getUser();
System.out.println("用户:"+user.getName()+",注册成功,发送邮件通知。");
}

总结

我们在传统项目中往往各个业务逻辑之间耦合性较强,因为我们在service都是直接引用的关联service或者jpa来作为协作处理逻辑,然而这种方式在后期更新、维护性难度都是大大提高了。然而我们采用事件通知、事件监听形式来处理逻辑时耦合性则是可以降到最小。

本文转载自: 掘金

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

Go语言入门系列一

发表于 2021-11-25

公众号:畅游码海 里面有更多精品原创文章~

为什么要学Go语言?

  1. 开发效率和运行效率二者的完美融合,天生的并发编程支持。
  2. 完善的标准库包括互联网应用、系统编程和网络编程。
  3. 部署起来非常的方便
  4. 拥有强大的编译检查、严格的编码规范和完整的软件生命周期工具

编译程序

方式一:直接运行

1
go复制代码go run xxxx.go

方式二:build方式

1
go复制代码go build xxx.go

变量声明

Go 语言的变量的声明都是类型信息放在变量之后

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
Go复制代码/// 如下:
var v1 int
var v2 string
var v3 [10]int //数组
var v4 []int //数组切片
var v5 struct {
f int
}
var v6 * int
var v7 map[string] int // map 类型 key 为string value 为int类型
var v8 func(a int) int

// var 可以把var 关键词使用花括号括起来 这样可以不用重复写var
var {
v1 int
v2 string
}

变量的初始化方式

1
2
3
4
Go复制代码var v1 int = 10
var v2 = 10 //可以自动推断类型
v3 := 10 // 可以是这样初始化一个变量
//缺点:不能提供数据类型、只能在函数内部

三种类型声明的变量都不应该声明过 否则会出现编译错误

避免想修改全局变量结果不小心定义了一个新的局部变量。

变量赋值

1
2
3
4
5
6
7
8
9
10
11
go复制代码// 新式的赋值方式
i , j = j , i //可以用来交换两个变量

// 在接受函数的返回值的时候可以提供匿名变量

func GetName() (firstName, lastName, nickName string){
return "MAy" , "Chan" , "Chibi Maruko"
}

//此时如果只想获得nickName的话可以
_, _, nickName := GetName

退化赋值

前提:最少有一个新变量被定义,且必须是同一作用域

1
2
3
4
5
6
7
8
9
10
11
12
13
go复制代码func main(){
x := 100

···

x := 200 //不满足至少一个新变量被定义,不会出现退化赋值

//x,y :=200, 300 //满足条件,会出现退化赋值
{
//x,y := 200, 300 //不在同一作用域,不会出现退化赋值
}

}

通常err变量利用退化赋值,来实现重复使用

常量

Go 中常量类型可以是数值类型等

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
go复制代码//使用const 可以用来定义常量
const pi float64 = 3.1415
cosnt zero = 0.0
const {
size int64 = 10
eof = -1
}

const u , v float32 = 0.3
const a, b , c = 3 , 4 , "foo"

cosnt mask = 1 << 3 // 这样是可以的因为这在编译期间是可以确定的

//预定义常量 true false iota
//iota是一个每次出现都会自增1 的值 其在每个const的开头都被重置为0

cosnt {
c0 = iota //0
c1 = iota //1
c3 = iota //2
}
//或者
const{
c0 = iota //0
c1 //1
c3 //2
}

cosnt x = iota //0
const y = iota //0

布尔类型不能够转换为其它类型

两种不同类型的整数是不能进行比较的

取反在C语言中是 ~x ,在go 中是 ^x

浮点数不是一种精确的表示方式所以不要直接比较可以使用函数 IsEqual() 函数来比较

复数类型

1
2
3
4
5
6
7
8
9
go复制代码var value complex64  // 实际是由float32构成的复数类型

//三种赋值形式
value = 3.2 + 12i
value := 3.2 + 12i
value := complex(3.2, 12)

//可以使用real(value) 来获取实部
//可以使用imag(value)获取虚部

字符串类型

**字符串可以用来初始化 ,但是和数组不同 不可以再次改变否则会编译错误,不过可以向数组一样进行下表的字符取值 **

我们常常使用len函数来对字符串进行长度的获取

Go语言支持UTF-8编码 因此可以中英文混在一起,源文件注意保存为UTF-8形式

我们常常使用iconv库来处理文本文档

每个中文字符在UTF-8中占3个字节

1
2
3
4
5
6
7
8
9
10
11
12
go复制代码//两种遍历方式

//以字符数组方式进行遍历
str := "Hello,世界"
n := len(str)
for i := 0 , i < n , i++ {
ch := str[i]
}
//以Unicode方式遍历
for i , ch := range str{
fmt.Println(i, ch)
}

引用类型

包含:silice、map、channel这三种预定义类型

引用类型除分配内存外,还须初始化一系列属性,诸如指针、长度、甚至包括哈希分布和数据队列

new 函数可为引用类型分配内存,但是这不是完整创建的。仅分配了数据类型本身(指针包装)所需的内存,并没有分配键值存储的内存,也没有初始化一些内部属性

类型转换要求:显式类型转换

自定义类型

使用关键字type定义用户自定义类型,包括基于现有基础类型创建,或者是结构体、函数类型等

注意:即便指定了基础类型,也只表面它们有相同的底层数据结构,两者间不存在任何关系,属于完全不同的两种类型。除操作符外,自定义类型不会继承基础类型的其他信息(包括方法)。不能视为别名,不能隐式转换,不能直接用于比较表达式

1
2
3
4
5
6
7
8
9
10
go复制代码type flags byte
const {
read flags = 1 << itoa
write
exec
}
func main() {
f := read | exec
fmt.Printf("%b\n",f) //输出二进制标记位
}

未完待续...

本文转载自: 掘金

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

BorderLayout布局

发表于 2021-11-25

「这是我参与11月更文挑战的第24天,活动详情查看:2021最后一次更文挑战」

BorderLayout布局

BorderLayout也是一种简单的布局策略,如果一个容器使用这种布局,那么容器空间简单地划分为东、西、南、北、中5个区域,中间的区域最大。每加入一个组件都应该指明把这个组件加在哪个区域中,区域由BorderLayout中的静态常量CENTER、NORTH、SOUTH.WEST、EAST表示,例如,一个使用BorderLayout布局的容器con,可以使用add方法将一个组件b添加到中心区域:

con.add(b,BorderLayout.CENTER);

添加到某个区域的组件将占据整个这个区域。每个区域只能放置一个组件,如果向某个已放置了组件的区域再放置一个组件,那么先前的组件将被后者替换掉。使用BorderLayout布局的容器最多能添加5个组件,如果容器中需要加入超过5个组件,就必须使用容器的嵌套或改用其他的布局策略。

边界布局管理器把容器的的布局分为五个位置:CENTER、EAST、WEST、NORTH、SOUTH。依次对应为:上北(NORTH)、下南(SOUTH)、左西(WEST)、右东(EAST),中(CENTER),如下图所示。

image.png

BorderLayout布局管理器

1.BorderLayout类在java.awt包中

2.JFrame类的默认布局是BorderLayout

3.BorderLayout布局把容器划分为东、西、南、
北、中五个区域,中间区域最大

在这里插入图片描述

2.添加组件的方法有两个:

• add(x, BorderLayout.pos)

• add(BorderLayout.pos, x)

• pos是NORTH, SOUTH, EAST, WEST,CENTER之一

BorderLayout布局

1.东、西区和南、北区充满程度不一样

2.BorderLayout布局的容器某个位置的某个组件占据了整个区域,而不是选择一个合适自己的尺寸

3.在某个位置重复添加组件的效果是:只有最后一个添加的组件可见

4.缺少某个位置的组件时,其他位置的组件会侵占该位置

边框布局管理器

1.BorderLayout(边框布局管理器)是 Window、JFrame 和 JDialog 的默认布局管理器

2.边框布局管理器将窗口分为 5 个区域:North、South、East、West 和 Center边框布局管理器将窗口分为 5 个区域:North、South、East、West 和 Center

3.North 表示北,将占据面板的上方;South 表示南,将占据面板的下方;East表示东,将占据面板的右侧;West 表示西,将占据面板的左侧;中间区域 Center 是在东、南、西、北都填满后剩下的区域

本文转载自: 掘金

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

yaml解析工具类(java)

发表于 2021-11-25

这是我参与11月更文挑战的第16天,活动详情查看:2021最后一次更文挑战

山有峰顶,海有彼岸,漫漫长路,终有回转,余味苦涩,终会有回甘。别被眼前的磨难打败了,或许光明就在你放弃前的那一刻。带着愉快的心情做一个愉快的梦,醒来后,又是新的一天。

简介

这是基于snakeyaml实现的一个读取yaml配置的工具类.
主要功能是获取指定层级之下的数据.

我不知道当初为什么写这个类了,但是我知道,我要是不把他发出来我就是白写了😂

主要功能和使用方法

demo.yml

1
2
3
4
5
6
7
8
9
10
11
yaml复制代码  zdc:
config:
key: value
list:
- 张三
- 李四
map:
- name: 张三map
age: 12
- name: 李四map
age: 121
  1. 支持获取指定层级之后的内容,并返回Map形式或Obj形式
    new ConfigBean("指定文件").prefix("zdc.config").getMap()

new ConfigBean("指定文件").prefix("zdc.config").getObj()

  1. 支持获取指定层级之后的对象数据,并格式化为指定类,如”zdc.config” :
    new ConfigBean("指定文件").prefix("zdc.config").getT(A.class)
  2. 支持获取list中指定数据,如”zdc.config.list.1” :
    new ConfigBean().prefix("zdc.config.list.1").getObj()
  3. 支持获取list中指定数据之后的数据,如”zdc.config.map.1.name”

new ConfigBean().prefix("zdc.config.list.1").getString()

具体代码

需要的maven包

1
2
3
4
5
6
7
8
9
10
xml复制代码<dependency>
<groupId>org.yaml</groupId>
<artifactId>snakeyaml</artifactId>
<version>1.25</version>
</dependency>
<dependency>
<groupId>commons-beanutils</groupId>
<artifactId>commons-beanutils-core</artifactId>
<version>1.8.3</version>
</dependency>

java代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
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
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
java复制代码package zdc.utils;

import org.apache.commons.beanutils.BeanUtils;
import org.yaml.snakeyaml.Yaml;

import java.io.InputStream;
import java.util.List;
import java.util.Map;

/**
* 读取resources下的yaml配置类
*
*使用的maven包
* <dependency>
* <groupId>org.yaml</groupId>
* <artifactId>snakeyaml</artifactId>
* <version>1.25</version>
* </dependency>
* map->bean使用此包
* <dependency>
* <groupId>commons-beanutils</groupId>
* <artifactId>commons-beanutils-core</artifactId>
* <version>1.8.3</version>
* </dependency>
*
*使用方法
*eg:
* zdc:
* config:
* key: value
* list:
* - 张三
* - 李四
* map:
* - name: 张三map
* age: 12
* - name: 李四map
* age: 121
*
*支持获取指定层级之后的数据,如"zdc.config" : new ConfigBean().prefix("zdc.config").getObj()
*
*支持获取list中指定数据,如"zdc.config.list.1" : new ConfigBean().prefix("zdc.config.list.1").getObj()
*
*支持获取list中指定数据之后的数据,如"zdc.config.map.1.name" :new ConfigBean().prefix("zdc.config.list.1").getString()
*
* @author ZDC
*/
public class ConfigBean {

/**
* 读取的资源名
*/
private String fileName ="application.yml";
/**
* 获取的对象
*/
private Object temp;


/**
* 创建一个资源获取对象,默认获取resources下的application.yml文件
*/
public ConfigBean() {
this.load();
}

/**
* 创建一个资源获取对象,默认获取resources下的fileName文件
* @param fileName
*/
public ConfigBean(String fileName) {
this.fileName =fileName;
this.load();
}

/**
* 加载指定的文件
*/
private ConfigBean load() {
Yaml yaml = new Yaml();
InputStream inputStream = this.getClass()
.getClassLoader()
.getResourceAsStream(this.fileName);
this.temp= yaml.load(inputStream);
return this;
}



/**
* eg "zdc.config.list"
* eg ""
* @param prefix
*/
public ConfigBean prefix(String prefix){

if(prefix==null || "".equals(prefix.trim())){
return this;
}
//获取层级关系
String[] keys = prefix.trim().split("\\.");
for (String key : keys) {
//判断数据类型
if(this.temp instanceof Map){
this.temp= ((Map) this.temp).get(key);
}
else if(this.temp instanceof List){
if (isNumeric(key)) {
this.temp= ((List) this.temp).get(Integer.parseInt(key));
}else{
throw new RuntimeException(String.format("当前层级类型为List,不能使用[%s]获取子集数据",key));
}
}else{
throw new RuntimeException("暂时没有解析该类型或不支持再次解析");
}
}
return this;
}

/**
* 返回对象类型的数据,可能是List,Map,Obj
* @return
*/
public Object getObj(){
return this.temp;
}

/**
* 返回Map类型的数据
* @return
*/
public Map getMap() {
if(this.temp instanceof Map){
return (Map)this.temp;
}
return null;
}

/**
* 返回List类型的数据
* @return
*/
public List getList() {
if(this.temp instanceof List){
return (List)this.temp;
}
return null;
}

/**
* 返回对象类型数据,如果集成其他类库可以直接调用其他类库的map2bean方法 该处引用的是 commons-beanutils-core ,可以根据自己本地环境替换
*
* @param clazz
* @param <T>
* @return
*/
public <T> T getT(Class<T> clazz) throws Exception {
T obj = clazz.newInstance();
Map map = this.getMap();
BeanUtils.populate(obj,map);
return obj;
}

/**
* 返回String类型的数据
* @return
*/
public String getString() {
return this.temp==null ? "":this.temp.toString();
}

/**
* 返回Integer类型的数据
* @return
*/
public Integer getInteger() {
String string = getString();
return string!=null? Integer.parseInt(string):null;
}

//TODO 可以自定也解析其他类型



/**
* 判断是否是数字
* @param cs
* @return
*/
public static boolean isNumeric(final CharSequence cs) {
if (cs == null || cs.length() == 0) {
return false;
}
final int sz = cs.length();
for (int i = 0; i < sz; i++) {
if (!Character.isDigit(cs.charAt(i))) {
return false;
}
}
return true;
}


}

这个系列的文章,总体来说是一些杂乱的记录.

其中的内容可能是在之前某个需要的时候记录下来的,当时需要,当时很有用.

但是在后续更多的时间里,他被封禁到了小角落.

你看不看,他都在那里,他并非一无是处.

你看到这里就给个三连吧

1
2
3
4
arduino复制代码    作者:ZOUZDC
链接:https://juejin.cn/post/7028963866063306760
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

本文转载自: 掘金

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

BeautifulSoup的使用(三)

发表于 2021-11-25

bs4的find_all的使用方法(重点)

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
python复制代码from bs4 import BeautifulSoup
import re

html = """
<html><head><title>The Dormouse's story</title></head>
<body>
<p class="title" name="dromouse"><b>The Dormouse's story</b></p>
<p class="story">Once upon a time there were three little sisters;
and their names were
<a href="http://example.com/elsie" class="sister" id="link1"><!-- E
lsie --></a>,
<a href="http://example.com/lacie" class="sister" id="link2">Lacie<
/a> and
<a href="http://example.com/tillie" class="sister" id="link3">Tilli
e</a>;
and they lived at the bottom of a well.</p>
<p class="story">...</p>
"""

soup = BeautifulSoup(html, "lxml") # "lxml解析方式"
print soup.find("p") # 找到第一个p标签
print soup.find_all("p") # 找到所有p标签,列表形式返回
print soup.find_all("p")[2] # 找到所有p标签,列表形式返回

for tag in soup.find_all(re.compile("^b")): # 使用正则的方式进行匹配
print tag.name
"""
<body>...</body>
<b>...</b>
"""

print soup.find_all(["title","b"]) # 按照列表进行搜索,只要有一个符合就返回数据
"""
[<title>The Dormouse's story</title>, <b>The Dormouse's story</b>]
"""

print soup.find_all(id="link2")
"""
[<a class="sister" href="http://example.com/lacie" id="link2">Lacie&lt;\n/a&gt; and\n</a>]
"""

print(soup.find_all(class_="sister")) # class特殊 使用class_
"""
[<a class="sister" href="http://example.com/elsie" id="link1"><!-- E\nlsie --></a>, <a class="sister" href="http://example.com/lacie" id="link2">Lacie&lt;\n/a&gt; and\n</a>, <a class="sister" href="http://example.com/tillie" id="link3">Tilli\ne</a>]
"""

print(soup.find_all(text="The"))
print(soup.find_all(text="The Dormouse's story"))
"""
精确的等于才能完成匹配
[]
[u"The Dormouse's story", u"The Dormouse's story"]
"""

print(soup.find_all("a",class_="sister")) # 限定查找a标签的class属性为sister的元素数据

bs4的select的使用方法(重点)

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
python复制代码from bs4 import BeautifulSoup
import re

html = """
<html><head><title>The Dormouse's story</title></head>
<body>
<p class="title" name="dromouse"><b>The Dormouse's story</b></p>
<p class="story">Once upon a time there were three little sisters;
and their names were
<a href="http://example.com/elsie" class="sister" id="link1"><!-- E
lsie --></a>,
<a href="http://example.com/lacie" class="sister" id="link2">Lacie<
/a> and
<a href="http://example.com/tillie" class="sister" id="link3">Tilli
e</a>;
and they lived at the bottom of a well.</p>
<p class="story">...</p>
"""

soup = BeautifulSoup(html, "lxml") # "lxml解析方式"
print soup.select("title") # 按照标签的名字进行查找
print soup.select("a") # 按照标签的名字进行查找,一次定位多个

print soup.select(".sister") # 按照class进行查找
print soup.select("#link1") # 按照id进行查找

print soup.select("p #link1") # p标签内部的id进行查找
"""
[<a class="sister" href="http://example.com/elsie" id="link1"><!-- E\nlsie --></a>]
"""

print soup.select("head > title") # 后代(层级关系)

print soup.select("a[class="sister"]") # 标签为a的class=sister的元素
"""
[<a class="sister" href="http://example.com/elsie" id="link1"><!-- E\nlsie --></a>, <a class="sister" href="http://example.com/lacie" id="link2">Lacie&lt;\n/a&gt; and\n</a>, <a class="sister" href="http://example.com/tillie" id="link3">Tilli\ne</a>]
"""

print(soup.select("title")[0].get_text()) # 获取标签之间的内容
"""
The Dormouse's story
"""

本文转载自: 掘金

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

关于SpringCloud中灰度路由的使用 1 灰度路由的简

发表于 2021-11-25

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

在微服务中, 通常为了高可用, 同一个服务往往采用集群方式部署, 即同时存在几个相同的服务,而灰度的核心就 是路由, 通过我们特定的策略去调用目标服务线路

1 灰度路由的简介

灰度发布(又名金丝雀发布)是指在黑与白之间,能够平滑过渡的一种发布方式。在其上可以进行A/B testing,即让一部分用户继续用产品特性A,一部分用户开始用产品特性B,如果用户对B没有什么反对意见,那么逐步扩大范围,把所有用户都迁移到B上面来。灰度发布可以保证整体系统的稳定,在初始灰度的时候就可以发现、调整问题,以保证其影响度.

关于SpringCloud微服务+nacos的灰度发布实现, 首先微服务中之间的调用通常使用Feign方式和Resttemplate方式(较少使用),因此 , 我们需要指定服务之间的调用, 首先要给各个服务添加唯一标识, 我们可是使用一些特殊的标记, 如版本号version等, 其次,要干预微服务中Ribbon的默认轮询调用机制, 我们需要根据微服务的版本等不同, 来进行调用, 最后, 在服务之间, 需要传递调用链路的信息, 我们可以在请求头中,添加调用链路的信息.

整理思路为:

1 在请求头中添加调用链路信息

2 微服务之间调用时,使用feign拦截器,增强请求头

3 微服务调用选择时,根据指定的策略(如唯一标识版本等)从nacos中获取指定的服务,调用

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
xml复制代码   <parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.3.4.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>

<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<java.version>1.8</java.version>
<!--spring cloud 版本-->
<spring-cloud.version>Greenwich.RELEASE</spring-cloud.version>
</properties>

<dependencies>

<!--nacos-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
<version>0.2.2.RELEASE</version>
</dependency>
<dependency>
<groupId>com.alibaba.nacos</groupId>
<artifactId>nacos-client</artifactId>
<version>1.1.0</version>
</dependency>


<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.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>

<!--feign-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>


<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
<exclusions>
<exclusion>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-ribbon</artifactId>
</exclusion>
</exclusions>
</dependency>


<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-loadbalancer</artifactId>
</dependency>


</dependencies>

工具服务

feign拦截器

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复制代码@Slf4j
public class FeignInterceptor implements RequestInterceptor {

/**
* feign接口拦截, 添加上灰度路由请求头
* @param template
*/
@Override
public void apply(RequestTemplate template) {

String header = null;

try {
header = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes())
.getRequest().getHeader("gray-route");
if (null == header || header.isEmpty()) {
return;
}
} catch (Exception e) {
log.info("请求头获取失败, 错误信息为: {}", e.getMessage());
}
template.header("gray-route", header);

}
}

灰度路由属性类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
java复制代码@ConfigurationProperties(prefix = "spring.cloud.nacos.discovery.metadata.gray-route", ignoreUnknownFields = false)
@Data
@RefreshScope
public class GrayRouteProp {

/**
* 逗号
*/
public final static String COMMA_SEP = ".";
/**
* 灰度路由
*/
public final static String GRAY_ROUTE = "gray-route";
/**
* 版本
*/
public final static String VERSION = "version";
/**
* 全链路版本
*/
public final static String ALL = "all";
/**
* 用户自定义版本
*/
public final static String CUSTOM = "custom";

/**
* 版本key, 可用于Redis等中存储
*/
public final static String VERSION_KEY = GRAY_ROUTE + COMMA_SEP + VERSION;


/**
* 是否开启灰度路由
*/
private boolean enable = false;
/**
* 本服务的版本
*/
private String version;

/**
* 本服务到下一跳服务的版本路由规则
*/
private RouteProp route;

}

路由属性类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
java复制代码@Data
@ConfigurationProperties(prefix = "spring.cloud.nacos.discovery.metadata.gray-route.route", ignoreUnknownFields = false)
@RefreshScope
public class RouteProp {

/**
* 本服务直接调用的所有服务的统一版本号
*/
private String all;

/**
* 指定调用服务的版本 serviceA:v1 表示在调用时只会调用v1版本服务
*/
private Map<String,String> custom;

}

灰度路由规则类(继承ZoneAvoidanceRule类)

微服务在拦截处理后, Ribbon组件会从服务实例列表中获取一个实现进行转发, 且Ribbon默认的规则是ZoneAvoidanceRule类, 我们定义自己的规则, 只需要继承该类,重写choose方法即可.

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
java复制代码@Slf4j
public class GrayRouteRule extends ZoneAvoidanceRule {

@Autowired
protected GrayRouteProp grayRouteProperties;

/**
* 参考 {@link PredicateBasedRule#choose(Object)}
*
*/
@Override
public Server choose(Object key) {
// 根据灰度路由规则,过滤出符合规则的服务 this.getServers()
// 再根据负载均衡策略,过滤掉不可用和性能差的服务,然后在剩下的服务中进行轮询 getPredicate().chooseRoundRobinAfterFiltering()
Optional<Server> server = getPredicate()
.chooseRoundRobinAfterFiltering(this.getServers(), key);
return server.isPresent() ? server.get() : null;
}

/**
* 灰度路由过滤服务实例
*
* 如果设置了期望版本, 则过滤出所有的期望版本 ,然后再走默认的轮询 如果没有一个期望的版本实例,则不过滤,降级为原有的规则,进行所有的服务轮询。(灰度路由失效) 如果没有设置期望版本
* 则不走灰度路由,按原有轮询机制轮询所有
*/
protected List<Server> getServers() {
// 获取spring cloud默认负载均衡器
ZoneAwareLoadBalancer lb = (ZoneAwareLoadBalancer) getLoadBalancer();
// 获取本次请求生效的灰度路由规则
RouteProp routeRule = this.getGrayRoute();
// 获取本次请求期望的服务版本号
String version = getDesiredVersion(routeRule, lb.getName());
// 获取所有待选的服务
List<Server> allServers = lb.getAllServers();
if (CollectionUtils.isEmpty(allServers)) {
return new ArrayList<>();
}
// 如果没有设置要访问的版本,则不过滤,返回所有,走原有默认的轮询机制
if (StringUtils.isEmpty(version)) {
return allServers;
}

// 开始灰度规则匹配过滤
List<Server> filterServer = new ArrayList<>();
for (Server server : allServers) {
// 获取服务实例在注册中心上的元数据
Map<String, String> metadata = ((NacosServer) server).getMetadata();
// 如果注册中心上服务的版本标签和期望访问的版本一致,则灰度路由匹配成功
if (null != metadata && version.equals(metadata.get(GrayRouteProp.VERSION_KEY))) {
filterServer.add(server);
}
}
// 如果没有匹配到期望的版本实例服务,为了保证服务可用性,让灰度规则失效,走原有的轮询所有可用服务的机制
if (CollectionUtils.isEmpty(filterServer)) {
log.warn(String.format("没有找到版本version[%s]的服务[%s],灰度路由规则降级为原有的轮询机制!", version,
lb.getName()));
filterServer = allServers;
}
return filterServer;
}

/**
* 获取本次请求 期望的服务版本号
*
* @param routeRule 生效的配置规则
* @param appName 服务名
*/
protected String getDesiredVersion(RouteProp routeRule, String appName) {
// 取路由规则里指定要访问的微服务的版本号
String version = null;
if (routeRule != null) {
if (routeRule.getCustom() != null) {
// 优先取custom里指定版本
version = routeRule.getCustom().get(appName);
} else {
// custom里没有指定就找all里面设置的统一版本
version = routeRule.getAll();
}
}
return version;
}

/**
* 获取设置的灰度路由规则
*/
protected RouteProp getGrayRoute() {
// 确定路由规则(请求头优先,yml配置其次)
RouteProp routeRule;
String route_header = null;

try {
route_header = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes())
.getRequest().getHeader(GrayRouteProp.GRAY_ROUTE);
} catch (Exception e) {
log.error("灰度路由从上下文获取路由请求头异常!");
}

if (!StringUtils.isEmpty(route_header)) {//header
routeRule = JSONObject.parseObject(route_header, RouteProp.class);
} else {
// yml配置
routeRule = grayRouteProperties.getRoute();
}
return routeRule;
}

}

业务服务

一个client服务;两个consumer服务,分版本v1和v2;两个provider服务,分版本v1和v2

client服务

Controller控制器

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

@Autowired
private ConsumerFeign consumerFeign;

@GetMapping("/client")
public String list() {
String info = "我是客户端,8000 ";
log.info(info);
String result = consumerFeign.list();
return JSON.toJSONString(info + result);
}

}

Feign接口

1
2
3
4
5
6
7
8
java复制代码@FeignClient(value = "consumer-a")
public interface ConsumerFeign {

@ResponseBody
@GetMapping("/consumer")
String list();

}

Application启动器

1
2
3
4
5
6
7
java复制代码@SpringBootApplication
@EnableFeignClients({"com.cf.client.feign"})
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}

application.yml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
yml复制代码server:
port: 8000
spring:
cloud:
nacos:
discovery:
server-addr: 127.0.0.1:8848 # 配置nacos 服务端地址
namespace: public
metadata:
# gray-route是灰度路由配置的开始
gray-route:
enable: true
version: v1
application:
name: client-test # 服务名称

pom依赖

1
2
3
4
5
6
7
8
xml复制代码  <!--自定义commons工具包-->
<dependencies>
<dependency>
<groupId>com.cf</groupId>
<artifactId>commons</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
</dependencies>

consumer1服务

Controller控制器

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

@Autowired
private ProviderFeign providerFeign;

@GetMapping("/consumer")
public String list() {
String info = "我是consumerA,8081 ";
log.info(info);
String result = providerFeign.list();
return JSON.toJSONString(info + result);
}

}

Feign接口

1
2
3
4
5
6
7
8
java复制代码@FeignClient(value = "provider-a")
public interface ProviderFeign {

@ResponseBody
@GetMapping("/provider")
String list();

}

Application启动类

1
2
3
4
5
6
7
8
9
10
java复制代码@EnableDiscoveryClient
@SpringBootApplication
@EnableFeignClients({"com.cf.consumer.feign"})
public class AConsumerApplication {

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

}

application.yml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
java复制代码server:
port: 8081
spring:
cloud:
nacos:
discovery:
server-addr: 127.0.0.1:8848 # 配置nacos 服务端地址
namespace: public
metadata:
# gray-route是灰度路由配置的开始
gray-route:
enable: true
version: v1
application:
name: consumer-a # 服务名称

pom依赖

1
2
3
4
5
6
7
xml复制代码  <dependencies>
<dependency>
<groupId>com.cf</groupId>
<artifactId>commons</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
</dependencies>

consumer2服务

consumer2服务和consumer1服务一样,只是灰度路由版本不一样(同一个服务器时,其端口也不一致)

application.yml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
yml复制代码server:
port: 8082
spring:
cloud:
nacos:
discovery:
server-addr: 127.0.0.1:8848 # 配置nacos 服务端地址
namespace: public
metadata:
# gray-route是灰度路由配置的开始
gray-route:
enable: true
version: v2
application:
name: consumer-a # 服务名称

provider1服务

Controller控制器

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

@GetMapping("/provider")
public String list() {
String info = "我是 providerA,9091 ";
log.info(info);
return JSON.toJSONString(info);
}
}

Application启动类

1
2
3
4
5
6
7
8
java复制代码@EnableDiscoveryClient
@SpringBootApplication
public class AProviderApplication {

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

application.yml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
yml复制代码server:
port: 9091
spring:
cloud:
nacos:
discovery:
server-addr: 127.0.0.1:8848 # 配置nacos 服务端地址
namespace: public
metadata:
# gray-route是灰度路由配置的开始
gray-route:
enable: true
version: v1
application:
name: provider-a # 服务名称

provider2服务

provider2服务和provider1服务相比, 就是灰度路由版本不一致(同一个服务器时,其端口也不一致)

application.yml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
yml复制代码server:
port: 9091
spring:
cloud:
nacos:
discovery:
server-addr: 127.0.0.1:8848 # 配置nacos 服务端地址
namespace: public
metadata:
# gray-route是灰度路由配置的开始
gray-route:
enable: true
version: v2
application:
name: provider-a # 服务名称

验证测试

1 启动本地nacos服务

2 启动五个项目服务

此时,在nacos中,存在服务列表中存在三个, 分别是client-test服务(1个),provider-a服务(2个实例),consumer-a服务(2个实例)

3 使用postman进行测试

1 不指定请求头灰度路由

1
2
3
4
5
6
7
txt复制代码"我是客户端,8000  \"我是consumerA,8081    \\\"我是 providerA,9091  \\\"\""

"我是客户端,8000 \"我是consumerB,8082 \\\"我是 providerA,9091 \\\"\""

"我是客户端,8000 \"我是consumerA,8081 \\\"我是 providerB,9092 \\\"\""

"我是客户端,8000 \"我是consumerB,8082 \\\"我是 providerB,9092 \\\"\""

调用四次, 采用的是Ribbon中默认的轮询策略.

2 指定请求头灰度路由

请求头中设置gray-route = {"all":"v1"}

1
2
3
4
txt复制代码"我是客户端,8000  \"我是consumerA,8081    \\\"我是 providerA,9091  \\\"\""
"我是客户端,8000 \"我是consumerA,8081 \\\"我是 providerA,9091 \\\"\""
"我是客户端,8000 \"我是consumerA,8081 \\\"我是 providerA,9091 \\\"\""
"我是客户端,8000 \"我是consumerA,8081 \\\"我是 providerA,9091 \\\"\""

四次测试结果, 每个服务都是v1版本, 灰度路由生效.

请求头中设置{custom":{"consumer-a":"v1","provider-a":"v1"}}

1
2
3
4
txt复制代码"我是客户端,8000  \"我是consumerA,8081    \\\"我是 providerA,9091  \\\"\""
"我是客户端,8000 \"我是consumerA,8081 \\\"我是 providerA,9091 \\\"\""
"我是客户端,8000 \"我是consumerA,8081 \\\"我是 providerA,9091 \\\"\""
"我是客户端,8000 \"我是consumerA,8081 \\\"我是 providerA,9091 \\\"\""

四次测试结果, 每个服务都是v1版本, 灰度路由生效.

请求头中设置{custom":{"consumer-a":"v1","provider-a":"v2"}}

1
2
3
4
txt复制代码"我是客户端,8000  \"我是consumerA,8081    \\\"我是 providerB,9092     \\\"\""
"我是客户端,8000 \"我是consumerA,8081 \\\"我是 providerB,9092 \\\"\""
"我是客户端,8000 \"我是consumerA,8081 \\\"我是 providerB,9092 \\\"\""
"我是客户端,8000 \"我是consumerA,8081 \\\"我是 providerB,9092 \\\"\""

四次测试结果, consumer服务都是v1版本, provider服务都是版本2,灰度路由生效.

请求头中设置{custom":{"consumer-a":"v1"}}

1
2
3
4
txt复制代码"我是客户端,8000  \"我是consumerA,8081    \\\"我是 providerA,9091  \\\"\""
"我是客户端,8000 \"我是consumerA,8081 \\\"我是 providerB,9092 \\\"\""
"我是客户端,8000 \"我是consumerA,8081 \\\"我是 providerA,9091 \\\"\""
"我是客户端,8000 \"我是consumerA,8081 \\\"我是 providerB,9092 \\\"\""

四次测试结果, consumer服务都是v1版本, provider服务没有指定,所以采用默认轮询机制,灰度路由生效.

参考资料:

segmentfault.com/a/119000001…

www.cnblogs.com/linyb-geek/…

本文转载自: 掘金

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

常见的几种JAVA垃圾收集器

发表于 2021-11-25

这是我参与11月更文挑战的第25天,活动详情查看:2021最后一次更文挑战

  1. 垃圾收集器

垃圾收集算法就像是 Java 中的接口一样,而垃圾收集器是接口的具体实现。所以,不同的厂商,不同版本的虚拟机实现的方式都有所不同。甚至是很大的差别。

下图是常见的 HotSpot 虚拟机中的垃圾收集器。

垃圾收集器

其中,新生代有 Serial、ParNew、Parallel Scavenge,老年代包括 CMS、MSC、Parallel old,收集器之间的连线说明两者可以搭配使用。

5.1 Serial 收集器

Serial 是最基本,历史最悠久,也是最简单的一个收集器。它是一个单线程的收集器。当它开始进行垃圾回收时,必须暂停其他所有工作线程,直到收集结束。虽然在垃圾回收时会发生“Stop The World”导致用户体验不佳,但是 Serial 收集器还是有优点的,在单CPU环境下 Serial 收集器由于没有线程操作的开销,只做垃圾收集,所以效率远高于其他收集器。

5.2 ParNew 收集器

ParNew 收集器是 Serial 收集器的一个并行版本,与 Serial 的区别在于 STW 期间并行进行垃圾收集。它是大多数 Server 模式下新生代首选的收集器,一个比较重要的原因是,目前只有 ParNew 和 Serial 能与老年代的 CMS 收集器使用。也就是说如果老年代的收集器你选择了 CMS 新生代的收集器就只能从这两者中选择一个。

需要注意的是,在单CPC环境中,由于 ParNew 收集器存在与线程交互的开销,所以效率一定没有 Serial 收集器高。
甚至在两个 CPU 的情况下都不能百分之百的保证可以高于 Serial。当然,随着 CPU 的数量增多, ParNew 收集器的性能会越来越好。

5.3 Parallel Scavenge 收集器

Parallel Scavenge 收集器是新生代的收集器,采用的是复制算法,并行收集。功能和 ParNew 很类似。其他收集器的关注点都是如何尽可能的缩短 STW 的时间,而 Parallel Scavenge 收集器的目标是控制 STW 占用时间的百分比。这个百分比称为吞吐量。其实就是 用户代码的执行时间与CPU的总执行时间的比值。比如,JVM 总共运行了 100 分钟,而 GC 用了 2 分钟,那么吞吐量就是 98%( 98/(98+2) )。使用 Parallel Scavenge 收集器,你可以通过“-XX:GCTimeRatio“参数设置吞吐量的大小。

GCTimeRatio 参数的值是一个(0,100)闭区间的整数值,含义是垃圾收集时间的占比。相当于吞吐量的倒数。默认值时 99,即最大允许 1%( 1/(1+99),的时间用来做垃圾收集。

还可以通过”-XX:MaxGCPauseMillis“ 参数设置每次 GC 的周期,收集器将尽可能的保证垃圾收集的时间不超过该值。不过,不要认为将该值设置的稍微小一点就能使得 GC 的时间更快。GC 的停顿时间是通过牺牲新生代的空间来换来的。新生代空间小了,有可能导致 GC 的次数增多,总体算下来吞吐量有可能不增反减。

5.4 Serial Old 收集器

Serial Old 收集器是 Serial 收集器的老年代版本,同样是单线程的,使用的是”标记-整理算法“。它的两个主要用途是,一个用于 Client 端。另一个是在 Server 端用于 CMS 收集器的后备预案(详见 CMS 收集器)。

5.5 Parallel Old收集器

同样,Parallel Old 是 Parallel Scavenge 收集器的老年代版本,使用多线程和“标记-整理”算法”。需要注意的是,如果老年代使用 Parallel Old 那么新生代就只能使用 Parallel Scavenge 与之配合。在某些吞吐量优先的场景下可以考虑这种组合。

本文转载自: 掘金

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

Go Defer 关键字原理浅析

发表于 2021-11-25

这是我参与11月更文挑战的第22天,活动详情查看:2021最后一次更文挑战。

defer 是什么?

defer 修饰的函数是一个延迟函数,在包含它的函数返回时运行。

defer 执行时机

A “defer” statement invokes a function whose execution is deferred to the moment the surrounding function returns, either because the surrounding function executed a return statement, reached the end of its function body, or because the corresponding goroutine is panicking

defer 触发时机是:

  1. 函数执行到函数体末端
  2. 函数执行return语句
  3. 当前协程panic

defer 实现原理

defer 数据结构源码在 src/runtime/runtime2.go

在这里插入图片描述

1
2
3
4
5
6
7
8
9
10
11
go复制代码type _defer struct {
siz int32 // 由deferproc第一个参数传入,参数和结果的内存大小
started bool // 标识defer函数是否已经开始执行
heap bool // 堆分配、栈分配
openDefer bool //表示当前 defer 是否经过开放编码的优化
sp uintptr // 栈指针程序计数器,注册defer函数的函数栈指针
pc uintptr // 调用方程序计数器,deferproc函数返回后要继续执行的指令地址
fn *funcval // 由deferproc的第二个参数传入,也就是被注册的defer函数
_panic *_panic // 是触发defer函数执行的panic指针,正常流程执行defer时它就是nil
link *_defer //结构体是延迟调用链表上的一个元素,所有的结构体都会通过 link 字段串联成链表。
}

defer 执行机制

在中间代码生成阶段, 有三种不同的机制处理 defer 关键字

  • 堆上分配(Go 版本1.1-1.12)默认兜底方案
  • 栈分配(Go版本 1.13 )相比堆分配能够减少 30%堆额外开销。
  • 开放编码(Go 版本 1.14) 额外开销可以忽略不计。

堆上分配

  1. 在defer 语句堆位置插入 runtime.deferproc, 在被执行时,延迟调用会被保存为一个 _defer 记录,并将被延迟调用的入口地址与参数复制保存,存入 Gorountine 得调用链表中。
  2. 在函数返回之前的位置插入 runtime.deferreturn,当被执行时,会将延迟调用从 Goroutine 链表中取出并执行,多个延迟调用则以 jmpdefer 尾递归调用方式连续执行

runtime.deferproc 负责注册, runtime.deferreturn 负责执行。

在这里插入图片描述

derfer 关键字最重要的三个函数

  1. deferproc。在每遇到一个defer关键字时,实际上都会转换为deferproc函数,deferproc函数的作用是将defer函数存入链表中。(go关键字是使用newproc函数,它两的实现有着不少相似之处)
  2. deferreturn。在return指令前调用,从链表中取出defer函数并执行。
  3. deferprocStack。go1.13后对defer做的优化,通过利用栈空间提高效率。

call 函数

cmd/compile/internal/gc.state.call 会负责为所有函数和方法调用生成中间代码:

  1. 获取需要执行的函数名,代码制作和函数调用的接收方
  2. 需要获取栈地址并将函数或者方法的参数写入栈中
  3. 使用 cmd/compile/internal/gc.state.newValue1A 函数生成函数调用的中间代码
  4. 如果当前调用的函数是 defer, 那么会单独生成相关的结束代码块。
  5. 获取函数的返回值,并结束当前调用。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
scss复制代码// Calls the function n using the specified call type.
// Returns the address of the return value (or nil if none).
func (s *state) call(n *Node, k callKind) *ssa.Value {
var call *ssa.Value
if k == callDeferStack {
// 在栈上初始化 defer 结构体
call = s.newValue1A(ssa.OpStaticCall, types.TypeMem, deferprocStack, s.mem())
...
} else {
...
switch {
case k == callDefer:
call = s.newValue1A(ssa.OpStaticCall, types.TypeMem, deferproc, s.mem())
...
}
call.AuxInt = stksize
}
s.vars[&memVar] = call
...
}

deferproc 函数

runtime.deferproc 的功能时注册延迟函数,会为 defer 创建一个新的 runtime._defer的结构体、设置它的函数指针 fn、程序计数器 pc 和 栈制作 sp 并将相关的函数参数拷贝到相邻的内存空间。

  • deferproc 函数有两个参数, 第一个是被注册的 defer 函数的参数返回值占多少字节。第二个参数是一个 runtime.funcval 结构体的指针
  • runtime.newdefer通过 runtime.mallocgc 在堆上创建一个新的结构体,并添加到link字段上形成链表。
  • 最后调用的 runtime.return0 是唯一一个不会触发延迟调用的函数,它可以避免递归 runtime.deferreturn 的递归调用。
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
scss复制代码func deferproc(siz int32, fn *funcval) { 
// 这里的g就是gorouutine,详看golang的调度模型
// 只有用户使用的goroutine可以应用defer
if getg().m.curg != getg() {
throw("defer on system stack")
}
sp := getcallersp() // 调用deferproc之前的rsp寄存器的值
argp := uintptr(unsafe.Pointer(&fn)) + unsafe.Sizeof(fn)
callerpc := getcallerpc() // deferproc函数的返回地址,也就是执行完defer后应当跳回哪段代码上
d := newdefer(siz)
if d._panic != nil {
throw("deferproc: d.panic != nil after newdefer")
}
d.fn = fn
d.pc = callerpc
d.sp = sp
// 对defer函数参数进行处理
switch siz {
case 0:
// Do nothing.
case sys.PtrSize:
*(*uintptr)(deferArgs(d)) = *(*uintptr)(unsafe.Pointer(argp))
default:
memmove(deferArgs(d), unsafe.Pointer(argp), uintptr(siz))
}
return0()
}

先通过 newdefer 获取一个
defer 关键字的插入顺序是从后向前的,而 defer 关键字执行是从前向后的,后调用的 defer 会优先执行。

在这里插入图片描述
newdefer追加新等延迟调用

deferreturn 函数

runtime.deferreturn 是触发延迟函数链表的执行,会从 Goroutine 的 _defer 链表中取出最前面的 runtime._defer 并调用 runtime.jmpdefer 传入需要执行的函数和参数。

  • runtime.jmpdefer 是一个用汇编语言实现的运行时函数,它的主要工作是跳转到 defer 所在的代码段并在执行结束之后跳转回 runtime.deferreturn。
  • runtime.deferreturn 会多次判断当前 Goroutine 的 _defer 链表中是否有未执行的结构体,该函数只有在所有延迟函数都执行后才会返回
  • 最后调用的 runtime.return0 是唯一一个不会触发延迟调用的函数,它可以避免递归 runtime.deferreturn 的递归调用。
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
go复制代码func deferreturn(arg0 uintptr) {
gp := getg()
d := gp._defer
if d == nil {
// 结束条件1,没有defer函数了,也就是所有defer函数都执行完成了
// 还记得defer的链式结构吗,其实就是一个递归函数不断调用
// 为nil的话就代表这条链遍历完成了
return
}
sp := getcallersp()
if d.sp != sp {
// 结束条件2,如果保存在_defer对象中的sp值与调用deferretuen时的栈顶位置不一样,直接返回
// 因为sp不一样表示d代表的是在其他函数中通过defer注册的延迟调用函数,比如:
// a()->b()->c()它们都通过defer注册了延迟函数,那么当c()执行完时只能执行在c中注册的函数
return
}
switch d.siz {
case 0:
// Do nothing.
case sys.PtrSize:
*(*uintptr)(unsafe.Pointer(&arg0)) = *(*uintptr)(deferArgs(d))
default:
memmove(unsafe.Pointer(&arg0), deferArgs(d), uintptr(d.siz))
}
fn := d.fn
d.fn = nil
gp._defer = d.link
freedefer(d)
jmpdefer(fn, uintptr(unsafe.Pointer(&arg0)))
}

包含如下几个步骤:

  1. 判断执行条件
  2. 参数拷贝
  3. 释放_defer对象
  4. 执行函数

值得一提的是,defer具有即时传值的特点,defer也同样满足闭包和匿名函数的特性。

以代码为例子,讲解 defer 注册和执行流程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
less复制代码// 源程序
func A1(a int) {
fmt.Println(a)
}
func A() {
a, b := 1, 2
defer A1(a)

a = a + b
fmt.Println(a, b)
}

//函数A编译后的伪指令
func A() {
a, b := 1, 2
runtime.deferproc(8, A1,1) // siz=8, A1=延迟函数入口, 1=A1函数入参

a = a + b
fmt.Println(a, b)//3,2

runtime.deferreturn()//执行defer链表
return
}
  1. 函数A定义局部变量a=1,b=2,存储在A函数的栈中
    在这里插入图片描述
  2. deferproc函数注册defer函数A1时,
  • func deferproc(siz int32, fn *funcval)
  • siz:A1没有返回值,64位下一个整型参数占用8字节。
  • fn:A1函数入口地址,addr1

deferproc函数调用时,编译器会在它自己的两个参数后面,开辟一段空间,用于存放defer函数A1的返回值和参数。这一段空间会在注册defer时,直接拷贝到_defer结构体的后面。

在这里插入图片描述

  1. 在堆上分配存储空间,并存放_defer结构体
  • A1的参数加返回值共占8字节
  • defer函数尚未执行,所以started=false
  • sp就是调用者A的栈指针
  • pc就是deferproc函数的返回地址return addr
  • 被注册的function value为A1
  • defer结构体后面的8字节用来保存传递给A1的参数。
    然后这个_defer结构体就被添加到defer链表头,deferproc注册结束。

频繁的堆分配势必影响性能,所以Go语言会预分配不同规格的deferpool,执行时从空闲_defer中取一个出来用。没有空闲的或者没有大小合适的,再进行堆分配。用完以后,再放回空闲_defer池。这样可以避免频繁的堆分配与回收。

在这里插入图片描述

  1. deferreturn执行defer链表:从当前goroutine找到链表头上的这个_defer结构体,通过_defer.fn找到defer函数的funcval结构体,进而拿到函数A1的入口地址。接下来就可以调用A1了。
    调用A1时,会把_defer后面的参数与返回值整个拷贝到A1的调用者栈上。然后A1开始执行,输入参数值a=1。

在这里插入图片描述

欢迎关注公众号:程序员财富自由之路

参考资料

  • draveness.me/golang/docs…

本文转载自: 掘金

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

Java经典多线程问题——哲学家恰饭

发表于 2021-11-25

这是我参与11月更文挑战的第17天,活动详情查看:2021最后一次更文挑战

Java经典多线程问题——哲学家恰饭

哲学家进餐问题:
五个哲学家共用一张圆桌,分别坐在周围的五张椅子上,在桌子上有五只碗和五只筷子,他们的生活方式是交替地进行思考和恰饭。平时,一个哲学家进行思考,饥饿时便试图取用其左右最靠近他的筷子,一次只能拿左边或者右边的一根筷子,拿到一根筷子之后才能拿第二根,只有在他拿到两只筷子时才能恰饭。恰饭完毕,放下筷子继续思考。

image.png

这个问题是我们学习多线程的入门问题,接下来我们将对这个问题进行探讨。

首先,如果不对哲学家获取筷子进行限制,则有可能会出现死锁,

1. 解法一:锁住所有筷子

解法一的方法是,把所有筷子都给锁住。

当一个哲学家去拿筷子的时候,我们就把所有筷子都锁定,同时阻塞其他再来拿筷子的哲学家,只有等第一个哲学家拿到一双筷子,才允许下一个哲学家去拿筷子。这样就不会出现死锁问题,但这种解法,效率比较差。

2. 解法二:锁住相邻的2支筷子

当一个哲学家去拿他左边筷子的时候,他的右边筷子是不允许其他哲学家拿的,这个方法就是分段加锁,拿左边筷子的时候,先把左右两边的筷子先锁起来,这样就避免了只能拿到一支筷子的问题。

3. 解法三:其中一位科学家是左撇子,从左边拿筷子,其余都从右边拿

当一个哲学家先拿他左边筷子(下面我们称他为左撇子)的时候,其他的科学家有先拿右边的筷子。

1.左撇子拿到筷子,那他左边的哲学家就不能抓到自己右边的筷子,则此时这位哲学家就拿不到一支筷子,他左右的筷子都将给到其他哲学家,4个哲学家拿5个筷子,肯定有一人可以拿到2支筷子。

1.左撇子没拿到筷子,那此时他就是那个一支筷子都没有的任,4个哲学家拿5支筷子,肯定有一人可以拿到2支筷子。

但这个算法,同一时间只能有一个人能拿到两支筷子。

4. 解法四:有一半人是左撇子

我们可以根据解法三的算法稍微改进下,例如:将“有一位左撇子”改为有“有一半左撇子”,而且左撇子和左撇子相隔开来,如果用奇偶表示,就是奇数为右撇子,偶数为左撇子,此时算法效率就优于解法三。

本文转载自: 掘金

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

Spring Securty(一)初探,学会配置

发表于 2021-11-25

「这是我参与11月更文挑战的第17天,活动详情查看:2021最后一次更文挑战」 Wireshark模块介绍

什么是Spring Security

基于Spring AOP和Servlet过滤器的安全框架,在web请求级和方法调用级处理身份确认和授权。

在Spring Framework基础上,Spring Security充分利用了依赖注入(DI)和面向切面编程(AOP)功能

核心功能

1.认证 2.验证(权限)3.攻击防护

原理技术
Filter、Servlet、Spring DI、SpringAOP

常用安全框架

主要是Spring Security和Apache Shiro
都支持1.认证 2.授权 3.加密 4.会话 5.缓存支持 6.remberMe功能 ……

  • 区别

1.Spring Security基于Spring,项目若以Spring作为基础,配合Spring Security更加方便。

Apache Shiro需要和Spring进行整合。

2.Spring Security功能比Shiro更加丰富,如安全防护

3.Spring Security社区资源更加丰富

4.Spring Security可以和Spring Boot、Spring Cloud无缝集成

1.Shiro使用配置简单

2.Shiro依赖性低,不需要任何框架、容器,独立运行。Spring Security依赖Spring容器。

依赖

spring-boot-starter-parent

spring-boot-starter-web

spring-boot-starter-security

(什么是STS开发工具)

项目启动后,添加测试controller

1
2
3
4
5
6
7
8
9
less复制代码@RequestMapping("/hello")
@RestController
public class TestController {

@GetMapping
public String getWelcome() {
return "hello";
}
}

启动后访问路径
会自动跳转一下路径: http://localhost:8181/login

image.png
在Spring Boot中,默认的Spring Security生效,这时的接口都被保护了,通过验证才能正常访问。
username默认为user 密码在项目启动时打印的日志

image.png
使用账号密码登录之后就可以正常访问接口了。
这个验证可以关掉吗?

可以的

启动类中添加如下代码:

1
python复制代码 @SpringBootApplication(exclude = SecurityAutoConfiguration.class)

image.png
自定义用户名和密码:

image.png

怎么配置多个账号信息
定义一个配置类WebSecurityConfig继承WebSecurityConfigurerAdapter

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
scala复制代码import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;

// 声明是个配置类
@Configuration
// 开启Spring Security
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
/**
* 从内存中进行加载认证信息
* 配置两个用户
*/
auth.inMemoryAuthentication().withUser("user").password("123456").roles();
auth.inMemoryAuthentication().withUser("lkk").password("123456").roles();

}
}

记得启动类修改

1
2
3
4
5
6
7
8
typescript复制代码@SpringBootApplication
public class SpringSecurityApplication {

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

}

启动项目
这时候发现项目启动成功了,但是请求接口输入账号密码后,后台报错:

1
python复制代码java.lang.IllegalArgumentException: There is no PasswordEncoder mapped for the id "null"

image.png
Spring Security 5.0新增了多种加密方式,改变了密码的格式。以上报错的原因是没有指定加密方式。

加密
方式1(推荐):
在WebSecurityConfig配置类中,通过@Bean注入PasswordEncoder具体实现类

1
2
3
4
typescript复制代码    @Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}

给用户密码加密

1
2
3
4
5
6
7
8
9
10
scss复制代码@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
/**
* 从内存中进行加载认证信息
* 配置两个用户
*/
auth.inMemoryAuthentication().withUser("user").password(passwordEncoder().encode("123456")).roles();
auth.inMemoryAuthentication().withUser("lkk").password(passwordEncoder().encode("123456")).roles();

}

重启项目后即可成功登录请求接口。

方式2:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
less复制代码@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
/**
* 从内存中进行加载认证信息
* 配置两个用户
*/
auth.inMemoryAuthentication()
.withUser("user")
.password(passwordEncoder().encode("123456"))
.roles("admin");
auth.inMemoryAuthentication()
.withUser("lkk")
.password(passwordEncoder().encode("123456"))
.roles("normal");
}

Bcrypt:是一种跨平台的文件加密工具。使用的是Blowfish加密算法。由它加密的文件可在所有支持的操作系统和处理器上进行转移。它的口令必须是8至56个字符,并将在内部被转化为448位的密钥。

给指定用户指定角色

1
2
3
4
5
6
7
8
scss复制代码/**
* 添加角色
*/
auth.inMemoryAuthentication()
.passwordEncoder(new BCryptPasswordEncoder())
.withUser("user")
.password("123456")
.roles("admin");

开启方法级安全控制
@EnableGlobalMethodSecurity注解

  • prePostEnabled:决定Spring Security前注解是否可用@PreAuthrorize@ PostAuthorize…
  • secureEnabled:决定Spring Security的保障作用@Secured是否可用
  • jsr250Enables:决定 JSR-250 annotations 注解[@RolesAllowed…] 是否可用
1
2
3
4
5
6
7
8
9
less复制代码// 声明是个配置类
@Configuration
// 开启Spring Security
@EnableWebSecurity
// 开启方法级安全控制
// @PreAuthorize("hasAnyRole('admin')")才能生效
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
}

配置方法级权限控制
@PreAuthorize(“hasAnyRole(‘admin’)”)

1
2
3
4
5
less复制代码@GetMapping
@PreAuthorize("hasAnyRole('admin')")
public String getWelcome() {
return "hello";
}

本文转载自: 掘金

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

1…190191192…956

开发者博客

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