HandlerMapping源码学习

spring framework 5.1.8

问题一:HandlerMapping是什么?
objects that define a mapping between requests and handler objects.

HandlerMapping接口的注释来看,它是一个关联请求与处理器的对象。它的作用是根据request找到相应的处理器Handler和Interceptors。

HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception;

其中HandlerExecutionChain对象主要由HandlerHandlerInterceptor[] interceptors组成。

可以类比我们在springMVC中的用法:

HandlerMapping 可以根据url请求找到最适合的controller,继而处理程序。

问题二: HandlerMapping的常用实现
  • AbstractHandlerMapping
  • AbstractUrlHandlerMapping
  • SimpleUrlHandlerMapping
  • BeanNameUrlHandlerMapping
  • RequestMappingHandlerMapping

在这里插入图片描述

AbstractHandlerMapping部分源码分析
...
  private final List<Object> interceptors = new ArrayList<>();

	private final List<HandlerInterceptor> adaptedInterceptors = new ArrayList<>();
...
//初始化interceptors
@Override
	protected void initApplicationContext() throws BeansException {
  //这个方法没有被实现 是空方法
		extendInterceptors(this.interceptors);
		detectMappedInterceptors(this.adaptedInterceptors);
		initInterceptors();
	}

//检测Interceptors
//将当前上下文以及当前上下文的父级上下文中类型是MappedInterceptor的Bean 添加到mappedInterceptors集合
protected void detectMappedInterceptors(List<HandlerInterceptor> mappedInterceptors) {
  //这种从spring上下文中检索Bean的方法很常用,在初始化HandlerMappings和HandlerAdapters中也多次用到
		mappedInterceptors.addAll(
				BeanFactoryUtils.beansOfTypeIncludingAncestors(
						obtainApplicationContext(), MappedInterceptor.class, true, false).values());
	}
//初始化Interceptors
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");
				}
        //将适配后的interceptor添加到adaptedInterceptors集合中
				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());
		}
	}
public final HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception {
	//根据request找到适配的Handler	①
  Object handler = getHandlerInternal(request);
  //如果handler,则选用默认的Handler
		if (handler == null) {
			handler = getDefaultHandler();
		}
		if (handler == null) {
			return null;
		}
  //如果handler是个字符串,那么从IOC容器中根据名称找到对应的Bean
		if (handler instanceof String) {
			String handlerName = (String) handler;
			handler = obtainApplicationContext().getBean(handlerName);
		}
		//②
		HandlerExecutionChain executionChain = getHandlerExecutionChain(handler, request);

		if (logger.isTraceEnabled()) {
			logger.trace("Mapped to " + handler);
		}
		else if (logger.isDebugEnabled() && !request.getDispatcherType().equals(DispatcherType.ASYNC)) {
			logger.debug("Mapped to " + executionChain.getHandler());
		}
		
  //如果是跨域请求
		if (CorsUtils.isCorsRequest(request)) {
			CorsConfiguration globalConfig = this.corsConfigurationSource.getCorsConfiguration(request);
			CorsConfiguration handlerConfig = getCorsConfiguration(handler, request);
			CorsConfiguration config = (globalConfig != null ? globalConfig.combine(handlerConfig) : handlerConfig);
			executionChain = getCorsHandlerExecutionChain(request, executionChain, config);
		}

		return executionChain;
	}


//① 其子类AbstractURLHandlerMapping实现了该方法
protected Object getHandlerInternal(HttpServletRequest request) throws Exception {
  //从request中取出请求路径
		String lookupPath = getUrlPathHelper().getLookupPathForRequest(request);
  	//①① 从hanlderMap关系中 获取handler
		Object handler = lookupHandler(lookupPath, request);
		if (handler == null) {
			
			Object rawHandler = null;
      //如果请求路径=/
			if ("/".equals(lookupPath)) {
				rawHandler = getRootHandler();
			}
      //如果rawHandler为空,那么使用默认Handler
			if (rawHandler == null) {
				rawHandler = getDefaultHandler();
			}
			if (rawHandler != null) {
				//如果handler是字符串,则在IOC容器中找到Bean
				if (rawHandler instanceof String) {
					String handlerName = (String) rawHandler;
					rawHandler = obtainApplicationContext().getBean(handlerName);
				}
        //验证handler
				validateHandler(rawHandler, request);
        //根据handler构造handlerExecutionChain,其中放入数个拦截器
				handler = buildPathExposingHandler(rawHandler, lookupPath, lookupPath, null);
			}
		}
		return handler;
	}
