Spring系列(三)——Spring的AOP开发
前言
AOP指的是Aspect Oriented Programming的缩写,意思是面向切面编程,AOP思想也是Spring框架中的重要组成部分,利用这个特性我们可以实现不同业务逻辑间的解耦。作为java后端开发人员,熟练掌握Spring的AOP操作也是必备技能,本篇文章将对Spring的采用配置和注解两种方式进行AOP开发的步骤进行讲解,希望对各位读者有所参考。
一、Spring的AOP简介
什么是AOP?
AOP 为 Aspect Oriented Programming 的缩写,意思为面向切面编程,是通过预编译方式和运行期动态代理实现程序功能的统一维护的一种技术。
AOP是OOP的延续,是软件开发中的一个热点,也是Spring框架中的一个重要内容,是函数式编程的一种衍生范型。利用AOP可以对业务逻辑的各个部分进行隔离,从而使得业务逻辑各部分之间的耦合度降低,提高程序的可重用性,同时提高了开发的效率。
简单来说,AOP就是通过运行期间生成动态代理的方式,实现代码隔离,维护方便,功能增强效果。
AOP的使用场景
我们在上面的简介中提到了,AOP可以帮助我们实现代码间的解耦。这么一说可能大家并不能很好地理解其中的意思,不妨可以看一下下面这个例子:
假如现在老板交代了一个任务给你,要你把项目现有的所有方法都加上日记搜集的功能,你一听后发现这可是个大工程,但是项目的类和方法这么多,在每个方法中都写上日志搜集的代码显然很冗余。有没有更好的解决办法呢?
这时,你想到了可以把日志搜集的功能封装成一个工具类,每个方法只需要调用一下工具类中的方法即可。这样的工作量就比之前好太多了。你暗自夸了一下自己聪明后,就马不停蹄地做了起来,虽然工作量少了,但要改的地方还是不少。终于,你改了一整天后终于改好了,想着终于可以下班吃泡面了。老板走了过来说,“小明啊,我觉得早上提的那个需求还是先不做了”。这时,只留下你一个人看着一天的工作成果欲哭无泪。
说到底,会出现这种情况的原因是日志搜集的代码和原有的代码还是耦合在了一起,所以一旦日志搜集的代码不需要了,那么处理起来就会十分的麻烦,而AOP就可以很好的帮助我们解决这个问题。我们通过动态代理的方式,在生成的代理对象中对原有的方法进行日志搜集的增强,当不需要日志搜集的功能之后,我们只需要取消掉这个增强即可。这样,就大大地降低了我们维护代码的难度。
再说说AOP的底层实现原理
实际上,AOP 的底层是通过 Spring 提供的的动态代理技术实现的。在运行期间,Spring通过动态代理技术动态的生成代理对象,代理对象方法执行时进行增强功能的介入,在去调用目标对象的方法,从而完成功能的增强。而Spring底层生成动态代理的方式主要有两种,第一种是JDK动态代理,第二种是cglib动态代理。二者的区别在于前者只能对实现了接口的类实现动态代理,而cglib主要是对指定的类生成一个子类,覆盖其中的方法,没有接口的限制。在实际应用中,spring框架会根据目标类是否实现了接口来决定采用哪种动态代理的方式。
由于实现动态代理的底层逻辑并不是本篇文章的要点,所以这里并不对其进行讲解。我们更多地关注如何使用Spring进行AOP的配置。
AOP的相关术语介绍
- Target(目标对象):代理的目标对象
- Proxy (代理):一个类被 AOP 织入增强后,就产生一个结果代理类
- Joinpoint(连接点):所谓连接点是指那些被拦截到的点。在spring中,这些点指的是方法,因为spring只支持方法类型的连接点
- Pointcut(切入点):所谓切入点是指我们要对哪些 Joinpoint 进行拦截的定义
- Advice(通知/ 增强):所谓通知是指拦截到 Joinpoint 之后所要做的事情就是通知
- Aspect(切面):是切入点和通知(引介)的结合
- Weaving(织入):是指把增强应用到目标对象来创建新的代理对象的过程。spring采用动态代理织入,而AspectJ采用编译期织入和类装载期织入
二、基于XML的AOP开发
(一)快速入门案例
我们先通过实现一个AOP的小案例来体验一下AOP的作用
步骤一:引入依赖
<!--引入spring context依赖-->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.0.5.RELEASE</version>
</dependency>
<!--引入aspectj的依赖-->
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.8.13</version>
</dependency>
<!--引入spring测试依赖-->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
<version>5.0.5.RELEASE</version>
</dependency>
<!--引入Junit-->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
<scope>test</scope>
</dependency>
这里的话由于使用到了spring集成Junit的测试依赖,所以上面额外引多了两个依赖。否则的话引入spring-context
和aspectjweaver
就可以了。
步骤二:编写测试接口、实现类和切面类
- 测试接口
public interface UserDao {
void save();
}
- 实现类
public class UserDaoImpl implements UserDao{
public void save() {
System.out.println("已经执行了save方法...");
}
}
- 切面类
// 创建自定义界面类,主要封装增强的方法
public class MyAspect {
public void beforeAllMethods(){
System.out.println("前置增强执行了...");
}
}
步骤三:配置spring核心配置文件
- 先引入对应的约束头
xmlns:aop="http://www.springframework.org/schema/aop"
xsi:schemaLocation="http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd"
- 添加
bean
和aop
配置
<!-- 注入bean-->
<bean id="userDao" class="com.qiqv.configuration.UserDaoImpl"></bean>
<bean id="myAspect" class="com.qiqv.configuration.MyAspect"></bean>
<!--配置aop-->
<aop:config>
<!--引用myAspect作为切面对象-->
<aop:aspect ref="myAspect">
<!--设置拦截的连接点-->
<aop:before method="beforeAllMethods" pointcut="execution(public void com.qiqv.configuration.UserDao.save())"></aop:before>
</aop:aspect>
</aop:config>
步骤四:编写测试类代码
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = {"classpath:applicationContext.xml"})
public class SpringAOPTest {
@Autowired
private UserDao userDao;
@Test
public void aopOfConfiuration(){
userDao.save();
}
}
我们在上面的代码中可以看到,增强(即增强方法)和连接点(即被增强的功能代码)并没有耦合在一起,但实际上我们执行代码的时候代码却确确实实得到了增强,原因就是当spring识别到当前执行的方法是切入点后,就会使用代理机制,动态创建userDaoImpl类的代理对象,根据通知类别,在代理对象的对应位置,将通知对应的功能织入,完成完整的代码逻辑运行。
(二)切面表达式的写法
表达式语法:
execution([修饰符] 返回值类型 包名.类名.方法名(参数))
- 访问修饰符可以省略
- 返回值类型、包名、类名、方法名可以使用星号* 代表任意
- 包名与类名之间一个点 . 代表当前包下的类,两个点 .. 表示当前包及其子包下的类
- 参数列表可以使用两个点 .. 表示任意个数,任意类型的参数列表
我们可以使用上一小节的切面表达式为例
execution(public void com.qiqv.configuration.UserDao.save())
execution(void com.qiqv.configuration.UserDao.*(..))
execution(* com.qiqv.configuration.*.*(..))
execution(* com.qiqv.configuration..*.*(..))
execution(* *..*.*(..)) // 这样配置的话几乎所有的方法都会被拦截到
(三)AOP通知的类型
AOP的通知(增强)类型有好几种,我们可以选择对指定方法进行前置增强、后置增强等
- 通知的配置语法
<aop:通知类型 method=“切面类中方法名” pointcut=“切点表达式"></aop:通知类型>
名称 | 标签 | 说明 |
---|---|---|
前置通知 <aop:before> | 用于配置前置通知。 | 指定增强的方法在切入点方法之前执行 |
后置通知 <aop:after-returning> | 用于配置后置通知。 | 指定增强的方法在切入点方法之后执行 |
环绕通知 <aop:around> | 用于配置环绕通知。 | 指定增强的方法在切入点方法之前和之后都执行 |
异常抛出通知 <aop:throwing> | 用于配置异常抛出通知。 | 指定增强的方法在出现异常时执行 |
最终通知 <aop:after> | 用于配置最终通知。 | 无论增强方式执行是否有异常都会执行 |
这里的话,我们举个例子来演示一下各个类型的通知
- Spring核心配置文件配置AOP
<!--配置aop-->
<aop:config>
<!--引用myAspect作为切面对象-->
<aop:aspect ref="myAspect">
<!--设置拦截的连接点-->
<aop:pointcut id="mypointcut" expression="execution(* *..UserDao.*(..))"/>
<aop:before method="beforeAllMethods" pointcut-ref="mypointcut"></aop:before>
<aop:after-returning method="afterAllMethods" pointcut-ref="mypointcut"></aop:after-returning>
<aop:around method="aroundMethods" pointcut-ref="mypointcut"></aop:around>
<aop:after-throwing method="adviceAfterException" pointcut-ref="mypointcut"></aop:after-throwing>
<aop:after method="finalAdvice" pointcut-ref="mypointcut"></aop:after>
</aop:aspect>
</aop:config>
这里需要注意的是,但存在多个表达式的内容一样的时候,我们可以使用pointcut
标签对共有的表达式进行提取。
- 切面类代码
public class MyAspect {
public void beforeAllMethods(){
System.out.println("前置增强执行了...");
}
public void afterAllMethods(){
System.out.println("后置增强执行了...");
}
public Object aroundMethods(ProceedingJoinPoint joinPoint) throws Throwable {
System.out.println("开始环绕增强了...");
Object result = joinPoint.proceed();
System.out.println("结束环绕增强...");
return result;
}
public void adviceAfterException(){
System.out.println("异常抛出通知增强...");
}
public void finalAdvice(){
System.out.println("最终通知增强...");
}
}
- 代理类方法
public class UserDaoImpl implements UserDao{
public void save() {
System.out.println("已经执行了save方法...");
int i = 1/0;
}
}
测试代码和上一小节一样,执行后结果如下:
三、基于注解的AOP开发
(一)使用注解AOP开发的基本入门
如果已经掌握了AOP的配置文件开发,熟悉AOP的实际原理的话,那么掌握AOP的注解开发其实学习成本是很低的。我们下面就用一个小案例来看如何使用注解进行开发吧。
- spring核心配置文件
<!--配置组件扫描-->
<context:component-scan base-package="com.qiqv.anno"></context:component-scan>
<!--配置开启aop的自动代理-->
<aop:aspectj-autoproxy></aop:aspectj-autoproxy>
这里需要注意,除了常规的组件扫描外,我们还需要使用aop:aspectj-autoproxy
注解开启动态代理,这个注解启动后,对应配置@Aspect
注解的切面类才能够被找到
- 接口
public interface UserDao {
void save();
}
- 实现类
@Component("userDao")
public class UserDaoImpl implements UserDao{
public void save() {
System.out.println("已经执行了save方法...");
}
}
- 切面类
@Component("myAspect")
@Aspect
public class MyAspect {
@Before("execution(* *..UserDao.*(..))")
public void beforeAllMethods(){
System.out.println("前置增强执行了...");
}
}
这里需要注意,切面类需要加上@Aspect
注解,表明该类作为切面类。同时在我们可以在增强方法上面配置通知的类型和拦截的具体方法。
- 测试类
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = {"classpath:applicationContext.xml"})
public class SpringAOPTest {
@Autowired
private UserDao userDao;
@Test
public void aopOfConfiuration(){
userDao.save();
}
}
(二)不同通知类型的注解方式
名称 | 注解 | 说明 |
---|---|---|
前置通知 | @Before | 用于配置前置通知。指定增强的方法在切入点方法之前执行 |
后置通知 | @AfterReturning | 用于配置后置通知。指定增强的方法在切入点方法之后执行 |
环绕通知 | @Around | 用于配置环绕通知。指定增强的方法在切入点方法之前和之后都执行 |
异常抛出通知 | @AfterThrowing | 用于配置异常抛出通知。指定增强的方法在出现异常时执行 |
最终通知 | @After | 用于配置最终通知。无论增强方式执行是否有异常都会执行 |
(三)切点表达式的抽取
同 xml 配置 aop 一样,我们可以将切点表达式抽取。抽取方式是在切面内定义方法,在该方法上使用@Pointcut注解定义切点表达式,然后在在增强注解中进行引用。具体如下:
@Before("MyAspect.myPointcut()")
public void beforeAllMethods(){
System.out.println("前置增强执行了...");
}
@Pointcut("execution(* *..UserDao.*(..))")
public void myPointcut(){
}
至此,对于Spring使用AOP进行开发的介绍就到此结束了,想要了解更多关于Spring的内容,可以看我Spring其他系列的文章。