1. 程式人生 > 其它 >& 檔案透傳整理

& 檔案透傳整理

通用檔案上傳

在工作開發當中,檔案上傳是一個很常見的功能,例如: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;
    }    
}