spring AOP 之一:spring AOP功能介绍
一、AOP简介
AOP:是一种面向切面的编程范式,是一种编程思想,旨在通过分离横切关注点,提高模块化,可以跨越对象关注点。Aop的典型应用即spring的事务机制,日志记录。利用AOP可以对业务逻辑的各个部分进行隔离,从而使得业务逻辑各部分之间的耦合度降低,提高程序的可重用性,同时提高了开发的效率。主要功能是:日志记录,性能统计,安全控制,事务处理,异常处理等等;主要的意图是:将日志记录,性能统计,安全控制,事务处理,异常处理等代码从业务逻辑代码中划分出来,通过对这些行为的分离,我们希望可以将它们独立到非指导业务逻辑的方法中,进而改变这些行为的时候不影响业务逻辑的代码。
1.1、AOP几个相关的概念:
名称 | 说明 |
切面(Aspect) | 一个关注点的模块化,这个关注点可能会横切多个对象 |
连接点(Joinpoint) | 程序执行过程中的某个特定的点。例如类初始化、方法执行、方法调用、字段调用或处理异常等等,Spring只支持方法执行连接点。 |
通知(Advice) | 在切面的某个特定的连接点上执行的动作。(通知定义了切面是什么以及何时使用。描述了切面要完成的工作和何时需要执行这个工作。) |
切入点(Pointcut) | 匹配连接点的断言,在AOP中通知和一个切入点的表达式。(例如某个类或方法的名称,Spring中允许我们方便的用正则表达式来指定) |
引入(Introduction) | 再不修改类代码的前提下,为类添加新的方法和属性。(也称为内部类型声明,为已有的类添加额外新的字段或方法,Spring允许引入新的接口(必须对应一个实现)到所有被代理对象(目标对象)) |
目标对象(Target Object) | 被一个或多个切面所通知的对象。(需要被织入横切关注点的对象,即该对象是切入点选择的对象,需要被通知的对象,从而也可称为“被通知对象”;由于Spring AOP 通过代理模式实现,从而这个对象永远是被代理对象) |
AOP代理(AOP Proxy) | AOP框架创建的对象,用来实现切面契约(aspect contract)(包括通知方法执行等功能) |
织入(Weaving) | 把切面连接到其他的应用程序类型或者对象上,并创建一个被通知的对象, 分为:编译时织入、类加载时织入、执行时织入。(将切面应用到目标对象从而创建出AOP代理对象的过程,织入可以在编译期、类装载期、运行期进行) |
把切面应用到目标对象来创建新的代理对象的过程,织入一般发生在如下几个时机:
(1)编译时:当一个类文件被编译时进行织入,这需要特殊的编译器才可以做的到,例如AspectJ的织入编译器
(2)类加载时:使用特殊的ClassLoader在目标类被加载到程序之前增强类的字节代码
(3)运行时:切面在运行的某个时刻被织入,SpringAOP就是以这种方式织入切面的,原理应该是使用了JDK的动态代理技术
1.2、Advice的类型
名称 | 说明 |
前置通知(Before advice) | 在某个连接点(join point)之前执行的通知,但不能阻止连接点前的执行(除非它抛出异常) |
返回后通知(After returning advice) | 在某个连接点(join point)正常完成后执行的通知 |
抛出异常后通知(After throwing advice) | 在方法抛出异常退出时执行的通知 |
后通知(After(finally) advice) | 当某个连接点退出的时候执行的通知(无论是正常返回还是异常退出) |
环绕通知(Around advice) | 包围一个连接点(join point)的通知 |
二、Spring的AOP实现
AOP实现方案:AspectJ和Spring AOP。
AspectJ:Aspectj是aop的java实现方案,AspectJ是一种编译期的用注解形式实现的AOP。
(1)AspectJ是一个代码生成工具(Code Generator),其中AspectJ语法就是用来定义代码生成规则的语法。基于自己的语法编译工具,编译的结果是JavaClass文件,运行的时候classpath需要包含AspectJ的一个jar文件(Runtime lib),支持编译时织入切面,即所谓的CTW机制,可以通过一个Ant或Maven任务来完成这个操作。
(2)AspectJ有自己的类装载器,支持在类装载时织入切面,即所谓的LTW机制。使用AspectJ LTW有两个主要步骤,第一,通过JVM的-javaagent参数设置LTW的织入器类包,以代理JVM默认的类加载器;第二,LTW织入器需要一个 aop.xml文件,在该文件中指定切面类和需要进行切面织入的目标类。
(3)AspectJ同样也支持运行时织入,运行时织入是基于动态代理的机制。(默认机制)
见《AspectJ入门》
Spring AOP:Spring AOP是AOP实现方案的一种,它支持在运行期基于动态代理的方式将aspect织入目标代码中来实现AOP。但是spring aop的切入点支持有限,而且对于static方法和final方法都无法支持aop(因为此类方法无法生成代理类);另外spring aop只支持对于ioc容器管理的bean,其他的普通java类无法支持aop。现在的spring整合了aspectj,在spring体系中可以使用aspectj语法来实现aop。
2.1、有接口无接口的Spring AOP 实现区别
- Spring AOP默认使用标准的javaSE动态代理作为AOP代理,这使得任何接口(或者接口集)都可以被代理
- Spring AOP中也可以使用CGLib代理(如果一个业务对象并没有实现一个接口)
2.2、Spring提供了4种实现AOP的方式:
1.经典的基于代理的AOP
2.@AspectJ注解驱动的切面 《spring AOP 之二:@Aspect注解的3种配置》
3.AOP标签的纯POJO切面
4.注入式AspectJ切面(编译期注入)见《AspectJ入门》
前三种都是Spring AOP实现的变体,Spring AOP构建在动态代理基础之上,因此,Spring对AOP的支持局限于方法拦截。
示例1
首先写一个接口叫Sleepable,这是一个牛X的接口,所有具有睡觉能力的东西都可以实现该接口(不光生物,包括关机选项里面的休眠)
package com.dxz.aop.demo1; public interface Sleepable { void sleep();
}
然后写一个Human类,他实现了这个接口
package com.dxz.aop.demo1; public class Human implements Sleepable {
public void sleep() {
System.out.println("睡觉了!梦中自有颜如玉!");
}
}
好了,这是主角,不过睡觉前后要做些辅助工作的,最基本的是脱穿衣服,失眠的人还要吃安眠药什么的,但是这些动作与纯粹的睡觉这一“业务逻辑”是不相干的,如果把这些代码全部加入到sleep方法中,是不是有违单一职责呢?,这时候我们就需要AOP了。
编写一个SleepHelper类,它里面包含了睡觉的辅助工作,用AOP术语来说它就应该是通知了,我们需要实现上面的接口。
package com.dxz.aop.demo1;
import java.lang.reflect.Method; import org.springframework.aop.AfterReturningAdvice;
import org.springframework.aop.MethodBeforeAdvice; public class SleepHelper implements MethodBeforeAdvice,AfterReturningAdvice{ public void before(Method mtd, Object[] arg1, Object arg2)
throws Throwable {
System.out.println("通常情况下睡觉之前要脱衣服!");
} public void afterReturning(Object arg0, Method arg1, Object[] arg2,
Object arg3) throws Throwable {
System.out.println("起床后要先穿衣服!");
} }
然后在spring配置文件applicationContext-aop1.xml中进行配置:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd"> <bean id="human" class="com.dxz.aop.demo1.Human">
</bean> <bean id="sleepHelper" class="com.dxz.aop.demo1.SleepHelper">
</bean> <bean id="sleepPointcut" class="org.springframework.aop.support.JdkRegexpMethodPointcut">
<property name="pattern" value=".*sleep" />
</bean> <bean id="sleepHelperAdvisor" class="org.springframework.aop.support.DefaultPointcutAdvisor">
<property name="advice" ref="sleepHelper" />
<property name="pointcut" ref="sleepPointcut" />
</bean> <bean id="humanProxy" class="org.springframework.aop.framework.ProxyFactoryBean">
<property name="target" ref="human" />
<property name="interceptorNames" value="sleepHelperAdvisor" />
<property name="proxyInterfaces" value="com.dxz.aop.demo1.Sleepable" />
</bean>
</beans>
测试类:
package com.dxz.aop.demo1; import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext; public class Test { public static void main(String[] args){
ApplicationContext appCtx = new ClassPathXmlApplicationContext("applicationContext-aop1.xml");
Sleepable sleeper = (Sleepable)appCtx.getBean("humanProxy");
sleeper.sleep();
}
}
程序运行产生结果:
十月 23, 2017 5:12:05 下午 org.springframework.context.support.ClassPathXmlApplicationContext prepareRefresh
信息: Refreshing org.springframework.context.support.ClassPathXmlApplicationContext@5197848c: startup date [Mon Oct 23 17:12:05 CST 2017]; root of context hierarchy
十月 23, 2017 5:12:05 下午 org.springframework.beans.factory.xml.XmlBeanDefinitionReader loadBeanDefinitions
信息: Loading XML bean definitions from class path resource [applicationContext-aop1.xml]
通常情况下睡觉之前要脱衣服!
睡觉了!梦中自有颜如玉!
起床后要先穿衣服!
OK!这是我们想要的结果,但是上面这个过程貌似有点复杂,尤其是配置切点跟通知,Spring提供了一种自动代理的功能,能让切点跟通知自动进行匹配,修改配置文件如下:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd"> <bean id="sleepHelper" class="com.dxz.aop.demo1.SleepHelper">
</bean> <bean id="sleepAdvisor"
class="org.springframework.aop.support.RegexpMethodPointcutAdvisor">
<property name="advice" ref="sleepHelper" />
<property name="pattern" value=".*sleep" />
</bean> <bean id="human" class="com.dxz.aop.demo1.Human">
</bean> <bean
class="org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator" />
</beans>
执行程序:
public static void main(String[] args){
ApplicationContext appCtx = new ClassPathXmlApplicationContext("applicationContext-aop11.xml");
Sleepable sleeper = (Sleepable)appCtx.getBean("human");
sleeper.sleep();
}
成功输出结果跟前面一样!
只要我们声明了org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator就能为方法匹配的bean自动创建代理!
但是这样还是要有很多工作要做,有更简单的方式吗?有!
一种方式是使用AspectJ提供的注解:
package com.dxz.aop.demo2; import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut; /**
* @Aspect的注解来标识切面
*/
@Aspect
//@Component
public class SleepHelper { public SleepHelper() { } @Pointcut("execution(* *.sleep())")
public void sleeppoint() {
} @Before("sleeppoint()")
public void beforeSleep() {
System.out.println("睡觉前要脱衣服!");
} @AfterReturning("sleeppoint()")
public void afterSleep() {
System.out.println("睡醒了要穿衣服!");
} }
用@Aspect的注解来标识切面,注意不要把它漏了,否则Spring创建代理的时候会找不到它,@Pointcut注解指定了切点,@Before和@AfterReturning指定了运行时的通知,注意的是要在注解中传入切点的名称。
然后我们在Spring配置文件上下点功夫,首先是增加AOP的XML命名空间和声明相关schema,见配置文件applicationContext-aop2.xml:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:context="http://www.springframework.org/schema/context"
xmlns:aop="http://www.springframework.org/schema/aop"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop-2.0.xsd"> <aop:aspectj-autoproxy/> <bean id="human" class="com.dxz.aop.demo2.Human">
</bean> <bean id="sleepHelper" class="com.dxz.aop.demo2.SleepHelper">
</bean>
</beans>
记得加上这个标签:
<aop:aspectj-autoproxy/> 有了这个Spring就能够自动扫描被@Aspect标注的切面了。
最后是运行,很简单方便了:
public static void main(String[] args){
ApplicationContext appCtx = new ClassPathXmlApplicationContext("applicationContext-aop2.xml");
Sleepable human = (Sleepable)appCtx.getBean("human");
human.sleep();
}
下面我们来看最后一种常用的实现AOP的方式:使用Spring来定义纯粹的POJO切面
AOP标签
前面我们用到了<aop:aspectj-autoproxy/>标签,Spring在aop的命名空间里面还提供了其他的配置元素:
<aop:advisor> 定义一个AOP通知者
<aop:after> 后通知
<aop:after-returning> 返回后通知
<aop:after-throwing> 抛出后通知
<aop:around> 周围通知
<aop:aspect>定义一个切面
<aop:before>前通知
<aop:config>顶级配置元素,类似于<beans>这种东西
<aop:pointcut>定义一个切点
我们用AOP标签来实现:
package com.dxz.aop.demo3; public class SleepHelper { public void beforeSleep()
throws Throwable {
System.out.println("通常情况下睡觉之前要脱衣服!");
} public void afterSleep() throws Throwable {
System.out.println("起床后要先穿衣服!");
} }
代码就不用继承啥了,只是修改配置文件,加入AOP配置即可:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:context="http://www.springframework.org/schema/context"
xmlns:aop="http://www.springframework.org/schema/aop"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop-2.0.xsd"> <!-- 配置AOP 《方式一》 -->
<aop:config>
<!-- 配置切面及通知 -->
<aop:aspect ref="sleepHelper">
<aop:before method="beforeSleep" pointcut="execution(public * *..Sleepable.sleep(..))" />
<aop:after method="afterSleep" pointcut="execution(public * *..Sleepable.sleep(..))" />
</aop:aspect>
</aop:config> <!-- 配置AOP 《方式二》-->
<aop:config>
<!-- 配置切点表达式 -->
<aop:pointcut id="pointcut"
expression="execution(public * *..Sleepable.sleep(..))" />
<!-- 配置切面及通知 -->
<aop:aspect ref="sleepHelper">
<aop:before method="beforeSleep" pointcut-ref="pointcut" />
<aop:after method="afterSleep" pointcut-ref="pointcut" />
</aop:aspect>
</aop:config>
<bean id="human" class="com.dxz.aop.demo3.Human">
</bean> <bean id="sleepHelper" class="com.dxz.aop.demo3.SleepHelper">
</bean>
</beans>
4 注入AspectJ切面
虽然Spring AOP能够满足许多应用的切面需求,但是与AspectJ相比,Spring AOP 是一个功能 比较弱的AOP解决方案。AspectJ提供了Spring AOP所不能支持的许多类型的切点。 例如,当我们需要在创建对象时应用通知,构造器切点就非常方便。不像某些其他面向对象语 言中的构造器,Java构造器不同于其他的正常方法。这使得Spring基于代理的AOP无法把通知 应用于对象的创建过程。 对于大部分功能来讲,AspectJ切面与Spring是相互独立的。虽然它们可以织入到任意的Java应 用中,这也包括了Spring应用,但是在应用AspectJ切面时几乎不会涉及到Spring。 但是精心设计且有意义的切面很可能依赖其他类来完成它们的工作。如果在执行通知时,切 面依赖于一个或多个类,我们可以在切面内部实例化这些协作的对象。但更好的方式是,我们 可以借助Spring的依赖注入把bean装配进AspectJ切面中。
示例见《AspectJ入门》中的示例
三、Spring AOP 原理剖析
通过前面介绍可以知道:AOP 代理其实是由 AOP 框架动态生成的一个对象,该对象可作为目标对象使用。AOP 代理包含了目标对象的全部方法,但 AOP 代理中的方法与目标对象的方法存在差异:AOP 方法在特定切入点添加了增强处理,并回调了目标对象的方法。
AOP 代理所包含的方法与目标对象的方法示意图如图 3 所示。
图 3.AOP 代理的方法与目标对象的方法
Spring 的 AOP 代理由 Spring 的 IoC 容器负责生成、管理,其依赖关系也由 IoC 容器负责管理。因此,AOP 代理可以直接使用容器中的其他 Bean 实例作为目标,这种关系可由 IoC 容器的依赖注入提供。
纵观 AOP 编程,其中需要程序员参与的只有 3 个部分:
- 定义普通业务组件。
- 定义切入点,一个切入点可能横切多个业务组件。
- 定义增强处理,增强处理就是在 AOP 框架为普通业务组件织入的处理动作。
上面 3 个部分的第一个部分是最平常不过的事情,无须额外说明。那么进行 AOP 编程的关键就是定义切入点和定义增强处理。一旦定义了合适的切入点和增强处理,AOP 框架将会自动生成 AOP 代理,而 AOP 代理的方法大致有如下公式:
代理对象的方法 = 增强处理 + 被代理对象的方法
在上面这个业务定义中,不难发现 Spring AOP 的实现原理其实很简单:AOP 框架负责动态地生成 AOP 代理类,这个代理类的方法则由 Advice 和回调目标对象的方法所组成。
对于前面提到的图 2 所示的软件调用结构:当方法 1、方法 2、方法 3 ……都需要去调用某个具有“横切”性质的方法时,传统的做法是程序员去手动修改方法 1、方法 2、方法 3 ……、通过代码来调用这个具有“横切”性质的方法,但这种做法的可扩展性不好,因为每次都要改代码。
于是 AOP 框架出现了,AOP 框架则可以“动态的”生成一个新的代理类,而这个代理类所包含的方法 1、方法 2、方法 3 ……也增加了调用这个具有“横切”性质的方法——但这种调用由 AOP 框架自动生成的代理类来负责,因此具有了极好的扩展性。程序员无需手动修改方法 1、方法 2、方法 3 的代码,程序员只要定义切入点即可—— AOP 框架所生成的 AOP 代理类中包含了新的方法 1、访法 2、方法 3,而 AOP 框架会根据切入点来决定是否要在方法 1、方法 2、方法 3 中回调具有“横切”性质的方法。
简而言之:AOP 原理的奥妙就在于动态地生成了代理类,这个代理类实现了图 2 的调用——这种调用无需程序员修改代码。接下来介绍的 CGLIB 就是一个代理生成库,下面介绍如何使用 CGLIB 来生成代理类。
spring AOP 之一:spring AOP功能介绍的更多相关文章
- Spring学习之Jar包功能介绍(转)
spring.jar 是包含有完整发布模块的单个jar 包.但是不包括mock.jar, aspects.jar, spring-portlet.jar, and spring-hibernate2. ...
- Spring框架IOC和AOP介绍
说明:本文部分内容参考其他优秀博客后结合自己实战例子改编如下 Spring框架是个轻量级的Java EE框架.所谓轻量级,是指不依赖于容器就能运行的.Struts.Hibernate也是轻量级的. 轻 ...
- spring学习(五)详细介绍AOP
AOP称为面向切面编程,在程序开发中主要用来解决一些系统层面上的问题,比如日志,事务,权限等待 它利用一种称为"横切"的技术,剖解开封装的对象内部,并将那些影响了多个类的公共行为封 ...
- Spring面向切面之AOP深入探讨
Spring之AOP深入探讨 刚接触AOP之前我已经找了网上各种博客论坛上的关于AOP的文章利于我理解因为听好多人说AOP很复杂,很深奥当我接触之后发现根本不是那么的难于理解.它只是一个基于OOP技术 ...
- [置顶] 深入浅出Spring(三) AOP详解
上次的博文深入浅出Spring(二) IoC详解中,我为大家简单介绍了一下Spring框架核心内容中的IoC,接下来我们继续讲解另一个核心AOP(Aspect Oriented Programming ...
- 专治不会看源码的毛病--spring源码解析AOP篇
昨天有个大牛说我啰嗦,眼光比较细碎,看不到重点.太他爷爷的有道理了!要说看人品,还是女孩子强一些.原来记得看到一个男孩子的抱怨,说怎么两人刚刚开始在一起,女孩子在心里就已经和他过完了一辈子.哥哥们,不 ...
- AOP及spring AOP的使用
介绍 AOP是一种概念(思想),并没有设定具体语言的实现. AOP是对oop的一种补充,不是取而代之. 具体思想:定义一个切面,在切面的纵向定义处理方法,处理完成之后,回到横向业务流. 特征 散布于应 ...
- JAVA WEB快速入门之通过一个简单的Spring项目了解Spring的核心(AOP、IOC)
接上篇<JAVA WEB快速入门之从编写一个JSP WEB网站了解JSP WEB网站的基本结构.调试.部署>,通过一个简单的JSP WEB网站了解了JAVA WEB相关的知识,比如:Ser ...
- Spring boot中使用aop详解
版权声明:本文为博主武伟峰原创文章,转载请注明地址http://blog.csdn.net/tianyaleixiaowu. aop是spring的两大功能模块之一,功能非常强大,为解 ...
随机推荐
- java-类中需注意的问题
1.对成员变量的操作只能放在方法中,方法可以对成员变量和该方法中声明的局部变量进行操作. 在声明类的成员变量时,可以同时赋予初值,例如: class Test { int a=12; float b= ...
- C# 后台获取前台交互判断
前台传来明细 ,判断是否修改,在把前台 的数据组成新的类保存 public class tt { public string id { get; set; } public string e_id { ...
- dijksta 模板
#include<bits/stdc++.h> using namespace std; #define INF 0x3f3f3f3f ]; ]; ][]; void dijkstra(i ...
- 《DSP using MATLAB》Problem 5.15
代码: %% ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ %% Output In ...
- day43 数据库学习 转自egon 老师博客 单表查询和多表查询
一 单表查询的语法 SELECT 字段1,字段2... FROM 表名 WHERE 条件 GROUP BY field HAVING 筛选 ORDER BY field LIMIT 限制条数 二 关键 ...
- VMware Ubuntu如何连接互联网
Brigde——桥接 :默认使用VMnet0 1.原理: Bridge 桥”就是一个主机,这个机器拥有两块网卡,分别处于两个局域网中,同时在”桥”上,运行着程序,让局域网A中的所有数据包原封不动的 ...
- centos7 svn服务器的搭建
centos7下svn的安装与配置 1.环境 centos7 2.安装svnyum -y install subversion 3.配置 建立版本库目录mkdir /www/svndata svn ...
- centos6.5 系统乱码解决 i18n --摘自http://blog.csdn.net/yangkai_hudong/article/details/19033393
二.终端. gedit 显示乱码 #vi /etc/sysconfig/i18n 将LANG="en_US.UTF-8" SYSFONT="latarcyrheb-sun ...
- NDK学习笔记(三):DynamicKnobs的机制
最近的NDK开发涉及到了动态input及动态knobs的问题. 开发需求如下:建立一个节点,该节点能获取每一个input上游的inputframerange信息. 具体下来就是:需要Node的inpu ...
- Python实例讲解 -- wxpython 基本的控件 (按钮)
使用按钮工作 在wxPython 中有很多不同类型的按钮.这一节,我们将讨论文本按钮.位图按钮.开关按钮(toggle buttons )和通用(generic )按钮. 如何生成一个按钮? 在第一部 ...