Spring Aop

Spring Aop

AOP概述

AOP:Aspect Oriented Programming面向切面编程,其实对一类特定问题的统一处理方法,例如:在一个网站中一些操作是必须登录以后才可以进行操作的,因此需要进行这些操作需要判断用户是否登录,可以通过AOP将这些需要判断是否登录的方法集中处理,AOP是⼀种思想, 是对某⼀类事情的集中处理

AOP的实现方式有Spring AOP 、Aspectj 、CGlib

假如现在项目的执行效率较低,此时需要找出是那些方法执行效率低下,可能会在每个方法执行前和执行后记录时间,二者之差就是该方法的执行时间,但这样每个方法都添加工作量太大了

这里就可以使用AOP,AOP在不改变原始方法的基础上,对一些特定方法进行加强,这里也就是计算出方法执行的时间

Spring AOP快速入门

统计指定方法的执行时间

先引入AOP依赖

pom.xml

xml 复制代码
<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-aop</artifactId>
</dependency>

这里使用Spring Boot是3.多的版本,不需要指定版本,但是当前如果使用4.多需要指定aop的版本

记录Controller包下的所有方法的各个执行时间

java 复制代码
@Slf4j
@Component
@Aspect
public class TimeRecordAspect {
    //对那个文件进行操作
    @Around("execution(* com.example.demo.controller.*.*(..))")
    public Object recordTime(ProceedingJoinPoint joinPoint) throws Throwable {
        long start = System.currentTimeMillis();
        Object result = joinPoint.proceed();//执行原始方法
        long end = System.currentTimeMillis();

        log.info(joinPoint.getSignature() + " 耗时:{}ms",end - start);

        return result;
    }
}

@Aspect:标识这是⼀个切⾯类

@Around:环绕通知,在⽬标⽅法的前后都会被执⾏.后⾯的表达式表⽰对哪些⽅法进⾏增强.

ProceedingJoinPoint.proceed()让原始⽅法执⾏

通过上面程序可以感受到AOP的优势

减少重复代码、代码无侵入(不会影响到原始方法的执行)、提高开发效率、方便维护

Spring AOP详解

切点(Pointcut)

切断的作用就是提供一组规则,增强了那些方法,对那些方法生效

这里的execution(* com.example.demo.controller.. (...))就是切点表达式

连接点(Join Point)

连接点就是满足切点表达式规则的方法,就是连接点,也可以说是AOP增强的方法

通知(Advice)

就是这个做了那些操作,例如:上面记录业务执行时间就是通知

通知放的都是部分重复的代码逻辑抽出来单独定义

切⾯(Aspect)

切⾯(Aspect) = 切点(Pointcut) + 通知(Advice)

AOP针对了那些方法执行了那些操作

通知类型

Spring AOP中的通知类型如下:

@Around: 环绕通知 , 此注解标注的通知⽅法在⽬标⽅法前, 后都被执⾏

@Before: 前置通知 , 此注解标注的通知⽅法在⽬标⽅法前被执⾏

@After: 后置通知 , 此注解标注的通知⽅法在⽬标⽅法后被执⾏, ⽆论是否有异常都会执⾏

@AfterReturning: 返回后通知 , 此注解标注的通知⽅法在⽬标⽅法后被执⾏, 有异常不会执⾏

@AfterThrowing: 异常后通知, 此注解标注的通知⽅法发⽣异常后执⾏

java 复制代码
@Slf4j
@Aspect
@Component
public class AspectDemo {
    @Around("execution(* com.aop.Controller.*.*(..))")
    public Object doAround(ProceedingJoinPoint joinPoint) throws Throwable {
        log.info("执行doAround 前");
        Object result = joinPoint.proceed();
        log.info("执行doAround 后");

        return result;
    }
    @Before("execution(* com.aop.Controller.*.*(..))")
    public void doBefore() throws Throwable {
        log.info("执行doBefore ");
    }

