Spring AOP

  AOP并不是Spring框架所特有的,Spring只是支持AOP编程的框架之一。前面已经谈到过,Spring只支持方法拦截的AOP,在Spring中有4种方式去实现AOP的拦截器:

  • 使用ProxyFactoryBean和对应的接口实现AOP
  • 使用xml配置AOP
  • 使用@AspectJ注解驱动切面
  • 使用AspectJ注入切面

  在四种方式中,真正常用的是用@AspectJ注解的方式去实现切面,有时xml配置也会用在一些遗留项目中或者起辅助作用,而其它两种方式基本不会使用,所以我们就对使用@AspectJ注解驱动切面和使用xml配置AOP做出讨论。

使用@AspectJ注解开发Spring AOP

  使用@AspectJ注解方式已经成为主流,所以先以@AspectJ注解方式详细讨论Spring AOP的开发,有了对@AspectJ注解实现的理解,其它的方式都是大同小异的。先看一下关键步骤:

<?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
        https://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/context
        https://www.springframework.org/schema/context/spring-context.xsd
        http://www.springframework.org/schema/aop
        https://www.springframework.org/schema/aop/spring-aop.xsd">
    <!--
        自动扫描+注解驱动:通过注解进行自动装配
        <context:component-scan base-package="edu.uestc.avatar"/>
        base-package:spring自动扫描该包及子包的bean
    -->
    <context:component-scan base-package="edu.uestc.avatar"/>
    <!-- 开启自动代理 -->
    <aop:aspectj-autoproxy/>
</beans>

选择连接点

  spring是方法级别的AOP框架,我们主要以某个类的某些方法作为连接点,用动态代理的理论来说,就是要拦截哪些方法织入对应AOP通知。为了更好地测试,先建立一个业务接口

package edu.uestc.avatar.service;

import edu.uestc.avatar.domain.Book;
import java.util.List;

public interface BookService {
    Integer save(Book book);
    void removeById(Integer id);
    void update(Book book);
    Book findById(Integer id);
    List<Book> list();
}

  接下来提供一个实现类

package edu.uestc.avatar.service.impl;

import edu.uestc.avatar.dao.BookDao;
import edu.uestc.avatar.domain.Book;
import edu.uestc.avatar.service.BookService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;

@Service
public class BookServiceImpl implements BookService { private AtomicInteger idGenerator = new AtomicInteger(); @Autowired private BookDao dao; @Override public Integer save(Book book) { System.out.println("save()执行"); var id = idGenerator.incrementAndGet(); book.setId(id); dao.save(book); return id; } @Override public void removeById(Integer id) { System.out.println("removeById()执行"); if(id == 0) //测试异常通知 throw new IllegalArgumentException("id不能为0"); dao.removeById(id); } @Override public void update(Book book) { System.out.println("update()执行"); dao.update(book); } @Override public Book findById(Integer id) { System.out.println("findById()执行"); return dao.findById(id); } @Override public List<Book> list() { System.out.println("list()执行"); return dao.list(); } }

  现在将业务实现类里的所有方法作为AOP的连接点,用动态代理的语言就是要为类BookServiceImpl生成代理对象,然后拦截这些业务方法,于是可以产生各种AOP通知方法。

创建切面(切点+增强引介)

  选择好了连接点就可以创建切面了,对于动态代理语言而言,它就如同一个拦截器,在Spring中只要使用@Aspect注解一个类,那么Spring IoC容器就会认为这是一个切面了。

package edu.uestc.avatar.aop;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;

/**
 * 定义切面:切点和增强(引介)组成的,它包括了对横切关注功能的定义,也包括了对连接点的定义
 * 在Spring中只要使用@Aspect注解一个类,那么Spring IoC容器就会认为这是一个切面类
 */
@Component
@Aspect
public class ServiceAspect {
    /**
     * 定义切点@Pointcut:哪些方法要被拦截
     *     @Pointcut("execution(* edu.uestc.avatar.service.*.*(..))")
     *          execution:代表执行方法的时候会触发
     *          *:任意的返回类型
     *          edu.uestc.avatar.service.*.*:位于edu.uestc.avatar.service下所有接口及实现了该接口的子包下的所有方法
     *          (..):任意的参数
     */
    @Pointcut("execution(* edu.uestc.avatar.service.*.*(..))")
    public void pointcut(){

    }

