什么是AOP
AOP 为 Aspect Oriented Programming 的缩写,意为:面向切面编程,可通过运行期动态代理实现程序功能的统一维护的一种技术。AOP 是 Spring 框架中的一个重要内容。利用 AOP可以对业务逻辑的各个部分进行隔离,从而使得业务逻辑各部分之间的耦合度降低,提高程序的可重用性,同时提高了开发的效率。AOP 的底层就是采用动态代理模式实现的。采用了两种代理:JDK 的动态代理,CGLIB的动态代理。
AOP编程术语
切面(Aspect)
切面泛指交叉业务逻辑。事务处理、日志处理就可以理解为切面。常用的切面是通知(Advice)。实际就是对主业务逻辑的一种增强。
连接点(JoinPoint)
连接点指可以被切面植入的具体方法。通常业务接口中的方法均为连接点。
切入点(Pointcut)
切入点指声明的一个或多个连接点的集合。通过切入点指定一组方法。被final修饰的方法是不能作为连接点与切入点的,因为最终的是不能被修改的,不能被增强的。
目标对象(Target)
目标对象指将要被增强的对象。即包含主业务逻辑的类的对象。
通知(Advice)
通知表示切面的执行时间,Advice也叫增强。通知定义了增强代码切入到目标代码的时间点,是目标方法执行之前还是执行之后。通知类型不同,切入时间不同。
AspectJ对AOP的实现
对于 AOP 这种编程思想,很多框架都进行了实现。Spring 就是其中之一,可以完成面向切面编程。然而,AspectJ 也实现了 AOP 的功能,且其实现方式更为简捷,使用更为方便,而且还支持注解式开发。所以,Spring 又将 AspectJ 的对于 AOP 的实现也引入到了自己的框架中。在 Spring 中使用 AOP 开发时,一般使用 AspectJ 的实现方式。
AspectJ的通知类型
AspectJ中常用的通知有五种类型:
(1)前置通知
(2)后置通知
(3)环绕通知
(4)异常通知
(5)最终通知
AspectJ的切入点表达式
AspectJ 定义了专门的表达式用于指定切入点。表达式的原型是:
execution(modifiers-pattern? ret-type-pattern declaring-type-pattern?name-pattern(param-pattern) throws-pattern?)
其中:
modifiers-pattern 访问权限类型
ret-type-pattern 返回值类型
declaring-type-pattern 包名类名
name-pattern(param-pattern) 方法名(参数类型和参数个数)
throws-pattern 抛出异常类型
?表示可选的部分
在表达式中可以使用符号来代替一些字符:
星号(*):表示0至多个任意字符
两点(. .):用在方法参数中,表示任意多个参数;用在包名后,表示当前包及其子包路径
例如:
execution(public * (…))
指定切入点为:任意公共方法
execution( set*(…))
指定切入点为:任何一个以“set”开始的方法。
execution(* com.xyz.service..(…))
指定切入点为:定义在 service 包里的任意类的任意方法。
AspectJ的开发环境
(1)maven依赖
使用AspectJ需要加入maven依赖
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aspects</artifactId>
<version>5.2.5.RELEASE</version>
</dependency>
(2)引入AOP约束
在 AspectJ 实现 AOP 时,要引入 AOP 的约束。配置文件中使用的 AOP 约束中的标签,均是 AspectJ 框架使用的,而非 Spring 框架本身在实现 AOP 时使用的。AspectJ 对于 AOP 的实现有注解和配置文件两种方式,常用是注解方式。
AspectJ基于注解的AOP实现
定义目标类
public interface SomeService {
void doSome(String name, Integer age);
String doOther(String name, Integer age);
String doFirst(String name, Integer age);
}
public class SomeServiceImpl implements SomeService {
@Override
public void doSome(String name, Integer age) {
System.out.println("目标类的doSome()方法执行");
}
@Override
public String doOther(String name, Integer age) {
System.out.println("目标类的doOther()方法执行");
return "1234";
}
@Override
public String doFirst(String name, Integer age) {
System.out.println("目标类的doFirst()方法执行");
return "doFirst";
}
@Aspect 切面类注解
该注解表示当前类是切面类,出现在类定义的上面。
@Before 前置通知注解
前置通知注解用来表示前置通知方法。该注解有一个value属性,属性值就是切入点表达式,表示切面功能执行的位置。
前置通知方法是用来实现切面功能的,该方法可以没有参数,但是如果有参数的话,参数必须有JoinPoint,且必须在参数列表的第一位。该方法会在目标方法执行之前先执行。
JoinPoint指需要加入切面功能的那个目标方法,即切入点表达式指向的那个方法。他的作用是可以获取到目标方法执行时的信息,例如方法的名称、实参等。
切面类:
@Aspect
public class MyAspect {
@Before(value = "execution(* *..SomeServiceImpl.doSome(..))")
public void myBefore(JoinPoint jp){
//获取方法的完整定义
System.out.println("方法的签名(定义)=" + jp.getSignature());
//获取方法的名称
System.out.println("方法的名称=" + jp.getSignature().getName());
//获取方法的实参
Object args[] = jp.getArgs();
for(Object obj : args){
System.out.println("参数=" + obj);
}
System.out.println("前置通知,切面功能:在目标方法执行前输出执行时的时间" + new Date());
}
}
测试类:
public class MyTest01 {
@Test
public void test01(){
String config = "applicationContext.xml";
ApplicationContext ctx = new ClassPathXmlApplicationContext(config);
//从容器中获取目标对象,这里得到的其实是代理对象
SomeService proxy = (SomeService) ctx.getBean("someService");
//将得到的代理对象进行输出
System.out.println("proxy:" + proxy.getClass().getName());
//通过代理对象执行目标方法,实现方法执行时的功能增强
proxy.doSome("zhangsan", 30);
}
}
测试结果:
@AfterReturning 后置通知注解
后置通知注解用来表示后置通知方法。该注解有两个属性,一个是value属性,属性值是切入点表达式(这点与@Before相同);另一个是returning属性,属性值是一个自定义的变量,表示目标方法的返回值,自定义变量名必须与后置通知方法的形参名相同。
后置通知方法与前置通知方法相同,也是用来实现切面功能的。该方法有一个参数,推荐使用Object类型的参数。该方法在目标方法执行之后再执行,可以获取到目标方法的返回值,因此可以根据返回值做一些处理。
切面类:
@Aspect
public class MyAspect {
@AfterReturning(value="execution(* *..SomeServiceImpl.doOther(..))",
returning = "res")
public void myAfterReturning(Object res){
//Object res:目标方法执行之后的返回值
System.out.println("后置通知:在目标方法执行之后执行,获取的返回值是:" + res);
//根据返回值做切面处的功能处理
if(res != null){
res = 111;
System.out.println("res=" + res);
}
}
}
测试类:
public class MyTest02 {
@Test
public void test01(){
String config = "applicationContext.xml";
ApplicationContext ctx = new ClassPathXmlApplicationContext(config);
//从容器中获取目标对象,这里得到的其实是代理对象
SomeService proxy = (SomeService) ctx.getBean("someService");
//将得到的代理对象进行输出
System.out.println("proxy:" + proxy.getClass().getName());
//通过代理对象执行目标方法,实现方法执行时的功能增强
String str = proxy.doOther("lisi", 20);
System.out.println("str=" + str);
}
}
测试结果:
从测试结果我们可以看出,虽然我们对返回值进行了更改,但是并没有影响最后调用的结果
@Around 环绕通知注解
环绕通知注解用来表示环绕通知方法。该注解只有一个value属性,属性值是切入点表达式。该注解有以下特点:
(1)是功能最强的通知
(2)在目标方法执行前后都能增强功能
(3)可以控制目标方法是否执行
(4)可以修改原来目标方法的执行结果,并影响最后的调用结果
环绕通知方法要求必须有返回值,推荐使用Object类型。并且该方法可以包含一个ProceedingJoinPoint类型的参数,该参数就等同于jdk动态代理中invoke()方法中的Method,作用是执行目标方法。
切面类:
@Aspect
public class MyAspect {
@Around(value = "execution(* *..SomeServiceImpl.doFirst(..))")
public Object myAround(ProceedingJoinPoint pjp) throws Throwable {
Object res = null;
//通过ProceedingJoinPoint得到目标方法的参数
String name = null;
Object args[] = pjp.getArgs();
if(args != null && args.length > 1){
Object arg = args[0];
name = (String) arg;
}
//在目标方法之前增强输出时间功能
System.out.println("环绕通知:在目标方法之前输出执行时间=" + new Date());
//通过目标方法的参数,控制目标方法是否执行
if("zhangsan".equals(name)){
//调用执行目标方法
res = pjp.proceed();//等同于jdk动态代理中的method.invoke();Object res = doFirst();
}
//在目标方法之后增强提交事务功能
System.out.println("环绕通知:在目标方法之后提交事务,res=" + res);
//修改原来的目标方法的执行结果,影响最后的调用结果
if(res != null){
res = "aaaaa";
System.out.println("res=" + res);
}
return res;
}
}
测试类:
public class MyTest03 {
@Test
public void test01(){
String config = "applicationContext.xml";
ApplicationContext ctx = new ClassPathXmlApplicationContext(config);
//从容器中获取目标对象,这里得到的其实是代理对象
SomeService proxy = (SomeService) ctx.getBean("someService");
//将得到的代理对象进行输出
System.out.println("proxy:" + proxy.getClass().getName());
//通过代理对象执行目标方法,实现方法执行时的功能增强
String str = proxy.doFirst("zhangsan", 20);//实际上执行的是myAround()
System.out.println("str=" + str);
}
}
测试结果:
从测试结果我们可以看出,我们在切面类中对返回值进行了修改,最终影响了最后的调用结果
Pointcut 定义切入点的注解
当较多的通知增强方法使用相同的 execution 切入点表达式时,编写、维护均较为麻烦。AspectJ 提供了@Pointcut 注解,用于定义 execution 切入点表达式。其用法是,将@Pointcut 注解在一个方法之上,以后所有的 execution 的 value 属性值均可使用该方法名作为切入点。代表的就是@Pointcut 定义的切入点。这个使用@Pointcut 注解的方法一般使用 private 的标识方法,即没有实际作用的方法
@Before(value = "mypt()")
public void myAfter(){
System.out.println("前置通知");
}
@Pointcut(value = "execution(* *..SomeServiceImpl.doSome(..))")
private void mypt(){
//无需代码
}