2

We have a situation where we want to apply the strategy pattern based on method level. So we want an interface or abstract class which has methods, and based on your security role you are allowed to execute a different implementation.

We use Spring AOP annotations, to execute the functionality to determine which class/functionality should be used:

Annotation Class

    /**
 * Annotation placed upon a method from a "default" class.
 * This default class can be seen as an abstract class which returns default values.
 * This default class has multiple "siblings" or implementations, based on the product-company in the token.
 * These implementations extend the default class with all its methods and give it its own function-
 * ality, similar like the strategy pattern: https://sourcemaking.com/design_patterns/strategy
 *
 * EG:              TestClass
 *              /       |       \
 *   AIPTestClass  AIITestClass  AIFTestClass
 *
 * REQUIREMENTS:
 * 1) Have a default class with a base name, eg: TestClass
 * 2) For each possible implementation foresee an implementation, eg: AIPTestClass
 * 3) Add the annotation to the default methods which should have a product-company bound implementation
 * 4) Annotation should be placed in  @Component based classes (or service, controller, ...)
 *
 * Check ProductCompanyBoundImplSelectionInterceptor for how this is handled
 */
@Inherited
@Target({METHOD})
@Retention(RUNTIME)
public @interface ProductCompanyImplSelection {

}

Base Class to derive the standard methods from

@Component
public class StrategyPattern {

    @ProductCompanyImplSelection
    public String executeMethod(TestObject value, int primitive, String valueString) {
        return null;
    }

}

Multiple "Strategy Implementation" Classes

@Component
public class AIPStrategyPattern {

    public String executeMethod(TestObject value, int primitive, String valueString) {
        return "AIP";
    }

}

@Component
public class AIFStrategyPattern {

    public String executeMethod(TestObject value, int primitive, String valueString) {
        return "AIF";
    }

}

Interceptor to define which implementation to be used

@Aspect @Slf4j public class ProductCompanyBoundImplSelectionInterceptor implements MethodInterceptor {

private final ApplicationContext applicationContext;

public ProductCompanyBoundImplSelectionInterceptor(ApplicationContext applicationContext) {
    this.applicationContext = applicationContext;
}

@Override
public Object invoke(MethodInvocation methodInvocation) throws Throwable {
    String productCompany = getDivision();

    //Class invocation
    Object executionClass = methodInvocation.getThis();
    Object productCompanySpecificInstance;
    Class<?> productCompanySpecificClass;
    try {
        productCompanySpecificInstance = applicationContext.getBean(
                productCompany + executionClass.getClass().getSimpleName());
        productCompanySpecificClass = productCompanySpecificInstance.getClass();
    } catch (Exception e) {
        throw new ProductCompanySelectionClassMissingException(
                "No class implementation found for class " + executionClass.getClass()
                        .getSimpleName() + " and productcompany " + productCompany);
    }
    //method invocation
    String methodName = methodInvocation.getMethod().getName();
    Class<?>[] paramClasses =
            new Class<?>[methodInvocation.getMethod().getParameterTypes().length];
    for (int paramIndex = 0; paramIndex < methodInvocation.getMethod()
            .getParameterTypes().length; paramIndex++) {
        Class<?> parameterType = methodInvocation.getMethod().getParameterTypes()[paramIndex];
        if (parameterType.isPrimitive()) {
            paramClasses[paramIndex] = parameterType;
        } else {
            Class<?> paramClass = Class.forName(parameterType.getName());
            paramClasses[paramIndex] = paramClass;
        }
    }
    Method productCompanySpecificMethod =
            productCompanySpecificClass.getMethod(methodName, paramClasses);
    return productCompanySpecificMethod.invoke(productCompanySpecificInstance,
            methodInvocation.getArguments());

}

private String getDivision() {
    UsernamePasswordAuthenticationToken authentication =
            (UsernamePasswordAuthenticationToken) SecurityContextHolder.getContext()
                    .getAuthentication();
    AuthenticationDetails details = (AuthenticationDetails) authentication.getDetails();

    String getDivision = details.getDivision();
    return getDivision;
}

}

Config

@Configuration
@EnableAspectJAutoProxy
@ComponentScan("com.stackoverflowmvce.strategypatternaop.*")
public class SpringSecurityAOPConfig {

    @Bean
    public Advisor productCompanyBoundImplSelectionAdvisor(ApplicationContext applicationContext) {
        AspectJExpressionPointcut pointcut = new AspectJExpressionPointcut();
        pointcut.setExpression(
                "@annotation(com.stackoverflowmvce.strategypatternaop.annotations.ProductCompanyImplSelection)");
        DefaultPointcutAdvisor pointcutAdvisor =
                new DefaultPointcutAdvisor(pointcut, new ProductCompanyBoundImplSelectionInterceptor(
                        applicationContext));
        return pointcutAdvisor;
    }

}

Test Classes @SpringBootTest class StrategyPatternAopApplicationTests {

@Autowired
private StrategyPattern baseStrategyPattern;

@Test
void whenDivisionAIP_returnAIPResult() {
    this.assertDivisionStategyIsOk("AIP");
}

@Test
void whenDivisionAIF_returnAIFResult() {
    this.assertDivisionStategyIsOk("AIF");
}

@Test
void whenDivisionAII_notFound_returnException() {
    Assertions.assertThrows(ProductCompanySelectionClassMissingException.class, () -> {
        this.assertDivisionStategyIsOk("AII");
    });

}

private void assertDivisionStategyIsOk(String division) {
    this.setupSecurityContext(division);
    String strategyResult =
            this.baseStrategyPattern.executeMethod(new TestObject("test"), 0, "TEST");
    assertThat(division).isEqualTo(strategyResult);

}

private void setupSecurityContext(String division) {
    SecurityContext context = SecurityContextHolder.getContext();
    UsernamePasswordAuthenticationToken authentication =
            new UsernamePasswordAuthenticationToken("oid", null);
    AuthenticationDetails authenticationDetails = new AuthenticationDetails(division);
    authentication.setDetails(authenticationDetails);
    context.setAuthentication(authentication);
    SecurityContextHolder.getContext().setAuthentication(authentication);
}

}

