Comprehensive guide on setting up JNDI context in Apache Tomcat

There are several ways where you can define a JNDI resource in Tomcat. Let’s take a look at each one.

In this guide:

  • Tomcat is located in /system/server/tomcat
  • Web applications are in /system/site

Tomcat has built in factories to instantiate certain classes e.g. javax.sql.DataSource, JavaBeans.

Unless stated otherwise, all resources are defined as singleton instances (you can define singleton=”false” inside <Resource /> to make a resource to not be a singleton).
This is the example.SimpleBean class

package example;

public class SimpleBean {

	// cannot use field called name
	// cannot use field called description
	private String beanId;
	private String beanKey;
	private String beanValue;

	public SimpleBean() {
		System.out.println(Thread.currentThread().getClass() + " constructed");
	}

	public String getBeanId() {
		System.out.println(Thread.currentThread().getClass() + " getId(): " + beanId);
		return beanId;
	}

	public void setBeanId(String beanId) {
		System.out.println(Thread.currentThread().getClass() + " setId(" + beanId + ")");
		this.beanId = beanId;
	}

	public String getBeanKey() {
		System.out.println(Thread.currentThread().getClass() + " getName(): " + beanKey);
		return beanKey;
	}

	public void setBeanKey(String beanKey) {
		System.out.println(Thread.currentThread().getClass() + " setName(" + beanKey + ")");
		this.beanKey = beanKey;
	}

	public String getBeanValue() {
		System.out.println(Thread.currentThread().getClass() + " getDescription(): " + beanValue);
		return beanValue;
	}

	public void setBeanValue(String beanValue) {
		System.out.println(Thread.currentThread().getClass() + " setDescription(" + beanValue + ")");
		this.beanValue = beanValue;
	}

}

There is also this controller class, which we will use later to inspect

package example;

import java.io.IOException;
import java.io.PrintWriter;

import javax.naming.Context;
import javax.naming.InitialContext;
import javax.naming.NamingEnumeration;
import javax.naming.NamingException;

import jakarta.servlet.ServletException;
import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;

@WebServlet("/contextCheck")
public class ContextCheckController extends HttpServlet {
	protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
		PrintWriter writer = response.getWriter();
		writer.print("<pre>");
		try {
			Context ctx = new InitialContext();
			NamingEnumeration iterator = ctx.list("java:comp/env");
			while (iterator.hasMore()) {
				Object o = iterator.next();
				writer.println("- " + o);
			}

			String name = request.getParameter("name");
			if (name != null && !name.trim().isEmpty()) {
				Object o = ctx.lookup("java:comp/env/" + name);
				if (o != null) {
					writer.println(o);
				}
			}
		} catch (NamingException e) {
			writer.println("Error " + e.getExplanation());
			e.printStackTrace(writer);
		}
		writer.print("</pre>");
		writer.close();
	}
}

Let give each method a code name so it is easier to reference later.

Application level defined in application’s META-INF/ folder (code name: MetaInf)

root@aurora:/system/site/default/ROOT/META-INF# tree /system/site/default
/system/site/default
└── ROOT
    ├── index.html
    ├── META-INF
    │   └── context.xml
    └── WEB-INF
        └── classes
            └── example
                ├── ContextCheckController.class
                └── SimpleBean.class

Content of /system/site/default/ROOT/META-INF/context.xml

<Context>
        <Resource name="entity-meta-inf" type="example.SimpleBean" factory="org.apache.naming.factory.BeanFactory" />
</Context>

This is defined at application level, therefore as long as Tomcat can find SimpleBean.class under WEB-INF, Tomcat is fine. The drawback of this, if this is a DataSource and it is packaged in a war, the pipeline that builds this war needs to know the details of the DataSource like username, password, connection string, etc, which some people think it is not good because it is tight coupling. A change of database details would make it necessary to redeploy. An alternative to this is, the repository contains an empty context.xml file, during deploy, a script can create/copy the appropriate context.xml file.

Host level defined in the server’s conf/ host folder (code name: ConfHost)

root@aurora:/system/server/tomcat/conf/Catalina# tree /system/server/tomcat/conf/Catalina/
/system/server/tomcat/conf/Catalina/
└── default
    └── context.xml.default <- it must be named context.xml.default

Content of /system/server/tomcat/conf/Catalina/default/context.xml.default

<Context>
        <Resource name="entity-conf-application" type="example.SimpleBean" factory="org.apache.naming.factory.BeanFactory" description="Just a random thought" />
</Context>

The JNDI resource is now defined by the server’s administrators (not application’s developers).

Server level defined at server’s conf/context.xml file (code name: ConfContext)

Add the resource in /system/server/tomcat/conf/context.xml

<?xml version="1.0" encoding="UTF-8"?>
<Context>
    <WatchedResource>WEB-INF/web.xml</WatchedResource>
    <WatchedResource>WEB-INF/tomcat-web.xml</WatchedResource>
    <WatchedResource>${catalina.base}/conf/web.xml</WatchedResource>
    <Resource name="entity-conf-root" type="example.SimpleBean" factory="org.apache.naming.factory.BeanFactory" /> <- add this line
</Context>

With this method, each application can see this resource in their InitialContext.

In order for the server to instantiate the resource, the class file(s) need to be included in /system/server/tomcat/lib folder. In my example here I copied the class file(s) directly, alternatively you can drop your jar file(s).

root@aurora:/system/server/tomcat/lib/example# tree /system/server/tomcat/lib
/system/server/tomcat/lib
├── annotations-api.jar
├── catalina-ant.jar
├── catalina-ha.jar
├── catalina.jar
├── catalina-ssi.jar
├── catalina-storeconfig.jar
├── catalina-tribes.jar
├── ecj-4.27.jar
├── el-api.jar
├── example
│   └── SimpleBean.class
...

Server level defined at server’s conf/server.xml file inside GlobalNamingResources (code name: GlobalResource)

<GlobalNamingResources>
    <Resource name="universe-server-xml" type="javax.sql.DataSource" />
</GlobalNamingResources>

Define the above block of text inside the <Server /> element inside server.xml. This is a single instance per server. All applications will be using the same instance. However, with this method, each application cannot see this resource. In order to access this, an application needs to define a <ResourceLink /> element. Note that you can see that I’m defining a DataSource instead of SimpleBean, for some reason, if I use SimpleBean even when defining the factory, it does not work.

In order for the server to instantiate the resource, the class file(s) need to be included in /system/server/tomcat/lib folder.

root@aurora:/system/server/tomcat/conf/Catalina/default# cat context.xml.default
<Context>
        <Resource name="entity-conf-application" type="example.SimpleBean" factory="org.apache.naming.factory.BeanFactory" description="Just a random thought" />
        <ResourceLink name="universe-conf-application" type="javax.sql.DataSource" global="universe-server-xml" /> <- ResourceLink element
</Context>

Visibility

Now that we have defined our resources at different configuration files, it is time to test. Go to our controller contextCheck in the browser.

Can see each of our resources

We can inspect one by one

This is interesting, what is this error about?

If you look at the definition for this resource

<Context>
        <Resource name="entity-conf-application" type="example.SimpleBean" factory="org.apache.naming.factory.BeanFactory" description="Just a random thought" />
</Context>

It has this description=”Just a random thought”, for fields that are not reserved inside <Resource /> (e.g. name, type, factory are reserved field names), it will try to call the setter method, i.e. setDescription(), but since SimpleBean does not have that setter, we get an exception. Let’s change the definition to beanValue instead.

<Context>
        <Resource name="entity-conf-application" type="example.SimpleBean" factory="org.apache.naming.factory.BeanFactory" beanValue="Just a random thought" />
</Context>
No error now

And you see these log entries in catalina.out, after the server finished starting up, which means they are lazily loaded on first access it seems.

18-Oct-2023 15:10:01.843 INFO [main] org.apache.catalina.startup.Catalina.start Server startup in [295] milliseconds
class org.apache.tomcat.util.threads.TaskThread constructed
class org.apache.tomcat.util.threads.TaskThread setDescription(Just a random thought)
<GlobalNamingResources>
    <Resource name="universe-server-xml" type="javax.sql.DataSource" />
</GlobalNamingResources>

As mentioned above, notice that you do not see this resource (universe-server-xml), since it is defined under GlobalNamingResources, but once you linked it using <ResourceLink /> it becomes visible (i.e. universe-conf-application)

Sharing

Our server.xml looks like this

<?xml version="1.0" encoding="UTF-8"?>

<Server port="8005" shutdown="TERMINATE">
  <Listener className="org.apache.catalina.startup.VersionLoggerListener" />
  <Listener className="org.apache.catalina.core.AprLifecycleListener" SSLEngine="on" />
  <Listener className="org.apache.catalina.core.JreMemoryLeakPreventionListener" />
  <Listener className="org.apache.catalina.mbeans.GlobalResourcesLifecycleListener" />
  <Listener className="org.apache.catalina.core.ThreadLocalLeakPreventionListener" />

  <GlobalNamingResources>
      <Resource name="universe-server-xml" type="javax.sql.DataSource" />
  </GlobalNamingResources>

  <Service name="Catalina">

    <Connector port="80" protocol="HTTP/1.1"
               maxThreads="200"
               connectionTimeout="20000"
               redirectPort="443"
               compression="on"
               compressionMinSize="2048"
               compressibleMimeType="text/html,text/xml,text/plain,text/css,application/json,application/javascript" />

    <Engine name="Catalina" defaultHost="default">
      <Host name="default" autoDeploy="false" appBase="/system/site/default">
        <Valve className="org.apache.catalina.valves.AccessLogValve" directory="logs/default" prefix="access_log" suffix=".txt" pattern="%h %l %u %t &quot;%r&quot; %s %b %{User-Agent}i" />
      </Host>
      <Host name="one.softwareperformance.expert" autoDeploy="false" appBase="/system/site/one">
        <Valve className="org.apache.catalina.valves.AccessLogValve" directory="logs/one" prefix="access_log" suffix=".txt" pattern="%h %l %u %t &quot;%r&quot; %s %b %{User-Agent}i" />
      </Host>
      <Host name="two.softwareperformance.expert" autoDeploy="false" appBase="/system/site/two">
        <Valve className="org.apache.catalina.valves.AccessLogValve" directory="logs/two" prefix="access_log" suffix=".txt" pattern="%h %l %u %t &quot;%r&quot; %s %b %{User-Agent}i" />
      </Host>
    </Engine>
  </Service>
