[收藏]对AspectJ作者的采访

<script type="text/javascript"> </script> <script type="text/javascript" src="http://pagead2.googlesyndication.com/pagead/show_ads.js"> </script>
<script type="text/javascript"> </script><script type="text/javascript" src="http://pagead2.googlesyndication.com/pagead/show_ads.js"> </script>
AspectJ 作者采访

来自:http://www.theserverside.com/talks/videos/GregorKiczalesText/interview.tss 译者:taowen

Gregor Kiczales领导了开发AOP和AspectJ的PARC团队。他是知名的AOP传教士,并且致力于建立同时给实践者和研究者服务的社区。他在开发高级编程技术并把这些技术传授给开发者方面有二十年的经验,他是ANSI CLOS设计团队的成员,并且是CLOS参考实现的作者,以及《The Art of Metaobject Protocol》(MIT出版社, 1991)的作者之一。他现在在NSERC,Xerox,British Columbia大学软件设计方面的Sierra System教授,在那儿他带领他的学生在AOP方面进行各种研究,包括操作系统,重构,模式和新的语言构造的应用等等方面。

Dion:告诉我们一些关于AOP的历史方面的事情吧?

Gregor:AOP从几个不同组织的研究中发展而来。这些研究包括反射(Reflect),面向对象编程中的各种张力等等。早期的研究——这儿我指的是90年代初——受到了十多年来用OOP构建系统的经验的推动,而且确实开始看到了一些OOP的限制,在大系统中,在分布式系统中,以及在灵活性很重要的系统中。

在Xerox PARC,我很幸运的领导的团队,起初致力于反射和metaobject protocols方面的研究,这些研究现在认为在OO程序中,对提供某种类型的灵活性非常关键。然后我们开始研究称为open implementation的概念,这和现在称为白盒抽象之类的东西比较类似。最终,我们走到了AOP那,在那我们感觉我们已经关注到了一个关键问题,这个问题对于让程序员模块化他们代码中横切的结构的各种技术来说都非常常见。我们从1997年开始致力于AspectJ的开发,第一次发布给外部用户是在1998 年,而1.0 release是在2001年。AspectJ是由.DARPA资助的。

Dion:横切的结构是什么意思?

Gregor:它的意思是有两个或者更多的结构(或者说是分解),它们不能很好的装入另外一个中去。如果我们看看下面这个经典的AspectJ figure package的例子,我们可以看到一个结构,用黑色标记,它包含着FigureElement,Point和Line;这个结构是和figure elements的状态和绘出行为有关的。第二个结构,用红颜色标记,包含DisplayUpdating aspect;它是对figure elements的状态如何改变会触发屏幕刷新有关的。

setX,setY,setP1和setP2方法的一个aspect在黑色的figure element结构中描述了——setter方法如何改变figure elements的。同一个方法的其他aspect是在 display updating结构中描述的——它们都在被调用之后更新display。

(我知道这个例子看上去有一点微不足道,而且它实际上应该精确跟踪某个figure elements所在的是哪个display,但是在这个简单的形式中它确实帮助我们看清楚横切是什么意思。)在图中,DisplayUpdating aspect是一个单独的unit。但是同样的行为不能在单独一个黑色的结构中得到localized(局部化)。尝试着这么做会强制代码实现一种行为,这种行为分散在所有会改变display状态的方法之中。我们说figure element的状态这个关注点和display的更新这个关注点彼此横切了。

横切的结构不同于分层结构。横切意味着对系统的不同刻画,而不是仅仅移除细节以获得更抽象的视图。横切是比代码分散更深层次的属性。分散指的是给定一个实现,关注点的代码分布得到处都是,例如主要的图形行为和display刷新行为固有的彼此横切。AOP的目标是让模块(非离散的)能够实现横切的关注点。

Dion:AOP的现状如何?

Gregor:AOP技术和实践现在已经从发明阶段转向创新阶段。新一波的人们正在发明新的技术并且帮助它们得到广泛的采纳。因而这是一个伟大和令人激动的时刻。

发明阶段的特征是关注实际应用中的问题,以及比一般编程技术快得多的商业化速度。但是当然,从创新者的观点看来,任何发明阶段够看起来都那样缓慢,而且新来的人正在寻找加速的办法。作为一个社区,我们正在寻找AOP的第一个killer application。许多人们打赌那会是分布式的企业应用程序。

