目录
Aop面向切面编程
概述
底层是采用动态代理模式实现的。采用了两种代理:JDK的动态代理,CGLIB的动态代理。
AOP其实就是动态代理的一种规范化实现。
Aspect:切面:给目标类增加的方法,一般称为切面。一般都是非业务方法,可以独立使用。常见的有:日志,事务,统计信息,参数检查,权限验证。
JoinPoint:连接点,连接业务和切面的位置
Pointcut:切入点,指多个连接点的集合
advice:通知:表示切面功能的执行时间。
切面的三个关键要素:
1、切面的功能。
2、切面的执行位置,使用PointCut表示。
3、切面的执行时间,使用advice表示,在目标方法之前还是之后。
aspectJ框架:
一个开源的aop框架。spring集成了。使用方式有两种:
1、xml配置文件:配置全局事务。
2、注解;注解有五个
aspectJ
切面的执行时间
在规范中叫做Advance(通知;增强)
在aspectJ框架中使用注解表示,也可以使用xml配置:
@Before
@AfterReturning
@Around
@AfterThrowing
@After
切面的位置(切入点表达式
aspectJ框架定义了专门的表达式,用于指定切入点:
execution(<修饰符> <返回类型> <包.类.方法(参数)> <异常>)
修饰符和异常可以省略。
* : 任意个字符
.. : 用在方法参数中,表示任意个参数。用在包名处,表示包、子孙包下的所有类。
+ : 用在类名后,表示当前类及其子类。用在接口后,表示当前接口及其实现类
使用例子
execution(public * *(..))
:匹配目标类的所有public方法,第一个*
代表返回类型,第二个*
代表方法名,..
代表方法的参数。execution(**User(..))
:匹配目标类所有以User为后缀的方法。第一个*
代表返回类型,*User
代表以User为后缀的方法execution(* com.cad.demo.User.*(..))
:匹配 User 类里的所有方法execution(* com.cad.demo.User+.*(..))
:匹配该类的子类包括该类的所有方法execution(* com.cad.*.*(..))
:匹配 com.cad 包下的所有类的所有方法execution(* com.cad..*.*(..))
:匹配 com.cad 包下、子孙包下所有类的所有方法execution(* addUser(Spring, int))
:匹配 addUser 方法,且第一个参数类型是 String,第二个参数类型是 int
使用
-
加入依赖
-
创建目标类,给此类增加功能
-
创建切面类
- 在类上加入注解
@Aspect
。 - 在切面类中创建方法
- 在方法上加入
Advance
注解:如@Before
- 还需要指定切入点表达式
- 在类上加入注解
-
创建spring配置文件,声明对象,将对象交由容器管理
- 声明目标对象
- 声明切面类对象
- 声明aspectJ中的自动代理生成器标签
- 自动代理生成器:用来完成代理对象的自动创建功能
加入依赖
<dependencies>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.11</version>
<scope>test</scope>
</dependency>
<!-- https://mvnrepository.com/artifact/org.springframework/spring-context -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.2.5.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aspects</artifactId>
<version>5.2.5.RELEASE</version>
</dependency>
</dependencies>
创建目标类,给此类增加功能
@Component
public class SomeServiceImpl implements SomeService {
@Override
public void doSome(String name, Integer age) {
System.out.println("=====目标类doSome=====");
}
}
创建切面类
/*
* 表示当前类是切面类
* */
@Component("myAspect")
@Aspect
public class MyAspect {
/*
* 定义方法,实现切面功能
* 要求:
* 1、公共方法public
* 2、没有返回值void
* 3、如果有参数,则只能为JoinPoint(下面会说)
* 前置:@Before:
* 属性:value:切入点表达式
* 特点:
* 在目标方法前执行
* 不会改变、影响目标方法的执行和结果
* */
@Before(value = "execution(public void com.wm.ba01.SomeServiceImpl.doSome(String, Integer))")
public void myBefore(){
System.out.println("前置通知:方法执行时间"+ new Date());
}
}
创建spring配置文件,声明对象,将对象交由容器管理
<!-- 扫描注解,创建对象 -->
<context:component-scan base-package="com.wm.ba01"/>
<!--
声明自动代理生成器:使用aspectJ的内部功能,创建目标对象的代理对象
创建代理对象是在内存中实现的,修改目标目标对象在内存中的结构。创建为代理对象
所以目标对象就是被修改后的代理对象。
aspectj-autoproxy会将spring容器中的所有对象都生成代理对象
-->
<aop:aspectj-autoproxy/>
测试使用
@Test
public void test01(){
String config = "applicationContext.xml";
ApplicationContext applicationContext = new ClassPathXmlApplicationContext(config);
//从容器中获取目标对象
SomeService someServiceImpl = (SomeService) applicationContext.getBean("someServiceImpl");
someServiceImpl.doSome("a", 1);
}
执行结果
前置通知:方法执行时间Tue Aug 04 23:44:05 CST 2020
=====目标类doSome=====
JoinPoint:连接点参数
连接点就表示被代理的方法。
相当于jdk自带代理的Method参数。
表示业务方法:如果需要获取被代理方法的名称、实参等信息时,可以加入JoinPoint参数。
必须是第一个位置的参数
@Before(value = "execution(public void com.wm.ba01.SomeServiceImpl.doSome(String, Integer))")
public void myBefore(JoinPoint joinPoint){
System.out.println("获取方法的签名(定义):"+ joinPoint.getSignature());
System.out.println("获取方法的名称:"+ joinPoint.getSignature().getName());
Object[] args = joinPoint.getArgs();
for (Object arg : args) {
System.out.println("args:"+ arg);
}
System.out.println("前置通知:方法执行时间"+ new Date());
}
执行结果为:
获取方法的签名(定义):void com.wm.ba01.SomeService.doSome(String,Integer)
获取方法的名称:doSome
args:a
args:1
前置通知:方法执行时间Wed Aug 05 09:47:51 CST 2020
=====目标类doSome=====
name:a, age:1
注解
@Before
在被代理方法之前执行,如上“使用”环节的演示。
@AfterReturning
目标方法:
@Override
public String doOther(String name, Integer age) {
System.out.println("=====doOther=====");
System.out.println("name:"+name+", age:"+age);
return "abc";
}
通知方法:
/*
* 后置通知方法的定义:
* 要求:
* 1、公共方法public
* 2、没有返回值void
* 3、方法有参数,推荐是Object类型的,参数名自定义
* @AfterReturning:
* 属性:
* value:切入点表达式
* returning:自定义的变量,表示目标方法的返回值。
* 自定义变量名必须和通知方法的形参名一样。
* 即returning = "res",和 void myAfterReturning(Object res),res要一样
* 特点:
* 在目标方法后执行
* 能够获取目标方法的返回值,可以根据这个值做不同的处理
* Object res = doOther();
* 可以修改这个返回值
* */
@AfterReturning(value = "execution(* *..SomeServiceImpl.doOther(..))",
returning = "res")
public void myAfterReturning(Object res){
//Object res:是目标方法的返回值,可以根据这个返回值做不同的功能处理
System.out.println("后置通知:获取的返回值:"+ res);
//在此处可以对res进行修改等操作,但是因为String是不可修改的,这里返回值是String就不做演示,下方会展示
}
修改返回值:
实体类
@Component
public class Student {
String name;
int age;
}
目标方法
@Override
public Student doOther2(String name, Integer age) {
Student student = new Student();
student.setAge(age);
student.setName(name);
return student;
}
通知方法:
@AfterReturning(value = "execution(* *..SomeServiceImpl.doOther2(..))",
returning = "res")
public void myAfterReturning2(Object res){
System.out.println("后置通知:获取的返回值:"+ res);
Student student = (Student)res;
//这里将student对象的name属性修改。
student.setName("修改了");
}
测试
@Test
public void test03(){
String config = "applicationContext.xml";
ApplicationContext applicationContext = new ClassPathXmlApplicationContext(config);
//从容器中获取目标对象
SomeService someServiceImpl = (SomeService) applicationContext.getBean("someServiceImpl");
Student s = someServiceImpl.doOther2("b", 2);
System.out.println(s);
}
结果为:
后置通知:获取的返回值:Student{name='b', age=2}
Student{name='修改了', age=2}
//这里可以发现,确实修改了目标方法的返回值
另外
@AfterReturning的方法也可以用JoinPoint,但要放在参数的第一个位置。
@Around
基本使用:
目标方法:
@Override
public String doFirst(String name, Integer age) {
System.out.println("===doFirst===");
return "doFirst";
}
增强方法:
/*
* 环绕通知方法定义:
* 1、public
* 2、必须有返回值,推荐Object
* 3、必须有参数:ProceedingJoinPoint类型的
*
* @Around(value = ""):环绕通知
* 属性:value:切入点表达式
* 特点:
* 功能最强的通知
* 在目标方法前后都能增强目标方法功能
* 控制目标方法是否被调用执行
* 修改目标方法的执行结果,影响最后的调用结果
* 等同于jdk动态代理的InvocationHandler接口
*
* 参数:ProceedingJoinPoint就等同于Method
* 返回值:就是目标方法的执行结果,可以被修改
* */
@Around(value = "execution(* com.wm.ba02.SomeServiceImpl.doFirst(..))")
public Object myAround(ProceedingJoinPoint pjp) throws Throwable {
//实现环绕通知
//0、在目标方法前执行
System.out.println("目标方法前执行"+ new Date());
//1、目标方法调用
Object o = pjp.proceed();
//2、目标方法后执行
System.out.println("目标方法后执行");
return o;
}
测试和执行结果
@Test
public void test01(){
String config = "applicationContext.xml";
ApplicationContext applicationContext = new ClassPathXmlApplicationContext(config);
//从容器中获取目标对象
SomeService someServiceImpl = (SomeService) applicationContext.getBean("someServiceImpl");
someServiceImpl.doFirst("doFirst", 2);
}
目标方法前执行Wed Aug 05 12:15:52 CST 2020
===doFirst===
目标方法后执行
另外,ProceedingJoinPoint继承自JoinPoint,因此JoinPoint的方法也都可以用。
环绕通知用的最多,经常被使用在事务方面,方法执行前开启事务,方法执行后提交事务。
@AfterThrowing
在目标方法抛出异常时执行
增强方法:
/*
* 异常通知方法定义:
* 1、public
* 2、没有返回值
* 3、必须有参数:Exception类型,可选:JoinPoint
*
* @AfterThrowing:异常通知
* 属性:value:切入点表达式
* throwing:自定义的,表示目标方法抛出的异常。
* 变量名必须和通知方法的Exception参数的名字一样
* 特点:
* 在目标方法抛出异常时执行
* 可以做异常的监控程序,监控目标方法执行时有无异常
* 如果有异常可以发邮件、短信通知
* */
@AfterThrowing(value = "execution(* *..SomeServiceImpl.doSecond(..))",throwing = "e")
public void myAfterThrowing(Exception e){
System.out.println("异常通知:"+e.getMessage());
}
让后我们让目标方法抛出异常:
@Override
public void doSecond() {
System.out.println("====doSecond===="+ 10/0);//这里让除数为0
}
测试并查看执行结果:
@Test
public void test01(){
String config = "applicationContext.xml";
ApplicationContext applicationContext = new ClassPathXmlApplicationContext(config);
//从容器中获取目标对象
SomeService someServiceImpl = (SomeService) applicationContext.getBean("someServiceImpl");
someServiceImpl.doSecond();
}
//结果
异常通知:/ by zero
java.lang.ArithmeticException: / by zero
@After
最终通知:总是会执行(就算中途有异常,也会执行)
增强方法:
/*
*最终通知方法定义规则:
* public void
* 参数可以没有,如果有,只能是JoinPoint
*
* @After(value = ""):value:切入点表达式
* 特点:
* 总是会执行(就算中途有异常,也会执行)
* 类似 finally 代码块
* 在目标方法之后执行
* 一般用来做资源清除
* */
@After(value = "")
public void myAfter(){
System.out.println("====myAfter====");
}
@Pointcut
用来定义、管理切入点表达式的。当有多个重复的切入点表达式需要复用时,则可以通过@Pointcut
标签来进行复用。
使用:
- 定义在一个方法之上(void,无需代码)
@Pointcut(value = "")
在value中定义要复用的表达式- 在要使用
@Pointcut
表达式的方法上,用@Pointcut
之下的方法名表示
例子如下:
@After(value = "Pointcut()")
public void myAfter(){
System.out.println("====myAfter====");
}
@Before(value = "Pointcut()")
public void myBefore(){
System.out.println("====myBefore====");
}
@Pointcut(value = "execution(* *..SomeServiceImpl.doThird(..))")
public void Pointcut(){
}
以上动态代理全部是基于jdk的动态代理。
而若是不是继承接口的普通类,想要实现动态代理的方法,也是一样的,不过默认就是不是jdk的方式,而是spring提供的CGLIB
如果继承了接口,又想使用CGLIB动态代理的话,则可以在配置文件中加入:
<aop:aspectj-autoproxy proxy-target-class="true"/>
<!-- 即,将proxy-target-class设置为true -->