当前位置: 首页 > 工具软件 > JavaPoet > 使用案例 >

Java开源工具库使用之java源代码生成库JavaPoet

夏青青
2023-12-01

前言

JavaPoet 是一个用于生成 .java 源代码文件的 Java API。截止博客发表为止,整个项目核心源码就17个类文件,github上却有 9.9k stars,可谓短小精悍。

javapoet 实现了自动导包和语句分号添加,代码流程控制,代码格式化等功能,很方便开发者生成一些模板代码

github 地址: https://github.com/square/javapoet

pom 依赖:

<dependency>
    <groupId>com.squareup</groupId>
    <artifactId>javapoet</artifactId>
    <version>1.13.0</version>
</dependency>

一、API

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

JavaPoet 所有类:

class说明
AnnotationSpec用于生成注解
ArrayTypeName定义数组类型需要用这个类
ClassName对应一个类、接口或 enum 的名字,由 package 名字和类名字两部分组成
CodeBlock用于生成代码块
CodeWriter代码生成类,将 JavaFile 转换为人类可读和javac使用的字符串。
FieldSpec用于生成字段
JavaFile用于生成.java文件
LineWrapper行相关
MethodSpec用于生成方法或构造方法
NameAllocator指定Java标识符名称以避免冲突、关键字和无效字符
ParameterizedTypeName泛型中的参数化类型
ParameterSpec用于生成参数
TypeName类名字
TypeSpec类、接口或Enum
TypeVariableName泛型中的类型变量
Util工具类
WildcardTypeName泛型中的通配符?

JavaPoet支持语句中变量替换,几个常用替换符号的功能介绍如下:

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

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

类别生成的类型举例javapoet 表达方式
基本类型intTypeName.INT
基本类型包装类型IntegerInteger.class
数组int[]ArrayTypeName.of(int.class)
对象类型StringString.class
参数化类型List<String>ParameterizedTypeName.get(List.class, String.class)
类型变量TTypeVariableName.get(“T”)
通配符类型? extends StringWildcardTypeName.subtypeOf(String.class)

1.1 字段

使用 FieldSpec 可以设置字段类型和名字,还可以设置初始值,和修饰符 public/private/protect

注:javapoet 中 Modifier修饰符都是 javax.lang.model.element.Modifier,而不是 java.lang.reflect.Modifier

// String 类型
FieldSpec name = FieldSpec.builder(String.class, "name")
        .addModifiers(Modifier.PUBLIC).build();
// int 类型
FieldSpec age = FieldSpec.builder(TypeName.INT, "age")
        .addModifiers(Modifier.PRIVATE)
        .initializer("12").build();
// String 数组类型
FieldSpec skills = FieldSpec.builder(ArrayTypeName.of(String.class), "skills")
        .addModifiers(Modifier.PUBLIC).build();

能够生成下面代码

  public String name;

  private int age = 12;

  public String[] skills;

1.2 方法

方法使用 MethodSpec 来构建方法,常用的可以添加修饰符public,返回值,参数,语句等等

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

生成 java 代码 如下

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

除了以上,还支持添加注解, 代码块,异常,javadoc,注释等等

CodeBlock codeBlock = CodeBlock.builder()
        .addStatement("int i = 0")
        .addStatement("$T.out.println(i)", System.class)
        .build();

MethodSpec main = MethodSpec.methodBuilder("main")
        .addModifiers(Modifier.PUBLIC, Modifier.STATIC)
        .returns(void.class)
        .addParameter(String[].class, "args")
        .addAnnotation(Benchmark.class)
        .addException(Exception.class)
        .addJavadoc("this is javapoet demo")
        .addCode(codeBlock)
    	.addComment("这是注释")
        .addStatement("$T.out.println($S)", System.class, "Hello, JavaPoet!")
    	.addComment("这是另一段注释")
        .build();

生成方法

/**
 * this is javapoet demo
 */
@Benchmark
public static void main(String[] args) throws Exception {
    int i = 0;
    System.out.println(i);
    // 这是注释
    System.out.println("Hello, JavaPoet!");
    // 这是另一段注释
}

此外,还支持构造方法等等

MethodSpec m = MethodSpec.constructorBuilder()
                .addModifiers(Modifier.PUBLIC)
                .build();

1.3 代码块

代码块使用 CodeBlock 来生成,可以使用 beginControlFlow endControlFlow 来控制流程,特别是减少了循环和 if 的大括号手动添加

CodeBlock codeBlock = CodeBlock.builder()
        .addStatement("int i = 0")
        .beginControlFlow("for (;i < 10; i++)")
        .addStatement("$T.out.println(i)", System.class)
        .endControlFlow()
        .beginControlFlow("if (i > 9)")
        .beginControlFlow("if (i < 15)")
        .addStatement("i = 200")
        .nextControlFlow("else ")
        .addStatement("i = 300")
        .endControlFlow()
        .addStatement("i++")
        .endControlFlow()
        .build();

生成代码

	int i = 0;
    for (;i < 10; i++) {
      System.out.println(i);
    }
    if (i > 9) {
      if (i < 15) {
        i = 200;
      } else  {
        i = 300;
      }
      i++;
    }

异常代码块也可以用 nextControlFlow 控制

CodeBlock cb = CodeBlock.builder()
                .beginControlFlow("try")
                .addStatement("throw new Exception($S)", "Failed")
                .nextControlFlow("catch ($T e)", Exception.class)
                .addStatement("throw new $T(e)", RuntimeException.class)
                .endControlFlow()
                .build();

生成代码

	try {
      throw new Exception("Failed");
    } catch (Exception e) {
      throw new RuntimeException(e);
    }

1.4 类

