SpringMVC组件之一: HandlerAdapter 深入学习

背景

最近由于需要了解参数解析器,涉及到HandlerAdapter ,由于对HandlerAdapter 不太熟,所以写了一篇文章熟读。

根据SpringMVC原理,SpringMVC 中通过 HandlerAdapter 来让 Handler 得到执行,为什么拿到 Handler 之后不直接执行呢?那是因为 SpringMVC 中我们定义 Handler 的方式多种多样(例如注解定义,可以使用@PostMapping,@GetMapping,也可以使用@RequestMapping),不同的 Handler 当然对应不同的执行方式,所以这中间就需要一个适配器 HandlerAdapter。

HandlerAdapter 体系

在这里插入图片描述
HandlerAdapter的子类并不是很多,可以逐一了解。

HandlerAdapter

HttpRequestHandlerAdapter

HttpRequestHandlerAdapter 主要用来处理实现了 HttpRequestHandler 接口的 handler,可以从它的实现看出:

public class HttpRequestHandlerAdapter implements HandlerAdapter {
	/**
	 * supports方法是判断当前的HandlerAdapter 是否能够处理handler对象
	 * 这里只有hanlder是实现了HttpRequestHandler接口才支持处理
	 */
	@Override
	public boolean supports(Object handler) {
		return (handler instanceof HttpRequestHandler);
	}

	/**
	 * HandlerAdapter的handle方法调用Handler对象的方法,这里直接调用handleRequest方法。
	 * 并返回ModelAndView 对象。
	 */
	@Override
	@Nullable
	public ModelAndView handle(HttpServletRequest request, HttpServletResponse response, Object handler)
			throws Exception {

		((HttpRequestHandler) handler).handleRequest(request, response);
		return null;
	}

	@Override
	public long getLastModified(HttpServletRequest request, Object handler) {
		if (handler instanceof LastModified) {
			return ((LastModified) handler).getLastModified(request);
		}
		return -1L;
	}

}

SimpleControllerHandlerAdapter

SimpleControllerHandlerAdapter 主要用来处理实现了 Controller接口的 handler,可以从它的实现看出:

public class SimpleControllerHandlerAdapter implements HandlerAdapter {
	/**
	 * 这里仅支持实现了Controller接口的Handler
	 */
	@Override
	public boolean supports(Object handler) {
		return (handler instanceof Controller);
	}

	@Override
	@Nullable
	public ModelAndView handle(HttpServletRequest request, HttpServletResponse response, Object handler)
			throws Exception {
		//调用Controller的handleRequest并返回ModelAndView对象
		return ((Controller) handler).handleRequest(request, response);
	}

	@Override
	public long getLastModified(HttpServletRequest request, Object handler) {
		if (handler instanceof LastModified) {
			return ((LastModified) handler).getLastModified(request);
		}
		return -1L;
	}

}

SimpleServletHandlerAdapter

这个用来处理实现了 Servlet 接口的 handler,在实际开发中我们很少用到这种,这里只是简单介绍一下

public class SimpleServletHandlerAdapter implements HandlerAdapter {

	/**
	 * 仅支持Servlet接口实现的Handler
	 * 
	 */
	@Override
	public boolean supports(Object handler) {
		return (handler instanceof Servlet);
	}

    /**
	 * 调用Servlet接口实现的service方法
	 * 
	 */
	@Override
	@Nullable
	public ModelAndView handle(HttpServletRequest request, HttpServletResponse response, Object handler)
			throws Exception {

		((Servlet) handler).service(request, response);
		return null;
	}

	@Override
	public long getLastModified(HttpServletRequest request, Object handler) {
		return -1;
	}

}

可以看到,这三种 HandlerAdapter 简单的原因主要是因为要调用的方法比较简单,直接调用就可以了。而 RequestMappingHandlerAdapter 复杂是因为调用的方法名不固定,所以复杂。

AbstractHandlerMethodAdapter

public abstract class AbstractHandlerMethodAdapter extends WebContentGenerator implements HandlerAdapter, Ordered {

	private int order = Ordered.LOWEST_PRECEDENCE;


