之前介绍了Spring boot利用wangEditor实现图片上传,其实本质是图片上传和文件上传是同样的一回事,我之所以要重新讲文件上传是因为这里跟之前的图片上传有一点差别。
差别就是上传的文件是转化为二进制流存在数据库里面的(这是一个项目的实际需求),为了降低对数据库的访问数在用户首次访问文件时将文件缓存在磁盘中下次再访问相同文件时就直接从磁盘中获取文件而不需要重新读取数据库了。
整个流程如图所示
这个示例本质是上是在spring boot官方文件上传示例上进行的重构,因此要感谢spring boot官方提供的各种示例,\^_^
后面准备针对写的各技术文档专门建一个项目来进行描述,Talking is cheap,show me the code!
代码地址spring
代码结构如下
├── pom.xml
├── README.md
├── spring.iml
├── src
│ ├── main
│ │ ├── java
│ │ │ └── com
│ │ │ └── spring
│ │ │ ├── Application.java
│ │ │ ├── common
│ │ │ │ ├── dao
│ │ │ │ │ └── MyBaseRepository.java
│ │ │ │ └── service
│ │ │ │ ├── BaseServiceImpl.java
│ │ │ │ └── BaseService.java
│ │ │ ├── config
│ │ │ │ ├── datasource
│ │ │ │ │ ├── DruidDataSourceConfiguration.java
│ │ │ │ │ └── DruidDataSourceProperties.java
│ │ │ │ ├── jpa
│ │ │ │ │ └── RepositoryConfig.java
│ │ │ │ └── SpringConfig.java
│ │ │ ├── controller
│ │ │ │ ├── FileController.java
│ │ │ │ └── UserController.java
│ │ │ ├── dao
│ │ │ │ ├── FileDao.java
│ │ │ │ └── UserDao.java
│ │ │ ├── entity
│ │ │ │ ├── FileEntity.java
│ │ │ │ └── UserEntity.java
│ │ │ ├── exception
│ │ │ │ ├── GenericException.java
│ │ │ │ ├── ItemNotFoundException.java
│ │ │ │ ├── ItemSaveException.java
│ │ │ │ └── StorageFileNotFoundException.java
│ │ │ ├── service
│ │ │ │ ├── FileServiceImpl.java
│ │ │ │ ├── FileService.java
│ │ │ │ ├── FileSystemstorageServiceImpl.java
│ │ │ │ ├── FileSystemStorageService.java
│ │ │ │ ├── StorageServiceImpl.java
│ │ │ │ ├── StorageService.java
│ │ │ │ ├── UserServiceImpl.java
│ │ │ │ └── UserService.java
│ │ │ ├── ServletInitializer.java
│ │ │ └── util
│ │ │ └── CommonUtil.java
│ │ ├── main.iml
│ │ └── resources
│ │ ├── application.properties
│ │ ├── logback-spring.xml
│ │ ├── static
│ │ └── templates
│ │ └── upload.ftl
│ └── test
│ ├── java
│ │ └── com
│ │ └── spring
│ │ └── ApplicationTests.java
│ └── test.iml
我们来看看各包的具体划分,因为只是做功能展示因此没有划分具体的模块,所以只有一个entity、dao、service、controller,在实际项目中要进行分模块设计
common:一些通用的功能模块,例如通用的dao,范型service
config:系统的各种配置参数,jpa、datasource、config等
util:一些工具类
execption:自定义异常类
主要使用了Spring自带的文件上传、Java的nio对文件进行操作、简单的lambda遍历
下面我们就针对核心的实现进行说明
DAO 层
我们看看具体的FileDao
package com.spring.dao;
import com.spring.common.dao.MyBaseRepository;
import com.spring.entity.FileEntity;
import org.springframework.data.jpa.repository.Query;
import org.springframework.stereotype.Repository;
import java.util.List;
/**
* @ com.spring.dao
* @author cj
* 2017/12/23
**/
@Repository
public interface FileDao extends MyBaseRepository<FileEntity,String>{
/**
* 只查询文件名、文件id、文件类型不加载具体的文件内容
* @return
*/
@Query("select new FileEntity (f.fileid,f.filename,f.filetype,f.fileext)from FileEntity f")
List<FileEntity> findAllFile();
}
通常我们利用jpa查询的时候会把一条记录的所有数据全部查询出来,但是因为要获取的是文件内容不可能在获取文件类表的时候把具体的文件内容也加载出来,因此我们只查询特定的字段内容
@Query(“select new FileEntity (f.fileid,f.filename,f.filetype,f.fileext)from FileEntity f”)
但是要实现这个我们在FileEntity中需要添加对应的构造函数,不然是会出错的
public FileEntity() {
}
public FileEntity(String fileid, String filename, String filetype, String fileext) {
this.fileid = fileid;
this.filename = filename;
this.filetype = filetype;
this.fileext = fileext;
}
public FileEntity(String fileid, String filename, String filetype, String fileext, byte[] filedata) {
this.fileid = fileid;
this.filename = filename;
this.filetype = filetype;
this.fileext = fileext;
this.filedata = filedata;
}
这样我们就能查询指定字段的实体了
Service 层
数据库Service
package com.spring.service;
import com.spring.common.service.BaseService;
import com.spring.entity.FileEntity;
import java.util.List;
/**
* com.spring.service
* @author jacky
* @date 2017/12/23
**/
public interface FileService extends BaseService<FileEntity, String> {
/**
* 查询所有文件
* @return
*/
List<FileEntity> findAllFile();
}
package com.spring.service;
import com.spring.common.service.BaseServiceImpl;
import com.spring.dao.FileDao;
import com.spring.entity.FileEntity;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
/**
* com.spring.service
*
* @author jacky
* @date 2017/12/23
*
* 与数据库交互实现数据库层面的文件读写
*
**/
@Service
@Transactional(readOnly = true)
public class FileServiceImpl extends BaseServiceImpl<FileEntity, String> implements FileService {
private FileDao fileDao;
@Autowired
public FileServiceImpl(FileDao fileDao) {
this.fileDao = fileDao;
}
/**
* 查询所有文件
*
* @return
*/
@Override
public List<FileEntity> findAllFile() {
return fileDao.findAllFile();
}
}
文件系统Service
package com.spring.service;
import com.spring.entity.FileEntity;
import java.nio.file.Path;
/**
* com.spring.service
* @author jacky
* @date 2017/12/23
**/
public interface FileSystemStorageService {
/**
* 初始化存储路径
*/
void init();
/**
* 从磁盘加载指定文件
* @param filename
* @return
*/
Path load(String filename);
/**
* 将文件缓存到磁盘
* @param fileEntity
*/
void store(FileEntity fileEntity);
}
package com.spring.service;
import com.spring.config.SpringConfig;
import com.spring.entity.FileEntity;
import com.spring.exception.GenericException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardCopyOption;
/**
* com.spring.service
*
* @author jacky
* @date 2017/12/23
*
* 与磁盘交互实现磁盘上的文件读写
*
**/
@Component
public class FileSystemstorageServiceImpl implements FileSystemStorageService {
private final Path rootLocation;
@Autowired
public FileSystemstorageServiceImpl(SpringConfig config) {
this.rootLocation = Paths.get(config.getLocation());
}
/**
* 初始化存储路径
*/
@Override
public void init() {
try {
//判断文件夹是否存在,不存在则创建文件夹
if (!Files.isDirectory(rootLocation)) {
Files.createDirectories(rootLocation);
}
} catch (Exception e) {
throw new GenericException("不能初始化存储位置", e);
}
}
/**
* 从磁盘加载指定文件
*
* @param filename
* @return
*/
@Override
public Path load(String filename) {
this.init();
return rootLocation.resolve(filename);
}
/**
* 将文件缓存到磁盘
*
* @param fileEntity
*/
@Override
public void store(FileEntity fileEntity) {
try {
String fileName = fileEntity.getFileid() + "." + fileEntity.getFileext();
if (fileEntity.getInputStream() != null) {
//利用nio将文件从二进制流写入磁盘文件中
Files.copy(fileEntity.getInputStream(), rootLocation.resolve(fileName),
StandardCopyOption.REPLACE_EXISTING);
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
文件操作抽象
package com.spring.service;
import com.spring.entity.FileEntity;
import org.springframework.core.io.Resource;
import org.springframework.web.multipart.MultipartFile;
import java.util.List;
public interface StorageService {
/**
* 获取所有文件列表
* @return
*/
List<FileEntity> findAll();
/**
* 加载指定的文件
* @param fileId
* @param fileName
* @return
*/
Resource loadAsResource(String fileId, String fileName);
/**
* 保存文件
* @param file
*/
void store(MultipartFile file);
}
package com.spring.service;
import com.spring.entity.FileEntity;
import com.spring.exception.GenericException;
import com.spring.exception.ItemNotFoundException;
import com.spring.util.CommonUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.io.ByteArrayResource;
import org.springframework.core.io.Resource;
import org.springframework.core.io.UrlResource;
import org.springframework.stereotype.Component;
import org.springframework.web.multipart.MultipartFile;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
/**
* @author jackycheng
* @date 2017-12-29-下午3:54
*
* 作为文件处理的抽象实现供Controller调用
*
*/
@Component
public class StorageServiceImpl implements StorageService {
private FileSystemStorageService fileSystemStorageService;
private FileService fileService;
@Autowired
public StorageServiceImpl(FileSystemStorageService fileSystemStorageService, FileService fileService) {
this.fileSystemStorageService = fileSystemStorageService;
this.fileService = fileService;
}
/**
* 获取所有文件列表
*
* @return
*/
@Override
public List<FileEntity> findAll() {
return fileService.findAllFile();
}
/**
* 加载指定的文件
*
* @param fileId
* @param fileName
* @return
*/
@Override
public Resource loadAsResource(String fileId, String fileName) {
String[] fileProperties = fileName.split("\\.");
String fileext = fileProperties[1];
try {
Path file = fileSystemStorageService.load(fileId + "." + fileext);
Resource resource = null;
if (Files.exists(file)) {
resource = new UrlResource(file.toUri());
return resource;
} else {
FileEntity fileEntity = fileService.findOne(fileId);
if (fileEntity != null) {
resource = new ByteArrayResource(fileEntity.getFiledata());
fileSystemStorageService.store(fileEntity);
return resource;
} else {
throw new ItemNotFoundException("数据库未找到该文件,文件编号为:" + fileId);
}
}
} catch (Exception e) {
throw new GenericException("获取指定文件失败!", e.getCause());
}
}
/**
* 保存文件
*
* @param file
*/
@Override
public void store(MultipartFile file) {
FileEntity fileEntity = new FileEntity();
fileEntity.setFileid(CommonUtil.getUuid());
fileEntity.setFiletype(file.getContentType());
String[] fileProperties = file.getOriginalFilename().split("\\.");
fileEntity.setFileext(fileProperties[1]);
fileEntity.setFilename(fileProperties[0]);
try {
fileEntity.setFiledata(file.getBytes());
} catch (IOException e) {
e.printStackTrace();
}
try {
fileService.save(fileEntity);
} catch (Exception e) {
e.printStackTrace();
}
}
}
Controller层
package com.spring.controller;
import com.spring.entity.FileEntity;
import com.spring.service.StorageService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.io.Resource;
import org.springframework.http.HttpHeaders;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.servlet.mvc.method.annotation.MvcUriComponentsBuilder;
import org.springframework.web.servlet.mvc.support.RedirectAttributes;
import java.io.UnsupportedEncodingException;
import java.util.ArrayList;
import java.util.List;
/**
* com.spring.controller
*
* @author jacky
* @date 2017/12/23
**/
@Controller
@RequestMapping("/file")
public class FileController {
private StorageService storageService;
@Autowired
public FileController(StorageService storageService) {
this.storageService = storageService;
}
@GetMapping
public String main(Model model) {
List<FileEntity> files = storageService.findAll();
if (files != null && files.size() > 0) {
List<String> paths = new ArrayList<>();
//lambda表达式遍历list
//利用MvcUriComponentsBuilder自动生成url
files.forEach(fileEntity -> paths.add(MvcUriComponentsBuilder.fromMethodName(FileController.class, "load",
fileEntity.getFileid(), fileEntity.getFilename() +"."+ fileEntity.getFileext()).build().toString()));
model.addAttribute("files", paths);
}
return "upload";
}
@GetMapping("/{fileId}/{fileName:.+}")
@ResponseBody
public ResponseEntity<Resource> load(@PathVariable String fileId, @PathVariable String fileName) {
Resource file = storageService.loadAsResource(fileId, fileName);
try {
//new String(fileName.getBytes("UTF-8"),"ISO8859-1") 为了解决文件名乱码
return ResponseEntity.ok().header(HttpHeaders.CONTENT_DISPOSITION,
"attachment; filename=\"" + new String(fileName.getBytes("UTF-8"),"ISO8859-1") + "\"").body(file);
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
return null;
}
/**
* 文件上传
* @param file
* @param redirectAttributes
* @return
*/
@PostMapping
public String upload(@RequestParam("file") MultipartFile file,
RedirectAttributes redirectAttributes) {
storageService.store(file);
redirectAttributes.addFlashAttribute("message",
"上传文件成功," + file.getOriginalFilename() + "!");
return "redirect:/file";
}
}
页面
页面使用freemarker作为模板
<html>
<head>
<title>文件上传</title>
</head>
<body>
<#if message??>
<h2>${message}!</h2>
</#if>
<div>
<form method="POST" enctype="multipart/form-data" action="/file">
<table>
<tr>
<td>File to upload:</td>
<td><input type="file" name="file"/></td>
</tr>
<tr>
<td></td>
<td><input type="submit" value="Upload"/></td>
</tr>
</table>
</form>
</div>
<div>
<ul>
<#if files??>
<#list files as file>
<li>
<a href="${file}">${file}</a>
</li>
</#list>
</#if>
</ul>
</div>
</body>
</html>
这样我们就完成了基于Spring boot的文件上传具体代码地址
代码地址