From Raw Code to AOP: Using Decorator, Proxy, CGLIB, and Spring AOP for Method Timing

This article walks through the evolution from plain Java implementations of a DAO interface to using the Decorator pattern, Java dynamic proxies, CGLIB proxies, and finally Spring AOP, demonstrating how each approach adds reusable timing logic while discussing their advantages and drawbacks.

Java Captain
Java Captain
Java Captain
From Raw Code to AOP: Using Decorator, Proxy, CGLIB, and Spring AOP for Method Timing

The author revisits a previous introductory article on Spring AOP and, feeling that the original content lacked depth, rewrites the tutorial starting from the most basic Java code and progressively applying the Decorator pattern, Java dynamic proxies, CGLIB, and finally Spring AOP to illustrate why AOP is useful.

Original code

public interface Dao {
    public void insert();
    public void delete();
    public void update();
}

public class DaoImpl implements Dao {
    @Override
    public void insert() { System.out.println("DaoImpl.insert()"); }
    @Override
    public void delete() { System.out.println("DaoImpl.delete()"); }
    @Override
    public void update() { System.out.println("DaoImpl.update()"); }
}

public class ServiceImpl {
    private Dao dao = new DaoImpl();
    public void insert() {
        System.out.println("insert() start:" + System.currentTimeMillis());
        dao.insert();
        System.out.println("insert() end:" + System.currentTimeMillis());
    }
    public void delete() { dao.delete(); }
    public void update() {
        System.out.println("update() start:" + System.currentTimeMillis());
        dao.update();
        System.out.println("update() end:" + System.currentTimeMillis());
    }
}

The raw implementation suffers from two main problems: the timing logic cannot be reused across methods, and every new Dao implementation would require a new wrapper class, causing class explosion.

Decorator pattern

public class LogDao implements Dao {
    private Dao dao;
    public LogDao(Dao dao) { this.dao = dao; }
    @Override
    public void insert() {
        System.out.println("insert() start:" + System.currentTimeMillis());
        dao.insert();
        System.out.println("insert() end:" + System.currentTimeMillis());
    }
    @Override
    public void delete() { dao.delete(); }
    @Override
    public void update() {
        System.out.println("update() start:" + System.currentTimeMillis());
        dao.update();
        System.out.println("update() end:" + System.currentTimeMillis());
    }
}

Using the decorator makes the timing logic transparent to callers and avoids class explosion, but the logging code is still tightly coupled to the methods and must be modified for each new method.

Java dynamic proxy

public class LogInvocationHandler implements InvocationHandler {
    private Object obj;
    public LogInvocationHandler(Object obj) { this.obj = obj; }
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        String name = method.getName();
        if ("insert".equals(name) || "update".equals(name)) {
            System.out.println(name + " start:" + System.currentTimeMillis());
            Object result = method.invoke(obj, args);
            System.out.println(name + " end:" + System.currentTimeMillis());
            return result;
        }
        return method.invoke(obj, args);
    }
}

public static void main(String[] args) {
    Dao dao = new DaoImpl();
    Dao proxyDao = (Dao) Proxy.newProxyInstance(
        LogInvocationHandler.class.getClassLoader(),
        new Class<?>[]{Dao.class},
        new LogInvocationHandler(dao));
    proxyDao.insert();
    proxyDao.delete();
    proxyDao.update();
}

The proxy reuses the timing logic for any interface method, but JDK proxies only work for interfaces and still require code changes to handle additional methods such as delete.

CGLIB proxy

public class DaoProxy implements MethodInterceptor {
    @Override
    public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
        String name = method.getName();
        if ("insert".equals(name) || "update".equals(name)) {
            System.out.println(name + " start:" + System.currentTimeMillis());
            proxy.invokeSuper(obj, args);
            System.out.println(name + " end:" + System.currentTimeMillis());
            return obj;
        }
        proxy.invokeSuper(obj, args);
        return obj;
    }
}

public static void main(String[] args) {
    DaoProxy daoProxy = new DaoProxy();
    Enhancer enhancer = new Enhancer();
    enhancer.setSuperclass(DaoImpl.class);
    enhancer.setCallback(daoProxy);
    Dao dao = (Dao) enhancer.create();
    dao.insert();
    dao.delete();
    dao.update();
}

CGLIB can proxy concrete classes, solving the limitation of JDK proxies, but the code to set up the proxy is still somewhat cumbersome and the timing logic remains coupled to the interceptor.

Spring AOP

public class TimeHandler {
    public void printTime(ProceedingJoinPoint pjp) throws Throwable {
        MethodSignature ms = (MethodSignature) pjp.getSignature();
        Method method = ms.getMethod();
        System.out.println(method.getName() + " start:" + System.currentTimeMillis());
        try {
            pjp.proceed();
            System.out.println(method.getName() + " end:" + System.currentTimeMillis());
        } catch (Throwable e) { }
    }
}

The corresponding aop.xml config defines a pointcut that matches all methods of Dao and applies printTime before and after execution. Spring handles the proxy creation, allowing developers to focus solely on the aspect logic.

Advantages of AOP highlighted include reusable aspect code, removal of manual proxy creation, and decoupling of cross‑cutting concerns from business logic.

Practical AOP use cases

Automatic transaction commit and SqlSession cleanup after insert, update, or delete operations.

Permission checking in web controllers by intercepting the handleRequest method, extracting the user ID and request URI, and throwing an exception when access is denied.

Both examples demonstrate how AOP can encapsulate repetitive tasks (logging, transaction management, security) without polluting business code, and how the same aspect can be reused across many components.

In conclusion, the article shows the step‑by‑step evolution from raw Java code to a full Spring AOP solution, explaining the pros and cons of each approach and providing concrete code snippets for readers to follow.

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.

Design PatternsJavaProxyaopspringDecoratorcglib
Java Captain
Written by

Java Captain

Focused on Java technologies: SSM, the Spring ecosystem, microservices, MySQL, MyCat, clustering, distributed systems, middleware, Linux, networking, multithreading; occasionally covers DevOps tools like Jenkins, Nexus, Docker, ELK; shares practical tech insights and is dedicated to full‑stack Java development.

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.