Can Java Interceptors Avoid Dynamic Proxies? Feat’s AOT Solution Explained

Feat 2.3.0 introduces compile‑time generated interceptors that replace Spring‑style runtime proxies, showing how AOT weaving eliminates reflection overhead, reduces startup cost, and offers a clean stack trace, while outlining the trade‑offs, limitations, and practical usage guidelines for Java developers.

Three Knives
Three Knives
Three Knives
Can Java Interceptors Avoid Dynamic Proxies? Feat’s AOT Solution Explained

Version updates in Feat 2.3.0

Fixes a large‑body

PostInputStream
StackOverflowError

, upgrades the underlying smart-socket library to version 2.1.3, changes the default loading order of @Bean from 0 to Byte.MAX_VALUE, and adds a Nacos configuration example.

Interceptor syntax

Define an interceptor class:

@Interceptor
public class LoggingInterceptor {
    @AroundInvoke
    public Object intercept(InvocationContext context) throws Exception {
        String methodName = context.getMethod().getDeclaringClass().getSimpleName() + "." + context.getMethod().getName();
        long start = System.nanoTime();
        System.out.println("进入 " + methodName + ", 参数=" + java.util.Arrays.toString(context.getParameters()));
        try {
            Object result = context.proceed();
            System.out.println("完成 " + methodName + ", 返回值=" + result);
            return result;
        } finally {
            long elapsed = System.nanoTime() - start;
            System.out.println("耗时 " + elapsed + " ns");
        }
    }
}

Bind the interceptor to a method:

@Controller("orders")
public class OrderController {
    @RequestMapping("/42")
    @Interceptors(LoggingInterceptor.class)
    public String findOrder() {
        return "order-42";
    }
}

After running mvn compile and invoking the endpoint, the console prints:

进入 OrderController.findOrder, 参数=[]
完成 OrderController.findOrder, 返回值=order-42
耗时 325100 ns

Compile‑time generation vs runtime proxy

During mvn compile, Feat’s AOT processor scans for @Interceptor, @AroundInvoke and @Interceptors, then generates new Java source files. For each intercepted class a subclass is created; the overridden methods contain the ordered interceptor chain. No reflection or proxy objects are used at runtime; the call becomes a plain Java method invocation.

Comparison of dimensions:

Weaving timing : Spring AOP – runtime during bean initialization; Feat – compile time via mvn compile.

Call overhead : Spring AOP – proxy + reflection/method handle; Feat – ordinary Java method call.

Startup burden : Spring AOP – creates proxies and resolves pointcuts; Feat – call chain already determined at compile time.

Debug stack : Spring AOP – includes proxy classes; Feat – generated plain class, clean stack.

Usage limits : Spring AOP – cannot proxy final methods/classes; Feat – cannot generate subclass for final classes or override final methods.

Flexibility : Spring AOP – supports runtime expression matching and dynamic adjustments; Feat – binding is fixed at compile time, immutable at runtime.

Why Feat retains only core capabilities

Feat keeps three essential actions:

Declare an interceptor with @Interceptor.

Bind it to a class or method using @Interceptors.

Advance the chain with context.proceed().

Two reasons drive this reduction:

In most real‑world scenarios interceptors are used for logging, permission checks, transaction handling, or timing; advanced features are rarely needed.

Dynamic features (e.g., runtime parameter replacement, context propagation) would require reflection, contradicting the goal of compile‑time plain Java code.

The resulting API requires a public interceptor class with a single @AroundInvoke method, a no‑arg accessible constructor, and a single binding annotation per interceptor.

Constraints and usage boundaries

The intercepted class cannot be final because Feat needs to generate a subclass.

The intercepted method must be a public instance method; static, final, or private methods are not woven.

The interceptor class must have an accessible no‑argument constructor.

Each interceptor class may contain only one @AroundInvoke method.

After changing interceptor annotations, re‑run mvn compile to regenerate code.

If an interceptor does not call proceed() and returns directly, the return type must be compatible with the target method, otherwise type‑conversion errors occur.

When code heavily relies on final classes or methods, a dynamic‑proxy solution may be more appropriate.

Project reference

Repository: https://gitee.com/smartboot/feat

Documentation: https://smartboot.tech/feat/

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

backendJavainterceptorSpring AOPAOTCompile-timeFeat
Three Knives
Written by

Three Knives

Every line of code you contribute to open source could help make the future better.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.