HuTool

克隆

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

空接口

  • JDK中的Cloneable接口只是一个空接口,并没有定义成员;

image-20230107141306389

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

泛型克隆接口

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

@Data
public class CatByHuTool implements Cloneable<CatByHuTool> {
    /**
     * 名字
     */
    private String name = "miaomiao";
    /**
     * 年龄
     */
    private int age = 2;

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

泛型克隆类

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

/**
 * @author BNTang
 * @version 1.0
 * @description
 * @since 2023-16-07
 **/
@Getter
@Setter
@ToString
public class DogByInherit extends CloneSupport<DogByInherit> {
    /**
     * 名字
     */
    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) 用于简化克隆调用。

类型转换

  • 痛点

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

Convert 类

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

Java 常见类型转换

  • 转换为字符串:
public static void paramToString() {
    int a = 1;
    // aStr为"1"
    String aStr = Convert.toStr(a);
    System.out.println("aStr = " + aStr);

    long[] b = {1, 2, 3, 4, 5};
    // bStr为:"[1, 2, 3, 4, 5]"
    String bStr = Convert.toStr(b);
    System.out.println("b = " + b);
    System.out.println("bStr = " + bStr);
}
  • 转换为指定类型数组:
public static void paramToArray() {
    String[] b = {"1", "2", "3", "4"};
    //结果为Integer数组
    Integer[] intArray = Convert.toIntArray(b);
    System.out.println("intArray = " + intArray);

    long[] c = {1, 2, 3, 4, 5};
    //结果为Integer数组
    Integer[] intArray2 = Convert.toIntArray(c);
    System.out.println("intArray2 = " + intArray2);
}
  • 转换为日期对象:
public static void paramToDate() {
    String a = "2017-05-06";
    Date value = Convert.toDate(a);
    System.out.println("value = " + value);
}
  • 转换为集合:
public static void paramToList() {
    Object[] a = {"a", "你", "好", "", 1};
    List<?> list = Convert.convert(List.class, a);
    System.out.println("list = " + list);

    //从4.1.11开始可以这么用
    List<?> list2 = Convert.toList(a);
    System.out.println("list2 = " + list2);

    Student student = new Student();
    List<?> objects = Convert.toList(student);
    System.out.println(objects);
}

其它类型转换

标准类型

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

泛型类型

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

半角和全角转换

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

  • 半角转全角:
public static void HalfAngleToFullAngle() {
    String a = "123456789";

    //结果为:"123456789"
    String sbc = Convert.toSBC(a);
    System.out.println("sbc = " + sbc);
}
  • 全角转半角:
public static void fullAngleToHalfAngle() {
    String a = "123456789";

    //结果为"123456789"
    String dbc = Convert.toDBC(a);
    System.out.println("dbc = " + dbc);
}

(Hex) 16 进制

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

  • 转为16进制(Hex)字符串
public static void convertToSixTeenBase() {
    String a = "我是一个小小的可爱的字符串";

    //结果:"e68891e698afe4b880e4b8aae5b08fe5b08fe79a84e58fafe788b1e79a84e5ad97e7aca6e4b8b2"
    String hex = Convert.toHex(a, CharsetUtil.CHARSET_UTF_8);
    System.out.println("hex = " + hex);
}
  • 将16进制(Hex)字符串转为普通字符串:
public static void convertSixTeenBaseToString() {
    String hex = "e68891e698afe4b880e4b8aae5b08fe5b08fe79a84e58fafe788b1e79a84e5ad97e7aca6e4b8b2";

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

    //注意:在4.1.11之后hexStrToStr将改名为hexToStr
    String raw2 = Convert.hexToStr(hex, CharsetUtil.CHARSET_UTF_8);
    System.out.println("raw2 = " + raw2);
}

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

Unicode 和字符串转换

与16进制类似,Convert类同样可以在字符串和Unicode之间轻松转换:

public static void unicodeAndStringConvert() {
    String a = "我是一个小小的可爱的字符串";

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

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

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

编码转换

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

public static void codingConvert() {
    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);

    System.out.println("raw.equals(a) = " + raw.equals(a));
}

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

时间单位转换

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

public static void timeUnitConvert() {
    long a = 4535345;

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

    System.out.println("minutes = " + minutes);
}

金额大小写转换

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

public static void amountCaseConvert() {
    double a = 67556.32;

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

    System.out.println("digitUppercase = " + digitUppercase);
}

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

数字转换

