Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

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.

like image 544
epdittmer Avatar asked Oct 08 '14 13:10

epdittmer


2 Answers

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.

like image 137
Oliver Drotbohm Avatar answered Oct 24 '22 10:10

Oliver Drotbohm


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;
    }
}   
like image 41
igor.zh Avatar answered Oct 24 '22 11:10

igor.zh