阅读更多
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()); } } } } }