尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

Spring Boot内置20个工具类,简化业务代码重复劳动

Spring Boot内置20个工具类,简化业务代码重复劳动 说实话刚入行那两年我写业务代码的时候最烦的一件事就是判断空值要自己写、拷贝属性要自己写、做计时统计还要自己写。后来翻Spring Boot的依赖一看好家伙框架自己就带了一整套工具类分散在spring-core、spring-web这些模块里只是很少有人系统地捋过。这个系列的初衷就是把Spring Boot内置的这20个工具类拿出来逐个过一遍看看它们到底能帮我们省掉多少重复劳动。这些工具类不引入任何额外依赖全部来自Spring Framework本身在Spring Boot项目里开箱即用。它们解决的是日常开发里最琐碎的那部分判空、断言、反射、拷贝、IO、路径匹配、计时统计。适合谁看两类人最合适一类是写业务代码时老觉得“缺个工具类”、随手就想引Hutool或Guava的另一类是刚接触Spring Boot不久想搞清楚框架底层常用组件到底是干嘛的。这篇文章不会涉及太高深原理就以实际用法为主线把每个类的典型场景和坑点讲透。1. 先理清楚这些工具类到底藏在哪里1.1 工具类分布在Spring的哪些模块Spring Boot本身并不是一堆独立的类它底层是Spring Framework那套东西。工具类主要散落在spring-core、spring-beans、spring-web、spring-context这几个模块里。你只要创建的是Spring Boot项目这些jar包就一定在classpath里所以工具类属于“天然存在、随取随用”的状态。很多人没意识到这一点是因为日常写代码很少会直接点开jar包去看。我最早接触Spring的断言类Assert是在写Controller参数校验时遇到一个NullPointerException才去翻的源码。后来发现不光是Assert字符串处理、对象转换、反射封装、流拷贝Spring全都提供了一套自己的实现。与其说这是一篇工具类盘点不如说是一次“查漏补缺”看看哪些功能你用第三方库写了半天其实Spring两行就搞定了。1.2 20个工具类的完整清单速查在拆细节之前先把清单列出来。下面的表格不只是罗列类名我还标注了每个类所在的模块和使用场景方便你对照项目实际去查漏。序号工具类所属模块主要用途1Assertspring-core方法参数断言为“不合法参数”快速抛出异常2StringUtilsspring-core字符串判空、截取、拆分、大小写转换3ObjectUtilsspring-core对象判空、数组工具、null安全比较4NumberUtilsspring-core数字类型转换、格式化5CollectionUtilsspring-core集合判空、集合交并差、数组转集合6BeanUtilsspring-beansJavaBean属性读写、属性拷贝7ReflectionUtilsspring-core反射操作封装处理异常和被忽略的坑8ClassUtilsspring-core类加载、判断代理类、获取包名类名9AopUtilsspring-aop判断代理类型、获取目标对象10AopContextspring-aop获取当前AOP代理对象11ResourceUtilsspring-coreclasspath与文件系统资源定位12FileCopyUtilsspring-core文件与流之间的复制操作13StreamUtilsspring-coreJava 8 Stream相关操作补充14SerializationUtilsspring-coreJava序列化与深拷贝15StopWatchspring-core代码耗时统计多任务计时16AntPathMatcherspring-coreAnt风格路径匹配常用于URL匹配17UrlPathHelperspring-webURL路径解析与解码18UriComponentsBuilderspring-web构建和编码URI替代手动拼接19ConcurrentReferenceHashMapspring-core高性能并发Map弱引用key20WebUtilsspring-webWeb场景通用工具参数获取与Cookie处理提示以上工具的模块归属你直接看spring-core里就能找到一大半。Spring 5.3版本起CollectionUtils重新回归spring-core使用前注意自己的版本号。2. 最常用的几个断言、字符串、对象与集合操作2.1 Assert把if throw缩成一行写过接口的人都知道参数校验最朴素的写法是这样的if (userId null) { throw new IllegalArgumentException(userId不能为空); }这个模式每天都写但Spring直接给了更紧凑的写法Assert.notNull(userId, userId不能为空); Assert.hasText(username, 用户名不能为空); Assert.isTrue(age 18, 未成年用户禁止注册);Assert内部做的事情本质上就是判断条件不满足时抛异常只是把样板代码全部省掉了。常用的还有Assert.notEmpty、Assert.isAssignable基本覆盖了判空、布尔判断、类型判断几个大类。它抛出的异常是IllegalArgumentException在全局异常处理器里统一接住非常顺手。我个人的经验是如果项目里已经有javax.validation那套注解那业务方法内部的非空校验用Assert做补充两者不冲突。注解负责入口参数Assert负责流程内部的中间结果比如某个Service里从Redis取出来发现过期失效、但又不想让它继续往下走的场景直接Assert一下最干净。2.2 StringUtils比你自己写的判空靠谱Spring的StringUtils和Apache Commons那个StringUtils长得非常像但它是独立的实现不需要额外引入依赖。最常用的是几个静态方法StringUtils.isEmpty(str); // 只判断 null 和 StringUtils.hasLength(str); // 和isEmpty相反null、 都算false StringUtils.hasText(str); // 会忽略空白字符全空格也判为false StringUtils.trimWhitespace(str); StringUtils.commaDelimitedListToStringArray(str);第一个坑就隐藏在isEmpty和hasText的差别里isEmpty把 三个空格判定为非空但hasText会把这些纯空白内容判为空。实际开发里用户输入框传来的东西哪怕是空格也是有意义的脏数据所以字符串“是不是有实际内容”的判断我习惯用hasText而不是isEmpty。StringUtils.split和StringUtils.tokenizeToStringArray是把字符串按分隔符拆成数组或List的常用方法字符集处理上比直接调用split要稳妥一些。唯一要提醒的是Spring的StringUtils没有类似Apache Commons里那些随机字符串生成、首字母大写之类的方法。如果在某个场景里找不到想要的功能先看看是不是找错了类。2.3 ObjectUtils与NumberUtils细节里的空安全ObjectUtils主要处理对象层面的判断很多方法对比用ObjectUtils.nullSafeEquals而不是直接用equals这样可以避免两边都为null时出问题。数组判断也有涉及比如ObjectUtils.isEmpty可以同时判断对象、字符串、数组、Collection、Map是否为空比起各自去判断省不少事ObjectUtils.isEmpty(userList) ObjectUtils.isEmpty(new String[]{a}) ObjectUtils.isEmpty(map)NumberUtils的应用场景主要是数字转换。比如前端传过来一个字符串“00123”想要把它转换成整数直接用Integer.valueOf会解析成123但如果想要更严格的规则NumberUtils提供了基础的转换帮助。它还有NumberUtils.parseNumber可以把字符串转换成指定数字类型。说实话这个类的使用频率不如前面的ObjectUtils高因为大部分数据到达Java层已经是数字类型了但在做配置项解析时它能派上用场。2.4 CollectionUtils集合判空与常见算法Spring 5.3以后spring-core里带了一个专门的CollectionUtils。核心方法有CollectionUtils.isEmpty(list); CollectionUtils.containsAny(collection, elements); CollectionUtils.hasUniqueObject(collection); CollectionUtils.arrayToList(array);重点是Spring这个CollectionUtils不能和Apache Commons Collections里的CollectionUtils搞混两个类的API差异很大。Spring这个主要强调判断和转换不提供什么花哨的集合框架算法。使用时看import确认是org.springframework.util.CollectionUtils。判断空集合这个比自定义的“list ! null list.size() 0”要简洁得多代码可读性直接提升一个档次。3. 反射、Bean属性与AOP框架底层的抽象3.1 BeanUtils属性拷贝的正确打开方式BeanUtils.copyProperties可能是这20个工具类里最出名的了因为它在实体类转换场景里实在太好用。用法UserDTO dto new UserDTO(); BeanUtils.copyProperties(userEntity, dto);要注意的是两个参数的顺序第一个是源对象第二个是目标对象。写反了就是目标对象属性拷贝到源对象Bug找半天都发现不了。它内部通过反射实现get和set方法需要符合JavaBean规范字段名和类型相同的自动复制。如果两个类属性名不一样或者某些字段不想被拷贝就得手动set。性能方面如果拷贝量巨大、每秒调用上千次反射的开销是客观存在的。但这种场景在常规业务里很少遇到。真遇到了可以考虑MapStruct这类编译期映射方案或者直接手写setter。如果只是简单DTO转换BeanUtils.copyProperties是性价比之王。3.2 ReflectionUtils让反射代码少掉一半自己写反射代码最恼火的是处理一大堆受检异常以及父类私有字段访问时容易踩坑。Spring的ReflectionUtils把这些都封装好了ReflectionUtils.findField(User.class, age); ReflectionUtils.makeAccessible(field); ReflectionUtils.setField(field, user, 30); ReflectionUtils.invokeMethod(method, target, args);它还有个很有用的方法doWithFields可以遍历一个类及其父类的所有字段做统一处理。我拿它做过一个日志打印工具把实体类的每个字段名和值拼成字符串当时不懂的时候自己写循环遍历getDeclaredFields遇到继承体系的字段直接傻眼后来才发现ReflectionUtils.doWithFields正是为这种场景设计的。使用ReflectionUtils时要注意Field、Method这个东西不能跨线程共享每次调用都新获取一个不要自己缓存Field实例否则并发环境下可能出现不可预知的问题。3.3 ClassUtils、AopUtils与AopContextClassUtils常被用来判断类的基本信息ClassUtils.getShortName(className); ClassUtils.isAssignable(Class, Class); ClassUtils.isCglibProxy(object);在Spring里很多类其实是CGLIB代理出来的子类直接通过getName可能拿到一堆像“UserService$$EnhancerBySpringCGLIB”的名字用ClassUtils.getShortName能把真实类名提取得干净一些。AopUtils和AopContext主要在写AOP逻辑时使用。AopUtils.isProxy判断当前对象是不是代理对象AopUtils.getTargetClass返回被代理的目标类AopContext.currentProxy则能在被代理对象内部获取当前代理解决自调用导致AOP注解失效的问题。自调用的问题值得展开说一句类内部方法用this调用aop切面会失效因为this是原始对象而不是代理。想强制走代理逻辑可以用AopContext.currentProxy拿到代理对象再调用。前提是启动类设置过exposeProxytrue否则拿不到。4. 资源、IO与流处理别再用自己写的循环拷贝了4.1 FileCopyUtils与StreamUtils一行代码完成复制文件上传、文件下载、临时文件落盘这些场景有个共同点要把InputStream的数据转成字节数组或写入文件。新手最容易写成while循环自己读然后还要处理finally里的close。Spring的FileCopyUtils这里是真省事byte[] bytes FileCopyUtils.copyToByteArray(inputStream); FileCopyUtils.copy(inputStream, outputStream); FileCopyUtils.copy(byteArray, file);FileCopyUtils内部会处理流的关闭和异常包装。需要注意它复制的是二进制数据对文本内容做替换、编码转换的情况就别用它了。StreamUtils主要针对Java 8 Stream做辅助操作。最常见的场景是这样StreamUtils.drain(stream); StreamUtils.copy(String, Charset, OutputStream);它还有一个有意思的方法StreamUtils.copyToString可以把整个InputStream按字符集读成字符串读取Jar包内容或网络流时很顺手。功能不大但胜在干净。4.2 ResourceUtils定位classpath资源的捷径ResourceUtils.getClasspathResource是读取resources目录下文件的常用入口Resource resource ResourceUtils.getURLResource(classpath:application.yml);需要注意的是ResourceUtils并不是所有“classpath:*”的通配场景都能处理*号通配是PathMatchingResourcePatternResolver的活。如果想要扫描一个包下所有class文件、所有文件推荐用PathMatchingResourcePatternResolver它是Spring扫描包的核心。ResourceUtils解决的是“我知道路径帮我拿流”的问题。4.3 SerializationUtils深拷贝的隐藏好手Java对象的深拷贝传统方式是实现Serializable接口然后自己写ObjectOutputStream、ObjectInputStream。SerializationUtils把这段流程简化了User copy SerializationUtils.clone(original); byte[] data SerializationUtils.serialize(original); User obj (User) SerializationUtils.deserialize(data);这个类的限制很明显被拷贝的对象必须实现java.io.Serializable接口否则运行时报错。并且基于序列化的深拷贝和直接new对象相比性能差一个数量级。所以它的适用场景是对象不复杂、拷贝频率低、但结构里嵌套层级多手写clone方法要到崩溃的那种情况。它还有一个好处字节数组序列化结果可以用来做缓存内容的临时存储比如往Redis里塞对象前自己先序列化然后反序列化回来避免依赖Redis的序列化器行为不一致造成问题。5. Web与路径处理URL匹配、路径解析和URI构建5.1 AntPathMatcher路径规则的运行时匹配Spring MVC的路由匹配默认就是基于AntPathMatcher实现的。它处理的是通配符匹配AntPathMatcher matcher new AntPathMatcher(); matcher.match(/api/*/user, /api/v1/user); // true matcher.match(/api/**/user, /api/v1/v2/user); // true matcher.match(/api/{id}/user, /api/123/user); // true*代表一层路径**代表多层任意路径{id}是路径变量。有时候业务里要判断一个请求路径是否符合某个权限模板这个类就能派上用场。比如配置了一批“白名单URL”判断当前请求是否在白名单内手写正则容易出错直接用AntPathMatcher清晰又灵活。有一点要注意AntPathMatcher线程安全可以声明为静态常量复用。同时它默认是大小写敏感的如果不想敏感需要自己扩展重写或者提前把路径toLowerCase再比对。5.2 UrlPathHelper与UriComponentsBuilderUrlPathHelper是Spring MVC内部解析请求路径的工具。它可以拿到去除上下文路径和ServletPath之后的真正路径、解码URI、处理连续斜杠等。UricComponentsBuilder则是在代码里拼接URL地址时避免手写字符串的好帮手URI uri UriComponentsBuilder.fromPath(/api/user) .queryParam(name, 张三) .queryParam(page, 1) .build() .encode() .toUri();它的核心价值有两个一是参数自动做URL编码中文、特殊符号不会乱二是结构化拼装不会出现“多个参数之间有没有加”这种低级问题。在调用第三方支付或消息推送接口、需要动态拼接回调地址的场景我几乎都用它来做。6. 实战串联一个注册接口把工具类用起来6.1 场景设定文字拆了一堆最好还是来看一个完整的小例子。我拿“用户注册”这个经典场景把前面提到的工具类串起来入参校验用Assert参数清洗用StringUtilsDTO转换用BeanUtils耗时统计用StopWatch异常兜底用全局处理器。流程是注册接口收到请求后校验用户名密码合法性清洗字符串空格检查用户是否已存在然后把DTO转成实体落库最后记录处理耗时。6.2 参数校验层Assert一步到位public void register(RegisterDTO dto) { Assert.hasText(dto.getUsername(), 用户名不能为空); Assert.hasText(dto.getPassword(), 密码不能为空); Assert.isTrue(dto.getPassword().length() 6, 密码长度至少6位); }这一步的关键是把“非空判断 异常抛出”压缩成一行可读性高出了错也马上知道卡在哪条规则上。有人会觉得断言写在Controller层更好但我的习惯是写在Service层这样即使后续有其他调用方绕过ControllerService的校验也兜得住。6.3 参数清洗与长度控制String username StringUtils.trimWhitespace(dto.getUsername()); username StringUtils.truncate(username, 30);StringUtils.truncate是按字符长度截断的适合对输入超长内容做截断处理。trimWhitespace和trimLeadingCharacter配合用可以针对用户输入的收尾空格、Windows换行符做一次清洁。这里是少写很多“replaceAll”的典型位置因为replaceAll需要正则表达式StringUtils.truncate则直接按字数截断。6.4 属性拷贝与自动填充User user new User(); BeanUtils.copyProperties(dto, user); user.setCreateTime(LocalDateTime.now()); user.setStatus(PENDING);这里的坑在文章前面提过一遍再强调一次copyProperties第一个参数是源、第二个参数是目标两个类同名字段才会自动复制。如果中间有个字段空空如也先检查是不是两个类字段名对不上。6.5 计时统计StopWatch与多任务计时StopWatch stopWatch new StopWatch(注册接口耗时); stopWatch.start(参数校验); Thread.sleep(50); stopWatch.stop(); stopWatch.start(业务落库); Thread.sleep(120); stopWatch.stop(); log.info(stopWatch.prettyPrint());StopWatch最爽的地方是它可以记录多个任务prettyPrint直接输出一个好看的表格区分每个步骤耗时。平时排查N1查询、慢接口我就在怀疑的代码段前后套上start和stop根本不需要往Redis或数据库写耗时计数本地日志一打就行。注意StopWatch是线程不安全的局部变量使用别做成共享成员否则多线程一跑计时就乱了。6.6 完整代码和优化方向Service public class UserService { public void register(RegisterDTO dto) { Assert.hasText(dto.getUsername(), 用户名不能为空); Assert.hasText(dto.getPassword(), 密码不能为空); Assert.isTrue(dto.getPassword().length() 6, 密码长度至少6位); // 清洗用户名 String username StringUtils.trimWhitespace(dto.getUsername()); username StringUtils.truncate(username, 30); // 检查用户是否存在 if (userMapper.selectByUsername(username) ! null) { throw new BusinessException(用户名已存在); } User user new User(); BeanUtils.copyProperties(dto, user); user.setUsername(username); user.setCreateTime(LocalDateTime.now()); user.setStatus(PENDING); userMapper.insert(user); } }这里还能优化的点用户名查重和密码加密最好在同一个事务里密码加密用BCrypt而不是MD5。这是业务层面的要求不是工具类本身的问题。工具类解决的是流程中“怎么写更清爽”的问题但业务规则还是要业务代码自己控制。7. 常见坑点与实用性避坑指南7.1 Assert的异常类型与全局异常处理Assert抛的是IllegalArgumentException。如果全局异常处理器只捕获了BusinessException或RuntimeException那IllegalArgumentException可能直接变成500而不是400。解决方案是全局异常处理器里显式加上IllegalArgumentException的处理返回400或约定的错误码。我见过太多团队接入了Assert却忘了在异常处理器里接住它结果参数校验失败变成了服务器内部错误前端拿着500一脸懵。7.2 别把Spring StringUtils和Apache Commons搞混这是最容易踩的导入坑。spring-core里的StringUtils在org.springframework.util包下Apache Commons Lang3的StringUtils在org.apache.commons.lang3包下。两者都叫StringUtils但方法差异很大。比如isBlank只有Apache那个有Spring的没有。我见过代码里import写错然后调了isBlank编译期还好好的等运行时报了没这方法的错才回头排查。一个判断技巧写代码时看到方法名或者用法第一反应先看import。如果你本来只想判个空用Spring的就够了如果要用到字符串生成、填充、缩写那些高级功能直接引commons-lang3不要硬在Spring里找。7.3 BeanUtils.copyProperties的深、浅拷贝陷阱BeanUtils.copyProperties执行的是浅拷贝如果属性是个引用类型两个对象的这个属性是指向同一个对象的。改变这个引用对象的内部值源对象也会变。这种问题在“实体转DTODTO再转VO”这种链路里很容易出现尤其是嵌套了List、Map等复杂结构时一定要意识到拷贝只是“最外层的浅复制”。其次性能极端敏感场景下反射拷贝的开销可以被MapStruct直接拉开几倍的差距。如果做过压测就会发现转换耗时排名靠前的往往是反射拷贝。但在大多数请求量级下这根本引发不了问题属于理论坑大于实际坑。7.4 CollectionUtils版本差异一览Spring 5.3之前spring-core里的CollectionUtils是删掉的后来5.3又加了回来但API和最早的版本也有微调。语义旧写法5.3推荐判断空Listlist null || list.isEmpty()CollectionUtils.isEmpty(list)判断空Mapmap null || map.isEmpty()CollectionUtils.isEmpty(map)集合转数组list.toArray()CollectionUtils.toArray(list)找唯一元素自己循环CollectionUtils.hasUniqueObject(c)如果你还在用Spring Boot 2.3之类的老版本并想使用CollectionUtils建议先看一眼实际的Spring版本究竟是5.2还是5.3否则代码写好了上线编译不过那是真尴尬。7.5 StopWatch多任务计时的注意事项StopWatch的start和stop必须成对出现如果你在代码里start了但某次异常直接return了stop就没有执行。下次再start时会报“Cant start StopWatch: its already running”。我一开始不熟悉在try里start、catch里手动stop后来更干脆的做法是每段代码独立新一个StopWatch实例要统计哪段就是哪段不用想着复用同一个虽然多new几次但排错时精神负担小得多。还有StopWatch的prettyPrint在同时跑多个任务时会输出一种固定格式的表格。如果你对这些日志做了采集和监控注意一下日志格式和采集字段的匹配不要指望去正则抠出数值结果格式稍微一变动就挂了。7.6 使用内置工具类的总体原则最后聊一点整体的体会。Spring内置工具类虽然好用但没必要为了用而用。我的选择标准是一个任务如果自己手写不超过三行、且逻辑很直白比如就判断一个对象是不是null那直接用原生写法也行如果要写好几层判断、异常处理、循环那就优先看看Spring有没有现成的再不行才考虑引入第三方库。这套判断标准下来项目里第三方工具依赖量大幅下降对代码的掌控力反而更强了。还有个小技巧平时闲着没事直接去Spring源码里翻org.springframework.util包把里面的类一个个看过去比翻任何文档都直观。源码注释就是最好的指南。看到某个类的方法顺手搜索一下谁在用就知道框架是如何调用它的学习效率比记API高得多。
返回列表