Vikdor Vikdor - 6 months ago 38
Java Question

is it possible to dynamically register servlets from a javaagent?

I have a java agent with a class annotated with

@WebServlet
. I am attaching this agent to a servlet-based application using Servlet 3.0, by specifying the
-javaagent:/path/to/agent/jar
in
JAVA_OPTS
.

Yet, the servlet doesn't appear to be loaded and I get 404 error when trying to access the servlet.

Is this even possible?

Answer

TLDR: https://github.com/tsabirgaliev/tomcat-agent

Quick and dirty solution for Tomcat 8.0

public class Agent {
    public static class Target {

        public static final String GLOBAL_SERVLET_PATTERN = "/globalServlet";
        public static final String GLOBAL_SERVLET_NAME = "globalServlet";

        public boolean intercept(
                @SuperCall Callable<Boolean> zuper
                , @Argument(0) InputSource source
                , @Argument(1) Object dest
                , @Argument(2) boolean fragment
                , @This Object self
        ) {
            try {
                boolean ok = zuper.call();

                if (!fragment) {
                    Method getServletMappings = dest.getClass().getMethod("getServletMappings");
                    Map<String, String> mappings = (Map<String, String>)getServletMappings.invoke(dest);

                    if (!mappings.containsKey(GLOBAL_SERVLET_PATTERN)) {
                        ClassLoader loader = self.getClass().getClassLoader();

                        Class<?> servletDefClass = Class
                        .forName("org.apache.tomcat.util.descriptor.web.ServletDef", true, loader);

                        Object servletDef = servletDefClass.newInstance();

                        servletDefClass.getMethod("setServletClass", String.class)
                        .invoke(servletDef, "io.tair.myagent.GlobalServlet");

                        servletDefClass.getMethod("setServletName", String.class)
                        .invoke(servletDef, GLOBAL_SERVLET_NAME);

                        dest.getClass().getMethod("addServlet", servletDefClass)
                        .invoke(dest, servletDef);

                        dest.getClass().getMethod("addServletMapping", String.class, String.class)
                        .invoke(dest, GLOBAL_SERVLET_PATTERN, GLOBAL_SERVLET_NAME);
                    }

                }

                return ok;
            } catch (Exception e) {
                throw new RuntimeException(e);
            }
        }

    }

    public static void premain(String agentArgs, Instrumentation inst) {

        new AgentBuilder.Default()
            .type(named("org.apache.tomcat.util.descriptor.web.WebXmlParser"))
            .transform((builder, typeDescription, classLoader) ->
                builder.method (
                    named("parseWebXml")
                    .and(takesArgument(0, InputSource.class))
                )
                .intercept(MethodDelegation.to(new Target()))
            )
            .installOn(inst);

    }
}

The idea is to intercept the calls to org.apache...WebXmlParser#parseWebXml(InputSource, WebXml, boolean) and add the necessary servlet mappings right after the parsing of web.xml files.

The heavy part is handled by excellent ByteBuddy [1]. For this agent to work you will have to include ByteBuddy classes.

If GlobalServlet is packaged within the agent, you will also have to include ServletApi classes. This for sure can be avoided, but I don't know enough ByteBuffer magic to implement that.

UPDATE for Tomcat 7.0

The following solution will need jsp-api in addition to servlet-api.

public class Agent {
    public static class Target {

        public static final String GLOBAL_SERVLET_PATTERN = "/globalServlet";
        public static final String GLOBAL_SERVLET_NAME = "globalServlet";

        public void intercept(
                @SuperCall Callable<Void> zuper
                , @This Object self
        ) {
            try {
                Method getServletMappings = self.getClass().getMethod("getServletMappings");
                Map<String, String> mappings = (Map<String, String>)getServletMappings.invoke(self);

                if (!mappings.containsKey(GLOBAL_SERVLET_PATTERN)) {
                    ClassLoader loader = self.getClass().getClassLoader();

                    Class<?> servletDefClass = Class
                            .forName("org.apache.catalina.deploy.ServletDef", true, loader);

                    Object servletDef = servletDefClass.newInstance();

                    servletDefClass.getMethod("setServletClass", String.class)
                            .invoke(servletDef, "io.tair.myagent.GlobalServlet");

                    servletDefClass.getMethod("setServletName", String.class)
                            .invoke(servletDef, GLOBAL_SERVLET_NAME);

                    self.getClass().getMethod("addServlet", servletDefClass)
                            .invoke(self, servletDef);

                    self.getClass().getMethod("addServletMapping", String.class, String.class)
                            .invoke(self, GLOBAL_SERVLET_PATTERN, GLOBAL_SERVLET_NAME);
                }

                zuper.call();

            } catch (Exception e) {
                throw new RuntimeException(e);
            }
        }

    }

    public static void premain(String agentArgs, Instrumentation inst) {

        new AgentBuilder.Default()
            .type(named("org.apache.catalina.deploy.WebXml"))
            .transform((builder, typeDescription, classLoader) ->
                builder.method (
                    named("configureContext")
                )
                .intercept(MethodDelegation.to(new Target()))
            )
            .installOn(inst);

    }
}

[1] http://bytebuddy.net/

Comments