Modify Struts2 to use custom classloaders

Tags:

In my last post I described how you could programitically load a struts.xml file at runtime. This is necessary if your web application uses a plugin framework where the plugins themselves need to use Struts2. This is because you are probably loading your plugins using a different classloader in order so you can re-deploy your plugins without restarting your servlet container.

My web application was using a plugin framework and the plugins could have a struts.xml if they wanted it. Unfortunately that's where the simple bit ended, the plugins were only loaded at runtime and worse than that they were loaded in another classloader. So, how do you tell Struts to use anything but the default:


Thread.currentThread().getContextClassLoader().getResources(resourceName)

classloader that struts2 uses? The answer wasn't too easy to work out and I had to modify two of the classes in the struts framework to get it going; by default Struts only uses the classloader:

In order to modify the classloaders that Struts is using we need to edit two files. For some reason Struts2 contains two almost identical classes which are:

there are only very slight variations between them and only the XWork one seems to be used but the Struts2 one is probably there for a reason! So I have modified both to be on the safe side. The XWork one is first, you can use the technique of creating a folder structure in my previous post for how to integrate these modifications into your own project.

All the changes use the same technique. After Struts2 has looked in the usual class loaders for the resource then ask it to look inside of the classloaders that you have uses to load your resources.

In the getResources(String resourceName, Class callingClass, boolean aggregate) method comment out the line:


iterator.addEnumeration(Thread.currentThread().getContextClassLoader().getResources(resourceName));

and then add this directly underneath it:

You will need to edit the lines that use the ProsocPluginManager in order to get your own ClassLoaders that you need to check. You will need to do this for each code segment shown in this post.

Enumeration enumerationOfResources = Thread.currentThread().getContextClassLoader().getResources(resourceName);
iterator.addEnumeration(enumerationOfResources);
if(ProsocPluginManager.isHasBeenInitialised()){
    try {
        ProsocPluginManager prosocPluginManager = ProsocPluginManager.getInstance();
        ArrayList<pluginclassloader> pluginClassLoaders = prosocPluginManager.getPluginClassLoaders();
        for (PluginClassLoader thisPluginClassLoader : pluginClassLoaders){
            enumerationOfResources = thisPluginClassLoader.getResources(resourceName);
        }
    } catch (ExceptionProsocPluginManagerNotInitialised eppmni) {
        // Handle your exception here
    }
    iterator.addEnumeration(enumerationOfResources);
}

So, what is all this and what is that ProsocPluginManager? Well, I am modifying the classloaders because I need to have plugins in my web application that can make use of the Struts2 framework. Therefore I have a ProsocPluginManager that initialises the plugins for me. Each plugin is loaded with a different classloader and when it is loaded I keep a reference to the classloader so that I can retreive it and ask Struts2 to use that classloader if it can't find the requested resource in it's default classloaders. If the ProsocPluginManager is not initialised then I don't need to run this code change. If it is any consolation then I am not sure the above modification was really needed but I wanted to make sure I didn't get any strange errors later on.

The second modification is to the getResource(String resourceName, Class callingClass) method. After the first two checks for:

if (url == null)

Add this:


if(ProsocPluginManager.isHasBeenInitialised()){
    if (url == null) {
        try {
            ProsocPluginManager prosocPluginManager = ProsocPluginManager.getInstance();
            ArrayList<pluginclassloader> pluginClassLoaders = prosocPluginManager.getPluginClassLoaders();
            for (PluginClassLoader thisPluginClassLoader : pluginClassLoaders) {
                url = thisPluginClassLoader.getResource(resourceName);
                if (url != null) {
                    break;
                }
            }
        } catch (ExceptionProsocPluginManagerNotInitialised eppmni) {
            // Handle your exception here
        }
    }
}

As you can see it is almost identical to the first change that was made so I won't explain it. Lastly, is the change to the loadClass(String className, Class callingClass) method which is a little harder on the eye. I have simply posted the whole method below including my changes as it was easier than explaining.


public static Class loadClass(String className, Class callingClass) throws ClassNotFoundException {
        try {
            return Thread.currentThread().getContextClassLoader().loadClass(className);
        } catch (ClassNotFoundException e) {
            try {
                return Class.forName(className);
            } catch (ClassNotFoundException ex) {
                try {
                    return ClassLoaderUtil.class.getClassLoader().loadClass(className);
                } catch (ClassNotFoundException exc) {
                    try { // This line added for Prosoc
                        return callingClass.getClassLoader().loadClass(className); // This line is an original line
                    } catch (ClassNotFoundException cnfe) { // This line added for Prosoc
                        // START: Added for Prosoc so that Struts will look in the pluginClassLoaders if it can't find what it needs on its own.
                        if(ProsocPluginManager.isHasBeenInitialised()){
                            logger.debug("Struts framework is looking for class [" + className + "] in prosocPluginClassLoader");
                            try {
                                ProsocPluginManager prosocPluginManager = ProsocPluginManager.getInstance();
                                ArrayList<pluginclassloader> pluginClassLoaders = prosocPluginManager.getPluginClassLoaders();
                                logger.debug("About to loop through all known pluginClassLoaders");
                                for (PluginClassLoader thisPluginClassLoader : pluginClassLoaders) {
                                    try {
                                       return thisPluginClassLoader.loadClass(className);
                                    } catch (ClassNotFoundException cnfe2) {
                                        logger.debug("class [" + className + "] was not found in prosocPluginClassLoader [" + thisPluginClassLoader + "]"); 
                                    }
                                }
                                return callingClass.getClassLoader().loadClass(className); // The default value before this class was modified for Prosoc
                            } catch (ExceptionProsocPluginManagerNotInitialised eppmni) {
                                logger.error("ExceptionProsocPluginManagerNotInitialised [" + eppmni + "]");
                                // The default value before this class was modified for Prosoc
                                return callingClass.getClassLoader().loadClass(className);
                            }
                        } else {
                            // The default value before this class was modified for Prosoc
                            return callingClass.getClassLoader().loadClass(className);
                        }
                    // END: Added for Prosoc so that Struts will look in the pluginClassLoaders if it can't find what it needs on its own.
                    }
                }
            }
        }
    }

Again it is almost identical and if you need to understand it then I would suggest a comparison with the original source code of this method. Using a difference editor should show you the changes that have been made.

To finish off you need to modify the org.apache.struts2.util.ClassLoaderUtils file. It is actually slightly easier and is identical (as far as I can remember) to the above. This post has already gone on for long enough so I will leave that for you to do.

As a final note of caution, I haven't used my modifications for more than a couple of days and so I can't say for certain that the above changes are stable, I don't see any reason why they shouldn't be but if you have problems then I would be happy to hear about them.

Comments:

Post a Comment:

HTML Syntax: Allowed