Dion:你现在看到的摆在AOP发展面前的一些挑战是什么?

Gregor:在回答这个问题时,我想我们可以通过回顾OOP的历史学到很多东西,特别是80年代的那段历史。第一个主要的挑战是避免过度神话那项技术。我们必须小心不让我们的狂热使得我们听起来正在宣称AOP将解决所有的软件问题。AOP是一个模块化技术——比如你有一些好的开发者,他们理解要解决的问题,AOP将帮助他们开发更好的,更干净的,更容易重用的代码而且更快更简单。它实际上比那做得还要多一些,因为我们已经学会了OOP,一个可以帮助我们在过程中刻画出问题的样子的模块化技术。但是AOP不会使得所有的软件变简单。它不会解决所有的模块化问题。它是开发者工具箱中重要的新工具,解决一类软件结构上的困难问题。我们对此必须小心,只有这样我们才不会显得正在做的是疯狂的咆哮。

另外一个挑战是确信我们没有遗忘以往的教训。无论何时一项技术从发明阶段转到创新阶段,总是有遗忘前一个阶段的某些教训的危险。对于这样情况的发生有一个自然的趋势,因为创新者要重新制造并且重新打包技术以服务特定的技术或者是特殊领域。我认为在发明者和创新者之间建立彼此沟通的桥梁至关重要——再一次的,我们可以从OO身上学到一些东西,OOPSLA会议是一个搭建这样桥梁的极好的例子。我愿意看到创新者和发明者积极的进行沟通。一些在前阶段的设计上的权衡可能对于今天并不是合适的;但是有一些在前阶段学到的关键技术问题应该被传承下去。我认为我们都能从这种交流中学到很多东西,并且将真正帮助我们更快地进步。

