在之前讲解了Spring中的IOC思想,接下来我们讲解Spring的另外一大要素就是AOP,Spring最为重要的两个组成就是IOC和AOP
AOP
什么是AOP
AOP是OOP的延续,是Aspect Oriented Programming的缩写,意思是面向切面编程。可以通过预编译方式和运行期动态代理实现在不修改源代码的情况下给程序动态统一添加功能的一种技术。AOP实际是GoF设计模式的延续,设计模式孜孜不倦追求的是调用者和被调用者之间的解耦,AOP可以说也是这种目标的一种实现。
我们现在做的一些非业务,如:日志、事务、安全等都会写在业务代码中(也即是说,这些非业务类横切于业务类),但这些代码往往是重复,复制——粘贴式的代码会给程序的维护带来不便,AOP就实现了把这些业务需求与系统需求分开来做。这种解决的方式也称代理机制。
AOP实现方式
在之前的学习中,如果我们的对象有一个func()
方法可以调用,那么我们可以直接调用
但是,如果我们想在不对func()
方法做任何调整的情况下,对func()
中原有的方法进行增强,该如何处理,这里就是用到了我们之前讲解过的代理
在代理模式中,可以为对象设置一个代理对象,代理对象为func()
方法提供一个代理方法,通过代理对象的func()
方法调用源对象的func()
方法时,就可以在代理方法中添加新的功能,这就是所谓的增强处理,增强的功能既可以插到源对象的func()
方法前面,也可以插到其后面
在此模式下,就可以在原有代码乃至原业务流程都不变的情况下,直接在业务流程汇总切入新代码,增加新功能,这就是所谓的面向切面编程。
AOP的使用,使开发人员在编写业务逻辑时可以专心于核心业务,而不用过多的关注其他业务的实现,这不但提高了开发的效率,也增强了代码的课维护性
AOP的优点
OOP将应用程序分解为多个层次的对象,而AOP将程序分解为多个切面,日志、事务、安全验证等这些“通用的”、散布在系统各处的需要在实现业务逻辑时关注的事情称为“方面”,也可以称为“关注点”。如果将这些“方面”集中处理,然后在具体运行时,再由容器动态织入这些“方面”,至少有以下两个好处:
- 可以减少“方面”代码里的错误,处理策略时可以做到统一修改
- 可以在编写业务逻辑时专心于核心业务
因此,AOP要做的就是从系统中分离出“方面”,然后集中实现,从而独立的编写业务代码和方面代码,在系统运行时,再将方面“织入”到系统中
AOP术语
- 切入点
一个关注点的模块化,该关注点可能会横切多个对象。比如方面(日志、事务、安全验证)的实现,如日志切面、事务切面、权限切面等。在实际应用中,通常存放方面实现的普通Java类,该类需要被AOP容器识别为切面,需要在配置中通过<bean>
标记指定
- 连接点
程序执行中的某个具体的执行点,比如某方法调用的时候或者异常处理的时候。在Spring AOP中,一个连接点总是表示一个方法的执行
- 切入点
切入点是指切面与程序流程的交叉点,也就是需要处理的连接点。当某个连接点满足预先指定的条件时,AOP 框架能够成功定位到这个连接点,该连接点将被添加增强处理,该连接点也就变成了切入点。通常在程序中,切入点指的是类或者方法名,如某个通知要应用到所有已add开头的方法中,那么满足这一规则的方法都是切入点
- 通知/增强处理
在切面上的某个特定连接点上执行的动作,是切面的具体实现,以目标方法为参照点,根据放置位置的不同,可以分为前置通知、后置通知、异常通知、环绕通知和最终通知。许多AOP 框架都是以拦截器作为通知模型,并维护一个以连接点为中心的拦截器链
- 目标对象
目标对象时指被一个或者多个切面所通知的对象,也就是被通知对象,如果采用动态AOP实现,那么该对象就是一个被代理的对象
- 代理对象
代理对象是指将通知应用到目标对象之后,被童泰创建的对象
- 织入
织入是指生成代理对象并将切面内容放入到流程中的过程,即将切面代码插入到目标对象上,从而创建一个新的代理对象的过程
基于XML配置文件的AOP实现
我们以前置通知为例
我们新建一个项目
在pom.xml文件中除了导入我们之前讲解《Spring中IOC思想(2)和依赖注入详解》时候导入的依赖,我们还需要导入4个新的依赖
<!-- https://mvnrepository.com/artifact/org.springframework/spring-aop -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aop</artifactId>
<version>5.0.4.RELEASE</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.springframework/spring-aspects -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aspects</artifactId>
<version>5.0.4.RELEASE</version>
</dependency>
<!-- https://mvnrepository.com/artifact/aopalliance/aopalliance -->
<dependency>
<groupId>aopalliance</groupId>
<artifactId>aopalliance</artifactId>
<version>1.0</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.aspectj/aspectjweaver -->
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.9.1</version>
</dependency>
将这4个依赖导入后,我们就可以使用AOP
我们先在java目录下创建com目录,在com目录下创建westos,在westos目录下创建我们后面需要到的目录
我们先创建service目录,在该目录中创建service层的实体类,来作为AOP的切入点
在service包中创建接口ProductService接口,添加browse方法,用来模拟用户浏览商品的业务
package com.westos.service;
public interface ProductService {
//定义抽象方法browse,模拟用户浏览某商品
public void browse(String loginName, String ProductName);
}
继续在service包下创建ProductService
接口的实现类ProductServiceImpl
,在该类中重写我们之前接口中的抽象方法
package com.westos.service;
public class ProductServiceImpl implements ProductService{
//实现browse方法,模拟用户浏览某商品
@Override
public void browse(String loginName, String ProductName) {
System.out.println("执行业务browse");
}
}
在westos包下创建aop包,在aop包下创建通知类AllLogAdvice
,在类中我们编写生成日志记录的方法myBeforeAdvice
package com.westos.aop;
import org.aspectj.lang.JoinPoint;
import java.text.SimpleDateFormat;
import java.util.Arrays;
import java.util.Date;
import java.util.List;
public class AllLogAdvice {
//将此方法作为前置通知
public void myBeforeAdvice(JoinPoint joinPoint){
//获得切入点的方法的参数
List<Object> args = Arrays.asList(joinPoint.getArgs());
//日志格式字符串
String logInfoText = "前置通知:" + new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()) + " " + args.get(0).toString() + "浏览商品" + args.get(1).toString();
//将日志信息输出
System.out.println(logInfoText);
}
}
因为我们是将myBeforeAdvice
方法作为browse
方法的前置通知,也就是将这个方法添加到目标方法之前运行,我们可以使用JoinPoint
接口类型的参数joinPoint
,Spring会自动注入实例,通过joinPoint
的getArgs()
方法,myBeforeAdvice
方法就能获得browse
方法的参数loginName
, ProductName
,以便实施相关的判断和处理
获得的两个参数的信息被放置在列表args
中,我们通过索引获取后可以使用到这些参数
在编写完上面的1个接口和2个类后,我们就需要编写Spring的配置文件,和之前的一样,在resources目录下创建applicationContext.xml文件,在该文件中采用AOP配置方式进行配置,在该文件中粘贴我们《Spring中IOC思想(2)和依赖注入详解》博客中讲解的配置文件模板
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd">
</beans>
在该配置文件中我们需要将业务类ProductServiceImpl
和日志通知类AllLogAdvice
两个互不相关的类通过AOP进行装配,将日志通知类AllLogAdvice
织入到ProductServiceImpl
中
因为AOP配置的标签是放置在aop空间命名下的,需要我们导入AOP命名空间及其配套的schemaLocation
xmlns:aop="http://www.springframework.org/schema/aop"
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop.xsd
这里一定要注意添加的位置
至此applicationContext.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
https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop.xsd
">
</beans>
我们在 <beans>
标签中开始我们的配置
我们先需要将ProductServiceImpl
和AllLogAdvice
类进行实例化
<!--实例化业务类的Bean-->
<bean id="productService" class="com.westos.service.ProductServiceImpl"></bean>
<!--实例化日志类的Bean-->
<bean id="allLogAdvice" class="com.westos.aop.AllLogAdvice"/>
在将这两个类进行实例化后,我们才能进行相关操作
接下来我们配置aop
<!--配置aop-->
<aop:config>
<!--配置日志切面,切面需要一个名字和依赖,依赖就是我们日志通知类的实例allLogAdvice-->
<aop:aspect id="logaop" ref="allLogAdvice">
<!--定义切入点,切入点也需要一个名字,供以后使用-->
<aop:pointcut id="logpointcut" expression="execution(public void browse(String, String))"/>
<!--将日志通知类中的myBeforeAdvice方法指定为前置通知,这里需要我们设置的切入点的名字-->
<aop:before method="myBeforeAdvice" pointcut-ref="logpointcut"/>
</aop:aspect>
</aop:config>
这里我们需要使用aop的一些标签
我们通过<aop:config>
元素进行AOP配置,在配置AOP时,通过<aop:aspect >
子元素配置日志切面,在配置日志切面时,先通过 <aop:pointcut>
子元素定义切入点,切入点使用正则表达式execution(public void browse(String, String))
,含义是对browse(String, String)
方法进行拦截,再通过<aop:before>
将日志通知类中的方法myBeforeAdvice
方法指定为前置通知
execution
是切入点指示符,括号中时切入点表达式,用于标识需要切入增强处理的方法特征,支持模糊匹配,我后续会补充
在编写完上面的内容后,我们可进行测试,和之前一样,在test目录下创建测试类
package com.westos;
import com.westos.service.ProductService;
import com.westos.service.ProductServiceImpl;
import org.junit.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
public class test {
@Test
public void test(){
//初始化Spring容器,加载applicationContext.xml文件
ApplicationContext ctx = new ClassPathXmlApplicationContext("applicationContext.xml");
//在容器中获取ProductServiceImpl类的实例
ProductService productService = (ProductServiceImpl)ctx.getBean("productService");
//调用browse方法
productService.browse("强静州", "蛋白粉");
}
}
测试运行
运行成功,这里要注意测试类中的从容器中获取实例那一步强制转换的类型
上面就是前置的讲解,其他几种通知步骤类似,后续我会补充使用注解实现AOP,使用注解省去了冗长的配置文件,推荐使用