在工作过程中,有时候需要根据不同的枚举(常量)执行不同的逻辑。
比如不同的用户类型,使用不同的优惠政策;不同的配置变化,走不同的处理逻辑等。
下面模拟一个根据不同用户类型,走不同业务逻辑的案例。
不同的用户类型有不同的处理方式,接口为 Handler
,示例代码如下:
public interface Handler {
void someThing();
}
小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 语句,不太优雅。
小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 又该如何实现?
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 ,当业务枚举修改时,可强感知到影响的地方,可以使用下面几种解法。
对 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();;
}
}
}
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
时用到,其他时候用不到,是否可以消灭掉它呢?
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);
});
}
}
在实际开发中,可以结合根据实际情况灵活运用。
可能很多人思考到这里就很满足了,但是作为有追求的程序员,我们不可能止步于此。
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
。
<!-- 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 机制,大同小异。
虽然这里用到了反射,但是只执行一次,不会存在性能问题。
可以在外部 Jar 包内定义实现,使用 SPI 机制获取所有实现,执行操作。
可以在构造子类型时自动将自身添加都某个容器中,这样使用时直接从容器拿到当前对象即可。
可能还有其他不错的方式,欢迎补充。
本文简单介绍了通过 Spring 自动注入实现策略模式的方法,还提供了在非 Spring 环境下的实现方式。
避免新增一个新的 bean 时,多一处修改(硬编码 or 硬配置)。
对编写新的处理类的同学来说非常友好,符合开闭原则,符合封装复杂度的要求。