    @After("execution(* com.aop.Controller.*.*(..))")
    public void doAfter () throws Throwable {
        log.info("执行doBefore ");
    }

    @AfterReturning("execution(* com.aop.Controller.*.*(..))")
    public void doAfterReturning() throws Throwable {
        log.info("执行doAfterReturning ");
    }

    @AfterThrowing("execution(* com.aop.Controller.*.*(..))")
    public void doAfterThrowing () throws Throwable {
        log.info("执行doAfterThrowing ");
    }
}
java 复制代码
@Slf4j
@RequestMapping("/test")
@RestController
public class TestController {
    @RequestMapping("/t1")
    public String t1(){
        log.info("执行t1");
        return "t1";
    }

    @RequestMapping("/t2")
    public String t2(){
        log.info("执行t2");
        int i = 10 / 0;
        return "t2";
    }
}

正常执行

http://127.0.0.1:8080/test/t1

这里没有异常并不会执行这里的@AfterThrowing

反之执行这里的t2,存在异常这里10 / 0

http://127.0.0.1:8080/test/t2

这里就会执行@AfterThrowing,但是因为这里异常没有被处理 直接抛出了,这里该方法执行后就结束了,doAround后就没有执行

但是这里如果进行捕获,但不抛出异常,此时就会全部执行

java 复制代码
    @Around("execution(* com.aop.Controller.*.*(..))")
    public Object recordTime(ProceedingJoinPoint joinPoint) {

        log.info("do around 前");

        //执行目标方法
        Object result = null;
        try {
            result = joinPoint.proceed();
        } catch (Throwable e) {
            log.info("do around 发生异常");
//            throw new RuntimeException(e);
        }
        log.info("do around 后");
        return result;
    }

如果这里捕获后进行抛出,此时这里do around 后也不会执行

@Around环绕通知需要调用ProceedingJoinPoint.proceed() 执行原始方法,其他通知不需要考虑目标方法执行,并且其必须要有返回值,必须指定Object,来接收原始方法的返回值

@PointCut

上面可以发现测试的通知其实都是对execution(* com.aop.Controller..(...))都是对同一个包下方法进行增强,Spring提供了@PointCut 注解可以把公共的切点表达式提取出来

java 复制代码
@Slf4j
@Aspect
@Component
public class AspectDemo {

    @Pointcut("execution(* com.aop.Controller.*.*(..))")
    private void pt(){}
    @Around("pt()")
    public Object recordTime(ProceedingJoinPoint joinPoint) {

        log.info("do around 前");

        //执行目标方法
        Object result = null;
        try {
            result = joinPoint.proceed();
        } catch (Throwable e) {
            log.info("do around 发生异常");
            throw new RuntimeException(e);
        }
        log.info("do around 后");
        return result;
    }
}

此时这里private修改,外部类不可以使用,如果其他类也要使用,需要将其修改为public,引用方式:全限定类名.方法名()

java 复制代码
@Slf4j
@Aspect
@Component
public class AspectDemo2 {
    //前置通知
    @Before("com.aop.aspect.AspectDemo.pt()")
    public void doBefore() {
        log.info("执⾏ AspectDemo2 的 Before ⽅法");
    }

}

切⾯优先级@Order

当我们在一个项目中,定义多个切面类时,并且这些切面类的多个切入点同时匹配一个目标方法

java 复制代码
@Slf4j
@Aspect
@Component
public class AspectDemo2 {
    //前置通知
    @Before("com.aop.aspect.AspectDemo.pt()")
    public void doBefore() {
        log.info("执⾏ AspectDemo2 的 Before ⽅法");
    }

    @After("com.aop.aspect.AspectDemo.pt()")
    public void doAfter() {
        log.info("执⾏ AspectDemo2 的 doAfter ⽅法");
    }
}
java 复制代码
@Slf4j
@Aspect
@Component
public class AspectDemo3 {
    //前置通知
    @Before("com.aop.aspect.AspectDemo.pt()")
    public void doBefore() {
        log.info("执⾏ AspectDemo3 的 Before ⽅法");
    }

