菜单
- 一、创建Package并命名为module
- 二、在module下创建package并命名为file
- 三、在创建File类
- 四、创建接口Uploader
- 五、创建接口UploadHandler
- 六、创建FileUtil
- 7、创建FileConstant
- 8、创建AbstractUploader
- 9、创建FileProperties
- 10、在common/factory创建YamlPropertySourceFactory
- 11、创建LocalUploader
- 12、QiniuUploader
- 13、UploaderConfiguration
- 14、创建config.yml
- 15、创建FileBO
- 16、在controller创建FileController
- 17创建FileDO
- 18创建BaseModel
- 19在model下创建FileDO
- 20创建FileMapper
- 21创建Service
一、创建Package并命名为module
二、在module下创建package并命名为file
三、在创建File类
在module/file创建类File
package com.fcors.fcors.module.file;
import lombok.*;
/**
* @author pedro@TaleLin
*/
@Setter
@Getter
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class File {
/**
* 真实url
*/
private String url;
/**
* 前端上传的key
*/
private String key;
/**
* 若 local,表示文件路径
*/
private String path;
/**
* LOCAL REMOTE
*/
private String type;
/**
* 文件名称
*/
private String name;
/**
* 扩展名,例:.jpg
*/
private String extension;
/**
* 文件大小
*/
private Integer size;
/**
* md5值,防止上传重复文件
*/
private String md5;
}
四、创建接口Uploader
在module/file创建接口Uploader
package com.fcors.fcors.module.file;
import org.springframework.util.MultiValueMap;
import org.springframework.web.multipart.MultipartFile;
import java.util.List;
/**
* 文件上传服务接口
*
* @author pedro@TaleLin
*/
public interface Uploader {
/**
* 上传文件
*
* @param fileMap 文件map
* @return 文件数据
*/
List<File> upload(MultiValueMap<String, MultipartFile> fileMap);
/**
* 上传文件
*
* @param fileMap 文件map
* @param uploadHandler 预处理器
* @return 文件数据
*/
List<File> upload(MultiValueMap<String, MultipartFile> fileMap, UploadHandler uploadHandler);
}
五、创建接口UploadHandler
在module/file下创建接口UploadHandler
package com.fcors.fcors.module.file;
import org.springframework.util.MultiValueMap;
import org.springframework.web.multipart.MultipartFile;
import java.util.List;
/**
* 文件前预处理器
*
* @author pedro@TaleLin
*/
public interface UploadHandler {
/**
* 在文件写入本地或者上传到云之前调用此方法
*
* @return 是否上传,若返回false,则不上传
*/
boolean preHandle(File file);
/**
* 在文件写入本地或者上传到云之后调用此方法
*/
void afterHandle(File file);
}
六、创建FileUtil
在module/file下创建类FileUtil
package com.fcors.fcors.module.file;
import org.springframework.util.DigestUtils;
import org.springframework.util.unit.DataSize;
import java.io.File;
import java.nio.file.FileSystem;
import java.nio.file.FileSystems;
import java.nio.file.Path;
/**
* @author pedro@TaleLin
* @author colorful@TaleLin
*/
public class FileUtil {
public static FileSystem getDefaultFileSystem() {
return FileSystems.getDefault();
}
public static boolean isAbsolute(String str) {
Path path = getDefaultFileSystem().getPath(str);
return path.isAbsolute();
}
@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();
}
java.io.File file = new File(absDir);
if (!file.exists()) {
file.mkdirs();
}
}
public static String getCmd() {
return System.getProperty("user.dir");
}
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();
}
}
public static String getFileExt(String filename) {
int index = filename.lastIndexOf('.');
return filename.substring(index);
}
public static String getFileMD5(byte[] bytes) {
return DigestUtils.md5DigestAsHex(bytes);
}
public static Long parseSize(String size) {
DataSize singleLimitData = DataSize.parse(size);
return singleLimitData.toBytes();
}
}
7、创建FileConstant
在module/file下创建类 FileConstant
package com.fcors.fcors.module.file;
/**
* 文件相关常量值
*
* @author pedro@TaleLin
*/
public class FileConstant {
/**
* 本地文件
*/
public static final String LOCAL = "LOCAL";
/**
* 远程文件,例如OSS
*/
public static final String REMOTE = "REMOTE";
}
8、创建AbstractUploader
在module/file下创建类 AbstractUploader
package io.github.talelin.latticy.module.file;
import io.github.talelin.autoconfigure.exception.*;
import org.springframework.util.MultiValueMap;
import org.springframework.web.multipart.MultipartFile;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
/**
* 文件上传类的基类
* 模版模式
*
* @author pedro@TaleLin
* @author Juzi@TaleLin
* @author colorful@TaleLin
*/
public abstract class AbstractUploader implements Uploader {
private UploadHandler uploadHandler;
@Override
public List<File> upload(MultiValueMap<String, MultipartFile> fileMap) {
checkFileMap(fileMap);
// 得到单个文件的大小限制
// 本地存储需先初始化存储文件夹
return handleMultipartFiles(fileMap);
}
@Override
public List<File> upload(MultiValueMap<String, MultipartFile> fileMap, UploadHandler uploadHandler) {
this.uploadHandler = uploadHandler;
return this.upload(fileMap);
}
protected List<File> handleMultipartFiles(MultiValueMap<String, MultipartFile> fileMap) {
long singleFileLimit = getSingleFileLimit();
List<File> res = new ArrayList<>();
fileMap.keySet().forEach(key -> fileMap.get(key).forEach(file -> {
if (!file.isEmpty()) {
handleOneFile0(res, singleFileLimit, file);
}
}));
return res;
}
private void handleOneFile0(List<File> res, long singleFileLimit, MultipartFile file) {
byte[] bytes = getFileBytes(file);
String[] include = getFileProperties().getInclude();
String[] exclude = getFileProperties().getExclude();
String ext = checkOneFile(include, exclude, singleFileLimit, file.getOriginalFilename(), bytes.length);
String newFilename = getNewFilename(ext);
String storePath = getStorePath(newFilename);
// 生成文件的md5值
String md5 = FileUtil.getFileMD5(bytes);
File fileData = File.builder().
name(newFilename).
md5(md5).
key(file.getName()).
path(storePath).
size(bytes.length).
type(getFileType()).
extension(ext).
build();
// 如果预处理器不为空,且处理结果为false,直接返回, 否则处理
if (uploadHandler != null && !uploadHandler.preHandle(fileData)) {
return;
}
boolean ok = handleOneFile(bytes, newFilename);
if (ok) {
res.add(fileData);
// 上传到本地或云上成功之后,调用afterHandle
if (uploadHandler != null) {
uploadHandler.afterHandle(fileData);
}
}
}
private long getSingleFileLimit() {
String singleLimit = getFileProperties().getSingleLimit();
return FileUtil.parseSize(singleLimit);
}
/**
* 得到文件配置
*
* @return 文件配置
*/
protected abstract FileProperties getFileProperties();
/**
* 处理一个文件
*/
protected abstract boolean handleOneFile(byte[] bytes, String newFilename);
/**
* 返回文件路径
*
* @param newFilename 文件名
* @return 文件路径
*/
protected abstract String getStorePath(String newFilename);
/**
* 返回文件存储位置类型
*
* @return LOCAL | REMOTE
*/
protected abstract String getFileType();
/**
* 获得新文件的名称
*
* @param ext 文件后缀
* @return 新名称
*/
protected String getNewFilename(String ext) {
String uuid = UUID.randomUUID().toString().replace("-", "");
return uuid + ext;
}
/**
* 检查文件
*/
protected void checkFileMap(MultiValueMap<String, MultipartFile> fileMap) {
if (fileMap.isEmpty()) {
throw new NotFoundException(10026);
}
int nums = getFileProperties().getNums();
if (fileMap.size() > nums) {
throw new FileTooManyException(10121);
}
}
/**
* 获得文件的字节
*
* @param file 文件
* @return 字节
*/
protected byte[] getFileBytes(MultipartFile file) {
byte[] bytes;
try {
bytes = file.getBytes();
} catch (Exception e) {
throw new FailedException(10190, "read file date failed");
}
return bytes;
}
/**
* 单个文件检查
*
* @param singleFileLimit 单个文件大小限制
* @param originName 文件原始名称
* @param length 文件大小
* @return 文件的扩展名,例如: .jpg
*/
protected String checkOneFile(String[] include, String[] exclude, long singleFileLimit, String originName, int length) {
// 写到了本地
String ext = FileUtil.getFileExt(originName);
// 检测扩展
if (!this.checkExt(include, exclude, ext)) {
throw new FileExtensionException(ext + "文件类型不支持");
}
// 检测单个大小
if (length > singleFileLimit) {
throw new FileTooLargeException(originName + "文件不能超过" + singleFileLimit);
}
return ext;
}
/**
* 检查文件后缀
*
* @param ext 后缀名
* @return 是否通过
*/
protected 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 this.findInInclude(include, ext);
} else if (inLen > 0) {
// 有include,无exclude
return this.findInInclude(include, ext);
} else if (exLen > 0) {
// 有exclude,无include
return this.findInExclude(exclude, ext);
} else {
// 二者都没有
return true;
}
}
protected boolean findInInclude(String[] include, String ext) {
for (String s : include) {
if (s.equals(ext)) {
return true;
}
}
return false;
}
protected boolean findInExclude(String[] exclude, String ext) {
for (String s : exclude) {
if (s.equals(ext)) {
return true;
}
}
return false;
}
}
9、创建FileProperties
package com.fcors.fcors.module.file;
import com.fcors.fcors.common.factory.YamlPropertySourceFactory;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.PropertySource;
import org.springframework.stereotype.Component;
/**
* @author pedro@TaleLin
*/
@Component
@ConfigurationProperties("lin.file")
@PropertySource(
value = "classpath:config/config.yml",
encoding = "UTF-8", factory = YamlPropertySourceFactory.class)
public class FileProperties {
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;
/**
* 文件存储路径
*/
private String servePath = "assets/**";
public String getServePath() {
return servePath;
}
public void setServePath(String servePath) {
this.servePath = servePath;
}
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;
}
}
10、在common/factory创建YamlPropertySourceFactory
package com.fcors.fcors.common.factory;
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;
public class YamlPropertySourceFactory 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);
}
}
11、创建LocalUploader
在extension/file中创建类
package com.fcors.fcors.extension.file;
import com.fcors.fcors.module.file.AbstractUploader;
import com.fcors.fcors.module.file.FileConstant;
import com.fcors.fcors.module.file.FileProperties;
import com.fcors.fcors.module.file.FileUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
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.Date;
/**
* 文件上传服务默认实现,上传到本地
*
* @author pedro@TaleLin
*/
@Slf4j
public class LocalUploader extends AbstractUploader {
@Autowired
private FileProperties fileProperties;
@PostConstruct
public void initStoreDir() {
// 本地存储需先初始化存储文件夹
FileUtil.initStoreDir(this.fileProperties.getStoreDir());
}
@Override
protected boolean handleOneFile(byte[] bytes, String newFilename) {
String absolutePath =
FileUtil.getFileAbsolutePath(fileProperties.getStoreDir(), getStorePath(newFilename));
try {
BufferedOutputStream stream =
new BufferedOutputStream(new FileOutputStream(new java.io.File(absolutePath)));
stream.write(bytes);
stream.close();
} catch (Exception e) {
log.error("write file to local err:", e);
// throw new FailedException(10190);
return false;
}
return true;
}
@Override
protected FileProperties getFileProperties() {
return fileProperties;
}
@SuppressWarnings("ResultOfMethodCallIgnored")
@Override
protected String getStorePath(String newFilename) {
Date now = new Date();
String format = new SimpleDateFormat("yyyy/MM/dd").format(now);
Path path = Paths.get(fileProperties.getStoreDir(), format).toAbsolutePath();
java.io.File file = new File(path.toString());
if (!file.exists()) {
file.mkdirs();
}
return Paths.get(format, newFilename).toString();
}
@Override
protected String getFileType() {
return FileConstant.LOCAL;
}
}
12、QiniuUploader
在extension/file中创建类
package com.fcors.fcors.extension.file;
import com.fcors.fcors.module.file.FileConstant;
import com.qiniu.common.QiniuException;
import com.qiniu.http.Response;
import com.qiniu.storage.Configuration;
import com.qiniu.storage.Region;
import com.qiniu.storage.UploadManager;
import com.fcors.fcors.module.file.AbstractUploader;
import com.fcors.fcors.module.file.FileProperties;
import com.qiniu.util.Auth;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import java.io.ByteArrayInputStream;
@Slf4j
public class QiniuUploader extends AbstractUploader {
@Autowired
private FileProperties fileProperties;
@Value("${lin.file.qiniuyun.access-key}")
private String accessKey;
@Value("${lin.file.qiniuyun.secret-key}")
private String secretKey;
@Value("${lin.file.qiniuyun.bucket}")
private String bucket;
private UploadManager uploadManager;
private String upToken;
public void initUploadManager() {
Configuration cfg = new Configuration(Region.region2());
uploadManager = new UploadManager(cfg);
Auth auth = Auth.create(accessKey, secretKey);
upToken = auth.uploadToken(bucket);
}
@Override
protected FileProperties getFileProperties() {
return fileProperties;
}
@Override
protected String getStorePath(String newFilename) {
return fileProperties.getDomain() + newFilename;
}
@Override
protected String getFileType() {
return FileConstant.REMOTE;
}
/**
* 处理一个文件数据
*
* @param bytes 文件数据,比特流
* @param newFilename 新文件名称
* @return 处理是否成功,如果出现异常则返回 false,避免把失败的写入数据库
*/
@Override
protected boolean handleOneFile(byte[] bytes, String newFilename) {
initUploadManager();
ByteArrayInputStream byteInputStream = new ByteArrayInputStream(bytes);
try {
Response response = uploadManager.put(byteInputStream, newFilename, upToken, null, null);
log.info(response.toString());
return response.isOK();
} catch (QiniuException ex) {
Response r = ex.response;
log.error("qiniuyun upload file error: {}", r.error);
return false;
}
}
}
13、UploaderConfiguration
在extension/file中创建类 UploaderConfiguration
package com.fcors.fcors.extension.file;
import com.fcors.fcors.module.file.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;
/**
* 文件上传配置类
*
* @author Juzi@TaleLin
* @author colorful@TaleLin
*/
@Configuration(proxyBeanMethods = false)
public class UploaderConfiguration {
/**
* @return 本地文件上传实现类
*/
@Bean
@Order
@ConditionalOnMissingBean
public Uploader uploader(){
return new LocalUploader();
}
}
14、创建config.yml
在resource/config创建config.yml
#文件上传配置
lin:
file:
# 文件服务域名
domain: http://localhost:8082/
# 排除文件类型
exclude:
# 包括文件类型
include:
- .jpg
- .png
- .jpeg
# 文件最大数量
nums: 10
# 服务器文件路径
serve-path: assets/**
# 单个文件最大体积
single-limit: 2MB
# 本地文件保存位置
store-dir: assets/
spring:
servlet:
multipart:
# 总体文件最大体积(只能从max-file-size设置总体文件的大小)
max-file-size: 20MB
15、创建FileBO
package com.fcors.fcors.bo;
import lombok.Data;
/**
* @author pedro@TaleLin
* @author Juzi@TaleLin
*/
@Data
public class FileBO {
/**
* 文件 id
*/
private Integer id;
/**
* 文件 key,上传时指定的
*/
private String key;
/**
* 文件路径
*/
private String path;
/**
* 文件 URL
*/
private String url;
}
16、在controller创建FileController
/**
* @author pedro@TaleLin
* @author Juzi@TaleLin
*/
@RestController
@RequestMapping("/cms/file")
public class FileController {
@Autowired
private FileService fileService;
/**
* 文件上传
*
* @param multipartHttpServletRequest 携带文件的 request
* @return 文件信息
*/
@PostMapping
@LoginRequired
public List<FileBO> upload(MultipartHttpServletRequest multipartHttpServletRequest) {
MultiValueMap<String, MultipartFile> fileMap =
multipartHttpServletRequest.getMultiFileMap();
return fileService.upload(fileMap);
}
}
17创建FileDO
18创建BaseModel
@Data
public class BaseModel {
@TableId(value = "id", type = IdType.AUTO)
private Integer id;
@JsonIgnore
private Date createTime;
@JsonIgnore
private Date updateTime;
@TableLogic
@JsonIgnore
private Date deleteTime;
}
19在model下创建FileDO
@Data
@TableName("lin_file")
@EqualsAndHashCode(callSuper = true)
public class FileDO extends BaseModel implements Serializable {
private static final long serialVersionUID = -3203293656352763490L;
private String path;
/**
* LOCAL REMOTE
*/
private String type;
private String name;
private String extension;
private Integer size;
/**
* md5值,防止上传重复文件
*/
private String md5;
}
20创建FileMapper
package com.fcors.fcors.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.fcors.fcors.model.FileDO;
import org.apache.ibatis.annotations.Param;
import org.springframework.stereotype.Repository;
@Repository
public interface FileMapper extends BaseMapper<FileDO> {
FileDO selectByMd5(@Param("md5") String md5);
int selectCountByMd5(@Param("md5") String md5);
}
21创建Service
21.1接口FileService
package com.fcors.fcors.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.fcors.fcors.bo.FileBO;
import com.fcors.fcors.model.FileDO;
import org.springframework.util.MultiValueMap;
import org.springframework.web.multipart.MultipartFile;
import java.util.List;
/**
* @author pedro@TaleLin
*/
public interface FileService extends IService<FileDO> {
/**
* 上传文件
*
* @param fileMap 文件map
* @return 文件数据
*/
List<FileBO> upload(MultiValueMap<String, MultipartFile> fileMap);
/**
* 通过md5检查文件是否存在
*
* @param md5 md5
* @return true 表示已存在
*/
boolean checkFileExistByMd5(String md5);
}
21.2实现类FileServiceImpl
package com.fcors.fcors.service;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.fcors.fcors.bo.FileBO;
import com.fcors.fcors.mapper.FileMapper;
import com.fcors.fcors.model.FileDO;
import com.fcors.fcors.module.file.*;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.util.MultiValueMap;
import org.springframework.web.multipart.MultipartFile;
import java.util.ArrayList;
import java.util.List;
@Service
public class FileServiceImpl extends ServiceImpl<FileMapper, FileDO> implements FileService {
@Autowired
private Uploader uploader;
/**
* 文件上传配置信息
*/
@Autowired
private FileProperties fileProperties;
/**
* 为什么不做批量插入
* 1. 文件上传的数量一般不多,3个左右
* 2. 批量插入不能得到数据的id字段,不利于直接返回数据
* 3. 批量插入也仅仅只是一条sql语句的事情,如果真的需要,可以自行尝试一下
*/
@Override
public List<FileBO> upload(MultiValueMap<String, MultipartFile> fileMap) {
List<FileBO> res = new ArrayList<>();
uploader.upload(fileMap, new UploadHandler() {
@Override
public boolean preHandle(File file) {
FileDO found = baseMapper.selectByMd5(file.getMd5());
// 数据库中不存在,存储操作放在上传到本地或云上之后,
// 修复issue131:https://github.com/TaleLin/lin-cms-spring-boot/issues/131
if (found == null) {
return true;
}
// 已存在,则直接转化返回
res.add(transformDoToBo(found, file.getKey()));
return false;
}
@Override
public void afterHandle(File file) {
// 保存到数据库, 修复issue131:https://github.com/TaleLin/lin-cms-spring-boot/issues/131
FileDO fileDO = new FileDO();
BeanUtils.copyProperties(file, fileDO);
getBaseMapper().insert(fileDO);
res.add(transformDoToBo(fileDO, file.getKey()));
}
});
return res;
}
@Override
public boolean checkFileExistByMd5(String md5) {
return this.getBaseMapper().selectCountByMd5(md5) > 0;
}
private FileBO transformDoToBo(FileDO file, String key) {
FileBO bo = new FileBO();
BeanUtils.copyProperties(file, bo);
if (file.getType().equals(FileConstant.LOCAL)) {
String s = fileProperties.getServePath().split("/")[0];
// replaceAll 是将 windows 平台下的 \ 替换为 /
if(System.getProperties().getProperty("os.name").toUpperCase().contains("WINDOWS")){
bo.setUrl(fileProperties.getDomain() + s + "/" + file.getPath().replaceAll("\\\\","/"));
}else {
bo.setUrl(fileProperties.getDomain() + s + "/" + file.getPath());
}
} else {
bo.setUrl(file.getPath());
}
bo.setKey(key);
return bo;
}
}