青花小记

这个人很懒,只想写写代码做做饭

0%

SpringBoot的项目在使用内置的Tomcat部署时,需要定义相关的Tomcat参数,本文主要讲述需要配置哪些参数。

1
2
3
4
5
6
7
8
9
server:
tomcat:
accesslog:
enabled: false #打开tomcat访问日志
directory: logs # 访问日志所在的目录
accept-count: #允许HTTP请求缓存到请求队列的最大个数,默认不限制
max-connections: #最大连接数,默认不设置
max-http-post-size: #HTTP POST内容最大长度,默认不限制
max-threads: #最大工作线程数

这些参数最终在ServerProperties.Tomcat类中体现

Tomcat是一个静态内部类,其中包含了一下属性

  • maxThreads 最大工作线程数
  • minSpareThreads 最小工作线程数
  • maxHttpPostSize HTTP POST内容最大长度
  • internalProxies 受信任IP校验正则表达式
  • protocolHeader 协议头,通常设置为X-Forwarded-Proto
  • protocolHeaderHttpsValue 协议头的内容,判断是否使用了SSL,默认值是https
  • portHeader 用于覆盖原始端口值的HTTP头的名称,默认为X-Forwarded-Port
  • redirectContextRoot 对上下文根的请求是否应该通过附加**/**到路径来重定向
  • useRelativeRedirects 设置通过调用sendRedirect生成的HTTP 1.1和后面的位置头是使用相对重定向还是使用绝对重定向
  • remoteIpHeader 提取远程IP的HTTP头的名称。例如X-FORWARDED-FOR
  • maxConnections 最大连接数,如果一旦连接数到达,剩下的连接将会保存到请求缓存队列里,也就是accept-count指定队列
  • maxHttpHeaderSize HTTP消息头的最大值(以字节为单位)
  • acceptCount 当所有可能的请求处理线程都在使用时,传入连接请求的最大队列长度

Tomcat拥有一个静态内部类Accesslog,主要用于配置Tomcat访问日志的参数信息

  • enabled 是否启用访问日志
  • pattern 访问日志的格式化模式,默认为common
  • directory 创建日志文件的目录。可以是绝对的或相对于Tomcat的基目录,默认是logs
  • prefix 日志文件名称前缀,默认为access_log
  • suffix 日志文件名称后缀,默认为.log
  • rotate 是否启用访问日志旋转,默认为true
  • renameOnRotate 是否推迟将日期戳包含在文件名中直到旋转时间。
  • fileDateFormat 日志文件名称中的日期格式,默认为**.yyyy-MM-dd**。
  • requestAttributesEnabled 为请求使用的IP地址、主机名、协议和端口设置请求属性。
  • buffered 是否缓冲输出,使其只定期刷新,默认为true

Tomcat还有名为Resource的静态内部类,主要用于配置静态资源缓存的生存周期

SpringBoot的默认端口是8080,如果想要修改端口信息,可以通过以下两种方式

配置文件修改

在application.properties或application.yml文件中修改server.port

修改application.properties

1
server.port=8090

修改application.yml

1
2
server:
port: 8090

在命令行中指定启动端口

比如传入参数–server.port=8090

1
java -jar sprite.jar --server.port=8090

或者传入虚拟机系统属性

1
java -Dserver.port=8090 -jar sprite.jar

特性ACID

  • 原子性 Atomicity
  • 一致性 Consistency
  • 隔离性 Isolation
  • 持久性 Durability

原子性 Atomicity

原子性是指事务包含的所有操作要么全部成功,要么全部失败回滚,事务的各个元素是不可分的,事务是一个完整操作。

一致性 Consistency

一致性是指事务必须使数据库从一个一致性状态变换到另一个一致性状态,也就是说一个事务执行之前和执行之后都必须处于一致性状态。

拿转账来说,假设用户A和用户B两者的钱加起来一共是5000,那么不管A和B之间如何转账,转几次账,事务结束后两个用户的钱相加起来应该还得是5000,这就是事务的一致性。

隔离性 Isolation

隔离性是当多个用户并发访问数据库时,比如操作同一张表时,数据库为每一个用户开启的事务,不能被其他事务的操作所干扰,多个并发事务之间要相互隔离。

即要达到这么一种效果:对于任意两个并发的事务T1和T2,在事务T1看来,T2要么在T1开始之前就已经结束,要么在T1结束之后才开始,这样每个事务都感觉不到有其他事务在并发地执行。

持久性 Durability

持久性是指一个事务一旦被提交了,那么对数据库中的数据的改变就是永久性的,即便是在数据库系统遇到故障的情况下也不会丢失提交事务的操作。

例如我们在使用JDBC操作数据库时,在提交事务方法后,提示用户事务操作完成,当我们程序执行完成直到看到提示后,就可以认定事务以及正确提交,即使这时候数据库出现了问题,也必须要将我们的事务完全执行完成,否则就会造成我们看到提示事务处理完毕,但是数据库因为故障而没有执行事务的重大错误。

事务的隔离性

问题

如果不考虑事务的隔离性,会发生几种问题

脏读

脏读是指在一个事务处理过程里读取了另一个未提交的事务中的数据。

当一个事务正在多次修改某个数据,而在这个事务中这多次的修改都还未提交,这时一个并发的事务来访问该数据,就会造成两个事务得到的数据不一致。例如:用户A向用户B转账100元,对应SQL命令如下

1
2
3
update account set money = money + 100 where name = 'B';  (此时A通知B)

update account set money = money - 100 where name = 'A';

当只执行第一条SQL时,A通知B查看账户,B发现确实钱已到账(此时即发生了脏读),而之后无论第二条SQL是否执行,只要该事务不提交,则所有操作都将回滚,那么当B以后再次查看账户时就会发现钱其实并没有转。

不可重复读

不可重复读是指在对于数据库中的某个数据,一个事务范围内多次查询却返回了不同的数据值,这是由于在查询间隔,被另一个事务修改并提交了。

例如事务T1在读取某一数据,而事务T2立马修改了这个数据并且提交事务给数据库,事务T1再次读取该数据就得到了不同的结果,发送了不可重复读。

不可重复读和脏读的区别是,脏读是某一事务读取了另一个事务未提交的脏数据,而不可重复读则是读取了前一事务提交的数据。

在某些情况下,不可重复读并不是问题,比如我们多次查询某个数据当然以最后查询得到的结果为主。但在另一些情况下就有可能发生问题,例如对于同一个数据A和B依次查询就可能不同,A和B就可能打起来了……

幻读

幻读是事务非独立执行时发生的一种现象。例如事务T1对一个表中所有的行的某个数据项做了从“1”修改为“2”的操作,这时事务T2又对这个表中插入了一行数据项,而这个数据项的数值还是为“1”并且提交给数据库。而操作事务T1的用户如果再查看刚刚修改的数据,会发现还有一行没有修改,其实这行是从事务T2中添加的,就好像产生幻觉一样,这就是发生了幻读。

幻读和不可重复读都是读取了另一条已经提交的事务(这点就脏读不同),所不同的是不可重复读查询的都是同一个数据项,而幻读针对的是一批数据整体(比如数据的个数)。

事务隔离级别

Serializable 串行化

可避免脏读、不可重复读、幻读的发生。

Repeatable read 可重复读

可避免脏读、不可重复读的发生。

Read committed 读已提交

可避免脏读的发生。

Read uncommitted 读未提交

最低级别,任何情况都无法保证。

说明

以上四种隔离级别最高的是Serializable级别,最低的是Read uncommitted级别,当然级别越高,执行效率就越低。像Serializable这样的级别,就是以锁表的方式(类似于Java多线程中的锁)使得其他的线程只能在锁外等待,所以平时选用何种隔离级别应该根据实际情况。**在MySQL数据库中默认的隔离级别为Repeatable read (可重复读)**。

在MySQL数据库中,支持上面四种隔离级别,默认的为Repeatable read (可重复读);而在Oracle数据库中,只支持Serializable (串行化)级别和Read committed (读已提交)这两种级别,其中默认的为Read committed级别。

MySQL 查询缓存存储客户端请求的查询结果信息( SELECT 语句)。MySQL 服务器在接收到相同的请求时,会直接将查询缓存中的结果返回给客户端,不再去数据库中重新查询。而且,查询缓存是在多个会话中共享的,一个客户端的查询缓存结果可以被另一个客户端的相同请求复用,从而加快了查询效率。

需要注意的是,查询缓存在 MySQL5.7.20 版本已过时,在 MySQL8.0 版本中被移除,所以使用时需要注意 MySQL 服务器的版本信息。

查询缓存配置

查询缓存的数据库支持

可以使用 SHOW VARIABLES LIKE ‘have_query_cache’ 指令查询当前的 MySQL 数据库是否支持查询缓存:

1
2
3
4
5
6
7
8
mysql> SHOW VARIABLES LIKE 'have_query_cache';
+------------------+-------+
| Variable_name | Value |
+------------------+-------+
| have_query_cache | YES |
+------------------+-------+
1 row in set

具体配置

也可以通过 SHOW VARIABLES LIKE ‘%query_cache%’ 来查看查询缓存的详细配置信息:

1
2
3
4
5
6
7
8
9
10
11
mysql> SHOW VARIABLES LIKE '%query_cache%';
+------------------------------+---------+
| Variable_name | Value |
+------------------------------+---------+
| have_query_cache | YES |
| query_cache_limit | 1048576 |
| query_cache_min_res_unit | 4096 |
| query_cache_size | 0 |
| query_cache_type | ON |
| query_cache_wlock_invalidate | OFF |
+------------------------------+---------+
query_cache_limit

指定单个查询能够使用的缓冲区的大小,默认值是1M大小

query_cache_min_res_unit

查询缓存分配的最小块的大小,默认大小是 4KB。

MySQL 用于查询的缓存的内存被分成一个个变长数据块,用来存储类型,大小,数据等信息。当服务器启动的时候,会初始化缓存需要的内存。当查询结果需要缓存的时候,先从空闲块中申请一个数据块大于参数 query_cache_min_res_unit 的配置,即使缓存数据很小,申请数据块也是这个,因为查询开始返回结果的时候就分配空间,此时无法预知结果多大。分配内存块需要先锁住空间块,所以操作很慢,MySQL 会尽量避免这个操作,选择尽可能小的内存块,如果不够,继续申请,如果存储完时有多余的空间,则多余的空间将会被释放。

query_cache_size

查询缓存可用的内存大小,默认情况下是 0,表示无法使用查询缓存。

查询缓存的内存可以手动配置,最小值不能小于 40K,具体的值需要参考系统的数据量和应用场景。如果设置的值太小,则设置不生效。

1
2
3
4
5
6
7
8
9
mysql> SET GLOBAL query_cache_size  = 40000;
Query OK, 0 rows affected

mysql> SHOW VARIABLES LIKE 'query_cache_size';
+------------------+-------+
| Variable_name | Value |
+------------------+-------+
| query_cache_size | 0 |
+------------------+-------+

官方文档中说明,该情况下,MySQL 服务器还会返回一个警告,我本地的 MySQL 版本是 5.0.96-community-nt 的,并没有返回该警告。如果想看警告效果的话,可以去官网进行查看。

query_cache_size 设置的值,如果不是 1024byte 的整数倍,将自动转化为最靠近 1024byte 的值。

1
2
3
4
5
6
7
8
9
10
mysql>  SET GLOBAL query_cache_size = 1000000;
Query OK, 0 rows affected

mysql> SHOW VARIABLES LIKE 'query_cache_size';
+------------------+--------+
| Variable_name | Value |
+------------------+--------+
| query_cache_size | 999424 |
+------------------+--------+
1 row in set
query_cache_type

当 query_cache_size 大于 0 的时候,查询缓存的应用将受到 query_cache_type 的影响。

  • 0 表示不使用查询缓存

  • 1 表示使用使用查询缓存;如果不想用查询缓存的话,可以使用 SELECT SQL_NO_CACHE 进行查询操作

  • 2 表示按需使用查询缓存,SQL 语句以 SELECT SQL_CACHE 开头表示使用查询缓存

当 query_cache_size 为 0 时,应该同步将 query_cache_type 设置为 0,这样的话 MySQL 服务在执行查询语句的时候讲不再检查查询互斥锁,从而可以提高查询的效率。

query_cache_type 是全局生效的,所有连接到 MySQL 服务的客户端都会共用这个配置,如果某个客户端想设置自己的 query_cache_type 属性,可以通过以下指令实现。

1
mysql> SET SESSION query_cache_type = OFF;

在启动时也可以设置 query_cache_type 的值,但是只能设置数字(0,1,2),不能再使用 ON 和 OFF。

状态监控

通过 SHOW STATUS LIKE ‘Qcache%’ 命令可以查询查询缓存当前的情况

1
2
3
4
5
6
7
8
9
10
11
12
13
mysql> SHOW STATUS LIKE 'Qcache%';
+-------------------------+--------+
| Variable_name | Value |
+-------------------------+--------+
| Qcache_free_blocks | 1 |
| Qcache_free_memory | 990760 |
| Qcache_hits | 0 |
| Qcache_inserts | 0 |
| Qcache_lowmem_prunes | 0 |
| Qcache_not_cached | 2 |
| Qcache_queries_in_cache | 0 |
| Qcache_total_blocks | 1 |
+-------------------------+--------+

应用场景

查询缓存适用于进行大量重复的查询操作、并且改动量小的表。

如果数据库表中数据和结构发生变化时(增删改、表结构调整),查询缓存将会失效并被清除。

缓存清理

可以使用以下对查询缓存进行清理操作

  • FLUSH QUERY CACHE 清空查询缓存中的碎片

  • RESET QUERY CACHE 从查询缓存中移除所有查询

  • FLUSH TABLES 关闭所有打开的表,该操作将同步清空查询缓存中的内容

读取application.properties文件中的配置

在 application.properties 文件中加入项目的标题、版本、作者等信息

1
2
3
project.title=sample
project.version=v1.0.0-SNAPSHOT
project.author=lemon

@Value注解

新建一个配置类,存储项目的配置信息

1
2
3
4
5
6
7
8
9
10
11
12
13
@Component
@Data
public class ProjectConfig {

@Value("${project.title}")
private String title;

@Value("${project.version}")
private String version;

@Value("${project.author}")
private String author;
}

声明配置的前缀(@ConfigurationProperties)

也可以指定配置的前缀,这样的话就不需要使用 @Value 注解了

1
2
3
4
5
6
7
8
9
@Component
@Data
@ConfigurationProperties(prefix = "project")
public class ProjectConfig {

private String title;
private String version;
private String author;
}

使用Environment

在需要读取配置的地方注入 Environment,通过 Environmen t的 getProperty() 方法获取配置信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@RestController
public class ProjectController {

@Autowired
private Environment env;

/**
* 获取标题
* @return
*/
@RequestMapping(value = "title")
public String title() {
return env.getProperty("project.title");
}
}