//①① 其中方法中的handlerMap数据是怎么来的后面会说。
//Map<String, Object> handlerMap = new LinkedHashMap<>(),存储的数据类似于key="/say",value=DemoController
@Nullable
	protected Object lookupHandler(String urlPath, HttpServletRequest request) throws Exception {
		//从path-handler关联关系中取出handler
		Object handler = this.handlerMap.get(urlPath);
		if (handler != null) {
			//如果handler是字符串,那么获取到其Bean
			if (handler instanceof String) {
				String handlerName = (String) handler;
				handler = obtainApplicationContext().getBean(handlerName);
			}
      //校验该handler
			validateHandler(handler, request);
      //根据handler生成HandlerExecutuonChain,并往其中加入PathExposingHandlerInterceptor/UriTemplateVariablesHandlerInterceptor 这两个拦截器
			return buildPathExposingHandler(handler, urlPath, urlPath, null);
		}

		//如果路径是采用的正则表达式匹配,那么这里会寻找一个最合适的url
		List<String> matchingPatterns = new ArrayList<>();
		for (String registeredPattern : this.handlerMap.keySet()) {
			if (getPathMatcher().match(registeredPattern, urlPath)) {
				matchingPatterns.add(registeredPattern);
			}
			else if (useTrailingSlashMatch()) {
				if (!registeredPattern.endsWith("/") && getPathMatcher().match(registeredPattern + "/", urlPath)) {
					matchingPatterns.add(registeredPattern + "/");
				}
			}
		}

		String bestMatch = null;
		Comparator<String> patternComparator = getPathMatcher().getPatternComparator(urlPath);
		if (!matchingPatterns.isEmpty()) {
			matchingPatterns.sort(patternComparator);
			if (logger.isTraceEnabled() && matchingPatterns.size() > 1) {
				logger.trace("Matching patterns " + matchingPatterns);
			}
			bestMatch = matchingPatterns.get(0);
		}
    //根据找到的最合适的url 找到Handler,随后跟上面一样 构造HandlerExecutionChain,并添加拦截器
		if (bestMatch != null) {
			handler = this.handlerMap.get(bestMatch);
			if (handler == null) {
				if (bestMatch.endsWith("/")) {
					handler = this.handlerMap.get(bestMatch.substring(0, bestMatch.length() - 1));
				}
				if (handler == null) {
					throw new IllegalStateException(
							"Could not find handler for best pattern match [" + bestMatch + "]");
				}
			}
			// Bean name or resolved handler?
			if (handler instanceof String) {
				String handlerName = (String) handler;
				handler = obtainApplicationContext().getBean(handlerName);
			}
			validateHandler(handler, request);
			String pathWithinMapping = getPathMatcher().extractPathWithinPattern(bestMatch, urlPath);

			// There might be multiple 'best patterns', let's make sure we have the correct URI template variables
			// for all of them
			Map<String, String> uriTemplateVariables = new LinkedHashMap<>();
			for (String matchingPattern : matchingPatterns) {
				if (patternComparator.compare(bestMatch, matchingPattern) == 0) {
					Map<String, String> vars = getPathMatcher().extractUriTemplateVariables(matchingPattern, urlPath);
					Map<String, String> decodedVars = getUrlPathHelper().decodePathVariables(request, vars);
					uriTemplateVariables.putAll(decodedVars);
				}
			}
			if (logger.isTraceEnabled() && uriTemplateVariables.size() > 0) {
				logger.trace("URI variables " + uriTemplateVariables);
			}
			return buildPathExposingHandler(handler, bestMatch, pathWithinMapping, uriTemplateVariables);
		}

		// No handler found...
		return null;
	}

//② 生成handler执行链
protected HandlerExecutionChain getHandlerExecutionChain(Object handler, HttpServletRequest request) {
		HandlerExecutionChain chain = (handler instanceof HandlerExecutionChain ?
				(HandlerExecutionChain) handler : new HandlerExecutionChain(handler));
		//?
		String lookupPath = this.urlPathHelper.getLookupPathForRequest(request);
		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;
	}

MappedInterceptor与HandlerInterceptor的区别?

MappedInterceptor:一个包括includePatterns和excludePatterns字符串集合并带有HandlerInterceptor的类。 很明显,就是对于某些地址做特殊包括和排除的拦截器。