    @After("com.aop.aspect.AspectDemo.pt()")
    public void doAfter() {
        log.info("执⾏ AspectDemo3 的 doAfter ⽅法");
    }
}
java 复制代码
@Slf4j
@Aspect
@Component
public class AspectDemo4 {
    //前置通知
    @Before("com.aop.aspect.AspectDemo.pt()")
    public void doBefore() {
        log.info("执⾏ AspectDemo4 的 Before ⽅法");
    }

    @After("com.aop.aspect.AspectDemo.pt()")
    public void doAfter() {
        log.info("执⾏ AspectDemo4 的 doAfter ⽅法");
    }
}

可以发现在多个切面类时,默认按照切面类名字母排序

@Before 通知:字⺟排名靠前的先执⾏

@After 通知:字⺟排名靠前的后执⾏

使用@Order注解确定执行顺序

Order中的数越大,其越先执行

切点表达式

  1. @execution():根据方法标签进行匹配
  2. @annotation():根据注解进行匹配

execution(<访问修饰符> <返回类型> <包名.类名.⽅法(⽅法参数)> <异常>)

这里的访问修饰符和异常是可以省略的

匹配任意字符,只匹配⼀个元素(返回类型,包,类名,⽅法或者⽅法参数)

包名使⽤ * 表⽰任意包(⼀层包使⽤⼀个*)

类名使⽤ * 表⽰任意类

返回值使⽤ * 表⽰任意返回值类型

⽅法名使⽤ * 表⽰任意⽅法

参数使⽤ * 表⽰⼀个任意类型的参数

... (两个点):匹配多个连续的任意符号, 可以通配任意层级的包,或任意类型,任意个数的参数

使⽤ ...配置包名,标识此包以及此包下的所有⼦包

可以使⽤ ...配置参数,任意个任意类型的参数

匹配TestController中t1的无参方法,返回值任意

java 复制代码
execution(* com.example.demo.controller.TestController.t1())

匹配TestController 下的所有⽆参⽅法

java 复制代码
execution(* com.example.demo.controller.TestController.*())

匹配controller包下所有的类的所有⽅法

java 复制代码
execution(* com.example.demo.controller.*.*(..))

匹配所有包下⾯的TestController

java 复制代码
execution(* com..TestController.*(..))

execution更适合有规则的,具有一定规律的方法,例如:一个包下的方法/一个类下的方法等

而如果是无规则 的使用@annotation更合适

1.先编写自定义注解

2.使用@annotation表达式来描述切点

3.在连接点的方法上添加自定义注解

java 复制代码
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
public @interface TimeRecord {
}

@Retention指的是Annotation被保留时间长短,标明注解的生命周期

@Target 标识了 Annotation 所修饰的对象范围, 即该注解可以⽤在什么地⽅

ElementType.TYPE: ⽤于描述类、接⼝(包括注解类型) 或enum声明

ElementType.METHOD: 描述⽅法

ElementType.PARAMETER: 描述参数

ElementType.TYPE_USE: 可以标注任意类型

实现这个注解

java 复制代码
@Slf4j
@Aspect
@Component
public class TimeRecordAspect {
    @Around("@annotation(com.aop.aspect.TimeRecord)")
    public Object timeRecord(ProceedingJoinPoint joinPoint) throws Throwable {
        long start = System.currentTimeMillis();
        Object result = joinPoint.proceed();
        log.info(joinPoint.getSignature() + "耗时:{} ms", System.currentTimeMillis()-start);
        return result;
    }
}

这里定义的注解是作用在方法上的

java 复制代码
@Slf4j
@RequestMapping("/test")
@RestController
public class TestController {

