Spring AOP

1.什么是AOP

AOP是Aspect Oriented Programming(面向切片编程),切面就是指某一类特定的问题,所以AOP也可以理解为面向特定方法编程。可以实现登入校验拦截器,统一数据返回格式,统一异常处理。

简单来说:AOP是一种思想,是对某一类事情的集中处理。

2.Spring AOP详解

2.1 切点(Pointcut)

切点,也称为切入点。Pointcut的作用就是提供一组规则,告诉程序对哪些方法进行功能增强。

    @Around("execution(* com.adviser.springaop1.controller.*.*(..))")
    public Object recordTime(ProceedingJoinPoint pjp) throws Throwable {
        long begin = System.currentTimeMillis();
        Object result = pjp.proceed();
        long end = System.currentTimeMillis();
        log.info("{}执⾏耗时: {}ms", pjp.getSignature(), end - begin);
        return result;
    }

上面的表达式execution(* com.adviser.springaop1.controller.*.*(..))就是切点表达式。

2.2 连接点(Join Point)

满足切点表达式规则的方法,就是连接点,也就是可以被AOP控制的方法,以上述的程序为例,所有com.adviser.springaop1.controller路径下的所有方法都是连接点。

package com.adviser.springaop1.controller;


import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RequestMapping("/test")
@Slf4j
@RestController
public class TestController {
    @RequestMapping("/t1")
    public String t1() {
        log.info("执行t1方法......");
        return "t1";
    }

    @RequestMapping("/t2")
    public String t2() {
        log.info("执行t2方法......");
        return "t2";
    }
}

 上述TestController中所有的方法都是连接点

切点和连接点的关系,连接点是满足切点表达式的元素。切点可以看作是保存了众多连接点的一个集合。说白了切点就是表示一个集合,连接点就是集合中的单独的个体

比如:

切点表达式:学校全体老师

连接点:张三,李四等各个老师

2.3 通知(Advice)

通知就是具体要做的工作,指那些重复的逻辑,也就是共性功能(最终体现为一个方法)

比如上述程序中记录业务方法的耗时时间,就是通知。

就是红色框住的这些。在AOP面向切片编程当中,我们把这部分重复的代码逻辑抽取起来单独定义,这部分代码就是通知的内容。 

2.4 切面(Aspect)

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

通过切面就能够描述当前AOP程序需要针对于哪些方法,在什么时候执行什么样的操作。

切面既包含了通知的逻辑定义,也包含了连接点的定义。

切面所在的类,我们一般称为切面类(被@Aspect注解标识的类) 

3. 通知类型

上面我们讲述了什么是通知,接下来学习通知的类型。@Around就是其中一种通知类型,标识环绕通知。

Spring中AOP的通知类型有以下几种:

@Around:环绕通知,此注解标注的通知方法在目标方法前后都被执行。

@Before:前置通知,此注解标注的通知方法在目标方法前被执行。

@After:后置通知,此注解标注的通知方法在目标方法后被执行。无论是否有异常都会执行。

@AfterReturning:返回后通知,此注解标注的通知方法在目标方法后被执行,有异常不会执行。

@AfterThrowing:异常后通知,此注解标注的通知方法发生异常后执行。

我们通过代码来加深对这几个通知的理解

package com.adviser.springaop1.aspect;

import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;

@Slf4j
@Component
@Aspect
public class AspectDemo {
    //前置通知
    @Before("execution(* com.adviser.springaop1.controller.*.*(..))")
    public void doBefore() {
        log.info("执行 Before 方法");
    }

    //后置通知
    @After("execution(* com.adviser.springaop1.controller.*.*(..))")
    public void doAfter() {
        log.info("执行 doAfter 方法");
    }

    //返回后通知
    @AfterReturning("execution(* com.adviser.springaop1.controller.*.*(..))")
    public void doAfterReturn() {
        log.info("执行 doAfterReturn 方法");
    }

    //抛出异常后通知
    @AfterThrowing("execution(* com.adviser.springaop1.controller.*.*(..))")
    public void doAfterThrowing() {
        log.info("执行 doAfterThrowing 方法");
    }

