欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

编译时动态生成代码技术之javapoet(四)

程序员文章站 2022-05-29 21:45:48
...

Javapoet简介

javapoet是android之神JakeWharton开源的一款快速代码生成工具,配合APT在项目编译期间动态生成代码,并且使用其API可以自动生成导包语句。这可以减少我们在项目开发中模板化代码的编写,减轻程序员开发所需要的时间,提高编码效率,这也是好的架构努力方向。
javapoet github链接:https://github.com/square/javapoet

核心类

JavaPoet定义了一系列类来尽可能优雅的描述java源文件的结构。观察JavaPoet的代码主要的类可以分为以下几种:

  • Spec 用来描述Java中基本的元素,包括类型,注解,字段,方法和参数等。
    1. AnnotationSpec
    2. FieldSpec
    3. MethodSpec
    4. ParameterSpec
    5. TypeSpec
  • Name 用来描述类型的引用,包括Void,原始类型(int,long等)和Java类等。
    1. TypeName
    2. ArrayTypeName
    3. ClassName
    4. ParameterizedTypeName
    5. TypeVariableName
    6. WildcardTypeName
  • CodeBlock 用来描述代码块的内容,包括普通的赋值,if判断,循环判断等。
  • JavaFile 完整的Java文件,JavaPoet的主要的入口。
  • CodeWriter 读取JavaFile并转换成可阅读可编译的Java源文件。

MethodSpec介绍

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介绍

TypeSpec helloWorld = TypeSpec.classBuilder("HelloWorld")//构造一个类,类名
    .addModifiers(Modifier.PUBLIC, Modifier.FINAL)//定义类的修饰符
    .addMethod(main)//添加类的方法,也就是上面生成的MethodSpec对象
    .build();

JavaFile介绍

JavaFile javaFile = JavaFile.builder("baijunyu.com.testelement", helloWorld)//定义生成的包名,和类
    .build();
javaFile.writeTo(System.out);//输出路径,可以收一个file地址

使用javapoet首先添加gradle依赖:

  compile 'com.squareup:javapoet:1.11.1'

整理一下代码,如下

private void creatCode(){
        MethodSpec main = MethodSpec.methodBuilder("mian")
                .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)
                .addMethod(main)
                .build();
        JavaFile javaFile = JavaFile.builder("baijunyu.com.testelement", helloWorld).build();
        try {
            javaFile.writeTo(System.out);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

生成的代码,在控制台打印如下
编译时动态生成代码技术之javapoet(四)在上面的代码中我们用MethodSpec定义了一个main函数,然后用addModifiers给代码添加了public和static修饰符,returns添加返回值,addParameter添加参数,addStatement给方法添加代码块。定义好方法之后我们就需要将方法加入类中,利用TypeSpec可以构建相应的类信息,然后将main方法通过addMethod()添加进去,如此就将类也构建好了。而JavaFile包含一个*的Java类文件,需要将刚刚TypeSpec生成的对象添加进去,通过这个JavaFile我们可以决定将这个java类以文本的形式输出或者直接输出到控制台。

  • MethodSpec addCode和addStatement

addCode生成代码

        MethodSpec main = MethodSpec.methodBuilder("main")
                .addCode("" 
                        + "int total = 0;\n" 
                        + "for (int i = 0; i < 10; i++) {\n" 
                        + "  total += i;\n" 
                        + "}\n")
                .build();

结果
编译时动态生成代码技术之javapoet(四)一般的类名和方法名是可以被模仿书写的,但是方法中的构造语句是不确定的,这时候就可以用addCode来添加代码块来实现此功能。但是addCode也有其缺点,我们将第一个helloworld用addCode和addStatement生成的代码对比看下

//addCode生成的
package com.example.helloworld;

import java.lang.String;

public final class HelloWorld {
  public static void main(String[] args) {
    System.out.println('Hello World');
  }
}
//addStatement生成的
package com.example.helloworld;

import java.lang.String;
import java.lang.System;

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

可以看出用addStatement生成的多了一行导包语句,也就是说用addStatement生成的代码可以自动同时生成导包代码语句,同时使用addStatement可以减少手工的分号,换行符,括号书写,直接使用Javapoet的api时,代码的生成简单。

  • beginControlFlow() + endControlFlow(),替代for循环
 MethodSpec main = MethodSpec.methodBuilder("main")
                .addStatement("int total = 0")
                .beginControlFlow("for (int i = 0; i < 10; i++)")
                .addStatement("total += i")
                .endControlFlow()
                .build();

也可以使用拼接的方式,动态的传递循环次数控制:

.beginControlFlow("for (int i = " + from + "; i < " + to + "; i++)")

与代码拼接的方式生成的结果是一样的,既然我们使用了javapoet,就可以使用其API来替换这种字符拼接模式,从而维护代码的可阅读性。

javapoet占位符

占位符使一个字符串的拼接形式转化为String.format的格式,提高代码的可读性。
javapoet中几个常用的占位符:

1). $L 文本值

