共计 9645 个字符,预计需要花费 25 分钟才能阅读完成。
能够看到当咱们在 B 类上增加注解 @InheritClass 并指定 A1.class 和 A2.class 之后,咱们的 B 实例就有了 A1 和 A2 的属性和办法
就如同 B 同时继承了 A1 和 A2
这。。。难道是黑魔法?(为什么脑子里会忽然冒出来巴啦啦能量?)
来人,把.class 文件带上来
其实就是把 A1 和 A2 的属性和办法都复制到了 B 上,和继承没有半毛钱关系!
这玩意儿有啥用
说起来当初实现的性能和当初的目标还是有点出入的
家喻户晓,Lombok 中提供了 @Builder 的注解来生成一个类对应的 Builder
然而我想在 build 之前校验某些字段就不太好实现
于是我就思考,能不能实现一个注解,只是生成对应的字段和办法(毕竟最麻烦的就是要复制一堆的属性),而 build 办法由咱们本人来实现,相似上面的代码
public class A {
private String a;
public A(String a) {this.a = a;}
@BuilderWith(A.class)
public static class Builder {// 注解主动生成 a 属性和 a(String a) 办法
public A build() {if (a == null) {throw new IllegalArgumentException("a is null");
}
return new A(a);
}
}
}
复制代码
这样的话,咱们不仅不必手动解决大量的属性,还能够在 build 之前退出额定的逻辑,不至于像 Lombok 的 @Builder 那么不灵便
而后在前面实现的过程中就发现:
能够把一个类的属性复制过去,那也能够把一个类的办法复制过去!
能够复制一个类,那也能够复制多个类!
于是就倒退成了当初这样,给人一种多继承的错觉
所以说这种形式也会存在很多限度和抵触,比方雷同名称但不同类型的字段,雷同名称雷同入参但不同返回值的办法,或是调用了 super 的办法等等,毕竟只是一个缝合怪
这兴许就是 Java 不反对多继承的次要起因,不然要校验要留神的中央就太多了,一不小心就会有歧义,出问题
目前我次要能想到两种应用场景
Builder
Builder 原本就是我最后的目标,所以必定要想着法儿的实现
public class A {
private String a;
public A(String a) {this.a = a;}
@InheritField(sources = A.class, flags = InheritFlag.BUILDER)
public static class Builder {// 注解主动生成 a 属性和 a(String a) 办法
public A build() {if (a == null) {throw new IllegalArgumentException("a is null");
}
return new A(a);
}
}
}
复制代码
这个用法和之前构想的没有太大区别,就是对应的注解有点不太一样
@InheritField 能够用来复制属性,而后 flags = InheritFlag.BUILDER 示意同时生成属性对应的办法
参数组合
另一种场景就是用来组合参数
比方咱们当初有两个实体 A 和 B
@Data
public class A {
private String a1;
private String a2;
...
private String a20;
}
@Data
public class B {
private String b1;
private String b2;
...
private String b30;
}
复制代码
之前遇到过一些相似的场景,有一些比拟老的我的项目,要加参数然而不能改参数的构造
个别状况下,如果要一个入参接管所有的参数咱们会这样写
@Data
public class Params extends B {
private String a1;
private String a2;
...
private String a20;
}
复制代码
新写一个类继承属性多的 B,而后把 A 的属性复制过来
然而如果批改了 A 就要同时批改这个新的类
如果用咱们的这个就是这样的
@InheritField(sources = {A.class, B.class}, flags = {InheritFlag.GETTER, InheritFlag.SETTER})
public class Params {
}
复制代码
不须要手动复制了,A 和 B 如果有批改也会主动同步
当然这个性能也是很只因肋了,因为我想不出还有其余的用法了,哭
手把手教你实现
要实现这个性能须要别离实现对应的注解处理器和 IDEA 插件
注解处理器用于在编译的时候依据注解生成对应的代码
IDEA 插件用于在标记注解后可能有对应的提醒
Annotation Processor
咱们先来实现注解处理器
public class InheritProcessor extends AbstractProcessor {
@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {// 自定义的解决流程}
@Override
public Set<String> getSupportedAnnotationTypes() {
// 须要扫描的注解的全限定名
return new HashSet<>();}
}
复制代码
首先咱们要继承 javax.annotation.processing.AbstractProcessor 这个类
其中 getSupportedAnnotationTypes 办法中返回须要扫描的注解的全限定名
而后就能够在 process 办法中增加本人的逻辑了,第一个参数 Set<? extends TypeElement> annotations 就是扫描到的注解
咱们先拿到这些注解标注的类
public class InheritProcessor extends AbstractProcessor {
@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {for (TypeElement annotation : annotations) {
// 取得标注了注解的类
Set<? extends Element> targetClassElements = roundEnv.getElementsAnnotatedWith(annotation);
}
}
}
复制代码
通过第二个参数 RoundEnvironment 的办法 getElementsAnnotatedWith 就能取得标注了注解的类
接着咱们来取得这些类的语法树,取得这些类的语法树之后,咱们就能够通过语法树来批改这个类了
JavacElements elementUtils = (JavacElements) processingEnv.getElementUtils();
JCTree.JCClassDecl targetClassDef = (JCTree.JCClassDecl) elementUtils.getTree(targetClassElement);
复制代码
processingEnv 是 AbstractProcessor 中自带的,间接用就行了,通过 processingEnv 能够取得 JavacElements 对象
再通过 JavacElements 就能够取得类的语法树 JCTree.JCClassDecl
为了前面更好辨别,咱们把这些标注了注解的类叫做【指标类】,把注解上标记的类叫做【起源类】,咱们要将【起源类】中的字段和办法复制到【指标类】中
咱们只有拿到【起源类】的语法树,就能够取得对应的字段和办法而后增加到【指标类】的语法树中
先通过【指标类】取得类上的注解而后筛选出咱们须要的注解,这里我的注解因为反对了 @Repeatable,所以还要多解析一步
// 取得类上所有的注解
List<? extends AnnotationMirror> annotations = targetClassElement.getAnnotationMirrors();
// 解析 @Repeatable 取得理论的注解
List<AnnotationMirror> children = (List<AnnotationMirror>)annotation.getElementValues().values();
复制代码
拿到注解之后,就能够取得注解上的属性
private Map<String, Object> getAttributes(AnnotationMirror annotation) {
Map<String, Object> attributes = new LinkedHashMap<>();
for (Map.Entry<? extends ExecutableElement, ? extends AnnotationValue> entry : annotation.getElementValues().entrySet()) {Symbol.MethodSymbol key = (Symbol.MethodSymbol) entry.getKey();
attributes.put(key.getQualifiedName().toString(), entry.getValue().getValue());
}
return attributes;
}
复制代码
通过办法 getElementValues 就能够取得注解办法和返回值的键值对,其中键为办法,所以间接强转 Symbol.MethodSymbol 就行了
而对应的值是特定了类型
值的类型值的类类 Attribute.Class 字符串 Attribute.Constant 枚举 Attribute.Enum
还有一些我没有用到所以这里就没有列出来了
所以咱们拿到的值有的时候不能间接用,比方咱们当初要取得【起源类】的语法树
Attribute.Class attributeClass = …
Type.ClassType sourceClass = (Type.ClassType)attribute.getValue();
JCTree.JCClassDecl sourceClassDef = (JCTree.JCClassDecl) elementUtils.getTree(sourceClass.asElement());
复制代码
通过上述的形式咱们就能够拿到注解上的【起源类】的语法树
接着咱们就能够把【起源类】上的字段和办法复制到【指标类】了
for (JCTree sourceDef : sourceClassDef.defs) {
// 如果是非动态的字段
if (InheritUtils.isNonStaticVariable(sourceDef)) {JCTree.JCVariableDecl sourceVarDef = (JCTree.JCVariableDecl) sourceDef;
//Class 中未定义
if (!InheritUtils.isFieldDefined(targetClassDef, sourceVarDef)) {
// 增加字段
targetClassDef.defs = targetClassDef.defs.append(sourceVarDef);
}
}
// 办法相似,这里不具体展现了
}
复制代码
通过【起源类】语法树的 defs 属性就能取得所有的字段和办法,筛选出咱们须要的字段和办法之后再通过【指标类】语法树的 defs 属性的 append 办法增加就行了
这样咱们就把一个类的字段和办法复制到了另一个类上
最初一步,咱们须要在 resources/META-INF/services 下增加一个 javax.annotation.processing.Processor 的文件,并在文件中增加咱们实现类的全限定类名
这一步也能够应用上面的形式主动生成
compileOnly ‘com.google.auto.service:auto-service:1.0.1’
annotationProcessor ‘com.google.auto.service:auto-service:1.0.1’
复制代码
@AutoService(Processor.class)
public class InheritProcessor extends AbstractProcessor {
}
复制代码
引入 auto-service 包后,在咱们实现的 InheritProcessor 上标注 @AutoService(Processor.class)注解就会在编译的时候主动生成对应的文件
到此咱们的注解处理器就开发实现了
咱们只须要用 compileOnly 和 annotationProcessor 引入咱们的包就能够啦
Intellij Plugin
尽管咱们实现了注解处理器,然而 IDEA 上是不会有提醒的,这就须要另外开发 IDEA 的插件来实现对应的性能了
举荐一下大佬写的小册《IntelliJ IDE 插件开发指南》,可能比拟零碎的理解 IDEA 的插件开发
这是我的 推广链接,如果大家真的要买的,那就棘手点我的 推广链接 买吧,嘿嘿
所以我的项目搭建之类的我就不啰嗦了
IDEA 提供了很多接口用于扩大,这里咱们要用到的就是 PsiAugmentProvider 这个接口
public class InheritPsiAugmentProvider extends PsiAugmentProvider {
@Override
protected @NotNull <Psi extends PsiElement> List<Psi> getAugments(@NotNull PsiElement element, @NotNull Class<Psi> type) {return new ArrayList<>();
}
}
复制代码
getAugments 办法就是用于取得额定的元素
其中第一个参数 PsiElement element 就是扩大的主体,以咱们以后须要实现的性能来说,如果这个参数是类并且类上标注了咱们指定的注解,那么咱们就须要进行解决
第二个参数是须要的类型,以咱们以后须要实现的性能来说,如果这个类型是字段或办法,那么咱们就须要进行解决
间接看代码会清晰一点
public class InheritPsiAugmentProvider extends PsiAugmentProvider {
@Override
protected @NotNull <Psi extends PsiElement> List<Psi> getAugments(@NotNull PsiElement element, @NotNull Class<Psi> type) {
// 只解决类
if (element instanceof PsiClass) {if (type.isAssignableFrom(PsiField.class)) {// 如果标记了注解,则返回额定的字段}
if (type.isAssignableFrom(PsiMethod.class)) {// 如果标记了注解,则返回额定的办法}
}
return new ArrayList<>();}
}
复制代码
也就是说扩大的字段和办法是离开来获取的,另外须要留神是额定的字段和办法,不是全副的字段和办法
接下来咱们须要先取得类上的注解
private Collection<PsiAnnotation> findAnnotations(PsiClass targetClass) {
Collection<PsiAnnotation> annotations = new ArrayList<>();
for (PsiAnnotation annotation : targetClass.getAnnotations()) {if ("注解的全限定名".contains(annotation.getQualifiedName())) {annotations.add(annotation);
}
if ("@Repeatable 注解的全限定名".contains(annotation.getQualifiedName())) {handleRepeatableAnnotation(annotation, annotations);
}
}
return annotations;
}
/**
- 取得 Repeatable 中的理论注解
*/
private void handleRepeatableAnnotation(PsiAnnotation annotation, Collection<PsiAnnotation> annotations) {
PsiAnnotationMemberValue value = annotation.findAttributeValue("value");
if (value != null) {PsiElement[] children = value.getChildren();
for (PsiElement child : children) {if (child instanceof PsiAnnotation) {annotations.add((PsiAnnotation) child);
}
}
}
}
复制代码
取得注解之后,咱们就能够通过注解取得注解的属性了
Collection<PsiType> sources = findTypes(annotation.findAttributeValue(“sources”));
private Collection<PsiType> findTypes(PsiElement element) {
Collection<PsiType> types = new HashSet<>();
findTypes0(element, types);
return types;
}
private void findTypes0(PsiElement element, Collection<PsiType> types) {
if (element == null) {return;}
if (element instanceof PsiTypeElement) {PsiType type = ((PsiTypeElement) element).getType();
types.add(type);
}
for (PsiElement child : element.getChildren()) {findTypes0(child, types);
}
}
复制代码
这里须要留神,Psi 是文件树而不是语法树
比方这样的写法 @InheritClass(sources = {A.class, B.class})
咱们通过 findAttributeValue(“sources”)失去的是 {A.class, B.class},最上层是{},{} 的子节点才是 A.class, B.class,所以 Psi 体现的是文件的构造
接着咱们就能够取得对应的字段和办法了
PsiClass sourceClass = PsiUtil.resolveClassInType(PsiType);
/**
- 取得所有字段
*/
public static Collection<PsiField> collectClassFieldsIntern(@NotNull PsiClass psiClass) {
if (psiClass instanceof PsiExtensibleClass) {return new ArrayList<>(((PsiExtensibleClass) psiClass).getOwnFields());
} else {return filterPsiElements(psiClass, PsiField.class);
}
}
/**
- 取得所有办法
*/
public static Collection<PsiMethod> collectClassMethodsIntern(@NotNull PsiClass psiClass) {
if (psiClass instanceof PsiExtensibleClass) {return new ArrayList<>(((PsiExtensibleClass) psiClass).getOwnMethods());
} else {return filterPsiElements(psiClass, PsiMethod.class);
}
}
private static <T extends PsiElement> Collection<T> filterPsiElements(@NotNull PsiClass psiClass, @NotNull Class<T> desiredClass) {
return Arrays.stream(psiClass.getChildren()).filter(desiredClass::isInstance).map(desiredClass::cast).collect(Collectors.toList());
}
复制代码
下面这几个办法我都是从 Lombok 外面复制过去的,至于 else 分支我也看不懂,可能会有多种状况,我也没遇到过,hhh
而后咱们就能够对字段和办法进行复制啦
String fieldName = field.getName();
LightFieldBuilder fieldBuilder = new LightFieldBuilder(
manager,
fieldName,
field.getType());
// 拜访限定
fieldBuilder.setModifierList(new LightModifierList(field));
// 初始化
fieldBuilder.setInitializer(field.getInitializer());
// 所属的 Class
fieldBuilder.setContainingClass(targetClass);
// 是否 Deprecated
fieldBuilder.setIsDeprecated(field.isDeprecated());
// 正文
fieldBuilder.setDocComment(field.getDocComment());
// 导航
fieldBuilder.setNavigationElement(field);
复制代码
LightMethodBuilder methodBuilder = new LightMethodBuilder(
manager,
JavaLanguage.INSTANCE,
method.getName(),
method.getParameterList(),
method.getModifierList(),
method.getThrowsList(),
method.getTypeParameterList());
// 返回值
methodBuilder.setMethodReturnType(method.getReturnType());
// 所属的 Class
methodBuilder.setContainingClass(targetClass);
// 导航
methodBuilder.setNavigationElement(method);
复制代码
这里大家肯定要新实例化所有的字段和办法,不要间接返回【起源类】的字段和办法,因为【起源类】的字段和办法是和【起源类】关联的,而咱们返回的是【指标类】的字段和办法,两者不匹配会导致 IDEA 间接报错
最初咱们只须要在 plugin.xml 中增加这个扩大就行了
<extensions defaultExtensionNs=”com.intellij”>
<lang.psiAugmentProvider implementation="xxx.xxx.xxx.InheritPsiAugmentProvider"/>
</extensions>