3、AbstractHandlerMethodMapping

AbstractHandlerMethodMapping是处理HandlerMethod的HandlerMapping,其中有一个实现了是我们常用的
它就是RequestMappingHandlerMapping

我们直接看到RequestMappingHandlerMapping的类图

在这里插入图片描述
这里大部分接口我们在spring源码分析的时候已经解释过了,这里解释一下那些没有分析过的接口的作用

  • EmbeddedValueResolverAware 在bean进行初始化时设置StringValueResolver,这个StringValueResolver是一个占位符解析器
  • ApplicationObjectSupport 实现了ApplicationContextAware,额外提供了获取MessageSource(用于国际化)的方法
  • HandlerMapping 获取handler和能够用于拦截找到的handler的拦截器,然后组成HandlerExecutionChain

几个实现类的分析

  • AbstractHandlerMapping
public abstract class AbstractHandlerMapping extends WebApplicationObjectSupport
		implements HandlerMapping, Ordered {

	private int order = Integer.MAX_VALUE;  // default: same as non-Ordered

	private Object defaultHandler;
    //构建urlPathHelper
	private UrlPathHelper urlPathHelper = new UrlPathHelper();
    //AntPathMatcher,这个类我们在分析spring源码的时候进行分析过
	private PathMatcher pathMatcher = new AntPathMatcher();

	private final List<Object> interceptors = new ArrayList<Object>();

	private final List<HandlerInterceptor> adaptedInterceptors = new ArrayList<HandlerInterceptor>();
    //默认跨域处理器
	private CorsProcessor corsProcessor = new DefaultCorsProcessor();
    //基于url的跨域配置源
	private final UrlBasedCorsConfigurationSource corsConfigSource = new UrlBasedCorsConfigurationSource();
	
	。。。。。。
}
public final HandlerExecutionChain getHandler(PortletRequest request) throws Exception {
        //抽象方法,有子类实现
		Object handler = getHandlerInternal(request);
		//如果没有获取到,尝试使用默认的handler,一般不设置
		if (handler == null) {
			handler = getDefaultHandler();
		}
		if (handler == null) {
			return null;
		}
		// Bean name or resolved handler?
		//如果是字符串类型,那么从容器中获取
		if (handler instanceof String) {
			String handlerName = (String) handler;
			handler = getApplicationContext().getBean(handlerName);
		}
		//将获取到的handler和拦截器构建成HandlerExecutionChain
		return getHandlerExecutionChain(handler, request);
	}
  • AbstractHandlerMethodMapping 这个抽象类实现了InitializingBean接口,让我们来看看它的afterPropertiesSet做了什么
//这个方法被子类RequestMappingHandlerMapping覆盖
public void org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping.afterPropertiesSet() {
        //构建配置
		this.config = new RequestMappingInfo.BuilderConfiguration();
		this.config.setPathHelper(getUrlPathHelper());
		this.config.setPathMatcher(getPathMatcher());
		this.config.setSuffixPatternMatch(this.useSuffixPatternMatch);
		this.config.setTrailingSlashMatch(this.useTrailingSlashMatch);
		this.config.setRegisteredSuffixPatternMatch(this.useRegisteredSuffixPatternMatch);
		this.config.setContentNegotiationManager(getContentNegotiationManager());
        
		super.afterPropertiesSet();
	}
	
	public void AbstractHandlerMethodMapping.afterPropertiesSet() {
		initHandlerMethods();
	}

//初始化HandlerMethod
protected void initHandlerMethods() {
		if (logger.isDebugEnabled()) {
			logger.debug("Looking for request mappings in application context: " + getApplicationContext());
		}
		//从容器中获取所有beanName,detectHandlerMethodsInAncestorContexts表示是否需要获取父容器的beanName
		//默认是false,所以一般我们配置的Controller都是在SpringMVC加载配置文件中
		String[] beanNames = (this.detectHandlerMethodsInAncestorContexts ?
				BeanFactoryUtils.beanNamesForTypeIncludingAncestors(getApplicationContext(), Object.class) :
				getApplicationContext().getBeanNamesForType(Object.class));

		for (String beanName : beanNames) {
		    //是否以scopedTarget.为前缀,这个scopedTarget.表示范围对象代理,比如request,session等这些scope的对象
		    //如果我们要把短生命周期的注册到长生命周期的对象中,就应当使用代理,否则就变成和长生命周期的对象一样长的寿命
		    //这里不考虑这类被代理过的Controller
			if (!beanName.startsWith(SCOPED_TARGET_NAME_PREFIX)) {
				Class<?> beanType = null;
				try {
				    //获取beanName对应bean的类型
					beanType = getApplicationContext().getType(beanName);
				}
				catch (Throwable ex) {
					// An unresolvable bean type, probably from a lazy bean - let's ignore it.
					if (logger.isDebugEnabled()) {
						logger.debug("Could not resolve target class for bean with name '" + beanName + "'", ex);
					}
				}
				//判断当前类是否是需要的handler,isHandler方法被子类RequestMappingHandlerMapping实现
				//其判断逻辑就一行代码
				//((AnnotationUtils.findAnnotation(beanType, Controller.class) != null) ||
				//(AnnotationUtils.findAnnotation(beanType, RequestMapping.class) != null))
				//类是否被@Controller和@RequestMapping两个注解标注
				if (beanType != null && isHandler(beanType)) {
				    //装配HanderMethod
					detectHandlerMethods(beanName);
				}
			}
		}
		//获取到handlerMethod的后置处理方法,为空方法。
		handlerMethodsInitialized(getHandlerMethods());
	}

装配HanderMethod

protected void detectHandlerMethods(final Object handler) {
        //获取对应handler的类型
		Class<?> handlerType = (handler instanceof String ?
				getApplicationContext().getType((String) handler) : handler.getClass());
		final Class<?> userType = ClassUtils.getUserClass(handlerType);
        //筛选@RequestMapping注解注释的方法
		Map<Method, T> methods = MethodIntrospector.selectMethods(userType,
				new MethodIntrospector.MetadataLookup<T>() {
					@Override
					public T inspect(Method method) {
					    //获取@RequestMapp注解对应的映射信息,由子类实现
					    //RequestMappingHandlerMapping返回的是RequestMappingInfo
						return getMappingForMethod(method, userType);
					}
				});

		if (logger.isDebugEnabled()) {
			logger.debug(methods.size() + " request handler methods found on " + userType + ": " + methods);
		}
		for (Map.Entry<Method, T> entry : methods.entrySet()) {
		    //注册HandlerMethod
			registerHandlerMethod(handler, entry.getKey(), entry.getValue());
		}
	}
  • RequestMappingHandlerMapping

