Java框架-代理模式详细介绍、Spring的AOP

1. 代理模式详介

1.1 分类和作用

  • 分类:静态代理、jdk动态代理(接口代理)、cglib动态代理(子类代理)技术
  • 使用代理的原因:实际开发中通常都会调用别人编写的代码/框架来完成业务需求。很多情况是需要对这些代码/框架进行微调或扩展,而如果修改原代码很容易出现错误。这时候就需要使用代理。
  • 作用:通过代理访问目标对象。在目标对象实现的基础上,增强额外的功能操作,即在不修改原代码的基础上扩展目标对象的功能。

1.2 静态代理(不推荐)

  • 原理:
    1. 代理类实现与目标对象相同的接口。通过构造器或set方法给代理对象注入目标对象;
    2. 实现代理对象接口方法时,内部调用目标对象真正实现方法,并且可添加额外的业务控制。
  • 实现要求:
    1. 代理对象需要实现与目标对象一样的接口;
    2. 代理对象需要维护一个目标对象(需要目标对象调用目标对象自身方法);
    3. 代理对象一定会调用目标对象的方法。
  • 优点:在不修改原有代码的基础上,对指定的目标对象进行扩展
  • 缺点:
    1. 对于代理对象而言,接口的所有方法都要重写,即便需要增强的方法仅是其中的少数几个;
    2. 目标对象的接口修改后,目标对象和代理对象都得相应修改,不便于维护
    3. 每个目标对象至少有一个代理对象,导致代理类过多;

1.2.1 代码实现概述

代理类的代码实现:

  1. 代理类实现目标对象的接口;
  2. 代理类中创建目标对象;
  3. 重写接口方法,调用目标对象的方法,同时添加额外的业务逻辑。

1.3 jdk动态代理(接口代理)

  • 概念:代理类在程序运行时动态创建

  • 特点

    1. 目标对象必须实现接口;
    2. 程序运行期间,利用jdk的proxy相关api,在内存中动态构建字节码对象,从而生成代理对象,同时让代理对象实现目标对象的接口。
    3. jdk动态代理对象在内存的名称是$Proxy加上数字,如 P r o x y 3 Proxy3、 Proxy4等
  • 生成动态代理的API:

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

    • 参数1 loader:当前使用哪个类加载器生成代理对象
    • 参数2 interfaces:目标对象实现的接口类型数组
    • 参数3 h:事件处理程序(当执行代理方法时候会触发),需要传入一个InvocationHandler接口实现类对象。(详见案例代码)

1.3.1 jdk动态代理的入门案例

1.3.1.1 目标对象接口
public interface IStar {
    /*明星的功能*/
    void singing(double money);
    void act(double money);
}
1.3.1.2 目标对象类,实现接口
public class Star implements IStar {
    @Override
    public void singing(double money) {
        System.out.println("[开演唱会!收费标准:]" + money);
    }

    @Override
    public void act(double money) {
        System.out.println("[拍戏!收费标准:]" + money);
    }
}
1.3.1.3 测试类中实现动态代理
public class Test_jdk {
    public static void main(String[] args) {
        //1.定义目标对象
        IStar target = new Star();
        //2.对目标对象动态生成代理对象
        /**
         * jdk动态代理
         *
         * 1.运行时期,利用jdk的api,在内存中动态构建字节码对象,从而生成代理对象。
         * 2.要求:目标对象一定要实现接口
         * 3.生成代理的Api
         * |-->Proxy
         *   |--> static Object newProxyInstance(ClassLoader loader, Class<?>[] interfaces,InvocationHandler h)
         *      参数1:loader       当前使用哪个类加载器生成代理对象
         *      参数2:interfaces   目标对象实现的接口类型数组
         *      参数3:h            事件处理程序(当执行代理方法时候会触发),需要传入一个InvocationHandler接口实现类对象
         *          InvocationHandler接口只有一个方法Object invoke(Object proxy, Method method, Object[] args)
         *              其中:proxy指当前的代理对象
         *                   method指代理对象调用的方法,使用反射的invoke()来执行方法代码
         *                   args指传入的参数数组
         * 4.原理
         *     生成代理对象:class $Proxy3 implements IStar
         *     通过类加载器生成动态代理对象,通过接口类型数组确定代理对象实现的接口,通过处理程序代码确定代理所要实现的功能
         * 5.API的写法套路如下,可以另行定义事件处理程序方法,再把方法传入
         */
        IStar proxy = (IStar) Proxy.newProxyInstance(
                Test_jdk.class.getClassLoader(),
                new Class[]{IStar.class},
                new InvocationHandler() {
                    @Override
                    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                        // 获取方法参数
                        Double money = (Double) args[0];
                        // 添加额外的业务判断
                        if (money > 100000) {
                            // 访问目标对象的方法
                            return method.invoke(target,args);
                        } else {
                            System.out.println("档期忙,没空!");
                        }
                        return null;
                    }
                }
        );
        //实行代理方法
        proxy.singing(100001);
        proxy.act(1);
    }
}
1.3.1.4 代理工厂类优化代理类
/**
 * 代理工厂
 * 1. 对所有的目标对象生成代理对象,使用泛型
 * 2. 要求:目标对象一定要实现接口。因为用的是jdk接口代理
 */
public class ProxyFactory<T> {
    //创建泛型目标对象
    private T target;
    //代理工厂构造函数,工厂对象创建时会传入目标对象
    public ProxyFactory(T target) {
        this.target = target;
    }

    // 针对传入的目标对象生成代理对象并返回
    public T createProxy() {
        return (T)Proxy.newProxyInstance(
                this.getClass().getClassLoader(),   //根据工厂类获取类加载器
                target.getClass().getInterfaces(),  //根据目标对象获取所实现的所有接口的数组
                new InvocationHandler() {
                    @Override
                    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                        // 获取方法参数
                        Double money = (Double) args[0];
                        // 判断
                        if (money > 100000) {
                            // 调用目标对象的方法
                            // 使用method对象的invoke()执行方法代码
                            return method.invoke(target, args);
                        } else {
                            System.out.println("没有调用目标对象方法!不满足条件");
                            return null;
                        }
                    }
                });
    }
}

1.4 cglib动态代理(子类代理)

  • 使用场景:如果目标对象没有实现任何接口,无法使用接口代理。此时就要用到CGLIB子类代理。
  • 原理:在内存中动态构建一个子类对象,从而实现对目标对象功能的扩展
  • 使用要求:需要导入CGLIB的jar包,或者直接引入Spring-Core核心包。
  • 注意事项:
    1. 代理的类不能为final,否则报错;
    2. **目标对象的方法如果为final/static,那么就不会被拦截,即不会执行目标对象额外的业务方法!!
  • 特点:
    1. cglib动态代理对象在内存的名称是class com.azure.proxy3_cglib.Star$$EnhancerByCGLIB$$6e7ec13a,格式是:类全名$$EnhancerByCGLIB$$内存地址

1.4.1 cglib动态代理的入门案例

1.4.1.1 引入依赖

添加spring-core依赖

1.4.1.2 目标类
public class Star {
    public void singing(double money) {
        System.out.println("[开演唱会!收费标准:]" + money);
    }

    public void act(double money) {
        System.out.println("[拍戏!收费标准:]" + money);
    }
}
1.4.1.3 测试类中实现动态代理
public class Test_cglib {
    public static void main(String[] args) {
        //1.定义目标对象
        Star target = new Star();

        //2.对目标对象动态生成cglib代理对象
        Star proxy = (Star) Enhancer.create(
                Star.class,
                new MethodInterceptor() {
                    @Override
                    public Object intercept(Object proxy, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
                        // 获取方法名称
                        String methodName = method.getName();
                        // 获取方法参数
                        double money = (double) args[0];
                        // 方法返回值
                        Object reValue = null;
                        // 判断
                        if ("singing".equals(methodName)){
                            // 判断
                            if (money > 100000) {
                                // 调用唱歌方法
                                reValue = method.invoke(target,args);
                            }else{
                                System.out.println("忙,没空开演唱会!");
                            }
                        }
                        else if ("act".equals(methodName)) {
                            if (money > 1000000) {
                                // 调用目标对象
                                reValue = method.invoke(target,args);
                            } else {
                                System.out.println("忙,不接新戏!");
                            }
                        }
                        return reValue;
                    }
                }
        );
        //System.out.println(proxy);
        //实行代理方法
        proxy.singing(1);
        proxy.act(1234567);
    }
}