    //添加环绕通知
    @Around("execution(* com.adviser.springaop1.controller.*.*(..))")
    public Object doAround(ProceedingJoinPoint joinPoint) throws Throwable {
        log.info("Around 方法开始执行");
        Object result = joinPoint.proceed();
        log.info("Around 方法结束执行");
        return result;
    }
}

 接下来是测试程序

package com.adviser.springaop1.controller;


import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RequestMapping("/test")
@Slf4j
@RestController
public class TestController {
    @RequestMapping("/t1")
    public String t1() {
        log.info("执行t1方法......");
        return "t1";
    }

    @RequestMapping("/t2")
    public Boolean t2() {
        log.info("执行t2方法......");
        int a = 10 / 0;
        return true;
    }
}

 用postman测试一下

程序正常运行的时候@AfterThrowing标识的通知方法不会执行

从上图也可以看出,@Around标识的通知方法包含两部分,一个前置逻辑,一个后置逻辑。其中前置逻辑会先于@Before标识的通知方法执行,获知逻辑会晚于@After标识的通知方法执行。

 异常时的情况

程序发生异常的情况下:

@AfterReturning标识的通知方法不会执行,@AfterThrowing标识的通知方法执行了。

@Around环绕通知中原始方法调用时有异常,通知中的环绕后的代码逻辑也不会在执行了(因为原始方法调用出异常了)

注意事项: 

@Around环绕通知需要调用ProceedingJoinPoint.proceed() 来让原始方法执行,其他通知不需要考虑目标方法执行。

@Around环绕通知方法的返回值,必须是Object,来接收原始方法的返回值,否则原始方法执行完毕后是获取不到返回值的。

一个切面类可以有多个切点      

4.@Pointcut

上面代码存在一个问题,就是存在大量重复的切点表达式

execution(* com.adviser.springaop1.controller.*.*(..)),spring提供了@Pointcut注解,把公共的切点表达式提取出来,需要用到时引入切入点表达式即可。


@Slf4j
@Component
@Aspect
public class AspectDemo {
    //定义切点(公共切点表达式)
    @Pointcut("execution(* com.adviser.springaop1.controller.*.*(..))")
    private void pt(){}



    //前置通知
    @Before("pt()")
    public void doBefore() {
        log.info("执行 Before 方法");
    }

    //后置通知
    @After("pt()")
    public void doAfter() {
        log.info("执行 doAfter 方法");
    }

    //返回后通知
    @AfterReturning("pt()")
    public void doAfterReturn() {
        log.info("执行 doAfterReturn 方法");
    }

    //抛出异常后通知
    @AfterThrowing("pt()")
    public void doAfterThrowing() {
        log.info("执行 doAfterThrowing 方法");
    }

    //添加环绕通知
    @Around("pt()")
    public Object doAround(ProceedingJoinPoint joinPoint) throws Throwable {
        log.info("Around 方法开始执行");
        Object result = joinPoint.proceed();
        log.info("Around 方法结束执行");
        return result;
    }
}

当切点用private修饰时,仅能在当前切面类中中使用,当其他切面类也要使用当前切点定义时,就需要把private改为public。引用方式为:全限定类名.方法名() 

@Aspect
@Slf4j
@Component
public class AspectDemo1 {
    @Before("com.adviser.springaop1.aspect.AspectDemo.pt()")
    public void doBefore() {
        log.info("执行 AspectDemo1 -> Before方法");
    }

}

5.切面优先级@Order

当我们在一个项目中,定义了多个切面类时,并且这些切面类的多个切入点都匹配到了同一个目标方法。当这些目标方法运行的时候,这些切面类中的通知方法都会执行,那么这几个通知方法的执行顺序是什么样的?

定义多个切面类:


@Aspect
@Slf4j
@Component
public class AspectDemo1 {

    @Pointcut("execution(* com.adviser.springaop1.controller.*.*(..))")
    private void pt(){}