类使用 TypeSpec 来生成,可以添加字段,方法,构造方法,静态代码块,接口,父类等等方法

TypeSpec helloWorld = TypeSpec.classBuilder("HelloWorld")
        .addModifiers(Modifier.PUBLIC, Modifier.FINAL)
        .addField(name)
        .addField(age)
        .addField(skills)
        .addAnnotation(Benchmark.class)
        .addSuperinterface(Serializable.class)
        .superclass(AbstractList.class)
        .addMethod(main)
        .addMethod(m)
        .build();

不仅可以生成普通类,还可以生成枚举,接口,匿名类等

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

生成代码如下

public enum Roshambo {
  ROCK,

  SCISSORS,

  PAPER
}

1.5 java 文件

最终 java 文件可以通过 JavaFile 生成

JavaFile javaFile = JavaFile.builder("com.demo.helloworld", helloWorld)
    			.skipJavaLangImports(true)
                .build();
// 在控制台打印代码
javaFile.writeTo(System.out);
// 将代码存储为文件
javaFile.writeTo(new File("D:\\java"));

二、使用例子

2.1 数据库表生成 Bean

将一个数据库的所有表转化为实体类。使用 spring-boot-starter-jdbc 连接数据库,guava库实现表和字段的驼峰转化,实体类带有lombok的@Data注解

@SpringBootTest
public class EnityGenerate {
    @Autowired
    private JdbcTemplate jdbcTemplate;

    @Test
    public void main() {
        String databaseName = "test";
        String packageName = "com.aabond.demo.enity";

        List<Map<String, Object>> list = jdbcTemplate.queryForList("select\n" +
                "\t\ttable_name,\n" +
                "    column_name,\n" +
                "    data_type\n" +
                "from information_schema.columns\n" +
                "where table_schema = '" + databaseName + "'");

        Map<Object, List<Map<String, Object>>> name = list.stream().collect(Collectors.groupingBy(m -> m.get("table_name")));
        name.forEach((tableName, fields) -> {
            Map<String, Type> tmp = new HashMap<>();
            for (Map<String, Object> field : fields) {
                String columnName = (String) field.get("column_name");
                String dataType = (String) field.get("data_type");
                try {
                    tmp.put(CaseFormat.LOWER_UNDERSCORE.to(CaseFormat.LOWER_CAMEL, columnName), mysqlTypeToJavaType(dataType));
                } catch (Exception e) {
                    throw new RuntimeException(e);
                }
            }
            String className = CaseFormat.UPPER_UNDERSCORE.to(CaseFormat.UPPER_CAMEL, (String) tableName);
            TypeSpec generate = generate(className, tmp);
            JavaFile javaFile = JavaFile.builder(packageName, generate)
                    .indent("    ")
                	.skipJavaLangImports(true)
                    .build();
            try {
                javaFile.writeTo(Paths.get("src/main/java"));
            } catch (IOException e) {
                throw new RuntimeException(e);
            }
        });
    }

    private Type mysqlTypeToJavaType(String mysqlType) throws Exception {
        switch (mysqlType) {
            case "varchar":
            case "blob":
            case "text":
            case "tinyblob":
                return String.class;
            case "int":
                return Integer.class;
            case "bigint":
                return Long.class;
            case "timestamp":
                return LocalDateTime.class;
            case "decimal":
                return BigDecimal.class;
            default:
                throw new Exception("unknow mysql type: " + mysqlType);
        }
    }

    private TypeSpec generate(String table, Map<String, Type> field) {
        List<FieldSpec> collect = field.entrySet().stream().map((a) -> FieldSpec.builder(a.getValue(), a.getKey(), Modifier.PRIVATE).build()).collect(Collectors.toList());
        TypeSpec typeSpec = TypeSpec.classBuilder(table)
                .addFields(collect)
                .addAnnotation(Data.class).build();
        return typeSpec;
    }
}

2.2 Service测试类生成

将service层的所有接口的所有方法生成测试类

@Test
public void serviceTestGenerate() {
    String packagePath = "src/main/java/com/aabond/demo/service";
    String packageName = "com.aabond.demo.service";
    String testPath = "src/test/java";

    File path = Paths.get(packagePath).toFile();
    File[] files = path.listFiles(file -> file.getName().endsWith("Service.java"));
    System.out.println(Arrays.toString(files));
    Arrays.stream(Objects.requireNonNull(files)).map(f -> {
        try {
            return Class.forName(packageName + "." + f.getName().replace(".java", ""));
        } catch (ClassNotFoundException e) {
            throw new RuntimeException(e);
        }
    }).forEach(c -> {
        String name = c.getSimpleName();
        Method[] methods = c.getMethods();
        List<String> strings = Arrays.stream(methods).map(Method::getName).collect(Collectors.toList());

        TypeSpec generate = generate(name, strings);
        JavaFile javaFile = JavaFile.builder(packageName, generate)
                .indent("    ")
                .skipJavaLangImports(true)
                .build();
        try {
//                javaFile.writeTo(System.out);
            javaFile.writeTo(Paths.get(testPath));
        } catch (IOException e) {
            throw new RuntimeException(e);
        }

    });

}

private TypeSpec generate(String className, List<String> methodNames) {
    List<MethodSpec> collect = methodNames.stream().map((a) -> MethodSpec.methodBuilder(a)
            .addModifiers(Modifier.PUBLIC).addAnnotation(Test.class).build())
            .collect(Collectors.toList());
    TypeSpec typeSpec = TypeSpec.classBuilder(className)
            .addMethods(collect)
            .build();
    return typeSpec;
}

参考

  1. 基于JavaPoet自动生成java代码文件
 类似资料: