面向切面的Spring

前言

转眼间,快到夏天了,又让我想起来往年盛夏时,被空调、西瓜、冰淇淋支配的恐惧,南方的天气是真的热,在这种天气下,西瓜、冰淇淋可以没有,但是空调是必不可少的。但是空调的缺点是耗电,而电需要钱(这不废话吗)。为了享受凉爽和舒适,我们没有什么办法可以避免这种开销。这是因为每家每户都有一个电表来记录用电量,每个月都会有人来查电表(不是查水表就行),这样电力公司就知道应该收取多少费用了,用户也没办法赖账。

现在想象一下,如果没有电表,也没有人来查看用电量,假设现在由用户来联系电力公司并报告自己的用电量。虽然可能会有一些特别执着的用户会详细记录使用电灯、电视以及空调的情况,但大多数人肯定不会这么做。基于信用的电力收费对于消费者来说可能非常不错,但对于电力公司来说结果可能就不那么美妙了。

监控用电量是一个很重要的功能,但并不是大多数家庭关注的问题。所有家庭实际上所关注的可能是打扫卫生、更换电器、购买每天的食材等事项。从家庭的角度来看,监控房屋的用电量是一个被动事件。

软件系统中的一些功能就像我们家里面的电表一样。这些功能需要用到应用程序的多个地方,但是我们又不想在每个点都明确调用它们,因为这样会导致类似的代码重复出现在各个地方。日志、安全和事务管理的确都很重要,但它们是否为应用对象主动参与的行为呢?如果让应用对象只关注于自己所针对的业务逻辑问题,而其他方面的问题由其他应用对象来处理,这会不会更好?就好比让电表来帮我监控用电量,让电力公司来按电表记录收取费用即可,而我们不需要去过多的关心这些问题。

在软件开发中,散布于应用多处的功能被称为横切关注点(cross-cutting concern)。通常来讲,这些横切关注点从概念上是与应用的业务逻辑相分离的(但是往往会直接嵌入到应用的业务逻辑之中)。把这些横切关注点与业务逻辑相分离正是面向切面编程(AOP)所要解决的问题。


什么是面向切面编程

AOP(Aspect Oriented Programming),即面向切面编程,可以说是OOP(Object Oriented Programming,面向对象编程)的补充和完善。OOP引入封装、继承、多态等概念来建立一种对象层次结构,用于模拟公共行为的一个集合。不过OOP允许开发者定义纵向的关系,但并不适合定义横向的关系,例如日志功能。日志代码往往横向地散布在所有对象层次中,而与它对应的对象的核心功能毫无关系对于其他类型的代码,如安全性、异常处理和透明的持续性也都是如此,这种散布在各处的无关的代码被称为横切(cross cutting),在OOP设计中,它导致了大量代码的重复,而不利于各个模块的重用。

AOP技术恰恰相反,它利用一种称为"横切"的技术,剖解开封装的对象内部,并将那些影响了多个类的公共行为封装到一个可重用模块,并将其命名为"Aspect",即切面。所谓"切面",简单说就是那些与业务无关,却为业务模块所共同调用的逻辑或责任封装起来,便于减少系统的重复代码,降低模块之间的耦合度,并有利于未来的可操作性和可维护性。

使用"横切"技术,AOP把软件系统分为两个部分:核心关注点和横切关注点。业务处理的主要流程是核心关注点,与之关系不大的部分是横切关注点。横切关注点的一个特点是,他们经常发生在核心关注点的多处,而各处基本相似,比如权限认证、日志、事务等。AOP的作用在于分离系统中的各种关注点,将核心关注点和横切关注点分离开来,将横切关注点进行模块化。

如前所述,切面能帮助我们模块化横切关注点。简而言之,横切关注点可以被描述为影响应用多处的功能。例如,安全就是一个横切关注点,应用中的许多方法都会涉及到安全规则,毕竟客户端所输入的数据永远是不可信的。下图直观呈现了横切关注点的概念:

上图展现了一个被划分为模块的典型应用。每个模块的核心功能都是为特定业务领域提供服务,但是这些模块都需要类似的辅助功能,例如安全和事务管理。