	public AbstractHandlerMethodAdapter() {
		// no restriction of HTTP methods by default
		super(false);
	}


	/**
	 * HandlerAdapter 指定order顺序
	 */
	public void setOrder(int order) {
		this.order = order;
	}

	@Override
	public int getOrder() {
		return this.order;
	}


	 /**
	  * 这里的supports方法大致和上面三个类相同,需要handler实现HandlerMethod 接口
	  * 但是还有一个supportsInternal方法,交给子类实现,supportsInternal返回true才行
	  */
	@Override
	public final boolean supports(Object handler) {
		return (handler instanceof HandlerMethod && supportsInternal((HandlerMethod) handler));
	}

	/**
	 * 交给子类实现,supportsInternal返回true才能使用当前Adapter
	 */
	protected abstract boolean supportsInternal(HandlerMethod handlerMethod);

	/**
	 * 里面调用了handleInternal方法,handleInternal方法交给子类实现
	 */
	@Override
	@Nullable
	public final ModelAndView handle(HttpServletRequest request, HttpServletResponse response, Object handler)
			throws Exception {

		return handleInternal(request, response, (HandlerMethod) handler);
	}

	/**
	 * handleInternal方法交给子类实现,是处理HTTP请求的主要方法
	 */
	@Nullable
	protected abstract ModelAndView handleInternal(HttpServletRequest request,
			HttpServletResponse response, HandlerMethod handlerMethod) throws Exception;

	@Override
	public final long getLastModified(HttpServletRequest request, Object handler) {
		return getLastModifiedInternal(request, (HandlerMethod) handler);
	}

	protected abstract long getLastModifiedInternal(HttpServletRequest request, HandlerMethod handlerMethod);

}

RequestMappingHandlerAdapter

RequestMappingHandlerAdapter是AbstractHandlerMethodAdapter的直接子类,根据上面对AbstractHandlerMethodAdapter的了解,RequestMappingHandlerAdapter应该是要实现supportsInternal,handleInternal,getLastModifiedInternal三个方法的。

supportsInternal方法:

/**
 * 总是返回true,所以只要Handler实现了HandlerMethod 接口就可以使用此Adapter
 */
protected boolean supportsInternal(HandlerMethod handlerMethod) {
     return true;
  }

除此之外,这个类还有很多方法,所以我一步步的讲解:

初始化过程:

RequestMappingHandlerAdapter 实现了InitializingBean 接口,因此在Bean实例创建之后,会调用它的afterPropertiesSet()方法,先看看它的初始化方法做了什么:

public void afterPropertiesSet() {
		/**
		 * 看到这里想起来了什么?
		 * 就是@ControllerAdvice注解相关的方法,用来做全局异常统一处理的
		 * 功能:主要就是解析@ControllerAdvice注解的类,将@RequestMapping,@ModelAttribute,
		 * @InitBinder方法放入缓存中,以当前bean为key。
		 */
        this.initControllerAdviceCache();
        List handlers;
        /**
         * 初始化参数解析器HandlerMethodArgumentResolver,并把所有的参数解析器都放入
		 * HandlerMethodArgumentResolverComposite实例中
         */
        if (this.argumentResolvers == null) {
            handlers = this.getDefaultArgumentResolvers();
            this.argumentResolvers = (new HandlerMethodArgumentResolverComposite()).addResolvers(handlers);
        }
		
		/**
		 * 也是初始化参数解析器HandlerMethodArgumentResolver,并把所有的参数解析器都放入
		 * HandlerMethodArgumentResolverComposite实例中
		 */
        if (this.initBinderArgumentResolvers == null) {
            handlers = this.getDefaultInitBinderArgumentResolvers();
            this.initBinderArgumentResolvers = (new HandlerMethodArgumentResolverComposite()).addResolvers(handlers);
        }

		/**
		 * 初始化返回值处理器集合HandlerMethodReturnValueHandler,并把所有的返回值处理器都放入
		 * HandlerMethodReturnValueHandlerComposite中
		 */
        if (this.returnValueHandlers == null) {
            handlers = this.getDefaultReturnValueHandlers();
            this.returnValueHandlers = (new HandlerMethodReturnValueHandlerComposite()).addHandlers(handlers);
        }

    }

