Spring AOP

目录

​🐳今日良言:理想的人生是靠拼来的,许愿到不了

🐳一、AOP

🐯1.基本概念

🐯2.AOP 组成

🐳二、Spring AOP 实现原理

🐼1.JDK 动态代理

🐼2.GCLIB

🐼3.二者的区别 


🐳今日良言:理想的人生是靠拼来的,许愿到不了

🐳一、AOP

🐯1.基本概念

在介绍Spring AOP 之前,首先需要了解一下什么是AOP?

AOP(Aspect Oriented Programming): 面向切面编程,是一种思想,是一种软件开发的编程范式。

在传统的面向对象编程中,程序的功能逻辑被分散在各个对象中,而横切关注点(如日志记录、事务管理、安全控制等)则分散在多个对象之间,导致代码重复、可维护性差,并且难以修改和扩展。AOP 的目标就是解决这些问题。

以用户登录权限的校验,没学 AOP 之前,我们所有需要判断用户登录的页面中的方法,都要实现或调用用户验证的方法,然后有了 AOP 之后,我们只需要在某一处配置一下,所有需要判断用户登录页面(中的方法)就全部可以实现用户登录验证了,不再需要每个方法中都写相同的用户登录校验了。

以 Spring AOP 如下代码为例:

 @RestController
    @RequestMapping("/user")
    public class UserController {
        /**
         * 某⽅法 1
         */
        @RequestMapping("/m1")
        public Object method(HttpServletRequest request) {
            // 有 session 就获取,没有不会创建
            HttpSession session = request.getSession(false);
            if (session != null && session.getAttribute("userinfo") != null) {
                // 说明已经登录,业务处理
                return true;
            } else {
                // 未登录
                return false;
            }
        }
         /**
         * 某⽅法 2
         */
        @RequestMapping("/m2")
        public Object method2(HttpServletRequest request) {
            // 有 session 就获取,没有不会创建
            HttpSession session = request.getSession(false);
            if (session != null && session.getAttribute("userinfo") != null) {
               // 说明已经登录
                return true;
            } else {
                // 未登录
                return false;
            }
        }
        // 其他方法....
从上述代码可以看出,每个⽅法中都有相同的⽤户登录验证权限,它的缺点是:

1. 每个⽅法中都要单独写⽤户登录验证的⽅法,即使封装成公共⽅法,也⼀样要传参调⽤和在⽅法中
进⾏判断。
2. 这些⽤户登录验证的⽅法和接下来要实现的业务⼏何没有任何关联,但每个⽅法中都要写⼀遍。
所以可以提供⼀个公共的 AOP ⽅法来进⾏统⼀的⽤户登录权限验证。

一言以蔽之:AOP 是面向切面编程:面向某一部分做集中处理的编程,如用户登录权限验证等,AOP 是对某一类事情的集中处理。

🐯2.AOP 组成

1). 切面(Aspect) 

某一方面的具体内容就是一个切面,比如用户登录校验就是一个“切面”,而日志的统计记录又是一个“切面”。

切面是包含了:切点,通知的类,相当于 AOP 实现某个功能的集合。

2).切点(Pointcut)

方法

定义了一个拦截规则

切点相当于保存了众多连接点的一个集合(如果把切点看成一个表,而连接点就是表中的一条条数据)

3).通知(Advice)

方法具体实现代码

切面的工作(要做的事)被称为通知。

Spring 切⾯类中,可以在⽅法上使⽤以下注解,会设置⽅法为通知⽅法,在满⾜条件后会通知本方法进行调用,通知主要有五种:

前置通知使⽤ @Before:通知⽅法会在⽬标⽅法调⽤之前执⾏。
后置通知使⽤ @After:通知⽅法会在⽬标⽅法返回或者抛出异常后调⽤。 

返回通知使⽤ @AfterReturning:通知⽅法会在⽬标⽅法返回后调⽤。
异常通知使⽤ @AfterThrowing:通知⽅法会在⽬标⽅法抛出异常后调⽤。
环绕通知使⽤ @Around:通知包裹了被通知的⽅法,在被通知的⽅法通知之前和调⽤之后执⾏⾃定义的⾏为 

4).连接点(Join Point)

所有可能触发切点的点。

5).织入(Weaving)

代理的生成时机

主要有三个时机:

编译期:切⾯在⽬标类编译时被织⼊。这种⽅式需要特殊的编译器。AspectJ的织⼊编译器就是以这种⽅式织⼊切⾯的。

