03-HandlerMapping

2022-07-18

03-HandlerMapping

springmvc之HandlerMapping

一 概述

HandlerMapping的作用是根据request找到对应的处理器Handler

@Nullable
HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception;

二. 源码分析

HandlerMapping家族成员如下所示:

image-20210603165935316

通常我们使用的是RequestMappingHandlerMapping

接下来分析下整个家族的实现方式,另外一条继承线路AbstractUrlHandlerMapping不常使用,不做分析

2.1 AbstractHandlerMapping

2.1.1 创建

该类继承了WebApplicationObjectSupport,因此在初始化时会自动调用initApplicationContext()

@Override
protected void initApplicationContext() throws BeansException {
  extendInterceptors(this.interceptors);
  detectMappedInterceptors(this.adaptedInterceptors);
  initInterceptors();
}
  • extendInterceptors()是模板方法,用于给子类提供一个添加interceptors的入口

  • detectMappedInterceptors()用于将springmvc容器和父容器中所有MappedInterceptor类型的Bean添加到adaptedInterceptors属性

    protected void detectMappedInterceptors(List<HandlerInterceptor> mappedInterceptors) {
    		mappedInterceptors.addAll(
    				BeanFactoryUtils.beansOfTypeIncludingAncestors(
    						obtainApplicationContext(), MappedInterceptor.class, true, false).values());
    	}
    

    很简单的一段代码,利用spring找到所有MappedInterceptor类型的类,加入到adaptedInterceptors

  • initInterceptors()

    将第一步中找到的Interceptor添加到adaptedInterceptors中

    protected void initInterceptors() {
    	if (!this.interceptors.isEmpty()) {
    		for (int i = 0; i < this.interceptors.size(); i++) {
    			Object interceptor = this.interceptors.get(i);
    			if (interceptor == null) {
    				throw new IllegalArgumentException("Entry number " + i + " in interceptors array is null");
    			}
    			this.adaptedInterceptors.add(adaptInterceptor(interceptor));
    		}
    	}
    }
    
    protected HandlerInterceptor adaptInterceptor(Object interceptor) {
    	if (interceptor instanceof HandlerInterceptor) {
    		return (HandlerInterceptor) interceptor;
    	}
    	else if (interceptor instanceof WebRequestInterceptor) {
    		return new WebRequestHandlerInterceptorAdapter((WebRequestInterceptor) interceptor);
    	}
    	else {
    		throw new IllegalArgumentException("Interceptor type not supported: " + interceptor.getClass().getName());
    	}
    }
    

    小技巧:

    从这里可以看到,拦截器除了可以实现HandlerInterceptor之外,还可以实现WebRequestInterceptor接口

2.1.2 使用

由前面springmvc执行请求的流程可以看到,DispatchServlet是调用getHandler()方法来根据request找到对应的Handler的

public final HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception {
	Object handler = getHandlerInternal(request);
	if (handler == null) {
		handler = getDefaultHandler();
	}
	if (handler == null) {
		return null;
	}
	// Bean name or resolved handler?
	if (handler instanceof String) {
		String handlerName = (String) handler;
		handler = obtainApplicationContext().getBean(handlerName);
	}

	HandlerExecutionChain executionChain = getHandlerExecutionChain(handler, request);

	if (hasCorsConfigurationSource(handler)) {
		CorsConfiguration config = (this.corsConfigurationSource != null ? this.corsConfigurationSource.getCorsConfiguration(request) : null);
		CorsConfiguration handlerConfig = getCorsConfiguration(handler, request);
		config = (config != null ? config.combine(handlerConfig) : handlerConfig);
		executionChain = getCorsHandlerExecutionChain(request, executionChain, config);
	}

	return executionChain;
}
  1. getHandlerInternal()是一个模板方法,具体的寻找过程由子类去完成
  2. 如果没有找到,使用默认的Handler,这里为null
  3. 如果handler是一个String类型的,从容器中根据名称找到对应的类
  4. 然后将Handler转为HandlerExecutionChain,并且将拦截器也都加进去
