通用文件上传
在工作开发当中,文件上传是一个很常见的功能,例如: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("
转发路径响应信息:{}", 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("
转发路径响应信息:{}", 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("
");
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("
转发路径响应信息:{}",responeEntity);
return responeEntity;
}
}
return null;
}
}