Spring源码解析(四):SpringMVC源码解析

SpringMVC是Spring一个非常重要的模块,从大体上看,在使用SpringMVC的时候,需要在web.xml中配置DispatcherServlet,这个DispatcherServlet可以看成是一个前端控制器的实现,web请求会通过它分发给各个对应的Controller,然后会看到ModelAndView数据的生成,并把ModelAndView数据交给对应的View视图来进行呈现。下面我们来对SpringMVC的设计进行详细的分析。

一、根上下文在容器中的启动

SpringMVC作为Spring的一个模块,也是建立在IOC容器的基础上的,首先我们来了解下Spring的IOC容器是如何在Web环境中载入并起作用的。如果要在web环境中使用IOC容器,需要为IOC容器设计一个启动过程,把IOC容器导入,这个启动的过程是与web容器的启动过程集成在一起的,我们以tomcat作为web容器来进行分析。先来看一个SpringMVC相关的web.xml中的配置

<context-param>
    <param-name>contextConfigLocation</param-name>
    <param-value>classpath:/applicationContext.xml</param-value>
  </context-param>
  <listener>
    <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
  </listener>
  <servlet>
    <servlet-name>dispatcher</servlet-name>
    <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
    <init-param>
      <param-name>contextConfigLocation</param-name>
      <param-value>classpath:/spring-mvc.xml</param-value>
    </init-param>
    <load-on-startup>1</load-on-startup>
  </servlet>
  <servlet-mapping>
    <servlet-name>dispatcher</servlet-name>
    <url-pattern>/</url-pattern>
  </servlet-mapping>

在这个web.xml中,定义了一个Servlet对象——DispatcherSerlvet,这个类是SpringMVC的核心类,它的实现原理我们后面再来分析,context-param参数的配置用来指定SpringIOC容器读取Bean定义的xml文件的路径。在这个文件中,一般用来定义Spring应用的Bean配置。最后,我们可以看到一个监听器的配置——ContextLoaderListener,这个类作为SpringMVC的启动类,负责完成IOC容器在web环境中的启动工作,它是与web容器的生命周期相关联的,下面我们来具体看看IOC容器在web容器中的启动过程是如何实现的。

容器的启动过程是与ServletContext相伴而生的,由ContextLoaderListener启动的上下文为根上下文,此外,还有一个与Web MVC相关的上下文用来保存控制器(DispatcherServlet)需要的MVC对象,作为根上下文的子上下文,构成一个层次化的上下文体系。

ContextLoaderListener实现了ServletContextListener接口,这个接口里的函数会结合Web容器的生命周期被调用,因为ServletContextListener是ServletContext的监听者,如果ServletContext发生变化会触发相应的事件,这些事件包括在服务器启动,ServletContext被创建的时候回触发contextInitialized方法;服务关闭时,ServletContext被销毁的时候触发contextDestroyed方法。IOC容器的初始化就是在contextInitialized方法中完成,这个方法的代码如下:


    public void contextInitialized(ServletContextEvent event) {
	initWebApplicationContext(event.getServletContext());
    }

​

initWebApplicationContext方法的具体实现在的ContextLoaderListener父类ContextLoader中

public WebApplicationContext initWebApplicationContext(ServletContext servletContext) {
                //如果已经有根上下文存在
		if (servletContext.getAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE) != null) {
			throw new IllegalStateException(
					"Cannot initialize context because there is already a root application context present - " +
					"check whether you have multiple ContextLoader* definitions in your web.xml!");
		}

		Log logger = LogFactory.getLog(ContextLoader.class);
		servletContext.log("Initializing Spring root WebApplicationContext");
		if (logger.isInfoEnabled()) {
			logger.info("Root WebApplicationContext: initialization started");
		}
		long startTime = System.currentTimeMillis();

		try {
	                //初始化上下文
			if (this.context == null) {
				this.context = createWebApplicationContext(servletContext);
			}
			if (this.context instanceof ConfigurableWebApplicationContext) {
				ConfigurableWebApplicationContext cwac = (ConfigurableWebApplicationContext) this.context;
				if (!cwac.isActive()) {
					// The context has not yet been refreshed -> provide services such as
					// setting the parent context, setting the application context id, etc
                                        //载入上下文的双亲上下文并设置双亲上下文
					if (cwac.getParent() == null) {
						// The context instance was injected without an explicit parent ->
						// determine parent for root web application context, if any.
						ApplicationContext parent = loadParentContext(servletContext);
						cwac.setParent(parent);
					}

                                //对上下文的属性进行一些设置并调用refresh方法初始化
				configureAndRefreshWebApplicationContext(cwac, servletContext);
				}
			}
	
                                //将上下文对象设置到ServletContext中
    servletContext.setAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE, this.context);

			ClassLoader ccl = Thread.currentThread().getContextClassLoader();
			if (ccl == ContextLoader.class.getClassLoader()) {
				currentContext = this.context;
			}
			else if (ccl != null) {
				currentContextPerThread.put(ccl, this.context);
			}

			if (logger.isDebugEnabled()) {
				logger.debug("Published root WebApplicationContext as ServletContext attribute with name [" +
						WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE + "]");
			}
			if (logger.isInfoEnabled()) {
				long elapsedTime = System.currentTimeMillis() - startTime;
				logger.info("Root WebApplicationContext: initialization completed in " + elapsedTime + " ms");
			}

			return this.context;
		}
		catch (RuntimeException ex) {
			logger.error("Context initialization failed", ex);
			servletContext.setAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE, ex);
			throw ex;
		}
		catch (Error err) {
			logger.error("Context initialization failed", err);
			servletContext.setAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE, err);
			throw err;
		}
	}

