https://www.hoshino.asia/archives/hutool

克隆

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

我们解决什么问题

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

泛型克隆接口

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

/**
 * 猫猫类,使用实现Cloneable方式
 * @author Looly
 *
 */
private static 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()方法:

/**
 * 狗狗类,用于继承CloneSupport类
 * @author Looly
 *
 */
private static class Dog extends CloneSupport<Dog>{
	private String name = "wangwang";
	private int age = 3;
}

当然,使用CloneSupport的前提是你没有继承任何的类,谁让Java不支持多重继承呢(你依旧可以让父类继承这个类,如果可以的话)。如果没办法继承类,那实现cn.hutool.clone.Cloneable也是不错的主意,因此hutool提供了这两种方式,任选其一,在便捷和灵活上都提供了支持。

深克隆

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

ObjectUtil.cloneByStream(obj)

前提是对象必须实现Serializable接口。

ObjectUtil同样提供一些静态方法:clone(obj)cloneIfPossible(obj) 用于简化克隆调用,详细的说明请查看核心类的相关文档。

类型转换

类型转换工具类-Convert

痛点

在Java开发中我们要面对各种各样的类型转换问题,尤其是从命令行获取的用户参数、从HttpRequest获取的Parameter等等,这些参数类型多种多样,我们怎么去转换他们呢?常用的办法是先整成String,然后调用XXX.parseXXX方法,还要承受转换失败的风险,不得不加一层try catch,这个小小的过程混迹在业务代码中会显得非常难看和臃肿。

Convert类

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

Java常见类型转换

  1. 转换为字符串:

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);
  1. 转换为指定类型数组:

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);
  1. 转换为日期对象:

String a = "2017-05-06";
Date value = Convert.toDate(a);
  1. 转换为集合

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

其它类型转换

  1. 标准类型

通过Convert.convert(Class<T>, Object)方法可以将任意类型转换为指定类型,Hutool中预定义了许多类型转换,例如转换为URI、URL、Calendar等等,这些类型的转换都依托于ConverterRegistry类。通过这个类和Converter接口,我们可以自定义一些类型转换。详细的使用请参阅“自定义类型转换”一节。

  1. 泛型类型

通过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实例化后制定泛型类型,即可转换对象为我们想要的目标类型。

半角和全角转换

在很多文本的统一化中这两个方法非常有用,主要对标点符号的全角半角转换。

半角转全角:

String a = "123456789";

//结果为:"123456789"
String sbc = Convert.toSBC(a);

全角转半角:

String a = "123456789";

//结果为"123456789"
String dbc = Convert.toDBC(a);

16进制(Hex)

在很多加密解密,以及中文字符串传输(比如表单提交)的时候,会用到16进制转换,就是Hex转换,为此Hutool中专门封装了HexUtil工具类,考虑到16进制转换也是转换的一部分,因此将其方法也放在Convert类中,便于理解和查找,使用同样非常简单:

转为16进制(Hex)字符串

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

//结果:"e68891e698afe4b880e4b8aae5b08fe5b08fe79a84e58fafe788b1e79a84e5ad97e7aca6e4b8b2"
String hex = Convert.toHex(a, CharsetUtil.CHARSET_UTF_8);

将16进制(Hex)字符串转为普通字符串:

String hex = "e68891e698afe4b880e4b8aae5b08fe5b08fe79a84e58fafe788b1e79a84e5ad97e7aca6e4b8b2";

//结果为:"我是一个小小的可爱的字符串"
String raw = Convert.hexStrToStr(hex, CharsetUtil.CHARSET_UTF_8);

//注意:在4.1.11之后hexStrToStr将改名为hexToStr
String raw = Convert.hexToStr(hex, CharsetUtil.CHARSET_UTF_8);

因为字符串牵涉到编码问题,因此必须传入编码对象,此处使用UTF-8编码。 toHex方法同样支持传入byte[],同样也可以使用hexToBytes方法将16进制转为byte[]

Unicode和字符串转换

与16进制类似,Convert类同样可以在字符串和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);

注意 经过测试,UTF-8编码后用GBK解码,再用GBK编码后用UTF-8解码会存在某些中文转换失败的问题。

时间单位转换

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

long a = 4535345;

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

金额大小写转换

面对财务类需求,Convert.digitToChinese将金钱数转换为大写形式:

double a = 67556.32;

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

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

数字转换

  1. 数字转为英文表达

// ONE HUNDRED AND CENTS TWENTY THREE ONLY
String format = Convert.numberToWord(100.23);
  1. 数字简化

