java - Spring MVC : @RequestBody when no content-type is specified

标签 java json spring

我有一个 Spring MVC 应用程序,它以 JSON 字符串的形式从外部系统接收 HTTP 请求,它的响应以类似 JSON 字符串的形式返回。我的 Controller 使用 @RequestBody@ResponseBody 进行了正确注释,并且我进行了集成测试,这些测试实际发送请求以验证一切是否按预期工作。

但是,当我针对将要使用它的实际外部系统测试我的应用程序时,我发现传入的请求没有指定内容类型!这完全混淆了 Spring 并导致以下类型的错误:

DEBUG [] 2014-04-17 13:33:13,471 AbstractHandlerExceptionResolver.java:132 resolveException - Resolving exception from handler [com.example.controller.MyController@1d04f0a]: org.springframework.web.HttpMediaTypeNotSupportedException: Cannot extract parameter (ValidationRequest request): no Content-Type found

那么,有没有办法强制 Spring 通过 MappingJacksonHttpMessageConverter 路由这样的请求,通过某种方式强制 Spring 使用自定义处理程序链或修改传入请求以显式设置内容-类型?

我已经尝试了一些事情:

  • 扩展 MappingJacksonHttpMessageConverter,使其 canRead()canWrite() 方法始终返回 true。不幸的是,由于缺少内容类型,Spring 在退出之前甚至没有达到查看消息转换器的地步。
  • 使用拦截器或 Servlet 过滤器手动设置内容类型。不幸的是,除了设置新属性之外,我看不到任何一种方法可以让这些机制真正对传入请求进行更改。

欢迎任何想法。


为了解决下面的评论,我的 @RequestMapping 看起来像:

@RequestMapping(value="/{service}" )
public @ResponseBody MyResponseObject( @PathVariable String service, @RequestBody MyRequestObject request) {

所以这里没有指定 JSON 的内容,但是如果没有内容类型,Spring 似乎甚至不会尝试根据传入请求构建我的请求对象(这是有道理的,因为它没有足够的信息来确定如何这样做)。

至于@geoand 的评论询问“为什么不能在 Servlet 过滤器或 Spring 拦截器中添加内容类型的 http header ”,答案是“因为我很笨,忘记了 servlet 过滤器是如何工作的”。这是我最终用来解决问题的方法,我将立即添加它作为答案。

最佳答案

当我问这个问题时我有点傻,因为我正在寻找一种在 Spring 中直接操作传入请求或以其他方式明确告诉处理程序链我希望请求始终被视为 JSON 的方法。仔细考虑了一下,我意识到这正是 Servlet 过滤器的用途。

首先,我创建了一个新的 HttpServletRequestWrapper,如下所示:

public class ForcedContentTypeHttpServletRequestWrapper extends HttpServletRequestWrapper {

    private static final Logger log = Logger.getLogger( ForcedContentTypeHttpServletRequestWrapper.class );

    // this is the header to watch out for and what we should make sure it always resolves to.
    private static final String CONTENT_TYPE_HEADER = "content-type";
    private static final String CONTENT_TYPE = "application/json";


    public ForcedContentTypeHttpServletRequestWrapper( HttpServletRequest request ) {
        super( request );
    }

    /**
     * If content type is explicitly queried, return our hardcoded value
     */
    @Override
    public String getContentType() {
        log.debug( "Overriding request's content type of " + super.getContentType() );
        return CONTENT_TYPE;
    }

    /**
     * If we are being asked for the content-type header, always return JSON
     */
    @Override
    public String getHeader( String name ) {
        if ( StringUtils.equalsIgnoreCase( name, CONTENT_TYPE_HEADER ) ) {
            if ( super.getHeader( name ) == null ) {
                log.debug( "Content type was not originally included in request" );
            }
            else {
                log.debug( "Overriding original content type from request: " + super.getHeader( name ) );
            }
            log.debug( "Returning hard-coded content type of " + CONTENT_TYPE );
            return CONTENT_TYPE;
        }

        return super.getHeader( name );
    }

    /**
     * When asked for the names of headers in the request, make sure "content-type" is always
     * supplied.
     */
    @SuppressWarnings( { "unchecked", "rawtypes" } )
    @Override
    public Enumeration getHeaderNames() {

        ArrayList headerNames = Collections.list( super.getHeaderNames() );
        if ( headerNames.contains( CONTENT_TYPE_HEADER ) ) {
            log.debug( "content type already specified in request. Returning original request headers" );
            return super.getHeaderNames();
        }

        log.debug( "Request did not specify content type. Adding it to the list of headers" );
        headerNames.add( CONTENT_TYPE_HEADER );
        return Collections.enumeration( headerNames );
    }

    /**
     * If we are being asked for the content-type header, always return JSON
     */
    @SuppressWarnings( { "rawtypes", "unchecked" } )
    @Override
    public Enumeration getHeaders( String name ) {
        if ( StringUtils.equalsIgnoreCase( CONTENT_TYPE_HEADER, name ) ) {
            if ( super.getHeaders( name ) == null ) {
                log.debug( "Content type was not originally included in request" );
            }
            else {
                log.debug( "Overriding original content type from request: " + Collections.list( super.getHeaders( name ) ) );
            }
            log.debug( "Returning hard-coded content type of " + CONTENT_TYPE );
            return Collections.enumeration( Arrays.asList( CONTENT_TYPE ) );
        }

        return super.getHeaders( name );
    }

}

然后我将这个包装器用于过滤器中,如下所示:

public class ContentTypeFilter implements Filter {

    /**
     * @see Filter#destroy()
     */
    @Override
    public void destroy() {
        // do nothing
    }

    /**
     * @see Filter#doFilter(ServletRequest, ServletResponse, FilterChain)
     */
    @Override
    public void doFilter( ServletRequest request, ServletResponse response, FilterChain chain ) throws IOException, ServletException {
        ForcedContentTypeHttpServletRequestWrapper requestWrapper = new ForcedContentTypeHttpServletRequestWrapper( (HttpServletRequest) request );
        chain.doFilter( requestWrapper, response );
    }

    /**
     * @see Filter#init(FilterConfig)
     */
    @Override
    public void init( FilterConfig fConfig ) throws ServletException {
        // do nothing
    }

}

它并非完全无懈可击,但它可以正确处理来自该应用程序实际关心的同一个来源的请求。

关于java - Spring MVC : @RequestBody when no content-type is specified,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/23133429/

相关文章:

java - hibernate 异常 : Missing Column (column exists)

java - @Transactional 需要回滚所有内容,除了一个保存在数据库中

python - 将 JSON 对象发送到服务

javascript - React - 访问返回数据中的嵌套对象

java - java 8 中 stream().map() 和 stream.map({}) 的区别

JavaFX - 访问父 fx :id from child

c# - 无法使用 Json 序列化 Web API 中的响应

spring - @RequestPart 与混合多部分请求,Spring MVC 3.2

java - 我可以在@Value注释中传递变量来读取属性文件,其 key 存储在字符串变量中吗?

java - 一般来说,学习 Spring AOP 或 AOP 的最佳方法是什么?