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:
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(…).
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:
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.
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.
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.
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:
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.