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
9 changes: 5 additions & 4 deletions .github/workflows/pull_request_5x.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,10 @@ jobs:
build:

runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v1
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Set up JDK 17
uses: actions/setup-java@v1
with:
Expand All @@ -20,5 +21,5 @@ jobs:
with:
path: ~/.m2
key: ${{ runner.os }}-${{ hashFiles('**/pom.xml') }}
- name: Build with Maven
run: ./mvnw test -q
- name: Build changed modules with Maven
run: ./mvnw test -q -pl "$(./changes.sh)"
24 changes: 19 additions & 5 deletions changes.sh
Original file line number Diff line number Diff line change
@@ -1,18 +1,32 @@
#!/usr/bin/env bash

# 收集变更模块
modules=$(git diff --name-only HEAD~1 HEAD | \
while read file; do
set -euo pipefail

# 收集变更模块。
# PR 场景按目标分支对比,避免 CI 修复类提交只修改 .github 时退化为全仓库测试;
# push 场景保持原有 HEAD~1..HEAD 行为,兼容发布工作流。
if [ -n "${GITHUB_BASE_REF:-}" ]; then
git fetch origin "${GITHUB_BASE_REF}" --depth=1 >/dev/null 2>&1 || true
if git rev-parse --verify "origin/${GITHUB_BASE_REF}" >/dev/null 2>&1; then
diff_args=("origin/${GITHUB_BASE_REF}...HEAD")
else
diff_args=("HEAD~1" "HEAD")
fi
else
diff_args=("HEAD~1" "HEAD")
fi

modules=$(git diff --name-only "${diff_args[@]}" | while read -r file; do
dir=$(dirname "$file")
while [ "$dir" != "." ] && [ "$dir" != "/" ]; do
if [ -f "$dir/pom.xml" ]; then echo "$dir"; break; fi
dir=$(dirname "$dir")
done
done | sort -u | tr '\n' ',' | sed 's/,$//')
done | sort -u | paste -sd, -)

