Spring核心特性—— AOP(面向切面编程)

本文涉及的产品
日志服务 SLS,月写入数据量 50GB 1个月
简介: Spring核心特性—— AOP(面向切面编程)

前言

我们曾经在谈到Spring 的Transactional 注解时提到了AOP,并言明了AOP是该注解实现的基础。

但是说到底,还没有系统的介绍过AOP,讲Spring不提AOP总归是缺了点什么的。而且,相信大家在面试的时候也经历过不少AOP相关的提问,例如

一、SpringAop是什么?

在IT行业里,AOP为Aspect Oriented Programming的缩写,意为:面向切面编程,通过预编译方式和运行期间动态代理实现程序功能的统一维护的一种技术。


打个比方,我们现在有一个老项目,因为早期项目的不规范,输出的日志很少,导致定位BUG很困难。所以想对业务方法能以log形式打印出入参、出参。但是我们不可能真的每个方法都去改代码。

这时候就可以用到AOP,,我们可以先写一段打印日志的代码,然后把业务方法作为切入点,把打印日志的代码作为增强模块植入进去。这样,在每次访问业务方法前和后,就会执行打印日志的代码,输出入参及出参


二、简单使用

1. 术语介绍

在接触SpringAop之前,我们需要知道一些术语:


  • 连接点(Joinpoint)
  • 能够被增强的方法都算连接点

  • 切入点(Pointcut)
  • 实际需要被增强的方法的集合

  • 增强/通知(Advice)
  • 实际增强的那部分代码称为增强

  • 切面(Aspect)
  • 增强和切入点的结合


import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.stereotype.Component;
@Aspect
@Component
public class AopTest {
    // 通知(增强):本方法的内容
    // 切入点:com.zhanfu.service包下的所有方法
    // 增强种类:前置增强(before)
    // 切面:本类包含通知及切入点,本类就是切面类
    // 本方法的含义就是,在执行com.zhanfu.service包的所有方法前,都先执行本方法
    @Before("execution(* com.zhanfu.service..*.*(..))")
    public void before(JoinPoint joinPoint){
        System.out.println("前置通知:在目标执行前被调用的通知");
    }
}

2. 增强的种类

我们前面说了增强其实就是在执行目标方法时,多执行一点”增强“的内容,那么根据”增强“内容 与 原方法的代码顺序或代码顺序,我们很自然的把增强分为几种类型。


  • Before 前置增强
  • 增强方法先于目标方法执行。在核心功能之前执行的额外功能

  • After 后置增强
  • 增强方法在目标方法执行后执行,无论目标方法运行期间是否出现异常。在核心功能之后执行的额外功能

  • AfterReturning 返回增强
  • 在目标方法执行后,返回执行结果时执行,当目标函数抛出异常时它不会执行

  • AfterThrowing 异常增强
  • 当目标函数抛出异常时,异常增强会执行

  • Around 环绕增强
  • 环绕增强中可以实现上述四种增强

  • DeclareParents 引入增强
  • 这种增强与上面不同,它不是在某个方法前执行代码,而是为被增强的类,增加一个接口,并提供这个接口的实现方法,我们可以理解成它为目标类增加了方法


3. 常见用法

使用AOP最常见的用法,就是定义个@Aspect注解类,在该类里面写上大量的切入点和增强方法,当然也可以以一种组合的方式,直接把pointcut的定义写在增强方法注解上。

