Why Java Bean Property Copy Tools Can Hide Dangerous Type Bugs
This article explains why using automatic property copy utilities like Spring BeanUtils, CGLIB BeanCopier, or MapStruct can lead to hidden type conversion errors in Java, compares their performance, demonstrates pitfalls with generic type erasure, and recommends writing explicit conversion classes to avoid runtime bugs.
1 Background
Previously we advised against using property‑copy utilities and suggested defining conversion classes manually, letting IDE plugins generate getters and setters.
The main reasons are:
Some tools have poor performance.
Some tools contain bugs.
Using them can introduce hidden risks, as shown in the examples below.
2 Example
In a real case, a colleague switched from commons‑beanutils to Spring's BeanUtils and saw a big performance improvement. However, Spring's BeanUtils can cause type‑conversion problems.
import lombok.Data;
import java.util.List;
@Data
public class A {
private String name;
private List<Integer> ids;
} import lombok.Data;
@Data
public class B {
private String name;
private List<String> 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 this code throws a ClassCastException because the ids field in B remains a List<Integer> after copying.
The debugger shows that the generic type information is lost at runtime.
When printing the list without converting to String, no error 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();
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 only appears at runtime.
Now look at MapStruct:
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 fine
System.out.println(each);
}
}
}MapStruct automatically converts List<Integer> to List<String>.
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<String> integerListToStringList(List<Integer> list) {
if (list == null) {
return null;
}
List<String> list1 = new ArrayList<String>(list.size());
for (Integer integer : list) {
list1.add(String.valueOf(integer));
}
return list1;
}
}While convenient, this automatic conversion can hide type mismatches.
If class A has a String number field and class B has a Long number field, MapStruct will generate code that parses the string to a long, throwing NumberFormatException for non‑numeric values.
if (car.getNumber() != null) {
// Problem here
b.setNumber(Long.parseLong(car.getNumber()));
}CGLIB, on the other hand, simply leaves the number field null.
Manually writing a converter avoids these hidden issues:
public final class A2BConverter {
public static B from(A first) {
B b = new B();
b.setName(first.getName());
b.setIds(first.getIds());
return b;
}
}Such explicit code lets the compiler catch mismatched types early.
3 Conclusion
Because Java generics are erased after compilation, both List<Integer> and List<String> become raw List at runtime, allowing assignments that hide type errors. This makes many property‑mapping tools prone to subtle bugs.
MapStruct reads generic types during annotation processing, enabling compile‑time mapping, but it can also silently perform conversions that introduce side effects if the developer defines the wrong target type.
Performance tests (see image below) show varying speeds among tools. Therefore, it is advisable to use explicit conversion classes—potentially generated by IDE plugins—so that any type mismatch is caught at compile time and runtime overhead remains minimal.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Java Backend Technology
Focus on Java-related technologies: SSM, Spring ecosystem, microservices, MySQL, MyCat, clustering, distributed systems, middleware, Linux, networking, multithreading. Occasionally cover DevOps tools like Jenkins, Nexus, Docker, and ELK. Also share technical insights from time to time, committed to Java full-stack development!
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