    @TimeRecord
    @RequestMapping("/t1")
    public String t1(){
        log.info("执行t1");
        return "t1";
    }
}    

这里直接放到要作用的类上即可,加上对应注解

Spring AOP实现方式:https://cloud.tencent.com/developer/article/2032268

AOP原理

代理模式

为其他对象提供⼀种代理以控制对这个对象的访问.它的作⽤就是通过提供⼀个代理类,让我们在调⽤⽬标⽅法的时候,不再是直接对⽬标⽅法进⾏调⽤,⽽是通过代理类间接调⽤,代理就像中介一样来控制两端交互

Subject:业务接⼝类.可以是抽象类或者接口

RealSubject:业务实现类.具体的业务执⾏,被代理对象.

Proxy:代理类.RealSubject的代理.

以租房举例

这里的Subject就是房东要做的事情,交给中介代理

RealSubject:指的就是房东,被代理的对象

Proxy:中介

静态代理:在程序运⾏前,代理类的.class⽂件就已经存在了

java 复制代码
public interface HouseSubject {
    void rentHouse();
}
java 复制代码
public class RealHouseSubject implements HouseSubject{
    @Override
    public void rentHouse() {
        System.out.println("我是房东,出租房屋");
    }
}
java 复制代码
public class HouseProxy implements HouseSubject{

    //被代理对象
    private HouseSubject houseSubject;

    public HouseProxy(HouseSubject houseSubject){
        this.houseSubject = houseSubject;
    }
    @Override
    public void rentHouse() {
        System.out.println("我是中介,开始代理");
        houseSubject.rentHouse();
        System.out.println("我是中介,代理结束");
    }
}
java 复制代码
public class main {
    public static void main(String[] args) {
        RealHouseSubject realHouseSubject = new RealHouseSubject();
        HouseProxy proxy = new HouseProxy(realHouseSubject);
        proxy.rentHouse();
    }
}

这里静态代理是写死的,不是特别灵活

比如此时要加一个销售房屋的功能,此时HouseSubject 添加saleHouse方法

RealHouseSubject 和 HouseProxy 都要实现这个方法,

此时就要使用动态代理

我们不需要针对每个⽬标对象都单独创建⼀个代理对象⽽是把这个创建代理对象的⼯作推迟到程序运⾏时由JVM来实现. 也就是说动态代理在程序运⾏时,根据需要动态创建⽣成

1.定义⼀个接⼝及其实现类(静态代理中的 HouseSubject 和RealHouseSubject)

2.定义InvocationHandler 并重写 invoke ⽅法,处理调用被代理方法一些逻辑

3.通过Proxy.newProxyInstance(ClassLoader loader,Class<?>\[\]

interfaces,InvocationHandler h) ⽅法创建代理对象

java 复制代码
public interface HouseSubject {
    void rentHouse();
    void saleHouse();
}
java 复制代码
public class RealHouseSubject implements HouseSubject{
    @Override
    public void rentHouse() {
        System.out.println("我是房东,出租房屋");
    }

    @Override
    public void saleHouse() {
        System.out.println("我是房东,出售房屋");

    }
}
java 复制代码
public class JDKInvocationHandler implements InvocationHandler {

    private Object target;

    public JDKInvocationHandler(Object target) {
        this.target = target;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        //代理增强内容
        System.out.println("我是中介, 开始代理");
        Object result = method.invoke(target, args);  //调用目标类
        System.out.println("我是中介, 结束代理");
        return result;
    }
}
java 复制代码
public class dynamicMain {
    public static void main(String[] args) {
        HouseSubject target = new RealHouseSubject();

        //JDK动态生成代理类
        //通过被代理类、及其对应接口
        HouseSubject proxy = (HouseSubject) Proxy.newProxyInstance(
                target.getClass().getClassLoader(),
                new Class[]{HouseSubject.class},
                new JDKInvocationHandler(target)
        );

        proxy.saleHouse();
        proxy.rentHouse();
    }

}