  • 数字转为英文表达
public static void digitalConvertEnglishExpress() {
    // ONE HUNDRED AND CENTS TWENTY THREE ONLY
    String format = Convert.numberToWord(100.23);

    System.out.println("format = " + format);
}
  • 数字简化
public static void digitalSimplify() {
    // 1.2k
    String format = Convert.numberToSimple(1200);

    System.out.println("format = " + format);
}
  • 数字转中文

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

public static void digitalConvertChinese() {
    // 一万零八百八十九点七二
    String f1 = Convert.numberToChinese(10889.72356, false);
    System.out.println("f1 = " + f1);

    // 使用金额大写
    // 壹万贰仟陆佰伍拾叁
    String f2 = Convert.numberToChinese(12653, true);
    System.out.println("f2 = " + f2);
}
  • 数字中文表示转换为数字
public static void chineseNumberToNumber() {
    // 1012
    int f1 = Convert.chineseToNumber("一千零一十二");
    System.out.println("f1 = " + f1);
}

原始类和包装类转换

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

public static void originalClassPackagingConvert() {
    //去包装
    Class<?> wrapClass = Integer.class;

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

    //包装
    Class<?> primitiveClass = long.class;
    //结果为:Long.class
    Class<?> wraped = Convert.wrap(primitiveClass);
    System.out.println("wraped = " + wraped);
}

自定义类型转换 ConverterRegistry

  • 由来

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

  • 解决

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

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

通过这种方式,实现类灵活的类型转换。使用方式如下:

public static void customTypeConvert() {
    int a = 3423;
    ConverterRegistry converterRegistry = ConverterRegistry.getInstance();
    String result = converterRegistry.convert(String.class, a);
    // Assert.assertEquals("3423", result);

    System.out.println("\"3423\".equals(result) = " + "3423".equals(result));
}

自定义转换

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

