Using Byte Buddy to expand Spring Boot's classpath - instrumentation

The AWS SDK can locate API call interceptors via classpath scanning (looking for software/amazon/awssdk/global/handlers/execution.interceptors and instantiating classes specified there).
I'm writing a Java Agent with the intention of causing my interceptors to be locatable by the AWS SDK.
My interceptor is bundled with the Java Agent.
My interceptor implements AWS's ExecutionInterceptor.
The AWS SDK is not bundled with my agent, because I'd like the end-user to provide their own AWS SDK version.
For regular standalone applications, this is a no-brainer, as the Java Agent is automatically added to the runtime classpath of the application. The AWS SDK finds my interceptors with no problem.
However, this approach completely breaks with Spring Boot applications where the AWS SDK is bundled as a dependency under BOOT-INF/lib. The reason boils down to Spring Boot's classloading hierarchy. My interceptor class can be found, but its loading fails due to inability to find AWS's ExecutionInterceptor, as it is loaded in a "lower" classloader in the hierarchy.
So I figured that my approach should be to somehow modify Spring Boot's classloader search. However, I'm facing these issues:
At the time of the agent being called, Spring Boot's "lower" classloader isn't created yet.
I am not entirely sure what it is that I need to instrument.
I've read of Byte Buddy being able to help in such "interesting" circumstances but haven't found a way to make this work yet. Any ideas?
(EDIT: I'm looking for a solution that doesn't require code/packaging changes, hence the Java Agent approach)
(EDIT: Things I've tried)
Following Rafael's answer: The method in the SDK that resolves all interceptors is in the class SdkDefaultClientBuilder, and is called resolveExecutionInterceptors.
The following, then, works for standalone JARs which are not SpringBoot applications:
public static void installAgent(Instrumentation inst) {
new AgentBuilder.Default()
.with(RedefinitionStrategy.DISABLED)
.type(ElementMatchers.nameEndsWith("SdkDefaultClientBuilder"))
.transform(
new Transformer() {
#Override
public Builder<?> transform(Builder<?> builder, TypeDescription typeDescription,
ClassLoader classLoader, JavaModule module) {
return builder.visit(Advice.to(MyAdvice.class).on(ElementMatchers.named("resolveExecutionInterceptors")));
}
}
).installOn(inst);
}
For SpringBoot applications, however, it looks like the advice isn't applied at all. I am guessing that this is because the SdkDefaultClientBuilder type isn't even available at the time when the agent starts. It is available during SpringBoot's runtime, in a different classloader.

Byte Buddy allows you to inject code in any method of any class, so the first and only major thing you would need to find out would be where your interceptor is instantiated. This can typically be done by setting a breakpoint in the constructor of the interceptor in the working scenario and investigating the methods in the stack. Find out where the classes are discovered, for example the method where software/amazon/awssdk/global/handlers/execution.interceptors is read.
Once you have identified this method, you would need to find a way to manually extract the interceptors defined by your agent and to manually add them. For example, if the file-extracted interceptors are added to an argument of type List<Interceptor>, you could use Byte Buddy to modify this method to also add those of your agent.
Normally, you use Byte Buddy's AgentBuilder in conjunction with Advice to do so. Advice let's you inline code into another method as for example, assuming you find a method with an argument of type List<Interceptor>:
class MyAdvice {
#Advice.OnMethodEnter
static void enter(#Advice.Argument(0) List<Interceptor> interceptors) {
interceptors.addAll(MyAgent.loadMyInterceptors());
}
}
You can now inline this code into the method in question by:
class MyAgent {
public static void premain(String arg, Instrumentation inst) {
new AgentBuilder.Default().type(...).transform((builder, ...) -> builder
.visit(Advice.to(MyAdvice.class).on(...))).install(inst);
}
}
You might need to use AgentBuilder.Transformer.ForAdvice if the classes in question are not available on the agent's class loader where Byte Buddy resolves the advice using both the target and the agent class loader.

Related

Why ClassGraph finds classes from other deployment on Wildfly?

I prepared WAR archive and I deployed it on Wildfly 21.
I want to find all classes annotated with javax.ws.rs.ApplicationPath annotation.
try (ScanResult scanResult = new ClassGraph().enableAllInfo().whitelistPackages( PACKAGE ).scan())
{
final ClassInfoList classesWithAnnotation =
scanResult.getClassesWithAnnotation( ApplicationPath.class.getName() );
}
It finds two classes, one from my deployment and one from other deployment. I undeployed other deployment to make sure that one class is taken from it and I am sure, if only one deployment is deployed on Wildfly then it return only one class.
I do not understand why ClassGraph finds classes from other deployment. How to configure ClassGraph to scan only deployment which ClassGraph itself belongs? I guess that I have to configure class loader, but I do not know how to do it correctly.
Unfortunately it seems that there is no any configuration options that could meet my requirements. Source code modification is needed.
There is JBossClassLoaderHandler#findClasspathOrder method.
First org.jboss.modules.Module is retrieved from class loader. Next getCallerModuleLoader method is invoked and returns different class loader. Original class loader is an instance of org.jboss.modules.ModuleClassLoader class. Class loader retrieved by getCallerModuleLoader method is an instance of org.jboss.as.server.moduleservice.ServiceModuleLoader class. Next moduleMap method is invoked and returns map of modules. This map contains both my deployments. Next operations are executed for each map entry.
I also do not see possibility to add my own implementation of nonapi.io.github.classgraph.classloaderhandler.ClassLoaderHandler without source code modification because handlers are kept in unmodifiable final filed in ClassLoaderHandlerRegistry class.

NServiceBus Removing IBus - Utilising IPipelineContext and IMessageSession

I am in the process of migrating NServiceBus up to v6 and am at a roadblock in the process of removing reference to IBus.
We build upon a common library for many of our applications (Website, Micro Services etc) and this library has the concept of IEventPublisher which is essentially a Send and Publish interface. This library has no knowledge of NSB.
We can then supply the implementation of this IEventPublisher using DI from the application, this allows the library's message passing to be replaced with another technology very easily.
So what we end up with is an implementation similar to
public class NsbEventPublisher : IEventPublisher
{
IEndpointInstance _instance;
public NsbEventPublisher(IEndpointInstance endpoint)
{
instance = endpoint;
}
public void Send(object message)
{
instance.Send(message, sendOptions);
}
public void Publish(object message)
{
instance.Publish(message, sendOptions);
}
}
This is a simplification of what actually happens but illustrates my problem.
Now when the DI container is asked for an IEventPublisher it knows to return a NsbEventPublisher and it knows to resolve the IEndpointInstance as we bind this in the bootstrapper for the website to the container as a singleton.
All is fine and my site runs perfect.
I am now migrating the micro-services (running in NSB.Host) and the DI container is refusing to resolve IEndpointInstance when resolving the dependencies within a message handler. Reading the docs this is intentional and I should be using IMessageHandlerContext when in a message handler.
https://docs.particular.net/nservicebus/upgrades/5to6/moving-away-from-ibus
The docs even elude to the issue I have in the bottom example around the class MyContextAccessingDependency. The suggestion is to pass the message context through the method which puts a hard dependency on the code running in the context of a message handler.
What I would like to do is have access to a sender/publisher and the DI container can give me the correct implementation. The code does not need any concept of the caller and if it was called from a message handler or from a self hosted application that just wants to publish.
I see that there is two interfaces for communicating with the "Bus" IPipelineContext and IMessageSession which IMessageHandlerContext and IEndpointInstance interfaces extend respectively.
What I am wondering is there some unification of the two interfaces that gets bound by NSB into the container so I can accept an interface that sends/publishes messages. In a handler it is an IMessageHandlerContext and on my self hosted application the IEndPointInstance.
For now I am looking to change my implementation of IEventPublisher depending on application hosting. I was just hoping there might be some discussion about how this approach is modeled without a reliable interface to send/publish irrespective of what initiated the execution of the code path.
A few things to note before I get to the code:
The abstraction over abstraction promise, never works. I have never seen the argument of "I'm going to abstract ESB/Messaging/Database/ORM so that I can swap it in future" work. ever.
When you abstract message sending functionality like that, you'll lose some of the features the library provides. In this case, you can't perform 'Conversations' or use 'Sagas' which would hinder your overall experience, e.g. when using monitoring tools and watching diagrams in ServiceInsight, you won't see the whole picture but only nugets of messages passing through the system.
Now in order to make that work, you need to register IEndpointInstance in your container when your endpoint starts up. Then that interface can be used in your dependency injection e.g. in NsbEventPublisher to send the messages.
Something like this (depending which IoC container you're using, here I assume Autofac):
static async Task AsyncMain()
{
IEndpointInstance endpoint = null;
var builder = new ContainerBuilder();
builder.Register(x => endpoint)
.As<IEndpointInstance>()
.SingleInstance();
//Endpoint configuration goes here...
endpoint = await Endpoint.Start(busConfiguration)
.ConfigureAwait(false);
}
The issues with using IEndpointInstance / IMessageSession are mentioned here.

Arquillian and #BeforeClass, #AfterClass annotations

It looks like mentioned annotations are executed inside the deployment. I need them to be run outside, let's say to start some simulator class on startup and stop it on the end. How can I do it? The simulator uses socket communication and it should not be started inside the server.
How to mix arquillian with "plain" junit(not executed in container).
You can use the arquillian #RunAsClient annotation combined with the junit #BeforeClass and #AfterClass:
#BeforeClass
#RunAsClient // runs as client
public static void shouldBeAbleToRunOnClientSideBeforeRemoteTest() throws Exception {
System.out.println("before!!");
}
#AfterClass
#RunAsClient // runs as client
public static void shouldBeAbleToRunOnClientSideAfterRemoteTest() throws Exception {
System.out.println("after!!");
}
The answer that Franck gives will certainly work, and will probably be what most users will want to use. If, however, you need to get some more detail about what's going on, or need some more control you can certainly hook into the Arquillian life cycle and register observers for all sorts of events that Arquillian emits. Unfortunately, it isn't as easy as listening to a CDI event.
You'll need to create a services entry in META-INF/services with the file name of org.jboss.arquillian.core.spi.LoadableExtension. The contents of that file will be the Fully Qualified Name (FQN) of the classes that implement the LoadableExtension interface from Arquillian. You can then in the register(ExtensionBuilder) method register any classes that will observe events. Those classes will simply need a public void methodName(#Observes EventType) method for all the events they want to listen for. The #Observes annotation is in the org.jboss.arquillian.core.api.annotation package.
You can see this in action the Arquillian Recorder Reporter extension here, here, and here. I understand this is probably more than what most people will want to do, but again, if you need the power and hooks, Arquillian should be able to give you what you need.

How to define aspects and pointcuts in WildFly?

We are migrating from JBoss 5 to WildFly 8.2. Still using Spring 3.1. Suddenly none of our aspects can be found when application starts.
We might have solved (partially) the XML configuration (by placing more wildcards around), but annotation based configuration of aspects cannot be solved the same way (no wildcard possible for aspect itself since this is annotated class). Here is the Aspect class definition:
package com.mycompany.session;
#Aspect
#Component("mySessionManager")
public class SessionManager {
// intercepting any class, any method starting with com.mycompany.some
#Pointcut("execution(* com.mycompany.some.*.*(..))")
public void myPointcut() {}
#Around(value="myPointcut()")
public Object process(ProceedingJoinPoint jointPoint)
throws Throwable
{ ... the rest of code }
}
When starting this code without changes under WildFly we get this error:
java.lang.IllegalArgumentException: warning can't determine implemented interfaces of missing type com.mycompany.session.SessionManager
Anything wrong with code? Anything needs to be different in WildFly versus older jboss?
Thanks,
Nikolay

NSubstitute mocking a NLog logger fails

I have a test that looks like following:
[Test]
public void LoggerTest()
{
var log = Substitute.For<Logger>();
log.DidNotReceiveWithAnyArgs().Info("");
log.ReceivedWithAnyArgs().Info("");
}
The test succeeds, which it obviously shouldn't. To my best knowledge this is the syntax according to the NSubstitue website.
Can anybody tell me where the problem lies?
I use NSubstitue in version 1.7.2.0 from the NuGet package manager.
NLog.Logger is a concrete class with non-virtual members, so NSubstitute will not be able work with it (see the note on Creating a substitute).
If you need to test logging one option is to wrap the log functionality in your own, app-specific interface. This makes it easier to substitute in a new logger for testing purposes (can use NSubstitute or other mocking library, or hand-code your own TestLogger tailored for your test purposes).