Spring AOP

AOP

AOP(Aspect Oriented Programming,面向切面编程) 最早是 AOP 联盟的组织提出的,指定的一套规范,spring 将 AOP 的思想引入框架之中,通过预编译方式和运行期间动态代理实现程序的统一维护的一种技术。

OOP 和 AOP 的区别:

  • 面向对象编程(OOP),是针对业务处理过程的实体及其属性和行为进行抽象封装,以获得更加清晰高效的逻辑单元划分。

  • 面向切面编程(AOP),则是针对业务处理过程中的切面进行提取,它所面对的是处理过程的某个步骤或阶段,以获得逻辑过程的中各部分之间低耦合的隔离效果。这两种设计思想在目标上有着本质的差异。

概念

  • 连接点(Jointpoint):表示需要在程序中插入横切关注点的扩展点。

    连接点可能是类初始化、方法执行、方法调用、字段调用或处理异常等等,Spring 只支持方法执行连接点。在 AOP 中表示为在哪里干。

  • 切入点(Pointcut): 通知功能被应用的范围。

    Spring 支持 perl5 正则表达式和 AspectJ 切入点模式,Spring 默认使用 AspectJ 语法。在 AOP 中表示为在哪里干的集合。

  • 通知(Advice):在连接点上执行的行为,通知提供了在 AOP 中,需要在切入点所选择的连接点处进行扩展现有行为的手段。

    通知的方式有五种:

    • 前置通知(Before advice)@Before,在某连接点之前执行的通知,但这个通知不能阻止连接点之前的执行流程(除非它抛出一个异常)。

    • 后置通知(After returning advice)@AfterReturning,在某连接点正常完成后执行的通知:例如,一个方法没有抛出任何异常,正常返回。

    • 异常通知(After throwing advice)@AfterThrowing,在方法抛出异常退出时执行的通知。

    • 最终通知(After (finally) advice)@After,当某连接点退出的时候执行的通知(不论是正常返回还是异常退出)。

    • 环绕通知(Around Advice)@Around,包围一个连接点的通知,如方法调用。

      这是最强大的一种通知类型。环绕通知可以在方法调用前后完成自定义的行为。它也会选择是否继续执行连接点或直接返回它自己的返回值或抛出异常来结束执行。

    在 Spring 中通过代理模式实现 AOP,并通过拦截器模式以环绕连接点的拦截器链织入通知。在 AOP 中表示为干什么;

  • 方面/切面(Aspect):横切关注点的模块化,比如上边提到的日志组件。可以认为是通知、引入和切入点的组合;

    在 Spring 中可以使用 Schema 和 @AspectJ 方式进行组织实现。在AOP中表示为在哪干和干什么集合。

  • 引入(inter-type declaration):也称为内部类型声明,为已有的类添加额外新的字段或方法。

    Spring 允许引入新的接口(必须对应一个实现)到所有被代理对象(目标对象)。 在 AOP 中表示为干什么(引入什么);

  • 目标对象(Target Object):需要被织入横切关注点的对象,即该对象是切入点选择的对象,需要被通知的对象,从而也可称为被通知对象。

    由于Spring AOP 通过代理模式实现,从而这个对象永远是被代理对象,在AOP中表示为对谁干;

  • 织入(Weaving):把切面连接到其它的应用程序类型或者对象上,并创建一个被通知的对象。

    这些可以在编译时(例如使用AspectJ编译器),类加载时和运行时完成。Spring和其他纯Java AOP框架一样,在运行时完成织入。在AOP中表示为怎么实现的;

  • AOP代理(AOP Proxy):AOP框架使用代理模式创建的对象,从而实现在连接点处插入通知(即应用切面),就是通过代理来对目标对象应用切面。

    在 Spring 中,AOP 代理可以用 JDK 动态代理或 CGLIB 代理实现,而通过拦截器模型应用切面。在 AOP 中表示为怎么实现的一种典型方式。

它们的关系,如下图所示:

image

Spring AOP 和 AspectJ

AspectJ

