Spring(4)之AOP配置

Spring(4)之AOP配置

微信搜索 zze_coding 或扫描 👉 二维码关注我的微信公众号获取更多资源推送:

介绍

概述

在软件业,AOP 为 Aspect Oriented Programming 的缩写,意为:面向切面编程,通过预编译方式和运行期动态代理实现程序功能的统一维护的一种技术。AOP 是 OOP 的延续,是软件开发中的一个热点,也是 Spring 框架中的一个重要内容,是函数式编程的一种衍生范型。利用 AOP 可以对业务逻辑的各个部分进行隔离,从而使得业务逻辑各部分之间的耦合度降低,提高程序的可重用性,同时提高了开发的效率。

AOP 思想最早是由 AOP 联盟组织提出,Spring 是目前使用这种思想最好的框架。

Spring 本身有自己的 AOP 实现方式,但是非常繁琐。AspectJ 是一个 AOP 框架,Spring 后期引入了 AspectJ 用作自身 AOP 开发。

Spring 中 AOP 实现原理

Spring 底层是使用动态代理技术实现 AOP。当被代理类实现了接口,此时就会使用 JDK 动态代理方式生成代理对象。当被代理类未实现接口,此时 Spring 就会使用 Cglib 动态代理方式生成代理对象。

AOP相关术语

连接点(Joinpoint)

程序执行的某个特定位置:如类开始初始化前、类初始化后、类某个方法调用前、调用后、方法抛出异常后。一个类或一段程序代码拥有一些具有边界性质的特定点,这些点中的特定点就称为“连接点”。Spring 仅支持方法的连接点,即仅能在方法调用前、方法调用后、方法抛出异常时以及方法调用前后这些程序执行点织入增强。连接点由两个信息确定:第一是用方法表示的程序执行点;第二是用相对点表示的方位。

通俗讲其实就是可以被拦截到的点(方法),增删改查方法都可以被拦截增强,这些方法就可以称为是连接点。

切入点(Pointcut)

每个程序类都拥有多个连接点,如一个拥有两个方法的类,这两个方法都是连接点,即连接点是程序类中客观存在的事物。AOP 通过“切点”定位特定的连接点。连接点相当于数据库中的记录,而切点相当于查询条件。切点和连接点不是一对一的关系,一个切点可以匹配多个连接点。在 Spring 中,切点通过 org.springframework.aop.Pointcut 接口进行描述,它使用类和方法作为连接点的查询条件,Spring AOP 的规则解析引擎负责切点所设定的查询条件,找到对应的连接点。其实确切地说,不能称之为查询连接点,因为连接点是方法执行前、执行后等包括方位信息的具体程序执行点,而切点只定位到某个方法上,所以如果希望定位到具体连接点上,还需要提供方位信息。

通俗讲就是真正被拦截到的点(方法),如果在开发中只对 save 方法进行增强,那么 save 方法就称为是切入点。

增强、通知(Advice)

增强是织入到目标类连接点上的一段程序代码,在 Spring 中,增强除用于描述一段程序代码外,还拥有另一个和连接点相关的信息,这便是执行点的方位。结合执行点方位信息和切点信息,我们就可以找到特定的连接点。

通俗讲就是方法层面的增强,如果要使用 checkPermission 方法进行权限校验,那么 checkPermission 方法就称为是增强。

有如下几种类型的通知:

  • 前置通知:在目标方法执行之前进行的操作。
  • 后置通知:在目标方法执行之后进行的操作。
  • 环绕通知:在目标方法执行之前和之后进行的操作。
  • 异常抛出通知:在程序出现异常时进行的操作。
  • 最终通知:无论代码是否有异常,总是会执行。

目标对象(Target)

增强逻辑的织入目标类。如果没有 AOP,目标业务类需要自己实现所有逻辑,而在 AOP 的帮助下,目标业务类只实现那些非横切逻辑的程序逻辑,而性能监视和事务管理等这些横切逻辑则可以使用 AOP 动态织入到特定的连接点上。

通俗讲就是指被增强的类,如果对 UserDao 类进行增强,那么 UserDao 类就是目标对象。

引介(Introduction)

引介是一种特殊的增强,它为类添加一些属性和方法。这样,即使一个业务类原本没有实现某个接口,通过 AOP 的引介功能,我们可以动态地为该业务类添加接口的实现逻辑,让业务类成为这个接口的实现类。

通俗讲就是类层面的增强。