    @Before("pt()")
    public void doBefore() {
        log.info("执行 AspectDemo1 -> Before方法");
    }

    @After("pt()")
    public void doAfter() {
        log.info("执行 AspectDemo1 -> doAfter方法");
    }

}
@Aspect
@Slf4j
@Component
public class AspectDemo2 {

    @Pointcut("execution(* com.adviser.springaop1.controller.*.*(..))")
    private void pt(){}

    @Before("pt()")
    public void doBefore() {
        log.info("执行 AspectDemo2 -> Before方法");
    }

    @After("pt()")
    public void doAfter() {
        log.info("执行 AspectDemo2 -> doAfter方法");
    }

}
@Aspect
@Slf4j
@Component
public class AspectDemo2 {

    @Pointcut("execution(* com.adviser.springaop1.controller.*.*(..))")
    private void pt(){}

    @Before("pt()")
    public void doBefore() {
        log.info("执行 AspectDemo2 -> Before方法");
    }

    @After("pt()")
    public void doAfter() {
        log.info("执行 AspectDemo2 -> doAfter方法");
    }

}

运行程序使用postman测试一下

 通过上述日志可以看出:

存在多个切面类时,默认按照切面类的类名字母排序:

@Before通知:字母排名靠前的先执行

@After通知:字母排名靠后的先执行

但这种方式不方便管理,我们的类名更多的还是具备一定含义的。

Spring给我们提供一个新的注解,来控制这些切面通知的执行顺序:@Order

使用方式如下

@Aspect
@Slf4j
@Order(3)
@Component
public class AspectDemo1 {

}




@Aspect
@Slf4j
@Order(1)
@Component
public class AspectDemo2 {


}



@Aspect
@Slf4j
@Order(2)
@Component
public class AspectDemo3 {


}

 再次访问http://127.0.0.1:8080/test/t1

通过上述程序的运行结果,得出结论:

@Order注解标识的切面类,执行顺序如下:

@Before通知:数字越小先执行

@After通知:数字越大先执行

@Order控制切面的优先级,先执行优先级较高的切面,在执行优先级较低的切面,最终执行目标方法。 

6.切点表达式

上面的代码中,我们一直在使用切点表达式来描述切点。下面我们来介绍一下切点表达式的语法。

切点表达式常见有两种表达方式

1.execution(........):根据方法的签名来匹配

2.@annotation(........):根据注解匹配

6.1execution表达式

execution()是最常用的切点表达式,用来匹配方法,语法为:

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

其中访问修饰限定符和异常可以省略

切点表达式支持通配符表达:

1.*:匹配任意字符,只匹配一个元素(返回类型,包,类名,方法或者方法参数)

a. 包名使用 * 标识任意包(一层包使用一个 * )

b. 类名使用 * 标识任意类

c. 返回值是用 * 表示任意返回值类型

d.方法名使用 * 表示任意方法

e. 参数使用 * 表示一个任意类型的参数

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

a.使用 .. 配置包名,表示此包以及此包下的所有子包

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

切边表达式示例:

TestController下的public修饰,返回类型为String方法名为t1,无参方法

execution(public String com.example.demo.controller.TestController.t1())

省略访问修饰符

execution(String com.example.demo.controller.TestController.t1())  

 匹配所有返回类型

execution(* com.example.demo.controller.TestController.t1())

 匹配TestController 下的所有无参⽅法

execution(* com.example.demo.controller.TestController.*())

 匹配TestController 下的所有⽅法

execution(* com.example.demo.controller.TestController.*(..))

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

execution(* com.example.demo.controller.*.*(..))

匹配所有包下⾯的TestController

execution(* com..TestController.*(..)) 

 匹配com.example.demo包下, ⼦孙包下的所有类的所有⽅法

execution(* com.example.demo..*(..))

6.2@annotation

execution表达式更适合有规则的,如果我们要匹配多个无规则的方法呢,比如TestController中t1()和UserController中的u1()这两个方法。

这个时候我们使用execution这种切点表达式来描述就不是很方便了

我们可以借助自定义注解的方式以及另一种切点表达式@annotation来描述这一类的切点