  • 自定义转换器
public static class CustomConverter implements Converter<String> {
    @Override
    public String convert(Object value, String defaultValue) throws IllegalArgumentException {
        return "Custom: " + value.toString();
    }
}
  • 注册转换器
public static ConverterRegistry registeredConverter() {
    ConverterRegistry converterRegistry = ConverterRegistry.getInstance();

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

    return converterRegistry;
}
  • 执行转换
public static void execConvert() {
    int a = 454553;

    ConverterRegistry converterRegistry = registeredConverter();

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

    System.out.println("\"Custom: 454553\".equals(result) = " + "Custom: 454553".equals(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值一一对应

月份枚举

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

public static void dateTimeMonthEnum() {
    // 31
    int lastDay = Month.of(Calendar.JANUARY).getLastDay(false);
    System.out.println("lastDay = " + lastDay);
}

另外,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之间的相互转换:

public static void dateLongCalendarConvert() {
    // 当前时间
    Date date = DateUtil.date();
    System.out.println("date = " + date);

    // 当前时间
    Date date2 = DateUtil.date(Calendar.getInstance());
    System.out.println("date2 = " + date2);

    // 当前时间
    Date date3 = DateUtil.date(System.currentTimeMillis());
    System.out.println("date3 = " + date3);

    // 当前时间字符串,格式:yyyy-MM-dd HH:mm:ss
    String now = DateUtil.now();
    System.out.println("now = " + now);

    // 当前日期字符串,格式:yyyy-MM-dd
    String today = DateUtil.today();
    System.out.println("today = " + 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
public static void stringConvertDate() {
    String dateStr = "2017-03-01";
    Date date = DateUtil.parse(dateStr);
    System.out.println("date = " + date);
}

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

public static void customDateFormatStringToDate() {
    String dateStr = "2017-03-01";
    Date date = DateUtil.parse(dateStr, "yyyy-MM-dd");
    System.out.println("date = " + date);
}
  • 格式化日期输出
public static void formatDateOutPut() {
    String dateStr = "2017-03-01";
    Date date = DateUtil.parse(dateStr);

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

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

    // 结果:2017-03-01 00:00:00
    String formatDateTime = DateUtil.formatDateTime(date);
    System.out.println("formatDateTime = " + formatDateTime);

    // 结果:00:00:00
    String formatTime = DateUtil.formatTime(date);
    System.out.println("formatTime = " + formatTime);
}
  • 获取Date对象的某个部分
public static void getDatePart() {
    Date date = DateUtil.date();
    // 获得年的部分
    System.out.println("DateUtil.year(date) = " + DateUtil.year(date));

    // 获得月份,从0开始计数
    System.out.println("DateUtil.month(date) = " + DateUtil.month(date));

    // 获得月份枚举
    System.out.println("DateUtil.monthEnum(date) = " + DateUtil.monthEnum(date));
    // .....
}
  • 开始和结束时间

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

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

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

    // 一天的结束,结果:2017-03-01 23:59:59
    Date endOfDay = DateUtil.endOfDay(date);
    System.out.println("endOfDay = " + endOfDay);
}
  • 日期时间偏移

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

public static void dateTimeOffSet() {
    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);
    System.out.println("newDate = " + newDate);

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

    // 常用偏移,结果:2017-03-01 19:33:23
    DateTime newDate3 = DateUtil.offsetHour(date, -3);
    System.out.println("newDate3 = " + newDate3);
}
  • 针对当前时间,提供了简化的偏移方法(例如昨天、上周、上个月等):
public static void currentDateOffSet() {
    // 昨天
    System.out.println("DateUtil.yesterday() = " + DateUtil.yesterday());

    // 明天
    System.out.println("DateUtil.tomorrow() = " + DateUtil.tomorrow());

    // 上周
    System.out.println("DateUtil.lastWeek() = " + DateUtil.lastWeek());

    // 下周
    System.out.println("DateUtil.nextWeek() = " + DateUtil.nextWeek());

    // 上个月
    System.out.println("DateUtil.lastMonth() = " + DateUtil.lastMonth());

    // 下个月
    System.out.println("DateUtil.nextMonth() = " + DateUtil.nextMonth());
}
  • 日期时间差

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

public static void dateTimeDifference(){
    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);
    System.out.println("betweenDay = " + betweenDay);
}
  • 格式化时间差

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

public static void formatTimeDifference() {
    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);

    long between = DateUtil.betweenMs(date1, date2);

    // Level.MINUTE表示精确到分
    String formatBetween = DateUtil.formatBetween(between, BetweenFormatter.Level.MINUTE);

    // 输出:31天1小时
    Console.log(formatBetween);
}
  • 星座和属相
public static void constellationAndChineseZodiac() {
    // "双子座"
    String zodiac = DateUtil.getZodiac(Month.MAY.getValue(), 25);
    System.out.println("zodiac = " + zodiac);

    // "蛇"
    String chineseZodiac = DateUtil.getChineseZodiac(2001);
    System.out.println("chineseZodiac = " + chineseZodiac);
}
  • 其它
public static void other() {
    // 年龄
    System.out.println("DateUtil.ageOfNow(\"2001-05-25\") = " + DateUtil.ageOfNow("2001-05-25"));

    // 是否闰年
    System.out.println("DateUtil.isLeapYear(2023) = " + DateUtil.isLeapYear(2023));
}

DateTime 日期时间对象

由来

考虑工具类的局限性,在某些情况下使用并不简便,于是DateTime类诞生。DateTime对象充分吸取Joda-Time库的优点,并提供更多的便捷方法,这样我们在开发时不必再单独导入Joda-Time库便可以享受简单快速的日期时间处理过程。

说明

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

使用

  • 新建对象

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

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

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

public static void newObj() {
    Date date = new Date();

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

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

    Console.log(now);
    Console.log(dt);
}
  • 使用对象

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

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

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

    // 年,结果:2017
    int year = dateTime.year();
    System.out.println("year = " + year);

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

    // 月份,结果:Month.JANUARY
    Month month = dateTime.monthEnum();
    System.out.println("month = " + month);

    // 日,结果:5
    int day = dateTime.dayOfMonth();
    System.out.println("day = " + day);
}

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