首先来看看createWebApplicationContext方法中如何初始化根上下文

protected WebApplicationContext createWebApplicationContext(ServletContext sc) {
		//判断作为IOC容器的类的类型
                Class<?> contextClass = determineContextClass(sc);
		if (!ConfigurableWebApplicationContext.class.isAssignableFrom(contextClass)) {
			throw new ApplicationContextException("Custom context class [" + contextClass.getName() +
					"] is not of type [" + ConfigurableWebApplicationContext.class.getName() + "]");
		}
		//实例化IOC容器
                return (ConfigurableWebApplicationContext) BeanUtils.instantiateClass(contextClass);
	}
protected Class<?> determineContextClass(ServletContext servletContext) {
                //读取对contextClass的配置
		String contextClassName = servletContext.getInitParameter(CONTEXT_CLASS_PARAM);
                //如果配置了contextClass那么就使用这个配置的class,当然前提是这个class可用
		if (contextClassName != null) {
			try {
				return ClassUtils.forName(contextClassName, ClassUtils.getDefaultClassLoader());
			}
			catch (ClassNotFoundException ex) {
				throw new ApplicationContextException(
						"Failed to load custom context class [" + contextClassName + "]", ex);
			}
		}
	        //否则使用默认的类,这个默认的上下文的类是org.springframework.web.context.support.XmlWebApplicationContext,
                //是在ContextLoader.properties文件中配置的
                else {
			contextClassName = defaultStrategies.getProperty(WebApplicationContext.class.getName());
			try {
				return ClassUtils.forName(contextClassName, ContextLoader.class.getClassLoader());
			}
			catch (ClassNotFoundException ex) {
				throw new ApplicationContextException(
						"Failed to load default context class [" + contextClassName + "]", ex);
			}
		}
	}

然后看看configureAndRefreshWebApplicationContext方法中对于容器的参数的一些配置,方法代码如下

protected void configureAndRefreshWebApplicationContext(ConfigurableWebApplicationContext wac, ServletContext sc) {
		if (ObjectUtils.identityToString(wac).equals(wac.getId())) {
			// The application context id is still set to its original default value
			// -> assign a more useful id based on available information
			String idParam = sc.getInitParameter(CONTEXT_ID_PARAM);
			if (idParam != null) {
				wac.setId(idParam);
			}
			else {
				// Generate default id...
				wac.setId(ConfigurableWebApplicationContext.APPLICATION_CONTEXT_ID_PREFIX +
						ObjectUtils.getDisplayString(sc.getContextPath()));
			}
		}

		//将ServletContext设置到容器中
                wac.setServletContext(sc);
		//设置配置文件的位置参数
                String configLocationParam = sc.getInitParameter(CONFIG_LOCATION_PARAM);
		if (configLocationParam != null) {
			wac.setConfigLocation(configLocationParam);
		}

		// The wac environment's #initPropertySources will be called in any case when the context
		// is refreshed; do it eagerly here to ensure servlet property sources are in place for
		// use in any post-processing or initialization that occurs below prior to #refresh
		ConfigurableEnvironment env = wac.getEnvironment();
		if (env instanceof ConfigurableWebEnvironment) {
			((ConfigurableWebEnvironment) env).initPropertySources(sc, null);
		}

		customizeContext(sc, wac);
		//调用refresh方法启动容器的初始化
                wac.refresh();
	}

这就是IOC容器在web容器中的启动过程,在初始化这个上下文之后,该上下文会被存储到ServletContext中,这样就建立了一个全局的关于整个应用的根上下文。在后面我们还会看到,在DispatcherServlet进行自己的上下文初始化时,会将这个根上下文设置为DispatcherServlet自带的上下文的双亲上下文。

二、Spring MVC的设计与实现

在前面的web.xml中我们看到,除了配置ContextLoaderListener之外,还要对DispatcherServlet进行配置。DispatcherServlet十分重要,它可以说是Spring MVC实现中最核心的部分,它作为一个前端控制器,所有的web请求都需要通过它来处理,进行转发、匹配、数据处理后,转由页面进行展现,它的设计与分析也是下面分析Spring MVC的一条主线。

DispatcherServlet的工作大致可以分为两个部分:一是初始化部分,DispatcherServlet通过这部分功能对MVC模块的其他部分进行了初始化,比如handlerMapping、ViewResolver等;另一个是对http请求进行响应,DispatcherServlet对请求的转发及处理在这部分功能中完成。

2.1 DispatcherServlet的启动及初始化

我们先来分析DispatcherServlet的初始化过程,作为一个Servlet,DispatcherServlet启动时首先会调用它的init方法,这个方法的实现在它的父类HttpServletBean中,代码如下

public final void init() throws ServletException {
		if (logger.isDebugEnabled()) {
			logger.debug("Initializing servlet '" + getServletName() + "'");
		}

		// 获取Servlet的初始化参数,对Bean的属性进行设置
		try {
			PropertyValues pvs = new ServletConfigPropertyValues(getServletConfig(), this.requiredProperties);
			BeanWrapper bw = PropertyAccessorFactory.forBeanPropertyAccess(this);
			ResourceLoader resourceLoader = new ServletContextResourceLoader(getServletContext());
			bw.registerCustomEditor(Resource.class, new ResourceEditor(resourceLoader, getEnvironment()));
			initBeanWrapper(bw);
			bw.setPropertyValues(pvs, true);
		}
		catch (BeansException ex) {
			logger.error("Failed to set bean properties on servlet '" + getServletName() + "'", ex);
			throw ex;
		}

		// 调用子类的initServletBean方法进行具体的初始化,主要的初始化过程在这个方法中
		initServletBean();

		if (logger.isDebugEnabled()) {
			logger.debug("Servlet '" + getServletName() + "' configured successfully");
		}
	}

