小而全的Java工具类库 Hutool (核心篇)

Hutool是一个小而全的Java工具类库,通过静态方法封装,降低相关API的学习成本,提高工作效率,使Java拥有函数式语言般的优雅

Hutool的存在就是为了减少代码搜索成本,避免网络上参差不齐的代码出现导致的bug。

官方文档:https://www.hutool.cn/

Hutool包含的模块

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

模块 介绍
hutool-aop JDK动态代理封装,提供非IOC下的切面支持
hutool-bloomFilter 布隆过滤,提供一些Hash算法的布隆过滤
hutool-cache 简单缓存实现
hutool-core 核心,包括Bean操作、日期、各种Util等
hutool-cron 定时任务模块,提供类Crontab表达式的定时任务
hutool-crypto 加密解密模块,提供对称、非对称和摘要算法封装
hutool-db JDBC封装后的数据操作,基于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-json JSON实现
hutool-captcha 图片验证码实现
hutool-poi 针对POI中Excel和Word的封装
hutool-socket 基于Java的NIO和AIO的Socket封装
hutool-jwt JSON Web Token (JWT)封装实现

可以根据需求对每个模块单独引入

<dependency>
    <groupId>cn.hutool</groupId>
    <artifactId>hutool-core</artifactId>
    <version>5.8.18</version>
</dependency>

也可以通过引入hutool-all方式引入所有模块。

<dependency>
    <groupId>cn.hutool</groupId>
    <artifactId>hutool-all</artifactId>
    <version>5.8.18</version>
</dependency>

如果你想像Spring-Boot一样引入Hutool,再由子模块决定用到哪些模块,你可以在父模块中加入:

<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>cn.hutool</groupId>
            <artifactId>hutool-bom</artifactId>
            <version>${hutool.version}</version>
            <type>pom</type>
            <!-- 注意这里是import -->
            <scope>import</scope>
        </dependency>
    </dependencies>
</dependencyManagement>

在子模块中就可以引入自己需要的模块了:

<dependencies>
    <dependency>
        <groupId>cn.hutool</groupId>
        <artifactId>hutool-http</artifactId>
    </dependency>
</dependencies>

hutool-core

核心,包括Bean操作、日期、各种Util等。

<dependency>
    <groupId>cn.hutool</groupId>
    <artifactId>hutool-core</artifactId>
    <version>5.8.18</version>
</dependency>

克隆

支持泛型的克隆接口和克隆类

JDK中的Cloneable接口只是一个空接口,并没有定义成员,它存在的意义仅仅是指明一个类的实例化对象支持位复制(就是对象克隆),如果不实现这个类,调用对象的clone()方法就会抛出CloneNotSupportedException异常。而且,因为clone()方法在Object对象中,返回值也是Object对象,因此克隆后我们需要自己强转下类型。

泛型克隆接口

cn.hutool.core.clone.Cloneable接口应运而生。此接口定义了一个返回泛型的成员方法,这样,实现此接口后会提示必须实现一个public的clone方法,调用父类clone方法即可:

public class Cat implements Cloneable<Cat>{
    private String name = "miaomiao";
    private int age = 2;

    @Override
    public Cat clone() {
        try {
            return (Cat) super.clone();
        } catch (CloneNotSupportedException e) {
            throw new CloneRuntimeException(e);
        }
    }
}

泛型克隆类

实现此接口必须自己实现一个public类型的clone()方法,还要调用父类(Object)的clone方法并处理异常,这太麻烦 了。于是cn.hutool.clone.CloneSupport类产生,这个类帮我们实现了上面的clone方法,因此只要继承此类,不用写任何代码即可使用clone()方法:

public class Dog extends CloneSupport<Dog>{
    private String name = "wangwang";
    private int age = 3;
}

深克隆

实现Cloneable接口后克隆的对象是浅克隆,要想实现深克隆,请使用:

ObjectUtil.cloneByStream(obj)

对象必须实现Serializable接口。

类型装换

类型转换工具类-Convert

Convert类可以说是一个工具方法类,里面封装了针对Java常见类型的转换,用于简化类型转换。Convert类中大部分方法为toXXX,参数为Object,可以实现将任意可能的类型转换为指定类型。同时支持第二个参数defaultValue用于在转换失败时返回一个默认值。

转换为字符串

int a = 1;
//aStr为"1"
String aStr = Convert.toStr(a);

long[] b = {1,2,3,4,5};
//bStr为:"[1, 2, 3, 4, 5]"
String bStr = Convert.toStr(b);

转换为指定类型数组

String[] b = { "1", "2", "3", "4" };
//结果为Integer数组
Integer[] intArray = Convert.toIntArray(b);

long[] c = {1,2,3,4,5};
//结果为Integer数组
Integer[] intArray2 = Convert.toIntArray(c);

转换为日期对象

String a = "2017-05-06";
Date value = Convert.toDate(a);

转换为集合

Object[] a = {"a", "你", "好", "", 1};
List<?> list = Convert.convert(List.class, a);
//从4.1.11开始可以这么用
List<?> list = Convert.toList(a);

泛型类型

通过convert(TypeReference<T> reference, Object value)方法,自行new一个TypeReference对象可以对嵌套泛型进行类型转换。例如,我们想转换一个对象为List<String>类型,此时传入的标准Class就无法满足要求,此时我们可以这样:

Object[] a = { "a", "你", "好", "", 1 };
List<String> list = Convert.convert(new TypeReference<List<String>>() {}, a);

通过TypeReference实例化后制定泛型类型,即可转换对象为我们想要的目标类型。

Unicode和字符串转换

String a = "我是一个小小的可爱的字符串";

//结果为:"\\u6211\\u662f\\u4e00\\u4e2a\\u5c0f\\u5c0f\\u7684\\u53ef\\u7231\\u7684\\u5b57\\u7b26\\u4e32"	
String unicode = Convert.strToUnicode(a);

//结果为:"我是一个小小的可爱的字符串"
String raw = Convert.unicodeToStr(unicode);

如果你在properties文件中写过中文,你会明白这个方法的重要性。

编码转换

在接收表单的时候,我们常常被中文乱码所困扰,其实大多数原因是使用了不正确的编码方式解码了数据。于是Convert.convertCharset方法便派上用场了,它可以把乱码转为正确的编码方式:

String a = "我不是乱码";
//转换后result为乱码
String result = Convert.convertCharset(a, CharsetUtil.UTF_8, CharsetUtil.ISO_8859_1);
String raw = Convert.convertCharset(result, CharsetUtil.ISO_8859_1, "UTF-8");
Assert.assertEquals(raw, a);

时间单位转换

Convert.convertTime方法主要用于转换时长单位,比如一个很大的毫秒,我想获得这个毫秒数对应多少分:

long a = 4535345;

//结果为:75
long minutes = Convert.convertTime(a, TimeUnit.MILLISECONDS, TimeUnit.MINUTES);

金额大小写转换

double a = 67556.32;

//结果为:"陆万柒仟伍佰伍拾陆元叁角贰分"
String digitUppercase = Convert.digitToChinese(a);

注意:转换为大写只能精确到分(小数点儿后两位),之后的数字会被忽略。

数字转中文

数字转中文方法中,只保留两位小数。

// 一万零八百八十九点七二
String f1 = Convert.numberToChinese(10889.72356, false);

// 使用金额大写
// 壹万贰仟陆佰伍拾叁
String f1 = Convert.numberToChinese(12653, true);

数字中文表示转换为数字

// 1012
String f1 = Convert.numberToChinese("一千零一十二");

自定义类型转换-ConverterRegistry

最早的类型转换类Convert,对于每一种类型转换都是用一个静态方法表示,但是这种方式有一个潜在问题,那就是扩展性不足,这导致Hutool只能满足部分类型转换的需求。

为了解决这些问题,我对Hutool中这个类做了扩展。思想如下:

  • Converter 类型转换接口,通过实现这个接口,重写convert方法,以实现不同类型的对象转换
  • ConverterRegistry 类型转换登记中心。将各种类型Convert对象放入登记中心,通过convert方法查找目标类型对应的转换器,将被转换对象转换之。在此类中,存放着默认转换器和自定义转换器,默认转换器是Hutool中预定义的一些转换器,自定义转换器存放用户自定的转换器。

使用方式如下:

int a = 3423;
ConverterRegistry converterRegistry = ConverterRegistry.getInstance();
String result = converterRegistry.convert(String.class, a);
Assert.assertEquals("3423", result);

自定义转换

Hutool的默认转换有时候并不能满足我们自定义对象的一些需求,这时我们可以使用ConverterRegistry.getInstance().putCustom()方法自定义类型转换。

自定义转换器

public class CustomConverter implements Converter<String>{
    @Override
    public String convert(Object value, String defaultValue) throws IllegalArgumentException {
        return "Custom: " + value.toString();
    }
}

注册转换器

ConverterRegistry converterRegistry = ConverterRegistry.getInstance();
//此处做为示例自定义String转换,因为Hutool中已经提供String转换,请尽量不要替换
//替换可能引发关联转换异常(例如覆盖String转换会影响全局)
converterRegistry.putCustom(String.class, CustomConverter.class);

执行转换

int a = 454553;
String result = converterRegistry.convert(String.class, a);
Assert.assertEquals("Custom: 454553", result);

注意: convert(Class type, Object value, T defaultValue, boolean isCustomFirst)方法的最后一个参数可以选择转换时优先使用自定义转换器还是默认转换器。convert(Class type, Object value, T defaultValue)和convert(Class type, Object value)两个重载方法都是使用自定义转换器优先的模式。

日期时间

日期时间工具类

  • DateUtil 针对日期时间操作提供一系列静态方法
  • DateTime 提供类似于Joda-Time中日期时间对象的封装,继承自Date类,并提供更加丰富的对象方法。
  • FastDateFormat 提供线程安全的针对Date对象的格式化和日期字符串解析支持。此对象在实际使用中并不需要感知,相关操作已经封装在DateUtil和DateTime的相关方法中。
  • DateBetween 计算两个时间间隔的类,除了通过构造新对象使用外,相关操作也已封装在DateUtil和DateTime的相关方法中。
  • TimeInterval 一个简单的计时器类,常用于计算某段代码的执行时间,提供包括毫秒、秒、分、时、天、周等各种单位的花费时长计算,对象的静态构造已封装在DateUtil中。
  • DatePattern 提供常用的日期格式化模式,包括String类型和FastDateFormat两种类型。

月份枚举

通过月份枚举可以获得某个月的最后一天

// 31
int lastDay = Month.of(Calendar.JANUARY).getLastDay(false);

另外,Hutool还定义了周枚举、季度枚举。Season.SPRING为第一季度,表示1~3月。季度的概念并不等同于季节,因为季节与月份并不对应,季度常用于统计概念。 

时间枚举

时间枚举DateUnit主要表示某个时间单位对应的毫秒数,常用于计算时间差。

例如:DateUnit.MINUTE表示分,也表示一分钟的毫秒数,可以通过调用其getMillis()方法获得其毫秒数。

日期时间工具-DateUtil

Date、long、Calendar之间的相互转换

//当前时间
Date date = DateUtil.date();
//当前时间
Date date2 = DateUtil.date(Calendar.getInstance());
//当前时间
Date date3 = DateUtil.date(System.currentTimeMillis());
//当前时间字符串,格式:yyyy-MM-dd HH:mm:ss
String now = DateUtil.now();
//当前日期字符串,格式:yyyy-MM-dd
String today= DateUtil.today();

字符串转日期

DateUtil.parse方法会自动识别一些常用格式,包括:

  • yyyy/MM/dd HH:mm:ss
  • yyyy.MM.dd HH:mm:ss
  • yyyy年MM月dd日 HH时mm分ss秒
  • yyyy-MM-dd
  • yyyy/MM/dd
  • HH:mm:ss
  • HH时mm分ss秒
  • yyyy-MM-dd HH:mm
  • yyyy-MM-dd HH:mm:ss.SSS
  • yyyyMMddHHmmss
  • yyyyMMddHHmmssSSS
  • yyyyMMdd
String dateStr = "2017-03-01";
Date date = DateUtil.parse(dateStr);

我们也可以使用自定义日期格式转化:

String dateStr = "2017-03-01";
Date date = DateUtil.parse(dateStr, "yyyy-MM-dd");

格式化日期输出

String dateStr = "2017-03-01";
Date date = DateUtil.parse(dateStr);

//结果 2017/03/01
String format = DateUtil.format(date, "yyyy/MM/dd");

//常用格式的格式化,结果:2017-03-01
String formatDate = DateUtil.formatDate(date);

//结果:2017-03-01 00:00:00
String formatDateTime = DateUtil.formatDateTime(date);

//结果:00:00:00
String formatTime = DateUtil.formatTime(date);

获取Date对象的某个部分

Date date = DateUtil.date();
//获得年的部分
DateUtil.year(date);
//获得月份,从0开始计数
DateUtil.month(date);
//获得月份枚举
DateUtil.monthEnum(date);

开始和结束时间

有的时候我们需要获得每天的开始时间、结束时间,每月的开始和结束时间等等,DateUtil也提供了相关方法:

String dateStr = "2017-03-01 22:33:23";
Date date = DateUtil.parse(dateStr);

//一天的开始,结果:2017-03-01 00:00:00
Date beginOfDay = DateUtil.beginOfDay(date);

//一天的结束,结果:2017-03-01 23:59:59
Date endOfDay = DateUtil.endOfDay(date);

日期时间偏移

日期或时间的偏移指针对某个日期增加或减少分、小时、天等等,达到日期变更的目的。Hutool也针对其做了大量封装

String dateStr = "2017-03-01 22:33:23";
Date date = DateUtil.parse(dateStr);

//结果:2017-03-03 22:33:23
Date newDate = DateUtil.offset(date, DateField.DAY_OF_MONTH, 2);

//常用偏移,结果:2017-03-04 22:33:23
DateTime newDate2 = DateUtil.offsetDay(date, 3);

//常用偏移,结果:2017-03-01 19:33:23
DateTime newDate3 = DateUtil.offsetHour(date, -3);

针对当前时间,提供了简化的偏移方法(例如昨天、上周、上个月等):

//昨天
DateUtil.yesterday()
//明天
DateUtil.tomorrow()
//上周
DateUtil.lastWeek()
//下周
DateUtil.nextWeek()
//上个月
DateUtil.lastMonth()
//下个月
DateUtil.nextMonth()

日期时间差

有时候我们需要计算两个日期之间的时间差(相差天数、相差小时数等等),Hutool将此类方法封装为between方法:

String dateStr1 = "2017-03-01 22:33:23";
Date date1 = DateUtil.parse(dateStr1);

String dateStr2 = "2017-04-01 23:33:23";
Date date2 = DateUtil.parse(dateStr2);

//相差一个月,31天
long betweenDay = DateUtil.between(date1, date2, DateUnit.DAY);

日期范围

// 创建日期范围生成器
DateTime start = DateUtil.parse("2021-01-31");
DateTime end = DateUtil.parse("2021-03-31");
DateRange range = DateUtil.range(start, end, DateField.MONTH);

// 简单使用
// 开始时间
DateRange startRange = DateUtil.range(DateUtil.parse("2017-01-01"), DateUtil.parse("2017-01-31"), DateField.DAY_OF_YEAR);
// 结束时间
DateRange endRange = DateUtil.range(DateUtil.parse("2017-01-31"), DateUtil.parse("2017-02-02"), DateField.DAY_OF_YEAR);
// 交集 返回 [2017-01-31 00:00:00]
List<DateTime> dateTimes = DateUtil.rangeContains(startRange, endRange);
// 差集 返回 [2017-02-01 00:00:00, 2017-02-02 00:00:00]
List<DateTime> dateNotTimes = DateUtil.rangeNotContains(startRange,endRange);
// 区间 返回[2017-01-01 00:00:00, 2017-01-02 00:00:00, 2017-01-03 00:00:00]
List<DateTime> rangeToList = DateUtil.rangeToList(DateUtil.parse("2017-01-01"), DateUtil.parse("2017-01-03"), DateField.DAY_OF_YEAR);

其它

//年龄
DateUtil.ageOfNow("1990-01-30");

//是否闰年
DateUtil.isLeapYear(2017);

日期时间对象-DateTime

DateTime类继承于java.util.Date类,为Date类扩展了众多简便方法,这些方法多是DateUtil静态方法的对象表现形式,使用DateTime对象可以完全替代开发中Date对象的使用。

新建对象