读取自定义的配置文件

自定义配置文件 server.properties,放在 resources/config 目录下

1
server.ip=127.0.0.1

创建Bean,指定读取的配置文件

1
2
3
4
5
6
7
8
@PropertySource("classpath:/config/server.properties")
@ConfigurationProperties(prefix = "server")
@Component
@Data
public class ServerConfig {
private String ip;
}

由此,可以读取到自定义的配置文件中的配置。

最近一段时间 OpenSSH 爆发了远程访问漏洞,需要升级现有服务器的OpenSSH版本,目前官网的最新版本为7.7

查看现有版本

1
2
# ssh -V
OpenSSH_5.3p1, OpenSSL 1.0.1e-fips 11 Feb 2013

安装Telnet服务

安装 telnet 是为了防止卸载 OpenSSH 之后无法远程连接服务器

安装服务

1
# yum -y install telnet-server* telnet

修改配置

1
# vim /etc/xinetd.d/telnet

将其中 disable 字段的 yes 改为 no,允许 root 用户通过 telnet 登录(升级之后再修改回来)

启动服务

1
2
# service xinetd start
Starting xinetd: [ OK ]

设置开机启动

1
# chkconfig xinetd on

卸载原有OpenSSH

备份原有文件

1
2
# mv /etc/securetty /etc/securetty.old
# mv /etc/init.d/sshd /etc/init.d/sshd.old

卸载

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# rpm -qa | grep openssh
openssh-clients-5.3p1-123.el6_9.x86_64
openssh-5.3p1-123.el6_9.x86_64
openssh-server-5.3p1-123.el6_9.x86_64

# rpm -e --nodeps openssh-5.3p1-123.el6_9.x86_64
warning: erase unlink of /etc/ssh/moduli failed: No such file or directory
warning: erase unlink of /etc/ssh failed: No such file or directory

# rpm -e --nodeps openssh-server-5.3p1-123.el6_9.x86_64
error reading information on service sshd: No such file or directory
error: %preun(openssh-server-5.3p1-123.el6_9.x86_64) scriptlet failed, exit status 1

