Spring AOP -- 简介

一、AOP简介

1、AOP概念

AOP(Aspect Oreriented Programming)面向切面编程
OOP(Object Oreriented Programming)面向对象编程
面向切面编程:基于OOP新的编程思想,指在程序运行期间,将某段代码动态切入到指定方法的指定位置进行运行的编程方式

2、场景

计算器在计算的时候进行日志记录

2.1、创建一个AOP工程

在这里插入图片描述

2.2、实验

2.2.1、创建一个Calculator接口

创建加减乘除四个运算方法

package com.fxp;

public interface Calculator {

//定义加减乘除四个方法
public int add(int i,int j);
public int sub(int i,int j);
public int mul(int i,int j);
public int div(int i,int j);
}

2.2.2、定义一个实现类

package com.fxp.impl;
import com.fxp.inter.Calculator;
public class MyCalculator implements Calculator {
@Override
public int add(int i, int j) {
    int result = i+j;
    return result;
    }

@Override
    public int sub(int i, int j) {
    int result = i-j;
    return result;
}
@Override
public int mul(int i, int j) {
    int result = i*j;
    return result;
}

@Override
public int div(int i, int j) {
    int result = i/j;
    return result;
}
}

继承calculator接口,并重写它的方法。

2.2.3、定义一个测试类

package com.fxp.test;
import com.fxp.impl.MyCalculator;
import com.fxp.inter.Calculator;
import org.junit.Test;
import static org.junit.Assert.*;

public class AopTest {
    @Test
public void test(){
    Calculator calculator = new MyCalculator();
    calculator.add(1,2);
    calculator.sub(2,1);
    calculator.mul(2,3);
    calculator.div(4,2);
}

}

运行结果什么都没有显示,我们的目的是加一个日志记录

2.2.4、加日志记录

2.2.4.1、第一种:直接编写在方法内部,不推荐,修改维护很麻烦

在每一个方法的内部输出一句话:

package com.fxp.impl;
import com.fxp.inter.Calculator;
public class MyCalculator implements Calculator {
//在每个方法开始之前和结束之后都加入日志信息
@Override
public int add(int i, int j) {
    System.out.println("add方法开始了.....它使用的参数是:"+i+","+j+"");
    int result = i+j;
    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("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("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("div方法结束了.....运行结果是"+result+"");
    return result;
}
}

运行结果:
在这里插入图片描述

这种方式很麻烦,现在只是为一个计算器添加日志记录,如果以后为更加庞大的代码量的工程去添加日志的话,显然是不合适的。
而且,日志记录只是系统的辅助功能,是可有可无的,而加减乘除才是真正的业务逻辑功能,如果把代码全都写到一起,就形成了耦合,这是我们要极力避免的

3、动态代理(第二种加日志的方式)

我们希望的是,业务逻辑是核心功能,日志模块在核心功能运行期间,自己动态的加上。

扫描二维码关注公众号,回复: 12678537 查看本文章

3.1、创建一个帮Calculoator生成代理对象的类

package com.fxp.proxy;
import com.fxp.inter.Calculator;

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

/**
 * 这个类就是帮Calculator生成代理对象的类
 * static Object newProxyInstance(ClassLoader loader, 类<?>[] interfaces, InvocationHandler h)
 * 返回指定接口的代理类的实例,该接口将方法调用分派给指定的调用处理程序。
 */
public class CalculatorProxy {
/**
 * 为传入的参数创建一个代理对象
 * 传入的Calculator calculator:被代理对象
 * 返回:代理对象:return proxy;
 */
//这里getproxy里面传入的参数要加final,是因为下面的InvocationHandler里面要用到calculator这个参数,具体下面有解释
public static Calculator getProxy(final Calculator calculator) {

    //proxy为目标对象创建代理对象
    /**
     * newProxy有三个参数
     * InvocationHandler :方法执行器,帮我们的目标对象执行目标方法
     * Class<?>[] interfaces:目标对象实现了的接口
     * ClassLoader loader:目标对象的类加载器
     */

    Object proxy = Proxy.newProxyInstance(calculator.getClass().getClassLoader(), calculator.getClass().getInterfaces(), new InvocationHandler() {
       
        /**
         * new一个InvocaotionHandler,这是一个匿名内部类,有三个参数
         * Object proxy:代理对象,给jdk使用的,任何时候我们都不要动这个东西
         * Method method:当前将要执行的目标对象的方法
         *  Object[] args:这个方法调用时,外界传入的参数
         */
       
        @Override
        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
            
            //加入日志信息(方法开始前),Arrays.aslist是将args参数显示出来,如果不用array.aslist,控制台会显示其地址值
            System.out.println("["+method.getName()+"]开始执行.....它的参数是:"+ Arrays.asList(args)+"");
            
            
            //利用反射使用method.invoke,执行目标方法
            //method.invoke里面有两个参数:
            //object:这里是calculator,就是执行哪个对象,为哪个对象创建代理对象,就填哪个对象,但是有一个规定:如果要在
            //内部类里面要用一个声明的参数,要加final,所以要在getProxy方法里面的calculator参数前面加final
            //args:目标方法里面的参数
            //返回一个result:这个result就是目标方法执行完后的返回值,也就是加减乘除执行完后的返回值
            Object result = method.invoke(calculator, args);

            //加入日志信息(方法开始后)
            System.out.println("["+method.getName()+"]执行结束.....它的结果是:"+result+"");

            //这个返回值必须返回出去,外界才能拿到真正执行后的返回值
            return result;
        }
    });
    return (Calculator) proxy;

}
}