通过

Loader:类加载器,用于加载代理对象

interfaces:被代理类实现的一些接口

h:实现InvocationHandler的接口对象

CGLIB

其是一个开源项目,需要引入对应依赖

xml 复制代码
<dependency>
 <groupId>cglib</groupId>
 <artifactId>cglib</artifactId>
 <version>3.3.0</version>
</dependency>

Spring Boot中内置了CGLIB

java 复制代码
public class CGLIBInterceptor implements MethodInterceptor {
    //目标对象, 即被代理对象
    private Object target;

    public CGLIBInterceptor(Object target){
        this.target = target;
    }

//o: 被代理的对象 
//method: ⽬标⽅法
//objects: ⽅法⼊参 
//methodProxy: ⽤于调⽤原始⽅法 
    @Override
    public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
        // 代理增强内容
        System.out.println("我是中介, 开始代理");
        //通过反射调用被代理类的方法
        Object retVal = methodProxy.invoke(target, objects);
        //代理增强内容
        System.out.println("我是中介, 代理结束");
        return retVal;
    }
}
java 复制代码
public class dynamicMain {
    public static void main(String[] args) {
        HouseSubject target = new RealHouseSubject();

        //JDK动态生成代理类
        //通过被代理类、及其对应接口
//        HouseSubject proxy = (HouseSubject) Proxy.newProxyInstance(
//                target.getClass().getClassLoader(),
//                new Class[]{HouseSubject.class},
//                new JDKInvocationHandler(target)
//        );

//        proxy.saleHouse();
//        proxy.rentHouse();

//CGLIB
        HouseSubject proxy = (HouseSubject) Enhancer.create(HouseSubject.class, new CGLIBInterceptor(target));
        proxy.rentHouse();
        proxy.saleHouse();
    }

}

type:被代理类的类型(类或接⼝)

callback:⾃定义⽅法实现的MethodInterceptor

其实本质上都是通过代理工厂,代理工厂中有一个重要的属性:proxyTargetClass,默认是false,可以通过

spring.aop.proxy-target-class=false设置

proxyTargetClass ⽬标对象 代理⽅式
false 实现了接⼝ jdk代理
false 未实现接⼝(只有实现类) cglib代理
true 实现了接⼝ cglib代理
true 未实现接⼝(只有实现类) cglib代理

jdk代理只能是实现了接口的类,而cglib即可代理类,又可以代理接口

在Spring中,实现了接口的就是jdk代理,未实现接口就是cglib代理

Spring Boot2.X版本以后,默认是使用cglib代理

相关推荐
keyipatience28 分钟前
MySQL 学习(4)常用数据类型
数据库·mysql
达梦产品与服务30 分钟前
SQLark V4.2 更新 | 新增TiDB、SSL安全连接、数据字典、清空/截断表等40+项功能已上线!
数据库·安全·tidb·ssl
道尔柯南37 分钟前
C++ STL 入门详解:从认识标准模板库到掌握学习方法
java·c++·学习方法
杨凯凡40 分钟前
Spring Boot 4 升级为什么卡在 Jackson 3?包名、配置与兼容迁移指南
java·spring boot 4·技术升级·jackson 3·spring framework
天天被压力40 分钟前
【A股分钟级高频数据完全手册 #01】Pro版接口体系总览与REST鉴权:42篇地图与从0接入
java·人工智能·python
xyouyuweizhu44 分钟前
Java InetAddress 详解:从 IP 地址表示到主机名解析
java·开发语言·python
码农阿豪1 小时前
SQL Server数据迁移完成后,凌晨的数据库告警该怎么查?
数据库·sqlserver
bjzhang751 小时前
sqlsugar入门
数据库·sqlsugar
全栈陈序员3 小时前
【Git】从入门到精通(十)—— 撤销与恢复:git checkout与git restore
运维·网络·数据库·git