1.5 jdk动态代理和CGLIB动态代理的区别

  1. JDK动态代理只能对接口或实现了接口的类生成代理,而不能针对类;
  2. CGLIB是针对类实现代理,主要是对指定的类生成一个子类,重写其中的方法。因为是继承,所以该类或方法不能声明成final 。

2. AOP概念

  • 官方概念:AOP(Aspect Oriented Programming):面向切面编程,通过预编译方式和运行期动态代理实现程序功能的统一维护的一种技术。
  • 建议理解:使用动态代理技术,实现在不修改java源代码的情况下,运行时实现方法功能的增强。

2.1 AOP作用及优势

  • 作用:在程序运行期间,不修改源码对已有方法进行增强。
  • 优势:减少重复代码,提高开发效率;统一管理统一调用,方便维护。

2.2 AOP的实现原理

  • 原理:动态代理技术
  • 在 spring 中,框架会根据目标类是否实现了接口来决定采用哪种动态代理的方式。如果目标对象实现接口,使用jdk代理;目标对象没有实现接口使用cglib代理。

2.3 AOP的基本概念

  • Joinpoint(连接点)

    被增强功能的候选方法,spring只支持方法类型的连接点

  • Pointcut(切入点)

    指要拦截的方法;

    切入点表达式: 拦截方法,对满足表达式的类,自动生成代理对象;

  • Advice(通知/增强)

    所谓通知是指拦截到 Joinpoint 之后所要做的事情就是通知。

    简单而言,就是对原有功能添加新功能

    通知的类型: 前置通知、后置通知、异常通知、最终通知、环绕通知。

  • Target(目标对象)

    被代理的对象

  • Weaving(织入)

    指把增强功能用于目标对象,创建代理对象的过程;spring采用动态代理织入。

    简单而言,将新功能添加到目标对象原有功能;

  • Proxy(代理)

    被织入增强后,产生一个结果代理类

  • Aspect(切面)

    切面指的是切入点和通知的结合

    简单而言,就是项目中重复使用的代码

2.4 spring的AOP需要明确的事情【掌握】

2.4.1开发阶段(我们做的)

  • 根据业务需求,编写核心业务代码

  • 把公用代码抽取出来,制作成通知,通知所在的类就是切面类

  • 通过配置的方式,建立切入点(业务功能)和通知的关系

2.4.2 运行阶段(spring框架完成)

  • spring框架监控切入点方法执行。
  • 一旦监控到切入点方法被执行,使用动态代理机制,创建目标对象的代理对象,根据通知类型,在代理对象当前执行方法的对应位置,织入通知功能,完成完整的代码逻辑执行。

3. AOP编程

案例需求:执行方法时自动记录日志信息

3.1 基于xml的AOP配置【重点】

  • 先建立一个入门案例

3.1.1 创建项目、添加依赖

  • 添加ioc基础框架包和AOP支持包(aspectjweaver包)

pom.xml文件

<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-context</artifactId>
    <version>5.0.2.RELEASE</version>
</dependency>
<dependency>
    <groupId>org.aspectj</groupId>
    <artifactId>aspectjweaver</artifactId>
    <version>1.8.7</version>
</dependency>

3.1.2 service层模拟实现保存账户功能

3.1.2.1 service接口
public interface IAccountService {
    /*模拟保存账户功能*/
    void save();
}
3.1.2.2 service实现类
public class AccountServiceImpl implements IAccountService {
    @Override
    public void save() {
        System.out.println("账户保存!");
    }
}

3.1.3 创建记录日志的工具类Logger

/**
 * 由于记录日志是每个被调用方法都要执行的代码(重复代码),所以可以将代码抽取出来编写成通知方法
 * 其他方法被执行的时会都会执行通知方法,也就是日志记录会被自动执行(需要配置)
 * 创建一个工具类用来记录日志(也可以成为切面类/通知类)
 */
public class Logger {
    /*将重复代码编写记录日志方法*/
    public void printLog(){
        System.out.println("记录用户操作日志...");
    }
}

3.1.4 bean.xml配置(重点)

