what’s new in spring 3

本文详细解读从Spring 1.x到Spring 3.x的升级过程,重点介绍了功能增强、书写方式变化以及Spring MVC框架的显著更新。从依赖注入到注解配置,从XML到Annotation的发展之路,以及在配置文件、事务、AOP等方面的关键改进。同时,通过新增标签和代码示例,深入探讨了Spring MVC中annotation-driven配置的最新特性,包括返回值处理器、参数解析和消息转换器的使用,以及URL模板和验证功能的增强。

spring3发布已经很久了,但现在为止还不太清楚spring3到底增加了些什么内容,好像一夜之间,就从spring2.x升级到了spring3了,也没感觉到有什么变化。不过的确,对于使用ssh的人来说,spring确实没带来太多的惊喜。除了spring mvc增强之外,其它的部分也暂时用不上了。从网上找到一个英文版的《what's new in spring 3》pdf,原文作者为Habuma。简单翻译一下,从spring1到spring3作一个升级性的描述,看每个版本都增加了什么。以方便直接使用spring3或从spring2升级上来的开发人员从总体上对spring有一个了解和把握。

 


单从配置文件上来说,从spring1.x开始,开始使用了基于bean的声明式语法,所有的声明都是以bean开头的,即便是事务也是以继承AbstractTransactionBeanProxy来实现的;从spring2开始,就有了命名空间了,其中事务可以以tx命名空间开始,aop也可以aop命名空间开始了;到spring2.5,加入了注解的概念,开始让配置文件有所简化,spring也走上了一个从xml到annotation发展的道路。

 

第一部分:功能的增强
spring1.0
Dependency Injection:引入了依赖注入的思想,使得对象之间的关系更好地进行维护了;
POJO-oriented development:引进了而向pojo开发的编程开式,所有的对象都可以为简单的java对象;
Declarative AOP and transactions:申明式事务以及面向切面编程的支持,可以通过继承beanTransaction的方式进行事务编程;
MVC framework:引入了一个简单的mvc开发框架,从web层面提出一个开发方式。

spring2.0
Problem-specific XML:更简单的xml书写方式,提供命名空间支持;
Extensible configuration:支持自定义xml解析器,可以增加第三方的xml解析方式;
Bean scoping:bean生命周期的支持,可以增加为request,session周期内了;
Groovy, JRuby, and BeanShell:脚本语言支持;
JSP tag library:提供了一套mvc标签库,mvc框架增强;
Java 5 autoboxing and generics:java5的自动包装和解包,在配置层面可以直接用数字等代替了

Spring2.5
Annotation-driven wiring:提供了基于注解的配置方式,可以在java类上直接使用注解来将对象配置成一个bean;
Automatic bean configuration:提供自动发现机制,通过声明一个package,使spring直接从一个package中寻找bean并自动配置;
New annotation-driven MVC framework:mvc框架增强,提供@Controller等注解;
JUnit 4-based integration testing:junit4整合测试,使用基于注解的测试方式

第二部分:书写方式的变化
spring1.0

标签<property>和<value>以及和<ref>必须分开写,即首先写一个<property>标签,再在里面写一个<value>标签。


 

改进了1.0的写法,替代<value>标签,value可以直接作为一个属性写在<property>里面了,即如下的书写方式:

 

 

<property name="color" value="blue" />
 

spring2.0
提供了命名空间的支持,即不再需要书写<property>标签了,而是以命名空间属性的方式来书写,将属性直接写在bean声明中:

<bean id="linus"      class="com.springinaction.peanuts.Linus"      p:blanket-ref="blanket" />

 spring2.5
提供了自动发现机制,可以直接从一个package中寻找所有的bean配置了:

<context:component-scan   base-package="com.springinaction.peanuts" />
 

第三部分:spring3带来了什么
Spring Expression Language

spring解析式语言,和ognl相同的一套语法解析器,可以在spring配置文件中,注解配置中使用表达式,来从另一个地方读取一些数据,或者直接引用另一个对象的一些属性。支持的引用范围有:

  • Any bean ID:所有的bean都可以通过#{beanId}进行引用
  • systemProperties:系统变量,读取系统环境变量,通过#{systemProperties.favoriteColor}的读取方式
  • Scope/Context-specific:上下文变量,如request,session等

