0

I have an abstract service class.

abstract class AbstractService<T> {
    public void saveNew(T entity) {
    }
}

And I have two more abstract classes extends AbstractService and implement a shared interface.

abstract class MoreAbstractService2<T extends Some2>
        extends AbstractService<T>
        implements SharedInterface {
}
abstract class MoreAbstractService3<T extends Some3>
        extends AbstractService<T>
        implements SharedInterface {
}

Now I want to validate the entity argument on these two extending services' saveNew(T) method.

How can I define a @Pointcut and (or) an @Around for following conditions?

  • extends the AbstractService class
  • implements the SharedInterface interface
Jin Kwon
  • 20,295
  • 14
  • 115
  • 184

3 Answers3

1

you can use within as following:

 within(com.somepackage.Super+)

where com.somepackage.Super is the fully qualified base class name and + means "all subclasses". Other pointcut is

execution(* com.somepackage.Super+.*(..))
Reeta Wani
  • 197
  • 4
  • 13
1

Following code can be used for the validation mentioned.

The point cut is to intercept on the execution of a specific method for the subclasses of AbstractService and the code logic is to only validate if SharedInterface is a superinterface of the target bean.

The use of isAssignableFrom() is required as the interfaces proxied by the AOP proxy does not include SharedInterface. As per my understanding , a pointcut expression to match the second criteria will not be possible for the same reason and hence handled the requirement in the code logic.

Hope this helps

@Aspect
@Component
public class ValidationAspect {

    @Pointcut("execution(* package.to.AbstractService+.saveNew(..))")
    public void isAbstractServiceType() {
    }

    @Around("isAbstractServiceType() && args(entity) && target(bean)")
    public void validateEntityArugments(ProceedingJoinPoint pjp, Object entity, Object bean) throws Throwable {
        if (SharedInterface.class.isAssignableFrom(bean.getClass())) {
            System.out.println(entity);
            // ..validate
        }
        pjp.proceed();
    }
}
R.G
  • 6,436
  • 3
  • 19
  • 28
1

R.G's solution has a few disadvantages:

  • The pointcut matches too many joinpoints.
  • Thus it needs reflection in order to filter out the unnecessary ones.

I am going to show you a stand-alone AspectJ solution (no Spring, I am not a Spring user), but the aspect would look just the same in Spring, you only need to make it a @Component or declare a @Bean factory in your configuration. But the same applies to all the classes you want to intercept, of course.

Because I prefer a full MCVE with all necessary dependency classes in order for you to be able to copy, compile and run it, and because I also added negative test cases (sub-classes only extending the abstract base class or only implementing the interface), this is a lot of code. So please bear with me:

Abstract classes, interface and helper classes:

package de.scrum_master.app;

public abstract class AbstractService<T> {
  public void saveNew(T entity) {
    System.out.println("Saving new entity " + entity);
  }
}
package de.scrum_master.app;

public class Some2 {}
package de.scrum_master.app;

public class Some3 {}
package de.scrum_master.app;

public abstract class MoreAbstractService2<T extends Some2>
  extends AbstractService<T>
  implements SharedInterface {}
package de.scrum_master.app;

public abstract class MoreAbstractService3<T extends Some3>
  extends AbstractService<T>
  implements SharedInterface {}
package de.scrum_master.app;

public interface SharedInterface {
  void doSomething();
}

Driver application (AspectJ + POJOs, not Spring):

This driver class contains some static inner classes subclassing the given base classes and/or implementing the shared interface. Two are used for positive tests (should be intercepted), two for negative tests (should not be intercepted). Each class also contains an additional method as another negative test case which should not be matched - better safe than sorry.

package de.scrum_master.app;

