Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion hsweb-core/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,15 @@
FastBeanCopier.copy(source,target,"id");

```

#### 访问器支持

`FastBeanCopier` 支持标准 JavaBean 访问器、链式 setter(如 `getName()` / `Bean setName(String)`),以及同名的 fluent 方法对(如 `name()` / `Bean name(String)`,包括继承的方法)。扩展识别仅匹配公开实例方法,getter 返回类型必须与 setter 参数类型一致,避免重载选择依赖反射枚举顺序;已有标准 JavaBean 属性优先。属性仍需同时可读、可写,单独的 fluent 读取方法、静态 setter 及参数类型不匹配的方法对不会被识别为可复制属性。

实现位于 `hsweb-core/src/main/java/org/hswebframework/web/bean/FastBeanCopier.java`,使用独立的 `PropertyUtilsBean` 和内省扩展点,不修改全局 BeanUtils 配置,复用现有代码生成、类型转换和缓存。

验证:`FastBeanCopierBeanTest` 的新增回归测试覆盖 Bean/Map 双向复制、不同访问器风格的 Bean 间复制、继承泛型转换、忽略属性、标准访问器优先级、方法配对边界、全局配置隔离、setter 重载声明顺序及静态 setter 排除。执行 `mvn -o -pl hsweb-core -am '-Dtest=org.hswebframework.web.bean.*Test' -Dsurefire.failIfNoSpecifiedTests=false -Dmaven.test.redirectTestOutputToFile=true test`,编译成功,34 项测试全部通过。

约定: 如果属性类实现了`Cloneable`接口,在复制的时候将调用`clone`方法.所以如果你实现了`Cloneable`接口,就必须重写`clone`方法并且为`public`修饰的.

### 数据字典
Expand Down Expand Up @@ -80,4 +89,3 @@ public class MyEntity{
}

```

Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,12 @@
import org.springframework.util.NumberUtils;
import org.springframework.util.ReflectionUtils;

