Spring AOP

176 阅读5分钟

一、AOP

前面一篇文章简单介绍了Spring中的IoC,今天介绍Spring另一个重要的特点AOP。

AOP:Aspect Oriented Programming即面向切面编程。

还是通过一个场景帮助大家理解面向切面。考虑如下情况,Controller层有10个接口,现在想在接口调用前后进行日志打印,并记录每个接口的调用时长。难道我们要在每个接口前后都增加日志打印的逻辑么?有的同学说,10个接口改一下没啥,那要是一百个,一千个呢?很明显逐一修改不可取。这个时候就要用到面向切面的思想了。

1.1什么是面向切面?

我们将调用一个接口的流程看做一条线,我们想要在接口调用前后做一些额外的操作,而不影响接口的正常业务逻辑,学过设计模式的同学可能会觉得眼熟,这不是代理模式么?没错,Spring中的APO就是基于代理模式的。有基于JDK的和基于CGLib的,不再赘述。

切面其实就是换个角度看程序的流程,每个接口有每个接口的业务逻辑,换个角度,从接口调用开始到接口调用结束,不仅仅有业务逻辑,还有一些运维需要的逻辑,比如参数校验,日志记录,是对具体业务逻辑的更高一层的抽象。

1.2AOP的核心概念

  • 切面:对横切关注点的抽象

  • 连接点:被拦截到的点,Spring只支持方法类型的连接点,也就是被拦截到的方法

  • 切点:对连接点进行拦截的定位,通常是配合注解来使用

  • 通知:也就是方法增强,对拦截到的方法进行额外的逻辑处理

  • 目标对象:代理的目标对象

  • 引介:特殊的增强,可以动态的为类添加一些属性和方法

  • 织入:将增强添加到目标类的具体连接点的过程,根据织入的时期可以分为三种

    • 编译器织入(AspectJ)
    • 类加载期织入(AspectJ)
    • 运行期织入(Spring)

1.3 AOP的环绕方式

  • 前置通知(@Before)
  • 返回通知(@AfterReturning)
  • 异常通知(@AfterThrowing)
  • 后置通知(@After)
  • 环绕通知(@Around)

下面给出示例代码,帮助大家理解如何运用AOP编程

TIPS:使用@Aspect标注的类,一定要作为Bean对象注入容器,否则就无法生成代理。

@Aspect//使用@Aspect注解标注为一个切面类
@Component
public class WebLogAspect {

    private final static Logger logger = LoggerFactory.getLogger(WebLogAspect.class);
    
    //使用@Pointcut定义切点
    @Pointcut("@annotation(cn.fighter3.spring.aop_demo.WebLog)")
    public void webLog() {}

    @Before("webLog()")
    public void doBefore(JoinPoint joinPoint) throws Throwable {
        // 开始打印请求日志
        ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        HttpServletRequest request = attributes.getRequest();
        // 打印请求相关参数
        logger.info("========= Start ========= ");
        // 打印请求 url
        logger.info("URL            : {}", request.getRequestURL().toString());
        // 打印 Http method
        logger.info("HTTP Method    : {}", request.getMethod());
        // 打印调用 controller 的全路径以及执行方法
        logger.info("Class Method   : {}.{}", joinPoint.getSignature().getDeclaringTypeName(), joinPoint.getSignature().getName());
        // 打印请求的 IP
        logger.info("IP             : {}", request.getRemoteAddr());
        // 打印请求入参
        logger.info("Request Args   : {}",new ObjectMapper().writeValueAsString(joinPoint.getArgs()));
    }

    @After("webLog()")
    public void doAfter() throws Throwable {
        // 结束后打个分隔线,方便查看
        logger.info("========= End ========= ");
    }

    @Around("webLog()")
    public Object doAround(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
        //开始时间
        long startTime = System.currentTimeMillis();
        Object result = proceedingJoinPoint.proceed();
        // 打印出参
        logger.info("Response Args  : {}", new ObjectMapper().writeValueAsString(result));
        // 执行耗时
        logger.info("Time-Consuming : {} ms", System.currentTimeMillis() - startTime);
        return result;
    }
}

1.4切点的定义

要使用AOP编程,最关键就是找准切点,在Spring中切点只支持方法类型。简而言之就是,我想对哪个方法进行增强。这就涉及到@PointCut注解的使用了,支持三种方式来定位要增强的方法:

  • 带有特定注解的方法:@annotation(com.example.com.example.annotation.Loggable)
  • 特定类中的所有方法:within(com.example.controller..*)
  • 执行某个方法:execution表达式

二、Spring事务

为什么讲AOP要讲事务呢?因为Spring中对AOP的一大应用就是事务。而Spring事务分为:

  • 编程式事务
  • 声明式事务:Spring推荐通过@Transactional进行事务管理,且日常开发中常用

而声明式事务就是建立在AOP之上的。

2.1Spring事务的隔离级别

SQL 标准定义了四个隔离级别,Spring 都支持,并且提供了对应的机制来配置它们,定义在 TransactionDefinition 接口中,分别是

  • ISOLATION_DEFAULT
  • ISOLATION_READ_UNCOMMITTED
  • ISOLATION_READ_COMMITTED
  • ISOLATION_REPEATABLE_READ
  • ISOLATION_SERIALIZABLE

不同隔离级别,分别应对脏读、幻读、可重复读,此处不再赘述。

2.2Spring中的事务传播机制

事务的传播机制定义了在方法被另一个事务方法调用时,这个方法的事务行为应该如何。

Spring 提供了一系列事务传播行为,这些传播行为定义了事务的边界和事务上下文如何在方法调用链中传播。

事务传播机制描述
REQUIRED默认。需要事务,没有就新建,有就加入
SUPPORTS支持当前机制,入乡随俗,有就有,没有就算了
MANDATORY强制使用当前事务,没有就报错
REQUIRED_NEW需要一个新的事务,对于旧有的事务,没有最好,有就挂起旧事务
NOT_SUPPORTED不支持事务,当前有事务就异常
NESTED如果当前存在事务,则在事务内执行,如果当前没有事务,则执行与REQUIRED类似操作

2.3Spring中事务失效的的情况

Spring中的事务实现用到了ThreadLocal,所以在事务执行过程中,新建线程并不会生效。事务失效场景如下

  • 调用方法在新线程中
  • 非public方法:Spring事务是基于AOP的,而AOP代理对象只对public修饰的方法生效
  • 同一个类中方法调用:Spring事务是基于AOP的,原因是事务方法被当前类以外的代码调用时,才会由 Spring 生成的代理对象来管理