AspectJ:是一个面向切面的框架,它扩展了 Java 语言。AspectJ 定义了 AOP 语法,它有一个专门的编译器用来生成遵守 Java 字节编码规范的 Class 文件。

AspectJ 是最首创的 AOP 技术,用来提供全面的 AOP 方案。

织入方式

AspectJ 使用了三种不同类型的织入方式:

  • 编译期织入(Compile-time weaving):编译器将切面和应用的源代码编译在一个字节码文件中。

  • 编译后织入(Post-compile weaving):也称为二进制织入,将已有的字节码文件与切面编制在一起。

  • 加载时织入(Load-time weaving):与编译后织入一样,只是织入时间会推迟到类加载到jvm时。

Spring AOP

Spring AOP 默认使用标准 JDK 动态代理作为 AOP 代理。这使得任何接口(或一组接口)都可以被代理。Spring AOP 还可以使用 CGLIB 代理,这对于代理类而不是接口是必要的。默认情况下,如果业务对象未实现接口,则使用 CGLIB。

虽然,Spring AOP 使用了 AspectJ 5 的注释,不过,Spring AOP 运行时仍然是纯 Spring AOP,并且不依赖于 AspectJ 编译器或编织器。

织入方式

Spring AOP 使用运行时织入(runtime weaving)。在运行时织入,是使用目标对象的代理对象织入的。

主要使用了两种技术:JDK 动态代理CGLIB 动态代理

  • 对于接口,使用的是 JDK 动态代理;

  • 对于继承,使用的是 CGLIB 动态代理。

image

对比

因为织入方式的区别,两者所支持的 Joinpoint 也是不同的。像 final 的方法和静态方法,无法通过动态代理来改变,所以 Spring AOP 无法支持。但 AspectJ 是直接在运行前织入实际的代码,所以功能会强大很多。

Joinpoint Spring AOP Supported AspectJ Supported
Method Call N Y
Method Execution Y Y
Constructor Call N Y
Constructor Execution N Y
Static initializer execution N Y
Object initialization N Y
Field reference N Y
Field assignment N Y
Handler execution N Y
Advice execution N Y

小结

Spring AOP是用纯 Java 实现的,不需要特殊的编译过程。Spring AOP 不需要控制类加载器层次结构,因此适合在 servlet 容器或应用程序服务器中使用。编译织入会比较运行时织入快很多,Spring AOP 是使用代理模式在运行时才创建对应的代理类,所以,效率没有 AspectJ 高。

Spring AOP 处理 AOP 的方法与大多数其他 AOP 框架不同。目的不是提供最完整的 AOP 实现。相反,其目的是提供 AOP 实现和 Spring IoC 之间的紧密集成,以帮助解决企业应用程序中的常见问题。

应用

Spring 中可以通过 XML 配置 AOP,也可以通过注解的方式配置 AOP,可以根据自己的喜好配置,这里,我们主要介绍通过注解的方式配置 AOP。

基于注解配置 AOP

@AspectJ 指的是一种将切面声明为带有注释的常规 Java 类的风格。@AspectJ 样式是由 AspectJ 项目作为 AspectJ 5 版本的一部分引入的。Spring 注解与 AspectJ 5 相同的注解,使用 AspectJ 提供的库进行切入点解析和匹配。不过,AOP 运行时仍然是纯 Spring AOP,并且不依赖于 AspectJ 编译器或编织器。

启用 @AspectJ 支持

import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.EnableAspectJAutoProxy;

@Configuration
@EnableAspectJAutoProxy
public class AppConfig {
}

这一步,也可以通过 XML 的配置启用 @AspectJ 支持,只需要在 XML 中添加如下内容即可:

<aop:aspectj-autoproxy/>

声明一个切面

任何带有 @Aspect 注解的切面类的 bean 都会被 Spring 自动检测到,并用于配置 Spring AOP。

注意,切面类必须被作为一个 Bean 被自动注入到 IoC 容器中,因此,切面类必须带有 @Bean 或者 @Component 这种能被 Spring 自动检测到并对其自动注入的注解。