把原来加减乘除方法中的日志信息全部删除

AOPtest源码:

package com.fxp.test;
import com.fxp.impl.MyCalculator;
import com.fxp.inter.Calculator;
import com.fxp.proxy.CalculatorProxy;
import org.junit.Test;
import static org.junit.Assert.*;

public class AopTest {
    @Test
public void test(){
    Calculator calculator = new MyCalculator();
    calculator.add(1,2);
    calculator.sub(2,1);
    calculator.mul(2,3);
    calculator.div(4,2);

    //分隔符
    System.out.println("===============================================");
    //以前我们都是直接new了一个MyCalculator()的对象,然后用这个对象去调用方法
    //现在我们可以创建一个MyCalculator的代理对象,用这个代理对象去执行加减乘除方法

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

}

}

运行结果:
在这里插入图片描述

3.2、动态代理流程整理

1、创建一个用于生成目标对象(Calculator)的代理对象的类(CalculatorProxy),在这个类里面写上一个方法(getProxy),此方法的返回值是目标对象,然后在这个方法传入目标对象(getProxy(final Calculator calculator)),参数要加一个final的值,原因见下

2、在此方法里面直接用Proxy.newProxyInstance得到一个代理对象,里面有三个参数

·InvocationHandler :方法执行器,帮我们的目标对象执行目标方法
·Class<?>[] interfaces:目标对象实现了的接口
·ClassLoader loader:目标对象的类加载器

其中InvocationHandler是一个匿名内部类,传参的时候,直接new一个InvocationHandler即可,它会重写一个invoke方法,这个invoke方法也有三个参数:

·Object proxy:代理对象,给jdk使用的,任何时候我们都不要动这个东西
·Method method:当前将要执行的目标对象的方法
·Object[] args:这个方法调用时,外界传入的参数

3、利用反射menthod.invoke执行目标方法,invoke方法要传入两个参数:

·object:这里是calculator,就是目标对象,为哪个对象创建代理对象,就填哪个对象。
但是有一个规定:如果要在内部类里面要用一个声明的参数,要加final,因为calculator在最开始的getProxy方法里已经声明过了,InvocationHandler内部类里的invoke方法要	用到这个参数,所以要在getProxy方法里面的calculator参数前面加final
·args:目标方法里面的参数

method.invoke返回一个result:这个result就是目标方法执行完后的返回值,也就是加减乘除执行完后的返回值(这是invoke方法的返回值,并不是里面的参数)

4、将result返回出去,这个result就是真正的方法的返回值,也就是加减乘除方法的返回 值,至此,匿名内部类实现完毕

5、最后,将Proxy.newProxyInstance返回的proxy对象返回出去,这个proxy就是为目标对 象(Calculator)生成的代理对象

6、在测试端调用:

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

3.3、改进

例如这个计算器的例子,可能除法运算的时候会出现异常,所以我们还可以用try-catch

package com.fxp.proxy;
import com.fxp.inter.Calculator;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.Arrays;
/**
 * 这个类就是帮Calculator生成代理对象的类
 * static Object newProxyInstance(ClassLoader loader, 类<?>[] interfaces, InvocationHandler h)
 * 返回指定接口的代理类的实例,该接口将方法调用分派给指定的调用处理程序。
 */
