Spring AOP

1 AOP 概念

# AOP:Aspect Oriented Programming面向切面编程

	1 AOP可以说是OOP(Object Oriented Programming,面向对象编程)的补充和完善。
	2 OOP引入封装、继承、多态等概念来建立一种对象层次结构,用于模拟公共行为的一个集合。
	3 OOP缺陷:
			OOP允许开发者定义纵向的关系,但并不适合定义横向的关系,	例如日志功能。
			日志代码往往横向地散布在所有对象层次中,而与它对应的对象的核心功能毫无关系对于其他类型的代码,
			如安全性、异常处理和透明的持续性也都是如此
			这种散布在各处的无关的代码被称为横切(cross cutting)
			在OOP设计中,它导致了大量代码的重复,而不利于各个模块的重用。
	4 AOP的作用:
			AOP技利用一种称为"横切"的技术,剖解开封装的对象内部,
			将那些影响了多个类的公共行为封装到一个可重用模块,并将其命名为"Aspect",即切面。
			所谓"切面",就是那些与业务无关,却为业务模块所共同调用的逻辑或责任封装起来
			便于减少系统的重复代码,降低模块之间的耦合度,并有利于未来的可操作性和可维护性。
			使用AOP,可以在不修改原来代码的基础上添加新功能。
1 AOP:Aspect Oriented Programming

2 AOP是一种设计思想,面向切面编程的思想,是面向对象编程的补充和完善

3 AOP面向切面编程通过    预编译方式   和    运行期动态代理的方式   
	  实现了在不修改源代码的情况下给程序动态的、统一的添加额外功能。
	  
4 OOP是纵向继承机制,能抽取的一定是连续的代码
	AOP是横向抽取机制,专门解决非连续性代码的抽取问题

5 AOP的底层就是动态代理

1.2 AOP相关术语

横切关注点

从每个方法中抽取出来的同一类   非核心业务
每个项目中可以使用  多个横切关注点  对相关方法进行多个不同方面的增强
例如:计算器程序中的日志功能就是  横切关注点

通知

封装横切关注点的类就是切面
注意:
	1 切面中的每一个横切关注点都是一个    通知
	2 附加功能代码,在目标对象中叫横切关注点,抽取出来之后,封装到了切面中,在切面中叫通知方法
# 通知的分类:
		1 前置通知
				目标方法执行前的附加代码
		2 返回通知
				目标方法执行完毕后写的附加代码
		3 异常通知
				目标代码出现异常的时候执行的附加代码
		4 后置通知
				finally代码块中,即无论异常发生与否都要执行的附加代码
		5 环绕通知
				相当于前面的四种
				使用try catch 环绕整个目标方法,包含前四种通知的所有位置

切面

作用:切面就是用来封装横切关注点的

封装横切关注点的类就是切面
封装通知方法的类叫切面
		横切关注点是相对于目标方法的叫法
		通知方法是相对于切面的叫法,通知方法就是横切关注点被抽取到了切面中的叫法

目标对象

# 目标对象指代当前的目标对象,例如计算机器程序中Target对象封装了加、减、乘、除方法
# 需要在目标对象中抽取附加功能代码(非核心业务代码)
# 需要对目标对象进行功能增强

代理对象

# 为当前目标对象创建出来的代理对象
# AOP中的代理对象、代理工厂,都不需要程序员创建,AOP中封装了代理模式

连接点

  • 逻辑层面提出的,非语法定义
抽取横切关注点的位置
	例如:
		连接点1:计算器中,目标对象方法执行前位置抽取的
		连接点2:计算器中,目标对象方法执行后位置抽取的
		连接点3:计算器中,目标对象方法执行出现异常位置抽取的
		连接点4:计算器中,目标对象方法执行finally代码块位置中抽取的

切入点

  • 切入点是一个表达式
  • 切入点是从代码层面提出的
定位连接点的方式

总结

