文章目录
- 介绍
- 一、解析简单 xml 数据案例
- 引入 Jackson 的 xml 支持
- 定义 Message 对象&MessageHeader 对象
- 定义 Controller 方法
- 调用结果
- 二、解析带泛型的 XML 数据案例
- 2.1 直接给 Message 加上泛型 T
- 2.2 无法直接解析泛型参数了
- 三、自定义 MVC 的参数解析器实现泛型参数解析
- MVC 的消息解析器和方法参数解析器介绍
- 让 MVC 解析泛型参数的方案
- 具体方案
- 定义一个类似@RequestBody的注解,来关联我们自定义的HandlerMethodArgumentResolver
- 自定义HandlerMethodArgumentResolver 来实现将我们自定义注解中的泛型类告诉 JackSon-xml
- 注入我们自己写的方法参数解析器
- 测试效果
- 将@RequestBody 改成我们自定义的注解@XmlGenerics
- 结果成功解析
- 四、你可能存在的一些疑问
- 为什么响应的时候不需要加泛型 Jackson 都可以正确的进行序列化
介绍
在 SpringBoot 应用中,自带的 json 序列化框架是 fastxml-jackson。引入 jackson 的jackson-dataformat-xml
包后,配合Bean上的jackson XML注解就可以自动的将 XML 请求参数进行反序列化,将返回对象进行序列化。
但是对于包含泛型的对象参数,由于泛型的擦除机制程序运行时无法得知泛型内容自然也就无法正确的将 XML 请求参数反序列化成指定的 Java 对象了。这时候想要正确解析,就得手动告诉 jackson 我想反序列化的对象是什么类型。本篇博客通过手写一个自定义的HandlerMethodArgument 来实现泛型对象的请求参数解析。
程序源代码下载 demoxml.rar
一、解析简单 xml 数据案例
目标,使用 Controller 将传入的 Message XML 格式数据解析成 Java 对象。
请求的 XML 格式
<Request>
<MessageHeader>
<Sender>EMSS</Sender>
<Receiver>ESB</Receiver>
<SendTime>20240401145010</SendTime>
<EventType>EMSS_SDZX</EventType>
<MsgId>8952f937-2f36-41b4-af71-2742fde43f09</MsgId>
</MessageHeader>
</Request>
引入 Jackson 的 xml 支持
<dependency>
<groupId>com.fasterxml.jackson.dataformat</groupId>
<artifactId>jackson-dataformat-xml</artifactId>
</dependency>
定义 Message 对象&MessageHeader 对象
使用 JacksonXMLxxxx 的注解对对象进行标记。
@Data
@AllArgsConstructor
@NoArgsConstructor
@JacksonXmlRootElement(localName="Request")
public class Message {
@JacksonXmlProperty(localName = "MessageHeader")
private MessageHeader messageHeader;
}
@Data
public class MessageHeader {
/**
*必填 发送应用程序 例如HIS
*/
@JacksonXmlProperty(localName = "Sender")
private String sender;
/**
*必填 接收应用程序
*/
@JacksonXmlProperty(localName = "Receiver")
private String receiver;
/**
*必填 消息创建时间 格式:YYYY-MM-DD HH:MM:SS
*/
@JacksonXmlProperty(localName = "SendTime")
private String sendTime;
/**
* 必填 事件类型 例如 ACK_PCA_SEARCH_ARCH
*/
@JacksonXmlProperty(localName = "EventType")
private String eventType;
/**
*必填 消息ID 建议用UUID生成
*/
@JacksonXmlProperty(localName = "MsgId")
private String msgId;
}
定义 Controller 方法
@Data
@AllArgsConstructor
@NoArgsConstructor
@JacksonXmlRootElement(localName="Request")
public class Message<T extends MessageBody> {
@JacksonXmlProperty(localName = "MessageHeader")
private MessageHeader messageHeader;
@JacksonXmlProperty(localName = "MessageBody")
private T messageBody;
}
调用结果
可以看到 XML 参数可以被正确序列化和反序列化
二、解析带泛型的 XML 数据案例
2.1 直接给 Message 加上泛型 T
@Data
@AllArgsConstructor
@NoArgsConstructor
@JacksonXmlRootElement(localName="Request")
public class Message<T extends MessageBody> {
@JacksonXmlProperty(localName = "MessageHeader")
private MessageHeader messageHeader;
@JacksonXmlProperty(localName = "MessageBody")
private T messageBody;
}
@Data
public class MessageBody implements Serializable {
}
定义 MessageBody 的一个实现类 AnkeUpdatePatientLocaBody
/**
* @author Jean
* @date 2024/04/16
*/
@Data
public class AnkeUpdatePatientLocaBody extends MessageBody {
/**
*ankeid
*/
@JacksonXmlProperty(localName = "AnkeId")
private String ankeId;
/**
*经度,不能为空
*/
@JacksonXmlProperty(localName = "Longitude")
private String longitude;
/**
*纬度,不能为空
*/
@JacksonXmlProperty(localName = "Latitude")
private String latitude;
}
2.2 无法直接解析泛型参数了
可以看到 Controller 方法的 Message 泛型参数没有被正确的解析出来。为什么呢,因为泛型参数只存在于编译期,运行时程序并没有保存泛型数据。所有的泛型参数都会被擦除被替换为其上限类型(没有则替换为 Object)类型。不过虽然运行时没有泛型参数,但是类和方法上的泛型声明仍然可以通过反射获取到。
三、自定义 MVC 的参数解析器实现泛型参数解析
MVC 的消息解析器和方法参数解析器介绍
- 消息转换器(HttpMessageConverter):
- 当请求到达并且需要读取请求体内容时,如使用
@RequestBody
注解的参数,Spring MVC会根据consumes
指定的媒体类型,选择合适的HttpMessageConverter
来将请求体中的数据(如JSON、XML)转换为Java对象。 - 在响应阶段,如果Controller方法返回一个对象,并且需要转换为HTTP响应体(比如使用
@ResponseBody
注解),Spring会使用相应的HttpMessageConverter
将Java对象转换为指定格式(如JSON、XML)的数据写入响应体。
- 当请求到达并且需要读取请求体内容时,如使用
- 消息解析器(HandlerMethodArgumentResolver):
- 在调用Controller方法之前,Spring MVC会根据方法签名中的参数类型和注解,选择合适的
HandlerMethodArgumentResolver
来解析和填充参数。例如,对于@PathVariable
、@RequestParam
、@RequestHeader
等注解的参数,Spring会使用对应的解析器来获取请求中的参数值。 - 对于
@RequestBody
注解的参数,虽然实际的数据转换是由HttpMessageConverter
完成,但触发这个转换的决策过程是在RequestBodyArgumentResolver
(它是HandlerMethodArgumentResolver
的一种)中进行的,它负责判断参数是否标记了@RequestBody
,然后调用对应的HttpMessageConverter
来处理请求体数据。
让 MVC 解析泛型参数的方案
- jackson 无法解析泛型 XML 参数的原因是因为泛型被擦除,jackson 也没有针对泛型做其他处理方案
- 我们写代码的时候是知道具体的类型的,我们通过某种防范将泛型类告诉 jackson 就行了
- jackson 负责实现 XMl 字符串解析成 Java 对象的过程,那我们自定义一个**HandlerMethodArgumentResolver **来实现泛型参数解析不就行了吗
具体方案
定义一个类似@RequestBody的注解,来关联我们自定义的HandlerMethodArgumentResolver
@Target({ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
public @interface XmlGenerics {
//用来保存泛型信息
Class<? extends MessageBody> value();
}
自定义HandlerMethodArgumentResolver 来实现将我们自定义注解中的泛型类告诉 JackSon-xml
这里为了方便直接写了 Message.class,如果需要通用只需要改成根据 controller 方法来获取参数类型即可
/**
*
* 自定义一个请求参数消息解析器,解析Message类型的Xml请求数据不走@RequestBody的解析器
* 1.请求方法参数中使用了泛型后 jackson-xml 无法正确的反序列化,因为泛型抹除后无法获知具体的泛型类型。
* 2.此处使用自定义注解保存泛型信息来进行反序列化参数。
* <p>
* {@link RequestResponseBodyMethodProcessor} @RequestBody方法解析器
* {@link org.springframework.boot.autoconfigure.http.JacksonHttpMessageConvertersConfiguration} jackson-xml的消息转换器的自动配置
*
* @author
* @date 2024/04/16 17:22
*/
public class XmlMessageResolver implements HandlerMethodArgumentResolver {
//用来解析xml的Mapper
private final XmlMapper xmlMapper = new XmlMapper();
public XmlMessageResolver() {
//配置xmlMapper遇到未知属性时忽略,而不是报错
xmlMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
}
/**
* supports参数
*
* @param parameter 参数
* @return boolean
*/
@Override
public boolean supportsParameter(MethodParameter parameter) {
boolean support = parameter.hasParameterAnnotation(XmlGenerics.class) && parameter.getParameterType().equals(Message.class);
return support;
}
@Override
public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer,
NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {
//读取请求体
HttpServletRequest servletRequest = webRequest.getNativeRequest(HttpServletRequest.class);
String requestBody = StreamUtils.copyToString(servletRequest.getInputStream(), StandardCharsets.UTF_8);
//获取注解参数中标识的泛型类型
XmlGenerics parameterAnnotation = parameter.getParameterAnnotation(XmlGenerics.class);
Class<? extends MessageBody> genericClass = parameterAnnotation.value();
//构造类型参考对象
TypeReference<Message<?>> typeRef = new TypeReference<Message<?>>() {
@Override
public Type getType() {
return new ParameterizedType() {
//指定泛型实际参数,来自注解中的class标识
@Override
public Type[] getActualTypeArguments() {
return new Type[]{genericClass};
}
//获取参数类型
@Override
public Type getRawType() {
return Message.class;
}
//标识泛型参数所属的内部类类型,这里不包含内部类直接返回NULL
@Override
public Type getOwnerType() {
return null;
}
};
}
};
//使用参考类型来反序列化XML请求数据
Message<?> message = xmlMapper.readValue(requestBody, typeRef);
return message;
}
}
注入我们自己写的方法参数解析器
@Configuration
public class WebConfig implements WebMvcConfigurer {
// ... 上面的HttpMessageConverter配置...
@Override
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> argumentResolvers) {
argumentResolvers.add(0, new XmlMessageResolver());
System.out.println("注入完成");
}
}
测试效果
将@RequestBody 改成我们自定义的注解@XmlGenerics
@RestController
public class AnkeDataReceiverController {
/**
* 接收xml消息
*
* @return {@link Message}
*/
@PostMapping(value = "/test", consumes = MediaType.APPLICATION_XML_VALUE, produces = MediaType.APPLICATION_XML_VALUE)
public Message pushPatientDataToJjld(@XmlGenerics(AnkeUpdatePatientLocaBody.class) Message<AnkeUpdatePatientLocaBody> message) {
AnkeUpdatePatientLocaBody messageBody = message.getMessageBody();
System.out.println(messageBody);
return message;
}
}
结果成功解析
四、你可能存在的一些疑问
为什么响应的时候不需要加泛型 Jackson 都可以正确的进行序列化
因为响应的时候,已经存在对象了,可以直接获取到对象及其参数的真实类型。所以反序列化不会存在这种问题