织入(Weaving)
织入是将增强添加对目标类具体连接点上的过程。AOP 像一台织布机,将目标类、增强或引介通过 AOP 这台织布机天衣无缝地编织到一起。根据不同的实现技术,AOP 有三种织入的方式:

a、编译期织入,这要求使用特殊的 Java 编译器。
b、类装载期织入,这要求使用特殊的类装载器。
c、动态代理织入,在运行期为目标类添加增强生成子类的方式。

Spring 采用动态代理织入,而 AspectJ 采用编译期织入和类装载期织入。

通俗讲就是将通知(Advice)应用到目标对象(Target)的过程。

代理(Proxy)

一个类被 AOP 织入增强后,就产出了一个结果类,它是融合了原类和增强逻辑的代理类。根据不同的代理方式,代理类既可能是和原类具有相同接口的类,也可能就是原类的子类,所以我们可以采用调用原类相同的方式调用代理类。

通俗讲就是最后返回的代理对象。

切面(Aspect)

切面由切入点和增强(引介)组成,它既包括了横切逻辑的定义,也包括了连接点的定义,Spring AOP 就是负责实施切面的框架,它将切面所定义的横切逻辑织入到切面所指定的连接点中。

通俗讲就是切入点与通知的组合。

使用

AOP 的配置方式也分为 XML 和注解方式两种,看如下示例。

XML 方式

1、导包,除了要引入 6 个基础 jar 包外,还需引入如下 jar 包:

image.png

2、编写切面类:

// com.zze.aspect.MyAspect
package com.zze.aspect;

import org.aspectj.lang.ProceedingJoinPoint;

/**
 * 切面类
 */
public class MyAspect {
    public void before() {
        System.out.println("前置通知");
    }

    public void afterReturning(Object result) {
        System.out.println("后置通知" + result);
    }

    public void around(ProceedingJoinPoint joinPoint) throws Throwable {
        System.out.println("环绕通知前");
        joinPoint.proceed();
        System.out.println("环绕通知后");
    }

    public void afterThrowing(Throwable ex) {
        System.out.println("异常抛出通知" + ex);
    }

    public void after(){
        System.out.println("最终通知");
    }
}

3、编写目标类:

// com.zze.dao.UserDao
package com.zze.dao;

public interface UserDao {
    public void save();

    public Boolean delete();

    public void list();

    public void update();
}
// com.zze.dao.impl.UserDaoImpl
package com.zze.dao.impl;

import com.zze.dao.UserDao;

public class UserDaoImpl implements UserDao {
    public void save() {
        System.out.println("保存操作 from com.zze.dao.impl.UserDaoImpl.save()");
    }

    public Boolean delete() {
        System.out.println("删除操作 from com.zze.dao.impl.UserDaoImpl.delete()");
        return true;
    }

    public void list() {
        System.out.println("查询操作 from com.zze.dao.impl.UserDaoImpl.list()");
        int i = 1 / 0;
    }

    public void update() {
        System.out.println("修改操作 from com.zze.dao.impl.UserDaoImpl.update()");
    }
}

4、配置 applicationContext.xml

<!-- applicationContext.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">
    <!--配置目标对象-->
    <bean name="userDao" class="com.zze.dao.impl.UserDaoImpl" />
    <!--配置切面类,将切面类交给 Spring 管理-->
    <bean name="myAspect" class="com.zze.aspect.MyAspect"/>

    <!--通过 AOP 配置对目标类代理-->
    <aop:config>
        <!--
        表达式配置使用哪些方法类增强哪些类
        语法:
            [访问修饰符] 方法返回值 包名.类名.方法名(参数)
            * 表示通配符
            参数位置使用 .. 匹配任意参数
        -->
        <aop:pointcut id="pc1" expression="execution(* com.zze.dao.UserDao.save(..))"/>
        <aop:pointcut id="pc2" expression="execution(* com.zze.dao.UserDao.delete(..))"/>
        <aop:pointcut id="pc3" expression="execution(* com.zze.dao.UserDao.update(..))"/>
        <aop:pointcut id="pc4" expression="execution(* com.zze.dao.UserDao.list(..))"/>
        <!--配置切面-->
        <aop:aspect ref="myAspect">
            <!--前置通知-->
            <aop:before pointcut-ref="pc1" method="before"/>
            <!--后置通知-->
            <aop:after-returning pointcut-ref="pc2" method="afterReturning" returning="result"/>
            <!--环绕通知-->
            <aop:around pointcut-ref="pc3" method="around"/>
            <!--异常抛出通知-->
            <aop:after-throwing pointcut-ref="pc4" method="afterThrowing" throwing="ex"/>
            <!--最终通知-->
            <aop:after pointcut-ref="pc4" method="after"/>
        </aop:aspect>
    </aop:config>