# AOP主要干两件事情:
	 1 抽横切关注点封装到切面中,横切关注点就成了通知,通过切入点定位到连接点
	 2 然后就可以在不改变目标对象代码的前提下,把切面中的通知
	 			通过切入点表达式套到连接点上,实现目标代码的功能增强
# AOP的作用:
		1 简化代码
				抽取方法中固定位置的重复代码,让被抽取的方法更加专注于自己的功能,提高内聚性
		2 代码增强
				把特定的功能封装到切面中,看哪里有需要就哪里套,被套用了切面中的逻辑的方法就得到了功能增强

2 AOP的应用场景

AOP 是一种编程范式,它通过将通用的横切关注点(如日志、事务、权限控制等)与业务逻辑分离,
	使得代码更加清晰、简洁、易于维护。
AOP 可以应用于各种场景,以下是一些常见的AOP应用场景:

1.  # 日志记录:
			在系统中记录日志是非常重要的,可以使用AOP来实现日志记录的功能,
			可以在方法执行前、执行后或异常抛出时记录日志。
2.  # 事务处理:
			在数据库操作中使用事务可以保证数据的一致性,可以使用AOP来实现事务处理的功能,
			可以在方法开始前开启事务,在方法执行完毕后提交或回滚事务。
3.  # 安全控制:
			在系统中包含某些需要安全控制的操作,如登录、修改密码、授权等,
			可以使用AOP来实现安全控制的功能。可以在方法执行前进行权限判断,
			如果用户没有权限,则抛出异常或转向到错误页面,以防止未经授权的访问。
4.  # 性能监控:
			在系统运行过程中,有时需要对某些方法的性能进行监控,以找到系统的瓶颈并进行优化。
			可以使用AOP来实现性能监控的功能,可以在方法执行前记录时间戳,
			在方法执行完毕后计算方法执行时间并输出到日志中。
5.  # 异常处理:
			系统中可能出现各种异常情况,如空指针异常、数据库连接异常等,
			可以使用AOP来实现异常处理的功能,在方法执行过程中,如果出现异常,
			则进行异常处理(如记录日志、发送邮件等)。
6.  # 缓存控制:在系统中有些数据可以缓存起来以提高访问速度,可以使用AOP来实现缓存控制的功能,
			可以在方法执行前查询缓存中是否有数据,如果有则返回,否则执行方法并将方法返回值存入缓存中。
7.  # 动态代理:
			AOP的实现方式之一是通过动态代理,可以代理某个类的所有方法,用于实现各种功能。
    
    
 综上所述,AOP可以应用于各种场景,它的作用
 		是将通用的横切关注点与业务逻辑分离,使得代码更加清晰、简洁、易于维护。

3 AOP的实现

3.1 基于注解的AOP

  • AspectJ是AOP思想的一种具体实现方式
  • 有接口使用JDK动态代理或者是cglib动态代理,没接口只能使用cglib动态代理
JDK动态代理:
		JDK 的API中,java.lang.reflect包中的Proxy类提供了创建动态代理的方法
		JDK 动态代理使用前提是:被代理的目标类必须实现接r
			原因是因为JDK动态代理要求目标对象和代理对象实现相同的接口。
		JDK 动态代理生成的代理类在com.sun.proxy包下,类名为$proxy+数字
cglib动态代理:
		目标类通过被继承而实现的被代理,所以也不需要目标类实现接口,顾没有前提条件

在这里插入图片描述

Spring配置文件

<beans xmlns="http://www.springframework.org/schema/beans"
       ...
       xsi:schemaLocation="http://www.springframework.org/schema/aop/spring-aop.xsd">

    <!--
        AOP注意事项:
            1 切面类和目标类都需要交给 IoC 管理
                    交给IOC有两种方式:注解+扫描   /   配置形式
                    本次案例以注解+扫描实现
            2 切面类必须通过@Aspect注解标识为一个切面
            3 配置文件还需要设置开启注解形式AOP,<aop:aspectj-autoproxy />

    -->
  
    <!--注解+扫描实现,类交给IOC管理-->
    <context:component-scan base-package="com.zhiyou100.aop.annotation"></context:component-scan>
    <!--开启基于注解的AOP功能-->
    <aop:aspectj-autoproxy />
