Dragon killing in Java: how to generate it easily and quickly class file

Posted by Giri J on Tue, 14 Dec 2021 10:54:52 +0100

**In the previous "dragon killing in Java: how to modify the syntax tree", we introduced in detail how to modify the syntax tree using the tool classes provided by the Javac source code**

On this basis, there is an open source tool javapoet that can generate bytecode more quickly. The implementation principle is actually the encapsulation of JavaAPT. However, javapoet has a limitation that it can only generate new bytecode Class file, but it can't modify the original class, which is one of its limitations. Next, let's look at how it works.

0x00 overview

Annotation series

The last article was limited to APT, and this one will continue javapoet , is an open source library of square. Just like its name, Java poet generates java source files through annotations. javapoet is usually used in conjunction with Filer. It is mainly used in conjunction with annotations to eliminate duplicate template code (such as what butterknife and databinding do). Of course, you can also use this technology to make your code more cool.

0x01 easy to use

You should import this library before using it

compile 'com.squareup:javapoet:1.7.0'
Copy code

javapoet is used to generate code, which needs the help of

Common class

Before using javapoet, you need to know 8 common classes

Class name effect
MethodSpec Represents a constructor or method declaration
TypeSpec Represents a class, interface, or enumeration declaration
FieldSpec Represents a member variable and a field declaration
JavaFile A Java file that contains a top-level class
ParameterSpec Used to create parameters
AnnotationSpec Used to create annotations
ClassName Used to wrap a class
TypeName Type. For example, when adding a return value type, use typename VOID

In addition, javapool provides a set of custom string formatting rules, commonly used are

Formatting rules Express meaning
$L Literal
$S character string
$T Class and interface
$N variable

0x02 use advanced

The following explains the usage step by step from simple to deep

Method & control flow:

  • Add methods addCode and addstatement. You can directly use addCode for minimal code without class introduction
MethodSpec main = MethodSpec.methodBuilder("main")
    .addCode(""
        + "int total = 0;\n"
        + "for (int i = 0; i < 10; i++) {\n"
        + "  total += i;\n"
        + "}\n")
    .build();
Copy code

Generated is

void main() {
  int total = 0;
  for (int i = 0; i < 10; i++) {
    total += i;
  }
}
Copy code

If the import method is required, as above addStatement("$T.out.println($S)", System.class, "Hello, JavaPoet!") You need to use it Addstatement to declare

  • More elegant flow control

beginControlFlow flow starts addStatement processing statement endControlFlow() flow ends

As above, rewriting with stream is

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

placeholder

javapoet provides placeholders to help us better generate code

  • $L literal constant (Literals)
private MethodSpec computeRange(String name, int from, int to, String op) {
  return MethodSpec.methodBuilder(name)
      .returns(int.class)
      .addStatement("int result = 0")
      .beginControlFlow("for (int i = $L; i < $L; i++)", from, to)
      .addStatement("result = result $L i", op)
      .endControlFlow()
      .addStatement("return result")
      .build();
}
Copy code

This is a for loop, and op is responsible for symbols such as addition, subtraction, multiplication and division

  • $S String constant (String)
  • $T types

The biggest feature is the automatic import of packages

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

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

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

javaFile.writeTo(System.out);

Copy code

The generated code is as follows, and the package will be automatically imported

package com.example.helloworld;

import java.util.Date;

public final class HelloWorld {
  Date today() {
    return new Date();
  }
}
Copy code

  • $N names usually refer to method names or variable names generated by ourselves

Like this code block

public String byteToHex(int b) {
  char[] result = new char[2];
  result[0] = hexDigit((b >>> 4) & 0xf);
  result[1] = hexDigit(b & 0xf);
  return new String(result);
}

public char hexDigit(int i) {
  return (char) (i < 10 ? i + '0' : i - 10 + 'a');
}
Copy code

We can pass hexDigit() instead.

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

Get corresponding class

There are two ways:

  • ClassName.bestGuess("full class name") returns the classname object. The class represented by the full class name here must exist and the corresponding package will be imported automatically

  • ClassName.get("package name", "class name") returns the classname object without checking whether the class exists

    Therefore, if you use javapoe, you often need to pay special attention to changing the class name in subsequent code refactoring

ClassName hoverboard = ClassName.get("com.mattel", "Hoverboard");
ClassName list = ClassName.get("java.util", "List");
ClassName arrayList = ClassName.get("java.util", "ArrayList");
TypeName listOfHoverboards = ParameterizedTypeName.get(list, hoverboard);

MethodSpec beyond = MethodSpec.methodBuilder("beyond")
    .returns(listOfHoverboards)
    .addStatement("$T result = new $T<>()", listOfHoverboards, arrayList)
    .addStatement("result.add(new $T())", hoverboard)
    .addStatement("result.add(new $T())", hoverboard)
    .addStatement("result.add(new $T())", hoverboard)
    .addStatement("return result")
    .build();
Copy code

Then generate

package com.example.helloworld;

import com.mattel.Hoverboard;
import java.util.ArrayList;
import java.util.List;

public final class HelloWorld {
  List<Hoverboard> beyond() {
    List<Hoverboard> result = new ArrayList<>();
    result.add(new Hoverboard());
    result.add(new Hoverboard());
    result.add(new Hoverboard());
    return result;
  }
}
Copy code

Building elements of classes

  • method

Modification of methods, such as modifiers ABSTRACT

MethodSpec flux = MethodSpec.methodBuilder("flux")
    .addModifiers(Modifier.ABSTRACT, Modifier.PROTECTED)
    .build();

TypeSpec helloWorld = TypeSpec.classBuilder("HelloWorld")
    .addModifiers(Modifier.PUBLIC, Modifier.ABSTRACT)
    .addMethod(flux)
    .build();
Copy code

This will generate the following code

public abstract class HelloWorld {
  protected abstract void flux();
}

Copy code

Of course, Methods and methodspec are required Builder configuration to add method parameters, exceptions, javadoc, annotations, etc.

  • constructor

This is actually a function method, so you can use MethodSpec to generate constructor methods. For example:

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

Will generate

public class HelloWorld {
  private final String greeting;

  public HelloWorld(String greeting) {
    this.greeting = greeting;
  }
}
Copy code

  • Parameters (important)

Previously, we set parameters directly through addstatement. In fact, the parameters also have their own special class ParameterSpec, which we can use Builder () to generate parameters, and then use the addParameter of MethodSpec, which is more elegant.

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

Generated code

void welcomeOverlords(final String android, final String robot) {
}
Copy code

For slightly more complex types, such as generic types and maps, you need to understand several classes that specifically describe types defined by javapool

Common are

classification Generated type Javapool writing It can also be written like this (equivalent Java writing)
Built in type int TypeName.INT int.class
Array type int[] ArrayTypeName.of(int.class) int[].class
Type of package name to be introduced java.io.File ClassName.get("java.io", "File") java.io.File.class
ParameterizedType List ParameterizedTypeName.get(List.class, String.class) -
The type variable (WildcardType) is used to declare generics T TypeVariableName.get("T") -
wildcard type ? extends String WildcardTypeName.subtypeOf(String.class) -
/*
 *Build input type, format as :
 *Map<String, Class<? extends IRouteGroup>>
 */
    ParameterizedTypeName inputMapTypeOfRoot = ParameterizedTypeName.get(
            ClassName.get(Map.class),
            ClassName.get(String.class),
            ParameterizedTypeName.get(
                    ClassName.get(Class.class),
                    WildcardTypeName.subtypeOf(ClassName.get(type_IRouteGroup))
            )
    );

    /*
     *Map<String, RouteMeta>
     */
    ParameterizedTypeName inputMapTypeOfGroup = ParameterizedTypeName.get(
            ClassName.get(Map.class),
            ClassName.get(String.class),
            ClassName.get(RouteMeta.class)
    );        

    /*
     *Build input param name.
     */
    ParameterSpec rootParamSpec = ParameterSpec.builder(inputMapTypeOfRoot, "routes").build();
    ParameterSpec groupParamSpec = ParameterSpec.builder(inputMapTypeOfGroup, "atlas").build();
Copy code

Generate parameter type

public class ARouter$Root$app implements IRouteRoot {
  @Override
  public void loadInto(Map<String, Class<? extends IRouteGroup>> routes) {
    routes.put("service", ARouter$Group$service.class);
    routes.put("test", ARouter$Group$test.class);
  }
}

public class ARouter$Group$service implements IRouteGroup {
  @Override
  public void loadInto(Map<String, RouteMeta> atlas) {
    atlas.put("/service/hello", RouteMeta.build(RouteType.PROVIDER, HelloServiceImpl.class, "/service/hello", "service", null, -1, -2147483648));
    atlas.put("/service/json", RouteMeta.build(RouteType.PROVIDER, JsonServiceImpl.class, "/service/json", "service", null, -1, -2147483648));
    atlas.put("/service/single", RouteMeta.build(RouteType.PROVIDER, SingleService.class, "/service/single", "service", null, -1, -2147483648));
  }
}

Copy code

  • field

You can use FieldSpec to declare fields and add them to Method for processing

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

Then generate the code

public class HelloWorld {
  private final String android;

  private final String robot;
}
Copy code

Generally, Builder can create field contents in more detail, such as javadoc, annotations or initializing field parameters, such as:

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

Corresponding generated code

private final String android = "Lollipop v." + 5.0;
Copy code

  • Interface

The interface method must be PUBLIC ABSTRACT, and the interface field must be PUBLIC STATIC FINAL, using typespec interfaceBuilder

as follows

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

The generated code is as follows

public interface HelloWorld {
  String ONLY_THING_THAT_IS_CONSTANT = "change";

  void beep();
}
Copy code

  • Inherit parent class implementation interface

