Spring and JNDI (Tomcat or Jetty)
Recently I had need to deploy some Spring webapps which required predeploy configuration. Being the first time I had to find a serious answer I looked to the mythical JNDI for an answer. This document is meant to complement other Spring JNDI documents out there.
Essentially the problem is this. We need to deploy a webapp. The webapp needs configurations (database and webservice endpoint locations). Editing properties files or XML config within the webapp isn’t nice, because on a redeploy the config will be lost. Inside containers like Tomcat I am not aware of a way to easily add extra items to the classpath which won’t get nuked unexpectedly, so solutions like PropertyPlaceholderConfigurer don’t really fly as the properties file will end up within the webapp. And I don’t like the idea of setting environment variables for to locate such things.
In steps JNDI. JNDI is the Java answer to namespaced, centralised configuration. Application containers like Tomcat, Jetty, Glassfish, etc all allow you to export objects via JNDI. This may not be a completely correct description, but it is sufficient for this demonstration. The trick is how to use these. I’ll show Jetty configs (which in Maven live in src/main/webapp/WEB-INF/jetty-env.xml) as well as some references to Tomcat (in $CATALINA_HOME/conf/server.xml or better still, in $CATALINA_HOME/conf/Catalina/[engine]/<webapp>.xml) (more on Tomcat here). This means that the config lives OUTSIDE the webapp, and is immune to inadvertant changes, making hot-patching sites easier as War/webapp is independent of the site config.
First, exposing a DB.
This exposes a Postgres DB on the name icatDB. Note, there is a special JDBC namespace. Also note I am not using the normal Postgres connection class, rather I’m using the connection pooling class.
Jetty:
<?xml version="1.0"?>
<!DOCTYPE Configure PUBLIC "-//Mort Bay Consulting//DTD Configure//EN"
"http://jetty.mortbay.org/configure.dtd">
<Configure class="org.mortbay.jetty.webapp.WebAppContext">
<New id="icatDB" class="org.mortbay.jetty.plus.naming.Resource">
<Arg>jdbc/icatDB</Arg>
<Arg>
<New class="org.postgresql.ds.PGPoolingDataSource">
<Set name="serverName">localhost</Set>
<Set name="databaseName">icat2</Set>
<Set name="user">nigel</Set>
<Set name="password"></Set>
</New>
</Arg>
</New>
</Configure>
Tomcat: Example from a different project:
<Context path="/continuum">
<Resource name="jdbc/users"
auth="Container"
type="javax.sql.DataSource"
username="sa"
password=""
driverClassName="org.apache.derby.jdbc.EmbeddedDriver"
url="jdbc:derby:database/users;create=true" />
</Context>
Spring: I am going to pass this into an entity manager:
...
<bean id="entityManagerFactory"
class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean">
<property name="dataSource" ref="dataSource" />
<property name="jpaVendorAdapter">
<bean
class="org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter">
<property name="database" value="POSTGRESQL" />
<property name="showSql" value="true" />
<property name="generateDdl" value="true" />
</bean>
</property>
</bean>
<bean id="dataSource" class="org.springframework.jndi.JndiObjectFactoryBean">
<property name="jndiName" value="java:comp/env/jdbc/icatDB"/>
</bean>
So I cheated here. The data source already has a JNDI entrypoint so Spring isn’t involved. However in this next example I need to pass in a String which is a webservice endpoint address:Passing a String:
These kinds of elements are passed via the env namespace. From the Jetty JNDI page it tells me we can only pass in these types:
- java.lang.String
- java.lang.Integer
- java.lang.Float
- java.lang.Double
- java.lang.Long
- java.lang.Short
- java.lang.Character
- java.lang.Byte
- java.lang.Boolean
This is fine for configuration work, which is all we are doing.
Jetty:
<?xml version="1.0"?>
<!DOCTYPE Configure PUBLIC "-//Mort Bay Consulting//DTD Configure//EN"
"http://jetty.mortbay.org/configure.dtd">
<Configure class="org.mortbay.jetty.webapp.WebAppContext">
<New class="org.mortbay.jetty.plus.naming.EnvEntry">
<Arg>icatWebservice</Arg>
<Arg type="java.lang.String">http://hostname:8081/ws/ICAT</Arg>
</New>
</Configure>
Tomcat:
<Context path="/icat"
docBase="/var/home/tomcat/icat.war">
<Environment name="mcatextWebservice"
type="java.lang.String"
value="http://localhost:8180/mcatext/ws"/>
</Context>
Now Spring.
First import the jee namespace into your Spring config:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
...
xmlns:jee="http://www.springframework.org/schema/jee"
xsi:schemaLocation="...
http://www.springframework.org/schema/jee http://www.springframework.org/schema/jee/spring-jee-2.0.xsd">
...
Now we can use the jee:jndi-lookup element in place of a value element:
...
<bean id="icatConnectionManagerBase" scope="session"
class="au.edu.archer.services.icat.ICATWsClientImpl">
<constructor-arg index="0">
<jee:jndi-lookup jndi-name="java:comp/env/icatWebservice"/>
</constructor-arg>
</bean>
There has also been discussion of writing a PropertyPlaceholderConfigurer like bean which can bring all the JNDI into the properties scope so we could just use ${env.property} notation.
Maven classpath issues at compile time
Here’s a very weird Maven/Java issue. The error message (below) occurs in my build phase where JaxB is called to produce some Java objects from XML. JaxB calls HyperJaxB, and on some systems it crashes.
[ERROR] XJC while compiling schema(s): org.apache.commons.logging.LogConfigurationException: org.apache.commons.logging.LogConfigurationException: org.apache.commons.logging.LogConfigurationException: Class org.apache.commons.logging.impl.Log4JLogger does not implement Log
Caused by: org.apache.commons.logging.LogConfigurationException: org.apache.commons.logging.LogConfigurationException: org.apache.commons.logging.LogConfigurationException: Class org.apache.commons.logging.impl.Log4JLogger does not implement Log
Caused by: org.apache.commons.logging.LogConfigurationException: org.apache.commons.logging.LogConfigurationException: Class org.apache.commons.logging.impl.Log4JLogger does not implement Log
Caused by: org.apache.commons.logging.LogConfigurationException: Class org.apache.commons.logging.impl.Log4JLogger does not implement Log
But not all. My main dev machine works, my deployment machine doesn’t. My other deployment machine also works, but dave’s machine doesn’t. All of us are using the same Maven version (2.0.8), mostly the same Java versions (1.5.0_xx), and some are amd64, other are x86. However, the crash/work divide does not fall on this line. In fact, on the deployment machine both local users can compile OK, but the LDAP users can’t.
On Dave’s machine if we move the local repo to /tmp (-Dmaven.repo.local=/tmp/repository) it starts working. I did capture the class path on a few occasions, and it seems to be in quite a random order. I noticed that commons-logging is far closer to the start of the class path when the compile works, but I don’t have enough samples to confirm this.
Another curiosity I saw was that if your home directory is in a non-standard place, like /var/home/, then maven puts your repository in a directory called ? in the current working directory. Thats right, question mark. Oh dear.
Some machine details below:
Working machine:
Maven version: 2.0.8
Java version: 1.5.0_13
OS name: “linux” version: “2.6.23.9-mactel-ns” arch: “i386″ Family: “unix”
Broken machine:
Maven version: 2.0.8
Java version: 1.5.0_11
OS name: “linux” version: “2.6.18-8.1.3.el5xen” arch: “amd64″
Username: test.user
Maven version: 2.0.8
Java version: 1.5.0_13
OS name: “linux” version: “2.6.23.9-mactel-ns” arch: “i386″ Family: “unix”
Worked
Username: tomcat – LDAP/NFS
Didn’t work
Username: root
Worked
Username: srb
Worked
Powered by ScribeFire.
-
Archives
- July 2009 (1)
- February 2009 (1)
- December 2008 (1)
- November 2008 (1)
- October 2008 (2)
- September 2008 (1)
- August 2008 (1)
- April 2008 (2)
- February 2008 (4)
- January 2008 (2)
- November 2007 (1)
- October 2007 (1)
-
Categories
-
RSS
Entries RSS
Comments RSS