  • 对象的可变性

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

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

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

    // 设置为不可变对象后变动将返回新对象,此时offset != dateTime
    dateTime.setMutable(false);
    offset = dateTime.offset(DateField.YEAR, 0);
    System.out.println("offset = " + offset);
    System.out.println("offset == dateTime = " + (offset == dateTime));
}
  • 格式化为字符串

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

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

    // 结果:2017-01-05 12:34:23
    String dateStr = dateTime.toString();
    System.out.println("dateStr = " + dateStr);

    // 结果:2017/01/05
    String dateStr2 = dateTime.toString("yyyy/MM/dd");
    System.out.println("dateStr2 = " + dateStr2);
}

ChineseDate 农历日期

介绍

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

使用

  • 构建 ChineseDate 对象

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

public static void createChineseDateObj() {
    // 通过农历构建
    ChineseDate chineseDate = new ChineseDate(1992, 12, 14);
    System.out.println("chineseDate = " + chineseDate);

    // 通过公历构建
    ChineseDate chineseDate2 = new ChineseDate(DateUtil.parseDate("1993-01-06"));
    System.out.println("chineseDate2 = " + chineseDate2);
}
  • 基本使用
public static void chineseDateBasicUse() {
        // 通过公历构建
//        ChineseDate date = new ChineseDate(DateUtil.parseDate("2023-01-13"));
        ChineseDate date = new ChineseDate(DateUtil.parseDate("2020-01-25"));

        // 一月
        System.out.println("date.getChineseMonth() = " + date.getChineseMonth());

        // 正月
        System.out.println("date.getChineseMonthName() = " + date.getChineseMonthName());

        // 初一
        System.out.println("date.getChineseDay() = " + date.getChineseDay());

        // 庚子
        System.out.println("date.getCyclical() = " + date.getCyclical());

        // 生肖:鼠
        System.out.println("date.getChineseZodiac() = " + date.getChineseZodiac());

        // 传统节日(部分支持,逗号分隔):春节
        System.out.println("date.getFestivals() = " + date.getFestivals());

        // 庚子鼠年 正月初一
        System.out.println("date = " + date);
    }
  • 获取天干地支

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

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

    // 庚子年甲申月癸卯日
    String cyclicalYMD = chineseDate.getCyclicalYMD();
    System.out.println("cyclicalYMD = " + cyclicalYMD);
}

LocalDateTime 工具 LocalDateTimeUtil

介绍

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

使用

  • 日期转换
public static void dateConvert() {
    String dateStr = "2020-01-23T12:23:56";
    DateTime dt = DateUtil.parse(dateStr);

    // Date对象转换为LocalDateTime
    LocalDateTime of = LocalDateTimeUtil.of(dt);
    System.out.println("of = " + of);

    // 时间戳转换为LocalDateTime
    of = LocalDateTimeUtil.ofUTC(dt.getTime());
    System.out.println("of = " + of);
}
  • 日期字符串解析
