Glen Mazza's Weblog

https://glenmazza.net/blog/date/20140615 Sunday June 15, 2014

Creating a Java Swing alternative to JConsole for calling MBeans

Our iterative development process at work requires use of JConsole to interact with MBeans offered by two localhost processes. JConsole provides the functionality we need but as a generic tool is not streamlined for our particular requirements and so ends up slowing us down during development. I thought it would be good for our team to create a Java-Swing based GUI alternative to JConsole that we could optimize for our needs. In particular:

  • Eliminate need for the JConsole "New Connection" dialog and manual selection of processes by their process ID (PID). After we activate our MBeans-offering processes, we can start this GUI alternative and it will be hardcoded to automatically connect to the MBeans we need.
  • Immediate access to the MBean operations upon connection. As the GUI will replace just the functionality provided by the JConsole MBeans Operations tab, developers will immediately see the controls they need without needing to navigate to the JConsole MBeans operations page each time they connect.
  • A user interface customized for our work needs. Where developers once had to type in operation names and parameters, enable them to double-click desired operations from a list box instead and use other types of widgets to simplify supplying needed data.
  • Graceful reconnect of restarted processes. As the processes providing the MBeans are terminated and then re-deployed during the development cycle, provide a "Reload MBean" button on the GUI that will quickly find and connect to the desired MBean of the relaunched process.

