项目中使用到了jaxb2技术,即通过运行maven-jaxb2-plugin插件,来根据XSD文件生成一堆Java类。
后端使用SpringMVC技术,需要在返回给前端的时候,返回XML数据,而不是JSON数据。
另外还有一些小的需求:
在返回的XML中按照要求返回特定的属性
在XML中限定日期和日期时间的格式
英文全称是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中找到它。
这个是Jackson的扩展的Module,用户使用JAXB框架生成的Java类(包含很多JAXB注解),而这个Module提供了对JAXB(javax.xml.bind)注解的支持。
com.fasterxml.jackson.module jackson-module-jaxb-annotations 2.9.8
有两种方式可以启用这个module,来实现对JAXB注解的支持:
注册JaxbAnnotationModule
直接添加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);
使用Jackson来实现XML的序列化和反序列化:MappingJackson2XmlHttpMessageConverter。
使用Jackson的@JsonIgnoreProperties去忽略掉不需要返回的属性。
Jackson有个module:可以检测并支持JAXB2的注解。注意:有部分注解是不支持的。
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
添加此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");}
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...
}
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注解是生效的。
我们需要使用@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;// ...
}
再调用接口,结果如下:
项目需要把日期格式改成: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;
让我们再调用下接口:
假定我们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来处理。
这里在@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 extends HttpMessageConverter>>) 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);}
}
百度百科-JAXB2
maven jaxb2利用xsd生成Java类
JAXB2的几个Maven插件的区别
Spring Docs - OXM Marshaller and Unmarshaller
jackson module jaxb annotation