简介
AOP:Aspect Oriented Programming,面向切面编程,通过预编译方式和运行动态代理实现程序功能的统一维护的一种技术。就是把程序中的重复代码抽取出来,在需要执行的时候使用动态代理技术在不修改源码的基础上,对我们已有的方法进行增强。
实现:使用动态代理的技术
作用:在程序运行期间,不修改源码对已有方法进行增强
优势:减少重复代码,提高开发效率,维护方便
AOP相关术语
Joinpoint 连接点:指被那些被拦截的点,spring只支持方法类型的连接点,在spring中,这些点指的是方法。
Pointcut 切入点:指我们要对哪些Joinpoint进行拦截定义,指被增强的方法
所有的切入点都是连接点,连接点不一定是切入点
Advice 通知/增强:拦截到Joinpoint之后要做的事情就是通知
通知的类型有前置通知,后置通知,异常通知,环绕通知
Introduction 引介:一种特殊的通知,在不修改代码的前提下,可以在运行期为类动态的添加一些方法
Target 目标对象:代理的目标对象,即被代理对象
Weaving 织入:把增强应用到目标对象来创建新的代理对象的过程,也就是代理方法中执行的代码实现的功能
spring采用动态代理织入,Aspectj采用编译器织入和类装载期织入
Proxy 代理:一个类被AOP织入增强后,就产生一个结果代理类,即代理对象。
Aspect 切面:是切入点和通知(引介)的结合,哪些方法被增强,建立切入点方法和通知方法在执行的调用关系,就是切面,也就是在方法前执行、或者方法后执行
spring中AOP开发步骤
开发阶段-程序员工作
- 根据需求编写核心业务代码
- 把公用代码抽离制作成通知
- 在配置文件中声明切入点与通知间的关系,即切面,也就是在方法前执行、或者方法后执行等
运行阶段-spring框架完成
spring监控切入点的方法执行一旦监控到切入点方法被运行,就是用代理机制,动态创建目标的代理对象,根据通知类别,在代理对象的对应位置,将通知对应的功能织入,完成完整的代码逻辑运行。
spring 基于XML 的AOP配置
基于XML的AOP配置步骤
1.把通知bean交给spring管理
2.使用aop:config标签表名开始AOP配置
3.使用aop:aspect标签配置切面
属性:id - 给切面提供有个唯一标识
ref - 指定通知类bean的id
4.在aop:aspect标签内部使用对应标签配置通知的类型
aop:before:前置通知 method-指定类中那个方法是前置通知
aop:after-returning: 后置通知,在切入点正常执行后执行
aop:after-throwing:异常通知,在切入点方法执行异常后执行
aop:after:最终通知,无论切入点是否异常都会在切入点方法执行后执行
aop:around:环绕通知
pointcut:用于指定切入点表达式,该表达式的含义指的是对业务层中哪些方法的增强
<?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">
<!-- 注入service -->
<bean id="accountService" class="org.example.service.impl.AccountService"></bean>
<!-- 配置logger 类-->
<bean id="logger" class="org.example.util.Logger"></bean>
<!--配置AOP-->
<aop:config>
<!-- 配置切面 -->
<aop:aspect id="logAdvice" ref="logger">
<!-- 配置通知类型 并建立通知方法和切入点方法的关联-->
<!-- 常用写法-->
<aop:before method="printLog" pointcut="execution( * org.example.service.impl.*.*(..))"></aop:before>
<!--精准匹配到某一个方法-->
<aop:before method="printLog" pointcut="execution( * org.example.service.impl.AccountService.saveAccount())"></aop:before>
<!--通配写法-->
<aop:before method="printLog" pointcut="execution(* *..*.*(..))"></aop:before>
</aop:aspect>
</aop:config>
</beans>
--创建Logger 类
package org.example.util;
public class Logger {
/**
* 用于打印日志,在切入点方法前执行
*/
public void printLog(){
System.out.println("Logger类中的printLog方法开始记录日志了。。。");
}
}
//创建service接口类
package org.example.service;
public interface IAccountService {
/**
* 模拟保存账户
*/
void saveAccount();
/**
* 模拟更新账户
* @param i 1
*/
void updateAccount(int i);
/**
* 模拟删除账户
*/
int deleteAccount();
}
//创建 service实现类
package org.example.service.impl;
import org.example.service.IAccountService;
public class AccountService implements IAccountService {
@Override
public void saveAccount() {
System.out.println("执行了保存");
}
@Override
public void updateAccount(int i) {
System.out.println("执行了更新" + i);
}
@Override
public int deleteAccount() {
System.out.println("执行了删除");
return 0;
}
}
//创建测试类
package test;
import org.example.service.IAccountService;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
public class AOPtest {
public static void main(String[] args) {
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("classpath:springBean.xml");
IAccountService accountService = applicationContext.getBean("accountService", IAccountService.class);
accountService.saveAccount();
accountService.updateAccount(1);
accountService.deleteAccount();
}
}
切入点表达式的写法
表达式解析依赖jar包
<!-- 解析切入点表达式 -->
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.8.7</version>
</dependency>
关键字:execution(表达式)
表达式:访问修饰符 返回值 包名.包名....类名.方法名(参数列表)
示例:execution(public void org.example.service.impl.AccountService.saveAccount()
表达式全通配写法:* *..*.*(..)
1.访问修饰符可以省略
示例:execution(void org.example.service.impl.AccountService.saveAccount()
2.返回值可以使用通配符 * ,表示任意返回值
示例:execution(* org.example.service.impl.AccountService.saveAccount()
3.包名可以使用通配符表示任意包,但是有几级包就需要写几个*
示例:execution(* *.*.*.*.AccountService.saveAccount()
org.example.service.impl 改成 *.*.*.*
4.包名可以使用.. 表示当前包及其子包
示例:execution(* *..AccountService.saveAccount()
5.类名和方法名可以使用*号通配
示例:类名改成* execution(* *..*.saveAccount()
示例:方法名改成* execution(* *..*.*()
6.参数列表可以直接写数据类型
基本类型直接写名称
引用类型写 包名.类型
示例:execution( * *..*.*(int)) 写上数据类型,配匹配有对应类型参数的方法
6.1.可以使用通配符表示任意类型,但是必须有参数可以使用..表示
示例:execution( * *..*.*(..)) 参数.. 匹配所有方法
7.实际开发中切入点表达式的通常写法
切换到业务层实现类下的所有方法 *.包名.实现类包名.*.*(..)
示例:execution(* org.example.service.impl.*.*(..)
多个切入点相同表达式配置优化
可以使用 aop:pointcut标签提取相同的表达式,配置的通知类型根据标签的ID引用表达式
aop:pointcut的属性:
id:表示表达式的唯一标识
expression:指定表达式的内容
通知类型的标签根据 pointcut-ref 属性引用表达式的ID
aop:pointcut可以写在aspect里边,只能当前切面可用,写在aspect外面,所有切面均可引用,写在aspect外面,必须在aspect标签之前。
<!--配置AOP-->
<aop:config>
<!-- 配置切面 -->
<aop:aspect id="logAdvice" ref="logger">
<!--aop:pointcut:配置切入点表达式
id:表示表达式的唯一标识
expression:指定表达式的内容
aop:pointcut可以写在aspect里边,只能当前切面可用,写在aspect外面,所有切面均可引用,写在aspect外面,必须在aspect标签之前。
-->
<aop:pointcut id="logPointcut" expression="execution( * org.example.service.impl.*.*(..))"/>
<!-- 配置通知类型 并建立通知方法和切入点方法的关联-->
<!-- 前置通知 在切入点方法之前执行-->
<aop:before method="beforePrintLog" pointcut-ref="logPointcut"></aop:before>
<!-- 后置通知 在切入点正常执行后执行-->
<aop:after-returning method="afterReturningPrintLog" pointcut-ref="logPointcut"></aop:after-returning>
<!-- 异常通知 在切入点方法执行异常后执行-->
<aop:after-throwing method="afterThrowingPrintLog" pointcut-ref="logPointcut"></aop:after-throwing>
<!-- 最终通知 无论切入点是否异常都会在切入点方法执行后执行-->
<aop:after method="afterPrintLog" pointcut-ref="logPointcut"></aop:after>
</aop:aspect>
</aop:config>
常用通知类型
前置通知:aop:before标签,在切入点方法之前执行
后置通知:aop:after-returning标签,在切入点正常执行后执行
异常通知:aop:after-throwing标签,在切入点方法执行异常后执行
最终通知:aop:after标签,无论切入点是否异常都会在切入点方法执行后执行
<?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">
<!-- 注入service -->
<bean id="accountService" class="org.example.service.impl.AccountService"></bean>
<!-- 配置logger 类-->
<bean id="logger" class="org.example.util.Logger"></bean>
<!--配置AOP-->
<aop:config>
<!-- 配置切面 -->
<aop:aspect id="logAdvice" ref="logger">
<!-- 配置通知类型 并建立通知方法和切入点方法的关联-->
<!-- 前置通知 在切入点方法之前执行-->
<aop:before method="beforePrintLog" pointcut="execution( * org.example.service.impl.*.*(..))"></aop:before>
<!-- 后置通知 在切入点正常执行后执行-->
<aop:after-returning method="afterReturningPrintLog" pointcut="execution( * org.example.service.impl.*.*(..))"></aop:after-returning>
<!-- 异常通知 在切入点方法执行异常后执行-->
<aop:after-throwing method="afterThrowingPrintLog" pointcut="execution( * org.example.service.impl.*.*(..))"></aop:after-throwing>
<!-- 最终通知 无论切入点是否异常都会在切入点方法执行后执行-->
<aop:after method="afterPrintLog" pointcut="execution( * org.example.service.impl.*.*(..))"></aop:after>
</aop:aspect>
</aop:config>
</beans>
环绕通知
标签:aop:around
如果只配置环绕通知,没有在环绕通知的方法里执行业务方法的调用,只会执行通知,不会执行业务代码,spring提供了ProceedingJoinPoint接口的proceed方法执行业务方法的调用,把ProceedingJoinPoint作为环绕通知方法的参数,在通知方法中通过ProceedingJoinPoint对象的proceed方法调用被增强的方法
getArgs():ProceedingJoinPoint的 getArgs()方法获取被增强方法的参数列表
proceed():ProceedingJoinPoint的proceed()方法执行被增强方法的调用
<aop:config>
<!-- 配置切面 -->
<aop:aspect id="logAdvice" ref="logger">
<!--表达式配置-->
<aop:pointcut id="logPointcut" expression="execution( * org.example.service.impl.*.*(..))"/>
<!--环绕通知配置-->
<aop:around method="aroundReturningPrintLog" pointcut-ref="logPointcut"></aop:around>
<!--
配置了环绕通知 方法没有执行,通知方法执行了
原因:动态代理中环绕通知有明确的切入点方法调用,而我们代码中没有
解决:spring提供了ProceedingJoinPoint接口的proceed方法,可以调用切入点方法
-->
</aop:aspect>
</aop:config>
/**
* 环绕通知方法
*/
public Object aroundReturningPrintLog(ProceedingJoinPoint pjp){
try {
System.out.println("环绕通知--方法调用前的代码是前置通知");
//获取参数列表
Object[] args = pjp.getArgs();
//方法调用
Object proceed = pjp.proceed(args);
System.out.println("环绕通知--方法调用后的代码是后置通知");
return proceed;
} catch (Throwable e) {
System.out.println("环绕通知--异常里的异常通知");
throw new RuntimeException(e);
}finally {
System.out.println("环绕通知--finally里的最终通知");
}
}
spring基于注解的AOP配置
1.配置spring容器创建时要扫描的包
context:component-scan标签,或者使用配置类
2.开启AOP事务支持
aop:aspectj-autoproxy 标签,或者在配置类中加上@EnableAspectJAutoProxy
3.编写切面类,并注入到spring容器中
使用@Aspect表示当前类为切面类
4.定义通知类型,注解如下
@Pointcut("表达式") :前置通知
@AfterReturning(“表达式”):后置通知
@AfterThrowing(“表达式”):异常通知
@After(“表达式”):最终通知
@Around(“表达式”):环绕通知
5.定义统一表达式的方法
@Pointcut("execution(“表达式”):定义一个无返回值方法,在该方法加上 @Pointcut("execution(“表达式”)注解,在通知类型的注解中把表达式换成方法名进行引用
<?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
https://www.springframework.org/schema/context/spring-context.xsd">
<!--配置spring容器创建容器时要扫描的包-->
<context:component-scan base-package="org.example"></context:component-scan>
<!-- 配置开启AOP注解支持-->
<aop:aspectj-autoproxy></aop:aspectj-autoproxy>
</beans>
package org.example.util;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;
@Aspect //表示当前类是一个切面类
@Component("logger") //注入到spring容器中
public class Logger {
//定义表达式
@Pointcut("execution( * org.example.service.impl.*.*(..))")
private void pt1(){}
/**
* 前置通知
*/
@Before("pt1()")
public void beforePrintLog(){
System.out.println("前置通知******Logger类中的beforePrintLog方法开始记录日志了。。。");
}
/**
* 后置通知
*/
@AfterReturning("pt1()")
public void afterReturningPrintLog(){
System.out.println("后置通知******Logger类中的afterReturningPrintLog方法开始记录日志了。。。");
}
/**
* 异常通知
*/
@AfterThrowing("pt1()")
public void afterThrowingPrintLog(){
System.out.println("异常通知******Logger类中的afterThrowingPrintLog方法开始记录日志了。。。");
}
/**
* 最终通知
*/
@After("pt1()")
public void afterPrintLog(){
System.out.println("最终通知******8Logger类中的afterPrintLog方法开始记录日志了。。。");
}
/**
* 环绕通知方法
*/
@Around("pt1()")
public Object aroundReturningPrintLog(ProceedingJoinPoint pjp){
try {
System.out.println("环绕通知--方法调用前的代码是前置通知");
//获取参数列表
Object[] args = pjp.getArgs();
//方法调用
Object proceed = pjp.proceed(args);
System.out.println("环绕通知--方法调用后的代码是后置通知");
return proceed;
} catch (Throwable e) {
System.out.println("环绕通知--异常里的异常通知");
throw new RuntimeException(e);
}finally {
System.out.println("环绕通知--finally里的最终通知");
}
}
}