Search code examples
spring-bootaopaspectjspring-aop

Annotation aspect for both Class Level and Method Level in Spring Boot (NullPointerException)


I am just facing interesting issue when I want to create an aspect for annotation like @Transactional.

Here is the controller and annotation:

@RestController
@SimpleAnnotation
public class HelloController{
    private static final Logger LOGGER = LoggerFactory.getLogger(HelloController.class);
    
    @GetMapping("/hello")
    @SimpleAnnotation(isAllowed=true)
    public String helloController(){
        final String methodName = "helloController";
        callAnotherMerhod();
        LOGGER.info("HelloController for method : {}", methodName);
        return "Hello";
    }

    private void callAnotherMethod(){

    }
}
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD, ElementType.TYPE})
public @interface SimpleAnnotation {

    boolean isAllowed() default false;
}

Aspect for that annotation is here:

@Aspect
@Component
public class SimpleAspect {
    private static final Logger LOGGER = LoggerFactory.getLogger(SimpleAspect.class);

    @Around(value = "@within(simpleAnnotation) || @annotation(simpleAnnotation)", argNames = "simpleAnnotation")
    public Object simpleAnnotation(ProceedingJoinPoint proceedingJoinPoint, SimpleAnnotation simpleAnnotation) throws Throwable{
        LOGGER.info("Simple annotation value: {}, ASPECT-LOG {}", simpleAnnotation.isAllowed(),proceedingJoinPoint.getSignature().getName());
        return proceedingJoinPoint.proceed();
    }
}

When I run the application and hit the http://localhost:8080/hello , everything is fine:

2020-11-09 11:36:48.230  INFO 8479 --- [nio-8080-exec-1] c.s.springaop.aspects.SimpleAspect       : Simple annotation value: true, ASPECT-LOG helloController

2020-11-09 11:36:48.246  INFO 8479 --- [nio-8080-exec-1] c.s.s.controller.HelloController         : HelloController for method : helloController

However if I removed annotation on the method:

@GetMapping("/hello")
    public String helloController(){
        final String methodName = "helloController";
        callAnotherMethod();
        LOGGER.info("HelloController for method : {}", methodName);
        return "Hello";
    }

Then simpleAnnotation parameter becomes null, and aspect method throws NullPointerException.

After that, I changed the order of the aspect like the below, it start to work:

@Around(value = " @annotation(simpleAnnotation) || @within(simpleAnnotation)", argNames = "simpleAnnotation")

However, in this situation if I remove annotation on class level and just put only the method level, then i am facing the same NPE.

I think, in some way aspect's conditions overwrites the values.

  • I tried to separate the class level annotation advice and method level advice, but in that case if i have the annotation on both the class and method level, both advices work (which I do not want)

  • I tried to update like this:

    @Around(value = "@within(simpleAnnotation) || @annotation(simpleAnnotation) || @within(simpleAnnotation)", argNames = "simpleAnnotation")

This seems to be working, but is it a good solution?

EDIT: This solution is not working also. If I have annotation both on the class and method level and let's say class level annotation value is false, and method's level is true, then annotation value will be false.


Solution

  • To workaround the NPE , you may refactor the pointcut designators (@within and @annotation) to two different advice methods within the same aspect.

    The logic to process based on the isAllowed value can be held in a common method and called from both the advice methods.

    To illustrate :

    @Aspect
    @Component
    public class SimpleAspect {
    
        @Around(value = "@annotation(simpleAnnotation) && !@within(my.package.SimpleAnnotation)", argNames = "simpleAnnotation")
        public Object simpleAnnotationOnMethod(ProceedingJoinPoint proceedingJoinPoint, SimpleAnnotation simpleAnnotation)
                throws Throwable {
            System.out.println("Simple annotation value:" + simpleAnnotation.isAllowed() + " , ASPECT-LOG :"
                    + proceedingJoinPoint.getSignature().getName());
            process(simpleAnnotation);
            return proceedingJoinPoint.proceed();
        }
    
        @Around(value = "@within(simpleAnnotation)", argNames = "simpleAnnotation")
        public Object simpleAnnotationOnType(ProceedingJoinPoint proceedingJoinPoint, SimpleAnnotation simpleAnnotation)
                throws Throwable {
            System.out.println("Simple annotation value:" + simpleAnnotation.isAllowed() + " , ASPECT-LOG :"
                    + proceedingJoinPoint.getSignature().getName());
            process(simpleAnnotation);
            return proceedingJoinPoint.proceed();
        }
    
        private void process(SimpleAnnotation simpleAnnotation) {
            // advice logic
        }
    }
    

    Update : Modified the code as commented by @kriegaex