欢迎你来读这篇博客。本文不是一篇“工具类 API 字典”,而是从真实 Java 后端开发的角度,系统梳理 Java 项目里常见工具类的使用边界、选型逻辑、常见坑位和团队规范。
序言:工具类的本质,不是少写几行代码 Java 工具类的存在,表面上是为了“少写重复代码”,但更深层的意义是统一语义。
比如判断字符串是否为空:
1 str == null || str.length() == 0
这当然能写,但问题是,团队里有人认为 " " 是空,有人认为只有 "" 是空;有人写 trim(),有人写 strip();有人遇到 null 直接 NPE,有人悄悄吞掉。时间久了,代码里就会出现大量“看起来差不多,语义不一样”的判断。
工具类解决的不是代码长度,而是:
统一空值语义;
统一集合语义;
统一 IO 关闭、编码、异常处理方式;
统一对象转换、Bean 拷贝、反射访问方式;
降低底层 API 的使用成本;
把易错细节封装在成熟库里。
但是,工具类也不是越多越好。一个项目同时引入 Apache Commons、Guava、Hutool、Spring、Jodd,再加上自己封装的 XXXUtil ,最后很容易变成“工具类动物园”。代码表面很优雅,实际依赖混乱、语义冲突、升级困难。
所以本文的核心观点是:
工具类的最佳实践,不是“看到好用就引”,而是建立一套稳定的优先级:JDK 优先,框架已有优先,第三方按场景引入,业务语义自己封装。
一、工具类选型的总原则 在进入具体库之前,先给出一个项目级的选择顺序。
1. JDK 能解决的,优先使用 JDK JDK 从 Java 8 之后已经补齐了很多能力:
Objects
Optional
Collections
Comparator
Stream
Files
Path
Base64
java.time
HttpClient
Pattern
StringJoiner
UUID
CompletableFuture
如果只是简单的非空判断、集合不可变包装、文件读写、日期计算、Base64 编解码,没有必要第一时间引第三方库。
例如:
1 2 3 4 5 6 7 8 Objects.requireNonNull(userId, "userId must not be null" ); List<String> names = List.of("Tom" , "Jerry" );String content = Files.readString(path, StandardCharsets.UTF_8);String token = Base64.getUrlEncoder().withoutPadding() .encodeToString(bytes);
JDK 的优势是稳定、无额外依赖、长期兼容。它的问题是 API 有时偏底层,部分场景写起来不够顺手。
2. 项目已经使用 Spring,就优先使用 Spring 工具类 Spring 项目里天然会有:
org.springframework.util.StringUtils
CollectionUtils
ObjectUtils
Assert
ReflectionUtils
ClassUtils
ResourceUtils
FileCopyUtils
StreamUtils
BeanUtils
LinkedMultiValueMap
UriComponentsBuilder
StopWatch
这些工具类不一定比 Apache Commons 更强,但它们和 Spring 的资源体系、类型转换体系、Bean 体系、Web 体系结合更自然。
例如在 Spring Web 项目中构造 URI:
1 2 3 4 5 6 URI uri = UriComponentsBuilder .fromUriString("https://api.example.com/orders" ) .queryParam("page" , 1 ) .queryParam("size" , 20 ) .build() .toUri();
这比手写字符串拼接靠谱得多,也比普通字符串工具类更贴近 Web 场景。
3. Apache Commons 适合做稳定、细粒度、可控的基础增强 Apache Commons 的特点是模块拆分清晰:
commons-lang3:语言基础增强;
commons-collections4:集合增强;
commons-io:IO 和文件增强;
commons-text:文本处理增强;
commons-codec:编码摘要增强;
commons-beanutils:JavaBean 反射增强。
它适合企业级项目长期使用,因为功能稳定、模块独立、侵入性低。
4. Guava 适合需要不可变集合、特殊集合、本地缓存、Preconditions 的项目 Guava 是 Google 的 Java 核心库,特点是“设计感强”,不是简单的工具方法堆砌。
它最值得用的部分是:
ImmutableList / ImmutableMap / ImmutableSet
Multimap
Multiset
BiMap
Table
Preconditions
CacheBuilder
RateLimiter
Splitter
Joiner
CharMatcher
Guava 的代码风格和 JDK 很接近,适合对代码质量、不可变对象、集合建模有要求的项目。
Hutool 的定位是“小而全”。它把大量常见能力都封装成 XXXUtil:
StrUtil
ObjectUtil
CollUtil
MapUtil
BeanUtil
DateUtil
FileUtil
IoUtil
Convert
JSONUtil
HttpUtil
SecureUtil
ReflectUtil
ClassUtil
IdUtil
Validator
它的优势是上手极快,文档中文友好,业务开发效率很高。缺点也明显:如果直接引 hutool-all,能力太全,依赖边界容易变宽;在基础框架层或中间件层,最好按模块引入。
6. Jodd 适合偏轻量、偏独立工具的项目 Jodd 相比 Hutool 更小众,但设计上强调轻量、少依赖。常见模块包括:
jodd-util
jodd-bean
jodd-http
jodd-json
jodd-props
它适合对依赖体积敏感、不想引入大框架,但又希望有更好用工具类的项目。
7. Tika 不是普通工具类,而是文档解析工具箱 Apache Tika 主要用于:
文件类型识别;
PDF、Word、Excel、PPT 等文档文本抽取;
元数据抽取;
搜索引擎索引前处理;
内容分析。
它不是“随手引一个工具类”的库,而是文档解析能力组件。Tika 很强,但依赖可能较重,解析不可信文件时还要考虑安全、超时、内存和隔离。
二、Maven 依赖建议 下面给一个常见依赖模板,版本建议交给 Spring Boot Dependency Management、公司 BOM 或统一依赖管理控制。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 <dependency > <groupId > org.apache.commons</groupId > <artifactId > commons-lang3</artifactId > </dependency > <dependency > <groupId > org.apache.commons</groupId > <artifactId > commons-collections4</artifactId > </dependency > <dependency > <groupId > commons-io</groupId > <artifactId > commons-io</artifactId > </dependency > <dependency > <groupId > org.apache.commons</groupId > <artifactId > commons-text</artifactId > </dependency > <dependency > <groupId > org.apache.tika</groupId > <artifactId > tika-core</artifactId > </dependency > <dependency > <groupId > org.apache.tika</groupId > <artifactId > tika-parsers-standard-package</artifactId > </dependency > <dependency > <groupId > com.google.guava</groupId > <artifactId > guava</artifactId > </dependency > <dependency > <groupId > cn.hutool</groupId > <artifactId > hutool-core</artifactId > </dependency > <dependency > <groupId > org.jodd</groupId > <artifactId > jodd-util</artifactId > </dependency >
如果你在写基础框架、starter、中间件,建议不要轻易引 hutool-all、Tika parsers 全家桶、Guava 全局暴露类型。业务系统可以更务实,底层框架要更克制。
三、字符串工具类:最常用,也最容易语义混乱 字符串工具类是 Java 项目里使用频率最高的一类。
1. JDK 原生字符串能力 JDK 自带能力包括:
1 2 3 4 5 6 7 8 9 10 11 String.isBlank(); String.isEmpty(); String.strip(); String.trim(); String.join(); String.format(); String.repeat(); String.contains(); String.startsWith(); String.endsWith(); String.replace();
Java 11 以后,String.isBlank() 和 strip() 已经很好用。
1 2 3 4 String value = " " ; System.out.println(value.isEmpty()); System.out.println(value.isBlank());
区别很关键:
isEmpty() 只判断长度是否为 0;
isBlank() 判断是否为空白字符串;
trim() 处理传统空白;
strip() 基于 Unicode 空白处理,更现代。
如果项目是 Java 11+,很多简单字符串判断可以直接用 JDK。
2. Apache Commons Lang:StringUtils StringUtils 是 commons-lang3 里最常用的工具类。
常见方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 StringUtils.isBlank(str); StringUtils.isNotBlank(str); StringUtils.isEmpty(str); StringUtils.isNotEmpty(str); StringUtils.defaultIfBlank(str, "default" ); StringUtils.defaultString(str); StringUtils.equals(a, b); StringUtils.equalsIgnoreCase(a, b); StringUtils.containsIgnoreCase(str, "abc" ); StringUtils.substringBefore(str, ":" ); StringUtils.substringAfter(str, ":" ); StringUtils.leftPad("7" , 3 , "0" ); StringUtils.join(list, "," );
它的核心特点是:对 null 更宽容。
1 2 3 4 5 6 7 StringUtils.isBlank(null ); StringUtils.isBlank("" ); StringUtils.isBlank(" " ); StringUtils.isEmpty(null ); StringUtils.isEmpty("" ); StringUtils.isEmpty(" " );
在业务代码中,isBlank 往往比 isEmpty 更符合输入校验语义。
例如:
1 2 3 4 5 public void createUser (String username) { if (StringUtils.isBlank(username)) { throw new IllegalArgumentException ("username must not be blank" ); } }
StringUtils 的适用场景 适合:
参数校验;
表单输入处理;
字符串截取;
默认值处理;
null-safe 比较;
字符串补齐;
简单格式转换。
不适合:
复杂模板渲染;
正则密集处理;
国际化文本;
富文本处理;
SQL 拼接。
工具类不是魔法棒,StringUtils 再强,也别拿它拼 SQL。那不是工具类的问题,是代码开始“玄学炼丹”了。
3. Apache Commons Text:StringSubstitutor commons-text 更偏文本处理。最常见的是 StringSubstitutor。
1 2 3 4 5 6 7 8 9 10 11 Map<String, String> values = Map.of( "name" , "Mario" , "env" , "prod" );String template = "Hello ${name}, current env is ${env}." ;String result = StringSubstitutor.replace(template, values); System.out.println(result);
这非常适合:
消息模板;
配置模板;
简单文本替换;
日志文案生成;
SQL 之外的非危险模板。
也支持默认值:
1 2 String template = "Hello ${name:-unknown}" ;String result = StringSubstitutor.replace(template, Map.of());
StringSubstitutor 的安全边界 不要把不可信用户输入直接交给复杂插值功能,尤其是涉及系统属性、环境变量、文件、URL、XML 等 lookup 的场景。
安全建议:
1 2 3 4 5 Map<String, String> values = Map.of("name" , userName);StringSubstitutor substitutor = new StringSubstitutor (values);String result = substitutor.replace("Hello ${name}" );
更简单地说:
业务模板用 Map 替换;不可信模板不要乱开高级 lookup。
4. Spring StringUtils Spring 的 StringUtils 和 Apache 的 StringUtils 名字一样,但语义不同。
常用方法:
1 2 3 4 5 StringUtils.hasText(str); StringUtils.hasLength(str); StringUtils.trimWhitespace(str); StringUtils.commaDelimitedListToStringArray(str); StringUtils.collectionToCommaDelimitedString(list);
Spring 常用的是:
1 StringUtils.hasText(str)
它表示字符串不为 null,长度大于 0,并且至少包含一个非空白字符。
在 Spring 项目里做框架层判断时,用它很自然:
1 2 3 if (!StringUtils.hasText(beanName)) { throw new IllegalArgumentException ("beanName must have text" ); }
但是如果业务代码里已经统一用 Apache StringUtils.isBlank,就不要混着用,否则读代码的人会一直在心里翻译:“这俩到底哪个判断空格?”
Hutool 的 StrUtil 非常好用,尤其在中文业务项目里。
常见方法:
1 2 3 4 5 6 7 8 9 StrUtil.isBlank(str); StrUtil.isNotBlank(str); StrUtil.emptyToDefault(str, "default" ); StrUtil.blankToDefault(str, "default" ); StrUtil.format("hello {}" , "world" ); StrUtil.split(str, ',' ); StrUtil.join("," , list); StrUtil.subBefore(str, ":" , false ); StrUtil.subAfter(str, ":" , false );
StrUtil.format 很适合轻量字符串格式化:
1 String msg = StrUtil.format("用户 {} 创建订单 {}" , userId, orderNo);
相比 String.format,它更简单;相比日志占位符,它能直接返回字符串。
6. Guava Joiner / Splitter Guava 的 Joiner 和 Splitter 设计非常优雅。
1 2 3 4 5 String result = Joiner.on("," ) .skipNulls() .join("a" , null , "b" );
1 2 3 4 5 6 List<String> list = Splitter.on("," ) .trimResults() .omitEmptyStrings() .splitToList("a, b, , c" );
Guava 这里的优势是“链式配置语义明确”。
对比一下普通写法:
1 2 3 4 Arrays.stream(str.split("," )) .map(String::trim) .filter(s -> !s.isEmpty()) .toList();
也不是不能写,但 Guava 的语义更直观。
四、对象与空值工具类:别把 Optional 用成新型 null 1. JDK Objects Objects 是最值得优先使用的 JDK 工具类之一。
常用方法:
1 2 3 4 5 6 7 Objects.requireNonNull(obj); Objects.requireNonNull(obj, "obj must not be null" ); Objects.equals(a, b); Objects.hash(a, b, c); Objects.toString(obj, "" ); Objects.nonNull(obj); Objects.isNull(obj);
典型用法:
1 2 3 4 5 6 public OrderService (OrderRepository orderRepository) { this .orderRepository = Objects.requireNonNull( orderRepository, "orderRepository must not be null" ); }
在 Stream 里过滤 null:
1 2 3 List<String> result = list.stream() .filter(Objects::nonNull) .toList();
2. Apache ObjectUtils commons-lang3 的 ObjectUtils 提供更多 null-safe 工具。
1 2 3 4 5 6 ObjectUtils.defaultIfNull(value, defaultValue); ObjectUtils.firstNonNull(a, b, c); ObjectUtils.anyNull(a, b, c); ObjectUtils.allNotNull(a, b, c); ObjectUtils.isEmpty(obj); ObjectUtils.isNotEmpty(obj);
比如多个候选值取第一个非空:
1 2 3 4 5 6 String name = ObjectUtils.firstNonNull( user.getNickName(), user.getRealName(), user.getUsername(), "unknown" );
注意:ObjectUtils.isEmpty 虽然方便,但语义比较宽,会同时处理字符串、数组、集合、Map、Optional 等。团队里如果想要语义更明确,建议不要滥用它。
3. Spring ObjectUtils Spring 的 ObjectUtils 常见于框架层。
1 2 3 4 ObjectUtils.isEmpty(obj); ObjectUtils.nullSafeEquals(a, b); ObjectUtils.nullSafeHashCode(obj); ObjectUtils.containsElement(array, element);
Spring 的 ObjectUtils.isEmpty 也会判断数组、集合、Map、Optional 等。它适合 Spring 框架上下文,但业务代码里不要和 Apache、Hutool 混用。
4. Guava Preconditions Guava 的 Preconditions 非常适合参数校验。
1 2 3 checkNotNull(userId, "userId must not be null" ); checkArgument(amount.compareTo(BigDecimal.ZERO) > 0 , "amount must be positive" ); checkState(order.canPay(), "order can not pay" );
通常建议静态导入:
1 2 3 import static com.google.common.base.Preconditions.checkArgument;import static com.google.common.base.Preconditions.checkNotNull;import static com.google.common.base.Preconditions.checkState;
语义区别:
1 2 3 checkNotNull(obj); checkArgument(expr); checkState(expr);
例如:
1 2 3 4 5 6 7 8 public void pay (Order order, BigDecimal amount) { checkNotNull(order, "order must not be null" ); checkNotNull(amount, "amount must not be null" ); checkArgument(amount.compareTo(BigDecimal.ZERO) > 0 , "amount must be positive" ); checkState(order.canPay(), "order status does not allow payment" ); order.pay(amount); }
Preconditions 的好处是语义极其清晰。坏处是项目如果没用 Guava,为了几个校验方法单独引它就没必要。
5. Spring Assert Spring 项目里也可以用 Assert:
1 2 3 4 Assert.notNull(userId, "userId must not be null" ); Assert.hasText(username, "username must have text" ); Assert.notEmpty(list, "list must not be empty" ); Assert.isTrue(amount.compareTo(BigDecimal.ZERO) > 0 , "amount must be positive" );
Spring Assert 常见于框架代码、组件初始化、Bean 参数校验。业务校验不建议全部用 Assert,因为它抛出的是运行时异常,错误码、国际化、前端提示都不好统一。
业务场景更建议:
1 2 3 if (amount.compareTo(BigDecimal.ZERO) <= 0 ) { throw new BizException (ErrorCode.INVALID_AMOUNT); }
五、集合工具类:从“能用”到“表达建模” 集合工具类是 Java 后端绕不开的主题。
1. JDK Collections JDK 的 Collections 提供基础集合算法和包装。
常见方法:
1 2 3 4 5 6 7 8 9 10 Collections.emptyList(); Collections.emptyMap(); Collections.singletonList(value); Collections.unmodifiableList(list); Collections.sort(list); Collections.reverse(list); Collections.shuffle(list); Collections.binarySearch(list, key); Collections.frequency(list, value); Collections.disjoint(list1, list2);
例如:
1 2 3 4 public List<Order> listOrders (Long userId) { List<Order> orders = orderRepository.findByUserId(userId); return orders == null ? Collections.emptyList() : orders; }
不过 Java 9+ 以后,更推荐:
1 2 List<String> names = List.of("a" , "b" ); Map<String, Integer> scoreMap = Map.of("Tom" , 90 , "Jerry" , 95 );
2. Apache Commons Collections4 commons-collections4 是对 JDK 集合体系的增强。
常用类:
CollectionUtils
ListUtils
SetUtils
MapUtils
IterableUtils
IteratorUtils
ComparatorUtils
Predicate
Transformer
Bag
MultiValuedMap
常见方法:
1 2 3 4 5 6 7 8 CollectionUtils.isEmpty(collection); CollectionUtils.isNotEmpty(collection); CollectionUtils.emptyIfNull(collection); CollectionUtils.union(a, b); CollectionUtils.intersection(a, b); CollectionUtils.subtract(a, b); CollectionUtils.disjunction(a, b); CollectionUtils.isEqualCollection(a, b);
例如计算两个集合的交集、差集:
1 2 3 4 5 6 7 8 9 10 List<Long> oldIds = List.of(1L , 2L , 3L ); List<Long> newIds = List.of(2L , 3L , 4L ); Collection<Long> added = CollectionUtils.subtract(newIds, oldIds); Collection<Long> removed = CollectionUtils.subtract(oldIds, newIds); Collection<Long> exists = CollectionUtils.intersection(oldIds, newIds); System.out.println("新增:" + added); System.out.println("删除:" + removed); System.out.println("保留:" + exists);
在权限、菜单、角色、商品标签、结算单关联关系等场景里,集合差集非常常用。
3. MapUtils MapUtils 适合从 Map 中安全取值。
1 2 3 4 String name = MapUtils.getString(map, "name" );Integer age = MapUtils.getInteger(map, "age" );Long userId = MapUtils.getLong(map, "userId" );Boolean enabled = MapUtils.getBoolean(map, "enabled" );
但要注意:Map 取值工具会弱化类型约束。业务核心代码里,如果长期依赖 Map<String, Object>,说明模型设计可能已经开始“泥石流”了。
建议:
边界层可以用 Map;
解析层可以用 Map;
业务核心层尽量转成 DTO / Command / Entity。
4. Guava Immutable Collections Guava 最值得使用的能力之一,就是不可变集合。
1 2 3 4 5 6 ImmutableList<String> names = ImmutableList.of("Tom" , "Jerry" ); ImmutableMap<String, Integer> scoreMap = ImmutableMap.of( "Tom" , 90 , "Jerry" , 95 );
不可变集合的意义不是“少写代码”,而是表达:
这个集合创建之后不应该再被修改。
这对配置、常量、策略表、白名单、枚举映射非常有价值。
1 2 3 4 5 private static final ImmutableMap<String, BillType> BILL_TYPE_MAP = ImmutableMap.<String, BillType>builder() .put("AR" , BillType.RECEIVABLE) .put("AP" , BillType.PAYABLE) .build();
如果你用普通 HashMap:
1 private static final Map<String, BillType> BILL_TYPE_MAP = new HashMap <>();
即使变量是 final,Map 内容仍然可以被修改。final 只保证引用不变,不保证对象内部状态不变。
5. Guava Multimap Java 里经常遇到一对多关系:
1 Map<Long, List<Order>> userOrderMap = new HashMap <>();
传统写法:
1 userOrderMap.computeIfAbsent(userId, k -> new ArrayList <>()).add(order);
Guava 可以用 Multimap:
1 2 3 4 5 6 7 Multimap<Long, Order> userOrderMap = ArrayListMultimap.create();for (Order order : orders) { userOrderMap.put(order.getUserId(), order); } Collection<Order> userOrders = userOrderMap.get(userId);
这个模型更自然:一个 key 对应多个 value,本来就是 Multimap。
适用场景:
用户 -> 订单;
店铺 -> 商品;
角色 -> 权限;
分类 -> 标签;
批次号 -> 明细;
单据 ID -> 操作日志。
6. Guava Multiset Multiset 可以理解为“带计数的 Set”。
1 2 3 4 5 6 7 Multiset<String> words = HashMultiset.create(); words.add("apple" ); words.add("apple" ); words.add("banana" ); System.out.println(words.count("apple" ));
适合:
词频统计;
SKU 出现次数统计;
错误码统计;
任务状态数量统计;
数据重复值分析。
7. Spring CollectionUtils Spring 的 CollectionUtils 常用方法:
1 2 3 4 5 CollectionUtils.isEmpty(collection); CollectionUtils.isEmpty(map); CollectionUtils.containsAny(source, candidates); CollectionUtils.findFirstMatch(source, candidates); CollectionUtils.hasUniqueObject(collection);
Spring 里还有一个很好用的结构:MultiValueMap。
1 2 3 4 MultiValueMap<String, String> params = new LinkedMultiValueMap <>(); params.add("tag" , "java" ); params.add("tag" , "spring" ); params.add("page" , "1" );
它在 HTTP 参数、Header、表单提交里很常见。
Hutool 的 CollUtil 是业务开发利器。
1 2 3 4 5 6 7 CollUtil.isEmpty(list); CollUtil.isNotEmpty(list); CollUtil.newArrayList("a" , "b" , "c" ); CollUtil.join(list, "," ); CollUtil.getFirst(list); CollUtil.subtract(list1, list2); CollUtil.intersection(list1, list2);
如果项目已经大量使用 Hutool,业务层用 CollUtil 很舒服。但如果项目已经统一用 Apache Commons Collections,就没必要再混一套集合工具。
六、IO 与文件工具类:编码、关闭、大小,是三座大坑 1. JDK Files / Path 现代 Java 文件操作优先使用 Path 和 Files。
1 2 3 4 5 6 7 Path path = Path.of("/tmp/demo.txt" ); Files.writeString(path, "hello" , StandardCharsets.UTF_8);String content = Files.readString(path, StandardCharsets.UTF_8); List<String> lines = Files.readAllLines(path, StandardCharsets.UTF_8);
创建目录:
1 Files.createDirectories(Path.of("/tmp/app/logs" ));
复制文件:
1 Files.copy(source, target, StandardCopyOption.REPLACE_EXISTING);
遍历目录:
1 2 3 4 5 try (Stream<Path> stream = Files.walk(root)) { List<Path> files = stream .filter(Files::isRegularFile) .toList(); }
注意:Files.walk 返回的是 Stream,需要关闭,所以要放在 try-with-resources 里。
2. Apache Commons IO:FileUtils / IOUtils commons-io 很适合简化传统 IO。
常用类:
FileUtils
IOUtils
FilenameUtils
FileNameUtils
LineIterator
FileFilterUtils
IOCase
ByteOrderMark
BOMInputStream
读取文件:
1 String content = FileUtils.readFileToString(file, StandardCharsets.UTF_8);
写文件:
1 FileUtils.writeStringToFile(file, content, StandardCharsets.UTF_8);
复制目录:
1 FileUtils.copyDirectory(srcDir, destDir);
删除目录:
1 FileUtils.deleteDirectory(dir);
流复制:
1 2 3 4 try (InputStream in = ...; OutputStream out = ...) { IOUtils.copy(in, out); }
读取流为字符串:
1 String content = IOUtils.toString(inputStream, StandardCharsets.UTF_8);
这里要特别注意:字符集一定要显式指定。
错误示例:
1 String content = IOUtils.toString(inputStream);
这种写法依赖平台默认编码,在开发机没问题,上生产可能就变成乱码。线上乱码就像幽灵,你看不见它,但它会在凌晨三点准时拍你肩膀。
3. FilenameUtils 处理文件名不要手写字符串截取。
1 2 3 4 String name = FilenameUtils.getName(path);String baseName = FilenameUtils.getBaseName(path);String extension = FilenameUtils.getExtension(path);String fullPath = FilenameUtils.getFullPath(path);
例如:
1 2 3 4 String fileName = "report.final.v2.xlsx" ; System.out.println(FilenameUtils.getBaseName(fileName)); System.out.println(FilenameUtils.getExtension(fileName));
Hutool 的文件工具更偏快捷。
1 2 3 4 5 String content = FileUtil.readUtf8String(file); FileUtil.writeUtf8String(content, file); FileUtil.copy(src, dest, true ); FileUtil.del(file); List<String> lines = FileUtil.readUtf8Lines(file);
流工具:
1 2 3 byte [] bytes = IoUtil.readBytes(inputStream);String content = IoUtil.read(inputStream, StandardCharsets.UTF_8); IoUtil.copy(inputStream, outputStream);
业务小工具、脚本、后台管理系统里非常方便。
5. Spring Resource / StreamUtils / FileCopyUtils Spring 项目里读取 classpath 资源,建议使用 Resource 抽象。
1 2 3 4 5 Resource resource = new ClassPathResource ("template/order.txt" );try (InputStream inputStream = resource.getInputStream()) { String content = StreamUtils.copyToString(inputStream, StandardCharsets.UTF_8); }
不要迷信 ResourceUtils.getFile("classpath:xxx")。在本地开发时它可能能拿到文件,但打成 jar 后,classpath 资源不一定是真实文件系统里的File。更稳妥的是使用 Resource#getInputStream()。
七、日期时间工具类:优先 java.time,少用 Date 1. JDK java.time Java 8 后,应优先使用 java.time。
常用类:
LocalDate
LocalTime
LocalDateTime
ZonedDateTime
Instant
Duration
Period
DateTimeFormatter
ZoneId
示例:
1 2 3 4 5 6 7 LocalDate today = LocalDate.now();LocalDate nextMonth = today.plusMonths(1 );LocalDateTime now = LocalDateTime.now();String text = now.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss" ));
时间间隔:
1 2 3 4 5 6 7 8 9 Instant start = Instant.now();Instant end = Instant.now();Duration duration = Duration.between(start, end); System.out.println(duration.toMillis());
不要反复 new SimpleDateFormat。SimpleDateFormat 不是线程安全的。
推荐:
1 2 3 4 private static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss" );String text = LocalDateTime.now().format(DATE_TIME_FORMATTER);
如果你的项目里还有大量 Date,Hutool 的 DateUtil 很好用。
1 2 3 4 5 6 7 8 9 Date now = DateUtil.date();String nowText = DateUtil.now();String today = DateUtil.today();Date date = DateUtil.parse("2026-06-07 12:00:00" );String text = DateUtil.formatDateTime(date);
它适合老项目、遗留代码、后台管理系统。但新代码建议优先 java.time。
4. 时间工具的团队建议 建议统一规范:
新业务实体字段:优先 LocalDateTime / LocalDate / Instant;
数据库时间:明确时区策略;
前后端传输:统一 ISO-8601 或固定格式;
老接口兼容:可用 DateUtil 做转换;
不要在代码里到处散落 "yyyy-MM-dd HH:mm:ss"。
八、Bean、反射与对象拷贝:最方便,也最危险 1. Spring BeanUtils Spring 的 BeanUtils.copyProperties 很常见。
1 2 UserDTO dto = new UserDTO (); BeanUtils.copyProperties(user, dto);
它适合简单对象之间的浅拷贝。
注意:它是浅拷贝,不是深拷贝。
1 source.getAddress() == target.getAddress()
复杂嵌套对象、集合对象、类型转换、字段名不同的情况,不建议用它硬拷。
错误示例:
1 BeanUtils.copyProperties(orderEntity, orderDetailVO);
如果里面有:
BigDecimal 转 String
Integer 转枚举
List<Entity> 转 List<VO>
字段名不一致
嵌套对象转换
那就别用 BeanUtils 硬扛。应该用 MapStruct 或手写转换器。
Hutool BeanUtil 更灵活。
1 UserDTO dto = BeanUtil.copyProperties(user, UserDTO.class);
忽略 null:
1 2 3 BeanUtil.copyProperties(source, target, CopyOptions.create() .setIgnoreNullValue(true ) .setIgnoreError(true ));
Map 转 Bean:
1 User user = BeanUtil.toBean(map, User.class);
Bean 转 Map:
1 Map<String, Object> map = BeanUtil.beanToMap(user);
Hutool BeanUtil 在业务开发中很方便,尤其适合:
DTO 转换;
Map 和 Bean 互转;
部分字段更新;
后台管理系统;
导入导出场景。
但也不要滥用。对象转换如果承载业务语义,建议明确写转换逻辑。
3. Jodd BeanUtil Jodd 的 BeanUtil 也非常强,支持属性读写、嵌套属性、List、Map、数组路径访问等。
示例风格:
1 2 3 BeanUtil.pojo.setProperty(user, "name" , "Mario" );Object value = BeanUtil.pojo.getProperty(user, "name" );
嵌套属性:
1 BeanUtil.pojo.getProperty(order, "items[0].skuCode" );
这类工具适合通用框架、配置解析、动态属性访问。业务核心逻辑里要慎用,因为字符串路径没有编译期检查。
4. Spring ReflectionUtils Spring 的 ReflectionUtils 适合框架层反射。
1 2 3 Field field = ReflectionUtils.findField(User.class, "name" ); ReflectionUtils.makeAccessible(field);Object value = ReflectionUtils.getField(field, user);
反射工具类的使用原则:
框架层可以用;
基础设施可以用;
业务核心少用;
能用正常方法调用,就不要反射;
反射结果要缓存,否则性能和可读性都不好。
5. Apache Commons BeanUtils Apache Commons BeanUtils 历史很久,能力偏 JavaBean 反射、属性填充、类型转换。
1 2 3 BeanUtils.copyProperties(dest, orig); BeanUtils.setProperty(bean, "name" , "Mario" );String name = BeanUtils.getProperty(bean, "name" );
但是它的类型转换、性能、异常处理、历史安全问题都需要谨慎看待。现代项目里,简单拷贝可以用 Spring/Hutool,复杂映射推荐 MapStruct。
6. 对象拷贝建议 建议按复杂度分层:
场景
推荐方式
简单 POJO 同名字段浅拷贝
Spring BeanUtils / Hutool BeanUtil
DTO 与 Entity 字段稍有差异
手写 converter
大量对象映射、编译期安全
MapStruct
嵌套对象、集合转换
MapStruct / 手写
动态属性路径
Jodd BeanUtil / Spring BeanWrapper
Map 与 Bean 互转
Hutool BeanUtil
高性能核心链路
手写,少反射
九、类型转换工具类:Convert、ConversionService 与 TypeHandler 1. JDK 原生转换 基础转换可以用 JDK:
1 2 3 4 5 Integer.parseInt("123" ); Long.valueOf("123" ); Boolean.parseBoolean("true" );BigDecimal value = new BigDecimal ("12.34" ); Enum.valueOf(OrderStatus.class, "PAID" );
简单、清晰、无额外依赖。
Hutool 的 Convert 很适合处理边界层数据。
1 2 3 4 5 Integer age = Convert.toInt(value, 0 );Long userId = Convert.toLong(value);Boolean enabled = Convert.toBool(value, false );BigDecimal amount = Convert.toBigDecimal(value);String str = Convert.toStr(value);
例如请求参数、Excel 导入、Map 数据解析:
1 2 Integer quantity = Convert.toInt(row.get("quantity" ), 0 );BigDecimal price = Convert.toBigDecimal(row.get("price" ), BigDecimal.ZERO);
注意:Convert 很方便,但不要在核心领域模型里大量吞默认值。比如金额转换失败直接变成 BigDecimal.ZERO,可能会把数据错误伪装成正常业务。
边界层可以容错,核心层必须严谨。
3. Spring ConversionService Spring 的类型转换体系更适合框架级转换。
1 2 3 ConversionService conversionService = ...;Integer value = conversionService.convert("123" , Integer.class);
自定义 Converter:
1 2 3 4 5 6 7 8 @Component public class StringToOrderStatusConverter implements Converter <String, OrderStatus> { @Override public OrderStatus convert (String source) { return OrderStatus.of(source); } }
适合:
Spring MVC 参数绑定;
配置属性绑定;
表单转换;
框架级类型转换;
枚举转换。
4. MyBatis TypeHandler MyBatis 的类型转换核心是 TypeHandler。
比如枚举与数据库字段转换:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 public class OrderStatusTypeHandler extends BaseTypeHandler <OrderStatus> { @Override public void setNonNullParameter ( PreparedStatement ps, int i, OrderStatus parameter, JdbcType jdbcType ) throws SQLException { ps.setString(i, parameter.getCode()); } @Override public OrderStatus getNullableResult (ResultSet rs, String columnName) throws SQLException { return OrderStatus.of(rs.getString(columnName)); } @Override public OrderStatus getNullableResult (ResultSet rs, int columnIndex) throws SQLException { return OrderStatus.of(rs.getString(columnIndex)); } @Override public OrderStatus getNullableResult (CallableStatement cs, int columnIndex) throws SQLException { return OrderStatus.of(cs.getString(columnIndex)); } }
MyBatis 场景里,不要在 SQL 查询后再到处手动转换枚举。统一交给 TypeHandler,代码会干净很多。
十、Apache Tika:文件识别与内容抽取 Tika 的定位不是普通工具类,而是内容分析工具箱。
1. 文件类型识别 1 2 3 4 5 Tika tika = new Tika ();String mimeType = tika.detect(file); System.out.println(mimeType);
可以用于:
上传文件校验;
判断真实文件类型;
搜索索引前处理;
内容安全扫描前置判断。
不要只相信文件后缀:
它可能根本不是 PDF。后缀只是名字,MIME 检测才更接近内容本身。
2. 文本抽取 1 2 3 Tika tika = new Tika ();String text = tika.parseToString(file);
适合从 PDF、Word、Excel、PPT 等文档中抽取文本。
比如做一个简单的文档搜索:
1 2 3 4 5 6 7 8 public DocumentIndex buildIndex (File file) throws IOException, TikaException { Tika tika = new Tika (); String mimeType = tika.detect(file); String content = tika.parseToString(file); return new DocumentIndex (file.getName(), mimeType, content); }
3. Tika 使用注意事项 Tika 很强,但不要随便在主业务线程里解析大文件。
建议:
文件大小限制;
解析超时控制;
异步任务处理;
对不可信文件做隔离;
控制 parser 依赖;
解析失败要降级;
不要把全文直接塞进日志;
大文件不要一次性读入内存。
如果你做的是文档系统、知识库、搜索系统,Tika 很有价值。如果只是上传头像,就别上 Tika 全家桶了,杀鸡别用高达。
十一、Guava:不只是工具类,而是建模能力 Guava 很多类不是“工具方法”,而是让代码表达更准确。
1. Preconditions 前面已经讲过,适合参数和状态校验。
1 2 checkArgument(pageNo > 0 , "pageNo must be positive" ); checkArgument(pageSize > 0 && pageSize <= 500 , "invalid pageSize" );
2. ImmutableMap 做策略表 1 2 3 4 5 private static final ImmutableMap<String, Function<Order, BigDecimal>> AMOUNT_CALCULATORS = ImmutableMap.<String, Function<Order, BigDecimal>>builder() .put("AR" , Order::calculateReceivableAmount) .put("AP" , Order::calculatePayableAmount) .build();
这比一堆 if-else 更稳定:
1 2 3 4 5 6 7 Function<Order, BigDecimal> calculator = AMOUNT_CALCULATORS.get(order.getBillType());if (calculator == null ) { throw new IllegalArgumentException ("unsupported bill type" ); }BigDecimal amount = calculator.apply(order);
3. CacheBuilder 本地缓存 1 2 3 4 5 6 7 8 9 LoadingCache<Long, User> userCache = CacheBuilder.newBuilder() .maximumSize(10_000 ) .expireAfterWrite(10 , TimeUnit.MINUTES) .build(new CacheLoader <>() { @Override public User load (Long userId) { return userRepository.findById(userId); } });
使用:
1 User user = userCache.get(userId);
适合:
本地热点数据;
字典配置;
低频变化数据;
单机临时缓存;
降低重复计算。
不适合:
分布式一致性缓存;
强一致数据;
多实例共享状态;
订单金额、库存扣减这类核心状态。
分布式缓存用 Redis,本地缓存用 Guava/Caffeine。边界要清楚,不然缓存会从优化工具变成线上事故制造机。
4. RateLimiter Guava RateLimiter 可做单机限流。
1 2 3 4 5 RateLimiter limiter = RateLimiter.create(100.0 );if (limiter.tryAcquire()) { doSomething(); }
注意:这是单机限流,不是分布式限流。多实例部署时,每个实例都有自己的限流器。
5. Guava 使用建议 推荐使用:
Preconditions
ImmutableXXX
Multimap
Multiset
BiMap
CacheBuilder
RateLimiter
Splitter
Joiner
谨慎使用:
Guava Optional,现代 Java 项目更建议用 java.util.Optional;
在公共 API 返回 Guava 类型,除非团队明确接受;
Android 项目要注意 Guava 版本和体积;
和 Caffeine、Apache Commons 功能重复时要统一规范。
Hutool 的优势是“什么都有,而且好上手”。
1. StrUtil 1 2 3 StrUtil.isBlank(name); StrUtil.blankToDefault(name, "unknown" ); StrUtil.format("订单 {} 支付成功" , orderNo);
2. CollUtil 1 2 3 4 5 6 7 List<String> list = CollUtil.newArrayList("a" , "b" , "c" );String text = CollUtil.join(list, "," ); List<String> intersection = CollUtil.intersection(list1, list2) .stream() .toList();
3. Convert 1 2 Integer age = Convert.toInt(value, 0 );LocalDateTime time = Convert.toLocalDateTime(value);
4. DateUtil 1 2 Date begin = DateUtil.beginOfDay(new Date ());Date end = DateUtil.endOfDay(new Date ());
5. BeanUtil 1 UserDTO dto = BeanUtil.copyProperties(user, UserDTO.class);
6. FileUtil 1 2 String content = FileUtil.readUtf8String(file); FileUtil.writeUtf8String(content, file);
7. IdUtil 1 2 3 String uuid = IdUtil.fastSimpleUUID();long snowflakeId = IdUtil.getSnowflake(1 , 1 ).nextId();
注意:雪花 ID 的 workerId、datacenterId 不能随便写死到所有机器一样,否则分布式环境可能冲突。
8. HttpUtil 1 String result = HttpUtil.get("https://api.example.com/users" );
适合简单 HTTP 调用。但复杂生产场景,比如连接池、重试、熔断、超时治理、链路追踪,建议用 Spring RestClient、WebClient、OkHttp 或 Apache HttpClient。
适合:
业务系统;
后台管理;
数据导入导出;
小工具;
快速开发;
中文团队。
谨慎:
基础框架层;
starter;
SDK;
强依赖边界项目;
对依赖体积敏感的服务;
安全要求很高的场景。
建议:
1 2 3 4 5 <dependency > <groupId > cn.hutool</groupId > <artifactId > hutool-core</artifactId > </dependency >
而不是所有地方都:
1 2 3 4 5 <dependency > <groupId > cn.hutool</groupId > <artifactId > hutool-all</artifactId > </dependency >
hutool-all 很爽,但爽也要有边界。代码世界里,最贵的两个字就是“随便”。
十三、Jodd:轻量工具与微框架思路 Jodd 的存在感不如 Commons、Guava、Hutool,但它有自己的特点:轻量、独立、少依赖。
1. Jodd StringUtil StringUtil 提供很多字符串增强能力:
1 2 3 4 StringUtil.isBlank(str); StringUtil.isEmpty(str); StringUtil.replace(str, "a" , "b" ); StringUtil.split(str, "," );
2. Jodd BeanUtil Jodd BeanUtil 适合动态属性访问:
1 2 3 BeanUtil.pojo.setProperty(user, "name" , "Mario" );Object name = BeanUtil.pojo.getProperty(user, "name" );
嵌套:
1 Object value = BeanUtil.pojo.getProperty(order, "items[0].skuCode" );
这类能力适合框架、规则引擎、配置映射,不建议在普通业务代码里频繁使用字符串路径。
3. Jodd Props Jodd Props 是对 Java properties 的增强,支持 UTF-8、sections、profiles、macros 等。
如果你不想引完整配置中心,但又觉得 JDK Properties 太弱,Jodd Props 是个轻量选择。
4. Jodd HTTP / JSON Jodd HTTP 是轻量 HTTP 客户端,Jodd JSON 是轻量 JSON 序列化解析工具。
不过在现代 Spring Boot 项目里:
JSON 通常由 Jackson 负责;
HTTP 通常由 RestClient/WebClient/OkHttp/Apache HttpClient 负责;
配置通常由 Spring Boot 配置体系负责。
所以 Jodd 更适合轻量独立项目,而不是大型 Spring 体系里重复造一层。
十四、Spring 常用工具类深度盘点 1. Assert 1 2 3 Assert.notNull(obj, "obj must not be null" ); Assert.hasText(text, "text must have text" ); Assert.notEmpty(collection, "collection must not be empty" );
适合框架内部参数校验。
2. CollectionUtils 1 2 CollectionUtils.isEmpty(list); CollectionUtils.isEmpty(map);
3. ObjectUtils 1 2 ObjectUtils.isEmpty(obj); ObjectUtils.nullSafeEquals(a, b);
4. ClassUtils 1 2 3 ClassUtils.getShortName(UserService.class); ClassUtils.isPresent("com.example.Demo" , classLoader); ClassUtils.getDefaultClassLoader();
适合写 starter、自动配置、条件装配时使用。
5. ReflectionUtils 1 2 3 Method method = ReflectionUtils.findMethod(User.class, "getName" ); ReflectionUtils.makeAccessible(method);Object value = ReflectionUtils.invokeMethod(method, user);
6. BeanUtils 1 BeanUtils.copyProperties(source, target);
只适合简单浅拷贝。
7. LinkedMultiValueMap 1 2 3 4 MultiValueMap<String, String> form = new LinkedMultiValueMap <>(); form.add("username" , "mario" ); form.add("role" , "admin" ); form.add("role" , "developer" );
适合请求参数、表单、Header 等一 key 多 value 场景。
8. StopWatch 1 2 3 4 5 6 7 8 9 10 11 StopWatch stopWatch = new StopWatch ("order-create" ); stopWatch.start("validate" ); validate(command); stopWatch.stop(); stopWatch.start("save" ); saveOrder(command); stopWatch.stop(); System.out.println(stopWatch.prettyPrint());
适合本地调试、性能分段分析。生产链路监控还是建议用 Micrometer、日志埋点、Tracing。
9. Resource 1 2 3 4 5 Resource resource = new ClassPathResource ("template/order.txt" );try (InputStream inputStream = resource.getInputStream()) { String content = StreamUtils.copyToString(inputStream, StandardCharsets.UTF_8); }
Spring 的资源抽象非常重要。它能统一处理:
classpath;
file;
URL;
servlet context;
jar 内资源。
10. UriComponentsBuilder 1 2 3 4 5 String url = UriComponentsBuilder .fromHttpUrl("https://api.example.com/orders" ) .queryParam("userId" , userId) .queryParam("status" , "PAID" ) .toUriString();
不要手动拼 URL:
1 String url = baseUrl + "?userId=" + userId + "&status=" + status;
一旦遇到 URL 编码、空格、特殊字符、重复参数,手写拼接很容易翻车。
十五、MyBatis 中常见工具和内部组件 MyBatis 不是工具类库,但它内部有一些常用组件值得了解。
1. Resources MyBatis Resources 可用于读取 classpath 资源:
1 2 3 try (Reader reader = Resources.getResourceAsReader("mybatis-config.xml" )) { SqlSessionFactory factory = new SqlSessionFactoryBuilder ().build(reader); }
在纯 MyBatis 项目里很常见。
如果是 Spring Boot + MyBatis,通常不需要自己用它读取配置,交给自动配置即可。
2. TypeAliasRegistry MyBatis 支持类型别名:
1 2 3 4 <typeAliases > <typeAlias alias ="User" type ="com.example.User" /> </typeAliases >
底层由 TypeAliasRegistry 管理。
在自定义 MyBatis 扩展时可能会接触:
1 configuration.getTypeAliasRegistry().registerAlias("User" , User.class);
3. TypeHandlerRegistry 类型处理器注册表。
1 2 configuration.getTypeHandlerRegistry() .register(OrderStatus.class, OrderStatusTypeHandler.class);
建议把数据库字段和 Java 类型的转换逻辑沉到 TypeHandler,而不是散落在业务代码里。
MetaObject 是 MyBatis 反射系统的重要对象,用于读取和设置对象属性。
1 2 3 4 5 MetaObject metaObject = configuration.newMetaObject(user);Object name = metaObject.getValue("name" ); metaObject.setValue("name" , "Mario" );
它也支持嵌套属性路径。插件、自动填充、审计字段处理时可能会用到。
5. GenericTokenParser MyBatis 内部解析 ${}、#{} 这类 token 时会使用类似 token parser 的机制。你写自定义 SQL 解析器、模板替换器时可以参考这种设计:
核心思想是:
定义开始 token;
定义结束 token;
遇到 token 后交给 handler 处理;
输出处理后的字符串。
这比粗暴正则替换更可控。
6. MyBatis 工具使用建议
业务代码不要随意依赖 MyBatis 内部工具;
插件、拦截器、自动填充、框架扩展可以使用;
Spring Boot 项目优先使用自动配置;
类型转换优先 TypeHandler;
SQL 动态逻辑优先 XML / Provider / Dynamic SQL,而不是字符串拼接。
十六、JDK 常用工具类完整盘点 1. Objects 1 2 3 4 Objects.equals(a, b); Objects.requireNonNull(obj); Objects.hash(a, b, c); Objects.toString(obj, "" );
2. Optional Optional 适合做返回值,不适合做字段。
推荐:
1 2 3 public Optional<User> findUser (Long userId) { return userRepository.findById(userId); }
使用:
1 2 User user = findUser(userId) .orElseThrow(() -> new BizException ("user not found" ));
不推荐:
1 private Optional<String> name;
也不推荐方法参数:
1 2 public void update (Optional<String> name) { }
参数可选应该用重载、DTO 字段或明确传 null 后校验,不要把 Optional 当新型 null 容器。
3. Collections 1 2 3 4 5 Collections.emptyList(); Collections.singletonList("a" ); Collections.unmodifiableList(list); Collections.sort(list); Collections.reverse(list);
4. Arrays 1 2 3 4 5 Arrays.asList("a" , "b" ); Arrays.stream(array); Arrays.copyOf(array, newLength); Arrays.equals(a, b); Arrays.deepEquals(a, b);
注意:
1 2 List<String> list = Arrays.asList("a" , "b" ); list.add("c" );
Arrays.asList 返回的是固定长度 List,不支持增删。
如果要可变:
1 List<String> list = new ArrayList <>(Arrays.asList("a" , "b" ));
5. Comparator 1 2 3 4 5 6 List<User> users = ... users.sort( Comparator.comparing(User::getAge) .thenComparing(User::getName) );
处理 null:
1 2 3 4 5 6 users.sort( Comparator.comparing( User::getAge, Comparator.nullsLast(Integer::compareTo) ) );
6. Base64 1 2 3 4 5 String encoded = Base64.getEncoder() .encodeToString(bytes);byte [] decoded = Base64.getDecoder() .decode(encoded);
URL 安全:
1 2 3 String token = Base64.getUrlEncoder() .withoutPadding() .encodeToString(bytes);
7. UUID 1 String id = UUID.randomUUID().toString();
如果不想要横杠:
1 String id = UUID.randomUUID().toString().replace("-" , "" );
不过高并发分布式 ID,建议用专门的 ID 生成方案,例如雪花算法、数据库号段、Redis、自研发号器等。
8. Pattern 正则建议预编译:
1 2 3 4 5 6 private static final Pattern PHONE_PATTERN = Pattern.compile("^1[3-9]\\d{9}$" );public boolean isPhone (String phone) { return PHONE_PATTERN.matcher(phone).matches(); }
不要在高频方法里反复:
1 Pattern.compile(regex).matcher(value).matches();
9. HttpClient Java 11 提供了标准 HTTP Client。
1 2 3 4 5 6 7 8 9 10 11 HttpClient client = HttpClient.newHttpClient();HttpRequest request = HttpRequest.newBuilder() .uri(URI.create("https://api.example.com/users" )) .GET() .build(); HttpResponse<String> response = client.send( request, HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8) );
适合:
简单 HTTP 调用;
无需复杂连接池封装;
不想引第三方 HTTP 客户端。
复杂微服务调用仍建议使用成熟 HTTP 客户端并统一治理超时、重试、熔断、指标和链路追踪。
10. CompletableFuture 1 2 3 4 5 6 7 8 9 10 11 12 CompletableFuture<User> userFuture = CompletableFuture.supplyAsync(() -> userService.getUser(userId)); CompletableFuture<List<Order>> orderFuture = CompletableFuture.supplyAsync(() -> orderService.listOrders(userId)); CompletableFuture<Void> all = CompletableFuture.allOf(userFuture, orderFuture); all.join();User user = userFuture.join(); List<Order> orders = orderFuture.join();
注意:默认线程池是 ForkJoinPool.commonPool(),业务系统中建议显式传入自定义线程池。
1 CompletableFuture.supplyAsync(task, executor);
十七、常见工具类冲突与团队规范 1. StringUtils 冲突 常见冲突:
1 2 org.apache.commons.lang3.StringUtils org.springframework.util.StringUtils
建议:
业务层统一 Apache StringUtils 或 Hutool StrUtil;
Spring 框架层使用 Spring StringUtils;
不要一个类里混用两个 StringUtils。
2. ObjectUtils 冲突 常见冲突:
1 2 org.apache.commons.lang3.ObjectUtils org.springframework.util.ObjectUtils
建议:
普通业务默认 JDK Objects;
多默认值、对象空判断用 Apache;
Spring 框架内部用 Spring。
3. BeanUtils 冲突 常见冲突:
1 2 org.springframework.beans.BeanUtils org.apache.commons.beanutils.BeanUtils
两个方法签名和行为不同,很容易看错。
Spring:
1 BeanUtils.copyProperties(source, target);
Apache Commons:
1 BeanUtils.copyProperties(dest, orig);
参数顺序就足够让人踩坑。
建议:
项目里尽量只用一种;
不要静态导入;
复杂转换不要用 BeanUtils;
建议封装成明确的 converter。
4. Optional 冲突 现在项目里不要再使用 Guava Optional 作为新代码首选。
推荐:
除非老项目历史原因已经深度使用 Guava Optional。
5. 日期工具冲突 不要同时大量混用:
Date
Calendar
LocalDateTime
Instant
Hutool DateTime
Joda-Time
建议:
新代码统一 java.time;
老代码边界转换;
数据库存储策略统一;
DTO 时间格式统一。
十八、推荐的团队工具类优先级 可以制定如下规范。
1. 字符串 优先级:
JDK String
Apache StringUtils
Hutool StrUtil
Spring StringUtils 仅限 Spring 框架上下文
推荐:
1 2 StringUtils.isBlank(name); StringUtils.equals(a, b);
2. 集合 优先级:
JDK Collection / List / Map / Stream
JDK Collections
Apache Commons Collections
Guava 特殊集合
Hutool CollUtil
推荐:
1 CollectionUtils.isEmpty(list);
但要统一到底是 Spring 还是 Apache。
3. 文件 IO 优先级:
JDK Files / Path
Apache Commons IO
Spring Resource
Hutool FileUtil
推荐:
1 Files.readString(path, StandardCharsets.UTF_8);
4. Bean 拷贝 优先级:
手写 converter
MapStruct
Spring BeanUtils / Hutool BeanUtil
反射动态工具
推荐:
1 UserDTO dto = UserConverter.INSTANCE.toDTO(user);
而不是所有地方都:
1 BeanUtils.copyProperties(user, dto);
5. 参数校验 优先级:
业务异常手写判断
JDK Objects.requireNonNull
Guava Preconditions
Spring Assert
业务校验:
1 2 3 if (amount.compareTo(BigDecimal.ZERO) <= 0 ) { throw new BizException (ErrorCode.INVALID_AMOUNT); }
框架校验:
1 Assert.notNull(beanFactory, "beanFactory must not be null" );
6. 日期时间 优先级:
java.time
Hutool DateUtil 兼容旧 Date
避免继续使用 Calendar
不建议新项目引 Joda-Time
7. HTTP 优先级:
Spring 项目:RestClient / WebClient
简单 JDK 项目:JDK HttpClient
轻量脚本:Hutool HttpUtil / Jodd Http
复杂连接治理:OkHttp / Apache HttpClient
8. 文档解析 优先级:
简单 MIME:Tika Core
文档全文抽取:Tika Parsers
大文件解析:异步 + 超时 + 隔离
不可信文件:安全沙箱或独立服务
十九、实际业务案例:订单导入场景的工具类组合 假设有一个 Excel/CSV 订单导入场景,流程如下:
上传文件;
判断文件类型;
保存临时文件;
读取内容;
转成 DTO;
校验字段;
分组处理;
保存数据库;
返回错误报告。
可以这样组合:
1. 文件名与类型 1 2 3 4 5 String extension = FilenameUtils.getExtension(fileName);if (!Set.of("xlsx" , "csv" ).contains(extension)) { throw new BizException ("unsupported file type" ); }
更进一步用 Tika:
1 String mimeType = new Tika ().detect(file);
2. 保存文件 1 2 3 4 5 Path uploadPath = Path.of("/data/upload" , fileName); Files.createDirectories(uploadPath.getParent()); Files.copy(inputStream, uploadPath, StandardCopyOption.REPLACE_EXISTING);
3. 字段判断 1 2 3 if (StringUtils.isBlank(row.getOrderNo())) { errors.add("订单号不能为空" ); }
4. 类型转换 1 2 3 4 5 BigDecimal amount = Convert.toBigDecimal(row.getAmount(), null );if (amount == null ) { errors.add("金额格式错误" ); }
5. 分组 1 2 Map<String, List<OrderImportRow>> groupByShop = rows.stream().collect(Collectors.groupingBy(OrderImportRow::getShopCode));
或者 Guava:
1 2 3 4 5 Multimap<String, OrderImportRow> groupByShop = ArrayListMultimap.create();for (OrderImportRow row : rows) { groupByShop.put(row.getShopCode(), row); }
6. 差异计算 1 2 Collection<String> newOrderNos = CollectionUtils.subtract(importOrderNos, existsOrderNos); Collection<String> duplicatedOrderNos = CollectionUtils.intersection(importOrderNos, existsOrderNos);
7. DTO 转 Entity 简单:
1 OrderEntity entity = BeanUtil.copyProperties(row, OrderEntity.class);
复杂:
1 OrderEntity entity = OrderImportConverter.toEntity(row);
这里更推荐手写或 MapStruct,因为导入通常有大量业务语义。
二十、实际业务案例:财务结算场景的工具类组合 财务结算类系统里,工具类更要克制。因为金额、状态、账期、结余,一旦因为默认值处理不当出错,后果很麻烦。
1. 金额不要随便 default zero 危险写法:
1 BigDecimal amount = Convert.toBigDecimal(value, BigDecimal.ZERO);
如果 value 是非法字符串,直接变成 0,可能掩盖数据问题。
更安全:
1 2 3 4 5 6 7 BigDecimal amount;try { amount = new BigDecimal (value); } catch (NumberFormatException ex) { throw new BizException ("金额格式错误" ); }
或者边界层记录错误,不进入核心计算。
2. 账期判断 1 2 boolean overlap = existingStart.compareTo(currentEnd) <= 0 && existingEnd.compareTo(currentStart) >= 0 ;
这个判断比花里胡哨的日期工具更重要。业务规则清楚,代码自然清楚。
3. 状态校验 1 checkState(bill.canSettle(), "bill can not settle" );
或者业务异常:
1 2 3 if (!bill.canSettle()) { throw new BizException (ErrorCode.BILL_STATUS_NOT_ALLOWED); }
4. 日志字符串 1 2 3 4 5 6 String msg = StrUtil.format( "结算单处理完成, billNo={}, billType={}, amount={}" , billNo, billType, amount );
但如果是正式日志,更建议用日志框架占位符:
1 2 log.info("结算单处理完成, billNo={}, billType={}, amount={}" , billNo, billType, amount);
不要为了字符串工具类牺牲日志框架的延迟格式化能力。
二十一、哪些工具类不建议滥用 1. BeanUtils 不要替代领域转换 错误倾向:
1 BeanUtils.copyProperties(a, b);
满项目都是这句,看似优雅,实际没人知道字段是怎么过去的。
建议关键对象转换明确写:
1 2 3 4 5 6 7 public static OrderDTO toDTO (Order order) { OrderDTO dto = new OrderDTO (); dto.setOrderNo(order.getOrderNo()); dto.setAmount(order.getAmount()); dto.setStatusName(order.getStatus().getName()); return dto; }
2. ObjectUtils.isEmpty 不要承载复杂业务语义 1 2 if (ObjectUtils.isEmpty(value)) { }
这句话太宽泛。value 是字符串?集合?数组?Optional?不同语义混在一起,后续维护者容易误判。
3. Convert.toXXX 默认值要慎重
适合 UI 展示、导入容错,不适合核心计算。
4. FileUtils.readFileToString 不要读超大文件 1 String content = FileUtils.readFileToString(file, StandardCharsets.UTF_8);
小文件没问题,大文件会直接吃内存。大文件应使用流式处理。
5. StringSubstitutor 不要处理不可信复杂模板 简单 Map 替换没问题,高级 lookup 要谨慎。
业务系统可以,基础 SDK、starter、公共组件要克制。否则别人引你的一个小工具,顺手带走一车依赖,像买螺丝送挖掘机。
二十二、项目里是否还需要自定义 XXXUtil? 需要,但要少。
自定义工具类应该满足两个条件:
承载业务语义;
第三方工具无法准确表达。
例如可以有:
1 2 3 4 5 6 MoneyUtils BillPeriodUtils OrderStatusUtils TenantContextUtils TraceLogUtils SettlementAmountUtils
不建议有:
1 2 3 4 5 StringUtil DateUtil CollectionUtil BeanUtil ObjectUtil
这些基础工具类成熟库已经做得很好了。自己再写一套,通常只会制造 Bug。
好的业务工具类示例 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 public final class BillPeriodUtils { private BillPeriodUtils () { } public static boolean isOverlap ( LocalDate existingStart, LocalDate existingEnd, LocalDate currentStart, LocalDate currentEnd ) { Objects.requireNonNull(existingStart, "existingStart must not be null" ); Objects.requireNonNull(existingEnd, "existingEnd must not be null" ); Objects.requireNonNull(currentStart, "currentStart must not be null" ); Objects.requireNonNull(currentEnd, "currentEnd must not be null" ); return existingStart.compareTo(currentEnd) <= 0 && existingEnd.compareTo(currentStart) >= 0 ; } }
这不是重复造轮子,这是把业务规则命名。
二十三、推荐工具类清单 JDK 必会
Objects
Optional
Collections
Arrays
Comparator
Files
Path
Base64
UUID
Pattern
java.time
CompletableFuture
HttpClient
Apache Commons 推荐
StringUtils
ObjectUtils
ArrayUtils
RandomStringUtils
NumberUtils
BooleanUtils
CollectionUtils
ListUtils
SetUtils
MapUtils
FileUtils
IOUtils
FilenameUtils
StringSubstitutor
StringEscapeUtils
Guava 推荐
Preconditions
ImmutableList
ImmutableMap
ImmutableSet
Multimap
Multiset
BiMap
Table
Joiner
Splitter
CacheBuilder
RateLimiter
StrUtil
ObjectUtil
CollUtil
MapUtil
BeanUtil
Convert
DateUtil
FileUtil
IoUtil
IdUtil
ReflectUtil
ClassUtil
HttpUtil
JSONUtil
SecureUtil
Spring 推荐
Assert
StringUtils
CollectionUtils
ObjectUtils
ClassUtils
ReflectionUtils
BeanUtils
Resource
StreamUtils
FileCopyUtils
LinkedMultiValueMap
UriComponentsBuilder
StopWatch
MyBatis 推荐了解
Resources
TypeAliasRegistry
TypeHandlerRegistry
MetaObject
SystemMetaObject
GenericTokenParser
PropertyTokenizer
二十四、最终选型建议 如果是普通 Spring Boot 后端项目,我建议这样选:
基础优先级 1 JDK > Spring 内置 > Apache Commons > Guava > Hutool > Jodd/Tika 按场景
字符串 1 Apache StringUtils 或 Hutool StrUtil,二选一统一
集合 1 2 JDK Stream + Apache CollectionUtils 特殊集合用 Guava
IO 1 2 3 JDK Files 优先 复杂文件操作用 Commons IO Spring 资源读取用 Resource
Bean 拷贝 1 2 简单用 Spring/Hutool 复杂用 MapStruct/手写
日期 1 2 java.time 优先 老 Date 兼容用 Hutool DateUtil
HTTP 1 2 3 Spring 项目用 RestClient/WebClient 轻量脚本用 Hutool/Jodd 标准库可用 JDK HttpClient
文档解析
结语:工具类是秩序,不是捷径 优秀的 Java 项目,工具类不会到处乱飞,而是有清晰边界:
哪些用 JDK;
哪些用 Spring;
哪些用 Apache Commons;
哪些用 Guava;
哪些用 Hutool;
哪些必须业务自己封装;
哪些坚决不能滥用。
工具类写得好,代码会更短;工具类选得好,系统会更稳;工具类边界管得好,团队协作会更轻松。
真正成熟的工程实践,不是看到一个 XXXUtil 就兴奋,而是知道它应该出现在哪里、不应该出现在哪里。
代码可以偷懒,架构不能糊涂。Hutool 可以“甜甜的”,但生产事故可一点都不甜。
参考资料建议
Oracle Java SE API 文档
Apache Commons Lang 官方文档
Apache Commons Collections 官方文档
Apache Commons IO 官方文档
Apache Commons Text 官方文档
Apache Tika 官方文档
Google Guava 官方文档
Hutool 官方文档
Jodd 官方文档
Spring Framework 官方 Javadoc
MyBatis 官方文档