Stubborn
Stubborn

Reputation: 330

SpringBoot application fails startup when I have Aspect defined on a Bean Method

Working with Springboot 2.7.0. I had a a working application and I made these changes on top of it

Aspect Configuration

@Configuration
@EnableAspectJAutoProxy
@ComponentScan
public class AspectConfig {}

Aspect Interface

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Timed  { }

Aspect Class to Measure method execution time

@Around("@annotation(Timed)")
public Object measureExecutionTime(ProceedingJoinPoint joinPoint) throws Throwable {
    long start = System.currentTimeMillis();
    Object proceed = joinPoint.proceed();
    LOG.info("Time taken for {} is {} ms, joinPoint.getSignature(), System.currentTimeMillis() - start,);
    return proceed;
}

Added the new @Timed annotation to an existing method in a bean (omitting non relevant code)

@Component
@ConditionalOnExpression("${oauth.enabled}")
public class JwtAuthFilter extends OncePerRequestFilter {
    @Timed
    public boolean verifySignatureAndExpiry(String bearerToken){
        // method logic
    }
}

This causes the Springboot application to fail startup.

I can get it to start if I add @Aspect to the JwtAuthFilter class.

but why would I need to do that? It makes the @Timed annotation limited use if I have to annotate every class that needs to use it with @Aspect. Not to mention, though there are no errors, the functionality won't work because an Aspect cannot work on another Aspect.

@Timed works on my controller method though.

@RestController
@RequestMapping(value = "/api/v1", produces = MediaType.APPLICATION_JSON_VALUE)
public class HealthController {

    @GetMapping("/health")
    @Timed
    public Map<String, String> health(){
        return Map.of("status", "up");
    }
}

Upvotes: 1

Views: 1022

Answers (1)

kriegaex
kriegaex

Reputation: 67287

This causes the Spring Boot application to fail startup.

You should always post error messages and relevant stack traces, not just say "fails to start up". You are lucky that in this case, I remember the situation, so I can answer your question. Normally, I would be unable to do so without further information.

I can get it to start if I add @Aspect to the JwtAuthFilter class.

That does not make any sense. Why would you add @Aspect to something which is not an aspect? Of course, it makes the start-up error go away, but it also makes your real aspect not fire, because one Spring AOP aspect cannot advise another one, as you already mentioned. Therefore, this approach is - with all due respect - complete nonsense.

The reason for the exception is: You cannot advise your filter by Spring AOP, because it is derived from GenericFilterBean, which has some final methods. Final methods cannot be overriden, therefore not be proxied either. This has the effect of those methods being called upon the proxy instance directly instead of being delegated to the target object, i.e. if such a method accesses an instance field, it shall find it uninitialised, because the proxy's fields are not meant to be initialised, only the target object's ones. See also my answer here for more info.

In this case, final method org.springframework.web.filter.GenericFilterBean#init is trying to access this.logger, which leads to the NPE which makes Spring Boot's Tomcat fail to start up. This has been reported and briefly explained in this comment in Spring issue #27963, which has been closed as invalid.

@Timed works on my controller method though.

Yes, because your controller does not have a problem with accessing an instance field from a final method.

If you absolutely think that you need to measure your filter method's execution time from an aspect, you can switch from Spring AOP to native AspectJ, either for the whole project via load-time weaving or selectively for some target classes via compile-time weaving. I have tried locally, it works with the right pointcut. Then you can also advise your filter. FYI, the pointcut would be something like:

// Annotated class
@Around("execution(* *(..)) && !within(MyAspect) && @target(Timed)")
// Annotated method
@Around("execution(* *(..)) && !within(MyAspect) && @annotation(Timed)")

AspectJ is more powerful than Spring AOP, so you explicitly need to limit matching to method executions, otherwise other joinpoints such as method calls, constructor calls and others would be affected, too. You also need to make sure that the aspect does not advise itself or other aspects, which is perfectly possible in AspectJ.

Upvotes: 1

Related Questions