// 1.2k
String format1 = Convert.numberToSimple(1200, false);
  1. 数字转中文

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

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

// 使用金额大写
// 壹万贰仟陆佰伍拾叁
String f1 = Convert.numberToChinese(12653, true);
  1. 数字中文表示转换为数字

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

原始类和包装类转换

有的时候,我们需要将包装类和原始类相互转换(比如Integer.class 和 int.class),这时候我们可以:

//去包装
Class<?> wrapClass = Integer.class;

//结果为:int.class
Class<?> unWraped = Convert.unWrap(wrapClass);

//包装
Class<?> primitiveClass = long.class;

//结果为:Long.class
Class<?> wraped = Convert.wrap(primitiveClass);

自定义类型转换-ConverterRegistry

由来

Hutool中类型转换最早只是一个工具类,叫做“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()方法自定义类型转换。

  1. 自定义转换器

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

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

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)两个重载方法都是使用自定义转换器优先的模式。

ConverterRegistry单例和对象模式

ConverterRegistry提供一个静态方法getInstance()返回全局单例对象,这也是推荐的使用方式,当然如果想在某个限定范围内自定义转换,可以实例化ConverterRegistry对象。

日期时间

概述

介绍

日期时间包是Hutool的核心包之一,提供针对JDK中Date和Calendar对象的封装,封装对象如下:

日期时间工具

  • DateUtil 针对日期时间操作提供一系列静态方法

  • DateTime 提供类似于Joda-Time中日期时间对象的封装,继承自Date类,并提供更加丰富的对象方法。

  • FastDateFormat 提供线程安全的针对Date对象的格式化和日期字符串解析支持。此对象在实际使用中并不需要感知,相关操作已经封装在DateUtilDateTime的相关方法中。

  • DateBetween 计算两个时间间隔的类,除了通过构造新对象使用外,相关操作也已封装在DateUtilDateTime的相关方法中。

  • TimeInterval 一个简单的计时器类,常用于计算某段代码的执行时间,提供包括毫秒、秒、分、时、天、周等各种单位的花费时长计算,对象的静态构造已封装在DateUtil中。

  • DatePattern 提供常用的日期格式化模式,包括String类型和FastDateFormat两种类型。

日期枚举

考虑到Calendar类中表示时间的字段(field)都是使用int表示,在使用中非常不便,因此针对这些int字段,封装了与之对应的Enum枚举类,这些枚举类在DateUtilDateTime相关方法中做为参数使用,可以更大限度的缩小参数限定范围。

这些定义的枚举值可以通过getValue()方法获得其与Calendar类对应的int值,通过of(int)方法从Calendar中int值转为枚举对象。

Calendar对应的这些枚举包括:

  • Month 表示月份,与Calendar中的int值一一对应。

  • Week 表示周,与Calendar中的int值一一对应

月份枚举

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

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

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

时间枚举

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

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

日期时间工具-DateUtil

由来

考虑到Java本身对日期时间的支持有限,并且Date和Calendar对象的并存导致各种方法使用混乱和复杂,故使用此工具类做了封装。这其中的封装主要是日期和字符串之间的转换,以及提供对日期的定位(一个月前等等)。

对于Date对象,为了便捷,使用了一个DateTime类来代替之,继承自Date对象,主要的便利在于,覆盖了toString()方法,返回yyyy-MM-dd HH:mm:ss形式的字符串,方便在输出时的调用(例如日志记录等),提供了众多便捷的方法对日期对象操作,关于DateTime会在相关章节介绍。

方法

转换

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日 HH时mm分ss秒

  • yyyy-MM-dd

  • 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

  • EEE, dd MMM yyyy HH:mm:ss z

  • EEE MMM dd HH:mm:ss zzz yyyy

  • yyyy-MM-dd'T'HH:mm:ss'Z'

  • yyyy-MM-dd'T'HH:mm:ss.SSS'Z'

  • yyyy-MM-dd'T'HH:mm:ssZ

  • yyyy-MM-dd'T'HH:mm:ss.SSSZ

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);

格式化时间差

有时候我们希望看到易读的时间差,比如XX天XX小时XX分XX秒,此时使用DateUtil.formatBetween方法:

//Level.MINUTE表示精确到分
String formatBetween = DateUtil.formatBetween(between, Level.MINUTE);
//输出:31天1小时
Console.log(formatBetween);

星座和属相