AbstractUrlHandlerMapping部分源码分析

AbstractHandlerMapping作为抽象类,很多地方法实现都放在了AbstractUrlHandlerMapping类中,如getHandlerInternal()等,由于在上面说过了,这里不再赘述。至于handlerMap的值是怎么来的,在初始化handlerMapping时再介绍


SimpleUrlHandlerMapping部分源码分析
...
  private final Map<String, Object> urlMap = new LinkedHashMap<>();
...
  
@Override
	public void initApplicationContext() throws BeansException {
    //调用super,其实就是AbstractHandlerMapping#initApplicationContext()
		super.initApplicationContext();
  //① this.urlMap怎么被赋值的,我们在后面说
		registerHandlers(this.urlMap);
	}

protected void registerHandlers(Map<String, Object> urlMap) throws BeansException {
		if (urlMap.isEmpty()) {
			logger.trace("No patterns in " + formatMappingName());
		}
		else {
			urlMap.forEach((url, handler) -> {
				// Prepend with slash if not already present.
				if (!url.startsWith("/")) {
					url = "/" + url;
				}
				// Remove whitespace from handler bean name.
				if (handler instanceof String) {
					handler = ((String) handler).trim();
				}
        //②
				registerHandler(url, handler);
			});
			if (logger.isDebugEnabled()) {
				List<String> patterns = new ArrayList<>();
				if (getRootHandler() != null) {
					patterns.add("/");
				}
				if (getDefaultHandler() != null) {
					patterns.add("/**");
				}
				patterns.addAll(getHandlerMap().keySet());
				logger.debug("Patterns " + patterns + " in " + formatMappingName());
			}
		}
	}


//② 调用了父类AbstractURLHandlerMapping
protected void registerHandler(String urlPath, Object handler) throws BeansException, IllegalStateException {
		Assert.notNull(urlPath, "URL path must not be null");
		Assert.notNull(handler, "Handler object must not be null");
		Object resolvedHandler = handler;

		// Eagerly resolve handler if referencing singleton via name.
  //如果handler不是懒加载且是字符串类型
		if (!this.lazyInitHandlers && handler instanceof String) {
			String handlerName = (String) handler;
			ApplicationContext applicationContext = obtainApplicationContext();
			if (applicationContext.isSingleton(handlerName)) {
        //如果handlerName是单例Bean名称,则获取Bean
				resolvedHandler = applicationContext.getBean(handlerName);
			}
		}
		//根据urlPath 从handlerMap中获取handler
		Object mappedHandler = this.handlerMap.get(urlPath);
		if (mappedHandler != null) {
      //如果通过url解析出的handler和通过urlMap注册来的handler不一致 则抛出异常
			if (mappedHandler != resolvedHandler) {
				throw new IllegalStateException(
						"Cannot map " + getHandlerDescription(handler) + " to URL path [" + urlPath +
						"]: There is already " + getHandlerDescription(mappedHandler) + " mapped.");
			}
		}
  //如果mappedHandler为空
		else {
			if (urlPath.equals("/")) {
				if (logger.isTraceEnabled()) {
					logger.trace("Root mapping to " + getHandlerDescription(handler));
				}
        //如果请求路径是/ 将resolveHandler设置为rootHandler
				setRootHandler(resolvedHandler);
			}
			else if (urlPath.equals("/*")) {
				if (logger.isTraceEnabled()) {
					logger.trace("Default mapping to " + getHandlerDescription(handler));
				}
        //如果请求路径是/ 将resolveHandler设置为defaultHandler
				setDefaultHandler(resolvedHandler);
			}
			else {
        //将url与handler加入到handlerMap中 
				this.handlerMap.put(urlPath, resolvedHandler);
				if (logger.isTraceEnabled()) {
					logger.trace("Mapped [" + urlPath + "] onto " + getHandlerDescription(handler));
				}
			}
		}
	}

handlerMap赋值是在AbstractUrlHandlerMapping#registerHandler


BeanNameUrlHandlerMapping 部分源码分析

由于BeanNameUrlHandlerMapping继承自AbstractDetectingUrlHandlerMapping,所以initApplicationContext在父类

@Override
	public void initApplicationContext() throws ApplicationContextException {
    //该方法在AbstractHandlerMapping
		super.initApplicationContext();
		detectHandlers();
	}

