目的:
在Spring WebFlux框架中支持XML格式的请求与返回。主要考虑尽可能的使用配置和JAXB注解来完成,尽量避免手动XML处理逻辑。
备选方案:
-
jackson-dataformat-xml
优点:功能强大,对Jackson用户友好
缺点:不支持Reactive,经实测RestController也无法正常使用。参考:Spring Boot issue #9581 - ** glassfish jaxb-runtime**
优点:Spring Boot默认支持,仅需导入包
缺点:功能不够完全,例如不支持@XmlCDATA输出。Java 9以上需要不同的配置,参考:Spring Boot with Java 9 and above -
EclipseLink Moxy
优点:功能比较强大,Stack Overflow有很多相关配置解答
缺点:文档例子很简略,有部分遗留问题,最近更新不频繁 -
VTD-XML
优点:使用简单,高性能
缺点:全手动XML处理,不支持JAXB注解,很久未更新
最终方案:EclipseLink Moxy
关于EclipseLink Moxy的文档,建议参考EclipseLink官网文档以及Oracle网站上的使用教程
具体配置 (使用版本:Java 11 + Spring Boot 2.3.3 + EclipseLink Moxy 2.7.7):
-
pom.xml
引入Annotation包jakarta.xml.bind-api
,引入org.eclipse.persistence.moxy
包
<dependency>
<groupId>jakarta.xml.bind</groupId>
<artifactId>jakarta.xml.bind-api</artifactId>
<version>2.3.3</version>
</dependency>
<dependency>
<groupId>org.eclipse.persistence</groupId>
<artifactId>org.eclipse.persistence.moxy</artifactId>
<version>2.7.7</version>
</dependency>
-
src/main/resources/META-INF/services/javax.xml.bind.JAXBContext
WebFlux中原生支持了org.springframework.oxm.jaxb.Jaxb2Marshaller
,所以可以通过创建javax.xml.bind.JAXBContext
文件来指向Moxy,内容为
org.eclipse.persistence.jaxb.JAXBContextFactory
XML输入处理:
Handler文件中需要显式地标注MediaType,如:
@RequestMapping(produces = {MediaType.APPLICATION_XML_VALUE}, consumes = {MediaType.APPLICATION_XML_VALUE})
对于如下HTTP请求的XML输入进行解析:
<xml>
<mch_id>10000100</mch_id>
<sign>FDD167FAA73459FD921B144BAF4F4CA2</sign>
</xml>
示例代码:
@Validated
@XmlRootElement(name = "xml")
@XmlAccessorType(XmlAccessType.FIELD)
public class XmlRequest {
@XmlElement(name = "mch_id")
private String mchId = null;
private String sign = null;
...
}
代码解析:
-
@XmlRootElement
显式声明了根元素,通过name
匹配元素名。 -
@XmlAccessorType
表示JAXB引擎的绑定方式,共有四种情况:
XmlAccessType.PROPERTY
绑定所有Getter
/Setter
方法,以及显式地标注了XML注解的变量。
XmlAccessType.FIELD
绑定所有非静态,非临时变量,以及显式地标注了XML注解的Gettr
/Setter
方法。
XmlAccessType.PUBLIC_MEMBER
默认值,绑定所有public变量或者Getter
/Setter
方法,以及显式地标注了XML注解的非public的变量和Gettr
/Setter
方法。
XmlAccessType.NONE
只绑定显式地标注了XML注解的变量和Gettr
/Setter
方法。 - 这里使用
XmlAccessType.FIELD
,所以sign
被自动绑定,但是注意元素mch_id
的对应的实际变量名为mchId
,所以需要添加显式的声明@XmlElement(name = "mch_id")
来匹配驼峰命名。 - 如果存在继承关系,只需要在父类中对应添加
@XmlAccessorType
和@XmlElement
即可。 -
@Validated
支持Bean Validation,此处不展开解释。
XML输出处理:
同样,Handler文件中需要显式地标注MediaType.
示例代码:
@XmlRootElement(name = "xml")
@XmlAccessorType(XmlAccessType.FIELD)
@XmlAccessorOrder(XmlAccessOrder.ALPHABETICAL)
public class XmlResponse {
private TradeStatusEnum tradeStatus = null;
@XmlPath("response/currency/text()")
private String currency = null;
@XmlJavaTypeAdapter(DateTimeFormatDefault.class)
@XmlPath("response/gmt_create/text()")
private LocalDateTime gmtCreate = null;
...
}
代码最终得到如下的XML输出:
<xml>
<is_success>Y</is_success>
<response>
<currency>CNY</currency>
<gmt_create>2015-06-24 21:43:56</gmt_create>
</response>
</xml>
代码解析:
-
@XmlRootElement
同前。 -
@XmlAccessorType
同前。 - 这里使用
XmlAccessOrder.ALPHABETICAL
对于结果进行了字母排序。 -
@XmlElement
的使用方式同前,此处使用了@XmlPath
来定制复杂的路径。
类型转换输出:
对于复杂的类型进行绑定,可以使用@XmlJavaTypeAdapter
来自定义的解析匹配。配合上面时间类型转换的代码如下:
public class LocalDataTimeAdapter extends XmlAdapter<String, LocalDateTime> {
private final DateTimeFormatter formatter;
public LocalDataTimeAdapter(String pattern) {
formatter = DateTimeFormatter.ofPattern(pattern);
}
@Override
public LocalDateTime unmarshal(String dateTimeText) {
return LocalDateTime.parse(dateTimeText, formatter);
}
@Override
public String marshal(LocalDateTime dateTime) {
return dateTime.format(formatter);
}
public static final class DateTimeFormatDefault extends LocalDataTimeAdapter {
public DateTimeFormatDefault() {
super("yyyy-MM-dd HH:mm:ss");
}
}
public static final class DateTimeFormatShort extends LocalDataTimeAdapter {
public DateTimeFormatShort() {
super("yyyyMMddHHmmss");
}
}
}
自定义的转换类需要继承XmlAdapter<ValueType, BoundType>
抽象类,并实现marshal
和unmarshal
方法来实现对应的类型转换。此处为了支持多种时间输出格式,我们使用了多个静态内部类来对应。在@XmlJavaTypeAdapter
中使用具体格式类就可以进行多种匹配了。