initControllerAdviceCache方法:通读源代码,主要就是解析@ControllerAdvice注解的类,将@RequestMapping,@ModelAttribute,@InitBinder方法放入缓存中,以当前bean为key。

private void initControllerAdviceCache() {
		if (getApplicationContext() == null) {
			return;
		}
		/**
		 * 获取@ControllerAdvice注解的bean
		 */
		List<ControllerAdviceBean> adviceBeans = ControllerAdviceBean.findAnnotatedBeans(getApplicationContext());

		List<Object> requestResponseBodyAdviceBeans = new ArrayList<>();

		for (ControllerAdviceBean adviceBean : adviceBeans) {
			Class<?> beanType = adviceBean.getBeanType();
			if (beanType == null) {
				throw new IllegalStateException("Unresolvable type for ControllerAdviceBean: " + adviceBean);
			}
			/**
			 * 查询当前Bean被@RequestMapping和@ModelAttribute注解的方法
			 * 把他们的方法对象放入this.modelAttributeAdviceCache集合,key = 当前bean对象
			 */
			Set<Method> attrMethods = MethodIntrospector.selectMethods(beanType, MODEL_ATTRIBUTE_METHODS);
			if (!attrMethods.isEmpty()) {
				this.modelAttributeAdviceCache.put(adviceBean, attrMethods);
			}
			/**
			 * 查询当前Bean被InitBinder注解的方法
			 * 把他们的方法对象放入this.initBinderAdviceCache集合,key = 当前bean对象
			 */
			Set<Method> binderMethods = MethodIntrospector.selectMethods(beanType, INIT_BINDER_METHODS);
			if (!binderMethods.isEmpty()) {
				this.initBinderAdviceCache.put(adviceBean, binderMethods);
			}
			/**
			 * 判断beanType是不是RequestBodyAdvice或者ResponseBodyAdvice的父类
			 * 是的话把当前bean加入到requestResponseBodyAdviceBeans集合
			 * requestResponseBodyAdviceBeans集合不为空,则放入this.requestResponseBodyAdvice集合
			 */
			if (RequestBodyAdvice.class.isAssignableFrom(beanType) || ResponseBodyAdvice.class.isAssignableFrom(beanType)) {
				requestResponseBodyAdviceBeans.add(adviceBean);
			}
		}
		if (!requestResponseBodyAdviceBeans.isEmpty()) {
			this.requestResponseBodyAdvice.addAll(0, requestResponseBodyAdviceBeans);
		}

        /**
         * 输出调试日志
         */
		if (logger.isDebugEnabled()) {
			int modelSize = this.modelAttributeAdviceCache.size();
			int binderSize = this.initBinderAdviceCache.size();
			int reqCount = getBodyAdviceCount(RequestBodyAdvice.class);
			int resCount = getBodyAdviceCount(ResponseBodyAdvice.class);
			if (modelSize == 0 && binderSize == 0 && reqCount == 0 && resCount == 0) {
				logger.debug("ControllerAdvice beans: none");
			}
			else {
				logger.debug("ControllerAdvice beans: " + modelSize + " @ModelAttribute, " + binderSize +
						" @InitBinder, " + reqCount + " RequestBodyAdvice, " + resCount + " ResponseBodyAdvice");
			}
		}
	}

再回到afterPropertiesSet()方法中,在设置参数解析器和返回值处理器时,都用到getDefaultXXX方法获取默认值。