//AbstractDetectingUrlHandlerMapping类
protected void detectHandlers() throws BeansException {
  //获取实际使用的ApplicationContext
		ApplicationContext applicationContext = obtainApplicationContext();
  //detectHandlersInAncestorContexts 表示是否从祖先上下文中检索handler 默认是false
		String[] beanNames = (this.detectHandlersInAncestorContexts ?
				BeanFactoryUtils.beanNamesForTypeIncludingAncestors(applicationContext, Object.class) :
                          //从上下文中取出type=Object的Bean ?拿出来的beanName是什么
				applicationContext.getBeanNamesForType(Object.class));

		// 接受任何可以确定url的bean名称.
		for (String beanName : beanNames) {
      //这个方法在BeanNameUrlHandlerMapping中
			String[] urls = determineUrlsForHandler(beanName);
			if (!ObjectUtils.isEmpty(urls)) {
        //AbstractUrlhandlerMapping#registerHandler
				registerHandler(urls, beanName);
			}
		}

		if ((logger.isDebugEnabled() && !getHandlerMap().isEmpty()) || logger.isTraceEnabled()) {
			logger.debug("Detected " + getHandlerMap().size() + " mappings in " + formatMappingName());
		}
	}

怎么查看系统默认的handlerMapping?

在Dispacther.properties文件中可以查看,如下:

org.springframework.web.servlet.HandlerMapping=org.springframework.web.servlet.handler.BeanNameUrlHandlerMapping,\
	org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping
handlerMapping的初始化
//DispatcherServlet
private void initHandlerMappings(ApplicationContext context) {
		this.handlerMappings = null;
		//是否检索所有Handlermappings 默认是true
		if (this.detectAllHandlerMappings) {
			// Find all HandlerMappings in the ApplicationContext, including ancestor contexts.
      //从当前上下文以及祖先上下文中查找类型是HandlerMapping的Bean
			Map<String, HandlerMapping> matchingBeans =
					BeanFactoryUtils.beansOfTypeIncludingAncestors(context, HandlerMapping.class, true, false);
			if (!matchingBeans.isEmpty()) {
				this.handlerMappings = new ArrayList<>(matchingBeans.values());
				// We keep HandlerMappings in sorted order.
        //由于HandlerMapping的实现类都实现了Ordered,所以诸多HandlerMapping之间是有顺序的,这里是进行排序
				AnnotationAwareOrderComparator.sort(this.handlerMappings);
			}
		}
		else {
			try {
        //如果不是检索所有上下文,则从当前上下文中检索handlerMapping
				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.
			}
		}

		// Ensure we have at least one HandlerMapping, by registering
		// a default HandlerMapping if no other mappings are found.
		if (this.handlerMappings == null) {
      //如果前面都获取不到HandlerMappings,则采用默认的HandlerMapping,即DispatchServlet.properties中预设的
			this.handlerMappings = getDefaultStrategies(context, HandlerMapping.class);
			if (logger.isTraceEnabled()) {
				logger.trace("No HandlerMappings declared for servlet '" + getServletName() +
						"': using default strategies from DispatcherServlet.properties");
			}
		}
	}

SimpleUrlHandlerMapping中urlMap是如何被赋值的&HandlerMapping是什么时候被加载进容器的?

首先跟HandlerMapping有关的即是Web项目,springmvc框架有@Enable机制,比如@EnbaleWebMvc

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Documented
@Import(DelegatingWebMvcConfiguration.class)
public @interface EnableWebMvc {
}

会执行DelegatingWebMvcConfiguration,而HanlderMapping注入为Bean是在其父类WebMvcConfigurationSupport

	@Bean
	public BeanNameUrlHandlerMapping beanNameHandlerMapping() {
		BeanNameUrlHandlerMapping mapping = new BeanNameUrlHandlerMapping();
		mapping.setOrder(2);
		mapping.setInterceptors(getInterceptors());
		mapping.setCorsConfigurations(getCorsConfigurations());
		return mapping;
	}
---
@Bean
	@Nullable
	public HandlerMapping resourceHandlerMapping() {
		Assert.state(this.applicationContext != null, "No ApplicationContext set");
		Assert.state(this.servletContext != null, "No ServletContext set");

		ResourceHandlerRegistry registry = new ResourceHandlerRegistry(this.applicationContext,
				this.servletContext, mvcContentNegotiationManager(), mvcUrlPathHelper());
		addResourceHandlers(registry);

		AbstractHandlerMapping handlerMapping = registry.getHandlerMapping();
		if (handlerMapping == null) {
			return null;
		}
		handlerMapping.setPathMatcher(mvcPathMatcher());
		handlerMapping.setUrlPathHelper(mvcUrlPathHelper());
		handlerMapping.setInterceptors(getInterceptors());
		handlerMapping.setCorsConfigurations(getCorsConfigurations());
		return handlerMapping;
	}
