Course – LS – All

Get started with Spring and Spring Boot, through the Learn Spring course:

>> CHECK OUT THE COURSE

1. Introduction

In this tutorial, we’ll see how to access MBeans from a shell script and the most common tools available.

The first thing to note is that JMX is based on RMI. So, the handling of the protocol is in Java. But we can wrap it inside a shell script to call it from the command line. In other words, this is especially useful if we want to automate stuff.

Despite being easy to implement, most JMX tools were abandoned or became unavailable. So let’s check a few tools and then write our own.

2. Write a Simple MBean

To test our tools, we’ll need an MBean. First, let’s create a simple calculator, starting with its interface:

public interface CalculatorMBean {

    Integer sum();

    Integer getA();

    void setA(Integer a);

    Integer getB();

    void setB(Integer b);
}

And then, let’s see the implementation:

public class Calculator implements CalculatorMBean {

    private Integer a = 0;
    private Integer b = 0;

    // getters and setters

    @Override
    public Integer sum() {
        return a + b;
    }
}

Let’s now create a simple CLI app to register our MBean. This code is standard, and the critical part is the implementation class and name we choose for our MBean. We’ll do this by calling registerMBean() on MBeanServer, passing an instance of Calculator, and instantiating an ObjectName.

To clarify, ObjectName receives an arbitrary String. But, to follow conventions, we’ll include our package name as the domain, along with a list of “key=value” pairs:

public class JmxCalculatorMain {

    public static void main(String[] args) throws Exception {
        MBeanServer server = ManagementFactory.getPlatformMBeanServer();
        server.registerMBean(
          new Calculator(), new ObjectName("com.baeldung.jxmshell:type=basic,name=calculator")
        );

        // ...
    }
}

Moreover, the only required key is “type,” which is irrelevant in our scenario. But this groups our MBean among other MBeans in the domain.

Finally, to keep our app from terminating, we’ll make it wait for user input:

try (Scanner scanner = new Scanner(System.in)) {
    System.out.println("<press enter to terminate>");
    scanner.nextLine();
}

To simplify our examples, we’ll run our app on port 11234. Also, let’s disable authentication and SSL with these JVM parameters:

-Dcom.sun.management.jmxremote.port=11234
-Dcom.sun.management.jmxremote.authenticate=false
-Dcom.sun.management.jmxremote.ssl=false

Now, we’re ready to try some tools.

3. Use Jmxterm From a Shell Script

Jmxterm is an interactive CLI tool offered as an alternative to monitoring MBeans with JConsole. But, with some input manipulation, we can create a script to use it in a non-interactive way. Let’s start by downloading jmxterm-1.0.4-uber.jar and putting it in the /tmp folder.

3.1. Wrapping Jmxterm in a Script

We’ll use a few features of the Jmxterm jar in our script: execute MBean operations and get or set attribute values. And to simplify it, we’ll define default values for its jar location, MBean address and name, operation, and the final command.

So, let’s create a file named jmxterm.sh:

#!/bin/sh

jar='/tmp/jmxterm-1.0.4-uber.jar'
address='localhost:11234'

mbean='com.baeldung.jxmshell:name=calculator,type=basic'
operation='sum'
command="info -b $mbean"

We should note that the address is only accessible after running JmxCalculatorMain. We’ll hold a method name or attribute from our MBean in the operation variable. Meanwhile, the command variable holds the final command sent to Jmxterm. As a default value, the info command displays operations and attributes available, and -b specifies the MBean to display.

Then, we’ll do some argument parsing, processing the –run, –set, and –get options by building the command variable:

while test $# -gt 0
do
    case "$1" in
    --run)
        shift; operation=$1
        command="run -b ${mbean} ${operation}"
    ;;
    --set)
        shift; operation="$1"
        shift; attribute_value="$1"
    
        command="set -b ${mbean} ${operation} ${attribute_value}"
    ;;
    --get)
        shift; operation="$1"

        command="get -b ${mbean} ${operation}"
    ;;
    esac
    shift
done

With run, we execute an MBean method. Similarly, with set, we set an attribute value. And, with get, we get its current value.

Finally, we echo our command, piping it to the Jmxterm jar along with the address of our running application. The -v silent option turns off verbosity, while -n tells Jmxterm not to print the user prompt. Therefore, this option is helpful as we’re not using it interactively:

echo $command | java -jar $jar -l $address -n -v silent 

3.2. Manipulating Our MBean

After making our script executable, we’ll run it without arguments to see its default behavior. Assuming the script is in the current directory, let’s run it:

./jmxterm.sh

This queries information on our MBean:

# attributes
  %0   - A (java.lang.Integer, rw)
  %1   - B (java.lang.Integer, rw)
# operations
  %0   - java.lang.Integer sum()

We can notice that set is removed from our setter names, and they appear in the attributes section.

So, let’s call setA():

./jmxterm.sh --set A 1

As a result, there’s no output if everything works. So let’s now call getA() to check the current value:

./jmxterm.sh --get A

Here’s the output we get:

A = 1;

Now, let’s set B to 2 and call sum():

./jmxter.sh --set B 2
./jmxter.sh --run sum

And here’s the output:

3

This solution works quite well for simple MBean operations.

4. Call cmdline-jmxclient From the Command Line

