巧用 Spring 自动注入实现策略模式升级版
Posted 明明如月学长
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了巧用 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 注入进来。
<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 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.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 的 Map
时 key
为对应枚举即可。
大家可以看到这里注入进来的 List<Handler>
其实就在构造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);
});
}
}
在实际开发中,可以结合根据实际情况灵活运用。
可能很多人思考到这里就很满足了,但是作为有追求的程序员,我们不可能止步于此。
三、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 其他 (待补充)
可以在构造子类型时自动将自身添加都某个容器中,这样使用时直接从容器拿到当前对象即可。
可能还有其他不错的方式,欢迎补充。
四、总结
本文简单介绍了通过 Spring 自动注入实现策略模式的方法,还提供了在非 Spring 环境下的实现方式。
避免新增一个新的 bean 时,多一处修改(硬编码 or 硬配置)。
对编写新的处理类的同学来说非常友好,符合开闭原则,符合封装复杂度的要求。
创作不易,你的支持和鼓励是我创造的最大动力,如果本文对你有帮助,欢迎点赞、收藏,也欢迎评论和我交流。
以上是关于巧用 Spring 自动注入实现策略模式升级版的主要内容,如果未能解决你的问题,请参考以下文章
Spring 源码学习系列ApplicationContextAware#setApplicationContext 方法的调用时机