在SpringBoot中使用策略模式

此篇博客完全参考该文章:SpringBoot 策略模式的运用,其中些许部分做了修改。

这两天有这么个需求,需要后台使用不同的策略做计算,个人感觉写if-else实在是有点low,就想着用点设计模式,一开始看的是菜鸟的策略模式,但是发现不好加进SpringBoot项目中,后来找到了上边这篇文章,写的特别简单易懂,就借鉴了下。

1.自定义注解

1
2
3
4
5
6
7
8
9
10
11
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
public @interface StrategyAnnotation {

/**
* @return 策略
*/
String value();
}

各个注解的作用:@Target:注解的作用目标

  1. @Target(ElementType.TYPE):接口、类、枚举、注解
  2. @Retention(RetentionPolicy.RUNTIME):这种类型的Annotations将被JVM保留,所以他们能在运行时被JVM或其他使用反射机制的代码所读取和使用。
  3. @Documented:说明该注解将被包含在javadoc中
  4. @Inherited:说明子类可以继承父类中的该注解

2.策略接口

原作者在这里使用的是抽象类,但我理解不同的策略其实是不同的动作,策略是动作的抽象,而不是对象的抽象,所以我在这里使用的是接口,具体不同的策略实现这个接口

1
2
3
public interface IStrategy {
String handler(String strategy);
}

2.1 实现策略接口

1
2
3
4
5
6
7
8
9
10
@StrategyAnnotation(value = "timeBest")
@Component
@Slf4j
public class TimeBest implements IStrategy {
@Override
public String handler(String strategy) {
log.info("时间最优-log");
return null;
}
}
1
2
3
4
5
6
7
8
9
10
@StrategyAnnotation(value = "skillBest")
@Component
@Slf4j
public class SkillBest implements IStrategy {
@Override
public String handler(String strategy) {
log.info("技能最优-log");
return "技能最优";
}
}
1
2
3
4
5
6
7
8
9
10
@StrategyAnnotation(value = "distanceBest")
@Component
@Slf4j
public class DistanceBest implements IStrategy {
@Override
public String handler(String strategy) {
log.info("距离最优-log");
return "距离最优";
}
}

3.构造策略上下文

构造策略上下文是非常重要的,具体的策略实现了,如何调用?就是通过实例化一个上下文对象来执行策略,这个对象接收不同的参数,从而执行不同的策略。集成到SpringBoot中,我就有些不太会操作了。贴上代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
@Component
public class StrategyContext implements ApplicationContextAware {
@Resource
private ApplicationContext applicationContext;

private static final Map<String, Class> strategyMap = new HashMap<>();

@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
// 获取所有使用了自定义注解的Bean
Map<String, Object> annotationBeans = applicationContext.getBeansWithAnnotation(StrategyAnnotation.class);
if (annotationBeans != null && annotationBeans.size() > 0){
for (Object item : annotationBeans.values()){
// 获取 Bean 的运行时类
Class<?> itemClass = item.getClass();
String strategy = itemClass.getAnnotation(StrategyAnnotation.class).value();
strategyMap.put(strategy, itemClass);
}
}
}

public IStrategy getStrategy(String strategyType){
Class aClass = strategyMap.get(strategyType);
if (aClass == null){
throw new ServiceException("暂不支持此策略");
}
return (IStrategy) applicationContext.getBean(aClass);
}
}

这里是通过实现org.springframework.context.ApplicationContextAware接口来实现的,重写了setApplicationContext方法。查看文档,可以知道这个方法的具体作用:

Set the ApplicationContext that this object runs in. Normally this call will be used to initialize the object.

Invoked after population of normal bean properties but before an init callback such as org.springframework.beans.factory.InitializingBean.afterPropertiesSet() or a custom init-method. Invoked after ResourceLoaderAware.setResourceLoader, ApplicationEventPublisherAware.setApplicationEventPublisher and MessageSourceAware, if applicable.

简言之就是:

设置对象运行的ApplicationContext。通常此调用将用于初始化该对象。

调用时机:

  1. 填充普通bean的属性之后,但在init回调自定义回调之前被调用。init回调举例:org.springframework.beans.factory.InitializingBean.afterPropertiesSet()
  2. 如果ResourceLoaderAware.setResourceLoader, ApplicationEventPublisherAware.setApplicationEventPublisherMessageSourceAware适用,则在他俩之后被调用。

4.使用策略

这一步就很简单了,只需要在service中注入strategy,就可以调用了。

1
2
3
4
5
6
7
8
9
@Resource
private StrategyContext strategyContext;

@Override
public List<EngineerVO> recommend(String caseId, String strategyType) {
IStrategy strategyHandler = strategyContext.getStrategy(strategyType);
strategyHandler.handler(strategyType);
return null;
}

至此,我们就可以在项目中使用策略模式了,再新建不同的策略,只需要实现IStrategy接口即可。

后记

关于@Component注解

这里说说@Component注解,不管是实现IStrategy接口还是构造策略上下文,我们都使用的是@Component注解,用别的可以吗?例如@Service,技术上是可以的,肯定不会报错,因为他们底层都是把被注解类变成Bean,但是为什么还得用不同的名字呢?我认为主要还是看场景区分,策略模式跟业务没太大关系,这可以作为一个组件,安插进项目,使代码结构更清晰且易于维护,@Service一般用来注解跟业务相关的代码。如果我们认为实现的策略是跟业务有关的,那么使用@Service也可以。

关于ApplicationContextAware

查看文档:

Interface to be implemented by any object that wishes to be notified of the ApplicationContext that it runs in.

Implementing this interface makes sense for example when an object requires access to a set of collaborating beans. Note that configuration via bean references is preferable to implementing this interface just for bean lookup purposes.

This interface can also be implemented if an object needs access to file resources, i.e. wants to call getResource, wants to publish an application event, or requires access to the MessageSource. However, it is preferable to implement the more specific ResourceLoaderAware, ApplicationEventPublisherAware or MessageSourceAware interface in such a specific scenario.

Note that file resource dependencies can also be exposed as bean properties of type Resource, populated via Strings with automatic type conversion by the bean factory. This removes the need for implementing any callback interface just for the purpose of accessing a specific file resource.

ApplicationObjectSupport is a convenience base class for application objects, implementing this interface.

For a list of all bean lifecycle methods, see the BeanFactory javadocs.

任何对象希望收到ApplicationContext的通知都应该实现这个接口。

Spring容器会扫描到所有实现了这个接口的bean,在初始化bean后,会调用setApplicationContext()方法,并将applicationContext对象(容器)传递给该方法,从而对容器中的bean进行一些个性化操作。