常用开发库知识体系

转自:https://zhuanlan.zhihu.com/p/516151901

Home

常用开发库 - Apache Common包

Apache Commons是对JDK的拓展,包含了很多开源的工具,用于解决平时编程经常会遇到的问题,减少重复劳动。官网网址:http://commons.apache.org

常用包梳理

Commons BeanUtils

这玩意我没找到,我感觉org.springframework.beans.BeanUtils用的挺多哈。

Spring的BeanUtils(性能好),Apache的BeanUtils (性能差 不推荐使用)

https://www.cnblogs.com/Eric-F/p/9681996.html

https://wenku.baidu.com/view/52997bb4d9ef5ef7ba0d4a7302768e9951e76ec0.html

Commons Codec

是编码和解码组件,提供常用的编码和解码方法,如DES、SHA1、MD5、Base64、URL和Soundx等。

org.apache.commons.codec.digest.DigestUtils.md5Hex()

Commons Collections

是一个集合组件,扩展了Java标准Collections API,对常用的集合操作进行了很好的封装、抽象和补充,在保证性能的同时大大简化代码。目前更新的是org.apache.commons.collections4

我们先来浏览一下它的包结构。一共是12个:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
org.apache.commons.collections – CommonsCollections自定义的一组公用的接口和工具类

org.apache.commons.collections.bag – 实现Bag接口的一组类

org.apache.commons.collections.bidimap – 实现BidiMap系列接口的一组类

org.apache.commons.collections.buffer – 实现Buffer接口的一组类

org.apache.commons.collections.collection –实现java.util.Collection接口的一组类

org.apache.commons.collections.comparators– 实现java.util.Comparator接口的一组类

org.apache.commons.collections.functors –Commons Collections自定义的一组功能类

org.apache.commons.collections.iterators – 实现java.util.Iterator接口的一组类

org.apache.commons.collections.keyvalue – 实现集合和键/值映射相关的一组类

org.apache.commons.collections.list – 实现java.util.List接口的一组类

org.apache.commons.collections.map – 实现Map系列接口的一组类

org.apache.commons.collections.set – 实现Set系列接口的一组类
  • 作为容器类的补充,我们可以找到Bag、Buffer、BidiMap、OrderedMap等等;
  • 作为操作类的补充,我们可以找到CollectionUtils、IteratorUtils、ListUtils、SetUtils等等;
  • 作为辅助类的补充,我们可以找到MapIterator、Closure、Predicate、Transformer等等;

Commons Compress

是一个压缩、解压缩文件的组件,可以操作rar、cpio、Unix dump、tar、zip、gzip、XZ、Pack200和bzip2格式的压缩文件。

Commons Configuration

是一个Java应用程序的配置管理工具,可以从properties或者xml文件中加载配置信息。

Commons CSV

是一个用来读写各种Comma Separated Value(CSV)格式文件的Java类库。

Commons Daemon

实现将普通的Java应用变成系统的后台服务,例如 Tomcat 就是利用这个项目来实现作为 Linux 和 Windows 的服务启动和停止的。

Commons DBCP

数据库连接池。

Commons DBUtils

是JDBC工具组件,对传统操作数据库的类进行二次封装,可以把结果集转化成List。

Commons Digester

是XML到Java对象的映射工具集。

Commons Email

是邮件操作组件,对Java Mail API进行了封装,提供了常用的邮件发送和接收类,简化邮件操作。该组件依赖Java Mail API。

Commons Exec

提供一些常用的方法用来执行外部进程,如执行exe文件或命令行。

Commons FileUpload

为Web应用程序或Servlet提供文件上传功能,Struts2和SpringMVC的文件上传组件。

Commons IO

是处理IO的工具类包,对http://java.io进行扩展,提供了更加方便的IO操作。

Commons JCI

提供通用的Java编译器接口。

Commons Lang3

是处理Java基本对象方法的工具类包,该类包提供对字符、数组等基本对象的操作,弥补了java.lang api基本处理方法上的不足。

https://commons.apache.org/proper/commons-lang/javadocs/api-release/index.html

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
ArrayUtils – 用于对数组的操作,如添加、查找、删除、子数组、倒序、元素类型转换等;

BitField – 用于操作位元,提供了一些方便而安全的方法;

BooleanUtils – 用于操作和转换boolean或者Boolean及相应的数组;

CharEncoding – 包含了Java环境支持的字符编码,提供是否支持某种编码的判断;

CharRange – 用于设定字符范围并做相应检查;

CharSet – 用于设定一组字符作为范围并做相应检查;

CharSetUtils – 用于操作CharSet;

CharUtils – 用于操作char值和Character对象;

ClassUtils – 用于对Java类的操作,不使用反射;

ObjectUtils – 用于操作Java对象,提供null安全的访问和其他一些功能;

RandomStringUtils – 用于生成随机的字符串;

SerializationUtils – 用于处理对象序列化,提供比一般Java序列化更高级的处理能力;

StringEscapeUtils – 用于正确处理转义字符,产生正确的Java、JavaScript、HTML、XML和SQL代码;

StringUtils – 处理String的核心类,提供了相当多的功能;

SystemUtils – 在java.lang.System基础上提供更方便的访问,如用户路径、Java版本、时区、操作系统等判断;

Validate – 提供验证的操作,有点类似assert断言;

WordUtils – 用于处理单词大小写、换行等。

Commons Logging

提供统一的日志接口,同时兼顾轻量级和不依赖于具体的实现。类包给中间件/日志工具开发者一个简单的日志操作抽象,允许程序开发人员使用不同的具体日志实现工具。

Commons Math

轻量级自容器的数学和统计计算方法类包,包含大多数常用的数值算法。

Commons Net

封装了各种网络协议的客户端,支持FTP、NNTP、SMTP、POP3、Telnet等协议。

Commons Pool

提供了一整套用于实现对象池化的框架,以及若干各具特色的对象池实现,可以有效地减少处理对象池化时的工作量。类包用于提高像文件句柄、数据库连接、socket通信这类大对象的调用效率,简单的说就是一种对象一次创建多次使用的技术。

Commons Primitives

提供了一个更小,更快和更易使用的对Java基本类型的支持。

Commons Validator

提供了一个简单的、可扩展的框架来在一个XML文件中定义校验器(校验方法)和校验规则。支持校验规则的和错误消息的国际化。

Apache HttpClient

曾经是Apache Commons的子项目,后来独立出来。HttpClient简化HTTP客户端与服务器的各种通讯,实现HTTP客户端程序(也就是浏览器程序)的功能。

Common 包检索和使用

在这里检索吧:Apache Common 包

常用开发库 - Google Guava包

Google Guava简介

https://www.cnblogs.com/moongeek/p/12831296.html

Guava工程包含了若干被Google的 Java项目广泛依赖 的核心库,例如:集合 [collections] 、缓存 [caching] 、原生类型支持 [primitives support] 、并发库 [concurrency libraries] 、通用注解 [common annotations] 、字符串处理 [string processing] 、I/O 等等。 所有这些工具每天都在被Google的工程师应用在产品服务中。

guava的优点:

  • 高效设计良好的API,被Google的开发者设计,实现和使用
  • 遵循高效的java语法实践
  • 使代码更刻度,简洁,简单
  • 节约时间,资源,提高生产力

推荐网址

使用Guava

注意:JDK 1.8 or higher.

1
2
3
4
5
6
7
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>28.2-jre</version>
<!-- or, for Android: -->
<version>28.2-android</version>
</dependency>

常用开发库 - Hutool包

Hutool作为后起之秀,功能上也比较全。但是要注意一点,它的开源协议是:中国第一个开源协议木兰宽松许可证, 第1版(opens new window),对此在商业项目中需要谨慎些,在个人项目无所谓。所以公司项目没有使用这个的。

包含组件

一个Java基础工具类,对文件、流、加密解密、转码、正则、线程、XML等JDK方法进行封装,组成各种Util工具类,同时提供以下组件:

模块介绍
hutool-aopJDK动态代理封装,提供非IOC下的切面支持
hutool-bloomFilter布隆过滤,提供一些Hash算法的布隆过滤
hutool-cache简单缓存实现
hutool-core核心,包括Bean操作、日期、各种Util等
hutool-cron定时任务模块,提供类Crontab表达式的定时任务
hutool-crypto加密解密模块,提供对称、非对称和摘要算法封装
hutool-dbJDBC封装后的数据操作,基于ActiveRecord思想
hutool-dfa基于DFA模型的多关键字查找
hutool-extra扩展模块,对第三方封装(模板引擎、邮件、Servlet、二维码、Emoji、FTP、分词等)
hutool-http基于HttpUrlConnection的Http客户端封装
hutool-log自动识别日志实现的日志门面
hutool-script脚本执行封装,例如Javascript
hutool-setting功能更强大的Setting配置文件和Properties封装
hutool-system系统参数调用封装(JVM信息等)
hutool-jsonJSON实现
hutool-captcha图片验证码实现
hutool-poi针对POI中Excel的封装
hutool-socket基于Java的NIO和AIO的Socket封装

可以根据需求对每个模块单独引入,也可以通过引入hutool-all方式引入所有模块。

文档

安装

Maven

在项目的pom.xml的dependencies中加入以下内容:

1
2
3
4
5
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.1.0</version>
</dependency>

Gradle

1
compile 'cn.hutool:hutool-all:5.1.0'

非Maven项目

点击以下任一链接,下载hutool-all-X.X.X.jar即可:

注意 Hutool 5.x支持JDK8+,对Android平台没有测试,不能保证所有工具类获工具方法可用。 如果你的项目使用JDK7,请使用Hutool 4.x版本

编译安装

访问Hutool的码云主页:https://gitee.com/loolly/hutool(opens new window) 下载整个项目源码(v5-master或v5-dev分支都可)然后进入Hutool项目目录执行:

1
./hutool.sh install

然后就可以使用Maven引入了。

常用开发库 - Spring常用工具类