// "摩羯座"
String zodiac = DateUtil.getZodiac(Month.JANUARY.getValue(), 19);

// "狗"
String chineseZodiac = DateUtil.getChineseZodiac(1994);

日期范围

// 创建日期范围生成器
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类诞生。DateTime对象充分吸取Joda-Time库的优点,并提供更多的便捷方法,这样我们在开发时不必再单独导入Joda-Time库便可以享受简单快速的日期时间处理过程。

说明

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 dt = DateTime.of(date);
DateTime now = DateTime.now();

使用对象

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();

更多成员方法请参阅官网API文档。

概览 (hutool 5.8.28 API)

对象的可变性

DateTime对象默认是可变对象(调用offset、setField、setTime方法默认变更自身),但是这种可变性有时候会引起很多问题(例如多个地方共用DateTime对象)。我们可以调用setMutable(false)方法使其变为不可变对象。在不可变模式下,offsetsetField方法返回一个新对象,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
String dateStr = dateTime.toString("yyyy/MM/dd");

农历日期-ChineseDate

介绍

农历日期,提供了生肖、天干地支、传统节日等方法。

使用

  1. 构建ChineseDate对象

ChineseDate表示了农历的对象,构建此对象既可以使用公历的日期,也可以使用农历的日期。

//通过农历构建
ChineseDate chineseDate = new ChineseDate(1992,12,14);

//通过公历构建
ChineseDate chineseDate = new ChineseDate(DateUtil.parseDate("1993-01-06"));
  1. 基本使用

//通过公历构建
ChineseDate date = new ChineseDate(DateUtil.parseDate("2020-01-25"));
// 一月
date.getChineseMonth();
// 正月
date.getChineseMonthName();
// 初一
date.getChineseDay();
// 庚子
date.getCyclical();
// 生肖:鼠
date.getChineseZodiac();
// 传统节日(部分支持,逗号分隔):春节
date.getFestivals();
// 庚子鼠年 正月初一
date.toString();
  1. 获取天干地支

5.4.1开始,Hutool支持天干地支的获取:

//通过公历构建
ChineseDate chineseDate = new ChineseDate(DateUtil.parseDate("2020-08-28"));

// 庚子年甲申月癸卯日
String cyclicalYMD = chineseDate.getCyclicalYMD();

LocalDateTime工具-LocalDateTimeUtil

介绍

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

使用

  1. 日期转换

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());
  1. 日期字符串解析

// 解析ISO时间
LocalDateTime localDateTime = LocalDateTimeUtil.parse("2020-01-23T12:23:56");


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

解析同样支持LocalDate

LocalDate localDate = LocalDateTimeUtil.parseDate("2020-01-23");

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

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

// "2020-01-23 12:23:56"
String format = LocalDateTimeUtil.format(localDateTime, DatePattern.NORM_DATETIME_PATTERN);
  1. 日期偏移

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);
  1. 计算时间间隔

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();
  1. 一天的开始和结束

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中是一个较为复杂的过程,我们在面对不同的场景时,要选择不同的InputStreamOutputStream实现来完成这些操作。而如果想读写字符流,还需要ReaderWriter的各种实现类。这些繁杂的实现类,一方面给我们提供了更多的灵活性,另一方面也增加了复杂性。

封装

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有所区别。我查阅过一些资料,使用NIO对文件流的操作有一定的提升,我并没有做具体实验。相关测试请参阅博客:http://www.cnblogs.com/gaopeng527/p/4896783.html

Stream转Reader、Writer

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

  • IoUtil.getWriter:将OutputStream转为OutputStreamWriter用于写入字符流,它是部分writeXXX的基础。

本质上这两个方法只是简单new一个新的Reader或者Writer对象,但是封装为工具方法配合IDE的自动提示可以大大减少查阅次数(例如你对BufferedReader、OutputStreamWriter不熟悉,是不是需要搜索一下相关类?)

读取流中的内容

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

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

  • InputStream

  • Reader

  • FileChannel

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

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

  • readBytes 返回byte数组(读取图片等)

  • readHex 读取16进制字符串

  • readObj 读取序列化对象(反序列化)

  • readLines 按行读取

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

  • String 转换为ByteArrayInputStream

  • File 转换为FileInputStream

写入到流

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

  • IoUtil.writeObjects 用于将可序列化对象序列化后写入到流中。

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

关闭

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

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

  1. 被关闭对象为空

  2. 对象关闭失败(或对象已关闭)

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

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

文件工具类-FileUtil

简介

