【Java】再谈Springboot 策略模式
第一次使用策略模式是一年前的一个项目:
https://www.cnblogs.com/mindzone/p/16046538.html
当时还不知道Spring支持集合类型的自动装配
在最近一个项目,我发现很多业务需要频繁的使用这种模式去聚合代码
一、牛刀小试
这是最开始的定义策略的业务接口
/**
* 业务推送管理规范
* @author oncloud9
* @version 1.0
* @project amerp-server
* @date 2023年03月11日 15:16
*/
public interface PushManageService {
/* 业务标识 */
String businessIdent();
/* 翻页数据 */
IPage<? extends Object> getPushDataPage(String json);
/* 推送数据 */
Map<String, Object> pushData(Map<String, Object> pushParam, KingdeeApiSettings settings);
}
每个业务的实现,businessIdent方法返回的标识唯一,以此来获取具体的业务推送Bean
装配到集中处理的Bean时,直接用装配注解完成依赖注入:
@Autowired private List<PushManageService> pushManageServices;
区分方法:
这里我直接对List集合进行一个stream过滤,用标识方法和入参值进行匹配来查找bean
也是策略模式的关键逻辑,如果匹配不到bean,则说明不存在,直接断言异常抛出
/**
* @author oncloud9
* @date 2023/3/11 15:47
* @description 通过Spring类型集中注入推送的服务对象,根据设置的业务标识获取对应实例
* @params [businessIdent]
* @return cn.hyite.amerp.system.push.manage.service.PushManageService
*/
private PushManageService getSpecificInstance(final String businessIdent) {
PushManageService pushManageService = pushManageServices.stream().filter(pm -> pm.businessIdent().equals(businessIdent)).findFirst().orElse(null);
Assert.isTrue(Objects.isNull(pushManageService), ResultMessage.CUSTOM_ERROR, "没有这个业务的推送管理Bean! [" + businessIdent + "]");
return pushManageService;
}
对接Controller, 前端传递标识信息,以及翻页的数据:
经过策略翻找,返回对应该业务的实现bean, 并处理逻辑
/**
* @author oncloud9
* @date 2023/3/11 15:45
* @description 推送记录翻页查询
* @params [businessIdent, json]
* @return com.baomidou.mybatisplus.core.metadata.IPage<? extends java.lang.Object>
*/
@PostMapping("/{businessIdent}/page")
public PageResult<?> getPushDataPage(@PathVariable("businessIdent") final String businessIdent, @RequestBody final String json) {
/* 推送业务的服务实例是否存在 */
final PushManageService specificInstance = getSpecificInstance(businessIdent);
return PageResult.toPageResult(specificInstance.getPushDataPage(json));
}
/**
* @author oncloud9
* @date 2023/3/11 15:45
* @description 推送
* @params [businessIdent, param]
* @return void
*/
@PostMapping("/{businessIdent}/push")
public Map<String, Object> pushData(@PathVariable("businessIdent") final String businessIdent, @RequestBody Map<String, Object> param) {
/* 拷贝现有的配置Bean,原有账号改为前端传入 */
final KingdeeApiSettings apiSetting = BeanUtil.copyProperties(this.kingdeeApiSettings, KingdeeApiSettings.class);
apiSetting.setUserName(param.get("username").toString());
apiSetting.setPassWord(param.get("password").toString());
/* 登陆校验检查 */
boolean loginFlag = KingdeeHelper.login(apiSetting);
Assert.isFalse(loginFlag, ResultMessage.CUSTOM_ERROR, "金蝶系统登录失败,请检查账号密码是否正确");
/* 推送业务的服务实例是否存在 */
final PushManageService specificInstance = getSpecificInstance(businessIdent);
Assert.isTrue(Objects.isNull(specificInstance), ResultMessage.NOT_FOUNT_ERROR, businessIdent);
/* 开始推送 */
PushManageService instance = getSpecificInstance(businessIdent);
return instance.pushData(param, apiSetting);
}
二、问题暴露
接口是很好扩展的,一个普通的类,可以实现若干个接口
我们有各种各样的业务策略,可以同时在一个业务实现类中实现这些策略的内容
像下面这样,实现了MybatisPlus的接口后,再对我的推送规范也进行一个实现:
/**
* fin_ex_apply 报销申请表 服务实现类
*
* @author oncloud9
* @version 1.0
* @project
* @date 2022-10-15
*/
@Service("finExApplyService")
public class FinExApplyServiceImpl extends BaseService<FinExApplyDAO, FinExApplyDTO> implements IFinExApplyService, PushManageService
但是在这个接口实现中,我的接口被Mybatis的MapperProxyFactory标记为规范,也注入进来了

