0%

Java-Thirdparty-Library

阅读更多

1 Frequently-Used-Utils

commons

  1. commons-lang:commons-lang
  2. commons-io:commons-io
  3. commons-collections:commons-collections
  4. commons-cli:commons-cli

apache

  1. org.apache.commons:commons-lang3
  2. org.apache.commons:commons-collections4

google

  1. com.google.guava:guava
  2. com.google.code.gson:gson

Plugin

  1. org.apache.maven.plugins:maven-compiler-plugin
  2. org.springframework.boot:spring-boot-maven-plugin
    • 配置参数(<configuration>):
      • includeSystemScope
      • mainClass
    • 默认情况下,会讲资源文件打包,并放置在BOOT-INF/classes。需要使用Thread.currentThread().getContextClassLoader()而不能使用ClassLoader.getSystemClassLoader()。因为Thread.currentThread().getContextClassLoader()这个类加载器是Spring Boot应用程序运行时默认使用的类加载器,它知道资源文件放在了BOOT-INF/classes,而ClassLoader.getSystemClassLoader()并不知道这一信息

2 SLF4J

SLF4J,即简单日志门面(Simple Logging Facade for Java, SLF4J),不是具体的日志解决方案,它只服务于各种各样的日志系统。按照官方的说法,SLF4J是一个用于日志系统的简单Facade,允许最终用户在部署其应用时使用其所希望的日志系统

实际上,SLF4J所提供的核心API是一些接口以及一个LoggerFactory的工厂类。从某种程度上,SLF4J有点类似JDBC,不过比JDBC更简单,在JDBC中,你需要指定驱动程序,而在使用SLF4J的时候,不需要在代码中或配置文件中指定你打算使用那个具体的日志系统。如同使用JDBC基本不用考虑具体数据库一样,SLF4J提供了统一的记录日志的接口,只要按照其提供的方法记录即可,最终日志的格式、记录级别、输出方式等通过具体日志系统的配置来实现,因此可以在应用中灵活切换日志系统

简单地说,SLF4J只提供日志框架的接口,而不提供具体的实现。因此SLF4J必须配合具体的日志框架才能正常工作

2.1 Maven依赖

1
2
3
4
5
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
<version>1.7.25</version>
</dependency>

slf4j-log4j12模块包含了slf4j-api以及log4j,因此使用slf4j+log4j只需要依赖slf4j-log4j12即可

2.2 Log4j

Log4j由三个重要的组件构成:

  1. 日志信息的优先级:从高到低有ERRORWARNINFODEBUG,分别用来指定这条日志信息的重要程度
  2. 日志信息的输出目的地:指定了日志将打印到控制台还是文件中
  3. 日志信息的输出格式:控制了日志信息的显示内容

2.2.1 Log级别

  1. ALL Level:等级最低,用于打开所有日志记录
  2. DEBUG Level:指出细粒度信息事件对调试应用程序是非常有帮助的
  3. INFO level:表明消息在粗粒度级别上突出强调应用程序的运行过程
  4. WARN level:表明会出现潜在错误的情形
  5. ERROR level:指出虽然发生错误事件,但仍然不影响系统的继续运行
  6. FATAL level:指出每个严重的错误事件将会导致应用程序的退出
  7. OFF Level:等级最高,用于关闭所有日志记录
  • Log4j建议只使用四个级别,优先级从高到低分别是ERRORWARNINFODEBUG。通过在这里定义的级别,您可以控制到应用程序中相应级别的日志信息的开关。比如在这里定义了INFO级别,则应用程序中所有DEBUG级别的日志信息将不被打印出来,也是说大于等于的级别的日志才输出

2.2.2 Log4j配置

可以完全不使用配置文件,而是在代码中配置Log4j环境。但是,使用配置文件将使应用程序更加灵活。Log4j支持两种配置文件格式,一种是XML格式的文件,一种是属性文件。下面我们介绍属性文件做为配置文件的方法

2.2.2.1 配置根Logger

配置根Logger,其语法如下:

1
log4j.rootLogger = [ level ] , appenderName, appenderName, ...
  • level是日志记录的优先级,分为OFFFATALERRORWARNINFODEBUGALL或者自定义的级别。Log4j建议只使用四个级别,优先级从高到低分别是ERRORWARNINFODEBUG。通过在这里定义的级别,可以控制到应用程序中相应级别的日志信息的开关。比如在这里定义了INFO级别,则应用程序中所有DEBUG级别的日志信息将不被打印出来
  • appenderName就是指日志信息输出到哪个地方。可以同时指定多个输出目的地

2.2.2.2 配置日志信息输出目的地Appender

语法如下:

1
2
3
4
log4j.appender.<appenderName> = <fully qualified name of appender class>
log4j.appender.<appenderName>.<option1> = <value1>
...
log4j.appender.<appenderName>.<optionN> = <valueN>

其中,Log4j提供的appender有以下几种

  1. org.apache.log4j.ConsoleAppender:控制台
  2. org.apache.log4j.FileAppender:文件
  3. org.apache.log4j.DailyRollingFileAppender:每天产生一个日志文件
  4. org.apache.log4j.RollingFileAppender:文件大小到达指定尺寸的时候产生一个新的文件
  5. org.apache.log4j.WriterAppender:将日志信息以流格式发送到任意指定的地方

2.2.2.3 配置日志信息的格式

语法如下:

1
2
3
4
log4j.appender.<appenderName> = <fully qualified name of appender class>
log4j.appender.<appenderName>.<option1> = <value1>
...
log4j.appender.<appenderName>.<optionN> = <valueN>

其中,Log4j提供的layout有以下几种

  1. org.apache.log4j.HTMLLayout:以HTML表格形式布局
  2. org.apache.log4j.PatternLayout:可以灵活地指定布局模式
  3. org.apache.log4j.SimpleLayout:包含日志信息的级别和信息字符串
  4. org.apache.log4j.TTCCLayout:包含日志产生的时间、线程、类别等等信息

Log4J采用类似C语言中的printf函数的打印格式格式化日志信息

  1. %%:输出一个%字符
  2. %c:输出所属的类目,通常就是所在类的全名
  3. %d:输出日志时间点的日期或时间,默认格式为ISO8601,也可以在其后指定格式,比如:%d{yyyy-MM-dd HH:mm:ss},输出类似:2017-03-22 18:14:34
  4. %F:输出日志消息产生时所在的文件名称
  5. %l:输出日志事件的发生位置,包括类目名、发生的线程,以及在代码中的行数。举例:Testlog4.main(TestLog4.java:10)
  6. %L:输出代码中的行号
  7. %m:输出代码中指定的消息,产生的日志具体信息
  8. %n:输出一个回车换行符,Windows平台为rnUnix平台为n
  9. %p:输出优先级,即DEBUGINFOWARNERRORFATAL
  10. %r:输出自应用启动到输出该log信息耗费的毫秒数
  11. %t:输出产生该日志事件的线程名
  12. %x:输出和当前线程相关联的NDC(嵌套诊断环境),尤其用到像java servlets这样的多客户多线程的应用中

2.2.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
### 设置###
log4j.rootLogger = debug,debug,info,error,stdout

### 输出信息到控制抬 ###
log4j.appender.stdout = org.apache.log4j.ConsoleAppender
log4j.appender.stdout.Target = System.out
log4j.appender.stdout.layout = org.apache.log4j.PatternLayout
log4j.appender.stdout.layout.ConversionPattern = [%-5p] %d{yyyy-MM-dd HH:mm:ss,SSS} method:%l - %m%n

### DEBUG 级别以上的日志到指定路径 ###
log4j.appender.debug = org.apache.log4j.DailyRollingFileAppender
log4j.appender.debug.File = ./aliyun/target/logs/debug.log
log4j.appender.debug.Append = true
log4j.appender.debug.Threshold = DEBUG
log4j.appender.debug.layout = org.apache.log4j.PatternLayout
log4j.appender.debug.layout.ConversionPattern = %-d{yyyy-MM-dd HH:mm:ss} [ %t:%r ] - [ %p ] %m%n

### INFO 级别以上的日志到指定路径 ###
log4j.appender.info = org.apache.log4j.DailyRollingFileAppender
log4j.appender.info.File = ./aliyun/target/logs/info.log
log4j.appender.info.Append = true
log4j.appender.info.Threshold = INFO
log4j.appender.info.layout = org.apache.log4j.PatternLayout
log4j.appender.info.layout.ConversionPattern = %-d{yyyy-MM-dd HH:mm:ss} [ %t:%r ] - [ %p ] %m%n

### 输出ERROR 级别以上的日志到指定路径 ###
log4j.appender.error = org.apache.log4j.DailyRollingFileAppender
log4j.appender.error.File =./aliyun/target/logs/error.log
log4j.appender.error.Append = true
log4j.appender.error.Threshold = ERROR
log4j.appender.error.layout = org.apache.log4j.PatternLayout
log4j.appender.error.layout.ConversionPattern = %-d{yyyy-MM-dd HH:mm:ss} [ %t:%r ] - [ %p ] %m%n

2.2.4 参考

2.3 Logback

2.3.1 Logback的结构

Logback被分为3个组件:

  1. logback-core:提供了Logback的核心功能,是另外两个组件的基础
  2. logback-classic:实现了SLF4JAPI,所以当想配合SLF4J使用时,需要引入logback-classic
  3. logback-access:为了集成Servlet环境而准备的,可提供HTTP-access的日志接口

2.3.2 <configuration>

