如何在Spring中注册自定义HttpMessageConverter来处理无效的Content-Type?

时间:2017-05-24 21:31:11

标签: java spring spring-mvc

我正在使用POST将代码编写到RestTemplate数据到第三方API。该API使用内容类型text;charset=UTF-8进行响应,而Spring会抛出InvalidMediaTypeException,因为该内容类型不包含/。是否有可能向Spring表明内容类型text应该被视为text/plain的内容类型?如果是这样,我该如何做到这一点?

这是导致问题的代码。我无法展示URL,但我认为这并不重要。

// Make the body of the request.
MultiValueMap<String, String> body = new LinkedMultiValueMap<String, String>();
body.add("Customer Street", "a test street!");

// Make the headers of the request.
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
headers.setAccept(Arrays.asList(MediaType.TEXT_PLAIN));

// Create an HTTP entity.
HttpEntity<MultiValueMap<String, String>> entity = new HttpEntity<MultiValueMap<String, String>>(body, headers);

// Get a rest template
RestTemplate rest = new RestTemplate();

// Post the data.
String resp = null;
try {
    resp = rest.postForObject(URL, entity, String.class);
} catch (InvalidMediaTypeException e) {
    e.printStackTrace();
    return;
}

相关SO问题

This问题几乎完全描述了我遇到的问题。这个问题的公认答案基本上是“看到这个问题”,我做了;它描述如下。

this问题中(在上面的答案中链接),istibekesi询问使用自定义内容类型myXml,并提供无效的示例配置。 Brian Clozel's回答帮助我理解了之前我不理解的内容类型的一些事情,但我仍然对这些问题感到困惑:

  1. Brian说,给定的配置应将myXml注册为路径扩展/参数,以便与application/xml进行协商。我最初的理解是,Brian的意思是,“将接受标头设置为myXml的请求应与接受标头设置为application/xml的请求相同。”但是,现在我很确定Brian的意思是“以.myXml结尾或使用查询参数format=myXml的请求应被视为application/xml。”我的第二种解释是否正确?如果是,为什么给定的配置强制Spring将接受标头设置为myXml作为application/xml处理请求?

  2. Brian说,istibekesi应该做的是注册HttpMessageConverter然后注册application/xmlmyXml。我想我理解Brian的意思是“注册一个HttpMessageConverter”,但我无法弄清楚如何注册自定义媒体类型(例如myXml)来使用HttpMessageConverter }。

  3. 不幸的是,Brian建议“使用类似application/vnd.foobar.v.1.0+xml的媒体类型”对我没有帮助,因为我无法控制我发送的响应的内容类型。我已尝试将请求的接受标头设置为text/plain,但它没有更改响应。

  4. 其他研究

    Spring抛出的异常的堆栈跟踪是

    org.springframework.http.InvalidMediaTypeException: Invalid mime type "text;charset=UTF-8": does not contain '/'
        at org.springframework.http.MediaType.parseMediaType(MediaType.java:452)
        at org.springframework.http.HttpHeaders.getContentType(HttpHeaders.java:745)
        at org.springframework.web.client.HttpMessageConverterExtractor.getContentType(HttpMessageConverterExtractor.java:114)
        at org.springframework.web.client.HttpMessageConverterExtractor.extractData(HttpMessageConverterExtractor.java:85)
        at org.springframework.web.client.RestTemplate.doExecute(RestTemplate.java:655)
        at org.springframework.web.client.RestTemplate.execute(RestTemplate.java:613)
        at org.springframework.web.client.RestTemplate.postForObject(RestTemplate.java:380)
        at com.agileBTS.hellosignTest.App.main(App.java:47)
    Caused by: org.springframework.util.InvalidMimeTypeException: Invalid mime type "text;charset=UTF-8": does not contain '/'
        at org.springframework.util.MimeTypeUtils.parseMimeType(MimeTypeUtils.java:256)
        at org.springframework.http.MediaType.parseMediaType(MediaType.java:449)
    

    我浏览了堆栈跟踪中每个函数的源代码,看看我是否能弄清楚发生了什么,我可以清楚地看到parseMimeType方法(抛出异常的第一种方法)非常简单:如果mime-type不包含/,则抛出异常。我不明白任何代码将如何解决这个问题,除非我继承MimeTypeUtils并强制Spring使用我的子类。这是必要的吗?这似乎很难。

    更新

    1

    在他们的回答中,Sean Carroll建议我使用行注册"text" mime类型

    c.setSupportedMediaTypes(Arrays.asList(MediaType.TEXT_PLAIN, MediaType.parseMediaType("text")));
    

    但是,如果您在第487行查看#parseMediaType方法here的源代码,您会看到#parseMediaType将大部分工作交给{{1} }} 方法。从第176行开始查看 源代码here,很明显MimeTypeUtils#parseMimeType会在第193行抛出#parseMimeType,因为IllegalMimeTypeException没有包含"text"(实际上,这是在我的应用程序中抛出/的确切代码行)。我需要的是解决这个问题。

    2

    经过测试,我已经确定配置内容协商管理器在我的情况下也不起作用。基于this教程,我认为XML配置很明显:

    IllegalMimeTypeException

    等同于此Java配置:

    <bean id="contentNegotiationManager" class="org.springframework.web.accept.ContentNegotiationManagerFactoryBean">
        <property name="mediaTypes">
            <map>
                <entry key="json" value="application/json" />
                <entry key="xml" value="application/xml" />
           </map>
        </property>
    </bean>
    

    看一下@Override public void configureContentNegotiation(ContentNegotiationConfigurer configurer) { configurer.mediaType("xml", MediaType.APPLICATION_XML) .mediaType("json", MediaType.APPLICATION_JSON); } 方法文档here,我看到了一行“从密钥添加映射,从路径扩展或查询参数中提取 ......“(强调我的);我想从该引文中排除“接受标题”是故意的。

