Thursday, May 15, 2014

Using Java generics for proxy implementation

Overview

Java's generics is very powerful and easy to use. Its common use is straight forward, for example in the Collections API. In this post I would like to show another use, not so common, of generics. The purpose is to implement a proxy for remote class, e.g. when using RMI (Remote Method Invocation), JMX (Java Management Extensions), etc. This is handy since you usually have similar basic functionality in your proxy - connect, disconnect, etc.

Design

Class diagram of the participants in the design
The participants in the design - remote classes in green, local classes in blue (the interface is deployed remotely and locally)
As you can see in the class diagram above, there are 4 participants:
  • MyInterface -
    This is the interface of the remote class. This interface must be deployed both remotely and locally.
  • MyRemoteClass -
    This is the implementation of the remote class. It is deployed remotely and is the target for our proxy (in the client-side)
  • AbstractProxy -
    This is an abstract implementation of a proxy. It has some basic common proxy functionality such as connect, disconnect, etc. and it holds the actual proxy object encapsulated inside. This class is deployed locally (in the client-side).
  • MyProxy -
    This is the specific proxy for MyRemoteClass. It implements MyInterface and extends AbstractProxy setting the generic type to be MyInterface.

Implementation

Here is partial and simplified implementation of the classes in the design above.

MyInterface

public interface MyInterface {

    void doSomething();

}

MyRemoteClass

public class MyRemoteClass implements MyInterface {

    @Override
    public void doSomething() {
        // Do something...
    }

}

AbstractProxy

public abstract class AbstractProxy<T> {
 
    private T proxy;
 
    protected T getProxy() {
        return proxy;
    }
 
    public void connect() {
        // Connect to the remote class - set the encapsulated proxy object 
    }
 
    public void disconnect() {
        // Disconnect from the remote class - reset the encapsulated proxy object 
    }
 
    public boolean isConnected() {
        // Check whether the proxy is connected
        return true;
    }

}

MyProxy

public class MyProxy extends AbstractProxy<MyInterface> implements MyInterface {

    @Override
    public void doSomething() {
        // Delegate the call to the proxy
        getProxy().doSomething();
    }

}

Use Case Example

Here is a more realistic example of how I used this pattern (with some simplification and modification) for implementing an abstract proxy for JMX agents.

Background

(If you are not interested in the "why", you can skip to the Implementation part)

A few years ago, as part of a distributed system I was working on (school project...), I wanted to implement agents that will be deployed on the servers and listen for requests. This agents needed to answer 2 main requirements:

  1. Remote method invocation -
    The ability to connect from one machine to another and trigger actions on the remote server.
  2. Monitor the agents on the remote server -
    The ability to connect from a PC to the agent on the remote server and interact with it for monitoring, configuration, perform "manual" actions, etc.

In the beginning I chose Java RMI for the task. It's quite easy to use (in development), and once you run the server you're OK. But then, if you want to monitor it from a remote PC you need to develop a dedicated client UI for that. Therefore I looked for another technology that will give me similar capabilities (of Remote Method Invocation) using an API, but will also answer my other requirement with an existing UI (command line or graphical).
The answer I found was JMX (Java Management Extensions). Although this is not exactly the purpose of JMX, it was close enough and it answers my requirements. The UI for monitoring is JConsole (part of the SDK). It enables you to connect to a Java process (local or remote) and monitor its threads, memory, loaded classes, registered MBeans and more. In the MBeans section you can view & update the attributes each MBean exposes and invoke its methods. This is a simple generic UI which was quite enough for the requirements.

After implementing the MBeans I needed to implement the clients which will enable me to programmatically connect to the remote agents. When I started I noticed there is some basic code that I repeats itself in all clients, just with small differences. These are good candidates for refactoring.

Implementation


public abstract class AbstractJmxClient<MBeanInterface> {
    private MBeanInterface proxy = null;
    private JMXConnector conn = null;
    private MBeanServerConnection mbsc = null;

    // JMX properties which are used to connect to the registered MBean 
    private String jmxHost = null;
    private String jmxDomain = null;
    private String jmxKeyName = null;
    private String jmxKeyValue = null;
    private int    jmxPort = -1;

    // Constructor getting all the JMX properties...
    // Getters for all the JMX properties...

    protected MBeanInterface getProxy() {
        return _proxy;
    }

    /**
     * Connect to the remote JMX agent
     */
    @SuppressWarnings("unchecked")
    public void connect() throws Exception
    {
        String urlSuffix = null;
        try {
            urlSuffix = this.jmxHost + ":" + this.jmxPort + "/jmxrmi";
            ObjectName oName = new ObjectName(this.jmxDomain, this.jmxKeyName, this.jmxKeyValue);
            JMXServiceURL url = new JMXServiceURL("service:jmx:rmi:///jndi/rmi://" + urlSuffix);
            if (this.conn == null) {
                this.conn = JMXConnectorFactory.connect(url, null);
            }
            if (this.mbsc == null) {
                this.mbsc = this.conn.getMBeanServerConnection();
            }
            Type type = getClass().getGenericSuperclass();
            Class<MBeanInterface> mbiClass = null;
            if (type instanceof ParameterizedType) {
                ParameterizedType paramType = (ParameterizedType) type;
                mbiClass = (Class<MBeanInterface>) paramType.getActualTypeArguments()[0];
            }
            this.proxy = JMX.newMBeanProxy(this.mbsc, oName, mbiClass);
        } catch (RuntimeException e) {
            String errMsg = String.format(
                "Could not connect to the JMX agent: [%s:%s=%s] at %s due to the following error: %s",
                this.jmxDomain, this.jmxKeyName, this.jmxKeyValue, urlSuffix, e.getMessage());
            throw new Exception(errMsg, e);
        }
    }

    /**
     * Disconnect from the remote JMX agent
     */
    public void disconnect() {
        this.proxy = null;
        this.mbsc = null;
        if (this.conn != null) {
            try {
                this.conn.close();
            } catch (IOException e) {
                // Write to the trace
            }
            this.conn = null;
        }
    }

    /**
     * Check whether the client is connected to the remote JMX agent
     * 
     * @return true if the client is connected
     */
    public boolean isConnected() {
        if (this.proxy == null || this.conn == null) {
            return false;
        }
        try {
            this.conn.getConnectionId();
            return true;
        } catch (IOException e) {
            return false;
        }
    }

}


No comments:

Post a Comment