对于字符串的拼接,在使用时是很分散的,太多的拼接的操作符,不太容易观看。为了去解决这个问题,Javapoet提供一个语法,它接收$ L去输出一个文本值,就像 String.format()。
这个$L可以是字符串,基本类型。

使用字符串拼接的改为$L:

  MethodSpec main = MethodSpec.methodBuilder("main")
                .returns(int.class) .addStatement("int result = 0")
                .beginControlFlow("for (int i = $L; i < $L; i++)", 0, 10)
                .addStatement("result = result $L i", '*')
                .endControlFlow()
                .addStatement("return result")
                .build();

可以看出简化了不少,可以将代码中改变的部分0,10,* 等通过参数传入,而对于不变的直接使用javapoet生成。

2). $S 字符串

当我们想输出字符串文本时,我们可以使用 $S去输出一个字符串,而如果我们我们使用 $L 并不会帮我们加上双引号。

使用 $S:

 public static void test() throws IOException {
        TypeSpec helloWorld = TypeSpec.classBuilder("HelloWorld")
                .addModifiers(Modifier.PUBLIC, Modifier.FINAL)
                .addMethod(whatsMyName("slimShady"))
                .addMethod(whatsMyName("eminem"))
                .addMethod(whatsMyName("marshallMathers"))
                .build();
        JavaFile javaFile = JavaFile.builder("com.example.helloworld", helloWorld).build();
        javaFile.writeTo(System.out);
    }

    private static MethodSpec whatsMyName(String name) {
        return MethodSpec.methodBuilder(name).returns(String.class).addStatement("return $S", name).build();
    }

代码输出:
编译时动态生成代码技术之javapoet(四)使用 $L代码输出结果:
编译时动态生成代码技术之javapoet(四)3). $T 对象

对于我们Java开发人来说,JDK和SDK提供的各种java和android的API极大程度的帮助我们开发应用程序。对于Javapoet它也充分支持各种Java类型,包括自动生成导包语句,仅仅使用 $T 就可以了。

    MethodSpec main = MethodSpec.methodBuilder("today")
                .returns(Date.class)
                .addStatement("return new $T()", Date.class)
                .build();

代码输出结果:
编译时动态生成代码技术之javapoet(四)以上是生成JDK和SDK导包语句,通过传入 Date.class来生成代码,如果想导入自定义的包和类怎么办?
我们可以直接通过反射获取一个ClassName对象,作为类传入

        ClassName hoverboard = ClassName.get("com.mattel", "Hoverboard");
        MethodSpec main = MethodSpec.methodBuilder("tomorrow")
                .returns(hoverboard)
                .addStatement("return new $T()", hoverboard)
                .build();

代码输出结果:
编译时动态生成代码技术之javapoet(四)Javpoet也可以支持静态导入,它通过显示地收集类型成员名来实现

 JavaFile javaFile = JavaFile.builder("baijunyu.com.testelement", helloWorld)
                .addStaticImport(hoverboard, "getAge")
                .addStaticImport(hoverboard, "getName")
                .addStaticImport(Collections.class, "*")
                .build();

代码输出结果:
编译时动态生成代码技术之javapoet(四)静态导入,我们只需要在JavaFile的builder中链式调用addStaticImport方法就可以,第一个参数为Classname对象,第二个为需要导入的对象中的静态方法。

import static 是静态导入,是jdk1.5的新特征.利用import static 可以不通过调用包名,直接使用包里的静态方法。

4).$N 名字

有时候生成的代码是我们自己需要引用的,这时候可以使用 $N来调用根据生成的方法名。

MethodSpec hexDigit = MethodSpec.methodBuilder("hexDigit")
                .addParameter(int.class, "i")
                .returns(char.class)
                .addStatement("return (char) (i < 10 ? i + '0' : i - 10 + 'a')")
                .build();
        MethodSpec byteToHex = MethodSpec.methodBuilder("byteToHex")
                .addParameter(int.class, "b")
                .returns(String.class)
                .addStatement("char[] result = new char[2]")
                .addStatement("result[0] = $N((b >>> 4) & 0xf)", hexDigit)
                .addStatement("result[1] = $N(b & 0xf)", hexDigit)
                .addStatement("return new String(result)").build();
        TypeSpec helloWorld = TypeSpec.classBuilder("HelloWorld")
                .addModifiers(Modifier.PUBLIC, Modifier.FINAL)
                .addMethod(hexDigit)
                .addMethod(byteToHex)
                .build();
        JavaFile javaFile = JavaFile.builder("com.example.helloworld", helloWorld).build();
        try {
            javaFile.writeTo(System.out);
        } catch (IOException e) {
            e.printStackTrace();
        }