</beans>

切面类

@Component
@Aspect//将当前组件标识为切面
public class LoggerAspect {
    /**
     * 切面:
     *    抽取横切关注点为方法
     *    被抽取的横切关注点的方法即为通知,切面就是封装通知的类
     *    一共有5种通知
     *
     *  切面中需要做的事情:
     *     通过指定的注解将方法标识为通知
     *     @Before:表示前置通知的注解
     *     ...
     *
     */
    @Before("execution(public int com.zhiyou100.aop.annotation.Target.add(int,int))")
    public void adviced01(){
        System.out.println("前置通知:=======>");
    }
}

测试切面

前置通知:=======>
目标代码:(lnum + rnum)=101

简化切入点

  • 切入点比较复杂
//切入点和通知
@Before("execution(public int com.zhiyou100.aop.annotation.Target.add(int,int))")
public void adviced01(){
    System.out.println("前置通知:=======>");
}
  • 如果需要将前置通知作用所有的方法,以下为优化
//@Before("execution(public int com.zhiyou100.aop.annotation.Target.add(int,int))")
//第1个* 任意的访问修饰符  第二个* 任意的方法  补充:类位置处也可以写*,代表包下的任意类
@Before("execution(* com.zhiyou100.aop.annotation.Target.*(..))")
public void adviced01(){
    System.out.println("前置通知:=======>");
}
前置通知:=======>
目标代码:(lnum + rnum)=101
前置通知:=======>
目标代码:(lnum - rnum)=-99
前置通知:=======>
目标代码:(lnum * rnum)=100
前置通知:=======>
目标代码:(lnum / rnum)=0

测试连接点

  • 获取连接点的方法名
  • 获取连接点的参数列表
前置通知,操作名add,参数[1, 100]=======>
目标代码:(lnum + rnum)=101
前置通知,操作名sub,参数[1, 100]=======>
目标代码:(lnum - rnum)=-99
前置通知,操作名mul,参数[1, 100]=======>
目标代码:(lnum * rnum)=100
前置通知,操作名div,参数[1, 100]=======>
目标代码:(lnum / rnum)=0
//@Before("execution(public int com.zhiyou100.aop.annotation.Target.add(int,int))")
//第1个* 任意的访问修饰符  第二个* 任意的方法  补充:类位置处也可以写*,代表包下的任意类
@Before("execution(* com.zhiyou100.aop.annotation.Target.*(..))")
public void adviced01(JoinPoint joinPoint){
  //signature是方法的签名信息:
  String methodName=joinPoint.getSignature().getName();
  System.out.println("前置通知,操作名"+methodName+",参数"+
                     Arrays.toString(joinPoint.getArgs())+"=======>");
}

测试切入点表达式的复用

		前置通知,操作名add,参数[1, 100]=======>
目标代码:(lnum + rnum)=101
    后置通知,操作名add,参数[1, 100]=======>

    前置通知,操作名sub,参数[1, 100]=======>
目标代码:(lnum - rnum)=-99
    后置通知,操作名sub,参数[1, 100]=======>

    前置通知,操作名mul,参数[1, 100]=======>
目标代码:(lnum * rnum)=100
    后置通知,操作名mul,参数[1, 100]=======>

    前置通知,操作名div,参数[1, 100]=======>
目标代码:(lnum / rnum)=0
    后置通知,操作名div,参数[1, 100]=======>
@Pointcut("execution(* *.*(..)))")
public void pointCut() {}

@After("pointCut()")
public void advice02(JoinPoint joinPoint) {
    String methodName = joinPoint.getSignature().getName();
    System.out.println("    后置通知,操作名" + methodName + 
                       ",参数" + Arrays.toString(joinPoint.getArgs()) + "=======>\n");
}

@Before("pointCut()")
public void adviced01(JoinPoint joinPoint) {
    //signature是方法的签名信息:
    String methodName = joinPoint.getSignature().getName();
    System.out.println("    前置通知,操作名" + methodName + 
                       ",参数" + Arrays.toString(joinPoint.getArgs()) + "=======>");
}

4 五种通知

# 1 前置通知
			Before("切入点")
			执行时机:目标方法开始前
# 3 返回通知
			AfterReturning(value="切入点",returning="result")
			执行时机:目标方法正常结束后
# 4 异常通知
			AfterThrowing(value="切入点",throwing="result")
			执行时机:目标方法出现异常后
# 2 后置通知
			After("切入点")
			执行时机:目标方法最终结束后,finally 代码块中执行
# 5 环绕通知	
			使用 try...catch...finally 结构围绕整个被代理的目标方法
			包括上面四种通知对应的所有位置
			注意:环绕通知返回值必须和目标操作返回值一致
			理由:
				环绕通知更像动态代理,动态代理中,代理对象返回的一定要和目标对象一致
				因为代理对象只是对目标进行增强,代理对象不能改变目标原有的特性

4.1 测试返回通知

  • 时机:目标方法正常结束后执行
@Aspect
@Component
public class LoggerAspect02 {//切面就是一个类,封装  切入点+通知
    //定义切入点
    @Pointcut("execution(*   *.*(..)))")
    public void pointCut01(){}

    //定义返回通知
    @AfterReturning(value="pointCut01()",returning = "res")
    public void returnAdvice(JoinPoint joinPoint,Object res){
        //此处只能写Object,否则报错:0 formal unbound in pointcut
        //原因:切点表达式定义了参数,但是通知中没有进行正确绑定
        String mname=joinPoint.getSignature().getName();
        System.out.println("=======>切入点:全部,计算结果:"+res+
                           ",当前方法:"+mname+",参数:"+ 
                           Arrays.toString(joinPoint.getArgs()));
    }
}
目标代码:(lnum + rnum)=101
=======>切入点:全部,计算结果:101,当前方法:add,参数:[1, 100]
目标代码:(lnum - rnum)=-99
=======>切入点:全部,计算结果:-99,当前方法:sub,参数:[1, 100]
目标代码:(lnum * rnum)=100
=======>切入点:全部,计算结果:100,当前方法:mul,参数:[1, 100]
目标代码:(lnum / rnum)=0
=======>切入点:全部,计算结果:0,当前方法:div,参数:[1, 100]

4.2 测试异常通知

  • 时机:目标方法出现异常时执行,catch代码块中执行
public void testAdvice01(){
    ApplicationContext ioc = new ClassPathXmlApplicationContext("aopdemo.xml");
    /**
     * 1 此处bean的类型应该为代理对象类型,但是不知道代理对象的名字,只知道代理对象实现的接口,接口也是代理对象的类型
     * 2 此处获取的是该接口下的实现类,代理对象和目标对象实现的同一个接口
     * 3 代理模式中无法获取到目标对象,因此此处获取的一定是代理对象
     */
    Calculator calculator = (Calculator)ioc.getBean(Calculator.class);
    calculator.add(1,100);
    calculator.sub(1,100);
    calculator.mul(1,100);
    calculator.div(10,0);
}
@Aspect
@Component
public class LoggerAspect03 {
    @Pointcut("execution(* *.div(..))")//定义切点,切点:切点就是一个表达式,定义具体哪些连接点被拦截然后应用通知
    public void pointCut02(){}

    @AfterThrowing(value="pointCut02()",throwing = "ex")
    public void advice01(JoinPoint joinPoint,Exception ex){
        String mname = joinPoint.getSignature().getName();
        String args = Arrays.toString(joinPoint.getArgs());
        System.out.println("*****异常日志:"+mname+",参数:"+args+",异常原因:"+ex);
    }

}
目标代码:(lnum + rnum)=101
目标代码:(lnum - rnum)=-99
目标代码:(lnum * rnum)=100
*****异常日志:div,参数:[10, 0],异常原因:java.lang.ArithmeticException: / by zero

