EJB 3.0简介

EJB 3.0简介

SUN中国软件技术中心 王强 wynne.wang@sun.com

1 简化开发的目标

1.1我们的目标

EJB3.0是当前很多人谈论的话题,企业软件开发的一个关键是,提供一个尽量简单的的应用框架:它可以使开发人员不用关注于复杂的问题,比如事务处理、安全和持久化等。可以集中精力关注于商业逻辑,而不用关心那些低层的技术细节,从而提高开发者的效率,得到高质量的软件。这也是制定EJB 3.0规范的目标,简化开发!


1.2 当前的问题


EJB3.0希望开发人员能够从这种新的开发模式中受益,更好地推进J2EE的应用. 随着JAVA不断的进步和发展,越来越多的企业选择J2EE作为它們的解決方案,J2EE体系结构提供一个简化的中间层集成框架来满足应用的需求。然而,对于一般的开发人员,目前J2EE 1.4下的EJB 2.1 框架有些过于复杂了。 按照EJB2.1规范的定义,EJB组件必须事先很多的接口, 比如Home接口、Remote接口、local 接口,等等.还要针对各种应用类型定义许多的xml描述文件。当我们需要访问某个组件或者服务的时候,必须通过JDNI查找,通过名字绑定服务,才能找到我们需要的对象。

比如我们要使用某个EJB shopping cart, 就要首先实现一个initial context,然后通过他查找这个EJB 的home 接口,然后调用home街口的create方法,得到一个EJB object, 最后调用这个EJB object的商务方法. 下面是一个例子:
// EJB 2.1 Client view of the ShoppingCart Bean
...
Context initialContext = new InitialContext();
ShoppingCartHome myCartHome = (ShoppingCartHome)
initialContext.lookup(“JAVA:comp/env/ejb/cart”);
ShoppingCart myCart= myCartHome.create();
//Use the Bean
Collection widgets = myCart.startToShop(Widgets )
...
// Don't forget code to handle JAVAx.ejb.CreateException
...
EJB2.1的规范还要求我们必须实现Javax.EJB里面定义的接口, 实现里面的Methods比如 EJBCreate(), EJBPassivate(), and EJBActivate()。大多数情况下这些方法是不需要开发人员作任何修改的。这些规定实际上都和真正核心的商务逻辑没什么关系,都只是一些技术模板,规定了开发人员必须按照这样的模板进行开发. 比如下面这段代码:

人们开始思考,怎么样才能把EJB的开发变的更加简单,更好的利用这种技术. 当前正在制定的EJB 3.0的标准的目标就是简化开发, 让更多的开发人员被它的易用和强大功能所吸引过来,喜爱这项技术。为了达到这个目标,要做的第一件事情,也是最重要的事情,就是从一个开发人员的角度,将EJB的使用尽量的简化。

1.3 JCP专家组的工作


这个工作是由EJB3.0 专家组来完成的。我们知道JAVA的开发和推动是由一个开放的组织来完成的,这个组织的名字叫做JAVA Community Process。简称JCP. SUN的理念是: 创新无处不在. 所以JCP小组从世界的每个角落听取关于JAVA的建议,将各方面对JAVA的要求通过制定JSR(JAVA Specification Request )形式确定下来. EJB 3.0规范的JSR编号是 220. 整个专家组的制定成员包括J2EE 注册用户, 应用服务器的开发厂商和J2EE社区的成员.


简化一个现有的技术,尤其是得到广泛的开发人支持的技术,比如EJB, 不是一个简单的工作. 作为铺垫,专家组进行了大量的准备工作,检验了EJB技术的复杂性,当前EJB流域流行的各种模式和反模式,以及从客户和开发人员来的各种需求。开发人员和用户根据实际的需要,希望EJB能够提供他们满意的特性.
检验结果发现,大多数情况下,人们不需要更高级的技术,而是需要更简化的技术,来简化当前的开发模式。而不是像以前的EJB发布一样,在技术复杂度上的提高。定义一个新技术,不仅对老的技术有一定的更新, 也要能充分并容老的技术, 提供一定的向后兼容性. 因为采用这些技术的企业,已经在这些技术上投资了很多. 如果因为技术的更新就使得对应的IT系统和数据变得不能使用,是一件非常糟糕的事情.


