Tomcat and JNDI

Let’s consider the standard baseline Java EE application: it’s a web application, with some kind of framework, using JDBC or something that wraps it.
In canonical Java EE form, it would be a JSF front end and request routing mechanism, with JPA as the data framework, with resources acquired via JNDI. (Note: “canonical Java EE form” doesn’t imply in any way that this is what people actually do, because normally people avoid JSF like the plague.)
With Tomcat, however, you are more likely to see Spring MVC or Struts 2, with Hibernate or MyBatis, talking directly to the database with an embedded database driver and connection pool.

This is anecdata with respect to the typical Tomcat deployment. We don’t pretend otherwise, and neither should you – nor should you be offended that we see this sort of thing so often that the anecdata seems pretty valid, even without formal substantiation.

One of the problems is that while Tomcat has a proper JNDI container, it’s not very easy to use. It even has documentation on setting up a JNDI datasource (which you should be doing) but it’s not complete; I followed the documentation and it did not work.
I’m not even sure why it didn’t work. With that said, I created an example that does work, and it’s on github.
So let’s walk through this sample project. I’m going to include a JNDI resource, and a servlet that uses it; the servlet will show some metadata about the JDBC driver acquired from JNDI, which should be enough of a smoke test that it validates the deployment.
The container I’m using is Tomcat 8.
My Maven configuration is quite simple, and comes directly from a Maven archetype, with the addition of the database driver and the modification of the Java version to 1.8. (Changes are shown in bold.)

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://maven.apache.org/POM/4.0.0"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <artifactId>examples-parent</artifactId>
        <groupId>org.javachannel.examples</groupId>
        <version>1.0-SNAPSHOT</version>
    </parent>
    <groupId>org.javachannel.examples</groupId>
    <artifactId>servlet-jndi-example</artifactId>
    <version>1.0-SNAPSHOT</version>
    <packaging>war</packaging>
    <name>servlet-jndi-example</name>
    <properties>
        <endorsed.dir>${project.build.directory}/endorsed</endorsed.dir>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>
    <dependencies>
        <dependency>
            <groupId>javax</groupId>
            <artifactId>javaee-web-api</artifactId>
            <version>7.0</version>
            <scope>provided</scope>
        </dependency>
        <dependency>
            <groupId>org.hsqldb</groupId>
            <artifactId>hsqldb</artifactId>
            <version>2.3.2</version>
        </dependency>
    </dependencies>
    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.1</version>
                <configuration>
                    <source>1.8</source>
                    <target>1.8</target>
                    <compilerArguments>
                        <endorseddirs>${endorsed.dir}</endorseddirs>
                    </compilerArguments>
                </configuration>
            </plugin>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-war-plugin</artifactId>
                <version>2.3</version>
                <configuration>
                    <failOnMissingWebXml>false</failOnMissingWebXml>
                </configuration>
            </plugin>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-dependency-plugin</artifactId>
                <version>2.6</version>
                <executions>
                    <execution>
                        <phase>validate</phase>
                        <goals>
                            <goal>copy</goal>
                        </goals>
                        <configuration>
                            <outputDirectory>${endorsed.dir}</outputDirectory>
                            <silent>true</silent>
                            <artifactItems>
                                <artifactItem>
                                    <groupId>javax</groupId>
                                    <artifactId>javaee-endorsed-api</artifactId>
                                    <version>7.0</version>
                                    <type>jar</type>
                                </artifactItem>
                            </artifactItems>
                        </configuration>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>
</project>

All that’s well and good; now let’s take a look at our servlet, the thing that serves as our smoke test. We’re not including the import or package statements, for brevity.

@WebServlet(name = "example", urlPatterns = "/exampleservlet")
public class ExampleServlet extends HttpServlet {
    @Resource(name = "java:comp/env/jdbc/hsqldb")
    DataSource dataSource;
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp)
            throws ServletException, IOException {
        PrintWriter out = resp.getWriter();
        resp.setContentType("text/plain");
        try (Connection conn = dataSource.getConnection()) {
            DatabaseMetaData dmd = conn.getMetaData();
            out.printf("We hit the servlet.%nCan we get a data source? %s%n"
                    + "   (instance is %s)%n",
                    dataSource == null ? "no" : "yes",
                    dataSource == null ? "null" : dataSource.toString());
            out.printf("Database version: %d.%d%n",
                    dmd.getDatabaseMajorVersion(),
                    dmd.getDatabaseMinorVersion());
        } catch (SQLException e) {
            e.printStackTrace(out);
        }
    }
}

Note the @Resource in that servlet – it says that the container is responsible for doing a JNDI lookup (using the name “java:comp/env/jdbc/hsqldb“) with the right type (javax.sql.DataSource). We don’t have any other magic to perform… except that we don’t have that name configured anywhere yet.
The way this is done is via a file called context.xml, which – as you might surmise – defines things for a specific context. You can specify contexts in a lot of ways; one way is to modify the container’s main context.xml (which handles global container changes), and you can also modify contexts on a per-host or per-application basis.
Because this is a developer-targeted web application, we’re going to make it an application-specific context; this is going to be held in a resource named /META-INF/context.xml in our deployable WAR file.
Context files are XML, so we need a root node, <Context />, because nothing’s good enough without mixed-case in XML. (Yay, Tomcat!)
What we need to do is define the actual JDBC resource, which is done with the following XML:

<Resource name="jdbc/hsqldb" auth="Container"
          type="javax.sql.DataSource"
          maxTotal="100" maxIdle="30"
          maxWaitMillis="10000"
          username="sa" password=""
          driverClassName="org.hsqldb.jdbcDriver"
          url="jdbc:hsqldb:mem:testdb"/>

This says to use the org.hsqldb.jdbcDriver class as a DataSource, located at the name “jdbc/hsqldb“. In Tomcat, if this is at the web application’s level, it gets placed in the local context (at “java:comp/env/jdbc/hsqldb“, and on deployment, our servlet will have the resource located at that name injected, and we get the exciting output of:

We hit the servlet.
Can we get a data source? yes
   (instance is org.apache.tomcat.dbcp.dbcp2.BasicDataSource@25dc1971)
Database version: 2.3

Now, this isn’t horribly useful, honestly, because it ties the resource directly into the local namespace.
You can (and probably should) put it at the general context.xml (held in $CATALINA_HOME/conf/context.xml), which should be done rarely (because the container needs to be restarted when this file is changed). Then you’d need a ResourceLink that took the global name (which in this example would be “jdbc/hsqldb“) and link it to the local name (“java:comp/env/jdbc/hsqldb“), which would fit the way most Java EE containers would (and should) do it.

Relevant Links