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

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


  • 首页

  • 归档

  • 搜索

Netty 框架学习 —— 基于 Netty 的 HTTP/

发表于 2021-06-27

通过 SSL/TLS 保护应用程序

SSL 和 TLS 安全协议层叠在其他协议之上,用以实现数据安全。为了支持 SSL/TLS,Java 提供了 javax.net.ssl 包,它的 SSLContext 和 SSLEngine 类使得实现解密和加密变得相当简单。Netty 通过一个名为 SsLHandler 的 ChannelHandler 实现了这个 API,其中 SSLHandler 在内部使用 SSLEngine 来完成实际工作

Netty 还提供了基于 OpenSSL 工具包的 SSLEngine 实现,比 JDK 提供的 SSLEngine 具有更好的性能。如果 OpenSSL 可用,可以将 Netty 应用程序配置为默认使用 OpenSSLEngine。如果不可用,Netty 将会退回到 JDK 实现

下述代码展示了如何使用 ChannelInitializer 来将 SslHandler 添加到 ChannelPipeline 中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
java复制代码public class SslChannelInitializer extends ChannelInitializer<Channel> {

private final SslContext context;
private final boolean startTls;

public SslChannelInitializer(SslContext context, boolean startTls) {
this.context = context;
this.startTls = startTls;
}

@Override
protected void initChannel(Channel ch) throws Exception {
SSLEngine engine = context.newEngine(ch.alloc());
ch.pipeline().addFirst("ssl", new SslHandler(engine, startTls));
}
}

大多数情况下,Sslhandler 将是 ChannelPipeline 中的第一个 ChannelHandler,这确保了只有在所有其他的 ChannelHandler 将它们的逻辑应用到数据之后,才会进行加密

SSLHandler 具有一些有用的方法,如表所示,例如,在握手阶段,两个节点将相互验证并且商定一种加密方式,你可以通过配置 SslHandler 来修改它的行为,或者在 SSL/TLS 握手一旦完成之后提供通知,握手阶段之后,所有的数据都将会被加密

方法名称 描述
setHandshakeTimeout(long, TimeUnit)setHandshakeTimeoutMillis(long)getHandshakeTimeoutMillis() 设置和获取超时时间,超时之后,握手 ChannelFuture 将会被通知失败
setCloseNotifyTimeout(long, TimeUnit)setCloseNotifyTimeoutMillis(long)getCloseNotifyTimeoutMillis() 设置和获取超时时间,超时之后,将会触发一个关闭通知并关闭连接,这也会导致通知该 ChannelFuture 失败
handshakeFuture() 返回一个在握手完成后将会得到通知的 ChannelFuture,如果握手先前已经执行过,则返回一个包含了先前握手结果的 ChannelFuture
close()close(ChannelPipeline)close(ChannelHandlerContext, ChannelPromise) 发送 close_notify 以请求关闭并销毁底层的 SslEngine

HTTP 编解码器

HTTP 是基于请求/响应模式的,客户端向服务器发送一个 HTTP 请求,然后服务器将会返回一个 HTTP 响应,Netty 提供了多种多种编码器和解码器以简化对这个协议的使用

下图分别展示了生产和消费 HTTP 请求和 HTTP 响应的方法

如图所示,一个 HTTP 请求/响应可能由多个数据部分组成,并且总以一个 LastHttpContent 部分作为结束

下表概要地介绍了处理和生成这些消息的 HTTP 解码器和编码器

名称 描述
HttpRequestEncoder 将 HTTPRequest、HttpContent 和 LastHttpContent 消息编码为字节
HttpResponseEncoder 将 HTTPResponse、HttpContent 和 LastHttpContent 消息编码为字节
HttpRequestDecoder 将字节编码为 HTTPRequest、HttpContent 和 LastHttpContent 消息
HttpResponseDecoder 将字节编码为 HTTPResponse、HttpContent 和 LastHttpContent 消息

下述代码中的 HttpPipelineInitializer 类展示了将 HTTP 支持添加到你的应用程序是多么简单 —— 只需要将正确的 ChannelHandler 添加到 ChannelPipeline 中

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复制代码public class HttpPipelineInitializer extends ChannelInitializer<Channel> {

private final boolean client;

public HttpPipelineInitializer(boolean client) {
this.client = client;
}

@Override
protected void initChannel(Channel ch) throws Exception {
ChannelPipeline pipeline = ch.pipeline();
if (client) {
// 如果是客户端,则添加 HttpResponseDecoder 处理来自服务器的响应
pipeline.addLast("decoder", new HttpResponseDecoder());
// 如果是客户端,则添加 HttpRequestEncoder 向服务器发送请求
pipeline.addLast("encoder", new HttpRequestEncoder());
} else {
// 如果是服务端,则添加 HttpRequestDecoder 处理来自客户端的请求
pipeline.addLast("decoder", new HttpRequestDecoder());
// 如果是客户端,则添加 HttpResponseEncoder 向客户端发送响应
pipeline.addLast("encoder", new HttpResponseEncoder());
}
}
}

聚合 HTTP 消息

在 ChannelInitializer 将 ChannelHandler 安装到 ChannelPipeline 中之后,你就可以处理不同类型的 HTTPObject 消息了。但由于 HTTP 请求和响应可能由许多部分组成,因此你需要聚合它们以形成完整的消息。Netty 提供了一个聚合器,它可以将多个消息部分合并为 FullHttpRequest 或者 FullHttpResponse 消息

由于消息分段需要被缓冲,直到可以转发下一个完整的消息给下一个 ChannelInboundHandler,所以这个操作有轻微的开销,其所带来的好处就是你可以不必关心消息碎片了

引入这种自动聚合机制只不过是向 ChannelPipeline 中添加另外一个 ChannelHandler 罢了,下述代码展示了如何做到这一点:

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

private final boolean isClient;

public HttpAggregatorInitializer(boolean isClient) {
this.isClient = isClient;
}

@Override
protected void initChannel(Channel ch) throws Exception {
ChannelPipeline pipeline = ch.pipeline();
if (isClient) {
// 如果是客户端,则添加 HttpClientCodec
pipeline.addLast("codec", new HttpClientCodec());
} else {
// 如果是服务器,则添加 HttpServerCodec
pipeline.addLast("codec", new HttpServerCodec());
}
// 将最大的消息大小为 512KB 的 HTTPObjectAggregator 添加到 ChannelPipeline
pipeline.addLast("aggregator", new HttpObjectAggregator(512 * 1024));
}
}

HTTP 压缩

当使用 HTTP 时,建议开启压缩功能以尽可能多地减小传输数据的大小。虽然压缩会带来一些消耗,但通常来说它都是一个好主意,尤其是对于文本数据而言

Netty 为压缩和解压都提供了 ChannelHandler 实现,它们同时支持 gzip 和 deflate 编码

客户端可以通过提供以下头部信息来指示服务器它所支持的压缩格式

GET /encrypted-area HTTP/1.1

Host: www.example.com

Accept-Encoding: gzip, deflate

然而,需要注意的是,服务器没有义务压缩它所发送的数据

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复制代码public class HttpCompressionInitializer extends ChannelInitializer<Channel> {

private final boolean isClient;

public HttpCompressionInitializer(boolean isClient) {
this.isClient = isClient;
}

@Override
protected void initChannel(Channel ch) throws Exception {
ChannelPipeline pipeline = ch.pipeline();
if (isClient) {
// 如果是客户端,则添加 HTTPClientCodec
pipeline.addLast("codec", new HttpClientCodec());
// 如果是客户端,则添加 HttpContentDecompressor 以处理来自服务器的压缩内容
pipeline.addLast("decompressor", new HttpContentDecompressor());
} else {
// 如果是服务端,则添加 HttpServerCodec
pipeline.addLast("codec", new HttpServerCodec());
// 如果是服务器,则添加 HttpContentDecompressor 来压缩数据
pipeline.addLast("decompressor", new HttpContentDecompressor());
}
}
}

HTTPS

启用 HTTPS 只需要将 SslHandler 添加到 ChannelPipeline 的 ChannelHandler 组合中

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

private final SslContext context;
private final boolean isClient;

public HttpsCodecInitializer(SslContext context, boolean isClient) {
this.context = context;
this.isClient = isClient;
}

@Override
protected void initChannel(Channel ch) throws Exception {
ChannelPipeline pipeline = ch.pipeline();
SSLEngine engine = context.newEngine(ch.alloc());
pipeline.addLast("ssl", new SslHandler(engine));
if (isClient) {
pipeline.addLast("codec", new HttpClientCodec());
} else {
pipeline.addLast("codec", new HttpServerCodec());
}
}
}

WebSocket

WebSocket 解决了一个长期存在的问题:既然底层协议(HTTP)是一个请求/响应模式的交互序列,那么如何实时地发布信息呢?AJAX一定程度上解决了这个问题,但数据流仍然是由客户端所发送的请求驱动的

WebSocket 提供了在单个 TCP 连接上提供双向的通信,它为网页和远程服务器之间的双向通信提供了一种替代 HTTP 轮询的方案

要想向你的应用程序添加对于 WebSocket 的支持,你需要将适当的客户端或者服务器 WebSocketChannelHandler 添加到 ChannelPipeline 中。这个类将处理由 WebSocket 定义的称为帧的特殊消息类型,如表所示,WebSocketFrame 可以被归类为数据帧或者控制帧

名称 描述
BinaryWebSocketFrame 数据帧:二进制数据
TextWebSocketFrame 数据帧:文本数据
ContinuationWebSocketFrame 数据帧:属于上一个 BinaryWebSocketFrame 或者 TextWebSocketFrame 的文本或者二进制的数据
CloseWebSocketFrame 控制帧:一个 CLOSE 请求,关闭的状态码以及关闭的原因
PingWebSocketFrame 控制帧:请求一个 PongWebSocketFrame
PongWebSocketFrame 控制帧:对 PingWebSocketFrame 请求的响应

因为 Netty 主要是一种服务器端技术,所以我们重点创建 WebSocket 服务器。下述代码展示了使用 WebSocketChannelHandler 的简单示例,这个类会处理协议升级握手,以及三种控制帧 —— Close、Ping 和 Pong,Text 和 Binary 数据帧将会被传递给下一个 ChannelHandler 进行处理

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
java复制代码public class WebSocketServerInitializer extends ChannelInitializer<Channel> {

@Override
protected void initChannel(Channel ch) throws Exception {
ch.pipeline().addLast(
new HttpServerCodec(),
new HttpObjectAggregator(65536),
// 如果被请求的端点是 /websocket,则处理该升级握手
new WebSocketServerProtocolHandler("/websocket"),
// TextFrameHandler 处理 TextWebSocketFrame
new TextFrameHandler(),
// BinaryFrameHandler 处理 BinaryWebSocketFrame
new BinaryFrameHandler(),
// ContinuationFrameHandler 处理 Continuation WebSocketFrame
new ContinuationFrameHandler());
}

public static final class TextFrameHandler extends SimpleChannelInboundHandler<TextWebSocketFrame> {

@Override
protected void messageReceived(ChannelHandlerContext ctx, TextWebSocketFrame msg) throws Exception {
// do something
}
}

public static final class BinaryFrameHandler extends SimpleChannelInboundHandler<BinaryWebSocketFrame> {

@Override
protected void messageReceived(ChannelHandlerContext ctx, BinaryWebSocketFrame msg) throws Exception {
// do something
}
}

public static final class ContinuationFrameHandler extends SimpleChannelInboundHandler<ContinuationWebSocketFrame> {

@Override
protected void messageReceived(ChannelHandlerContext ctx, ContinuationWebSocketFrame msg) throws Exception {
// do something
}
}
}

本文转载自: 掘金

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

DDD领域驱动(二) - 项目分层设计 1 Domain

发表于 2021-06-27

最近看到了阿里团队分享的几篇关于DDD的文章,对自己启发挺大的,这里做一个总结、记录、学习分享。内容比较多,具体可以看原文,这里对文章做一下提炼总结,方便大家有个全局的认识。

参考文章:

阿里技术专家详解 DDD 系列- Domain Primitive

殷浩详解DDD:如何避免写流水账代码?

往期文章:

DDD领域驱动 - 设计聚合

  1. Domain Primitive

1.1. 什么是Primitive

Primitive的定义: 原始的

这里先不解释Domain Primitive,先做个类比,Java Primitive,像String、Integer、Long等,这些可以称为Java编程语言的Primitive,它们是Java的基础。

但这些类型是在编程语言层面,对于领域来说,关联性就很小,所以就有了Domain Primitive的定义,那么什么是Domain的基础呢,Domain是用来处理复杂业务的,是业务相关的,所以它的Primitive应该是有业务属性的,显然String、Integer没有业务属性。

所以Domain Primitive其实进一步的封装,举个栗子,比如要注册一个用户:

1
2
3
4
5
6
7
8
9
10
11
12
java复制代码public class User {
Long userId;
String name;
String phone;
String address;
Long repId;
}

public interface RegistrationService {
User register(String name, String phone, String address);
}
// 这种方式调用放register("13312331233", "张三", "beijing"),代码这样写出来,编译也是可以通过的。

这样的入参形式三个String类型是和业务无关,在调用过程中如果字段传入顺序错误,编码过程中是很难发现的,可能只有在代码发布,甚至上线后才被发现。

再看一下另一种方式:

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
java复制代码public class User {
UserId userId;
Name name;
PhoneNumber phone;
Address address;
RepId repId;
}

public class Name {
private finale String name;

public Name(String name) {
if(StringUtils.isBlank(name)) {
throw new ValidationException("name不能为空");
}
}

public String getName() {
return name;
}
}

public class PhoneNumber {

private final String number;
public String getNumber() {
return number;
}

public PhoneNumber(String number) {
if (number == null) {
throw new ValidationException("number不能为空");
} else if (isValid(number)) {
throw new ValidationException("number格式错误");
}
this.number = number;
}
}

public interface RegistrationService {
User register(Name name, PhoneNumber phone, Address address);
}

分析下这样的方式:

  1. 将String字段,封装为一个具体的对象,在构造方法中加入校验逻辑,所以只要这个对象创建成功,则说明其必然是合法的;
  2. 入参每个值都有对应的对象,所以不存在传错参数的问题,如果传错,直接在编译器就能发现。

这种形式,就是Domain Primitive。

1.2. Domain Primitive总结

Domain Primitive定义:

  • DP是一个传统意义上的Value Object,拥有Immutable的特性
  • DP是一个完整的概念整体,拥有精准定义
  • DP使用业务域中的原生语言
  • DP可以是业务域的最小组成部分、也可以构建复杂组合

使用Domain Primitive三原则:

  • 让隐性的概念显性化
  • 让隐性的上下文显性化
  • 封装多对象行为
  1. DDD代码分层

这方面可以参考下阿里工程师开源的COLA4.0脚手架 -> alibaba/COLA: 🥤 COLA: Clean Object-oriented & Layered Architecture (github.com)

2.1. Interface层

接口层作为对外的门户,将网络协议与业务逻辑解耦。可以包含鉴权、Session管理、限流、异常处理、日志等功能,当然如果有一个统一的网关服务的话,可以抽离出鉴权、Session、限流、日志等逻辑。

返回值

接口层返回值统一封装Response对象,比如在COLA架构中,返回值分为四个:Response / SingleResponse / PageResponse / MultiResponse

具体细节没有展示,每个公司可能都有封装这样的对象,实现细节大同小异

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
java复制代码public class Response extends DTO {
private static final long serialVersionUID = 1L;
private boolean success;
private String errCode;
private String errMessage;
}

public class SingleResponse<T> extends Response {
private T data;
}

public class PageResponse<T> extends Response {
private static final long serialVersionUID = 1L;
private int totalCount = 0;
private int pageSize = 1;
private int pageIndex = 1;
private Collection<T> data;
}

public class MultiResponse<T> extends Response {
private static final long serialVersionUID = 1L;
private Collection<T> data;
}

Interface层的接口数量与业务间的隔离

一个Interface层的类应该是“小而美”的,应该是面向“一个单一的业务”或“一类同样需求的业务”,需要尽量避免用同一个类承接不同类型业务的需求。

2.2. Application层

Application层的几个核心类:

  • Service应用服务:负责业务流程编排但本身不负责任何业务逻辑
  • DTO Assembler:负责将内部领域模型转化为可对外的DTO
  • Command、Query、Event对象:作为ApplicationService的入参
  • 返回DTO:作为Service的出参

Command、Query、Event对象

  • Command: 对系统进行操作的指令,通常为写操作,涉及到“增、删、改”。通常指需要一个明确的返回值。
  • Query: 指调用方查询操作,包含查询参数、过滤、分页等条件,属于只读操作。
  • Event: 指一件已经发生过的既有事实,需要系统根据这个事实作出改变或者响应的,通常事件处理都会有一定的写操作。事件处理器一般不会有返回值,为异步操作。