I was able to create such a customized utility for our team, liberating us from JConsole. Some notes on making your own implementation:

  1. Perhaps best to develop the Swing GUI part first--create your needed buttons, list boxes, entry fields, etc., and (empty) action handlers for them--then work on weaving the JMX MBean accessing code into those action handlers. For my limited requirements, a single class of about 350 lines was all I needed, for both the Swing and JMX code. To help you quickly get started with a Swing replacement, below is a sample MyJMXSwingApp offering a listbox and a button with empty click handlers for them. It also provides sample MBean-accessing code (discussed further down) that you can modify for your own needs. However, it does not actually connect to an MBean until you hardcode a specific one for it. This class can be activated by using the Maven pom.xml in the next step.

    package sample;
    
    import com.sun.tools.attach.AttachNotSupportedException;
    import com.sun.tools.attach.VirtualMachine;
    import com.sun.tools.attach.VirtualMachineDescriptor;
    
    import javax.management.JMX;
    import javax.management.MBeanServerConnection;
    import javax.management.ObjectName;
    import javax.management.remote.JMXConnector;
    import javax.management.remote.JMXConnectorFactory;
    import javax.management.remote.JMXServiceURL;
    import javax.swing.*;
    import javax.swing.event.MouseInputAdapter;
    import java.awt.*;
    import java.awt.event.ActionEvent;
    import java.awt.event.ActionListener;
    import java.awt.event.MouseEvent;
    import java.io.File;
    import java.util.Properties;
    import java.util.Set;
    
    public class MyJMXSwingApp extends MouseInputAdapter {
    
        private JFrame frame;
        private DefaultListModel sampleListModel;
        private JList sampleList;
        private JButton sampleButton;
    
        public static void main(String[] args) throws Exception {
            //Schedule a job for the event-dispatching thread:
            //creating and showing this application's GUI.
            javax.swing.SwingUtilities.invokeLater(new Runnable() {
    
                public void run() {
                    try {
                        new MyJMXSwingApp();
                    } catch (Exception e) {
                        consoleOutput("Exception occurred: " + e.getMessage());
                    }
                }
            });
        }
    
        public MyJMXSwingApp() throws Exception {
            frame = new JFrame("Sample JMX-Swing App");
            frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
            addComponentsToPane(frame.getContentPane());
            // call similar to below could be used to obtain a MBean proxy whose operations can be called based on UI actions
            // findVMsAndTheirMBeans(null, null);
            frame.pack();
            frame.setVisible(true);
        }
    
        // assume some MBean implements this interface
        interface MyMBeanInterface {
            public String getSomeAttribute();
            public void setSomeAttribute(String value);
            public String[] someOperation(String param1, int param2);
            boolean doSomething();
        }
    
        /*
        * Sample dual-purpose method to show how to connect to an MBean
        * if vmName & mBeanName is null, just output to terminal the local
        * Java virtual machines running and their MBeans & return null.
        * If vmName & mBeanName is provided, this method shows a way of connecting
        * to the MBean and calling its operations.  For the purposes of this
        * sample the mBeanName is assumed to implement the MyMBeanInterface above.
        * Use the (null, null) settings to determine the vmName(s) and mBeanName(s)
        * you wish to subsequently connect to.
        */
        private void findVMsAndTheirMBeans(String vmName, String mBeanName) {
            java.util.List vms = VirtualMachine.list();
            for (VirtualMachineDescriptor desc : vms) {
                VirtualMachine vm;
                try {
                    // if vmName supplied, skip all but selected one.
                    if (vmName != null && !desc.displayName().contains(vmName)) {
                        continue;
                    }
                    vm = VirtualMachine.attach(desc);
                    consoleOutput("Attached to VM: " + desc.displayName() + ", querying its MBeans...");
                    String CONNECTOR_ADDRESS = "com.sun.management.jmxremote.localConnectorAddress";
                    Properties props = vm.getAgentProperties();
                    String connectorAddress = props.getProperty(CONNECTOR_ADDRESS);
                    if (connectorAddress == null) {
                        consoleOutput("Connector address could not be found, starting its JMX agent...");
    
                        String agent = vm.getSystemProperties().getProperty("java.home") +
                                File.separator + "lib" + File.separator + "management-agent.jar";
                        vm.loadAgent(agent);
    
                        // agent is started, get the connector address
                        connectorAddress = vm.getAgentProperties().getProperty(CONNECTOR_ADDRESS);
                        if (connectorAddress == null) {
                            consoleOutput("*Still* no connector address, skipping this VM...");
                            continue;
                        } else {
                            consoleOutput("Agent started");
                        }
                    }
                    JMXServiceURL url = new JMXServiceURL(connectorAddress);
                    JMXConnector jmxConnector = JMXConnectorFactory.connect(url);
                    try {
                        MBeanServerConnection mbeanConn = jmxConnector.getMBeanServerConnection();
                        Set beanSet = mbeanConn.queryNames(null, null);
                        for (ObjectName on : beanSet) {
                            if (mBeanName == null) {
                                consoleOutput("    Found MBean: " + on.getCanonicalName());
                            } else {
                                // show how to connect to and call methods on an MBean proxy.
                                if (mBeanName.equals(on.getCanonicalName())) {
                                    MyMBeanInterface proxy =
                                            JMX.newMBeanProxy(mbeanConn, on,
                                                    MyMBeanInterface.class, true);
    
                                    String mBeanDesc = mBeanName + " of VM " + desc.displayName();
                                    if (proxy != null) {
                                        consoleOutput("Connected to MBean " + mBeanDesc);
                                        // call operations provided by the MBean
                                        boolean result = proxy.doSomething();
                                        String names[] = proxy.someOperation("hello", 4);
                                        // ... do something with result, names[], etc.
                                    } else {
                                        consoleOutput("Error connecting to " + mBeanDesc);
                                        return;
                                    }
                                }
                            }
                        }
                    } finally {
                            jmxConnector.close();
                    }
                } catch (AttachNotSupportedException e) {
                    consoleOutput("Attach API not supported for VM: " + desc.displayName() + "; skipping.");
                } catch (Exception e) {
                    consoleOutput("Error: " + e.getMessage());
                }
            }
        }
    
    
        private void addComponentsToPane(Container pane) {
            pane.setLayout(new GridBagLayout());
            addComponent(new JLabel("Sample Items"), 0, 0, 2);
    
            sampleListModel = new DefaultListModel();
            sampleListModel.addElement("Dog");
            sampleListModel.addElement("Cat");
            sampleListModel.addElement("Fish");
    
            sampleList = new JList(sampleListModel);
            sampleList.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
            sampleList.addMouseListener(this);
            sampleList.setVisibleRowCount(9);
    
            addComponent(new JScrollPane(sampleList), 1, 0, 2);
    
            sampleButton = new JButton("Sample Button");
            sampleButton.addActionListener(new ActionListener() {
                public void actionPerformed(ActionEvent e) {
                    alertBox("Button Clicked.");
                }
            });
            addComponent(sampleButton, 1, 2, 1);
        }
    
        private void addComponent(JComponent component, int gridy, int gridx, int gridwidth) {
            addComponent(component, gridy, gridx, gridwidth, GridBagConstraints.CENTER);
        }
    
        private void addComponent(JComponent component, int gridy, int gridx, int gridwidth, int anchor) {
            GridBagConstraints gbc = new GridBagConstraints();
            gbc.gridx = gridx;
            gbc.gridy = gridy;
            gbc.gridwidth = gridwidth;
            gbc.anchor = anchor;
            frame.getContentPane().add(component, gbc);
        }
    
        public void mouseClicked(MouseEvent event)
        {
            if (event.getClickCount() == 2) {
                if (event.getSource() == sampleList) {
                    alertBox("List Box double-click: " + sampleList.getSelectedValue());
                }
            }
        }
    
        private static void consoleOutput(String msg) {
            System.out.println(msg);
        }
    
        private static void alertBox(String msg) {
            JOptionPane.showMessageDialog(null, msg);
        }
    
    }
    
  2. To make MyJMXSwingApp as easy to start from a terminal window as JConsole, the above sample can be placed into a Maven project and subsequently run via the Exec Maven Plugin's mvn exec:java command, after a one-time build via mvn clean install. A POM for the above sample:

    <?xml version="1.0" encoding="UTF-8"?>
    <project xmlns="http://maven.apache.org/POM/4.0.0"
             xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
             xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
        <modelVersion>4.0.0</modelVersion>
    
        <groupId>LDA-Runner</groupId>
        <artifactId>LDA-Runner</artifactId>
        <version>1.0-SNAPSHOT</version>
    
        <dependencies>
            <dependency>
                <groupId>sun.jdk</groupId>
                <artifactId>tools</artifactId>
                <version>1.7.0_42</version>
                <scope>system</scope>
                <systemPath>${java.home}/../lib/tools.jar</systemPath>
            </dependency>
        </dependencies>
    
        <build>
            <plugins>
                <plugin>
                    <groupId>org.codehaus.mojo</groupId>
                    <artifactId>exec-maven-plugin</artifactId>
                    <version>1.3</version>
                    <executions>
                        <execution>
                            <goals>
                                <goal>java</goal>
                            </goals>
                        </execution>
                    </executions>
                    <configuration>
                        <executable>java</executable>
                        <includePluginDependencies>true</includePluginDependencies>
                        <mainClass>sample.MyJMXSwingApp</mainClass>
                    </configuration>
                    <dependencies>
                        <dependency>
                            <groupId>sun.jdk</groupId>
                            <artifactId>tools</artifactId>
                            <version>1.7.0_42</version>
                            <scope>system</scope>
                            <systemPath>${java.home}/../lib/tools.jar</systemPath>
                        </dependency>
                    </dependencies>
                </plugin>
            </plugins>
        </build>
    </project>
    

    Note the tools.jar dependency listed in two places above; this is needed for working with the JMX MBeans.

  3. MyJMXSwingApp relies on JMX.newMBeanProxy() to call the MBean's operations. This method requires your MBean to implement an interface containing the desired operations and attributes you wish to access. If your MBean doesn't implement an interface and you don't have the ability to modify it, the createMBean(...) methods of MBeanServerConnection may provide an alternative approach, I have not worked with it however. The sample app includes and uses the following MBean interface, expanded a bit from the sample one provided by the newMBeanProxy() JavaDoc:

    public interface MyMBeanInterface {
        public String getSomeAttribute();
        public void setSomeAttribute(String value);
        public String[] someOperation(String param1, int param2);
        boolean doSomething();
    }
    

    Make sure your operations don't start with a get... or a set... else the proxy will think that they are attributes instead and raise a "can't find attribute XXX" error when you try to call the operation.

  4. Obtain the names of the Java VM and which of its MBeans you wish to connect to, you'll need these strings to connect to the MBean. Uncommenting the call to findVMsAndTheirMBeans(null, null) in MyJMXSwingApp's constructor outputs to the console window the names of all the Java VMs presently operating on your machine along with their available MBeans. For example, for an IntelliJ IDEA instance running on your machine:

    Attached to VM: com.intellij.idea.Main, querying its MBeans...
        Found MBean: java.lang:name=CMS Old Gen,type=MemoryPool
        Found MBean: java.lang:type=Memory
        Found MBean: java.lang:name=Code Cache,type=MemoryPool
        Found MBean: java.lang:type=Runtime
        Found MBean: java.lang:type=ClassLoading
        Found MBean: java.nio:name=direct,type=BufferPool
        Found MBean: java.lang:type=Threading
        Found MBean: java.lang:name=ConcurrentMarkSweep,type=GarbageCollector
    ...several others...
    
  5. Once you know the name of the VM and its MBean, calling findVMsAndTheirMBeans(vmName, mBeanName) will connect to a proxy for the MBean and call operations on it. However, this method assumes your MBean is implementing the fictional MyMBeanInterface defined in this class and calls two of its operations:

    MyMBeanInterface proxy = JMX.newMBeanProxy(mbeanConn, on, MyMBeanInterface.class, true);
    boolean result = proxy.doSomething();
    String names[] = proxy.someOperation("hello", 4);
    

    A real newMBeanProxy() call would need to use your specific MBean interface and call on that proxy the operations that it provides.

  6. Note that JConsole is open source with its code available from the OpenJDK project, helpful if there's other functionality offered by JConsole that you're interested in modifying or mimicking. For source checkouts, OpenJDK uses Mercurial as its version control system, but you can also browse the JConsole source online or just download a source bundle, extract it and navigate to the JConsole folder under src/share/classes/sun/tools/jconsole.

Posted by Glen Mazza in Programming at 07:00AM Jun 15, 2014 | Comments[0]

Comments
Post a Comment:

Calendar
« November 2019
Sun Mon Tue Wed Thu Fri Sat
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
Today
About Me
Java Software Engineer
TightBlog maintainer, Apache CXF committer
Arlington, Virginia USA
gmazza at apache dot org
GitHub LinkedIn
Blog Search
Apache CXF/SOAP tutorial
Blog article index


Today's Blog Hits: 2002

Navigation
About Blog
Blog software: TightBlog 3.5.3
Application Server: Tomcat
Database: MySQL
Hosted on: Linode
SSL Certificate: Let's Encrypt
Installation Instructions