Spring - 浅谈AOP思想及Spring的三种实现

前言

Spring的核心思想IoC、DI,及beans、context的XML配置都有了一定的理解
下一个是Spring的又一核心:AOP


什么是AOP

AOP:Aspect Oriented Programming,面向切面编程

面向切面编程,通过预编译方式和运行期间动态代理实现程序功能的统一维护的一种技术。AOP是OOP的延续,是软件开发中的一个热点,也是Spring框架中的一个重要内容,是函数式编程的一种衍生范型。利用AOP可以对业务逻辑的各个部分进行隔离,从而使得业务逻辑各部分之间的耦合度降低,提高程序的可重用性,同时提高了开发的效率。

在OOP面对对象编程时,一个个功能分隔为对象,也可以说是分层,如何在功能层次间添加一些功能?
AOP拦截 功能层次(切面),在层次间通过动态代理,添加功能
横切

在这里插入图片描述

AOP的知识:

在这里插入图片描述

这涉及到代理模式:设计模式(15)结构型模式 - 代理模式:静态、动态代理、Cglib代理

动态代理的强悍在与:不管什么类,通过获得class,即可以动态的在指定的方法前后添加功能


AOP相关术语

  • 目标对象(Target Object): 包含连接点的对象。也被称作被通知或被代理对象,也就是我们想要处理的对象

  • 代理(Proxy):Java中提供了代理类,Spring进行了封装加强,代理类会获得目标对象的class,然后根据需求修改目标类,Spring提供了jdk代理、Cglib代理,由AopProxyFactory根据AdvisedSupport对象的配置来决定(目标对象是接口用jdk代理,否则用Cglib代理)

  • 横切关注点:对哪些方法进行拦截,拦截后怎么处理,这些关注点称之为横切关注点

  • Advice(通知):AOP在特定的切入点上执行的增强处理,定义AOP何时被调用,有before(前置),after(后置),afterReturning(最终),afterThrowing(异常),around(环绕)

  • JointPoint(连接点):应用执行过程中能够插入切面的一个点,这个点可以是方法的调用、异常的抛出,一般是方法的调用。是被拦截到的点,因为Spring只支持方法类型的连接点,所以在Spring中连接点指的就是具体业务被拦截到的方法,实际上连接点还可以是字段或者构造器

  • Pointcut(切入点):指定一个通知将被引发的一系列连接点的集合,可以书写切入点表达式execution(* com.learn.springdemo.aop.Service.(..))

  • Aspect(切面):通常是一个类,里面可以定义切入点和通知,Spring配置文件表现为pointcut、advice的父标签,其中可以配置相关标签

  • weave(织入):将切面应用到目标对象并创建一个被增强的对象的过程

  • introduction(引入):在不修改代码的前提下,引入可以在运行期为类动态地添加一些方法或字段

来自:spring aop 及实现方式,做一定的补充

在后续的使用中会逐渐了解这些知识


通知Advice

Advice是表示何时在目标对象中切入:

  • BeforeAdvice前置通知:在目标对象连接点之前执行
  • AfterReturningAdvice后置通知:在连接点正常执行完成后执行,如果连接点抛出异常,则不会执行
  • AroundAdvice环绕通知:围绕在连接点前后,如一个方法调用的前后。这是最强大的通知类型,能在方法调用前后自定义一些操作
  • AfterThrowingAdvice异常通知:在连接点抛出异常后执行
  • AfterAdvice最终通知:在连接点执行完成后执行,不管是正常执行完成,还是抛出异常,都会执行返回通知中的内容

在Spring有两种接口实现通知,Advice和Interceptor

在这里插入图片描述

Advice是AOP编程中某一个方面(Aspect切面)在某个连接点(JoinPoint)所执行的特定动作,这个连接点(JoinPoint)可以是自 定义的;
而Spring中的Interceptor更多关注程序运行时某些特定连接点(属性访问,对象构造,方法调用)时的动作。
确切的说,Interceptor的范围更窄一些


Spring中的AOP相关类

