Backend Development 18 min read

Java Dynamic Proxies Explained: From Static to ByteBuddy with Performance Tips

This article explores Java's proxy patterns, detailing static proxy implementation, various dynamic proxy techniques—including JDK, Cglib, Javassist, and ByteBuddy—providing code examples, performance comparisons, and practical guidance for integrating these proxies into backend development for cleaner, more flexible code.

Sanyou's Java Diary
Sanyou's Java Diary
Sanyou's Java Diary
Java Dynamic Proxies Explained: From Static to ByteBuddy with Performance Tips

Preface

Proxy pattern is a design pattern that allows extending functionality without modifying the original target. In Java there are static and dynamic proxies; dynamic proxies are the core of Spring AOP, interceptors, transaction control, and many other features.

Static Proxy

Static proxy uses an explicit proxy class to access the target object. The example defines two interfaces, Person and Animal , each with two implementations (Student, Doctor, Dog, Cat). The Student class is shown below.

<code>public class Student implements Person {
    private String name;

    public Student() {}

    public Student(String name) {
        this.name = name;
    }

    @Override
    public void wakeup() {
        System.out.println(StrUtil.format("学生[{}]早晨醒来啦", name));
    }

    @Override
    public void sleep() {
        System.out.println(StrUtil.format("学生[{}]晚上睡觉啦", name));
    }
}
</code>

To add a line before wakeup() and sleep() , two proxy classes are created: PersonProxy and AnimalProxy .

<code>public class PersonProxy implements Person {
    private Person person;

    public PersonProxy(Person person) {
        this.person = person;
    }

    @Override
    public void wakeup() {
        System.out.println("早安~");
        person.wakeup();
    }

    @Override
    public void sleep() {
        System.out.println("晚安~");
        person.sleep();
    }
}
</code>
<code>public class AnimalProxy implements Animal {
    private Animal animal;

    public AnimalProxy(Animal animal) {
        this.animal = animal;
    }

    @Override
    public void wakeup() {
        System.out.println("早安~");
        animal.wakeup();
    }

    @Override
    public void sleep() {
        System.out.println("晚安~");
        animal.sleep();
    }
}
</code>

Execution code creates proxies for each concrete class and invokes the methods, producing output such as:

<code>早安~
学生[张三]早晨醒来啦
晚安~
学生[张三]晚上睡觉啦
...</code>

JDK Dynamic Proxy

JDK dynamic proxy generates a proxy class at runtime using java.lang.reflect.Proxy and InvocationHandler . The core proxy class is shown below.

<code>public class JdkProxy implements InvocationHandler {
    private Object bean;

    public JdkProxy(Object bean) {
        this.bean = bean;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        String methodName = method.getName();
        if (methodName.equals("wakeup")) {
            System.out.println("早安~~~");
        } else if (methodName.equals("sleep")) {
            System.out.println("晚安~~~");
        }
        return method.invoke(bean, args);
    }
}
</code>

Execution creates a Proxy instance for each target and calls the methods, achieving the same effect with a single proxy class.

Cglib Dynamic Proxy

Cglib creates a subclass of the target class at runtime, avoiding the need for an interface. It is faster than JDK proxy and can be assigned to concrete classes.

<code>public class CglibProxy implements MethodInterceptor {
    private Enhancer enhancer = new Enhancer();
    private Object bean;

    public CglibProxy(Object bean) {
        this.bean = bean;
    }

    public Object getProxy() {
        enhancer.setSuperclass(bean.getClass());
        enhancer.setCallback(this);
        return enhancer.create();
    }

    @Override
    public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
        String methodName = method.getName();
        if (methodName.equals("wakeup")) {
            System.out.println("早安~~~");
        } else if (methodName.equals("sleep")) {
            System.out.println("晚安~~~");
        }
        return method.invoke(bean, args);
    }
}
</code>

Execution code creates a Cglib proxy for each concrete class and invokes the methods.

Javassist Dynamic Proxy

Javassist manipulates bytecode directly to create proxy classes. The proxy implementation looks like this:

<code>public class JavassitProxy {
    private Object bean;

    public JavassitProxy(Object bean) {
        this.bean = bean;
    }

    public Object getProxy() throws IllegalAccessException, InstantiationException {
        ProxyFactory f = new ProxyFactory();
        f.setSuperclass(bean.getClass());
        f.setFilter(m -> java.util.Arrays.asList("wakeup", "sleep").contains(m.getName()));
        Class c = f.createClass();
        MethodHandler mi = (self, method, proceed, args) -> {
            String methodName = method.getName();
            if (methodName.equals("wakeup")) {
                System.out.println("早安~~~");
            } else if (methodName.equals("sleep")) {
                System.out.println("晚安~~~");
            }
            return method.invoke(bean, args);
        };
        Object proxy = c.newInstance();
        ((Proxy) proxy).setHandler(mi);
        return proxy;
    }
}
</code>

Execution creates proxies for Student, Doctor, Dog, and Cat similarly.

ByteBuddy Dynamic Proxy

ByteBuddy builds on ASM to generate subclasses at runtime. The proxy class is defined as:

<code>public class ByteBuddyProxy {
    private Object bean;

    public ByteBuddyProxy(Object bean) {
        this.bean = bean;
    }

    public Object getProxy() throws Exception {
        return new ByteBuddy()
            .subclass(bean.getClass())
            .method(ElementMatchers.namedOneOf("wakeup", "sleep"))
            .intercept(InvocationHandlerAdapter.of(new AopInvocationHandler(bean)))
            .make()
            .load(ByteBuddyProxy.class.getClassLoader())
            .getLoaded()
            .newInstance();
    }

    public static class AopInvocationHandler implements InvocationHandler {
        private Object bean;
        public AopInvocationHandler(Object bean) { this.bean = bean; }
        @Override
        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
            String methodName = method.getName();
            if (methodName.equals("wakeup")) {
                System.out.println("早安~~~");
            } else if (methodName.equals("sleep")) {
                System.out.println("晚安~~~");
            }
            return method.invoke(bean, args);
        }
    }
}
</code>

Running the proxy yields the same behavior as the other implementations.

Performance Comparison

A simple single‑threaded benchmark running each proxy implementation 10,000 times produced the following timings:

<code>JDK PROXY 10000 iterations: 0.714970125 seconds
Cglib 10000 iterations: 0.434937833 seconds
Javassist 10000 iterations: 1.294194708 seconds
ByteBuddy 10000 iterations: 9.731999042 seconds
</code>

The results show Cglib to be the fastest in this test, while ByteBuddy appears much slower, possibly due to the test setup.

Conclusion

Dynamic proxy technology is a powerful tool for middleware developers, making code more elegant and simplifying many Spring core concepts. Readers are encouraged to run the examples to deepen their understanding.

https://gitee.com/bryan31/proxy-demo
JavaperformanceJDKDynamic ProxyByteBuddyProxy PatternCGLIB
Sanyou's Java Diary
Written by

Sanyou's Java Diary

Passionate about technology, though not great at solving problems; eager to share, never tire of learning!

0 followers
Reader feedback

How this landed with the community

login 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.