SpringBoot系列(11):文件上传实战(提供包括NIO在内的多种实现方式)


作者: 修罗debug
版权声明:本文为博主原创文章,遵循 CC 4.0 by-sa 版权协议,转载请附上原文出处链接和本声明。

摘要: 在开发企业级应用项目业务模块期间,“上传文件/附件”的功能相信每个小伙伴都遇见过,甚至有的曾以代码实战过。本文Debug将带领各位小伙伴重新回温一下在Spring Web应用中如何实现文件的上传,其中提供了包括Java NIO在内的多种方式。

内容:在企业级应用项目的开发过程中,“上传文件/附件”这一功能相信很多小伙伴都实现过,当然啦,其实现方式也是迥异不同。接下来,Debug就给各位小伙伴展示一下在Java Web应用中如何实现文件的上传。

在介绍实战之前,我们先来创建一个数据库表appendix,用于记录存储每个业务对象上传上来的图片的详情,包括其所属的业务对象的注解、所属业务模块、图片名称、大小、存储的磁盘路径等等,其DDL如下所示:

CREATE TABLE `appendix` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`module_id` int(11) DEFAULT NULL COMMENT '所属模块记录主键id',
`module_code` varchar(255) CHARACTER SET utf8mb4 DEFAULT NULL COMMENT '所属模块编码',
`module_name` varchar(255) CHARACTER SET utf8mb4 DEFAULT NULL COMMENT '所属模块名称',
`name` varchar(100) CHARACTER SET utf8mb4 DEFAULT NULL COMMENT '文件名称',
`size` varchar(255) CHARACTER SET utf8mb4 DEFAULT NULL COMMENT '文件大小',
`suffix` varchar(50) CHARACTER SET utf8mb4 DEFAULT NULL COMMENT '文件后缀名',
`file_url` varchar(500) CHARACTER SET utf8mb4 DEFAULT NULL COMMENT '文件访问的磁盘目录',
`is_active` tinyint(4) DEFAULT '1' COMMENT '是否有效(1=是;0=否)',
`create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8 COMMENT='附件(文件)上传记录';


一、接下来,我们先来介绍第一种方式吧,基于Java NIO的方式实现文件上传功能

1、首先,创建FileController,用于接收前端上传过来的文件以及其他业务信息,其源代码如下所示:

@RestController
@RequestMapping("file")
public class FileController extends AbstractController{
@Autowired
private IFileService fileService;

/**
* 为商品上传图片
* 上传文件-方式1:MultipartHttpServletRequest 接收前端参数
* @return
*/
@RequestMapping(value = "upload/v1",method = RequestMethod.POST)
public BaseResponse uploadV1(MultipartHttpServletRequest request){
BaseResponse response=new BaseResponse(StatusCode.Success);
Map<String,Object> resMap= Maps.newHashMap();
try {
String url=fileService.uploadFileV1(request);

resMap.put("fileUrl",url);
}catch (Exception e){
e.printStackTrace();
response=new BaseResponse(StatusCode.Fail.getCode(),e.getMessage());
}
response.setData(resMap);
return response;
}
}


2、创建IFileService的实现类,用于处理实际的业务信息以及实现文件的上传,其完整的源代码如下所示:  

/**
* 上传文件
* @Author:debug (SteadyJack)
* @Date: 2019/10/27 11:08
**/
@Service
public class FileService implements IFileService{

private static final Logger log= LoggerFactory.getLogger(FileService.class);

@Autowired
private ItemMapper itemMapper;

@Autowired
private CommonFileService commonFileService;

//第一种方法
@Override
@Transactional(rollbackFor = Exception.class)
public String uploadFileV1(MultipartHttpServletRequest request) throws Exception {
MultipartFile multipartFile=request.getFile("appendix");
//实际的业务信息
String itemName=request.getParameter("itemName");
String itemCode=request.getParameter("itemCode");
String itemTotal=request.getParameter("itemTotal");

Item item=new Item(itemName,itemCode,Long.valueOf(itemTotal));
item.setPurchaseTime(DateTime.now().toDate());
itemMapper.insertSelective(item);
//实现文件的上传
String url="";
if (item.getId()>0){
url=commonFileService.upload(multipartFile,item.getId(), Constant.SysModule.ModuleItem);
}
return url;
}

3、commonFileService.upload 方法即为真正的实现文件的上传与数据库记录的存储,其完整的源代码如下所示:  

/**
* @Author:debug (SteadyJack)
* @Date: 2019/10/27 11:36
**/
@Component
public class CommonFileService {
private static final SimpleDateFormat FORMAT=new SimpleDateFormat("yyyyMMdd");

@Autowired
private AppendixMapper appendixMapper;

/**
* 上传文件 - nio的方式
* @param file
* @throws Exception
*/
public String upload(MultipartFile file, final Integer moduleId, final Constant.SysModule module) throws Exception{
String fileName=file.getOriginalFilename();
String suffix=StringUtils.substring(fileName,StringUtils.indexOf(fileName,"."));
Long size=file.getSize();

//附件输入流
InputStream is=file.getInputStream();

//创建新文件存储的磁盘目录前缀、创建磁盘目录
String filePathPrefix=FORMAT.format(DateTime.now().toDate())+File.separator+module.getCode()+moduleId;
String rootPath=Constant.FilePrefix+filePathPrefix;
Path path=Paths.get(rootPath);
if (!Files.exists(path)){
Files.createDirectories(path);
}
//创建新的文件
String newFileName=System.nanoTime()+suffix;
String newFile=rootPath+File.separator+newFileName;
path=Paths.get(newFile);

//方式一
//Files.copy(is,path, StandardCopyOption.REPLACE_EXISTING); //如果存在则覆盖
//方式二
Files.write(path,file.getBytes());

Appendix entity=new Appendix(moduleId,module.getCode(),module.getName(),newFileName,size.toString(),suffix,newFile);
appendixMapper.insertSelective(entity);

return newFile;
}
}

下面进入测试环节,三图概括如下所示:  






二、最后,我们再来介绍第二种方式吧,基于传统File的方式实现文件上传功能

1、同样的道理,我们在FileController创建用于接收前端上传文件请求的方法,其源代码如下所示:

/**
* 上传文件-方式2
* @return
*/
@RequestMapping(value = "upload/v2",method = RequestMethod.POST)
public BaseResponse uploadV2(@RequestParam("appendix") MultipartFile file, FileUploadRequest request){
BaseResponse response=new BaseResponse(StatusCode.Success);
Map<String,Object> resMap= Maps.newHashMap();
try {
String url=fileService.uploadFileV2(file,request);

resMap.put("fileUrl",url);
}catch (Exception e){
e.printStackTrace();
response=new BaseResponse(StatusCode.Fail.getCode(),e.getMessage());
}
response.setData(resMap);
return response;
}


//访问图片时:域名 + 图片所在的磁盘目录-即数据库存储的file_url

在这里,我们采用了不同于第一种“面向字段获取”的方式,而是采用“面向对象”的思想,将所有业务相关的信息封装成实体对象FileUploadRequest进行接收,而文件/附件相关的文件数据流则采用MultipartFile对象进行接收,实体对象FileUploadRequest源代码如下所示:  

/**
* @Author:debug (SteadyJack)
* @Date: 2019/10/27 11:15
**/
@Data
public class FileUploadRequest implements Serializable{
private String itemName;
private String itemCode;
private Long itemTotal;
}


2、接下来是FileService处理业务信息以及文件上传的逻辑,其完整的源代码如下所示:  

//第二种方法
@Override
@Transactional(rollbackFor = Exception.class)
public String uploadFileV2(MultipartFile file, FileUploadRequest request) throws Exception {
Item item=new Item(request.getItemName(),request.getItemCode(),request.getItemTotal());
item.setPurchaseTime(DateTime.now().toDate());
itemMapper.insertSelective(item);

String url="";
if (item.getId()>0){
url=commonFileService.upload2(file,item.getId(), Constant.SysModule.ModuleItem);
}
return url;
}


3、commonFileService.upload2()方法即为核心的用于处理文件上传的逻辑方法,其完整的源代码如下所示:  

/**
* 上传文件 - 传统的方式
* @throws Exception
*/
public String upload2(MultipartFile multipartFile, final Integer moduleId, final Constant.SysModule module) throws Exception{
String fileName=multipartFile.getOriginalFilename();
String suffix=StringUtils.substring(fileName,StringUtils.indexOf(fileName,"."));
Long size=multipartFile.getSize();

//附件输入流
InputStream is=multipartFile.getInputStream();

//创建新文件存储的磁盘目录前缀、创建磁盘目录
String filePathPrefix=FORMAT.format(DateTime.now().toDate())+File.separator+module.getCode()+moduleId;
String rootPath=Constant.FilePrefix2+filePathPrefix;

//创建新的文件
String newFileName=System.nanoTime()+suffix;
String newFile=rootPath+File.separator+newFileName;

//创建目录
File file=new File(newFile);
if (!file.getParentFile().exists()){
file.getParentFile().mkdirs();
}
//直接执行数据流的转换
multipartFile.transferTo(file);

Appendix entity=new Appendix(moduleId,module.getCode(),module.getName(),newFileName,size.toString(),suffix,newFile);
appendixMapper.insertSelective(entity);

return newFile;
}


4、从中,可以看出来其核心的处理方法为MultipartFile的transferTo方法实现文件的上传存储,即multipartFile.transferTo(file);接下来,进入测试环节,三图以概括吧:




至此,对于文件的上传我们就介绍到这里了,对于文件的上传实战,Debug建议可以综合这两种加以实现,即“上传文件的请求信息的接收采用第二种方式,对于文件的处理、存储到具体的物理磁盘则采用第一种方式,即Java NIO的方式”。

好了,本篇文章我们就介绍到这里了,感兴趣的小伙伴可以关注底部Debug的技术公众号,或者加Debug的微信,拉你进“微信版”的真正技术交流群!一起学习、共同成长!


补充:

1、本文涉及到的相关的源代码可以到此地址,check出来进行查看学习:

https://gitee.com/steadyjack/SpringBootTechnology

2、目前Debug已将本文所涉及的内容整理录制成视频教程,感兴趣的小伙伴可以前往观看学习:

https://www.fightjava.com/web/index/course/detail/5

3、关注一下Debug的技术微信公众号,最新的技术文章、技术课程以及技术专栏将会第一时间在公众号发布哦!