第一次使用策略模式是一年前的一个项目:
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(); } }
标签:businessIdent,return,String,再谈,strategy,Java,key,import,Springboot From: https://www.cnblogs.com/mindzone/p/17459060.html