initServletBean的实现在DispatcherServlet的父类FrameworkServlet中

protected final void initServletBean() throws ServletException {
		getServletContext().log("Initializing Spring FrameworkServlet '" + getServletName() + "'");
		if (this.logger.isInfoEnabled()) {
			this.logger.info("FrameworkServlet '" + getServletName() + "': initialization started");
		}
		long startTime = System.currentTimeMillis();

		try {
			//在这里初始化上下文
                        this.webApplicationContext = initWebApplicationContext();
			initFrameworkServlet();
		}
		catch (ServletException ex) {
			this.logger.error("Context initialization failed", ex);
			throw ex;
		}
		catch (RuntimeException ex) {
			this.logger.error("Context initialization failed", ex);
			throw ex;
		}

		if (this.logger.isInfoEnabled()) {
			long elapsedTime = System.currentTimeMillis() - startTime;
			this.logger.info("FrameworkServlet '" + getServletName() + "': initialization completed in " +
					elapsedTime + " ms");
		}
	}
protected WebApplicationContext initWebApplicationContext() {
		//获取之前在ContextLoaderListener中保存到ServletContext中的根上下文
                WebApplicationContext rootContext =
				WebApplicationContextUtils.getWebApplicationContext(getServletContext());
		WebApplicationContext wac = null;

		        //如果当前类中的webApplicationContext不为空 
                        if (this.webApplicationContext != null) {
			// A context instance was injected at construction time -> use it
			wac = this.webApplicationContext;
			if (wac instanceof ConfigurableWebApplicationContext) {
				ConfigurableWebApplicationContext cwac = (ConfigurableWebApplicationContext) wac;
				if (!cwac.isActive()) {
					//将当前根上下文作为双亲上下文
					if (cwac.getParent() == null) {
						// The context instance was injected without an explicit parent -> set
						// the root application context (if any; may be null) as the parent
						cwac.setParent(rootContext);
					}
                                //设置上下文的相关属性并调用refresh方法触发初始化					
                                configureAndRefreshWebApplicationContext(cwac);
				}
			}
		}
		if (wac == null) {
			wac = findWebApplicationContext();
		}
                if (wac == null) {
			//具体的初始化的过程
			wac = createWebApplicationContext(rootContext);
		}

		if (!this.refreshEventReceived) {
			// Either the context is not a ConfigurableApplicationContext with refresh
			// support or the context injected at construction time had already been
			// refreshed -> trigger initial onRefresh manually here.
			onRefresh(wac);
		}

		//将当前建立的上下文存到Servletcontext中,属性名为
                //org.springframework.web.servlet.FrameworkServlet.CONTEXT.dispatcher
                if (this.publishContext) {
			// Publish the context as a servlet context attribute.
			String attrName = getServletContextAttributeName();
			getServletContext().setAttribute(attrName, wac);
			if (this.logger.isDebugEnabled()) {
				this.logger.debug("Published WebApplicationContext of servlet '" + getServletName() +
						"' as ServletContext attribute with name [" + attrName + "]");
			}
		}

		return wac;
	}
protected WebApplicationContext createWebApplicationContext(ApplicationContext parent) {
		//获取需要初始化的上下文的对应的类,默认是XmlWebApplicationContext
                Class<?> contextClass = getContextClass();
		if (this.logger.isDebugEnabled()) {
			this.logger.debug("Servlet with name '" + getServletName() +
					"' will try to create custom WebApplicationContext context of class '" +
					contextClass.getName() + "'" + ", using parent context [" + parent + "]");
		}
		if (!ConfigurableWebApplicationContext.class.isAssignableFrom(contextClass)) {
			throw new ApplicationContextException(
					"Fatal initialization error in servlet with name '" + getServletName() +
					"': custom WebApplicationContext class [" + contextClass.getName() +
					"] is not of type ConfigurableWebApplicationContext");
		}
		//通过反射初始化得到上下文对象
                ConfigurableWebApplicationContext wac =
				(ConfigurableWebApplicationContext) BeanUtils.instantiateClass(contextClass);
                //设置一些相关的属性并将根上下文设置为双亲上下文,这样一来根上下文中
                //的Bean也可以被DispatcherServlet中的上下文使用
		wac.setEnvironment(getEnvironment());
		wac.setParent(parent);
		wac.setConfigLocation(getContextConfigLocation());
                //设置相关属性并调用refresh方法进行容器的初始化
		configureAndRefreshWebApplicationContext(wac);

		return wac;
	}

configureAndRefreshWebApplicationContext与前面的ContextLoader中的configureAndRefreshWebApplicationContext有些类似

protected void configureAndRefreshWebApplicationContext(ConfigurableWebApplicationContext wac) {
		//对上下文的属性进行设置
                if (ObjectUtils.identityToString(wac).equals(wac.getId())) {
			if (this.contextId != null) {
				wac.setId(this.contextId);
			}
			else {
				// Generate default id...
				wac.setId(ConfigurableWebApplicationContext.APPLICATION_CONTEXT_ID_PREFIX +
						ObjectUtils.getDisplayString(getServletContext().getContextPath()) + "/" + getServletName());
			}
		}

		wac.setServletContext(getServletContext());
		wac.setServletConfig(getServletConfig());
		wac.setNamespace(getNamespace());
		wac.addApplicationListener(new SourceFilteringListener(wac, new ContextRefreshListener()));

		
		ConfigurableEnvironment env = wac.getEnvironment();
		if (env instanceof ConfigurableWebEnvironment) {
			((ConfigurableWebEnvironment) env).initPropertySources(getServletContext(), getServletConfig());
		}

		postProcessWebApplicationContext(wac);
		applyInitializers(wac);
		//调用refresh方法触发容器的初始化
                wac.refresh();
	}

