小而全的Java工具类库 Hutool (扩展篇)
配置文件(Hutool-setting)
对于Properties的广泛使用使我也无能为力,有时候遇到Properties文件又想方便的读写也不容易,于是对Properties做了简单的封装,提供了方便的构造方法(与Setting一致),并提供了与Setting一致的getXXX方法来扩展Properties类,Props类继承自Properties,所以可以兼容Properties类。
Props的使用方法和Properties以及Setting一致(同时支持):
Props props = new Props("test.properties");
String user = props.getProperty("user");
String driver = props.getStr("driver");
日志(Hutool-log)
日志工厂-LogFactory
Hutool-log作为一个日志门面,为了兼容各大日志框架,一个用于自动创建日志对象的日志工厂类必不可少。
LogFactory类用于灵活的创建日志对象,通过static方法创建我们需要的日志,主要功能如下:
- LogFactory.get 自动识别引入的日志框架,从而创建对应日志框架的门面Log对象(此方法创建一次后,下次再次get会根据传入类名缓存Log对象,对于每个类,Log对象都是单例的),同时自动识别当前类,将当前类做为类名传入日志框架。
- LogFactory.createLog 与get方法作用类似。但是此方法调用后会每次创建一个新的Log对象。
- LogFactory.setCurrentLogFactory 自定义当前日志门面的日志实现类。当引入多个日志框架时,我们希望自定义所用的日志框架,调用此方法即可。需要注意的是,此方法为全局方法,在获取Log对象前只调用一次即可
获取当前类对应的Log对象:
//推荐创建不可变静态类成员变量
private static final Log log = LogFactory.get();
如果你想获得自定义name的Log对象(像普通Log日志实现一样),那么可以使用如下方式获取Log:
private static final Log log = LogFactory.get("我是一个自定义日志名");
自定义日志实现
//自定义日志实现为Apache Commons Logging
LogFactory.setCurrentLogFactory(new ApacheCommonsLogFactory());
//自定义日志实现为JDK Logging
LogFactory.setCurrentLogFactory(new JdkLogFactory());
//自定义日志实现为Console Logging
LogFactory.setCurrentLogFactory(new ConsoleLogFactory());
自定义日志工厂(自定义日志门面实现)
LogFactory是一个抽象类,我们可以继承此类,实现createLog方法即可(同时我们可能需要实现Log接口来达到自定义门面的目的),这样我们就可以自定义一个日志门面。最后通过LogFactory.setCurrentLogFactory方法装入这个自定义LogFactory即可实现自定义日志门面。
自定义日志门面的实现可以参考cn.hutool.log.dialect包中的实现内容自定义扩展。 本质上,实现Log接口,做一个日志实现的Wrapper,然后在相应的工厂类中创建此Log实例即可。同时,LogFactory中还可以初始化一些启动配置参数。
静态调用日志-StaticLog
很多时候,我们只是想简简单的使用日志,最好一个方法搞定,我也不想创建Log对象,那么StaticLog或许是你需要的。
StaticLog.info("This is static {} log.", "INFO");
同样StaticLog提供了trace、debug、info、warn、error方法,提供变量占位符支持,使项目中日志的使用简单到没朋友。
StaticLog类中同样提供log方法,可能在极致简洁的状况下,提供非常棒的灵活性(打印日志等级由level参数决定)
缓存(Hutool-cache)
此模块提供一种缓存的简单实现方案,在小型项目中对于简单的缓存需求非常好用。
Hutoo-cache模块提供了几种缓存策略实现:
FIFOCache
FIFO(first in first out) 先进先出策略。元素不停的加入缓存直到缓存满为止,当缓存满时,清理过期缓存对象,清理后依旧满则删除先入的缓存(链表首部对象)。
优点:简单快速
缺点:不灵活,不能保证最常用的对象总是被保留
LFUCache
LFU(least frequently used) 最少使用率策略。根据使用次数来判定对象是否被持续缓存(使用率是通过访问次数计算),当缓存满时清理过期对象,清理后依旧满的情况下清除最少访问(访问计数最小)的对象并将其他对象的访问数减去这个最小访问数,以便新对象进入后可以公平计数。
LRUCache
LRU (least recently used)最近最久未使用缓存。根据使用时间来判定对象是否被持续缓存,当对象被访问时放入缓存,当缓存满了,最久未被使用的对象将被移除。此缓存基于LinkedHashMap,因此当被缓存的对象每被访问一次,这个对象的key就到链表头部。这个算法简单并且非常快,他比FIFO有一个显著优势是经常使用的对象不太可能被移除缓存。缺点是当缓存满时,不能被很快的访问。
TimedCache
定时缓存,对被缓存的对象定义一个过期时间,当对象超过过期时间会被清理。此缓存没有容量限制,对象只有在过期后才会被移除。
WeakCache
弱引用缓存。对于一个给定的键,其映射的存在并不阻止垃圾回收器对该键的丢弃,这就使该键成为可终止的,被终止,然后被回收。丢弃某个键时,其条目从映射中有效地移除。该类使用了WeakHashMap做为其实现,缓存的清理依赖于JVM的垃圾回收。
FileCach
FileCach是一个独立的缓存,主要是将小文件以byte[]的形式缓存到内容中,减少文件的访问,以解决频繁读取文件引起的性能问题。
主要实现有:
- LFUFileCache
- LRUFileCache
缓存工具-CacheUtil
CacheUtil是缓存创建的快捷工具类。用于快速创建不同的缓存对象。
//新建FIFOCache
Cache<String,String> fifoCache = CacheUtil.newFIFOCache(3);
同样其它类型的Cache也可以调用newXXX的方法创建。
先入先出-FIFOCache
FIFO(first in first out) 先进先出策略。元素不停的加入缓存直到缓存满为止,当缓存满时,清理过期缓存对象,清理后依旧满则删除先入的缓存(链表首部对象)。
优点:简单快速
缺点:不灵活,不能保证最常用的对象总是被保留
Cache<String,String> fifoCache = CacheUtil.newFIFOCache(3);
//加入元素,每个元素可以设置其过期时长,DateUnit.SECOND.getMillis()代表每秒对应的毫秒数,在此为3秒
fifoCache.put("key1", "value1", DateUnit.SECOND.getMillis() * 3);
fifoCache.put("key2", "value2", DateUnit.SECOND.getMillis() * 3);
fifoCache.put("key3", "value3", DateUnit.SECOND.getMillis() * 3);
//由于缓存容量只有3,当加入第四个元素的时候,根据FIFO规则,最先放入的对象将被移除
fifoCache.put("key4", "value4", DateUnit.SECOND.getMillis() * 3);
//value1为null
String value1 = fifoCache.get("key1");
文件缓存-FileCache
FileCache主要是将小文件以byte[]的形式缓存到内存中,减少文件的访问,以解决频繁读取文件引起的性能问题。
- LFUFileCache
- LRUFileCache
//参数1:容量,能容纳的byte数
//参数2:最大文件大小,byte数,决定能缓存至少多少文件,大于这个值不被缓存直接读取
//参数3:超时。毫秒
LFUFileCache cache = new LFUFileCache(1000, 500, 2000);
byte[] bytes = cache.getFileBytes("d:/a.jpg");
最少使用-LFUCache
LFU(least frequently used) 最少使用率策略。根据使用次数来判定对象是否被持续缓存(使用率是通过访问次数计算),当缓存满时清理过期对象,清理后依旧满的情况下清除最少访问(访问计数最小)的对象并将其他对象的访问数减去这个最小访问数,以便新对象进入后可以公平计数。
Cache<String, String> lfuCache = CacheUtil.newLFUCache(3);
//通过实例化对象创建
//LFUCache<String, String> lfuCache = new LFUCache<String, String>(3);
lfuCache.put("key1", "value1", DateUnit.SECOND.getMillis() * 3);
lfuCache.get("key1");//使用次数+1
lfuCache.put("key2", "value2", DateUnit.SECOND.getMillis() * 3);
lfuCache.put("key3", "value3", DateUnit.SECOND.getMillis() * 3);
lfuCache.put("key4", "value4", DateUnit.SECOND.getMillis() * 3);
//由于缓存容量只有3,当加入第四个元素的时候,根据LRU规则,最少使用的将被移除(2,3被移除)
String value2 = lfuCache.get("key2");//null
String value3 = lfuCache.get("key3");//null
最近最久未使用-LRUCache
LRU (least recently used)最近最久未使用缓存。根据使用时间来判定对象是否被持续缓存,当对象被访问时放入缓存,当缓存满了,最久未被使用的对象将被移除。此缓存基于LinkedHashMap,因此当被缓存的对象每被访问一次,这个对象的key就到链表头部。这个算法简单并且非常快,他比FIFO有一个显著优势是经常使用的对象不太可能被移除缓存。缺点是当缓存满时,不能被很快的访问。
Cache<String, String> lruCache = CacheUtil.newLRUCache(3);
//通过实例化对象创建
//LRUCache<String, String> lruCache = new LRUCache<String, String>(3);
lruCache.put("key1", "value1", DateUnit.SECOND.getMillis() * 3);
lruCache.put("key2", "value2", DateUnit.SECOND.getMillis() * 3);
lruCache.put("key3", "value3", DateUnit.SECOND.getMillis() * 3);
lruCache.get("key1");//使用时间推近
lruCache.put("key4", "value4", DateUnit.SECOND.getMillis() * 3);
//由于缓存容量只有3,当加入第四个元素的时候,根据LRU规则,最少使用的将被移除(2被移除)
String value2 = lruCache.get("key");//null
超时-TimedCache
定时缓存,对被缓存的对象定义一个过期时间,当对象超过过期时间会被清理。此缓存没有容量限制,对象只有在过期后才会被移除。
//创建缓存,默认4毫秒过期
TimedCache<String, String> timedCache = CacheUtil.newTimedCache(4);
//实例化创建
//TimedCache<String, String> timedCache = new TimedCache<String, String>(4);
timedCache.put("key1", "value1", 1);//1毫秒过期
timedCache.put("key2", "value2", DateUnit.SECOND.getMillis() * 5);
timedCache.put("key3", "value3");//默认过期(4毫秒)
//启动定时任务,每5毫秒清理一次过期条目,注释此行首次启动仍会清理过期条目
timedCache.schedulePrune(5);
//等待5毫秒
ThreadUtil.sleep(5);
//5毫秒后由于value2设置了5毫秒过期,因此只有value2被保留下来
String value1 = timedCache.get("key1");//null
String value2 = timedCache.get("key2");//value2
//5毫秒后,由于设置了默认过期,key3只被保留4毫秒,因此为null
String value3 = timedCache.get("key3");//null
//取消定时清理
timedCache.cancelPruneSchedule();
弱引用-WeakCache
弱引用缓存。对于一个给定的键,其映射的存在并不阻止垃圾回收器对该键的丢弃,这就使该键成为可终止的,被终止,然后被回收。丢弃某个键时,其条目从映射中有效地移除。该类使用了WeakHashMap做为其实现,缓存的清理依赖于JVM的垃圾回收。
与TimedCache使用方法一致:
WeakCache<String, String> weakCache = CacheUtil.newWeakCache(DateUnit.SECOND.getMillis() * 3);
WeakCache也可以像TimedCache一样设置定时清理时间,同时具备垃圾回收清理。
JSON(Hutool-json)
Hutool-json的核心类只有两个:
- JSONObject
- JSONArray 这与其它JSON包是类似的,与此同时,还提供一个JSONUtil工具类用于简化针对JSON的各种操作和转换。
除了核心类,还提供了一些辅助类用于实现特定功能:
- JSONSupport Bean类继承此对象即可无缝转换为JSON或JSON字符串。同时实现了toString()方法可将当前对象输出为JSON字符串。
- XML 提供JSON与XML之间的快速转换,同时JSONUtil中有相应静态封装。
- JSON JSONObject和JSONArray共同实现的接口类,JSONUtil.parse方法默认返回此对象(因为不知道是JSON对象还是JSON数组),然后可以根据实际类型判断后转换对象类型。
与FastJSON类似,JSONObject实现了Map接口,JSONArray实现了List接口,这样我们便可以使用熟悉的API来操作JSON。
在JSON中,Hutool封装了getXXX方法,支持大部分内置类型的值获取操作。比如:
JSONObject json1 = JSONUtil.createObj();
json1.getStr("key");
json1.getInt("key");
json1.getLong("key");
json1.getDouble("key");
json1.getBigDecimal("key");
这些成员方法的加入,可以省掉大量的类型转换代码,大大提高JSON的操作简便性。
JSON工具-JSONUtil
JSON字符串创建
JSONUtil.toJsonStr可以将任意对象(Bean、Map、集合等)直接转换为JSON字符串。 如果对象是有序的Map等对象,则转换后的JSON字符串也是有序的。
SortedMap<Object, Object> sortedMap = new TreeMap<Object, Object>() {
private static final long serialVersionUID = 1L;
{
put("attributes", "a");
put("b", "b");
put("c", "c");
}};
JSONUtil.toJsonStr(sortedMap);
结果:
{"attributes":"a","b":"b","c":"c"}
如果我们想获得格式化后的JSON,则:
JSONUtil.toJsonPrettyStr(sortedMap);
结果:
{
"attributes": "a",
"b": "b",
"c": "c"
}
JSON字符串解析
String html = "{\"name\":\"Something must have been changed since you leave\"}";
JSONObject jsonObject = JSONUtil.parseObj(html);
jsonObject.getStr("name");
XML字符串转换为JSON
String s = "<sfzh>123</sfzh><sfz>456</sfz><name>aa</name><gender>1</gender>";
JSONObject json = JSONUtil.parseFromXml(s);
json.get("sfzh");
json.get("name");
JSON转换为XML
final JSONObject put = JSONUtil.createObj()
.set("aaa", "你好")
.set("键2", "test");
// <aaa>你好</aaa><键2>test</键2>
final String s = JSONUtil.toXmlStr(put);
JSON转Bean
我们先定义两个较为复杂的Bean(包含泛型)
@Data
public class ADT {
private List<String> BookingCode;
}
@Data
public class Price {
private List<List<ADT>> ADT;
}
String json = "{\"ADT\":[[{\"BookingCode\":[\"N\",\"N\"]}]]}";
Price price = JSONUtil.toBean(json, Price.class);
price.getADT().get(0).get(0).getBookingCode().get(0);
Bean转JSON
5.x的Hutool中增加了一个自定义注解:@Alias,通过此注解可以给Bean的字段设置别名。
@Data
public class Test {
private String name;
@Alias("aliasSex")
private String sex;
public static void main(String[] args) {
Test test = new Test();
test.setName("handy");
test.setSex("男");
// 结果: {"name":"handy","aliasSex":"男"}
String json = JSONUtil.toJsonStr(test);
}
}
readXXX
这类方法主要是从JSON文件中读取JSON对象的快捷方法。包括:
- readJSON
- readJSONObject
- readJSONArray
其它方法
除了上面中常用的一些方法,JSONUtil还提供了一些JSON辅助方法:
- quote 对所有双引号做转义处理(使用双反斜杠做转义)
- wrap 包装对象,可以将普通任意对象转为JSON对象
- formatJsonStr 格式化JSON字符串,此方法并不严格检查JSON的格式正确与否
JSON对象-JSONObject
创建
JSONObject json1 = JSONUtil.createObj()
.put("a", "value1")
.put("b", "value2")
.put("c", "value3");
JSONUtil.createObj()是快捷新建JSONObject的工具方法,同样我们可以直接new:
JSONObject json1 = new JSONObject();
...
转换
JSON字符串解析
String jsonStr = "{\"b\":\"value2\",\"c\":\"value3\",\"a\":\"value1\"}";
//方法一:使用工具类转换
JSONObject jsonObject = JSONUtil.parseObj(jsonStr);
//方法二:new的方式转换
JSONObject jsonObject2 = new JSONObject(jsonStr);
//JSON对象转字符串(一行)
jsonObject.toString();
// 也可以美化一下,即显示出带缩进的JSON:
jsonObject.toStringPretty();
JavaBean解析
// 注解使用Lombok
@Data
public class UserA {
private String name;
private String a;
private Date date;
private List<Seq> sqs;
}
解析为JSON:
UserA userA = new UserA();
userA.setName("nameTest");
userA.setDate(new Date());
userA.setSqs(CollectionUtil.newArrayList(new Seq(null), new Seq("seq2")));
// false表示不跳过空值
JSONObject json = JSONUtil.parseObj(userA, false);
Console.log(json.toStringPretty());
结果:
{
"date": 1585618492295,
"a": null,
"sqs": [
{
"seq": null
},
{
"seq": "seq2"
}
],
"name": "nameTest"
}
可以看到,输出的字段顺序和Bean的字段顺序不一致,如果想保持一致,可以:
// 第二个参数表示保持有序
JSONObject json = JSONUtil.parseObj(userA, false, true);
结果:
{
"name": "nameTest",
"a": null,
"date": 1585618648523,
"sqs": [
{
"seq": null
},
{
"seq": "seq2"
}
]
}
默认的,Hutool将日期输出为时间戳,如果需要自定义日期格式,可以调用:
json.setDateFormat("yyyy-MM-dd HH:mm:ss");
结果为:
{
"name": "nameTest",
"a": null,
"date": "2020-03-31 09:41:29",
"sqs": [
{
"seq": null
},
{
"seq": "seq2"
}
]
}
JSON数组-JSONArray
创建
//方法1
JSONArray array = JSONUtil.createArray();
//方法2
JSONArray array = new JSONArray();
array.add("value1");
array.add("value2");
array.add("value3");
//转为JSONArray字符串
array.toString();
从Bean列表解析
@Data
public class KeyBean{
private String akey;
private String bkey;
}
KeyBean b1 = new KeyBean();
b1.setAkey("aValue1");
b1.setBkey("bValue1");
KeyBean b2 = new KeyBean();
b2.setAkey("aValue2");
b2.setBkey("bValue2");
ArrayList<KeyBean> list = CollUtil.newArrayList(b1, b2);
// [{"akey":"aValue1","bkey":"bValue1"},{"akey":"aValue2","bkey":"bValue2"}]
JSONArray jsonArray = JSONUtil.parseArray(list);
// aValue1
jsonArray.getJSONObject(0).getStr("akey");
从JSON字符串解析
String jsonStr = "[\"value1\", \"value2\", \"value3\"]";
JSONArray array = JSONUtil.parseArray(jsonStr);
转换为bean的List
@Data
static class User {
private Integer id;
private String name;
}
String jsonArr = "[{\"id\":111,\"name\":\"test1\"},{\"id\":112,\"name\":\"test2\"}]";
JSONArray array = JSONUtil.parseArray(jsonArr);
List<User> userList = JSONUtil.toList(array, User.class);
// 111
userList.get(0).getId();
转换为Dict的List
Dict是Hutool定义的特殊Map,提供了以字符串为key的Map功能,并提供getXXX方法,转换也类似:
String jsonArr = "[{\"id\":111,\"name\":\"test1\"},{\"id\":112,\"name\":\"test2\"}]";
JSONArray array = JSONUtil.parseArray(jsonArr);
List<Dict> list = JSONUtil.toList(array, Dict.class);
// 111
list.get(0).getInt("id");
转换为数组
String jsonArr = "[{\"id\":111,\"name\":\"test1\"},{\"id\":112,\"name\":\"test2\"}]";
JSONArray array = JSONUtil.parseArray(jsonArr);
User[] list = array.toArray(new User[0]);
JSON路径
如果JSON的层级特别深,那么获取某个值就变得非常麻烦,代码也很臃肿,Hutool提供了getByPath
方法可以通过表达式获取JSON中的值。
String jsonStr = "[{\"id\": \"1\",\"name\": \"a\"},{\"id\": \"2\",\"name\": \"b\"}]";
final JSONArray jsonArray = JSONUtil.parseArray(jsonStr);
// b
jsonArray.getByPath("[1].name");
加密解密(Hutool-crypto)
加密分为三种:
- 对称加密(symmetric),例如:AES、DES等
- 非对称加密(asymmetric),例如:RSA、DSA等
- 摘要加密(digest),例如:MD5、SHA-1、SHA-256、HMAC等
hutool-crypto针对这三种加密类型分别封装,并提供常用的大部分加密算法。
对于非对称加密,实现了:
- RSA
- DSA
对于对称加密,实现了:
- AES
- ARCFOUR
- Blowfish
- DES
- DESede
- RC2
- PBEWithMD5AndDES
- PBEWithSHA1AndDESede
- PBEWithSHA1AndRC2_40
对于摘要算法实现了:
- MD2
- MD5
- SHA-1
- SHA-256
- SHA-384
- SHA-512
- HmacMD5
- HmacSHA1
- HmacSHA256
- HmacSHA384
- HmacSHA512
其中,针对常用到的算法,模块还提供SecureUtil工具类用于快速实现加密。
加密解密工具-SecureUtil
SecureUtil主要针对常用加密算法构建快捷方式,还有提供一些密钥生成的快捷工具方法。
对称加密
- SecureUtil.aes
- SecureUtil.des
摘要算法
- SecureUtil.md5
- SecureUtil.sha1
- SecureUtil.hmac
- SecureUtil.hmacMd5
- SecureUtil.hmacSha1
非对称加密
- SecureUtil.rsa
- SecureUtil.dsa
UUID
- SecureUtil.simpleUUID 方法提供无“-”的UUID
密钥生成
- SecureUtil.generateKey 针对对称加密生成密钥
- SecureUtil.generateKeyPair 生成密钥对(用于非对称加密)
- SecureUtil.generateSignature 生成签名(用于非对称加密)
DFA查找(Hutool-dfa)
就是用所有关键字构造一棵树,然后用正文遍历这棵树,遍历到叶子节点即表示文章中存在这个关键字。
我们暂且忽略构建关键词树的时间,每次查找正文只需要O(n)复杂度就可以搞定。
针对DFA算法以及网上的一些实现,Hutool做了整理和改进,最终形成现在的Hutool-dfa模块。
构建关键词树
WordTree tree = new WordTree();
tree.addWord("大");
tree.addWord("大土豆");
tree.addWord("土豆");
tree.addWord("刚出锅");
tree.addWord("出锅");
查找关键词
//正文
String text = "我有一颗大土豆,刚出锅的";
1、情况一:标准匹配,匹配到最短关键词,并跳过已经匹配的关键词
// 匹配到【大】,就不再继续匹配了,因此【大土豆】不匹配
// 匹配到【刚出锅】,就跳过这三个字了,因此【出锅】不匹配(由于刚首先被匹配,因此长的被匹配,最短匹配只针对第一个字相同选最短)
List<String> matchAll = tree.matchAll(text, -1, false, false);
Assert.assertEquals(matchAll.toString(), "[大, 土豆, 刚出锅]");
2、情况二:匹配到最短关键词,不跳过已经匹配的关键词
// 【大】被匹配,最短匹配原则【大土豆】被跳过,【土豆继续被匹配】
// 【刚出锅】被匹配,由于不跳过已经匹配的词,【出锅】被匹配
matchAll = tree.matchAll(text, -1, true, false);
Assert.assertEquals(matchAll.toString(), "[大, 土豆, 刚出锅, 出锅]");
3、情况三:匹配到最长关键词,跳过已经匹配的关键词
// 匹配到【大】,由于到最长匹配,因此【大土豆】接着被匹配
// 由于【大土豆】被匹配,【土豆】被跳过,由于【刚出锅】被匹配,【出锅】被跳过
matchAll = tree.matchAll(text, -1, false, true);
Assert.assertEquals(matchAll.toString(), "[大, 大土豆, 刚出锅]");
4、情况四:匹配到最长关键词,不跳过已经匹配的关键词(最全关键词)
// 匹配到【大】,由于到最长匹配,因此【大土豆】接着被匹配,由于不跳过已经匹配的关键词,土豆继续被匹配
// 【刚出锅】被匹配,由于不跳过已经匹配的词,【出锅】被匹配
matchAll = tree.matchAll(text, -1, true, true);
Assert.assertEquals(matchAll.toString(), "[大, 大土豆, 土豆, 刚出锅, 出锅]");
除了matchAll方法,WordTree还提供了match和isMatch两个方法,这两个方法只会查找第一个匹配的结果,这样一旦找到第一个关键字,就会停止继续匹配,大大提高了匹配效率。
针对特殊字符
有时候,正文中的关键字常常包含特殊字符,比如:"〓关键☆字",针对这种情况,Hutool提供了StopChar类,专门针对特殊字符做跳过处理,这个过程是在match方法或matchAll方法执行的时候自动去掉特殊字符。