Skip to Content
Course content

166: Creating and Using Custom Annotations

Click on the "Edit" button in the top corner of the screen to edit your slide content.

I've seen this happen to almost every developer the first time they try to build their own annotations. You spend an hour carefully designing your annotation, placing it perfectly on your methods, and then you write the reflection code to actually do something with it—only to find that your code acts like the annotation doesn't even exist.

The Mystery of the Vanishing Annotation

Let's look at a scenario where we want to create a simple permission system. We want to mark certain methods as requiring "ADMIN" privileges. Here is the code as a learner might first write it:

public @interface RequiresRole {
    String value();
}

public class UserService {
    @RequiresRole("ADMIN")
    public void deleteUser(int id) {
        System.out.println("User deleted.");
    }
}

public class SecurityScanner {
    public static void main(String[] args) throws Exception {
        var method = UserService.class.getMethod("deleteUser", int.class);
        RequiresRole roleAnnotation = method.getAnnotation(RequiresRole.class);
        
        if (roleAnnotation == null) {
            System.out.println("No role annotation found!"); 
        } else {
            System.out.println("Required role: " + roleAnnotation.value());
        }
    }
}

If you run this, you'll get "No role annotation found!". This is frustrating because the annotation is right there in the source code. You didn't misspell anything, and the reflection logic is correct. So, where did it go?

Defining Retention and Target

The problem is that Java annotations have a default "lifespan." By default, custom annotations use RetentionPolicy.CLASS. This means the compiler stores the annotation in the .class file, but the JVM ignores it when the program actually runs. Since we are using reflection at runtime to check for the role, the annotation has already been discarded.

To fix this, we need to use Meta-Annotations—which are just annotations that you apply to other annotations. We need @Retention to tell Java to keep the annotation available during execution, and I highly recommend using @Target to prevent people from accidentally putting your annotation on a class or a field when it was only meant for methods.

Here is the corrected definition:

import java.lang.annotation.*;

@Retention(RetentionPolicy.RUNTIME) // This is the magic fix
@Target(ElementType.METHOD)         // Restricts this to methods only
public @interface RequiresRole {
    String value();
}

Now, when method.getAnnotation(RequiresRole.class) is called, the JVM looks at the loaded class metadata, finds the annotation, and returns it. I usually suggest always defaulting to RUNTIME unless you're building something specifically for a bytecode manipulator or a static analysis tool.

Processing Annotation Data

Now that the annotation is actually visible, you can treat it like a regular Java object. Since RequiresRole defines a method value(), the annotation instance provides that same method to retrieve the string.

In a real-world framework (like Spring), you wouldn't manually call getMethod() in a main class. Instead, you'd likely use a Dynamic Proxy or an AOP (Aspect-Oriented Programming) interceptor to check for this annotation every time a method is invoked. But the core logic remains the same: check if the annotation is present, extract its value, and decide whether to allow the execution to proceed.

One quick tip: if you name your attribute value(), Java allows users to omit the attribute name when using the annotation. That's why we can write @RequiresRole("ADMIN") instead of @RequiresRole(value="ADMIN"). It's a small syntactic sugar that makes your API feel much cleaner.




📋 Practical Task

Building a Field-Level Validation Engine

Your task is to create a custom validation system that checks if fields in a class are empty or null based on a custom annotation.

  • Create an annotation named @NotNullOrEmpty. Ensure it is available at runtime and can only be applied to fields.
  • Create a class named UserRegistration with at least three fields (e.g., username, email, password). Mark two of them with @NotNullOrEmpty.
  • Create a class named ValidationEngine with a method public void validate(Object obj).
  • Inside the validate method, use reflection to iterate through all fields of the provided object. If a field is marked with @NotNullOrEmpty and its value is null or (if it's a String) empty, throw a new IllegalArgumentException specifying which field failed validation.
  • Test your engine by creating a UserRegistration object with some empty fields and passing it to the ValidationEngine.
Rating
0 0

There are no comments for now.

to be the first to leave a comment.