我改写一下该策略的Controller:
调用时按照原来的匹配逻辑查找,提供一个找不到的key
@Slf4j
@RestController
@RequestMapping("/strategy")
public class StrategyController {
private static Map<String, TestStrategy> strategyMap;
private static List<TestStrategy> strategyList;
/**
* qualifier用法 https://juejin.cn/post/6959759591835959326
* @param strategyList
*/
public StrategyController(List<TestStrategy> strategyList) {
StrategyController.strategyList = strategyList;
StrategyController.strategyMap = StrategyUtil.getStrategyMap(strategyList, ServiceFlag.class, ServiceFlag::flagName);
}
/**
* strategy/exec
* @param key Bean标识
* @return String
*/
@GetMapping("/exec")
public String executeStrategy(@RequestParam("key") String key) {
log.info("strategyMap {}", strategyMap);
// TestStrategy strategy = strategyMap.get(key);
// if (Objects.isNull(strategy)) throw new ServiceException("未能查找到此策略Bean! flag:" + key);
// TestStrategy strategy = StrategyUtil.getStrategyByKey(strategyMap, key, "未能查找到此策略Bean! flag");
// return strategy.strategyMethod();
return strategyList.stream().filter(x -> x.ident().equals(key)).findAny().get().strategyMethod();
}
}
这时就会发现,不是我们断言的异常,而是mybatis的mapper绑定失败异常:

其原理尚未能深究...
我个人的理解是,实现bean跳转到MybatisMapperProxy时调用ident方法,被Proxy对象理解为mapper方法调用
从而查找对应的实现,然而并没有对应实现...
在B站刷视频时也有求教:
https://www.bilibili.com/video/BV1xX4y1a7Sr

up主的解答给我提供了一些思路...
三、处理方案:
问题的根源是Spring没有准确的自动装配Bean集合
那解决思路有两种:
1、那我一开始就过滤掉,没有乱七八糟的bean混进来就解决了
2、我没法过滤掉,我的策略匹配是通过bean的方法才知晓,那我可以通过其他方法调用来完成策略匹配?
第一个解法思路是使用@Qualifier注解进行标记
参考掘金文章:
https://juejin.cn/post/6959759591835959326
@Qualifier可以搭配@Autowired装配时,指定bean名称来决定到底注入哪一个Bean,但这只是其中一个用法
第二个用法是可以在标记为注册的Bean时,再打一个@Qualifier,再注入集合类型时,对集合也标记@Qualifier,Spring将只会注入标记了@Qualifier的bean
@Qualifier也支持在自定义注解中注解,是不是可以写自定义注解交给Spring识别呢?(暂未尝试)
第二个解法思路是采用注解标记完成策略匹配:
参考掘金文章:
我发现通过注解解析是可以绕过方法调用的,这样可以不用调用方法触发mybatis的绑定异常了
https://juejin.cn/post/7035414939657306126#comment
然后注解这种方式可以方便业务扩展
比起第一个解法的灵活度更大,这里我采用的是第二种解法
四、注解解析实现
先写一个策略注解:
该注解只标记在类上
package cn.cloud9.server.test.strategy;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface StrategyFlag {
String flag();
}
然后实现类标记

