AOP 概述
AOP(Aspect Orient Programming),面向切面编程。面向切面编程是从动态的角度考虑程序的运行过程。
这是 Spring 框架中的一个重要的内容,利用 AOP 可以对业务逻辑的各个部分进行隔离,从而使得业务逻辑之间的耦合度降低,提高程序的复用性,同时提高了开发效率。
AOP 底层,就是采用了动态代理的模式来实现的。 其中有两种代理:JDK 的动态代理、CGLIB 的动态代理
面向切面编程,就是将交叉业务逻辑封装成切面,利用 AOP 容器的功能将切面植入到主业务逻辑中。所谓的“交叉业务”,就是指通用的、与主业务逻辑无关的代码。例如:日志信息、安全检查、事务、缓存、设置字符编码、发短信 等。
若不使用 AOP 。则会出现代码纠缠不清。不重要的业务功能和重要的业务功能代码混杂咋一起,使得整个程序的结构混乱不清。
例如,转账功能。在真正转账业务逻辑前后,需要权限控制、日志记录、加载事务、结束事务等交叉业务逻辑。 而这些业务逻辑和主要的业务逻辑之间并没有直接的关系。但是他们的代码量能达到总代码量的一半甚至更多! 他们的存在,不仅产生了大量“冗余”的代码,还大大干扰了主业务逻辑的结构——转账
AOP 有什么好处?
- 减少重复
- 专注业务
注意:AOP 这是面向对象编程的一种补充
不使用 AOP 的开发方式 (实例)
(待补充)
AOP 术语 (掌握)
切面(Aspect)
切面泛指交叉业务逻辑。上述的事务处理、日志功能等,就可以理解为是切面,常用的切面是通知(Advice)(也可以理解为切入到目标代码的时间点)。
实际上就是对主业务逻辑的一种增强
连接点(JoinPoint)
连接点指可以被切面植入的具体方法,通常业务接口中的方法均为连接点
切入点(Pointcut)
切入点指的是 声明的一个或者多个连接点的集合。通过切入点指定的 一组方法 。
注意: 被标记为 final 的方法是不能作为连接点与切入点的,因为 final 是不能被修改、不能被增强的、
目标对象(Target)
目标对象指的是要被增强的对象,即包含主业务逻辑的类的对象。例如 StudentServiceImpl 的对象若被增强,则该类被称为目标类,该对象被称为目标对象。 若不能增强也就无所谓目标不目标的
通知(Advice)
通知,表示切面的执行时间,Advice 也叫增强。例如:MyInvocationHandler 就可以理解为是一种通知。
换个角度来说, 通知定义了增强代码切入到目标代码的时间点: 是目标方法执行前执行、还是目标方法执行后执行……
通知类型不同,切入的时间也不同。 切入点定义了切入的位置,通知定义了切入的时间
AspectJ 对 AOP 的实现 (掌握)
步骤
定义业务接口类和实现类
//接口类
public interface SomService{
void doSome(String name,int age);
}
//实现类
public class SomeServiceImpl implements SomService{
@Override
public void doSome(String name,int age){
System.out.priontln("执行了业务方法doSome");
}
}
定义切面类
//类中定义了若干普通方法,用来增强功能
//@Aspectj 是aspectj框架的注解,表示当前类是切面类
@Aspect
public class MyAspect{
/*
@Before:前置通知
属性:value,值 = 切入点表达式,表示切面的执行位置
位置:方法之上
*/
public void myBefore(){
System.out.println("前置通知:在目标方法之前执行,例如打印日志信息");
}
}
XML 文件的配置
在定义好切面 Aspect 后,要通知 Spring 容器,让容器生成 “目标类” + “切面” 的代理对象。这个代理是由容器自动生成的。只需要在 Spring 配置文件中注册一个基于 aspectj 的自动代理生成器,它就会自动扫描到 @Aspect 注解,并按照通知类型与切入点,将其植入,并生成代理
<!-- 声明目标类对象 -->
<bean id="someServiceTarget" class="com.gg.service.SomeServiceImpl" />
<!-- 声明切面类对象 -->
<bean id="myAspect" class="com.gg.aspect.MyAspect" />
<!-- 注册AspectJ的自动代理 -->
<!-- 声明自动代理生成器,创建代理 -->
<aop:aspectj-autoproxy />
aop:aspectj-autoproxy 的底层是由 AnnotationAwareAspectJAutoProxyCreator 这个类实现的。
从名字就可以看出,是基于AspectJ 的注解适配自动代理生成器
它的工作原理是,aop:aspectj-autoproxy 通过扫描找到 @Aspect 定义的切面类,再 由切面类根据切入点表达式 找到目标类的目标方法,再由通知类型找到切入的时间点。
@Before 前置通知,方法有 JoinPoint 参数
被 @Before 标记的增强方法,在目标方法执行之前执行。被注接为前置通知的方法,可以包含一个 JoinPoint 类型的参数。该类型的对象本身,就是切入点表达式。通过这个参数,可以获取切入点表达式、方法签名、目标对象等。
补充: 不光是前置通知的方法可以包含 JoinPoint 类型的参数,所有的通知方法都可以包含 JoinPoint 这个参数
/*
1、通知方法:使用了通知注解修饰的方法
2、通知方法可以有参数,但参数不是任意
3、JoinPoint:表示连接点方法
3.1、该参数只能出现在形参的第一位,其他形参可以跟在后面
3.2、任何通知的方法中都可以包含该参数
*/
@Before(value="execution(* *..SomeServiceImpl.do*(..))")
public void myBefore(JoinPoint jp){
//通过JoinPoint获取方法签名,方法定义,方法参数等
System.out.println("连接点的方法定义" + jp.getSignature);
System.out.println("连接点方法参数个数" + jp.getArgs().length);
//获取方法参数信息
Object[] args = jp.getArgs();
for(Object arg:args){
System.out.println(arg);
}
//切面代码功能,例如日志输出,事务处理
System.out.println("前置通知:输出日志");
}
@AfterReturning 后置通知 - 注解带有 returning 属性
被 @AfterReturning 注解标记的方法是后置通知,在目标方法执行之后执行。
由于是目标方法之后执行,所以可以获取到目标方法的返回值。该注解的 returning 属性就是用于指定接收方法返回值的变量名的。(此参数用于指定变量名,这个变量名就代表方法的返回值)
所以,被注解为后置通知的方法,除了可以包含 JoinPoint 参数外,还可以包含用于接收返回值参数的变量,并做出修改。 该变量最好使用 Object 类型 ,因为目标方法中的返回值可能是任何类型。
定义接口与实现类
//新增接口中的方法
public interface SomService{
void doSome(String name,int age);
String doOther(String name,int age); //新增的方法
}
//实现类
public class SomeServiceImpl implements SomService{
@Override
public String doOther(String name,int age){
System.out.priontln("执行了业务方法doOther");
return "abcd";
}
}
定义切面类中方法
@AfterReturning(value="execution(* *..SomeServiceImpl.doOther(..))",returning="result")
public void myAfterReturning(){
//修改目标方法中的执行结果
if(result != null){ //此处的result就是returning属性的参数名(自定义参数名),result代表的是:目标方法的返回值,可能是任意类型
String s = (String)result;
result = s.toUpperCase();
}
System.out.println("后置通知:在目标方法执行之的功能增强,例如事务处理。" + result);
}
@Around 环绕通知-增强法有 ProceedingJoinPoint 参数
被 @AroundProceedingJoinPoint 注解标记的增强方法, 在目标方法执行的前后均执行 。被注解为环绕通知的方法要有返回值,Object 类型。并且方法可以包含一个 ProceedingJoinPoint 类型的参数。接口 ProceedingJoinPoint 里面有一个 proceed() 方法,用于执行目标方法。
若目标方法有返回值,则该方法的返回值就是目标方法的返回值。
最后,环绕增强方法将其返回值返回,实际上就是拦截了目标方法的执行
定义接口与实现类
//新增接口中的方法
public interface SomService{
void doSome(String name,int age);
String doOther(String name,int age);
String doFirst(String name,ing age); //新增的方法
}
//实现类
public class SomeServiceImpl implements SomService{
@Override
public String doFirst(String name,int age){
System.out.priontln("执行了业务方法doFirst");
return "doFirst";
}
}
定义切面
@Around(value="execution(* *..SomeServiceImpl.doFirst(..))")
public Object myAround(ProceedingJoinPoint pjp) throws Throwable{
Object obj = null;
//增强功能
System.out.println("环绕通知:在目标方法执行之前执行,例如输出日志");
//执行目标方法的调用,等同于method.invoke(target,args)
obj = pjp.proceed();
//增强功能
System.out.println("环绕通知:在目标方法之后执行的内容。如事务");
return obj;
}
@AfterThrowing 异常通知 - 注解中有 throwing 属性(了解内容)
被 @AfterThrowing 标记的方法,在目标方法抛出异常后执行。该注解的 throwing 属性用于指定所发生的异常类对象。当然,被注解为异常通知的方法可以包含一个参数 Throwable,参数名称为 throwing 指定的名称,表示发生的异常对象。
定义接口与实现类
//新增接口中的方法
public interface SomService{
void doSome(String name,int age);
void doAfterThrowing(); //新增的方法
}
//实现类
public class SomeServiceImpl implements SomService{
@Override
public void doAfterThrowing(){
System.out.println("执行了业务方法doAfterThrowing" + 10/0);
}
}
定义切面类
@AfterThrowing(value="execution(* *..SomeServiceImpl.doAfterThrowing(..))",throwing="ex")
public void myAfterThrowing(Throwable ex){
//把异常发生的时间、位置、原因 记录到数据库,日志文件等地方
//可以在异常发生时,把异常信息通过短信、邮件发送给开发人员
System.out.println("异常通知:在目标方法抛出时异常执行,异常原因:" + ex.getMessage());
}
@After 最终通知(了解内容)
无论目标方法是否抛出异常,该增强均会被执行
定义接口与实现类
//新增接口中的方法
public interface SomService{
void doSome(String name,int age);
void doAfter(); //新增的方法
}
//实现类
public class SomeServiceImpl implements SomService{
@Override
public void doAfter(){
System.out.println("执行了业务方法doAfter" + (10/0));
}
}
定义切面类
@After(value="execution(* *..SomeService.doAfter(..))")
public void myAfter(){
System.out.println("最终通知:总是会被执行的方法");
}
@Pointcut 定义切入点
当较多的通知增强方法使用相同的 execution 切入点表达式时,编写、维护都比较麻烦。AspectJ 提供了 @Pointcut 注解,用于定义 execution 切入点表达式。
用法:将 @Pointcut 注解在一个方法之上,以后所有的 execution 的 value 属性均可使用该方法名作为切入点。代表的就是 @Pointcut 定义的切入点。
这个使用 @Pointcut 注解的方法一般使用 private 的标识方法,即,没有实际作用的方法。
@Pointcut(value="execution(* *..SomeService.doThird(..))")
private void myPointcut{
// 此工具方法内无需代码
}
@After(value="myPointcut()")
public void myAfter(){
System.out.println("最终通知:总是会被执行的方法");
}