所以,定义了EJB3.0规范的同时,如何支持现有的EJB技术是非常必要的. 要保证现有的EJB API是持续可用的, 还要和新的EJB3.0 API结合起来, 对早期的API应该继续提供支持而不是标记为不赞成使用

1.4 标注的新功能


EJB3.0的很多新特性是通过JAVA SE5.0来实现的。这里我们就要谈到JAVA SE 5.0,它所提供的许多特性,其中最有趣的一点就是标注(Annotation)的功能。我们知道以前的JAVA语言都是命令格式的, 比如a.b(), 表示让类a做事情b, 但是很多时候我们只是需要对某个对象做一些注解,比如对某个类标记为可持续化的Serializable. 这只是一个标记,为了以后的处理提供说明,本身不需要做任何操作。
在Deploy 的时候, 提供了很多说明的XML文件,比如部署描述文件,里面说明了引用的EJB的名字,接口, 以及当前EJB的Transaction Type等等信息. 所有这些信息都是说明性的,而不是命令性的. 用来对某个对象的某个属性坐一段说明. 因此,有一个非常有趣的想法,能不能通过对JAVA语言的扩展,结合标注和命令这两者的优点 ?


这也是有一个专家组在JCP的组织内完成的, JSR规范的编号是JSR 175, 为JAVA SE 5.0支持注解(Annotation)的功能. 这个规范为EJB3.0 的简化实现提供了一些基本的支持, 也是最关键的支持. 标注可以有自己的属性,也可以定义自己的持续时间,表示这段信息是否保存到 源代码中,还是一直持续到Class中,或者一直保持到运行时间. 标注有自己的缺省值.大多数情况下,无须说明我们就可以推算出来这个对象的行为。 
2 轻松的实现开发

2.1 减轻开发人员的负担

EJB3.0的简化工作包括下面几个部分:
 提供一个简化的API, 包括对EJB的定义,对EJB的引用等等
 减少开发的类数目,不再需要那么多的interface
 相关性注入
 简化的查询机制
 从开发人员的角度不必要使用部署描述文件, 很多的工作可以放到代码里面用标注来说明,比如Entity Bean 的Transaction Type
 简化的持久化功能
 简化和改善数据对象的O/R Mapping.


标注可以应用到编程语言的一些基本元素上,比如类,方法,变量,包等等. 当我们在代码中使用了这些标注, 根据这个标注对应的持续策略, 它可以被编译到Class 文件中去,或者一直保持到运行的时候。大多数在 EJB 3.0 中定义的标注都是Runtime保持策略, 这样做的好处是提供了最大的灵活性。而且由于大量工作放到了运行的时候来做,也减少一部分Deploy的工作。


我们通过定义缺省的语法来说明大多数常见的情况。开发人员不需要再专门说明常见的情况,“OK, 没问题,缺省的设置就已经可以满足需要了“ 这样,开发人员的工作大大减轻了。
这也引出来了EJB3.0中的一个很有意思的概念 "Configuration By Exception" --只有在例外的情况下才需要我们的参与.


EJB3.0的目标是简化开发人员的工作,让他们专注于商务应用的开发而不是把精力放到很多繁琐的例行工作上,这些工作可以交给Container来完成。EJB通过注入来指定自己需要的资源,不用再写那些麻烦的方法. 将对象的创建和获取提取到外部。由外部容器提供需要的组件。这样,开发人员只用在开始的时候定义,说我需要这个资源, 后面就可以直接使用这个资源, 这样会大大的简化开发, 因为开发人员只用关心如何使用这个对象和商务方法, 而不用担心其他的技术细节。

2.2 抛开繁琐的细节
下面我们看看都作了那些简化。我们的目的是把那些繁琐的技术细节隐藏起来,程序开发人员只用关心自己的商务逻辑代码,而不用关心那些复杂的技术模板,必须实现的接口等,哪怕这些方法和接口根本不需要实现.

 不再需要EJB的部件接口
 每个EJB 都只是一个普通的JAVA Class
 不再需要home接口, 我们不再用home 来创建这个EJB
 不再需要实现javax.ejb.EnterpriseBean借口
 对于需要在回调方法里实现的部分,我们采用标注的方式说明一个方法为回调方法
 不再需要使用复杂的JNDI名字调用机制,对于需要服务或者资源的地方
 我们采用了相关性注入的方法,另外也可以通过简化的lookup方法来查找资源

