SpringMVC源码总结(八)类型转换PropertyEditor的背后

PropertyEditor是Spring最初采用的转换策略。将会转移到Converter上。本文章主要对@InitBinder注解背后代码层面的运行过程做介绍。所以最好先熟悉它的用法然后来看通代码流程。 


先看实例,controller代码如下:
 
Java代码   收藏代码
  1. @Controller  
  2. public class FormAction{  
  3.       
  4. // 这样的方法里,一般是用来注册一些PropertyEditor  
  5.     @InitBinder    
  6.     public void initBinder(WebDataBinder binder) throws Exception {    
  7.         DateFormat df = new SimpleDateFormat("yyyy---MM---dd HH:mm:ss");    
  8.         CustomDateEditor dateEditor = new CustomDateEditor(df, true);    
  9.         binder.registerCustomEditor(Date.class, dateEditor);        
  10.     }     
  11.       
  12.       
  13.     @RequestMapping(value="/test/json",method=RequestMethod.GET)  
  14.     @ResponseBody  
  15.     public Map<String,Object> getFormData(Date date){  
  16.         Map<String,Object> map=new HashMap<String,Object>();  
  17.         map.put("name","lg");  
  18.         map.put("age",23);  
  19.         map.put("date",new Date());  
  20.         return map;  
  21.     }  
  22. }  

xml文件仅仅开启mvc:ananotation-driven:  
Java代码   收藏代码
  1. <mvc:annotation-driven />    

然后访问  http://localhost:8080/test/json?date=2014---08---3 03:34:23,便看到成功的获取到了数据。接下来源代码代码分析这一过程: 

由于使用了@RequestMapping所以会选择RequestMappingHandlerAdapter来调度执行相应的方法,如下:
 
Java代码   收藏代码
  1. /** 
  2.      * Invoke the {@link RequestMapping} handler method preparing a {@link ModelAndView} 
  3.      * if view resolution is required. 
  4.      */  
  5.     private ModelAndView invokeHandleMethod(HttpServletRequest request,  
  6.             HttpServletResponse response, HandlerMethod handlerMethod) throws Exception {  
  7.   
  8.         ServletWebRequest webRequest = new ServletWebRequest(request, response);  
  9. //我们关注的重点重点重点重点重点重点重点重点  
  10.         WebDataBinderFactory binderFactory = getDataBinderFactory(handlerMethod);  
  11.         ModelFactory modelFactory = getModelFactory(handlerMethod, binderFactory);  
  12.         ServletInvocableHandlerMethod requestMappingMethod = createRequestMappingMethod(handlerMethod, binderFactory);  
  13.   
  14.         ModelAndViewContainer mavContainer = new ModelAndViewContainer();  
  15.         mavContainer.addAllAttributes(RequestContextUtils.getInputFlashMap(request));  
  16.         modelFactory.initModel(webRequest, mavContainer, requestMappingMethod);  
  17.         mavContainer.setIgnoreDefaultModelOnRedirect(this.ignoreDefaultModelOnRedirect);  
  18.   
  19.         AsyncWebRequest asyncWebRequest = WebAsyncUtils.createAsyncWebRequest(request, response);  
  20.         asyncWebRequest.setTimeout(this.asyncRequestTimeout);  
  21.   
  22.         final WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);  
  23.         asyncManager.setTaskExecutor(this.taskExecutor);  
  24.         asyncManager.setAsyncWebRequest(asyncWebRequest);  
  25.         asyncManager.registerCallableInterceptors(this.callableInterceptors);  
  26.         asyncManager.registerDeferredResultInterceptors(this.deferredResultInterceptors);  
  27.   
  28.         if (asyncManager.hasConcurrentResult()) {  
  29.             Object result = asyncManager.getConcurrentResult();  
  30.             mavContainer = (ModelAndViewContainer) asyncManager.getConcurrentResultContext()[0];  
  31.             asyncManager.clearConcurrentResult();  
  32.   
  33.             if (logger.isDebugEnabled()) {  
  34.                 logger.debug("Found concurrent result value [" + result + "]");  
  35.             }  
  36.             requestMappingMethod = requestMappingMethod.wrapConcurrentResult(result);  
  37.         }  
  38.   
  39.         requestMappingMethod.invokeAndHandle(webRequest, mavContainer);  
  40.   
  41.         if (asyncManager.isConcurrentHandlingStarted()) {  
  42.             return null;  
  43.         }  
  44.   
  45.         return getModelAndView(mavContainer, modelFactory, webRequest);  
  46.     }  