@Aspect
@Component
public class MyAspect {
    /**
     * 以注解方式找到切入点
     * 此处是找到被 MyAnnotation 注解标记的 methodA()方法,把这些方法作为切入点
     */
    @Pointcut("@annotation(com.zhanfu.springtest.MyAnnotation)")
    public void someService1(){}
    /**
     * 在指定的范围找切入点
     * 此处是 找到定义在springtest包里的任意方法
     */
    @Pointcut("execution(* com.zhanfu.springtest.*.*(..)) ")
    public void someService2(){}
    /**
     * 增强方法,本增强方法作用于上面定义的切入点someService1(),
     * 增强方式为Around
     */
    @Around("someService1()")
    public Object doSomething (ProceedingJoinPoint joinPoint){
        System.out.println("执行前");
        try {
            Object result = joinPoint.proceed();
            System.out.println("执行后");
            return result;
        } catch (Throwable e) {
            System.out.println("发生异常");
            e.printStackTrace();
        }
        return null;
    }
    /**
     * 增强方法,组合使用,这种方式不需要另外写个pointcut,而是直接把 pointcut 写在@before注解里面
     * 增强方式为Before
     */
    @Before("execution(* com.zhanfu.springtest.*.main(..))")
    public void before(ProceedingJoinPoint joinPoint){
        String name = joinPoint.getSignature().getName();
        System.out.println("前置通知:在" + name + "执行前");
    }
}

4. Spring内置的增强

除了上述供开发者去创建的增强外,Spring其实框架本身也内置了一些增强样例,使得我们仅只用注解就可以享受到一些特定功能的增强,从而大大减少程序员的工作量,比如事务相关的 @Transctional 或者 缓存相关的 @Cacheable


我们在项目中开启相关功能后,使用上述注解,比如 @Transctional,就可以不必重复写获取数据库连接,会话,以及各种设置等代码,Spring会根据配置自动完成这些内容。具体内容,可以看我的另一篇博文 Spring事务畅谈 —— 由浅入深彻底弄懂 @Transactional注解


5. SpringAop 与动态代理

我们都知道,SpringAop 是基于动态代理实现的。但是两者并不是等号关系,Aop是一种技术手段和思想,它的实现会用到很多其他技术,其中就包含了动态代理技术。但动态代理并不是全部,事实上,生成动态代理仅需要几行代码,但在这之前的准备工作却是巨大的,这部分工作都由Spring完成了。这让我们可以使用简单的注解或配置,实现复杂场景下的动态代理


而动态代理的创建,在Spring里用到了两种:


  1. jdk动态代理:
  2. 原对象需要有实现接口,生成的代理,只包含接口里定义的方法

  3. CGlib动态代理:
  4. 生成原对象的继承对象,并重写同名方法

6. Aop功能的易错点(重点)

我们在使用Aop的时候,实际是在利用动态代理和原对象打交道,中间多了一层,导致存在大量失效场景,这是我们在使用SpringAop时需要格外注意的:


  • 接口和修饰符问题
  • 采用Cglib时,final 修饰的、static 修饰的 、private 修饰的方法无法代理,因为这些方法无法继承。同样的采用jdk动态代理时,需要原对象有接口,且只能代理接口里定义的方法

  • 同一个类中的方法调用
  • 同一个类里的两个方法,它们之间相互调用时不会触发增强内容,如下图,方法B的增强并不会被执行到,因为同一个对象里,方法A调用方法B,用的是this.B()。即执行原对象的方法A时,发现要用方法B,就会直接调用本对象的方法B,而不会再绕回去调用代理对象的方法B

51beeab6f1914d2694fee87a9e3490b5.png


复杂项目场景


  1. 一些复杂项目可能会有多个BeanFactory,即Bean容器,这些容器之间的Bean不通用,可能会导致没有产生代理
  2. 过早创建的Bean,有一些Bean由于不合理的设定或者循环引用,导致过早(在BeanPostProcessor生效前)被创建并放入容器,那么这些Bean就没有创建代理,需找到对应位置,使用@lazy 延缓其Bean创建时间

三、手写动态代理

1. 创建动态代理

我们如果想了解AOP,不如自己动手做一遍,这里我们只看最核心的部分,有兴趣的可以复制我的代码直接测试,我们这里利用的是 jdk动态代理:


首先,我们定义一个接口:吃

public interface EatInterface {
    void eat();
}

然后为接口写个实现类:人

public class Human implements EatInterface {
    @Override
    public void eat() {
        System.out.println("我是战斧 , 我吃东西");
    }
    public void cook() {
        System.out.println("我是战斧 , 我做饭");
    }
}

然后再写个增强类,这个类必须实现 InvocationHandler

