Spring-AOP 讲解

14 篇文章 0 订阅

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)

结果:

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值