protected HandlerExecutionChain getHandlerExecutionChain(Object handler, HttpServletRequest request) {
HandlerExecutionChain chain = (handler instanceof HandlerExecutionChain ?
		(HandlerExecutionChain) handler : new HandlerExecutionChain(handler));

String lookupPath = this.urlPathHelper.getLookupPathForRequest(request, LOOKUP_PATH);
for (HandlerInterceptor interceptor : this.adaptedInterceptors) {
	if (interceptor instanceof MappedInterceptor) {
		MappedInterceptor mappedInterceptor = (MappedInterceptor) interceptor;
		if (mappedInterceptor.matches(lookupPath, this.pathMatcher)) {
			chain.addInterceptor(mappedInterceptor.getInterceptor());
		}
	}
	else {
		chain.addInterceptor(interceptor);
	}
}
return chain;
}

HandlerExecutionChain中封装了Handler和interceptors,都用来对请求进行处理,这里在对拦截器处理的时候,如果是MappedInterceptor类型的,需要对路径进行匹配

由此可见,MappedInterceptor也就是对拦截器又进行了一层封装,可以对拦截器作用的路径进行定制,而不是所有的请求

  1. 最后,是对跨域资源的处理

    首先检测handler是否有跨域资源,如果handler是CorsConfigurationSource类型的或者注册信息有CorsConfiguration,即有@CrossOrigin

    @Override
    protected boolean hasCorsConfigurationSource(Object handler) {
      return super.hasCorsConfigurationSource(handler) ||
        (handler instanceof HandlerMethod && this.mappingRegistry.getCorsConfiguration((HandlerMethod) handler) != null) ||
        handler.equals(PREFLIGHT_AMBIGUOUS_MATCH);
    }
    
    protected boolean hasCorsConfigurationSource(Object handler) {
      if (handler instanceof HandlerExecutionChain) {
        handler = ((HandlerExecutionChain) handler).getHandler();
      }
      return (handler instanceof CorsConfigurationSource || this.corsConfigurationSource != null);
    }
    
    

    如果是跨域资源,将CorsInterceptor加到拦截器的第一个位置

    chain.addInterceptor(0, new CorsInterceptor(config));
    

    小技巧:

    从这里的分析可以看出,由于我们处理静态资源所使用的

    @Override
        public void addResourceHandlers(ResourceHandlerRegistry registry) {
            registry.addResourceHandler("/**").addResourceLocations("classpath:/static/");
        }
    

    实际上是注入了ResourceHttpRequestHandler对象,这个类是CorsConfigurationSource的子类,因此,放在/static/文件夹下的资源可以跨域

2.2 AbstractHandlerMethodMapping

2.2.1 创建

该类是将Method作为Handler来使用的,这也是我们现在用得最多的一种Handler,比如经常使用的@RequestMapping所注释的方法就是这种Handler,它专门有一个类型——HandlerMethod,也就是Method类型的Handler。

实现了InitializingBean接口,所以spring容器会自动调用其afterPropertiesSet方法

@Override
	public void afterPropertiesSet() {
		initHandlerMethods();
	}
protected void initHandlerMethods() {
  for (String beanName : getCandidateBeanNames()) {
    if (!beanName.startsWith(SCOPED_TARGET_NAME_PREFIX)) {
      processCandidateBean(beanName);
    }
  }
  handlerMethodsInitialized(getHandlerMethods());
}

protected String[] getCandidateBeanNames() {
  return (this.detectHandlerMethodsInAncestorContexts ?
          BeanFactoryUtils.beanNamesForTypeIncludingAncestors(obtainApplicationContext(), Object.class) :
          obtainApplicationContext().getBeanNamesForType(Object.class));
}

首先找到容器中所有的类(根据detectHandlerMethodsInAncestorContexts决定是否查找父容器,默认为false)

然后根据一定的规则对找到的类进行筛选,将筛选出来的类保存到相应的Map中

protected void processCandidateBean(String beanName) {
		Class<?> beanType = null;
		beanType = obtainApplicationContext().getType(beanName);
		if (beanType != null && isHandler(beanType)) {
			detectHandlerMethods(beanName);
		}
	}