根元素<configuration>包含的属性包括:

  1. scan:当此属性设置为true时,配置文件如果发生改变,将会被重新加载,默认值为true
  2. scanPeriod:设置监测配置文件是否有修改的时间间隔,如果没有给出时间单位,默认单位是毫秒。当scantrue时,此属性生效。默认的时间间隔为1分钟
  3. debug:当此属性设置为true时,将打印出logback内部日志信息,实时查看logback运行状态。默认值为false

示例

1
2
3
<configuration scan="true" scanPeriod="60 second" debug="false">  
<!-- 其他配置省略-->
</configuration>

2.3.2.1 <contextName>

<contextName>用于设置上下文名称

每个logger都关联到logger上下文,默认上下文名称为default。但可以使用<contextName>设置成其他名字,用于区分不同应用程序的记录。一旦设置,不能修改

示例

1
2
3
4
<configuration scan="true" scanPeriod="60 second" debug="false">  
<contextName>myAppName</contextName>
<!-- 其他配置省略-->
</configuration>

2.3.2.2 <property>

<property>用来定义变量值的元素,其有两个属性,namevalue

  1. 其中name的值是变量的名称
  2. value的值时变量定义的值
  • 通过<property>定义的值会被插入到logger上下文中。定义变量后,可以使${}来使用变量

示例

1
2
3
4
5
<configuration scan="true" scanPeriod="60 second" debug="false">  
<property name="APP_Name" value="myAppName" />
<contextName>${APP_Name}</contextName>
<!-- 其他配置省略-->
</configuration>

2.3.2.3 <timestamp>

<timestamp>元素用于获取时间戳字符串,有两个属性

  • key:标识此<timestamp>的名字
  • datePattern:设置将当前时间(解析配置文件的时间)转换为字符串的模式,遵循Java.txt.SimpleDateFormat的格式

示例

1
2
3
4
5
<configuration scan="true" scanPeriod="60 second" debug="false">  
<timestamp key="bySecond" datePattern="yyyyMMdd'T'HHmmss"/>
<contextName>${bySecond}</contextName>
<!-- 其他配置省略-->
</configuration>

2.3.2.4 <logger>

一个<logger>元素对应了一个或者多个org.slf4j.Logger实例

  • 如果我们在程序中采用Logger logger = LoggerFactory.getLogger(A.class);来获取一个Logger的实例,那么在<logger>元素中用name属性来设定包名或者类名就可以控制该Logger实例的行为
  • 如果我们在程序中采用Logger logger = LoggerFactory.getLogger("MyLogger");来获取一个Logger的实例,那么在<logger>元素中用name属性设定同样的名字MyLogger)就可以控制该Logger实例的行为

<logger>元素用来设置一个或者多个Logger实例的日志打印级别、以及指定<appender>

<logger>仅有一个name属性,一个可选的level和一个可选的additivity属性

  • name:用来指定受此logger约束的一个或多个Logger实例
    • 可以是包名
    • 可以是类名
    • 可以是自定义的名字
  • level:用来设置打印级别,大小写无关:TRACEDEBUGINFOWARNERRORALLOFF,还有一个特殊值INHERITED或者同义词NULL,代表强制执行上级的级别
    • 如果未设置此属性,那么当前logger将会继承上级的级别
  • additivity:是否向上级logger传递打印信息。默认是true
    • 如果配置了两个logger,一个loggername属性配置的是包名(记为logger1),另一个loggername属性配置的是类名(记为logger2),那么logger1logger2上级logger
    • 其余情况,一个logger的**上级logger**就是root

<logger>可以包含零个或多个<appender-ref>元素,标识这个appender将会添加到这个logger

2.3.2.5 <root>

<root>也是<logger>元素,但是它是logger只有一个level属性,应为已经被命名为root

  • level:用来设置打印级别,大小写无关:TRACEDEBUGINFOWARNERRORALLOFF,不能设置为INHERITED或者同义词NULL。默认是DEBUG

<root>可以包含零个或多个<appender-ref>元素,标识这个appender将会添加到这个logger

2.3.2.6 <appender>

<appender><configuration>的子元素,是负责写日志的组件。<appender>有两个必要属性nameclassname指定appender名称,class指定appender的全限定名

2.3.2.6.1 ConsoleAppender

把日志添加到控制台,有以下子元素:

  • <encoder>:对日志进行格式化
  • <target>:字符串System.out或者System.err,默认System.out

示例

1
2
3
4
5
6
7
8
9
10
11
<configuration>  
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%-4relative [%thread] %-5level %logger{35} - %msg %n</pattern>
</encoder>
</appender>

<root level="DEBUG">
<appender-ref ref="STDOUT" />
</root>
</configuration>
2.3.2.6.2 FileAppender

把日志添加到文件,有以下子元素:

  • <file>:被写入的文件名,可以是相对目录,也可以是绝对目录,如果上级目录不存在会自动创建,没有默认值
  • <append>:如果是true,日志被追加到文件结尾,如果是false,清空现存文件,默认是true
  • <encoder>:对记录事件进行格式化
  • <prudent>:如果是true,日志会被安全的写入文件,即使其他的FileAppender也在向此文件做写入操作,效率低,默认是false

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
<configuration>  
<appender name="FILE" class="ch.qos.logback.core.FileAppender">
<file>testFile.log</file>
<append>true</append>
<encoder>
<pattern>%-4relative [%thread] %-5level %logger{35} - %msg%n</pattern>
</encoder>
</appender>

<root level="DEBUG">
<appender-ref ref="FILE" />
</root>
</configuration>
2.3.2.6.3 RollingFileAppender

滚动记录文件,先将日志记录到指定文件,当符合某个条件时,将日志记录到其他文件。有以下子元素:

  • <file>:被写入的文件名,可以是相对目录,也可以是绝对目录,如果上级目录不存在会自动创建,没有默认值
  • <append>:如果是true,日志被追加到文件结尾,如果是false,清空现存文件,默认是true
  • <encoder>:对记录事件进行格式化。(具体参数稍后讲解)
  • <rollingPolicy>:当发生滚动时,决定RollingFileAppender的行为,涉及文件移动和重命名
  • <triggeringPolicy>: 告知RollingFileAppender何时激活滚动
  • <prudent>:当为true时,不支持FixedWindowRollingPolicy。支持TimeBasedRollingPolicy,但是有两个限制:
    1. 不支持也不允许文件压缩
    2. 不能设置file属性,必须留空
2.3.2.6.3.1 <rollingPolicy>

ch.qos.logback.core.rolling.TimeBasedRollingPolicy:最常用的滚动策略,它根据时间来制定滚动策略,既负责滚动也负责触发滚动。有以下子节点:

  • <fileNamePattern>: 必要节点,包含文件名及%d转换符
    • %d可以包含一个Java.text.SimpleDateFormat指定的时间格式,如:%d{yyyy-MM}
    • 如果直接使用%d,默认格式是yyyy-MM-dd
    • RollingFileAppenderfile子元素可有可无,通过设置file,可以为活动文件和归档文件指定不同位置,当前日志总是记录到file指定的文件(活动文件),活动文件的名字不会改变;如果没设置file,活动文件的名字会根据fileNamePattern的值,每隔一段时间改变一次。/或者\会被当做目录分隔符
  • <maxHistory>:可选元素,控制保留的归档文件的最大数量,超出数量就删除旧文件。假设设置每个月滚动,且<maxHistory>6,则只保存最近6个月的文件,删除之前的旧文件。注意,删除旧文件是,那些为了归档而创建的目录也会被删除

ch.qos.logback.core.rolling.FixedWindowRollingPolicy:根据固定窗口算法重命名文件的滚动策略。有以下子元素:

  • <minIndex>:窗口索引最小值
  • <maxIndex>:窗口索引最大值,当用户指定的窗口过大时,会自动将窗口设置为12
  • <fileNamePattern>:必须包含%i
    • 例如,假设最小值和最大值分别为1和2,命名模式为mylog%i.log,会产生归档文件mylog1.logmylog2.log
    • 还可以指定文件压缩选项,例如,mylog%i.log.gz或者log%i.log.zip

示例1:每天生产一个日志文件,保存30天的日志文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<configuration>   
<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">

<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>logFile.%d{yyyy-MM-dd}.log</fileNamePattern>
<maxHistory>30</maxHistory>
</rollingPolicy>

<encoder>
<pattern>%-4relative [%thread] %-5level %logger{35} - %msg%n</pattern>
</encoder>
</appender>

<root level="DEBUG">
<appender-ref ref="FILE" />
</root>
</configuration>

示例2:按照固定窗口模式生成日志文件,当文件大于20MB时,生成新的日志文件。窗口大小是13,当保存了3个归档文件后,将覆盖最早的日志

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<configuration>   
<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>test.log</file>

<rollingPolicy class="ch.qos.logback.core.rolling.FixedWindowRollingPolicy">
<fileNamePattern>tests.%i.log.zip</fileNamePattern>
<minIndex>1</minIndex>
<maxIndex>3</maxIndex>
</rollingPolicy>

<triggeringPolicy class="ch.qos.logback.core.rolling.SizeBasedTriggeringPolicy">
<maxFileSize>5MB</maxFileSize>
</triggeringPolicy>
<encoder>
<pattern>%-4relative [%thread] %-5level %logger{35} - %msg%n</pattern>
</encoder>
</appender>

<root level="DEBUG">
<appender-ref ref="FILE" />
</root>
</configuration>
2.3.2.6.3.2 <triggeringPolicy>

ch.qos.logback.core.rolling.SizeBasedTriggeringPolicy:查看当前活动文件的大小,如果超过指定大小会告知RollingFileAppender触发当前活动文件滚动。只有一个子元素:

  • <maxFileSize>:这是活动文件的大小,默认值是10MB

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<configuration>   
<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>test.log</file>