筛选HandlerMethod

public static <T> Map<Method, T> org.springframework.core.MethodIntrospector.selectMethods(Class<?> targetType, final MetadataLookup<T> metadataLookup) {
		final Map<Method, T> methodMap = new LinkedHashMap<Method, T>();
		Set<Class<?>> handlerTypes = new LinkedHashSet<Class<?>>();
		Class<?> specificHandlerType = null;
        //判断当前类是否被代理过,如果没有被代理,那么当前类也应当考虑进去
		if (!Proxy.isProxyClass(targetType)) {
			handlerTypes.add(targetType);
			specificHandlerType = targetType;
		}
		//添加接口
		handlerTypes.addAll(Arrays.asList(targetType.getInterfaces()));
        
		for (Class<?> currentHandlerType : handlerTypes) {
		    //设置目标类,用于获取最具体的方法(比如最终的那个覆盖方法)
			final Class<?> targetClass = (specificHandlerType != null ? specificHandlerType : currentHandlerType);

			ReflectionUtils.doWithMethods(currentHandlerType, new ReflectionUtils.MethodCallback() {
				@Override
				public void doWith(Method method) {
				    //从targetClass获取最具体的方法,比如接口的方法可能被targetClass覆盖
					Method specificMethod = ClassUtils.getMostSpecificMethod(method, targetClass);
					
					
					//(*1*)
					T result = metadataLookup.inspect(specificMethod);
					if (result != null) {
					    //获取被桥的方法(被桥的方法才是用户真正的定义的方法,桥方法的解释在spring源码分析的时候已经解释过了)
						Method bridgedMethod = BridgeMethodResolver.findBridgedMethod(specificMethod);
						//如果被桥方法就是当前具体的方法或者这个被桥的方法就是HandlerMethod方法,那么保存数据
						if (bridgedMethod == specificMethod || metadataLookup.inspect(bridgedMethod) == null) {
							methodMap.put(specificMethod, result);
						}
					}
				}
				//一个类过滤器,过滤掉桥方法和Object类定义的方法
			}, ReflectionUtils.USER_DECLARED_METHODS);
		}

		return methodMap;
	}
	
		//(*1*)
	protected RequestMappingInfo org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping.getMappingForMethod(Method method, Class<?> handlerType) {
	    //创建方法请求映射信息
		RequestMappingInfo info = createRequestMappingInfo(method);
		if (info != null) {
		    //如果类上面也有@RequestMapping注解,那么需要创建类请求映射信息
			RequestMappingInfo typeInfo = createRequestMappingInfo(handlerType);
			if (typeInfo != null) {
			    //进行合并
				info = typeInfo.combine(info);
			}
		}
		//返回最终映射信息
		return info;
	}

创建RequestMappingInfo

private RequestMappingInfo org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping.createRequestMappingInfo(AnnotatedElement element) {
        //获取RequestMapping注解,注意这里获取的RequestMapping注解类型是一个代理对象,主要是因为spring会对元数据进行处理,
        //元数据被当成注解的父类,他们的属性会被继承,覆盖,AnnotatedElementUtils在spring源码分析中分析过
		RequestMapping requestMapping = AnnotatedElementUtils.findMergedAnnotation(element, RequestMapping注解类型可能.class);
		//获取自定义的请求条件,我们可以继承这个RequestMappingHandlerMapping,然后实现这个方法并注册到spring容器中
		RequestCondition<?> condition = (element instanceof Class<?> ?
				getCustomTypeCondition((Class<?>) element) : getCustomMethodCondition((Method) element));
		//创建RequestMappingInfo
		//(*1*)
		return (requestMapping != null ? createRequestMappingInfo(requestMapping, condition) : null);
	}
	
//(*1*)
protected RequestMappingInfo createRequestMappingInfo(
			RequestMapping requestMapping, RequestCondition<?> customCondition) {

		return RequestMappingInfo
		        //设置路径信息,路径信息如果有${}占位符,会进行替换
				.paths(resolveEmbeddedValuesInPatterns(requestMapping.path()))
				//设置请求方法
				.methods(requestMapping.method())
				//请求参数,比如a=10
				.params(requestMapping.params())
				//设置头部信息
				.headers(requestMapping.headers())
				//指定能够处理此媒体类型的处理器去处理请求
				.consumes(requestMapping.consumes())
				//指定请求返回时能够接受的媒体类型
				.produces(requestMapping.produces())
				//映射名
				.mappingName(requestMapping.name())
				//设置自定义的请求条件
				.customCondition(customCondition)
				//配置对象设置
				.options(this.config)
				//返回RequestMappingInfo对象
				.build();
	}

在上面的代码中给RequestMappingInfo设置一个配置,下面就是这个配置的类定义

public static class org.springframework.web.servlet.mvc.method.RequestMappingInfo.BuilderConfiguration {
        //用户获取url路径的助手类
		private UrlPathHelper urlPathHelper;
        //路径匹配器
		private PathMatcher pathMatcher;
        //是否应用尾部的/匹配
		private boolean trailingSlashMatch = true;
        //是否匹配后缀
		private boolean suffixPatternMatch = true;
        //注册后缀
		private boolean registeredSuffixPatternMatch = false;
        //内容协商管理器,用于解析媒体类型,后缀等
		private ContentNegotiationManager contentNegotiationManager;
		
    。。。。。。		
}

build

public RequestMappingInfo build() {
            //从配置操作中获取内容协商管理器
			ContentNegotiationManager manager = this.options.getContentNegotiationManager();
            //创建模式请求条件,虽然可以是通配符的形式,但是我们在实际的SpringMVC的项目中,似乎很少使用
            //有通配符的路径,基本上是很具体的路径
			PatternsRequestCondition patternsCondition = new PatternsRequestCondition(
					this.paths, this.options.getUrlPathHelper(), this.options.getPathMatcher(),
					this.options.useSuffixPatternMatch(), this.options.useTrailingSlashMatch(),
					this.options.getFileExtensions());
            //创建RequestMappingInfo
			return new RequestMappingInfo(this.mappingName, patternsCondition,
					new RequestMethodsRequestCondition(methods),
					new ParamsRequestCondition(this.params),
					new HeadersRequestCondition(this.headers),
					new ConsumesRequestCondition(this.consumes, this.headers),
					new ProducesRequestCondition(this.produces, this.headers, manager),
					this.customCondition);
		}