注解的解析方法
private boolean flagMatch(Object target, String key) {
// 获取目标bean的字节对象
Class<?> targetClass = target.getClass();
// 在字节对象中可以获取到注解信息
StrategyFlag strategyFlag = targetClass.getAnnotation(StrategyFlag.class);
// 有可能目标对象是Spring的CgLib增强的代理对象, 那实际对象在上一层父类
if (Objects.isNull(strategyFlag)) {
// 取得父类再次获取注解
Class<?> superclass = targetClass.getSuperclass();
strategyFlag = superclass.getAnnotation(StrategyFlag.class);
}
// 如果父类和当前类都没有,可以确定没有注解了
if (Objects.isNull(strategyFlag)) return false;
// 提取注解上的标识记录 进行匹配
String flag = strategyFlag.flag();
return flag.equals(key);
}
现在这个Controller接口可以改写成这样了:
/**
* strategy/exec
* @param key Bean标识
* @return String
*/
@GetMapping("/exec")
public String executeStrategy(@RequestParam("key") String key) {
log.info("strategyMap {}", strategyMap);
Optional<TestStrategy> any = strategyList.stream().filter(x -> flagMatch(x, StrategyFlag.class)).findAny();
return any.get().strategyMethod();
}
五、工具封装
再回顾 掘金这篇文章:
https://juejin.cn/post/7035414939657306126#comment
1、可以先把注入的List集合注入进来转换为Map,每次调用时通过map调用处理
2、注解类型可以不限定,获取策略标记的方法也是不限定的
3、注解支持的常量标记有String和枚举这两种,其他类型的意义不大
于是我再通过方法引用的方式,加上泛型抽象化,简单写了一个策略工具类:
package cn.cloud9.server.test.strategy;
import cn.cloud9.server.struct.exception.ServiceException;
import java.lang.annotation.Annotation;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.function.Function;
import java.util.stream.Collectors;
/**
* 策略工具类
* 按注解来区分
*
* 参考文档实现:
* https://juejin.cn/post/7035414939657306126#comment
*/
public class StrategyUtil {
/**
* 获取策略Map
* @param interfaceList
* @param annotationTypeClass
* @param annotationFunction
* @param <Interface>
* @param <AnnotationType>
* @return
*/
public static <Interface, AnnotationType extends Annotation, FlagType>
Map<FlagType, Interface> getStrategyMap(
final List<Interface> interfaceList,
final Class<AnnotationType> annotationTypeClass,
final Function<AnnotationType, FlagType> annotationFunction
) {
return interfaceList.stream().filter(x -> flagFilter(x, annotationTypeClass)).collect(Collectors.toMap(
x -> identGet(x, annotationTypeClass, annotationFunction),
x -> x
));
}
private static <Type extends Annotation> boolean flagFilter(Object target, Class<Type> typeClass) {
Class<?> targetClass = target.getClass();
Type type = targetClass.getAnnotation(typeClass);
if (Objects.isNull(type)) {
Class<?> superclass = targetClass.getSuperclass();
type = superclass.getAnnotation(typeClass);
return Objects.nonNull(type);
}
return true;
}
private static <AnnotationType extends Annotation, FlagType> FlagType identGet(
Object obj,
Class<AnnotationType> annotationClass,
Function<AnnotationType, FlagType> function
) {
Class<?> aClass = obj.getClass();
AnnotationType annotation = aClass.getAnnotation(annotationClass);
if (Objects.isNull(annotation)) annotation = aClass.getSuperclass().getAnnotation(annotationClass);
return function.apply(annotation);
}
public static <Interface> Interface getStrategyByKey(Map<String, Interface> strategyMap, String key, String exceptionMessage) {
Interface anInterface = strategyMap.get(key);
if (Objects.isNull(anInterface)) throw new ServiceException(exceptionMessage + key);
return anInterface;
}
}
最终策略Controller就可以这样编写了:
package cn.cloud9.server.test.controller;
import cn.cloud9.server.test.strategy.ServiceFlag;
import cn.cloud9.server.test.strategy.StrategyUtil;
import cn.cloud9.server.test.strategy.TestStrategy;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
import java.util.Map;
@Slf4j
@RestController
@RequestMapping("/strategy")
public class StrategyController {
private static Map<String, TestStrategy> strategyMap;
/**
* qualifier用法 https://juejin.cn/post/6959759591835959326
* @param strategyList
*/
public StrategyController(@Qualifier List<TestStrategy> strategyList) {
strategyMap = StrategyUtil.getStrategyMap(strategyList, ServiceFlag.class, ServiceFlag::flagName);
}
/**
* strategy/exec
* @param key Bean标识
* @return String
*/
@GetMapping("/exec")
public String executeStrategy(@RequestParam("key") String key) {
log.info("strategyMap {}", strategyMap);
TestStrategy strategy = StrategyUtil.getStrategyByKey(strategyMap, key, "未能查找到此策略Bean! flag");
return strategy.strategyMethod();
}
}
2023年07月02日,更新:
ServiceLocatorFactoryBean 也具备策略模式的能力,但是不够灵活
https://www.jianshu.com/p/cedfae10e2ea

浙公网安备 33010602011771号