这里面就是整个执行过程。首先绑定请求参数到方法的参数上,然后执行方法,接下来根据方法返回的类型来选择合适的HandlerMethodReturnValueHandler来进行处理,最后要么走view路线,要么直接写入response的body中返回。 

我们此时关注的重点是:如何绑定请求参数到方法的参数上的呢? 
WebDataBinderFactory binderFactory = getDataBinderFactory(handlerMethod); 
针对每次对该handlerMethod请求产生一个绑定工厂,由这个工厂来完成数据的绑定。 
这里的handlerMethod包含了 controller对象FormAction和、test/json映射到的方法即getFormData。 
然后详细看下getDataBinderFactory的实现:
 
Java代码   收藏代码
  1. private WebDataBinderFactory getDataBinderFactory(HandlerMethod handlerMethod) throws Exception {  
  2. //这里的handlerType便是controller的类型FormAction  
  3.         Class<?> handlerType = handlerMethod.getBeanType();  
  4.         Set<Method> methods = this.initBinderCache.get(handlerType);  
  5.         if (methods == null) {  
  6. //关注点1:找出FormAction类的所有的含有@InitBinder的方法(方法的返回类型必须为void),找到后同时缓存起来  
  7.             methods = HandlerMethodSelector.selectMethods(handlerType, INIT_BINDER_METHODS);  
  8.             this.initBinderCache.put(handlerType, methods);  
  9.         }  
  10.         List<InvocableHandlerMethod> initBinderMethods = new ArrayList<InvocableHandlerMethod>();  
  11.         // Global methods first  
  12. //关注点2:再寻找出全局的初始化Binder的方法  
  13.         for (Entry<ControllerAdviceBean, Set<Method>> entry : this.initBinderAdviceCache .entrySet()) {  
  14.             if (entry.getKey().isApplicableToBeanType(handlerType)) {  
  15.                 Object bean = entry.getKey().resolveBean();  
  16.                 for (Method method : entry.getValue()) {  
  17.                     initBinderMethods.add(createInitBinderMethod(bean, method));  
  18.                 }  
  19.             }  
  20.         }  
  21.         for (Method method : methods) {  
  22.             Object bean = handlerMethod.getBean();  
  23.             initBinderMethods.add(createInitBinderMethod(bean, method));  
  24.         }  
  25. //关注点3:找到了所有的与该handlerMethod有关的初始化binder的方法,保存起来  
  26.         return createDataBinderFactory(initBinderMethods);  
  27.     }  

上面稍微做了些注释,然后看下详细的内容: 
关注点1:就是使用过滤,过滤类为:INIT_BINDER_METHODS,如下
 
Java代码   收藏代码
  1. /** 
  2.      * MethodFilter that matches {@link InitBinder @InitBinder} methods. 
  3.      */  
  4.     public static final MethodFilter INIT_BINDER_METHODS = new MethodFilter() {  
  5.   
  6.         @Override  
  7.         public boolean matches(Method method) {  
  8.             return AnnotationUtils.findAnnotation(method, InitBinder.class) != null;  
  9.         }  
  10.     };  

这个过滤类就是在handlerType即FormAction中过滤那些含有@InitBinder注解的方法。找到了之后就缓存起来,供下次使用。key为:handlerType,value为找到的方法。存至initBinderCache中。 

关注点2:从initBinderAdviceCache中获取所有支持这个handlerType的method。这一块有待继续研究,这个initBinderAdviceCache是如何初始化来的等等。针对目前的工程来说,initBinderAdviceCache是为空的。 

关注点3:遍历所有找到的和handlerType有关的method,然后封装成InvocableHandlerMethod,如下:
 
Java代码   收藏代码
  1. for (Method method : methods) {  
  2.             Object bean = handlerMethod.getBean();  
  3.             initBinderMethods.add(createInitBinderMethod(bean, method));  
  4.         }  