</Server>

Let’s imagine we have this set of applications defined.

root@aurora:/system/site# tree /system/site
/system/site
├── default
│   ├── blue
│   │   ├── index.html
│   │   ├── META-INF
│   │   │   └── context.xml
│   │   └── WEB-INF
│   │       └── classes
│   │           └── example
│   │               ├── ContextCheckController.class
│   │               └── SimpleBean.class
│   ├── green
│   │   ├── index.html
│   │   ├── META-INF
│   │   │   └── context.xml
│   │   └── WEB-INF
│   │       └── classes
│   │           └── example
│   │               ├── ContextCheckController.class
│   │               └── SimpleBean.class
│   └── ROOT
│       ├── index.html
│       ├── META-INF
│       │   └── context.xml
│       └── WEB-INF
│           └── classes
│               └── example
│                   ├── ContextCheckController.class
│                   └── SimpleBean.class
├── one
│   ├── circle
│   │   ├── index.html
│   │   ├── META-INF
│   │   │   └── context.xml
│   │   └── WEB-INF
│   │       └── classes
│   │           └── example
│   │               ├── ContextCheckController.class
│   │               └── SimpleBean.class
│   ├── ROOT
│   │   ├── index.html
│   │   ├── META-INF
│   │   │   └── context.xml
│   │   └── WEB-INF
│   │       └── classes
│   │           └── example
│   │               ├── ContextCheckController.class
│   │               └── SimpleBean.class
│   └── square
│       ├── index.html
│       ├── META-INF
│       │   └── context.xml
│       └── WEB-INF
│           └── classes
│               └── example
│                   ├── ContextCheckController.class
│                   └── SimpleBean.class
└── two
    ├── a
    │   ├── index.html
    │   ├── META-INF
    │   │   └── context.xml
    │   └── WEB-INF
    │       └── classes
    │           └── example
    │               ├── ContextCheckController.class
    │               └── SimpleBean.class
    ├── b
    │   ├── index.html
    │   ├── META-INF
    │   │   └── context.xml
    │   └── WEB-INF
    │       └── classes
    │           └── example
    │               ├── ContextCheckController.class
    │               └── SimpleBean.class
    └── ROOT
        ├── index.html
        ├── META-INF
        │   └── context.xml
        └── WEB-INF
            └── classes
                └── example
                    ├── ContextCheckController.class
                    └── SimpleBean.class

/system/server/tomcat/conf/Catalina
├── default
│   └── context.xml.default
├── one.softwareperformance.expert
│   └── context.xml.default
└── two.softwareperformance.expert
    └── context.xml.default

In summary, we have 3 hosts, each host has 3 applications for a total of 9 applications.

  • one.softwareperformance.expert
  • two.softwareperformance.expert
  • a default host or catch all

In one.softwareperformance.expert we have 3 applications

  • http://one.softwareperformance.expert/ (the ROOT application)
  • http://one.softwareperformance.expert/circle
  • http://one.softwareperformance.expert/square

In two.softwareperformance.expert we have 3 applications

  • http://two.softwareperformance.expert/ (the ROOT application)
  • http://two.softwareperformance.expert/a
  • http://two.softwareperformance.expert/b

In the default host we also have 3 applications

  • http://192.168.1.127/ (the ROOT application)
  • http://192.168.1.127/blue
  • http://192.168.1.127/green

Remember as described above, we have 4 different levels of access

  • MetaInf – context defined in META-INF folder
  • ConfHost – context defined in conf folder in each host
  • ConfContext – context defined in conf folder context.xml
  • GlobalResource – context defined inside server.xml

Based on my observation it seems that only in GlobalResource we are sharing the same instance. Apart from that, each application has its own copy of the JNDI resource instance.

I’m on a Windows 10 host, accessing a Linux server (192.168.1.127) on my LAN. I have overridden my Windows file C:\Windows\System32\drivers\etc\hosts to contain these domain names. You will need to run notepad.exe in Administrator mode if you want to edit the Windows hosts file.

GlobalResource

GlobalResource has same instance accessed by 3 different applications in the default host
Same instance if accessed from host one.softwareperformance.expert
Same instance if accessed from host two.softwareperformance.expert

MetaInf

Each application has its own copy i.e. the instances are different

ConfHost

Each application has its own copy i.e. the instances are different
To make sure, I also created DataSource based resources (in case they behave differently), same results as SimpleBean i.e. different instance for each application

ConfContext

Each application has its own copy i.e. the instances are different
To make sure, I also created DataSource based resources (in case they behave differently), same results as SimpleBean i.e. different instance for each application

Leave a Reply

Your email address will not be published. Required fields are marked *