Friday, July 30, 2010

JMX with Spring and Annotation (Part 3)

A few problems I encountered during the whole setup process.
1. In the CustomJMXAuthenticator's authenticate method, my original code was:
public Subject authenticate(Object credentials) {
    if(credentials == null || !(credentials instanceof String[]))
        throw new SecurityException("Credentials are required!");
    String[] info = (String[]) credentials;
    Subject subject = new Subject();
    if(StringUtils.equals(info[0], userName)
        && StringUtils.equals(info[1], password))
        subject.getPrincipals().add(new JMXPrincipal(userName));
    return subject;
}
This works fine in Java 6 Jconsole, incorrect user name and password will result in access deny.  However, in Java 5 Jconsole, CustomJMXAuthenticator becomes useless.  Turns out, in Java 5, as long as a subject is returned, Jconsole will consider the authentication is a success.  So I have to throw a SecurityException to make the authentication work in Java 5.

2. Timing issue in Spring beans.  registry bean has to be fully created before serverConnector bean is being created.  Otherwise, Spring will throw exception at runtime.  For it to work, I have to put registry bean in the top of the context file and serverConnector bean at the bottom.

3. Linux system has a known issue with JMX remote access.  The issue is that when resolving host, if no java.rmi.server.hostname property is defined, it will return ip address 127.0.1.1 instead of 127.0.0.1.  The solution then is set java.rmi.server.hostname to localhost in system property as well as environment map in serverConnector bean.  So aside from the Spring configuration in Part 2.  I also added following code before registry bean:
<bean class="org.springframework.beans.factory.config.MethodInvokingFactoryBean">
    <property name="staticMethod" value="java.lang.System.setProperty"/>
    <property name="arguments">
        <list>
            <value>java.rmi.server.hostname</value>
            <value>localhost</value>
        </list>
    </property>
</bean>

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;
}

Tuesday, July 20, 2010

Basic Authentication with Spring Security (Part 3)

Basic authentication with Spring Security works great in my application until a web site is trying to access my application by pass in Authorization information through parameter instead of request header.  I have to create a CustomBasicProcessingFilter to handle the special case, which leads to my new application context configuration:
<security:http auto-config="false"
    access-denied-page="/noaccess.jsp"
    session-fixation-protection="none"
    entry-point-ref="authenticationEntryPoint">
    <security:intercept-url pattern="/helper*" filters="none" />
    <security:intercept-url pattern="/index.jsp*"
        access="ROLE_ANONYMOUS" />
    <security:intercept-url pattern="/logout.*"
        access="ROLE_ANONYMOUS" />
    <security:intercept-url pattern="/401.jsp*"
        access="ROLE_ANONYMOUS" />
    <security:intercept-url pattern="/noaccess.jsp*"
        access="ROLE_ANONYMOUS" />
    <security:intercept-url pattern="/*.do*"
        access="ROLE_USER" />
    <security:http-basic/>
    <security:anonymous />
    <security:logout logout-url="/logout.do"
        logout-success-url="/logout.html" />
    <security:concurrent-session-control
        max-sessions="1"
        exception-if-maximum-exceeded="true"/>
</security:http>

<security:authentication-provider
    user-service-ref="authenticationProvider" />

<bean name="authenticationProvider"
    class="com.my.company.web.CustomUserDetailsService"/>

<security:authentication-manager
    alias="authenticationManager"/>

<bean id="authenticationEntryPoint" class="org.springframework.security.ui.basicauth.BasicProcessingFilterEntryPoint">
    <property name="realmName" value="My Realm"/>
</bean>


<bean id="customFilter" class="com.my.company.web.CustomBasicProcessingFilter">
     <security:custom-filter after="BASIC_PROCESSING_FILTER"/>
</bean>
As you can see from above, Spring Security's Basic Authentication is still in use.  However, when this failed, CustomBasicProcessingFilter will be called to retry authentication.

entry-point-ref in security:http is a must for custom filters.  Without it, the application won't work at all.

One more think you'll need to watch out when adding custom filter.  If you have more than one servlet defined in web.xml but not all of them requires authentication, you might need to add them to the configuration for exclusion even if it was working without the configuration before the custom filter.