Sunday, January 25, 2009

Apache in front of Tomcat and JBoss

Recently I have to deploy two Java web applications on a single server (one IP address). Where static content was to be served by Apache Http web server instead of Java application servers. When we have multiple applications deployed on an instance of Tomcat or JBoss we have their URLs something like http://server:port/appA, http://server:port/appB etc. This kind of URLs does not look good when we publish it to external world. So instead of 'http://domainA.tld/appA' we wanted to have URL as 'http://domainA.tld/'. This can be achieved by running applications on different instances of Tomcat / JBoss and fronting them with Apache using virtual hosts.

Running multiple instances of JBoss on a machine with single IP address is a nightmare, while it is much easier to run several Tomcat instances, as one has to change just four ports (8005, 8080, 8443, 8009). An application can be run at root context in Tomcat by adding a line like below, inside <host> element in $TOMCATHOME/conf/server.xml-

    <context path="" docbase="appA.war" unpackWAR="false" />

There are pretty decent documentation available on Apache, Tomcat and JBoss' website about their installation and configuration. So I will leave that discussion here itself. Though there is lots of documentation available for Apache virtual hosting also but it takes lots of effort for a newbie to do the configuration and put it in front of application servers. Here I am assuming that Apache Tomcat Connector (mod_jk) is also installed along with Apache web server.

To establish communication between Apache and application server, first we need to define connector workers as shown below-

    worker.list=appA,appB

    # Define appA
    worker.appA.port=9009
    worker.appA.host=localhost
    worker.appA.type=ajp13
    worker.appA.lbfactor=1

    # Define appB
    worker.appB.port=8009
    worker.appB.host=localhost
    worker.appB.type=ajp13
    worker.appB.lbfactor=1


In the above configuration appA's server's connector is listening on port 9009 and appB's on 8009. This connector port is different from http port (8080 by default for Tomcat). Now define the virtual hosts, this can be done either in Apache's httpd.conf or in a separate file and include that in httpd.conf.

   NameVirtualHost *:80

   LoadModule jk_module modules/mod_jk.so

   # Where to find workers defined above
   JkWorkersFile conf/extra/worker.properties

   <virtualhost *:80>
       ServerAdmin someone@domainA.tld
       DocumentRoot "/path/to/the/content"
       ServerName domainA.tld
       ErrorLog "logs/domainA-error.log"
       CustomLog "logs/domainA-access.log" common

       JkMount / appA
       JkMount /* appA
       JkUnMount /static|/* appA

       JkLogFile logs/mod_jk_appA.log
       JkLogLevel info
       JkLogStampFormat "[%a %b %d %H:%M:%S %Y]"
       JkOptions +ForwardKeySize +ForwardURICompatUnparsed -ForwardDirectories
       JkRequestLogFormat "%w %V %T"
   </virtualhost>

   <virtualhost *:80>
       ServerAdmin someone@domainB.tld
       DocumentRoot "/path/to/the/content"
       ServerName domainB.tld
       ErrorLog "logs/domainB-error.log"
       CustomLog "logs/domainB-access.log" common

       JkMount / appB
       JkMount /* appB
       JkUnMount /static|/* appB

       JkLogFile logs/mod_jk_appB.log
       JkLogLevel info
       JkLogStampFormat "[%a %b %d %H:%M:%S %Y]"
       JkOptions +ForwardKeySize +ForwardURICompatUnparsed -ForwardDirectories
       JkRequestLogFormat "%w %V %T"
   </virtualhost>


With above configuration all requests to domainA.tld are served by worker appA and domainB.tld is served by worker appB. The 'JkUnMount /static|/* appA' causes any URL like http://domainA.tld/static/* to be served by Apache instead of the application server. As Apache performs far better while serving the static content like images, html, css, java script etc. so it is good to unmount their URLs from the mod_jk worker. Most of the Jk* properties can be defined out of the <virtualhost> element at global level, that will avoid duplicate entries. Though defining them inside <virtualhost> element gives flexibility to have different values for each virtual host. Detailed information about Jk* is available here.

I have tested above configuration with following environment-

  • Java: 1.6.0_10
  • Tomcat: 6.0.18
  • JBoss: 5.0.0.GA
  • Apache: 2.2.11
  • OS: Fedora 9, Red Hat Enterprise Linux 5 and Windows Vista
Hope this will be useful to others and save their time.

Wednesday, December 24, 2008

JNDI lookup on Tomcat and JBoss using Spring

Unfortunately Java EE specs does not specify any standard way of JNDI naming conventions, hence most of the application servers have their own way of JNDI naming. On specifying a Datasource's JNDI name as 'jdbc/myDatasource', Tomcat (6) binds that as 'java:/comp/env/jdbc/myDatasource' while JBoss (5) binds as 'java:/jdbc/myDatasource'. So if one wants to deploy the application on multiple application servers then at least JNDI names has to be changed. I was wondering if Spring Framework has a solution for this, so tried to do lookup using <jee:jndi-lookup ...> as shown below-

<jee:jndi-lookup id="dataSource" jndi-name="jdbc/myDatasource" resource-ref="true"/>
it works well with Tomcat but fails on JBoss. Reason, the JNDI prefix is hard coded in Spring to 'java:comp/env/'.
package org.springframework.jndi;

public abstract class JndiLocatorSupport extends JndiAccessor {

    /** JNDI prefix used in a J2EE container */
    public static final String CONTAINER_PREFIX = "java:comp/env/";
    . . .
}
So it is obvious that it will fail on JBoss. To overcome this limitation, I came up with following solution-
public final class ServiceLocator {

