前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >巧用 Spring 自动注入实现策略模式升级版

巧用 Spring 自动注入实现策略模式升级版

作者头像
明明如月学长
发布2022-04-13 15:21:08
1.7K1
发布2022-04-13 15:21:08
举报
文章被收录于专栏:明明如月的技术专栏

一、前言

1.1 背景

在工作过程中,有时候需要根据不同的枚举(常量)执行不同的逻辑。

比如不同的用户类型,使用不同的优惠政策;不同的配置变化,走不同的处理逻辑等。

下面模拟一个根据不同用户类型,走不同业务逻辑的案例。

不同的用户类型有不同的处理方式,接口为 Handler ,示例代码如下:

代码语言:javascript
复制
public interface Handler {

    void someThing();
}

1.2 不同同学的做法

1.2.1 switch case 模式

小A同学,通过编写 switch 来判断当前类型,去调用对应的 Handler:

代码语言:javascript
复制
@Service
public class DemoService {

    @Autowired
    private CommonHandler commonHandler;

    @Autowired
    private VipHandler vipHandler;

    public void test(){
      String type ="Vip";
      switch (type){
          case "Vip":
              vipHandler.someThing();
              break;
          case "Common":
              commonHandler.someThing();
              break;
          default:
              System.out.println("警告");
      }
    }
}

这样新增一个类型,需要写新的 case 语句,不太优雅。

1.2.2 xml 注入 type 到 bean 的映射

小B 同学选择在 Bean 中定义一个 Map<String,Handler>type2BeanMap,然后使用 xml 的方式,将常量和对应 bean 注入进来。

【注意】: 这里的 key 并不是 beanName ,而是某个业务枚举值,如用户类型

代码语言:javascript
复制
<bean id="demoService" class="com.demo.DemoService">
	<property name="type2BeanMap">
		<map>
			<entry key="Vip" value-ref="vipHandler"></entry>
			<entry key="Common" value-ref="commonHandler"></entry>
		</map>
	</property>
</bean>

这样拿到用户类型(vip 或 common)之后,就可以通过该 map 拿到对应的处理 bean 去执行,代码清爽了好多。

代码语言:javascript
复制
@Service
public class DemoService {

    @Setter
    private Map<String,Handler> type2BeanMap;

    public void test(){
        String type ="Vip";
        type2BeanMap.get(type).someThing();
    }
}

这样做会导致,新增一个策略虽然不用修改代码,但是仍然需要修改SomeService 的 xml 配置,本质上和 switch 差不多。

如新增一个 superVip 类型

代码语言:javascript
复制
<bean id="demoService" class="com.demo.DemoService">
	<property name="type2BeanMap">
		<map>
			<entry key="Vip" value-ref="vipHandler"></entry>
			<entry key="Common" value-ref="commonHandler"></entry>
				<entry key="SuperVip" value-ref="superVipHandler"></entry>
		</map>
	</property>
</bean>

那么有没有更有好的解决办法呢?如果脱离 Spring 又该如何实现?

二、解法

2.1 自动注入

代码语言:javascript
复制
import org.springframework.stereotype.Component;

@Component("Vip")
public class VipHandler implements Handler{
   
    @Override
    public void someThing() {
        System.out.println("Vip用户,走这里的逻辑");
    }


}

使用 @Autowired 注解 自动注入 beanName -> Bean 即可使用:

代码语言:javascript
复制
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.HashMap;
import java.util.Map;


@Service
public class DemoService {

    @Autowired
    private   Map<String,Handler> beanMap = new HashMap<>();

    public void test()  {

        // 执行逻辑
        String type ="Vip";
        beanMap.get(type).someThing();
    }

}

这样写法比较简洁,很好的解决了问题。

如果当前场景比较简单,建议可以采用这种办法。

值得探讨的是,使用业务类型当做 Bean 的 name 是否合适?(中性的描述) (1)同一个业务枚举可能会有多种策略,如果每种策略都以业务类型作为 name 会出现很多重名不同类型的 Bean,是否会造成困惑? (2)如果业务枚举名称有修改,Bean 是否能更快感知到影响(如 这里的 VIp 被改为了 Star 那么每个 策略的 name 都要进行修改) (3)有时候不是简单的类型 -> bean 的映射,可能是每个 Class -> Bean 或者 Bean -> Bean 的映 此时这种方法就不太能完美的解决。

如果不希望业务类型影响到 Bean 的 name ,当业务枚举修改时,可强感知到影响的地方,可以使用下面几种解法。

2.2 PostConstruct

Handler 接口新增一个方法,用于区分不同的用户类型。

代码语言:javascript
复制
public interface Handler {

    String getType();

    void someThing();
}

每个子类都给出自己可以处理的类型,如:

