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,这是一种显式且安全的使用形式。