DateTime对象包含众多的构造方法,构造方法支持的参数有:

  • Date
  • Calendar
  • String(日期字符串,第二个参数是日期格式)
  • long 毫秒数

构建对象有两种方式:DateTime.of()和new DateTime():

Date date = new Date();
		
//new方式创建
DateTime time = new DateTime(date);
Console.log(time);

//of方式创建
DateTime now = DateTime.now();
DateTime dt = DateTime.of(date);

使用对象

DateTime的成员方法与DateUtil中的静态方法所对应,因为是成员方法,因此可以使用更少的参数操作日期时间。

示例:获取日期成员(年、月、日等)

DateTime dateTime = new DateTime("2017-01-05 12:34:23", DatePattern.NORM_DATETIME_FORMAT);
		
//年,结果:2017
int year = dateTime.year();

//季度(非季节),结果:Season.SPRING
Season season = dateTime.seasonEnum();

//月份,结果:Month.JANUARY
Month month = dateTime.monthEnum();

//日,结果:5
int day = dateTime.dayOfMonth();

对象的可变性

DateTime对象默认是可变对象(调用offset、setField、setTime方法默认变更自身),但是这种可变性有时候会引起很多问题(例如多个地方共用DateTime对象)。我们可以调用setMutable(false)方法使其变为不可变对象。在不可变模式下,offset、setField方法返回一个新对象,setTime方法抛出异常。

DateTime dateTime = new DateTime("2017-01-05 12:34:23", DatePattern.NORM_DATETIME_FORMAT);

//默认情况下DateTime为可变对象,此时offset == dateTime
DateTime offset = dateTime.offset(DateField.YEAR, 0);

//设置为不可变对象后变动将返回新对象,此时offset != dateTime
dateTime.setMutable(false);
offset = dateTime.offset(DateField.YEAR, 0);

格式化为字符串

调用toString()方法即可返回格式为yyyy-MM-dd HH:mm:ss的字符串,调用toString(String format)可以返回指定格式的字符串。

DateTime dateTime = new DateTime("2017-01-05 12:34:23", DatePattern.NORM_DATETIME_FORMAT);
//结果:2017-01-05 12:34:23
String dateStr = dateTime.toString();

//结果:2017/01/05

LocalDateTime工具-LocalDateTimeUtil

Hutool的5.4.x开始,Hutool加入了针对JDK8+日期API的封装,此工具类的功能包括LocalDateTime和LocalDate的解析、格式化、转换等操作。

日期转换

String dateStr = "2020-01-23T12:23:56";
DateTime dt = DateUtil.parse(dateStr);

// Date对象转换为LocalDateTime
LocalDateTime of = LocalDateTimeUtil.of(dt);

// 时间戳转换为LocalDateTime
of = LocalDateTimeUtil.ofUTC(dt.getTime());

日期字符串解析

// 解析ISO时间
LocalDateTime localDateTime = LocalDateTimeUtil.parse("2020-01-23T12:23:56");
// 解析自定义格式时间
localDateTime = LocalDateTimeUtil.parse("2020-01-23", DatePattern.NORM_DATE_PATTERN);

LocalDate localDate = LocalDateTimeUtil.parseDate("2020-01-23");
// 解析日期时间为LocalDate,时间部分舍弃
localDate = LocalDateTimeUtil.parseDate("2020-01-23T12:23:56", DateTimeFormatter.ISO_DATE_TIME);

日期格式化

LocalDateTime localDateTime = LocalDateTimeUtil.parse("2020-01-23T12:23:56");
// "2020-01-23 12:23:56"
String format = LocalDateTimeUtil.format(localDateTime, DatePattern.NORM_DATETIME_PATTERN);

日期偏移

final LocalDateTime localDateTime = LocalDateTimeUtil.parse("2020-01-23T12:23:56");
// 增加一天
// "2020-01-24T12:23:56"
LocalDateTime offset = LocalDateTimeUtil.offset(localDateTime, 1, ChronoUnit.DAYS);

如果是减少时间,offset第二个参数传负数即可:

// "2020-01-22T12:23:56"
offset = LocalDateTimeUtil.offset(localDateTime, -1, ChronoUnit.DAYS);

计算时间间隔

LocalDateTime start = LocalDateTimeUtil.parse("2019-02-02T00:00:00");
LocalDateTime end = LocalDateTimeUtil.parse("2020-02-02T00:00:00");

Duration between = LocalDateTimeUtil.between(start, end);

// 365
between.toDays();

 一天的开始和结束

LocalDateTime localDateTime = LocalDateTimeUtil.parse("2020-01-23T12:23:56");

// "2020-01-23T00:00"
LocalDateTime beginOfDay = LocalDateTimeUtil.beginOfDay(localDateTime);

// "2020-01-23T23:59:59.999999999"
LocalDateTime endOfDay = LocalDateTimeUtil.endOfDay(localDateTime);

计时器工具-TimeInterval

Hutool通过封装TimeInterval实现计时器功能,即可以计算方法或过程执行的时间。

TimeInterval支持分组计时,方便对比时间。

TimeInterval timer = DateUtil.timer();

//---------------------------------
//-------这是执行过程
//---------------------------------

timer.interval();//花费毫秒数
timer.intervalRestart();//返回花费时间,并重置开始时间
timer.intervalMinute();//花费分钟数

分组计时

final TimeInterval timer = new TimeInterval();

// 分组1
timer.start("1");
ThreadUtil.sleep(800);

// 分组2
timer.start("2");
ThreadUtil.sleep(900);

Console.log("Timer 1 took {} ms", timer.intervalMs("1"));
Console.log("Timer 2 took {} ms", timer.intervalMs("2"));

IO流相关

IO的操作包括读和写,应用场景包括网络操作和文件操作。IO操作在Java中是一个较为复杂的过程,我们在面对不同的场景时,要选择不同的InputStream和OutputStream实现来完成这些操作。而如果想读写字符流,还需要Reader和Writer的各种实现类。这些繁杂的实现类,一方面给我我们提供了更多的灵活性,另一方面也增加了复杂性。

io包的封装主要针对流、文件的读写封装,主要以工具类为主,提供常用功能的封装,这包括:

  • IoUtil 流操作工具类
  • FileUtil 文件读写和操作的工具类。
  • FileTypeUtil 文件类型判断工具类
  • WatchMonitor 目录、文件监听,封装了JDK1.7中的WatchService
  • ClassPathResource针对ClassPath中资源的访问封装
  • FileReader 封装文件读取
  • FileWriter 封装文件写入

除了针对JDK的读写封装外,还针对特定环境和文件扩展了流实现。

  • BOMInputStream针对含有BOM头的流读取
  • FastByteArrayOutputStream 基于快速缓冲FastByteBuffer的OutputStream,随着数据的增长自动扩充缓冲区(from blade)
  • FastByteBuffer 快速缓冲,将数据存放在缓冲集中,取代以往的单一数组(from blade)

IO工具类-IoUtil

IO工具类的存在主要针对InputStream、OutputStream、Reader、Writer封装简化,并对NIO相关操作做封装简化。总体来说,Hutool对IO的封装,主要是工具层面,我们努力做到在便捷、性能和灵活之间找到最好的平衡点。

拷贝

流的读写可以总结为从输入流读取,从输出流写出,这个过程我们定义为拷贝。这个是一个基本过程,也是文件、流操作的基础。

以文件流拷贝为例:

BufferedInputStream in = FileUtil.getInputStream("d:/test.txt");
BufferedOutputStream out = FileUtil.getOutputStream("d:/test2.txt");
long copySize = IoUtil.copy(in, out, IoUtil.DEFAULT_BUFFER_SIZE);

copy方法同样针对Reader、Writer、Channel等对象有一些重载方法,并提供可选的缓存大小。默认的,缓存大小为1024个字节,如果拷贝大文件或流数据较大,可以适当调整这个参数。

针对NIO,提供了copyByNIO方法,以便和BIO有所区别。

Stream转Reader、Writer

  • IoUtil.getReader:将InputStream转为BufferedReader用于读取字符流,它是部分readXXX方法的基础。
  • IoUtil.getWriter:将OutputStream转为OutputStreamWriter用于写入字符流,它是部分writeXXX的基础。

本质上这两个方法只是简单new一个新的Reader或者Writer对象,但是封装为工具方法配合IDE的自动提示可以大大减少查阅次数

读取流中的内容

读取流中的内容总结下来,可以分为read方法和readXXX方法。

read方法有诸多的重载方法,根据参数不同,可以读取不同对象中的内容,这包括:

  • InputStream
  • Reader
  • FileChannel

这三个重载大部分返回String字符串,为字符流读取提供极大便利。

readXXX方法主要针对返回值做一些处理,例如:

  • readBytes 返回byte数组(读取图片等)
  • readHex 读取16进制字符串
  • readObj 读取序列化对象(反序列化)
  • readLines 按行读取

toStream方法则是将某些对象转换为流对象,便于在某些情况下操作:

  • String 转换为ByteArrayInputStream
  • File 转换为FileInputStream

写入到流

  • IoUtil.write方法有两个重载方法,一个直接调用OutputStream.write方法,另一个用于将对象转换为字符串(调用toString方法),然后写入到流中。
  • IoUtil.writeObjects 用于将可序列化对象序列化后写入到流中。

write方法并没有提供writeXXX,需要自己转换为String或byte[]。

关闭

对于IO操作来说,使用频率最高(也是最容易被遗忘)的就是close操作,好在Java规范使用了优雅的Closeable接口,这样我们只需简单封装调用此接口的方法即可。

关闭操作会面临两个问题:

  • 被关闭对象为空
  • 对象关闭失败(或对象已关闭)

IoUtil.close方法很好的解决了这两个问题。

在JDK1.7中,提供了AutoCloseable接口,在IoUtil中同样提供相应的重载方法,在使用中并不能感觉到有哪些不同。

文件工具类-FileUtil

在IO操作中,文件的操作相对来说是比较复杂的,但也是使用频率最高的部分,我们几乎所有的项目中几乎都躺着一个叫做FileUtil或者FileUtils的工具类。

总体来说,FileUtil类包含以下几类操作工具:

  • 文件操作:包括文件目录的新建、删除、复制、移动、改名等
  • 文件判断:判断文件或目录是否非空,是否为目录,是否为文件等等。
  • 绝对路径:针对ClassPath中的文件转换为绝对路径文件。
  • 文件名:主文件名,扩展名的获取
  • 读操作:包括类似IoUtil中的getReader、readXXX操作
  • 写操作:包括getWriter和writeXXX操作

在FileUtil中,作者将方法名与Linux相一致,例如创建文件的方法并不是createFile,而是touch,这种统一对于熟悉Linux的人来说,大大提高了上手速度。当然,如果你不熟悉Linux,那FileUtil工具类的使用则是在帮助你学习Linux命令。这些类Linux命令的方法包括:

  • ls 列出目录和文件
  • touch 创建文件,如果父目录不存在也自动创建
  • mkdir 创建目录,会递归创建每层目录
  • del 删除文件或目录(递归删除,不判断是否为空),这个方法相当于Linux的delete命令
  • copy 拷贝文件或目录

这些方法提供了人性化的操作,例如touch方法,在创建文件的情况下会自动创建上层目录,同样mkdir也会创建父目录。

需要注意的是,del方法会删除目录而不判断其是否为空,这一方面方便了使用,另一方面也可能造成一些预想不到的后果(比如拼写错路径而删除不应该删除的目录),所以请谨慎使用此方法。

文件监听-WatchMonitor

很多时候我们需要监听一个文件的变化或者目录的变动,包括文件的创建、修改、删除,以及目录下文件的创建、修改和删除,在JDK7前我们只能靠轮询方式遍历目录或者定时检查文件的修改事件,这样效率非常低,性能也很差。因此在JDK7中引入了WatchService。不过考虑到其API并不友好,于是Hutool便针对其做了简化封装,使监听更简单,也提供了更好的功能,这包括:

  • 支持多级目录的监听(WatchService只支持一级目录),可自定义监听目录深度
  • 延迟合并触发支持(文件变动时可能触发多次modify,支持在某个时间范围内的多次修改事件合并为一个修改事件)
  • 简洁易懂的API方法,一个方法即可搞定监听,无需理解复杂的监听注册机制。
  • 多观察者实现,可以根据业务实现多个Watcher来响应同一个事件(通过WatcherChain)

在Hutool中,WatchMonitor主要针对JDK7中WatchService做了封装,针对文件和目录的变动(创建、更新、删除)做一个钩子,在Watcher中定义相应的逻辑来应对这些文件的变化。

在hutool-setting模块,使用WatchMonitor监测配置文件变化,然后自动load到内存中。WatchMonitor的使用可以避免轮询,以事件响应的方式应对文件变化。

WatchMonitor提供的事件有:

  • ENTRY_MODIFY 文件修改的事件
  • ENTRY_CREATE 文件或目录创建的事件
  • ENTRY_DELETE 文件或目录删除的事件
  • OVERFLOW 丢失的事件

这些事件对应StandardWatchEventKinds中的事件。

监听指定的事件

File file = FileUtil.file("example.properties");
//这里只监听文件或目录的修改事件
WatchMonitor watchMonitor = WatchMonitor.create(file, WatchMonitor.ENTRY_MODIFY);
watchMonitor.setWatcher(new Watcher(){
	@Override
	public void onCreate(WatchEvent<?> event, Path currentPath) {
		Object obj = event.context();
		Console.log("创建:{}-> {}", currentPath, obj);
	}

	@Override
	public void onModify(WatchEvent<?> event, Path currentPath) {
		Object obj = event.context();
		Console.log("修改:{}-> {}", currentPath, obj);
	}

	@Override
	public void onDelete(WatchEvent<?> event, Path currentPath) {
		Object obj = event.context();
		Console.log("删除:{}-> {}", currentPath, obj);
	}

	@Override
	public void onOverflow(WatchEvent<?> event, Path currentPath) {
		Object obj = event.context();
		Console.log("Overflow:{}-> {}", currentPath, obj);
	}
});

//设置监听目录的最大深入,目录层级大于制定层级的变更将不被监听,默认只监听当前层级目录
watchMonitor.setMaxDepth(3);
//启动监听
watchMonitor.start();

监听全部事件

其实我们不必实现Watcher的所有接口方法,Hutool同时提供了SimpleWatcher类,只需重写对应方法即可。

同样,如果我们想监听所有事件,可以:

WatchMonitor.createAll(file, new SimpleWatcher(){
	@Override
	public void onModify(WatchEvent<?> event, Path currentPath) {
		Console.log("EVENT modify");
	}
}).start();

createAll方法会创建一个监听所有事件的WatchMonitor,同时在第二个参数中定义Watcher来负责处理这些变动。

延迟处理监听事件

在监听目录或文件时,如果这个文件有修改操作,JDK会多次触发modify方法,为了解决这个问题,我们定义了DelayWatcher,此类通过维护一个Set将短时间内相同文件多次modify的事件合并处理触发,从而避免以上问题。

WatchMonitor monitor = WatchMonitor.createAll("d:/", new DelayWatcher(watcher, 500));
monitor.start();

文件类型判断-FileTypeUtil

在文件上传时,有时候我们需要判断文件类型。但是又不能简单的通过扩展名来判断(防止恶意脚本等通过上传到服务器上),于是我们需要在服务端通过读取文件的首部几个二进制位来判断常用的文件类型。

使用Hutool,通过调用FileTypeUtil.getType即可判断,这个方法同时提供众多的重载方法,用于读取不同的文件和流。

File file = FileUtil.file("d:/test.jpg");
String type = FileTypeUtil.getType(file);
//输出 jpg则说明确实为jpg文件
Console.log(type);

原理和局限性

这个类是通过读取文件流中前N个byte值来判断文件类型,在类中我们通过Map形式将常用的文件类型做了映射,这些映射都是网络上搜集而来。也就是说,我们只能识别有限的几种文件类型。但是这些类型已经涵盖了常用的图片、音频、视频、Office文档类型,可以应对大部分的使用场景。 

对于某些文本格式的文件我们并不能通过首部byte判断其类型,比如JSON,这类文件本质上是文本文件,我们应该读取其文本内容,通过其语法判断类型。

自定义类型

为了提高FileTypeUtil的扩展性,我们通过putFileType方法可以自定义文件类型。

FileTypeUtil.putFileType("ffd8ffe000104a464946", "new_jpg");

第一个参数是文件流的前N个byte的16进制表示,我们可以读取自定义文件查看,选取一定长度即可(长度越长越精确),第二个参数就是文件类型,然后使用FileTypeUtil.getType即可。

 注意 xlsx、docx本质上是各种XML打包为zip的结果,因此会被识别为zip格式。