<rollingPolicy class="ch.qos.logback.core.rolling.FixedWindowRollingPolicy">
<fileNamePattern>tests.%i.log.zip</fileNamePattern>
<minIndex>1</minIndex>
<maxIndex>3</maxIndex>
</rollingPolicy>

<triggeringPolicy class="ch.qos.logback.core.rolling.SizeBasedTriggeringPolicy">
<maxFileSize>5MB</maxFileSize>
</triggeringPolicy>
<encoder>
<pattern>%-4relative [%thread] %-5level %logger{35} - %msg%n</pattern>
</encoder>
</appender>

<root level="DEBUG">
<appender-ref ref="FILE" />
</root>
</configuration>
2.3.2.6.4 <encoder>

<encoder>元素负责两件事,一是把日志信息转换成字节数组,二是把字节数组写入到输出流

目前PatternLayoutEncoder是唯一有用的且默认的encoder,有一个<pattern>节点,用来设置日志的输入格式。使用%转换符方式,如果要输出%,则必须用\%进行转义

layout官方文档

示例

1
2
3
<encoder>   
<pattern>%-4relative [%thread] %-5level %logger{35} - %msg%n</pattern>
</encoder>

格式修饰符,与转换符共同使用:可选的格式修饰符位于%和转换符之间

  • 第一个可选修饰符是左对齐标志,符号是减号-
  • 接着是可选的最小宽度修饰符,用十进制数表示。如果字符小于最小宽度,则左填充或右填充,默认是左填充(即右对齐),填充符为空格。如果字符大于最小宽度,字符永远不会被截断
  • 最大宽度修饰符,符号是点号.后面加十进制数。如果字符大于最大宽度,则从前面截断。点符号.后面加减号-在加数字,表示从尾部截断

2.3.3 Test

test/resources目录下添加logback-test.xml文件即可生效

2.3.4 示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
<?xml version="1.0" encoding="UTF-8"?>
<!--
-scan:当此属性设置为true时,配置文件如果发生改变,将会被重新加载,默认值为true
-scanPeriod:设置监测配置文件是否有修改的时间间隔,如果没有给出时间单位,默认单位是毫秒
- 当scan为true时,此属性生效。默认的时间间隔为1分钟
-debug:当此属性设置为true时,将打印出logback内部日志信息,实时查看logback运行状态。默认值为false
-
- configuration 子节点为 appender、logger、root
-->
<configuration scan="true" scanPeriod="60 second" debug="false">

<!-- 负责写日志,控制台日志 -->
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">

<!-- 一是把日志信息转换成字节数组,二是把字节数组写入到输出流 -->
<encoder>
<Pattern>[%d{yyyy-MM-dd HH:mm:ss.SSS}] [%5level] [%thread] %logger{0} %msg%n</Pattern>
<charset>UTF-8</charset>
</encoder>
</appender>

<!-- 文件日志 -->
<appender name="DEBUG" class="ch.qos.logback.core.FileAppender">
<file>debug.log</file>
<!-- append: true,日志被追加到文件结尾; false,清空现存文件;默认是true -->
<append>true</append>
<filter class="ch.qos.logback.classic.filter.LevelFilter">
<!-- LevelFilter: 级别过滤器,根据日志级别进行过滤 -->
<level>DEBUG</level>
<onMatch>ACCEPT</onMatch>
<onMismatch>DENY</onMismatch>
</filter>
<encoder>
<Pattern>[%d{yyyy-MM-dd HH:mm:ss.SSS}] [%5level] [%thread] %logger{0} %msg%n</Pattern>
<charset>UTF-8</charset>
</encoder>
</appender>

<!-- 滚动记录文件,先将日志记录到指定文件,当符合某个条件时,将日志记录到其他文件 -->
<appender name="INFO" class="ch.qos.logback.core.rolling.RollingFileAppender">
<File>info.log</File>

<!-- ThresholdFilter:临界值过滤器,过滤掉 TRACE 和 DEBUG 级别的日志 -->
<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
<level>INFO</level>
</filter>

<encoder>
<Pattern>[%d{yyyy-MM-dd HH:mm:ss.SSS}] [%5level] [%thread] %logger{0} %msg%n</Pattern>
<charset>UTF-8</charset>
</encoder>

<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<!-- 每天生成一个日志文件,保存30天的日志文件
- 如果隔一段时间没有输出日志,前面过期的日志不会被删除,只有再重新打印日志的时候,会触发删除过期日志的操作
-->
<fileNamePattern>info.%d{yyyy-MM-dd}.log</fileNamePattern>
<maxHistory>30</maxHistory>
<TimeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
<maxFileSize>100MB</maxFileSize>
</TimeBasedFileNamingAndTriggeringPolicy>
</rollingPolicy>
</appender >

<!--<!– 异常日志输出 –>-->
<!--<appender name="EXCEPTION" class="ch.qos.logback.core.rolling.RollingFileAppender">-->
<!--<file>exception.log</file>-->
<!--<!– 求值过滤器,评估、鉴别日志是否符合指定条件. 需要额外的两个JAR包,commons-compiler.jar和janino.jar –>-->
<!--<filter class="ch.qos.logback.core.filter.EvaluatorFilter">-->
<!--<!– 默认为 ch.qos.logback.classic.boolex.JaninoEventEvaluator –>-->
<!--<evaluator>-->
<!--<!– 过滤掉所有日志消息中不包含"Exception"字符串的日志 –>-->
<!--<expression>return message.contains("Exception");</expression>-->
<!--</evaluator>-->
<!--<OnMatch>ACCEPT</OnMatch>-->
<!--<OnMismatch>DENY</OnMismatch>-->
<!--</filter>-->

<!--<triggeringPolicy class="ch.qos.logback.core.rolling.SizeBasedTriggeringPolicy">-->
<!--<!– 触发节点,按固定文件大小生成,超过5M,生成新的日志文件 –>-->
<!--<maxFileSize>5MB</maxFileSize>-->
<!--</triggeringPolicy>-->
<!--</appender>-->

<appender name="ERROR" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>error.log</file>

<encoder>
<Pattern>[%d{yyyy-MM-dd HH:mm:ss.SSS}] [%5level] [%thread] %logger{0} %msg%n</Pattern>
<charset>UTF-8</charset>
</encoder>

<!-- 按照固定窗口模式生成日志文件,当文件大于20MB时,生成新的日志文件
- 窗口大小是1到3,当保存了3个归档文件后,将覆盖最早的日志
- 可以指定文件压缩选项
-->
<rollingPolicy class="ch.qos.logback.core.rolling.FixedWindowRollingPolicy">
<fileNamePattern>error.%d{yyyy-MM}(%i).log.zip</fileNamePattern>
<minIndex>1</minIndex>
<maxIndex>3</maxIndex>
<timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
<maxFileSize>100MB</maxFileSize>
</timeBasedFileNamingAndTriggeringPolicy>
<maxHistory>30</maxHistory>
</rollingPolicy>
</appender>

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

<!--
- 1.name:包名或类名,用来指定受此logger约束的某一个包或者具体的某一个类
- 2.未设置打印级别,所以继承他的上级<root>的日志级别“DEBUG”
- 3.未设置additivity,默认为true,将此logger的打印信息向上级传递
- 4.未设置appender,此logger本身不打印任何信息,级别为“DEBUG”及大于“DEBUG”的日志信息传递给root
- root接到下级传递的信息,交给已经配置好的名为“STDOUT”的appender处理,“STDOUT”appender将信息打印到控制台
-->
<logger name="ch.qos.logback" />

<!--
- 1.将级别为“INFO”及大于“INFO”的日志信息交给此logger指定的名为“STDOUT”的appender处理,在控制台中打出日志
- 不再向次logger的上级 <logger name="logback"/> 传递打印信息
- 2.level:设置打印级别(TRACE, DEBUG, INFO, WARN, ERROR, ALL 和 OFF),还有一个特殊值INHERITED或者同义词NULL,代表强制执行上级的级别
- 如果未设置此属性,那么当前logger将会继承上级的级别
- 3.additivity:为false,表示此logger的打印信息不再向上级传递,如果设置为true,会打印两次
- 4.appender-ref:指定了名字为"STDOUT"的appender
-->
<logger name="com.weizhi.common.LogMain" level="INFO" additivity="false">
<appender-ref ref="STDOUT"/>
<!--<appender-ref ref="DEBUG"/>-->
<!--<appender-ref ref="EXCEPTION"/>-->
<!--<appender-ref ref="INFO"/>-->
<!--<appender-ref ref="ERROR"/>-->
<appender-ref ref="ASYNC"/>
</logger>

<!--
- 根logger
- level:设置打印级别,大小写无关:TRACE, DEBUG, INFO, WARN, ERROR, ALL 和 OFF,不能设置为INHERITED或者同义词NULL
- 默认是DEBUG
-appender-ref:可以包含零个或多个<appender-ref>元素,标识这个appender将会添加到这个logger
-->
<root level="DEBUG">
<appender-ref ref="STDOUT"/>
<!--<appender-ref ref="DEBUG"/>-->
<!--<appender-ref ref="EXCEPTION"/>-->
<!--<appender-ref ref="INFO"/>-->
<appender-ref ref="ASYNC"/>
</root>
</configuration>

2.3.5 Spring集成

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<configuration>
<!-- 引入外部属性文件 -->
<include resource="org/springframework/boot/logging/logback/defaults.xml"/>