这里isHandler()是一个模板方法,由子类去实现如何判断类是一个符合条件的Handler

protected void detectHandlerMethods(Object handler) {
  Class<?> handlerType = (handler instanceof String ?
                          obtainApplicationContext().getType((String) handler) : handler.getClass());

  if (handlerType != null) {
    //返回用户自定义的类,对于代理类也返回原始对象
    Class<?> userType = ClassUtils.getUserClass(handlerType);
    Map<Method, T> methods = MethodIntrospector.selectMethods(userType,
                                                              (MethodIntrospector.MetadataLookup<T>) method -> {
                                                                try {
                                                                  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);
      registerHandlerMethod(handler, invocableMethod, mapping);
    });
  }
}

detectHandlerMethods()这个方法主要完成了以下几件事:

  1. 如果handler是String类型的,那么找到类名对应的Class对象,否则直接返回Class对象

  2. 返回用户自定义的类,对于代理类也返回原始对象

  3. 对该类中所有的方法进行遍历,根据getMappingForMethod()方法进行筛选,返回{方法:匹配条件类}的map

    这里的泛型T表示 匹配Handler的条件专门使用的一种类,在RequestMappingHandlerMapping中使用的是RequestMappingInfo

  4. 然后对符合条件的Method进行遍历,进行注册,registerHandlerMethod也是一个模板方法,由子类实现

2.2.2 使用

AbstractHandlerMapping中的模板方法getHandlerInternal由子类实现

@Override
	protected HandlerMethod getHandlerInternal(HttpServletRequest request) throws Exception {
		String lookupPath = getUrlPathHelper().getLookupPathForRequest(request);
		request.setAttribute(LOOKUP_PATH, lookupPath);
		this.mappingRegistry.acquireReadLock();
		try {
			HandlerMethod handlerMethod = lookupHandlerMethod(lookupPath, request);
			return (handlerMethod != null ? handlerMethod.createWithResolvedBean() : null);
		}
		finally {
			this.mappingRegistry.releaseReadLock();
		}
	}

这个方法主要做了以下几件事情:

  1. 根据request获取lookupPath,可以简单地理解为url

  2. 将路径属性设置到request

  3. 根据lookupPath和request找到对应处理的HandlerMethod

  4. 如果找到了,使用createWithResolvedBean()创建新的HandlerMethod并返回

    public HandlerMethod createWithResolvedBean() {
    		Object handler = this.bean;
    		if (this.bean instanceof String) {
    			Assert.state(this.beanFactory != null, "Cannot resolve bean name without BeanFactory");
    			String beanName = (String) this.bean;
    			handler = this.beanFactory.getBean(beanName);
    		}
    		return new HandlerMethod(this, handler);
    	}
    

    判断handlerMethod里的handler是不是String类型,如果是则改为将其作为beanName从容器中所取到的bean,不过HandlerMethod里的属性都是final类型的,不可以修改,所以在createWithResolvedBean方法中又用原来的属性和修改后的handler新建了一个HandlerMethod。

2.3 RequestMappingHandlerMapping

这个类就是我们最常用的HandlerMapping,根据上面的分析,该类主要实现了父类的几个模板方法,来完成基于request查找Handler的功能

2.3.1 isHandler()

protected boolean isHandler(Class<?> beanType) {
  return (AnnotatedElementUtils.hasAnnotation(beanType, Controller.class) ||
          AnnotatedElementUtils.hasAnnotation(beanType, RequestMapping.class));
}

判断是否是一个Handler,即有@Controller或者@RequestMapping注解

2.3.2 getMappingForMethod()

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).build().combine(info);
    }
  }
  return info;
}

根据method和所在类返回对应的RequestMappingInfo信息

RequestMappingInfo封装多种映射信息,主要有以下几个属性:

image-20210604161934614

image-20210604161824187

分别对应@RequestMapping不同的属性

