Spring对AOP的支持

Posted 凯哥学堂

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Spring对AOP的支持相关的知识,希望对你有一定的参考价值。

声明:本栏目所使用的素材都是凯哥学堂VIP学员所写,学员有权匿名,对文章有最终解释权;凯哥学堂旨在促进VIP学员互相学习的基础上公开笔记。

Spring对AOP的支持

创建切点来定义切面所织入的连接点是AOP框架的基本功能,Spring和AspectJ之间有大量的协作,而且Spring对AOP的支持在很多方面借鉴了AspectJ。

Spring提供了4种类型的AOP支持:

基于代理的经典Spring AOP
纯pojo切面
使用 @AspectJ 注解驱动的切面
注入式AspectJ切面(适用于Spring各版本)

前三种都是Spring AOP实现的变体,Spring AOP构建在动态代理之上,因此,Spring对AOP的支持局限于方法拦截。

Spring的经典AOP模块并不怎么样,虽然曾经的它的确非常棒。但是现在Spring提供了更简洁和干净的面向切面编程方式。引入了简单的声明式AOP和基于注解的AOP之后,Spring经典的AOP看起来就显得非常笨重和过于复杂,所以现在基本都不再使用Spring经典的AOP方式进行面向切面编程了,本文中也不会再介绍经典的Spring AOP。

Spring AOP框架的一些关键知识点:

Spring通知是Java编写的,所以不需要特殊的开发环境就能开发切面,定义通知所应用的切点可以通过注解或xml来进行配置,通常情况下注解比较常用

Spring是在程序运行时通知对象,通过在代理类中包裹切面,Spring在运行期把切面织入到Spring管理的bean中,所以我们不需要特殊的编译器来织入Spring AOP的切面

因为Spring的AOP基于动态代理,所以Spring只支持方法级别的连接点,不过方法级别的拦截已经可以满足大部分的需求。如果需要方法拦截之外的连接点拦截功能,那么我们就需要使用AspectJ来补充Spring AOP的功能。~~

Spring AOP的基本概念我们了解得差不多了,下面来简单介绍一下如何使用Spring AOP创建切面:

首先配置依赖的jar包,我这里使用的是maven工程,pom.xml配置内容如下:

    <dependencies>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-context</artifactId>
            <version>4.3.14.RELEASE</version>
        </dependency>
        <dependency>
            <groupId>c3p0</groupId>
            <artifactId>c3p0</artifactId>
            <version>0.9.1.2</version>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>5.1.44</version>
        </dependency>
        <dependency>
            <groupId>org.aspectj</groupId>
            <artifactId>aspectjrt</artifactId>
            <version>1.8.13</version>
        </dependency>
        <dependency>
            <groupId>org.aspectj</groupId>
            <artifactId>aspectjweaver</artifactId>
            <version>1.8.13</version>
        </dependency>
    </dependencies>

Spring配置文件内容如下:

<?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:p="http://www.springframework.org/schema/p"       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:annotation-config/>
    <context:component-scan base-package="org.zero01"/>
    <aop:aspectj-autoproxy/>

    <bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource"          p:driverClass="com.mysql.jdbc.Driver"          p:jdbcUrl="jdbc:mysql:///school"          p:user="root"          p:password="your_password"          p:maxPoolSize="10"          p:minPoolSize="1"    /></beans>

先来介绍几个注解的作用,最后我们会使用AOP来编写一个数据库的事务控制:

  1. @Aspect 注解用于定义一个切面类,示例:

package org.zero01.aop;import org.aspectj.lang.annotation.Aspect;import org.springframework.stereotype.Component;

@Aspect // 定义这是一个切面类@Component
public class TransactionAOP {
}

在介绍其他注解之前,先说明一下如何编写切点,在Spring AOP中的切点是使用AspectJ的切点表达式来定义的。最重要的一点就是Spring仅支持AspectJ切点指示器的一个子集,因为Spring是基于代理的,而某些切点表达式是与基于代理的AOP无关的。

下表列出了Spring AOP所支持的AspectJ切点指示器:

AspectJ 指示器 描述
args() 限制连接点匹配参数为执行类型的执行方法
@args() 限制连接点匹配参数由执行注解标注的执行方法
execution() 用于匹配是连接点的执行方法
this() 限制连接点匹配AOP代理的Bean引用类型为指定类型的Bean
target() 限制连接点匹配目标对象为指定类型的类
@target() 限制连接点匹配目标对象被指定的注解标注的类
within() 限制连接点匹配匹配指定的类型
@within() 限制连接点匹配指定注解标注的类型
@annotation 限制匹配带有指定注解的连接点

以上Spring所支持的指示器中,只有execution()指示器是实际执行匹配的,而其他的指示器都是用来限制匹配的,所以execution()指示器是我们编写切点时最主要使用的指示器,其他的指示器则只有需要限制匹配的切点时才会使用。 几个常用的 execution 示器表达式介绍: 匹配所有

       execution("* *.*(..)")

匹配所有以set开头的方法

        execution("* *.set*(..))

匹配com包下所有的方法

        execution("* com.david.biz.service.impl.*(..))

匹配com包以及其子包下的所有方法

        execution("* com.david..*(..)")

匹配com包以及其子包下 参数类型为String 的方法

        execution("* com.david..*(java.lang.String))

为了介绍Spring的切面,我们需要有个主题来定义切面的切点。为此,我们定义一个DAO接口:

package org.zero01.dao;import org.zero01.pojo.Student;import java.util.List;public interface DAO {    public int insert(Student student);    public int delete(int sid);    public List<Student> selectAll();    public int update(Student student);

}

DAO 可以包含数据库的增删查改,我们希望这些方法在被调用时能够触发切点所匹配的通知,这样就可以进行事务的控制了,所以这些方法都可以是切点。 DAO的实现类,StudentDAO代码如下:

package org.zero01.dao;import org.springframework.stereotype.Component;import org.zero01.pojo.Student;import java.util.List;@Component("stuDAO")public class StudentDAO implements DAO{    public int insert(Student student) {
        System.out.println("insert 方法执行了");        return 0;
    }    public int delete(int sid) {
        System.out.println("delete 方法执行了");        return 0;
    }    public List<Student> selectAll() {
        System.out.println("selectAll 方法执行了");        return null;
    }    public int update(Student student) {
        System.out.println("update 方法执行了");        return 0;
    }
}

Student表格的字段封装类:

package org.zero01.pojo;import org.springframework.stereotype.Component;@Component("stu")public class Student {    private int sid;    private String sname;    private int age;    private String sex;    private String address;    public int getSid() {        return sid;
    }    public void setSid(int sid) {        this.sid = sid;
    }    public String getSname() {        return sname;
    }    public void setSname(String sname) {        this.sname = sname;
    }    public int getAge() {        return age;
    }    public void setAge(int age) {        this.age = age;
    }    public String getSex() {        return sex;
    }    public void setSex(String sex) {        this.sex = sex;
    }    public String getAddress() {        return address;
    }    public void setAddress(String address) {        this.address = address;
    }
}
  1. @Before 注解让通知方法在目标方法调用之前执行,属于前置通知,示例:

package org.zero01.aop;import org.aspectj.lang.annotation.Aspect;import org.aspectj.lang.annotation.Before;import org.springframework.stereotype.Component;@Aspect@Componentpublic class TransactionAOP {    @Before("execution(* org.zero01.dao.DAO.insert(..))")
    public void testBefore(){
        System.out.println("@Before 我在目标方法调用前执行");
    }
}

测试代码:

package org.zero01.test;import org.springframework.context.ApplicationContext;import org.springframework.context.support.ClassPathXmlApplicationContext;import org.zero01.dao.DAO;import org.zero01.pojo.Student;public class Test {    public static void main(String[] args) {

        ApplicationContext app = new ClassPathXmlApplicationContext("app.xml");
        DAO dao = (DAO) app.getBean("stuDAO");
        Student student = (Student) app.getBean("stu");        // 调用配置了切点的方法
        dao.insert(student);
    }
}

运行结果:

@Before 我在目标方法调用前执行insert 方法执行了
  1. @After 注解让通知方法在目标方法调用完毕之后或者抛出异常时执行,属于后置通知,示例:

package org.zero01.aop;import org.aspectj.lang.annotation.After;import org.aspectj.lang.annotation.Aspect;import org.springframework.stereotype.Component;@Aspect@Componentpublic class TransactionAOP {    @After("execution(* org.zero01.dao.DAO.insert(..))")
    public void testAfter() {
        System.out.println("@After 我在目标方法调用完毕后执行");
    }
}

测试代码和之前一样,略。
运行结果:

insert 方法执行了@After 我在目标方法调用完毕后执行
  1. @AfterReturning 注解让通知方法在目标方法调用完毕之后执行,属于返回通知,它与 @After 注解主要的区别在于当目标方法抛出异常时,使用@AfterReturning配置的方法是不会执行的,而@After 配置的方法会执行,示例:

package org.zero01.aop;import org.aspectj.lang.annotation.AfterReturning;import org.aspectj.lang.annotation.Aspect;import org.springframework.stereotype.Component;@Aspect@Componentpublic class TransactionAOP {    @AfterReturning("execution(* org.zero01.dao.DAO.insert(..))")
    public void testAfterReturning() {
        System.out.println("@AfterReturning 我在目标方法调用完毕后执行");
    }
}

测试代码和之前一样,略。

运行结果:

insert 方法执行了@AfterReturning 我在目标方法调用完毕后执行
  1. @AfterThrowing 注解让通知方法在目标方法抛出异常时执行,属于异常通知,示例:

package org.zero01.aop;import org.aspectj.lang.annotation.AfterThrowing;import org.aspectj.lang.annotation.Aspect;import org.springframework.stereotype.Component;@Aspect@Componentpublic class TransactionAOP {    @AfterThrowing("execution(* org.zero01.dao.DAO.insert(..))")
    public void testAfterThrowing() {
        System.out.println("@AfterThrowing 我在目标方法调用完毕后执行");
    }
}

StudentDAO的insert方法修改如下:

    public int insert(Student student) {
        System.out.println("insert 方法执行了");        throw new NullPointerException();
    }

测试代码和之前一样,略。

运行结果:

insert 方法执行了异常打印......
@AfterThrowing 我在目标方法抛出异常时执行

所以 @AfterReturning 和 @AfterThrowing 合体后就是 @After。

  1. @Around 注解让通知方法在目标方法调用前后都执行,属于环绕通知,示例:

package org.zero01.aop;import org.aspectj.lang.ProceedingJoinPoint;import org.aspectj.lang.annotation.Around;import org.aspectj.lang.annotation.Aspect;import org.springframework.stereotype.Component;@Aspect@Componentpublic class TransactionAOP {   @Around("execution(* org.zero01.dao.DAO.insert(..))")
   public int testAround(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
       System.out.println("@Around 我在目标方法调用前执行");       // 把调用传递到目标方法上
       proceedingJoinPoint.proceed();

       System.out.println("@Around 我在目标方法调用后执行");       return 0;
   }
}

如上代码中,可以看到 @Around 注解的使用方式和之前所介绍到的注解就有些不同了,首先该方法的返回值要和目标方法的返回值一致,然后就是需要接收 ProceedingJoinPoint 类型的参数,Spring会在织入切点时自动将这个参数传递进来。除此之外还需要通过调用该参数对象的 proceed 方法将调用传递到目标方法上,这一点类似于Servlet技术中Filter过滤器的doFilter方法。

这个 @Around 注解之所以能够做到环绕通知,就是因为可以在 proceed 方法的前后写上代码,这样就把 proceed 方法围起来了形成一个环绕通知,就如上的面的示例。

测试代码和之前一样,略。

运行结果:

@Around 我在目标方法调用前执行insert 方法执行了@Around 我在目标方法调用后执行

要注意的是当目标方法抛出异常时,proceed 方法下面的代码是不会被执行的,例如:

StudentDAOinsert方法修改如下:
    public int insert(Student student) {
        System.out.println("insert 方法执行了");        throw new NullPointerException();
}

切面代码以及测试代码和之前一样,略。 运行结果: @Around 我在目标方法调用前后都执行 insert 方法执行了 异常打印...略... 之前我们提到过,Spring的AOP是基于动态代理的,那么我们就来看看拿出来的是不是代理对象,测试代码如下:

package org.zero01.test;import org.springframework.context.ApplicationContext;import org.springframework.context.support.ClassPathXmlApplicationContext;import org.zero01.dao.DAO;public class Test {    public static void main(String[] args) {

        ApplicationContext app = new ClassPathXmlApplicationContext("app.xml");
        DAO dao = (DAO) app.getBean("stuDAO");

        System.out.println(dao.getClass().getName());
    }
}

运行结果:

com.sun.proxy.$Proxy7

从运行结果中可以看到,打印出来的是典型的代理对象名称。但是这只是其中一种情况,因为StudentDAO实现了DAO接口,所以这时候拿出来的是实现了该接口的代理对象。现在我把StudentDAO的实现语句去掉之后,看看拿出来的是否还是代理对象。测试代码如下:

public static void main(String[] args) {

    ApplicationContext app = new ClassPathXmlApplicationContext("app.xml");
    StudentDAO dao = (StudentDAO) app.getBean("stuDAO");

    System.out.println(dao.getClass().getName());
}

运行结果:

org.zero01.dao.StudentDAO

如上,打印的结果并不是代理对象,这是因为StudentDAO没有实现的接口,这时产生的代理类是继承于StudentDAO的,所以拿出StudentDAO对象的时候并不是一个代理对象。

  1. @Pointcut 注解能够在一个切面类里定义可复用的切点,例如以上我们代码中写的指示器表达式基本都是一样的,这时候我们就可以使用 @Pointcut 注解来定义一个可复用的切点,示例:

package org.zero01.aop;import org.aspectj.lang.ProceedingJoinPoint;import org.aspectj.lang.annotation.*;import org.springframework.stereotype.Component;@Aspect@Componentpublic class TransactionAOP {   @Pointcut("execution(* org.zero01.dao.StudentDAO.insert(..))")
   public void dao() {
   }   @Before("dao()")
   public void testBefore(){
       System.out.println("@Before 我在目标方法调用前执行");
   }   @After("dao()")
   public void testAfter() {
       System.out.println("@After 我在目标方法调用完毕后执行");
   }   @AfterReturning("dao()")
   public void testAfterReturning() {
       System.out.println("@AfterReturning 我在目标方法调用完毕后执行");
   }   @AfterThrowing("dao()")
   public void testAfterThrowing() {
       System.out.println("@AfterThrowing 我在目标方法调用完毕后执行");
   }   @Around("dao()")
   public int testBefore(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
       System.out.println("@Around 我在目标方法调用前执行");       // 把调用传递到目标方法上
       proceedingJoinPoint.proceed();

       System.out.println("@Around 我在目标方法调用后执行");       return 0;
   }
}

测试代码:

package org.zero01.test;import org.springframework.context.ApplicationContext;import org.springframework.context.support.ClassPathXmlApplicationContext;import org.zero01.dao.DAO;import org.zero01.pojo.Student;public class Test {    public static void main(String[] args) {

        ApplicationContext app = new ClassPathXmlApplicationContext("app.xml");
        DAO dao = (DAO) app.getBean("stuDAO");
        Student student = (Student) app.getBean("stu");
        dao.insert(student);
    }
}

运行结果:

@Around 我在目标方法调用前执行@Before 我在目标方法调用前执行insert 方法执行了@Around 我在目标方法调用后执行@After 我在目标方法调用完毕后执行@AfterReturning 我在目标方法调用完毕后执行

从这个打印结果中,我们还可以看到这些注解的优先级。 把以上所介绍到的注解总结成表如下:

注解 作用
@Aspect 用于定义一个切面类
@Before 让通知方法在目标方法调用之前执行
@After 让通知方法在目标方法调用完毕之后或者抛出异常时执行
@AfterReturning 让通知方法在目标方法调用完毕之后执行
@AfterThrowing 让通知方法在目标方法抛出异常时执行
@Around 让通知方法在目标方法调用前后都执行


以上是关于Spring对AOP的支持的主要内容,如果未能解决你的问题,请参考以下文章

Spring AOP学习

Spring---Aop

关于Spring——AOP

关于Spring——AOP

spring事务管理aop

spring笔记3-AOP