实现技术方式对比
JAXB(Java Architecture for XML Binding) 是一个业界的标准,可以实现java类和xml的互转
jdk中包括JAXB
JAXB vsjackson-dataformat-xml
spring boot中默认使用jackson返回json,jackson-dataformat-xml 中的 XmlMapper extendsObjectMapper 所以对于xml而已跟json的使用方式更类似,并且可以识别
pojo上的@JsonProperty、 @JsonIgnore 等注解,所以推荐使用jackson-dataformat-xml 来处理xml
jaxb 对list的支持不好也,使用比较复杂
packagecom.example.demo; importcom.fasterxml.jackson.annotation.JsonAutoDetect.Visibility; importcom.fasterxml.jackson.annotation.JsonIgnore; importcom.fasterxml.jackson.annotation.JsonProperty; importcom.fasterxml.jackson.annotation.PropertyAccessor; importcom.fasterxml.jackson.core.JsonProcessingException; importcom.fasterxml.jackson.databind.ObjectMapper; importcom.fasterxml.jackson.databind.SerializationFeature; importcom.fasterxml.jackson.dataformat.xml.XmlMapper; classMyPojo { @JsonProperty("_id") privateString id; privateString name; private intage; @JsonIgnore privateString note; publicString getNote() { returnnote; } public voidsetNote(String note) { this.note =note; } publicString getId() { returnid; } public voidsetId(String id) { this.id =id; } publicString getName() { returnname; } public voidsetName(String name) { this.name =name; } public intgetAge() { returnage; } public void setAge(intage) { this.age =age; } } public classTest { public static void main(String[] args) throwsJsonProcessingException { XmlMapper mapper1 = newXmlMapper(); ObjectMapper mapper2 = newObjectMapper(); mapper1.setVisibility(PropertyAccessor.FIELD, Visibility.ANY); mapper2.setVisibility(PropertyAccessor.FIELD, Visibility.ANY); mapper1.enable(SerializationFeature.INDENT_OUTPUT); mapper2.enable(SerializationFeature.INDENT_OUTPUT); MyPojo mypojo = newMyPojo(); 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 " + " " + jsonStringXML + " "); System.out.println("Json is " + " " +jsonStringJSON); } }
接口返回xml
spring boot中默认用注册的xmlHttpMessageConverter 为Jaxb2RootElementHttpMessageConverter
接口返回xml
//需要有注解,否则会报No converter for [class com.example.demo.IdNamePair] with preset Content-Type 'null' 错误 @XmlRootElement public classIdNamePair { Integer id; String name; publicInteger getId() { returnid; } public voidsetId(Integer id) { this.id =id; } publicString getName() { returnname; } public voidsetName(String name) { this.name =name; } }
原因:Jaxb2RootElementHttpMessageConverter 中
@Override public boolean canWrite(Class<?>clazz, @Nullable MediaType mediaType) { return (AnnotationUtils.findAnnotation(clazz, XmlRootElement.class) != null &&canWrite(mediaType)); }
控制器
packagecom.example.demo; importorg.springframework.http.MediaType; importorg.springframework.web.bind.annotation.RequestMapping; importorg.springframework.web.bind.annotation.RestController; @RestController public classWelcomeController { /*** <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
publicIdNamePair xml() { IdNamePair idNamePair = newIdNamePair(); idNamePair.setId(123); idNamePair.setName("蓝银草"); returnidNamePair; } }
一个请求同时支持返回 json 和 xml
1、根据header中的Accept自动判定
@RestController public classWelcomeController { @RequestMapping(value = "/both") publicIdNamePair both() { IdNamePair idNamePair = newIdNamePair(); idNamePair.setId(456); idNamePair.setName("蓝银草"); returnidNamePair; } }
2、根据指定的参数
@Configuration public class WebInterceptorAdapter implementsWebMvcConfigurer { @Override public voidconfigureContentNegotiation(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 classContentnegotiation { /*** 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 implementsWebMvcConfigurer { @Override public voidconfigureContentNegotiation(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} * @throwsHttpMediaTypeNotAcceptableException if the 'Accept' header cannot be parsed */@Override public List<MediaType>resolveMediaTypes(NativeWebRequest request) throwsHttpMediaTypeNotAcceptableException { String[] headerValueArray =request.getHeaderValues(HttpHeaders.ACCEPT); if (headerValueArray == null) { returnMEDIA_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 newHttpMediaTypeNotAcceptableException( "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. * @seeHandlerMapping#URI_TEMPLATE_VARIABLES_ATTRIBUTE * @seeHandlerMapping#MATRIX_VARIABLES_ATTRIBUTE * @seeHandlerMapping#PRODUCIBLE_MEDIA_TYPES_ATTRIBUTE */@Override protected voidhandleMatch(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>{@linkMediaType#ALL} * </ul> * @since4.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()); } } returnresult; } else{ returnCollections.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. * @since4.2 */ protected void addDefaultHeaders(HttpHeaders headers, T t, @Nullable MediaType contentType) throwsIOException { 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 = newMediaType(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 voidwriteHeaders() { 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