image-20210604162542805

  • name(): 映射的名字,可以不指定

  • value(),path()都是代表请求路径url,路径信息会被封装到PatternsRequestCondition

  • method()代表支持的请求方式,可以多种,例如RequestMethod.GET,该信息被封装到RequestMethodsRequestCondition

  • params()代表支持的参数值,例如params = "test!=mhn",表示参数名称为test的属性,不接受值为mhn请求

    如果test传了mhn,就会抛出UnsatisfiedServletRequestParameterException,大小写敏感

  • headers() 代表支撑的请求头,大小写不敏感

    params 和 headers 相同点均有以下三种表达式格式:

    1. !param1: 表示允许不含有 param1 请求参数/请求头参数(以下简称参数)
    2. param2!=value2:表示允许不包含 param2 或者 虽然包含 param2 参数但是值不等于 value2;不允许包含param2参数且值等于value2
    3. param3=value3:表示需要包含 param3 参数且值等于 value3

    表达式的逻辑解析见AbstractNameValueExpression

  • consumes()代表设置允许映射的ContentType,例如MediaType.TEXT_PLAIN_VALUE,也可以使用表达式

  • produces()其功能有两个

    功能1:当请求头中Accept的value与produces()配置的属性匹配上,则进行映射,否则返回客户端HTTP 406(Not Acceptable)响应,或415 unsupported mediaType

    功能2:默认会把produces中配置的内容写到响应头的Content-Type中去

  • consumers和produces都有正反 2 种表达式

下面是一个完整的带参数的@RequestMapping

@RequestMapping(name = "Mahaonan", value = "/test3", method = RequestMethod.GET, params = "test!=mhn",
                headers = "User-Agent=Mozilla/5.0 (Macintosh; Intel Mac OS X 11_2_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.72 Safari/537.36",
                consumes = "text/plain",
                produces = MediaType.APPLICATION_JSON_VALUE)
@ResponseBody
public ResponseResult<String> test3(String test,) {
  return ResponseResult.ok("success");
}

这个名为Mahaonan的方法,映射路径为/test3,请求类型的为GET,test参数的值不能为mhn,请求头中只接受User-Agent为指定浏览器,并且请求Content-Type为text/plain,accept中必须包含application/json,并且会产生该类型的数据

2.3.3 registerHandlerMethod()

@Override
protected void registerHandlerMethod(Object handler, Method method, RequestMappingInfo mapping) {
  super.registerHandlerMethod(handler, method, mapping);
  updateConsumesCondition(mapping, method);
}

首先调用了父类AbstractHandlerMethodMapping的注册方法

