Schema,即XML Schema,XSD (XML Schema Definition)是W3C于2001年5月发布的推荐标准,指出如何形式描述XML文档的元素。本章为整个AOP专题的开篇,将以最基本的XML配置的形式,讲解Spring AOP的知识。
AOP(Aspect Oriented Programming)俗称面向切面编程,是OOP(Object Oriented Programming)面向对象编程的补充。通常在一个程序开发中会有一些重叠性功能代码,最常见的就是权限认证、日志统计、全局异常、接口API统计等,使用面向对象编程,实现这些功能,你可以想想看,可能需要每个地方都要调用一遍这些代码,导致大量重复代码,不利于模块的重复理用。
那有没有一个类似拦截功能的方案来解决这个问题呢,那就是AOP了。它能剖解开封装的对象内部,横切这些纵向逻辑!简单的来说,就是一个系统中提供了无数个功能,每个功能都需要记录执行的过程。OOP就是每个功能中调用一遍日志统计的代码,而AOP,将日志统计的代码根据定义的规则,横插在每个功能的需要的地方,比如方法执行结束。
AOP横切示意图
AOP的相关概念
首先描述一些概念性的东西,帮助你更好的理解AOP是干什么的以及整个流程,我还画了他们之间的关系图,这些概念你理解即可,不理解的可以先阅读一下,做个了解,最终,将会以代码的方式体现,会更直观,相关AOP概念如下:
-
关注点:也就是需求点,想对哪些方法(功能)进行横切以及怎么处理
-
切面(aspect):切面它也是一个对象,是关注点特征的抽象。就像面向对象中类是对物体特征的抽象。
-
连接点(joinpoint):连接点也就是被切面拦截的点,通常指被拦截的方法
-
切入点(pointcut):对拦截连接点的定义,也就是怎么拦截,拦截哪些方法。
-
通知(advice):拦截到连接点之后要执行的代码,通知分为前置、后置、异常、返回结果、环绕通知
-
目标对象:代理的目标对象,也就是拦截的对象
-
织入(weave):将切面应用到目标对象并创建代理对象的过程
-
引入(introduction):在运行期为类动态地添加一些方法、字段或者改变继承关系
Spring AOP
Spring AOP基于动态代理实现,使用JDK动态代理与CGLIB代理,他们的区别在于前者基于接口,后者基于类。Spring对AOP的支持离不开Spring的IOC容器,其代理对象的生成,管理及其依赖关系都是由IOC容器负责,所以IOC容器管理的bean都可以作为AOP的目标对象。
基于Schema的AOP实现,基本上遵循一下三个步骤:
- 实现一个业务组件或者功能组件,比如登录
- 定义一个切面,定义切入点,以及需要的通知方法,也可称为织入处理方法。
- 在XML中配置切面与目标对象
Spring AOP需要的依赖包,我这里以Spring4为例,aspectjrt与aspectjweaver是必须的,版本最好在1.6以上
spring-aop-4.3.2.RELEASE.jar
aspectjrt-1.8.10.jar
aspectjweaver-1.8.10.jar
基于Schema的AOP定义通过“aop"命名空间,所有的相关定义都在<aop:congfig>内。<aop:congfig>包含<aop:pointcut>、<aop:advisor>、<aop:aspect>,三者的配置顺序不能变。
- <aop:pointcut>:切入点的定义
- <aop:advisor>:通知的定义
- <aop:aspect>:切面的定义
我们的重点关注在<aop:aspect>上面
切面的定义
切面就是包含切入点和通知的对象,在Spring容器中将被定义为一个Bean,使用<aop:aspect>标签指定,其中ref属性指定切面Bean,order属性可以指定多个切面情况下执行的顺序。首先我们定义一个切面类,暂时什么都不做:
public class TestAspect {
}
然后,定义一个业务组件(被切的业务类),并实现一个业务方法,随便打印一句话
public class TestBiz {
public void biz(){
System.out.println("test biz");
}
}
按照前面描述的实现AOP步骤,接下来就是在XML中描述Bean与AOP,在classpath下新建"spring-aop-cfg.xml"作为Spring配置文件,然后定义Bean与切面,看如下的代码:
<?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:aop="http://www.springframework.org/schema/aop"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-4.0.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop.xsd">
<bean id="testAspect" class="com.mmdet.learn.ssm.testaop.TestAspect"/>
<bean id="testBiz" class="com.mmdet.learn.ssm.testaop.TestBiz"/>
<aop:config>
<aop:aspect id="testAspectAop" ref="testAspect">
</aop:aspect>
</aop:config>
</beans>
testBiz为目标类,通过<aop:aspect>指定testAspect为切面
切入点的定义
切入点在Spring中也是一个Bean,可以声明id,切入点的定义有三种方式:
- 在<aop:config>中通过<aop:pointcut>定义,它与<aop:aspect>是平级关系,该切入点可以被多个切面使用。
- 在<aop:aspect>中定义,也是使用<aop:pointcut>,此时他是<aop:aspect>的子标签。为当前切面所使用。
- 在声明通知时通过pointcut属性指定切入点表达式,该切入点是匿名切入点,只被该通知使用。
虽然有三种方式,但是定义确是如出一辙,以<aop:aspect>中定义为例,定义一个切入点,看如下代码:
<?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:aop="http://www.springframework.org/schema/aop"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-4.0.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop.xsd">
<bean id="testAspect" class="com.mmdet.learn.ssm.testaop.TestAspect"/>
<bean id="testBiz" class="com.mmdet.learn.ssm.testaop.TestBiz"/>
<aop:config>
<aop:aspect id="testAspectAop" ref="testAspect">
<aop:pointcut
id="testAspectPointcut"
expression="execution(* com.mmdet.learn.ssm.testaop.TestBiz.*(..))"/>
</aop:aspect>
</aop:config>
</beans>
通过expression定义切入点规则,也就是要拦截谁,例子中表示拦截TestBiz类中的所有方法。至于切入点的定义规则有很多种,这里只是演示了一种,不过不用担心,你先将整个流程走通,回过头来,本专题会有单独一篇文章精讲切入点定义与后面的增强方法匹配规则,敬请关注。
切入点定义完毕,接下来就是定义通知了。
通知的定义
通知就是根据切入点匹配到目标后执行的一系列处理方法,根据上面定义的切点,通俗的讲,就是拦截到TestBiz的方法执行,就会执行切面的某个方法,它一共有五种。
- 前置通知:在切入点匹配的方法之前执行,通过<aop:aspect>标签下的<aop:before>标签声明
- 后置返回通知:在切入点匹配的方法正常返回时执行,通过<aop:aspect>标签下的<aop:after-returning>标签声明
- 后置异常通知:在切入点匹配的方法抛出异常时执行,通过<aop:aspect>标签下的<aop:after-throwing>标签声明
- 后置通知:在切入点匹配的方法返回时(完成时)执行,不管是正常返回还是抛出异常都执行,通过<aop:aspect>标签下的<aop:after >标签声明
- 环绕通知:环绕着在切入点匹配的连接点处的方法所执行的通知,也就是目标方法执行前后所执行的通知,可通过<aop:aspect>标签下的<aop:around >标签声明
前四个通知的定义是一致的,除了指定的方法不同,以前置通知为例说明,其他雷同,环绕通知另外说明:
<?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:aop="http://www.springframework.org/schema/aop"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-4.0.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop.xsd">
<bean id="testAspect" class="com.mmdet.learn.ssm.testaop.TestAspect"/>
<bean id="testBiz" class="com.mmdet.learn.ssm.testaop.TestBiz"/>
<aop:config>
<aop:aspect id="testAspectAop" ref="testAspect">
<aop:pointcut
id="testAspectPointcut"
expression="execution(* com.mmdet.learn.ssm.testaop.TestBiz.*(..))"/>
<aop:before method="before" pointcut-ref="testAspectPointcut"/>
</aop:aspect>
</aop:config>
</beans>
<aop:before>定义了前置通知,其中pointcut和pointcut-ref属性二者选一即可,指定切入点,method指定前置通知实现方法名,代码中为before,我们需要在切面类中定义它:
public class TestAspect {
public void before(){
System.out.println("Aspect before");
}
}
其他几个通知也一样,我们给补齐,完整代码如下:
public class TestAspect {
public void before(){
System.out.println("Aspect before");
}
public void after(){
System.out.println("Aspect after");
}
public void afterReturning(){
System.out.println("Aspect after Returning");
}
public void afterThrowing(){
System.out.println("Aspect after throwing");
}
}
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:aop="http://www.springframework.org/schema/aop"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-4.0.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop.xsd">
<bean id="testAspect" class="com.mmdet.learn.ssm.testaop.TestAspect"/>
<bean id="testBiz" class="com.mmdet.learn.ssm.testaop.TestBiz"/>
<aop:config>
<aop:aspect id="testAspectAop" ref="testAspect">
<aop:pointcut
id="testAspectPointcut"
expression="execution(* com.mmdet.learn.ssm.testaop.TestBiz.*(..))"/>
<aop:before method="before" pointcut-ref="testAspectPointcut"/>
<aop:after-returning method="afterReturning" pointcut-ref="testAspectPointcut"/>
<aop:after method="after" pointcut-ref="testAspectPointcut"/>
<aop:after-throwing method="afterThrowing" pointcut-ref="testAspectPointcut"/>
</aop:aspect>
</aop:config>
</beans>
如上,已经将四个通知定义好了,进入测试环节:
public class Test {
public static void main(String[] args) {
ApplicationContext context = new ClassPathXmlApplicationContext("spring-aop-cfg.xml");
TestBiz testBiz = (TestBiz)context.getBean("testBiz");
testBiz.biz();
}
}
测试结果,输出顺序如下:
Aspect before
test biz
Aspect after
Aspect after Returning
环绕通知
环绕通知相对其他四个通知来说,稍微有点区别,它至少接收一个ProceedingJoinPoint类型的参数,并且需要返回值。
首先定义一个环绕通知around()
public class TestAspect {
...
public Object around(ProceedingJoinPoint joinPoint){
Object obj = null;
try {
System.out.println("Aspect before around");
obj = joinPoint.proceed();
System.out.println("Aspect after around");
} catch (Throwable throwable) {
throwable.printStackTrace();
}
return obj;
}
}
环绕通知 ProceedingJoinPoint 执行proceed方法的作用是让目标方法执行,并返回执行结果,如上代码中,在其前后我们可以插入相关代码,所以称围绕。然后在xml中配置<aop:around>:
<?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:aop="http://www.springframework.org/schema/aop"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-4.0.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop.xsd">
<bean id="testAspect" class="com.mmdet.learn.ssm.testaop.TestAspect"/>
<bean id="testBiz" class="com.mmdet.learn.ssm.testaop.TestBiz"/>
<aop:config>
<aop:aspect id="testAspectAop" ref="testAspect">
<aop:pointcut
id="testAspectPointcut"
expression="execution(* com.mmdet.learn.ssm.testaop.TestBiz.*(..))"/>
<aop:before method="before" pointcut-ref="testAspectPointcut"/>
<aop:after-returning method="afterReturning" pointcut-ref="testAspectPointcut"/>
<aop:after method="after" pointcut-ref="testAspectPointcut"/>
<aop:after-throwing method="afterThrowing" pointcut-ref="testAspectPointcut"/>
<aop:around method="around" pointcut-ref="testAspectPointcut"/>
</aop:aspect>
</aop:config>
</beans>
配置完成后,测试,测试代码和上面的测试的代码一样,不变。测试结果:
Aspect before
Aspect before around
test biz
Aspect after around
Aspect after
Aspect after Returning
画了一张图,大家感受一下整个过程:
通知执行的点(顺序)异常通知
上述测试代码中,并没体现异常,我们修改一下业务代码,添加一行会异常的代码:
public class TestBiz {
public void biz(){
System.out.println("test biz");
System.out.println(2/0);
}
}
执行测试代码,执行时请在XML中暂时去掉<aop:around>的配置,运行结果如下:
Aspect before
test biz
Aspect after
Aspect after throwing
由于环绕通知中是捕获了异常的,所以若是执行到环绕通知的话,Aspect after throwing就不会执行了。所以在通知处理中是否捕获异常还是抛出,根据你的具体需求来定。
参数传递
上面的例子中都是无参方法,假如一个业务组件是有参数的,并且参数要传递给通知,那么怎么做呢?在切入点上和通知方法上做一些调整,以环绕通知为例,其他雷同!
首先定一个带参数的业务方法:
public class TestBiz {
public void init(String name,int age){
System.out.println("test init name " + name + ",age " + age);
}
}
定义一个接收参数的环绕通知
public class TestAspect {
public Object aroundinit(ProceedingJoinPoint joinPoint,String name,int age){
Object obj = null;
try {
System.out.println("Aspect before around");
obj = joinPoint.proceed();
System.out.println("Aspect around" + name+","+age);
System.out.println("Aspect after around");
} catch (Throwable throwable) {
throwable.printStackTrace();
}
return obj;
}
}
在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:aop="http://www.springframework.org/schema/aop"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-4.0.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop.xsd">
<bean id="testAspect" class="com.mmdet.learn.ssm.testaop.TestAspect"/>
<bean id="testBiz" class="com.mmdet.learn.ssm.testaop.TestBiz"/>
<aop:config>
<aop:aspect id="testAspectAop" ref="testAspect">
<aop:around method="aroundinit" pointcut="execution(* com.mmdet.learn.ssm.testaop.TestBiz.init(String,int))
and args(name, age)"/>
</aop:aspect>
</aop:config>
</beans>
编写测试代码,并执行
public class Test {
public static void main(String[] args) {
ApplicationContext context = new ClassPathXmlApplicationContext("spring-aop-cfg.xml");
TestBiz testBiz = (TestBiz)context.getBean("testBiz");
testBiz.init("gj",25);
}
}
执行结果:
Aspect before around
test init name gj,age 25
Aspect aroundgj,25
Aspect after around
成功接收到了参数,关键在于切入点的定义上
"execution(* com.mmdet.learn.ssm.testaop.TestBiz.init(String,int))
and args(name, age)"
init中定义参数的类型,args中定义的是参数名,用and连接,要和业务组件定的参数名一致,通知接收的时候,也必须保持一致。
引入
暂且这么叫吧,Spring引入允许为目标对象引入新的接口,通过在< aop:aspect>标签内使用< aop:declare-parents>标签进行引入,它有三个属性:
- types-matching:匹配需要引入接口的目标对象的AspectJ语法类型表达式;
- implement-interface:定义需要引入的接口;
- default-impl和delegate-ref:定义引入接口的默认实现,二者选一,default-impl是接口的默认实现类全限定名,而delegate-ref是默认的实现的委托Bean名;
定一个新的接口以及实现类
public interface Flt {
void filter();
}
public class FltImpl implements Flt {
@Override
public void filter() {
System.out.println("filter filter");
}
}
在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:aop="http://www.springframework.org/schema/aop"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-4.0.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop.xsd">
<bean id="testAspect" class="com.mmdet.learn.ssm.testaop.TestAspect"/>
<bean id="testBiz" class="com.mmdet.learn.ssm.testaop.TestBiz"/>
<aop:config>
<aop:aspect id="testAspectAop" ref="testAspect">
<aop:declare-parents
types-matching="com.mmdet.learn.ssm.testaop.*+"
implement-interface="com.mmdet.learn.ssm.testaop.Flt"
default-impl="com.mmdet.learn.ssm.testaop.FltImpl"/>
</aop:aspect>
</aop:config>
</beans>
如上代码表示 匹配testaop包下的所有类,将其接口替换为Flt,默认实现类为FltImpl
测试代码
public class Test {
public static void main(String[] args) {
ApplicationContext context = new ClassPathXmlApplicationContext("spring-aop-cfg.xml");
Flt flt = (Flt)context.getBean("testBiz");
flt.filter();
}
}
这里获取到的Bean类型为Flt。执行结果:
filter filter
成功输出FltImpl 类的filter方法执行结果。
总结
AOP是一种面向切面的编程方式,是对面向对象的补充,能够横切我们的业务代码,不要被它的概念唬住,注重理解整个AOP的实现过程(开发业务组件,定义切面,定义切点,通知处理)以及它的概念在代码中具体的应用。
该篇主要基于XML来实现整个AOP流程的,其中一些细节在于尝试,比如异常那部分,可以试试捕获,可以试试抛出,试试环绕通知中做点更多的事情,比如获取方法的参数、方法名等。
还有一部分就是关于切入点的匹配规则,以及引入时也有一个匹配关系定义,这些内容比较琐碎繁多,会单开一篇专门讲述。有任何问题,你也可以关注下面的公众号咨询,bye。
公众号
网友评论