跳到主要内容

切面(interceptor)

切面编程(Aspect-Oriented Programming, AOP)是一种编程范式, 它通过将横切关注点(cross-cutting concerns)与业务逻辑分离, 来增强代码的模块化. 在企业级 Java 开发中, AOP 通常用于日志记录, 安全性, 事务管理等方面

主流的 AOP 技术与实现:

框架类型原理缺点
Spring AOP运行时JDK 动态代理: 适用于接口代理. 通过创建实现了目标接口的代理对象来拦截方法调用
CGLIB 代理: 适用于类代理. 通过创建目标类的子类来拦截方法调用
性能开销: Spring AOP 基于代理机制, 这意味着每次方法调用都会有额外的代理开销, 可能会影响性能, 尤其是在大量频繁调用的场景下
受限于 Spring 容器: Spring AOP 仅在 Spring 容器管理的 Bean 上有效, 对于非 Spring 管理的对象无法应用
AspectJ编译时/加载时/运行时编译时织入(Compile-Time Weaving): 在源代码编译为字节码时, 直接将切面代码织入到字节码中. 这需要使用 AspectJ 提供的编译器 ajc, 或者通过将 AspectJ 集成到构建工具(如 Maven 或 Gradle)中
后编译时织入(Post-Compile Weaving): 也称为二进制织入(Binary Weaving), 是在已经编译好的字节码文件中进行织入. 这种方式适用于需要在编译后的字节码上进行切面增强的场景
加载时织入(Load-Time Weaving): 在类加载时, 将切面代码动态织入到目标类的字节码中. 这通常需要一个特殊的类加载器, 使用 Java 的 Instrumentation API 实现
学习曲线陡峭: AspectJ 具有强大的功能, 但其语法和概念(如切点表达式, 通知类型, 织入方式等)可能对初学者来说比较复杂, 需要一定的学习成本
编译和构建复杂性: 使用 AspectJ 编译器(ajc)可能需要额外的构建配置, 尤其是在大型项目中集成 AspectJ 时, 可能会增加构建过程的复杂性
运行时性能开销: 虽然 AspectJ 的编译时织入性能较好, 但加载时织入可能会带来一定的性能开销
调试困难: 由于 AspectJ 修改了字节码, 调试切面代码可能会变得困难, 特别是在问题定位和排查方面

Nozdormu 基于Jakarta Interceptors 规范, 使用透明高效的 Annotation Processing(JSR-269) 技术, 通过对切片代码的静态分析, 在编译前阶段将切面织入到源码中, 易于调试, 充分发挥编译器和 IDE 的检查能力

安装

添加依赖

repositories {
mavenCentral()
jcenter()
}

dependencies {
implementation 'org.graphoenix:nozdormu-inject:0.1.0'
implementation 'org.graphoenix:nozdormu-interceptor:0.1.0'

annotationProcessor 'org.graphoenix:nozdormu-inject:0.1.0'
annotationProcessor 'org.graphoenix:nozdormu-interceptor:0.1.0'

// ...
}

方法切面

定义方法注解

使用 @InterceptorBinding 标记方法注解

Launch.java
import jakarta.interceptor.InterceptorBinding;

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

@InterceptorBinding
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(value = RetentionPolicy.RUNTIME)
public @interface Launch {
String value() default "";
}

实现方法切面

使用 @Interceptor方法注解标记切面类, 使用 @AroundInvoke指定切面实现的方法, 如果需要指定切面方法的执行顺序使用 @Priority 注解指定

使用 InvocationContext 中的 proceed() 方法可以执行目标方法并返回结果

FirstLaunchInterceptor.java
import io.nozdormu.interceptor.test.annotation.Launch;
import jakarta.annotation.Priority;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.interceptor.AroundInvoke;
import jakarta.interceptor.Interceptor;
import jakarta.interceptor.InvocationContext;