Spring作为常用的开发框架,在Spring框架应用中,排在ApacheCommon,Guava, Huool等通用库后,第二优先级可以考虑使用Spring-core-xxx.jar中的util包。

内置的resouce类型

  • UrlResource
  • ClassPathResource
  • FileSystemResource
  • ServletContextResource
  • InputStreamResource
  • ByteArrayResource
  • EncodedResource 也就是Resource加上encoding, 可以认为是有编码的资源
  • VfsResource(在jboss里经常用到, 相应还有 工具类 VfsUtils)
  • org.springframework.util.xml.ResourceUtils 用于处理表达资源字符串前缀描述资源的工具. 如: “classpath:”. 有 getURL, getFile, isFileURL, isJarURL, extractJarFileURL

工具类

  • org.springframework.core.annotation.AnnotationUtils 处理注解
  • org.springframework.core.io.support.PathMatchingResourcePatternResolver 用 于处理 ant 匹配风格(com/.jsp, com//.jsp),找出所有的资源, 结合上面的resource的概念一起使用,对于遍历文件很有用. 具体请详细查看javadoc
  • org.springframework.core.io.support.PropertiesLoaderUtils 加载Properties资源工具类,和Resource结合
  • org.springframework.core.BridgeMethodResolver 桥接方法分析器. 关于桥接方法请参考: http://java.sun.com/docs/books/jls/third_edition/html/expressions.html#15.12.4.5
  • org.springframework.core.GenericTypeResolver 范型分析器, 在用于对范型方法, 参数分析.
  • org.springframework.core.NestedExceptionUtils

xml工具

  • org.springframework.util.xml.AbstractStaxContentHandler
  • org.springframework.util.xml.AbstractStaxXMLReader
  • org.springframework.util.xml.AbstractXMLReader
  • org.springframework.util.xml.AbstractXMLStreamReader
  • org.springframework.util.xml.DomUtils
  • org.springframework.util.xml.SimpleNamespaceContext
  • org.springframework.util.xml.SimpleSaxErrorHandler
  • org.springframework.util.xml.SimpleTransformErrorListener
  • org.springframework.util.xml.StaxUtils
  • org.springframework.util.xml.TransformerUtils

其它工具集

  • org.springframework.util.xml.AntPathMatcherant风格的处理
  • org.springframework.util.xml.AntPathStringMatcher
  • org.springframework.util.xml.Assert断言,在我们的参数判断时应该经常用
  • org.springframework.util.xml.CachingMapDecorator
  • org.springframework.util.xml.ClassUtils用于Class的处理
  • org.springframework.util.xml.CollectionUtils用于处理集合的工具
  • org.springframework.util.xml.CommonsLogWriter
  • org.springframework.util.xml.CompositeIterator
  • org.springframework.util.xml.ConcurrencyThrottleSupport
  • org.springframework.util.xml.CustomizableThreadCreator
  • org.springframework.util.xml.DefaultPropertiesPersister
  • org.springframework.util.xml.DigestUtils摘要处理, 这里有用于md5处理信息的
  • org.springframework.util.xml.FileCopyUtils文件的拷贝处理, 结合Resource的概念一起来处理, 真的是很方便
  • org.springframework.util.xml.FileSystemUtils
  • org.springframework.util.xml.LinkedCaseInsensitiveMap key值不区分大小写的LinkedMap
  • org.springframework.util.xml.LinkedMultiValueMap一个key可以存放多个值的LinkedMap
  • org.springframework.util.xml.Log4jConfigurer一个log4j的启动加载指定配制文件的工具类
  • org.springframework.util.xml.NumberUtils处理数字的工具类, 有parseNumber 可以把字符串处理成我们指定的数字格式, 还支持format格式, convertNumberToTargetClass 可以实现Number类型的转化.
  • org.springframework.util.xml.ObjectUtils有很多处理null object的方法. 如nullSafeHashCode, nullSafeEquals, isArray, containsElement, addObjectToArray, 等有用的方法
  • org.springframework.util.xml.PatternMatchUtilsspring里用于处理简单的匹配. 如 Spring’s typical “xxx”, “xxx” and “xxx” pattern styles
  • org.springframework.util.xml.PropertyPlaceholderHelper用于处理占位符的替换
  • org.springframework.util.xml.ReflectionUtils反映常用工具方法. 有 findField, setField, getField, findMethod, invokeMethod等有用的方法
  • org.springframework.util.xml.SerializationUtils用于java的序列化与反序列化. serialize与deserialize方法
  • org.springframework.util.xml.StopWatch一个很好的用于记录执行时间的工具类, 且可以用于任务分阶段的测试时间. 最后支持一个很好看的打印格式. 这个类应该经常用
  • org.springframework.util.xml.StringUtils
  • org.springframework.util.xml.SystemPropertyUtils
  • org.springframework.util.xml.TypeUtils用于类型相容的判断. isAssignable
  • org.springframework.util.xml.WeakReferenceMonitor弱引用的监控

和web相关的工具

  • org.springframework.web.util.CookieGenerator

  • org.springframework.web.util.HtmlCharacterEntityDecoder

  • org.springframework.web.util.HtmlCharacterEntityReferences

  • org.springframework.web.util.HtmlUtils

  • org.springframework.web.util.HttpUrlTemplate

  • 这个类用于用字符串模板构建url, 它会自动处理url里的汉字及其它相关的编码. 在读取别人提供的url资源时, 应该经常用

  • String url = “http://localhost/myapp/{name}/{id}“;

  • org.springframework.web.util.JavaScriptUtils

  • org.springframework.web.util.Log4jConfigListener

  • 用listener的方式来配制log4j在web环境下的初始化

  • org.springframework.web.util.UriTemplate

  • org.springframework.web.util.UriUtils处理uri里特殊字符的编码

  • org.springframework.web.util.WebUtils

常用开发库 - 日志类库详解

Java日志库是最能体现Java库在进化中的渊源关系的,在理解时重点理解日志框架本身和日志门面,以及比较好的实践等。要关注其历史渊源和设计(比如桥接),而具体在使用时查询接口即可, 否则会陷入JUL(Java Util Log), JCL(Commons Logging), Log4j, SLF4J, Logback,Log4j2傻傻分不清楚的境地。

日志库简介

  • 最重要的一点是 区分日志系统日志门面;
  • 其次是日志库的使用, 包含配置与API使用;配置侧重于日志系统的配置,API使用侧重于日志门面;
  • 最后是选型,改造和最佳实践等

日志库之日志系统

java.util.logging (JUL)

JDK1.4 开始,通过 java.util.logging 提供日志功能。虽然是官方自带的log lib,JUL的使用确不广泛。主要原因:

  • JUL从JDK1.4 才开始加入(2002年),当时各种第三方log lib已经被广泛使用了
  • JUL早期存在性能问题,到JDK1.5上才有了不错的进步,但现在和Logback/Log4j2相比还是有所不如
  • JUL的功能不如Logback/Log4j2等完善,比如Output Handler就没有Logback/Log4j2的丰富,有时候需要自己来继承定制,又比如默认没有从ClassPath里加载配置文件的功能

Log4j

Log4j 是 apache 的一个开源项目,创始人 Ceki Gulcu。Log4j 应该说是 Java 领域资格最老,应用最广的日志工具。Log4j 是高度可配置的,并可通过在运行时的外部文件配置。它根据记录的优先级别,并提供机制,以指示记录信息到许多的目的地,诸如:数据库,文件,控制台,UNIX 系统日志等。

Log4j 中有三个主要组成部分:

  • loggers - 负责捕获记录信息。
  • appenders - 负责发布日志信息,以不同的首选目的地。
  • layouts - 负责格式化不同风格的日志信息。

官网地址:http://logging.apache.org/log4j/2.x/(opens new window)

Log4j 的短板在于性能,在Logback 和 Log4j2 出来之后,Log4j的使用也减少了。

Logback

Logback 是由 log4j 创始人 Ceki Gulcu 设计的又一个开源日志组件,是作为 Log4j 的继承者来开发的,提供了性能更好的实现,异步 logger,Filter等更多的特性。

logback 当前分成三个模块:logback-core、logback-classic 和 logback-access。

  • logback-core - 是其它两个模块的基础模块。
  • logback-classic - 是 log4j 的一个 改良版本。此外 logback-classic 完整实现 SLF4J API 使你可以很方便地更换成其它日志系统如 log4j 或 JDK14 Logging。
  • logback-access - 访问模块与 Servlet 容器集成提供通过 Http 来访问日志的功能。

官网地址: http://logback.qos.ch/

Log4j2

维护 Log4j 的人为了性能又搞出了 Log4j2。

Log4j2 和 Log4j1.x 并不兼容,设计上很大程度上模仿了 SLF4J/Logback,性能上也获得了很大的提升。

Log4j2 也做了 Facade/Implementation 分离的设计,分成了 log4j-api 和 log4j-core。

官网地址: http://logging.apache.org/log4j/2.x/

Log4j vs Logback vs Log4j2

从性能上Log4J2要强,但从生态上Logback+SLF4J优先。

初步对比

撇开血统不谈,比较一下log4j2和logback:

  • log4j2比logback更新:log4j2的GA版在2014年底才推出,比logback晚了好几年,这期间log4j2确实吸收了slf4j和logback的一些优点(比如日志模板),同时应用了不少的新技术
  • 由于采用了更先进的锁机制和LMAX Disruptor库,log4j2的性能优于logback,特别是在多线程环境下和使用异步日志的环境下
  • 二者都支持Filter(应该说是log4j2借鉴了logback的Filter),能够实现灵活的日志记录规则(例如仅对一部分用户记录debug级别的日志)
  • 二者都支持对配置文件的动态更新
  • 二者都能够适配slf4j,logback与slf4j的适配应该会更好一些,毕竟省掉了一层适配库
  • logback能够自动压缩/删除旧日志
  • logback提供了对日志的HTTP访问功能
  • log4j2实现了“无垃圾”和“低垃圾”模式。简单地说,log4j2在记录日志时,能够重用对象(如String等),尽可能避免实例化新的临时对象,减少因日志记录产生的垃圾对象,减少垃圾回收带来的性能下降
  • log4j2和logback各有长处,总体来说,如果对性能要求比较高的话,log4j2相对还是较优的选择。

日志库之日志门面

common-logging

common-logging 是 apache 的一个开源项目。也称Jakarta Commons Logging,缩写 JCL。

common-logging 的功能是提供日志功能的 API 接口,本身并不提供日志的具体实现(当然,common-logging 内部有一个 Simple logger 的简单实现,但是功能很弱,直接忽略),而是在运行时动态的绑定日志实现组件来工作(如 log4j、java.util.loggin)。

官网地址: http://commons.apache.org/proper/commons-logging/(opens new window)

slf4j

全称为 Simple Logging Facade for Java,即 java 简单日志门面。

类似于 Common-Logging,slf4j 是对不同日志框架提供的一个 API 封装,可以在部署的时候不修改任何配置即可接入一种日志实现方案。但是,slf4j 在编译时静态绑定真正的 Log 库。使用 SLF4J 时,如果你需要使用某一种日志实现,那么你必须选择正确的 SLF4J 的 jar 包的集合(各种桥接包)。

官网地址: http://www.slf4j.org/

common-logging vs slf4j

slf4j 库类似于 Apache Common-Logging。但是,他在编译时静态绑定真正的日志库。这点似乎很麻烦,其实也不过是导入桥接 jar 包而已。

slf4j 一大亮点是提供了更方便的日志记录方式:

不需要使用logger.isDebugEnabled()来解决日志因为字符拼接产生的性能问题。slf4j 的方式是使用{}作为字符串替换符,形式如下:

1
logger.debug("id: {}, name: {} ", id, name);

日志库使用方案

使用日志解决方案基本可分为三步:

  • 引入 jar 包
  • 配置
  • 使用 API

常见的各种日志解决方案的第 2 步和第 3 步基本一样,实施上的差别主要在第 1 步,也就是使用不同的库。

日志库jar包

这里首选推荐使用 slf4j + logback 的组合。

如果你习惯了 common-logging,可以选择 common-logging+log4j。

强烈建议不要直接使用日志实现组件(logback、log4j、java.util.logging),理由前面也说过,就是无法灵活替换日志库。

还有一种情况:你的老项目使用了 common-logging,或是直接使用日志实现组件。如果修改老的代码,工作量太大,需要兼容处理。在下文,都将看到各种应对方法。

注:据我所知,当前仍没有方法可以将 slf4j 桥接到 common-logging。如果我孤陋寡闻了,请不吝赐教。

slf4j 直接绑定日志组件

  • slf4j + logback

添加依赖到 pom.xml 中即可。

logback-classic-1.0.13.jar 会自动将 slf4j-api-1.7.21.jar 和 logback-core-1.0.13.jar 也添加到你的项目中。

1
2
3
4
5
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>1.0.13</version>
</dependency>
  • slf4j + log4j

添加依赖到 pom.xml 中即可。

slf4j-log4j12-1.7.21.jar 会自动将 slf4j-api-1.7.21.jar 和 log4j-1.2.17.jar 也添加到你的项目中。

1
2
3
4
5
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
<version>1.7.21</version>
</dependency>
  • slf4j + java.util.logging

添加依赖到 pom.xml 中即可。

slf4j-jdk14-1.7.21.jar 会自动将 slf4j-api-1.7.21.jar 也添加到你的项目中。

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

日志库配置 - 针对于日志框架

log4j2 配置

log4j2 基本配置形式如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<?xml version="1.0" encoding="UTF-8"?>;
<Configuration>
<Properties>
<Property name="name1">value</property>
<Property name="name2" value="value2"/>
</Properties>
<Filter type="type" ... />
<Appenders>
<Appender type="type" name="name">
<Filter type="type" ... />
</Appender>
...
</Appenders>
<Loggers>
<Logger name="name1">
<Filter type="type" ... />
</Logger>
...
<Root level="level">
<AppenderRef ref="name"/>
</Root>
</Loggers>
</Configuration>

配置示例:

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
<?xml version="1.0" encoding="UTF-8"?>
<Configuration status="debug" strict="true" name="XMLConfigTest"
packages="org.apache.logging.log4j.test">
<Properties>
<Property name="filename">target/test.log</Property>
</Properties>
<Filter type="ThresholdFilter" level="trace"/>

<Appenders>
<Appender type="Console" name="STDOUT">
<Layout type="PatternLayout" pattern="%m MDC%X%n"/>
<Filters>
<Filter type="MarkerFilter" marker="FLOW" onMatch="DENY" onMismatch="NEUTRAL"/>
<Filter type="MarkerFilter" marker="EXCEPTION" onMatch="DENY" onMismatch="ACCEPT"/>
</Filters>
</Appender>
<Appender type="Console" name="FLOW">
<Layout type="PatternLayout" pattern="%C{1}.%M %m %ex%n"/><!-- class and line number -->
<Filters>
<Filter type="MarkerFilter" marker="FLOW" onMatch="ACCEPT" onMismatch="NEUTRAL"/>
<Filter type="MarkerFilter" marker="EXCEPTION" onMatch="ACCEPT" onMismatch="DENY"/>
</Filters>
</Appender>
<Appender type="File" name="File" fileName="${filename}">
<Layout type="PatternLayout">
<Pattern>%d %p %C{1.} [%t] %m%n</Pattern>
</Layout>
</Appender>
</Appenders>

<Loggers>
<Logger name="org.apache.logging.log4j.test1" level="debug" additivity="false">
<Filter type="ThreadContextMapFilter">
<KeyValuePair key="test" value="123"/>
</Filter>
<AppenderRef ref="STDOUT"/>
</Logger>

<Logger name="org.apache.logging.log4j.test2" level="debug" additivity="false">
<AppenderRef ref="File"/>
</Logger>

<Root level="trace">
<AppenderRef ref="STDOUT"/>
</Root>
</Loggers>

</Configuration>

logback 配置

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
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
<?xml version="1.0" encoding="UTF-8" ?>

<!-- logback中一共有5种有效级别,分别是TRACE、DEBUG、INFO、WARN、ERROR,优先级依次从低到高 -->
<configuration scan="true" scanPeriod="60 seconds" debug="false">

<property name="DIR_NAME" value="spring-helloworld"/>

<!-- 将记录日志打印到控制台 -->
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d{HH:mm:ss.SSS} [%thread] [%-5p] %c{36}.%M - %m%n</pattern>
</encoder>
</appender>

<!-- RollingFileAppender begin -->
<appender name="ALL" class="ch.qos.logback.core.rolling.RollingFileAppender">
<!-- 根据时间来制定滚动策略 -->
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>${user.dir}/logs/${DIR_NAME}/all.%d{yyyy-MM-dd}.log</fileNamePattern>
<maxHistory>30</maxHistory>
</rollingPolicy>

<!-- 根据文件大小来制定滚动策略 -->
<triggeringPolicy class="ch.qos.logback.core.rolling.SizeBasedTriggeringPolicy">
<maxFileSize>30MB</maxFileSize>
</triggeringPolicy>

<encoder>
<pattern>%d{HH:mm:ss.SSS} [%thread] [%-5p] %c{36}.%M - %m%n</pattern>
</encoder>
</appender>

<appender name="ERROR" class="ch.qos.logback.core.rolling.RollingFileAppender">
<!-- 根据时间来制定滚动策略 -->
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>${user.dir}/logs/${DIR_NAME}/error.%d{yyyy-MM-dd}.log</fileNamePattern>
<maxHistory>30</maxHistory>
</rollingPolicy>

<!-- 根据文件大小来制定滚动策略 -->
<triggeringPolicy class="ch.qos.logback.core.rolling.SizeBasedTriggeringPolicy">
<maxFileSize>10MB</maxFileSize>
</triggeringPolicy>

<filter class="ch.qos.logback.classic.filter.LevelFilter">
<level>ERROR</level>
<onMatch>ACCEPT</onMatch>
<onMismatch>DENY</onMismatch>
</filter>

<encoder>
<pattern>%d{HH:mm:ss.SSS} [%thread] [%-5p] %c{36}.%M - %m%n</pattern>
</encoder>
</appender>

<appender name="WARN" class="ch.qos.logback.core.rolling.RollingFileAppender">
<!-- 根据时间来制定滚动策略 -->
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>${user.dir}/logs/${DIR_NAME}/warn.%d{yyyy-MM-dd}.log</fileNamePattern>
<maxHistory>30</maxHistory>
</rollingPolicy>

<!-- 根据文件大小来制定滚动策略 -->
<triggeringPolicy class="ch.qos.logback.core.rolling.SizeBasedTriggeringPolicy">
<maxFileSize>10MB</maxFileSize>
</triggeringPolicy>

<filter class="ch.qos.logback.classic.filter.LevelFilter">
<level>WARN</level>
<onMatch>ACCEPT</onMatch>
<onMismatch>DENY</onMismatch>
</filter>

<encoder>
<pattern>%d{HH:mm:ss.SSS} [%thread] [%-5p] %c{36}.%M - %m%n</pattern>
</encoder>
</appender>

<appender name="INFO" class="ch.qos.logback.core.rolling.RollingFileAppender">
<!-- 根据时间来制定滚动策略 -->
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>${user.dir}/logs/${DIR_NAME}/info.%d{yyyy-MM-dd}.log</fileNamePattern>
<maxHistory>30</maxHistory>
</rollingPolicy>

<!-- 根据文件大小来制定滚动策略 -->
<triggeringPolicy class="ch.qos.logback.core.rolling.SizeBasedTriggeringPolicy">
<maxFileSize>10MB</maxFileSize>
</triggeringPolicy>

<filter class="ch.qos.logback.classic.filter.LevelFilter">
<level>INFO</level>
<onMatch>ACCEPT</onMatch>
<onMismatch>DENY</onMismatch>
</filter>

<encoder>
<pattern>%d{HH:mm:ss.SSS} [%thread] [%-5p] %c{36}.%M - %m%n</pattern>
</encoder>
</appender>

<appender name="DEBUG" class="ch.qos.logback.core.rolling.RollingFileAppender">
<!-- 根据时间来制定滚动策略 -->
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>${user.dir}/logs/${DIR_NAME}/debug.%d{yyyy-MM-dd}.log</fileNamePattern>
<maxHistory>30</maxHistory>
</rollingPolicy>

<!-- 根据文件大小来制定滚动策略 -->
<triggeringPolicy class="ch.qos.logback.core.rolling.SizeBasedTriggeringPolicy">
<maxFileSize>10MB</maxFileSize>
</triggeringPolicy>

<filter class="ch.qos.logback.classic.filter.LevelFilter">
<level>DEBUG</level>
<onMatch>ACCEPT</onMatch>
<onMismatch>DENY</onMismatch>
</filter>

<encoder>
<pattern>%d{HH:mm:ss.SSS} [%thread] [%-5p] %c{36}.%M - %m%n</pattern>
</encoder>
</appender>

<appender name="TRACE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<!-- 根据时间来制定滚动策略 -->
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>${user.dir}/logs/${DIR_NAME}/trace.%d{yyyy-MM-dd}.log</fileNamePattern>
<maxHistory>30</maxHistory>
</rollingPolicy>

<!-- 根据文件大小来制定滚动策略 -->
<triggeringPolicy class="ch.qos.logback.core.rolling.SizeBasedTriggeringPolicy">
<maxFileSize>10MB</maxFileSize>
</triggeringPolicy>

<filter class="ch.qos.logback.classic.filter.LevelFilter">
<level>TRACE</level>
<onMatch>ACCEPT</onMatch>
<onMismatch>DENY</onMismatch>
</filter>

<encoder>
<pattern>%d{HH:mm:ss.SSS} [%thread] [%-5p] %c{36}.%M - %m%n</pattern>
</encoder>
</appender>

<appender name="SPRING" class="ch.qos.logback.core.rolling.RollingFileAppender">
<!-- 根据时间来制定滚动策略 -->
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>${user.dir}/logs/${DIR_NAME}/springframework.%d{yyyy-MM-dd}.log
</fileNamePattern>
<maxHistory>30</maxHistory>
</rollingPolicy>

<!-- 根据文件大小来制定滚动策略 -->
<triggeringPolicy class="ch.qos.logback.core.rolling.SizeBasedTriggeringPolicy">
<maxFileSize>10MB</maxFileSize>
</triggeringPolicy>

<encoder>
<pattern>%d{HH:mm:ss.SSS} [%thread] [%-5p] %c{36}.%M - %m%n</pattern>
</encoder>
</appender>
<!-- RollingFileAppender end -->

<!-- logger begin -->
<!-- 本项目的日志记录,分级打印 -->
<logger name="org.zp.notes.spring" level="TRACE" additivity="false">
<appender-ref ref="STDOUT"/>
<appender-ref ref="ERROR"/>
<appender-ref ref="WARN"/>
<appender-ref ref="INFO"/>
<appender-ref ref="DEBUG"/>
<appender-ref ref="TRACE"/>
</logger>

<!-- SPRING框架日志 -->
<logger name="org.springframework" level="WARN" additivity="false">
<appender-ref ref="SPRING"/>
</logger>

<root level="TRACE">
<appender-ref ref="ALL"/>
</root>
<!-- logger end -->

</configuration>

日志库API - 针对于日志门面

slf4j 用法

使用 slf4j 的 API 很简单。使用LoggerFactory初始化一个Logger实例,然后调用 Logger 对应的打印等级函数就行了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class App {
private static final Logger log = LoggerFactory.getLogger(App.class);
public static void main(String[] args) {
String msg = "print log, current level: {}";
log.trace(msg, "trace");
log.debug(msg, "debug");
log.info(msg, "info");
log.warn(msg, "warn");
log.error(msg, "error");
}
}

common-logging 用法

common-logging 用法和 slf4j 几乎一样,但是支持的打印等级多了一个更高级别的:fatal。

此外,common-logging 不支持{}替换参数,你只能选择拼接字符串这种方式了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

public class JclTest {
private static final Log log = LogFactory.getLog(JclTest.class);

public static void main(String[] args) {
String msg = "print log, current level: ";
log.trace(msg + "trace");
log.debug(msg + "debug");
log.info(msg + "info");
log.warn(msg + "warn");
log.error(msg + "error");
log.fatal(msg + "fatal");
}
}

日志库选型与改造

对Java日志组件选型的建议

slf4j已经成为了Java日志组件的明星选手,可以完美替代JCL,使用JCL桥接库也能完美兼容一切使用JCL作为日志门面的类库,现在的新系统已经没有不使用slf4j作为日志API的理由了。

日志记录服务方面,log4j在功能上输于logback和log4j2,在性能方面log4j2则全面超越log4j和logback。所以新系统应该在logback和log4j2中做出选择,对于性能有很高要求的系统,应优先考虑log4j2

对日志架构使用比较好的实践

总是使用Log Facade,而不是具体Log Implementation

正如之前所说的,使用 Log Facade 可以方便的切换具体的日志实现。而且,如果依赖多个项目,使用了不同的Log Facade,还可以方便的通过 Adapter 转接到同一个实现上。如果依赖项目使用了多个不同的日志实现,就麻烦的多了。

具体来说,现在推荐使用 Log4j-API 或者 SLF4j,不推荐继续使用 JCL。

只添加一个 Log Implementation依赖

毫无疑问,项目中应该只使用一个具体的 Log Implementation,建议使用 Logback 或者Log4j2。如果有依赖的项目中,使用的 Log Facade不支持直接使用当前的 Log Implementation,就添加合适的桥接器依赖。具体的桥接关系可以看上一篇文章的图。

具体的日志实现依赖应该设置为optional和使用runtime scope

在项目中,Log Implementation的依赖强烈建议设置为runtime scope,并且设置为optional。例如项目中使用了 SLF4J 作为 Log Facade,然后想使用 Log4j2 作为 Implementation,那么使用 maven 添加依赖的时候这样设置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-core</artifactId>
<version>${log4j.version}</version>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-slf4j-impl</artifactId>
<version>${log4j.version}</version>
<scope>runtime</scope>
<optional>true</optional>
</dependency>

设为optional,依赖不会传递,这样如果你是个lib项目,然后别的项目使用了你这个lib,不会被引入不想要的Log Implementation 依赖;

https://blog.csdn.net/qq_42449963/article/details/105439486

Scope设置为runtime,是为了防止开发人员在项目中直接使用Log Implementation中的类,而不适用Log Facade中的类。

https://blog.csdn.net/festone000/article/details/119425651

如果有必要, 排除依赖的第三方库中的Log Impementation依赖

这是很常见的一个问题,第三方库的开发者未必会把具体的日志实现或者桥接器的依赖设置为optional,然后你的项目继承了这些依赖——具体的日志实现未必是你想使用的,比如他依赖了Log4j,你想使用Logback,这时就很尴尬。另外,如果不同的第三方依赖使用了不同的桥接器和Log实现,也极容易形成环。

这种情况下,推荐的处理方法,是使用exclude来排除所有的这些Log实现和桥接器的依赖,只保留第三方库里面对Log Facade的依赖。

比如阿里的JStorm就没有很好的处理这个问题,依赖jstorm会引入对Logback和log4j-over-slf4j的依赖,如果你想在自己的项目中使用Log4j或其他Log实现的话,就需要加上excludes:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<dependency>
<groupId>com.alibaba.jstorm</groupId>
<artifactId>jstorm-core</artifactId>
<version>2.1.1</version>
<exclusions>
<exclusion>
<groupId>org.slf4j</groupId>
<artifactId>log4j-over-slf4j</artifactId>
</exclusion>
<exclusion>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
</exclusion>
</exclusions>
</dependency>

避免为不会输出的log付出代价

Log库都可以灵活的设置输出界别,所以每一条程序中的log,都是有可能不会被输出的。这时候要注意不要额外的付出代价。

先看两个有问题的写法:

1
2
logger.debug("start process request, url: " + url);
logger.debug("receive request: {}", toJson(request));

避免为不会输出的log付出代价

Log库都可以灵活的设置输出界别,所以每一条程序中的log,都是有可能不会被输出的。这时候要注意不要额外的付出代价。

先看两个有问题的写法:

1
2
logger.debug("start process request, url: " + url);
logger.debug("receive request: {}", toJson(request));

第一条是直接做了字符串拼接,所以即使日志级别高于debug也会做一个字符串连接操作;第二条虽然用了SLF4J/Log4j2 中的懒求值方式来避免不必要的字符串拼接开销,但是toJson()这个函数却是都会被调用并且开销更大。

推荐的写法如下:

1
2
3
4
5
6
logger.debug("start process request, url:{}", url); // SLF4J/LOG4J2
logger.debug("receive request: {}", () -> toJson(request)); // LOG4J2
logger.debug(() -> "receive request: " + toJson(request)); // LOG4J2
if (logger.isDebugEnabled()) { // SLF4J/LOG4J2
logger.debug("receive request: " + toJson(request));
}

日志格式中最好不要使用行号,函数名等字段

原因是,为了获取语句所在的函数名,或者行号,log库的实现都是获取当前的stacktrace,然后分析取出这些信息,而获取stacktrace的代价是很昂贵的。如果有很多的日志输出,就会占用大量的CPU。在没有特殊需要的情况下,建议不要在日志中输出这些这些字段。

最后, log中不要输出稀奇古怪的字符!

部分开发人员为了方便看到自己的log,会在log语句中加上醒目的前缀,比如:

1
logger.debug("========================start process request=============");

虽然对于自己来说是方便了,但是如果所有人都这样来做的话,那log输出就没法看了!正确的做法是使用grep 来看只自己关心的日志。

对现有系统日志架构的改造建议

如果现有系统使用JCL作为日志门面,又确实面临着JCL的ClassLoader机制带来的问题,完全可以引入slf4j并通过桥接库将JCL api输出的日志桥接至slf4j,再通过适配库适配至现有的日志输出服务(如log4j),如下图:

这样做不需要任何代码级的改造,就可以解决JCL的ClassLoader带来的问题,但没有办法享受日志模板等slf4j的api带来的优点。不过之后在现系统上开发的新功能就可以使用slf4j的api了,老代码也可以分批进行改造。

如果现有系统使用JCL作为日志门面,又头疼JCL不支持logback和log4j2等新的日志服务,也可以通过桥接库以slf4j替代JCL,但同样无法直接享受slf4j api的优点。

如果想要使用slf4j的api,那么就不得不进行代码改造了,当然改造也可以参考1中提到的方式逐步进行。

如果现系统面临着log4j的性能问题,可以使用Apache Logging提供的log4j到log4j2的桥接库log4j-1.2-api,把通过log4j api输出的日志桥接至log4j2。这样可以最快地使用上log4j2的先进性能,但组件中缺失了slf4j,对后续进行日志架构改造的灵活性有影响。另一种办法是先把log4j桥接至slf4j,再使用slf4j到log4j2的适配库。这样做稍微麻烦了一点,但可以逐步将系统中的日志输出标准化为使用slf4j的api,为后面的工作打好基础。

常用开发库 - JSON库详解

JSON应用非常广泛,对于Java常用的JSON库要完全掌握; 其中考虑到FastJson代码质量,漏洞,坑等等,应该尽量避免使用。

JSON简介

JSON是什么

  • JSON 指的是 JavaScript 对象表示法(JavaScript Object Notation)
  • JSON 是轻量级的文本数据交换格式
  • JSON 独立于语言:JSON 使用 Javascript语法来描述数据对象,但是 JSON 仍然独立于语言和平台。JSON 解析器和 JSON 库支持许多不同的编程语言。 目前非常多的动态(PHP,JSP,.NET)编程语言都支持JSON。
  • JSON 具有自我描述性,更易理解

结构与类型

  • 只有两种结构:对象内的键值对集合结构和数组,对象用{}表示、内部是”key”:”value”,数组用[]表示,不同值用逗号分开
  • 基本数值有7个: false / null / true / object / array / number / string
  • 再加上结构可以嵌套,进而可以用来表达复杂的数据

一个简单实例

JSON优秀资源

JSON在线解析工具

JSON类库

Java中并没有内置JSON的解析,因此使用JSON需要借助第三方类库。

下面是几个常用的 JSON 解析类库:

  • FastJson: 阿里巴巴开发的 JSON 库,性能优秀。
  • Jackson: 社区十分活跃且更新速度很快。
  • Gson: 谷歌开发的 JSON 库,功能十分全面。

性能测试对比

从下面的测试结果可以看出,序列化次数比较小的时候,Gson性能最好,当不断增加的时候到了100000,Gson明显弱于Jackson和FastJson, 这时候FastJson性能是真的牛,另外还可以看到不管数量少还是多,Jackson一直表现优异。而那个Json-lib可以直接忽略。

  • JSON序列化性能

  • JSON反序列化性能

更多请参考: Java几种常用JSON库性能比较(opens new window)

FastJson

先泼一盆冷水,个人非常不推荐使用FastJson, 为什么?

  • FastJson 源码质量较低
  • FastJson Bug、漏洞较多
  • FastJson 牺牲多数场景下的稳定性而提高的效率

Fastjson 简介

Fastjson 是一个 Java 库,可以将 Java 对象转换为 JSON 格式,当然它也可以将 JSON 字符串转换为 Java 对象。

Fastjson 可以操作任何 Java 对象,即使是一些预先存在的没有源码的对象。

Fastjson 特性

  • 提供服务器端、安卓客户端两种解析工具,性能表现较好。
  • 提供了 toJSONString() 和 parseObject() 方法来将 Java 对象与 JSON 相互转换。调用toJSONString方 法即可将对象转换成 JSON 字符串,parseObject 方法则反过来将 JSON 字符串转换成对象。
  • 允许转换预先存在的无法修改的对象(只有class、无源代码)。
  • Java泛型的广泛支持。
  • 允许对象的自定义表示、允许自定义序列化类。
  • 支持任意复杂对象(具有深厚的继承层次和广泛使用的泛型类型)。

下载和使用

你可以在 maven 中央仓库中直接下载:http://repo1.maven.org/maven2/com/alibaba/fastjson/(opens new window)

配置 maven 依赖:

1
2
3
4
5
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>x.x.x</version>
</dependency>

其中 x.x.x 是版本号,根据需要使用特定版本,建议使用最新版本。

序列化一个对象成JSON字符串

1
2
3
4
5
6
7
User user = new User();
user.setName("校长");
user.setAge(3);
user.setSalary(new BigDecimal("123456789.0123"));
String jsonString = JSON.toJSONString(user);
System.out.println(jsonString);
// 输出 {"age":3,"name":"校长","old":false,"salary":123456789.0123}

反序列化一个JSON字符串成Java对象

1
2
3
4
5
6
7
8
9
String jsonString = "{\"age\":3,\"birthdate\":1496738822842,\"name\":\"校长\",\"old\":true,\"salary\":123456789.0123}";
User u = JSON.parseObject(jsonString ,User.class);
System.out.println(u.getName());
// 输出 校长

String jsonStringArray = "[{\"age\":3,\"birthdate\":1496738822842,\"name\":\"校长\",\"old\":true,\"salary\":123456789.0123}]";
List<User> userList = JSON.parseArray(jsonStringArray, User.class);
System.out.println(userList.size());
// 输出 1

FastJson漏洞问题

尽量使用最新版本。

好了,我要开喷了。

DANGER

远离FastJson这个库,老程序员都知道这里有多少坑:

JackSon

JackSon简介

Jackson组件

3个核心模块:

  • Streaming: jackson-core jar,定义了底层的streaming API和实现了Json特性。
  • Annotations: jackson-annotations jar,包含了标准的Jackson注解。本文暂不介绍。
  • Databind: jackson-databind jar,实现了数据绑定和对象序列化,它依赖于streaming和annotations的包。

第三方数据类型模块

这些扩展是插件式的Jackson模块,用ObjectMapper.registerModule()注册,并且通过添加serializers和deserializers以便Databind包(ObjectMapper / ObjectReader / ObjectWriter)可以读写这些类型,来增加对各种常用的Java库的数据类型的支持。

数据格式模块

Jackson也有处理程序对JAX-RS标准实现者例如Jersey, RESTeasy, CXF等提供了数据格式支持。处理程序实现了MessageBodyReader和MessageBodyWriter,目前支持的数据格式包括JSON, Smile, XML, YAML和CBOR。

数据格式提供了除了Json之外的数据格式支持,它们绝大部分仅仅实现了streaming API abstractions,以便数据绑定组件可以按照原来的方式使用。另一些(几乎不需要)提供了databind标准功能来处理例如schemas。

Jackson的使用

引用maven jar包:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<!-- https://mvnrepository.com/artifact/com.fasterxml.jackson.core/jackson-core -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-core</artifactId>
<version>2.10.1</version>
</dependency>
<!-- https://mvnrepository.com/artifact/com.fasterxml.jackson.core/jackson-databind -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.10.1</version>
</dependency>
<!-- https://mvnrepository.com/artifact/com.fasterxml.jackson.core/jackson-annotations -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-annotations</artifactId>
<version>2.10.1</version>
</dependency>

序列化一个对象成JSON字符串

1
2
3
4
5
6
7
8
9
10
11
12
public void toJson() throws JsonProcessingException {

ObjectMapper mapper = new ObjectMapper();

City case1 = new City();
case1.setCity("SZ");
case1.setAge(123);

String jsonStr = mapper.writeValueAsString(case1);
System.out.println("JSON:" + jsonStr);
}
// 输出:JSON:{"city":"SZ","age":123}

反序列化一个JSON字符串成Java对象

1
2
3
4
5
6
7
8
public void toObj() throws JsonParseException, JsonMappingException, IOException {
ObjectMapper mapper = new ObjectMapper();
String inputjsonstr = "{\"city\":\"SZ\",\"age\":123}";

City readcase = mapper.readValue(inputjsonstr, City.class);

System.out.println("city info:" + readcase);
}

如果里面有未知属性,比如json中有desc字段,但是City中没有相应字段,会报错, 需要设置如下:

1
mapper.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);