代码语言:javascript
复制
import org.springframework.stereotype.Component;

@Component
public class VipHandler implements Handler{
    @Override
    public String getType() {
        return "Vip";
    }

    @Override
    public void someThing() {
        System.out.println("Vip用户,走这里的逻辑");
    }
}

普通用户:

代码语言:javascript
复制
@Component
public class CommonHandler implements Handler{

    @Override
    public String getType() {
        return "Common";
    }

    @Override
    public void someThing() {
        System.out.println("普通用户,走这里的逻辑");
    }
}

然后在使用的地方自动注入目标类型的 bean List 在初始化完成后构造类型到bean 的映射:

代码语言:javascript
复制
import javax.annotation.PostConstruct;
import java.util.List;
import java.util.Map;
import java.util.function.Function;
import java.util.stream.Collectors;

@Service
public class DemoService {

    @Autowired
    private List<Handler> handlers;

    private Map<String, Handler> type2HandlerMap;

    @PostConstruct
    public void init(){
        type2HandlerMap= handlers.stream().collect(Collectors.toMap(Handler::getType, Function.identity()));
    }
    public void test(){
      String type ="Vip";
      type2HandlerMap.get(type).someThing();
    }
}

此时,Spring 会自动将 Handler 类型的所有 bean 注入 List<VipHandler> handlers 中。

注意:如果同一个类型可以有多处理器,需定义为 private Map<String, List<Handler> type2HandlersMap 然后在 init 方法进行构造即可,示例代码:

代码语言:javascript
复制
@Service
public class DemoService {

    @Autowired
    private List<Handler> handlers;

    private Map<String, List<Handler>> type2HandlersMap;

    @PostConstruct
    public void init(){
        type2HandlersMap= handlers.stream().collect(Collectors.groupingBy(Handler::getType));
    }

    public void test(){
      String type ="Vip";
      for(Handler handler : type2HandlersMap.get(type)){
          handler.someThing();;
      }
    }
}

2.3 实现 InitializingBean 接口

然后 init 方法将在依赖注入完成后构造类型到 bean 的映射。(也可以通过实现 InitializingBean 接口,在 afterPropertiesSet 方法中编写上述 init 部分逻辑。 )

在执行业务逻辑时,直接可以根据类型获取对应的 bean 执行即可。

测试类:

代码语言:javascript
复制
public class AnnotationConfigApplication {
    
    public static void main(String[] args) throws Exception {
        ApplicationContext ctx = new AnnotationConfigApplicationContext(QuickstartConfiguration.class);
        DemoService demoService = ctx.getBean(DemoService.class);
        demoService.test();
    }
}

运行结果:

Vip用户,走这里的逻辑

当然这里的 getType 的返回值也可以直接定义为枚举类型,构造类型到bean 的 Mapkey 为对应枚举即可。

大家可以看到这里注入进来的 List<Handler> 其实就在构造type 到 bean 的映射 Map 时用到,其他时候用不到,是否可以消灭掉它呢?


2.4 实现 ApplicationContextAware 接口

我们可以实现 ApplicationContextAware 接口,在 setApplicationContext 时,通过 applicationContext.getBeansOfType(Handler.class) 拿到 Hander 类型的 bean map 后映射即可:

代码语言:javascript
复制
@Service
public class DemoService implements ApplicationContextAware {


    private Map<String, List<Handler>> type2HandlersMap;

    public void test(){
      String type ="Vip";
      for(Handler handler : type2HandlersMap.get(type)){
          handler.someThing();;
      }
    }


    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {

        Map<String, Handler> beansOfType = applicationContext.getBeansOfType(Handler.class);
        beansOfType.forEach((k,v)->{
            type2HandlersMap = new HashMap<>();
            String type =v.getType();
            type2HandlersMap.putIfAbsent(type,new ArrayList<>());
            type2HandlersMap.get(type).add(v);
        });
    }
}

在实际开发中,可以结合根据实际情况灵活运用。

可能很多人思考到这里就很满足了,但是作为有追求的程序员,我们不可能止步于此。

三、More

3.1 如果 SomeService 不是 Spring Bean 又该如何解决?

如果 Handler 是 Spring Bean 而 SomeService 不是 Spring 的 Bean,可以同样 @PostConstruct 使用 ApplicationHolder 的方式构造映射。

构造 ApplicationHolder

代码语言:javascript
复制
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.stereotype.Component;

import java.util.Map;

@Component
public class ApplicationContextHolder implements ApplicationContextAware {
    private static ApplicationContext context;
    
    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        ApplicationContextHolder.context = applicationContext;
    }

    public static <T> T getBean(String id, Class<T> tClass) {
        return context.getBean(id,tClass);
    }

    public static <T> Map<String,T> getBeansOfType(Class<T> tClass){
        return context.getBeansOfType(tClass);
    }

}