public class CalculatorProxy {
/**
 * 为传入的参数创建一个代理对象
 * 传入的Calculator calculator:被代理对象
 * 返回:代理对象:return proxy;
 */
//这里getproxy里面传入的参数要加final,是因为下面的InvocationHandler里面要用到calculator这个参数,具体下面有解释
public static Calculator getProxy(final Calculator calculator) {

    //proxy为目标对象创建代理对象
    /**
     * newProxyInstance有三个参数
     * InvocationHandler :方法执行器,帮我们的目标对象执行目标方法
     * Class<?>[] interfaces:目标对象实现了的接口
     * ClassLoader loader:目标对象的类加载器
     */

    Object proxy = Proxy.newProxyInstance(calculator.getClass().getClassLoader(), calculator.getClass().getInterfaces(), new InvocationHandler() {

        /**
         * new一个InvocaotionHandler,这是一个匿名内部类,有三个参数
         * Object proxy:代理对象,给jdk使用的,任何时候我们都不要动这个东西
         * Method method:当前将要执行的目标对象的方法
         *  Object[] args:这个方法调用时,外界传入的参数
         */

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


            //利用反射使用method.invoke,执行目标方法
            //menthod.invoke里面有两个参数:
            //object:这里是calculator,就是执行哪个对象,为哪个对象创建代理对象,就填哪个对象,但是有一个规定:如果要在
            //内部类里面要用一个声明的参数,要加final,所以要在getProxy方法里面的calculator参数前面加final
            //args:目标方法里面的参数
            //返回一个result:这个result就是目标方法执行完后的返回值,也就是加减乘除执行完后的返回值
            Object result = null;
            try {
                //加入日志信息(方法开始前),Arrays.aslist是将args参数显示出来,如果不用array.aslist,控制台会显示其地址值
                System.out.println("["+method.getName()+"]开始执行.....它的参数是:"+ Arrays.asList(args)+"");
                result = method.invoke(calculator, args);
                //加入日志信息(方法开始后)
                System.out.println("["+method.getName()+"]正常执行完毕.....,结果是:"+result+"");

            }
            catch(Exception e){
                System.out.println("["+method.getName()+"]出现异常:"+e.toString()+"");
            }
            finally {
                System.out.println("方法最终执行完毕");
            }

            //这个返回值必须返回出去,外界才能拿到真正执行后的返回值
            return result;
        }
    });
    return (Calculator) proxy;

}
}

然后测试一下除法:

Calculator proxy = CalculatorProxy.getProxy(calculator);
proxy.add(1,2);
proxy.div(2,0);

运行结果:
在这里插入图片描述

3.4、继续改进,解耦

创建一个LogUtils类,用于记录日志信息,把原来代理对象类的方法里面的日志信息提取出来封装成一个新的LogUtils类,进行解耦
CalculatorProxy:

package com.fxp.proxy;
import com.fxp.inter.Calculator;
import com.fxp.util.LogUtils;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.Arrays;
/**
 * 这个类就是帮Calculator生成代理对象的类
 * static Object newProxyInstance(ClassLoader loader, 类<?>[] interfaces, InvocationHandler h)
 * 返回指定接口的代理类的实例,该接口将方法调用分派给指定的调用处理程序。
 */
