在java领域中SpringBoot是一个非常好用的框架,可以快速地构建web项目,这里记录一下使用SpringBoot来实现文件的上传、下载和在线预览功能。
本文主要用到用户和用户所属的文件,所以这里就只需要设计用户表和文件表即可。
用户表t_user
CREATE TABLE `t_user` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`username` varchar(80) DEFAULT NULL,
`password` varchar(80) DEFAULT NULL,
PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8 ROW_FORMAT=DYNAMIC
insert into t_user(username, password) values("pikacho", "123456");
文件表t_files
CREATE TABLE `t_files` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`file_name` varchar(200) DEFAULT NULL,
`ext` varchar(20) DEFAULT NULL,
`path` varchar(300) DEFAULT NULL,
`size` bigint(64) DEFAULT NULL,
`type` varchar(120) DEFAULT NULL,
`download_counts` int(6) DEFAULT NULL,
`upload_time` datetime DEFAULT NULL,
`user_id` int(11) NOT NULL,
PRIMARY KEY (`id`),
KEY `user_id` (`user_id`),
CONSTRAINT `t_files_ibfk_1` FOREIGN KEY (`user_id`) REFERENCES `t_user` (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8
这里不用多说,使用IDEA创建SpringBoot项目,删除多余没用的文件,让项目结构保持简洁。
简单地创建一个控制器,然后启动项目,测试该SpringBoot项目是否构建成功。
@Controller
public class HelloController {
@RequestMapping("hello")
@ResponseBody
public String hello(){
return "hello SpringBoot";
}
}
访问项目:http://localhost:8080/hello,出现如下画面说明项目能够成功运行;先保证项目能成功运行,然后再考虑开发功能。
pom.xml依赖
org.springframework.boot
spring-boot-starter-thymeleaf
org.springframework.boot
spring-boot-starter-web
org.projectlombok
lombok
true
org.mybatis.spring.boot
mybatis-spring-boot-starter
2.2.0
mysql
mysql-connector-java
runtime
org.apache.shiro
shiro-spring
1.4.1
commons-fileupload
commons-fileupload
1.4
com.alibaba
druid
1.1.19
org.springframework.boot
spring-boot-starter-test
test
application.yml主配置文件
spring:
application:
name: fileStorage
thymeleaf:
cache: false
datasource:
type: com.alibaba.druid.pool.DruidDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/fileservice?serverTimezone=Asia/Shanghai&userUnicode=true&characterEncoding=utf-8
username: root
password: root
server:
port: 8080
mybatis:
mapper-locations: classpath:mybatis/mapper/*.xml
type-aliases-package: com.pikacho.entity
添加完依赖以及将主配置文件配置完成,完成本项目功能需要的基本环境就已经全部搭建完成了,启动项目测试项目是否可以正常运行;出现下面的画面说明项目基本环境搭建成功。
即使一个非常简单的demo项目,也是少不了前端页面。对于一个后端开发者来说,要自己去写前端页面布局和样式是一件挺痛苦也困难的事(是真的不会写啊!)。所以这里使用layui框架来快速构建前端页面。当然,这只是简单的使用一下该框架,想要熟练使用。还需继续深入学习才行。
1.引入layui
将layui需要的文件添加到static文件夹下
2.创建登录页面
login.html
login
请 登 录
UserController.java
/**
* 前往登录页面
* @return
*/
@RequestMapping("toLogin")
public String toLogin(){
return "login";
}
这样,之后我们就可以轻松地创建前端页面了,并且不用自己去设计样式和布局等问题。还是要提一下,像这样简单使用一下这种程度就够了,但是要熟练使用的话,就要更加深入学习才行。
3.完成登录功能
@Data
@NoArgsConstructor
@AllArgsConstructor
@ToString
public class User {
private Integer id;
private String username;
private String password;
}
@Mapper
@Repository
public interface UserDao {
/**
* 用户登录功能
* @return
*/
public User login(User user);
}
public interface UserService {
public User login(User user);
}
@Service
public class UserServiceImpl implements UserService {
@Autowired
public UserDao userDao;
@Override
public User login(User user){
return userDao.login(user);
}
}
Shiro框架可以快速实现登录认证等安全权限功能,这里也是简单使用一下,想要深入理解使用还需查阅其官方文档。
UserRealm.java
public class UserRealm extends AuthorizingRealm {
@Autowired
private UserService userService;
// 授权
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection){
return null;
}
// 认证
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) {
UsernamePasswordToken usernamePasswordToken = (UsernamePasswordToken)token;
User user = new User();
user.setUsername(usernamePasswordToken.getUsername());
user.setPassword(String.valueOf(usernamePasswordToken.getPassword()));
// 从数据库查询出来
User userDB = userService.login(user);
if(userDB == null){
return null;
}
return new SimpleAuthenticationInfo(userDB, user.getPassword(), "");
}
}
ShiroConfig.java
@Configuration
public class ShiroConfig {
// 第一步:创建UserRealm对象
@Bean
public UserRealm userRealm(){
return new UserRealm();
}
// 第二步:创建默认安全管理器DefaultWebSecurityManager
@Bean(name = "securityManager")
public DefaultWebSecurityManager getDefaultWebSecurityManager(
@Qualifier("userRealm") UserRealm userRealm ){
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
securityManager.setRealm(userRealm);
return securityManager;
}
// 第三步:创建ShiroFilterFactoryBean
@Bean
public ShiroFilterFactoryBean getShiroFilterFactoryBean(
@Qualifier("securityManager") DefaultWebSecurityManager securityManager){
ShiroFilterFactoryBean bean = new ShiroFilterFactoryBean();
// 设置安全管理器
bean.setSecurityManager(securityManager);
/*
* anon: 无需认证就能访问
authc: 必须认证后才能访问
user: 必须拥有 记住我 功能才能用
perms: 拥有对某个资源的权限才能访问
role: 拥有某个角色权限才能访问
*/
// 拦截/file/* ,该路径只有登录的情况下才能查看
Map filterMap = new LinkedHashMap<>();
filterMap.put("/file/*", "authc");
bean.setFilterChainDefinitionMap(filterMap);
// 设置没有访问权限时,跳转页面
bean.setLoginUrl("/login");
return bean;
}
}
@PostMapping("login")
public String login(User user, Model model){
Subject subject = SecurityUtils.getSubject();
// 封装用户信息
UsernamePasswordToken token = new UsernamePasswordToken(user.getUsername(), user.getPassword());
try{
subject.login(token);
Session session = subject.getSession();
session.setAttribute("user", (User)subject.getPrincipal());
// 前往文件列表页面
return "list";
}catch (UnknownAccountException e){
model.addAttribute("msg", "用户名或密码错误");
return "login";
}catch (IncorrectCredentialsException e){
model.addAttribute("msg", "用户名或密码错误");
return "login";
}
}
@SpringBootApplication
@MapperScan(basePackages = "com.pikacho.dao")
public class SpringbootFileuploadApplication {
public static void main(String[] args) {
SpringApplication.run(SpringbootFileuploadApplication.class, args);
}
}
登录功能代码已经开发完毕了,现在测试一下功能是否正常。
访问项目:http://localhost:8080/user/toLogin,之前创建用户表时,就已经创建了一个用户。用户名:pikacho 密码:123456。
1.创建注册页面
register.html
register
2.完成注册功能
/**
*
* @param user
*/
public void register(User user);
insert into t_user (username, password)
values(#{username}, #{password});
/**
*
* @param user
*/
public void register(User user);
/**
*
* @param user
*/
@Override
public void register(User user){
userDao.register(user);
}
测试注册功能, 访问项目:http://localhost:8080/user/toRegister ;添加用户pikacho2 :123456
前面只是简单的创建一个list页面验证登录功能,现在补全list页面。
list.html
FileList
/**
* 注册功能
* @return
*/
@GetMapping("logout")
public String logout(){
Subject subject = SecurityUtils.getSubject();
subject.logout();
// 这里需要注意:redirect:user/toLogin 实际的请求路径http://localhost:8080/user/user/toLogin
// 所以需要使用redirect:toLogin
return "redirect:toLogin";
}
@Data
@NoArgsConstructor
@AllArgsConstructor
@ToString
@Accessors(chain = true)
public class UserFile {
private Integer id;
private String fileName;
private String ext;
private String path;
private long size;
private String type;
private Integer downloadCounts;
private Date uploadTime;
private Integer userId;
}
@Mapper
@Repository
public interface UserFileDao {
/**
* 根据用户id获得用户文件列表
* @param id
* @param begin
* @param offset
* @return
*/
public List queryByUserId(Integer id, Integer begin, int offset);
/**
* 根据用户id获得该用户文件总数
* @param id
* @return
*/
public int queryFileCount(Integer id);
}
public interface UserFileService {
/**
* 根据用户id获得文件列表
* @param id
* @param page
* @param limit
* @return
*/
public List queryByUserId(Integer id, Integer page, Integer limit);
/**
* 根据用户id获得文件数
* @param id
* @return
*/
public int queryFileCounts(Integer id);
}
@Service
public class UserFileServiceImpl implements UserFileService {
@Autowired
private UserFileDao userFileDao;
/**
* 根据用户id获得文件列表
* @param id
* @param page
* @param limit
* @return
*/
@Override
public List queryByUserId(Integer id, Integer page, Integer limit){
// page表示第几页,limit表示每页显示多少行数据
int begin = (page-1)*limit; // 该计算方法获得开始的位置
int offset = limit;
return userFileDao.queryByUserId(id, begin, limit);
}
/**
* 根据用户id获得文件数
* @param id
* @return
*/
@Override
public int queryFileCounts(Integer id){
return userFileDao.queryFileCounts(id);
}
}
@Controller
@RequestMapping("file")
public class UserFileController {
@Autowired
private UserFileService userFileService;
/**
* 返回文件列表
* @param session
* @param request
* @return
*/
@PostMapping("all")
@ResponseBody
public Map queryAllFile(HttpSession session, HttpServletRequest request){
int page = Integer.parseInt(request.getParameter("page"));
int limit = Integer.parseInt(request.getParameter("limit"));
User user = (User) session.getAttribute("user");
List files = userFileService.queryByUserId(user.getId(), page, limit);
Map res = new HashMap<>();
res.put("code", 0);
res.put("count", userFileService.queryFileCounts(user.getId()));
res.put("data", files);
return res;
}
}
// 渲染表格
table.render({
elem: '#fileList',
height: 600,
minWidth: 80,
url: '/file/all', // 这一定要是/file/all,之前我是file/all不停的报错,无法访问成功
parseData: function (res) {
return {
"code": res.code,
"msg": "",
"count": res.count,
"data": res.data
};
},
method: 'post',
limit: 10,
page: true,
cols: [[
{field:'id', title:'ID', sort:true, fixed:'left'},
{field:'fileName', title:'文件名'},
{field:'ext', title:'文件后缀'},
{field:'path', title:"存储路径"},
{field:'size', title:'大小'},
{field:'type', title:"类型"},
{field:'downloadCounts', title:'下载次数'},
{field:'uploadTime', title:'上传时间'},
{tilte:'操作',align:'center', toolbar:'optBar', width:200, fixed:'right'}
]]
});
/**
* 上传文件
* @param userFile
*/
public void save(UserFile userFile);
insert into t_files(file_name, ext, path, size, type, download_counts, upload_time, user_id)
values(#{fileName}, #{ext}, #{path}, #{size}, #{type}, #{downloadCounts}, #{uploadTime}, #{userId});
/**
* 上传文件
* @param userFile
*/
public void save(UserFile userFile);
/**
* 上传文件
* @param userFile
*/
@Override
public void save(UserFile userFile){
userFile.setDownloadCounts(0).setUploadTime(new Date());
userFileDao.save(userFile);
}
@GetMapping("index")
public String fileIndex(){
return "list";
}
/**
* 上传文件
* @param file
* @param session
* @return
*/
@PostMapping("upload")
@ResponseBody
public Map uploadFile(@RequestParam("file")MultipartFile file, HttpSession session){
Map res = new HashMap<>();
try{
User user = (User) session.getAttribute("user");
String fileName = file.getOriginalFilename();
String extension = FilenameUtils.getExtension(fileName);
long size = file.getSize();
String type = file.getContentType();
// 根据日期生成目录
String localContainer = "/fileContainer";
String uploadPath = ResourceUtils.getURL("classpath").getPath()+localContainer;
String dateFormat = new SimpleDateFormat("yyyy-MM-dd").format(new Date());
File dateDirPath = new File(uploadPath+File.separator+dateFormat);
if(!dateDirPath.exists()){
dateDirPath.mkdirs();
}
file.transferTo(new File(dateDirPath, fileName));
// 将文件信息存入数据库
UserFile userFile = new UserFile();
userFile.setFileName(fileName)
.setExt('.'+extension)
.setPath(Paths.get(localContainer, dateFormat, fileName).toString())
.setSize(size)
.setType(type)
.setUserId(user.getId());
userFileService.save(userFile);
res.put("code", "0");
res.put("msg", "上传成功");
res.put("url", "/file/index");
}catch(IOException e){
res.put("code", "-1");
res.put("msg", "上传失败");
res.put("url", "/file/index");
}
return res;
}
这里文件存储在我的本地磁盘上,文件表中存储的是文件在我磁盘存储的存储路径。
开发流程与上面基本一致。
// UserFiledao
/**
* 下载文件
* @param id
* @return
*/
public UserFile queryByUserFileId(Integer id);
/**
* 更新文件下载次数
* @param userFile
*/
public void update(UserFile userFile);
// UserFileService
/**
* 下载文件
* @param id
* @return
*/
public UserFile queryByUserFileId(Integer id);
/**
* 跟新文件下载次数
* @param userFile
*/
public void update(UserFile userFile);
// UserFileServiceImpl
/**
* 下载文件
* @param id
* @return
*/
@Override
public UserFile queryByUserFileId(Integer id) {
return userFileDao.queryByUserFileId(id);
}
/**
* 跟新文件下载次数
* @param userFile
*/
@Override
public void update(UserFile userFile) {
userFileDao.update(userFile);
}
update t_files set download_counts = #{downloadCounts} where id = #{id};
/**
* 下载文件
* @param id
* @param response
*/
@GetMapping("download/{id}")
public void download(@PathVariable("id") Integer id, HttpServletResponse response){
String openStyle = "attachment";
try{
getFile(openStyle, id, response);
}catch (Exception e){
e.printStackTrace();
}
}
/**
* 更新文件下载次数
* @param openStyle
* @param id
* @param response
* @throws Exception
*/
public void getFile(String openStyle, Integer id, HttpServletResponse response) throws Exception {
UserFile userFile = userFileService.queryByUserFileId(id);
String realPath = ResourceUtils.getURL("classpath").getPath()+userFile.getPath();
FileInputStream is = new FileInputStream(new File(realPath));
// 附件下载
response.setHeader("content-disposition", openStyle+";filename=" + URLEncoder.encode(userFile.getFileName(), "UTF-8"));
// 获取响应response输出流
ServletOutputStream os = response.getOutputStream();
// 文件拷贝
IOUtils.copy(is, os);
IOUtils.closeQuietly(is);
IOUtils.closeQuietly(os);
if(openStyle.equals("attachment")){
userFile.setDownloadCounts(userFile.getDownloadCounts()+1);
userFileService.update(userFile);
}
}
list.html
//监听工具条
table.on('tool(fileTable)', function (obj) {
var data = obj.data;
if (obj.event === 'download') {
window.open("/file/download/" + data.id);
obj.update({
"downloadCounts": data.downloadCounts + 1
});
} else if (obj.event === 'delete') {
layer.confirm('真的删除文件吗?', function (index) {
$.ajax({
url: "/file/delete/" + data.id,
type: "Get",
success: function (res) {
layer.msg(res.msg);
obj.del();
},
error: function (res) {
$.message.alert('msg', res.msg);
}
});
layer.close(index);
});
} else if (obj.event === 'preview') {
layer.open({
type: 2,
skin: 'layui-layer-demo', //样式类名
title: '文件预览',
closeBtn: 1, //显示关闭按钮
anim: 2,
area: ['893px', '600px'],
shadeClose: true, //开启遮罩关闭
content: '/file/preview/' + data.id
});
}
});
文件删除开发流程也是同上。
/**
* 文件预览
* @param id
* @param response
* @throws IOException
*/
@GetMapping("preview/{id}")
public void preview(@PathVariable("id") Integer id, HttpServletResponse response) throws Exception {
String openStyle = "inline";
getFile(openStyle,id,response);
}
到此,这个小项目就基本完成。
demo源码地址https://github.com/picacho-pkq/SpringBoot-demo
demo下载地址https://download.csdn.net/download/pikcacho_pkq/75146243