Fork me on Gitee

SpringMVC的@PathVariable中的特殊符号

下载文件中遇到的扩展名,即:@PathVariable中的特殊符号

背景

同事在开发业务功能时,遇到一个pdf下载的需求:列表查询返回数据后,点击【生成PDF】按钮,调用后台生成PDF的接口,上传到阿里云的OSS后,返回PDF的URL链接。
用户根据自身需要,觉得是否下载PDF文件。在和前端联调时,前端要求是下载并另存为的交互方式,而非在浏览器中预览。

根据自己之前在上传图片的经(cai)验(keng),为了解决IE浏览器下前端会下载后台返回的JSON,就将返回的对象改为String字符串,不同浏览器可以自动判断返回数据的content-type,并正常执行。
代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
/**
* 图片上传,返回String,优化IE浏览器提示下载json
*/
@ApiOperation(value = "阿里云OSS上传")
@PostMapping(value = "/aliyunOssUpload")
public String aliyunOssUpload(@RequestParam("file") MultipartFile uploadFile) {
...
Map<String,String> map = new HashMap<String,String>();
map.put("fileName", fileName);
map.put("fileUrl", fileUrl);
return JSON.toJSONString(CommonResult.success(map));
}

类似的问题,原因并不是相同,但万物之间都存在相互的关系,自己也想了解到原因,所以也加入了他们的讨论。
前端的要求很明确,但仔细分析下来发现,他要求的打开PDF链接后,是下载的文件的方式,但PDF链接是阿里云OSS的地址,不收后端控制。再借鉴
自己之前写的TestLink在线Excel用例转换xml 下载xml的代码,马上将找到的原因。
至少,我们需要将返回的PDF链接调整为自己服务的地址,前端请求PDF链接时,后端用流将数据写到前端。
问题明确后,代码很快就出来。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
   @ApiOperation("生成pdf")
@PostMapping(path = "/createPdf")
public CommonResult createPdf(@RequestBody @Valid PdfReq req)
MsCommonResult result= commonService.createPdf(req);
return result;
}

@ApiOperation("下载pdf")
@Authority(isSkip = true)
@GetMapping(path = "/downloadPdf/{fileName}")
public void downloadPdf(@PathVariable String fileName) {
// 这里是坑,被截掉了.pdf
if(!fileName.contains(".")) {
fileName = fileName + FileUtils.PDF_SUFFIX;
}
try (InputStream inputStream = ossClient.download("oss-bucket-pdf", fileName);){
HttpServletResponse response = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getResponse();
...
}
}

代码逻辑没有问题,但在downloadPdf方法中遇到接收的PathVariable参数fileName的扩展名没有了,前端传递的是20190712001.pdf,接受到的fileName却是20190712001。
为了实现功能,只好判断是否包含“.”,然后根据需要,拼接扩展名。

强大的SpringMVC竟然有这个坑,一定是自己没有抓到原因,必须研究学习下具体原因和原理。

分析

根据SpringMVC处理流程图,可以大概定位是RequestMappingHandlerMapping中出现了Bug。
跟着SpringBoot项目启动时,自动配置的相关代码org.springframework.boot.autoconfigure.EnableAutoConfiguration
找到spring-boot-autoconfigure-version.jar中的META-INF/spring.factories文件。

其中配置了mvc相关的主要配置有以下两个:

1
2
3
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
org.springframework.boot.autoconfigure.web.servlet.DispatcherServletAutoConfiguration,\
org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguration,\

而RequestMappingHandlerMapping就是在这里创建的。

1
2
3
4
5
6
7
@Bean
@Primary
@Override
public RequestMappingHandlerMapping requestMappingHandlerMapping() {
// Must be @Primary for MvcUriComponentsBuilder to work
return super.requestMappingHandlerMapping();
}

在RequestMappingHandlerMapping的源码中,有一个明显的属性:useSuffixPatternMatch,这真是太吸引人了~
注释中说,如果设置为true,则映射到/users的方法可以匹配/users.*,我们离真相越来越近了。

1
2
3
4
5
6
7
8
9
10
/**
* Whether to use suffix pattern match (".*") when matching patterns to
* requests. If enabled a method mapped to "/users" also matches to "/users.*".
* <p>The default value is {@code true}.
* <p>Also see {@link #setUseRegisteredSuffixPatternMatch(boolean)} for
* more fine-grained control over specific suffixes to allow.
*/
public void setUseSuffixPatternMatch(boolean useSuffixPatternMatch) {
this.useSuffixPatternMatch = useSuffixPatternMatch;
}

debugger到创建bean后,查看bean的useSuffixPatternMatch的值为true,这也就是为什么接收带有“.”的PathVariable参数总出现问题了。

解决方案

使用Spel【墙裂推荐】

在@RequestMapping的value中使用SpEL来表示,value中的{version}换成{version:.+}或者{version:.*}

1
2
3
4
5
6
@GetMapping(path = "/downloadPdf/{fileName:.+}")
public void downloadPdf(@PathVariable String fileName) {

@GetMapping(path = "/downloadPdf/{fileName:.*}")
public void downloadPdf(@PathVariable String fileName) {
}

Spring web项目(未验证)

1
2
3
<bean class="org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping">
<property name="useSuffixPatternMatch" value="false" />
</bean>

SpringBoot项目

在WebMvcConfigurerAdapter的子类中添加对应的配置。

1
2
3
4
5
6
7
8
9
@Configuration
@Component
public class TecCloudWebMvcConfigurerAdapter extends WebMvcConfigurerAdapter {

@Override
public void configurePathMatch(PathMatchConfigurer configurer) {
configurer.setUseSuffixPatternMatch(true);
}
}

静态字符串

不以{PathVariable}结尾,或者不用@PathVariable,而是用@RequestParam

1
2
@GetMapping(path = "/downloadPdf/{fileName}/pdf")
public void downloadPdf(@PathVariable String fileName) {

PS

  • 在SpringBoot2.0及Spring 5.0 WebMvcConfigurerAdapter已被废弃,目前找到解决方案有

    1 直接实现WebMvcConfigurer (官方推荐)
    2 直接继承WebMvcConfigurationSupport

  • 在SpringBoot2.x 中,新增了org.springframework.boot.autoconfigure.web.servlet.WebMvcProperties.Pathmatch,其中useSuffixPattern默认值为false,也就是说升级框架时,可以顺带解决掉该问题哦。
感谢您发财的小手,我们一起进步
TEC-CHEN 微信支付

微信支付

TEC-CHEN 支付宝

支付宝

TEC-CHEN 微信-赞赏码

微信-赞赏码

TEC-CHEN 公众号

公众号


-------------本文结束感谢您的阅读-------------
0%