So what do we want: to replace StrategyPattern class with an Interface or Abstract Class. Now we use a default class which does nothing, which is ugly.

So any suggestions how we do this, because the annotations only work with methods which should be executed.

EDIT 22/10/2021

Changed code to work with Spring @Component auto-detection and ApplicationContext

For an MVCE, as suggested by kriegaex, clone following github repo: https://github.com/nvanhoeck/strategy-pattern-aop.git

The tests are successful, what we want is that StrategyPattern class becomes an interface and still make this work.

kriegaex
  • 63,017
  • 15
  • 111
  • 202
Niko
  • 75
  • 5
  • Your use case is interesting and the code instructive, but difficult to reason about without having an [MCVE](https://stackoverflow.com/help/mcve), ideally on GitHub, to execute and see everything in action, because readers need to parse your code in theirs heads including reflective calls etc. BTW, why are you using a method interceptor instead of a normal Spring AOP aspect? – kriegaex Oct 17 '21 at 10:18
  • I am also unsure what your problem is. You did not clearly describe it. What do you expect to happen and what happens instead? Is your problem that you annotate base class or interface methods and expect the annotations to be inherited by subclasses? Because that would not work due to being a JVM limitation, see [my explanation and native AspectJ workaround here](https://stackoverflow.com/a/42607016/1082681). To me, your approach looks complicated, over-engineered and like an instance on the [XY problem](https://meta.stackexchange.com/a/66378/309898). – kriegaex Oct 17 '21 at 10:22
  • @kriegaex It's hard to share lots of code, due to company policy. What I can do is add some tests (will follow later). The problem is, we use now TestClass as a base class to derive strategy implementations from. But TestClass would be an empty class doing nothing. The better approach, IMO, would be to derive from an interface or abstract class. – Niko Oct 18 '21 at 09:27
  • Did you read the article about MCVE which I linked to? I do not need your original project, I need a minimal version of it reproducing the problem. So your excuse about sharing code and company policy is simply invalid. Condense your use case into a minimal version of it which clearly reproduces the problem. We are talking about your design pattern here, not about confidential code. Besides, you still did not explain what the actual problem is, at least not in a comprehensible way. – kriegaex Oct 18 '21 at 09:31
  • The description of what this code does, can be found in the comments on ProductCompanyImplSelection. The issue is TestClass is just an empty class that does and returns nothing. But I want it to be an interface or an abstract class. Is this possible with Spring AOP / Advisors? You can compare it with Service annotation. Where you have an service interface, but can use multiple Service implementations for it. The issue here, at runtime Spring complains to have found multiple beans for that interface. We, with this annotation, do this selection on our own based on a security value. – Niko Oct 18 '21 at 09:44
  • Yes, I read it. It is not about annotation inheritence, but the annotation triggering some functionality (as stated above), hoping that when calling a method on an interface, this functionality gets executed. – Niko Oct 18 '21 at 11:06
  • An annotation never triggers anything, it just sits there, waiting to be read. So if you wish for an aspect or method interceptor to even know about a method annotation on an overridden parent method or implemented interface method (the JVM doesn't), you need to either use my native AspectJ solution with ITD in order to quasi inherit the annotation, or you use Spring's annotation utilities in order to discover the annotation dynamically. But then, the aspect or interceptor will always be executed (inefficient) and you manually have to check if the target method should be handles specially. – kriegaex Oct 18 '21 at 13:42
  • Hmm, yeah that's a bit what I was afraid of – Niko Oct 20 '21 at 08:54
  • No need to be afraid. I can help you. I just need you to contribute the MCVE as a precondition, so I have something to work with. It will also be beneficial for you to extract and condense your problem into an MCVE in order to be able to play with it in isolation. This is what good programmers do in order to maximise learning and solve problems. – kriegaex Oct 20 '21 at 09:41
  • @kriegaex I'll try to do it this weekend. Some changes have been added, but I'll update this whole post this weekend. – Niko Oct 21 '21 at 13:09
  • @kriegaex provided MVCE with github repo. – Niko Oct 22 '21 at 15:48

1 Answers1

1

As discussed in this GitHub issue, I created not one but three alternative solutions for you, pushed to distinct branches of my repository fork:

  1. using two marker annotations for strategy methods and default implementation classes + a basic interface implemented by the actual strategies
  2. using two marker annotations for strategy methods and default implementation classes + a base class extended by the actual strategies
  3. using one marker annotation for strategy methods + a basic interface implemented by the actual strategies, matching the default implementation by class name prefix. Even though we save one marker annotation here compared to solution #1, this comes at the cost of being less efficient (more proxies, more aspect executions, dynamic cass name filtering).

Each solution only uses Spring AOP, i.e. there is no need to use native AspectJ. This comes at a performance cost, but works. The GitHub issue links to the 3 branches implementing each of the solutions mentioned above.

kriegaex
  • 63,017
  • 15
  • 111
  • 202