fix: BeanConverter和MapConverter对源Bean使用isReadableBean替代isBean

修了 #4245 的问题。当 List 里的元素类用了 @Setter(AccessLevel.PROTECTED)
只有 protected setter 的时候,BeanUtil.copyProperties 会抛
ConvertException: Unsupported source type,但实际上这个类有 public getter,
作为源对象完全可以被读取。

原因是 BeanConverter 和 MapConverter 在处理源对象时用 isBean() 做检查,
但 isBean() 看的是有没有 public setter,这是判断"能不能写"的标准,
而源对象需要的是"能不能读"。BeanUtil 里其实已经有 isReadableBean() 方法
(检查 public getter),正好适合这个场景,换过来就行。两个转换器各改了一行,
另外补了两个单测覆盖这种情况。
This commit is contained in:
Faerytale
2026-05-19 02:15:39 +08:00
parent eaecb109cf
commit 64fc614618
3 changed files with 83 additions and 2 deletions

View File

@@ -79,7 +79,7 @@ public class BeanConverter<T> extends AbstractConverter<T> {
if(value instanceof Map ||
value instanceof ValueProvider ||
BeanUtil.isBean(value.getClass())) {
BeanUtil.isReadableBean(value.getClass())) {
if(value instanceof Map && this.beanClass.isInterface()) {
// 将Map动态代理为Bean
return MapProxy.create((Map<?, ?>)value).toProxyBean(this.beanClass);

View File

@@ -73,7 +73,7 @@ public class MapConverter extends AbstractConverter<Map<?, ?>> {
map = MapUtil.createMap(mapClass);
}
convertMapToMap((Map) value, map);
} else if (BeanUtil.isBean(value.getClass())) {
} else if (BeanUtil.isReadableBean(value.getClass())) {
if(value.getClass().getName().equals("cn.hutool.json.JSONArray")){
// issue#3795 增加JSONArray转Map错误检查
throw new UnsupportedOperationException(StrUtil.format("Unsupported {} to Map.", value.getClass().getName()));

View File

@@ -974,4 +974,85 @@ public class BeanUtilTest {
final boolean bean = BeanUtil.isBean(Dict.class);
assertFalse(bean);
}
// ============ Test for GitHub issue #4245 ============
@Data
@NoArgsConstructor
@AllArgsConstructor
public static class Order4245 {
private Long orderId;
private String orderNo;
private List<OrderItem4245> orderItemList;
}
@Getter
@Setter(AccessLevel.PROTECTED)
@AllArgsConstructor(access = AccessLevel.PROTECTED)
@NoArgsConstructor
public static class OrderItem4245 {
private Long itemId;
private String productName;
private Integer quantity;
}
@Data
public static class OrderDto4245 {
private Long orderId;
private String orderNo;
private List<OrderItemDto4245> orderItemList;
}
@Data
public static class OrderItemDto4245 {
private Long itemId;
private String productName;
private Integer quantity;
}
/**
* GitHub issue#4245: BeanUtil.copyProperties throws ConvertException
* when source list element has protected setters.
*
* <p>Root cause: BeanConverter used isBean() (which checks for public setters)
* on the SOURCE object instead of isReadableBean() (which checks for public getters).
* A source bean only needs to be readable, not writable.</p>
*/
@Test
public void issue4245Test() {
final Order4245 order = new Order4245(
1L,
"01",
new ArrayList<>()
);
order.getOrderItemList().add(
new OrderItem4245(1L, "aa", 1)
);
// This should not throw ConvertException
final OrderDto4245 dto = BeanUtil.copyProperties(order, OrderDto4245.class);
assertNotNull(dto);
assertEquals(order.getOrderId(), dto.getOrderId());
assertEquals(order.getOrderNo(), dto.getOrderNo());
assertNotNull(dto.getOrderItemList());
assertEquals(1, dto.getOrderItemList().size());
assertEquals(Long.valueOf(1L), dto.getOrderItemList().get(0).getItemId());
assertEquals("aa", dto.getOrderItemList().get(0).getProductName());
assertEquals(Integer.valueOf(1), dto.getOrderItemList().get(0).getQuantity());
}
/**
* Test map conversion with protected setter bean
*/
@Test
public void issue4245MapConvertTest() {
final OrderItem4245 item = new OrderItem4245(1L, "aa", 1);
// Should not throw UnsupportedOperationException
final Map<String, Object> map = BeanUtil.beanToMap(item);
assertNotNull(map);
assertEquals(1L, map.get("itemId"));
assertEquals("aa", map.get("productName"));
assertEquals(1, map.get("quantity"));
}
}