MVC增强

  • @RequestParam:在mvc方法中使用注解来注入一个界面参数(spring2.5)
  • @PathVariable:不再使用参数,而是使用更rest的路径变量来进行注入,如http://localhost:8080/spitter/{name}/list,来注入name变量
  • @RequestHeader:请求头注入,直接从request请求头信息中读取信息
  • @CookieValue:从cookie中读取注入信息
  • DefaultValue:除注入信息外,可以提供默认的信息

声明式验证
通过支持Hibernate or JSR-303来达到bean自动验证的目的

ETag Support
通过ShallowEtagHeaderFilter来达到界面缓存的目的,如果数据没变化,直接返回相应的界面,而不再进行请求
HTTP Methods支持
通过@RequestMapping来支持http put和delete请求
多种view支持
支持其它界面展现形式,比如json,rss等

总结
从spring1.x到spring3.x,总的一个思路是spring在向着使开发更简单化发展,当然随着技术的进步,本身也会越来越复杂,但复杂是仅指在框架内部,在使用上是越来越简单的。包括从xml的配置全面转向Annotataion,都表明了spring是向着使开发更简单的目标前进的。

 

=====转载http://www.iflym.com/index.php/code/whats-new-in-spring-3-english-translate.html================

 

SpringMVC:

 

1:mvc annotation-driven 新增标签
以下为spring mvc 3.1中annotation-driven所支持的全部配置。

Xml代码 复制代码 收藏代码
  1. < mvc:annotation-driven message-codes-resolver = "bean ref" validator = "" conversion-service = "" >
  2. < mvc:return-value-handlers >
  3. < bean > </ bean >
  4. </ mvc:return-value-handlers >
  5. < mvc:argument-resolvers >
  6. </ mvc:argument-resolvers >
  7. < mvc:message-converters >
  8. </ mvc:message-converters > [/color]
  9. </ mvc:annotation-driven >
<mvc:annotation-driven  message-codes-resolver ="bean ref" validator="" conversion-service="">
   
     <mvc:return-value-handlers>
        <bean></bean>
    </mvc:return-value-handlers>
    
    <mvc:argument-resolvers>
    </mvc:argument-resolvers>
    
    <mvc:message-converters>
    </mvc:message-converters>[/color]
</mvc:annotation-driven>



其中3.1新增部分如下
return-value-handlers
允许注册实现了HandlerMethodReturnValueHandler接口的bean,来对handler method的特定的返回类型做处理。
HandlerMethodReturnValueHandler接口中定义了两个方法
supportsReturnType 方法用来确定此实现类是否支持对应返回类型。
handleReturnValue 则用来处理具体的返回类型。

例如以下的handlerMethod

Java代码 复制代码 收藏代码
  1. @RequestMapping ( "/testReturnHandlers" )
  2. public User testHandlerReturnMethod(){
  3. User u = new User();
  4. u.setUserName("test" );
  5. return u;
  6. }
	@RequestMapping("/testReturnHandlers")
	public User testHandlerReturnMethod(){
		User u  = new User();
		u.setUserName("test");
		return u;
	}


所返回的类型为一个pojo,正常情况下spring mvc无法解析,将转由DefaultRequestToViewNameTranslator 解析出一个缺省的view name,转到 testReturnHandlers.jsp,
我们增加以下配置

Xml代码 复制代码 收藏代码
  1. < mvc:annotation-driven validator = "validator" >
  2. color = red ] < mvc:return-value-handlers >
  3. < bean class = "net.zhepu.web.handlers.returnHandler.UserHandlers" > </ bean >
  4. </ mvc:return-value-handlers > [/color]
  5. </ mvc:annotation-driven >
	<mvc:annotation-driven validator="validator">
[color=red]        <mvc:return-value-handlers> 
            <bean  class="net.zhepu.web.handlers.returnHandler.UserHandlers"></bean> 
		</mvc:return-value-handlers>[/color]
	</mvc:annotation-driven>


及如下实现类

