带有方法拦截器的基于接口的策略模式

Interface-based strategy pattern with method interceptors

我们有一种情况想要应用基于方法级别的策略模式。因此,我们需要一个具有方法的接口或抽象 class,并且根据您的安全角色,您可以执行不同的实现。

我们使用 Spring AOP 注释来执行确定应该使用哪个 class/functionality 的功能:

注解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 从

派生标准方法
@Component
public class StrategyPattern {

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

}

多个“策略实施”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";
    }

}

定义使用哪个实现的拦截器

@看点 @Slf4j public class ProductCompanyBoundImplSelectionInterceptor 实现 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;
}

}

配置

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

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

}

测试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);
}

}

那么我们想要什么:用接口或抽象 Class 替换 StrategyPattern class。现在我们使用默认的 class 什么都不做,这很丑。

所以任何关于我们如何做到这一点的建议,因为注释只适用于应该执行的方法。

编辑 22/10/2021

更改代码以使用 Spring @Component 自动检测和 ApplicationContext

对于 MVCE,按照 kriegaex 的建议,克隆以下 github 存储库: https://github.com/nvanhoeck/strategy-pattern-aop.git

测试成功,我们想要的是 StrategyPattern class 成为一个接口,并且仍然可以正常工作。

this GitHub issue 中所述,我为您创建了 三个备选解决方案 ,并推送到我的存储库分叉的不同分支:

  1. 为策略方法和默认实现使用两个标记注释classes + 由实际策略实现的基本接口
  2. 为策略方法和默认实现使用两个标记注释 classes + 由实际策略扩展的基础 class
  3. 对策略方法使用一个标记注释+实际策略实现的基本接口,通过class名称前缀匹配默认实现。尽管与解决方案 #1 相比,我们在这里保存了一个标记注释,但这是以效率较低为代价的(更多代理、更多方面执行、动态 cass 名称过滤)。

每个解决方案只使用Spring AOP,即不需要使用原生的AspectJ。这是以性能成本为代价的,但是有效。 GitHub 问题链接到实现上述每个解决方案的 3 个分支。