@ApplicationScoped
@Launch //方法
@Priority(0) //顺序注解
@Interceptor //切面注解
public class FirstLaunchInterceptor {

@AroundInvoke
public Object aroundInvoke(InvocationContext invocationContext) {
try {
return "first stage fired -> " + invocationContext.proceed();
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}
SecondLaunchInterceptor.java
import io.nozdormu.interceptor.test.annotation.Launch;
import jakarta.annotation.Priority;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.interceptor.AroundInvoke;
import jakarta.interceptor.Interceptor;
import jakarta.interceptor.InvocationContext;

@ApplicationScoped
@Launch //方法注解
@Priority(1) //顺序注解
@Interceptor //切面注解
public class SecondLaunchInterceptor {

@AroundInvoke
public Object aroundInvoke(InvocationContext invocationContext) {
try {
return "second stage fired -> " + invocationContext.proceed();
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}

标记目标方法

在目标方法上使用方法注解

Satellite.java
import io.nozdormu.interceptor.test.annotation.Launch;
import jakarta.enterprise.context.ApplicationScoped;

@ApplicationScoped
public class Satellite {

@Launch
public String startup(String name) {
return "hello " + name + " I am " + owner.getName();
}
}

测试

InterceptorTest.java
import io.nozdormu.interceptor.test.beans.Satellite;
import io.nozdormu.spi.context.BeanContext;
import org.junit.jupiter.api.Test;

import static org.junit.jupiter.api.Assertions.assertEquals;

public class InterceptorTest {

@Test
void testSatellite() {
Satellite satellite = BeanContext.get(Satellite.class);
assertEquals(satellite.checkResult(), "first stage ready -> second stage ready -> all check ready, fire");
}
}

构造器切面

定义构造器注解

使用 @InterceptorBinding 标记构造器注解

Install.java
import jakarta.interceptor.InterceptorBinding;

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

@InterceptorBinding
@Target({ElementType.TYPE, ElementType.CONSTRUCTOR})
@Retention(value = RetentionPolicy.RUNTIME)
public @interface Install {
}

实现构造器切面

使用 @Interceptor构造器注解标记切面类, 使用 @AroundConstruct 指定切面实现的方法, 如果需要指定切面方法的执行顺序使用 @Priority 注解指定

使用 InvocationContext 中的 proceed() 方法可以执行目标构造器并返回结果

FirstInstallInterceptor.java
import io.nozdormu.interceptor.test.annotation.Install;
import jakarta.annotation.Priority;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.interceptor.AroundConstruct;
import jakarta.interceptor.Interceptor;
import jakarta.interceptor.InvocationContext;

import java.util.ArrayList;
import java.util.List;

@ApplicationScoped
@Launch //构造器注解
@Priority(0) //顺序注解
@Interceptor //切面注解
public class FirstInstallInterceptor {

@AroundConstruct
public Object aroundConstruct(InvocationContext invocationContext) {
List<String> infoList = ((ArrayList<String>) invocationContext.getContextData().computeIfAbsent("infoList", (key) -> new ArrayList<String>()));
infoList.add("first stage ready ->");
try {
return invocationContext.proceed();
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}
SecondInstallInterceptor.java
import io.nozdormu.interceptor.test.annotation.Install;
import io.nozdormu.interceptor.test.beans.Satellite;
import jakarta.annotation.Priority;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.interceptor.AroundConstruct;
import jakarta.interceptor.Interceptor;
import jakarta.interceptor.InvocationContext;

import java.util.ArrayList;
import java.util.List;

@ApplicationScoped
@Launch //构造器注解
@Priority(1) //顺序注解
@Interceptor //切面注解
public class SecondInstallInterceptor {

@AroundConstruct
public Object aroundConstruct(InvocationContext invocationContext) {
List<String> infoList = ((ArrayList<String>) invocationContext.getContextData().computeIfAbsent("infoList", (key) -> new ArrayList<String>()));
infoList.add("second stage ready ->");
try {
Satellite satellite = (Satellite) invocationContext.proceed();
satellite.setInfoList(infoList);
return satellite;
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}

标记目标构造器

在目标方法上使用构造器注解

Satellite.java
import io.nozdormu.interceptor.test.annotation.Install;
import jakarta.enterprise.context.ApplicationScoped;

import java.util.ArrayList;
import java.util.List;

@ApplicationScoped
public class Satellite {

private List<String> infoList = new ArrayList<>();

private final Owner owner;

@Install
public Satellite(Owner owner) {
this.owner = owner;
}

public void setInfoList(List<String> infoList) {
this.infoList = infoList;
}

public String checkResult() {
return String.join(" ", infoList) + " all check ready, fire";
}
}

测试

InterceptorTest.java
import io.nozdormu.interceptor.test.beans.Satellite;
import io.nozdormu.spi.context.BeanContext;
import org.junit.jupiter.api.Test;

import static org.junit.jupiter.api.Assertions.assertEquals;

public class InterceptorTest {

@Test
void testSatellite() {
Satellite satellite = BeanContext.get(Satellite.class);
assertEquals(satellite.startup("nozdormu"), "first stage fired -> second stage fired -> hello nozdormu I am NASA");
}
}

切面 API

InvocationContext

使用 InvocationContext 获得切面运行时的参数和方法

注解参数返回值说明
Object getTarget()Object返回目标实例
Method getMethod()Method返回调用拦截器的目标类的方法
Constructor<?> getConstructor()Constructor<?>返回调用拦截器方法的目标类的构造函数
Object[] getParameters()Object[]返回将传递给目标类的方法或构造函数的参数值
void setParameters(Object[] params)参数值设置将传递给目标类的方法或构造函数的参数值
Map<String, Object> getContextData()Map<String, Object>与此调用或生命周期回调相关的上下文数据
Object proceed() throws ExceptionObject继续执行拦截器链中的下一个拦截器

注解说明

Interceptor

注解目标说明
jakarta.interceptor.InterceptorBinding注解标记需要切面处理的注解
jakarta.interceptor.Interceptor标记切面的实现类
jakarta.interceptor.AroundInvoke方法标记方法切面的实现方法
jakarta.interceptor.AroundConstruct方法标记构造器切面的实现方法
jakarta.annotation.Priority配置切面的执行顺序

本节示例

https://github.com/doukai/nozdormu/tree/main/nozdormu-interceptor/src/test/java/io/nozdormu/interceptor/test