SpringMVC使用Jackson返回XML格式
创始人
2025-05-31 16:09:40
0

1 背景及需求

项目中使用到了jaxb2技术,即通过运行maven-jaxb2-plugin插件,来根据XSD文件生成一堆Java类。

后端使用SpringMVC技术,需要在返回给前端的时候,返回XML数据,而不是JSON数据。

另外还有一些小的需求:

  • 在返回的XML中按照要求返回特定的属性

  • 在XML中限定日期和日期时间的格式

2 知识补充

2.1 JAXB2

英文全称是Java Architecture XML Binding,可以实现将Java对象转换为XML,反之亦然。

我们把对象关系映射称之为ORM(Object Relationship Mapping),而把对象与XML之间的映射称之为OXM(Object XML Mapping)。

在JDK6中,SUN把JAXB 2.0(JSR 222)放到的Java SE中,所以我们可以在rt.jar中找到它。

2.2 jackson-module-jaxb-annotations

这个是Jackson的扩展的Module,用户使用JAXB框架生成的Java类(包含很多JAXB注解),而这个Module提供了对JAXB(javax.xml.bind)注解的支持。

2.2.1 Maven依赖

com.fasterxml.jackson.modulejackson-module-jaxb-annotations2.9.8

2.2.2 用法

有两种方式可以启用这个module,来实现对JAXB注解的支持:

  1. 注册JaxbAnnotationModule

  1. 直接添加JaxbAnnotationIntrospector