private List<HandlerMethodArgumentResolver> getDefaultArgumentResolvers() {
		List<HandlerMethodArgumentResolver> resolvers = new ArrayList<>(30);

		// Annotation-based argument resolution
		resolvers.add(new RequestParamMethodArgumentResolver(getBeanFactory(), false));
		resolvers.add(new RequestParamMapMethodArgumentResolver());
		resolvers.add(new PathVariableMethodArgumentResolver());
		resolvers.add(new PathVariableMapMethodArgumentResolver());
		resolvers.add(new MatrixVariableMethodArgumentResolver());
		resolvers.add(new MatrixVariableMapMethodArgumentResolver());
		resolvers.add(new ServletModelAttributeMethodProcessor(false));
		resolvers.add(new RequestResponseBodyMethodProcessor(getMessageConverters(), this.requestResponseBodyAdvice));
		resolvers.add(new RequestPartMethodArgumentResolver(getMessageConverters(), this.requestResponseBodyAdvice));
		resolvers.add(new RequestHeaderMethodArgumentResolver(getBeanFactory()));
		resolvers.add(new RequestHeaderMapMethodArgumentResolver());
		resolvers.add(new ServletCookieValueMethodArgumentResolver(getBeanFactory()));
		resolvers.add(new ExpressionValueMethodArgumentResolver(getBeanFactory()));
		resolvers.add(new SessionAttributeMethodArgumentResolver());
		resolvers.add(new RequestAttributeMethodArgumentResolver());

		// Type-based argument resolution
		resolvers.add(new ServletRequestMethodArgumentResolver());
		resolvers.add(new ServletResponseMethodArgumentResolver());
		resolvers.add(new HttpEntityMethodProcessor(getMessageConverters(), this.requestResponseBodyAdvice));
		resolvers.add(new RedirectAttributesMethodArgumentResolver());
		resolvers.add(new ModelMethodProcessor());
		resolvers.add(new MapMethodProcessor());
		resolvers.add(new ErrorsMethodArgumentResolver());
		resolvers.add(new SessionStatusMethodArgumentResolver());
		resolvers.add(new UriComponentsBuilderMethodArgumentResolver());

		// Custom arguments
		if (getCustomArgumentResolvers() != null) {
			resolvers.addAll(getCustomArgumentResolvers());
		}

		// Catch-all
		resolvers.add(new RequestParamMethodArgumentResolver(getBeanFactory(), true));
		resolvers.add(new ServletModelAttributeMethodProcessor(true));

		return resolvers;
	}

private List<HandlerMethodArgumentResolver> getDefaultInitBinderArgumentResolvers() {
		List<HandlerMethodArgumentResolver> resolvers = new ArrayList<>(20);

		// Annotation-based argument resolution
		resolvers.add(new RequestParamMethodArgumentResolver(getBeanFactory(), false));
		resolvers.add(new RequestParamMapMethodArgumentResolver());
		resolvers.add(new PathVariableMethodArgumentResolver());
		resolvers.add(new PathVariableMapMethodArgumentResolver());
		resolvers.add(new MatrixVariableMethodArgumentResolver());
		resolvers.add(new MatrixVariableMapMethodArgumentResolver());
		resolvers.add(new ExpressionValueMethodArgumentResolver(getBeanFactory()));
		resolvers.add(new SessionAttributeMethodArgumentResolver());
		resolvers.add(new RequestAttributeMethodArgumentResolver());

		// Type-based argument resolution
		resolvers.add(new ServletRequestMethodArgumentResolver());
		resolvers.add(new ServletResponseMethodArgumentResolver());

		// Custom arguments
		if (getCustomArgumentResolvers() != null) {
			resolvers.addAll(getCustomArgumentResolvers());
		}

		// Catch-all
		resolvers.add(new RequestParamMethodArgumentResolver(getBeanFactory(), true));

		return resolvers;
	}

返回值处理器的默认值:

