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

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


  • 首页

  • 归档

  • 搜索

Spring Security专栏(如何使用高级主题保护we

发表于 2021-11-10

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

写在前面

今天我们一起来学习如何使用高级主题保护web应用,何为高级主题,即包括过滤器、CSRF 保护、CORS 以及全局方法,这些都是非常实用的功能特性,今天这个算是对以前的学习进行下总结吧。

本次分为两部分来说。那么作为阶段性的总结,今天的内容将利用这些功能特性构建在安全领域中的一种典型的认证机制,即多因素认证(Multi-Factor Authentication,MFA)机制。

下面我们通过一些案例分享下

案例设计和初始化

在今天的案例中,我们构建多因素认证的思路并不是采用第三方成熟的解决方案,而是基于 Spring Security 的功能特性来自己设计并实现一个简单而完整的认证机制。

开头说到多因素认证:多因素认证是一种安全访问控制的方法,基本的设计理念在于用户想要访问最终的资源,至少需要通过两种以上的认证机制。

那么,我们如何实现多种认证机制呢?一种常见的做法是分成两个步骤,第一步通过用户名和密码获取一个认证码(Authentication Code),第二步基于用户名和这个认证码进行安全访问。基于这种多因素认证的基本执行流程如下图所示:

image.png

系统初始化

为了实现多因素认证,我们需要构建一个独立的认证服务 Auth-Service,该服务同时提供了基于用户名+密码以及用户名+认证码的认证形式。当然,实现认证的前提是构建用户体系,因此我们需要提供如下所示的 User 实体类:

1
2
3
4
5
6
7
8
9
10
java复制代码@Entity
public class User {
 
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Integer id;
   
    private String username;
    private String password;
}

可以看到,User 对象中包含了用户名 Username 和密码 Password 的定义。同样的,在如下所示的代表认证码的 AuthCode 对象中包含了用户名 Username 和具体的认证码 Code 字段的定义:

1
2
3
4
5
6
7
8
9
10
java复制代码@Entity
public class AuthCode {
 
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Integer id;
   
    private String username;
    private String code;  
}

基于 User 和 AuthCode 实体对象,我们也给出创建数据库表的对应 SQL 定义,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
java复制代码CREATE TABLE IF NOT EXISTS `spring_security_demo`.`user` (
    `id` INT NOT NULL AUTO_INCREMENT,
    `username` VARCHAR(45) NULL,
    `password` TEXT NULL,
    PRIMARY KEY (`id`));
 
CREATE TABLE IF NOT EXISTS `spring_security_demo`.`auth_code` (
    `id` INT NOT NULL AUTO_INCREMENT,
    `username` VARCHAR(45) NOT NULL,
    `code` VARCHAR(45) NULL,
    PRIMARY KEY (`id`));
)

有了认证服务,接下来我们需要构建一个业务服务 Business-Service,该业务服务通过集成认证服务,完成具体的认证操作,并返回访问令牌(Token)给到客户端系统。因此,从依赖关系上讲,Business-Service 会调用 Auth-Service,如下图所示:

image.png

接下来,我们分别从这两个服务入手,实现多因素认证机制。

实现多因素认证机制

对于多因素认证机制而言,实现认证服务是基础,但难度并不大,我们往下看。

实现认证服务

从表现形式上看,认证服务也是一个 Web 服务,所以内部需要通过构建 Controller 层组件实现 HTTP 端点的暴露。为此,我们构建了如下所示的 AuthController:

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
java复制代码@RestController
public class AuthController {
 
    @Autowired
    private UserService userService;
 
    //添加User
    @PostMapping("/user/add")
    public void addUser(@RequestBody User user) {
        userService.addUser(user);
    }
 
    //通过用户名+密码对用户进行首次认证
    @PostMapping("/user/auth")
    public void auth(@RequestBody User user) {
        userService.auth(user);
    }
 
    //通过用户名+认证码进行二次认证
    @PostMapping("/authcode/check")
    public void check(@RequestBody AuthCode authCode, HttpServletResponse response) {
        if (userService.check(authCode)) {
            response.setStatus(HttpServletResponse.SC_OK);
        } else {
            response.setStatus(HttpServletResponse.SC_FORBIDDEN);
        }
    }
}

可以看到,这里除了一个添加用户信息的 HTTP 端点之外,我们分别实现了通过用户名+密码对用户进行首次认证的”/user/auth”端点,以及通过用户名+认证码进行二次认证的”/authcode/check”端点。

这两个核心端点背后的实现逻辑都位于 UserService 中,我们先来看其中的 auth() 方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
java复制代码public void auth(User user) {
        Optional<User> o =
                userRepository.findUserByUsername(user.getUsername());
 
        if(o.isPresent()) {
            User u = o.get();
            if (passwordEncoder.matches(user.getPassword(), u.getPassword())) {
                 //生成或刷新认证码
                generateOrRenewAutoCode(u);
            } else {
                throw new BadCredentialsException("Bad credentials.");
            }
        } else {
            throw new BadCredentialsException("Bad credentials.");
        }
}

上述代码中的关键流程就是在完成用户密码匹配之后的刷新认证码流程,负责实现该流程的 generateOrRenewAutoCode() 方法如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
java复制代码private void generateOrRenewAutoCode (User u) {
        String generatedCode = GenerateCodeUtil.generateCode();
 
        Optional<AuthCode> autoCode = autoCodeRepository.findAuthCodeByUsername(u.getUsername());
        if (autoCode.isPresent()) {//如果存在认证码,则刷新该认证码
            AuthCode code = autoCode.get();
            code.setCode(generatedCode);
        } else {//如果没有找到认证码,则生成并保存一个新的认证码
            AuthCode code = new AuthCode();
            code.setUsername(u.getUsername());
            code.setCode(generatedCode);
            autoCodeRepository.save(code);
        }
}

上述方法的流程也很明确,首先通过调用工具类 GenerateCodeUtil 的 generateCode() 方法生成一个认证码,然后根据当前数据库中的状态决定是否对已有的认证码进行刷新,或者直接生成一个新的认证码并保存。因此,每次调用 UserService 的 auth() 方法就相当于对用户的认证码进行了动态重置。

一旦用户获取了认证码,并通过该认证码访问系统,认证服务就可以对该认证码进行校验,从而确定其是否有效。对认证码进行验证的方法如下所示:

1
2
3
4
5
6
7
8
9
10
11
java复制代码public boolean check(AuthCode authCodeToValidate) {
        Optional<AuthCode> authCode = autoCodeRepository.findAuthCodeByUsername(authCodeToValidate.getUsername());
        if (authCode.isPresent()) {
            AuthCode authCodeInStore = authCode.get();
            if (authCodeToValidate.getCode().equals(authCodeInStore.getCode())) {
                return true;
            }
        }
 
        return false;
}

这里的逻辑也很简单,就是把从数据库中获取的认证码与用户传入的认证码进行比对。

至此,认证服务的核心功能已经构建完毕

下期我们来看业务服务的实现过程。我们还是要一点点学,这样才能消化得了。

下期再见 加油!!!

弦外之音

感谢你的阅读,如果你感觉学到了东西,您可以点赞,关注。也欢迎有问题我们下面评论交流

加油! 我们下期再见!

给大家分享几个我前面写的几篇骚操作

copy对象,这个操作有点骚!

干货!SpringBoot利用监听事件,实现异步操作

本文转载自: 掘金

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

SpringBoot基础之日志

发表于 2021-11-10

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

(一) 什么是日志框架

  1. 在系统开发过程中会将一些关键信息输出,用于测试或者异常问题的回溯.
  2. 随着输出的信息越来越多,信息的表现形式也各式各样,因此抽离并整合出了日志框架,表现形式上就是jar包
  3. 市面上常见的日志框架: Java Util Logging ,Apache Commons Logging,slf4j,log4j,log4j2,logback
  4. 日志框架各种各样,但是总体就分为两类: 日志门面和日志实现 ,就像传话的领导和干活的码农

(1)日志门面和日志实现对比

  1. 日志门面是一种规范,只定义了输入的规范,不提供具体的输出功能,不可以单独使用
  2. 日志实现则是真正的实现了输出功能,它们也有自己的输入规范.理论上可以单独使用而不需要日志门面
  3. 实际上我们总会把日志门面和日志实现组合起来使用.这是因为每个日志实现都有自己的输入规范,一旦需求变动或者功能迁移,那样管理日志输出配置或者日志转换就会冗杂其他.
    就像:原来,我有了问题我直接找领导,领导再去安排具体干活的人; 现在,我直接找对应的人, 但是项目变动了,我需要同时找多个人,但是这几个人思维逻辑和处理方式都不一样,在这种情况下就必要花费更多的经历来沟通,实际上我们承担了领导的这个职责.
  4. Apache Commons Logging,slf4j就是 日志门面 , Java Util Logging ,log4j,log4j2,logback 就是日志实现.

(二)日志框架的使用

