14、SpringBoot实现文件上传与下载(数据库版) 您所在的位置:网站首页 java中文件上传下载生成 14、SpringBoot实现文件上传与下载(数据库版)

14、SpringBoot实现文件上传与下载(数据库版)

2024-07-11 17:30| 来源: 网络整理| 查看: 265

需求

之前写过一个图片上传实现方法: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,返回给前端。

完整实现 依赖 org.springframework.boot spring-boot-starter-web org.projectlombok lombok true com.baomidou mybatis-plus-boot-starter 3.4.0 mysql mysql-connector-java org.springframework.boot spring-boot-starter-test test org.junit.vintage junit-vintage-engine

SpringBoot版本

org.springframework.boot spring-boot-starter-parent 2.3.5.RELEASE 目录结构

 

 

 文件上传工具类

文件上传工具类有三个,功能不一致。

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 { } 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 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 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就可以实现文件的上传,只是不能对数据进行二次处理,且不是异步的,如果文件大,会比较耗时。

ajax

如果是异步的话,并且前后端分离,那么后端要解决一下跨域问题。

@Configuration public class WebMvcConfig implements WebMvcConfigurer { @Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping("/**").allowedOrigins("*") .allowedHeaders("*") .allowedMethods("*") .maxAge(30*1000); } }

前端代码

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') } }) } axios

axios是ajax的封装,因为用的人比较多,我也贴一下

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') } }) } 前端下载代码

项目中实现下载功能通常有两种方法。

方法一:

前端不做任何处理,直接访问后台的地址,比如本文中的  http://localhost:8080/download/file,后台返回的是文件的输出流,浏览器会自动转化成文件,开始下载。

(本文就是按照这种方式实现的,可以看示例中的  “controller层” 第三个接口)

方法二:

后端不做处理,只提供数据接口,前端接收到数据后,通过js将数据整理并转成对应格式的文件,比如doc、pdf之类的。

推荐:

推荐使用第一种方法,因为数据量比较大时,通过前端导出的话,后台需要向前台传大量的数据,压力比较大。不如后台处理,直接转化成文件流交给浏览器处理,还省了rpc的开销。

总结

上述代码以经过简单测试,无中文乱码现象,逻辑基本满足目前项目使用。

因为项目用到文件的地方不是很多,所以就把文件和项目放在一个服务器里面,不涉及远程调用。

如果文件上传下载使用频繁,例如电子档案系统,电子书,网盘等等,需要考虑使用专门的文件服务器,拆分业务,缓解服务端压力。

 

如果对您有帮助,欢迎给在下点个推荐。

如有错误,恳请批评指正!

 

 


【本文地址】

公司简介

联系我们

今日新闻

    推荐新闻

    专题文章
      CopyRight 2018-2019 实验室设备网 版权所有