编写 DemoService:

代码语言:javascript
复制
public class DemoService {

        private static final Map<String,Handler> TYPE_TO_BEAN_MAP = null;


    public void test(){
        // 构造 map
        initType2BeanMap();

        // 执行逻辑
        String type ="Vip";
        type2BeanMap.get(type).someThing();
    }

    private  synchronized void initType2BeanMap() {
        if (TYPE_TO_BEAN_MAP == null) {
            TYPE_TO_BEAN_MAP = new HashMap<>();

            Map<String, Handler> beansOfType = ApplicationContextHolder.getBeansOfType(Handler.class);
            beansOfType.forEach((k,v)->{
                TYPE_TO_BEAN_MAP.put(v.getType(),v);
            });
        }
    }
}

加上锁,避免首次构造多个 DemoService 时,多次执行 initType2BeanMap

3.2 如果 Handler 也不是 Spring 的Bean 怎么办?

3.2.1 基于反射

代码语言:javascript
复制
      <!-- https://mvnrepository.com/artifact/org.reflections/reflections -->
        <dependency>
            <groupId>org.reflections</groupId>
            <artifactId>reflections</artifactId>
            <version>0.10.2</version>
        </dependency>

示例代码:

代码语言:javascript
复制
import org.reflections.Reflections;

import java.util.HashMap;
import java.util.Map;
import java.util.Set;

import static org.reflections.scanners.Scanners.SubTypes;


public class DemoService {

    private static final Map<String,Handler> TYPE_TO_BEAN_MAP = new HashMap<>();

    private  synchronized void initType2BeanMap()  {
        try{
        // 构造方法中传入扫描的目标包名
                Reflections reflections = new Reflections("com.demo.xxx");
                Set<Class<?>> subTypes =  reflections.get(SubTypes.of(Handler.class).asClass());
                for(Class<?> clazz : subTypes){
                    Handler  handler = (Handler)clazz.newInstance();
                    TYPE_TO_BEAN_MAP.put(handler.getType(),handler);
                }
        }catch(Exception ignore){
            // 实际编码时可忽略,也可以抛出
        }

    }

    public void test()  {

        // 构造 map
        initType2BeanMap();

        // 执行逻辑
        String type ="Vip";
        TYPE_TO_BEAN_MAP.get(type).someThing();
    }


}

运行测试代码正常:

代码语言:javascript
复制
public class Demo {
    public static void main(String[] args) {
        DemoService demoService = new DemoService();
        demoService.test();
    }
}

运行结果

Vip用户,走这里的逻辑

本质上是通过 Java 反射机制来扫描某个接口子类型来代替 Spring 通过 BeanFactory 扫描里面某种类型的 Bean 机制,大同小异。

虽然这里用到了反射,但是只执行一次,不会存在性能问题。

3.2.2 基于 SPI

可以在外部 Jar 包内定义实现,使用 SPI 机制获取所有实现,执行操作。

3.2.3 其他 (待补充)

可以在构造子类型时自动将自身添加都某个容器中,这样使用时直接从容器拿到当前对象即可。

可能还有其他不错的方式,欢迎补充。

四、总结

本文简单介绍了通过 Spring 自动注入实现策略模式的方法,还提供了在非 Spring 环境下的实现方式。

避免新增一个新的 bean 时,多一处修改(硬编码 or 硬配置)。

对编写新的处理类的同学来说非常友好,符合开闭原则,符合封装复杂度的要求。

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2022/02/25 ,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 一、前言
    • 1.1 背景
      • 1.2 不同同学的做法
        • 1.2.1 switch case 模式
        • 1.2.2 xml 注入 type 到 bean 的映射
    • 二、解法
      • 2.1 自动注入
        • 2.2 PostConstruct
          • 2.3 实现 InitializingBean 接口
            • 2.4 实现 ApplicationContextAware 接口
            • 三、More
              • 3.1 如果 SomeService 不是 Spring Bean 又该如何解决?
                • 3.2 如果 Handler 也不是 Spring 的Bean 怎么办?
                  • 3.2.1 基于反射
                  • 3.2.2 基于 SPI
                  • 3.2.3 其他 (待补充)
              • 四、总结
              相关产品与服务
              容器服务
              腾讯云容器服务(Tencent Kubernetes Engine, TKE)基于原生 kubernetes 提供以容器为核心的、高度可扩展的高性能容器管理服务,覆盖 Serverless、边缘计算、分布式云等多种业务部署场景,业内首创单个集群兼容多种计算节点的容器资源管理模式。同时产品作为云原生 Finops 领先布道者,主导开源项目Crane,全面助力客户实现资源优化、成本控制。
              领券
              问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档