Java Reflection: BeanUtils vs PropertyDescriptor
1. Introduction
Java reflection Beans Property API and Apache Commons BeanUtils are two APIs that provide Beans Property APIs to inspect and manipulate JavaBean objects at runtime. The PropertyDescriptor enables low-level access to properties via getter and setter methods. The Apache Commons BeanUtils simplifies common operations such as reading, writing, populating from maps, and copying bean properties. In this example, we’ll demonstrate both Java reflection beanutils propertydescriptor APIs.
- java.beans.PropertyDescriptor describes one property that a
JavaBeanexports via a pair of accessor methods. It’s part of JavaBeans Introspection API. - org.apache.commons.beanutils.BeanUtils provides a special BeanIntrospector to access the class properties on
JavaBean. - java.lang.reflect provides the low-level classes and interfaces to inspect and manipulate any classes at runtime. It includes Field to access fields, Method to invoke methods, and Constructor to create new instances.
2. Setup
In this step, I will create a simple Java project in Eclipse with JDK 21, Apache Common BeanUtils, and Junit5. Here are the Java source files:
Java Source Files
C:\Users\zzhen\eclipse-workspace\ReflectionBeanApi\src\main\org\zheng\demo>dir Volume in drive C is OS Volume Serial Number is FE59-88F4 Directory of C:\Users\zzhen\eclipse-workspace\ReflectionBeanApi\src\main\org\zheng\demo 09/14/2025 08:09 AM . 09/11/2025 01:22 PM .. 09/14/2025 11:02 AM 686 ImproperBean.java 09/14/2025 01:24 PM 709 JavaBeanExample.java Directory of C:\Users\zzhen\eclipse-workspace\ReflectionBeanApi\src\test\org\zheng\demo 09/14/2025 09:22 AM . 09/11/2025 09:07 PM .. 09/14/2025 01:06 PM 3,531 TestApacheBeanUtils.java 09/14/2025 11:34 AM 1,077 TestBase.java 09/14/2025 11:17 AM 2,443 TestPropertyDescriptor.java 09/14/2025 11:16 AM 2,214 TestRelectionApi.java
3. Java Beans
A JavaBean is not a special type. It is a design pattern used in frameworks, tools, and libraries. JavaBean is a Java class that follows the following conventions:
- Public no-argument constructor
- Allows frameworks (e.g., Spring, Hibernate, BeanUtils) to create instances reflectively.
- Private fields with public getters and setters
- Properties must follow naming conventions:
isXxx() forboolean.getXxx() /setXxx(…) for all other types.
- Properties must follow naming conventions:
- Serializable (optional but common)
- Implements
java.io.Serializableso they can be easily saved, transferred, or cached.
- Implements
3.1 Base Class without No-Argument Constructor
In this step, I will create a ImproperBean.java without a no-argument constructor.
ImproperBean.java
package org.zheng.demo;
public class ImproperBean {
private int id;
private String name;
private boolean valid;
//this makes it as improper JavaBean as the default no-args constructor not exists.
public ImproperBean(int id, boolean valid, String name) {
this.id = id;
this.valid = valid;
this.name = name;
}
public int getId() {
return id;
}
public String getName() {
return name;
}
public boolean isValid() {
return valid;
}
public void setId(int id) {
this.id = id;
}
public void setName(String name) {
this.name = name;
}
public void setValid(boolean valid) {
this.valid = valid;
}
}
- Line 12: The default no-argument constructor is not generated by the compiler as there is a constructor already. The class might start as a
JavaBeanbecause the compiler generates default no-argument constructor if there is no any constructor. The best practice forJavaBeanis to create a no-argument constructor explicitly.
3.2 Create a Java Bean Example
In this example, I will create the JavaBeanExample.java that extends from ImproperBean. It is a JavaBean as it has the explicitly no-argument constructor.
JavaBeanExample.java
package org.zheng.demo;
import java.io.Serializable;
public class JavaBeanExample extends ImproperBean implements Serializable {
private static final long serialVersionUID = -8912489223018164443L;
private String internalField;
// no-args constructor is added
public JavaBeanExample() {
super(0, false, "default");
this.internalField = "Internal";
}
public JavaBeanExample(int id, boolean isValud, String name) {
super(id, isValud, name);
}
@Override
public String toString() {
return "JavaBeanExample [internalField=" + internalField + ", getId()=" + getId() + ", getName()=" + getName()
+ ", isValid()=" + isValid() + "]";
}
}
- Line 9: the
private internalFieldis not accessible via eitherBeanUtilsnorPropertyDescriptoras it has no public getter and setter. - Line 12: adding the no-argument constructor makes the
JavaBeanExampleas aJavaBean
4. Access JavaBean via PropertyDescriptor
In this step, I will create a TestPropertyDescriptor.java that accesses and miniplates the JavaBean’s properties.
TestPropertyDescriptor.java
package org.zheng.demo;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import java.beans.BeanInfo;
import java.beans.IntrospectionException;
import java.beans.Introspector;
import java.beans.PropertyDescriptor;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import org.junit.jupiter.api.Test;
class TestPropertyDescriptor extends TestBase {
@Test
void get_set_value_via_PropertyDescriptors()
throws IntrospectionException, IllegalAccessException, InvocationTargetException {
BeanInfo testBeanInfo = Introspector.getBeanInfo(JavaBeanExample.class);
for (PropertyDescriptor pd : testBeanInfo.getPropertyDescriptors()) {
String pdName = pd.getName();
Method getter = pd.getReadMethod();
Method setter = pd.getWriteMethod();
if (getter != null && !"class".equals(pdName)) {
Object value = getter.invoke(testBean);
System.out.printf("\nBean Property via Getter: %s=%s", pdName, value);
}
if (setter != null) {
switch (pdName) {
case ID:
setter.invoke(testBean, 20);
break;
case NAME:
setter.invoke(testBean, "NewName");
break;
case VALID:
setter.invoke(testBean, true);
break;
default:
System.out.printf("New property added, %s", pdName);
}
}
}
assertEquals(20, testBean.getId());
assertEquals("NewName", testBean.getName());
assertEquals(true, testBean.isValid());
}
@Test
void set_value_via_PropertyDescriptor()
throws IntrospectionException, IllegalAccessException, InvocationTargetException {
PropertyDescriptor idPd = new PropertyDescriptor(ID, JavaBeanExample.class);
idPd.getWriteMethod().invoke(testBean, 22);
assertEquals(22, testBean.getId());
}
@Test
void throw_exception_wrongType()
throws IntrospectionException, IllegalAccessException, InvocationTargetException {
PropertyDescriptor idPd = new PropertyDescriptor(ID, JavaBeanExample.class);
assertThrows(IllegalArgumentException.class, () -> idPd.getWriteMethod().invoke(testBean, "22"));
}
@Test
void throw_exception_nogetter_field()
throws IntrospectionException, IllegalAccessException, InvocationTargetException {
assertThrows(IntrospectionException.class,
() -> new PropertyDescriptor("internalField", JavaBeanExample.class));
}
}
- Line 21: create a
BeanInfoinstance fromIntrospector.getBeanInfo(JavaBeanExample.class). - Line 23: loop through the
PropertyDescriptors. - Line 25, 26: obtain the
Methodfor both read and write methods. - Line 29: invoke the read methods and return type is
Object. - Line 36, 39, 42: invoke the write methods to update the properties.
- Line 59: create a
PropertyDescriptorbased on the “ID” property inJavaBeanExample.class. - Line 69:
PropertyDescriptorthrows exception when the type mismatches. - Line 76:
PropertyDescriptorthrows exception when the field missed getter nor setter.
Run TestPropertyDescriptor.java as a Junit Test and capture the output.
TestPropertyDescriptor.java
Initial Bean: JavaBeanExample [internalField=Internal, getId()=1, getName()=Mary, isValid()=true] Updated Bean: JavaBeanExample [internalField=Internal, getId()=1, getName()=Mary, isValid()=true]Initial Bean: JavaBeanExample [internalField=Internal, getId()=1, getName()=Mary, isValid()=true] Updated Bean: JavaBeanExample [internalField=Internal, getId()=22, getName()=Mary, isValid()=true]Initial Bean: JavaBeanExample [internalField=Internal, getId()=1, getName()=Mary, isValid()=true] Updated Bean: JavaBeanExample [internalField=Internal, getId()=1, getName()=Mary, isValid()=true]Initial Bean: JavaBeanExample [internalField=Internal, getId()=1, getName()=Mary, isValid()=true] Bean Property via Getter: id=1 Bean Property via Getter: name=Mary Bean Property via Getter: valid=true Updated Bean: JavaBeanExample [internalField=Internal, getId()=20, getName()=NewName, isValid()=true]
5. Access JavaBean via Apache BeanUtils
In this step, I will create a TestApacheBeanUtils.java that accesses and miniplates the JavaBean’s properties.
TestApacheBeanUtils.java
package org.zheng.demo;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import java.lang.reflect.InvocationTargetException;
import java.util.HashMap;
import java.util.Map;
import org.apache.commons.beanutils.BeanUtils;
import org.junit.jupiter.api.Test;
class TestApacheBeanUtils extends TestBase {
@Test
void copyProperties_from_otherBean()
throws IllegalAccessException, InvocationTargetException, NoSuchMethodException {
JavaBeanExample testBean2 = new JavaBeanExample(2, false, "Zheng");
BeanUtils.copyProperties(testBean, testBean2);
// always return String regardless of its type
assertEquals("2", BeanUtils.getProperty(testBean, ID));
assertEquals("Zheng", BeanUtils.getProperty(testBean, NAME));
assertEquals("false", BeanUtils.getProperty(testBean, VALID));
}
@Test
void get_set_value_via_get_setProperty()
throws IllegalAccessException, InvocationTargetException, NoSuchMethodException {
assertEquals("1", BeanUtils.getProperty(testBean, ID));
assertEquals("Mary", BeanUtils.getProperty(testBean, NAME));
assertEquals("true", BeanUtils.getProperty(testBean, VALID));
BeanUtils.setProperty(testBean, ID, 33);
BeanUtils.setProperty(testBean, NAME, "ZHENG");
BeanUtils.setProperty(testBean, VALID, false);
assertEquals("33", BeanUtils.getProperty(testBean, ID));
assertEquals("ZHENG", BeanUtils.getProperty(testBean, NAME));
assertEquals("false", BeanUtils.getProperty(testBean, VALID));
}
@Test
void populate_from_map() throws IllegalAccessException, InvocationTargetException, NoSuchMethodException {
Map<String, Object> values = new HashMap<>();
values.put(ID, 3);
values.put(NAME, "Bob");
values.put(VALID, false);
BeanUtils.populate(testBean, values);
assertEquals("3", BeanUtils.getProperty(testBean, ID));
assertEquals("Bob", BeanUtils.getProperty(testBean, NAME));
assertEquals("false", BeanUtils.getProperty(testBean, VALID));
}
@Test
void throw_exception_no_getter() throws IllegalAccessException, InvocationTargetException, NoSuchMethodException {
assertThrows(NoClassDefFoundError.class, () -> BeanUtils.getProperty(testBean, "internalField"));
}
@Test
void throw_exception_unknownField()
throws IllegalAccessException, InvocationTargetException, NoSuchMethodException {
Map<String, Object> values = new HashMap<>();
values.put(ID, 3);
values.put("unknownField", 3);
assertThrows(NoClassDefFoundError.class, () -> BeanUtils.populate(testBean, values));
}
@Test
void throw_exception_map_nogetter()
throws IllegalAccessException, InvocationTargetException, NoSuchMethodException {
Map<String, Object> values = new HashMap<>();
values.put(ID, 3);
values.put(NAME, "Bob");
values.put("internalField", "Oops");
assertThrows(NoClassDefFoundError.class, () -> BeanUtils.populate(testBean, values));
}
@Test
void ignore_wrongType() throws IllegalAccessException, InvocationTargetException, NoSuchMethodException {
Map<String, Object> values = new HashMap<>();
values.put(ID, "33A");
values.put(NAME, 123);
testBean = new JavaBeanExample();
BeanUtils.populate(testBean, values);
assertEquals("0", BeanUtils.getProperty(testBean, ID));
assertEquals("123", BeanUtils.getProperty(testBean, NAME));
assertEquals("false", BeanUtils.getProperty(testBean, VALID));
}
}
- Line 20, 23-25: the
BeanUtils.copyProperties()method works as expected for JavaBean; - Line 36-38: verify the
BeanUtils.setProperty()update the property value. - Line 52: verify
BeanUtils.populate()set the property’s value for a map whose key matches the JavaBean’s properties. - Line 61: the
BeanUtils.getProperty()throwing exception if the property has no public getter and setter. - Line 80, 82: the
BeanUtils.populate()throwing exception if the map’s key has no public getter and setter. - Line 88, 92, 93: the
BeanUtils.populate()ignore if the map’s key value has wrong data type.
Run TestApacheBeanUtils.java as a Junit test and capture the output.
TestApacheBeanUtilsOutput
Initial Bean: JavaBeanExample [internalField=Internal, getId()=1, getName()=Mary, isValid()=true] Updated Bean: JavaBeanExample [internalField=Internal, getId()=33, getName()=ZHENG, isValid()=false]Initial Bean: JavaBeanExample [internalField=Internal, getId()=1, getName()=Mary, isValid()=true] Updated Bean: JavaBeanExample [internalField=Internal, getId()=3, getName()=Bob, isValid()=false]Initial Bean: JavaBeanExample [internalField=Internal, getId()=1, getName()=Mary, isValid()=true] Updated Bean: JavaBeanExample [internalField=Internal, getId()=0, getName()=123, isValid()=false]Initial Bean: JavaBeanExample [internalField=Internal, getId()=1, getName()=Mary, isValid()=true] Updated Bean: JavaBeanExample [internalField=Internal, getId()=1, getName()=Mary, isValid()=true]Initial Bean: JavaBeanExample [internalField=Internal, getId()=1, getName()=Mary, isValid()=true] Updated Bean: JavaBeanExample [internalField=Internal, getId()=1, getName()=Mary, isValid()=true]Initial Bean: JavaBeanExample [internalField=Internal, getId()=1, getName()=Mary, isValid()=true] Updated Bean: JavaBeanExample [internalField=Internal, getId()=2, getName()=Zheng, isValid()=false]Initial Bean: JavaBeanExample [internalField=Internal, getId()=1, getName()=Mary, isValid()=true] Updated Bean: JavaBeanExample [internalField=Internal, getId()=1, getName()=Mary, isValid()=true]
6. Compare to Java Reflection API
In this step, I will create a TestRelectionApi.java that accesses and miniplates the JavaBean’s properties. Note: java.lang.reflect works for any Java objects.
TestRelectionApi.java
package org.zheng.demo;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import org.junit.jupiter.api.Test;
class TestRelectionApi extends TestBase {
@Test
void getValue_via_Method() throws NoSuchMethodException, SecurityException, IllegalAccessException,
InvocationTargetException, NoSuchFieldException {
Method getId = JavaBeanExample.class.getMethod(GET_ID);
Object value = getId.invoke(testBean);
assertEquals(1, (int) value);
Method getName = JavaBeanExample.class.getMethod(GET_NAME);
value = getName.invoke(testBean);
assertEquals("Mary", (String) value);
Method isValid = JavaBeanExample.class.getMethod(IS_VALID);
value = isValid.invoke(testBean);
assertEquals(true, (boolean) value);
}
@Test
void get_set_value_via_Field() throws NoSuchMethodException, SecurityException, IllegalAccessException,
InvocationTargetException, NoSuchFieldException {
Field internalField = JavaBeanExample.class.getDeclaredField("internalField");
internalField.setAccessible(true);
internalField.set(testBean, "Oops");
assertEquals("Oops", internalField.get(testBean));
}
@Test
void update_via_setMethod()
throws NoSuchMethodException, SecurityException, IllegalAccessException, InvocationTargetException {
JavaBeanExample.class.getMethod(SET_ID, int.class).invoke(testBean, 4);
assertEquals(4, testBean.getId());
JavaBeanExample.class.getMethod(SET_NAME, String.class).invoke(testBean, "Zheng");
assertEquals("Zheng", testBean.getName());
JavaBeanExample.class.getMethod(SET_VALID, boolean.class).invoke(testBean, true);
assertTrue(testBean.isValid());
}
@Test
void throw_exception_wrongType()
throws IllegalAccessException, InvocationTargetException, NoSuchMethodException, SecurityException {
assertThrows(IllegalArgumentException.class,
() -> JavaBeanExample.class.getMethod(SET_ID, int.class).invoke(testBean, "123"));
}
}
- Line 19: obtain
getIdmethod fromJavaBeanExample.class.getMethod(GET_ID). - Line 20, 21:
getIdis invoked fortestBeanand returnedvalueis casted to theinttype. - Line 23, 24: similar to above, but for the
getNamemethod. - Line 27, 28: similar to above, but for the
isValidmethod. - Line 36,37,38: the reflect API works for any object, any field. so it changed the
private internalField. - Line 47,50,53: the reflect API works for any fields.
- Line 61: the reflect set method throw exception when the argument type mismatchs.
Run TestRelectionApi.java as a Junit and capture output
TestRelectionApi Output
Initial Bean: JavaBeanExample [internalField=Internal, getId()=1, getName()=Mary, isValid()=true] Updated Bean: JavaBeanExample [internalField=Internal, getId()=4, getName()=Zheng, isValid()=true]Initial Bean: JavaBeanExample [internalField=Internal, getId()=1, getName()=Mary, isValid()=true] Updated Bean: JavaBeanExample [internalField=Oops, getId()=1, getName()=Mary, isValid()=true]Initial Bean: JavaBeanExample [internalField=Internal, getId()=1, getName()=Mary, isValid()=true] Updated Bean: JavaBeanExample [internalField=Internal, getId()=1, getName()=Mary, isValid()=true]Initial Bean: JavaBeanExample [internalField=Internal, getId()=1, getName()=Mary, isValid()=true] Updated Bean: JavaBeanExample [internalField=Internal, getId()=1, getName()=Mary, isValid()=true]
7. TestBase
In this step, I will create a TestBase.java that sets up the common test methods and constants.
TestBase.java
package org.zheng.demo;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
public class TestBase {
public static final String GET_ID = "getId";
public static final String GET_NAME = "getName";
public static final String ID = "id";
public static final String IS_VALID = "isValid";
public static final String NAME = "name";
public static final String SET_ID = "setId";
public static final String SET_NAME = "setName";
public static final String SET_VALID = "setValid";
public static final String VALID = "valid";
ImproperBean improperBean = null;
JavaBeanExample testBean = null;
@AfterEach
void end() {
System.out.printf("\nUpdated Bean: %s", testBean.toString());
}
@BeforeEach
void start() {
testBean = new JavaBeanExample();
testBean.setId(1);
testBean.setName("Mary");
testBean.setValid(true);
// no longer create it via no-argument constructor
improperBean = new ImproperBean(2, false, "Zheng");
System.out.printf("Initial Bean: %s\n", testBean.toString());
}
}
- Line 34: the
improperBeanvariable cannot be created via the no-argument constructor.
8. Conclusion
In this example, I created a simple Java program that accesses and miniplates the JavaBean’s properties via Java.beans.PropertyDescriptor and org.apache.commons.beanutils.BeanUtils. The introspection API java.beans is more structured, property-centric API and based on the lower-level java.lang.reflect. I compared them to the lower-level java.lang.reflect API. Note: using the low-level reflect API can expose the sensitive properties uninterionally.
| Difference | PropertyDescriptor | BeanUtils |
| Read Method | PropertyDescriptor read method returns type is Object.class | BeanUtils.getProperty() returns type is String.class |
| Write Method | PropertyDescriptor write method throws IllegalArgumentException if the type mismatches. | BeanUtils.setProperty() ignores the property if the type mismatches. |
9. Download
This was an example of a Java project which accessed and manipulated the JavaBean properties via Java Bean PropertyDescriptor, Apache Common BeanUtils, and Java lang Reflect API.
You can download the full source code of this example here: Java Reflection Beans Property API Example

