Spring-AOP:开发准备、初识动态代理、使用步骤、

Aop场景

我们构建一个计算器场景进行学习

Calculator接口 (计算器)

package com.jiang.inter;

/**
 * @Title:
 * @author: JiangPeng
 * @Code: No Bug
 */
public interface Calculator {
    public int add(int a,int b);
    public int del(int a,int b);
    public int mul(int a,int b);
    public int div(int a,int b);
}

实现类 MyCalculator

package com.jiang.impl;

import com.jiang.inter.Calculator;

/**
 * @Title:
 * @author: JiangPeng
 * @Code: No Bug
 */
public class MyCalculator implements Calculator {

    public int add(int a, int b) {
        int result = a + b;
        return result;
    }

    public int del(int a, int b) {
        int result = a - b;
        return result;
    }

    public int mul(int a, int b) {
        int result = a * b;
        return result;
    }

    public int div(int a, int b) {
        int result = a / b;
        return result;
    }
}

测试运行Test01

public class Test01 {
    public static void main(String[] args) {
        Calculator calculator = new MyCalculator();
        System.out.println(calculator.add(1,2););
    }
}

很明显 我们此时的结果是输出了1+2的值 也就是3

1.增加日志

那么我们此刻需要增加一个日志功能:即就在方法运行结果的前后 加点日志 提示我们输入的值和结果

方法的内部编写日志记录

public class MyCalculator implements Calculator {

    public int add(int a, int b) {
        System.out.println("【add】方法开始了,它的参数值是【"+a+"和"+b+"】");
        int result = a + b;
        System.out.println("【add】方法运行结束了,它的结果是【"+result+"】");
        return result;
    }

    public int del(int a, int b) {
        System.out.println("【del】方法开始了,它的参数值是【"+a+"和"+b+"】");
        int result = a - b;
        System.out.println("【del】方法运行结束了,它的结果是【"+result+"】");
        return result;
    }

    public int mul(int a, int b) {
        System.out.println("【mul】方法开始了,它的参数值是【"+a+"和"+b+"】");
        int result = a * b;
        System.out.println("【mul】方法运行结束了,它的结果是【"+result+"】");
        return result;
    }

    public int div(int a, int b) {
        System.out.println("【div】方法开始了,它的参数值是【"+a+"和"+b+"】");
        int result = a / b;
        System.out.println("【div】方法运行结束了,它的结果是【"+result+"】");
        return result;
    }
}

此时我们运行Test 看看结果

【add】方法开始了,它的参数值是【1和2】
【add】方法运行结束了,它的结果是【3】

我们在结果的前和后都有了日志打印;

但假如我们此时需要进行一个日志修改,将【】改成[],我们是不是就要一个个进行修改,业务量小看起来好像没问题,但假如我们在企业开发中,多个业务中用到日志输出,那么修改起来会特别麻烦,所以我们肯定是不推荐这种写法的!

这种将日志记录写在代码里,而且当我们需要更改日志记录的某个环节还需要改源代码时,这就叫耦合

抽取成日志工具类

将日志抽取成LogUtil

package com.jiang.utils;

import java.util.Arrays;

/**
 * @Title:
 * @author: JiangPeng
 */
public class LogUtil {
    public static void logStart(Object...objects){
        System.out.println("【xx】方法开始了,它的参数值是【"+ Arrays.asList(objects) +"】");
    }
}

此时的实现类

public class MyCalculator implements Calculator {

    public int add(int a, int b) {
        LogUtil.logStart(a,b);
        int result = a + b;
        System.out.println("【add】方法运行结束了,它的结果是【"+result+"】");
        return result;
    }

    public int del(int a, int b) {
        LogUtil.logStart(a,b);
        int result = a - b;
        System.out.println("【del】方法运行结束了,它的结果是【"+result+"】");
        return result;
    }

    public int mul(int a, int b) {
        LogUtil.logStart(a,b);
        int result = a * b;
        System.out.println("【mul】方法运行结束了,它的结果是【"+result+"】");
        return result;
    }

    public int div(int a, int b) {
        LogUtil.logStart(a,b);
        int result = a / b;
        System.out.println("【div】方法运行结束了,它的结果是【"+result+"】");
        return result;
    }
}

