【Java】再谈Springboot 策略模式
第一次使用策略模式是一年前的一个项目:
1 | https: //www.cnblogs.com/mindzone/p/16046538.html |
当时还不知道Spring支持集合类型的自动装配
在最近一个项目,我发现很多业务需要频繁的使用这种模式去聚合代码
一、牛刀小试
这是最开始的定义策略的业务接口
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | /** * 业务推送管理规范 * @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时,直接用装配注解完成依赖注入:
1 2 | @Autowired private List<PushManageService> pushManageServices; |
区分方法:
这里我直接对List集合进行一个stream过滤,用标识方法和入参值进行匹配来查找bean
也是策略模式的关键逻辑,如果匹配不到bean,则说明不存在,直接断言异常抛出
1 2 3 4 5 6 7 8 9 10 11 12 | /** * @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, 并处理逻辑
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 | /** * @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的接口后,再对我的推送规范也进行一个实现:
1 2 3 4 5 6 7 8 9 10 | /** * 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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 | @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站刷视频时也有求教:
1 | https: //www.bilibili.com/video/BV1xX4y1a7Sr |
up主的解答给我提供了一些思路...
三、处理方案:
问题的根源是Spring没有准确的自动装配Bean集合
那解决思路有两种:
1、那我一开始就过滤掉,没有乱七八糟的bean混进来就解决了
2、我没法过滤掉,我的策略匹配是通过bean的方法才知晓,那我可以通过其他方法调用来完成策略匹配?
第一个解法思路是使用@Qualifier注解进行标记
参考掘金文章:
1 | https: //juejin.cn/post/6959759591835959326 |
@Qualifier可以搭配@Autowired装配时,指定bean名称来决定到底注入哪一个Bean,但这只是其中一个用法
第二个用法是可以在标记为注册的Bean时,再打一个@Qualifier,再注入集合类型时,对集合也标记@Qualifier,Spring将只会注入标记了@Qualifier的bean
@Qualifier也支持在自定义注解中注解,是不是可以写自定义注解交给Spring识别呢?(暂未尝试)
第二个解法思路是采用注解标记完成策略匹配:
参考掘金文章:
我发现通过注解解析是可以绕过方法调用的,这样可以不用调用方法触发mybatis的绑定异常了
1 | https: //juejin.cn/post/7035414939657306126#comment |
然后注解这种方式可以方便业务扩展
比起第一个解法的灵活度更大,这里我采用的是第二种解法
四、注解解析实现
先写一个策略注解:
该注解只标记在类上
1 2 3 4 5 6 7 8 9 10 11 12 | 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(); } |
然后实现类标记
注解的解析方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | 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接口可以改写成这样了:
1 2 3 4 5 6 7 8 9 10 11 12 | /** * 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(); } |
五、工具封装
再回顾 掘金这篇文章:
1 | https: //juejin.cn/post/7035414939657306126#comment |
1、可以先把注入的List集合注入进来转换为Map,每次调用时通过map调用处理
2、注解类型可以不限定,获取策略标记的方法也是不限定的
3、注解支持的常量标记有String和枚举这两种,其他类型的意义不大
于是我再通过方法引用的方式,加上泛型抽象化,简单写了一个策略工具类:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 | 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就可以这样编写了:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 | 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 也具备策略模式的能力,但是不够灵活
1 | https: //www.jianshu.com/p/cedfae10e2ea |
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· SQL Server 2025 AI相关能力初探
· 单线程的Redis速度为什么快?
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 开源Multi-agent AI智能体框架aevatar.ai,欢迎大家贡献代码
2020-06-05 【UEditor】富文本编辑器 简单上手