# rpm -e --noscripts openssh-server-5.3p1-123.el6_9.x86_64
warning: erase unlink of /etc/ssh/sshd_config failed: No such file or directory
warning: erase unlink of /etc/rc.d/init.d/sshd failed: No such file or directory

# rpm -e --nodeps openssh-clients-5.3p1-123.el6_9.x86_64
warning: erase unlink of /etc/ssh/ssh_config failed: No such file or directory
# rpm -qa | grep openssh

安装新版本OpenSSH

依赖安装

1
# yum install -y gcc openssl-devel pam-devel rpm-build

下载源文件

1
# wget http://ftp.openbsd.org/pub/OpenBSD/OpenSSH/portable/openssh-7.3p1.tar.gz

解压

1
# tar -zxvf openssh-7.3p1.tar.gz 

进入主目录

1
# cd openssh-7.3p1

配置相关参数

1
# ./configure --prefix=/usr --sysconfdir=/etc/ssh --with-md5-passwords --with-pam --with-zlib --with-openssl-includes=/usr --with-privsep-path=/var/lib/sshd 

编译安装

1
2
# make
# make install

修改配置

默认情况下,root 用户禁止远程登录,需要修改 /etc/ssh/sshd_config 文件,将 PermitRootLogin 参数改为 yes,如果没有该配置,则在文件末尾追加

1
# vim /etc/ssh/sshd_config

添加服务并启动

1
2
3
4
5
6
7
# cp -p contrib/redhat/sshd.init /etc/init.d/sshd
# chmod +x /etc/init.d/sshd
# chkconfig --add sshd
# chkconfig sshd on
# chkconfig --list sshd
sshd 0:off 1:off 2:on 3:on 4:on 5:on 6:off
# service sshd restart

检查版本

1
2
# ssh -V
OpenSSH_7.3p1, OpenSSL 1.0.1e-fips 11 Feb 2013

至此,OepnSSH 升级完毕!

别忘了关闭 telnet 的 root 远程访问权限

元注解

在 Spring 中,我们经常能够看到各种各样的注解,Java 自身也定义了很多的注解,这些注解的添加能够让程序员明确的知道这个类的状态。

Java中比较常见的注解类:

  • @Override 重写父类的方法
  • @Deprecated 标记过时,不建议再使用
  • @SuppressWarnings 消除警告

Java中的注解类,都使用 @interface 标记

1
2
3
4
5
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.SOURCE)
public @interface Override {

}

在 Override 注解类中,可以看到有两个注解 @Targe t和 @Retention 对 Override 进行了修饰,这些用来修饰注解类的注解称为元注解,元注解是注解的注解。

Java 中的元注解主要有以下几种

  • Target
  • Retention
  • Documented
  • Inherited

Target

1
2
3
4
5
6
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.ANNOTATION_TYPE)
public @interface Target {
ElementType[] value();
}

可以看到,Target 有一个属性 value,类型是 ElementType 数组,ElementType 是一个枚举类型,主要用来标记被@Target修饰的注解类可应用在什么位置上

枚举值为

  • ANNOTATION_TYPE 可应用在注解类上
  • CONSTRUCTOR 构造器
  • FIELD 成员变量
  • LOCAL_VARIABLE 局部变量
  • METHOD 方法
  • PACKAGE 包
  • PARAMETER 参数
  • TYPE 类、接口以及枚举

JDK1.8 之后新增了两个枚举属性

  • TYPE_PARAMETER 类型参数声明
  • TYPE_USE 使用的类型

Retention

1
2
3
4
5
6
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.ANNOTATION_TYPE)
public @interface Retention {
RetentionPolicy value();
}

Retention 注解有一个属性 value,是 RetentionPolicy 类型的,RetentionPolicy 是一个枚举类型,这个枚举决定了 Retention 注解的保留机制

RetentionPolicy 有3个值:

  • CLASS 表示当程序编译时注解的信息被保留在 class 文件(字节码文件)中,但在运行的时候不会被虚拟机读取
  • RUNTIME 表示当程序编译时注解的信息被保留在 class 文件(字节码文件)中,运行时也会被虚拟机读取
  • SOURCE 表示注解的信息会被编译器抛弃,不会留在 class 文件中,注解的信息只会留在源文件中

Documented

1
2
3
4
5
6
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.ANNOTATION_TYPE)
public @interface Documented {

}

@Documented 元注解用于声明被该注解修饰的注解类是可以写入 JavaDoc 中的

Inherited

1
2
3
4
5
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.ANNOTATION_TYPE)
public @interface Inherited {
}

@Inherited 用于指定某个注解用于父类时是否能够被子类继承

@Inherited 使用的较少,稍微理解即可

自定义注解

下面以日志注解为例介绍如何自定义注解。

首先需定义注解类 MyLog

1
2
3
4
5
6
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface MyLog {

}

@Target(ElementType.METHOD) 表明该注解只能在方法上使用

@Retention(RetentionPolicy.RUNTIME) 表明该注解会在程序编译时放到 class 文件(字节码文件)中,运行时也会被虚拟机读取

@Documented 声明被该注解修饰的注解类是可以写入 JavaDoc 中

@Target 和 @Retention 是必须的,@Documented 根据自己的需要进行添加

此时,只是定义了MyLog注解,但是并不是加了该注解就起作用的,还需要配置AOP,使之与注解配合,达到日志记录的作用

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
@Aspect
@Component
@Slf4j
public class MyLogAspect {

@Pointcut("@annotation(com.example.demo.annotation.MyLog)")
public void myLog() {

}

/**
* 前置通知【在连接点前面执行,前置通知不会影响连接点的执行,除非此处抛出异常】
* @param joinPoint
*/
@Before("myLog()")
public void before(JoinPoint joinPoint) {
if (log.isDebugEnabled()) {
log.debug("MyLog日志注解 - 前置通知【在连接点前面执行,前置通知不会影响连接点的执行,除非此处抛出异常】");
}
}

/**
* 环绕通知【环绕通知围绕在连接点前后,比如一个方法调用的前后】
* 环绕通知还需要负责决定是继续处理join point(调用ProceedingJoinPoint的proceed方法)还是中断执行
* @param joinPoint
*/
@Around("myLog()")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable{
if (log.isDebugEnabled()) {
log.debug("MyLog日志注解 - 环绕通知【环绕通知围绕在连接点前后,比如一个方法调用的前后】");
HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
//获取请求相关信息
log.debug("URL ------> " + request.getRequestURL().toString());
log.debug("METHOD ----> " + request.getMethod());
log.debug("ContentType ----->" + request.getContentType());
log.debug("Parameters ----->" + request.getParameterMap());

//获取切入点的相关信息
log.debug("className ------> " + joinPoint.getTarget().getClass().getName());
log.debug("method ----> " + joinPoint.getSignature().getName());
log.debug("params ----->" + Arrays.toString(joinPoint.getArgs()));
}
//DO SOMETHING
return joinPoint.proceed();
}

/**
* 正常返回通知【在连接点正常执行完成后执行,如果连接点抛出异常,则不会执行】
* @param joinPoint
*/
@AfterReturning("myLog()")
public void afterReturning(JoinPoint joinPoint) {
if (log.isDebugEnabled()) {
log.debug("MyLog日志注解 - 正常返回通知【在连接点正常执行完成后执行,如果连接点抛出异常,则不会执行】");
}
}

/**
* 异常返回通知【在连接点抛出异常后执行】
* @param joinPoint
*/
@AfterThrowing("myLog()")
public void afterThrowing(JoinPoint joinPoint) {
if (log.isDebugEnabled()) {
log.debug("MyLog日志注解 - 异常返回通知【在连接点抛出异常后执行】");
}
}

/**
* 返回通知【在连接点执行完成后执行,不管是正常执行完成,还是抛出异常,都会执行返回通知中的内容】
* @param joinPoint
*/
@After("myLog()")
public void after(JoinPoint joinPoint) {
if (log.isDebugEnabled()) {
log.debug("MyLog日志注解 - 返回通知【在连接点执行完成后执行,不管是正常执行完成,还是抛出异常,都会执行返回通知中的内容】");
}
}
}

正常情况下,各个通知的执行顺序如下:

  • 环绕通知
  • 前置通知
  • 返回通知
  • 正常返回通知

在发生异常的情况下,执行顺序如下:

  • 环绕通知
  • 前置通知
  • 返回通知
  • 异常返回通知

一般我们只需要配置环绕通知和异常返回通知即可

然后我们添加Controller类进行测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@RestController
public class HelloController {

@RequestMapping("/")
public String hello() {
return "hello";
}

@GetMapping("/user")
@MyLog
public String user(@RequestParam String username) {
return "hello " + username;
}
}