初始化的过程中还会调用DispatcherServlet中的onRefresh方法,onRefresh方法中会调用DispatcherServlet的initStrategies,这个方法会触发整个Spring MVC框架的初始化

protected void onRefresh(ApplicationContext context) {
		initStrategies(context);
	}
protected void initStrategies(ApplicationContext context) {
		initMultipartResolver(context);
		initLocaleResolver(context);
		initThemeResolver(context);
		initHandlerMappings(context);
		initHandlerAdapters(context);
		initHandlerExceptionResolvers(context);
		initRequestToViewNameTranslator(context);
		initViewResolvers(context);
		initFlashMapManager(context);
	}

对于上面的方法名称,很容易理解。以initHandlerMappings方法为例来看看这个初始化过程,方法代码如下

private void initHandlerMappings(ApplicationContext context) {
		//这个handlerMappings是个List,用来存放所有的HandlerMapping
                this.handlerMappings = null;
                //导入所有的HandlerMapping,detectAllHandlerMappings默认为真,即默认的从所有的IOC容器中取
		if (this.detectAllHandlerMappings) {
			// 从容器中取得所有的HandlerMapping,包括父级容器,如果配置了<mvc:annotation-driven/>,
                        //那么这里默认是BeanNameUrlHandlerMapping和RequestMappingHandlerMapping,
                        //matchingBeans的键分别是它们完整的类名
			Map<String, HandlerMapping> matchingBeans =
					BeanFactoryUtils.beansOfTypeIncludingAncestors(context, HandlerMapping.class, true, false);
			if (!matchingBeans.isEmpty()) {
				this.handlerMappings = new ArrayList<HandlerMapping>(matchingBeans.values());
				// We keep HandlerMappings in sorted order.
				AnnotationAwareOrderComparator.sort(this.handlerMappings);
			}
		}
		else {//也可以根据名称从当前的IOC容器中取得handlerMapping
			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.
			}
		}

		// 如果都没有取到的话,那么从DispatcherServlet.properties中取默认设置的HandlerMapping
		if (this.handlerMappings == null) {
			this.handlerMappings = getDefaultStrategies(context, HandlerMapping.class);
			if (logger.isDebugEnabled()) {
				logger.debug("No HandlerMappings found in servlet '" + getServletName() + "': using default");
			}
		}
	}

这样,DispatcherServlet就把所有的HandlerMapping初始化到了handlerMapping变量中。其他的初始化过程与HandlerMapping比较类似,都是从IOC容器中读取配置。经过这些过程,DispatcherServlet的初始化就全部完成了。

2.2 SpringMVC的分发请求

在分析SpringMVC对请求的分发之前,我们先来看看HandlerMapping的设计原理,HandlerMapping在SpringMVC分发请求的过程中起着十分重要的作用,一般每个handlerMapping都持有一系列从url请求到Controller的映射,以RequestMappingHandlerMapping为例,RequstMappingHandlerMapping是我们经常会用到的一个HandlerMapping,当我们配置<mvc:annotation-driven/>时,DispatcherServlet中会用它作为HandlerMapping。

RequstMappingHandlerMapping的父类实现了InitializingBean,所以通过getBean实例化它时会调用它的afterPropertiesSet方法,该方法代码如下

        public void afterPropertiesSet() {
		//实例化一个BuilderConfiguration对象并为它设置一些属性
                this.config = new RequestMappingInfo.BuilderConfiguration();
		this.config.setUrlPathHelper(getUrlPathHelper());
		this.config.setPathMatcher(getPathMatcher());
		this.config.setSuffixPatternMatch(this.useSuffixPatternMatch);
		this.config.setTrailingSlashMatch(this.useTrailingSlashMatch);
		this.config.setRegisteredSuffixPatternMatch(this.useRegisteredSuffixPatternMatch);
		this.config.setContentNegotiationManager(getContentNegotiationManager());
                //调用父类的afterPropertiesSet方法
		super.afterPropertiesSet();
	}

该方法中会调用它的父类AbstractHandlerMethodMapping中的afterPropertiesSet方法

        public void afterPropertiesSet() {
		//该方法用来将请求路径与控制器中的方法进行匹配
                initHandlerMethods();
	}
protected void initHandlerMethods() {
		if (logger.isDebugEnabled()) {
			logger.debug("Looking for request mappings in application context: " + getApplicationContext());
		}
		//获取上下文对象中所有的beanName
                String[] beanNames = (this.detectHandlerMethodsInAncestorContexts ?
				BeanFactoryUtils.beanNamesForTypeIncludingAncestors(getApplicationContext(), Object.class) :
				getApplicationContext().getBeanNamesForType(Object.class));

		//遍历所有的bean
                for (String beanName : beanNames) {
			if (!beanName.startsWith(SCOPED_TARGET_NAME_PREFIX)) {
				Class<?> beanType = null;
				try {
					//获取它们的类型
                                        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);
					}
				}
				//判断是否为Controller或者RequestMapping注解,如果是则取得类中处理请求的方法
                                if (beanType != null && isHandler(beanType)) {
					detectHandlerMethods(beanName);
				}
			}
		}
		handlerMethodsInitialized(getHandlerMethods());
	}
