前言

@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

参数的处理分为两个阶段:

  1. 判断当前环境中存在的resolvers,是否支持解析当前参数
  2. 处理参数

判断是否支持解析当前参数

我的环境中存在27个resolvers,通过命名我们大概可以猜测出 PathVariableMethodArgumentResolverPathVariableMapMethodArgumentResolver 是处理 @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 方法大家可以自行阅读,相关源码如下:

大概分为以下六个步骤:

  1. 构建NamedValueInfo对象
  2. 处理Spel表达式
  3. 解析参数
  4. 处理默认值
  5. 类型转换
  6. 给 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();
}

Logo

2万人民币佣金等你来拿,中德社区发起者X.Lab,联合德国优秀企业对接开发项目,领取项目得佣金!!!

更多推荐