在 HelloController 类中,我们只对 user() 方法添加了 MyLog 注解,使用Postman进行接口测试

调用 hello() 方法未打印出 MyLogAspect 中的日志信息, 而调用 user() 方法则打印出各个方法内的日志信息

1
2
3
4
5
6
7
8
9
10
11
12
151079 2018-12-15 15:47:17.330 [http-nio-8080-exec-1] DEBUG o.s.b.f.s.DefaultListableBeanFactory - Returning cached instance of singleton bean 'myLogAspect' 
151079 2018-12-15 15:47:17.330 [http-nio-8080-exec-1] DEBUG com.example.demo.aspect.MyLogAspect - MyLog日志注解 - 环绕通知【环绕通知围绕在连接点前后,比如一个方法调用的前后】
151079 2018-12-15 15:47:17.330 [http-nio-8080-exec-1] DEBUG com.example.demo.aspect.MyLogAspect - URL ------> http://127.0.0.1:8080/user
151079 2018-12-15 15:47:17.330 [http-nio-8080-exec-1] DEBUG com.example.demo.aspect.MyLogAspect - METHOD ----> GET
151079 2018-12-15 15:47:17.330 [http-nio-8080-exec-1] DEBUG com.example.demo.aspect.MyLogAspect - ContentType ----->null
151080 2018-12-15 15:47:17.331 [http-nio-8080-exec-1] DEBUG com.example.demo.aspect.MyLogAspect - Parameters ----->org.apache.catalina.util.ParameterMap@5a243ff8
151080 2018-12-15 15:47:17.331 [http-nio-8080-exec-1] DEBUG com.example.demo.aspect.MyLogAspect - className ------> com.example.demo.controller.HelloController
151081 2018-12-15 15:47:17.332 [http-nio-8080-exec-1] DEBUG com.example.demo.aspect.MyLogAspect - method ----> user
151081 2018-12-15 15:47:17.332 [http-nio-8080-exec-1] DEBUG com.example.demo.aspect.MyLogAspect - params ----->[qinghuazangshui]
151081 2018-12-15 15:47:17.332 [http-nio-8080-exec-1] DEBUG com.example.demo.aspect.MyLogAspect - MyLog日志注解 - 前置通知【在连接点前面执行,前置通知不会影响连接点的执行,除非此处抛出异常】
151084 2018-12-15 15:47:17.335 [http-nio-8080-exec-1] DEBUG com.example.demo.aspect.MyLogAspect - MyLog日志注解 - 返回通知【在连接点执行完成后执行,不管是正常执行完成,还是抛出异常,都会执行返回通知中的内容】
151085 2018-12-15 15:47:17.336 [http-nio-8080-exec-1] DEBUG com.example.demo.aspect.MyLogAspect - MyLog日志注解 - 正常返回通知【在连接点正常执行完成后执行,如果连接点抛出异常,则不会执行】

注解内容丰富

可以在 MyLog 中添加方法,为注解添加相关说明。

1
2
3
4
5
6
7
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface MyLog {

ActionPolicy action();
}

我们在 MyLog 中添加 action() 方法, 该方法的作用是指定用户操作的类型,返回值为 ActionPolicy。 ActionPolicy 是自定义的枚举类,定义了登录、登出、增删改查、上传、下载等操作类型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public enum ActionPolicy {

LOGIN,

LOGOUT,

SAVE,

DELETE,

UDDATE,

SEARCH,

UPLOAD,

DOWNLOAD
}

在使用 MyLog 时需要添加 action 值的声明,否则会发生编译错误

1
2
3
4
5
@GetMapping("/user")
@MyLog(action = ActionPolicy.SEARCH)
public String user(@RequestParam String username) {
return "hello " + username;
}

在 MyLogAspect 的环绕通知方法下加入 action 的判断,进行相关的业务处理

1
2
3
4
5
6
7
8
9
10
@Around("myLog()")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable{
Method method = ((MethodSignature)joinPoint.getSignature()).getMethod();
MyLog myLog = method.getAnnotation(MyLog.class);
ActionPolicy actionPolicy = myLog.action();
if (actionPolicy == ActionPolicy.SEARCH) {
log.debug("MyLog日志记录 ---------> 查询方法调用");
}
return joinPoint.proceed();
}

同理,也可以增加其他的属性方法,仿照 action() 方法即可

近期使用WVS扫描工具对 Web 系统进行扫描,出现了 Slow HTTP Denial Of Service Attack 的安全漏洞,即缓慢的 HTTP DOS 攻击。

该攻击依赖于 HTTP POST ,指定一个非常大的 content-length ,以很低的频率每次发送数据包的一部分,如果一个 HTTP 请求是不完整的或者数据传输的速率非常低,服务器将占用连接资源等待其余数据的传输,如果发送大量这样的请求,则服务器将拒绝其他的请求服务,导致服务器瘫痪。

解决方案

对 web 服务器的 HTTP 头部传输的最大许可时间进行限制,修改最大许可时间为 5s(建议为 5s~10s )

Tomcat:修改 server.xml 的 connectionTimeout (默认为20000ms即 20s)

1
2
3
4
5
6
<Connector 
port="8080"
protocol="HTTP/1.1"
connectionTimeout="5000"
redirectPort="8443"
/>

根据一次插入失败报错来深入了解MyBatis Plus主键设置策略.

今天学习使用MyBatis Plus,发现使用代码生成器生成对应的实体类、Service和Mapper后,在保存数据时报错

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
com.baomidou.mybatisplus.exceptions.MybatisPlusException: java.lang.reflect.InvocationTargetException