代码输出结果:
编译时动态生成代码技术之javapoet(四)在上面例子中,byteToHex想要调用 hexDigit方法,我们就可以使用$ N来调用,hexDigit()方法作为参数传递给byteToHex()方法通过使用$N来达到方法自引用。

代码块格式字符串

CodeBlock的用法,代码块

 AnnotationSpec.Builder builder = AnnotationSpec.builder(ClassName.get("baijunyu.com.test.poet", "MyAnnotation"));
        CodeBlock.Builder codeBlockBuilder = CodeBlock.builder().add("$S", "world");
        builder.addMember("hello", codeBlockBuilder.build());
        AnnotationSpec annotationSpec = builder.build();

        MethodSpec.Builder methoBuilder = MethodSpec.methodBuilder("toString");
        methoBuilder.addModifiers(Modifier.PUBLIC);
        methoBuilder.returns(TypeName.get(String.class));

        CodeBlock.Builder toStringCodeBuilder = CodeBlock.builder();
        toStringCodeBuilder.beginControlFlow("if( hello != null )");
        toStringCodeBuilder.add(CodeBlock.of("return \"hello \"+hello;\n"));
        toStringCodeBuilder.nextControlFlow("else");
        toStringCodeBuilder.add(CodeBlock.of("return \"\";\n"));
        toStringCodeBuilder.endControlFlow();
        
        methoBuilder.addCode(toStringCodeBuilder.build());
        MethodSpec main = methoBuilder.build();


        TypeSpec helloWorld = TypeSpec.classBuilder("HelloWorld")
                .addModifiers(Modifier.PUBLIC)
                .addMethod(main)
                .addAnnotation(annotationSpec)
                .build();
        JavaFile javaFile = JavaFile.builder("baijunyu.com.testelement", helloWorld)
                .build();
        try {
            javaFile.writeTo(System.out);
        } catch (IOException e) {
            e.printStackTrace();
        }

代码输出结果:
编译时动态生成代码技术之javapoet(四)相关参数格式化

CodeBlock codeBlock = CodeBlock.builder().add("I ate $L $L", 3, "ta").build();

通过位置参数指定要用的参数

CodeBlock.builder().add("I ate $2L $1L", "tacos", 3)

名字参数

通过$argumentName:X这样的语法形式来达到通过key名字寻找值,然后使用的功能,参数名可以使用 a-z, A-Z, 0-9, and _ ,但是必须使用小写字母开头。

  Map<String, Object> map = new LinkedHashMap<>();
        //map的key必须小写字母开头 
        map.put("food", "tacos");
        map.put("count", 3); 
        CodeBlock.builder().addNamed("I ate $count:L $food:L", map);

构造方法

MethodSpec 也可以生成构造方法

    MethodSpec flux = MethodSpec.constructorBuilder()
                .addModifiers(Modifier.PUBLIC)
                .addParameter(String.class, "greeting")
                .addStatement("this.$N = $N", "greeting", "greeting")
                .build();
        TypeSpec helloWorld = TypeSpec.classBuilder("HelloWorld")
                .addModifiers(Modifier.PUBLIC)
                .addField(String.class, "greeting", Modifier.PRIVATE, Modifier.FINAL)
                .addMethod(flux)
                .build();
        JavaFile javaFile = JavaFile.builder("com.example.helloworld", helloWorld).build();
        try {
            javaFile.writeTo(System.out);
        } catch (IOException e) {
            e.printStackTrace();
        }

代码输出结果:
编译时动态生成代码技术之javapoet(四)

方法参数

经常我们需要给方法加上入口参数,此时我们就可以通过ParameterSpec来达到这一目的

        ParameterSpec android = ParameterSpec.builder(String.class, "android")
                .addModifiers(Modifier.FINAL)
                .build();
        MethodSpec welcomeOverlords = MethodSpec.methodBuilder("welcomeOverlords")
                .addParameter(android)
                .addParameter(String.class, "robot", Modifier.FINAL)
                .build();

代码输出结果:
编译时动态生成代码技术之javapoet(四)

成员变量

