• 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 " + "
    " + jsonStringXML + "
    ");
            System.out.println("Json is " + "
    " + 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 

     
  • 相关阅读:
    #Leetcode# 164. Maximum Gap
    #Leetcode# 155. Min Stack
    #Leetcode# 165. Compare Version Numbers
    Linux——信息分析(四)域名分析dig、host、
    Linux——信息采集(三)dmitry、路由跟踪命令tracerouter
    密码学——cookie攻击
    final关键字
    重载与重写
    break and continue
    计算机硬件系统
  • 原文地址:https://www.cnblogs.com/siqi/p/13493955.html
Copyright © 2020-2023  润新知