- 请求参数中如果包含
.
,会造成参数丢失,请看如下代码
以下代码,省略@RestController
控制层类代码
@RequestMapping(value = "hello/{name}")
public Map<String, Object> sayHello(@PathVariable("name") String name, HttpServletRequest request) {
Map<String, Object> rtnMap = new HashMap<>();
rtnMap.put("msg", "hello " + name);
return rtnMap;
}
请求地址: hello/ddf
,则正常返回{"msg": "hello ddf"}
请求地址: hello/ddf.com
,依然还是返回{"msg": "hello ddf"}
如果需要解决上面这个问题,则可以将代码更改如下(该解决方式从网上搜寻)
@RequestMapping(value = "hello/{name:.*}")
public Map<String, Object> sayHello(@PathVariable("name") String name, HttpServletRequest request) {
Map<String, Object> rtnMap = new HashMap<>();
rtnMap.put("msg", "hello " + name);
return rtnMap;
}
- 如果使用
@PathVariable
以.sh
或.bat
等特殊字符结尾,会影响实际返回数据
报错如下:
{
"timestamp": 1541405292119,
"status": 406,
"error": "Not Acceptable",
"exception": "org.springframework.web.HttpMediaTypeNotAcceptableException",
"message": "Could not find acceptable representation",
"path": "/HDOrg/user/hello/ddf.sh"
}
还是上面的代码
以下代码,省略@RestController
控制层类代码
@RequestMapping(value = "hello/{name:.*}")
public Map<String, Object> sayHello(@PathVariable("name") String name, HttpServletRequest request) {
Map<String, Object> rtnMap = new HashMap<>();
rtnMap.put("msg", "hello " + name);
return rtnMap;
}
如果这时候请求地址为hello/ddf.sh
或hello/ddf.com.sh
,只要是以.sh
结尾,这时候业务逻辑代码不会受到影响,但走到Spring
自己的代码去处理返回数据的时候,有一个功能会根据扩展名来决定返回的类型,而以.sh
结尾扩展名为sh
,会被解析成对应的Content-Type: application/x-sh
。
解决办法如下,第一种方法是从网上找到的,可以直接禁用该功能,但有可能会影响到静态资源的访问,不能确定,也没有进行尝试
@Configuration
public class Config extends WebMvcConfigurerAdapter {
@Override
public void configureContentNegotiation(
ContentNegotiationConfigurer configurer) {
configurer.favorPathExtension(false);
}
}
然后以下就是闲着没事很想换个思路尝试去看看这到底是怎么回事,由于个人能力有限,不保证以下内容的重要性;
第二种方式解决思路是,既然扩展名以.sh
等结尾会有问题,那么能不能不要让程序将扩展名识别为.sh
,或者干脆就跳过处理,比如我是否可以加个.sh/
这样就会影响到实际的扩展名,但是又不会影响到已有的代码,其实这里有个偷懒的写法,可以直接在@RequestMapping
里的value
最后直接加一个/
,但是这要求客户端必须在原有的条件上最终拼一个/
,否则会找不到对应的映射,直接404
,我这里碰到这个问题的时候,因为该方法已经上线并且被其它几个系统调用,因此更改起来会有些繁琐,所以寻求了一种麻烦的方式,先将解决方式放在下面,不确定是否会影响其它问题
这种方式解决方式如下:注释中的两行代码二选一都可,推荐前面的写法,直接已经跳过
@RequestMapping(value = "hello/{name:.*}")
public String sayHello(@PathVariable("name") String name) {
// 该方法跳过通过上面描述的那种方式来确定MediaType
request.setAttribute(PathExtensionContentNegotiationStrategy.class.getName() + ".SKIP", true);
// 后面参数的值前半部分必须和该方法的RequestMapping一致,否则无效,不包括ContextPath
request.setAttribute(WebUtils.INCLUDE_REQUEST_URI_ATTRIBUTE, "/hello/" + name + "/");
return "hello " + name;
}
下面依赖源码来看一下为什么可以这么去做,先看一下为什么会造成这个结果?以下步骤只关心与当前问题有关的部分,并只大概关注其中问题,不作细节的深入
经过debug可以看到错误是在处理以下过程报错,首先如下
public class RequestResponseBodyMethodProcessor extends AbstractMessageConverterMethodProcessor {
@Override
public void handleReturnValue(Object returnValue, MethodParameter returnType,
ModelAndViewContainer mavContainer, NativeWebRequest webRequest)
throws IOException, HttpMediaTypeNotAcceptableException, HttpMessageNotWritableException {
mavContainer.setRequestHandled(true);
ServletServerHttpRequest inputMessage = createInputMessage(webRequest);
ServletServerHttpResponse outputMessage = createOutputMessage(webRequest);
// Try even with null return value. ResponseBodyAdvice could get involved.
writeWithMessageConverters(returnValue, returnType, inputMessage, outputMessage);
}
}
出现这个问题,一般的查找思路就是是否是请求或响应的Content-Type
是否出现了问题,那么在上面这个方法上无论是inputMessage
还是outputMessage
都是正常的,重点来看一下writeWithMessageConverters()
方法,该方法,部分代码如下
public abstract class AbstractMessageConverterMethodProcessor extends AbstractMessageConverterMethodArgumentResolver
implements HandlerMethodReturnValueHandler {
@SuppressWarnings("unchecked")
protected <T> void writeWithMessageConverters(T value, MethodParameter returnType,
ServletServerHttpRequest inputMessage, ServletServerHttpResponse outputMessage)
throws IOException, HttpMediaTypeNotAcceptableException, HttpMessageNotWritableException {
Object outputValue;
Class<?> valueType;
Type declaredType;
if (value instanceof CharSequence) {
outputValue = value.toString();
valueType = String.class;
declaredType = String.class;
}
else {
outputValue = value;
valueType = getReturnValueType(outputValue, returnType);
declaredType = getGenericType(returnType);
}
HttpServletRequest request = inputMessage.getServletRequest();
List<MediaType> requestedMediaTypes = getAcceptableMediaTypes(request);
List<MediaType> producibleMediaTypes = getProducibleMediaTypes(request, valueType, declaredType);
// 后面处理MediaType的部分在这里全部省略
}
/**
* Returns the media types that can be produced:
* <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
*/
protected List<MediaType> getProducibleMediaTypes(HttpServletRequest request, Class<?> valueClass, Type declaredType) {
Set<MediaType> mediaTypes = (Set<MediaType>) request.getAttribute(HandlerMapping.PRODUCIBLE_MEDIA_TYPES_ATTRIBUTE);
if (!CollectionUtils.isEmpty(mediaTypes)) {
return new ArrayList<MediaType>(mediaTypes);
}
else if (!this.allSupportedMediaTypes.isEmpty()) {
List<MediaType> result = new ArrayList<MediaType>();
for (HttpMessageConverter<?> converter : this.messageConverters) {
if (converter instanceof GenericHttpMessageConverter && declaredType != null) {
if (((GenericHttpMessageConverter<?>) converter).canWrite(declaredType, valueClass, null)) {
result.addAll(converter.getSupportedMediaTypes());
}
}
else if (converter.canWrite(valueClass, null)) {
result.addAll(converter.getSupportedMediaTypes());
}
}
return result;
}
else {
return Collections.singletonList(MediaType.ALL);
}
}
}
先看方法getAcceptableMediaTypes()
,是根据请求来决定当前的HttpServletRequest
到底是要请求什么类型的数据,该方法调用链在后面说明;
getProducibleMediaTypes()
方法返回可以生成的MediaType
,能够生成哪些是看当前项目一共有多少可以被支持的MediaType
,当然也能看到也可以通过HttpServletRequest
明确设置属性HandlerMapping.PRODUCIBLE_MEDIA_TYPES_ATTRIBUTE
来确定用哪种方式;
拿到这两个列表后,需要判断requestedMediaTypes
是否兼容producibleMediaTypes
,如*/*
则可以兼容所有的可以生成的MediaType
,最终将兼容的requestedMediaTypes
循环处理,看是否是一个具体的MediaType
而不是通配符,那么最终生效的MediaType
就是这个,当然存在多个,则也就存在多个不是通配也满足条件的,所以再循环前也做了一次排序,保证优先级最高的一定会生效。
public abstract class AbstractMessageConverterMethodProcessor extends AbstractMessageConverterMethodArgumentResolver
implements HandlerMethodReturnValueHandler {
private List<MediaType> getAcceptableMediaTypes(HttpServletRequest request) throws HttpMediaTypeNotAcceptableException {
List<MediaType> mediaTypes = this.contentNegotiationManager.resolveMediaTypes(new ServletWebRequest(request));
return (mediaTypes.isEmpty() ? Collections.singletonList(MediaType.ALL) : mediaTypes);
}
}
MediaType.java
public class MediaType extends MimeType implements Serializable {
public static final MediaType ALL;
/**
* A String equivalent of {@link MediaType#ALL}.
*/
public static final String ALL_VALUE = "*/*";
// 静态初始化MediaType.ALL的值省略
}
该方法的结果可以看到如果调用的方法返回了一个空的列表,则该方法返回MediaType.ALL
的列表,通过代码可以看到它的值为*/*
,该方法往下调用部分代码如下:
public class ContentNegotiationManager implements ContentNegotiationStrategy, MediaTypeFileExtensionResolver {
@Override
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();
}
}
调用如下:
public class WebMvcAutoConfiguration {
@Override
public List<MediaType> resolveMediaTypes(NativeWebRequest webRequest)
throws HttpMediaTypeNotAcceptableException {
private static final String SKIP_ATTRIBUTE = PathExtensionContentNegotiationStrategy.class
.getName() + ".SKIP";
Object skip = webRequest.getAttribute(SKIP_ATTRIBUTE,
RequestAttributes.SCOPE_REQUEST);
if (skip != null && Boolean.parseBoolean(skip.toString())) {
return Collections.emptyList();
}
return this.delegate.resolveMediaTypes(webRequest);
}
}
在这里可以看到有一个属性为skip
,如果它的属性为PathExtensionContentNegotiationStrategy
的类全名+".SKP"
并且它的值为true
,那么这里则不继续往下处理直接返回空的集合,而在前面也已经看到如果返回的空的集合,实际上最终返回给调用方的是*/*
,结合前面看到的org.springframework.web.servlet.mvc.method.annotation.AbstractMessageConverterMethodProcessor#writeWithMessageConverters(T, org.springframework.core.MethodParameter, org.springframework.http.server.ServletServerHttpRequest, org.springframework.http.server.ServletServerHttpResponse)
这个方法,*/*
是可以匹配任何生成的producibleMediaTypes
,所以最终结果能够按照原先应该返回的类型正确返回,而不会被.sh
等后缀影响到;
其实最初没有看到skip
的时候,看到了一些后面的代码,最终也解决了这个问题,不论正确与否,先把整个过程记录下来,假如在上面的步骤中没有设置skip=true
,那么程序继续下去的部分走向如下
// 如果uid以.sh结尾的话,在逻辑处理完成之后框架处理return数据的时候,会根据扩展名来决定返回的content-type,sh结尾
// 会影响返回的content-type为application/x-sh,这会影响该方法的实际功能,解决办法是:
// 要么禁用该功能,要么修改该方法的@RequestMapping,禁用不能确定是否会对直接访问的静态资源有影响,
// 而且该方法调用方项目已上线,不宜轻易修改,只能这里改变这个属性的地址,影响框架
// 后面获取请求的后缀为null,而避免这个问题,但尚不能确认requestUrl和mappingUrl不一致是否会有别的问题
// request.setAttribute(WebUtils.INCLUDE_REQUEST_URI_ATTRIBUTE, "/user/" + uid + "/");