Debugging and Logging with Custom Annotations in Java

Illustration for Debugging and Logging with Custom Annotations in Java
By Last updated:

A common mistake developers make when debugging Java applications is cluttering business logic with logging statements (System.out.println or Logger.debug). This makes the code harder to read and maintain. Worse, developers often forget to remove or disable these debug logs in production, leading to noisy logs and performance overhead.

Custom annotations combined with reflection provide a cleaner way to separate logging from business logic. For example, you can annotate methods with @LogExecution and have a logging utility automatically print method calls, arguments, and execution times. This is the same principle behind Spring AOP and other logging frameworks.

Think of custom logging annotations as stickers on a package—instead of opening every box to check its contents, you scan the sticker to get the necessary information.


Step 1: Define a Custom Logging Annotation

import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.lang.annotation.ElementType;

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface LogExecution {
    boolean logParams() default true;
    boolean logTime() default true;
}
  • @Retention(RUNTIME) ensures the annotation is available at runtime.
  • @Target(METHOD) restricts usage to methods.
  • The annotation includes options to log parameters and execution time.

Step 2: Apply the Annotation

class Calculator {

    @LogExecution(logParams = true, logTime = true)
    public int add(int a, int b) {
        return a + b;
    }

    @LogExecution(logParams = false, logTime = true)
    public int multiply(int a, int b) {
        return a * b;
    }
}

Step 3: Create the Logging Handler with Reflection

import java.lang.reflect.Method;

public class LoggerProcessor {

    public static void processLogs(Object obj) throws Exception {
        for (Method method : obj.getClass().getDeclaredMethods()) {
            if (method.isAnnotationPresent(LogExecution.class)) {
                LogExecution log = method.getAnnotation(LogExecution.class);

                long start = System.currentTimeMillis();
                Object result = method.invoke(obj, 5, 3); // Example inputs

                long end = System.currentTimeMillis();

                System.out.println("Executed method: " + method.getName());
                if (log.logParams()) {
                    System.out.println("Parameters: (5, 3)");
                }
                System.out.println("Result: " + result);

                if (log.logTime()) {
                    System.out.println("Execution time: " + (end - start) + " ms");
                }
            }
        }
    }

    public static void main(String[] args) throws Exception {
        Calculator calc = new Calculator();
        processLogs(calc);
    }
}

Output:

Executed method: add
Parameters: (5, 3)
Result: 8
Execution time: 0 ms

Executed method: multiply
Result: 15
Execution time: 0 ms

Real-World Applications

  • Spring AOP – Uses annotations like @Transactional and @Loggable to apply cross-cutting concerns.
  • Hibernate – Logs SQL statements via annotations and configuration.
  • JUnit – Uses annotations like @Test to log test execution results.

📌 What's New in Java Versions?

  • Java 5 – Introduced annotations and reflection improvements.
  • Java 8 – Added parameter reflection (useful for logging parameter names).
  • Java 9 – Module encapsulation affected reflective access to private methods.
  • Java 11 – No significant logging-related annotation changes.
  • Java 17 – Strong encapsulation enforced under modules.
  • Java 21 – No significant updates for logging annotations.

Pitfalls and Best Practices

Pitfalls

  • Hardcoding parameters in reflection (method.invoke(obj, ...)) instead of dynamically retrieving them.
  • Excessive logging in production may slow down performance.
  • Not handling exceptions in reflective calls can break the logging mechanism.

Best Practices

  • Use Method.getParameters() to dynamically log arguments.
  • Wrap reflective calls in robust exception handling.
  • Use logging frameworks (SLF4J, Log4j) instead of System.out.
  • Allow enabling/disabling logs via configuration.

Summary + Key Takeaways

  • Logging with custom annotations reduces code clutter and improves maintainability.
  • Annotations like @LogExecution let you add logging behavior without polluting business logic.
  • Reflection makes it possible to dynamically process annotated methods at runtime.
  • Similar principles are used in frameworks like Spring AOP.

FAQ

  1. Can I log method parameters dynamically instead of hardcoding?
    Yes, use method.getParameters() and args[] to capture real parameters.

  2. Is reflection-based logging slower than direct logging?
    Slightly, but negligible unless used in performance-critical loops.

  3. How does this differ from AOP in Spring?
    Spring uses proxies and bytecode weaving for efficiency, but the concept is similar.

  4. Can I log exceptions with this annotation?
    Yes, wrap method invocation in a try-catch block and log exceptions.

  5. How do I disable logs in production?
    Add configuration flags or integrate with existing logging frameworks.

  6. Can I use multiple annotations on the same method?
    Yes, Java supports multiple annotations; reflection can detect them.

  7. What’s the difference between reflection-based logging and interceptors?
    Interceptors are framework-provided abstractions; reflection is the raw mechanism.

  8. Can annotations log execution order?
    Yes, you can track start and end times of each method execution.

  9. Does Java 8 parameter reflection help logging?
    Yes, it allows capturing parameter names for better debugging logs.

  10. Is it safe to log sensitive data with annotations?
    No, always filter or mask sensitive data before logging.