下面让我们看一个简单的无状态SessionBean的例子。无状态session Bean是最简单也是最常用 Bean,很多初学EJB的人都从无状态Session Bean开始。如何让无状态Session Bean 变的简单易用成为一个非常有意义的话题。

前面假设我们已经定义了相关的interface, 这个EJB2.1的的功能是对员工的工资做处理,打开一个数据库连接,进行员工工资信息的某些操作,等等.

// EJB 2.1
public Class PayrollBean implements JAVAx.ejb.SessionBean
{
SessionContext ctx;
DataSource empDB;
public void setSessionContext(SessionContext ctx) {
this.ctx = ctx;
}
public void ejbCreate() {
Context initialContext = new InitialContext();
empDB = (DataSource)initialContext.lookup( JAVA:comp/env/jdbc/empDB );
}
public void ejbActivate() {}
public void ejbPassivate() {}
public void ejbRemove() {}
public void setBenefitsDeduction (int empId, double deduction) {
...
Connection conn = empDB.getConnection();
...
}
...
}
// NOTE deployment descriptor needed


这里我们首先要实现一个 sessionBean interface,保持一个对sessioncontext的引用,然后是在EJBcreate 方法里面我们调用JNDI得到一个datasource。 后面我们必须要定义一些回调方法,虽然这些方法我们不会实现任何逻辑。然后在 商务方法里面,我们打开一个数据库连接。

注意,我们还没有完。为了使用这个EJB, 必须加上xml的部署描述文件打好包。

下面是一个常见的部署描述文件可以是这样子的。

<session>
<ejb-name>PayrollBean</ejb-name>
<local-home>PayrollHome</local-home>
<local>Payroll</local>
<ejb-class>com.example.PayrollBean</ejb-class>
<session-type>Stateless</session-type>
<transaction-type>Container</transaction-type>
<resource-ref>
<res-ref-name>jdbc/empDB</res-ref-name>
<res-ref-type>javax.sql.DataSource</res-ref-type>
<res-auth>Container</res-auth>
</resource-ref>
</session>
...
<assembly-descriptor>
...
</assembly-descriptor>

从开发人员的角度来检查这样一个简单的EJB 是非常有意义的。我们可以更好的理解有那些地方可以有改动。一个主要的问题是,定义了这么多的方法和结构以后,程序的清晰化受到了影响,结构变得混乱。真正需要关注的商务方法没有很好的强调和体现。

下面让我们看一看EJB 3.0 是如何实现一个简单的stateless session Bean的。

// Same example, EJB 3.0
@Stateless public class PayrollBean implements Payroll {
@Resource DataSource empDB;
public void setBenefitsDeduction (int empId, double deduction) {
...
Connection conn = empDB.getConnection();
...
}
...
}


@Stateless 标注表示这是一个stateless session Bean. 使用这样的标注可以让我们不再需要使用SessionBean 接口, 这样就大大的简化了EJB的实现类。 Bean的商务接口是payroll. 这是一个普通的JAVA interface. 缺省的情况,container会把他作成一个local interface. 如果需要实现一个远程接口,只需再定义一个标注 @Remote. 注意在接口里面不需要定义 RemoteExceptions,它由Container 层在后面处理掉了

2.3 引用对象的新方法
在老的EJB规范中,还有一个比较复杂的地方是访问环境对象。EJB 2.1 里面访问环境要首先在组件定义的相关性引用,比如resource-refs, EJB-refs, 然后在JNDI名字空间里面配置这些环境对象。 最后查找运行的时候JNDI空间里面查找这些环境对象
EJB 3.0对此提供了两种简化的方案:

1) 相关性注入
2) 一个简化的查询lookup方法

相关性注入是一种技术,开发人员在原代码中加入标注的一个定义,说明需要这个环境对象,然后由Container在初始化的时候把真的环境对象注入里面。
目前EJB3.0 spec里面有两种注入方式,setter注入和变量注入, 这些在以后的规范中可能会有变化,比如一种统一的注入方式。 我们可以通过注入标记@EJB来定义一个EJB的引用,也可以通过注入标记@resource 表示我们要引用一个资源,它可以是EJB以外的一切环境对象. 专家组为了尽可能的简化开发,将使用注入的对象类型作了简化。

