Showing posts with label Annotation. Show all posts
Showing posts with label Annotation. Show all posts

Friday, July 30, 2010

JMX with Spring and Annotation (Part 2)

Now that MBeanExporter is setup, we need to set the server-side connector to expose the MBeanServer.

For security concern, I decide to add in JMX authentication.  It is very complicated to setup with Java's configuration.  However, I managed to do so with Spring with two simple steps:
1. CustomJMXAuthenticator.java
package com.my.company.server;

import javax.management.remote.JMXAuthenticator;
import javax.management.remote.JMXPrincipal;
import javax.security.auth.Subject;

import org.apache.commons.lang.StringUtils;

/**
 * @author Yaohua Wang
 *
 */
public class CustomJMXAuthenticator implements JMXAuthenticator {

    private String userName;

    private String password;

    public void setUserName(String userName) {
        this.userName = userName;
    }

    public void setPassword(String password) {
        this.password = password;
    }

    public Subject authenticate(Object credentials) {
        if(credentials == null
            || !(credentials instanceof String[]))
            throw new SecurityException("Credentials are required!");

        String[] info = (String[]) credentials;
        if(StringUtils.equals(info[0], userName)
            && StringUtils.equals(info[1], password)){
            Subject subject = new Subject();
            subject.getPrincipals().add(new JMXPrincipal(userName));
            return subject;
        }
        throw new SecurityException("Unable to match credentials!");
    }
}

2. Spring configuration:
<bean id="registry"
    class="org.springframework.remoting.rmi.RmiRegistryFactoryBean"
    destroy-method="destroy" autowire="no">
    <property name="port" value="1099"/>
    <property name="alwaysCreate" value="true"/>
</bean>
...
<bean id="serverConnector" class="org.springframework.jmx.support.ConnectorServerFactoryBean" autowire="no" depends-on="registry">
    <property name="objectName" value="connector:name=rmi"/>
    <property name="serviceUrl"
        value="service:jmx:rmi:///jndi/rmi://localhost:1099/jmxrmi"/>
    <property name="threaded" value="true"/>
    <property name="daemon" value="true"/>
    <property name="environmentMap">
        <map>
            <entry key="java.rmi.server.hostname" value="localhost"/>
            <entry key="jmx.remote.authenticator">
                <bean
class="com.my.company.server.CustomJMXAuthenticator">
                    <property name="userName" value="jmxuser"/>
                    <property name="password" value="jmxpassword"/>
                </bean>
            </entry>
        </map>
    </property>
</bean>

Now start the server and open jconsole.  Type in following information:
  • Remote Process: localhost:1099
  • User Name: jmxuser
  • Password: jmxpassword
You should see the domain "ChannelServer" in MBeans tab.

Thursday, July 29, 2010

JMX with Spring and Annotation (Part 1)

I was working on migrating a standalone server to Spring when I started looking into Spring's JMX support.  And then I found how convienent it is to use Annotation to define MBeans instead of creating MBean interfaces.

First of all, I got rid of the old MBean interface:
ServerMBean.java
package com.my.company.server;

import java.io.IOException;

public interface ServerMBean {

    public String getName() throws IOException;

    public String getVersion() throws IOException;

    public long getChannelCount() throws IOException;

    public void shutdown() throws IOException;

    public void sendMessage(String message) throws IOException;

    public String[] getLoggers() throws IOException;

    public String getLogLevel(String category) throws IOException;

    public void setLogLevel(String category, String level) throws IOException;
}
And rewrite the Server class like:
package com.my.company.server;

import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Enumeration;

import org.apache.log4j.Level;
import org.apache.log4j.LogManager;
import org.apache.log4j.Logger;
import org.springframework.jmx.export.annotation.ManagedAttribute;
import org.springframework.jmx.export.annotation.ManagedOperation;
import org.springframework.jmx.export.annotation.ManagedOperationParameter;
import org.springframework.jmx.export.annotation.ManagedOperationParameters;
import org.springframework.jmx.export.annotation.ManagedResource;

