前言
Spring AOP 是Spring框架的特点之一,今天来详细说一下Spring AOP
目录
3.使用Aspectj实现切面,使用Spring AOP进行配置
一,AOP概念
1.基础概念
AOP为Aspect Oriented Programming的缩写,意为:面向切面编程,通过预编译方式和运行期间动态代理实现程序功能的统一维护的一种技术。AOP是OOP的延续,是软件开发中的一个热点,也是Spring框架中的一个重要内容,是函数式编程的一种衍生范型。利用AOP可以对业务逻辑的各个部分进行隔离,从而使得业务逻辑各部分之间的耦合度降低,提高程序的可重用性,同时提高了开发的效率。
2.举例说明
简单来讲,比如说项目的登录功能需要验证账号的权限是管理员还是普通用户,在以往的编程思维中,我们需要在原有的程序代码中加入判断账号权限的代码,伪代码如下:
登录界面方法(账号,密码){
if 普通用户 {
前往普通用户界面;
}else if 管理员用户{
前往管理员页面;
}
}
按照以往的编程思维来说这样的做法是没有什么问题的,但是在真实的项目环境中,需求也许会不断地变动,此时再使用传统的编程思想似乎会让修改功能这件事情变得异常的繁琐,此时就需要用到面向切面的编程思想了,我们可以形象地将这种编程思想理解为:以程序运行的某个环节为切入点,将代码织入到程序当中去,而原本的功能代码不作修改,仅需要配置以后方可实现我们需要的功能,如下图:
原功能:
用AOP的方式增强(扩展)原功能 :
3.理解AOP
很明显,相较于以前的的方式,我们原有的功能接口并没有受到影响,它们仍可以专注于自己所负责的业务功能,而不需要关注其他非该接口关注的逻辑或业务,通过上面这个例子我们能明显感受到AOP降低了代码的耦合性,提高程序的可重用性,同时提高 了开发的效率。
通俗的讲,AOP就是不通过修改源代码的方式,在主干功能里添加新的功能。
4.AOP的功能
除了用户登录验证之外,AOP 还可以实现:
- 统一日志记录
- 统一方法执行时间统计
- 统一的返回格式设置
- 统一的异常处理
- 事务的开启和提交等
也就是说使用 AOP 可以扩充多个对象的某个能力,所以 AOP 可以说是 OOP(Object Oriented Programming,面向对象编程)的补充和完善。
二,AOP组成
- Aspect(切面)
- Joint point(连接点)
- Pointcut(切点)
- Advice(增强)
- Target(目标对象)
- Weaving(织入)
1. @Aspect
切面(Aspect
)由切点(Pointcut)和通知(Advice)组成,它既包含了横切逻辑的定义,也包括了连接点的定义。
切面类 用@Aspect
注解修饰。
切面是包含了:通知、切点和切面的类,相当于 AOP 实现的某个功能的集合。
2. 连接点(Join Point)
应用执行过程中能够插入切面的一个点,这个点可以是方法调用时、抛出异常时、甚至修改字段时。切面代码可以利用这些点插入到应用的正常流程之中,并添加新的行为。
连接点相当于需要被增强的某个 AOP 功能的所有方法。
3. 切入点 @Pointcut
Pointcut 是匹配 连接点 (Join Point) 的谓词。
Pointcut 的作用就是提供一组规则(使用 AspectJ pointcut expression language 来描述)来匹配 Join Point,给满足规则的 Join Point 添加 Advice。
切点相当于保存了众多连接点的一个集合(如果把切点看成一个表,而连接点就是表中一条一条的数据)。
4. 通知 @Advice
切面也是有目标的 ——它必须完成的工作。在 AOP 术语中,切面的工作被称之为通知。
通知:定义了切面是什么,何时使用,其描述了切面要完成的工作,还解决何时执行这个工作的问题。切点相当于要增强的方法。
Spring 切面类中,可以在方法上使用以下注解,会设置方法为通知方法,在满足条件后会通知本方法进行调用:
- 前置通知 @Before:通知方法会在目标方法调用之前执行。
- 后置通知 @After:通知方法会在目标方法返回或者抛出异常后调用。
- 返回之后通知 @AfterReturning:通知方法会在目标方法返回后调用。
- 抛异常后通知 @AfterThrowing:通知方法会在目标方法抛出异常后调用。
- 环绕通知使用 @Around:通知包裹了被通知的方法,在被通知的方法通知之前和调用之后执行自定义的行为。
此处引入一张图片便于大家理解:
三,Spring AOP的使用
1.配置prom.xml
<!--aop依赖1:aspectjrt -->
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjrt</artifactId>
<version>1.9.5</version>
</dependency>
<!--aop依赖2: aspectjweaver -->
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.9.5</version>
</dependency>
2.编写通知方法
public class LogAspectj {
//前置通知
public void beforeAdvice(JoinPoint joinPoint){
System.out.println("========== 【Aspectj前置通知】 ==========");
}
//后置通知:方法正常执行后,有返回值,执行该后置通知:如果该方法执行出现异常,则不执行该后置通知
public void afterReturningAdvice(JoinPoint joinPoint,Object returnVal){
System.out.println("========== 【Aspectj后置通知】 ==========");
}
public void afterAdvice(JoinPoint joinPoint){
System.out.println("========== 【Aspectj后置通知】 ==========");
}
//环绕通知
public Object aroundAdvice(ProceedingJoinPoint joinPoint) throws Throwable {
System.out.println("##########【环绕通知中的前置通知】##########");
Object returnVale = joinPoint.proceed();
System.out.println("##########【环绕通知中的后置通知】##########");
return returnVale;
}
/**
* 异常通知:方法出现异常时,执行该通知
*/
public void throwAdvice(JoinPoint joinPoint, Exception ex){
System.out.println("出现异常:" + ex.getMessage());
}
}
3.使用Aspectj实现切面,使用Spring AOP进行配置
<!--业务组件bean-->
<bean id="userServiceBean" class="com.apesource.service.impl.UserServiceImpl"/>
<!--日志Aspect切面-->
<bean id="logAspectjBean" class="com.apesource.log.LogAspectj"/>
<!--使用Aspectj实现切面,使用Spring AOP进行配置-->
<aop:config>
<!--配置切面-->
<!--注入切面bean-->
<aop:aspect ref="logAspectjBean">
<!--定义Pointcut:通过expression表达式,来查找 特定的方法(pointcut)-->
<aop:pointcut id="pointcut"
expression="execution(* com.apesource.service.impl.*.create*(..))"/>
<!--配置"前置通知"-->
<!--在pointcut切入点(serviceMethodPointcut)查找到 的方法执行"前",
来执行当前logAspectBean的doBefore-->
<aop:before method="beforeAdvice" pointcut-ref="pointcut"/>
<!--配置“后置通知”-->
<!--returning属性:配置当前方法中用来接收返回值的参数名-->
<aop:after-returning returning="returnVal"
method="afterReturningAdvice" pointcut-ref="pointcut"/>
<aop:after method="afterAdvice" pointcut-ref="pointcut"/>
<!--配置"环绕通知"-->
<aop:around method="aroundAdvice" pointcut-ref="pointcut"/>
<!--配置“异常通知”-->
<!--throwing属性:配置当前方法中用来接收当前异常的参数名-->
<aop:after-throwing throwing="ex" method="throwAdvice" pointcut-ref="pointcut"/>
</aop:aspect>
</aop:config>
在 Spring 中,尽管使用 XML 配置文件可以实现 AOP 开发,但是如果所有的相关配置都集中在配置文件中,势必会导致 XML 配置文件过于臃肿,从而给维护和升级带来一定的困难。我们还可以尝试一种基于注解的实现方式来进行实现:
4.使用Aspectj实现切面(基于注解的实现方式)
//声明当前类为Aspect切面,并交给Spring容器管理
@Component
@Aspect
public class LogAnnotationAspectj {
private final static String EXPRESSION =
"execution(* com.apesource.service.impl.*.create*(..))";
//前置通知
@Before(EXPRESSION)
public void beforeAdvice(JoinPoint joinPoint){
System.out.println("========== 【Aspectj前置通知】 ==========");
}
//后置通知:方法正常执行后,有返回值,执行该后置通知:如果该方法执行出现异常,则不执行该后置通知
@AfterReturning(value = EXPRESSION,returning = "returnVal")
public void afterReturningAdvice(JoinPoint joinPoint,Object returnVal){
System.out.println("========== 【Aspectj后置通知】 ==========");
}
//后置通知
@After(EXPRESSION)
public void afterAdvice(JoinPoint joinPoint){
System.out.println("========== 【Aspectj后置通知】 ==========");
}
//环绕通知
@Around(EXPRESSION)
public Object aroundAdvice(ProceedingJoinPoint joinPoint) throws Throwable {
System.out.println("##########【环绕通知中的前置通知】##########");
Object returnVale = joinPoint.proceed();
System.out.println("##########【环绕通知中的后置通知】##########");
return returnVale;
}
// 异常通知:方法出现异常时,执行该通知
@AfterThrowing(value = EXPRESSION,throwing = "ex")
public void throwAdvice(JoinPoint joinPoint, Exception ex){
System.out.println("********** 【Aspectj异常通知】执行开始 **********");
System.out.println("出现异常:" + ex.getMessage());
System.out.println("********** 【Aspectj异常通知】执行结束 **********");
}
}
在配置文件中添加自动扫描:
<!-- 自动扫描器 -->
<context:component-scan base-package="com.apesource"/>
<!--配置Aspectj的自动代理-->
<aop:aspectj-autoproxy/>
四.AOP的实现
Spring的AOP实现原理其实很简单,就是通过动态代理实现的。
Spring AOP 采用了两种混合的实现方式:JDK 动态代理和 CGLib 动态代理。
- JDK动态代理:Spring AOP的首选方法。 每当目标对象实现一个接口时,就会使用JDK动态代理。目标对象必须实现接口
- CGLIB代理:如果目标对象没有实现接口,则可以使用CGLIB代理。
此处主要讲述JDK动态代理
JDK动态代理
Spring默认使用JDK的动态代理实现AOP,类如果实现了接口,Spring就会使用这种方式实现动态代理。
JDK实现动态代理需要两个组件,首先第一个就是InvocationHandler接口。
我们在使用JDK的动态代理时,需要编写一个类,去实现这个接口,然后重写invoke方法,这个方法其实就是我们提供的代理方法。
(关于invoke方法API文档中的解释:)
Object invoke (Object proxy,
Method method,
Object[] args)
throws Throwable
参数
- proxy - 调用该方法的代理实例
- method -所述方法对应于调用代理实例上的接口方法的实例。 方法对象的声明类将是该方法声明的接口,它可以是代理类继承该方法的代理接口的超级接口。
- args -包含的方法调用传递代理实例的参数值的对象的阵列,或null如果接口方法没有参数。 原始类型的参数包含在适当的原始包装器类的实例中,例如java.lang.Integer或java.lang.Boolean 。
结果
从代理实例上的方法调用返回的值。 如果接口方法的声明返回类型是原始类型,则此方法返回的值必须是对应的基本包装类的实例; 否则,它必须是可声明返回类型的类型。 如果此方法返回的值是null和接口方法的返回类型是基本类型,那么NullPointerException将由代理实例的方法调用抛出。 如上所述,如果此方法返回的值,否则不会与接口方法的声明的返回类型兼容,一个ClassCastException将代理实例的方法调用将抛出。
异常
Throwable - 从代理实例上的方法调用抛出的异常。 异常类型必须可以分配给接口方法的throws子句中声明的任何异常类型java.lang.RuntimeException检查的异常类型java.lang.RuntimeException或java.lang.Error 。 如果检查的异常是由这种方法是不分配给任何的中声明的异常类型throws接口方法的子句,则一个UndeclaredThrowableException包含有由该方法抛出的异常将通过在方法调用抛出代理实例。
源码如下:
1.创建接口,定义方法
// 创建接口
public interface UserDao {
// 假设这是一个数据添加的方法
public int add(int a, int b);
}
2.创建接口实现类实现方法
public class UserDaoImpl implements UserDao {
@Override
public int add(int a, int b) {
System.out.println("add方法执行了!");
//模拟返回数据
return a + b;
}
}
3.创建Proxy类实现代码增强
public class JDKProxy{
public static void main(String[] args) {
//创建接口实现类的代理对象
Class[] interfaces {UserDao.class};
UserDaoImpl userDao = new UserDaoImpl();
UserDao dao = (UserDao) Proxy.newProxyInstance(JDKProxy.class.getClassLoader(), interfaces, new UserDaoProxy(userDao));
int result = dao.add(1, 2);
System.out.println("result = " + result);
}
}
4.创建代理对象代码
public class UserDaoProxy implements InvocationHandler {
//创建哪个代理对象传递哪个
private Object obj;
public UserDaoProxy(Object obj) {
this.obj = obj;
}
//增强(扩展)的逻辑
@Override
public Object invoke(Object obj, Method method, Object[] obj) throws Throwable {
// 方法执行前
System.out.println("方法执行前执行" + method.getName() + "传递的参数" + Arrays.toString(args));
Object result = method.invoke(obj, args);
// 方法执行后
System.out.println("方法执行后执行" + obj);
return result;
}
}
运行结果:
方法执行前执行 add 传递的参数 [1, 2]
add方法执行了!
方法执行后执行 add com.ape.springtest.UserDaoImpl@698p1da6
result = 3
JDK代理的优缺点:
优点:
JDK动态代理是JDK原生的,不需要任何依赖即可使用;
通过反射机制生成代理类的速度要比CGLib操作字节码生成代理类的速度更快;
缺点:
如果要使用JDK动态代理,被代理的类必须实现了接口,否则无法代理;
JDK动态代理无法为没有在接口中定义的方法实现代理,假设我们有一个实现了接口的类,我们为它的一个不属于接口中的方法配置了切面,Spring仍然会使用JDK的动态代理,但是由于配置了切面的方法不属于接口,为这个方法配置的切面将不会被织入。
JDK动态代理执行代理方法时,需要通过反射机制进行回调,此时方法执行的效率比较低;
五,总结
AOP 是 Spring 的核心之一,在 Spring 中经常会使用 AOP 来简化编程。在 Spring 框架中使用 AOP 主要有以下优势。
提供声明式企业服务,特别是作为 EJB 声明式服务的替代品。最重要的是,这种服务是声明式事务管理。
允许用户实现自定义切面。在某些不适合用 OOP 编程的场景中,采用 AOP 来补充。
可以对业务逻辑的各个部分进行隔离,从而使业务逻辑各部分之间的耦合度降低,提高程序的可重用性,同时也提高了开发