Lachezar Balev
Lachezar Balev

Reputation: 12021

Allow method execution only if the caller is of certain type

We have an XRepository which extends JpaRepository. Since deleting X entities got pretty ad-hoc recently we created an XDeletionService which contains lots of... stuff :-) and uses the XRepository.

Now we have an interesting idea to forbid the execution of any delete methods in the XRepository unless these are called from within the XDeletionService. E.g. if a colleague calls directly by mistake XRepository.delete(..) from within TheirService it will throw an exception.

We still cannot find an elegant solution for this idea. What we did so far is to create an aspect with a pointcut expression which matches the delete methods of the repository. This aspect throws an exception by consulting the stacktrace, e.g.:

boolean calledFromXDeletionService = Arrays.stream(Thread.currentThread().getStackTrace())
        .anyMatch(stackTraceElement ->
            XDeletionService.class.getName().equals(stackTraceElement.getClassName())); 
if (!calledFromXDeletionService)
   throw new ....

This seems pretty ugly though. Do you have a better idea how to implement this "feature"?

Upvotes: 1

Views: 708

Answers (2)

kriegaex
kriegaex

Reputation: 67297

I suggest to switch from Spring AOP to AspectJ, which can be used with or completely without Spring. I am going to post a stand-alone Java example, but the Spring manual also explains how you can configure AspectJ LTW (load-time weaving) for Spring.

Sample classes + driver application:

package de.scrum_master.app;

public class NormalType {
  public void callTargetMethod(Application application) {
    System.out.println("Normal caller");
    application.doSomething();
  }
}
package de.scrum_master.app;

public class SpecialType {
  public void callTargetMethod(Application application) {
    System.out.println("Special caller");
    application.doSomething();
  }
}
package de.scrum_master.app;

public class Application {
  public static void main(String[] args) {
    Application application = new Application();
    application.callTargetMethod(application);
    callTargetMethodStatic(application);
    new NormalType().callTargetMethod(application);
    new SpecialType().callTargetMethod(application);
  }

  public void callTargetMethod(Application application) {
    System.out.println("Normal caller");
    application.doSomething();
  }

  public static void callTargetMethodStatic(Application application) {
    System.out.println("Static caller");
    application.doSomething();
  }

  public void doSomething() {
    System.out.println("Doing something");
  }
}

The expectation is that when running the little driver application, only the call to Application.doSomething() issued from within the instance method SpecialType.callTargetMethod(..) will actually be intercepted, not calls from other classes' instance methods and also not calls from static methods (which can be intercepted in AspectJ in contrast to Spring AOP).

The solution is to use a call() pointcut which is a kind of counterpart to execution() and unavailable in Spring AOP. It intercepts a method call inside the caller class, not the corresponding execution in the callee. This is what we want because then we can use this() in order to determine or narrow down the caller class.

Only for call() there is a difference between the values of this() (caller) and target() (callee). For execution() both values are the same, which is why Spring AOP cannot be used for this purpose without resorting to stack trace inspection or more elegantly and efficiently the Stack Walking API (https://www.baeldung.com/java-9-stackwalking-api) in Java 9+.

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 MyAspect {
  @Before(
    "call(void de.scrum_master.app.Application.doSomething()) && " +
    "this(de.scrum_master.app.SpecialType)"
  )
  public void myAdvice(JoinPoint joinPoint) {
    System.out.println(joinPoint);
  }
}

Console log:

Normal caller
Doing something
Static caller
Doing something
Normal caller
Doing something
Special caller
call(void de.scrum_master.app.Application.doSomething())
Doing something

If you also want to log the caller instance, you can modify the aspect like this, binding it to an advice method parameter:

package de.scrum_master.aspect;

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

import de.scrum_master.app.SpecialType;

@Aspect
public class MyAspect {
  @Before(
    "call(void de.scrum_master.app.Application.doSomething()) && " +
    "this(specialType)"
  )
  public void myAdvice(JoinPoint joinPoint, SpecialType specialType) {
    System.out.println(joinPoint + " -> " + specialType);
  }
}

The console log would then be:

Normal caller
Doing something
Static caller
Doing something
Normal caller
Doing something
Special caller
call(void de.scrum_master.app.Application.doSomething()) -> de.scrum_master.app.SpecialType@402a079c
Doing something

Update: You may also want to experiment with adding a JoinPoint.EnclosingStaticPart enclosingStaticPart parameter to your advice, then print and/or inspect it. It helps you find out more information about the caller for call() pointcuts without having to resort to stack traces or stack walking API.

Upvotes: 1

Edgar Domingues
Edgar Domingues

Reputation: 990

The TheirService should use an Interface that doesn't have the delete methods, this is called the Interface Segregation Principle.

Upvotes: 0

Related Questions