C语言 c++ php mysql nginx linux lnmp lamp lanmp memcache redis 面试 笔记 ppt 设计模式 问题 远程连接

随笔 - 301  文章 - 1  评论 - 49 

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 

 

 
posted on 2020-08-13 00:31  思齐_  阅读(227)  评论(0编辑  收藏