EJB3.0 将仍然提供一个动态查询的方法,但是从程序开发人员的角度,不需要再使用JNDI API,而是采用更为简化的EJB Context 的 Lookup方法。

在新的EJB 3.0规范中,因为不再需要复杂的home接口和EJB 接口。EJB client端的编码也大大的简化了,和访问一个普通的JAVA 对象没有什么区别。上面的那个EJB2.1的例子比较起来,在EJB 3.0 里面使用一个EJB变的非常简单,只需要两行代码:

// EJB 3.0 client view
@EJB ShoppingCart myCart;
...
Collection widgets = myCart.startToShop(Widgets );
...

首先定义一个EJB的应用,然后就可以直接调用这个EJB的商务方法,剩下的工作由Container来为我们完成。

2.4 新的事务管理

EJB中的事务(Transaction)管理大大简化了用户开发程序。把应用分成一个一个小的单元,叫做transaction. 事务系统保证了这个单元里面的任务是完整的,要么全部执行,要么出错后完全退回到初始状态。


J2EE中有两种类型的事务, 容器管理的和Bean管理的。他们在如何启动和结束事务上是不同的。Bean 管理的事务由组件使用 UserTransaction类显式启动和结束的。代码中需要调用方法 UserTransaction.begin() 和 UserTransaction.commit() 。Container 管理的事物是由container 自动来完成的。
针对不同的事务类型,可以定义6种不同的事务属性。
事务属性告诉 Container 是否把EJB方法里面的工作放到用户的事务里面。还是针对这个方法重新启动一个新的事务. 或者执行这个方法而不包含在事务里面,等等。
在EJB3.0规范中,我们缺省的定义是容器管理的事务. 而且针对所有的Bean方法,应用Required Transaction属性,它的意思是如果调用这个方法的应用没有Transaction Environment,那么这个方法会自动创建一个新的。
开发人员可以使用标注的方式在针对整个EJB或者某个具体的方法指定他的Transaction Attribute.
首先我们看一个工资处理的EJB的例子, 这是一个标准的EJB 3.0 Stateless Session Bean。缺省情况下,每个方法是都是由container来管理Transaction的,缺省的事务属性是required
// Uses container-managed transction, REQUIRED attribute
@Stateless public PayrollBean implements Payroll {
public void setBenefitsDeduction(int empId, double deduction) {...}
public double getBenefitsDeduction(int empId) {...}
public double getSalary(int empId) {...}
public void setSalary(int empId, double salary) {...}
}

下面还是这个例子,我们知道对于有关重要数据的改动,总是非常敏感的。比如我们在更改某个用户的工资的时候同时修改他的所得税,我们希望这两个调用是在同一个Transaction里面发生的。

@Stateless public PayrollBean implements Payroll {
@TransactionAttribute(MANDATORY)
public void setBenefitsDeduction(int empId, double deduction) {...}
public double getBenefitsDeduction(int empId) {...}
public double getSalary(int empid) {...}
@TransactionAttribute(MANDATORY)
public void setSalary(int empId, double salary) {...}
}

那么调用用户的工资改动的方法就必须在一个已经存在的Transaction Environment
中,为此,我们用@transactionattribute(mandatory)标注 这个方法必须在一个客户端的transaction中,如果客户端没有这样的一个 Transaction Context, Container会扔出来一个 Javax.ejb.EjbTransactionRequiredException 的错误信息。

 

2.5 新的安全机制
EJB架构不鼓励开发人员用代码的方式实现安全机制,而是采用安全角色的方法,通过定义可以访问的安全角色,来限制对某个方法的访问权限。

缺省情况,在3.0里面所有的方法都是"unchecked"。也就是说缺省情况下对所有方法是不用安全控制策略的。如果调用某个方法的客户端具有某个用户角色(Role) ,我们可以制定是否这个方法也是沿用这个角色。缺省情况下的安全策略是"Caller Identity",也就是说被调用者的角色和调用者的角色应该是一致的

这是一个EJB 3.0的安全实现的例子。我们对于其他的方法都没有设定安全策略。但是对于设定某个员工的工资是多少,这样的安全要求比较高的方法设定了只有人事部门的管理员才能调用。我们采用了一个