    /**
     * 定义前置通知:在被代理对象调用前调用
     */
    @Before("pointcut()")
    public void before(){
        System.out.println("前置通知:开启事务");
    }

    /**
     * 后置通知:在被代理对象调用后调用
     */
    @AfterReturning(pointcut = "pointcut()")
    public void afterReturning(){
        System.out.println("后置通知:提交事务");
    }

    /**
     * 例外通知(异常通知)
     *      如何得到异常对象
     */
    @AfterThrowing(pointcut = "pointcut()",throwing = "th")
    public void afterThrowing(Throwable th){
        System.out.println("例外通知:记录异常信息==>" + th);
    }

    @After("pointcut()")
    public void after(){
        System.out.println("最终通知:释放资源");
    }

    /**
     * 环绕通知:
     *      区别:有权决定对目标对象调用与否(权限拦截)
     *           改变最终的返回值(最好不要这样干)
     * @param pjd:连接点(被拦截到的方法)
     */
    @Around("pointcut()")
    public Object around(ProceedingJoinPoint pjd) throws Throwable {
        Object ret = null;
        System.out.println("环绕通知:" + pjd);
        ret = pjd.proceed();//调用连接点方法
        System.out.println("方法返回值:" + ret);
        return ret;//可以改变方法的最终返回值
    }
}

定义切点

  确定连接点。在上面的代码清单@Pointcut注解中定义了execution的正则表达式,spring正是通过这个正则表达式判断是否需要拦截业务方法的。这个表达式如下:

  execution(* edu.uestc.avatar.service.*.*(..))具体含义见代码清单注释。
AspectJ的指示器
AspectJ指示器 描述
arg() 限制连接点匹配参数为指定类型的方法
@args() 限制连接点匹配参数为执行注解标注的执行方法
execution() 匹配连接点的执行方法,最常用的匹配,可以使用类似于代码清单中的正则表达式进行匹配
this() 限制连接点匹配AOP代理的Bean,引用为指定类型的类
target() 限制连接点匹配被代理对象为指定的类型 
@target()  限制连接点匹配特定的执行对象,这些对象要符合指定的注解类型
within() 限制连接点匹配指定的包 
@within() 限制连接点匹配指定的类型 
@annotation() 限制匹配带有指定注解的连接点

测试AOP

package edu.uestc.avatar.service;

import edu.uestc.avatar.domain.Book;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

class BookServiceTest {
    private static ApplicationContext context;
    private  static BookService service;
    @BeforeAll
    public static void before(){
        context = new ClassPathXmlApplicationContext("spring-beans.xml");
        service = context.getBean("bookService",BookService.class);
    }

    @Test
    void save() {
        var book = new Book().setTitle("射雕英雄传-jdbc")
                .setAuthor("金庸")
                .setMarketPrice(300f).setSellPrice(180f).setBuyPrice(100f)
                .setPublisher("三联出版社");
        //通过spring IoC容器获取bean
        System.out.println(service.save(book));
    }

    @Test
    void removeById() {
        service.removeById(22);
    }

    @Test
    void update() {
        var book = service.findById(21);
        book.setTitle("spring data jdbc");
        service.update(book);
    }

    @Test
    void findById() {
        System.out.println(service.findById(21));
    }

    @Test
    void list() {
        service.list().forEach(System.out::println);
    }
}

  注意观察运行结果

织入

  织入是生成代理对象并将切面内容放入约定流程的过程。在上述代码中,连接点所在的类都是拥有接口的类,而事实上即使没有接口,Spring也能提供AOP的功能,所以是否拥有接口不是使用Spring AOP的一个强制要求。目标类如果实现了接口,Spring使用JDK的动态代理技术生成代理对象;如果目标类没有实现接口,Spring使用CGLib来生成代理对象。从而织入各个通知。

  动态代理对象是由Spring IoC容器根据描述生成的,一般不需要修改它,对于使用者而言,只要知道AOP属于中的约定就可以使用AOP了,只是在Spring中建议使用接口编程,这样的好处是使定和实现分离,有利于实现变化和替换,更为灵活。

给通知传递参数

package edu.uestc.avatar.aop;
import edu.uestc.avatar.annotation.Privilege;
import edu.uestc.avatar.domain.Book;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;

/**
 * 定义切面:切点和增强(引介)组成的,它包括了对横切关注功能的定义,也包括了对连接点的定义
 * 在Spring中只要使用@Aspect注解一个类,那么Spring IoC容器就会认为这是一个切面类
 */
@Component
@Aspect
public class ServiceAspect {
    /**
     * 定义切点@Pointcut:哪些方法要被拦截
     *     @Pointcut("execution(* edu.uestc.avatar.service.*.*(..))")
     *          execution:代表执行方法的时候会触发
     *          *:任意的返回类型
     *          edu.uestc.avatar.service.*.*:位于edu.uestc.avatar.service下所有的子包下的所有方法
     *          (..):任意的参数
     */
    @Pointcut("execution(* edu.uestc.avatar.service.*.*(..))")
    public void pointcut(){

    }