private List<HandlerMethodReturnValueHandler> getDefaultReturnValueHandlers() {
		List<HandlerMethodReturnValueHandler> handlers = new ArrayList<>(20);

		// Single-purpose return value types
		handlers.add(new ModelAndViewMethodReturnValueHandler());
		handlers.add(new ModelMethodProcessor());
		handlers.add(new ViewMethodReturnValueHandler());
		handlers.add(new ResponseBodyEmitterReturnValueHandler(getMessageConverters(),
				this.reactiveAdapterRegistry, this.taskExecutor, this.contentNegotiationManager));
		handlers.add(new StreamingResponseBodyReturnValueHandler());
		handlers.add(new HttpEntityMethodProcessor(getMessageConverters(),
				this.contentNegotiationManager, this.requestResponseBodyAdvice));
		handlers.add(new HttpHeadersReturnValueHandler());
		handlers.add(new CallableMethodReturnValueHandler());
		handlers.add(new DeferredResultMethodReturnValueHandler());
		handlers.add(new AsyncTaskMethodReturnValueHandler(this.beanFactory));

		// Annotation-based return value types
		handlers.add(new ModelAttributeMethodProcessor(false));
		handlers.add(new RequestResponseBodyMethodProcessor(getMessageConverters(),
				this.contentNegotiationManager, this.requestResponseBodyAdvice));

		// Multi-purpose return value types
		handlers.add(new ViewNameMethodReturnValueHandler());
		handlers.add(new MapMethodProcessor());

		// Custom return value types
		if (getCustomReturnValueHandlers() != null) {
			handlers.addAll(getCustomReturnValueHandlers());
		}

		// Catch-all
		if (!CollectionUtils.isEmpty(getModelAndViewResolvers())) {
			handlers.add(new ModelAndViewResolverMethodReturnValueHandler(getModelAndViewResolvers()));
		}
		else {
			handlers.add(new ModelAttributeMethodProcessor(true));
		}

		return handlers;
	}

请求执行过程:

根据前面的介绍,请求执行的入口方法实际上就是 handleInternal,所以这里我们就从 handleInternal 方法开始分析:

    @Override
	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) {
					//调用handler的方法
					mav = invokeHandlerMethod(request, response, handlerMethod);
				}
			}
			else {
				// No HttpSession available -> no mutex necessary
				//调用handler的方法
				mav = invokeHandlerMethod(request, response, handlerMethod);
			}
		}
		else {
			// No synchronization on session demanded at all...
			//调用handler的方法
			mav = invokeHandlerMethod(request, response, handlerMethod);
		}

		/**
		 * 处理响应
		 */
		if (!response.containsHeader(HEADER_CACHE_CONTROL)) {
			if (getSessionAttributesHandler(handlerMethod).hasSessionAttributes()) {
				applyCacheSeconds(response, this.cacheSecondsForSessionAttributeHandlers);
			}
			else {
				prepareResponse(response);
			}
		}

		return mav;
	}

以上做了三件事情:

  1. checkRequest(request)检查请求
  2. invokeHandlerMethod(request, response, handlerMethod)调用handler方法
  3. 如果session存在处理,处理缓存问题

checkRequest(request)方法:

protected final void checkRequest(HttpServletRequest request) throws ServletException {
		String method = request.getMethod();
		/**
		 * this.supportedMethods 其实一般不配置,为null
		 * 当 supportedMethods 不为空的时候,去检查是否支持请求方法。例如GET,POST
		 * 可以通过restrictDefaultSupportedMethods指定只处理GET,POST,HEAD,不过这个参数很少用
		 * 而且改起来相当麻烦,已经在抽象父类AbstractHandlerMethodAdapter 中写死为false了。
		 * 
		 */
		if (this.supportedMethods != null && !this.supportedMethods.contains(method)) {
			throw new HttpRequestMethodNotSupportedException(method, this.supportedMethods);
		}

		// Check whether a session is required.
		/**
		 * 作用:检查session是否存在
		 * 默认是不检查的,因为requireSession 默认为false;
		 */
		if (this.requireSession && request.getSession(false) == null) {
			throw new HttpSessionRequiredException("Pre-existing session required but none found");
		}
	}

invokeHandlerMethod方法:

