文件下载接口的需求:
- 接口可以正常下载时, 响应的content-type为application/octet-stream;charset=UTF-8. 返回的是一个文件;
- 接口出现异常时, 响应的content-type为application/json, 返回的是包含错误码、错误信息的json格式;
过程一: 文件写入response.outputStream, 方法返回值为void
@RequestMapping("/downloadFile")
public void downloadFile(@RequestParam("file") String fileName, HttpServletResponse response) throws IOException {
response.setHeader("Content-Disposition", "attachment;filename=" + fileName);
response.setContentType("application/octet-stream;charset=UTF-8");// 设置response内容的类型
downloadService.downloadFile(fileName, response);
}
代码缺陷:
在接口出现异常时无错误信息返回.
在controller的AOP切面中, 通常对切入点这样处理: 会对返回值做一个全局报文封装
CommonResp resp = new CommonResp(ErrorCode.SERVER_ERROR, null);
try {
resp = (CommonResp) point.proceed();
return resp;
} catch (BusinessException ex) {
resp.setCode(ex.getErrorCode());
resp.setMessage(ex.getErrorMessage());
return resp;
} catch (Exception ex) {
LOGGER.error("服务端异常:{}", new Object[]{point.getTarget().getClass().getName(), point.getSignature().getName()}, ex);
return resp;
} finally {
// 设置通用返回值
resp.setRequestId(requestId);
resp.setTimestamp(now);
}
因此, 这样就会出现一个隐式约定: controller的方法必须有返回值而且是通用返回值CommonResp.
如果文件下载接口方法返回值为void, 会导致在AOP切面中对resp的后续处理出现NPE:
- 在接口正常情况下, 文件已经写入输出流, 因为servlet的机制, 接口依然可以获得返回, AOP切面中产生的NPE会在tomcat的StandardWrapperValve中捕获, 并写入tomcat日志
- 在接口异常情况下, 文件没有写入输出流, 在AOP切面中捕获了接口异常, 返回的resp为NULL, 在请求接口处得不到任何返回, 在AOP的finally也会因为resp为NULL产生NPE.
过程二: 将文件写入response.outputStream, 同时方法也有返回值, 但是没有设置contentType和没有主动关闭输出流
@RequestMapping("/downloadFile")
public CommonResp downloadFile(@RequestParam("file") String fileName, HttpServletResponse response) throws IOException {
//response.setHeader("Content-Disposition", "attachment;filename=" + fileName);
//response.setContentType("application/octet-stream;charset=UTF-8");// 设置response内容的类型
try {
downloadService.downloadFile(fileName, response);
} catch (BusinessException e) {
//response.setContentType(ContentType.APPLICATION_JSON.getMimeType());
return new CommonResp<>(e.getErrorCode(), e.getErrorMessage());
}
return CommonResp.ok();
}
因为没有主动关闭输出流, 会导致在返回的内容中, 追加了方法的返回值信息. 类似这样
(文件内容:)XXXXXXXXX{"code":0,"message":"success","timestamp":1635334499414,"request_id":"bb35896b15dd41e8a9d019bb727a547d"}
过程三: 将文件写入response.outputStream, 同时方法也有返回值, 没有设置contentType, 主动关闭输出流
@RequestMapping("/downloadFile")
public CommonResp downloadFile(@RequestParam("file") String fileName, HttpServletResponse response) throws IOException {
//response.setHeader("Content-Disposition", "attachment;filename=" + fileName);
//response.setContentType("application/octet-stream;charset=UTF-8");// 设置response内容的类型
try {
downloadService.downloadFile(fileName, response);
response.getOutputStream().close();
} catch (BusinessException e) {
//response.setContentType(ContentType.APPLICATION_JSON.getMimeType());
return new CommonResp<>(e.getErrorCode(), e.getErrorMessage());
}
return CommonResp.ok();
}
返回的内容不再追加方法返回值信息.
过程四: 将文件写入response.outputStream, 同时方法也有返回值, 设置contentType, 无需再主动关闭输出流
@RequestMapping("/downloadFile")
public CommonResp downloadFile(@RequestParam("file") String fileName, HttpServletResponse response) throws IOException {
response.setHeader("Content-Disposition", "attachment;filename=" + fileName);
response.setContentType("application/octet-stream;charset=UTF-8");// 设置response内容的类型
try {
downloadService.downloadFile(fileName, response);
} catch (BusinessException e) {
response.setContentType(ContentType.APPLICATION_JSON.getMimeType());
return new CommonResp<>(e.getErrorCode(), e.getErrorMessage());
}
return CommonResp.ok();
}
设置contentType后, 接口以文件的形式返回. 因为spring的机制, 无需主动关闭输出流, 文件中不会追加方法返回值信息.