【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
【Java】再谈Springboot 策略模式的更多相关文章
- Java设计模式6:策略模式
策略模式 策略模式的用意是针对一组算法,将每一个算法封装到具有共同接口的独立类中,从而使得它们可以相互替换.策略模式使得算法可以在不影响到客户端的情况下发生变化. 策略模式的结构 策略模式是对算法的包 ...
- Java的设计模式----strategy(策略模式)
设计模式: 一个程序员对设计模式的理解: “不懂”为什么要把很简单的东西搞得那么复杂.后来随着软件开发经验的增加才开始明白我所看到的“复杂”恰恰就是设计模式的精髓所在,我所理解的“简单”就是一把钥匙开 ...
- Java设计模式之十一 ---- 策略模式和模板方法模式
前言 在上一篇中我们学习了行为型模式的访问者模式(Visitor Pattern)和中介者模式(Mediator Pattern).本篇则来学习下行为型模式的两个模式,策略模式(Strategy Pa ...
- java设计模式-----8、策略模式
Strategy模式也叫策略模式是行为模式之一,它对一系列的算法加以封装,为所有算法定义一个抽象的算法接口,并通过继承该抽象算法接口对所有的算法加以封装和实现,具体的算法选择交由客户端决定(策略).S ...
- 《JAVA设计模式》之策略模式(Strategy)
在阎宏博士的<JAVA与模式>一书中开头是这样描述策略(Strategy)模式的: 策略模式属于对象的行为模式.其用意是针对一组算法,将每一个算法封装到具有共同接口的独立的类中,从而使得它 ...
- Java再谈方法
1.3再谈方法 1.3.1 什么是方法(函数) ①方法是类或对象行为特征的抽象,也称为函数. ②Java里的方法不能独立存在,所有的方法必须定义在类里. 修饰符 返回值类型 方法名(参数类型 形参1, ...
- Java设计模式系列之策略模式
策略模式的定义: 策略模式定义了一系列的算法,并将每一个算法封装起来,而且使它们还可以相互替换,策略模式让算法独立于使用它的客户而独立变化. 策略模式使这些算法在客户端调用它们的时候能够互不影响地变化 ...
- Java设计模式学习记录-策略模式
策略模式 策略模式的定义是:定义了一系列的算法,把它们一个个的封装起来,并且使它们可相互替换,让算法可以独立于使用它的客户而变化. 设计原则是:把一个类中经常改变或者将来可能会经常改变的部分提取出来作 ...
- [java之设计模式]策略模式
策略模式(strategy pattern) 定义>> 将一系列的算法封装到一些列的类里面,并且可以相互替换 作用>> 将算法的变化独立于客户端,将算法的指责和算法的行为分开, ...
- JAVA设计模式之【策略模式】
策略模式 定义一些独立的类来封装不同的算法 类似于common方法或者引用类 角色 环境类Context 抽象策略Strategy 具体策略ConcreteStrategy 重构伴随着设计模式 重构类 ...
随机推荐
- Opencv笔记(12)傅里叶变换
在之前了解的OpenCV为我们实现的图像变换,这些本质上是从图像到输出图像的映射,即输入仍是一幅图像.本章的傅里叶变换,输出数组的值在含义上和原图像的强度值大不相同,是输入图像的频域表示. cv::d ...
- Nacos 源码环境搭建
最近在学习nacos,通过调式源码查看服务注册和发现流程和原理,本地部署naos源码需要一定的步骤,本文主要做nacos源码部署. nacos版本:2.1.1 下载源码 从github上下载源码到本地 ...
- 开发视频会议系统:使用GPU解码渲染视频
现在,使用视频会议系统远程协同办公.沟通交流,已经非常普遍了.如果我们要开发自己的视频会议系统,那么,GPU解码渲染技术是不可缺少的. 在视频会议系统中,经常需要同时观看会议中多个参会人员的视频图像, ...
- ES 数据太敏感不让看,怎么办?
在使用 ES 的过程中,如果 ES 集群中存放的是敏感数据,是不能够随便供人查看的.什么?在排查故障?那也不行,合规高于一切. 不知道大家有没有遇到过上面描述的情景,或者如果是你遇到了,你会怎么办呢? ...
- Java连接mySql——简单JDBC连接数据库
利用JDBC开发数据库 经典应该用框架: 第一步,加载JDBC数据库驱动程序(不同的数据库有不同的数据库驱动,所以在连接数据库之前,需加载驱动) 格式: String dri ...
- skywalking需要引入的背景(查询调用链),传统的日志查询方法, 引入EFK日志搜索重要性
1.根据两次请求日志的关键点来截取日志,缩小日志的范围.tail -f orderApi.log | grep "orderKeyWordSubmit" 确定两次异常请求的 ...
- CNN --入门MNIST识别
Smiling & Weeping ---- 下次你撑伞低头看水洼, 就会想起我说雨是神的烟花. 简介:主要是看刘二大人的视频讲解:https://www.bilibili.com/video ...
- .NET使用原生方法实现文件压缩和解压
前言 在.NET中实现文件或文件目录压缩和解压可以通过多种方式来完成,包括使用原生方法(System.IO.Compression命名空间中的类)和第三方库(如:SharpZipLib.SharpCo ...
- 为什么不推荐使用Linq?
相信很多.NETer看了标题,都会忍不住好奇,点进来看看,并且顺便准备要喷作者! 这里,首先要申明一下,作者本人也非常喜欢Linq,也在各个项目中常用Linq. 我爱Linq,Linq优雅万岁!!!( ...
- python重拾基础第一天
本节内容 Python介绍 发展史 Python 2 or 3? 安装 Hello World程序 变量 用户输入 模块初识 .pyc是个什么鬼? 数据类型初识 数据运算 表达式if ...else语 ...