Java代码   收藏代码
  1. private InvocableHandlerMethod createInitBinderMethod(Object bean, Method method) {  
  2.         InvocableHandlerMethod binderMethod = new InvocableHandlerMethod(bean, method);  
  3.         binderMethod.setHandlerMethodArgumentResolvers(this.initBinderArgumentResolvers);  
  4.         binderMethod.setDataBinderFactory(new DefaultDataBinderFactory(this.webBindingInitializer));  
  5.         binderMethod.setParameterNameDiscoverer(this.parameterNameDiscoverer);  
  6.         return binderMethod;  
  7.     }  

在封装的过程中,同时设置一些RequestMappingHandlerAdapter的一些参数进去initBinderArgumentResolvers、webBindingInitializer、parameterNameDiscoverer。 
封装完所有的方法后,创建出最终的WebDataBinderFactory。如下:
 
Java代码   收藏代码
  1. protected InitBinderDataBinderFactory createDataBinderFactory(List<InvocableHandlerMethod> binderMethods)  
  2.             throws Exception {  
  3.   
  4.         return new ServletRequestDataBinderFactory(binderMethods, getWebBindingInitializer());  
  5.     }  

getWebBindingInitializer()也是RequestMappingHandlerAdapter的webBindingInitializer参数。 

至此绑定数据的工厂完成了,包含了这个handlerType的所有的PropertyEditor。这是准备工作,然后就是等待执行这个我们自己的方法getFormData执行时来完成参数的绑定过程。 

绑定参数过程即getFormData的执行过程如下:
 
Java代码   收藏代码
  1. ServletInvocableHandlerMethod requestMappingMethod = createRequestMappingMethod(handlerMethod, binderFactory);  
  2. 略  
  3. requestMappingMethod.invokeAndHandle(webRequest, mavContainer);  

其中的requestMappingMethod经过了进一步的包装,已经包含刚才已经创建的绑定工厂。 
执行过程如下:
 
Java代码   收藏代码
  1. public final Object invokeForRequest(NativeWebRequest request, ModelAndViewContainer mavContainer,  
  2.             Object... providedArgs) throws Exception {  
  3.   
  4.         Object[] args = getMethodArgumentValues(request, mavContainer, providedArgs);  
  5.         if (logger.isTraceEnabled()) {  
  6.             StringBuilder sb = new StringBuilder("Invoking [");  
  7.             sb.append(getBeanType().getSimpleName()).append(".");  
  8.             sb.append(getMethod().getName()).append("] method with arguments ");  
  9.             sb.append(Arrays.asList(args));  
  10.             logger.trace(sb.toString());  
  11.         }  
  12.         Object returnValue = invoke(args);  
  13.         if (logger.isTraceEnabled()) {  
  14.             logger.trace("Method [" + getMethod().getName() + "] returned [" + returnValue + "]");  
  15.         }  
  16.         return returnValue;  
  17.     }  

分两大步,绑定参数和执行方法体。最重要的就是如何来绑定参数呢?  
Java代码   收藏代码
  1. private Object[] getMethodArgumentValues(NativeWebRequest request, ModelAndViewContainer mavContainer,  
  2.             Object... providedArgs) throws Exception {  
  3.   
  4.         MethodParameter[] parameters = getMethodParameters();  
  5.         Object[] args = new Object[parameters.length];  
  6.         for (int i = 0; i < parameters.length; i++) {  
  7.             MethodParameter parameter = parameters[i];  
  8.             parameter.initParameterNameDiscovery(this.parameterNameDiscoverer);  
  9.             GenericTypeResolver.resolveParameterType(parameter, getBean().getClass());  
  10.             args[i] = resolveProvidedArgument(parameter, providedArgs);  
  11.             if (args[i] != null) {  
  12.                 continue;  
  13.             }  
  14.             if (this.argumentResolvers.supportsParameter(parameter)) {  
  15.                 try {  
  16.                     args[i] = this.argumentResolvers.resolveArgument(  
  17.                             parameter, mavContainer, request, this.dataBinderFactory);  
  18.                     continue;  
  19.                 }  
  20.                 catch (Exception ex) {  
  21.                     if (logger.isTraceEnabled()) {  
  22.                         logger.trace(getArgumentResolutionErrorMessage("Error resolving argument", i), ex);  
  23.                     }  
  24.                     throw ex;  
  25.                 }  
  26.             }  
  27.             if (args[i] == null) {  
  28.                 String msg = getArgumentResolutionErrorMessage("No suitable resolver for argument", i);  
  29.                 throw new IllegalStateException(msg);  
  30.             }  
  31.         }  
  32.         return args;  
  33.     }  

