SpringMVC使用Jackson返回XML格式

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.module
	jackson-module-jaxb-annotations
	2.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 {

	@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");
 }

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得到的结果如下:

SpringMVC使用Jackson返回XML格式_第1张图片

可以看到,属性名称都是按照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;
    
    // ...
}

再调用接口,结果如下:

SpringMVC使用Jackson返回XML格式_第2张图片

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 {

	@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;

让我们再调用下接口:

SpringMVC使用Jackson返回XML格式_第3张图片

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给调用方:

SpringMVC使用Jackson返回XML格式_第4张图片

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

如果@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和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>) 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

你可能感兴趣的:(xml,java,spring,springmvc,xml)