SpringBoot系列(11):文件上傳實(shí)戰(zhàn)(提供包括NIO在內(nèi)的多種實(shí)現(xiàn)方式)


作者: 修羅debug
版權(quán)聲明:本文為博主原創(chuàng)文章,遵循 CC 4.0 by-sa 版權(quán)協(xié)議,轉(zhuǎn)載請(qǐng)附上原文出處鏈接和本聲明。

摘要: 在開發(fā)企業(yè)級(jí)應(yīng)用項(xiàng)目業(yè)務(wù)模塊期間,“上傳文件/附件”的功能相信每個(gè)小伙伴都遇見(jiàn)過(guò),甚至有的曾以代碼實(shí)戰(zhàn)過(guò)。本文Debug將帶領(lǐng)各位小伙伴重新回溫一下在Spring Web應(yīng)用中如何實(shí)現(xiàn)文件的上傳,其中提供了包括Java NIO在內(nèi)的多種方式。

內(nèi)容:在企業(yè)級(jí)應(yīng)用項(xiàng)目的開發(fā)過(guò)程中,“上傳文件/附件”這一功能相信很多小伙伴都實(shí)現(xiàn)過(guò),當(dāng)然啦,其實(shí)現(xiàn)方式也是迥異不同。接下來(lái),Debug就給各位小伙伴展示一下在Java Web應(yīng)用中如何實(shí)現(xiàn)文件的上傳。

在介紹實(shí)戰(zhàn)之前,我們先來(lái)創(chuàng)建一個(gè)數(shù)據(jù)庫(kù)表appendix,用于記錄存儲(chǔ)每個(gè)業(yè)務(wù)對(duì)象上傳上來(lái)的圖片的詳情,包括其所屬的業(yè)務(wù)對(duì)象的注解、所屬業(yè)務(wù)模塊、圖片名稱、大小、存儲(chǔ)的磁盤路徑等等,其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 '文件訪問(wèn)的磁盤目錄',
`is_active` tinyint(4) DEFAULT '1' COMMENT '是否有效(1=是;0=否)',
`create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '創(chuàng)建時(shí)間',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8 COMMENT='附件(文件)上傳記錄';


一、接下來(lái),我們先來(lái)介紹第一種方式吧,基于Java NIO的方式實(shí)現(xiàn)文件上傳功能

1、首先,創(chuàng)建FileController,用于接收前端上傳過(guò)來(lái)的文件以及其他業(yè)務(wù)信息,其源代碼如下所示:

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

/**
* 為商品上傳圖片
* 上傳文件-方式1:MultipartHttpServletRequest 接收前端參數(shù)
* @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、創(chuàng)建IFileService的實(shí)現(xiàn)類,用于處理實(shí)際的業(yè)務(wù)信息以及實(shí)現(xiàn)文件的上傳,其完整的源代碼如下所示:  

/**
* 上傳文件
* @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");
//實(shí)際的業(yè)務(wù)信息
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);
//實(shí)現(xiàn)文件的上傳
String url="";
if (item.getId()>0){
url=commonFileService.upload(multipartFile,item.getId(), Constant.SysModule.ModuleItem);
}
return url;
}

3、commonFileService.upload 方法即為真正的實(shí)現(xiàn)文件的上傳與數(shù)據(jù)庫(kù)記錄的存儲(chǔ),其完整的源代碼如下所示:  

/**
* @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();

//創(chuàng)建新文件存儲(chǔ)的磁盤目錄前綴、創(chuàng)建磁盤目錄
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);
}
//創(chuàng)建新的文件
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;
}
}

下面進(jìn)入測(cè)試環(huán)節(jié),三圖概括如下所示:  






二、最后,我們?cè)賮?lái)介紹第二種方式吧,基于傳統(tǒng)File的方式實(shí)現(xiàn)文件上傳功能

1、同樣的道理,我們?cè)贔ileController創(chuàng)建用于接收前端上傳文件請(qǐng)求的方法,其源代碼如下所示:

/**
* 上傳文件-方式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;
}


//訪問(wèn)圖片時(shí):域名 + 圖片所在的磁盤目錄-即數(shù)據(jù)庫(kù)存儲(chǔ)的file_url

在這里,我們采用了不同于第一種“面向字段獲取”的方式,而是采用“面向?qū)ο蟆钡乃枷?,將所有業(yè)務(wù)相關(guān)的信息封裝成實(shí)體對(duì)象FileUploadRequest進(jìn)行接收,而文件/附件相關(guān)的文件數(shù)據(jù)流則采用MultipartFile對(duì)象進(jìn)行接收,實(shí)體對(duì)象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、接下來(lái)是FileService處理業(yè)務(wù)信息以及文件上傳的邏輯,其完整的源代碼如下所示:  

//第二種方法
@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()方法即為核心的用于處理文件上傳的邏輯方法,其完整的源代碼如下所示:  

/**
* 上傳文件 - 傳統(tǒng)的方式
* @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();

//創(chuàng)建新文件存儲(chǔ)的磁盤目錄前綴、創(chuàng)建磁盤目錄
String filePathPrefix=FORMAT.format(DateTime.now().toDate())+File.separator+module.getCode()+moduleId;
String rootPath=Constant.FilePrefix2+filePathPrefix;

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

//創(chuàng)建目錄
File file=new File(newFile);
if (!file.getParentFile().exists()){
file.getParentFile().mkdirs();
}
//直接執(zhí)行數(shù)據(jù)流的轉(zhuǎn)換
multipartFile.transferTo(file);

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

return newFile;
}


4、從中,可以看出來(lái)其核心的處理方法為MultipartFile的transferTo方法實(shí)現(xiàn)文件的上傳存儲(chǔ),即multipartFile.transferTo(file);接下來(lái),進(jìn)入測(cè)試環(huán)節(jié),三圖以概括吧:




至此,對(duì)于文件的上傳我們就介紹到這里了,對(duì)于文件的上傳實(shí)戰(zhàn),Debug建議可以綜合這兩種加以實(shí)現(xiàn),即“上傳文件的請(qǐng)求信息的接收采用第二種方式,對(duì)于文件的處理、存儲(chǔ)到具體的物理磁盤則采用第一種方式,即Java NIO的方式”。

好了,本篇文章我們就介紹到這里了,感興趣的小伙伴可以關(guān)注底部Debug的技術(shù)公眾號(hào),或者加Debug的微信,拉你進(jìn)“微信版”的真正技術(shù)交流群!一起學(xué)習(xí)、共同成長(zhǎng)!


補(bǔ)充:

1、本文涉及到的相關(guān)的源代碼可以到此地址,check出來(lái)進(jìn)行查看學(xué)習(xí):

https://gitee.com/steadyjack/SpringBootTechnology

2、目前Debug已將本文所涉及的內(nèi)容整理錄制成視頻教程,感興趣的小伙伴可以前往觀看學(xué)習(xí):

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

3、關(guān)注一下Debug的技術(shù)微信公眾號(hào),最新的技術(shù)文章、技術(shù)課程以及技術(shù)專欄將會(huì)第一時(shí)間在公眾號(hào)發(fā)布哦!