这里,我们声明了一个切面类 WebLogAspect,源码如下:

import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;

import java.util.Arrays;

@Aspect
@Slf4j
@Component
public class WebLogAspect {

    @Pointcut("execution(public * com.athena.service.*.*.*(..))")
    public void webLog() {
    }

    @Before(value = "webLog()")
    public void doBefore(JoinPoint joinPoint) {
        log.info("@Before: setup user profile.");
    }

    @After(value = "webLog()")
    public void doAfter(JoinPoint joinPoint) {
        log.info("@After: setup user profile.");
    }

    @AfterReturning(value = "webLog()", returning = "ret")
    public void doAfterReturning(Object ret) {
        log.info("@AfterReturning:" + ret.toString());
    }

    @AfterThrowing(pointcut = "webLog()", throwing = "ex")
    public void doAfterThrowing(IllegalArgumentException ex){
        log.info("@AfterThrowing: there has been an exception: " + ex.toString());
    }

    @Around(value = "webLog()")
    public Object doAround(ProceedingJoinPoint proceedingJoinPoint) throws Throwable{
        log.info("@Around: before around...");
        log.info("parameters : " + Arrays.toString(proceedingJoinPoint.getArgs()));
        log.info("target : " + proceedingJoinPoint.getTarget());
        log.info("signature : " + proceedingJoinPoint.getSignature());
        log.info("this : " + proceedingJoinPoint.getThis());
        log.info("advised : " + proceedingJoinPoint);
        Object result = proceedingJoinPoint.proceed();
        log.info("@Around: after around...");
        return result;
    }
}

切入点支持的指示符

Spring AOP 支持在切入点表达式中使用以下 AspectJ 切入点指示符 (Pointcut Desinator,PCD):

  • execution:用于匹配方法执行的连接点。这是使用 Spring AOP 时使用的主要切入点指示符。

  • within:限制匹配某些类型中的连接点(使用 Spring AOP 时执行在匹配类型中声明的方法)。

  • this:限制对连接点(使用 Spring AOP 时方法的执行)的匹配,其中 bean 引用(Spring AOP 代理)是给定类型的实例。

  • target:限制对连接点(使用 Spring AOP 时方法的执行)的匹配,其中目标对象(被代理的应用程序对象)是给定类型的实例。

  • args:限制对连接点(使用 Spring AOP 时方法的执行)的匹配,其中参数是给定类型的实例。

  • @target:限制匹配连接点(使用 Spring AOP 时的方法执行),其中执行对象的类具有给定类型的注释。

  • @args:限制对连接点(使用 Spring AOP 时方法的执行)的匹配,其中传递的实际参数的运行时类型具有给定类型的注释。

  • @within:限制匹配具有给定注释的类型内的连接点(使用 Spring AOP 时,执行具有给定注释的类型中声明的方法)。

  • @annotation:限制匹配连接点,其中连接点的主题(在 Spring AOP 中运行的方法)具有给定的注释。

由于 Spring 的 AOP 框架基于代理的性质,根据定义,目标对象内的调用不会被拦截。对于JDK代理,只能拦截代理上的公共接口方法调用。使用 CGLIB,代理上的公共和受保护方法调用将被拦截。但是,通过代理进行的常见交互应始终通过公共签名来设计。

声明一个目标对象

这里,我们用一个简单的目标对象来作为示例:

注意,目标对象的全路径限定符,必须与切点声明时,指示的路径匹配。

// com.athena.service.user.UserServiceImpl
@Service
@Slf4j
public class UserServiceImpl implements UserService {
    public User getUserById(String userId) {
        log.info("start to query user");
        return new User(userId);
    }
}

通过 Controllor 触发该 getUserById 方法的调用时,就会产生如下打印:

