项目中使用到了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 {
@Override
public 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 {
@Override
public XMLGregorianCalendar unmarshal(String v) throws Exception {
return XmlDateUtils.convertToXMLGregorianCalendar(v, XmlDateUtils.XML_DATE_FORMAT);
}
@Override
public 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 {
@Override
public XMLGregorianCalendar unmarshal(String v) throws Exception {
return XmlDateUtils.convertToXMLGregorianCalendar(v, XmlDateUtils.XML_DATETIME_FORMAT);
}
@Override
public 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和targetType
if (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,变量为selectedMediaType
MediaType 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指定的MediaType
List acceptableTypes = getAcceptableMediaTypes(request);
// Response中可以产生的MediaType
List producibleTypes = getProducibleMediaTypes(request, valueType, targetType);
if (body != null && producibleTypes.isEmpty()) {
throw new HttpMessageNotWritableException(
"No converter found for return value of type: " + valueType);
}
// 选择可以使用的MediaType
List 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);
// 选取最终要使用的MediaType
for (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