对于SpringBoot来说,默认使用的日志门面是Apache Commons Logging,默认使用的日志实现是logbac, 但是我们常用的组合方式则是slfj4+logback 或 slfj4+log4j2

(1) slfj4的使用

  1. 导入slf4j-api.jar包
  2. 在代码中使用日志
    1). 官方推荐方式
1
2
ini复制代码//在class类中使用, XXX代表的是当前类的类名.
Logger logger = LoggerFactory.getLogger(XXX.class);

2). 不推荐的方式

1
ini复制代码Logger logger = LoggerFactory.getLogger(this.getClass());
  1. 如果使用了lombok,推荐的方式则是直接在类上注解@Slf4j

(2) logback的使用

  1. SpringBoot中支持的logback配置有两种:logback.xml和logback-spring.xml
    logback.xml和logback-spring.xml都可以用来配置logback,但是2者的加载顺序是不一样的.

logback.xml—>application.yml—>logback-spring.xml

logback.xml加载早于application.yml,所以如果你需要使用application.yml中的变量,则需要使用logback-spring.xml这种方式.

  1. logback-spring.xml配置
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
xml复制代码<?xml version="1.0" encoding="UTF-8"?>
<configuration debug="false">

<!--定义日志文件的存储地址 尽量不要在 LogBack 的配置中使用相对路径-->
<property name="LOG_HOME" value="${LOG_PATH:-.}" />

<!--控制台日志, 控制台输出 -->
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
<!--格式化输出:%d表示日期,%thread表示线程名,%-5level:级别从左显示5个字符宽度,%msg:日志消息,%n是换行符-->
<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n</pattern>
</encoder>
</appender>


<appender name="FILE_ROLL" class="ch.qos.logback.core.rolling.RollingFileAppender">
<!-- 追加日志到原文件结尾 -->
<Prudent>true</Prudent>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<!--日志文件输出的文件名 每小时生成日志文件-->
<FileNamePattern>${LOG_HOME}/%d{yy-MM-dd}/asdfsdafasdf.%d{yyyy-MM-dd-HH}.%i.log</FileNamePattern>
<!--日志文件保留天数-->
<MaxHistory>30</MaxHistory>
<timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
<!-- 除按日志记录之外,还配置了日志文件不能超过10M(默认),若超过10M,日志文件会以索引0开始, -->
<maxFileSize>100MB</maxFileSize>
</timeBasedFileNamingAndTriggeringPolicy>
</rollingPolicy>
<encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
<!--格式化输出:%d表示日期,%thread表示线程名,%-5level:级别从左显示5个字符宽度 %method 方法名 %L 行数 %msg:日志消息,%n是换行符-->
<pattern> %d{HH:mm:ss.SSS} [%thread] %-5level %logger{56}.%method:%L - %msg%n</pattern>
<charset>utf-8</charset>
</encoder>
</appender>

<!-- 异步输出 -->
<appender name="ASYNC_FILE_ROLL" class="ch.qos.logback.classic.AsyncAppender">
<!-- 不丢失日志.默认的,如果队列的80%已满,则会丢弃TRACT、DEBUG、INFO级别的日志 -->
<discardingThreshold>0</discardingThreshold>
<!-- 更改默认的队列的深度,该值会影响性能.默认值为256 -->
<queueSize>256</queueSize>
<!-- 添加附加的appender,最多只能添加一个 -->
<appender-ref ref="FILE_ROLL"/>
</appender>


<!--指定某些类只输出某种级别-->
<logger name="java.sql.PreparedStatement" level="DEBUG"/>
<logger name="org.apache.http" level="WARN"/>
<logger name="ch.qos.logback" level="WARN"/>
<logger name="o.s.c.annotation" level="INFO"/>
<logger name="org.springframework.jndi" level="INFO"/>

<!-- 日志输出级别 -->
<root level="debug">
<appender-ref ref="STDOUT"/>
<appender-ref ref="ASYNC_FILE_ROLL"/>
</root>

</configuration>

在此处使用了变量${LOG_PATH:-.} ,该变量从application.yml中定义

1
2
3
yaml复制代码logging:
file:
path: ./logs/

因此如果日志有分环境需求,则可以使用logback-spring.xml的方式,然后变量在application-XXX.yml中定义.

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

本文转载自: 掘金

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

删库,误清数据怎么办?MySQL数据恢复指南

发表于 2021-11-10

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

相信很多同学在面对线上数据库时都畏手畏脚,即使这样都难免手滑,一不小心手一抖就将数据或者是表,库删除。当然一些注重规范的公司,不会给开发人员删除表或者是库的权限,但误删数据是常有的事,那么这种情况发生,我们改怎么办呢?跑路?哈哈,当然删库跑路是句玩笑话,本文就为大家介绍一些数据误删除恢复的办法。

1 前言

一般来说,在生产环境DBA都会在定期生成全量数据的备份,然后开启binlog记录增量数据。恢复的时候借助数据备份和binlog日志一般情况下是可以很大程度上复原数据,当然一般情况下开发也不会拥有删库的权限,一般都是有删除数据的权限。所以我们在遇到这种紧急情况不能慌,要赶紧去想办法补救。

2 备份

最简单,也是最实用的方式就是在我们接到,清理数据,或者是修改数据的需求时,先将数据备份,备份是王道。这样会让我们的数据恢复变得更容易。一般在企业中,DBA都会有备份脚本,他们会长期定时对数据进行备份,防止发生悲剧。

3 规范操作

  1. 操作前,先备份,不要怕麻烦,出错后就悔不当初了;
  2. 删除数据库、表时,不要直接用drop命令,而是重命名到一个专用归档库里;
  3. 删除数据时,不要直接用delete或truncate命令,尤其是truncate命令,目前不支持事务,无法回滚;
  4. 使用delete命令删除数据时,应当先开启事务,这样误操作时,还是有机会进行回滚;
  5. 要大批量删除数据时,可以将这些数据插入到一个新表中,确认无误后再删除。或者把要保留的数据写到新表,然后将表重命名对掉,这样需要注意的是增量数据,不要把新插入的数据丢掉;

4 基本的恢复流程

  • 看看是否有办法快速补救(没有可以看下一条)
  • 看看是否有定期备份,和binlog日志(没有就凉凉)
  • 先备份数据恢复
  • 用mysqlbinlog命令将上述的binlog文件导出为sql文件,并剔除其中的drop语句
  • 恢复binlog中增量数据的部分

5 补救措施

  1. 优先考虑是否能只通过binlog恢复,不能的化,再考虑其它
  2. 执行 DROP DATABASE / DROP TABLE 命令误删库表时,如果采用的是共享表空间模式,还有恢复的机会。如果不是,直接从备份文件恢复吧;在共享表空间模式下,误删后立刻杀掉(kill -9)mysql相关进程(mysqld_safe、mysqld),然后尝试从ibdataX文件中恢复数据;
  3. 误删除正在运行中的MySQL表ibd或ibdataX文件。利用linux系统的proc文件特点,把该ibd文件从内存中拷出来,再进行恢复,因为此时mysqld实例在内存中是保持打开该文件的,切记这时不要把mysqld实例关闭了。此模式恢复,需要停止线上业务对该实例的写入操作,不再写入新数据,防止丢失新数据。把复制出来的ibd 或 ibdataX文件拷贝回datadir后,重启mysqld进入recovery模式,innodb_force_recovery 选项从 0 - 6 逐级测试,直至能备份出整个实例或单表的所有数据后,再重建实例或单表,恢复数据。
  4. 未开启事务模式下,执行delete误删数据。发现问题严重性后,立即将mysqld(以及mysqld_safe)进程杀掉(kill -9),然后再用工具将表空间数据读取出来。因为执行delete删除后,实际数据并没有从磁盘清除,只是先打上deleted-mark标签,后续再统一清理,因此快速杀掉进程可以防止数据被物理删除。
  5. 执行truncate误清整张表。如果没使用共享表空间模式的话,直接使用备份恢复和binlog恢复。
  6. 执行不带where条件的update,或者update错数据。数据规模大没法补救的话,也只能通过走备份恢复和binlog恢复。

6 相关操作

  1. 查看是否开启binlog日志
1
2
3
4
5
6
7
ini复制代码# log_bin是ON,就说明打开了 OFF就是关闭状态。
show variables like 'log_bin';
# log_bin相关的内容都能查到
show variables like '%log_bin%';
​
# 设置开启log_bin 一般情况下都是通过配置进行设置
SET SQL_LOG_BIN=1
  1. binlog日志位置
1
sql复制代码show variables like '%datadir%';
  1. 根据binlog日志恢复数据
* cd 到binlog文件目录
* mysql安装目录/mysql/bin/下找到binlog日志解析工具mysqlbinlog
* 通过mysqlbinlog工具命令按照对应时间解析binlog日志内容,输出到新的文件中


该工具也支持过滤指定表的相关操作记录