如果要复用通用功能的话,最常见的面向对象的技术是继承(inheritance)或委托(delegation)。但是,如果在整个应用中都使用相同的基类,继承往往会导致一个脆弱的对象体系,面向对象的设计原则之一的合成/聚合复用原则,就强调了尽量使用合成/聚合,尽量不要使用类继承;而使用委托可能需要对委托对象进行复杂的调用。

切面提供了取代继承和委托的另一种可选方案,而且在很多场景下更清晰简介。在使用面向切面编程时,我们仍然在一个地方定义通用功能,但是可以通过声明的方式定义这个功能要以何种方式在何处应用,而无需修改受影响的类。以上也提到了横切关注点可以被模块化为特殊的类,这些类就被称为切面。这样做有两个好处:首先,现在每个关注点都集中于一个地方,而不是分散到多处代码中,其次,服务模块更简洁,因为它们只包含主要关注点(或核心功能)的代码,而次要关注点的代码被转移到切面中了。


定义 AOP 术语

与大多数技术一样,AOP已经形成了自己的术语。描述切面的常用术语有通知(advice)、切点(pointcut)和连接点(join point),下图展示了这些概念是如何关联在一起的:

遗憾的是,大多数用于描述AOP功能的术语并不直观,尽管如此,它们现在已经是AOP“行话"的组成部分了,为了理解AOP,我们必须了解这些术语。在我们进入某个领域之前,必须学会在这个领域该如何说话。

通知(Advice)

当抄表员出现在我们家门口时,它们要登记用电量并回去向电力公司报告。显然,它们必须有一份需要抄表的住户清单,它们所汇报的信息也很重要,但记录用电量才是抄表员的主要工作。

类似的,切面也有目标——它必须要完成的工作。在AOP术语中,切面的工作被称为通知

通知定义了切面是什么以及何时调用。除了描述切面要完成的工作,通知还解决了何时执行这个工作的问题。它应该在某个方法被调用之前?之后?之前和之后都调用?还是只在方法抛出异常时调用?

Spring切面可以应用5种类型的通知:

  • 前置通知(Before):在目标方法被调用之前调用通知功能
  • 后置通知(After):在目标方法被调用之后调用通知,此时不会关心方法的输出是什么
  • 返回通知(After-returning):在目标方法成功执行之后调用通知
  • 异常通知(After-throwing):在目标方法抛出异常后调用通知
  • 环绕通知(Around):通知包裹了被通知的方法,在被通知的方法调用之前和调用之后执行自定义行为

连接点(Join point)

电力公司为多个住户提供服务,甚至可能是整个城市。每家都有一个电表,这些电表上的数字都需要读取,因此每家都是抄表员的潜在目标。抄表员也许能够读取各种类型的设备,但是为了完成他的工作,他的目标应该是房屋内所安装的电表。

同样,我们的应用可能也有数以千计的时机应用通知。这些时机被称为连接点。连接点是在应用执行过程能够插入切面的一个点。这个点可以是调用方法时、抛出异常时、甚至修改一个字段时。切面代码可以利用这些点插入到应用的正常流程之中,并添加新的行为。

切点(Pointct)

如果让一位抄表员访问电力公司所服务的所有住户,那肯定是不现实的。实际上,电力公司为每一个抄表员都分别指定某一块区域的住户。类似的,一个切面并不需要通知应用的所有连接点。切点有助于缩小切面所通知的连接点的范围。

如果说通知定义了切面的 “什么” 和 “何时” 的话,那么切点就定义了 “何处”。切点的定义会匹配通知所要织入的一个或多个连接点。我们通常使用明确的类和方法名称,或是利用正则表达式定义所匹配的类和方法名称来指定这些切点。有些AOP框架允许我们创建动态的切点,可以根据运行时的决策(比如方法中的参数值)来决定是否应用通知。

切面(Aspect)

当抄表员开始一天的工作时,他知道自己要做的事情(报告用电量)和从哪些房屋收集信息。因此,他知道要完成工作所需要的一切东西。

切面就是通知和切点的结合,通知和切点共同定义了切面的全部内容——它是什么,在何时和何处完成其功能。

引入(Introduction)

引入允许我们向现有的类添加新方法或属性。例如,我们可以创建一个Auditable通知类,该类记录了对象最后一次修改时的状态。这很简单,只需要一个方法,setLastModified(Date date),和一个实例变量来保存这个状态。然后,这个新方法和实例变量就可以被引入到现有的类中,从而可以在无需修改这些现有的类的情况下,让他们具有新的行为和状态。

