还在写冗余模板代码?Apache Commons Lang 让 Java 开发少写 50% 代码,告别 NPE 与重复劳动
做 Java 开发时,你是不是总被“原生 API 的反人类设计”搞得崩溃?
- 字符串判空要叠三层条件:
if (str != null && !str.isEmpty() && !str.trim().isEmpty()),少一层就可能抛 NPE; - 异常处理要手动循环:
Throwable root = e; while (root.getCause() != null) { root = root.getCause(); },还得手动拼接堆栈; - 对象比较要写一堆
if-else:if (a == null) return b == null; return a.equals(b);,字段多了根本维护不过来; - 日期增减要算毫秒:
new Date(date.getTime() + 24*60*60*1000),忽略时区和闰年,容易出错; - 反射获取类信息要写 try-catch:
Class<?> clazz = Class.forName("com.example.User");,代码又长又丑。
其实 Apache Commons Lang 早就把这些“重复劳动”全包了。它是 Java 生态的“基础设施级工具库”,扩展了 java.lang 包的核心能力,用一行代码搞定字符串处理、异常处理、对象操作等高频场景,直接砍掉一半冗余代码,还能从根源上避免空指针和逻辑错误。今天就扒一扒它的 5 个核心用法,让你从“模板代码的泥潭”里解脱出来,开发效率翻倍。
各个知识整理的思维导图
一、先搞懂:Commons Lang 为什么是 Java 开发的“标配”?
Apache Commons Lang 是 Apache 基金会的经典工具库(GitHub 星标 1.5k+,下载量超 10 亿次),专为解决 Java 原生 API 的 5 大痛点而生:
- null 安全:所有工具方法都能接收
null输入,不会抛 NPE(如StringUtils.isEmpty(null)返回true,而非报错); - 简化模板代码:把
equals比较、哈希计算、堆栈拼接等重复工作封装成一行 API; - 线程安全:日期处理等工具类(如
DateUtils)避免了SimpleDateFormat的线程安全问题; - 无侵入:无需修改原有类(如无需加注解),直接通过静态方法调用,开箱即用;
- 兼容性强:支持 Java 8+,与 Spring、MyBatis 等框架无缝集成,几乎所有 Java 项目都能直接用。
和 Java 原生 API 对比,效率天差地别:
| 场景 | 原生写法(代码量) | Commons Lang 写法(代码量) | 核心优势 |
|---|---|---|---|
| 字符串判空(含空格) | 3 行(`null | “” | |
| 获取完整异常堆栈 | 6 行(StringWriter+PrintWriter) | 1 行(ExceptionUtils.getStackTrace(e)) | 无需手动流操作 |
| 对象安全比较 | 4 行(null判断+equals) | 1 行(ObjectUtils.equals(a,b)) | 自动处理 null,无 NPE |
| 日期加 1 天 | 3 行(计算毫秒+new Date) | 1 行(DateUtils.addDays(date,1)) | 无需手动算毫秒,兼容时区 |
| 反射获取类信息 | 3 行(try-catch+forName) | 1 行(ClassUtils.getName(clazz)) | 简化异常处理,支持 null |
简单说:Commons Lang 把 Java 开发中“重复、易错、繁琐”的模板代码,变成了“一行调用”的工具方法,你不用再关心“null 怎么处理”“异常怎么拼接”“日期怎么计算”,专注业务逻辑即可。
二、5 个核心用法:从“冗余模板”到“一行搞定”
按“使用频率”排序,每个用法都附“原生写法 vs Commons Lang 写法”对比,搭配实际业务场景,学完就能用。
1. StringUtils:字符串处理的“瑞士军刀”(最常用)
核心场景:字符串判空、拼接、分割、替换、截取(所有操作均 null 安全)。
示例:判断用户输入是否为空、拼接多个字符串(含 null)
传统写法(约 5 行代码,冗余且易抛 NPE):
public class StringHandleTraditional {
public static void main(String[] args) {
String username = " "; // 用户输入的空格
String nickname = null; // 可能为 null
String suffix = "先生";
// 1. 判空(含 null、空字符串、纯空格)
boolean isUsernameValid = false;
if (username != null) {
String trimmed = username.trim();
isUsernameValid = !trimmed.isEmpty();
}
// 2. 拼接字符串(避免 null 被拼为"null")
String fullName = "";
if (nickname != null) {
fullName = nickname + suffix;
} else {
fullName = "未知" + suffix;
}
System.out.println("用户名是否有效:" + isUsernameValid); // 输出:false
System.out.println("完整昵称:" + fullName); // 输出:未知先生
}
}
问题:判空需要多层嵌套,拼接需要手动处理 null,代码冗余;若遗漏 null 判断,直接调用 username.trim() 会抛 NPE。
Commons Lang 写法(约 2 行代码,null 安全):
import org.apache.commons.lang3.StringUtils;
public class StringHandleCommonsLang {
public static void main(String[] args) {
String username = " ";
String nickname = null;
String suffix = "先生";
// 1. 一行判空(isBlank 包含 null、""、纯空格)
boolean isUsernameValid = StringUtils.isNotBlank(username);
// 2. 一行拼接(自动忽略 null,用默认值替代)
String fullName = StringUtils.defaultIfBlank(nickname, "未知") + suffix;
System.out.println("用户名是否有效:" + isUsernameValid); // 输出:false
System.out.println("完整昵称:" + fullName); // 输出:未知先生
}
}
扩展用法(高频):
- 安全截取:
StringUtils.substring("abcdef", 2, 4)→"cd"(null 输入返回 null,不抛异常); - 移除前缀/后缀:
StringUtils.removeStart("file://test.txt", "file://")→"/test.txt"; - 重复字符串:
StringUtils.repeat("ab", 3)→"ababab"; - 分割保留空元素:
StringUtils.splitPreserveAllTokens("a,,b", ",")→["a", "", "b"](原生 split 会忽略空元素)。
业务场景:校验用户注册表单
public boolean validateRegisterForm(String username, String phone) {
// 用户名非空且长度 2-20,手机号非空且匹配正则
return StringUtils.isNotBlank(username) && username.length() >= 2 && username.length() <= 20
&& StringUtils.isNotBlank(phone) && StringUtils.match(phone, "^1[3-9]\\d{9}$");
}
2. ExceptionUtils:异常处理的“效率工具”
核心场景:获取完整堆栈、定位根异常、检查异常链类型(避免手动循环 getCause)。
示例:记录异常堆栈、找到嵌套异常的根因
传统写法(约 8 行代码,循环+流操作):
import java.io.PrintWriter;
import java.io.StringWriter;
public class ExceptionHandleTraditional {
public static void main(String[] args) {
try {
// 模拟嵌套异常:业务异常→SQL异常→IO异常
throw new BusinessException("保存失败",
new SQLException("数据库连接超时",
new IOException("网络中断")));
} catch (Exception e) {
// 1. 手动获取完整堆栈
StringWriter sw = new StringWriter();
PrintWriter pw = new PrintWriter(sw);
e.printStackTrace(pw);
String stackTrace = sw.toString();
// 2. 手动定位根异常
Throwable rootCause = e;
while (rootCause.getCause() != null) {
rootCause = rootCause.getCause();
}
System.out.println("堆栈信息:" + stackTrace);
System.out.println("根异常:" + rootCause.getMessage()); // 输出:网络中断
}
}
static class BusinessException extends RuntimeException {
public BusinessException(String message, Throwable cause) {
super(message, cause);
}
}
}
问题:获取堆栈需要创建流对象,定位根异常需要手动循环,代码冗余且易因循环引用陷入死循环。
Commons Lang 写法(约 2 行代码,一键搞定):
import org.apache.commons.lang3.exception.ExceptionUtils;
public class ExceptionHandleCommonsLang {
public static void main(String[] args) {
try {
throw new BusinessException("保存失败",
new SQLException("数据库连接超时",
new IOException("网络中断")));
} catch (Exception e) {
// 1. 一行获取完整堆栈
String stackTrace = ExceptionUtils.getStackTrace(e);
// 2. 一行定位根异常(自动处理循环引用)
Throwable rootCause = ExceptionUtils.getRootCause(e);
// 3. 一行检查异常链是否包含 IOExceptions
boolean hasIoException = ExceptionUtils.containsThrowable(e, IOException.class);
System.out.println("堆栈信息:" + stackTrace);
System.out.println("根异常:" + rootCause.getMessage()); // 输出:网络中断
System.out.println("是否含 IO 异常:" + hasIoException); // 输出:true
}
}
static class BusinessException extends RuntimeException {
public BusinessException(String message, Throwable cause) {
super(message, cause);
}
}
}
业务场景:全局异常处理器
@ControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(Exception.class)
public ResponseEntity<ErrorResponse> handleException(Exception e) {
// 拼接完整异常消息链(外层→中层→根因)
String errorMsg = ExceptionUtils.getRootCauseMessage(e);
// 记录完整堆栈
log.error("系统异常:{}", ExceptionUtils.getStackTrace(e));
return ResponseEntity.status(500)
.body(new ErrorResponse(500, "操作失败:" + errorMsg));
}
}
3. ObjectUtils:对象操作的“安全卫士”
核心场景:对象判空、安全比较、设置默认值、计算哈希值(避免 NPE)。
示例:比较两个对象、为 null 对象设置默认值
传统写法(约 6 行代码,多层 null 判断):
public class ObjectHandleTraditional {
public static void main(String[] args) {
Integer a = null;
Integer b = 100;
User user1 = new User(1L, "张三");
User user2 = null;
// 1. 安全比较两个对象(避免 NPE)
boolean isEqual;
if (a == null && b == null) {
isEqual = true;
} else if (a == null || b == null) {
isEqual = false;
} else {
isEqual = a.equals(b);
}
// 2. 为 null 对象设置默认值
User defaultUser = user2 != null ? user2 : new User(0L, "默认用户");
System.out.println("a 和 b 是否相等:" + isEqual); // 输出:false
System.out.println("默认用户:" + defaultUser.getName()); // 输出:默认用户
}
static class User {
private Long id;
private String name;
// 构造器、getter 省略
}
}
问题:对象比较需要多层 null 判断,代码冗余;若直接调用 a.equals(b),当 a 为 null 时抛 NPE。
Commons Lang 写法(约 2 行代码,自动处理 null):
import org.apache.commons.lang3.ObjectUtils;
public class ObjectHandleCommonsLang {
public static void main(String[] args) {
Integer a = null;
Integer b = 100;
User user1 = new User(1L, "张三");
User user2 = null;
// 1. 一行安全比较(null 视为小于非 null)
boolean isEqual = ObjectUtils.equals(a, b);
// 2. 一行设置默认值(null 时返回默认对象)
User defaultUser = ObjectUtils.defaultIfNull(user2, new User(0L, "默认用户"));
// 3. 一行计算哈希值(null 返 0)
int hashCode = ObjectUtils.hashCode(user1);
System.out.println("a 和 b 是否相等:" + isEqual); // 输出:false
System.out.println("默认用户:" + defaultUser.getName()); // 输出:默认用户
System.out.println("user1 哈希值:" + hashCode); // 输出:非 0 整数
}
static class User {
private Long id;
private String name;
// 构造器、getter 省略
}
}
业务场景:处理查询结果默认值
public User getUserById(Long id) {
// 数据库查询可能返回 null,用默认用户替代
User user = userMapper.selectById(id);
return ObjectUtils.defaultIfNull(user, new User(0L, "匿名用户"));
}
4. DateUtils:日期处理的“简化工具”
核心场景:日期增减、格式化、解析、比较(替代线程不安全的 SimpleDateFormat)。
示例:日期加 3 天、解析多格式日期字符串
传统写法(约 8 行代码,try-catch+毫秒计算):
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
public class DateHandleTraditional {
public static void main(String[] args) {
Date now = new Date();
String dateStr = "2023年10月01日";
// 1. 日期加 3 天(手动计算毫秒,易出错)
long threeDays = 3 * 24 * 60 * 60 * 1000L;
Date nextThreeDays = new Date(now.getTime() + threeDays);
// 2. 解析日期字符串(需指定格式,处理 ParseException)
SimpleDateFormat sdf = new SimpleDateFormat("yyyy年MM月dd日");
Date parsedDate = null;
try {
parsedDate = sdf.parse(dateStr);
} catch (ParseException e) {
e.printStackTrace();
}
System.out.println("三天后:" + sdf.format(nextThreeDays));
System.out.println("解析后的日期:" + parsedDate);
}
}
问题:日期增减需手动计算毫秒(忽略时区、闰年),解析需指定格式(不支持多格式),SimpleDateFormat 线程不安全(多线程下会错乱)。
Commons Lang 写法(约 2 行代码,自动适配):
import org.apache.commons.lang3.time.DateUtils;
import java.util.Date;
public class DateHandleCommonsLang {
public static void main(String[] args) {
Date now = new Date();
String dateStr = "2023年10月01日";
// 1. 一行日期加 3 天(自动处理时区、闰年)
Date nextThreeDays = DateUtils.addDays(now, 3);
// 2. 一行解析多格式日期(自动匹配格式,无需 try-catch)
String[] patterns = {"yyyy-MM-dd", "yyyy/MM/dd", "yyyy年MM月dd日"};
Date parsedDate = DateUtils.parseDate(dateStr, patterns);
// 3. 一行比较是否同一天
boolean isSameDay = DateUtils.isSameDay(parsedDate, nextThreeDays);
System.out.println("三天后:" + DateUtils.format(nextThreeDays, "yyyy年MM月dd日"));
System.out.println("解析后的日期:" + parsedDate);
System.out.println("是否同一天:" + isSameDay); // 输出:false
}
}
扩展用法:
- 日期截断:
DateUtils.truncate(now, Calendar.DAY_OF_MONTH)→ 清除时分秒,保留日期; - 日期差计算:
DateUtils.getFragmentInDays(nextThreeDays, now)→ 计算两个日期的天数差; - 线程安全格式化:
DateUtils.format(date, "yyyy-MM-dd")→ 替代SimpleDateFormat。
业务场景:计算订单有效期
public Date getOrderExpireTime(Date createTime) {
// 订单创建后 7 天过期
return DateUtils.addDays(createTime, 7);
}
// 校验订单是否过期
public boolean isOrderExpired(Date createTime) {
Date expireTime = getOrderExpireTime(createTime);
return DateUtils.isAfter(new Date(), expireTime); // 当前时间是否在过期时间之后
}
5. ClassUtils:反射与类操作的“辅助工具”
核心场景:获取类信息、检查类关系、加载类(简化反射代码,避免繁琐 try-catch)。
示例:获取类名、检查类是否为子类、加载类
传统写法(约 6 行代码,try-catch+手动判断):
public class ClassHandleTraditional {
public static void main(String[] args) {
Class<?> userClass = User.class;
String className = "com.example.User";
// 1. 获取类的全限定名(原生 API 无简化方法)
String fullClassName = userClass.getName();
// 2. 检查 Integer 是否为 Number 的子类
boolean isSubclass = Number.class.isAssignableFrom(Integer.class);
// 3. 加载类(需处理 ClassNotFoundException)
Class<?> loadedClass = null;
try {
loadedClass = Class.forName(className);
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
System.out.println("类名:" + fullClassName); // 输出:com.example.User
System.out.println("是否子类:" + isSubclass); // 输出:true
System.out.println("加载的类:" + loadedClass);
}
static class User {}
}
问题:加载类需要手动处理 ClassNotFoundException,检查类关系的方法名晦涩(isAssignableFrom 不易理解)。
Commons Lang 写法(约 2 行代码,直观简洁):
import org.apache.commons.lang3.ClassUtils;
public class ClassHandleCommonsLang {
public static void main(String[] args) {
Class<?> userClass = User.class;
String className = "com.example.User";
// 1. 一行获取类名(支持 null,返回"null")
String fullClassName = ClassUtils.getName(userClass);
String nullClassName = ClassUtils.getName(null); // 输出:"null"
// 2. 一行检查是否为子类(方法名直观)
boolean isSubclass = ClassUtils.isAssignable(Integer.class, Number.class);
// 3. 一行加载类(无需手动 try-catch,抛 unchecked 异常)
Class<?> loadedClass = ClassUtils.getClass(className);
// 4. 一行获取所有父类和接口
Iterable<Class<?>> allTypes = ClassUtils.getAllSuperclassesAndInterfaces(userClass);
System.out.println("类名:" + fullClassName); // 输出:com.example.User
System.out.println("是否子类:" + isSubclass); // 输出:true
System.out.println("加载的类:" + loadedClass);
}
static class User {}
}
业务场景:反射实例化对象
public Object createInstance(String className) {
try {
// 加载类并实例化(无参构造)
Class<?> clazz = ClassUtils.getClass(className);
return clazz.getDeclaredConstructor().newInstance();
} catch (Exception e) {
log.error("实例化对象失败:{}", ExceptionUtils.getRootCauseMessage(e));
return null;
}
}
三、避坑指南:这 5 个细节 90% 的人会踩
Commons Lang 虽简单,但新手容易因“误解方法逻辑”踩坑,整理实战中最常遇到的问题:
1. StringUtils.isBlank() vs isEmpty():别用混!
isEmpty(str):仅判断str == null或str.equals("")(" "视为非空);isBlank(str):判断str == null、str.equals("")或str.trim().equals("")(" "视为空)。
错误示例:用户输入空格视为“未填写”,却误用 isEmpty:
String userInput = " ";
if (StringUtils.isEmpty(userInput)) {
System.out.println("未填写"); // 不执行(isEmpty 认为" "非空)
}
正确做法:用户输入、文本内容判空优先用 isBlank。
2. DateUtils 解析多格式时,注意格式顺序
DateUtils.parseDate(str, patterns) 按数组顺序匹配格式,优先级:前面的格式先匹配。
错误示例:先放 yyyy/MM/dd,再放 yyyy-MM-dd,导致 2023-10-01 匹配失败(被误认为 yyyy/MM/dd):
String[] patterns = {"yyyy/MM/dd", "yyyy-MM-dd"};
DateUtils.parseDate("2023-10-01", patterns); // 抛 ParseException
正确做法:将“更具体”或“更常用”的格式放在前面。
3. ExceptionUtils.getStackTrace(e) 堆栈可能很长,注意日志大小
生产环境中,频繁打印超长堆栈会占用大量磁盘空间,建议:
- 非关键异常(如用户输入错误)仅打印消息,不打印完整堆栈;
- 关键异常(如数据库连接失败)打印堆栈,但可通过
StringUtils.abbreviate截断:String stackTrace = StringUtils.abbreviate(ExceptionUtils.getStackTrace(e), 2000); // 截断为 2000 字符
4. ObjectUtils.defaultIfNull 注意默认值类型匹配
默认值类型需与目标类型一致,否则可能触发类型转换异常:
错误示例:Integer a = null; Integer b = ObjectUtils.defaultIfNull(a, "0");(默认值是 String,目标类型是 Integer);
正确做法:默认值类型与目标类型一致:Integer b = ObjectUtils.defaultIfNull(a, 0);。
5. ClassUtils.getClass() 加载类时,依赖类路径
ClassUtils.getClass(className) 用当前线程的类加载器加载类,若类不在类路径(如第三方 jar 未引入),会抛 ClassNotFoundException(unchecked 异常),需提前确保依赖齐全。
四、和其他工具对比:为什么 Commons Lang 是“标配”?
Java 生态中还有 Guava、Hutool 等工具库,Commons Lang 的核心优势在哪?
| 工具 | 核心优势 | 劣势 | 适用场景 |
|---|---|---|---|
| Apache Commons Lang | 兼容性强(Java 8+)、生态成熟、无依赖、Spring 默认集成 | 功能较基础(无复杂集合运算) | 所有 Java 项目(尤其是传统项目、Spring 项目) |
| Guava | 功能全面(集合、缓存、并发)、API 优雅 | 依赖较大(JAR 包 2MB+)、学习成本高 | 复杂业务场景(如分布式缓存、高并发) |
| Hutool | 中文文档友好、功能聚合(含 AI、Excel 等) | 生态较新、部分场景稳定性待验证 | 国内项目、快速开发场景 |
结论:
- 传统项目/ Spring 项目:优先用 Commons Lang(无依赖、无缝集成);
- 复杂业务场景:用 Guava 补充(如集合运算、缓存);
- 国内快速开发:用 Hutool(中文文档、功能全面)。
总结
Apache Commons Lang 的核心价值,是用“简洁、安全、无侵入”的工具方法,解决了 Java 原生 API 的 5 大痛点:
- 字符串处理:
StringUtils一行判空、拼接,无 NPE; - 异常处理:
ExceptionUtils一键获取堆栈、定位根因; - 对象操作:
ObjectUtils安全比较、设置默认值; - 日期处理:
DateUtils简化增减、解析,线程安全; - 类操作:
ClassUtils简化反射、类信息获取。
核心用法记住这 5 句口诀:
- 字符串用
StringUtils,判空优先isBlank; - 异常用
ExceptionUtils,堆栈根因一键拿; - 对象用
ObjectUtils,比较默认不 NPE; - 日期用
DateUtils,增减解析多格式; - 类操作
ClassUtils,反射加载少 try-catch。
下次写模板代码时,别再手动判空、循环拼接了——Commons Lang 早就把这些工作做成了“开箱即用的 API”,让你少写代码,少踩坑。
需要我针对“Commons Lang 在 Spring Boot 中的实战配置(如全局日期格式化、参数校验)”补充更详细的案例吗?
更多推荐


所有评论(0)