Java SPI & 注解

发布于 2024-08-21 00:47:24 字数 8795 浏览 9 评论 0

SPI

JavaPoet

Java/Android 框架经常会使用编译期注解,其中的关键一步就是如何生成 Java 文件。JavaPoet 是用来生成 java 代码的一个 Java Library。

核心的几个类:

  • JavaFile 对应的 Java 文件,包名直接用一个字符串表示。
  • TypeSpec 表示一个 class 定义。
  • FieldSpec 表示类中的属性。
  • MethodSpec 表示类中的方法。
  • JavaPoet 专门定义了如下几种专门描述类型的类:
分类生成的类型JavaPoet 写法等效的 java 写法
内置类型intTypeName.INT 
数组类型int[]ArrayTypeName.of(int.class) 
需要引入包名的类型java.io.FileClassName.get("java.io", “File”) 
参数化类型ListParameterizedTypeName.get(List.class, String.class) 
类型变量,用于声明泛型TTypeVariableName.get("T") 
通配符类型? extends StringWildcardTypeName.subtypeOf(String.class) 

这些类型之间可以相互嵌套,比如 ParameterizedTypeName.get(List.class, String.class) ​ 其中 List.class 等价于 ClassName.get("java.util", "List") ​。 因此

ParameterizedTypeName.get(List.class, String.class)

可以写为

ParameterizedTypeName.get(ClassName.get("java.util", "List"), ClassName.get("java.lang", "String"))。

注:前者的好处是简洁,后者的好处是"使用 ClassName 代表某个类型而无需引入该类型"。比如: 由于在 java 工程中是没有 android 的 sdk, 所以你在 java 工程中想生成 android.app.Activity 这种类型是不能直接 Activity.class。这种情况下只能通过 ClassName 进行引用。”

Statement 中的占位符

  • $T 是类型替换, 一般用于 ("$T foo", List.class) => List foo ​,$T 的好处在于 JavaPoet 会自动帮你补全文件开头的 import. 如果直接写 ("List foo") 虽然也能生成 List foo, 但是最终的 java 文件就不会自动帮你添加 import java.util.List. $L 是字面量替换, 比如 ("abc$L123", "FOO") => abcFOO123 ​. 也就是直接替换.
  • $S 是字符串替换, 比如: ("$S.length()", "foo") => "foo".length() ​ 注意 $S 是将参数替换为了一个带双引号的字符串. 免去了手写 "\"foo\".length()" ​ 中转义 (\") 的麻烦.
  • $N 是名称替换, 比如你之前定义了一个函数 MethodSpec methodSpec = MethodSpec.methodBuilder("foo").build(); ​ 现在你可以通过
  • $N 获取这个函数的名称 ("$N", methodSpec) => foo ​.
MethodSpec main = MethodSpec.methodBuilder("main")
    .addModifiers(Modifier.PUBLIC, Modifier.STATIC)
    .returns(void.class)
    .addParameter(String[].class, "args")
    .addStatement("$T.out.println($S)", System.class, "Hello, JavaPoet!")
    .build();

TypeSpec helloWorld = TypeSpec.classBuilder("HelloWorld")
    .addModifiers(Modifier.PUBLIC, Modifier.FINAL)
    .addMethod(main)
    .build();

JavaFile javaFile = JavaFile.builder("com.example.helloworld", helloWorld)
    .build();

javaFile.writeTo(System.out);

采用 javapoet 执行上述代码可以生成如下的代码:

package com.example.helloworld;

public final class HelloWorld {
  public static void main(String[] args) {
    System.out.println("Hello, JavaPoet!");
  }
}

代码逻辑转换

void main() {
  int total = 0;
  for (int i = 0; i < 10; i++) {
    total += i;
  }
}
MethodSpec main = MethodSpec.methodBuilder("main")
    .addStatement("int total = 0")
    .beginControlFlow("for (int i = 0; i < 10; i++)")
    .addStatement("total += i")
    .endControlFlow()
    .build();

自定义注解

编译时注解

首先创建一个 java module(命名 annotation,即注解类放到该 module 下)

以 MyAnnotation 为例:

@Retention(RetentionPolicy.CLASS)
@Target(ElementType.TYPE)
public @interface MyAnnotation {
    String value() default "MyAnnotation";
}

然后创建 processor 类所在的 module,也是一个 java module(命名 compiler)

这里以 MyProcessor 为例:

@AutoService(Processor.class)
public class MyProcessor extends AbstractProcessor {

    private Elements elements;
    private Filer filer;
    private Messager messager;

    @Override
    public synchronized void init(ProcessingEnvironment processingEnvironment) {
        super.init(processingEnvironment);

        elements = processingEnvironment.getElementUtils();
        filer = processingEnvironment.getFiler();
        messager = processingEnvironment.getMessager();
    }

    /**
     * 添加支持的注解类型
     * @return
     */
    @Override
    public Set<String> getSupportedAnnotationTypes() {
        Set<String> supportType = new LinkedHashSet<>();
        supportType.add(MyAnnotation.class.getCanonicalName());
        return supportType;
    }

    @Override
    public SourceVersion getSupportedSourceVersion() {
        return SourceVersion.latestSupported();
    }

    @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnvironment) {
        // 获取 MyAnnotation 注解类的集合
        Set<? extends Element> set = roundEnvironment.getElementsAnnotatedWith(MyAnnotation.class);
        for (Element element: set) {
            // 进行注解类的匹配判断,看是否符合要求
            if (element.getKind() == ElementKind.CLASS) {
                // TODO 定义我们希望生成的文件样式
                TypeElement ele = (TypeElement) element;
                String qualifiedName = ele.getQualifiedName().toString();
                messager.printMessage(Diagnostic.Kind.NOTE, "qualifiedName: " + qualifiedName);
                brewJavaFile(ele);
            }else {
                // 注解的非类  则报错
                messager.printMessage(Diagnostic.Kind.ERROR, String.format("Only classes can be annotated with @%s", MyAnnotation.class.getSimpleName()), element);
                return true;
            }
        }
        return false;
    }

    private void brewJavaFile(TypeElement element) {
        //
        MyAnnotation myAnnotation = element.getAnnotation(MyAnnotation.class);
        MethodSpec methodSpec = MethodSpec.methodBuilder("sayHello").addModifiers(Modifier.PUBLIC, Modifier.STATIC)
                .returns(void.class).addStatement("$T.out.println($S)", System.class, "Hello" + myAnnotation.value()).build();

        TypeSpec typeSpec = TypeSpec.classBuilder(element.getSimpleName().toString() + "$$HelloWorld")
                .addModifiers(Modifier.PUBLIC, Modifier.FINAL)
                .addMethod(methodSpec)
                .build();

        // 获取包路径
        JavaFile javaFile = JavaFile.builder(elements.getPackageOf(element).getQualifiedName().toString(), typeSpec).build();
        try {
            javaFile.writeTo(filer);
        }catch (IOException e) {
            e.printStackTrace();
        }
    }
}