文件读取-FileReader

在FileUtil中本来已经针对文件的读操作做了大量的静态封装,但是根据职责分离原则,还是有必要针对文件读取单独封装一个类,这样项目更加清晰。当然,使用FileUtil操作文件是最方便的。

在JDK中,同样有一个FileReader类,但是并不如想象中的那样好用,于是Hutool便提供了更加便捷FileReader类。 

//默认UTF-8编码,可以在构造中传入第二个参数做为编码
FileReader fileReader = new FileReader("test.properties");
String result = fileReader.readString();

FileReader提供了以下方法来快速读取文件内容:

  • readBytes
  • readString
  • readLines

同时,此类还提供了以下方法用于转换为流或者BufferedReader:

  • getReader
  • getInputStream

文件写入-FileWriter

相应的,文件读取有了,自然有文件写入类,使用方式与FileReader也类似:

FileWriter writer = new FileWriter("test.properties");
writer.write("test");

写入文件分为追加模式和覆盖模式两类,追加模式可以用append方法,覆盖模式可以用write方法,同时也提供了一个write方法,第二个参数是可选覆盖模式。

同样,此类提供了:

  • getOutputStream
  • getWriter
  • getPrintWriter

这些方法用于转换为相应的类提供更加灵活的写入操作。

文件追加-FileAppender

FileAppender类表示文件追加器。此对象持有一个一个文件,在内存中积累一定量的数据后统一追加到文件,此类只有在写入文件时打开文件,并在写入结束后关闭之。因此此类不需要关闭。

在调用append方法后会缓存于内存,只有超过容量后才会一次性写入文件,因此内存中随时有剩余未写入文件的内容,在最后必须调用flush方法将剩余内容刷入文件。

也就是说,这是一个支持缓存的文件内容追加器。此类主要用于类似于日志写出这类需求所用。

FileAppender appender = new FileAppender(file, 16, true);
appender.append("123");
appender.append("abc");
appender.append("xyz");

appender.flush();
appender.toString();

文件跟随-Tailer

有时候我们要启动一个线程实时“监控”文件的变化,比如有新内容写出到文件时,我们可以及时打印出来,这个功能非常类似于Linux下的tail -f命令。

Tailer tailer = new Tailer(FileUtil.file("f:/test/test.log"), Tailer.CONSOLE_HANDLER, 2);
tailer.start();

其中Tailer.CONSOLE_HANDLER表示文件新增内容默认输出到控制台。

public class ConsoleLineHandler implements LineHandler {
    @Override
    public void handle(String line) {
        Console.log(line);
    }
}

我们也可以实现自己的LineHandler来处理每一行数据。 

注意 此方法会阻塞当前线程

文件名工具-FileNameUtil

文件名操作工具类,主要针对文件名获取主文件名、扩展名等操作,同时针对Windows平台,清理无效字符。

此工具类在5.4.1之前是FileUtil的一部分,后单独剥离为FileNameUtil工具。

获取文件名

File file = FileUtil.file("/opt/test.txt");
// test.txt
String name = FileNameUtil.getName(file);

获取主文件名和扩展名

File file = FileUtil.file("/opt/test.txt");
// "test"
String name = FileNameUtil.mainName(file);
// "txt"
String name = FileNameUtil.extName(file);

注意,此处获取的扩展名不带.。 FileNameUtil.mainName和FileNameUtil.getPrefix等价,同理FileNameUtil.extName和FileNameUtil.getSuffix等价,保留两个方法用于适应不同用户的习惯。

资源工具-ResourceUtil

Resource接口

凡是存储数据的地方都可以归类到资源,我们想从这些地方读到byte[]或者String。那么,我们就可以抽象一个Resource接口,让代码变得简单:

read(Resource resource){...}

用户只需传入Resource的实现即可。

常见的,我们需要从资源中获取流(getStream),获取Reader来读取文本(getReader),直接读取文本(readStr),于是定义如下:

public interface Resource {
    String getName();
    URL getUrl();
    InputStream getStream();
    BufferedReader getReader(Charset charset);
    String readStr(Charset charset);
}

定义了Resource,我们就可以预定义一些特别的资源:

  • BytesResource 从byte[]中读取资源
  • InputStreamResource 从流中读取资源
  • StringResource 从String中读取资源
  • UrlResource 从URL中读取资源
  • FileResource 从文件中读取资源
  • ClassPathResource 从classpath(src/resources下)中读取资源
  • WebAppResource 从web root中读取资源
  • MultiResource 从多种资源中混合读取资源
  • MultiFileResource 从多个文件中混合读取资源

当然,我们还可以根据业务需要自己实现Resource接口,完成自定义的资源读取。

为了便于资源的查找,可以使用ResourceUtil快捷工具来获得我们需要的资源。

ResourceUtil

ResourceUtil中最核心的方法是getResourceObj,此方法可以根据传入路径是否为绝对路径而返回不同的实现。比如路径是:file:/opt/test,或者/opt/test都会被当作绝对路径,此时调用FileResource来读取数据。如果不满足以上条件,默认调用ClassPathResource读取classpath中的资源或者文件。

同样,此工具类还封装了readBytes和readStr用于快捷读取bytes和字符串。

举个例子,假设我们在classpath下放了一个test.xml,读取就变得非常简单:

String str = ResourceUtil.readUtf8Str("test.xml");

假设我们的文件存放在src/resources/config目录下,则读取改为:

String str = ResourceUtil.readUtf8Str("config/test.xml");

注意 在IDEA中,新加入文件到src/resources目录下,需要重新import项目,以便在编译时顺利把资源文件拷贝到target目录下。如果提示找不到文件,请去target目录下确认文件是否存在。

ClassPath资源访问-ClassPathResource

简单说来ClassPath就是查找class文件的路径,在Tomcat等容器下,ClassPath一般是WEB-INF/classes,在普通java程序中,我们可以通过定义-cp或者-classpath参数来定义查找class文件的路径,这些路径就是ClassPath。

为了项目方便,我们定义的配置文件肯定不能使用绝对路径,所以需要使用相对路径,这时候最好的办法就是把配置文件和class文件放在一起,便于查找。

在Java编码过程中,我们常常希望读取项目内的配置文件,按照Maven的习惯,这些文件一般放在项目的src/main/resources下,读取的时候使用:

String path = "config.properties";
InputStream in = this.class.getResource(path).openStream();

使用当前类来获得资源其实就是使用当前类的类加载器获取资源,最后openStream()方法获取输入流来读取文件流。

面对这种复杂的读取操作,Hutool 封装了ClassPathResource类来简化这种资源的读取:

ClassPathResource resource = new ClassPathResource("test.properties");
Properties properties = new Properties();
properties.load(resource.getStream());

Console.log("Properties: {}", properties);

这样就大大简化了ClassPath中资源的读取。

Hutool提供针对properties的封装类Props,同时提供更加强大的配置文件Setting类,这两个类已经针对ClassPath做过相应封装,可以以更加便捷的方式读取配置文件。

工具类

根据用途归类为XXXUtil,提供大量的工具方法。在工具类中,主要以类方法(static方法)为主,且各个类无法实例化为对象,一个方法是一个独立功能,无相互影响。

字符串工具-StrUtil

hasBlank、hasEmpty方法

就是给定一些字符串,如果一旦有空的就返回true,常用于判断好多字段是否有空的(例如web表单数据)。

这两个方法的区别是hasEmpty只判断是否为null或者空字符串(""),hasBlank则会把不可见字符也算做空,isEmpty和isBlank同理。

removePrefix、removeSuffix方法

这两个是去掉字符串的前缀后缀的,例如去掉文件名的扩展名啥。

String fileName = StrUtil.removeSuffix("pretty_girl.jpg", ".jpg")  //fileName -> pretty_girl

还有忽略大小写的removePrefixIgnoreCase和removeSuffixIgnoreCase都比较实用。

sub方法

String的subString方法越界啥的都会报异常,得自己判断。sub方法考虑了各种情况,而且index的位置还支持负数,-1表示最后一个字符,但是因为sub方法的结束index是不包含的,因此传-1最后一个字符是取不到的:

String str = "abcdefgh";
String strSub1 = StrUtil.sub(str, 2, 3); //strSub1 -> c
String strSub2 = StrUtil.sub(str, 2, -3); //strSub2 -> cde
String strSub3 = StrUtil.sub(str, 3, 2); //strSub2 -> c
String strSub4 = StrUtil.sub(str, 2, -1); // cdefg

如果想截取后半段,可以使用StrUtil.subSuf方法。

String str = "abcdefgh";
String strSub1 = StrUtil.subSuf(str, str.length()-1); //h

str、bytes方法

String.getByte(String charsetName)会抛出UnsupportedEncodingException异常。Hutool 特意提出者两个方法指定字符编码并处理异常。

format方法

String template = "{}爱{},就像老鼠爱大米";
String str = StrUtil.format(template, "我", "你"); //str -> 我爱你,就像老鼠爱大米

参数定义成了Object类型,如果传别的类型的也可以,会自动调用toString()方法的。

为了方便,这个类定义了一些比较常见的字符串常量在里面,像点、空串、换行符等等,还有HTML中的一些转义字符。

//StrUtil implements StrPool
public interface StrPool {
    char C_SPACE = ' ';
    char C_TAB = '\t';
    char C_DOT = '.';
    char C_SLASH = '/';
    char C_BACKSLASH = '\\';
    char C_CR = '\r';
    char C_LF = '\n';
    char C_UNDERLINE = '_';
    char C_COMMA = ',';
    char C_DELIM_START = '{';
    char C_DELIM_END = '}';
    char C_BRACKET_START = '[';
    char C_BRACKET_END = ']';
    char C_COLON = ':';
    char C_AT = '@';
    String TAB = "\t";
    String DOT = ".";
    String DOUBLE_DOT = "..";
    String SLASH = "/";
    String BACKSLASH = "\\";
    String CR = "\r";
    String LF = "\n";
    String CRLF = "\r\n";
    String UNDERLINE = "_";
    String DASHED = "-";
    String COMMA = ",";
    String DELIM_START = "{";
    String DELIM_END = "}";
    String BRACKET_START = "[";
    String BRACKET_END = "]";
    String COLON = ":";
    String AT = "@";
    String HTML_NBSP = "&nbsp;";
    String HTML_AMP = "&amp;";
    String HTML_QUOTE = "&quot;";
    String HTML_APOS = "&apos;";
    String HTML_LT = "&lt;";
    String HTML_GT = "&gt;";
    String EMPTY_JSON = "{}";
}

16进制工具-HexUtil

16进制一般针对无法显示的一些二进制进行显示,常用于: 图片的字符串表现形式、加密解密、编码转换。

HexUtil主要以encodeHex和decodeHex两个方法为核心,提供一些针对字符串的重载方法。

String str = "我是一个字符串";
String hex = HexUtil.encodeHexStr(str, CharsetUtil.CHARSET_UTF_8);
//hex是:
//e68891e698afe4b880e4b8aae5ad97e7aca6e4b8b2
String decodedStr = HexUtil.decodeHexStr(hex);
//解码后与str相同

URL工具-URLUtil

URL(Uniform Resource Locator)中文名为统一资源定位符,有时也被俗称为网页地址。表示为互联网上的资源,如网页或者FTP地址。在Java中,也可以使用URL表示Classpath中的资源(Resource)地址。

获取URL对象

  • URLUtil.url 通过一个字符串形式的URL地址创建对象
  • URLUtil.getURL 主要获得ClassPath下资源的URL,方便读取Classpath下的配置文件等信息。

URLUtil.normalize 标准化URL链接。对于不带http://头的地址做简单补全。

String url = "http://www.hutool.cn//aaa/bbb";
// 结果为:http://www.hutool.cn/aaa/bbb
String normalize = URLUtil.normalize(url);

url = "http://www.hutool.cn//aaa/\\bbb?a=1&b=2";
// 结果为:http://www.hutool.cn/aaa/bbb?a=1&b=2
normalize = URLUtil.normalize(url);

URLUtil.encode 封装URLEncoder.encode,将需要转换的内容(ASCII码形式之外的内容),用十六进制表示法转换出来,并在之前加上%开头。

String body = "366466 - 副本.jpg";
// 结果为:366466%20-%20%E5%89%AF%E6%9C%AC.jpg
String encode = URLUtil.encode(body);
  • URLUtil.decode 封装URLDecoder.decode,将%开头的16进制表示的内容解码。
  • URLUtil.getPath 获得path部分 URI -> http://www.aaa.bbb/search?scope=ccc&q=ddd PATH -> /search
  • URLUtil.toURI 转URL或URL字符串为URI。

XML工具-XmlUtil

在日常编码中,我们接触最多的除了JSON外,就是XML格式了,一般而言,我们首先想到的是引入Dom4j包,却不知JDK已经封装有XML解析和构建工具:w3c dom。但是由于这个API操作比较繁琐,因此Hutool中提供了XmlUtil简化XML的创建、读和写的过程。

读取xml

读取XML分为两个方法:

  • XmlUtil.readXML 读取XML文件
  • XmlUtil.parseXml 解析XML字符串为Document对象

写xml

  • XmlUtil.toStr 将XML文档转换为String
  • XmlUtil.toFile 将XML文档写入到文件

创建xml

  • XmlUtil.createXml 创建XML文档, 创建的XML默认是utf8编码,修改编码的过程是在toStr和toFile方法里,既XML在转为文本的时候才定义编码。

xml操作

通过以下工具方法,可以完成基本的节点读取操作。

  • XmlUtil.cleanInvalid 除XML文本中的无效字符
  • XmlUtil.getElements 根据节点名获得子节点列表
  • XmlUtil.getElement 根据节点名获得第一个子节点
  • XmlUtil.elementText 根据节点名获得第一个子节点
  • XmlUtil.transElements 将NodeList转换为Element列表

xml与对象转换

  • writeObjectAsXml 将可序列化的对象转换为XML写入文件,已经存在的文件将被覆盖。
  • readObjectFromXml 从XML中读取对象。

注意 这两个方法严重依赖JDK的XMLEncoder和XMLDecoder,生成和解析必须成对存在(遵循固定格式),普通的XML转Bean会报错。

Xpath操作

  • createXPath 创建XPath
  • getByXPath 通过XPath方式读取XML节点等信息
<?xml version="1.0" encoding="utf-8"?>

<returnsms> 
  <returnstatus>Success(成功)</returnstatus>  
  <message>ok</message>  
  <remainpoint>1490</remainpoint>  
  <taskID>885</taskID>  
  <successCounts>1</successCounts> 
</returnsms>
Document docResult=XmlUtil.readXML(xmlFile);
//结果为“ok”
Object value = XmlUtil.getByXPath("//returnsms/message", docResult, XPathConstants.STRING);

XmlUtil只是w3c dom的简单工具化封装,减少操作dom的难度,如果项目对XML依赖较大,依旧推荐Dom4j框架。 

对象工具-ObjectUtil

借助于lambda表达式,ObjectUtil可以完成判断给定的值是否为null,不为null执行特定逻辑的功能。

final String dateStr = null;

// 此处判断如果dateStr为null,则调用`Instant.now()`,不为null则执行`DateUtil.parse`
Instant result1 = ObjectUtil.defaultIfNull(dateStr,
		() -> DateUtil.parse(dateStr, DatePattern.NORM_DATETIME_PATTERN).toInstant(), Instant.now());

ObjectUtil.equal

比较两个对象是否相等,相等需满足以下条件之一:

  • obj1 == null && obj2 == null
  • obj1.equals(obj2)
Object a = null;
Object b = null;

// true
ObjectUtil.equals(a, b);

ObjectUtil.length

计算对象长度,如果是字符串调用其length方法,集合类调用其size方法,数组调用其length属性,其他可遍历对象遍历计算长度。

支持的类型包括:

  • CharSequence
  • Collection
  • Map
  • Iterator
  • Enumeration
  • Array
int[] array = new int[]{1,2,3,4,5};

// 5
int length = ObjectUtil.length(array);

Map<String, String> map = new HashMap<>();
map.put("a", "a1");
map.put("b", "b1");
map.put("c", "c1");

// 3
length = ObjectUtil.length(map);

ObjectUtil.contains

对象中是否包含元素。

支持的对象类型包括:

  • String
  • Collection
  • Map
  • Iterator
  • Enumeration
  • Array
int[] array = new int[]{1,2,3,4,5};
// true
final boolean contains = ObjectUtil.contains(array, 1);