Our next tool is cmdline-jmxclient. We’ll use version 0.10.3, which works similarly to Jmxterm, but with no interactive option.

For the sake of brevity, let’s see an example of how to call it from the command line. First, we’ll set attribute values, then execute an operation:

jar=cmdline-jmxclient-0.10.3.jar
address=localhost:11234
mbean=com.baeldung.jxmshell:name=calculator,type=basic
java -jar $jar - $address $mbean A=1
java -jar $jar - $address $mbean B=1
java -jar $jar - $address $mbean sum

It’s worth noting that since our MBean doesn’t require authentication, we pass “” instead of the authentication parameters.

Here’s the output from running these commands:

11/11/2022 22:10:15 -0300 org.archive.jmx.Client sum: 2

The main difference from this tool is its output format. Also, it’s much lighter, with around 20 KB in size vs. more than 6 MB from Jmxterm.

5. Write a Custom Solution

Since the available options are pretty old, it’s an excellent time to understand how MBeans work under the hood. So let’s start implementing our solution with a wrapper for the classes we need from the javax.management package:

public class JmxConnectionWrapper {

    private final Map<String, MBeanAttributeInfo> attributeMap;
    private final MBeanServerConnection connection;
    private final ObjectName objectName;

    public JmxConnectionWrapper(String url, String beanName) throws Exception {
        objectName = new ObjectName(beanName);

        connection = JMXConnectorFactory.connect(new JMXServiceURL(url))
          .getMBeanServerConnection();

        MBeanInfo bean = connection.getMBeanInfo(objectName);
        MBeanAttributeInfo[] attributes = bean.getAttributes();

        attributeMap = Stream.of(attributes)
          .collect(Collectors.toMap(MBeanAttributeInfo::getName, Function.identity()));
    }

    // ...
}

First, our constructor receives a URL to connect to and an MBean name. We keep a reference of an ObjectName to use in future calls, then start the connection with JMXConnectorFactory. Then, we get an MBeanInfo reference, from which we extract attributes. Finally, we convert the MBeanAttributeInfo[] into a map. We’ll use this map later to determine if an argument is an attribute or an operation.

Now, let’s write some helper methods. We’ll start with a method to get and set attribute values. When we receive a value, we set it before returning the current one:

public Object attributeValue(String name, String value) throws Exception {
    if (value != null)
        connection.setAttribute(objectName, new Attribute(name, Integer.valueOf(value)));

    return connection.getAttribute(objectName, name);
}

Since we know our MBean contains only Integer attributes, we use Integer.valueOf() to construct our Attribute value. But, for a more robust solution, we’d need to use the information in our attributeMap to determine the correct type.

Likewise, since we know our MBean contains only no-arg operations, we call invoke() on our connection and pass empty arrays for params and signature:

public Object invoke(String operation) throws Exception {
    Object[] params = new Object[] {};
    String[] signature = new String[] {};
    return connection.invoke(objectName, operation, params, signature);
}

We’ll use this to invoke any methods in our MBean that aren’t related to attributes.

5.1. Write the CLI Part

Finally, let’s create a CLI app to manipulate our MBean from the shell:

public class JmxInvoker {

    public static void main(String... args) throws Exception {
        String attributeValue = null;
        if (args.length > 3) {
            attributeValue = args[3];
        }

        String result = execute(args[0], args[1], args[2], attributeValue);
        System.out.println(result);
    }

    public static String execute(
      String url, String mBeanName, String operation, String attributeValue) {
        JmxConnectionWrapper connection = new JmxConnectionWrapper(url, mBeanName);

        // ...
    }
}

Our main() method passes arguments to execute(), which processes them to decide if we want to perform an operation or get/set an attribute:

if (connection.hasAttribute(operation)) {
    Object value = connection.attributeValue(operation, attributeValue);
    return operation + "=" + value;
} else {
    Object result = connection.invoke(operation);
    return operation + "(): " + result;
}

If our MBean contains an attribute with the name of our operation, we call attributeValue(). Otherwise, we call invoke().

5.2. Call It From the Command Line

Assuming we packaged our application in a jar and put it in the location specified by the jar variable, let’s define our defaults:

jar=/tmp/jmx-invoker.jar
address='service:jmx:rmi:///jndi/rmi://localhost:11234/jmxrmi'
invoker='com.baeldung.jmxshell.custom.JmxInvoker'
mbean='com.baeldung.jxmshell:name=calculator,type=basic'

Then, we run these commands to set attributes and execute the sum() method in our MBean in a similar way to previous solutions:

$ java -cp $jar $invoker $address $mbean A 1
A=1

$ java -cp $jar $invoker $address $mbean B 1
B=1
$ java -cp $jar $invoker $address $mbean sum
sum(): 2

This custom JmxInvoker helps build MBeans handlers and to have complete control when parsing MBeans.

6. Conclusion

In this article, we saw tools used to manage MBeans and how to use them from a shell script. Then, we created a custom tool to handle them. Finally, more script examples are available in the repository.

And as always, the source code is available over on GitHub.

Course – LS – All

Get started with Spring and Spring Boot, through the Learn Spring course:

>> CHECK OUT THE COURSE
res – REST with Spring (eBook) (everywhere)
Comments are open for 30 days after publishing a post. For any issues past this date, use the Contact form on the site.