Apache Commons BeanUtils 快速入门与集成示例

Commons BeanUtils 1.11.0 快速入门:五个可运行示例覆盖嵌套属性读写、对象拷贝类型转换、自定义日期转换器、DynaBean 与 Map 互转,并对比 Spring 与 MapStruct 属性拷贝方案,双系统实测通过。

Apache Commons BeanUtilsApache Commons BeanUtils2026/10/13

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 环境的通病,与库本身无关。

十、参考链接

  1. Apache Commons BeanUtils 官方主页:https://commons.apache.org/proper/commons-beanutils/
  2. 官方用户指南:https://commons.apache.org/proper/commons-beanutils/userguide.html
  3. 官方 Javadoc:https://commons.apache.org/proper/commons-beanutils/apidocs/
  4. Maven 中央仓库坐标页:https://repo.maven.apache.org/maven2/commons-beanutils/commons-beanutils/
  5. 官方下载目录:https://downloads.apache.org/commons/beanutils/