判断是否为null

  • ObjectUtil.isNull
  • ObjectUtil.isNotNull

 注意:此方法不能判断对象中字段为空的情况,如果需要检查Bean对象中字段是否全空,请使用BeanUtil.isEmpty。

克隆

ObjectUtil.clone 克隆对象,如果对象实现Cloneable接口,调用其clone方法,如果实现Serializable接口,执行深度克隆,否则返回null。

class Obj extends CloneSupport<Obj> {
    public String doSomeThing() {
        return "OK";
    }
}
Obj obj = new Obj();
Obj obj2 = ObjectUtil.clone(obj);

// OK
obj2.doSomeThing();

ObjectUtil.cloneIfPossible 返回克隆后的对象,如果克隆失败,返回原对象

ObjectUtil.cloneByStream 序列化后拷贝流的方式克隆,对象必须实现Serializable接口

序列化和反序列化

  • serialize 调用JDK序列化
  • deserialize 调用JDK反序列化

反射工具-ReflectUtil

Java的反射机制,可以让语言变得更加灵活,对对象的操作也更加“动态”,因此在某些情况下,反射可以做到事半功倍的效果。Hutool针对Java的反射机制做了工具化封装,封装包括:

  • 获取构造方法
  • 获取字段
  • 获取字段值
  • 获取方法
  • 执行方法(对象方法和静态方法)

获取某个类的所有方法

Method[] methods = ReflectUtil.getMethods(ExamInfoDict.class);

获取某个类的指定方法

Method method = ReflectUtil.getMethod(ExamInfoDict.class, "getId");

构造对象

ReflectUtil.newInstance(ExamInfoDict.class);

执行方法

TestClass testClass = new TestClass();
ReflectUtil.invoke(testClass, "setA", 10);

泛型类型工具-TypeUtil

针对 java.lang.reflect.Type 的工具类封装,最主要功能包括:

  • 获取方法的参数和返回值类型(包括Type和Class)
  • 获取泛型参数类型(包括对象的泛型参数或集合元素的泛型类型)

首先我们定义一个类:

public class TestClass {
    public List<String> getList(){
        return new ArrayList<>();
    }

    public Integer intTest(Integer integer) {
        return 1;
    }
}

示例:

Method method = ReflectUtil.getMethod(TestClass.class, "intTest", Integer.class);
Type type = TypeUtil.getParamType(method, 0);
// 结果:Integer.class

Method method = ReflectUtil.getMethod(TestClass.class, "getList");
Type type = TypeUtil.getReturnType(method);
// 结果:java.util.List<java.lang.String>

Method method = ReflectUtil.getMethod(TestClass.class, "getList");
Type type = TypeUtil.getReturnType(method);
Type type2 = TypeUtil.getTypeArgument(type);
// 结果:String.class

分页工具-PageUtil

分页工具类并不是数据库分页的封装,而是分页方式的转换。在我们手动分页的时候,常常使用页码+每页个数的方式,但是有些数据库需要使用开始位置和结束位置来表示。很多时候这种转换容易出错(边界问题),于是封装了PageUtil工具类。

transToStartEnd

将页数和每页条目数转换为开始位置和结束位置。 此方法用于不包括结束位置的分页方法。

例如:

  • 页码:0,每页10 -> [0, 10]
  • 页码:1,每页10 -> [10, 20]
int[] startEnd1 = PageUtil.transToStartEnd(0, 10);//[0, 10]
int[] startEnd2 = PageUtil.transToStartEnd(1, 10);//[10, 20]

方法中,页码从0开始,位置从0开始。

totalPage

根据总数计算总页数

int totalPage = PageUtil.totalPage(20, 3);//7

分页彩虹算法

在页面上显示下一页时,常常需要显示前N页和后N页,PageUtil.rainbow作用于此。

例如我们当前页为第5页,共有20页,只显示6个页码,显示的分页列表应为:

上一页 3 4 [5] 6 7 8 下一页
//参数意义分别为:当前页、总页数、每屏展示的页数
int[] rainbow = PageUtil.rainbow(5, 20, 6);
//结果:[3, 4, 5, 6, 7, 8]

类工具-ClassUtil

这个工具主要是封装了一些反射的方法,使调用更加方便。而这个类中最有用的方法是scanPackage方法,这个方法会扫描classpath下所有类,这个在Spring中是特性之一。 

scanPackage方法

此方法唯一的参数是包的名称,返回结果为此包以及子包下所有的类。方法使用很简单,但是过程复杂一些,包扫面首先会调用 getClassPaths方法获得ClassPath,然后扫描ClassPath,如果是目录,扫描目录下的类文件,或者jar文件。如果是jar包,则直接从jar包中获取类名。

命令行工具-RuntimeUtil

在Java世界中,如果想与其它语言打交道,处理调用接口,或者JNI,就是通过本地命令方式调用了。Hutool封装了JDK的Process类,用于执行命令行命令(在Windows下是cmd,在Linux下是shell命令)。

  • exec 执行命令行命令,返回Process对象,Process可以读取执行命令后的返回内容的流
  • execForStr 执行系统命令,返回字符串
  • execForLines 执行系统命令,返回行列表
//在Windows下可以获取网卡信息
String str = RuntimeUtil.execForStr("ipconfig");

数字工具-NumberUtil

加减乘除

  • NumberUtil.add 针对数字类型做加法
  • NumberUtil.sub 针对数字类型做减法
  • NumberUtil.mul 针对数字类型做乘法
  • NumberUtil.div 针对数字类型做除法,并提供重载方法用于规定除不尽的情况下保留小数位数和舍弃方式。

以上四种运算都会将double转为BigDecimal后计算,解决float和double类型无法进行精确计算的问题。这些方法常用于商业计算。

保留小数

NumberUtil.round 方法主要封装BigDecimal中的方法来保留小数,返回BigDecimal,这个方法更加灵活,可以选择四舍五入或者全部舍弃等模式。

double te1=123456.123456;
double te2=123456.128456;
Console.log(round(te1,4));//结果:123456.1235
Console.log(round(te2,4));//结果:123456.1285

NumberUtil.roundStr 方法主要封装String.format方法,舍弃方式采用四舍五入。

double te1=123456.123456;
double te2=123456.128456;
Console.log(roundStr(te1,2));//结果:123456.12
Console.log(roundStr(te2,2));//结果:123456.13

decimalFormat

针对 DecimalFormat.format进行简单封装。按照固定格式对double或long类型的数字做格式化操作。

long c=299792458;//光速
String format = NumberUtil.decimalFormat(",###", c);//299,792,458

格式中主要以 # 和 0 两种占位符号来指定数字长度。0 表示如果位数不足则以 0 填充,# 表示只要有可能就把数字拉上这个位置。

  • 0 -> 取一位整数
  • 0.00 -> 取一位整数和两位小数
  • 00.000 -> 取两位整数和三位小数
  • # -> 取所有整数部分
  • #.##% -> 以百分比方式计数,并取两位小数
  • #.#####E0 -> 显示为科学计数法,并取五位小数
  • ,### -> 每三位以逗号进行分隔,例如:299,792,458
  • 光速大小为每秒,###米 -> 将格式嵌入文本

是否为数字

  • NumberUtil.isNumber 是否为数字
  • NumberUtil.isInteger 是否为整数
  • NumberUtil.isDouble 是否为浮点数
  • NumberUtil.isPrimes 是否为质数

随机数

  • NumberUtil.generateRandomNumber 生成不重复随机数 根据给定的最小数字和最大数字,以及随机数的个数,产生指定的不重复的数组。
  • NumberUtil.generateBySet 生成不重复随机数 根据给定的最小数字和最大数字,以及随机数的个数,产生指定的不重复的数组。

整数列表

NumberUtil.range 方法根据范围和步进,生成一个有序整数列表。 NumberUtil.appendRange 将给定范围内的整数添加到已有集合中。

其他方法

  • NumberUtil.factorial 阶乘
  • NumberUtil.sqrt 平方根
  • NumberUtil.divisor 最大公约数
  • NumberUtil.multiple 最小公倍数
  • NumberUtil.getBinaryStr 获得数字对应的二进制字符串
  • NumberUtil.binaryToInt 二进制转int
  • NumberUtil.binaryToLong 二进制转long
  • NumberUtil.compare 比较两个值的大小
  • NumberUtil.toStr 数字转字符串,自动并去除尾小数点儿后多余的0

数组工具-ArrayUtil

数组工具类主要是解决对象数组(包括包装类型数组)和原始类型数组使用方法不统一的问题。

判空

数组的判空类似于字符串的判空,标准是null或者数组长度为0,ArrayUtil中封装了针对原始类型和泛型数组的判空和判非空:

判空

int[] a = {};
int[] b = null;
ArrayUtil.isEmpty(a);
ArrayUtil.isEmpty(b);

判断非空

int[] a = {1,2};
ArrayUtil.isNotEmpty(a);

新建泛型数组

Array.newInstance并不支持泛型返回值,在此封装此方法使之支持泛型返回值。

String[] newArray = ArrayUtil.newArray(String.class, 3);

调整大小

使用 ArrayUtil.resize方法生成一个新的重新设置大小的数组。

合并数组

ArrayUtil.addAll方法采用可变参数方式,将多个泛型数组合并为一个数组。

克隆

数组本身支持clone方法,因此确定为某种类型数组时调用ArrayUtil.clone(T[]),不确定类型的使用ArrayUtil.clone(T),两种重载方法在实现上有所不同,但是在使用中并不能感知出差别。

泛型数组调用原生克隆

Integer[] b = {1,2,3};
Integer[] cloneB = ArrayUtil.clone(b);
Assert.assertArrayEquals(b, cloneB);

非泛型数组(原始类型数组)调用第二种重载方法

int[] a = {1,2,3};
int[] clone = ArrayUtil.clone(a);
Assert.assertArrayEquals(a, clone);

有序列表生成

ArrayUtil.range方法有三个重载,这三个重载配合可以实现支持步进的有序数组或者步进为1的有序数组。

拆分数组

ArrayUtil.split方法用于拆分一个byte数组,将byte数组平均分成几等份,常用于消息拆分。

过滤

ArrayUtil.filter方法用于过滤已有数组元素,只针对泛型数组操作,原始类型数组并未提供。 方法中Filter接口用于返回boolean值决定是否保留。

过滤数组,只保留偶数

Integer[] a = {1,2,3,4,5,6};
// [2,4,6]
Integer[] filter = ArrayUtil.filter(a, (Editor<Integer>) t -> (t % 2 == 0) ? t : null);

对已有数组编辑,获得编辑后的值

Integer[] a = {1, 2, 3, 4, 5, 6};
// [1, 20, 3, 40, 5, 60]
Integer[] filter = ArrayUtil.filter(a, (Editor<Integer>) t -> (t % 2 == 0) ? t * 10 : t);

编辑

修改元素对象,此方法会修改原数组。

Integer[] a = {1, 2, 3, 4, 5, 6};
// [1, 20, 3, 40, 5, 60]
ArrayUtil.edit(a, t -> (t % 2 == 0) ? t * 10 : t);

zip

ArrayUtil.zip方法传入两个数组,第一个数组为key,第二个数组对应位置为value

String[] keys = {"a", "b", "c"};
Integer[] values = {1,2,3};
Map<String, Integer> map = ArrayUtil.zip(keys, values, true);

//{a=1, b=2, c=3}

是否包含元素

ArrayUtil.contains方法只针对泛型数组,检测指定元素是否在数组中。

包装和拆包

在原始类型元素和包装类型中,Java实现了自动包装和拆包,但是相应的数组无法实现,于是便是用ArrayUtil.wrap和ArrayUtil.unwrap对原始类型数组和包装类型数组进行转换。

判断对象是否为数组

ArrayUtil.isArray方法封装了obj.getClass().isArray()。

转为字符串

ArrayUtil.toString 通常原始类型的数组输出为字符串时无法正常显示,于是封装此方法可以完美兼容原始类型数组和包装类型数组的转为字符串操作。

ArrayUtil.join 方法使用间隔符将一个数组转为字符串,比如[1,2,3,4]这个数组转为字符串,间隔符使用“-”的话,结果为 1-2-3-4,join方法同样支持泛型数组和原始类型数组。

随机工具-RandomUtil

RandomUtil主要针对JDK中Random对象做封装,严格来说,Java产生的随机数都是伪随机数,因此Hutool封装后产生的随机结果也是伪随机结果。不过这种随机结果对于大多数情况已经够用。

RandomUtil.randomInt 获得指定范围内的随机数

例如我们想产生一个[10, 100)的随机数,则:

int c = RandomUtil.randomInt(10, 100);

RandomUtil.randomBytes 随机bytes,一般用于密码或者salt生成

byte[] c = RandomUtil.randomBytes(10);

RandomUtil.randomEle 随机获得列表中的元素

RandomUtil.randomEleSet 随机获得列表中的一定量的不重复元素,返回Set

Set<Integer> set = RandomUtil.randomEleSet(CollUtil.newArrayList(1, 2, 3, 4, 5, 6), 2);

RandomUtil.randomString 获得一个随机的字符串(只包含数字和字符)

RandomUtil.randomNumbers 获得一个只包含数字的字符串

RandomUtil.weightRandom 权重随机生成器,传入带权重的对象,然后根据权重随机获取对象

唯一ID工具-IdUtil

在分布式环境中,唯一ID生成应用十分广泛,生成方法也多种多样,Hutool针对一些常用生成策略做了简单封装。

唯一ID生成器的工具类,涵盖了:

  • UUID
  • ObjectId(MongoDB)
  • Snowflake(Twitter)

UUID

UUID全称通用唯一识别码(universally unique identifier),JDK通过java.util.UUID提供了 Leach-Salz 变体的封装。在Hutool中,生成一个UUID字符串方法如下:

//生成的UUID是带-的字符串,类似于:a5c8a5e8-df2b-4706-bea4-08d0939410e3
String uuid = IdUtil.randomUUID();

//生成的是不带-的字符串,类似于:b17f24ff026d40949c85a24f4f375d42
String simpleUUID = IdUtil.simpleUUID();

说明 Hutool重写java.util.UUID的逻辑,对应类为cn.hutool.core.lang.UUID,使生成不带-的UUID字符串不再需要做字符替换,性能提升一倍左右。

ObjectId

ObjectId是MongoDB数据库的一种唯一ID生成策略,是UUID version1的变种。

Hutool针对此封装了cn.hutool.core.lang.ObjectId,快捷创建方法为:

//生成类似:5b9e306a4df4f8c54a39fb0c
String id = ObjectId.next();

//方法2:从Hutool-4.1.14开始提供
String id2 = IdUtil.objectId();

Snowflake

分布式系统中,有一些需要使用全局唯一ID的场景,有些时候我们希望能使用一种简单一些的ID,并且希望ID能够按照时间有序生成。Twitter的Snowflake 算法就是这种生成器。

使用方法如下:

//参数1为终端ID
//参数2为数据中心ID
Snowflake snowflake = IdUtil.getSnowflake(1, 1);
long id = snowflake.nextId();

//简单使用
long id = IdUtil.getSnowflakeNextId();
String id = IdUtil.getSnowflakeNextIdStr();

注意 IdUtil.createSnowflake每次调用会创建一个新的Snowflake对象,不同的Snowflake对象创建的ID可能会有重复,因此请自行维护此对象为单例,或者使用IdUtil.getSnowflake使用全局单例对象。

压缩工具-ZipUtil

在Java中,对文件、文件夹打包,压缩是一件比较繁琐的事情,我们常常引入Zip4j进行此类操作。但是很多时候,JDK中的zip包就可满足我们大部分需求。ZipUtil就是针对java.util.zip做工具化封装,使压缩解压操作可以一个方法搞定,并且自动处理文件和目录的问题,不再需要用户判断,压缩后的文件也会自动创建文件,自动创建父目录,大大简化的压缩解压的复杂度。

Zip

ZipUtil.zip 方法提供一系列的重载方法,满足不同需求的压缩需求,这包括:

  • 打包到当前目录(可以打包文件,也可以打包文件夹,根据路径自动判断)
//将aaa目录下的所有文件目录打包到d:/aaa.zip
ZipUtil.zip("d:/aaa");
  • 指定打包后保存的目的地,自动判断目标是文件还是文件夹
//将aaa目录下的所有文件目录打包到d:/bbb/目录下的aaa.zip文件中
// 此处第二个参数必须为文件,不能为目录
ZipUtil.zip("d:/aaa", "d:/bbb/aaa.zip");

