Implementing a Java Agent (Part 3)

The previous posts in this series described how to create a Java agent and how to dynamically attach it to a running JVM. Once the agent is running, you may want to interact with it, for example to collect data from it or to execute specific methods on demand. Today’s post will show how to do just that using the Java Management Extensions (JMX). As before, you can download the complete source code as a Maven project.

We’ll start by creating an MBean interface that will encapsulate the management operations of the agent. In this case, the only supported operation will be to get the time when the agent was loaded:

public interface AgentManagementMBean {
    // Name used to access MBean
    String JMX_BEAN = "com.afqa123:type=AgentManagement";
    // Returns the timestamp when the agent was started.
    long getStartTime();
}

The actual implementation of the interface is fairly straightforward:

public class AgentManagement implements AgentManagementMBean {
    // Store time when agent was first loaded
    private static final long START_TIME = System.currentTimeMillis();
    @Override
    public long getStartTime() {
        return START_TIME;
    }    
}

Next, we’ll have to modify the agent class to register the new interface with the MBean server. The code to do that will have to be executed from both the premain and agentmain methods, so we’ll create a new method for that purpose:

public class MyAgent {
    // Instance of AgentManagement MBean
    private static AgentManagement mgmtBean;

    private static void registerMBean() {
        if (mgmtBean == null) {
            mgmtBean = new AgentManagement();
            // Register MBean with MBean Server
            MBeanServer mbs = ManagementFactory.getPlatformMBeanServer();
            try {
                ObjectName name = new ObjectName(AgentManagementMBean.JMX_BEAN);
                if (mbs.isRegistered(name)) {
                    mbs.unregisterMBean(name);
                }
                mbs.registerMBean(mgmtBean, name);
            } catch (JMException ex) {
                ex.printStackTrace();
            }
        }
    }

Additionally, we’ll update the existing methods to call this new method during startup:

    public static void premain(String args, Instrumentation inst) {
        System.out.println("MyAgent start: " + args);
        registerMBean();
    }
    
    public static void agentmain(String args, Instrumentation inst) {
        System.out.println("MyAgent main: " + args);
        registerMBean();
    }    

Now, switch over to the controller application we created last time. As you may remember, we used it to attach to the JVM and then to load our Java agent. We will extend that class to also load the JMX agent. Once that is done, we can get the JMX service URL:

        String agentJar = args[1];
        String jmxUrl = null;
        try {
            VirtualMachine vm = VirtualMachine.attach(targetJvm);
            // Load our agent
            vm.loadAgent(agentJar, null);
            // Load the JMX agent
            vm.loadAgent(System.getProperty("java.home", "") + "/lib/management-agent.jar", 
                    "com.sun.management.jmxremote");
            // The agent properties contain the JMX URL
            Properties props = vm.getAgentProperties();
            jmxUrl = props.getProperty("com.sun.management.jmxremote.localConnectorAddress");
            vm.detach();

I should note that if you are using Java 8 or newer, you can use a newly introduced method to load the JMX agent instead of having to provide the path to the management agent jar.

With the service URL, we can then connect to the JMX agent and create an instance of our management bean. Finally, we call the method we defined earlier, which will be executed by the agent running on the target JVM:

            // Connect to JMX agent
            JMXServiceURL url = new JMXServiceURL(jmxUrl);
            try (JMXConnector jmxc = JMXConnectorFactory.connect(url)) {
                MBeanServerConnection connection = jmxc.getMBeanServerConnection();
                // Get reference to our agent's management bean
                ObjectName name = new ObjectName(AgentManagementMBean.JMX_BEAN);
                AgentManagementMBean bean = JMX.newMBeanProxy(connection, name, AgentManagementMBean.class);
                // Invoke agent method
                Date start = new Date(bean.getStartTime());
                System.out.println("Agent has been running since: " + start);
            }
        } catch (Throwable t) {
            t.printStackTrace();
        }

After you build the project, copy the JARs to a single directory, and as before start the sample application:

> java -jar MyApplication-0.0.1-SNAPSHOT.jar

Finally, run the controller again:

> java.exe -cp "%JAVA_HOME%/lib/tools.jar;MyController-0.0.1-SNAPSHOT.jar;MyAgent-0.0.1-SNAPSHOT.jar" com.afqa123.example.MyController MyApplication-0.0.1-SNAPSHOT.jar MyAgent-0.0.1-SNAPSHOT.jar 
Agent has been running since: Fri Feb 19 15:28:17 PST 2016

You’ll notice that if you execute the controller multiple times the start date will not change, because the agent jar is only loaded once.

2 throughts on "Implementing a Java Agent (Part 3)"

Leave a Reply

Your email address will not be published.