类加载期:切⾯在⽬标类加载到JVM时被织⼊。这种⽅式需要特殊的类加载器(ClassLoader),它可以在⽬标类被引⼊应⽤之前增强该⽬标类的字节码。AspectJ5的加载时织⼊(load-time weaving. LTW)就⽀持以这种⽅式织⼊切⾯。

运⾏期:切⾯在应⽤运⾏的某⼀时刻被织⼊。⼀般情况下,在织⼊切⾯时,AOP容器会为⽬标对象动态创建⼀个代理对象。SpringAOP就是以这种⽅式织⼊切⾯的 

接下来,学习一下 Spring AOP的使用:

1). 首先,添加Spring AOP框架:

在Maven中央仓库进行搜索:

选择Spring Boot AOP

 

 

2)、创建切面:

package com.example.demo.common;

import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;

/**
 * @author 26568
 * @date 2023-06-27 13:45
 */
@Aspect  // 切面
@Component
public class UserAop {

}

 3)、创建切点,定义一个拦截规则 

 @Pointcut("execution(* com.example.demo.controller.Usercontroller.*(..))")
    public void pointcut() {
        
    }

 

4)、创建通知

 创建前置通知:

   @Before("pointcut()") // 切点名
    public void doBefore() {
        System.out.println("执行了前置通知:"+ LocalDateTime.now());
    }

5)、创建连接点

 创建controller包,然后在该包下创建UserController类:

package com.example.demo.controller;

import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * @author 26568
 * @date 2023-06-27 14:39
 */
@RestController
@RequestMapping("/user")
public class UserController {
    @RequestMapping("/test1")
    public String sayHi() {
        System.out.println("执行了 sayHi 方法");
        return "hello spring aop";
    }
    @RequestMapping("/login")
    public String login() {
        System.out.println("执行了 login 方法");
        return "do user login";
    }
}

然后再controller 包下,创建一个ArticleController类:

package com.example.demo.controller;

import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * @author 26568
 * @date 2023-06-27 14:42
 */
@RestController
@RequestMapping("/art")
public class ArticleController {
    @RequestMapping("/test1")
    public String sayHi() {
        System.out.println("执行了 ArticleController 的sayHi 方法");
        return "article: hello spring boot aop";
    }
}

 启动项目,首先输入url:127.0.0.1:8080/user/test1

 查看控制台打印信息:

 会发现,每次都会执行前置通知,然后执行sayHi 方法。

然后输入URL:127.0.0.1:8080/user/login

 

 也会先执行前置通知,然后执行login方法。

然后输入URL: 127.0.0.1:8080/art/test1


查看控制台打印结果:

 

 多访问几次:

会发现,并没有打印前置通知,这是因为之前配置的拦截规则只拦截UserController类中所有方法,其他的都不会拦截。

 

异常通知和返回通知以及后置通知和前置通知使用类似,就不作介绍了,主要介绍一下比较复杂的环绕通知,代码如下:

  @Around("pointcut()")
    public Object doAround(ProceedingJoinPoint joinPoint) throws Throwable {
        System.out.println("开始执行环绕通知");
        // 执行拦截方法
        Object object = joinPoint.proceed();
        System.out.println("结束环绕通知");
        return object;
    }

 

 

将前置通知的代码先注销掉,只观察环绕通知的效果:

启动项目,输入URL: 127.0.0.1:8080/user/test1

 

 查看控制台打印信息:

 放开前置通知的代码,完整代码如下:

此时,启动项目,输入URL:127.0.0.1:8080/user/test1

 会发现先执行开始环绕通知,然后执行前置通知,再执行后置通知(如果有),最后结束环绕通知。


🐳二、Spring AOP 实现原理

AOP 是一种思想,而Spring AOP 是一个框架,提供了一种对 AOP 思想的实现,他们的关系和IoC  与 DI 类似。

AOP 的实现技术主要有两种:

1).静态代理:

静态代理是一种在编译时就已经确定代理关系的代理方式。在静态代理中,代理类和被代理类都要实现同一个接口或继承同一个父类,代理类中包含了被代理类的实例,并在调用被代理类的方法前后执行相应的操作。

静态代理的优点是实现简单,易于理解和掌握,但是它的缺点是需要为每个被代理类编写一个代理类,当被代理类的数量增多时,代码量会变得很大。

2)动态代理:

动态代理是一种在运行时动态生成代理类的代理方式。

动态代理的优点是可以为多个被代理类生成同一个代理类,从而减少了代码量,但是它的缺点是实现相对复杂,需要了解 Java 反射机制和动态生成字节码的技术。

