HttpServletRequest 转发 Web 请求导致报错:org.springframework.web.HttpMediaTypeNotSupportedException: Content

2020年01月17日 14:24 · 阅读(497) ·

开发环境

名称 版本
操作系统 Windows 10 X64
JDK JDK1.8(jdk-8u151-windows-x64)
IntelliJ IDEA IntelliJ IDEA 2018.3
Maven Maven 3.6.0

相关代码

详细可以参考:
IntelliJ IDEA 2018.3 创建 Maven 多模块(Module)项目+负载均衡
SpringBoot POI 导入,导出 Excel

浏览器

浏览器调用 Web 层的接口,传入一个 Excel 文件。

Web 层

Web 层解析 Excel 文件,得到一个 List。

  1. /**
  2. * 供应商识别号清单-上传替换
  3. *
  4. * @param file 上传文件
  5. * @return 上传成功返回 true,否则返回 false
  6. */
  7. @ApiImplicitParams({@ApiImplicitParam(paramType = "header", name = "Authorization", value = "凭证", required = true, dataType = "string"),})
  8. @PostMapping("/supplierInfoImport")
  9. @RequiresPermissions(value = {"payment"}, logical = Logical.OR)
  10. public ResponseVO supplierInfoImport(@RequestParam(value = "filename") MultipartFile file) {
  11. ResponseVO<Boolean> responseVO = new ResponseVO<>(ResponseCode.SYSTEM_EXCEPTION);
  12. responseVO.setData(false);
  13. if (file.isEmpty()) {
  14. responseVO.setCode(ResponseCode.PARAM_INVALID.value());
  15. responseVO.setMessage("文件为空!");
  16. return responseVO;
  17. }
  18. //获取文件名
  19. String fileName = file.getOriginalFilename();
  20. if(!POIExcelUtils.validateExcel(fileName)){
  21. responseVO.setCode(ResponseCode.PARAM_INVALID.value());
  22. responseVO.setMessage("必须上传 Excel 文件!");
  23. return responseVO;
  24. }
  25. InputStream inputStream = null;
  26. try {
  27. inputStream = file.getInputStream();
  28. Sheet sheet = POIExcelUtils.getSheet(fileName, inputStream);
  29. List<SupplierInfo> listData = new ArrayList<>();
  30. Iterator<Row> rowItr = sheet.rowIterator();//遍历全部非空行
  31. int count = 1;
  32. //for(Row row : sheet){ //忽略表头 if(row.getRowNum() == 0) continue; //会获取所有行,包括空行
  33. //while(rowItr.hasNext()){ Row row=rowItr.next();//会获取所有行,包括空行
  34. int rowCount = sheet.getPhysicalNumberOfRows();
  35. for (int i = 1; i < rowCount; i++) {
  36. Row row = sheet.getRow(i);
  37. //如果得到的行是空的一行,可以根据row.getFirstCellNum()是否大于等于0 来判断
  38. //该行为空那么:row.getFirstCellNum()=-1的,否则row.getFirstCellNum()=0
  39. if (null == row) break;
  40. if (row.getFirstCellNum() == -1) break;
  41. Integer col = 0;//列序号
  42. SupplierInfo data = new SupplierInfo();
  43. //供应商编码 supplier_code
  44. data.setSupplierCode(POIExcelUtils.getStringValue(row.getCell(col++)));
  45. //供应商名称 supplier_name
  46. data.setSupplierName(POIExcelUtils.getStringValue(row.getCell(col++)));
  47. //供应商类型 supplier_type
  48. data.setSupplierType(POIExcelUtils.getStringValue(row.getCell(col++)));
  49. //扣缴类型 withhold_type
  50. data.setWithholdType(POIExcelUtils.getStringValue(row.getCell(col++)));
  51. if (!supplierInfoIsNull(data)) {
  52. listData.add(data);
  53. count++;
  54. }
  55. }
  56. if (listData.size() > 0 && count == rowCount) {
  57. responseVO = foreignPaymentConsumer.replaceSupplierInfo(listData);
  58. } else {
  59. responseVO.setCode(ResponseCode.PARAM_INVALID.value());
  60. responseVO.setMessage("文件内容检验不通过,除备注字段外的所有字段都不能为空!");
  61. }
  62. } catch (IOException ex) {
  63. responseVO.setMessage(ex.getMessage());
  64. } finally {
  65. if (inputStream != null) {
  66. try {
  67. inputStream.close();
  68. } catch (IOException e) {
  69. e.printStackTrace();
  70. return responseVO;
  71. }
  72. }
  73. }
  74. return responseVO;
  75. }