实现步骤:

1.编写自定义注解

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

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

测试代码

@RequestMapping("/test")
@Slf4j
@RestController
public class TestController {
    @RequestMapping("/t1")
    public String t1() {
        log.info("执行t1方法......");
        return "t1";
    }

    @RequestMapping("/t2")
    public Boolean t2() {
        log.info("执行t2方法......");
        return true;
    }
}



@RequestMapping("/user")
@RestController
public class UserController {
    @RequestMapping("/u1")
    public String u1() {
        return "u1";
    }

    @RequestMapping("/u2")
    public String u2() {
        return "u2";
    }
}

6.2.1自定义注解@MyAspect

package com.adviser.springaop1.config;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface MyAspect {
}

  1. @Target标识了Annotation所修饰的对象范围,即该注解可以用在什么地方。

      ElementType.TYPE:用于描述类,接口(包括注解类型)或enum声明 

      ElementType.METHOD:修饰方法

      ElementType.PARAMETER:描述参数

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

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

      1.RetentionPolicy.SOURCE:表示注解仅存在于源码中,编译成字节码后会被丢弃。这意味着在运行时无法获取到该注解的信息,只能在编译时使用。比如@SuppressWarnings,以及lombok提供的注解@Data,@slf4j

      2.RetentionPolicy.CLASS:编译时注解。表示注解存在于源代码和字节码中,但在运行时会被丢弃。这意味着在编译时和字节码中可以通过反射获取到该注解的信息,但在实际运行过程中无法获取。通常用于一些框架和工具的注解。

      3.RetentionPolicy.RUNTIME:运行时注解。表示注解存在于源代码,字节码和运行时。这意味着在编译时,字节码中和实际运行中都可以通过反射获取到该注解的信息。通常用于一些需要在运行时处理的注解,如Spring的@Controller,@ResponseBody

6.2.2切面类

使用@annotation切点表达式定义切点,只对@MyAspect生效


@Aspect
@Component
@Slf4j
public class MyAspectDemo {

    @Before("@annotation(com.adviser.springaop1.config.MyAspect)")
    public void before() {
        log.info("MyAspect -> before ...");
    }

    @After("@annotation(com.adviser.springaop1.config.MyAspect)")
    public void after() {
        log.info("MyAspect -> after ...");
    }

}

6.2.3添加自定义注解

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


@MyAspect
@RequestMapping("/u1")
public String u1() {
   return "u1";
}

测试运行一下

 

可以看到切面通知执行了。

7. Spring AOP的实现方式 

1.基于注解Aspect

2.基于自定义注解da

3.基于Spring API(通过xml配置的方式,自从SpringBoot广泛使用之后,着用方式几乎看不到了)

4.基于打完 

8. Spring AOP原理 

Spring AOP是基于动态代理来实现AOP的

JDK动态代理  

1.定义一个接口及其实现类 

2.自定义InvocationHandler并重写invoke方法,在invoke方法中我们会调用目标方法(被代理类的方法)并自定义一些处理逻辑。

3.通过Proxy.newProxyInstance(ClassLoader loader, Class<?>[ ] Interfaces, InvocationHandler h)方法创建代理对象。

定义JDK动态代理类,实现InvocationHandler接口

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;

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 retVal = method.invoke(target, args);
        //代理增强内容
        System.out.println("我是中介代理结束");
        return retVal;
    }
}

 创建一个代理对象并使用

public interface House {
    void rent();
}


public class RealHouse implements House{
    @Override
    public void rent() {
        System.out.println("我是房东,出租房子");
    }
}
public class Main {
    public static void main(String[] args) {
        House target = new RealHouse();
        //创建一个代理类,通过被代理类,被代理实现的接口,方法调用处理器来创建
        House proxy = (House) Proxy.newProxyInstance(target.getClass().getClassLoader(),
                new Class[]{House.class},
                new JDKInvocationHandler(target));
        proxy.rent();
    }
}

1.InvocationHandler