protected void detectHandlerMethods(final Object handler) {
		//获取类的类型
                Class<?> handlerType = (handler instanceof String ?
				getApplicationContext().getType((String) handler) : handler.getClass());
		
                final Class<?> userType = ClassUtils.getUserClass(handlerType);

		//这个map的key是Controller中的Method,value是RequestMappingInfo,
                //MetadataLookup中的方法会在selectMethods中被回调
                Map<Method, T> methods = MethodIntrospector.selectMethods(userType,
				new MethodIntrospector.MetadataLookup<T>() {
					@Override
					public T inspect(Method method) {
						try {
							return getMappingForMethod(method, userType);
						}
						catch (Throwable ex) {
							throw new IllegalStateException("Invalid mapping on handler class [" +
									userType.getName() + "]: " + method, ex);
						}
					}
				});

		if (logger.isDebugEnabled()) {
			logger.debug(methods.size() + " request handler methods found on " + userType + ": " + methods);
		}
		//遍历methods并注册处理请求的方法
                for (Map.Entry<Method, T> entry : methods.entrySet()) {
			Method invocableMethod = AopUtils.selectInvocableMethod(entry.getKey(), userType);
			T mapping = entry.getValue();
			registerHandlerMethod(handler, invocableMethod, mapping);
		}
	}

下面来看看selectMethods中是如何获取到RequestMappingInfo的

public static <T> Map<Method, T> 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;
        
		//添加处理请求的bean(Controller)
                if (!Proxy.isProxyClass(targetType)) {
			handlerTypes.add(targetType);
			specificHandlerType = targetType;
		}
		handlerTypes.addAll(Arrays.asList(targetType.getInterfaces()));
                //遍历这些bean
		for (Class<?> currentHandlerType : handlerTypes) {
			final Class<?> targetClass = (specificHandlerType != null ? specificHandlerType : currentHandlerType);
                        //doWithMethods中会调用MethodCallback的doWith方法对bean中的所有方法进行处理,包括父类的方法
			ReflectionUtils.doWithMethods(currentHandlerType, new ReflectionUtils.MethodCallback() {
				@Override
				public void doWith(Method method) {
					Method specificMethod = ClassUtils.getMostSpecificMethod(method, targetClass);
					//调用metadataLookup的inspect方法获取RequestioMappingInfo
                                        T result = metadataLookup.inspect(specificMethod);
					//将方法添加到map中
                                        if (result != null) {
						Method bridgedMethod = BridgeMethodResolver.findBridgedMethod(specificMethod);
						if (bridgedMethod == specificMethod || metadataLookup.inspect(bridgedMethod) == null) {
							methodMap.put(specificMethod, result);
						}
					}
				}
			}, ReflectionUtils.USER_DECLARED_METHODS);
		}

		return methodMap;
	}

具体取得RequestMappingInfo的过程在RequestMappingHandlerMapping的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);
			}
		}
		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));
		//这里是真正生成RequestMappingHandlerMapping的地方
                return (requestMapping != null ? createRequestMappingInfo(requestMapping, condition) : null);
	}
protected RequestMappingInfo createRequestMappingInfo(
			RequestMapping requestMapping, RequestCondition<?> customCondition) {
                //这里使用建造者模式构造RequestMappingHandlerMapping,build方法的实现在
		return RequestMappingInfo
				.paths(resolveEmbeddedValuesInPatterns(requestMapping.path()))
				.methods(requestMapping.method())
				.params(requestMapping.params())
				.headers(requestMapping.headers())
				.consumes(requestMapping.consumes())
				.produces(requestMapping.produces())
				.mappingName(requestMapping.name())
				.customCondition(customCondition)
				.options(this.config)
				.build();
	}
public RequestMappingInfo build() {
			ContentNegotiationManager manager = this.options.getContentNegotiationManager();

			PatternsRequestCondition patternsCondition = new PatternsRequestCondition(
					this.paths, this.options.getUrlPathHelper(), this.options.getPathMatcher(),
					this.options.useSuffixPatternMatch(), this.options.useTrailingSlashMatch(),
					this.options.getFileExtensions());

			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);
		}

到这里,我们不难看出RequestMappingInfo实际上是封装了请求路径到对应的处理的方法的映射。然后回到方法detectHandlerMethods中,接下来就是将处理请求的方法进行注册

protected void registerHandlerMethod(Object handler, Method method, T mapping) {
		//这个mappingRegistry是一个MappingRegistry的实例
                this.mappingRegistry.register(mapping, handler, method);
	}
public void register(T mapping, Object handler, Method method) {
			this.readWriteLock.writeLock().lock();
			try {
				HandlerMethod handlerMethod = createHandlerMethod(handler, method);
				assertUniqueMethodMapping(handlerMethod, mapping);

				if (logger.isInfoEnabled()) {
					logger.info("Mapped \"" + mapping + "\" onto " + handlerMethod);
				}
				//将映射与对应方法存起来
                                this.mappingLookup.put(mapping, handlerMethod);
                                //将请求路径与映射对象存起来
				List<String> directUrls = getDirectUrls(mapping);
				for (String url : directUrls) {
					this.urlLookup.add(url, mapping);
				}

				String name = null;
				//获取RequestMappingInfo的name,将name与method存起来
                                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<T>(mapping, handlerMethod, directUrls, name));
			}
			finally {
				this.readWriteLock.writeLock().unlock();
			}
		}

这样,所有请求的路径到Controller中处理请求的方法的映射都被注册到了RequestMappingHandlerMapping中。下面我们就来具体看看SpringMVC中对请求的分发处理的过程。让我们重新回到DispatcherServlet中,前面说过,web请求的分发主要通过它来完成,作为一个Servlet,DispatcherServlet通过doService方法来相应http请求,下面我们来看看doService方法的实现

