Spring AOP

代理

二十三种设计模式中的一种,属于结构型模式。它的作用就是通过提供一个代理类,让我们在调用目标方法的时候,不再是直接对目标方法进行调用,而是通过代理类间接调用。让不属于目标方法核心逻辑的代码从目标方法中剥离出来——解耦。调用目标方法时先调用代理对象的方法,减少对目标方法的调用和打扰,同时让附加功能能够集中在一起也有利于统一维护。 

先来看一个需求:
设计一个计算器程序,要求在计算的前后输出日志

先定义一个接口

package com.atguigu.spring6.aop.annoaop;

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 com.atguigu.spring6.aop.annoaop;

import org.springframework.stereotype.Component;

//基本实现类
@Component
public class CalculatorImpl implements Calculator {

    @Override
    public int add(int i, int j) {

        int result = i + j;

        System.out.println("方法内部 result = " + result);

        return result;
    }

    @Override
    public int sub(int i, int j) {

        int result = i - j;

        System.out.println("方法内部 result = " + result);

        return result;
    }

    @Override
    public int mul(int i, int j) {

        int result = i * j;

        System.out.println("方法内部 result = " + result);

        return result;
    }

    @Override
    public int div(int i, int j) {

        int result = i / j;

        System.out.println("方法内部 result = " + result);

        return result;
    }
}

在程序里面设计一个在计算前后输出日志的功能
 

package com.atguigu.spring6.aop.example;

//带日志
public class CalculatorLogImpl implements Calculator{

    @Override
    public int add(int i, int j) {

        System.out.println("[日志] add 方法开始了,参数是:" + i + "," + j);

        int result = i + j;

        System.out.println("方法内部 result = " + result);

        System.out.println("[日志] add 方法结束了,结果是:" + result);

        return result;
    }

    @Override
    public int sub(int i, int j) {

        System.out.println("[日志] sub 方法开始了,参数是:" + i + "," + j);

        int result = i - j;

        System.out.println("方法内部 result = " + result);

        System.out.println("[日志] sub 方法结束了,结果是:" + result);

        return result;
    }

    @Override
    public int mul(int i, int j) {

        System.out.println("[日志] mul 方法开始了,参数是:" + i + "," + j);

        int result = i * j;

        System.out.println("方法内部 result = " + result);

        System.out.println("[日志] mul 方法结束了,结果是:" + result);

        return result;
    }

    @Override
    public int div(int i, int j) {

        System.out.println("[日志] div 方法开始了,参数是:" + i + "," + j);

        int result = i / j;

        System.out.println("方法内部 result = " + result);

        System.out.println("[日志] div 方法结束了,结果是:" + result);

        return result;
    }
}

①现有代码缺陷

针对带日志功能的实现类,我们发现有如下缺陷:

  • 对核心业务功能有干扰,导致程序员在开发核心业务功能时分散了精力
  • 附加功能分散在各个业务功能方法中,不利于统一维护

②解决思路

解决这两个问题,核心就是:解耦。我们需要把附加功能从业务功能代码中抽取出来。

③困难

解决问题的困难:要抽取的代码在方法内部,靠以前把子类中的重复代码抽取到父类的方式没法解决。所以需要引入新的技术。

 

在没有使用代理之前,我们调用的逻辑是这样的:
 

下面我们要使用代理,代理的调用逻辑是这样的: 

 

使用代理以后得代码

package com.atguigu.spring6.aop.example;

public class CalculatorStaticProxy implements Calculator{

    //被代理目标对象传递过来
    private Calculator calculator;
    public CalculatorStaticProxy(Calculator calculator) {
        this.calculator = calculator;
    }

    @Override
    public int add(int i, int j) {
        //输出日志
        System.out.println("[日志] add 方法开始了,参数是:" + i + "," + j);

        //调用目标对象的方法实现核心业务
        int addResult = calculator.add(i, j);

        System.out.println("[日志] add 方法结束了,结果是:" + addResult);
        return addResult;
    }

    @Override
    public int sub(int i, int j) {
        return 0;
    }

    @Override
    public int mul(int i, int j) {
        return 0;
    }

    @Override
    public int div(int i, int j) {
        return 0;
    }
}

上面就是使用了代理技术,不过使用的是静态代理。

静态代理确实实现了解耦,但是由于代码都写死了,完全不具备任何的灵活性。就拿日志功能来说,将来其他地方也需要附加日志,那还得再声明更多个静态代理类,那就产生了大量重复的代码,日志功能还是分散的,没有统一管理。