at com.baomidou.mybatisplus.MybatisSqlSessionTemplate$SqlSessionInterceptor.invoke(MybatisSqlSessionTemplate.java:405)
at com.sun.proxy.$Proxy70.insert(Unknown Source)
at com.baomidou.mybatisplus.MybatisSqlSessionTemplate.insert(MybatisSqlSessionTemplate.java:243)
at com.baomidou.mybatisplus.activerecord.Model.insert(Model.java:56)
at com.lemon.rabbit.common.base.city.CityInitial.printInfo(CityInitial.java:112)
at com.lemon.rabbit.common.base.city.CityInitial.parseNextLevel(CityInitial.java:87)
at com.lemon.rabbit.common.base.city.CityInitial.test(CityInitial.java:59)
at com.lemon.rabbit.RabbitApplicationTests.contextLoads(RabbitApplicationTests.java:19)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:498)
at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:50)
at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12)
at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:47)
at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17)
at org.springframework.test.context.junit4.statements.RunBeforeTestExecutionCallbacks.evaluate(RunBeforeTestExecutionCallbacks.java:73)
at org.springframework.test.context.junit4.statements.RunAfterTestExecutionCallbacks.evaluate(RunAfterTestExecutionCallbacks.java:83)
at org.springframework.test.context.junit4.statements.RunBeforeTestMethodCallbacks.evaluate(RunBeforeTestMethodCallbacks.java:75)
at org.springframework.test.context.junit4.statements.RunAfterTestMethodCallbacks.evaluate(RunAfterTestMethodCallbacks.java:86)
at org.springframework.test.context.junit4.statements.SpringRepeat.evaluate(SpringRepeat.java:84)
at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:325)
at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.runChild(SpringJUnit4ClassRunner.java:251)
at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.runChild(SpringJUnit4ClassRunner.java:97)
at org.junit.runners.ParentRunner$3.run(ParentRunner.java:290)
at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:71)
at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:288)
at org.junit.runners.ParentRunner.access$000(ParentRunner.java:58)
at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:268)
at org.springframework.test.context.junit4.statements.RunBeforeTestClassCallbacks.evaluate(RunBeforeTestClassCallbacks.java:61)
at org.springframework.test.context.junit4.statements.RunAfterTestClassCallbacks.evaluate(RunAfterTestClassCallbacks.java:70)
at org.junit.runners.ParentRunner.run(ParentRunner.java:363)
at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.run(SpringJUnit4ClassRunner.java:190)
at org.junit.runner.JUnitCore.run(JUnitCore.java:137)
at com.intellij.junit4.JUnit4IdeaTestRunner.startRunnerWithArgs(JUnit4IdeaTestRunner.java:68)
at com.intellij.rt.execution.junit.IdeaTestRunner$Repeater.startRunnerWithArgs(IdeaTestRunner.java:47)
at com.intellij.rt.execution.junit.JUnitStarter.prepareStreamsAndStart(JUnitStarter.java:242)
at com.intellij.rt.execution.junit.JUnitStarter.main(JUnitStarter.java:70)
Caused by: java.lang.reflect.InvocationTargetException
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:498)
at com.baomidou.mybatisplus.MybatisSqlSessionTemplate$SqlSessionInterceptor.invoke(MybatisSqlSessionTemplate.java:401)
... 37 more
Caused by: org.apache.ibatis.exceptions.PersistenceException:
### Error updating database. Cause: org.apache.ibatis.reflection.ReflectionException: Could not set property 'id' of 'class com.lemon.rabbit.model.City' with value '1017367047558582273' Cause: java.lang.IllegalArgumentException: argument type mismatch
### Cause: org.apache.ibatis.reflection.ReflectionException: Could not set property 'id' of 'class com.lemon.rabbit.model.City' with value '1017367047558582273' Cause: java.lang.IllegalArgumentException: argument type mismatch
at org.apache.ibatis.exceptions.ExceptionFactory.wrapException(ExceptionFactory.java:30)
at org.apache.ibatis.session.defaults.DefaultSqlSession.update(DefaultSqlSession.java:200)
at org.apache.ibatis.session.defaults.DefaultSqlSession.insert(DefaultSqlSession.java:185)
... 42 more
Caused by: org.apache.ibatis.reflection.ReflectionException: Could not set property 'id' of 'class com.lemon.rabbit.model.City' with value '1017367047558582273' Cause: java.lang.IllegalArgumentException: argument type mismatch
at org.apache.ibatis.reflection.wrapper.BeanWrapper.setBeanProperty(BeanWrapper.java:185)
at org.apache.ibatis.reflection.wrapper.BeanWrapper.set(BeanWrapper.java:59)
at org.apache.ibatis.reflection.MetaObject.setValue(MetaObject.java:140)
at com.baomidou.mybatisplus.MybatisDefaultParameterHandler.populateKeys(MybatisDefaultParameterHandler.java:217)
at com.baomidou.mybatisplus.MybatisDefaultParameterHandler.processBatch(MybatisDefaultParameterHandler.java:156)
at com.baomidou.mybatisplus.MybatisDefaultParameterHandler.<init>(MybatisDefaultParameterHandler.java:71)
at com.baomidou.mybatisplus.MybatisXMLLanguageDriver.createParameterHandler(MybatisXMLLanguageDriver.java:37)
at org.apache.ibatis.session.Configuration.newParameterHandler(Configuration.java:545)
at org.apache.ibatis.executor.statement.BaseStatementHandler.<init>(BaseStatementHandler.java:69)
at org.apache.ibatis.executor.statement.PreparedStatementHandler.<init>(PreparedStatementHandler.java:40)
at org.apache.ibatis.executor.statement.RoutingStatementHandler.<init>(RoutingStatementHandler.java:46)
at org.apache.ibatis.session.Configuration.newStatementHandler(Configuration.java:558)
at org.apache.ibatis.executor.SimpleExecutor.doUpdate(SimpleExecutor.java:48)
at org.apache.ibatis.executor.BaseExecutor.update(BaseExecutor.java:117)
at org.apache.ibatis.executor.CachingExecutor.update(CachingExecutor.java:76)
at org.apache.ibatis.session.defaults.DefaultSqlSession.update(DefaultSqlSession.java:198)
... 43 more
Caused by: java.lang.IllegalArgumentException: argument type mismatch
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:498)
at org.apache.ibatis.reflection.invoker.MethodInvoker.invoke(MethodInvoker.java:41)
at org.apache.ibatis.reflection.wrapper.BeanWrapper.setBeanProperty(BeanWrapper.java:180)
... 58 more

实体类City的主键是Integer类型的,在进行insert操作时,MyBatis Plus自动生成了一个Long类型的主键id,导致参数类型不匹配,出现上述错误

经过查看日志和调试发现,MyBatis最终调用BeanWrapper的setBeanProperty方法,通过反射执行最终的插入操作(增删改查应该都是通过此处的反射,不过暂时只调试了insert方法)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
private void setBeanProperty(PropertyTokenizer prop, Object object, Object value) {
try {
Invoker method = metaClass.getSetInvoker(prop.getName());
Object[] params = {value};
try {
method.invoke(object, params);
} catch (Throwable t) {
throw ExceptionUtil.unwrapThrowable(t);
}
} catch (Throwable t) {
throw new ReflectionException("Could not set property '" + prop.getName() + "' of '" + object.getClass() + "' with value '" + value + "' Cause: " + t.toString(), t);
}
}

此处传入的object即为我们想要保存到数据库的实体信息(不带ID信息),value为主键信息,此时主键值已经是一个Long类型的值,我们接着向上看value是哪里传过来的

setBeanProperty是一个私有方法,在本类调用,查询看到set(PropertyTokenizer prop, Object value)方法调用了它

1
2
3
4
5
6
7
8
9
@Override
public void set(PropertyTokenizer prop, Object value) {
if (prop.getIndex() != null) {
Object collection = resolveCollection(prop, object);
setCollectionValue(prop, collection, value);
} else {
setBeanProperty(prop, object, value);
}
}

set方法中的value也是其他地方传入的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
Caused by: org.apache.ibatis.reflection.ReflectionException: Could not set property 'id' of 'class com.lemon.rabbit.model.City' with value '1017367047558582273' Cause: java.lang.IllegalArgumentException: argument type mismatch
at org.apache.ibatis.reflection.wrapper.BeanWrapper.setBeanProperty(BeanWrapper.java:185)
at org.apache.ibatis.reflection.wrapper.BeanWrapper.set(BeanWrapper.java:59)
at org.apache.ibatis.reflection.MetaObject.setValue(MetaObject.java:140)
at com.baomidou.mybatisplus.MybatisDefaultParameterHandler.populateKeys(MybatisDefaultParameterHandler.java:217)
at com.baomidou.mybatisplus.MybatisDefaultParameterHandler.processBatch(MybatisDefaultParameterHandler.java:156)
at com.baomidou.mybatisplus.MybatisDefaultParameterHandler.<init>(MybatisDefaultParameterHandler.java:71)
at com.baomidou.mybatisplus.MybatisXMLLanguageDriver.createParameterHandler(MybatisXMLLanguageDriver.java:37)
at org.apache.ibatis.session.Configuration.newParameterHandler(Configuration.java:545)
at org.apache.ibatis.executor.statement.BaseStatementHandler.<init>(BaseStatementHandler.java:69)
at org.apache.ibatis.executor.statement.PreparedStatementHandler.<init>(PreparedStatementHandler.java:40)
at org.apache.ibatis.executor.statement.RoutingStatementHandler.<init>(RoutingStatementHandler.java:46)
at org.apache.ibatis.session.Configuration.newStatementHandler(Configuration.java:558)
at org.apache.ibatis.executor.SimpleExecutor.doUpdate(SimpleExecutor.java:48)
at org.apache.ibatis.executor.BaseExecutor.update(BaseExecutor.java:117)
at org.apache.ibatis.executor.CachingExecutor.update(CachingExecutor.java:76)
at org.apache.ibatis.session.defaults.DefaultSqlSession.update(DefaultSqlSession.java:198)
... 43 more

查看报错日志可以看到,BeanWrapper的上一层是MetaObject,我们找到MetaObject,看到在getValue方法中调用了BeanWrapper的set方法(BeanWrapper实现了ObjectWrapper)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public void setValue(String name, Object value) {
PropertyTokenizer prop = new PropertyTokenizer(name);
if (prop.hasNext()) {
MetaObject metaValue = metaObjectForProperty(prop.getIndexedName());
if (metaValue == SystemMetaObject.NULL_META_OBJECT) {
if (value == null && prop.getChildren() != null) {
// don't instantiate child path if value is null
return;
} else {
metaValue = objectWrapper.instantiatePropertyValue(name, prop, objectFactory);
}
}
metaValue.setValue(prop.getChildren(), value);
} else {
objectWrapper.set(prop, value);
}
}

