Skip navigation

I've mentioned in the past that one of the new features in version 2.0 of the JMX API is "client contexts", which will allow a client to communicate context information to a server, and a server to adjust its behaviour accordingly.

The most obvious example is locale, where for example the client says that it is in the French locale and the server translates its messages and descriptions into French.

A slightly less obvious example is transaction ids. Here the client sets up a transaction (perhaps related to a database), performs a number of requests on the server in the context of that transactions, then commits or aborts the transaction. Updating configuration is the most obvious example where this is useful, especially if the configuration is controlled by several JMX MBeans. You want the update to have the usual transaction properties, like atomicity and consistency, and for this to be possible the participating MBeans must know their common transaction id.

(I should add that we don't have explicit support for transactions in the JMX API, but we do now have everything you need to build the transactional solution that is appropriate for you.)

Generalizing, a client context is aMap<String,String>, where the keys name context items such as locale or transaction id, with the corresponding values. For example, the locale item is named by the standard string "jmx.locale", so if the context includes the French locale then the Map will have the key"jmx.locale" with associated value"fr".

So what does all this look like in code? Let's take the French example to see.

Client side

Suppose the client has created a connection to the server in the usual way:

    JMXServiceURL url = ...server address...;
    JMXConnector connector = JMXConnectorFactory.connect(url);
    MBeanServerConnection connection = connector.getMBeanServerConnection();
 

All the new stuff related to client contexts is in the unimaginatively named ClientContext class. We can make afrenchConnection that is the same asconnection except that it addsjmx.locale=fr to every request:

    MBeanServerConnection frenchConnection =
        ClientContext.withContext(connection, "jmx.locale", "fr");
 

For the particular case of jmx.locale, there is also a special-purpose method, so another way to do it would be:

    MBeanServerConnection frenchConnection =
        ClientContext.withLocale(connection, Locale.FRENCH);
 

In either case, you can do everything withfrenchConnection that you could do withconnection, such as:

    String message = (String)
        frenchConnection.getAttribute(objectName, "Message");
 

Server side

Now suppose we want to write the MBean named byobjectName above. We want it to return a string from its Message attribute that is translated into the client's locale. Here's what the MBean would look like without any exotic context stuff:

    public interface ExampleMBean {
        public String getMessage();
    }

    public class Example implements ExampleMBean {
        public String getMessage() {
            return "My hovercraft is full of eels";
        }
    }
 

Here's a simplified context-aware version:

    public class Example implements ExampleMBean {
        public String getMessage() {
            Locale locale = ClientContext.getLocale();
            String language = locale.getLanguage();
            if (language.equals(Locale.FRENCH.getLanguage()))
                return "Mon aéroglisseur est rempli d'anguilles";
            else
                return "My hovercraft is full of eels";
        }
    }
 

(In reality you would want to use ResourceBundles and the like, rather than hard-coding translations like this.)

Like ClientContext.withLocale on the client side, on the server side ClientContext.getLocale() is a special-purpose version of the more general-purposeClientContext.getContext(). That method returns the completeMap<String,String> that makes up the context. So if you are using "com.example.xid" as the context key for transaction ids, then an MBean could get the current transaction id with:

        String xid = ClientContext.getContext().get("com.example.xid");
 

Context for local clients

A client in the same Virtual Machine as the server may have a direct reference to the MBeanServer object. In that case it has an alternative way of setting the context. Instead of usingwithContext to get an MBeanServer object where a context item has been set, it can use doWithContext to execute some code with the desired context:

    Map<String,String> newContext =
        Collections.singletonMap("jmx.locale", Locale.FRENCH.toString());
    String message =
    ClientContext.doWithContext(
            newContext,
            new Callable<String>() {
                return (String)
                    mbeanServer.getAttribute(objectName, "Message");
            }
        );
 

If you want to add items to the context rather than replacing it, you can do that straightforwardly:

    Map<String,String> newContext =
        new HashMap<String,String>(ClientContext.getContext());  // make a copy of the context
    newContext.put("jmx.locale", Locale.FRENCH.toString());      // change the copy
    ...doWithContext(newContext, ...)...
 

The Map returned by ClientContext.getContext() is unmodifiable. The only way to set it is usingdoWithContext to execute some code with the new context. As soon as that code returns, the previous context is restored. This means that you can safely call other code without worrying that it might change your context.

No need to care after this point

That's basically all you need to know to understand contexts. If you're not interested in the gory details, you can stop reading now.

How it works: the ugly truth

If we had designed contexts into the JMX Remote API from the very beginning then this would all work in an obvious way. Each protocol request from the client to the server would include the context, if any. On the server side, the context would be decoded from the request and attached to thread handling the request.

While we could have modified the JMX Remote API to work this way in the 2.0 API, it would have posed interoperability concerns. Contexts would only have been available if the client, server, and connector were all running the latest version. Pre-2.0 clients would have had no way to send contexts to servers; pre-2.0 servers would have had no way to receive contexts from clients; and pre-2.0 connector protocols (which is all connectors today) would have been unable to communicate contexts.

Instead, based on an idea by Simone Bordet, we defined a way to encode the context of a request into the target ObjectName of that request. We do this with a special pseudo-namespace called jmx.context//. When you access the ObjectName com.example:type=Foo in the French locale, you are actually accessingjmx.context//jmx.locale=fr//com.example:type=Foo. Since this looks like a namespace,ClientContext.withContext can reuse the narrowToNamespace mechanism (which is similar to a shell "cd" command). Pre-2.0 clients can manually insert thejmx.context//jmx.locale=fr// prefix. Pre-2.0 servers can install an MBeanServerForwarder to remove this prefix and use it to establish the thread context. And connector protocols do not need to be modified in any way, so all existing connectors support contexts.

I called jmx.context// a pseudo-namespace because even though it looks like one it is not actually implemented as one. There are a few reasons for this, but basically it boils down to a question of ordering with respect to other things you might want to add in the path between the client and the server. You might want to add an MBeanServerForwarder to localize the descriptions in the MBeanInfo returned by getMBeanInfo, but for this to work the locale would already need to be set when this localizer intercepted thegetMBeanInfo call. (In fact, the new API includes such a forwarder.) Or, you might already have anMBeanServerForwarder that performs security checks based on the ObjectName in the requests it sees; but if you now start adding jmx.context// into thatObjectName you'll have to change your checking logic.

For these reasons, we simulate ajmx.context// namespace in a specialMBeanServerForwarder that is deployed by default in the RMI connector and can be deployed in any other connector. This works well in practice, and in particular solves the problems described just above. The only drawback is that a local client of the MBean Server does not see the jmx.context//namespace by default. This usually does not matter, because a local client can use ClientContext.doWithContext, which will work. Client code that sometimes needs to operate with a remoteMBeanServerConnection and sometimes with a localMBeanServer should be rare. When it does arise, the solution is to wrap the local MBeanServer inside thecontext namespace forwarder. Since anMBeanServerForwarder is-an MBeanServer, you can just do this once and use the wrappedMBeanServer everywhere you used the originalMBeanServer before.

French locale encoded into ObjectName and decoded in MBeanServerForwarder 

[Tags: jmx jsr255]