    /**
     * 定义前置通知:在被代理对象调用前调用
     * 向前置通知传递参数
     */
    @Before("pointcut() && args(book)")
    public void before(Book book){
        System.out.println("前置通知:开启事务" + book);
    }

    /**
     * 后置通知:在连接点调用后调用
     *
     */
    @AfterReturning(pointcut = "pointcut()",returning = "retValue")
    public void afterReturning(Integer retValue){
        System.out.println("后置通知:提交事务,返回结果:" + retValue);
    }

    /**
     * 例外通知(异常通知)
     *      如何得到异常对象
     */
    @AfterThrowing(pointcut = "pointcut()",throwing = "th")
    public void afterThrowing(Throwable th){
        System.out.println("例外通知:记录异常信息==>" + th);
    }

    @After("pointcut()")
    public void after(){
        System.out.println("最终通知:释放资源");
    }

    /**
     * 环绕通知:
     *      区别:有权决定对目标对象调用与否(权限拦截)
     *           改变最终的返回值(最好不要这样干)
     * @param pjd:连接点(被拦截到的方法)
     *
     * 拦截标注了@Privilege方法,意味着这些方法需要对应的权限
     */
    @Around("pointcut() && @annotation(privilege)")
    public Object around(ProceedingJoinPoint pjd, Privilege privilege) throws Throwable {
        Object ret = null;
        System.out.println(pjd.getSignature() + "需要出示权限:" + privilege.permission());
        System.out.println("环绕通知:" + pjd);
        //if(当前登录用户拥有privilege.permission()) {
            ret = pjd.proceed();//调用连接点方法
            System.out.println("方法返回值:" + ret);
        //}
        return ret;//可以改变方法的最终返回值
    }
}

引入(为已有的类添加新的接口)

  Spring AOP只是通过动态代理技术,把各类通知织入到它所约定的流程中,而事实上,有时候我们希望通过引入其他类的方法来获得更好的实现,这时候就可以引入其它的方法了。比如业务类中的save方法要求,如果当图书为空时则不再进行保存,那么可以引入一个新的检测器对其进行检测。先定义一个检测接口:

public interface BookVerifier{
      //检测book对象是否不为空
      default boolean verify(Book book){
            return book != null;
      }
}

  可以在切面类中加入一个新的属性,用于检测Book是否为空

 

使用XML配置AOP

  原理和使用注解是相同的,所以介绍一些用法即可。需要在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
        https://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/context
        https://www.springframework.org/schema/context/spring-context.xsd
        http://www.springframework.org/schema/aop
        https://www.springframework.org/schema/aop/spring-aop.xsd">
    <context:component-scan base-package="edu.uestc.avatar"/>
    <!-- 开启aop的@Aspectj的支持 -->
    <!--<aop:aspectj-autoproxy/>-->

    <!-- 通过xml配置AOP -->
    <aop:config>
        <!-- 定义切面(切点 + 通知) -->
        <aop:aspect ref="serviceAspect2">
            <!-- 配置切点 -->
            <aop:pointcut id="pointcut" expression="execution(* edu.uestc.avatar.service.*.*(..))"/>
            <!-- 前置通知-->
            <aop:before method="before" pointcut="execution(* edu.uestc.avatar.service.*.*(..)) and args(book)"/>
            <aop:after-returning method="afterReturning" pointcut-ref="pointcut" returning="retValue"/>
            <aop:after method="after" pointcut-ref="pointcut"/>
            <aop:after-throwing method="afterThrowing" pointcut-ref="pointcut" throwing="th"/>
            <aop:around method="around" pointcut="execution(* edu.uestc.avatar.service.*.*(..)) and @annotation(privilege)"/>
        </aop:aspect>
    </aop:config>
</beans>

 

posted @ 2023-08-25 17:00  Tiger-Adan  阅读(191)  评论(0编辑  收藏  举报