@RolesAllowed("HR_PayrollAdministrator ")
// Security view
@Stateless public PayrollBean implements Payroll {
public void setBenefitsDeduction(int empId, double deduction) {...}
public double getBenefitsDeduction(int empId) {...}
public double getSalary(int empid) {...}
// salary setting is intended to be more restricted
@RolesAllowed( HR_PayrollAdministrator )
public void setSalary(int empId, double salary) {...}
}

2.6 事件的通知和检查

EJB 3.0中, 开发人员不需要实现那些不必要的callback methods。 他可以把任意方法指定为一个事件通知方法。通过标记一个通知标注,我们把一个方法标记为一个回调方法, 例如:

@Stateful public class AccountManagementBean
implements AccountManagement {
Socket cs;
@PostConstruct
@PostActivate
public void initRemoteConnectionToAccountSystem {
...
}

@PreDestroy
@PrePassivate
public void closeRemoteConnectionToAccountSystem {
...
}
...
}

在这个EJB 3.0 的例子当中, 我们定义了一个有状态的session Bean,把初始化的工作都交给init remote connection方法,同时标记他为一个回调方法,在construct ,和 activite之后调用
同时我们把清理的工作都交给close remote connection方法,同时标记他为一个回调方法,在destroy 和passivate之前调用

对于那些高级的用户,需要定制自己的事件检查和侦听机制。检查方法和所被检查的对象方法可以在同一个 Bean 中,也可以在不同的JAVA Class里面。 这种检查机制有下面的特点:
 在方法周围进行检查
 包装商务方法的整个调用过程
 可以对方法调用的参数和结果进行处理
 检查类的序列中,可以拿到上下文数据
 多个检查类可以按照指定的顺序执行
 可以用部署表述符来指定执行顺序

我们用 @Interceptors 标注指定一个外部的检查方法类, 用 @AroundInvoke制定内部的某个方法为检查方法。在检查方法里面,我们用proceed()来调用具体的商务方法。目前的检查方法是检查一个Bean里面的所有方法,专家组正在制定标准, 让它可以具体到检查制定的某个方法。

下面是一个EJB3.0的检查方法的例子:

我们制定了这个无状态session Bean检查方法类是这三个类.
accountaudit, metrics, customsecurity。那么实际执行的时候会按照这个制定的顺序来实行
检查.

@Interceptors({
com.acme.AccountAudit.class,
com.acme.Metrics.class,
com.acme.CustomSecurity.class
})
@Stateless
public class AccountManagementBean
implements AccountManagement {
public void createAccount(int accountId, Details details) {...}
public void deleteAccount(int accountId) {...}
public void activateAccount(int accountId) {...}
public void deactivateAccount(int accountId) {...}
...
}

2.7 部署描述文件的优先级

在EJB3.0中,我们可以用标注的方法来指定对环境对象的引用,也可以用部署描述符文件(Deployment Description)的方式来制定对环境对象的应用, 也可以两个同时使用. 如果我们在部署描述文件和代码标注中都制定了环境对象,那么部署描述文件中的那个引用有更高的优先级, 这样就给了应用的Deployer相对比较大的灵活性来控制.

3 持久化的魔力


3.1 最初的目标

EJB 3.0 专家组的另外一个目标是为实体Bean( Entity Bean ) 和对象/关系的映射,提供一个轻量级的模型。实际上,目前关于实体Bean的争论很多, 很多批评人士对它的架构感到不满,我们的目标是改善EJB 容器管理的持久化模型,从各地的一些优秀的开源软件中吸取灵感,从一些反模式(Anti-Pattern)中吸取经验, 从而让新的标准称为技术上的领跑者。 EJB3.0的持续化包括下面的一些特性:

 简化实体bean的编程方式,减少不必要的开发接口
 改善EJB的持久化, 为O/R映射提供继承和多态的信息
 可以在EJB Container之外使用实体Bean
 不需要Container 就可以对商务方法进行测试
 不再需要数据传输对象DTO (Data Transfer Objects) 之类的设计模式(Design Pattern)
 改善的EJB QL

为了解决这些意见,ejb 3.0 的专家组集中在一个经过简化的持久化模型,目前业界已经有类似的产品和模式,比如Hibernate和Toplink, 这是一个全新的方向,代表着轻量级的对象/关系(O/R)映射模型。


