Spring的AOP原理

AOP是什么?

转自:https://www.tianmaying.com/tutorial/spring-aop

软件工程有一个基本原则叫做“关注点分离”(Concern Separation),通俗的理解就是不同的问题交给不同的部分去解决,每部分专注于解决自己的问题。这年头互联网也天天强调要专注嘛!

这其实也是一种“分治”或者“分类”的思想,人解决复杂问题的能力是有限的,所以为了控制复杂性,我们解决问题时通常都要对问题进行拆解,拆解的同时建立各部分之间的关系,各个击破之后整个问题也迎刃而解了。人类的思考,复杂系统的设计,计算机的算法,都能印证这一思想。额,扯远了,这跟AOP有神马关系?

面向切面编程(Aspect Oriented Programming,AOP)其实就是一种关注点分离的技术,在软件工程领域一度是非常火的研究领域。我们软件开发时经常提一个词叫做“业务逻辑”或者“业务功能”,我们的代码主要就是实现某种特定的业务逻辑。但是我们往往不能专注于业务逻辑,比如我们写业务逻辑代码的同时,还要写事务管理、缓存、日志等等通用化的功能,而且每个业务功能都要和这些业务功能混在一起,痛苦!所以,为了将业务功能的关注点和通用化功能的关注点分离开来,就出现了AOP技术。这些通用化功能的代码实现,对应的就是我们说的切面(Aspect)。

业务功能代码和切面代码分开之后,责任明确,开发者就能各自专注解决问题了,代码可以优雅的组织了,设计更加高内聚低耦合了(终极目标啊!)。但是请注意,代码分开的同时,我们如何保证功能的完整性呢? 你的业务功能依然需要有事务和日志等特性,即切面最终需要合并(专业术语叫做织入Weave)到业务功能中。怎么做到呢? 这里就涉及AOP的底层技术啦,有三种方式:

  1. 编译时织入:在代码编译时,把切面代码融合进来,生成完整功能的Java字节码,这就需要特殊的Java编译器了,AspectJ属于这一类
  2. 类加载时织入:在Java字节码加载时,把切面的字节码融合进来,这就需要特殊的类加载器,AspectJ和AspectWerkz实现了类加载时织入
  3. 运行时织入:在运行时,通过动态代理的方式,调用切面代码增强业务功能,Spring采用的正是这种方式。动态代理会有性能上的开销,但是好处就是不需要神马特殊的编译器和类加载器啦,按照写普通Java程序的方式来就行了!

一个场景

接下来上例子!David对土豪老板定机票的例子比较满意,所以决定继续沿用这个例子。

Boss在订机票时,我们希望能够记录订机票这个操作所消耗的时间,同时记录日志(这里我们简单的在控制台打印预定成功的信息)。

我们来看普通青年的做法吧:

 

package com.tianmaying.aopdemo;

public class Boss {

    private BookingService bookingService;

    public Boss() {
        this.bookingService = new QunarBookingService();
    }

    //...

    public void goSomewhere() {
        long start = System.currentTimeMillis();

                //订机票
        boolean status = bookingService.bookFlight();

        //查看耗时
        long duration = System.currentTimeMillis() - start;
        System.out.println(String.format("time for booking flight is %d seconds", duration));

        //记录日志
        if (status) {
            System.out.println("booking flight succeeded!");
        } else {
            System.out.println("booking flight failed!");
        }
    }
}

我们看到,在订机票的同时,还要处理查看耗时和记录日志,关注的事情太多了,头大啊。而且项目大了之后,除了订机票之外,很多业务功能都要写类似的代码。让AOP来拯救我们吧!

使用AOP的场景

相比在IoC例子中的代码,我们让BookingServicebookFlight()方法返回一个boolean值,表示是否预定成功。这样我们可以演示如何获取被切方法的返回值。