添加 MyAnnotation 为支持的注解类型,同时对 MyProcessor 类进行 @AutoService(Processor.class) 注解

这里的 build 文件的配置要注意(针对 gradle5.x 版本可能需要按照如下方式依赖 auto-service)

apply plugin: 'java-library'

dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])
    // 依赖注解库
    implementation project(':annotation')
    // 依赖 javaPoet 框架
    implementation 'com.squareup:javapoet:1.9.0'
    // 依赖 AutoService
    implementation 'com.google.auto.service:auto-service:1.0-rc4'
    annotationProcessor 'com.google.auto.service:auto-service:1.0-rc4'
}

sourceCompatibility = "1.8"
targetCompatibility = "1.8"

接下来编译之后会在 build 目录下生成一个 META-INF 的目录,如上图所示。 javax.annotation.processing.Processor 这个文件里会包含该 module 下定义的所有的 processor 类,这里 就包括 BaseProcessor 和 MyProcessor

最后我们创建一个 android 工程,在 MainActivity 上添加注解 @MyAnnotation;编译运行之后会在如下图所示的目录下,按照 MyProcessor 生成文件的规则创建对应的类(实例)

public final class MainActivity$$HelloWorld {
  public static void sayHello() {
    System.out.println("HelloMyAnnotation");
  }
}

接下来就可以在 MainActivity 中通过反射的方式直接获取该生成类的实例,调用对应的方法

  String name = this.getClass().getCanonicalName();
        try {
            Class clazz = Class.forName(name + "$$HelloWorld");
            Method method = clazz.getMethod("sayHello");
            method.invoke(null);
        }catch (Exception e) {
            Log.e("MainActivity", "exception: " + e.getMessage());
        }

运行之后控制台输出:

至此便完成了借助 javapoet 框架&AutoService,通过自定义编译时注解的方式创建实例,调用相关方法的操作。

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据

关于作者

0 文章
0 评论
497 人气
更多

推荐作者

我们的影子

文章 0 评论 0

素年丶

文章 0 评论 0

南笙

文章 0 评论 0

18215568913

文章 0 评论 0

qq_xk7Ean

文章 0 评论 0

    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文