@Nullable
protected ModelAndView invokeHandlerMethod(HttpServletRequest request,
		HttpServletResponse response, HandlerMethod handlerMethod) throws Exception {
	/**
	 * 将HttpServletRequest 和HttpServletResponse 封装为一个ServletWebRequest对象
	 * ServletWebRequest实现了NativeWebRequest接口,可以通过getNativeRequest方法和
	 * getNativeResponse方法获取到将HttpServletRequest 和HttpServletResponse
	 * 
	 */
	ServletWebRequest webRequest = new ServletWebRequest(request, response);
	try {
		/**
		 * 首先获取一个 WebDataBinderFactory 对象,该对象将用来构建 WebDataBinder。
		 */
		WebDataBinderFactory binderFactory = getDataBinderFactory(handlerMethod);
		/**
		 * 获取一个 ModelFactory 对象,该对象用来初始化/更新 Model 对象
		 */
		ModelFactory modelFactory = getModelFactory(handlerMethod, binderFactory);

		/**
		 * 接下来创建 ServletInvocableHandlerMethod 对象,一会方法的调用,将由它完成
		 */
		ServletInvocableHandlerMethod invocableMethod = createInvocableHandlerMethod(handlerMethod);
		/**
		 * 这里自然就是应用参数解析器到invocableMethod里面
		 */
		if (this.argumentResolvers != null) {
			invocableMethod.setHandlerMethodArgumentResolvers(this.argumentResolvers);
		}
		/**
		 * 这里自然就是应用返回值到invocableMethod里面
		 */
		if (this.returnValueHandlers != null) {
			invocableMethod.setHandlerMethodReturnValueHandlers(this.returnValueHandlers);
		}
		/**
		 * 设置上面获取到的WebDataBinderFactory 
		 */
		invocableMethod.setDataBinderFactory(binderFactory);
		invocableMethod.setParameterNameDiscoverer(this.parameterNameDiscoverer);

		/**
		 * 造一个 ModelAndViewContainer 对象,简单理解为存储 Model 和 View的对象就行
		 */
		ModelAndViewContainer mavContainer = new ModelAndViewContainer();
		/**
		 * 把 FlashMap 中的数据先添加进 ModelAndViewContainer 容器中
		 */
		mavContainer.addAllAttributes(RequestContextUtils.getInputFlashMap(request));
		/**
		 * 初始化 Model
		 */
		modelFactory.initModel(webRequest, mavContainer, invocableMethod);
		mavContainer.setIgnoreDefaultModelOnRedirect(this.ignoreDefaultModelOnRedirect);
		/**
		 * 创建异步请求
		 */
		AsyncWebRequest asyncWebRequest = WebAsyncUtils.createAsyncWebRequest(request, response);
		asyncWebRequest.setTimeout(this.asyncRequestTimeout);

		WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);
		asyncManager.setTaskExecutor(this.taskExecutor);
		asyncManager.setAsyncWebRequest(asyncWebRequest);
		asyncManager.registerCallableInterceptors(this.callableInterceptors);
		asyncManager.registerDeferredResultInterceptors(this.deferredResultInterceptors);

		if (asyncManager.hasConcurrentResult()) {
			Object result = asyncManager.getConcurrentResult();
			mavContainer = (ModelAndViewContainer) asyncManager.getConcurrentResultContext()[0];
			asyncManager.clearConcurrentResult();
			LogFormatUtils.traceDebug(logger, traceOn -> {
				String formatted = LogFormatUtils.formatValue(result, !traceOn);
				return "Resume with async result [" + formatted + "]";
			});
			invocableMethod = invocableMethod.wrapConcurrentResult(result);
		}

        /**
         * 调用 invokeAndHandle 方法去真正执行接口方法
         */ 
		invocableMethod.invokeAndHandle(webRequest, mavContainer);
		if (asyncManager.isConcurrentHandlingStarted()) {
			//如果是异步请求,则直接返回即可。
			return null;
		}
		/**
		 * 接下来调用 getModelAndView 方法去构造 ModelAndView 并返回
		 */
		return getModelAndView(mavContainer, modelFactory, webRequest);
	}
	finally {
		// 最后设置请求完成。
		webRequest.requestCompleted();
	}
}

总结

从这次分析HandlerAdapter的原理,可以得知:

  1. 为什么需要HandlerAdapter?
    因为有很多种方式定义控制器方法,而且适配这些不同方式定义的控制器方法,所以提供了HandlerAdapter。

  2. 默认参数解析器,默认返回值处理器有哪些?
    看上面的代码分析

  3. 控制器方法是怎么执行的
    看上面的invokeHandlerMethod方法。

  4. 要怎么添加自己自定义的参数解析器和返回值处理器?
    答:直接注册到Spring容器里面是没用的,必须要通过RequestMappingHandlerAdapter的方法去设置,它提供了ArgumentResolversReturnValueHandlers的settter和getter方法,在默认的配置的基础上,添加自定义的配置。

参考

https://segmentfault.com/a/1190000039775488

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值