Spring的xml配置文件有AOP的相关标签,而这些标签都与Java类相对应
可想而知,Spring会提供一些对应的AOP相关类

所以,从最符合我们思维的类开始(后续可以通过XML标签、注解实现)

首先,定义好service层,也就是要切入的目标对象

在这里插入图片描述

UserService:

package com.aop.service;

public interface UserService {
    public void add();
    public void update();
    public void delete();
    public void select();
}

UserServiceImpl:

package com.aop.service;

public class UserServiceImpl implements UserService {
    public void add() {
        System.out.println("增加了一个User");
    }

    public void update() {
        System.out.println("更新了一个User");
    }

    public void delete() {
        System.out.println("删除了一个User");
    }

    public void select() {
        System.out.println("查询一个User");
    }
}

通过实现Advice接口的子接口,设置成具体通知类:

前置通知:

package com.aop.Log;

import org.springframework.aop.MethodBeforeAdvice;

import java.lang.reflect.Method;

//前置通知
public class BeforeLog implements MethodBeforeAdvice {
    
    public void before(Method method, Object[] args, Object target) throws Throwable {
        System.out.println(target.getClass().getName()+" 的 "+method.getName()+" 被执行了 ");
    }
}

后置通知(多了个返回值returnValue):

package com.aop.Log;

import org.springframework.aop.AfterReturningAdvice;

import java.lang.reflect.Method;

//后置通知
public class AfterLog implements AfterReturningAdvice {

    public void afterReturning(Object returnValue, Method method, Object[] args, Object target) throws Throwable {
        System.out.println("执行了:"+method.getName()+"返回的结果为:"+returnValue );
    }
}

目标类、通知类都设置好了,现在需要通过Spring将它们关联起来

在Spring的配置文件:application.xml中配置:

  1. 往Spring注册Bean
  2. 添加AOP约束
  3. 配置AOP:设置切入点、通知,因为是aop:config标签,只有原生advisor,具体的通知类型会到自定义的通知类中决定
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       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/aop
    https://www.springframework.org/schema/aop/spring-aop.xsd">

    <bean id="userService" class="com.aop.service.UserServiceImpl"/>
    <bean id="beforeLog" class="com.aop.Log.BeforeLog"/>
    <bean id="afterLog" class="com.aop.Log.AfterLog"/>
    
    <!--使用原生spring api接口-->
    <!--配置aop:导入aop约束-->
    <aop:config>
        <!--切入点 expression 表达式 execution(要执行的位置)-->
        <aop:pointcut id="pointcut" expression="execution(* com.aop.service.UserServiceImpl.*(..))"/>
        <!--执行环绕-->
        <aop:advisor advice-ref="beforeLog" pointcut-ref="pointcut"/>
        <aop:advisor advice-ref="afterLog" pointcut-ref="pointcut"/>
    </aop:config>
</beans>

测试类:依旧是解析xml配置文件,获得bean

    @Test
    public void testLog(){
        ApplicationContext context = new ClassPathXmlApplicationContext("application.xml");
        //动态代理代理的是接口
        UserService userService = (UserService) context.getBean("userService");
        userService.add();
    }

仅仅是运行了add方法

在这里插入图片描述

多了前置通知和后置通知,且可以通过4个参数获得类、方法、返回值等等属性

其他接口类的使用也类似


基于XML配置文件

前面只使用了最简单的aop标签,具体判断是什么类型的通知需要到具体的自定义的通知类

比较麻烦,需要继承不同的通知类实现对应的方法,Spring提供了对应的after、before等等标签,简化操作

现在,重写一个AOP类:
可以通过JoinPoint获得连接点的对象的属性(JoinPoint与ProceedingJoinPoint)

package com.aop.diy;

import org.aspectj.lang.JoinPoint;

public class DiyPointcut {
    public void before(JoinPoint joinPoint){
        System.out.println("=== 方法执行前 ===");
        System.out.println("=== 方法名:"+joinPoint.getSignature().getName()+" ===");
        System.out.println(joinPoint.getSourceLocation());
        System.out.println(joinPoint.getStaticPart());
    }
    public void after(){
        System.out.println("=== 方法执行后 ===");
    }

}

