一.需求引入
在开发过程中,总会有一些功能与业务逻辑代码耦合度不强(例如保存日志,提交事务,权限验证,异常处理),我们可以将这些代码提取到一个工具类中,需要使用时在调用工具类来实现. 但是这样也会有弊端,那就是我们的代码已经开发完毕,后期如果需要增加公共功能就需要更改原来的代码,有时工作量很大,非常繁杂. 那么我们能不能在不修改原有代码的情况下,也能添加公共功能? AOP面向切面编程技术应用而生
二.AOP概述
AOP的全称为Aspect-Orientend-Programming,即面向切面编程.它是面向对象编程(OOP)的一种补充. 为了解决上述问题,AOP思想随之产生,AOP采取横向抽取机制,将分散在各个方法中的重复代码提取出来,然后在程编译或运行时,再将这些提取出来的代码应用到需要执行的地方.这种采用横向抽取机制的方式,采用传统的OOP思想是无法办到的,因为OOP只能实现父子关系的纵向重用.
AOP核心原理:
使用动态代理的方式在方法执行前后或者出现异常时加入相关的逻辑.
注:
-
虽然AOP是一种新的编程思想,但是却不是OOP的替代品,是对OOP的延伸和补充.
-
AOP也不是Spring所特有的思想,很多框架都引入了AOP思想
三.Spring AOP基本术语
-
连接点(JoinPoint): 程序执行的某个特定位置,如某个方法调用前,调用后,方法抛出异常后,这些代码中的特定点称为连接点.可以简单理解为可以被增强的方法
-
切入点(pointcut): 类中有很多方法可以被增强,但比如实际只有add(),update()方法被增强了,那么add,update方法就被称为切入点,可以简单理解为实际被增强的方法
-
通知(Advice): 通知是指一个特定的切面在切入点要做的事情,通知分为方法执行前通知,方法执行后通知,环绕通知等.可以简单理解为实际增强的功能
-
切面(Aspect): 把通知添加到切入点的过程称为切面
-
目标(Target): 代理的目标对象(连接点,切入点,所在的类实际的执行者)
-
代理(Proxy): 向目标对象应用通知时创建的代理对象
四.Spring AOP实现
AspectJ 是一个基于 Java 语言的 AOP 框架,它提供了强大的 AOP 功能,且其实现方式更为简捷,使用更为方便, 而且还支持注解式开发。所以,Spring 又将 AspectJ 的对于 AOP 的实现也引入到了自己的框架中。
1.导入spring aspect jar包
其在Maven仓库中的项目坐标如下:
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aspects</artifactId>
<version>5.2.2.RELEASE</version>
</dependency>
2.编写业务功能及需要增强的功能
此处我们以保存管理员之前增加日志功能为例
2.1原有业务功能:
package com.ffyc.springdemo.dao;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Repository;
@Repository
public class AdminDao {
@Autowired
JdbcTemplate jdbcTemplate;
public void saveAdmin(){
System.out.println("保存管理员");
}
public void update(){
System.out.println("修改管理员");
}
}
2.2保存日志功能
package com.ffyc.springdemo.util;
public class CommonUtil {
public void saveLog(){
System.out.println("保存日志");
}
}
3.1基于 aspectj 的 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:context="http://www.springframework.org/schema/context"
xmlns:aop="http://www.springframework.org/schema/aop"
xsi:schemaLocation="http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop.xsd">
<!--开启Spring注解扫描-->
<context:component-scan base-package="com.ffyc.springdemo"></context:component-scan>
<!--导入数据库连接拦截-->
<import resource="db.xml"></import>
<!--让spring管理要增强功能的类-->
<bean id="commonUtil" class="com.ffyc.springdemo.util.CommonUtil"></bean>
<!--aop配置-->
<aop:config>
<!--配置切入点-->
<aop:pointcut id="saveAdmin" expression="execution(* com.ffyc.springdemo.dao.AdminDao.saveAdmin(..))"/>
<!-- 配置通知和切入点 -->
<aop:aspect ref="commonUtil">
<aop:before method="saveLog" pointcut-ref="saveAdmin"></aop:before>
</aop:aspect>
</aop:config>
</beans>
注:
AspectJ中有五种通知类型:
-
前置通知(Before Advice): 在连接点之前执行的通知(我们上述栗子就使用的是前置通知)
-
后置通知(After Advice): 在连接点之后执行的通知
-
环绕通知(Around Advice): 包围一个连接点的通知,这是最强大的通知类型.环绕通知可以在方法调用前后完成自定义的行为.它也可以选择是否继续执行连接点或直接返回它们自己的返回值或抛出异常来结束执行.
-
返回后通知(AfterReturning Advice): 在连接点正常完成后执行的通知
-
抛出异常后通知(After Throwing Advice): 在方法抛出异常退出时执行的通知
3.2基于注解方式的实现
基于注解方式的实现与基于xml文件实现的方式相同,都是基于AspectJ的实现,因此也有五种通知类型,在此我们只示例部分即可.
1.启动AspectJ 支持:
我们需要将此配置加入spring.xml文件中,以启动AspectJ支持
<aop:aspectj-autoproxy />
2.在业务类上添加注解: @Component @Aspect
package com.ffyc.springdemo.util;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
@Component
@Aspect
public class CommonUtil {
}
3.在所需方法上添加注解:
package com.ffyc.springdemo.util;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.AfterThrowing;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
@Component
@Aspect
public class CommonUtil {
@Before("execution(* com.ffyc.springdemo.dao.AdminDao.*(..))")
public void saveLog(){
System.out.println("保存日志");
}
@AfterThrowing(value = "execution(* com.ffyc.springdemo.dao.AdminDao.*(..))",throwing = "e")
public void exception(Throwable e){
System.out.println("系统忙"+e.getMessage());
}
}
注:
-
注解通知中的“execution(* com.ffyc.springdemo.dao.* .*(..))”称为切入点表达式 execution()是最常用的切点表达式,可分为5个部分:
-
execution为表达式主体
-
第一个*为返回值类型, * 表示可返回所有类型
-
com.ffyc.springdemo.dao.AdminDao为包名,表示需要拦截的包名
-
第二个*为类名, * 表示包中所有类
-
*(..)最后的 * 表示方法名, * 表示类中的所有方法;括号中表示方法的参数, .. 表示任意参数
-
4.结果分析:
我们在上述saveLog()方法上添加了前置通知,并选择为AdminDao中所有方法都执行,因此在保存管理员和修改管理员之前都执行了保存日志操作.