使用spring aop Java平台AOP方案稳固,SpringAOP结合AspectJ各取所长
如今,AOP( )已经不是什么崭新的概念了,在经历了代码生成、动态代理、字节码增强甚至静态编译等不同时代的洗礼之后,Java 平台上的 AOP 方案基本上已经以 结合 的方式稳固下来(虽然大家依然可以自己通过各种字节码工具偶尔“打造一些轮子”)。
现在 框架提供的 AOP 方案倡导了一种各取所长的方案,即使用 的面向对象的方式来编写和组织织入逻辑,并使用 的 描述语言配合 来标注和指明织入点()。
原则上来说,我们只要引入 框架中 AOP 的相应依赖就可以直接使用 的 AOP 支持了,不过,为了进一步为大家使用 提供便利, 还是“不厌其烦”地为我们提供了一个 -boot--aop 自动配置模块。
-boot--aop 自动配置行为由两部分内容组成:
一般情况下,只要项目依赖中加入了 -boot--aop,其实就会自动触发 AOP 的关联行为,包括构建相应的 ,将横切关注点织入(Weave)相应的目标对象等,不过 依然为我们提供了可怜的两个配置项,用来有限地干预 AOP 相关配置:
对我们来说,这两个配置项的最大意义在于:允许我们投反对票,比如可以选择关闭自动的 aop 配置(.aop.auto=false),或者启用针对 class 而不是 级别的 aop 代理(aop proxy)。
AOP 的应用场景很多,我们不妨以当下最热门的 APM( )为实例场景,尝试使用 -boot--aop 的支持打造一个应用性能监控的工具原型。
-boot--aop 在构建 -boot-- 自定义模块中的应用
对于应用性能监控来说,架构逻辑上其实很简单,基本上就是三步走(如图 1 所示)。
本节暂时只构建一个 -boot-- 自定义的自动配置模块用来解决“应用性能数据采集”的问题。

