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

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();
        }
    }

生成的代码,在控制台打印如下
在这里插入图片描述在上面的代码中我们用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();

结果
在这里插入图片描述一般的类名和方法名是可以被模仿书写的,但是方法中的构造语句是不确定的,这时候就可以用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();
    }

代码输出:
在这里插入图片描述使用 $L代码输出结果:
在这里插入图片描述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();

代码输出结果:
在这里插入图片描述以上是生成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();

代码输出结果:
在这里插入图片描述Javpoet也可以支持静态导入,它通过显示地收集类型成员名来实现

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

代码输出结果:
在这里插入图片描述静态导入,我们只需要在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();
        }

代码输出结果:
在这里插入图片描述在上面例子中,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();
        }

代码输出结果:
在这里插入图片描述相关参数格式化

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();
        }

代码输出结果:
在这里插入图片描述

方法参数

经常我们需要给方法加上入口参数,此时我们就可以通过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();

代码输出结果:
在这里插入图片描述

成员变量

我们可以通过 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();

代码输出结果:
在这里插入图片描述initializer可以初始化成员变量

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

代码输出结果:
在这里插入图片描述

接口

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();

代码输出结果:
在这里插入图片描述

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

如果不加编译会报错
在这里插入图片描述

枚举

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

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

代码输出结果:
在这里插入图片描述复杂一点的枚举

    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();

代码输出结果:
在这里插入图片描述在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();

代码输出结果:
在这里插入图片描述

注解

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

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

代码输出结果:
在这里插入图片描述javapoet的简单用法到此介绍完了,大部分都是github的官方示例,掌握了这些基本用法我相信对于阅读开源框架的代码会大有用处,对于我们开发自己的架构和框架更是必不可少

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值