multipart/form-data 在低版本spring和webFlux中的解析

背景

最近在做一个技术项目的迁移,将老的springMVC项目迁移到SpringWebFlux项目中,在流量迁移过程中发现有一个业务方传过来的参数新项目拿不到,究其原因是老版本的spring解析器和新版本的解析器对multipart/form-data类型的contentType解析方式不一致。

复现请求

发送请求

@Autowired
private RestTemplate restTemplate;

@Test
public void testMutiplepart() {
  String url = "https://localhost:8080/api/test?category_id=115348&app_id=1000912&timestamp=1673339039&sig=041b5b6e4e6eae430208f9fbc45dc3a8";
  MultiValueMap<String, Object> map = new LinkedMultiValueMap<>(5);
  map.add("category_id", "115348");
  //注意,这里是int类型
  map.add("app_id", 1000912);
  //注意,这里是String类型      
  map.add("app_food_code", "1253");
  map.add("timestamp", 1673339039);
  map.add("sig", "041b5b6e4e6eae430208f9fbc45dc3a8");
  String result = restTemplate.postForObject(url, map, String.class);
  System.out.println(result);
}

请求原始body

--xe3PJOFEjosCT8h0phpVSBCgSQK7kLtcY0JEj
Content-Disposition: form-data; name="category_id"
Content-Type: text/plain;charset=UTF-8
Content-Length: 6

115348
--xe3PJOFEjosCT8h0phpVSBCgSQK7kLtcY0JEj
Content-Disposition: form-data; name="app_id"
Content-Type: application/json

1000912
--xe3PJOFEjosCT8h0phpVSBCgSQK7kLtcY0JEj
Content-Disposition: form-data; name="app_food_code"
Content-Type: text/plain;charset=UTF-8
Content-Length: 4

1253
--xe3PJOFEjosCT8h0phpVSBCgSQK7kLtcY0JEj
Content-Disposition: form-data; name="timestamp"
Content-Type: application/json

1673339039
--xe3PJOFEjosCT8h0phpVSBCgSQK7kLtcY0JEj
Content-Disposition: form-data; name="sig"
Content-Type: text/plain;charset=UTF-8
Content-Length: 32

041b5b6e4e6eae430208f9fbc45dc3a8

webFlux解析

在解析每一个part的时候,会根据header
org.springframework.http.codec.multipart.PartGenerator#newPart

private void newPart(State currentState, HttpHeaders headers) {
    //如果是formField
    if (isFormField(headers)) {
        changeStateInternal(new FormFieldState(headers));
        requestToken();
    }
    else if (!this.streaming) {
        changeStateInternal(new InMemoryState(headers));
        requestToken();
    }
    else {
        Flux<DataBuffer> streamingContent = Flux.create(contentSink -> {
            State newState = new StreamingState(contentSink);
            if (changeState(currentState, newState)) {
                contentSink.onRequest(l -> requestToken());
                requestToken();
            }
        });
        emitPart(DefaultParts.part(headers, streamingContent));
    }
}

判断是否是formFiled的条件

private static boolean isFormField(HttpHeaders headers) {
    MediaType contentType = headers.getContentType();
    //判断条件是MediType必须为Text/Plain的子类型
    return (contentType == null || MediaType.TEXT_PLAIN.equalsTypeAndSubtype(contentType))
            && headers.getContentDisposition().getFilename() == null;
}

CommonsMultipartResolver解析

org.apache.commons.fileupload.FileUploadBase.FileItemIteratorImpl#findNextItem
注意看,老版本的解析是判断文件名是否为空来决定它是不是一个formField。

String fieldName = getFieldName(headers);
if (fieldName != null) {
    String subContentType = headers.getHeader(CONTENT_TYPE);
    if (subContentType != null
            &&  subContentType.toLowerCase(Locale.ENGLISH)
                    .startsWith(MULTIPART_MIXED)) {
        currentFieldName = fieldName;
        // Multiple files associated with this field name
        byte[] subBoundary = getBoundary(subContentType);
        multi.setBoundary(subBoundary);
        skipPreamble = true;
        continue;
    }
    String fileName = getFileName(headers);
    currentItem = new FileItemStreamImpl(fileName,
            fieldName, headers.getHeader(CONTENT_TYPE),
            //如果文件名为空,则是表单类型                             
            fileName == null, getContentLength(headers));
    currentItem.setHeaders(headers);
    notifier.noteItem();
    itemValid = true;
    return true;
}

如果子类型为application/json,也会被直接解析成formField。
在这里插入图片描述
CommonsMultipartResolver解析Multiparts

protected MultipartParsingResult parseFileItems(List<FileItem> fileItems, String encoding) {
  MultiValueMap<String, MultipartFile> multipartFiles = new LinkedMultiValueMap<String, MultipartFile>();
  Map<String, String[]> multipartParameters = new HashMap<String, String[]>();
  Map<String, String> multipartParameterContentTypes = new HashMap<String, String>();
  for (FileItem fileItem : fileItems) {
    //这里判断是否是表单类型的
    if (fileItem.isFormField()) {
      String value;
      String partEncoding = determineEncoding(fileItem.getContentType(), encoding);
      if (partEncoding != null) {
        try {
          value = fileItem.getString(partEncoding);
        }
        catch (UnsupportedEncodingException ex) {
          if (logger.isWarnEnabled()) {
            logger.warn("Could not decode multipart item '" + fileItem.getFieldName() +
                        "' with encoding '" + partEncoding + "': using platform default");
          }
          value = fileItem.getString();
        }
      }
      else {
        value = fileItem.getString();
      }
      String[] curParam = multipartParameters.get(fileItem.getFieldName());
      if (curParam == null) {
        // simple form field
        multipartParameters.put(fileItem.getFieldName(), new String[] {value});
      }
      else {
        // array of simple form fields
        String[] newParam = StringUtils.addStringToArray(curParam, value);
        multipartParameters.put(fileItem.getFieldName(), newParam);
      }
      multipartParameterContentTypes.put(fileItem.getFieldName(), fileItem.getContentType());
    }
    else {
      // multipart file field
      CommonsMultipartFile file = new CommonsMultipartFile(fileItem);
      multipartFiles.add(file.getName(), file);
      if (logger.isDebugEnabled()) {
        logger.debug("Found multipart file [" + file.getName() + "] of size " + file.getSize() +
                     " bytes with original filename [" + file.getOriginalFilename() + "], stored " +
                     file.getStorageDescription());
      }
    }
  }
  return new MultipartParsingResult(multipartFiles, multipartParameters, multipartParameterContentTypes);
}

解决方案

1.将MultiValueMap里面的value都转为String类型。

@Autowired
private RestTemplate restTemplate;

@Test
public void testMutiplepart() {
  String url = "https://localhost:8080/api/test?category_id=115348&app_id=1000912&timestamp=1673339039&sig=041b5b6e4e6eae430208f9fbc45dc3a8";
  MultiValueMap<String, Object> map = new LinkedMultiValueMap<>(5);
  map.add("category_id", "115348");
  map.add("app_id", "1000912");
  //注意,这里是String类型      
  map.add("app_food_code", "1253");
  map.add("timestamp", "1673339039");
  map.add("sig", "041b5b6e4e6eae430208f9fbc45dc3a8");
  String result = restTemplate.postForObject(url, map, String.class);
  System.out.println(result);
}

2.指定contentType

public void testMutiplepart() {
 String url = "http://localhost:8081/multipartPart?category_id=115348&app_id=1000912&timestamp=1673339039&sig=041b5b6e4e6eae430208f9fbc45dc3a8";
 MultiValueMap<String, Object> map = new LinkedMultiValueMap<>(5);
 map.add("category_id", "115348");
 map.add("app_id", 1000912);
 map.add("app_food_code", "1253");
 map.add("timestamp", 1673339039);
 map.add("sig", "041b5b6e4e6eae430208f9fbc45dc3a8");
 HttpHeaders headers = new HttpHeaders();

 headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
 HttpEntity<MultiValueMap<String, Object>> request = new HttpEntity<>(map, headers);
 ResponseEntity<String> exchange = restTemplate.exchange(url, HttpMethod.POST,  request, String.class);
 System.out.println(exchange.getBody());
}

为什么contentType会变成application/json

如果value值不是string,则是multipart

private boolean isMultipart(MultiValueMap<String, ?> map, @Nullable MediaType contentType) {
  if (contentType != null) {
    return contentType.getType().equalsIgnoreCase("multipart");
  }
  for (List<?> values : map.values()) {
    for (Object value : values) {
        //如果value值不是string,则是multipart
      if (value != null && !(value instanceof String)) {
        return true;
      }
    }
  }
  return false;
}
protected void addDefaultHeaders(HttpHeaders headers, T t, @Nullable MediaType contentType) throws IOException {
  if (headers.getContentType() == null) {
    MediaType contentTypeToUse = contentType;
    if (contentType == null || !contentType.isConcrete()) {
      contentTypeToUse = getDefaultContentType(t);
    }
    else if (MediaType.APPLICATION_OCTET_STREAM.equals(contentType)) {
      MediaType mediaType = getDefaultContentType(t);
      contentTypeToUse = (mediaType != null ? mediaType : contentTypeToUse);
    }
    if (contentTypeToUse != null) {
      if (contentTypeToUse.getCharset() == null) {
        Charset defaultCharset = getDefaultCharset();
        if (defaultCharset != null) {
          contentTypeToUse = new MediaType(contentTypeToUse, defaultCharset);
        }
      }
       //在此处设置了application/json类型的contentType
      headers.setContentType(contentTypeToUse);
    }
  }
  if (headers.getContentLength() < 0 && !headers.containsKey(HttpHeaders.TRANSFER_ENCODING)) {
    Long contentLength = getContentLength(t, headers.getContentType());
    if (contentLength != null) {
      headers.setContentLength(contentLength);
    }
  }
}
  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值