2023-10-19T10:11:59.067+08:00  INFO 15976 : @Around: before around...
2023-10-19T10:11:59.067+08:00  INFO 15976 : parameters : [2]
2023-10-19T10:11:59.068+08:00  INFO 15976 : target : com.athena.service.user.UserServiceImpl@3bc60e1b
2023-10-19T10:11:59.068+08:00  INFO 15976 : signature : User com.athena.service.user.UserServiceImpl.getUserById(String)
2023-10-19T10:11:59.068+08:00  INFO 15976 : this : com.athena.service.user.UserServiceImpl@3bc60e1b
2023-10-19T10:11:59.068+08:00  INFO 15976 : advised : execution(User com.athena.service.user.UserServiceImpl.getUserById(String))
2023-10-19T10:11:59.068+08:00  INFO 15976 : @Before: setup user profile.
2023-10-19T10:11:59.068+08:00  INFO 15976 : start to query user
2023-10-19T10:11:59.069+08:00  INFO 15976 : @AfterReturning:User(userName=defaultName, userId=2)
2023-10-19T10:11:59.069+08:00  INFO 15976 : @After: setup user profile.
2023-10-19T10:11:59.069+08:00  INFO 15976 : @Around: after around...

可以看出,环绕通知最先被调用,功能也最强大。

通知能力对比

能力 前置通知@Before 后置通知@After 返回通知@AfterReturning 异常通知@AfterThrowing 环绕通知@Around
阻止方法执行
修改方法参数
修改返回值
处理异常
执行自定义逻辑
获取方法参数
获取返回值
获取异常信息
控制多次执行

示例

鉴权场景

基于注解的方式

package com.vela.auth.aspect;

import com.vela.auth.annotation.VelaResource;
import com.vela.auth.service.AuthService;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import java.lang.reflect.Method;

/**
 * 方式一:使用 @Aspect 注解
 */
@Aspect
public class VelaResourceAspect {
    
    @Autowired
    private AuthService authService;
    
    /**
     * 切点:匹配方法上标注了 @VelaResource 的方法
     */
    @Pointcut("@annotation(com.vela.auth.annotation.VelaResource)")
    public void velaResourcePointcut() {}
    
    /**
     * 环绕通知:执行鉴权
     */
    @Around("velaResourcePointcut()")
    public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
        // 1. 获取方法上的注解
        MethodSignature signature = (MethodSignature) joinPoint.getSignature();
        Method method = signature.getMethod();
        VelaResource velaResource = method.getAnnotation(VelaResource.class);
        
        // 2. 获取方法参数
        Object[] args = joinPoint.getArgs();
        
        // 3. 调用 authorize() 接口进行鉴权
        boolean hasPermission = authService.authorize(
            velaResource.resource(),
            velaResource.action(),
            args
        );
        
        // 4. 鉴权失败,抛出异常
        if (!hasPermission) {
            throw new SecurityException(velaResource.message());
        }
        
        // 5. 鉴权通过,执行原方法
        return joinPoint.proceed();
    }
}

@Configuration
@ConditionalOnClass(Aspect.class)
public class VelaResourceAspectAutoConfiguration {

    @Bean
    public VelaResourceAspect velaAuthAspect() {
        return new VelaResourceAspect();
    }
}

基于编程的方式

使用 StaticMethodMatcherPointcut 编程的方式实现:

package com.vela.auth.pointcut;

import com.vela.auth.annotation.VelaResource;
import com.vela.auth.service.AuthService;
import org.aopalliance.intercept.MethodInterceptor;
import org.aopalliance.intercept.MethodInvocation;
import org.springframework.aop.Advisor;
import org.springframework.aop.support.DefaultPointcutAdvisor;
import org.springframework.aop.support.StaticMethodMatcherPointcut;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.lang.reflect.Method;

/**
 * 切点类:匹配方法上的 @VelaResource 注解
 */
public class VelaResourceStaticPointcut extends StaticMethodMatcherPointcut {
    
    @Override
    public boolean matches(Method method, Class<?> targetClass) {
        // 只匹配方法上标注了 @VelaResource 的方法
        return method.isAnnotationPresent(VelaResource.class);
    }
}

/**
 * 通知类:执行鉴权逻辑
 */
