上次对Spring中的IOC容器做了一个简单的分析,本篇文章,我在大概说一下我对Spring AOP的理解。
什么是AOP?
首先说一说什么是AOP,我们知道OOP是面向对象编程,而AOP叫做面向切面编程,它是对面向对象编程的补充和完善,通过面向对象编程,我们可以再对象运行过程中,可以为对象动态织入一些扩展功能,比如,日志记录,权限控制,事务管理等。
AOP可以解决哪些问题?
AOP目前被广泛应用于项目开发中,我们在一个项目中,一般会把项目分为几个功能模块,这些功能有核心功能,也有一些扩展功能,这些扩展功能的代码实现有可能是重复的,因此, 我们可以将重复代码抽离出来,对其进行封装,然后再需要的地方调用,这样的编程方式属于硬编码的方式,甚至还有可能需要修改原来的代码,这样就违反了开闭原则。这个时候我们就用到了AOP。
我们可以利用面向切面编程的方式,在遵循开闭原则的基础上,为原有的系统添加一些扩展功能,并且可以控制对象的行为。
AOP底层原理分析
AOP的底层是使用代理机制来实现功能扩展,主要使用的两种代理模式为:jdk动态代理和cglib代理。
二者区别:jdk动态代理需要代理对象和被代理对象实现相同的接口,而cglib代理则是集成被代理对象来实现AOP的功能扩展。二者代理机制不同,但是在AOP中的应用是相同的,只要产生代理对象即可。
我们知道,之前我们在Controller层注入Service接口的实现类,当请求过来时,Controller先找到Service接口,然后找到接口的实现类,在我们使用代理之后,我们的Service接口不会再直接去找他的实现类,而是去找实现类的代理对象。由代理对象来代替被代理对象来执行相应的操作。在执行相应操作之前,会先执行我们定义的切面类中的方法,也即是我们的扩展功能,以此来达到不修改原有代码而为系统添加扩展功能的目的。
如下图:是jdk动态代理和cglib代理
AOP编程的基本步骤
第一步导入依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
我这里是在Springboot中引入依赖,这是Spring整合AOP的依赖。如果不是SpringBoot项目需要导入以下依赖:
1.org.springframework.aop-3.1.1.RELEASE 这个是spring的 AOP编程必备包
2.cglib-nodep-2.1_3
3.aspectjweaver-1.6.2
4.aspectj-1.6.12
5.aopalliance-1.0
这是Maven仓库的网站 https://mvnrepository.com/
第二步创建切面处理类
切面处理类有两部分构成,一个是切入点,使用注解@Pointcut注解来标识,一个是扩展方法使用@Around,@Before等注解来标识。代码如下:
@Aspect
@Component
@Slf4j
public class SysLogAspect {
/**
* @Pointcut注解用于定义切入点
* bean表达式为切入点表达式,
* bean表达式内部指定的bean对象中
* 所有方法为切入点(进行功能扩展的点)
*/
@Pointcut("bean(sysUserServiceImpl)")
public void logPointCut() {}
/**
* @Around 描述的方法为环绕通知,用于功能增强
* 环绕通知(目标方法执行之前和之后都可以执行)
* @param jp 连接点 (封装了要执行的目标方法信息)
* @return 目标方法的执行结果
* @throws Throwable
*/
@Around("logPointCut()")
public Object around(ProceedingJoinPoint jp)
throws Throwable{
try {
log.info("start:"+System.currentTimeMillis());
Object result=jp.proceed();//调用下一个切面方法或目标方法
log.info("after:"+System.currentTimeMillis());
return result;
}catch(Throwable e) {
log.error(e.getMessage());
throw e;
}
}
}
其中:
@Aspect 注解用于标识此类为一个AOP横切面对象
@Component是把此切面类交给Spring管理
@Pointcut 注解用于定义本类中的切入点,本案例中切入点表达式用的是 bean表达式,这个表达式以bean开头,bean括号中的内容为一个spring管理的某个bean对象的id。指定了为哪一个类添加扩展功能
@Around用于定义一个环绕通知(满足切入点表达式的核心业务方法执行之前和之后执行的一个操作)
@Slf4j是我使用了log4j实现日志输出,于AOP无关。因为我做的是一个日志功能增强,所以用到了这个。
切面术语:用于封装扩展业务的一个类的对象。
通知术语:切面扩展业务中的一个操作(方法)。
这样一个AOP切面功能增强就完成了。上边代码,我为SysUserServiceImpl添加了一个日志处理功能,输出SysUserServiceImpl中方法的执行时间。这样就在不修改原来代码的基础上添加了扩展功能,这就是AOP面向切面编程。
AOP中的切入点表达式
在Spring中通过切入点表达式定义具体切入点,为指定位置添加扩展功能。一下是Sprng中的几个切入点表达式:
指示符 | 作用 |
---|---|
bean | 用于匹配指定bean id的的方法执行 |
within | 用于匹配指定包名下类型内的方法执行 |
execution | 用于进行细粒度方法匹配执行具体业务 |
@annotation | 用于匹配指定注解修饰的方法执行 |
Bean表达式应用
bean表达式应用于类级别,实现粗粒度的切入点定义
形式 | 作用 |
---|---|
bean(“userServiceImpl”)) | 指定一个类中所有方法 |
bean("*ServiceImpl") | 指定所有的后缀为serviceImpl的类 |
说明:bean表达式内部的对象是由spring容器管理的一个bean对象,表达式内部的内部的名字应该时spring容器中某个bean的key.
within表达式应用
within表达式应用于类级别,实现粗粒度的切入点表达式定义:
形式 | 作用 |
---|---|
within(“aop.service.UserServiceImpl”) | 指定类,只能指定一个类 |
within(“aop.service.*”) | 只包括当前目录下的类 |
within(“aop.service . .*”) | 指定当前目录包含所有子目录中的类 |
execution 表达式应用
execution表达式应用于方法级别,细粒度的切入点表达式定义
语法:execution(返回值类型 包名.类名.方法名(参数列表))
形式 | 作用 |
---|---|
execution(void aop.service.UserServiceImpl.addUser()) | 匹配方法 |
execution(void aop.service.PersonServiceImpl.addUser(String)) | 方法参数必须为字符串 |
within(“aop.service. * . *”) | 万能配置 |
@annotation表达式应用
@annotaion表达式应用于方法级别,实现细粒度的切入点表达式定义:
形式 | 作用 |
---|---|
@annotation(com.jt.common.anno.RequestLog) | 指定一个需要实现增强功能的方法 |
其中:RequestLog为我们自己定义的注解,当我们使用@RequestLog注解修饰业务层方法时,系统底层会在执行此方法时进行扩展操作.它只能指定被注解修饰的方法。
AOP中的通知类型
在AOP编程中有五种类型的通知:
1)前置通知 (@Before) 方法执行之前执行
2)返回通知 (@AfterReturning) 方法return之后执行
3)异常通知 (@AfterThrowing) 方法出现异常之后执行
4)后置通知 (@After) : 又称之为最终通知(finally)
5)环绕通知 (@Around)
其中环绕通知@Around的优先级最高的通知,被@Around通知修饰的方法会优先执行,然后依次是:@Before、@AfterReturning@AfterThrowing、@After。
另外还有一个注解是@Order,当有多个切面类时可能会用到这个注解,这个注解中可以配置一个Vauel属性值,int类型,数值越小,切面的优先级为越高,优先级高的切面会优先执行。