Server

Web 层调用 Server 层接口,传入 List 数据,保存到数据库。

  1. /**
  2. * 替换供应商(删除之前的所有数据)
  3. *
  4. * @param param 供应商数据
  5. * @return 替换成功返回 true,失败返回 false
  6. */
  7. @PostMapping("/replaceSupplierInfo")
  8. public ResponseVO replaceSupplierInfo(@RequestBody List<SupplierInfo> param) {
  9. supplierInfoService.delAll();
  10. supplierInfoService.adds(param);
  11. ResponseVO<Boolean> responseVO = new ResponseVO<>(ResponseCode.OK);
  12. responseVO.setData(true);
  13. return responseVO;
  14. }

Feign 接口

  1. /**
  2. * 替换供应商(删除之前的所有数据)
  3. *
  4. * @param param 供应商数据
  5. * @return 替换成功返回 true,失败返回 false
  6. */
  7. @PostMapping("/replaceSupplierInfo")
  8. ResponseVO replaceSupplierInfo(@RequestBody List<SupplierInfo> param);
  9. // ResponseVO replaceSupplierInfo(@RequestParam(value = "param") SupplierInfoObjectVO param);

问题描述

在 Web 层调用 Server 层接口的时候,Server 层报错:

  1. 2019-12-27 11:51:55.050 WARN [tax-foreign-service-producer,e3531fa5e887cd88,e3531fa5e887cd88,true] 4752 --- [io-18800-exec-9] .m.m.a.ExceptionHandlerExceptionResolver : Resolved [org.springframework.web.HttpMediaTypeNotSupportedException: Content type 'multipart/form-data;boundary=--------------------------595537775916308170362405;charset=UTF-8' not supported]

尝试方法1-把文件传到 Server 去解析

修改 Feign 接口和 Server 接口的实现代码

  1. /**
  2. * 替换供应商(删除之前的所有数据)
  3. *
  4. * @param param 供应商数据
  5. * @return 替换成功返回 true,失败返回 false
  6. */
  7. @PostMapping("/replaceSupplierInfo")
  8. ResponseVO replaceSupplierInfo(@RequestParam(value = "filename") MultipartFile file);

结果还是失败,文件从 Web 传不到 Server 去(框架的坑)

尝试方法2-Feign 接口和 Server 添加 Header

  1. /**
  2. * 替换供应商(删除之前的所有数据)
  3. *
  4. * @param param 供应商数据
  5. * @return 替换成功返回 true,失败返回 false
  6. */
  7. @Headers("Content-Type: application/json")
  8. @PostMapping("/replaceSupplierInfo")
  9. ResponseVO replaceSupplierInfo(@RequestBody List<SupplierInfo> param);

还是失败,报同样的错误

尝试方法3-Feign 接口和 Server 添加 consumes 标记

  1. /**
  2. * 替换供应商(删除之前的所有数据)
  3. *
  4. * @param param 供应商数据
  5. * @return 替换成功返回 true,失败返回 false
  6. */
  7. @RequestMapping(value = "/replaceSupplierInfo", method = RequestMethod.POST, consumes = "application/json")
  8. ResponseVO replaceSupplierInfo(@RequestBody List<SupplierInfo> param);

还是报错!!

原因分析

描述是说 Server 解析不到类型 content-type:multipart/form-data

可是我 Feign 和 Server 接口的参数定义是 @RequestBody List<SupplierInfo> param 啊!很奇怪!

好像是 Web 层调用 Feign 接口的时候,HttpServletRequest 把浏览器的 ccontent-type:multipart/form-data 转发到 Server 去了。

那么如果我们在 Web 层调用 Feign 接口的时候,修改 content-typecontent-type:application/json 就可以了。

问题解决

在框架的 Web 层发现了一个配置类——FeignInterceptor,这个配置类有去使用 HttpServletRequest 重新透传调用者 request head

在这个类里面相关部分加入下面的代码:

  1. if(!"content-type".equals(name)){
  2. template.header(name,value);
  3. }
  4. template.header("content-type","application/json");

