Proxycian
Small Java library for generating dynamic proxies on top of ByteBuddy or Javassist. You can generate data transfer objects, rich traits or even whole implicit DAO implimentations dynamically at runtime easily. This library solves the complex stuff so you can focus on application logic. Serializability, cloning are already solved by us. We also aim for transparent and easily debuggable proxies, because as we know proxies is usually part of "magic" for the team. Hence the name of this library - Proxycian as a magician for the proxies ;)
Install / Use
/learn @FgForrest/ProxycianREADME
Proxycian
A small Java library for generating dynamic proxies on top of ByteBuddy or Javassist. You can generate data transfer objects, rich traits or even whole implicit DAO implementations dynamically at runtime easily. This library solves the complex stuff, so you can focus on application logic. Serializability and cloning are already solved by us. We also aim for transparent and easily debuggable proxies, because as we know proxies is usually part of "magic" for the team. Hence, the name of this library - Proxycian as a magician for the proxies ;)
History
Developers in FG Forrest use dynamic proxies successfully for over 10 years. Our initial implementation was based on the Spring framework abstractions, but we quickly realized that their implementation is overly complex and prone to subtle errors leading to memory leaks as well as being poorly observable / debuggable. Our second implementation took advantage of JBoss Javassist but it still kept many unnecessary abstractions and principles we learned from the Spring implementation.
This implementation is our third take on proxies that is the leanest and the most opinionated one so far. It was designed from scratch and based on current and actively developed libraries and with the emphasis on:
- simplicity
- clear and transparent classes / method implementation caching
- debuggability - you just want your debugger to step in the dynamic implementation without much fuss around
- transparency - you can easily find out why the library chose the implementation it chose
Let us know if we achieved our goals or not. Opinions and feedback is welcome.
What we do solve with Proxycian
Let's see a few practical examples on what you can do with Proxycian:
1. stateful and dynamic traits
Java doesn't have multiple inheritance, but you can imitate it to certain level with default methods on interfaces, but there will be always limitations. Classes might implement multiple interfaces, but these interfaces can't have fields and keep data in them. Also, you cannot decide which traits your class will have in runtime.
Proxycian allows us to create self-sustainable traits keeping both logic and data that don't require any orchestration or cooperation with the main class which they are attached to. Also, we can easily create specialized class with set of traits selected in runtime, usually on some text-based configuration or user interaction with the application.
2. interception / delegation
We use Proxycian to wrap external classes - such as Spring ReloadableResourceBundle or JDBC DataSource to intercept calls in the development environment to provide information for the developers - reporting which message codes / messages were used while rendering the page, or which (and how many) SQL queries were executed through the DataSource.
There are many use-cases where the interception might come handy. You can make a class wrapping original and delegate method calls to it by hand, but it quickly gets incomprehensible and maintaining is costly. AOP with dynamic proxies offer a more clever and shorter way to achieve the same.
3. mocking contracts
There are situations when your application works with some interface for which the real implementation is not yet known, but eventually it will be resolved. In our modular system the modules export and require some interfaces. In order to fulfill them we needed to configure modules, how they depended on each other. This was the tricky for part of our developers and soon there were situations when two modules might have needed each other (circular dependency). This is a sign of poorly architected modules, but life brings situations when a circular dependency might resemble the least of all evils.
Proxycian allows us to create a dynamic proxy, which implements required interfaces and use it for immediate wiring by the dependency injection mechanism. The module has its requirement fulfilled and can be started. Any call to the proxy method will end with an exception, but we usually need to call method after entire system starts. As soon the other module that provides the interface starts, the proxy internal state is filled with a reference to the implementation and delegates each method call to it.
4. implicit DAO/Service interface implementation
Do you know Ruby's Active Record or Spring Data libraries? You can easily implement your own using Proxycian. It's matter of a few lines of code.
Prerequisites
- JDK 1.8, 11 or 17 (all are supported with same JARs)
- Log4J 2 (2.17+)
- Apache Commons Lang 3 (3.12+)
- ByteBuddy / Javassist are bundled in our library, there will be no conflict with possible existing libraries on your classpath in different version
- Configured toolchains:
<?xml version="1.0" encoding="UTF-8"?>
<toolchains>
<toolchain>
<type>jdk</type>
<provides>
<version>8</version>
<vendor>oracle</vendor>
</provides>
<configuration>
<jdkHome>/opt/Java/jdk8u262-b10/bin/java</jdkHome>
</configuration>
</toolchain>
<toolchain>
<type>jdk</type>
<provides>
<version>11</version>
<vendor>oracle</vendor>
</provides>
<configuration>
<jdkHome>/opt/Java/jdk-11.0.8/bin/java</jdkHome>
</configuration>
</toolchain>
<toolchain>
<type>jdk</type>
<provides>
<version>17</version>
<vendor>oracle</vendor>
</provides>
<configuration>
<jdkHome>/opt/Java/jdk-17.0.2/bin/java</jdkHome>
</configuration>
</toolchain>
</toolchains>
How to compile
Currently, we are supporting default flow with JDK 1.8 and Multi-Release JAR
Use standard Maven 3 command to compile for multi-jar:
mvn clean install
To compile for JDK 1.8, 11 and 17 with specific profile use specific command:
mvn clean install -Pmulti-jar
Profiles for specific JVM versions are created too.
mvn clean install -Pjava8
mvn clean install -Pjava11
mvn clean install -Pjava17
How to run tests
Run your tests in an IDE or run:
mvn clean verify -Pjava8
mvn clean verify -Pjava11
mvn clean verify -Pjava7
Running tests with specific JVM are possible via commands test or verify, with standard JUnit runner the Multi-Release JAR is ignored and the default JDK 1.8 class is executed.
Help us maintain at least 80% code coverage!
How to verify authenticity
Download our PGP public key and verify via PGP verify Maven plugin. See this article to find out why.
How to use
How to integrate to your application
Include the Proxycian library in your Maven descriptor (pom.xml):
<dependency>
<groupId>one.edee.oss</groupId>
<artifactId>proxycian_bytebuddy</artifactId>
<version>1.3.0</version>
</dependency>
Or Gradle:
dependencies {
compile 'one.edee.oss:proxycian_bytebuddy:1.3.0'
}
Or use proxycian_javassist if you prefer this implementation (it has much smaller memory JAR size). Otherwise,
ByteBuddy is preferred implementation because it's actively maintained and supports the newest JDK version.
How to generate a dynamic proxy class
Note: In this documentation we stick to ByteBuddy implementation in examples, but you can easily translate all of them
to Javassist implementation by replacing word ByteBuddy with Javassist. The contracts are identical in Proxycian.
To create new a class with the requested contracts, just use:
final Class<?> theInstance = ByteBuddyProxyGenerator.getProxyClass(
Person.class,
Trait1.class,
Trait2.class
);
If you call it for the first time, a new class extending Person.class and implementing Trait1.class and Trait2.class
is created for you. If you call it second time, you'll receive the previously created (cached) class with that contract.
The cache is kept in the static field of the generator and might be anytime cleared by
calling ByteBuddyProxyGenerator.clearClassCache().
If you want to extend some class, it must be stated as the first class of the proxy contract, but you might also create
proxies based on a bunch of interfaces and no superclass (then the java.lang.Object becomes the superclass of the proxy).
You can also create proxies based on superclasses without a default constructor (i.e. having only one constructor with one or
more arguments). Imagine that the Person.class has the only constructor protected Person(String firstName, String lastName):
final Class<?> theInstance = ByteBuddyProxyGenerator.getProxyClass(
new Class<?>[]{
Person.class,
Trait1.class,
Trait2.class
},
new Class<?>[]{
String.class,
String.class
}
);
You can also specify a classloader that will maintain the created class, but this is usually not necessary. Proxycian
uses by default the same classloader that loads ByteBuddyProxyGenerator.class itself.
But this is not the way Proxycian was meant to be used - read the next chapter for a general usage scenario.
How to generate a dynamic proxy instance
Creating classes is not the common way how y