常用注解

  • @JsonProperty(“xxx”): 将当前的属性名在json字符串中重新命名为当前设置的这个值,比如在示例中,将age–>mAge
  • @JsonIgnore: 将被标注的属性在生成json字符串的时候,直接忽略
  • @JsonInclude: 是一个类级别的设置,JsonInclude.Include.NON_EMPTY标识只有非NULL的值才会被纳入json string之中,其余的都被忽略,比如这里的location属性,并没有出现在最终的结果字符串中。
  • @JsonSerialize: 使用自定义的类来实现自定义的字段转换。写入操作。
  • @JsonDeserialize: 解析的时候,自定义的转换器;读取操作。
  • @JsonAutoDetect: 设置类的访问策略,是否所有的属性都可以,还是按照一定的方式来提取。
  • @JsonRawValue: 无转换的将属性值写入到json 字符串中。 写入操作
  • @JsonValue: 标注方法,用以替代缺省的方法,由该方法来完成json的字符输出。

GSON

Gson简介

Gson是这样一个Java类库,它可以将Java对象转换为相应的JSON形式,也可以将JSON字符串转换为对应的Java对象。 Gson可以使用任意Java对象,包括哪些预先存在的、不在你的源代码中的对象(因此,你并不知道对象的属性)。

Gson的目标

  • 提供一种机制,使得将Java对象转换为JSON或相反如使用toString()以及构造器(工厂方法)一样简单。
  • 允许预先存在的不可变的对象转换为JSON或与之相反。
  • 允许自定义对象的表现形式
  • 支持任意复杂的对象
  • 输出轻量易读的JSON

