SpringBoot项目必备工具类库盘点:从Hutool到MapStruct的实战选型指南
2026/9/11 2:49:01 网站建设 项目流程

1. 为什么要专门整理一份工具类库清单

先说说我为什么想写这篇东西。做了几年SpringBoot后端,发现一个很有意思的现象:很多项目组里,每个程序员都有自己的“私藏工具包”,有的人用Hutool用得飞起,有的人只认Apache Commons,还有的人干脆自己写工具类。结果就是同一个团队里,字符串判空就有三四种写法,对象拷贝一会儿用BeanUtils一会儿用MapStruct,代码风格乱七八糟。

SpringBoot本身确实帮我们解决了框架集成的问题,但它并没有告诉我们:业务代码里那些琐碎的活——集合判空、日期格式化、对象属性拷贝、Excel导入导出——该用什么工具类来处理。这些“脏活累活”如果全靠手写,不仅浪费时间,还容易埋bug。比如用String.split()处理空字符串、用SimpleDateFormat在多线程环境下撸日期,都是经典翻车现场。

所以这篇博文,我把自己这些年实际在SpringBoot项目里用过、踩过坑、最后沉淀下来的工具类库清单整理出来。覆盖了集合操作、字符串处理、日期时间、对象拷贝、JSON序列化、Excel导入导出、文件解析等日常开发最高频的场景。每个库都给出真实的代码示例,不是那种文档里复制出来的demo,而是我在项目里实际这么写的写法。

这篇内容适合谁看?刚入行或者正在做毕业设计的同学,可以直接把这些工具类用进自己的项目里,代码质量立刻上一个台阶;工作了三五年但一直在重复造轮子的老手,也可以看看别人是怎么选型、怎么避坑的。我尽量把“为什么这样选”也讲清楚,而不是只丢给你一堆工具类的API。

2. 集合与通用工具:Guava 和 Apache Commons 到底怎么选

2.1 Guava 的不可变集合和 Multimap 是真香

Guava是Google开源的一套Java核心库,它在集合领域的功力可以说是“独步武林”。SpringBoot项目里引入Guava,最常见的使用场景有三个:不可变集合、Multimap、以及它的缓存模块。

先看不可变集合。业务代码里经常要定义一些常量List或者Map,如果直接new ArrayList<>()再往里add,任何一个地方都能往里面塞数据,很容易被无意修改。Guava的ImmutableList就解决了这个问题:

// 定义只读的白名单列表 private static final ImmutableList<String> WHITE_LIST = ImmutableList.of( "/api/login", "/api/register", "/api/captcha" ); // 尝试修改会抛出 UnsupportedOperationException WHITE_LIST.add("/api/admin");

这个写法在配置拦截器白名单、权限校验名单时非常实用。它的底层实现也是经过优化的,遍历效率比普通ArrayList并不会差。

再说Multimap。我们经常遇到一个场景:一个Key对应多个Value,比如一个用户可能有多个角色。如果用Map<String, List<String>>,每次put都要先判断key存不存在、不存在就新建一个List,代码写起来很啰嗦。Guava的ArrayListMultimap直接帮你把这个逻辑封装好了:

Multimap<String, String> userRoles = ArrayListMultimap.create(); userRoles.put("张三", "admin"); userRoles.put("张三", "operator"); userRoles.put("李四", "viewer"); // 直接获取张三的所有角色,不会出现NullPointerException Collection<String> roles = userRoles.get("张三");

Guava的缓存模块CacheBuilder也值得一说。本地缓存在很多场景下比Redis更合适——比如配置信息、数据字典这种更新频率极低但访问频率极高的数据。用Guava Cache可以设置过期时间、最大容量,还能在缓存失效时自动加载:

LoadingCache<String, DictVO> dictCache = CacheBuilder.newBuilder() .maximumSize(1000) .expireAfterWrite(30, TimeUnit.MINUTES) .build(new CacheLoader<String, DictVO>() { @Override public DictVO load(String key) { // 这里写从数据库查询字典的逻辑 return dictMapper.selectByCode(key); } }); // 业务代码里直接用,缓存没有就自动去load DictVO dict = dictCache.get("order_status");

2.2 Apache Commons Lang3 的字符串和对象操作

Apache Commons家族太庞大了,但在SpringBoot项目里,我实际用得最多的还是commons-lang3。这个库里面的StringUtils类,可以说是Java后端开发者的“瑞士军刀”。

最常用的几个方法:

// 判空:同时判断null、空字符串、纯空格 boolean blank = StringUtils.isBlank(str); boolean notBlank = StringUtils.isNotBlank(str); // 截取字符串 String sub = StringUtils.substring("hello-world", 0, 5); // 判断是否包含,忽略大小写 boolean contains = StringUtils.containsIgnoreCase("Hello World", "hello"); // 数组转字符串 String joined = StringUtils.join(new String[]{"a", "b", "c"}, ",");

为什么推荐用StringUtils.isBlank()而不是自己写str != null && !str.isEmpty()?因为isBlank把纯空格的情况也处理了。我之前就遇到过线上事故:前端传了个全是空格的名字过来,str.isEmpty()返回false,程序把空格当成了合法输入存进了数据库,最后查询的时候怎么都查不出来,排查了半天。

commons-lang3RandomStringUtils也经常用到,生成随机验证码、临时密码全靠它:

// 生成6位数字验证码 String code = RandomStringUtils.randomNumeric(6); // 生成32位字母数字混合的临时密码 String tempPwd = RandomStringUtils.randomAlphanumeric(32);

不过这里要提醒一个坑:RandomStringUtils生成的随机数是基于Random的,不适合用来生成安全令牌、密钥这类对安全性要求极高的数据,那种场景要用SecureRandom

2.3 Commons Collections4 和 BeanUtils 的实战用法

commons-collections4是对JDK集合框架的补充,它的CollectionUtils提供了很多集合判空和集合操作的方法。SpringBoot项目里的Controller层经常要判断前端传过来的List是否为空,用这个就很方便:

import org.apache.commons.collections4.CollectionUtils; // 判断集合是否为空(null或size为0都算空) if (CollectionUtils.isEmpty(list)) { // 参数校验不通过,直接return } // 集合并集、交集、差集 Collection<String> union = CollectionUtils.union(list1, list2); Collection<String> intersection = CollectionUtils.intersection(list1, list2); Collection<String> subtract = CollectionUtils.subtract(list1, list2);

这里我特别想说一下BeanUtils.copyProperties。Spring自带了一个,Apache Commons也有一个,两者方法签名一样,但行为有区别:Apache Commons的BeanUtils在拷贝时会自动做类型转换,性能较差,而且遇到不同类型还会悄悄忽略;Spring的BeanUtils则以纯反射方式拷贝,类型不一致直接抛异常。这个差异是面试高频题,也是实际开发中容易踩的坑。

我的建议是:项目里如果需要对象属性拷贝,优先用Spring的BeanUtils,因为它在类型安全上更严,而且Spring本身就在classpath里,不需要额外引入依赖。但如果是大量、高频的对象转换场景,比如DTO和Entity之间来回转换,推荐用后面要讲的MapStruct,性能碾压反射方案。

3. 国产神器 Hutool:一个库搞定日常开发

3.1 为什么我强烈推荐Hutool

如果说Guava算“精工细作”,那Hutool就是“大而全”的代表。Hutool是国内程序员开源的一个Java工具类库,它对JDK做了大量的封装,API设计非常贴合中文开发者的使用习惯。文档全部是中文,遇到问题直接看文档就能解决,不用像看英文JavaDoc那样费劲。

我早期对Hutool是有偏见的,觉得“大而全”就意味着“不精”,但实际用了之后发现,它在绝大多数场景下都够用,而且很好用。就说cn.hutool.core.util.IdUtil这个类,生成唯一ID再也不用自己去写UUID拼接的代码了:

// 生成不带横线的UUID String uuid = IdUtil.fastSimpleUUID(); // 生成带横线的UUID(默认) String uuid2 = IdUtil.fastUUID(); // 生成雪花算法ID(适合做数据库主键) long snowflakeId = IdUtil.getSnowflakeNextId();

雪花算法生成的ID是Long类型,在MySQL里用bigint做主键比用VARCHAR的UUID性能好不少,因为B+树对有序数字的插入更友好。以前团队里有人直接用UUID字符串做主键,数据量到千万级别后,索引膨胀得非常严重,后来改成雪花算法ID才解决问题。

3.2 Hutool 高频模块使用示例

Hutool的工具类覆盖了开发中的方方面面,我这里挑几个我项目里最常用的模块,直接上代码。

StrUtil是用来替代Apache Commons的StringUtils的:

import cn.hutool.core.util.StrUtil; // 格式化字符串,类似slf4j的占位符方式 String msg = StrUtil.format("用户{}登录成功,IP: {}", userName, ip); // 驼峰转下划线,配合MyBatis-Plus很好用 String columnName = StrUtil.toUnderlineCase("userName"); // 输出 user_name // 脱敏处理,比如手机号、身份证号 String masked = StrUtil.hide("13812345678", 3, 7); // 输出 138****5678

DateUtilLocalDateTimeUtil是处理日期时间的:

import cn.hutool.core.date.DateUtil; // 字符串转日期 DateTime dateTime = DateUtil.parse("2024-06-01 10:30:00"); // 日期格式化 String format = DateUtil.format(dateTime, "yyyy年MM月dd日"); // 计算两个日期之间的间隔 long betweenDays = DateUtil.between(date1, date2, DateUnit.DAY);

尤其要提一下Hutool对LocalDateTime的支持。JDK8之后官方推荐用LocalDateTime替代Date,但LocalDateTime的解析和格式化API用起来比较啰嗦。Hutool的LocalDateTimeUtil封装之后,代码简洁很多:

import cn.hutool.core.date.LocalDateTimeUtil; LocalDateTime dateTime = LocalDateTimeUtil.parse("2024-06-01 10:30:00", "yyyy-MM-dd HH:mm:ss"); String format = LocalDateTimeUtil.format(dateTime, "yyyy-MM-dd HH:mm:ss");

Http请求工具HttpUtil同样非常实用。我在项目里做第三方接口回调通知时,经常需要向外部系统发送HTTP请求,用Hutool的HttpUtil几行代码就搞定了:

import cn.hutool.http.HttpUtil; // 发送GET请求 String result = HttpUtil.get("https://api.example.com/order/123"); // 发送POST请求,传递JSON Map<String, Object> params = new HashMap<>(); params.put("orderId", "123"); String result2 = HttpUtil.post("https://api.example.com/notify", JSONUtil.toJsonStr(params));

3.3 使用Hutool的注意事项

Hutool虽然好用,但也有一些使用上的注意事项,我踩过几次坑,分享给大家。

第一,Hutool的BeanUtil.copyProperties性能一般。如果只是偶尔拷贝一两个对象没问题,但如果是在循环里高频调用,比如批量处理一万条数据,性能会明显下降。这种场景建议用MapStruct,或者写个静态方法手动set。

第二,Hutool的JSONUtil在做JSON序列化时,对泛型的处理有时候会出现类型擦除问题。比如把List<OrderDTO>从JSON字符串反序列化时,JSONUtil.toList()方法能正常工作,但如果是嵌套泛型Map<String, List<OrderDTO>>,它可能解析出来的还是JSONObject而不是OrderDTO。这种复杂场景我建议直接用Jackson的TypeReference

第三,Hutool版本更新比较快,有些API在旧版本里没有或者行为不同。建议直接使用最新稳定版,我项目中用的是5.8.x版本,API比较稳定。另外,如果项目已经引入了Guava,再引入Hutool确实会有一些功能重复,但实际使用下来两者并不冲突,Guava强在集合和缓存,Hutool强在工具类覆盖面和中文文档。

4. 代码简化类:Lombok 和 MapStruct 的实践

4.1 Lombok 在SpringBoot项目中的正确姿势

Lombok在SpringBoot项目里的普及率已经非常高了,它通过注解在编译期生成getter、setter、构造方法等样板代码,让实体类保持简洁。

我用它最频繁的注解就是@Data和@Builder:

import lombok.Data; import lombok.Builder; @Data @Builder public class UserDTO { private Long id; private String userName; private String email; private Integer status; }

有了@Data,getter、setter、toString、equals、hashCode全部自动生成。有了@Builder,创建对象时可以非常优雅地链式赋值:

UserDTO dto = UserDTO.builder() .id(1001L) .userName("张三") .email("zhangsan@example.com") .status(1) .build();

在SpringBoot项目中,还有几个高频组合是大家容易忽略的。@Slf4j可以直接在类中注入log对象:

@Slf4j @Service public class OrderService { public void createOrder(OrderDTO dto) { log.info("开始创建订单,订单号: {}", dto.getOrderNo()); // 业务逻辑... } }

没有@Slf4j的话,你要在每个类里手动写:

private static final Logger log = LoggerFactory.getLogger(OrderService.class);

写了一百遍之后真的会烦。@Slf4j直接解决。

4.2 MapStruct:对象属性拷贝的性能之王

MapStruct是一个编译期生成对象映射代码的库,它的性能比BeanUtils高一个数量级,因为它在编译期就生成了原生的setter调用代码,而不是靠运行时反射。

SpringBoot项目里引入MapStruct很简单,Maven依赖配置如下:

<dependency> <groupId>org.mapstruct</groupId> <artifactId>mapstruct</artifactId> <version>1.5.5.Final</version> </dependency>

然后在Maven的spring-boot-maven-plugin里加上lombok-mapstruct-binding,不然Project Lombok和MapStruct一起用的时候,Lombok生成的getter/setter在MapStruct编译期是看不到的,会报找不到属性的错误:

<plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-compiler-plugin</artifactId> <configuration> <annotationProcessorPaths> <path> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <version>1.18.30</version> </path> <path> <groupId>org.mapstruct</groupId> <artifactId>mapstruct-processor</artifactId> <version>1.5.5.Final</version> </path> </annotationProcessorPaths> </configuration> </plugin>

创建一个Mapper接口:

import org.mapstruct.Mapper; import org.mapstruct.Mapping; import org.mapstruct.factory.Mappers; @Mapper public interface UserMapper { UserMapper INSTANCE = Mappers.getMapper(UserMapper.class); @Mapping(source = "userName", target = "name") UserEntity dtoToEntity(UserDTO dto); }

当DTO的属性名和Entity不一致时,用@Mapping来指定对应关系。编译之后,MapStruct会生成一个实现类,里面的代码就是你手写的那种逐属性set:

// 这是编译后生成的代码(示意) public class UserMapperImpl implements UserMapper { @Override public UserEntity dtoToEntity(UserDTO dto) { UserEntity entity = new UserEntity(); entity.setName(dto.getUserName()); // ... return entity; } }

没有任何反射开销,性能可想而知。在批量处理数据、高频转换对象的场景下,MapStruct的优势非常明显。

5. JSON与文件处理:Jackson、EasyExcel、Tika

5.1 Jackson 的进阶玩法

SpringBoot默认的JSON序列化框架就是Jackson,所以只要项目里用了spring-boot-starter-web,Jackson就已经在classpath里了。但大部分人对Jackson的用法停留在ObjectMapper.writeValueAsString()的层面,其实它还有很多实用特性。

泛型反序列化要用TypeReference,这个必须要会:

import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.ObjectMapper; ObjectMapper objectMapper = new ObjectMapper(); // 反序列化成 List<OrderDTO> List<OrderDTO> orders = objectMapper.readValue(jsonStr, new TypeReference<List<OrderDTO>>() {}); // 反序列化成 Map<String, OrderDTO> Map<String, OrderDTO> orderMap = objectMapper.readValue(jsonStr, new TypeReference<Map<String, OrderDTO>>() {});

如果不用TypeReference,直接传List.class,得到的结果是List<LinkedHashMap>,后续你调用getOrderNo()方法时会报ClassCastException,这个坑我在实际项目里踩过不止一次。

Jackson处理null值的策略也值得配置一下。默认情况下,实体类里为null的字段在序列化成JSON时会被保留,即{"name": null}。如果你希望不输出null字段,可以配置:

ObjectMapper objectMapper = new ObjectMapper(); objectMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);

在SpringBoot里,可以直接通过配置文件全局生效:

spring.jackson.default-property-inclusion=non_null

不过要提醒一下,别把所有接口都设置成忽略null字段。有些前端需要根据接口返回的字段是否存在来判断某些逻辑,如果后台一律不返回null字段,可能导致前端拿到缺失字段而报错。这个要跟团队前端约定好再改。

5.2 EasyExcel:大文件导入导出的最佳选择

Excel导入导出是后台管理系统躲不掉的需求。早期大家都用Apache POI直接操作,写一个导出功能动不动两百行代码,而且大文件导出时内存直接爆掉。阿里巴巴开源的EasyExcel就是为解决这个问题而生的,它采用SAX模式解析Excel,内存占用极低。

SpringBoot项目里引入EasyExcel:

<dependency> <groupId>com.alibaba</groupId> <artifactId>easyexcel</artifactId> <version>3.3.4</version> </dependency>

定义一个和Excel表头对应的实体类:

import com.alibaba.excel.annotation.ExcelProperty; public class OrderExcelVO { @ExcelProperty("订单号") private String orderNo; @ExcelProperty("订单金额") private BigDecimal amount; @ExcelProperty("下单时间") private String createTime; }

导出数据到Excel文件:

List<OrderExcelVO> dataList = new ArrayList<>(); // 填充数据... String fileName = "/tmp/orders.xlsx"; EasyExcel.write(fileName, OrderExcelVO.class) .sheet("订单数据") .doWrite(dataList);

读取Excel文件也很简单:

EasyExcel.read(fileName, OrderExcelVO.class, new AnalysisEventListener<OrderExcelVO>() { @Override public void invoke(OrderExcelVO data, AnalysisContext context) { // 每解析到一行数据就会回调这个方法,在这里做逐行处理 System.out.println("解析到一条数据: " + data); } @Override public void doAfterAllAnalysed(AnalysisContext context) { // 所有数据解析完成后的回调 log.info("Excel解析完成"); } }).sheet().doRead();

这里要注意一个问题:invoke方法回调是同步的,默认是一个单元格一个单元格地解析,如果数据量大,处理逻辑尽量不要写在invoke里做耗时操作,先把数据收集到List,攒够一定数量再批量入库,效率会高很多。

5.3 Apache Tika:从各类文档中提取文本

Apache Tika是一个文件内容解析工具库,专门用来从PDF、Word、Excel、HTML、图片等各种格式中提取文本内容和元数据。这个库在搜索引擎、知识库建设、内容审核系统里用得比较多。

SpringBoot项目引入Tika:

<dependency> <groupId>org.apache.tika</groupId> <artifactId>tika-core</artifactId> <version>2.9.0</version> </dependency>

使用示例:

import org.apache.tika.Tika; Tika tika = new Tika(); // 从文件路径直接提取文本 String text = tika.parseToString(new File("/tmp/简历.pdf")); // 从字节数组提取文本(适合上传文件的场景) byte[] fileBytes = multipartFile.getBytes(); String content = tika.parseToString(new FileInputStream(""), new Metadata());

需要注意,Tika的parseToString方法依赖tika-parsers标准包才能解析各类格式,只用tika-core的话只能处理少量文本格式。完整使用需要引入tika-parsers-standard-package。另外,Tika处理超大PDF时耗时会比较长,建议异步处理或者限制文件大小。

6. 常见问题与排查技巧实录

6.1 工具类静态引入带来的问题

用好这些工具类库,还得注意一个非常隐蔽的坑:静态导入导致的代码可读性下降

有人为了少写几个字母,喜欢这样写:

import static cn.hutool.core.util.StrUtil.*; // 然后代码里直接用 isBlank() if (isBlank(str)) { // ... }

当项目里同时引入了Hutool和Apache Commons的StringUtils时,两个库都有isBlank()方法,静态导入会让IDE的自动导入变得混乱,看代码的人根本不知道这个isBlank()是哪个库的。这个在Code Review时被我实名反对过。我的建议是:保留类名的显式调用,比如写StrUtil.isBlank(str),看起来没省多少事,但代码的清晰度高了一整个档次。

6.2 版本冲突处理策略

SpringBoot项目引入多个工具类库后,最常见的麻烦是依赖冲突。之前出现过一次比较典型的问题:项目引了Hutool的5.8.x,又引了依赖Hutool 4.x的第三方SDK,Maven仲裁选了个4.x版本,导致SDK正常但项目里用到5.8.x新API的代码直接编译不过。

排查依赖冲突,我是用Maven插件查看依赖树:

mvn dependency:tree -Dincludes=cn.hutool

然后通过exclusion排除掉不需要的版本。这个操作一定要做,不然项目跑一段时间后莫名其妙出现NoSuchMethodError,非常折磨人。

6.3 自制工具类什么时候才合理

最后一点,也是我认为最重要的:工具类库已经这么丰富了,什么情况下才值得自己写工具类?

我的判断标准是:如果这个工具方法不是项目特有的业务逻辑,而是通用的技术类操作,先查一下Hutool和Apache Commons有没有现成的,如果有就别自己写。自己写的工具类往往存在三个问题:没有经过充分的边界测试、没有完整的文档说明、团队成员不知道有这个东西反而重复写了一遍。

但如果是和业务紧密绑定的工具方法,比如“订单号生成规则”“金额分转元并且去掉尾数0”这种,那一定要自己写,并且要把这类方法放到自己项目的utilssupport包里,加上清晰的注释和单元测试。工具类不是越少越好,而是越准越好。

我现在体会最深的一点:工具类库选型不是越多越好,也不是越少越好,而是要让团队里每个成员都知道“这个东西我们已经有了,不用重复造轮子”。建议在你的SpringBoot项目脚手架里,把这些常用依赖提前配置好,再在项目Wiki里建一个“常用工具类速查”文档,列清楚什么场景该用哪个库的哪个方法。后面进来的新同事照着用就行,不用再走一遍我踩过的坑。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询