AOP全程Aspect Oriented Programming面向切面编程,是一种编程范式,用于指导开发者如何组织程序结构。相关的概念比如OOP全称为Object Oriented Programming面向对象编程。
AOP的作用是在不惊动原始设计的基础上为其功能进行增强,这也是Spring倡导的一种概念:无侵入式/无入侵式编程。
例如:需要获得SQL执行的时间
Long startTime = System.currentTimeMillis();
//业务逻辑...
Long endTime = System.currentTimeMillis();
System.out.println(endTime - startTime);
具体实现是抽取统计逻辑通用代码(通知),在需要添加统计执行时长的位置(连接点),在需要添加的具体方法的特定位置(切入点),将通知与切入点绑定的操作称为切面。
核心概念 | 说明 |
---|---|
代理(Proxy) | SpringAOP核心本质是采用代理模式实现的 |
连接点(JoinPoint) | 程序执行过程中的任意位置,粒度为执行方法、抛出异常、设置变量等。SpringAOP中可理解为方法的执行。 |
切入点(PointCut) | 匹配连接点的式子 |
通知(Advice) | 在切入点处执行的操作,即共性功能。SpringAOP中功能最终会以方法的形式来呈现。 |
通知类 | 定义通知的类 |
切面(Aspect) | 描述通知与切入点的对应关系 |
SpringAOP中一个切入点可以指描述一个具体方法,也可以匹配多个方法。
- 描述一个具体方法:比如
com.jc.dao
包下的UserDao接口中的无形参无返回值的save()
方法 - 匹配多个方法:所有的
save()
方法、所有以get开头的方法,所有以Dao结尾的接口中的任意方法、所有带有一个参数的方法
入门
实现:在接口执行前输出当前系统时间
实现方式有两种可通过XML也可以使用注解的方式,推荐注解方式开发。
操作思路
- 导入坐标
- 制作连接点方法
- 提取共性功能,对应通知类的通知。
- 定义切入点
- 绑定切入点与通知之间的关系(切面)
导入坐标
默认spring-context
包中已经包含了SpringAOP包
导入aspectj包
org.aspectj
aspectjweaver
1.9.7
在Spring核心配置中添加@EnableAspectJAutoProxy
来开启Spring对AOP注解驱动支持
$ vim config/SpringConfig.java
@Configuration
@ComponentScan("com.jc")
@EnableAspectJAutoProxy
public class SpringConfig {
}
定义通知类并添加@Component
受到Spring容器管理,同时添加@Aspect
定义当前类为切面类。
$ vim aop/BaseAdvice.java
//定义通知类
@Component
@Aspect
public class BaseAdvice {
}
定义切入点,切入点定义依托一个不具有实际意义的方法,即无参数、无返回值、方法体无实际逻辑,格式为private void pt(){}
。
//定义通知类
@Component
@Aspect
public class BaseAdvice {
//定义切入点
@Pointcut("execution(void com.jc.dao.UserDao.update())")
private void pt(){}
}
绑定切入点与通知关系,并指定通知添加到原始连接点的具体执行位置。例如:@Before("pt()")
。
//定义切入点
@Pointcut("execution(void com.jc.dao.UserDao.update())")
private void pt(){}
//定义共享功能
@Before("pt()")
public void method(){
System.out.println(System.currentTimeMillis());
}
流程
- SpringAOP本质是代理模式
SpringAOP工作流程
- SpringIoC容器启动
- 通过
@EnableAspectJAutoProxy
和@Aspect
读取所有切面配置的切入点 - 初始化Bean,判断Bean对应类中的方法是否匹配到任意切入点。若匹配失败则创建Bean对象,若匹配成功则创建原始对象(目标对象)的代理对象。
- 获取Bean执行方法,获取Bean后调用方法并执行以完成操作,当获取的Bean是代理对象时则根据代理对象的运行模式执行原始方法与增强的内容来完成操作。
SpringAOP核心概念
- 目标对象(Target):原始功能去掉共性功能对应的类产生的对象,这种对象是无法直接完成最终工作的。
- 代理(Proxy):目标对象无法直接完成工作,需要对其进行功能回填,通过原始对象的代理对象实现。
错误:添加@EnableAspectJAutoProxy
注解后出现无法访问的错误
Exception in thread "main" org.springframework.beans.factory.NoSuchBeanDefinitionException: No qualifying bean of type 'com.jc.service.impl.AccountServiceImpl' available
因为SpringAOP是基于动态代理创建的对象,而默认是使用JDK的动态。对于JDK的动态代理对象是实现类的兄弟类,所以通过实现类的对象去匹配是找不到目标对象的,可通过接口类型或实现类的ID去匹配。若要非在实现类的对象去拿的话,可以修改Spring动态代理方式为cglib,cglib是代理出一个实现类的子类,可使用实现类的对象去匹配。
@EnableAspectJAutoProxy(proxyTargetClass=true)
切入点表达式
切入点是要进行增强的方法,切入点表达式则是要进行增强方法的描述方式。
描述方式1:描述接口
例如:执行com.jc.service
包下AccountService
接口中的无参save()
方法
@Pointcut("execution(void com.jc.service.AccountService.save())")
描述方式2:描述实现类
@Pointcut("execution(void com.jc.service.impl.AccountServiceImpl.save())")
切入点表达式标准格式
动词关键词(访问修饰符 返回值 包名.类/接口名.方法名(参数) 异常名)
例如:
@Service
public class AccountServiceImpl implements AccountService {
@Autowired
private SysUserMapper userMapper;
@Override
public SysUser getById(long id){
return userMapper.getById(id);
}
}
@Pointcut("execution(public com.jc.domain.SysUser com.jc.service.impl.AccountServiceImpl.getById(long))")
元素 | 实例 | 说明 |
---|---|---|
动作关键词 | execution | 描述切入点的行为动作,比如execution表示执行到指定切入点。 |
访问修饰符 | public | 可省略 |
返回值 | com.jc.domain.SysUser | - |
包名 | com.jc.service.impl | - |
类/接口名 | AccountServiceImpl | - |
方法名 | getById | - |
参数 | long | - |
异常名 | - | 方法定义中抛出指定异常,可省略。 |
通配符
使用通配符描述切入点
通配符 | 说明 | 示例 |
---|---|---|
* | 单个独立的任意符号,可独立出现,可作为前缀或后缀的匹配符出现。 | public * com.jc.*.UserService.find*(*) |
.. | 多个连续的任意符号,可独立出现,用于简化包名与参数的书写。 | public User com..UserService.findById(..) |
+ | 专用于匹配子类型 | * *..*Service+.*(..) |
常用书写
* com.jc.*.*Service.*(..)
所有代码按标准规范开发,否则以下技巧会全部失效。
书写技巧 | 说明 |
---|---|
切入点 | 通常描述接口,而不描述实现类。 |
访问控制修饰符 | 针对接口开发均采用public 描述,可省略。 |
返回值类型 | 对增/删/改使用精准类型加速匹配,对于查询使用* 通配快速描述。 |
包名 | 书写尽量不使用.. 匹配,因为效率过低,常用* 做单个包描述匹配或精准匹配。 |
接口名/类名 | 与模块相关的采用* 匹配。比如UserService书写成*Service 来绑定业务层接口名。 |
方法名 | 动词采用精准匹配,名词采用* 匹配。比如getById书写成getBy* 。 |
参数 | 规则较为复杂,根据业务方法灵活调整。 |
异常 | 通常不使用异常作为匹配规则 |
通知类型
AOP通知描述了抽取的共性功能,根据共性功能抽取的位置不同,最终运行代码时,要将其加入到合理的位置。
AOP通知共分为5种类型:
通知类型 | 标注 |
---|---|
前置通知 | @Before |
后置通知 | @After |
环绕通知 | @Around |
返回后通知 | @AfterReturning |
抛出异常后通知 | @AfterThrowing |
环绕通知
- 环绕通知必须依赖形参
ProceedingJoinPoint
才能实现对原始方法的调用,进而实现原始方法调用前后同时添加通知。 - 通知中如果没有使用
ProceedingJoinPoint
对原始方法进行调用,将跳过原始方法的执行。 - 对原始方法的调用可以不接收返回值,通知方法设置为
void
即可。若接收返回值则必须设定为Object
类型。 - 原始方法的返回值如果是
void
类型,通知方法的返回值可以设置成void
,也可以设置成Object
。 - 由于无法预知原始方法运行后是否会抛出异常,因此环绕通知方法必须抛出
Throwable
对象。
//定义共享功能
@Around("pt()")
public Object method(ProceedingJoinPoint pjp) throws Throwable {
System.out.println("around before advice...");
Object obj = pjp.proceed();//对原始操作的调用
System.out.println("around after advice...");
return obj;
}
例如:测量业务层接口万次执行效率
业务层接口执行前后分别记录时间,求差值得到执行时长。通知类型选择前后均可以增强的类型-环绕通知。
//定义通知类
@Component
@Aspect
public class BaseAdvice {
//定义切入点:匹配业务层所有方法
@Pointcut("execution(* com.jc.service.*Service.*(..))")
private void servicePt(){}
//定义共享功能
@Around("servicePt()")
public Object runSpeed(ProceedingJoinPoint pjp) throws Throwable {
Long startTime = System.currentTimeMillis();
Object obj = pjp.proceed();//对原始操作的调用
Long endTime = System.currentTimeMillis();
Long spendTime = endTime - startTime;
//获取执行签名信息
Signature s = pjp.getSignature();
//通过签名获取执行类型
String className = s.getDeclaringTypeName();
//通过签名获取执行操作名称
String methodName = s.getName();
System.out.println(className + " " + methodName + " spend " + spendTime + "ms");
return obj;
}
}
com.jc.service.AccountService getById spend 813ms
通知获取数据
AOP通知获取数据分为三种:参数、返回值、异常
获取切入点方法的参数
- JoinPoint 适用于前置、后置、返回后、抛出异常后通知
@Before("servicePt()")
public void before(JoinPoint jp){
Object[] args = jp.getArgs();
System.out.println("Before:" + Arrays.toString(args));
}
@After("servicePt()")
public void after(JoinPoint jp){
Object[] args = jp.getArgs();
System.out.println("After:" + Arrays.toString(args));
}
- ProceedJointPoint 适用于环绕通知
@Around("servicePt()")
public Object runSpeed(ProceedingJoinPoint pjp) throws Throwable {
Object[] args = pjp.getArgs();
Object obj = pjp.proceed(args);//对原始操作的调用
return obj;
}
获取切入点方法返回值
- 返回后通知
@AfterReturning(value = "servicePt()", returning = "ret")
public void afterReturning(Object ret){
System.out.println("After Returning:" + ret);
}
若同时需要JoinPoint和Object两个返回值,则JoinPoint必须在前。
@AfterReturning(value = "servicePt()", returning = "ret")
public void afterReturning(JoinPoint jp, Object ret){
System.out.println("After Returning:" + jp + " " + ret);
}
- 环绕通知
获取切入点方法运行异常信息
- 抛出异常后通知
- 环绕通知
例如:在业务方法执行前对所有输入参数去空格处理,使用处理后的参数调用原始方法,环绕通知中存在对原始方法的调用。
@Around("servicePt()")
public Object trimSpace(ProceedingJoinPoint pjp) throws Throwable {
Object[] args = pjp.getArgs();
//去除字符串参数中的空格
for (int i = 0; i < args.length; i++) {
if(args[i].getClass().equals(String.class)){
args[i] = args[i].toString().trim();
}
}
//对原始操作的调用
Object obj = pjp.proceed(args);
return obj;
}