& 檔案透傳整理
阿新 • • 發佈:2021-11-13
通用檔案上傳
在工作開發當中,檔案上傳是一個很常見的功能,例如:Excel匯入解析、司機身份證OCR識別等,都會用到檔案上傳功能。
不管是微服務架構應用,還是單體架構應用,都會有一個通用檔案上傳介面,來實現統一的檔案上傳功能。例:
@ApiOperationSupport(order = 1) @ApiOperation(value = "單檔案上傳") @ApiImplicitParams(value = { @ApiImplicitParam(name = "file", value = "檔案", required = true, dataTypeClass = MultipartFile.class), @ApiImplicitParam(name = "type", value = "檔案用途<br> " + "1:司機身份證資訊<br> " + "2:OCR識別<br> " + "3:備案資料Excel匯入<br> " + "4:物料資料Excel匯入", required = true, dataTypeClass = String.class) }) @PostMapping("/upload") public ServerResponse<FileDto> upload(@RequestParam("file") MultipartFile file, @RequestParam(value = "type",required = false) String type){ return ServerResponse.createBySuccess(uploadService.upload(file,type)); }
對於Web前端開發同學而言,往往需要先呼叫檔案上傳介面,拿到檔案上傳後的id,再呼叫業務介面,才能完成該功能。
如果能只調用一次介面,就完成上面的邏輯,是否可行呢?
檔案透傳-單體架構應用
整體分析:
檔案上傳介面:返回給前端的是檔案的詳細資訊,例如:檔案id、檔案路徑、檔案大小等。
業務介面:接收的是檔案的id,然後根據檔案id獲取檔案資訊,處理業務邏輯。
檔案透傳介面:將檔案上傳完之後,接著呼叫業務介面,將業務介面響應的資訊原封不動的返回給前端。
程式碼示例:
環境:SpringBoot、華為雲檔案儲存等,使用什麼環境無所謂,思路都一樣。
/** * 功能描述: 檔案上傳(支援批量)並透傳到其他介面 */ //若使用此介面,業務介面接收檔案id時,必須使用fileIds進行接收 //application/x-www-form-urlencoded //application/json @ApiOperationSupport(order = 3) @ApiOperation(value = "檔案上傳並透傳其他介面") @ApiImplicitParams(value = { @ApiImplicitParam(name = "file1", value = "檔案(多個檔案file1,file2,file3...)", required = true, dataTypeClass = MultipartFile.class), @ApiImplicitParam(name = "type", value = "檔案用途<br> " + "1:司機身份證資訊<br> " + "2:OCR識別<br> " + "3:備案資料Excel匯入<br> " + "4:物料資料Excel匯入", required = true, dataTypeClass = String.class), @ApiImplicitParam(name = "destUrl", value = "透傳介面的URL(當destUrl為空時,代表不透傳其他介面,只進行檔案上傳)", required = false, dataTypeClass = String.class), @ApiImplicitParam(name = "contentType", value = "透傳介面的請求方式<br>" + "application/x-www-form-urlencoded<br>" + "application/json<br>" + "當destUrl有值時,此項必填", required = false, dataTypeClass = String.class), @ApiImplicitParam(name = "otherParam", value = "透傳介面的其他引數,如沒有可不填", required = false, dataTypeClass = String.class), }) @PostMapping("/uploadPassthrough") public Object uploadPassthrough(HttpServletRequest request){ return uploadService.uploadPassthrough(request); }
@Autowired private FileService fileService; @Value("${server.port}") private String port; @Autowired private TransactionService transactionService; private static final String DEST_URL = "destUrl"; private static final String TOKEN = "token"; private static final String CONTENT_TYPE = "contentType"; private static final String FILE_TYPE = "fileType"; private static final String FILE_IDS = "fileIds"; @Override public Object uploadPassthrough(HttpServletRequest request) { String token = request.getHeader(TOKEN); if (StrUtils.isBlank(token)) { throw new BusinessTypeException("令牌為空!"); } if (request instanceof StandardMultipartHttpServletRequest) { StandardMultipartHttpServletRequest multipartRequest = (StandardMultipartHttpServletRequest) request; Map<String, String[]> paramterMap = multipartRequest.getParameterMap(); String[] destUrls = paramterMap.get(DEST_URL); String[] contentTypes = paramterMap.get(CONTENT_TYPE); String[] fileTypes = paramterMap.get(FILE_TYPE); if(!arrayIsEmpty(fileTypes)){ throw new BusinessTypeException("引數不全!"); } //destUrls不為空 但contentType為空 if (arrayIsEmpty(destUrls) && !arrayIsEmpty(contentTypes)) { throw new BusinessTypeException("引數不全!"); } List<Long> fileIdList = new ArrayList<>(); List<FileDto> fileDtoList = new ArrayList<>(); MultiValueMap<String, MultipartFile> fileMultiValueMap = multipartRequest.getMultiFileMap(); if (fileMultiValueMap.isEmpty()) { throw new BusinessTypeException("上傳檔案為空!"); } //手動開啟事務 transactionService.begin(); try { for (Map.Entry<String, List<MultipartFile>> fileEntry : fileMultiValueMap.entrySet()) { if (fileEntry.getValue() != null && !fileEntry.getValue().isEmpty()) { MultipartFile file = fileEntry.getValue().get(0); //this.getFileDto()是上傳檔案,並返回FileDto FileDto fileDto = this.getFileDto(file, fileTypes[0]); //將檔案資訊儲存到資料庫 FileDto upload = fileService.save(fileDto); if (null != upload.getId()) { fileIdList.add(upload.getId()); fileDtoList.add(upload); } } } if (CollectionUtils.isEmpty(fileIdList)) { throw new BusinessTypeException("上傳檔案失敗,請稍候重試!"); } }catch (Exception e){ //此處可以選擇回滾並刪除上傳的檔案 //transactionService.rollback(); //示例程式碼 無需回滾 (有檔案刪除任務排程) log.error(e.getMessage(),e); throw new BusinessTypeException("上傳檔案失敗,請稍候重試!"); }finally { transactionService.commit(); } if (arrayIsEmpty(destUrls)) { paramterMap.remove(DEST_URL); paramterMap.remove(CONTENT_TYPE); String destUrl = destUrls[0]; String contentType = contentTypes[0]; //開始呼叫業務介面 String restString = this.sendPost(token, destUrl, contentType, paramterMap, fileIdList); if (StrUtils.isNotBlank(restString)) { return restString; } else { throw new BusinessTypeException("操作失敗"); } } else { //把檔案資訊返回 return ServerResponse.createBySuccess(fileDtoList); } } throw new BusinessTypeException("操作失敗"); } /** * @Description 呼叫業務介面 必須為Post請求 * * @Param token 令牌 * @Param destUrl 業務介面的路徑 * @Param contentType 業務介面的請求方式 * @Param paramterMap 業務介面的引數 * @Param fileIdList 檔案id集合 * @return 業務介面的響應資訊 **/ private String sendPost(String token, String destUrl, String contentType, Map<String, String[]> paramterMap, List<Long> fileIdList) { if (paramterMap != null && !paramterMap.isEmpty()) { if (StrUtils.isBlank(destUrl)) { return null; } if (!destUrl.startsWith("/")) { destUrl = "/".concat(destUrl); } log.info("轉發路徑destUrl:{}", destUrl); //第一種請求方式 post/json if (contentType.equals(MediaType.APPLICATION_FORM_URLENCODED.toString())) { HttpHeaders headers = new HttpHeaders(); headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED); headers.add(TOKEN, token); MultiValueMap<String, Object> multiValueMap = new LinkedMultiValueMap<>(); paramterMap.forEach((k, v) -> multiValueMap.add(k, v[0])); if (fileIdList != null && !fileIdList.isEmpty()) { fileIdList.forEach(o -> multiValueMap.add(FILE_IDS, o)); } //請求體 HttpEntity<MultiValueMap<String, Object>> formEntity = new HttpEntity<>(multiValueMap, headers); String responeEntity = RestTemplateUtils.getRestTemplate().postForObject(String.format("http://127.0.0.1:%s", port).concat(destUrl), formEntity, String.class); log.info("\r\n轉發路徑響應資訊:{}", responeEntity); return responeEntity; //第二種請求方式 post/param } else { HttpHeaders headers = new HttpHeaders(); headers.setContentType(MediaType.APPLICATION_JSON); headers.add(TOKEN, token); Map<String, Object> reqMap = new HashMap<>(); paramterMap.forEach((k, v) -> reqMap.put(k, v[0])); if (fileIdList != null && !fileIdList.isEmpty()) { fileIdList.forEach(o -> reqMap.put(FILE_IDS, o)); } String reqData = JsonUtil.beanToJson(reqMap); log.error("reqData:{}",reqData); HttpEntity<Object> requestEntity = new HttpEntity<>(reqData, headers); ResponseEntity<String> exchange = RestTemplateUtils.getRestTemplate().exchange(String.format("http://127.0.0.1:%s", port).concat(destUrl), HttpMethod.POST, requestEntity, String.class); log.info("\r\n轉發路徑響應資訊:{}", exchange); return exchange.getBody(); } } return null; }
測試
@ApiOperation(value = "測試透傳介面(application/x-www-form-urlencoded請求方式)")
@ApiOperationSupport(order = 4)
@PostMapping("/api/test1")
public ServerResponse<Object> uploadPassthroughTest1(@RequestParam String fileIds, @RequestParam String customer){
FileDto fileById = fileService.findFileById(Long.parseLong(fileIds));
System.out.println("fileById:"+fileById);
return ServerResponse.createBySuccess(fileIds + customer);
}
@ApiOperation(value = "測試透傳介面(application/json請求方式)")
@ApiOperationSupport(order = 5)
@PostMapping("/api/test2")
public ServerResponse<Object> uploadPassthroughTest2(@RequestBody Map map){
return ServerResponse.createBySuccess(JSONUtil.toJsonStr(map));
}
以上我們完成了對單體架構應用的檔案透傳介面的改造!!!
檔案透傳-微服務架構應用
整體分析:
微服務架構應用與單體架構應用的檔案透傳就一個區別:透傳
單體架構應用是透傳到介面
微服務架構應用是透傳到服務
那麼在介面透傳時,只需要將介面轉發到閘道器服務即可,由閘道器再轉發到子服務。
環境:SpringCloud、SpringBoot、FastDFS等,使用什麼環境無所謂,思路都一樣。
需要注意:
下面程式碼示例中的:private static final String SERVICE_NAME="API-GATEWAY";
代表閘道器服務名,
需要與閘道器的application.name
一致
程式碼示例:
@RestController
@RequestMapping("fileUpDown")
@Api(value = "檔案上傳下載", tags = "提供檔案上傳下載介面")
public class UploadDownController {
private static final Logger logger=LoggerFactory.getLogger(UploadDownController.class);
private static final String DOWN_FILE="L";
private static final String DEL_FILE="D";
private static final String INVALID_TOKEN = "invalid token";
private static final String SERVICE_NAME="API-GATEWAY";
@Autowired
private FastDFSClient fastClient;
// 宣告 bean
@Bean
@LoadBalanced // 增加 load balance 特性.
public RestTemplate restTemplate() {
return new RestTemplate();
}
// 注入
@Autowired
private RestTemplate restTemplate;
@Autowired
private UpDownFileService logService;
/**
* 功能描述: 檔案上傳(支援批量)並透傳到其他服務
*/
@PostMapping("/upload")
@ApiOperation(value = "檔案上傳(支援批量)並透傳到其他服務" , notes = "head中需增加userId與token",produces = "application/json")
public String uploadFiles(HttpServletRequest request, HttpServletResponse response) {
//操作通用返回類
ResultResponse resp=new ResultResponse();
String userId=request.getHeader("userId");
String token=request.getHeader("token");
if(StringUtils.isBlank(userId)||StringUtils.isBlank(token)) {
resp.setCode("401");
resp.setMessage(INVALID_TOKEN);
logger.error("token或userId為空,請求拒絕!userId:{},token:{}",userId,token);
return JSONObject.toJSONString(resp);
}
if(request instanceof StandardMultipartHttpServletRequest) {
long start=System.currentTimeMillis();
List<String> fileNameList= new ArrayList<String>();
List<String> filePathList= new ArrayList<String>();
List<String> fileSizeList= new ArrayList<String>();
StandardMultipartHttpServletRequest multipartRequest=(StandardMultipartHttpServletRequest)request;
MultiValueMap<String, MultipartFile> fileMultiValueMap = multipartRequest.getMultiFileMap();
if(fileMultiValueMap!=null&&!fileMultiValueMap.isEmpty()) {
for (Map.Entry<String,List<MultipartFile>> fileEntry: fileMultiValueMap.entrySet()) {
if(fileEntry.getValue()!=null&&!fileEntry.getValue().isEmpty()) {
try {
UpFilePo upFilePo=new UpFilePo();
MultipartFile file=fileEntry.getValue().get(0);
String utf8FileName=file.getOriginalFilename();
//檔案上傳操作
String filePath=fastClient.uploadFileWithMultipart(file);
upFilePo.setFileNm(utf8FileName);
upFilePo.setFilePath(filePath);
upFilePo.setFileSize(String.valueOf(file.getSize()));
upFilePo.setUsrId(Long.parseLong(userId));
//log insert
logService.insert(upFilePo);
fileNameList.add(file.getOriginalFilename());
filePathList.add(filePath);
fileSizeList.add(String.valueOf(file.getSize()));
logger.info("檔案上傳成功!filePath:{} 耗時:{}",filePath,System.currentTimeMillis()-start);
} catch (FastDFSException e) {
logger.error(e.getMessage(), e);
resp.setCode(ConstantCode.CommonCode.FILE_UP_EXCEPTION.getCode());
resp.setMessage(ConstantCode.CommonCode.FILE_UP_EXCEPTION.getMsg());
}catch (Exception e) {
logger.error(e.getMessage(), e);
resp.setCode(ConstantCode.CommonCode.EXCEPTION.getCode());
resp.setMessage(ConstantCode.CommonCode.EXCEPTION.getMsg());
}
}
}
}
Map<String, String[]> paramterMap=multipartRequest.getParameterMap();
String[] destUrls=paramterMap.get("destUrl");
if(destUrls!=null&&destUrls[0].length()>0) {
//移除重定向url引數
paramterMap.remove("destUrl");
String destUrl=destUrls[0];
String restString= sendPost(userId,token,destUrl, paramterMap,fileNameList,filePathList, fileSizeList);
if(StringUtils.isNotBlank(restString)) {
return restString;
}else {
resp.setCode(ConstantCode.CommonCode.EXCEPTION.getCode());
resp.setMessage("響應結果為空!");
}
}else {
//把檔案路徑返回
resp.setData(filePathList);
}
//無檔案直接透傳
}else if(request instanceof RequestFacade){
RequestFacade req=(RequestFacade)request;
Map<String, String[]> paramterMap=new LinkedHashMap<String,String[]>();
paramterMap.putAll(req.getParameterMap());
String[] destUrls=paramterMap.get("destUrl");
if(destUrls!=null&&destUrls.length>0) {
//移除重定向url引數
paramterMap.remove("destUrl");
String destUrl=destUrls[0];
String restString= sendPost(userId,token,destUrl, paramterMap,null,null, null);
if(StringUtils.isNotBlank(restString)) {
return restString;
}else {
resp.setCode(ConstantCode.CommonCode.EXCEPTION.getCode());
resp.setMessage("響應結果為空!");
}
}else {
//返回失敗
resp.setCode(ConstantCode.CommonCode.EXCEPTION.getCode());
resp.setMessage(ConstantCode.CommonCode.EXCEPTION.getMsg());
}
}
return JSONObject.toJSONString(resp);
}
/**
* 傳送post請求到其他服務
*
* @param userId
* @param token
* @param paramterMap
* 請求的所有引數
* @param fileNameList
* 檔名集合
* @param filePathList
* 檔案路徑集合
* @param fileSizeList
* 檔案大小(bytes)
* @return
*/
private String sendPost(String userId,
String token,
String destUrl,
Map<String, String[]> paramterMap,
List<String> fileNameList,
List<String> filePathList,
List<String> fileSizeList) {
if(paramterMap!=null&&!paramterMap.isEmpty()) {
if(StringUtils.isNotBlank(destUrl)) {
if(!destUrl.startsWith("/")) {
destUrl="/".concat(destUrl);
}
logger.info("轉發路徑destUrl:{}",destUrl);
//設定請求頭
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
headers.add("Accept", MediaType.APPLICATION_JSON_UTF8_VALUE);
headers.add("userId", userId);
headers.add("token", token);
MultiValueMap<String, Object> multiValueMap= new LinkedMultiValueMap<String, Object>();
paramterMap.forEach((k,v)-> multiValueMap.add(k, v[0]));
if(fileNameList!=null&&!fileNameList.isEmpty()) {
fileNameList.forEach(o-> multiValueMap.add("fileNames", o));
}
if(filePathList!=null&&!filePathList.isEmpty()) {
filePathList.forEach(o-> multiValueMap.add("filePaths", o));
}
if(fileSizeList!=null&&!fileSizeList.isEmpty()) {
fileSizeList.forEach(o-> multiValueMap.add("fileSize", o));
}
//請求體
HttpEntity<MultiValueMap<String, Object>> formEntity = new HttpEntity<MultiValueMap<String, Object>>(multiValueMap, headers);
String responeEntity=null;
try {
//此處轉發到閘道器服務
responeEntity=restTemplate.postForObject(String.format("http://%s", SERVICE_NAME).concat(destUrl), formEntity,String.class);
}catch(Exception e){
logger.error("\r\n");
logger.error(e.getMessage(), e);
//如果發生異常刪除上傳檔案
if(filePathList!=null&&!filePathList.isEmpty()) {
filePathList.forEach(filepath-> {
try {
fastClient.deleteFile(filepath);
FileLogPo downFilePo=new FileLogPo();
downFilePo.setUsrId(Long.parseLong(userId));
downFilePo.setFilePath(filepath);
downFilePo.setType(DEL_FILE);//刪除
logService.insert(downFilePo);
} catch (FastDFSException e1) {
logger.error(e1.getMessage(),e1);
}
});
}
ResultResponse resp=new ResultResponse();
resp.setCode(ConstantCode.CommonCode.EXCEPTION.getCode());
resp.setMessage(e.getMessage());
return JSONObject.toJSONString(resp);
}
logger.info("\r\n轉發路徑響應資訊:{}",responeEntity);
return responeEntity;
}
}
return null;
}
}