AOP的使用、通知类型介绍和AOP原理

  • Post author:
  • Post category:其他


承上:

SpringAOP的基本认识

先从AOP前置通知类型为例,写一个简单的demo,从入门级了解AOP的使用。

IDE环境:Intellj IDEA

步骤:

1先建立一个Spring项目。项目结构如下:

这里写图片描述

已经知道AOP四要素

方面组件————-LogUtil类(添加日志功能)

目标组件————-EntityDao类(进行数据库操作的功能)

EntityDao类代码:(因为主要学习AOP,所以数据库的操作方法简单写一下就行)

package com.hnust.service;

import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import org.springframework.stereotype.Service;

@Service
public class EntityDao {

    public void save(){
        System.out.println("保存操作");
    }

    public void update(){
        System.out.println("更新操作");
    }

    public void delete(){
        System.out.println("删除操作");
    }

    public static void main(String[] args){
        //获取Spring容器
        ApplicationContext atc = new ClassPathXmlApplicationContext("spring-config.xml");
        //通过Spring容器创建EntityDao对象
        EntityDao ed =  atc.getBean(EntityDao.class);
        System.out.println("目标组件"+ed.getClass().getName());
        ed.save();
        ed.update();
        ed.delete();
    }
}

Spring配置文件spring-config.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">

    <context:component-scan base-package="com.hnust"></context:component-scan>
</beans>

这里涉及IOC注解注入和组件扫描的知识,先不讲解(因为我也还没学0.0),知道就是添加了这个注解,通过组件扫描,就可以将对象添加进入Spring容器中,交由Spring管理,这是Spring容器利用IOC创建对象的一种方式就行。

我们可以运行代码,得到一下结果

这里写图片描述

现在已经模拟完成数据库的增删改操作啦。

为了增加数据库的安全性,我们现在对数据库每一个操作之前添加日志文件,也就是在目标组件中添加方面组件模块,而且目标组件中的方法有格式要求,方法必须是共有的,无返回值,至于带不带参数,取决于通知类型,我们先使用前置通知类型进行测试,不需要带参数。

LogUtil类代码:

package com.hnust.aspect;

import org.springframework.stereotype.Component;

@Component
public class LogUtil {

    public void log1(){
        System.out.println("**数据库操作日志已记录**");
    }
}

有了目标组件和方面组件,AOP四要素还缺两个,就是通知和切入点,这两个都在Spring配置文件中配置就行。

在Spring配置文件spring-config.xml文件添加AOP配置

<?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.hnust"></context:component-scan>
    <!--AOP配置-->
    <aop:config>
        <!--声明方面组件-->
        <aop:aspect ref="logUtil">
            <aop:before method="log1"
                        pointcut="within(com.hnust.service.*)"/>
        </aop:aspect>
    </aop:config>
</beans>

我们学习一下配置规则。

这里写图片描述

配置之后,再运行目标组件,正常情况结果是这样

这里写图片描述

可以看到,AOP的配置使得目标组件成功调用方面组件的功能。

需要注意的是,Intellij IDEA中创建Spring项目时,包没有导全,我们在使用AOP功能时,需要导入aspectjweaver-1.8.10.jar,如果使用maven构建项目,那么在maven配置文件中添加该包的依赖,如果没有使用maven,那么就下载这个包,添加进项目的lib文件中就行(坑呀,这个问题花了大半天没有解决,最后还是仔细看错误提示才找到原因,提醒各位出现BUG时习惯先看错误输出,不要无脑去看源程序)。

AOP的入门使用大概就是上面讲解的了,我们知道AOP使得程序更加灵活,之间就讲过,AOP可以使得方面组件可以植入目标组件的任意位置,也就是说目标组件哪些方法可以调用方面组件,什么时机可以调用方面组件,所以,我们可以通过修改AOP配置中的切入点和通知去改变。

下面介绍AOP通知类型:

  1. 前置通知:调用目标组件前,调用方面组件(before)
  2. 后置通知:调用目标组件后,调用方面组件(after-returning)
  3. 最终通知:调用目标组件后,在finally里调用方面组件(after)
  4. 异常通知:目标组件发生异常时,调用方面组件。(after-throwing)
  5. 环绕通知:调用目标组件前、后,分别调用一次方面组件。(around)

    AOP就是按照这五种通知类型进行分类的。

    用代码可以更清晰的明白各通知之间的位置,我们关注点不在各通知之间的相对顺序,只要关注某个通知类型和关键业务代码的顺序就行了。
try{
    try{
        //环绕通知-前置
        //前置通知
        ed.save();
        //环绕通知-后置
    }(Exception e){
        throw e;
    }finally{
        //最终通知
    }
    //后置通知
}catch(Exception e){
    //异常通知
}

我们利用上面的Demo来测试一下这五种类型的通知吧,了解不同类型通知的差异。

前面Demo使用的是前置通知,后置通知和最终通知是一类,因为方面组件方法是无参的。只需要在配置文件中改变通知类型就可以。