// 方式1:Module注册的标准方式
JaxbAnnotationModule module = new JaxbAnnotationModule();
// 在ObjectMapper上注册Module
objectMapper.registerModule(module);// 方式2:
AnnotationIntrospector introspector = new JaxbAnnotationIntrospector();
// 如果只需要使用JAXB注解,不需要Jackson的注解
mapper.setAnnotationIntrospector(introspector);// 既需要JAXB注解也需要Jackson注解的支持
// 注意:默认情况下,JAXB注解是primary,Jackson注解是secondary。
AnnotationIntrospector secondary = new JacksonAnnotationIntrospector();
mapper.setAnnotationIntrospector(new AnnotationIntrospector.Pair(introspector, secondary);

3 调查

使用Jackson来实现XML的序列化和反序列化:MappingJackson2XmlHttpMessageConverter。

使用Jackson的@JsonIgnoreProperties去忽略掉不需要返回的属性。

Jackson有个module:可以检测并支持JAXB2的注解。注意:有部分注解是不支持的。

4 解决

4.1 引入Jackson相关的依赖

jackson-dataformat-xml:使得Jackson支持对XML的序列化和反序列化,主要使用的类是XmlMapper。

jackson-module-jaxb-annotations:这是Jackson的一个module,可以识别Jaxb的注解。

 com.fasterxml.jackson.dataformat  jackson-dataformat-xml 
  
 com.fasterxml.jackson.module  jackson-module-jaxb-annotations 

4.2 添加MappingJackson2XmlHttpMessageConverter

添加此HttpMessageConverter可以使得接口能够使用XmlMapper进行XML的序列化或反序列化。

此外,这里在创建XmlMapper的时候,添加了注解检测器:首先检测Jackson注解,其次检测Jaxb注解。

@Configuration
@EnableWebMvc
public class WebMvcConfigurer extends WebMvcConfigurerAdapter {@Overridepublic void configureMessageConverters(List> converters) {Jackson2ObjectMapperBuilder builder =new Jackson2ObjectMapperBuilder().createXmlMapper(true).annotationIntrospector(new AnnotationIntrospectorPair(new JacksonAnnotationIntrospector(), new JaxbAnnotationIntrospector()));converters.add(new MappingJackson2XmlHttpMessageConverter(builder.build()));}
}

MappingJackson2XmlHttpMessageConverter默认注册的Media Type如下:

public MappingJackson2XmlHttpMessageConverter(ObjectMapper objectMapper) {super(objectMapper, new MediaType("application", "xml"), new MediaType("text", "xml"), new MediaType("application", "*+xml"));Assert.isInstanceOf(XmlMapper.class, objectMapper, "XmlMapper required");}

4.3 原有的包含JAXB注解的类

User类,包含ID,USER_NAME,BirthDate,CreateDate。

package com.example.demo;import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlAttribute;
import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlType;
import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter;
import javax.xml.datatype.XMLGregorianCalendar;import com.example.demo.adapter.XmlDateAdapter;
import com.example.demo.adapter.XmlDateTimeAdapter;@XmlAccessorType(XmlAccessType.FIELD)
@XmlType(name = "User", propOrder = { "username", "id" })
public class User {@XmlAttribute(name = "ID", required = true)private Long id;@XmlAttribute(name = "USER_NAME")private String username;@XmlElement(name = "BirthDate")private XMLGregorianCalendar birthDate;@XmlElement(name = "CreateDate")private XMLGregorianCalendar createDate;@XmlElement(name = "Clazz")private Clazz clazz;// setter/getter...
}

Clazz类:包含Class_ID和name。

package com.example.demo;import javax.xml.bind.annotation.XmlAttribute;
import javax.xml.bind.annotation.XmlType;@XmlType(name = "Clazz")
public class Clazz {@XmlAttribute(name = "Class_ID")private Long id;@XmlAttribute(name = "name")private String name;// setter/getter...
}

4.4 编写Controller并查看序列化后返回的XML

package com.example.demo.controller;import java.util.Date;import org.apache.commons.lang3.exception.ExceptionUtils;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;import com.example.demo.Clazz;
import com.example.demo.User;
import com.example.demo.util.XmlDateUtils;
import com.fasterxml.jackson.databind.JsonMappingException;@RestController
public class XmlController {@GetMapping(value = "/xml")public ResponseEntity xmlTest() {User user = new User();user.setId(1L);user.setUsername("test user");user.setBirthDate(XmlDateUtils.convertToXMLGregorianCalendar(new Date()));user.setCreateDate(XmlDateUtils.convertToXMLGregorianCalendar(new Date()));Clazz clazz = new Clazz();clazz.setId(1L);clazz.setName("class 1");user.setClazz(clazz);return ResponseEntity.ok(user);}
}

调用Controller得到的结果如下:

可以看到,属性名称都是按照JAXB注解中name属性指定的名称,也就是说JAXB注解是生效的。

4.5 不要返回特定的属性:User和Clazz的ID属性

我们需要使用@JsonIgnoreProperties注解来去掉不需要返回的属性:

// 去除掉User类的ID属性
@JsonIgnoreProperties(value = { "ID"})
@XmlAccessorType(XmlAccessType.FIELD)
@XmlType(name = "User", propOrder = { "username", "id" })
public class User {// ...// 去除Clazz类的Class_ID属性@JsonIgnoreProperties(value = { "Class_ID" })@XmlElement(name = "Clazz")private Clazz clazz;// ...
}

再调用接口,结果如下:

4.6 处理日期

项目需要把日期格式改成:yyyy-MM-dd,日期时间格式为yyyy-MM-dd HH:mm:ss。

解决思路:

1、编写类实现XmlAdapter

2、在需要的属性上添加@XmlJavaTypeAdapter(value = 上面的实现类.class)

日期格式实现代码如下:实现XMLGregorianCalendar与String相互转换

package com.example.demo.adapter;import javax.xml.bind.annotation.adapters.XmlAdapter;
import javax.xml.datatype.XMLGregorianCalendar;import com.example.demo.util.XmlDateUtils;/*** XMLGregorianCalendar marshal to String (Only date part, no time part) or String unmarshal to XMLGregorianCalendar.*/
public class XmlDateAdapter extends XmlAdapter {@Overridepublic XMLGregorianCalendar unmarshal(String v) throws Exception {return XmlDateUtils.convertToXMLGregorianCalendar(v, XmlDateUtils.XML_DATE_FORMAT);}@Overridepublic String marshal(XMLGregorianCalendar v) throws Exception {return XmlDateUtils.convertToString(v, XmlDateUtils.XML_DATE_FORMAT);}}

日期时间格式实现如下:

package com.example.demo.adapter;import javax.xml.bind.annotation.adapters.XmlAdapter;
import javax.xml.datatype.XMLGregorianCalendar;import com.example.demo.util.XmlDateUtils;/*** XMLGregorianCalendar marshal to String (date + time) or String unmarshal to XMLGregorianCalendar.*/
public class XmlDateTimeAdapter extends XmlAdapter {@Overridepublic XMLGregorianCalendar unmarshal(String v) throws Exception {return XmlDateUtils.convertToXMLGregorianCalendar(v, XmlDateUtils.XML_DATETIME_FORMAT);}@Overridepublic String marshal(XMLGregorianCalendar v) throws Exception {return XmlDateUtils.convertToString(v, XmlDateUtils.XML_DATETIME_FORMAT);}}

Use类中添加@XmlJavaTypeAdapter注解:

@XmlElement(name = "BirthDate")
@XmlJavaTypeAdapter(value = XmlDateAdapter.class) // 日期
private XMLGregorianCalendar birthDate;@XmlElement(name = "CreateDate")
@XmlJavaTypeAdapter(value = XmlDateTimeAdapter.class) // 日期时间
private XMLGregorianCalendar createDate;

让我们再调用下接口:

4.7 异常处理

假定我们Request也使用Xml进行传输,如果接口请求方传入错误的参数,则应该给予提示。

现在我们使用Result来包装返回结果:

package com.example.demo;public class Result {private T data;private String message;public Result(ResultBuilder builder) {this.data = builder.data;this.message = builder.message;}public static class ResultBuilder {private T data;private String message;public ResultBuilder data(T data) {this.data = data;return this;}public ResultBuilder message(String message) {this.message = message;return this;}public Result build() {return new Result(this);}}// setter/getter...
}

调整后的Controller如下:添加对JsonMappingException的处理

package com.example.demo.controller;import java.util.Date;import org.apache.commons.lang3.exception.ExceptionUtils;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;import com.example.demo.Clazz;
import com.example.demo.Result;
import com.example.demo.User;
import com.example.demo.util.XmlDateUtils;
import com.fasterxml.jackson.databind.JsonMappingException;@RestController
public class XmlController {/*** JsonMappingException Handler.* * @param e*            JsonMappingException* @return ResponseEntity>*/@ExceptionHandler(value = JsonMappingException.class)public ResponseEntity> exceptionHandler(JsonMappingException e) {JsonMappingException.Reference reference = e.getPath().get(0);Throwable rootCause = ExceptionUtils.getRootCause(e);String message = reference.getDescription() + ": " + rootCause.getMessage();return ResponseEntity.badRequest().body(new Result.ResultBuilder().message(message).build());}@PostMapping(value = "/xml", produces = "application/xml")public ResponseEntity> xmlTest(@RequestBody User request) {User user = new User();user.setId(1L);user.setUsername("test user");user.setBirthDate(XmlDateUtils.convertToXMLGregorianCalendar(new Date()));user.setCreateDate(XmlDateUtils.convertToXMLGregorianCalendar(new Date()));Clazz clazz = new Clazz();clazz.setId(1L);clazz.setName("class 1");user.setClazz(clazz);return ResponseEntity.ok(new Result.ResultBuilder().data(user).build());}}

如果传入参数不对,则返回相应的message给调用方:

注意:关于异常处理的问题

如果@ExceptionHandler也处理RuntimeException的情况下,则不会进入到@ExceptionHandler(JsonMappingException.class)。

这里会按照ExceptionDepthComparator进行排序,所以我们需要调整一下@ExceptionHandler的值:

JsonMappingException => HttpMessageNotReadableException.class

HttpMessageNotReadableException这个异常才是HttpMessageConverter#read方法抛出的异常。

我们可以拿到这个异常,然后从这个异常中的cause中找到JsonMappingException来处理。

4.8 为什么@RequestMapping没有添加produces = "application/xml;charset=utf-8"

这里在@RequestMapping注解上可以不添加produces = "application/xml;charset=utf-8",因为目前SpringMVC中只注册了一个HttpMessageConverter,那就是MappingJackson2XmlHttpMessageConverter。其支持application/xml,text/xml,application/*+xml三种类型。

如果指定了produces = "application/xml;charset=utf-8",那么就会使用这个作为要返回的MediaType。如果没有指定,则按照目前的配置,也会使用application/xml来作为返回的MediaType。

且看AbstractMessageConverterMethodProcessor类中的writeWithMessageConverters方法:

protected  void writeWithMessageConverters(@Nullable T value, MethodParameter returnType,ServletServerHttpRequest inputMessage, ServletServerHttpResponse outputMessage)throws IOException, HttpMediaTypeNotAcceptableException, HttpMessageNotWritableException {Object body;Class valueType;Type targetType;// 要点1:根据传入参数,来设置body,valueType和targetTypeif (value instanceof CharSequence) {body = value.toString();valueType = String.class;targetType = String.class;}else {body = value;valueType = getReturnValueType(body, returnType);targetType = GenericTypeResolver.resolveType(getGenericType(returnType), returnType.getContainingClass());}if (isResourceType(value, returnType)) {outputMessage.getHeaders().set(HttpHeaders.ACCEPT_RANGES, "bytes");if (value != null && inputMessage.getHeaders().getFirst(HttpHeaders.RANGE) != null &&outputMessage.getServletResponse().getStatus() == 200) {Resource resource = (Resource) value;try {List httpRanges = inputMessage.getHeaders().getRange();outputMessage.getServletResponse().setStatus(HttpStatus.PARTIAL_CONTENT.value());body = HttpRange.toResourceRegions(httpRanges, resource);valueType = body.getClass();targetType = RESOURCE_REGION_LIST_TYPE;}catch (IllegalArgumentException ex) {outputMessage.getHeaders().set(HttpHeaders.CONTENT_RANGE, "bytes */" + resource.contentLength());outputMessage.getServletResponse().setStatus(HttpStatus.REQUESTED_RANGE_NOT_SATISFIABLE.value());}}}// 要点2:找到要返回的MediaType,变量为selectedMediaTypeMediaType selectedMediaType = null;MediaType contentType = outputMessage.getHeaders().getContentType();if (contentType != null && contentType.isConcrete()) {if (logger.isDebugEnabled()) {logger.debug("Found 'Content-Type:" + contentType + "' in response");}selectedMediaType = contentType;}else {HttpServletRequest request = inputMessage.getServletRequest();// Request中Accept指定的MediaTypeList acceptableTypes = getAcceptableMediaTypes(request);// Response中可以产生的MediaTypeList producibleTypes = getProducibleMediaTypes(request, valueType, targetType);if (body != null && producibleTypes.isEmpty()) {throw new HttpMessageNotWritableException("No converter found for return value of type: " + valueType);}// 选择可以使用的MediaTypeList mediaTypesToUse = new ArrayList<>();for (MediaType requestedType : acceptableTypes) {for (MediaType producibleType : producibleTypes) {if (requestedType.isCompatibleWith(producibleType)) {mediaTypesToUse.add(getMostSpecificMediaType(requestedType, producibleType));}}}if (mediaTypesToUse.isEmpty()) {if (body != null) {throw new HttpMediaTypeNotAcceptableException(producibleTypes);}if (logger.isDebugEnabled()) {logger.debug("No match for " + acceptableTypes + ", supported: " + producibleTypes);}return;}MediaType.sortBySpecificityAndQuality(mediaTypesToUse);// 选取最终要使用的MediaTypefor (MediaType mediaType : mediaTypesToUse) {if (mediaType.isConcrete()) {selectedMediaType = mediaType;break;}else if (mediaType.isPresentIn(ALL_APPLICATION_MEDIA_TYPES)) {selectedMediaType = MediaType.APPLICATION_OCTET_STREAM;break;}}if (logger.isDebugEnabled()) {logger.debug("Using '" + selectedMediaType + "', given " +acceptableTypes + " and supported " + producibleTypes);}}// 遍历所有的MessageConverter,找到第一个合适的MessageConverter去处理if (selectedMediaType != null) {selectedMediaType = selectedMediaType.removeQualityValue();for (HttpMessageConverter converter : this.messageConverters) {GenericHttpMessageConverter genericConverter = (converter instanceof GenericHttpMessageConverter ?(GenericHttpMessageConverter) converter : null);if (genericConverter != null ?((GenericHttpMessageConverter) converter).canWrite(targetType, valueType, selectedMediaType) :converter.canWrite(valueType, selectedMediaType)) {body = getAdvice().beforeBodyWrite(body, returnType, selectedMediaType,(Class>) converter.getClass(),inputMessage, outputMessage);if (body != null) {Object theBody = body;LogFormatUtils.traceDebug(logger, traceOn ->"Writing [" + LogFormatUtils.formatValue(theBody, !traceOn) + "]");addContentDispositionHeader(inputMessage, outputMessage);if (genericConverter != null) {genericConverter.write(body, targetType, selectedMediaType, outputMessage);}else {((HttpMessageConverter) converter).write(body, selectedMediaType, outputMessage);}}else {if (logger.isDebugEnabled()) {logger.debug("Nothing to write: null body");}}return;}}}if (body != null) {throw new HttpMediaTypeNotAcceptableException(this.allSupportedMediaTypes);}
}