2 个答案:

答案 0 :(得分:1)

这就是我解释Brian的答案的方式。我假设你正在使用java配置。要注册HttpMessageConverter,您可以执行以下操作

    @Configuration
@EnableWebMvc
public class WebConfig extends WebMvcConfigurerAdapter {

    @Override
    public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
        super.configureMessageConverters(converters);
        StringHttpMessageConverter stringMessageConverter = new StringHttpMessageConverter();

        // add support for "text" media type. This may require Charset of UTF-8
        c.setSupportedMediaTypes(Arrays.asList(MediaType.TEXT_PLAIN, MediaType.parseMediaType("text")));
        converters.add(stringMessageConverter);
    }

}

<击> 编辑:如何使用内容协商管理器将文本映射到text / plain?来自javadocs

  

对于路径扩展和参数策略,您可以显式添加MediaType映射。这将用于将路径扩展或参数值(如“json”)解析为媒体类型,例如“application / json”。

文档明确提到扩展和参数,我不完全确定,但它也可以在接受标题上工作(我需要更深入地研究代码)。如果不是,您可能需要查看自定义ContentNegotiationStrategy

<mvc:annotation-driven content-negotiation-manager="contentNegotiationManager" />

<bean id="contentNegotiationManager" class="org.springframework.web.accept.ContentNegotiationManagerFactoryBean">
    <property name="mediaTypes" >
        <value>
            text=plain/text
        </value>
    </property>
</bean>

答案 1 :(得分:1)

可以使用ClientHttpRequestInterceptor注册RestTemplate,允许在Spring对它们做任何事情之前编辑客户端响应。为了解决我的问题,我创建了一个实现ClientHttpRequestInterceptor的类,并覆盖了intercept方法,用text;标头中的application/json;替换Content-Type的所有实例。然后我使用我的ClientHttpRequestInterceptor bean定义注册了我的RestTemplate,这样每当我自动加载RestTemplate时,它就会有这个拦截器。

ClientHttpRequestInterceptor的代码:

public class ContentTypeTextToTextJson implements ClientHttpRequestInterceptor {

    private static final Logger LOG = LoggerFactory.getLogger(ContentTypeTextToTextJson.class);

    @Override
    public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution)
            throws IOException {
        LOG.debug("intercepting execution");

        // Get the response as normal.
        ClientHttpResponse response = execution.execute(request, body);
        LOG.debug("intercepted response: " + response);

        // Get the headers.
        HttpHeaders headers = response.getHeaders();
        LOG.debug("response had headers: " + headers);

        // Grab all the content types.
        List<String> contentTypes = headers.get("Content-Type");
        LOG.debug("response had content-types: " + contentTypes);

        // Loop over the content-types.
        for(int i = 0; i < contentTypes.size(); i++) {
            String contentType = contentTypes.get(i);
            LOG.debug("processing content type: " + contentType);
            // I'm not sure if it's possible for a content-type to be null, but I guess it's
            // better safe then sorry?
            if(null == contentType) {
                continue;
            }
            // If it starts with "text;", replace "text" with "text/json" and replace the old content type.
            if(contentType.startsWith("text;")) {
                contentType = contentType.replaceFirst("text", "application/json");
                LOG.debug("replacing content type " + contentTypes.get(i) + " with content type " + contentType);
                contentTypes.set(i, contentType);
            }
        }

        // Return the response.
        return response;
    }

}

RestTemplate bean定义:

<bean id="restTemplate" class="org.springframework.web.client.RestTemplate">
    <property name="interceptors">
        <list>
            <bean class="restinterceptors.ContentTypeTextToTextJson" />
        </list>
    </property>
</bean>