0

I have the following Spring Boot class, annotated with the custom annotation Counted:

@RestController
@RequestMapping("/identity")
public class IdentityController {

    @Autowired
    private IdentityService identityService;

    @PostMapping
    @Counted(value = "post_requests_identity")
    public Integer createIdentity() {
        return identityService.createIdentity();
    }
}

The Countedannotation is defined as follows:

@Target({ElementType.METHOD})
@Retention(RetentionPolicy.SOURCE)
public @interface Counted {
    String value();
}

What I want is to write an annotation processor that effectively makes my Controller behave like the below code.

@RestController
@RequestMapping("/identity")
public class IdentityController {

    @Autowired
    private IdentityService identityService;

    @Autowired
    private PrometheusMeterRegistry registry;

    @PostConstruct
    public void init() {
        registry.counter("post_requests_identity");
    }

    @PostMapping
    public Integer createIdentity() {
        registry.counter("post_requests_identity").increment();
        return identityService.createIdentity();
    }
}

I have been able to do this with reflection at runtime, but that greatly extends startup time. Is there a way to do the above with just annotations and a custom annotation processor? Put into words, I want to create an annotation that adds an annotated method to a class, and adds an arbitrary method call to an already existing method.

I am aware that annotation processing does not really support modifying the source. I'd be interested in knowing any other method for me to do the above without putting the registry and its associated code directly in my source code.

Ben
  • 332
  • 3
  • 16
  • 2
    What you need is AOP not AnnotationProcessing – Dean Xu Dec 24 '18 at 22:54
  • 1
    @DeanXu is right. in Java EE, one would use [Interceptors](https://javaee.github.io/tutorial/cdi-adv006.html#GKHJX). If you really incist on modifying existing sources at compile-time, take a look at [Project Lombok](https://projectlombok.org/). They use the annotation processor as an entry point and modify the AST through some undocumented compiler API. – Turing85 Dec 24 '18 at 23:03
  • I've tried that approach, but didn't find a good way to initialize my counters using the Spring AOP API. That's the case I ended up using reflection for, which I didn't like. What should I be looking at for that particular case? For whatever it's worth, I'm not insistent on using annotation processing to achieve this functionality. – Ben Dec 24 '18 at 23:15

1 Answers1

0

You can absolutely make your own interceptor or create your own PostProcessor. However, Spring has a nice built-in feature (which is in additional actually used all around the framework) called Application Events. It's a nice little thing that leverages DI and Spring as a bus via a nice little abstraction exactly for needs like yours. (also see this blog article for more info).

From the ApplicationEvent accepting side, you could have a setup as simple as:

// Create an ApplicationEvent type for your "count" event like so...
public class CountEvent extends ApplicationEvent {

    private String counterName;

    ...

}

// Create a class that accepts the count events and meters them...
@Component
public class MeterEventService {

    @Autowired
    private PrometheusMeterRegistry registry;

    @EventListener
    public void createIdentity(CountEvent countEvent) {
        String countedMethod = countEvent.getCounterName();
        registry.counter(countedMethod).increment();
    }
}

From the sending side, you can use your custom annotation to easily continue from where you left off in your question:

@Component
@Aspect
public class Mail {

    // Autowire in ApplicationContext
    private ApplicationContext applicationContext;

    @After("execution (@<package to annotation>.Counted * *(..))")
    public void countMetric(JoinPoint jp){
        MethodSignature signature = (MethodSignature) jp.getSignature();
        String method = signature.getMethod().getName();
        applicationContext.publishEvent(new CountEvent(method));
    }
}

IMHO it's not too much code for the great convenience of having a feature like this.

Also, if you'd rather use the value from @Counted instead of the method name, you can pull off the annotation similarly doing this.

Dovmo
  • 8,121
  • 3
  • 30
  • 44
  • I found a solution using AOP and the Reflections library. Thanks for putting me on the right track. – Ben Dec 25 '18 at 16:33