Succinct is a compile-time metaprogramming tool. It operates similarly to Lombok in how it modifies Java's ASTs, but it allows the user to define what Java source they'd like to generate.
Templates are user-written pieces of code which tell Succinct how the user wants their code to compile. The important patterns are Authors, Modifiers, and Attributes.
Authors (@Authored) generate brand-new elements,
while Modifiers (@Modified) alter existing elements.
Attributes (Attr) control what is generated or changed.
Below are some examples of boilerplate code that a user could generate with Succinct.
Recreating Getter
This is a very simple example of a @Getter annotation, similar to Lombok's, but not as robust.
Take note of the usage of $Name$ replacement, this.fieldValue, and @UseAnnotated(Attr.TYPE).
These symbols are all resolved at compile time, and generated into their classes.
packageany.pckg.test;
importme.redot.succinct.api.annotation.Authored;
importme.redot.succinct.api.annotation.extra.Attr;
importme.redot.succinct.api.annotation.extra.UseAnnotated;
importme.redot.succinct.api.author.FieldAuthor;
importjava.lang.annotation.*;
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Getter {
classGetterAuthorextendsFieldAuthor<Object, Object> {
@Authored@UseAnnotated(Attr.TYPE)
publicObjectget$Name$() {
returnthis.fieldValue;
}
}
}If we apply this annotation onto a field, as it was intended:
packageany.pckg.test;
publicclassExample {
@GetterprivatefinalStringBuilderstringBuilder = newStringBuilder("Hello, world!");
}...and then compile, we see this file as the output:
packageany.pckg.test;
publicclassExample {
privatefinalStringBuilderstringBuilder = newStringBuilder("Hello, world!");
publicStringBuildergetStringBuilder() {
returnthis.stringBuilder;
}
}Thread Safety
Looking at an example where we're generating multiple elements per-annotated-member, we could create an author class such as the following:
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface ReadLocked {
classReadLockAuthorextendsModifiedMethodAuthor<Object> {
@AuthoredprivatefinalReadWriteLockreadWriteLock = newReentrantReadWriteLock();
@Modified(Attr.BODY)
publicvoidread() {
this.readWriteLock.readLock().lock();
try {
generateOriginalBody();
} finally {
this.readWriteLock.readLock().unlock();
}
}
}
}Note its usage of @Modified(Attr.BODY) here - the user's intention is to change only the body
of the method. The most important detail here, however, is generateOriginalBody(). This is a static method inherited from
ModifiedMethodAuthor, but importantly, Succinct replaces its pattern with the entire body of the annotated method.
Alongside the target method being modified, a ReadWriteLock field is authored, which is plain and simple.
If we take an equivalent author annotation for write operations (expand to see):
WriteLocked Author
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface WriteLocked {
classWriteLockAuthorextendsModifiedMethodAuthor<Object> {
@AuthoredprivatefinalReadWriteLockreadWriteLock = newReentrantReadWriteLock();
@Modified(Attr.BODY)
publicvoidwriteLock() {
this.readWriteLock.writeLock().lock();
try {
generateOriginalBody();
} finally {
this.readWriteLock.writeLock().unlock();
}
}
}
}...and apply these annotations to two ordinary methods in our Example class:
packageany.pckg.test;
publicclassExample {
@ReadLockedpublicObjectreadOperation() {
System.out.println("Pretend this is a read operation!");
returnnewObject();
}
@WriteLockedpublicvoidwriteOperation(Objectobject) {
System.out.println("Pretend this is a write operation!");
}
}...we see that this class compiles to:
packageany.pckg.test;
importjava.util.concurrent.locks.ReadWriteLock;
importjava.util.concurrent.locks.ReentrantReadWriteLock;
publicclassExample {
privatefinalReadWriteLockreadWriteLock = newReentrantReadWriteLock();
publicObjectreadOperation() {
this.readWriteLock.readLock().lock();
Objectvar1;
try {
System.out.println("Pretend this is a read operation!");
var1 = newObject();
} finally {
this.readWriteLock.readLock().unlock();
}
returnvar1;
}
publicvoidwriteOperation(Objectobject) {
this.readWriteLock.writeLock().lock();
try {
System.out.println("Pretend this is a write operation!");
} finally {
this.readWriteLock.writeLock().unlock();
}
}
}Note that only one ReadWriteLock field was generated. Succinct warns for these patterns:
[WARNING] Skipped generated field 'readWriteLock', a field with this name already exists.
...but they're completely fine if intentional.
Logger
Succinct also has the ability to inline whatever values the annotation instance is holding.
Here's an example:
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Log {
Stringvalue() default"Hello, world!";
classLogAuthorextendsModifiedMethodAuthor<Object> {
@Modified(Attr.BODY)
publicvoidanyMethod() {
StringinlinedValue = getAnnotationField(0);
System.out.println(inlinedValue);
generateOriginalBody();
}
}
}The author's usage of getAnnotationField(0) here is key. Succinct takes whatever value was supplied in
the annotation's instance, and reconstructs it into an inline expression. The integer supplied to
this method will be which field value (in order) to get. This 'collection' always uses zero-based indexing.
Now, if we annotated this method:
packageany.pckg.test;
publicclassExample {
@Log("Entered some method...")
publicstaticStringsomeMethod() {
Stringother = "We're in some method...";
System.out.println(other);
returnother;
}
}...this would be generated:
packageany.pckg.test;
publicclassExample {
publicstaticStringsomeMethod() {
StringinlinedValue = "Entered some method...";
System.out.println(inlinedValue);
Stringother = "We're in some method...";
System.out.println(other);
returnother;
}
}Unless annotated with
@Persistent, author classes interestingly do not need to compile, so long as their authored elements can compile in their target classes. This is because author classes are, by default, removed after Succinct's processing finishes.Succinct is plug-and-play, just like Lombok. Include the dependency, and immediately generate code.
<dependency>
<groupId>me.redot.succinct</groupId>
<artifactId>processor</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>- This repository is hosted publicly via Zenith Studios. Feel free to use this in your project!
<repository>
<id>zenith-artifactory</id>
<name>Succinct</name>
<url>https://artifactory.zenithstudios.dev/artifactory/succinct</url>
</repository>Succinct is not a replacement for Lombok. Not to worry though, Lombok and Succinct are compatible, so you can use both!
Succinct is still in its early stages and needs lots of work. There are many behaviors and patterns that are very unsupported, as of now. Some patterns will never compile though, so be careful to always think about what contexts you are generating code into.
Have fun!
Sincerely,
Redot ❤️