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 

 

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM