Annotations are everywhere in modern Java development. You’ve likely seen them in frameworks like Spring (@RestController
, @Autowired
), JPA (@Entity
, @Table
), or testing tools like JUnit (@Test
, @BeforeEach
). These annotations simplify code, abstract complex logic, and help developers focus on core functionality.
Today, however, we will shift gears. Instead of exploring existing annotations, we’ll learn how to create a custom one – an annotation capable of generating methods, similar to how Lombok’s @Getter and @Setter work.
But it’s easier said than done. Creating an annotation that will create a method at runtime is not easy. One has to know how compilation works in Java to add a method in the same class before/during compilation. This is where Lombok comes into the picture.
Among the various approaches to achieving this functionality, Lombok stands out as an effective and straightforward solution. Rather than starting from scratch, we’ll leverage Lombok’s capabilities to build our own custom annotation on top of its framework.
Pre-requisite: Since we will build on top of Lombok’s code, which is based on ANT, please install Ant on your machine before proceeding.
We will follow below steps
- Clone the Lombok’s project
- Build the Lombok’s project
- Import Lombok’s project in eclipse
- Creating custom annotation
- Creating Handlers
- Install a JAR file into your local Maven repository
- Use custom annotation in a separate project
So, let’s start.
Clone the Lombok’s project
First, clone Lombok’s project on your local machine. Here’s the path of its git repository – https://github.com/projectlombok/lombok
Run the below command on the terminal –
git clone https://github.com/projectlombok/lombok.git
This will probably be the output of your git clone command.
Build the Lombok’s project
Lombok’s contributors have developed the project using the Eclipse IDE. Therefore, we have also decided to use Eclipse to build the custom annotation since they noted that we might encounter issues using IntelliJ with Lombok’s. However, we still attempted to build the project in IntelliJ, and we really encountered several problems while building Lombok’s project. So, we returned to Eclipse, where building Lombok’s project turned out to be quite straightforward.
Reference for preference of using Eclipse IDE – https://projectlombok.org/contributing/contributing
Now, go into the “lombok” directory (which we just cloned) and then run the “ant eclipse
” command. So, overall, run these two commands –
cd lombok
ant eclipse
Note: The “ant eclipse” command might take some time to complete.
After successful completion of the above command, you might see below logs on the terminal.
Import Lombok’s project in eclipse
Import the Lombok’s project in Eclipse by using the following steps –
- Right-click in Eclipse and Select Import > Import
- Now, select “Existing Projects into Workspace” and click on Next.
- Now, select the Lombok’s project and click on “Finish”.
Now, we have imported the project into Eclipse. Let’s move on to create our Custom annotation.
Creating custom annotation
We will follow Lombok’s project structure to write our annotation. Looking into Lombok’s project, you’ll notice that all annotations are defined in the “src/core/lombok” folder. So, we will also make our own annotations in the same folder.
But first, we need to define what our annotation will do.
Let’s create an annotation @Hello that will create the below method in the same class.
public String hello(){
return "hello";
}
For now, we will consider placing our annotation only on the class and not on the member fields/variables.
So, let’s create the @Hello annotation in the “src/core/lombok” folder.
package lombok;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target( ElementType.TYPE)
@Retention(RetentionPolicy.SOURCE)
public @interface Hello {
}
Why did we use ElementType.TYPE ?
We wanted the @Hello annotation to work only at the class level, so we used ElementType.TYPE for our annotation. If we wanted our annotation to work at the field level as well, we would have used ElementType.Field.
Why did we use RetentionPolicy.SOURCE ?
There are three types of Retention policies –
- SOURCE: The annotation is discarded during compilation.
- CLASS: The annotation is retained in the bytecode but not available at runtime.
- RUNTIME: The annotation is retained in the bytecode and available at runtime via reflection.
As our annotation only has relevance before compilation, which is why we have used RetentionPolicy.SOURCE.
Creating Handlers
We will now create handlers in the Lombok project to generate methods when the @Hello annotation is detected. To accomplish this, we need to develop two handlers: one for the Eclipse JDT compiler and another for the Javac compiler.
Why do we need to create two handlers?
Eclipse uses its own compiler instead of the standard Javac, while IntelliJ uses the Javac compiler. Therefore, if we want to add a method by placing our annotation above a class, we need to consider both compilers and their respective handling of the annotation.
For the sake of simplicity in this post, we will create only the javac handler, which is used by most editors to compile Java programs.
Javac handlers are placed in the “src/core/lombok/javac/handlers” folder. So, let’s create “HandleHello” handler class in this directory to handle the @Hello annotation implementation.
HandleHello will extend the JavacAnnotationHandler provided by the Lombok library. Then, we would need to implement the handle() method, which will eventually add the required method to the class.
package lombok.javac.handlers;
import com.sun.tools.javac.tree.JCTree.JCAnnotation;
import lombok.Hello;
import lombok.core.AnnotationValues;
import lombok.javac.JavacAnnotationHandler;
import lombok.javac.JavacNode;
import lombok.spi.Provides;
@Provides
public class HandleHello extends JavacAnnotationHandler<Hello>{
@Override
public void handle(AnnotationValues<Hello> annotation, JCAnnotation ast, JavacNode annotationNode) {
}
}
Here is the implementation of the handle function –
package lombok.javac.handlers;
import static lombok.javac.handlers.JavacHandlerUtil.deleteAnnotationIfNeccessary;
import static lombok.javac.handlers.JavacHandlerUtil.deleteImportFromCompilationUnit;
import static lombok.javac.handlers.JavacHandlerUtil.injectMethod;
import static lombok.javac.handlers.JavacHandlerUtil.methodExists;
import java.util.Collection;
import com.sun.tools.javac.code.Flags;
import com.sun.tools.javac.tree.JCTree;
import com.sun.tools.javac.tree.JCTree.JCAnnotation;
import com.sun.tools.javac.tree.JCTree.JCLiteral;
import com.sun.tools.javac.util.List;
import com.sun.tools.javac.util.Name;
import lombok.Hello;
import lombok.core.AST.Kind;
import lombok.core.AnnotationValues;
import lombok.javac.JavacAnnotationHandler;
import lombok.javac.JavacNode;
import lombok.javac.JavacTreeMaker;
import lombok.javac.handlers.JavacHandlerUtil.MemberExistsResult;
import lombok.spi.Provides;
@Provides
public class HandleHello extends JavacAnnotationHandler<Hello> {
@Override
public void handle(AnnotationValues<Hello> annotation, JCAnnotation ast, JavacNode annotationNode) {
Collection<JavacNode> fields = annotationNode.upFromAnnotationToFields();
deleteAnnotationIfNeccessary(annotationNode, Hello.class);
deleteImportFromCompilationUnit(annotationNode, "lombok.AccessLevel");
JavacNode node = annotationNode.up();
// this is to check whether @Hello is placed on the class level only
if (node.getKind() != Kind.TYPE) {
node.addError("@Hello is only supported on a class level.");
return;
}
String methodName = "hello";
Name name = node.toName(methodName);
// internal method provided by lombok to check if a method already exists
// it returns three values - EXISTS_BY_LOMBOK, EXISTS_BY_USER and NOT_EXISTS
MemberExistsResult me = methodExists(methodName, node, false, 0);
// skip if the method already exists
if (!me.equals(MemberExistsResult.NOT_EXISTS)) return;
// Our return type is "String". We need to provide the whole path
JCTree.JCExpression returnType = chainDots(node, "java.lang.String".split("\\."));
// Use List.<JCTree.JCTypeParameter>nil() for type parameters
List<JCTree.JCTypeParameter> typeParams = List.nil();
// Use List.<JCTree.JCVariableDecl>nil() for method parameters
List<JCTree.JCVariableDecl> methodParams = List.nil();
// Use List.<JCTree.JCExpression>nil() for throws clause
List<JCTree.JCExpression> throwsClauses = List.nil();
// creating "return hello" method body
JCTree.JCBlock body = createHelloReturnStatement(node);
JavacTreeMaker maker = node.getTreeMaker();
long accessFlag = Flags.PUBLIC;
JCTree.JCMethodDecl method = maker.MethodDef(maker.Modifiers(accessFlag), name, // method name
returnType, typeParams, // Corrected for type parameters
methodParams, // Corrected for parameters
throwsClauses, // Corrected for throws clause
body, null // Default value, remains null
);
// finally inject the method in the class
injectMethod(node, method);
}
private JCTree.JCBlock createHelloReturnStatement(JavacNode fieldNode) {
// Access the TreeMaker to create AST nodes
JavacTreeMaker maker = fieldNode.getTreeMaker();
// Create a string literal with the value "hello"
JCLiteral helloLiteral = maker.Literal("hello");
// Create a return statement that returns the string literal
JCTree.JCStatement returnStmt = maker.Return(helloLiteral);
List<JCTree.JCStatement> statements = List.of(returnStmt);
// Create the method body as a block
JCTree.JCBlock body = maker.Block(0, statements);
return body;
}
private JCTree.JCExpression chainDots(JavacNode node, String... elements) {
JavacTreeMaker maker = node.getTreeMaker();
JCTree.JCExpression expr = maker.Ident(node.toName(elements[0]));
for (int i = 1; i < elements.length; i++) {
expr = maker.Select(expr, node.toName(elements[i]));
}
return expr;
}
}
We understand that the implementation became quite extensive, but we made sure to include comments with each statement to clarify their purpose. Now, we are ready for the final phase, which involves using @Hello in another location or code while importing Lombok as a library.
Install a JAR file into your local Maven repository
Run the commands below to install a JAR file in your local Maven repository.
ant dist
mvn install:install-file -Dfile=dist/lombok-1.18.37.jar -DgroupId=org.projectlombok -DartifactId=lombok -Dversion=1.18.DemoCustom -Dpackaging=jar
Note that we have set the version to 1.18.DemoCustom. The version of the jar created may differ for you; in our case, it was “lombok-1.18.37”.
Running the ant dist would give the below output –
It created a jar “lombok-1.18.37.jar” inside the “dist” directory. Now, we will install this jar into your local maven repository by using the below command.
mvn install:install-file -Dfile=dist/lombok-1.18.37.jar -DgroupId=org.projectlombok -DartifactId=lombok -Dversion=1.18.DemoCustom -Dpackaging=jar
Use Custom annotation in a separate project
Import the below Lombok’s maven dependency in your maven project
<!-- https://mvnrepository.com/artifact/org.projectlombok/lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.DemoCustom</version>
<scope>provided</scope>
</dependency>
Check the version of the dependency. This is the version we used when installing the jar in the local Maven repository.
Now, you should be able to use the @Hello annotation in your code.
Now, how do we verify that the hello() method was automatically created in the class?
Since the method will be generated during compilation, we can run the “mvn clean install
” command. This command will also execute the compile goal. After running it, we can check whether the method has been created in the `.class` file.
Here are the contents of the Demo.class file.
import lombok.Generated;
public class Demo {
public Demo() {
}
@Generated
public String hello() {
return "hello";
}
}
We can see that the method was successfully created and we can also use this method in our code as well.
public class DemoTest {
public static void main(String[] args) {
Demo demo = new Demo();
System.out.println(demo.hello());
}
}
Running the above program will give you the below output –
hello
Although the program executed successfully, your editor will still show an error: “Cannot resolve method ‘hello’ in ‘Demo’.”
The reason for this is that the editor cannot determine which methods will be generated at compile time. To address this, Lombok has provided a plugin for IntelliJ that displays autocomplete suggestions for the methods that will be created. However, discussing that topic would require a separate post.
This is it for this post. We created a custom annotation on top of Lombok’s code that will add the hello() method to the same class.
We hope that you have liked this article. If you have any doubts or concerns, please write us in the comments or mail us at admin@codekru.com