前言
记录下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();
}
}
- 访问结果
项目源码
Github
: https://github.com/Maggieq8324/java-learn-demo/tree/master/springboot-file-simple
Gitee
: https://gitee.com/maggieq8324/java-learn-demo/tree/master/springboot-file-simple
![](https://img-blog.csdnimg.cn/d1b51b924c4d4ea88f8fecaa32708d63.jpg)