public class CalculatorProxy {
/**
 * 为传入的参数创建一个代理对象
 * 传入的Calculator calculator:被代理对象
 * 返回:代理对象:return proxy;
 */
//这里getproxy里面传入的参数要加final,是因为下面的InvocationHandler里面要用到calculator这个参数,具体下面有解释
public static Calculator getProxy(final Calculator calculator) {

    //proxy为目标对象创建代理对象
    /**
     * newProxyInstance有三个参数
     * InvocationHandler :方法执行器,帮我们的目标对象执行目标方法
     * Class<?>[] interfaces:目标对象实现了的接口
     * ClassLoader loader:目标对象的类加载器
     */

    Object proxy = Proxy.newProxyInstance(calculator.getClass().getClassLoader(), calculator.getClass().getInterfaces(), new InvocationHandler() {

        /**
         * new一个InvocationHandler,这是一个匿名内部类,有三个参数
         * Object proxy:代理对象,给jdk使用的,任何时候我们都不要动这个东西
         * Method method:当前将要执行的目标对象的方法
         *  Object[] args:这个方法调用时,外界传入的参数
         */

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

            //利用反射使用method.invoke,执行目标方法
            //menthod.invoke里面有两个参数:
            //object:这里是calculator,就是执行哪个对象,为哪个对象创建代理对象,就填哪个对象,但是有一个规定:如果要在
            //内部类里面要用一个声明的参数,要加final,所以要在getProxy方法里面的calculator参数前面加final
            //args:目标方法里面的参数
            //返回一个result:这个result就是目标方法执行完后的返回值,也就是加减乘除执行完后的返回值
            Object result = null;
            try {
                //加入日志信息(方法开始前),Arrays.aslist是将args参数显示出来,如果不用array.aslist,控制台会显示其地址值
                //System.out.println("["+method.getName()+"]开始执行.....它的参数是:"+ Arrays.asList(args)+"");

                //上面这句输出语句的日志信息还能用LogUtils来封装,把上面这条语句封装到LogUtils类的静态LogStart方法里面,然后调用
                LogUtils.logStart(method,args);

                result = method.invoke(calculator, args);

                //加入日志信息(方法开始后)
                //System.out.println("["+method.getName()+"]正常执行完毕.....,结果是:"+result+"");
                //上面这句输出语句的日志信息还能用LogUtils来封装,把上面这条语句封装到LogUtils类的静态LogStart方法里面,然后调用

                LogUtils.LogReturn(method,args);

            }
            catch(Exception e){
                //System.out.println("["+method.getName()+"]出现异常:"+e.toString()+"");
                //上面这条语句也可以放在Loutils中
                LogUtils.LogErr(method,e);
            }
            finally {
                //System.out.println("方法最终执行完毕");
                //在Logutils中的表示方法
                LogUtils.LogEnd();
            }

            //这个返回值必须返回出去,外界才能拿到真正执行后的返回值
            return result;
        }
    });
    return (Calculator) proxy;

}
}

Logutils源码:

package com.fxp.util;

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

public class LogUtils {
public static void logStart(Method method,Object... args){
    System.out.println("["+method.getName()+"]开始执行.....它的参数是:"+ Arrays.asList(args)+"");
}
//方法正常执行,正常返回的方法
public static void LogReturn(Method method,Object... result){
    System.out.println("["+method.getName()+"]正常执行完毕.....,结果是:"+result+"");
}

public static void LogErr(Method method,Exception e) {
    System.out.println("["+method.getName()+"]出现异常:"+e.getCause()+"");
}

public static void LogEnd() {
    System.out.println("方法最终执行完毕");
}
}

运行结果
在这里插入图片描述

至此,我们达到了我们刚开始的期望,使用了动态代理,在目标方法执行的前后进行执行

3.5、动态代理的缺点

3.5.1、写起来太难

现在这个例子,只是给计算器加了个动态代理,如果还有其他类要写动态代理,就要写很多遍,很麻烦。

3.5.2、目标类必须实现接口

1、在创建动态代理的时候,Proxy.newProxyInstance里面有三个参数,其中一个是Class<?> [ ]interfaces,这个参数是目标类所实现的所有接口,如果说目标类并没有实现任何接口,就意味着根本创建不了动态代理。

2、我们可以看一下Proxy真正的类型

System.out.println(proxy.getClass());

在这里插入图片描述

3、代理对象和被代理对象唯一能产生的关联,就是实现了同一个接口

4、我们可以看一下proxy实现的接口:

//proxy实现的接口
System.out.println(Arrays.asList(proxy.getClass().getInterfaces()));

运行结果:
在这里插入图片描述

可以看到proxy也是实现了Calculator这个接口。

5、结论:
所以如果目标类没有实现任何接口,是不能为目标类创建代理对象的。

3.6、解决办法

由于动态代理太难写,所以Spring实现了AOP功能,AOP的底层就是动态代理
可以利用Spring一句代码都不写的去创建动态代理,实现简单,而且没有强制要求目标类必须实现接口。

3.7、AOP的专业术语

在这里插入图片描述

以Calculator为例:
它有四个方法:add、sub、mul、div,

横切关注点:每个方法都能在方法的开始、返回、异常、结束的地方记录日志,这些地方就叫做横切关注点
通知方法:其实就是记录日志信息的方法,比如在方法执行前输出的信息等等.....
切面类:横切关注点和通知方法所在的类
连接点:每一个方法的每一个位置(方法开始、返回、异常、结束)都是一个连接点
切入点:这些连接点中,我们真正需要记录日志的地方
切入点表达式:在众多连接点中选出我们真正需要的点的表达式

猜你喜欢

转载自blog.csdn.net/weixin_42389904/article/details/114405360