Gson的使用

使用Gson的首要类是Gson类,你可以仅仅通过new Gson()的方式创建它。你也可以通过GsonBuilder类去创建Gson实例,这个类允许你进行一系列配置,例如版本控制等等。

Gson实例不会保存任何进行Json操作时的状态。因此,你可以自由的服用相同的Gson对象进行诸多的Json序列化和反序列化操作。

引用maven jar包:

1
2
3
4
5
6
<!-- https://mvnrepository.com/artifact/com.google.guava/guava -->
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>28.2-jre</version>
</dependency>

序列化

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
// 基础类型
Gson gson = new Gson();
gson.toJson(1); ==> prints 1
gson.toJson("abcd"); ==> prints "abcd"
gson.toJson(new Long(10)); ==> prints 10
int[] values = { 1 };
gson.toJson(values); ==> prints [1]

// 对象
BagOfPrimitives obj = new BagOfPrimitives();
Gson gson = new Gson();
String json = gson.toJson(obj);
==> json is {"value1":1,"value2":"abc"}

// 数组
Gson gson = new Gson();
int[] ints = {1, 2, 3, 4, 5};
String[] strings = {"abc", "def", "ghi"};
gson.toJson(ints); ==> prints [1,2,3,4,5]
gson.toJson(strings); ==> prints ["abc", "def", "ghi"]

