小言_互联网的博客

Spring之面向切面编程:AOP(八)

412人阅读  评论(0)

文章目录

(一)AOP的概念
(二)spring中AOP的术语和细节
(三)spring基于XML的AOP:编写必要的代码
(四)spring基于XML的AOP:配置步骤
(五)切入点表达式的写法
(六)四种常用的通知类型
(七)通用化切入点表达式
(八)spring中的环绕通知
(九)spring基于注解的AOP配置

(一)AOP的概念

AOP:Aspect Oriented Programming 面向切面编程

百度百科:AOP是通过预编译方式运行期动态代理实现程序功能统一维护的一种技术,利用AOP可以对业务逻辑的各个部分进行隔离,从而使得业务逻辑各部分之间的耦合度降低,提高程序的可重用性,同时提高了开发效率

简单的说:它就是把我们程序重复的代码抽取出来,在需要执行的时候,使用动态代理的技术,在不修改源码的基础上,对我们的已有方法进行增强

其实我们刚才的银行转账案例中使用动态代理实现事务控制就体现了AOP的思想,如下:

我们抽取后,业务层实现类的代码各部分逻辑(事务逻辑和业务逻辑)就被隔离了,耦合性降低了

而Spring可以通过配置xml或者注解来更加简单地实现AOP

(二)spring中AOP的术语和细节

  • Joinpoint(连接点):
    所谓连接点是指那些被拦截到的点
    在spring中,这些点指的是方法,因为 spring 只支持方法类型的连接点
    回到代码上来看,连接点就是业务层接口的方法,如下:

    接口中的方法负责连接业务增强方法,所以被称为连接点
  • Pointcut(切入点):
    所谓切入点是指我们要对哪些Joinpoint进行拦截的定义
    我们结合代码来讲解什么是切入点,我们给业务层接口增加一个test()方法,如下:
    (此时不要在意业务层没有写该方法的实现,不影响我们讲解)

    然后在动态代理那边也写一段代码,实现功能:不对test()方法进行增强
    (说明:此时只要是接口中的方法默认都会被增强

    此时test()方法被排除在外,不会被增强;但是它仍然连接着业务层代码和增强方法
    我们把test()方法总结为:它是连接点,但不是切入点,因为没有被增强
    换而言之,test()方法之外的所有方法,即是连接点,也是切入点
  • Advice通知/增强):
    所谓通知是指拦截到Joinpoint(连接点)之后所要做的事情就是通知
    通知的类型:前置通知后置通知异常通知最终通知环绕通知
    我们之前写的demo里面是动态代理里面的invoke()方法进行拦截的,而拦截之后执行的代码就是Advice(通知),如下:

    注意:txManager对应的类是通知bean,而它在invoke()方法中执行的语句称为通知
  • Introduction(引介):
    引介是一种特殊的通知在不修改类代码的前提下, Introduction 可以在运行期为类动态地添加一些方法或 Field(字段)
    (现在先不介绍引介)
  • Target(目标对象):
    代理的目标对象
  • Weaving(织入):
    是指把增强应用到目标对象来创建新的代理对象的过程
    spring采用动态代理织入,而AspectJ采用编译期织入和类装载期织入(之后介绍)
  • Proxy(代理):
    一个类被 AOP 织入增强后,就产生一个结果代理类
    上图的“返回被增强的代理对象”就是Proxy(代理),同理“创建要代理的对象”则是Target(目标对象)
  • Aspect(切面):
    切入点通知(引介)的结合
    (说明:我们后期需要配置切入点和通知之间的关系,此时一整个配置叫做切面
    配置文件的好处:像之前的test()方法不需要被增强,这些都可以在配置文件配置清楚的

我们再来介绍一些细节,如下:

开发阶段(我们做的):
		编写核心业务代码(开发主线),要求熟悉业务需求-----------大部分程序员来做
		把公用代码抽取出来,制作成通知。(开发阶段最后再做)------AOP 编程人员来做
		在配置文件中,声明切入点与通知间的关系,即切面 --------- AOP 编程人员来做
运行阶段( Spring 框架完成的):
		Spring 框架监控切入点方法的执行
		一旦监控到切入点方法被运行,使用代理机制,动态创建目标对象的代理对象
		根据通知类别,在代理对象的对应位置,将通知对应的功能织入,完成完整的代码逻辑运行

(三)spring基于XML的AOP:编写必要的代码

创建一个普通的maven项目来讲解基于XML的AOP,并且导入maven坐标,如下:
(我们这次用到新的技术:Aspectj

然后创建一个业务层接口,如下:

我们模拟三个方法,分别讲解这三种情况(后面会详解)

接下来写一个业务层实现类,如下:

/**
 * 账户的业务层实现类
 */
public class AccountServiceImpl implements IAccountService {
    public void saveAccount() {
        System.out.println("执行了保存");
    }

    public void updateAccount(int i) {
        System.out.println("执行了更新" + i);

    }

    public int deleteAccount() {
        System.out.println("执行了删除");
        return 0;
    }
}

最后再写一个抽取公共代码的类,如下:

/**
 * 用于记录日志的工具类,它里面提供了公共的代码
 */
public class Logger {

    /**
     * 用于打印日志:计划让其在切入点方法执行之前执行(切入点方法就是业务层方法)
     */
    public void printLog() {
        System.out.println("Logger类中的printLog方法开始记录日志了。。。");
    }
}

此时如果我们想要实现AOP,用之前银行转账案例动态代理的方法是可行的
但是我们接下来使用的是XML配置,这会更加的简单,并且更能体现切面的思想

(四)spring基于XML的AOP:配置步骤

创建一个bean.xml文件,如下:

到官网搜索xmlns:aop,并且复制

<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">

    <!--        配置spring的ioc,把service对象配置进来-->
    <bean id="accountService" class="com.zzq.service.impl.AccountServiceImpl"></bean>
</beans>

接下来就配置AOP,步骤如下:
通知bean也交给spring来管理

    <!--    配置Logger类-->
    <bean id="logger" class="com.zzq.utils.Logger"></bean>

使用aop:config标签表明开始AOP的配置

    <!--    配置AOP-->
    <aop:config>
        
    </aop:config>

使用aop:aspect标签表明配置切面
id属性:给切面提供一个唯一表示(理论上可以随便取,这里取logAdvice,为日志通知的意思)
ref属性:执行通知类bean的id

    <!--    配置AOP-->
    <aop:config>
        <!--        配置切面-->
        <aop:aspect id="logAdvice" ref="logger">

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

aop:aspect标签的内部使用对应的标签来配置通知的类型
我们现在的示例是让printLog方法在切入点方法之前执行:所以是前置通知

aop:before:表明配置前置通知
		method属性:用于指定Logger类中哪个方法是前置通知
		pointcut属性:用于指定切入点表达式,该表达式的含义指的是对业务层中哪些方法增强
切入点表达式的写法:
    关键字:execution(表达式)
    表达式:
        访问修饰符  返回值  包名.包名.包名...类名.方法名(参数列表)
    标准的表达式写法:
        public void com.zzq.service.impl.AccountServiceImpl.saveAccount()
    <!--    配置AOP-->
    <aop:config>
        <!--        配置切面-->
        <aop:aspect id="logAdvice" ref="logger">
            <!--            配置通知的类型,并且建立通知方法和切入点方法的关联-->
            <aop:before method="printLog"
                        pointcut="execution(public void com.zzq.service.impl.AccountServiceImpl.saveAccount())"></aop:before>
        </aop:aspect>
    </aop:config>


接下来编写测试类,如下:

/**
 * 测试AOP的配置
 */
public class AOPTest {

    public static void main(String[] args) {
        //1.获取容器
        ApplicationContext ac = new ClassPathXmlApplicationContext("bean.xml");
        //2.获取对象
        IAccountService as = (IAccountService) ac.getBean("accountService");
        //3.执行方法
        as.saveAccount();
    }
}

运行结果如下:

(五)切入点表达式的写法

接下来详细介绍切入点表达式,我们先看一个现象

因为我们之前的切入点表达式只指定了一个方法,所以只有saveAccount()方法被增强
但是通常情况下,我们是对业务层实现类的所有方法都进行增强的,那应该怎么操作呢?

全通配写法:
    * *..*.*(..)

执行效果如下:

* *..*.*(..)这种写法是怎么来的呢?下面一一分析

访问修饰符可以省略
    void com.zzq.service.impl.AccountServiceImpl.saveAccount()

返回值可以使用通配符,表示任意返回值
    * com.itheima.service.impl.AccountServiceImpl.saveAccount()

包名可以使用通配符,表示任意包。但是有几级包,就需要写几个*.
    * *.*.*.*.AccountServiceImpl.saveAccount())

包名可以使用..表示当前包及其子包
    * *..AccountServiceImpl.saveAccount()

意思是:任意包下,只要有AccountServiceImpl类,都对它的saveAccount()方法进行增强

类名和方法名都可以使用*来实现通配
    * *..*.*()

我们先用*替换掉类名,如下:

再用*替换掉方法名,如下:
(此时两个无参的方法被增强,因为括号里没有指明参数,所以不增强带参方法)

参数列表:
    可以直接写数据类型:
        基本类型直接写名称           int
        引用类型写包名.类名的方式   java.lang.String
    可以使用通配符表示任意类型,但是必须有参数
    可以使用..表示有无参数均可,有参数可以是任意类型

我们尝试加上int试试

全通配写法:
    * *..*.*(..)

这种写法一开始已经展示过了,这里不再展示

实际开发中切入点表达式的通常写法:
    切到业务层实现类下的所有方法
        * com.zzq.service.impl.*.*(..)


我们一开始导入了Aspectj的坐标,它的作用正是:解析切入点表达式

(六)四种常用的通知类型

创建一个普通的maven项目,并且导入相关的maven坐标和代码,如下:

接下来修改Logger类,如下:

/**
 * 用于记录日志的工具类,它里面提供了公共的代码
 */
public class Logger {

    /**
     * 前置通知
     */
    public void beforePrintLog() {
        System.out.println("前置通知:Logger类中的beforePrintLog方法开始记录日志了。。。");
    }

    /**
     * 后置通知
     */
    public void afterReturningPrintLog() {
        System.out.println("后置通知:Logger类中的afterReturningPrintLog方法开始记录日志了。。。");
    }

    /**
     * 异常通知
     */
    public void afterThrowingPrintLog() {
        System.out.println("异常通知:Logger类中的afterThrowingPrintLog方法开始记录日志了。。。");
    }

    /**
     * 最终通知
     */
    public void afterPrintLog() {
        System.out.println("最终通知:Logger类中的afterPrintLog方法开始记录日志了。。。");
    }
}

修改bean.xml,如下:

    <!--    配置AOP-->
    <aop:config>
        <!--        配置切面-->
        <aop:aspect id="logAdvice" ref="logger">
            <!--            配置前置通知-->
            <aop:before method="beforePrintLog"
                        pointcut="execution(* com.zzq.service.impl.*.*(..))"></aop:before>
            <!--            配置后置通知-->
            <aop:after-returning method="afterReturningPrintLog"
                                 pointcut="execution(* com.zzq.service.impl.*.*(..))"></aop:after-returning>
            <!--            配置异常通知-->
            <aop:after-throwing method="afterThrowingPrintLog"
                                pointcut="execution(* com.zzq.service.impl.*.*(..))"></aop:after-throwing>
            <!--            配置最终通知-->
            <aop:after method="afterPrintLog"
                       pointcut="execution(* com.zzq.service.impl.*.*(..))"></aop:after>
        </aop:aspect>
    </aop:config>

测试类只需要执行一个方法即可,如下:

运行结果如下:

我们发现除了异常通知以外,所有通知都出来了
为什么异常通知没有出来呢?因为程序没有抛出异常
我们手动制造异常试试,如下:

因为异常是在业务方法之后产生的,所以业务方法也可以正常执行
如果异常是在业务方法之前产生的,那么业务方法将无法正常执行,如下:

总结

  1. 前置通知:在切入点方法执行之前执行
  2. 后置通知:在切入点方法正常执行之后执行,它和异常通知永远只能执行一个
  3. 异常通知:在切入点执行产生异常之后执行,它和后置通知永远只能执行一个
  4. 最终通知:无论切入点方法是否正常执行,它都会在其后面执行

回顾之前的银行转账案例中涉及到的事务问题,如下:

后置通知和异常通知永远只能执行一个,在上面体现为:提交回滚只能二选一

(七)通用化切入点表达式

我们发现bean.xml中的配置切面的代码有点繁杂,我们能不能简化一下呢?
我们可以把切入点表达式抽取出来,如下:

注意:此标签写在aop:aspect标签内部只能供当前切面使用
如果想供所有切面使用,可以写在aop:aspect标签外面,此时可以供所有切面使用,如下:

但是却发现报错了,因为约束规定这种写法只能把该标签置于切面之前,这是规定好的

运行结果就不展示了

(八)spring中的环绕通知

接下来单独介绍环绕通知,把其它通知先注释掉,如下:

然后配置环绕通知,此时方法是没有的,配置完再去创建,如下:

	<!--        配置环绕通知-->
	<aop:around method="aroundPrintLog" pointcut-ref="pt1"></aop:around>

Logger.java

    /**
     * 环绕通知
     */
    public void aroundPrintLog() {
        System.out.println("环绕通知:Logger类中的aroundPrintLog方法开始记录日志了。。。");
    }

运行测试类,如下:

问题:当我们配置了环绕通知之后,切入点方法没有执行,而通知的方法执行了
分析:通过对比动态代理中的环绕通知代码,发现动态代理的环绕通知有明确的切入点方法调用,而我们的代码中没有。

动态代理的:
(整个invoke()方法为环绕通知,里面调用了切入点方法)

我们的:
(我们的环绕通知没有调用切入点方法,所以也就不会执行了)

解决:Spring框架为我们提供了一个ProceedingJoinPoint接口,该接口有一个proceed()方法,此方法就相当于明确调用切入点方法
该接口可以作为环绕通知的方法参数,在程序执行时,Spring框架会为我们提供该接口的实现类,以供我们使用,如下:

    /**
     * 环绕通知
     */
    public Object aroundPrintLog(ProceedingJoinPoint pjp) {
        Object rtValue = null;
        try {
            Object[] args = pjp.getArgs();//得到方法执行所需的参数
            System.out.println("前置通知:Logger类中的beforePrintLog方法开始记录日志了。。。");
            rtValue = pjp.proceed(args);//明确调用业务层方法(切入点方法)
            System.out.println("后置通知:Logger类中的afterReturningPrintLog方法开始记录日志了。。。");
            return rtValue;
        } catch (Throwable throwable) {
            System.out.println("异常通知:Logger类中的afterThrowingPrintLog方法开始记录日志了。。。");
            throw new RuntimeException(throwable);
        } finally {
            System.out.println("最终通知:Logger类中的afterPrintLog方法开始记录日志了。。。");
        }
    }

此时写在切入点之前的增强方法就是前置通知,写在后面的增强方法就是后置通知,以此类推

总结:环绕通知时Spring框架为我们提供的一种可以在代码中手动控制增强方法何时执行的方式

我们现在可以用bean.xml或者后面的注解去配置,也可以使用环绕通知用代码编写,两种方法都ok

(九)spring基于注解的AOP配置

我们把上一个项目的代码拷贝过来,如下:

配置注解相关的约束,如下:

配置要扫描的包的路径,如下:

    <!--    配置spring创建容器时要扫描的包-->
    <context:component-scan base-package="com.zzq"></context:component-scan>

开启注解AOP的支持,如下

    <!--    配置spring开启注解AOP的支持-->
    <aop:aspectj-autoproxy></aop:aspectj-autoproxy>

注释掉xml配置的bean标签,如下:

以注解取而代之,如下:

Logger类中添加Aspect注解以表明该类是一个切面类,如下:

抽取一个切入点表达式,如下:

    @Pointcut("execution(* com.zzq.service.impl.*.*(..))")
    private void pt1() {
    }

给每一个通知加上注解,并且引用切入点表达式(此时注释掉环绕通知)

运行效果如下:

我们发现最终通知后置通知的顺序有问题,我们看看异常通知是否正常,如下:

可以看到问题依然存在,这是为什么呢?哈哈,其实这是spring的BUG,所以这种写法要慎重考虑
我们不用这四种通知类型,我们试试环绕通知看看

可以看到顺序终于正常了(推荐使用环绕通知
我们回头看看我们的bean.xml,如下:

我们能不能做到纯注解、不适用xml标签呢?首先把配置要扫描的包的路径转换成配置类就不说了
至于第二个,只需要在配置类上加一个注解即可,如下图:

这里不再演示了


转载:https://blog.csdn.net/qq_42528769/article/details/105923756
查看评论
* 以上用户言论只代表其个人观点,不代表本网站的观点或立场