public class WashHandler implements InvocationHandler {
    private Object target;
    public WashHandler(Object target) {
        this.target = target;
    }
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        System.out.println("做任何事前都要洗手");
        Object retVal = method.invoke(target, args);
        System.out.println("做任何事后都要洗手");
        return retVal;
    }
}

最后来个类,执行其main方法

public class MainClass {
    public static void main(String[] args) {
      // 设置系统属性,使得生成的动态代理类可以被保存下来
        System.getProperties().setProperty("sun.misc.ProxyGenerator.saveGeneratedFiles", "true");
        Human human = new Human();
        WashHandler handler = new WashHandler(human);
        EatInterface proxyRes = (EatInterface) Proxy.newProxyInstance(human.getClass().getClassLoader(),
                human.getClass().getInterfaces(), handler);
        proxyRes.eat();
    }
}

执行的结果是符合预期的

bb71ef282e8c418c94e28110ccf88138.png

就这样,我们生成了个动态代理,并且完成了一次增强。


2. 查看代理类

我们或许好奇是如何实现的,其实这是使用的JDK的动态代理,那我们就可以看看这个动态代理类的内容是什么,我们先前加的代码


System.getProperties().setProperty(“sun.misc.ProxyGenerator.saveGeneratedFiles”, “true”);


就起了作用,我们可以在项目源码根目录找到生成的 $Proxy0

3273f62f519342769345490bb4675c07.png


我们打开该类,查看其代码

package com.sun.proxy;
import com.zhanfu.service.EatInterface;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.lang.reflect.UndeclaredThrowableException;
public final class $Proxy0 extends Proxy implements EatInterface {
    private static Method m1;
    private static Method m3;
    private static Method m2;
    private static Method m0;
    public $Proxy0(InvocationHandler var1) throws  {
        super(var1);
    }
    public final boolean equals(Object var1) throws  {
        try {
            return (Boolean)super.h.invoke(this, m1, new Object[]{var1});
        } catch (RuntimeException | Error var3) {
            throw var3;
        } catch (Throwable var4) {
            throw new UndeclaredThrowableException(var4);
        }
    }
    public final void eat() throws  {
        try {
            super.h.invoke(this, m3, (Object[])null);
        } catch (RuntimeException | Error var2) {
            throw var2;
        } catch (Throwable var3) {
            throw new UndeclaredThrowableException(var3);
        }
    }
    public final String toString() throws  {
        try {
            return (String)super.h.invoke(this, m2, (Object[])null);
        } catch (RuntimeException | Error var2) {
            throw var2;
        } catch (Throwable var3) {
            throw new UndeclaredThrowableException(var3);
        }
    }
    public final int hashCode() throws  {
        try {
            return (Integer)super.h.invoke(this, m0, (Object[])null);
        } catch (RuntimeException | Error var2) {
            throw var2;
        } catch (Throwable var3) {
            throw new UndeclaredThrowableException(var3);
        }
    }
    static {
        try {
            m1 = Class.forName("java.lang.Object").getMethod("equals", Class.forName("java.lang.Object"));
            m3 = Class.forName("com.zhanfu.service.EatInterface").getMethod("eat");
            m2 = Class.forName("java.lang.Object").getMethod("toString");
            m0 = Class.forName("java.lang.Object").getMethod("hashCode");
        } catch (NoSuchMethodException var2) {
            throw new NoSuchMethodError(var2.getMessage());
        } catch (ClassNotFoundException var3) {
            throw new NoClassDefFoundError(var3.getMessage());
        }
    }
}

不难发现几个有意思的点:


创建的动态代理类继承了 Proxy 类,同时实现了我们传给它的接口 EatInterface

代理类包含接口的方法 ,且补全了几个Object的方法,即hashCode()、toString()、equals(),但却没有实现类自己的方法cook()

所有的方法都采用 super.h.invoke 进行调用,而这里的h,自然就是我们传进去的WashHandler

3. 分析调用链路

其实从上面两个小节可以感受到,动态代理的创建并不复杂。


我们首先的创建个 ”调用处理器“,它的作用是在调用我们给定的 ”某个方法“ 时,能够执行点别的代码。这也就是我们说的”增强“模块。所以它的方法 invoke 入参包含了对象的方法以及方法的入参。

