mirror of
https://gitee.com/chinabugotech/hutool.git
synced 2025-04-19 03:01:48 +08:00
添加@MirrorFor和@AliasFor注解
This commit is contained in:
parent
e4a7576d7f
commit
491c53e7dd
@ -0,0 +1,71 @@
|
||||
package cn.hutool.core.annotation;
|
||||
|
||||
import cn.hutool.core.lang.Assert;
|
||||
import cn.hutool.core.util.ObjectUtil;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* {@link AnnotationAttributeWrapper}的基本实现
|
||||
*
|
||||
* @author huangchengxing
|
||||
* @see ForceAliasedAnnotationAttribute
|
||||
* @see AliasedAnnotationAttribute
|
||||
* @see MirroredAnnotationAttribute
|
||||
*/
|
||||
public abstract class AbstractAnnotationAttributeWrapper implements AnnotationAttributeWrapper {
|
||||
|
||||
protected final AnnotationAttribute original;
|
||||
protected final AnnotationAttribute linked;
|
||||
|
||||
protected AbstractAnnotationAttributeWrapper(AnnotationAttribute original, AnnotationAttribute linked) {
|
||||
Assert.notNull(original, "target must not null");
|
||||
Assert.notNull(linked, "linked must not null");
|
||||
this.original = original;
|
||||
this.linked = linked;
|
||||
}
|
||||
|
||||
@Override
|
||||
public AnnotationAttribute getOriginal() {
|
||||
return original;
|
||||
}
|
||||
|
||||
@Override
|
||||
public AnnotationAttribute getLinked() {
|
||||
return linked;
|
||||
}
|
||||
|
||||
@Override
|
||||
public AnnotationAttribute getNonWrappedOriginal() {
|
||||
AnnotationAttribute curr = null;
|
||||
AnnotationAttribute next = original;
|
||||
while (next != null) {
|
||||
curr = next;
|
||||
next = next.isWrapped() ? ((AnnotationAttributeWrapper)curr).getOriginal() : null;
|
||||
}
|
||||
return curr;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<AnnotationAttribute> getAllLinkedNonWrappedAttributes() {
|
||||
List<AnnotationAttribute> leafAttributes = new ArrayList<>();
|
||||
collectLeafAttribute(this, leafAttributes);
|
||||
return leafAttributes;
|
||||
}
|
||||
|
||||
private void collectLeafAttribute(AnnotationAttribute curr, List<AnnotationAttribute> leafAttributes) {
|
||||
if (ObjectUtil.isNull(curr)) {
|
||||
return;
|
||||
}
|
||||
if (!curr.isWrapped()) {
|
||||
leafAttributes.add(curr);
|
||||
return;
|
||||
}
|
||||
AnnotationAttributeWrapper wrappedAttribute = (AnnotationAttributeWrapper)curr;
|
||||
collectLeafAttribute(wrappedAttribute.getOriginal(), leafAttributes);
|
||||
collectLeafAttribute(wrappedAttribute.getLinked(), leafAttributes);
|
||||
}
|
||||
|
||||
}
|
@ -26,36 +26,36 @@ public class AliasAttributePostProcessor implements SynthesizedAnnotationPostPro
|
||||
|
||||
@Override
|
||||
public void process(SynthesizedAnnotation annotation, SyntheticAnnotation syntheticAnnotation) {
|
||||
Map<String, AnnotationAttribute> attributeMap = annotation.getAttributes();
|
||||
final Map<String, AnnotationAttribute> attributeMap = annotation.getAttributes();
|
||||
|
||||
// 记录别名与属性的关系
|
||||
ForestMap<String, AnnotationAttribute> attributeAliasMappings = new LinkedForestMap<>(false);
|
||||
final ForestMap<String, AnnotationAttribute> attributeAliasMappings = new LinkedForestMap<>(false);
|
||||
attributeMap.forEach((attributeName, attribute) -> {
|
||||
String alias = Opt.ofNullable(attribute.getAnnotation(Alias.class))
|
||||
final String alias = Opt.ofNullable(attribute.getAnnotation(Alias.class))
|
||||
.map(Alias::value)
|
||||
.orElse(null);
|
||||
if (ObjectUtil.isNull(alias)) {
|
||||
return;
|
||||
}
|
||||
AnnotationAttribute aliasAttribute = attributeMap.get(alias);
|
||||
final AnnotationAttribute aliasAttribute = attributeMap.get(alias);
|
||||
Assert.notNull(aliasAttribute, "no method for alias: [{}]", alias);
|
||||
attributeAliasMappings.putLinkedNodes(alias, aliasAttribute, attributeName, attribute);
|
||||
});
|
||||
|
||||
// 处理别名
|
||||
attributeMap.forEach((attributeName, attribute) -> {
|
||||
AnnotationAttribute resolvedAttributeMethod = Opt.ofNullable(attributeName)
|
||||
final AnnotationAttribute resolvedAttribute = Opt.ofNullable(attributeName)
|
||||
.map(attributeAliasMappings::getRootNode)
|
||||
.map(TreeEntry::getValue)
|
||||
.map(aliasAttribute -> (AnnotationAttribute)new ForceAliasedAnnotationAttribute(attribute, aliasAttribute))
|
||||
.orElse(attribute);
|
||||
Assert.isTrue(
|
||||
ObjectUtil.isNull(resolvedAttributeMethod)
|
||||
|| ClassUtil.isAssignable(attribute.getAttribute().getReturnType(), resolvedAttributeMethod.getAttribute().getReturnType()),
|
||||
ObjectUtil.isNull(resolvedAttribute)
|
||||
|| ClassUtil.isAssignable(attribute.getAttributeType(), resolvedAttribute.getAttributeType()),
|
||||
"return type of the root alias method [{}] is inconsistent with the original [{}]",
|
||||
resolvedAttributeMethod.getClass(), attribute.getAttribute().getReturnType()
|
||||
resolvedAttribute.getClass(), attribute.getAttributeType()
|
||||
);
|
||||
attributeMap.put(attributeName, resolvedAttributeMethod);
|
||||
attributeMap.put(attributeName, resolvedAttribute);
|
||||
});
|
||||
annotation.setAttributes(attributeMap);
|
||||
}
|
||||
|
@ -0,0 +1,35 @@
|
||||
package cn.hutool.core.annotation;
|
||||
|
||||
import java.lang.annotation.*;
|
||||
|
||||
/**
|
||||
* <p>表示“原始属性”将作为“关联属性”的别名。
|
||||
* <ul>
|
||||
* <li>当“原始属性”为默认值时,获取“关联属性”将返回“关联属性”本身的值;</li>
|
||||
* <li>当“原始属性”不为默认值时,获取“关联属性”将返回“原始属性”的值;</li>
|
||||
* </ul>
|
||||
* <b>注意,该注解与{@link Link}或{@link MirrorFor}一起使用时,将只有被声明在最上面的注解会生效</b>
|
||||
*
|
||||
* @author huangchengxing
|
||||
* @see Link
|
||||
* @see RelationType#ALIAS_FOR
|
||||
*/
|
||||
@Link(type = RelationType.ALIAS_FOR)
|
||||
@Documented
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Target({ElementType.METHOD, ElementType.ANNOTATION_TYPE})
|
||||
public @interface AliasFor {
|
||||
|
||||
/**
|
||||
* 产生关联的注解类型,当不指定时,默认指注释的属性所在的类
|
||||
*/
|
||||
@Link(annotation = Link.class, attribute = "annotation", type = RelationType.FORCE_ALIAS_FOR)
|
||||
Class<? extends Annotation> annotation() default Annotation.class;
|
||||
|
||||
/**
|
||||
* {@link #annotation()}指定注解中关联的属性
|
||||
*/
|
||||
@Link(annotation = Link.class, attribute = "attribute", type = RelationType.FORCE_ALIAS_FOR)
|
||||
String attribute() default "";
|
||||
|
||||
}
|
@ -1,9 +1,11 @@
|
||||
package cn.hutool.core.annotation;
|
||||
|
||||
import cn.hutool.core.lang.Opt;
|
||||
import cn.hutool.core.util.ObjectUtil;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.function.BinaryOperator;
|
||||
|
||||
/**
|
||||
* 处理注解中带有{@link Link}注解,且{@link Link#type()}为{@link RelationType#FORCE_ALIAS_FOR}
|
||||
@ -22,7 +24,7 @@ public class AliasForLinkAttributePostProcessor implements SynthesizedAnnotation
|
||||
|
||||
@Override
|
||||
public void process(SynthesizedAnnotation annotation, SyntheticAnnotation syntheticAnnotation) {
|
||||
Map<String, AnnotationAttribute> attributeMap = new HashMap<>(annotation.getAttributes());
|
||||
final Map<String, AnnotationAttribute> attributeMap = new HashMap<>(annotation.getAttributes());
|
||||
attributeMap.forEach((originalAttributeName, originalAttribute) -> {
|
||||
// 获取注解
|
||||
final Link link = SyntheticAnnotationUtil.getLink(
|
||||
@ -33,7 +35,7 @@ public class AliasForLinkAttributePostProcessor implements SynthesizedAnnotation
|
||||
}
|
||||
|
||||
// 获取注解属性
|
||||
SynthesizedAnnotation aliasAnnotation = SyntheticAnnotationUtil.getLinkedAnnotation(link, syntheticAnnotation, annotation.annotationType());
|
||||
final SynthesizedAnnotation aliasAnnotation = SyntheticAnnotationUtil.getLinkedAnnotation(link, syntheticAnnotation, annotation.annotationType());
|
||||
if (ObjectUtil.isNull(aliasAnnotation)) {
|
||||
return;
|
||||
}
|
||||
@ -43,12 +45,40 @@ public class AliasForLinkAttributePostProcessor implements SynthesizedAnnotation
|
||||
|
||||
// aliasFor
|
||||
if (RelationType.ALIAS_FOR.equals(link.type())) {
|
||||
aliasAnnotation.setAttributes(aliasAttribute.getAttributeName(), new AliasedAnnotationAttribute(aliasAttribute, originalAttribute));
|
||||
wrappingLinkedAttribute(syntheticAnnotation, originalAttribute, aliasAttribute, AliasedAnnotationAttribute::new);
|
||||
return;
|
||||
}
|
||||
// forceAliasFor
|
||||
aliasAnnotation.setAttributes(aliasAttribute.getAttributeName(), new ForceAliasedAnnotationAttribute(aliasAttribute, originalAttribute));
|
||||
wrappingLinkedAttribute(syntheticAnnotation, originalAttribute, aliasAttribute, ForceAliasedAnnotationAttribute::new);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 对指定注解属性进行包装,若该属性已被包装过,则递归以其为根节点的树结构,对树上全部的叶子节点进行包装
|
||||
*/
|
||||
private void wrappingLinkedAttribute(
|
||||
SyntheticAnnotation syntheticAnnotation, AnnotationAttribute originalAttribute, AnnotationAttribute aliasAttribute, BinaryOperator<AnnotationAttribute> wrapping) {
|
||||
// 不是包装属性
|
||||
if (!aliasAttribute.isWrapped()) {
|
||||
processAttribute(syntheticAnnotation, originalAttribute, aliasAttribute, wrapping);
|
||||
return;
|
||||
}
|
||||
// 是包装属性
|
||||
final AbstractAnnotationAttributeWrapper wrapper = (AbstractAnnotationAttributeWrapper)aliasAttribute;
|
||||
wrapper.getAllLinkedNonWrappedAttributes().forEach(
|
||||
t -> processAttribute(syntheticAnnotation, originalAttribute, t, wrapping)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取指定注解属性,然后将其再进行一层包装
|
||||
*/
|
||||
private void processAttribute(
|
||||
SyntheticAnnotation syntheticAnnotation, AnnotationAttribute originalAttribute,
|
||||
AnnotationAttribute target, BinaryOperator<AnnotationAttribute> wrapping) {
|
||||
Opt.ofNullable(target.getAnnotationType())
|
||||
.map(syntheticAnnotation::getSynthesizedAnnotation)
|
||||
.ifPresent(t -> t.replaceAttribute(target.getAttributeName(), old -> wrapping.apply(old, originalAttribute)));
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -1,29 +1,37 @@
|
||||
package cn.hutool.core.annotation;
|
||||
|
||||
import cn.hutool.core.lang.Assert;
|
||||
|
||||
/**
|
||||
* <p>表示一个具有别名的属性。
|
||||
* 当别名属性值为默认值时,优先返回原属性的值,当别名属性不为默认值时,优先返回别名属性的值
|
||||
*
|
||||
* @author huangchengxing
|
||||
* @see AliasForLinkAttributePostProcessor
|
||||
* @see RelationType#ALIAS_BY
|
||||
* @see RelationType#FORCE_ALIAS_FOR
|
||||
* @see RelationType#ALIAS_FOR
|
||||
*/
|
||||
public class AliasedAnnotationAttribute extends AnnotationAttributeWrapper implements AnnotationAttribute {
|
||||
public class AliasedAnnotationAttribute extends AbstractAnnotationAttributeWrapper {
|
||||
|
||||
private final AnnotationAttribute aliasAttribute;
|
||||
|
||||
protected AliasedAnnotationAttribute(AnnotationAttribute origin, AnnotationAttribute aliasAttribute) {
|
||||
super(origin);
|
||||
Assert.notNull(aliasAttribute, "aliasAttribute must not null");
|
||||
this.aliasAttribute = aliasAttribute;
|
||||
protected AliasedAnnotationAttribute(AnnotationAttribute origin, AnnotationAttribute linked) {
|
||||
super(origin, linked);
|
||||
}
|
||||
|
||||
/**
|
||||
* 若{@link #linked}为默认值,则返回{@link #original}的值,否则返回{@link #linked}的值
|
||||
*
|
||||
* @return 属性值
|
||||
*/
|
||||
@Override
|
||||
public Object getValue() {
|
||||
return aliasAttribute.isValueEquivalentToDefaultValue() ? super.getValue() : aliasAttribute.getValue();
|
||||
return linked.isValueEquivalentToDefaultValue() ? super.getValue() : linked.getValue();
|
||||
}
|
||||
|
||||
/**
|
||||
* 当{@link #original}与{@link #linked}都为默认值时返回{@code true}
|
||||
*
|
||||
* @return 是否
|
||||
*/
|
||||
@Override
|
||||
public boolean isValueEquivalentToDefaultValue() {
|
||||
return linked.isValueEquivalentToDefaultValue() && original.isValueEquivalentToDefaultValue();
|
||||
}
|
||||
}
|
||||
|
@ -16,8 +16,9 @@ import java.lang.reflect.Method;
|
||||
*
|
||||
* @author huangchengxing
|
||||
* @see SynthesizedAnnotationPostProcessor
|
||||
* @see CacheableAnnotationAttribute
|
||||
* @see AnnotationAttributeWrapper
|
||||
* @see CacheableAnnotationAttribute
|
||||
* @see AbstractAnnotationAttributeWrapper
|
||||
* @see ForceAliasedAnnotationAttribute
|
||||
* @see AliasedAnnotationAttribute
|
||||
* @see MirroredAnnotationAttribute
|
||||
@ -38,6 +39,15 @@ public interface AnnotationAttribute {
|
||||
*/
|
||||
Method getAttribute();
|
||||
|
||||
/**
|
||||
* 获取声明属性的注解类
|
||||
*
|
||||
* @return 声明注解的注解类
|
||||
*/
|
||||
default Class<?> getAnnotationType() {
|
||||
return getAttribute().getDeclaringClass();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取属性名称
|
||||
*
|
||||
@ -90,7 +100,7 @@ public interface AnnotationAttribute {
|
||||
* @return boolean
|
||||
*/
|
||||
default boolean isWrapped() {
|
||||
return this instanceof AnnotationAttributeWrapper;
|
||||
return false;
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -1,14 +1,25 @@
|
||||
package cn.hutool.core.annotation;
|
||||
|
||||
import cn.hutool.core.lang.Assert;
|
||||
|
||||
import java.lang.annotation.Annotation;
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.Collection;
|
||||
|
||||
/**
|
||||
* <p>表示一个被包装过的{@link AnnotationAttribute},
|
||||
* 该实例中的一些方法可能会被代理到其他的注解属性对象中,
|
||||
* 从而使得通过原始的注解属性的方法获取到另一注解属性的值。
|
||||
* 该实例中的一些方法可能会被代理到另一个注解属性对象中,
|
||||
* 从而使得通过原始的注解属性的方法获取到另一注解属性的值。<br>
|
||||
* 除了{@link #getValue()}以外,其他方法的返回值应当尽可能与{@link #getOriginal()}
|
||||
* 返回的{@link AnnotationAttribute}对象的方法返回值一致。
|
||||
*
|
||||
* <p>当包装类被包装了多层后,则规则生效优先级按包装的先后顺序倒序排序,
|
||||
* 比如a、b互为镜像,此时a、b两属性应当都被{@link MirroredAnnotationAttribute}包装,
|
||||
* 若再指定c为a的别名字段,则c、a、b都要在原基础上再次包装一层{@link AliasedAnnotationAttribute}。<br>
|
||||
* 此时a、b同时被包装了两层,则执行时,优先执行{@link AliasedAnnotationAttribute}的逻辑,
|
||||
* 当该规则不生效时,比如c只有默认值,此时上一次的{@link MirroredAnnotationAttribute}的逻辑才会生效。
|
||||
*
|
||||
* <p>被包装的{@link AnnotationAttribute}实际结构为一颗二叉树,
|
||||
* 当包装类再次被包装时,实际上等于又添加了一个新的根节点,
|
||||
* 此时需要同时更新树的全部关联叶子节点。
|
||||
*
|
||||
* @author huangchengxing
|
||||
* @see AnnotationAttribute
|
||||
@ -16,42 +27,101 @@ import java.lang.reflect.Method;
|
||||
* @see AliasedAnnotationAttribute
|
||||
* @see MirroredAnnotationAttribute
|
||||
*/
|
||||
public abstract class AnnotationAttributeWrapper implements AnnotationAttribute {
|
||||
public interface AnnotationAttributeWrapper extends AnnotationAttribute {
|
||||
|
||||
protected final AnnotationAttribute origin;
|
||||
// =========================== 新增方法 ===========================
|
||||
|
||||
protected AnnotationAttributeWrapper(AnnotationAttribute origin) {
|
||||
Assert.notNull(origin, "target must not null");
|
||||
this.origin = origin;
|
||||
/**
|
||||
* 获取被包装的{@link AnnotationAttribute}对象,该对象也可能是{@link AnnotationAttribute}
|
||||
*
|
||||
* @return 被包装的{@link AnnotationAttribute}对象
|
||||
*/
|
||||
AnnotationAttribute getOriginal();
|
||||
|
||||
/**
|
||||
* 获取最初的被包装的{@link AnnotationAttribute}
|
||||
*
|
||||
* @return 最初的被包装的{@link AnnotationAttribute}
|
||||
*/
|
||||
AnnotationAttribute getNonWrappedOriginal();
|
||||
|
||||
/**
|
||||
* 获取包装{@link #getOriginal()}的{@link AnnotationAttribute}对象,该对象也可能是{@link AnnotationAttribute}
|
||||
*
|
||||
* @return 包装对象
|
||||
*/
|
||||
AnnotationAttribute getLinked();
|
||||
|
||||
/**
|
||||
* 遍历以当前实例为根节点的树结构,获取所有未被包装的属性
|
||||
*
|
||||
* @return 叶子节点
|
||||
*/
|
||||
Collection<AnnotationAttribute> getAllLinkedNonWrappedAttributes();
|
||||
|
||||
// =========================== 代理实现 ===========================
|
||||
|
||||
/**
|
||||
* 获取注解对象
|
||||
*
|
||||
* @return 注解对象
|
||||
*/
|
||||
@Override
|
||||
default Annotation getAnnotation() {
|
||||
return getOriginal().getAnnotation();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取注解属性对应的方法
|
||||
*
|
||||
* @return 注解属性对应的方法
|
||||
*/
|
||||
@Override
|
||||
public Annotation getAnnotation() {
|
||||
return origin.getAnnotation();
|
||||
default Method getAttribute() {
|
||||
return getOriginal().getAttribute();
|
||||
}
|
||||
|
||||
/**
|
||||
* 该注解属性的值是否等于默认值 <br>
|
||||
* 默认仅当{@link #getOriginal()}与{@link #getLinked()}返回的注解属性
|
||||
* 都为默认值时,才返回{@code true}
|
||||
*
|
||||
* @return 该注解属性的值是否等于默认值
|
||||
*/
|
||||
@Override
|
||||
public Method getAttribute() {
|
||||
return origin.getAttribute();
|
||||
default boolean isValueEquivalentToDefaultValue() {
|
||||
return getOriginal().isValueEquivalentToDefaultValue() && getLinked().isValueEquivalentToDefaultValue();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取属性类型
|
||||
*
|
||||
* @return 属性类型
|
||||
*/
|
||||
@Override
|
||||
public boolean isValueEquivalentToDefaultValue() {
|
||||
return origin.isValueEquivalentToDefaultValue();
|
||||
default Class<?> getAttributeType() {
|
||||
return getOriginal().getAttributeType();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取属性上的注解
|
||||
*
|
||||
* @param annotationType 注解类型
|
||||
* @return 注解对象
|
||||
*/
|
||||
@Override
|
||||
public Class<?> getAttributeType() {
|
||||
return origin.getAttributeType();
|
||||
default <T extends Annotation> T getAnnotation(Class<T> annotationType) {
|
||||
return getOriginal().getAnnotation(annotationType);
|
||||
}
|
||||
|
||||
/**
|
||||
* 当前注解属性是否已经被{@link AnnotationAttributeWrapper}包装
|
||||
*
|
||||
* @return boolean
|
||||
*/
|
||||
@Override
|
||||
public <T extends Annotation> T getAnnotation(Class<T> annotationType) {
|
||||
return origin.getAnnotation(annotationType);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isWrapped() {
|
||||
default boolean isWrapped() {
|
||||
return true;
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -359,7 +359,8 @@ public class AnnotationUtil {
|
||||
* @see SyntheticAnnotation
|
||||
*/
|
||||
public static <T extends Annotation> T getSynthesisAnnotation(Annotation annotation, Class<T> annotationType) {
|
||||
return SyntheticAnnotation.of(annotation).getAnnotation(annotationType);
|
||||
// TODO 缓存合成注解信息,避免重复解析
|
||||
return SyntheticAnnotation.of(annotation).syntheticAnnotation(annotationType);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -385,6 +386,30 @@ public class AnnotationUtil {
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取元素上距离指定元素最接近的合成注解
|
||||
* <ul>
|
||||
* <li>若元素是类,则递归解析全部父类和全部父接口上的注解;</li>
|
||||
* <li>若元素是方法、属性或注解,则只解析其直接声明的注解;</li>
|
||||
* </ul>
|
||||
*
|
||||
* @param annotatedEle {@link AnnotatedElement},可以是Class、Method、Field、Constructor、ReflectPermission
|
||||
* @param annotationType 注解类
|
||||
* @param <T> 注解类型
|
||||
* @return 合成注解
|
||||
* @see SyntheticAnnotation
|
||||
*/
|
||||
public static <T extends Annotation> T getSyntheticAnnotation(AnnotatedElement annotatedEle, Class<T> annotationType) {
|
||||
AnnotationScanner[] scanners = new AnnotationScanner[]{
|
||||
new MetaAnnotationScanner(), new TypeAnnotationScanner(), new MethodAnnotationScanner(), new FieldAnnotationScanner()
|
||||
};
|
||||
return AnnotationScanner.scanByAnySupported(annotatedEle, scanners).stream()
|
||||
.map(annotation -> getSynthesisAnnotation(annotation, annotationType))
|
||||
.filter(Objects::nonNull)
|
||||
.findFirst()
|
||||
.orElse(null);
|
||||
}
|
||||
|
||||
/**
|
||||
* 扫描注解类,以及注解类的{@link Class}层级结构中的注解,将返回除了{@link #META_ANNOTATIONS}中指定的JDK默认注解外,
|
||||
* 按元注解对象与{@code annotationType}的距离和{@link Class#getAnnotations()}顺序排序的注解对象集合
|
||||
|
@ -8,7 +8,7 @@ import java.util.Collection;
|
||||
import java.util.Comparator;
|
||||
|
||||
/**
|
||||
* 带缓存功能的{@link SynthesizedAnnotationAttributeProcessor}实现,
|
||||
* <p>带缓存功能的{@link SynthesizedAnnotationAttributeProcessor}实现,
|
||||
* 构建时需要传入比较器,获取属性值时将根据比较器对合成注解进行排序,
|
||||
* 然后选择具有所需属性的,排序最靠前的注解用于获取属性值
|
||||
*
|
||||
|
@ -1,40 +1,49 @@
|
||||
package cn.hutool.core.annotation;
|
||||
|
||||
import cn.hutool.core.lang.Assert;
|
||||
|
||||
/**
|
||||
* 表示一个被指定了强制别名的注解属性。
|
||||
* 当调用{@link #getValue()}时,总是返回{@link #aliasAttribute}的值
|
||||
* 当调用{@link #getValue()}时,总是返回{@link #linked}的值
|
||||
*
|
||||
* @author huangchengxing
|
||||
* @see AliasAttributePostProcessor
|
||||
* @see AliasForLinkAttributePostProcessor
|
||||
* @see RelationType#FORCE_ALIAS_BY
|
||||
* @see RelationType#ALIAS_FOR
|
||||
* @see RelationType#FORCE_ALIAS_FOR
|
||||
*/
|
||||
public class ForceAliasedAnnotationAttribute extends AnnotationAttributeWrapper implements AnnotationAttribute {
|
||||
public class ForceAliasedAnnotationAttribute extends AbstractAnnotationAttributeWrapper {
|
||||
|
||||
private final AnnotationAttribute aliasAttribute;
|
||||
|
||||
protected ForceAliasedAnnotationAttribute(AnnotationAttribute origin, AnnotationAttribute aliasAttribute) {
|
||||
super(origin);
|
||||
Assert.notNull(aliasAttribute, "aliasAttribute must not null");
|
||||
this.aliasAttribute = aliasAttribute;
|
||||
protected ForceAliasedAnnotationAttribute(AnnotationAttribute origin, AnnotationAttribute linked) {
|
||||
super(origin, linked);
|
||||
}
|
||||
|
||||
/**
|
||||
* 总是返回{@link #linked}的{@link AnnotationAttribute#getValue()}的返回值
|
||||
*
|
||||
* @return {@link #linked}的{@link AnnotationAttribute#getValue()}的返回值
|
||||
*/
|
||||
@Override
|
||||
public Object getValue() {
|
||||
return aliasAttribute.getValue();
|
||||
return linked.getValue();
|
||||
}
|
||||
|
||||
/**
|
||||
* 总是返回{@link #linked}的{@link AnnotationAttribute#isValueEquivalentToDefaultValue()}的返回值
|
||||
*
|
||||
* @return {@link #linked}的{@link AnnotationAttribute#isValueEquivalentToDefaultValue()}的返回值
|
||||
*/
|
||||
@Override
|
||||
public boolean isValueEquivalentToDefaultValue() {
|
||||
return aliasAttribute.isValueEquivalentToDefaultValue();
|
||||
return linked.isValueEquivalentToDefaultValue();
|
||||
}
|
||||
|
||||
/**
|
||||
* 总是返回{@link #linked}的{@link AnnotationAttribute#getAttributeType()}的返回值
|
||||
*
|
||||
* @return {@link #linked}的{@link AnnotationAttribute#getAttributeType()}的返回值
|
||||
*/
|
||||
@Override
|
||||
public Class<?> getAttributeType() {
|
||||
return aliasAttribute.getAttributeType();
|
||||
return linked.getAttributeType();
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -5,7 +5,7 @@ import java.lang.annotation.*;
|
||||
/**
|
||||
* <p>用于在同一注解中,或具有一定关联的不同注解的属性中,表明这些属性之间具有特定的关联关系。
|
||||
* 在通过{@link SyntheticAnnotation}获取合成注解后,合成注解获取属性值时会根据该注解进行调整。<br />
|
||||
* <b>注意:该注解的优先级低于{@link Alias}
|
||||
* <b>注意:该注解的优先级低于{@link Alias};且与{@link MirrorFor}或{@link AliasFor}一起使用时,将只有被声明在最上面的注解会生效</b>
|
||||
*
|
||||
* @author huangchengxing
|
||||
* @see SyntheticAnnotation
|
||||
|
@ -0,0 +1,38 @@
|
||||
package cn.hutool.core.annotation;
|
||||
|
||||
import java.lang.annotation.*;
|
||||
|
||||
/**
|
||||
* <p>表示注解的属性与指定的属性互为镜像,通过一个属性将能够获得对方的值。<br>
|
||||
* 它们遵循下述规则:
|
||||
* <ul>
|
||||
* <li>互为镜像的两个属性,必须同时通过指定模式为{@code MIRROR_FOR}的{@link Link}注解指定对方;</li>
|
||||
* <li>互为镜像的两个属性,类型必须一致;</li>
|
||||
* <li>互为镜像的两个属性在获取值,且两者的值皆不同时,必须且仅允许有一个非默认值,该值被优先返回;</li>
|
||||
* <li>互为镜像的两个属性,在值都为默认值或都不为默认值时,两者的值必须相等;</li>
|
||||
* </ul>
|
||||
* <b>注意,该注解与{@link Link}或{@link AliasFor}一起使用时,将只有被声明在最上面的注解会生效</b>
|
||||
*
|
||||
* @author huangchengxing
|
||||
* @see Link
|
||||
* @see RelationType#MIRROR_FOR
|
||||
*/
|
||||
@Link(type = RelationType.MIRROR_FOR)
|
||||
@Documented
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Target({ElementType.METHOD, ElementType.ANNOTATION_TYPE})
|
||||
public @interface MirrorFor {
|
||||
|
||||
/**
|
||||
* 产生关联的注解类型,当不指定时,默认指注释的属性所在的类
|
||||
*/
|
||||
@Link(annotation = Link.class, attribute = "annotation", type = RelationType.FORCE_ALIAS_FOR)
|
||||
Class<? extends Annotation> annotation() default Annotation.class;
|
||||
|
||||
/**
|
||||
* {@link #annotation()}指定注解中关联的属性
|
||||
*/
|
||||
@Link(annotation = Link.class, attribute = "attribute", type = RelationType.FORCE_ALIAS_FOR)
|
||||
String attribute() default "";
|
||||
|
||||
}
|
@ -46,10 +46,10 @@ public class MirrorLinkAttributePostProcessor implements SynthesizedAnnotationPo
|
||||
|
||||
// 包装这一对镜像属性,并替换原注解中的对应属性
|
||||
final AnnotationAttribute mirroredOriginalAttribute = new MirroredAnnotationAttribute(originalAttribute, mirrorAttribute);
|
||||
syntheticAnnotation.getSynthesizedAnnotation(originalAttribute.getAttribute().getDeclaringClass())
|
||||
.setAttributes(originalAttributeName, mirroredOriginalAttribute);
|
||||
syntheticAnnotation.getSynthesizedAnnotation(originalAttribute.getAnnotationType())
|
||||
.setAttribute(originalAttributeName, mirroredOriginalAttribute);
|
||||
final AnnotationAttribute mirroredTargetAttribute = new MirroredAnnotationAttribute(mirrorAttribute, originalAttribute);
|
||||
mirrorAnnotation.setAttributes(link.attribute(), mirroredTargetAttribute);
|
||||
mirrorAnnotation.setAttribute(link.attribute(), mirroredTargetAttribute);
|
||||
});
|
||||
}
|
||||
|
||||
@ -59,7 +59,7 @@ public class MirrorLinkAttributePostProcessor implements SynthesizedAnnotationPo
|
||||
// 镜像属性返回值必须一致
|
||||
SyntheticAnnotationUtil.checkAttributeType(original, mirror);
|
||||
// 镜像属性上必须存在对应的注解
|
||||
final Link mirrorAttributeAnnotation = mirror.getAnnotation(Link.class);
|
||||
final Link mirrorAttributeAnnotation = SyntheticAnnotationUtil.getLink(mirror, RelationType.MIRROR_FOR);
|
||||
Assert.isTrue(
|
||||
ObjectUtil.isNotNull(mirrorAttributeAnnotation) && RelationType.MIRROR_FOR.equals(mirrorAttributeAnnotation.type()),
|
||||
"mirror attribute [{}] of original attribute [{}] must marked by @Link, and also @LinkType.type() must is [{}]",
|
||||
|
@ -9,28 +9,25 @@ import cn.hutool.core.lang.Assert;
|
||||
* @see MirrorLinkAttributePostProcessor
|
||||
* @see RelationType#MIRROR_FOR
|
||||
*/
|
||||
public class MirroredAnnotationAttribute extends AnnotationAttributeWrapper implements AnnotationAttribute {
|
||||
public class MirroredAnnotationAttribute extends AbstractAnnotationAttributeWrapper {
|
||||
|
||||
private final AnnotationAttribute mirrorAttribute;
|
||||
|
||||
public MirroredAnnotationAttribute(AnnotationAttribute origin, AnnotationAttribute mirrorAttribute) {
|
||||
super(origin);
|
||||
this.mirrorAttribute = mirrorAttribute;
|
||||
public MirroredAnnotationAttribute(AnnotationAttribute origin, AnnotationAttribute linked) {
|
||||
super(origin, linked);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object getValue() {
|
||||
boolean originIsDefault = origin.isValueEquivalentToDefaultValue();
|
||||
boolean targetIsDefault = mirrorAttribute.isValueEquivalentToDefaultValue();
|
||||
Object originValue = origin.getValue();
|
||||
Object targetValue = mirrorAttribute.getValue();
|
||||
final boolean originIsDefault = original.isValueEquivalentToDefaultValue();
|
||||
final boolean targetIsDefault = linked.isValueEquivalentToDefaultValue();
|
||||
final Object originValue = original.getValue();
|
||||
final Object targetValue = linked.getValue();
|
||||
|
||||
// 都为默认值,或都为非默认值时,两方法的返回值必须相等
|
||||
if (originIsDefault == targetIsDefault) {
|
||||
Assert.equals(
|
||||
originValue, targetValue,
|
||||
"the values of attributes [{}] and [{}] that mirror each other are different: [{}] <==> [{}]",
|
||||
origin.getAttribute(), mirrorAttribute.getAttribute(), originValue, targetValue
|
||||
original.getAttribute(), linked.getAttribute(), originValue, targetValue
|
||||
);
|
||||
return originValue;
|
||||
}
|
||||
@ -38,4 +35,14 @@ public class MirroredAnnotationAttribute extends AnnotationAttributeWrapper impl
|
||||
// 两者有一者不为默认值时,优先返回非默认值
|
||||
return originIsDefault ? targetValue : originValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* 当{@link #original}与{@link #linked}都为默认值时返回{@code true}
|
||||
*
|
||||
* @return 是否
|
||||
*/
|
||||
@Override
|
||||
public boolean isValueEquivalentToDefaultValue() {
|
||||
return original.isValueEquivalentToDefaultValue() && linked.isValueEquivalentToDefaultValue();
|
||||
}
|
||||
}
|
||||
|
@ -4,6 +4,7 @@ import cn.hutool.core.collection.CollUtil;
|
||||
|
||||
import java.lang.annotation.Annotation;
|
||||
import java.util.Map;
|
||||
import java.util.function.UnaryOperator;
|
||||
|
||||
/**
|
||||
* 用于在{@link SyntheticAnnotation}中表示一个处于合成状态的注解对象
|
||||
@ -82,7 +83,7 @@ public interface SynthesizedAnnotation extends Annotation {
|
||||
*/
|
||||
default void setAttributes(Map<String, AnnotationAttribute> attributes) {
|
||||
if (CollUtil.isNotEmpty(attributes)) {
|
||||
attributes.forEach(this::setAttributes);
|
||||
attributes.forEach(this::setAttribute);
|
||||
}
|
||||
}
|
||||
|
||||
@ -92,7 +93,15 @@ public interface SynthesizedAnnotation extends Annotation {
|
||||
* @param attributeName 属性名称
|
||||
* @param attribute 注解属性
|
||||
*/
|
||||
void setAttributes(String attributeName, AnnotationAttribute attribute);
|
||||
void setAttribute(String attributeName, AnnotationAttribute attribute);
|
||||
|
||||
/**
|
||||
* 替换属性值
|
||||
*
|
||||
* @param attributeName 属性名
|
||||
* @param operator 替换操作
|
||||
*/
|
||||
void replaceAttribute(String attributeName, UnaryOperator<AnnotationAttribute> operator);
|
||||
|
||||
/**
|
||||
* 获取属性值
|
||||
|
@ -88,7 +88,7 @@ class SyntheticAnnotationProxy implements InvocationHandler {
|
||||
methods.put("getHorizontalDistance", (method, args) -> annotation.getHorizontalDistance());
|
||||
methods.put("hasAttribute", (method, args) -> annotation.hasAttribute((String)args[0], (Class<?>)args[1]));
|
||||
methods.put("getAttributes", (method, args) -> annotation.getAttributes());
|
||||
methods.put("setAttributes", (method, args) -> {
|
||||
methods.put("setAttribute", (method, args) -> {
|
||||
throw new UnsupportedOperationException("proxied annotation can not reset attributes");
|
||||
});
|
||||
methods.put("getAttributeValue", (method, args) -> annotation.getAttributeValue((String)args[0]));
|
||||
|
@ -23,7 +23,7 @@ class SyntheticAnnotationUtil {
|
||||
*/
|
||||
static Link getLink(AnnotationAttribute attribute, RelationType... relationTypes) {
|
||||
return Opt.ofNullable(attribute)
|
||||
.map(t -> t.getAnnotation(Link.class))
|
||||
.map(t -> AnnotationUtil.getSyntheticAnnotation(attribute.getAttribute(), Link.class))
|
||||
.filter(a -> ArrayUtil.contains(relationTypes, a.type()))
|
||||
.get();
|
||||
}
|
||||
|
@ -10,6 +10,7 @@ import java.lang.annotation.Annotation;
|
||||
import java.lang.reflect.AnnotatedElement;
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.*;
|
||||
import java.util.function.UnaryOperator;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
@ -117,6 +118,7 @@ public class SyntheticMetaAnnotation implements SyntheticAnnotation {
|
||||
this.metaAnnotationMap = new LinkedHashMap<>();
|
||||
|
||||
// 初始化元注解信息,并进行后置处理
|
||||
// TODO 缓存元注解信息,避免重复解析
|
||||
loadMetaAnnotations();
|
||||
annotationPostProcessors.forEach(processor ->
|
||||
metaAnnotationMap.values().forEach(synthesized -> processor.process(synthesized, this))
|
||||
@ -411,10 +413,24 @@ public class SyntheticMetaAnnotation implements SyntheticAnnotation {
|
||||
* @param attribute 注解属性
|
||||
*/
|
||||
@Override
|
||||
public void setAttributes(String attributeName, AnnotationAttribute attribute) {
|
||||
public void setAttribute(String attributeName, AnnotationAttribute attribute) {
|
||||
attributeMethodCaches.put(attributeName, attribute);
|
||||
}
|
||||
|
||||
/**
|
||||
* 替换属性值
|
||||
*
|
||||
* @param attributeName 属性名
|
||||
* @param operator 替换操作
|
||||
*/
|
||||
@Override
|
||||
public void replaceAttribute(String attributeName, UnaryOperator<AnnotationAttribute> operator) {
|
||||
AnnotationAttribute old = attributeMethodCaches.get(attributeName);
|
||||
if (ObjectUtil.isNotNull(old)) {
|
||||
attributeMethodCaches.put(attributeName, operator.apply(old));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取属性值
|
||||
*
|
||||
|
@ -1,5 +1,6 @@
|
||||
package cn.hutool.core.annotation;
|
||||
|
||||
import cn.hutool.core.util.ReflectUtil;
|
||||
import org.junit.Assert;
|
||||
import org.junit.Test;
|
||||
|
||||
@ -7,6 +8,7 @@ import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
import java.lang.reflect.Method;
|
||||
|
||||
/**
|
||||
* 合成注解{@link SyntheticMetaAnnotation}的测试用例
|
||||
@ -53,6 +55,15 @@ public class SyntheticMetaAnnotationTest {
|
||||
Assert.assertThrows(IllegalArgumentException.class, () -> new SyntheticMetaAnnotation(grandParentAnnotation));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void linkTest() {
|
||||
Method method = ReflectUtil.getMethod(AnnotationForLinkTest.class, "value");
|
||||
SyntheticAnnotation syntheticAnnotation = new SyntheticMetaAnnotation(method.getAnnotation(AliasFor.class));
|
||||
Link link = syntheticAnnotation.syntheticAnnotation(Link.class);
|
||||
Assert.assertEquals(AnnotationForLinkTest.class, link.annotation());
|
||||
Assert.assertEquals("name", link.attribute());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void mirrorAttributeTest() {
|
||||
AnnotationForMirrorTest annotation = ClassForMirrorTest.class.getAnnotation(AnnotationForMirrorTest.class);
|
||||
@ -108,6 +119,31 @@ public class SyntheticMetaAnnotationTest {
|
||||
Assert.assertEquals("Foo", childAnnotation.value());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void aliasForAndMirrorTest() {
|
||||
AnnotationForMirrorThenAliasForTest annotation = ClassForAliasForAndMirrorTest.class.getAnnotation(AnnotationForMirrorThenAliasForTest.class);
|
||||
SyntheticAnnotation synthetic = new SyntheticMetaAnnotation(annotation);
|
||||
MetaAnnotationForMirrorThenAliasForTest metaAnnotation = synthetic.syntheticAnnotation(MetaAnnotationForMirrorThenAliasForTest.class);
|
||||
Assert.assertEquals("test", metaAnnotation.name());
|
||||
Assert.assertEquals("test", metaAnnotation.value());
|
||||
AnnotationForMirrorThenAliasForTest childAnnotation = synthetic.syntheticAnnotation(AnnotationForMirrorThenAliasForTest.class);
|
||||
Assert.assertEquals("test", childAnnotation.childValue());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void multiAliasForTest() {
|
||||
AnnotationForMultiAliasForTest annotation = ClassForMultiAliasForTest.class.getAnnotation(AnnotationForMultiAliasForTest.class);
|
||||
SyntheticAnnotation synthetic = new SyntheticMetaAnnotation(annotation);
|
||||
|
||||
MetaAnnotationForMultiAliasForTest1 metaAnnotation1 = synthetic.syntheticAnnotation(MetaAnnotationForMultiAliasForTest1.class);
|
||||
Assert.assertEquals("test", metaAnnotation1.name());
|
||||
Assert.assertEquals("test", metaAnnotation1.value1());
|
||||
MetaAnnotationForMultiAliasForTest2 metaAnnotation2 = synthetic.syntheticAnnotation(MetaAnnotationForMultiAliasForTest2.class);
|
||||
Assert.assertEquals("test", metaAnnotation2.value2());
|
||||
AnnotationForMultiAliasForTest childAnnotation = synthetic.syntheticAnnotation(AnnotationForMultiAliasForTest.class);
|
||||
Assert.assertEquals("test", childAnnotation.value3());
|
||||
}
|
||||
|
||||
// 注解结构如下:
|
||||
// AnnotatedClass -> @ChildAnnotation -> @ParentAnnotation -> @GrandParentAnnotation
|
||||
// -> @GrandParentAnnotation
|
||||
@ -143,9 +179,11 @@ public class SyntheticMetaAnnotationTest {
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Target({ ElementType.METHOD, ElementType.TYPE })
|
||||
@interface AnnotationForMirrorTest {
|
||||
@Link(attribute = "name")
|
||||
//@Link(attribute = "name")
|
||||
@MirrorFor(attribute = "name")
|
||||
String value() default "";
|
||||
@Link(attribute = "value")
|
||||
//@Link(attribute = "value")
|
||||
@MirrorFor(attribute = "value")
|
||||
String name() default "";
|
||||
}
|
||||
@AnnotationForMirrorTest("Foo")
|
||||
@ -164,10 +202,9 @@ public class SyntheticMetaAnnotationTest {
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Target({ ElementType.METHOD, ElementType.TYPE })
|
||||
@interface AnnotationForAliasForTest {
|
||||
@Link(
|
||||
@AliasFor(
|
||||
annotation = MetaAnnotationForAliasForTest.class,
|
||||
attribute = "name",
|
||||
type = RelationType.ALIAS_FOR
|
||||
attribute = "name"
|
||||
)
|
||||
String value() default "";
|
||||
}
|
||||
@ -197,4 +234,53 @@ public class SyntheticMetaAnnotationTest {
|
||||
@AnnotationForceForAliasForTest("Foo")
|
||||
static class ClassForForceAliasForTest2 {}
|
||||
|
||||
@interface AnnotationForLinkTest {
|
||||
@AliasFor(attribute = "name", annotation = AnnotationForLinkTest.class)
|
||||
String value() default "value";
|
||||
String name() default "name";
|
||||
}
|
||||
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Target({ ElementType.METHOD, ElementType.TYPE })
|
||||
@interface MetaAnnotationForMirrorThenAliasForTest {
|
||||
@MirrorFor(attribute = "value")
|
||||
String name() default "";
|
||||
@MirrorFor(attribute = "name")
|
||||
String value() default "";
|
||||
}
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Target({ ElementType.METHOD, ElementType.TYPE })
|
||||
@MetaAnnotationForMirrorThenAliasForTest("Meta")
|
||||
@interface AnnotationForMirrorThenAliasForTest {
|
||||
@AliasFor(attribute = "name", annotation = MetaAnnotationForMirrorThenAliasForTest.class)
|
||||
String childValue() default "value";
|
||||
}
|
||||
@AnnotationForMirrorThenAliasForTest(childValue = "test")
|
||||
static class ClassForAliasForAndMirrorTest{}
|
||||
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Target({ ElementType.METHOD, ElementType.TYPE })
|
||||
@interface MetaAnnotationForMultiAliasForTest1 {
|
||||
@MirrorFor(attribute = "value1")
|
||||
String name() default "";
|
||||
@MirrorFor(attribute = "name")
|
||||
String value1() default "";
|
||||
}
|
||||
@MetaAnnotationForMultiAliasForTest1
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Target({ ElementType.METHOD, ElementType.TYPE })
|
||||
@interface MetaAnnotationForMultiAliasForTest2 {
|
||||
@AliasFor(attribute = "name", annotation = MetaAnnotationForMultiAliasForTest1.class)
|
||||
String value2() default "";
|
||||
}
|
||||
@MetaAnnotationForMultiAliasForTest2
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Target({ ElementType.METHOD, ElementType.TYPE })
|
||||
@interface AnnotationForMultiAliasForTest {
|
||||
@AliasFor(attribute = "value2", annotation = MetaAnnotationForMultiAliasForTest2.class)
|
||||
String value3() default "value";
|
||||
}
|
||||
@AnnotationForMultiAliasForTest(value3 = "test")
|
||||
static class ClassForMultiAliasForTest{}
|
||||
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user