第一次使用策略模式是一年前的一个项目:

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 策略模式的更多相关文章

  1. Java设计模式6:策略模式

    策略模式 策略模式的用意是针对一组算法,将每一个算法封装到具有共同接口的独立类中,从而使得它们可以相互替换.策略模式使得算法可以在不影响到客户端的情况下发生变化. 策略模式的结构 策略模式是对算法的包 ...

  2. Java的设计模式----strategy(策略模式)

    设计模式: 一个程序员对设计模式的理解: “不懂”为什么要把很简单的东西搞得那么复杂.后来随着软件开发经验的增加才开始明白我所看到的“复杂”恰恰就是设计模式的精髓所在,我所理解的“简单”就是一把钥匙开 ...

  3. Java设计模式之十一 ---- 策略模式和模板方法模式

    前言 在上一篇中我们学习了行为型模式的访问者模式(Visitor Pattern)和中介者模式(Mediator Pattern).本篇则来学习下行为型模式的两个模式,策略模式(Strategy Pa ...

  4. java设计模式-----8、策略模式

    Strategy模式也叫策略模式是行为模式之一,它对一系列的算法加以封装,为所有算法定义一个抽象的算法接口,并通过继承该抽象算法接口对所有的算法加以封装和实现,具体的算法选择交由客户端决定(策略).S ...

  5. 《JAVA设计模式》之策略模式(Strategy)

    在阎宏博士的<JAVA与模式>一书中开头是这样描述策略(Strategy)模式的: 策略模式属于对象的行为模式.其用意是针对一组算法,将每一个算法封装到具有共同接口的独立的类中,从而使得它 ...

  6. Java再谈方法

    1.3再谈方法 1.3.1 什么是方法(函数) ①方法是类或对象行为特征的抽象,也称为函数. ②Java里的方法不能独立存在,所有的方法必须定义在类里. 修饰符 返回值类型 方法名(参数类型 形参1, ...

  7. Java设计模式系列之策略模式

    策略模式的定义: 策略模式定义了一系列的算法,并将每一个算法封装起来,而且使它们还可以相互替换,策略模式让算法独立于使用它的客户而独立变化. 策略模式使这些算法在客户端调用它们的时候能够互不影响地变化 ...

  8. Java设计模式学习记录-策略模式

    策略模式 策略模式的定义是:定义了一系列的算法,把它们一个个的封装起来,并且使它们可相互替换,让算法可以独立于使用它的客户而变化. 设计原则是:把一个类中经常改变或者将来可能会经常改变的部分提取出来作 ...

  9. [java之设计模式]策略模式

    策略模式(strategy pattern) 定义>> 将一系列的算法封装到一些列的类里面,并且可以相互替换 作用>> 将算法的变化独立于客户端,将算法的指责和算法的行为分开, ...

  10. JAVA设计模式之【策略模式】

    策略模式 定义一些独立的类来封装不同的算法 类似于common方法或者引用类 角色 环境类Context 抽象策略Strategy 具体策略ConcreteStrategy 重构伴随着设计模式 重构类 ...

随机推荐

  1. Opencv笔记(13)积分图

    积分图时一种允许子区域快速求和的数据结构,这种求和在很多方面都很有用,值得一提的是haar小波的计算,它用于人脸识别和类似的算法.Opencv支持积分图的三种变体,分别是总和.平方求和以及倾斜求和.每 ...

  2. js 实现仿百度换肤效果

    图片自行换掉即可查看效果,原理就是基于tab切换的效果实现的 效果图 1 <!DOCTYPE html> 2 <html> 3 4 <head> 5 <met ...

  3. Easysearch:语义搜索、知识图和向量数据库概述

    什么是语义搜索? 语义搜索是一种使用自然语言处理算法来理解单词和短语的含义和上下文以提供更准确的搜索结果的搜索技术.旨在更好地理解用户的意图和查询内容,而不仅仅是根据关键词匹配,还通过分析查询的语义和 ...

  4. FlashDuty Changelog 2023-12-18 | 值班管理、服务日历、自定义操作和邮件集成

    FlashDuty:一站式告警响应平台,前往此地址免费体验! 值班管理 UI 交互优化 [个人日程]从头像下拉菜单调整到值班列表页面,快速查看个人值班日程 [值班列表]支持原地预览最近一周值班情况,包 ...

  5. SpringMVC 工作原理?

    a.客户端发送请求到 DispatcherServlet b.DispatcherServlet 查询 handlerMapping 找到处理请求的 Controller c.Controller 调 ...

  6. StringRedisHelper

    @Slf4j @Service public class StringRedisHelper { private StringRedisTemplate redisTemplate; @Autowir ...

  7. Javascript高级程序设计第一章 | ch1 | 阅读笔记

    什么是JavaScript 历史回顾 JavaScript实现 完整的JavaScript实现包括 核心 ECMAScript -> 语法.类型.关键字.保留字...(规范) 文档对象模型 DO ...

  8. 第三届机器人、人工智能与信息工程国际学术会议(RAIIE 2024)

    [ACM独立出版/Fellow大咖云集]2024年第二届机器人.人工智能与信息工程国际学术会议(RAIIE 2024) 2024 3rd International Symposium on Robo ...

  9. Thread的join方法demo

    Thread的join方法demo /** * 关于join官方的解释是 Waits for this thread to die. 也就是等待一个线程结束. */ public class Thre ...

  10. Bind DNS服务——带KEY的区域传送与子域授权

    Linux基础服务--Bind DNS服务 Part3 带KEY的区域传送与子域授权 带KEY的区域传送 上文提到了区域传送,但实际上在区域传送的时,传送的区域文件并不会被加密.因此一般的区域传送并不 ...