MetaObject中的主键值也是上层调用传入的,继续根据错误日志向上看:MybatisDefaultParameterHandler

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
/**
* <p>
* 自定义元对象填充控制器
* </p>
*
* @param metaObjectHandler 元数据填充处理器
* @param tableInfo 数据库表反射信息
* @param ms MappedStatement
* @param parameterObject 插入数据库对象
* @return Object
*/
protected static Object populateKeys(MetaObjectHandler metaObjectHandler, TableInfo tableInfo,
MappedStatement ms, Object parameterObject) {
if (null == tableInfo || StringUtils.isEmpty(tableInfo.getKeyProperty()) || null == tableInfo.getIdType()) {
/* 不处理 */
return parameterObject;
}
/* 自定义元对象填充控制器 */
MetaObject metaObject = ms.getConfiguration().newMetaObject(parameterObject);
if (ms.getSqlCommandType() == SqlCommandType.INSERT) {
if (tableInfo.getIdType().getKey() >= 2) {
Object idValue = metaObject.getValue(tableInfo.getKeyProperty());
/* 自定义 ID */
if (StringUtils.checkValNull(idValue)) {
if (tableInfo.getIdType() == IdType.ID_WORKER) {
metaObject.setValue(tableInfo.getKeyProperty(), IdWorker.getId());
} else if (tableInfo.getIdType() == IdType.ID_WORKER_STR) {
metaObject.setValue(tableInfo.getKeyProperty(), IdWorker.getIdStr());
} else if (tableInfo.getIdType() == IdType.UUID) {
metaObject.setValue(tableInfo.getKeyProperty(), IdWorker.get32UUID());
}
}
}
// 插入填充
if (metaObjectHandler.openInsertFill()) {
metaObjectHandler.insertFill(metaObject);
}
} else if (ms.getSqlCommandType() == SqlCommandType.UPDATE && metaObjectHandler.openUpdateFill()) {
// 更新填充
metaObjectHandler.updateFill(metaObject);
}
return metaObject.getOriginalObject();
}

在populateKeys方法中调用了metaObject.setValue()

1
2
3
4
5
6
7
8
9
if (StringUtils.checkValNull(idValue)) {
if (tableInfo.getIdType() == IdType.ID_WORKER) {
metaObject.setValue(tableInfo.getKeyProperty(), IdWorker.getId());
} else if (tableInfo.getIdType() == IdType.ID_WORKER_STR) {
metaObject.setValue(tableInfo.getKeyProperty(), IdWorker.getIdStr());
} else if (tableInfo.getIdType() == IdType.UUID) {
metaObject.setValue(tableInfo.getKeyProperty(), IdWorker.get32UUID());
}
}

可以看到,此处根据IdType生成不同类型的主键id,IdType是一个枚举类,定义了生成ID的类型

  • AUTO 数据库ID自增
  • INPUT 用户输入ID
  • ID_WORKER 全局唯一ID,Long类型的主键
  • ID_WORKER_STR 字符串全局唯一ID
  • UUID 全局唯一ID,UUID类型的主键
  • NONE 该类型为未设置主键类型

当IdType的类型为ID_WORKER、ID_WORKER_STR或者UUID时,主键由MyBatis Plus的IdWorker类生成

ID_WORKER

调用IdWorker的getId()方法,生成一个与时间相关的主键id

1
2
3
public static long getId() {
return worker.nextId();
}

IdWorker的getId()方法引用了Sequence的nextId()方法

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
public synchronized long nextId() {
long timestamp = timeGen();
if (timestamp < lastTimestamp) {//闰秒
long offset = lastTimestamp - timestamp;
if (offset <= 5) {
try {
wait(offset << 1);
timestamp = timeGen();
if (timestamp < lastTimestamp) {
throw new RuntimeException(String.format("Clock moved backwards. Refusing to generate id for %d milliseconds", offset));
}
} catch (Exception e) {
throw new RuntimeException(e);
}
} else {
throw new RuntimeException(String.format("Clock moved backwards. Refusing to generate id for %d milliseconds", offset));
}
}

if (lastTimestamp == timestamp) {
// 相同毫秒内,序列号自增
sequence = (sequence + 1) & sequenceMask;
if (sequence == 0) {
// 同一毫秒的序列数已经达到最大
timestamp = tilNextMillis(lastTimestamp);
}
} else {
// 不同毫秒内,序列号置为 1 - 3 随机数
sequence = ThreadLocalRandom.current().nextLong(1, 3);
}

lastTimestamp = timestamp;

return ((timestamp - twepoch) << timestampLeftShift) // 时间戳部分
| (datacenterId << datacenterIdShift) // 数据中心部分
| (workerId << workerIdShift) // 机器标识部分
| sequence; // 序列号部分
}

ID_WORKER_STR

将worker.nextId()的返回值转化为字符串,和getId()方法相似

1
2
3
public static String getIdStr() {
return String.valueOf(worker.nextId());
}

UUID

去除中划线的UUID字符串

1
2
3
public static synchronized String get32UUID() {
return UUID.randomUUID().toString().replace("-", "");
}

此时,已经基本可以确认,问题出在IdType的配置上,那么这个IdType从哪里获取的呢?TableInfo中获取的!

TableInfo是数据库表反射信息实体类,此处由其他方法传入的,查看日志:

1
2
at com.baomidou.mybatisplus.MybatisDefaultParameterHandler.populateKeys(MybatisDefaultParameterHandler.java:217)
at com.baomidou.mybatisplus.MybatisDefaultParameterHandler.processBatch(MybatisDefaultParameterHandler.java:156)

通过日志可以看出,populateKeys()方法是在processBatch()方法中调用的,找到该方法

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
/**
* <p>
* 批量(填充主键 ID)
* </p>
*
* @param ms
* @param parameterObject 插入数据库对象
* @return
*/
protected static Object processBatch(MappedStatement ms, Object parameterObject) {
//检查parameterObject
if (null == parameterObject) {
return null;
}
boolean isFill = false;
// 全局配置是否配置填充器
MetaObjectHandler metaObjectHandler = GlobalConfigUtils.getMetaObjectHandler(ms.getConfiguration());
/* 只处理插入或更新操作 */
if (ms.getSqlCommandType() == SqlCommandType.INSERT) {
isFill = true;
} else if (ms.getSqlCommandType() == SqlCommandType.UPDATE
&& metaObjectHandler.openUpdateFill()) {
isFill = true;
}
if (isFill) {
Collection<Object> parameters = getParameters(parameterObject);
if (null != parameters) {
List<Object> objList = new ArrayList<>();
for (Object parameter : parameters) {
TableInfo tableInfo = TableInfoHelper.getTableInfo(parameter.getClass());
if (null != tableInfo) {
objList.add(populateKeys(metaObjectHandler, tableInfo, ms, parameter));
} else {
/*
* 非表映射类不处理
*/
objList.add(parameter);
}
}
return objList;
} else {
TableInfo tableInfo = null;
if (parameterObject instanceof Map) {
Map map = (Map) parameterObject;
if (map.containsKey("et")) {
Object et = map.get("et");
if (et != null) {
if (et instanceof Map) {
Map realEtMap = (Map) et;
if (realEtMap.containsKey("MP_OPTLOCK_ET_ORIGINAL")) {//refer to OptimisticLockerInterceptor.MP_OPTLOCK_ET_ORIGINAL
tableInfo = TableInfoHelper.getTableInfo(realEtMap.get("MP_OPTLOCK_ET_ORIGINAL").getClass());
}
} else {
tableInfo = TableInfoHelper.getTableInfo(et.getClass());
}
}
}
} else {
tableInfo = TableInfoHelper.getTableInfo(parameterObject.getClass());
}
return populateKeys(metaObjectHandler, tableInfo, ms, parameterObject);
}
}
return parameterObject;
}

下面对TableInfo生成的这段代码做个说明

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
TableInfo tableInfo = null;
if (parameterObject instanceof Map) {
Map map = (Map) parameterObject;
if (map.containsKey("et")) {
Object et = map.get("et");
if (et != null) {
if (et instanceof Map) {
Map realEtMap = (Map) et;
if (realEtMap.containsKey("MP_OPTLOCK_ET_ORIGINAL")) {//refer to OptimisticLockerInterceptor.MP_OPTLOCK_ET_ORIGINAL
tableInfo = TableInfoHelper.getTableInfo(realEtMap.get("MP_OPTLOCK_ET_ORIGINAL").getClass());
}
} else {
tableInfo = TableInfoHelper.getTableInfo(et.getClass());
}
}
}
} else {
tableInfo = TableInfoHelper.getTableInfo(parameterObject.getClass());
}

parameterObject:要保存到数据库中的实体类信息

我在保存数据时,参数形式并不是Map类型的,所以直接跳转到else中

1
tableInfo = TableInfoHelper.getTableInfo(parameterObject.getClass());

根据保存的实体类的类型去获取数据库表反射信息

我们看下getTableInfo()方法的实现

1
2
3
public static TableInfo getTableInfo(Class<?> clazz) {
return tableInfoCache.get(ClassUtils.getUserClass(clazz).getName());
}

从tableInfoCache中获取指定类型的数据库反射信息。tableInfoCache是一个线程安全的私有静态Map,主要用于存放类型和数据库表的映射关系。断点调试到此处,看下tableInfoCache的内容