4.3 测试环绕通知

  • 使用 try catch finally 围绕整个被代理的目标方法
@Aspect
@Component
public class LoggerAspect04 {
    @Pointcut("execution(* *.*(..))")
    public void pointCut01(){}
    @Around(value="pointCut01()")
    public Object roundAdvice(ProceedingJoinPoint joinPoint){
        Object res=0;
        try {
            String name = joinPoint.getSignature().getName();
            String args = Arrays.toString(joinPoint.getArgs());
            System.out.println("------>环绕---前置:"+name+"::::"+args);
            res = joinPoint.proceed();//环绕---------->通知必须有这一点代表目标方法的执行
            System.out.println("------>环绕---返回:"+res);
        } catch (Throwable e) {
            System.out.println("------>环绕---异常:"+e.getMessage());
            throw new RuntimeException(e);
        } finally {
            System.out.println("------>环绕---后置执行完毕\n");
        }
        return res;//环绕---------->通知必须返回的和目标方法一致
    }
}
------>环绕---前置:add::::[1, 100]
     目标方法:(lnum + rnum)=101
------>环绕---返回:101
------>环绕---后置执行完毕

------>环绕---前置:sub::::[1, 100]
     目标方法:(lnum - rnum)=-99
------>环绕---返回:-99
------>环绕---后置执行完毕

------>环绕---前置:mul::::[1, 100]
     目标方法:(lnum * rnum)=100
------>环绕---返回:100
------>环绕---后置执行完毕

------>环绕---前置:div::::[10, 0]
------>环绕---异常:/ by zero
------>环绕---后置执行完毕

4.4 四大非环绕

@Before("pointCut01()")
public void advice02(JoinPoint joinPoint){
    String name = joinPoint.getSignature().getName();
    String args = Arrays.toString(joinPoint.getArgs());
    System.out.println("前置---------->通知:方法名:"+name+",参数:"+args);
}
@AfterReturning(value = "pointCut01()",returning = "res")
public void advice03(JoinPoint joinPoint,Object res){
    String name = joinPoint.getSignature().getName();
    String args = Arrays.toString(joinPoint.getArgs());
    System.out.println("返回---------->通知:结果:"+res+"\n");
}
@AfterThrowing(value = "pointCut01()",throwing = "ex")
public void advice04(JoinPoint joinPoint,Exception ex){
    System.out.println("异常---------->通知:异常:"+ex);
}
@After(value = "pointCut01()")
public void advice05(JoinPoint joinPoint){
    System.out.println("后置---------->通知执行完毕");
}

