3

I have a multimodule project called k-sdk. It has modules like

---> agent

---> core

---> api

---> integration

---> sdk

where (sdk module contains core, api, integration modules and acting as a shaded jar). This jar can be imported as a dependency in the user application.

Here k-sdk is acting as a middleware to capture request and response of the api calls (both external and internal) and do some custom processing. And to intercept the internal calls i am using servlet-filter & for external calls, i am using ByteBuddy and wrote the required code in the premain method. The corresponding interceptors are written in integration module. User can use this k-sdk by running agent jar(present in agent module) of the k-sdk.

Now comes to the actual problem. Suppose I have written an Interceptor for OkHttp client version 4.10.0 in the integration module and the user is using the same client of version 3.14.9, now these two versions share completely same package name. And there are some uncommon apis, methods etc, which is being used in the OkHttpInterceptor which results in java.lang.NoSuchMethodError because maven only resolves that version of dependency which has the shortest path. And only 1 version of a dependency will be loaded in the classloader.

Now, To resolve this version conflict what i was thinking that i will use the below custom classloader. (ref) as i know that the solution lies in this approach only.

public class ChildFirstClassLoader extends URLClassLoader {

    private final ClassLoader sysClzLoader;

    public ChildFirstClassLoader(URL[] urls, ClassLoader parent) {
        super(urls, parent);
        sysClzLoader = getSystemClassLoader();
    }

    @Override
    protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
        // has the class loaded already?
        Class<?> loadedClass = findLoadedClass(name);
        if (loadedClass == null) {
            try {
                if (sysClzLoader != null) {
                    loadedClass = sysClzLoader.loadClass(name);
                }
            } catch (ClassNotFoundException ex) {
                // class not found in system class loader... silently skipping
            }

            try {
                // find the class from given jar urls as in first constructor parameter.
                if (loadedClass == null) {
                    loadedClass = findClass(name);
                }
            } catch (ClassNotFoundException e) {
                // class is not found in the given urls.
                // Let's try it in parent classloader.
                // If class is still not found, then this method will throw class not found ex.
                loadedClass = super.loadClass(name, resolve);
            }
        }

        if (resolve) {      // marked to resolve
            resolveClass(loadedClass);
        }
        return loadedClass;
    }



    @Override
    public Enumeration<URL> getResources(String name) throws IOException {
        List<URL> allRes = new LinkedList<>();

        // load resources from sys class loader
        Enumeration<URL> sysResources = sysClzLoader.getResources(name);
        if (sysResources != null) {
            while (sysResources.hasMoreElements()) {
                allRes.add(sysResources.nextElement());
            }
        }

        // load resource from this classloader
        Enumeration<URL> thisRes = findResources(name);
        if (thisRes != null) {
            while (thisRes.hasMoreElements()) {
                allRes.add(thisRes.nextElement());
            }
        }

        // then try finding resources from parent classloaders
        Enumeration<URL> parentRes = super.findResources(name);
        if (parentRes != null) {
            while (parentRes.hasMoreElements()) {
                allRes.add(parentRes.nextElement());
            }
        }

        return new Enumeration<URL>() {
            Iterator<URL> it = allRes.iterator();

            @Override
            public boolean hasMoreElements() {
                return it.hasNext();
            }

            @Override
            public URL nextElement() {
                return it.next();
            }
        };
    }

    @Override
    public URL getResource(String name) {
        URL res = null;
        if (sysClzLoader != null) {
            res = sysClzLoader.getResource(name);
        }
        if (res == null) {
            res = findResource(name);
        }
        if (res == null) {
            res = super.getResource(name);
        }
        return res;
    }
}

And to improve the user experience, i have to make changes only in the premain method. Now i don't know how to use this classloader to resolve this conflicting issue.

The above classloader is extending URLClassLoader, and the application class loader is no longer an instance of java.net.URLClassLoader in java 9+.

I am very new to this complex solution, Please! help me out.

Before this solution, i tried to repackage classes using maven-shade-plugin but that solution was not working because in premain method using ByteBuddy i was targeting a class of actual package and was using different package in the interceptor so the flow was not going to the interceptor as the class was not matching.

  • 1
    You are mixing up two different tasks. First, creating a runtime environment which can cope with different versions of the same library. Second, implementing a Java Agent which can work correctly in such an environment. There is no way around the second task, so if `maven-shade-plugin` did already solve the first task but your Agent did not use the correct package, try fixing your Agent. – Holger Feb 20 '23 at 14:02
  • Basically what i was thinking that i will set my custom classloader as the default and will not load the `integration` module by adding some check and when user application's flow will go to the `premain` method in `agent` module then i will get user external dependencies through getClass().getClassLoader().And then will load only those dependencies which user is using.And also i will have to make submodules of `integration` module by different dependency versions). and will load only those modules for which interceptor is being used. For eg: okhttp 3.14.9 interceptor module. if user uses it. – Gourav Kumar Feb 20 '23 at 15:18
  • You can refer this repo[https://github.com/keploy/java-sdk]. In this project i am trying to achieve the above task. – Gourav Kumar Feb 20 '23 at 15:20

0 Answers0