重写一个配置类:diyAop.xml:

<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       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/aop
    https://www.springframework.org/schema/aop/spring-aop.xsd">

    <bean id="userService" class="com.aop.service.UserServiceImpl"/>
    <!--自定义aop-->
    <bean id="diy" class="com.aop.diy.DiyPointcut"/>

    <aop:config>
        <!--自定义切面 ref引用类-->
        <aop:aspect ref="diy">
            <!--切入点-->
            <aop:pointcut id="pointcut" expression="execution(* com.aop.service.UserServiceImpl.*(..))"/>
            <!--通知-->
            <aop:before method="before" pointcut-ref="pointcut"/>
            <aop:after method="after" pointcut-ref="pointcut"/>
        </aop:aspect>
    </aop:config>

</beans>

写一个测试方法:

    @Test
    public void test(){
        ApplicationContext context = new ClassPathXmlApplicationContext("diyAop.xml");
       
        UserService userService = (UserService) context.getBean("userService");
        userService.add();
    }

在这里插入图片描述

这样,就不需要实现特定的通知接口,只需定义一个类,然后在Spring中标识就可以完成AOP

Spring提供了对应的标签,我们只需写好AOP类,注入Spring容器并标识即可


JoinPoint和ProceedingJoinPoint

因为我们的通知类仅仅是一个普通的类,想要获得目标对象的属性,就需要外部提供的参数,Spring提供了两个对象:JoinPoint和ProceedingJoinPoint

  • JoinPoint

提供的方法:
在这里插入图片描述

很简单,就几个主要的get方法

String toString():连接点所在位置的相关信息  
String toShortString():连接点所在位置的简短相关信息  
String toLongString():连接点所在位置的全部相关信息  
Object getThis():获取切面对象本身
Object getTarget():返回目标对象
Object[] getArgs():获取连接点方法的参数集合 
Signature getSignature() :获取连接点的方法对象
SourceLocation getSourceLocation():返回连接点方法所在类文件中的位置  
String getKind():连接点类型  
StaticPart getStaticPart():返回连接点静态部分 
  • ProceedingJoinPoint
    ProceedingJoinPoint继承JoinPoint接口,新增了两个用于执行连接点方法的方法:
Object proceed() throws Throwable:通过反射执行目标对象的连接点处的方法;
Object proceed(Object[] args) throws Throwable:通过反射执行目标对象连接点处的方法,不过使用新的入参替换原来的入参。

ProceedingJoinPoint 用于环绕通知,因为环绕通知=前置通知+目标方法执行+后置通知,proceed使目标方法执行

proceed是aop代理链执行的方法,决定是否走代理链还是走自己拦截的其他逻辑

简单实现一个环绕通知,在前面的DiyAop类中添加:

    public Object doAround(ProceedingJoinPoint pjp) throws Throwable{
        System.out.println("======执行环绕通知开始=========");
        // 调用方法的参数
        Object[] args = pjp.getArgs();
        // 调用的方法名
        String method = pjp.getSignature().getName();
        // 获取目标对象
        Object target = pjp.getTarget();
        // 执行完方法的返回值
        // 调用proceed()方法,就会触发切入点方法执行
        Object result=pjp.proceed();
        System.out.println("输出,方法名:" + method + ";目标对象:" + target + ";返回值:" + result);
        System.out.println("======执行环绕通知结束=========");
        return result;
    }

然后在xml中配置<aop:around method="doAround" pointcut-ref="pointcut"/>

在这里插入图片描述

环绕通知一定要使用proceed方法执行目标方法

如果不执行:

    public void doAround(ProceedingJoinPoint pjp) throws Throwable{
        System.out.println("======执行环绕通知开始=========");
        // 调用方法的参数
        Object[] args = pjp.getArgs();
        // 调用的方法名
        String method = pjp.getSignature().getName();
        // 获取目标对象
        Object target = pjp.getTarget();
        // 执行完方法的返回值
        // 调用proceed()方法,就会触发切入点方法执行
        /*Object result=pjp.proceed();*/
        System.out.println("输出,方法名:" + method + ";目标对象:" + target  /*+ ";返回值:" + result*/);
        System.out.println("======执行环绕通知结束=========");
    }