public void register(T mapping, Object handler, Method method) {
  // Assert that the handler method is not a suspending one.
  if (KotlinDetector.isKotlinType(method.getDeclaringClass()) && KotlinDelegate.isSuspend(method)) {
    throw new IllegalStateException("Unsupported suspending handler method detected: " + 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();
  }
}

整个方法其实就是给不同的map放入对应的属性,下面看看注册信息中封装的几种map及其作用

class MappingRegistry {

		private final Map<T, MappingRegistration<T>> registry = new HashMap<>();

		private final Map<T, HandlerMethod> mappingLookup = new LinkedHashMap<>();

		private final MultiValueMap<String, T> urlLookup = new LinkedMultiValueMap<>();

		private final Map<String, List<HandlerMethod>> nameLookup = new ConcurrentHashMap<>();

		private final Map<HandlerMethod, CorsConfiguration> corsLookup = new ConcurrentHashMap<>();
}
  • mappingLookup: 根据映射信息查找对应Handler的map,key-> RequestMappingInfo,即映射信息;value->HandlerMethod

  • urlLookup:根据请求路径找到对应的映射信息的map,key->请求路径;value-> 映射信息

    由于@RequestMapping的value可以为多个值,即多个路径映射同一个方法,因此这里是一个MultiValueMap

  • nameLookup:根据name找到对应的handler,一个name可能对应多个Handler

  • corsLookup:根据HandlerMethod找到对应的跨域配置

  • registry:总的map,根据映射信息找到对应映射注册类

接下来,调用了updateConsumesCondition(mapping, method);

private void updateConsumesCondition(RequestMappingInfo info, Method method) {
		ConsumesRequestCondition condition = info.getConsumesCondition();
		if (!condition.isEmpty()) {
			for (Parameter parameter : method.getParameters()) {
				MergedAnnotation<RequestBody> annot = MergedAnnotations.from(parameter).get(RequestBody.class);
				if (annot.isPresent()) {
					condition.setBodyRequired(annot.getBoolean("required"));
					break;
				}
			}
		}
	}

对于consumers属性,专门判断方法参数上是否有@RequestBody注解,如果有,给consumers条件设置 bodyRequired

2.4 SimpleUrlHandlerMapping

这种HandlerMapping是基于另一体系继承链的,父类我们不做分析,这里简单看下其所起的作用

SimpleUrlHandlerMapping主要是为静态资源提供Handler

首先来看看默认情况下,springboot会给容器中注入的HandlerMapping

image-20210608150454757

可以看到SimpleUrlHandlerMapping处于最后一个位置,这个可以从注入的时候看出来

DispatchServlet在调用getHandler方法时,会依次遍历容器中的HandlerMapping,由于静态资源在前面的几个中都找不到对应的Handler,因此就会来到最后的Simple

这个HandlerMapping中存放的映射关系是ResourceHttpRequestHandler类型的,这个Handler即处理静态资源的handler

@Bean
	@Nullable
	public HandlerMapping resourceHandlerMapping(
			@Qualifier("mvcUrlPathHelper") UrlPathHelper urlPathHelper,
			@Qualifier("mvcPathMatcher") PathMatcher pathMatcher,
			@Qualifier("mvcContentNegotiationManager") ContentNegotiationManager contentNegotiationManager,
			@Qualifier("mvcConversionService") FormattingConversionService conversionService,
			@Qualifier("mvcResourceUrlProvider") ResourceUrlProvider resourceUrlProvider) {

		Assert.state(this.applicationContext != null, "No ApplicationContext set");
		Assert.state(this.servletContext != null, "No ServletContext set");

		ResourceHandlerRegistry registry = new ResourceHandlerRegistry(this.applicationContext,
				this.servletContext, contentNegotiationManager, urlPathHelper);
		addResourceHandlers(registry);

		AbstractHandlerMapping handlerMapping = registry.getHandlerMapping();
		if (handlerMapping == null) {
			return null;
		}

springboot在处理mvc配置的时候都大同小异,都是利用Registry进行注册,把我们在mvcConfig中配置的内容转为对应的类,对于静态资源而言,起作用的是ResourceHandlerRegistry

@Override
    public void addResourceHandlers(ResourceHandlerRegistry registry) {
        registry.addResourceHandler("/**").addResourceLocations("classpath:/static/");
    }

registry.getHandlerMapping();这个返回的就是SimpleUrlHandlerMapping

@Nullable
protected AbstractHandlerMapping getHandlerMapping() {
  if (this.registrations.isEmpty()) {
    return null;
  }

  Map<String, HttpRequestHandler> urlMap = new LinkedHashMap<>();
  for (ResourceHandlerRegistration registration : this.registrations) {
    for (String pathPattern : registration.getPathPatterns()) {
      ResourceHttpRequestHandler handler = registration.getRequestHandler();
      if (this.pathHelper != null) {
        handler.setUrlPathHelper(this.pathHelper);
      }
      if (this.contentNegotiationManager != null) {
        handler.setContentNegotiationManager(this.contentNegotiationManager);
      }
      handler.setServletContext(this.servletContext);
      handler.setApplicationContext(this.applicationContext);
      try {
        handler.afterPropertiesSet();
      }
      catch (Throwable ex) {
        throw new BeanInitializationException("Failed to init ResourceHttpRequestHandler", ex);
      }
     
      urlMap.put(pathPattern, handler);
    }
  }

  return new SimpleUrlHandlerMapping(urlMap, this.order);
}

将我们填写的静态资源路径,例如class:/static/作为key,ResourceHttpRequestHandler作为value放入SimpleUrlHandlerMpping中保存。后续在有符合条件的静态资源进来时,就会找到该Handler进行处理

小技巧:

设置了这个以后,访问静态资源时不需要带有static路径,例如static/1.js

请求路径为: localhost:8080/1.js

三. 使用技巧

3.1 MappedInterceptor

MappedInterceptor类型的拦截器是一种增加对请求路径判断的拦截器,可以对拦截器进行定制路径,表示该拦截器只作用的此路径的请求。

但是从MappedInterceptor的源码可以看到,该类是一个final类型的,意味着我们并不能直接继承该类来往容器中注入该类型的类型,使detectMappedInterceptors能够侦测到该类型的拦截器。

因此,在springboot中,通过另一种方式,实现了这种拦截器的注入。我们通常在mvcConfig中配置的拦截器都是属于这种

@Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new CreateSessionHandlerInterceptor());
        registry.addInterceptor(new CheckConfigHandlerInterceptor(sysConfigService)).addPathPatterns("/**");
    }

