下面就介绍Spring MVC目前的唯一构造方案:通过@RequestMapping来构造一个RequestMappingInfo
RequestMappingHandlerMapping 唯一实现类
根据@RequestMapping注解生成RequestMappingInfo,同时提供isHandler实现。
直到这个具体实现类,才与具体的实现方式@RequestMapping做了强绑定了
有了三层抽象的实现,其实留给本类需要实现的功能已经不是非常的多了~
// @since 3.1 Spring3.1才提供的这种注解扫描的方式的支持~~~ 它也实现了MatchableHandlerMapping分支的接口 // EmbeddedValueResolverAware接口:说明要支持解析Spring的表达式~ public class RequestMappingHandlerMapping extends RequestMappingInfoHandlerMapping implements MatchableHandlerMapping, EmbeddedValueResolverAware { ... private Map<String, Predicate<Class<?>>> pathPrefixes = new LinkedHashMap<>(); // 配置要应用于控制器方法的路径前缀 // @since 5.1:Spring5.1才出来的新特性,其实有时候还是很好的使的 下面给出使用的Demo // 前缀用于enrich每个@RequestMapping方法的映射,至于匹不匹配由Predicate来决定 有种前缀分类的效果~~~~ // 推荐使用Spring5.1提供的类:org.springframework.web.method.HandlerTypePredicate public void setPathPrefixes(Map<String, Predicate<Class<?>>> prefixes) { this.pathPrefixes = Collections.unmodifiableMap(new LinkedHashMap<>(prefixes)); } // @since 5.1 注意pathPrefixes是只读的~~~因为上面Collections.unmodifiableMap了 有可能只是个空Map public Map<String, Predicate<Class<?>>> getPathPrefixes() { return this.pathPrefixes; } public void setUseRegisteredSuffixPatternMatch(boolean useRegisteredSuffixPatternMatch) { this.useRegisteredSuffixPatternMatch = useRegisteredSuffixPatternMatch; this.useSuffixPatternMatch = (useRegisteredSuffixPatternMatch || this.useSuffixPatternMatch); } // If enabled a method mapped to "/users" also matches to "/users/". public void setUseTrailingSlashMatch(boolean useTrailingSlashMatch) { this.useTrailingSlashMatch = useTrailingSlashMatch; } @Override public void afterPropertiesSet() { // 对RequestMappingInfo的配置进行初始化 赋值 this.config = new RequestMappingInfo.BuilderConfiguration(); this.config.setUrlPathHelper(getUrlPathHelper()); // 设置urlPathHelper默认为UrlPathHelper.class this.config.setPathMatcher(getPathMatcher()); //默认为AntPathMatcher,路径匹配校验器 this.config.setSuffixPatternMatch(this.useSuffixPatternMatch); // 是否支持后缀补充,默认为true this.config.setTrailingSlashMatch(this.useTrailingSlashMatch); // 是否添加"/"后缀,默认为true this.config.setRegisteredSuffixPatternMatch(this.useRegisteredSuffixPatternMatch); // 是否采用mediaType匹配模式,比如.json/.xml模式的匹配,默认为false this.config.setContentNegotiationManager(getContentNegotiationManager()); //mediaType处理类:ContentNegotiationManager // 此处 必须还是要调用父类的方法的 super.afterPropertiesSet(); } ... // 判断该类,是否是一个handler(此处就体现出@Controller注解的特殊性了) // 这也是为何我们的XXXController用@Bean申明是无效的原因(前提是类上木有@RequestMapping注解,否则也是阔仪的哦~~~) // 因此我个人建议:为了普适性,类上的@RequestMapping也统一要求加上,即使你不写@Value也木关系,这样是最好的 @Override protected boolean isHandler(Class<?> beanType) { return (AnnotatedElementUtils.hasAnnotation(beanType, Controller.class) || AnnotatedElementUtils.hasAnnotation(beanType, RequestMapping.class)); } // 还记得父类:AbstractHandlerMethodMapping#detectHandlerMethods的时候,回去该类里面找所有的指定的方法 // 而什么叫指定的呢?就是靠这个来判定方法是否符合条件的~~~~~ @Override @Nullable protected RequestMappingInfo getMappingForMethod(Method method, Class<?> handlerType) { // 第一步:先拿到方法上的info RequestMappingInfo info = createRequestMappingInfo(method); if (info != null) { // 方法上有。在第二步:拿到类上的info RequestMappingInfo typeInfo = createRequestMappingInfo(handlerType); if (typeInfo != null) { // 倘若类上面也有,那就combine把两者结合 // combile的逻辑基如下: // names:name1+#+name2 // path:路径拼接起来作为全路径(容错了方法里没有/的情况) // method、params、headers:取并集 // consumes、produces:以方法的为准,没有指定再取类上的 // custom:谁有取谁的。若都有:那就看custom具体实现的.combine方法去决定把 简单的说就是交给调用者了~~~ info = typeInfo.combine(info); } // 在Spring5.1之后还要处理这个前缀匹配~~~ // 根据这个类,去找看有没有前缀 getPathPrefix():entry.getValue().test(handlerType) = true算是hi匹配上了 // 备注:也支持${os.name}这样的语法拿值,可以把前缀也写在专门的配置文件里面~~~~ String prefix = getPathPrefix(handlerType); if (prefix != null) { // RequestMappingInfo.paths(prefix) 相当于统一在前面加上这个前缀~ info = RequestMappingInfo.paths(prefix).build().combine(info); } } return info; } // 根据此方法/类,创建一个RequestMappingInfo @Nullable private RequestMappingInfo createRequestMappingInfo(AnnotatedElement element) { // 注意:此处使用的是findMergedAnnotation 这也就是为什么虽然@RequestMapping它并不具有继承的特性,但是你子类仍然有继承的效果的原因~~~~ RequestMapping requestMapping = AnnotatedElementUtils.findMergedAnnotation(element, RequestMapping.class); // 请注意:这里进行了区分处理 如果是Class的话 如果是Method的话 // 这里返回的是一个condition 也就是看看要不要处理这个请求的条件~~~~ RequestCondition<?> condition = (element instanceof Class ? getCustomTypeCondition((Class<?>) element) : getCustomMethodCondition((Method) element)); // 这个createRequestMappingInfo就是根据一个@RequestMapping以及一个condition创建一个 // 显然如果没有找到此注解,这里就返回null了,表面这个方法啥的就不是一个info~~~~ return (requestMapping != null ? createRequestMappingInfo(requestMapping, condition) : null); } // 他俩都是返回的null。protected方法留给子类复写,子类可以据此自己定义一套自己的规则来限制匹配 // Provide a custom method-level request condition. // 它相当于在Spring MVC默认的规则的基础上,用户还可以自定义条件进行处理~~~~ @Nullable protected RequestCondition<?> getCustomTypeCondition(Class<?> handlerType) { return null; } @Nullable protected RequestCondition<?> getCustomMethodCondition(Method method) { return null; } // 根据@RequestMapping 创建一个RequestMappingInfo protected RequestMappingInfo createRequestMappingInfo(RequestMapping requestMapping, @Nullable RequestCondition<?> customCondition) { RequestMappingInfo.Builder builder = RequestMappingInfo // 强大的地方在此处:path里竟然还支持/api/v1/${os.name}/hello 这样形式动态的获取值 // 也就是说URL还可以从配置文件里面读取 Spring考虑很周到啊~~~ // @GetMapping("/${os.name}/hello") // 支持从配置文件里读取此值 Windows 10 .paths(resolveEmbeddedValuesInPatterns(requestMapping.path())) .methods(requestMapping.method()) .params(requestMapping.params()) .headers(requestMapping.headers()) .consumes(requestMapping.consumes()) .produces(requestMapping.produces()) .mappingName(requestMapping.name()); // 调用者自定义的条件~~~ if (customCondition != null) { builder.customCondition(customCondition); } // 注意此处:把当前的config设置进去了~~~~ return builder.options(this.config).build(); } @Override public RequestMatchResult match(HttpServletRequest request, String pattern) { ... } // 支持了@CrossOrigin注解 Spring4.2提供的注解 @Override protected CorsConfiguration initCorsConfiguration(Object handler, Method method, RequestMappingInfo mappingInfo) { ... } }
至此RequestMappingHandlerMapping的初始化完成了。像pathPrefixes
这种配置,可以全局统一配置来控制每个Controller
,如常用的/api/v1
前缀~
如何配置呢?我给出个示例供给你参考:
@Configuration @EnableWebMvc public class WebMvcConfig extends WebMvcConfigurerAdapter { @Override public void configurePathMatch(PathMatchConfigurer configurer) { //configurer.setUseSuffixPatternMatch(false); //关闭后缀名匹配,关闭最后一个/匹配 //configurer.setUseTrailingSlashMatch(false); // 这样HelloController上的方法自动就会有此前缀了,而别的controller上是不会有的 // 注意:这是Spring5.1后才支持的新特性 configurer.addPathPrefix("/api/v1", clazz -> clazz.isAssignableFrom(HelloController.class)); // 使用Spring提供的HandlerTypePredicate,更加的强大 HandlerTypePredicate predicate = HandlerTypePredicate.forBasePackage("com.fsx"); //HandlerTypePredicate predicate = HandlerTypePredicate.forBasePackageClass(HelloController.class); //HandlerTypePredicate predicate = HandlerTypePredicate.forAssignableType(...); //HandlerTypePredicate predicate = HandlerTypePredicate.forAnnotation(...); //HandlerTypePredicate predicate = HandlerTypePredicate.builder() // .basePackage() // .basePackageClass() // .build(); configurer.addPathPrefix("/api/v2", predicate); } }
细节注意:若添加了两prefix都可以作用在某个Controller上,那么会按照放入的顺序(因为它是LinkedHashMap)以先匹配上的为准,可参考RequestMappingHandlerMapping#getPathPrefix方法~
RequestMappingHandlerMapping 向容器中注册的时候,检测到实现了 InitializingBean接口,容器去执行afterPropertiesSet(),在afterPropertiesSet中完成Controller中完成方法的映射
以上就是Spring MVC在容器启动过程中,完成URL到Handler映射的所有内容~
@RequestMapping属性详解
使用@RequestMapping 来映射URL 到控制器类,或者是到Controller 控制器的处理方法上。
当@RequestMapping 标记在Controller 类上的时候,里面使用@RequestMapping 标记的方法的请求地址都是相对于类上的@RequestMapping 而言的;当Controller 类上没有标记@RequestMapping 注解时,方法上的@RequestMapping 都是绝对路径。
这种绝对路径和相对路径所组合成的最终路径都是相对于根路径“/ ”而言的。
这个注解的属性众多,下面逐个解释一下:
// @since 2.5 用于将Web请求映射到具有灵活方法签名的请求处理类中的方法的注释 Both Spring MVC and `Spring WebFlux` support this annotation // @Mapping这个注解是@since 3.0 但它目前还只有这个地方使用到了~~~ 我感觉是多余的 @Target({ElementType.METHOD, ElementType.TYPE}) // 能够用到类上和方法上 @Retention(RetentionPolicy.RUNTIME) @Documented @Mapping public @interface RequestMapping { //给这个Mapping取一个名字。若不填写,就用HandlerMethodMappingNamingStrategy去按规则生成 String name() default ""; // 路径 数组形式 可以写多个。 一般都是按照Ant风格进行书写~ @AliasFor("path") String[] value() default {}; @AliasFor("value") String[] path() default {}; // 请求方法:GET, HEAD, POST, PUT, PATCH, DELETE, OPTIONS, TRACE // 显然可以指定多个方法。如果不指定,表示适配所有方法类型~~ // 同时还有类似的枚举类:org.springframework.http.HttpMethod RequestMethod[] method() default {}; // 指定request中必须包含某些参数值时,才让该方法处理 // 使用 params 元素,你可以让多个处理方法处理到同一个URL 的请求, 而这些请求的参数是不一样的 // 如:@RequestMapping(value = "/fetch", params = {"personId=10"} 和 @RequestMapping(value = "/fetch", params = {"personId=20"} // 这两个方法都处理请求`/fetch`,但是参数不一样,进入的方法也不一样~~~~ // 支持!myParam和myParam!=myValue这种~~~ String[] params() default {}; // 指定request中必须包含某些指定的header值,才能让该方法处理请求 // @RequestMapping(value = "/head", headers = {"content-type=text/plain"} String[] headers() default {}; // 指定处理请求request的**提交内容类型**(Content-Type),例如application/json、text/html等 // 相当于只有指定的这些Content-Type的才处理 // @RequestMapping(value = "/cons", consumes = {"application/json", "application/XML"} // 不指定表示处理所有~~ 取值参见枚举类:org.springframework.http.MediaType // 它可以使用!text/plain形如这样非的表达方式 String[] consumes() default {}; // 指定返回的内容类型,返回的内容类型必须是request请求头(Accept)中所包含的类型 // 仅当request请求头中的(Accept)类型中包含该指定类型才返回; // 参见枚举类:org.springframework.http.MediaType // 它可以使用!text/plain形如这样非的表达方式 String[] produces() default {}; }
Spring4.3之后提供了组合注解5枚:
@GetMapping @PostMapping @PutMapping @DeleteMapping @PatchMapping
consumes 与 headers 区别
consumes produces params headers四个属性都是用来缩小请求范围。
consumes只能指定 content-Type 的内容类型,但是headers可以指定所有。
所以可以认为:headers是更为强大的(所有需要指定key和value嘛),而consumes和produces是专用的,头的key是固定的,所以只需要写value值即可,使用起来也更加的方便~。
推荐一个类:org.springframework.http.HttpHeaders,它里面有常量:几乎所有的请求头的key,以及我们可以很方便的构建一个HttpHeader,平时可以作为参考使用
Spring MVC默认使用的HandlerMapping是什么?
Spring对这块的设计也是很灵活的,允许你自己配置,也允许你啥都不做使用Spring默认的配置。处理代码在:DispatcherServlet#initHandlerMappings
public class DispatcherServlet extends FrameworkServlet { // 为此DispatcherServlet 初始化HandlerMappings // 备注:DispatcherServlet是允许你有多个的~~~~ private void initHandlerMappings(ApplicationContext context) { this.handlerMappings = null; //detectAllHandlerMappings该属性默认为true,表示会去容器内找所有的HandlerMapping类型的定义信息 // 若想改为false,请调用它的setDetectAllHandlerMappings() 自行设置值(绝大部分情况下没啥必要) if (this.detectAllHandlerMappings) { // 这里注意:若你没有标注注解`@EnableWebMvc`,那么这里找的结果是空的 // 若你标注了此注解,这个注解就会默认向容器内注入两个HandlerMapping:RequestMappingHandlerMapping和BeanNameUrlHandlerMapping Map<String, HandlerMapping> matchingBeans = BeanFactoryUtils.beansOfTypeIncludingAncestors(context, HandlerMapping.class, true, false); if (!matchingBeans.isEmpty()) { this.handlerMappings = new ArrayList<>(matchingBeans.values()); // 多个的话 还需要进行一次排序~~~ AnnotationAwareOrderComparator.sort(this.handlerMappings); } } // 不全部查找,那就只找一个名字为`handlerMapping`的HandlerMapping 实现精准控制 // 绝大多数情况下 我们并不需要这么做~ else { try { HandlerMapping hm = context.getBean(HANDLER_MAPPING_BEAN_NAME, HandlerMapping.class); this.handlerMappings = Collections.singletonList(hm); } catch (NoSuchBeanDefinitionException ex) { // Ignore, we'll add a default HandlerMapping later. } } // 若一个都没找到自定义的,回滚到Spring的兜底策略,它会想容器注册两个:RequestMappingHandlerMapping和BeanNameUrlHandlerMapping if (this.handlerMappings == null) { this.handlerMappings = getDefaultStrategies(context, HandlerMapping.class); // 输出trace日志:表示使用了兜底策略~ // 兜底策略配置文件:DispatcherServlet.properties if (logger.isTraceEnabled()) { logger.trace("No HandlerMappings declared for servlet '" + getServletName() + "': using default strategies from DispatcherServlet.properties"); } } } }
通过这段代码,我们能够很清晰的看到。绝大部分情况下,我们容器内会有这两个HandlerMapping Bean:
RequestMappingHandlerMapping和BeanNameUrlHandlerMapping
换句话说,默认情况下@RequestMapping和BeanNameUrl的方式都是被支持的~
请注意:使用@EnableWebMvc和不使用它有一个非常非常重要的区别:
使用@EnableWebMvc原来是依托于这个WebMvcConfigurationSupport config类向容器中注入了对应的Bean,所以他们都是交给了Spring管理的(所以你可以@Autowired他们)
但是,但是,但是(重说三),若是走了Spring它自己去读取配置文件走默认值,它的Bean是没有交给Spring管理的,没有交给Spring管理的。它是这样创建的:context.getAutowireCapableBeanFactory().createBean(clazz) 它创建出来的Bean都不会交给Spring管理。
参考博文:【小家Spring】为脱离Spring IOC容器管理的Bean赋能【依赖注入】的能力,并分析原理(借助AutowireCapableBeanFactory赋能)
小插曲:在Spring5以下,DispatcherServlet.properties这个配置文件里写的是这样的:
相当于最底层默认使用的是DefaultAnnotationHandlerMapping,而在Spring5之后,改成了RequestMappingHandlerMapping。DefaultAnnotationHandlerMapping是Spring2.5用来处理@RequestMapping注解的,自从Spring3.2后已被标记为:@Deprecated
需要注意的是:纯Spring MVC环境下我们都会开启@EnableWebMvc,所有我们实际使用的还是RequestMappingHandlerMapping的。
而在SpringBoot环境下,虽然我们一般不建议标注@EnableWebMvc,但是Boot它默认也会注册RequestMappingHandlerMapping它的。现在Spring5/Boot2以后一切都爽了~~~~
DefaultAnnotationHandlerMapping的一个小坑
在功能上DefaultAnnotationHandlerMapping和RequestMappingHandlerMapping绝大多数是等价的。但是因为DefaultAnnotationHandlerMapping过于古老了,它并不支持像@GetMapping(Spring4.3后提供)这样的组合注解的。 从源码角度理由如下:
比如Handler这么写的:
DefaultAnnotationHandlerMapping处理代码为:
值如下:
发现我们的URL并没有获取到。但是RequestMappingHandlerMapping的获取代码为:
可以发现使用AnnotatedElementUtils.findMergedAnnotation是支持这个组合注解的。但是AnnotatedElementUtils整个工具类才Spring4.0后才有,而DefaultAnnotationHandlerMapping早在Spring3.2后就被标记为废弃了,因为就无需Spring也就无需继续维护了~~~~
所以若你是纯Spring MVC环境,为确保万无一失,请开启SpringMVC:@EnableWebMvc
备注:若使用非组合注解如@RequestMapping,两者大体一样。但既然人家都废弃了,所以非常不建议再继续使用~~~
其实在Spring5.以后,就直接把这个两个类拿掉了,所以也就没有后顾之忧了。(DispatcherServlet.properties这个配置文件也做了对应的修改)
总结
Spring MVC在启动时会扫描所有的@RequestMapping并封装成对应的RequestMapingInfo。
一个请求过来会与RequestMapingInfo进行逐个比较,找到最适合的那个RequestMapingInfo。
Spring MVC通过HandlerMapping建立起了Url Pattern和Handler的对应关系,这样任何一个URL请求过来时,就可以快速定位一个唯一的Handler,然后交给其进行处理了~
当然这里面还有很多实现细节,其中还有一个非常重要的一块:HandlerAdapter,会在下文继续源码分析,请保持持续关注~
关于定制RequestMappingHandlerMapping,以及自定义RequestCondition来灵活的使用映射器,我推荐可参阅这篇文章:Spring Mvc之定制RequestMappingHandlerMapping