【小家Spring】Spring MVC容器的web九大组件之---HandlerMapping源码详解(二)---RequestMappingHandlerMapping系列(下)

本文涉及的产品
容器镜像服务 ACR,镜像仓库100个 不限时长
简介: 【小家Spring】Spring MVC容器的web九大组件之---HandlerMapping源码详解(二)---RequestMappingHandlerMapping系列(下)

下面就介绍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这个配置文件里写的是这样的:


image.png


相当于最底层默认使用的是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处理代码为:


 
         


值如下:


image.png


发现我们的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

相关文章
|
3月前
|
Java API 数据库
构建RESTful API已经成为现代Web开发的标准做法之一。Spring Boot框架因其简洁的配置、快速的启动特性及丰富的功能集而备受开发者青睐。
【10月更文挑战第11天】本文介绍如何使用Spring Boot构建在线图书管理系统的RESTful API。通过创建Spring Boot项目,定义`Book`实体类、`BookRepository`接口和`BookService`服务类,最后实现`BookController`控制器来处理HTTP请求,展示了从基础环境搭建到API测试的完整过程。
68 4
|
1月前
|
Java 开发者 微服务
Spring Boot 入门:简化 Java Web 开发的强大工具
Spring Boot 是一个开源的 Java 基础框架,用于创建独立、生产级别的基于Spring框架的应用程序。它旨在简化Spring应用的初始搭建以及开发过程。
85 6
Spring Boot 入门:简化 Java Web 开发的强大工具
|
4月前
|
缓存 前端开发 Java
【Java面试题汇总】Spring,SpringBoot,SpringMVC,Mybatis,JavaWeb篇(2023版)
Soring Boot的起步依赖、启动流程、自动装配、常用的注解、Spring MVC的执行流程、对MVC的理解、RestFull风格、为什么service层要写接口、MyBatis的缓存机制、$和#有什么区别、resultType和resultMap区别、cookie和session的区别是什么?session的工作原理
|
3月前
|
前端开发 Java 应用服务中间件
【Spring】Spring MVC的项目准备和连接建立
【Spring】Spring MVC的项目准备和连接建立
74 2
|
3月前
|
存储 编译器 C++
【C++篇】揭开 C++ STL list 容器的神秘面纱:从底层设计到高效应用的全景解析(附源码)
【C++篇】揭开 C++ STL list 容器的神秘面纱:从底层设计到高效应用的全景解析(附源码)
97 2
|
3月前
|
XML 前端开发 Java
Spring,SpringBoot和SpringMVC的关系以及区别 —— 超准确,可当面试题!!!也可供零基础学习
本文阐述了Spring、Spring Boot和Spring MVC的关系与区别,指出Spring是一个轻量级、一站式、模块化的应用程序开发框架,Spring MVC是Spring的一个子框架,专注于Web应用和网络接口开发,而Spring Boot则是对Spring的封装,用于简化Spring应用的开发。
283 0
Spring,SpringBoot和SpringMVC的关系以及区别 —— 超准确,可当面试题!!!也可供零基础学习
|
3月前
|
前端开发 Java
【案例+源码】详解MVC框架模式及其应用
【案例+源码】详解MVC框架模式及其应用
240 0
|
4月前
|
前端开发 安全 Java
技术进阶:使用Spring MVC构建适应未来的响应式Web应用
【9月更文挑战第2天】随着移动设备的普及,响应式设计至关重要。Spring MVC作为强大的Java Web框架,助力开发者创建适应多屏的应用。本文推荐使用Thymeleaf整合视图,通过简洁的HTML代码提高前端灵活性;采用`@ResponseBody`与`Callable`实现异步处理,优化应用响应速度;运用`@ControllerAdvice`统一异常管理,保持代码整洁;借助Jackson简化JSON处理;利用Spring Security增强安全性;并强调测试的重要性。遵循这些实践,将大幅提升开发效率和应用质量。
79 7
|
4月前
|
前端开发 测试技术 开发者
MVC模式在现代Web开发中有哪些优势和局限性?
MVC模式在现代Web开发中有哪些优势和局限性?
|
5月前
|
开发者 前端开发 Java
架构模式的诗与远方:如何在MVC的田野上,用Struts 2编织Web开发的新篇章
【8月更文挑战第31天】架构模式是软件开发的核心概念,MVC(Model-View-Controller)通过清晰的分层和职责分离,成为广泛采用的模式。随着业务需求的复杂化,Struts 2框架应运而生,继承MVC优点并引入更多功能。本文探讨从MVC到Struts 2的演进,强调架构模式的重要性。MVC将应用程序分为模型、视图和控制器三部分,提高模块化和可维护性。
53 0