tableInfoCache

可以看到,idType的值为ID_WORKER,即生成一个与时间相关的Long类型的id。在放入到tableInfoCache的时候就已经指定了idType的值。

查看TableInfoHelper的源码可以得知,initTableInfo()方法负责initTableInfo的初始化

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
public synchronized static TableInfo initTableInfo(MapperBuilderAssistant builderAssistant, Class<?> clazz) {
//检测是否已存在
TableInfo tableInfo = tableInfoCache.get(clazz.getName());
if (StringUtils.checkValNotNull(tableInfo)) {
if (StringUtils.checkValNotNull(builderAssistant)) {
tableInfo.setConfigMark(builderAssistant.getConfiguration());
}
return tableInfo;
}
tableInfo = new TableInfo();
GlobalConfiguration globalConfig;
if (null != builderAssistant) {
tableInfo.setCurrentNamespace(builderAssistant.getCurrentNamespace());
tableInfo.setConfigMark(builderAssistant.getConfiguration());
//获取全局配置,其中包括了idType
globalConfig = GlobalConfigUtils.getGlobalConfig(builderAssistant.getConfiguration());
} else {
// 兼容测试场景
globalConfig = GlobalConfigUtils.DEFAULT;
}
/* 表名 */
TableName table = clazz.getAnnotation(TableName.class);
String tableName = clazz.getSimpleName();
if (table != null && StringUtils.isNotEmpty(table.value())) {
tableName = table.value();
} else {
// 开启字段下划线申明
if (globalConfig.isDbColumnUnderline()) {
tableName = StringUtils.camelToUnderline(tableName);
}
// 大写命名判断
if (globalConfig.isCapitalMode()) {
tableName = tableName.toUpperCase();
} else {
// 首字母小写
tableName = StringUtils.firstToLowerCase(tableName);
}
// 存在表前缀
if (null != globalConfig.getTablePrefix()) {
tableName = globalConfig.getTablePrefix() + tableName;
}
}
tableInfo.setTableName(tableName);

// 开启了自定义 KEY 生成器
if (null != globalConfig.getKeyGenerator()) {
tableInfo.setKeySequence(clazz.getAnnotation(KeySequence.class));
}

/* 表结果集映射 */
if (table != null && StringUtils.isNotEmpty(table.resultMap())) {
tableInfo.setResultMap(table.resultMap());
}
List<TableFieldInfo> fieldList = new ArrayList<>();
List<Field> list = getAllFields(clazz);
// 标记是否读取到主键
boolean isReadPK = false;
boolean existTableId = existTableId(list);
for (Field field : list) {
/*
* 主键ID 初始化
*/
if (!isReadPK) {
if (existTableId) {
isReadPK = initTableId(globalConfig, tableInfo, field, clazz);
} else {
isReadPK = initFieldId(globalConfig, tableInfo, field, clazz);
}
if (isReadPK) {
continue;
}

}
/*
* 字段初始化
*/
if (initTableField(globalConfig, tableInfo, fieldList, field, clazz)) {
continue;
}

/*
* 字段, 使用 camelToUnderline 转换驼峰写法为下划线分割法, 如果已指定 TableField , 便不会执行这里
*/
fieldList.add(new TableFieldInfo(globalConfig, tableInfo, field));
}

/* 字段列表 */
tableInfo.setFieldList(globalConfig, fieldList);
/*
* 未发现主键注解,提示警告信息
*/
if (StringUtils.isEmpty(tableInfo.getKeyColumn())) {
logger.warn(String.format("Warn: Could not find @TableId in Class: %s.", clazz.getName()));
}
/*
* 注入
*/
tableInfoCache.put(clazz.getName(), tableInfo);
return tableInfo;
}

在existTableId方法中判断主键注解@TableId是否存在

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/**
* <p>
* 判断主键注解是否存在
* </p>
*
* @param list 字段列表
* @return
*/
public static boolean existTableId(List<Field> list) {
boolean exist = false;
for (Field field : list) {
TableId tableId = field.getAnnotation(TableId.class);
if (tableId != null) {
exist = true;
break;
}
}
return exist;
}

当不存在主键注解时,会调用initFieldId()方法对主键属性进行初始化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
private static boolean initFieldId(GlobalConfiguration globalConfig, TableInfo tableInfo, Field field, Class<?> clazz) {
String column = field.getName();
if (globalConfig.isCapitalMode()) {
column = column.toUpperCase();
}
if (DEFAULT_ID_NAME.equalsIgnoreCase(column)) {
if (StringUtils.isEmpty(tableInfo.getKeyColumn())) {
tableInfo.setIdType(globalConfig.getIdType());
tableInfo.setKeyColumn(column);
tableInfo.setKeyProperty(field.getName());
return true;
} else {
throwExceptionId(clazz);
}
}
return false;
}

至此, 我们基本可以判断是因为在实体类City中id属性没有加@TableId注解,我们看下TableId的源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface TableId {

/**
* <p>
* 字段值(驼峰命名方式,该值可无)
* </p>
*/
String value() default "";

/**
* <p>
* 主键ID
* </p>
* {@link IdType}
*/
IdType type() default IdType.NONE;

}

TableId的类型通过type来指定,默认是IdType.NONE(该类型为未设置主键类型),City表是通过数据库的自增序列实现的,所以设置为AUTO

1
2
@TableId(type = IdType.AUTO)
private Integer id;

然后测试,程序正常运行,保存数据成功。

扩展

对于tableInfo默认的idType值配置,可以看出用的是全局配置的idType,全局配置的值是在initTableInfo()方法中获取的,有兴趣的话可以去看看全局配置的实现,此处暂不深入了

1
2
3
4
5
6
7
8
9
GlobalConfiguration globalConfig;
if (null != builderAssistant) {
tableInfo.setCurrentNamespace(builderAssistant.getCurrentNamespace());
tableInfo.setConfigMark(builderAssistant.getConfiguration());
globalConfig = GlobalConfigUtils.getGlobalConfig(builderAssistant.getConfiguration());
} else {
// 兼容测试场景
globalConfig = GlobalConfigUtils.DEFAULT;
}

队列(queue),是先进先出(FIFO, First-In-First-Out)的线性表。在具体应用中通常用链表或者数组来实现。

队列的操作方式和堆栈类似,唯一的区别在于队列只允许新数据在后端进行添加

队列只允许在后端(称为 rear )进行插入操作,在前端(称为 front )进行删除操作。进行插入操作的端称为队尾,进行删除操作的端称为队头。从队头删除元素的操作叫做出队,从队尾追加元素的操作叫做入队。

队列可分为三种:顺序队列、循环队列、双端队列

顺序队列

顺序存储结构存储的队列称为顺序队列。和顺序表一样,用一个一维数组存。队头在数组的低下标端,队尾设在高下表端。队头,队尾指针值是数组元素的下标。队头指针始终指向队头结点的前一个结点位置,初始值为-1(理论上应该为0,但是Java中索引是从0开始的,索引为0代表队列中第一个元素,所以定为-1)。队尾指针是指向队尾结点位置,初始值也为-1。

队列中没有元素时,称为空队列,

  • 初始化条件:front=rear=-1

  • 队列空的条件:front=rear

  • 队列满的条件: rear = MAXSIZE

Java实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
public class OrderQueue {

private Object[] data;
private int front = -1;
private int rear = -1;

/**
* 队列的长度
*/
private int length;

private static final int DEFAULT_LENGTH = 10;

/**
* 默认长度的队列
* 创建一个新的实例OrderQueue.
*/
public OrderQueue() {
if (length == 0) {
length = DEFAULT_LENGTH;
}
data = new Object[length];
}

/**
* 指定队列的长度
* 创建一个新的实例OrderQueue.
* @param length
*/
public OrderQueue(int length) {
this.length = length;
data = new Object[length];
}

/**
* 入队
* @author qinghuazangshui 2018年7月27日 上午11:02:30
* @param obj
* @return
* @since v1.0
*/
public synchronized Object in(Object obj) {
if (rear == length-1) {
return null;
}
data[++rear] = obj;
return obj;
}

/**
* 入列
* @author qinghuazangshui 2018年7月27日 上午11:11:15
* @param obj
* @return
* @since v1.0
*/
public synchronized boolean pull(Object obj) {
if (rear == length-1) {
throw new IndexOutOfBoundsException("队列已满");
}
data[++rear] = obj;
return true;
}

/**
* 出队
* @author qinghuazangshui 2018年7月27日 上午11:18:57
* @return
* @since v1.0
*/
public synchronized Object out() {
if(front == rear) {
return null;
}
front++;
Object obj = data[front];
data[front] = null;
return obj;
}

/**
* 出队
* @author qinghuazangshui 2018年7月27日 上午11:19:06
* @return
* @since v1.0
*/
public synchronized boolean push() {
if(front == rear) {
throw new IndexOutOfBoundsException("队列已空");
}
front++;
data[front] = null;
return true;
}

public void print() {
System.out.println(Arrays.toString(data));
}
}