不幸地是,建立这种桥梁并不那么简单——我知道我已经作出了一些失败的尝试!每年的Aspect-Oriented Software Development会议(http://aosd.net/conference)想仿造OOPSLA的模式,它是产生这样的桥梁作用的好场所。明年春天的会议将有一个由Danny Sabbah(IBM Vice President, Development and SWG Technology, Application Integration & Middleware Division, Software Group)做的keynote讲话。因为IBM正在AOP方面进行大量的投资,所以这个讲演应该非常有趣。

另外一个关键的地方是AOP社区应该向传统市场进行推广,而不是在已经存在的AOP用户中彼此进行竞争。这儿,再一次的你可以看看OOP社区的成功经验。在OOPSLA 会议的早期,对不同语言阵营有一个协定好的努力,那就是去谈论关于其他OO语言什么是其优点,并且推动一般意义上的OOP胜过具体的哪个OOP。

Dion:许多人似乎使用相同的系统级关注点去谈论AOP(例如logging,debugging,transaction demarcation, security等等)。你能给我们举出一个你遇到的业务方面的关注点的例子吗?

Gregor:我意识到的第一件事情是你提到的这些关注点是非常重要的。但是为了回答你的问题,在具体的系统中,还是有许多其他的关注点有横切的结构的。Aspects可以用于维护一个类的几个方法之间的内在一致性。它非常适合强制一种按契约进行设计的编程风格。它可以用于强制各种常见的编码习惯。大量的OO设计模式也有横切的结构并且使用AspectJ能以一种模块化可重用的形式编码。

所以用AOP进行编程包括同时使用classes和aspects作为功能的自然单位编程,而且aspects和classes一样是真正的通用概念。IBM的Adrian Colyer,它是 eclipse.org上AspectJ的领导者,对这个问题讲得很好:

当任何人第一次看到AOP的时候,他们看到的例子以及他们要处理的关注点的类型趋向于对他们正在编写的核心程序(或者叫non-functional如果你更喜欢这个词的话)非常正交。我们正在谈论的经典情况包括tracing,logging以及错误处理等。在AspectJ的教程中我们有时称它们为辅助aspects。

当使用和过于得到OOP支持方式相似的AspectJ时,你会开始不仅仅只看到辅助aspects,还有我所说的遍布在你的代码中的核心aspect。就我个人来说,我甚至会使用一个内嵌在class中的aspect来处理横切这个类的几个方法的policy(比如在数据库访问类中的链接获取和释放),这样使得意图更加清晰。

在AOSD会议上,Ivar Jacobsen做了一个讲演,他解释了uses cases有横切的结构——这是一个使得它们有用的关键部分——并且你可以使用AOP来模块化的实现use- cases。

Dion:工具之间的不同看上去与织入和部署有关。

Gregor:在创新阶段发生的一件事情就是工程上的权衡在这个技术所考虑的特定市场的限制下重新被考虑。当我们做 AspectJ的时候,我们没有考虑aspects的动态部署因为我们看到了一些性能上的负担,而且我们想要保证AspectJ没有任何隐藏的性能负担。 但是现在,至少对于某些应用来说,AOP动态部署的威力时值得付出这种额外的性能代价的。像Mira Mezini这样的人们正在给AspectJ增加动态部署的能力,而且大部分新近的框架,像 JBoss,完全以动态部署来工作。 我的感觉是随着事物的发展,人们会开始关注性能。AspectJ已经在静态部署方面有好的性能,而且做静态强类型的动态部署这个问题看上去是可以处理的,因而我们应该在任何AOP工具中都应该能够提供极好的性能。 Dion:你预测未来人们会使用编译期织入的工具还是运行时织入的工具?标准是什么? Gregor:在AOP方法学中,织入意味着横切的aspect与剩下的代码进行调和。因而它可以由预处理器,编译期,编译后的链接器,loader,JIT或者VM来完成。 我们现在所处的情况是不同的工具在不同的时间织入。一些新近的框架,像AspectWerkz和JBoss AOP,使用interceptor在运行时织入。AspectJ在编译期或者后编译期完成织入。 在设计AspectJ的时候,我们努力的把何时进行织入的问题从语意中尽可能的分离开。因而AspectJ是可以顺从编译期到VM支持全阶段中任意时期织入的安排的。我相信如我们过去观察 OOP一样,AOP的这种类型的语意会以开发者最容易用的形式终结。因而我猜测将来用户会因为不同的工具提供不同的语意,而不是它们在不同的时候进行织入去使用某种工具。换句话说,织入是一个实现技术,像method dispatch(译注:指的是虚函数的调用)一样,它应该躲在幕后才对。我们应当为它提供的语意选择AOP工具(假定已经有了一个合适的实现)。 Dion:你对是否需要标准化有什么看法? Gregor:这是一个有趣的问题。Java has done pretty well for itself following a rule of not including any new ideas in the actual language standard. By "new" here I mean that have not been in the innovation phase for some time). I think this kind of conservatism is a good strategy for something with as many users as JLS. 但是AOP不再是那么新的了。想法本身有8年了,而且AspectJ已经有了5年的用户了。核心的元素到现在已经稳定了好几年了。在这点上我相信一边定出一个可以减少AOP工具之间不必要的差异的标准,一边留下继续实验的空间是一个对社群来说不错的主意。 我看这样的标准至少可以以两种形式起作用 AspectJ是当前事实上的标准,而且这变得越来越不可动摇。社区能够看清AspectJ的核心,而且AspectJ的开源开发者会同意把这些保持稳定。AspectJ现在是Eclipse的一部分意味着对它有持续增加的支持,因而这是继续下去的好办法。 但是某些人对AspectJ有顾虑,特别是有关动态部署,对Java的句法上的改变,以及其尺寸。因而另外一个可能就是发展一个AOP的核心,它支持包括AspectJ,JBoss,AspectWerkz以及甚至是研究中的系统。这个核心不定义任何语法,仅仅是核心的语意。首先以某种形式的草案出现。它可以使得在几年期间底层的实现专家专注于效率,安全等问题;同时,其他人可以专注于如何去打造这项技术的程序员界面。 Dion:你认为元数据(JSR-175)和AOP的关系是什么?我们以后能基于方法的元数据定义pointcut吗? Gregor:很清楚,在AOP中使用attribute是非常有用。一旦JSR175发布,我怀疑AspectJ至少会扩展成能够定义基于attributes匹配的pointcut。 随着时间的发展,我们会学会什么时候最好使用基于attributes的pointcut,什么时候最后使用其他类型的pointcuts。 在使用基于attributes的pointcuts的时候,我想有一些细微的问题,这些是我们未来必须要解决的。它们中的一些已经在 TSS的其他帖子上提出来了。 很清楚的是某种使用attributes的风格是“just macros”或者是“just declarative programming”。当然这不是什么坏事情,但是它可能不能提供我们所要的所有的模块性。它还可能使得难于使用AOP大部分最强大的特性——pointcut 组合。 我“认为”在使用attributes的时候我们应当小心,起一个描述“POJD”(plain old Java declaration)要描述的属性的名字,然后定义一个像advice这样的构造告诉什么样的aspect作用于带有那样 attribute的POJDs。这是和已经存在的实践相左的,现在attribute直接命名应当应用的aspect。 具体来说,让我们使用上一个figure的例子。我相信我们已经写这样的代码(假定方法在class中,advice在aspect中):
@ChangesDisplayState void setX(int x) { this.x = x; } after(): call(@ChangesDisplayState * *(..)) { Display.update(); }

