考虑这样一个问题:需要对系统中的某些业务做日志记录,比如支付系统中的支付业务需要记录支付相关日志,对于支付系统可能相当复杂,比如可能有自己的支付系统,也可能引入第三方支付平台,面对这样的支付系统该如何解决呢?
传统解决方案
1.日志部分定义公共类LogUtils,定义logPayBegin方法用于记录支付开始日志,
logPayEnd用于记录支付结果
logPayBegin(long userId,long money)
logPayEnd(long userId,long money,boolean success)
2.支付部分,定义IPayService接口并定义支付方法pay,并定义了两个实现:
PointPayService表示积分支付,RMBPayService表示人民币支付,并且在每个支付实现中支付逻辑和记录日志
IPayService
boolean pay(long userId,,long money)
PointPayService
boolean pay(long userId,long money){
LogUtils.logPayBegin(userId,money);
支付逻辑
logUtils.logPayEnd(userId,money,success);
}
RMBPayService
boolean pay(long userId,long money){
LogUtils.logPayBegin(userId,money);
支付逻辑
logUtils.logPayEnd(userId,money,success);
}
3.支付实现很明显有重复代码 ,这个重复很明显可以使用模板设计模式消除重复:
IPayService
boolean pay(long userId,,long money)
BasePayService
boolean pay(long userId,,long money)
//支付模板
boolean payInternal(long userId,long money){
LogUtils.logPayBegin(userId,money);
boolean success = payInternal(userId,money);
logUtils.logPayEnd(userId,money,success);
}
PointPayService
boolean pay(long userId,long money){
支付逻辑
}
RMBPayService
boolean pay(long userId,long money){
支付逻辑
}
4.到此我们设计了一个可以复用的接口,但大家觉得这样记录日志会很好吗?有没有更好的解决方案?
如果对积分支付方式添加统计功能,比如在支付时记录下用户总积分数、当前消费的积分数,那我们该如何做呢?直接修改源代码添加日志记录,这完全违背了面向对象最重要的原则之一:开闭原则(对扩展开放,对修改关闭)?
更好的解决方案:
采用面向切面编程方式
AOP主要用于横切关注点分离和织入,因此需要理解横切关注点和织入
关注点:可以认为是所关注的任何东西,比如上边的支付组件
关注点分离:将问题细化从而单独部分,即可以理解为不可再分割的组件,如上边的日志组件和支付组件
横切关注点:一个组件无法完成需要的功能,需要其他组件协作完成,如日志组件横切于支付组件
织入:横切关注点分离后,需要通过某种技术将横切关注点融合到系统中从而完成需要的功能,因此需要织入,织入可能在编译期、加载期、运行期等进行。
横切关注点可能包含很多,比如非业务的:日志、事务处理、缓存、性能统计、权限控制等等这些非业务的基础功能;还可能是业务的:如某个业务组件横切于多个模块。
aop能干什么:
用于横切关注点的分离和织入横切关注点到系统;比如上边提到的日志等等
完善OOP
降低组件和模块之间的耦合性
使系统容易扩展
而且由于关注点分离从而可以获得组件的更好复用
连接点(Jointpoint) :表示需要在程序中插入横切关注点的扩展点,连接点可能是类初始化、方法执行、方法调用、字段调用或处理异常等等,Spring只支持方法执行连接点,在AOP中表示为“在哪里干”;
切入点(Pointecut):选择一组相关连接点的模式,即可以认为连接点的集合,Spring支持perl5正则表达式和AspectJ切入点模式,Spring默认使用AspectJ语法,在AOP中表示未“在哪里干的集合”;
通知(Advice):在连接点上执行的行为,通知提供了在AOP中需要在切入点所选择的连接点处进行扩展现有行为的手段;包括前置通知before advice、后置通知after advice、环绕通知 around advice,在Spring中通过代理模式实现AOP,并通过拦截器模式以环绕连接点的拦截器链织入通知:在AOP中表示未“干什么”;
方面/切面(Aspect):横切关注点的模块化,比如上面提到的日志组件。可以认为是通知、引入和切入点的组合;在Spring中可以使用Schema和@AspectJ方式进行组织实现;在AOP中表示未“在哪干和干什么集合”;
引入(inter-type declaration) :也称为内部类型声明,为已有的类添加额外新的字段或方法,Spring允许引入新的接口(必须对应一个实现)到所有被代理对象(目标对象),在AOP中表示为“干什么(引入什么)”;
目标对象(Target Object):需要被织入横切关注点的对象,即该对象是切入点选择的对象,需要被通知的对象,从而也可以称为“被通知对象”;由于SpringAOP通过代理模式实现,从而这个对象永远是被代理对象,在AOP中表示未“对谁干”
AOP代理(AOP Proxy):AOP框架使用代理模式创建的对象,从而实现在连接点处插入通知(即应用切面),就是通过代理来对目标对象应用切面。在Spring中,AOP代理可以用JDK动态代理或CGLIB代理实现,而通过拦截器模型应用切面。
织入(Weaving):织入是一个过程,是将切面应用到目标对象从而创建出AOP代理对象的过程,织入可以在编译期、类装载期、运行期进行。
在AOP中,通过切入点选择目标对象的连接点,然后在目标对象的相应连接点处织入通知,而切入点和通知就是切面(横切关注点),而在目标对象连接点处应用切面的实现方式是通过AOP代理对象。
通知类型:
前置通知 Before Advice :在切入点选择的连接点处的方法之前执行的通知,该通知不影响正常程序执行流程(除非该通知抛出异常,该异常将中断当前方法链的执行而返回)
后置通知 After Advice:在切入点选择的连接点处的方法之后执行的通知,包括如下类型的后置通知:
后置返回通知After returning Advice:在切入点选择的连接点处的方法正常执行完毕时执行的通知,必须是连接点处的方法没抛出任何异常正常返回时才调用后置返回通知。
后置异常通知After throwing Advice:在切入点选择的连接点处的方法抛出异常返回时执行的通知,必须是连接点处的方法抛出任何异常返回时才调用异常通知。
后置最终通知After finally Advice :在切入点选择的连接点处的方法返回时执行的通知,不管抛没有抛异常都执行。
环绕通知 Around Advice:环绕着在切入点选择的连接点处的方法所执行的通知,环绕通知可以在方法调用之前和之后自定义任何行为,并且可以决定是否执行连接点处的方法、替换返回值、抛出异常等等。
AOP代理就是AOP框架通过代理模式创建的对象,Spring使用JDK动态代理或CGLIB代理来实现,Spring缺省使用JDK动态代理来实现,从而任何接口都可被代理,如果被代理的对象实现不是接口将模式使用CGLIB代理,不过CGLIB代理当然也可应用到接口。
AOP代理的目的就是将切面织入到目标对象AOP的HelloWorld
准备环境:
org.springframework.aop-3.0.1.RELEASE-A.jar
com.springsource.org.aopalliance-1.0.0.jar
com.springsource.org.aspectj.weaver-1.6.8.RELEASE.jar
com.springsource.net.sf.cglib-2.2.0.jar
定义目标类
//1.定义目标接口
package com.lizhenhua.AOP.service;
/**
* 代理目标的接口IHelloWorldService
* @author Administrator
*
*/
public interface IHelloWorldService {
public void sayHello();
}
//2.定义目标接口实现
package com.lizhenhua.AOP.service.impl;
import com.lizhenhua.AOP.service.IHelloWorldService;
public class HelloWorldService implements IHelloWorldService {
@Override
public void sayHello() {
System.out.println("=========Hello World!");
}
}
//注:在日常开发中最后将业务逻辑定义在一个专门的service包下,而实现定义在service包下的impl包中,服务接口以IXXXService形式,而服务实现就是XXXService,这就是规约设计,见名知义。
有了目标类,该定义切面了,切面就是通知和切入点的组合,而切面是通过配置方式定义的,因此定义切面前,我们需要定义切面支持类,切面支持类提供了通知实现:
package com.lizhenhua.AOP.aop;
public class HelloWorldAspect {
//前置通知
public void beforeAdvice(){
System.out.println("======before advice");
}
//后置通知
public void afterFinallyAdivice(){
System.out.println("========after finally advice");
}
}
此处HelloWorldAspect类不是真正的切面实现,只是定义了通知实现的类,在此我们可以把它看作就是缺少了切入点的切面。
注:对于aop相关类最后专门放到一个包下,如aop包,因为AOP是动态织入的,所以如果某个目标类被AOP拦截了并应用了通知,可能很难发现这个通知实现在哪个包里,因此推荐使用规约命名,方便以后维护人员查找相应的AOP实现。
有了通知实现,那就让我们来配置切面
1)首先配置AOP需要aop命名空间,配置头如下:
<beans
xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:p="http://www.springframework.org/schema/p"
xmlns:aop="http://www.springframework.org/schema/aop"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop-3.0.xsd">
beans>
配置目标类
<beans
xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:p="http://www.springframework.org/schema/p"
xmlns:aop="http://www.springframework.org/schema/aop"
xmlns:tx="http://www.springframework.org/schema/tx"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop-3.0.xsd
http://www.springframework.org/schema/tx
http://www.springframework.org/schema/tx/spring-tx-3.0.xsd">
<bean id="aspect" class="com.lizhenhua.AOP.aop.HelloWorldAspect">bean>
<bean id="helloWorldService" class="com.lizhenhua.AOP.service.impl.HelloWorldService">bean>
<aop:config >
<aop:pointcut expression="execution(* com.lizhenhua.AOP.service...*.*(..))" id="pointcut"/>
<aop:aspect ref="aspect">
<aop:before pointcut-ref="pointcut" method="beforeAdvice"/>
<aop:after pointcut="execution(* com.lizhenhua.AOP.service...*.*(..))" method="afterFinallyAdvice"/>
aop:aspect>
aop:config>
beans>
调用被代理Bean跟调用普通Bean完全一样,Spring AOP将为目标对象创建AOP代理
package com.lizhenhua.AOP;
import org.junit.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import com.lizhenhua.AOP.service.IHelloWorldService;
public class AopTest {
@Test
public void testHelloWorld(){
ApplicationContext ctx = new ClassPathXmlApplicationContext("com/lizhenhua/AOP/aop/applicationContext.xml");
IHelloWorldService helloWorldService = ctx.getBean("helloWorldService", IHelloWorldService.class);
helloWorldService.sayHello();
}
}
在测试时,如果未输出结果before和after方法。那么就是execution(* com.lizhenhua.AOP.service...*.*(..))
中注意“*”号后面有一个空格,没有输入空格,就调用不到配置文件的前置通知和后置通知。
======before advice
=========Hello World!
========after finally advice
在Spring配置文件中,所以AOP必须放在aop:config标签下,该标签下可以有
aop:pointct、aop:advisor、aop:aspect标签,配置顺序不可变
<aop:config>
<aop:pointcut/>
<aop:aspect>
<aop:after-returning/>
<aop:after-throwing>
<aop:after/>
<aop:around/>
<aop:declare-parents/>
aop:aspect>
aop:config>
切面就是包含切入点和通知的对象,在Spring容器中将被定义为一个Bean,Schema方式的切面需要一个切面支持Bean,该支持Bean的字段和方法提供给了切面的状态和行为信息,并通过配置方式来指定切入点和通知实现。
切面使用标签指定,ref属性用来引用切面支持Bean
<aop:aspect ref="aspectBean">
<aop:after-returning/>
<aop:after-throwing>
<aop:after/>
<aop:around/>
<aop:declare-parents/>
aop:aspect>
切入点在Spring中也是一个Bean,Bean定义方式可以有三种方式
1.在<aop:config>标签下使用<aop:pointcut>声明一个切入点Bean。该定义支持共享。id属性指定Bean名字,id的使用场合:在通知定义时使用poincut-ref属性通
过该id引用切入点,expression属性指定切入点表达式
<aop:pointcut/>
2.在<aop:aspect>标签下使用<aop:pointcut>声明一个切入点Bean。该切入点可以被多个切面使用,但一般该切入点只被该切面使用,当然也可以被其他切面使用,
但最好不要那样使用,该切入点使用id属性指定Bean名字,在通知定义时使用pointcut-ref属性通过id引用切入点,expression属性指定切入点表达式
3.匿名切入点Bean。可以在声明通知时通过poincut属性指定切入点表达式,该切入点事匿名切入点,只被该通知使用
前置通知:在切入点选择的方法之前执行,通过aop:aspect标签下的aop:before标签声明
"aspect1" class="com.lizhenhua.AOP.aop.HelloWorldAspect">
"helloWorldService" class="com.lizhenhua.AOP.service.impl.HelloWorldService">
ref="aspect1">
"execution(* com.lizhenhua.AOP...*.sayBefore(..)) and args(param)" method="beforeAdvice(java.lang.String)"
arg-names="param"/>
@Test
public void testHelloWorld(){
ApplicationContext ctx = new ClassPathXmlApplicationContext("com/lizhenhua/AOP/aop/aop.xml");
IHelloWorldService helloWorldService = ctx.getBean("helloWorldService", IHelloWorldService.class);
helloWorldService.sayBefore("nihao");
}
切入点匹配:在配置中使用execution(* com.lizhenhua.AOP...*.sayBefore(..)) 匹配目标方法sayBefore,且使用args(param)匹配目标方法只有一个参数且传入的参数类
型为通知实现方法中同名的参数类型。
目标方法定义:使用method="beforeAdvice(java.lang.String)"指定前置通知实现方法,且该通知有一个参数类型为java.lang.String参数;
目标方法参数命名: 其中使用arg-names="param"指定通知实现方法参数名为param,切入点钟使用args(param)匹配的目标方法参数将自动传递给通知实现方法同名
参数。