Taking over the Spring context from a neighbouring application on Tomcat


A little while ago I was developing a small application from which I wanted to reuse the Spring beans that are defined in a context of another application. I thought this might be useful for someone so here’s a short explanation of how I achieved this result. Both applications were deployed on Tomcat 6.0.18, but I don’t think that minor version number differences would have much of an impact on the technique. The core concept is quite simple – I gave one application the ability to access another application’s classpath, thus all the classes, Spring context and the beans. Let’s call the standard Spring-based application the victim and the application that will take over the context – rogue. There aren’t any specific requirements for the victim application, but I did put the context files in the classpath, not just the WEB-INF directory. The rogue application has two context files, one of them loaded by the Spring context listener is just a stub and contains no beans, but the other one tries to include the spring context file which belongs to the victimapplication:

<?xml version="1.0" encoding="UTF-8"?></div>
<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.5.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-2.5.xsd">
    <context:annotation-config />
    <import resource="classpath:application-context.xml"/>
</beans>

We need to delay the loading of the victim‘s application context because we can’t access its classpath just yet.  Next it’s important to define our own implementation of the context listener in the web.xml file:

<listener>
    <listener-class>com.example.rogue.web.ContextListener</listener-class>
</listener>

The rogue application’s context listener implementation should look like this:

public class ContextListener implements ServletContextListener {

  public void contextInitialized(ServletContextEvent event) {
    String springConfig = "classpath:rogue-context.xml";
    ServletContext servletContext = event.getServletContext();
    WebApplicationContext webContext =
      WebApplicationContextUtils.getWebApplicationContext(servletContext);

    ClassPathXmlApplicationContext context =
      new ClassPathXmlApplicationContext(webContext);
    context.setClassLoader(getModifiedClassLoader());
    context.setAllowBeanDefinitionOverriding(true);
    context.setConfigLocation(springConfig);
    context.refresh();
  }

  public void contextDestroyed(ServletContextEvent event) {
    Model.getInstance().destroy();
  }

    /**
     * Scans the lib directory of victim application and adds all
     * the jar files as valid repositories for the current application
     */
    private WebappClassLoader getModifiedClassLoader() {
      WebappClassLoader classLoader =
        (WebappClassLoader) this.getClass().getClassLoader();

      String tmp = classLoader.getURLs()[0].getPath();
      String appDir = tmp.substring(1, tmp.indexOf("/webapps/") + 8);
      String classesDir = appDir + "/victim/WEB-INF/classes/";
      String libDir = appDir + "/victim/WEB-INF/lib/";

      classLoader.addRepository("file:/"+classesDir);
      File dir = new File(libDir);
      for (String file : dir.list()) {
        if (file.endsWith(".jar")) {
          classLoader.addRepository("file:/" + libDir + file);
        }
      }
      return classLoader;
    }
  }

Note that in the getModifiedClassLoader() method we’re casting the class loader to the WebappClassLoader class which is a class loader for Tomcat hosted web applications so this line would probably fail on other servlet containers, though I didn’t actually tried it. The directories for the victim application’s classpath are hardcoded here, so make sure you change them if you’ll try to play around with a different application. The actual payload here is the Spring application context named context in the contextInitialized() method so make sure you save it somewhere. A good choice might be to save it as a servlet context attribute or in a singleton object so you’ll be able to retrieve it later when you actually need it.

Now the fun part – using the Spring beans of another application.  If you have access to the interfaces or classes of the beans that you want to call you could add them as libraries for the rogue application, but to avoid any dependency on the victim application I’m using the reflective method invocation here.

public class CallerController implements Controller {

  public ModelAndView handleRequest(HttpServletRequest request,
                                    HttpServletResponse response)
    throws ServletException, IOException {

    ClassPathXmlApplicationContext rogueCtx =
      Model.getInstance().getAppContext();
    Object o = rogueCtx.getBean("importantService");

    for(Method method : o.getClass().getMethods()) {
      if(method.getName().equalsIgnoreCase("doSeriousWork")) {
        try {
          Object result = method.invoke(o);
          request.getSession().setAttribute("result", result);
        } catch (Exception e) {
          e.printStackTrace();
        }
      }
    }
    return new ModelAndView("caller.jsp");
  }
}

Such usage requires you to know the Spring bean names, method names and the parameter types, but if you’re able to deploy your application next to the victim, you can figure those out. I was trying to keep the post short so I’m not sure if I explained the details well enough. If you have any thoughts on this leave a comment and you can download two proof of concept applications and play around with them if you like.