AOP面向切面编程
1. AOP
• AOP (Aspect Orient Programming) 面向切面编程是从动态角度考虑程序的运行过程
• AOP底层,就是采用动态代理模式实现的,采用了两种代理,JDK的动态代理和CGLib的动态代理
• 实际上AOP就是动态代理的一种规范化,因为动态代理种类繁多,掌握较难,所以就规范了一套统一的方式,这就是AOP
• AOP把动态代理的实现步骤,方式都定义好了,让开发人员用一种统一的方式,去用动态代理。
1.1 动态代理实现方式
JDK:
• jdk动态代理,要求jdk中有Proxy,Method,InvocationHandler创建代理对象
• jdk代理要求目标类必须实现接口
CGLib:
• 第三方的工具库,创建代理对象,原理是继承,通过继承目标类创建子类,子类就是代理对象
• CGLib要求目标类不能是final的 方法也不能是final的
1.2 动态代理的作用:
1. 可以在目标类源代码不改变的情况下去增加功能
2. 减少重复代码
3. 专注业务逻辑代码
4. 解耦合,让你的业务功能和日志,事务非业务功能分离
1.3 理解AOP面向切面编程
AOP Aspect:切面 给你目标类增加的功能就是切面,就比如要添加的日志 他就属于切面
切面的特点:一般都是非业务方法,可以独立使用
AOP Orient:面向
AOP Programming:编程
1. 需要在分析项目功能时,找出切面。(并不是所有功能都能当切面)
2. 合理的安排切面的执行时间(在目标方法前面,还是在目标方法后面)
3. 合理的安排切面执行的位置,在哪个类,在哪个方法增加增强功能
1.4 什么时候使用AOP技术
• 当你要给一个项目存在的类修改功能,但是原有的类的功能不完善,并且没有源代码,就可以使用aop增加功能
• 给项目中的多个类,增加一个相同的功能,使用aop
• 给业务方法增加是事务,日志输出
1.5 面向切面编程的术语
• Aspect:切面,表示增强的功能,就是一堆代码,完成某一个功能,非业务功能,
常见的切面功能:日志,事务,统计信息,参数检查,权限验证
• JoinPoint:连接点 连接业务方法和切面的位置,其实就是类中的业务方法
• Pointcut:切入点 指多个连接点方法的集合
• 目标对象:给哪个类的方法添加功能,这个类就是目标对象
• Advice:通知, 通知表示切面功能执行的时间,(在方法之前还是在方法之后)
1.6 一个切面有三个关键的要素:
1. 切面的功能代码,Aspect 切面要干什么
2. 切面的执行位置,Pointcut 也就是在那个方法里加功能
3. 切面执行的时间,Advice 也就是在方法前还是后
2. Aspect/切入点表达式
2.1 AOP
AOP是一个规范,是动态的一个规范化,一个标准
2.2 AOP的技术实现框架:
Spring:
spring在内部中实现了AOP规范,能做AOP的工作
spring主要在事务处理时使用AOP
我们项目开发中很少使用spring的AOP实现,因为springAOP比较笨重。
AspectJ:
一个开源的专门做AOP的框架,是业内最专业的AOP框架,又精又厉害
spring中已经集成了AspectJ的框架,所以通过spring就可以直接使用AspectJ的功能了
2.3 Aspect实现AOP有两种方式
1. 使用xml配置文件:配置全局事务
2. 使用注解,我们在项目中要做AOP功能,一般都是用注解,aspectJ有5个注解【表示切面的执行时间】
2.4 AspectJ框架的使用
切面的执行时间,这个执行时间在规范中,他叫做通知/增强 Advice
在AspectJ中使用注解来表示,当然也可以使用xml文件中的标签
• @Before
• @AfterReturning
• @Around
• @AfterThrowing
• @After
2.5 切入点表达式
表示切面执行的位置使用的是 切入点表达式
execution(modifiers-pattern? ret-type-pattren
declaring-type-pattern?name-pattern(param-pattern)
throws-pattern?)
execution:关键字
小括号开始 小括号结束
第一个参数:方法的访问修饰符【modifiers-pattern】 后面问号表示可选,可以不填
然后一个空格
第二个参数:方法返回值的数据类型【ret-type-pattren】
第三个参数:方法所在的包名类名 ?【declaring-type-pattern】
第四个参数:方法名【name-pattern】
(param-pattern)方法的参数,只填写类型就行,形参名不需要
第五个:方法所抛出的异常
execution(方法的访问修饰符? 方法的返回值类型 方法所在的包名类名? 方法名(方法的参数) 方法所抛出的异常?)
? 表示可选的部分。
以上表达式最重要的四个部分
Execution(访问权限 方法返回值 方法声明(参数) 异常类型)
在parttern的地方表示都可以使用通配符
2.6通配符
符号 | 意义 |
---|---|
* | 0至多个任意字符 |
.. | 用在方法参数中,表示任意多个参数用在包名后,表示当前包及其子包路径 |
+ | 用在类名后面,表示当前类及其子类 用在接口后,表示当前接口及实现类 |
例如:
• execution(public * *(..)) 指定切入点为:任意功能方法
• execution(* set*(..)) 指定切入点为:任意一个以 set 开始的方法
• execution(* com.xyz.service.*.*(..)) 切入点:service包中的所有类 的所有方法参数任意(子包中不算)
• execution(* com.xyz.service..*.*(..)) 切入点:定义在service或者子包中任意类的任意方法
• ..出现在类名中时,后面必须跟”*“表示包,子包下的所有类
• execution(* *..service.*.*(..)) + 指定所有包下的service子包下所有类(接口)中所有方法为切入点
• execution(* *.service.*.*(..)) 指定只有以及包下的service子包下所有类(接口)中所以后方法为切入点
3. AspectJ实现步骤(注解方式)
使用AspectJ框架实现AOP(给一些已经存在的类和方法,增加额外的功能【前提是不改变原来类的代码】)
1. 先创建一个maven功能
• 加入spring依赖
• 加入aspectJ依赖
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aspects</artifactId>
<version>5.2.5.RELEASE</version>
</dependency>
2. 创建目标类:必须有接口和他的实现类(底层目前是JDK动态代理)
给类中的方法增加功能
//目标类
@Component
public class SomeServiceImpl implements SomeService{
@Override
public String doSome(String s) {
//给doSome方法增加功能,在方法执行之前 输出方法的执行时间
System.out.println("SomeServiceImpl的doSome()方法执行了");
System.out.println(s);
return s;
}
}
3. 创建切面类:就是一个普通类
a. 在类的上面添加注解 @Aspect
b. 在类中定义方法,方法就是切面要执行的功能代码
c. 在方法的上面加入aspectJ中的通知注解,例如@Before
d. 指定切入点表达式execution() execution(public void com.yixuexi.ba01.SomeServiceImpl.doSome(..))
/*
* 注解作用:AspectJ框架中的注解,表示当前类是切面类
* 切面类:是用来给业务方法增加功能的类,在这个类中有很多的功能代码
* 使用位置:类的上面
* */
@Component
@Aspect
public class MyAspect {
/*在类里面定义方法,方法是实现切面功能的
* 方法的定义要求
* 1.公共方法
* 2.方法没有返回值
* 3.方法名称自定义
* 4.方法可以有参数,也可以没有参数
* 如果有参数,参数不是自定义的,有几个参数类型可以使用...
*
* */
/*
* @Before:前置通知注解
* 属性:value 是切入点表达式,表示切面的功能执行的位置
* 位置:在方法的上面添加注解@Before
* 特点:
* 1.在目标方法之前先执行
* 2.不会改变目标方法的执行结果
* 3.不会影响目标方法的执行
* */
@Before(value = "execution(public * *(..))")
public void myBefore(JoinPoint joinPoint){
/* System.out.println(joinPoint.getSignature().getDeclaringTypeName());
System.out.println(joinPoint.getSignature());
for (Object arg : joinPoint.getArgs()) {
System.out.println(arg);
}*/
//切面要执行的功能代码
System.out.println("前置通知,切面功能:在目标方法执行之前输出时间" + new Date());
}
/*保证returning的值和方法的形参名一致就行*/
@AfterReturning(value = "execution(* *(..))", returning="res")
public void doOther(Object res){
res += "Hello world";
System.out.println(res);
System.out.println("后置通知执行");
}
}
4. 创建spring的配置文件,在文件中声明对象,把对象交给容器统一管理(IOC管理)
• 声明对象可以使用xml或者注解的方式
• 声明目标对象(使用注解也可以)
• <bean id="someService" class="com.yixuexi.ba01.SomeServiceImpl"></bean>
• 声明切面类对象(使用注解也可以)
• <bean id="myAspect" class="com.yixuexi.ba01.MyAspect"></bean>
• 声明AspectJ框架中的自动代理生成器标签(自动代理生成器:完成代理对象自动创建功能)
<aop:aspectj-autoproxy/>
<!--声明自动代理生成器,使用aspectJ框架内部的功能,创建目标对象的代理对象
创建代理对象是在内存中实现的,修改目标对象的内存中的结构。创建为代理对象
所以目标对象就是被修改后的代理对象
aop:aspectj-autoproxy:会把spring容器中所有的目标对象一次行都生成代理对象
-->
5. 创建测试类,从spring容器中获取目标对象(实际就是代理对象)
a. 通过代理执行方法,实现aop的功能增强
b. 这个返回的就不是哪个类的对象了,而是他的接口的实现类
@org.junit.Test
public void test(){
String path = "application-context.xml";
ApplicationContext ac = new ClassPathXmlApplicationContext(path);
//从容器中获取目标对象,获取的目标对象不再是原来的类型 而是它的接口类型
//实际上是 获取的就是代理对象
String applicationName = ac.getApplicationName();
System.out.println(applicationName);
SomeService someServiceImpl = (SomeService) ac.getBean("someService");
//通过代理对象执行方法,完成在调用方法时增强功能
someServiceImpl.doSome("我是你爸爸");
}
4. 前置/后置/环绕通知注解
表示切面的执行时间使用的是通知注解
4.1 前置通知注解 @Before
• 在切面类的切面方法上使用@Before 表示在目标方法执行前执行(表示执行的时间,是在前还是在后)
• 属性:value是切入点表达式,表示切面的功能执行的位置
• 位置:在方法的上面添加注解@Before
• 特点:
1.在目标方法之前先执行
2.不会改变目标方法的执行结果
3.不会影响目标方法的执行
• 前置通知方法的定义要求
1.公共方法
2.方法没有返回值
3.方法名称自定义
4.方法可以有参数,也可以没有参数
如果有参数,参数不是自定义的,有几个参数类型可以使用...
4.2 后置通知 @AfterReturning
@AfterReturning
属性:
1. value 切入点表达式
2. Returning 自定义变量,表示目标方法的返回值
自定义的变量名必须和通知方法的形参名一样
使用位置:在方法定义的上面
特点:
1. 在目标方法执行之后执行
2. 能够获取到目标方法的返回值,根据返回值做不同的处理功能
Object res = 调用目标类的目标方法返回值
3. 可以修改返回值
• 如果是引用传递 那么可以修改值,也就是说传递的是一个对象的话 返回值是可以在通知方法中修改的
因为传递的是内存中的同一个对象,通知方法中修改的也是同一个对象
• 如果是值传递,也就是说传递的是字符串或者是数字的话 那么返回值是不能在通知方法里修改的
后置通知方法
1. 公共方法
2. 方法没有返回值
3. 方法名称自定义
4. 方法有参数的 推荐使用Object
/*保证returning的值和方法的形参名一致就行*/
@AfterReturning(value = "execution(* *(..))", returning="res")
public void myAfterReturning(Object res){ 一致
//res就是目标方法的返回值
System.out.println("后置通知执行");
}
后置通知的执行 res相当于:
Object res = doOther(); // 目标方法
myAfterReturning(res); // 在把目标方法的返回值 传递给通知方法
@After和@AfterReturning的区别
@After通知是最终通知 也就是说就算抛出了异常 @After通知也是可以执行的,有点向finally
但是@AfterReturning不是 一但抛出异常@AfterReturning不执行
4.3 指定通知方法的形参
(被通知注解修饰的方法叫通知方法)
[所有的通知方法都能使用 必须是形参的第一位]
JoinPoint:代表的是业务方法 也就是@Before(“execution(public void doSome(..))”)里的doSome方法
要加入切面功能的业务方法
作用是:在通知方法中获取方法执行时的信息,例如方法名称,方法的实际参数
如果你的切面功能中需要用到方法的信息,就加入JoinPoint
使用要求:
JionPoint的值是框架自动赋予的,必须是第一个位置的参数
• 获取方法的形参 joinPoint.getArgs() //返回一个String数组 ,遍历即可
• 获取方法的定义joinPoint.getSignature()
5. 环绕/异常/最终通知注解
5.1 @Around环绕通知
经常用来做 事务 在目标方法之前开启事务,目标方法之后结束事务
环绕通知方法的定义和格式
1. public方法
2. 必须有返回值,推荐使用Object
3. 方法名称自定义
4. 方法有参数,固定参数 ProceedingJoinPoint
@Around("execution(public void doSome())")
public Object myAspect(ProceedingJoinPoint pjp) throws Throwable {
return null;
}
@Around
1. 属性:value 切入点表达式
2. 位置:在方法的定义上面
3. 特点:
a. 功能最强的通知
b. 可以在目标方法的前/后执行。
• 在pjp.proceed(); 方法上面的代码就是前,下面就是后
c. 控制目标方法是否被调用执行
• 通过 调不调用pjp.proceed();方法来控制目标方法是否被调用
• 也可以 通过条件判断 如果xxx就执行 如果xxx不执行
• ProceedingJoinPoint是接口 继承JoinPoint所以可以通过 他的getArgs()获取到参数然后进行判断
d. 修改原来的目标方法的执行结果,影响最后的调用结果
• 最后的返回结果是环绕通知的return 决定的
4. 跟JDK的动态代理一样
参数ProceedingJoinPoint :
• 用来执行目标方法的 //Object obj = pjp.proceed(); 执行目标方法 接收返回值
返回值:
• 就是目标方法的执行结果,可以被修改
• [在测试类中写的 调用 doOther() 其实内部调用的是 myAspect()方法,这就是动态代理]
• 调用目标方法 == 调用myAround()
在pjp.proceed()上面的代码 是方法执行之前执行,下面是执行之后执行
5.2 @AfterThrowing 异常通知(了解即可)
在程序抛出异常是被触发调用
异常通知方法定义:
1. public
2. 没有返回值
3. 方法名字自定义
4. 方法可以有参数,如果有就是JoinPoint / Exception
public void myAfterThrowing(Exception e){
}
@AfterThrowing
属性:
• value 切入点表达式
• throwing 自定义的变量,表示目标方法抛出的异常对象
• throwing=“xx” xx必须和 Exception xx 一致
特点:
• 在目标方法抛出异常时执行的
• 可以做异常的监控程序,监控目标方法执行时是不是有异常,如果有异常可以发送邮件或者短信
执行原理就是:在底层其实是有一个try{}catch(){}的 如果遇到了异常就进入到异常 然后调用你的异常通知方法
5.3 @After最终通知
最终通知方法定义
1. public
2. 没有返回值
3. 名称自定义
4. 没参数,如果有JoinPoint
属性:value 切入点表达式
使用位置:方法的上面
特点:
1. 总是会执行(就算抛出了异常也是会执行的) 类似于finally
2. 在目标方法之后执行
作用:一般是用来做内存清除的
5.3 @Pointcut注解
• 当有很多的切入点表达式的时候,就比较难管理,这时就可以使用过@Pointcut注解
• 使用场景:用很多通知 都需要使用同一个切入点表达式。
• 属性: value 切入点表达式
• 位置:在自定义方法上面
• 特点:
• 当使用了Pointcut定义在一个方法的上面,此时这个方法的名称就是切入点表达式的别名。
• 其他的通知中,value的属性就可以使用这个方法的名称来代替切入点表达式了
5.4 代理的使用方法
1. 如果目标类有接口,框架使用jdk动态代理
2. 如果目标类没有接口,默认使用的cglib动态代理
3. 有接口也可以强制使用cglib动态代理
<aop:aspectj-autoproxy proxy-target-class="true"/>
<dependency>
<groupId>cglib</groupId>
<artifactId>cglib</artifactId>
<version>2.2.2</version>
</dependency>