前言
記錄下SpringBoot下
靜態資源儲存伺服器的搭建。
環境
win10 + SpringBoot2.5.3
實現效果
- 檔案上傳:
- 檔案儲存位置:
- 檔案訪問:
具體實現
檔案上傳
配置類
pom.xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<scope>provided</scope>
</dependency>
application.yml
spring:
# 檔案編碼 UTF8
mandatory-file-encoding: UTF-8
server:
# 服務埠
port: 8000
#檔案上傳配置
file:
# 檔案服務域名
domain: http://localhost:8000/
# 排除檔案型別
exclude:
# 包括檔案型別
include:
- .jpg
- .png
- .jpeg
# 檔案最大數量
nums: 10
# 伺服器檔案路徑
serve-path: assets/**
# 單個檔案最大體積
single-limit: 2MB
# 本地檔案儲存位置
store-dir: assets/
yml
讀取工廠類
import org.springframework.boot.env.YamlPropertySourceLoader;
import org.springframework.core.env.PropertySource;
import org.springframework.core.io.support.EncodedResource;
import org.springframework.core.io.support.PropertySourceFactory;
import java.io.IOException;
import java.util.List;
/**
* @Description yml讀取工廠類
* @author coisini
* @date Sep 7, 2021
* @Version 1.0
*/
public class YmlPropertySourceFactory implements PropertySourceFactory {
@Override
public PropertySource<?> createPropertySource(String name, EncodedResource resource) throws IOException {
List<PropertySource<?>> sources = new YamlPropertySourceLoader().load(resource.getResource().getFilename(), resource.getResource());
return sources.get(0);
}
}
- 檔案上傳屬性配置類
import com.coisini.file.factory.YmlPropertySourceFactory;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.PropertySource;
import org.springframework.stereotype.Component;
/**
* @Description 檔案上傳屬性配置類
* @author coisini
* @date Sep 7, 2021
* @Version 1.0
*/
@Component
@ConfigurationProperties(prefix = "file")
@PropertySource(value = "classpath:application.yml",
encoding = "UTF-8",factory = YmlPropertySourceFactory.class)
public class FilePropertiesConfiguration {
private static final String[] DEFAULT_EMPTY_ARRAY = new String[0];
private String storeDir = "/assets";
private String singleLimit = "2MB";
private Integer nums = 10;
private String domain;
private String[] exclude = DEFAULT_EMPTY_ARRAY;
private String[] include = DEFAULT_EMPTY_ARRAY;
public String getStoreDir() {
return storeDir;
}
public void setStoreDir(String storeDir) {
this.storeDir = storeDir;
}
public String getSingleLimit() {
return singleLimit;
}
public void setSingleLimit(String singleLimit) {
this.singleLimit = singleLimit;
}
public Integer getNums() {
return nums;
}
public void setNums(Integer nums) {
this.nums = nums;
}
public String[] getExclude() {
return exclude;
}
public void setExclude(String[] exclude) {
this.exclude = exclude;
}
public String[] getInclude() {
return include;
}
public void setInclude(String[] include) {
this.include = include;
}
public String getDomain() {
return domain;
}
public void setDomain(String domain) {
this.domain = domain;
}
}
上傳介面
- 檔案上傳控制器
import com.coisini.file.vo.FileVo;
import com.coisini.file.service.FileService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.util.MultiValueMap;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.multipart.MultipartHttpServletRequest;
import javax.servlet.http.HttpServletRequest;
import java.util.List;
/**
* @Description 檔案上傳控制器
* @author coisini
* @date Sep 7, 2021
* @Version 1.0
*/
@RestController
@RequestMapping("/file")
public class FileController {
@Autowired
private FileService fileService;
/**
* 檔案上傳
* @param request
* @return
*/
@PostMapping("/upload")
public List<FileVo> upload(HttpServletRequest request) {
MultipartHttpServletRequest multipartHttpServletRequest = ((MultipartHttpServletRequest) request);
MultiValueMap<String, MultipartFile> fileMap = multipartHttpServletRequest.getMultiFileMap();
List<FileVo> files = fileService.upload(fileMap);
return files;
}
}
- 檔案上傳介面
import com.coisini.file.vo.FileVo;
import org.springframework.util.MultiValueMap;
import org.springframework.web.multipart.MultipartFile;
import java.util.List;
/**
* @Description 檔案上傳介面
* @author coisini
* @date Sep 7, 2021
* @Version 1.0
*/
public interface FileService {
/**
* 上傳檔案
* @param fileMap 檔案map
* @return 檔案資料
*/
List<FileVo> upload(MultiValueMap<String, MultipartFile> fileMap);
}
- 檔案上傳實現類
import com.coisini.file.model.FileModel;
import com.coisini.file.core.Uploader;
import com.coisini.file.vo.FileVo;
import com.coisini.file.service.FileService;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.util.MultiValueMap;
import org.springframework.web.multipart.MultipartFile;
import java.util.List;
import java.util.stream.Collectors;
/**
* @Description 檔案上傳實現類
* @author coisini
* @date
* @Version 1.0
*/
@Service
public class FileServiceImpl implements FileService {
@Autowired
private Uploader uploader;
@Value("${file.domain}")
private String domain;
@Value("${file.serve-path:assets/**}")
private String servePath;
@Override
public List<FileVo> upload(MultiValueMap<String, MultipartFile> fileMap) {
return uploader.upload(fileMap).stream().map(item ->{
/**
* 這裡可以拿到檔案具體資訊
* 在此做資料庫儲存記錄操作等業務處理
*/
return transform(item);
}).collect(Collectors.toList());
}
/**
* 出參序列化
* @param fileModel
* @return
*/
private FileVo transform(FileModel fileModel) {
FileVo model = new FileVo();
BeanUtils.copyProperties(fileModel, model);
String s = servePath.split("/")[0];
model.setUrl(domain + s + "/" + fileModel.getPath());
return model;
}
}
上傳實現
- 檔案上傳配置類
import com.coisini.file.core.LocalUploader;
import com.coisini.file.core.Uploader;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order;
/**
* @Description 檔案上傳配置類
* @author coisini
* @date Sep 7, 2021
* @Version 1.0
*/
@Configuration
public class UploaderConfiguration {
/**
* @return 本地檔案上傳實現類
*/
@Bean
@Order
@ConditionalOnMissingBean
public Uploader uploader(){
return new LocalUploader();
}
}
- 檔案上傳服務介面
import com.coisini.file.model.FileModel;
import org.springframework.util.MultiValueMap;
import org.springframework.web.multipart.MultipartFile;
import java.util.List;
/**
* @Description 檔案上傳服務介面
* @author coisini
* @date Sep 7, 2021
* @Version 1.0
*/
public interface Uploader {
/**
* 上傳檔案
* @param fileMap 檔案map
* @return 檔案資料
*/
List<FileModel> upload(MultiValueMap<String, MultipartFile> fileMap);
}
- 本地上傳實現類
import com.coisini.file.config.FilePropertiesConfiguration;
import com.coisini.file.model.FileModel;
import com.coisini.file.util.FileUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.util.MultiValueMap;
import org.springframework.web.multipart.MultipartFile;
import javax.annotation.PostConstruct;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
/**
* @Description 本地上傳
* @author coisini
* @date Sep 7, 2021
* @Version 1.0
*/
@Slf4j
public class LocalUploader implements Uploader {
@Autowired
private FilePropertiesConfiguration filePropertiesConfiguration;
/**
* 初始化本地儲存
* 依賴注入完成後初始化
*/
@PostConstruct
public void initStoreDir() {
System.out.println("initStoreDir start:" + this.filePropertiesConfiguration.getStoreDir());
FileUtil.initStoreDir(this.filePropertiesConfiguration.getStoreDir());
System.out.println("initStoreDir end");
}
/**
* 檔案上傳
* @param fileMap 檔案map
* @return
*/
@Override
public List<FileModel> upload(MultiValueMap<String, MultipartFile> fileMap) {
// 檢查檔案
checkFileMap(fileMap);
return handleMultipartFiles(fileMap);
}
/**
* 檔案配置
* @return
*/
protected FilePropertiesConfiguration getFilePropertiesConfiguration() {
return filePropertiesConfiguration;
}
/**
* 單個檔案體積限制
* @return
*/
private long getSingleFileLimit() {
String singleLimit = getFilePropertiesConfiguration().getSingleLimit();
return FileUtil.parseSize(singleLimit);
}
/**
* 檢查檔案
* @param fileMap
*/
protected void checkFileMap(MultiValueMap<String, MultipartFile> fileMap){
if (fileMap.isEmpty()) {
throw new RuntimeException("file not found");
}
// 上傳檔案數量限制
int nums = getFilePropertiesConfiguration().getNums();
if (fileMap.size() > nums) {
throw new RuntimeException("too many files, amount of files must less than" + nums);
}
}
/**
* 檔案處理
* @param fileMap
* @return
*/
protected List<FileModel> handleMultipartFiles(MultiValueMap<String, MultipartFile> fileMap) {
long singleFileLimit = getSingleFileLimit();
List<FileModel> res = new ArrayList<>();
fileMap.keySet().forEach(key -> fileMap.get(key).forEach(file -> {
if (!file.isEmpty()) {
handleOneFile(res, singleFileLimit, file);
}
}));
return res;
}
/**
* 單檔案處理
* @param res
* @param singleFileLimit
* @param file
*/
private void handleOneFile(List<FileModel> res, long singleFileLimit, MultipartFile file) {
byte[] bytes = FileUtil.getFileBytes(file);
String[] include = getFilePropertiesConfiguration().getInclude();
String[] exclude = getFilePropertiesConfiguration().getExclude();
String ext = UploadHelper.checkOneFile(include, exclude, singleFileLimit, file.getOriginalFilename(), bytes.length);
String newFilename = UploadHelper.getNewFilename(ext);
String storePath = getStorePath(newFilename);
// 生成檔案的md5值
String md5 = FileUtil.getFileMD5(bytes);
FileModel fileModelData = FileModel.builder().
name(newFilename).
md5(md5).
key(file.getName()).
path(storePath).
size(bytes.length).
extension(ext).
build();
boolean ok = writeFile(bytes, newFilename);
if (ok) {
res.add(fileModelData);
}
}
/**
* 寫入儲存
* @param bytes
* @param newFilename
* @return
*/
protected boolean writeFile(byte[] bytes, String newFilename) {
// 獲取絕對路徑
String absolutePath =
FileUtil.getFileAbsolutePath(filePropertiesConfiguration.getStoreDir(), getStorePath(newFilename));
System.out.println("absolutePath:" + absolutePath);
try {
BufferedOutputStream stream =
new BufferedOutputStream(new FileOutputStream(new File(absolutePath)));
stream.write(bytes);
stream.close();
} catch (Exception e) {
System.out.println("write file error:" + e);
return false;
}
return true;
}
/**
* 獲取快取地址
* @param newFilename
* @return
*/
@SuppressWarnings("ResultOfMethodCallIgnored")
protected String getStorePath(String newFilename) {
Date now = new Date();
String format = new SimpleDateFormat("yyyy/MM/dd").format(now);
Path path = Paths.get(filePropertiesConfiguration.getStoreDir(), format).toAbsolutePath();
File file = new File(path.toString());
if (!file.exists()) {
file.mkdirs();
}
return Paths.get(format, newFilename).toString();
}
}
輔助類
- 檔案上傳Helper
import com.coisini.file.util.FileUtil;
import java.util.UUID;
/**
* @Description 檔案上傳Helper
* @author coisini
* @date Sep 7, 2021
* @Version 1.0
*/
public class UploadHelper {
/**
* 單個檔案檢查
* @param singleFileLimit 單個檔案大小限制
* @param originName 檔案原始名稱
* @param length 檔案大小
* @return 檔案的副檔名,例如: .jpg
*/
public static String checkOneFile(String[] include, String[] exclude, long singleFileLimit, String originName, int length) {
// 寫到了本地
String ext = FileUtil.getFileExt(originName);
// 檢測擴充套件
if (!UploadHelper.checkExt(include, exclude, ext)) {
throw new RuntimeException(ext + "檔案型別不支援");
}
// 檢測單個大小
if (length > singleFileLimit) {
throw new RuntimeException(originName + "檔案不能超過" + singleFileLimit);
}
return ext;
}
/**
* 檢查檔案字尾
* @param ext 字尾名
* @return 是否通過
*/
public static boolean checkExt(String[] include, String[] exclude, String ext) {
int inLen = include == null ? 0 : include.length;
int exLen = exclude == null ? 0 : exclude.length;
// 如果兩者都有取 include,有一者則用一者
if (inLen > 0 && exLen > 0) {
return UploadHelper.findInInclude(include, ext);
} else if (inLen > 0) {
// 有include,無exclude
return UploadHelper.findInInclude(include, ext);
} else if (exLen > 0) {
// 有exclude,無include
return UploadHelper.findInExclude(exclude, ext);
} else {
// 二者都沒有
return true;
}
}
/**
* 檢查允許的檔案型別
* @param include
* @param ext
* @return
*/
public static boolean findInInclude(String[] include, String ext) {
for (String s : include) {
if (s.equals(ext)) {
return true;
}
}
return false;
}
/**
* 檢查不允許的檔案型別
* @param exclude
* @param ext
* @return
*/
public static boolean findInExclude(String[] exclude, String ext) {
for (String s : exclude) {
if (s.equals(ext)) {
return true;
}
}
return false;
}
/**
* 獲得新檔案的名稱
* @param ext 檔案字尾
* @return 新名稱
*/
public static String getNewFilename(String ext) {
String uuid = UUID.randomUUID().toString().replace("-", "");
return uuid + ext;
}
}
- 檔案工具類
import org.springframework.util.DigestUtils;
import org.springframework.util.StringUtils;
import org.springframework.util.unit.DataSize;
import org.springframework.web.multipart.MultipartFile;
import java.io.File;
import java.nio.file.FileSystem;
import java.nio.file.FileSystems;
import java.nio.file.Path;
/**
* @Description 檔案工具類
* @author coisini
* @date Sep 7, 2021
* @Version 1.0
*/
public class FileUtil {
/**
* 獲取當前檔案系統
* @return
*/
public static FileSystem getDefaultFileSystem() {
return FileSystems.getDefault();
}
/**
* 是否絕對路徑
* @param str
* @return
*/
public static boolean isAbsolute(String str) {
Path path = getDefaultFileSystem().getPath(str);
return path.isAbsolute();
}
/**
* 初始化儲存資料夾
* @param dir
*/
@SuppressWarnings("ResultOfMethodCallIgnored")
public static void initStoreDir(String dir) {
String absDir;
if (isAbsolute(dir)) {
absDir = dir;
} else {
String cmd = getCmd();
Path path = getDefaultFileSystem().getPath(cmd, dir);
absDir = path.toAbsolutePath().toString();
}
File file = new File(absDir);
if (!file.exists()) {
file.mkdirs();
}
}
/**
* 獲取程式當前路徑
* @return
*/
public static String getCmd() {
return System.getProperty("user.dir");
}
/**
* 獲取檔案絕對路徑
* @param dir
* @param filename
* @return
*/
public static String getFileAbsolutePath(String dir, String filename) {
if (isAbsolute(dir)) {
return getDefaultFileSystem()
.getPath(dir, filename)
.toAbsolutePath().toString();
} else {
return getDefaultFileSystem()
.getPath(getCmd(), dir, filename)
.toAbsolutePath().toString();
}
}
/**
* 獲取副檔名
* @param filename
* @return
*/
public static String getFileExt(String filename) {
int index = filename.lastIndexOf('.');
return filename.substring(index);
}
/**
* 獲取檔案MD5值
* @param bytes
* @return
*/
public static String getFileMD5(byte[] bytes) {
return DigestUtils.md5DigestAsHex(bytes);
}
/**
* 檔案體積
* @param size
* @return
*/
public static Long parseSize(String size) {
DataSize singleLimitData = DataSize.parse(size);
return singleLimitData.toBytes();
}
/**
* 是否是絕對路徑
* @param path
* @return
*/
public static boolean isAbsolutePath(String path) {
if (StringUtils.isEmpty(path)) {
return false;
} else {
return '/' == path.charAt(0) || path.matches("^[a-zA-Z]:[/\\\\].*");
}
}
/**
* 檔案位元組
* @param file 檔案
* @return 位元組
*/
public static byte[] getFileBytes(MultipartFile file) {
byte[] bytes;
try {
bytes = file.getBytes();
} catch (Exception e) {
throw new RuntimeException("read file date failed");
}
return bytes;
}
}
實體
- 檔案具體資訊
import lombok.*;
/**
* @Description 檔案具體資訊,可儲存資料庫
* @author coisini
* @date Sep 7, 2021
* @Version 1.0
*/
@Setter
@Getter
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class FileModel {
/**
* url
*/
private String url;
/**
* key
*/
private String key;
/**
* 檔案路徑
*/
private String path;
/**
* 檔名稱
*/
private String name;
/**
* 副檔名,例:.jpg
*/
private String extension;
/**
* 檔案大小
*/
private Integer size;
/**
* md5值,防止上傳重複檔案
*/
private String md5;
}
- 檔案出參
import lombok.Data;
/**
* @Description 檔案出參
* @author coisini
* @date Sep 7, 2021
* @Version 1.0
*/
@Data
public class FileVo {
/**
* 檔案 key
*/
private String key;
/**
* 檔案路徑
*/
private String path;
/**
* 檔案 URL
*/
private String url;
}
上傳測試
- 上傳
- 檔案儲存位置為當前專案
/assets
目錄
檔案訪問
配置類
SpringBoot
訪問靜態資源有兩種方式:模板引擎和改變資源對映,這裡採用改變資源對映來實現Spring MVC
配置類
import com.coisini.file.util.FileUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import java.nio.file.FileSystems;
import java.nio.file.Path;
/**
* @Description Spring MVC 配置
* @author coisini
* @date Sep 7, 2021
* @Version 1.0
*/
@Configuration(proxyBeanMethods = false)
@Slf4j
public class WebConfiguration implements WebMvcConfigurer {
@Value("${file.store-dir:assets/}")
private String dir;
@Value("${file.serve-path:assets/**}")
private String servePath;
/**
* 跨域設定
*/
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedOriginPatterns("*")
.allowedMethods("GET", "HEAD", "POST", "PUT", "DELETE", "OPTIONS")
.allowCredentials(true)
.maxAge(3600)
.allowedHeaders("*");
}
/**
* 攔截處理請求資訊
* 新增檔案真實地址
* @param registry
*/
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler(getDirServePath())
.addResourceLocations("file:" + getAbsDir() + "/");
}
/**
* 獲取伺服器url
* @return
*/
private String getDirServePath() {
return servePath;
}
/**
* 獲得資料夾的絕對路徑
*/
private String getAbsDir() {
if (FileUtil.isAbsolutePath(dir)) {
return dir;
}
String cmd = System.getProperty("user.dir");
Path path = FileSystems.getDefault().getPath(cmd, dir);
return path.toAbsolutePath().toString();
}
}
- 訪問結果
專案原始碼
Gitee
: https://gitee.com/maggieq8324/java-learn-demo/tree/master/springboot-file-simple