为什么Spring会覆盖头部中的Content-Type?

3

当我在Controller中设置http头时,如下所示:

@Controller
@Slf4j
public class PlayerController {    
    @ModelAttribute
    public void setVaryResponseHeader(HttpServletResponse response) {
        response.setHeader("Content-Type", "application/vnd.apple.mpegurl");
    }

    @ResponseBody
    @RequestMapping(value = "/live/timeshift.m3u8", method = RequestMethod.GET)
    public String playbackLive(@RequestParam(value = "delay") Integer delay) {
        ....
    }
}

后来Spring将它覆盖为纯文本,调用栈如下:

writeWithMessageConverters:184, AbstractMessageConverterMethodProcessor (org.springframework.web.servlet.mvc.method.annotation)
handleReturnValue:174, RequestResponseBodyMethodProcessor (org.springframework.web.servlet.mvc.method.annotation)
handleReturnValue:81, HandlerMethodReturnValueHandlerComposite (org.springframework.web.method.support)
......
run:624, ThreadPoolExecutor$Worker (java.util.concurrent)
run:61, TaskThread$WrappingRunnable (org.apache.tomcat.util.threads)
run:748, Thread (java.lang)

为什么Spring会这样做?很难理解,我认为用户定制逻辑应该具有更高的优先级,Spring不应该自动做决定。
实际上,我认为问题已经有足够的解释了,但是SO认为代码占大部分,要求添加更多细节,我能怎么做呢,提前感谢。

请添加控制器和调用 setVaryResponseHeader 方法。 - Stav Shamir
@StavShamir 你好,它被注释为ModelAttribute,并由SpringFramework调用。 - http8086
2个回答

4

简而言之

为什么和如何在Spring中覆盖Header中的ContentType?

Spring有一个默认的配置,基于三种策略来选择返回的内容类型。这个配置可以被修改。

自定义所有控制器响应的content-type header

有两种方式可以自定义所有响应的content-type header协商配置,一种是通过XML配置,另一种是通过注解驱动配置。

自定义某些URL的content-type header值

在相同的配置中,有一种方法可以注入一个定制的策略来选择哪些URL受到更改content-type header的规则影响。

在Spring Boot上

幸运的是,在Spring Boot上添加produce属性到控制器的@RequestMapping注解中并更改属性即可获得所需的行为:

  • @RequestMapping(value = "/live/timeshift.m3u8", method = RequestMethod.GET, produces = "application/vnd.apple.mpegurl")
  • Property - > spring.http.encoding.enabled设置为true

详细回答

为什么和如何在Spring中覆盖Header中的ContentType?

在Spring MVC中,有三个选项来确定请求的媒体类型:

  1. 请求中的URL后缀(扩展名)(如.xml/.json)
  2. 请求中的URL参数(如?format=json)
  3. 您控制器方法所做的接受头部

按照这个顺序,Spring协商content-type header响应和body响应格式,如果没有启用任何一个,则可以指定回退到默认内容类型。

自定义所有控制器响应的content-type header

因此,要自定义此行为,我们应提供回退的默认内容类型并禁用上述三种策略。有两种方法可以实现它,使用XML配置或注解配置:

@Configuration
@EnableWebMvc
public class WebConfig extends WebMvcConfigurerAdapter {

  @Override
  public void configureContentNegotiation(final ContentNegotiationConfigurer configurer) {
    configurer.favorPathExtension(false).
    favorParameter(false).
    ignoreAcceptHeader(true).
    useJaf(false).
    defaultContentType("application/vnd.apple.mpegurl");
  }
}

或者XML配置
<bean id="contentNegotiationManager"
  class="org.springframework.web.accept.ContentNegotiationManagerFactoryBean">
    <property name="favorPathExtension" value="false" />
    <property name="favorParameter" value="false"/>
    <property name="ignoreAcceptHeader" value="true" />
    <property name="defaultContentType" value="application/vnd.apple.mpegurl"/>
    <property name="useJaf" value="false" />
