我正在参与「·启航方案」
从今以后,只需谁说Java
不能多承继
我都会说,是的没错(秒怂)
要不你再看看标题写了啥?
没毛病啊,你说Java
不能多承继,我也说Java
不能多承继
这不是巧了么,没想到咱们对一件事物的看法竟如此一致,看来这便是猿粪啊
此承继非彼承继
那你这又是唱哪出?
直接上图!
能够看到当咱们在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>
最终的最终,需求发布一下插件或是本地集成
完毕
附一张插件图
假如咱们有兴趣能够看看这个库,还有许多其他的功用