在IO操作中,文件的操作相对来说是比较复杂的,但也是使用频率最高的部分,我们几乎所有的项目中都躺着一个叫做FileUtil或者FileUtils的工具类,我想Hutool应该将这个工具类纳入其中,用来解决大部分的文件操作问题。

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

  1. 文件操作:包括文件目录的新建、删除、复制、移动、改名等

  2. 文件判断:判断文件或目录是否非空,是否为目录,是否为文件等等。

  3. 绝对路径:针对ClassPath中的文件转换为绝对路径文件。

  4. 文件名:主文件名,扩展名的获取

  5. 读操作:包括类似IoUtil中的getReader、readXXX操作

  6. 写操作:包括getWriter和writeXXX操作

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

  • ls 列出目录和文件

  • touch 创建文件,如果父目录不存在也自动创建

  • mkdir 创建目录,会递归创建每层目录

  • del 删除文件或目录(递归删除,不判断是否为空),这个方法相当于Linux的delete命令

  • copy 拷贝文件或目录

这些方法提供了人性化的操作,例如touch方法,在创建文件的情况下会自动创建上层目录(我想对于使用者来说这也是大部分情况下的需求),同样mkdir也会创建父目录。

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

关于FileUtil中更多工具方法,请参阅API文档。

概览 (hutool 5.8.28 API)

文件监听-WatchMonitor

由来

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

  • 支持多级目录的监听(WatchService只支持一级目录),可自定义监听目录深度

  • 延迟合并触发支持(文件变动时可能触发多次modify,支持在某个时间范围内的多次修改事件合并为一个修改事件)

  • 简洁易懂的API方法,一个方法即可搞定监听,无需理解复杂的监听注册机制。

  • 多观察者实现,可以根据业务实现多个Watcher来响应同一个事件(通过WatcherChain)

WatchMonitor

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

内部应用

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

使用

WatchMonitor提供的事件有:

  • ENTRY_MODIFY 文件或目录的修改事件

  • ENTRY_CREATE 文件或目录的创建事件

  • ENTRY_DELETE 文件或目录的删除事件

  • OVERFLOW 丢失的事件

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

下面我们介绍WatchMonitor的使用:

监听指定事件

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

由来

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

使用

这个工具类使用非常简单,通过调用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表示文件新增内容默认输出到控制台。

/**
 * 命令行打印的行处理器
 * 
 * @author looly
 * @since 4.5.2
 */
public static class ConsoleLineHandler implements LineHandler {
	@Override
	public void handle(String line) {
		Console.log(line);
	}
}

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

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

文件名工具-FileNameUtil

由来

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

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

使用

  1. 获取文件名

File file = FileUtil.file("/opt/test.txt");

// test.txt
String name = FileNameUtil.getName(file);
  1. 获取主文件名和扩展名

File file = FileUtil.file("/opt/test.txt");

// "test"
String name = FileNameUtil.mainName(file);

// "txt"
String name = FileNameUtil.extName(file);

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

资源

概述

由来

资源(Resource)在Hutool中是一个广泛的概念,凡是存储数据的地方都可以归类到资源,那为何要提供一个如此抽象的接口呢?

在实际编码当中,我们需要读取一些数据,比如配置文件、文本内容、图片甚至是任何二进制流,为此我们要加入很多的重载方法,比如:

read(File file){...}

read(InputStream in){...}

read(byte[] bytes){...}

read(URL url){...}

等等如此,这样会造成整个代码变得非常冗余,查找API也很费劲。其实无论数据来自哪里,最终目的是,我们想从这些地方读到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的详细定义见:Resource.java

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

  • BytesResource 从byte[]中读取资源

  • InputStreamResource 从流中读取资源

  • StringResource 从String中读取资源

  • UrlResource 从URL中读取资源

  • FileResource 从文件中读取资源

  • ClassPathResource 从classpath(src/resources下)中读取资源

  • WebAppResource 从web root中读取资源

  • MultiResource 从多种资源中混合读取资源

  • MultiFileResource 从多个文件中混合读取资源

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

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

资源工具-ResourceUtil

介绍

ResourceUtil提供了资源快捷读取封装。

使用

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

同样,此工具类还封装了readBytesreadStr用于快捷读取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

简单说来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()方法获取输入流来读取文件流。

封装

面对这种复杂的读取操作,我们封装了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做过相应封装,可以以更加便捷的方式读取配置文件。相关文档请参阅Hutool-setting章节

Ciallo~(∠・ω< )⌒☆