public static void dateStringAnalysis() {
    // 解析ISO时间
    LocalDateTime localDateTime = LocalDateTimeUtil.parse("2020-01-23T12:23:56");
    System.out.println("localDateTime = " + localDateTime);

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

解析同样支持LocalDate

public static void dateStringAnalysisToLocalDate() {
    LocalDate localDate = LocalDateTimeUtil.parseDate("2020-01-23");
    System.out.println("localDate = " + localDate);

    // 解析日期时间为LocalDate,时间部分舍弃
    localDate = LocalDateTimeUtil.parseDate("2020-01-23T12:23:56", DateTimeFormatter.ISO_DATE_TIME);
    System.out.println("localDate = " + localDate);
}
  • 日期格式化
public static void dateFormat() {
    LocalDateTime localDateTime = LocalDateTimeUtil.parse("2020-01-23T12:23:56");

    // "2020-01-23 12:23:56"
    String format = LocalDateTimeUtil.format(localDateTime, DatePattern.NORM_DATETIME_PATTERN);
    System.out.println("format = " + format);
}
  • 日期偏移
public static void dateOffSet() {
    final LocalDateTime localDateTime = LocalDateTimeUtil.parse("2020-01-23T12:23:56");

    // 增加一天
    // "2020-01-24T12:23:56"
    LocalDateTime offset = LocalDateTimeUtil.offset(localDateTime, 1, ChronoUnit.DAYS);
    System.out.println("offset = " + offset);
}

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

public static void dateOffSetReduce() {
    final LocalDateTime localDateTime = LocalDateTimeUtil.parse("2020-01-23T12:23:56");

    // 增加一天
    // "2020-01-24T12:23:56"
    LocalDateTime offset = LocalDateTimeUtil.offset(localDateTime, -1, ChronoUnit.DAYS);
    System.out.println("offset = " + offset);
}
  • 计算时间间隔
public static void calculateDateInterval() {
    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
    System.out.println("between.toDays() = " + between.toDays());
}
  • 一天的开始和结束
public static void getOneDayStartEnd() {
    LocalDateTime localDateTime = LocalDateTimeUtil.parse("2020-01-23T12:23:56");

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

    // "2020-01-23T23:59:59.999999999"
    LocalDateTime endOfDay = LocalDateTimeUtil.endOfDay(localDateTime);
    System.out.println("endOfDay = " + endOfDay);
}

TimeInterval 计时器工具

介绍

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

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

使用

public static void useTimeInterval() {
    TimeInterval timer = DateUtil.timer();

    //---------------------------------
    //------- 这是执行过程
    //---------------------------------
    Timer timerVar = new Timer();
    TimerTask task = new TimerTask() {
        @Override
        public void run() {
            //添加需要执行的代码
            System.out.println("业务代码!");
        }
    };
    // 设置定时任务,5000 毫秒后执行一次
    timerVar.schedule(task, 1000);

    // 花费毫秒数
    long ms = timer.interval();
    System.out.println("ms = " + ms);

    // 返回花费时间,并重置开始时间
    long time = timer.intervalRestart();
    System.out.println("time = " + time);

    // 花费分钟数
    long minutes = timer.intervalMinute();
    System.out.println("minutes = " + minutes);
}
  • 也可以实现分组计时:
public static void groupTim() {
    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)

IoUtil IO工具类

?> 由来

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

?> 方法

  • 拷贝

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

以文件流拷贝为例:

public static void copy() {
    BufferedInputStream in = FileUtil.getInputStream("D:/Java/io/hutool/IoUtil/test.txt");
    BufferedOutputStream out = FileUtil.getOutputStream("D:/Java/io/hutool/IoUtil/test2.txt");

    long copySize = IoUtil.copy(in, out, IoUtil.DEFAULT_BUFFER_SIZE);
    System.out.println("copySize = " + copySize);
}

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文档。

FileTypeUtil 文件类型判断

?> 由来

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

?> 使用

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

public static void getType() {
    File file = FileUtil.file("E:/电脑壁纸/ROG.jpg");
    String type = FileTypeUtil.getType(file);

    // 输出jpg则说明确实为jpg文件
    Console.log(type);
}

?> 原理和局限性

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

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

?> 自定义类型

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

public static void customFileType() {
    FileTypeUtil.putFileType("ffd8ffe000104a464946", "new_jpg");
}

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

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

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的使用:

?> 监听指定事件

public static void listenAppointEvent() {
    File file = FileUtil.file("classpath:example/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类,只需重写对应方法即可。

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

public static void listenAllEvent() {
    File file = FileUtil.file("classpath:example/example.properties");

    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的事件合并处理触发,从而避免以上问题。

public static void delayFileChangeListenEvent() {
    File file = FileUtil.file("classpath:example/example.properties");
    WatchMonitor monitor = WatchMonitor.createAll(file, new DelayWatcher(new SimpleWatcher() {
        @Override
        public void onModify(WatchEvent<?> event, Path currentPath) {
            Console.log("EVENT modify");
        }
    }, 500));
    monitor.start();
}

文件

FileReader 文件读取

?> 由来

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

?> 使用

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

public static void useFileReader() {
    // 默认UTF-8编码,可以在构造中传入第二个参数做为编码
    FileReader fileReader = new FileReader("classpath:example/example.properties");
    String result = fileReader.readString();
    System.out.println("result = " + result);
}

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

  • readBytes
  • readString
  • readLines

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

  • getReader
  • getInputStream

FileWriter 文件写入

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

public static void useFileWriter() {
    FileWriter writer = new FileWriter("classpath:example/example.properties");
    writer.write("leader_tang");
    Console.log("写入完毕!");
}

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

同样,此类提供了:

  • getOutputStream
  • getWriter
  • getPrintWriter

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

FileAppender 文件追加

?> 由来

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

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

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

?> 使用

public static void useFileAppender() {
    File file = new File("D:/organization/dromara/hutool/io/IoUtil/FileAppender.txt");

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

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

Tailer 文件跟随

?> 由来

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

?> 使用

public static void useTailer() {
    Tailer tailer = new Tailer(FileUtil.file("D:/organization/dromara/hutool/file/test.log"), Tailer.CONSOLE_HANDLER, 2);
    tailer.start();
}

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

当然我们也可以自定义行处理器,实现 LineHandler 接口,然后重写 handle 方法即可。

/**
 * 命令行打印的行处理器
 * 
 * @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. 获取文件名
public static void getFileName() {
    File file = FileUtil.file("D:/organization/dromara/hutool/file/test.log");

    // test.txt
    String name = FileNameUtil.getName(file);
    System.out.println("name = " + name);
}
  1. 获取主文件名和扩展名
public static void getMainFileNameAndExtension() {
    File file = FileUtil.file("D:/organization/dromara/hutool/file/test.log");

    // "test"
    String mainName = FileNameUtil.mainName(file);
    System.out.println("mainName = " + mainName);

    // "log"
    String extName = FileNameUtil.extName(file);
    System.out.println("extName = " + extName);
}

!> 注意,此处获取的扩展名不带.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,读取就变得非常简单:

test.xml:

<?xml version="1.0" encoding="UTF-8" ?>
<BNTnag>

</BNTnag>
public static void useResourceUtilReadStr() {
    String str = ResourceUtil.readUtf8Str("test.xml");
    System.out.println("str = " + str);
}

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

public static void useResourceUtilReadStrByNewPath() {
    String str = ResourceUtil.readUtf8Str("config/test.xml");
    System.out.println("str = " + str);
}

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

ClassPathResource ClassPath资源访问

?> 什么是ClassPath

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

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

?> 由来

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

public static void getClassPathFile() throws IOException {
    String path = "config.properties";
    InputStream in = THuToolClassPathResource.class.getClassLoader().getResource(path).openStream();
    System.out.println("in = " + in);
}

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

?> 封装

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

public static void useClassPathResource() throws IOException {
    ClassPathResource resource = new ClassPathResource("config.properties");
    
    Properties properties = new Properties();
    properties.load(resource.getStream());

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

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

!> Hutool提供针对properties的封装类Props,同时提供更加强大的配置文件Setting类,这两个类已经针对ClassPath做过相应封装,可以以更加便捷的方式读取配置文件。相关文档请参阅Hutool-setting章节

工具类

?> 包含内容

此包中的工具类为未经过分类的一些工具类,提供一些常用的工具方法。

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

关于工具类的说明和使用,请参阅下面的章节。

StrUtil 字符串工具

?> 由来

这个工具的用处类似于Apache Commons Lang中的StringUtil,之所以使用StrUtil而不是使用StringUtil是因为前者更短,而且Str这个简写我想已经深入人心了,大家都知道是字符串的意思。常用的方法例如isBlankisNotBlankisEmptyisNotEmpty这些我就不做介绍了,判断字符串是否为空,下面我说几个比较好用的功能。

?> 方法

  1. hasBlankhasEmpty方法

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

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

  1. removePrefixremoveSuffix方法

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

public static void useRemoveSuffix() {
    // fileName -> pretty_girl
    String fileName = StrUtil.removeSuffix("pretty_girl.jpg", ".jpg");
    System.out.println("fileName = " + fileName);
}

还有忽略大小写的removePrefixIgnoreCaseremoveSuffixIgnoreCase都比较实用。

  1. sub方法

不得不提一下这个方法,有人说String有了subString你还写它干啥,我想说subString方法越界啥的都会报异常,你还得自己判断,难受死了,我把各种情况判断都加进来了,而且index的位置还支持负数哦,-1表示最后一个字符(这个思想来自于Python,如果学过Python的应该会很喜欢的),还有就是如果不小心把第一个位置和第二个位置搞反了,也会自动修正(例如想截取第4个和第2个字符之间的部分也是可以的哦~) 举个栗子

public static void useSub() {
    String str = "abcdefgh";

    // strSub1 -> c
    String strSub1 = StrUtil.sub(str, 2, 3);
    System.out.println("strSub1 = " + strSub1);

    // strSub2 -> cde
    String strSub2 = StrUtil.sub(str, 2, -3);
    System.out.println("strSub2 = " + strSub2);

    // strSub2 -> c
    String strSub3 = StrUtil.sub(str, 3, 2);
    System.out.println("strSub3 = " + strSub3);
}
  1. strbytes方法

好吧,我承认把String.getByte(String charsetName)方法封装在这里了,原生的String.getByte()这个方法太坑了,使用系统编码,经常会有人跳进来导致乱码问题,所以我就加了这两个方法强制指定字符集了,包了个try抛出一个运行时异常,省的我得在我业务代码里处理那个恶心的UnsupportedEncodingException

  1. format方法

我会告诉你这是我最引以为豪的方法吗?灵感来自slf4j,可以使用字符串模板代替字符串拼接,我也自己实现了一个,而且变量的标识符都一样,神马叫无缝兼容~~来,上栗子(吃多了上火吧……)

public static void useFormat() {
    String template = "{}爱{},就像老鼠爱大米";

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

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

  1. 定义的一些常量

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

更多方法请参阅API文档。

posted @   BNTang  阅读(363)  评论(0编辑  收藏  举报
编辑推荐:
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 基于Microsoft.Extensions.AI核心库实现RAG应用
· Linux系列:如何用heaptrack跟踪.NET程序的非托管内存泄露
· 开发者必知的日志记录最佳实践
阅读排行:
· winform 绘制太阳,地球,月球 运作规律
· 超详细:普通电脑也行Windows部署deepseek R1训练数据并当服务器共享给他人
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· AI 智能体引爆开源社区「GitHub 热点速览」
· 写一个简单的SQL生成工具
历史上的今天:
2021-01-09 杂七杂八
点击右上角即可分享
微信分享提示