5

record-builder/README.md at jordanz/enhancer · Randgalt/record-builder · GitHub

 2 years ago
source link: https://github.com/Randgalt/record-builder/blob/jordanz/enhancer/record-builder-enhancer/README.md
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

RecordBuilder Enhancer

What is RecordBuilder Enhancer

Inject verification, defensive copying, null checks or custom code into your Java Record constructors during compilation.

Features:

Is it safe? Does it use undocumented features of Java?

  • The Enhancer modifies your Java class files. There are some inherent safety concerns with this. However:
    • The industry standard ASM library is used to do the modifications. ASM is even used in the JDK itself.
    • The Enhancer only inserts code in the default constructor of Java records. The code is inserted just after the call to super() and before any existing code in the constructor.
    • It's implemented as a standard javac plugin and uses no undocumented features
  • If you don't like the builtin enhancers you can write your own

How does it relate to RecordBuilder?

They aren't directly related but they share some code and both work on java records.

Why RecordBuilder Enhancer?

Java Records are fantastic data carriers and solve much of the pain of the lack of these types of classes in older versions of Java. RecordBuilder adds builders and withers to records. However, records are still missing simple null checks and a few other niceties. This means boilerplate for every record. RecordBuilder Enhancer is targeted at being able to write Java records without having to add any additional code.

How to Use RecordBuilder Enhancer

First, configure your build environment: see details below. Then, use the Enhancer's annotations to specify Java records that you want to be enhanced.

@RecordBuilderEnhance(enhancers = RequireNonNull.class)
public record MyRecord(String s, List<String> l) {
}

Enhancer inserts code into the default constructor as if you wrote this:

public record MyRecord(String s, List<String> l) {
    public MyRecord {
        Objects.requireNonNull(s, "s is null");
        Objects.requireNonNull(l, "l is null");
    }
}

The class file will be updated like this:

disassembly1.png

Enhancers are applied in the order listed in the annotation. E.g.

// will apply RequireNonNull and then CopyCollection
@RecordBuilderEnhance(enhancers = {RequireNonNull.class, CopyCollection.class})
public record MyRecord(List<String> l) {}

becomes

public record MyRecord(List<String> l) {
    Objects.requireNonNull(l);
    l = List.copyOf(l);
}

Arguments

Enhancers can optional receive arguments (the builtin NotNullAnnotations does). Use @RecordBuilderEnhanceArguments to pass arguments. E.g.

@RecordBuilderEnhance(enhancers = NotNullAnnotations.class, 
        arguments = @RecordBuilderEnhanceArguments(enhancer = NotNullAnnotations.class, arguments = "altnonnull"))
public record MyRecord(@AltNonNull List<String> l) {}

Builtin Enhancers

ID Arguments Description
EmptyNullOptional - Use empty() for null Optional/OptionalInt/OptionalLong/OptionalDouble record components
EmptyNullString - Use "" for null String record components
CopyCollection - Make defensive copies of Collection, List and Map record components
CopyCollectionNullableEmpty - Make defensive copies of Collection, List and Map record components or empty collections when null
GuavaCopyCollection - Same as CopyCollection but uses Google Guava collections
GuavaCopyCollectionNullableEmpty - Same as CopyCollectionNullableEmpty but uses Google Guava collections
RequireNonNull - Call requireNonNull() on all non-primitive record components - note: checks other enhancers and doesn't apply to Strings if EmptyNullString is being used, etc.
NotNullAnnotations expression (optional) Any parameter with an annotation whose name matches this enhancer's regular expression argument will be passed to requireNonNull(). The argument is optional. If not supplied then (notnull)|(nonnull) is used. Matching is always case insensitive.

Examples

EmptyNullOptional

@RecordBuilderEnhance(enhancers = EmptyNullOptional.class)
public record MyRecord(Optional<String> s, OptionalInt i) {}

becomes

public record MyRecord(Optional<String> s, OptionalInt i) {
    public MyRecord {
        s = (s != null) ? s : Optional.empty();
        i = (i != null) ? i : OptionalInt.empty();
    }
}

EmptyNullString

@RecordBuilderEnhance(enhancers = EmptyNullString.class)
public record MyRecord(String s) {}

becomes

public record MyRecord(String s) {
    public MyRecord {
        s = (s != null) ? s : "";
    }
}

CopyCollection

@RecordBuilderEnhance(enhancers = CopyCollection.class)
public record MyRecord(Collection<String> c, Set<String> s, List<String> l, Map<String, String> m) {}

becomes

