Learn Computing from the Experts | The Rheinwerk Computing Blog

The Proxy Pattern in Spring

Written by Rheinwerk Computing | Feb 5, 2025 2:00:00 PM

What can be vividly explained using rings, we want to translate into the world of software engineering with a sequence diagram.

 

The ring illustrates the proxy pattern, which is a well-known design pattern. In the core shown in the figure below is the target object. This target object has an interface, let’s call it I, with operations. The proxy object (aka substitute) wraps around this target object. It has the same interface I to the outside as the target object, which is also called the subject. If a client has a reference to “something of type I,” the client doesn’t know whether it’s talking to the proxy or to the target object.

 

 

A proxy doesn’t sound exciting, but it can do two useful things:

  • Before the proxy goes to the target object with the operation, it can perform preprocessing. That is, the proxy can call internal methods and modify the parameters. Then, the proxy can go to the target object or block the call.
  • When the target object returns the result to the proxy, the proxy can perform postprocessing. For example, it can cache the data. Later, the proxy returns the result to the client. 

Note: With the corresponding Class object, you can find out whether you have a proxy object in front of you or not. The toString() method also responds differently. Spring includes in org.springframework.aop.support.AopUtils the helper method isAopProxy(…) for Spring proxies:

 

public static boolean isAopProxy(@Nullable Object object) {

   return

      (object instanceof SpringProxy &&

         (Proxy.isProxyClass(object.getClass()) ||

      object.getClass().getName().contains(

      ClassUtils.CGLIB_CLASS_SEPARATOR)));

}

 

When implementing equals(…), it must be taken into account in places that comparisons are programmed with instanceof instead of getClass(…).

 

Proxy Deployment in Spring

In general, a proxy is limited in its abilities and is incapable of modifying the behavior of the target operation. However, the preprocessing and post-processing capabilities of a proxy are frequently sufficient for a variety of use cases. Consequently, Spring employs proxies in numerous contexts. On several occasions, the code is injected with a proxy, which often goes unnoticed because the proxy has the same interface as the target object.

 

Following are a few examples of how Spring uses proxies:

  • Transaction management: Methods can initiate new transactions, and everything the method does to the database takes place in a transactional context. If there are exceptions in the method, the proxy catches those exceptions and starts a rollback; if there are no errors, then a commit follows at the end of the method.
  • Caching: If a client calls a method with a certain argument, a proxy can determine that the method has seen the argument before and can return the result computed earlier.
  • Validation: The purpose of validation is to ensure that methods are called only with valid values. A proxy can check that the arguments are in the correct value ranges and pass them to the target object only if they are valid.
  • Asynchronous calls: Normally, a method is called synchronously (blocking). A special proxy can start a background thread—or fall back to a thread of a thread pool—and then asynchronously process this operation.
  • Spring Retry project: Aborts can occur—especially with remote accesses; the Spring Retry project can use a proxy that starts another attempt if a target object’s operation fails, until the call finally succeeds or you give up.

To better comprehend how Spring operates with proxies, let’s translate the concept into Java source code.

 

The proxy pattern, programmed in code, has the following basic form:

 

class Subject {

   public String operation( String input ) { return input; }

}

 

class Proxy extends Subject {

   private final Subject subject;

 

   Proxy( Subject subject ) { this.subject = subject; }

 

   @Override public String operation( String input ) {

 

      // Preprocess the input

 

      String result = subject.operation( input );

 

      // Postprocess the result

 

      return result;

   }

}

 

The proxy must be connected to the target object. This is how the constructor stores the reference.

 

What is symbolic here in Java code is automatically generated at runtime in the Spring Framework.

 

Dynamically Generate Proxies

The Spring Framework can generate proxies at runtime; this is why we refer to this form of proxy as dynamic proxies. The Spring Framework uses two techniques for this purpose.

  • The Java Development Kit (JDK) can build dynamic proxies; these are called JDK dynamic proxies. In this case, there is a limitation because Java SE can only realize proxies for interfaces—this isn’t possible for classes.
  • When Spring generates a proxy for classes, the framework makes use of the library cglib (short for Byte Code Generation Library). This Byte Code Generation Library is relatively old and no longer maintained, so the Spring team has heavily patched the version to make it fit for the current Java versions.

Rules for Target Objects

If proxies are to work well, then the classes must take some rules into account. If Spring builds proxy objects for classes, then a library creates bytecode for a subclass. It follows that the superclass (the subject) must not have a private constructor. This is clear because a subclass always calls the constructor of the superclass, and if the superclass doesn’t have a visible constructor, the constructor can’t be called. Therefore, the constructors in the subject must not be private.

 

