Backend Development 9 min read

Pitfalls of Using Property Copy Tools in Java and Recommended Practices

This article explains why using generic property copy utilities like Apache Commons BeanUtils, Spring BeanUtils, CGLIB BeanCopier, or MapStruct can lead to performance degradation, type‑conversion errors, and hidden bugs in Java applications, and recommends defining explicit conversion classes with IDE‑generated getters and setters for safer, compile‑time‑checked mappings.

Java Captain
Java Captain
Java Captain
Pitfalls of Using Property Copy Tools in Java and Recommended Practices

In the background section the author argues against using generic property copy tools and suggests defining explicit conversion classes with IDE‑generated getter/setter methods.

The main reasons are poor performance, occasional bugs, and hidden risks that may surface only at runtime.

For example, a real case in a company showed that Apache Commons BeanUtils performed poorly compared to Spring's BeanUtils, prompting a switch to the latter.

Below are simple Java bean definitions used in the demonstrations:

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

@Data
public class A {
    private String name;
    private List
ids;
}
import lombok.Data;
import java.util.List;

@Data
public class B {
    private String name;
    private List
ids;
}

Using Spring's BeanUtils.copyProperties to copy from A to B results in a ClassCastException because the generic type List is assigned to List at runtime:

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 error
            System.out.println(each);
        }
    }
}

The same runtime error occurs when using CGLIB's BeanCopier without a custom converter.

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();
        BeanCopier beanCopier = BeanCopier.create(A.class, B.class, false);
        beanCopier.copy(first, second, null);
        for (String each : second.getIds()) { // type conversion error
            System.out.println(each);
        }
    }
}

MapStruct can generate a mapper that automatically converts the list of integers to a list of strings, hiding the type mismatch:

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

@Mapper
public interface Converter {
    Converter INSTANCE = Mappers.getMapper(Converter.class);
    B aToB(A source);
}
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 fine
            System.out.println(each);
        }
    }
}

The generated implementation ( ConverterImpl ) performs the conversion internally, which can be convenient but may also mask unintended type changes:

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 source) {
        if (source == null) {
            return null;
        }
        B b = new B();
        b.setName(source.getName());
        b.setIds(integerListToStringList(source.getIds()));
        return b;
    }

    protected List
integerListToStringList(List
list) {
        if (list == null) {
            return null;
        }
        List
result = new ArrayList<>(list.size());
        for (Integer i : list) {
            result.add(String.valueOf(i));
        }
        return result;
    }
}

If a field type differs (e.g., String number in A and Long number in B ), MapStruct will generate code that parses the string to a long, which can throw NumberFormatException when the value is not numeric.

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

When using CGLIB without a custom converter, the number field is simply ignored, leaving it null in the target object.

Manually writing a simple converter avoids these hidden transformations and lets compile‑time checks catch mismatched or missing fields:

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

Because Java generics are erased after compilation, both List and List appear as raw List at runtime, allowing assignments that compile but fail logically. Tools that perform automatic mapping can therefore introduce subtle bugs.

MapStruct reads generic types during annotation processing, enabling compile‑time mapping, yet it can still produce unwanted conversions if the developer defines incorrect target types.

The author previously benchmarked several property‑mapping tools, showing that direct getter/setter calls are the most efficient and that custom conversion classes provide the safest, most maintainable solution.

Hope this article helps; if it does, please like and support the author.
JavaPerformanceBeanUtilsMapStructCGLIBProperty Mapping
Java Captain
Written by

Java Captain

Focused on Java technologies: SSM, the Spring ecosystem, microservices, MySQL, MyCat, clustering, distributed systems, middleware, Linux, networking, multithreading; occasionally covers DevOps tools like Jenkins, Nexus, Docker, ELK; shares practical tech insights and is dedicated to full‑stack Java development.

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.