Spring 使用教程(二): AOP 使用

554 阅读4分钟

Java 是面向对象设计的语言,面向对象的继承特性是纵向的扩展,而 AOP(Object Oriented Programming,面向对象编程)可以认为是横向的扩展,Spring 提供的 AOP 能力与 「SOLID」 原则中的 OCP 原则(面向拓展开放,面向修改关闭)也是非常契合的。

工程创建和基本依赖参考 IOC 容器使用,参考:Spring使用教程(一): IOC 容器使用

pom 依赖

使用 Spring 的 AOP,需要在 IOC 容器的依赖基础上,再加入以下依赖:

<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-aop</artifactId>
    <version>${spring_version}</version>
</dependency>

<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-aspects</artifactId>
    <version>${spring_version}</version>
</dependency>

applicationContext.xml

applicationContext.xml 中需要加配置,如下:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
	   xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	   xmlns:context="http://www.springframework.org/schema/context"
	   xmlns:aop="http://www.springframework.org/schema/aop"
	   xsi:schemaLocation="http://www.springframework.org/schema/beans
	http://www.springframework.org/schema/beans/spring-beans-3.1.xsd
	http://www.springframework.org/schema/context
	http://www.springframework.org/schema/context/spring-context-3.1.xsd http://www.springframework.org/schema/aop https://www.springframework.org/schema/aop/spring-aop.xsd"
	   default-autowire="byName">

<!--	proxy-target-class默认为false,表示织入模式是基于接口的动态代理;为true表示织入模式是基于CGLib-->
<!--	当然,如果被增强的类没有实现接口,Spring将自动使用CGLib动态代理-->
	<aop:aspectj-autoproxy proxy-target-class="true"/>
	<context:component-scan base-package="org.example"/>
</beans>

CGLib 是基于字节码增强技术的动态代理技术,有兴趣可以看下Java 代理模式介绍

基本概念

AOP 中有两个重要的概念:切面和切入点。

切面,即一个横跨多个核心逻辑的功能,或者称之为系统关注点

通俗来说,切面是一个类,这个类中定义了若干增强方法,并借助切入点去声明这些增强方法作用的对象。

切入点,即定义在应用程序流程的何处插入切面的执行

切入点的定义方式有 「execution 表达式」和「注解」两种,execution 是使用正则表达式去定义切面中增强方法作用的范围,比如 execution(* org.example.*DAO.*(..)) 表达式定义了在 org.example 包下,所有 XxxDAO 类的所有方法都会被增强。

它看起来是个一劳永逸的方法,但正因为这样,它的危害也是很大的。比如一个项目的维护人换了,后来的同学可能并不知道有这样的配置,也许他并不想他新增的DAO被代理,这样可能就会带来一些意想不到的效果。

因此,这里也只介绍如何结合注解使用 AOP,这是一种强感知的方法(就像 Spring 中对于需要事务的方法,需要显式使用 Transactionnal 注解,而不是默认给方法都增加事务),不会出现上面的问题。

注解 AOP 实战

假设现在需要新增一个切面,它的功能是:统计方法的执行时间。

注解 Metric

为此,我们先定义一个监控注解 Metric 如下:

package org.example.aop.annotations;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Metric {
    String methodName();
}

其中 Target 注解声明了 Metric 注解是作用于方法上的,Retention 注解声明了 Metric 注解是运行时生效。

methodName 属性要求使用该注解的地方,需要告知它的方法名称。

切面类 MetricAspect

再定义切面类 MetricAspect 如下:

package org.example.aop.aspects;

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.example.aop.annotations.Metric;
import org.springframework.stereotype.Component;

@Aspect
@Component
public class MetricAspect {

    /**
     * Before 注解中的 value 属性表示切入点
     */
    @Before(value = "@annotation(metric)")
    public void metricBefore(Metric metric) {
        System.out.printf("method:[%s] begin metric !%n", metric.methodName());
    }

    /**
     * Around 注解中的 value 属性表示切入点
     */
    @Around(value = "@annotation(metric)")
    public Object metricAround(ProceedingJoinPoint joinPoint, Metric metric) throws Throwable {
        long start = System.currentTimeMillis();
        try {
            return joinPoint.proceed();
        } finally {
            long spend = System.currentTimeMillis() - start;
            String msg = String.format("method:[%s], spendTime:[%s]ms", metric.methodName(), spend);
            System.out.println(msg);
            // 可以输出到监控日志
        }
    }
}

注意切面类也需要注入到 IOC 容器中,因此 MetricAspect 有 Component 注解声明,另外再使用 Aspect 注解声明这是一个切面类。

Before 注解声明该方法增强的切入时机是在方法执行前,它的 value 表明该增强的切入点是具有 Metric 注解声明的方法。

Around 注解声明该方法增强的切入时机包括之前和之后,被增强的方法执行的时间是由 joinPoint.proceed() 控制。因此 metricAround 方法逻辑就是在方法执行前记录系统时间,在方法执行后统计方法执行时间并打印出来。

目标对象 Calculator

定义一个类 Calculator,如下:

package org.example.pojo;

import org.example.aop.annotations.Metric;
import org.springframework.stereotype.Component;

import java.util.Random;

@Component("calculator")
public class Calculator {
    @Metric(methodName = "Calculator.add")
    public int add(int a, int b) throws InterruptedException {
        // 模拟方法耗时
        Thread.sleep(new Random().nextInt(3000));
        return a + b;
    }
}

Calculator 提供了一个 add 方法,提供计算两数之和的功能,add 方法具有 Metric 注解声明,表明该方法需要监控计算时长。

main 方法测试 AOP

创建 Application 类如下:

package org.example.start;

import org.example.pojo.Calculator;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

public class Application {
    
    public static void main(String[] args) throws InterruptedException {
        ApplicationContext applicationContext = new ClassPathXmlApplicationContext("classpath:applicationContext.xml");
        Calculator calculator = (Calculator) applicationContext.getBean("calculator");
        calculator.add(5, 5);
    }
    
}

ApplicationContext 用于启动 IOC 容器装载上下文,这在前面的 IOC 容器使用中有介绍。

执行 main 方法如下:

method:[Calculator.add] begin metric !
method:[Calculator.add], spendTime:[451]ms

第一行是切面类 MetricAspect.metricBefore 方法的效果,第二行是 MetricAspect.metricAround 方法的效果。

Junit 测试和打 jar 包测试方法与 IOC 容器使用章节类似,这里就不再介绍。

总结

  • Spring 中 AOP 的使用需要新增 pom 依赖,并需要在 xml 配置文件中开启 AOP 开关。
  • 推荐使用注解 AOP,这是一种显式且安全的使用形式。