CQE规范: ApplicationService的接口入参只能是一个Command、Query、Event对象,需要能代表当前方法的语义。唯一可以例外的是单一ID查询,可以省略一个Query对象。

CQE vs DTO

表面上看,两种对象都是简单的POJO对象,但其实是有很大区别的:

  • CQE: **是ApplicationService的输入,有明确的“意图”,对象的内部需要保证其正确性**。
    • 每一个CQE都是有明确“意图”的,所以要尽量避免CQE的复用,哪怕所有参数都一样,只要语义不同,就不应该复用。
  • DTO: 只是数据容器,只是为了和外部交互,所以本身不包含任何逻辑,只是贫血对象。

因为CQE是有“意图”的,所以,理论上CQE的数量是无限的。但DTO作为数据容器,是和模型对应的,所以是有限的。

ApplicationService

当一个领域中流程较多,每一个流程对应一个或多个方法,将这些方法都收敛到一个service类中,好处是有一个完整的业务流程,流程清晰。但缺点是,这样会导致service中代码量过大。

可以通过CommandHandler、EventHandler来降低代码量,同时,不要在ApplicationService中定义private方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
java复制代码@Component
public class CheckoutCommandHandler implements CommandHandler<CheckoutCommand, OrderDTO> {
@Override
public OrderDTO handle(CheckoutCommand cmd) {
// ....
}
}
// ApplicationService
public class CheckoutServiceImpl implements CheckoutService {
@Resource
private CheckoutCommandHandler checkoutCommandHandler;

@Override
public OrderDTO checkout(@Valid CheckoutCommand cmd) {
return checkoutCommandHandler.handle(cmd);
}
}

ApplicationService是业务流程的封装,不处理业务逻辑。

判断一段代码是否是业务流程的几个点:

  • 不要有if/else分支逻辑
  • 不要有计算逻辑
  • 一些数据转化可以交给其他对象来做DTO Assembler(建议使用MapStruct类库实现)

常用的ApplicationService套路:

  • 数据准备:包括从外部服务或持久化源取出相应的Entity、VO以及外部服务返回的DTO
  • 执行操作:包括新对象的创建、赋值,以及调用领域对象的方法对其进行操作,通常是纯内存操作,非持久化。
  • 持久化

防腐层

微服务场景下,Application经常会引用外部服务,外部服务可能提供的是http接口、RPC接口、FeignApi等。

无防腐层的情况:

image-20210627135335100

有防腐层的情况:

image-20210627135439283

ACL的加入,通过转换为内部对象,通过FacadeInterface接口类,屏蔽了外部服务的类、方法和外部对象。如果未来外部服务有变化,只需要修改Facade实现类和数据转化逻辑,而不需要修改ApplicationService逻辑。

加入防腐层的优点在于,将外部服务进行了解耦,屏蔽了外部服务的变化。但这也收有代价的,它使得对象转换,外部服务封装代码增多,增加了代码量,和维护成本。但从长远角度来看,这样的代价其收益远高于弊端。

2.3. Domain层

封装核心业务逻辑,并通过领域服务(Domain Service)和(Domain Entity)的方法对Application层提供业务实体和业务逻辑计算。领域层是应用的核心,只关注业务,不关注技术实现细节,所以它不依赖任何其它层次。

2.4. Infrastructure层

主要负责技术细节处理,比如数据库CRUD、缓存、消息服务、搜索引擎、RPC等。

2.5. 异常处理

Interface层处理所有异常,返回统一的Response对象,捕获所有异常。

Application层不负责处理异常,可以随意抛出异常,返回DTO。

本文转载自: 掘金

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

FeignClient注解及参数

发表于 2021-06-27

www.cnblogs.com/smiler/p/10…
一、FeignClient注解

FeignClient注解被@Target(ElementType.TYPE)修饰,表示FeignClient注解的作用目标在接口上

1
2
3
4
5
java复制代码@FeignClient(name = "github-client", url = "https://api.github.com", configuration = GitHubExampleConfig.class)
public interface GitHubClient {
@RequestMapping(value = "/search/repositories", method = RequestMethod.GET)
String searchRepo(@RequestParam("q") String queryStr);
}

 声明接口之后,在代码中通过@Resource注入之后即可使用。@FeignClient标签的常用属性如下:

  • name:指定FeignClient的名称,如果项目使用了Ribbon,name属性会作为微服务的名称,用于服务发现
  • url: url一般用于调试,可以手动指定@FeignClient调用的地址
  • decode404:当发生http 404错误时,如果该字段位true,会调用decoder进行解码,否则抛出FeignException
  • configuration: Feign配置类,可以自定义Feign的Encoder、Decoder、LogLevel、Contract
  • fallback: 定义容错的处理类,当调用远程接口失败或超时时,会调用对应接口的容错逻辑,fallback指定的类必须实现@FeignClient标记的接口
  • fallbackFactory: 工厂类,用于生成fallback类示例,通过这个属性我们可以实现每个接口通用的容错逻辑,减少重复的代码
  • path: 定义当前FeignClient的统一前缀
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
java复制代码@FeignClient(name = "github-client",
url = "https://api.github.com",
configuration = GitHubExampleConfig.class,
fallback = GitHubClient.DefaultFallback.class)
public interface GitHubClient {
@RequestMapping(value = "/search/repositories", method = RequestMethod.GET)
String searchRepo(@RequestParam("q") String queryStr);

/**
* 容错处理类,当调用失败时,简单返回空字符串
*/
@Component
public class DefaultFallback implements GitHubClient {
@Override
public String searchRepo(@RequestParam("q") String queryStr) {
return "";
}
}
}

 在使用fallback属性时,需要使用@Component注解,保证fallback类被Spring容器扫描到,GitHubExampleConfig内容如下:

1
2
3
4
5
6
7
java复制代码@Configuration
public class GitHubExampleConfig {
@Bean
Logger.Level feignLoggerLevel() {
return Logger.Level.FULL;
}
}

  在使用FeignClient时,Spring会按name创建不同的ApplicationContext,通过不同的Context来隔离FeignClient的配置信息,在使用配置类时,不能把配置类放到Spring App Component scan的路径下,否则,配置类会对所有FeignClient生效.

二、Feign Client 和@RequestMapping

当前工程中有和Feign Client中一样的Endpoint时,Feign Client的类上不能用@RequestMapping注解否则,当前工程该endpoint http请求且使用accpet时会报404
Controller:

1
2
3
4
5
6
7
8
9
10
11
12
13
java复制代码@RestController
@RequestMapping("/v1/card")
public class IndexApi {

@PostMapping("balance")
@ResponseBody
public Info index() {
Info.Builder builder = new Info.Builder();
builder.withDetail("x", 2);
builder.withDetail("y", 2);
return builder.build();
}
}

Feign Client

1
2
3
4
5
6
7
8
9
10
11
12
13
java复制代码@FeignClient(
name = "card",
url = "http://localhost:7913",
fallback = CardFeignClientFallback.class,
configuration = FeignClientConfiguration.class
)
@RequestMapping(value = "/v1/card")
public interface CardFeignClient {

@RequestMapping(value = "/balance", method = RequestMethod.POST, produces = MediaType.APPLICATION_JSON_VALUE)
Info info();

}

if @RequestMapping is used on class, when invoke http /v1/card/balance, like this :

如果 @RequestMapping注解被用在FeignClient类上,当像如下代码请求/v1/card/balance时,注意有Accept header:

1
2
3
4
java复制代码Content-Type:application/json
Accept:application/json

POST http://localhost:7913/v1/card/balance

那么会返回 404。

如果不包含Accept header时请求,则是OK:

1
2
java复制代码Content-Type:application/json
POST http://localhost:7913/v1/card/balance

或者像下面不在Feign Client上使用@RequestMapping注解,请求也是ok,无论是否包含Accept:

1
2
3
4
5
6
7
8
9
10
11
12
13
java复制代码@FeignClient(
name = "card",
url = "http://localhost:7913",
fallback = CardFeignClientFallback.class,
configuration = FeignClientConfiguration.class
)

public interface CardFeignClient {

@RequestMapping(value = "/v1/card/balance", method = RequestMethod.POST, produces = MediaType.APPLICATION_JSON_VALUE)
Info info();

}

三、Feign请求超时问题

Hystrix默认的超时时间是1秒,如果超过这个时间尚未响应,将会进入fallback代码。而首次请求往往会比较慢(因为Spring的懒加载机制,要实例化一些类),这个响应时间可能就大于1秒了
解决方案有三种,以feign为例。

  • 方法一
    hystrix.command.default.execution.isolation.thread.timeoutInMilliseconds: 5000
    该配置是让Hystrix的超时时间改为5秒
  • 方法二
    hystrix.command.default.execution.timeout.enabled: false
    该配置,用于禁用Hystrix的超时时间
  • 方法三
    feign.hystrix.enabled: false
    该配置,用于索性禁用feign的hystrix。该做法除非一些特殊场景,不推荐使用。
    参见:www.itmuch.com/spring-clou…

本文转载自: 掘金

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

Java 反射调用的实现 反射 API root & met

发表于 2021-06-27
本文将以 `method.invoke` 为入口,学习 JDK 中反射实现调用的两种方式,并分析切换条件和切换方式。

反射 API

大家都是老司机了,关于反射 API 的使用不再过多介绍。

1
2
3
4
5
java复制代码Class<?> clazz = Class.forName("git.frank.load.User");
Object o = clazz.newInstance();

Method foo = clazz.getMethod("foo", String.class);
foo.invoke(o,"bar");

root & methodAccessor 复用

开始之前,我们先来做一个实验。

1
2
3
4
5
java复制代码Method foo1 = clazz.getMethod("foo", String.class);
Method foo2 = clazz.getMethod("foo", String.class);

System.out.println(foo1 == foo2);
System.out.println(foo1.equals(foo2));

输出分别是 false true。

可以看出,相同参数多次调用 getMethod 返回的并不是同一个对象。

获取 Method 实例

先看一下 getMethod 方法的调用流程。

getMethod.png

看到 copyMethod ,就知道为什么多次返回的 method 对象不是同一个了吧~。

method 对象层级

java.lang.reflect.Method 中有一个很重要的成员属性:root。

1
2
3
4
5
6
7
java复制代码// For sharing of MethodAccessors. This branching structure is
// currently only two levels deep (i.e., one root Method and
// potentially many Method objects pointing to it.)
//
// If this branching structure would ever contain cycles, deadlocks can
// occur in annotation code.
private Method root;

该 root 对象就是为了共享 MethodAccessors 对象而设计的。

并且,每个 java 方法只会有一个 method 对象做为 root ,在每次通过 getMethod 获取 method 对象时,都会把 root 复制一份返回给用户。同时,会把复制出的对象与 root 建立关联。

通过这种层级委派的方式,不仅保护了缓存的 method 对象不会被外部随意更改,又可以有效使用已生成的 MethodAccessors。

copyMethod 逻辑如下:

  • 根据现有属性创建出新的 method 对象。
  • 设置 root 指针
  • 设置 methodAccessor 指针
1
2
3
4
5
6
7
java复制代码Method res = new Method(clazz, name, parameterTypes, returnType,
exceptionTypes, modifiers, slot, signature,
annotations, parameterAnnotations, annotationDefault);
res.root = this;
// Might as well eagerly propagate this if already present
res.methodAccessor = methodAccessor;
return res;

获取 methodAccessor

在获取 methodAccessor 时,统一使用 acquireMethodAccessor 方法。

在该方法内,封装了到 root 中获取的逻辑,完成基于层级结构对 methodAccessor 的复用。

1
2
3
4
5
6
7
8
9
10
11
java复制代码    MethodAccessor tmp = null;
if (root != null) tmp = root.getMethodAccessor();
if (tmp != null) {
methodAccessor = tmp;
} else {
// Otherwise fabricate one and propagate it up to the root
tmp = reflectionFactory.newMethodAccessor(this);
setMethodAccessor(tmp);
}

return tmp;

值得注意的是,该方法并没有被 synchronization 修饰,在注释中也有说明:

1
2
3
4
java复制代码// NOTE that there is no synchronization used here. It is correct
// (though not efficient) to generate more than one MethodAccessor
// for a given Method. However, avoiding synchronization will
// probably make the implementation more scalable.

可能会由于并发问题造成同一个 Method 被创建出多个 MethodAccessor。

image.png

invoke 委派实现

1
2
3
4
5
6
7
8
9
10
11
12
13
java复制代码java.lang.reflect.Method#invoke

public Object invoke(Object obj, Object... args)
throws IllegalAccessException, IllegalArgumentException,
InvocationTargetException
{
...
MethodAccessor ma = methodAccessor; // read volatile
if (ma == null) {
ma = acquireMethodAccessor();
}
return ma.invoke(obj, args);
}

可以看出,反射中的 invoke 逻辑主要是交给 MethodAccessor 去做的。

而获取 MethodAccessor 的方法,acquireMethodAccessor 就是我们上面看过的,会统一取 root 中关联的 MethodAccessor 对象。

创建 MethodAccessor

可以看到,MethodAccessor 是一个接口,具体使用的实现类还是要看 ReflectionFactory#newMethodAccessor。

主要的判断逻辑是这个 if。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
java复制代码if (noInflation && !ReflectUtil.isVMAnonymousClass(method.getDeclaringClass())) {
return new MethodAccessorGenerator().
generateMethod(method.getDeclaringClass(),
method.getName(),
method.getParameterTypes(),
method.getReturnType(),
method.getExceptionTypes(),
method.getModifiers());
} else {
NativeMethodAccessorImpl acc =
new NativeMethodAccessorImpl(method);
DelegatingMethodAccessorImpl res =
new DelegatingMethodAccessorImpl(acc);
acc.setParent(res);
return res;
}

这里,会有两个关联的配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
java复制代码    // "Inflation" mechanism. Loading bytecodes to implement
// Method.invoke() and Constructor.newInstance() currently costs
// 3-4x more than an invocation via native code for the first
// invocation (though subsequent invocations have been benchmarked
// to be over 20x faster). Unfortunately this cost increases
// startup time for certain applications that use reflection
// intensively (but only once per class) to bootstrap themselves.
// To avoid this penalty we reuse the existing JVM entry points
// for the first few invocations of Methods and Constructors and
// then switch to the bytecode-based implementations.

private static boolean noInflation = false;
private static int inflationThreshold = 15;

如注释所说,MethodAccessor 有两种实现,一种为使用 bytecodes 实现的 java 版本,另外一种为 native 版本。

java 版本因为在第一次使用时需要生成代码,首次调用会比 native 慢 3 - 4 倍,但后续的调用会比 native 快 20+ 倍。

不幸的是,这些会大大影响应用的启动耗时。为了避免这种影响,JVM 在前几次反射调用使用 native 版本,后续会切换为基于 bytecode 的反射实现。

反射实现切换

为了完成切换而设计的中间层 :DelegatingMethodAccessorImpl。

1
2
3
4
5
6
7
8
9
java复制代码    DelegatingMethodAccessorImpl(MethodAccessorImpl delegate) {
setDelegate(delegate);
}

public Object invoke(Object obj, Object[] args)
throws IllegalArgumentException, InvocationTargetException
{
return delegate.invoke(obj, args);
}

image.png

可以看到到,在初始阶段,DelegatingMethodAccessorImpl 和 NativeMethodAccessorImpl 互相持有对方的指针。

在完成切换后,会将 DelegatingMethodAccessorImpl 切换至生成的 GeneratedMethodAccessor。

动态生成类实现

在默认配置下,在第 16 次反射调用时,JDK 会生成一个新的 methodAccessor 实现并加载。
我们先通过添加 -verbose:class,并循环调用反射看一下类加载情况。

可以看到,在第 16 次反射调用前,JVM 加载了这个类:

1
java复制代码[Loaded sun.reflect.GeneratedMethodAccessor1 from __JVM_DefineClass__]

而这个类是 JDK 使用 MethodAccessorGenerator 动态生成出来的。

由于这里采用的是拼接字节码的形式,几乎没有什么可读性,我们就看一下他生成的结果就好了:

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复制代码public class GeneratedMethodAccessor1 extends MethodAccessorImpl {
public GeneratedMethodAccessor1() {
}

public Object invoke(Object var1, Object[] var2) throws InvocationTargetException {
if (var1 == null) {
throw new NullPointerException();
} else {
User var10000;
String var10001;
try {
var10000 = (User)var1;
if (var2.length != 1) {
throw new IllegalArgumentException();
}

var10001 = (String)var2[0];
} catch (NullPointerException | ClassCastException var4) {
throw new IllegalArgumentException(var4.toString());
}

try {
return var10000.foo(var10001);
} catch (Throwable var3) {
throw new InvocationTargetException(var3);
}
}
}
}

