(给
ImportNew加星标,提高Java技能)
一、引言
为了满足不同环境和需求的变化,我们需要让自己写的代码涉及面更加广泛,为了支持不同平台的对象,我决定设计一个支持各种平台的文件上传,删除功能,相信一点能满足你的需求。二、介绍主角
- @ConditionalOnProperty:根据指定的属性值条件,决定是否创建该组件的实例。这使得组件的创建可以根据配置文件中的属性进行动态控制。
- @ConfigurationProperties:将配置文件中的属性值绑定到该类的字段上,实现属性的自动注入。这样可以方便地从配置文件中读取和使用属性值。
下面是一个简单的案例
首先,定义一个文件上传接口 FileUploadService,其中包含文件上传的方法:public interface FileUploadService {
void uploadFile(MultipartFile file);
}
然后,创建不同平台的文件上传实现类,例如 LocalFileUploadService 和 S3FileUploadService:@Component
@ConditionalOnProperty(value = "file.upload.platform", havingValue = "local")
public class LocalFileUploadService implements FileUploadService {
@Override
public void uploadFile(MultipartFile file) {
System.out.println("Uploading file to local platform...");
}
}
@Component
@ConditionalOnProperty(value = "file.upload.platform", havingValue = "s3")
public class S3FileUploadService implements FileUploadService {
@Override
public void uploadFile(MultipartFile file) {
System.out.println("Uploading file to S3 platform...");
}
}
通过在配置文件(例如 application.properties)中设置 file.upload.platform 属性的值,可以选择性地使用不同平台的文件上传实现类:file.upload.platform=local
这样,你就可以根据需要选择性地实现不同平台的文件上传接口,并通过配置文件来控制使用哪个实现类。三、具体编写
3.1 构思
我们需要对上传文件进行自定义校验,比如文件名,文件大小、文件后缀判断、对文件重命名等等,这些都是每个接口可能需要实现的内容,我们不可能让每个接口都去实现,这样就会造成以下情况:
public class LocalFileStorage{
public String uploadFile(String dir, MultipartFile file, String[] allowedExtension) {
}
}
public class AliyunFileStorage{
public String uploadFile(String dir, MultipartFile file, String[] allowedExtension) {
}
}
public class TencentFileStorage{
public String uploadFile(String dir, MultipartFile file, String[] allowedExtension) {
}
}
这让我想到了,AOP(Aspect Oriented Programming),我们可以对接口进行切面,在切面中实现这些不就好了?这样一下就解决了第 1 点和第 2 点。第 3 点:构思的解决方案主要是在接口上进行扩展,很好解决。第 4 点:在文章开始已经说明,采用 @ConditionalOnProperty(..) 即可.3.2 “赛前” 准备工作
媒体类型定义
public class MimeTypeConstant {
public static final String[] IMAGE_EXTENSION = {"bmp", "gif", "jpg", "jpeg", "png"};
public static final String[] FLASH_EXTENSION = {"swf", "flv"};
public static final String[] MEDIA_EXTENSION = {"swf", "flv", "mp3", "wav", "wma", "wmv", "mid", "avi", "mpg",
"asf", "rm", "rmvb"};
public static final String[] VIDEO_EXTENSION = {"mp4", "avi", "rmvb"};
public static final String[] DEFAULT_ALLOWED_EXTENSION = {
"bmp", "gif", "jpg", "jpeg", "png",
"doc", "docx", "xls", "xlsx", "ppt", "pptx", "html", "htm", "txt",
"rar", "zip", "gz", "bz2",
"mp4", "avi", "rmvb",
"pdf"};
}
配置类定义
用于读取配置类中一些基础配置方便后续根据用户配置进行校验。
@Component
@ConfigurationProperties(prefix = "file.storage")
@Data
public class FileStorageConfig {
private String type = "local";
private String[] allowedExtension = IMAGE_EXTENSION;
private int fileNameLength = 100;
private boolean coverFileName = true;
}
FileUtils 工具类编写
public class FileUtils {
public static final String DOT = ".";
public static final DateTimeFormatter FORMATTER =
DateTimeFormatter.ofPattern("yyyy" + File.separator + "MM" + File.separator + "dd" + File.separator);
public static boolean isAllowedExtension(String extension, String[] allowedExtension) {
for (String str : allowedExtension) {
if (str.equalsIgnoreCase(extension)) {
return true;
}
}
return false;
}
public static String getFileExtension(MultipartFile file) {
String originalFilename = file.getOriginalFilename();
if (originalFilename == null) {
throw new RuntimeException("FileUtils: originalFilename is null");
}
String fileExtension;
int lastDotIndex = originalFilename.lastIndexOf('.');
if (lastDotIndex > 0) {
fileExtension = originalFilename.substring(lastDotIndex + 1);
} else {
MimeType mimeType = MimeTypeUtils.parseMimeType(Objects.requireNonNull(file.getContentType()));
fileExtension = mimeType.getSubtype();
}
return fileExtension;
}
public static String correctAndJoinPaths(String... paths) {
StringBuilder result = new StringBuilder();
for (String path : paths) {
if (path != null && !path.isEmpty()) {
if (!result.isEmpty() && result.charAt(result.length() - 1) != File.separatorChar && !path.startsWith(File.separator)) {
result.append(File.separator);
}
result.append(path.replaceAll("[" + File.separator + "/\\]+$", ""));
}
}
if (!result.isEmpty() && result.charAt(result.length() - 1) != File.separatorChar) {
result.append(File.separator);
}
return result.toString();
}
public static String datePath() {
return LocalDate.now().format(FORMATTER);
}
}
3.3 编写代码
定义 FileStorageService 接口
- 这个接口提供了一组方法来处理文件上传和删除的操作,并提供了一些默认实现来简化使用。你可以根据自己的需求实现该接口,并在实现类中提供具体的上传和删除逻辑。
- 虽然看着接口内容很多,但是实现者只需要实现 uploadFile() , deleteFile() 即可。
public interface FileStorageService {
String uploadFile(String dir, MultipartFile file, String[] allowedExtension);
default String[] uploadFiles(String dir, MultipartFile[] files, String[] allowedExtension) {
return Arrays.stream(files).map(file -> this.uploadFile(dir, file, allowedExtension)).toArray(String[]::new);
}
boolean deleteFile(String dir, String url);
default boolean deleteFiles(String dir, String[] urls) {
return Arrays.stream(urls).allMatch(url -> this.deleteFile(dir, url));
}
default boolean deleteFile(String url) {
return deleteFile("", url);
}
default boolean deleteFiles(String[] urls) {
return Arrays.stream(urls).allMatch(this::deleteFile);
}
default String uploadFile(String dir, MultipartFile file) {
return uploadFile(dir, file, null);
}
default String uploadFile(MultipartFile file, String[] allowedExtension) {
return uploadFile("", file, allowedExtension);
}
default String uploadFile(MultipartFile file) {
return uploadFiles("", new MultipartFile[]{file}, null)[0];
}
default String[] uploadFiles(String dir, MultipartFile[] files) {
return Arrays.stream(files).map(file -> this.uploadFile(dir, file)).toArray(String[]::new);
}
default String[] uploadFiles(MultipartFile[] files, String[] allowedExtension) {
return Arrays.stream(files).map(file -> this.uploadFile(file, allowedExtension)).toArray(String[]::new);
}
default String[] uploadFiles(MultipartFile[] files) {
return Arrays.stream(files).map(this::uploadFile).toArray(String[]::new);
}
}
本地上传进行实现
接口对应实现,这里只给出本地文件上传的实现方法,其他方法类似。
@Component
@ConditionalOnProperty(value = "file.storage.type", havingValue = "local", matchIfMissing = true)
@ConfigurationProperties(prefix = "file.storage.local")
@RequiredArgsConstructor
@Data
public class LocalFileStorageImpl implements FileStorageService {
private static final Logger log = LoggerFactory.getLogger(LocalFileStorageImpl.class);
private String uploadPath;
private String accessUrl = "/images";
@Bean
public WebMvcConfigurer resourceHandlerConfigurer() {
return new WebMvcConfigurer() {
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
String fileUploadPath = FileUtils.correctAndJoinPaths(uploadPath);
registry.addResourceHandler(accessUrl + "/**").addResourceLocations("file:" + fileUploadPath);
}
};
}
@Override
public String uploadFile(String dir, MultipartFile file, String[] allowedExtension) {
String absolutePath = FileUtils.correctAndJoinPaths(uploadPath, dir, FileUtils.datePath());
try {
File absoluteFile = getAbsoluteFile(absolutePath, file.getOriginalFilename());
file.transferTo(absoluteFile);
} catch (IOException e) {
log.error("上传文件失败, 常见错误: 未开启路径权限: {}", absolutePath);
throw new ServiceException(ResultCode.FILE_UPLOAD_ERROR);
}
return getRequestFileName(file, accessUrl + "/" + dir + "/" + FileUtils.datePath());
}
@Override
public boolean deleteFile(String dir, String url) {
String fileName = extractFileNameFromUrl(url);
String absolutePath = FileUtils.correctAndJoinPaths(uploadPath, dir, fileName);
File file = new File(absolutePath);
if (file.exists()) {
if (!file.delete()) {
log.error("File deletion failed: {}", absolutePath);
return false;
}
} else {
log.warn("File does not exist for deletion: {}", absolutePath);
return false;
}
return true;
}
private String getRequestFileName(MultipartFile file, String accessUrl) {
HttpServletRequest request = ((ServletRequestAttributes) Objects.requireNonNull(RequestContextHolder.getRequestAttributes())).getRequest();
String baseUrl = request.getRequestURL().toString().replace(request.getRequestURI(), request.getContextPath());
String originalFilename = file.getOriginalFilename();
String requestFileName = baseUrl + accessUrl + originalFilename;
return requestFileName.replace("//", "/").replace(File.separator, "/");
}
private File getAbsoluteFile(String uploadDir, String fileName) {
File desc = new File(uploadDir + fileName);
if (!desc.exists()) {
if (!desc.getParentFile().exists()) {
desc.getParentFile().mkdirs();
}
}
return desc;
}