</beans>

5、测试:

import com.zze.dao.UserDao;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;

import javax.annotation.Resource;

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration("classpath:applicationContext.xml")
public class Test{
    @Resource(name = "userDao")
    private UserDao userDao;

    @Test
    public void test() {
        userDao.save();
        userDao.delete();
        userDao.update();
        userDao.list();
        /*
        前置通知
        保存操作 from com.zze.dao.impl.UserDaoImpl.save()
        删除操作 from com.zze.dao.impl.UserDaoImpl.delete()
        后置通知true
        环绕通知前
        修改操作 from com.zze.dao.impl.UserDaoImpl.update()
        环绕通知后
        查询操作 from com.zze.dao.impl.UserDaoImpl.list()
        最终通知
        异常抛出通知java.lang.ArithmeticException: / by zero
         */
    }
}

这里使用了 Spring 整合 JUnit 测试,需要额外导入 spring-test 包。

注解方式

1、导包,同 XML 配置。

2、编写目标类:

// com.zze.dao.UserDao
package com.zze.dao;

public class UserDao {
    public void save() {
        System.out.println("保存操作 from com.zze.dao.impl.UserDao.save()");
    }

    public Boolean delete() {
        System.out.println("删除操作 from com.zze.dao.impl.UserDao.delete()");
        return true;
    }

    public void list() {
        System.out.println("查询操作 from com.zze.dao.impl.UserDao.list()");
        int i = 1 / 0;
    }

    public void update() {
        System.out.println("修改操作 from com.zze.dao.impl.UserDao.update()");
    }
}

3、编写切面类并使用注解配置:

// com.zze.aspect.MyAspect
package com.zze.aspect;

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;

/**
 * 切面类
 */
@Aspect
public class MyAspect {

    /**
     * 定义一个切入点表达式,在下方可引用
     */
    @Pointcut("execution(* com.zze.dao.UserDao.list(..))")
    public void pc1() {
    }

    /**
     * 前置通知
     */
    @Before(value = "execution(* com.zze.dao.UserDao.save(..))")
    public void before() {
        System.out.println("前置通知");
    }

    /**
     * 后置通知
     */
    @AfterReturning(value = "execution(* com.zze.dao.UserDao.delete(..))", returning = "result")
    public void afterReturning(Object result) {
        System.out.println("后置通知" + result);
    }

    /**
     * 环绕通知
     */
    @Around(value = "execution(* com.zze.dao.UserDao.update(..))")
    public void around(ProceedingJoinPoint joinPoint) throws Throwable {
        System.out.println("环绕通知前");
        joinPoint.proceed();
        System.out.println("环绕通知后");
    }

    /**
     * 异常抛出通知
     */
    @AfterThrowing(value = "pc1()", throwing = "ex")
    public void afterThrowing(Throwable ex) {
        System.out.println("异常抛出通知" + ex);
    }

    /**
     * 最终通知
     */
    @After(value = "pc1()")
    public void after() {
        System.out.println("最终通知");
    }
}

4、配置 applicationContext.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">
    <!--开启注解 AOP 开发-->
    <aop:aspectj-autoproxy/>
    <!--目标类-->
    <bean name="userDao" class="com.zze.dao.UserDao"/>
    <!--切面类-->
    <bean name="myAspect" class="com.zze.aspect.MyAspect"/>
</beans>

5、测试:

package com.zze.test;

import com.zze.dao.UserDao;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration("classpath:applicationContext.xml")
public class Demo2 {
    @Autowired
    private UserDao userDao;

    @Test
    public void test() {
        userDao.save();
        userDao.delete();
        userDao.update();
        userDao.list();
        /*
        前置通知
        保存操作 from com.zze.dao.impl.UserDao.save()
        删除操作 from com.zze.dao.impl.UserDao.delete()
        后置通知true
        环绕通知前
        修改操作 from com.zze.dao.impl.UserDao.update()
        环绕通知后
        查询操作 from com.zze.dao.impl.UserDao.list()
        最终通知
        异常抛出通知java.lang.ArithmeticException: / by zero
         */
    }
}

Copyright: 采用 知识共享署名4.0 国际许可协议进行许可

Links: https://www.zze.xyz/archives/spring4.html

Buy me a cup of coffee ☕.