<?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: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/aop http://www.springframework.org/schema/aop/spring-aop.xsd">

    <!--创建service对象-->
    <bean id="accountService" class="com.azure.service.impl.AccountServiceImpl"></bean>
    <!--创建记录日志的工具具类(切面类、通知类)-->
    <bean id="logger" class="com.azure.utils.Logger"></bean>
    <!--
        Aop 配置
        1.aop:pointcut 配置切入点表达式
          作用:spring在创建容器时候,对符合切入点表达式的类自动生成代理对象。
        2.aop:aspect 配置切面类
           ref 引用的切面类(日志工具类)
           aop:before 前置通知,在执行目标对象方法之前执行
               method 对用logger切面类的方法
               pointcut-ref 引用的切入点表达式对象
        -->
    <aop:config>
        <!--设置切入点表达式-->
        <aop:pointcut id="pt" expression="execution(* com.azure.service.impl.AccountServiceImpl.save())"></aop:pointcut>
        <!--设置切面,里面包含切入点表达式和通知-->
        <aop:aspect ref="logger">
            <!--前置通知,在执行目标对象方法之前执行-->
            <!--切入点表达式可以在通知标签中定义,但为了复用性,故将切入点表达式抽取出来-->
            <aop:before method="printLog" pointcut-ref="pt"/>
        </aop:aspect>
    </aop:config>
    
</beans>

3.1.5 测试类

public class App {
    public static void main(String[] args) {
        // 创建容器
        ApplicationContext ac = new ClassPathXmlApplicationContext("bean.xml");
        // 从容器中获取service对象
        IAccountService accountService = (IAccountService) ac.getBean("accountService");
        // 查看对象类型:class com.sun.proxy.$Proxy4,使用jdk代理。
        // 符合切入点表达式的类生成代理对象
        System.out.println(accountService.getClass());

        // 执行代理方法
        accountService.save();
    }
}

3.2 切入点表达式(重点)

  • 作用:对符合切入点表达式的类,会自动生成代理对象

3.2.1官方语法

在这里插入图片描述

3.2.2 九(种写法)+一(种常用写法)

  1. 最全的写法
    假设拦截返回void,指定类的指定方法,参数有2个:int、String
    execution(public void com.azure.service.impl.AccountServiceImpl.save(int,java.lang.String))
  2. 省略访问修饰符,返回值任意的指定类的save方法,无参数
    execution(* com.azure.service.impl.AccountServiceImpl.save())
  3. 返回值void,拦截com包下所有的类、以及其子包下所有的类的save()方法
    execution(void com..*.save()) 包名与类名或方法名称都可以使用 *
  4. 返回值任意类型,拦截save()方法/拦截所有方法
    execution(* save()) 拦截save()
    execution(* *()) 拦截所有方法
  5. 不拦截save()方法
    !execution(* save())或者not execution(* save()) 注意not前要有空格!!
  6. 返回值任意类型,拦截save()方法或者update()方法,注意从逻辑角度,不能写and
    execution(* save()) || execution(* update())"
    execution(* save()) or execution(* update())"
  7. 拦截所有方法,参数任意,但必须有参数
    execution(* *(*))
  8. 拦截所有方法,参数任意,参数可有可无
    execution(* *(..))*
  9. 对ioc容器中以Service结尾的类,生成代理对象
    bean(*Service)
  10. 最常用(重要)
    execution(* com.azure..*ServiceImpl.*(..))
    表示com.azure包及其所有子包下所有的以ServiceImpl结尾的类生成代理对象
  • 注意事项:

    ..:如果出现在类中表示包及其子包;如果出现在方法参数中,表示可以有参数也可以没有参数

    *ServiceImpl:在类中,表示以ServiceImpl结尾的类。注意,*与ServiceImpl之间没有空格

    *在返回值类型上,表示返回值为任意类型;在包名中,见上;在方法名上,表示所有方法。