class VelaResourceAdvice implements MethodInterceptor {
    
    private AuthService authService;
    
    public VelaResourceAdvice(AuthService authService) {
        this.authService = authService;
    }
    
    @Override
    public Object invoke(MethodInvocation invocation) throws Throwable {
        // 1. 获取方法上的注解
        Method method = invocation.getMethod();
        VelaResource velaResource = method.getAnnotation(VelaResource.class);
        
        // 2. 获取方法参数
        Object[] args = invocation.getArguments();
        
        // 3. 调用 authorize() 接口进行鉴权
        boolean hasPermission = authService.authorize(
            velaResource.resource(),
            velaResource.action(),
            args
        );
        
        // 4. 鉴权失败,抛出异常
        if (!hasPermission) {
            throw new SecurityException(velaResource.message());
        }
        
        // 5. 鉴权通过,执行原方法
        return invocation.proceed();
    }
}

/**
 * Spring 配置类
 */
@Configuration
public class VelaResourceStaticConfig {
    
    @Autowired
    private AuthService authService;
    
    @Bean
    public Advisor velaResourceAdvisor() {
        // 组合切点和通知,传入 authService
        return new DefaultPointcutAdvisor(
            new VelaResourceStaticPointcut(),
            new VelaResourceAdvice(authService)
        );
    }
}

测试代码

测试代码如下:

package com.vela.auth.service;

import com.vela.auth.annotation.Action;
import org.springframework.stereotype.Service;

/**
 * 鉴权服务接口
 */
public interface AuthService {
    
    /**
     * 鉴权接口
     * @param resource 资源标识
     * @param action 操作类型
     * @param args 方法参数
     * @return true-有权限,false-无权限
     */
    boolean authorize(String resource, Action action, Object[] args);
}

@Service
public class UserService {
    
    @VelaResource(resource = "user", action = Action.READ, message = "无查询权限")
    public User findById(Long id) {
        System.out.println("执行: 查询用户 " + id);
        return new User(id, "张三");
    }
    
    @VelaResource(resource = "user", action = Action.UPDATE, message = "无更新权限")
    public void updateUser(Long id, String name) {
        System.out.println("执行: 更新用户 " + id + " 为 " + name);
    }
    
    @VelaResource(resource = "user", action = Action.DELETE, message = "无删除权限")
    public void deleteUser(Long id) {
        System.out.println("执行: 删除用户 " + id);
    }
    
    @VelaResource(resource = "order", action = Action.CREATE, message = "无创建订单权限")
    public void createOrder(String orderNo) {
        System.out.println("执行: 创建订单 " + orderNo);
    }
}

@Service
public class OrderService {
    
    @VelaResource(resource = "order", action = Action.READ, message = "无查询订单权限")
    public void findOrder(Long orderId) {
        System.out.println("执行: 查询订单 " + orderId);
    }
}

核心差异对比

实现方式对比:

对比项 @Aspect 方式 StaticMethodMatcherPointcut 方式
代码量 ~30行 ~50行
authService 注入 直接 @Autowired 通过构造函数传递
切点定义 @Pointcut("@annotation(...)") matches() 方法
通知实现 @Around 注解 实现 MethodInterceptor
配置 只需 @EnableAspectJAutoProxy 需要手动配置 Advisor Bean

两种方案都正确调用了 authorize() 接口,但 @Aspect 方式明显更简洁、更易维护

能力对比:

通知类型 @Aspect 注解 StaticMethodMatcherPointcut 实现难度
前置通知 @Before 实现 MethodBeforeAdvice @Aspect 更简单
后置通知 @After 需要环绕通知模拟 @Aspect 更简单
返回通知 @AfterReturning 实现 AfterReturningAdvice @Aspect 更简单
异常通知 @AfterThrowing 实现 ThrowsAdvice @Aspect 更简单
环绕通知 @Around 实现 MethodInterceptor 相当

参考:

posted @ 2023-10-19 10:16  LARRY1024  阅读(179)  评论(0)    收藏  举报