# 如果为空,则使用默认值 '.'
if [ -z "$modules" ]; then
echo "."
else
echo "$modules"
fi
fi
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
import java.beans.PropertyDescriptor;
import java.lang.reflect.Array;
import java.lang.reflect.Field;
import java.lang.reflect.RecordComponent;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
Expand All @@ -40,6 +41,8 @@
public final class FastBeanCopier {
private static final Map<CacheKey, Copier> CACHE = new ConcurrentHashMap<>();

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

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

private static final ConvertUtilsBean convertUtils = BeanUtilsBean.getInstance().getConvertUtils();
Expand Down Expand Up @@ -112,6 +115,9 @@ public static <T, S> T copy(S source, Supplier<T> target, String... ignore) {

@SneakyThrows
public static <T, S> T copy(S source, Class<T> target, String... ignore) {
if (target.isRecord()) {
return copyToRecord(source, target, DEFAULT_CONVERT, ignore);
}
return copy(source, target.newInstance(), DEFAULT_CONVERT, ignore);
}

Expand All @@ -125,6 +131,10 @@ public static <T, S> T copy(S source, T target, Set<String> ignore) {

@SuppressWarnings("all")
public static <T, S> T copy(S source, T target, Converter converter, Set<String> ignore) {
if (target != null && getUserClass(target).isRecord()) {
// record 不可变,无法写入已存在实例;这里按组件名重新构造并返回新实例。
return (T) copyToRecord(source, (Class) getUserClass(target), converter, ignore);
}
if (source instanceof Map && target instanceof Map) {
if (CollectionUtils.isEmpty(ignore)) {
((Map) target).putAll(((Map) source));
Expand All @@ -144,6 +154,45 @@ public static <T, S> T copy(S source, T target, Converter converter, Set<String>
return target;
}

static <T, S> T copyToRecord(S source, Class<T> target, Converter converter, String... ignore) {
Set<String> ignored = (ignore == null || ignore.length == 0)
? Collections.emptySet()
: new HashSet<>(Arrays.asList(ignore));
return copyToRecord(source, target, converter, ignored);
}

@SuppressWarnings("unchecked")
static <T, S> T copyToRecord(S source, Class<T> target, Converter converter, Set<String> ignore) {
return (T) getRecordCopier(getUserClass(source), target)
.copy(source, ignore == null ? Collections.emptySet() : ignore, converter);
}

private static RecordCopier getRecordCopier(Class<?> source, Class<?> target) {
CacheKey key = createCacheKey(source, target);
return RECORD_CACHE.computeIfAbsent(key, k -> createRecordCopier(k.sourceType, k.targetType));
}

private static RecordCopier createRecordCopier(Class<?> source, Class<?> target) {
String method = "public Object copy(Object s, java.util.Set ignore, " +
"org.hswebframework.web.bean.Converter converter){\n" +
"try{\n\t" +
createRecordCopierCode(source, target) +
"}catch(Throwable e){\n" +
"\tthrow new UnsupportedOperationException(e.getMessage(), e);" +
"\n}\n" +
"\n}";
try {
@SuppressWarnings("all")
Proxy<RecordCopier> proxy = Proxy
.create(RecordCopier.class, new Class[]{source, target})
.addMethod(method);
return proxy.newInstance();
} catch (Exception e) {
log.error("创建record copy代理对象失败:\n{}", method, e);
throw new UnsupportedOperationException(e.getMessage(), e);
}
}

static Class<?> getUserClass(Object object) {
if (object instanceof Map) {
return Map.class;
Expand Down Expand Up @@ -220,6 +269,10 @@ public static Copier createCopier(Class<?> source, Class<?> target) {

private static Map<String, ClassProperty> createProperty(Class<?> type) {

if (type.isRecord()) {
return createRecordProperty(type);
}

List<String> fieldNames = Arrays
.stream(type.getDeclaredFields())
.map(Field::getName)
Expand All @@ -236,6 +289,15 @@ private static Map<String, ClassProperty> createProperty(Class<?> type) {

}

private static Map<String, ClassProperty> createRecordProperty(Class<?> type) {
return Arrays.stream(type.getRecordComponents())
.map(RecordClassProperty::new)
.collect(Collectors.toMap(ClassProperty::getName,
Function.identity(),
(k, k2) -> k,
LinkedHashMap::new));
}

private static Map<String, ClassProperty> createMapProperty(Map<String, ClassProperty> template) {
return template
.values()
Expand All @@ -244,6 +306,69 @@ private static Map<String, ClassProperty> createMapProperty(Map<String, ClassPro
.collect(Collectors.toMap(ClassProperty::getName, Function.identity(), (k, k2) -> k, LinkedHashMap::new));
}

private static String createRecordCopierCode(Class<?> source, Class<?> target) {
Map<String, ClassProperty> sourceProperties = Map.class.isAssignableFrom(source)
? createMapProperty(createRecordProperty(target))
: createProperty(source);
String sourceTypeName = getTypeName(source);
String targetTypeName = getTypeName(target);
boolean sourceIsMap = Map.class.isAssignableFrom(source);
StringBuilder code = new StringBuilder();
code.append(sourceTypeName).append(" $$__source=(").append(sourceTypeName).append(")s;\n\t");

RecordComponent[] components = target.getRecordComponents();
List<String> constructorArgs = new ArrayList<>(components.length);
for (RecordComponent component : components) {
String name = component.getName();
Class<?> type = component.getType();
String typeName = getTypeName(type);
String varName = "$$__" + name;
code.append(typeName).append(" ").append(varName).append("=").append(defaultValueCode(type)).append(";\n\t");
code.append("if(!ignore.contains(\"").append(name).append("\")){\n\t\t");

ClassProperty sourceProperty = sourceProperties.get(name);
if (sourceProperty == null) {
code.append("// source property not found, keep default value.\n\t");
code.append("}\n\t");
constructorArgs.add(varName);
continue;
}

String valueExpression = sourceIsMap
? "$$__source.get(\"" + name + "\")"
: "$$__source." + sourceProperty.getReadMethod();
if (sourceProperty.isPrimitive()) {
valueExpression = wrapperClassMapping
.get(sourceProperty.getType())
.getName() + ".valueOf(" + valueExpression + ")";
}
String generic = resolveRecordGenericCode(component);
code.append("Object $$__value=(Object)(").append(valueExpression).append(");\n\t\t");
code.append("if($$__value!=null){\n\t\t\t");
if (requiresRecordGenericConversion(component, type)) {
code.append(varName).append("=").append(convertValueCode(type, generic)).append(";\n\t\t");
} else {
code.append("if(").append(directAssignableCode(type, "$$__value")).append("){\n\t\t\t");
code.append(varName).append("=").append(castValueCode(type, "$$__value")).append(";\n\t\t\t");
code.append("}else{\n\t\t\t");
code.append(varName).append("=").append(convertValueCode(type, generic)).append(";\n\t\t\t");
code.append("}\n\t\t");
}
code.append("}\n\t");
code.append("}\n\t");
constructorArgs.add(varName);
}
code.append("return new ").append(targetTypeName).append("(")
.append(String.join(",", constructorArgs))
.append(");\n");
return code.toString();
}

private static boolean requiresRecordGenericConversion(RecordComponent component, Class<?> type) {
return ResolvableType.forType(component.getGenericType()).hasGenerics()
&& (Collection.class.isAssignableFrom(type) || Map.class.isAssignableFrom(type));
}

private static String createCopierCode(Class<?> source, Class<?> target) {
Map<String, ClassProperty> sourceProperties = null;

Expand Down Expand Up @@ -521,6 +646,21 @@ public BeanClassProperty(PropertyDescriptor descriptor) {
}
}

static class RecordClassProperty extends ClassProperty {
public RecordClassProperty(RecordComponent component) {
type = component.getType();
readMethodName = component.getAccessor().getName();
writeMethodName = null;

getter = createGetterFunction();
setter = createSetterFunction(paramGetter -> {
throw new UnsupportedOperationException("Record property is read-only: " + component.getName());
});
name = component.getName();
beanType = component.getDeclaringRecord();
}
}

static class MapClassProperty extends ClassProperty {
public MapClassProperty(String name) {
type = Object.class;
Expand All @@ -545,6 +685,78 @@ public String getReadMethodName() {
}


private static String resolveRecordGenericCode(RecordComponent component) {
Class<?>[] genericTypes = Arrays.stream(ResolvableType.forType(component.getGenericType()).getGenerics())
.map(ResolvableType::getRawClass)
.filter(Objects::nonNull)
.toArray(Class[]::new);
if (genericTypes.length == 0) {
return "org.hswebframework.web.bean.FastBeanCopier.EMPTY_CLASS_ARRAY";
}
return "new Class[]{" + Arrays.stream(genericTypes)
.map(type -> getTypeName(type) + ".class")
.collect(Collectors.joining(",")) + "}";
}

private static String getTypeName(Class<?> type) {
if (type.isArray()) {
return getTypeName(type.getComponentType()) + "[]";
}
return type.getName();
}

private static String defaultValueCode(Class<?> type) {
if (!type.isPrimitive()) {
return "null";
}
if (type == boolean.class) {
return "false";
}
if (type == char.class) {
return "(char)0";
}
if (type == byte.class) {
return "(byte)0";
}
if (type == short.class) {
return "(short)0";
}
if (type == long.class) {
return "0L";
}
if (type == float.class) {
return "0F";
}
if (type == double.class) {
return "0D";
}
return "0";
}

private static String castValueCode(Class<?> type, String value) {
if (!type.isPrimitive()) {
return "(" + getTypeName(type) + ")" + value;
}
Class<?> wrapper = wrapperClassMapping.get(type);
return "((" + wrapper.getName() + ")" + value + ")." + type.getName() + "Value()";
}

private static String convertValueCode(Class<?> type, String generic) {
String converted = "converter.convert($$__value," + getTypeName(type) + ".class," + generic + ")";
if (!type.isPrimitive()) {
return "(" + getTypeName(type) + ")" + converted;
}
Class<?> wrapper = wrapperClassMapping.get(type);
return "((" + wrapper.getName() + ")" + converted + ")." + type.getName() + "Value()";
}

private static String directAssignableCode(Class<?> type, String value) {
Class<?> directType = type.isPrimitive()
? wrapperClassMapping.get(type)
: type;
return value + " instanceof " + getTypeName(directType);
}

public static final class DefaultConverter implements Converter {
private BeanFactory beanFactory = BEAN_FACTORY;

Expand Down Expand Up @@ -697,6 +909,9 @@ public <T> T convert(Object source, Class<T> targetClass, Class[] genericType) {
source = ((Date) source).getTime();
}
}
if (targetClass.isRecord()) {
return copyToRecord(source, targetClass, this, Collections.emptySet());
}
try {
org.apache.commons.beanutils.Converter converter = convertUtils.lookup(targetClass);
if (null != converter) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package org.hswebframework.web.bean;

import java.util.Set;

/**
* Record constructor copier.
* <p>
* Record is immutable and cannot reuse {@link Copier}'s in-place target mutation contract,
* so generated implementations return a newly constructed record instance.
*
* @author zhouhao
* @since 5.0.2
*/
public interface RecordCopier {

/**
* Copy source values into a new record instance by canonical constructor order.
*
* @param source source bean, record or map; generated implementations cast it to the cached source type
* @param ignore record component names to keep at Java default values
* @param converter value converter used when source and target component types are not directly assignable
* @return new record instance; never mutates an existing target record
*/
Object copy(Object source, Set<String> ignore, Converter converter);
}
Loading
Loading