加入后的代码为(FeignInterceptor.java):

  1. package com.foreign.payment.web.config;
  2. import feign.RequestInterceptor;
  3. import feign.RequestTemplate;
  4. import lombok.extern.slf4j.Slf4j;
  5. import org.springframework.context.annotation.Configuration;
  6. import org.springframework.web.context.request.RequestAttributes;
  7. import org.springframework.web.context.request.RequestContextHolder;
  8. import org.springframework.web.context.request.ServletRequestAttributes;
  9. import javax.servlet.http.HttpServletRequest;
  10. import java.nio.charset.StandardCharsets;
  11. import java.util.Enumeration;
  12. /**
  13. * web通过FeignClient 调用service,防止request head头部信息丢失
  14. * 重新透传调用者request head
  15. * 将用户的登录id放在了请求头中传递给内部服务。但是当内部服务之间存在feign调用时,那么请求头信息会在feign请求的时候传递吗?
  16. * 不会,请求的头信息和请求参数都不会进行传递。但是我们可以通过通过实现RequestInterceptor接口,完成对所有的Feign请求,传递请求头和请求参数
  17. * 原文:https://blog.csdn.net/AaronSimon/article/details/82711036 *
  18. */
  19. @Configuration
  20. @Slf4j
  21. public class FeignInterceptor implements RequestInterceptor {
  22. @Override
  23. public void apply(RequestTemplate template) {
  24. RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
  25. if (requestAttributes == null) {
  26. return;
  27. }
  28. HttpServletRequest request = ((ServletRequestAttributes) requestAttributes).getRequest();
  29. Enumeration<String> headerNames = request.getHeaderNames();
  30. if (headerNames != null) {
  31. while (headerNames.hasMoreElements()) {
  32. String name = headerNames.nextElement();
  33. Enumeration<String> values = request.getHeaders(name);
  34. while (values.hasMoreElements()) {
  35. String value = values.nextElement();
  36. //template.header(name, value);
  37. if(!"content-type".equals(name)){
  38. template.header(name,value);
  39. }
  40. }
  41. }
  42. template.header("content-type","application/json");
  43. }
  44. //Enumeration<String> bodyNames = request.getParameterNames();
  45. //StringBuffer body = new StringBuffer();
  46. //if (bodyNames != null) {
  47. // while (bodyNames.hasMoreElements()) {
  48. // String name = bodyNames.nextElement();
  49. // String values = request.getParameter(name);
  50. // body.append(name).append("=").append(values).append("&");
  51. // }
  52. //}
  53. //
  54. //
  55. //if (body.length() != 0) {
  56. // body.deleteCharAt(body.length() - 1);
  57. // body.append(new String(template.body(), StandardCharsets.UTF_8));
  58. // template.body(body.toString());
  59. //}
  60. }
  61. }

加入这段代码后,问题解决。数据能够从 Web 传到 Server 了。

这个是这个框架的坑,只是暂时解决了这个问题。

这样存在一个问题,每个 Web 传到 Server 的请求都必须是 content-type:application/json

看以后还有什么更好的办法完全解决!

问题解决-第二种办法

注释掉 FeignInterceptor.java,新增配置类 FeignConfiguration.java

  1. package com.foreign.payment.web.config;
  2. import feign.RequestInterceptor;
  3. import org.springframework.context.annotation.Bean;
  4. import org.springframework.stereotype.Component;
  5. import org.springframework.util.StringUtils;
  6. import org.springframework.web.context.request.RequestContextHolder;
  7. import org.springframework.web.context.request.ServletRequestAttributes;
  8. import javax.servlet.http.HttpServletRequest;
  9. /**
  10. * feign 之间传递header,传少量感兴趣的key,别去解析其他的value,否则会抛出意想不到的bug
  11. *
  12. * @author dragon
  13. * @date 2020/01/17
  14. */
  15. @Component
  16. public class FeignConfiguration {
  17. @Bean
  18. public RequestInterceptor headerInterceptor() {
  19. return requestTemplate -> {
  20. ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder
  21. .getRequestAttributes();
  22. if (attributes == null) {
  23. return;
  24. }
  25. HttpServletRequest request = attributes.getRequest();
  26. String token= request.getHeader("Authorization");
  27. if (!StringUtils.isEmpty(token)) {
  28. requestTemplate.header("Authorization", token);
  29. }
  30. };
  31. }
  32. }

这个配置类里面的 AuthorizationHeader 里面 Token 的名称,是需要在 Web 和 Server 之间传递的 Header 参数。