织入(Weaving)

织入是把切面应用到目标对象并创建新的代理对象的过程。切面在指定的连接点被织入到目标对象中。在目标对象的生命周期里有多个点可以进行织入:

  • 编译期:切面在目标类编译时被织入。这种方式需要特殊的编译器。AspectJ的织入编译器就是以这种方式织入的。
  • 类加载期:切面在目标类加载到JVM时被织入。这种方式需要特殊的类加载器(ClassLoader),它可以在目标类被引入应用之前增强该目标类的字节码。AspectJ 5 的加载时织入(load-time weaving,LTW)就支持这种方式织入切面。
  • 运行期:切面在应用运行的某个时刻被织入。一般情况下,在织入切面时,AOP容器会为目标对象动态的创建一个代理对象,也就是Java中的动态代理模式。Spring AOP就是以这种方式织入切面的。

想学好AOP面向切面编程,要掌握的东西可不少,虽然Spring AOP框架帮我们简化了这种面向切面编程的方式,但是我们得学习其中的一些基本的概念,可以的话最好研究其实现方式(读源码),不能只知其然而不知其所以然。

现在我们已经了解了如下知识点:

  • 通知包含了需要用于多个用于对象的横切行为
  • 连接点是程序执行过程中能够应用通知的所有点
  • 切点定义了通知被应用的具体位置(在哪些连接点)
  • 切面是通知和切点的结合,通知和切点共同定义了切面的全部内容
  • 引入允许我们在不修改现有类的前提下,向现有的类添加新方法或属性
  • 织入是把切面应用到目标对象并创建新的代理对象的过程

其中关键概念是切点定义了哪些连接点会得到通知。


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;
    }
}

2. @Before 注解让通知方法在目标方法调用之前执行,属于前置通知,示例:

package org.zero01.aop;

import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.stereotype.Component;

@Aspect
@Component
public 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 方法执行了

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

package org.zero01.aop;

import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;

@Aspect
@Component
public class TransactionAOP {

    @After("execution(* org.zero01.dao.DAO.insert(..))")
    public void testAfter() {
        System.out.println("@After 我在目标方法调用完毕后执行");
    }
}

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

运行结果:

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

4. @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
@Component
public class TransactionAOP {

    @AfterReturning("execution(* org.zero01.dao.DAO.insert(..))")
    public void testAfterReturning() {
        System.out.println("@AfterReturning 我在目标方法调用完毕后执行");
    }
}

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

运行结果:

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

5. @AfterThrowing 注解让通知方法在目标方法抛出异常时执行,属于异常通知,示例:

package org.zero01.aop;

import org.aspectj.lang.annotation.AfterThrowing;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;

@Aspect
@Component
public 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。

6. @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
@Component
public 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 方法下面的代码是不会被执行的,例如:

StudentDAO的insert方法修改如下:

    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对象的时候并不是一个代理对象。

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

package org.zero01.aop;

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

@Aspect
@Component
public 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实现简单的数据库事务控制:

1.需求:

  • 现在有一个school库,里面有一张student表以及一张studentLog表,student表用于记录学生信息,studentLog表则用于记录student表的日志信息。要求对student表进行操作时,将操作信息记录日志到studentLog表里,并且要有事务控制,当用户对student表操作失败或程序出现异常时,事务需要进行回滚,两张表都不能写入数据,必须保持两张表的数据一致。
  • student表结构如下:
  • studentLog表结构如下:

2.编写两张表格字段的封装类:

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;
    }
}

package org.zero01.pojo;

import java.util.Date;

public class StudentLog {

    private int log_id;
    private int sid;
    private String sname;
    private int age;
    private String sex;
    private String address;
    private String operation_type;
    private Date log_time;

    public int getLog_id() {
        return log_id;
    }

    public void setLog_id(int log_id) {
        this.log_id = log_id;
    }

    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;
    }

    public String getOperation_type() {
        return operation_type;
    }

    public void setOperation_type(String operation_type) {
        this.operation_type = operation_type;
    }

    public Date getLog_time() {
        return log_time;
    }

    public void setLog_time(Date log_time) {
        this.log_time = log_time;
    }
}

