前言
最近看到了一种上传的方式,通过配置文件可以更换自己的上传方式,例如本地上传或者是阿里云oss上传方式,于是大概总结了一番。
策略模式
策略模式(Strategy Pattern):定义一族算法类,将每个算法分别封装起来,让它们可以互相替换。将每个算法封装在一个具有共同接口的独立类中,使得它们可以相互替换。策略模式让算法的变化独立于使用它们的客户端。
策略模式的主要优点是:
- 策略模式提供了一种将算法家族封装起来的方式,这有助于简化代码和提高代码的可读性、可维护性和可扩展性。
- 策略模式支持开闭原则(Open-Closed Principle),在不修改现有代码的情况下添加新的算法。
- 策略模式可以让客户端根据需要选择不同的算法实现,从而实现不同的行为。
正文
一般使用策略模式步骤是:抽象策略-具体策略A-具体策略B-上下文-客户端。
我们可以定义一个抽象策略
Strategy
,以及多个具体策略例如ConcreteStrategyA
和ConcreteStrategyB
。Context
类负责持有一个策略对象的引用,并提供设置和执行策略的方法。客户端可以根据需要选择不同的策略实现,并通过上下文对象来执行相应的策略。
实现步骤
yml配置
# 文件上传策略 local、oss
upload:
strategy: local
local:
# nginx映射本地文件路径
url: https://192.168.0.1:8080
# 本地文件存储路径
path: D:/java/file
# oss存储
oss:
url: http://Bucket域名/
endpoint: your-oss-endpoint
bucketName: your-oss-bucket-name
accessKeyId: your-oss-access-key-id
accessKeySecret: your-oss-access-key-secret
阿里的配置
@Data
@Configuration
@ConfigurationProperties(prefix = "upload.oss")
public class OssProperties {
/**
* oss域名
*/
private String url;
/**
* 终点
*/
private String endpoint;
/**
* 访问密钥id
*/
private String accessKeyId;
/**
* 访问密钥密码
*/
private String accessKeySecret;
/**
* bucket名称
*/
private String bucketName;
}
一 抽象策略
public interface UploadStrategy {
/**
* 上传文件
*
* @param file 文件
* @param path 上传路径
* @return {@link String} 文件地址
*/
String uploadFile(MultipartFile file, String path);
@Service
public abstract class AbstractUploadStrategyImpl implements UploadStrategy {
@Override
public String uploadFile(MultipartFile file, String path) {
try {
// 获取文件md5值
String md5 = FileUtils.getMd5(file.getInputStream());
// 获取文件扩展名
String extName = FileUtils.getExtension(file);
// 重新生成文件名
String fileName = md5 + "." + extName;
// 判断文件是否已存在
if (!exists(path + fileName)) {
// 不存在则继续上传
upload(path, fileName, file.getInputStream());
}
// 返回文件访问路径
return getFileAccessUrl(path + fileName);
} catch (Exception e) {
e.printStackTrace();
throw new ServiceException("文件上传失败");
}
}
/**
* 上传
*
* @param path 路径
* @param fileName 文件名
* @param inputStream 输入流
* @throws IOException io异常
*/
public abstract void upload(String path, String fileName, InputStream inputStream) throws IOException;
工具类
这里我们用到了一些工具类
@Log4j2
public class FileUtils {
/**
* 获取文件md5值
*
* @param inputStream 文件输入流
* @return {@link String} 文件md5值
*/
public static String getMd5(InputStream inputStream) {
String md5 = null;
try {
md5 = DigestUtils.md5DigestAsHex(inputStream);
} catch (Exception e) {
log.error("get md5 error, {}", e.getMessage());
}
return md5;
}
/**
* 获取文件名的后缀
*
* @param file 表单文件
* @return 后缀名
*/
public static String getExtension(MultipartFile file) {
String extension = FilenameUtils.getExtension(file.getOriginalFilename());
if (StringUtils.isEmpty(extension)) {
extension = MimeTypeUtils.getExtension(Objects.requireNonNull(file.getContentType()));
}
return extension;
}
/**
* 转换file
*
* @param multipartFile 文件
* @return {@link File} 临时文件
*/
public static File multipartFileToFile(MultipartFile multipartFile) {
File tempFile = null;
try {
// 获取文件md5值
String md5 = getMd5(multipartFile.getInputStream());
// 获取文件扩展名
String extName = getExtension(multipartFile);
// 重新生成文件名
String fileName = md5 + extName;
// 创建临时文件
tempFile = File.createTempFile(fileName, extName);
multipartFile.transferTo(tempFile);
} catch (IOException e) {
e.printStackTrace();
}
return tempFile;
}
二 本地上传
/**
* 访问url
*/
@Value("${upload.local.url}")
private String localUrl;
@Override
public void upload(String path, String fileName, InputStream inputStream) throws IOException {
// 判断目录是否存在
File directory = new File(localPath + path);
if (!directory.exists()) {
if (!directory.mkdirs()) {
throw new ServiceException("创建目录失败");
}
}
// 写入文件
File file = new File(localPath + path + fileName);
if (file.createNewFile()) {
try (BufferedInputStream bis = new BufferedInputStream(inputStream);
BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(file))) {
byte[] bytes = new byte[4096];
int length;
while ((length = bis.read(bytes)) != -1) {
bos.write(bytes, 0, length);
}
}
}
}
public class MimeTypeUtils {
public static final String IMAGE_PNG = "image/png";
public static final String IMAGE_JPG = "image/jpg";
public static final String IMAGE_JPEG = "image/jpeg";
public static final String IMAGE_BMP = "image/bmp";
public static final String IMAGE_GIF = "image/gif";
public static String getExtension(String prefix) {
switch (prefix) {
case IMAGE_PNG:
return "png";
case IMAGE_JPG:
return "jpg";
case IMAGE_JPEG:
return "jpeg";
case IMAGE_BMP:
return "bmp";
case IMAGE_GIF:
return "gif";
default:
return "";
}
}
}
三 阿里oss上传
@Slf4j
@Service("ossUploadStrategyImpl")
public class OssUploadStrategyImpl extends AbstractUploadStrategyImpl {
@Autowired
private OssProperties ossProperties;
@Override
public Boolean exists(String filePath) {
return getOssClient().doesObjectExist(ossProperties.getBucketName(), filePath);
}
@Override
public void upload(String path, String fileName, InputStream inputStream) {
OSS ossClient = getOssClient();
try {
// 调用oss方法上传
ossClient.putObject(ossProperties.getBucketName(), path + fileName, inputStream);
} catch (OSSException oe) {
log.error("Error Message:" + oe.getErrorMessage());
log.error("Error Code:" + oe.getErrorCode());
log.info("Request ID:" + oe.getRequestId());
log.info("Host ID:" + oe.getHostId());
} catch (ClientException ce) {
log.error("Caught an ClientException, Error Message:" + ce.getMessage());
} finally {
if (ossClient != null) {
ossClient.shutdown();
}
}
}
四 定义上下文
@Service
public class UploadStrategyContext {
/**
* 上传模式
*/
@Value("${upload.strategy}")
private String uploadStrategy;
@Autowired
private Map<String, UploadStrategy> uploadStrategyMap;
/**
* 上传文件
*
* @param file 文件
* @param path 路径
* @return {@link String} 文件地址
*/
public String executeUploadStrategy(MultipartFile file, String path) {
return uploadStrategyMap.get(getStrategy(uploadStrategy)).uploadFile(file, path);
}
这里我们还需要一些枚举类,来选择配置,通过配置文件所写的上传模式来选择具体的实现类。
@Getter
@AllArgsConstructor
public enum UploadModeEnum {
/**
* 本地
*/
LOCAL("local", "localUploadStrategyImpl"),
/**
* oss
*/
OSS("oss", "ossUploadStrategyImpl"),
/**
* 模式
*/
private final String mode;
/**
* 策略
*/
private final String strategy;
/**
* 获取策略
*
* @param mode 模式
* @return 搜索策略
*/
public static String getStrategy(String mode) {
for (UploadModeEnum value : UploadModeEnum.values()) {
if (value.getMode().equals(mode)) {
return value.getStrategy();
}
}
return null;
}
}
最后我们来写一个客户端上传文件的例子
controller
@RestController
@RequestMapping("/admin")
@Slf4j
public class FileController {
@Autowired
private FubenFileService fubenFileService;
@ApiOperation(value = "上传文件")
@ApiImplicitParam(name = "file", value = "图片", required = true, dataType = "MultipartFile")
@PostMapping("/file/upload")
public Result<?> uploadFile(@RequestParam("file") MultipartFile file, @RequestParam("path") String path) {
fubenFileService.uploadFile(file, path);
return Result.success("上传成功!");
}
ServiceImpl
这里我就直接写实现了
@Autowired
private FubenFileMapper fubenFileMapper;
@Autowired
private UploadStrategyContext uploadStrategyContext;
@Override
public void uploadFile(MultipartFile file, String path) {
try {
String uploadPath = "/".equals(path) ? path : path + "/";
// 上传文件
String url = uploadStrategyContext.executeUploadStrategy(file, uploadPath);
// 获取文件md5值
String md5 = FileUtils.getMd5(file.getInputStream());
// 获取文件扩展名
String extName = FileUtils.getExtension(file);
FubenFile existFile = fubenFileMapper.selectOne(new LambdaQueryWrapper<FubenFile>()
.select(FubenFile::getId)
.eq(FubenFile::getFileName, md5)
.eq(FubenFile::getFilePath, path));
Assert.isNull(existFile, "文件已存在");
// 保存文件信息
FubenFile newFile = FubenFile.builder()
.fileUrl(url)
.fileName(md5)
.filePath(path)
.fileType(extName)
.fileSize((int) file.getSize())
.isDir(FALSE)
.createTime(DateTime.now())
.build();
fubenFileMapper.insert(newFile);
} catch (IOException e) {
e.printStackTrace();
}
}