通过AOP我们怎么做呢,David今天送出独门秘籍,告诉你通过3W方法(What-Where-When)来理解AOP。

  • What:What当然指的时切面啦!首先我们将记录消耗时间和记录日志这两个功能的代码分离出来,我们可以做成两个切面,命名为TimeRecordingAspectLogAspect
  • Where:切面的织入发生在哪呢?切面针对的目标对象Target)是SmartBoss(区别于Boss)!这里还有有一个很关键的概念叫做切入点Pointcut),在这个场景中就是指在SmartBoss调用什么方法的时候的时候应用切面。显然,我们希望增强的是bookFlight()方法,即在bookFlight方法调用的地方,我们加入时间记录和日志。
  • When: 什么时候织入呢?这涉及到织入的时机问题,我们可以在bookFlight()执行前织入,执行后织入,或者执行前后同时切入。When的概念用专业术语来说叫做通知Advice)。

了解了3W之后,来看看代码吧,先上LogAspect:

插入一段,POM文件不要忘记了引入Spring AOP相关的依赖:

 

    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-context</artifactId>
        <version>4.2.0.RELEASE</version>
    </dependency>
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-aop</artifactId>
        <version>4.2.0.RELEASE</version>
    </dependency>
    <dependency>
        <groupId>org.aspectj</groupId>
        <artifactId>aspectjweaver</artifactId>
        <version>1.8.5</version>
    </dependency>  
    <dependency>
        <groupId>org.aspectj</groupId>
        <artifactId>aspectjrt</artifactId>
        <version>1.8.5</version>
    </dependency>

LogAspect

 

package com.tianmaying.aopdemo.aspect;

import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;

@Aspect //1
@Component
public class LogAspect {

    @Pointcut("execution(* com.tianmaying.aopdemo..*.bookFlight(..))") //2
    private void logPointCut() {
    }

    @AfterReturning(pointcut = "logPointCut()", returning = "retVal") //3
    public void logBookingStatus(boolean retVal) {  //4
        if (retVal) {
            System.out.println("booking flight succeeded!");
        } else {
            System.out.println("booking flight failed!");
        }
    }
}

我们看这段代码:

1通过一个@Apsect标注,表示LogAspect是一个切面,解决了What问题。2通过定义一个标注了@Pointcut的方法,定义了Where的问题,"execution(* com.tianmaying.aopdemo..*.bookFlight(..))"表示在com.tianmaying.aopdemo包或者子包种调用名称为bookFlight的地方就是切入点!定义Pioncut的语法这里不详解了,David这里要告诉你的时它的作用:解决Where的问题!3通过一个@AfterReturning标注表示在bookFlight()调用之后将切面织入,这是一个AfterReturning类型的Advice,注意这里可以通过returning属性获取bookFlight()的返回值。4这里定义了实现切面功能的代码,经过这么一番闪转腾挪,最后写日志的代码跑到这里来了!

再来看TimeRecordingAspect:

TimeRecordingAspect:

 

package com.tianmaying.aopdemo.aspect;

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;

@Aspect
@Component
public class TimeRecordingAspect {

    @Pointcut("execution(* com.tianmaying.aopdemo..*.bookFlight(..))")
    private void timeRecordingPointCut() {
    }

    @Around("timeRecordingPointCut()") //1
    public Object recordTime(ProceedingJoinPoint pjp) throws Throwable {  //2

        long start = System.currentTimeMillis();
        Object retVal = pjp.proceed(); // 3

        long duration = System.currentTimeMillis() - start;
        System.out.println(String.format(
                "time for booking flight is %d seconds", duration));

        return retVal;
    }
}
  • LogAspect不同,因为要计算bookFlight()的耗时,我们必须在调用前后到切入代码,才能算出来这之间的时间差。因此,在1处,我们定义的是一个Around类型的Advice。
  • 2处是实现AroundAdvice的方法,其方法的参数和返回值是固定写法。
  • 3处也是固定写法,表示对目标方法(即bookFlight())的调用,注意不要漏了,漏掉的话原方法就不会被调用了,通常情况下肯定不是你想要的结果!