// 集合
Gson gson = new Gson();
Collection<Integer> ints = Lists.immutableList(1,2,3,4,5);
String json = gson.toJson(ints); ==> json is [1,2,3,4,5]

其中的对象代码:

1
2
3
4
5
6
7
8
class BagOfPrimitives {
private int value1 = 1;
private String value2 = "abc";
private transient int value3 = 3;
BagOfPrimitives() {
// no-args constructor
}
}

反序列化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 基础类型
Gson gson = new Gson();
int one = gson.fromJson("1", int.class);
Integer one = gson.fromJson("1", Integer.class);
Long one = gson.fromJson("1", Long.class);
Boolean false = gson.fromJson("false", Boolean.class);
String str = gson.fromJson("\"abc\"", String.class);
String anotherStr = gson.fromJson("[\"abc\"]", String.class);

// 对象
BagOfPrimitives obj2 = gson.fromJson(json, BagOfPrimitives.class);
==> obj2 is just like obj

// 数组
Gson gson = new Gson();
int[] ints2 = gson.fromJson("[1,2,3,4,5]", int[].class);
==> ints2 will be same as ints

// 集合
Gson gson = new Gson();
Type collectionType = new TypeToken<Collection<Integer>>(){}.getType();
Collection<Integer> ints2 = gson.fromJson(json, collectionType);
ints2 is same as ints