下面是RequestCondition的类层级结构

RequestCondition (org.springframework.web.servlet.mvc.condition)
    RequestMappingInfo (org.springframework.web.servlet.mvc.method)
    AbstractRequestCondition (org.springframework.web.servlet.mvc.condition)
        RequestMethodsRequestCondition (org.springframework.web.servlet.mvc.condition)
        ProducesRequestCondition (org.springframework.web.servlet.mvc.condition)
        PatternsRequestCondition (org.springframework.web.servlet.mvc.condition)
        ParamsRequestCondition (org.springframework.web.servlet.mvc.condition)
        RequestConditionHolder (org.springframework.web.servlet.mvc.condition)
        ConsumesRequestCondition (org.springframework.web.servlet.mvc.condition)
        HeadersRequestCondition (org.springframework.web.servlet.mvc.condition)
        CompositeRequestCondition (org.springframework.web.servlet.mvc.condition)

我们随便选其中一个RequestCondition来大致分析下,比如ConsumesRequestCondition,我们直接看到它实现的getMatchingCondition方法

public ConsumesRequestCondition getMatchingCondition(HttpServletRequest request) {
		if (isEmpty()) {
			return this;
		}
		//消费媒体类型表达式,用于进行匹配
		Set<ConsumeMediaTypeExpression> result = new LinkedHashSet<ConsumeMediaTypeExpression>(expressions);
		for (Iterator<ConsumeMediaTypeExpression> iterator = result.iterator(); iterator.hasNext();) {
			ConsumeMediaTypeExpression expression = iterator.next();
			if (!expression.match(request)) {
			    //把不匹配的去掉
				iterator.remove();
			}
		}
		//返回能够匹配当前request请求的消费请求条件,主要用于后面将要进行最佳匹配RequestMappingInfo的筛选
		return (result.isEmpty()) ? null : new ConsumesRequestCondition(result);
	}

ConsumeMediaTypeExpression的匹配

public final boolean match(HttpServletRequest request) {
		try {
		    //(*1*)
			boolean match = matchMediaType(request);
			return (!this.isNegated ? match : !match);
		}
		catch (HttpMediaTypeException ex) {
			return false;
		}
	}
	
	protected boolean matchMediaType(HttpServletRequest request) throws HttpMediaTypeNotSupportedException {
			try {
			    //解析request中告知的媒体类型
				MediaType contentType = StringUtils.hasLength(request.getContentType()) ?
						MediaType.parseMediaType(request.getContentType()) :
						MediaType.APPLICATION_OCTET_STREAM;
						//将程序中设置的媒体类型与请求的媒体类型进行对比
						return getMediaType().includes(contentType);
			}
			catch (InvalidMediaTypeException ex) {
				throw new HttpMediaTypeNotSupportedException(
						"Can't parse Content-Type [" + request.getContentType() + "]: " + ex.getMessage());
			}
		}
	}

解析request请求中的媒体类型

public static MimeType org.springframework.util.MimeTypeUtils.parseMimeType(String mimeType) {
		if (!StringUtils.hasLength(mimeType)) {
			throw new InvalidMimeTypeException(mimeType, "'mimeType' must not be empty");
		}
		//分号分割,比如text/html;charset=utf-8
		String[] parts = StringUtils.tokenizeToStringArray(mimeType, ";");
        //获取完整的类型
		String fullType = parts[0].trim();
		// java.net.HttpURLConnection returns a *; q=.2 Accept header
		//如果是一个*号,那么设置fulltype为*/*
		if (MimeType.WILDCARD_TYPE.equals(fullType)) {
			fullType = "*/*";
		}
		//获取子类型的分割符下标
		int subIndex = fullType.indexOf('/');
		//无效的媒体类型
		if (subIndex == -1) {
			throw new InvalidMimeTypeException(mimeType, "does not contain '/'");
		}
		//媒体类型必须是主类型与子类型进行/分割的
		if (subIndex == fullType.length() - 1) {
			throw new InvalidMimeTypeException(mimeType, "does not contain subtype after '/'");
		}
		//获取主类型
		String type = fullType.substring(0, subIndex);
		//获取子类型
		String subtype = fullType.substring(subIndex + 1, fullType.length());
		//错误的媒体类型,不能*/html
		if (MimeType.WILDCARD_TYPE.equals(type) && !MimeType.WILDCARD_TYPE.equals(subtype)) {
			throw new InvalidMimeTypeException(mimeType, "wildcard type is legal only in '*/*' (all mime types)");
		}
        //解析参数
		Map<String, String> parameters = null;
		if (parts.length > 1) {
			parameters = new LinkedHashMap<String, String>(parts.length - 1);
			for (int i = 1; i < parts.length; i++) {
			    //比如charset=utf-8
				String parameter = parts[i];
				int eqIndex = parameter.indexOf('=');
				if (eqIndex != -1) {
					String attribute = parameter.substring(0, eqIndex);
					String value = parameter.substring(eqIndex + 1, parameter.length());
					//比如 charset utf-8
					parameters.put(attribute, value);
				}
			}
		}

		try {
		    //创建MimeType
			return new MimeType(type, subtype, parameters);
		}
		catch (UnsupportedCharsetException ex) {
			throw new InvalidMimeTypeException(mimeType, "unsupported charset '" + ex.getCharsetName() + "'");
		}
		catch (IllegalArgumentException ex) {
			throw new InvalidMimeTypeException(mimeType, ex.getMessage());
		}
	}

匹配媒体类型