回头再看SmartBoss的代码,比Boss简单多了,goSomewhere()方法中只剩下一条语句,酷的掉渣啊,只关注订机票,其他的事情都F**k Off吧!

 

package com.tianmaying.aopdemo;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

@Component
public class SmartBoss {
    private BookingService bookingService;

        //...

    public void goSomewhere() {
        bookingService.bookFlight();
    }
}

当然,要让代码Run起来,还需要在App类中加上@EnableAspectJAutoProxy标注,这样Spring启动时就去去扫描AOP相关的标注,在创建对象时帮我们去执行织入过程!

回到定义

例子讲完!现在我们再来逐一看看AOP中的那些名词定义,这个时候理解这些概念,你应该不会觉得冷冰冰了,应该要有一种"Ya!"的感觉啦!

  • 切面(Aspect):指的就是通用功能的代码实现,比如我们上面演示的时间记录切面,日志切面,它们都是普通的Java类:TimeRecordingAspectLogAspect

  • 目标对象(Target):要被织入切面的对象,例子中的CtripBookingService,有了AOP,它们可以专注于核心业务逻辑代码了!

  • 切入点(Pointcut):定义通知应该切入到什么地方,Spring支持的切入点就是方法调用,切入点的定义可以使用正则表达式,用以描述什么类型的方法调用。@Pointcut就是用来定义切入点的。

  • 通知(Advice):切面是一个类,而通知就是类里的方法以及这个方法如何织入到目标方法的方式(用@AfterReturning@Around标注的方法)。我们的例子中只展示了两类通知,根据织入到目标方法方式的不同,一共可以分为5种:

    • 前置通知(Before)
    • 后置通知(AfterReturning)
    • 异常通知(AfterThrowing)
    • 最终通知(After)
    • 环绕通知(Around)
  • 织入(Weaving):AOP实现的过程,即将切面应用到目标对象,从而创建一个新的代理对象的过程,对于Spring来说,就是初始化Context中的对象时,完成织入操作。

现在你应该理解Spring中AOP的关键知识和核心原理了,剩下的就是在David给你的3W框架下,去学习每一部分的知识了,比如不同类型通知的写法,PointCut的各种类型的定义方法,切面和目标对象之间参数传递,等等。当然,最关键的还是赶紧实践中用起来!

