spring boot 支持返回 xml
实现技术方式对比
JAXB(Java Architecture for XML Binding) 是一个业界的标准,可以实现java类和xml的互转
jdk中包括JAXB
JAXB vs jackson-dataformat-xml
spring boot中默认使用jackson返回json,jackson-dataformat-xml 中的 XmlMapper extends ObjectMapper 所以对于xml而已跟json的使用方式更类似,并且可以识别
pojo上的 @JsonProperty、 @JsonIgnore 等注解,所以推荐使用 jackson-dataformat-xml 来处理xml
jaxb 对list的支持不好也,使用比较复杂
package com.example.demo; import com.fasterxml.jackson.annotation.JsonAutoDetect.Visibility; import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.annotation.PropertyAccessor; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.SerializationFeature; import com.fasterxml.jackson.dataformat.xml.XmlMapper; class MyPojo { @JsonProperty("_id") private String id; private String name; private int age; @JsonIgnore private String note; public String getNote() { return note; } public void setNote(String note) { this.note = note; } public String getId() { return id; } public void setId(String id) { this.id = id; } public String getName() { return name; } public void setName(String name) { this.name = name; } public int getAge() { return age; } public void setAge(int age) { this.age = age; } } public class Test { public static void main(String[] args) throws JsonProcessingException { XmlMapper mapper1 = new XmlMapper(); ObjectMapper mapper2 = new ObjectMapper(); mapper1.setVisibility(PropertyAccessor.FIELD, Visibility.ANY); mapper2.setVisibility(PropertyAccessor.FIELD, Visibility.ANY); mapper1.enable(SerializationFeature.INDENT_OUTPUT); mapper2.enable(SerializationFeature.INDENT_OUTPUT); MyPojo mypojo = new MyPojo(); mypojo.setName("Dhani"); mypojo.setId("18082013"); mypojo.setAge(5); String jsonStringXML = mapper1.writeValueAsString(mypojo); String jsonStringJSON = mapper2.writeValueAsString(mypojo); // takes java class with def or customized constructors and creates JSON System.out.println("XML is " + "\n" + jsonStringXML + "\n"); System.out.println("Json is " + "\n" + jsonStringJSON); } }
接口返回xml
spring boot中默认用注册的xml HttpMessageConverter 为 Jaxb2RootElementHttpMessageConverter
接口返回xml
//需要有注解,否则会报No converter for [class com.example.demo.IdNamePair] with preset Content-Type 'null' 错误 @XmlRootElement public class IdNamePair { Integer id; String name; public Integer getId() { return id; } public void setId(Integer id) { this.id = id; } public String getName() { return name; } public void setName(String name) { this.name = name; } }
原因:Jaxb2RootElementHttpMessageConverter 中
@Override public boolean canWrite(Class<?> clazz, @Nullable MediaType mediaType) { return (AnnotationUtils.findAnnotation(clazz, XmlRootElement.class) != null && canWrite(mediaType)); }
控制器
package com.example.demo; import org.springframework.http.MediaType; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @RestController public class WelcomeController { /** * <IdNamePair> * <id>123</id> * <name>蓝银草</name> * </IdNamePair> * * @return */ @RequestMapping(value = "/xml")
// produces = MediaType.APPLICATION_JSON_VALUE 增加可以强制指定返回的类型,不指定则默认根据 请求头中的 Accept 进行判定
// 注意返回类型 HttpServletResponse response; response.setContentType(MediaType.APPLICATION_JSON_VALUE); 设置不生效 todo
// 示例:*/* 、 text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
public IdNamePair xml() { IdNamePair idNamePair = new IdNamePair(); idNamePair.setId(123); idNamePair.setName("蓝银草"); return idNamePair; } }
一个请求同时支持返回 json 和 xml
1、根据header中的Accept自动判定
@RestController public class WelcomeController { @RequestMapping(value = "/both") public IdNamePair both() { IdNamePair idNamePair = new IdNamePair(); idNamePair.setId(456); idNamePair.setName("蓝银草"); return idNamePair; } }
2、根据指定的参数
@Configuration public class WebInterceptorAdapter implements WebMvcConfigurer { @Override public void configureContentNegotiation(ContentNegotiationConfigurer configurer) { configurer.favorParameter(true) // 是否支持参数化处理请求 .parameterName("format") // 参数的名称, 默认为format .defaultContentType(MediaType.APPLICATION_JSON) // 全局的默认返回类型 .mediaType("xml", MediaType.APPLICATION_XML) // format 参数值与对应的类型XML .mediaType("json", MediaType.APPLICATION_JSON); // format 参数值与对应的类型JSON } }
请求url
http://127.0.0.1:8080/both?format=json
http://127.0.0.1:8080/both?format=xml
该功能默认未开启
参考源码:
public static class Contentnegotiation { /** * Whether the path extension in the URL path should be used to determine the * requested media type. If enabled a request "/users.pdf" will be interpreted as * a request for "application/pdf" regardless of the 'Accept' header. */ private boolean favorPathExtension = false; /** * Whether a request parameter ("format" by default) should be used to determine * the requested media type. */ private boolean favorParameter = false; /** * Map file extensions to media types for content negotiation. For instance, yml * to text/yaml. */ private Map<String, MediaType> mediaTypes = new LinkedHashMap<>(); /** * Query parameter name to use when "favor-parameter" is enabled. */ private String parameterName;
浏览器访问以前返回json的现在都返回xml问题
以前的消息转换器不支持xml格式,但有支持json的消息转换器,根据浏览器请求头 中的 Accept 字段,先匹配xml【不支持】在匹配json,所以最后为json
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
引入 fastxml 后支持 xml格式消息转换器,并且Accept中又是优先匹配 xml,故以前所有的接口现在浏览器访问都变成 xml 格式的了,但用postman仍旧为json 【Accept:*/* 】
解决:
@Configuration public class WebInterceptorAdapter implements WebMvcConfigurer { @Override public void configureContentNegotiation(ContentNegotiationConfigurer configurer) { //configurer. configurer .ignoreAcceptHeader(true) //忽略头信息中 Accept 字段 .defaultContentType(MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML); //采用固定的内容协商策略 FixedContentNegotiationStrategy } }
配置前内容协商:
如果没有忽略自动协商【按Accept】
org.springframework.web.accept.ContentNegotiationManager
会自动添加 strategies.add(new HeaderContentNegotiationStrategy());
org.springframework.web.accept.HeaderContentNegotiationStrategy#resolveMediaTypes
/** * {@inheritDoc} * @throws HttpMediaTypeNotAcceptableException if the 'Accept' header cannot be parsed */ @Override public List<MediaType> resolveMediaTypes(NativeWebRequest request) throws HttpMediaTypeNotAcceptableException { String[] headerValueArray = request.getHeaderValues(HttpHeaders.ACCEPT); if (headerValueArray == null) { return MEDIA_TYPE_ALL_LIST; } List<String> headerValues = Arrays.asList(headerValueArray); try {
//根据Accept字段计算 media type List<MediaType> mediaTypes = MediaType.parseMediaTypes(headerValues); MediaType.sortBySpecificityAndQuality(mediaTypes); return !CollectionUtils.isEmpty(mediaTypes) ? mediaTypes : MEDIA_TYPE_ALL_LIST; } catch (InvalidMediaTypeException ex) { throw new HttpMediaTypeNotAcceptableException( "Could not parse 'Accept' header " + headerValues + ": " + ex.getMessage()); } }
配置后内容协商:
org.springframework.web.accept.FixedContentNegotiationStrategy#resolveMediaTypes
@Override public List<MediaType> resolveMediaTypes(NativeWebRequest request) {
//固定返回配置的类型 defaultContentType(MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML); return this.contentTypes; }
controller 中设置 content-type失效问题
1、在带有返回值的情况下,在controller中设置content-type是无效的,会被消息转换器覆盖掉
2、优先使用 produces = MediaType.TEXT_PLAIN_VALUE ,没有则会根据请求头中的 accept 和 HttpMessageConverter 支持的类型
计算出一个
org.springframework.web.servlet.mvc.method.RequestMappingInfoHandlerMapping#handleMatch //寻找合适的 HandlerMapping,找到后执行一写处理逻辑,中间包括处理 @RequestMappin 中的 produces /** * Expose URI template variables, matrix variables, and 【producible media types 】in the request. * @see HandlerMapping#URI_TEMPLATE_VARIABLES_ATTRIBUTE * @see HandlerMapping#MATRIX_VARIABLES_ATTRIBUTE * @see HandlerMapping#PRODUCIBLE_MEDIA_TYPES_ATTRIBUTE */ @Override protected void handleMatch(RequestMappingInfo info, String lookupPath, HttpServletRequest request) { super.handleMatch(info, lookupPath, request); String bestPattern; Map<String, String> uriVariables; Set<String> patterns = info.getPatternsCondition().getPatterns(); if (patterns.isEmpty()) { bestPattern = lookupPath; uriVariables = Collections.emptyMap(); } else { bestPattern = patterns.iterator().next(); uriVariables = getPathMatcher().extractUriTemplateVariables(bestPattern, lookupPath); } request.setAttribute(BEST_MATCHING_PATTERN_ATTRIBUTE, bestPattern); if (isMatrixVariableContentAvailable()) { Map<String, MultiValueMap<String, String>> matrixVars = extractMatrixVariables(request, uriVariables); request.setAttribute(HandlerMapping.MATRIX_VARIABLES_ATTRIBUTE, matrixVars); } Map<String, String> decodedUriVariables = getUrlPathHelper().decodePathVariables(request, uriVariables); request.setAttribute(HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE, decodedUriVariables); //处理@RequestMapping中的produces属性,后面计算合适的mediatype时会用到 if (!info.getProducesCondition().getProducibleMediaTypes().isEmpty()) { Set<MediaType> mediaTypes = info.getProducesCondition().getProducibleMediaTypes(); request.setAttribute(PRODUCIBLE_MEDIA_TYPES_ATTRIBUTE, mediaTypes); } } #消息转换器写消息 org.springframework.web.servlet.mvc.method.annotation.AbstractMessageConverterMethodProcessor#writeWithMessageConverters(T, org.springframework.core.MethodParameter, org.springframework.http.server.ServletServerHttpRequest, org.springframework.http.server.ServletServerHttpResponse) #寻找合适的可以返回的 mediatype org.springframework.web.servlet.mvc.method.annotation.AbstractMessageConverterMethodProcessor#getProducibleMediaTypes(javax.servlet.http.HttpServletRequest, java.lang.Class<?>, java.lang.reflect.Type) /** * Returns the media types that can be produced. The resulting media types are: * <ul> * <li>The producible media types specified in the request mappings, or * <li>Media types of configured converters that can write the specific return value, or * <li>{@link MediaType#ALL} * </ul> * @since 4.2 */ @SuppressWarnings("unchecked") protected List<MediaType> getProducibleMediaTypes( HttpServletRequest request, Class<?> valueClass, @Nullable Type targetType) { //如果注解中有则直接使用 Set<MediaType> mediaTypes = (Set<MediaType>) request.getAttribute(HandlerMapping.PRODUCIBLE_MEDIA_TYPES_ATTRIBUTE); if (!CollectionUtils.isEmpty(mediaTypes)) { return new ArrayList<>(mediaTypes); } else if (!this.allSupportedMediaTypes.isEmpty()) { //注解中没有在根据支持的消息转换器计算出一个来 List<MediaType> result = new ArrayList<>(); for (HttpMessageConverter<?> converter : this.messageConverters) { if (converter instanceof GenericHttpMessageConverter && targetType != null) { if (((GenericHttpMessageConverter<?>) converter).canWrite(targetType, valueClass, null)) { result.addAll(converter.getSupportedMediaTypes()); } } else if (converter.canWrite(valueClass, null)) { result.addAll(converter.getSupportedMediaTypes()); } } return result; } else { return Collections.singletonList(MediaType.ALL); } } org.springframework.http.converter.AbstractGenericHttpMessageConverter#write org.springframework.http.converter.AbstractHttpMessageConverter#addDefaultHeaders /** * Add default headers to the output message. * <p>This implementation delegates to {@link #getDefaultContentType(Object)} if a * content type was not provided, set if necessary the default character set, calls * {@link #getContentLength}, and sets the corresponding headers. * @since 4.2 */ protected void addDefaultHeaders(HttpHeaders headers, T t, @Nullable MediaType contentType) throws IOException { if (headers.getContentType() == null) { MediaType contentTypeToUse = contentType; if (contentType == null || !contentType.isConcrete()) { contentTypeToUse = getDefaultContentType(t); } else if (MediaType.APPLICATION_OCTET_STREAM.equals(contentType)) { MediaType mediaType = getDefaultContentType(t); contentTypeToUse = (mediaType != null ? mediaType : contentTypeToUse); } if (contentTypeToUse != null) { if (contentTypeToUse.getCharset() == null) { Charset defaultCharset = getDefaultCharset(); if (defaultCharset != null) { contentTypeToUse = new MediaType(contentTypeToUse, defaultCharset); } } //增加计算出的 content-type , controller中设置的可以存下来,但是不会最终使用到 headers.setContentType(contentTypeToUse); } } if (headers.getContentLength() < 0 && !headers.containsKey(HttpHeaders.TRANSFER_ENCODING)) { Long contentLength = getContentLength(t, headers.getContentType()); if (contentLength != null) { headers.setContentLength(contentLength); } } } org.springframework.http.server.ServletServerHttpResponse#getBody org.springframework.http.server.ServletServerHttpResponse#writeHeaders private void writeHeaders() { if (!this.headersWritten) { //上面的设置的头信息 getHeaders().forEach((headerName, headerValues) -> { for (String headerValue : headerValues) { //this.servletResponse 控制器重设置的content-type现在被覆盖掉了 this.servletResponse.addHeader(headerName, headerValue); } }); // HttpServletResponse exposes some headers as properties: we should include those if not already present
//从 this.servletResponse【原始的request对象,有写会被覆盖,所以会不生效,如content-type 】中补充一些其他的头信息
if (this.servletResponse.getContentType() == null && this.headers.getContentType() != null) { this.servletResponse.setContentType(this.headers.getContentType().toString()); } if (this.servletResponse.getCharacterEncoding() == null && this.headers.getContentType() != null && this.headers.getContentType().getCharset() != null) { this.servletResponse.setCharacterEncoding(this.headers.getContentType().getCharset().name()); } this.headersWritten = true; } }
附录
目前的 httpclient 和 okHttp中都不会传 Accept 头
#httpclient post Array ( [Content-Length] => 0 [Host] => jksong.cm [Connection] => Keep-Alive [User-Agent] => Apache-HttpClient/4.5.6 (Java/1.8.0_251) [Accept-Encoding] => gzip,deflate ) #httpclient get ( [Host] => jksong.cm [Connection] => Keep-Alive [User-Agent] => Apache-HttpClient/4.5.6 (Java/1.8.0_251) [Accept-Encoding] => gzip,deflate ) #okhttp get ( [Host] => jksong.cm [Connection] => Keep-Alive [Accept-Encoding] => gzip [User-Agent] => okhttp/3.14.4 ) #okhttp post ( [Content-Type] => text/plain; charset=utf-8 [Content-Length] => 0 [Host] => jksong.cm [Connection] => Keep-Alive [Accept-Encoding] => gzip [User-Agent] => okhttp/3.14.4 )
#curl get
Array
(
[Host] => jksong.cm
[User-Agent] => curl/7.64.1
[Accept] => */*
)
参考:
https://stackoverflow.com/questions/39304246/xml-serialization-jaxb-vs-jackson-dataformat-xml
https://blog.csdn.net/jiangchao858/article/details/85346041