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

news2024/11/16 17:46:51

背景

最近在做一个技术项目的迁移,将老的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);
    }
  }
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/184631.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

《深入浅出计算机组成原理》学习笔记 Day14

数据通路&#xff08;下&#xff09;1. PC 寄存器的实现2. 读写数据所需要的译码器3. 数据通路完整实现4. 总结参考1. PC 寄存器的实现 PC 寄存器又名程序计数器&#xff08;Program Counter&#xff09;。 PC 寄存器由两个部分组成&#xff1a; 时钟信号。提供定时的输入&a…

刚来的00后太卷了,上班还没2年,跳到我们公司起薪25k....

都说00后躺平了&#xff0c;但是有一说一&#xff0c;该卷的还是卷。 这不&#xff0c;前段时间我们公司来了个00后&#xff0c;工作都没两年&#xff0c;跳槽到我们公司起薪18K&#xff0c;都快接近我了。后来才知道人家是个卷王&#xff0c;从早干到晚就差搬张床到工位睡觉了…

Arthas 入门到实战(四)arhtas idea plugin集成插件

前言&#xff1a; Arthas 官方的工具还不够足够的简单&#xff0c;需要记住一些命令&#xff0c;但是我们需要的是一个能够简单处理字符串信息的插件即可使用。当在处理线上问题的时候需要最快速、最便捷的命令&#xff0c;因此插件还是有存在的意义和价值的。 一、idea插件安…

Blender 粒子系统

文章目录简介.添加粒子系统.属性.自发光&#xff08;发射&#xff09;.源.烘焙(仅发射体).速度(仅发射体).旋转(仅发射体&#xff0c;除非毛发系统开启了高级属性).物理(仅发射体).毛发动力学(仅毛发).渲染.路径.视图显示.子级.簇集.糙度.扭结.力场权重.顶点组.粒子编辑.渲染毛…

GraalVM和Spring Native尝鲜,一步步让Springboot启动飞起来,66ms完成启动

简介 GraalVM是高性能的JDK&#xff0c;支持Java/Python/JavaScript等语言。它可以让Java变成二进制文件来执行&#xff0c;让程序在任何地方运行更快。这或许是Java与Go的一场战争&#xff1f; 下载安装GraalVM 安装GraalVM 首先到官网下载&#xff0c;我是直接到GitHub Re…

7-2输入/输出系统-I/O方式

文章目录一.程序查询方式二.程序中断方式三.DMA方式1.传送过程2.DMA控制器&#xff08;DMAC&#xff09;的内部结构3.DMA传送方式&#xff08;1&#xff09;停止CPU访问主存&#xff08;2&#xff09;DMA与CPU交替访存&#xff08;3&#xff09;周期挪用/周期窃取4.DMA方式的特…

基于改进粒子群算法的混合储能系统容量优化(Matlab代码实现)

&#x1f4a5;&#x1f4a5;&#x1f49e;&#x1f49e;欢迎来到本博客❤️❤️&#x1f4a5;&#x1f4a5; &#x1f3c6;博主优势&#xff1a;&#x1f31e;&#x1f31e;&#x1f31e;博客内容尽量做到思维缜密&#xff0c;逻辑清晰&#xff0c;为了方便读者。 ⛳️座右铭&a…

css特性(继承、层叠、优先级)

1、继承性 特性: 子元素有默认继承父元素样式的特点(子承父业) 可以继承的常见属性(文字控制属性都可以继承) colorfont-style、 font-weight、 font-size、 font-familytext-indent、text-align、line-height … 注意点: 控制字的都能继承 可以通过调试工具判断样式是否可以…

应用系统基于CAS实现单点登录的解决方案

单点登录 (SingleSign-On&#xff0c;SSO) &#xff0c;是一种帮助用户快捷访问网络中多个站点的安全通信技术。单点登录系统基于一种安全的通信协议&#xff0c;该协议通过多个系统之间的用户身份信息的交换来实现单点登录。使用单点登录系统时&#xff0c;用户只需要登录一次…

JAVA的PDF Viewer:Big Faceless PDF Viewer Crack

PDF Viewer是一个可以显示PDF文档的Swing组件。针对不需要完整 API 的客户&#xff0c;Viewer 可以作为 Applet、应用程序或通过 Java Web Start 安装&#xff0c;或嵌入到 Swing 应用程序中。 产品概览 打印、保存、文本搜索、表单、数字签名和注释是众多可用功能中的一部分 -…

IB 课程的挑战(二)

以下我想换个角度用自身的经历以及多年来教学的经验去分享修读 IB课程的挑战。 &#xff08;一&#xff09;必修文学科2022尽管我于会考中文获得「A」级成绩&#xff0c;但面对IB课程的中国文学 (IB Chinese A Literature)时&#xff0c;却仍感到十分吃力。我之前从未接触过中国…

网络IO模型

1 介绍一提到网络IO&#xff0c;甚至一些网络框架&#xff0c;就无法避免遇到阻塞、非阻塞、同步、异步的概念&#xff0c;要理解这些概念&#xff0c;先要清楚网络IO是什么&#xff0c;以及网络IO如何工作。网络IO本质上也是IO的一种&#xff0c;就是数据的输入输出&#xff0…

护网行动(防守方)linux服务器通用安全加固指南(1)

实验所属系列&#xff1a;Linux服务器搭建/Linux服务安全 Linux 是一个开放式系统&#xff0c;可以在网络上找到许多现成的程序和工具&#xff0c;这既方便了用户&#xff0c;也方便了黑客&#xff0c;因为他们也能很容易地找到程序和工具来潜入 Linux 系统&#xff0c;或者盗…

CnOpenData中国观鸟记录数据

一、数据简介 观鸟&#xff0c;是指在自然环境中利用望远镜等观测记录设备在不影响野生鸟类正常生活的前提下观察鸟类的一种科学性质的户外活动。 鸟类&#xff0c;是生物多样性中最具指示性的类群。由全国各地观鸟爱好者们共同记录的观鸟信息&#xff0c;能够展示中国各地区的…

cesium +vue3 +vite 实现点线面功能

第一步&#xff1a;搭建vue3项目 1、npm在相应文件夹下创建 npm create vitelatest2、选择项目名称 3、选择vue 4、选择是否使用typescript 或者JavaScript 5、记得在终端npm install一下就可以启动项目了 第二步&#xff1a;引入cesiumJS 1、npm引入 npm i cesium vite…

海康Visionmaster-VM2D,VM3D,VM深度学习对电脑配置要求

为确保VM算法平台能正常安装及运行&#xff0c;对PC配置有所要求。 第一&#xff1a;推荐配置&#xff1a; 操作系统&#xff1a;Windows7/10&#xff08;64位中文操作系统&#xff09; CPU&#xff1a;Intel Core i7-6700 3.4GHz 或以上&#xff08;如需使用CPU相关深度学习…

32岁面试字节软件测试岗,想不到居然这么简单......

前段时间有个在小公司干了七八年的朋友离职了&#xff0c;想要拼一拼大厂&#xff0c;于是他选择了字节跳动。面试总共花费了 20 天左右&#xff0c;包含了 4 轮电话面试、1 轮笔试、1 轮主管视频面试、1 轮hr视频面试。 据他了解到的信息&#xff0c;其实去字节面试的人很多&…

爬虫(一)爬虫基本概念

爬虫概念网络爬虫 (网页蜘蛛, 网络机器人)爬虫就是模拟客户端发送网络请求, 接受请求对应的响应, 一种按照一定的规则,自动地抓取互联网信息的程序。理论上来说, 只要是用户通过客户端(浏览器)能够做到事情, 爬虫都能够去做。爬虫: 模拟客户端访问, 抓取数据。反爬: 保护重要数…

3、边界值分析

如何选定边界值 很难提供一份如何进行边界值分析的“详细说明”&#xff0c;因为这种方法需要一定程度的创造性&#xff0c;以及对问题采取一定程度的特殊处理办法。 但是可以提供一些通用指南&#xff1a; 如果输入条件规定了一个输入值范围&#xff0c;那么应针对范围的边…

WY49 数对

【答案解析】&#xff1a; 暴力破解&#xff1a;将 x 和 y 分别遍历 [1, n] &#xff0c;进行判断当 x % y > k 时统计计数 count 即可,但是这样的话当 n 的值非常大 的时候循环次数将非常恐怖&#xff0c;需要循环 n^2 次。 更优解法&#xff1a; 假设输入 n10 , k3 &#…