One‑Line Annotation for Method Monitoring in Spring Boot

This article demonstrates how to add a single @Monitor annotation to Spring Boot 3.5.0 services and automatically obtain call‑trace and execution‑time metrics by leveraging Spring AOP, SimpleTraceInterceptor, PerformanceMonitorInterceptor, custom advisors, and a BeanPostProcessor, with full code examples and a test controller.

Spring Full-Stack Practical Cases
Spring Full-Stack Practical Cases
Spring Full-Stack Practical Cases
One‑Line Annotation for Method Monitoring in Spring Boot

Environment: Spring Boot 3.5.0.

1. Introduction In Spring Boot projects, method‑level call tracing and execution‑time monitoring can be achieved with low‑intrusion by using Spring AOP together with built‑in interceptors. A custom annotation drives the process, allowing developers to toggle tracing and performance monitoring without writing repetitive code.

2. Practical example

2.1 Custom annotation

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.METHOD})
public @interface Monitor {
    // trace method call
    boolean trace() default true;
    // record execution time
    boolean monitor() default true;
}

Any class or method annotated with @Monitor will be automatically proxied; the trace and monitor attributes control whether call tracing or performance statistics are collected.

2.2 Define abstract advisor for tracing

public class TraceAdvisor implements PointcutAdvisor {
    public Advice getAdvice() {
        return new SimpleTraceInterceptor(true);
    }
    public Pointcut getPointcut() {
        return new Pointcut() {
            public MethodMatcher getMethodMatcher() {
                return new AbstractMethodMatcher() {
                    public boolean matches(Method method, Class<?> targetClass) {
                        Monitor annotation = method.getAnnotation(Monitor.class);
                        return annotation != null && annotation.trace();
                    }
                };
            }
            public ClassFilter getClassFilter() {
                return ClassFilter.TRUE;
            }
        };
    }
}

2.3 Define advisor for performance monitoring

public class MonitorAdvisor implements PointcutAdvisor {
    public Advice getAdvice() {
        return new PerformanceMonitorInterceptor(true);
    }
    public Pointcut getPointcut() {
        return new Pointcut() {
            public MethodMatcher getMethodMatcher() {
                return new AbstractMethodMatcher() {
                    public boolean matches(Method method, Class<?> targetClass) {
                        Monitor annotation = method.getAnnotation(Monitor.class);
                        return annotation != null && annotation.monitor();
                    }
                };
            }
            public ClassFilter getClassFilter() {
                return ClassFilter.TRUE;
            }
        };
    }
}

2.4 Abstract method matcher (no parameter matching needed)

public abstract class AbstractMethodMatcher implements MethodMatcher {
    @Override
    public boolean isRuntime() { return false; }
    @Override
    public boolean matches(Method method, Class<?> targetClass, Object... args) { return false; }
}

2.5 BeanPostProcessor to attach advisors

@Component
public class MonitorBeanPostProcessor implements BeanPostProcessor {
    @Override
    public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
        Class<?> sourceClass = AopProxyUtils.ultimateTargetClass(bean);
        boolean exists = sourceClass.isAnnotationPresent(Monitor.class);
        if (!exists) {
            for (Method m : sourceClass.getDeclaredMethods()) {
                if (m.isAnnotationPresent(Monitor.class)) { exists = true; break; }
            }
        }
        if (!exists) return bean;
        TraceAdvisor traceAdvisor = new TraceAdvisor();
        MonitorAdvisor monitorAdvisor = new MonitorAdvisor();
        if (AopUtils.isAopProxy(bean) && bean instanceof Advised advised) {
            advised.addAdvisor(traceAdvisor);
            advised.addAdvisor(monitorAdvisor);
        } else {
            ProxyFactory factory = new ProxyFactory(bean);
            factory.setExposeProxy(true);
            factory.addAdvisor(traceAdvisor);
            factory.addAdvisor(monitorAdvisor);
            bean = factory.getProxy();
        }
        return bean;
    }
}

2.6 Test controller

@RestController
@RequestMapping("/monitors")
public class MonitorController {
    @GetMapping("/1")
    @Monitor
    public Object query() throws Exception {
        TimeUnit.MILLISECONDS.sleep(new Random().nextLong(1000));
        return "query...";
    }
    @GetMapping("/2")
    @Monitor(trace = false)
    public Object save() throws Exception {
        TimeUnit.MILLISECONDS.sleep(new Random().nextLong(1000));
        return "save...";
    }
}

Running the two endpoints prints trace and performance information to the console, as shown in the accompanying screenshot.

Thus, with a single @Monitor annotation developers obtain unified call‑trace and performance statistics, simplifying debugging and operational monitoring.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

AOPSpring BootCustom AnnotationBeanPostProcessorMethod MonitoringPerformanceMonitorInterceptor
Spring Full-Stack Practical Cases
Written by

Spring Full-Stack Practical Cases

Full-stack Java development with Vue 2/3 front-end suite; hands-on examples and source code analysis for Spring, Spring Boot 2/3, and Spring Cloud.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.