How to instrument / advice a Spring Data (JPA) repository?

2014-10-08 java spring aspectj spring-data-jpa spring-aop

I'm failing in my effort to advice a spring data jpa repository. The goal is to instrument (around) all non-void public methods in a particular repository annotated with a custom annotation (ResourceNotFound in this example) and throw an exception when the return value is either null or an empty collection.

@Repository 
@ResourceNotFound
@Transactional(readOnly = true)
public interface CityRepository extends JpaRepository<City, Long>, JpaSpecificationExecutor<City> { … }

The following advice is to wire all public methods of the implementations of the interface annotated with @ResourceNotFound.

@Pointcut("within(com.digitalmisfits.spring.aop.annotation.ResourceNotFound *)")
public void beanAnnotatedWithResourceNotFound() {}

@Pointcut("execution(public * *(..))")
public void publicMethod() {}

@Around("beanAnnotatedWithResourceNotFound() && publicMethod()")
public Object publicMethodInsideAClassMarkedWithResourceNotFound(ProceedingJoinPoint pjp) throws Throwable {

    System.out.println("publicMethodInsideAClassMarkedWithResourceNotFound " + pjp.getTarget().toString());;

    Object retVal =  pjp.proceed();

    if(((MethodSignature) pjp.getSignature()).getReturnType() != Void.TYPE && isObjectEmpty(retVal))
        throw new RuntimeException("isObjectEmpty == true");

    return retVal;
}

The publicMethodInsideAClassMarkedWithResourceNotFound(…) method works when the pointcut isspecified as:

@Pointcut("execution(public * package.CityRepository+.*(..))")

However, the @ResourceNotFound annotation is not being picked up. This might be due to the fact that the underlying class of the repository interface is a (proxied) SimpleJpaRepository which does not have that particular annotation.

Is there a way to propagate @ResourceNotFound to the implementation?

-- update --

Changed the question to reflect the fact that the advice (around) only should apply to repositories with a custom annotation.

Answers

The problem is not inherent to AspectJ or Spring-AOP but to Java itself:

Normally annotations from parent classes are not inherited by subclasses, but you can explicitly use @Inherited to specify that it should be inherited. Even in this case, inheritance only occurs along the class hierarchy, not from interfaces to implementing classes, see Javadoc:

Note that this meta-annotation type has no effect if the annotated type is used to annotate anything other than a class. Note also that this meta-annotation only causes annotations to be inherited from superclasses; annotations on implemented interfaces have no effect.

Update: Because I have answered this question several times before, I have just documented the problem and also a workaround in Emulate annotation inheritance for interfaces and methods with AspectJ.

Update: If you annotate your implementing classes instead of the interface itself (e.g. by creating an abstract base class which is annotated by the inheritable annotation), you can simplify your advice with the check for void return type etc. like this:

@Around("execution(public !void (@com.digitalmisfits..ResourceNotFound *).*(..))")
public Object myAdvice(ProceedingJoinPoint thisJoinPoint) throws Throwable {
    System.out.println(thisJoinPoint);
    Object retVal = thisJoinPoint.proceed();
    if (isObjectEmpty(retVal))
        throw new RuntimeException("Illegal empty result");
    return retVal;
}

If you want to intercept the repository call on the repository level, you don't actually need to introduce a custom annotation for that. You should be able to get this working with a plain type match:

 @Pointcut("execution(public !void org.springframework.data.repository.Repository+.*(..))")

This will intercept the execution of all non-void methods of all Spring beans that extend the Spring Data Repository interface.

A slightly related example can be found in the Spring Data examples repository.

I was able to solve my problem using the following construct (basically inspecting the interface chain and search for the specific Annotation):

@Pointcut("execution(public !void org.springframework.data.repository.Repository+.*(..))")
public void publicNonVoidRepositoryMethod() {}

@Around("publicNonVoidRepositoryMethod()")
public Object publicNonVoidRepositoryMethod(ProceedingJoinPoint pjp) throws Throwable {

    Object retVal =  pjp.proceed();

    boolean hasClassAnnotation = false;
    for(Class<?> i: pjp.getTarget().getClass().getInterfaces()) {
        if(i.getAnnotation(ThrowResourceNotFound.class) != null) {
            hasClassAnnotation = true;
            break;
        }
    }

    if(hasClassAnnotation && isObjectEmpty(retVal))
        throw new RuntimeException(messageSource.getMessage("exception.resourceNotFound", new Object[]{}, LocaleContextHolder.getLocale()));

    return retVal;
}
Class[] objs = Arrays.stream(joinPoint.getArgs()).map(item -> item.getClass()).toArray(Class[]::new);
System.out.println("[AspectJ] args interfaces :"+objs);

Class clazz = Class.forName(joinPoint.getSignature().getDeclaringTypeName());
System.out.println("[AspectJ] signature class :"+clazz);

Method method = clazz.getDeclaredMethod(joinPoint.getSignature().getName(), objs) ;
System.out.println("[AspectJ] signature method :"+method);

Query m = method.getDeclaredAnnotation(Query.class) ;
System.out.println("[AspectJ] signature annotation value:"+ (m!=null?m.value():m) );

Although the OP heavily relied on AspectJ solutions, the question as it stands doesn't directly suggest that solutions should be limited to AspectJ. Therefore I'd like to offer a non-AspectJ way to advise a Spring Data JPA Repository. It is based upon adding a custom Interceptor into barebone Spring AOP proxy interceptor chain.

First, configure your custom RepositoryFactoryBean, e.g.

@Configuration
@EnableJpaRepositories(repositoryFactoryBeanClass = CustomRepositoryFactoryBean.class)
public class ConfigJpaRepositories {
}

Next, implement CustomRepositoryFactoryBean to add your own RepositoryProxyPostProcessor to the JpaRepositoryFactory

class CustomRepositoryFactoryBean<R extends JpaRepository<T, I>, T , I extends Serializable> extends JpaRepositoryFactoryBean<R, T, I> {

  protected RepositoryFactorySupport createRepositoryFactory(EntityManager em) {
    RepositoryFactorySupport factory = super.createRepositoryFactory(em);
    factory.addRepositoryProxyPostProcessor(new ResourceNotFoundProxyPostProcessor());
    return factory;
  }

}

Your RepositoryProxyPostProcessor implementation should add your MethodInterceptor to the ProxyFactory for a particular Repository (inspect RepositoryInformation):

class ResourceNotFoundProxyPostProcessor implements RepositoryProxyPostProcessor {

    @Override
    public void postProcess(ProxyFactory factory, RepositoryInformation repositoryInformation) {
        if (repositoryInformation.getRepositoryInterface().equals(CityRepository.class))
            factory.addAdvice(new ResourceNotFoundMethodInterceptor());
    }

}

and in your MethodInterceptor (which, BTW, is subinterface of org.aopalliance.aop.Advice, so still an advice :) ) you have a full power of AspectJ @Around advice:

class ResourceNotFoundMethodInterceptor implements MethodInterceptor {

    @Override
    public Object invoke(MethodInvocation invocation) throws Throwable {
        Method method = invocation.getMethod();
        ResourceNotFound resourceNotFound = method.getAnnotation(ResourceNotFound.class);
        //...
        Object result = invocation.proceed();
        //...
        return result;
    }
}   

Related