●Hutool是什么?
接下来的两篇文章,笔者将给大家安利一个能够明显提升Java开发效率的开源项目Hutool。官方对其描述为:A set of tools to keep java sweet。它其实是一套Java工具包,提供了许多与业务无关的常用方法,避免重复开发。截止笔者撰文,它在Github上已经收获了2033个Star,要知道,大名鼎鼎的Tomcat也不过2628,可见,它是受广大程序员推崇的。
笔者之所以要推荐这个开源项目给大家,最主要是因为其三大优势——
优势1:遵循Apache2.0开源许可协议,商业友好,可以无风险地接入使用;
优势2:提供的工具众多且实用,拥有完善的API手册,方便查找;
优势3:按需配置依赖,伸缩轻量灵活;
Hutool项目Github传送门:https://github.com/looly/hutool/
官方最新的API说明文档位于:https://apidoc.gitee.com/loolly/hutool/
使用示例位于:http://hutool.mydoc.io/#category_76195
●最甜的几块糖(Part 1)
以下章节将会重点介绍笔者觉得Hutool中最高效实用的几个类,涉及大量的举例代码,读者可以选择性的阅读,也可以作为手册,后期随时翻阅查看。需声明,以下代码均为笔者自行编写,非项目直接来源,大家无序对号入座,还请结合自己的实际业务去使用。
●类型转换
该类几乎是一个万能的类型转换工具类,提供了大量平时常用的类型转换函数,相比传统写法,会更简洁。举例几个经典例子:
1、String转换为Date。不需要进行异常捕获,转换失败返回null,而不抛出异常;同时也提供单参数与双参数的重载函数,后者在转换失败时返回给定值,该特性Convert中其他大多数函数也具备,下同,不再赘述
-
//传统写法
-
String string = "2018-11-11 00:00:01";
-
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
-
try {
-
Date date = sdf.parse(string);
-
} catch (ParseException e) {
-
e.printStackTrace();
-
Date date = new Date();
-
}
-
//“糊涂”写法
-
String string = "2018-11-11 00:00:01";
-
Date date = Convert.toDate(string, new Date());
2、时间单位的转换,例如毫秒转化为天,避免了人为计算,也增加了代码的可读性,其中,该函数的第二第三个参数来自于java.util.concurrent下的枚举变量TimeUnit
-
long milliseconds = 145248462254L;
-
//传统写法
-
long day1 = milliseconds/1000/60/60/24;
-
//“糊涂”写法
-
long day2 = Convert.convertTime(milliseconds,TimeUnit.MILLISECONDS,TimeUnit.DAYS);
3、自定义转换器。如果Hutool提供的转换函数还不够用,想定义自己类的转换器,也是可以的。我们以代码为例,使用自定义转换器有两个步骤:一是继承Converter<T>接口,实现接口方法(插一句嘴,这是一个函数式接口,完全可以用Lambda的形式去实现,留给感兴趣的读者去尝试。本例将以传统方式书写);二是在转换器仓库ConverterRegistry中注册自定义的转换器,之后就可以使用了。
有如下两个类:报警树节点类与通用树节点类,二者不是继承关系,前者特有报警状态,但没有后者是否显示的标志位。
-
public class AlarmTreeNode {
-
private AlarmTreeNode childTreeNode;
-
private boolean isRoot;
-
private String alarmName;
-
private int deviceIndex;
-
private byte alarmStatus;
-
public AlarmTreeNode(){
-
this.childTreeNode = null;
-
this.isRoot = false;
-
this.alarmName = "";
-
this.deviceIndex = 0;
-
this.alarmStatus = 1;
-
}
-
public AlarmTreeNode(AlarmTreeNode childTreeNode, boolean isRoot, String alarmName, int deviceIndex, byte alarmStatus) {
-
this.childTreeNode = childTreeNode;
-
this.isRoot = isRoot;
-
this.alarmName = alarmName;
-
this.deviceIndex = deviceIndex;
-
this.alarmStatus = alarmStatus;
-
}
-
public AlarmTreeNode getChildTreeNode() {
-
return childTreeNode;
-
}
-
public void setChildTreeNode(AlarmTreeNode childTreeNode) {
-
this.childTreeNode = childTreeNode;
-
}
-
public boolean isRoot() {
-
return isRoot;
-
}
-
public void setRoot(boolean root) {
-
isRoot = root;
-
}
-
public String getAlarmName() {
-
return alarmName;
-
}
-
public void setAlarmName(String alarmName) {
-
this.alarmName = alarmName;
-
}
-
public int getDeviceIndex() {
-
return deviceIndex;
-
}
-
public void setDeviceIndex(int deviceIndex) {
-
this.deviceIndex = deviceIndex;
-
}
-
public byte getAlarmStatus() {
-
return alarmStatus;
-
}
-
public void setAlarmStatus(byte alarmStatus) {
-
this.alarmStatus = alarmStatus;
-
}
-
}
-
public class GeneralTreeNode {
-
private GeneralTreeNode childTreeNode;
-
private boolean isRoot;
-
private String nodeName;
-
private long nodeIndex;
-
private boolean isDisplay;
-
public GeneralTreeNode() {
-
this.childTreeNode = null;
-
this.isRoot = false;
-
this.nodeName = "";
-
this.nodeIndex = 0L;
-
this.isDisplay = false;
-
}
-
public GeneralTreeNode(GeneralTreeNode childTreeNode, boolean isRoot, String nodeName, long nodeIndex, boolean isDisplay) {
-
this.childTreeNode = childTreeNode;
-
this.isRoot = isRoot;
-
this.nodeName = nodeName;
-
this.nodeIndex = nodeIndex;
-
this.isDisplay = isDisplay;
-
}
-
public GeneralTreeNode getChildTreeNode() {
-
return childTreeNode;
-
}
-
public void setChildTreeNode(GeneralTreeNode childTreeNode) {
-
this.childTreeNode = childTreeNode;
-
}
-
public boolean isRoot() {
-
return isRoot;
-
}
-
public void setRoot(boolean root) {
-
isRoot = root;
-
}
-
public String getNodeName() {
-
return nodeName;
-
}
-
public void setNodeName(String nodeName) {
-
this.nodeName = nodeName;
-
}
-
public long getNodeIndex() {
-
return nodeIndex;
-
}
-
public void setNodeIndex(long nodeIndex) {
-
this.nodeIndex = nodeIndex;
-
}
-
public boolean isDisplay() {
-
return isDisplay;
-
}
-
public void setDisplay(boolean display) {
-
isDisplay = display;
-
}
-
}
重点看一下我们自定义的转换类,它具有一个转换函数,用于将报警树节点类的对象转换为通用树节点类的对象。该类继承了Hutool的接口Converter<T>,泛型T为待返回的类型。实现接口的convert函数。第一个参数为待转换的对象;第二个参数为默认值。
-
public class CustomConvert implements Converter<GeneralTreeNode> {
-
public GeneralTreeNode convert(Object o, GeneralTreeNode generalTreeNode) throws IllegalArgumentException {
-
if(null != o){
-
GeneralTreeNode resultTreeNode = new GeneralTreeNode();
-
//报警树节点→通用树节点:报警状态舍弃,子节点类型转换为通用的(递归),是否显示标志位设为ture
-
AlarmTreeNode alarmTreeNode = (AlarmTreeNode) o;
-
if(null != alarmTreeNode.getChildTreeNode()){
-
resultTreeNode.setChildTreeNode(convert(alarmTreeNode.getChildTreeNode(),null));
-
}
-
else{
-
resultTreeNode.setChildTreeNode(null);
-
}
-
resultTreeNode.setDisplay(true);
-
resultTreeNode.setNodeIndex(alarmTreeNode.getDeviceIndex());
-
resultTreeNode.setNodeName(alarmTreeNode.getAlarmName());
-
resultTreeNode.setRoot(alarmTreeNode.isRoot());
-
return resultTreeNode;
-
}
-
else{
-
//如果对象为空,返回第二个参数中的默认值
-
return generalTreeNode;
-
}
-
}
-
}
之后我们在转换器仓库ConverterRegistry中注册上自定义的转换函数,这样就可以使用了。
-
//注册自定义转换器
-
ConverterRegistry converterRegistry = ConverterRegistry.getInstance();
-
converterRegistry.putCustom(GeneralTreeNode.class, CustomConvert.class);
-
//使用自定义转换器
-
AlarmTreeNode childTreeNode = new AlarmTreeNode();
-
AlarmTreeNode alarmTreeNode = new AlarmTreeNode(childTreeNode, true, "报警根节点", 1001, (byte) 0);
-
GeneralTreeNode generalTreeNode = converterRegistry.convert(GeneralTreeNode.class, alarmTreeNode, null);
-
AlarmTreeNode nullAlarmTreeNode = null;
-
GeneralTreeNode nullGeneralTreeNode = converterRegistry.convert(GeneralTreeNode.class, nullAlarmTreeNode, null);
●日期与时间
Hutool提供了对Java原生Date的包装,并且提供了大量的工具类,操纵时间和日期。
1、时间格式化输出。传统方式做Date的格式化处理都需要用到SimpleDateFormat,导致项目中出现大量的模板代码,而Hutool的DateUtil工具类则很好解决了这个问题。
-
//传统写法
-
SimpleDateFormat sdfTime = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
-
SimpleDateFormat sdfDate = new SimpleDateFormat("yyyy-MM-dd");
-
Date date = new Date();
-
String currentTime = sdfTime.format(date);
-
String currentDate = sdfDate.format(date);
-
//“糊涂”写法
-
String currentHutoolTime = DateUtil.now();
-
String currentHutoolDate = DateUtil.today();
2、日期与时间这部分也提供了一个String转Date的函数,作用与之前的converter里的一样。它能解析一些常用的格式,如代码所示。
-
//“糊涂”写法
-
String dateStr1 = "2017-03-01";
-
String dateStr2 = "2017-03-01 12:00";
-
String dateStr3 = "2017-03-01 12:18:02";
-
String dateStr4 = "2017-03-01 12:23:12.31";
-
String dateStr5 = "15:09:32";
-
Date date = DateUtil.parse(dateStr1);
3、比较好用的是计算时间差的相关工具函数,提供了计算各种单位的时间差,以及格式化组合的时间差。传统的写法不仅代码冗长,还需要考虑闰年等因素,非常麻烦。
-
//“糊涂”写法
-
String dateStr1 = "2017-03-01 18:01:14";
-
String dateStr2 = "2018-11-11 00:00:00";
-
Date date1 = DateUtil.parse(dateStr1);
-
Date date2 = DateUtil.parse(dateStr2);
-
//参数1、2谁前谁后也不影响,没必要非得小时间在前,非常方便
-
Long timeBetween1 = DateUtil.between(date1,date2,DateUnit.SECOND);
-
Long timeBetween2 = DateUtil.between(date1,date2,DateUnit.WEEK);
-
String timeBetween3 = DateUtil.formatBetween(date1,date2, BetweenFormater.Level.SECOND);
-
System.out.printf("俩时间差%d秒\n", timeBetween1);
-
System.out.printf("俩时间差%s周\n", timeBetween2);
-
System.out.printf("俩时间差(精确到秒)为:%s\n", timeBetween3);
4、除了时间差,经常会遇到做时间的加减计算,也就是时间偏移量的问题,看看传统写法与Hutool的封装:
-
String dateStr = "2018-11-11 00:10:15.124";
-
//传统写法
-
SimpleDateFormat sdf= new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS");
-
Date date = null;
-
try {
-
date = sdf.parse(dateStr);
-
} catch (ParseException e) {
-
e.printStackTrace();
-
}
-
Calendar dataTime = Calendar.getInstance();
-
dataTime.setTime(date);
-
dataTime.add(Calendar.MINUTE,10);
-
System.out.printf("处理后的时间:%s\n",sdf.format(dataTime.getTime()));
-
//"糊涂"写法
-
Date hutoolDate = DateUtil.parse(dateStr);
-
Date newDate = DateUtil.offsetMinute(hutoolDate,10);
-
System.out.printf("处理后的时间:%s\n",DateUtil.format(newDate,sdf));
5、最后来看一下计时器。可以用于记录代码执行时间。
-
//传统写法
-
long millseconds1 = System.currentTimeMillis();
-
long count1 = 1;
-
for(int i=1 ; i<1000 ;i++){
-
count1+=i;
-
Thread.sleep(1);
-
}
-
System.out.printf("计算结果%d --- ",count1);
-
long millseconds2 = System.currentTimeMillis();
-
System.out.printf("代码运行了%d毫秒\n",millseconds2-millseconds1);
-
//“糊涂”写法
-
TimeInterval timer = DateUtil.timer();
-
long count2 = 1;
-
for(int i=1 ; i<1000 ;i++){
-
count2+=i;
-
Thread.sleep(1);
-
}
-
System.out.printf("计算结果%d --- ",count2);
-
System.out.printf("代码运行了%d毫秒\n",timer.interval());
●字段验证器
我们都知道,在互联网行业,服务器后台在接收到页面传来的请求时,几乎都要做参数校验的,目的是防止SQL注入等攻击,引起系统故障。为了避免重复开发参数校验的相关代码,Hutool也为大家提供了好用的字段校验器,能满足常用的参数格式校验。
1、常用的校验函数
-
String parameter = "+8610000000000";
-
int min = 5;
-
int max = 8;
-
//验证是否是身份证
-
boolean isCitizenId = Validator.isCitizenId(parameter);
-
//验证是否是Email
-
boolean isEmail = Validator.isEmail(parameter);
-
//验证是否是汉字
-
boolean isChinese = Validator.isChinese(parameter);
-
//验证是否是生日
-
boolean isBirthday = Validator.isBirthday(parameter);
-
//验证是否是Ipv4地址
-
boolean isIpv4 = Validator.isIpv4(parameter);
-
//验证是否是mac地址
-
boolean isMac = Validator.isMac(parameter);
-
//验证是否是中国的手机号
-
boolean isMobile = Validator.isMobile(parameter);
-
//验证是否是中国的车牌号
-
boolean isPlateNumber = Validator.isPlateNumber(parameter);
-
//验证是否是满足长度的仅含英文、数字、下划线的字符串
-
boolean isGeneral = Validator.isGeneral(parameter, min, max);
2、自定义的校验函数。当Hutool提供的字段校验器还无法满足大家的使用要求时,可以根据业务场景自定义正则表达式来进行校验,举个例子,我们要校验用户提交的注册邮箱是否满足:邮箱前缀只能是英文和数字的组合,并且需要英文开头,一共5位;邮箱后缀需要是csdn.com或者csdn.com.cn。那我们就可以这样去定义和使用自己的字段校验器。相比使用Pattern和Matcher的模板代码,要精简许多。
-
//自定义正则表达式进行验证
-
String regex = "[a-zA-Z]{1}[a-zA-Z0-9]{4}@(csdn.com)|(csdn.com.cn)";
-
//传统写法
-
Pattern pattern = Pattern.compile(regex);
-
Matcher matcher = pattern.matcher(parameter);
-
boolean isMatchRegex = matcher.matches();
-
System.out.printf("注册邮箱%s要求!\n",isMatchRegex?"符合":"不符合");
-
//“糊涂写法”
-
boolean isMatchMyRegex = Validator.isMactchRegex(regex,parameter);
-
System.out.printf("注册邮箱%s要求!\n",isMatchMyRegex?"符合":"不符合");
●配置工具
通过,我们的项目中都会有properties配置文件,例如配置数据源、系统相关参数等。使用中我们会发现,利用Properties来加载properties文件,存在固定步骤的模板代码,Hutool对其进行封装为Props,加载过程变得更加简洁,并且还提供了额外的拓展功能,例如读取的配置项直接转换为想要的类型(Properties的getProperty()获取到的是String类型,需要手动转换)
假设有如下配置文件config.properties
-
## 1 oracle, 2 hbase, 3 postgresql
-
dbsource=3
-
## pg db
-
pg.driverClassName=org.postgresql.Driver
-
pg.url=jdbc\:postgresql\://10.10.10.10\:5432/port
-
pg.username=user
-
pg.password=password
-
pg.maxconnection=10
-
## ip and port
-
web_ip=10.10.10.10:80
-
oss_ip=12.12.12.12:8088
我们对其进行加载与获取配置项的操作
-
//传统写法,通过Properties操作配置文件
-
Properties properties = new Properties();
-
InputStream inputestream = Thread.currentThread().getContextClassLoader().getResourceAsStream("config.properties");
-
try {
-
properties.load( inputestream);
-
} catch (IOException e) {
-
e.printStackTrace();
-
}
-
String webIp = properties.getProperty("web_ip");
-
System.out.printf("获取到配置项web_ip:%s\n", webIp);
-
//"糊涂"写法,对Properties封装为Props,简化load操作
-
Props props = new Props("config.properties");
-
String huToolWebIp = props.getProperty("web_ip");
-
System.out.printf("获取到配置项web_ip:%s\n",huToolWebIp);
-
int huToolMaxConnection = props.getInt("pg.maxconnection");
-
System.out.printf("获取到配置项最大连接数:%d\n", huToolMaxConnection);
配置文件除了properties,用得更多的是xml,但xml的书写和解析同样是有点复杂,不够精简的,因此Hutool给大家提供了一种新的配置文件,叫做setting文件,它的格式类似properties,吸收了其简洁直观的优点;同时,它支持分组,借鉴了xml中的思路。假设有如下配置文件config.setting。相比config.properties,我们可以用中括号[]定义分组,操作的时候更灵活。
-
[database]
-
## 1 oracle, 2 hbase, 3 postgresql
-
id = 1
-
dbsource=3
-
## pg db
-
pg.driverClassName=org.postgresql.Driver
-
pg.url=jdbc\:postgresql\://10.10.10.10\:5432/port
-
pg.username=user
-
pg.password=password
-
pg.maxconnection=10
-
[server]
-
## ip and port
-
id = 2
-
web_ip=10.10.10.10:80
-
oss_ip=12.12.12.12:8088
我们对其进行加载与获取配置项的操作
-
//"糊涂"写法,更进一步的解决方案,采用setting文件代替properties文件,支持更多功能
-
Setting setting = new Setting("config.setting");
-
//支持分组、默认值
-
String webIp = setting.getStr("web_ip","server","127.0.0.1");
-
int datebaseSource = setting.getInt("dbsource","database",10);
-
int cacheSize = setting.getInt("cachesize","database",500);
-
System.out.printf("获取到配置项web_ip:%s, 最大连接数:%d, 缓存大小:%d Mb\n", webIp, datebaseSource, cacheSize);
-
int databaseID = setting.getInt("id","database");
-
int serverID = setting.getInt("id","server");
-
System.out.printf("databaseID为:%d , serverID为:%d\n", databaseID, serverID);
可以看到,我们除了可以使用分组 外,还可以设置默认值,方便配置文件中未填写时,系统给出一个默认的。
●日志工具
大家在项目中,日志的解决方案很多都选用日志门面slf4j再加上具体的日志实现,例如log4j,这么做最重要的好处在于slf4j采用了外观模式(Facade)的设计思路,能够为程序员提供一套统一的日志操作接口,无论后面真实的日志实现是log4j还是logback等,屏蔽了不同日志实现的差异性,再项目更换具体日志实现的时候不需要改动代码,仅处理下配置文件和引用的jar包即可,松耦合。值得一提的很多成熟的公司,例如阿里巴巴在其《阿里巴巴Java开发手册》中都有明确且强制的规定:应用中不可直接使用日志系统( Log4j、 Logback) 中的 API,而应依赖使用日志框架SLF4J 中的 API,使用门面模式的日志框架,有利于维护和各个类的日志处理方式统一。
-
public class LogTest {
-
private static final Logger logger = LoggerFactory.getLogger(LogTest.class);
-
public void log4jAndSlf4jTest(){
-
//slf4+log4j “传统”日志
-
logger.info("当前毫秒: {},对应秒数为{}", System.currentTimeMillis(),System.currentTimeMillis()/1000);
-
Exception e = new NullPointerException();
-
//logger.error(e,"空指针异常,发生时间:","now"); 无法这样使用
-
logger.error("空指针异常",e);
-
}
-
}
按理slf4j已经是很不错的日志门面框架的,但是Hutool依然提供了一个日志门面Log。这是为什么呢?我们看看上面的代码可以看到slf4j的另外一个好处在于支持占位符{},避免了字符串拼接,消耗资源更低,书写阅读也更清晰,这一点类似之前代码中System.out.printf()中的%s、%d等。然而,遗憾的是,如果在异常捕获中,想记录异常的同时使用占位符确是不可以的。并且,我们能看到,创建日志对象logger的时候,每个类都得去手动写LoggerFactory.getLogger()里的参数,笔者第一次学习slf4j的时候,就没有注意到这个,导致写到了别的类上。按理,这应该也是程序可以自动识别的。
基于此,Hutool提供了Log日志门面,与slf4j类似,并且改善了一些小细节,使用更加方便。它对具体日志实现的检测顺序是:Logback > Log4j > Log4j2 > Apache Commons Logging > JDK Logging > Console。
我们来看看代码。几乎是没有额外学习成本的,只要会使用slf4j就能顺利过渡到Log。
-
public class LogTest {
-
private static final Log log = LogFactory.get();
-
public void hutoolLogTest(){
-
//Log+log4j “糊涂”日志
-
log.info("当前时间:{}", DateTime.now());
-
Exception e = new NullPointerException();
-
log.error(e,"空指针异常,类型{}",e.getClass());
-
}
-
}