在EJB3.0中,实体Bean是普通的Java 类, 这是一些真正的类, 而不是抽象类。而且更为简化的是,开发人员不再需要实现任何接口,不论是商务借口或者是回调接口。
而且不再需要实现数据传输对象(DTO), 因为现在的Entity Bean本身就是一个简单的普通JAVA 对象POJO (Plain Old Java Object), 标记为序列化之后就可以在客户端和服务端进行传递,不再需要特殊的处理。

3.2 管理类的角色
在新的EJB3.0规范里面,我们看到了一个 EntityManager 类, 对于实体Bean而言,这是一个类似于调用工厂(Factory)或者统一的Home接口之类的角色。Entity manager自己的生命周期可以由Container或者应用程序都可以来管理。
Entity Manager 将负责跟踪数据库事务上下文中, 实体bean 对象的状态。对于开发人员来说, javax.persistence.EntityManager 成为对实体bean的统一访问点。 可以把它看作是对实体Bean操作的一个”home”. 我们要通过entity manager 调用实体bean的生命周期管理。 比如:Persist, Remove, Merge, Flush, Refresh, 等方法。

Entity Manager是所有Entity Bean的持久化管理接口,任何对Entity Bean的操作都必须通过它来进行。有的开发人员会对这个接口感到熟悉,因为它与Hibernate的Session接口和JDO的Persistence Manager非常相似。Entity Manager的接口主要方法如下:

package javax.ejb;

public interface EntityManager {
public void create(Object entity);
public < T > T merge(T entity);
public void remove(Object entity);
public Object find(String entityName, Object primaryKey);
public < T > T find(Class < T > entityClass, Object primaryKey);
public void flush();
public Query createQuery(String ejbqlString);
public Query createNamedQuery(String name);
public Query createNativeQuery(String sqlString);
public void refresh(Object entity);
public void evict(Object entity);
public boolean contains(Object entity);
}

EntityManager 还大大方便了查找方法的实现,记得我们在EJB2.1里面是怎么做的吗?在EJB 3.0里面,可以直接调用EntityManager.find(String entityName, Object primaryKey),查找具有某个主键的实体 bean 实例。例如:

public OrderBean findByPrimaryKey(String orderId)
{ return (OrderBean)em.find("OrderBean" , orderId);
}

EntityManager作为Query对象的生产工厂, 可以用createQuery(String ejbQlString) 创建一个EJB QL 查询,也可以用createNamedQuery(String queryName)来创建一个 NamedQuery 查询。下面是一个例子:


public List findWithAddr(String addr) {
return em.createQuery(
"SELECT o FROM Orders o WHERE o.addr LIKE :orderAddress")
.setParameter("orderAddress", addr)
.setMaxResults(100)
.listResults();
}

3.3 O/R Mapping的标注


O/R Mappings 标注的元数据,使得用户可以修饰他们的EJB3.0 Entity Bean. 在 EJB 3.0缺省环境下,表的名字就是类的名字, 两者是一致的, public Java Bean getter方法假定为访问表中同样名字的属性值, 开发人员可以通过@Table, @column等各种不同的标注来修改这个缺省的定义. 例子:


@Entity
@Table(name="CUSTOMER")
@SecondaryTable(name="CUST_DETAIL",
pkJoin=@PrimaryKeyJoinColumn(name="CUST_ID"))
public class Customer { ... }


@Entity 说明这是一个Entity Bean, Table标记了对应的O/R Mapping的主表, 而 SecondaryTable标记了 Entity对应的辅助表,
持续化的API, 和查询语言,以及O/R Mapping标注, 都是EJB 3.0规范的一部分。
持久化API的设计目标是能够独立于Container的运行,只需要有一个Java SE环境就可以运行了。

4 小结


本文以描述了EJB 3.0规范的一些新特性。EJB 3.0将是EJB历史上最大的一次改动,它充分吸收了一些开源项目,比如Spring、Hibernate的经验,变得更加方便实用,体现了简化开发的设计目标。这篇文章希望能够给大家带来一点关于EJB 3.0的印象,目前EJB 3.0规范已经进入了Proposed Final Draft阶段,当然,将来这个规范的技术细节还可能发生变化。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值