好得很程序员自学网

<tfoot draggable='sEl'></tfoot>

如何处理@PathVariable中的特殊字符问题

上代码:

?

1

2

3

4

5

@GetMapping (value= "/user/{useraccount}" )

public void getUserAccount( @PathVariable ( "useraccount" ) String userAccount) {

 

logger.info( "useraccount :" + userAccount);

}

正常访问:

/user/zhangsan

打印:useraccount : zhangsan

看似一切正常

but:

访问:/user/zhangsan/lisi

打印:useraccount : zhangsan

咦,为啥不是useraccount :zhangsan/lisi ?

@PathVariable并没有我们想象的聪明,对于参数中的/并不能跟实际路径/分开

事实上,有. ; -等都不能正确切分。

怎么办呢?

两种方案:

1,简单点,直接使用@RequestParam代替

?

1

2

3

4

5

@GetMapping (value= "/user" )

public void getUserAccount( @RequestParam ( "useraccount" ) String userAccount) {

 

logger.info( "useraccount :" + userAccount);

}

用/user?useraccount=zhangsan 访问

2,使用正则过滤

?

1

2

3

4

5

@GetMapping (value= "/user/{useraccount:[a-zA-Z0-9\\.\\-\\_\\;\\\]+}" )

public void getUserAccount( @PathVariable ( "useraccount" ) String userAccount) {

 

logger.info( "useraccount :" + userAccount);

}

正常访问:

/user/zhangsan

打印:useraccount : zhangsan

当然,这个就有点不灵活了,第一种简单又方便

补充:记一次@PathVariable特殊参数会丢失的排查问题

请求参数中如果包含.,会造成参数丢失,请看如下代码

以下代码,省略@RestController控制层类代码

?

1

2

3

4

5

6

@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测试数据,依然还是返回{"msg": "hello ddf"}

如果需要解决上面这个问题,则可以将代码更改如下(该解决方式从网上搜寻)

?

1

2

3

4

5

6

@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等特殊字符结尾,会影响实际返回数据

报错如下:

?

1

2

3

4

5

6

7

8

{

  "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控制层类代码

?

1

2

3

4

5

6

@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测试数据.sh,只要是以.sh结尾,这时候业务逻辑代码不会受到影响,但走到Spring自己的代码去处理返回数据的时候,有一个功能会根据扩展名来决定返回的类型,而以.sh结尾扩展名为sh,会被解析成对应的Content-Type: application/x-sh。

解决办法如下,第一种方法是从网上找到的,可以直接禁用该功能,但有可能会影响到静态资源的访问,不能确定,也没有进行尝试

?

1

2

3

4

5

6

7

8

@Configuration

public class Config extends WebMvcConfigurerAdapter {

  @Override

  public void configureContentNegotiation(

   ContentNegotiationConfigurer configurer) {

  configurer.favorPathExtension( false );

  }

}

然后以下就是闲着没事很想换个思路尝试去看看这到底是怎么回事,由于个人能力有限,不保证以下内容的重要性;

第二种方式解决思路是,既然扩展名以.sh等结尾会有问题,那么能不能不要让程序将扩展名识别为.sh,或者干脆就跳过处理,比如我是否可以加个.sh/这样就会影响到实际的扩展名,但是又不会影响到已有的代码,其实这里有个偷懒的写法,可以直接在@RequestMapping里的value最后直接加一个/,但是这要求客户端必须在原有的条件上最终拼一个/,否则会找不到对应的映射,直接404,我这里碰到这个问题的时候,因为该方法已经上线并且被其它几个系统调用,因此更改起来会有些繁琐,所以寻求了一种麻烦的方式,先将解决方式放在下面,不确定是否会影响其它问题

这种方式解决方式如下:注释中的两行代码二选一都可,推荐前面的写法,直接已经跳过

?

1

2

3

4

5

6

7

8

@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可以看到错误是在处理以下过程报错,首先如下

?

1

2

3

4

5

6

7

8

9

10

11

12

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()方法,该方法,部分代码如下

?

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

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就是这个,当然存在多个,则也就存在多个不是通配也满足条件的,所以再循环前也做了一次排序,保证优先级最高的一定会生效。

?

1

2

3

4

5

6

7

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

?

1

2

3

4

5

6

7

8

9

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的列表,通过代码可以看到它的值为*/*,该方法往下调用部分代码如下:

?

1

2

3

4

5

6

7

8

9

10

11

12

13

14

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();

  }

}

调用如下:

?

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

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不一致是否会有别的问题

?

1

request.setAttribute(WebUtils.INCLUDE_REQUEST_URI_ATTRIBUTE, "/user/" + uid + "/" );

以上为个人经验,希望能给大家一个参考,也希望大家多多支持。如有错误或未考虑完全的地方,望不吝赐教。

原文链接:https://blog.csdn.net/xiaoxiaojavacsdn/article/details/101059339

查看更多关于如何处理@PathVariable中的特殊字符问题的详细内容...

  阅读:23次