InvocationHandler接口是Java动态代理的关键接口之一,它定义了一个单一方法invoke(), 用于处理被代理对象的方法调用。

public interface InvocationHandler {
 /**
 * 参数说明
 * proxy:代理对象
 * method:代理对象需要实现的⽅法,即其中需要重写的⽅法
 * args:method所对应⽅法的参数
 */
 public Object invoke(Object proxy, Method method, Object[] args)
 throws Throwable;
}

通过实现InnovationHandler接口,可以对被代理对象的方法进行功能增强。

2.Proxy

Proxy类中使用频率最高的方法是:newProxyInstance() 这个方法主要用来生成一个代理对象 

public static Object newProxyInstance(ClassLoader loader,
         Class<?>[] interfaces, InvocationHandler h)  throws IllegalArgumentException{

             //...代码省略

}

 这个方法一共三个参数:        

        Loader:类加载器,用于加载代理对象(确保代理类和被代理类使用的类加载器一致)

        interfaces:被代理类实现的一些接口(这个参数的定义,也决定了JDK动态代理只能代理实现了接口的一些类)

        h:实现了invocationHandler接口对象

CGLIB动态代理

JDK动态代理有一个致命的缺点,就是只能代理实现了接口的类。

有些场景下,我们业务代码是直接实现的。并没有接口定义,为了解决这个问题我们可以用CGLIB动态代理机制来解决。

CGLIB动态代理类实现步骤

1.定义一个类(被代理类)

2.自定义MethodInterceptor并重写intercept方法,intercept用于增强目标方法,和JDK动态代理中的invoke方法类似。

3.通过Enhancer类的create()创建代理类

public class CGLIBMethodInterceptor implements MethodInterceptor {
    private Object target;

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

    @Override
    public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
        System.out.println("我是中介,开始代理");
        Object result = method.invoke(target, args);
        System.out.println("我是中介,代理结束");
        return result;
    }
}

 创建代理类,并使用

public class Main {
    public static void main(String[] args) {
        RealHouse target = new RealHouse();
        RealHouse proxy = (RealHouse) Enhancer.create(target.getClass(), new CGLIBMethodInterceptor(target));
        proxy.rent();
    }
}

1.MethodInterceptor

MethodInterceptor和JDK动态代理中的InvocationHandler类似,他只是定义了一个方法intercept(),用于增强目标方法。

public interface MethodInterceptor extends Callback {
 /**
 * 参数说明:
 * o: 被代理的对象
 * method: ⽬标⽅法(被拦截的⽅法, 也就是需要增强的⽅法)
 * objects: ⽅法⼊参
 * methodProxy: ⽤于调⽤原始⽅法
 */
 Object intercept(Object o, Method method, Object[] objects, MethodProxy 
methodProxy) throws Throwable;
}

2.  Enhancer.create()

Enhancer.create()用来生成一个代理对象

public static Object create(Class type, Callback callback) {
 //...代码省略
}

参数说明:

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

callback:自定义方法拦截器MethodInterceptor 

JDK和CGLIB代理的区别

JDK代理:只能为实现了接口的类实现代理,需要提供接口。

CGLIB代理可以为任何类生成代理,不要求目标实现接口。

        而Spring AOP 会根据被代理类的情况自适应选择代理方式。如果被代理类实现了一个或多个接口,Spring 将使用 JDK 动态代理;如果被代理类没有实现任何接口,Spring 则会使用 CGLIB 代理。这种机制确保了无论被代理类的结构如何,Spring AOP 都能够有效地为其添加切面逻辑。

9. 总结

1.AOP是一种思想,是对某一类事情的集中处理。Spring框架实现了AOP,称之为Spring AOP

2.Spring AOP常见实现方式有两种:

        1. 基于注解@Aspect来实现

        2.基于自定义注解来实现,还有一些更原始的方式,比如基于代理,基于xml配置,但目标比较少见

3. Spring AOP是基于动态代理实现的,有两种方式:

        1.基本JDK动态代理实现

        2.基于CGLIB动态代理实现

运行时使用哪种方式与项目配置和代理的对象有关。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值