5 名词解释

 /**
     * 切面:
     * 抽取横切关注点为方法
     * 被抽取的横切关注点的方法即为通知,切面就是封装通知的类
     * 一共有5种通知
     * 切面中需要做的事情:
     * 通过指定的注解将方法标识为通知
     *
# 横切关注点
		从每个方法中抽取出来的同一类  非核心业务
		在同一个项目中,我们可以使用多个横切关注点对相关方法进行多个不同方面的增强。
		这个概念不是语法层面天然存在的,而是根据附加功能的逻辑上的需要:有十个附加功能,就有十个横切关注点
		
		AOP把软件系统分为两个部分:
			核心关注点 和 横切关注点
			业务处理的主要流程是核心关注点,与之关系不大的部分是横切关注点。
			横切关注点的一个特点是,他们经常发生在核心关注点的多处,而各处基本相似,
			比如权限认证、日志、事务、异常等。
			AOP的作用在于分离系统中的各种关注点,将核心关注点和横切关注点分离开来。
# 通知
		每一个横切关注点上要做的事情都需要写一个方法来实现,这样的方法就叫通知方法。
		前置通知:目标方法前执行
		返回通知:目标方法成功结束后执行
		异常通知:目标方法异常结束后执行
		后置通知:目标方法最终结束后(可能正常结束,可能异常结束,定义在finally中)
		环绕通知:使用try catch包围整个目标方法,包含以上四种通知的四个位置
# 连接点  joinpoint
		纯逻辑概念,非语法定义
		指被拦截的那些点
# 切入点  pointcut
		定义了哪些连接点会被拦截并应用通知
		是一个表达式
			例如:
			execution(\* com.spring.service.impl.*.*(..))
			符合条件的每个方法都是一个具体的连接点。
# 切面	  aspect	
		切面=切入点+通知
		切面是一个封装了切入点和通知的类
# 目标		target
		被代理的目标对象
# 代理		proxy
		向目标对象应用通知之后创建的代理对象
# 织入 		weave
		指把通知应用到目标上,生成代理对象的过程
		可以在编译期织入,也可以在运行期织入,Spring采用运行期进行织入

切入点 PointCut

-- 连接点是程序执行过程中的某个特定点,例如方法调用、异常抛出等
-- 切点定义了哪些连接点会被拦截并应用通知
-- 简单来说,切点就是用来定义切面的位置,即捕获哪些连接点的调用然后执行通知的操作

6 Spring AOP框架介绍和关系梳理

1. # AOP是一种区别于OOP的编程思维,用来完善和解决OOP的非核心代码冗余和不方便统一维护问题!
2. # 代理技术(动态代理|静态代理)是实现AOP思维编程的具体技术,但是自己使用动态代理实现代码比较繁琐!
3. # Spring AOP框架
				基于AOP编程思维,封装动态代理技术
				简化了动态代理技术实现的框架!
					SpringAOP内部帮助我们实现动态代理,我们只需写少量的配置,指定生效范围即可
					即可完成面向切面思维编程的实现!

7 动态代理

  • 动态代理(InvocationHandler):
    • JDK原生的实现方式,需要被代理的目标类必须实现接口
    • 要求代理对象和目标对象实现同样的接口
  • cglib:
    • 通过继承被代理的目标类实现代理,所以不需要目标类实现接口
  • AspectJ:
    • 早期的AOP实现的框架,SpringAOP借用了AspectJ中的AOP注解

8 通知的执行顺序

在这里插入图片描述

9 通知的优先级

  • 切面加一个@Order注解即可实现切面的优先级
  • 默认214748…. ,为int 的最大值
  • value值越小切面优先级越高
@Aspect
@Component
@Order(2)
public class LoggerAspect04 {
    @Pointcut("execution(* *.*(..))")
    public void pointCut01(){}
    @Around(value="pointCut01()")
    public Object roundAdvice(ProceedingJoinPoint joinPoint){
        Object res=0;
        try {
            String name = joinPoint.getSignature().getName();
            String args = Arrays.toString(joinPoint.getArgs());
            System.out.println("------>环绕---前置:"+name+"::::"+args);
            res = joinPoint.proceed();//环绕---------->通知必须有这一点代表目标方法的执行
            System.out.println("------>环绕---返回:"+res);
        } catch (Throwable e) {
            System.out.println("------>环绕---异常:"+e.getMessage());
            throw new RuntimeException(e);
        } finally {
            System.out.println("------>环绕---后置执行完毕\n");
        }
        return res;//环绕---------->通知必须返回的和目标方法一致
    }
}
@Aspect
@Component
@Order(1)
public class ValidateAspect01 {
    @Pointcut("execution(* *.div(..))")
    public void pointCut(){}

    @Before("pointCut()")//检查除数是否为0
    public void checkDivisorIsZeroAdvice(JoinPoint joinPoint){
        Object[] arr = joinPoint.getArgs();
        if(arr[1].equals(0)){
            System.out.println("校验通知:!!!!!!!!除数为0");
        }else{
            System.out.println("校验通知:==========>除数不为0");

        }
    }
}
校验通知:!!!!!!!!除数为0
------>环绕---前置:div::::[10, 0]
------>环绕---异常:/ by zero
------>环绕---后置执行完毕
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值