**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.