import java.beans.Introspector;
import java.beans.PropertyDescriptor;
import java.lang.reflect.Array;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.lang.reflect.RecordComponent;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
Expand All @@ -43,7 +46,7 @@ public final class FastBeanCopier {

private static final Map<CacheKey, RecordCopier> RECORD_CACHE = new ConcurrentHashMap<>();

private static final PropertyUtilsBean propertyUtils = BeanUtilsBean.getInstance().getPropertyUtils();
private static final PropertyUtilsBean propertyUtils = new PropertyUtilsBean();

private static final ConvertUtilsBean convertUtils = BeanUtilsBean.getInstance().getConvertUtils();

Expand All @@ -66,6 +69,37 @@ public static BeanFactory getBeanFactory() {
}

static {
// 访问器扩展仅作用于 copier,避免改变全局 BeanUtils 的属性规则。
propertyUtils.addBeanIntrospector(context -> {
Class<?> type = context.getTargetClass();
for (Method method : type.getMethods()) {
if (Modifier.isStatic(method.getModifiers()) || method.isBridge()) {
continue;
}
if (method.getName().startsWith("set") && method.getParameterCount() == 1) {
String name = Introspector.decapitalize(method.getName().substring(3));
PropertyDescriptor property = context.getPropertyDescriptor(name);
// 链式 setter 必须与 getter 类型一致,避免重载选择依赖反射方法顺序。
if (property != null
&& property.getReadMethod() != null
&& property.getWriteMethod() == null
&& property.getReadMethod().getReturnType() == method.getParameterTypes()[0]) {
context.addPropertyDescriptor(new PropertyDescriptor(name, property.getReadMethod(), method));
}
continue;
}
if (method.getParameterCount() != 0
|| method.getReturnType() == void.class
|| context.hasProperty(method.getName())) {
continue;
}
// 仅补充同名且类型匹配的 fluent 方法对,保留标准 JavaBean 属性的优先级。
Method setter = ClassUtils.getMethodIfAvailable(type, method.getName(), method.getReturnType());
if (setter != null && !Modifier.isStatic(setter.getModifiers())) {
context.addPropertyDescriptor(new PropertyDescriptor(method.getName(), method, setter));
}
}
});
wrapperClassMapping.put(byte.class, Byte.class);
wrapperClassMapping.put(short.class, Short.class);
wrapperClassMapping.put(int.class, Integer.class);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,14 @@

import lombok.Getter;
import lombok.Setter;
import lombok.experimental.Accessors;
import org.apache.commons.beanutils.BeanUtilsBean;
import org.apache.commons.beanutils.PropertyUtilsBean;
import org.junit.Assert;
import org.junit.Test;

import java.beans.PropertyDescriptor;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

Expand Down Expand Up @@ -40,6 +45,242 @@ public void testCopyGenericSuperclassListField() {
Assert.assertEquals("image.jpg", output.getFileData().get(0).getName());
}

@Test
public void testCopyChainAccessors() {
Map<String, Object> values = Map.of("name", "chain", "age", 12, "enabled", true);
ChainBean bean = FastBeanCopier.copy(values, new ChainBean());

Assert.assertEquals("chain", bean.getName());
Assert.assertEquals(12, bean.getAge());
Assert.assertTrue(bean.isEnabled());
Assert.assertEquals(values, FastBeanCopier.copy(bean, new LinkedHashMap<>()));

StandardBean standard = FastBeanCopier.copy(bean, new StandardBean());
Assert.assertEquals("chain", standard.getName());
Assert.assertEquals(12, standard.getAge());
ChainBean copied = FastBeanCopier.copy(standard, new ChainBean());
Assert.assertEquals(values, FastBeanCopier.copy(copied, new LinkedHashMap<>()));
}

@Test
public void testCopyFluentAccessors() {
Map<String, Object> values = Map.of("name", "fluent", "age", 18, "enabled", true);
FluentBean bean = FastBeanCopier.copy(values, new FluentBean());

Assert.assertEquals("fluent", bean.name());
Assert.assertEquals(18, bean.age());
Assert.assertTrue(bean.enabled());
Assert.assertEquals(values, FastBeanCopier.copy(bean, new LinkedHashMap<>()));

StandardBean standard = FastBeanCopier.copy(bean, new StandardBean());
Assert.assertEquals("fluent", standard.getName());
Assert.assertEquals(18, standard.getAge());
FluentBean copied = FastBeanCopier.copy(standard, new FluentBean());
Assert.assertEquals(values, FastBeanCopier.copy(copied, new LinkedHashMap<>()));
}

@Test
public void testCopyBetweenAccessorStylesAndIgnoreProperty() {
ChainBean source = new ChainBean().setName("chain").setAge(20).setEnabled(true);
FluentBean fluent = FastBeanCopier.copy(source, new FluentBean());
Assert.assertEquals("chain", fluent.name());
Assert.assertEquals(20, fluent.age());
Assert.assertTrue(fluent.enabled());

ChainBean target = FastBeanCopier.copy(fluent, new ChainBean().setName("retained"), "name");
Assert.assertEquals("retained", target.getName());
Assert.assertEquals(20, target.getAge());
Assert.assertTrue(target.isEnabled());

FluentBean converted = FastBeanCopier.copy(Map.of("age", "21"), new FluentBean());
Assert.assertEquals(21, converted.age());
}

@Test
public void testCopyInheritedFluentGenericList() {
FluentOutput output = FastBeanCopier.copy(
Map.of("fileData", List.of(Map.of("name", "fluent.jpg"))),
new FluentOutput()
);

Assert.assertEquals(1, output.fileData().size());
Assert.assertTrue(output.fileData().get(0) instanceof FileItem);
Assert.assertEquals("fluent.jpg", output.fileData().get(0).getName());
}

@Test
public void testIgnoreUnpairedStaticAndMismatchedFluentMethods() {
Assert.assertTrue(FastBeanCopier.copy(new InvalidAccessors(), new LinkedHashMap<>()).isEmpty());
}

@Test
public void testAccessorExtensionsDoNotChangeGlobalBeanUtils() throws Exception {
PropertyUtilsBean global = BeanUtilsBean.getInstance().getPropertyUtils();
PropertyDescriptor descriptor = global.getPropertyDescriptor(new ChainBean(), "name");
Assert.assertNull(descriptor.getWriteMethod());

Assert.assertEquals("isolated", FastBeanCopier.copy(Map.of("name", "isolated"), new ChainBean()).getName());

Assert.assertNull(global.getPropertyDescriptor(new ChainBean(), "name").getWriteMethod());
}

@Test
public void testChainSetterOverloadsMatchGetterType() {
Map<String, Object> values = Map.of("name", "exact");

Assert.assertEquals("exact", FastBeanCopier.copy(values, new OverloadedChainA()).getName());
Assert.assertEquals("exact", FastBeanCopier.copy(values, new OverloadedChainB()).getName());
}

@Test
public void testIgnoreStaticSetters() {
Map<String, Object> values = Map.of("name", "updated");

StaticChainSetter chain = FastBeanCopier.copy(values, new StaticChainSetter());
StaticVoidSetter standard = FastBeanCopier.copy(values, new StaticVoidSetter());
Assert.assertEquals("retained", chain.getName());
Assert.assertEquals("retained", standard.getName());
Assert.assertTrue(FastBeanCopier.copy(chain, new LinkedHashMap<>()).isEmpty());
Assert.assertTrue(FastBeanCopier.copy(standard, new LinkedHashMap<>()).isEmpty());

Assert.assertEquals("updated", FastBeanCopier.copy(values, new ChainWithStaticOverload()).getName());
}

public static class OverloadedChainA {
private String name;

public String getName() {
return name;
}

public OverloadedChainA setName(Object name) {
throw new AssertionError("Setter must match getter type");
}

public OverloadedChainA setName(String name) {
this.name = name;
return this;
}
}

public static class OverloadedChainB {
private String name;

public String getName() {
return name;
}

public OverloadedChainB setName(String name) {
this.name = name;
return this;
}

public OverloadedChainB setName(Object name) {
throw new AssertionError("Setter must match getter type");
}
}

public static class StaticChainSetter {
public String getName() {
return "retained";
}

public static StaticChainSetter setName(String name) {
throw new AssertionError("Static setter must not be called");
}
}

public static class StaticVoidSetter {
public String getName() {
return "retained";
}

public static void setName(String name) {
throw new AssertionError("Static setter must not be called");
}
}

public static class ChainWithStaticOverload extends ChainBean {
public static ChainBean setName(Object name) {
throw new AssertionError("Static overload must not be called");
}
}

@Getter
@Setter
public static class StandardBean {
private String name;
private int age;
private boolean enabled;

public String name() {
throw new AssertionError("Standard getter must take precedence");
}

public StandardBean name(String name) {
throw new AssertionError("Standard setter must take precedence");
}
}

@Getter
@Setter
@Accessors(chain = true)
public static class ChainBean {
private String name;
private int age;
private boolean enabled;
}

@Getter
@Setter
@Accessors(fluent = true)
public static class FluentBean {
private String name;
private int age;
private boolean enabled;

public FluentBean name(String name) {
this.name = name;
return this;
}

public FluentBean name(Integer name) {
throw new AssertionError("Setter must match getter type");
}
}

@Getter
@Setter
@Accessors(fluent = true)
public static class FluentGenericBase<T> {
private List<T> fileData;
}

public static class FluentOutput extends FluentGenericBase<FileItem> {
}

public static class InvalidAccessors {
public String readOnly() {
throw new AssertionError("Unpaired method must not be copied");
}

public static String staticValue() {
throw new AssertionError("Static method must not be copied");
}

public static InvalidAccessors staticValue(String value) {
throw new AssertionError("Static method must not be copied");
}

public String mismatched() {
throw new AssertionError("Mismatched method must not be copied");
}

public InvalidAccessors mismatched(Integer value) {
throw new AssertionError("Mismatched method must not be copied");
}
}

@Getter
@Setter
public static class DirectOutput {
Expand Down
Loading