Featured resource
pluralsight tech forecast
2025 Tech Forecast

Which technologies will dominate in 2025? And what skills do you need to keep up?

Check it out
Hamburger Icon
  • Labs icon Lab
  • Core Tech
Labs

Guided: Java Annotations

This code lab will teach you how to use Java annotations to build a dynamic feature toggle system. You will learn to create and apply custom annotations, implement runtime logic to enable or disable features, and develop a compile-time processor to detect unused features. By the end of this lab, you will have practical experience leveraging annotations to create modular and maintainable solutions for real-world applications.

Labs

Path Info

Level
Clock icon Intermediate
Duration
Clock icon 1h 7m
Published
Clock icon Dec 06, 2024

Contact sales

By filling out this form and clicking submit, you acknowledge our privacy policy.

Table of Contents

  1. Challenge

    Introduction

    Welcome to the lab Guided: Java annotations.

    Annotations are a form of metadata that provide information about types and their members for:

    1. Declarative configuration. Annotations can replace XML configuration files, making the code more readable, self-documenting, and reducing boilerplate code.
    2. Compile-time checking. Annotations can enable compile-time error detection, validate code, or generate warnings for potential issues.
    3. Runtime processing. Annotations can also enable dynamic behavior based on metadata, implement cross-cutting concerns, and support dependency injection and aspect-oriented programming.

    Java provides several built-in annotations that you might already be familiar with. For example, here are some annotations defined in the java.lang package:

    • @Override. Indicates that a method overrides a superclass method.
    • @Deprecated. Marks code as obsolete.
    • @FunctionalInterface. Indicates that an interface is a functional interface.

    However, you can create your own annotations to:

    • Add semantic meaning to your code
    • Generate documentation
    • Control method behavior
    • Validate code at compile time

    In this lab, you'll build a feature toggle system using custom annotations that will:

    1. Mark methods as toggleable features
    2. Group related features together
    3. Process annotations at runtime to control method execution
    4. Check for unused features during compilation

    By the end of this lab, you'll have a solid understanding of runtime and compile-time annotation processing, two major uses of annotations in Java. ---

    Familiarizing with the Program Structure

    The application includes the following classes in the src/main/java directory:

    • com.pluralsight.enums.Feature: An enum that defines all possible feature names (e.g., NEW_USER_REGISTRATION, EMAIL_NOTIFICATION, etc.).
    • com.pluralsight.annotations.FeatureGroup: A custom annotation used to categorize classes into feature groups.
    • com.pluralsight.annotations.FeatureToggle: A custom annotation that enables or disables methods based on feature names.
    • com.pluralsight.processors.FeatureToggleProcessor: A runtime annotation processor that scans the annotated methods of a class and executes those marked as enabled.
    • com.pluralsight.validations.FeatureToggleAnnotationProcessor: A compile-time annotation processor that detects and logs unused feature definitions.
    • com.pluralsight.services.UserService: A service class containing methods annotated with @FeatureToggle and categorized using @FeatureGroup.
    • com.pluralsight.Main: A class provided to run the application and demonstrate the feature toggle system in action.

    The Feature enum and the Main class are fully implemented. The other files are partially implemented, with placeholders for you to complete.

    You can compile and run the application using the Run button located at the bottom-right corner of the Terminal. Initially, the application will compile but will not execute any features.

    Begin by examining the code to understand the program's structure. Once you're ready, start coding. If you encounter issues, a solution directory is available for reference. It contains subdirectories named step2, step3, and so on, each corresponding to a step in the lab. Within each subdirectory, solution files follow the naming convention [file]-[step]-[task].java (e.g., FeatureToggle-2-1.java in step2), where [file] represents the file name, the first number indicates the step, and the second indicates the task.

  2. Challenge

    Defining Custom Annotations

    Custom annotations allow you to define your own metadata types that can be applied to code elements. Unlike classes or interfaces, annotations have a special syntax and are defined using the @interface keyword.

    To create a custom annotation, use the following syntax:

    public @interface CustomAnnotation {
      // Annotation elements go here
    }
    

    Annotation elements act like methods but with some key differences:

    • They cannot have parameters.
    • They cannot have a body.
    • Their return types are limited to primitives, String, Class, enums, annotations, and arrays of these types.
    • They can specify default values.

    For example, if you define the following custom annotation:

    public @interface CustomAnnotation {
      int number();
      String message() default "Default message";
    }
    

    You can then apply it to a class, field, or method:

    @CustomAnnotation(number = 3, message = "Hello, Custom Annotation!")
    public class AnnotatedClass {
    
      @CustomAnnotation(number = 6, message = "Field annotation example.")
      private String annotatedField;
    
      @CustomAnnotation(number = 9)
      public void annotatedMethod() {
        System.out.println("This method is annotated.");
      }
    }
    

    If your annotation includes a single element named value, you can use a shorthand notation:

    // Annotation definition
    public @interface CustomAnnotation {
      String value();
    }
    
    // Usage with shorthand
    @CustomAnnotation("Hello")
    public class AnnotatedClass {}
    

    Marker annotations are annotations without elements. They serve as markers or flags. For example:

    // Annotation definition
    public @interface RequiresLogging {}
    
    // Usage
    public class Service {
    
      @RequiresLogging
      public void processData() {
        // ...
      }
    }
    

    Finally, meta-annotations control how your custom annotations behave. The two most important ones are:

    • @Retention: Specifies how long the annotation is retained. Options include:

      • RetentionPolicy.SOURCE: The annotation is available only in the source code and discarded during compilation.
      • RetentionPolicy.CLASS: The annotation is recorded in the class file but not retained at runtime by the Java Virtual Machine (default if no @Retention is specified).
      • RetentionPolicy.RUNTIME: The annotation is recorded in the class file and retained at runtime, making it accessible via reflection.
    • @Target: Specifies the elements an annotation can be applied to. Without @Target, an annotation can be applied to any element. Common options include:

      • ElementType.TYPE: For classes, interfaces, or enums.
      • ElementType.METHOD: For methods.
      • ElementType.FIELD: For fields.
      • ElementType.PARAMETER: For method parameters.
      • ElementType.CONSTRUCTOR: For constructors.

    By applying these concepts, you can now create the custom annotations for the application.

  3. Challenge

    Implementing a Runtime Annotation Processor

    A runtime annotation processor scans classes and their members for specific annotations and performs actions based on the annotation data.

    The Java Reflection API allows you to examine and interact with annotations at runtime. To start, retrieve the Class object of an instance:

    Class<?> clazz = object.getClass();
    

    Then, you can get all declared methods in the class:

    Method[] methods = clazz.getDeclaredMethods();
    

    This way, the processor can read annotation values from the methods and use them to control program behavior:

    // Get the annotation instance
    CustomAnnotation annotation = method.getAnnotation(CustomAnnotation.class);
    // Retrieve a value from the annotation
    String operation = annotation.operation();
    

    For example, you can invoke a method on an object based on the annotation data:

    if (operation.equals("send")) {
      sendMethod.invoke(targetObject, methodParameters);
    }
    

    You can also use the following method to check for an annotation:

    // Check if an annotation is present
    boolean hasAnnotation = element.isAnnotationPresent(CustomAnnotation.class);
    

    In the application, the com.pluralsight.processors.FeatureToggleProcessor class serves as a central component for managing active features at runtime. It implements a feature toggle system using annotations. Any class that employs these feature annotations can be processed dynamically by the static executeFeatures method to enable or disable specific functionality.

    This method is responsible for two main tasks:

    1. Checking the class for a @FeatureGroup annotation:

      • This annotation helps organize and categorize related features.
    2. Examining each method for @FeatureToggle annotations:

      • For every annotated method, the processor should:
        • Determine if the feature is enabled.
        • Execute the method if enabled, or log that the feature is disabled.

    In the next tasks, you will complete the implementation of the executeFeatures method.

  4. Challenge

    Annotating a Class

    Annotations are applied to code elements using the @ symbol followed by the annotation name. When an annotation includes multiple elements or a single element not named value, use parentheses and specify the element names explicitly:

    @CustomAnnotation(
      name = "example",
      enabled = true
    )
    

    If an annotation has a single element named value, you can omit the element name for simplicity:

    @GroupName("Primary")
    

    Applications often use service classes to encapsulate business logic and operations. In the feature toggle application:

    • The com.pluralsight.services.UserService class groups user-related features.
    • Individual methods, such as registerNewUser, represent distinct features that can be enabled or disabled.
    • The @FeatureToggle annotation provides metadata indicating which features should be active.

    Annotating service classes and their methods enables a declarative approach to:

    • Categorize related functionality.
    • Control the availability of specific operations.
    • Manage feature rollouts dynamically without altering the codebase.

    In the upcoming tasks, you will annotate the UserService class with @FeatureGroup and @FeatureToggle.

  5. Challenge

    Implementing a Compile-time Annotation Processor

    Compile-time annotation processors run during the compilation phase, enabling you to:

    • Validate code.
    • Generate warnings.
    • Create new source files.
    • Prevent compilation if specific conditions are not met.

    This approach provides early feedback to developers without introducing runtime overhead.

    To create a compile-time processor, extend the javax.annotation.processing.AbstractProcessor class and implement its methods:

    @SupportedAnnotationTypes("com.example.CustomAnnotation")
    @SupportedSourceVersion(SourceVersion.RELEASE_21)
    public class CustomProcessor extends AbstractProcessor {
      @Override
      public boolean process(Set<? extends TypeElement> annotations, 
                   RoundEnvironment roundEnv) {
        // Process annotations here
        return true;
      }
    }
    

    In this example:

    • The @SupportedAnnotationTypes annotation specifies the annotations the processor handles.
    • The @SupportedSourceVersion annotation declares the supported Java version.
    • The process() method is where the actual annotation processing occurs.

    Within the process() method, the processor can:

    • Locate annotated elements using roundEnv.getElementsAnnotatedWith().
    • Access annotation values.
    • Report messages using processingEnv.getMessager(). This allows for levels such as NOTE, WARNING, and ERROR (errors cause the compilation to fail). For example:
    processingEnv.getMessager().printMessage(
      Diagnostic.Kind.WARNING,
      "Warning message"
    );
    

    Once the processor is implemented, it must be registered by adding its fully qualified class name to a file named javax.annotation.processing.Processor located in the META-INF/services/ directory. For example, in this application, this file is located in the src/main/resourcesMETA-INF/services/ directory and its content is:

    com.pluralsight.validations.FeatureToggleAnnotationProcessor
    

    Using a Processor with Maven

    Since the application uses Maven as its build tool, there are two main approaches to incorporating a compile-time processor:

    1. Separate Module Approach:

      • Create a dedicated module for the processor.
      • Add the processor module as a dependency in the project.
    2. Same Project Approach:

      • Configure the Maven compiler plugin to first compile the processor and then compile the rest of the source code.

    For simplicity, the application adopts the second approach. The pom.xml file is configured with two executions of the Maven compiler plugin:

    • First Execution: This execution compiles the processor without performing annotation processing. It runs in the generate-sources phase:
    <execution>
      <id>compile-annotations</id>
      <phase>generate-sources</phase>
      <goals>
        <goal>compile</goal>
      </goals>
      <configuration>
        <proc>none</proc>
        <includes>
          <include>com/pluralsight/validations/FeatureToggleAnnotationProcessor.java</include>
        </includes>
      </configuration>
    </execution>
    
    • Second Execution: This execution compiles the remaining source files with annotation processing enabled. It runs in the compile phase:
    <execution>
      <id>default-compile</id>
      <phase>compile</phase>
      <goals>
        <goal>compile</goal>
      </goals>
      <configuration>
        <proc>full</proc>
      </configuration>
    </execution>
    

    In this setup:

    • The first execution uses proc=none to compile only the annotation processor itself.
    • The second execution uses proc=full to process annotations in the rest of the code.

    With Maven already configured and the javax.annotation.processing.Processor file in place, you only need to complete the implementation of the FeatureToggleAnnotationProcessor class. This will enable compile-time validation of feature usage.

  6. Challenge

    Conclusion

    Congratulations on successfully completing this Code Lab!

    To compile and run the application, you can either click the Run button in the bottom-right corner of the screen or use the Terminal with the following commands:

    1. Compile and package the application:

      mvn clean package
      
    2. Execute the application:

      java -cp target/feature-toggle-1.0-SNAPSHOT.jar com.pluralsight.Main
      

    During compilation, the following warning message should appear in the output, indicating that the CHANGE_PASSWORD feature is not used in the UserService class:

    [WARNING] Unused feature detected: CHANGE_PASSWORD
    

    The program output should look similar to the following:

    Executing features...
    Processing features in group: User Operations
    Registering a new user...
    Feature EMAIL_NOTIFICATION is disabled.
    

    If you set enabledByDefault to true in the @FeatureToggle annotation of the UserService.sendEmail method, the program will execute the method the next time you compile and run it:

    Executing features...
    Processing features in group: User Operations
    Registering a new user...
    Sending an email...
    ``` ---
    
    ### Extending the Program
    
    Here are some ideas to further enhance your skills and extend the application's capabilities:
    
    1. **Enhance the `@FeatureGroup` Annotation**:
       - Add metadata such as `version` or `author` to provide more context about feature groups.
    
    2. **Strengthen the Compile-Time Processor**:
       - Implement checks to ensure consistent usage of `@FeatureGroup` and `@FeatureToggle`. For example, enforce that all methods in a class categorized by `@FeatureGroup` are annotated with `@FeatureToggle`.
    
    3. **Support Complex Annotations**:
       - Add support for advanced features, such as conditional execution of methods based on parameters or contextual data. ---
    
    ### Related Courses in Pluralsight's Library
    
    If you'd like to continue building your Java skills or explore related topics, check out these courses available on Pluralsight:
    
    - [Java](https://app.pluralsight.com/paths/skill/java-se-17)
    
    These courses cover a wide range of Java programming topics. Explore them to further your learning journey in Java!

Esteban Herrera has more than twelve years of experience in the software development industry. Having worked in many roles and projects, he has found his passion in programming with Java and JavaScript. Nowadays, he spends all his time learning new things, writing articles, teaching programming, and enjoying his kids.

What's a lab?

Hands-on Labs are real environments created by industry experts to help you learn. These environments help you gain knowledge and experience, practice without compromising your system, test without risk, destroy without fear, and let you learn from your mistakes. Hands-on Labs: practice your skills before delivering in the real world.

Provided environment for hands-on practice

We will provide the credentials and environment necessary for you to practice right within your browser.

Guided walkthrough

Follow along with the author’s guided walkthrough and build something new in your provided environment!

Did you know?

On average, you retain 75% more of your learning if you get time for practice.