首页 > 编程语言 >【Java】再谈Springboot 策略模式

【Java】再谈Springboot 策略模式

时间:2023-06-05 23:11:55浏览次数:46  
标签:businessIdent return String 再谈 strategy Java key import 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();
    }

}

  

 

 

 

 

 

 

 

 

 

 

 

 

 


 

标签:businessIdent,return,String,再谈,strategy,Java,key,import,Springboot
From: https://www.cnblogs.com/mindzone/p/17459060.html

相关文章

  • TypeScript Vs JavaScript 区别
    一、观察1.JS平常的复制类型letval;val=123;val="123";val=true;val=[1,3,5];注意点:由于JS是弱类型的,所以只要定义了一个变量,就可以往这个变量中存储任意类型的数据也正是因为如此,所以会给我们带来一个问题2.假设a是一个数组,b是一个数值func......
  • Elastic_Search 和java的入门结合
    1,pom文件添加依赖... 2,config配置文件  3,写接口文件 ......
  • Java如何实现去重?这是在炫技吗?
    大家好,我3y啊。由于去重逻辑重构了几次,好多股东直呼看不懂,于是我今天再安排一波对代码的解析吧。austin支持两种去重的类型:N分钟相同内容达到N次去重和一天内N次相同渠道频次去重。Java开源项目消息推送平台......
  • Java学习笔记(十五)
    1.请描述你理解的转换流 转换流(InputStreamReader和OutputStreamWriter)是字节流和字符流之间的一种桥梁,用于将字节流转换为字符流或将字符流转换为字节流。转换流可以解决字节流和字符流之间的编码转换问题,从而使得我们可以方便地在不同的字符集之间进行转换。2.请描述你理解......
  • java程序设计对文件的操作
    1文件的读入与写进importjava.io.*;publicclassMain{publicstaticvoidmain(Stringargs[]){try{FileinFile=newFile("D://example.txt");BufferedReaderbf=newBufferedReader(newFileReader(inFile));......
  • SpringBoot 文件上传下载工具样例
    最近工作遇到这样的情景:一大堆linux内网服务器,上面部署了mysql,nacos,xxljob等中间件,当然也给了一个很干净的windows内网服务器,什么软件都没有安装。比较欣慰的是:可以通过浏览器访问nacos、xxljob的管理页面。不幸的是:没有安装mysql客户端和xshell等工具。我可以通过......
  • Java中为什么禁止把SimpleDateFormat定位为static变量以及如果非要使用static定位Simp
    场景Java中ExecutorService线程池的使用(Runnable和Callable多线程实现):https://blog.csdn.net/BADAO_LIUMANG_QIZHI/article/details/126242904Java中创建线程的方式以及线程池创建的方式、推荐使用ThreadPoolExecutor以及示例:https://blog.csdn.net/BADAO_LIUMANG_QIZHI/art......
  • Java开发手册中为什么不建议在for循环中使用"+"进行字符串操作
    场景java开发手册中对于循环体中进行字符串的拼接要求如下:【推荐】循环体内,字符串的连接方式,使用StringBuilder的append方法进行扩展。说明:下例中,反编译出的字节码文件显示每次循环都会new出一个StringBuilder对象,然后进行append操作,最后通过toString方法返回Stri......
  • 记录--JavaScript 中有趣的 9 个常用编码套路
    这里给大家分享我在网上总结出来的一些知识,希望对大家有所帮助1️⃣set对象:数组快速去重常规情况下,我们想要筛选唯一值,一般会想到遍历数组然后逐个对比,或者使用成熟的库比如lodash之类的。不过,ES6带来了一个新玩意儿!它引入了一个全新的对象类型:Set!而且,如果结合上...展开运算符......
  • 详解JavaScript中的__proto__和prototype
    目录一、JS的对象创建方法二、双对象法则三、__proto__和prototypeprototype是什么?__proto__又是什么?对于JS来说,__proto__和prototype的区别是个绕不开的话题。本文就试图从它们的根本上说清楚它们是什么,又有什么区别,所以本文会从JS的对象开始说起,以其期待把本文的主题......