Applying Java Dynamic Proxy to Simplify MVP Null Checks and Module Decoupling

This article explains the fundamentals of Java dynamic proxy, demonstrates how to use Proxy and InvocationHandler to eliminate repetitive null‑checks in MVP presenters and to create flexible, decoupled module interactions, providing complete code examples and practical insights.

Liulishuo Tech Team
Liulishuo Tech Team
Liulishuo Tech Team
Applying Java Dynamic Proxy to Simplify MVP Null Checks and Module Decoupling

This article introduces the Java dynamic proxy pattern, highlighting its ability to increase code flexibility, and presents two practical examples: reducing repetitive null‑checks in MVP presenters and achieving more flexible, pure decoupling between modules.

Dynamic proxy relies on two core classes, Proxy and InvocationHandler. The proxy object can only implement Interface, not a concrete Class or Abstract Class, because all generated proxy classes inherit from Proxy, and Java supports only single inheritance.

Below is a minimal demonstration where an interface A is implemented by AIpml, a proxy handler AProxy intercepts method calls, and a proxy instance is created via Proxy.newProxyInstance. Running the program prints the invocation log followed by the original method output.

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;

public class DemonstrationProxy {

    interface A {
        void method();
    }

    static class AIpml implements A {
        @Override
        public void method() {
            System.out.println("method in AIpml");
        }
    }

    // Dynamic proxy handler
    static class AProxy implements InvocationHandler {
        final Object origin;
        AProxy(Object origin) { this.origin = origin; }
        @Override
        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
            System.out.println("start to invoke method " + method.getName() + " proxy is " + proxy.getClass().getCanonicalName());
            return method.invoke(origin, args);
        }
    }

    public static void main(String[] args) {
        final A a = new AIpml();
        final InvocationHandler handler = new AProxy(a);
        final A proxyA = (A) Proxy.newProxyInstance(a.getClass().getClassLoader(), a.getClass().getInterfaces(), handler);
        proxyA.method();
    }
}

Output:

start to invoke method proxy is $Proxy0
method in AIpml

In MVP architecture, the Presenter holds a reference to a View and frequently checks if (getView() != null) to avoid NullPointerExceptions after the View is destroyed. By returning a dynamic proxy view when the real view is unavailable, the Presenter can call getView() without null checks, keeping the code clean and safe.

public class AbsPresenter<View extends IView> implements IPresenter {

    private View mView;
    private Class<? extends IView> mViewClass;

    public AbsPresenter(@NonNull View iView) {
        this.mView = iView;
        this.mViewClass = iView.getClass();
        if (this.mViewClass.getInterfaces().length == 0) {
            throw new IllegalArgumentException("iView must implement IView interface");
        }
    }

    public void detach() { this.mView = null; }

    public @NonNull View getView() {
        if (mView == null) {
            return ViewProxy.newInstance(mViewClass);
        }
        return mView;
    }

    private static final class ViewProxy implements InvocationHandler {
        public static <View> View newInstance(Class<? extends IView> clazz) {
            return (View) Proxy.newProxyInstance(clazz.getClassLoader(), clazz.getInterfaces(), new ViewProxy());
        }
        @Override
        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
            Class type = method.getReturnType();
            if (type == boolean.class) return false;
            else if (type == int.class) return 0;
            else if (type == short.class) return (short)0;
            else if (type == char.class) return (char)0;
            else if (type == byte.class) return (byte)0;
            else if (type == long.class) return 0L;
            else if (type == float.class) return 0f;
            else if (type == double.class) return 0D;
            else return null;
        }
    }
}

For module decoupling, a shared ModuleProvider supplies interfaces of various functional modules. Previously, each missing module required an explicit empty implementation, leading to duplicated code. By using a dynamic proxy that returns default values, the provider can generate a proxy instance on demand, eliminating the need for separate empty classes.

public class EmptyModuleProxy implements InvocationHandler {
    public static <T> T newInstance(Class<T> clazz) {
        return (T) Proxy.newProxyInstance(clazz.getClassLoader(), new Class[]{clazz}, new EmptyModuleProxy());
    }
    @Override
    public Object invoke(Object o, Method method, Object[] objects) throws Throwable {
        // Check for custom annotations that specify return values
        SpecifyClassValue classAnn = method.getAnnotation(SpecifyClassValue.class);
        if (classAnn != null) return classAnn.returnValue();
        SpecifyIntegerValue intAnn = method.getAnnotation(SpecifyIntegerValue.class);
        if (intAnn != null) return intAnn.returnValue();
        SpecifyStringValue strAnn = method.getAnnotation(SpecifyStringValue.class);
        if (strAnn != null) return strAnn.returnValue();
        SpecifyBooleanValue boolAnn = method.getAnnotation(SpecifyBooleanValue.class);
        if (boolAnn != null) return boolAnn.returnValue();
        return defaultValueByType(method.getReturnType());
    }
    private Object defaultValueByType(Class type) {
        if (type == boolean.class) return false;
        else if (type == int.class) return 0;
        else if (type == short.class) return (short)0;
        else if (type == char.class) return (char)0;
        else if (type == byte.class) return (byte)0;
        else if (type == long.class) return 0L;
        else if (type == float.class) return 0f;
        else if (type == double.class) return 0D;
        else return null;
    }
}

Consequently, ModuleProvider.getB() can be implemented as:

public static B getB() {
    if (b == null) {
        b = EmptyModuleProxy.newInstance(B.class);
    }
    return b;
}

Custom annotations such as @SpecifyBooleanValue(returnValue = true) allow developers to define specific return values for methods when a module is absent, facilitating testing without altering production code.

In summary, the two real‑world examples demonstrate how Java dynamic proxy can dramatically improve code flexibility, reduce boilerplate, and enable clean decoupling; the technique is also employed by libraries like Retrofit to turn service interfaces into executable network calls.

The article concludes with a hiring notice for mobile developers at the Fluently Speaking technology team.

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-patternsJavaBackend DevelopmentDynamic ProxyMVPmodule decoupling
Liulishuo Tech Team
Written by

Liulishuo Tech Team

Help everyone become a global citizen!

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.