Java Dynamic proxy mechanism and how Spring is using it
In this post, we will talk about Proxy pattern and how you can implement it using the JDK-provided-mechanism, Dynamic Proxies. It is one of the ways Spring Framework is handling cross-cutting concerns, like transactions (@Transactional), caches (@Cacheable) and so on.
1. Introduction
Proxy pattern, as described in Gang of Four, “provides a placeholder interface to an underlying object to control access, reduce cost or reduce complexity”. In simpler terms, it acts as a surrogate.
There are multiple ways of creating proxies:
- Using plain java objects, if you know the interface beforehand (a good example can be found here). These are static proxies because you know at compile time the types of all involved objects.
- Using a byte-code generation/manipulation library (e.g. cglib, javassist). Used primarily when the target (class to be proxied) does not implement any interface. One of its limitations is that the proxy target must not be a
final
class, as this approach uses inheritance to generate the proxy. - Using JDK Dynamic Proxies which creates implementations of Java
interface
s at runtime using Reflection.
The latter two are dynamic proxies because they work without knowing at compile time the types to be proxied but they leverage reflection to determine it at runtime.
The following sections will explain how to implement a proxy using JDK Dynamic proxy mechanism. Full code can be found on Github.
2. Create a dynamic proxy
Let’s assume that we want to implement an app that plays audio files, records how many times each file was played and stores this info in a database. We will create a proxy that takes care of the transactional behaviour we need in the interaction with the database.
2.1 Define the interface
First, we will define the interfaces we need for building our app. Remember that JDK dynamic proxies mechanism creates implementations based on interfaces so these are a must.
Nothing out of the ordinary here, just a plain Java interface. An audio file can be played from the beginning, from a specific point, or only a portion of the file. When playing, you can also jump to a specified location (seek audio file). Let’s create an interface for that, too.
Now that we have our interfaces ready, we will start creating the service which will contain our business logic.
2.2 Creating the service
For this example, we will create a simple service whose methods will return just the description of the invoked action.
To be more Spring-like, let’s create some annotations which will help to add transactional behaviour.
Nothing new here, just a simple annotation, which we will apply to our methods. It’s a simpler version of Spring’s @Transactional
. We’ll also create a @TransactionalService
annotation (similar to Spring's@Service
).
Applying the above annotations to our service, we end up with:
2.3 Creating a proxy
To create a proxy is easy, you just invoke the newProxyInstance
static method of java.lang.reflect.Proxy
class with 3 arguments:
public static Object newProxyInstance(ClassLoader loader, Class<?>[] interfaces, InvocationHandler h)
For example:
// object to be proxied
PlayerService service = new PlayerService()// Create the proxy
Object proxy = Proxy.newProxyInstance(getClass().getClassLoader(),
service.getClass().getInterfaces(),
new MyInvocationHandler(service));
The InvocationHandler#invoke
method will be executed when any of the methods defined in the interfaces returned by service.getClass().getInterfaces()
will be invoked. Below is a simple example of a handler which prints the execution time of each method.
2.4 Creating a proxy factory
For a better display of how is Spring leveraging this mechanism, we will create a proxy factory which, using reflection, will scan a specified package for classes annotated with @TransactionalService
. For those classes, we are going to create proxies and keep them in a bean registry.
For scanning the classpath, we are going to employ an easy to use library called Reflections:
Reflections reflections = new Reflections(packageToLookup); Set<Class<?>> transactionalServiceClasses = reflections.getTypesAnnotatedWith(TransactionalService.class);
We need an instance of each of those classes to be our proxy targets and we are going to use the no-arg constructor to do create them:
Now that we have our proxy targets, we can create the actual proxies:
MyTransactionInvocationHandler
implements InvocationHandler
and takes care of opening and closing transactions for the annotated methods. To do so, we must check if the invoked method is annotated with @MyCustomTransaction
. The tricky part is that the method passed to InvocationHandler#invoke
is the one defined in the interface, but the annotation is on the actual class that implements it.
We need to get the actual method from our target:
Now, if the returned method is annotated with @MyCustomTransaction
, we can trigger the custom transactional logic. Otherwise, we just invoke the same method on the target (pass-through). The handler looks like this
Going back to our createProxy
method from ProxyFactory
(this one), we notice that we are returning an instance of MyCustomProxy
. This is just a container-class which includes the proxy itself (returned by Proxy#newProxyInstance
) and the interfaces this proxy can handle.
This container-class helps us to return the correct proxy for a specified interface (in a type-safe manner).
To recap, our ProxyFactory
will scan the classpath for a specific package, get all classes annotated with @TransactionalService
, instantiate them using no-arg constructor and create a proxy for each one. These proxies will be stored in a bean registry (List<MyCustomProxy> beanRegistry
) and using getBean(Class<T> clazz)
, we will get the proxy for a specified type.
To see how it works, we are going to create amain
method and invoke the methods defined in our interfaces:
In the output we can see that for the methods annotated with @MyCustomTransaction
, our handler gets invoked and we have transactional behaviour (opening and committing transaction). For seekTo
there is no transactional behaviour as it is not annotated.
business.play(file.getPath())
Opening transaction [default MyTransaction] with params [SongPath]
Committing transaction default MyTransaction...
Playing song SongPathbusiness.play(file)
Opening transaction [file] with params [SongPath]
Committing transaction file...
Playing song SongPathbusiness.play(file, 10)
Opening transaction [starting at] with params [SongPath, 10]
Committing transaction starting at...
Playing song SongPath starting at 10business.play(file, 10, 15)
Opening transaction [interval] with params [SongPath, 10, 15]
Committing transaction interval...
Playing song SongPath starting at 10 till 15seekablePlayer.seekTo(20)
Seek to 20
3. Conclusion
In this post, we discussed a bit about Proxy Pattern, ways to create a proxy in Java and showed an example of how JDK Dynamic Proxy works and how it is used in Spring Framework. It is a very powerful way to handle cross-cutting concerns. Full code can be found on Github.