diff --git a/aop/pom.xml b/aop/pom.xml index f9a2d41f9..35221ccd0 100644 --- a/aop/pom.xml +++ b/aop/pom.xml @@ -3,7 +3,7 @@ run.soeasy.framework framework - 1.0.0 + 1.0.1 aop aop diff --git a/aop/src/main/java/run/soeasy/framework/aop/Aop.java b/aop/src/main/java/run/soeasy/framework/aop/Aop.java index 5d42741da..f82ee17df 100644 --- a/aop/src/main/java/run/soeasy/framework/aop/Aop.java +++ b/aop/src/main/java/run/soeasy/framework/aop/Aop.java @@ -7,8 +7,8 @@ import run.soeasy.framework.core.RandomUtils; import run.soeasy.framework.core.StringUtils; import run.soeasy.framework.core.collection.ArrayUtils; -import run.soeasy.framework.core.collection.Elements; import run.soeasy.framework.core.execute.Execution; +import run.soeasy.framework.core.streaming.Streamable; /** * AOP核心组件,继承自{@link JdkProxyFactory},提供基于JDK动态代理的AOP功能实现, @@ -119,12 +119,12 @@ public Proxy getProxy(@NonNull Class sourceClass, Class[] interfaces, this.id); // 合并拦截器:标识拦截器 + 全局注册拦截器 + 自定义拦截器 - Elements executionInterceptors; + Streamable executionInterceptors; if (executionInterceptor == null) { - executionInterceptors = Elements.forArray(delegatedObjectExecutionInterceptor, + executionInterceptors = Streamable.array(delegatedObjectExecutionInterceptor, getExecutionInterceptorRegistry()); } else { - executionInterceptors = Elements.forArray(delegatedObjectExecutionInterceptor, + executionInterceptors = Streamable.array(delegatedObjectExecutionInterceptor, getExecutionInterceptorRegistry(), executionInterceptor); } @@ -177,12 +177,12 @@ public Execution getProxyFunction(@NonNull Execution execution, ExecutionInterce this.id); // 合并拦截器 - Elements executionInterceptors; + Streamable executionInterceptors; if (executionInterceptor == null) { - executionInterceptors = Elements.forArray(delegatedObjectExecutionInterceptor, + executionInterceptors = Streamable.array(delegatedObjectExecutionInterceptor, getExecutionInterceptorRegistry()); } else { - executionInterceptors = Elements.forArray(delegatedObjectExecutionInterceptor, + executionInterceptors = Streamable.array(delegatedObjectExecutionInterceptor, getExecutionInterceptorRegistry(), executionInterceptor); } @@ -225,11 +225,11 @@ public Proxy getProxy(@NonNull Class sourceClass, T source, Cla source); // 合并拦截器:切换目标拦截器 + 自定义拦截器 - Elements executionInterceptors; + Streamable executionInterceptors; if (executionInterceptor == null) { - executionInterceptors = Elements.forArray(switchableTargetExecutionInterceptor); + executionInterceptors = Streamable.array(switchableTargetExecutionInterceptor); } else { - executionInterceptors = Elements.forArray(switchableTargetExecutionInterceptor, executionInterceptor); + executionInterceptors = Streamable.array(switchableTargetExecutionInterceptor, executionInterceptor); } // 包装为拦截器集合并创建代理 diff --git a/aop/src/main/java/run/soeasy/framework/aop/DelegatedObjectExecutionInterceptor.java b/aop/src/main/java/run/soeasy/framework/aop/DelegatedObjectExecutionInterceptor.java index ac8d43c40..2c13db1f7 100644 --- a/aop/src/main/java/run/soeasy/framework/aop/DelegatedObjectExecutionInterceptor.java +++ b/aop/src/main/java/run/soeasy/framework/aop/DelegatedObjectExecutionInterceptor.java @@ -44,7 +44,7 @@ class DelegatedObjectExecutionInterceptor implements InvocationInterceptor, Seri @Override public Object intercept(@NonNull Invocation executor) throws Throwable { // 判断是否为获取代理容器ID的方法调用(方法名匹配且无参数) - if (executor.getMetadata().getParameterTemplate().isEmpty() + if (executor.getMetadata().getParameterMapping().isEmpty() && executor.getMetadata().getName().equals(DelegatedObject.PROXY_CONTAINER_ID_METHOD_NAME)) { return id; // 返回预设的代理容器ID } diff --git a/aop/src/main/java/run/soeasy/framework/aop/ExecutionInterceptorRegistry.java b/aop/src/main/java/run/soeasy/framework/aop/ExecutionInterceptorRegistry.java index e544ebfc0..a7c294ddf 100644 --- a/aop/src/main/java/run/soeasy/framework/aop/ExecutionInterceptorRegistry.java +++ b/aop/src/main/java/run/soeasy/framework/aop/ExecutionInterceptorRegistry.java @@ -3,6 +3,7 @@ import lombok.NonNull; import run.soeasy.framework.core.execute.Execution; import run.soeasy.framework.core.spi.ConfigurableServices; +import run.soeasy.framework.core.spi.ServiceComparator; /** * 执行拦截器注册中心,继承自{@link ConfigurableServices}并实现{@link ExecutionInterceptor}接口, @@ -30,7 +31,7 @@ public class ExecutionInterceptorRegistry extends ConfigurableServices该类持有一个拦截器元素集合({@link Elements}),在拦截执行时,通过创建{@link ExecutionInterceptorChain} + *

该类持有一个拦截器元素集合({@link Streamable}),在拦截执行时,通过创建{@link ExecutionInterceptorChain} * 将拦截器集合转换为拦截链,按迭代顺序依次执行每个拦截器,最终触发{@code nextChain}(目标执行逻辑), * 实现多拦截器的有序增强。 * * @author soeasy.run * @see ExecutionInterceptor * @see ExecutionInterceptorChain - * @see Elements + * @see Streamable */ @Data public class ExecutionInterceptors implements ExecutionInterceptor { @@ -25,7 +25,7 @@ public class ExecutionInterceptors implements ExecutionInterceptor { * 拦截器元素集合(非空),包含需要按序执行的拦截器 */ @NonNull - private final Elements executionInterceptors; + private final Streamable executionInterceptors; /** * 拦截链执行完毕后触发的下一个执行节点(通常为目标业务逻辑) @@ -47,7 +47,7 @@ public class ExecutionInterceptors implements ExecutionInterceptor { @Override public Object intercept(@NonNull Execution function) throws Throwable { // 创建拦截器链,关联当前拦截器集合与下一个执行节点 - ExecutionInterceptorChain chain = new ExecutionInterceptorChain(executionInterceptors.iterator(), nextChain); + ExecutionInterceptorChain chain = new ExecutionInterceptorChain(executionInterceptors.toList().iterator(), nextChain); // 执行拦截链 return chain.intercept(function); } diff --git a/aop/src/main/java/run/soeasy/framework/aop/ProxyFactories.java b/aop/src/main/java/run/soeasy/framework/aop/ProxyFactories.java index 9cfccd930..9d86539d7 100644 --- a/aop/src/main/java/run/soeasy/framework/aop/ProxyFactories.java +++ b/aop/src/main/java/run/soeasy/framework/aop/ProxyFactories.java @@ -2,6 +2,7 @@ import lombok.NonNull; import run.soeasy.framework.core.spi.ConfigurableServices; +import run.soeasy.framework.core.spi.ServiceComparator; import run.soeasy.framework.core.type.ClassUtils; /** @@ -23,7 +24,7 @@ public class ProxyFactories extends ConfigurableServices implement * 构造代理工厂集合,指定服务类为{@link ProxyFactory} */ public ProxyFactories() { - setServiceClass(ProxyFactory.class); + super(ServiceComparator.defaultServiceComparator()); } /** diff --git a/beans/pom.xml b/beans/pom.xml index 118a8218c..774b1e1f9 100644 --- a/beans/pom.xml +++ b/beans/pom.xml @@ -3,7 +3,7 @@ run.soeasy.framework framework - 1.0.0 + 1.0.1 beans beans diff --git a/beans/src/main/java/run/soeasy/framework/beans/BeanFormat.java b/beans/src/main/java/run/soeasy/framework/beans/BeanFormat.java index 081c692d0..206253332 100644 --- a/beans/src/main/java/run/soeasy/framework/beans/BeanFormat.java +++ b/beans/src/main/java/run/soeasy/framework/beans/BeanFormat.java @@ -5,10 +5,7 @@ import lombok.Setter; import run.soeasy.framework.codec.Codec; import run.soeasy.framework.codec.format.KeyValueFormat; -import run.soeasy.framework.core.convert.value.TypedValueAccessor; import run.soeasy.framework.core.domain.KeyValue; -import run.soeasy.framework.core.transform.property.TypedProperties; -import run.soeasy.framework.core.transform.templates.Mapping; /** * 基于JavaBean的键值格式化器,继承自{@link KeyValueFormat},专门用于将JavaBean对象与键值对格式(如查询字符串、表单数据)进行互转, @@ -46,13 +43,7 @@ public BeanFormat(@NonNull CharSequence delimiter, @NonNull CharSequence connect // 注册Object类型的映射器:将任意Bean转换为键值对流 getKeyValueMapper().getMappingProvider().register(Object.class, (bean, type) -> { // 通过BeanMapper获取Bean的类型化属性集合 - TypedProperties typedProperties = BeanUtils.getProperties(bean, type); - - // 定义Bean到键值对流的映射逻辑 - Mapping mapping = () -> typedProperties.getElements() - .map(property -> KeyValue.of(property.getKey(), property.getValue())); - - return mapping; + return BeanUtils.getProperties(bean, type); }); } } \ No newline at end of file diff --git a/beans/src/main/java/run/soeasy/framework/beans/BeanInfoFactory.java b/beans/src/main/java/run/soeasy/framework/beans/BeanInfoFactory.java index 463a046b9..d3364cc3a 100644 --- a/beans/src/main/java/run/soeasy/framework/beans/BeanInfoFactory.java +++ b/beans/src/main/java/run/soeasy/framework/beans/BeanInfoFactory.java @@ -4,21 +4,21 @@ import java.beans.IntrospectionException; import lombok.NonNull; -import run.soeasy.framework.core.transform.property.ObjectTemplateFactory; -import run.soeasy.framework.core.transform.property.PropertyTemplate; +import run.soeasy.framework.core.mapping.property.ObjectTemplateFactory; +import run.soeasy.framework.core.mapping.property.PropertyMapping; /** * Bean信息工厂接口,继承自{@link ObjectTemplateFactory},定义获取JavaBean信息({@link BeanInfo})和创建Bean属性模板的规范, * 作为JavaBean内省机制的抽象,提供Bean信息的统一获取方式,适配不同的Bean信息解析策略。 * *

- * 该接口结合内省机制,既可以直接获取{@link BeanInfo}用于自定义处理,也可以通过默认方法创建{@link BeanPropertyTemplate}, + * 该接口结合内省机制,既可以直接获取{@link BeanInfo}用于自定义处理,也可以通过默认方法创建{@link BeanInfoMapping}, * 简化Bean属性元数据的访问与管理流程。 * * @author soeasy.run * @see ObjectTemplateFactory * @see BeanInfo - * @see BeanPropertyTemplate + * @see BeanInfoMapping */ public interface BeanInfoFactory extends ObjectTemplateFactory { @@ -27,7 +27,7 @@ public interface BeanInfoFactory extends ObjectTemplateFactory { * *

* 通过内省机制解析Bean类,获取包含属性、方法、事件等信息的{@link BeanInfo}对象, - * 是后续创建{@link BeanProperty}和{@link BeanPropertyTemplate}的基础。 + * 是后续创建{@link BeanProperty}和{@link BeanInfoMapping}的基础。 * * @param beanClass 目标Bean类(非空) * @return 包含Bean元信息的{@link BeanInfo}对象 @@ -39,14 +39,14 @@ public interface BeanInfoFactory extends ObjectTemplateFactory { * 创建指定Bean类的属性模板(默认实现) * *

- * 基于当前{@link BeanInfoFactory}实例和目标Bean类,创建{@link BeanPropertyTemplate}, + * 基于当前{@link BeanInfoFactory}实例和目标Bean类,创建{@link BeanInfoMapping}, * 实现{@link ObjectTemplateFactory}接口的规范方法,提供Bean属性模板的默认创建逻辑。 * * @param objectClass 目标Bean类(即JavaBean的类对象) - * @return 针对该Bean类的{@link BeanPropertyTemplate}实例 + * @return 针对该Bean类的{@link BeanInfoMapping}实例 */ @Override - default PropertyTemplate getObjectTemplate(Class objectClass) { - return new BeanPropertyTemplate(objectClass, this); + default PropertyMapping getObjectTemplate(Class objectClass) { + return new BeanInfoMapping(objectClass, this).standard(); } } \ No newline at end of file diff --git a/beans/src/main/java/run/soeasy/framework/beans/BeanInfoMapping.java b/beans/src/main/java/run/soeasy/framework/beans/BeanInfoMapping.java new file mode 100644 index 000000000..c2c445f1a --- /dev/null +++ b/beans/src/main/java/run/soeasy/framework/beans/BeanInfoMapping.java @@ -0,0 +1,44 @@ +package run.soeasy.framework.beans; + +import java.beans.BeanInfo; +import java.beans.IntrospectionException; +import java.util.stream.Stream; + +import lombok.NonNull; +import run.soeasy.framework.core.mapping.property.PropertyMapping; +import run.soeasy.framework.core.mapping.property.StreamablePropertyMapping; +import run.soeasy.framework.core.streaming.Streamable; +import run.soeasy.framework.core.type.ReflectionUtils; + +public class BeanInfoMapping extends StreamablePropertyMapping { + + public BeanInfoMapping(@NonNull Streamable elements) { + super(elements); + } + + public BeanInfoMapping(@NonNull Class beanClass, @NonNull BeanInfo beanInfo) { + this(Streamable.array(beanInfo.getPropertyDescriptors()) + .map((descriptor) -> new BeanProperty(beanClass, descriptor))); + } + + protected BeanInfoMapping(@NonNull Class beanClass, @NonNull BeanInfoFactory beanInfoFactory) { + this(Streamable.of(() -> { + BeanInfo beanInfo; + try { + beanInfo = beanInfoFactory.getBeanInfo(beanClass); + } catch (IntrospectionException e) { + throw new FatalBeanException("Failed to obtain BeanInfo for class [" + beanClass + "]", e); + } + // 将属性描述符转换为BeanProperty,并过滤忽略的属性 + return Stream.of(beanInfo.getPropertyDescriptors()) + .map((descriptor) -> new BeanProperty(beanClass, descriptor)).iterator(); + })); + } + + public PropertyMapping standard() { + return new BeanInfoMapping(this.elements().filter((property) -> { + return property.getReadMethod() != null && property.getReadMethod().getSource() != null + && !ReflectionUtils.isObjectMethod(property.getReadMethod().getSource()); + })).toMultiMapped(); + } +} diff --git a/beans/src/main/java/run/soeasy/framework/beans/BeanMapper.java b/beans/src/main/java/run/soeasy/framework/beans/BeanMapper.java index 90fbe73fb..90266b0d5 100644 --- a/beans/src/main/java/run/soeasy/framework/beans/BeanMapper.java +++ b/beans/src/main/java/run/soeasy/framework/beans/BeanMapper.java @@ -1,7 +1,7 @@ package run.soeasy.framework.beans; import lombok.Getter; -import run.soeasy.framework.core.transform.property.PropertyMapper; +import run.soeasy.framework.core.mapping.property.PropertyMapper; /** * Bean映射工具类,继承自{@link PropertyMapper},专门用于处理JavaBean之间的属性复制与转换, diff --git a/beans/src/main/java/run/soeasy/framework/beans/BeanPropertyTemplate.java b/beans/src/main/java/run/soeasy/framework/beans/BeanPropertyTemplate.java deleted file mode 100644 index f8619f0d8..000000000 --- a/beans/src/main/java/run/soeasy/framework/beans/BeanPropertyTemplate.java +++ /dev/null @@ -1,71 +0,0 @@ -package run.soeasy.framework.beans; - -import java.beans.BeanInfo; -import java.beans.IntrospectionException; -import java.util.stream.Stream; - -import lombok.NonNull; -import run.soeasy.framework.core.transform.property.MapPropertyTemplate; -import run.soeasy.framework.core.transform.property.PropertyTemplate; -import run.soeasy.framework.core.type.ReflectionUtils; - -/** - * 基于JavaBean的属性模板实现,继承自{@link MapPropertyTemplate},实现{@link PropertyTemplate}接口, - * 用于管理和访问JavaBean的属性信息(封装为{@link BeanProperty}),支持通过JavaBean内省机制获取属性元数据, - * 并过滤掉无需处理的基础属性(如Object类的方法对应的属性)。 - * - *

该类通过{@link BeanInfoFactory}获取JavaBean的{@link BeanInfo},将属性描述符转换为{@link BeanProperty}对象, - * 构建属性模板集合,适用于属性映射、数据转换、反射操作等场景。 - * - * @author soeasy.run - * @see MapPropertyTemplate - * @see PropertyTemplate - * @see BeanProperty - * @see BeanInfo - */ -class BeanPropertyTemplate extends MapPropertyTemplate> - implements PropertyTemplate { - - /** - * 构造Bean属性模板(基于指定的Bean类和Bean信息工厂) - * - *

通过{@link BeanInfoFactory}获取目标Bean类的{@link BeanInfo},解析其中的属性描述符并转换为{@link BeanProperty}, - * 同时过滤掉需要忽略的属性(如Object类自带方法对应的属性),完成属性模板的初始化。 - * - * @param beanClass 目标JavaBean的类对象(非空) - * @param beanInfoFactory 用于获取Bean信息的工厂(非空) - * @throws FatalBeanException 当获取BeanInfo失败时抛出(包装{@link IntrospectionException}) - */ - public BeanPropertyTemplate(@NonNull Class beanClass, BeanInfoFactory beanInfoFactory) { - super(() -> { - BeanInfo beanInfo; - try { - beanInfo = beanInfoFactory.getBeanInfo(beanClass); - } catch (IntrospectionException e) { - throw new FatalBeanException("Failed to obtain BeanInfo for class [" + beanClass + "]", e); - } - // 将属性描述符转换为BeanProperty,并过滤忽略的属性 - return Stream.of(beanInfo.getPropertyDescriptors()) - .map((descriptor) -> new BeanProperty(beanClass, descriptor)) - .filter((property) -> !isIgnoreProperty(property)) - .iterator(); - }, false); - } - - /** - * 判断是否为需要忽略的属性 - * - *

忽略规则:若属性的读方法(getter)是Object类中定义的方法(如{@link Object#getClass()}),则视为需要忽略的属性, - * 避免处理JavaBean继承自Object的基础属性。 - * - * @param property 待判断的Bean属性 - * @return 是需要忽略的属性则返回true,否则返回false - */ - private static boolean isIgnoreProperty(BeanProperty property) { - if (property.getReadMethod() != null && property.getReadMethod().getSource() != null - && ReflectionUtils.isObjectMethod(property.getReadMethod().getSource())) { - return true; - } - return false; - } -} \ No newline at end of file diff --git a/beans/src/main/java/run/soeasy/framework/beans/BeanUtils.java b/beans/src/main/java/run/soeasy/framework/beans/BeanUtils.java index 2ba45ffe4..410adedff 100644 --- a/beans/src/main/java/run/soeasy/framework/beans/BeanUtils.java +++ b/beans/src/main/java/run/soeasy/framework/beans/BeanUtils.java @@ -5,9 +5,9 @@ import lombok.NonNull; import lombok.experimental.UtilityClass; import run.soeasy.framework.core.convert.TypeDescriptor; -import run.soeasy.framework.core.transform.property.PropertyMappingFilter; -import run.soeasy.framework.core.transform.property.PropertyTemplate; -import run.soeasy.framework.core.transform.property.TypedProperties; +import run.soeasy.framework.core.mapping.property.PropertyAccessor; +import run.soeasy.framework.core.mapping.property.PropertyMapping; +import run.soeasy.framework.core.mapping.property.PropertyMappingFilter; /** * Bean 操作工具类:基于 {@link BeanMapper} 封装 Bean 属性核心操作,提供「属性复制、属性提取、属性模板获取」三大核心能力, @@ -17,17 +17,17 @@ * 1. 线程安全:通过双重检查锁单例模式初始化 {@link BeanMapper},保证多线程环境下实例唯一性; * 2. 灵活扩展:支持自定义 {@link PropertyMappingFilter} 过滤属性或自定义映射规则; * 3. 类型适配:支持显式指定源/目标类(应对多态场景)和自动推断类型(简化常规调用); - * 4. 元数据支持:提供属性模板({@link PropertyTemplate})和带类型信息的属性集合({@link TypedProperties})提取能力。 + * 4. 元数据支持:提供属性模板({@link PropertyMapping})和带类型信息的属性集合({@link PropertyMapping})提取能力。 *

* 依赖说明: * - 核心依赖:{@link BeanMapper}(属性映射核心实现,所有操作最终委托其执行); * - 扩展依赖:{@link PropertyMappingFilter}(属性映射过滤/自定义)、{@link TypeDescriptor}(类型描述); - * - 元数据依赖:{@link TypedProperties}(带类型的属性集合)、{@link PropertyTemplate}(属性模板)。 + * - 元数据依赖:{@link PropertyMapping}(带类型的属性集合)、{@link PropertyMapping}(属性模板)。 * * @author soeasy.run * @see BeanMapper 属性映射核心实现类(具体执行策略由其定义) - * @see TypedProperties 带类型信息的属性集合 - * @see PropertyTemplate Bean 属性元数据模板 + * @see PropertyMapping 带类型信息的属性集合 + * @see PropertyMapping Bean 属性元数据模板 */ @UtilityClass public class BeanUtils { @@ -103,11 +103,11 @@ public static BeanMapper getBeanMapper() { * 自动通过 Bean 实例生成 {@link TypeDescriptor},具体属性提取规则由 {@link BeanMapper} 实现类定义。 * * @param bean 待提取属性的 Bean 对象(非空,具体支持的对象类型由 {@link BeanMapper} 定义) - * @return 带类型信息的属性集合 {@link TypedProperties},具体返回格式由 {@link BeanMapper#getMapping} 定义 + * @return 带类型信息的属性集合 {@link PropertyMapping},具体返回格式由 {@link BeanMapper#getMapping} 定义 * @throws NullPointerException 若 bean 为 null - * @see TypedProperties 带类型信息的属性集合(支持类型安全的属性取值) + * @see PropertyMapping 带类型信息的属性集合(支持类型安全的属性取值) */ - public static TypedProperties getProperties(@NonNull Object bean) { + public static PropertyMapping getProperties(@NonNull Object bean) { return getProperties(bean, TypeDescriptor.forObject(bean)); } @@ -119,11 +119,11 @@ public static TypedProperties getProperties(@NonNull Object bean) { * * @param bean 待提取属性的 Bean 对象(具体支持的空值处理、对象类型由 {@link BeanMapper} 定义) * @param typeDescriptor 类型描述器(非空,用于 {@link BeanMapper} 精确解析属性元数据,支持泛型、参数化类型) - * @return 带类型信息的属性集合 {@link TypedProperties},具体返回格式由 {@link BeanMapper#getMapping} 定义 + * @return 带类型信息的属性集合 {@link PropertyMapping},具体返回格式由 {@link BeanMapper#getMapping} 定义 * @throws NullPointerException 若 typeDescriptor 为 null * @see TypeDescriptor 类型描述器(封装类类型、泛型信息等) */ - public static TypedProperties getProperties(Object bean, @NonNull TypeDescriptor typeDescriptor) { + public static PropertyMapping getProperties(Object bean, @NonNull TypeDescriptor typeDescriptor) { return getBeanMapper().getMapping(bean, typeDescriptor); } @@ -134,12 +134,12 @@ public static TypedProperties getProperties(Object bean, @NonNull TypeDescriptor * 具体元数据解析规则、缓存策略由 {@link BeanMapper} 实现类定义。 * * @param beanClass Bean 的类对象(非空,具体支持的类类型由 {@link BeanMapper} 定义) - * @return Bean 的属性模板 {@link PropertyTemplate},具体模板内容由 {@link BeanMapper#getObjectTemplate} 定义 + * @return Bean 的属性模板 {@link PropertyMapping},具体模板内容由 {@link BeanMapper#getObjectTemplate} 定义 * @throws NullPointerException 若 beanClass 为 null - * @see PropertyTemplate 属性模板(缓存 Bean 的属性元数据,提升重复操作性能) + * @see PropertyMapping 属性模板(缓存 Bean 的属性元数据,提升重复操作性能) * @see BeanProperty 单个属性的元数据(包含属性名、类型、getter/setter 等) */ - public static PropertyTemplate getTemplate(@NonNull Class beanClass) { + public static PropertyMapping getTemplate(@NonNull Class beanClass) { return getBeanMapper().getObjectTemplate(beanClass); } } \ No newline at end of file diff --git a/beans/src/main/java/run/soeasy/framework/beans/ConfigurableBeanInfoFactory.java b/beans/src/main/java/run/soeasy/framework/beans/ConfigurableBeanInfoFactory.java index c5c03a934..1198fc398 100644 --- a/beans/src/main/java/run/soeasy/framework/beans/ConfigurableBeanInfoFactory.java +++ b/beans/src/main/java/run/soeasy/framework/beans/ConfigurableBeanInfoFactory.java @@ -6,6 +6,7 @@ import lombok.NonNull; import run.soeasy.framework.core.spi.ConfigurableServices; +import run.soeasy.framework.core.spi.ServiceComparator; /** * 可配置的Bean信息工厂,继承自{@link ConfigurableServices}并实现{@link BeanInfoFactory}, @@ -26,6 +27,7 @@ class ConfigurableBeanInfoFactory extends ConfigurableServices * 用于后续通过SPI机制加载该接口的实现类。 */ ConfigurableBeanInfoFactory() { + super(ServiceComparator.defaultServiceComparator()); setServiceClass(BeanInfoFactory.class); } diff --git a/beans/src/test/java/run/soeasy/framewrok/beans/BeanFormatTest.java b/beans/src/test/java/run/soeasy/framewrok/beans/BeanFormatTest.java new file mode 100644 index 000000000..3cd3c5598 --- /dev/null +++ b/beans/src/test/java/run/soeasy/framewrok/beans/BeanFormatTest.java @@ -0,0 +1,322 @@ +package run.soeasy.framewrok.beans; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +import java.io.StringReader; +import java.io.StringWriter; +import java.util.Arrays; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +import org.junit.Before; +import org.junit.Test; + +import run.soeasy.framework.beans.BeanFormat; +import run.soeasy.framework.codec.Codec; +import run.soeasy.framework.codec.CodecException; +import run.soeasy.framework.core.collection.LinkedMultiValueMap; +import run.soeasy.framework.core.collection.MultiValueMap; +import run.soeasy.framework.core.convert.TypeDescriptor; +import run.soeasy.framework.core.domain.KeyValue; +import run.soeasy.framework.core.streaming.Mapping; + +/** + * KeyValueFormat 核心功能单元测试(JUnit4 原生断言 + 流程打印) + * 覆盖:多值处理、对象<->字符串转换、编解码、边界场景 + * + * @author soeasy.run + */ +public class BeanFormatTest { + + // 测试基础参数:分隔符&,连接符=,透传编解码器(无实际编解码) + private static final CharSequence DELIMITER = "&"; + private static final CharSequence CONNECTOR = "="; + private static final Codec PASSTHROUGH_CODEC = new Codec() { + @Override + public String encode(String source) throws CodecException { + return source; + } + + @Override + public String decode(String source) throws CodecException { + return source; + } + }; + + // 测试实例 + private BeanFormat keyValueFormat; + + /** + * 每个测试方法执行前初始化 + */ + @Before + public void setUp() { + keyValueFormat = new BeanFormat(DELIMITER, CONNECTOR, PASSTHROUGH_CODEC, PASSTHROUGH_CODEC); + System.out.println("====================================="); + System.out.println("测试环境初始化完成,KeyValueFormat 实例已创建"); + System.out.println("分隔符:" + DELIMITER + ",连接符:" + CONNECTOR); + System.out.println("====================================="); + } + + // ------------------------------ join方法测试(多值处理) ------------------------------ + @Test + public void testJoin_IterableValue() throws Exception { + System.out.println("\n【开始测试】testJoin_IterableValue(Iterable类型多值拼接)"); + // 测试Iterable类型值(List) + KeyValue> element = KeyValue.of("hobby", Arrays.asList("reading", "coding")); + StringWriter writer = new StringWriter(); + System.out.println("准备测试数据:键=hobby,值=List[reading, coding]"); + + long count = keyValueFormat.join(writer, 0, element); + System.out.println("执行join方法结果:拼接字符串=" + writer.toString() + ",计数=" + count); + + // 验证:拼接结果为 "hobby=reading&hobby=coding",计数为2 + assertEquals("hobby=reading&hobby=coding", writer.toString()); + assertEquals(2, count); + System.out.println("【验证通过】testJoin_IterableValue 测试完成"); + } + + @Test + public void testJoin_ArrayValue() throws Exception { + System.out.println("\n【开始测试】testJoin_ArrayValue(数组类型多值拼接)"); + // 测试数组类型值 + KeyValue element = KeyValue.of("age", new Integer[]{18, 20}); + StringWriter writer = new StringWriter(); + System.out.println("准备测试数据:键=age,值=Integer[18, 20]"); + + long count = keyValueFormat.join(writer, 0, element); + System.out.println("执行join方法结果:拼接字符串=" + writer.toString() + ",计数=" + count); + + // 验证:拼接结果为 "age=18&age=20",计数为2 + assertEquals("age=18&age=20", writer.toString()); + assertEquals(2, count); + System.out.println("【验证通过】testJoin_ArrayValue 测试完成"); + } + + @Test + public void testJoin_SingleValue() throws Exception { + System.out.println("\n【开始测试】testJoin_SingleValue(普通单值拼接)"); + // 测试普通单值 + KeyValue element = KeyValue.of("name", "test"); + StringWriter writer = new StringWriter(); + System.out.println("准备测试数据:键=name,值=test"); + + long count = keyValueFormat.join(writer, 0, element); + System.out.println("执行join方法结果:拼接字符串=" + writer.toString() + ",计数=" + count); + + // 验证:拼接结果为 "name=test",计数为1 + assertEquals("name=test", writer.toString()); + assertEquals(1, count); + System.out.println("【验证通过】testJoin_SingleValue 测试完成"); + } + + // ------------------------------ to方法测试(对象→字符串) ------------------------------ + @Test + public void testTo_MapWithMultiValue() throws Exception { + System.out.println("\n【开始测试】testTo_MapWithMultiValue(Map转键值对字符串)"); + // 测试含多值的Map转换为字符串 + Map sourceMap = new LinkedHashMap<>(); + sourceMap.put("name", "admin"); + sourceMap.put("roles", Arrays.asList("admin", "user")); + sourceMap.put("age", 25); + System.out.println("准备测试数据:Map=" + sourceMap); + + StringWriter writer = new StringWriter(); + TypeDescriptor sourceType = TypeDescriptor.forObject(sourceMap); + keyValueFormat.to(sourceMap, sourceType, writer); + System.out.println("执行to方法结果:拼接字符串=" + writer.toString()); + + // 验证:拼接结果符合顺序(LinkedHashMap保证) + String expected = "name=admin&roles=admin&roles=user&age=25"; + assertEquals(expected, writer.toString()); + System.out.println("【验证通过】testTo_MapWithMultiValue 测试完成(预期结果=" + expected + ")"); + } + + // ------------------------------ from方法测试(字符串→对象) ------------------------------ + @Test + @SuppressWarnings("unchecked") + public void testFrom_StringToMap() throws Exception { + System.out.println("\n【开始测试】testFrom_StringToMap(键值对字符串转多值Map)"); + // 测试键值对字符串转换为多值Map + String sourceStr = "fruit=apple&fruit=banana&color=red"; + StringReader reader = new StringReader(sourceStr); + TypeDescriptor targetType = TypeDescriptor.map(Map.class, String.class, List.class); + System.out.println("准备测试数据:键值对字符串=" + sourceStr); + + Map> resultMap = (Map>) keyValueFormat.from(reader, targetType); + System.out.println("执行from方法结果:Map=" + resultMap); + + // 验证:多值键解析正确 + List fruitList = resultMap.get("fruit"); + assertEquals(2, fruitList.size()); + assertEquals("apple", fruitList.get(0)); + assertEquals("banana", fruitList.get(1)); + System.out.println("验证多值键fruit:" + fruitList + "(符合预期)"); + + // 验证:单值键解析正确 + List colorList = resultMap.get("color"); + assertEquals(1, colorList.size()); + assertEquals("red", colorList.get(0)); + System.out.println("验证单值键color:" + colorList + "(符合预期)"); + + // 验证Map大小 + assertEquals(2, resultMap.size()); + System.out.println("【验证通过】testFrom_StringToMap 测试完成(Map大小=" + resultMap.size() + ")"); + } + + @Test + public void testFrom_StringToPojo() throws Exception { + System.out.println("\n【开始测试】testFrom_StringToPojo(键值对字符串转POJO)"); + // 测试字符串转换为自定义POJO + String sourceStr = "username=test&hobbies=reading&hobbies=coding&age=20"; + StringReader reader = new StringReader(sourceStr); + TypeDescriptor targetType = TypeDescriptor.forType(User.class); + System.out.println("准备测试数据:键值对字符串=" + sourceStr); + + User user = (User) keyValueFormat.from(reader, targetType); + System.out.println("执行from方法结果:User=" + user.getUsername() + ", hobbies=" + user.getHobbies() + ", age=" + user.getAge()); + + // 验证:POJO基础字段赋值正确 + assertNotNull(user); + assertEquals("test", user.getUsername()); + assertEquals(Integer.valueOf(20), user.getAge()); + System.out.println("验证基础字段:username=" + user.getUsername() + ",age=" + user.getAge() + "(符合预期)"); + + // 验证:多值List字段赋值正确 + List hobbies = user.getHobbies(); + assertEquals(2, hobbies.size()); + assertEquals("reading", hobbies.get(0)); + assertEquals("coding", hobbies.get(1)); + System.out.println("验证多值字段hobbies:" + hobbies + "(符合预期)"); + System.out.println("【验证通过】testFrom_StringToPojo 测试完成"); + } + + // ------------------------------ encode/decode方法测试(编解码) ------------------------------ + @Test + public void testEncode_Mapping() throws CodecException { + System.out.println("\n【开始测试】testEncode_Mapping(Mapping编码为字符串)"); + // 测试Mapping编码为字符串 + MultiValueMap multiValueMap = new LinkedMultiValueMap<>(); + multiValueMap.add("k1", "v1"); + multiValueMap.add("k1", "v2"); + multiValueMap.add("k2", "v3"); + Mapping mapping = Mapping.ofMultiMapped(multiValueMap); + System.out.println("准备测试数据:MultiValueMap=" + multiValueMap); + + String encoded = keyValueFormat.encode(mapping); + System.out.println("执行encode方法结果:编码字符串=" + encoded); + + // 验证:编码结果正确 + assertEquals("k1=v1&k1=v2&k2=v3", encoded); + System.out.println("【验证通过】testEncode_Mapping 测试完成(预期结果=" + encoded + ")"); + } + + @Test + public void testDecode_StringToMapping() throws CodecException { + System.out.println("\n【开始测试】testDecode_StringToMapping(字符串解码为Mapping)"); + // 测试字符串解码为Mapping + String sourceStr = "k1=v1&k1=v2&k2=v3"; + System.out.println("准备测试数据:解码字符串=" + sourceStr); + + Mapping mapping = keyValueFormat.decode(sourceStr); + System.out.println("执行decode方法结果:Mapping集合=" + mapping); + // 验证:多值键解码正确 + List k1Values = mapping.getValues("k1").toList(); + assertEquals(2, k1Values.size()); + assertEquals("v1", k1Values.get(0)); + assertEquals("v2", k1Values.get(1)); + System.out.println("验证多值键k1:" + k1Values + "(符合预期)"); + + // 验证:单值键解码正确 + List k2Values = mapping.getValues("k2").toList(); + assertEquals(1, k2Values.size()); + assertEquals("v3", k2Values.get(0)); + System.out.println("验证单值键k2:" + k2Values + "(符合预期)"); + + // 验证键的顺序(LinkedHashMap保证) + System.out.println(mapping.keys().toList()); + Iterator keyIterator = mapping.keys().toList().iterator(); + assertEquals("k1", keyIterator.next()); + assertEquals("k2", keyIterator.next()); + assertFalse(keyIterator.hasNext()); + System.out.println("验证键顺序:k1 → k2(符合预期)"); + System.out.println("【验证通过】testDecode_StringToMapping 测试完成"); + } + + // ------------------------------ 边界场景测试 ------------------------------ + @Test + @SuppressWarnings("unchecked") + public void testEdge_EmptyString() throws Exception { + System.out.println("\n【开始测试】testEdge_EmptyString(空字符串转换)"); + // 测试空字符串转换为Map + StringReader reader = new StringReader(""); + TypeDescriptor targetType = TypeDescriptor.map(Map.class, String.class, List.class); + System.out.println("准备测试数据:空字符串"); + + Map> resultMap = (Map>) keyValueFormat.from(reader, targetType); + System.out.println("执行from方法结果:Map=" + resultMap); + + assertTrue(resultMap.isEmpty()); + System.out.println("【验证通过】testEdge_EmptyString 测试完成(Map为空)"); + } + + @Test + public void testEdge_NullValue() throws Exception { + System.out.println("\n【开始测试】testEdge_NullValue(null值拼接)"); + // 测试值为null的场景 + Map sourceMap = new LinkedHashMap<>(); + sourceMap.put("nullKey", null); + System.out.println("准备测试数据:Map={nullKey: null}"); + + StringWriter writer = new StringWriter(); + keyValueFormat.to(sourceMap, TypeDescriptor.forObject(sourceMap), writer); + System.out.println("执行to方法结果:拼接字符串=" + writer.toString()); + + // 验证:null值拼接为 "nullKey=" + assertEquals("nullKey=", writer.toString()); + System.out.println("【验证通过】testEdge_NullValue 测试完成(预期结果=nullKey=)"); + } + + // ------------------------------ 自定义测试POJO ------------------------------ + /** + * 测试用POJO(用于from方法转换测试) + */ + public static class User { + private String username; + private List hobbies; + private Integer age; + + // 必须提供无参构造(类型转换要求) + public User() {} + + // Getter & Setter + public String getUsername() { + return username; + } + + public void setUsername(String username) { + this.username = username; + } + + public List getHobbies() { + return hobbies; + } + + public void setHobbies(List hobbies) { + this.hobbies = hobbies; + } + + public Integer getAge() { + return age; + } + + public void setAge(Integer age) { + this.age = age; + } + } +} \ No newline at end of file diff --git a/beans/src/test/java/run/soeasy/framewrok/beans/BeanUtilsTest.java b/beans/src/test/java/run/soeasy/framewrok/beans/BeanUtilsTest.java index da765ca66..82fde69c8 100644 --- a/beans/src/test/java/run/soeasy/framewrok/beans/BeanUtilsTest.java +++ b/beans/src/test/java/run/soeasy/framewrok/beans/BeanUtilsTest.java @@ -6,7 +6,7 @@ import lombok.Data; import run.soeasy.framework.beans.BeanUtils; -import run.soeasy.framework.core.transform.property.PropertyMappingFilter; +import run.soeasy.framework.core.mapping.property.PropertyMappingFilter; public class BeanUtilsTest { @Data diff --git a/core/pom.xml b/core/pom.xml index aa22d0ce8..19aaa9385 100644 --- a/core/pom.xml +++ b/core/pom.xml @@ -4,7 +4,7 @@ run.soeasy.framework framework - 1.0.0 + 1.0.1 core core diff --git a/core/src/main/java/run/soeasy/framework/codec/crypto/HmacMD5.java b/core/src/main/java/run/soeasy/framework/codec/crypto/HmacMD5.java deleted file mode 100644 index acbf346c4..000000000 --- a/core/src/main/java/run/soeasy/framework/codec/crypto/HmacMD5.java +++ /dev/null @@ -1,29 +0,0 @@ -package run.soeasy.framework.codec.crypto; - -/** - * HmacMD5消息认证码实现类,继承自{@link MAC},专门用于基于HmacMD5算法计算消息认证码, - * 封装了HmacMD5算法的初始化逻辑,简化使用HmacMD5进行数据完整性和真实性验证的流程。 - * - *

HmacMD5算法结合MD5哈希函数和密钥,生成128位(16字节)的消息认证码, - * 适用于对安全性要求不高但需要快速计算的消息认证场景(如 legacy 系统兼容、简单数据校验等)。 - * - * @author soeasy.run - * @see MAC - * @see javax.crypto.Mac - */ -public class HmacMD5 extends MAC { - - /** - * HmacMD5算法名称常量,对应标准加密算法中的"HmacMD5" - */ - public static final String ALGORITHM = "HmacMD5"; - - /** - * 构造HmacMD5编码器(基于密钥字节数组) - * - * @param secretKey 密钥字节数组(用于初始化HmacMD5算法) - */ - public HmacMD5(byte[] secretKey) { - super(ALGORITHM, secretKey); - } -} \ No newline at end of file diff --git a/core/src/main/java/run/soeasy/framework/codec/crypto/HmacSHA1.java b/core/src/main/java/run/soeasy/framework/codec/crypto/HmacSHA1.java deleted file mode 100644 index 3a9eb73ca..000000000 --- a/core/src/main/java/run/soeasy/framework/codec/crypto/HmacSHA1.java +++ /dev/null @@ -1,29 +0,0 @@ -package run.soeasy.framework.codec.crypto; - -/** - * HmacSHA1消息认证码实现类,继承自{@link MAC},专门用于基于HmacSHA1算法计算消息认证码, - * 封装了HmacSHA1算法的初始化逻辑,简化使用HmacSHA1进行数据完整性和真实性验证的流程。 - * - *

HmacSHA1算法结合SHA1哈希函数和密钥,生成160位(20字节)的消息认证码, - * 适用于需要中等安全强度的消息认证场景(如API接口签名、数据传输校验等)。 - * - * @author soeasy.run - * @see MAC - * @see javax.crypto.Mac - */ -public class HmacSHA1 extends MAC { - - /** - * HmacSHA1算法名称常量,对应标准加密算法中的"HmacSHA1" - */ - public static final String ALGORITHM = "HmacSHA1"; - - /** - * 构造HmacSHA1编码器(基于密钥字节数组) - * - * @param secretKey 密钥字节数组(用于初始化HmacSHA1算法,密钥长度推荐不小于160位) - */ - public HmacSHA1(byte[] secretKey) { - super(ALGORITHM, secretKey); - } -} \ No newline at end of file diff --git a/core/src/main/java/run/soeasy/framework/codec/format/CharsetCodec.java b/core/src/main/java/run/soeasy/framework/codec/format/CharsetCodec.java index 5a981549e..999087ffb 100644 --- a/core/src/main/java/run/soeasy/framework/codec/format/CharsetCodec.java +++ b/core/src/main/java/run/soeasy/framework/codec/format/CharsetCodec.java @@ -15,8 +15,7 @@ import run.soeasy.framework.codec.Encoder; import run.soeasy.framework.codec.binary.Gzip; import run.soeasy.framework.codec.binary.ToBinaryCodec; -import run.soeasy.framework.codec.crypto.HmacMD5; -import run.soeasy.framework.codec.crypto.HmacSHA1; +import run.soeasy.framework.codec.crypto.MAC; import run.soeasy.framework.io.IOUtils; /** @@ -117,12 +116,8 @@ public Codec gzip() { return gzip(HexCodec.DEFAULT); } - public Encoder toHmacMD5(String secretKey) { - return toEncoder(new HmacMD5(encode(secretKey)).toEncoder(HexCodec.DEFAULT)); - } - - public Encoder toHmacSHA1(String secretKey) { - return toEncoder(new HmacSHA1(encode(secretKey)).toEncoder(HexCodec.DEFAULT)); + public Encoder toMac(String algorithm, String secretKey) { + return toEncoder(new MAC(algorithm, encode(secretKey))); } public static CharsetCodec charset(Charset charset) { diff --git a/core/src/main/java/run/soeasy/framework/codec/format/KeyValueFormat.java b/core/src/main/java/run/soeasy/framework/codec/format/KeyValueFormat.java index 8b60ad83f..8ee458b3a 100644 --- a/core/src/main/java/run/soeasy/framework/codec/format/KeyValueFormat.java +++ b/core/src/main/java/run/soeasy/framework/codec/format/KeyValueFormat.java @@ -13,73 +13,70 @@ import run.soeasy.framework.codec.Codec; import run.soeasy.framework.codec.CodecException; import run.soeasy.framework.core.collection.ArrayUtils; -import run.soeasy.framework.core.collection.DefaultMultiValueMap; -import run.soeasy.framework.core.collection.MultiValueMap; import run.soeasy.framework.core.convert.ConversionException; import run.soeasy.framework.core.convert.Converter; import run.soeasy.framework.core.convert.ConverterAware; import run.soeasy.framework.core.convert.TypeDescriptor; import run.soeasy.framework.core.convert.strings.StringFormat; import run.soeasy.framework.core.convert.support.SystemConversionService; -import run.soeasy.framework.core.convert.value.TypedValueAccessor; import run.soeasy.framework.core.domain.KeyValue; import run.soeasy.framework.core.join.Joiner; import run.soeasy.framework.core.join.KeyValueSplitter; -import run.soeasy.framework.core.transform.collection.MapEntryMapping; -import run.soeasy.framework.core.transform.templates.DefaultMapper; -import run.soeasy.framework.core.transform.templates.Mapping; +import run.soeasy.framework.core.mapping.property.MapMapping; +import run.soeasy.framework.core.mapping.property.Property; +import run.soeasy.framework.core.mapping.property.PropertyAccessor; +import run.soeasy.framework.core.mapping.property.PropertyMapper; +import run.soeasy.framework.core.streaming.Mapping; import run.soeasy.framework.core.type.ResolvableType; /** - * 键值对格式处理器,提供键值对数据的编解码、类型转换和流式处理能力,适用于配置解析、参数处理、数据格式化等场景。 + * 键值对格式处理器,实现键值对字符串与对象/ {@link Mapping} 的双向转换,支持多值键、自定义分隔符和类型转换。 * - *

核心功能: + *

核心能力: *

* - *

实现特性: + *

实现规范: *

* * @author soeasy.run - * @see MultiValueMap 多值映射接口,支持一个键对应多个值 - * @see KeyValueSplitter 键值对分割工具,提供基础的分割与拼接能力 + * @see KeyValueSplitter 键值对分割/拼接基础类 + * @see Mapping 多值映射数据结构 * @see SystemConversionService 默认类型转换服务 */ @Getter @Setter public class KeyValueFormat extends KeyValueSplitter - implements StringFormat, ConverterAware, Codec, String> { - + implements StringFormat, ConverterAware, Codec, String> { + /** - * 键值对映射器,负责对象与键值对结构的映射转换, - * 支持将任意对象转换为键值对集合,或将键值对集合转换为指定类型的对象。 + * 键值对属性映射器,用于对象与键值对结构的双向映射: + *
  • 对象 → 键值对:解析对象属性为键值对集合;
  • + *
  • 键值对 → 对象:将键值对集合映射为指定类型对象。
  • */ - private final DefaultMapper> keyValueMapper = new DefaultMapper<>(); + private final PropertyMapper keyValueMapper = new PropertyMapper<>(); /** * 获取当前使用的类型转换器 - * - * @return 类型转换器,默认使用{@link SystemConversionService} + * @return 类型转换器,默认返回 {@link SystemConversionService} 实例 */ public Converter getConverter() { return keyValueMapper.getMapper().getConverter(); } /** - * 设置类型转换器,用于对象与键值对之间的类型转换 - * - * @param converter 类型转换器,不可为null + * 设置类型转换器,用于键值对与对象属性的类型转换 + * @param converter 类型转换器,不可为 null */ @Override public void setConverter(Converter converter) { @@ -87,36 +84,35 @@ public void setConverter(Converter converter) { } /** - * 构造函数,初始化键值对格式处理器的核心参数 - * - * @param delimiter 键值对之间的分隔符(例如:"," 或 "; ") - * @param connector 键与值之间的连接符(例如:"=" 或 ":") - * @param keyCodec 键的编解码器,用于键的编码(如URL编码)和解码 - * @param valueCodec 值的编解码器,用于值的编码(如URL编码)和解码 + * 构造键值对格式处理器 + * @param delimiter 键值对之间的分隔符(如 "&"、","),不可为 null + * @param connector 键与值之间的连接符(如 "="、":"),不可为 null + * @param keyCodec 键的编解码器,不可为 null + * @param valueCodec 值的编解码器,不可为 null */ public KeyValueFormat(@NonNull CharSequence delimiter, @NonNull CharSequence connector, @NonNull Codec keyCodec, @NonNull Codec valueCodec) { super(delimiter, connector, keyCodec, valueCodec); setConverter(SystemConversionService.getInstance()); keyValueMapper.getMappingProvider().registerFactory(Map.class, - (map, type) -> new MapEntryMapping(map, type, getConverter())); + (map, type) -> new MapMapping(map, type, getConverter())); } /** - * 将键值对元素拼接并追加到目标Appendable,支持多值键的处理 + * 将键值对元素拼接并追加到目标 Appendable,支持多值处理 * - *

    特殊处理: + *

    多值处理规则: *

      - *
    • 若值为{@link Iterable}(如List),则为每个元素生成一个键值对
    • - *
    • 若值为数组,则为每个数组元素生成一个键值对
    • - *
    • 普通值直接生成一个键值对
    • + *
    • 值为 Iterable:为每个元素生成独立键值对;
    • + *
    • 值为数组:遍历数组元素生成独立键值对;
    • + *
    • 普通值:直接生成单个键值对。
    • *
    * - * @param appendable 目标追加器(如StringBuilder、FileWriter) - * @param count 已处理的键值对数量,用于分隔符的添加(首个元素前不加分隔符) - * @param element 待处理的键值对元素 + * @param appendable 目标追加器(如 StringBuilder),不可为 null + * @param count 已处理的键值对数量(用于分隔符添加) + * @param element 待处理的键值对元素,不可为 null * @return 处理后的键值对总数量 - * @throws IOException 当向Appendable写入数据失败时抛出 + * @throws IOException 写入 Appendable 失败时抛出 */ @Override public long join(Appendable appendable, long count, KeyValue element) @@ -130,8 +126,9 @@ public long join(Appendable appendable, long count, KeyValue iterator = ArrayUtils.elements(value).stream().iterator(); + while (iterator.hasNext()) { + count += super.join(appendable, count, KeyValue.of(element.getKey(), iterator.next())); } return count; } @@ -139,46 +136,30 @@ public long join(Appendable appendable, long count, KeyValue处理流程: - *
      - *
    1. 通过{@link #keyValueMapper}将对象转换为键值对集合
    2. - *
    3. 过滤出可读取的值,生成键值对迭代器
    4. - *
    5. 使用{@link Joiner}将键值对拼接为字符串并写入Appendable
    6. - *
    - * - * @param source 待格式化的对象(通常为Map或可转换为Map的类型) - * @param sourceTypeDescriptor 源对象的类型描述符,用于精确转换 - * @param appendable 目标追加位置(如StringBuilder、FileWriter) - * @throws IOException 当向Appendable写入数据失败时抛出 - * @throws ConversionException 当对象转换为键值对失败时抛出 + * 将对象格式化为键值对字符串并写入 Appendable + * @param source 待格式化的对象(如 Map/POJO),不可为 null + * @param sourceTypeDescriptor 源对象的类型描述符 + * @param appendable 目标追加器,不可为 null + * @throws ConversionException 对象转换为键值对失败时抛出 + * @throws IOException 写入 Appendable 失败时抛出 */ @Override public void to(Object source, TypeDescriptor sourceTypeDescriptor, Appendable appendable) throws ConversionException, IOException { - Mapping mapping = keyValueMapper.getMapping(source, sourceTypeDescriptor); - Iterator> iterator = mapping.getElements().stream() + Mapping mapping = keyValueMapper.getMapping(source, sourceTypeDescriptor); + Iterator> iterator = mapping.stream() .filter((e) -> e.getValue().isReadable()).map((e) -> KeyValue.of(e.getKey(), e.getValue().get())) .iterator(); Joiner.joinAll(appendable, iterator, this); } /** - * 从可读数据源(如Reader)读取键值对字符串并转换为指定类型的对象 - * - *

    处理流程: - *

      - *
    1. 将可读数据源解析为键值对集合
    2. - *
    3. 转换为Map结构(键为String类型,值为String列表)支持多值键
    4. - *
    5. 通过{@link #keyValueMapper}将Map转换为目标类型的对象
    6. - *
    - * - * @param readable 可读数据源(如StringReader、FileReader) - * @param targetTypeDescriptor 目标对象的类型描述符 + * 从 Readable 读取键值对字符串并转换为指定类型的对象 + * @param readable 可读数据源(如 StringReader),不可为 null + * @param targetTypeDescriptor 目标对象的类型描述符,不可为 null * @return 转换后的目标类型对象 - * @throws ConversionException 当键值对转换为目标类型失败时抛出 - * @throws IOException 当从Readable读取数据失败时抛出 + * @throws ConversionException 键值对转换为目标对象失败时抛出 + * @throws IOException 读取 Readable 失败时抛出 */ @Override public Object from(Readable readable, TypeDescriptor targetTypeDescriptor) throws ConversionException, IOException { @@ -190,41 +171,26 @@ public Object from(Readable readable, TypeDescriptor targetTypeDescriptor) throw } /** - * 将{@link MultiValueMap}编码为键值对字符串 - * - *

    默认实现通过类型转换完成,等价于将MultiValueMap转换为String类型。 - * - * @param source 待编码的MultiValueMap + * 将 {@link Mapping} 编码为键值对字符串 + * @param source 待编码的 Mapping,不可为 null * @return 编码后的键值对字符串 - * @throws CodecException 当编码过程失败时抛出(如转换异常) + * @throws CodecException 编码失败时抛出 */ @Override - public final String encode(MultiValueMap source) throws CodecException { + public final String encode(Mapping source) throws CodecException { return convert(source, String.class); } /** - * 将键值对字符串解码为{@link MultiValueMap} - * - *

    处理流程: - *

      - *
    1. 将字符串分割为键值对集合
    2. - *
    3. 按键分组,收集每个键对应的多个值(保持插入顺序)
    4. - *
    5. 转换为{@link DefaultMultiValueMap}返回
    6. - *
    - * - * @param source 待解码的键值对字符串 - * @return 解码后的MultiValueMap,键值顺序与字符串中保持一致 - * @throws CodecException 当解码过程失败时抛出(如分割异常) + * 将键值对字符串解码为 {@link Mapping} + * @param source 待解码的键值对字符串,不可为 null + * @return 解码后的 Mapping(保持键的插入顺序) + * @throws CodecException 解码失败时抛出 */ @Override - public final MultiValueMap decode(String source) throws CodecException { - // 使用Stream流处理键值对,分组收集到LinkedHashMap保持顺序 - Map> map = split(source).collect(Collectors.groupingBy(KeyValue::getKey, // 按键分组 - LinkedHashMap::new, // 使用LinkedHashMap保持键的顺序 - Collectors.mapping(KeyValue::getValue, Collectors.toList()) // 收集值列表 - )); - return new DefaultMultiValueMap<>(map); + public final Mapping decode(String source) throws CodecException { + Map> map = split(source).collect(Collectors.groupingBy(KeyValue::getKey, + LinkedHashMap::new, Collectors.mapping(KeyValue::getValue, Collectors.toList()))); + return Mapping.ofMultiMapped(map); } -} - \ No newline at end of file +} \ No newline at end of file diff --git a/core/src/main/java/run/soeasy/framework/core/ObjectUtils.java b/core/src/main/java/run/soeasy/framework/core/ObjectUtils.java index 99d7cd3fb..a1af89680 100644 --- a/core/src/main/java/run/soeasy/framework/core/ObjectUtils.java +++ b/core/src/main/java/run/soeasy/framework/core/ObjectUtils.java @@ -1,6 +1,8 @@ package run.soeasy.framework.core; import java.util.Arrays; +import java.util.logging.Level; +import java.util.logging.Logger; import lombok.experimental.UtilityClass; import run.soeasy.framework.core.collection.ArrayUtils; @@ -10,20 +12,22 @@ * 对象工具类,提供对象操作的通用静态方法,包括对象相等性比较、哈希计算、 * 对象转字符串以及资源关闭等功能。该类遵循空安全设计原则,所有方法都能妥善处理null输入。 * - *

    核心特性: + *

    + * 核心特性: *

      - *
    • 空安全:所有方法均支持null参数输入,不会抛出NullPointerException
    • - *
    • 深度处理:支持深度比较、深度哈希计算和深度字符串转换
    • - *
    • 数组支持:自动处理数组类型,提供数组元素级别的操作
    • - *
    • 资源管理:提供安全关闭AutoCloseable资源的方法
    • + *
    • 空安全:所有方法均支持null参数输入,不会抛出NullPointerException
    • + *
    • 深度处理:支持深度比较、深度哈希计算和深度字符串转换
    • + *
    • 数组支持:自动处理数组类型,提供数组元素级别的操作
    • + *
    • 资源管理:提供安全关闭AutoCloseable资源的方法
    • *
    * - *

    使用场景: + *

    + * 使用场景: *

      - *
    • 对象相等性比较,尤其是包含数组的复杂对象
    • - *
    • 集合元素的哈希计算,确保数组元素的正确哈希
    • - *
    • 对象调试信息输出,生成包含数组内容的字符串表示
    • - *
    • 资源释放,安全关闭多个AutoCloseable资源
    • + *
    • 对象相等性比较,尤其是包含数组的复杂对象
    • + *
    • 集合元素的哈希计算,确保数组元素的正确哈希
    • + *
    • 对象调试信息输出,生成包含数组内容的字符串表示
    • + *
    • 资源释放,安全关闭多个AutoCloseable资源
    • *
    * * @see ArrayUtils @@ -31,186 +35,185 @@ */ @UtilityClass public class ObjectUtils { - - /** - * 空对象数组常量,用于避免重复创建空数组实例。 - */ - public static final Object[] EMPTY_ARRAY = new Object[0]; - - /** - * 判断对象是否为数组类型。 - *

    - * 该方法会检查对象是否非空且其Class是否表示数组类型。 - * - * @param obj 待检查的对象 - * @return 如果对象是数组返回true,否则返回false - */ - public static boolean isArray(Object obj) { - return (obj != null && obj.getClass().isArray()); - } - - /** - * 将对象转换为字符串表示,支持深度转换。 - *

    - * 处理逻辑: - *

      - *
    • 如果对象为null,返回null
    • - *
    • 如果对象是数组,调用{@link ArrayUtils#toString}进行深度转换
    • - *
    • 否则,调用对象自身的toString()方法
    • - *
    - * - * @param source 待转换的对象 - * @param deep 是否进行深度转换(对数组元素递归处理) - * @return 对象的字符串表示,可能为null - */ - public static String toString(Object source, boolean deep) { - if (source == null) { - return null; - } else if (source.getClass().isArray()) { - return ArrayUtils.toString(source, deep); - } else { - return source.toString(); - } - } - - /** - * 将对象转换为字符串表示,默认启用深度转换。 - *

    - * 等价于调用{@link #toString(Object, boolean)}并传入true。 - * - * @param source 待转换的对象 - * @return 对象的字符串表示,可能为null - */ - public static String toString(Object source) { - return toString(source, true); - } - - /** - * 计算对象的哈希值,支持深度计算。 - *

    - * 处理逻辑: - *

      - *
    • 如果对象为null,返回0
    • - *
    • 如果对象是数组,调用{@link ArrayUtils#hashCode}进行深度计算
    • - *
    • 否则,调用对象自身的hashCode()方法
    • - *
    - * - * @param source 待计算哈希值的对象 - * @param deep 是否进行深度计算(对数组元素递归处理) - * @return 对象的哈希值 - */ - public static int hashCode(Object source, boolean deep) { - if (source == null) { - return 0; - } else if (source.getClass().isArray()) { - return ArrayUtils.hashCode(source, deep); - } else { - return source.hashCode(); - } - } - - /** - * 计算对象的哈希值,默认启用深度计算。 - *

    - * 等价于调用{@link #hashCode(Object, boolean)}并传入true。 - * - * @param source 待计算哈希值的对象 - * @return 对象的哈希值 - */ - public static int hashCode(Object source) { - return hashCode(source, true); - } - - /** - * 判断两个对象是否相等,支持深度比较。 - *

    - * 比较逻辑: - *

      - *
    1. 如果两个对象引用相同,返回true
    2. - *
    3. 如果任一对象为null,返回false
    4. - *
    5. 调用对象的equals()方法,如果返回true则相等
    6. - *
    7. 如果两个对象都是数组,调用{@link ArrayUtils#equals}进行深度比较
    8. - *
    9. 否则返回false
    10. - *
    - * - * @param left 左操作数 - * @param right 右操作数 - * @param deep 是否进行深度比较(对数组元素递归处理) - * @return 如果对象相等返回true,否则返回false - */ - public static boolean equals(Object left, Object right, boolean deep) { - if (left == right) { - return true; - } - - if (left == null || right == null) { - return false; - } - - if (left.equals(right)) { - return true; - } - - if (left.getClass().isArray() && right.getClass().isArray()) { - return ArrayUtils.equals(left, right, deep); - } - return false; - } - - /** - * 判断两个对象是否相等,默认启用深度比较。 - *

    - * 等价于调用{@link #equals(Object, Object, boolean)}并传入true。 - * - * @param left 左操作数 - * @param right 右操作数 - * @return 如果对象相等返回true,否则返回false - */ - public static boolean equals(Object left, Object right) { - return equals(left, right, true); - } - - /** - * 安全关闭多个AutoCloseable资源,遇到异常时抛出。 - *

    - * 该方法会按顺序关闭所有资源,若任一资源关闭失败, - * 则抛出异常并继续尝试关闭后续资源。 - * - * @param autoCloseables 待关闭的资源数组 - * @throws Exception 如果关闭过程中发生异常 - */ - public static void close(AutoCloseable... autoCloseables) throws Exception { - CollectionUtils.acceptAll(Arrays.asList(autoCloseables), (e) -> { - if(e == null) { - return; - } - e.close(); - }); - } - - /** - * 安静地关闭多个AutoCloseable资源,忽略所有异常。 - *

    - * 该方法会按顺序关闭所有资源,若任一资源关闭失败, - * 会捕获异常并继续尝试关闭后续资源。 - * - * @param autoCloseables 待关闭的资源数组 - */ - public static void closeQuietly(AutoCloseable... autoCloseables) { - if (autoCloseables == null) { - return; - } - - for (AutoCloseable autoCloseable : autoCloseables) { - if (autoCloseable == null) { - continue; - } - - try { - autoCloseable.close(); - } catch (final Exception e) { - // ignore - } - } - } + private static Logger logger = Logger.getLogger(ObjectUtils.class.getName()); + + /** + * 空对象数组常量,用于避免重复创建空数组实例。 + */ + public static final Object[] EMPTY_ARRAY = new Object[0]; + + /** + * 判断对象是否为数组类型。 + *

    + * 该方法会检查对象是否非空且其Class是否表示数组类型。 + * + * @param obj 待检查的对象 + * @return 如果对象是数组返回true,否则返回false + */ + public static boolean isArray(Object obj) { + return (obj != null && obj.getClass().isArray()); + } + + /** + * 将对象转换为字符串表示,支持深度转换。 + *

    + * 处理逻辑: + *

      + *
    • 如果对象为null,返回null
    • + *
    • 如果对象是数组,调用{@link ArrayUtils#toString}进行深度转换
    • + *
    • 否则,调用对象自身的toString()方法
    • + *
    + * + * @param source 待转换的对象 + * @param deep 是否进行深度转换(对数组元素递归处理) + * @return 对象的字符串表示,可能为null + */ + public static String toString(Object source, boolean deep) { + if (source == null) { + return null; + } else if (source.getClass().isArray()) { + return ArrayUtils.toString(source, deep); + } else { + return source.toString(); + } + } + + /** + * 将对象转换为字符串表示,默认启用深度转换。 + *

    + * 等价于调用{@link #toString(Object, boolean)}并传入true。 + * + * @param source 待转换的对象 + * @return 对象的字符串表示,可能为null + */ + public static String toString(Object source) { + return toString(source, true); + } + + /** + * 计算对象的哈希值,支持深度计算。 + *

    + * 处理逻辑: + *

      + *
    • 如果对象为null,返回0
    • + *
    • 如果对象是数组,调用{@link ArrayUtils#hashCode}进行深度计算
    • + *
    • 否则,调用对象自身的hashCode()方法
    • + *
    + * + * @param source 待计算哈希值的对象 + * @param deep 是否进行深度计算(对数组元素递归处理) + * @return 对象的哈希值 + */ + public static int hashCode(Object source, boolean deep) { + if (source == null) { + return 0; + } else if (source.getClass().isArray()) { + return ArrayUtils.hashCode(source, deep); + } else { + return source.hashCode(); + } + } + + /** + * 计算对象的哈希值,默认启用深度计算。 + *

    + * 等价于调用{@link #hashCode(Object, boolean)}并传入true。 + * + * @param source 待计算哈希值的对象 + * @return 对象的哈希值 + */ + public static int hashCode(Object source) { + return hashCode(source, true); + } + + /** + * 判断两个对象是否相等,支持深度比较。 + *

    + * 比较逻辑: + *

      + *
    1. 如果两个对象引用相同,返回true
    2. + *
    3. 如果任一对象为null,返回false
    4. + *
    5. 调用对象的equals()方法,如果返回true则相等
    6. + *
    7. 如果两个对象都是数组,调用{@link ArrayUtils#equals}进行深度比较
    8. + *
    9. 否则返回false
    10. + *
    + * + * @param left 左操作数 + * @param right 右操作数 + * @param deep 是否进行深度比较(对数组元素递归处理) + * @return 如果对象相等返回true,否则返回false + */ + public static boolean equals(Object left, Object right, boolean deep) { + if (left == right) { + return true; + } + + if (left == null || right == null) { + return false; + } + + if (left.equals(right)) { + return true; + } + + if (left.getClass().isArray() && right.getClass().isArray()) { + return ArrayUtils.equals(left, right, deep); + } + return false; + } + + /** + * 判断两个对象是否相等,默认启用深度比较。 + *

    + * 等价于调用{@link #equals(Object, Object, boolean)}并传入true。 + * + * @param left 左操作数 + * @param right 右操作数 + * @return 如果对象相等返回true,否则返回false + */ + public static boolean equals(Object left, Object right) { + return equals(left, right, true); + } + + /** + * 安全关闭多个AutoCloseable资源,遇到异常时抛出。 + *

    + * 该方法会按顺序关闭所有资源,若任一资源关闭失败, 则抛出异常并继续尝试关闭后续资源。 + * + * @param autoCloseables 待关闭的资源数组 + * @throws Exception 如果关闭过程中发生异常 + */ + public static void close(AutoCloseable... autoCloseables) throws Exception { + CollectionUtils.acceptAll(Arrays.asList(autoCloseables), (e) -> { + if (e == null) { + return; + } + e.close(); + }); + } + + /** + * 安静地关闭多个AutoCloseable资源,忽略所有异常。 + *

    + * 该方法会按顺序关闭所有资源,若任一资源关闭失败, 会捕获异常并继续尝试关闭后续资源。 + * + * @param autoCloseables 待关闭的资源数组 + */ + public static void closeQuietly(AutoCloseable... autoCloseables) { + if (autoCloseables == null) { + return; + } + + for (AutoCloseable autoCloseable : autoCloseables) { + if (autoCloseable == null) { + continue; + } + + try { + autoCloseable.close(); + } catch (final Throwable e) { + logger.log(Level.FINEST, e, () -> autoCloseable.toString()); + } + } + } } \ No newline at end of file diff --git a/core/src/main/java/run/soeasy/framework/core/StringUtils.java b/core/src/main/java/run/soeasy/framework/core/StringUtils.java index 94278bd8d..616e507ac 100644 --- a/core/src/main/java/run/soeasy/framework/core/StringUtils.java +++ b/core/src/main/java/run/soeasy/framework/core/StringUtils.java @@ -6,10 +6,10 @@ import java.util.StringTokenizer; import lombok.experimental.UtilityClass; -import run.soeasy.framework.core.collection.Elements; import run.soeasy.framework.core.domain.CharSequenceSplitIterator; import run.soeasy.framework.core.domain.CharSequenceTemplate; import run.soeasy.framework.core.domain.Range; +import run.soeasy.framework.core.streaming.Streamable; /** * 字符串工具类 提供字符串操作的各种实用方法,包括查找、替换、分割、比较等功能 @@ -792,7 +792,7 @@ public static int lastIndexOf(CharSequence source, int sourceOffset, int sourceC * @param filters 分隔符序列 * @return 分割后的元素集合 */ - public static Elements split(CharSequence charSequence, boolean trimTokens, + public static Streamable split(CharSequence charSequence, boolean trimTokens, boolean ignoreEmptyTokens, CharSequence... filters) { return split(charSequence, filters).map((s) -> trimTokens ? (s == null ? s : s.trim()) : s) .filter((s) -> (ignoreEmptyTokens ? StringUtils.isNotEmpty(s) : true)); @@ -805,9 +805,9 @@ public static Elements split(CharSequence charSequence, bo * @param filters 分隔符序列 * @return 分割后的元素集合 */ - public static Elements split(CharSequence charSequence, CharSequence... filters) { + public static Streamable split(CharSequence charSequence, CharSequence... filters) { if (charSequence == null) { - return Elements.empty(); + return Streamable.empty(); } return split(charSequence, 0, charSequence.length(), Arrays.asList(filters)); } @@ -819,10 +819,10 @@ public static Elements split(CharSequence charSequence, Ch * @param filters 分隔符集合 * @return 分割后的元素集合 */ - public static Elements split(CharSequence charSequence, + public static Streamable split(CharSequence charSequence, Collection filters) { if (charSequence == null) { - return Elements.empty(); + return Streamable.empty(); } return split(charSequence, 0, charSequence.length(), filters); } @@ -836,10 +836,10 @@ public static Elements split(CharSequence charSequence, * @param filters 分隔符集合 * @return 分割后的元素集合 */ - public static Elements split(CharSequence charSequence, int beginIndex, int endIndex, + public static Streamable split(CharSequence charSequence, int beginIndex, int endIndex, Collection filters) { if (StringUtils.isEmpty(charSequence)) { - return Elements.empty(); + return Streamable.empty(); } boolean find = false; @@ -851,9 +851,9 @@ public static Elements split(CharSequence charSequence, in } if (!find) { - return Elements.singleton(new CharSequenceTemplate(charSequence)); + return Streamable.singleton(new CharSequenceTemplate(charSequence)); } - return Elements.of(() -> new CharSequenceSplitIterator(charSequence, filters, beginIndex, endIndex)); + return Streamable.of(() -> new CharSequenceSplitIterator(charSequence, filters, beginIndex, endIndex)); } /** @@ -952,9 +952,9 @@ public static boolean startsWithIgnoreCase(String str, String prefix) { * @param delimiters 分隔符 * @return 分词后的元素集合 */ - public static Elements tokenize(String text, String delimiters) { + public static Streamable tokenize(String text, String delimiters) { if (StringUtils.isEmpty(text)) { - return Elements.empty(); + return Streamable.empty(); } return tokenize(new StringTokenizer(text, delimiters)); @@ -969,7 +969,7 @@ public static Elements tokenize(String text, String delimiters) { * @param ignoreEmptyTokens 是否忽略空标记 * @return 分词后的元素集合 */ - public static Elements tokenize(String str, String delimiters, boolean trimTokens, + public static Streamable tokenize(String str, String delimiters, boolean trimTokens, boolean ignoreEmptyTokens) { return tokenize(str, delimiters).map((s) -> trimTokens ? (s == null ? s : s.trim()) : s) .filter((s) -> (ignoreEmptyTokens ? StringUtils.isNotEmpty(s) : true)); @@ -981,12 +981,12 @@ public static Elements tokenize(String str, String delimiters, boolean t * @param tokenizer 字符串分词器 * @return 分词后的元素集合 */ - public static Elements tokenize(StringTokenizer tokenizer) { + public static Streamable tokenize(StringTokenizer tokenizer) { if (tokenizer == null) { - return Elements.empty(); + return Streamable.empty(); } - return Elements.of(() -> new Iterator() { + return Streamable.of(() -> new Iterator() { @Override public boolean hasNext() { return tokenizer.hasMoreTokens(); diff --git a/core/src/main/java/run/soeasy/framework/core/annotation/AbstractAnnotationPropertyMapping.java b/core/src/main/java/run/soeasy/framework/core/annotation/AbstractAnnotationPropertyMapping.java index 7f3442301..6f2fd67a1 100644 --- a/core/src/main/java/run/soeasy/framework/core/annotation/AbstractAnnotationPropertyMapping.java +++ b/core/src/main/java/run/soeasy/framework/core/annotation/AbstractAnnotationPropertyMapping.java @@ -7,24 +7,25 @@ /** * 注解属性映射抽象基类,实现{@link AnnotationProperties}接口, - * 提供注解属性映射的基础实现,包括hashCode和equals方法的默认实现, - * 是框架中构建具体注解属性映射器的抽象基类。 + * 提供注解属性映射的基础实现,包括hashCode和equals方法的默认实现, 是框架中构建具体注解属性映射器的抽象基类。 *

    * 该类基于注解类型和属性元素实现了hashCode和equals方法,确保相同注解类型和属性值的实例相等, * 并提供了合理的哈希值计算方式,适用于需要统一处理注解属性映射的场景。 * - *

    核心特性: + *

    + * 核心特性: *

      - *
    • 标准化相等性:基于注解类型和属性值实现equals方法
    • - *
    • 一致哈希值:基于注解类型和属性值计算hashCode
    • - *
    • 抽象模板:定义了注解属性映射的基本框架,子类只需实现具体属性获取逻辑
    • + *
    • 标准化相等性:基于注解类型和属性值实现equals方法
    • + *
    • 一致哈希值:基于注解类型和属性值计算hashCode
    • + *
    • 抽象模板:定义了注解属性映射的基本框架,子类只需实现具体属性获取逻辑
    • *
    * - *

    实现说明: + *

    + * 实现说明: *

      - *
    • equals方法:比较注解类型和所有属性值的相等性
    • - *
    • hashCode方法:结合注解类型和属性值计算哈希值
    • - *
    • 属性比较:使用ObjectUtils.equals进行深层值比较
    • + *
    • equals方法:比较注解类型和所有属性值的相等性
    • + *
    • hashCode方法:结合注解类型和属性值计算哈希值
    • + *
    • 属性比较:使用ObjectUtils.equals进行深层值比较
    • *
    * * @author soeasy.run @@ -33,47 +34,47 @@ */ public abstract class AbstractAnnotationPropertyMapping implements AnnotationProperties { - /** - * 计算注解属性映射的哈希值 - *

    - * 哈希值计算基于: - *

      - *
    1. 注解类型的哈希值
    2. - *
    3. 所有属性元素的哈希值(通过ArrayUtils.hashCode计算)
    4. - *
    - * - * @return 注解属性映射的哈希值 - */ - @Override - public int hashCode() { - return getType().hashCode() + ArrayUtils.hashCode(getElements().toList()); - } + /** + * 计算注解属性映射的哈希值 + *

    + * 哈希值计算基于: + *

      + *
    1. 注解类型的哈希值
    2. + *
    3. 所有属性元素的哈希值(通过ArrayUtils.hashCode计算)
    4. + *
    + * + * @return 注解属性映射的哈希值 + */ + @Override + public int hashCode() { + return getType().hashCode() + ArrayUtils.hashCode(elements().toArray()); + } - /** - * 判断与其他对象的相等性 - *

    - * 相等性判断基于: - *

      - *
    1. 对象引用相等
    2. - *
    3. 注解类型相同
    4. - *
    5. 所有属性值相同(使用ObjectUtils.equals进行深层比较)
    6. - *
    - * - * @param obj 待比较的对象 - * @return 如果对象相等返回true,否则返回false - */ - @Override - public boolean equals(Object obj) { - if (obj == this) { - return true; - } + /** + * 判断与其他对象的相等性 + *

    + * 相等性判断基于: + *

      + *
    1. 对象引用相等
    2. + *
    3. 注解类型相同
    4. + *
    5. 所有属性值相同(使用ObjectUtils.equals进行深层比较)
    6. + *
    + * + * @param obj 待比较的对象 + * @return 如果对象相等返回true,否则返回false + */ + @Override + public boolean equals(Object obj) { + if (obj == this) { + return true; + } - if (obj instanceof AnnotationProperties) { - AnnotationProperties annotationProperties = (AnnotationProperties) obj; - return getType() == annotationProperties.getType() && getElements() - .equals(annotationProperties.getElements(), (a, b) -> ObjectUtils.equals(a.getKey(), b.getKey()) - && ObjectUtils.equals(a.getValue().get(), b.getValue().get())); - } - return false; - } + if (obj instanceof AnnotationProperties) { + AnnotationProperties annotationProperties = (AnnotationProperties) obj; + return getType() == annotationProperties.getType() + && equalsInAnyOrder(annotationProperties, (a, b) -> ObjectUtils.equals(a.getKey(), b.getKey()) + && ObjectUtils.equals(a.getValue().get(), b.getValue().get())); + } + return false; + } } \ No newline at end of file diff --git a/core/src/main/java/run/soeasy/framework/core/annotation/AnnotationProperties.java b/core/src/main/java/run/soeasy/framework/core/annotation/AnnotationProperties.java index 6a57fa1a6..0c64f6311 100644 --- a/core/src/main/java/run/soeasy/framework/core/annotation/AnnotationProperties.java +++ b/core/src/main/java/run/soeasy/framework/core/annotation/AnnotationProperties.java @@ -7,107 +7,111 @@ import run.soeasy.framework.core.convert.Converter; import run.soeasy.framework.core.convert.TypeDescriptor; -import run.soeasy.framework.core.convert.value.TypedValue; -import run.soeasy.framework.core.transform.property.TypedProperties; +import run.soeasy.framework.core.mapping.property.PropertyAccessor; +import run.soeasy.framework.core.mapping.property.PropertyMapping; import run.soeasy.framework.core.type.ReflectionUtils; /** - * 注解属性接口,继承自{@link TypedProperties}和{@link InvocationHandler}, + * 注解属性接口,继承自{@link PropertyMapping}和{@link InvocationHandler}, * 提供注解属性的动态访问能力和合成动态注解的功能,是框架中处理动态注解的核心抽象。 *

    * 该接口通过代理模式将属性操作转换为注解的方法调用,支持基于属性集合动态生成注解实例, * 适用于需要在运行时动态构造注解的场景,如测试框架、动态代理、注解处理器等。 * - *

    核心特性: + *

    + * 核心特性: *

      - *
    • 属性-注解映射:将属性集合转换为注解的方法调用
    • - *
    • 动态合成:通过{@link #synthesize()}方法生成代理注解实例
    • - *
    • 类型安全:通过泛型约束确保合成注解的类型一致性
    • - *
    • 代理处理:实现InvocationHandler接口,处理注解方法的调用
    • + *
    • 属性-注解映射:将属性集合转换为注解的方法调用
    • + *
    • 动态合成:通过{@link #synthesize()}方法生成代理注解实例
    • + *
    • 类型安全:通过泛型约束确保合成注解的类型一致性
    • + *
    • 代理处理:实现InvocationHandler接口,处理注解方法的调用
    • *
    * - *

    泛型说明: + *

    + * 泛型说明: *

      - *
    • {@code A}:合成的注解类型,必须是{@link Annotation}的子类
    • + *
    • {@code A}:合成的注解类型,必须是{@link Annotation}的子类
    • *
    * - *

    使用场景: + *

    + * 使用场景: *

      - *
    • 动态注解生成:在运行时根据配置生成自定义注解
    • - *
    • 测试框架:动态构造测试所需的注解
    • - *
    • 代理增强:为代理对象添加动态注解
    • - *
    • 注解处理器:动态处理和生成注解实例
    • - *
    • 元编程:基于属性集合动态生成注解元数据
    • + *
    • 动态注解生成:在运行时根据配置生成自定义注解
    • + *
    • 测试框架:动态构造测试所需的注解
    • + *
    • 代理增强:为代理对象添加动态注解
    • + *
    • 注解处理器:动态处理和生成注解实例
    • + *
    • 元编程:基于属性集合动态生成注解元数据
    • *
    * * @author soeasy.run - * @see TypedProperties + * @see PropertyMapping * @see InvocationHandler * @see Proxy */ -public interface AnnotationProperties
    extends TypedProperties, InvocationHandler { +public interface AnnotationProperties + extends PropertyMapping, InvocationHandler { - /** - * 获取合成注解的类型 - * - * @return 注解的Class对象,如{@code MyAnnotation.class} - */ - Class getType(); + /** + * 获取合成注解的类型 + * + * @return 注解的Class对象,如{@code MyAnnotation.class} + */ + Class getType(); - /** - * 处理代理方法的调用(InvocationHandler实现) - *

    - * 该方法实现了注解代理的核心逻辑,处理以下类型的调用: - *

      - *
    1. Object类方法:equals/hashCode/toString
    2. - *
    3. 注解元方法:annotationType()
    4. - *
    5. 注解属性方法:通过属性名称获取对应值
    6. - *
    - * - * @param proxy 代理实例 - * @param method 被调用的方法 - * @param args 方法参数 - * @return 方法调用结果 - * @throws Throwable 调用过程中抛出的异常 - */ - @Override - default Object invoke(Object proxy, Method method, Object[] args) throws Throwable { - if (ReflectionUtils.isEqualsMethod(method)) { - return equals(args[0]); - } - if (ReflectionUtils.isHashCodeMethod(method)) { - return hashCode(); - } - if (ReflectionUtils.isToStringMethod(method)) { - return toString(); - } + /** + * 处理代理方法的调用(InvocationHandler实现) + *

    + * 该方法实现了注解代理的核心逻辑,处理以下类型的调用: + *

      + *
    1. Object类方法:equals/hashCode/toString
    2. + *
    3. 注解元方法:annotationType()
    4. + *
    5. 注解属性方法:通过属性名称获取对应值
    6. + *
    + * + * @param proxy 代理实例 + * @param method 被调用的方法 + * @param args 方法参数 + * @return 方法调用结果 + * @throws Throwable 调用过程中抛出的异常 + */ + @Override + default Object invoke(Object proxy, Method method, Object[] args) throws Throwable { + if (ReflectionUtils.isEqualsMethod(method)) { + return equals(args[0]); + } + if (ReflectionUtils.isHashCodeMethod(method)) { + return hashCode(); + } + if (ReflectionUtils.isToStringMethod(method)) { + return toString(); + } - if (method.getName().equals("annotationType") && method.getParameterCount() == 0) { - return getType(); - } + if (method.getName().equals("annotationType") && method.getParameterCount() == 0) { + return getType(); + } - if (hasKey(method.getName())) { - TypedValue typedValue = get(method.getName()); - return typedValue == null ? null - : typedValue.map(TypeDescriptor.forExecutableReturnType(method), Converter.assignable()).get(); - } - throw new IllegalArgumentException( - String.format("Method [%s] is unsupported for synthesized annotation type [%s]", method, getType())); - } + if (hasKey(method.getName())) { + return getValues(method.getName()).findFirst() + .map((typedValue) -> typedValue + .map(TypeDescriptor.forExecutableReturnType(method), Converter.assignable()).get()) + .orElse(null); + } + throw new IllegalArgumentException( + String.format("Method [%s] is unsupported for synthesized annotation type [%s]", method, getType())); + } - /** - * 合成动态注解实例 - *

    - * 该方法使用Java动态代理生成实现指定注解类型的代理实例, - * 代理的方法调用将委托给当前{@link AnnotationProperties}实现, - * 从而实现基于属性集合的动态注解。 - * - * @return 动态合成的注解实例 - */ - @SuppressWarnings("unchecked") - default A synthesize() { - Class type = getType(); - ClassLoader classLoader = type.getClassLoader(); - return (A) Proxy.newProxyInstance(classLoader, new Class[] { type, SynthesizedAnnotation.class }, this); - } + /** + * 合成动态注解实例 + *

    + * 该方法使用Java动态代理生成实现指定注解类型的代理实例, 代理的方法调用将委托给当前{@link AnnotationProperties}实现, + * 从而实现基于属性集合的动态注解。 + * + * @return 动态合成的注解实例 + */ + @SuppressWarnings("unchecked") + default A synthesize() { + Class type = getType(); + ClassLoader classLoader = type.getClassLoader(); + return (A) Proxy.newProxyInstance(classLoader, new Class[] { type, SynthesizedAnnotation.class }, this); + } } \ No newline at end of file diff --git a/core/src/main/java/run/soeasy/framework/core/annotation/CustomizeAnnotationPropertyMapping.java b/core/src/main/java/run/soeasy/framework/core/annotation/CustomizeAnnotationPropertyMapping.java index 4d9a7808b..a5962e7df 100644 --- a/core/src/main/java/run/soeasy/framework/core/annotation/CustomizeAnnotationPropertyMapping.java +++ b/core/src/main/java/run/soeasy/framework/core/annotation/CustomizeAnnotationPropertyMapping.java @@ -1,3 +1,4 @@ + package run.soeasy.framework.core.annotation; import java.lang.annotation.Annotation; @@ -5,12 +6,13 @@ import lombok.Getter; import lombok.NonNull; import lombok.RequiredArgsConstructor; -import run.soeasy.framework.core.transform.property.TypedProperties; -import run.soeasy.framework.core.transform.property.TypedPropertiesWrapper; +import run.soeasy.framework.core.mapping.property.PropertyAccessor; +import run.soeasy.framework.core.mapping.property.PropertyMapping; +import run.soeasy.framework.core.mapping.property.PropertyMappingWrapper; /** * 自定义注解属性映射器,继承自{@link AbstractAnnotationPropertyMapping}, - * 实现{@link AnnotationProperties}和{@link TypedPropertiesWrapper}接口, + * 实现{@link AnnotationProperties}和{@link PropertyMappingWrapper}接口, * 用于将任意类型的属性包装为注解属性,支持自定义注解类型与属性的映射关系。 *

    * 该类通过组合模式将外部属性源与注解类型绑定,实现注解属性的动态访问, @@ -18,7 +20,7 @@ * *

    核心特性: *