zl程序教程

您现在的位置是:首页 >  其他

当前栏目

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

2023-03-14 22:40:06 时间

一、前言

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 中定义一个 Maptype2BeanMap,然后使用 xml 的方式,将常量和对应 bean 注入进来。

<bean id="someService" class="com.demo.SomeService">
	<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="someService" class="com.demo.SomeService">
	<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>

那么有没有更有好的解决办法呢?

二、解法

2.1 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 handlers 中。

注意:如果同一个类型可以有多处理器,需定义为 private Map 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.2 实现 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 其实就在构造type 到 bean 的映射 Map 时用到,其他时候用不到,是否可以消灭掉它呢?


2.3 实现 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);
        });
    }
}

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

三、总结

本文简单介绍了一种通过 Spring 自动注入实现策略模式的方法。 避免新增一个新的 bean 时,多一处修改(硬编码 or 硬配置)。 对编写新的处理类的同学来说非常友好。