0

I have created a custom annotation for some logging purpose. This annotation is applied over spring jpa repository created in project by extending JpaRepository. So what is happening now is that for read methods it is working correctly but for save part @Around advice never get invoked. Below is my @Around advice

@Around("@within(com.myproject.annotations.RepoAware)")
public void log(final ProceedingJoinPoint jp){
  return log(jp,true);
}

my log method is taking one boolean argument on the basis of which i log something.Below is the repo code

@Repository
@RepoAware
public interface MyRepo extends JpaRepository<Student,Long>{
}

Now when i call repo method that is not part of my repository MyRepo like save, saveAll or specifically the method that exists in parent hierarchy then @Around advice not working. When i applied debugger then i can see that during save call proxy is of type CrudRepository. So when i override the save method in MyRepo.class it starts working. I am confused here because MyRepo eventually has CrudRepository extended through JpaRepository. Please let me know how to fix this or what i am doing wrong here.

Also provide help over how to use not expression in pointcut. Say for above example i want to target my all repositories except that have @RepoAware annotation. I created below advice but it's also not working.

@Around("target(org.springframework.data.jpa.repository.JpaRepository) and !@within(com.myproject.annotations.RepoAware)")
public Object logDBMetrics(final ProceedingJoinPoint pjp) throws Throwable {
        return log(pjp,false);
}

above advice get invoked also for repos that have @RepoAware annotation.

Thanks in advance !