我们可以通过 Fields来达到生成成员变量的作用

        FieldSpec android = FieldSpec.builder(String.class, "android")
                .addModifiers(Modifier.PRIVATE, Modifier.FINAL)
                .build();
        TypeSpec helloWorld = TypeSpec.classBuilder("HelloWorld")
                .addModifiers(Modifier.PUBLIC)
                .addField(android)
                .addField(String.class, "robot", Modifier.PRIVATE, Modifier.FINAL)
                .build();

代码输出结果:
编译时动态生成代码技术之javapoet(四)initializer可以初始化成员变量

 FieldSpec android = FieldSpec.builder(String.class, "android")
                .addModifiers(Modifier.PRIVATE, Modifier.FINAL)
                .initializer("$S + $L", "Lollipop v.", 5.0d)
                .build();

代码输出结果:
编译时动态生成代码技术之javapoet(四)

接口

Javpoet中的接口方法必须始终用 PUBLIC ABSTRACT 修饰符修饰,而对于字段Field必须用PUBLIC STATIC FINAL修饰。当生成一个接口时,这些都是非常必要的。

        TypeSpec helloWorld = TypeSpec.interfaceBuilder("HelloWorld")
                .addModifiers(Modifier.PUBLIC)
                .addField(FieldSpec.builder(String.class, "ONLY_THING_THAT_IS_CONSTANT")
                        .addModifiers(Modifier.PUBLIC, Modifier.STATIC, Modifier.FINAL)
                        .initializer("$S", "change")
                        .build())
                .addMethod(MethodSpec.methodBuilder("beep")
                        .addModifiers(Modifier.PUBLIC, Modifier.ABSTRACT)
                        .build())
                .build();

代码输出结果:
编译时动态生成代码技术之javapoet(四)

生成接口对象时,这些修饰符都会省略掉

如果不加编译会报错
编译时动态生成代码技术之javapoet(四)

枚举

使用enumBuilder 去创建一个枚举类型,使用addEnumConstant()去添加枚举常量值

    TypeSpec helloWorld = TypeSpec.enumBuilder("Roshambo")
                .addModifiers(Modifier.PUBLIC)
                .addEnumConstant("ROCK")
                .addEnumConstant("SCISSORS")
                .addEnumConstant("PAPER")
                .build();

代码输出结果:
编译时动态生成代码技术之javapoet(四)复杂一点的枚举

    TypeSpec helloWorld = TypeSpec.enumBuilder("Roshambo")
                .addModifiers(Modifier.PUBLIC)
                .addEnumConstant("ROCK", TypeSpec.anonymousClassBuilder("$S", "fist")
                        .addMethod(MethodSpec.methodBuilder("toString")
                                .addAnnotation(Override.class)
                                .addModifiers(Modifier.PUBLIC)
                                .addStatement("return $S", "avalanche!")
                                .build())
                        .build())
                .addEnumConstant("SCISSORS", TypeSpec.anonymousClassBuilder("$S", "peace")
                        .build())
                .addEnumConstant("PAPER", TypeSpec.anonymousClassBuilder("$S", "flat")
                        .build())
                .addField(String.class, "handsign", Modifier.PRIVATE, Modifier.FINAL)
                .addMethod(MethodSpec.constructorBuilder()
                        .addParameter(String.class, "handsign")
                        .addStatement("this.$N = $N", "handsign", "handsign")
                        .build())
                .build();

代码输出结果:
编译时动态生成代码技术之javapoet(四)在Android官方的性能优化相关课程中曾经提到使用枚举存在的性能问题,不建议在Android代码中使用枚举。为了弥补Android平台不建议使用枚举的缺陷,官方推出了两个注解,IntDefStringDef,用来提供编译期的类型检查。

匿名内部类

对于匿名内部类,我们可以使用Types.anonymousInnerClass()来生成代码块,然后在匿名内部类中使用,可以通过 $L引用

        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 helloWorld = TypeSpec.classBuilder("HelloWorld")
                .addMethod(MethodSpec.methodBuilder("sortByLength")
                        .addParameter(ParameterizedTypeName.get(List.class, String.class), "strings")
                        .addStatement("$T.sort($N, $L)", Collections.class, "strings", comparator)
                        .build())
                .build();

代码输出结果:
编译时动态生成代码技术之javapoet(四)

注解

前文在介绍CodeBlock的时候已经用了一次 AnnotationSpec生成注解,用在类上,这里简单实例下方法上的注解,以@override为例,很简单

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

代码输出结果:
编译时动态生成代码技术之javapoet(四)javapoet的简单用法到此介绍完了,大部分都是github的官方示例,掌握了这些基本用法我相信对于阅读开源框架的代码会大有用处,对于我们开发自己的架构和框架更是必不可少