public boolean org.springframework.util.MimeType.includes(MimeType other) {
		if (other == null) {
			return false;
		}
		//如果是*/*,表示匹配所有
		if (this.isWildcardType()) {
			// */* includes anything
			return true;
		}
		else if (getType().equals(other.getType())) {
		    //主和子类型都匹配,那么直接返回true
			if (getSubtype().equals(other.getSubtype())) {
				return true;
			}
			//是否存在子类型通配符
			if (this.isWildcardSubtype()) {
				// wildcard with suffix, e.g. application/*+xml
				int thisPlusIdx = getSubtype().indexOf('+');
				//如果没有+号,也就是 application/*,那么返回true
				if (thisPlusIdx == -1) {
					return true;
				}
				else {
					// application/*+xml includes application/soap+xml
					int otherPlusIdx = other.getSubtype().indexOf('+');
					if (otherPlusIdx != -1) {
						String thisSubtypeNoSuffix = getSubtype().substring(0, thisPlusIdx);
						String thisSubtypeSuffix = getSubtype().substring(thisPlusIdx + 1);
						String otherSubtypeSuffix = other.getSubtype().substring(otherPlusIdx + 1);
						//如果+号后面的字符串匹配并且+号前面确实是*号,那么表示匹配
						if (thisSubtypeSuffix.equals(otherSubtypeSuffix) && WILDCARD_TYPE.equals(thisSubtypeNoSuffix)) {
							return true;
						}
					}
				}
			}
		}
		return false;
	}

好了,回到获取handlerMethod的逻辑,当 当前类,父类,接口的方法都进行一番检查之后,形成了一个Map<Method, RequestMappingInfo>,接下来就是注册了

protected void org.springframework.web.servlet.handler.AbstractHandlerMethodMapping.registerHandlerMethod(Object handler, Method method, T mapping) {
        //注册映射
		this.mappingRegistry.register(mapping, handler, method);
	}

注册映射

public void org.springframework.web.servlet.handler.AbstractHandlerMethodMapping.MappingRegistry.register(T mapping, Object handler, Method method) {
			this.readWriteLock.writeLock().lock();
			try {
			    //创建HandlerMethod,一个维护handler,RequestMappingInfo,method,beanFactory,bridgedMethod,parameters
				HandlerMethod handlerMethod = createHandlerMethod(handler, method);
				//检查是否是唯一的方法映射,RequestMappingInfo -》 handlerMethod
				assertUniqueMethodMapping(handlerMethod, mapping);

				if (logger.isInfoEnabled()) {
					logger.info("Mapped \"" + mapping + "\" onto " + handlerMethod);
				}
				//一个map《RequestMappingInfo -》 handlerMethod》
				this.mappingLookup.put(mapping, handlerMethod);
                
                //获取直接路径,也就是具体路径,没有*号,?号这类通配符的路径
				List<String> directUrls = getDirectUrls(mapping);
				for (String url : directUrls) {
				    //map《直接路径,List<RequestMappingInfo>》
					this.urlLookup.add(url, mapping);
				}

				String name = null;
				//获取命名策略,这个对象是在创建RequestMappingHandlerMapping时,其父类RequestMappingInfoHandlerMapping的构造器中设置的
				//它是RequestMappingInfoHandlerMethodMappingNamingStrategy的实例,当然如果在@RequestMapping注解中
				//设置了name就不会再次设置,否则将通过类名的第一个大写字符#方法名组成映射名字
				if (getNamingStrategy() != null) {
					name = getNamingStrategy().getName(handlerMethod, mapping);
					
					
					//Map<String, List<HandlerMethod>> nameLookup
					addMappingName(name, handlerMethod);
				}
                //初始化跨域配置
				CorsConfiguration corsConfig = initCorsConfiguration(handler, method, mapping);
				if (corsConfig != null) {
				    //map《handlerMethod, 跨域配置》
					this.corsLookup.put(handlerMethod, corsConfig);
				}
                //map《mapping, MappingRegistration》
				this.registry.put(mapping, new MappingRegistration<T>(mapping, handlerMethod, directUrls, name));
			}
			finally {
				this.readWriteLock.writeLock().unlock();
			}
		}

跨域配置

protected CorsConfiguration org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping.initCorsConfiguration(Object handler, Method method, RequestMappingInfo mappingInfo) {
        //创建HandlerMethod,和前面的创建方式一样
		HandlerMethod handlerMethod = createHandlerMethod(handler, method);
		//获取类上的@CrossOrigin信息
		CrossOrigin typeAnnotation = AnnotatedElementUtils.findMergedAnnotation(handlerMethod.getBeanType(), CrossOrigin.class);
		//获取方法上的@CrossOrigin信息
		CrossOrigin methodAnnotation = AnnotatedElementUtils.findMergedAnnotation(method, CrossOrigin.class);

		if (typeAnnotation == null && methodAnnotation == null) {
			return null;
		}
        //跨域配置,比如设置允许的来源url,方法,请求头,有效时间,凭证
		CorsConfiguration config = new CorsConfiguration();
		//设置类上的跨域配置
		updateCorsConfig(config, typeAnnotation);
		//设置方法上的跨域配置(更新操作)
		updateCorsConfig(config, methodAnnotation);
        //如果没有设置,那么设置默认的,默认的为*,表示允许所有
		if (CollectionUtils.isEmpty(config.getAllowedOrigins())) {
			config.setAllowedOrigins(Arrays.asList(CrossOrigin.DEFAULT_ORIGINS));
		}
		if (CollectionUtils.isEmpty(config.getAllowedMethods())) {
			for (RequestMethod allowedMethod : mappingInfo.getMethodsCondition().getMethods()) {
				config.addAllowedMethod(allowedMethod.name());
			}
		}
		if (CollectionUtils.isEmpty(config.getAllowedHeaders())) {
			config.setAllowedHeaders(Arrays.asList(CrossOrigin.DEFAULT_ALLOWED_HEADERS));
		}
		if (config.getAllowCredentials() == null) {
			config.setAllowCredentials(CrossOrigin.DEFAULT_ALLOW_CREDENTIALS);
		}
		if (config.getMaxAge() == null) {
			config.setMaxAge(CrossOrigin.DEFAULT_MAX_AGE);
		}
		return config;
	}

总结一下:RequestMappingHandlerMapping在初始化的时候会获取容器中所有的beanName,然后找到所有被标注了@Controller或者@RequestMapping注解的bean,然后筛选HandlerMethod,也就是被@RequestMapping标注的方法,创建RequestMappingInfo,最后进行注册。

这里有个问题,那就是在bean进行初始化的时候获取所有的bean,哎,这里会发生controller bean的遗漏吗?不会的,我们在springmvc源码分析的前几节的时候可以看到SpringMVC是在所有容器启动之后在onRefresh中进行的,这也解答了,SpringMVC为什么要通过ApplicationContext.getBean(BeanDefinition)进行bean的创建了,而不是直接new。