<!-- 依据profile选择性配置 -->
<springProfile name="local, ci">
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
...
</appender>
<root level="INFO">
<appender-ref ref="STDOUT"/>
</root>
</springProfile>

<springProfile name="!local, !ci">
<appender name="ROLLINGFILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
...
</appender>
<root level="INFO">
<appender-ref ref="ROLLINGFILE"/>
</root>
</springProfile>
</configuration>

2.3.6 Spring-Boot默认的配置

参考org.springframework.boot.logging.logback.DefaultLogbackConfiguration

相关配置项参考spring-configuration-metadata.json

  • logging.pattern.console:默认的console pattern配置
  • logging.config:用于指定spring加载的logback配置文件

2.3.7 排坑

2.3.7.1 关于AsyncAppender

AsyncAppender会异步打印日志,从而避免磁盘IO阻塞当前线程的业务逻辑,其异步的实现方式也是常规的ThreadPool+BlockingQueue,那么当线程池与队列都被打满时,其行为是如何的?

直接上源码,起点是ch.qos.logback.classic.Logger,所有的日志方法都会收敛到filterAndLog_0_Or3PlusfilterAndLog_1filterAndLog_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
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
private void filterAndLog_1(final String localFQCN, final Marker marker, final Level level, final String msg, final Object param, final Throwable t) {

final FilterReply decision = loggerContext.getTurboFilterChainDecision_1(marker, this, level, msg, param, t);

if (decision == FilterReply.NEUTRAL) {
if (effectiveLevelInt > level.levelInt) {
return;
}
} else if (decision == FilterReply.DENY) {
return;
}

buildLoggingEventAndAppend(localFQCN, marker, level, msg, new Object[] { param }, t);
}

private void filterAndLog_2(final String localFQCN, final Marker marker, final Level level, final String msg, final Object param1, final Object param2,
final Throwable t) {

final FilterReply decision = loggerContext.getTurboFilterChainDecision_2(marker, this, level, msg, param1, param2, t);

if (decision == FilterReply.NEUTRAL) {
if (effectiveLevelInt > level.levelInt) {
return;
}
} else if (decision == FilterReply.DENY) {
return;
}

buildLoggingEventAndAppend(localFQCN, marker, level, msg, new Object[] { param1, param2 }, t);
}

private void buildLoggingEventAndAppend(final String localFQCN, final Marker marker, final Level level, final String msg, final Object[] params,
final Throwable t) {
LoggingEvent le = new LoggingEvent(localFQCN, this, level, msg, t, params);
le.setMarker(marker);
callAppenders(le);
}

public void callAppenders(ILoggingEvent event) {
int writes = 0;
for (Logger l = this; l != null; l = l.parent) {
writes += l.appendLoopOnAppenders(event);
if (!l.additive) {
break;
}
}
// No appenders in hierarchy
if (writes == 0) {
loggerContext.noAppenderDefinedWarning(this);
}
}

private int appendLoopOnAppenders(ILoggingEvent event) {
if (aai != null) {
return aai.appendLoopOnAppenders(event);
} else {
return 0;
}
}

继续跟踪AppenderAttachableImplappendLoopOnAppenders方法

1
2
3
4
5
6
7
8
9
10
public int appendLoopOnAppenders(E e) {
int size = 0;
final Appender<E>[] appenderArray = appenderList.asTypedArray();
final int len = appenderArray.length;
for (int i = 0; i < len; i++) {
appenderArray[i].doAppend(e);
size++;
}
return size;
}

如果AppenderAsyncAppender,那么继续跟踪UnsynchronizedAppenderBasedoAppend方法

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
public void doAppend(E eventObject) {
// WARNING: The guard check MUST be the first statement in the
// doAppend() method.

// prevent re-entry.
if (Boolean.TRUE.equals(guard.get())) {
return;
}

try {
guard.set(Boolean.TRUE);

if (!this.started) {
if (statusRepeatCount++ < ALLOWED_REPEATS) {
addStatus(new WarnStatus("Attempted to append to non started appender [" + name + "].", this));
}
return;
}

if (getFilterChainDecision(eventObject) == FilterReply.DENY) {
return;
}

// ok, we now invoke derived class' implementation of append
this.append(eventObject);

} catch (Exception e) {
if (exceptionCount++ < ALLOWED_REPEATS) {
addError("Appender [" + name + "] failed to append.", e);
}
} finally {
guard.set(Boolean.FALSE);
}
}

继续跟踪AsyncAppenderBaseappend方法,重点来了,注意第一个if语句

  • 条件1:如果当前队列的容量的剩余值小于discardingThreshold,该值默认为队列容量的1/5
  • 条件2:如果当前日志事件可以丢弃,对于AsyncAppender来说,INFO以下的日志是可以丢弃的
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
protected void append(E eventObject) {
if (isQueueBelowDiscardingThreshold() && isDiscardable(eventObject)) {
return;
}
preprocess(eventObject);
put(eventObject);
}

private boolean isQueueBelowDiscardingThreshold() {
return (blockingQueue.remainingCapacity() < discardingThreshold);
}

public void start() {
// 省略无关代码...

if (discardingThreshold == UNDEFINED)
discardingThreshold = queueSize / 5;

// 省略无关代码...
}

AsyncAppenderisDiscardable方法

1
2
3
4
protected boolean isDiscardable(ILoggingEvent event) {
Level level = event.getLevel();
return level.toInt() <= Level.INFO_INT;
}

总结:根据上面的分析可以发现,如果打日志的并发度非常高,且打的是WARNERROR日志,仍然会阻塞当前线程

2.3.8 Tips

2.3.8.1 颜色

1
2
3
4
5
6
7
8
9
10
11
<configuration debug="true">
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<withJansi>true</withJansi>
<encoder>
<pattern>[%thread] %highlight(%-5level) %cyan(%logger{15}) - %msg %n</pattern>
</encoder>
</appender>
<root level="DEBUG">
<appender-ref ref="STDOUT" />
</root>
</configuration>

2.3.9 参考

3 Test

3.1 EasyMock

mock测试就是在测试过程中,对于某些不容易构造或者不容易获取的对象,用一个虚拟的对象(不要被虚拟误导,就是Java对象,虚拟描述的是这个对象的行为)来创建以便测试的测试方法

真实对象具有不可确定的行为,产生不可预测的效果,(如:股票行情,天气预报)真实对象很难被创建的真实对象的某些行为很难被触发真实对象实际上还不存在的(和其他开发小组或者和新的硬件打交道)等等

使用一个接口来描述这个对象。在产品代码中实现这个接口,在测试代码中实现这个接口,在被测试代码中只是通过接口来引用对象,所以它不知道这个引用的对象是真实对象,还是mock对象

3.1.1 示例

该示例的目的并不是教你如何去用mock进行测试,而是给出mock对象的创建过程以及它的行为

  1. 首先创建Mock对象,即代理对象
  2. 设定EasyMock的相应逻辑,即打桩
  3. 调用mock对象的相应逻辑
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
interface Human {
boolean isMale(String name);
}

public class TestEasyMock {
public static void main(String[] args) {
Human mock = EasyMock.createMock(Human.class);

EasyMock.expect(mock.isMale("Bob")).andReturn(true);
EasyMock.expect(mock.isMale("Alice")).andReturn(true);

EasyMock.replay(mock);

System.out.println(mock.isMale("Bob"));
System.out.println(mock.isMale("Alice"));
System.out.println(mock.isMale("Robot"));
}
}

以下是输出

1
2
3
4
5
6
7
8
9
true
true
java.lang.AssertionError:
Unexpected method call Human.isMale("Robot"):
at org.easymock.internal.MockInvocationHandler.invoke(MockInvocationHandler.java:44)
at org.easymock.internal.ObjectMethodsFilter.invoke(ObjectMethodsFilter.java:85)
Disconnected from the target VM, address: '127.0.0.1:59825', transport: 'socket'
at org.liuyehcf.easymock.$Proxy0.isMale(Unknown Source)
at org.liuyehcf.easymock.TestEasyMock.main(TestEasyMock.java:28)

输出的结果很有意思,在EasyMock.replay(mock)语句之前用两个EasyMock.expect设定了BobAlice的预期结果,因此结果符合设定;而Robot并没有设定,因此抛出异常

接下来我们将分析以下上述例子中所涉及到的源码,解开mock神秘的面纱

3.1.2 源码详解

首先来看一下静态方法EasyMock.createMock,该方法返回一个Mock对象(给定接口的实例)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/**
* Creates a mock object that implements the given interface, order checking
* is disabled by default.
*
* @param <T>
* the interface that the mock object should implement.
* @param toMock
* the class of the interface that the mock object should
* implement.
* @return the mock object.
*/
public static <T> T createMock(final Class<T> toMock) {
return createControl().createMock(toMock);
}

其中createMockIMocksControl接口的方法。该方法接受Class对象,并返回Class对象所代表类型的实例

1
2
3
4
5
6
7
8
9
10
11
12
/**
* Creates a mock object that implements the given interface.
*
* @param <T>
* the interface or class that the mock object should
* implement/extend.
* @param toMock
* the interface or class that the mock object should
* implement/extend.
* @return the mock object.
*/
<T> T createMock(Class<T> toMock);

