1.问题现象
想必经常在使用springmvc过程中,在接口路径中添加一些参数
但是当参数值包含了文件的后缀名时,例如xxx.mp4,xxx.jpg等,在springboot2.0版本中使用正常,但是在springboot1.5中则会抛出 Could not find acceptable representation 异常,没有找到正确的响应类型.
2.异常原因分析
无奈只好一步步断点进入查找原因,最终发现异常原因是springmvc会自动根据URL最后的一段参数解析响应媒体类型,解析出的响应类型与实际的响应类型不匹配,最后没有适合的MediaType响应类型则抛出异常,先给大家查下结果
最后解析为video/mp4类型
部分源码分析如下:
public abstract class AbstractMessageConverterMethodProcessor extends AbstractMessageConverterMethodArgumentResolver
implements HandlerMethodReturnValueHandler {
protected <T> void writeWithMessageConverters(T value, MethodParameter returnType,
ServletServerHttpRequest inputMessage, ServletServerHttpResponse outputMessage)
throws IOException, HttpMediaTypeNotAcceptableException, HttpMessageNotWritableException {
HttpServletRequest request = inputMessage.getServletRequest();
//获取适合的响应MediaType
List<MediaType> requestedMediaTypes = getAcceptableMediaTypes(request);
List<MediaType> producibleMediaTypes = getProducibleMediaTypes(request, valueType, declaredType);
if (outputValue != null && producibleMediaTypes.isEmpty()) {
throw new IllegalArgumentException("No converter found for return value of type: " + valueType);
}
Set<MediaType> compatibleMediaTypes = new LinkedHashSet<MediaType>();
for (MediaType requestedType : requestedMediaTypes) {
for (MediaType producibleType : producibleMediaTypes) {
//MediaType进行匹配
if (requestedType.isCompatibleWith(producibleType)) {
compatibleMediaTypes.add(getMostSpecificMediaType(requestedType, producibleType));
}
}
}
//此处没有匹配到合适的MediaType则抛出异常
if (compatibleMediaTypes.isEmpty()) {
if (outputValue != null) {
throw new HttpMediaTypeNotAcceptableException(producibleMediaTypes);
}
return;
}
}
}
关键方法是获取适合的MediaType,getAcceptableMediaTypes,源码比较简单,关键代码如下:
public List<MediaType> resolveMediaTypes(NativeWebRequest request) throws HttpMediaTypeNotAcceptableException {
for (ContentNegotiationStrategy strategy : this.strategies) {
List<MediaType> mediaTypes = strategy.resolveMediaTypes(request);
if (mediaTypes.isEmpty() || mediaTypes.equals(MEDIA_TYPE_ALL)) {
continue;
}
return mediaTypes;
}
return Collections.emptyList();
}
strategies 是解析策略,项目启动时会初始化,而解析URL后缀名来获取mediaType的策略是 ServletPathExtensionContentNegotiationStrategy类,而这个是啥时候初始化进去的呢,分析发现是在创建ContentNegotiationManagerFactoryBean类时初始化的.
ContentNegotiationManagerFactoryBean类继承了FactoryBean并实现来 InitializingBean,获取bean对象时完成初始化,关键源码如下:
@Override
public void afterPropertiesSet() {
List<ContentNegotiationStrategy> strategies = new ArrayList<ContentNegotiationStrategy>();
//favorPathExtension为true则添加
if (this.favorPathExtension) {
PathExtensionContentNegotiationStrategy strategy;
if (this.servletContext != null && !isUseJafTurnedOff()) {
//添加解析URI后缀名策略
strategy = new ServletPathExtensionContentNegotiationStrategy(
this.servletContext, this.mediaTypes);
}
else {
strategy = new PathExtensionContentNegotiationStrategy(this.mediaTypes);
}
strategy.setIgnoreUnknownExtensions(this.ignoreUnknownPathExtensions);
if (this.useJaf != null) {
strategy.setUseJaf(this.useJaf);
}
strategies.add(strategy);
}
this.contentNegotiationManager = new ContentNegotiationManager(strategies);
}
favorPathExtension默认值为true
springboot1.5默认添加了ServletPathExtensionContentNegotiationStrategy 进行解析.而在springboot2.0中此值被org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguration.WebMvcAutoConfigurationAdapter#configureContentNegotiation(springboot1.5和springboot2,0实现不一样,这里就不再深入研究了) 设置为了false
解析到这里,异常原因大概已经清楚了,这里只对比了springboot1.5.4和springboot2.2.2版本.
3.解决方法
3.1 方式一
从上面分析中发现,URI解析的方法是 UriUtils.extractFileExtension(path),故可以在URI最后中多添加 '/' ,则可以不让这方法获取到后缀名.例如 http://127.0.0.1:8080/videoMerge/xxxxxx.mp4/
这种方式虽然可以解决,但是请求路径中多添加 / 感觉还是特别别扭.
3.2 方式二
从上面分析可以发现,Springboot2.0将favorPathExtension设置为了false不添加ServletPathExtensionContentNegotiationStrategy 策略,故我们也可以添加全局配置,不根据URI后缀名解析响应MediaType
@Configuration
public class ContentNegotiationPathConfig extends WebMvcConfigurerAdapter {
@Override
public void configureContentNegotiation(ContentNegotiationConfigurer configurer) {
//不解析接口URL后缀名来解析mediaTye
configurer.favorPathExtension(false);
}
}