好了,一切都已经准备就绪,接下来就是寻找handlerMethod了

protected HandlerMethod org.springframework.web.servlet.handler.AbstractHandlerMethodMapping.getHandlerInternal(HttpServletRequest request) throws Exception {
        //获取请求路径
		String lookupPath = getUrlPathHelper().getLookupPathForRequest(request);
		if (logger.isDebugEnabled()) {
			logger.debug("Looking up handler method for path " + lookupPath);
		}
		this.mappingRegistry.acquireReadLock();
		try {
		    //寻找HandlerMethod
			HandlerMethod handlerMethod = lookupHandlerMethod(lookupPath, request);
			if (logger.isDebugEnabled()) {
				if (handlerMethod != null) {
					logger.debug("Returning handler method [" + handlerMethod + "]");
				}
				else {
					logger.debug("Did not find handler method for [" + lookupPath + "]");
				}
			}
			//返回handlerMethod,创建一个新的handlerMethod,防止被修改,如果内部持有的handler是字符串类型,那么会从
			//容器中寻找对应
			return (handlerMethod != null ? handlerMethod.createWithResolvedBean() : null);
		}
		finally {
			this.mappingRegistry.releaseReadLock();
		}
	}

lookupHandlerMethod

protected HandlerMethod org.springframework.web.servlet.handler.AbstractHandlerMethodMapping.lookupHandlerMethod(String lookupPath, HttpServletRequest request) throws Exception {
		List<Match> matches = new ArrayList<Match>();
		//首先通过精确匹配的方式获取   从urlLookup中获取
		List<T> directPathMatches = this.mappingRegistry.getMappingsByUrl(lookupPath);
		if (directPathMatches != null) {
		    //向matches添加能够匹配到的Match对象,Match对象维护着RequestMappingInfo和对应的HandlerMethod
			addMatchingMappings(directPathMatches, matches, request);
		}
		//如果没有获取到,那么进行全对比
		if (matches.isEmpty()) {
			// No choice but to go through all mappings...
			//向matches添加能够匹配到的Match对象,Match对象维护着RequestMappingInfo和对应的HandlerMethod
			addMatchingMappings(this.mappingRegistry.getMappings().keySet(), matches, request);
		}
        //如果找到了,那么还需要选出最适合的
		if (!matches.isEmpty()) {
		    //获取映射比较器
			Comparator<Match> comparator = new MatchComparator(getMappingComparator(request));
			//进行排序
			Collections.sort(matches, comparator);
			if (logger.isTraceEnabled()) {
				logger.trace("Found " + matches.size() + " matching mapping(s) for [" +
						lookupPath + "] : " + matches);
			}
			//第一个被认为是最佳匹配
			Match bestMatch = matches.get(0);
			//如果匹配到的个数大于1
			if (matches.size() > 1) {
			    //检查是否为跨域请求,如果是并且是options请求类型,不允许跨域,返回EmptHandler
				if (CorsUtils.isPreFlightRequest(request)) {
					return PREFLIGHT_AMBIGUOUS_MATCH;
				}
				Match secondBestMatch = matches.get(1);
				//如果第一个和第二个在比较中是相等的,那么出现歧义,抛错
				if (comparator.compare(bestMatch, secondBestMatch) == 0) {
					Method m1 = bestMatch.handlerMethod.getMethod();
					Method m2 = secondBestMatch.handlerMethod.getMethod();
					throw new IllegalStateException("Ambiguous handler methods mapped for HTTP path '" +
							request.getRequestURL() + "': {" + m1 + ", " + m2 + "}");
				}
			}
			handleMatch(bestMatch.mapping, lookupPath, request);
			//获取handlerMethod
			return bestMatch.handlerMethod;
		}
		else {
		    //木有找到
			return handleNoMatch(this.mappingRegistry.getMappings().keySet(), lookupPath, request);
		}
	}

addMatchingMappings

private void org.springframework.web.servlet.handler.AbstractHandlerMethodMapping.addMatchingMappings(Collection<T> mappings, List<Match> matches, HttpServletRequest request) {
        //循环RequestMappingInfo集合
		for (T mapping : mappings) {
		    //获取匹配的RequestMappingInfo
		    //(*1*)
			T match = getMatchingMapping(mapping, request);
			if (match != null) {
			    //包装成Match,添加到matches集合中
				matches.add(new Match(match, this.mappingRegistry.getMappings().get(mapping)));
			}
		}
	}
	
	//(*1*)
	protected RequestMappingInfo org.springframework.web.servlet.mvc.method.RequestMappingInfoHandlerMapping.getMatchingMapping(RequestMappingInfo info, HttpServletRequest request) {
	    //(*2*)
		return info.getMatchingCondition(request);
	}
	
	//(*2*)
	public RequestMappingInfo org.springframework.web.servlet.mvc.method.RequestMappingInfo.getMatchingCondition(HttpServletRequest request) {
	
	    //匹配请求方法,和我们在上面分析的ConsumesRequestCondition差不多,就是把匹配的方法重新包装返回回来
	    //,如果注解中没有配置此项,返回其本身(一个空的condition)
		RequestMethodsRequestCondition methods = this.methodsCondition.getMatchingCondition(request);
		//匹配请求参数,把能匹配到的重新构建ParamsRequestCondition返回,如果注解中没有配置此项,返回其本身(一个空的condition)
		ParamsRequestCondition params = this.paramsCondition.getMatchingCondition(request);
		//匹配请求头,把能匹配的重新构建HeadersRequestCondition返回,如果注解中没有配置此项,返回其本身(一个空的condition)
		HeadersRequestCondition headers = this.headersCondition.getMatchingCondition(request);
		//匹配请求媒体类型,把能匹配到的重新构建ConsumesRequestCondition返回,如果注解中没有配置此项,返回其本身(一个空的condition)
		ConsumesRequestCondition consumes = this.consumesCondition.getMatchingCondition(request);
		//匹配返回媒体类型,把能够匹配到的重新构建ProducesRequestCondition返回,如果注解中没有配置此项,返回其本身(一个空的condition)
		ProducesRequestCondition produces = this.producesCondition.getMatchingCondition(request);
        
        //如果有一个不匹配,看是不是跨域,如果没有跨域直接返回null,表示没有找到
		if (methods == null || params == null || headers == null || consumes == null || produces == null) {
			if (CorsUtils.isPreFlightRequest(request)) {
			    //匹配跨域请求方法,并重新构建RequestMethodsRequestCondition返回
				methods = getAccessControlRequestMethodCondition(request);
				if (methods == null || params == null) {
					return null;
				}
			}
			else {
				return null;
			}
		}
        
        //通配符匹配
		PatternsRequestCondition patterns = this.patternsCondition.getMatchingCondition(request);
		if (patterns == null) {
			return null;
		}
        //自定义请求条件匹配
		RequestConditionHolder custom = this.customConditionHolder.getMatchingCondition(request);
		if (custom == null) {
			return null;
		}
        //如果匹配,那么把与当前请求最精确的匹配重新构建成RequestMappingInfo返回
		return new RequestMappingInfo(this.name, patterns,
				methods, params, headers, consumes, produces, custom.getCondition());
	}
	

