策略模式

问题:

虽然使用策略模式能很好的解决这个问题,通过将不同的云存储的sdk封装为各自的工具类,都去实现同一个接口,让我们在使用文件上传的时候无需关注到底是通过哪个云存储去上传。也很方便的增加或者减少各种工具类。但是在Spring如何去判断和导入各种不同平台的文件上传工具类呢?

我们定义了这么多策略,应该怎么优雅的组织起来呢,这就需要用到Spring提供的一些扩展特性了,Spring主要为我们提供了三类扩展点,分别对应不同Bean生命周期阶段:

  • Aware接口

  • BeanPostProcessor

  • InitializingBean 和 init-method

具体实现

1、编写文件上传的接口

1
2
3
4
5
public interface FileUploadService {

void upload(String bucketName, String fileName, InputStream stream);

}

2、添加不同的云存储sdk工具类,都实现该接口

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
30
31
32
@Component("Minio")
@Slf4j
public class MinionUtil implements FileUploadService {

@Resource
private MinioClient minioClient;

@Override
@SneakyThrows(Exception.class)
public void upload(String bucketName, String fileName, InputStream stream) {
if (stream == null){
log.error("文件流为空,上传文件失败");
return;
}
minioClient.putObject(
PutObjectArgs.builder()
.bucket(bucketName)
.object(fileName)
.stream(stream, stream.available(), -1)
.build());
//return getFileUrl(bucketName, fileName);
log.info(fileName + "上传成功");
}

}
@Component("Oss")
public class OssUtil implements FileUploadService {
@Override
public void upload(String bucketName, String fileName, InputStream stream) {
//TODO: oss上传文件
}
}

类似这样直接添加需要的就行,在上述的代码中可以使用 @Component 注解类交由 Spring 进行管理。

3、实现策略选择的过程

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
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
@Component
@Slf4j
public class UploadStrategyContext implements ApplicationContextAware, InitializingBean {


/**
* 默认云存储类型
*/
private static final String defaultCloudType = CloudType.MINIO.getValue();


private ApplicationContext applicationContext;


private Map<String, FileUploadService> uploadStrategyMap = new ConcurrentHashMap<>();

/**
* 注入uploadStrategyMap
* @author njy
* @since 16:12 2022/6/26
*/
@Override
public void afterPropertiesSet() throws Exception {
uploadStrategyMap = applicationContext.getBeansOfType(FileUploadService.class);
}

@Override
public void setApplicationContext(@NotNull ApplicationContext applicationContext) throws BeansException {
this.applicationContext = applicationContext;
}

/**
* 根据云存储类型获取对应文件上传实现类
* @author njy
* @since 16:13 2022/6/26
* @param cloudType 云存储类型
* @return com.video.upload.util.upload.FileUploadService
*/
public FileUploadService getResource(String cloudType) {
FileUploadService uploadService = uploadStrategyMap.get(cloudType);
return uploadService != null ? uploadService : uploadStrategyMap.get(defaultCloudType);
}


}

这里使用了 Spring 的 ApplicationContextAware 接口,该接口要求实现 setApplicationContext 方法,然后在通过 applicationContext 获取所有的 FileUploadService 接口实现。

同时这样做一个最大的好处就是添加新的存储系统的时候直接继承 FileUploadService 接口进行实现即可,无需对原有的代码进行修改,做到了真正的对修改封闭,对扩展开放

上面我们这里用到的主要是 Aware 接口和 InitializingBean 两个扩展点,关键点就在于实现 ApplicationContextAware 接口的 setApplicationContext 方法和 InitializingBean 接口的 afterPropertiesSet 方法。

实现 ApplicationContextAware 接口的目的就是要拿到 Spring 容器的资源,从而方便的使用它提供的 getBeansOfType 方法(该方法返回的是 map 类型,key 对应 beanName, value 对应 bean);而实现 InitializingBean 接口的目的则是方便为 Service 类的 handlers 属性执行定制初始化逻辑。

image-20220626221022264

可以很明显的看出,如果以后还有一些其他的业务需要制定相应的鉴权逻辑,我们只需要编写对应的策略类就好了,无需再破坏当前 Service 类的逻辑,很好的保证了开闭原则。

4、封装成统一的文件上传工具类,所有文件上传通过此工具类上传

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
@Slf4j
@Component
public class FileUploadUtil {

@Resource
private CloudProperties cloudProperties;

@Resource
private UploadStrategyContext uploadStrategyContext;


/**
* 统一文件上传入口
* @author njy
* @since 16:39 2022/6/26
* @param bucketName 桶名称
* @param fileName 文件名
* @param stream 流名称
*/
public void upload(String bucketName, String fileName, InputStream stream){
FileUploadService uploadService = uploadStrategyContext.getResource(cloudProperties.getType());
uploadService.upload(bucketName,fileName,stream);
}


}

参考:https://zhuanlan.zhihu.com/p/347389684?utm_source=wechat_session&utm_medium=social&utm_oi=779429778875772928&utm_campaign=shareopn