public record MyRecord(String s) {
    public MyRecord {
        c = Set.copyOf(c);
        s = Set.copyOf(s);
        l = List.copyOf(l);
        m = Map.copyOf(m);
    }
}

CopyCollectionNullableEmpty

@RecordBuilderEnhance(enhancers = CopyCollectionNullableEmpty.class)
public record MyRecord(Collection<String> c, Set<String> s, List<String> l, Map<String, String> m) {}

becomes

public record MyRecord(String s) {
    public MyRecord {
        c = (c != null) ? Set.copyOf(c) : Set.of();
        s = (s != null) ? Set.copyOf(s) : Set.of();
        l = (l != null) ? List.copyOf(l) : List.of();
        m = (m != null) ? Map.copyOf(m) : Map.of();
    }
}

GuavaCopyCollection

@RecordBuilderEnhance(enhancers = GuavaCopyCollection.class)
public record MyRecord(Collection<String> c, Set<String> s, List<String> l, Map<String, String> m) {}

becomes

public record MyRecord(String s) {
    public MyRecord {
        c = ImmutableSet.copyOf(c);
        s = ImmutableSet.copyOf(s);
        l = ImmutableList.copyOf(l);
        m = Map.copyOf(m);
    }
}

GuavaCopyCollectionNullableEmpty

@RecordBuilderEnhance(enhancers = GuavaCopyCollectionNullableEmpty.class)
public record MyRecord(Collection<String> c, Set<String> s, List<String> l, Map<String, String> m) {}

becomes

public record MyRecord(String s) {
    public MyRecord {
        c = (c != null) ? ImmutableSet.copyOf(c) : ImmutableSet.of();
        s = (s != null) ? ImmutableSet.copyOf(s) : ImmutableSet.of();
        l = (l != null) ? ImmutableList.copyOf(l) : ImmutableList.of();
        m = (m != null) ? ImmutableMap.copyOf(m) : ImmutableMap.of();
    }
}

RequireNonNull

@RecordBuilderEnhance(enhancers = RequireNonNull.class)
public record MyRecord(String s, Instant t) {}

becomes

public record MyRecord(String s, Instant t) {
    public MyRecord {
        Objects.requireNonNull(s, "s is null");
        Objects.requireNonNull(t, "t is null");
    }
}

NotNullAnnotations

@RecordBuilderEnhance(enhancers = NotNullAnnotations.class,
        arguments = @RecordBuilderEnhanceArguments(enhancer = NotNullAnnotations.class, arguments = "(notnull)|(mynil)"))
public record MyRecord(String s, @NotNull Instant t, @MyNil Thing thing) {}

becomes

public record MyRecord(String s, @NotNull Instant t, @MyNil Thing thing) {
    public MyRecord {
        Objects.requireNonNull(t, "t is null");
        Objects.requireNonNull(thing, "thing is null");
    }
}

Create A Custom Annotation

Using @RecordBuilderEnhance.Template you can create your own RecordBuilderEnhance annotation that always uses the set of enhancers that you want.

@RecordBuilderEnhance.Template(enhancers = {RequireNonNull.class, NotNullAnnotations.class},
        arguments = @RecordBuilderEnhanceArguments(enhancer = NotNullAnnotations.class, arguments = "altnonnull.*"))
@Retention(RetentionPolicy.SOURCE)
@Target(ElementType.TYPE)
@Inherited
public @interface MyCoEnhance {
}

Now, you can use @MyCoEnhance instead of @RecordBuilderEnhance and the record will be enhanced with the enhancers specified.

Javac Plugin

Add a dependency that contains the discoverable javac plugin to your build tool (see below for Maven or Gradle). javac will auto discover the Enhancer plugin. By default the enhancer assumes the standard directory layout used by most Java build systems. i.e. if a Java source file is at /foo/bar/myproject/src/main/java/my/package/MyClass.java the Enhancer will assume that the compiled class file for that source file will be found at /foo/bar/myproject/target/classes/my/package/MyClass.class. If your build system does not use this method then the Enhancer will need additional configuration (see below). You can also configure some of the behavior of the Enhancer (see below).

Maven

<!-- only needed during compilation -->
<dependency>
    <groupId>io.soabase.record-builder</groupId>
    <artifactId>record-builder-enhancer</artifactId>
    <version>${record.builder.version}</version>
    <scope>provided</scope>
</dependency>

<!-- contains the annotations -->
<dependency>
    <groupId>io.soabase.record-builder</groupId>
    <artifactId>record-builder-enhancer-core</artifactId>
    <version>${record.builder.version}</version>
</dependency>

Gradle

dependencies {
  compileOnly 'io.soabase.record-builder:record-builder-enhancer:$version-goes-here'
  api 'io.soabase.record-builder:record-enhancer-core:$version-goes-here'
}