这里我们再看下PatternsRequestCondition是如何匹配的

public PatternsRequestCondition getMatchingCondition(HttpServletRequest request) {
        //如果没有设置路径匹配,那么直接返回自身
		if (this.patterns.isEmpty()) {
			return this;
		}
        //获取请求路径
		String lookupPath = this.pathHelper.getLookupPathForRequest(request);
		//获取匹配的路径
		//(*1*)
		List<String> matches = getMatchingPatterns(lookupPath);
        //如果不为空,那么重新构建一个与当前request最匹配的PatternsRequestCondition
		return matches.isEmpty() ? null :
			new PatternsRequestCondition(matches, this.pathHelper, this.pathMatcher, this.useSuffixPatternMatch,
					this.useTrailingSlashMatch, this.fileExtensions);
	}
	
	//(*1*)
	public List<String> getMatchingPatterns(String lookupPath) {
		List<String> matches = new ArrayList<String>();
		for (String pattern : this.patterns) {
		    //获取匹配的路径,可能会做些处理,比如添加后缀什么的
		    //(*2*)
			String match = getMatchingPattern(pattern, lookupPath);
			if (match != null) {
				matches.add(match);
			}
		}
		//进行排序,使用的排序器是AntPatternComparator
		Collections.sort(matches, this.pathMatcher.getPatternComparator(lookupPath));
		return matches;
	}
	
	//(*2*)
	private String getMatchingPattern(String pattern, String lookupPath) {
	    //如果直接相等,那么直接返回,这个路径已经超级具体了
		if (pattern.equals(lookupPath)) {
			return pattern;
		}
		//是否允许后缀匹配
		if (this.useSuffixPatternMatch) {
		    //如果有备用后缀和请求路径就有后缀
			if (!this.fileExtensions.isEmpty() && lookupPath.indexOf('.') != -1) {
				for (String extension : this.fileExtensions) {
				    //匹配
					if (this.pathMatcher.match(pattern + extension, lookupPath)) {
					    //返回添加了后缀的路径,比如/test/*/abc + .html
						return pattern + extension;
					}
				}
			}
			else {
			    //如果@RequestMapping注解定义的路径没有后缀
				boolean hasSuffix = pattern.indexOf('.') != -1;
				//那么加上.*进行匹配
				if (!hasSuffix && this.pathMatcher.match(pattern + ".*", lookupPath)) {
					return pattern + ".*";
				}
			}
		}
		//直接路径匹配
		if (this.pathMatcher.match(pattern, lookupPath)) {
			return pattern;
		}
		//是否使用后缀/匹配,如果@RequestMapping注解定义的路径不是/结尾,那么加上后再匹配
		if (this.useTrailingSlashMatch) {
			if (!pattern.endsWith("/") && this.pathMatcher.match(pattern + "/", lookupPath)) {
				return pattern +"/";
			}
		}
		return null;
	}

回到org.springframework.web.servlet.handler.AbstractHandlerMethodMapping.lookupHandlerMethod(String, HttpServletRequest)方法
这个时候我们可能已经找到了候选的HandlerMethod,也可能没有找到

找到和没找到,SpringMVC还有一些后置处理(比如路径变量等),我们先来看下找到后的

protected void org.springframework.web.servlet.mvc.method.RequestMappingInfoHandlerMapping.handleMatch(RequestMappingInfo info, String lookupPath, HttpServletRequest request) {
        
        //request.setAttribute(HandlerMapping.PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE, lookupPath);
        //记录查找到当前handlerMethod的请求路径到request中
		super.handleMatch(info, lookupPath, request);

        //最佳匹配路径
		String bestPattern;
		//路径变量
		Map<String, String> uriVariables;
		//解码后的路径变量
		Map<String, String> decodedUriVariables;
        //获取匹配到的所有pattern路径
		Set<String> patterns = info.getPatternsCondition().getPatterns();
		if (patterns.isEmpty()) {
		    //最佳匹配路径当然是request中的uri
			bestPattern = lookupPath;
			//uri变量为空
			uriVariables = Collections.emptyMap();
			decodedUriVariables = Collections.emptyMap();
		}
		else {
		    //如果patterns不为空,那么第一个被认为是最佳的路径
			bestPattern = patterns.iterator().next();
			//解析路径变量,比如bestPattern为/test/{a},lookupPath为/test/520,那么路径变量就是a=520
			uriVariables = getPathMatcher().extractUriTemplateVariables(bestPattern, lookupPath);
			//对路径变量的值进行url解码
			decodedUriVariables = getUrlPathHelper().decodePathVariables(request, uriVariables);
		}
        //记录最佳的模式路径 HandlerMapping.class.getName() + ".bestMatchingPattern" -》 bestPattern
		request.setAttribute(BEST_MATCHING_PATTERN_ATTRIBUTE, bestPattern);
		//HandlerMapping.class.getName() + ".uriTemplateVariables" -》 decodedUriVariables
		request.setAttribute(HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE, decodedUriVariables);

        //是否需要解析矩阵变量。默认是不解析的
		if (isMatrixVariableContentAvailable()) {
		    //解析矩阵参数 比如/test/{a},然后发来的请求是/test/arg1=0;arg2=1;arg3=2,3,4,5,6
		    //然后这里的返回值就是a -> {arg1:[0],arg2:[1],arg2:[2,3,4,5,6]}
		    //如果是这种/test/0;arg2=1;arg3=2,3,4,5,6,也就是第一个参数没有等号
		    //那么uriVariables原先对应的a=0;arg2=1;arg3=2,3,4,5,6变成了a=0
		    //然后matrixVars就是{arg2:[1],arg2:[2,3,4,5,6]}
			Map<String, MultiValueMap<String, String>> matrixVars = extractMatrixVariables(request, uriVariables);
			//HandlerMapping.class.getName() + ".matrixVariables" -》 matrixVars
			request.setAttribute(HandlerMapping.MATRIX_VARIABLES_ATTRIBUTE, matrixVars);
		}
        //设置允许返回的媒体类型
		if (!info.getProducesCondition().getProducibleMediaTypes().isEmpty()) {
			Set<MediaType> mediaTypes = info.getProducesCondition().getProducibleMediaTypes();
			//HandlerMapping.class.getName() + ".producibleMediaTypes" -> mediaTypes
			request.setAttribute(PRODUCIBLE_MEDIA_TYPES_ATTRIBUTE, mediaTypes);
		}
	}

