阅读更多
1 Frequently-Used-Utils
commons
:
commons-lang:commons-lang
commons-collections:commons-collections
commons-configuration:commons-configuration
commons-io:commons-io
commons-codec:commons-codec
commons-net:commons-net
commons-cli:commons-cli
commons-logging:commons-logging
apache
:
org.apache.commons:commons-lang3
org.apache.commons:commons-collections4
org.apache.commons:commons-configuration2
org.apache.httpcomponents:httpclient
org.apache.commons:commons-pool2
org.apache.commons:commons-csv
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 log4j2
Apache Log4j Doc
2.1.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 26 27 28 29 30 31 32 <dependencies > <dependency > <groupId > org.slf4j</groupId > <artifactId > slf4j-api</artifactId > <version > ${slf4j.version}</version > </dependency > <dependency > <groupId > org.apache.logging.log4j</groupId > <artifactId > log4j-api</artifactId > <version > ${log4j2.version}</version > </dependency > <dependency > <groupId > org.apache.logging.log4j</groupId > <artifactId > log4j-core</artifactId > <version > ${log4j2.version}</version > </dependency > <dependency > <groupId > org.apache.logging.log4j</groupId > <artifactId > log4j-slf4j-impl</artifactId > <version > ${log4j2.version}</version > </dependency > <dependency > <groupId > org.apache.logging.log4j</groupId > <artifactId > log4j-1.2-api</artifactId > <version > ${log4j2.version}</version > </dependency > </dependencies >
2.1.2 Demo
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 <?xml version="1.0" encoding="UTF-8" ?> <Configuration status ="WARN" > <Appenders > <Console name ="ConsoleAppender" target ="SYSTEM_OUT" > <PatternLayout pattern ="%d{yyyy-MM-dd HH:mm:ss.SSS} %style{[%thread]}{bright} %highlight{[%-5level] %logger{36}}{STYLE=Logback} - %msg%n" /> </Console > <RollingRandomAccessFile name ="RollingRandomAccessFileAppender" fileName ="logs/app.log" filePattern ="logs/app-%d{yyyy-MM-dd}-%i.log" > <PatternLayout pattern ="%d{yyyy-MM-dd HH:mm:ss.SSS} %style{[%thread]}{bright} %highlight{[%-5level] %logger{36}}{STYLE=Logback} - %msg%n" /> <Policies > <TimeBasedTriggeringPolicy interval ="1" modulate ="true" /> <SizeBasedTriggeringPolicy size ="10MB" /> </Policies > <DefaultRolloverStrategy max ="7" > <Delete basePath ="logs" maxDepth ="1" > <IfFileName glob ="app-*.log" /> <IfLastModified age ="7d" /> </Delete > </DefaultRolloverStrategy > </RollingRandomAccessFile > <Async name ="AsyncAppender" > <AppenderRef ref ="ConsoleAppender" /> <AppenderRef ref ="RollingRandomAccessFileAppender" /> </Async > </Appenders > <Loggers > <Root level ="info" > <AppenderRef ref ="AsyncAppender" /> </Root > </Loggers > </Configuration >
Delete Action only works when file rolling triggered.
2.1.3 Tips
2.1.3.1 Command-Line Options
Specify configuration path: -Dlog4j.configurationFile=path/to/log4j2.xml
Enable debug mode (refers to debug mode of log4j2
itself):-Dlog4j.debug=true
2.1.3.2 Logger Name Pattern
Syntanx: %logger{precision}
or %c{precision}
Pattern
LoggerName
Result
%c{1}
org.apache.commons.Foo
Foo
%c{2}
org.apache.commons.Foo
commons.Foo
%c{1.}
org.apache.commons.Foo
o.a.c.Foo
2.1.3.3 Color Configuration
Pattern Layout
%highlight{pattern}{style}
%highlight{[%thread] [%-5level] [%logger{36}]}{FATAL=red blink, ERROR=red, WARN=yellow, INFO=green, DEBUG=cyan, TRACE=blue}
%highlight{[%-5level] %logger{36}}{STYLE=Logback}
%style{pattern}{ANSI style}
%style{%logger}{red}
%style{[%thread]}{bright}
2.1.3.4 Lookups
Lookups provide a way to add values to the Log4j configuration at arbitrary places. They are a particular type of Plugin that implements the StrLookup interface. Information on how to use Lookups in configuration files can be found in the Property Substitution section of the Configuration page.
System Properties Lookup: ${sys:xxx}
${sys:xxx:-yyy}
: With default
Environment Lookup: ${env:xxx}
${env:xxx:-yyy}
: With default
2.1.3.5 SPI
Path: META-INF/org/apache/logging/log4j/core/config/plugins/Log4j2Plugins.dat
2.1.4 Issues
2.1.4.1 No appenders are available for AsyncAppender
Generally, AsyncAppender relies on one or more Appenders, but if the initializations of these dependencies failed, this exception may occur. Possible reasons are:
RollingRandomAccessFile
has no permission to perform write operation, like create directory and file.
2.1.4.2 Binding to log4j-1 Unexpectedly
1 2 3 log4j:WARN Please initialize the log4j system properly. log4j:WARN See http://logging.apache.org/log4j/1.2/faq.html#noconfig for more info. log4j:WARN No appenders could be found for logger (org.apache.hadoop.util.Shell).
Maybe you don’t exclude all log4j-1’s dependency in your project, like:
org.slf4j:slf4j-reload4j
ch.qos.reload4j:reload4j
2.2 Logback
2.2.1 Maven
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 <dependencies > <dependency > <groupId > org.slf4j</groupId > <artifactId > slf4j-api</artifactId > <version > ${slf4j.version}</version > </dependency > <dependency > <groupId > ch.qos.logback</groupId > <artifactId > logback-classic</artifactId > <version > ${logback.version}</version > </dependency > <dependency > <groupId > ch.qos.logback</groupId > <artifactId > logback-core</artifactId > <version > ${logback.version}</version > </dependency > </dependencies >
2.2.2 Demo
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.2.3 Tips
2.2.3.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.2.3.2 Work with Unit Test
在test/resources
目录下添加logback-test.xml
文件即可生效
2.2.3.3 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.2.3.4 Spring-Boot默认的配置
参考org.springframework.boot.logging.logback.DefaultLogbackConfiguration
相关配置项参考spring-configuration-metadata.json
logging.pattern.console
:默认的console pattern
配置
logging.config
:用于指定spring
加载的logback
配置文件
2.2.3.5 关于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.2.4 参考
2.3 Tips
2.3.1 How to make sure there is only one logging implementation
Use mvn dependency:tree
to check dependencies:
log4j
:
log4j2
:
org.apache.logging.log4j:*
logback
:
reload4j
:
org.slf4j:slf4j-reload4j
ch.qos.reload4j:reload4j
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
10 JMH
比较的序列化框架如下
fastjson
kryo
hessian
java-builtin
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 <?xml version="1.0" encoding="UTF-8" ?> <project xmlns ="http://maven.apache.org/POM/4.0.0" xmlns:xsi ="http://www.w3.org/2001/XMLSchema-instance" 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 > jmh</artifactId > <version > 1.0.0</version > <modelVersion > 4.0.0</modelVersion > <dependencies > <dependency > <groupId > org.openjdk.jmh</groupId > <artifactId > jmh-core</artifactId > <version > 1.21</version > </dependency > <dependency > <groupId > org.openjdk.jmh</groupId > <artifactId > jmh-generator-annprocess</artifactId > <version > 1.21</version > </dependency > <dependency > <groupId > com.caucho</groupId > <artifactId > hessian</artifactId > <version > 4.0.51</version > </dependency > <dependency > <groupId > com.esotericsoftware</groupId > <artifactId > kryo</artifactId > <version > 4.0.2</version > </dependency > <dependency > <groupId > com.alibaba</groupId > <artifactId > fastjson</artifactId > <version > 1.2.62</version > </dependency > </dependencies > <build > <finalName > ${artifactId}</finalName > <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 > </plugins > </build > </project >
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 package org.liuyehcf.jmh.serialize;import org.openjdk.jmh.annotations.*;import org.openjdk.jmh.runner.Runner;import org.openjdk.jmh.runner.options.Options;import org.openjdk.jmh.runner.options.OptionsBuilder;import java.util.HashMap;import java.util.Map;import java.util.UUID;import java.util.concurrent.TimeUnit;@Warmup(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS) @Measurement(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS) @Fork(1) @BenchmarkMode(Mode.AverageTime) @OutputTimeUnit(TimeUnit.NANOSECONDS) public class SerializerCmp { private static final Map<String, String> MAP_SIZE_1 = new HashMap <>(); private static final Map<String, String> MAP_SIZE_10 = new HashMap <>(); private static final Map<String, String> MAP_SIZE_100 = new HashMap <>(); static { for (int i = 0 ; i < 1 ; i++) { MAP_SIZE_1.put(UUID.randomUUID().toString(), UUID.randomUUID().toString()); } for (int i = 0 ; i < 10 ; i++) { MAP_SIZE_10.put(UUID.randomUUID().toString(), UUID.randomUUID().toString()); } for (int i = 0 ; i < 100 ; i++) { MAP_SIZE_100.put(UUID.randomUUID().toString(), UUID.randomUUID().toString()); } } @Benchmark public void json_size_1 () { CloneUtils.jsonClone(MAP_SIZE_1); } @Benchmark public void kryo_size_1 () { CloneUtils.kryoClone(MAP_SIZE_1); } @Benchmark public void hessian_size_1 () { CloneUtils.hessianClone(MAP_SIZE_1); } @Benchmark public void java_size_1 () { CloneUtils.javaClone(MAP_SIZE_1); } @Benchmark public void json_size_10 () { CloneUtils.jsonClone(MAP_SIZE_10); } @Benchmark public void kryo_size_10 () { CloneUtils.kryoClone(MAP_SIZE_10); } @Benchmark public void hessian_size_10 () { CloneUtils.hessianClone(MAP_SIZE_10); } @Benchmark public void java_size_10 () { CloneUtils.javaClone(MAP_SIZE_10); } @Benchmark public void json_size_100 () { CloneUtils.jsonClone(MAP_SIZE_100); } @Benchmark public void kryo_size_100 () { CloneUtils.kryoClone(MAP_SIZE_100); } @Benchmark public void hessian_size_100 () { CloneUtils.hessianClone(MAP_SIZE_100); } @Benchmark public void java_size_100 () { CloneUtils.javaClone(MAP_SIZE_100); } public static void main (String[] args) throws Exception { Options opt = new OptionsBuilder () .include(SerializerCmp.class.getSimpleName()) .build(); new Runner (opt).run(); } }
注解含义解释
Warmup
:配置预热参数
iterations
: 预测执行次数
time
:每次执行的时间
timeUnit
:每次执行的时间单位
Measurement
:配置测试参数
iterations
: 测试执行次数
time
:每次执行的时间
timeUnit
:每次执行的时间单位
Fork
:总共运行几轮
BenchmarkMode
:衡量的指标,包括平均值,峰值等
OutputTimeUnit
:输出结果的时间单位
输出
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 ... Benchmark Mode Cnt Score Error Units SerializerCmp.hessian_size_1 avgt 5 5307.286 ± 327.709 ns/op SerializerCmp.hessian_size_10 avgt 5 8745.407 ± 248.948 ns/op SerializerCmp.hessian_size_100 avgt 5 47200.123 ± 2167.454 ns/op SerializerCmp.java_size_1 avgt 5 4959.845 ± 300.456 ns/op SerializerCmp.java_size_10 avgt 5 12948.638 ± 229.580 ns/op SerializerCmp.java_size_100 avgt 5 97033.259 ± 1465.232 ns/op SerializerCmp.json_size_1 avgt 5 631.037 ± 39.292 ns/op SerializerCmp.json_size_10 avgt 5 4364.215 ± 740.259 ns/op SerializerCmp.json_size_100 avgt 5 57205.805 ± 4211.084 ns/op SerializerCmp.kryo_size_1 avgt 5 1780.679 ± 84.639 ns/op SerializerCmp.kryo_size_10 avgt 5 5426.511 ± 264.606 ns/op SerializerCmp.kryo_size_100 avgt 5 48886.075 ± 13722.655 ns/op
10.1 参考
11 Antlr4
ANTLR (ANother Tool for Language Recognition) is a powerful parser generator for reading, processing, executing, or translating structured text or binary files.
Calculator Demo
12 httpclient
RetryConfig: new DefaultHttpRequestRetryHandler(3, true)
, this can retry for different IP addresses(maybe including both IPv4/IPv6) which DNS server returns
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 package org.byconity.common;import org.apache.http.client.config.RequestConfig;import org.apache.http.client.methods.CloseableHttpResponse;import org.apache.http.client.methods.HttpGet;import org.apache.http.impl.client.CloseableHttpClient;import org.apache.http.impl.client.DefaultHttpRequestRetryHandler;import org.apache.http.impl.client.HttpClients;import java.io.InputStream;import java.nio.file.Files;import java.nio.file.Path;import java.nio.file.Paths;import java.nio.file.StandardCopyOption;public class HttpClientDemo { public static void download (String url, String localPath) throws Exception { Path path = Paths.get(localPath); RequestConfig requestConfig = RequestConfig.custom().setConnectTimeout(5000 ).setSocketTimeout(5000 ).build(); try (CloseableHttpClient httpClient = HttpClients.custom() .setDefaultRequestConfig(requestConfig) .setRetryHandler(new DefaultHttpRequestRetryHandler (3 , true )).build()) { HttpGet request = new HttpGet (url); try (CloseableHttpResponse response = httpClient.execute(request)) { if (response.getStatusLine().getStatusCode() == 200 ) { InputStream inputStream = response.getEntity().getContent(); Files.copy(inputStream, path, StandardCopyOption.REPLACE_EXISTING); System.out.println("File downloaded: " + path); } else { System.out.println( "Failed to download file. HTTP Status: " + response.getStatusLine() .getStatusCode()); } } } } }