</bean>

自定义某些URL的内容类型标头值

另一个定制化的解决方案是创建自己的@DefaultContentType注解。覆盖RequestMappingHandlerMapping#getCustomMethodCondition方法,该方法检查方法上是否有注解@DefaultContentType。自定义条件将始终匹配,但在compareTo中,它将优先考虑具有注解的方法而非没有注解的方法。

如果需要多次使用,则可以执行上述解决方案。

对于一次性事件,可以通过ContentNegotiationConfigurer插入自定义defaultContentTypeStrategy,该策略检查特定URL并返回首选媒体类型,例如:

public class MyCustomContentNegotiationStrategy implements ContentNegotiationStrategy {

  @Override
  public List<MediaType> resolveMediaTypes (final NativeWebRequest nativeWebRequest)
            throws HttpMediaTypeNotAcceptableException {
      final List<MediaType> mediaTypes = new ArrayList<>();
      final String url =((ServletWebRequest)request).getRequest().getRequestURI().toString();
      final String yourUrlpatternString = ".*http://.*";

      final Pattern yourUrlPattern = Pattern.compile(patternString);
      final Matcher matcher = pattern.matcher(url);

     if(matcher.matches()) {
          mediaTypes.add("application/vnd.apple.mpegurl");
      return mediaTypes;
  }
}

然后,通过配置添加您的自定义策略:

@EnableWebMvc
@Configuration
public class MyWebConfig extends WebMvcConfigurerAdapter {

  @Override
  public void configureContentNegotiation (ContentNegotiationConfigurer configurer) {
      configurer.defaultContentTypeStrategy(new MyCustomContentNegotiationStrategy());
  }
}

关于Spring Boot

最后,如果你正在使用Spring Boot,就像@StavShamir建议的那样,在回答https://dev59.com/Qbvoa4cB1Zd3GeqP76mm#62422889中,有一堆常见的应用程序属性可能对这种情况有帮助:

# HTTP encoding (HttpEncodingProperties)
# Charset of HTTP requests and responses. Added to the "Content-Type" header if not set explicitly.
spring.http.encoding.charset=UTF-8 
# Whether to enable http encoding support.
spring.http.encoding.enabled=true 
# Whether to force the encoding to the configured charset on HTTP requests and responses.
spring.http.encoding.force= 
# Whether to force the encoding to the configured charset on HTTP requests. Defaults to true when "force" has not been 
spring.http.encoding.force-request= specified.
# Whether to force the encoding to the configured charset on HTTP responses.
spring.http.encoding.force-response= 
# Locale in which to encode mapping.
spring.http.encoding.mapping= 

在这种情况下,将 spring.http.encoding.enabled 属性设置为true并使用@ RequestMapping注释中的参数将起作用: https://docs.spring.io/spring-boot/docs/current-SNAPSHOT/reference/htmlsingle/#common-application-properties
@RequestMapping(value = "/live/timeshift.m3u8", method = RequestMethod.GET, produces = "application/vnd.apple.mpegurl")

@workplaylifecycle,这个清晰明了吗?你完成任务了吗? - Damian229

3
这是一个好问题,但对于您来说最简单的解决方案可能是使用映射注释的produces参数:
@ResponseBody
@RequestMapping(value = "/live/timeshift.m3u8", method = RequestMethod.GET, produces = "application/vnd.apple.mpegurl")
public String playbackLive(@RequestParam(value = "delay") Integer delay) {
    ....
}

除非您需要动态分配响应的内容类型,否则我认为这种方式是最简单的。

太棒了...一切都正常,除了最终输出是 Content-Type: application/vnd.apple.mpegurl;charset=UTF-8,有没有办法只输出 Content-Type: application/vnd.apple.mpegurl - http8086
@workplaylifecycle 请看这个问题 - https://dev59.com/qVwY5IYBdhLWcg3weXrV。您需要在application.properties中设置 spring.http.encoding.force=true - Stav Shamir

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接