基于Springboot和Mybatis的文件上传与下载
0. 前言
因为太久太久没有碰项目了(上一次还是2021年8月开发个人博客的时候),所以这一次打算从头到尾把整个学习和搭建的过程记录下来。
1. 简介
这是一个基于SpringBoot和Mybatis的企业级文件上传下载的实战项目,没有太多前端和UI的东西。
2. 链接
【编程不良人】基于SpringBoot和Mybatis企业级文件上传下载项目实战
1. 设计
1. 需求
- 用户登陆,展示用户的所有文件(文件如果是图片则在页面中显示图片)
- 完成文件的下载和在线打开(在线打开不计入下载次数)
- 在一张页面中完成文件的上传功能,上传的目录要根据日期,每天创建一个文件夹(文件夹统一命名格式:“yyyy-MM-dd”),上传文成后要跳转到查询所有页面
2. 页面图
3. 库表设计
根据需求,可以简单把库表设计成:1. 用户表(用户信息:ID,用户名,密码);2. 文件表(文件信息:用户ID,文件名…)
DROP TABLE IF EXISTS `t_files`;
CREATE TABLE `t_files` (`id` int(8) NOT NULL,`oldFileName` varchar(200) DEFAULT NULL,`newFileName` varchar(300) DEFAULT NULL,`ext` varchar(20) DEFAULT NULL,`path` varchar(300) DEFAULT NULL,`size` varchar(200) DEFAULT NULL,`type` varchar(120) DEFAULT NULL,`isImg` varchar(8) DEFAULT NULL,`downcounts` int(6) DEFAULT NULL,`uploadTime` datetime DEFAULT NULL,PRIMARY KEY (`id`),KEY `userId_idx` (`userId`),CONSTRAINT `userId` FOREIGN KEY (`userId`) REFERENCES `t_user` (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;DROP TABLE IF EXISTS `t_user`;
CREATE TABLE `t_user` (`id` int(8) NOT NULL,`username` varchar(80) DEFAULT NULL,`password` varchar(80) DEFAULT NULL,PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
为后续方便,可以把db.sql
放到resources/
下面
2. 项目创建
因为不用华丽的前端,所以只需要创建一个简单的Springboot项目即可。
1. 依赖
在创建项目的时候导入基本依赖:
2. 没用文件
把用不到的文件删掉
本项目为了快捷演示,没有作任何测试,下面把
spring-boot-starter-test
依赖删掉了,所以可以把/test/java
下的文件都删掉
3. pom.xml(版本
SpringBoot
版本,视频教程比较老,里面用的版本是2.2.5
,现在用的是2.6.3
MySQL
版本一般用5.x
版本,这里改成5.1.47
,同时还可以把<scope>runtime</scope>
去掉,scope区别Lombok
版本用1.8.20
,同时去掉<optional>true</optional>
spring-boot-starter-test
,关于测试也可以去掉(不知道为什么)
3. 配置
application.properties
# 应用名在微服务架构中至关重要
spring.application.name=files
server.port=8989
server.servlet.context-path=/filesspring.thymeleaf.cache=false
spring.thymeleaf.suffix=.html
spring.thymeleaf.encoding=UTF-8
spring.thymeleaf.prefix=classpath:/templates/
spring.web.resources.static-locations=classpath:/templates/,classpath:/static/spring.datasource.type=com.alibaba.druid.pool.DruidDataSource
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/files?characterEncoding=UTF-8
spring.datasource.username=root
spring.datasource.password=passwordmybatis.mapper-locations=classpath:/com/zzw/mapper/*.xml
mybatis.type-aliases-package=com.zzw.entity# 日志
logging.level.root=info
logging.level.com.zzw.dao=debug
记得添加一下数据库连接池
druid
的依赖数据库
username
和password
换成自己的url
里的数据库名称使用之前创建的需要创建
dao
,entity
,mapper
包此时运行项目,若能正常启动,说明环境没有问题
在main所在类上面添加
@MapperScan("com.zzw.dao")
,为了之后扫描dao所在的包
4. 三层简单开发
这个项目主要是学习文件上传和下载,所以不在前端和UI花费太多时间,一切从简。
以下的三层简单开发,代码块中实现了文件查询
和文件上传
功能。为避免重复粘贴,文件下载
,在线打开
和文件删除
功能的代码不在这一章节贴出来。
1. Entity
1. User
@Data
@AllArgsConstructor
@NoArgsConstructor
@ToString
@Accessors(chain = true)
public class User {private Integer id;private String username;private String password;
}
注意@Accessors(chain = true)
这个注解可以实现set的链式编程:
Lombok插件@Accessors(chain = true)开启链式开发
2. UserFile
@Data
@AllArgsConstructor
@NoArgsConstructor
@ToString
@Accessors(chain = true)
public class UserFile {private Integer id;private String oldFileName;private String newFileName;private String ext;private String path;private String size;private String type;private String isImg;private Integer downcounts;private Date uploadTime;private Integer userId;
}
2. DAO
注意DAO都是interface
,并且要记得添加@Mapper
1. UserDAO
@Mapper
public interface UserDAO {User login(User user);
}
2. UserFileDAO
public interface UserFileDAO {// 根据用户的ID获取用户的文件列表List<UserFile> findByUserId(Integer id);// 存储图片信息void save(UserFile userFile);
}
3. Mapper
1. UserDAOMapper
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.zzw.dao.UserDAO"><!-- login --><select id="login" parameterType="User" resultType="User">select id, username, passwordfrom files.t_user where username = #{username} and password = #{password}</select></mapper>
注意
namespace
要写需要map的DAO
所在位置
2. UserFileDAOMapper
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.zzw.dao.UserFileDAO"><!-- find user file list --><select id="findByUserId" parameterType="Integer" resultType="UserFile">select id, oldFileName, newFileName, ext, path, size, type, isImg, downcounts, uploadTime, userIdfrom files.t_fileswhere userId=#{id}</select><!-- save image --><insert id="save" parameterType="UserFile" useGeneratedKeys="true" keyProperty="id">insert into files.t_filesvalues (#{id}, #{oldFileName}, #{newFileName}, #{ext},#{path}, #{size}, #{type}, #{isImg},#{downcounts}, #{uploadTime}, #{userId})</insert></mapper>
4. Service
注意service有service interface和serviceImpl,impl上需要加@Service
注解
1. UserService
UserService
public interface UserService {User login(User user);
}
UserServiceImpl
@Service
@Transactional
public class UserServiceImpl implements UserService {@Autowiredprivate UserDAO userDAO;@Override@Transactional(propagation = Propagation.SUPPORTS)public User login(User user) {return userDAO.login(user);}
}
这里有一个点需要注意:
userDAO
在使用@Autowired
来自动注入时,Intellij会提示:
而如果在DAO上添加@Repository
,这个错误提示就会自动消除。尽管我们都知道 Dao 层的 Bean 实际上都是有的,并且可以设置关闭这恼人的提示,但是我们有没有想过为什么 Intellij 就找不到这个 Bean 呢?可参考以下文章:
https://blog.csdn.net/tianshan2010/article/details/105889133/
正确做法是:DAO层不需要加@Repository
注解。
2. UserFileService
UserFileService
public interface UserFileService {List<UserFile> findByUserId(Integer id);void save(UserFile userFile);
}
UserFileServiceImpl
@Service
@Transactional
public class UserFileServiceImpl implements UserFileService{@Autowiredprivate UserFileDAO userFileDAO;@Overridepublic List<UserFile> findByUserId(Integer id) {return userFileDAO.findByUserId(id);}@Overridepublic void save(UserFile userFile) {String isImg = userFile.getType().startsWith("image") ? "Yes" : "No";userFile.setIsImg(isImg);userFile.setDowncounts(0);userFile.setUploadTime(new Date());userFileDAO.save(userFile);}
}
5. Controller
1. UserController
@Controller
@RequestMapping("user")
public class UserController {@Autowiredprivate UserService userService;/*** 登陆方法*/@PostMapping("login")public String login(User user, HttpSession session) {User userDB = userService.login(user);if (userDB != null) {session.setAttribute("user", userDB);return "redirect:/file/showAll";} else {// 这里需要使用redirect来避免表单的重复提交return "redirect:/index"; // 注意这里不能重定向到 /login.html,而是/login }}
}
2. IndexController
对一些index的映射
@Controller
public class IndexController {@GetMapping("index")public String toLogin() {return "login"; // 这里自动由Spring来解析}
}
注意访问路径/login
和/login.html
的区别:
/login
:会被indexController解析,然后返回login.html
,同时里面的thymeleaf语句也会被解析
/login.html
:直接返回login.html
,里面的thymeleaf语句不会被解析
3. FileController
@Controller
@RequestMapping("file")
public class FileController {@Autowiredprivate UserFileService userFileService;/*** 展示所有文件信息*/@GetMapping("showAll")public String findAll() {System.out.println("查询所有进入......");return "showAll";}}
5. 功能开发
0. 文件查询
上面简单的三层开发已经把这个业务功能实现了,这里不重点讨论。下面截图是实现文件上传后所截。
1. 文件上传
所有上传的文件都会被存在``/static/files`目录下,又根据需求,上传的文件需要根据日期放在不同的文件中。
先添加依赖,主要用来获取上传文件的后缀:
<dependency><groupId>commons-fileupload</groupId><artifactId>commons-fileupload</artifactId><version>1.4</version>
</dependency>
// 获取原文件的后缀名
String extension = "." + FilenameUtils.getExtension(aaa.getOriginalFilename());
controller开发:
前端通过表单来上传文件,这里上传文件有大小限制
<h3>上传文件:</h3> <form th:action="@{/file/upload}" method="post" enctype="multipart/form-data"><input type="file" name="aaa"> <input type="submit" value="上传文件"> </form>
Controller层通过
MultipartFile
来接受文件,注意变量名必须与前端表单中的name
属性中的值一致,否则接受不到/*** 处理上传文件:保存文件信息到数据库* 注意参数MultipartFile的变量名必须和表单中的name属性一致,这样才可以接收到文件*/ @PostMapping("upload") public String upload(MultipartFile aaa) {// 获取上传文件用户的idUser user = (User) session.getAttribute("user");// 获取原文件名String oldFilename = aaa.getOriginalFilename();// 获取原文件的后缀名String extension = "." + FilenameUtils.getExtension(aaa.getOriginalFilename());// 生成新文件名String newFileName = new SimpleDateFormat("yyyyMMddHHmmss").format(new Date()) + UUID.randomUUID().toString() + extension;// 文件大小long size = aaa.getSize();// 文件类型String type = aaa.getContentType();// 以下属性放到业务层Service去做// 图片ID// 是否是图片// 下载次数// 上传时间// 根据日期生成目录String realPath = ResourceUtils.getURL("classpath:").getPath() + "/static/files";String dateFormat = new SimpleDateFormat("yyyy-MM-dd").format(new Date());String dateDirPath = realPath + "/" + dateFormat;File dateDir = new File(dateDirPath);if (!dateDir.exists()) {dateDir.mkdirs(); // mkdirs()可以创建多级目录}// 处理上传文件aaa.transferTo(new File(dateDirPath, newFileName));// 将文件信息放入数据库中保存// 注意,我们现在在控制层,某一些业务需要放到业务层去做UserFile userFile = new UserFile();userFile.setOldFileName(oldFilename).setNewFileName(newFileName).setExt(extension).setSize(String.valueOf(size)).setType(type).setPath("/files/" + dateFormat).setUserId(user.getId());userFileService.save(userFile);return "redirect:/file/showAll"; // 文件上传后,重定向到showAll页面 }
每次上传文件后重定向到
showAll
,来展示用户的文件列表其中关于文件的id、是否是图片、下载次数、上传时间放在业务层中处理,而剩余文件信息(主要是路径、文件名)放在控制层处理。关于是否是图片,其实文件的type有已经判断过是否为图片,如果为图片就会返回"image/xxx"。
业务层:
@Override public void save(UserFile userFile) {String isImg = userFile.getType().startsWith("image") ? "Yes" : "No";userFile.setIsImg(isImg);userFile.setDowncounts(0);userFile.setUploadTime(new Date()); // 注意这个Date用的是 java.util.DateuserFileDAO.save(userFile); }
此外,如果文件是图片,我们还应该直接显示图片,这里主要需要在前端作修改:
<table border="1px"><tr><th>ID</th><th>文件原始名称</th><th>文件新名称</th><th>文件后缀</th><th>存储路径</th><th>文件大小</th><th>类型</th><th>是否是图片</th><th>下载次数</th><th>上传时间</th><th>操作</th></tr><tr th:each="file, fileStat : ${files}"><td><span th:text="${file.id}"></span></td><td><span th:text="${file.oldFileName}"></span></td><td><span th:text="${file.newFileName}"></span></td><td><span th:text="${file.ext}"></span></td><td><span th:text="${file.path}"></span></td><td><span th:text="${file.size}"></span></td><td><span th:text="${file.type}"></span></td><td><span th:if="${file.isImg} != 'Yes'" th:text="${file.isImg}"></span><img th:if="${file.isImg} == 'Yes'" style="height: 80px; width: 80px" th:src="${#servletContext.contextPath} + ${file.path} + '/' + ${file.newFileName}" alt=""></td><td><span th:id="${file.id}" th:text="${file.downcounts}"></span></td><td><span th:text="${#dates.format(file.uploadTime, 'yyyy-MM-dd HH:mm:ss')}"></span></td><th><a th:href="@{/file/download(id=${file.id})}">下载</a><a th:href="@{/file/download(id=${file.id}, openStyle='inline')}">在线打开</a><a th:href="@{/file/delete(id=${file.id})}">删除</a></th></tr> </table>
显示图片时,项目路径获取:
通常是在js中获取
@{/}
:获取项目路径,但是需要拼接/
,并且会报错${#servletContext.contextPath}
:无需拼接/
,这是servlet里的技术
2. 文件下载
关于下载和删除,我们在取文件时需要知道文件名,但是每个文件(每条文件信息记录)都有唯一的id绑定,所以我们可以使用id
FileController
/*** 文件下载*/
@GetMapping("download")
public void download(String id, HttpServletResponse response) throws IOException {// 根据文件id获取对应的文件信息UserFile userFile = userFileService.findById(Integer.valueOf(id));// 更新下载次数userFile.setDowncounts(userFile.getDowncounts() + 1);userFileService.update(userFile);// 获取文件路径System.out.println("File: " + userFile);String realPath = ResourceUtils.getURL("classpath:").getPath() + "/static/" + userFile.getPath();// 获取文件输入流(把磁盘上的文件通过IO加载到程序(内存)中FileInputStream fis = new FileInputStream(new File(realPath, userFile.getNewFileName()));// 附件下载// 注意:为防止中文乱码,需要使用UTF-8response.setHeader("content-disposition", "attachment;fileName=" + URLEncoder.encode(userFile.getOldFileName(), "UTF-8"));// 获取响应流(找到后需要通过Response发送回给用户ServletOutputStream os = response.getOutputStream();// 文件拷贝IOUtils.copy(fis, os);// 关闭流IOUtils.closeQuietly(fis);IOUtils.closeQuietly(os);
}
- 下载次数:更新次数虽然+1,但是这个更新是发生在后台的,我们单次点击“下载”,只向服务器进行了一次请求,对下载次数以及文件信息并没有在前端页面进行更新(如果使用重定向,页面会产生刷新的效果,用户体验不好)。
- 路径:数据库中保存的文件路径都是相对路径,我们需要使用绝对路径(带上项目名)和
/static
。 - 注意输入流和输出流的使用
- 注意附件的使用
- 注意编码格式和中文乱码的问题
- 这里相应的Service层和DAO层(Mapper层和Mybatis)这里就不贴代码,相对简单。
下载次数更新
注意如果要做到即时的“下载次数更新”,需要结合WebSocket的技术。 这里采用一个稍有延迟的数据更新,即用Ajax:
引入JQuery
写一个Ajax,每3秒查询一次下载数。(个人觉得这个方案不太好:1.轮询有延迟;2.当用户数量多、文件数量多、并发高时,数据库访问压力过大)
$(function () {// 周期函数setInterval(function(){$.get("[[@{/file/showAllJSON}]]", function (res) {$.each(res, function (index, file) {$('#'+file.id).text(file.downcounts);})})}, 3000); })
写后端代码,返回JSON格式文件列表,和
findAll
方法一样,但是使用@ResponseBody
/*** 展示所有文件信息,并以JSON格式返回*/ @GetMapping("showAllJSON") @ResponseBody public List<UserFile> findAllJSON(HttpSession session) {User user = (User)session.getAttribute("user");List<UserFile> files = userFileService.findByUserId(user.getId());return files; }
3. 在线打开
业务分析:
在线打开不会计入下载次数,并且该功能只能打开可以打开的文件,无法被在线打开的文件还是会下载。
在线打开其实和附件下载的代码几乎一样,区别就是:
附件下载(attachment)
response.setHeader("content-disposition", "attachment;fileName=" + URLEncoder.encode(userFile.getOldFileName(), "UTF-8"));
在线打开(inline)
response.setHeader("content-disposition", "inline;fileName=" + URLEncoder.encode(userFile.getOldFileName(), "UTF-8"));
所以这里其实可以代码复用:
showAll.html
<th><a th:href="@{/file/download(id=${file.id})}">下载</a><a th:href="@{/file/download(id=${file.id}, openStyle='inline')}">在线打开</a><a th:href="@{/file/delete(id=${file.id})}">删除</a>
</th>
FileController
/*** 文件下载*/
@GetMapping("download")
public void download(String openStyle, String id, HttpServletResponse response) throws IOException {openStyle = openStyle == null ? "attachment" : openStyle;// 根据文件id获取对应的文件信息UserFile userFile = userFileService.findById(id);if ("attachment".equals(openStyle)) {// 更新下载次数userFile.setDowncounts(userFile.getDowncounts() + 1);userFileService.update(userFile);}// 获取文件路径System.out.println("File: " + userFile);String realPath = ResourceUtils.getURL("classpath:").getPath() + "/static/" + userFile.getPath();// 获取文件输入流(把磁盘上的文件通过IO加载到程序(内存)中FileInputStream fis = new FileInputStream(new File(realPath, userFile.getNewFileName()));// 附件下载// 注意:为防止中文乱码,需要使用UTF-8response.setHeader("content-disposition", openStyle + ";fileName=" + URLEncoder.encode(userFile.getOldFileName(), "UTF-8"));// 获取响应流(找到后需要通过Response发送回给用户ServletOutputStream os = response.getOutputStream();// 文件拷贝IOUtils.copy(fis, os);// 关闭流IOUtils.closeQuietly(fis);IOUtils.closeQuietly(os);
}
做一个openStyle
字符串的判断:判断是文件下载还是在线打开,以及是否增加下载次数
4. 文件删除
文件删除主要包括:查询文件,删除文件,删除数据库中的文件信息。
/*** 文件删除,包括数据库的文件信息和文件本身* @param id 要删除文件的id* @return 重定向至用户文件列表首页*/
@GetMapping("delete")
public String delete(String id) throws FileNotFoundException {// 根据id查询信息UserFile userFile = userFileService.findById(id);// 删除文件:// 获取绝对路径String realPath = ResourceUtils.getURL("classpath:").getPath() + "/static" + userFile.getPath();// 获得文件File file = new File(realPath, userFile.getNewFileName());if (file.exists()) {file.delete(); // 立即删除}// 删除数据库中的文件信息userFileService.delete(id);return "redirect:/file/showAll";
}
6. 效果
基于Springboot和Mybatis的文件上传与下载相关推荐
- Struts2.3.5+Hibernate3+Spring3.1基于注解实现的多文件上传,下载
Struts2.3.5+Hibernate3+Spring3.1基于注解实现的的多文件上传,下载,这里是上传文件到数据库中,上传控件可以增加和删除,有需要的朋友可以看看. 以下是源码下载地址:http ...
- Springboot中常用的文件上传和下载通用接口
记录下通用的文件上传和下载接口,一般的开发中都是会使用到的,不过写的最简单的版本. 文章目录 程序测试 本文小结 程序测试 在yml中配置一个文件保存的路径 #保存文件的路径 common:file: ...
- 基于阿里云的OSS文件上传和下载
OSS概述 OSS是基于阿里云的一个云平台文件保存的系统,我们可以将服务器的文件上传至云端从而减轻服务器的压力. 初体验 首先创建一个bucket (给你的云储存器配置名字等基本信息) 生成Asses ...
- Springboot实现简单的文件上传和下载功能
一.第一步, 第一步依然是创建数据库,我简单设计了三个字段(file_id,file_name,create_time) CREATE TABLE `txtfile` (`file_id` int N ...
- 基于FTP协议的Excel文件上传与下载
1.关于FTP协议 FTP(文件传输协议)是TCP/IP协议组中的协议之一,作为网络共享文件的传输协议,在网络应用软件中具有广泛的应用.FTP协议的全称为File Transfer Protocol, ...
- 基于华为云obs实现文件上传下载(技术栈mysql+springboot+Maven+jsp+java)的技术分享
基于华为云obs实现文件上传下载(技术栈mysql+springboot+jsp+java)的技术分享 obs实现文件上传下载 前言 一.OBS是什么? 二.使用步骤 1.1 前期准备 2 工具的内容 ...
- SpringBoot整合阿里云OSS文件上传、下载、查看、删除
SpringBoot整合阿里云OSS文件上传.下载.查看.删除 该项目源码地址:https://github.com/ggb2312/springboot-integration-examples ( ...
- SpringBoot实现文件上传和下载
文件上传需要使用到 MultipartResolver接口. Spring MVC 使用 MultipartResolver接口的实现类:CommonsMultipartResolver .Commo ...
- 基于springboot和vue实现oss上传
基于springboot实现oss上传 1 OSS对象存储 阿里云对象存储服务(Object Storage Service,简称 OSS),是阿里云提供的海量.安全.低成本.高可靠的云存储服务.OS ...
- SpringBoot下文件上传与下载的实现
原文:http://blog.csdn.net/colton_null/article/details/76696674 SpringBoot后台如何实现文件上传下载? 最近做的一个项目涉及到文件上传 ...
最新文章
- SLAM前端 ---------特征提取之ORB(ORB与SIFT与SURF)
- 计算机术语表达祝福,考研祝福| | 计算机 愿你们历经千帆,终达彼岸
- Leet Code OJ 203. Remove Linked List Elements [Difficulty: Easy]
- 图片加到json中,提交到服务器端处理异常问题。
- 面试薪资这样谈,让你的月薪加倍!
- 手机型号大全_双十一高价位华为手机推荐,2020年哪款更值得入手
- 转贴:Google Reader:信息背后的信息,无可替代的伟大
- Linux命令使用练习三
- unity透明通道加颜色_Unity的Gamma颜色空间和Linear颜色空间的小研究
- ad7705c语言程序,基于51单片机的的AD7705的运用
- Linux - 增加用户、添加用户组
- 百度云直链下载-IDM+网页解析(三)
- python实现多人聊天论文_Python基于Socket实现简易多人聊天室的示例代码
- 计算机语言与语法,编程语言中语法和语义有什么区别?
- 细说——sqlmap
- 【C语言】杨辉三角(数组)
- linux的chmod与chown
- 无人机激光雷达的路径规划仿真
- 通过邮件收发传真的方法与步骤
- 互联网摸鱼日报(2022-11-22)