3.编写数据层以及逻辑层的接口:

package org.zero01.dao;

import org.zero01.pojo.Student;

import java.util.List;

public interface DAO {

    public int insert(Student student) throws Exception;

    public int delete(int sid) throws Exception;

    public Student selectById(int sid) throws Exception;

    public List<Student> selectAll() throws Exception;

    public int update(Student student) throws Exception;

}

package org.zero01.dao;

import org.zero01.pojo.StudentLog;

import java.util.List;

public interface LogDAO {

    public int insert(StudentLog studentLog)throws Exception;

    public int delete(int log_id)throws Exception;

    public List<StudentLog> selectAll()throws Exception;

    public int update(StudentLog studentLog)throws Exception;

}

package org.zero01.service;

import org.zero01.pojo.Student;

import java.util.List;

public interface School {

    public int enterSchool(Student student) throws Exception;

    public int deleteStudentData(int sid) throws Exception;

    public Student searchStudentData(int sid) throws Exception;

    public List<Student> searchStudentsData() throws Exception;

    public int alterStudentData(Student student) throws Exception;

}

4.编写切面类,控制数据库事务:

package org.zero01.aop;

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import javax.sql.DataSource;
import java.sql.Connection;
import java.sql.SQLException;

@Aspect
@Component("tranAOP")
public class TransactionAOP {

    private final DataSource dataSource;

    @Autowired
    public TransactionAOP(DataSource dataSource) {
        this.dataSource = dataSource;
    }

    // 保存连接对象的池子
    private ThreadLocal<Connection> threadLocal = new ThreadLocal<Connection>();

    public ThreadLocal<Connection> getThreadLocal() {
        return threadLocal;
    }

    @Pointcut("execution(* org.zero01.service.*.*(..))")
    private void dao() {
    }

    /**
    * @Description: 控制数据库事务
    * @Param: 
    * @return: 
    * @Author: 01
    * @Date: 2018/3/6
    */ 
    @Around("dao()")
    public Object tranController(ProceedingJoinPoint proceedingJoinPoint) throws SQLException {

        Connection connection = null;
        Object result = null;
        try {
            connection = dataSource.getConnection();
            connection.setAutoCommit(false);
            threadLocal.set(connection);
            // 把调用传递到目标方法上
            result = proceedingJoinPoint.proceed();

            connection.commit();
        } catch (Throwable t) {
            if (connection != null) {
                connection.rollback();
                t.printStackTrace();
            }
        } finally {
            if (connection != null) {
                connection.setAutoCommit(true);
                connection.close();
            }
        }
        return result;
    }
}

5.编写数据层的实现类:

package org.zero01.dao;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.zero01.aop.TransactionAOP;
import org.zero01.pojo.Student;

import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;

@Component("stuDAO")
public class StudentDAO implements DAO {

    @Autowired
    private TransactionAOP trabAOP;

    /**
     * @Description: 添加学生数据
     * @Param: 表格的字段封装对象
     * @return: 返回插入行的id
     * @Author: 01
     * @Date: 2018/3/6
     */
    public int insert(Student student) throws SQLException {

        Connection connection = trabAOP.getThreadLocal().get();

        String sql = "INSERT INTO student(sname,age,sex,address) VALUES (?,?,?,?)";
        PreparedStatement preparedStatement = connection.prepareStatement(sql);
        preparedStatement.setString(1, student.getSname());
        preparedStatement.setInt(2, student.getAge());
        preparedStatement.setString(3, student.getSex());
        preparedStatement.setString(4, student.getAddress());
        preparedStatement.executeUpdate();

        ResultSet resultSet = connection.createStatement().executeQuery("SELECT LAST_INSERT_ID()");

        if (resultSet.next()) {
            return resultSet.getInt(1);
        }

        return 0;
    }

    /**
     * @Description: 删除某个学生数据
     * @Param:  要删除行的id
     * @return: 返回影响的行数
     * @Author: 01
     * @Date: 2018/3/6
     */
    public int delete(int sid) throws SQLException {
        Connection connection = trabAOP.getThreadLocal().get();

        String sql = "DELETE FROM student WHERE sid=?";
        PreparedStatement preparedStatement = connection.prepareStatement(sql);
        preparedStatement.setInt(1, sid);

        return preparedStatement.executeUpdate();
    }

