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

一、前言

1.1 背景

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

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

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

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

public interface Handler {

    void someThing();
}

1.2 不同同学的做法

在这里插入图片描述

1.2.1 switch case 模式

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

@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 ,而是某个业务枚举值,如用户类型

<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 去执行,代码清爽了好多。

@Service
public class DemoService {

    @Setter
    private Map<String,Handler> type2BeanMap;

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

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

如新增一个 superVip 类型

<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 自动注入

import org.springframework.stereotype.Component;

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


}

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

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 接口新增一个方法,用于区分不同的用户类型。

public interface Handler {

    String getType();

    void someThing();
}

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

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用户,走这里的逻辑");
    }
}

普通用户:

@Component
public class CommonHandler implements Handler{

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

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

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


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 方法进行构造即可,示例代码:

@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 执行即可。

测试类:

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 后映射即可:

@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

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:


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 基于反射

      <!-- https://mvnrepository.com/artifact/org.reflections/reflections -->
        <dependency>
            <groupId>org.reflections</groupId>
            <artifactId>reflections</artifactId>
            <version>0.10.2</version>
        </dependency>

示例代码:

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();
    }


}

运行测试代码正常:

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 硬配置)。

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

创作不易,你的支持和鼓励是我创造的最大动力,如果本文对你有帮助,欢迎点赞、收藏,也欢迎评论和我交流。
在这里插入图片描述

评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

明明如月学长

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值