ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

Java项目必备 commons-lang3 工具类与避坑实践

Java项目必备 commons-lang3 工具类与避坑实践 1. 为什么Java项目里几乎都躺着这个jar包做Java后端的同学应该都有过这种体验翻一个老项目的pom.xml或者翻开某个开源框架的依赖树总能在里面看到一个熟悉的身影——commons-lang3。它不像Spring那样名声在外也不像MyBatis那样有明确的功能边界但几乎每个稍具规模的Java工程都会顺手把它引进来。原因其实很朴素JDK自带的API在很多日常场景下并不好用而Apache Commons-Lang3正好把这些零碎的、高频的、容易写错的活儿全部打包好让开发者少写几百行样板代码也少踩一些空指针的坑。这篇文章面向的读者比较宽如果你刚开始接触Java Web开发想知道这个库到底能帮上什么忙哪些类值得先学如果你已经用了几年但平时只是零星地调过StringUtils.isEmpty()没系统梳理过它的全貌那这篇文章也能帮你把手里的工具用得更顺。我会按实际项目里的使用频率来组织内容从最常用的StringUtils开始一路讲ArrayUtils、DateUtils、NumberUtils、ObjectUtils、RandomStringUtils这些典型案例再结合我自己在项目里踩过的坑把版本兼容、依赖冲突、行为差异这些容易被忽略的细节也一并说清楚。commons-lang3本质上是Apache Jakarta Commons项目里Lang组件在Java 5之后的一次重写。老的commons-lang停留在Java 1.4时代包名是org.apache.commons.lang而lang3的包名变成了org.apache.commons.lang3全面拥抱了泛型、可变参数、枚举这些新特性。也正因为包名换了两个版本理论上可以在同一个工程里共存但实践中没人会闲着没事同时引两个除非你在做老系统迁移被迫过渡一段时间。理解这一点很关键因为很多网上的老博客还在写org.apache.commons.lang.StringUtils你照着抄进lang3工程里IDE直接就标红了不是代码错是包路径对不上。1.1 从JDK原生API的痛点说起抛开commons-lang3不谈先看看只用JDK会是什么体验。判断一个字符串是不是空原生写法是str null || str.length() 0如果还想把纯空格也算作空就得变成str null || str.trim().length() 0。这段代码本身没错但它会在项目里反复出现几十上百次地复制粘贴。更麻烦的是一旦某处漏掉了null判断线上就可能出一个NullPointerException而这种空指针往往藏在调用链深处定位起来非常烦。字符串拼接也是重灾区。JDK提供了String.format和StringBuilder前者写起来清爽但性能一般后者性能好但代码啰嗦。再比如数组判空原生得写arr null || arr.length 0两个数组是否相等得先判空再逐个比对还得处理长度不一致的情况数组转List、List转数组之间的边界条件总是容易写漏。这些操作单个看都不复杂但量一大代码的可读性和健壮性都会下降。commons-lang3的价值就在于它把这些高频操作封装成了语义明确、经过充分测试的静态方法让你在写业务逻辑的时候不用再分心去想这些边角情况。还有一个容易被忽略的点参数校验。很多工具方法内部会做防御性判断比如StringUtils.equals(null, abc)会直接返回false而不是抛异常。这种“不抛异常、静默给出合理结果”的设计风格虽然有人觉得不够严谨但在业务代码里确实能省掉大量前置判空。这也是为什么很多人用惯了StringUtils.equals之后就再也不愿意写a ! null a.equals(b)了。1.2 依赖引入与版本选择引入方式没什么难度Maven项目加一段依赖即可。写这篇文章时3.x系列已经迭代到比较靠后的版本了选择时要留意两点一是项目本身的Java版本二是依赖树里是否已经有别的组件传入了旧版本。老版本比如3.5之前某些方法的实现和现在有差异升级时容易出问题后面章节我会专门展开。dependency groupIdorg.apache.commons/groupId artifactIdcommons-lang3/artifactId version3.12.0/version /dependencyGradle用户对应写成implementation org.apache.commons:commons-lang3:3.12.0就行。实际选版本的时候我建议优先跟团队其他项目保持一致或者看Spring Boot的依赖管理里默认锁的是哪个版本——很多Spring Boot版本会自带对commons-lang3的版本管理你只要不显式写version它就会用父POM里定的那个这样能减少版本冲突。注意不要因为“想用新特性”就盲目升级到最新版尤其是老项目。Lang3在3.6、3.7、3.10这几个版本附近都调整过一些方法的内部行为比如StringUtils.isEmpty和isBlank的语义、数字转换的边界处理升级前最好跑一遍回归。依赖冲突是另一个常见话题。假设你的项目A依赖了组件BB又把commons-lang3打进了自己的fat jar同时你自己也显式引了一个不同版本最终classpath上哪个生效就取决于构建工具的仲裁策略。Maven默认走“最近优先”Gradle默认走“最高版本”。如果两个版本差异较大可能出现“本机跑得好好的打包上线就报NoSuchMethodError”这种情况。排查时用mvn dependency:tree或gradle dependencies把依赖树打出来搜一下commons-lang3看看是不是有多个版本被拉进来了这是解决这类问题的第一步。2. StringUtils一个类撑起一半的字符串处理需求如果commons-lang3里只允许你记住一个类那一定是StringUtils。它的方法数量非常多但真正常用的就那么二三十个把它们用熟日常开发里涉及字符串的分支判断、拼接、截取、转换基本就不缺工具了。这个类最大的特点是对null极度友好绝大多数方法传入null不会抛异常而是返回一个合理的结果。理解这个设计前提你在写业务代码时就能放心地少写很多判空。2.1 isEmpty、isBlank、isNoneBlank 到底差在哪这三个方法是我见过被问得最多的一组它们的区别说穿了很简单但用错场景就会出bug。isEmpty(CharSequence cs)cs null || cs.length() 0。注意它不看内容是不是空格 在它眼里是非空的。isBlank(CharSequence cs)cs null || cs.trim().length() 0。纯空格、制表符、换行符组成的字符串都会被判定为空。isNotBlank/isNotEmpty分别是上面两个的反义方法建议在需要“非空判断”时优先用这两个可读性比加!更好。isNoneBlank/isAnyBlank接收可变参数用来一次判断多个字符串适合校验多个必填字段。实际项目里最容易出事的就是把isEmpty当成isBlank用。比如表单提交的用户名前端可能传过来一个空格你用isEmpty判断觉得没问题存进数据库就变成了脏数据。反过来如果某个字段的业务语义就是“允许空格”那你用isBlank反而会误判。我的建议是只要是用户输入、文本内容一律用isBlank如果是程序内部生成的标识、编码明确不允许空串用isEmpty。提示StringUtils.isBlank(null)返回trueStringUtils.isBlank()返回trueStringUtils.isBlank( )返回true。这一点和很多人的直觉不同用之前最好先写个单元测试确认一遍。2.2 拼接、截取、补齐、替换的实战写法字符串拼接方面StringUtils.join和StringUtils.joinWith是高频方法。前者接收一个可迭代对象或数组用指定分隔符连接后者是后来加入的第一个参数就是分隔符用起来更直观。String joined StringUtils.join(Arrays.asList(a, b, c), ,); // 结果a,b,c String joined2 StringUtils.joinWith(-, 2024, 01, 15); // 结果2024-01-15join的好处是自动跳过null元素而不会拼接出a,null,c这种奇怪结果。当然如果你希望null以空串形式占位就得先自己处理。截取方面JDK的substring最让人头疼的就是越界StringUtils.substring(str, start, end)做了安全处理越界不会抛异常而是返回能取到的那部分。还有一个StringUtils.substringBetween和substringAfter、substringBefore用来从一段文本里抠出两个标记之间的内容解析简单的报文、提取URL参数时很省事。补齐padding是另一个实用但容易被忽略的功能。生成固定长度的编码、对齐日志输出时经常用到StringUtils.leftPad(7, 5, 0); // 00007 StringUtils.rightPad(abc, 6, *); // abc***替换方面replace、replaceEach、replaceEachRepeatedly各有用途。replaceEach支持一次替换多组目标比循环调用replace更清晰。注意replaceEach是按顺序执行的如果替换目标之间有重叠结果会受顺序影响这个细节在处理模板字符串时要特别小心。2.3 equals、contains、startsWith 的空指针免疫力这组方法的价值在于“两边都可能为null”的场景。原生写a.equals(b)只要a是null就炸写b.equals(a)只要b是null照样炸。StringUtils.equals(a, b)内部做了判空两个都是null返回true一个null一个非null返回false两个都非null才真正调用equals。contains、startsWith、endsWith、indexOf同理传入null都会安全返回false或者-1。这一点在处理外部接口返回的数据时非常有用你不需要在每一个调用点前面加if判断。if (StringUtils.startsWith(orderNo, ORD)) { // 安全处理orderNo为null也不会NPE }还有一个进阶用法是StringUtils.equalsAny和equalsAnyIgnoreCase用来判断一个字符串是否等于给定的多个候选值之一写枚举匹配、状态判断时比一串||清爽得多。3. 别只盯着StringUtils这几个工具类同样值钱很多人对commons-lang3的印象就停留在StringUtils上其实这套库里还有一批同样能打的工具类只是使用频率相对低一些导致被忽视。把它们用起来你会发现手写工具方法的场合又少了一大截。3.1 ArrayUtils数组判空与增删改数组在Java里是定长的原生操作起来边界条件特别多。ArrayUtils覆盖了常见的判空、包含、追加、删除、截取、翻转、转包装类型等操作。最常用的还是isEmpty和isNotEmpty可以同时处理null和长度为0两种情况。int[] nums {1, 2, 3}; ArrayUtils.isEmpty(nums); // false ArrayUtils.contains(nums, 2); // true int[] added ArrayUtils.add(nums, 4); // [1, 2, 3, 4] int[] removed ArrayUtils.remove(nums, 1); // [1, 3]需要说明的是add和remove返回的是新数组不会修改原数组这符合Java里数组不可变的直觉。另外toObject和toPrimitive用来在int[]和Integer[]之间转换对接泛型集合时很有用省得自己写循环。还有一个ArrayUtils.nullToEmpty把null数组转成空数组避免后续遍历时空指针。注意ArrayUtils.add对于基本类型数组和包装类型数组的行为略有差异包装类型数组里可以塞入null元素基本类型不行。混用时留意一下编译器的类型推断。3.2 DateUtils日期加减与截断DateUtils围绕java.util.Date和Calendar做了一层封装虽然现在新项目大多用java.time了但维护老系统时它依然天天见。常用方法有addDays、addMonths、addYears、truncate、isSameDay、parseDate。Date now new Date(); Date tomorrow DateUtils.addDays(now, 1); Date dayStart DateUtils.truncate(now, Calendar.DAY_OF_MONTH); boolean same DateUtils.isSameDay(now, tomorrow);truncate这个操作很值得单独说。它会把时间“截断”到指定的精度比如截到天那么时分秒毫秒全部归零。做按天统计、判断是否是同一天时比手写Calendar.set方便太多。isSameDay看起来简单但如果两个日期跨时区实现里会考虑Calendar的时区设置需要留意。需要强调的是commons-lang3里的日期工具类在3.0之后做了不少调整一些方法被标记为deprecated转而推荐用java.time。如果你的项目已经全面迁移到LocalDate、LocalDateTime那DateUtils的用武之地就大幅减少了但完全弃用它还为时尚早毕竟很多老代码、老框架还在用Date。3.3 NumberUtils字符串转数字的安全姿势Integer.parseInt遇到非数字会直接抛NumberFormatException而NumberUtils.toInt(str, defaultValue)在解析失败时返回你给的默认值不会抛异常。这在处理外部配置、请求参数时非常实用。int port NumberUtils.toInt(configValue, 8080); long id NumberUtils.toLong(request.getParameter(id), -1L);还有NumberUtils.isCreatable用来判断一个字符串能不能转成数字注意在旧版本里这个方法叫isNumber3.5之后改名的升级时要留意。NumberUtils.max、min接收可变参数一次比较多个数字返回极值比手写循环简洁。此外createNumber能从字符串里推断出合适的Number类型处理“可能是整数也可能是小数”的输入时很方便但返回值是Number后续要自己转具体类型。3.4 ObjectUtils 与 SystemUtils判空与运行环境识别ObjectUtils里最常用的是isEmpty和defaultIfNull。前者对数组、集合、Map、字符串、Optional做了统一判空后者在对象为null时返回默认值。String name ObjectUtils.defaultIfNull(input, unknown); boolean empty ObjectUtils.isEmpty(collection);SystemUtils则是用来读取运行环境信息的比如IS_OS_WINDOWS、IS_JAVA_8、JAVA_VERSION、USER_DIR这些常量。写跨平台脚本、做环境判断时不用自己读系统属性再拼字符串。3.5 RandomStringUtils生成验证码、随机串生成随机字符串是很多场景的刚需比如验证码、临时文件名、测试数据。RandomStringUtils.randomAlphanumeric(6)一行就能生成6位字母数字混合串randomNumeric(6)生成纯数字random(10, true, false)可以指定是否包含字母、数字。String code RandomStringUtils.randomNumeric(6); String token RandomStringUtils.randomAlphanumeric(32);注意RandomStringUtils底层用的是java.util.Random不是安全随机数。如果你的场景涉及安全敏感比如会话标识、密码重置令牌不要用它改用SecureRandom。这个坑我见过不止一次有人拿它生成密码重置链接的token这就是典型的误用。4. 真实项目里踩过的坑与排查实操工具库用得多了坑自然也踩得多。下面这些场景都是我在实际项目或同事代码里真真切切遇到过的整理出来希望能帮你少走弯路。4.1 常见问题速查表现象可能原因排查与解决编译报红提示找不到StringUtils引的是lang3但代码写的是lang的包路径把org.apache.commons.lang改为org.apache.commons.lang3NoSuchMethodErrorclasspath上存在多个版本的lang3用依赖树命令确认版本统一为同一版本isBlank结果不符合预期混淆了isEmpty和isBlank的语义明确业务语义用户输入一律用isBlank数字转换抛异常直接用了parseInt而非toInt换成NumberUtils.toInt(str, default)数组操作后原数组变了误以为add/remove是原地修改记住它们返回新数组需要接收返回值日期加减结果跨时区错乱服务器时区与预期不一致检查Calendar时区设置统一用UTC做内部计算4.2 版本升级带来的行为差异Lang3在3.6到3.7之间调整过一次StringUtils里部分方法的实现具体来说是一些join相关方法对null的处理、以及NumberUtils.isNumber到isCreatable的改名。这些改动单看都不大但如果你的项目里有大量单元测试升级后跑一遍就能发现哪里的行为对不上。还有一次影响比较大的是StringUtils.repeat在超长重复次数下的内存行为调整之前某些版本会因为参数过大直接申请巨额内存后来做了保护。这类改动官方发布说明里通常有写升级前把版本之间的Release Notes扫一遍是个好习惯比事后救火强。我自己的经验是升级这类基础工具库不要和业务功能开发混在同一次发布里。单独拉一个分支只做依赖升级跑完整回归确认没问题再合并。这样万一出问题回滚的范围很小。4.3 依赖冲突与包名陷阱包名陷阱在迁移老项目时很常见。有些老代码是commons-lang时代的包路径是org.apache.commons.lang.StringUtils迁移到lang3时如果只改了pom.xml没改import编译就会报错。更隐蔽的情况是两个包同时存在于classpath因为全限定名不同编译器不会报错但运行时你调用的可能压根不是你以为的那个类。这种情况通常出现在项目里既引了老库、又通过某个框架间接引了新库的时候。排查思路很直接在IDE里按住Ctrl点进StringUtils看它到底跳到了哪个包。如果是你预期的lang3那就没问题如果跳到了lang说明有个旧依赖悄悄影响了你的代码得去依赖树里把它排除掉。提示用mvn dependency:tree -Dincludescommons-lang*可以快速定位是哪个依赖把老版本拉进来的然后在该依赖上加exclusions把它剔掉。5. 性能、选型与团队规范最后聊聊我在团队里推行这个库时的一些思考。工具库本身没有对错关键是用得对不对、团队里有没有共识。性能方面StringUtils的大部分方法是静态工具方法内部实现经过优化日常业务场景下不会是瓶颈。但有两个点值得注意一是join在处理超大集合时会构建StringBuilder本身是线性复杂度没什么问题二是StringUtils里某些带正则的方法比如split、replacePattern会编译Pattern如果在高频循环里调用建议自己在外面把Pattern缓存起来。我在一次压测里就遇到过循环里调StringUtils.split导致CPU偏高的案例后来改成先编译一次Pattern复用性能立刻下来了。选型上新项目如果已经全面采用java.time和现代的字符串处理习惯commons-lang3的很多功能可以被JDK自身或者其他轻量库替代。但现实是绝大多数Java项目多少都会间接依赖到它与其抗拒不如把它用好。团队内部最好明确几条约定比如“判空统一用StringUtils.isBlank”“字符串比较统一用StringUtils.equals”“外部参数转数字统一用NumberUtils.toXxx带默认值”把这些约定写进代码规范新人入职照着做就行能省下很多review时的口水。至于未来commons-lang3本身已经相当成熟接口稳定不太会有颠覆性变化。把它当成项目基础设施的一部分按需使用、按版本管理就够了。真正需要警惕的不是这个库本身而是那种“为了用而用”的倾向——明明一行JDK代码就能解决非要绕道调工具方法那才是舍近求远。
返回列表