当我们运行结果:

【xx】方法开始了,它的参数值是【[1, 2]】
【add】方法运行结束了,它的结果是【3】

可以看到,我们无法获取到具体的方法名,所以这个方法兼容性问题需要考虑,其实我们只不过把System.out.println的方法换成了调用LogUtil的方法罢了,所以还是耦合!


2.动态代理

我们期望在不修改业务逻辑代码的前提下,日志模块在核心功能运行期间,自己动态的加上。

将实现类代码复原

public class MyCalculator implements Calculator {

    public int add(int a, int b) {
        int result = a + b;
        return result;
    }

    public int del(int a, int b) {
        int result = a - b;
        return result;
    }

    public int mul(int a, int b) {
        int result = a * b;
        return result;
    }

    public int div(int a, int b) {
        int result = a / b;
        return result;
    }
}

我们可以看到我们的在运行业务代码的时候都是对象本身直接去调用业务代码。

我们希望有一个代理对象可以替我们去间接的执行我们的业务代码,但是可以在执行的前后做点别的事

Calculator proxy = new CalculatorPorxy.getProxy(calculator);
proxy.add(2,1);

当proxy.add的时候其实是proxy调用的calculator.add,那么我们是不是可以不修改calculator原本业务代码,但我们在proxy中添加输出日志记录的功能

//本来是proxy.add => proxy.calculator.add
proxy{
    System.....("日志....结果"+a+"..");
    calculator{
        add();
        del();
    }
    System.....("日志....结束"+result+"..");
}

CalculatorPorxy动态代理类

  1. 方法getProxy的参数值 Calculator calculator:传入的被代理对象
  2. 我们创建代理对象是通过JDK的Proxy.newProxyInstance()
  3. Proxy.newProxyInstance(classLoader, classes, invocationHandler);创建好以后这三个参数分别是
  4. classLoader:被代理对象的类加载器
  5. classes:被代理对象所实现的所有接口
  6. invocationHandler:方法执行器,帮我们目标对象执行目标方法 使用起来需要new然后有个匿名实现
  7. 匿名实现invoke,参数值 Object proxy:代理对象:给JDK使用,我们任何时候不要动这个对象
  8. 匿名实现invoke,参数值 Method method:当前将要执行的目标对象的方法。
  9. 匿名实现invoke,参数值 Object[] args:这个方法调用时外界传入的参数值
  10. 在invoke内部我们要利用反射执行目标方法,method.invoke(calculator, args); 返回值 Object result
  11. calculator:执行的代理对象;在这里如果需要在内部类里用一个声明的参数,需要将这个参数设置final,即getProxy方法中的参数值变为 getProxy(final Calculator calculator)
  12. result:目标方法执行后的返回值,返回值必须返回出去,外界才能拿到真正执行后的返回值
public class CalculatorPorxy {
    public static Calculator getProxy(final Calculator calculator){
        /*
           为传入的参数创建一个动态代理对象
           Calculator calculator:传入的被代理对象
           创建代理对象通过JDK的Proxy.newProxyInstance()
         */
        
        //被代理对象的类加载器
        ClassLoader classLoader = calculator.getClass().getClassLoader();
        
        //被代理对象所实现的所有接口
        Class<?>[] classes = calculator.getClass().getInterfaces();

        InvocationHandler invocationHandler = new InvocationHandler() {
            /*
                Object proxy:代理对象:给JDK使用,我们任何时候不要动这个对象
                Method method:当前将要执行的目标对象的方法。
                Object[] args:这个方法调用时外界传入的参数值
             */
            public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {

                //利用反射执行目标方法
                //calculator:执行的代理对象;在这里如果需要在内部类里用一个声明的参数,需要将这个参数设置final
                //result:目标方法执行后的返回值
                Object result = method.invoke(calculator, args);
                return result;//返回值必须返回出去,外界才能拿到真正执行后的返回值
            }
        };//方法执行器,帮我们目标对象执行目标方法

        //proxy为目标对象创建代理对象
        Object proxy = Proxy.newProxyInstance(classLoader, classes, invocationHandler);
        return (Calculator) proxy;
    }


}

接下来用在日志中,就是在我们invoke方法调用前后添加输出日志语句