Java代码 复制代码 收藏代码
  1. public class UserHandlers implements HandlerMethodReturnValueHandler {
  2. Logger logger = LoggerFactory.getLogger(this .getClass());
  3. @Override
  4. public boolean supportsReturnType(MethodParameter returnType) {
  5. Class<?> type = returnType.getParameterType();
  6. if (User. class .equals(type))
  7. {
  8. return true ;
  9. }
  10. return false ;
  11. }
  12. @Override
  13. public void handleReturnValue(Object returnValue,
  14. MethodParameter returnType, ModelAndViewContainer mavContainer,
  15. NativeWebRequest webRequest) throws Exception {
  16. logger.info("handler for return type users " );
  17. mavContainer.setViewName("helloworld" );
  18. }
  19. }
public class UserHandlers implements HandlerMethodReturnValueHandler {
    Logger logger = LoggerFactory.getLogger(this.getClass());
	@Override
	public boolean supportsReturnType(MethodParameter returnType) {
		Class<?> type = returnType.getParameterType();
		if(User.class.equals(type))
		{
			return true;
		}
		return false;
	}

	@Override
	public void handleReturnValue(Object returnValue,
			MethodParameter returnType, ModelAndViewContainer mavContainer,
			NativeWebRequest webRequest) throws Exception {
		logger.info("handler  for return type users ");
		mavContainer.setViewName("helloworld");
	}

}


此时再访问 http://localhost:8080/springmvc/testReturnHandlers ,将交由 UserHandlers来处理返回类型为User的返回值。

argument-resolvers
允许注册实现了WebArgumentResolver接口的bean,来对handlerMethod中的用户自定义的参数或annotation进行解析
例如

Xml代码 复制代码 收藏代码
  1. < mvc:annotation-driven validator = "validator" >
  2. < mvc:argument-resolvers >
  3. < bean
  4. class = "net.zhepu.web.handlers.argumentHandler.MyCustomerWebArgumentHandler" />
  5. </ mvc:argument-resolvers >
  6. </ mvc:annotation-driven >
	<mvc:annotation-driven validator="validator">
		<mvc:argument-resolvers>
			<bean
				class="net.zhepu.web.handlers.argumentHandler.MyCustomerWebArgumentHandler" />
		</mvc:argument-resolvers>

	</mvc:annotation-driven>


对应java代码如下

Java代码 复制代码 收藏代码
  1. public class MyCustomerWebArgumentHandler implements WebArgumentResolver {
  2. @Override
  3. public Object resolveArgument(MethodParameter methodParameter,
  4. NativeWebRequest webRequest) throws Exception {
  5. if (methodParameter.getParameterType().equals(MyArgument. class )) {
  6. MyArgument argu = new MyArgument();
  7. argu.setArgumentName("winzip" );
  8. argu.setArgumentValue("123456" );
  9. return argu;
  10. }
  11. return UNRESOLVED;
  12. }
  13. }
public class MyCustomerWebArgumentHandler implements WebArgumentResolver {

	@Override
	public Object resolveArgument(MethodParameter methodParameter,
			NativeWebRequest webRequest) throws Exception {
		if (methodParameter.getParameterType().equals(MyArgument.class)) {
			MyArgument argu = new MyArgument();
			argu.setArgumentName("winzip");
			argu.setArgumentValue("123456");
			return argu;
		}
		return UNRESOLVED;
	}

}


这里我们定义了一个 customer webArgumentHandler,当handler method中参数类型为 MyArgument时生成对参数的类型绑定操作。
注意新注册的webArgumentHandler的优先级最低,即如果系统缺省注册的ArgumentHandler已经可以解析对应的参数类型时,就不会再调用到新注册的customer ArgumentHandler了。

message-converters
允许注册实现了HttpMessageConverter接口的bean,来对requestbody 或responsebody中的数据进行解析
例如
假设我们使用 text/plain格式发送一串字符串来表示User对象,各个属性值使用”|”来分隔。例如 winzip|123456|13818888888,期望转为user对象,各属性内容为user.username = winzip,user.password=123456;user.mobileNO = 13818888888
以下代码中supports表示此httpmessageConverter实现类针对 User类进行解析。
构造函数中调用 super(new MediaType("text", "plain"));以表示支持 text/plain格式的输入。