了解了createMock接口定义后,我们来看看具体的实现(MocksControl#createMock)

1
2
3
4
5
6
7
8
9
10
11
12
public <T> T createMock(final Class<T> toMock) {
try {
state.assertRecordState();
//创建一个代理工厂
final IProxyFactory<T> proxyFactory = createProxyFactory(toMock);
//利用工厂产生代理类的对象
return proxyFactory.createProxy(toMock, new ObjectMethodsFilter(toMock,
new MockInvocationHandler(this), null));
} catch (final RuntimeExceptionWrapper e) {
throw (RuntimeException) e.getRuntimeException().fillInStackTrace();
}
}

IProxyFactory接口有两个实现,JavaProxyFactoryJDK动态代理)和ClassProxyFactoryCglib)。我们以JavaProxyFactory为例进行讲解,动态代理的实现不是本篇博客的重点。下面给出JavaProxyFactory#createProxy方法的源码

1
2
3
4
public T createProxy(final Class<T> toMock, final InvocationHandler handler) {
//就是简单调用了JDK动态代理的接口,没有任何难度
return (T) Proxy.newProxyInstance(toMock.getClassLoader(), new Class[] { toMock }, handler);
}

我们再来回顾一下上述例子中的代码,我们发现一个很奇怪的现象。在EasyMock.replay方法前后,调用mock.isMale所产生的行为是不同的。在这里EasyMock.replay类似于一个开关,可以改变mock对象的行为。可是这是如何做到的呢?

1
2
3
4
5
6
7
8
9
10
11
//这里调用mock的isMale方法不会抛出异常
EasyMock.expect(mock.isMale("Bob")).andReturn(true);
EasyMock.expect(mock.isMale("Alice")).andReturn(true);

//关键开关语句
EasyMock.replay(mock);

//这里只能调用上面预定义行为的方法,若没有设定预期值那么将抛出异常
System.out.println(mock.isMale("Bob"));
System.out.println(mock.isMale("Alice"));
System.out.println(mock.isMale("Robot"));

生成代理对象的方法分析(IMocksControl#createMock)我们先暂时放在一边,我们现在先来跟踪一下EasyMock.replay方法的执行逻辑。源码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
/**
* Switches the given mock objects (more exactly: the controls of the mock
* objects) to replay mode. For details, see the EasyMock documentation.
*
* @param mocks
* the mock objects.
*/
public static void replay(final Object... mocks) {
for (final Object mock : mocks) {
//依次对每个mock对象执行下面的逻辑
getControl(mock).replay();
}
}

源码的官方注释中提到,该方法用于切换mock对象的控制模式。再来看下EasyMock.getControl方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
private static MocksControl getControl(final Object mock) {
return ClassExtensionHelper.getControl(mock);
}

public static MocksControl getControl(final Object mock) {
try {
ObjectMethodsFilter handler;

//mock是由JDK动态代理产生的类型的实例
if (Proxy.isProxyClass(mock.getClass())) {
handler = (ObjectMethodsFilter) Proxy.getInvocationHandler(mock);
}
//mock是由Cglib产生的类型的实例
else if (Enhancer.isEnhanced(mock.getClass())) {
handler = (ObjectMethodsFilter) getInterceptor(mock).getHandler();
} else {
throw new IllegalArgumentException("Not a mock: " + mock.getClass().getName());
}
//获取ObjectMethodsFilter封装的MockInvocationHandler的实例,并从MockInvocationHandler的实例中获取MocksControl的实例
return handler.getDelegate().getControl();
} catch (final ClassCastException e) {
throw new IllegalArgumentException("Not a mock: " + mock.getClass().getName());
}
}

注意到ObjectMethodsFilterInvocationHandler接口的实现,而ObjectMethodsFilter内部(delegate字段)又封装了一个InvocationHandler接口的实现,其类型是MockInvocationHandler。下面给出MockInvocationHandler的源码

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
public final class MockInvocationHandler implements InvocationHandler, Serializable {

private static final long serialVersionUID = -7799769066534714634L;

//非常重要的字段,直接决定了下面invoke方法的行为
private final MocksControl control;

//注意到构造方法接受了MocksControl作为参数
public MockInvocationHandler(final MocksControl control) {
this.control = control;
}

public Object invoke(final Object proxy, final Method method, final Object[] args) throws Throwable {
try {
//如果是记录模式
if (control.getState() instanceof RecordState) {
LastControl.reportLastControl(control);
}
return control.getState().invoke(new Invocation(proxy, method, args));
} catch (final RuntimeExceptionWrapper e) {
throw e.getRuntimeException().fillInStackTrace();
} catch (final AssertionErrorWrapper e) {
throw e.getAssertionError().fillInStackTrace();
} catch (final ThrowableWrapper t) {
throw t.getThrowable().fillInStackTrace();
}
//then let all unwrapped exceptions pass unmodified
}

public MocksControl getControl() {
return control;
}
}

再回到EasyMock.replay方法中,getControl(mock)方法返回后调用MocksControl#replay方法,下面给出MocksControl#replay的源码

1
2
3
4
5
6
7
8
9
10
public void replay() {
try {
state.replay();
//替换state,将之前收集到的行为(behavior)作为参数传给ReplayState的构造方法
state = new ReplayState(behavior);
LastControl.reportLastControl(null);
} catch (final RuntimeExceptionWrapper e) {
throw (RuntimeException) e.getRuntimeException().fillInStackTrace();
}
}

这就是为什么调用EasyMock.replay前后mock对象的行为会发生变化的原因。可以这样理解,如果stateRecordState时,调用mock的方法将会记录行为;如果stateReplayState时,调用mock的方法将会从之前记录的行为中进行查找,如果找到了则调用,如果没有则抛出异常

EasyMock的源码就分析到这里,日后再细究ReplayStateRecordState的源码

4 Lombok

4.1 Overview

lombok中常用的注解

  1. @AllArgsConstructor
  2. @NoArgsConstructor
  3. @RequiredArgsConstructor
  4. @Builder
  5. @Getter
  6. @Setter
  7. @Data
  8. @ToString
  9. @EqualsAndHashCode
  10. @Singular
  11. @Slf4j

原理:lombok注解都是编译期注解,编译期注解最大的魅力就是能够干预编译器的行为,相关技术就是JSR-269。我在另一篇博客中详细介绍了JSR-269的相关原理以及接口的使用方式,并且实现了类似lombok@Builder注解。对原理部分感兴趣的话,请移步Java-JSR-269-插入式注解处理器

4.2 构造方法

lombok提供了3个注解,用于创建构造方法,它们分别是

  1. @AllArgsConstructor@AllArgsConstructor会生成一个全量的构造方法,包括所有的字段(非final字段以及未在定义处初始化的final字段)
  2. @NoArgsConstructor@NoArgsConstructor会生成一个无参构造方法(当然,不允许类中含有未在定义处初始化的final字段)
  3. @RequiredArgsConstructor@RequiredArgsConstructor会生成一个仅包含必要参数的构造方法,什么是必要参数呢?就是那些未在定义处初始化的final字段

4.3 @Builder

@Builder是我最爱的lombok注解,没有之一。通常我们在业务代码中,时时刻刻都会用到数据传输对象(DTO),例如,我们调用一个RPC接口,需要传入一个DTO,代码通常是这样的

1
2
3
4
5
6
7
8
9
// 首先构造DTO对象
XxxDTO xxxDTO = new XxxDTO();
xxxDTO.setPro1(...);
xxxDTO.setPro2(...);
...
xxxDTO.setPron(...);

// 然后调用接口
rpcService.doSomething(xxxDTO);

其实,上述代码中的xxxDTO对象的创建以及赋值的过程,仅与rpcService有关,但是从肉眼来看,这确确实实又是两部分,我们无法快速确定xxxDTO对象只在rpcService.doSomething方法中用到。显然,这个代码片段最核心的部分就是rpcService.doSomething方法调用,而上面这种写法使得核心代码淹没在非核心代码中

借助lombok@Builder注解,我们便可以这样重构上面这段代码

1
2
3
4
5
6
7
8
rpcService.doSomething(
XxxDTO.builder()
.setPro1(...)
.setPro2(...)
...
.setPron(...)
.build()
);

这样一来,由于XxxDTO的实例仅在rpcService.doSomething方法中用到,我们就把创建的步骤放到方法参数里面去完成,代码更内聚了。通过这种方式,业务流程的脉络将会更清晰地展现出来,而不至于淹没在一大堆set方法的调用之中

4.3.1 使用方式

如果是一个简单的DTO那么直接在类上方标记@Builder注解,同时需要提供一个全参构造方法lombok就会在编译期为该类创建一个建造者模式的静态内部类

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
@Builder
public class BaseCarDTO {
private Double width;

private Double length;

private Double weight;

public BaseCarDTO() {
}

public BaseCarDTO(Double width, Double length, Double weight) {
this.width = width;
this.length = length;
this.weight = weight;
}

public Double getWidth() {
return width;
}

public void setWidth(Double width) {
this.width = width;
}

public Double getLength() {
return length;
}

public void setLength(Double length) {
this.length = length;
}

public Double getWeight() {
return weight;
}

public void setWeight(Double weight) {
this.weight = weight;
}
}

将编译后的.class文件反编译得到的.java文件如下。可以很清楚的看到,多了一个静态内部类,且采用了建造者模式,这也是@Builder注解名称的由来

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
public class BaseCarDTO {
private Double width;
private Double length;
private Double weight;

public BaseCarDTO() {
}

public BaseCarDTO(Double width, Double length, Double weight) {
this.width = width;
this.length = length;
this.weight = weight;
}

public Double getWidth() {
return this.width;
}

public void setWidth(Double width) {
this.width = width;
}

public Double getLength() {
return this.length;
}

public void setLength(Double length) {
this.length = length;
}

public Double getWeight() {
return this.weight;
}

public void setWeight(Double weight) {
this.weight = weight;
}

public static BaseCarDTO.BaseCarDTOBuilder builder() {
return new BaseCarDTO.BaseCarDTOBuilder();
}

public static class BaseCarDTOBuilder {
private Double width;
private Double length;
private Double weight;

BaseCarDTOBuilder() {
}

public BaseCarDTO.BaseCarDTOBuilder width(Double width) {
this.width = width;
return this;
}

public BaseCarDTO.BaseCarDTOBuilder length(Double length) {
this.length = length;
return this;
}

public BaseCarDTO.BaseCarDTOBuilder weight(Double weight) {
this.weight = weight;
return this;
}

public BaseCarDTO build() {
return new BaseCarDTO(this.width, this.length, this.weight);
}

public String toString() {
return "BaseCarDTO.BaseCarDTOBuilder(width=" + this.width + ", length=" + this.length + ", weight=" + this.weight + ")";
}
}
}

4.3.2 具有继承关系的DTO

我们来考虑一种更特殊的情况,假设有两个DTO,一个是TruckDTO,另一个是BaseCarDTOTruckDTO继承了BaseCarDTO。其中BaseCarDTOTruckDTO如下

  • 我们需要在@Builder注解指定builderMethodName属性,区分一下两个静态方法
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
@Builder
public class BaseCarDTO {
private Double width;

private Double length;

private Double weight;

public BaseCarDTO() {
}

public BaseCarDTO(Double width, Double length, Double weight) {
this.width = width;
this.length = length;
this.weight = weight;
}

public Double getWidth() {
return width;
}

public void setWidth(Double width) {
this.width = width;
}

public Double getLength() {
return length;
}

public void setLength(Double length) {
this.length = length;
}

public Double getWeight() {
return weight;
}

public void setWeight(Double weight) {
this.weight = weight;
}
}

@Builder(builderMethodName = "trunkBuilder")
public class TrunkDTO extends BaseCarDTO {
private Double volume;

public TrunkDTO(Double volume) {
this.volume = volume;
}

public Double getVolume() {
return volume;
}

public void setVolume(Double volume) {
this.volume = volume;
}
}

我们来看一下TrunkDTO编译得到的.class文件经过反编译得到的.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
public class TrunkDTO extends BaseCarDTO {
private Double volume;

public TrunkDTO(Double volume) {
this.volume = volume;
}

public Double getVolume() {
return this.volume;
}

public void setVolume(Double volume) {
this.volume = volume;
}

public static TrunkDTO.TrunkDTOBuilder trunkBuilder() {
return new TrunkDTO.TrunkDTOBuilder();
}

public static class TrunkDTOBuilder {
private Double volume;

TrunkDTOBuilder() {
}

public TrunkDTO.TrunkDTOBuilder volume(Double volume) {
this.volume = volume;
return this;
}

public TrunkDTO build() {
return new TrunkDTO(this.volume);
}

public String toString() {
return "TrunkDTO.TrunkDTOBuilder(volume=" + this.volume + ")";
}
}
}

可以看到,这个内部类TrunkDTOBuilder仅包含了子类TrunkDTO的字段,而不包含父类BaseCarDTO的字段

那么,我们如何让TrunkDTOBuilder也包含父类的字段呢?答案就是,我们需要将@Builder注解标记在构造方法处,构造方法包含多少字段,那么这个静态内部类就包含多少个字段,如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class TrunkDTO extends BaseCarDTO {
private Double volume;

@Builder(builderMethodName = "trunkBuilder")
public TrunkDTO(Double width, Double length, Double weight, Double volume) {
super(width, length, weight);
this.volume = volume;
}

public Double getVolume() {
return volume;
}

public void setVolume(Double volume) {
this.volume = volume;
}
}

上述TrunkDTO编译得到的.class文件经过反编译得到的.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
public class TrunkDTO extends BaseCarDTO {
private Double volume;

public TrunkDTO(Double width, Double length, Double weight, Double volume) {
super(width, length, weight);
this.volume = volume;
}

public Double getVolume() {
return this.volume;
}

public void setVolume(Double volume) {
this.volume = volume;
}

public static TrunkDTO.TrunkDTOBuilder trunkBuilder() {
return new TrunkDTO.TrunkDTOBuilder();
}

public static class TrunkDTOBuilder {
private Double width;
private Double length;
private Double weight;
private Double volume;

TrunkDTOBuilder() {
}

public TrunkDTO.TrunkDTOBuilder width(Double width) {
this.width = width;
return this;
}

public TrunkDTO.TrunkDTOBuilder length(Double length) {
this.length = length;
return this;
}

public TrunkDTO.TrunkDTOBuilder weight(Double weight) {
this.weight = weight;
return this;
}

public TrunkDTO.TrunkDTOBuilder volume(Double volume) {
this.volume = volume;
return this;
}

public TrunkDTO build() {
return new TrunkDTO(this.width, this.length, this.weight, this.volume);
}

public String toString() {
return "TrunkDTO.TrunkDTOBuilder(width=" + this.width + ", length=" + this.length + ", weight=" + this.weight + ", volume=" + this.volume + ")";
}
}
}

4.3.3 初始值

仅靠@Builder注解,那么生成的静态内部类是不会处理初始值的,如果我们要让静态内部类处理初始值,那么就需要在相关的字段上标记@Builder.Default注解

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
@Builder
public class BaseCarDTO {
@Builder.Default
private Double width = 5.0;

private Double length;

private Double weight;

public BaseCarDTO() {
}

public BaseCarDTO(Double width, Double length, Double weight) {
this.width = width;
this.length = length;
this.weight = weight;
}

public Double getWidth() {
return width;
}

public void setWidth(Double width) {
this.width = width;
}

public Double getLength() {
return length;
}

public void setLength(Double length) {
this.length = length;
}

public Double getWeight() {
return weight;
}

public void setWeight(Double weight) {
this.weight = weight;
}
}

注意,字段在被@Builder.Default修饰后,生成class文件中是没有初始值的,这是个大坑!

4.3.4 @EqualsAndHashCode

@EqualsAndHashCode注解用于创建ObjecthashCode方法以及equals方法,同样地,如果一个DTO包含父类,那么最平凡的@EqualsAndHashCode注解不会考虑父类包含的字段。因此如果子类的hashCode方法以及equals方法需要考虑父类的字段,那么需要将@EqualsAndHashCode注解的callSuper属性设置为true,这样就会调用父类的同名方法

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

private Double width = 5.0;

private Double length;

private Double weight;

public Double getWidth() {
return width;
}

public void setWidth(Double width) {
this.width = width;
}

public Double getLength() {
return length;
}

public void setLength(Double length) {
this.length = length;
}

public Double getWeight() {
return weight;
}

public void setWeight(Double weight) {
this.weight = weight;
}
}