通过这个方法添加的拦截器,如果有addPathPatterns都会转成MappedInterceptor类型

springboot通过InterceptorRegistry这个拦截器注册器实现了MappedInterceptor的注入,下面看看springboot是如何注入的

WebMvcConfigurationSupport自动注入RequestMappingHandlerMapping

@Bean
	public RequestMappingHandlerMapping requestMappingHandlerMapping(
			@Qualifier("mvcContentNegotiationManager") ContentNegotiationManager contentNegotiationManager,
			@Qualifier("mvcConversionService") FormattingConversionService conversionService,
			@Qualifier("mvcResourceUrlProvider") ResourceUrlProvider resourceUrlProvider) {

		RequestMappingHandlerMapping mapping = createRequestMappingHandlerMapping();
		mapping.setOrder(0);
		mapping.setInterceptors(getInterceptors(conversionService, resourceUrlProvider));
		//....

		return mapping;
	}

其他的配置不做分析,这里重点看看getInterceptors

protected final Object[] getInterceptors(
			FormattingConversionService mvcConversionService,
			ResourceUrlProvider mvcResourceUrlProvider) {
		if (this.interceptors == null) {
			InterceptorRegistry registry = new InterceptorRegistry();
			addInterceptors(registry);
			registry.addInterceptor(new ConversionServiceExposingInterceptor(mvcConversionService));
			registry.addInterceptor(new ResourceUrlProviderExposingInterceptor(mvcResourceUrlProvider));
			this.interceptors = registry.getInterceptors();
		}
		return this.interceptors.toArray();
	}

这个方法的addInterceptors(registry)是注入拦截器的关键

//DelegatingWebMvcConfiguration
@Override
protected void addInterceptors(InterceptorRegistry registry) {
  this.configurers.addInterceptors(registry);
}
//WebMvcConfigurerComposite
private final List<WebMvcConfigurer> delegates = new ArrayList<>();
@Override
public void addInterceptors(InterceptorRegistry registry) {
  for (WebMvcConfigurer delegate : this.delegates) {
    delegate.addInterceptors(registry);
  }
}

这里的delegates即包括我们自定义的

@EnableWebMvc
public class MvcConfig implements WebMvcConfigurer{}

image-20210608114619679

到这里就会执行我们上述添加拦截器的方法

然后this.interceptors = registry.getInterceptors();这个方法,会把我们注入的拦截器转成MappedInterceptor类型

protected Object getInterceptor() {
		if (this.includePatterns.isEmpty() && this.excludePatterns.isEmpty()) {
			return this.interceptor;
		}

		String[] include = StringUtils.toStringArray(this.includePatterns);
		String[] exclude = StringUtils.toStringArray(this.excludePatterns);
		MappedInterceptor mappedInterceptor = new MappedInterceptor(include, exclude, this.interceptor);
		if (this.pathMatcher != null) {
			mappedInterceptor.setPathMatcher(this.pathMatcher);
		}
		return mappedInterceptor;
	}

可以看到如果有路径参数,new一个MappedInterceptor,重新包装加入了路径参数

由此分析可以知道:

我们实现WebMvcConfigurer接口重写addInterceptors()添加的拦截器,springboot在注入RequestMappingHandlerMapping的时候添加进去,然后如果有路径参数,会重新包装为MappedInterceptor