自定义序列化和反序列化机制

有时候,默认的实现并不是你想要的。这在处理类库时常常发生(例如DateTime)。Gson允许你注册自己自定义的序列化器和反序列化器。该过程分为两部分:

  • Json序列化器:需要为一个对象自定义序列化机制。
  • Json反序列化器:需要为一个类型自定义反序列化机制。

实例构造者:并不需要,如果无参构造器是可用的或者注册了一个反序列化器。

1
2
3
4
5
GsonBuilder gson = new GsonBuilder();
gson.registerTypeAdapter(MyType2.class, new MyTypeAdapter());
gson.registerTypeAdapter(MyType.class, new MySerializer());
gson.registerTypeAdapter(MyType.class, new MyDeserializer());
gson.registerTypeAdapter(MyType.class, new MyInstanceCreator());

registerTypeAdapter会检查类型适配器是否实现了上面三个接口中的一个以上并且它们都注册了类型适配器。

常用开发库 - Lombok工具库详解

Lombok是一款非常实用Java工具,可用来帮助开发人员消除Java的冗长代码,尤其是对于简单的Java对象(POJO)。实际上我并不推荐使用Lombok(不主动使用它), 但是因为它有着很大的使用量,我们仍然有必要掌握它,不仅知道如何使用和它解决的问题,还要知道它的坑。

Lombok的引入

我们通常需要编写大量代码才能使类变得有用。如以下内容:

  • toString()方法
  • hashCode() and equals()方法
  • Getter and Setter 方法
  • 构造函数

对于这种简单的类,这些方法通常是无聊的、重复的,而且是可以很容易地机械地生成的那种东西(ide通常提供这种功能)。

在引入Lombok之前我们是怎么做的

IDE中添加getter/setter, toString等代码

Lombok的安装和使用

Lombok官网

Lombok安装

IDEA搜索Lombok插件

另外需要注意的是,在使用lombok注解的时候记得要导入lombok.jar包到工程,如果使用的是Maven的工程项目的话,要在其pom.xml中添加依赖如下

1
2
3
4
5
6
7
<!-- https://mvnrepository.com/artifact/org.projectlombok/lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.12</version>
<scope>provided</scope>
</dependency>

Lombok注解说明