    private static final Map<String, Object> services = new ConcurrentHashMap<String, Object>();

    private static ServiceLocator instance;

    private static Context context;

    static {
        try {
            Context initContext = new InitialContext();
            if (ServerDetector.isJBoss()) {
                context = (Context) initContext.lookup("java:");
            } else if (ServerDetector.isTomcat()) {
                context = (Context) initContext.lookup("java:/comp/env");
            } else {
                context = initContext;
                // or add more 'else if' blocks according to servers to be supported
            }
        } catch (Exception e) {
            throw new ExceptionInInitializerError(e);
        }
    }

    public DataSource getDataSource(String name) throws Exception {
        if (name == null || name.length() <= 0)
            throw new IllegalArgumentException("name");

        if (services.containsKey(name))
            return (DataSource) services.get(name);

        DataSource ds = (DataSource) context.lookup(name);

        services.put(name, ds);
        return ds;
    }
}

Here ServerDetector is a good utility class I found in Liferay code base. Now ServiceLocator takes care of application server specific JNDI prefixes and avoids hassles of changing configuration or code to deploy it on a specific server. In case of Spring instead of using <jee:jndi-lookup ...> one can do the lookup using above-mentioned ServiceLocator as shown below-
    <bean id="serviceLocator" class="com.vinodsingh.ServiceLocator" factory-method="getInstance" />

    <bean id="dataSource" factory-bean="serviceLocator" factory-method="getDataSource">
        <constructor-arg value="jdbc/myDatasource" />
    </bean>
Now entire code (including configuration files) becomes truly portable, at least for JNDI lookups :-)

Friday, September 19, 2008

JAX-WS web service and JBoss

Yesterday I wrote an entry about building JAX-WS web service. So thought about testing them on latest versions of JBoss. I chose 4.2.3 and 5.0.0 CR2 (released yesterday), both with Java 6.

JBoss 4.2.3
The deployment of web service failed with following error-

Error configuring application listener of class com.sun.xml.ws.transport.http.servlet.WSServletContextListener
java.lang.ClassNotFoundException: com.sun.xml.ws.transport.http.servlet.WSServletContextListener


which looks quite natural as JBoss has its own implementation of JAX-WS, so it does not have the RI classes. After bundling RI jars with application deployment was error free. I created a small standalone program to invoke the web service, which succeeded without any errors.

What about if web service consumer is also a web application? To test this scenario I created a small web application to consume the above-mentioned service and deployed on JBoss. On invoking web service the following exception was thrown-

org.jboss.ws.metadata.wsdl.WSDLException: Invalid default namespace: null

I thought that by bundling JAX-WS jars with the application, I can overcome this error. But faced another exception-
com.sun.xml.ws.client.WSServiceDelegate cannot be cast to javax.xml.ws.spi.ServiceDelegate21

Now I started scratching my head :-/ When in difficulty the developer community takes help of Google, but here Google did not gave clues. After investigating the JBoss directory structure and their contents I discovered an endorsed directory at ‘jboss-4.2.3.GA\lib\endorsed’ location. Then I deleted all jars (related to JAXB, JAXWS and JAAS) from there except the following ones-
  • Serializer.jar
  • Xalan.jar
  • xercesImpl.jar
and copied JAX-WS RI jars here. Now everything worked even no need to bundle the JAX-WS jars with the applications.

JBoss 5 RC2
Making the web service work on this version of the JBoss also required same steps as with 4.2.3 and service was consumed by a standalone application without any hiccups. Then tried to consume the service using same web application as used for 4.2.3 and with JBoss 5 also it failed with similar error on trying to invoke a service method-

org.jboss.ws.metadata.wsdl.WSDLException: Invalid default namespace: null

On bundling JAX-WS RI jars with the consumer application everything worked fine, so no more ClassCastException or using the endorsed directory mechanism. This is a good improvement over previous versions of JBoss.