@EqualsAndHashCode(callSuper = true)
public class TrunkDTO extends BaseCarDTO {
private Double volume;

public Double getVolume() {
return volume;
}

public void setVolume(Double volume) {
this.volume = volume;
}
}

上述TrunkDTO编译得到的.class文件经过反编译得到的.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
public class TrunkDTO extends BaseCarDTO {
private Double volume;

public TrunkDTO() {
}

public Double getVolume() {
return this.volume;
}

public void setVolume(Double volume) {
this.volume = volume;
}

public boolean equals(Object o) {
if (o == this) {
return true;
} else if (!(o instanceof TrunkDTO)) {
return false;
} else {
TrunkDTO other = (TrunkDTO)o;
if (!other.canEqual(this)) {
return false;
} else if (!super.equals(o)) {
return false;
} else {
Object this$volume = this.getVolume();
Object other$volume = other.getVolume();
if (this$volume == null) {
if (other$volume != null) {
return false;
}
} else if (!this$volume.equals(other$volume)) {
return false;
}

return true;
}
}
}

protected boolean canEqual(Object other) {
return other instanceof TrunkDTO;
}

public int hashCode() {
int PRIME = true;
int result = 1;
int result = result * 59 + super.hashCode();
Object $volume = this.getVolume();
result = result * 59 + ($volume == null ? 43 : $volume.hashCode());
return result;
}
}

4.4 @Getter/@Setter

@Getter以及@Setter注解用于为字段创建getter方法以及setter方法

4.5 @ToString

@ToString注解用于创建ObjecttoString方法

4.6 @Data

Data注解包含了@Getter@Setter@RequiredArgsConstructor@ToString以及@EqualsAndHashCode、的功能

4.7 @Slf4j

@Slf4j注解用于生成一个log字段,可以指定参数topic的值,其值代表loggerName

@Slf4j(topic = "error")等效于下面这段代码

1
private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger("error");

4.8 Tips

4.8.1 java16编译失败

若编译器版本是java16的话,编译使用了lombok的项目会出现如下的错误

1
Fatal error compiling: java.lang.ExceptionInInitializerError: Unable to make field private com.sun.tools.javac.processing.JavacProcessingEnvironment$DiscoveredProcessors com.sun.tools.javac.processing.JavacProcessingEnvironment.discoveredProcs accessible: module jdk.compiler does not "opens com.sun.tools.javac.processing" to unnamed module

解决方式:安装低版本的java,比如java8,设置JAVA_HOME环境变量用于指定java版本

5 Mina

Mina是一个Java版本的ssh-lib

5.1 Maven依赖

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
<!-- mina -->
<dependency>
<groupId>org.apache.sshd</groupId>
<artifactId>sshd-core</artifactId>
<version>2.1.0</version>
</dependency>
<dependency>
<groupId>org.apache.sshd</groupId>
<artifactId>sshd-sftp</artifactId>
<version>2.1.0</version>
</dependency>

<!-- jsch -->
<dependency>
<groupId>com.jcraft</groupId>
<artifactId>jsch</artifactId>
<version>0.1.55</version>
</dependency>