protected void doService(HttpServletRequest request, HttpServletResponse response) throws Exception {
		if (logger.isDebugEnabled()) {
			String resumed = WebAsyncUtils.getAsyncManager(request).hasConcurrentResult() ? " resumed" : "";
			logger.debug("DispatcherServlet with name '" + getServletName() + "'" + resumed +
					" processing " + request.getMethod() + " request for [" + getRequestUri(request) + "]");
		}

		// Keep a snapshot of the request attributes in case of an include,
		// to be able to restore the original attributes after the include.
		Map<String, Object> attributesSnapshot = null;
		if (WebUtils.isIncludeRequest(request)) {
			attributesSnapshot = new HashMap<String, Object>();
			Enumeration<?> attrNames = request.getAttributeNames();
			while (attrNames.hasMoreElements()) {
				String attrName = (String) attrNames.nextElement();
				if (this.cleanupAfterInclude || attrName.startsWith("org.springframework.web.servlet")) {
					attributesSnapshot.put(attrName, request.getAttribute(attrName));
				}
			}
		}

		// 将一些参数设置到request请求中
		request.setAttribute(WEB_APPLICATION_CONTEXT_ATTRIBUTE, getWebApplicationContext());
		request.setAttribute(LOCALE_RESOLVER_ATTRIBUTE, this.localeResolver);
		request.setAttribute(THEME_RESOLVER_ATTRIBUTE, this.themeResolver);
		request.setAttribute(THEME_SOURCE_ATTRIBUTE, getThemeSource());

		FlashMap inputFlashMap = this.flashMapManager.retrieveAndUpdate(request, response);
		if (inputFlashMap != null) {
			request.setAttribute(INPUT_FLASH_MAP_ATTRIBUTE, Collections.unmodifiableMap(inputFlashMap));
		}
		request.setAttribute(OUTPUT_FLASH_MAP_ATTRIBUTE, new FlashMap());
		request.setAttribute(FLASH_MAP_MANAGER_ATTRIBUTE, this.flashMapManager);

		try {
			//具体分发的请求过程在这个方法中
                        doDispatch(request, response);
		}
		finally {
			if (!WebAsyncUtils.getAsyncManager(request).isConcurrentHandlingStarted()) {
				// Restore the original attribute snapshot, in case of an include.
				if (attributesSnapshot != null) {
					restoreAttributesAfterInclude(request, attributesSnapshot);
				}
			}
		}
	}
protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
		HttpServletRequest processedRequest = request;
		HandlerExecutionChain mappedHandler = null;
		boolean multipartRequestParsed = false;

		WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);

		try {
			ModelAndView mv = null;
			Exception dispatchException = null;

			try {
				processedRequest = checkMultipart(request);
				multipartRequestParsed = (processedRequest != request);

				// 根据请求获取HandlerExecutionChain,这个mappedHandler是一个HandlerExecutionChain对象,
                                //HandlerExecutionChain中设置了一个拦截器链,用来对handler进行增强,handler实际就是请求对应的Controller中的处理方法
				mappedHandler = getHandler(processedRequest);
				if (mappedHandler == null || mappedHandler.getHandler() == null) {
					noHandlerFound(processedRequest, response);
					return;
				}

				// 获取对应的HandlerAdapter,如果是配置了<mvc:annotation-driven/>,那么这里是RequestMappingHandlerAdapter
				HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());

				// Process last-modified header, if supported by the handler.
				String method = request.getMethod();
				boolean isGet = "GET".equals(method);
				if (isGet || "HEAD".equals(method)) {
					long lastModified = ha.getLastModified(request, mappedHandler.getHandler());
					if (logger.isDebugEnabled()) {
						logger.debug("Last-Modified value for [" + getRequestUri(request) + "] is: " + lastModified);
					}
					if (new ServletWebRequest(request, response).checkNotModified(lastModified) && isGet) {
						return;
					}
				}

				//调用HandlerExecutionChain中的interceptor进行前置处理
                                if (!mappedHandler.applyPreHandle(processedRequest, response)) {
					return;
				}

				// 调用HandlerAdapter的handler方法对handler进行处理并将处理的结果封装到ModelAndView中,为视图展现提供数据
				mv = ha.handle(processedRequest, response, mappedHandler.getHandler());

				if (asyncManager.isConcurrentHandlingStarted()) {
					return;
				}
                                //如果视图为空那么设置一个默认的视图
				applyDefaultViewName(processedRequest, mv);
				//调用HandlerExecutionChain中的interceptor进行后置处理
                                mappedHandler.applyPostHandle(processedRequest, response, mv);
			}
			catch (Exception ex) {
				dispatchException = ex;
			}
			catch (Throwable err) {
				// As of 4.3, we're processing Errors thrown from handler methods as well,
				// making them available for @ExceptionHandler methods and other scenarios.
				dispatchException = new NestedServletException("Handler dispatch failed", err);
			}
			//对ModelAndView视图的展现
                        processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);
		}
		catch (Exception ex) {
			triggerAfterCompletion(processedRequest, response, mappedHandler, ex);
		}
		catch (Throwable err) {
			triggerAfterCompletion(processedRequest, response, mappedHandler,
					new NestedServletException("Handler processing failed", err));
		}
		finally {
			if (asyncManager.isConcurrentHandlingStarted()) {
				// Instead of postHandle and afterCompletion
				if (mappedHandler != null) {
					mappedHandler.applyAfterConcurrentHandlingStarted(processedRequest, response);
				}
			}
			else {
				// Clean up any resources used by a multipart request.
				if (multipartRequestParsed) {
					cleanupMultipart(processedRequest);
				}
			}
		}
	}

我们来看看getHandler中是如何获取HandlerExecutionChain的

protected HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception {
		//遍历handlerMappings,调用HandlerMapping的getHandler方法获取HandlerExecutionChain,
                //在其中一个取到了立即返回
                for (HandlerMapping hm : this.handlerMappings) {
			if (logger.isTraceEnabled()) {
				logger.trace(
						"Testing handler map [" + hm + "] in DispatcherServlet with name '" + getServletName() + "'");
			}
			HandlerExecutionChain handler = hm.getHandler(request);
			if (handler != null) {
				return handler;
			}
		}
		return null;
	}