//将aaa目录下的所有文件目录打包到d:/bbb/目录下的ccc.zip文件中
ZipUtil.zip("d:/aaa", "d:/bbb/ccc.zip");
  • 可选是否包含被打包的目录。比如我们打包一个照片的目录,打开这个压缩包有可能是带目录的,也有可能是打开压缩包直接看到的是文件。zip方法增加一个boolean参数可选这两种模式,以应对众多需求。
//将aaa目录以及其目录下的所有文件目录打包到d:/bbb/目录下的ccc.zip文件中
ZipUtil.zip("d:/aaa", "d:/bbb/ccc.zip", true);
  • 多文件或目录压缩。可以选择多个文件或目录一起打成zip包。
ZipUtil.zip(FileUtil.file("d:/bbb/ccc.zip"), false, 
    FileUtil.file("d:/test1/file1.txt"),
    FileUtil.file("d:/test1/file2.txt"),
    FileUtil.file("d:/test2/file1.txt"),
    FileUtil.file("d:/test2/file2.txt")
);

ZipUtil.unzip 解压。同样提供几个重载,满足不同需求。

//将test.zip解压到e:\\aaa目录下,返回解压到的目录
File unzip = ZipUtil.unzip("E:\\aaa\\test.zip", "e:\\aaa");

Gzip

Gzip是网页传输中广泛使用的压缩方式,Hutool同样提供其工具方法简化其过程。

ZipUtil.gzip 压缩,可压缩字符串,也可压缩文件 ZipUtil.unGzip 解压Gzip文件

Zlib

ZipUtil.zlib 压缩,可压缩字符串,也可压缩文件 ZipUtil.unZlib 解压zlib文件。

注意 ZipUtil默认情况下使用系统编码,也就是说:

  • 如果你在命令行下运行,则调用系统编码(一般Windows下为GBK、Linux下为UTF-8)
  • 如果你在IDE(如Eclipse)下运行代码,则读取的是当前项目的编码(详细请查阅IDE设置,我的项目默认都是UTF-8编码,因此解压和压缩都是用这个编码)

引用工具-ReferenceUtil

引用工具类,主要针对Reference 工具化封装。

主要封装包括:

  • SoftReference 软引用,在GC报告内存不足时会被GC回收
  • WeakReference 弱引用,在GC时发现弱引用会回收其对象
  • PhantomReference 虚引用,在GC时发现虚引用对象,会将PhantomReference插入ReferenceQueue。此时对象未被真正回收,要等到ReferenceQueue被真正处理后才会被回收

create方法根据类型枚举创建引用。

身份证工具-IdcardUtil

在日常开发中,我们对身份证的验证主要是正则方式(位数,数字范围等),但是中国身份证,尤其18位身份证每一位都有严格规定,并且最后一位为校验位。而我们在实际应用中,针对身份证的验证理应严格至此。于是IdcardUtil应运而生。

IdcardUtil现在支持大陆15位、18位身份证,港澳台10位身份证。

工具中主要的方法包括:

  • isValidCard 验证身份证是否合法
  • convert15To18 身份证15位转18位
  • getBirthByIdCard 获取生日
  • getAgeByIdCard 获取年龄
  • getYearByIdCard 获取生日年
  • getMonthByIdCard 获取生日月
  • getDayByIdCard 获取生日天
  • getGenderByIdCard 获取性别
  • getProvinceByIdCard 获取省份
String ID_18 = "321083197812162119";
String ID_15 = "150102880730303";

//是否有效
boolean valid = IdcardUtil.isValidCard(ID_18);
boolean valid15 = IdcardUtil.isValidCard(ID_15);

//转换
String convert15To18 = IdcardUtil.convert15To18(ID_15);
Assert.assertEquals(convert15To18, "150102198807303035");

//年龄
DateTime date = DateUtil.parse("2017-04-10");
		
int age = IdcardUtil.getAgeByIdCard(ID_18, date);
Assert.assertEquals(age, 38);

int age2 = IdcardUtil.getAgeByIdCard(ID_15, date);
Assert.assertEquals(age2, 28);

//生日
String birth = IdcardUtil.getBirthByIdCard(ID_18);
Assert.assertEquals(birth, "19781216");

String birth2 = IdcardUtil.getBirthByIdCard(ID_15);
Assert.assertEquals(birth2, "19880730");

//省份
String province = IdcardUtil.getProvinceByIdCard(ID_18);
Assert.assertEquals(province, "江苏");

String province2 = IdcardUtil.getProvinceByIdCard(ID_15);
Assert.assertEquals(province2, "内蒙古");

信息脱敏工具-DesensitizedUtil

在数据处理或清洗中,可能涉及到很多隐私信息的脱敏工作,因此Hutool针对常用的信息封装了一些脱敏方法。

现阶段支持的脱敏数据类型包括:

  • 用户id
  • 中文姓名
  • 身份证号
  • 座机号
  • 手机号
  • 地址
  • 电子邮件
  • 密码
  • 中国大陆车牌,包含普通车辆、新能源车辆
  • 银行卡

整体来说,所谓脱敏就是隐藏掉信息中的一部分关键信息,用*代替,自定义隐藏可以使用StrUtil.hide方法完成

// 5***************1X
DesensitizedUtil.idCardNum("51343620000320711X", 1, 2);

// 180****1999
DesensitizedUtil.mobilePhone("18049531999");

// **********
DesensitizedUtil.password("1234567890");

SPI加载工具-ServiceLoaderUtil

SPI(Service Provider Interface),是一种服务发现机制。它通过在ClassPath路径下的META-INF/services文件夹查找文件,自动加载文件里所定义的类。

定义一个接口:

package cn.hutool.test.spi;
public interface SPIService {
    void execute();
}

有两个实现:

package cn.hutool.test.spi;
public class SpiImpl1 implements SPIService{
    public void execute() {
        Console.log("SpiImpl1.execute()");
    }
}

package cn.hutool.test.spi;
public class SpiImpl2 implements SPIService{
    public void execute() {
        Console.log("SpiImpl2.execute()");
    }
}

然后在classpath的META-INF/services下创建一个文件,叫cn.hutool.test.spi.SPIService,内容为:

cn.hutool.test.spi.SpiImpl1
cn.hutool.test.spi.SpiImpl2

加载第一个可用服务,如果用户定义了多个接口实现类,只获取第一个不报错的服务。这个方法多用于同一接口多种实现的自动甄别加载, 通过判断jar是否引入,自动找到实现类。

SPIService service = ServiceLoaderUtil.loadFirstAvailable(SPIService.class);
service.execute();

字符编码工具-CharsetUtil

CharsetUtil主要针对编码操作做了工具化封装,同时提供了一些常用编码常量。

编码字符串转为Charset对象

CharsetUtil.charset方法用于将编码形式字符串转为Charset对象

转换编码

CharsetUtil.convert方法主要是在两种编码中转换。主要针对因为编码识别错误而导致的乱码问题的一种解决方法。

系统默认编码

CharsetUtil.defaultCharset方法是Charset.defaultCharset()的封装方法。返回系统编码。 CharsetUtil.defaultCharsetName方法返回字符串形式的编码类型。

类加载工具-ClassLoaderUtil

提供ClassLoader相关的工具类,例如类加载(Class.forName包装)等

getContextClassLoader

获取当前线程的ClassLoader,本质上调用Thread.currentThread().getContextClassLoader()

getClassLoader

按照以下顺序规则查找获取ClassLoader:

  • 获取当前线程的ContextClassLoader
  • 获取ClassLoaderUtil类对应的ClassLoader
  • 获取系统ClassLoader(ClassLoader.getSystemClassLoader())

loadClass

加载类,通过传入类的字符串,返回其对应的类名,使用默认ClassLoader并初始化类(调用static模块内容和可选的初始化static属性)

扩展Class.forName方法,支持以下几类类名的加载:

  • 原始类型,例如:int
  • 数组类型,例如:int[]、Long[]、String[]
  • 内部类,例如:java.lang.Thread.State会被转为java.lang.Thread$State加载

同时提供loadPrimitiveClass方法用于快速加载原始类型的类。包括原始类型、原始类型数组和void。

isPresent

指定类是否被提供,通过调用loadClass方法尝试加载指定类名的类,如果加载失败返回false。

加载失败的原因可能是此类不存在或其关联引用类不存在。

语言特性

主要针对JDK中的一些数据结构和接口的完善,包括:

  • caller 获取方法调用者
  • copier 复制器抽象接口
  • func 函数接口
  • hash 哈希算法
  • loader 加载器抽象接口
  • mutable 提供可变对象
  • tree 提供树状结构

HashMap扩展-Dict

Dict继承HashMap,其key为String类型,value为Object类型,通过实现BasicTypeGetter接口提供针对不同类型的get方法,同时提供针对Bean的转换方法,大大提高Map的灵活性。

创建

Dict dict = Dict.create()
	.set("key1", 1)//int
	.set("key2", 1000L)//long
	.set("key3", DateTime.now());//Date

通过链式构造,创建Dict对象,同时可以按照Map的方式使用。

获取指定类型的值

Long v2 = dict.getLong("key2");//1000

单例工具-Singleton

public class SingletonDemo {

    /**
     * 动物接口
     * @author loolly
     *
     */
    public static interface Animal{
        public void say();
    }

    /**
     * 狗实现
     * @author loolly
     *
     */
    public static class Dog implements Animal{
        @Override
        public void say() {
            System.out.println("汪汪");
        }
    }

    /**
     * 猫实现
     * @author loolly
     *
     */
    public static class Cat implements Animal{
        @Override
        public void say() {
            System.out.println("喵喵");
        }
    }

    public static void main(String[] args) {
        Animal dog = Singleton.get(Dog.class);
        Animal cat = Singleton.get(Cat.class);

        //单例对象每次取出为同一个对象,除非调用Singleton.destroy()或者remove方法
        System.out.println(dog == Singleton.get(Dog.class));        //True
        System.out.println(cat == Singleton.get(Cat.class));            //True

        dog.say();        //汪汪
        cat.say();        //喵喵
    }
}

断言-Assert

Java中有assert关键字,但是存在许多问题:

  • assert关键字需要在运行时候显式开启才能生效,否则你的断言就没有任何意义。
  • 用assert代替if是陷阱之二。assert的判断和if语句差不多,但两者的作用有着本质的区别:assert关键字本意上是为测试调试程序时使用的,但如果不小心用assert来控制了程序的业务流程,那在测试调试结束后去掉assert关键字就意味着修改了程序的正常的逻辑。
  • assert断言失败将面临程序的退出。

因此,并不建议使用此关键字。相应的,在Hutool中封装了更加友好的Assert类,用于断言判定。

Assert类更像是Junit中的Assert类,也很像Guava中的Preconditions,主要作用是在方法或者任何地方对参数的有效性做校验。当不满足断言条件时,会抛出IllegalArgumentException或IllegalStateException异常。

String a = null;
cn.hutool.lang.Assert.isNull(a);

更多方法:

  • isTrue 是否True
  • isNull 是否是null值,不为null抛出异常
  • notNull 是否非null值
  • notEmpty 是否非空
  • notBlank 是否非空白符
  • notContain 是否为子串
  • notEmpty 是否非空
  • noNullElements 数组中是否包含null元素
  • isInstanceOf 是否类实例
  • isAssignable 是子类和父类关系
  • state 会抛出IllegalStateException异常

二进码十进数-BCD

BCD码(Binary-Coded Decimal)亦称二进码十进数或二-十进制代码,BCD码这种编码形式利用了四个位元来储存一个十进制的数码,使二进制和十进制之间的转换得以快捷的进行。

这种编码技巧最常用于会计系统的设计里,因为会计制度经常需要对很长的数字串作准确的计算。相对于一般的浮点式记数法,采用BCD码,既可保存数值的精确度,又可免却使电脑作浮点运算时所耗费的时间。此外,对于其他需要高精确度的计算,BCD编码亦很常用。

BCD码是四位二进制码, 也就是将十进制的数字转化为二进制, 但是和普通的转化有一点不同, 每一个十进制的数字0-9都对应着一个四位的二进制码,对应关系如下: 十进制0 对应 二进制0000 ;十进制1 对应二进制0001 ....... 9 1001,接下来的10就有两个上述的码来表示,10 表示为00010000,也就是BCD码是遇见1001就产生进位,不像普通的二进制码,到1111才产生进位10000。

String strForTest = "123456ABCDEF";
		
//转BCD
byte[] bcd = BCD.strToBcd(strForTest);
//解码BCD
String str = BCD.bcdToStr(bcd);
Assert.assertEquals(strForTest, str);

控制台打印封装-Console

编码中我们常常需要调试输出一些信息,除了打印日志,最长用的要数System.out和System.err

比如我们打印一个Hello World,可以这样写:

System.out.println("Hello World");

但是面对纷杂的打印需求,System.out.println无法满足,比如:

  • 不支持参数,对象打印需要拼接字符串
  • 不能直接打印数组,需要手动调用Arrays.toString

考虑到以上问题,Hutool 封装了Console对象。

Console.log 这个方法基本等同于System.out.println,但是支持类似于Slf4j的字符串模板语法,同时也会自动将对象(包括数组)转为字符串形式。

String[] a = {"abc", "bcd", "def"};
Console.log(a);//控制台输出:[abc, bcd, def]

Console.log("This is Console log for {}.", "test");
//控制台输出:This is Console log for test.

Console.error 这个方法基本等同于System.err.println,,但是支持类似于Slf4j的字符串模板语法,同时也会自动将对象(包括数组)转为字符串形式。

字段验证器-Validator

验证给定字符串是否满足指定条件,一般用在表单字段验证里。

此类中全部为静态方法。

判断验证

直接调用Validator.isXXX(String value)即可验证字段,返回是否通过验证。

例如:

boolean isEmail = Validator.isEmail("loolly@gmail.com")

表示验证给定字符串是否符合合电子邮件格式。

如果Validator里的方法无法满足自己的需求,那还可以调用

Validator.isMatchRegex("需要验证字段的正则表达式", "被验证内容")

来通过正则表达式灵活的验证内容。

异常验证

除了手动判断,我们有时需要在判断未满足条件时抛出一个异常,Validator同样提供异常验证机制:

Validator.validateChinese("我是一段zhongwen", "内容中包含非中文");

因为内容中包含非中文字符,因此会抛出ValidateException。

树结构工具-TreeUtil

定义结构

我们假设要构建一个菜单,可以实现系统管理和店铺管理,菜单的样子如下:

系统管理
    |- 用户管理
    |- 添加用户

店铺管理
    |- 商品管理
    |- 添加商品

那这种结构如何保存在数据库中呢?一般是这样的:

我们看到,每条数据根据parentId相互关联并表示层级关系,parentId在这里也叫外键。

构建Tree

// 构建node列表
List<TreeNode<String>> nodeList = CollUtil.newArrayList();

nodeList.add(new TreeNode<>("1", "0", "系统管理", 5));
nodeList.add(new TreeNode<>("11", "1", "用户管理", 222222));
nodeList.add(new TreeNode<>("111", "11", "用户添加", 0));
nodeList.add(new TreeNode<>("2", "0", "店铺管理", 1));
nodeList.add(new TreeNode<>("21", "2", "商品管理", 44));
nodeList.add(new TreeNode<>("221", "2", "商品管理2", 2));

TreeNode表示一个抽象的节点,也表示数据库中一行数据。 如果有其它数据,可以调用setExtra添加扩展字段。

// 0表示最顶层的id是0
List<Tree<String>> treeList = TreeUtil.build(nodeList, "0");

因为两个Tree是平级的,再没有上层节点,因此为List。

自定义字段名

//配置
TreeNodeConfig treeNodeConfig = new TreeNodeConfig();
// 自定义属性名 都要默认值的
treeNodeConfig.setWeightKey("order");
treeNodeConfig.setIdKey("rid");
// 最大递归深度
treeNodeConfig.setDeep(3);

//转换器 (含义:找出父节点为字符串零的所有子节点, 并递归查找对应的子节点, 深度最多为 3)
List<Tree<String>> treeNodes = TreeUtil.<TreeNode, String>build(nodeList, "0", treeNodeConfig,
		(treeNode, tree) -> {
			tree.setId(treeNode.getId());
			tree.setParentId(treeNode.getParentId());
			tree.setWeight(treeNode.getWeight());
			tree.setName(treeNode.getName());
			// 扩展属性 ...
			tree.putExtra("extraField", 666);
			tree.putExtra("other", new Object());
		});

通过TreeNodeConfig我们可以自定义节点的名称、关系节点id名称,这样就可以和不同的数据库做对应。

