2023年11月25日发(作者:)

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

上代码:

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

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

("useraccount " + userAccount);

}

正常访问:

/user/zhangsan

打印:useraccount zhangsan

看似⼀切正常

but:

访问:/user/zhangsan/lisi

打印:useraccount zhangsan

咦,为啥不是useraccount zhangsan/lisi

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

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

怎么办呢?

两种⽅案:

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

@GetMapping(value="/user")

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

("useraccount " + userAccount);

}

/user?useraccount=zhangsan 访问

2,使⽤正则过滤

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

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

("useraccount " + userAccount);

}

正常访问:

/user/zhangsan

打印:useraccount zhangsan

当然,这个就有点不灵活了,第⼀种简单⼜⽅便

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

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

@RequestMapping(value = "hello/{name:.*}")

public Map sayHello(@PathVariable("name") String name, HttpServletRequest request) {

Map rtnMap = new HashMap<>();

("msg", "hello " + name);

return rtnMap;

}

如果使⽤@PathVariable.sh.bat等特殊字符结尾,会影响实际返回数据

报错如下:

{

"timestamp": 19,

"status": 406,

"error": "Not Acceptable",

"exception": "diaTypeNotAcceptableException",

"message": "Could not find acceptable representation",

"path": "/HDOrg/user/hello/"

}

还是上⾯的代码

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

@RequestMapping(value = "hello/{name:.*}")

public Map sayHello(@PathVariable("name") String name, HttpServletRequest request) {

Map rtnMap = new HashMap<>();

("msg", "hello " + name);

return rtnMap;

}

如果这时候请求地址为hello/hello/,只要是以.sh结尾,这时候业务逻辑代码不会受到影响,但⾛到Spring⾃⼰的代码去处理

返回数据的时候,有⼀个功能会根据扩展名来决定返回的类型,⽽以.sh结尾扩展名为sh,会被解析成对应的Content-Type: application/x-

sh

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

@Configuration

public class Config extends WebMvcConfigurerAdapter {

@Override

public void configureContentNegotiation(

ContentNegotiationConfigurer configurer) {

athExtension(false);

}

}

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

第⼆种⽅式解决思路是,既然扩展名以.sh等结尾会有问题,那么能不能不要让程序将扩展名识别为.sh,或者⼲脆就跳过处理,⽐如我是否可

以加个.sh/这样就会影响到实际的扩展名,但是⼜不会影响到已有的代码,其实这⾥有个偷懒的写法,可以直接在@RequestMapping⾥的

value最后直接加⼀个/,但是这要求客户端必须在原有的条件上最终拼⼀个/,否则会找不到对应的映射,直接404,我这⾥碰到这个问题的时

候,因为该⽅法已经上线并且被其它⼏个系统调⽤,因此更改起来会有些繁琐,所以寻求了⼀种⿇烦的⽅式,先将解决⽅式放在下⾯,不确

定是否会影响其它问题

这种⽅式解决⽅式如下:注释中的两⾏代码⼆选⼀都可,推荐前⾯的写法,直接已经跳过

@RequestMapping(value = "hello/{name:.*}")

public String sayHello(@PathVariable("name") String name) {

// 该⽅法跳过通过上⾯描述的那种⽅式来确定MediaType

ribute(e() + ".SKIP", true);

}

}

出现这个问题,⼀般的查找思路就是是否是请求或响应的Content-Type是否出现了问题,那么在上⾯这个⽅法上⽆论是inputMessage还是

outputMessage都是正常的,重点来看⼀下writeWithMessageConverters()⽅法,该⽅法,部分代码如下

public abstract class AbstractMessageConverterMethodProcessor extends AbstractMessageConverterMethodArgumentResolver

implements HandlerMethodReturnValueHandler {

@SuppressWarnings("unchecked")

protected 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 = ng();

valueType = ;

declaredType = ;

}

else {

outputValue = value;

valueType = getReturnValueType(outputValue, returnType);

declaredType = getGenericType(returnType);

}

HttpServletRequest request = vletRequest();

List requestedMediaTypes = getAcceptableMediaTypes(request);

List producibleMediaTypes = getProducibleMediaTypes(request, valueType, declaredType);

// 后⾯处理MediaType的部分在这⾥全部省略

}

/**

* Returns the media types that can be produced:

*

    *

  • The producible media types specified in the request mappings, or

    *

  • Media types of configured converters that can write the specific return value, or

    *

  • {@link MediaType#ALL}

    *

* @since 4.2

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 = "*/*";

// 静态初始化的值省略

}

该⽅法的结果可以看到如果调⽤的⽅法返回了⼀个空的列表,则该⽅法返回的列表,通过代码可以看到它的值为*/*,该⽅法

往下调⽤部分代码如下:

public class ContentNegotiationManager implements ContentNegotiationStrategy, MediaTypeFileExtensionResolver {

@Override

public List resolveMediaTypes(NativeWebRequest request) throws HttpMediaTypeNotAcceptableException {

for (ContentNegotiationStrategy strategy : gies) {

List mediaTypes = eMediaTypes(request);

if (y() || (MEDIA_TYPE_ALL)) {

continue;

}

return mediaTypes;

}

return ist();

}

}

调⽤如下:

public class WebMvcAutoConfiguration {

@Override

public List resolveMediaTypes(NativeWebRequest webRequest)

throws HttpMediaTypeNotAcceptableException {

private static final String SKIP_ATTRIBUTE =

.getName() + ".SKIP";

Object skip = ribute(SKIP_ATTRIBUTE,

_REQUEST);

if (skip != null && oolean(ng())) {

return ist();

}

return eMediaTypes(webRequest);

}

}

在这⾥可以看到有⼀个属性为skip,如果它的属性为PathExtensionContentNegotiationStrategy的类全名+".SKP"并且它的值为true,那么这

⾥则不继续往下处理直接返回空的集合,⽽在前⾯也已经看到如果返回的空的集合,实际上最终返回给调⽤⽅的是*/*,结合前⾯看到的

ctMessageConverterMethodProcessor#writeWithMessageConverters(T,

Parameter, tServerHttpRequest,

tServerHttpResponse)

这个⽅法,*/*是可以匹配任何⽣成的producibleMediaTypes,所以最终结果能够按照原先应该返回的类型正确返回,⽽不会被.sh等后缀影响