前言
@PathVariable 注解是我们进行JavaEE开发,最常见的几个注解之一,这篇博文我们以案例和源码相结合,帮助大家更好的了解@PathVariable 注解
使用案例
1.获取 URL 上的值
@RequestMapping("/id/{id}")
public Object getId(@PathVariable(value = "id") Integer id) {
return id;
}
2.获取 URL 上的多个值
多个值以 / 分割,可以相邻也可以不相邻
2.1 多个值相邻
@GetMapping("/info/{id}/{name}")
public Object getInfo(@PathVariable(value = "id") Integer id, @PathVariable(value = "name") String name) {
return "id:" + id + ",name:" + name;
}
2.1 多个值不相邻
@GetMapping("/info/{id}/delimiter/{name}")
public Object getDelimiterInfo(@PathVariable(value = "id") Integer id, @PathVariable(value = "name") String name) {
return "id:" + id + ",name:" + name;
}
3. 不指定value(name)
@RequestMapping("/default/{id}")
public Object getDefaultId(@PathVariable Integer id) {
return id;
}
4. 使用 Map 接受多个 URL 上的值
@GetMapping("/info_map/{id}/{name}")
public Object getInfoToMap(@PathVariable Map<String, Object> map) {
StringBuilder sb = new StringBuilder();
map.forEach((key, value) -> sb.append(key).append(":").append(value).append(","));
return sb.substring(0, sb.length() - 1);
}
源码解析
InvocableHandlerMethod#getMethodArgumentValues
参数的处理分为两个阶段:
- 判断当前环境中存在的resolvers,是否支持解析当前参数
- 处理参数
判断是否支持解析当前参数
我的环境中存在27个resolvers,通过命名我们大概可以猜测出 PathVariableMethodArgumentResolver、PathVariableMapMethodArgumentResolver 是处理 @PathVariable 注解的 resolver
PathVariableMethodArgumentResolver#supportsParameter
@Override
public boolean supportsParameter(MethodParameter parameter) {
if (!parameter.hasParameterAnnotation(PathVariable.class)) {
return false;
}
if (Map.class.isAssignableFrom(parameter.nestedIfOptional().getNestedParameterType())) {
PathVariable pathVariable = parameter.getParameterAnnotation(PathVariable.class);
return (pathVariable != null && StringUtils.hasText(pathVariable.value()));
}
return true;
}
如果参数存在 @PathVariable 注解,并且指定了 value(name),则支持解析
PathVariableMapMethodArgumentResolver#supportsParameter
@Override
public boolean supportsParameter(MethodParameter parameter) {
PathVariable ann = parameter.getParameterAnnotation(PathVariable.class);
return (ann != null && Map.class.isAssignableFrom(parameter.getParameterType()) &&
!StringUtils.hasText(ann.value()));
}
如果参数存在 @PathVariable 注解,并且未指定了 value(name),则支持解析
处理参数
接来下我们将重点分析 PathVariableMethodArgumentResolver 的 resolveArgument 方法,PathVariableMapMethodArgumentResolver 的 resolveArgument 方法大家可以自行阅读,相关源码如下:
大概分为以下五个步骤:
- 构建NamedValueInfo对象
- 处理Spel表达式
- 解析参数
- 处理默认值
- 类型转换
- 给 Request 域赋值
构建NamedValueInfo对象
创建NamedValueInfo对象
@PathVariable 注解的默认值是 ValueConstants.DEFAULT_NONE,并且无法手动设置
更新NamedValueInfo对象
updateNamedValueInfo 方法主要针对 @PathVariable 注解未指定 value(name)的情况,比如上文中的案例3,NamedValueInfo对象的 name 属性值为方法的参数名
处理Spel表达式
默认情况下,@PathVariable 注解是不支持解析 Spel 表达式的,我们通过源码分析一下。
如果 resolver 的 configurableBeanFactory 或 expressionContext 属性为 null ,则不进行Spel表达式的解析工作
RequestMappingHandlerAdapter#getDefaultArgumentResolvers
默认注册 PathVariableMethodArgumentResolver 使用的是无参构造方法,也就是 configurableBeanFactory 和 expressionContext 属性为 null,所以默认情况下,@PathVariable 注解不支持解析Spel表达式。可能有的小伙伴说我可以利用 WebMvcConfigurer ,自定义一个resolver,这里存在一个优先级问题,自定义的 resolver 优先级低于 mvc 手动注册的 resolver,所以一般情况下 @PathVariable 注解都不支持解析Spel表达式
我们可以看到手动注册的 resolver 优先级很低,一般情况下都是利用 mvc 内置的 resolver 进行解析
如何让 @PathVariable 注解支持解析 Spel 表达式 ?
替换内置的 PathVariableMapMethodArgumentResolver
@SpringBootApplication
@PropertySource("classpath:keys.properties")
public class BootApplication {
public static void main(String[] args) {
ConfigurableApplicationContext context = SpringApplication.run(BootApplication.class);
ConfigurableListableBeanFactory beanFactory = context.getBeanFactory();
RequestMappingHandlerAdapter adapter = context.getBean(RequestMappingHandlerAdapter.class);
List<HandlerMethodArgumentResolver> resolvers = adapter.getArgumentResolvers();
if (!CollectionUtils.isEmpty(resolvers)) {
try {
List<HandlerMethodArgumentResolver> newResolvers = new ArrayList<>();
for (HandlerMethodArgumentResolver resolver : resolvers) {
if (resolver instanceof PathVariableMethodArgumentResolver) {
PathVariableMethodArgumentResolver pathVariableMethodArgumentResolver = new PathVariableMethodArgumentResolver();
Field factoryField = AbstractNamedValueMethodArgumentResolver.class.getDeclaredField("configurableBeanFactory");
factoryField.setAccessible(true);
factoryField.set(pathVariableMethodArgumentResolver, beanFactory);
Field expressionField = AbstractNamedValueMethodArgumentResolver.class.getDeclaredField("expressionContext");
expressionField.setAccessible(true);
expressionField.set(pathVariableMethodArgumentResolver, new BeanExpressionContext(beanFactory, new RequestScope()));
newResolvers.add(pathVariableMethodArgumentResolver);
} else {
newResolvers.add(resolver);
}
}
adapter.setArgumentResolvers(Collections.unmodifiableList(newResolvers));
} catch (Exception ignore) {
}
}
}
}
这里我们引用了一个 keys.properties 文件,为下文中演示解析Spel表达式做准备,文件明细如下
key=a
解析 ${}
@RequestMapping("/spel_1/{a}/{b}")
public Object spel1(@PathVariable(value = "${key}") String key) {
return key;
}
解析 #{}
创建 RequestKey
@Component
public class RequestKey {
private String key = "b";
public String getKey() {
return key;
}
public void setKey(String key) {
this.key = key;
}
}
接口及响应
@RequestMapping("/spel_2/{a}/{b}")
public Object spel2(@PathVariable(value = "#{requestKey['key']}") String key) {
return key;
}
参数解析
主要就是获取 URL 上的值
处理默认值
@PathVariable 注解不支持设置默认值,源码这里又处理默认值,感觉有点突兀。其实这里使用了模板模式,@RequestParam 注解的处理步骤是和 @PathVariable 注解一致的,然而 @RequestParam 注解是可以设置默认值的。我的上一篇博文写了@RequestParam 注解的使用和源码解析,有兴趣的小伙伴可以阅读一下,它们的处理流程基本是一致的,就是细节有差别。博文链接 : @RequestParam注解的使用及源码解析
类型转换
SpringBoot 会提前内置很多 convert,当存在一个 convert 可以将当前类型转换为目标类型,则会进行转换。案例演示:
创建实体类 Dog
public class Dog {
private String name;
public Dog(String name) {
this.name = name;
}
@Override
public String toString() {
return "Dog{" +
"name='" + name + '\'' +
'}';
}
}
创建配置类 WebConfig
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addFormatters(FormatterRegistry registry) {
registry.addConverter(new Converter<String, Dog>() {
@Override
public <U> Converter<String, U> andThen(Converter<? super Dog, ? extends U> after) {
return Converter.super.andThen(after);
}
@Override
public Dog convert(String source) {
return new Dog(source);
}
});
}
}
接口及响应
@GetMapping("/convert/{dog}")
public String getDog(@PathVariable(value = "dog") Dog dog) {
return dog.toString();
}
给 Request 域赋值
案例演示
@GetMapping("/attribute/{id}")
public String getAttribute(@PathVariable(value = "id") Integer id, HttpServletRequest request) {
return request.getAttribute(View.PATH_VARIABLES).toString();
}