该方法的实现在AbstractHandlerMapping中,是一个final方法

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 = getApplicationContext().getBean(handlerName);
		}

		//获取HandlerExecutionChain
                HandlerExecutionChain executionChain = getHandlerExecutionChain(handler, request);
		if (CorsUtils.isCorsRequest(request)) {
			CorsConfiguration globalConfig = this.corsConfigSource.getCorsConfiguration(request);
			CorsConfiguration handlerConfig = getCorsConfiguration(handler, request);
			CorsConfiguration config = (globalConfig != null ? globalConfig.combine(handlerConfig) : handlerConfig);
			executionChain = getCorsHandlerExecutionChain(request, executionChain, config);
		}
		return executionChain;
	}

getHandlerInternal方法的实现在AbstractHandlerMethodMapping类中实现

protected HandlerMethod 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返回
                        return (handlerMethod != null ? handlerMethod.createWithResolvedBean() : null);
		}
		finally {
			this.mappingRegistry.releaseReadLock();
		}
	}
protected HandlerMethod lookupHandlerMethod(String lookupPath, HttpServletRequest request) throws Exception {
		List<Match> matches = new ArrayList<Match>();
		//这个directPathMatches实际上是一个RequestMappingInfo的集合,getMappingsByUrl方法会从之前

                //RequestMappingHandlerMapping初始化时缓存到mappingRegistry中的urlLookup里取得对应的RequestMappingInfo
                List<T> directPathMatches = this.mappingRegistry.getMappingsByUrl(lookupPath);
		//从RequestMappingHandlerMapping初始化时缓存到mappingRegistry中的mappingLookup里获取对应的HandlerMethod,
                //并和RequestMappingHandlerMapping、RequestMappingInfo一起封装到matches中
                if (directPathMatches != null) {
			addMatchingMappings(directPathMatches, matches, request);
		}
		if (matches.isEmpty()) {
			// No choice but to go through all mappings...
			addMatchingMappings(this.mappingRegistry.getMappings().keySet(), matches, request);
		}
                //返回一个最匹配的HandlerMethod
		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);
			if (matches.size() > 1) {
				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);
			return bestMatch.handlerMethod;
		}
		else {
			return handleNoMatch(this.mappingRegistry.getMappings().keySet(), lookupPath, request);
		}
	}

getHandlerExecutionChain的代码如下

protected HandlerExecutionChain getHandlerExecutionChain(Object handler, HttpServletRequest request) {
		//创建一个HandlerExecutionChain 
                HandlerExecutionChain chain = (handler instanceof HandlerExecutionChain ?
				(HandlerExecutionChain) handler : new HandlerExecutionChain(handler));
                //获取请求的路径
		String lookupPath = this.urlPathHelper.getLookupPathForRequest(request);
		//遍历adaptedInterceptors,这个adaptedInterceptors是一个HandlerInterceptor的集合,集合中的interceptor是在初始化时添加进去的
                for (HandlerInterceptor interceptor : this.adaptedInterceptors) {
			if (interceptor instanceof MappedInterceptor) {
				MappedInterceptor mappedInterceptor = (MappedInterceptor) interceptor;
				//如果拦截器与路径匹配的话就将拦截器添加到HandlerExecutionChain的interceptorList中
                                if (mappedInterceptor.matches(lookupPath, this.pathMatcher)) {
					chain.addInterceptor(mappedInterceptor.getInterceptor());
				}
			}
			else {
				chain.addInterceptor(interceptor);
			}
		}
		return chain;
	}

这样就取到了所需要的HandlerExecutionChain,然后回到DispatcherServlet的doDispatch方法中,接下来我们看看这个方法中如何通过调用HandlerAdapter的handle方法获取到ModelAndView,以RequestMappingHandlerAdapter为例,如果配置了<mvc:annotation-driven/>,就会以这个类作为HandlerAdapter的实现,具体的获取ModelAndView的过程在handleInternal方法中

protected ModelAndView handleInternal(HttpServletRequest request,
			HttpServletResponse response, HandlerMethod handlerMethod) throws Exception {

		ModelAndView mav;
		checkRequest(request);

		// Execute invokeHandlerMethod in synchronized block if required.
		if (this.synchronizeOnSession) {
			HttpSession session = request.getSession(false);
			if (session != null) {
				Object mutex = WebUtils.getSessionMutex(session);
				synchronized (mutex) {
					mav = invokeHandlerMethod(request, response, handlerMethod);
				}
			}
			else {
				// No HttpSession available -> no mutex necessary
				mav = invokeHandlerMethod(request, response, handlerMethod);
			}
		}
		else {
			//调用处理请求的handlerMethod,将方法中的参数及返回的视图封装到ModelAndView中
			mav = invokeHandlerMethod(request, response, handlerMethod);
		}

		if (!response.containsHeader(HEADER_CACHE_CONTROL)) {
			if (getSessionAttributesHandler(handlerMethod).hasSessionAttributes()) {
				applyCacheSeconds(response, this.cacheSecondsForSessionAttributeHandlers);
			}
			else {
				prepareResponse(response);
			}
		}

		return mav;
	}

再回到doDispatcher中,然后会调用processDispatchResult方法对视图进行展现