Options

For normal usage you won't need to set any options for the Enhancer. The following options are available if you need them:

[-hv] [--disable] [--dryRun] [--outputDirectory=<outputTo>] [DIRECTORY]

      [DIRECTORY]   The build's output directory - i.e. where javac writes generated classes. 
                    The value can be a full path or a relative path. If not provided the Enhancer 
                    plugin will attempt to use standard directories.
      --disable     Deactivate/disable the plugin
      --dryRun      Dry run only - doesn't modify any classes. You should enable verbose as well via: -v
  -h, --help        Outputs this help
      --outputDirectory=<outputTo>
                    Optional alternate output directory for enhanced class files
  -v, --verbose     Verbose output during compilation

javac plugin options are specifed on the javac command line or as part of your build tool. On the command line:

javac -Xplugin:"recordbuilderenhancer ...arguments..."

In Maven:

<plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-compiler-plugin</artifactId>
    <configuration>
        <compilerArgument>-Xplugin:recordbuilderenhancer ...arguments...</compilerArgument>
    </configuration>
</plugin>

Write Your Own Custom Enhancer

Notes on writing your own enhancer:

  • Custom Enhancers must be built as separate modules from the code base you want to enhance. This is because the enhancers must be available during compilation.
  • You should somewhat be familiar with Java's AST classes though they are not hard to understand for newcomers
  • Enhancers use the ASM library to produce a list of statements to be inserted into Java record constructors
  • You will need some knowledge of Java bytecode specifics and the Java Language specification. However, it's pretty simple to use javac and javap to show you what bytecodes you need to specify (see below)
  • IMPORTANT - the ASM library has been shaded into the RecordBuilder Enhancer JAR. Make sure your custom enhancer uses the ASM classes that are in the package recordbuilder.org.objectweb.asm.*. Many libraries use ASM and you may see the same classes in multiple packages.

For reference look at the implementation of the builtin enhancers to see exactly how to write one. EmptyNullString is a simple one to use as an example.

Add the SPI to your build

The RecordBuilder Enhancer SPI must be added to the module for your custom enhancer. It's artifact ID is record-builder-enhancer-core. Your enhancer module can contain as many enhancers as you want. Each enhancer must implement the following interface:

public interface RecordBuilderEnhancer {
    InsnList enhance(Processor processor, TypeElement element, List<String> arguments);
}

Your custom enhancer is called for Java records that are annotated to list your enhancer class. Your enhancer is created once during the start of the build process and will be called multiple times for each Java record that is annotated with your enhancer. Your enhancer must return a list of instructions to insert or an empty list. The element parameter refers to the Java record being currently compiled. arguments are any arguments specified in the annotation for the Java record. Processor holds utilities useful during enhancing. Look at the builtin enhancers to see details on how to write them - EmptyNullString is a good one to start with.

Build and install your enhancer JAR and you can then use your new custom enhancer as a dependency in other projects and it will be available as an enhancer. Alternatively, if you have a multi-module build your enhancer can be a module and used to enhance the other modules in the project. The record-builder-test-custom-enhancer does this.

How to get the bytecodes for your enhancer

The trick for getting the bytecodes for your enhancer is to write a simple Java source file that does what you want and then use the java tools to get the bytecodes. For example, let's say you want a custom enhancer that outputs the current date and time to standard out.

Create a text file called "Temp.java" ala:

import java.time.Instant;

public class Temp {
  public Temp() {
    System.out.println(Instant.now());
  }
}

From a terminal compile the class:

javac Temp.java

Then dump the java bytecodes from the compiled class:

javap -c Temp.class

You will see:

Compiled from "Temp.java"
public class Temp {
  public Temp();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: getstatic     #7                  // Field java/lang/System.out:Ljava/io/PrintStream;
       7: invokestatic  #13                 // Method java/time/Instant.now:()Ljava/time/Instant;
      10: invokevirtual #19                 // Method java/io/PrintStream.println:(Ljava/lang/Object;)V
      13: return
}

The first two lines are the call to super(). The lines labeld 4, 7, and 10 are the bytecodes that you want for your enhancer.

Your enhance() implementation would look like this:

InsnList insnList = new InsnList();
insnList.add(new FieldInsnNode(Opcodes.GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;"));
insnList.add(new MethodInsnNode(Opcodes.INVOKESTATIC, "java/time/Instant", "now", "()Ljava/time/Instant;"));
insnList.add(new MethodInsnNode(Opcodes.INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/Object;)V"));
return insnList;

Recommend

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK