自定义HttpMessageConverter处理多个不同数组形式的JSON数据

本文介绍了在Spring MVC中如何处理两种不同数组形式的JSON数据,特别是当需要对Score和Student实体进行批量插入时。通过分析和实验,对比了实现HttpMessageConverter接口和继承MappingJackson2HttpMessageConverter类的自定义转换器。结论是,对于包含泛型类型的转换,应该使用继承MappingJackson2HttpMessageConverter的方式,因为它能提供更详细的类型信息。同时,文章指出了仅通过Class对象获取泛型信息的限制。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

需求

在一个成绩管理系统中,有实体类Score和实体类Student,现需要对这两个实体类关联的数据库表分别进行批量插入,因而需要处理两种不同的JSON数据(均为数组形式),并转换为相应的List。在两种实体类http请求中,Student类中的成员变量与对应json数据格式的一致,而Score类中的成员变量则与相应http请求中的json数据格式则不一致,因此需要在转型的过程中进行自定义的操作。

分析

对于Student类,可以直接用springmvc提供的MappingJackson2HttpMessageConverter转换器进行类与json字符串之间的转换,对于Score类,则需要自定义转换器。自定义转换器我能想到的有两种方式:
一丶实现HttpMessageConverter接口
二丶继承MappingJackson2HttpMessageConverter类并对其功能进行扩展
在现在这种情况下,方式一实际上是行不通的,因为springmvc在选择转换器时,是通过转换器类中的canRead方法来进行判断的,放上两种方式中实际调用的canRead方法的签名:
实现HttpMessageConverter接口时:
boolean canRead(Class< ? > aClass, MediaType mediaType)
继承MappingJackson2HttpMessageConverter类时:
boolean canRead(Type type, Class< ? > aClass, MediaType mediaType)
可以看到两个方法的参数并不一样,对于第一种方式,转型的目的java对象类型信息(此处为List< Score >)存放于Class< ? > aClass中,该变量只能记录List擦除后的信息,无法记录List中持有对象的类型(只知道是List而不知道是持有什么类型的List),因而无法为springmvc对转换器的选择提供准确的判断信息;而对于第二种方式,Type type中存放了所有相关的详细信息(包括List的泛型信息),因而可行。

实验

接下来向

发送请求体为json格式的数据请求,springmvc会选择相应的转换器进行处理,看看两种方式下springmvc对转换器的处理情况:

第一种方式,注册实现了HttpMessageConverter接口的转换器:

当springmvc调用该转换器的canRead方法时可以看到如下结果:
这里写图片描述

第二种方式,注册继承了MappingJackson2HttpMessageConverter类的转换器:

当springmvc调用该转换器的canRead方法时可以看到如下结果:

那么为什么springmvc为两种方式下转换器的canRead方法传递的参数不同呢?springmvc主要在下面的方法中对转换器进行判断并调用:

注册继承了MappingJackson2HttpMessageConverter类的转换器,对该方法进行断点调试:
这里写图片描述
注册实现了HttpMessageConverter接口的转换器,对该方法进行断点调试:

结论

springmvc根据表达式converter instanceof GenericHttpMessageConverter进行判断,并为不同的转换器converter提供不同的参数。实现了HttpMessageConverter接口的转换器不是GenericHttpMessageConverter的导出类型,因此其canRead方法无法获取具体的泛型类型信息,不适用于包含泛型类型的转型;MappingJackson2HttpMessageConverter或其子类是GenericHttpMessageConverter的导出类型,因而适用于包含泛型类型的转型。判断结束后springmvc会调用转换器中的Read方法,将请求体中的json字符串转换为相应的List集合并返回。

ps:开始我用的是实现了HttpMessageConverter接口的转换器,百度上很多人说仅通过class对象就能获取相应的泛型类型信息,就像下面这样:

        List<Long> longs = new ArrayList<Long>();
        //目标class对象
        Class c = longs.getClass();
        //若c对应的对象继承了泛型类
        ParameterizedType type = (ParameterizedType) c.getGenericSuperclass();
        //若c对应的对象实现了(多个)泛型接口
        //ParameterizedType[] type = (ParameterizedType[]) c.getGenericInterfaces();
        //输出其泛型参数类型
        System.out.println(type.getActualTypeArguments()[0]);

但其输出结果为:E
E表示的是ArrayList直接父类的泛型类型(此处仅为一占位符),因此我认为通过这种方式,只能获取到目标class对象的直接父类(或者其所实现接口)的泛型类型信息,无法获取class对象本身的泛型类型信息。
以上均为本人通过调试或者查资料所得出的结论,若大家发现有不对的地方,希望能够及时指出,对此我表示万分感谢!

<think>我们注意到用户坚持使用@RequestBody来接收JSON,但之前的分析已经指出在multipart/form-data请求中同时使用@RequestParam和@RequestBody会导致冲突。然而,用户明确要求使用@RequestBody,因此我们需要调整方法参数结构。 实际上,在multipart/form-data请求中,不能直接使用@RequestBody,因为@RequestBody用于读取整个请求体并绑定到一个对象,而multipart请求体是分部分的(包括文件和文本部分)。因此,正确的做法是使用@RequestPart或@RequestParam来接收multipart请求中的各个部分。 但是,用户坚持使用@RequestBody,这通常只适用于非multipart请求(如application/json)。因此,我们需要改变请求方式:将文件也作为请求体的一部分,但这样就不能使用multipart/form-data了。这通常不是一个标准的做法,因为文件上传通常使用multipart。 鉴于用户的需求,我们有两种选择: 1. 放弃multipart/form-data,将文件以Base64编码的形式放在JSON中,然后整个请求体作为一个JSON对象,使用@RequestBody接收。 2. 使用multipart/form-data,但按照标准方式使用@RequestPart或@RequestParam接收各个部分。 由于用户要求使用@RequestBody,我们只能采用第一种方式。 因此,我们调整接口设计: 请求方式:POST,Content-Type: application/json 请求体结构: { "fileBase64": "base64编码的PDF文件字符串", "jsonContent": { ... } // 这里可以是原来的JSON数组,但注意整个请求体是一个JSON对象 } 但是,这种方式需要将文件转换成Base64字符串,可能会增加传输大小(约增加33%),并且需要后端解码。 代码调整如下: ```java @PostMapping("/compares") public R compares(@RequestBody CompareRequest request) { // 1. 从请求体中获取Base64字符串并解码为文件 byte[] fileBytes = Base64.getDecoder().decode(request.getFileBase64()); // 如果需要,可以将字节数组转换为MultipartFile的代理或者直接使用字节数组 // 这里假设extractContent方法可以接受字节数组,或者我们创建一个临时文件 // 例如: // String pdfText = compareService.extractContent(fileBytes); // 2. 解析jsonContent(此时已经是Java对象,无需再解析字符串,但注意请求体中的jsonContent可能是Map或直接是对象) // 如果请求体中的jsonContent是字符串,那么我们需要再次解析(不推荐,应该直接传对象) // 更好的做法是:请求体中的jsonContent直接对应为JsonNode或自定义对象 // 3. 后续处理... } @Data static class CompareRequest { private String fileBase64; // Base64编码的字符串 private JsonNode jsonContent; // 或者使用自定义,但需要确保Jackson可以解析 } ``` 但是,这种方法需要前端将文件转换为Base64,并且整个请求体变为一个大的JSON对象。 然而,根据用户之前的描述,他们是在Apifox中使用form-data测试的,这意味着他们可能更倾向于使用multipart。因此,我们强烈建议用户使用标准multipart方式,即使用@RequestParam或@RequestPart。 如果用户坚持在form-data中使用@RequestBody,这是不可能的,因为@RequestBody只能用于非multipart请求。所以,我们只能改变请求的Content-Type为application/json,并改变前端发送数据的方式。 考虑到用户可能无法改变前端(比如正在测试),我们提供以下折中方案: 使用@RequestPart(Spring提供的专门用于multipart请求中非文件部分的注解)来接收JSON部分,而文件部分用@RequestPart MultipartFile。注意,@RequestPart可以自动将JSON字符串转换为对象(如果配置了正确的转换)。 代码示例: ```java @PostMapping("/compares") public R compares( @RequestPart("File") MultipartFile file, @RequestPart("jsonContent") JsonNode jsonConfig) { // 注意:这里jsonContent直接解析为JsonNode // 读取PDF文本 String pdfText = compareService.extractContent(file); // 此时jsonConfig已经是解析好的JsonNode,无需再手动解析 // 执行对比校验 List<ValidationResult> results = compareService.compareContent(pdfText, jsonConfig); // ... 后续处理 } ``` 这样,Spring会使用配置的HttpMessageConverter(如MappingJackson2HttpMessageConverter)将multipart请求中名为"jsonContent"的部分转换为JsonNode对象。 在Apifox中,设置: - File参数:类为File,选择文件 - jsonContent参数:类为Text,输入JSON字符串(注意不是JSON对象,而是字符串),或者Apifox可能支持直接选择JSON文件,但这里需要输入字符串。 但是,注意:如果使用@RequestPart,并且希望Spring自动将JSON字符串转换为对象,需要确保请求的Content-Type为multipart/form-data,并且jsonContent部分的Content-Type设置为application/json(在Apifox中,可以在该参数后面选择Content-Type)。 在Apifox中设置jsonContent参数时,需要: - 在key为jsonContent的那一行,选择类为Text,然后在右侧设置Content-Type为application/json(如果Apifox支持的话) - 然后在value中粘贴JSON字符串 如果无法设置部分内容的Content-Type,可以尝试以下方式: - 使用@RequestPart("jsonContent") String jsonContent,然后手动解析(这样就不需要设置Content-Type为application/json了) - 或者,使用@RequestPart("jsonContent") YourOwnClass obj,但需要定义对应的Java类。 鉴于用户已经有JsonNode,我们可以使用第一种自动转换,但需要确保Spring知道如何转换。默认情况下,Spring Boot已经配置了Jackson,所以只要请求中该部分的Content-Type是application/json,就可以转换。 但是,如果Apifox没有为jsonContent部分设置Content-Type,可能会默认为text/plain,这样Spring就会将其作为普通字符串处理,然后尝试转换为JsonNode会失败(因为需要的是JSON格式的字符串,但转换期望Content-Type为application/json)。因此,最好在Apifox中显式设置jsonContent的Content-Type为application/json。 如果用户无法在Apifox中设置,那么我们就只能手动解析: ```java @PostMapping("/compares") public R compares( @RequestPart("File") MultipartFile file, @RequestPart("jsonContent") String jsonContent) { // 作为字符串接收 // 手动解析JSON ObjectMapper objectMapper = new ObjectMapper(); JsonNode jsonConfig = objectMapper.readTree(jsonContent); // 后续处理... } ``` 这样就不需要依赖Spring的自动转换,但需要手动处理解析异常。 综上所述,我们提供两种解决方案(根据用户实际环境选择): 方案A(推荐,使用自动转换): - 修改方法参数为@RequestPart - 确保前端在发送jsonContent部分时设置Content-Type为application/json 方案B(更通用,手动解析): - 使用@RequestPart或@RequestParam接收jsonContent字符串,然后手动解析 由于用户之前报错400,我们建议先采用方案B,因为其适应性更强。 代码调整为: ```java @PostMapping("/compares") public R compares( @RequestParam("File") MultipartFile file, @RequestParam("jsonContent") String jsonContent) { // 使用@RequestParam try { JsonNode jsonConfig = compareService.parseJson(jsonContent); // 后续处理... } catch (Exception e) { // 处理异常 } } ``` 同时,在Apifox中,仍然使用form-data,两个参数:File(文件)和jsonContent(文本,粘贴JSON字符串)。 这样修改后,应该可以避免400错误。 如果仍然出现400错误,请检查: - 参数名是否完全匹配(注意大小写) - 文件是否过大(检查Spring Boot上传文件大小配置) - JSON字符串是否合法(可以在线验证) 因此,我们最终给出的代码调整是:放弃@RequestBody,使用两个@RequestParam(或一个@RequestParam和一个@RequestPart,但这里用@RequestParam更简单)。 如果用户坚持使用@RequestBody,那么只能改变整个请求的Content-Type为application/json,并采用Base64编码文件,但这会改变整个请求结构,且不适用于用户当前的Apifox form-data测试方式。 所以,我们最终建议:使用两个@RequestParam。 修改后的代码: ```java @PostMapping("/compares") public R compares( @RequestParam("File") MultipartFile file, @RequestParam("jsonContent") String jsonContent) { // 读取PDF文本 String pdfText = compareService.extractContent(file); // 解析JSON配置 JsonNode jsonConfig = null; try { jsonConfig = compareService.parseJson(jsonContent); } catch (Exception e) { return R.fail(MessageUtils.message("failed.convert.json")); } // 执行对比校验 List<ValidationResult> results = compareService.compareContent(pdfText, jsonConfig); // 返回失败结果 List<ValidationResult> failedResults = results.stream() .filter(result -> !result.isValid()) .collect(Collectors.toList()); if (failedResults.isEmpty()) { return R.ok("条件符合规范"); } else { return R.ok(failedResults); } } ``` 配置上传限制(在application.yml中): ```yaml spring: servlet: multipart: max-file-size: 10MB max-request-size: 20MB ``` 请用户尝试此方案。 §§相关问题§§ 1. 在Spring Boot中,@RequestParam和@RequestPart在处理multipart请求时有何区别? 2. 如何配置Spring Boot以支持更大的文件上传? 3. 当使用Base64编码上传文件时,如何优化后端解码和内存使用? 4. Apifox中如何为multipart/form-data请求的某个部分设置特定的Content-Type(如application/json)? 5. 如何通过拦截或过滤记录multipart请求的详细信息以方便调试?</think>### 问题分析与解决方案 您遇到 400 Bad Request 错误的核心原因是:**在 `multipart/form-data` 请求中错误使用了 `@RequestBody`**。`@RequestBody` 只能用于读取整个请求体(如 JSON/XML 格式),而 `multipart/form-data` 请求是多部分组成的,无法直接用 `@RequestBody` 接收。 ### 正确解决方案 #### 方案一:使用 `@RequestParam` 接收 JSON 字符串(推荐) ```java @PostMapping("/compares") public R compares( @RequestParam("File") MultipartFile file, @RequestParam("jsonContent") String jsonContent) { // 关键修改 // 1. 添加参数验证 if (file.isEmpty()) { return R.fail("文件不能为空"); } if (jsonContent == null || jsonContent.isBlank()) { return R.fail("JSON内容不能为空"); } // 2. 读取PDF文本 String pdfText = compareService.extractContent(file); // 3. 解析JSON配置 JsonNode jsonConfig; try { jsonConfig = compareService.parseJson(jsonContent); } catch (Exception e) { log.error("JSON解析失败: {}", jsonContent.substring(0, 50), e); return R.fail("JSON格式错误: " + e.getMessage()); } // 4. 后续处理保持不变... } ``` #### 方案二:使用 `@RequestPart` 接收 JSON 对象(需 Apifox 配合) ```java @PostMapping("/compares") public R compares( @RequestParam("File") MultipartFile file, @RequestPart("jsonContent") JsonNode jsonConfig) { // 直接接收对象 // 无需手动解析JSON // 其他代码保持不变... } ``` ### Apifox 正确配置 1. **Body 类**:`form-data` 2. **参数设置**: | Key | Type | Value | |--------------|--------|--------------------| | File | File | 选择PDF文件 | | jsonContent | Text | 粘贴完整JSON内容 | 3. **重要细节**: - 确保参数名 **完全匹配**(区分大小写) - JSON 内容必须放在 **Text 类**参数中 - 不要设置额外的 `Content-Type` 请求头 ### 错误排查步骤 1. **添加日志调试**: ```java @PostMapping("/compares") public R compares(...) { // 添加请求日志 log.info("收到文件: {} 大小: {}", file.getOriginalFilename(), file.getSize()); log.info("收到JSON长度: {}", jsonContent.length()); log.info("JSON前50字符: {}", jsonContent.substring(0, Math.min(50, jsonContent.length()))); // ...其他代码 } ``` 2. **验证 JSON 格式**: ```java // 在解析前添加验证 try { new ObjectMapper().readTree(jsonContent); } catch (Exception e) { return R.fail("无效JSON: " + e.getMessage()); } ``` 3. **配置文件上传限制**: ```yaml # application.yml spring: servlet: multipart: max-file-size: 10MB max-request-size: 20MB enabled: true ``` ### 常见错误原因 1. **JSON 格式问题**: - 检查 `SHIPTOZIP` 和 `SOLDTOZIP` 应为字符串(您的 JSON 中是数字) - 修正建议:`"SHIPTOZIP": "201800"` 2. **特殊字符问题**: - 确保 JSON 中的中文和特殊字符正确转义 - 在 Apifox 中使用 **Raw JSON** 模式编辑 3. **参数名大小写敏感**: - 检查 `@RequestParam("File")` 中的 "File" 必须与 Apifox 中的 Key 完全一致
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值