Create custom annotation that adds a method in the same class like lombok’s

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

So, let’s start.

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.

git clone lombok

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

Using eclipse IDE

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.

ant eclipse command

Import the Lombok’s project in Eclipse by using the following steps –

  • Right-click in Eclipse and Select Import > Import
Import a new project
  • Now, select “Existing Projects into Workspace” and click on Next.
Existing Projects into workspace
  • Now, select the Lombok’s project and click on “Finish”.
select lombok's project

Now, we have imported the project into Eclipse. Let’s move on to create our 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.

create a new annotation
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 {
	
}

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.

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.

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.

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.

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 –

ant dist command

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

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.

Using hello annotation

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’.”

Error in hello() method

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

Liked the article? Share this on

Leave a Comment

Your email address will not be published. Required fields are marked *