提出进一步的需求:将日志功能集中到一个代理类中,将来有任何日志需求,都通过这一个代理类来实现。这就需要使用动态代理技术了。

 

动态代理的过程:
 

实现代码

package com.atguigu.spring6.aop.example;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.Arrays;

public class ProxyFactory {

    //目标对象
    private Object target;
    public ProxyFactory(Object target) {
        this.target = target;
    }

    //返回代理对象
    public Object getProxy() {
        /**
         * Proxy.newProxyInstance()方法
         * 有三个参数
         * 第一个参数:ClassLoader: 加载动态生成代理类的来加载器
         * 第二个参数: Class[] interfaces:目录对象实现的所有接口的class类型数组
         * 第三个参数:InvocationHandler:设置代理对象实现目标对象方法的过程
         */
        //第一个参数:ClassLoader: 加载动态生成代理类的来加载器
        ClassLoader classLoader = target.getClass().getClassLoader();
        //第二个参数: Class[] interfaces:目录对象实现的所有接口的class类型数组
        Class<?>[] interfaces = target.getClass().getInterfaces();
        //第三个参数:InvocationHandler:设置代理对象实现目标对象方法的过程
        InvocationHandler invocationHandler =new InvocationHandler() {

            //第一个参数:代理对象
            //第二个参数:需要重写目标对象的方法
            //第三个参数:method方法里面参数
            @Override
            public Object invoke(Object proxy,
                                 Method method,
                                 Object[] args) throws Throwable {

                //方法调用之前输出
                System.out.println("[动态代理][日志] "+method.getName()+",参数:"+ Arrays.toString(args));

                //调用目标的方法
                Object result = method.invoke(target, args);

                //方法调用之后输出
                System.out.println("[动态代理][日志] "+method.getName()+",结果:"+ result);
                return result;
            }
        };
        return Proxy.newProxyInstance(classLoader,interfaces,invocationHandler);
    }
}
package com.atguigu.spring6.aop.example;

public class TestCal {

    public static void main(String[] args) {
        //创建代理对象(动态)
        ProxyFactory proxyFactory = new ProxyFactory(new CalculatorImpl());
        Calculator proxy = (Calculator)proxyFactory.getProxy();
        //proxy.add(1,2);
        proxy.mul(2,4);
    }
}

 

 

 

 

AOP

AOP(Aspect Oriented Programming)是一种面向切面编程的思想,切面指的是系统的某一个方面。AOP是针对程序中的某一类(某个方面)的功能做统一处理,比如用户登录权限的效验。Spring AOP是一个框架,提供了AOP思想的实现,AOP和Spring AOP的关系就像IoC和DI。

AOP可以实现一下功能:

统一日志记录、统一方法执行时间统计、统一格式返回格式设置、统一的异常处理、事务的开启和提交等,AOP编程是对OOP编程的补充和完善。

在登录授权中AOP的作用:

AOP的组成 

AOP主要由四点组成,分别是:切面、切点、连接点、通知

切面 

切面由切点和通知组成,它是针对某一个功能的具体定义,这个功能可能是登录验证功能,也有可能是日志记录功能,这里可以将AOP形象的比喻为一个数据库,一个AOP就是一个数据库。

切点 

切点是一个保存了众多连接点的一个集合,切点是切面中的某个方法,如果将切面比作数据库,那么切点就是数据库里面的表。

通知 

我们将切面必须完成工作称为通知,切面的工作就是通知。通知有五种:前置通知(使用@Before)、后置通知(使用@After)、返回之后通知(使用@AfterReturning)、抛出异常通知(使用AfterThrowing)、环绕通知(使用@Around)

连接点

所有可能触发AOP(拦截方法的点)就称之为连接点

上面AOP的组成可以用一张图来表示:

Spring AOP的实现 

基于注解的方式实现

Spring AOP的实现步骤如下:

1.添加Spring AOP框架支持;

2.定义切面和切点;

3.定义通知;

添加Spring AOP框架支持 

在pom.xml文件中插入以下框架 

        <!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-aop -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-aop</artifactId>
        </dependency>
定义切面和切点 

@Aspect //定义切面
@Component
public class UserAspect {
    //切点(配置拦截规则)
    @Pointcut("execution(* com.example.myblog.controller.UserController.*(..))")
    public void pointcut(){
    }
}

对上面代码的解释:

@Aspect这个注解表示这个类是一个AOP类,也就是说这个类是一个切面;