Spring AOP 是建立在动态代理基础上的,因此 Spring 对 AOP 的支持局域于方法级别的拦截。Sptring AOP 支持JDK Proxy 和 GCLIB 方式实现动态代理(动态代理两种常用实现方法)。

🐼1.JDK 动态代理

JDK 动态代理是一种使用 Java 标准库中的 java.lang.reflect.Proxy 类来实现动态代理的技术。在 JDK 动态代理中,被代理类必须实现一个或多个接口,并通过 InvocationHandler 接口来实现代理类的具体逻辑。

JDK 动态代理通过反射实现动态代理。

JDK 动态代理的优点:实现简单,易于掌握和理解。

JDK 动态代理的缺点:只能代理实现了接口的类,无法代理没有实现接口的类。

代码实现:

import org.example.demo.service.AliPayService;
import org.example.demo.service.PayService;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
 // 动态代理:使⽤JDK提供的api(InvocationHandler、Proxy实现)
 // 此种⽅式实现,要求被代理类必须实现接口
    public class PayServiceJDKInvocationHandler implements InvocationHandler {

        //⽬标对象即就是被代理对象
        private Object target;

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

        //proxy代理对象
        @Override
        public Object invoke(Object proxy, Method method, Object[] args) throw
        s Throwable {
            //1.安全检查
            System.out.println("安全检查");
            //2.记录⽇志
            System.out.println("记录⽇志");
            //3.时间统计开始
            System.out.println("记录开始时间");
            //通过反射调⽤被代理类的⽅法
            Object retVal = method.invoke(target, args);
            //4.时间统计结束
            System.out.println("记录结束时间");
            return retVal;
        }
        public static void main(String[] args) {
            PayService target= new AliPayService();
            //⽅法调⽤处理器
            InvocationHandler handler =
                    new PayServiceJDKInvocationHandler(target);
            //创建⼀个代理类:通过被代理类、被代理实现的接⼝、⽅法调⽤处理器来创建
            PayService proxy = (PayService) Proxy.newProxyInstance(target.getClass().getClassLoader(),
                    new Class[]{PayService.class},
                    handler
            );
            proxy.pay();
        }
    }

🐼2.GCLIB

GCLIB 动态代理是一种使用GCLIB库来实现动态代理的技术。在GCLIB 动态代理中,通过实现代理类的子类来实现动态代理,而不是实现代理类的接口。

GCLIB 动态代理的优点:可以代理没有实现接口的类。

GCLIB 动态代理的缺点:实现相对复杂,需要了解GCLIB 库的使用方法。

代码实现:

import org.springframework.cglib.proxy.Enhancer;
import org.springframework.cglib.proxy.MethodInterceptor;
import org.springframework.cglib.proxy.MethodProxy;
import org.example.demo.service.AliPayService;
import org.example.demo.service.PayService;
import java.lang.reflect.Method;
public class PayServiceCGLIBInterceptor implements MethodInterceptor {
        //被代理对象
        private Object target;

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

        @Override
        public Object intercept(Object o, Method method, Object[] args, Method
                Proxy methodProxy) throws Throwable {
            //1.安全检查
            System.out.println("安全检查");
            //2.记录⽇志
            System.out.println("记录⽇志");
            //3.时间统计开始
            System.out.println("记录开始时间");
            //通过cglib的代理⽅法调⽤
            Object retVal = methodProxy.invoke(target, args);
            //4.时间统计结束
            System.out.println("记录结束时间");
            return retVal;
        }

        public static void main(String[] args) {
            PayService target= new AliPayService();
            PayService proxy= (PayService) Enhancer.create(target.getClass(),n
                    ew PayServiceCGLIBInterceptor(target));
            proxy.pay();
        }
    }

🐼3.二者的区别

1).JDK 动态代理要求代理对象必须实现接口,CGLIB 不要求代理对象实现接口。

2).JDK 动态代理使用java.lang.reflect.Proxy 和 java.lang.reflect.InvocationHandler 来生成代理对象。

      GCLIB 动态代理使用GCLIB 库来生成代理对象。

3).JDK 动态代理生成的代理对象是目标对象的接口实现,

       GCLIB 动态代理生成的代理对象是目标对象的子类。

4).在JDK1.8之前,JDK 动态代理相对 GCLIB 动态代理 性能较低,生成代理对象速度较慢。

       在JDK1.8以及之后,因为JDK 动态代理做了优化,所以它的性能比 GCLIB 要高。


注:在Spring 框架中,既使用了 JDK 动态代理又使用了 GCLIB 动态代理,默认情况下使用的是 JDK 动态代理,但是如果目标对象没有实现接口,就会使用 GCLIB 动态代理。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值