上一篇https://blog.csdn.net/chen_yao_kerr/article/details/130194864
我们已经分析了Spring MVC的配置,并且说明了如何通过注解的方式去替换各种各样的xml配置文件。本篇将更深入分析:
之前我们说过,定义一个类使用 @EnableWebMvc 注解开启Spring MVC的。 我们用一个@EnableWebMvc 就可以完全取代 xml 配置, 其实两者完成的工作是一样的,都是为了创建必要组件的实例。等同于在Spring mvc.xml文件中如下配置:
自定义的类:
package com.xiangxue.jack.mvc;
import com.xiangxue.jack.interceptor.UserInterceptor;
import com.xiangxue.jack.interceptor.UserInterceptor1;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.HandlerExceptionResolver;
import org.springframework.web.servlet.config.annotation.*;
import org.springframework.web.servlet.view.json.MappingJackson2JsonView;
import java.util.List;
@Configuration
@EnableWebMvc
public class AppConfig extends WebMvcConfigurerAdapter {
//拦截器
@Autowired
private UserInterceptor userInterceptor;
@Override
public void configureViewResolvers(ViewResolverRegistry registry) {
registry.enableContentNegotiation(new MappingJackson2JsonView());
registry.jsp("/jsp/", ".jsp");
}
@Override
public void addViewControllers(ViewControllerRegistry registry) {
registry.addViewController("/view/ok").setViewName("ok");
registry.addViewController("/view/index").setViewName("index");
}
//开启默认handlerMapping
@Override
public void configureDefaultServletHandling(DefaultServletHandlerConfigurer configurer) {
configurer.enable();
}
//钩子方法的实现,添加拦截器
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(userInterceptor).addPathPatterns("/user/**").excludePathPatterns("/user/query/**");
registry.addInterceptor(new UserInterceptor1()).addPathPatterns("/user/**").excludePathPatterns("");
}
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/image/**")
.addResourceLocations("classpath:/img/");
}
@Override
public void configureHandlerExceptionResolvers(List exceptionResolvers) {
super.configureHandlerExceptionResolvers(exceptionResolvers);
}
/* @Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/user/**")
.allowedOrigins("*")
.allowCredentials(true)
.allowedMethods("GET", "POST", "DELETE", "PUT","PATCH")
.maxAge(3600);
}*/
}
而@EnableWebMvc注解会import进来一个 DelegatingWebMvcConfiguration类,它是实现HandlerMapping的核心类:
而 DelegatingWebMvcConfiguration 继承了 WebMvcConfigurationSupport,在父类中有很多的@Bean方法, 这些方法完成很多组件的实例化, 比如 HandlerMapping, HandlerAdapter 等等。 如图:
然后在实例化过程中会涉及到很多钩子方法的调用, 而这些钩子方法就是我们需要去实现
的, 比如获取拦截器的钩子方法, 获取静态资源处理的钩子方法等等。这些方法都是在我们自定义的AppConfig类中实现的。这也解释了AppConfig 一大堆实现方法的原因。
在我们完成ContextLoaderListener 和 DispatcherServlet 实例化过程的时候,我们分别为这2个类生成了AnnotationConfigWebApplicationContext上下文类。
这两个类是有调用先后顺序的,先进行ContextLoaderListener 的初始化并且启动上下文类;然后再进行DispatcherServlet 的初始化并且调用上下文类:
ContextLoaderListener :
DispatcherServlet :
这里需要重点分析一下DispatcherServlet 启动上下文的情况。
也就是说DispatcherServlet 中的上下文是子,ContextLoaderListener 中的上下文是父,他们是父子关系。
1.ContextLoaderListener 启动上下文: 如果我们在扫描的时候,Spring能够扫描到含有@Controller、@RequestMapping注解的类,我们实例化@Controller、@RequestMapping类的时候,我们会把这些类的method 和 URL生成映射。
2. DispatcherServlet 启动上下文:如果Spring扫描不到@Controller、@RequestMapping注解的类, 而Spring MVC支持的类扫描到这些类,也会完成method 和 URL生成映射。
3. 无论是Spring建立映射关系,还是Spring MVC建立映射关系,底层代码的实现逻辑都是一样的。
在我们实例化完 RequestMappingHandlerMapping 对象以后,我们最终会进入initializeBean 方法进行映射关系的调用,具体调用如下:
而 invokeInitMethods最终会调用到 RequestMappingHandlerMapping 对象的 afterPropertiesSet方法:
也就是说,在实例化RequestMappingHandlerMapping 对象以后,我们会对所有的候选BeanDefinition进行遍历
在 processCandidateBean方法内部,我们首先判断当前实例化的Bean对象是否有@Controller注解或者@RequestMapping注解,如果有的话就建立映射关系.
接下来就进入了URL与Method的映射关系的核心流程,具体方法为 detectHandlerMethods
protected void detectHandlerMethods(Object handler) {
Class> handlerType = (handler instanceof String ?
obtainApplicationContext().getType((String) handler) : handler.getClass());
if (handlerType != null) {
Class> userType = ClassUtils.getUserClass(handlerType);
//获取方法对象和方法上面的@RequestMapping注解属性封装对象的映射关系
Map methods = MethodIntrospector.selectMethods(userType,
(MethodIntrospector.MetadataLookup) method -> {
try {
//回调方法,具体创建RequestMappingInfo的方法
return getMappingForMethod(method, userType);
}
catch (Throwable ex) {
throw new IllegalStateException("Invalid mapping on handler class [" +
userType.getName() + "]: " + method, ex);
}
});
if (logger.isTraceEnabled()) {
logger.trace(formatMappings(userType, methods));
}
methods.forEach((method, mapping) -> {
Method invocableMethod = AopUtils.selectInvocableMethod(method, userType);
//建立uri和方法的各种映射关系,反正一条,根据uri要能够找到method对象
registerHandlerMethod(handler, invocableMethod, mapping);
});
}
}
大体思路分为7步:
1. 根据搜集到有@Controller注解或者@RequestMapping注解的类,通过反射获取到所有的方法,逐个找到有@RequestMapping注解的方法
2. 根据方法上的 URL 和 类上方的 URL 拼接处一个完整的URL。 比如:类上的URL为:@RequestMapping("/user"), 方法上的URL为 @RequestMapping("/queryUser"), 那么完整的URL为 /user/queryUser。
@Controller
@RequestMapping("/user")
public class UserController {
@RequestMapping("/queryUser")
public @ResponseBody String queryUser() {
return "jack";
}
}
3. 把这个完整的URL封装成RequestMappingInfo对象
4. 把方法对应的Method对象 和 RequestMappingInfo对象放入map中,Method 为key, RequestMappingInfo 为 value
5. 遍历map, 根据Method 和 当前Controller类名,封装成唯一的 HandlerMethod对象,该类型封装了method, beanName, Bean, 方法类型等信息。
6. 遍历map, 将RequestMappingInfo作为key,HandlerMethod作为value,建立映射关系
7. 遍历map,将字符串 /user/queryUser作为key,RequestMappingInfo作为value,建立映射关系
5、6、7 步骤都是在遍历同一个map中完成的,这样就可以根据 字符串URL找到RequestMappingInfo,再根据RequestMappingInfo找到HandlerMethod,而HandlerMethod可以确定具体的方法。映射关系建立完毕。
下面对以上步骤逐步进行代码确认,下面这张图涉及到了前4步操作:
以上这张图涉及到了前4步流程,只是封装RequestMappingInfo对象涉及到回调,下面看一下具体回调到了哪个方法中:
5、6、7步都是在遍历map的时候进行的操作,看一下具体的遍历流程:
进入这个方法内部,最终会调到 register方法:
在这个方法内部,还涉及到 CrossOrigin注解的处理逻辑,而这个逻辑是和跨域访问相关的,后面单独分析,此处跳过。
至此,整个映射关系就建立起来了。