3.2.2.1 实例代码
<?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: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/aop http://www.springframework.org/schema/aop/spring-aop.xsd">

    <!--创建service对象-->
    <bean id="accountService" class="com.azure.service.impl.AccountServiceImpl"></bean>
    <!--创建记录日志的工具具类(切面类、通知类)-->
    <bean id="logger" class="com.azure.utils.Logger"></bean>
    <!--
        Aop 配置
        1.aop:pointcut 配置切入点表达式
          作用:spring在创建容器时候,对符合切入点表达式的类自动生成代理对象。
        2.aop:aspect 配置切面类
           ref 引用的切面类(日志工具类)
           aop:before 前置通知,在执行目标对象方法之前执行
               method 对用logger切面类的方法
               pointcut-ref 引用的切入点表达式对象
        -->
    <aop:config>
        <!--设置切入点表达式-->
        <!--1.最全的写法
            拦截返回void,指定类的指定方法,参数必须有2个:int、String
            execution(public void com.azure.service.impl.AccountServiceImpl.save(int,java.lang.String))
        
            2.省略访问修饰符,返回值任意的指定类的save方法,无参数
            execution(* com.azure.service.impl.AccountServiceImpl.save())
        
            3.拦截com包下所有的类、以及其子包下所有的类的save()方法
            execution(void com..*.save())  包名与类名或方法名称都可以使用 *
        
            4.拦截save()方法/拦截所有方法
            execution(* save()) 拦截save()
            execution(* *())    拦截所有方法
        
            5.不拦截save()方法
            !execution(* save())
              not execution(* save()) 注意not前要有空格
        
            6.拦截save()方法或者update()方法
            execution(* save()) || execution(* update())"
            execution(* save()) or execution(* update())"
        
            7.拦截所有方法,参数任意,但必须有参数
            execution(* *(*))
            8.拦截所有方法,参数任意,参数可有可无
            execution(* *(..))
            9.对ioc容器中以Service结尾的类,生成代理对象
            bean(*Service)
            10.最常用
            execution(* com.azure..*ServiceImpl.*(..))
            表示com.azure包及其所有子包下所有的以ServiceImpl结尾的类生成代理对象。          
        -->
        <aop:pointcut id="pt" expression="execution(* com.azure..*ServiceImpl.*(..))"></aop:pointcut>
        <!--设置切面,里面包含切入点表达式和通知-->
        <aop:aspect ref="logger">
            <!--前置通知,在执行目标对象方法之前执行-->
            <!--切入点表达式可以在通知标签中定义,但为了复用性,故将切入点表达式抽取出来-->
            <aop:before method="printLog" pointcut-ref="pt"/>
        </aop:aspect>
    </aop:config>

</beans>

3.3 常用标签说明

3.3.1 <aop:config>

  • 作用:声明aop配置。

3.3.2 <aop:aspect>

  • 作用:配置切面。

  • 属性:

    • id:唯一标识切面的名称
    • ref:引用通知类bean的id

3.3.3 <aop:pointcut>

  • 作用:配置切入点表达式。

  • 属性:

    • id:唯一标识切入点表达式名称
    • expression:定义切入点表达式

3.3.4 <aop:before>

  • 作用:配置前置通知

  • 属性:

    • method:指定通知方法名称
    • pointcut:定义切入点表达式
    • pointcut-ref:引用切入点表达式的id

3.3.5 <aop:after-returning>

  • 作用:配置后置通知

  • 属性:

    • method:指定通知方法名称
    • pointcut:定义切入点表达式
    • pointcut-ref:引用切入点表达式的id

3.3.6 <aop:after-throwing>

  • 作用:配置异常通知

  • 属性:

    • method:指定通知方法名称
    • pointcut:定义切入点表达式
    • pointcut-ref:引用切入点表达式的id

3.3.7 <aop:after>

  • 作用:配置最终通知

  • 属性:

    • method:指定通知方法名称
    • pointcut:定义切入点表达式
    • pointcut-ref:引用切入点表达式的id

3.3.8 <aop:around>

  • 作用:配置环绕通知

  • 属性:

    • method:指定通知方法名称
    • pointcut:定义切入点表达式
    • pointcut-ref:引用切入点表达式的id

3.4 通知类型(重点)

3.4.1 通知类型

  • 前置通知:在目标方法执行前执行

  • 后置通知:在目标方法正常返回后执行。它和异常通知只能执行一个

  • 异常通知:在目标方法发生异常后执行。它和后置通知只能执行一个

  • 最终通知:无论目标方法正常返回,还是发生异常都会执行

  • 环绕通知:综合了前面四类通知,可以手动控制通知的执行时间点和顺序

3.4.2 通知类型案例演示

3.4.2.1 Logger类改造
/**
 * 由于记录日志是每个被调用方法都要执行的代码(重复代码),所以可以将代码抽取出来编写成通知方法
 * 其他方法被执行的时会都会执行通知方法,也就是日志记录会被自动执行(需要配置)
 * 创建一个工具类用来记录日志(也可以成为切面类/通知类)
 */
public class Logger {
    /*将重复代码编写记录日志方法*//*
    public void printLog(){
        System.out.println("记录用户操作日志...");
    }*/
    
