文章目录
AOP简介
AOP(全称Aspect Oriented Programming) 即:面向切面编程。
通过预编译方式
和运行期间动态代理
实现程序功能的统一维护的一种技术。AOP是OOP的延续,是函数式编程的一种衍生范型。利用AOP可以对业务逻辑的各个部分进行隔离,从而使得业务逻辑各部分之间的耦合度降低,提高程序的可重用性,同时提高开发的效率。
- 作用:在程序运行期间,不修改源码对已有方法进行增强
- 优势:
- 减少重复代码
- 提高开发效率
- 维护方便
- AOP的实现方式:动态代理
- 主要应用在性能监控,事务管理,安全检查,缓存等等
AOP相关术语
- 连接点(Jointpoint):连接点指的是被拦截到的点,在spring中,这些点指的是方法
- 切入点(Pointcut):指我们要对那些连接点进行拦截的定义
- 通知/增强(Advice):指的是拦截点连接点之后所要做的事情就是通知,或者是增强
增强划分有5种,前置通知、后置通知、异常通知、最终通知、环绕通知
前置通知:在调用方法之前执行
后置通知:在调用方法之后执行
异常通知:在方法出现异常之后在执行
环绕通知:在调用方法之前,之后都执行
最终通知:在后置通知之后执行
- 切面(Aspect):切入点和通知的结合,把增强应用到切入点的过程
- 引介(Introduction):是一种特殊的通知,在不修改代码的前提下,introduction可以在运行期为类动态的添加一些方法或者属性
- 目标对象(traget ):代理的目标对象
- 织入(Weaving):把增强应用到目标对象的过程
- 代理(Proxy):一个类被AOP织入增强后,就产生了一个代理类
AOP的设计原理和思想
1、AOP横向抽象技术的介绍
例如,我们有一个User类,其中包含一个add()方法,如:
public class User {
//添加用户方法
public void add() {
//业务逻辑
}
}
现在,我们需要对其扩展功能,在添加用户的同时添加日志功能,记录在什么时候添加了那个用户,我们可以采用纵向抽取机制
解决。例如,实现一个扩展类,并使用户User类继承于扩展类,这样我们就可以在添加用户时调用添加日志的方法,如下:
//纵向抽取机制解决
public class BaseUser {
//创建打印日志功能
public void writeLog() {
//添加日志的逻辑
}
}
public class User extends BaseUser {
public void add() {
//扩展功能,添加日志操作
// 调用已存在的日志打印的功能
super.writeLog();
}
}
问题:
父类中的方法名发生修改,在子类的调用都需要修改
解决办法:
我们可以采取AOP横向抽象机制
实现扩展功能,它底层使用的是动态代理
的方式实现。此时,为了解决功能扩展的问题,我们可以对add()
方法提供一个相应的接口,并实现该接口,如下:
public interface UserDao {
public void add();
}
//该接口的真正实现类
public class UserIml implements UserDao {
public void add() {
//业务方法
}
}
然后,我们需要编写一个代理辅助类,并使其实现InvocationHandler
接口。我们可以在此处完成我们需要添加的扩展方法。
public class UserProxy implements InvocationHandler {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
//日志功能添加
method.invoke(object,args);
return null;
}
}
使用动态代理的方式来实现日志添加功能,需要注意的是这里的代理类和实现类是平级关系。在有接口情况下,使用的是JDK的动态代理,在没有接口情况下,使用CGLib的形式进行功能扩展。
程序运行的过程就是方法调用的过程。我们按照方法执行的顺序,将方法调用排成一串,这样就构成了Java程序流。
Java程序执行流
基于时间序列,我们可以将方法调用排成一条线。而每个方法调用则可以看成Java执行流中的一个节点。这个节点在AOP的术语中,被称为Join Point(连接点)
。一个Java程序的运行的过程,就是若干个连接点连接起来依次执行的过程。
通常面向对象的程序,代码都是按照时间序列纵向展开的,而他们都有一个共性:即都是以方法调用作为基本执行单位展开的。将方法调用当做一个连接点,那么由连接点串起来的程序执行流就是整个程序的执行过程。
AOP则是从另外一个角度来考虑整个程序的,AOP将每一个方法调用,即连接点作为编程的入口,针对方法调用进行编程。从执行的逻辑上来看,相当于在之前纵向的按照时间轴执行的程序横向切入。相当于将之前的程序横向切割成若干的面,即Aspect。每个面被称为切面。
所以,根据我的理解,AOP本质上是针对方法调用的编程思路。
AOP是针对切面进行的编程的,那么,你需要选择哪些切面(即 连接点Joint Point)作为你的编程对象呢?
因为切面本质上是每一个方法调用,选择切面的过程实际上就是选择方法的过程。那么,被选择的切面(Aspect)在AOP术语里被称为切入点(Point Cut)
。 切入点实际上也是从所有的连接点(Join point)
挑选自己感兴趣的连接点的过程。
在Spring中,AOP如何来捕获方法的调用呢?其实其本身是使用了代理模式的Java程序执行流。
2、使用代理模式的Java程序执行流
假设在Java代码中,实例对象是通过代理模式创建的代理对象,那么访问实例对象必须通过代理。
由上图可以看出,只要想调用某一个实例对象的方法时,都会经过这个实例对象相对应的代理对象, 即执行的控制权先交给代理对象。
代理模式属于Java代码中经常用到的、也是比较重要的设计模式。代理模式可以为某些对象除了实现本身的功能外,提供一些额外的功能,大致作用如下图所示:
加入了代理模式的Java程序执行流,使得所有的方法调用都经过了代理对象。对于Spring AOP框架而言,它负责控制着整个容器内部的代理对象
。当我们调用了某一个实例对象的任何一个非final的public方法
时,整个Spring框架都会知晓。既然Spring代理层可以察觉到你所做的每一次对实例对象的方法调用,那么,Spring就有机会在这个代理的过程中插入Spring的自己的业务代码
。
3、Spring AOP的工作原理
AOP的编程首先要选择自己感兴趣的Join Point(连接点)
——即切入点(Point cut)
,AOP对切入如何做编程呢?其实是在代理模式下通过找到某个连接点的细化来实现横向切面编程。
为了降低我们对Spring的AOP的理解难度,我在这里将代理角色的职能进行了简化,方便大家理解。(注意:真实的Spring AOP的proxy角色扮演的只能比这复杂的多,这里只是简化,方便大家理解,请不要先入为主)
代理模式的代理角色最起码要考虑三个阶段:
- 在调用真正对象的方法之前,应该需要做什么?
- 在调用真正对象的方法过程中,如果抛出了异常,需要做什么?
- 在调用真正对象的方法后,返回了结果了,需要做什么?
Spring AOP 根据proxy提供的类型名和方法签名,确定了在其感兴趣的切入点内,则返回advice(建议),proxy得到这个处理建议,然后执行建议。
上述的示意图中已经明确表明了Spring AOP应该做什么样的工作:根据proxy提供的特定类的特定方法执行的特定时期阶段给出相应的处理建议。要完成该工作,Spring AOP应该实现:
- 确定自己对什么类的什么方法感兴趣?——即确定 AOP的切入点(Point Cut),这个可以通过切入点(Point Cut)表达式来完成;
- 对应的的类的方法的执行特定时期给出什么处理建议?——这个需要Spring AOP提供相应的建议 ,即我们常说的Advice。
AOP的使用
我们在Spring框架中实现AOP功能主要是通过AspectJ实现的,AspectJ是一个面向切面的框架,它扩展了Java语言。AspectJ定义了AOP语法,它有一个专门的编译器用来生成遵守Java字节编码规范的Class文件。需要注意的是,AspectJ并不是Spring框架的一部分,它只是用于和Spring结合在一起共同完成有关AOP的操作而已。
AOP结合AspectJ的使用主要有两种方式:
- 基于XML配置的方式实现
- 基于注解的方式实现
1、引入AOP需要的相关jar包
<!--Spring的AOP jar包-->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aop</artifactId>
<version>4.3.2.RELEASE</version>
</dependency>
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.7.4</version>
</dependency>
<dependency>
<groupId>aopalliance</groupId>
<artifactId>aopalliance</artifactId>
<version>1.0</version>
</dependency>
2、配置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-3.0.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop.xsd">
</beans>
3、写出相关实例(实现类和增强类)
场景:在基础业务逻辑上增加某个功能的实现。例如,在书本相关操作的基础上添加日志操作。
实现类:
//基础类
public class Book {
//方法1:写方法
public void writeBook() {
//业务逻辑
System.out.println("writeBook");
}
//读方法
public void readBook() {
//业务逻辑
System.out.println("readBook");
}
}
增强类:
/**
* 增强类
* 完成添加日志
*/
public class DiyLog {
public void log() {
//业务逻辑
System.out.println("打印日志");
}
}
使用表达式来配置切入点:
execution
表达式
- 通过
execution
函数,可以来定义切点的方法切入 - 切入点:实际增强的方法。
表达式的样式:
execution(<访问限定符>?<返回类型> <方法名>(<参数>)<异常>)
execution(* com.tulun.service.UserService.login(..))
,表示会拦截UserService.下的login方法做增强execution(* com.tulun.service.*(..))
, service包路径下的所有的类的所有的方法execution(* com.tulun.service.MailService.*(..))
,MailService类下的所有的方法
- 匹配所有的public的方法:
execution(public *.*(..))
- 匹配指定包路径下的所有类方法(不包含子包):
execution(* com.tulun.service.*(..))
- 匹配指定包路径下的所有类方法(包含子包):
execution(* com.tulun.service..*(..))
- 匹配特定的接口的所有类方法:
execution(* com.tulun.service.User+.*(..))
- 匹配所有以com开头的方法:
execution(* com.*(..))
(1)基于XML配置型的实现
4、配置切入点
- 前置通知:
<?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-3.0.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop.xsd">
<!--将基本对象交给spring管理-->
<bean id="book" class="com.test.bean.Book"/>
<bean id="diyLog" class="com.test.bean.DiyLog"/>
<!--配置AOP操作-->
<aop:config>
<!--配置切入点:使用表达式,在实际的方法上添加通知的方法称之为切入点-->
<aop:pointcut id="pointcut1" expression="execution(* com.test.bean.Book.writeBook(..))"/>
<!--配置切面:把增强用到方法-->
<aop:aspect ref="diyLog">
<!--配置增强类型 method:增强类中的那个方法作为前置增强-->
<aop:before method="log" pointcut-ref="pointcut1"/>
<!--
aop:before:前置通知
aop:after 后置通知
aop:after-returning:最终通知
aop:after-throwing:异常通知
aop:around:环绕通知
-->
</aop:aspect>
</aop:config>
</beans>
5、使用AOP
public class Test {
public static void main(String[] args) {
//获取IOC的容器
ClassPathXmlApplicationContext applicationContext = new ClassPathXmlApplicationContext("applicationContext.xml");
//通过容器来获取当前的对象(通过无参构造来实例对象方法)
//获取book对象
//验证前置通知
Book book = (Book) applicationContext.getBean("book");
book.writeBook();
}
}
执行结果:
- 环绕通知:
import org.aspectj.lang.ProceedingJoinPoint;
/**
* 增强类
* 完成添加日志
*/
public class DiyLog {
public void log() {
//业务逻辑
System.out.println("打印日志");
}
//环绕通知 需要通过ProceedingJoinPoint类型的参数指定增强方法执行的时机
public void round(ProceedingJoinPoint joinPoint) throws Throwable{
System.out.println("方法执行前");
//执行增强的方法
joinPoint.proceed();
System.out.println("方法执行之后");
}
}
<?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-3.0.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop.xsd">
<!--将基本对象交给spring管理-->
<bean id="book" class="com.test.bean.Book"/>
<bean id="diyLog" class="com.test.bean.DiyLog"/>
<!--配置AOP操作-->
<aop:config>
<!--配置切入点:使用表达式,在实际的方法上添加通知的方法称之为切入点-->
<aop:pointcut id="pointcut1" expression="execution(* com.test.bean.Book.writeBook(..))"/>
<!--配置切面:把增强用到方法-->
<aop:aspect ref="diyLog">
<!--配置增强类型 method:增强类中的那个方法作为前置增强-->
<aop:around method="round" pointcut-ref="pointcut1"/>
<!--
aop:before:前置通知
aop:after 后置通知
aop:after-returning:最终通知
aop:after-throwing:异常通知
aop:around:环绕通知
-->
</aop:aspect>
</aop:config>
</beans>
执行结果:
(2)基于注解的方式实现
4、配置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-3.0.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop.xsd">
<!--将接触类和增强类交给IOC容器管理-->
<bean id="book" class="com.test.bean.Book7"/>
<bean id="diyLog" class="com.test.bean.DiyLog"/>
<!--开启AOP的操作-->
<aop:aspectj-autoproxy></aop:aspectj-autoproxy>
</beans>
5、在增强类上添加注解
@Aspect
public class DiyLog {
//前置增强
@Before(value = "execution(* com.test.bean.Book.readBook(..))")
//<aop:before method="round" pointcut-ref="pointcut1"/>
public void log() {
//业务逻辑
System.out.println("打印日志");
}
}
基于注解的AOP实现方式: 在增强类上添加@Aspect
注解。在该类中用哪一个方法来作为增强,就在方法上添加以下注解:
@Before(value = "execution(* com.test.bean.Book.readBook(..))")
,前置通知。@After(value = "")
,后置通知。@AfterThrowing
,异常通知。@Around(value = "")
,环绕通知 ,需要在方法入参上添加ProceedingJoinPoint
参数,见上面基于XML形式的具体实现。@AfterReturning
,最终通知。
6、使用AOP
public class Test {
public static void main(String[] args) {
//获取IOC的容器
ClassPathXmlApplicationContext applicationContext = new ClassPathXmlApplicationContext("applicationContext.xml");
//通过容器来获取当前的对象(通过无参构造来实例对象方法)
//获取book对象
//验证前置通知
Book book = (Book) applicationContext.getBean("book");
book.readBook();
}
执行结果: