13

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.

epdittmer
  • 488
  • 2
  • 4
  • 15

5 Answers5

12

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.

Oliver Drotbohm
  • 80,157
  • 18
  • 225
  • 211
  • Have you read the question? The OP whad already gotten that far by himself. What he wants is not what you describe. He wants to capture the methods for classes implementing *any* interface annotated by `@ResourceNotFound`, not matter if it is a `Repository` subclass or not. I think you were a little too fast in downvoting my answer. While my answer is maybe not the solution because, as I wrote, I have never used Spring, I was trying to solely answer the AOP-related part and that answer was correct. Yours is not. Should I also downvote it now, Oliver? – kriegaex Oct 10 '14 at 09:56
  • From the original post: "The goal is to instrument (around) all non-void public methods in the repository and throw an exception when the return value is either null or an empty collection". He has everything except the pointcut. That's what my answer provides. No personal offense: your answer doesn't relate to the question but to the attempted, solution and drives off into areas that don't have to do with the problem at hand, hence the down vote. – Oliver Drotbohm Oct 10 '14 at 10:17
  • 1
    I edited my answer to indicated that using the annotation is superfluous. The down vote is really only to make sure other people landing here don't think they need to mess around with annotations or even Spring Data internals just to achieve something very simple. – Oliver Drotbohm Oct 10 '14 at 10:25
  • 1
    You are right. :-) It is always better to solve the original problem in another way than to bang your head against the wall because there is a road block on your path. Maybe I should just delete my answer if the OP accepts yours and says it solved his problem. – kriegaex Oct 10 '14 at 10:49
  • Olivier, thank you for the input. The problem with your proposed solution is that it applies to ALL derived repositories and not just the ones annotated with a custom annotation like ResourceNotFound. I will update the question to explicitly state the fact that i needs to be based on an annotation. – epdittmer Oct 10 '14 at 11:27
  • Olivier: For the annotations to be picked up, it appars that I have to create a PostProxyProcessor (like @Transactional has) on a custom Factory and process the annotations myself. – epdittmer Oct 10 '14 at 11:35
  • @OliverGierke I dont want to apply it for all repositories and then I create an annotation and add this annotation to around addition on your declaration: `@within(com.sam.example.aspect.aspectexample.model.MyAnnotation)` . But it doesn't handle my annotation. My problem is like this: https://stackoverflow.com/questions/51580725/spring-aspectj-pointcut-on-crudrepository-and-annotation – Samet Baskıcı Jul 29 '18 at 14:43
12

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;
    }
}   
igor.zh
  • 1,410
  • 15
  • 19
  • 1
    Igor. I'm not sure if this was possible at the time the question was posted, but this is definitely an elegant solution to the problem and preferable over raw aspect j. cheers. – epdittmer Sep 21 '18 at 22:45
  • 1
    @epdittmer, Thank you so much for valuing my answer so high, I honestly think but both Oliver's and mine answers are just very different, and gave my one mainly for the reference. And yes, I believe configuring JpaRepository in your own way was there from the very beginning. – igor.zh Nov 29 '18 at 19:38
  • 1
    This is not usefull for Spring Data Mongo because there is `createRepositoryFactory` final so you are not able override this method and there is no way how to get factory. – Saljack Feb 07 '20 at 07:39
  • @Saljack, From what I see in `MongoRepositoryFactoryBean` it has protected non-final method `getFactoryInstance`, it returns same `RepositoryFactorySupport` - exactly what we need. Can't you use this one? – igor.zh Feb 07 '20 at 16:09
  • @igor.zh no it is useless too: `protected RepositoryFactorySupport getFactoryInstance(MongoOperations operations) { return new MongoRepositoryFactory(operations); }` – Saljack Feb 10 '20 at 16:22
  • @Saljack, still cannot get you... Can you define your own custom `CustomRepositoryFactoryBean` as a subclass of `MongoRepositoryFactoryBean`, like in the 2nd step in my answer? If you can do that, then why cannot you override in your custom subclass the `MongoRepositoryFactoryBean.getFactoryInstance` method, invoke your superclass method, get `MongoRepositoryFactory` instance and `addRepositoryProxyPostProcessor` to it? `{ RepositoryFactorySupport factory = super.getFactoryInstance(operations); factory.addRepositoryProxyPostProcessor(...); return factory;}` – igor.zh Feb 10 '20 at 23:35
3

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;
}
epdittmer
  • 488
  • 2
  • 4
  • 15
2

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;
}
Community
  • 1
  • 1
kriegaex
  • 63,017
  • 15
  • 111
  • 202
  • Thank you for the response. The problem with Spring Data JPA is that a dynamic proxy is generated (based on the SimpleJPARepostitory) which is beyond our control therefor you cannot add the annotation to the actual implementation (also, appying the ResourceNotFound annotation on the implementation class won't allow for any specialization on seperate Repositories). The @Transactional works out-of-the-box when applied to the Interface itself; didn't find out how that is achieved – epdittmer Oct 09 '14 at 13:09
  • You have the option in Spring to use class-based CGLIB proxies. Those are created as subclasses and thus inherit annotations declared as `@Inheritable`. – kriegaex Oct 09 '14 at 13:30
  • It appears that class based CGLIB proxies are marked as final, preventing further sub-classing (exception thrown upon targetting). – epdittmer Oct 09 '14 at 13:36
  • You should extend your classes, not the proxies. Those should be the "leaves" on your tree. – kriegaex Oct 09 '14 at 13:54
  • The underlying classes are managed by Spring Data. Basically you define an interface, apply the Repository annoation and enable them using the EnableJpaRepositories annotation in the application contex. When injecting a repository, spring data generates a proxy (based on SimpleJpaRepository) and injects them. This comes down to extending the proxy instead of the class i assume – epdittmer Oct 09 '14 at 14:04
  • I think it's not possible what i'm trying to achieve without extending the RepositoryFactoryBeanSupport to add support for custom Annotations. When looking at the way the Transaction Annotation is handled (via the proxy post processor TransactionalRepositoryProxyPostProcessor) its likely that i'll have to extend the factory bean in order to add an additional proxy post processor to add support for custom annotations – epdittmer Oct 09 '14 at 14:11
  • Yes, maybe. I cannot mame a qualified comment here because I have never actually used Spring. I just know something about its AOP capabilities in comparison or connection to AspectJ, the latter of which I use. – kriegaex Oct 09 '14 at 18:08
  • `@Inherited` has nothing to do with the case here as the Spring AOP mechanism looks up the annotations from a type hierarchy manually. `@Inherited` solely affects annotation lookups made by using reflection directly. Second, as the repository already *is* a proxy, there's already some AOP going on under the covers and an additional aspect would just be put in front of the advisor chain. So by far no need to hack any of the internals. – Oliver Drotbohm Oct 10 '14 at 08:17
-2
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) );