JavaBean

Bean工具-BeanUtil

Bean工具类主要是针对这些setXXX和getXXX方法进行操作,比如将Bean对象转为Map等等。

定义两个bean:

// Lombok注解
@Data
public class Person{
    private String name;
    private int age;
}

// Lombok注解
@Data
public class SubPerson extends Person {
    public static final String SUBNAME = "TEST";

    private UUID id;
    private String subName;
    private Boolean isSlow;
}

内省 Introspector

把一类中需要进行设置和获得的属性访问权限设置为private(私有的)让外部的使用者看不见摸不着,而通过public(共有的)set和get方法来对其属性的值来进行设置和获得,而内部的操作具体是怎样的?外界使用的人不用知道,这就称为内省。

Hutool中对内省的封装包括:

BeanUtil.getPropertyDescriptors 获得Bean字段描述数组

PropertyDescriptor[] propertyDescriptors = BeanUtil.getPropertyDescriptors(SubPerson.class);

BeanUtil.getFieldNamePropertyDescriptorMap 获得字段名和字段描述Map

BeanUtil.getPropertyDescriptor 获得Bean类指定属性描述

Bean属性注入

BeanUtil.fillBeanWithMap 使用Map填充bean

HashMap<String, Object> map = CollUtil.newHashMap();
map.put("name", "Joe");
map.put("age", 12);
map.put("openId", "DFDFSDFWERWER");

SubPerson person = BeanUtil.fillBeanWithMap(map, new SubPerson(), false);

BeanUtil.fillBeanWithMapIgnoreCase 使用Map填充bean,忽略大小写

HashMap<String, Object> map = CollUtil.newHashMap();
map.put("Name", "Joe");
map.put("aGe", 12);
map.put("openId", "DFDFSDFWERWER");
SubPerson person = BeanUtil.fillBeanWithMapIgnoreCase(map, new SubPerson(), false);

同时提供了map转bean的方法,与fillBean不同的是,此处并不是传Bean对象,而是Bean类,Hutool会自动调用默认构造方法创建对象。当然,前提是Bean类有默认构造方法(空构造),这些方法有:

BeanUtil.toBean

HashMap<String, Object> map = CollUtil.newHashMap();
map.put("a_name", "Joe");
map.put("b_age", 12);
// 设置别名,用于对应bean的字段名
HashMap<String, String> mapping = CollUtil.newHashMap();
mapping.put("a_name", "name");
mapping.put("b_age", "age");
Person person = BeanUtil.toBean(map, Person.class, CopyOptions.create().setFieldMapping(mapping));

BeanUtil.toBeanIgnoreCase

HashMap<String, Object> map = CollUtil.newHashMap();
map.put("Name", "Joe");
map.put("aGe", 12);

Person person = BeanUtil.toBeanIgnoreCase(map, Person.class, false);

Bean转为Map

BeanUtil.beanToMap方法则是将一个Bean对象转为Map对象。

SubPerson person = new SubPerson();
person.setAge(14);
person.setOpenid("11213232");
person.setName("测试A11");
person.setSubName("sub名字");

Map<String, Object> map = BeanUtil.beanToMap(person);

Bean转Bean

Bean之间的转换主要是相同属性的复制,因此方法名为copyProperties,此方法支持Bean和Map之间的字段复制。

BeanUtil.copyProperties方法同样提供一个CopyOptions参数用于自定义属性复制。

SubPerson p1 = new SubPerson();
p1.setSlow(true);
p1.setName("测试");
p1.setSubName("sub测试");

Map<String, Object> map = MapUtil.newHashMap();

BeanUtil.copyProperties(p1, map);

5.6.6+加入 复制集合中的Bean属性 List的转化可使用 copyToList

List<Student> studentList = new ArrayList<>();
Student student = new Student();
student.setName("张三");
student.setAge(123);
student.setNo(3158L);
studentList.add(student);

Student student2 = new Student();
student.setName("李四");
student.setAge(125);
student.setNo(8848L);
studentList.add(student2);
// 复制到 Person 类
List<Person> people = BeanUtil.copyToList(studentList, Person.class);

Alias注解

5.x的Hutool中增加了一个自定义注解:@Alias,通过此注解可以给Bean的字段设置别名。

首先我们给Bean加上注解:

// Lombok注解
@Getter
@Setter
public static class SubPersonWithAlias {
    @Alias("aliasSubName")
    private String subName;
    private Boolean slow;
}
SubPersonWithAlias person = new SubPersonWithAlias();
person.setSubName("sub名字");
person.setSlow(true);

// Bean转换为Map时,自动将subName修改为aliasSubName
Map<String, Object> map = BeanUtil.beanToMap(person);
// 返回"sub名字"
map.get("aliasSubName")

同样Alias注解支持注入Bean时的别名: 

Map<String, Object> map = MapUtil.newHashMap();
map.put("aliasSubName", "sub名字");
map.put("slow", true);

SubPersonWithAlias subPersonWithAlias = BeanUtil.mapToBean(map, SubPersonWithAlias.class, false);
// 返回"sub名字"
subPersonWithAlias.getSubName();

DynaBean

DynaBean是使用反射机制动态操作JavaBean的一个封装类,通过这个类,可以通过字符串传入name方式动态调用get和set方法,也可以动态创建JavaBean对象,亦或者执行JavaBean中的方法。

我们先定义一个JavaBean

// Lombok注解
@Data
public static class User{
    private String name;
    private int age;

    public String testMethod(){
        return "test for " + this.name;
    }
}

创建

DynaBean bean = DynaBean.create(user);
//我们也可以通过反射构造对象
DynaBean bean2 = DynaBean.create(User.class);

操作

我们通过DynaBean来包装并操作这个Bean

User user = new User();
DynaBean bean = DynaBean.create(user);
bean.set("name", "李华");
bean.set("age", 12);

String name = bean.get("name");//输出“李华”

invoke

除了标准的get和set方法,也可以调用invoke方法执行对象中的任意方法:

//执行指定方法
Object invoke = bean2.invoke("testMethod");
Assert.assertEquals("test for 李华", invoke);

说明:DynaBean默认实现了hashCode、equals和toString三个方法,这三个方法也是默认调用原对象的相应方法操作。

表达式解析-BeanPath

很多JavaBean嵌套有很多层对象,这其中还夹杂着Map、Collection等对象,因此获取太深的嵌套对象会让代码变得冗长不堪。因此我们可以考虑使用一种表达式还获取指定深度的对象,于是BeanResolver应运而生。

原理

通过传入一个表达式,按照表达式的规则获取bean下指定的对象。

表达式分为两种:

  • .表达式,可以获取Bean对象中的属性(字段)值或者Map中key对应的值
  • []表达式,可以获取集合等对象中对应index的值

例子:

  • person 获取Bean对象下person字段的值,或者Bean本身如果是Person对象,返回本身。
  • person.name 获取Bean中person字段下name字段的值,或者Bean本身如果是Person对象,返回其name字段的值。
  • persons[3] 获取persons字段下第三个元素的值(假设person是数组或Collection对象)
  • person.friends[5].name 获取person字段下friends列表(或数组)的第5个元素对象的name属性

首先我们创建这个复杂的Bean(实际当中这个复杂的Bean可能是从数据库中获取,或者从JSON转入)

这个复杂Bean的关系是这样的:定义一个Map包含用户信息(UserInfoDict)和一个标志位(flag),用户信息包括一些基本信息和一个考试信息列表(ExamInfoDict)。

//------------------------------------------------- 考试信息列表
ExamInfoDict examInfoDict = new ExamInfoDict();
examInfoDict.setId(1);
examInfoDict.setExamType(0);
examInfoDict.setAnswerIs(1);

ExamInfoDict examInfoDict1 = new ExamInfoDict();
examInfoDict1.setId(2);
examInfoDict1.setExamType(0);
examInfoDict1.setAnswerIs(0);

ExamInfoDict examInfoDict2 = new ExamInfoDict();
examInfoDict2.setId(3);
examInfoDict2.setExamType(1);
examInfoDict2.setAnswerIs(0);

List<ExamInfoDict> examInfoDicts = new ArrayList<ExamInfoDict>();
examInfoDicts.add(examInfoDict);
examInfoDicts.add(examInfoDict1);
examInfoDicts.add(examInfoDict2);

//------------------------------------------------- 用户信息
UserInfoDict userInfoDict = new UserInfoDict();
userInfoDict.setId(1);
userInfoDict.setPhotoPath("yx.mm.com");
userInfoDict.setRealName("张三");
userInfoDict.setExamInfoDict(examInfoDicts);

Map<String, Object> tempMap = new HashMap<String, Object>();
tempMap.put("userInfo", userInfoDict);
tempMap.put("flag", 1);

下面,我们使用BeanPath获取这个Map下此用户第一门考试的ID:

BeanPath resolver = new BeanPath("userInfo.examInfoDict[0].id");
Object result = resolver.get(tempMap);//ID为1

只需两句(甚至一句)即可完成复杂Bean中各层次对象的获取。

说明: 为了简化BeanPath的使用,Hutool在BeanUtil中也加入了快捷入口方法:BeanUtil.getProperty,这个方法的命名更容易理解(毕竟BeanPath不但可以解析Bean,而且可以解析Map和集合)。

集合类

集合包中封装了包括Enumeration、Iterator等的包装,这包括:

  • ArrayIterator 数组Iterator,便于数组利用Iterator方式遍历
  • CopiedIterator 为了解决并发情况下Iterator遍历导致的问题而封装的Iterator
  • EnumerationIterator Enumeration的Iterator表现形式
  • IteratorEnumeration Iterator的Enumeration表现形式

同时提供了IterUtil工具和CollUtil工具类用于简化对Iterator和集合的操作。

集合工具-CollUtil

这个工具主要增加了对数组、集合类的操作。

join 方法

将集合转换为字符串,这个方法还是挺常用,是StrUtil.split的反方法。这个方法的参数支持各种类型对象的集合,最后连接每个对象时候调用其toString()方法。

String[] col= new String[]{"a","b","c","d","e"};
List<String> colList = CollUtil.newArrayList(col);

String str = CollUtil.join(colList, "#"); //str -> a#b#c#d#e

sortPageAll、sortPageAll2方法

这个方法其实是一个真正的组合方法,功能是:将给定的多个集合放到一个列表(List)中,根据给定的Comparator对象排序,然后分页取数据。这个方法非常类似于数据库多表查询后排序分页,这个方法存在的意义也是在此。sortPageAll2功能和sortPageAll的使用方式和结果是 一样的,区别是sortPageAll2使用了BoundedPriorityQueue这个类来存储组合后的列表,不知道哪种性能更好一些,所以就都保留了。

//Integer比较器
Comparator<Integer> comparator = new Comparator<Integer>(){
	@Override
	public int compare(Integer o1, Integer o2) {
		return o1.compareTo(o2);
	}
};

//新建三个列表,CollUtil.newArrayList方法表示新建ArrayList并填充元素
List<Integer> list1 = CollUtil.newArrayList(1, 2, 3);
List<Integer> list2 = CollUtil.newArrayList(4, 5, 6);
List<Integer> list3 = CollUtil.newArrayList(7, 8, 9);

//参数表示把list1,list2,list3合并并按照从小到大排序后,取0~2个(包括第0个,不包括第2个),结果是[1,2]
@SuppressWarnings("unchecked")
List<Integer> result = CollUtil.sortPageAll(0, 2, comparator, list1, list2, list3);
System.out.println(result);     //输出 [1,2]

popPart方法

这个方法传入一个栈对象,然后弹出指定数目的元素对象,弹出是指pop()方法,会从原栈中删掉。

append方法

在给定数组里末尾加一个元素,其实List.add()也是这么实现的,这个方法存在的意义是只有少量的添加元素时使用,因为内部使用了System.arraycopy,每调用一次就要拷贝数组一次。这个方法也是为了在某些只能使用数组的情况下使用,省去了先要转成List,添加元素,再转成Array。

resize方法

重新调整数据的大小,如果调整后的大小比原来小,截断,如果比原来大,则多出的位置空着。(貌似List在扩充的时候会用到类似的方法)

addAll方法

将多个数据合并成一个数组

sub方法

对集合切片,其他类型的集合会转换成List,封装List.subList方法,自动修正越界等问题,完全避免IndexOutOfBoundsException异常。

isEmpty、isNotEmpty方法

判断集合是否为空(包括null和没有元素的集合)。

zip方法

给定两个集合,然后两个集合中的元素一一对应,成为一个Map。此方法还有一个重载方法,可以传字符,然后给定分分隔符,字符串会被split成列表。

Collection<String> keys = CollUtil.newArrayList("a", "b", "c", "d");
Collection<Integer> values = CollUtil.newArrayList(1, 2, 3, 4);

// {a=1,b=2,c=3,d=4}
Map<String, Integer> map = CollUtil.zip(keys, values);

列表工具-ListUtil

过滤列表

List<String> a = ListUtil.toLinkedList("1", "2", "3");
// 结果: [edit1, edit2, edit3]
List<String> filter = ListUtil.filter(a, str -> "edit" + str);

获取满足指定规则所有的元素的位置

List<String> a = ListUtil.toLinkedList("1", "2", "3", "4", "3", "2", "1");
// [1, 5]
int[] indexArray = ListUtil.indexOfAll(a, "2"::equals);

拆分

对集合按照指定长度分段,每一个段为单独的集合,返回这个集合的列表:

List<List<Object>> lists = ListUtil.split(Arrays.asList(1, 2, 3, 4), 1);
List<List<Object>> lists = ListUtil.split(null, 3);

也可以平均拆分,即平均分成N份,每份的数量差不超过1: 

// [[1, 2, 3, 4]]
List<List<Object>> lists = ListUtil.splitAvg(Arrays.asList(1, 2, 3, 4), 1);

// [[1, 2], [3], [4]]
lists = ListUtil.splitAvg(Arrays.asList(1, 2, 3, 4), 3);

编辑元素

我们可以针对集合中所有元素按照给定的lambda定义规则修改元素:

List<String> a = ListUtil.toLinkedList("1", "2", "3");
final List<String> filter = (List<String>) CollUtil.edit(a, str -> "edit" + str);

// edit1
filter.get(0);

查找位置

List<String> a = ListUtil.toLinkedList("1", "2", "3", "4", "3", "2", "1");

// 查找所有2的位置
// [1,5]
final int[] indexArray = ListUtil.indexOfAll(a, "2"::equals);

列表截取

final List<Integer> of = ListUtil.of(1, 2, 3, 4);

// [3, 4]
final List<Integer> sub = ListUtil.sub(of, 2, 4);

// 对子列表操作不影响原列表
sub.remove(0);

排序

如我们想按照bean对象的order字段值排序:

@Data
@AllArgsConstructor
class TestBean{
	private int order;
	private String name;
}

final List<TestBean> beanList = ListUtil.toList(
		new TestBean(2, "test2"),
		new TestBean(1, "test1"),
		new TestBean(5, "test5"),
		new TestBean(4, "test4"),
		new TestBean(3, "test3")
		);

final List<TestBean> order = ListUtil.sortByProperty(beanList, "order");

元素交换

List<Integer> list = Arrays.asList(7, 2, 8, 9);

// 将元素8和第一个位置交换
ListUtil.swapTo(list, 8, 1);

分页

List<String> list = new ArrayList<>();
list.add("a");
list.add("b");
list.add("c");
List<String> page = ListUtil.page(1, 2, list);

分组

List<String> list = new ArrayList<>();
list.add("a");
list.add("b");
list.add("c");
List<List<String>> partition = ListUtil.partition(list, 2);

Iterator工具-IterUtil

  • isEmpty 是否为null或者无元素
  • isNotEmpty 是否为非null或者至少一个元素
  • hasNull 是否有null元素
  • isAllNull 是否全部为null元素
  • countMap 根据集合返回一个元素计数的Map,所谓元素计数就是假如这个集合中某个元素出现了n次,那将这个元素做为key,n做为value
  • join 使用分隔符将集合转换为字符串
  • toMap toMap Entry列表转Map,或者key和value单独列表转Map
  • asIterator Enumeration转Iterator
  • asIterable Iterator转Iterable
  • getFirst 获取列表的第一个元素
  • getElementType 获取元素类型

有界优先队列-BoundedPriorityQueue

举个例子。我有一个用户表,这个表根据用户名被Hash到不同的数据库实例上,我要找出这些用户中最热门的5个,怎么做?我是这么做的:

  1. 在每个数据库实例上找出最热门的5个
  2. 将每个数据库实例上的这5条数据按照热门程度排序,最后取出前5条

