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.
We can inspect one by one
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>
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 "%r" %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 "%r" %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 "%r" %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
MetaInf
ConfHost
ConfContext