- 一、AOP概述
- 1. AOP简介
- 2. 如何理解AOP
- 二、AOP编程术语
- 1. 切面(Aspect)
- 2. 连接点(JoinPoint)
- 3. 切入点(Pointcut)
- 4. 目标对象(Target)
- 5. 通知(Advice)
- 三、Aspectj对AOP的实现
- 1. Aspectj的通知类型
- 2. Aspectj的切入点表达式
- 3. Aspectj的开发环境
- 四、AspectJ基于注解的AOP实现
- 1. 实现步骤
- 2. @Before前置通知
- 3. @AfterReturning后置通知
- 4. @Around环绕通知
- 5. @AfterThrowing 异常通知
- 6. @After最终通知
- 7. @Pointcut定义切入点
- 五、总结
- 1. 使用aspectj框架实现aop
- 2. Review
一、AOP概述
1. AOP简介
AOP(Aspect Orient Programming),面向切面编程。面向切面编程是从动态角度考虑程序运行过程
AOP 底层,就是采用动态代理模式实现的。采用了两种代理:JDK 的动态代理,与 CGLIB的动态代理
可以看之前写的动态代理,
https://www.cnblogs.com/mengd/p/13429797.html
- jdk动态代理:使用jdk中的Proxy,Method,InvocaitonHanderl创建代理对象,jdk动态代理要求目标类必须实现接口
- cglib动态代理:第三方的工具库,创建代理对象,原理是继承。 通过继承目标类,创建子类,子类就是代理对象。 要求目标类不能是final的, 方法也不能是final的
动态代理的作用:
- 在目标类源代码不改变的情况下,增加功能
- 减少代码的重复
- 专注业务逻辑代码
- 解耦合,让你的业务功能和日志分离,事务和非业务功能分离
2. 如何理解AOP
AOP(Aspect Orient Programming)面向切面编程
- Aspect: 切面,给你的目标类增加的功能,就是切面,切面的特点: 一般都是非业务方法,独立使用的
- Orient:面向
- oop: 面向对象编程
理解:
- 需要在分析项目功能时,找出切面
- 合理的安排切面的执行时间(在目标方法前, 还是目标方法后)
- 合理的安全切面执行的位置,在哪个类,哪个方法增加增强功能
二、AOP编程术语
1. 切面(Aspect)
表示增强的功能, 就是一堆代码,完成某个一个功能,非业务功能
常见的切面功能有日志, 事务, 统计信息, 参数检查, 权限验证
2. 连接点(JoinPoint)
连接业务方法和切面的位置,就某类中的业务方法
3. 切入点(Pointcut)
指多个连接点方法的集合,多个方法
4. 目标对象(Target)
给哪个类的方法增加功能, 这个类就是目标对象
5. 通知(Advice)
通知表示切面功能执行的时间
一个切面有三个关键的要素:
- 切面的功能代码,切面是干什么
- 切面的执行位置,使用Pointcut表示切面执行的位置
- 切面的执行时间,使用Advice表示时间,在目标方法之前,还是目标方法之后
三、Aspectj对AOP的实现
aop是一个规范,是动态的一个规范化,一个标准
aop的技术实现框架:
- spring:spring在内部实现了aop规范,能做aop的工作,我们项目开发中很少使用spring的aop实现。 因为spring的aop比较笨重
- aspectJ: 一个开源的专门做aop的框架。spring框架中集成了aspectj框架,通过spring就能使用aspectj的功能
aspectJ框架实现aop有两种方式:
- 使用xml的配置文件 : 配置全局事务
- 使用注解,我们在项目中要做aop功能,一般都使用注解, aspectj有5个注解
1. Aspectj的通知类型
AspectJ 中常用的通知有五种类型
- 前置通知
- 后置通知
- 环绕通知
- 异常通知
- 最终通知
2. Aspectj的切入点表达式
以上表达式共4个部分
execution(访问权限 方法返回值 方法声明(参数) 异常类型)
切入点表达式要匹配的对象就是目标方法的方法名。所以,execution 表达式中明显就
是方法的签名。
注意,表达式中黑色文字表示可省略部分,各部分间用空格分开
在其中可以使用以下符号:
常用的几个:
execution(public * * (..))
指定切入点的位置:任意的公共方法
execution(* set*(..))
指定切入点的位置:任何一个以set开始的方法
execution(* com.xyz.service.*.*(..))
指定切入点的位置:定义在service包里的任意类的任意方法
execution(* com.xyz.service..*.*(..))
指定切入点的位置:定义在service包或者子包里的任意类的任意方法
..
出现在类名中时,后面必须跟*
,表示包、子包下的所有类
execution(* *..service.*.*(..))
指定所有包下的service子包下所有类中所有的方法为切入点
3. Aspectj的开发环境
1. maven依赖
junit
junit
4.11
test
org.springframework
spring-context
5.2.5.RELEASE
org.springframework
spring-aspects
5.2.5.RELEASE
2. 引入AOP约束
在 AspectJ 实现 AOP 时,要引入 AOP 的约束。配置文件中使用的 AOP 约束中的标签,均是 AspectJ 框架使用的,而非 Spring 框架本身在实现 AOP 时使用的
AspectJ 对于 AOP 的实现有注解和配置文件两种方式,常用是注解方式
四、AspectJ基于注解的AOP实现
1. 实现步骤
1. 定义业务接口与实现类
package com.md.b1;
/**
* @author MD
* @create 2020-08-09 10:55
*/
public interface SomeService {
void doSome(String name , Integer age);
}
//-------------------------
package com.md.b1;
/**
* @author MD
* @create 2020-08-09 10:55
*/
// 目标类
public class SomeServiceImpl implements SomeService {
@Override
public void doSome(String name, Integer age) {
// 给doSome方法增加一个功能,在执行之前输出时间
System.out.println("目标方法doSome()");
}
}
2. 定义切面类
类中定义了若干普通方法,将作为不同的通知方法,用来增强功能
注意点:
@Aspect
- 这个注解是aspectj框架中的注解
- 作用:表示当前类是切面类
- 切面类:是用来给业务方法增加功能的类,在这个类中有切面的功能代码
- 位置:类定义的上面
定义方法,方法是实现切面功能的
方法的要求:
- 公共方法
- 方法名称自定义
- 方法没有返回值
- 方法可以有或没有参数,如果有参数,参数不是自定义的,有几个参数类型可以使用
package com.md.b1;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import java.util.Date;
/**
* @author MD
* @create 2020-08-09 10:58
*/
@Aspect
public class MyAspect {
// 前置通知,具体的在下变
@Before(value = "execution(public void com.md.b1.SomeServiceImpl.doSome(String,Integer))")
public void myBefore(){
// 就是你切面要执行的功能代码
System.out.println("前置通知,切面功能:在目标方法之前输出时间:"+new Date());
}
}
3. 定义目标对象切面类对象
还是在src/main/resources下建立applicationContext.xml
整体结构如下:
4. 注册AspectJ自动代理
在上面文件的基础上添加
从其类名就可看出,是基于 AspectJ 的注解适配自动代理生成器。
其工作原理是,
面类根据切入点找到目标类的目标方法,再由通知类型找到切入的时间点
5. 测试类中的使用
package com.md;
import com.md.b1.SomeService;
import org.junit.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import sun.security.provider.Sun;
/**
* @author MD
* @create 2020-08-09 15:28
*/
public class MyTest01 {
@Test
public void test01(){
String config = "applicationContext.xml";
ApplicationContext ac = new ClassPathXmlApplicationContext(config);
// 从容器中获取目标对象,此时的目标对象是经过了aspectj修改后的代理对象
SomeService proxy = (SomeService) ac.getBean("someService");
//com.sun.proxy.$Proxy8 jdk动态代理
//System.out.println(proxy.getClass().getName());
// 通过代理的对象执行方法,实现目标方法执行,增强了功能
proxy.doSome("张三",19);
// 前置通知,切面功能:在目标方法之前输出时间:Sun Aug 09 15:33:24 CST 2020
// 目标方法doSome()
}
}
2. @Before前置通知
在目标方法执行之前执行
被注解为前置通知的方法,可以包含一个 JoinPoint 类型参数。该类型的对象本身就是切入点表达式
通过该参数,可获取切入点表达式、方法签名、目标对象等
不光前置通知的方法,可以包含一个 JoinPoint 类型参数,所有的通知方法均可包含该参数。
这个JoinPoint参数的值是由框架赋予, 必须是第一个位置的参数
@Aspect
public class MyAspect {
// @Before():前置通知注解
// 属性:value 是切入点表达式,表示切面功能执行的位置
// 位置:在方法的上面
// 特点:
// 1. 在目标方法之前执行
// 2. 不会改变目标方法的执行结果
// 3. 不会影响目标方法的执行
// @Before(value = "execution(public void com.md.b1.SomeServiceImpl.doSome(String,Integer))")
// public void myBefore(){
// // 就是你切面要执行的功能代码
// System.out.println("前置通知,切面功能:在目标方法之前输出时间:"+new Date());
// }
// @Before(value = "execution( * *..SomeServiceImpl.do*(..))")
// public void myBefore(){
// // 就是你切面要执行的功能代码
// System.out.println("前置通知,切面功能:在目标方法之前输出时间:"+new Date());
// }
/**
* 指定通知方法中的参数 : JoinPoint
* JoinPoint:业务方法,要加入切面功能的业务方法
* 作用是:可以在通知方法中获取方法执行时的信息, 例如方法名称,方法的实参。
* 如果你的切面功能中需要用到方法的信息,就加入JoinPoint.
* 这个JoinPoint参数的值是由框架赋予, 必须是第一个位置的参数
*/
@Before(value = "execution(void *..SomeServiceImpl.doSome(String,Integer))")
public void myBefore(JoinPoint jp){
// 获取方法的完整定义
System.out.println("方法的定义:"+jp.getSignature());
System.out.println("方法的名称:"+jp.getSignature().getName());
// 获取方法的实参
Object[] args = jp.getArgs();
for (Object arg:args){
System.out.println("参数:"+arg);
}
// 方法的定义:void com.md.b1.SomeService.doSome(String,Integer)
// 方法的名称:doSome
// 参数:张三
// 参数:19
// 就是你切面要执行的功能代码
System.out.println("前置通知,切面功能:在目标方法之前输出时间:"+new Date());
}
}
3. @AfterReturning后置通知
在目标方法执行之后执行。由于是目标方法之后执行,所以可以获取到目标方法的返回值
该注解的 returning 属性就是用于指定接收方法返回值的变量名的
所以,被注解为后置通知的方法,除了可以包含 JoinPoint 参数外,还可以包含用于接收返回值的变量。该变量最好为 Object 类型,因为目标方法的返回值可能是任何类型
增加接口的方法
public interface SomeService {
void doSome(String name, Integer age);
String doOther(String name , Integer age);
}
//--------------------------------------------------
// 目标类
public class SomeServiceImpl implements SomeService {
@Override
public void doSome(String name, Integer age) {
// 给doSome方法增加一个功能,在执行之前输出时间
System.out.println("目标方法doSome()");
}
@Override
public String doOther(String name, Integer age) {
System.out.println("目标方法doOther()");
return "a";
}
}
定义切面
@Aspect
public class MyAspect {
/**
* 后置通知定义方法,方法是实现切面功能的。
* 方法的定义要求:
* 1.公共方法 public
* 2.方法没有返回值
* 3.方法名称自定义
* 4.方法有参数的,推荐是Object ,参数名自定义
*/
/**
* @AfterReturning:后置通知
* 属性:1.value 切入点表达式
* 2.returning 自定义的变量,表示目标方法的返回值的。
* 自定义变量名必须和通知方法的形参名一样。
* 位置:在方法定义的上面
* 特点:
* 1。在目标方法之后执行的。
* 2. 能够获取到目标方法的返回值,可以根据这个返回值做不同的处理功能
* Object res = doOther();
* 3. 可以修改这个返回值,但不影响最后的调用结果
*
* 后置通知的执行
* Object res = doOther();
* 参数传递: 传值, 传引用
* myAfterReturing(res);
* System.out.println("res="+res)
*
*/
@AfterReturning(value = "execution(* *..SomeServiceImpl.doOther(..))",returning = "res")
public void myAfterReturing(Object res){
if (res.equals("a")){
// 你可以做一些功能
System.out.println("登陆成功");
}else{
System.out.println("登陆失败");
}
// Object res:是目标方法执行后的返回值,可以根据返回值做切面功能处理
System.out.println("后置通知:获取的返回值是:"+res);
// 修改目标方法的返回值,是否影响最后的方法调用结果
// 无影响,
if (res != null){
res = "hello Aspectj";
}
}
}
4. @Around环绕通知
在目标方法执行之前之后执行。
被注解为环绕增强的方法要有返回值,Object 类型。并且方法可以包含一个 ProceedingJoinPoint 类型的参数。
接口 ProceedingJoinPoint 其有一个proceed()方法,用于执行目标方法。
若目标方法有返回值,则该方法的返回值就是目标方法的返回值。
最后,环绕增强方法将其返回值返回。该增强方法实际是拦截了目标方法的执行
首先增加方法和实现
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) {
// 给doSome方法增加一个功能,在执行之前输出时间
System.out.println("目标方法doSome()");
}
@Override
public String doOther(String name, Integer age) {
System.out.println("目标方法doOther()");
return "a";
}
@Override
public String doFirst(String name, Integer age) {
System.out.println("目标方法doFirst()");
return "doFirst";
}
}
切面类
@Aspect
public class MyAspect {
/**
* 环绕通知方法的定义格式
* 1.public
* 2.必须有一个返回值,推荐使用Object
* 3.方法名称自定义
* 4.方法有参数,固定的参数 ProceedingJoinPoint
*/
/**
* @Around: 环绕通知
* 属性:value 切入点表达式
* 位置:在方法定义的上面
* 特点:
* 1.它是功能最强的通知
* 2.在目标方法的前和后都能增强功能。
* 3.控制目标方法是否被调用执行
* 4.修改原来的目标方法的执行结果。 影响最后的调用结果
*
* 环绕通知,等同于jdk动态代理的,InvocationHandler接口
*
* 参数: ProceedingJoinPoint 就等同于 Method
* 作用:执行目标方法的
* 返回值: 就是目标方法的执行结果,可以被修改。
*
* 环绕通知: 经常做事务, 在目标方法之前开启事务,执行目标方法, 在目标方法之后提交事务
*/
// @Around(value = "execution(* *..SomeServiceImpl.doFirst(..))")
// public Object myAround(ProceedingJoinPoint pjp) throws Throwable {
//
// Object result = null;
//
//
// System.out.println("环绕通知,在目标方法之前加入通知:现在时间:"+new Date());
//
// // 1. 目标方法调用,等同于method.invoke(); 在这里等同于Object result = doFirst();
// result = pjp.proceed();
//
// // 2. 在目标方法前后加入功能
// System.out.println("环绕通知,在目标方法之后提交事务");
//
// // 3. 返回目标方法的执行结果
// return result;
//
// }
@Around(value = "execution(* *..SomeServiceImpl.doFirst(..))")
public Object myAround(ProceedingJoinPoint pjp) throws Throwable {
// 可以获取到调用方法的参数
String name = "";
// 获取第一个参数的值
Object args[] = pjp.getArgs();
if (args!=null && args.length > 1){
Object arg = args[0];
name = (String) arg;
}
// 实现环绕通知
Object result = null;
System.out.println("环绕通知,在目标方法之前加入通知:现在时间:"+new Date());
// 1. 目标方法调用,等同于method.invoke(); 在这里等同于Object result = doFirst();
if ("张三".equals(name)){
// 符合条件,调用目标方法
result = pjp.proceed();
}
// 2. 在目标方法前后加入功能
System.out.println("环绕通知,在目标方法之后提交事务");
// 还可以修改目标方法的执行结果,影响方法最后的调用结果
if (result != null){
result = "修改了";
}
// 3. 返回目标方法的执行结果
return result;
}
}
测试:
public class MyTest03 {
@Test
public void test01(){
String config = "applicationContext.xml";
ApplicationContext ac = new ClassPathXmlApplicationContext(config);
// 从容器中获取目标对象
SomeService proxy = (SomeService) ac.getBean("someService");
// 通过代理的对象执行方法,实现目标方法执行,增强了功能
String str = proxy.doFirst("张三",20);
System.out.println(str);
}
}
//环绕通知,在目标方法之前加入通知:现在时间:Mon Aug 10 20:52:07 CST 2020
// 目标方法doFirst()
// 环绕通知,在目标方法之后提交事务
// 修改了
5. @AfterThrowing 异常通知
在目标方法抛出异常后执行。该注解的 throwing 属性用于指定所发生的异常类对象。当然,被注解为异常通知的方法可以包含一个参数 Throwable,参数名称为 throwing 指定的名称,表示发生的异常对象
相当于try-catch中catch里面执行的
增加业务方法
public interface SomeService {
void doSome(String name, Integer age);
String doOther(String name, Integer age);
String doFirst(String name, Integer age);
void doSecond();
}
//实现类-----------------------------------------
@Override
public void doSecond() {
System.out.println("执行业务方法doSecond()" + (10/0));
}
切面类:
@Aspect
public class MyAspect {
/**
* 异常通知方法的定义格式
* 1.public
* 2.没有返回值
* 3.方法名称自定义
* 4.方法有个一个Exception, 如果还有是JoinPoint,
*/
/**
* @AfterThrowing:异常通知
* 属性:1. value 切入点表达式
* 2. throwinng 自定义的变量,表示目标方法抛出的异常对象。
* 变量名必须和方法的参数名一样
* 特点:
* 1. 在目标方法抛出异常时执行的
* 2. 可以做异常的监控程序, 监控目标方法执行时是不是有异常。
* 如果有异常,可以发送邮件,短信进行通知
*
* 执行就是:
* try{
* SomeServiceImpl.doSecond(..)
* }catch(Exception e){
* myAfterThrowing(e);
* }
*/
@AfterThrowing(value = "execution(* *..SomeServiceImpl.doSecond(..))",
throwing = "ex")
public void myAfterThrowing(Exception ex) {
System.out.println("异常通知:方法发生异常时,执行:"+ex.getMessage());
//发送邮件,短信,通知开发人员
}
}
6. @After最终通知
无论目标方法是否抛出异常,该增强均会被执行
相当于try-catch-finally中finally里面执行的
增加方法及实现
public interface SomeService {
void doSome(String name, Integer age);
String doOther(String name, Integer age);
String doFirst(String name, Integer age);
void doSecond();
void doThird();
}
//------------------------
@Override
public void doThird() {
System.out.println("执行业务方法doThird()"+ (10/0));
}
切面类
@Aspect
public class MyAspect {
/**
* 最终通知方法的定义格式
* 1.public
* 2.没有返回值
* 3.方法名称自定义
* 4.方法没有参数, 如果还有是JoinPoint,
*/
/**
* @After :最终通知
* 属性: value 切入点表达式
* 位置: 在方法的上面
* 特点:
* 1.总是会执行
* 2.在目标方法之后执行的
*
* try{
* SomeServiceImpl.doThird(..)
* }catch(Exception e){
*
* }finally{
* myAfter()
* }
*
*/
@After(value = "execution(* *..SomeServiceImpl.doThird(..))")
public void myAfter(){
System.out.println("执行最终通知,总是会被执行的代码");
//一般做资源清除工作的。
}
}
7. @Pointcut定义切入点
当较多的通知增强方法使用相同的 execution 切入点表达式时,编写、维护均较为麻烦AspectJ 提供了@Pointcut 注解,用于定义 execution 切入点表达式
其用法是,将@Pointcut 注解在一个方法之上,以后所有的 execution 的 value 属性值均可使用该方法名作为切入点。代表的就是@Pointcut 定义的切入点。这个使用@Pointcut 注解的方法一般使用 private 的标识方法,即没有实际作用的方法
五、总结
前面的概念有些绕,看了代码就比较清晰了,感觉和python的装饰器很像,只不过py的没有这么绕,就是为已经存在的对象添加额外的功能
总的来说就是在一个方法前或一个方法后执行一些通用的方法,提高效率,把那些业务的方法写一块,那些非业务的方法或那些业务方法经常使用的方法写成切面类,使用方便还便于管理
1. 使用aspectj框架实现aop
使用aop:目的是给已经存在的一些类和方法增加额外的功能,前提是不改变原来类的代码
- 新建maven项目
- 加入依赖:spring依赖和aspectj依赖以及junit单元测试
- 创建目标类
- 接口和它的实现类,要做的是给类中的方法增加功能
- 创建切面类:普通类
- 在类的上面加入@Aspect
- 在类中定义方法,这个方法就是切面要执行的功能代码
- 在方法的上面加入aspectj中的通知注解。例如:@Before
- 还需要指定切入点表达式,execution()
- 创建spring的配置文件,声明对象,把对象交给容器统一管理,声明对象可以使用注解或者
- 声明目标对象
- 声明切面类对象
- 声明aspectj框架中的自动代理生成器标签,自动代理生成器:用来完成代理对象的自动创建功能
- 创建测试类
- 从spring容器中获取目标对象(实际上就是代理对象),通过代理执行,实现aop的功能增强
2. Review
强制使用cglib代理
目标类有接口,还想用cglib代理
proxy-target-class="true" : 这句话就是告诉框架,要使用cglib动态代理