这个过程看似简单,但是你应用服务器上的代码要写不少。首先需要Query N个列表,加入到一个新列表中,排序,再取前5。这个过程不但代码繁琐,而且牵涉到多个列表,非常浪费空间。

于是,BoundedPriorityQueue应运而生。

public class BoundedPriorityQueueDemo {
	
	public static void main(String[] args) {
		//初始化队列,设置队列的容量为5(只能容纳5个元素),元素类型为integer使用默认比较器,在队列内部将按照从小到大排序
		BoundedPriorityQueue<Integer> queue = new BoundedPriorityQueue<Integer>(5);
		
		//初始化队列,使用自定义的比较器
		queue = new BoundedPriorityQueue<>(5, new Comparator<Integer>(){

			@Override
			public int compare(Integer o1, Integer o2) {
				return o1.compareTo(o2);
			}
		});
		
		//定义了6个元素,当元素加入到队列中,会按照从小到大排序,当加入第6个元素的时候,队列末尾(最大的元素)将会被抛弃
		int[] array = new int[]{5,7,9,2,3,8};
		for (int i : array) {
			queue.offer(i);
		}
		
		//队列可以转换为List哦~~
		ArrayList<Integer> list = queue.toList();

		System.out.println(queue);
	}
}

线程安全的HashSet-ConcurrentHashSet

JDK提供了线程安全的HashMap:ConcurrentHashMap,但是没有提供对应的ConcurrentHashSet,Hutool借助ConcurrentHashMap封装了线程安全的ConcurrentHashSet。

与普通的HashSet使用一致:

Set<String> set = new ConcurrentHashSet<>();
set.add("a");
set.add("b");

集合串行流工具-CollStreamUtil

Java8中的新特性之一就是Stream,Hutool针对常用操作做了一些封装。

集合转Map

@Data
@AllArgsConstructor
@ToString
public static class Student {
    private long termId;//学期id
    private long classId;//班级id
    private long studentId;//班级id
    private String name;//学生名称
}

我们可以建立一个学生id和学生对象之间的map:

List<Student> list = new ArrayList<>();
list.add(new Student(1, 1, 1, "张三"));
list.add(new Student(1, 1, 2, "李四"));
list.add(new Student(1, 1, 3, "王五"));

Map<Long, Student> map = CollStreamUtil.toIdentityMap(list, Student::getStudentId);

// 张三
map.get(1L).getName();

我们也可以自定义Map的key和value放的内容,如我们可以将学生信息的id和姓名生成map: 

Map<Long, String> map = map = CollStreamUtil.toMap(list, Student::getStudentId, Student::getName);

// 张三
map.get(1L);

分组

我们将学生按照班级分组:

List<Student> list = new ArrayList<>();
list.add(new Student(1, 1, 1, "张三"));
list.add(new Student(1, 2, 2, "李四"));
list.add(new Student(2, 1, 1, "擎天柱"));
list.add(new Student(2, 2, 2, "威震天"));
list.add(new Student(2, 3, 2, "霸天虎"));

Map<Long, List<Student>> map = CollStreamUtil.groupByKey(list, Student::getClassId);

转换提取

我们可以将学生信息列表转换提取为姓名的列表:

List<String> list = CollStreamUtil.toList(null, Student::getName);

合并

合并两个相同key类型的map,可自定义合并的lambda,将key value1 value2合并成最终的类型,注意value可能为空的情况。

Map<Long, Student> map1 = new HashMap<>();
map1.put(1L, new Student(1, 1, 1, "张三"));

Map<Long, Student> map2 = new HashMap<>();
map2.put(1L, new Student(2, 1, 1, "李四"));

定义merge规则:

private String merge(Student student1, Student student2) {
    if (student1 == null && student2 == null) {
        return null;
    } else if (student1 == null) {
        return student2.getName();
    } else if (student2 == null) {
        return student1.getName();
    } else {
        return student1.getName() + student2.getName();
    }
}
Map<Long, String> map = CollStreamUtil.merge(map1, map2, this::merge);

行遍历器-LineIter

将Reader包装为一个按照行读取的Iterator。

final LineIter lineIter = new LineIter(ResourceUtil.getUtf8Reader("test_lines.csv"));

for (String line : lineIter) {
	Console.log(line);
}

Map

包含以下特殊的map:

  • CaseInsensitiveMap 忽略大小写的Map,对KEY忽略大小写,get("Value")和get("value")获得的值相同,put进入的值也会被覆盖
  • CaseInsensitiveLinkedMap 忽略大小写的LinkedHashMap,对KEY忽略大小写,get("Value")和get("value")获得的值相同,put进入的值也会被覆盖
  • MapBuilder Map创建器,可以链式创建Map
  • MapProxy Map代理类,通过代理包装Map,提供一系列的getXXX方法

MapUtil 提供对Map常用操作的封装。

Map工具-MapUtil

MapUtil是针对Map的一一列工具方法的封装,包括getXXX的快捷值转换方法。

  • isEmpty、isNotEmpty 判断Map为空和非空方法,空的定义为null或没有值
  • newHashMap 快速创建多种类型的HashMap实例
  • createMap 创建自定义的Map类型的Map
  • of 此方法将一个或多个键值对加入到一个新建的Map中,下面是例子:
Map<Object, Object> colorMap = MapUtil.of(new String[][] {
     {"RED", "#FF0000"},
     {"GREEN", "#00FF00"},
     {"BLUE", "#0000FF"}
});

双向查找Map-BiMap

我们知道在Guava中提供了一种特殊的Map结构,叫做BiMap,它实现了一种双向查找的功能,即根据key查找value和根据value查找key,Hutool也同样提供此对象。

BiMap要求key和value都不能重复(非强制要求),如果key重复了,后加入的键值对会覆盖之前的键值对,如果value重复了,则会按照不确定的顺序覆盖key,这完全取决于map实现。比如HashMap无序(按照hash顺序),则谁覆盖谁和hash算法有关;如果是LinkedHashMap,则有序,是后加入的覆盖先加入的。

BiMap<String, Integer> biMap = new BiMap<>(new HashMap<>());
biMap.put("aaa", 111);
biMap.put("bbb", 222);

// 111
biMap.get("aaa");
// 222
biMap.get("bbb");

// aaa
biMap.getKey(111);
// bbb
biMap.getKey(222);

Map流式构建器-MapBuilder

MapBuilder提供了一种流式的Map创建方法。

Map<String, Object> srcMap = MapBuilder
	.create(new HashMap<String, Object>())
	.put("name", "AAA")
	.put("age", 45).map();

Codec编码

Base62编码解码-Base62

Base62编码是由10个数字、26个大写英文字母和26个小写英文字母组成,多用于安全领域和短URL生成。

String a = "伦家是一个非常长的字符串66";

// 17vKU8W4JMG8dQF8lk9VNnkdMOeWn4rJMva6F0XsLrrT53iKBnqo
String encode = Base62.encode(a);

// 还原为a
String decodeStr = Base62.decodeStr(encode);

Base64编码解码-Base64

Base64编码是用64(2的6次方)个ASCII字符来表示256(2的8次方)个ASCII字符,也就是三位二进制数组经过编码后变为四位的ASCII字符显示,长度比原来增加1/3。

String a = "伦家是一个非常长的字符串";
//5Lym5a625piv5LiA5Liq6Z2e5bi46ZW/55qE5a2X56ym5Liy
String encode = Base64.encode(a);

// 还原为a
String decodeStr = Base64.decodeStr(encode);

Base32编码解码-Base32

Base32就是用32(2的5次方)个特定ASCII码来表示256个ASCII码。所以,5个ASCII字符经过base32编码后会变为8个字符(公约数为40),长度增加3/5.不足8n用“=”补足。

String a = "伦家是一个非常长的字符串";

String encode = Base32.encode(a);
Assert.assertEquals("4S6KNZNOW3TJRL7EXCAOJOFK5GOZ5ZNYXDUZLP7HTKCOLLMX46WKNZFYWI", encode);
		
String decodeStr = Base32.decodeStr(encode);
Assert.assertEquals(a, decodeStr);

莫尔斯电码-Morse

摩尔斯电码也被称作摩斯密码,是一种时通时断的信号代码,通过不同的排列顺序来表达不同的英文字母、数字和标点符号。

摩尔斯电码是由点dot(.)划dash(-)这两种符号所组成的。

编码

final Morse morseCoder = new Morse();

String text = "Hello World!";

// ...././.-../.-../---/-...../.--/---/.-./.-../-../-.-.--/
morseCoder.encode(text);

解码

String text = "你好,世界!";

// -..----.--...../-.--..-.-----.-/--------....--../-..---....-.--./---.-.-.-..--../--------.......-/
String morse = morseCoder.encode(text);

morseCoder.decode(morse);

BCD码-BCD

BCD码(Binary-Coded Decimal)亦称二进码十进数或二-十进制代码。

BCD码这种编码形式利用了四个位元来储存一个十进制的数码,使二进制和十进制之间的转换得以快捷的进行。

String strForTest = "123456ABCDEF";

// 转BCD
byte[] bcd = BCD.strToBcd(strForTest);

// 解码BCD
String str = BCD.bcdToStr(bcd);

回转N位密码-Rot

RotN(rotate by N places),回转N位密码,是一种简易的替换式密码,也是过去在古罗马开发的凯撒加密的一种变体。

以Rot-13为例:

String str = "1f2e9df6131b480b9fdddc633cf24996";

// 4s5r2qs9464o713o2sqqqp966ps57229
String encode13 = Rot.encode13(str);

// 解码
String decode13 = Rot.decode13(encode13);

Punycode实现-PunyCode

Punycode是一个根据RFC 3492标准而制定的编码系统,主要用于把域名从地方语言所采用的Unicode编码转换成为可用于DNS系统的编码。

String text = "Hutool编码器";

// Hutool-ux9js33tgln
String strPunyCode = PunyCode.encode(text);

// Hutool编码器
String decode = PunyCode.decode("Hutool-ux9js33tgln");

// Hutool编码器
decode = PunyCode.decode("xn--Hutool-ux9js33tgln");

文本操作 

CSV文件处理工具-CsvUtil

逗号分隔值(Comma-Separated Values,CSV,有时也称为字符分隔值,因为分隔字符也可以不是逗号),其文件以纯文本形式存储表格数据(数字和文本)。

CsvUtil是CSV工具类,主要封装了两个方法:

  • getReader 用于对CSV文件读取
  • getWriter 用于生成CSV文件

这两个方法分别获取CsvReader对象和CsvWriter,从而独立完成CSV文件的读写。

读取CSV文件

读取为CsvRow

CsvReader reader = CsvUtil.getReader();
//从文件中读取CSV数据
CsvData data = reader.read(FileUtil.file("test.csv"));
List<CsvRow> rows = data.getRows();
//遍历行
for (CsvRow csvRow : rows) {
	//getRawList返回一个List列表,列表的每一项为CSV中的一个单元格(既逗号分隔部分)
	Console.log(csvRow.getRawList());
}

CsvRow对象还记录了一些其他信息,包括原始行号等。

读取为Bean列表

首先测试的CSV:test_bean.csv

姓名,gender,focus,age
张三,男,无,33
李四,男,好对象,23
王妹妹,女,特别关注,22

定义bean

// lombok注解
@Data
private static class TestBean{
    // 如果csv中标题与字段不对应,可以使用alias注解设置别名
    @Alias("姓名")
    private String name;
    private String gender;
    private String focus;
    private Integer age;
}

读取

final CsvReader reader = CsvUtil.getReader();
//假设csv文件在classpath目录下
final List<TestBean> result = reader.read(
    ResourceUtil.getUtf8Reader("test_bean.csv"), TestBean.class);

输出

CsvReaderTest.TestBean(name=张三, gender=男, focus=无, age=33)
CsvReaderTest.TestBean(name=李四, gender=男, focus=好对象, age=23)
CsvReaderTest.TestBean(name=王妹妹, gender=女, focus=特别关注, age=22)

生成CSV文件

//指定路径和编码
CsvWriter writer = CsvUtil.getWriter("e:/testWrite.csv", CharsetUtil.CHARSET_UTF_8);
//按行写出
writer.write(
	new String[] {"a1", "b1", "c1"}, 
	new String[] {"a2", "b2", "c2"}, 
	new String[] {"a3", "b3", "c3"}
);

Unicode编码转换工具-UnicodeUtil

此工具主要针对类似于\\u4e2d\\u6587这类Unicode字符做一些特殊转换。

字符串转Unicode符

//第二个参数true表示跳过ASCII字符(只跳过可见字符)
String s = UnicodeUtil.toUnicode("aaa123中文", true);
//结果aaa123\\u4e2d\\u6587

Unicode转字符串

String str = "aaa\\U4e2d\\u6587\\u111\\urtyu\\u0026";
String res = UnicodeUtil.toString(str);
//结果aaa中文\\u111\\urtyu&

由于\\u111为非Unicode字符串,因此原样输出。

字符串切割-StrSplitter

在Java的String对象中提供了split方法用于通过某种字符串分隔符来把一个字符串分割为数组。但是有的时候我们对这种操作有不同的要求,默认方法无法满足,这包括:

  • 分割限制分割数
  • 分割后每个字符串是否需要去掉两端空格
  • 是否忽略空白片
  • 根据固定长度分割
  • 通过正则分隔

因此,StrSplitter应运而生。StrSplitter中全部为静态方法,方便快捷调用。

split 切分字符串,众多可选参数,返回结果为List splitToArray 切分字符串,返回结果为数组 splitsplitByRegex 根据正则切分字符串 splitByLength 根据固定长度切分字符串

String str1 = "a, ,efedsfs,   ddf";
//参数:被切分字符串,分隔符逗号,0表示无限制分片数,去除两边空格,忽略空白项
List<String> split = StrSplitter.split(str1, ',', 0, true, true);

splitPath 切分字符串,分隔符为"/" splitPathToArray 切分字符串,分隔符为"/",返回数组。

注解

注解工具-AnnotationUtil

封装了注解获取等方法的工具类。

注解获取相关方法

  • getAnnotations 获取指定类、方法、字段、构造等上的注解列表
  • getAnnotation 获取指定类型注解
  • getAnnotationValue 获取指定注解属性的值

 我们定义一个注解:

// Retention注解决定MyAnnotation注解的生命周期
@Retention(RetentionPolicy.RUNTIME)
// Target注解决定MyAnnotation注解可以加在哪些成分上,如加在类身上,或者属性身上,或者方法身上等成分
@Target({ ElementType.METHOD, ElementType.TYPE })
public @interface AnnotationForTest {
	
	/**
	 * 注解的默认属性值
	 * 
	 * @return 属性值
	 */
	String value();
}

给需要的类加上注解:

@AnnotationForTest("测试")
public static class ClassWithAnnotation{

}

获取注解中的值:

// value为"测试"
Object value = AnnotationUtil.getAnnotationValue(ClassWithAnnotation.class, AnnotationForTest.class);

注解属性获取相关方法 

  • getRetentionPolicy 获取注解类的保留时间,可选值 SOURCE(源码时),CLASS(编译时),RUNTIME(运行时),默认为 CLASS
  • getTargetType 获取注解类可以用来修饰哪些程序元素,如 TYPE, METHOD, CONSTRUCTOR, FIELD, PARAMETER 等
  • isDocumented 是否会保存到 Javadoc 文档中
  • isInherited 是否可以被继承,默认为 false

比较器

各种比较器(Comparator)实现和封装

  • ReverseComparator 反转比较器,排序时提供反序
  • VersionComparator 版本比较器,支持如:1.3.20.8,6.82.20160101,8.5a/8.5c等版本形式
  • PropertyComparator Bean属性比较器,通过Bean的某个属性来对Bean对象进行排序
  • IndexedComparator 按照数组的顺序正序排列,数组的元素位置决定了对象的排序先后
  • ComparatorChain 比较器链。此链包装了多个比较器,最终比较结果按照比较器顺序综合多个比较器结果。
  • PinyinComparator 按照GBK拼音顺序对给定的汉字字符串排序。

比较工具-CompareUtil

在JDK提供的比较器中,对于null的比较没有考虑,Hutool封装了相关比较,可选null是按照最大值还是最小值对待。

// 当isNullGreater为true时,null始终最大,此处返回的compare > 0
int compare = CompareUtil.compare(null, "a", true);

// 当isNullGreater为false时,null始终最小,此处返回的compare < 0
int compare = CompareUtil.compare(null, "a", false);