官网这里(opens new window)

  • val:用在局部变量前面,相当于将变量声明为final
  • @NonNull:给方法参数增加这个注解会自动在方法内对该参数进行是否为空的校验,如果为空,则抛出NPE(NullPointerException)
  • @Cleanup:自动管理资源,用在局部变量之前,在当前变量范围内即将执行完毕退出之前会自动清理资源,自动生成try-finally这样的代码来关闭流
  • @Getter/@Setter:用在属性上,再也不用自己手写setter和getter方法了,还可以指定访问范围
  • @ToString:用在类上,可以自动覆写toString方法,当然还可以加其他参数,例如@ToString(exclude=”id”)排除id属性,或者@ToString(callSuper=true, includeFieldNames=true)调用父类的toString方法,包含所有属性
  • @EqualsAndHashCode:用在类上,自动生成equals方法和hashCode方法
  • @NoArgsConstructor, @RequiredArgsConstructor and @AllArgsConstructor:用在类上,自动生成无参构造和使用所有参数的构造函数以及把所有+ @NonNull属性作为参数的构造函数,如果指定staticName = “of”`参数,同时还会生成一个返回类对象的静态工厂方法,比使用构造函数方便很多
  • @Data:注解在类上,相当于同时使用了@ToString、@EqualsAndHashCode、@Getter、@Setter和@RequiredArgsConstrutor这些注解,对于POJO类十分有用
  • @Value:用在类上,是@Data的不可变形式,相当于为属性添加final声明,只提供getter方法,而不提供setter方法
  • @Builder:用在类、构造器、方法上,为你提供复杂的builder APIs,让你可以像如下方式一样调用Person.builder().name(“Adam Savage”).city(“San Francisco”).job(“Mythbusters”).job(“Unchained Reaction”).build();更多说明参考Builder
  • @SneakyThrows:自动抛受检异常,而无需显式在方法上使用throws语句
  • @Synchronized:用在方法上,将方法声明为同步的,并自动加锁,而锁对象是一个私有的属性$lock或$LOCK,而java中的synchronized关键字锁对象是this,锁在this或者自己的类对象上存在副作用,就是你不能阻止非受控代码去锁this或者类对象,这可能会导致竞争条件或者其它线程错误
  • @Getter(lazy=true):可以替代经典的Double Check Lock样板代码
  • @Log:根据不同的注解生成不同类型的log对象,但是实例名称都是log,有六种可选实现类
  • @CommonsLog Creates log = org.apache.commons.logging.LogFactory.getLog(LogExample.class);
  • @Log Creates log = java.util.logging.Logger.getLogger(LogExample.class.getName());
  • @Log4j Creates log = org.apache.log4j.Logger.getLogger(LogExample.class);
  • @Log4j2 Creates log = org.apache.logging.log4j.LogManager.getLogger(LogExample.class);
  • @Slf4j Creates log = org.slf4j.LoggerFactory.getLogger(LogExample.class);
  • @XSlf4j Creates log = org.slf4j.ext.XLoggerFactory.getXLogger(LogExample.class);

Lombok深入理解

接下来我们深入理解下Lombok:

Lombok解决了什么问题

这个简单,就是简化代码。

Lombok的原理

会发现在Lombok使用的过程中,只需要添加相应的注解,无需再为此写任何代码。自动生成的代码到底是如何产生的呢?

核心之处就是对于注解的解析上。JDK5引入了注解的同时,也提供了两种解析方式。

  • 运行时解析

运行时能够解析的注解,必须将@Retention设置为RUNTIME, 比如@Retention(RetentionPolicy.RUNTIME),这样就可以通过反射拿到该注解。java.lang,reflect反射包中提供了一个接口AnnotatedElement,该接口定义了获取注解信息的几个方法,Class、Constructor、Field、Method、Package等都实现了该接口,对反射熟悉的朋友应该都会很熟悉这种解析方式。

  • 编译时解析

编译时解析有两种机制,分别简单描述下:

1)Annotation Processing Tool

apt自JDK5产生,JDK7已标记为过期,不推荐使用,JDK8中已彻底删除,自JDK6开始,可以使用Pluggable Annotation Processing API来替换它,apt被替换主要有2点原因:

  • api都在com.sun.mirror非标准包下
  • 没有集成到javac中,需要额外运行

2)Pluggable Annotation Processing API

JSR 269: Pluggable Annotation Processing API(opens new window)自JDK6加入,作为apt的替代方案,它解决了apt的两个问题,javac在执行的时候会调用实现了该API的程序,这样我们就可以对编译器做一些增强,这时javac执行的过程如下:

Lombok本质上就是一个实现了“JSR 269 API”的程序。在使用javac的过程中,它产生作用的具体流程如下:

  • javac对源代码进行分析,生成了一棵抽象语法树(AST)
  • 运行过程中调用实现了“JSR 269 API”的Lombok程序
  • 此时Lombok就对第一步骤得到的AST进行处理,找到@Data注解所在类对应的语法树(AST),然后修改该语法树(AST),增加getter和setter方法定义的相应树节点
  • javac使用修改后的抽象语法树(AST)生成字节码文件,即给class增加新的节点(代码块)

从上面的Lombok执行的流程图中可以看出,在Javac 解析成AST抽象语法树之后, Lombok 根据自己编写的注解处理器,动态地修改 AST,增加新的节点(即Lombok自定义注解所需要生成的代码),最终通过分析生成JVM可执行的字节码Class文件。使用Annotation Processing自定义注解是在编译阶段进行修改,而JDK的反射技术是在运行时动态修改,两者相比,反射虽然更加灵活一些但是带来的性能损耗更加大。

Lombok类似原理工具有什么

  • 第一个问题,我可以通过上述原理,自己实现一个类似Lombok 吗?

可以的,给你找了一篇文章(opens new window)

Lombok没有未来 - Java14 Record了解下

Lombok是没有未来的,因为Java完全可以对于这种纯数据载体通过另外一种方式表示, 所以有了JEP 359: Records(opens new window), 简单而言就是通过一个语法糖来解决。

  • 从前
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 Range {

private final int min;
private final int max;

public Range(int min, int max) {
this.min = min;
this.max = max;
}

public int getMin() {
return min;
}

public int getMax() {
return max;
}

@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Range range = (Range) o;
return min == range.min && max == range.max;
}

@Override
public int hashCode() {
return Objects.hash(min, max);
}

@Override
public String toString() {
return "Range{" +
"min=" + min +
", max=" + max +
'}';
}
}
  • Java14 record
1
public record Range(int min, int max) {}

没错就是这个简单!这个语法糖是不是有 “卧槽” 的感觉?我们声明这种类使用record 标识(目前不知道 record 会不会上升到关键字的高度)。当你用record 声明一个类时,该类将自动拥有以下功能:

  • 获取成员变量的简单方法,以上面代码为例 min() 和 max() 。注意区别于我们平常getter的写法。
  • 一个 equals 方法的实现,执行比较时会比较该类的所有成员属性
  • 重写 equals 当然要重写 hashCode
  • 一个可以打印该类所有成员属性的 toString 方法。
  • 请注意只会有一个构造方法。

因为这个特性是 preview feature,默认情况下是无法编译和执行的。同样以上面为例我们需要执行:

1
2
$ javac -d classes --enable-preview --release 14 Range.java
$ java -classpath classes --enable-preview Range

虽然 record 声明的类没有 final 关键字,实际上它是一个不可变类。除了一些限制外,它依旧是一个普通的Java 类。因此,我们可以像添加普通类一样添加逻辑。我们可以在构造实例时强制执行前提条件:

1
2
3
4
5
6
public record Range(int min, int max) {
public Range {
if (min >= max)
throw new IllegalArgumentException("min should be less than max");
}
}

另外我们也可以给 Records 类增加普通方法、静态属性、静态方法,这里不再举例;

为了简化语法糖的推理,不能在类内声明成员属性。以下是错误的示范:

1
2
3
4
public record Range(int min, int max) {
// 错误的示范
private String name;
}

Lombok有什么坑

@Data的坑

在使用Lombok过程中,如果对于各种注解的底层原理不理解的话,很容易产生意想不到的结果。

举一个简单的例子,我们知道,当我们使用@Data定义一个类的时候,会自动帮我们生成equals()方法 。

但是如果只使用了@Data,而不使用@EqualsAndHashCode(callSuper=true)的话,会默认是@EqualsAndHashCode(callSuper=false),这时候生成的equals()方法只会比较子类的属性,不会考虑从父类继承的属性,无论父类属性访问权限是否开放。

这就可能得到意想不到的结果。

代码可读性,可调试性低

在代码中使用了Lombok,确实可以帮忙减少很多代码,因为Lombok会帮忙自动生成很多代码。但是这些代码是要在编译阶段才会生成的,所以在开发的过程中,其实很多代码其实是缺失的。

在代码中大量使用Lombok,就导致代码的可读性会低很多,而且也会给代码调试带来一定的问题。 比如,我们想要知道某个类中的某个属性的getter方法都被哪些类引用的话,就没那么简单了。

Lombok有很强的侵入性

  • 强J队友

因为Lombok的使用要求开发者一定要在IDE中安装对应的插件。如果未安装插件的话,使用IDE打开一个基于Lombok的项目的话会提示找不到方法等错误。导致项目编译失败。也就是说,如果项目组中有一个人使用了Lombok,那么其他人就必须也要安装IDE插件。否则就没办法协同开发。

更重要的是,如果我们定义的一个jar包中使用了Lombok,那么就要求所有依赖这个jar包的所有应用都必须安装插件,这种侵入性是很高的。

  • 影响升级

因为Lombok对于代码有很强的侵入性,就可能带来一个比较大的问题,那就是会影响我们对JDK的升级。按照如今JDK的升级频率,每半年都会推出一个新的版本,但是Lombok作为一个第三方工具,并且是由开源团队维护的,那么他的迭代速度是无法保证的。

所以,如果我们需要升级到某个新版本的JDK的时候,若其中的特性在Lombok中不支持的话就会受到影响。

还有一个可能带来的问题,就是Lombok自身的升级也会受到限制。因为一个应用可能依赖了多个jar包,而每个jar包可能又要依赖不同版本的Lombok,这就导致在应用中需要做版本仲裁,而我们知道,jar包版本仲裁是没那么容易的,而且发生问题的概率也很高。

Lombok破坏了封装性

以上几个问题,我认为都是有办法可以避免的。但是有些人排斥使用Lombok还有一个重要的原因,那就是他会破坏封装性。

众所周知,Java的三大特性包括封装性、继承性和多态性。

如果我们在代码中直接使用Lombok,那么他会自动帮我们生成getter、setter 等方法,这就意味着,一个类中的所有参数都自动提供了设置和读取方法。

举个简单的例子,我们定义一个购物车类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Data
public class ShoppingCart {

//商品数目
private int itemsCount;

//总价格
private double totalPrice;

//商品明细
private List items = new ArrayList<>();

}

//例子来源于《极客时间-设计模式之美》

我们知道,购物车中商品数目、商品明细以及总价格三者之前其实是有关联关系的,如果需要修改的话是要一起修改的。

但是,我们使用了Lombok的@Data注解,对于itemsCount 和 totalPrice这两个属性。虽然我们将它们定义成 private 类型,但是提供了 public 的 getter、setter 方法。

外部可以通过 setter 方法随意地修改这两个属性的值。我们可以随意调用 setter 方法,来重新设置 itemsCount、totalPrice 属性的值,这也会导致其跟 items 属性的值不一致。

而面向对象封装的定义是:通过访问权限控制,隐藏内部数据,外部仅能通过类提供的有限的接口访问、修改内部数据。所以,暴露不应该暴露的 setter 方法,明显违反了面向对象的封装特性。

好的做法应该是不提供getter/setter,而是只提供一个public的addItem方法,同时去修改itemsCount、totalPrice以及items三个属性。

以上问题其实也是可以解决的,但这提醒了我们需要理解Lombok,而不是一股脑的用@Data注解。

总结

  • 优缺点

  • 优点:大大减少了代码量,使代码非常简洁

  • 缺点:可能存在对队友不友好、对代码不友好、对调试不友好、对升级不友好等问题。Lombok还会导致破坏封装性的问题。@Data中覆盖equals和hashCode的坑等。

  • 什么样的情况使用Lombok

  • 团队整体的共识,IDE规范,相关代码规范等

  • 对Lombok足够了解,比如知道其中的坑等

  • 不推荐使用Lombok的理由

  • 其实我们不缺时间写Getter和Setter的,这些代码通常是由IDE生成的。简化也是有代价的。

  • 对Lombok认知不够,导致带来的坑。

  • Java14中Record了解下。

常用开发库 - MapStruct工具库详解

MapStruct是一款非常实用Java工具,主要用于解决对象之间的拷贝问题,比如PO/DTO/VO/QueryParam之间的转换问题。区别于BeanUtils这种通过反射,它通过编译器编译生成常规方法,将可以很大程度上提升效率。全面的官方手册可以参考官方文档PDF

为什么会引入MapStruct这类工具

JavaBean 问题引入

在开发的时候经常会有业务代码之间有很多的 JavaBean 之间的相互转化,比如PO/DTO/VO/QueryParam之间的转换问题。之前我们的做法是:

  • 拷贝技术

  • org.apache.commons.beanutils.PropertyUtils.copyProperties

  • org.apache.commons.beanutils.BeanUtils.copyProperties

  • org.springframework.beans.BeanUtils.copyProperties

  • net.sf.cglib.beans.BeanCopier

  • 纯get/set

  • 辅助IDE插件拷贝对象时可以自动set所有方法字段 (这种方式可能有些开发人员不清楚)

  • 不仅看上去冗余添加新的字段时依然需要手动

  • 开发效率比较低

MapStruct 带来的改变

MapSturct 是一个生成类型安全, 高性能且无依赖的 JavaBean 映射代码的注解处理器(annotation processor)。

工具可以帮我们实现 JavaBean 之间的转换, 通过注解的方式。

同时, 作为一个工具类,相比于手写, 其应该具有便捷, 不容易出错的特点。

MapStruct入门例子

这里展示最基本的PO转VO的例子,使用的是IDEA + Lombok + MapStruct

Pom.xml

注意:基于当前IDEA设置并不需要mapstruct-processor的依赖

一般来说会加载两个包:

  • org.mapstruct:mapstruct: 包含Mapstruct核心,比如注解等;如果是mapstruct-jdk8会引入一些jdk8的语言特性;
  • org.mapstruct:mapstruct-processor: 处理注解用的,可以根据注解自动生成mapstruct的mapperImpl类

如下示例基于IDEA实现,可以在build阶段的annotationProcessorPaths中配置mapstruct-processor的path。

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
<packaging>jar</packaging>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target>
<org.mapstruct.version>1.4.0.Beta3</org.mapstruct.version>
<org.projectlombok.version>1.18.12</org.projectlombok.version>
</properties>
<dependencies>
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct</artifactId>
<version>${org.mapstruct.version}</version>
</dependency>

<!-- lombok dependencies should not end up on classpath -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${org.projectlombok.version}</version>
<scope>provided</scope>
</dependency>

<!-- fastjson -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.71</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
<scope>test</scope>
</dependency>
</dependencies>

<build>
<pluginManagement>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.1</version>
<configuration>
<source>1.8</source>
<target>1.8</target>
<!-- See https://maven.apache.org/plugins/maven-compiler-plugin/compile-mojo.html -->
<!-- Classpath elements to supply as annotation processor path. If specified, the compiler -->
<!-- will detect annotation processors only in those classpath elements. If omitted, the -->
<!-- default classpath is used to detect annotation processors. The detection itself depends -->
<!-- on the configuration of annotationProcessors. -->
<!-- -->
<!-- According to this documentation, the provided dependency processor is not considered! -->
<annotationProcessorPaths>
<path>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct-processor</artifactId>
<version>${org.mapstruct.version}</version>
</path>
<path>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${org.projectlombok.version}</version>
</path>
</annotationProcessorPaths>
</configuration>
</plugin>
</plugins>
</pluginManagement>
</build>

Entity

这里面假设基于一些业务需求采用的是MySQL,且将一些扩展的数据放在了config字段中,并以JSON转String存储。

1
2
3
4
5
6
7
8
9
10
11
@Data
@Accessors(chain = true)
public class User {
private Long id;
private String username;
private String password; // 密码
private Integer sex; // 性别
private LocalDate birthday; // 生日
private LocalDateTime createTime; // 创建时间
private String config; // 其他扩展信息,以JSON格式存储
}

VO 类

最后真正展示的应该:

  • 不显示密码;
  • 将日期转换;
  • config要转成对象的list;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Data
@Accessors(chain = true)
public class UserVo {
private Long id;
private String username;
private String password;
private Integer gender;
private LocalDate birthday;
private String createTime;
private List<UserConfig> config;
@Data
public static class UserConfig {
private String field1;
private Integer field2;
}
}

mapper(或者converter)

注意:

  • 这里没用@Mappings,且看最后编译出的类文件,会自动加
  • 密码需要ignore
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@Mapper
public interface UserConverter {
UserConverter INSTANCE = Mappers.getMapper(UserConverter.class);

@Mapping(target = "gender", source = "sex")
@Mapping(target = "createTime", dateFormat = "yyyy-MM-dd HH:mm:ss")
UserVo do2vo(User var1);

@Mapping(target = "sex", source = "gender")
@Mapping(target = "password", ignore = true)
@Mapping(target = "createTime", dateFormat = "yyyy-MM-dd HH:mm:ss")
User vo2Do(UserVo var1);

List<UserVo> do2voList(List<User> userList);

default List<UserVo.UserConfig> strConfigToListUserConfig(String config) {
return JSON.parseArray(config, UserVo.UserConfig.class);
}

default String listUserConfigToStrConfig(List<UserVo.UserConfig> list) {
return JSON.toJSONString(list);
}
}

测试类

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
@Test
public void do2VoTest() {
User user = new User()
.setId(1L)
.setUsername("zhangsan")
.setSex(1)
.setPassword("abc123")
.setCreateTime(LocalDateTime.now())
.setBirthday(LocalDate.of(1999, 9, 27))
.setConfig("[{\"field1\":\"Test Field1\",\"field2\":500}]");

UserVo userVo = UserConverter.INSTANCE.do2vo(user);

// asset
assertNotNull(userVo);
assertEquals(userVo.getId(), user.getId());

// print
System.out.println(user);
System.out.println(userVo);
// User(id=1, username=zhangsan, password=abc123, sex=1, birthday=1999-09-27, createTime=2020-08-17T14:54:01.528, config=[{"field1":"Test Field1","field2":500}])
// UserVo(id=1, username=zhangsan, password=abc123, gender=1, birthday=1999-09-27, createTime=2020-08-17 14:54:01, config=[UserVo.UserConfig(field1=Test Field1, field2=500)])
}

@Test
public void vo2DoTest() {
UserVo.UserConfig userConfig = new UserVo.UserConfig();
userConfig.setField1("Test Field1");
userConfig.setField2(500);

UserVo userVo = new UserVo()
.setId(1L)
.setUsername("zhangsan")
.setGender(2)
.setCreateTime("2020-01-18 15:32:54")
.setBirthday(LocalDate.of(1999, 9, 27))
.setConfig(Collections.singletonList(userConfig));
User user = UserConverter.INSTANCE.vo2Do(userVo);

// asset
assertNotNull(userVo);
assertEquals(userVo.getId(), user.getId());

// print
System.out.println(user);
System.out.println(userVo);
}

MapStrcut实现的原理?

MapStruct 来生成的代码, 其类似于人手写。 速度上可以得到保证。

前面例子中生成的代码可以在编译后看到, 在 target/generated-sources/annotations 里可以看到; 同时真正在代码包执行的可以在target/classes包中看到。

编译后的类

  • 编译后的class位置

  • 编译后的内容
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
public class UserConverterImpl implements UserConverter {

@Override
public UserVo do2vo(User var1) {
if ( var1 == null ) {
return null;
}

UserVo userVo = new UserVo();

userVo.setGender( var1.getSex() );
if ( var1.getCreateTime() != null ) {
userVo.setCreateTime( DateTimeFormatter.ofPattern( "yyyy-MM-dd HH:mm:ss" ).format( var1.getCreateTime() ) );
}
userVo.setId( var1.getId() );
userVo.setUsername( var1.getUsername() );
userVo.setPassword( var1.getPassword() );
userVo.setBirthday( var1.getBirthday() );
userVo.setConfig( strConfigToListUserConfig( var1.getConfig() ) );

return userVo;
}

@Override
public User vo2Do(UserVo var1) {
if ( var1 == null ) {
return null;
}

User user = new User();

user.setSex( var1.getGender() );
if ( var1.getCreateTime() != null ) {
user.setCreateTime( LocalDateTime.parse( var1.getCreateTime(), DateTimeFormatter.ofPattern( "yyyy-MM-dd HH:mm:ss" ) ) );
}
user.setId( var1.getId() );
user.setUsername( var1.getUsername() );
user.setBirthday( var1.getBirthday() );
user.setConfig( listUserConfigToStrConfig( var1.getConfig() ) );

return user;
}

@Override
public List<UserVo> do2voList(List<User> userList) {
if ( userList == null ) {
return null;
}

List<UserVo> list = new ArrayList<UserVo>( userList.size() );
for ( User user : userList ) {
list.add( do2vo( user ) );
}

return list;
}
}

这里面用了什么机制?

这和Lombok实现机制一致。

核心之处就是对于注解的解析上。JDK5引入了注解的同时,也提供了两种解析方式。

  • 运行时解析

运行时能够解析的注解,必须将@Retention设置为RUNTIME, 比如@Retention(RetentionPolicy.RUNTIME),这样就可以通过反射拿到该注解。java.lang,reflect反射包中提供了一个接口AnnotatedElement,该接口定义了获取注解信息的几个方法,Class、Constructor、Field、Method、Package等都实现了该接口,对反射熟悉的朋友应该都会很熟悉这种解析方式。

  • 编译时解析

编译时解析有两种机制,分别简单描述下:

1)Annotation Processing Tool

apt自JDK5产生,JDK7已标记为过期,不推荐使用,JDK8中已彻底删除,自JDK6开始,可以使用Pluggable Annotation Processing API来替换它,apt被替换主要有2点原因:

  • api都在com.sun.mirror非标准包下
  • 没有集成到javac中,需要额外运行

2)Pluggable Annotation Processing API

JSR 269: Pluggable Annotation Processing API(opens new window)自JDK6加入,作为apt的替代方案,它解决了apt的两个问题,javac在执行的时候会调用实现了该API的程序,这样我们就可以对编译器做一些增强,这时javac执行的过程如下:

Lombok本质上就是一个实现了“JSR 269 API”的程序。在使用javac的过程中,它产生作用的具体流程如下:

  • javac对源代码进行分析,生成了一棵抽象语法树(AST)
  • 运行过程中调用实现了“JSR 269 API”的Lombok程序
  • 此时Lombok就对第一步骤得到的AST进行处理,找到@Data注解所在类对应的语法树(AST),然后修改该语法树(AST),增加getter和setter方法定义的相应树节点
  • javac使用修改后的抽象语法树(AST)生成字节码文件,即给class增加新的节点(代码块)

从上面的Lombok执行的流程图中可以看出,在Javac 解析成AST抽象语法树之后, Lombok 根据自己编写的注解处理器,动态地修改 AST,增加新的节点(即Lombok自定义注解所需要生成的代码),最终通过分析生成JVM可执行的字节码Class文件。使用Annotation Processing自定义注解是在编译阶段进行修改,而JDK的反射技术是在运行时动态修改,两者相比,反射虽然更加灵活一些但是带来的性能损耗更加大。

常用开发库 - 其它常用类库

常用类库资源

想了解哪个常用类库用的最多? 看这里搜索吧: Maven 仓库

搜索常用

还想看看国内一些比较好的开源的?https://gitee.com/explore

转载自www.pdai.tech/,用于自我学习,如果侵权,马上删除


常用开发库知识体系
https://flepeng.github.io/021-Java-常用开发库知识体系/
作者
Lepeng
发布于
2021年5月14日
许可协议