可以看到,在触发 GeneratedMethodAccessor 之后的反射调用就是正常的对目标方法进行 invokevirtual 调用。

native 调用实现

对应于 NativeMethodAccessorImpl。

其相关的方法为 native 关键字修饰的:

1
java复制代码    private static native Object invoke0(Method m, Object obj, Object[] args);

接下来跟进到 openJDK 源码中,看一下在本地代码中是如何调用 java 方法的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
c++复制代码oop Reflection::invoke_method(oop method_mirror, Handle receiver, objArrayHandle args, TRAPS) {
oop mirror = java_lang_reflect_Method::clazz(method_mirror);
int slot = java_lang_reflect_Method::slot(method_mirror);
bool override = java_lang_reflect_Method::override(method_mirror) != 0;
objArrayHandle ptypes(THREAD, objArrayOop(java_lang_reflect_Method::parameter_types(method_mirror)));

oop return_type_mirror = java_lang_reflect_Method::return_type(method_mirror);
BasicType rtype;
if (java_lang_Class::is_primitive(return_type_mirror)) {
rtype = basic_type_mirror_to_basic_type(return_type_mirror, CHECK_NULL);
} else {
rtype = T_OBJECT;
}

InstanceKlass* klass = InstanceKlass::cast(java_lang_Class::as_Klass(mirror));
Method* m = klass->method_with_idnum(slot);
if (m == NULL) {
THROW_MSG_0(vmSymbols::java_lang_InternalError(), "invoke");
}
methodHandle method(THREAD, m);

return invoke(klass, method, receiver, override, ptypes, rtype, args, true, THREAD);
}

关键在于 invoke 方法中,里面做了大量的数据校验和准备的工作,这里也不再详细看了。

直接快进到 JavaCalls::call_helper 中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
C++复制代码  // do call
{ JavaCallWrapper link(method, receiver, result, CHECK);
{ HandleMark hm(thread); // HandleMark used by HandleMarkCleaner

StubRoutines::call_stub()(
(address)&link,
// (intptr_t*)&(result->_value), // see NOTE above (compiler problem)
result_val_address, // see NOTE above (compiler problem)
result_type,
method(),
entry_point,
args->parameters(),
args->size_of_parameters(),
CHECK
);

result = link.result(); // circumvent MS C++ 5.0 compiler bug (result is clobbered across call)
// Preserve oop return value across possible gc points
if (oop_result_flag) {
thread->set_vm_result((oop) result->get_jobject());
}
}
} // Exit JavaCallWrapper (can block - potential return oop must be preserved)

跟进方法命名也可以猜到,在 C++ 中调用的 java 方法实际上执行的并不是真正的 java 代码,而是走到了一个桩方法中。

什么是桩方法 (stub)

桩代码就像 RPC 调用中在服务消费端生成的 agent 代码,他帮你做远程通讯,封装参数等工作,使用户认为就像使用本地方法一样。

详细可以参考 R大的回答:什么是桩代码(Stub)?

接着再看 call_stub,这里真正调用的是 _call_stub_entry。

1
C++复制代码  static CallStub call_stub()           { return CAST_TO_FN_PTR(CallStub, _call_stub_entry); }

而 _call_stub_entry 是一个函数指针:

1
2
C++复制代码    StubRoutines::_call_stub_entry =
generate_call_stub(StubRoutines::_call_stub_return_address);

最后终于来到了 generate_call_stub 方法中,在这里准备好执行指定代码所需的运行数据,并跳转执行。

建立调用栈帧

  • 首先,在调用前需要先对寄存器状态进行保存:
1
2
3
C++复制代码    const Address saved_rbx     (rbp, -3 * wordSize);
const Address saved_rsi (rbp, -2 * wordSize);
const Address saved_rdi (rbp, -1 * wordSize);
  • 然后,对调用目标方法需要的参数进行压栈:
1
2
3
4
5
6
7
C++复制代码    // stub code
__ enter();
__ movptr(rcx, parameter_size); // parameter counter
__ shlptr(rcx, Interpreter::logStackElementSize); // convert parameter count to bytes
__ addptr(rcx, locals_count_in_bytes); // reserve space for register saves
__ subptr(rsp, rcx);
__ andptr(rsp, -(StackAlignmentInBytes)); // Align stack
  • 另外,由于 java 中方法调用参数是逆序传递的,需要再将栈中参数顺序翻转:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
C++复制代码Label loop;
// Copy Java parameters in reverse order (receiver last)
// Note that the argument order is inverted in the process
// source is rdx[rcx: N-1..0]
// dest is rsp[rbx: 0..N-1]

__ movptr(rdx, parameters); // parameter pointer
__ xorptr(rbx, rbx);

__ BIND(loop);

// get parameter
__ movptr(rax, Address(rdx, rcx, Interpreter::stackElementScale(), -wordSize));
__ movptr(Address(rsp, rbx, Interpreter::stackElementScale(),
Interpreter::expr_offset_in_bytes(0)), rax); // store parameter
__ increment(rbx);
__ decrement(rcx);
__ jcc(Assembler::notZero, loop);

当其建立完栈帧后,栈帧应该是注释里面写的这个样子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
C++复制代码  //------------------------------------------------------------------------------------------------------------------------
// Call stubs are used to call Java from C
//
// [ return_from_Java ] <--- rsp
// [ argument word n ]
// ...
// -N [ argument word 1 ]
// -7 [ Possible padding for stack alignment ]
// -6 [ Possible padding for stack alignment ]
// -5 [ Possible padding for stack alignment ]
// -4 [ mxcsr save ] <--- rsp_after_call
// -3 [ saved rbx, ]
// -2 [ saved rsi ]
// -1 [ saved rdi ]
// 0 [ saved rbp, ] <--- rbp,
// 1 [ return address ]
// 2 [ ptr. to call wrapper ]
// 3 [ result ]
// 4 [ result_type ]
// 5 [ method ]
// 6 [ entry_point ]
// 7 [ parameters ]
// 8 [ parameter_size ]
// 9 [ thread ]

跳转

  • 最后,使用保存的待调用的方法入口: entry_point ,使用 call 完成跳转,执行函数调用。
1
2
3
4
5
6
C++复制代码    __ BIND(parameters_done);
__ movptr(rbx, method); // get Method*
__ movptr(rax, entry_point); // get entry_point
__ mov(rsi, rsp); // set sender sp
BLOCK_COMMENT("call Java function");
__ call(rax);

获取返回值

  • 当方法调用完成后,保存返回值类型和结果。
1
2
3
4
5
6
7
8
9
10
11
C++复制代码    // store result depending on type
// (everything that is not T_LONG, T_FLOAT or T_DOUBLE is treated as T_INT)
__ movptr(rdi, result);
Label is_long, is_float, is_double, exit;
__ movl(rsi, result_type);
__ cmpl(rsi, T_LONG);
__ jcc(Assembler::equal, is_long);
__ cmpl(rsi, T_FLOAT);
__ jcc(Assembler::equal, is_float);
__ cmpl(rsi, T_DOUBLE);
__ jcc(Assembler::equal, is_double);

恢复栈帧

  • 清除之前压栈放入的参数。
1
2
C++复制代码    // pop parameters
__ lea(rsp, rsp_after_call);
  • 恢复寄存器:
1
2
3
4
5
C++复制代码    // restore rdi, rsi and rbx,
__ movptr(rbx, saved_rbx);
__ movptr(rsi, saved_rsi);
__ movptr(rdi, saved_rdi);
__ addptr(rsp, 4*wordSize);
  • 添加返回语句。
1
2
3
C++复制代码    // return
__ pop(rbp);
__ ret(0);

至此,就完成了一次方法调用。

总结

  • 多次获取相同方法的 method 对象得到的并不是同一个对象实例,但是他们都有共同的根对象。
  • java 中反射调用会通过 method 自身维护的一个二层树型结构统一委派给同一个 methodAccessor 实现。
  • 在默认配置下,前 15 次反射调用会使用 native 的方式实现,在第 16 次反射调用时,会采用拼接字节码的形式动态生成调用点,将后续的反射调用优化为 invokevirtual。
  • 由于动态生成字节码比较耗时,所以并没有一开始就直接触发,可以通过 sun.reflect.noInflation 和 sun.reflect.inflationThreshold 来控制关闭或调整触发阈值。
  • native 反射调用实现中,是由 C++ 代码去操作运行时栈帧,准备模板方法的数据环境,并使用 call entry_point 完成调用目标方法。

参考资料

关于反射调用方法的一个log

极客时间 <深入拆解Java虚拟机>

本文转载自: 掘金

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

测试平台系列(6) 配置Flask-SQLAlchemy

发表于 2021-06-27

配置Flask-SQLAlchemy