1
bash复制代码mysqlbinlog --no-defaults --database=test --start-datetime="2021-11-10 09:00:00" --stop-datetime="2021-11-10 20:00:00" /data/mysql/mysql-bin.000020    > binlog.txt
* 利用解析出来的sql进行恢复或者根据需要恢复的位置,使用命令进行恢复
1
arduino复制代码mysqlbinlog --start-position=8000 --stop-position=8888 mysql-bin.000020 |mysql -uroot -p123456;
  1. 通过配置文件对binlog 日志进行配置
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
ini复制代码# 日志格式
# Statement模式,每一条会修改数据的sql都会记录在binlog中。
# Row模式,5.1.5版本的MySQL才开始支持row,它不记录sql语句上下文相关信息,仅保存哪条记录被修改。
# Mixed模式,一般的语句修改使用statment格式保存binlog,如一些函数,statement无法完成主从复制的操作,则采用row格式保存binlog,MySQL会根据执行的每一条具体的sql语句来区分对待记录的日志形式,也就是在Statement和Row之间选择一种。
# 设置日志格式
binlog_format = mixed
​
# 设置日志路径,需要注意的是该路经需要mysql用户有写权限
log-bin = /data/mysql/logs/mysql-bin.log
​
# 设置binlog清理时间
expire_logs_days = 7
​
# binlog每个日志文件大小
max_binlog_size = 100m
​
# binlog缓存大小
binlog_cache_size = 4m
​
# 最大binlog缓存大小
max_binlog_cache_size = 512m

各位大佬,删库请慎重!!!

本文转载自: 掘金

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

Npgsql net 版本的PostgreSQL数据库连接

发表于 2021-11-10

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

Npgsql 是.net 版本的postgreSQL的连接类库,当我们使用.net 进行PostgreSQL的相关开发时,首先接触的就是Npgsql,如果有兴趣的,可以去看看其源代码是怎么写的。

  • 📢欢迎点赞 :👍 收藏 ⭐留言 📝 如有错误敬请指正,赐人玫瑰,手留余香!
  • 📢本文作者:由webmote 原创,首发于 【CSDN】
  • 📢作者格言: 生活在于折腾,当你不折腾生活时,生活就开始折腾你,让我们一起加油!💪💪💪

1 连接字符串

PostgreSQL的精简版本连接字符串如下:

1
sql复制代码"Host=xxxx;Port=5432;Database=postgres;Username=postgres;Password=xxxxx;"

连接字符串的格式是 keyword1=value;keyword2=value;, Key不区分大小写,每组keyvalue,均使用英文分号分割开。

注意:Npgsql实现的链接库仅支持下列章节列出的关键字。

2 基本参数说明

范围 描述 默认
Host 指定运行 PostgreSQL 的主机名。可以指定多个主机,(逗号分隔多主机). 如果该值以斜杠开头,则将其用作 Unix 域套接字的目录 必需的
Port PostgreSQL 服务的 TCP 端口。 默认: 5432
Database 要连接的 PostgreSQL 数据库。 默认:与用户名相同
Username 要连接的用户名。如果使用 IntegratedSecurity,则不需要。 PGUSER
Password 要连接的密码。如果使用 IntegratedSecurity,则不需要。 PGPASSWORD
Passfile PostgreSQL 密码文件 (PGPASSFILE) 的路径,从中获取密码。 PGPASS文件
  1. 安全和加密参数

参数 描述 默认
SSL Mode 控制 SSL 是否开启, 6.0+推荐
Trust Server Certificate 是否信任服务证书而不用验证它. false
Client Certificate 客户端证书路径 PGSSLCERT
Client Certificate Key 证书的Key PGSSLKEY
Root Certificate 验证服务器证书的根证书路径 PGSSLROOTCERT
Check Certificate Revocation 授权后是否检查证书是否撤销 6.0+ 为true
Integrated Security 是否混合验证 (GSS/SSPI). false
Persist Security Info 是否设置连接密码为敏感信息,如果是,则获取连接字符串时不返回密码 false
Kerberos Service Name Kerberos 服务器名称 postgres
Include Realm 授权使用的Kerberos realm
Include Error Detail 当激活时,PostgreSQL错误和通知详情包含在 PostgresException.Detail 和 PostgresNotice.Detail内. false
Log Parameters 如果激活, 命令中的参数值被记录到日志 false

4.链接池参数

重要章节:

参数 描述 默认
Pooling 是否激活链接池. true ,默认激活
Minimum Pool Size 最小链接池数目. 0
Maximum Pool Size 最大链接池数目. 3.1+版本是100 ,以前版本是 20
Connection Idle Lifetime 单位:秒。如果链接数超过了最小链接数字限定,那么在关闭池子中的空闲链接时等待的时间 300
Connection Pruning Interval 链接修剪间隔(多长时间去处理池子中的空闲链接们) 10
ConnectionLifetime 链接的生命周期有多长,在从池子中返回链接时判断,如果超过了则关闭销毁该链接. 这在故障转移和负载均衡上非常有效 0 (disabled)

5 超时和心跳保持参数

极重要章节:

参数 描述 默认
Timeout 单位秒,链接的建立链接的超时时间 15
Command Timeout 单位秒,链接的命令执行超时时间,如果设置为0,则表示不超时 30
Internal Command Timeout 执行一个内部命令时的超时时间. -1 用 CommandTimeout, 0 不超时. -1
Cancellation Timeout 取消一个查询时的超时时间 ,-1 跳过等待, 0 无限等待. 2000
Keepalive 心跳时间,保活链接. 0 (disabled)
Tcp Keepalive 是否用系统默认的tcp keepalive 作为心跳参数 false
Tcp Keepalive Time 单位毫秒,tcp保活时间. 仅支持 Windows. 0 (disabled)
Tcp Keepalive Interval 单位毫秒,没有ACK收到时,连续心跳包的间隔时间 仅支持Windows. Tcp Keepalive Time 值

6 性能参数

参数 描述 默认值
Max Auto Prepare 在任何给定点可以自动准备的最大 SQL 语句数。超过这个数字,最近最少使用的语句将被回收。0禁用自动准备。 0
Auto Prepare Min Usages SQL 语句在自动准备之前使用的最少使用次数。 5
Use Perf Counters 使 Npgsql 将有关连接使用的性能信息写入 Windows 性能计数器。 在 5.0 + 版本后删除。 false
Read Buffer Size 确定 Npgsql 在读取时使用的内部缓冲区的大小。如果从数据库传输大字节流值,增加会提高性能。 8192
Write Buffer Size 确定 Npgsql 在写入时使用的内部缓冲区的大小。如果将大字节流值传输到数据库,增加会提高性能。 8192
Socket Receive Buffer Size 确定套接字接收缓冲区的大小。 System-dependent
Socket Send Buffer Size 确定套接字发送缓冲区的大小。 System-dependent
No Reset On Close 在某些情况下,通过在返回池时不重置连接状态来提高性能,但以泄漏状态为代价。仅在基准测试显示性能改进时使用 false

7 故障转移和负载平衡参数

参数 描述 默认
Target Session Attributes 确定首选的 PostgreSQL 目标服务器类型。 PGTARGETSESSIONATTRS, Any
Load Balance Hosts 通过负载在多个主机之间实现平衡。 false
Host Recheck Seconds 控制主机的缓存状态将被视为有效的时间。 10

8 一些小甜点的参数

参数 描述 默认
Options 指定任何有效 PostgreSQL 连接选项, 被单个刻度包围。在 5.0 中引入。 PGOPTIONS
Application Name 要在连接启动期间发送到后端的可选应用程序名称参数。
Enlist 是否在环境 TransactionScope 中登记。 true
Search Path 设置Schema搜索路径。
Client Encoding 获取或设置 client_encoding 参数。 PGCLIENTENCODING
Encoding 获取或设置将用于编码/解码 PostgreSQL 字符串数据的 .NET 编码。 UTF8
Timezone 获取或设置会话时区。 PGTZ
EF Template Database 在实体框架中创建数据库时指定的数据库模板。 template1
EF Admin Database 在实体框架中创建和删除数据库时要指定的数据库管理员。 template1
Load Table Composites 加载表复合类型定义,而不仅仅是独立的复合类型。 错误的
Array Nullability Mode 配置作为对象实例请求时返回值类型数组的方式。可能的值为:Never(值类型的数组总是作为不可空数组返回)、Always(值类型的数组总是作为可空数组返回)和 PerInstance(返回的数组类型在运行时确定)。 Never

9 兼容模式

参数 描述 默认
Server Compatibility Mode 特殊 PostgreSQL 服务器类型的兼容模式。目前支持“Redshift”,以及“NoTypeLoading”,它将绕过来自 PostgreSQL 目录表的正常类型加载机制并支持基本类型的硬编码列表。 none

10 环境变量

除了链接字符串参数外,Npgsql还能识别环境变量。

环境变量 描述
PGUSER 行为同 链接参数 User
PGPASSWORD 行为同 链接参password
PGPASSFILE 行为同 链接参数passfile
PGSSLCERT 行为同 链接参数 sslcert
PGSSLKEY 行为同 链接参数 sslkey
PGSSLROOTCERT 行为同 链接参数 sslrootcert
PGCLIENTENCODING 行为同 链接参数 client_encoding
PGTZ 行为同 链接参数 default time zone.
PGOPTIONS 行为同 链接参数 options
  1. 小结

