Change is the only constant thing and according to Darwin’s Theory of Survival of the fittest, the individual who adapts to the evolving world will sustain itself in the long run. Looks like this theory is true for Java also, as it is continuously evolving and serving for more than two decades. From JDK 1.0 to Java 17 (LTS) it has come a long way. Java 17 is the latest long-term support (LTS) release for the SE platform.
Friday, April 8, 2022
Top 5 Features of Java 17 That You Must Know
Wednesday, April 6, 2022
Host a dark service with Java, Spring Boot, and OpenZiti
Hide your application services to protect them against hackers.
This article’s headline mentions hosting a dark service. Huh? Why would you want your application services to be dark?
Open ports are everywhere. You need open ports, right? How are your users and applications going to connect to your services if there are no open ports? Well, they can still connect—but only if they are trusted.
A dark service is a service that’s had zero trust (ZT) principles applied across the board and, therefore, not only to the network layer. Indeed, the principles of ZT access are baked directly into the application itself.
Dark services have no listening ports, taking them off the internet (and even off the local network) where port scanners and nefarious actors are only an IP hop away.
What is zero trust?
The zero trust phrase has been thrown around a lot lately, but what does it really mean? Here’s how the concept is defined by NIST’s Computer Security Resource Center.
Zero trust (ZT) is the term for an evolving set of cybersecurity paradigms that move defenses from static, network-based perimeters to focus on users, assets, and resources. A zero trust architecture (ZTA) uses zero trust principles to plan industrial and enterprise infrastructure and workflows. Zero trust assumes there is no implicit trust granted to assets or user accounts based solely on their physical or network location (i.e., local area networks versus the internet) or based on asset ownership (enterprise or personally owned). Authentication and authorization (both subject and device) are discrete functions performed before a session to an enterprise resource is established. Zero trust is a response to enterprise network trends that include remote users, bring your own device (BYOD), and cloud-based assets that are not located within an enterprise-owned network boundary. Zero trust focuses on protecting resources (assets, services, workflows, network accounts, etc.), not network segments, as the network location is no longer seen as the prime component to the security posture of the resource.
In practice, you can boil this down to five basic tenants that apply to everything on your network, including both human users and automated processes.
◉ Deny access by default. Nobody can connect to the network unless you let them.
◉ Authorize on connect. Only allow connections to the network that use a strong, well-defined and known identity.
◉ Use explicit access grants. Even if they can connect, actors cannot do anything unless you let them. Limiting access prevents things such as traversal attacks and information leakage.
◉ Enforce least privilege access. Granting only permissions that are absolutely required for an actor to complete its tasks limits the damage that can be done when an account is compromised.
◉ Constantly monitor for security compliance. Zero trust is not just about the initial setup. It includes constantly monitoring policies, connections, and permissions to ensure that ZT principles continue to be enforced over time.
ZT principles should not stop at the network
You can apply all the ZT principles to your network, but even after that your application is still listening on a port after that. If there is a port open, your application can be attacked. That’s why the concept of app embedded zero trust moves the edge of the network into the application. Bringing ZT into your app programmatically via an SDK has several benefits.
◉ No listening ports. Your application can become totally dark with no open ports. When there are no open ports, the application cannot be scanned or attacked from any random person on the network.
◉ Zero trust of the entire network. When ZT is in the application, it extends across all networks, including the Internet, local-area networks, and even the operating system. Not trusting the network operating system makes applications immune to network-based side-channel attacks from malicious actors or ransomware that’s trying to attack the application from a stolen or infected device.
◉ Direct access. Applications are accessed directly from clients; they are not discovered. If the client doesn’t know where the application is, it can’t access it.
◉ Portability. Once ZT is configured and becomes part of the architectural overlay, your application and your clients need only outbound, commodity internet.
◉ Encrypted data. Application data is encrypted from the client all the way to the server and back. ZT enforces this.
◉ Micro segmentation. Applications cannot talk to other applications unless explicitly authorized to do so.
In terms of portability, embedding ZT into an application can help in common use cases. If you want to change clouds or deploy your application into a new data center, no changes need to be made. Simply develop your app once and deploy it anywhere, and it just works. What about mobile clients that want access through a secure shell (ssh) from home, a client site, or the airport? No problem! It doesn’t matter where the client is; trusted connectivity works from anywhere.
How is this achieved? When using OpenZiti, every endpoint (SDK for in-app access, tunnelers for the operating system or edge routers for the network) must have an identity with provisioned certificates. The certificates allow Ziti to perform authentication and authorization before any data flows across secure communications channels. These endpoints reach out of the private network to talk to the controller (control plane) and make connections to join the network fabric mesh (data plane). Therefore, services and endpoints in your private networks only make outbound connections—and thus, no holes are opened for inbound traffic.
An example of app with embedded zero trust
This example will use OpenZiti to provide the ZT overlay network and application SDKs. Spring Boot and Tomcat will host the service. The project will take about half an hour. You will need the following:
◉ Your favorite text editor or IDE
◉ JDK 11 or later
◉ Access to a Linux environment with the bash shell—or, for Windows users, a VM or Windows Subsystem for Linux 2 (WSL 2)
If you don’t have access to a Linux environment but wish to use it, you can grab a Linux VM from the Oracle Cloud free tier.
What is OpenZiti? It’s an open source project sponsored by NetFoundry. Oracle embraces open source and zero trust security, so Oracle partnered with NetFoundry to provide zero trust connections to applications, including Java applications, running in Oracle Cloud Infrastructure (OCI). NetFoundry’s Edge Router software is staged within the OCI Marketplace for deployment within any OCI region.
Get the code. The example code can be downloaded from here. Alternatively, you can clone it using Git with the following shell command:
git clone https://github.com/netfoundry/openziti-spring-boot
As with most Spring guides, you can start from scratch and complete each step, or you can bypass basic setup steps that are already familiar to you. Either way, you end up with working code.
Create the test network. This example will use a very simple OpenZiti network, shown in Figure 1.
Host a dark service using Spring Boot
Create a sample Java client application
Tuesday, April 5, 2022
Difference between Function.andThen and Function.compose
Here are two different ways to mix functions in Java:
◉ using andThen
◉ using compose
It is important to understand the difference between the two.
andThen: function1.andThen(function2) will first apply function1 to the input and the result of this will be passed to the function2.
compose: function1.compose(function2) will first apply the input to the function2 and the result of this will be passed to the function1
When they are used for operations that are not commutative then you will end up with totally different results.
You can see that in the example below:
Function<Double, Double> half = (a) -> a / 2;
Function<Double, Double> twice = (a) -> a * a;
Function<Double, Double> squareAndThenCube = half.andThen(twice);
Double result = squareAndThenCube.apply(3d);
System.out.println(result);
Function<Double, Double> squareComposeCube = half.compose(twice);
result = squareComposeCube.apply(3d);
System.out.println(result);
The output for the above will be:
Monday, April 4, 2022
The Main Differences Between Java (Latest) and Before
Good morning, and welcome to my talk about the new features in the latest version of Java/<insert your language here>.
You’re going to notice many new things:
◉ We now support something unimportant in strings, like emojis
◉ We’ve rewritten an API you don’t use
◉ We’ve added some optional language features you may one day find useful
◉ We’ve deprecated something you were kind of dependent on
◉ We’ve introduced some subtle bugs that you may struggle to notice… at first…
◉ None of your build tools are going to support this properly for a while
I hope you enjoyed my talk. Have a nice day.
What do you mean “Get out of the TED studios”?
Recent Java 17 Fun
Java 17 is the latest LTS version of Java, and it’s probably a good idea to adopt it. However, some recent pains:
◉ Though Amazon has released Corretto 17, they don’t actually support Java 17 in:
◉ AWS Lambda Runtimes
◉ CodeBuild Runtimes
◉ We can work around this by NOT using language level 17 on Lambdas, and by using a docker image to build in CodeBuild… which adds more pain when you’re using docker in docker for testing:
docker run -v ~/.docker:/root/.docker -v /var/run/docker.sock:/var/run/docker.sock -e CODEARTIFACT_AUTH_TOKEN=${CODEARTIFACT_AUTH_TOKEN} -e SNYK_TOKEN=${SNYK_TOKEN} -e SONAR_TOKEN=${SONAR_TOKEN} -v $(pwd):/project -w=/project amazoncorretto:17 ./gradlew clean build snyk-test sonarqube -Dsonar.branch.name="${CURRENT_BRANCH}" -i
◉ You have to update Gradle, SpotBugs and Jacoco to latest to get things to work
◉ Once you’ve installed JDK 17 on your machine, you’re kind of committed to getting every build to be compatible with the runtime, otherwise you have to jump through hoops to do builds
◉ Without the latest SystemStubs, the environment variables hacks in tests are no longer possible
◉ There’s a date bug!
Java 17 Date Bug
We use a date parser in one of our projects. As luck would have it, one of our unit tests tried to parse the date 16-Sep-2020 and started to fail on JDK 17.
We could so easily have been testing with 16-Nov-2020 and not noticed the fact that in en-GB locales, Java 17 no longer supports Sep as an abbreviation for the date format MMM. This means that DateFormatter‘s parse method is effectively broken.
Aaaaagh!
Someone on StackOverflow explained this well enough that I could fathom a fix, which is to specialise the date formatter to Locale.ENGLISH which avoid the problem. However, it makes no sense to me at all that a MMM three character month abbreviation should randomly switch from 3 to 4 characters for a single month.
One argument is that Sept is a better abbreviation for the month in natural English. Another argument is that the first argument is talking out of its a-hole.
Source: javacodegeeks.com
Saturday, April 2, 2022
Docker Compose Java Healthcheck
Docker compose is often used to run locally a development stack. Even if I would recommend to use minikube/microk8s/… + Yupiik Bundlebee, it is a valid option to get started quickly.
One trick is to handle dependencies between services.
A compose descriptor often looks like:
docker-compose.yaml
version: "3.9" (1)
services: (2)
postgres: (3)
image: postgres:14.2-alpine
restart: always
ports:
- "5432:5432"
environment:
POSTGRES_USERNAME: postgres
POSTGRES_PASSWORD: postgres
my-app-1: (4)
image: my-app
restart: always
ports:
- "18080:8080"
my-app-2: (4)
image: my-app
restart: always
depends_on: (5)
- my-app-1
1. the descriptor version
2. the list of services (often containers if there is no replicas)
3. some external images (often databases or transversal services like gateways)
4. custom application images
5. dependencies between images
for web services it is not recommended having dependencies between services but it is insanely useful if you have a batch provisioning your database and you want it to run only when a web service is ready. It is often the case if you have a Kubernetes CronJob calling one of your Deployment/Service.
Previous descriptor works but it can happen the web service is not fully started before the second app (simulating a batch/job) is launched.
To solve that we need to add a healthcheck on the first app and depend on the state of the application in the batch. Most of the examples will use curl or wget but it has the drawback to be forced to add these dependencies – and their dependencies – to the base image – don’t forget we want the image to be light – a bit for the size but generally more for security reasons – so that it shouldn’t be there.
So the overall trick will be to write a custom main based on plain Java – since we already have a Java application.
Here is what can look like the modified docker-compose.yaml file:
"my-app-1:
...
healthcheck: (1)
test: [
"CMD-SHELL", (2)
"_JAVA_OPTIONS=", (3)
"java", "-cp", "/opt/app/libs/my-jar-*.jar", (4)
"com.app.health.HealthCheck", (5)
"http://localhost:8080/api/health" (6)
]
interval: 30s
timeout: 10s
retries: 5
start_period: 5s
my-app-2:
...
depends_on:
my-app-1:
condition: service_healthy (7)
1. we register a healthcheck for the web service
2. we use CMD-SHELL and not CMD to be able to set environment variables in the command
3. we force the base image _JAVA_OPTION to be resetted to avoid to inherit the environment of the service (in particular if there is some debug option there)
4. we set the java command to use the jar containing our healthcheck main
5. we set the custom main we will write
6. we reference the local container health endpoint
7. on the batch service, we add the condition that the application must be service_healthy which means we control the state with the /health endpoint we have in the first application (and generally it is sufficient since initializations happen before it is deployed)
Now, the only remaining step is to write this main com.app.health.HealthCheck. Here is a trivial main class:
package com.app.health;
import java.io.IOException;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import static java.net.http.HttpResponse.BodyHandlers.discarding;
public final class HealthCheck {
private HealthCheck() {
// no-op
}
public static void main(final String... args)
throws IOException, InterruptedException {
final var builder = HttpRequest.newBuilder()
.GET()
.uri(URI.create(args[0]));
for (int i = 1; i < 1 + (args.length - 1) / 2; i++) {
final var base = 2 * (i - 1) + 1;
builder.header(args[base], args[base + 1]);
}
final var response = HttpClient.newHttpClient()
.send(builder.build(), discarding());
if (response.statusCode() < 200 || response.statusCode() > 299) {
throw new IllegalStateException("Invalid status: HTTP " + response.statusCode());
}
}
}
Friday, April 1, 2022
Using Byte Buddy for proxy creation
With the increasing adoption of Java 17 and its strict encapsulation, several unmaintained libraries that rely on internal JVM APIs have stopped working. One of these libraries is cglib, the code generation library, which allows to create and load proxy classes during the runtime of a JVM process. And while there are alternatives to cglib that support Java 17, migration is not always straight-forward. To ease such migration, this article discusses how Byte Buddy can be used for proxy creation and what concept changes need to be considered during a migration.
General concept
Other than cglib, Byte Buddy does not offer an API that is dedicated to the creation of proxies. Instead, Byte Buddy offers a generic API for defining classes. While this might feel less convenient at first, it typically aids the evolution of existing code over time since the proxy class generation can be adjusted without constraints.
With Byte Buddy’s general API, a proxy is therefore created by defining a subclass of the targeted class, where all methods are overridden. Since Java methods are dispatched virtually, these overridden methods will be invoked instead of the original methods. In essence, cglib defines a proxy just like that.
As an example, consider creating a proxy of the following Sample class:
public class Sample {
public String hello() {
return "Hello World!";
}
}
This Sample class can be proxied with Byte Buddy by overriding the hello method. A simple way of implementing this override is by using a MethodDelegation. A method delegation requires a delegation target, typically a class that defines a single static method. To interact with the overridden method, the method declares parameters which are annotated with the expected behavior. As an example, consider the following delegation target which mimics the parameters of cglib’s MethodInterceptor:
public class Interceptor {
@RuntimeType
public static Object intercept(@This Object self,
@Origin Method method,
@AllArguments Object[] args,
@SuperMethod Method superMethod) throws Throwable {
return superMethod.invoke(self, args);
}
}
As the annotations’ names suggest, the method accepts the intercepted. This instance, a description of the Origin method, AllArguments to the methods in form of an array, and a proxy to conduct a SuperCall to the original method implementation. With the above implementation, the interception simply invokes the original code which replicates the unproxied behavior. The method itself returns a RuntimeType as the returned value is cast to the actual return type which must be a String. If any other instance was returned, a ClassCastException would occur, just as with cglib.
With this Interceptor in place, Byte Buddy can create the proxy with only a few lines of code:
Class<?> type = new ByteBuddy()
.subclass(Sample.class)
.method(ElementMatchers.any()).intercept(MethodDelegation.to(Interceptor.class))
.make()
.load(Sample.class.getClassLoader())
.getLoaded();
The resulting class can now be instantiated using the reflection API. By default, Byte Buddy mimics all constructors that the super class is declaring. In the above case, a default constructor will be made available as Sample also declares one.
Note that Byte Buddy always requires a specification of the methods to intercept. If multiple matchers are specified, each their delegation target would be considered in the reverse order of their specification. If all methods should be intercepted, the any-matcher captures all methods. By default, Byte Buddy does however ignore the Object::finalize method. All other Object methods like hashCode, equals or toString are proxied.
Caching proxied classes
With class creation and loading being expensive operations, cglib offers a built-in cache for its proxy classes. As key for this cache, cglib considers the shape of the proxy class and recognizes if it created a class with a compatible shape previously.
While this is convenient, this cache can quickly turn into a leaky abstraction that is sensitive to minor changes. Also, the caching mechanism is performing rather poorly due to its ambitious implementation of recognizing shapes. For this reason, Byte Buddy rather offers an explicit TypeCache and requires its user to specify a mechanism for identifying a cache key. When proxying a single class, the proxied
Class typically suffices as a key:
TypeCache<Class<?>> cache = new TypeCache<>();
Class<?> type = cache.findOrInsert(Sample.class.getClassLoader(), Sample.class, () -> {
return new ByteBuddy()
.subclass(Sample.class)
.method(ElementMatchers.any()).intercept(MethodDelegation.to(Interceptor.class))
.make()
.load(Sample.class.getClassLoader())
.getLoaded();
});
With this cache a new proxy class is only created if no proxy class was previously stored for Sample. As an optional, additional argument, a monitor object can be provided. This monitor is then locked during class creation to avoid that the same proxy is created concurrently by different threads. This can increase contention but avoids unnecessary class generation.
If more complex caching is required, a dedicated library should of course be used instead of the cache that Byte Buddy offers.
Abstract methods and default values
Until now, we assumed that all proxied methods are implemented by the proxied class. But Byte Buddy – just as cglib – also intercepts abstract methods that do not offer a super method implementation. To support intercepting such methods, the previous interceptor must be adjusted, as it currently requires a super method proxy via its parameters. By setting a property for the SuperMethod annotation, the parameter can be considered as optional.
public class Interceptor {
@RuntimeType
public static Object intercept(@This Object self,
@Origin Method method,
@AllArguments Object[] args,
@SuperMethod(nullIfImpossible = true) Method superMethod,
@Empty Object defaultValue) throws Throwable {
if (superMethod == null) {
return defaultValue;
}
return superMethod.invoke(self, args);
}
}
In case of intercepting an abstract method, the proxy for the super method is set to null. Additionally, Empty injects a suitable null value for the intercepted method’s return type. For methods that return a reference type, this value will be null. For a primitive return type, the correct primitive zero is injected.
Managing instance-specific interceptor state
In the previous example, the interceptor method is static. In principle, method delegation can also delegate to an instance with a non-static method, but this would likely defeat the caching mechanism if the state would be specific for each created proxy.
cglib’s cache works around this limitation, but cannot handle several corner cases where the cache might start failing after minor changes. Byte Buddy, on the other hand, relies on the user to manage the state explicitly, typically by adding a field via the defineField step, which can then be read by the interceptor:
TypeCache<Class<?>> cache = new TypeCache<>();
Class<?> type = cache.findOrInsert(Sample.class.getClassLoader(), Sample.class, () -> {
return new ByteBuddy()
.subclass(Sample.class)
.defineField(InterceptorState.class, "state", Visibility.PUBLIC)
.method(ElementMatchers.any()).intercept(MethodDelegation.to(Interceptor.class))
.make()
.load(Sample.class.getClassLoader())
.getLoaded();
});
With this changed definition, any proxy instance can contain a designated instance of InterceptorState. The value can then be set via reflection or via a method handle.
Within the interceptor, this InterceptorState is accessible via an additional parameter with the FieldValue annotation which accepts the field’s name as its property. Doing so, the generated class itself remains stateless and can remain cached.
Handling non-default constructors
Byte Buddy creates valid, verifiable Java classes. As such, any class must invoke a constructor of its super class in its own constructors. For proxies, this can be inconvenient as a class without a default constructor might not be easily constructible. Some libraries like objenesis work around this limitation, but those libraries rely on JVM-internal API and their usage should be avoided.
As mentioned before, Byte Buddy replicates all visible constructors of a proxied class by default. But this behavior can be adjusted by specifying a ConstructorStrategy as a second argument to ByteBuddy::subclass. For example, it is possible to use ConstructorStrategy.ForDefaultConstructor which creates a default constructor by invoking a super constructor with default arguments for all parameters. As an example, considering the below ConstructorSample, Byte Buddy can define a default constructor for the proxy which provides null as an argument to the proxied super class:
public class ConstructorSample {
private final String value;
public ConstructorSample(String value) {
this.value = value;
}
public String hello() {
return "Hello " + value;
}
}
The dynamic type builder is now created by:
new ByteBuddy().subclass(
ConstructorSample.class,
new ConstructorStrategy.ForDefaultConstructor(ElementMatchers.takesArguments(String.class)));
Note that this approach would result in the proxied method returning Hello null as a result and that this might cause an exception during a constructor’s invocation if null is not considered a valid argument.
Class loading and modules
When Byte Buddy defines a class, it does not yet consider how this class will be loaded. Without any specification, Byte Buddy loads a proxy in a dedicated class loader that is a child of the class loader that is provided to the load method. While this is often convenient, creating a class loader is however an expensive operation which should be avoided, if possible. As a cheaper alternative, proxy classes should be injected into existing class loaders; normally into the one that loaded the class that is being proxied.
With Java 9, the JVM introduced an official API for class injection via MethodHandles.Lookup, and of course Byte Buddy supports this API. If Byte Buddy is however used on Java 8 or earlier, this strategy is not yet available. Typically, users fall back to using sun.misc.Unsafe, a JVM-internal API. As Java 8 does not yet encapsulate internal API and since sun.misc.Unsafe is available on most JVM implementations, this fallback does not normally render a problem.
A caveat of using MethodHandles.Lookup is its call site sensitivity. If Java modules are used, the instance must be created and provided by the module that owns the package of the proxied class. Therefore, the instance of MethodHandles.Lookup must be provided to Byte Buddy and cannot be created from within the library which represents a module of its own.
Byte Buddy configures class loading behavior by instances of ClassLoadingStrategy which can be passed as a second argument to the load method. To support most JVMs, Byte Buddy already offers a convenience method that resolves the best available injection strategy for a given JVM via:
ClassLoadingStrategy.UsingLookup.withFallback(() -> MethodHandles.lookup());
With the above strategy, a method handle lookup is used if possible and internal API is only used as a fallback. Since the method handles lookup is resolved within a lambda, it also represents the context of the module that is using Byte Buddy, assuming that this is the right module to define the proxy class. Alternatively, this Callable has to be passed from the right place. If the module system is not used, however, the above approach is normally sufficient as all classes are likely located within the unnamed module of the same class loader.
Avoiding runtime proxies with build-time instrumentation
With a rising interest for Graal and AOT compilation of Java programs in general, the creation of runtime proxies has fallen somewhat out of fashion. Of course, when running a native program without a byte code-processing JVM, classes cannot be created during runtime. Fortunately, proxies can often be created during build time instead.
For build-time code generation, Byte Buddy offers a Maven and a Gradle plugin which allow for the application of Plugin instances that manipulate and create classes before runtime. For other build tools, Byte Buddy also offers a Plugin.Engine as part of Byte Buddy which can be invoked directly. As a matter of fact, the byte-buddy artifact even contains a manifest that allows for using the jar file as an invokable of the plugin engine.
To implement a plugin for creating proxies, the proxy creator needs to implement Byte Buddy’s Plugin and Plugin.Factory interfaces. A plugin specifies what classes to instrument and how the instrumentation should be applied. For an easy example, the following plugin creates a proxy for the Sample class and adds the name of this proxy as an assumed annotation ProxyType onto the Sample class:
public class SamplePlugin implements Plugin, Plugin.Factory {
@Override
public boolean matches(TypeDescription type) {
return type.getName().equals("pkg.Simple");
}
@Override
public DynamicType.Builder<?> apply(DynamicType.Builder<?> builder,
TypeDescription typeDescription,
ClassFileLocator classFileLocator) {
DynamicType helper = new ByteBuddy()
.subclass(typeDescription)
.defineField(InterceptorState.class, "state", Visibility.PUBLIC)
.method(ElementMatchers.any()).intercept(MethodDelegation.to(Interceptor.class))
.make();
return builder
.require(helper)
.annotateType(AnnotationDescription.Builder.ofType(ProxyType.class)
.define("value", helper.getTypeDescription().getName())
.build());
}
@Override
public void close() { }
@Override
public Plugin make() { return this; }
}
With the annotation in place, the runtime can now check for the existence of a build-time proxy and avoid code generation altogether in such a case:
TypeCache<Class<?>> cache = new TypeCache<>();
Class<?> type = cache.findOrInsert(Sample.class.getClassLoader(), Sample.class, () -> {
ProxyType proxy = Sample.class.getAnnotation(ProxyType.class);
if (proxy != null) {
return proxy.value();
}
return new ByteBuddy()
.subclass(Sample.class)
.defineField(InterceptorState.class, "state", Visibility.PUBLIC)
.method(ElementMatchers.any()).intercept(MethodDelegation.to(Interceptor.class))
.make()
.load(Sample.class.getClassLoader())
.getLoaded();
});
An advantage of this approach is that the usage of the build-time plugin remains entirely optional. This allows for faster builds that only execute tests but do not create artifacts, and allows users that do not intend to AOT-compile their code to run their applications without an explicit build setup.
Note that a future version of Byte Buddy will likely make the use of Graal even easier by discovering and preparing runtime-generated classes when the Graal configuration agent is used. For performance reasons, using an explicit build tool is however expected to remain the most performant option. Do however note that this approach is somewhat restricted to classes of the compiled project since external dependencies are not processed by a build tool.
Inline proxy code without subclasses
With the above approach, the created proxies still require the use of reflection to create instances of the proxy. For an even more ambitious setup, Byte Buddy offers the Advice mechanism to change the code of classes directly. Advice is normally often used for the decoration of methods and a popular choice when developing Java agents. But it can also be used to emulate proxy behavior without creating a subclass.
As an example, the following advice class records the execution time of a method by declaring actions that are to be performed prior to invoking a method as well as after it. Advice offers similar annotations to MethodDelegation, be careful to not confuse those annotations as they are declared by different packages.
To emulate the previous behavior of the Interceptor, the following Decorator functions similarly to it. Note that the Decorator declares a set of proxies to recognize what instances are to be treated as proxies and which instances should function as if they were not proxied. Within the OnMethodEnter annotation, it is specified that the original code is skipped if a non-null value is returned.
public class Decorator {
static final Set<Object> PROXIES = new HashSet<>();
@Advice.OnMethodEnter(skipOn = Advice.OnNonDefaultValue.class)
public static Object enter(
@Advice.This Object self,
@Advice.Origin Method method,
@Advice.AllArguments Object[] arguments) throws Throwable {
if (PROXIES.contains(self)) {
return ProxyHandler.handle(self, method, arguments);
} else {
return null;
}
}
@Advice.OnMethodExit
public static void exit(
@Advice.Enter Object enter,
@Advice.Exit(readOnly = false, typing = Assigner.Typing.DYNAMIC) Object returned) {
if (enter != null) {
returned = enter;
}
}
}
With this code, the original method can be invoked by temporarily removing the instance from the proxy set within the ProxyHandler.
Object returned;
Decorator.PROXIES.remove(self);
try {
returned = method.invoke(self, arguments);
} finally {
Decorator.PROXIES.add(self);
}
Note that this is a naive approach which will fail if the proxy is used concurrently. If a proxy needs to be thread-safe, it is normally required to define a thread-local set that contains temporarily disabled proxies.
Of course, it is not normally possible to apply this decoration during a JVMs runtime, but only at build-time, unless a Java agent is used. To still allow for a fallback-implementation, Byte Buddy does however allow for Advice being used as both decorator:
new ByteBuddy().redefine(Sample.class)
.visit(Advice.to(Decorator.class).on(ElementMatchers.isMethod()))
.make();
and as an interceptor for creating a subclass proxy:
new ByteBuddy().subclass(Sample.class)
.method(ElementMatchers.isMethod())
.intercept(Advice.to(Decorator.class))
.make();
In this case, a build-time plugin can avoid a subclass creation where this is necessary. For example, it allows for proxying final classes or methods, if this should be supported. At the same time, inline proxies cannot proxy native methods.
Replacing other cglib utilities
cglib contains a row of other class generation utilities besides the Enhancer.
The good news is that most of this functionality has become obsolete. Immutable beans are less useful today as it has become much more common to model immutable objects by for example records. And similarly other bean utilities have found better equivalents in today’s Java, especially since method and var handles have entered the stage. Especially cglib’s FastMethod and FastClass utilities are no longer useful as reflection and method handles have passed the performance that is offered by these code generation tools.
Source: javacodegeeks.com