将before关键字依次改为after-returning、after,分别是后置通知和最终通知,运行结果一样,如下图:

这里写图片描述

那么后置通知和最终通知区别是什么呢?学过异常处理机制就很容易理解,如果目标组件中业务核心代码发生出错,那么后置通知不会执行,但是最终通知会执行,因为最终通知位于finally模块中。

那么异常通知类型呢,我们需要在方面组件的方法中加入异常处理的参数。

增加一个处理异常的方法log2

package com.hnust.aspect;

import org.springframework.stereotype.Component;

@Component
public class LogUtil {

    public void log1(){
        System.out.println("**数据库操作日志已记录**");
    }

    public void log2(Exception e){
        System.out.println("调用异常通知日志");
        //得到异常数组并输出
        String error = e.getStackTrace()[0].toString();
        System.out.println(error);
    }
}

在配置文件中配置异常通知类型

<?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.hnust"></context:component-scan>
    <!--AOP配置-->
    <aop:config>
        <!--声明方面组件-->
        <aop:aspect ref="logUtil">
            <!--前置、后置、最终通知-->
            <aop:after-returning method="log1"
                        pointcut="within(com.hnust.service.*)"/>
            <!--异常通知-->
            <aop:after-throwing method="log2" throwing="e"
                                 pointcut="within(com.hnust.service.*)"/>
        </aop:aspect>
    </aop:config>
</beans>

此时运行目标组件,发现异常通知并没有输出,因为目标组件中没有方法出现异常,所以方面组件中的异常方法不会触发,我们可以在目标组件某个方法中加入异常代码,可以看到输出异常信息

这里写图片描述

那么这个异常机制是怎么实现的呢?我们需要先了解一下AOP原理(感觉有点乱了,随意吧):

AOP实现原理使用的是动态代理的技术,可以在程序运行阶段,根据业务的要求在内存中创建代理类,Spring采用2中方式实现动态代理:

1:CGLIB工具包

动态创建目标的子类作为代理类,所有的类都可以用它来实现代理

2:JDK Proxy API

动态创建接口的实现类作为代理类,带有接口的类可以由它实现代理

可以知道后者有一定要求。

那么Spring如何使用动态代理技术实现AOP这种功能呢?

一张图可以明白

这里写图片描述

我们可以通过以上的实验结论验证

当没有使用AOP配置时:

这里写图片描述

使用了AOP配置后:

这里写图片描述

在目标组件中有这样一行代码

System.out.println("目标组件"+ed.getClass().getName());

这是输出目标组件的类名,调用AOP之后目标组件的类名便是代理类的类名,代理类是继承EntityDao的一个子类。

根据AOP的通知类型可以使得调用方面组件和super.save()方法顺序可变。

知道了AOP的动态代理技术,了解AOP的异常通知类型也就容易了。

在代理类中处理异常的机制是这样

try{
    super.save();
}catch(Exception e){
    logUtil.log2(e);
    }

当目标组件发生异常时,Sping 会将异常传给方面组件,然后在方面组件方法内部处理异常和输出异常日志。

最后介绍环绕通知的写法

增加一个处理环绕通知的方法log3

package com.hnust.aspect;

import org.aspectj.lang.ProceedingJoinPoint;
import org.springframework.stereotype.Component;

@Component
public class LogUtil {

    public void log1(){
        System.out.println("**数据库操作日志已记录**");
    }

    public void log2(Exception e){
        System.out.println("调用异常通知日志");
        //得到异常数组并输出
        String error = e.getStackTrace()[0].toString();
        System.out.println(error);
    }
    public Object log3(ProceedingJoinPoint pj)throws Throwable{
        System.out.println("调用log3()记录日志-环绕通知前置");
        Object obj=pj.proceed();
        System.out.println("调用log3()记录日志-环绕通知后置");
        return  obj;
    }
}

在配置文件中配置环绕通知类型

<?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.hnust"></context:component-scan>
    <!--AOP配置-->
    <aop:config>
        <!--声明方面组件-->
        <aop:aspect ref="logUtil">
            <!--前置、后置、最终通知-->
            <aop:after-returning method="log1"
                        pointcut="within(com.hnust.service.*)"/>

            <!--异常通知-->
            <aop:after-throwing method="log2" throwing="e"
                                pointcut="within(com.hnust.service.*)"/>

            <!--环绕通知-->
            <aop:around method="log3"
                                pointcut="within(com.hnust.service.*)"/>
        </aop:aspect>
    </aop:config>
</beans>

运行后:

这里写图片描述

环绕通知的原理:

代理对象在复写目标组件方法中做了两件事:

1.目标组件方法new了一个ProcessdingJoinPoint()对象,同时初始化目标组件对象,放到ProcessdingJoinPoint()对象中。

2.logUtil.log3(p)。

LogUtil类中log3方法怎么处理的呢?

前置内容

p.proceed()—-执行目标方法

后置内容



版权声明:本文为superxiaolong123原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。