If Spring implements a proxy through a subclass, as we did in our own proxy example with the types Proxy and Subject in the previous section, then the proxy must override the method from the subject. Therefore, the method must not be final; otherwise, it could not be overridden.

 

In principle, all methods for which a proxy is generated should be public. Spring relies on its own aspect-oriented programming (AOP) framework, which requires public methods. Although all of these restrictions can be circumvented, then the bytecode must be modified via AspectJ. We’ll only use the “usual” way here.

 

Besides the preceding rules, there are a few limitations if the framework is creating proxy objects. There is one thing to watch out for: A proxy can only do its job if it sits between the client and the subject. If the subject calls methods among themselves in their own code, then it won’t go through the proxy. This can quickly lead to a problem, for example, when refactoring.

 

To illustrate the issue at hand, let’s consider the example of a proxy class named TrimmingProxy. When parameter variables are annotated with @Trim, the TrimmingProxy class removes any leading or trailing white spaces from these strings before passing them to the target object. Here is an example usage of the TrimmingProxy:

 

IntParser proxy = new TrimmingProxy( new IntParser() );

proxy.parseInt( " 12423\t " );

 

Now let’s look at the code of the IntParser class with the method:

 

class IntParser {

   public int parseInt( String input ) {

      return parseInt( input, 10 );

   }

   public int parseInt( @Trim String input, int radix ) {

      return Integer.parseInt( input, radix );

   }

}

 

If the proxy pays attention to @Trim, it will only be activated when parseInt(String, int) is called directly; otherwise, it won’t be activated. Our example calls parseInt( String), and the parameter variable has no @Trim, so the proxy won’t remove white space either. If parseInt(String) later calls parseInt(String, int), it happens inside the target object, and the proxy isn’t involved.

 

Note: In practice, this is a mistake that is often made, so you must always remember that a proxy always goes into the core “from the outside” and only then does its job.

Build Proxies Yourself with ProxyFactory *

Spring provides the class ProxyFactory with which we can also build proxy objects. Here’s an example: A proxy object is to be created for a java.util.List. On this particular list, add(null) is to be ignored:

 

ProxyFactory factory = new ProxyFactory( new ArrayList<>() );

MethodInterceptor methodInterceptor = invocation ->

   (invocation.getMethod().getName() == "add"

   && isNull( invocation.getArguments()[ 0 ] )) ? false

                                                : invocation.proceed();

factory.addAdvice( methodInterceptor );

 

In the constructor of ProxyFactory, we pass the subject, an ArrayList. The goal is that ProxyFactory generates a proxy, and we pass certain methods to the list in the background.

 

The proxy intercepts every method call. It’s therefore elementary to configure how the proxy should behave on which method and arguments. A MethodInterceptor is helping us; the functional interface is declared as follows:

 

public interface MethodInterceptor extends Interceptor {

   @Nullable

   Object invoke(@Nonnull MethodInvocation invocation) throws Throwable;

}

 

If a method is called on the proxy from the outside, it passes the call to the configured MethodInterceptor and calls the invoke(…) method. In other words, no matter which method is accessed from the outside, it will always map to an invoke(…) method. The MethodInvocation parameter is so important because the object contains information about which method was called with which arguments.

 

Our implementation first tests with invocation.getMethod().getName() == "add" if the method add was really called. Because the strings of the method names are internal, it’s not a problem to use == for the comparison; however, this is rare. The subsequent check with isNull( invocation.getArguments()[ 0 ] ) checks if null was passed to the add(…) method. There are two outputs for the comparisons:

  • add(null) has been called. In this case, we return a boolean value because this is also the return type of the add(…) method. Returning false expresses that no change was made to the data structure. We don’t pass anything to the target objet. Although invoke(…) only returns Object, it’s important to make sure that the return type matches that of the target object in the method.
  • If add(null) was not called, so either another method or add(…) with another value than null, then the proxy goes to the target object with invocation.proceed(), and what the subject returns, we return at invoke(…).

With this code, the MethodInterceptor is implemented. After it’s passed into addAdvice(…), we can obtain and use the proxy object from the ProxyFactory. Here’s an example:

 

List list = (List) factory.getProxy();

list.add( "One" );

System.out.println( list );

list.add( null );

list.add( "Two" );

System.out.println( list );

 

The outputs are as follows:

 

[One]

[One, Two]

 

Having discussed home-coded proxies, let’s now examine some standard proxies provided by the Spring Framework.

 

 

Editor’s note: This post has been adapted from a section of the book Spring Boot 3 and Spring Framework 6 by Christian Ullenboom.