在此之前,有几个原则我们需要先说明一下:
虽然说采集应用性能数据可以帮助我们更好地分析和改进应用的性能指标,但这不意味着可以借着 APM 的名义对应用的核心职能形成侵害,加上应用性能数据采集功能一定会对应用的性能本身带来拖累,你拿到的所谓性能数据是分摊了你的数据采集方案带来的负担,所以,一般情况下,最好把应用性能数据采集模块的性能损耗控制在 10% 以内甚至更小。
其实提供了多种横切逻辑织入机制(),性能损耗上也是各有差别,从运行期间的动态代理和字节码增强 ,到类加载期间的 ,甚至高冷的 二次静态编译 ,大家可以根据情况灵活把握。
针对应用性能数据的采集,最好对应用开发者是透明的,通过配置外部化的形式,可以最大限度地剥离这部分对应用开发者来说非核心的关注点,只在部署和运行之前确定采集点并配置上线即可。
虽然本节实例采用基于 @ 的方式来标注性能采集点,但不意味着这是最优的方式,更多是基于技术方案()的现状给出的一种实践方式。
下面我们正式着手构建 -boot-- 自定义的自动配置模块的设计和实现方案。
笔者一向是只在有必要的时候才重新“造轮子”,绝不会为了炫技而去“造轮子”,所以,本次的主角我们选择 Java 中的 这个类库作为打造我们 APM 原型的起点。
为我们提供了多种不同类型的应用数据度量方案,且通过相应的数据处理算法在性能和批量状态的管理上做了很优秀的工作,只不过,如果我们直接用它的 API 来对自己的应用代码进行度量的话,那写起来代码太多,而且这些性能代码混杂在应用的核心逻辑执行路径上,一个是界面不友好,另外一个就是不容易维护:
public class MockService implements InitializingBean {
@Autowired
MetricRegistry metricRegistry;
private Timer timer;
private Counter counter;
// define more other metrics...
public void doSth() {
counter.inc();
Timer.Context context = timer.time();
try {
System.out.println("just do something.");
} finally {
context.stop();
}
}
@Override
public void afterPropertiesSet() throws Exception {
timer = metricRegistry.timer("timerToProfilingDoSthMethod");
counter = metricRegistry.counter("counterForDoSthMethod");
}
}
所以,对于这些非功能性的性能度量代码,我们可以使用 AOP 的方式剥离到相应的 中单独维护,而为了能够将这些性能度量的 挂接到指定的待度量代码上,基于现有的方案选型。
可以使用 - 提供的一系列 来标注织入位置,这样,开发者只要在需要度量的代码位置上标注相应的 ,我们提供的 -boot-- 自定义的自动配置模块就会自动地收集这些位置上指定的性能度量数据。
首先,我们通过 构建一个 的脚手架项目,选择以 Maven 编译(选择用 的同学自行甄别后面的配置如何具体进行),然后在创建好的 脚手架项目的 pom.xml 中添加如下必要配置:
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0modelVersion>
<groupId>com.keevolgroupId>
<artifactId>spring-boot-starter-metricsartifactId>
<version>0.0.1-SNAPSHOTversion>
<packaging>jarpackaging>
<name>spring-boot-starter-metricsname>
<description>auto configuration module for dropwizard metricsdescription>
<parent>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-parentartifactId>
<version>1.3.0.RELEASEversion>
<relativePath />
parent>
<properties>
<project.build.sourceEncoding>UTF-8project.build.sourceEncoding>
<java.version>1.8java.version>
<metrics.version>3.1.2metrics.version>
properties>
<dependencies>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starterartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-aopartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-actuatorartifactId>
dependency>
<dependency>
<groupId>io.dropwizard.metricsgroupId>
<artifactId>metrics-coreartifactId>
<version>
${metrics.version}
version>
dependency>
<dependency>
<groupId>io.dropwizard.metricsgroupId>
<artifactId>metrics-annotationartifactId>
<version>${metrics.version}version>
dependency>
<dependency>
<groupId>org.aspectjgroupId>
<artifactId>aspectjrtartifactId>
<version>1.8.7version>
dependency>
dependencies>
project>