    /**
     * 【前置通知】 在执行目标对象方法之前执行
     */
    public void beforeLog(){
        System.out.println("【前置通知】记录日志");
    }

    /**
     * 【后置通知】 在执行目标对象方法返回结果后执行。出现异常不执行
     */
    public void afterReturningLog(){
        System.out.println("【后置通知】记录日志");
    }

    /**
     * 【异常通知】 只有在执行目标对象方法出现异常时候执行
     */
    public void afterThrowing(){
        System.out.println("【异常通知】记录日志");
    }

    /**
     * 【最终通知】在执行目标方法之后执行,始终执行,不管目标对象方法有没有异常
     */
    public void after(){
        System.out.println("【最终通知】记录日志");
    }
}
3.4.2.2 bean.xml配置
<!--创建service对象-->
<bean id="accountService" class="com.azure.service.impl.AccountServiceImpl"></bean>
<!--创建记录日志的工具具类(切面类、通知类)-->
<bean id="logger" class="com.azure.utils.Logger"></bean>

<!--通知配置类型-->
<aop:config>
    <!--设置切面-->
    <aop:aspect ref="logger">
        <!--设置切入点表达式-->
        <aop:pointcut id="pt" expression="execution(* com.azure..*ServiceImpl.*(..))"></aop:pointcut>
        <!--设置通知-->
        <!--前置通知-->
        <aop:before method="beforeLog" pointcut-ref="pt"></aop:before>
        <!--后置通知-->
        <aop:after-returning method="afterReturningLog" pointcut-ref="pt"></aop:after-returning>
        <!--异常通知-->
        <aop:after-throwing method="afterThrowing" pointcut-ref="pt"></aop:after-throwing>
        <!--最终通知-->
        <aop:after method="after" pointcut-ref="pt"></aop:after>

    </aop:aspect>
</aop:config>

3.4.3 环绕通知(推荐使用)

  • spring框架为我们提供了手动控制通知执行时间点和顺序的一种特殊通知类型,它使用起来会更加灵活一些
  • 特点:可以获取方法信息,甚至可以修改方法参数及返回结果!
3.4.3.1 改造Logger,添加环绕通知

在改造前,给service接口添加update方法,参数为int id,返回值为String;

实现类输出方法参数,并return “修改成功!!”

  • 格式:public Object around(ProceedingJoinPoint pjp){}
/**
     * 【环绕通知】 环绕目标对象方法执行,
     * 好处:
     * 1.手动控制通知的执行时间顺序
     * 2.可以获取方法信息、*甚至可以修改方法参数及返回结果*
     * 3.推荐使用环绕通知
     * <p>
     * 获取参数:
     * 1.目标对象:pjp.getTarget()
     * 2.目标对象全路径:pjp.getTarget().getClass().getName()
     * 3.执行方法名称:pjp.getSignature().getName()
     *
     * @param pjp 可以获取方法信息:参数、方法名、方法所在类
     * @return
     */
    public Object around(ProceedingJoinPoint pjp) {
        System.out.println("目标对象:" + pjp.getTarget());
        System.out.println("目标对象全路径:" + pjp.getTarget().getClass().getName());
        System.out.println("执行方法名称:" + pjp.getSignature().getName());
        //获取方法参数
        Object[] args = pjp.getArgs();
        //修改参数
        args[0] = 168;

        try {
            System.out.println("环绕前");
            //执行目标对象方法
            Object retV = pjp.proceed(args);
            System.out.println("[环绕后]");
            //可以修改方法执行结果
            return retV + "~~~";
        } catch (Throwable throwable) {
            throwable.printStackTrace();
            System.out.println("[环绕后 异常]");
            return null;
        } finally {
            System.out.println("[环绕后 最终]");
        }
    }
3.4.3.2 bean.xml配置
<!--环绕通知-->
<aop:around method="around" pointcut-ref="pt"></aop:around>
3.4.3.3 测试类
public class App {
    public static void main(String[] args) {
        // 创建容器
        ApplicationContext ac = new ClassPathXmlApplicationContext("bean2.xml");
        // 从容器中获取service对象
        IAccountService accountService = (IAccountService) ac.getBean("accountService");
        // 查看对象类型:class com.sun.proxy.$Proxy4,使用jdk代理。
        // 符合切入点表达式的类生成代理对象
        System.out.println(accountService.getClass());

        // 执行代理方法
        accountService.update(888);
    }
}
3.4.3.4 结果