如果没有匹配到handlermethod又是怎么处理的呢?

protected HandlerMethod org.springframework.web.servlet.mvc.method.RequestMappingInfoHandlerMapping.handleNoMatch(Set<RequestMappingInfo> requestMappingInfos,
			String lookupPath, HttpServletRequest request) throws ServletException {
        //存放能匹配到的方法
		Set<String> allowedMethods = new LinkedHashSet<String>(4);
        //存放能够匹配上的原始RequestMappingInfo
		Set<RequestMappingInfo> patternMatches = new HashSet<RequestMappingInfo>();
		//存放既能匹配方法,又能匹配路径的RequestMappingInfo
		Set<RequestMappingInfo> patternAndMethodMatches = new HashSet<RequestMappingInfo>();

		for (RequestMappingInfo info : requestMappingInfos) {
		    //匹配路径
			if (info.getPatternsCondition().getMatchingCondition(request) != null) {
			    //如果存在匹配当前request的路径子集,那么存到patternMatches集合中
				patternMatches.add(info);
				if (info.getMethodsCondition().getMatchingCondition(request) != null) {
				    //如果连方法都能匹配到子集,那么存放到patternAndMethodMatches集合中
					patternAndMethodMatches.add(info);
				}
				else {
				    //如果方法未匹配到,那么记录当前循环到的RequestMappingInfo支持的方法
					for (RequestMethod method : info.getMethodsCondition().getMethods()) {
						allowedMethods.add(method.name());
					}
				}
			}
		}
        //如果路径都不匹配,那说明项目中确实没有定义处理这个请求的handler,返回null
		if (patternMatches.isEmpty()) {
			return null;
		}
		//如果路径匹配到了,却没有一个能够接受这个请求方法,那么没得说,项目中已经没有能够处理当前request的请求方法了
		//报错吧!
		else if (patternAndMethodMatches.isEmpty() && !allowedMethods.isEmpty()) {
			throw new HttpRequestMethodNotSupportedException(request.getMethod(), allowedMethods);
		}

		Set<MediaType> consumableMediaTypes;
		Set<MediaType> producibleMediaTypes;
		List<String[]> paramConditions;
        //如果有匹配到路径,程序也没有设置能够允许的请求方法,那么从patternMatches寻找不匹配的请求媒体类型
        //不匹配的返回媒体类型,不匹配的参数RequestMappingInfo
		if (patternAndMethodMatches.isEmpty()) {
			consumableMediaTypes = getConsumableMediaTypes(request, patternMatches);
			producibleMediaTypes = getProducibleMediaTypes(request, patternMatches);
			paramConditions = getRequestParams(request, patternMatches);
		}
		//从patternAndMethodMatches寻找不匹配的请求媒体类型
        //不匹配的返回媒体类型,不匹配的参数RequestMappingInfo
		else {
			consumableMediaTypes = getConsumableMediaTypes(request, patternAndMethodMatches);
			producibleMediaTypes = getProducibleMediaTypes(request, patternAndMethodMatches);
			paramConditions = getRequestParams(request, patternAndMethodMatches);
		}
        //然后确实发现了不匹配的请求类型的RequestMappingInfo
		if (!consumableMediaTypes.isEmpty()) {
			MediaType contentType = null;
			if (StringUtils.hasLength(request.getContentType())) {
				try {
					contentType = MediaType.parseMediaType(request.getContentType());
				}
				catch (InvalidMediaTypeException ex) {
					throw new HttpMediaTypeNotSupportedException(ex.getMessage());
				}
			}
			//抛出错误,表示你请求的媒体类型,springMVC中定义的handler不支持
			throw new HttpMediaTypeNotSupportedException(contentType, new ArrayList<MediaType>(consumableMediaTypes));
		}
		else if (!producibleMediaTypes.isEmpty()) {
		    //抛出没有可接受的返回类型
			throw new HttpMediaTypeNotAcceptableException(new ArrayList<MediaType>(producibleMediaTypes));
		}
		else if (!CollectionUtils.isEmpty(paramConditions)) {
		    //不满足指定的参数类型
			throw new UnsatisfiedServletRequestParameterException(paramConditions, request.getParameterMap());
		}
		else {
		    //其他的返回null
			return null;
		}
	}

回到AbstractHandlerMapping的getHandler方法

public final HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception {
		Object handler = getHandlerInternal(request);
		if (handler == null) {
		    //获取默认的handlerMethod
			handler = getDefaultHandler();
		}
		if (handler == null) {
			return null;
		}
		// Bean name or resolved handler?
		//如果是字符串类型,那么从容器中寻找
		if (handler instanceof String) {
			String handlerName = (String) handler;
			handler = getApplicationContext().getBean(handlerName);
		}
        //用拦截器和handlerMethod构建HandlerExecutionChain
		HandlerExecutionChain executionChain = getHandlerExecutionChain(handler, request);
		//检查request的请求头中是否存在Origin,如果有表示跨域
		if (CorsUtils.isCorsRequest(request)) {
		    //从当前HandlerMapping中获取全局的跨域请求配置
			CorsConfiguration globalConfig = this.corsConfigSource.getCorsConfiguration(request);
			//从注册mapping中获取跨域配置
			CorsConfiguration handlerConfig = getCorsConfiguration(handler, request);
			//合并
			CorsConfiguration config = (globalConfig != null ? globalConfig.combine(handlerConfig) : handlerConfig);
			//额外添加跨域相关的拦截器,用于判断是否允许跨域,不予许就被拦截,并设置禁止访问编码
			executionChain = getCorsHandlerExecutionChain(request, executionChain, config);
		}
		return executionChain;
	}