介绍这些参数很枯燥,有啥意义呢,不就时链接个数据库吗,搞这么复杂?

哎,谁遇到问题谁知道,这些参数个个救命,改天我再和你聊聊怎么解救了我的。

👓都看到这了,还在乎点个赞吗?

👓都点赞了,还在乎一个收藏吗?

👓都收藏了,还在乎一个评论吗?

本文转载自: 掘金

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

用 Long 做 Map 的 Key,存的对象花一下午才取出

发表于 2021-11-10


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

大家好,我是一航!

事情是这样!某天中午午休完,正在开始下午的搬砖任务,突然群里面热闹起来,由于忙,也就没有去看,过了一会儿,突然有伙伴在群里@我,就去爬楼看了一下大家的聊天记录,结果是发现了一个很有意思的Bug;看似很基础Map的取值问题,对于基础不是特别扎实的朋友来说,但如果真的遇到,可能会被坑惨,群里这位老弟就被坑了一下午,在这里分享给大家。

讨论的起因是一个老弟问了这样一个问题:

简单一句话表述就是:接口回了个Map,key是Long型的,Map中有数据,可取不到值;

由于基础数据类型的Key在以Json返回的时候,都被转成了String,有伙伴儿很快提出确认Key是不是被转成了String,结果都被否认了;但对于这个否认,我是持有怀疑态度的,所以,这里得必须亲自验证一下;

问题梳理

为了搞清楚状况,需要先简单的梳理一下;

  • 业务场景是这样:
    1. A服务提供了一个接口,返回了一个Map<Long , Object>
    2. B服务通过RestTemplate调用A服务对应的接口,入参就就是一个Long
    3. B服务通过得到Map<Long , Object>响应之后,再通过Long值作为Key,去得到Object
  • 问题点:至于这种接口设计方式是否合理,文末另说,这位老弟遇到的问题是:B服务能正常接收到Map<Long , Object>对象,也就是log.info("map:{}",map)都能正常输出对应的key和Object;但是通过map.get(sourceId)取Object,有时候正常,有时候取出来的null;这一下子就变的有意思了;程序员遇到Bug,只要是必现或者能百度到的,那都不算bug,轻轻松松拿下;唯独那种时而出现时而正常的bug,是最头疼的,可能让你一度怀疑人生;

复现Bug

为了能把这个问题点说清楚,按他的写法,我模拟了一下他的业务逻辑,写了一段简单代码复现一下正常情况和异常情况:

  • 能正常取值
    key为 Long l = 123456789000L;,代码如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
typescript复制代码@Slf4j
public class Main {
   public static void main(String[] args) throws Exception {
       //A服务的数据
       Map<Long,String> mp = new HashMap<>();
       Long l = 123456789000L;
       mp.put(l,"123");
       log.info("key:{}",l);

       // B服务通过网络请求得到A服务的响应文本
       String s1 = JSON.toJSONString(mp);
       log.info("json文本:{}",s1);

       // 将文本转换成Map对象
       Map<Long,String> mp2 = JSON.parseObject(s1,Map.class);
       log.info("json文本转换的Map对象:{}",mp2);

       // 通过key取值
       log.info("通过key:{}得到的值:{}",l,mp2.get(l));
  }
}

运行结果

  • 取值为null
    异常情况下唯一的区别是key换成了 Long l = 123456789L;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
typescript复制代码public class Main {
   public static void main(String[] args) throws Exception {
       //A服务的数据
       Map<Long,String> mp = new HashMap<>();
       Long l = 123456789L;
       mp.put(l,"123");

       // B服务通过网络请求得到A服务的响应文本
       String s1 = JSON.toJSONString(mp);
       log.info("json文本:{}",s1);

       // 将文本转换成Map对象
       Map<Long,String> mp2 = JSON.parseObject(s1,Map.class);
       log.info("json文本转换的Map对象:{}",mp2);

       // 通过key取值
       log.info("通过key:{}得到的值:{}",l,mp2.get(l));
  }
}

运行结果

结果分析

发现没有!两段代码,除了key不一样,逻辑部分没有任何区别,均无报错,且都能正常运行,那为何一段正常一段结果为null呢?

bug场景复现了,一切就别的简单多了,既然 mp2.get(l)取的值不同,问题点也肯定就出现在这个附近了,debug去分析一下mp2里面到底放了些啥:

好家伙!事出反常必有妖;

一看这两种情况下mp2对应key的类型(上图箭头部分),应该就明白,为什么key是 long l = 123456789l的时候,mp2取不到值了吧;因为转换后mp2里面存的压根儿就不是Long型的key,而是一个Integer的key?当Key是Long型的时候,就能正常取到值,当为Integer的时候,取出来的就是null

为什么变成了Integer

明明我存的是一个Long作为key,Json文本转mp2的时候我也是通过Map<Long,String>去接收,似乎一切都有理有据,为什么最后mp2的key一会儿是Integer,一会儿是Long呢?

毕竟核心代码只有这么简单的5行,稍作分析就能知道,问题点是出在这行代码

1
javascript复制代码Map<Long,String> mp2 = JSON.parseObject(s1,Map.class);

类型转换传递的对象仅仅是一个 Map.class;并没有指明Map中的key和value的具体类型是什么;因为泛型擦除,导致fastJson在遇到基础数字类型key的时候,无法判断其具体的类型,只能通过长度去匹配一个最合适的数据类型;由于 123456789可以使用Integer去接收,就将其转换成了Integer;而 123456789000就只能通过Long型接收,就转换成了Long型;

以下是fastJson源码中关于数字类型判断的一段代码;用来匹配当前的数字需要转换成什么类型逻辑判断:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
go复制代码        if (negative) {
           if (i > this.np + 1) {
               if (result >= -2147483648L && type != 76) {
                   if (type == 83) {
                       return (short)((int)result);
                  } else if (type == 66) {
                       return (byte)((int)result);
                  } else {
                       return (int)result;
                  }
              } else {
                   return result;
              }
          } else {
               throw new NumberFormatException(this.numberString());
          }
      } else {
           result = -result;
           if (result <= 2147483647L && type != 76) {
               if (type == 83) {
                   return (short)((int)result);
              } else if (type == 66) {
                   return (byte)((int)result);
              } else {
                   return (int)result;
              }
          } else {
               return result;
          }
      }

这样也就能明确解释这个bug所出现的原因了;

如何解决呢?

fastJson

如果单纯是通过fastJson将Json文本转对象,其实处理起来就很简单了,只需要指明一下Map中的key和value是什么类型的即可,代码如下

1
javascript复制代码Map<Long,String> mp2 = JSON.parseObject(s1,new TypeReference<Map<Long,String>>(){});

即使当key为 123456789的时候,依然能够造成获取到值

RestTemplate

本文的起因,是因为通过RestTemplate请求另外一个服务没有指明泛型对象造成的,因此也需要指明一下;

  • 示例接口
1
2
3
4
5
6
7
8
9
10
11
12
13
typescript复制代码@RestController
@RequestMapping("/a")
public class TestController {

   @GetMapping("/b")
   public Map<Long, String> b() {
       Map<Long, String> mp = new HashMap<>();
       mp.put(1L,"123");
       mp.put(123456789L,"456");
       mp.put(123456789000L,"789");
       return mp;
  }
}
  • restTemplate请求
1
2
3
4
5
6
7
8
9
10
11
12
13
dart复制代码@Autowired
RestTemplate restTemplate;

@Test
public void restTemplate() throws Exception {
   ParameterizedTypeReference<Map<Long, String>> typeRef = new ParameterizedTypeReference<Map<Long, String>>() {};
   Map<Long, String> mp = restTemplate.exchange("http://127.0.0.1:8080/a/b", HttpMethod.GET, new HttpEntity<>(null), typeRef).getBody();

   log.info("mp:{}", mp);
   log.info("获取key为:{} 的值:{}",1L,mp.get(1L));
   log.info("获取key为:{} 的值:{}",123456789L,mp.get(123456789L));
   log.info("获取key为:{} 的值:{}",123456789000L,mp.get(123456789000L));
}

思考

到这里,整个问题算是解决了!

但有另外一个点,也不得不说一下;这位老弟采用的是Map作为报文交互的对象,是非常不建议用的,通过Map,看似提高了灵活性,毕竟啥对象都可以扔进去,实则给代码的可读性、维护性带来了很大的障碍,因为我没有办法一眼看出这个Map中放了些什么数据,也不知道何时放了数据进去;如果我只是作为一个调用方,想去看一下你返回了些什么,仅仅通过接口定义,我是没办法清晰的看出,而是要深入阅读详细的代码,看你在Map中塞了些什么值,分别代表什么意思,才能加以明确。

而这一系列的问题,可能终将自己挖个深坑把自己给埋了