public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {

    System.out.println("【"+method.getName()+"】方法执行开始,该方法的参数值是【"+ Arrays.asList(args)+"】");
    Object result = method.invoke(calculator, args);
    System.out.println("【"+method.getName()+"】方法执行结束了");
    
    return result;
}

method.getName:通过反射获取方法的名称

Arrays.asList(args):将外界参数值变成数组

此时我们测试为

public static void main(String[] args) {
    Calculator calculator = new MyCalculator();
    //calculator.add(1,2);

    Calculator proxy = CalculatorPorxy.getProxy(calculator);
    proxy.add(1,2);

}

此时我们没有修改任何业务的代码我们就可以实现日志的功能

优点:我们可以使用动态代理将日志代码动态的在目标方法前后进行执行;

缺点:1.写起来太难了,我们此时只是对一个简单的计算器进行动态代理就已经很麻烦了,更何况在实际的开发中肯定要比现在更加复杂。

2.jdk默认的动态代理如果对象没有实现任何的接口,是无法为他创建代理对象的。因为代理对象和被代理对象唯一能产生关联的就是实现了同一个接口
Class<?>[] classes = calculator.getClass().getInterfaces();

3.Aop产生

所以Spring为了解决动态代理缺点这个难题,实现了AOP,底层还是动态代理,但没有强制需求对象必须有接口!大大简化了使用难度,以后与动态代理也就拜拜了

Aop专业名词

image-20200410204944752

  • 横切关注点:每一个方法开始都加了日志,我们是横向来看,叫横切关注点。
  • 通知方法:我们调每一个方法开始前都调了一个方法进行日志记录,叫通知方法 LogUtil.begin();
  • 切面类:就是通知方法的工具类:LogUtil
  • 连接点:十字交叉的位置,每一个方法的每一个位置都是一个连接点,上图一共16个
  • 切入点:真正需要执行日志记录的地方
  • 切入点表达式:在众多连接点中找出感兴趣的地方

Aop使用步骤

1.导包

<!-- https://mvnrepository.com/artifact/org.springframework/spring-aop -->
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-aop</artifactId>
    <version>5.2.4.RELEASE</version>
</dependency>
        <dependency>
            <groupId>org.aspectj</groupId>
            <artifactId>aspectjweaver</artifactId>
            <version>1.9.5</version>
        </dependency>

2.写配置

1.将目标类和切面类加入到 ioc容器中。切面类( LogUtil,封装了通知方法的类 )

给MyCalculator加@service,给LogUtil加@Component

2.告诉Spring那个是切面类(因为Spring不区分你这些注解的具体分类)

新的注解 @Aspect

3.告诉Spring切面类中的每一个方法都是何时运行

@Before:在目标方法之前运行,前置通知

@After:在目标方法结束之后,后置通知

@AfterReturning:在目标方法正常返回之后,返回通知

@AfterThrowing:在目标方法抛出异常之后运行,异常通知

@Around:环绕,环绕通知

try{
    @Before
    method.invoke(obj,args);
	@AfterReturning
}catch(e){
    @AfterThrowing
}finally{
    @After
}

4.开启基于注解的AOP功能

<aop:aspectj-autoproxy></aop:aspectj-autoproxy>

LogUtil类

@Aspect
@Component
public class LogUtil {
    //想在执行目标之前运行,写入切入点表达式
    //execution(访问权限符 返回值类型 方法签名)
    @Before("execution(public int com.jiang.impl.MyCalculator.add(int,int))")
    public static void logStart(){
        System.out.println("【】方法开始前,参数值是【】");
    }
}

execution(public int com.jiang.impl.MyCalculator.add(int,int))

execution(访问权限符 返回值类型 方法签名)

3.进行测试

注意从ioc容器中拿到目标对象,如果想通过类型获取,一定要用它的接口类型,不要用本类

public class Test {
    public static void main(String[] args) {
        ApplicationContext app = new ClassPathXmlApplicationContext("applicationContext.xml");

        Calculator bean = app.getBean(Calculator.class);
        bean.add(1,2);
    }
}
<?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.jiang"></context:component-scan>

    <aop:aspectj-autoproxy></aop:aspectj-autoproxy>

</beans>

原文地址:https://www.cnblogs.com/pengcode/p/12576009.html