14、SpringBoot实现文件上传与下载(数据库版)
需求
之前写过一个图片上传实现方法:https://www.cnblogs.com/phdeblog/p/13236363.html
不过这种方法局限性很大:
- 图片存储的位置写死,不可以灵活配置。
- 没有专门实现“下载”,虽然可以直接预览例如浏览器输入图片地址,http://localhost:8080/image/1.jpg,可以直接预览图片,但是如果想下载,必须右击选择下载到本地。
- 直接把文件放在项目工程里面,项目臃肿,服务器压力很大。
- 文件名写死,无法保留原文件的文件名。
现在新的需求是:
- 文件保存的路径可以配置。
- 可以通过文件名等标识符,下载指定文件。
- 保留文件原有的名称,在下载的时候可以指定新的文件名,也可以用原先的文件名。
- 可以指定只能上传特定格式的文件,例如word文档、压缩包、excel表格等。
思路
注意:
数据库只存放文件的描述信息(例如文件名、所在路径),不存文件本身。
上传流程:
(1)用户点击上传文件 ——> (2)传到后台服务器——>(3)初步校验,上传的文件不能为空——>(4)唯一性校验,如果你的项目只能存在一个文件,必须把已有的文件删去(可选)——> (5) 检查是否有同名文件,同名文件是否覆盖(可选)
——> (6) 开始上传文件 ——> (7) 检查文件类型是否满足需求——> (8) 用一个变量保留原有的名字,将文件写入服务器本地 ——> (9) 如果写入成功,将路径、新的文件名、旧的文件名、文件的功能 等等写入数据库。
下载流程:
从数据库取出指定文件的描述信息,描述信息里面有文件所在目录,用java的api获取文件对象,转化成字节写入response,返回给前端。
完整实现
依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.4.0</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</dependency>
SpringBoot版本
<parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.3.5.RELEASE</version> <relativePath/> <!-- lookup parent from repository --> </parent>
目录结构
文件上传工具类
文件上传工具类有三个,功能不一致。
FileUploadUtils
******可以在这里修改文件默认存放位置
上传文件,支持默认路径存储、也支持指定目录存储。
在SpringBoot还需要在配置文件中配置上传文件的大小上限,默认是2MB。
public class FileUploadUtils { /** * 默认大小 50M */ public static final long DEFAULT_MAX_SIZE = 50 * 1024 * 1024; /** * 默认的文件名最大长度 100 */ public static final int FILE_NAME_MAX = 100; /** * 默认上传的地址 */ private static String DEFAULT_BASE_FILE = "D:\\personalCode\\activemq-learn\\file-upload-learn\\src\\main\\resources\\upload"; /** * 按照默认的配置上床文件 * * @param file 文件 * @return 文件名 * @throws IOException */ public static final String upload(MultipartFile file) throws IOException { try { return upload(FileUploadUtils.DEFAULT_BASE_FILE, file, MimeTypeUtils.DEFAULT_ALLOWED_EXTENSION); } catch (Exception e) { throw new IOException(e.getMessage(), e); } } /** * 根据文件路径上传 * * @param baseDir 相对应用的基目录 * @param file 上传的文件 * @return 文件名称 * @throws IOException */ public static final String upload(String baseDir, MultipartFile file) throws IOException { try { return upload(baseDir, file, MimeTypeUtils.DEFAULT_ALLOWED_EXTENSION); } catch (Exception e) { throw new IOException(e.getMessage(), e); } } /** * 文件上传 * @param baseDir 相对应用的基目录 * @param file 上传的文件 * @param allowedExtension 上传文件类型 * @return 返回上传成功的文件名 * @throws FileSizeLimitExceededException 如果超出最大大小 * @throws IOException 比如读写文件出错时 */ public static final String upload(String baseDir, MultipartFile file, String[] allowedExtension) throws Exception { //合法性校验 assertAllowed(file, allowedExtension); String fileName = encodingFileName(file); File desc = getAbsoluteFile(baseDir, fileName); file.transferTo(desc); return desc.getAbsolutePath(); } private static final File getAbsoluteFile(String uploadDir, String fileName) throws IOException { File desc = new File(uploadDir + File.separator + fileName); if (!desc.getParentFile().exists()) { desc.getParentFile().mkdirs(); } if (!desc.exists()) { desc.createNewFile(); } return desc; } /** * 对文件名特殊处理一下 * * @param file 文件 * @return */ private static String encodingFileName(MultipartFile file) { SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd"); String datePath = simpleDateFormat.format(new Date()); return datePath + "-" + UUID.randomUUID().toString() + "." + getExtension(file); } /** * 文件合法性校验 * * @param file 上传的文件 * @return */ public static final void assertAllowed(MultipartFile file, String[] allowedExtension) throws Exception { if (file.getOriginalFilename() != null) { int fileNamelength = file.getOriginalFilename().length(); if (fileNamelength > FILE_NAME_MAX) { throw new Exception("文件名过长"); } } long size = file.getSize(); if (size > DEFAULT_MAX_SIZE) { throw new Exception("文件过大"); } String extension = getExtension(file); if (allowedExtension != null && !isAllowedExtension(extension, allowedExtension)) { throw new Exception("请上传指定类型的文件!"); } } /** * 判断MIME类型是否是允许的MIME类型 * * @param extension * @param allowedExtension * @return */ public static final boolean isAllowedExtension(String extension, String[] allowedExtension) { for (String str : allowedExtension) { if (str.equalsIgnoreCase(extension)) { return true; } } return false; } /** * 获取文件名的后缀 * * @param file 表单文件 * @return 后缀名 */ public static final String getExtension(MultipartFile file) { String fileName = file.getOriginalFilename(); String extension = null; if (fileName == null) { return null; } else { int index = indexOfExtension(fileName); extension = index == -1 ? "" : fileName.substring(index + 1); } if (StringUtils.isEmpty(extension)) { extension = MimeTypeUtils.getExtension(file.getContentType()); } return extension; } public static int indexOfLastSeparator(String filename) { if (filename == null) { return -1; } else { int lastUnixPos = filename.lastIndexOf(47); int lastWindowsPos = filename.lastIndexOf(92); return Math.max(lastUnixPos, lastWindowsPos); } } public static int indexOfExtension(String filename) { if (filename == null) { return -1; } else { int extensionPos = filename.lastIndexOf(46); int lastSeparator = indexOfLastSeparator(filename); return lastSeparator > extensionPos ? -1 : extensionPos; } } public void setDEFAULT_BASE_FILE(String DEFAULT_BASE_FILE) { FileUploadUtils.DEFAULT_BASE_FILE = DEFAULT_BASE_FILE; } public String getDEFAULT_BASE_FILE() { return DEFAULT_BASE_FILE; } }
FileUtils
******文件下载需要用到这边的writeByte
主要功能:删除文件、文件名校验、文件下载时进行字节流写入
public class FileUtils { //文件名正则校验 public static String FILENAME_PATTERN = "[a-zA-Z0-9_\\-\\|\\.\\u4e00-\\u9fa5]+"; public static void writeBytes(String filePath, OutputStream os) { FileInputStream fi = null; try { File file = new File(filePath); if (!file.exists()) { throw new FileNotFoundException(filePath); } fi = new FileInputStream(file); byte[] b = new byte[1024]; int length; while ((length = fi.read(b)) > 0) { os.write(b, 0, length); } } catch (Exception e) { e.printStackTrace(); } finally { if(os != null) { try { os.close(); }catch (IOException e) { e.printStackTrace(); } } if(fi != null) { try { fi.close(); }catch (IOException e) { e.printStackTrace(); } } } } /** * 删除文件 * @param filePath 文件路径 * @return 是否成功 */ public static boolean deleteFile(String filePath) { boolean flag = false; File file = new File(filePath); if (file.isFile() && file.exists()) { file.delete(); flag = true; } return flag; } /** * 文件名校验 * @param fileName 文件名 * @return true 正常, false 非法 */ public static boolean isValidName(String fileName) { return fileName.matches(FILENAME_PATTERN); } /** * 下载文件名重新编码 * * @param request 请求对象 * @param fileName 文件名 * @return 编码后的文件名 */ public static String setFileDownloadHeader(HttpServletRequest request, String fileName) throws UnsupportedEncodingException { final String agent = request.getHeader("USER-AGENT"); String filename = fileName; if (agent.contains("MSIE")) { // IE浏览器 filename = URLEncoder.encode(filename, "utf-8"); filename = filename.replace("+", " "); } else if (agent.contains("Firefox")) { // 火狐浏览器 filename = new String(fileName.getBytes(), "ISO8859-1"); } else if (agent.contains("Chrome")) { // google浏览器 filename = URLEncoder.encode(filename, "utf-8"); } else { // 其它浏览器 filename = URLEncoder.encode(filename, "utf-8"); } return filename; } }
MimeTypeUtils
******DEFAULT_ALLOWED_EXTENSION 可以指定允许文件上传类型
媒体工具类,支持指定上传文件格式。
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 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[] DEFAULT_ALLOWED_EXTENSION = { // 图片 "bmp", "gif", "jpg", "jpeg", "png", // word excel powerpoint "doc", "docx", "xls", "xlsx", "ppt", "pptx", "html", "htm", "txt", // 压缩文件 "rar", "zip", "gz", "bz2", // pdf "pdf"}; 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 ""; } } }
controller层
因为是测试demo,比较简陋,一般项目里会在controller层这边做异常捕捉,和统一返回格式。我这边就偷个懒,省了哈。
@RestController public class FileUploadController { @Autowired FileUploadService fileUploadService; //使用默认路径 @RequestMapping("/upload") public String upload(MultipartFile file) throws Exception { fileUploadService.upload(file, null); return null; } //自定义路径 @RequestMapping("/upload/template") public String uploadPlace(MultipartFile file) throws Exception { fileUploadService.upload(file, "H:\\upload"); return null; } //下载 @GetMapping("/download/file") public String downloadFile(HttpServletResponse response) throws IOException { fileUploadService.download(response, "上传模板"); return null; } }
entity实体类
@TableName("db_upload") @Data public class UploadEntity { @TableId(type = IdType.AUTO) private Long id; //存在本地的地址 private String location; //名称,业务中用到的名称,比如 ”档案模板“、”用户信息“、”登录记录“等等 private String name; //保留文件原来的名字 private String oldName; //描述(可以为空) private String description; private Date createTime; private Date updateTime; }
mapper
public interface UploadMapper extends BaseMapper<UploadEntity> { }
service层
public interface FileUploadService { void upload(MultipartFile file, String baseDir) throws Exception; void download(HttpServletResponse response , String newName) throws IOException; }
service实现层
@Service public class FileUploadServiceImpl implements FileUploadService { @Autowired UploadMapper uploadMapper; @Override public void upload(MultipartFile file, String baseDir) throws Exception { //就算什么也不传,controller层的file也不为空,但是originalFilename会为空(亲测) String originalFilename = file.getOriginalFilename(); if(originalFilename == null || "".equals(originalFilename)) { throw new Exception( "上传文件不能为空"); } //检测是否上传过同样的文件,如果有的话就删除。(这边可根据个人的情况修改逻辑) QueryWrapper<UploadEntity> queryWrapper = new QueryWrapper<>(); queryWrapper.eq("old_name", originalFilename); UploadEntity oldEntity = uploadMapper.selectOne(queryWrapper); //新的文件 UploadEntity uploadEntity = new UploadEntity(); uploadEntity.setCreateTime(new Date()); uploadEntity.setUpdateTime(new Date()); uploadEntity.setOldName(file.getOriginalFilename());
//这边可以根据业务修改,项目中不要写死 uploadEntity.setName("上传模板"); String fileLocation = null ; if(baseDir != null) { fileLocation = FileUploadUtils.upload(baseDir, file); }else { fileLocation = FileUploadUtils.upload(file); } uploadEntity.setLocation(fileLocation); uploadMapper.insert(uploadEntity); if(oldEntity != null) { //确保新的文件保存成功后,删除原有的同名文件(实体文件 and 数据库文件) FileUtils.deleteFile(oldEntity.getLocation()); uploadMapper.deleteById(oldEntity.getId()); } } @Override public void download(HttpServletResponse response, String newName) throws IOException { QueryWrapper<UploadEntity> queryWrapper = new QueryWrapper<>(); queryWrapper.eq("name", newName); UploadEntity uploadEntity = uploadMapper.selectOne(queryWrapper); response.setHeader("content-type", "application/octet-stream"); response.setContentType("application/octet-stream");
//这边可以设置文件下载时的名字,我这边用的是文件原本的名字,可以根据实际场景设置 response.setHeader("Content-Disposition", "attachment;filename=" + URLEncoder.encode(uploadEntity.getOldName(), "UTF-8")); FileUtils.writeBytes(uploadEntity.getLocation(), response.getOutputStream()); } }
启动类
@SpringBootApplication @MapperScan("com.dayrain.mapper") public class FileUploadLearnApplication { public static void main(String[] args) { SpringApplication.run(FileUploadLearnApplication.class, args); } }
配置文件
server: port: 8080 spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://ip:3306/upload?useUnicode=true&characterEncoding=UTF-8 username: root password: root servlet: multipart: max-file-size: 10MB #单次上传文件最大不超过10MB max-request-size: 100MB #文件总上传大小不超过100MB
SQL文件
DROP TABLE IF EXISTS `db_upload`;
CREATE TABLE `db_upload` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`location` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
`name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
`old_name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
`description` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
`create_time` datetime(0) NULL DEFAULT NULL,
`update_time` datetime(0) NULL DEFAULT NULL,
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 34 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Dynamic;
测试
如何用postman测试文件上传呢?
1、设置请求头
2、设置请求体
选择File
3、上传文件
前端上传代码
有朋友问前端代码,我就写了几个demo。因为不是专业的前端人员,如果有问题,欢迎指出。
表单
原生的html就可以实现文件的上传,只是不能对数据进行二次处理,且不是异步的,如果文件大,会比较耗时。
<html> <head></head> <body> <form id="upload" enctype="multipart/form-data" action="http://localhost:8080/upload" method="post"> <input type="file" name="file" /> <input type="submit" value="提交" /> </form> </body> </html>
ajax
如果是异步的话,并且前后端分离,那么后端要解决一下跨域问题。
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**").allowedOrigins("*")
.allowedHeaders("*")
.allowedMethods("*")
.maxAge(30*1000);
}
}
前端代码
<html> <head> </head> <body> <form id="upload" enctype="multipart/form-data" method="post"> <input type="file" name="file" id="pic" /> <!-- 多文件上传 --> <!-- <input type="file" name="file" id="pic" multiple="multipart"/> --> <input type="button" value="提交" onclick="uploadFile()" /> </form> </body> <script src="http://ajax.googleapis.com/ajax/libs/jquery/1.8.0/jquery.min.js"> </script> <script> function uploadFile() { //第一种 // formData = new FormData($('#upload')[0]); //第二种 formData = new FormData(); formData.append('file', $('#pic')[0].files[0]) $.ajax({ url: "http://localhost:8080/upload", type: "post", data: formData, processData: false, contentType: false, success: function (res) { alert('success') }, error: function (err) { alert('fail') } }) } </script> </html>
axios
axios是ajax的封装,因为用的人比较多,我也贴一下
<html> <head> </head> <body> <form id="upload" enctype="multipart/form-data" method="post"> <input type="file" name="file" id="pic" /> <!-- 多文件上传 --> <!-- <input type="file" name="file" id="pic" multiple="multipart"/> --> <input type="button" value="提交" onclick="uploadFile()" /> </form> </body> <script src="http://ajax.googleapis.com/ajax/libs/jquery/1.8.0/jquery.min.js"> </script> <script> function uploadFile() { //第一种 // formData = new FormData($('#upload')[0]); //第二种 formData = new FormData(); formData.append('file', $('#pic')[0].files[0]) $.ajax({ url: "http://localhost:8080/upload", type: "post", data: formData, processData: false, contentType: false, success: function (res) { alert('success') }, error: function (err) { alert('fail') } }) } </script> </html>
前端下载代码
项目中实现下载功能通常有两种方法。
方法一:
前端不做任何处理,直接访问后台的地址,比如本文中的 http://localhost:8080/download/file,后台返回的是文件的输出流,浏览器会自动转化成文件,开始下载。
(本文就是按照这种方式实现的,可以看示例中的 “controller层” 第三个接口)
方法二:
后端不做处理,只提供数据接口,前端接收到数据后,通过js将数据整理并转成对应格式的文件,比如doc、pdf之类的。
推荐:
推荐使用第一种方法,因为数据量比较大时,通过前端导出的话,后台需要向前台传大量的数据,压力比较大。不如后台处理,直接转化成文件流交给浏览器处理,还省了rpc的开销。
总结
上述代码以经过简单测试,无中文乱码现象,逻辑基本满足目前项目使用。
因为项目用到文件的地方不是很多,所以就把文件和项目放在一个服务器里面,不涉及远程调用。
如果文件上传下载使用频繁,例如电子档案系统,电子书,网盘等等,需要考虑使用专门的文件服务器,拆分业务,缓解服务端压力。
如果对您有帮助,欢迎给在下点个推荐。
如有错误,恳请批评指正!