那么为了提高接口的灵活性、可阅读性以及可扩展性,基于泛型的接口报文数据抽象化是一个重要手段;将报文的Json格式分为公共部分和业务数据部分,让整个数据结构变的更加灵活,但又不失整体的规范,通过响应对象,一眼就能明确你要返回的数据;可参考以下简单示例:

1
2
3
4
5
6
7
8
json复制代码// 公共部分
{
"code":0,
"msg":"成功",
"data":{
// 业务数据
}
}

对应的代码:

1
2
3
4
5
6
7
8
kotlin复制代码@Data
public class BaseBean<T> {
   private Integer code;

   private String msg;

   private T data;
}

通过泛型,即可灵活表达任意响应

  • 用户
1
2
3
4
5
6
sql复制代码@GetMapping("/user")
public BaseBean<User> user() {
   // 这里去获取User
   BaseBean<User> user = new BaseBean<>();
   return user;
}
  • 商品
1
2
3
4
5
6
swift复制代码@GetMapping("/goods")
public BaseBean<Goods> goods() {
   // 这里去获取商品
   BaseBean<Goods> goods = new BaseBean<>();
   return user;
}

….

好了,今天就分享到这里,愿看到此文的朋友,今后,再无Bug!!!

本文转载自: 掘金

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

EasyRule实战 1简述 2EasyRule的支持环

发表于 2021-11-10

1.简述

Easy Rules是一个简单而强大的Java规则引擎,提供以下功能:

  • 轻量级框架和易于学习的API
  • 基于POJO的开发与注解的编程模型
  • 定义抽象的业务规则并轻松应用它们
  • 支持从简单规则创建组合规则的能力
  • 支持使用表达式语言(如MVEL和SpEL)定义规则的能力

2.EasyRule的支持环境

EasyRule支持jdk1.7以上的版本。
maven依赖

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
xml复制代码<!--easy rules核心库-->
<dependency>
<groupId>org.jeasy</groupId>
<artifactId>easy-rules-core</artifactId>
<version>4.1.0</version>
</dependency>

<!--规则定义文件格式,支持json,yaml等-->
<dependency>
<groupId>org.jeasy</groupId>
<artifactId>easy-rules-support</artifactId>
<version>4.1.0</version>
</dependency>

<!--支持mvel规则语法库-->
<dependency>
<groupId>org.jeasy</groupId>
<artifactId>easy-rules-mvel</artifactId>
<version>4.1.0</version>
</dependency>

3.定义规则引擎

Parameter Type Required Default content
skipOnFirstAppliedRule boolean no MaxInt 告诉引擎在规则失败时跳过后面的规则
skipOnFirstFailedRule boolean no MaxInt 告诉引擎一个规则不会被触发跳过后面的规则
skipOnFirstNonTriggeredRule boolean no MaxInt 告诉引擎一个规则不会被触发跳过后面的规则
rulePriorityThreshold boolean no MaxInt 告诉引擎如果优先级超过定义的阈值,则跳过下一个规则。版本3.3已经不支持更改,默认MaxInt

3.1 rulePriorityThreshold

在创建规则引擎时,我们将其属性 rulePriorityThreshold 的值设置为了 1,这样的设置后的效果相当于在定义的所有规则中将 priority > 1 的规则去掉,换种说法就是只考虑 priority的值小于等于 1 的规则。

3.2 skipOnFirstAppliedRule

所有规则按照优先级从高到低的顺序进行判断,当发现一个满足条件的规则并执行了相关操作后,便不再继续判断其他规则

3.3 skipOnFirstNonTriggeredRule

所有规则按照优先级从高到低的顺序进行判断,如果满足当前的规则,则执行相应的操作,直到遇到不满足条件的规则为止,并且也不会对其他规则进行判断了

1
2
3
4
5
6
scss复制代码RulesEngineParameters parameters = new RulesEngineParameters() 
.rulePriorityThreshold(10)
.skipOnFirstAppliedRule(true)
.skipOnFirstFailedRule(true)
.skipOnFirstNonTriggeredRule(true);
RulesEngine rulesEngine = new DefaultRulesEngine(parameters);

4.定义规则

4.1 使用rule定义

我们将创建一个始终被触发的规则,在执行时将“hello world”打印到控制台。规则如下:

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

@Rule(priority = 1)
public static class FizzRule {
@Condition
public boolean isFizz(@Fact("number") Integer number) {
return number % 5 == 0;
}

@Action
public void printFizz() {
System.out.print("fizz");
}
}

@Rule(priority = 2)
public static class BuzzRule {
@Condition
public boolean isBuzz(@Fact("number") Integer number) {
return number % 7 == 0;
}

@Action
public void printBuzz() {
System.out.print("buzz");
}
}

public static class FizzBuzzRule extends UnitRuleGroup {

public FizzBuzzRule(Object... rules) {
for (Object rule : rules) {
addRule(rule);
}
}

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

@Rule(priority = 3)
public static class NonFizzBuzzRule {

@Condition
public boolean isNotFizzNorBuzz(@Fact("number") Integer number) {
// can return true, because this is the latest rule to trigger according to
// assigned priorities
// and in which case, the number is not fizz nor buzz
return number % 5 != 0 || number % 7 != 0;
}

@Action
public void printInput(@Fact("number") Integer number) {
System.out.print(number);
}
}

}

@Condition注解标记计算规则条件的方法。此方法必须是公共的,可以有一个或多个用@Fact注解的参数,并返回布尔类型。只有一个方法能用@Condition注解。

@Action注解标记要执行规则操作的方法。规则可以有多个操作。可以使用order属性按指定的顺序执行操作。默认情况下,操作的顺序为0。

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 RuleClient {
public static void main(String[] args) {
// create a rules engine
RulesEngineParameters parameters = new RulesEngineParameters().skipOnFirstAppliedRule(true);
RulesEngine fizzBuzzEngine = new DefaultRulesEngine(parameters);

// create rules
Rules rules = new Rules();
rules.register(new FizzRule());
rules.register(new BuzzRule());
rules.register(new RuleClass.FizzBuzzRule(new RuleClass.FizzRule(), new RuleClass.BuzzRule()));
rules.register(new NonFizzBuzzRule());

// fire rules
Facts facts = new Facts();
for (int i = 1; i <= 100; i++) {
facts.put("number", i);
fizzBuzzEngine.fire(rules, facts);
System.out.println();
}
}

}

4.2 使用RuleBuilder API定义规则

1
2
3
4
5
6
7
8
scss复制代码Rule rule = new RuleBuilder()
.name("myRule")
.description("myRuleDescription")
.priority(3)
.when(condition)
.then(action1)
.then(action2)
.build();

4.3 yml文件定义

规则文件的文件名为 rules.yml ,其内容如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
yaml复制代码---
name: "three"
description: "print three"
priority: 0
condition: "number % 3 == 0"
actions:
- "System.out.println("three")"

---
name: "five"
description: "print five"
priority: 1
condition: "number % 5 == 0"
actions:
- "System.out.println("five")"

---
name: "seven"
description: "print seven"
priority: 2
condition: "number % 7 == 0"
actions:
- "System.out.println("seven")"

---
name: "itself"
description: "print the number itself otherwise"
priority: 3
condition: "true"
actions:
- "System.out.println(number)"

客户端调用

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

public static void main(String[] args) throws FileNotFoundException {
RulesEngine rulesEngine = new DefaultRulesEngine();
Rules rules = MVELRuleFactory.createRulesFrom(new FileReader("rules.yml"));

Facts facts = new Facts();
for (int i = 1; i <= 20; i++) {
System.out.println("====== " + i + " ======");
facts.put("number", i);
rulesEngine.fire(rules, facts);
}
}
}

4.4 MVELRule

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

public static void main(String[] args) {

//规则引擎
// RulesEngine rulesEngine = new DefaultRulesEngine();
DefaultRulesEngine rulesEngine = new DefaultRulesEngine();
rulesEngine.registerRuleListener(new MyRuleListener());

// ((DefaultRulesEngine) rulesEngine).registerRuleListener(new MyRuleListener());

//规则
MVELRule ageRule = new MVELRule()
.name("my rule")
.description("test demo rule")
.priority(1)
.when("user.age > 18")
.then("map.put('code',200);map.put('msg','success');myResult.setCode('200');myResult.setMsg('success');");

Rules rules = new Rules();
rules.register(ageRule);

Facts facts = new Facts();

User user = new User();
user.setAge(19);
facts.put("user", user);
Map<String, String> map = new HashMap();
MyResult myResult = new MyResult();
facts.put("map", map);
facts.put("myResult", myResult);

rulesEngine.fire(rules, facts);
System.out.println(map);
System.out.println(myResult);

}
}

5.RuleListener

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

@Override
public boolean beforeEvaluate(Rule rule, Facts facts) {
return true;
}

@Override
public void afterEvaluate(Rule rule, Facts facts, boolean b) {
System.out.println("---MyRuleListener------afterEvaluate-----");
}

