带有方法拦截器的基于接口的策略模式
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 中所述,我为您创建了 三个备选解决方案 ,并推送到我的存储库分叉的不同分支:
- 为策略方法和默认实现使用两个标记注释classes + 由实际策略实现的基本接口
- 为策略方法和默认实现使用两个标记注释 classes + 由实际策略扩展的基础 class
- 对策略方法使用一个标记注释+实际策略实现的基本接口,通过class名称前缀匹配默认实现。尽管与解决方案 #1 相比,我们在这里保存了一个标记注释,但这是以效率较低为代价的(更多代理、更多方面执行、动态 cass 名称过滤)。
每个解决方案只使用Spring AOP,即不需要使用原生的AspectJ。这是以性能成本为代价的,但是有效。 GitHub 问题链接到实现上述每个解决方案的 3 个分支。
我们有一种情况想要应用基于方法级别的策略模式。因此,我们需要一个具有方法的接口或抽象 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 中所述,我为您创建了 三个备选解决方案 ,并推送到我的存储库分叉的不同分支:
- 为策略方法和默认实现使用两个标记注释classes + 由实际策略实现的基本接口
- 为策略方法和默认实现使用两个标记注释 classes + 由实际策略扩展的基础 class
- 对策略方法使用一个标记注释+实际策略实现的基本接口,通过class名称前缀匹配默认实现。尽管与解决方案 #1 相比,我们在这里保存了一个标记注释,但这是以效率较低为代价的(更多代理、更多方面执行、动态 cass 名称过滤)。
每个解决方案只使用Spring AOP,即不需要使用原生的AspectJ。这是以性能成本为代价的,但是有效。 GitHub 问题链接到实现上述每个解决方案的 3 个分支。