而不是像这样的代码:

@UpdateDisplay void setX(int x) { this.x = x; } after(): call(@UpdateDisplay * *(..)) { Display.update(); } 

我相信这样做更好的原因是它更好的模块化了横切的关注点。

在这份代码中横切的关注点是“改变display状态的方法应当更新display”。在第一份代码中,这是在advice中模块化的。第二个关注点,“什么方法改变display状态”遍布到各处了。 在第二份代码中,两个关注点混杂在一起了并且都遍布到了各处。 我相信遵循这种风格的实践的好处是你得到了更能重用的 attribute名——你更可能在新的pointcut中以有益的方式使用已存在的attribute。 另外注意如果你不用attribute,像这样:
pointcut changesDisplayState(): call(void Point.setX(int)) || ...; after(): changesDisplayState() { Display.update(); } 
then both concerns are well localized in the code! Which just goes back to that we have some learning to do in terms of guidelines for when to use attributes and when not to. I'm writing something about that now, but its not ready to send out. Dion:AOP和OOP如何互相结合? Gregor:AOP补充了OOP的不足。AOP能够补足其他的编程范型,像过程式编程。但是因为OOP是现在的主流范型,大部分AOP工具都是对OO工具的扩展。 AOP补足了OOP比起仅仅用各种AOP工具作为Java(或者.NET)的扩展来说要意味着更多。它意味着面向侧面的编程实践补足了面向对象编程实践。好的OO开发者学起AOP来相当的快,因为它们已经对它们的系统哪些方面是横切的有了直觉的看法。这些也在他们的工作中有所反应——例如,一个好的AspectJ程序倾向于85%或者更多的部分是一个好的普通的Java程序。 Dion:某些人说AOP破坏了OO模块性的原则。你怎么看? Gregor:模块性的原则是诸如 textual locality,narraow declarative interface,更小的耦合,更强的内聚等等此类的东西。使用恰当,AOP不但没有破坏这些原则,反而帮助你达到这些目标。要看到为什么是这样,你应当看看那些适合AOP的例子,而不是普通的OO已经产生了很好模块性的例子。 在可以从AOP受益OO程序中,像前面的屏幕更新的例子,普通的OO解决方案的模块性可能只能是一种折衷的情况。触发屏幕刷新的代码可能就得分散到Point和Line的类中。这个关注点的代码可能就不能得到局部化,可能就没有清晰的接口,可能就只能是textually coupled,而且可能就降低了分散开的代码的内聚性。 如果你用AOP写同一个程序,模块性更好了。触发屏幕刷新的代码得到了局部化,它有一个给其余代码的声明性的接口(pointcut),它耦合更小,而且所有的代码内聚更强。 当然折不意味着不能用AOP写非模块性的代码。当然是可以的。它也不意味着我们不应该在给开发者更简单的产生更优雅的AOP代码方面做一些工作(我的意思是使用像OO的accessor rule那样的private fields)。 但是我相信应该做的是给程序员提供能写更漂亮的程序的工具,而不是施加足够的安全机制让他们不能编写糟糕的程序。而且程序员能够使用AOP 改进模块性方面的证据是明显的。 Dion:对于AOP使得程序更难理解的顾虑你怎么看? Gregor:AOP使得程序更容易理解,而不是更难。 再说一次,你应该考虑确实需要AOP的例子,而不是OOP独自就能产生很好的模块性的例子。考虑考虑不能从树木看到森林这个比喻吧。 在非AOP的程序中,你有一个像让缓存失效之类的关注点的分散开的实现,很容易从你正在看的那行代码中读出它是否使缓存失效了——在那会有对某些invalidate方法的调用。但是很难看到的使整个森林的样子——全局的缓存失效的结构可以告诉你真正是怎么去做的,从而你能用这些信息能够指出困难之处。 在AspectJ的程序中,对缓存失效的调用不出现在它发生的代码处,但是简单的IDE支持能够给你一个视觉上的提示这儿有一个调用。因而在AspectJ程序中,一个简单的程序让你可以看见树木所在。你还可以浏览advice以及pointcut,清楚地知道全局的结构。因而 AspectJ使得复杂的部分——整个森林的感觉——更明显了,更声明式,更清楚。而简单的部分——树木所在——可以通过工具简单的发现。 Dion:似乎对AOP支持是否应该如AspectJ这样扩展语言的语法还是应该如AspectWerkz这样的工具一样使用某种XML语法有大量的争论。你对于这个问题的观点是什么? Gregor:我认为这个问题有一些被过分强调了。我不是说这个不重要;有很多的原因。但是没有任何一个原因和AOP的本质有关。例如,你可以相当简单的给AspectJ的语法设计XML语法。AOP的关键问题基本上和这个语法上的问题没有关系。 Dion:从你看来觉得为什么人们认为语法问题那么重要?是不是和OOP一样,人们开始的时候说“我在C中进行 OO...,我不需要另外一门语言”? Gregor:我认为这是非常正确的。对于大多数人来说,在他们把它作为框架用过一段时间之前,对于在语言中添加一些新的东西是有一个自然的反感的。我认为这种反感可能恰恰是件好事情;我需要对什么东西加入语言中非常保持。但是在这个阶段AOP真不是什么新东西了,而且AspectJ的设计是基于多年来的使用经验和用户的反馈。我认为AOP将最终作为语言的一部分,而不是在语言之外的框架之中,但是要让每个人都对这感到习惯需要更多的一些时间。 除了我刚刚提到的这种类型的保守,还有一个人们有时会给出的挺上去并不那么可怕的原因。因为它是常见的声音,让我在这儿谈谈它。人们有时候说像AspectJ这样的工具学起来要困难得多因为它“需要学习一门新得语言,而AOP框架不需要”。我认为这完全是错误得 ——两种工具豆需要学习一门新的子语言。在像AspectJ这样的情况中,子语言是紧密的嵌入到Java之中的。在像AspectWerkz这样的情况中,子语言是编码在XML之中的。它们都是开发者必须学习的little sub-language,而且在框架中你还必须学习框架的API。 你的问题暗指了1980年代的OOP,那个时候一些人认为OOP不应该添加到语言之中;作为替代我们应当把它编程到框架之中去。春天时,这在TSS的一篇讨论中提到了。我认为对OOP来说语言是正确的方向,而且我认为对AOP也是如此。但是我也相信当前围绕框架进行的一些创新也是有价值的,因为它帮助我们探索了新的设计权衡。 而且语言支持的额外好处是像AspectJ这样的东西实际上比像JBoss这样的框架要功能弱一些。因为它从Java继承了一些像静态的强类型这样的限制。这种形式的功能减弱实际上可以让它得到广阔的价值,因为它使得能够IDE提供一些支持,这些支持使得它更容易理解代码正在干什么。 Dion:AspectJ正在发生什么变化? Gregor:AspectJ现在是Eclipse Technology PMC项目中的一个,而且IBM的Adrian Colyer已经监管了项目对开发者社区的领导。这使得AspectJ能够得到长期稳健的开发支持,这对于AspectJ的用户来说太棒了。 1.1 release刚刚发布,而它具有了增量编译的功能,这推动了在大系统中的使用;字节码的织入,这使得能够使用其他的Java编译期;以及支持aspect库。IDE的支持得到了极大的改进。Eclipse现在至此了aspect调试, inline advice annotation,以及一个高级的,能让你看到森林的,对aspects如何横切整个系统的视图。像aspect-oriented 重构这样的特性正在开发之中。 在IBM和Eclipse之外,其他的厂商也在支持AspectJ。IntelliJ提供了它们的实现的一个早期版本,BEA也在支持开发者在WebLogic程序中使用AspectJDion: Where should people go to learn more about AOP? Gregor: There are more sources of training and support all the time. Tutorials are available at The Server Side Symposium, No Fluff Just Stuff, Software Development, OOPSLA, and of course AOSD conferences. Many of these are focused specifically on AOP for enterprise applications. (In fact there is an AspectJ and a JBoss AOP tutorial at NFJS next weekend in Seattle.) There are also several books on AspectJ. There are now consultants who provide training, mentoring, and design and implementation assistance. Some of those consultants can be reached through an informal consortium I set up called AspectMentor? (http://aspectmentor.com) Dion :开发者现在就应该开始了解和使用AOP了吗? Gregor:我认为任何想要在最新的技术上领先的开发者都应该这么去做,毫无疑问,开始去研究AOP。我在OO身上学到的一点就是采用越早获利越多。AOP像OOP一样,真正的价值从把整个产品架构用aspect思考到开发周期都有。开始越早的组织会比其他人领先更多。吸引的都是最优秀的人,而且首先去开发aspect库以及aspect设计方面的经验会有重大的商业价值。 同时,我不鼓励人们轻率的全面转向AOP。我们从OO身上学到的另外一件事情就是恰当的管理要采纳的东西对于用新技术达到成功是至关重要的。 Dion:你认为AOP会以渐进的方式进入主流吗? Gregor:渐进的采用是很重要的。开发者太忙了,很难有时间去了解这种需要从头看起的技术,而且你需要在全面的跳入其中之前花一些时间去探索这些想法。 例如,AspectJ为渐进式采用AOP小心翼翼的设计出来的。它通过支持JVM兼容,JLS向后兼容,以及有像IDE和Javadoc扩展这样的工具来达到这个目的。 此外,在我们使用AspectJ的五年中我们已经开发了一个分阶段的采用的过程。在这个过程中,首先是对aspects的使用是作为开发工具,调试和增强的测试,或者内部的契约检查的工具。另外一个场景是用AspectJ实现横切关注点的原型,然后一旦你搞明白了情况,手动把代码编织进去,把AspectJ的代码作为注释。这个在开发过程中使用aspect的方式使得开发者能够试验AspectJ而不需要说服开发团队的其他人,或者是他们的老板。 在这样干了几个月之后,开发者拥有了两件事情:对用AspectJ实现AOP有了更好的理解,对于它能够给他们的核心基础带来的具体价值的具体例子。这是尝试说服团队其他人和经理的最好时刻,而且开始在销售的产品中使用aspect。 Dion:你认为AOP会像OO那样对软件开发有革命性的变化吗,为什么? Gregor:这是一个有趣的问题。我的答案是也不是。一方面,我认为aspect下面的思想在某种意义上比对象的思想还要深。对象是把系统刻画成一个层次化的分解的新办法。他们和过程在这个方面是类似的。 但是aspect是关于有多个,横切的分解的。意识到组织软件结构不一定是层次状的这个思想是非常重要的。 但是业界现在要比1964年(革新于74,84和94)发明对象时成熟多了。因而当我们期望aspect会有一个重大的实践效果时,很难说比起49年前发生的一些事情来说看上去会是什么样子的。 但是很清楚的是,能够模块化横切的关注点的能力会产生较大的影响,不仅仅是在分布式的企业级应用之中,而且还在我们开发的其他复杂的软件之中。它会改善我们开发的软件的灵活性和质量,并且会减少开发耗费的时间和代价。它还会使得软件开发更加有趣。 <script type="text/javascript"> </script> <script type="text/javascript" src="http://pagead2.googlesyndication.com/pagead/show_ads.js"> </script>
<script type="text/javascript"> </script><script type="text/javascript" src="http://pagead2.googlesyndication.com/pagead/show_ads.js"> </script>
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值