@Override
public void beforeExecute(Rule rule, Facts facts) {
System.out.println("---MyRuleListener------beforeExecute-----");
}

@Override
public void onSuccess(Rule rule, Facts facts) {

System.out.println("---MyRuleListener------onSuccess-----");
}

@Override
public void onFailure(Rule rule, Facts facts, Exception e) {
System.out.println("---MyRuleListener------onFailure-----");
}

}
  • beforeEvaluate 该方法在执行@Condition修饰的方法之前执行。该方法返回false则不执行条件的判断,直接跳过该当前rule。
  • afterEvaluate 该方法在执行@Condition修饰的方法之后执行。
  • beforeExecute 该方法在执行@Action修饰的方法之前执行。
  • onSuccess 该方法在执行@Action修饰的方法之后执行。
  • onFailure 在执行@Action修饰的方法出现异常时,该方法执行。

在rulesEngine.fire(rules, facts);之前注册规则监听器

1
scss复制代码  ((DefaultRulesEngine) rulesEngine).registerRuleListener(new MyRuleListener());

6.RulesEngineListener

1
2
3
4
5
6
7
8
9
10
11
typescript复制代码public class MyRulesEngineListener implements RulesEngineListener {
@Override
public void beforeEvaluate(Rules rules, Facts facts) {
System.out.println("---MyRulesEngineListener------beforeEvaluate-----");
}

@Override
public void afterExecute(Rules rules, Facts facts) {
System.out.println("---MyRulesEngineListener------afterExecute-----");
}
}
  • beforeEvaluate 该方法在执行@Action修饰的方法之后执行。在RuleListener之前执行
  • afterExecute 该方法在执行@Condition修饰的方法之前执行。在RuleListener之后执行

在rulesEngine.fire(rules, facts);之前注册规则引擎监听器

1
scss复制代码((DefaultRulesEngine) rulesEngine).registerRulesEngineListener(new MyRulesEngineListener());

7.运行规则

1
2
3
4
5
6
7
8
9
10
scss复制代码// create facts
Facts facts = new Facts();

// create rules
Rules rules = new Rules();
rules.register(new HelloWorldRule());

// create a rules engine and fire rules on known facts
RulesEngine rulesEngine = new DefaultRulesEngine();
rulesEngine.fire(rules, facts);

本文转载自: 掘金

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

从JDK中学习设计模式——建造者模式

发表于 2021-11-10

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

概述

建造者模式(Builder Pattern)是一种复杂的创建型模式,它将一个复杂对象的构建与表示分离,使
同样的构建过程可以创建不同的表示。

建造者模式最主要的功能是基本方法调用顺序的安排,基本方法已经实现,顺序不同产生的对象也不同。例如:张三开了一家水壶制造厂,水壶有两道工序,打磨和喷漆,但都不是必须的,先打磨后喷漆和先喷漆后打磨,所生产出来的水壶是不同的。水壶制造厂就是具体建造者;定义打磨和喷漆两个工序就属于抽象构造者;水壶就是具体产品;打磨和喷漆的先后顺序就是指挥者负责的。

结构

建造者模式UML.png

  • Builder(抽象建造者):在该抽象类或接口中一般有两类方法,其中一类方法用于指定具体产品的构建,构建方法有一个或多个,它们用来指定具体产品的属性;另一类方法为具体产品的表现,有且仅有一个,这个方法用来返回具体的产品。
  • ConcreteBuilder(具体建造者):具体实现产品的构建和表现。
  • Product(具体产品):具体产品类。
  • Director(指挥者):负责安排产品的构建顺序,一般在其建造方法中调用建造者对象的构建方法和装配方法,完成复杂对象的建造。
  • Client(客户端):客户端确定具体建造者的类型,并实例化具体建造者对象(也可以通过配置文件和反射机制),然后通过指挥者类的构造函数或者 Setter 方法将该对象传入指挥者类中。

优点

  1. 客户端不必知道产品内部组成的细节,符合迪米特法则。
  2. 具体的建造者类之间是相互独立的,符合开闭原则,有利于系统的扩展。
  3. 可以逐步细化构建方法,便于控制细节风险。

缺点

如果产品内部变化复杂,就需要定义很多具体建造者类来实现这种变化,导致系统变得很庞大。

应用场景

产品的构建顺序不同会产生不同的结果时,使用建造者模式。

JDK 中的应用

java.lang.StringBuilder 就使用了建造者模式。

java.lang.Appendable 是抽象建造者。

1
2
3
4
5
6
7
8
java复制代码public interface Appendable {

Appendable append(CharSequence csq) throws IOException;

Appendable append(CharSequence csq, int start, int end) throws IOException;

Appendable append(char c) throws IOException;
}

java.lang.AbstractStringBuilder 是具体建造者。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
java复制代码abstract class AbstractStringBuilder implements Appendable, CharSequence {

@Override
public AbstractStringBuilder append(CharSequence s) {
...
}

@Override
public AbstractStringBuilder append(CharSequence s, int start, int end) {
...
}

@Override
public AbstractStringBuilder append(char c) {
...
}
}

java.lang.StringBuilder 是指挥者。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
java复制代码public final class StringBuilder
extends AbstractStringBuilder
implements java.io.Serializable, CharSequence
{

@Override
public StringBuilder append(Object obj) {
return append(String.valueOf(obj));
}

@Override
public StringBuilder append(String str) {
super.append(str);
return this;
}
...
}

本文转载自: 掘金

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

关于Spring-Retry的使用 1 Spring-Ret

发表于 2021-11-10

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

1 Spring-Retry的使用

1 Spring-Retry的简介

在日常的一些场景中, 很多需要进行重试的操作.而spring-retry是spring提供的一个基于spring的重试框架,非常简单好用.

2 Spring中的应用

1 导入maven坐标

1
2
3
4
5
xml复制代码 <dependency>
<groupId>org.springframework.retry</groupId>
<artifactId>spring-retry</artifactId>
<version>1.2.2.RELEASE</version>
</dependency>

2 添加被调用类

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

public static boolean retryMethod(Integer param) {
int i = new Random().nextInt(param);
log.info("随机生成的数:{}", i);

if (1 == i) {
log.info("为1,返回true.");
return true;
} else if (i < 1) {
log.info("小于1,抛出参数异常.");
throw new IllegalArgumentException("参数异常");
} else if (i > 1 && i < 10) {
log.info("大于1,小于10,抛出参数异常.");
return false;
} else {
//为其他
log.info("大于10,抛出自定义异常.");
throw new RemoteAccessException("大于10,抛出自定义异常");
}
}

}

3 添加测试类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
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
java复制代码@Slf4j
public class SpringRetryTest {

/**
* 重试间隔时间ms,默认1000ms
*/
private long fixedPeriodTime = 1000L;
/**
* 最大重试次数,默认为3
*/
private int maxRetryTimes = 3;
/**
* 表示哪些异常需要重试
* key一定要为Throwable异常的子类 Class<? extends Throwable>
* value为true表示需要重试
*/
private Map<Class<? extends Throwable>, Boolean> exceptionMap = new HashMap<>();


@Test
public void test() {

// 1 添加异常的处理结果 true为需要重试 false为不需要重试
exceptionMap.put(RemoteAccessException.class, true);

// 2 构建重试模板实例
RetryTemplate retryTemplate = new RetryTemplate();

// 3 设置重试回退操作策略 设置重试间隔时间
FixedBackOffPolicy backOffPolicy = new FixedBackOffPolicy();
backOffPolicy.setBackOffPeriod(fixedPeriodTime);

// 4 设置重试策略 设置重试次数 设置异常处理结果
SimpleRetryPolicy retryPolicy = new SimpleRetryPolicy(maxRetryTimes, exceptionMap);

//5 重试模板添加重试策略 添加回退操作策略
retryTemplate.setRetryPolicy(retryPolicy);
retryTemplate.setBackOffPolicy(backOffPolicy);

// 6 调用方法
Boolean resp = retryTemplate.execute(
// RetryCallback 重试回调方法
retryContext -> {
boolean result = RetryDemo.retryMethod(110);
log.info("方法返回结果= {}", result);
return result;
},
// RecoveryCallback 异常回调方法
retryContext -> {
//
log.info("超过最大重试次数或者抛出了未定义的异常!!!");
return false;
}
);

log.info("接口返回结果 = {}",resp);

}

}
/*
// 查看结果
[main] INFO com.cf.demo.SpringRetry.SpringRetryTest - 超过最大重试次数或者抛出了未定义的异常!!!
[main] INFO com.cf.demo.SpringRetry.SpringRetryTest - 接口返回结果 = false
*/

从代码的书写注解可以看到,RetryTemplate对象是Spring-Retry框架的重试执行者, 由它添加重试策略,回退操作策略等(注释第五步).RetryTemplate执行重试方法(注释第六步),通过execute方法, 传入的参数是重试回调逻辑对象RetryCallback 和执行操作结束的恢复对象RecoveryCallback. 且可以切换添加的异常种类, 得知,只有添加过相应的异常,才会触发重试操作,否则直接调用RecoveryCallback对象方法.