public Object invoke(Object proxy, Method method, Object[] args)
        throws Throwable;

第二步才是动态代理,以JDK代理为例,首先肯定是动态创建个类,然后将这个类实例化成对象(内部有调用处理器)。这个代理对象的方法和原对象一致,只是它的方法体非常简单,就是调用我们的 ”调用处理器“。因此不难发现,此时真正用到的还是我们的 ”调用处理器“,然后再由 ”调用处理器“ (内部有原对象)去调用原对象的方法

a1cf164bf2a54e12bf8275ad65c2065d.png



4.手写代理的不足

从上面的例子里,我们已经利用jdk自带方法创建了个动态代理,但是仔细想想,要在生产种使用这种方式实现Aop,它还存在以下问题,如


  • 需要手动创建所有对象,每次使用都得重新创建,非常麻烦
  • 当我需要对很多对象做同一种增强时,难以实现,因为要找到这些对象,还要将其存入调用处理器
  • 当某一个对象需要被多种调用处理器增强时,难以实现,因为入参只允许传一个调用处理器

解决方式也比较明了:


  1. 设计个容器,创建的对象扔在里面,随取随用,包括动态代理对象
  2. 支持以各种表达式的方式进行配置,灵活的为每一个调用处理器找到所有需增强的对象
  3. 当多个调用处理器对同一个对象做增强时,需要有排序,然后按照排序完成嵌套增强

当然,其实看这解决方式,我们会感到非常熟悉,是的,Spring已经包含了这些功能。这才使得,我们可以通过简单配置切面,就能完成对应的增强功能了。


