利用Spring2.0对VMM系统进行系统架构改造的实践

9 篇文章 0 订阅
8 篇文章 0 订阅

利用Spring2.0VMM系统进行系统架构改造的实践

 

摘要

本文源于VMM系统架构改造的实践经验。文章首先介绍了VMM原有系统架构以及分析了其存在的问题,以此引出对原有系统架构的改造思路。之后引入Spring,并介绍了SpringIoCAOP等基本概念。最后,文章详述了VMM系统是如何利用Spring来进行系统改造的,以及采用新系统架构之后的优点和优雅之处。在文章的最后,笔者还对系统改造需要注意的问题有一些经验之谈。

 

关键词 Key Words

VMM  NGN网络QoS监控系统 VoIP Monitor Manager   

Spring 一个轻量级的容器,实现IoC框架,并提供AOP的实现方式

IoC 控制反转 Inversion of Control

AOP面向切面编程 Aspect-oriented Programming

DI 依赖注入 Dependency Injection

CRUD 创建读取更新删除 Create Read Update Delete

SRP单一职责原则Single Responsibility Principle

DRY不重复原则Don’t Repeat Yourself

OCP 开闭原则  Open-Closed Principle

 

1.引言

当程序员们差不多习惯了夜以继日的书写那些重复的无奈的代码的时候,心中却从来没有放弃拥有一个快速、简单、高效、轻量级的系统架构美好梦想。Spring来了,犹如一缕春风,让人惬意。本文以VMM的系统改造实践经验为背景,介绍了Spring的一些实用技术。

 

2.VMM原有系统架构

2.1 VMM简介

VMMVoIP Monitor Manager)是mSwitch网络中用于监视VoIP媒体流的QoS信息的工具,通过VMM用户可以查看NGN网络QoS参数,帮助运营商诊断呼叫质量,以此掌握整个网络VoIP媒体流传输的质量。

2.2系统架构

VMM原有系统架构在Persistence Layer采用了HibernateOR Mapping组件。在之上的业务逻辑层,采用了Business DAO的方式来实现,亦即对每一个业务应用实现一个DAO,上层应用通过调用DAO来实现对底层数据的控制。如图2.1所示:

2.1 VMM原有系统架构

2.3系统架构的问题

2.3.1 问题一:DAO层过于臃肿

原有系统架构中DAO层承载了太多的功能。在DAO层,我们不但定义了事物处理、数据库访问,还包括了业务逻辑,提供了客户端的访问接口,DAO层就差不多是我们系统的全部。这对于我们代码的重用性等非常不利,也不利于系统的改善和重构。另外,DAO层顾名思义是应该注重数据访问功能,对其他业务逻辑功能应该弱化,否则会给客户端调用带来疑惑,不符合系统设计的单一职责原则(SRP),也降低了代码的可读性。

2.3.2 问题二:技术细节繁琐

随着项目开发的深入,我们发现, 直接使用Hibernate对程序员要求越来越高。程序员至少必须得非常透彻的明白图2.1所示Hibernate的核心接口以及相互之间关系,才可以很好的使用Hibernate:比如SessionFactory是重量级的,线程安全的,Session是单线程的等等,这些“潜规则”必须贯穿我们使用的整个过程,因为对Configration,SessionFactory以及Session接口的使用不当将给系统带来debug上的诸多麻烦。

2.1 Hibernate核心借口关系

2.3.3 问题三:冗余的代码太多

用过一段时间的Hibernate后,我们注意到,程序员需要时刻关注事务Transactionbegin/commit以及SessionOpen/Close,几乎每一次的调用都少不了这些繁琐的重复的而看起来和业务逻辑又没多大相关性的代码。或许这些笨重的代码就是程序员的生活吧!下面这段代码就是出现在系统中频率最高的模式:

try

{

Session session = HibernateSessionFactory.getInstance().getSession();

Transaction tx=session.beginTransaction();

session.save(aObject);//只有这行代码是我们想要的,

//其他全都是“被迫的无奈的”写的

tx.commit();

}

catch (Exception e)

{

log.error("savefailed", e);

}

finally

{ session.close();

}

 

上面这小段代码暴露了程序员有两个“被迫的和无奈的”地方:

1、  处理getSession ()/ beginTransaction()/commit/session.close()等这些对sessiontransaction的管理