在这里插入图片描述

并没有运行目标方法


基于注解方式

Spring可以使用注解代替XML标签

编写一个AnnotationPointcut:

  • @Aspect标注该类为切面类,等同与前面XML中<aop:aspect ref="diy">,设置切面类
  • @Before、@After、@Around就等同于对应的aop:before标签,其中要定义好切入点表达式
  • PointCut和ProceedingPointCut使用与前面一样
package com.aop.diy;

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;

//标注这个类是一个切面
@Aspect
public class AnnotationPointcut {
    //在环绕中,可以给定一个参数代表我们获取处理切入的点ProceedingJoinPoint
    @Before("execution(* com.aop.service.UserServiceImpl.*(..))")
    public void before(){
        System.out.println("=== 前置通知:方法执行前 ===");
    }
    @After("execution(* com.aop.service.UserServiceImpl.*(..))")
    public void after(){
        System.out.println("=== 最终通知:方法执行后 ===");
    }


    @Around("execution(* com.aop.service.UserServiceImpl.*(..))")
    public void around(ProceedingJoinPoint joinPoint) throws Throwable {

        System.out.println("==== 环绕通知:环绕前 ====");
        Signature signature = joinPoint.getSignature();
        System.out.println("signature: "+signature);
        Object proceed = joinPoint.proceed();
        System.out.println("==== 环绕通知:环绕后 ====");
    }
}

既然设置好了通知、切入点,即扫描注解即可
annotation.xml:这里还设置了bean,其实也可以通过注解自动扫描

<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       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/aop
    https://www.springframework.org/schema/aop/spring-aop.xsd">


    <bean id="userService" class="com.aop.service.UserServiceImpl"/>

    <bean id="annotationPointcut" class="com.aop.diy.AnnotationPointcut"/>
    <!--开启注解支持 jdk动态代理(proxy-target-class="false")-->
    <aop:aspectj-autoproxy/>
</beans>

测试类:和前面的一样,改一下xml文件:

在这里插入图片描述

这里有个问题,环绕通知在前置通知前? 前面通过XML配置,前置通知在环绕通知前


通知顺序问题

基于XML配置:
在这里插入图片描述

基于注解:

在这里插入图片描述

都是前置、最终、环绕,为什么顺序不同?

原因:如果是XML配置文件,书写顺序也会有一定的影响:

<aop:aspect ref="diy">
    <!--切入点-->
    <aop:pointcut id="pointcut" expression="execution(* com.aop.service.UserServiceImpl.*(..))"/>
    <!--通知-->
    <aop:before method="before" pointcut-ref="pointcut"/>
    <aop:after method="after" pointcut-ref="pointcut"/>
    <aop:around method="doAround" pointcut-ref="pointcut"/>
</aop:aspect>

这样,就是先运行before

调一下顺序,结果就不同:

<aop:around method="doAround" pointcut-ref="pointcut"/>
<aop:before method="before" pointcut-ref="pointcut"/>
<aop:after method="after" pointcut-ref="pointcut"/>

在这里插入图片描述

小结:
默认顺序是:环绕通知proceed方法调用前 - 》前置通知 - 》目标方法执行 - 》环绕通知proceed方法调用后 - 》最终通知 - 》后置通知/异常通知

  1. 最终通知是在后置通知前运行(可以测试)
  2. 后置通知和异常通知只能有一个,执行异常后置通知就不会生效

总结

  • AOP是面向切面编程,体现了面向对象的松散耦合
  • Spring有三种实现:继承通知类、XML标签、注解,本质上都是继承通知类的
  • 通知Advice有5中:前置、后置、最终、异常、环绕,默认执行顺序是:环绕通知proceed方法前-》前置通知-》环绕通知proceed方法 -》最终通知 -》后置通知、异常通知

学海无涯苦作舟

都看到这了,点个赞呗(^_−)☆

  • 8
    点赞
  • 33
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值