3.2 WebRequestInterceptor

3.3 自定义HandlerMapping

3.4 CorsConfiguration

跨域请求: 当前发起请求的域与该请求指向的资源所在的域不一样。

这里的域指的是这样的一个概念:我们认为若协议 + 域名 + 端口号均相同,那么就是同域

这里讨论的仅是资源的访问,例如ajax中请求另一个ip的资源,不包括cookie,session的跨域共享

CorsConfiguration起作用的是@CrossOrigin注解,用在类或者方法上,表示此Handler允许访问的策略

image-20210607193004362

  • origins:允许访问的域名,可以配置多个,*代表允许全部,配置后会给response设置一个header,Access-Control-Allow-Origin: *
  • allowedHeaders:允许访问的请求头,*代表允许全部,配置后会给response设置一个header,Access-Control-Allow-Headers
  • exposedHeaders:服务端暴露的请求头,一般用来自定义配置header,Access-Control-Expose-Headers
  • methods:允许访问的请求方式,例如Get,Post
  • allowCredentials:是否允许cookie随请求发送,使用时必须指定具体的域,Access-Control-Allow-Credentials
  • maxAge:预请求的结果的有效期,默认30分钟,Access-Control-Max-Age

前端代码:

<script>
    $(function () {
        console.log(123)
        $.ajax({
            type: 'post',
            url: "http://10.2.47.101:8081/source/test1",
            success: function (res) {
                console.log(res);
            }
        })
    })
</script>

本域为localhost,如果在ajax中发起跨域请求,如果不指定CrossOrigin,那么响应会被浏览器拦截

image-20210607195235278

这时候就需要在服务器做跨域配置:

  1. 指定某个方法的跨域配置:
@CrossOrigin(origins = "*", allowedHeaders = "*", methods = RequestMethod.GET, maxAge = 1800)
    public String test1() {}
  1. 全局的跨域配置:
@Configuration
public class MyConfiguration {

    @Bean
    public WebMvcConfigurer corsConfigurer() {
        return new WebMvcConfigurerAdapter() {
            @Override
            public void addCorsMappings(CorsRegistry registry) {
                registry.addMapping("/**");
            }
        };
    }
}

其他知识:

需要注意,cookie,session 的跨域,这种方式并不行。

对于tomcat而言,session对应的JSESSIONID是由tomcat维护并管理的,在springboot2.0之后貌似不支持修改domain。而且对于cookies和session而言,只能作用于本域,最多是二级域名扩展到一级域名,并不能跨域。

要解决cookie,session的跨域共享,可以使用spring session技术。

四. 总结

4.1 追踪请求

4.2 HandlerMapping杂谈

​ HandlerMapping在mvc中的作用就是通过request找到对应的Handler,为了完成不同的功能,以及提供多样的匹配方式,整个HandlerMapping又分为两大家族,我们通常使用的基于@RequestMapping的Handler使用到了ReqeustMappingHandlerMapping,而静态资源使用的Handler是ResourceHttpRequestHandler内置的,对应的HandlerMapping为SimpleUrHandlerMapping

​ ReqeustMappingHandlerMapping的创建初始化则从父类一步步开始

  1. 往容器中注入了拦截器,这是在AbstractHandlerMapping中完成的

  2. 找到容器中所有的bean,根据isHandler判断是否是我们需要的Handler,把符合条件的Handler,以及对应的映射信息保存在HandlerMapping中供后续使用。

  3. ReqeustMappingHandlerMapping中则提供了具体的逻辑,

    • 包括isHandler的判断;
    • 如何根据method找到对应的映射信息;(即对@RequestMapping注解的解析)
    • 如何注册映射信息;(即把对应的映射信息保存到HandlerMapping中)

这样,所有的相关信息就保存好了,那么,在使用时,只需要根据DispatchServlet中的getHandler依次查找即可,只要找到一个匹配的Handler就不会继续找了,返回给下一步使用。


标题:03-HandlerMapping
作者:mahaonan
地址:https://mahaonan.fun/articles/2022/07/18/1658147011966.html