一、AOP的概念
1、什么是面向切面编程呢?听起来很抽象,举个例子:假设我们要执行一个核心交易,但是我想打印核心交易前和交易后的日志,最简单的办法就是在核心交易的程序的前面和后面分别加上打印日志的程序。是的,很正确,但是我如果有成千上个这样的程序都要打印日志呢?如果你在每个核心的程序里面都加上打印日志的程序,核心程序和非核心的程序相互交错,不利于程序的维护,费时费力。
So,此时我们的面向切面编程就要横空出世了。面向切面编程可以让核心程序专心执行核心程序,不用分心做其它的事情。而把打印日志这种事情当做一个切面,由切面程序去执行。你看,核心程序执行核心的,切面程序执行切面程序,两者互不干扰,最后一起输出结果,岂不妙哉(实质切面程序是有代理关系的,暂且不管)。如果还想在核心程序上面加上其它的一些动作,都可以把这些划分为一个一个的切面。
2、AOP还有两个比较抽象的概念:通知、连接点和切点。
通知主要包括5种通知:
- @Before: 前置通知, 在方法执行之前执行
- @After: 后置通知, 在方法执行之后执行 。后置通知是在连接点完成之后执行的, 即连接点返回结果或者抛出异常的时候
- @AfterRunning: 返回通知, 在方法返回结果之后执行
- @AfterThrowing: 异常通知, 在方法抛出异常之后
- @Around: 环绕通知, 围绕着方法执行
连接点就是在什么位置把核心程序和切面程序连接起来,切点就是告诉去寻找连接点。有点抽象,下面通过代码来演示。
二、代码实例
本次实例核心人物是计算两个数字的加减乘除,但是我要在计算的前后分别打印日志,把打印日志放在切面程序里面。本实例采用spring4.0版本,在新建立一个工程时,需要导入一下几个jar包:
com.springsource.net.sf.cglib-2.2.0.jar
com.springsource.org.aopalliance-1.0.0.jar
com.springsource.org.aspectj.weaver-1.6.8.RELEASE.jar
commons-logging-1.1.1.jar
spring-aop-4.0.0.RELEASE.jar
spring-aspects-4.0.0.RELEASE.jar
spring-beans-4.0.0.RELEASE.jar
spring-context-4.0.0.RELEASE.jar
spring-core-4.0.0.RELEASE.jar
spring-expression-4.0.0.RELEASE.jar
1、建立一个接口ArithmeticCalculator ,
该接口定义了两个数字的加减乘除法
package lzj.com.spring.aop;
//服务接口
public interface ArithmeticCalculator {
int add(int i, int j);
int sub(int i, int j);
int mul(int i, int j);
int div(int i, int j);
}
2、创建该接口的实现类
package lzj.com.spring.aop;
import org.springframework.stereotype.Component;
@Component("arithmeticCalculator")
public class ArithmeticCalculatorIml implements ArithmeticCalculator {
/*其中,add、sub、mul、div分别为连接点*/
@Override
public int add(int i, int j) {
int result = i + j;
System.out.println("add->result:" + result);
return result;
}
@Override
public int sub(int i, int j) {
int result = i - j;
System.out.println("sub->result:" + result);
return result;
}
@Override
public int mul(int i, int j) {
int result = i * j;
System.out.println("mul->result:" + result);
return result;
}
@Override
public int div(int i, int j) {
int result = i / j;
System.out.println("div->result:" + result);
return result;
}
}
3、创建切面程序
该切面程序主要负责打印加减程序前后的日志
package lzj.com.spring.aop;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.stereotype.Component;
@Aspect
@Component
public class LogProxy {
//表示是在ArithmeticCalculator接口实现类的add方法前面执行beforMethod方法
//before中的内容为切点,即条件
@Before("execution(public int lzj.com.spring.aop.ArithmeticCalculator.add(int, int))")
public void beforMethod(){
System.out.println("在调用前……");
}
//表示是在ArithmeticCalculator接口实现类的add方法前面执行afterMethod方法
//after中的内容为切点,即条件
@After("execution(public int lzj.com.spring.aop.ArithmeticCalculator.add(int, int))")
public void afterMethod(){
System.out.println("在调用之后……");
}
}
4、创建配置文件bean-aop.xml,内容为:
<context:component-scan base-package="lzj.com.spring.aop"></context:component-scan>
<aop:aspectj-autoproxy></aop:aspectj-autoproxy>
5、创建测试类
package lzj.com.spring.aop;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
public class Main {
public static void main(String[] args) {
ApplicationContext ctx = new ClassPathXmlApplicationContext("bean-aop.xml");
ArithmeticCalculator arithmetic = (ArithmeticCalculator) ctx.getBean("arithmeticCalculator");
arithmetic.add(3, 2);
arithmetic.sub(3, 2);
arithmetic.mul(3, 2);
arithmetic.div(4, 2);
}
}
输出结果为:
add->result:5
在调用之后……
sub->result:1
mul->result:6
div->result:2
在执行int resultAdd = arithmetic.add(3, 2);这句代码后,分别打印了“在调用前……”和“在调用之后……”。
总结:你看,是不是减少了核心程序和日志程序之间的耦合性,你走你的阳关道,我走我的独木桥,但两者却同时到达了目的地。本次试验只演示了通知中的Before和After,关于后面的三种,有兴趣的自己演示即可,同上面的方法一模一样。
在上面示例中,LogProxy类即为切面类,里面定义了切面方法,切面方法为通知,即为完成目标方法时要通知的切面方法。目标方法在本例中为ArithmeticCalculatorIml类对应的对象,连接点为对象中的add、sub、mul、div方法。切点为@Before和@After中的判断条件。一个切点可能匹配多个连接点。
三、一个切点可能匹配多个连接点
在上面的实例中,只有在调用add方法前后调用了beforMethod和afterMethod方法,因为在@Before和@After的注解条件中,只能根据条件切点找到add这个方法的连接点,因此sub、mul、div这三个连接点没有对应的切点,那么如何实现让一个切点对应多个连接点呢?
只需要把切面类LogProxy 稍微改成如下:
@Aspect
@Component
public class LogProxy {
/*用*匹配了在ArithmeticCalculator类中任何含有两个int参数类型的方法,该类中任何满足该条件的方法调用前都会调用beforMethod方法*/
@Before("execution(public int lzj.com.spring.aop.ArithmeticCalculator.*(int, int))")
public void beforMethod(){
System.out.println("在调用前……");
}
/*用*匹配了在ArithmeticCalculator类中任何含有两个int参数类型的方法,该类中任何满足该条件的方法调用后都会调用afterMethod方法*/
@After("execution(public int lzj.com.spring.aop.ArithmeticCalculator.*(int, int))")
public void afterMethod(){
System.out.println("在调用之后……");
}
}
运行测试类,输出结果如下:
在调用前……
add->result:5
在调用之后……
在调用前……
sub->result:1
在调用之后……
在调用前……
mul->result:6
在调用之后……
在调用前……
div->result:2
在调用之后……
从结果中可以看出在add、sub、mul、div方法执行前后都分别调用了beforMethod和afterMethod方法。一个切点对应多个切点。
另外,若要一个切点对应多个连接点时,比较常用以下的匹配方法:
- execution * com.atguigu.spring.ArithmeticCalculator.(..): 匹配 ArithmeticCalculator 中声明的所有方法,第一个 代表任意修饰符及任意返回值. 第二个 * 代表任意方法. .. 匹配任意数量的参数. 若目标类与接口与该切面在同一个包中, 可以省略包名.
- execution public * ArithmeticCalculator.*(..): 匹配 ArithmeticCalculator 接口的所有公有方法.
- execution public double ArithmeticCalculator.*(..): 匹配 ArithmeticCalculator 中返回 double 类型数值的方法
- execution public double ArithmeticCalculator.*(double, ..): 匹配第一个参数为 double 类型的方法, .. 匹配任意数量任意类型的参数
- execution public double ArithmeticCalculator.*(double, double): 匹配参数类型为 double, double 类型的方法.
四、在切面方法中通过切点可以获取连接点的一些方法或属性
把切面类中切面方法beforMethod改为如下形式:
@Before("execution(public int lzj.com.spring.aop.ArithmeticCalculator.*(int, int))")
public void beforMethod(JoinPoint point){
String methodName = point.getSignature().getName();
List<Object> args = Arrays.asList(point.getArgs());
System.out.println("连接点方法为:" + methodName + ",参数为:" + args);
System.out.println("在调用前……");
}
然后运行测试类,输出结果如下:
连接点方法为:add,参数为:[3, 2]
在调用前……
add->result:5
在调用之后……
连接点方法为:sub,参数为:[3, 2]
在调用前……
sub->result:1
在调用之后……
连接点方法为:mul,参数为:[3, 2]
在调用前……
mul->result:6
在调用之后……
连接点方法为:div,参数为:[4, 2]
在调用前……
div->result:2
在调用之后……
从输出结果可以看出,每次调用beforMethod方法的时候都打印出出了对应的连接点方法和连接点方法的参数。