绑定参数又引出来另一个重要名词:HandlerMethodArgumentResolver。args[i] = this.argumentResolvers.resolveArgument( 
parameter, mavContainer, request, this.dataBinderFactory);的具体内容如下
: 
Java代码   收藏代码
  1. /** 
  2.      * Iterate over registered {@link HandlerMethodArgumentResolver}s and invoke the one that supports it. 
  3.      * @exception IllegalStateException if no suitable {@link HandlerMethodArgumentResolver} is found. 
  4.      */  
  5.     @Override  
  6.     public Object resolveArgument(  
  7.             MethodParameter parameter, ModelAndViewContainer mavContainer,  
  8.             NativeWebRequest webRequest, WebDataBinderFactory binderFactory)  
  9.             throws Exception {  
  10.   
  11.         HandlerMethodArgumentResolver resolver = getArgumentResolver(parameter);  
  12.         Assert.notNull(resolver, "Unknown parameter type [" + parameter.getParameterType().getName() + "]");  
  13.         return resolver.resolveArgument(parameter, mavContainer, webRequest, binderFactory);  
  14.     }  

遍历所有已注册的HandlerMethodArgumentResolver,然后找出一个适合的来进行参数绑定,对于本工程来说,getFormData(Date date)的参数date默认是request params级别的,所以使用RequestParamMethodArgumentResolver来处理这一过程。处理过程如下:  
Java代码   收藏代码
  1. @Override  
  2.     public final Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer,  
  3.             NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {  
  4.   
  5.         Class<?> paramType = parameter.getParameterType();  
  6.         NamedValueInfo namedValueInfo = getNamedValueInfo(parameter);  
  7.   
  8.         Object arg = resolveName(namedValueInfo.name, parameter, webRequest);  
  9.         if (arg == null) {  
  10.             if (namedValueInfo.defaultValue != null) {  
  11.                 arg = resolveDefaultValue(namedValueInfo.defaultValue);  
  12.             }  
  13.             else if (namedValueInfo.required) {  
  14.                 handleMissingValue(namedValueInfo.name, parameter);  
  15.             }  
  16.             arg = handleNullValue(namedValueInfo.name, arg, paramType);  
  17.         }  
  18.         else if ("".equals(arg) && (namedValueInfo.defaultValue != null)) {  
  19.             arg = resolveDefaultValue(namedValueInfo.defaultValue);  
  20.         }  
  21.   
  22.         if (binderFactory != null) {  
  23.             WebDataBinder binder = binderFactory.createBinder(webRequest, null, namedValueInfo.name);  
  24.             arg = binder.convertIfNecessary(arg, paramType, parameter);  
  25.         }  
  26.   
  27.         handleResolvedValue(arg, namedValueInfo.name, parameter, mavContainer, webRequest);  
  28.   
  29.         return arg;  
  30.     }  

NamedValueInfo namedValueInfo = getNamedValueInfo(parameter);获取参数信息,就是按照@RequestParam的3个属性来收集的,即defaultValue=null、required=false、name=date, 
Object arg = resolveName(namedValueInfo.name, parameter, webRequest);然后就是获取原始数据,获取过程如下:
 
