Backend Development 9 min read

Pitfalls of Java Property Copy Tools and Recommendations for Safe Bean Mapping

This article examines the performance drawbacks and hidden type‑conversion issues of common Java property‑copy utilities such as Spring BeanUtils, CGLIB BeanCopier, and MapStruct, demonstrates concrete code examples, and recommends using explicit conversion classes or IDE‑generated getters/setters to avoid runtime errors.

Selected Java Interview Questions
Selected Java Interview Questions
Selected Java Interview Questions
Pitfalls of Java Property Copy Tools and Recommendations for Safe Bean Mapping

1 Background

In a previous column we advised against using generic property‑copy utilities and recommended defining explicit conversion classes and methods, leveraging IDE plugins to auto‑generate getters and setters.

The main reasons for avoiding these tools are:

Some property‑copy tools have poor performance, contain bugs, and can introduce hidden risks during copying.

2 Example

Our company encountered a real case where org.apache.commons.beanutils.BeanUtils performed poorly for property copying, and switching to Spring's BeanUtils yielded a noticeable improvement. You can benchmark these tools yourself, but the focus here is on functional issues.

Below is a demonstration of a problem with Spring's BeanUtils.copyProperties when copying between classes with mismatched generic types:

import lombok.Data;
import java.util.List;

@Data
public class A {
    private String name;
    private List
ids;
}
@Data
public class B {
    private String name;
    private List
ids;
}
import org.springframework.beans.BeanUtils;
import java.util.Arrays;

public class BeanUtilDemo {
    public static void main(String[] args) {
        A first = new A();
        first.setName("demo");
        first.setIds(Arrays.asList(1, 2, 3));

        B second = new B();
        BeanUtils.copyProperties(first, second);
        for (String each : second.getIds()) { // type conversion exception
            System.out.println(each);
        }
    }
}

Running the above code throws a ClassCastException because the ids field in B remains a List after copying.

A breakpoint reveals that the generic type information is erased at runtime, so the assignment succeeds but later usage fails.

If the list is printed without converting to String , no exception occurs.

Using CGLIB's BeanCopier without a custom converter exhibits the same issue:

import org.easymock.cglib.beans.BeanCopier;
import java.util.Arrays;

public class BeanUtilDemo {
    public static void main(String[] args) {
        A first = new A();
        first.setName("demo");
        first.setIds(Arrays.asList(1, 2, 3));

        B second = new B();
        final BeanCopier beanCopier = BeanCopier.create(A.class, B.class, false);
        beanCopier.copy(first, second, null);
        for (String each : second.getIds()) { // type conversion exception
            System.out.println(each);
        }
    }
}

The problem surfaces only at runtime.

Now consider MapStruct, which generates a mapper implementation at compile time:

import org.mapstruct.Mapper;
import org.mapstruct.factory.Mappers;

@Mapper
public interface Converter {
    Converter INSTANCE = Mappers.getMapper(Converter.class);
    B aToB(A car);
}
import java.util.Arrays;

public class BeanUtilDemo {
    public static void main(String[] args) {
        A first = new A();
        first.setName("demo");
        first.setIds(Arrays.asList(1, 2, 3));

        B second = Converter.INSTANCE.aToB(first);
        for (String each : second.getIds()) { // works correctly
            System.out.println(each);
        }
    }
}

MapStruct automatically converts List to List by generating helper methods:

import java.util.ArrayList;
import java.util.List;
import javax.annotation.Generated;
import org.springframework.stereotype.Component;

@Generated(value = "org.mapstruct.ap.MappingProcessor", comments = "version: 1.3.1.Final, compiler: javac, environment: Java 1.8.0_202 (Oracle Corporation)")
@Component
public class ConverterImpl implements Converter {
    @Override
    public B aToB(A car) {
        if (car == null) {
            return null;
        }
        B b = new B();
        b.setName(car.getName());
        b.setIds(integerListToStringList(car.getIds()));
        return b;
    }

    protected List
integerListToStringList(List
list) {
        if (list == null) {
            return null;
        }
        List
list1 = new ArrayList
(list.size());
        for (Integer integer : list) {
            list1.add(String.valueOf(integer));
        }
        return list1;
    }
}

While convenient, this automatic conversion can hide type mismatches; for example, adding a String number field in A and a Long number field in B leads MapStruct to generate code that throws NumberFormatException when the string is not numeric.

@Override
public B aToB(A car) {
    if (car == null) {
        return null;
    }
    B b = new B();
    b.setName(car.getName());
    if (car.getNumber() != null) { // problem here
        b.setNumber(Long.parseLong(car.getNumber()));
    }
    b.setIds(integerListToStringList(car.getIds()));
    return b;
}

Using CGLIB without a custom converter does not map the number field, leaving it null in B .

Manually defining a converter (e.g., via an IDE plugin) makes the mismatch visible at compile time:

public final class A2BConverter {
    public static B from(A first) {
        B b = new B();
        b.setName(first.getName());
        b.setIds(first.getIds());
        return b;
    }
}

3 Conclusion

Because Java generics are erased after compilation, both List and List become plain List at runtime, allowing assignments that compile but fail later when the element types differ. This makes many bean‑mapping tools error‑prone.

MapStruct reads generic types during annotation processing, enabling compile‑time mapping, but it can also silently perform unwanted conversions if the source and target types are mismatched.

Performance comparisons among various property‑mapping tools show significant differences, so it is advisable to avoid generic copy utilities when possible. Defining explicit conversion classes (or using IDE‑generated getters/setters) provides compile‑time safety and high runtime efficiency.

JavaPerformanceBeanUtilsMapStructPropertyCopy
Selected Java Interview Questions
Written by

Selected Java Interview Questions

A professional Java tech channel sharing common knowledge to help developers fill gaps. Follow us!

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.