Sahil
  • 59
  • 1
  • 10
  • `like save, saveAll` do you know in some cases entities get saved even without calling save/saveAll? – Andrey B. Panfilov Jul 10 '22 at 12:29
  • 1
    yes @AndreyB.Panfilov for transactional it works without save call. But in my case i am persisting a new entity and calling save. – Sahil Jul 10 '22 at 12:35
  • AOP is a complex topic. An incoherent set of code snippets does not do it justice, unless you expect that everyone around here has nothing better to do than to add the missing pieces you left out, guessing what they might be. Please publish an [MCVE](https://stackoverflow.com/help/mcve), e.g. on GitHub, then be happy with the helpful, comprehensive answers you are going to get. Like this, answering is like a lottery. The interface you are trying to intercept does not even seem to have any methods. So how am I supposed to know what you are trying to intercept in the first place? – kriegaex Jul 10 '22 at 13:50

3 Answers3

2

The solution for a Spring AOP aspect, i.e. the proxy-based Spring-style AOP framework, looks like this, if you want to avoid runtime reflection:

@Pointcut(
  "execution(* (" +
  "@com.example.accessingdatajpa.CustomRepositoryAnnotation " +
  "org.springframework.data.repository.CrudRepository+" +
  ").*(..))"
)

This is targeting

  • executions of any methods
  • in any class where
    • the class is CrudRepository or one of its subclasses (+),
    • the class type is annotated by @CustomRepositoryAnnotation.

Update 2: A simpler, more generic version of the solution pointcut is:

@Pointcut("execution(* (@com.example.accessingdatajpa.CustomRepositoryAnnotation *..*).*(..))")

FYI, normally * instead of *..* should work as a simpler replacement for "any class in any package" in AspectJ syntax, but it seems that in this case it is not working.


What does not work and why:

  • @within(CustomRepositoryAnnotation): The save*, findAll*, delete* methods are not defined in a class annotated by @CustomRepositoryAnnotation, but in CrudRepository.
  • @target(CustomRepositoryAnnotation): Interface annotations are not inherited by implementing classes (therefore also not by dynamic proxies) in Java. This is a general JDK issue and unrelated to Spring or even Spring AOP. See my answer here, scroll down to the "update" section.

Update 1, answering questions asked in a comment:

@target(CustomRepositoryAnnotation) ... - how was you able to start application?

By excluding Spring packages as described in my answer here and also excluding final types from the JDK, which of course cannot subclassed and therefore not be be proxied either:

"@target(com.example.accessingdatajpa.CustomRepositoryAnnotation)"
    + " && !within(org.springframework..*) && !within(is(FinalType))"

But in this case, the pointcut does not match, because of what I explained above.

btw, it does not explain why target(MarkerInterface) does work, it seems that annotation support is broken in Spring AOP.

No, it is not broken, just your expectation that annotations be inherited by classes implementing annotated interfaces is wrong.

target(MarkerInterface) works, because the runtime type implements the interface, and therefore the pointcut matches. Class inheritance is not the same as annotation inheritance. Like it or not, the latter only exists to the limited degree explained in the answer I linked to and stated in the @Inherited javadoc.

kriegaex
  • 63,017
  • 15
  • 111
  • 202
  • @target(CustomRepositoryAnnotation) ... - how was you able to start application? Or is it an explanation why narrowing down aspect scope does not work in Spring? btw, it does not explain why `target(MarkerInterface)` does work, it seems that annotation support is broken in Spring AOP. – Andrey B. Panfilov Jul 15 '22 at 03:14
  • I have updated my answer in order to better explain the `@target` use case and also added a section specifically answering your follow-up questions. – kriegaex Jul 15 '22 at 14:22
  • **Update 2:** I have also added a simpler, less explicit, more generic version of the solution pointcut. – kriegaex Jul 15 '22 at 14:43
  • sorry, still not clear, why in this case `execution(* (@com.example.accessingdatajpa.CustomRepositoryAnnotation *..*).*(..))` does not work in LTW mode? [MVCE](https://github.com/andreybpanfilov/gs-accessing-data-jpa) - latest commit, `mvn test` – Andrey B. Panfilov Jul 15 '22 at 18:04
  • I have answered the question related to Spring AOP correctly already, explained a lot and also replied to follow-up questions. Before we get completely off-topic and the answer becomes even longer, please ask a new question. – kriegaex Jul 15 '22 at 18:55
  • I do not think that is off-topic. Actually I do not like the explanation why `@target` designator does not work, ok, it is clear that java can't merge annotation parameters from multiple interfaces, and that is the reason why annotation inheritance is not supported for interfaces, but that does not explain why `execution` designator is supposed to work - from my perspective it should suffer from the similar issue. – Andrey B. Panfilov Jul 17 '22 at 01:20
  • Moreover, switching to LTW reveals that AspectJ weaves only methods derived from annotated interface (plus it seems there are some issues with type inference) - that is exactly the same usecase as `@target` designator. – Andrey B. Panfilov Jul 17 '22 at 01:25
  • That you do not like my explanation of why `@target` cannot work here, might be caused by the fact that you do not understand it or simply are disappointed by the way the JVM handles annotations, which is unrelated to AspectJ. If my explanation, even though I updated it, still is not OK for you, maybe I did not explain well enough. If so, I am sorry, but I cannot write a full AspectJ tutorial here. I am sure you understand that different pointcut designators serve different purposes. If they were equivalent, why would be need them all? – kriegaex Jul 17 '22 at 07:23
  • As for the LTW situation, please trust me that it is a separate issue. Even if we would talk about it here, you could not accept my answer, because you are not the person who asked the question here. Granted, the question is related, but it is not the same - Spring AOP != native AspectJ. I have looked into it and also have some information for you, but not here. Let us stop hijacking this question. Just look at the sheer number of comments accumulated already. So why don't you ask a new question and link to this related one? – kriegaex Jul 17 '22 at 07:28
  • Alexander, thank you for the explanation, I do believe that was instructive for both me and you! My current understanding now is `Spring AOP` is not interchangeable with `LTW` even for the `execution` case. Thank you! Another Q on the topic won't help much because it will be `opinion based`, that is agains SO rules as well. – Andrey B. Panfilov Jul 17 '22 at 08:12
  • No, it will not be opinion-based, and your LTW test case should work just like for Spring AOP. I am not sure yet after some preliminary analysis, but there might be something wrong with the AspectJ matcher. So please, do ask a new question. When I asked you to trust me that this is another topic, I meant it. – kriegaex Jul 17 '22 at 09:15
  • @kriegaex thanks for pointing out the differences. As explained , i learned a lot from this. – Sahil Jul 17 '22 at 12:43
  • Sahil, you are new here, so let me advise you that you ought to accept an answer. While yours is working, it is the most suboptimal of the three. Please decide whether you prefer the clean aspect solution without reflection or the Andrey's advisor. You can accept an answer by clicking the grey check mark beside it, turning it green. Otherwise the question will be listed as unaccepted forever, attracting other developers looking for something to answer, only for them to find out that there are plenty of answers already and there is nothing left for them to do. – kriegaex Jul 17 '22 at 17:14
1

AspectJ has very limited functionality in case of spring infrastructure, however it is possible to implement your requirements via advisors (please also check: Spring Advisor in Java using @Bean)

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


@Component
public class CustomRepositoryAnnotationAdvisor extends AbstractPointcutAdvisor {

    private static final Logger log = LoggerFactory.getLogger(CustomRepositoryAnnotationAdvisor.class);

    private final Advice advice;

    private final Pointcut pointcut;

    public CustomRepositoryAnnotationAdvisor() {
        this.advice = new MethodInterceptor() {
            @Nullable
            @Override
            public Object invoke(@NonNull MethodInvocation invocation) throws Throwable {
                log.info("DemoAnnotationAdvisor: {}", invocation.getMethod().getName());
                return invocation.proceed();
            }
        };
        this.pointcut = new AnnotationMatchingPointcut(CustomRepositoryAnnotation.class, null, true);
    }

    @Override
    public Pointcut getPointcut() {
        return pointcut;
    }

    @Override
    public Advice getAdvice() {
        return advice;
    }

}

Demo project on GH


UPD.

Have performed some research on the topic and found a brilliant article about Spring and AspectJ pointcut designators

First of all, @within is not supposed to work in your case (intercepting superinterface methods) - you ought to use @target, unfortunately @target pointcut designator seems to be broken in Spring AOP: existence of advice with single @target pointcut causes spring to create CGLIB proxies for every bean, which is undesirable and even not possible (jdk classes, final classes, classes without public constructor, etc), attempt to narrow down the scope of advice also fails: for example spring does not apply advice @target(annotation) && target(JpaRepository) for unknown reason.

At second, in case of Spring APO & AspectJ it is possible to achieve your requirements via introducing marker interface and using target(marker interface) as poincut designator, however, I would prefer to stay clear of those puzzles.

Andrey B. Panfilov
  • 4,324
  • 2
  • 12
  • 18
  • _"AspectJ has very limited functionality in case of spring infrastructure, however it is possible to implement your requirements via advisors."_ Both statements are false. An advisor is little more than an old-fashioned, limited version of Spring AOP aspects, i.e. what you are doing here with the advisor can more elegantly be done using an aspect, too - without manually implementing interface methods returning advice and pointcut. – kriegaex Jul 10 '22 at 13:54
  • @kriegaex would you mind providing example? – Andrey B. Panfilov Jul 10 '22 at 14:01
  • It is not my job to provide an example. I shall provide an answer, if the question is clear. I already asked the OP for an [MCVE](https://stackoverflow.com/help/mcve), i.e. it is his job to provide the code to work with. I shall then gladly fix it for him, if I can. This is a Q/A site, not a tutorial site. – kriegaex Jul 10 '22 at 17:52
  • well, you are right. https://github.com/andreybpanfilov/gs-accessing-data-jpa - that is demo project that demonstrates the issue: when running `AccessingDataJpaApplication` it is required to intercept calls of `CrudRepository#save` method if child is marked by `@CustomRepositoryAnnotation`. What is written in `CustomRepositoryAnnotationAspect` does not work. – Andrey B. Panfilov Jul 10 '22 at 18:05
  • Thanks @AndreyB.Panfilov for providing mcve. kriegaex example provided by Andrey explains the issue. Please let us know if it works through annotations. I will try the advisor part. – Sahil Jul 11 '22 at 03:06
  • Andrey, I was not asking **you** for an MCVE but the OP (Sahil). Your MCVE is an educated guess about his situation, which is honourable. I know you were trying to help him. See the result? Instead of using your solution, he implemented an AOP-based workaround, and your work was for nothing. – kriegaex Jul 11 '22 at 19:58
  • @kriegaex in the first place that was interesting to me, I did know there are a lot of issues in Spring related to AspectJ support and prefer to stay clear of that, however I expected to learn something new from you. – Andrey B. Panfilov Jul 12 '22 at 03:08
  • @kriegaex what Andrey explained in his MVCE is the exact issue that I am facing. Hope you will help us to provide a solution. – Sahil Jul 12 '22 at 03:14
0

I finally able to get it worked and below is the approach i used. Thanks to @Andrey B. Panfilov.

I created an marker interface which is extended by MyRepo and then in my around advice i used class type to check if it is assignable from that interface. If yes then log with true otherwise false.

@Around("target(org.springframework.data.jpa.repository.JpaRepository)")
public void log(final ProceedingJoinPoint jp){
  Class<?> clazz=MyRepoInterface.class;
  return clazz.isAssignableFrom(pjp.getTarget().getClass())?log(jp,true):log(pjp,false);
}

----
@Repository
public interface MyRepo extends JpaRepository<Student,Long>,MyRepoInterface{
}

-----
public interface MyRepoInterface{}

spring-aop behaviour is still unknown for some point cuts. Like using AND , NOT with expression doesn't seems to work. I tried several mentioned approaches but none of them worked.

Sahil
  • 59
  • 1
  • 10
  • Sorry to crash your party, but neither your question nor your answer will help anyone, because you simply did not explain your situation well. Where is your [MCVE](https://stackoverflow.com/help/mcve) which helps to reproduce the problem and understand the logic of your workaround. I would not really call it a solution, because you use reflection to work around the fact that you do not know how to express your need in a pointcut. – kriegaex Jul 11 '22 at 20:22
  • https://github.com/andreybpanfilov/gs-accessing-data-jpa @kriegaex This link was already shared with you. It explains the issue preety well. Please get it checkout. I had explained my question preety clearly. Pls have a read again to problem statement. I basically wants a advice with pointcut that can work with custom annotations over spring repositories. Second I want a advice that should involve for all repositories other then that do not have that annotation. Hope you got it now. – Sahil Jul 12 '22 at 03:11
  • Of course, I had a look at the repository before commenting. Your example is about `@Repository @CustomAnnotation interface ... extends JpaRepository`, the GitHub example about `@CustomAnnotation interface ... extends CrudRepository`, which is not the same. Furthermore, neither the sample application nor the test in that repository trigger the demo advisor or the demo aspect. So I really do not understand how it demonstrates or solves your problem. It would be much better if the MCVE would actually contain code corresponding to your question and the aspect or advisor actually do something. – kriegaex Jul 12 '22 at 04:45
  • @kriegaex IMO, arguing about naming conversion is counterproductive. If you run `AccessingDataJpaApplication` you will find out that methods declared in `CustomerRepository` get intercepted by both `Aspect` and `Advisor`, but methods declared in `CrudRepository` get intercepted by `Advisor` only, if you do not see that it means there is a problem with your IDE, because `java -jar demo-0.0.1-SNAPSHOT.jar` provides "expected" output. – Andrey B. Panfilov Jul 12 '22 at 07:07
  • I am not talking about naming conventions, but about the sample code in the question not fitting the one in the answer. And how can it be an IDE problem if, even when I run `mvn test`, I see no output from either the aspect or the advisor? Did you forget to commit something in the example project? – kriegaex Jul 13 '22 at 16:44
  • @kriegaex I have no idea why you are trying to run `mvn test`, nobody have mentioned that before. – Andrey B. Panfilov Jul 13 '22 at 18:08
  • Because you added a test in your commit. I was assuming it was meant to reproduce the problem, which it does not. I was running Maven in order to exclude an IDE problem with the test. But it seems that I have to run the application. The test rerproduces nothing, only the application does. Now I have something to work with tonight after I finish working. – kriegaex Jul 14 '22 at 07:22