获取拦截器

protected HandlerExecutionChain org.springframework.web.servlet.handler.AbstractHandlerMapping.getHandlerExecutionChain(Object handler, HttpServletRequest request) {
		HandlerExecutionChain chain = (handler instanceof HandlerExecutionChain ?
				(HandlerExecutionChain) handler : new HandlerExecutionChain(handler));

		String lookupPath = this.urlPathHelper.getLookupPathForRequest(request);
		//this.adaptedInterceptors这里面的值是在HandlerMapping初始化设置ApplicationContext时扩展和自动装配的
		for (HandlerInterceptor interceptor : this.adaptedInterceptors) {
		    //判断是否是映射拦截器,如果是,那么需要进行uri匹配,一般我们配置的拦截器会配置在
		    //SpringMVC的配置文件中,并设置url匹配路径,最后会被包装成这个类型
			if (interceptor instanceof MappedInterceptor) {
				MappedInterceptor mappedInterceptor = (MappedInterceptor) interceptor;
				//(*1*)
				if (mappedInterceptor.matches(lookupPath, this.pathMatcher)) {
					chain.addInterceptor(mappedInterceptor.getInterceptor());
				}
			}
			else {
				chain.addInterceptor(interceptor);
			}
		}
		return chain;
	}
	
	//(*1*)
	public boolean matches(String lookupPath, PathMatcher pathMatcher) {
		PathMatcher pathMatcherToUse = (this.pathMatcher != null) ? this.pathMatcher : pathMatcher;
		//不拦截的路径
		if (this.excludePatterns != null) {
			for (String pattern : this.excludePatterns) {
			    //使用的AntPatchMatcher
				if (pathMatcherToUse.match(pattern, lookupPath)) {
					return false;
				}
			}
		}
		if (this.includePatterns == null) {
			return true;
		}
		else {
			for (String pattern : this.includePatterns) {
			    //匹配拦截路径
				if (pathMatcherToUse.match(pattern, lookupPath)) {
					return true;
				}
			}
			return false;
		}
	}

自此,我们查找的handlerMethod的任务就结束了,下一节分析一下AbstractUrlHandlerMapping

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
前言 第一篇 网站基础知识 第1章 网站架构及其演变过程2 1.1 软件的三大类型2 1.2 基础的结构并不简单3 1.3 架构演变的起点5 1.4 海量数据的解决方案5 1.4.1 缓存和页面静态化5 1.4.2 数据库优化6 1.4.3 分离活跃数据8 1.4.4 批量读取和延迟修改8 1.4.5 读写分离9 1.4.6 分布式数据库10 1.4.7 NoSQL和Hadoop10 1.5 高并发的解决方案11 1.5.1 应用和静态资源分离11 1.5.2 页面缓存12 1.5.3 集群与分布式12 1.5.4 反向代理13 1.5.5 CDN14 1.6 底层的优化15 1.7 小结15 第2章 常见协议和标准17 2.1 DNS协议17 2.2 TCP/IP协议与Socket18 2.3 HTTP协议20 2.4 Servlet与Java Web开发22 第3章 DNS的设置23 3.1 DNS解析23 3.2 Windows 7设置DNS服务器24 3.3 Windows设置本机域名和IP的对应关系25 第4章 Java中Socket的用法26 4.1 普通Socket的用法26 4.2 NioSocket的用法28 第5章 自己动手实现HTTP协议33 第6章 详解Servlet37 6.1 Servlet接口37 6.2 GenericServlet40 6.3 HttpServlet41 第7章 Tomcat分析44 7.1 Tomcat的顶层结构及启动过程44 7.1.1 Tomcat的顶层结构44 7.1.2 Bootstrap的启动过程45 7.1.3 Catalina的启动过程47 7.1.4 Server的启动过程48 7.1.5 Service的启动过程50 7.2 Tomcat的生命周期管理52 7.2.1 Lifecycle接口52 7.2.2 LifecycleBase53 7.3 Container分析59 7.3.1 ContainerBase的结构59 7.3.2 Container的4个子容器60 7.3.3 4种容器的配置方法60 7.3.4 Container的启动62 7.4 Pipeline-Value管道69 7.4.1 Pipeline-Value处理模式69 7.4.2 Pipeline-Value的实现方法70 7.5 Connector分析73 7.5.1 Connector的结构73 7.5.2 Connector自身类74 7.5.3 ProtocolHandler77 7.5.4 处理TCP/IP协议的Endpoint77 7.5.5 处理HTTP协议的Processor80 7.5.6 适配器Adapter81 第二篇 俯视Spring MVC 第8章 Spring MVC之初体验84 8.1 环境搭建84 8.2 Spring MVC最简单的配置84 8.2.1 在web.xml中配置Servlet85 8.2.2 创建Spring MVC的xml配置文件85 8.2.3  创建Controller和view86 8.3 关联spring源代码87 8.4 小结89 第9章 创建Spring MVC之器90 9.1 整体结构介绍90 9.2 HttpServletBean93 9.3 FrameworkServlet95 9.4 DispatcherServlet100 9.5 小结107 第10章 Spring MVC之用108 10.1 HttpServletBean108 10.2 FrameworkServlet108 10.3 DispatcherServlet114 10.4 doDispatch结构118 10.5 小结123 第三篇 Spring MVC组件分析 第11章 组件概览126 11.1 HandlerMapping126 11.2 HandlerAdapter128 11.3 HandlerExceptionResolver130 11.4 ViewResolver131 11.5 RequestToViewNameTranslator133 11.6 LocaleResolver133 11.7 ThemeResolver135 11.8 MultipartResolver137 11.9 FlashMapManager138 11.10 小结139 第12章 HandlerMapping140 12.1 AbstractHandlerMapping140 12.1.1 创建AbstractHandlerMapping之器141 12.1.2 AbstractHandlerMapping之用142 12.2 AbstractUr

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值