// @since 3.0 需要注意的是:它只支持标注在@RequestMapping的方法(处理器)上使用~ @Target(ElementType.PARAMETER) @Retention(RetentionPolicy.RUNTIME) @Documented public @interface PathVariable { @AliasFor("name") String value() default ""; @AliasFor("value") String name() default ""; // 注意:它并没有defaultValue哦~ // @since 4.3.3 它也是标记为false非必须的~~~~ boolean required() default true; } // @since 3.1 public class PathVariableMethodArgumentResolver extends AbstractNamedValueMethodArgumentResolver implements UriComponentsContributor { private static final TypeDescriptor STRING_TYPE_DESCRIPTOR = TypeDescriptor.valueOf(String.class); // 简单一句话描述:@PathVariable是必须,不管你啥类型 // 标注了注解,且是Map类型, @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; } @Override protected NamedValueInfo createNamedValueInfo(MethodParameter parameter) { PathVariable ann = parameter.getParameterAnnotation(PathVariable.class); return new PathVariableNamedValueInfo(ann); } private static class PathVariableNamedValueInfo extends NamedValueInfo { public PathVariableNamedValueInfo(PathVariable annotation) { // 默认值使用的DEFAULT_NONE~~~ super(annotation.name(), annotation.required(), ValueConstants.DEFAULT_NONE); } } // 根据name去拿值的过程非常之简单,但是它和前面的只知识是有关联的 // 至于这个attr是什么时候放进去的,AbstractHandlerMethodMapping.handleMatch()匹配处理器方法上 // 通过UrlPathHelper.decodePathVariables() 把参数提取出来了,然后放进request属性上暂存了~~~ // 关于HandlerMapping内容,可来这里:https://blog.csdn.net/f641385712/article/details/89810020 @Override @Nullable protected Object resolveName(String name, MethodParameter parameter, NativeWebRequest request) throws Exception { Map<String, String> uriTemplateVars = (Map<String, String>) request.getAttribute(HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE, RequestAttributes.SCOPE_REQUEST); return (uriTemplateVars != null ? uriTemplateVars.get(name) : null); } // MissingPathVariableException是ServletRequestBindingException的子类 @Override protected void handleMissingValue(String name, MethodParameter parameter) throws ServletRequestBindingException { throw new MissingPathVariableException(name, parameter); } // 值完全处理结束后,把处理好的值放进请求域,方便view里渲染时候使用~ // 抽象父类的handleResolvedValue方法,只有它复写了~ @Override @SuppressWarnings("unchecked") protected void handleResolvedValue(@Nullable Object arg, String name, MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer, NativeWebRequest request) { String key = View.PATH_VARIABLES; int scope = RequestAttributes.SCOPE_REQUEST; Map<String, Object> pathVars = (Map<String, Object>) request.getAttribute(key, scope); if (pathVars == null) { pathVars = new HashMap<>(); request.setAttribute(key, pathVars, scope); } pathVars.put(name, arg); } ... }
关于@PathVariable的使用,不用再给例子了。
说明:因为使用路径参数需要进行复杂的匹配流程以及正则匹配,所有效率相较来说低些,若以若是那种对响应事件强要求的(比如记录点击事件…),建议用请求参数代替(当然你也可以重写RequestMappingHandlerMapping的URL匹配方法来定制化你的需求)。
GET /list/cityId/1 属于RESTful /list/cityId?cityId=1不属于RESTful。通过Apache JMeter测试:非RESTful接口的性能是RESTful接口的两倍,接口相应时间上更是达到10倍左右(是–>300ms左右 非–>20ms左右)
针对RESTful此处我提出一个思考题:若你是一个现成的系统,现对相应提出要求:接口耗时必须控制在50ms以内,怎么破?
思路一:将所有的url修改为非RESTful风格(不使用@PathVariable)
痛点:系统已存在几百个接口,若修改不仅需要修改服务端,客户端也得改,工作量太大。并且稍有不慎,容易造成404现象~
思路二:定制化AbstractHandlerMethodMapping#lookupHandlerMethod方法
此方法负责URL的匹配,我们为了提效其实就是为了避免一些正则匹配(AntPathMatcher)。
对此文答案有兴趣的可参见此文:SpringMVC RESTful 性能优化
唯一需要说一下如果类型是Map类型的情况下的使用注意事项,如下:
@PathVariable("jsonStr") Map<String,Object> map
希望把jsonStr对应的字符串解析成键值对封装进Map里。那么你必须,必须,必须注册了能处理此字符串的Converter/PropertyEditor(自定义)。使用起来相对麻烦,但技术隐蔽性高。我一般不建议这么来用~
关于@PathVariable的required=false使用注意事项
这个功能是很多人比较疑问的,如何使用???
@ResponseBody @GetMapping("/test/{id}") public Person test(@PathVariable(required = false) Integer id) { ... }
以为这样写通过/test这个url就能访问到了,其实这样是不行的,会404。
正确姿势:
@ResponseBody @GetMapping({"/test/{id}", "/test"}) public Person test(@PathVariable(required = false) Integer id) { ... }
这样/test和/test/1这两个url就都能正常work了~
@PathVariable的required=false使用较少,一般用于在用URL传多个值时,但有些值是非必传的时候使用。比如这样的URL:"/user/{id}/{name}","/user/{id}","/user"
RequestParamMethodArgumentResolver
顾名思义,是解析标注有@RequestParam的方法入参解析器,这个注解比上面的注解强大很多了,它用于从请求参数(?后面的)中获取值完成封装。这是我们的绝大多数使用场景。除此之外,它还支持MultipartFile,也就是说能够从MultipartHttpServletRequest | HttpServletRequest 获取数据,并且并且并且还兜底处理没有标注任何注解的“简单类型”~
// @since 2.5 @Target(ElementType.PARAMETER) @Retention(RetentionPolicy.RUNTIME) @Documented public @interface RequestParam { @AliasFor("name") String value() default ""; // @since 4.2 @AliasFor("value") String name() default ""; boolean required() default true; String defaultValue() default ValueConstants.DEFAULT_NONE; }
// @since 3.1 public class RequestParamMethodArgumentResolver extends AbstractNamedValueMethodArgumentResolver implements UriComponentsContributor { private static final TypeDescriptor STRING_TYPE_DESCRIPTOR = TypeDescriptor.valueOf(String.class); // 这个参数老重要了: // true:表示参数类型是基本类型 参考BeanUtils#isSimpleProperty(什么Enum、Number、Date、URL、包装类型、以上类型的数组类型等等) // 如果是基本类型,即使你不写@RequestParam注解,它也是会走进来处理的~~~(这个@PathVariable可不会哟~) // fasle:除上以外的。 要想它处理就必须标注注解才行哦,比如List等~ // 默认值是false private final boolean useDefaultResolution; // 此构造只有`MvcUriComponentsBuilder`调用了 传入的false public RequestParamMethodArgumentResolver(boolean useDefaultResolution) { this.useDefaultResolution = useDefaultResolution; } // 传入了ConfigurableBeanFactory ,所以它支持处理占位符${...} 并且支持SpEL了 // 此构造都在RequestMappingHandlerAdapter里调用,最后都会传入true来Catch-all Case 这种设计挺有意思的 public RequestParamMethodArgumentResolver(@Nullable ConfigurableBeanFactory beanFactory, boolean useDefaultResolution) { super(beanFactory); this.useDefaultResolution = useDefaultResolution; } // 此处理器能处理如下Case: // 1、所有标注有@RequestParam注解的类型(非Map)/ 注解指定了value值的Map类型(自己提供转换器哦) // ======下面都表示没有标注@RequestParam注解了的======= // 1、不能标注有@RequestPart注解,否则直接不处理了 // 2、是上传的request:isMultipartArgument() = true(MultipartFile类型或者对应的集合/数组类型 或者javax.servlet.http.Part对应结合/数组类型) // 3、useDefaultResolution=true情况下,"基本类型"也会处理 @Override public boolean supportsParameter(MethodParameter parameter) { if (parameter.hasParameterAnnotation(RequestParam.class)) { if (Map.class.isAssignableFrom(parameter.nestedIfOptional().getNestedParameterType())) { RequestParam requestParam = parameter.getParameterAnnotation(RequestParam.class); return (requestParam != null && StringUtils.hasText(requestParam.name())); } else { return true; } } else { if (parameter.hasParameterAnnotation(RequestPart.class)) { return false; } parameter = parameter.nestedIfOptional(); if (MultipartResolutionDelegate.isMultipartArgument(parameter)) { return true; } else if (this.useDefaultResolution) { return BeanUtils.isSimpleProperty(parameter.getNestedParameterType()); } else { return false; } } } // 从这也可以看出:即使木有@RequestParam注解,也是可以创建出一个NamedValueInfo来的 @Override protected NamedValueInfo createNamedValueInfo(MethodParameter parameter) { RequestParam ann = parameter.getParameterAnnotation(RequestParam.class); return (ann != null ? new RequestParamNamedValueInfo(ann) : new RequestParamNamedValueInfo()); } // 内部类 private static class RequestParamNamedValueInfo extends NamedValueInfo { // 请注意这个默认值:如果你不写@RequestParam,那么就会用这个默认值 // 注意:required = false的哟(若写了注解,required默认可是true,请务必注意区分) // 因为不写注解的情况下,若是简单类型参数都是交给此处理器处理的。所以这个机制需要明白 // 复杂类型(非简单类型)默认是ModelAttributeMethodProcessor处理的 public RequestParamNamedValueInfo() { super("", false, ValueConstants.DEFAULT_NONE); } public RequestParamNamedValueInfo(RequestParam annotation) { super(annotation.name(), annotation.required(), annotation.defaultValue()); } } // 核心方法:根据Name 获取值(普通/文件上传) // 并且还有集合、数组等情况 @Override @Nullable protected Object resolveName(String name, MethodParameter parameter, NativeWebRequest request) throws Exception { HttpServletRequest servletRequest = request.getNativeRequest(HttpServletRequest.class); // 这块解析出来的是个MultipartFile或者其集合/数组 if (servletRequest != null) { Object mpArg = MultipartResolutionDelegate.resolveMultipartArgument(name, parameter, servletRequest); if (mpArg != MultipartResolutionDelegate.UNRESOLVABLE) { return mpArg; } } Object arg = null; MultipartRequest multipartRequest = request.getNativeRequest(MultipartRequest.class); if (multipartRequest != null) { List<MultipartFile> files = multipartRequest.getFiles(name); if (!files.isEmpty()) { arg = (files.size() == 1 ? files.get(0) : files); } } // 若解析出来值仍旧为null,那处理完文件上传里木有,那就去参数里取吧 // 由此可见:文件上传的优先级是高于请求参数的 if (arg == null) { //小知识点:getParameter()其实本质是getParameterNames()[0]的效果 // 强调一遍:?ids=1,2,3 结果是["1,2,3"](兼容方式,不建议使用。注意:只能是逗号分隔) // ?ids=1&ids=2&ids=3 结果是[1,2,3](标准的传值方式,建议使用) // 但是Spring MVC这两种都能用List接收 请务必注意他们的区别~~~ String[] paramValues = request.getParameterValues(name); if (paramValues != null) { arg = (paramValues.length == 1 ? paramValues[0] : paramValues); } } return arg; } ... }
可以看到ServletModelAttributeMethodProcessor和RequestParamMethodArgumentResolver一样,也是有兜底的效果的。