Java
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Object-Oriented Java
-
Section 4: Collections Framework
-
Section 5: Exception Handling
-
Section 6: Generics
-
Section 7: Functional Java
-
Section 8: Concurrency
-
Section 9: I/O and NIO
-
Section 10: JVM Internals
-
Section 11: Modern Java Features
-
Section 12: Build Tools and Project Structure
-
Section 13: Testing
-
Section 14: Databases and Persistence
-
Section 15: Networking
-
Section 16: Design and Best Practices
-
Section 17: Reflection and Annotations
-
Section 18: Logging and Diagnostics
-
Section 19: Date, Time, and Internationalization
-
Section 20: Java Platform Module System
-
Section 21: Security in Java
-
Section 22: Advanced Collections and Data Structures
-
Section 23: More Concurrency Patterns
-
Section 24: Compression, Files, and System Integration
-
Section 25: GUI Programming
-
Section 26: Practical Projects
-
Section 27: Data Structures and Algorithms
-
Section 28: Interview and Algorithm Practice
-
Section 29: JSON and Data Interchange
-
Section 30: More Concurrency Utilities
-
Section 31: More Collections and Streams Practice
-
Section 32: More File and System Programming
-
Section 33: Standard Library Deep Dive
-
Section 34: More Practice and Drills
-
Section 35: More Testing and Quality
-
Section 36: More Design Patterns and Architecture
-
Section 37: Career and Ecosystem
-
Section 38: More OOP and Architecture Practice
-
Section 39: More Enterprise Concepts
-
Section 40: Advanced JavaFX
-
Section 41: More Interview Practice
166: Creating and Using Custom Annotations
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
UserRegistrationwith at least three fields (e.g.,username,email,password). Mark two of them with@NotNullOrEmpty. - Create a class named
ValidationEnginewith a methodpublic void validate(Object obj). - Inside the
validatemethod, use reflection to iterate through all fields of the provided object. If a field is marked with@NotNullOrEmptyand its value isnullor (if it's a String) empty, throw a newIllegalArgumentExceptionspecifying which field failed validation. - Test your engine by creating a
UserRegistrationobject with some empty fields and passing it to theValidationEngine.
There are no comments for now.