能够看到当咱们在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 {
@Overridepublic boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) { //自定义的解决流程}@Overridepublic Set<String> getSupportedAnnotationTypes() { //须要扫描的注解的全限定名 return new HashSet<>();}
}
复制代码
首先咱们要继承javax.annotation.processing.AbstractProcessor这个类
其中getSupportedAnnotationTypes办法中返回须要扫描的注解的全限定名
而后就能够在process办法中增加本人的逻辑了,第一个参数Set<? extends TypeElement> annotations就是扫描到的注解
咱们先拿到这些注解标注的类
public class InheritProcessor extends AbstractProcessor {
@Overridepublic 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 {
@Overrideprotected @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 {
@Overrideprotected @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>