Java代码 复制代码 收藏代码
  1. public class MyCustomerMessageConverter extends
  2. AbstractHttpMessageConverter<Object> {
  3. @Override
  4. protected boolean supports(Class<?> clazz) {
  5. if (clazz.equals(User. class )) {
  6. return true ;
  7. }
  8. return false ;
  9. }
  10. public MyCustomerMessageConverter() {
  11. super ( new MediaType( "text" , "plain" ));
  12. }
  13. @Override
  14. protected Object readInternal(Class<? extends Object> clazz,
  15. HttpInputMessage inputMessage) throws IOException,
  16. HttpMessageNotReadableException {
  17. Charset charset;
  18. MediaType contentType = inputMessage.getHeaders().getContentType();
  19. if (contentType != null && contentType.getCharSet() != null ) {
  20. charset = contentType.getCharSet();
  21. } else {
  22. charset = Charset.forName("UTF-8" );
  23. }
  24. String input = FileCopyUtils.copyToString(new InputStreamReader(
  25. inputMessage.getBody(), charset));
  26. logger.info(input);
  27. String[] s = input.split("\\|" );
  28. User u = new User();
  29. u.setUserName(s[0 ]);
  30. u.setPassword(s[1 ]);
  31. u.setMobileNO(s[2 ]);
  32. return u;
  33. }
  34. @Override
  35. protected void writeInternal(Object t, HttpOutputMessage outputMessage)
  36. throws IOException, HttpMessageNotWritableException {
  37. }
public class MyCustomerMessageConverter extends
		AbstractHttpMessageConverter<Object> {
	@Override
	protected boolean supports(Class<?> clazz) {
		if (clazz.equals(User.class)) {
			return true;
		}
		return false;
	}

	public MyCustomerMessageConverter() {
		super(new MediaType("text", "plain"));
	}

	@Override
	protected Object readInternal(Class<? extends Object> clazz,
			HttpInputMessage inputMessage) throws IOException,
			HttpMessageNotReadableException {
		Charset charset;
		MediaType contentType = inputMessage.getHeaders().getContentType();
		if (contentType != null && contentType.getCharSet() != null) {
			charset = contentType.getCharSet();
		} else {
			charset = Charset.forName("UTF-8");
		}
		String input = FileCopyUtils.copyToString(new InputStreamReader(
				inputMessage.getBody(), charset));
		logger.info(input);
		String[] s = input.split("\\|");
		User u = new User();
		u.setUserName(s[0]);
		u.setPassword(s[1]);
		u.setMobileNO(s[2]);
		return u;
	}

	@Override
	protected void writeInternal(Object t, HttpOutputMessage outputMessage)
			throws IOException, HttpMessageNotWritableException {

	}


修改servlet context xml配置文件,增加message-converters的相应配置如下。

Xml代码 复制代码 收藏代码
  1. < mvc:message-converters >
  2. < bean class = "net.zhepu.web.handlers.messageConverterHandler.MyCustomerMessageConverter" > </ bean >
  3. </ mvc:message-converters >
        <mvc:message-converters>
            <bean class="net.zhepu.web.handlers.messageConverterHandler.MyCustomerMessageConverter"></bean>
        </mvc:message-converters>




message-codes-resolver
先看看spring mvc中对于messageCodeResolver的用法。
spring mvc中使用DefaultMessageCodesResolver作为缺省的MessageCodesResolver的实现类,其作用是对valid errors中的errorcode进行解析。其解析方式如下
当解析error global object注册的errorcode时,errorcode的查找顺序为
1:errorcode.validationobjectname
2:errorcode
例如
以下声明中

Java代码 复制代码 收藏代码
  1. public String helloWorld2( @ModelAttribute ( "user" ) User u,
  2. BindingResult result)
public String helloWorld2(@ModelAttribute("user") User u,
			BindingResult result)


当使用 result.reject("testFlag");来注册一个globat error object时,spring mvc将在messageSource中先查找 testFlag.user这个errorcode,当找不到时再查找testFlag这个errorcode。

当解析fields error时,将按以下顺序生成error code
1.: code + "." + object name + "." + field
2.: code + "." + field
3.: code + "." + field type
4.: code

还是以上面的代码为例,当使用 result.rejectValue("userName", "testFlag");来注册一个针对user.UserName属性的错误描述时,errors对象中将生成以下的error code list,
1.: testFlag.user.userName
2.: testFlag.userName
3.: testFlag.java.lang.String
4.: testFlag

而mvc:annotation-driven新增的属性message-codes-resolver则提供了注册自定义的MessageCodesResolver的手段。
例如上面想要在所有的error code前增加前缀validation.的话,可以这么来做

Xml代码 复制代码 收藏代码
  1. < mvc:annotation-driven validator = "validator" message-codes-resolver = "messageCodeResolver" >
  2. </ mvc:annotation-driven >
<mvc:annotation-driven validator="validator" message-codes-resolver="messageCodeResolver">
</mvc:annotation-driven>


新增messageCodeResolver bean定义如下

Java代码 复制代码 收藏代码
  1. <bean id= "messageCodeResolver" class = "org.springframework.validation.DefaultMessageCodesResolver" >
  2. <property name="prefix" value= "validation." ></property>
  3. </bean>
	<bean id="messageCodeResolver" class="org.springframework.validation.DefaultMessageCodesResolver">
	    <property name="prefix" value="validation."></property>
	</bean>


此时,所有的errorcode都会生成缺省前缀 validation.
例如前面的 result.reject("testFlag"); 生成的error code list就变为了
validation.testFlag.user 和 validation.testFlag了。




2: @RequestMapping 新增参数Consumes 和Produces
前面介绍过@RequestMapping的参数中有一个header的参数,来指定handler method能接受的http request 请求的header内容。
而consumes和produces则更进一步,直接指定所能接受或产生的request请求的content type。
例如

Java代码 复制代码 收藏代码
  1. @RequestMapping (value= "/testMsgConverter" ,consumes= "text/plain" ,produces= "application/json" )
@RequestMapping(value="/testMsgConverter",consumes="text/plain",produces="application/json")


表示handlermethod接受的请求的header中的 Content-Type为text/plain;
Accept为application/json


3: URI Template 新增功能
这部分的例子直接照抄Spring 3.1 M2: Spring MVC Enhancements 中的示例

1: @PathVariable 声明的参数可自动加入到model中。
例如

Java代码 复制代码 收藏代码
  1. @RequestMapping ( "/develop/apps/edit/{slug}" )
  2. public String editForm( @PathVariable String slug, Model model) {
  3. model.addAttribute("slug" , slug);
  4. // ...
  5. }
@RequestMapping("/develop/apps/edit/{slug}")
public String editForm(@PathVariable String slug, Model model) {
	model.addAttribute("slug", slug);
    // ...
}


现在可以写为

Java代码 复制代码 收藏代码
  1. @RequestMapping ( "/develop/apps/edit/{slug}" )
  2. public String editForm( @PathVariable String slug, Model model) {
  3. // model contains "slug" variable
  4. }
@RequestMapping("/develop/apps/edit/{slug}")
public String editForm(@PathVariable String slug, Model model) {
    // model contains "slug" variable
}



2:handler method中的redirect string可支持url template了
例如

Java代码 复制代码 收藏代码
  1. @RequestMapping (
  2. value="/groups/{group}/events/{year}/{month}/{slug}/rooms" ,
  3. method=RequestMethod.POST)
  4. public String createRoom(
  5. @PathVariable String group, @PathVariable Integer year,
  6. @PathVariable Integer month, @PathVariable String slug) {
  7. // ...
  8. return "redirect:/groups/" + group + "/events/" + year + "/" + month + "/" + slug;
  9. }
@RequestMapping(
    value="/groups/{group}/events/{year}/{month}/{slug}/rooms",
    method=RequestMethod.POST)
public String createRoom(
    @PathVariable String group, @PathVariable Integer year,
    @PathVariable Integer month, @PathVariable String slug) {
    // ...
    return "redirect:/groups/" + group + "/events/" + year + "/" + month + "/" + slug;
}


现在可写为

Java代码 复制代码 收藏代码
  1. @RequestMapping (
  2. value="/groups/{group}/events/{year}/{month}/{slug}/rooms" ,
  3. method=RequestMethod.POST)
  4. public String createRoom(
  5. @PathVariable String group, @PathVariable Integer year,
  6. @PathVariable Integer month, @PathVariable String slug) {
  7. // ...
  8. return "redirect:/groups/{group}/events/{year}/{month}/{slug}" ;
  9. }
@RequestMapping(
    value="/groups/{group}/events/{year}/{month}/{slug}/rooms",
    method=RequestMethod.POST)
public String createRoom(
    @PathVariable String group, @PathVariable Integer year,
    @PathVariable Integer month, @PathVariable String slug) {
    // ...
    return "redirect:/groups/{group}/events/{year}/{month}/{slug}";
}


3:url template中可支持databinding 了
例如

Java代码 复制代码 收藏代码
  1. @RequestMapping ( "/people/{firstName}/{lastName}/SSN" )
  2. public String find(Person person,
  3. @PathVariable String firstName,
  4. @PathVariable String lastName) {
  5. person.setFirstName(firstName);
  6. person.setLastName(lastName);
  7. // ...
  8. }
@RequestMapping("/people/{firstName}/{lastName}/SSN")
public String find(Person person,
                   @PathVariable String firstName,
                   @PathVariable String lastName) {
    person.setFirstName(firstName);
    person.setLastName(lastName);
    // ...
}



现在可以写成

Java代码 复制代码 收藏代码
  1. @RequestMapping ( "/people/{firstName}/{lastName}/SSN" )
  2. public String search(Person person) {
  3. // person.getFirstName() and person.getLastName() are populated
  4. // ...
  5. }
@RequestMapping("/people/{firstName}/{lastName}/SSN")
public String search(Person person) {
    // person.getFirstName() and person.getLastName() are populated
    // ...
}



4: Validation For @RequestBody

@RequestBody现在直接支持@valid标注了,如果validation失败,将抛出
RequestBodyNotValidException。
具体处理逻辑可见 spring 中的RequestResponseBodyMethodProcessor中的以下代码。

Java代码 复制代码 收藏代码
  1. public Object resolveArgument(MethodParameter parameter,
  2. ModelAndViewContainer mavContainer,
  3. NativeWebRequest webRequest,
  4. WebDataBinderFactory binderFactory) throws Exception {
  5. Object arg = readWithMessageConverters(webRequest, parameter, parameter.getParameterType());
  6. if (shouldValidate(parameter, arg)) {
  7. String argName = Conventions.getVariableNameForParameter(parameter);
  8. WebDataBinder binder = binderFactory.createBinder(webRequest, arg, argName);
  9. binder.validate();
  10. Errors errors = binder.getBindingResult();
  11. if (errors.hasErrors()) {
  12. throw new RequestBodyNotValidException(errors);
  13. }
  14. }
  15. return arg;
  16. }
	
public Object resolveArgument(MethodParameter parameter,
								  ModelAndViewContainer mavContainer,
								  NativeWebRequest webRequest,
								  WebDataBinderFactory binderFactory) throws Exception {
		Object arg = readWithMessageConverters(webRequest, parameter, parameter.getParameterType());
		if (shouldValidate(parameter, arg)) {
			String argName = Conventions.getVariableNameForParameter(parameter);
			WebDataBinder binder = binderFactory.createBinder(webRequest, arg, argName);
			binder.validate();
			Errors errors = binder.getBindingResult();
			if (errors.hasErrors()) {
				throw new RequestBodyNotValidException(errors);
			}
		}
		return arg;
	}



5:annotation-driven缺省注册类的改变

Spring 3.0.x中使用了annotation-driven后,缺省使用DefaultAnnotationHandlerMapping 来注册handler method和request的mapping关系。

AnnotationMethodHandlerAdapter来在实际调用handlermethod前对其参数进行处理。

并在dispatcherServlet中,当用户未注册自定义的ExceptionResolver时,注册AnnotationMethodHandlerExceptionResolver来对使用@ExceptionHandler标注的异常处理函数进行解析处理(这也导致当用户注册了自定义的exeptionResolver时将可能导致无法处理@ExceptionHandler)。

在spring mvc 3.1中,对应变更为
DefaultAnnotationHandlerMapping -> RequestMappingHandlerMapping
AnnotationMethodHandlerAdapter -> RequestMappingHandlerAdapter
AnnotationMethodHandlerExceptionResolver -> ExceptionHandlerExceptionResolver

以上都在使用了annotation-driven后自动注册。
而且对应分别提供了AbstractHandlerMethodMapping , AbstractHandlerMethodAdapter和 AbstractHandlerMethodExceptionResolver以便于让用户更方便的实现自定义的实现类。

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值