测试

1
2
3
4
5
6
7
8
9
10
11
public class TestOrderQueue {

public static void main(String[] args) {
OrderQueue queue = new OrderQueue();
for(int i=10; i<23; i++) {
Object obj = queue.in(i);
System.out.println(obj);
}
queue.print();
}
}

循环向队列中添加元素,队列的默认长度是10,插入操作结束后打印当前队列的内容

1
2
3
4
5
6
7
8
9
10
11
12
13
14
10
11
12
13
14
15
16
17
18
19
null
null
null
[10, 11, 12, 13, 14, 15, 16, 17, 18, 19]

可以看到,当插入10个元素后开始返回null值,表示队列已满,插入失败;最终打印的结果也只有10个元素。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class TestOrderQueue {

public static void main(String[] args) {
OrderQueue queue = new OrderQueue();
for(int i=10; i<23; i++) {
Object obj = queue.in(i);
//System.out.println(obj);
}
queue.print();

System.out.println("-----------分割线------------");

while(true) {
Object obj = queue.out();
if(obj == null) {
break;
}
System.out.println(obj);
}
queue.print();
}
}

输出结果

1
2
3
4
5
6
7
8
9
10
11
12
13
[10, 11, 12, 13, 14, 15, 16, 17, 18, 19]
-----------分割线------------
10
11
12
13
14
15
16
17
18
19
[null, null, null, null, null, null, null, null, null, null]

每次出队都返回此次出队的元素,并将队头指针向后移动一位,同时将原有索引的位置置空

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
public class TestOrderQueue {

public static void main(String[] args) {
OrderQueue queue = new OrderQueue();
for(int i=10; i<23; i++) {
Object obj = queue.in(i);
//System.out.println(obj);
}
//queue.print();

//System.out.println("-----------分割线------------");

while(true) {
Object obj = queue.out();
if(obj == null) {
break;
}
//System.out.println(obj);
}
//queue.print();

Object obj = queue.in(10);
System.out.println(obj);
}
}

将所有元素出队后,再向队列中添加元素,返回null值,即添加失败;但是队列中明明10个位置都是空的,可用的。这就是顺序队列中的假溢出问题。

假溢出

队尾指针已经指向队尾,即rear = length-1;但是队列的前面部分仍有可用空间,但是此时已经不能再向队列中插入元素了,此种情况称为“假溢出”

解决假溢出的方法,可以在每次元素出队时,所有元素整体向前移动,让空的存储单元留在队尾

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public synchronized Object out2() {
if(front == rear) {
return null;
}
front++;
Object obj = data[front];

Object[] temp = new Object[length];
for(int i=front+1; i<=rear; i++) {
temp[i-1] = data[i];
}
for(int i=rear+1; i<length; i++) {
temp[i] = null;
}
data = temp;
front = -1;
return obj;
}

测试

1
2
3
4
5
6
7
8
9
public static void main(String[] args) {
OrderQueue queue = new OrderQueue();
for(int i=10; i<23; i++) {
Object obj = queue.in(i);
}

System.out.println(queue.out2());
queue.print();
}

输出结果

1
2
10
[11, 12, 13, 14, 15, 16, 17, 18, 19, null]

可以看出,不再出现假溢出的问题

但是,这种处理方式涉及到大量的对象创建和数据移动,性能很低;还有一种较巧妙的解决方法,那就是循环队列

循环队列

循环队列可以更简单防止伪溢出的发生,但队列大小是固定的。

循环队列应用范围广泛,比如Linux内核当中的环形缓冲区就是基于循环队列来设计的。

循环队列:采用环状顺序表来存放队列元素,有两个指针,其中 front 指针指向队列的队头元素,rear指针指向队尾元素的下一个位置,往队列中加进或取出元素时分别改变这两个变量的计数。当头尾指针(front / rear)指向队列尾的元素(下标:QueueSize-1)时,其加1操作的结果是指向向量的下界0。

循环队列中,由于入队时尾指针向前追赶头指针;出队时头指针向前追赶尾指针,造成队空和队满时头尾指针均相等。因此,无法通过条件front==rear来判别队列是”空”还是”满”。

解决这个问题的方法至少有三种:

  • 另设一布尔变量以区别队列的空和满;
  • 少用一个元素的空间。约定入队前,测试尾指针在循环意义下加1后是否等于头指针,若相等则认为队满(注意:rear所指的单元始终为空);
  • 使用一个计数器记录队列中元素的总数(即队列长度)。

此处我们采用第二种方式,少用一个元素的空间,根据尾指针在循环意义下加1后是否等于头指针判断队列是否满

定义

  • 初始化条件: front == rear == 0
  • 队列满条件: MOD(rear+1,m) == front
  • 队列空条件: front == rear
  • 队头指针推进计算: front = MOD(front+1,m)
  • 队尾指针推进计算: rear = MOD(rear+1,m)

Java实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53

public class CycleQueue {

private Object[] data;
private int front = 0;
private int rear = 0;

private static final int DEFAULT_CAPACITY = 10;

private int length;

public CycleQueue() {
this.data = new Object[DEFAULT_CAPACITY];
length = data.length;
}

public boolean full() {
if ((rear + 1) % length == front) {
return true;
}
return false;
}

public boolean empty() {
if (front == rear) {
return true;
}
return false;
}

public synchronized Object in(Object obj) {
if (full()) {
return null;
}
data[rear] = obj;
rear = (rear + 1) % length;
return obj;
}

public synchronized Object out() {
if (empty()) {
return null;
}
Object obj = data[front];
data[front] = null;
front = (front + 1) % length;
return obj;
}

public void print() {
System.out.println(Arrays.toString(data));
}
}

测试1

1
2
3
4
5
6
7
8
9

public static void main(String[] args) {
CycleQueue queue = new CycleQueue();
for(int i=10; i<23; i++) {
Object obj = queue.in(i);
System.out.println(obj);
}
queue.print();
}

尝试向循环队列中插入元素,当插入9个元素后,判定队列已满(空闲一个元素位置,用于区分队列满和队列空),所以不再插入元素。

输出结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
10
11
12
13
14
15
16
17
18
null
null
null
null
[10, 11, 12, 13, 14, 15, 16, 17, 18, null]

测试2

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public static void main(String[] args) {
CycleQueue queue = new CycleQueue();
for(int i=10; i<23; i++) {
Object obj = queue.in(i);
//System.out.println(obj);
}
queue.print();


System.out.println("-----------分割线------------");

while(true) {
Object obj = queue.out();
if(obj == null) {
break;
}
System.out.println(obj);
}
queue.print();
System.out.println(queue.empty());
}

从循环队列中取元素,然后再打印查看效果:

1
2
3
4
5
6
7
8
9
10
11
12
13
[10, 11, 12, 13, 14, 15, 16, 17, 18, null]
-----------分割线------------
10
11
12
13
14
15
16
17
18
[null, null, null, null, null, null, null, null, null, null]
true

可以看到,调用出队方法后,队列中元素置为null,队列为空

测试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
public static void main(String[] args) {
CycleQueue queue = new CycleQueue();
for(int i=10; i<23; i++) {
Object obj = queue.in(i);
//System.out.println(obj);
}
queue.print();


System.out.println("-----------分割线------------");

while(true) {
Object obj = queue.out();
if(obj == null) {
break;
}
//System.out.println(obj);
}
queue.print();

System.out.println(queue.empty());

queue.in(20);
queue.in(21);
queue.in(22);
queue.in(23);
queue.in(24);
queue.in(25);
queue.in(26);
queue.in(27);
queue.in(28);
queue.in(29);
queue.print();

System.out.println(queue.full());
}

队列为空后,再向队列中插入元素,输出结果:

1
2
3
4
5
6
[10, 11, 12, 13, 14, 15, 16, 17, 18, null]
-----------分割线------------
[null, null, null, null, null, null, null, null, null, null]
true
[21, 22, 23, 24, 25, 26, 27, 28, null, 20]
true

可以看到空元素的位置发生了变化,元素29因为队列已满,并没有插入成功,调用full()方法返回true,队列已满。

链式队列

TODO

等整理完链表的实现再来完善