我们平时在使用Spring MVC进行WEB开发的时候都会使用@Controller跟@RequestMapping注解定义控制器实体跟处理请求的方法的,让我们从@Controller跟@ReuqstMapping这两个注解开始就看Spring MVC工作的
从JavaDoc上获取有用的信息
我们先来看一下@Controller的JavaDoc吧,但因为从@Controller的JavaDoc上看不到什么有用的信息这里也就不贴它的代码了.从@Controller的JavaDoc上来可以知道以下信息:
- 表明这个类在WEB中是一个Controller
- Spring在扫描类的时候会把它按照@Component一样处理
- 和@RequestMapping注解配合使用
/**
* Annotation for mapping web requests onto methods in request-handling classes
* with flexible method signatures.
*
* <p>Both Spring MVC and Spring WebFlux support this annotation through a
* {@code RequestMappingHandlerMapping} and {@code RequestMappingHandlerAdapter}
* in their respective modules and package structure. For the exact list of
* supported handler method arguments and return types in each, please use the
* reference documentation links below:
* <ul>
* <li>Spring MVC
* <a href="https://docs.spring.io/spring/docs/current/spring-framework-reference/web.html#mvc-ann-arguments">Method Arguments</a>
* and
* <a href="https://docs.spring.io/spring/docs/current/spring-framework-reference/web.html#mvc-ann-return-types">Return Values</a>
* </li>
* </ul>
* @author Juergen Hoeller
* @author Arjen Poutsma
* @author Sam Brannen
* @since 2.5
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Mapping
public @interface RequestMapping
从@RequestMapping的JavaDoc可以知道Spring MVC是通过RequestMappingHandlerMapping跟RequestMappingHandlerAdapter来实现对@RequestMapping的支持的,以及对支持什么参数跟什么返回值(这些跟本文无关有兴趣的同学可以点击JavaDoc上的链接地址查看文档)
@RequestMapping跟@Controller的注解信息是如何被读取的
顺着上面的注解信息查看RequestMappingHandlerMapping类的JavaDoc发现一句比较重要的线索Creates RequestMappingInfo instances from type and method-level @RequestMapping annotations in @Controller classes(读取被@Controller标记的类下被@RequestMapping标记的方法的信息封装成@RequestInfo)
从RequestMappingHandlerMapping上找到以下相关方法
protected RequestMappingInfo getMappingForMethod(Method method, Class<?> handlerType) {
RequestMappingInfo info = createRequestMappingInfo(method);
if (info != null) {
RequestMappingInfo typeInfo = createRequestMappingInfo(handlerType);
if (typeInfo != null) {
info = typeInfo.combine(info);
}
String prefix = getPathPrefix(handlerType);
if (prefix != null) {
info = RequestMappingInfo.paths(prefix).options(this.config).build().combine(info);
}
}
return info;
}
private RequestMappingInfo createRequestMappingInfo(AnnotatedElement element) {
RequestMapping requestMapping = AnnotatedElementUtils.findMergedAnnotation(element, RequestMapping.class);
RequestCondition<?> condition = (element instanceof Class ?
getCustomTypeCondition((Class<?>) element) : getCustomMethodCondition((Method) element));
return (requestMapping != null ? createRequestMappingInfo(requestMapping, condition) : null);
}
private RequestMappingInfo createRequestMappingInfo(AnnotatedElement element) {
RequestMapping requestMapping = AnnotatedElementUtils.findMergedAnnotation(element, RequestMapping.class);
RequestCondition<?> condition = (element instanceof Class ?
getCustomTypeCondition((Class<?>) element) : getCustomMethodCondition((Method) element));
return (requestMapping != null ? createRequestMappingInfo(requestMapping, condition) : null);
}
从上面的代码Spring Mvc在读取@RequestMapping的步骤如下:
- 先分别从方法跟类上面的@RequestMapping注解信息封装成一个RequestMappingInfo 信息
1.1 RequestMappingInfo由多个RequestCondition组成,RequestCondition代表一个HttpServletRequest的匹配规则
1.2 RequestCondition有多种子类用于判断不同的类型,例如RequestMethodsRequestCondition就是判断Http请求的
1.3 两个RequestMappingInfo整合成一个的时候会把Spring 定义的RequestCondition会合并在一起,正因为这样在类上配置注解设配get并且方法上设置设配post的话这个方法可以处理get,post http请求
1.4 我们可以通过继承RequestMappingHandlerMapping类并从写getCustomTypeCondition跟getCustomMethodCondition来定制自己需要的匹配规则,在将它注入到Spring ioc时设置@Order注解配置适当的优先顺序让它在RequestMappingHandlerMapping前设配http - 将从方法跟类上读取到RequestMappingInfo整个一个
- 判断一个类是否将映射的url添加前缀,可以通过WebMvcConfigurer类的configurePathMatch方法进行配置,例如通过如下配置定义所有被@Wkx注解的Controller类都需要添加/wkx前缀
@Configuration
public class WebConfig implements WebMvcConfigurer
@Override
public void configurePathMatch(PathMatchConfigurer configurer) {
configurer.addPathPrefix("wkx", clazz -> clazz.isAnnotationPresent(Wkx.class));
}
}
RequestMappingHandlerMapping是在什么时候读取@RequestMapping信息
通过idea的Call Hierarchy的知道RequestMappingHandlerMapping会在其afterPropertiesSet生命周期读取的
public class AbstractHandlerMethodMapping {
protected void initHandlerMethods() {
// 1 获取Spring ApplicationContext中所有的装配Bean的名称
for (String beanName : getCandidateBeanNames()) {
if (!beanName.startsWith(SCOPED_TARGET_NAME_PREFIX)) {
// 2 对所有Bean读取@RequestMapping信息
processCandidateBean(beanName);
}
}
handlerMethodsInitialized(getHandlerMethods());
}
}
protected void processCandidateBean(String beanName) {
Class<?> beanType = null;
try {
beanType = obtainApplicationContext().getType(beanName);
}
catch (Throwable ex) {
if (logger.isTraceEnabled()) {
logger.trace("Could not resolve type for bean '" + beanName + "'", ex);
}
}
// 3 对所有被@Controller类读取@RequestMapping信息
if (beanType != null && isHandler(beanType)) {
detectHandlerMethods(beanName);
}
}
protected void detectHandlerMethods(Object handler) {
Class<?> handlerType = (handler instanceof String ?
obtainApplicationContext().getType((String) handler) : handler.getClass());
if (handlerType != null) {
Class<?> userType = ClassUtils.getUserClass(handlerType);
// 4 使用工具类读取所有方法上的@RequestMapping信息 ps:国产开源任务调度框架也是使用这个工具类来读取@JobHandler注解的
Map<Method, T> methods = MethodIntrospector.selectMethods(userType,
(MethodIntrospector.MetadataLookup<T>) method -> {
try {
// 5 终于轮到重头戏了上面说的RequestMappingHandlerMapping具体读取@RequestMapping信息
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);
// 6 注册前面读取的RequestMappingInfo存储起来
registerHandlerMethod(handler, invocableMethod, mapping);
});
}
}
}
将读取的RequestMappingInfo保存起来
从前面我们知道Spring mvc在启动的时候会读取@RequestMapping信息并存储到内存中,接下来我们看看是怎么存储起来的
RequestMappingHandlerMapping 会先调用其父类的registerHandlerMethod方法,其父类会把注册规则交给MappingRegistry实现
class MappingRegistry {
public void register(T mapping, Object handler, Method method) {
this.readWriteLock.writeLock().lock();
try {
HandlerMethod handlerMethod = createHandlerMethod(handler, method);
validateMethodMapping(handlerMethod, mapping);
this.mappingLookup.put(mapping, handlerMethod);
List<String> directUrls = getDirectUrls(mapping);
for (String url : directUrls) {
this.urlLookup.add(url, mapping);
}
String name = null;
if (getNamingStrategy() != null) {
name = getNamingStrategy().getName(handlerMethod, mapping);
addMappingName(name, handlerMethod);
}
CorsConfiguration corsConfig = initCorsConfiguration(handler, method, mapping);
if (corsConfig != null) {
this.corsLookup.put(handlerMethod, corsConfig);
}
this.registry.put(mapping, new MappingRegistration<>(mapping, handlerMethod, directUrls, name));
}
finally {
this.readWriteLock.writeLock().unlock();
}
}
}
从上面的源码我们注册流程如下
- 先将method跟相关类实例封装成一个HandlerMethod方便后续调用
- 以RequestMappingInfo为Key,HandlerMethod为Value映射到map中
- 以Url为Key,RequestMapping为多值Value映射到一个MultiValueMap中
- 以RequestMapping的name值为key,以HandlerMethod为Value映射到map中
- 获取读取方法上的类跟方法上的@CrossOrigin注解信息封装成CorsConfiguration,并以HandlerMethod为Key,CorsConfiguration为Value映射到一个map中
看到这里可以猜测Spring通过HttpServletRequest的url是如何找到相关HandlerMethod并来处理请求的.有点类似与有点类似与电商系统中根据订单号获取订单的商品信息的来进行发货的.假设订单只有一个商品.首先根据订单号(url)找到订单(ReuestMapping),多个订单(RequestMappingInfo)可以与一个商品(HandlerMethod)相关联,找到商品(HandlerMethod)之后查看是否有物流发货配置(CorsConfiguration)某些获取像生鲜只能走冷链