2、  当出错的时候,我们必须处理异常,对每一个错误记一下日志,写一行log.error(“…..”)

另外,这小段代码将面临内存泄漏的风险。

如果程序员在书写这段代码的时候,由于笔误遗漏了TransactionCommit或者SessionClose,那么就会带来系统内存泄漏的风险。程序员直接介入事务管理,我们只能依靠程序员自身的谨慎和测试的周全来保证事务的完备性,不能从代码本身的机制来控制事务处理的异常。这是一种不够Smart的设计。

2.3.4 问题四:扩展性不好

这么多年了,或许程序员已经接受和习惯了笨重而重复的工作。可是系统开发还是得继续往前推进,需求总是在不断增加,正如很多系统一样,VMM系统也需要解决如下这些问题,除了基本的数据对象管理,我们还要加入很多service来解决业务应用需求:

ü        我们需要在系统中定制定时运行的任务;

ü        我们需要提供SOAWebService接口;

ü        我们还要支持多语言版本;

ü        我们还要… …

以上的每一个应用需求,已经有很多解决方案了,我们也都曾经在其他系统中实践过很多次,可是,一想起我们又要做一遍,头就大了:那些繁琐的调试工作啊,那些被迫的和无奈的代码啊,痛苦的回忆!

不良代码通常有两种病征:一是纷乱如麻,纠缠打结,可谓剪不断理还乱;二是叠床架屋,臃肿不堪。治疗此类病症一个有效的方法是抽象与分解:从问题中抽象出一些关注点,再以此为基础进行分解。分解后的子问题主题鲜明并且独立,不会牵一发而动全身。同时具有相同特征的部分可以象代数中的公因子一样提取出来,减少了代码重复。

 

3.改造系统架构的思索

我们是否可以改善一下我们程序员的生活呢?是否我们可以只关心我们想实现的业务逻辑,抛弃其他那些被迫的无奈的代码,甚至,我们是否可以连Catch Exception也不用写了呢?这种“优雅”的编程生活就是一个程序员最美好最纯真的梦想。

VMM系统自行研究了一个MicroKernel平台来整合一些常用服务。这个平台利用JMS集成了多种服务,比如/RMI/SOAP/Quartz/Hibernate/JDBC/FTP/Tomcat等等,可谓一个非常强大的开发平台了!这个平台对我们开发起着非常重要的作用,它让我们开发的时候可以减轻管理平台所集成服务生命周期的工作。可以说,这从一定层面上减轻了程序员的工作量。

但是,随着开发的逐渐深入,我们发现用MicroKernel平台其实并没有最初设计时想象的那么美好而优雅。

首先,MicroKernel所起的其实是一个简单的容器功能,所集成的服务都是程序员自行实现的。对各个服务的封装程度不一,封装方式不一。有时候仅仅是简单的集成了服务,并没有使服务的使用的使用更简单。比如对Hibernate的使用,我们仍然需要写那些重复写那些“被迫的无奈的”代码。其他的Quartz/SOAP/RMI/等也是如此,我们还是得对其服务的技术细节好好研究一番。

其次,MicroKernel是非常有针对性的。目前集成的服务就是我们目前需要的,以后如果有新需求产生,我们又必须写一些代码,来完成新服务的集成工作。同时依然存在集成是否可用以及使用效果是否良好的问题。比如我们以前没有集成多语言处理服务,那么,我们现在则必须把诸如ResourceBundle相关的处理逻辑完成coding,然后再加入平台中,当然,还有调试工作在等着我们。潜心修炼的难度总是比想象中要大。

问题很多,愿望只有两个字:简单。

ü        系统更简单

ü        架构层次更简单

ü        服务调用更简单

ü        代码更少更简单

……

本文前面篇幅里面反复描述的烦扰我们多年的各种问题,自从引入了Spring ,特别是SpringIoC思想和AOP思想后,瞬间就消失得无影无踪了……以前渴望多年的“优雅”的程序员生活,现在终于开始踏上征程了! 程序员终于快要解放了!

 

4.VMM的新系统架构

4.1 Spring简介

 Spring 的核心是个轻量级(Lightweight )的容器(Container ),它是实现IoCInversion of Control)容器、非侵入性(No intrusive )的框架,并提供AOPAspect-oriented programming )概念的实现方式,提供对持久层(Persistence )、事务(Transaction)的支持,提供MVC Web 框架的实现,并对一些常用的企业服务APIApplication Interface 提供一致的模型封装,是一个全方位的应用程序框架(Application framework ),除此之外,对于现存的各种框架(StrutsJSFHibernateQuartzResourceBundleRMI等),Spring 也提供了与它们相整合的方案。Spring最重要的思想就是IoCAOP,下面就简单介绍一下这两个概念。

ü        IoCInversion of Control

Spring 最重要的核心概念是Inversion of Control ,中文常译为“控制反转”,更具体的另一个名词是Dependency Injection ,中文常译为“依赖注入”;使用Spring,您不必自己在程序代码中维护对象的依赖关系,只需在配置文件中加以设定,Spring 核心容器会自动根据配置将依赖注入指定的对象。

ü        AOPAspect-oriented programming

Spring 最被人重视的另一方面是支持AOPAspect-oriented programming )的实现。

4.1 VMM Spring AOP原理及相关概念

4.1所示为Spring AOP原理及相关概念。我们在系统中有一些公共的但是却必须的服务,亦即在本文前面章节被称之为“被迫的无奈的代码”,比如日志记录、事务处理、还有安全管理等。这些服务不属于我们的核心业务逻辑,但是他们确贯穿于我们业务逻辑的整个过程。Spring AOP就将这些服务称之为Cross-Cutting Concerns,并将他们单独抽离业务逻辑,形成一些Aspect,并针对每一个Aspect,对应的设计一种处理机制,即Advice,再指定这些Advice使用的范围和地方,称为Point Cuts。在启动Spring的时候,Spring会根据事先的设置,将所指定的Point Cuts 织入(Weave)进业务逻辑中。Point Cuts和业务逻辑交汇的地方,称之为Joint Point

Spring实现AOP的内在机制是用了代理(Proxy)模式和拦截器(Interceptor)思想,是以方法为单位来进行处理的。即在我们调用业务逻辑时,当Spring拦截到事先我们定义好我们需要处理的方法时,就会调用代理,在业务流程执行的同时去完成那些AspectsAdvice

4.2新系统架构的改造

有了Spring这把利器,我们就可以围绕Spring的优良特性来改造我们的系统了。

  4.2.1 改造一:引入Spring作为管理Bean的容器

利用SpringIoC Container取代我们自行研究的Microkernel来管理我们服务以及所有业务对象。

一方面,把服务交给Spring来管理,轻松简单,并且Spring提供了很好的封装,调用起来也更方便。特别诸如我们常用的HibernateQuartzResource BundleRMIStruts等,Spring已经给我们提供了封装好的接口,使用起来尤其方便。其他Spring没有集成的服务,我们也可以利用IoC的依赖注入思想,自行加入Spring Container中。

另一方面,对业务对象的调用,我们可以免除书写 new XXXObject()这种创建业务对象的代码,Spring在初始化时就会把容器中的对象完成依赖注入,我们可以直接从Spring Container中获取对象,进行我们需要的对象操作即可。

以最常用的datasource对象为例,Spring容器定义Bean的方式如下:

<bean id="dataSource"         class="org.apache.commons.dbcp.BasicDataSource" >

<property name="driverClassName" value="oracle.jdbc.driver.OracleDriver"/>

<property name="url" value="jdbc:oracle:thin:@ipaddr:1521:orcl"/>

<property name="username" value="vmm"/>

<property name="password" value="vmm"/>

</bean>

 

可以看出,上面配制首先定义了beanid以及对应的class。然后就是property属性,定义了初始化这个bean需要的参数,比如datasource这个bean需要的driverClassName,url等。定义好这个dataSource后,我们不用再写代码来管理如何引用它,注册好这个BeanSpring就会做好初始化,并可以完成依赖注入,把这个dataSource注入到Hibernate等需要用到这个dataSource的对象中。

  4.2.3 改造二:引入AOP来管理日志/事务/安全

目前业界流行非常SoC原理和DRY原则。SoC就是Separation of concerns,即关注点分离;DRYDon’t Repeat Yourself,即尽量减少重复代码。