异常

异常工具-ExceptionUtil

针对异常封装,例如包装为RuntimeException。

包装异常

假设系统抛出一个非Runtime异常,我们需要包装为Runtime异常,那么:

IORuntimeException e = ExceptionUtil.wrap(new IOException(), IORuntimeException.class);

获取入口方法

StackTraceElement ele = ExceptionUtil.getRootStackElement();
// main
ele.getMethodName();

异常转换

如果我们想把异常转换指定异常为来自或者包含指定异常,那么:

IOException ioException = new IOException();
IllegalArgumentException argumentException = new IllegalArgumentException(ioException);

IOException ioException1 = ExceptionUtil.convertFromOrSuppressedThrowable(argumentException, IOException.class, true);

其他方法

  • getMessage 获得完整消息,包括异常名
  • wrapRuntime 使用运行时异常包装编译异常
  • getCausedBy 获取由指定异常类引起的异常
  • isCausedBy 判断是否由指定异常类引起
  • stacktraceToString 堆栈转为完整字符串

其它异常封装

针对Hutool中常见异常封装。

  • DependencyException 依赖异常
  • StatefulException 带有状态码的异常
  • UtilException 工具类异常
  • NotInitedException 未初始化异常
  • ValidateException 验证异常

线程和并发

线程工具-ThreadUtil

并发在Java中算是一个比较难理解和容易出问题的部分,而并发的核心在线程。好在从JDK1.5开始Java提供了concurrent包可以很好的帮我们处理大部分并发、异步等问题。

不过,ExecutorService和Executors等众多概念依旧让我们使用这个包变得比较麻烦,如何才能隐藏这些概念?又如何用一个方法解决问题?ThreadUtil便为此而生。

原理:Hutool使用GlobalThreadPool持有一个全局的线程池,默认所有异步方法在这个线程池中执行。

ThreadUtil.execute

直接在公共线程池中执行线程

ThreadUtil.newExecutor

获得一个新的线程池

ThreadUtil.execAsync

执行异步方法

ThreadUtil.newCompletionService

创建CompletionService,调用其submit方法可以异步执行多个任务,最后调用take方法按照完成的顺序获得其结果。若未完成,则会阻塞。

ThreadUtil.newCountDownLatch

新建一个CountDownLatch,一个同步辅助类,在完成一组正在其他线程中执行的操作之前,它允许一个或多个线程一直等待。

ThreadUtil.sleep

挂起当前线程,是Thread.sleep的封装,通过返回boolean值表示是否被打断,而不是抛出异常。

ThreadUtil.safeSleep方法是一个保证挂起足够时间的方法,当给定一个挂起时间,使用此方法可以保证挂起的时间大于或等于给定时间,解决Thread.sleep挂起时间不足问题,此方法在Hutool-cron的定时器中使用保证定时任务执行的准确性。

ThreadUtil.getStackTrace

  • getStackTrace 获得堆栈列表
  • getStackTraceElement 获得堆栈项

其它

  • createThreadLocal 创建本地线程对象
  • interupt 结束线程,调用此方法后,线程将抛出InterruptedException异常
  • waitForDie 等待线程结束. 调用 Thread.join() 并忽略 InterruptedException
  • getThreads 获取JVM中与当前线程同组的所有线程
  • getMainThread 获取进程的主线程

异步工具类-AsyncUtil

在JDK8中,提供了CompletableFuture进行异步执行

使用场景如异步调用微服务、异步查询数据库、异步运算大量数据等。

AsyncUtil.waitAll

等待所有任务执行完毕

AsyncUtil.waitAny

等待任意一个任务执行完毕

AsyncUtil.get

获取异步任务结果

自定义线程池-ExecutorBuilder

在JDK中,提供了Executors用于创建自定义的线程池对象ExecutorService,但是考虑到线程池中存在众多概念,这些概念通过不同的搭配实现灵活的线程管理策略,单独使用Executors无法满足需求,构建了ExecutorBuilder。

  • corePoolSize 初始池大小
  • maxPoolSize 最大池大小(允许同时执行的最大线程数)
  • workQueue 队列,用于存在未执行的线程
  • handler 当线程阻塞(block)时的异常处理器,所谓线程阻塞即线程池和等待队列已满,无法处理线程时采取的策略

线程池对待线程的策略

  1. 如果池中任务数 < corePoolSize -> 放入立即执行
  2. 如果池中任务数 > corePoolSize -> 放入队列等待
  3. 队列满 -> 新建线程立即执行
  4. 执行中的线程 > maxPoolSize -> 触发handler(RejectedExecutionHandler)异常

workQueue线程池策略

  • SynchronousQueue 它将任务直接提交给线程而不保持它们。当运行线程小于maxPoolSize时会创建新线程,否则触发异常策略
  • LinkedBlockingQueue 默认无界队列,当运行线程大于corePoolSize时始终放入此队列,此时maxPoolSize无效。当构造LinkedBlockingQueue对象时传入参数,变为有界队列,队列满时,运行线程小于maxPoolSize时会创建新线程,否则触发异常策略
  • ArrayBlockingQueue 有界队列,相对无界队列有利于控制队列大小,队列满时,运行线程小于maxPoolSize时会创建新线程,否则触发异常策略

默认线程池

  • 初始线程数为corePoolSize指定的大小
  • 没有最大线程数限制
  • 默认使用LinkedBlockingQueue,默认队列大小为1024(最大等待数1024)
  • 当运行线程大于corePoolSize放入队列,队列满后抛出异常
ExecutorService executor = ExecutorBuilder.create()..build();

单线程线程池

  • 初始线程数为 1
  • 最大线程数为 1
  • 默认使用LinkedBlockingQueue,默认队列大小为1024
  • 同时只允许一个线程工作,剩余放入队列等待,等待数超过1024报错
ExecutorService executor = ExecutorBuilder.create()//
	.setCorePoolSize(1)//
	.setMaxPoolSize(1)//
	.setKeepAliveTime(0)//
	.build();

更多选项的线程池

  • 初始5个线程
  • 最大10个线程
  • 有界等待队列,最大等待数是100
ExecutorService executor = ExecutorBuilder.create()
	.setCorePoolSize(5)
	.setMaxPoolSize(10)
	.setWorkQueue(new LinkedBlockingQueue<>(100))
	.build();

特殊策略的线程池

  • 初始5个线程
  • 最大10个线程
  • 它将任务直接提交给线程而不保持它们。当运行线程小于maxPoolSize时会创建新线程,否则触发异常策略
ExecutorService executor = ExecutorBuilder.create()
	.setCorePoolSize(5)
	.setMaxPoolSize(10)
	.useSynchronousQueue()
	.build();

高并发测试-ConcurrencyTester

很多时候,我们需要简单模拟N个线程调用某个业务测试其并发状况,于是Hutool提供了一个简单的并发测试类——ConcurrencyTester。

ConcurrencyTester tester = ThreadUtil.concurrencyTest(100, () -> {
	// 测试的逻辑内容
	long delay = RandomUtil.randomLong(100, 1000);
	ThreadUtil.sleep(delay);
	Console.log("{} test finished, delay: {}", Thread.currentThread().getName(), delay);
});

// 获取总的执行时间,单位毫秒
Console.log(tester.getInterval());

图片

图片工具-ImgUtil

针对awt中图片处理进行封装,这些封装包括:缩放、裁剪、转为黑白、加水印等操作。

scale 缩放图片

提供两种重载方法:其中一个是按照长宽缩放,另一种是按照比例缩放。

ImgUtil.scale(
    FileUtil.file("d:/face.jpg"), 
    FileUtil.file("d:/face_result.jpg"), 
    0.5f//缩放比例
);

cut 剪裁图片

ImgUtil.cut(
    FileUtil.file("d:/face.jpg"), 
    FileUtil.file("d:/face_result.jpg"), 
    new Rectangle(200, 200, 100, 100)//裁剪的矩形区域
);

slice 按照行列剪裁切片(将图片分为20行和20列)

ImgUtil.slice(FileUtil.file("e:/test2.png"), FileUtil.file("e:/dest/"), 10, 10);

convert 图片类型转换

支持GIF->JPG、GIF->PNG、PNG->JPG、PNG->GIF(X)、BMP->PNG等

ImgUtil.convert(FileUtil.file("e:/test2.png"), FileUtil.file("e:/test2Convert.jpg"));

gray 彩色转为黑白

ImgUtil.gray(FileUtil.file("d:/logo.png"), FileUtil.file("d:/result.png"));

pressText 添加文字水印

ImgUtil.pressText(//
    FileUtil.file("e:/pic/face.jpg"), //
    FileUtil.file("e:/pic/test2_result.png"), //
    "版权所有", Color.WHITE, //文字
    new Font("黑体", Font.BOLD, 100), //字体
    0, //x坐标修正值。 默认在中间,偏移量相对于中间偏移
    0, //y坐标修正值。 默认在中间,偏移量相对于中间偏移
    0.8f//透明度:alpha 必须是范围 [0.0, 1.0] 之内(包含边界值)的一个浮点数字
);

pressImage 添加图片水印

ImgUtil.pressImage(
    FileUtil.file("d:/picTest/1.jpg"), 
    FileUtil.file("d:/picTest/dest.jpg"), 
    ImgUtil.read(FileUtil.file("d:/picTest/1432613.jpg")), //水印图片
    0, //x坐标修正值。 默认在中间,偏移量相对于中间偏移
    0, //y坐标修正值。 默认在中间,偏移量相对于中间偏移
    0.1f
);

rotate 旋转图片

// 旋转180度
BufferedImage image = ImgUtil.rotate(ImageIO.read(FileUtil.file("e:/pic/366466.jpg")), 180);
ImgUtil.write(image, FileUtil.file("e:/pic/result.png"));

flip 水平翻转图片

ImgUtil.flip(FileUtil.file("d:/logo.png"), FileUtil.file("d:/result.png"));

图片编辑器-Img

针对awt中图片处理进行封装,这些封装包括:缩放、裁剪、转为黑白、加水印等操作。

图像切割

// 将face.jpg切割为原型保存为face_radis.png
Img.from(FileUtil.file("e:/pic/face.jpg"))
    .cut(0, 0, 200)//
    .write(FileUtil.file("e:/pic/face_radis.png"));

图片压缩

Img.from(FileUtil.file("e:/pic/1111.png"))
    .setQuality(0.8)//压缩比率
    .write(FileUtil.file("e:/pic/1111_target.jpg"));

网络

网络工具-NetUtil

在日常开发中,网络连接这块儿必不可少。日常用到的一些功能,隐藏掉部分IP地址、绝对相对路径的转换等等。

NetUtil 工具中主要的方法包括:

  • longToIpv4 根据long值获取ip v4地址
  • ipv4ToLong 根据ip地址计算出long型的数据
  • isUsableLocalPort 检测本地端口可用性
  • isValidPort 是否为有效的端口
  • isInnerIP 判定是否为内网IP
  • localIpv4s 获得本机的IP地址列表
  • toAbsoluteUrl 相对URL转换为绝对URL
  • hideIpPart 隐藏掉IP地址的最后一部分为 * 代替
  • buildInetSocketAddress 构建InetSocketAddress
  • getIpByHost 通过域名得到IP
  • isInner 指定IP的long是否在指定范围内
String ip= "127.0.0.1";
long iplong = 2130706433L;

//根据long值获取ip v4地址
String ip= NetUtil.longToIpv4(iplong);


//根据ip地址计算出long型的数据
long ip= NetUtil.ipv4ToLong(ip);

//检测本地端口可用性
boolean result= NetUtil.isUsableLocalPort(6379);

//是否为有效的端口
boolean result= NetUtil.isValidPort(6379);

//隐藏掉IP地址
 String result =NetUtil.hideIpPart(ip);

URL生成器-UrlBuilder

在JDK中,我们可以借助URL对象完成URL的格式化,但是无法完成一些特殊URL的解析和处理,例如编码过的URL、不标准的路径和参数。在旧版本的hutool中,URL的规范完全靠字符串的替换来完成,不但效率低,而且处理过程及其复杂。于是在5.3.1之后,加入了UrlBuilder类,拆分URL的各个部分,分别处理和格式化,完成URL的规范。

URL的结构如下:

  • [scheme:]scheme-specific-part[#fragment]

  • [scheme:][//authority][path][?query][#fragment]

  • [scheme:][//host:port][path][?query][#fragment]

按照这个格式,UrlBuilder将URL分成scheme、host、port、path、query、fragment部分,其中path和query较为复杂,又使用UrlPath和UrlQuery分别封装。

使用

相比URL对象,UrlBuilder更加人性化,例如:

URL url = new URL("www.hutool.cn")

此时会报java.net.MalformedURLException: no protocol的错误,而使用UrlBuilder则会有默认协议:

// 输出 http://www.hutool.cn/
String buildUrl = UrlBuilder.create().setHost("www.hutool.cn").build();

完整构建

// https://www.hutool.cn/aaa/bbb?ie=UTF-8&wd=test
String buildUrl = UrlBuilder.create()
	.setScheme("https")
	.setHost("www.hutool.cn")
	.addPath("/aaa").addPath("bbb")
	.addQuery("ie", "UTF-8")
	.addQuery("wd", "test")
	.build();

中文编码

当参数中有中文时,自动编码中文,默认UTF-8编码,也可以调用setCharset方法自定义编码。

// https://www.hutool.cn/s?ie=UTF-8&ie=GBK&wd=%E6%B5%8B%E8%AF%95
String buildUrl = UrlBuilder.create()
	.setScheme("https")
	.setHost("www.hutool.cn")
	.addPath("/s")
	.addQuery("ie", "UTF-8")
	.addQuery("ie", "GBK")
	.addQuery("wd", "测试")
	.build();

解析

当有一个URL字符串时,可以使用of方法解析:

UrlBuilder builder = UrlBuilder.ofHttp("www.hutool.cn/aaa/bbb/?a=张三&b=%e6%9d%8e%e5%9b%9b#frag1", CharsetUtil.CHARSET_UTF_8);

// 输出张三
Console.log(builder.getQuery().get("a"));
// 输出李四
Console.log(builder.getQuery().get("b"));

我们发现这个例子中,原URL中的参数a是没有编码的,b是编码过的,当用户提供此类混合URL时,Hutool可以很好的识别并全部decode,当然,调用build()之后,会全部再encode。

特殊URL解析

有时候URL中会存在&amp;这种分隔符,谷歌浏览器会将此字符串转换为&使用,Hutool中也同样如此:

String urlStr = "https://mp.weixin.qq.com/s?__biz=MzI5NjkyNTIxMg==&amp;mid=100000465&amp;idx=1";
UrlBuilder builder = UrlBuilder.ofHttp(urlStr, CharsetUtil.CHARSET_UTF_8);

// https://mp.weixin.qq.com/s?__biz=MzI5NjkyNTIxMg==&mid=100000465&idx=1
Console.log(builder.build());

UrlBuilder主要应用于http模块,在构建HttpRequest时,用户传入的URL五花八门,为了做大最好的适应性,减少用户对URL的处理,使用UrlBuilder完成URL的规范化。

源码编译

源码编译工具-CompilerUtil

JDK提供了JavaCompiler用于动态编译java源码文件,然后通过类加载器加载,这种动态编译可以让Java有动态脚本的特性,Hutool针对此封装了对应工具。

首先我们将编译需要依赖的class文件和jar文件打成一个包:

// 依赖A,编译B和C
final File libFile = ZipUtil.zip(FileUtil.file("lib.jar"),
                                 new String[]{"a/A.class", "a/A$1.class", "a/A$InnerClass.class"},
                                 new InputStream[]{
                                     FileUtil.getInputStream("test-compile/a/A.class"),
                                     FileUtil.getInputStream("test-compile/a/A$1.class"),
                                     FileUtil.getInputStream("test-compile/a/A$InnerClass.class")
                                     });

开始编译

final ClassLoader classLoader = CompilerUtil.getCompiler(null)
	// 被编译的源码文件
	.addSource(FileUtil.file("test-compile/b/B.java"))
	// 被编译的源码字符串
	.addSource("c.C", FileUtil.readUtf8String("test-compile/c/C.java"))
	// 编译依赖的库
	.addLibrary(libFile)
	.compile();

 加载编译好的类

final Class<?> clazz = classLoader.loadClass("c.C");
// 实例化对象c
Object obj = ReflectUtil.newInstance(clazz);

 

posted @ 2022-01-05 22:22  残城碎梦  阅读(1998)  评论(0编辑  收藏  举报