Apache Commons BeanUtils 快速入门与集成示例
Commons BeanUtils 1.11.0 快速入门:五个可运行示例覆盖嵌套属性读写、对象拷贝类型转换、自定义日期转换器、DynaBean 与 Map 互转,并对比 Spring 与 MapStruct 属性拷贝方案,双系统实测通过。
Apache Commons BeanUtils 快速入门与集成示例
目标:以五个可直接运行的示例走完 Apache Commons BeanUtils 1.11.0 的核心能力——嵌套属性读写、对象拷贝与自动类型转换、自定义日期转换器、DynaBean 动态 Bean、Map 与 Bean 互转,并给出属性拷贝方案选型对比,已在 Ubuntu 22.04 与 CentOS Linux 8 实测通过。
一、环境信息
本篇所有源码与回显均在以下环境实测采集:
- 操作系统:Ubuntu 22.04.5 LTS(x86_64)为主,CentOS Linux 8 全量复跑验证
- JDK:OpenJDK 17.0.20.1(Ubuntu)/ OpenJDK 17.0.1(CentOS 8)
- 构建工具:Apache Maven 3.6.3(Ubuntu)/ Apache Maven 3.10.0(CentOS 8)
- 组件版本:Apache Commons BeanUtils 1.11.0(含传递依赖 commons-logging 1.3.5、commons-collections 3.2.2)
- 实验工程:
/opt/lab/beanutils/proj,执行用户 root
示例共用同一个领域模型 Model.java(三层嵌套 Employee → Dept → Manager),先给出全文,五个示例都基于它:
package com.example.beanutils;
import java.util.Date;
/**
* 演示用领域模型:三层嵌套 Employee -> Dept -> Manager。
* 全部字段走标准 getter/setter,供 Commons BeanUtils 反射访问。
*/
public class Model {
public static class Manager {
private String name;
private String title;
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public String getTitle() { return title; }
public void setTitle(String title) { this.title = title; }
}
public static class Dept {
private String deptName;
private Manager manager = new Manager();
public String getDeptName() { return deptName; }
public void setDeptName(String deptName) { this.deptName = deptName; }
public Manager getManager() { return manager; }
public void setManager(Manager manager) { this.manager = manager; }
}
public static class Employee {
private String empName;
private Integer age;
private Date onboardDate;
private Dept dept = new Dept();
public String getEmpName() { return empName; }
public void setEmpName(String empName) { this.empName = empName; }
public Integer getAge() { return age; }
public void setAge(Integer age) { this.age = age; }
public Date getOnboardDate() { return onboardDate; }
public void setOnboardDate(Date onboardDate) { this.onboardDate = onboardDate; }
public Dept getDept() { return dept; }
public void setDept(Dept dept) { this.dept = dept; }
}
}
二、依赖引入与核心组件
pom.xml 关键段(完整版见安装指南):
<dependencies>
<dependency>
<groupId>commons-beanutils</groupId>
<artifactId>commons-beanutils</artifactId>
<version>1.11.0</version>
</dependency>
</dependencies>
核查依赖树:
# mvn dependency:tree 2>&1 | sed -n '/maven-dependency-plugin/,/BUILD SUCCESS/p' | head -9
[INFO] --- maven-dependency-plugin:2.8:tree (default-cli) @ beanutils-lab ---
[INFO] com.example:beanutils-lab:jar:1.0.0
[INFO] \- commons-beanutils:commons-beanutils:jar:1.11.0:compile
[INFO] +- commons-logging:commons-logging:jar:1.3.5:compile
[INFO] \- commons-collections:commons-collections:jar:3.2.2:compile
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
四个核心组件分工:
PropertyUtils:按属性表达式读写,不做类型转换,类型不匹配直接抛异常。BeanUtils:在 PropertyUtils 之上叠加自动类型转换,提供 copyProperties、describe、populate 等门面方法。ConvertUtilsBean:转换器注册表,决定 String 到 Date 等复杂类型的转换规则。DynaClass/DynaBean:无需预写 Java 类的动态 Bean 结构。
运行任一示例前先保证 UTF-8 locale(POSIX 默认下中文会变问号):
# export LANG=C.UTF-8
三、示例一:嵌套属性读写(PropertyUtils)
NestedDemo.java:
package com.example.beanutils;
import org.apache.commons.beanutils.PropertyUtils;
/** 示例一:PropertyUtils 跨层级嵌套属性读写 */
public class NestedDemo {
public static void main(String[] args) throws Exception {
Model.Employee emp = new Model.Employee();
emp.setEmpName("张三");
emp.setAge(30);
// 读嵌套属性:dept 为空字段时返回 null 而不抛异常
String deptName = (String) PropertyUtils.getProperty(emp, "dept.deptName");
String mgrName = (String) PropertyUtils.getProperty(emp, "dept.manager.name");
System.out.println("读 dept.deptName = " + deptName);
System.out.println("读 dept.manager.name = " + mgrName);
// 写嵌套属性:逐层定位目标对象后调用对应 setter
PropertyUtils.setProperty(emp, "dept.deptName", "平台架构部");
PropertyUtils.setProperty(emp, "dept.manager.name", "李四");
PropertyUtils.setProperty(emp, "dept.manager.title", "高级经理");
System.out.println("写后读 dept.deptName = " + PropertyUtils.getProperty(emp, "dept.deptName"));
System.out.println("写后读 dept.manager.title = " + PropertyUtils.getProperty(emp, "dept.manager.title"));
System.out.println("整对象核对 = " + emp.getDept().getManager().getName() + " @ " + emp.getDept().getDeptName());
}
}
运行:
# mvn -q compile exec:java -Dexec.mainClass=com.example.beanutils.NestedDemo
读 dept.deptName = null
读 dept.manager.name = null
写后读 dept.deptName = 平台架构部
写后读 dept.manager.title = 高级经理
整对象核对 = 李四 @ 平台架构部
要点:表达式 a.b.c 由 NESTED_DELIM(.)切分层级;读未赋值路径得到 null;写路径要求中间节点有实例(本例 Employee.dept、Dept.manager 在字段声明处初始化)。
四、示例二:对象拷贝与自动类型转换(BeanUtils.copyProperties)
在展开示例前,先看这类需求的选型全貌:
💡 技术拓展:Java 里做「属性拷贝」有多条路线,差异集中在转换能力与性能上,选型时对照下表:
| 方案 | 实现机制 | 类型转换 | 性能 | 额外依赖 |
|---|---|---|---|---|
| Apache Commons BeanUtils | 运行时反射 | 内置 ConvertUtils 自动转换 | 一般 | commons-beanutils |
| Spring BeanUtils | 运行时反射 | 不转换,仅同名同类型 | 较快 | spring-beans |
| MapStruct | 编译期生成代码 | 编译期显式映射 | 接近手写 | 注解处理器 |
| 手写 getter/setter | 手工编码 | 手工处理 | 最优 | 无 |
本篇采用 Apache Commons BeanUtils:表单字符串到实体数值/日期的自动转换是它独有的开箱能力。
CopyDemo.java:
package com.example.beanutils;
import org.apache.commons.beanutils.BeanUtils;
/** 示例二:BeanUtils.copyProperties 对象拷贝与 String -> Integer 自动类型转换 */
public class CopyDemo {
/** 模拟 Web 表单:所有输入都是字符串 */
public static class Form {
private String empName;
private String age;
public String getEmpName() { return empName; }
public void setEmpName(String empName) { this.empName = empName; }
public String getAge() { return age; }
public void setAge(String age) { this.age = age; }
}
public static void main(String[] args) throws Exception {
Model.Employee target = new Model.Employee();
Form source = new Form();
source.setEmpName("王五");
source.setAge("28");
// 注意:Apache 版参数顺序是 (dest, orig),与 Spring BeanUtils 相反
BeanUtils.copyProperties(target, source);
System.out.println("target.empName = " + target.getEmpName());
System.out.println("target.age = " + target.getAge() + " (类型 " + target.getAge().getClass().getName() + ")");
System.out.println("target.dept 未被拷贝,保持默认 = " + target.getDept().getDeptName());
}
}
运行:
# mvn -q compile exec:java -Dexec.mainClass=com.example.beanutils.CopyDemo
target.empName = 王五
target.age = 28 (类型 java.lang.Integer)
target.dept 未被拷贝,保持默认 = null
要点:"28" 被内置转换器转为 Integer 28;来源没有的属性(dept)不会动目标的值;参数顺序是 (dest, orig),与 Spring 相反,混用两套库时务必核对签名。
五、示例三:自定义日期转换器(BeanUtilsBean + DateConverter)
ConverterDemo.java:
package com.example.beanutils;
import java.text.SimpleDateFormat;
import java.util.Date;
import org.apache.commons.beanutils.BeanUtilsBean;
import org.apache.commons.beanutils.ConvertUtilsBean;
import org.apache.commons.beanutils.PropertyUtilsBean;
import org.apache.commons.beanutils.converters.DateConverter;
/** 示例三:BeanUtilsBean 注册自定义 DateConverter 完成字符串转日期 */
public class ConverterDemo {
/** 模拟表单:日期同样以字符串提交 */
public static class Form {
private String empName;
private String onboardDate;
public String getEmpName() { return empName; }
public void setEmpName(String empName) { this.empName = empName; }
public String getOnboardDate() { return onboardDate; }
public void setOnboardDate(String onboardDate) { this.onboardDate = onboardDate; }
}
public static void main(String[] args) throws Exception {
// 注册 "yyyy-MM-dd" 格式的日期转换器
ConvertUtilsBean convertUtils = new ConvertUtilsBean();
DateConverter dateConverter = new DateConverter();
dateConverter.setPattern("yyyy-MM-dd");
dateConverter.setUseLocaleFormat(true);
convertUtils.register(dateConverter, Date.class);
BeanUtilsBean beanUtils = new BeanUtilsBean(convertUtils, new PropertyUtilsBean());
Form source = new Form();
source.setEmpName("赵六");
source.setOnboardDate("2026-10-13");
Model.Employee target = new Model.Employee();
beanUtils.copyProperties(target, source);
System.out.println("target.empName = " + target.getEmpName());
System.out.println("target.onboardDate = " + new SimpleDateFormat("yyyy-MM-dd").format(target.getOnboardDate()));
System.out.println("target.onboardDate 类型 = " + target.getOnboardDate().getClass().getName());
// 对照:未注册日期格式转换器的默认 BeanUtilsBean 能否转换该字符串
try {
Model.Employee plain = new Model.Employee();
new BeanUtilsBean().copyProperties(plain, source);
if (plain.getOnboardDate() == null) {
System.out.println("默认转换器未转换,onboardDate 仍为 null");
} else {
System.out.println("默认转换器转换结果 = "
+ new SimpleDateFormat("yyyy-MM-dd").format(plain.getOnboardDate()));
}
} catch (Exception e) {
System.out.println("未注册转换器时抛出 " + e.getClass().getSimpleName() + ": " + e.getMessage());
}
}
}
运行(Ubuntu 22.04 实测):
# mvn -q compile exec:java -Dexec.mainClass=com.example.beanutils.ConverterDemo
target.empName = 赵六
target.onboardDate = 2026-10-13
target.onboardDate 类型 = java.util.Date
未注册转换器时抛出 ConversionException: DateConverter does not support default String to 'Date' conversion.
CentOS 8 复跑时对照段会先经 java.util.logging 打出两行 WARNING: DateConverter does not support default String to 'Date' conversion. 再抛异常,两系统最终行为一致。
要点:自定义转换器必须走自建 BeanUtilsBean 实例才生效;未注册时默认抛 ConversionException,这就是日期字段必须显式配转换器的原因。
六、示例四:DynaBean 动态 Bean
DynaBeanDemo.java:
package com.example.beanutils;
import org.apache.commons.beanutils.BasicDynaClass;
import org.apache.commons.beanutils.DynaBean;
import org.apache.commons.beanutils.DynaProperty;
import org.apache.commons.beanutils.LazyDynaBean;
/** 示例四:DynaClass/DynaBean 动态创建与访问 Bean */
public class DynaBeanDemo {
public static void main(String[] args) throws Exception {
// 1. 声明动态属性结构
DynaProperty[] props = new DynaProperty[] {
new DynaProperty("name", String.class),
new DynaProperty("age", Integer.class),
new DynaProperty("tags", String[].class)
};
BasicDynaClass dynaClass = new BasicDynaClass("userDyna", null, props);
// 2. 按结构实例化并读写
DynaBean user = dynaClass.newInstance();
user.set("name", "小七");
user.set("age", 26);
user.set("tags", new String[] {"java", "中间件"});
System.out.println("name = " + user.get("name"));
System.out.println("age = " + user.get("age") + " (类型 " + user.get("age").getClass().getName() + ")");
System.out.println("tags[1] = " + ((String[]) user.get("tags"))[1]);
System.out.println("DynaBean 实现类 = " + user.getClass().getName());
// 3. LazyDynaBean:不预定义结构,随赋值自动扩展
DynaBean lazy = new LazyDynaBean();
lazy.set("remark", "无需预定义属性即可赋值");
lazy.set("level", 3);
System.out.println("lazy.remark = " + lazy.get("remark"));
System.out.println("lazy.level = " + lazy.get("level") + " (类型 " + lazy.get("level").getClass().getName() + ")");
}
}
运行:
# mvn -q compile exec:java -Dexec.mainClass=com.example.beanutils.DynaBeanDemo
name = 小七
age = 26 (类型 java.lang.Integer)
tags[1] = 中间件
DynaBean 实现类 = org.apache.commons.beanutils.BasicDynaBean
lazy.remark = 无需预定义属性即可赋值
lazy.level = 3 (类型 java.lang.Integer)
要点:BasicDynaClass 第二参传 null 即用默认实现 BasicDynaBean,属性只能取自声明清单;LazyDynaBean 不需声明,赋什么有什么,适合字段集在运行期才能确定的场景。
七、示例五:Map 与 Bean 互转
MapBeanDemo.java:
package com.example.beanutils;
import java.util.LinkedHashMap;
import java.util.Map;
import org.apache.commons.beanutils.BeanUtils;
/** 示例五:BeanUtils.describe / populate 实现 Map 与 Bean 互转 */
public class MapBeanDemo {
public static void main(String[] args) throws Exception {
Model.Employee emp = new Model.Employee();
emp.setEmpName("周八");
emp.setAge(35);
emp.getDept().setDeptName("基础软件部");
// Bean -> Map<String, String>(含 class 键,值全部转为字符串)
Map<String, String> described = BeanUtils.describe(emp);
Map<String, String> printable = new LinkedHashMap<>();
described.forEach((k, v) -> {
if (!"dept".equals(k) && !"onboardDate".equals(k)) {
printable.put(k, v);
}
});
System.out.println("describe 过滤后 = " + printable);
// Map -> Bean(字符串自动转目标类型)
Map<String, String> params = new LinkedHashMap<>();
params.put("empName", "吴九");
params.put("age", "41");
Model.Employee another = new Model.Employee();
BeanUtils.populate(another, params);
System.out.println("populate 后 empName = " + another.getEmpName());
System.out.println("populate 后 age = " + another.getAge() + " (类型 " + another.getAge().getClass().getName() + ")");
}
}
运行:
# mvn -q compile exec:java -Dexec.mainClass=com.example.beanutils.MapBeanDemo
describe 过滤后 = {empName=周八, age=35}
populate 后 empName = 吴九
populate 后 age = 41 (类型 java.lang.Integer)
要点:describe 返回的全部值是字符串,且固定多出一个 class 键(值为全限定类名,本例打印前过滤了非展示字段);populate 按目标属性类型做自动转换,行为与 copyProperties 一致。
八、集成要点与实践建议
把五个示例映射到真实集成场景:
- Web 表单接入:表单参数先收进
Map<String, String>,populate一步灌入实体(示例五);日期字段先按示例三注册格式转换器,否则抛ConversionException。 - 跨层搬运:DTO 到实体的同名属性用
copyProperties(示例二),注意 dest 在前;仅个别字段时用PropertyUtils.setProperty点对点处理(示例一)。 - 动态结构:规则引擎、导入模板这类字段集不固定的场景选
LazyDynaBean(示例四),固定结构用BasicDynaClass预声明以防字段拼写错误。 - 导出/日志:
describe快速把 Bean 拍平成键值对,记得剥离class键(示例五)。 - 错误处理:属性不存在抛
NoSuchMethodException,转换失败抛ConversionException,两者都应在调用边界统一捕获。
九、FAQ
Q1:五个示例如何在无 Maven 的机器上运行?
按安装指南的方案 B 准备三 jar(主 jar、commons-logging 1.3.5、commons-collections 3.2.2),javac -encoding UTF-8 -cp lib/commons-beanutils-1.11.0.jar -d out $(find src -name "*.java") 编译后,java -cp out:lib/commons-beanutils-1.11.0.jar:lib/commons-logging-1.3.5.jar:lib/commons-collections-3.2.2.jar com.example.beanutils.NestedDemo 逐个运行。
Q2:copyProperties 会拷贝 null 值吗?
同名属性按来源值原样复制,来源为 null 的可空属性会覆盖目标;示例二中来源 Form 没有 dept 属性所以目标保持默认——只有「两边同名」的属性才参与拷贝。
Q3:为什么日期转换必须注册转换器?
实测默认 DateConverter 对任意字符串直接抛 ConversionException: DateConverter does not support default String to 'Date' conversion.,没有可用的隐式默认格式;按示例三设置 pattern 后才可解析。
Q4:中文输出乱码怎么处理?
先 export LANG=C.UTF-8 再执行 mvn 或 java 命令,编译源码保持 UTF-8 编码声明;这是 POSIX locale 环境的通病,与库本身无关。
十、参考链接
- Apache Commons BeanUtils 官方主页:https://commons.apache.org/proper/commons-beanutils/
- 官方用户指南:https://commons.apache.org/proper/commons-beanutils/userguide.html
- 官方 Javadoc:https://commons.apache.org/proper/commons-beanutils/apidocs/
- Maven 中央仓库坐标页:https://repo.maven.apache.org/maven2/commons-beanutils/commons-beanutils/
- 官方下载目录:https://downloads.apache.org/commons/beanutils/