利用Spring AOP思想,把我们的日志管理、事务处理、安全管理等逻辑从具体的业务逻辑中抽离出来。这个改造对我们系统的影响深远。众所周知,日志/事务/安全管理虽然不属于核心业务逻辑,但是对系统的运行起着举足轻重的作用。利用AOP来重构系统,把日志/事务/安全作为Aspect单独抽离出来,一方面使业务逻辑更加纯粹,不受外围处理的影响,另外一方面也让日志/事务/安全管理这些Aspect可以单独抽离出来,可以构造统一的管理的机制。从系统的角度,这是一个高可复用性软件体系结构,核心业务逻辑和这些抽离出来的Aspect各负其责,互不影响。AOP就是SoC原理和DRY原则的应用案例。

以日志管理为例。日志对每一个系统来讲都非常重要,当我们系统出错时,我们第一时间查看的就是日志。所以,为了系统稳定,出错时查找错误方便,程序员往往会花很多精力把系统日志记录详细,几乎是对“每一个类每一个方法”书写诸如以下形式的代码:

public String getMethod(String param1,Long param2){

try

{//business logic

}

catch (AException e){

String errMsg="ERROR:AService->getMethod(),";

            errMsg+="/n param1="+param1;

            errMsg+="/n param2="+param2;

           log.error(errMsg, e);

}

}

 

 

 

 

 

 

 

 

 

 

 


几乎每一个Java程序员对上面这段代码都不会陌生。这是一段典型的对系统异常记录日志的操作,日志记录了信息包括:

ü        出错的类

ü        出错的方法

ü        调用本方法时输入的所有参数

从功能上这基本上能满足我们对系统查错的日志需求,但是,这种手工处理的方式却有两点无奈之处:

ü        我们几乎需要对每一个方法书写类似雷同的代码。虽然框架一样,但每一个方法的需要记录具体的内容(类名/方法名/输入参数),确全然不同。所以还不能单纯拷贝。

ü        如果我们偷懒不写或者漏写异常处理代码,那意味着如果调用这个方法出错后,我们就不能很好跟踪和分析问题。日志覆盖率只能由程序员来保证。

引入AOP来管理日志,就完全不用理会每个具体类,具体方法的异常处理,只需要写一个公共的日志处理Aspect Advisor,那么Spring就可以利用拦截器,在调用每个方法异常的时候,把该方法的类名,方法名,以及出错时调用本方法的输入参数通过调用JoinPoint相应的方法全部记录在日志中。

引入Spring AOP后,日志管理对程序员来说就是一个一劳永逸的工作。我们只需要写一次日志,那么Spring就会按照我们的要求,横向切入到我们需要加入日志的每一个方法!这种方式比起原来那种“勤劳”的对每一个方法书写日志的方式可谓非一般的“优雅”啊!

日志管理抽离出来后,我们可以编写一个公共类(Advice)来处理异常,不放过任何一个异常,这对我们根据日志查找错误非常有帮助。我们还可以构造跟踪程序执行过程的日志类,来监控程序执行。比如在方法执行前(@Before),在方法执行后(@AfterReturning)打印日志,来进行跟踪程序执行情况。下面这个LoggingThrowsAdvice类我们只需要定义一次,就免除了以前需要在每个方法中重复的书写很多次日志的无奈了。

//标志符Aspect

@Aspect

public class LoggingThrowsAdvice implements ThrowsAdvice

{

private Logger logger =

Logger.getLogger(this.getClass().getName());

 

//Pointcut指定本Advisor适用的类及方法,支持表达式描述

@Pointcut("execution(* com.utstar.nms..*.*(..))")

private void logging() {}

 

// AfterThrowing指定Spring的拦截器在方法异常时调用

 @AfterThrowing(pointcut="logging()", throwing="throwable")

 public void afterThrowing(

JoinPoint jointPoint, Throwable throwable)

{

    String errMsg ="/n throwable:"+throwable;

           //记录出错类名

errMsg +="error Class:"+jointPoint.getSignature()

.getDeclaringTypeName();

 

//记录出错方法名

           errMsg +="error Method:"+jointPoint

.getSignature().getName();

         

            //记录出错方法的所有参数

 Object[] args = jointPoint.getArgs();

for (int i = 0; i < args.length; i++)

                {

                  errMsg+="["+i+"]:"+args[i]+",";

                } 

   

       logger.log(Level.ERROR, errMsg,throwable); 

 }

这样,我们就只需要写一次日志,Spring的拦截器就会帮我们在每个需要记录日志的方法出错时自动记录日志。这种来自简单的快乐,只有写过多年的重复的繁琐的被迫的无奈的日志的程序员才能体会!

  4.2.3 改造三:分离DAO层和Service

在系统架构层次上改造,按照单一职责原则(SRP),设计公共DAO层,把DAO层和业务逻辑的Service层分开。

DAO顾名思义,应该着重数据对象访问逻辑,而对数据对象的访问方法是共同的公共的,我们需要把这部分功能抽象出来。即公共DAO层封装好常用的CRUD操作。这样,DAO的代码只需要写一次。不用每个Service都需要写DAO相关的代码。

业务逻辑放入Service层,Service层继承公共DAO层,实现DAO层对数据对象的业务访问。Service层的每个具体的Service仅仅关注的是和自身相关的业务逻辑处理,而又具有公共DAO层的功能。

这样分层可以减轻DAO层,也让Service层更干净。

4.3新系统架构

新架构从下往上按照DBàPOJO(Domain Objects)àCommon DAOàServiceàWeb的架构来构造。可以看到,DAO层实现了一个公共DAO,不拘泥于实现,实现可以为Hibernate,也可以为iBatis或者JDBCService层位于DAO层之上,负责业务逻辑处理,并提供给Web调用接口。

Spring针对上面新架构的每一层,都有对应的解决方案帮助我们更好的管理代码。图2.1VMM系统加入Spring后的新系统架构:

2.1 VMM新系统架构

Database Layer,我们可以定义一个或多个Datasource对象(多数据库或异构数据库),并通过依赖注入的方式提供给Spring集成的Hibernate SessionFactory类:

org.springframework.orm.hibernate3.LocalSessionFactoryBean

DomainPOJOHibernate保持一致就行。我们可以利用Xdoclet工具自动生成。

DAO层,Spring提供了Template模版,封装了Hibernate具体的实现细节。可以利用Spring封装的接口更简单的使用Hibernate。同时,我们可以把所有对象放入Spring Container中统一管理。

Service层,除了利用Spring Container来管理Service Bean外,我们还可以利用AOP织入(Weave)一些非业务逻辑的处理。比如日至管理、事务处理、安全管理等。另外,我们还可以非常简单地使用Spring集成的QuartzRMIResource Bundle等服务框架。

在应用展示层,可以利用Spring封装的Struts来实现MVC

4.4新系统架构的优雅之处

Spring的目标就是让程序员能使用“优雅”的框架,写出“优雅”的代码。不用成天和一些“被迫的无奈的”那些代码打交道。完成系统架构改造之后,我们的切身体会又如何呢?

4.3.1 优雅一:利用Spring IoC容器来管理Bean,轻松,规范。

利用Spring容器来管理Bean我们不用再自己手写一个容器来处理有关Bean的生命周期问题,Spring已经做好这个工作,而且大多时候可能比我们自己做得更优雅,同时我们也不需要在调用每个对象之前用new XXXObject()这样的语句来出始化这个对象,并用程序来管理对象之间的关系。在Spring容器启动之后,所有对象都可以直接取用,不用编写任何以行程序代码来产生对象或者是建立对象与对象之间的依赖关系。

所以,利用Spring设计好对象以及关系后,程序员的工作就是,使用它!

4.3.2 优雅二:Spring API让程序调用更简单

有程序员曰:“简单就是美”。利用Spring集成的Hibernate/Quartz/RMI/ResourceBundle等框架,程序员调用经过Spring抽象封装后的APISpring提供了简化且一致的方式,让程序员在使用这些API或者组件服务时更加简单。我们不用费太多心思去研究每个框架、组件或者API的原理,实现机制,调用方法,注意事项等,这些Spring已经帮我们完成了,Spring提供了一些更直观更简单的接口以供调用。

Hibernate为例,在本文叙述原有系统架构问题三提到,我们调用Hibernate需要时刻关注事务Transactionbegin/commit以及SessionOpen/Close,几乎每一次的调用都少不了这些繁琐的重复的而看起来和业务逻辑又没多大相关性的代码。使用Spring封装后的Hibernate调用方式又是如何呢?

使用Spring后,只要我们继承HibernateDaoSupport,利用HibernateTemplate我们就可以直接操纵数据库,代码行数从直接操作Hibernate的十几行变成现在的一行:

getHibernateTemplate().saveOrUpdate(o);

Spring“背后的故事”很多。所有我们不想写的代码,Spring默默地全部帮我们在背后做了。一个成功的男人背后一定有一个支持他的女人,一个成功的程序员背后一定得有一个支持他的框架。Spring就是这样一种框架,它任劳任怨的完成了所有繁琐的事情,让程序员集中精力关注业务逻辑处理,这种伟大,只有使用它的程序员才明瞭,用赵本山先生的话说就是:“谁用谁知道”。

4.3.3 优雅三:AOP让代码更优雅

使用AOP实现日志、事务等统一管理后,我们可以创建公共切面的处理类,来统一管理这些公共逻辑。同时,这也会大大减少了代码量。

比如日志,引入Spring AOP后,日志管理对程序员来说就是一个一劳永逸的工作。我们只需要写一次日志,那么Spring就会按照我们的要求,横向切入到我们需要加入日志的每一个方法!这种方式比起原来那种勤劳的对每一个方法书写日志的方式可谓非一般的“优雅”啊!

5.系统改造需要注意的问题

5.1改造必须保证系统稳定性

OCP原则,即“开闭原则”是系统设计的基础原则。OCP原则建议程序设计要对扩展开放,对修改关闭。系统改造应该注意向下兼容,不能影响原有功能的运行。新系统架构可以继承原有系统架构,但是不能修改原有系统架构。

系统稳定永远是最重要的。系统改造也不可能一蹴而就,是一个持续的过程。原有系统即使不够聪明,性能可能也不够好,开发方式也很笨拙但是,原有系统经过了长期的测试,用户的使用,被证明是稳定的。对被证明是稳定的系统的改造需要特别谨慎,因为改动就意味着会有产生新问题的可能。有产生问题的可能就需要充分的测试。

所以,系统改造需要特别注意不能影响原有系统的稳定性。保守的做法就是保持原有系统不变,在原有系统基础上加入新系统架构,新功能利用新架构来完成。

在这个基础上,如果新系统架构被证明是稳定的,我们再把原有架构上的功能循序渐进的移植到新架构中来。

5.2新系统架构需要与原有系统架构兼容

改造原有系统架构时,除非是完全替换原有系统架构,否则就存在两个系统架构并存的局面,那么我们就需要注意新系统需要与原有系统兼容。不能因为上了新系统架构,而影响了原有系统架构的功能。设计新程序的接口也应该特别注意,新旧功能需要实现松散耦合。当系统存在两个系统架构的时候,两个系统架构应该尽量减少耦合。充分的解耦才能保证新旧架构互相不受影响。

5.3系统改造不能影响系统性能

虽然Spring给我们带来系统结构和开发上的帮助很大,但客观来讲,SpringMicrokernel Container的确会在一定程度上消耗额外的系统性能。如果是开发并发性能、实时性能等要求非常高的关键业务系统,诸如SpringHibernate等框架都要慎用。但对于一般系统而言,Spring给我们带来的优雅的编程生活,完全可以抵消这部分损失。

 

6.结束语

采用新架构,学习新技术是需要学习成本的。在企业应用中,系统基本上都是团队合作的成果,所以技术简单、容易、学习成本低是很重要的,眼下轻量级的概念如此受欢迎也源于此。

Spring的功能强大,可谓保罗万象,本文介绍的是SpringVMM系统中的一些常用应用场景。只要有IoC平台,再加上AOP这把利器,Spring就会让Coding变得有趣起来。相信“简单就是美”的程序员,已经随着Spring的步伐轻舞飞扬起来,从此过上简单而优雅的生活…...


参考书目:

Spring 2.0技术手册》林信良著,

Spring in Action》沃尔斯,布雷登巴赫 著,李磊,程立,周悦虹 译

《整合STRUTS+HIBERNATE+SPRING应用开发详解》 李刚

精通JavaEE项目案例——基于Eclipse Spring Struts Hibernate刘乃丽

Spring 2.0 核心技术与最佳实践廖雪峰

Spring框架高级编程》: 约翰逊 等著,蒋培

Java与模式》阎宏著

Head First Design PatternElisabeth Freeman,Eric Freeman等著

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值