Interface code

package com.test.javapoet;
public interface TestInterface<T> {
    void test(T testPara);
}
Copy code

Parent class code

public class TestExtendesClass {

}
Copy code

Use javapoet to implement the interface and inherit the parent class

final ClassName  InterfaceName = ClassName.get("com.test.javapoet","TestInterface");

    ClassName superinterface = ClassName.bestGuess("com.test.javapoet.TestClass");
    //ClassName superinterface = ClassName.get("com.test.javapoet","aa");

    TypeSpec.Builder spec = TypeSpec.classBuilder("TestImpl")
            .addModifiers(Modifier.PUBLIC)
            // Add an interface. Parameter 1 of ParameterizedTypeName is the interface and parameter 2 is the generic type of the interface
            .addSuperinterface(ParameterizedTypeName.get(InterfaceName, superinterface)) 
            //Use classname Bestguess automatically imports the package
            .superclass(ClassName.bestGuess("com.zs.javapoet.test.TestExtendesClass"));

    MethodSpec.Builder methodSpec = MethodSpec.methodBuilder("test")
            .addAnnotation(Override.class)
            .returns(TypeName.VOID)
            .addParameter(superinterface, "testPara")
            .addStatement("System.out.println(hello)" );

        TypeSpec typeSpec = spec.addMethod(methodSpec.build()).build();

    JavaFile file = JavaFile.builder("com.zs.javapoet", typeSpec).build();
    file.writeTo(System.out);

Copy code

Generate code

package com.test.javapoet;

    import com.zs.javapoet.test.TestExtendesClass;
    import java.lang.Override;

    public class TestImpl extends TestExtendesClass implements TestInterface<TestClass> {
      @Override
      void test(TestClass testPara) {
        System.out.println(hello);
      }
    }
Copy code

  • Enumeration type

Use typespec Create using enumbuilder and add using addEnumConstant

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

Generated code

public enum Roshambo {
  ROCK,

  SCISSORS,

  PAPER
}
Copy code

More complex types can also be supported, such as rewriting, annotation, etc

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

Generate code

public enum Roshambo {
  ROCK("fist") {
    @Override
    public void toString() {
      return "avalanche!";
    }
  },

  SCISSORS("peace"),

  PAPER("flat");

  private final String handsign;

  Roshambo(String handsign) {
    this.handsign = handsign;
  }
}
Copy code

  • Anonymous Inner Class

Type. Is required Anonymous innerclass (""), usually referred to as a $L placeholder

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

Generate code

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

A particularly tricky problem in defining anonymous inner classes is the construction of parameters. In the above code, we passed an empty string without parameters. TypeSpec.anonymousClassBuilder("").

  • annotation

Annotations are easy to use

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

Generate code

  @Override
  public String toString() {
    return "Hoverboard";
  }
Copy code

Through annotationspec Builder () can set properties on annotations:

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

The code is generated as follows

@Headers(
    accept = "application/json; charset=utf-8",
    userAgent = "Square Cash"
)
LogReceipt recordEvent(LogRecord logRecord);
Copy code

Annotations can also annotate other annotations, such as $L

MethodSpec logRecord = MethodSpec.methodBuilder("recordEvent")
    .addModifiers(Modifier.PUBLIC, Modifier.ABSTRACT)
    .addAnnotation(AnnotationSpec.builder(HeaderList.class)
        .addMember("value", "$L", AnnotationSpec.builder(Header.class)
            .addMember("name", "$S", "Accept")
            .addMember("value", "$S", "application/json; charset=utf-8")
            .build())
        .addMember("value", "$L", AnnotationSpec.builder(Header.class)
            .addMember("name", "$S", "User-Agent")
            .addMember("value", "$S", "Square Cash")
            .build())
        .build())
    .addParameter(LogRecord.class, "logRecord")
    .returns(LogReceipt.class)
    .build();
Copy code

Generate code

@HeaderList({
    @Header(name = "Accept", value = "application/json; charset=utf-8"),
    @Header(name = "User-Agent", value = "Square Cash")
})
LogReceipt recordEvent(LogRecord logRecord);
Copy code

notes

  • javadoc

0x03 subsequent

There is a javawriter before javapoet, but javapoet has a more powerful code model and a better understanding of classes. Therefore, it is recommended to use javapoet

Reference articles

Author: ferry boat link: https://juejin.cn/post/6844903456629587976 Source: the copyright of rare earth Nuggets belongs to the author. For commercial reprint, please contact the author for authorization, and for non-commercial reprint, please indicate the source.

WeChat public number [programmer Huang Xiaoxie] is the former engineer of ant Java, who focuses on sharing Java technology dry cargo and job search experience. It is not limited to BAT interview, algorithm, computer basis, database, distributed official account, spring family bucket, micro service, high concurrency, JVM, Docker container, ELK, big data, etc. [book] get 20 selected high-quality e-books necessary for Java interview.

Topics: Android