pom.xml 中有几个关键配置需要关注:
至于 ,是使用了最新的版本,原则上-boot--aop已经有依赖,这里可以不用明确添加配置。
如果单单是一个提供必要依赖的自动配置模块,那么到这里其实就可以结束了,但我们的 -boot-- 需要使用 AOP 提供相应的横切关注点逻辑。
所以,还需要编写并提供一些必要的代码组件,因此,最少我们先要提供一个 @ 配置类,用于将我们即将提供的这些 AOP 逻辑暴露给使用者:
@Configuration
@ComponentScan({ "com.keevol.springboot.metrics.lifecycle",
"com.keevol.springboot.metrics.aop" })
@AutoConfigureAfter(AopAutoConfiguration.class)
public class DropwizardMetricsMBeansAutoConfiguration {
@Value("${metrics.mbeans.domain.name:com.keevol.metrics}")
String metricsMBeansDomainName;
@Autowired
MBeanServer mbeanServer;
@Autowired
MetricRegistry metricRegistry;
@Bean
public JmxReporter jmxReporter() {
JmxReporter reporter = JmxReporte.forRegistry(metricRegistry)
.inDomain(metricsMBeansDomainName).registerWith(mbeanServer)
.build();
return reporter;
}
}
然后就是将这个配置类添加到 META-INF/.:
org..boot..ion=\com.....-,
不要认为将 -boot-- 打包作为类库发布出去就可以了,AOP 相关的代码还没写。
我们回头来看 配置类,这个配置类的实现很简单,注入了 和 的实例,并开放了一个 ...name 配置属性(默认值 com..)便于使用者指定自定义的 MBean 暴露和访问的命名空间。
当然,以上给这些其实都不是重点,因为它们都只是为了将我们要采集的性能数据指标以 JMX 的形式暴露出去而服务的,重点在于 头顶上的那几顶“帽子”:
现在,最后的秘密就隐藏在 @ 背后的两个 java 之下了。
首先是 com....aop,在这个 java 下面,我们只提供了一个 ,其定义如下:
@Component
@Aspectpublic
class AutoMetricsAspect {
protected ConcurrentMap<String, Meter> meters = new ConcurrentHashMap<>();
protected ConcurrentMap<String, Meter> exceptionMeters = new ConcurrentHashMap<>();
protected ConcurrentMap<String, Timer> timers = new ConcurrentHashMap<>();
protected ConcurrentMap<String, Counter> counters = new ConcurrentHashMap<>();
@Autowired
MetricRegistry metricRegistry;
@Pointcut(value = "execution(public * *(..))")
public void publicMethods() {
}
@Before("publicMethods() && @annotation(countedAnnotation)")
public void instrumentCounted(JoinPoint jp, Counted countedAnnotation) {
String name = name(jp.getTarget().getClass(), StringUtils.hasLength(countedAnnotation.name()) ? countedAnnotation.name() : jp.getSignature().getName(), "counter");
Counter counter = counters.computeIfAbsent(name, key -> metricRegistry.counter(key));
counter.inc();
}
@Before("publicMethods() && @annotation(meteredAnnotation)")
public void instrumentMetered(JoinPoint jp, Metered meteredAnnotation) {
String name = name(jp.getTarget().getClass(), StringUtils.hasLength(meteredAnnotation.name()) ? meteredAnnotation.name() : jp.getSignature().getName(), "meter");
Meter meter = meters.computeIfAbsent(name, key -> metricRegistry.meter(key));
meter.mark();
}
@AfterThrowing(pointcut = "publicMethods() && @annotation(exMe-teredAnnotation)", throwing = "ex")
public void instrumentExceptionMetered(JoinPoint jp, Throwable ex, ExceptionMetered exMeteredAnnotation) {
String name = name(jp.getTarget().getClass(), StringUtils.hasLength(exMeteredAnnotation.name()) ? exMeteredAnnotation.name() : jp.getSignature().getName(), "meter", "exception");
Meter meter = exceptionMeters.computeIfAbsent(name, meterName -> metricRegistry.meter(meterName));
meter.mark();
}
@Around("publicMethods() && @annotation(timedAnnotation)")
public Object instrumentTimed(ProceedingJoinPoint pjp, Timed timedAnnotation) throws Throwable {
String name = name(pjp.getTarget().getClass(), StringUtils.hasLength(timedAnnotation.name()) ? timedAnnotation.name() : pjp.getSignature().getName(), "timer");
Timer timer = timers.computeIfAbsent(name, inputName -> metricRegistry.timer(inputName));
Timer.Context tc = timer.time();
try {
return pjp.proceed();
} finally {
tc.stop();
}
}
}
@+@ 的目的在于告诉 框架:“我是一个 AOP 的 实现类并且你可以通过 @ 把我加入 IoC 容器之中。”当然,这不是重点。
io..:- 这个依赖包为我们提供了几个有趣的 :
这些语义良好的 定义可以用来标注相应的 AOP 逻辑扩展点,比如,针对同一个 ,我们可以将性能数据的度量和采集简化为只标注一两个 就可以了:
@Component
public class MockService {
@Timed
@Counted
public void doSth() {
System.out.println("just do something.");
}
}
但是, 注定只是 ,它们只是一些标记信息,要让它们发挥作用,需要有“伯乐”的眷顾,所以, 在这里就是这些 的“伯乐”。
通过拦截每一个 方法并检查方法上是否存在某个 ,我们就可以根据具体的 的类型,为匹配的方法注入相应性能数据采集代码逻辑,从而完成整个基于 AOP 和 的应用性能数据采集方案的实现。
受限于 自身的一些限制,并不是所有 AOP 的 类型都支持,而且,以上原型代码方向也不见得是性能最优的方案,大家需要结合自己的目标和手上可用的技术手段,根据自己的具体应用场景具体分析和权衡。
























