1、为什么会出现AOP思维
我们知道,在我们的项目中,会出现核心代码和非核心代码,对于非核心代码,在各个方法中可能是冗余的,此时为了解决这种非核心代码的冗余以及不方便管理的问题,就出现了AOP思维。
2、AOP思维是什么?
AOP:Aspect Oriented Programming面向切面编程。它是面向对象编程OOP的完善与补充。
面向对象编程是垂直性的,我们可以继承父类的方法,但是如果我们想添加一些其他东西,就需要完全重写该方法。
而AOP是切面性质的,我们需要做的就是解耦,将冗余代码取出来,然后再动态的添加到每个业务方法中。
此时我们就需要一些技术,来完成我们的AOP思维,该技术就是代理技术。
3、代理技术
代理模式是二十三种设计模式中的一种,属于结构型模式。它的作用就是通过提供一个代理类,让我们在调用目标方法的时候,不再是直接对目标方法进行调用,而是通过代理类间接调用。让不属于目标方法核心逻辑的代码从目标方法中剥离出来——解耦。调用目标方法时先调用代理对象的方法,减少对目标方法的调用和打扰,同时让附加功能能够集中在一起也有利于统一维护。
无代理类时:我们是直接调用业务方法。
当出现代理类时:
我们首先调用代理类,之后代理类再去调用我们的目标方法。最后将两者填充在一起,返回给最初的调用者。这样做的好处是:我们可以将那些冗余代码写在代理类中,进行统一管理,通过代理类来调用目标方法。可以让目标方法专注于去完成它自己的业务逻辑。
代理在开发中有两种模式:一种是静态代理,一种是动态代理(jdk,cblib)
静态代理需要我们为每个目标类都编写一个目标类,这样虽然实现了解耦,但是还是需要大量代码,并没有实现统一管理。
动态代理包括jdk动态代理和cglib
jdk动态代理与cglib的区别:
jdk动态代理:其目标类必须有一个接口,目标类实现该接口,他会根据目标类的接口动态生成一个代理对象!代理对象和目标对象有相同的接口!(拜把子)。
cglib:通过继承被代理的目标类实现代理,所以不需要目标类实现接口!(认干爹)
4、Spring aop框架
不论是静态代理还是动态代理技术,都需要编写大量代码,并没有简化操作。所以提出了Spring AOP框架,该框架底层用的仍然是代理技术,但是由于封装了起来,对程序员来说就很方便。
4.1 AOP主要应用场景(了解)
只要记住对于那些非核心的冗余代码,我们可以使用AOP即可。
AOP(面向切面编程)是一种编程范式,它通过将通用的横切关注点(如日志、事务、权限控制等)与业务逻辑分离,使得代码更加清晰、简洁、易于维护。AOP可以应用于各种场景,以下是一些常见的AOP应用场景:
1. 日志记录:在系统中记录日志是非常重要的,可以使用AOP来实现日志记录的功能,可以在方法执行前、执行后或异常抛出时记录日志。
2. 事务处理:在数据库操作中使用事务可以保证数据的一致性,可以使用AOP来实现事务处理的功能,可以在方法开始前开启事务,在方法执行完毕后提交或回滚事务。
3. 安全控制:在系统中包含某些需要安全控制的操作,如登录、修改密码、授权等,可以使用AOP来实现安全控制的功能。可以在方法执行前进行权限判断,如果用户没有权限,则抛出异常或转向到错误页面,以防止未经授权的访问。
4. 性能监控:在系统运行过程中,有时需要对某些方法的性能进行监控,以找到系统的瓶颈并进行优化。可以使用AOP来实现性能监控的功能,可以在方法执行前记录时间戳,在方法执行完毕后计算方法执行时间并输出到日志中。
5. 异常处理:系统中可能出现各种异常情况,如空指针异常、数据库连接异常等,可以使用AOP来实现异常处理的功能,在方法执行过程中,如果出现异常,则进行异常处理(如记录日志、发送邮件等)。
6. 缓存控制:在系统中有些数据可以缓存起来以提高访问速度,可以使用AOP来实现缓存控制的功能,可以在方法执行前查询缓存中是否有数据,如果有则返回,否则执行方法并将方法返回值存入缓存中。
7. 动态代理:AOP的实现方式之一是通过动态代理,可以代理某个类的所有方法,用于实现各种功能。
综上所述,AOP可以应用于各种场景,它的作用是将通用的横切关注点与业务逻辑分离,使得代码更加清晰、简洁、易于维护。
4.2 AOP术语
1-横切关注点
从每个方法中抽取出来的同一类非核心业务。在同一个项目中,我们可以使用多个横切关注点对相关方法进行多个不同方面的增强。
这个概念不是语法层面天然存在的,而是根据附加功能的逻辑上的需要:有十个附加功能,就有十个横切关注点。
AOP把软件系统分为两个部分:核心关注点和横切关注点。业务处理的主要流程是核心关注点,与之关系不大的部分是横切关注点。横切关注点的一个特点是,他们经常发生在核心关注点的多处,而各处基本相似,比如权限认证、日志、事务、异常等。AOP的作用在于分离系统中的各种关注点,将核心关注点和横切关注点分离开来。
2-通知(增强)
每一个横切关注点上要做的事情都需要写一个方法来实现,这样的方法就叫通知方法。
- 前置通知:在被代理的目标方法前执行
- 返回通知:在被代理的目标方法成功结束后执行(**寿终正寝**)
- 异常通知:在被代理的目标方法异常结束后执行(**死于非命**)
- 后置通知:在被代理的目标方法最终结束后执行(**盖棺定论**)
- 环绕通知:使用try...catch...finally结构围绕整个被代理的目标方法,包括上面四种通知对应的所有位置
3-连接点 joinpoint
这也是一个纯逻辑概念,不是语法定义的。
指那些被拦截到的点。在 Spring 中,可以被动态代理拦截目标类的方法
4-切入点 pointcut
定位连接点的方式,或者可以理解成被选中的连接点!
是一个表达式,比如execution(* com.spring.service.impl.*.*(..))。符合条件的每个方法都是一个具体的连接点。
5-切面 aspect
切入点和通知的结合。是一个类。
6-目标 target
被代理的目标对象。
7-代理 proxy
向目标对象应用通知之后创建的代理对象。
8-织入 weave
指把通知应用到目标上,生成代理对象的过程。可以在编译期织入,也可以在运行期织入,Spring采用后者。
5、Spring-AOP基于注解方式实现和细节
5.1 Spring AOP底层实现技术
- 动态代理(InvocationHandler):JDK原生的实现方式,需要被代理的目标类必须实现接口。因为这个技术要求代理对象和目标对象实现同样的接口(兄弟两个拜把子模式)。
- cglib:通过继承被代理的目标类(认干爹模式)实现代理,所以不需要目标类实现接口。
- AspectJ:早期的AOP实现的框架,SpringAOP借用了AspectJ中的AOP注解。
AspectJ 是AOP早期实现框架,Spring AOP继承了该注解方式,但是底层实现技术仍然是代理技术。
依赖:
我们需要导入的依赖有 spring-aop,spring-aspects,AspectJ
但是我们使用spring容器导入的有Spring-context,其带有Spring-AOP依赖,而AspectJ依赖也不用手动导入,只需要导入Spring-aspects依赖即可,其带有Aspectj依赖。
导入依赖:
<dependency> <groupId>org.springframework</groupId> <artifactId>spring-context</artifactId> <version>6.0.6</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-aspects</artifactId> <version>6.0.6</version> </dependency>
编写Springaop流程:首先我们先正常写我们的核心代码,①导入依赖,②编写核心代码,③配置ioc,④测试,之后再来管理aop流程,我们的核心是来⑤编写增强代码,编写一个类来写增强代码,增强类方法具体有几个,是要看我们的非核心代码的位置,不同位置增强方法不同。接下来是⑥增强类的配置(插入切点的位置,切点指定,切面配置等),最后是⑦开启AOP配置。
①导入依赖
②编写核心代码
接口
package demo05.aop; public interface Calculator { int add(int i, int j); int sub(int i, int j); int mul(int i, int j); int div(int i, int j); }
具体实现类
package demo05.aop.Impl; import demo05.aop.Calculator; import org.springframework.stereotype.Component; /** * 实现计算接口,单纯添加 + - * / 实现! 掺杂其他功能! */ @Component public class CalculatorPureImpl implements Calculator { @Override public int add(int i, int j) { int result = i + j; return result; } @Override public int sub(int i, int j) { int result = i - j; return result; } @Override public int mul(int i, int j) { int result = i * j; return result; } @Override public int div(int i, int j) { int result = i / j; return result; } }
③配置类
package demo05.aop.config; import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.EnableAspectJAutoProxy; @Configuration @ComponentScan(basePackages = {"demo05.aop.Impl","demo05.aop.proxy"}) @EnableAspectJAutoProxy public class Myconfig { }
④测试
⑤编写增强类
在这里我们在方法开始之前输出 以及方法结束和方法异常输出,所以需要写三个增强类方法。
package demo05.aop.proxy; import org.aspectj.lang.annotation.AfterReturning; import org.aspectj.lang.annotation.AfterThrowing; import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.Before; import org.springframework.stereotype.Component; @Aspect // @Aspect表示这个类是一个切面类 @Component // @Component注解保证这个切面类能够放入IOC容器 public class Myproxy { // @Before注解:声明当前方法是前置通知方法 // value属性:指定切入点表达式,由切入点表达式控制当前通知方法要作用在哪一个目标方法上 //第一个*表示 不考虑返回值类型,我们要插入哪一个包下的哪个类,第三个参数*表示该类下的所有方法 //第四个参数*(..)表示不考虑其参数类型以及有无参数。 @Before("execution(* demo05.aop.Impl.*.*(..))") public void start(){ System.out.println("方法开始"); } @AfterReturning("execution(* demo05.aop.Impl.*.*(..))") public void after(){ System.out.println("方法结束"); } @AfterThrowing("execution(* demo05.aop.Impl.*.*(..))") public void error(){ System.out.println("方法错误"); }}
⑥增强配置
在前面的代码中,我们已经将其写过了,包括切点配置,切面配置等
我们也需要将增强类加上ioc组件注解,因为代理也需要在ioc容器中,才能操作我们的核心代码类。最后返回的其实也是我们的代理类
⑦开启aop配置
我们可以在xml中开启
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:aop="http://www.springframework.org/schema/aop"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/aop https://www.springframework.org/schema/aop/spring-aop.xsd"><!-- 进行包扫描-->
<context:component-scan base-package="com.atguigu" />
<!-- 开启aspectj框架注解支持-->
<aop:aspectj-autoproxy />
</beans>
也可以在配置类中开启
package demo05.aop.config; import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.EnableAspectJAutoProxy; @Configuration @ComponentScan(basePackages = {"demo05.aop.Impl","demo05.aop.proxy"}) @EnableAspectJAutoProxy public class Myconfig { }
⑧测试
@Test
public void test_06(){
AnnotationConfigApplicationContext context=new AnnotationConfigApplicationContext(Myconfig.class);
//有接口时,使用的时jdk代理,这里要用接口类接收,因为返回的是代理类,是目标类的一个兄弟,不能用目标类接收
//如果没有接口,就可以用目标类接收,因为用cglib,返回的是目标类的一个继承代理类。
Calculator calculator=context.getBean(Calculator.class);
System.out.println(calculator.add(1,1));
context.close();
}
结果:
5.2获取切点详细信息
有时候,我们需要我们目标方法的具体信息,比如参数,方法名,方法类的信息,返回值等。
这时候我们就需要一些方法来实现该需求。
如果我们需要获取切点的详细信息,比如方法名,方法类的信息,参数信息等
我们需要在增强类的参数中添加上 JoinPoint joinPoint
如果要获取返回值或者异常信息 就要在参数以及注解中添加上额外信息,见下面代码
package demo05.aop.proxy; import org.aspectj.lang.JoinPoint; import org.aspectj.lang.annotation.AfterReturning; import org.aspectj.lang.annotation.AfterThrowing; import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.Before; import org.springframework.stereotype.Component; import java.lang.reflect.Modifier; @Aspect @Component /**编写增强方法 * 配置增强方法的位置 * 配置切点信息 * 配置切面以及ioc * 开启aspectj注解支持 */ public class Myadvice { @Before("execution(* demo05.aop.Impl.*.*(..))") public void before(JoinPoint joinPoint){ //获取方法名 String name = joinPoint.getSignature().getName(); //获取类型修饰符 int modifiers = joinPoint.getSignature().getModifiers(); String s = Modifier.toString(modifiers); //获取类信息 String simpleName = joinPoint.getTarget().getClass().getSimpleName(); //获取参数列表 Object[] args = joinPoint.getArgs(); } //只有正常返回 才能获取返回值 //获取返回值 1.在形参中添加要接受返回值的名字 //2. 在 @AfterReturning()中添加是用哪个形参名来接受的返回值 @AfterReturning(value = "execution(* demo05.aop.Impl.*.*(..))",returning ="returning" ) public void afterreturning(JoinPoint joinPoint,Object returning){ } //只有异常返回 才能接收异常信息 //获取异常信息 1.在形参中添加要接受异常信息的名字 //2. 在 @AfterThrowing()中添加是用哪个形参名来接受的异常信息 @AfterThrowing(value = "execution(* demo05.aop.Impl.*.*(..))",throwing ="throwable") public void afterthrowing(JoinPoint joinPoint,Throwable throwable){ } }
5.3 切点表达式
5.3.1 语法细节
固定语法:execution(1 2 3.4.5(6))
1:方法修饰符
2:方法返回值
如果两者都不考虑 统一用*代替,注意:要考虑都不考虑 不能只考虑一个
3 包
固定的包: com.atguigu.api | service | dao
单层的任意命名: com.atguigu.* = com.atguigu.api com.atguigu.dao * = 任意一层的任意命名 任意层任意命名: com.. = com.atguigu.api.erdaye com.a.a.a.a.a.a.a ..任意层,任意命名 用在包上!
注意: ..不能用作包开头 public int ..
错误语法 com..
找到任何包下: *..
4.类名
固定名称: UserService
任意类名: *
部分任意: com..service.impl.*Impl
任意包任意类: *..*
5.方法名
语法和类名一致
任意访问修饰符,任意类的任意方法: * *..*.*
6.方法参数
第七位: 方法的参数描述
具体值: (String,int) != (int,String) 没有参数 ()
模糊值: 任意参数 有 或者 没有 (..) ..任意参数的意识
部分具体和模糊:
第一个参数是字符串的方法 (String..)
最后一个参数是字符串 (..String)
字符串开头,int结尾 (String..int)
包含int类型(..int..)
小练习
1.查询某包某类下,访问修饰符是公有,返回值是int的全部方法
2.查询某包下类中第一个参数是String的方法
3.查询全部包下,无参数的方法!
4.查询com包下,以int参数类型结尾的方法
5.查询指定包下,Service开头类的私有返回值int的无参数方法
5.4 切点的统一管理
我们发现在增强类中 切点表达式是一样的,我们可以将其提取出来,然后复用
方法1:提取到当前类中(不推荐,每个类都需要提取)
写一个空方法 public void xxx(){}
添加注解PointCut
增强注解中直接引用方法名即可
package demo05.aop.proxy; import org.aspectj.lang.annotation.*; import org.springframework.stereotype.Component; @Aspect // @Aspect表示这个类是一个切面类 @Component // @Component注解保证这个切面类能够放入IOC容器 public class Myproxy { // @Before注解:声明当前方法是前置通知方法 // value属性:指定切入点表达式,由切入点表达式控制当前通知方法要作用在哪一个目标方法上 //第一个*表示 不考虑返回值类型,我们要插入哪一个包下的哪个类,第三个参数*表示该类下的所有方法 //第四个参数*(..)表示不考虑其参数类型以及有无参数。 @Pointcut("execution(* demo05.aop.Impl.*.*(..))") public void pc(){} @Before("pc()") public void start(){ System.out.println("方法开始"); } @AfterReturning("pc()") public void after(){ System.out.println("方法结束"); } @AfterThrowing("pc()") public void error(){ System.out.println("方法错误"); }}
方法2:创建一个存储切点的类
该类记得放在ioc容器中
单独维护切点表达式
增强方法使用该切点表达式: 类的全限定符.方法名
切点类:
package demo05.aop.pointcut; import org.aspectj.lang.annotation.Pointcut; import org.springframework.stereotype.Component; @Component public class Mypointcut { @Pointcut("execution(* demo05.aop.Impl.*.*(..))") public void pc(){ } }d
代理类:
package demo05.aop.proxy; import org.aspectj.lang.annotation.*; import org.springframework.stereotype.Component; @Aspect // @Aspect表示这个类是一个切面类 @Component // @Component注解保证这个切面类能够放入IOC容器 public class Myproxy { // @Before注解:声明当前方法是前置通知方法 // value属性:指定切入点表达式,由切入点表达式控制当前通知方法要作用在哪一个目标方法上 //第一个*表示 不考虑返回值类型,我们要插入哪一个包下的哪个类,第三个参数*表示该类下的所有方法 //第四个参数*(..)表示不考虑其参数类型以及有无参数。 @Pointcut("execution(* demo05.aop.Impl.*.*(..))") public void pc(){} @Before("demo05.aop.pointcut.Mypointcut.pc()") public void start(){ System.out.println("方法开始"); } @AfterReturning("demo05.aop.pointcut.Mypointcut.pc()") public void after(){ System.out.println("方法结束"); } @AfterThrowing("demo05.aop.pointcut.Mypointcut.pc()") public void error(){ System.out.println("方法错误"); }}
5.5 环绕通知
环绕通知可以包含brfore,after等这些通知。可以通过下面的例子来演示。
package demo05.aop.proxy; import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.annotation.Around; import org.aspectj.lang.annotation.Aspect; import org.springframework.stereotype.Component; @Aspect @Component public class Advicearound { //ProceedingJoinPoint 比JoinPoint 多了一个可执行目标函数的方法 @Around("demo05.aop.pointcut.Mypointcut.pc()") //配置切点和位置 public Object aroundSet(ProceedingJoinPoint joinPoint){ Object[] args = joinPoint.getArgs(); Object result=null; try { System.out.println("开启事务"); //相当于Before result = joinPoint.proceed(args); System.out.println("事务结束"); //相当于AfterReturning } catch (Throwable e){ System.out.println("事务异常");//相当于Afterthrowing throw new RuntimeException(e);//一定要抛出异常,不然外部无论如何都接收不到异常 } return result; } }
5.5 切面优先级设定
比如我们有一个日志增强,一个事务增强,如何确定两个增强的优先级呢?
可以通过@Order()注解
里边的数值越小,优先级越高,前置增强先执行,后置增强后执行
反之越低。
事务增强 @Order(10) 日志增强 @Order(20)
结果: