阅读更多
1 Frequently-Used-Utils
commons
:
commons-lang:commons-lang
commons-io:commons-io
commons-collections:commons-collections
commons-cli:commons-cli
apache
:
org.apache.commons:commons-lang3
org.apache.commons:commons-collections4
google
:
com.google.guava:guava
com.google.code.gson:gson
Plugin
:
org.apache.maven.plugins:maven-compiler-plugin
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
由三个重要的组件构成:
日志信息的优先级 :从高到低有ERROR
、WARN
、 INFO
、DEBUG
,分别用来指定这条日志信息的重要程度
日志信息的输出目的地 :指定了日志将打印到控制台还是文件中
日志信息的输出格式 :控制了日志信息的显示内容
2.2.1 Log级别
ALL Level
:等级最低,用于打开所有日志记录
DEBUG Level
:指出细粒度信息事件对调试应用程序是非常有帮助的
INFO level
:表明消息在粗粒度级别上突出强调应用程序的运行过程
WARN level
:表明会出现潜在错误的情形
ERROR level
:指出虽然发生错误事件,但仍然不影响系统的继续运行
FATAL level
:指出每个严重的错误事件将会导致应用程序的退出
OFF Level
:等级最高,用于关闭所有日志记录
Log4j
建议只使用四个级别,优先级从高到低分别是ERROR
、WARN
、INFO
、DEBUG
。通过在这里定义的级别,您可以控制到应用程序中相应级别的日志信息的开关。比如在这里定义了INFO
级别,则应用程序中所有DEBUG
级别的日志信息将不被打印出来,也是说大于等于的级别的日志才输出
2.2.2 Log4j配置
可以完全不使用配置文件,而是在代码中配置Log4j
环境。但是,使用配置文件将使应用程序更加灵活。Log4j
支持两种配置文件格式 ,一种是XML
格式的文件,一种是属性文件。下面我们介绍属性文件做为配置文件的方法
2.2.2.1 配置根Logger
配置根Logger
,其语法如下:
1 log4j.rootLogger = [ level ] , appenderName, appenderName, ...
level
是日志记录的优先级,分为OFF
、FATAL
、ERROR
、WARN
、INFO
、DEBUG
、ALL
或者自定义的级别。Log4j
建议只使用四个级别,优先级从高到低分别是ERROR
、WARN
、INFO
、DEBUG
。通过在这里定义的级别,可以控制到应用程序中相应级别的日志信息的开关。比如在这里定义了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有以下几种
org.apache.log4j.ConsoleAppender
:控制台
org.apache.log4j.FileAppender
:文件
org.apache.log4j.DailyRollingFileAppender
:每天产生一个日志文件
org.apache.log4j.RollingFileAppender
:文件大小到达指定尺寸的时候产生一个新的文件
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有以下几种
org.apache.log4j.HTMLLayout
:以HTML表格形式布局
org.apache.log4j.PatternLayout
:可以灵活地指定布局模式
org.apache.log4j.SimpleLayout
:包含日志信息的级别和信息字符串
org.apache.log4j.TTCCLayout
:包含日志产生的时间、线程、类别等等信息
Log4J采用类似C语言中的printf函数的打印格式格式化日志信息
%%
:输出一个%
字符
%c
:输出所属的类目,通常就是所在类的全名
%d
:输出日志时间点的日期或时间,默认格式为ISO8601
,也可以在其后指定格式,比如:%d{yyyy-MM-dd HH:mm:ss}
,输出类似:2017-03-22 18:14:34
%F
:输出日志消息产生时所在的文件名称
%l
:输出日志事件的发生位置,包括类目名、发生的线程,以及在代码中的行数。举例:Testlog4.main(TestLog4.java:10)
%L
:输出代码中的行号
%m
:输出代码中指定的消息,产生的日志具体信息
%n
:输出一个回车换行符,Windows
平台为rn
,Unix
平台为n
%p
:输出优先级,即DEBUG
,INFO
,WARN
,ERROR
,FATAL
%r
:输出自应用启动到输出该log
信息耗费的毫秒数
%t
:输出产生该日志事件的线程名
%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个组件:
logback-core
:提供了Logback的核心功能,是另外两个组件的基础
logback-classic
:实现了SLF4J
的API
,所以当想配合SLF4J
使用时,需要引入logback-classic
logback-access
:为了集成Servlet
环境而准备的,可提供HTTP-access
的日志接口
2.3.2 <configuration>
根元素<configuration>
包含的属性包括:
scan
:当此属性设置为true
时,配置文件如果发生改变,将会被重新加载,默认值为true
scanPeriod
:设置监测配置文件是否有修改的时间间隔,如果没有给出时间单位,默认单位是毫秒。当scan
为true
时,此属性生效。默认的时间间隔为1
分钟
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>
用来定义变量值的元素,其有两个属性,name
和value
其中name
的值是变量的名称
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
:用来设置打印级别,大小写无关:TRACE
,DEBUG
,INFO
,WARN
,ERROR
,ALL
和OFF
,还有一个特殊值INHERITED
或者同义词NULL
,代表强制执行上级的级别
如果未设置此属性,那么当前logger
将会继承上级的级别
additivity
:是否向上级logger
传递打印信息。默认是true
如果配置了两个logger
,一个logger
的name
属性配置的是包名(记为logger1
),另一个logger
的name
属性配置的是类名(记为logger2
),那么logger1
是logger2
的上级logger
其余情况,一个logger
的**上级logger
**就是root
<logger>
可以包含零个或多个<appender-ref>
元素,标识这个appender
将会添加到这个logger
2.3.2.5 <root>
<root>
也是<logger>
元素,但是它是根logger
。只有一个level
属性,应为已经被命名为root
level
:用来设置打印级别,大小写无关:TRACE
,DEBUG
,INFO
,WARN
,ERROR
,ALL
和OFF
,不能设置为INHERITED
或者同义词NULL
。默认是DEBUG
<root>
可以包含零个或多个<appender-ref>
元素,标识这个appender
将会添加到这个logger
2.3.2.6 <appender>
<appender>
是<configuration>
的子元素,是负责写日志的组件。<appender>
有两个必要属性name
和class
。name
指定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
,但是有两个限制:
不支持也不允许文件压缩
不能设置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
RollingFileAppender
的file
子元素可有可无,通过设置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.log
和mylog2.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
时,生成新的日志文件。窗口大小是1
到3
,当保存了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" ?> <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</append > <filter class ="ch.qos.logback.classic.filter.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 > <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" > <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 ="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 > <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" > <discardingThreshold > 0</discardingThreshold > <queueSize > 512</queueSize > <appender-ref ref ="ERROR" /> </appender > <logger name ="ch.qos.logback" /> <logger name ="com.weizhi.common.LogMain" level ="INFO" additivity ="false" > <appender-ref ref ="STDOUT" /> <appender-ref ref ="ASYNC" /> </logger > <root level ="DEBUG" > <appender-ref ref ="STDOUT" /> <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" /> <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_Or3Plus
、filterAndLog_1
、filterAndLog_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 ; } } if (writes == 0 ) { loggerContext.noAppenderDefinedWarning(this ); } } private int appendLoopOnAppenders (ILoggingEvent event) { if (aai != null ) { return aai.appendLoopOnAppenders(event); } else { return 0 ; } }
继续跟踪AppenderAttachableImpl
的appendLoopOnAppenders
方法
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; }
如果Appender
是AsyncAppender
,那么继续跟踪UnsynchronizedAppenderBase
的doAppend
方法
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) { 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 ; } this .append(eventObject); } catch (Exception e) { if (exceptionCount++ < ALLOWED_REPEATS) { addError("Appender [" + name + "] failed to append." , e); } } finally { guard.set(Boolean.FALSE); } }
继续跟踪AsyncAppenderBase
的append
方法,重点来了,注意第一个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 ; }
AsyncAppender
的isDiscardable
方法
1 2 3 4 protected boolean isDiscardable (ILoggingEvent event) { Level level = event.getLevel(); return level.toInt() <= Level.INFO_INT; }
总结:根据上面的分析可以发现,如果打日志的并发度非常高,且打的是WARN
或ERROR
日志,仍然会阻塞当前线程
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
对象的创建过程以及它的行为
首先创建Mock
对象,即代理对象
设定EasyMock
的相应逻辑,即打桩
调用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
设定了Bob
和Alice
的预期结果,因此结果符合设定;而Robot
并没有设定,因此抛出异常
接下来我们将分析以下上述例子中所涉及到的源码,解开mock
神秘的面纱
3.1.2 源码详解
首先来看一下静态方法EasyMock.createMock
,该方法返回一个Mock
对象(给定接口的实例)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 public static <T> T createMock (final Class<T> toMock) { return createControl().createMock(toMock); }
其中createMock
是IMocksControl
接口的方法。该方法接受Class
对象,并返回Class
对象所代表类型的实例
1 2 3 4 5 6 7 8 9 10 11 12 <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
接口有两个实现,JavaProxyFactory
(JDK
动态代理)和ClassProxyFactory
(Cglib
)。我们以JavaProxyFactory
为例进行讲解,动态代理的实现不是本篇博客的重点。下面给出JavaProxyFactory#createProxy
方法的源码
1 2 3 4 public T createProxy (final Class<T> toMock, final InvocationHandler handler) { 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 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 public static void replay (final Object... mocks) { for (final Object mock : mocks) { 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; if (Proxy.isProxyClass(mock.getClass())) { handler = (ObjectMethodsFilter) Proxy.getInvocationHandler(mock); } else if (Enhancer.isEnhanced(mock.getClass())) { handler = (ObjectMethodsFilter) getInterceptor(mock).getHandler(); } else { throw new IllegalArgumentException ("Not a mock: " + mock.getClass().getName()); } return handler.getDelegate().getControl(); } catch (final ClassCastException e) { throw new IllegalArgumentException ("Not a mock: " + mock.getClass().getName()); } }
注意到ObjectMethodsFilter
是InvocationHandler
接口的实现,而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 ; private final MocksControl control; 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(); } } 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 = new ReplayState (behavior); LastControl.reportLastControl(null ); } catch (final RuntimeExceptionWrapper e) { throw (RuntimeException) e.getRuntimeException().fillInStackTrace(); } }
这就是为什么调用EasyMock.replay
前后mock
对象的行为会发生变化的原因。可以这样理解,如果state
是RecordState
时,调用mock
的方法将会记录行为;如果state
是ReplayState
时,调用mock
的方法将会从之前记录的行为中进行查找,如果找到了则调用,如果没有则抛出异常
EasyMock
的源码就分析到这里,日后再细究ReplayState
与RecordState
的源码
4 Lombok
4.1 Overview
lombok中常用的注解
@AllArgsConstructor
@NoArgsConstructor
@RequiredArgsConstructor
@Builder
@Getter
@Setter
@Data
@ToString
@EqualsAndHashCode
@Singular
@Slf4j
原理:lombok
注解都是编译期
注解,编译期
注解最大的魅力就是能够干预编译器的行为,相关技术就是JSR-269
。我在另一篇博客中详细介绍了JSR-269
的相关原理以及接口的使用方式,并且实现了类似lombok
的@Builder
注解。对原理部分感兴趣的话,请移步Java-JSR-269-插入式注解处理器
4.2 构造方法
lombok
提供了3个注解,用于创建构造方法,它们分别是
@AllArgsConstructor
:@AllArgsConstructor
会生成一个全量的构造方法,包括所有的字段(非final
字段以及未在定义处初始化的final
字段)
@NoArgsConstructor
:@NoArgsConstructor
会生成一个无参构造方法(当然,不允许类中含有未在定义处初始化的final
字段)
@RequiredArgsConstructor
:@RequiredArgsConstructor
会生成一个仅包含必要参数的构造方法,什么是必要参数呢?就是那些未在定义处初始化的final
字段
4.3 @Builder
@Builder
是我最爱的lombok注解,没有之一 。通常我们在业务代码中,时时刻刻都会用到数据传输对象(DTO
),例如,我们调用一个RPC
接口,需要传入一个DTO
,代码通常是这样的
1 2 3 4 5 6 7 8 9 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
,另一个是BaseCarDTO
。TruckDTO
继承了BaseCarDTO
。其中BaseCarDTO
与TruckDTO
如下
我们需要在@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
注解用于创建Object
的hashCode
方法以及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
注解用于创建Object
的toString
方法
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 <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 > <dependency > <groupId > com.jcraft</groupId > <artifactId > jsch</artifactId > <version > 0.1.55</version > </dependency > <dependency > <groupId > com.1stleg</groupId > <artifactId > jnativehook</artifactId > <version > 2.1.0</version > </dependency >
其中
jsch
是另一个ssh-client
库
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;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) { } @Override public void nativeKeyReleased (NativeKeyEvent nativeKeyEvent) { } }); } 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;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(); 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;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(); 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 环境
IDEA
Maven3.3.9
Spring Boot
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(); } }
@Configuration
:让Spring
来加载该类配置
@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
最好指明paramType
与dataType
属性。paramType
可以是path
、query
、body
@ApiParam
没有paramType
与dataType
属性,因为该注解可以从参数(参数类型及其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
文件
由于Spring
配置文件的根元素beans
需要带上xmlns
,所以在添加根元素时需要填上xmlns
所对应的url
在读取该带有xmlns
的配置文件时,需要为SAXReader
绑定xmlns
在写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 " ); 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" ?> <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" > <property name ="nickName" value ="liuye" /> <property name ="age" value ="25" /> <property name ="country" value ="China" /> </bean > </beans >
输出如下
8.1 基本数据结构
dom4j几乎所有的数据类型都继承自Node接口,下面介绍几个常用的数据类型
Document
:表示整个xml文件
Element
:元素
Attribute
:元素的属性
8.2 Node.selectNodes
该方法根据xPathExpress
来选取节点,xPathExpress
的语法规则如下
"/beans/bean/property"
:从跟节点<beans>
开始,经过<bean>
节点的所有<property>
节点
"//property"
:所有<property>
节点
“property”:当前节点开始 的所有<property>
节点
"/beans//property"
:从根节点<beans>
开始,所有所有<property>
节点(无论经过几个中间节点)
"/beans/bean/property/@value"
:从跟节点<beans>
开始,经过<bean>
节点,包含属性value
的所有<property>
节点
"/beans/bean/property/@value=liuye"
:从跟节点<beans>
开始,经过<bean>
节点,包含属性value
且值为liuye
的所有<property>
节点
"/beans/*/property/@*=liuye"
:从跟节点<beans>
开始,经过任意节点(注意*
与//
不同,*
只匹配一个节点,//
匹配任意零或多层节点),包含任意属性且值为liuye
的所有<property>
节点
通配符
*
可以匹配任意节点
@*
可以匹配任意属性
|
表示或运算
所有以/
或者//
开始的xPathExpress
都与当前节点的位置无关
注意,如果xml
文件带有xmlns
,那么在写xPathExpress
时需要带上xmlns
前缀,例如示例中那样的写法
8.3 参考
9 Cglib