<think>好的,用户想了解Spring AOP原理,特别是代理模式、动态代理和静态代理。首先,我需要回顾一下代理模式的基本概念,然后区分静态代理和动态代理的不同,再联系到Spring AOP是如何应用这些技术的。 首先,代理模式的定义。根据引用[1],代理模式允许在不修改被代理对象的情况下,通过代理类进行功能增强。这里的关键是代理类分为静态和动态两种。静态代理需要手动创建代理类,而动态代理在运行时生成。用户可能想知道这两种代理在Spring AOP中的应用场景。 接下来,静态代理的细节。引用[1]提到静态代理的类在运行前已经存在,通常由程序员手动编写或工具生成。例如,假设有一个业务接口Subject和实现类RealSubject,静态代理类Proxy会实现Subject并持有RealSubject的引用,在方法调用前后添加逻辑。这种情况的缺点是每个被代理类都需要一个对应的代理类,导致代码冗余。 然后是动态代理,Spring AOP主要使用JDK动态代理和CGLIB。引用[2]指出JDK代理通过Proxy类生成代理对象,而CGLIB通过Enhancer创建。JDK动态代理要求目标类实现接口,利用InvocationHandler的invoke方法进行增强。CGLIB则通过继承目标类,重写方法来实现代理,适用于没有接口的情况。需要解释这两种方式的区别以及Spring如何选择使用哪一种。 还要结合Spring AOP的源码,如引用[2]提到的AOP源码剖析部分。Spring在运行时决定使用哪种代理方式,如果目标类实现了接口,默认使用JDK代理,否则使用CGLIB。这涉及到AOP的配置和底层机制。 用户可能还关心AOP的应用场景,比如事务管理、日志记录等,这些通过代理模式实现,无需修改原有代码。需要举例说明,比如在方法执行前后加入事务控制,动态代理如何拦截方法调用并添加额外操作。 另外,需要确保回答结构清晰,分点说明原理、代理类型、实现方式以及Spring的选择策略。同时,生成的相关问题要围绕用户可能进一步询问的点,比如性能比较、配置方法或实际应用例子。 最后,检查引用是否正确应用,如引用[1]关于代理模式的分类,引用[2]关于Spring AOP的具体实现,引用[3]关于代理模式的角色。确保每个技术点都有对应的引用支持,增加回答的可信度。</think>### Spring AOP原理与代理模式解析 #### 一、代理模式的核心作用 代理模式通过代理对象控制对真实对象的访问,在**不修改原始代码**的前提下实现功能增强。其核心角色包括: - **Subject**:定义业务接口(可以是接口或抽象类)[^3] - **RealSubject**:具体业务实现类(被代理对象) - **Proxy**:代理类,负责增强和控制对RealSubject的访问 #### 二、静态代理的实现方式 静态代理需**手动编写代理类**,代理关系在编译时确定。例如: ```java // 业务接口 interface UserService { void save(); } // 真实对象 class UserServiceImpl implements UserService { public void save() { /* 业务逻辑 */ } } // 静态代理类 class UserServiceProxy implements UserService { private UserService target; public UserServiceProxy(UserService target) { this.target = target; } public void save() { System.out.println("前置增强"); target.save(); // 调用真实对象方法 System.out.println("后置增强"); } } ``` **缺点**:每个业务类需对应一个代理类,代码冗余度高[^1]。 #### 三、动态代理的实现机制 Spring AOP主要采用两种动态代理技术: 1. **JDK动态代理**(基于接口) - 要求目标类必须实现至少一个接口 - 通过`java.lang.reflect.Proxy`生成代理对象 - 核心类`InvocationHandler`实现方法拦截: ```java class JdkProxyHandler implements InvocationHandler { private Object target; public Object getProxy(Object target) { this.target = target; return Proxy.newProxyInstance( target.getClass().getClassLoader(), target.getClass().getInterfaces(), this); } public Object invoke(Object proxy, Method method, Object[] args) { System.out.println("前置增强"); Object result = method.invoke(target, args); System.out.println("后置增强"); return result; } } ``` 2. **CGLIB代理**(基于继承) - 通过生成目标类的子类实现代理 - 使用`MethodInterceptor`接口: ```java class CglibInterceptor implements MethodInterceptor { public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) { System.out.println("前置增强"); Object result = proxy.invokeSuper(obj, args); System.out.println("后置增强"); return result; } } // 通过Enhancer创建代理 Enhancer enhancer = new Enhancer(); enhancer.setSuperclass(TargetClass.class); enhancer.setCallback(new CglibInterceptor()); TargetClass proxy = (TargetClass)enhancer.create(); ``` - **优势**:可代理无接口的类[^2] #### 四、Spring AOP的代理选择策略 | 条件 | 使用的代理方式 | |--------------------------|----------------| | 目标类实现了至少一个接口 | JDK动态代理 | | 目标类未实现任何接口 | CGLIB代理 | **底层实现**: - 通过`DefaultAopProxyFactory`类判断代理方式 - Spring Boot 2.x后默认强制使用CGLIB(可通过`spring.aop.proxy-target-class=true`配置) #### 五、AOP核心流程 1. 解析切面定义(@Aspect) 2. 匹配连接点(Pointcut) 3. 生成代理对象 4. 编织增强逻辑(Advice) 5. 通过责任链模式执行拦截器链 #### 六、性能对比 - **JDK动态代理**:生成速度快,执行效率稍低 - **CGLIB**:生成速度慢,执行效率高(现代JVM差距已不明显)
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值