@Component这个注解表示组件,当我们不知道具体该给某个类使用那个注解来注入到Spring框架时,我们就可以使用@Component这个注解;

@Pointcut这个注解表示该方法是一个切点,它后面跟的是AspectJ表达式(切点表达式)。该表达式的语法:execution(<修饰符><返回类型><包.类.方法(参数)><异常>)

上面的包和类一般情况下是要有的,但可以省略;异常可省略一般不写。 

AspectJ支持三种通配符:

*:匹配任意字符,只匹配一个元素(这个元素可以是包、类、或者方法、

方法参数)。

..:匹配任意字符,可以匹配多个元素,在表示类时,必须和*联合使用。 

+:表示按照类型匹配指定类的所有类,必须跟在类名后面,如com.cad.Car+,表示继承该类的所有子类包括本身。

表达式示例:

execution(* addUser(String,int)):匹配addUser方法,且第一个参数类型是String,第二个参数是int。

excution(* com.cad.demo.User.*(..)):匹配User类里面的所有方法。

excution(* com.cad.demo.User+.*(..)):匹配该类的子类包括该类的所有方法。

excution(* com.cad.*.*(..):匹配com.cad包下面的所有类的所有方法。

excution(* com.cad..*.*(..)):匹配com.cad包下、子孙包下所有类的所有方法。

定义通知(在什么时机执行什么方法)

通知定义的是被拦截的方法具体要执行的业务,比如用户登录权限验证就是具体要执行的业务。Spring AOP中,可以在方法上使用以下注解,会设置方法为通知方法,在满足条件后会通知本方法进行调用:

前置通知使用@Before:通知方法会在目标方法执行调用之前执行。

后置通知使用@After:通知方法会在目标方法返回或者抛出异常后调用。

返回之后通知使用@AfterRuturning:通知方法会在目标方法返回后调用。

抛出异常后通知使用@AfterThrowing:通知方法会在目标方法抛出异常后调用。

环绕通知使用@Around:通知包裹了被通知的方法,在被通知的方法通知之前和调用之后执行自定义的行为。 

执行前置通知:  

@Aspect //定义切面
@Component
public class UserAspect {
    //切点(配置拦截规则)
    @Pointcut("execution(* com.example.myblog.controller.UserController.*(..))")
    public void pointcut(){
    }

    //前置通知
    @Before("pointcut()")
    public void doBefore(){
        //业务代码...
        System.out.println();
        System.out.println("执行了前置通知");
        System.out.println();
    }
}

  

演示结果:

执行后置通知:

@Aspect //定义切面
@Component
public class UserAspect {
    //切点(配置拦截规则)
    @Pointcut("execution(* com.example.myblog.controller.UserController.*(..))")
    public void pointcut(){
    }

    //后置通知
    @After("pointcut()")
    public void doAfter(){
        //业务代码...
        System.out.println();
        System.out.println("执行了后置通知");
        System.out.println();
    }
}

 

执行返回之后通知:

@Aspect //定义切面
@Component
public class UserAspect {
    //切点(配置拦截规则)
    @Pointcut("execution(* com.example.myblog.controller.UserController.*(..))")
    public void pointcut(){
    }

    //前置通知
    @Before("pointcut()")
    public void doBefore(){
        //业务代码...
        System.out.println();
        System.out.println("执行了前置通知");
        System.out.println();
    }

    //后置通知
    @After("pointcut()")
    public void doAfter(){
        //业务代码...
        System.out.println();
        System.out.println("执行了后置通知");
        System.out.println();
    }


    //return之前通知
    @AfterReturning("pointcut()")
    public void doAfterReturning(){
        //业务代码...
        System.out.println();
        System.out.println("执行了doAfterReturning通知");
        System.out.println();
    }
}

可以看到执行返回之后通知它是在返回的时候之后调用的,在返回通知之前调用。 

执行抛出异常后通知:

@Aspect //定义切面
@Component
public class UserAspect {
    //切点(配置拦截规则)
    @Pointcut("execution(* com.example.myblog.controller.UserController.*(..))")
    public void pointcut(){
    }

    //前置通知
    @Before("pointcut()")
    public void doBefore(){
        //业务代码...
        System.out.println();
        System.out.println("执行了前置通知");
        System.out.println();
    }

    //后置通知
    @After("pointcut()")
    public void doAfter(){
        //业务代码...
        System.out.println();
        System.out.println("执行了后置通知");
        System.out.println();
    }


    //return之前通知
    @AfterReturning("pointcut()")
    public void doAfterReturning(){
        //业务代码...
        System.out.println();
        System.out.println("执行了doAfterReturning通知");
        System.out.println();
    }

    //抛出异常之前通知
    @AfterThrowing("pointcut()")
    public void doAfterThrowing(){
        //业务代码...
        System.out.println();
        System.out.println("执行了doAfterThrowing通知");
        System.out.println();
    }
}

  

执行环绕通知:

    //添加环绕通知
    @Around("pointcut()")
    public Object doAround(ProceedingJoinPoint joinPoint){
        Object o = null;
        System.out.println("Around 方法开始执行");
        try {
            o = joinPoint.proceed();
        } catch (Throwable e) {
            e.printStackTrace();
        }
        System.out.println("Around方法结束执行");
        return o;
    }

可以看到环绕通知是在所有通知方法之前和之后调用,因此可以用来统计方法的执行时间。 

@Aspect //定义切面
@Component
public class UserAspect {
    //切点(配置拦截规则)
    @Pointcut("execution(* com.example.myblog.controller.UserController.*(..))")
    public void pointcut(){
    }

    //添加环绕通知
    @Around("pointcut()")
    public Object doAround(ProceedingJoinPoint joinPoint){
        Object o = null;
        StopWatch stopWatch = new StopWatch();
        try {
            stopWatch.start();
            o = joinPoint.proceed();
            stopWatch.stop();
        } catch (Throwable e) {
            e.printStackTrace();
        }

        System.out.println(joinPoint.getSignature().getDeclaringTypeName()+"."+
                joinPoint.getSignature().getName()+" 方法执行花费的时间: "+
                stopWatch.getTotalTimeMillis()+"ms");
        return o;
    }
}

基于XML的方式实现 

package com.atguigu.spring6.aop.xmlaop;

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 com.atguigu.spring6.aop.xmlaop;

import org.springframework.stereotype.Component;

//基本实现类
@Component
public class CalculatorImpl implements Calculator {

    @Override
    public int add(int i, int j) {

        int result = i + j;

        System.out.println("方法内部 result = " + result);

        //为了测试,模拟异常出现
       // int a = 1/0;
        return result;
    }

    @Override
    public int sub(int i, int j) {

        int result = i - j;

        System.out.println("方法内部 result = " + result);

        return result;
    }

    @Override
    public int mul(int i, int j) {

        int result = i * j;

        System.out.println("方法内部 result = " + result);

        return result;
    }

    @Override
    public int div(int i, int j) {

        int result = i / j;

        System.out.println("方法内部 result = " + result);

        return result;
    }
}
package com.atguigu.spring6.aop.xmlaop;

import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;

import java.util.Arrays;

//切面类
@Component //ioc容器
public class LogAspect {

    //前置通知
    public void beforeMethod(JoinPoint joinPoint) {
        String methodName = joinPoint.getSignature().getName();
        Object[] args = joinPoint.getArgs();
        System.out.println("Logger-->前置通知,方法名称:"+methodName+",参数:"+Arrays.toString(args));
    }

    // 后置通知
    public void afterMethod(JoinPoint joinPoint) {
        String methodName = joinPoint.getSignature().getName();
        System.out.println("Logger-->后置通知,方法名称:"+methodName);
    }

    // 返回通知,获取目标方法的返回值
    public void afterReturningMethod(JoinPoint joinPoint,Object result) {
        String methodName = joinPoint.getSignature().getName();
        System.out.println("Logger-->返回通知,方法名称:"+methodName+",返回结果:"+result);
    }

    // 异常通知 获取到目标方法异常信息
    //目标方法出现异常,这个通知执行
    public void afterThrowingMethod(JoinPoint joinPoint,Throwable ex) {
        String methodName = joinPoint.getSignature().getName();
        System.out.println("Logger-->异常通知,方法名称:"+methodName+",异常信息:"+ex);
    }

    // 环绕通知
    public Object aroundMethod(ProceedingJoinPoint joinPoint) {
        String methodName = joinPoint.getSignature().getName();
        Object[] args = joinPoint.getArgs();
        String argString = Arrays.toString(args);
        Object result = null;
        try {
            System.out.println("环绕通知==目标方法之前执行");

            //调用目标方法
            result = joinPoint.proceed();

            System.out.println("环绕通知==目标方法返回值之后");
        }catch (Throwable throwable) {
            throwable.printStackTrace();
            System.out.println("环绕通知==目标方法出现异常执行");
        } finally {
            System.out.println("环绕通知==目标方法执行完毕执行");
        }
        return result;
    }

    //重用切入点表达式
    @Pointcut(value = "execution(* com.atguigu.spring6.aop.xmlaop.CalculatorImpl.*(..))")
    public void pointCut() {}
}
<?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
       http://www.springframework.org/schema/context/spring-context.xsd
       http://www.springframework.org/schema/aop
       http://www.springframework.org/schema/aop/spring-aop.xsd">

    <!--开启组件扫描-->
    <context:component-scan base-package="com.atguigu.spring6.aop.xmlaop"></context:component-scan>

    <!--配置aop五种通知类型-->
    <aop:config>
        <!--配置切面类-->
        <aop:aspect ref="logAspect">
            <!--配置切入点-->
            <aop:pointcut id="pointcut" expression="execution(* com.atguigu.spring6.aop.xmlaop.CalculatorImpl.*(..))"/>
            <!--配置五种通知类型-->
            <!--前置通知-->
            <aop:before method="beforeMethod" pointcut-ref="pointcut"></aop:before>

            <!--后置通知-->
            <aop:after method="afterMethod" pointcut-ref="pointcut"></aop:after>

            <!--返回通知-->
            <aop:after-returning method="afterReturningMethod" returning="result" pointcut-ref="pointcut"></aop:after-returning>

            <!--异常通知-->
            <aop:after-throwing method="afterThrowingMethod" throwing="ex" pointcut-ref="pointcut"></aop:after-throwing>

            <!--环绕通知-->
            <aop:around method="aroundMethod" pointcut-ref="pointcut"></aop:around>
        </aop:aspect>
    </aop:config>
</beans>
package com.atguigu.spring6.aop.xmlaop;

import org.junit.jupiter.api.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

public class TestAop {

    @Test
    public void testAdd() {
        ApplicationContext context =
                new ClassPathXmlApplicationContext("beanaop.xml");
        Calculator calculator = context.getBean(Calculator.class);
        calculator.add(4,3);
    }
}

 

 

切面的优先级 

相同目标方法上同时存在多个切面时,切面的优先级控制切面的内外嵌套顺序。

  • 优先级高的切面:外面
  • 优先级低的切面:里面

使用@Order注解可以控制切面的优先级:

  • @Order(较小的数):优先级高
  • @Order(较大的数):优先级低

 

 

 

场景:日志记录 

记录用户的登录和退出:

//定义切面
@Aspect
@Component
public class UserAspect {
    /**
     *  定义切点,切点是一个包含了众多拦截点的一个集合
     * @param joinPoint 所有需要aop处理的方法都称为连接点
     */
    //配置代理规则
    @Before("execution(* com.example.demo.controller.UserController.*(..))")
    public void logBefore(JoinPoint joinPoint) {
        String methodName = joinPoint.getSignature().getName();
        String className = joinPoint.getTarget().getClass().getSimpleName();
        System.out.println("Executing " + className + "." + methodName + "()");
        // 记录日志到日志文件或数据库
    }

}

用户的登录和注销操作在UserController类里面,当用户请求登录和注销操作时:

Spring AOP实现原理 

Spring AOP是构建在动态代理基础上的,因此Spring对AOP的支持局限于方法级别的拦截。Spring AOP支持JDK Proxy和CGLIB方式实现动态代理,而这两类方式底层都是通过反射来实现的。 

动态代理分两种,基于接口的动态代理和基于类的动态代理。

基于接口的动态代理 

基于接口的动态代理使用Java的反射机制,在运行时动态地创建代理对象。代理对象实现了与原始对象相同的接口,并将方法调用转发给原始对象,同时还可以在方法调用前后执行其他逻辑。使用`java.lang.reflect.Proxy`类和`java.lang.reflect.InvocationHandler`接口可以实现基于接口的动态代理。 

 

基于类的动态代理 

基于类的动态代理是通过继承或扩展来实现的。在运行时,它创建一个类来继承原始类,并覆盖其中的方法以添加额外的业务逻辑。使用第三方库,如CGLIB(Code Generation Library)可以实现基于类的动态代理。 

 

AspectJ 

AspectJ是AOP的框架, Spring依赖AspectJ的注解实现AOP的功能,AspectJ本质是一个静态代理。SpringAOP属于运行时增强,而AspectJ是编译时增强,SpringAOP基于代理,而AspectJ基于字节码操作。

 

  • 4
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 5
    评论
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

咸鱼吐泡泡

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值