前言
转眼间,快到夏天了,又让我想起来往年盛夏时,被空调、西瓜、冰淇淋支配的恐惧,南方的天气是真的热,在这种天气下,西瓜、冰淇淋可以没有,但是空调是必不可少的。但是空调的缺点是耗电,而电需要钱(这不废话吗)。为了享受凉爽和舒适,我们没有什么办法可以避免这种开销。这是因为每家每户都有一个电表来记录用电量,每个月都会有人来查电表(不是查水表就行),这样电力公司就知道应该收取多少费用了,用户也没办法赖账。
现在想象一下,如果没有电表,也没有人来查看用电量,假设现在由用户来联系电力公司并报告自己的用电量。虽然可能会有一些特别执着的用户会详细记录使用电灯、电视以及空调的情况,但大多数人肯定不会这么做。基于信用的电力收费对于消费者来说可能非常不错,但对于电力公司来说结果可能就不那么美妙了。
监控用电量是一个很重要的功能,但并不是大多数家庭关注的问题。所有家庭实际上所关注的可能是打扫卫生、更换电器、购买每天的食材等事项。从家庭的角度来看,监控房屋的用电量是一个被动事件。
软件系统中的一些功能就像我们家里面的电表一样。这些功能需要用到应用程序的多个地方,但是我们又不想在每个点都明确调用它们,因为这样会导致类似的代码重复出现在各个地方。日志、安全和事务管理的确都很重要,但它们是否为应用对象主动参与的行为呢?如果让应用对象只关注于自己所针对的业务逻辑问题,而其他方面的问题由其他应用对象来处理,这会不会更好?就好比让电表来帮我监控用电量,让电力公司来按电表记录收取费用即可,而我们不需要去过多的关心这些问题。
在软件开发中,散布于应用多处的功能被称为横切关注点(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技术如何应用在事务管理上,也知道了要将一些非核心关注点,但是又很多地方需要使用的功能交给切面去完成,并且需要把切面模块化,这样才能提高切面的复用性。
原文地址:http://blog.51cto.com/zero01/2083465