相关实践学习
日志服务之使用Nginx模式采集日志
本文介绍如何通过日志服务控制台创建Nginx模式的Logtail配置快速采集Nginx日志并进行多维度分析。
目录
相关文章
|
24天前
Micronaut AOP与代理机制:实现应用功能增强,无需侵入式编程的秘诀
AOP(面向切面编程)能够帮助我们在不修改现有代码的前提下,为应用程序添加新的功能或行为。Micronaut框架中的AOP模块通过动态代理机制实现了这一目标。AOP将横切关注点(如日志记录、事务管理等)从业务逻辑中分离出来,提高模块化程度。在Micronaut中,带有特定注解的类会在启动时生成代理对象,在运行时拦截方法调用并执行额外逻辑。例如,可以通过创建切面类并在目标类上添加注解来记录方法调用信息,从而在不侵入原有代码的情况下增强应用功能,提高代码的可维护性和可扩展性。
46 1
|
2月前
|
安全 前端开发 Java
随着企业应用复杂度提升,Java Spring框架以其强大与灵活特性简化开发流程,成为构建高效、可维护应用的理想选择
随着企业应用复杂度提升,Java Spring框架以其强大与灵活特性简化开发流程,成为构建高效、可维护应用的理想选择。依赖注入使对象管理交由Spring容器处理,实现低耦合高内聚;AOP则分离横切关注点如事务管理,增强代码模块化。Spring还提供MVC、Data、Security等模块满足多样需求,并通过Spring Boot简化配置与部署,加速微服务架构构建。掌握这些核心概念与工具,开发者能更从容应对挑战,打造卓越应用。
36 1
|
1月前
Micronaut AOP与代理机制:实现应用功能增强,无需侵入式编程的秘诀
【9月更文挑战第9天】AOP(面向切面编程)通过分离横切关注点提高模块化程度,如日志记录、事务管理等。Micronaut AOP基于动态代理机制,在应用启动时为带有特定注解的类生成代理对象,实现在运行时拦截方法调用并执行额外逻辑。通过简单示例展示了如何在不修改 `CalculatorService` 类的情况下记录 `add` 方法的参数和结果,仅需添加 `@Loggable` 注解即可。这不仅提高了代码的可维护性和可扩展性,还降低了引入新错误的风险。
38 13
|
2月前
|
XML Java 数据格式
Spring5入门到实战------11、使用XML方式实现AOP切面编程。具体代码+讲解
这篇文章是Spring5框架的AOP切面编程教程,通过XML配置方式,详细讲解了如何创建被增强类和增强类,如何在Spring配置文件中定义切入点和切面,以及如何将增强逻辑应用到具体方法上。文章通过具体的代码示例和测试结果,展示了使用XML配置实现AOP的过程,并强调了虽然注解开发更为便捷,但掌握XML配置也是非常重要的。
Spring5入门到实战------11、使用XML方式实现AOP切面编程。具体代码+讲解
|
1月前
|
安全 Java 开发者
强大!Spring Cloud Gateway新特性及高级开发技巧
在微服务架构日益盛行的今天,网关作为微服务架构中的关键组件,承担着路由、安全、监控、限流等多重职责。Spring Cloud Gateway作为新一代的微服务网关,凭借其基于Spring Framework 5、Project Reactor和Spring Boot 2.0的强大技术栈,正逐步成为业界的主流选择。本文将深入探讨Spring Cloud Gateway的新特性及高级开发技巧,助力开发者更好地掌握这一强大的网关工具。
132 6
|
2月前
|
存储 Java 开发者
使用Spring Boot 3.3全新特性CDS,启动速度狂飙100%!
【8月更文挑战第30天】在快速迭代的软件开发周期中,应用的启动速度是开发者不可忽视的一个重要指标。它不仅影响着开发效率,还直接关系到用户体验。随着Spring Boot 3.3的发布,其中引入的Class Data Sharing(CDS)技术为应用的启动速度带来了革命性的提升。本文将围绕这一全新特性,深入探讨其原理、使用方法以及带来的实际效益,为开发者们带来一场技术盛宴。
73 2
|
2月前
|
XML Java 应用服务中间件
深入探索Spring Boot框架的核心特性
Spring Boot 是一款基于Spring框架的开源框架,旨在简化新Spring应用的初始搭建以及开发过程。该框架使用了特定的方式(默认配置)来简化整个构建过程。
46 11
|
2月前
|
Java Spring XML
掌握面向切面编程的秘密武器:Spring AOP 让你的代码优雅转身,横切关注点再也不是难题!
【8月更文挑战第31天】面向切面编程(AOP)通过切面封装横切关注点,如日志记录、事务管理等,使业务逻辑更清晰。Spring AOP提供强大工具,无需在业务代码中硬编码这些功能。本文将深入探讨Spring AOP的概念、工作原理及实际应用,展示如何通过基于注解的配置创建切面,优化代码结构并提高可维护性。通过示例说明如何定义切面类、通知方法及其应用时机,实现方法调用前后的日志记录,展示AOP在分离关注点和添加新功能方面的优势。
44 0
|
2月前
|
Java Spring 容器
彻底改变你的编程人生!揭秘 Spring 框架依赖注入的神奇魔力,让你的代码瞬间焕然一新!
【8月更文挑战第31天】本文介绍 Spring 框架中的依赖注入(DI),一种降低代码耦合度的设计模式。通过 Spring 的 DI 容器,开发者可专注业务逻辑而非依赖管理。文中详细解释了 DI 的基本概念及其实现方式,如构造器注入、字段注入与 setter 方法注入,并提供示例说明如何在实际项目中应用这些技术。通过 Spring 的 @Configuration 和 @Bean 注解,可轻松定义与管理应用中的组件及其依赖关系,实现更简洁、易维护的代码结构。
38 0
|
2月前
|
Java Spring 供应链
Spring 框架事件发布与监听机制,如魔法风暴席卷软件世界,开启奇幻编程之旅!
【8月更文挑战第31天】《Spring框架中的事件发布与监听机制》介绍了Spring中如何利用事件发布与监听机制实现组件间的高效协作。这一机制像城市中的广播系统,事件发布者发送消息,监听器接收并响应。通过简单的示例代码,文章详细讲解了如何定义事件类、创建事件发布者与监听器,并确保组件间松散耦合,提升系统的可维护性和扩展性。掌握这一机制,如同拥有一把开启高效软件开发大门的钥匙。
39 0

热门文章

最新文章