    /**
    * @Description: 按id查找某个学生的数据
    * @Param: 要查询行的id
    * @return: 返回查询出来的学生数据
    * @Author: 01
    * @Date: 2018/3/6
    */
    public Student selectById(int sid) throws SQLException {
        Connection connection = trabAOP.getThreadLocal().get();

        String sql = "SELECT * FROM student WHERE sid=?";
        PreparedStatement preparedStatement = connection.prepareStatement(sql);
        preparedStatement.setInt(1, sid);

        ResultSet resultSet = preparedStatement.executeQuery();
        if (resultSet.next()) {
            Student student = new Student();
            student.setSid(resultSet.getInt("sid"));
            student.setSname(resultSet.getString("sname"));
            student.setAge(resultSet.getInt("age"));
            student.setSex(resultSet.getString("sex"));
            student.setAddress(resultSet.getString("address"));

            return student;
        }
        return null;
    }

    /**
    * @Description: 查询全部学生的数据
    * @return: 返回查询出来的数据集合
    * @Author: 01
    * @Date: 2018/3/6
    */
    public List<Student> selectAll() throws Exception {

        Connection connection = trabAOP.getThreadLocal().get();

        String sql = "SELECT * FROM student";
        PreparedStatement preparedStatement = connection.prepareStatement(sql);
        ResultSet resultSet = preparedStatement.executeQuery();

        List<Student> logList = new ArrayList<Student>();
        while (resultSet.next()) {
            Student student = new Student();

            student.setSid(resultSet.getInt("sid"));
            student.setSname(resultSet.getString("sname"));
            student.setAge(resultSet.getInt("age"));
            student.setSex(resultSet.getString("sex"));
            student.setAddress(resultSet.getString("address"));

            logList.add(student);
        }

        return logList;
    }

    /**
    * @Description: 修改某个学生的数据
    * @Param: 表格的字段封装对象
    * @return: 返回影响行数
    * @Author: 01
    * @Date: 2018/3/6
    */
    public int update(Student student) throws SQLException {

        Connection connection = trabAOP.getThreadLocal().get();

        String sql = "UPDATE student SET sname=?,age=?,sex=?,address=? WHERE sid=?";
        PreparedStatement preparedStatement = connection.prepareStatement(sql);
        preparedStatement.setString(1, student.getSname());
        preparedStatement.setInt(2, student.getAge());
        preparedStatement.setString(3, student.getSex());
        preparedStatement.setString(4, student.getAddress());
        preparedStatement.setInt(5, student.getSid());

        return preparedStatement.executeUpdate();
    }
}

package org.zero01.dao;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.zero01.aop.TransactionAOP;
import org.zero01.pojo.StudentLog;

import java.sql.Connection;
import java.sql.Date;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.util.ArrayList;
import java.util.List;

@Component("stuLogDAO")
public class StudentLogDAO implements LogDAO {

    @Autowired
    private TransactionAOP trabAOP;

    /**
    * @Description: 添加日志记录
    * @Param: 表格的字段封装对象
    * @return: 返回影响行数
    * @Author: 01
    * @Date: 2018/3/6
    */
    public int insert(StudentLog studentLog) throws Exception {

        Connection connection = trabAOP.getThreadLocal().get();

        String sql;
        PreparedStatement preparedStatement;

        if (studentLog.getOperation_type().equals("selectAll")) {
            sql = "INSERT INTO studentlog(operation_type,log_time) VALUES ('selectAll',sysdate())";
            preparedStatement = connection.prepareStatement(sql);

            return preparedStatement.executeUpdate();
        }

        sql = "INSERT INTO studentlog(sid,sname,age,sex,address,operation_type,log_time) VALUES (?,?,?,?,?,?,sysdate())";
        preparedStatement = connection.prepareStatement(sql);
        preparedStatement.setInt(1, studentLog.getSid());
        preparedStatement.setString(2, studentLog.getSname());
        preparedStatement.setInt(3, studentLog.getAge());
        preparedStatement.setString(4, studentLog.getSex());
        preparedStatement.setString(5, studentLog.getAddress());
        preparedStatement.setString(6, studentLog.getOperation_type());

        return preparedStatement.executeUpdate();
    }