public class Application {
  public static void main(String[] args) {
    // Should be intercepted
    InterceptMe1 interceptMe1 = new InterceptMe1();
    interceptMe1.saveNew(new Some2());
    interceptMe1.doSomething();
    interceptMe1.additional();
    printSeparator();

    // Should be intercepted
    InterceptMe2 interceptMe2 = new InterceptMe2();
    interceptMe2.saveNew(new Some3());
    interceptMe2.doSomething();
    interceptMe2.additional();
    printSeparator();

    // Should NOT be intercepted
    DontInterceptMe1 dontInterceptMe1 = new DontInterceptMe1();
    dontInterceptMe1.saveNew(new Some2());
    dontInterceptMe1.additional();
    printSeparator();

    // Should NOT be intercepted
    DontInterceptMe2 dontInterceptMe2 = new DontInterceptMe2();
    dontInterceptMe2.additional();
    printSeparator();
  }

  private static void printSeparator() {
    System.out.println("\n----------------------------------------\n");
  }

  static class InterceptMe1 extends MoreAbstractService2<Some2> {
    @Override
    public void doSomething() {
      System.out.println("Doing something in MoreAbstractService2<Some2>");
    }

    public void additional() {
      System.out.println("Additional method in MoreAbstractService2<Some2>");
    }
  }

  static class InterceptMe2 extends MoreAbstractService3<Some3> {
    @Override
    public void doSomething() {
      System.out.println("Doing something in MoreAbstractService3<Some3>");
    }

    public void additional() {
      System.out.println("Additional method in MoreAbstractService3<Some3>");
    }
  }

  static class DontInterceptMe1 extends AbstractService<Some2> {
    public void additional() {
      System.out.println("Additional method in AbstractService<Some2>");
    }
  }

  static class DontInterceptMe2 implements SharedInterface {
    @Override
    public void doSomething() {
      System.out.println("Doing something in SharedInterface");    }

    public void additional() {
      System.out.println("Additional method in SharedInterface");
    }
  }

}

Aspect:

package de.scrum_master.aspect;

import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;

@Aspect
public class EntityValidationAspect {
  @Before(
    "execution(* saveNew(*)) && " + 
    "args(entity) && " + 
    "target(de.scrum_master.app.SharedInterface) && " + 
    "target(de.scrum_master.app.AbstractService)"
  )
  public void validateEntity(JoinPoint thisJoinPoint, Object entity) {
    System.out.println("-> Pre-save entity validation: " + entity);
  }
}

As you can see, the aspect uses two target() pointcuts which must both match. It also specifically targets any saveNew method with a single argument saveNew(*), binding that argument as an advice method parameter via args().

For demonstration's sake I do not validate anything (I don't know how you want to do that) but just print the entity. Thus, a @Before advice is sufficient. If in case of negative validation you want to throw an exception, this advice type is also okay. If you need to do more, such as manipulate the entity's state or replace it before passing it on to the target method, call an alternative target method instead or none at all, return a specific result (in case of non-void methods, here not applicable), handle exceptions from the target method etc., you ought to use an @Around advice instead.

Console log:

-> Pre-save entity validation: de.scrum_master.app.Some2@28a418fc
Saving new entity de.scrum_master.app.Some2@28a418fc
Doing something in MoreAbstractService2<Some2>
Additional method in MoreAbstractService2<Some2>

----------------------------------------

-> Pre-save entity validation: de.scrum_master.app.Some3@5305068a
Saving new entity de.scrum_master.app.Some3@5305068a
Doing something in MoreAbstractService3<Some3>
Additional method in MoreAbstractService3<Some3>

----------------------------------------

Saving new entity de.scrum_master.app.Some2@1f32e575
Additional method in AbstractService<Some2>

----------------------------------------

Additional method in SharedInterface

----------------------------------------

Et voilà - the aspect does exactly what you asked for, as far as I understand your requirement. :-) More specifically, it does not get triggered in the third case when saveNew is being called, but the class does not implement the interface.

kriegaex
  • 63,017
  • 15
  • 111
  • 202
  • Really appreciate your review of my code.The use of `isAssignableFrom()` was required as the interfaces proxied by the AOP proxy did not include `SharedInterface` and I was unable to narrow down the pointcut expression as mentioned in the question. – R.G Apr 09 '20 at 03:35