<!-- java native hook -->
<dependency>
<groupId>com.1stleg</groupId>
<artifactId>jnativehook</artifactId>
<version>2.1.0</version>
</dependency>

其中

  1. jsch是另一个ssh-client
  2. jnativehook用于捕获键盘的输入,如果仅用Java标准输入,则无法捕获类似ctrl + c这样的按键组合

5.2 Demo

5.2.1 BaseDemo

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

import org.jnativehook.GlobalScreen;
import org.jnativehook.keyboard.NativeKeyEvent;
import org.jnativehook.keyboard.NativeKeyListener;

import java.io.IOException;
import java.io.PipedInputStream;
import java.io.PipedOutputStream;
import java.nio.charset.Charset;
import java.util.Scanner;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.logging.Level;
import java.util.logging.Logger;

/**
* @author hechenfeng
* @date 2018/12/20
*/
class BaseDemo {

private static final ExecutorService EXECUTOR = Executors.newCachedThreadPool();

private static final int PIPE_STREAM_BUFFER_SIZE = 1024 * 100;
final PipedInputStream sshClientInputStream = new PipedInputStream(PIPE_STREAM_BUFFER_SIZE);
final PipedOutputStream sshClientOutputStream = new PipedOutputStream();
private final PipedInputStream bizInputStream = new PipedInputStream(PIPE_STREAM_BUFFER_SIZE);
private final PipedOutputStream bizOutputStream = new PipedOutputStream();

BaseDemo() throws IOException {
sshClientInputStream.connect(bizOutputStream);
sshClientOutputStream.connect(bizInputStream);
}

void beginRead() {
EXECUTOR.execute(() -> {
final byte[] buffer = new byte[10240];
while (!Thread.currentThread().isInterrupted()) {
try {
int readNum = bizInputStream.read(buffer);

final byte[] actualBytes = new byte[readNum];
System.arraycopy(buffer, 0, actualBytes, 0, readNum);

writeAndFlush(actualBytes);
} catch (IOException e) {
e.printStackTrace();
}
}
});
}

void beginWriteJnativehook() {
EXECUTOR.execute(() -> {
try {
Logger logger = Logger.getLogger(GlobalScreen.class.getPackage().getName());
logger.setLevel(Level.OFF);
GlobalScreen.registerNativeHook();
GlobalScreen.addNativeKeyListener(new NativeKeyListener() {
@Override
public void nativeKeyTyped(NativeKeyEvent nativeKeyEvent) {
byte keyCode = (byte) nativeKeyEvent.getKeyChar();

try {
bizOutputStream.write(keyCode);
bizOutputStream.flush();
} catch (Throwable e) {
e.printStackTrace();
}
}

@Override
public void nativeKeyPressed(NativeKeyEvent nativeKeyEvent) {
// default
}

@Override
public void nativeKeyReleased(NativeKeyEvent nativeKeyEvent) {
// default
}
});
} catch (Throwable e) {
e.printStackTrace();
}
});
}

void beginWriteStd() {
EXECUTOR.execute(() -> {
try {
final Scanner scanner = new Scanner(System.in);
while (!Thread.currentThread().isInterrupted()) {
final String command = scanner.nextLine();

bizOutputStream.write((command + "\n").getBytes());
bizOutputStream.flush();
}
} catch (Throwable e) {
e.printStackTrace();
}
});
}

private void writeAndFlush(byte[] bytes) throws IOException {
synchronized (System.out) {
System.out.write(bytes);
System.out.flush();
}
}
}

5.2.2 MinaSshDemo

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
package org.liuyehcf.mina;

import org.apache.sshd.client.SshClient;
import org.apache.sshd.client.channel.ChannelShell;
import org.apache.sshd.client.channel.ClientChannelEvent;
import org.apache.sshd.client.future.ConnectFuture;
import org.apache.sshd.client.session.ClientSession;
import org.apache.sshd.common.util.io.NoCloseInputStream;
import org.apache.sshd.common.util.io.NoCloseOutputStream;

import java.io.IOException;
import java.util.Collections;

/**
* @author hechenfeng
* @date 2018/12/20
*/
public class MinaSshDemo extends BaseDemo {

private MinaSshDemo() throws IOException {

}

public static void main(String[] args) throws Exception {
new MinaSshDemo().boot();
}

private void boot() throws Exception {
final SshClient client = SshClient.setUpDefaultClient();
client.start();
final ConnectFuture connect = client.connect("HCF", "localhost", 22);
connect.await(5000L);
final ClientSession session = connect.getSession();
session.addPasswordIdentity("???");
session.auth().verify(5000L);

final ChannelShell channel = session.createShellChannel();
channel.setIn(new NoCloseInputStream(sshClientInputStream));
channel.setOut(new NoCloseOutputStream(sshClientOutputStream));
channel.setErr(new NoCloseOutputStream(sshClientOutputStream));

// 解决颜色显示以及中文乱码的问题
channel.setPtyType("xterm-256color");
channel.setEnv("LANG", "zh_CN.UTF-8");
channel.open();

beginRead();
// beginWriteJnativehook();
beginWriteStd();

channel.waitFor(Collections.singleton(ClientChannelEvent.CLOSED), 0);
}
}

5.2.3 JschSshDemo

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
package org.liuyehcf.mina;

import com.jcraft.jsch.ChannelShell;
import com.jcraft.jsch.JSch;
import com.jcraft.jsch.Session;

import java.io.IOException;
import java.util.concurrent.TimeUnit;

/**
* @author hechenfeng
* @date 2018/12/20
*/
public class JschSshDemo extends BaseDemo {

private JschSshDemo() throws IOException {

}

public static void main(final String[] args) throws Exception {
new JschSshDemo().boot();
}

private void boot() throws Exception {
JSch jsch = new JSch();

Session session = jsch.getSession("HCF", "localhost", 22);
java.util.Properties config = new java.util.Properties();
config.put("StrictHostKeyChecking", "no");
session.setConfig(config);
session.setPassword("???");
session.connect();

ChannelShell channel = (ChannelShell) session.openChannel("shell");
channel.setInputStream(sshClientInputStream);
channel.setOutputStream(sshClientOutputStream);
channel.connect();

beginRead();
beginWriteJnativehook();
// beginWriteStd();

TimeUnit.SECONDS.sleep(1000000);
}
}

5.3 修改IdleTimeOut

1
2
3
4
5
6
7
8
9
10
Class<FactoryManager> factoryManagerClass = FactoryManager.class;

Field field = factoryManagerClass.getField("DEFAULT_IDLE_TIMEOUT");
Field modifiersField = Field.class.getDeclaredField("modifiers");
modifiersField.setAccessible(true);
modifiersField.setInt(field, field.getModifiers() & ~Modifier.FINAL);

field.setAccessible(true);

field.set(null, TimeUnit.SECONDS.toMillis(config.getIdleIntervalFrontend()));

5.4 修复显示异常的问题

1
stty cols 190 && stty rows 21 && export TERM=xterm-256color && bash

5.5 参考

6 SonarQube

Quick-Start

1
docker run -d --name sonarqube -e SONAR_ES_BOOTSTRAP_CHECKS_DISABLE=true -p 9000:9000 sonarqube:latest

SonarScanner for Maven

1
mvn clean verify sonar:sonar -DskipTests -Dsonar.login=admin -Dsonar.password=xxxx

7 Swagger

下面给一个示例

7.1 环境

  1. IDEA
  2. Maven3.3.9
  3. Spring Boot
  4. Swagger

7.2 Demo工程目录结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
.
├── pom.xml
├── src
│   └── main
│   └── java
│   └── org
│   └── liuyehcf
│   └── swagger
│   ├── UserApplication.java
│   ├── config
│   │   └── SwaggerConfig.java
│   ├── controller
│   │   └── UserController.java
│   └── entity
│   └── User.java

7.3 pom文件

引入Spring-boot以及Swagger的依赖即可,完整内容如下

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
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://maven.apache.org/POM/4.0.0"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<groupId>org.liuyehcf</groupId>
<artifactId>swagger</artifactId>
<version>1.0-SNAPSHOT</version>
<modelVersion>4.0.0</modelVersion>

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

<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger2</artifactId>
<version>2.6.1</version>
</dependency>
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger-ui</artifactId>
<version>2.6.1</version>
</dependency>
</dependencies>

<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>1.5.9.RELEASE</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>

<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.6.0</version>
<configuration>
<source>1.8</source>
<target>1.8</target>
</configuration>
</plugin>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<version>1.5.9.RELEASE</version>
<configuration>
<fork>true</fork>
<mainClass>org.liuyehcf.swagger.UserApplication</mainClass>
</configuration>
<executions>
<execution>
<goals>
<goal>repackage</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>

7.4 Swagger Config Bean

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
package org.liuyehcf.swagger.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import springfox.documentation.builders.ApiInfoBuilder;
import springfox.documentation.builders.PathSelectors;
import springfox.documentation.builders.RequestHandlerSelectors;
import springfox.documentation.service.ApiInfo;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spring.web.plugins.Docket;
import springfox.documentation.swagger2.annotations.EnableSwagger2;