在这里插入图片描述

4. AOP编程-注解方法实现

4.0 常用的注解

4.0.1 @Aspect

  • 指定当前类为切面类

4.0.2 @Pointcut

  • 定义切点表达式,定义在方法上,以该方法名用作该表达式的id

4.0.3 @Before()

  • 定义前置通知,定义在方法上。属性值为切点表达式

案例需求:执行方法时自动记录日志信息

4.1 创建项目,添加依赖

<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-context</artifactId>
    <version>5.0.2.RELEASE</version>
</dependency>
<dependency>
    <groupId>org.aspectj</groupId>
    <artifactId>aspectjweaver</artifactId>
    <version>1.8.7</version>
</dependency>

4.2 创建实体类

public class User {
    private int id;
    private String name;
    /*省略*/
}

4.3 service层

4.3.1 接口

public interface IAccountService {
    /*模拟保存账户功能*/
    void save();

    int updateUser(User user);
}

4.3.2 实现类

public class AccountServiceImpl implements IAccountService {
    @Override
    public void save() {
        System.out.println("执行保存方法!");
    }

    @Override
    public int updateUser(User user) {
        System.out.println("执行修改方法");
        return 0;
    }
}

4.4 Logger切面类

/**
 * 日志切面类
 */
@Aspect     // 指定当前类为切面类
@Component  // 创建切面类对象
public class Logger2 {

    // 定义一个切入点表达式,使用时候直接引用方法即可
    @Pointcut("execution(* com.azure.service.impl.*.*(..))")
    public void pt() {
    }

    // 在执行目标方法之前执行
    @Before("pt()")     //value值为切点表达式所注解的方法名加小括号,方法只有一个时value属性名可省略。也可以直接将切点表达式写入。下同
    public void before() {
        System.out.println("[前置通知]");
    }

    // 在执行目标方法之后执行。出现异常不执行
    @AfterReturning("pt()")
    public void afterRetuning() {
        System.out.println("[后置通知]");
    }

    //在执行目标方法出现异常执行
    @AfterThrowing("pt()")
    public void afterThrowing() {
        System.out.println("[异常通知]");
    }

    //在执行目标方法之后执行。始终执行。
    @After("pt()")
    public void after() {
        System.out.println("[最终通知]");
    }
    // 环绕目标对象方法执行
    @Around("pt()")
    public Object around(ProceedingJoinPoint pjp) {
        try {
            System.out.println("[环绕通知] 环绕前");
            Object retV = pjp.proceed();
            System.out.println("[环绕通知] 环绕后");
            return retV;
        } catch (Throwable throwable) {
            throwable.printStackTrace();
            System.out.println("[环绕通知] 环绕异常");
            return null;
        } finally {
            System.out.println("[环绕通知] 环绕最终");
        }
    }
}

4.5 bean.xml配置

<?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">
    <!--创建service-->
    <bean id="accountService" class="com.azure.service.impl.AccountServiceImpl"></bean>

    <!--开启注解扫描-->
    <context:component-scan base-package="com.azure"></context:component-scan>

    <!--开启Aop自动代理-->
    <aop:aspectj-autoproxy></aop:aspectj-autoproxy>
    
</beans>

4.6 测试类

public class App {
    public static void main(String[] args) {
        // 创建容器
        ApplicationContext ac = new ClassPathXmlApplicationContext("bean3.xml");
        // 从容器中获取service对象
        IAccountService accountService = (IAccountService) ac.getBean("accountService");
        // 查看对象类型:class com.sun.proxy.$Proxy4,使用jdk代理。
        // 符合切入点表达式的类生成代理对象
        System.out.println(accountService.getClass());

        // 执行代理方法
        accountService.save();
    }
}

4.7 注意事项

  1. 前置通知、后置通知、异常通知和最终通知为一组,与环绕通知最好不要同时使用。要不用前者,要不用后者。
  2. 由于前者输出时最终通知会比后置通知早输出,输出的顺序混乱,故建议使用环绕通知。
  3. 前置通知、后置通知、异常通知、最终通知和环绕通知的value值写的时切入点表达式方法名(),千万不可省略小括号,否则会报错

猜你喜欢

转载自blog.csdn.net/KeepStruggling/article/details/83450027