private void processDispatchResult(HttpServletRequest request, HttpServletResponse response,
			HandlerExecutionChain mappedHandler, ModelAndView mv, Exception exception) throws Exception {

		boolean errorView = false;

		if (exception != null) {
			if (exception instanceof ModelAndViewDefiningException) {
				logger.debug("ModelAndViewDefiningException encountered", exception);
				mv = ((ModelAndViewDefiningException) exception).getModelAndView();
			}
			else {
				Object handler = (mappedHandler != null ? mappedHandler.getHandler() : null);
				mv = processHandlerException(request, response, handler, exception);
				errorView = (mv != null);
			}
		}

		// 这里是展现视图的地方
		if (mv != null && !mv.wasCleared()) {
			render(mv, request, response);
			if (errorView) {
				WebUtils.clearErrorRequestAttributes(request);
			}
		}
		else {
			if (logger.isDebugEnabled()) {
				logger.debug("Null ModelAndView returned to DispatcherServlet with name '" + getServletName() +
						"': assuming HandlerAdapter completed request handling");
			}
		}

		if (WebAsyncUtils.getAsyncManager(request).isConcurrentHandlingStarted()) {
			// Concurrent handling started during a forward
			return;
		}

		if (mappedHandler != null) {
			mappedHandler.triggerAfterCompletion(request, response, null);
		}
	}

具体的展现视图的方法在render方法中

protected void render(ModelAndView mv, HttpServletRequest request, HttpServletResponse response) throws Exception {
		// 从request中获取locale信息并设置到response中
		Locale locale = this.localeResolver.resolveLocale(request);
		response.setLocale(locale);

		View view;
		if (mv.isReference()) {
			// 从ModelAndView对象中获取视图名称并用ViewResolver解析得到视图对象
			view = resolveViewName(mv.getViewName(), mv.getModelInternal(), locale, request);
			if (view == null) {
				throw new ServletException("Could not resolve view with name '" + mv.getViewName() +
						"' in servlet with name '" + getServletName() + "'");
			}
		}
		else {
			// 如果ModelAndView中已经包含视图对象那么直接取
			view = mv.getView();
			if (view == null) {
				throw new ServletException("ModelAndView [" + mv + "] neither contains a view name nor a " +
						"View object in servlet with name '" + getServletName() + "'");
			}
		}

		// Delegate to the View object for rendering.
		if (logger.isDebugEnabled()) {
			logger.debug("Rendering view [" + view + "] in DispatcherServlet with name '" + getServletName() + "'");
		}
		try {
			if (mv.getStatus() != null) {
				response.setStatus(mv.getStatus().value());
			}
			//调用视图对象的render方法展现视图
                        view.render(mv.getModelInternal(), request, response);
		}
		catch (Exception ex) {
			if (logger.isDebugEnabled()) {
				logger.debug("Error rendering view [" + view + "] in DispatcherServlet with name '" +
						getServletName() + "'", ex);
			}
			throw ex;
		}
	}

SpringMVC中封装了很多视图,比如JSP视图,FreeMarker视图,Velocity视图,Excel和PDF视图等等,我们来看看我们平常用的最多的JSP视图是如何展现的。具体的render方法的实现在AbstractView中

public void render(Map<String, ?> model, HttpServletRequest request, HttpServletResponse response) throws Exception {
		if (logger.isTraceEnabled()) {
			logger.trace("Rendering view with name '" + this.beanName + "' with model " + model +
				" and static attributes " + this.staticAttributes);
		}
                //将model中的数据都收集到一个map中
		Map<String, Object> mergedModel = createMergedOutputModel(model, request, response);
		prepareResponse(request, response);
		//展现模型数据到视图中
                renderMergedOutputModel(mergedModel, getRequestToExpose(request), response);
	}

renderMergedOutputModel的实现在InternalResourceView中

protected void renderMergedOutputModel(
			Map<String, Object> model, HttpServletRequest request, HttpServletResponse response) throws Exception {

		// 将模型的数据设置到HTTPServletRequest中
		exposeModelAsRequestAttributes(model, request);

		// Expose helpers as request attributes, if any.
		exposeHelpers(request);

		// 获取视图页面的内部资源路径
		String dispatcherPath = prepareForRendering(request, response);

		// 获取RequestDispatcher
		RequestDispatcher rd = getRequestDispatcher(request, dispatcherPath);
		if (rd == null) {
			throw new ServletException("Could not get RequestDispatcher for [" + getUrl() +
					"]: Check that the corresponding file exists within your web application archive!");
		}

		// If already included or response already committed, perform include, else forward.
		if (useInclude(request, response)) {
			response.setContentType(getContentType());
			if (logger.isDebugEnabled()) {
				logger.debug("Including resource [" + getUrl() + "] in InternalResourceView '" + getBeanName() + "'");
			}
			rd.include(request, response);
		}

		else {
			// 将请求到资源上,比如JSP页面,JSP页面的展现由web容器负责,view只是转发请求
			if (logger.isDebugEnabled()) {
				logger.debug("Forwarding to resource [" + getUrl() + "] in InternalResourceView '" + getBeanName() + "'");
			}
			rd.forward(request, response);
		}
	}

至此,SpringMVC就完成了将HTTP请求通过DispatcherServlet转发给页面呈现的过程。总结一下,SpringMVC的实现由以下三个步骤完成:

1、初始化handlerMapping时,http请求到Controller之间的映射会被载入到HandlerMapping中

2、当接收到http请求之后,DispatcherServlet会根据具体的URL信息,在HandlerMapping中查询,然后得到对应的HandlerExecutionChain。HandlerExecutionChain中封装了对应的处理请求的HandlerMethod,然后HandlerAdapter会执行处理请求的方法,生成需要的ModelAndView对象。

3、得到ModelAndView对象之后,DispatcherServlet把获取的模型数据交给特定的视图对象,从而完成这些数据的呈现工作,呈现的过程由对应的视图类的render方法完成。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值