@Configuration
@EnableSwagger2
public class SwaggerConfig {

@Bean
public Docket createRestApi() {
return new Docket(DocumentationType.SWAGGER_2)
.apiInfo(apiInfo())
.select()
.apis(RequestHandlerSelectors.basePackage("org.liuyehcf.swagger")) //包路径
.paths(PathSelectors.any())
.build();
}

private ApiInfo apiInfo() {
return new ApiInfoBuilder()
.title("Spring Boot - Swagger - Demo")
.description("THIS IS A SWAGGER DEMO")
.termsOfServiceUrl("http://liuyehcf.github.io") //这个不知道干嘛的
.contact("liuye")
.version("1.0.0")
.build();
}

}
  1. @Configuration:让Spring来加载该类配置
  2. @EnableSwagger2:启用Swagger2
  • 注意替换.apis(RequestHandlerSelectors.basePackage("org.liuyehcf.swagger"))这句中的包路径

7.5 Controller

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
package org.liuyehcf.swagger.controller;

import io.swagger.annotations.ApiImplicitParam;
import io.swagger.annotations.ApiImplicitParams;
import io.swagger.annotations.ApiOperation;
import io.swagger.annotations.ApiParam;
import org.liuyehcf.swagger.entity.User;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.*;

import java.util.HashMap;
import java.util.Map;

@Controller
@RequestMapping("/user")
public class UserController {

private static Map<Integer, User> userMap = new HashMap<>();

@ApiOperation(value = "GET_USER_API_1", notes = "获取User方式1")
@RequestMapping(value = "getApi1/{id}", method = RequestMethod.GET)
@ResponseBody
public User getUserByIdAndName1(
@ApiParam(name = "id", value = "用户id", required = true) @PathVariable int id,
@ApiParam(name = "name", value = "用户名字", required = true) @RequestParam String name) {
if (userMap.containsKey(id)) {
User user = userMap.get(id);
if (user.getName().equals(name)) {
return user;
}
}
return null;
}

@ApiOperation(value = "GET_USER_API_2", notes = "获取User方式2")
@ApiImplicitParams({
@ApiImplicitParam(name = "id", value = "用户id", required = true, paramType = "path", dataType = "int"),
@ApiImplicitParam(name = "name", value = "用户名字", required = true, paramType = "query", dataType = "String")
})
@RequestMapping(value = "getApi2/{id}", method = RequestMethod.GET)
@ResponseBody
public User getUserByIdAndName2(
@PathVariable int id,
@RequestParam String name) {
if (userMap.containsKey(id)) {
User user = userMap.get(id);
if (user.getName().equals(name)) {
return user;
}
}
return null;
}

@ApiOperation(value = "ADD_USER_API_1", notes = "增加User方式1")
@RequestMapping(value = "/addUser1", method = RequestMethod.POST)
@ResponseBody
public String addUser1(
@ApiParam(name = "user", value = "用户User", required = true) @RequestBody User user) {
if (userMap.containsKey(user.getId())) {
return "failure";
}
userMap.put(user.getId(), user);
return "success";
}

@ApiOperation(value = "ADD_USER_API_2", notes = "增加User方式2")
@ApiImplicitParam(name = "user", value = "用户User", required = true, paramType = "body", dataType = "User")
@RequestMapping(value = "/addUser2", method = RequestMethod.POST)
@ResponseBody
public String addUser2(@RequestBody User user) {
if (userMap.containsKey(user.getId())) {
return "failure";
}
userMap.put(user.getId(), user);
return "success";
}

}

我们通过@ApiOperation注解来给API增加说明、通过@ApiParam@ApiImplicitParams@ApiImplicitParam注解来给参数增加说明(其实不加这些注解,API文档也能生成,只不过描述主要来源于函数等命名产生,对用户并不友好,我们通常需要自己增加一些说明来丰富文档内容

  • @ApiImplicitParam最好指明paramTypedataType属性。paramType可以是pathquerybody
  • @ApiParam没有paramTypedataType属性,因为该注解可以从参数(参数类型及其Spring MVC注解)中获取这些信息

7.5.1 User

Controller中用到的实体类

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
package org.liuyehcf.swagger.entity;

public class User {
private int id;

private String name;

private String address;

public int getId() {
return id;
}

public void setId(int id) {
this.id = id;
}

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

public String getAddress() {
return address;
}

public void setAddress(String address) {
this.address = address;
}
}

7.6 Application

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package org.liuyehcf.swagger;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.context.annotation.ComponentScan;

@EnableAutoConfiguration
@ComponentScan("org.liuyehcf.swagger.*")
public class UserApplication {

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

成功启动后,即可访问http://localhost:8080/swagger-ui.html

7.7 参考

8 dom4j

这里以一个Spring的配置文件为例,通过一个示例来展示Dom4j如何写和读取xml文件

  1. 由于Spring配置文件的根元素beans需要带上xmlns,所以在添加根元素时需要填上xmlns所对应的url
  2. 在读取该带有xmlns的配置文件时,需要为SAXReader绑定xmlns
  3. 在写xPathExpress时,需要带上xmlns前缀

代码清单

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

import org.dom4j.Document;
import org.dom4j.DocumentException;
import org.dom4j.DocumentHelper;
import org.dom4j.Element;
import org.dom4j.io.OutputFormat;
import org.dom4j.io.SAXReader;
import org.dom4j.io.XMLWriter;

import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

public class Dom4jDemo {
public static final String FILE_PATH = "dom4j/src/main/resources/sample.xml";

public static void main(String[] args) {
writeXml();

readXml();
}

private static void writeXml() {
Document doc = DocumentHelper.createDocument();
doc.addComment("a simple demo ");

//注意,xmlns只能在创建Element时才能添加,无法通过addAttribute添加xmlns属性
Element beansElement = doc.addElement("beans", "http://www.springframework.org/schema/beans");
beansElement.addAttribute("xmlns:xsi", "http://www.w3.org/2001/XMLSchema-instance");
beansElement.addAttribute("xsi:schemaLocation", "http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd");

Element beanElement = beansElement.addElement("bean");
beanElement.addAttribute("id", "sample");
beanElement.addAttribute("class", "org.liuyehcf.dom4j.Person");
beanElement.addComment("This is comment");

Element propertyElement = beanElement.addElement("property");
propertyElement.addAttribute("name", "nickName");
propertyElement.addAttribute("value", "liuye");

propertyElement = beanElement.addElement("property");
propertyElement.addAttribute("name", "age");
propertyElement.addAttribute("value", "25");

propertyElement = beanElement.addElement("property");
propertyElement.addAttribute("name", "country");
propertyElement.addAttribute("value", "China");

OutputFormat format = OutputFormat.createPrettyPrint();
XMLWriter writer = null;
try {
writer = new XMLWriter(new FileWriter(new File(FILE_PATH)), format);
writer.write(doc);
writer.flush();
writer.close();
} catch (IOException e) {
e.printStackTrace();
}
}

private static void readXml() {
SAXReader saxReader = new SAXReader();

Map<String, String> map = new HashMap<>();
map.put("xmlns", "http://www.springframework.org/schema/beans");
saxReader.getDocumentFactory().setXPathNamespaceURIs(map);

Document doc = null;
try {
doc = saxReader.read(new File(FILE_PATH));
} catch (DocumentException e) {
e.printStackTrace();
return;
}

List list = doc.selectNodes("/beans/xmlns:bean/xmlns:property");
System.out.println(list.size());

list = doc.selectNodes("//xmlns:bean/xmlns:property");
System.out.println(list.size());

list = doc.selectNodes("/beans/*/xmlns:property");
System.out.println(list.size());

list = doc.selectNodes("//xmlns:property");
System.out.println(list.size());

list = doc.selectNodes("/beans//xmlns:property");
System.out.println(list.size());

list = doc.selectNodes("//xmlns:property/@value=liuye");
System.out.println(list.size());

list = doc.selectNodes("//xmlns:property/@*=liuye");
System.out.println(list.size());

list = doc.selectNodes("//xmlns:bean|//xmlns:property");
System.out.println(list.size());

}
}

生成的xml文件如下

1
2
3
4
5
6
7
8
9
10
11
12
<?xml version="1.0" encoding="UTF-8"?>

<!--a simple demo -->
<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
<bean id="sample" class="org.liuyehcf.dom4j.Person">
<!--This is comment-->
<property name="nickName" value="liuye"/>
<property name="age" value="25"/>
<property name="country" value="China"/>
</bean>
</beans>

输出如下

1
2
3
4
5
6
7
8
3
3
3
3
3
1
1
4

8.1 基本数据结构

dom4j几乎所有的数据类型都继承自Node接口,下面介绍几个常用的数据类型

  1. Document:表示整个xml文件
  2. Element:元素
  3. Attribute:元素的属性

8.2 Node.selectNodes

该方法根据xPathExpress来选取节点,xPathExpress的语法规则如下

  1. "/beans/bean/property":从跟节点<beans>开始,经过<bean>节点的所有<property>节点
  2. "//property":所有<property>节点
  3. “property”:当前节点开始的所有<property>节点
  4. "/beans//property":从根节点<beans>开始,所有所有<property>节点(无论经过几个中间节点)
  5. "/beans/bean/property/@value":从跟节点<beans>开始,经过<bean>节点,包含属性value的所有<property>节点
  6. "/beans/bean/property/@value=liuye":从跟节点<beans>开始,经过<bean>节点,包含属性value且值为liuye的所有<property>节点
  7. "/beans/*/property/@*=liuye":从跟节点<beans>开始,经过任意节点(注意*//不同,*只匹配一个节点,//匹配任意零或多层节点),包含任意属性且值为liuye的所有<property>节点
  • 通配符
    • *可以匹配任意节点
    • @*可以匹配任意属性
    • |表示或运算
  • 所有以/或者//开始的xPathExpress都与当前节点的位置无关

注意,如果xml文件带有xmlns,那么在写xPathExpress时需要带上xmlns前缀,例如示例中那样的写法

8.3 参考

9 Cglib