實現技術方式對比
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