Java代码   收藏代码
  1. @Override  
  2.     protected Object resolveName(String name, MethodParameter parameter, NativeWebRequest webRequest) throws Exception {  
  3.         Object arg;  
  4.   
  5.         HttpServletRequest servletRequest = webRequest.getNativeRequest(HttpServletRequest.class);  
  6.         MultipartHttpServletRequest multipartRequest =  
  7.             WebUtils.getNativeRequest(servletRequest, MultipartHttpServletRequest.class);  
  8.   
  9.         if (MultipartFile.class.equals(parameter.getParameterType())) {  
  10.             assertIsMultipartRequest(servletRequest);  
  11.             Assert.notNull(multipartRequest, "Expected MultipartHttpServletRequest: is a MultipartResolver configured?");  
  12.             arg = multipartRequest.getFile(name);  
  13.         }  
  14.         else if (isMultipartFileCollection(parameter)) {  
  15.             assertIsMultipartRequest(servletRequest);  
  16.             Assert.notNull(multipartRequest, "Expected MultipartHttpServletRequest: is a MultipartResolver configured?");  
  17.             arg = multipartRequest.getFiles(name);  
  18.         }  
  19.         else if(isMultipartFileArray(parameter)) {  
  20.             assertIsMultipartRequest(servletRequest);  
  21.             Assert.notNull(multipartRequest, "Expected MultipartHttpServletRequest: is a MultipartResolver configured?");  
  22.             arg = multipartRequest.getFiles(name).toArray(new MultipartFile[0]);  
  23.         }  
  24.         else if ("javax.servlet.http.Part".equals(parameter.getParameterType().getName())) {  
  25.             assertIsMultipartRequest(servletRequest);  
  26.             arg = servletRequest.getPart(name);  
  27.         }  
  28.         else if (isPartCollection(parameter)) {  
  29.             assertIsMultipartRequest(servletRequest);  
  30.             arg = new ArrayList<Object>(servletRequest.getParts());  
  31.         }  
  32.         else if (isPartArray(parameter)) {  
  33.             assertIsMultipartRequest(servletRequest);  
  34.             arg = RequestPartResolver.resolvePart(servletRequest);  
  35.         }  
  36.         else {  
  37.             arg = null;  
  38.             if (multipartRequest != null) {  
  39.                 List<MultipartFile> files = multipartRequest.getFiles(name);  
  40.                 if (!files.isEmpty()) {  
  41.                     arg = (files.size() == 1 ? files.get(0) : files);  
  42.                 }  
  43.             }  
  44.             if (arg == null) {  
  45. //对于本工程,我们的重点在这里这里这里这里这里这里  
  46.                 String[] paramValues = webRequest.getParameterValues(name);  
  47.                 if (paramValues != null) {  
  48.                     arg = paramValues.length == 1 ? paramValues[0] : paramValues;  
  49.                 }  
  50.             }  
  51.         }  
  52.   
  53.         return arg;  
  54.     }  

通过webRequest.getParameterValues(name)来获取原始的字符串。这里便有涉及到了容器如tomcat的处理过程,这一获取参数的过程在本系列的第五篇文章tomcat的获取参数中进行了详细的源码介绍,那一篇主要是介绍乱码的。本文章不再介绍,接着说,这样就可以获取到我们请求的原始字符串"2014---08---3 03:34:23",接下来便是执行转换绑定的过程:  
Java代码   收藏代码
  1. if (binderFactory != null) {  
  2.             WebDataBinder binder = binderFactory.createBinder(webRequest, null, namedValueInfo.name);  
  3.             arg = binder.convertIfNecessary(arg, paramType, parameter);  
  4.         }  

这一过程就是要寻找我们已经注册的所有的PropertyEditor来进行转换,如果还没有找到,则使用另一套转换流程,使用conversionService来进行转换。我们慢慢来看这一过程,有了binderFactory便可以创建出WebDataBinder,具体的创建过程如下:  
Java代码   收藏代码
  1. public final WebDataBinder createBinder(NativeWebRequest webRequest, Object target, String objectName)  
  2.             throws Exception {  
  3.         WebDataBinder dataBinder = createBinderInstance(target, objectName, webRequest);  
  4.         if (this.initializer != null) {  
  5.             this.initializer.initBinder(dataBinder, webRequest);  
  6.         }  
  7.         initBinder(dataBinder, webRequest);  
  8.         return dataBinder;  
  9.     }  