参考

  1. 百度百科-JAXB2

  1. maven jaxb2利用xsd生成Java类

  1. JAXB2的几个Maven插件的区别

  1. Spring Docs - OXM Marshaller and Unmarshaller

  1. jackson module jaxb annotation

相关内容

热门资讯

23岁是个怎样的年纪 23岁是个怎样的年纪是个相当尴尬的年纪,别人都结婚生孩子,我就孤家寡人,老妈天天催我相亲
科达利跌1.38%,成交额3.... 6月9日,科达利跌1.38%,成交额3.19亿元,换手率1.45%,总市值304.68亿元。异动分析...
中国电建涨1.02%,成交额5... 6月9日,中国电建涨1.02%,成交额5.50亿元,换手率0.86%,总市值849.25亿元。异动分...
求大乔小乔被董卓干的漫画集 求大乔小乔被董卓干的漫画集这样的漫画我没看过,但是我看过一个跟这个差不多的漫画,叫九九八十一不能走吧...
亚玛顿涨1.68%,成交额33... 6月9日,亚玛顿涨1.68%,成交额3392.91万元,换手率1.13%,总市值30.08亿元。异动...
中航光电跌0.67%,成交额6... 6月9日,中航光电跌0.67%,成交额6.02亿元,换手率0.72%,总市值844.55亿元。异动分...
辽宁舰在日本附近太平洋海域活动... 6月9日,外交部发言人林剑主持例行记者会。日本共同社记者提问,日本政府证实,中国航母辽宁舰在日本附近...
胜宏科技涨0.60%,成交额4... 6月9日,胜宏科技涨0.60%,成交额43.18亿元,换手率5.00%,总市值869.59亿元。异动...
报喜鸟涨0.52%,成交额70... 6月9日,报喜鸟涨0.52%,成交额7076.48万元,换手率1.30%,总市值56.18亿元。异动...
皓宸医疗涨停,成交额1.89亿... 6月9日,皓宸医疗涨停,成交额1.89亿元,换手率7.25%,总市值26.21亿元。异动分析民营医院...
广电运通涨0.24%,成交额5... 6月9日,广电运通涨0.24%,成交额5.00亿元,换手率1.60%,总市值313.40亿元。异动分...
昆药集团涨1.16%,成交额1... 6月9日,昆药集团涨1.16%,成交额1.95亿元,换手率1.64%,总市值118.47亿元。异动分...
信立泰涨7.76%,成交额6.... 6月9日,信立泰涨7.76%,成交额6.16亿元,换手率1.11%,总市值564.99亿元。异动分析...
神农种业涨0.00%,成交额4... 6月9日,神农种业涨0.00%,成交额4.29亿元,换手率11.19%,总市值44.44亿元。异动分...
广发证券涨1.28%,成交额6... 6月9日,广发证券涨1.28%,成交额6.92亿元,换手率0.70%,总市值1267.13亿元。异动...
新疆浩源涨0.11%,成交额1... 6月9日,新疆浩源涨0.11%,成交额1579.15万元,换手率0.52%,总市值37.43亿元。异...
中国铝业涨1.34%,成交额1... 6月9日,中国铝业涨1.34%,成交额10.47亿元,换手率1.18%,总市值1170.01亿元。异...
维尔利涨1.47%,成交额24... 6月9日,维尔利涨1.47%,成交额2412.48万元,换手率0.91%,总市值26.89亿元。异动...
中远海特涨0.32%,成交额8... 6月9日,中远海特涨0.32%,成交额8570.24万元,换手率0.65%,总市值169.85亿元。...
平煤股份跌1.39%,成交额2... 6月9日,平煤股份跌1.39%,成交额2.51亿元,换手率1.30%,总市值193.06亿元。异动分...