RetryTemplate的部分源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
java复制代码	/**
* Keep executing the callback until it either succeeds or the policy dictates that we
* stop, in which case the recovery callback will be executed.
*
* @see RetryOperations#execute(RetryCallback, RecoveryCallback)
* @param retryCallback the {@link RetryCallback}
* @param recoveryCallback the {@link RecoveryCallback}
* @throws TerminatedRetryException if the retry has been manually terminated by a
* listener.
*/
@Override
public final <T, E extends Throwable> T execute(RetryCallback<T, E> retryCallback,
RecoveryCallback<T> recoveryCallback) throws E {
return doExecute(retryCallback, recoveryCallback, null);
}

RetryTemplate添加重试策略源码:

1
2
3
4
5
6
7
8
java复制代码	/**
* Setter for {@link RetryPolicy}.
*
* @param retryPolicy the {@link RetryPolicy}
*/
public void setRetryPolicy(RetryPolicy retryPolicy) {
this.retryPolicy = retryPolicy;
}

RetryPolicy接口的实现类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
java复制代码AlwaysRetryPolicy:允许无限重试,直到成功,可能会导致死循环

CircuitBreakerRetryPolicy:有熔断功能的重试策略,需设置3个参数openTimeout、resetTimeout和delegate

CompositeRetryPolicy:组合重试策略,有两种组合方式,乐观组合重试策略是指只要有一个策略允许即可以重试,
悲观组合重试策略是指只要有一个策略不允许即可以重试,但不管哪种组合方式,组合中的每一个策略都会执行

ExceptionClassifierRetryPolicy:设置不同异常的重试策略,类似组合重试策略,区别在于这里只区分不同异常的重试

NeverRetryPolicy:只允许调用RetryCallback一次,不允许重试

SimpleRetryPolicy:固定次数重试策略,默认重试最大次数为3次,RetryTemplate默认使用的策略

TimeoutRetryPolicy:超时时间重试策略,默认超时时间为1秒,在指定的超时时间内允许重试

RetryTemplate添加回退策略源码:

1
2
3
4
5
6
7
8
java复制代码	/**
* Setter for {@link BackOffPolicy}.
*
* @param backOffPolicy the {@link BackOffPolicy}
*/
public void setBackOffPolicy(BackOffPolicy backOffPolicy) {
this.backOffPolicy = backOffPolicy;
}

BackOffPolicy的实现类:

1
2
3
4
5
6
7
8
9
java复制代码ExponentialBackOffPolicy:指数退避策略,需设置参数sleeper、initialInterval、maxInterval和multiplier,initialInterval指定初始休眠时间,默认100毫秒,maxInterval指定最大休眠时间,默认30秒,multiplier指定乘数,即下一次休眠时间为当前休眠时间*multiplier

ExponentialRandomBackOffPolicy:随机指数退避策略,引入随机乘数可以实现随机乘数回退

FixedBackOffPolicy:固定时间的退避策略,需设置参数sleeper和backOffPeriod,sleeper指定等待策略,默认是Thread.sleep,即线程休眠,backOffPeriod指定休眠时间,默认1秒

NoBackOffPolicy:无退避算法策略,每次重试时立即重试