//ResourceHandlerRegistry#getHandlerMapping 
@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);
			}
		}

		SimpleUrlHandlerMapping handlerMapping = new SimpleUrlHandlerMapping();
		handlerMapping.setOrder(this.order);
    //这里就从外部将urlMap设置到SimpleUrlHandlerMapping中去
		handlerMapping.setUrlMap(urlMap);
		return handlerMapping;
	}
...
  //等等 在WebMvcConfigurationSupport类中 构成Bean的HandlerMapping还有多处

其次 作为SpringBoot项目,其AutoConfiguration也会注入HandlerMapping。

# spring.factories
# Auto Configure
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
...
org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguration,\
...
//WebMvcAutoConfiguration
...
  @Bean
		public WelcomePageHandlerMapping welcomePageHandlerMapping(ApplicationContext applicationContext) {
			return new WelcomePageHandlerMapping(new TemplateAvailabilityProviders(applicationContext),
					applicationContext, getWelcomePage(), this.mvcProperties.getStaticPathPattern());
		}
...
  @Bean
			public SimpleUrlHandlerMapping faviconHandlerMapping() {
				SimpleUrlHandlerMapping mapping = new SimpleUrlHandlerMapping();
				mapping.setOrder(Ordered.HIGHEST_PRECEDENCE + 1);
				mapping.setUrlMap(Collections.singletonMap("**/favicon.ico", faviconRequestHandler()));
				return mapping;
			}
...
  //等等

总结:SpringBoot程序在启动的过程中,将这些HandlerMapping注入为Bean,并为对应的实体中设置属性值。

并初始化HandlerMapping组件,在DispatchServlet#doDispatch() getHandler时,获取合适的可以使用的某一个HandlerExecutionChain

//DispatcherServlet
@Nullable
	protected HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception {
		if (this.handlerMappings != null) {
			for (HandlerMapping mapping : this.handlerMappings) {
				HandlerExecutionChain handler = mapping.getHandler(request);
				if (handler != null) {
					return handler;
				}
			}
		}
		return null;
	}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
HandlerMapping是Spring MVC框架中的一个接口,用于将HTTP请求映射到处理程序(即控制器)。 以下是HandlerMapping接口的源代码: ``` public interface HandlerMapping { String BEST_MATCHING_HANDLER_ATTRIBUTE = HandlerMapping.class.getName() + ".bestMatchingHandler"; @Nullable HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception; @Nullable default HandlerExecutionChain getHandler(HttpServletRequest request, boolean cache) throws Exception { if (cache) { Object handler = request.getAttribute(BEST_MATCHING_HANDLER_ATTRIBUTE); if (handler != null) { return (HandlerExecutionChain) handler; } } HandlerExecutionChain handler = getHandler(request); if (cache && handler != null) { request.setAttribute(BEST_MATCHING_HANDLER_ATTRIBUTE, handler); } return handler; } @Nullable default HandlerExecutionChain getHandlerInternal(HttpServletRequest request) throws Exception { return getHandler(request, true); } } ``` 该接口中定义了三个方法: 1. `getHandler(HttpServletRequest request)`:根据HTTP请求返回与之匹配的HandlerExecutionChain,如果没有匹配的则返回null。 2. `getHandler(HttpServletRequest request, boolean cache)`:根据HTTP请求返回与之匹配的HandlerExecutionChain,如果cache为true,则在request属性中查找是否已经缓存对应的HandlerExecutionChain,如果存在则直接返回。如果不存在则调用`getHandler(HttpServletRequest request)`方法,并将其缓存到request属性中。 3. `getHandlerInternal(HttpServletRequest request)`:该方法是`getHandler(HttpServletRequest request)`方法的默认实现,它会调用`getHandler(HttpServletRequest request, boolean cache)`方法,并将cache参数设置为true。 在Spring MVC中,有多种HandlerMapping的实现,如`RequestMappingHandlerMapping`、`SimpleUrlHandlerMapping`、`BeanNameUrlHandlerMapping`等。不同的HandlerMapping实现有不同的匹配规则和优先级,开发者可以根据自己的需要进行选择和配置。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值