    /**
     * @Description: 删除日志记录
     * @Param: 要删除行的id
     * @return: 返回影响行数
     * @Author: 01
     * @Date: 2018/3/6
     */
    public int delete(int log_id) throws Exception {
        Connection connection = trabAOP.getThreadLocal().get();

        String sql = "DELETE FROM studentlog WHERE log_id=?";
        PreparedStatement preparedStatement = connection.prepareStatement(sql);
        preparedStatement.setInt(1, log_id);

        return preparedStatement.executeUpdate();
    }

    /**
     * @Description: 查询全部日志记录
     * @return: 返回查询出来的数据集合
     * @Author: 01
     * @Date: 2018/3/6
     */
    public List<StudentLog> selectAll() throws Exception {
        Connection connection = trabAOP.getThreadLocal().get();

        String sql = "SELECT * FROM studentlog";
        PreparedStatement preparedStatement = connection.prepareStatement(sql);
        ResultSet resultSet = preparedStatement.executeQuery();

        List<StudentLog> logList = new ArrayList<StudentLog>();
        while (resultSet.next()) {
            StudentLog studentLog = new StudentLog();
            studentLog.setLog_id(resultSet.getInt("log_id"));
            studentLog.setSid(resultSet.getInt("sid"));
            studentLog.setSname(resultSet.getString("sname"));
            studentLog.setAge(resultSet.getInt("age"));
            studentLog.setSex(resultSet.getString("sex"));
            studentLog.setAddress(resultSet.getString("address"));
            studentLog.setOperation_type(resultSet.getString("operation_type"));
            studentLog.setLog_time(resultSet.getTimestamp("log_time"));

            logList.add(studentLog);
        }

        return logList;
    }

    /**
     * @Description: 修改某条日志记录
     * @Param: 表格的字段封装对象
     * @return: 返回影响行数
     * @Author: 01
     * @Date: 2018/3/6
     */
    public int update(StudentLog studentLog) throws Exception {

        Connection connection = trabAOP.getThreadLocal().get();

        String sql = "UPDATE student SET sid=?,sname=?,age=?,sex=?,address=?,operation_type=?,log_time=? WHERE log_id=?";
        PreparedStatement preparedStatement = connection.prepareStatement(sql);
        preparedStatement.setInt(1, studentLog.getSid());
        preparedStatement.setString(2, studentLog.getSname());
        preparedStatement.setInt(3, studentLog.getAge());
        preparedStatement.setString(4, studentLog.getSex());
        preparedStatement.setString(5, studentLog.getAddress());
        preparedStatement.setString(6, studentLog.getOperation_type());
        preparedStatement.setDate(7, (Date) studentLog.getLog_time());
        preparedStatement.setInt(8, studentLog.getLog_id());

        return preparedStatement.executeUpdate();
    }
}

6.编写逻辑层的实现类:

package org.zero01.service;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.zero01.dao.DAO;
import org.zero01.dao.LogDAO;
import org.zero01.pojo.Student;
import org.zero01.pojo.StudentLog;

import java.util.List;

@Component("schoolService")
public class SchoolService implements School {

    @Autowired
    private DAO dao;
    @Autowired
    private LogDAO logDAO;

    /**
     * @Description: 映射两张表格中相同的字段
     * @Author: 01
     * @Date: 2018/3/6
     */
    public StudentLog stuMap(Student student, String operation_type) {

        StudentLog studentLog = new StudentLog();
        if (student != null) {
            studentLog.setSid(student.getSid());
            studentLog.setSname(student.getSname());
            studentLog.setAge(student.getAge());
            studentLog.setSex(student.getSex());
            studentLog.setAddress(student.getAddress());
        }
        studentLog.setOperation_type(operation_type);

        return studentLog;
    }

    /**
     * @Description: 入学
     * @Param:
     * @return:
     * @Author: 01
     * @Date: 2018/3/6
     */
    public int enterSchool(Student student) throws Exception {

        int sid = dao.insert(student);
        student.setSid(sid);

        return logDAO.insert(stuMap(student, "add"));
    }

    /**
     * @Description: 删除学生数据
     * @Param:
     * @return:
     * @Author: 01
     * @Date: 2018/3/6
     */
    public int deleteStudentData(int sid) throws Exception {
        Student student = dao.selectById(sid);
        if (student != null) {
            student.setSid(sid);
            dao.delete(sid);
        } else {
            return 0;
        }

        return logDAO.insert(stuMap(student, "delete"));
    }

