Introduction to javapool

brief introduction

Javapool is a third-party dependency that can automatically generate java files. Javapool is the successor of JavaWriter. Javapool should be preferred for new projects because it has a more powerful code model: it can understand types and automatically manage imports. Javapool is also more suitable for composition: instead of circulating the contents of a. Java file from top to bottom at one time, it is better to assemble a file into a declaration tree.

introduce

Here are two import methods in the project
Introduction method in maven project: add in pom.xml

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

Introduction method in Android project: add in gradle that needs to use Javapoet

compile 'com.squareup:javapoet:1.13.0'

Other Java language projects should also be introduced in the relevant configuration files

Simple example

We can use javapoet to generate the following code

package com.example.helloJavaPoet;
import java.lang.String;
import java.lang.System;
public final class HelloJavaPoet {
  public static void main(String[] args) {
    
    System.out.println("Hello, JavaPoet!");
  }
}

Use the following code to generate the above code

import com.squareup.javapoet.JavaFile;
import com.squareup.javapoet.MethodSpec;
import com.squareup.javapoet.TypeSpec;
import javax.lang.model.element.Modifier;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;

public class HelloWorld {
    public static void main(String[] args) {
        // Add a method to the class
        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 represents a class
        TypeSpec helloWorld = TypeSpec.classBuilder("HelloJavaPoet")
                .addModifiers(Modifier.PUBLIC, Modifier.FINAL)
                .addMethod(main)
                .build();
        // JavaFile stands for Java file
        JavaFile javaFile = JavaFile.builder("com.example.helloJavaPoet", helloWorld)
                .build();
        File outFile = new File("com/example/helloJavaPoet.java");
        if(!outFile.getParentFile().exists()) {
            outFile.getParentFile().mkdirs();
        }
        if (!outFile.exists()) {
            try {
                outFile.createNewFile();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }

        FileWriter writer;
        try {
            writer = new FileWriter(outFile.getAbsolutePath());
            writer.write("");//Empty the contents of the original file
            writer.write(javaFile.toString());
            writer.flush();
            writer.close();
        } catch (IOException e) {
            e.printStackTrace();
        }


    }
}


Because it is not implemented in the Android project, the generated file is a little complicated. File and FileWriter are used

Common classes of javapool

JavaFile -- the class used to generate the Java file
TypeSpec -- a class used to generate classes, interfaces, and enumerated objects
MethodSpec -- class used to generate methods
ParameterSpec -- class used to generate parameters
AnnotationSpec -- class used to generate annotations
FieldSpec -- class used to generate variables
ClassName -- the object generated by package name and Class name, which is equivalent to specifying Class in javapool
ParameterizedTypeName -- generate a Class containing generic types through MainClass and IncludeClass

placeholder

$L for Literals

$L refers to the literal value without escape. It can be a string, basic data type, type declaration, annotation or even placeholder for other code blocks, just like% s of Formatter.
In the above code, replacing $S with $L will not be converted into a string (that is, without quotation marks) and will be directly replaced by a literal

$S for Strings

$S refers to placeholders for strings that can be quoted and escaped.
(the escape here and above are not well understood, and the official documents only give examples of quotation marks)

$T for Types

$T refers to the placeholder of array type. It not only supports built-in types, but also automatically generates import statements.

For example:
Use this

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

Generate this

package com.example.helloworld;

import java.util.Date;

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


$N for Names

It refers to a placeholder for a name, such as the name of the called method and the name of the variable (which can be automatically generated methods and variables).

For example:
Use this code

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

generate

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

parameter

Relative parameters

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

Generate string

I ate 3 tacos

Position parameters

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

The generated string is the same as above

Name parameter

Use the syntax $argumentName: X, where x is the letter in the placeholder above, and call CodeBlock.addNamed() with a map that contains all the parameter keys in the format string. Parameter names use A-Z, A-Z, 0-9, and_ And must start with a lowercase character.

Map<String, Object> map = new LinkedHashMap<>();
map.put("food", "tacos");
map.put("count", 3);
CodeBlock.builder().addNamed("I ate $count:L $food:L", map)

The generated string is the same as above

Usage details

Abstract method

Using Modifiers.ABSTRACT can be used to obtain a method without any principal. This is only valid when the class is an abstract class or interface.

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

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

Generate code

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

Generation construction method

The common method is generated by methodspec.methodbuilder (method name), and the construction method is generated by MethodSpec.constructorBuilder()

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

Generation code:

public class HelloWorld {
  private final String greeting;

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

Method parameters

Use ParameterSpec.builder() or addParameter()API of MethodSpec to declare parameters on methods and constructors.

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();
void welcomeOverlords(final String android, final String robot) {
}

Class variable

The generation of variables and method parameters are the same. Use FieldSpec.builder() or addField()API of TypeSpec. There is no explanation here.

Interface

When javapool generates an interface, the interface method must always be PUBLIC ABSTRACT, and the interface field must always be PUBLIC STATIC FINAL. These modifiers are necessary when defining interfaces

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

However, these modifiers are omitted during code generation. These are the default, so we don't need to include them for javac!

public interface HelloWorld {
  String ONLY_THING_THAT_IS_CONSTANT = "change";

  void beep();
}

enumeration

Use enumBuilder to create enumeration types and add values with EnumConstant().

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

Build:

public enum Roshambo {
  ROCK,

  SCISSORS,

  PAPER
}

In addition, javapoet supports fancy enumeration, where the enumeration value overrides the method or calls the superclass constructor. Here is a comprehensive example

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!")
            .returns(String.class)
            .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();

Generation code:

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

  SCISSORS("peace"),

  PAPER("flat");

  private final String handsign;

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

Anonymous Inner Class

In the enumeration code, we used TypeSpec.anonymousInnerClass(). Anonymous inner classes can also be used in code blocks. They are values that can be referenced with $L

example:

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

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

A particularly tricky part of defining anonymous inner classes is giving arguments to superclass constructors. In the above code, we passed an empty string without parameters: TypeSpec.anonymousClassBuilder(""). To pass different parameters, use javapool's code block syntax, separating the parameters with commas.

annotation

Generate annotations with addAnnotation()

For example:

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

Generate code

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

Use AnnotationSpec.builder() to set the properties of the annotation.

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

Generate code

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

In addition, the value of an annotation can be an annotation. Use $L for embedded annotation.

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();
@HeaderList({
    @Header(name = "Accept", value = "application/json; charset=utf-8"),
    @Header(name = "User-Agent", value = "Square Cash")
})
LogReceipt recordEvent(LogRecord logRecord);

Note that you can call addMember() multiple times with the same property name to fill in a list of values for the property.

Javadoc

Fields, methods, and types can be recorded in Javadoc

MethodSpec dismiss = MethodSpec.methodBuilder("dismiss")
    .addJavadoc("Hides {@code message} from the caller's history. Other\n"
        + "participants in the conversation will continue to see the\n"
        + "message in their own history unless they also delete it.\n")
    .addJavadoc("\n")
    .addJavadoc("<p>Use {@link #delete($T)} to delete the entire\n"
        + "conversation for all participants.\n", Conversation.class)
    .addModifiers(Modifier.PUBLIC, Modifier.ABSTRACT)
    .addParameter(Message.class, "message")
    .build();
  /**
   * Hides {@code message} from the caller's history. Other
   * participants in the conversation will continue to see the
   * message in their own history unless they also delete it.
   *
   * <p>Use {@link #delete(Conversation)} to delete the entire
   * conversation for all participants.
   */
  void dismiss(Message message);

Tags: Java

Posted on Sat, 18 Sep 2021 05:52:14 -0400 by alexvgtb