小而全的Java工具类库 Hutool (扩展篇)

发布时间 2023-09-10 10:53:15作者: 百里浅暮

配置文件(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方法执行的时候自动去掉特殊字符。

数据库(Hutool-db)