@ManagedResource(objectName="ChannelServer:type=Server", description="Server")
public class Server {

protected Logger log = Logger.getLogger(getClass());

private String name;
private String version;
private long uptime;
private int channelCount;

protected Server(String name, String version) {
    this.name = name;
    this.version = version;
}

@ManagedAttribute(description="Server Name")
public String getName() {
    return name;
}

@ManagedAttribute(description="Server Version")
public String getVersion() {
    return version;
}

public long getUpTime() {
return uptime;
}

@ManagedAttribute(description="Channel Count")
public long getChannelCount() throws IOException {
    return this.channelCount;
}

@ManagedOperation(description="Shutdown Server")
public void shutdown() throws IOException {
    stop();
}

@ManagedOperation(description="Send a message")
@ManagedOperationParameters({
    @ManagedOperationParameter(name="msg", description="message")
})
public void sendMessage(String msg) throws IOException {
    //blah blah
}

@ManagedOperation(description="Set log level to the given category")
@ManagedOperationParameters({
    @ManagedOperationParameter(name="category", description="category"),
    @ManagedOperationParameter(name="level", description="log level")
})
public void setLogLevel(String category, String level) throws IOException {
    log.warn("Setting log level " + level + " for " + category);
    Level logLevel = Level.toLevel(level);
    LogManager.getLogger(category).setLevel(logLevel);
}

@ManagedOperation(description="Log level for given category")
@ManagedOperationParameters({
    @ManagedOperationParameter(name="category", description="category")
})
public String getLogLevel(String category) throws IOException {
    return LogManager.getLogger(category).getLevel().toString();
}

@ManagedOperation(description="List of loggers")
public String[] getLoggers() throws IOException {
    Enumeration<?> loggers = LogManager.getCurrentLoggers();
    ArrayList<String> loggerList = new ArrayList<String>();
    while (loggers.hasMoreElements()) {
        Logger logger = (Logger)loggers.nextElement();
        loggerList.add(logger.getName());
    }
    Collections.sort(loggerList);
    return loggerList.toArray(new String[0]);
}

public final void start() {
    log.info("Starting channel server...");
    try {
        //blah blah
        uptime = System.currentTimeMillis();
    } catch (Exception ex) {
        log.fatal("Error starting channel server", ex);
        stop();
    }
}

public final void stop() {
    log.info("Shutting down channel server...");
    try {
        //blah blah
    } catch (Throwable e) {
        log.error("error stopping container", e);
        System.exit(1);
    }
    log.info("Shutdown completed");
    System.exit(0);
}
}
Now let's moving on to Spring configuration. First, define the MBeanExporter:
<bean name="jmxAttributeSource" class="org.springframework.jmx.export.annotation.AnnotationJmxAttributeSource"/>

<bean id="exporter" class="org.springframework.jmx.export.MBeanExporter" lazy-init="false" autowire="no">
<property name="autodetect" value="true"/>
<property name="assembler">
<bean class="org.springframework.jmx.export.assembler.MetadataMBeanInfoAssembler">
<property name="attributeSource" ref="jmxAttributeSource"/>
</bean>
</property>
<property name="namingStrategy">
<bean class="org.springframework.jmx.export.naming.MetadataNamingStrategy">
<property name="attributeSource" ref="jmxAttributeSource"/>
</bean>
</property>
</bean>
An much simpler alternative:
<beans default-autowire="byName"
xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-2.5.xsd">
    ...
    <context:mbean-export/>
</beans>

JAXB2 Annotations

It is scary to find out that a dependency of your project is no longer supported and you really need a code update. That's what happened to me when I requested the source code of one dependency and I was told no one can find it.

The dependency I need to update is a JAXB client implementation to connect to liveglobalbid (lgb) - a leading developer of online auction software providing a high quality, real time simultaneous video and audio broadcast of a live auction sale. And surprisingly, I was able to get rid of the dependency along with the JAXB dependencies and use JAXB 2.0 API that comes with Java 5 and above.  Besides that, by using JAXB 2.0 Annotations, the new client implementation is simple and light.