先创建出WebDataBinder,然后使用initializer的initBinder方法来初始化一些PropertyEditor,initializer的类型为我们常见的ConfigurableWebBindingInitializer即在mvc:annotation-driven时默认注册的最终设置为RequestMappingHandlerAdapter的webBindingInitializer属性值。this.initializer.initBinder(dataBinder, webRequest);过程如下:  
Java代码   收藏代码
  1. @Override  
  2.     public void initBinder(WebDataBinder binder, WebRequest request) {  
  3.         binder.setAutoGrowNestedPaths(this.autoGrowNestedPaths);  
  4.         if (this.directFieldAccess) {  
  5.             binder.initDirectFieldAccess();  
  6.         }  
  7.         if (this.messageCodesResolver != null) {  
  8.             binder.setMessageCodesResolver(this.messageCodesResolver);  
  9.         }  
  10.         if (this.bindingErrorProcessor != null) {  
  11.             binder.setBindingErrorProcessor(this.bindingErrorProcessor);  
  12.         }  
  13.         if (this.validator != null && binder.getTarget() != null &&  
  14.                 this.validator.supports(binder.getTarget().getClass())) {  
  15.             binder.setValidator(this.validator);  
  16.         }  
  17.         if (this.conversionService != null) {  
  18.             binder.setConversionService(this.conversionService);  
  19.         }  
  20.         if (this.propertyEditorRegistrars != null) {  
  21.             for (PropertyEditorRegistrar propertyEditorRegistrar : this.propertyEditorRegistrars) {  
  22.                 propertyEditorRegistrar.registerCustomEditors(binder);  
  23.             }  
  24.         }  
  25.     }  

即设置一些我们conversionService、messageCodesResolver、validator 等,这些参数即我们在mvc:annotation中进行设置的,若无设置,采用默认的。 
继续执行initBinder(dataBinder, webRequest);
 
Java代码   收藏代码
  1. public void initBinder(WebDataBinder binder, NativeWebRequest request) throws Exception {  
  2.         for (InvocableHandlerMethod binderMethod : this.binderMethods) {  
  3.             if (isBinderMethodApplicable(binderMethod, binder)) {  
  4.                 Object returnValue = binderMethod.invokeForRequest(request, null, binder);  
  5.                 if (returnValue != null) {  
  6.                     throw new IllegalStateException("@InitBinder methods should return void: " + binderMethod);  
  7.                 }  
  8.             }  
  9.         }  
  10.     }  

执行那些适合我们已经创建的WebDataBinder,怎样才叫适合的呢?看isBinderMethodApplicable(binderMethod, binder)方法  
Java代码   收藏代码
  1. protected boolean isBinderMethodApplicable(HandlerMethod initBinderMethod, WebDataBinder binder) {  
  2.         InitBinder annot = initBinderMethod.getMethodAnnotation(InitBinder.class);  
  3.         Collection<String> names = Arrays.asList(annot.value());  
  4.         return (names.size() == 0 || names.contains(binder.getObjectName()));  
  5.     }  

当initBinderMethod上的@InitBinder注解指定了value,该value可以是多个,当它包含了我们的方法的参数date,则这个initBinderMethod就会被执行。当@InitBinder注解没有指定value,则也会被执行。所以为了不用执行一些不必要的initBinderMethod,我们最好为这些initBinderMethod上的@InitBinder加上value限定。对于我们写的initBinder便因此开始执行了。 
由binderFactory创建出来的WebDataBinder就此完成,然后才是详细的转换过程:
 
Java代码   收藏代码
  1. public <T> T convertIfNecessary(String propertyName, Object oldValue, Object newValue,  
  2.             Class<T> requiredType, TypeDescriptor typeDescriptor) throws IllegalArgumentException {  
  3.   
  4.         Object convertedValue = newValue;  
  5.   
  6.         // Custom editor for this type?  
  7.         PropertyEditor editor = this.propertyEditorRegistry.findCustomEditor(requiredType, propertyName);  
  8.   
  9.         ConversionFailedException firstAttemptEx = null;  
  10.   
  11.         // No custom editor but custom ConversionService specified?  
  12.         ConversionService conversionService = this.propertyEditorRegistry.getConversionService();  
  13.   
  14.             //略  
  15. }  

这里首先使用已注册的PropertyEditor,当仍然没有找到时才使用ConversionService。对于本工程来说,由于已经手动注册了对于Date的转换的PropertyEditor即CustomDateEditor,然后便会执行CustomDateEditor的具体的转换过程。至此,大体过程就算是完了。
  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值