UniformRandomBackOffPolicy:随机时间退避策略,需设置sleeper、minBackOffPeriod和maxBackOffPeriod,该策略在[minBackOffPeriod,maxBackOffPeriod之间取一个随机休眠时间,minBackOffPeriod默认500毫秒,maxBackOffPeriod默认1500毫秒

3 SpringBoot中的应用

1 导入maven坐标

1
2
3
4
5
6
7
8
9
10
11
java复制代码 <dependency>
<groupId>org.springframework.retry</groupId>
<artifactId>spring-retry</artifactId>
<version>1.2.2.RELEASE</version>
</dependency>

<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.9.1</version>
</dependency>

2 添加一个管理类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
java复制代码@Service
@Slf4j
public class SpringRetryDemo {


/**
* 重试所调用方法
* @return
*/
// delay=2000L表示延迟2秒 multiplier=2表示两倍 即第一次重试2秒后,第二次重试4秒后,第三次重试8秒后
@Retryable(value = {RemoteAccessException.class}, maxAttempts = 3, backoff = @Backoff(delay = 2000L, multiplier = 2))
public boolean call(Integer param) {
return RetryDemo.retryMethod(param);
}

/**
* 超过最大重试次数或抛出没有指定重试的异常
* @param e
* @param param
* @return
*/
@Recover
public boolean recover(Exception e, Integer param) {
log.info("请求参数为: ", param);
log.info("超过最大重试次数或抛出没有指定重试的异常, e = {} ", e.getMessage());
return false;
}

}

3 启动类上添加注解@EnableRetry

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

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

4 添加测试类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
java复制代码@RunWith(SpringRunner.class)
@SpringBootTest(classes = DemoApplication.class)
@Slf4j
public class DemoApplicationTests {

@Autowired
private SpringRetryDemo springRetryDemo;

@Test
public void testRetry() {
boolean result = springRetryDemo.call(110);
log.info("方法返回结果为: {}", result);
}
}
/* 运行结果:

随机生成的数:77
大于10,抛出自定义异常.
随机生成的数:23
大于10,抛出自定义异常.
随机生成的数:82
大于10,抛出自定义异常.
请求参数为:
超过最大重试次数或抛出没有指定重试的异常, e = 大于10,抛出自定义异常
方法返回结果为: false
*/

注解说明:

@Enableretry注解,启用重试功能(默认是否基于子类代理,默认是否, 即是基于Java接口代理)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
java复制代码@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@EnableAspectJAutoProxy(proxyTargetClass = false)
@Import(RetryConfiguration.class)
@Documented
public @interface EnableRetry {

/**
* Indicate whether subclass-based (CGLIB) proxies are to be created as opposed
* to standard Java interface-based proxies. The default is {@code false}.
*
* @return whether to proxy or not to proxy the class
*/
boolean proxyTargetClass() default false;

}

@Retryable注解, 标记的方法发生异常时会重试

  • value 指定发生的异常进行重试
  • include 与value一样,默认为空,当exclude同时为空时,所有异常都重试
  • exclude 指定异常不重试,默认为空,当include同时为空,所有异常都重试
  • maxAttemps 重试次数,默认3
  • backoff 重试补充机制 默认是@Backoff()注解
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
java复制代码@Target({ ElementType.METHOD, ElementType.TYPE })
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Retryable {

/**
* Retry interceptor bean name to be applied for retryable method. Is mutually
* exclusive with other attributes.
* @return the retry interceptor bean name
*/
String interceptor() default "";

/**
* Exception types that are retryable. Synonym for includes(). Defaults to empty (and
* if excludes is also empty all exceptions are retried).
* @return exception types to retry
*/
Class<? extends Throwable>[] value() default {};

/**
* Exception types that are retryable. Defaults to empty (and if excludes is also
* empty all exceptions are retried).
* @return exception types to retry
*/
Class<? extends Throwable>[] include() default {};

/**
* Exception types that are not retryable. Defaults to empty (and if includes is also
* empty all exceptions are retried).
* @return exception types to retry
*/
Class<? extends Throwable>[] exclude() default {};

/**
* A unique label for statistics reporting. If not provided the caller may choose to
* ignore it, or provide a default.
*
* @return the label for the statistics
*/
String label() default "";

/**
* Flag to say that the retry is stateful: i.e. exceptions are re-thrown, but the
* retry policy is applied with the same policy to subsequent invocations with the
* same arguments. If false then retryable exceptions are not re-thrown.
* @return true if retry is stateful, default false
*/
boolean stateful() default false;

/**
* @return the maximum number of attempts (including the first failure), defaults to 3
*/
int maxAttempts() default 3;

/**
* @return an expression evaluated to the maximum number of attempts (including the first failure), defaults to 3
* Overrides {@link #maxAttempts()}.
* @since 1.2
*/
String maxAttemptsExpression() default "";

/**
* Specify the backoff properties for retrying this operation. The default is a
* simple {@link Backoff} specification with no properties - see it's documentation
* for defaults.
* @return a backoff specification
*/
Backoff backoff() default @Backoff();

/**
* Specify an expression to be evaluated after the {@code SimpleRetryPolicy.canRetry()}
* returns true - can be used to conditionally suppress the retry. Only invoked after
* an exception is thrown. The root object for the evaluation is the last {@code Throwable}.
* Other beans in the context can be referenced.
* For example:
* <pre class=code>
* {@code "message.contains('you can retry this')"}.
* </pre>
* and
* <pre class=code>
* {@code "@someBean.shouldRetry(#root)"}.
* </pre>
* @return the expression.
* @since 1.2
*/
String exceptionExpression() default "";

}

@Backoff注解

  • delay 延迟多久后重试
  • multiplier 延迟的倍数
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
java复制代码@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Import(RetryConfiguration.class)
@Documented
public @interface Backoff {

/**
* Synonym for {@link #delay()}.
*
* @return the delay in milliseconds (default 1000)
*/
long value() default 1000;

/**
* A canonical backoff period. Used as an initial value in the exponential case, and
* as a minimum value in the uniform case.
* @return the initial or canonical backoff period in milliseconds (default 1000)
*/
long delay() default 0;

/**
* The maximimum wait (in milliseconds) between retries. If less than the
* {@link #delay()} then the default of
* {@value org.springframework.retry.backoff.ExponentialBackOffPolicy#DEFAULT_MAX_INTERVAL}
* is applied.
*
* @return the maximum delay between retries (default 0 = ignored)
*/
long maxDelay() default 0;

/**
* If positive, then used as a multiplier for generating the next delay for backoff.
*
* @return a multiplier to use to calculate the next backoff delay (default 0 =
* ignored)
*/
double multiplier() default 0;

/**
* An expression evaluating to the canonical backoff period. Used as an initial value
* in the exponential case, and as a minimum value in the uniform case.
* Overrides {@link #delay()}.
* @return the initial or canonical backoff period in milliseconds.
* @since 1.2
*/
String delayExpression() default "";

/**
<<<<<<< HEAD
* An expression evaluating to the maximum wait (in milliseconds) between retries.
* If less than the {@link #delay()} then ignored.
=======
* An expression evaluating to the maximimum wait (in milliseconds) between retries.
* If less than the {@link #delay()} then the default of
* {@value org.springframework.retry.backoff.ExponentialBackOffPolicy#DEFAULT_MAX_INTERVAL}
* is applied.
>>>>>>> Fix @Backoff JavaDocs - maxDelay
* Overrides {@link #maxDelay()}
*
* @return the maximum delay between retries (default 0 = ignored)
* @since 1.2
*/
String maxDelayExpression() default "";

/**
* Evaluates to a vaule used as a multiplier for generating the next delay for backoff.
* Overrides {@link #multiplier()}.
*
* @return a multiplier expression to use to calculate the next backoff delay (default 0 =
* ignored)
* @since 1.2
*/
String multiplierExpression() default "";

/**
* In the exponential case ({@link #multiplier()} &gt; 0) set this to true to have the
* backoff delays randomized, so that the maximum delay is multiplier times the
* previous delay and the distribution is uniform between the two values.
*
* @return the flag to signal randomization is required (default false)
*/
boolean random() default false;

}

@Recover注解

  • 当重试达到规定的次数后,被注解标记的方法将被调用, 可以在此方法中进行日志的记录等操作.(该方法的入参类型,返回值类型需要和重试方法保持一致)
1
2
3
4
5
6
java复制代码@Target({ ElementType.METHOD, ElementType.TYPE })
@Retention(RetentionPolicy.RUNTIME)
@Import(RetryConfiguration.class)
@Documented
public @interface Recover {
}

参考资料:

blog.csdn.net/zzzgd_666/a…

本文转载自: 掘金

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

【分布式事务系列】Seata服务端的注册和统一配置

发表于 2021-11-10

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

将配置上传到Nacos配置中心

下载Seata源码,在源码包中有一个script文件夹,目录结构如下:

  • client:存放客户端的SQL脚本,参数配置
  • config-center:各个配置中心参数导入脚本,config.txt包含server和client,它为通用参数文件
  • server:服务端数据库脚本及各个容器配置

进入config-server目录,包含config.txt和不同配置中心的目录,这个目录下存放shell脚本和py脚本,config.txt存放的是seata客户端和服务器的所有配置信息。

在config-center/nacos目录下,执行脚本命令如下:

1
sh复制代码sh nacos-config.sh -h 10.0.100.254 -p 8848 -g SEATA_GROUP

这个脚本的作用是把config.txt中的配置信息上传到Nacos配置中心,config.txt中提供的是默认配置,在实际使用时可以先修改该文件的内容,再执行上传操作,上传完成后根据实际情况在nacos控制台上修改相关参数。

Seata服务端修改配置加载位置

进入${SEATA_HOME}/conf目录,修改registry.conf文件中的config配置项,配置如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
conf复制代码registry {
type = "nacos"

nacos {
application = "seata-server"
serverAddr = "10.0.100.254:8848"
group = "SEATA_GROUP"
namespace = ""
cluster = "default"
username = ""
password = ""
}
}

config {
type = "file"

nacos {
serverAddr = "10.0.100.254:8848"
namespace = ""
group = "SEATA_GROUP"
username = ""
password = ""
dataId = "seataServer.properties"
}
}

完成Seata服务端的注册和统一配置的管理。

Spring Cloud集成Seata 实现分布式事务

通过一个电商平台的购买逻辑,基于Spring Cloud环境集成Seata 实现分布式事务。

在整个业务流程中,涉及到三个服务,分别如下:

  1. 订单服务:创建订单
  2. 账户服务:从账户中扣除余额
  3. 库存服务:扣减指定商品的库存服务

用户执行下单请求时,会调用下单业务的REST接口,接口会分别调用库存服务和订单服务,订单服务还会调用账户服务先进行资金冻结,整个流程涉及到这三个服务的分布式事务问题。

本文转载自: 掘金

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

还让业务操作记录影响接口性能?

发表于 2021-11-10

一、前言

话说操作记录是每个业务系统必备组件!相信同在互联网打工的小伙伴肯定都在业务开发中记过操作记录吧!其实操作记录是一个比较简单的东西,涉及到修改的接口都要有,虽然简单,但也重要呀,平时感觉没什么用处,但是关键时候可能是一个重要的信息来源!

所以小伙伴们在开发中要重视记操作记录哦!

咱作为一名有工匠精神的攻城狮,细节这块那必须拿捏呀😎

虽说操作记录是必须的,耦合在我们的业务逻辑里面,但我们绝对不允许因操作记录而影响我们的接口性能。 都耦合在业务逻辑里面了,怎么不影响接口响应时间呢? 异步记录呀!!!

所以本文的重点就是带大家实现异步记操作记录,如果对操作记录不熟悉的小伙伴可以私信我。

二、思路

说到异步大家肯定就有思路了。接下来,先给大家上个图,大家第一反应可能是——这不就是MQ嘛。没错,这个思路跟MQ的实现有异曲同工之妙,可以说是基础版的MQ了。

虽说这个图一看的就懂思路了,但是看我画图这么辛苦的份上,听我掰扯掰扯文字版的思路哈哈,顺便给大家上一波实战代码😎

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
csharp复制代码/**
* 执行操作记录线程
*/
@Slf4j
public class OperationLogHandler implements Runnable{

// 操作存放队列++++++
private static ConcurrentLinkedQueue<OperationRecord> holderList = new ConcurrentLinkedQueue();

private OperationRecordService recordService = SpringBeanUtil.getBeanByClass(OperationRecordService.class);

@Override
public void run() {
for (;;){
Iterator<OperationRecord> iterator = holderList.iterator();
// 为空时 休眠
while (iterator.hasNext()){
OperationRecord record = iterator.next();
if (record == null) {
iterator.remove();
continue;
}
// 执行落库操作
boolean saveRet = recordService.save(record);
log.info("异步保存用户操作日志结果为:{}",saveRet);
iterator.remove();
}
}
}

/**
* 添加记录到队列(具体业务逻辑调用)
* @param operationRecords
*/
public static void addOpLogs(List<OperationRecord> operationRecords){
if (CollectionUtil.isEmpty(operationRecords)){
return;
}
boolean addRet = holderList.addAll(operationRecords);
log.info("添加操作日志到链表,结果:{}",addRet);
}
}

使用ConcurrentLinkedQueue这个线程安全的队列的原因是防止业务并发操作引发的线程安全问题,消费端因同时只有一个线程工作,所以没有线程安全问题。

接下去就是在服务启动的时候把线程启动

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
typescript复制代码@Component
public class SyncOperationLogListener implements ApplicationListener<ContextRefreshedEvent> {

@Override
public void onApplicationEvent(ContextRefreshedEvent event) {
run();
}

public void run(){
// 使用线程池方式启动
ThreadPoolUtil.getThreadPoolExecutor()
.execute(new OperationLogHandler());
System.out.println("=== operationLog handler start success ===");
}
}

到这我们就把设计思路和实现说完啦,希望可以大家有所帮助。

三、总结

还是不得不强调操作记录的重要性,还有打日志,可帮助我们再关键的时候查问题,可以快速定位问题的位置。 虽说咱们写的代码,近看密密麻麻一片,站远了看,满屏幕只有两个字,自信!所以作为一名有工匠精神的攻城狮,细节这块那必须拿捏呀😎

加油打工人!奥利给😎

我是rose,感谢各位的观看,各位的点赞就是rose输出的最大动力,我们下篇文章见!

注:如果本篇博客有任何错误和建议,欢迎小伙伴们留言,如果对操作记录不熟悉的小伙伴欢迎私信我哦!

本文转载自: 掘金

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

1…376377378…956

开发者博客

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