AOP概述
AOP(Aspect Oriented Programming):面向切面编程,通过预编译方式和运行期动态代理实现程序功能的统一维护的一种技术。AOP是OOP的延续,是软件开发中的一个热点,也是Spring框架中的一个重要内容,是函数式编程的一种衍生范型。利用AOP可以对业务逻辑的各个部分进行隔离,从而使得业务逻辑各部分之间的耦合度降低,提高程序的可重用性,同时提高了开发的效率。
AOP的作用
在不修改源代码的情况下,可以实现功能的增强。
AOP 思想: 基于代理思想,对原来目标对象,创建代理对象,在不修改原对象代码情况下,通过代理对象,调用增强功能的代码,从而对原有业务方法进行增强 。
AOP的应用场景
场景一: 记录日志
场景二: 监控方法运行时间 (监控性能)
场景三: 权限控制
场景四: 缓存优化 (第一次调用查询数据库,将查询结果放入内存对象, 第二次调用, 直接从内存对象返回,不需要查询数据库 )
场景五: 事务管理 (调用方法前开启事务, 调用方法后提交关闭事务 )
AOP的实现原理
Spring中AOP有两种实现方式:
1、JDK动态代理
2、Cglib动态代理
- JDK动态代理
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration("classpath:applicationContext.xml")
public class TestIOC {
@Autowired
private CustomerService customerService;
@Test
public void test3() {
/*
* newProxyInstance的三个参数解释:
* 参数1:代理类的类加载器,同目标类的类加载器
* 参数2:代理类要实现的接口列表,同目标类实现的接口列表
* 参数3:回调,是一个InvocationHandler接口的实现类对象;当调用代理对象的方法时,其实执行的是回调中的invoke方法
*/
CustomerService proxy = (CustomerService) Proxy.newProxyInstance(customerService.getClass().getClassLoader(),
customerService.getClass().getInterfaces(), new InvocationHandler() {
/*
* invoke方法的三个参数解释:
* 参数1:代理对象
* 参数2:目标方法,也就是被增强的方法,代理对象调用哪个方法,method就表示该方法
* 参数3:目标方法的形参列表
*/
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("增强啦...");
return method.invoke(customerService, args);
}
});
List<Customer> customers = proxy.findAllCustomer();
for (Customer customer : customers) {
System.out.println(customer);
}
}
}
- Cglib动态代理
在实际开发中,可能需要对没有实现接口的类增强,用JDK动态代理的方式就没法实现。采用Cglib动态代理可以对没有实现接口的类产生代理,实际上是生成了目标类的子类来增强。
① 首先,需要导入Cglib所需的jar包。提示:spring已经集成了cglib,我们已经导入了spring包,故不需要再导入其它包了。
@Test
public void test2(){
final LinkManDao linkManDao = new LinkManDao();
// 创建cglib核心对象
Enhancer enhancer = new Enhancer();
// 设置父类
enhancer.setSuperclass(linkManDao.getClass());
// 设置回调
enhancer.setCallback(new MethodInterceptor() {
/*
* 当你调用目标方法时,实质上是调用该方法
* intercept四个参数:
* proxy:代理对象
* method:目标方法
* args:目标方法的形参
* methodProxy:代理方法
*/
public Object intercept(Object proxy, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
System.out.println("记录日志...");
// Object result = method.invoke(linkManDao, args);
Object result = methodProxy.invokeSuper(proxy, args);
return result;
}
});
// 创建代理对象
LinkManDao linkMan = (LinkManDao) enhancer.create();
linkMan.save();
}
}
Spring的AOP中相关术语的介绍
- 连接点(Joinpoint):所谓连接点是指那些被拦截到的点,连接点可能是类初始化、方法执行、方法调用、字段调用或处理异常等等;Spring只支持方法执行连接点。
- 切入点(Pointcut):选择一组相关连接点的模式,即可以认为连接点的集合;是指我们要对哪些连接点进行拦截的定义;Spring支持perl5正则表达式和AspectJ切入点模式,Spring默认使用AspectJ语法。
- 通知/增强(Advice):指拦截到连接点之后要做的事情,通知分为:前置通知,后置通知,异常通知,最终通知,环绕通知。
- 引介(Introduction):是一种特殊的通知,在不修改源代码的前提下,Introduction可以在运行期为类动态的添加一些方法或属性。
- 目标对象(Target):代理的目标对象
- 织入(Weaving):是指把增强应用到目标对象来创建新的代理对象的过程,Spring采用动态代理织入,而AspectJ采用编译器织入和类装载期织入。
- 代理(Proxy):一个类被AOP织入增强后,就产生一个结果代理类
- 切面(Aspect):是切入点和通知(引介)的结合
Spring中AOP编程(xml方式)
- 创建项目引入依赖和配置文件applicationContext.xml和log4j.properties
<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.0.6.RELEASE</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
<version>1.7.22</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
<version>5.0.5.RELEASE</version>
</dependency>
<!-- aop的依赖 -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aspects</artifactId>
<version>4.3.13.RELEASE</version>
</dependency>
</dependencies>
- 编写接口和实现类
public interface CustomerDao {
public void save();
}
public class CustomerDaoImpl implements CustomerDao{
public void save() {
System.out.println("持久层:保存");
}
}
- 编写切面类
public class MyAspectXml {
public void writeLog(){
System.out.println("写日志...");
}
}
- applicationContext.xml文件配置aop以及相关的类交给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: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">
<bean id="customerDao" class="com.ityang.dao.impl.CustomerDaoImpl"></bean>
<bean id="myAspectXml" class="com.ityang.aspect.MyAspectXml"></bean>
<!-- aop配置 -->
<aop:config>
<!-- 配置切入点:告诉spring框架哪些方法需要被增强 -->
<aop:pointcut id="pointcut1" expression="execution(* com.ityang.dao.impl.CustomerDaoImpl.save(..))"></aop:pointcut>
<!-- 配置切面:告诉spring框架调用切面类中的哪个方法来增强 -->
<aop:aspect ref="myAspectXml">
<aop:before method="writeLog" pointcut-ref="pointcut1"></aop:before>
</aop:aspect>
</aop:config>
</beans>
- 测试
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration("classpath:applicationContext.xml")
public class TestAop {
@Autowired
private CustomerDao customerDao;
@Test
public void test1(){
customerDao.save();
}
}
- Aop代码编写流程总结:
1、编写目标类
2、编写切面类以及通知方法
3、把目标类和切面类交给spring管理
4、指定切面类:用哪个类来增强
5、配置切入点表达式:指定哪些方法需要增强
6、配置通知:指定遇到满足切入点表达式的方式用切面类中的哪个方法增强
AOP配置文件解析
- 切入点表达式
<!-- 配置切入点:告诉spring框架哪些方法需要被增强 -->
<aop:pointcut id="pointcut1" expression="execution(* com.ityang.dao.impl.CustomerDaoImpl.save(..))">
切入点表达式语法:[修饰符] 返回类型 包名.类名.方法名(形式参数)
常见写法:
① execution(public * * (. .)) ------所有的public方法
② execution(* set* (. .)) ------所有set开头的方法
③ execution(* com.xyz.service.AccountService.* (. .)) ------AccountService类中的所有方法
④ execution(* com.xyz.service. * . * (. .)) ------com.xyz.service包下所有的方法
⑤ execution(* com.xyz.service. . * . * (. .)) ------com.xyz.service包及其子包下所有的方法
- AOP的通知类型五种:
1、前置通知
应用: 权限控制 (权限不足,抛出异常)、 记录方法调用信息日志
① 创建接口和实现类
public interface CustomerDao {
public void save();
}
public class CustomerDaoImpl implements CustomerDao{
public void save() {
System.out.println("持久层:保存");
}
}
② 对CustomerDaoImpl中的save方法前置增强,需要创建一个切面类MyAspectXml,在其中创建前置通知方法
public class MyAspectXml {
/**
* joinPoint:连接点,指的是被增强的那个方法
* @param joinPoint
*/
public void before(JoinPoint joinPoint){
String username = "jack";
if(!"admin".equals(username)){
// 获取目标类的名字
String name = joinPoint.getTarget().getClass().getName();
// 获取被增强方法的名字
String name1 = joinPoint.getSignature().getName();
throw new RuntimeException("对不起!您没有对" + name + "类中"
+ name1 + "方法的访问权限");
}
}
}
③ 配置文件内配置
<bean id="customerDao" class="com.ityang.dao.impl.CustomerDaoImpl"></bean>
<bean id="myAspectXml" class="com.ityang.aspect.MyAspectXml"></bean>
<!-- aop配置 -->
<aop:config>
<!-- 配置切入点:告诉spring框架哪些方法需要被增强 -->
<aop:pointcut id="pointcut1" expression="execution(* com.ityang.dao.impl.CustomerDaoImpl.save(..))"></aop:pointcut>
<!-- 配置切面:告诉spring框架调用切面类中的哪个方法来增强 -->
<aop:aspect ref="myAspectXml">
<aop:before method="before" pointcut-ref="pointcut1"></aop:before>
</aop:aspect>
</aop:config>
④ 测试
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration("classpath:applicationContext.xml")
public class TestAop {
@Autowired
private CustomerDao customerDao;
@Test
public void test1(){
customerDao.save();
}
}
2、后置通知
特点:在目标方法运行后,返回值
后执行增强代码逻辑。
应用场景:与业务相关的,如ATM取款机取款后,自动下发短信。
① 创建接口和实现类
public interface CustomerDao {
public Integer delete();
}
public class CustomerDaoImpl implements CustomerDao{
public Integer delete() {
System.out.println("删除...");
return 100;
}
}
② 定义一个后置通知方法afterReturning方法,并添加一个形参result,表示目标方法的返回值
/**
* 后置通知方法
* @param joinPoint 被增强的那个方法
* @param result 被增强那个方法的返回值
*/
public void afterReturning(JoinPoint joinPoint,Object result){
Date date = new Date();
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
String dateStr = sdf.format(date);
System.out.println("尊敬的用户,您于" + dateStr + "取款" + result + "元");
}
③ 配置文件
<bean id="customerDao" class="com.ityang.dao.impl.CustomerDaoImpl"></bean>
<bean id="myAspectXml" class="com.ityang.aspect.MyAspectXml"></bean>
<!-- aop配置 -->
<aop:config>
<!-- 配置切入点:告诉spring框架哪些方法需要被增强 -->
<aop:pointcut id="pointcut2" expression="execution(* com.ityang.dao.impl.CustomerDaoImpl.delete(..))"></aop:pointcut>
<!-- 配置切面:告诉spring框架调用切面类中的哪个方法来增强 -->
<aop:aspect ref="myAspectXml">
<aop:after-returning method="afterReturning" pointcut-ref="pointcut2" returning="result"></aop:after-returning>
</aop:aspect>
</aop:config>
④ 测试(代码不贴了)
3、环绕通知
特点:目标执行前后,都进行增强(控制目标方法执行)
应用场景:日志、缓存、权限、性能监控、事务管理
增强代码的方法要求:
1、接受的参数:ProceedingJoinPoint(可执行的连接点)
2、返回值:Object返回值
3、抛出Throwable异常。
① 创建接口和实现类(代码省略)
② 在切面类MyAspectXml中定义一个环绕通知方法around方法,需要给该方法增加一个形参ProceedingJoinPoint ,表示正在执行的连接点(目标)
/**
*
* @param proceedingJoinPoint 正在执行的连接点
* @return
*/
public Object around(ProceedingJoinPoint proceedingJoinPoint){
System.out.println("开启事务");
// 获取目标方法的参数
Object[] args = proceedingJoinPoint.getArgs();
System.out.println(args.toString());
Object result = null;
try {
// 调用目标方法,获取目标方法的返回值
result = proceedingJoinPoint.proceed(args);
System.out.println(result);
// int a = 1 / 0;
System.out.println("提交事务");
} catch (Throwable throwable) {
System.out.println("回滚事务");
}
return result;
}
③ 配置文件
<bean id="customerDao" class="com.ityang.dao.impl.CustomerDaoImpl"></bean>
<bean id="myAspectXml" class="com.ityang.aspect.MyAspectXml"></bean>
<!-- aop配置 -->
<aop:config>
<!-- 配置切入点:告诉spring框架哪些方法需要被增强 -->
<aop:pointcut id="pointcut3" expression="execution(* com.ityang.dao.impl.CustomerDaoImpl.update(..))"></aop:pointcut>
<!-- 配置切面:告诉spring框架调用切面类中的哪个方法来增强 -->
<aop:aspect ref="myAspectXml">
<aop:around method="around" pointcut-ref="pointcut3"></aop:around>
</aop:aspect>
</aop:config>
④ 测试(代码不贴了)
4、异常通知
作用:目标代码出现异常,通知执行。记录异常日志、通知管理员(短信、邮件)
应用场景:处理异常(一般不可预知),记录日志
① 创建接口和实现类(代码省略…)
② 在切面类MyAspectXml中定义一个异常通知方法afterThrowing方法,在方法加一个异常参数,名字叫ex
/**
* 处理异常
* @param joinPoint 被增强的那个方法
* @param ex 目标方法抛出的异常,名字要与配置文件中配置的名字一致
*/
public void afterThrowing(JoinPoint joinPoint,Throwable ex){
System.out.println("注意了:在" + joinPoint.getTarget().getClass().getName() + "类中的"
+ joinPoint.getSignature().getName() + "方法中发生了异常:" + ex.getMessage());
}
③ 配置文件
<bean id="customerDao" class="com.ityang.dao.impl.CustomerDaoImpl"></bean>
<bean id="myAspectXml" class="com.ityang.aspect.MyAspectXml"></bean>
<!-- aop配置 -->
<aop:config>
<!-- 配置切入点:告诉spring框架哪些方法需要被增强 -->
<aop:pointcut id="pointcut4" expression="execution(* com.ityang.dao.impl.CustomerDaoImpl.list(..))"></aop:pointcut>
<!-- 配置切面:告诉spring框架调用切面类中的哪个方法来增强 -->
<aop:aspect ref="myAspectXml">
<aop:after-throwing method="afterThrowing" pointcut-ref="pointcut4" throwing="ex"></aop:after-throwing>
</aop:aspect>
</aop:config>
④ 测试(代码不贴了)
5、最终通知
作用:不管目标方法是否发生异常,最终通知都会执行(类似于finally代码功能)
应用场景:释放资源 (关闭文件、 关闭数据库连接、 网络连接、 释放内存对象 )
① 创建接口和实现类(代码省略…)
② 在切面类MyAspect中定义一个最终通知方法after方法
/**
* 最终通知方法 应用场景:释放资源 (关闭文件、 关闭数据库连接、 网络连接、 释放内存对象 )
* @param joinPoint 被增强的那个方法
*/
public void after(JoinPoint joinPoint) {
System.out.println("开始释放资源,对应的连接点信息为:" + joinPoint.getTarget().getClass().getName() + "的"
+ joinPoint.getSignature().getName() + "方法");
}
③ 配置文件
<!-- aop配置 -->
<aop:config>
<!-- 配置切入点:告诉spring框架哪些方法需要被增强 -->
<aop:pointcut id="pointcut4" expression="execution(* com.ityang.dao.impl.CustomerDaoImpl.list(..))"></aop:pointcut>
<!-- 配置切面:告诉spring框架调用切面类中的哪个方法来增强 -->
<aop:aspect ref="myAspectXml">
<aop:after method="after" pointcut-ref="pointcut4"/>
</aop:aspect>
</aop:config>
④ 测试(代码不贴了)
最后要注意:最终通知和后置通知的区别:最终通知,不管异常与否,都执行;而后置通知在异常时不执行
Spring中AOP编程(注解方式)
- 创建项目、引入依赖和配置文件applicationContext.xml和log4j.properties
依赖和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:context="http://www.springframework.org/schema/context"
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">
<!-- 开启spring注解扫描 -->
<context:component-scan base-package="com.ityang"/>
<!-- 开启aspectj自动代码 -->
<aop:aspectj-autoproxy/>
</beans>
- 创建接口和实现类
public interface CustomerDao {
public void save();
public Integer delete();
public void update();
public void list();
}
@Repository("customerDao")
public class CustomerDaoImpl implements CustomerDao{
public void save() {
System.out.println("持久层:保存");
}
public Integer delete() {
System.out.println("删除...");
return 100;
}
public void update() {
System.out.println("持久层:客户更新...");
}
public void list(){
System.out.println("aaa");
// int i = 1 / 0;
}
}
- 编写切面类
@Component
@Aspect // 表示该类是一个切面类
public class MyAspectAnnotation {
@Before("execution(* com.ityang.dao.impl.CustomerDaoImpl.save(..))")
public void writeLog(){
System.out.println("记录日志啦.....");
}
}
- 测试(代码不贴了)
用注解来配置Spring AOP中的各种通知
不用在核心配置文件中配置切入点和切面,只需要注解@Aspect // 表示该类是一个切面类和各类通知注解即可(代码只贴了主要的切面类,其他自己补充)
@Component
@Aspect // 表示该类是一个切面类
public class MyAspectAnnotation {
/**
* 前置通知 应用场景: 权限控制 (权限不足,抛出异常)、 记录方法调用信息日志
* joinPoint:连接点,指的是被增强的那个方法
* @param joinPoint
*/
@Before("execution(* com.ityang.dao.impl.CustomerDaoImpl.save(..))")
public void before(JoinPoint joinPoint){
String username = "jack";
if(!"admin".equals(username)){
// 获取目标类的名字
String name = joinPoint.getTarget().getClass().getName();
// 获取被增强方法的名字
String name1 = joinPoint.getSignature().getName();
throw new RuntimeException("对不起!您没有对" + name + "类中"
+ name1 + "方法的访问权限");
}
}
/**
* 后置通知方法 应用场景: ATM取款机取款后,自动下发短信
* @param joinPoint 被增强的那个方法
* @param result 被增强那个方法的返回值
*/
@AfterReturning(value = "execution(* com.ityang.dao.impl.CustomerDaoImpl.delete(..))" , returning = "result")
public void afterReturning(JoinPoint joinPoint,Object result){
Date date = new Date();
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
String dateStr = sdf.format(date);
System.out.println("尊敬的用户,您于" + dateStr + "取款" + result + "元");
}
/**
* 环绕通知 应用场景:事务处理
* @param proceedingJoinPoint 正在执行的连接点
* @return
*/
@Around("execution(* com.ityang.dao.impl.CustomerDaoImpl.update(..))")
public Object around(ProceedingJoinPoint proceedingJoinPoint){
System.out.println("开启事务");
// 获取目标方法的参数
Object[] args = proceedingJoinPoint.getArgs();
Object result = null;
try {
// 调用目标方法,获取目标方法的返回值
result = proceedingJoinPoint.proceed(args);
System.out.println("提交事务");
} catch (Throwable throwable) {
System.out.println("回滚事务");
}
return result;
}
/**
* 异常通知方法 应用场景:处理异常
* @param joinPoint 被增强的那个方法
* @param ex 目标方法抛出的异常,名字要与配置文件中配置的名字一致
*/
@AfterThrowing(value = "execution(* com.ityang.dao.impl.CustomerDaoImpl.list(..))",throwing = "ex")
public void afterThrowing(JoinPoint joinPoint,Throwable ex){
System.out.println("注意了:在" + joinPoint.getTarget().getClass().getName() + "类中的"
+ joinPoint.getSignature().getName() + "方法中发生了异常:" + ex.getMessage());
}
/**
* 最终通知方法 应用场景:释放资源 (关闭文件、 关闭数据库连接、 网络连接、 释放内存对象 )
* @param joinPoint 被增强的那个方法
*/
@After("execution(* com.ityang.dao.impl.CustomerDaoImpl.list(..))")
public void after(JoinPoint joinPoint) {
System.out.println("开始释放资源,对应的连接点信息为:" + joinPoint.getTarget().getClass().getName() + "的"
+ joinPoint.getSignature().getName() + "方法");
}
}