说到Flask-SQLAlchemy,有些人可能不太清楚是什么东西。简单的说,他就是一个orm库,帮助咱们能够更好地跟db打交道的。笔者有一篇文章有略微的介绍,可以速览一遍了解一下(其实主要是笔者才疏学浅,讲不出什么深奥的。

既然是登录,那么肯定需要有持久化的数据。那么我们肯定需要建立用户相关的表。

前置准备

  • 安装MySQL

根据自己的系统(Windows/Mac/Linux)安装好对应的MySQL并设置好账号密码,这里就不教育大家怎么安装了。端口号用默认的3306即可,并且确保服务要启动成功哦!

  • 安装mysql-connector-python(mysql官方驱动)

cmd窗口执行

1
复制代码pip3 install mysql-connector-python

配置MySQL连接信息

  • pity/config.py配置mysql连接信息

图片

MySQL配置

注意: 一定要确保MySQL的库存在哦, 没有的话可以用Navicat或Datagrip新建。

可以看出以上包含了5个重点内容,mysql的地址,端口号,用户名,密码和库名。接下来的SQLALCHEMY_DATABASE_URI代表了sql的连接信息,flask_sqlalchemy会自动根据这个变量去获取db连接等。其中mysql+mysqlconnector,代表的是试用mysql连接的方式,大家都知道Python连接mysql有很多库比如pymysql,mysqldb等。这里就是一个说明。

至于后面的警告大家可加可不加,如果被警告得太烦了可以这样解决。

1
2
css复制代码SQLALCHEMY_DATABASE_URI = 'mysql+mysqlconnector://{}:{}@{}:{}/{}'.format(
                                    MYSQL_USER, MYSQL_PWD, MYSQL_HOST, MYSQL_PORT, DBNAME)
  • 在pity/app/models/__init__.py初始化db

代码很简单,就是将app赋给SQLAlchemy从而生成一个db对象。

1
2
3
4
5
javascript复制代码from flask_sqlalchemy import SQLAlchemy

from app import pity

db = SQLAlchemy(pity)

配备用户类

  • 在models目录建立用户表

首先咱们这个平台肯定不是那种不需要登录的,因为会做一小部分的权限控制,但是肯定也不会很复杂化。所以我们可以先简略设计一下用户表。

既然是Orm,那么咱们的User表的体现即是Python中的一个类。

因为目前,还不太了解用户表的具体需要字段。但是我们能大概设计一下这个表,首先需要的字段肯定有用户名,密码,用户id,邮箱,团队id,职位等。其他的信息比如enable(是否可用),create_time(创建时间)等信息暂时先不考虑了。后续可以随意添加。

那么User表我的大概设计如下:

大致讲一下吧,大概就创建了以上几个字段。unique是字段是否唯一(可重复),primary key自然就是主键,db.String对应varchar,db.INT对应int,也就是说。现在我们要操作数据表,只需要对这个User类操作就行了,因为sqlalchemy会自动映射到对应库–对应表进行操作。

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
ini复制代码from app.models import db
from datetime import datetime


class User(db.Model):
    id = db.Column(db.INT, primary_key=True)
    username = db.Column(db.String(16), unique=True, index=True)
    name = db.Column(db.String(16), index=True)
    password = db.Column(db.String(32), unique=False)
    email = db.Column(db.String(64), unique=True, nullable=False)
    role = db.Column(db.INT, default=0, comment="0: 普通用户 1: 组长 2: 超级管理员")
    created_at = db.Column(db.DATETIME, nullable=False)
    updated_at = db.Column(db.DATETIME, nullable=False)
    deleted_at = db.Column(db.DATETIME)
    last_login_at = db.Column(db.DATETIME)

    def __init__(self, username, name, password, email):
        self.username = username
        self.password = password
        self.email = email
        self.name = name
        self.created_at = datetime.now()
        self.updated_at = datetime.now()
        self.role = 0

    def __repr__(self):
        return '<User %r>' % self.username
  • 在引入models的地方初始化数据表

我们在pity/app/dao/__init__.py dao层初始化所有表,以后新增一个表都需要在这儿import一次

1
2
3
4
javascript复制代码from app.models import db
from app.models.user import User

db.create_all()
  • 在run.py引入dao包使得建表语句db.create_all()生效

图片

重启服务后可以看到出现了user表:

图片

后端代码地址: github.com/wuranxu/pit…

前端代码地址: github.com/wuranxu/pit…

「觉得有用的话可以帮忙点个Star哦QAQ」

本文使用 文章同步助手 同步

本文转载自: 掘金

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

springboot整合redisson(二)实现超强的分布

发表于 2021-06-27

一、springboot整合redisson环境

请参考我的上一篇博客:springboot整合redisson(一)搭建Redisson环境。

二、什么是锁?

我们讲的锁一般指的是同步锁,同步锁是为了保证多线程的操作都能符合预期结果,不会因为cpu缓存等问题导致发生数据错乱问题,举一个现实中的例子,你可能就好理解了,在古代,由于没有计算机,所以钱庄都是使用账本记录每个客户的账户余额信息,张三再钱庄一共存了 10 两银子,突然有一天,李四飞鸽传书张三(顺便丢给了张三一个卡号),以嫖娼被抓为由借 3 两银子应急,作为铁哥们的张三,自然不会看着李四再衙门里受苦,所以就去钱庄给李四汇钱,不过这个时候张三的妈妈担心张三一个人再他乡受苦,她准备到附近的钱庄给张三打 50 两银子,正好张三给李四打钱和张三妈妈给张三打钱都在同一时间,钱庄店员 A 去翻阅账本查看张三的余额,库房总管告诉店员A张三账户余额:10 元,店员B也去翻阅账本查看张三的余额,库房总管告诉店员 B 张三账户余额10 元,店员 A 先将李四的账户增加了 3 两,告诉库房总管,张三的余额需要修改为 7 两,然后库房总管将账本上张三二点余额修改为 7 两。由于店员 B 看到张三的余额也是 10 两,减去张三妈妈账户 50 两,然后往张三的账户增加 10 两,店员 B 告诉库房总管,张三的余额需要变更为:60 两,所以这个时候张三的账户就变成了 60 两。 这个时候问题就出现了,张三的最后余额应该是 57 两,而不是 60 两。

久而久之,钱庄发现流水一直对不上,终于发现了问题所在,所以钱庄老板做出了改进方案:只要有店员来库房查询客户余额之后,库房总管就记录下是谁查询的,这个时候锁定库房,其他店员将无法查询库房中的账本信息,直到查询账本的店员修改完客户的余额为止。这样就解决了上面的问题,因为下一个看到的客户余额数据总是最新的,这里就是我们程序中所讲到的:锁。

虽然锁可以保证每个客户的余额不会出现差错,但是你们发现了没有,效率变差了很多,店员 A 需要对张三的账户余额做变更,店员 B 需要对李四余额做变更,但是因为店员 A 先去库房查询了张三的余额,导致库房总管将账本全部锁定,店员 B 无法查询,只能等店员 A 回来修改完张三的余额信息(这里可以理解为数据库的表锁)。

过了几个月,虽然流水没有出过问题,但是效率太差了,于是老板想到了一个新套路,一个店员去库房查询客户余额信息的时候,让库房总管标记一下哪个店员,查询了哪个客户,其他店员只要不是操作已经记录的客户,就能成功的获取到客户的余额信息,这样就大大的提高了办公的效率(参考数据库中的行级锁)。

上面的例子只是纯属个人瞎想,钱庄肯定不是这么干的,他们都有着一套完善的执行流程,比我这个强的多。

锁:就是开辟一块临界空间,只有拿到钥匙的人才能进入临界区,进行相关的操作,没有拿到要是的人只能在门口等着。

三、什么是分布式锁

分布式锁,是控制分布式系统之间同步访问共享资源的一种方式。在分布式系统中,常常需要协调他们的动作。如果不同的系统或是同一个系统的不同主机之间共享了一个或一组资源,那么访问这些资源的时候,往往需要互斥来防止彼此干扰来保证一致性,在这种情况下,便需要使用到分布式锁。

四、rediison分布式锁

1、可重入锁(Reentrant Lock),不可中断

redisson实现了 java.util.concurrent.locks.Lock 接口,同时还提供了异步(Async)、反射式(Reactive)和RxJava2标准的接口。

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
java复制代码package com.nlx.redisson.core;

import lombok.extern.slf4j.Slf4j;
import org.redisson.api.*;
import org.redisson.client.codec.Codec;
import org.redisson.codec.MarshallingCodec;
import org.springframework.context.annotation.Configuration;
import org.springframework.util.StringUtils;

import java.util.concurrent.TimeUnit;

/**
* @ClassName RedissonTemplate
* redisson封装操作类
* @author nlx
*/
@Configuration
@Slf4j
public class RedissonTemplate {


private final RedissonClient redissonClient;


/**
* 锁前缀
*/
private final String DEFAULT_LOCK_NAME = "nlx-instance";


public RedissonTemplate(RedissonClient redissonClient) {
this.redissonClient = redissonClient;
}





/**
* 加锁(可重入),会一直等待获取锁,不会中断
* @param lockName waitTimeout timeout
* @return boolean
* @author ymy
* @date 2021/5/13 17:53
*/
public boolean lock(String lockName, long timeout) {
checkRedissonClient();
RLock lock = getLock(lockName);
try {
if(timeout != -1){
// timeout:超时时间 TimeUnit.SECONDS:单位
lock.lock(timeout, TimeUnit.SECONDS);
}else{
lock.lock();
}
log.debug(" get lock success ,lockKey:{}", lockName);
return true;
} catch (Exception e) {
log.error(" get lock fail,lockKey:{}, cause:{} ",
lockName, e.getMessage());
return false;
}
}

/**
* 解锁
* @param lockName
*/
public void unlock(String lockName){
checkRedissonClient();
try {
RLock lock = getLock(lockName);
if(lock.isLocked() && lock.isHeldByCurrentThread()){
lock.unlock();
log.debug("key:{},unlock success",lockName);
}else{
log.debug("key:{},没有加锁或者不是当前线程加的锁 ",lockName);
}
}catch (Exception e){
log.error("key:{},unlock error,reason:{}",lockName,e.getMessage());
}
}



private RLock getLock(String lockName) {
String key = DEFAULT_LOCK_NAME + lockName;
return redissonClient.getLock(key);
}


private void checkRedissonClient() {
if (null == redissonClient) {
log.error(" redissonClient is null ,please check redis instance ! ");
throw new RuntimeException("redissonClient is null ,please check redis instance !");
}
if (redissonClient.isShutdown()) {
log.error(" Redisson instance has been shut down !!!");
throw new RuntimeException("Redisson instance has been shut down !!!");
}
}
}
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
java复制代码package com.nlx.redisson;

import com.nlx.redisson.core.RedissonTemplate;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;

@SpringBootTest
@Slf4j
class SpringbootRedissonApplicationTests {

@Autowired
private RedissonTemplate redissonTemplate;

private CountDownLatch count = new CountDownLatch(2);

@Test
void contextLoads() {
String lockName = "hello-test";


new Thread(() ->{

String threadName = Thread.currentThread().getName();
log.info("线程:{} 正在尝试获取锁。。。",threadName);
boolean lock = redissonTemplate.lock(lockName, 60L);
doSomthing(lock,lockName,threadName);

}).start();

new Thread(() ->{
String threadName = Thread.currentThread().getName();
log.info("线程:{} 正在尝试获取锁。。。",threadName);
boolean lock = redissonTemplate.lock(lockName, 60L);
doSomthing(lock,lockName,threadName);
}).start();

try {
count.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
log.info("子线程都已执行完毕,main函数可以结束了!");

}

private void doSomthing(boolean lock,String lockName,String threadName) {
if(lock){
log.info("线程:{},获取到了锁",threadName);
try{
try {
TimeUnit.SECONDS.sleep(5L);
count.countDown();
} catch (InterruptedException e) {
e.printStackTrace();
}
}finally {
redissonTemplate.unlock(lockName);
log.info("线程:{},释放了锁",threadName);
}
}
}

}

在单元测试中,使用了两个线程同时去获取锁:hello-test,获取到锁的线程休眠5秒,然后释放锁资源,下面我来解释一下比较重要的几行代码。
CountDownLatch:线程同步,为了能在main函数执行结束之前看到连个子线程的执行结果。

  • RedissonTemplate:我自己封装的redisson工具类。
  • RedissonClient:redisson官方封装的工具类。
  • redissonClient.getLock(key):获取锁对象。
  • lock.lock(timeout, TimeUnit.SECONDS):锁定,timeout:超时时间 ,TimeUnit.SECONDS:单位。
  • lock.lock(); 锁定,没有超时间时间,如果当前线程一直不释放锁资源,其他线程将会一直处于阻塞状态。 lock.unlock();解锁。

我们直接来看单元测试的执行结果:

在这里插入图片描述

我们可以发现,Thread-3 在15:04:18.947 获取到了锁,而这个时候 Thread-4 还处于阻塞状态,直到5秒之后 15:04:23.977 Thread-3释放了锁,Thread-4 在 15:04:23.991 获取到了锁,Thread-4 大概阻塞了5秒钟,可以理解为 Thread-4 一直在等待锁资源的释放,如果只有锁的线程一直不释放锁,那么 Thread-4 将会一直处于等待状态,那你可能就会有疑问了,Thread-4 会等他多久呢?不要怀疑它的专一,它会等到天荒地老,海枯石烂,宇宙毁灭。说直白一点就是,==除非有其他线程执行 Thread-4 线程的 interrupted()方法,否者 它的等待将用于休止==。

我们将这种无休止的等待称为:不可中断,我们使用 RLock 中的lock() 方法的特性就是不可中断,这种锁存在比较大二点安全隐患,稍不注意,就能让你程序万劫不复。这点可以参考java并发编程的明星锁:synchronized,它也是一种不可中断类型的锁。

我们的例子中是设置的锁的过期时间,他还支持不设置过期时间,这种情况下,只要程序不解锁,那么其他线程都将一直处于阻塞状态,这样就会引发一个很严重的问题,那就是在线程获取到了锁之后,程序或者服务器突然宕机,等重启完成之后,其他线程也会一直处于阻塞状态,因为宕机前获取的锁还没有被释放。

redisson也为我们考虑到了这个问题,所以它设置一个看门狗。它的作用是在Redisson实例被关闭前,不断的延长锁的有效期。默认情况下,看门狗的检查锁的超时时间是30秒钟,也可以通过修改Config.lockWatchdogTimeout来另行指定。

说直白一点,如果你加的锁没有指定过期时间,那么redisson会默认将这个锁的过期时间设置为 30 秒,快到 30 的程序去自动续期,直到程序把锁释放,如果这个时候服务器宕机了,那么程序的续期功能自然也就不存在了,锁最多还能再存活 30 秒,这个大家可以自己去测试一下,我这里就不做测试了,很简单,不带超时间锁定之后,去redis中查看当前锁的有效期是不是你 Config.lockWatchdogTimeout 参数指定的时间,然后过了这个时间,有效期是否自动刷新。

2.可重入锁(Reentrant Lock),可中断

我们先来看看RLock 给我提供的可中断锁的方法有哪些

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
java复制代码
boolean tryLock();

boolean tryLock(long time, TimeUnit unit) throws InterruptedException;

boolean tryLock(long waitTime, long leaseTime, TimeUnit unit) throws InterruptedException;

RFuture<Boolean> tryLockAsync(long waitTime, long leaseTime, TimeUnit unit);

RFuture<Boolean> tryLockAsync();

RFuture<Boolean> tryLockAsync(long threadId);

RFuture<Boolean> tryLockAsync(long waitTime, TimeUnit unit);

RFuture<Boolean> tryLockAsync(long waitTime, long leaseTime, TimeUnit unit, long threadId);

tryLock():很好理解,尝试着加锁,这里面有几个参数讲解一下:

  • time:等待锁的最长时间。
  • unit:时间单位。
  • waitTime:与time一致,等待锁的最长时间。
  • leaseTime:锁的过期时间。
  • threadId:线程id。

大致意思说的就是一个线程带等待 time/waitTime时长后如果还没有获取到锁,那么当前线程将会放弃获取锁资源的机会,去干其他事情。Async结尾的几个方法主要就是异步加锁的意思。

我们一起来写一个单元测试:
RedissonTemplate.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复制代码 /**
* 可中断锁
* @param lockName 锁名称
* @param waitTimeout 等待时长
* @param unit 时间单位
* @return
*/
public boolean tryLock(String lockName, long waitTimeout, TimeUnit unit) {
checkRedissonClient();
RLock lock = getLock(lockName);
try {
boolean res = lock.tryLock(waitTimeout,unit);
if (!res) {
log.debug(" get lock fail ,lockKey:{}", lockName);
return false;
}
log.debug(" get lock success ,lockKey:{}", lockName);
return true;
} catch (Exception e) {
log.error(" get lock fail,lockKey:{}, cause:{} ",
lockName, e.getMessage());
return false;
}
}

单元测试:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
java复制代码package com.nlx.redisson;

import com.nlx.redisson.core.RedissonTemplate;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;

@SpringBootTest
@Slf4j
class SpringbootRedissonApplicationTests {

@Autowired
private RedissonTemplate redissonTemplate;

private CountDownLatch count = new CountDownLatch(2);

@Test
void contextLoads() {
String lockName = "hello-test";
new Thread(() ->{

String threadName = Thread.currentThread().getName();
log.info("线程:{} 正在尝试获取锁。。。",threadName);
boolean lock = redissonTemplate.tryLock(lockName, 2L,TimeUnit.SECONDS);
doSomthing(lock,lockName,threadName);
}).start();

new Thread(() ->{

String threadName = Thread.currentThread().getName();
log.info("线程:{} 正在尝试获取锁。。。",threadName);
boolean lock = redissonTemplate.tryLock(lockName, 2L,TimeUnit.SECONDS);
doSomthing(lock,lockName,threadName);
}).start();
try {
count.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
log.info("子线程都已执行完毕,main函数可以结束了!");

}

private void doSomthing(boolean lock,String lockName,String threadName) {
if(lock){
log.info("线程:{},获取到了锁",threadName);
try{
try {
TimeUnit.SECONDS.sleep(5L);
} catch (InterruptedException e) {
e.printStackTrace();
}
}finally {
redissonTemplate.unlock(lockName);
log.info("线程:{},释放了锁",threadName);
}
}else{
log.info("线程:{},没有获取到锁,过了等待时长,结束等待",threadName);
}
count.countDown();
}

}

输出结果:
在这里插入图片描述
Thread-3 在 20:22:52.494 的时候尝试获取锁,20:22:52.527 的时候获取到了锁,并且进入到了休眠状态,Thread-4 在 20:22:52.494 的时候尝试获取锁,直到 20:22:54.513 也没有获取到,然后 Thread-4就放弃了等待,直接结束了线程,期间花费了两秒钟的时间,而我们设置的等待时间刚好就是两秒,所以单元测试通过。

3.公平锁(Fair Lock)

基于 Redis 的 Redisson 分布式可重入公平锁也是实现了 java.util.concurrent.locks.Lock 接口的一种 RLock 对象。同时还提供了异步(Async)、反射式(Reactive)和 RxJava2 标准的接口。它保证了当多个Redisson客户端线程同时请求加锁时,优先分配给先发出请求的线程。所有请求线程会在一个队列中排队,当某个线程出现宕机时,Redisson 会等待5秒后继续下一个线程,也就是说如果前面有5个线程都处于等待状态,那么后面的线程会等待至少25秒。

何为公平?就是所谓的先来后到,先获取锁的线程先拿到锁,后面的线程都在后面排着,这里你可以理解为你去做核算检测,工作人员刚把棚子搭好的时候,你就去了,这个时候没有人,你一去就直接做,第二次核算检测的时候,你正好在上班,下班回来之后发现做核算的队伍排得老长老长,这个时候你就不得不排在那些人的后面,等待前面的人核算都做完了,才会轮到你,这就是程序里面的公平锁。前两的那两种都不是公平锁,什么意思呢?非公平锁可以把他想象成小车过十字路,在没有红绿灯以及交警指挥的时候,每辆车都想自己最先通过十字路口,然后疯狂的向前开,然后就导致了后面的堵车,映射程序中利用大量cas去获取锁,非常消耗cpu,这也是为什么十字路口需要红路灯和交警的原因,但是有些十字路口也不需要红绿灯,因为这个十字路口几乎没有什么车,不会造成拥堵,程序也是这样,没有大量的线程竞争的时候,就没有必要设置成公平锁,毕竟红绿灯和公平锁也是需要成本的。

我们一起来看看公平锁的实现方式

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复制代码 /**
* 公平锁
* @param lockName
* @param waitTimeout
* @param timeout
* @param unit
* @return
*/
public boolean getFairLock(String lockName, long waitTimeout,long timeout, TimeUnit unit){
checkRedissonClient();
RLock lock = redissonClient.getFairLock(DEFAULT_LOCK_NAME + lockName);
try {
boolean res = lock.tryLock(waitTimeout,timeout,unit);
if (!res) {
log.debug(" get lock fail ,lockKey:{}", lockName);
return false;
}
log.debug(" get lock success ,lockKey:{}", lockName);
return true;
} catch (Exception e) {
log.error(" get lock fail,lockKey:{}, cause:{} ",
lockName, e.getMessage());
return false;
}
}
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
java复制代码 /**
* 公平锁
*/
@Test
public void testFairLock() throws InterruptedException {
CountDownLatch countDown = new CountDownLatch(3);

String lockName = "hello-test";
new Thread(() -> {
log.info("进入thread1 ======");
log.info("thread1 正在尝试获取锁。。。");
boolean lock = redissonTemplate.getFairLock(lockName, 20L, 7L,TimeUnit.SECONDS);
doSomthing(lock, lockName, "thread1");
}).start();

new Thread(() -> {
log.info("进入thread2 ======");
try {
TimeUnit.SECONDS.sleep(2L);
} catch (InterruptedException e) {
e.printStackTrace();
}
log.info("thread2 休眠结束 正在尝试获取锁。。。");
boolean lock = redissonTemplate.getFairLock(lockName, 20L,7L, TimeUnit.SECONDS);
doSomthing(lock, lockName, "thread2");
}).start();


new Thread(() -> {
log.info("进入thread3 ======");
try {
TimeUnit.SECONDS.sleep(3L);
} catch (InterruptedException e) {
e.printStackTrace();
}
log.info("thread3 休眠结束 正在尝试获取锁。。。");
boolean lock = redissonTemplate.getFairLock(lockName, 20L,7L, TimeUnit.SECONDS);
doSomthing(lock, lockName, "thread3");
}).start();
countDown.await();
}
1
2
3
4
5
6
7
8
9
10
11
12
java复制代码2021-06-27 11:22:13.753  INFO 1128 --- [       Thread-3] c.n.r.SpringbootRedissonApplicationTests : 进入thread1 ======
2021-06-27 11:22:13.754 INFO 1128 --- [ Thread-4] c.n.r.SpringbootRedissonApplicationTests : 进入thread2 ======
2021-06-27 11:22:13.754 INFO 1128 --- [ Thread-3] c.n.r.SpringbootRedissonApplicationTests : thread1 正在尝试获取锁。。。
2021-06-27 11:22:13.754 INFO 1128 --- [ Thread-5] c.n.r.SpringbootRedissonApplicationTests : 进入thread3 ======
2021-06-27 11:22:13.796 INFO 1128 --- [ Thread-3] c.n.r.SpringbootRedissonApplicationTests : 线程:thread1,获取到了锁
2021-06-27 11:22:15.759 INFO 1128 --- [ Thread-4] c.n.r.SpringbootRedissonApplicationTests : thread2 休眠结束 正在尝试获取锁。。。
2021-06-27 11:22:16.767 INFO 1128 --- [ Thread-5] c.n.r.SpringbootRedissonApplicationTests : thread3 休眠结束 正在尝试获取锁。。。
2021-06-27 11:22:18.810 INFO 1128 --- [ Thread-3] c.n.r.SpringbootRedissonApplicationTests : 线程:thread1 正在释放了锁
2021-06-27 11:22:18.867 INFO 1128 --- [ Thread-4] c.n.r.SpringbootRedissonApplicationTests : 线程:thread2,获取到了锁
2021-06-27 11:22:23.869 INFO 1128 --- [ Thread-4] c.n.r.SpringbootRedissonApplicationTests : 线程:thread2 正在释放了锁
2021-06-27 11:22:23.912 INFO 1128 --- [ Thread-5] c.n.r.SpringbootRedissonApplicationTests : 线程:thread3,获取到了锁
2021-06-27 11:22:28.914 INFO 1128 --- [ Thread-5] c.n.r.SpringbootRedissonApplicationTests : 线程:thread3 正在释放了锁

4.联锁(MultiLock)

基于Redis的Redisson分布式联锁RedissonMultiLock对象可以将多个RLock对象关联为一个联锁,每个RLock对象实例可以来自于不同的Redisson实例。

联锁指的是:同时对多个资源进行加索操作,只有所有资源都加锁成功的时候,联锁才会成功。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
java复制代码@Test
public void testMultiLock(){
RLock lock1 = redissonTemplate.getLock("lock1" );
RLock lock2 = redissonTemplate.getLock("lock2");
RLock lock3 = redissonTemplate.getLock("lock3");
RedissonMultiLock lock = new RedissonMultiLock(lock1, lock2, lock3);
boolean flag = lock.tryLock();
if(flag){
try {
log.info("联锁加索成功");
}finally {
//一定要释放锁
lock.unlock();
}
}
}

5.红锁(RedLock)

基于Redis的Redisson红锁RedissonRedLock对象实现了Redlock介绍的加锁算法。该对象也可以用来将多个RLock对象关联为一个红锁,每个RLock对象实例可以来自于不同的Redisson实例

与联锁比较相似,都是对多个资源进行加锁,但是红锁与连锁不同的是,红锁只需要在大部分资源加锁成功即可,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
java复制代码 /**
* 红锁
*/
@Test
public void testRedLock(){
RLock lock1 = redissonTemplate.getLock("lock1" );
RLock lock2 = redissonTemplate.getLock("lock2");
RLock lock3 = redissonTemplate.getLock("lock3");
RedissonRedLock lock = new RedissonRedLock (lock1, lock2, lock3);
boolean flag = lock.tryLock();
if(flag){
try {
log.info("红锁加索成功");
}finally {
//一定要释放锁
lock.unlock();
}
}
}

6.读写锁(ReadWriteLock)

基于Redis的Redisson分布式可重入读写锁RReadWriteLock Java对象实现了java.util.concurrent.locks.ReadWriteLock接口。其中读锁和写锁都继承了RLock接口。

分布式可重入读写锁允许同时有多个读锁和一个写锁处于加锁状态。这点相当于java并发sdk并发包中的 StampedLock 。
如果大家对读写锁还不太熟悉的话,可以参考我的另外两篇文章:
【并发编程】java并发编程之ReentrantReadWriteLock读写锁
【并发编程】面试官:有没有比读写锁更快的锁?

1
2
3
4
5
6
7
8
9
java复制代码/**
* 读写锁
*/
@Test
public void testReadWriteLock(){
RReadWriteLock rwlock = redissonTemplate.getReadWriteLock("testRWLock");
rwlock.readLock().lock();
rwlock.writeLock().lock();
}
1
2
3
4
5
6
7
8
9
java复制代码 /**
* 获取读写锁
* @param lockName
* @return
*/
public RReadWriteLock getReadWriteLock(String lockName) {
return redissonClient.getReadWriteLock(lockName);

}

7.信号量(Semaphore)

基于Redis的Redisson的分布式信号量(Semaphore)Java对象RSemaphore采用了与java.util.concurrent.Semaphore相似的接口和用法。同时还提供了异步(Async)、反射式(Reactive)和RxJava2标准的接口。

1
2
3
4
5
6
7
8
java复制代码 /**
* 信号量
* @param semaphoreName
* @return
*/
public RSemaphore getSemaphore(String semaphoreName) {
return redissonClient.getSemaphore(semaphoreName);
}
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复制代码 /**
* 信号量
*/
@Test
public void testSemaphore() throws InterruptedException {
RSemaphore semaphore = redissonTemplate.getSemaphore("testSemaphore");
//设置许可个数
semaphore.trySetPermits(10);
// //设置许可个数 异步
// semaphore.acquireAsync();
// //获取5个许可
// semaphore.acquire(5);
// //尝试获取一个许可
// semaphore.tryAcquire();
// //尝试获取一个许可 异步
// semaphore.tryAcquireAsync();
// //尝试获取一个许可 等待5秒如果未获取到,则返回false
// semaphore.tryAcquire(5, TimeUnit.SECONDS);
// //尝试获取一个许可 等待5秒如果未获取到,则返回false 异步
// semaphore.tryAcquireAsync(5, TimeUnit.SECONDS);
// //释放一个许可,将其返回给信号量
// semaphore.release();
// //释放 6 个许可 ,将其返回给信号量
// semaphore.release(6);
// //释放一个许可,将其返回给信号量 异步
// semaphore.releaseAsync();

CountDownLatch count = new CountDownLatch(10);
for (int i= 0;i< 15 ;++i){
new Thread(() -> {
try {
String threadName = Thread.currentThread().getName();
log.info("线程:{} 尝试获取许可。。。。。。。。。。。。。",threadName);
//默认获取一个许可,如果没有获取到,则阻塞线程
semaphore.acquire();
log.info("线程:{}获取许可成功。。。。。。。", threadName);
count.countDown();
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
}
count.await();
}

在这里插入图片描述
在实现信号量的时候一定要注意许可数量,如果被使用完,而你用完之后并没有将许可归还给信号量,那么有可能在许可用完之后,之后的线程一直处于阻塞阶段。

关于信号量还有一个:可过期性信号量(PermitExpirableSemaphore),获取到的许可有效期只有你设置的时长,

1
2
3
4
5
6
7
8
9
java复制代码/**
* 可过期性信号量
* @param permitExpirableSemaphoreName
* @return
*/
public RPermitExpirableSemaphore getPermitExpirableSemaphore(String permitExpirableSemaphoreName) {

return redissonClient.getPermitExpirableSemaphore(permitExpirableSemaphoreName);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
java复制代码/**
* 信号量
*/
@Test
public void testPermitExpirableSemaphore() throws InterruptedException {
RPermitExpirableSemaphore semaphore = redissonTemplate.getPermitExpirableSemaphore("testPermitExpirableSemaphore");
//设置许可个数
semaphore.trySetPermits(10);
// 获取一个信号,有效期只有2秒钟。
String permitId = semaphore.acquire(1, TimeUnit.SECONDS);
log.info("许可:{}",permitId);
semaphore.release(permitId);
}

8.闭锁(CountDownLatch)

基于Redisson的Redisson分布式闭锁(CountDownLatch)Java对象RCountDownLatch采用了与java.util.concurrent.CountDownLatch相似的接口和用法。

我在例子中也是用到了java sdk并发包中的 CountDownLatch ,主要是线程同步的作用,redisson同样也实现了这样的功能,我们一起来看一下redisson的代码实现

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复制代码 @Test
public void testCountDownLatch() throws InterruptedException {
RCountDownLatch latch = redissonTemplate.getCountDownLatch("testCountDownLatch");
latch.trySetCount(2);
new Thread(() ->{
log.info("这是一个服务的线程");
try {
TimeUnit.SECONDS.sleep(3);
log.info("线程:{},休眠结束",Thread.currentThread().getName());
latch.countDown();
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();


new Thread(() ->{
log.info("这是另外一个服务的线程");
try {
TimeUnit.SECONDS.sleep(3);
log.info("线程:{},休眠结束",Thread.currentThread().getName());
latch.countDown();
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
latch.await();
log.info("子线程执行结束。。。。。。");
}
1
2
3
4
5
6
7
8
java复制代码 /**
* 闭锁
* @param countDownLatchName
* @return
*/
public RCountDownLatch getCountDownLatch(String countDownLatchName) {
return redissonClient.getCountDownLatch(countDownLatchName);
}

springboot整合redisson实现强大的分布式锁到这里就讲的差不多了,最后在贴一份单元测试和 RedissonTemplate 的完整代码吧。

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
java复制代码package com.nlx.redisson.core;

import lombok.extern.slf4j.Slf4j;
import org.redisson.api.*;
import org.redisson.client.codec.Codec;
import org.redisson.codec.MarshallingCodec;
import org.springframework.context.annotation.Configuration;
import org.springframework.util.StringUtils;

import java.util.concurrent.TimeUnit;

/**
* @ClassName RedissonTemplate
* redisson封装操作类
* @author nlx
*/
@Configuration
@Slf4j
public class RedissonTemplate {


private final RedissonClient redissonClient;


/**
* 锁前缀
*/
private final String DEFAULT_LOCK_NAME = "nlx-instance";


public RedissonTemplate(RedissonClient redissonClient) {
this.redissonClient = redissonClient;
}





/**
* 加锁(可重入),会一直等待获取锁,不会中断
* @param lockName waitTimeout timeout
* @return boolean
* @author ymy
* @date 2021/5/13 17:53
*/
public boolean lock(String lockName, long timeout) {
checkRedissonClient();
RLock lock = getLock(lockName);
try {
if(timeout != -1){
// timeout:超时时间 TimeUnit.SECONDS:单位
lock.lock(timeout, TimeUnit.SECONDS);
}else{
lock.lock();
}
log.debug(" get lock success ,lockKey:{}", lockName);
return true;
} catch (Exception e) {
log.error(" get lock fail,lockKey:{}, cause:{} ",
lockName, e.getMessage());
return false;
}
}


/**
* 可中断锁
* @param lockName 锁名称
* @param waitTimeout 等待时长
* @param unit 时间单位
* @return
*/
public boolean tryLock(String lockName, long waitTimeout, TimeUnit unit) {
checkRedissonClient();
RLock lock = getLock(lockName);
try {
boolean res = lock.tryLock(waitTimeout,unit);
if (!res) {
log.debug(" get lock fail ,lockKey:{}", lockName);
return false;
}
log.debug(" get lock success ,lockKey:{}", lockName);
return true;
} catch (Exception e) {
log.error(" get lock fail,lockKey:{}, cause:{} ",
lockName, e.getMessage());
return false;
}
}

/**
* 公平锁
* @param lockName
* @param waitTimeout
* @param timeout
* @param unit
* @return
*/
public boolean getFairLock(String lockName, long waitTimeout,long timeout, TimeUnit unit){
checkRedissonClient();
RLock lock = redissonClient.getFairLock(DEFAULT_LOCK_NAME + lockName);
try {
boolean res = lock.tryLock(waitTimeout,timeout,unit);
if (!res) {
log.debug(" get lock fail ,lockKey:{}", lockName);
return false;
}
log.debug(" get lock success ,lockKey:{}", lockName);
return true;
} catch (Exception e) {
log.error(" get lock fail,lockKey:{}, cause:{} ",
lockName, e.getMessage());
return false;
}
}




/**
* 解锁
* @param lockName
*/
public void unlock(String lockName){
checkRedissonClient();
try {
RLock lock = redissonClient.getFairLock(DEFAULT_LOCK_NAME + lockName);
if(lock.isLocked() && lock.isHeldByCurrentThread()){
lock.unlock();
log.debug("key:{},unlock success",lockName);
}else{
log.debug("key:{},没有加锁或者不是当前线程加的锁 ",lockName);
}
}catch (Exception e){
log.error("key:{},unlock error,reason:{}",lockName,e.getMessage());
}
}



public RLock getLock(String lockName) {
String key = DEFAULT_LOCK_NAME + lockName;
return redissonClient.getLock(key);
}


private void checkRedissonClient() {
if (null == redissonClient) {
log.error(" redissonClient is null ,please check redis instance ! ");
throw new RuntimeException("redissonClient is null ,please check redis instance !");
}
if (redissonClient.isShutdown()) {
log.error(" Redisson instance has been shut down !!!");
throw new RuntimeException("Redisson instance has been shut down !!!");
}
}


/**
* 获取读写锁
* @param lockName
* @return
*/
public RReadWriteLock getReadWriteLock(String lockName) {
return redissonClient.getReadWriteLock(lockName);

}

/**
* 信号量
* @param semaphoreName
* @return
*/
public RSemaphore getSemaphore(String semaphoreName) {
return redissonClient.getSemaphore(semaphoreName);
}

/**
* 可过期性信号量
* @param permitExpirableSemaphoreName
* @return
*/
public RPermitExpirableSemaphore getPermitExpirableSemaphore(String permitExpirableSemaphoreName) {

return redissonClient.getPermitExpirableSemaphore(permitExpirableSemaphoreName);
}

/**
* 闭锁
* @param countDownLatchName
* @return
*/
public RCountDownLatch getCountDownLatch(String countDownLatchName) {
return redissonClient.getCountDownLatch(countDownLatchName);
}
}
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
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
java复制代码package com.nlx.redisson;

import com.nlx.redisson.core.RedissonTemplate;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Test;
import org.redisson.RedissonMultiLock;
import org.redisson.RedissonRedLock;
import org.redisson.api.*;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;

@SpringBootTest
@Slf4j
class SpringbootRedissonApplicationTests {

@Autowired
private RedissonTemplate redissonTemplate;

private CountDownLatch count = new CountDownLatch(2);

@Test
void contextLoads() {
String lockName = "hello-test";
new Thread(() ->{

String threadName = Thread.currentThread().getName();
log.info("线程:{} 正在尝试获取锁。。。",threadName);
boolean lock = redissonTemplate.tryLock(lockName, 2L,TimeUnit.SECONDS);
doSomthing(lock,lockName,threadName);
}).start();

new Thread(() ->{

String threadName = Thread.currentThread().getName();
log.info("线程:{} 正在尝试获取锁。。。",threadName);
boolean lock = redissonTemplate.tryLock(lockName, 2L,TimeUnit.SECONDS);
doSomthing(lock,lockName,threadName);
}).start();
try {
count.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
log.info("子线程都已执行完毕,main函数可以结束了!");

}

private void doSomthing(boolean lock,String lockName,String threadName) {
if(lock){
log.info("线程:{},获取到了锁",threadName);
try{
try {
TimeUnit.SECONDS.sleep(5L);
} catch (InterruptedException e) {
e.printStackTrace();
}
}finally {
log.info("线程:{} 正在释放了锁",threadName);
redissonTemplate.unlock(lockName);
}
}else{
log.info("线程:{},没有获取到锁,过了等待时长,结束等待",threadName);
}
count.countDown();
}


/**
* 公平锁
*/
@Test
public void testFairLock() throws InterruptedException {
CountDownLatch countDown = new CountDownLatch(3);

String lockName = "hello-test";
new Thread(() -> {
log.info("进入thread1 ======");
log.info("thread1 正在尝试获取锁。。。");
boolean lock = redissonTemplate.getFairLock(lockName, 20L, 7L,TimeUnit.SECONDS);
doSomthing(lock, lockName, "thread1");
}).start();

new Thread(() -> {
log.info("进入thread2 ======");
try {
TimeUnit.SECONDS.sleep(2L);
} catch (InterruptedException e) {
e.printStackTrace();
}
log.info("thread2 休眠结束 正在尝试获取锁。。。");
boolean lock = redissonTemplate.getFairLock(lockName, 20L,7L, TimeUnit.SECONDS);
doSomthing(lock, lockName, "thread2");
}).start();


new Thread(() -> {
log.info("进入thread3 ======");
try {
TimeUnit.SECONDS.sleep(3L);
} catch (InterruptedException e) {
e.printStackTrace();
}
log.info("thread3 休眠结束 正在尝试获取锁。。。");
boolean lock = redissonTemplate.getFairLock(lockName, 20L,7L, TimeUnit.SECONDS);
doSomthing(lock, lockName, "thread3");
}).start();
countDown.await();
}


/**
* 联锁
*/
@Test
public void testMultiLock(){
RLock lock1 = redissonTemplate.getLock("lock1" );
RLock lock2 = redissonTemplate.getLock("lock2");
RLock lock3 = redissonTemplate.getLock("lock3");
RedissonMultiLock lock = new RedissonMultiLock(lock1, lock2, lock3);
boolean flag = lock.tryLock();
if(flag){
try {
log.info("联锁加索成功");
}finally {
//一定要释放锁
lock.unlock();
}
}
}


/**
* 红锁
*/
@Test
public void testRedLock(){
RLock lock1 = redissonTemplate.getLock("lock1" );
RLock lock2 = redissonTemplate.getLock("lock2");
RLock lock3 = redissonTemplate.getLock("lock3");
RedissonRedLock lock = new RedissonRedLock (lock1, lock2, lock3);
boolean flag = lock.tryLock();
if(flag){
try {
log.info("红锁加索成功");
}finally {
//一定要释放锁
lock.unlock();
}
}
}

/**
* 读写锁
*/
@Test
public void testReadWriteLock(){
RReadWriteLock rwlock = redissonTemplate.getReadWriteLock("testRWLock");
rwlock.readLock().lock();
rwlock.writeLock().lock();
}


/**
* 信号量
*/
@Test
public void testSemaphore() throws InterruptedException {
RSemaphore semaphore = redissonTemplate.getSemaphore("testSemaphore");
//设置许可个数
semaphore.trySetPermits(10);
// //设置许可个数 异步
// semaphore.acquireAsync();
// //获取5个许可
// semaphore.acquire(5);
// //尝试获取一个许可
// semaphore.tryAcquire();
// //尝试获取一个许可 异步
// semaphore.tryAcquireAsync();
// //尝试获取一个许可 等待5秒如果未获取到,则返回false
// semaphore.tryAcquire(5, TimeUnit.SECONDS);
// //尝试获取一个许可 等待5秒如果未获取到,则返回false 异步
// semaphore.tryAcquireAsync(5, TimeUnit.SECONDS);
// //释放一个许可,将其返回给信号量
// semaphore.release();
// //释放 6 个许可 ,将其返回给信号量
// semaphore.release(6);
// //释放一个许可,将其返回给信号量 异步
// semaphore.releaseAsync();

CountDownLatch count = new CountDownLatch(10);
for (int i= 0;i< 15 ;++i){
new Thread(() -> {
try {
String threadName = Thread.currentThread().getName();
log.info("线程:{} 尝试获取许可。。。。。。。。。。。。。",threadName);
//默认获取一个许可,如果没有获取到,则阻塞线程
semaphore.acquire();
log.info("线程:{}获取许可成功。。。。。。。", threadName);
count.countDown();
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
}
count.await();
}



/**
* 信号量
*/
@Test
public void testPermitExpirableSemaphore() throws InterruptedException {
RPermitExpirableSemaphore semaphore = redissonTemplate.getPermitExpirableSemaphore("testPermitExpirableSemaphore");
//设置许可个数
semaphore.trySetPermits(10);
// 获取一个信号,有效期只有2秒钟。
String permitId = semaphore.acquire(1, TimeUnit.SECONDS);
log.info("许可:{}",permitId);
semaphore.release(permitId);
}

@Test
public void testCountDownLatch() throws InterruptedException {
RCountDownLatch latch = redissonTemplate.getCountDownLatch("testCountDownLatch");
latch.trySetCount(2);
new Thread(() ->{
log.info("这是一个服务的线程");
try {
TimeUnit.SECONDS.sleep(3);
log.info("线程:{},休眠结束",Thread.currentThread().getName());
latch.countDown();
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();


new Thread(() ->{
log.info("这是另外一个服务的线程");
try {
TimeUnit.SECONDS.sleep(3);
log.info("线程:{},休眠结束",Thread.currentThread().getName());
latch.countDown();
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
latch.await();
log.info("子线程执行结束。。。。。。");
}


}

原文路径:springboot整合redisson(二)实现超强的分布式锁

本文转载自: 掘金

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

线程的停止、休眠、礼让和强制执行 Java多线程(三)

发表于 2021-06-27

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


相关文章

Java多线程汇总:Java多线程


前言

静态代理属于设计模式中的代理模式。反之则有动态代理,本篇文章不展开讲,有兴趣的可自行谷歌研究研究。
其实继承Thread也属于静态代理的一种,所以在这里学习静态代理有助于我们学习多线程。

一、静态代理

  • 实际案例:买房
+ 买房人 我
+ 买房办理人 中介
+ 共同的行为 买房
  • 代码实现案例:
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
java复制代码class MyI implements BuyHouse {

//对我来说,我只需负责拿钱,签字即可
@Override
public void Buy() {
System.out.println("一百万,签合同,房子是我的了!");
}
}

class Agent implements BuyHouse{
private BuyHouse buyHouse;

public Agent(BuyHouse buyHouse){
this.buyHouse = buyHouse;
}

//先帮我准备合同等材料
public void work1(){
System.out.println("准备合同等材料~");
}
//带我去房管局办理手续
public void work2(){
System.out.println("带着客户去办手续~");
}

//中介收了我的钱,他得帮我准备购房材料,带着我跑购房流程等等
@Override
public void Buy() {
work1();
work2();
//客户买房
buyHouse.Buy();
}
}
  • 执行结果如下:

在这里插入图片描述

  • 结论:
    • 本质上还是相当于把业务分开,降低程序的耦合性,不管是中介还是我,最终的目的都是买房,我只关注于买房的业务,其他业务无需管,而中介需要准备材料,准备合同,带我去房管局等一系列流程。

二、线程停止(stop)

  • 在Java中有3种方法可以停止正在运行的线程:
+ 使用退出标志使线程正常终止,也就是当run方法完成后线程终止。
+ 使用**stop**方法强行终止线程,但是不推荐使用这个方法,因为stop和suspend、resume一样,都是过期作废的方法。
+ 使用interrupt方法中断线程。
  • 使用标志位来停止线程:
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复制代码/**
* 使用状态标记来停止线程
*/
public class TestThreadDemo01 implements Runnable{
public static void main(String[] args) throws Exception {
TestThreadDemo01 threadDemo01 = new TestThreadDemo01();
new Thread(threadDemo01).start();
for (int i=0;i<1000;i++){
System.out.println("主线程"+i);
if (i==900){
threadDemo01.stop();
System.out.println("线程停止了!");
break;
}
}
}

//状态标记
private boolean flag = true;

@Override
public void run() {
int i = 0;
while (flag){
System.out.println("线程!" + (i++));
}
}

public void stop(){
this.flag = false;
}

}
  • 执行结果如下:

在这里插入图片描述

三、线程休眠(sleep)

  • 总结:
+ sleep(时间)指定当前线程阻塞的毫秒数;
+ sleep存在异常InterruptedException
+ sleep时间到达后线程进入就绪状态
+ sleep可以模拟网络延时,倒计时等。
+ 每一个对象都有一个锁,sleep不会释放锁
  • 代码案例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
java复制代码/**
* 线程休眠 sleep
*/
public class TestThreadSleep implements Runnable{
public static void main(String[] args) {
TestThreadSleep th1 = new TestThreadSleep();
new Thread(th1).start();
}

@Override
public void run() {
//模拟倒计时
for (int i=10;i>0;i--){
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(i);
}

}
}
  • 执行结果如下:

在这里插入图片描述

四、线程礼让(yield)

  • 礼让线程,让当前正在执行的线程暂停,但不阻塞
  • 将线程从运行状态转为就绪状态
  • 让cpu重新调度,礼让不一定成功!看CPU心情。
  • 代码案例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
java复制代码/**
* 线程礼让
*/
public class TestThreadComity implements Runnable{
public static void main(String[] args) {
TestThreadComity th = new TestThreadComity();
new Thread(th,"线程1").start();
new Thread(th,"线程2").start();
}

@Override
public void run() {
System.out.println(Thread.currentThread().getName()+"开始执行!");
Thread.yield();
System.out.println(Thread.currentThread().getName()+"结束执行!");
}
}
  • 执行结果如下:

在这里插入图片描述

五、线程强制执行(join)

  • join合并线程,待此线程执行完成后,再执行其他线程,其他线程阻塞
  • 相当于插队
  • 代码案例:
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复制代码/**
* 线程强制执行---插队
*/
public class TestThreadJoin implements Runnable {

public static void main(String[] args) throws Exception {
Thread thread = new Thread(new TestThreadJoin());
for (int i = 0; i < 400; i++) {
System.out.println("主线程在排队!!!" + i);
if (i == 100) {
thread.start();
thread.join();

}
}
}

@Override
public void run () {
for (int i = 0; i < 100; i++) {
System.out.println("VIP线程来插队了!!!" + i);
}
}
}
  • 执行结果如下:等待插队线程执行完,才继续执行主线程!

在这里插入图片描述


路漫漫其修远兮,吾必将上下求索~

如果你认为i博主写的不错!写作不易,请点赞、关注、评论给博主一个鼓励吧~hahah

本文转载自: 掘金

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

开发插件:分享10个非常实用IDEA插件,值得看一看!

发表于 2021-06-27

IDEA是Java开发者必备的开发神器,今天小编给大家分享10个十分实用的插件,希望能对大家的实际开发工作提供帮助!\

  1. Jump To Line 快速导航插件

IntelliJ IDEA 调试器中的许多导航操作可让您在所需位置设置断点,但有时您只需单击即可到达一行。这是Jump To Line插件派上用场的地方。它允许您到达任何行并在那里设置执行点,而无需执行前面的代码。

它提供了简单的导航——只需在 Gutter 区域拖放一个箭头,在所需的行上放置一个执行点。请记住,您必须在移动箭头之前暂停程序。

  1. Key Promoter X 快捷键插件

无需鼠标的编码速度更快、效率更高,这已经不是什么秘密了,但是当IntelliJ IDEA有这么多快捷键需要记住时,你怎么能以键盘为中心呢?

它会训练你使用它们,就像一个持久而细致的coach一样,当您单击IDE中的元素时,它将显示一个带有相关快捷方式的工具提示。此外,对于没有快捷方式的按钮,Key promotor X会提示您创建快捷方式。

熟能生巧!过了一段时间,你会发现你下意识地保存自己的点击和使用必要的快捷方式。

3.Maven Helper Maven管理插件

如果您正在寻找处理Maven项目的其他操作,那么这个插件绝对是必须的。它允许您查看、分析和排除冲突的依赖项。还可以运行和调试Maven目标,等等。值得推荐!

  1. Rainbow brackets 花括号插件

如果你曾经对嵌套元素使用的重复括号感到恼火,这个插件将是你的救命稻草。它为每一组开、闭括号提供自己的颜色,从而更容易跟踪代码块的起始和结束位置。

5.Randomness 随机数插件

需要向项目中添加随机数据,如单词、数字或字符串?如果您希望使用各种变量值,请安装此插件,并在Windows和Linux或Windows上按Alt+R(单击macOS上的R),查看可以添加的可能数据类型的下拉列表。选择一个你需要的,然后魔术就会发生-随机插件将添加一个不同的值,每次你应用的行动。\

6、Translation 翻译插件

IDEA中非常使用的翻译插件,当你在为你一个变量命名费脑筋的时候,可以使用该插件快速翻译,找到合适的命名。

  1. EduTools 学习插件

这个插件对学习者和教育者都是有益的。它允许你学习和教编程语言,比如Kotlin, Java, Python, JavaScript, Rust, Scala, C/C++, and Go,如果您正在学习编码,我们鼓励您在实践中学习。安装插件以加入现成的公共编程课程,或注册您的老师或同事提供的自定义课程。是的,你听对了,Edu工具插件允许你创建练习并与你的队友分享。

8.GitToolBox 插件

IDEA已经支持全面的Git集成,但是这个插件提供了额外的次要功能来满足您的个人需求。人们得到它主要是因为内联的埋怨,这会显示谁改变了代码在一行和何时改变的。GitToolBox还添加了状态显示、自动获取、隐藏通知等功能。

安装这个插件可以加入50多万人的行列,他们使用它来简化他们的日常Git工作流程。

  1. WakaTime 代码跟踪插件

这就像一个健身追踪器,但用于监控您的编码活动。该WakaTime插件提供了实时跟踪服务,同时自动生成整洁和有吸引力的指标和见解。使用它来分析团队生产力或寻找提高自己编程速度的方法。\

10.Free Mybatis Plugin 插件

当你在使用mybatis框架的时候,你还在一个类一个类的点开寻找对应mapper或者dao程序的位置吗?那样就会显得特别麻烦且浪费时间。而这个Free Mybatis plugin插件提供了跳转的功能。通过点击箭头就可以跳转到相应的地方。

IT技术分享社区

个人博客网站:programmerblog.xyz

文章推荐:
程序员效率:画流程图常用的工具程序员效率:整理常用的在线笔记软件远程办公:常用的远程协助软件,你都知道吗?51单片机程序下载、ISP及串口基础知识硬件:断路器、接触器、继电器基础知识

本文转载自: 掘金

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

Go 每日一库之 resty

发表于 2021-06-27

简介

resty是 Go 语言的一个 HTTP client 库。resty功能强大,特性丰富。它支持几乎所有的 HTTP 方法(GET/POST/PUT/DELETE/OPTION/HEAD/PATCH等),并提供了简单易用的 API。

快速使用

本文代码使用 Go Modules。

创建目录并初始化:

1
2
cmd复制代码$ mkdir resty && cd resty
$ go mod init github.com/darjun/go-daily-lib/resty

安装resty库:

1
cmd复制代码$ go get -u github.com/go-resty/resty/v2

下面我们来获取百度首页信息:

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

import (
"fmt"
"log"

"github.com/go-resty/resty/v2"
)

func main() {
client := resty.New()

resp, err := client.R().Get("https://baidu.com")

if err != nil {
log.Fatal(err)
}

fmt.Println("Response Info:")
fmt.Println("Status Code:", resp.StatusCode())
fmt.Println("Status:", resp.Status())
fmt.Println("Proto:", resp.Proto())
fmt.Println("Time:", resp.Time())
fmt.Println("Received At:", resp.ReceivedAt())
fmt.Println("Size:", resp.Size())
fmt.Println("Headers:")
for key, value := range resp.Header() {
fmt.Println(key, "=", value)
}
fmt.Println("Cookies:")
for i, cookie := range resp.Cookies() {
fmt.Printf("cookie%d: name:%s value:%s\n", i, cookie.Name, cookie.Value)
}
}

resty使用比较简单。

  • 首先,调用一个resty.New()创建一个client对象;
  • 调用client对象的R()方法创建一个请求对象;
  • 调用请求对象的Get()/Post()等方法,传入参数 URL,就可以向对应的 URL 发送 HTTP 请求了。返回一个响应对象;
  • 响应对象提供很多方法可以检查响应的状态,首部,Cookie 等信息。

上面程序中我们获取了:

  • StatusCode():状态码,如 200;
  • Status():状态码和状态信息,如 200 OK;
  • Proto():协议,如 HTTP/1.1;
  • Time():从发送请求到收到响应的时间;
  • ReceivedAt():接收到响应的时刻;
  • Size():响应大小;
  • Header():响应首部信息,以http.Header类型返回,即map[string][]string;
  • Cookies():服务器通过Set-Cookie首部设置的 cookie 信息。

运行程序输出的响应基本信息:

1
2
3
4
5
6
7
golang复制代码Response Info:
Status Code: 200
Status: 200 OK
Proto: HTTP/1.1
Time: 415.774352ms
Received At: 2021-06-26 11:42:45.307157 +0800 CST m=+0.416547795
Size: 302456

首部信息:

1
2
3
4
5
6
7
8
9
10
11
12
golang复制代码Headers:
Server = [BWS/1.1]
Date = [Sat, 26 Jun 2021 03:42:45 GMT]
Connection = [keep-alive]
Bdpagetype = [1]
Bdqid = [0xf5a61d240003b218]
Vary = [Accept-Encoding Accept-Encoding]
Content-Type = [text/html;charset=utf-8]
Set-Cookie = [BAIDUID=BF2EE47AAAF7A20C6971F1E897ABDD43:FG=1; expires=Thu, 31-Dec-37 23:55:55 GMT; max-age=2147483647; path=/; domain=.baidu.com BIDUPSID=BF2EE47AAAF7A20C6971F1E897ABDD43; expires=Thu, 31-Dec-37 23:55:55 GMT; max-age=2147483647; path=/; domain=.baidu.com PSTM=1624678965; expires=Thu, 31-Dec-37 23:55:55 GMT; max-age=2147483647; path=/; domain=.baidu.com BAIDUID=BF2EE47AAAF7A20C716E90B86906D6B0:FG=1; max-age=31536000; expires=Sun, 26-Jun-22 03:42:45 GMT; domain=.baidu.com; path=/; version=1; comment=bd BDSVRTM=0; path=/ BD_HOME=1; path=/ H_PS_PSSID=34099_31253_34133_34072_33607_34135_26350; path=/; domain=.baidu.com]
Traceid = [1624678965045126810617700867425882583576]
P3p = [CP=" OTI DSP COR IVA OUR IND COM " CP=" OTI DSP COR IVA OUR IND COM "]
X-Ua-Compatible = [IE=Edge,chrome=1]

注意其中有一个Set-Cookie首部,这部分内容会出现在 Cookie 部分:

1
2
3
4
5
6
7
8
golang复制代码Cookies:
cookie0: name:BAIDUID value:BF2EE47AAAF7A20C6971F1E897ABDD43:FG=1
cookie1: name:BIDUPSID value:BF2EE47AAAF7A20C6971F1E897ABDD43
cookie2: name:PSTM value:1624678965
cookie3: name:BAIDUID value:BF2EE47AAAF7A20C716E90B86906D6B0:FG=1
cookie4: name:BDSVRTM value:0
cookie5: name:BD_HOME value:1
cookie6: name:H_PS_PSSID value:34099_31253_34133_34072_33607_34135_26350

自动 Unmarshal

现在很多网站提供 API 接口,返回结构化的数据,如 JSON/XML 格式等。resty可以自动将响应数据 Unmarshal 到对应的结构体对象中。下面看一个例子,我们知道很多 js 文件都托管在 cdn 上,我们可以通过api.cdnjs.com/libraries获取这些库的基本信息,返回一个 JSON 数据,格式如下:

接下来,我们定义结构,然后使用resty拉取信息,自动 Unmarshal:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
golang复制代码type Library struct {
Name string
Latest string
}

type Libraries struct {
Results []*Library
}

func main() {
client := resty.New()

libraries := &Libraries{}
client.R().SetResult(libraries).Get("https://api.cdnjs.com/libraries")
fmt.Printf("%d libraries\n", len(libraries.Results))

for _, lib := range libraries.Results {
fmt.Println("first library:")
fmt.Printf("name:%s latest:%s\n", lib.Name, lib.Latest)
break
}
}

可以看到,我们只需要创建一个结果类型的对象,然后调用请求对象的SetResult()方法,resty会自动将响应的数据 Unmarshal 到传入的对象中。这里设置请求信息时使用链式调用的方式,即在一行中完成多个设置。

运行:

1
2
3
4
golang复制代码$ go run main.go
4040 libraries
first library:
name:vue latest:https://cdnjs.cloudflare.com/ajax/libs/vue/3.1.2/vue.min.js

一共 4040 个库,第一个就是 Vue✌️。我们请求https://api.cdnjs.com/libraries/vue就能获取 Vue 的详细信息:

感兴趣可自行用resty来拉取这些信息。

一般请求下,resty会根据响应中的Content-Type来推断数据格式。但是有时候响应中无Content-Type首部或与内容格式不一致,我们可以通过调用请求对象的ForceContentType()强制让resty按照特定的格式来解析响应:

1
2
3
golang复制代码client.R().
SetResult(result).
ForceContentType("application/json")

请求信息

resty提供了丰富的设置请求信息的方法。我们可以通过两种方式设置查询字符串。一种是调用请求对象的SetQueryString()设置我们拼接好的查询字符串:

1
2
3
golang复制代码client.R().
SetQueryString("name=dj&age=18").
Get(...)

另一种是调用请求对象的SetQueryParams(),传入map[string]string,由resty来帮我们拼接。显然这种更为方便:

1
2
3
4
5
6
golang复制代码client.R().
SetQueryParams(map[string]string{
"name": "dj",
"age": "18",
}).
Get(...)

resty还提供一种非常实用的设置路径参数接口,我们调用SetPathParams()传入map[string]string参数,然后后面的 URL 路径中就可以使用这个map中的键了:

1
2
3
4
5
golang复制代码client.R().
SetPathParams(map[string]string{
"user": "dj",
}).
Get("/v1/users/{user}/details")

注意,路径中的键需要用{}包起来。

设置首部:

1
2
3
golang复制代码client.R().
SetHeader("Content-Type", "application/json").
Get(...)

设置请求消息体:

1
2
3
4
golang复制代码client.R().
SetHeader("Content-Type", "application/json").
SetBody(`{"name": "dj", "age":18}`).
Get(...)

消息体可以是多种类型:字符串,[]byte,对象,map[string]interface{}等。

设置携带Content-Length首部,resty自动计算:

1
2
3
4
golang复制代码client.R().
SetBody(User{Name:"dj", Age:18}).
SetContentLength(true).
Get(...)

有些网站需要先获取 token,然后才能访问它的 API。设置 token:

1
2
3
golang复制代码client.R().
SetAuthToken("youdontknow").
Get(...)

案例

最后,我们通过一个案例来将上面介绍的这些串起来。现在我们想通过 GitHub 提供的 API 获取组织的仓库信息,API 文档见文后链接。GitHub API 请求地址为https://api.github.com,获取仓库信息的请求格式如下:

1
golang复制代码GET /orgs/{org}/repos

我们还可以设置以下这些参数:

  • accept:首部,这个必填,需要设置为application/vnd.github.v3+json;
  • org:组织名,路径参数;
  • type:仓库类型,查询参数,例如public/private/forks(fork的仓库)等;
  • sort:仓库的排序规则,查询参数,例如created/updated/pushed/full_name等。默认按创建时间排序;
  • direction:升序asc或降序dsc,查询参数;
  • per_page:每页多少条目,最大 100,默认 30,查询参数;
  • page:当前请求第几页,与per_page一起做分页管理,默认 1,查询参数。

GitHub API 必须设置 token 才能访问。登录 GitHub 账号,点开右上角头像,选择Settings:

然后,选择Developer settings:

选择Personal access tokens,然后点击右上角的Generate new token:

填写 Note,表示 token 的用途,这个根据自己情况填写即可。下面复选框用于选择该 token 有哪些权限,这里不需要勾选:

点击下面的Generate token按钮即可生成 token:

注意,这个 token 只有现在能看见,关掉页面下次再进入就无法看到了。所以要保存好,另外不要用我的 token,测试完程序后我会删除 token😭。

响应中的 JSON 格式数据如下所示:

字段非常多,为了方便起见,我这里之处理几个字段:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
golang复制代码type Repository struct {
ID int `json:"id"`
NodeID string `json:"node_id"`
Name string `json:"name"`
FullName string `json:"full_name"`
Owner *Developer `json:"owner"`
Private bool `json:"private"`
Description string `json:"description"`
Fork bool `json:"fork"`
Language string `json:"language"`
ForksCount int `json:"forks_count"`
StargazersCount int `json:"stargazers_count"`
WatchersCount int `json:"watchers_count"`
OpenIssuesCount int `json:"open_issues_count"`
}

type Developer struct {
Login string `json:"login"`
ID int `json:"id"`
NodeID string `json:"node_id"`
AvatarURL string `json:"avatar_url"`
GravatarID string `json:"gravatar_id"`
Type string `json:"type"`
SiteAdmin bool `json:"site_admin"`
}

然后使用resty设置路径参数,查询参数,首部,Token 等信息,然后发起请求:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
golang复制代码func main() {
client := resty.New()

var result []*Repository
client.R().
SetAuthToken("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").
SetHeader("Accept", "application/vnd.github.v3+json").
SetQueryParams(map[string]string{
"per_page": "3",
"page": "1",
"sort": "created",
"direction": "asc",
}).
SetPathParams(map[string]string{
"org": "golang",
}).
SetResult(&result).
Get("https://api.github.com/orgs/{org}/repos")

for i, repo := range result {
fmt.Printf("repo%d: name:%s stars:%d forks:%d\n", i+1, repo.Name, repo.StargazersCount, repo.ForksCount)
}
}

上面程序拉取以创建时间升序排列的 3 个仓库:

1
2
3
4
golang复制代码$ go run main.go
repo1: name:gddo stars:1097 forks:289
repo2: name:lint stars:3892 forks:518
repo3: name:glog stars:2738 forks:775

Trace

介绍完resty的主要功能之后,我们再来看看resty提供的一个辅助功能:trace。我们在请求对象上调用EnableTrace()方法启用 trace。启用 trace 可以记录请求的每一步的耗时和其他信息。resty支持链式调用,也就是说我们可以在一行中完成创建请求,启用 trace,发起请求:

1
golang复制代码client.R().EnableTrace().Get("https://baidu.com")

在完成请求之后,我们通过调用请求对象的TraceInfo()方法获取信息:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
golang复制代码ti := resp.Request.TraceInfo()
fmt.Println("Request Trace Info:")
fmt.Println("DNSLookup:", ti.DNSLookup)
fmt.Println("ConnTime:", ti.ConnTime)
fmt.Println("TCPConnTime:", ti.TCPConnTime)
fmt.Println("TLSHandshake:", ti.TLSHandshake)
fmt.Println("ServerTime:", ti.ServerTime)
fmt.Println("ResponseTime:", ti.ResponseTime)
fmt.Println("TotalTime:", ti.TotalTime)
fmt.Println("IsConnReused:", ti.IsConnReused)
fmt.Println("IsConnWasIdle:", ti.IsConnWasIdle)
fmt.Println("ConnIdleTime:", ti.ConnIdleTime)
fmt.Println("RequestAttempt:", ti.RequestAttempt)
fmt.Println("RemoteAddr:", ti.RemoteAddr.String())

我们可以获取以下信息:

  • DNSLookup:DNS 查询时间,如果提供的是一个域名而非 IP,就需要向 DNS 系统查询对应 IP 才能进行后续操作;
  • ConnTime:获取一个连接的耗时,可能从连接池获取,也可能新建;
  • TCPConnTime:TCP 连接耗时,从 DNS 查询结束到 TCP 连接建立;
  • TLSHandshake:TLS 握手耗时;
  • ServerTime:服务器处理耗时,计算从连接建立到客户端收到第一个字节的时间间隔;
  • ResponseTime:响应耗时,从接收到第一个响应字节,到接收到完整响应之间的时间间隔;
  • TotalTime:整个流程的耗时;
  • IsConnReused:TCP 连接是否复用了;
  • IsConnWasIdle:连接是否是从空闲的连接池获取的;
  • ConnIdleTime:连接空闲时间;
  • RequestAttempt:请求执行流程中的请求次数,包括重试次数;
  • RemoteAddr:远程的服务地址,IP:PORT格式。

resty对这些区分得很细。实际上resty也是使用标准库net/http/httptrace提供的功能,httptrace提供一个结构,我们可以设置各个阶段的回调函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
golang复制代码// src/net/http/httptrace.go
type ClientTrace struct {
GetConn func(hostPort string)
GotConn func(GotConnInfo)
PutIdleConn func(err error)
GotFirstResponseByte func()
Got100Continue func()
Got1xxResponse func(code int, header textproto.MIMEHeader) error // Go 1.11
DNSStart func(DNSStartInfo)
DNSDone func(DNSDoneInfo)
ConnectStart func(network, addr string)
ConnectDone func(network, addr string, err error)
TLSHandshakeStart func() // Go 1.8
TLSHandshakeDone func(tls.ConnectionState, error) // Go 1.8
WroteHeaderField func(key string, value []string) // Go 1.11
WroteHeaders func()
Wait100Continue func()
WroteRequest func(WroteRequestInfo)
}

可以从字段名简单了解回调的含义。resty在启用 trace 后设置了如下回调:

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
golang复制代码// src/github.com/go-resty/resty/trace.go
func (t *clientTrace) createContext(ctx context.Context) context.Context {
return httptrace.WithClientTrace(
ctx,
&httptrace.ClientTrace{
DNSStart: func(_ httptrace.DNSStartInfo) {
t.dnsStart = time.Now()
},
DNSDone: func(_ httptrace.DNSDoneInfo) {
t.dnsDone = time.Now()
},
ConnectStart: func(_, _ string) {
if t.dnsDone.IsZero() {
t.dnsDone = time.Now()
}
if t.dnsStart.IsZero() {
t.dnsStart = t.dnsDone
}
},
ConnectDone: func(net, addr string, err error) {
t.connectDone = time.Now()
},
GetConn: func(_ string) {
t.getConn = time.Now()
},
GotConn: func(ci httptrace.GotConnInfo) {
t.gotConn = time.Now()
t.gotConnInfo = ci
},
GotFirstResponseByte: func() {
t.gotFirstResponseByte = time.Now()
},
TLSHandshakeStart: func() {
t.tlsHandshakeStart = time.Now()
},
TLSHandshakeDone: func(_ tls.ConnectionState, _ error) {
t.tlsHandshakeDone = time.Now()
},
},
)
}

然后在获取TraceInfo时,根据各个时间点计算耗时:

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
golang复制代码// src/github.com/go-resty/resty/request.go
func (r *Request) TraceInfo() TraceInfo {
ct := r.clientTrace

if ct == nil {
return TraceInfo{}
}

ti := TraceInfo{
DNSLookup: ct.dnsDone.Sub(ct.dnsStart),
TLSHandshake: ct.tlsHandshakeDone.Sub(ct.tlsHandshakeStart),
ServerTime: ct.gotFirstResponseByte.Sub(ct.gotConn),
IsConnReused: ct.gotConnInfo.Reused,
IsConnWasIdle: ct.gotConnInfo.WasIdle,
ConnIdleTime: ct.gotConnInfo.IdleTime,
RequestAttempt: r.Attempt,
}

if ct.gotConnInfo.Reused {
ti.TotalTime = ct.endTime.Sub(ct.getConn)
} else {
ti.TotalTime = ct.endTime.Sub(ct.dnsStart)
}

if !ct.connectDone.IsZero() {
ti.TCPConnTime = ct.connectDone.Sub(ct.dnsDone)
}

if !ct.gotConn.IsZero() {
ti.ConnTime = ct.gotConn.Sub(ct.getConn)
}

if !ct.gotFirstResponseByte.IsZero() {
ti.ResponseTime = ct.endTime.Sub(ct.gotFirstResponseByte)
}

if ct.gotConnInfo.Conn != nil {
ti.RemoteAddr = ct.gotConnInfo.Conn.RemoteAddr()
}

return ti
}

运行输出:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
golang复制代码$ go run main.go
Request Trace Info:
DNSLookup: 2.815171ms
ConnTime: 941.635171ms
TCPConnTime: 269.069692ms
TLSHandshake: 669.276011ms
ServerTime: 274.623991ms
ResponseTime: 112.216µs
TotalTime: 1.216276906s
IsConnReused: false
IsConnWasIdle: false
ConnIdleTime: 0s
RequestAttempt: 1
RemoteAddr: 18.235.124.214:443

我们看到 TLS 消耗了近一半的时间。

总结

本文我介绍了 Go 语言一款非常方便易用的 HTTP Client 库。 resty提供非常实用的,丰富的 API。链式调用,自动 Unmarshal,请求参数/路径设置这些功能非常方便好用,让我们的工作事半功倍。限于篇幅原因,很多高级特性未能一一介绍,如提交表单,上传文件等等等等。只能留待感兴趣的大家去探索了。

大家如果发现好玩、好用的 Go 语言库,欢迎到 Go 每日一库 GitHub 上提交 issue😄

参考

  1. Go 每日一库 GitHub:github.com/darjun/go-d…
  2. resty GitHub:github.com/go-resty/re…
  3. GitHub API:docs.github.com/en/rest/ove…

我

我的博客:darjun.github.io

欢迎关注我的微信公众号【GoUpUp】,共同学习,一起进步~

本文转载自: 掘金

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

Spring事件发布与监听机制

发表于 2021-06-27

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

本文正在参加「Java主题月 - Java 开发实战」,详情查看 活动链接

我是陈皮,一个在互联网 Coding 的 ITer,微信搜索「陈皮的JavaLib」第一时间阅读最新文章,回复【资料】,即可获得我精心整理的技术资料,电子书籍,一线大厂面试资料和优秀简历模板。

前言

Spring 提供了 ApplicationContext 事件机制,可以发布和监听事件,这个特性非常有用。

Spring 内置了一些事件和监听器,例如在 Spring 容器启动前,Spring 容器启动后,应用启动失败后等事件发生后,监听在这些事件上的监听器会做出相应的响应处理。

当然,我们也可以自定义监听器,监听 Spring 原有的事件。或者自定义我们自己的事件和监听器,在必要的时间点发布事件,然后监听器监听到事件就做出响应处理。

ApplicationContext 事件机制

ApplicationContext 事件机制采用观察者设计模式来实现,通过 ApplicationEvent 事件类和 ApplicationListener 监听器接口,可以实现 ApplicationContext 事件发布与处理。

每当 ApplicationContext 发布 ApplicationEvent 时,如果 Spring 容器中有 ApplicationListener bean,则监听器会被触发执行相应的处理。当然,ApplicationEvent 事件的发布需要显示触发,要么 Spring 显示触发,要么我们显示触发。

ApplicationListener 监听器

定义应用监听器需要实现的接口。此接口继承了 JDK 标准的事件监听器接口 EventListener,EventListener 接口是一个空的标记接口,推荐所有事件监听器必须要继承它。

1
2
3
4
5
6
7
8
9
10
11
12
java复制代码package org.springframework.context;

import java.util.EventListener;

@FunctionalInterface
public interface ApplicationListener<E extends ApplicationEvent> extends EventListener {

/**
* 处理应用事件
*/
void onApplicationEvent(E event);
}
1
2
3
4
java复制代码package java.util;

public interface EventListener {
}

ApplicationListener 是个泛型接口,我们自定义此接口的实现类时,如果指定了泛型的具体事件类,那么只会监听此事件。如果不指定具体的泛型,则会监听 ApplicationEvent 抽象类的所有子类事件。

如下我们定义一个监听器,监听具体的事件,例如监听 ApplicationStartedEvent 事件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
java复制代码package com.chenpi;

import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.context.event.ApplicationStartedEvent;
import org.springframework.context.ApplicationListener;
import org.springframework.stereotype.Component;

/**
* @Description
* @Author 陈皮
* @Date 2021/6/26
* @Version 1.0
*/
@Slf4j
@Component
public class MyApplicationListener implements ApplicationListener<ApplicationStartedEvent> {
@Override
public void onApplicationEvent(ApplicationStartedEvent event) {
log.info(">>> MyApplicationListener:{}", event);
}
}

启动服务,会发现在服务启动后,此监听器被触发了。

image-20210626123301531.png

如果不指定具体的泛型类,则会监听 ApplicationEvent 抽象类的所有子类事件。如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
java复制代码package com.chenpi;

import lombok.extern.slf4j.Slf4j;
import org.springframework.context.ApplicationEvent;
import org.springframework.context.ApplicationListener;
import org.springframework.stereotype.Component;

/**
* @Description
* @Author 陈皮
* @Date 2021/6/26
* @Version 1.0
*/
@Slf4j
@Component
public class MyApplicationListener implements ApplicationListener {
@Override
public void onApplicationEvent(ApplicationEvent event) {
log.info(">>> MyApplicationListener:{}", event);
}
}

image-20210626123623604.png

注意,监听器类的 bean 要注入到 Spring 容器中,不然不会生效。一种是使用注解注入,例如 @Component。另外可以使用 SpringApplicationBuilder.listeners() 方法添加,不过这两种方式有区别的,看以下示例。

首先我们使用 @Component 注解方式,服务启动时,监视到了2个事件:

  • ApplicationStartedEvent
  • ApplicationReadyEvent
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
java复制代码package com.chenpi;

import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.context.event.SpringApplicationEvent;
import org.springframework.context.ApplicationListener;
import org.springframework.stereotype.Component;

/**
* @Description
* @Author 陈皮
* @Date 2021/6/26
* @Version 1.0
*/
@Slf4j
@Component
public class MyApplicationListener implements ApplicationListener<SpringApplicationEvent> {
@Override
public void onApplicationEvent(SpringApplicationEvent event) {
log.info(">>> MyApplicationListener:{}", event);
}
}

image-20210626154143959.png

而使用 SpringApplicationBuilder.listeners() 方法添加监听器,服务启动时,监听到了5个事件:

  • ApplicationEnvironmentPreparedEvent
  • ApplicationContextInitializedEvent
  • ApplicationPreparedEvent
  • ApplicationStartedEvent
  • ApplicationReadyEvent
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
java复制代码package com.chenpi;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.context.ConfigurableApplicationContext;

@SpringBootApplication
public class Application {

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

SpringApplication app = new SpringApplicationBuilder(Application.class)
.listeners(new MyApplicationListener()).build();
app.run(args);
}
}

image-20210626154247410.png

其实这是和监听器 bean 注册的时机有关,@component 注解的监听器 bean 只有在 bean 初始化注册完后才能使用;而通过 SpringApplicationBuilder.listeners() 添加的监听器 bean 是在容器启动前,所以监听到的事件比较多。但是注意,这两个不要同时使用,不然监听器会重复执行两遍。

如果你想在监听器 bean 中注入其他 bean(例如 @Autowired),那最好是使用注解形式,因为如果太早发布监听器,可能其他 bean 还未初始化完成,可能会报错。

ApplicationEvent 事件

ApplicationEvent 是所有应用事件需要继承的抽象类。它继承了 EventObject 类,EventObject 是所有事件的根类,这个类有个 Object 类型的对象 source,代表事件源。所有继承它的类的构造函数都必须要显示传递这个事件源。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
java复制代码package org.springframework.context;

import java.util.EventObject;

public abstract class ApplicationEvent extends EventObject {

private static final long serialVersionUID = 7099057708183571937L;

// 发布事件的系统时间
private final long timestamp;

public ApplicationEvent(Object source) {
super(source);
this.timestamp = System.currentTimeMillis();
}

public final long getTimestamp() {
return this.timestamp;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
java复制代码package java.util;

public class EventObject implements java.io.Serializable {

private static final long serialVersionUID = 5516075349620653480L;

protected transient Object source;

public EventObject(Object source) {
if (source == null)
throw new IllegalArgumentException("null source");

this.source = source;
}

public Object getSource() {
return source;
}

public String toString() {
return getClass().getName() + "[source=" + source + "]";
}
}

在 Spring 中,比较重要的事件类是 SpringApplicationEvent。Spring 有一些内置的事件,当完成某种操作时会触发某些事件。这些内置事件继承 SpringApplicationEvent 抽象类。SpringApplicationEvent 继承 ApplicationEvent 并增加了字符串数组参数字段 args。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
java复制代码/**
* Base class for {@link ApplicationEvent} related to a {@link SpringApplication}.
*
* @author Phillip Webb
* @since 1.0.0
*/
@SuppressWarnings("serial")
public abstract class SpringApplicationEvent extends ApplicationEvent {

private final String[] args;

public SpringApplicationEvent(SpringApplication application, String[] args) {
super(application);
this.args = args;
}

public SpringApplication getSpringApplication() {
return (SpringApplication) getSource();
}

public final String[] getArgs() {
return this.args;
}
}

image-20210626125226462.png

我们可以编写自己的监听器,然后监听这些事件,实现自己的业务逻辑。例如编写 ApplicationListener 接口的实现类,监听 ContextStartedEvent 事件,当应用容器 ApplicationContext 启动时,会发布该事件,所以我们编写的监听器会被触发。

  • ContextRefreshedEvent:ApplicationContext 被初始化或刷新时,事件被发布。ConfigurableApplicationContext接口中的 refresh() 方法被调用也会触发事件发布。初始化是指所有的 Bean 被成功装载,后处理 Bean 被检测并激活,所有单例 Bean 被预实例化,ApplicationContext 容器已就绪可用。
  • ContextStartedEvent:应用程序上下文被刷新后,但在任何 ApplicationRunner 和 CommandLineRunner 被调用之前,发布此事件。
  • ApplicationReadyEvent:此事件会尽可能晚地被发布,以表明应用程序已准备好为请求提供服务。事件源是SpringApplication 本身,但是要注意修改它的内部状态,因为到那时所有初始化步骤都已经完成了。
  • ContextStoppedEvent:ConfigurableApplicationContext 接口的 stop() 被调用停止 ApplicationContext 时,事件被发布。
  • ContextClosedEvent:ConfigurableApplicationContext 接口的 close() 被调用关闭 ApplicationContext 时,事件被发布。注意,一个已关闭的上下文到达生命周期末端后,它不能被刷新或重启。
  • ApplicationFailedEvent:当应用启动失败后发布事件。
  • ApplicationEnvironmentPreparedEvent:事件是在 SpringApplication 启动时发布的,并且首次检查和修改 Environment 时,此时上 ApplicationContext 还没有创建。
  • ApplicationPreparedEvent:事件发布时,SpringApplication 正在启动,ApplicationContext 已经完全准备好,但没有刷新。在这个阶段,将加载 bean definitions 并准备使用 Environment。
  • RequestHandledEvent:这是一个 web 事件,只能应用于使用 DispatcherServlet 的 Web 应用。在使用 Spring 作为前端的 MVC 控制器时,当 Spring 处理用户请求结束后,系统会自动触发该事件。

自定义事件和监听器

前面介绍了自定义监听器,然后监听 Spring 原有的事件。下面介绍自定义事件和自定义监听器,然后在程序中发布事件,触发监听器执行,实现自己的业务逻辑。

首先自定义事件,继承 ApplicationEvent,当然事件可以自定义自己的属性。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
java复制代码package com.chenpi;

import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
import org.springframework.context.ApplicationEvent;

/**
* @Description 自定义事件
* @Author 陈皮
* @Date 2021/6/26
* @Version 1.0
*/
@Getter
@Setter
public class MyApplicationEvent extends ApplicationEvent {

// 事件可以增加自己的属性
private String myField;

public MyApplicationEvent(Object source, String myField) {
// 绑定事件源
super(source);
this.myField = myField;
}

@Override
public String toString() {
return "MyApplicationEvent{" + "myField='" + myField + '\'' + ", source=" + source + '}';
}
}

然后自定义监听器,监听我们自定义的事件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
java复制代码package com.chenpi;

import lombok.extern.slf4j.Slf4j;
import org.springframework.context.ApplicationListener;

/**
* @Description 自定义监听器
* @Author 陈皮
* @Date 2021/6/26
* @Version 1.0
*/
@Slf4j
public class MyApplicationListener implements ApplicationListener<MyApplicationEvent> {
@Override
public void onApplicationEvent(MyApplicationEvent event) {
log.info(">>> MyApplicationListener:{}", event);
}
}

注册监听器和发布事件。注册监听器上面讲解了有两种方式。事件的发布可以通过 ApplicationEventPublisher.publishEvent() 方法。此处演示直接用 configurableApplicationContext 发布,它实现了 ApplicationEventPublisher 接口。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
java复制代码package com.chenpi;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.context.ConfigurableApplicationContext;

@SpringBootApplication
public class Application {

public static void main(String[] args) {
// SpringApplication.run(Application.class, args);
// 注册监听器
SpringApplication app = new SpringApplicationBuilder(Application.class)
.listeners(new MyApplicationListener()).build();
ConfigurableApplicationContext configurableApplicationContext = app.run(args);
// 方便演示,在项目启动后发布事件,当然也可以在其他操作和其他时间点发布事件
configurableApplicationContext
.publishEvent(new MyApplicationEvent("我是事件源,项目启动成功后发布事件", "我是自定义事件属性"));
}
}

启动服务,结果显示确实监听到发布的事件了。

1
2
3
powershell复制代码2021-06-26 16:15:09.584  INFO 10992 --- [           main] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat started on port(s): 8081 (http) with context path ''
2021-06-26 16:15:09.601 INFO 10992 --- [ main] com.chenpi.Application : Started Application in 2.563 seconds (JVM running for 4.012)
2021-06-26 16:15:09.606 INFO 10992 --- [ main] com.chenpi.MyApplicationListener : >>> MyApplicationListener:MyApplicationEvent{myField='我是自定义事件属性', source=我是事件源,项目启动成功后发布事件}

事件监听机制能达到分发,解耦效果,例如可以在业务类中发布事件,让监听在此事件的监听器执行自己的业务处理。例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
java复制代码package com.chenpi;

import org.springframework.context.ApplicationEventPublisher;
import org.springframework.context.ApplicationEventPublisherAware;
import org.springframework.stereotype.Service;

/**
* @Description
* @Author 陈皮
* @Date 2021/6/26
* @Version 1.0
*/
@Service
public class MyService implements ApplicationEventPublisherAware {

private ApplicationEventPublisher applicationEventPublisher;

@Override
public void setApplicationEventPublisher(ApplicationEventPublisher applicationEventPublisher) {
this.applicationEventPublisher = applicationEventPublisher;
}

public void testEvent() {
applicationEventPublisher
.publishEvent(new MyApplicationEvent("我是事件源", "我是自定义事件属性"));
}
}

注解式监听器

除了实现 ApplicationListener 接口创建监听器外,Spring 还提供了注解 @EventListener 来创建监听器。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
java复制代码package com.chenpi;

import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Component;

import lombok.extern.slf4j.Slf4j;

/**
* @Description 自定义监听器
* @Author 陈皮
* @Date 2021/6/26
* @Version 1.0
*/
@Slf4j
@Component
public class MyApplicationListener01 {
@EventListener
public void onApplicationEvent(MyApplicationEvent event) {
log.info(">>> MyApplicationListener:{}", event);
}
}

而且注解还可以通过条件过滤只监听指定条件的事件。例如事件的 myField 属性的值等于”陈皮”的事件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
java复制代码package com.chenpi;

import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Component;

import lombok.extern.slf4j.Slf4j;

/**
* @Description 自定义监听器
* @Author 陈皮
* @Date 2021/6/26
* @Version 1.0
*/
@Slf4j
@Component
public class MyApplicationListener01 {
@EventListener(condition = "#event.myField.equals('陈皮')")
public void onApplicationEvent(MyApplicationEvent event) {
log.info(">>> MyApplicationListener:{}", event);
}
}

还可以在同一个类中定义多个监听,对同一个事件的不同监听还可以指定顺序。order 值越小越先执行。

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
java复制代码package com.chenpi;

import org.springframework.context.event.EventListener;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;

import lombok.extern.slf4j.Slf4j;

/**
* @Description 自定义监听器
* @Author 陈皮
* @Date 2021/6/26
* @Version 1.0
*/
@Slf4j
@Component
public class MyApplicationListener01 {
@Order(2)
@EventListener
public void onApplicationEvent(MyApplicationEvent event) {
log.info(">>> onApplicationEvent order=2:{}", event);
}

@Order(1)
@EventListener
public void onApplicationEvent01(MyApplicationEvent event) {
log.info(">>> onApplicationEvent order=1:{}", event);
}

@EventListener
public void otherEvent(YourApplicationEvent event) {
log.info(">>> otherEvent:{}", event);
}
}

执行结果如下:

1
2
3
powershell复制代码>>> onApplicationEvent order=1:MyApplicationEvent{myField='陈皮', source=我是事件源}
>>> onApplicationEvent order=2:MyApplicationEvent{myField='陈皮', source=我是事件源}
>>> otherEvent:MyApplicationEvent{myField='我是自定义事件属性01', source=我是事件源01}

事件的监听处理是同步的,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
java复制代码package com.chenpi;

import lombok.extern.slf4j.Slf4j;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.context.ApplicationEventPublisherAware;
import org.springframework.stereotype.Service;

/**
* @Description
* @Author 陈皮
* @Date 2021/6/26
* @Version 1.0
*/
@Service
@Slf4j
public class MyService implements ApplicationEventPublisherAware {

private ApplicationEventPublisher applicationEventPublisher;

@Override
public void setApplicationEventPublisher(ApplicationEventPublisher applicationEventPublisher) {
this.applicationEventPublisher = applicationEventPublisher;
}

public void testEvent() {
log.info(">>> testEvent begin");
applicationEventPublisher.publishEvent(new MyApplicationEvent("我是事件源", "陈皮"));
applicationEventPublisher.publishEvent(new YourApplicationEvent("我是事件源01", "我是自定义事件属性01"));
log.info(">>> testEvent end");
}
}

执行结果如下:

1
2
3
4
5
powershell复制代码2021-06-26 20:34:27.990  INFO 12936 --- [nio-8081-exec-1] com.chenpi.MyService                     : >>> testEvent begin
2021-06-26 20:34:27.990 INFO 12936 --- [nio-8081-exec-1] com.chenpi.MyApplicationListener01 : >>> onApplicationEvent order=1:MyApplicationEvent{myField='陈皮', source=我是事件源}
2021-06-26 20:34:27.991 INFO 12936 --- [nio-8081-exec-1] com.chenpi.MyApplicationListener01 : >>> onApplicationEvent order=2:MyApplicationEvent{myField='陈皮', source=我是事件源}
2021-06-26 20:34:27.992 INFO 12936 --- [nio-8081-exec-1] com.chenpi.MyApplicationListener01 : >>> otherEvent:MyApplicationEvent{myField='我是自定义事件属性01', source=我是事件源01}
2021-06-26 20:34:27.992 INFO 12936 --- [nio-8081-exec-1] com.chenpi.MyService : >>> testEvent end

不过,我们也可以显示指定异步方式去执行监听器,记得在服务添加 @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
26
27
28
29
30
31
32
33
34
35
36
37
38
java复制代码package com.chenpi;

import org.springframework.context.event.EventListener;
import org.springframework.core.annotation.Order;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Component;

import lombok.extern.slf4j.Slf4j;

/**
* @Description 自定义监听器
* @Author 陈皮
* @Date 2021/6/26
* @Version 1.0
*/
@Slf4j
@Component
public class MyApplicationListener01 {

@Async
@Order(2)
@EventListener
public void onApplicationEvent(MyApplicationEvent event) {
log.info(">>> onApplicationEvent order=2:{}", event);
}

@Order(1)
@EventListener
public void onApplicationEvent01(MyApplicationEvent event) {
log.info(">>> onApplicationEvent order=1:{}", event);
}

@Async
@EventListener
public void otherEvent(YourApplicationEvent event) {
log.info(">>> otherEvent:{}", event);
}
}

执行结果如下,注意打印的线程名。

1
2
3
4
5
powershell复制代码2021-06-26 20:37:04.807  INFO 9092 --- [nio-8081-exec-1] com.chenpi.MyService                     : >>> testEvent begin
2021-06-26 20:37:04.819 INFO 9092 --- [nio-8081-exec-1] com.chenpi.MyApplicationListener01 : >>> onApplicationEvent order=1:MyApplicationEvent{myField='陈皮', source=我是事件源}
2021-06-26 20:37:04.831 INFO 9092 --- [ task-1] com.chenpi.MyApplicationListener01 : >>> onApplicationEvent order=2:MyApplicationEvent{myField='陈皮', source=我是事件源}
2021-06-26 20:37:04.831 INFO 9092 --- [nio-8081-exec-1] com.chenpi.MyService : >>> testEvent end
2021-06-26 20:37:04.831 INFO 9092 --- [ task-2] com.chenpi.MyApplicationListener01 : >>> otherEvent:MyApplicationEvent{myField='我是自定义事件属性01', source=我是事件源01}

本文转载自: 掘金

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

1…630631632…956

开发者博客

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