First, a sample xml I'll get from lgb:
<?xml version="1.0" encoding="UTF-8" ?>
<caster xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="http://www.liveglobalbid.com/caster.xsd">
    <pid>25717</pid>
    <mountpoint name="blah blah">
        <source id="9">
            <type>sound</type>
            <remoteid>blah blah</remoteid>
            <average_frame_size>94</average_frame_size>
            <average_frame_rate>3.83</average_frame_rate>
        </source>
    </mountpoint>
</caster>

Basic structure here is:
  • Caster is the main object (XmlRootElement), which contains a pid and one or more mountpoint object (XmlElement).
  • A mountpoint object has one or more source object (XmlElement)
  • A source has type, remoteid, average_frame_size, and average_frame_rate (XmlElement)
  • So in total, we'll need 3 object models: Caster, MountPoint (XmlType), and Source (XmlType).
  • MountPoint has an attribute called name (XmlAttribute)
  • Source has an attribute called id (XmlAttribute)

Source.java
package com.my.company.caster.jaxb2;

import javax.xml.bind.annotation.XmlAttribute;
import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlType;

/**
 * @author k_wang
 *
 */
@XmlType
public class Source {

    @XmlAttribute
    protected int id;

    @XmlElement
    protected String type;

    @XmlElement
    protected String remoteid;

    @XmlElement
    protected int average_frame_size;

    @XmlElement
    protected double average_frame_rate;

    /**
     * default constructor
     */
    public Source() {
        super();
    }

    /**
     * full constructor
     */
    public Source(int id, String type, String remoteid,
          int average_frame_size, double average_frame_rate) {
        super();
        this.id = id;
        this.type = type;
        this.remoteid = remoteid;
        this.average_frame_size = average_frame_size;
        this.average_frame_rate = average_frame_rate;
    }

    public int getId() {
        return id;
    }

    public String getType() {
        return type;
    }

    public String getRemoteid() {
        return remoteid;
    }

    public int getAverageFrameSize() {
        return average_frame_size;
    }

    public int getAverageFrameRate() {
        return average_frame_rate;
    }
}


MountPoint.java
package com.my.company.caster.jaxb2;

import java.util.ArrayList;
import java.util.List;

import javax.xml.bind.annotation.XmlAttribute;
import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlType;

/**
 * @author k_wang
 *
 */
@XmlType(name="mountpoint")
public class MountPoint {

    @XmlAttribute
    protected String name;

    @XmlElement
    protected List<Source> source = new ArrayList<Source>();

    public MountPoint() {
        super();
    }

    /**
     * @param name
     * @param source
    */
    public MountPoint(String name, List<Source> source) {
        super();
        this.name = name;
        if(source != null)
            this.source = source;
    }

    public String getName() {
        return name;
    }

    public List<Source> getSource() {
        return source;
    }
}


Caster.java
package com.my.company.caster.jaxb2;

import java.util.ArrayList;
import java.util.List;

import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlRootElement;

/**
 * @author k_wang
 *
 */
 @XmlRootElement(name="caster")
public class Caster {

    @XmlElement
    protected long pid;

    @XmlElement
    protected List<MountPoint> mountpoint = new ArrayList<MountPoint>();

    public Caster() {
        super();
    }

    public Caster(long pid, List<MountPoint> mountpoint) {
        super();
        this.pid = pid;
        this.mountpoint = mountpoint;
    }

    public long getPid() {
        return pid;
    }

    public List<MountPoint> getMountpoint() {
        return mountpoint;
    }
}


And to get the Caster model from a URL:
public Caster getCaster(URL url) throws JAXBException {
    Caster caster = null;
    try {
        InputStream in = url.openStream();
        Unmarshaller unmarshaller = JAXBContext.newInstance(Caster.class).createUnmarshaller();
        caster = (Caster) unmarshaller.unmarshal(new StreamSource(in));
        in.close();
    } catch (IOException e) {
        new JAXBException("Unable to parse source input stream: ", e);
    }
    return caster;
}