    /**
     * @Description: 搜索某个学生的资料
     * @Param:
     * @return:
     * @Author: 01
     * @Date: 2018/3/6
     */
    public Student searchStudentData(int sid) throws Exception {
        Student student = dao.selectById(sid);
        if (student != null) {
            logDAO.insert(stuMap(student, "selectById"));
        } else {
            return null;
        }
        return student;
    }

    /**
     * @Description: 搜索全部学生的资料
     * @Param:
     * @return:
     * @Author: 01
     * @Date: 2018/3/6
     */
    public List<Student> searchStudentsData() throws Exception {
        List<Student> students = dao.selectAll();
        logDAO.insert(stuMap(null, "selectAll"));
        return students;
    }

    /**
     * @Description: 修改某个学生的资料
     * @Param:
     * @return:
     * @Author: 01
     * @Date: 2018/3/6
     */
    public int alterStudentData(Student studentNew) throws Exception {
        Student studentOld = dao.selectById(studentNew.getSid());
        int row = dao.update(studentNew);
        logDAO.insert(stuMap(studentOld, "alter"));

        return row;
    }
}

经过测试后两张表格的内容如下:

小结:

在以上代码中,我们通过Spring AOP编写了一个切面类,完成了一个简单的事务控制。事务控制与数据库连接对象的开关都交给切面类去完成,这样我们的JDBC代码里就不需要去控制事务了,只需要关注核心的SQL语句即可,也减少了很多重复的代码。

从这个例子里,我们认识到了AOP技术如何应用在事务管理上,也知道了要将一些非核心关注点,但是又很多地方需要使用的功能交给切面去完成,并且需要把切面模块化,这样才能提高切面的复用性。

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • Spring对JDBC的模板支持——JdbcTemplate

    Spring的JdbcTemplate是一个对JDBC的模板封装,它提供了一套JDBC的模板,能让我们写持久层代码时减少多余的代码,简化JDBC代码,使代码看起...

    端碗吹水
  • Spring使用注解配置依赖注入

    大部分情况下,使用Spring配置依赖注入时,都是使用注解来进行配置,因为注解比xml要方便和简单。不过类似于数据源对象这种配置信息容易变更的对象除外,这种对象...

    端碗吹水
  • 简单实现MVC模式

    View接受用户输入,并传递到Controller。 Controller统一进行处理命令,交由Model处理具体的业务。 经过处理Model更新后,通知Vie...

    端碗吹水
  • Spring对JDBC的模板支持——JdbcTemplate

    Spring的JdbcTemplate是一个对JDBC的模板封装,它提供了一套JDBC的模板,能让我们写持久层代码时减少多余的代码,简化JDBC代码,使代码看起...

    端碗吹水
  • SpringBoot集成Redis缓存

    https://www.cnblogs.com/noneplus/p/11532065.html

    Noneplus
  • SpringCloud微服务实战系列(十五)分布式链路跟踪Sleuth与Zipkin实现

    分布式链路追踪,是一种用于分析和监控应用程序的方法,尤其是那些使用微服务架构的那些应用。分布式链路跟踪有助于查找故障发生位置和导致性能低下的原因。

    品茗IT
  • Spring使用注解配置依赖注入

    大部分情况下,使用Spring配置依赖注入时,都是使用注解来进行配置,因为注解比xml要方便和简单。不过类似于数据源对象这种配置信息容易变更的对象除外,这种对象...

    端碗吹水
  • springboot之单元测试

    https://wap.ztestin.com/site/register?usercode=FAAAQwMQGAAXAwQBA3QhExcDHAQDPjVaA...

    小老鼠
  • spinrgboot配置之@PropertySource和@ImportResource

    注意:@ConfigurationProperties(prefix="person")不要注释掉。同时主配置文件中不能有person.properties相同...

    绝命生
  • Spring Boot:整合Shiro权限框架

    Shiro是Apache旗下的一个开源项目,它是一个非常易用的安全框架,提供了包括认证、授权、加密、会话管理等功能,与Spring Security一样属基于权...

    朝雨忆轻尘

扫码关注云+社区

领取腾讯云代金券