spring AOP 简介
AOP:全称是 Aspect Oriented Programming 即:面向切面编程。
简单的说它就是把我们程序重复的代码抽取出来,在需要执行的时候,使用动态代理的技术,在不修改源码的基础上,对我们的已有方法进行增强。
如果说 IoC 是 Spring 的核心,那么面向切面编程就是 Spring 最为重要的功能之一了,在数据库事务中切面编程被广泛使用。
AOP能够将那些与业务无关,却为业务模块所共同调用的逻辑或责任(例如事务处理、日志管理、权限控制等)封装起来,便于减少系统的重复代码,降低模块间的耦合度,并有利于未来的可拓展性和可维护性。
AOP称为面向切面编程,那我们怎么理解面向切面编程??
我们学Java面向对象的时候,如果代码重复了怎么办啊??可以分成下面几个步骤:
1:抽取成方法
2:抽取类
抽取成类的方式我们称之为:纵向抽取
通过继承的方式实现纵向抽取
但是,我们现在的办法不行:即使抽取成类还是会出现重复的代码,因为这些逻辑(开始、结束、提交事务)依附在我们业务类的方法逻辑中!
现在纵向抽取的方式不行了,AOP的理念:就是将分散在各个业务逻辑代码中相同的代码通过横向切割的方式抽取到一个独立的模块中!
将重复性的逻辑代码横切出来其实很容易(我们简单可认为就是封装成一个类就好了),但我们要将这些被我们横切出来的逻辑代码融合到业务逻辑中,来完成和之前(没抽取前)一样的功能!这就是AOP首要解决的问题了!
代理能干嘛?代理可以帮我们增强对象的行为!使用动态代理实质上就是调用时拦截对象方法,对方法进行改造、增强!
其实Spring AOP的底层原理就是动态代理!
Spring AOP使用纯Java实现,它不需要专门的编译过程,也不需要特殊的类装载器,它在运行期通过代理方式向目标类织入增强代码。在Spring中可以无缝地将Spring AOP、IoC和AspectJ整合在一起。
在Java中动态代理有两种方式:
JDK动态代理
CGLib动态代理
JDK动态代理是需要实现某个接口了,而我们类未必全部会有接口,于是CGLib代理就有了~~
CGLib代理其生成的动态代理对象是目标类的子类
Spring AOP默认是使用JDK动态代理,如果代理的类没有接口则会使用CGLib代理。
那么JDK代理和CGLib代理我们该用哪个呢?
如果是单例的我们最好使用CGLib代理,如果是多例的我们最好使用JDK代理
原因:JDK在创建代理对象时的性能要高于CGLib代理,而生成代理对象的运行性能却比CGLib的低。
如果是单例的代理,推荐使用CGLib
看到这里我们就应该知道什么是Spring AOP(面向切面编程)了:将相同逻辑的重复代码横向抽取出来,使用动态代理技术将这些重复代码织入到目标对象方法中,实现和原来一样的功能。
这样一来,我们就在写业务时只关心业务代码,而不用关心与业务无关的代码
AOP的术语
连接点(Join point):
能够被拦截的地方:Spring AOP是基于动态代理的,所以是方法拦截的。每个成员方法都可以称之为连接点~
切点(Poincut):
具体定位的连接点:上面也说了,每个方法都可以称之为连接点,我们具体定位到某一个方法就成为切点。
增强/通知(Advice):
表示添加到切点的一段逻辑代码,并定位连接点的方位信息。
简单来说就定义了是干什么的,具体是在哪干
Spring AOP提供了5种Advice类型给我们:前置、后置、返回、异常、环绕给我们使用!
织入(Weaving):
将增强/通知添加到目标类的具体连接点上的过程。
引入/引介(Introduction):
引入/引介允许我们向现有的类添加新方法或属性。是一种特殊的增强!
切面(Aspect):
切面由切点和增强/通知组成,它既包括了横切逻辑的定义、也包括了连接点的定义。
通知/增强包含了需要用于多个应用对象的横切行为;连接点是程序执行过程中能够应用通知的所有点;切点定义了通知/增强被应用的具体位置。其中关键的是切点定义了哪些连接点会得到通知/增强。
使用XML配置AOP
导入jar包依赖,用于解析切入点表达式
<!--解析切入点表达式(通配符 . 啥的)-->
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.8.7</version>
</dependency>
applicationContext.xml
注意:xmlns:aop="http://www.springframework.org/schema/aop"
必须有这个东西
<?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.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop.xsd">
<!--配置spring的IoC容器,把service对象配置进来-->
<bean id="accountService" class="com.service.impl.AccountServiceImpl"></bean>
<!--spring中基于XML的AOP配置步骤
1、把通知Bean交给spring管理
2、使用aop:config标签表明开始AOP的配置
3、使用aop:aspect标签表面配置切面
id属性:给切面提供一个唯一标识
ref属性:指定通知类bean的id
4、在aop:aspect标签内部使用对应标签来配置通知的类型
现在的需求是让printLog方法在切入点方法之前执行,所以是前置通知
aop:before:表示配置前置通知
method属性:用于指定Logger类中哪个方法是前置通知
pointcut属性:用于指定切入点表达式,该表达式的含义指对业务层中哪些方法增强
切入点表达式写法:
关键字:execution(表达式)
表达式:
访问修饰符 返回值 包名.包名.类名.方法名(参数列表)
表达式标准写法:
public void com.service.impl.AccountServiceImpl.saveAccount()
访问修饰符可以省略
返回值可以使用通配符表示任意返回值
包名可以使用通配符,表示任意包,但是有几个包,就需要写几个*.
包名可以使用..表示当前包及其子包
类名和方法名都可以使用*来实现通配
参数列表:
可以直接写数据类型:
基本类型直接写名称: int
引用类型写包名.类名的方式: java.lang.String
可以使用通配符表示任意类型,但是必须有参数
可以使用..表示有无参数均可,参数可以说任意类型
全通配写法:
* *..*.*(..)
实际项目中切入点表达式的通常写法:
切到业务层实现类下的所有方法
* com.service.impl.*.*(..)
-->
<!--配置Logger类-->
<bean id="logger" class="com.utils.Logger"></bean>
<!--配置AOP-->
<aop:config>
<!--配置切面-->
<aop:aspect id="logAdvice" ref="logger">
<!--配置通知的类型,并且建立通知方法和切入点方法的关联-->
<!--<aop:before method="printLog" pointcut="execution(public void com.service.impl.AccountServiceImpl.saveAccount())"></aop:before>-->
<aop:before method="printLog" pointcut="execution(* com.service.impl.*.*(..))"></aop:before>
</aop:aspect>
</aop:config>
</beans>
<!--xmlns:aop-->
通知类型
<?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.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop.xsd">
<!--配置spring的IoC容器,把service对象配置进来-->
<bean id="accountService" class="com.service.impl.AccountServiceImpl"></bean>
<!--配置Logger类-->
<bean id="logger" class="com.utils.Logger"></bean>
<!--配置AOP-->
<aop:config>
<!--配置切面-->
<!--此时aop:pointcu只能配置在aop:aspect标签之前,放在它后面会报错-->
<aop:pointcut id="pt1" expression="execution(* com.service.impl.*.*(..))"></aop:pointcut>
<aop:aspect id="logAdvice" ref="logger">
<!--配置通知的类型,并且建立通知方法和切入点方法的关联-->
<!--<aop:before method="printLog" pointcut="execution(public void com.service.impl.AccountServiceImpl.saveAccount())"></aop:before>-->
<!--前置通知:在切入点方法(业务方法)执行之前执行-->
<aop:before method="printLog" pointcut-ref="pt1"></aop:before>
<!--后置通知:在切入点方法(业务方法)正常执行之后执行 它和异常通知永远只能执行一个-->
<aop:after-returning method="afterprintLog" pointcut-ref="pt1"></aop:after-returning>
<!--异常通知:在切入点方法(业务方法)产生异常之后执行-->
<aop:after-throwing method="throwprintLog" pointcut-ref="pt1"></aop:after-throwing>
<!--最终通知:无论切入点方法是否正常执行都会在之后执行-->
<aop:after method="finalprintLog" pointcut-ref="pt1"></aop:after>
<!--配置切入点表达式 id:唯一标识 expression:指定表达式内容-->
<!--此标签写在aop:aspect标签内部,只能当前切面使用
它还可以写在aop:aspect标签外部,所有切面可用-->
<!--<aop:pointcut id="pt1" expression="execution(* com.service.impl.*.*(..))"></aop:pointcut>-->
<!--配置环绕通知-->
<aop:around method="aroundPrintLog" pointcut-ref="pt1"></aop:around>
</aop:aspect>
</aop:config>
</beans>
<!--xmlns:aop-->
环绕通知
/**
* 环绕通知
* 问题:当我们配置了环绕通知后,切入点方法(业务层方法)没有执行,而环绕通知方法执行了
* 分析:通过对比动态代理中的环绕通知代码,发现动态代理的环绕通知方法有明确的切入点方法调用,而我们此时方法没有
* 解决:spring框架为我们提供了一个接口,ProceedingJoinPoint,该接口有一个方法proceed(),此方法就相当于
* · 明确调用切入点方法,该接口可以作为环绕通知的参数,在程序执行时,spring会提供该接口的实现类供我们使用
* spring中的环绕通知:
* 它是spring框架为我们提供的一种可以在代码中手动控制增强方法何时执行的方式
*/
public Object aroundPrintLog(ProceedingJoinPoint pjp) {
Object rtValue = null;
try {
Object[] args = pjp.getArgs(); //得到方法执行所需的参数
System.out.println("Logger方法中的aroundPrintLog方法开始记录日志了 前置");
rtValue = pjp.proceed(args); //明确调用业务层方法
System.out.println("Logger方法中的aroundPrintLog方法开始记录日志了 后置");
return rtValue;
} catch (Throwable t) {
System.out.println("Logger方法中的aroundPrintLog方法开始记录日志了 异常");
throw new RuntimeException(t);
} finally {
System.out.println("Logger方法中的aroundPrintLog方法开始记录日志了 最终");
}
}
使用注解配置AOP
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"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd">
<!--配置spring容器创建时要扫描的包-->
<context:component-scan base-package="com"></context:component-scan>
<!--配置spring开启注解AOP的支持-->
<!--这里不写的话无法使用AOP进行代理增强-->
<aop:aspectj-autoproxy></aop:aspectj-autoproxy>
</beans>
Logger.java
package com.utils;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;
/**
* 用于记录日志的工具类,提供了公共代码
*/
@Component
@Aspect //表示当前类是一个切面类
public class Logger {
/**
* 切入点表达式
*/
@Pointcut("execution(* com.service.impl.*.*(..))")
private void pt2(){}
/**
* 前置通知
* 用于打印日志,计划让其在切入点方法执行之前执行(切入点方法就是业务层方法)
*/
@Before("pt2()")
public void printLog() {
System.out.println("Logger方法中的printLog方法开始记录日志了");
}
/**
* 后置通知
*
*/
@AfterReturning("pt2()")
public void afterprintLog() {
System.out.println("Logger方法中的afterprintLog方法开始记录日志了");
}
/**
* 异常通知
*/
@AfterThrowing("pt2()")
public void throwprintLog() {
System.out.println("Logger方法中的throwprintLog方法开始记录日志了");
}
/**
* 最终通知
*/
@After("pt2()")
public void finalprintLog() {
System.out.println("Logger方法中的finalprintLog方法开始记录日志了");
}
/**
* 环绕通知
* 问题:当我们配置了环绕通知后,切入点方法(业务层方法)没有执行,而环绕通知方法执行了
* 分析:通过对比动态代理中的环绕通知代码,发现动态代理的环绕通知方法有明确的切入点方法调用,而我们此时方法没有
* 解决:spring框架为我们提供了一个接口,ProceedingJoinPoint,该接口有一个方法proceed(),此方法就相当于
* · 明确调用切入点方法,该接口可以作为环绕通知的参数,在程序执行时,spring会提供该接口的实现类供我们使用
* spring中的环绕通知:
* 它是spring框架为我们提供的一种可以在代码中手动控制增强方法何时执行的方式
*/
@Around("pt2()")
public Object aroundPrintLog(ProceedingJoinPoint pjp) {
Object rtValue = null;
try {
Object[] args = pjp.getArgs(); //得到方法执行所需的参数
System.out.println("Logger方法中的aroundPrintLog方法开始记录日志了 前置");
rtValue = pjp.proceed(args); //明确调用业务层方法
System.out.println("Logger方法中的aroundPrintLog方法开始记录日志了 后置");
return rtValue;
} catch (Throwable t) {
System.out.println("Logger方法中的aroundPrintLog方法开始记录日志了 异常");
throw new RuntimeException(t);
} finally {
System.out.println("Logger方法中的aroundPrintLog方法开始记录日志了 最终");
}
}
}
AccountServiceImpl.java
package com.service.impl;
import com.service.IAccountService;
import org.springframework.stereotype.Service;
/**
* 账户的业务层实现类
*/
@Service("accountServiceImpl")
public class AccountServiceImpl implements IAccountService {
public void saveAccount() {
System.out.println("执行saveAccount");
// int i = 1/0;
}
public void updateAccount(int i) {
System.out.println("执行updateAccount");
}
public int deleteAccount() {
System.out.println("执行deleteAccount");
return 0;
}
}
测试类
import com.service.IAccountService;
import org.junit.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
/**
* 测试AOP的配置
*/
public class AOPTest {
@Test
public void test() {
//1、获取容器
ApplicationContext ac = new ClassPathXmlApplicationContext("applicationContext.xml");
//2、获取对象
IAccountService as = (IAccountService) ac.getBean("accountServiceImpl");
//3、执行方法
as.saveAccount();
// as.updateAccount(1);
// as.deleteAccount();
}
}
运行结果
Logger方法中的aroundPrintLog方法开始记录日志了 前置
Logger方法中的printLog方法开始记录日志了
执行saveAccount
Logger方法中的aroundPrintLog方法开始记录日志了 后置
Logger方法中的aroundPrintLog方法开始记录日志了 最终
Logger方法中的finalprintLog方法开始记录日志了
Logger方法中的afterprintLog方法开始记录日志了
Process finished with exit code 0
注意:
基于注解的spring AOP配置中,四种通知类型中后置通知和最终通知会发送调用顺序的错误,这个没办法改变,但是我们自己写的环绕通知则不存在问题