美文网首页
基于JavaPoet自动生成java代码文件

基于JavaPoet自动生成java代码文件

作者: 生饼 | 来源:发表于2021-11-17 11:25 被阅读0次

    0 前言

    要实现一个功能,我们通常编写一系列的java文件,如果需求发生变化,则修改这些java文件或增加一些新的java文件。为了避免为适应千变万化的需求而频繁修改项目代码,可以在运行时动态生成字节码,当然运行时生成字节码需要占用计算资源。当然,还有一种思路是根据条件动态生成java文件,而不是根据每种情况编写固定的代码,这样生成的项目与完全手工编写的代码没有任何区别。JavaPoet就是一个动态生成java文件的库,在caffine、butterknife、自动生成rpc stub文件等中间件中得到了应用。

    1 基本功能介绍

    我们知道,一个java类由类声明、字段、构造方法、方法、参数、注解等元素组成,JavaPoet为这些基本组成元素分别定义了相应的类,分别用来管理、生成相应元素相关的代码。

    JavaPoet常用的一些类:

    class 说明
    JavaFile 对应编写的.java文件
    TypeSpec 对应一个类、接口或enum
    MethodSpec 对应一个方法或构造方法
    FieldSpec 对应一个字段
    ParameterSpec 对应方法或构造方法的一个参数
    AnnotationSpec 对应类型、字段、方法或构造方法上的注解
    ClassName 对应一个类、接口或enum的名字,由package名字和类名字两部分组成
    CodeBlock 代码块,一般用来生成{}包裹起来的数据块
    ParameterizedTypeName 泛型中的参数化类型
    TypeVariableName 泛型中的类型变量
    WildcardTypeName 泛型中的通配符?

    1.1 一个简单的例子

    public class JavapoetApplication {
    
        public static void main(String[] args) {
            try {
                // 定义一个方法名为test的方法
                MethodSpec test = MethodSpec.methodBuilder("test")
                        // 方法的修饰符
                        .addModifiers(Modifier.PUBLIC, Modifier.STATIC)
                        // 方法的返回值类型
                        .returns(void.class)
                        // 方法的参数
                        .addParameter(Integer.class, "loop")
                        // 方法body内容
                        .addCode(""
                                + "int total = 0;\n"
                                + "for (int i = 0; i < loop; i++) {\n"
                                + "  total += i;\n"
                                + "}\n"
                                + "System.out.println(\"total value: \" + total);\n")
                        .build();
    
                // 定义一个类,名字为TestCode
                TypeSpec testCode = TypeSpec.classBuilder("TestCode")
                        // 类修饰符
                        .addModifiers(Modifier.PUBLIC, Modifier.FINAL)
                        // 添加方法
                        .addMethod(test)
                        .build();
    
                // 定义一个java文件,指定package和类定义
                JavaFile javaFile = JavaFile.builder("com.javatest.javapoet", testCode)
                        .build();
    
                // 将java文件内容写入文件中
                File file = new File("./javapoet");
                javaFile.writeTo(file);
            } catch (Exception e) {
                //
            }
        }
    
        public static void test1() throws Exception {
    
        }
    
    }
    

    生成的java文件内容为:

    package com.javatest.javapoet;
    
    import java.lang.Integer;
    
    public final class TestCode {
      public static void test(Integer loop) {
        int total = 0;
        for (int i = 0; i < loop; i++) {
          total += i;
        }
        System.out.println("total value: " + total);
      }
    }
    

    1.2 改进例子

    上面的自动生成代码中,虽然类和方法声明、修饰符、返回值类型、包名等是用编程实现的(编程实现就意味这可以参数化,通过控制参数生成不同的内容),但是方法体的内容与手工编写并没有什么区别,像语句分号、缩进、换行等都是人工编写的,看不出自动生成代码的优势。但JavaPoet提供了addStatement()beginControlFlow()endControlFlow()nextControlFlow()等方法方便代码生成。addStatement()会自动在语句后添加分号,并换行;beginControlFlow()nextControlFlow()会自动添加{符号并换行,控制后面语句的缩进;endControlFlow()会自动添加}符号,并换行,控制后面语句的缩进。

    重新编写上面test方法的生成代码,生成的结果代码是一样的:

        MethodSpec test = MethodSpec.methodBuilder("test")
                // 方法的修饰符
                .addModifiers(Modifier.PUBLIC, Modifier.STATIC)
                // 方法的返回值类型
                .returns(void.class)
                // 方法的参数
                .addParameter(Integer.class, "loop")
                // 方法body内容
                .addStatement("int total = 0")
                .beginControlFlow("for (int i = 0; i < loop; i++)")
                .addStatement("total += i")
                .endControlFlow()
                .addStatement("System.out.println(\"total value: \" + total)")
                .build();
    

    1.3 进一步改进例子

    现在生成代码的格式控制交给javaposet管理了,但是方法体的代码全是硬编码的,还没体现自动生成代码的灵活性,JavaPoet提供了L、S、T、N等替换符号来实现这方面的需求。

    进一步修改上面test方法的生成代码,如下:

        // 定义一个方法名为test的方法
        // 定义一个参数
        ParameterSpec loopParam = ParameterSpec.builder(Integer.class, "loop")
                .addModifiers(Modifier.FINAL)
                .build();
    
        String total = "total";
    
        MethodSpec test = MethodSpec.methodBuilder("test")
                // 方法的修饰符
                .addModifiers(Modifier.PUBLIC, Modifier.STATIC)
                // 方法的返回值类型
                .returns(void.class)
                // 方法的参数
                .addParameter(loopParam)
                // 方法body内容
                // $L 会替换为变量total的值
                .addStatement("int $L = 0", total)
                // $N 会替换为 loopParam的名字
                .beginControlFlow("for (int i = 0; i < $N; i++)", loopParam)
                .addStatement("$L += i", total)
                .endControlFlow()
                // $T 会替换为类的名字,如果需要,会在文件头添加相应的import语句
                // $S 会替换为变量的值,并用""包裹起来
                .addStatement("$T.out.println($S + $L)", System.class, "total value: ", total)
                .build();
    

    1.4 替换符号功能说明

    JavaPoet几个常用替换符号的功能介绍如下:

    替换符号 说明
    $T 参数是Class对象,替换为Class的名字,如果需要,同时在文件头添加相应的import语句
    $L 替换为变量值的字面量值,功能相当于字符串的format()方法
    $S 也是替换为变量值的字面量值,但是字面量值为被字符串双引号包裹起来
    $N 参数是ParameterSpec、TypeSpec、MethodSpec等,替换为这些变量的name值

    替换符号还可以指定替换参数的位置(Relative Arguments)或参数的名字(Named Arguments)
    例如:

    addStatement("System.out.println(\"I ate $L $L\")", 3, "tacos")
    

    # 指定参数位置
    addStatement("System.out.println(\"I ate $2L $1L\")", "tacos", 3)
    

    以及

    # 指定参数名字
    Map<String, Object> map = new LinkedHashMap<>();
    map.put("food", "tacos");
    map.put("count", 3);
    
    addStatement("System.out.println(\"I ate $count:L $food:L\")", map)
    

    生成的语句都是

    System.out.println("I ate 3 tacos");
    

    2 JavaPoet个组件使用说明

    2.1 类型

    变量、参数、返回值的类型即可以用java的class来表达,javapoet也定义了相应的类型表示系统,对应关系如下:

    类别 生成的类型举例 javapoet表达方式 java class表达方式
    基本类型 int TypeName.INT int.class
    基本类型包装类型 TypeName.BOXED_INT Integer.class
    数组 int[] ArrayTypeName.of(int.class) int[].class
    自定义类型 TestCode.class
    参数化类型 List<String> ParameterizedTypeName.get(List.class, String.class)
    类型变量 T TypeVariableName.get("T")
    通配符类型 ? extends String WildcardTypeName.subtypeOf(String.class)

    2.2 field

    字段的生成比较简单,只要指定字段的修饰符、类型、名字创建FieldSpec,然后添加到TypeSpec中就可以了

    // 显式创建FieldSpec
    
    
    FieldSpec android = FieldSpec.builder(String.class, "description")
        .addModifiers(Modifier.PRIVATE, Modifier.FINAL)
        // 指定初始化值,可选
        .initializer("$S", "this is a example")
        .build();
    
    ParameterizedTypeName type = ParameterizedTypeName.get(List.class, String.class);
    FieldSpec android = FieldSpec.builder(type, "name").build();
    

    分别生成:

    private final String description = "this is a example";
    List<String> name;
    

    2.3 class

    TypeSpec.classBuilder("Clazz")
        // 抽象类
        .addModifiers(Modifier.PUBLIC, Modifier.ABSTRACT)
        // 泛型
        .addTypeVariable(TypeVariableName.get("T"))
        // 继承与接口
        .superclass(String.class)
        .addSuperinterface(Serializable.class)
        .addSuperinterface(ParameterizedTypeName.get(Comparable.class, String.class))
        .addSuperinterface(ParameterizedTypeName.get(ClassName.get(Map.class), 
                                                     TypeVariableName.get("T"), 
                                                     WildcardTypeName.subtypeOf(String.class)))
        // 初始化块
        .addStaticBlock(CodeBlock.builder().build())
        .addInitializerBlock(CodeBlock.builder().build())
    
        // 添加字段
        // .addField(fieldSpec)
    
        // 构造方法和方法
        // .addMethod(constructorSpec)
        // .addMethod(methodSpec)
    
        // 内部类
        .addType(TypeSpec.classBuilder("InnerClass").build())
    
        .build();
    
    

    2.2 interface

    通过TypeSpec的interfaceBuilder()方法创建interface,其他元素添加跟class差不多

    TypeSpec helloWorld = TypeSpec.interfaceBuilder("TestInterface").build();
    

    2.3 enum

    通过TypeSpec的enumBuilder()方法创建interface, 通过addEnumConstant()添加enum成员
    一个基本例子:

    TypeSpec helloWorld = TypeSpec.enumBuilder("TestEnum")
        .addModifiers(Modifier.PUBLIC)
        .addEnumConstant("EXAM_0")
        .addEnumConstant("EXAM_1")
        .addEnumConstant("EXAM_2")
        .build();
    

    生成的代码如下:

    public enum TestEnum {
      EXAM_0,
      EXAM_1,
      EXAM_2
    }
    

    一个复杂一点的例子:

    TypeSpec testEnum = TypeSpec.enumBuilder("TestEnum")
        .addModifiers(Modifier.PUBLIC)
        .addEnumConstant("EXAM_0", TypeSpec.anonymousClassBuilder("$S", "exam0")
            .addMethod(MethodSpec.methodBuilder("toString")
                .addAnnotation(Override.class)
                .addModifiers(Modifier.PUBLIC)
                .addStatement("return $S", "exam0")
                .returns(String.class)
                .build())
            .build())
        .addEnumConstant("EXAM_1", TypeSpec.anonymousClassBuilder("$S", "exam1")
            .build())
        .addEnumConstant("EXAM_2", TypeSpec.anonymousClassBuilder("$S", "exam2")
            .build())
        .addField(String.class, "name", Modifier.PRIVATE, Modifier.FINAL)
        .addMethod(MethodSpec.constructorBuilder()
            .addParameter(String.class, "name")
            .addStatement("this.$N = $N", "name", "name")
            .build())
        .build();
    

    生成如下代码:

    public enum TestEnum {
      EXAM_0("exam0") {
        @Override
        public String toString() {
          return "exam0";
        }
      },
    
      EXAM_1("exam1"),
    
      EXAM_2("exam2");
    
      private final String name;
    
      Roshambo(String name) {
        this.name = name;
      }
    }
    

    2.4 匿名内部类

    使用TypeSpec的anonymousClassBuilder()方法生成匿名内部类,举例如下:

    TypeSpec comparator = TypeSpec.anonymousClassBuilder("")
        .addSuperinterface(ParameterizedTypeName.get(Comparator.class, String.class))
        .addMethod(MethodSpec.methodBuilder("compare")
            .addAnnotation(Override.class)
            .addModifiers(Modifier.PUBLIC)
            .addParameter(String.class, "a")
            .addParameter(String.class, "b")
            .returns(int.class)
            .addStatement("return $N.length() - $N.length()", "a", "b")
            .build())
        .build();
    
    TypeSpec testCode = TypeSpec.classBuilder("TestCode")
        .addMethod(MethodSpec.methodBuilder("sortByLength")
            .addParameter(ParameterizedTypeName.get(List.class, String.class), "strings")
            .addStatement("$T.sort($N, $L)", Collections.class, "strings", comparator)
            .build())
        .build();
    

    生成代码如下:

    void sortByLength(List<String> strings) {
      Collections.sort(strings, new Comparator<String>() {
        @Override
        public int compare(String a, String b) {
          return a.length() - b.length();
        }
      });
    }
    

    2.5 annotation

    给方法增加基本注解:

    MethodSpec toString = MethodSpec.methodBuilder("toString")
        .addAnnotation(Override.class)
        .returns(String.class)
        .addModifiers(Modifier.PUBLIC)
        .addStatement("return $S", "testCode")
        .build();
    

    给方法增加带参数值的注解:

    MethodSpec logRecord = MethodSpec.methodBuilder("recordEvent")
        .addModifiers(Modifier.PUBLIC, Modifier.ABSTRACT)
        .addAnnotation(AnnotationSpec.builder(Headers.class)
            .addMember("accept", "$S", "application/json; charset=utf-8")
            .addMember("userAgent", "$S", "Square Cash")
            .build())
        .addParameter(LogRecord.class, "logRecord")
        .returns(LogReceipt.class)
        .build();
    

    2.6 javadoc

    类和方法都可以通过TypeSpec和MethodSpec的addJavadoc()方法添加javadoc

    MethodSpec dismiss = MethodSpec.methodBuilder("dismiss")
        .addJavadoc("Hides {@code message} from the caller's history. Other\n"
            + "participants in the conversation will continue to see the\n"
            + "message in their own history unless they also delete it.\n")
        .addJavadoc("\n")
        .addJavadoc("<p>Use {@link #delete($T)} to delete the entire\n"
            + "conversation for all participants.\n", Conversation.class)
        .addModifiers(Modifier.PUBLIC, Modifier.ABSTRACT)
        .addParameter(Message.class, "message")
        .build();
    

    生成的结果为:

    /**
       * Hides {@code message} from the caller's history. Other
       * participants in the conversation will continue to see the
       * message in their own history unless they also delete it.
       *
       * <p>Use {@link #delete(Conversation)} to delete the entire
       * conversation for all participants.
       */
      void dismiss(Message message);
    

    2.7 import 静态字段或方法

    ClassName hoverboard = ClassName.get("com.mattel", "Hoverboard");
    ClassName namedBoards = ClassName.get("com.mattel", "Hoverboard", "Boards");
    
    JavaFile.builder("com.example.helloworld", hello)
        .addStaticImport(hoverboard, "createNimbus")
        .addStaticImport(namedBoards, "*")
        .addStaticImport(Collections.class, "*")
        .build();
    

    生成代码如下:

    package com.example.helloworld;
    
    import static com.mattel.Hoverboard.Boards.*;
    import static com.mattel.Hoverboard.createNimbus;
    import static java.util.Collections.*;
    
    class HelloWorld {
    
    }
    

    相关文章

      网友评论

          本文标题:基于JavaPoet自动生成java代码文件

          本文链接:https://www.haomeiwen.com/subject/zunqtrtx.html