tl;dr
How do I tell Tomcat 9 to use a Postgres-specific object factory for producing DataSource
object in response to JNDI query?
Details
I can easily get a DataSource
object from Apache Tomcat 9 by defining an XML file named the same as my context. For example, for a web-app named clepsydra
, I create this file:
<?xml version="1.0" encoding="UTF-8"?>
<Context>
<!-- Domain: DEV, TEST, ACPT, ED, PROD -->
<Environment name = "work.basil.example.deployment-mode"
description = "Signals whether to run this web-app with development, testing, or production settings."
value = "DEV"
type = "java.lang.String"
override = "false"
/>
<Resource
name="jdbc/postgres"
auth="Container"
type="javax.sql.DataSource"
driverClassName="org.postgresql.Driver"
url="jdbc:postgresql://127.0.0.1:5432/mydb"
user="myuser"
password="mypasswd"
/>
</Context>
I place that file in my Tomcat “base” folder, in conf
folder, in folders I created with engine name Catalina
and host name localhost
. Tomcat feeds settings into a resource factory to return an instance of DataSource
. I can access that instance via JNDI:
Context ctxInitial = new InitialContext();
DataSource dataSource =
( DataSource ) ctxInitial.lookup( "java:comp/env/jdbc/postgres" )
;
I do realize that postgres
in that lookup string could be something more specific to a particular app. But let's go with postgres
for the same of demonstration.
I want org.postgresql.ds.PGSimpleDataSource
, not org.apache.tomcat.dbcp.dbcp2.BasicDataSource
This setup is using Tomcat’s own resource factory for JDBC DataSource objects. The underlying class of the returned DataSource
class is org.apache.tomcat.dbcp.dbcp2.BasicDataSource
. Unfortunately, I do not want a DataSource
of that class. I want a DataSource
of the class provided by the JDBC driver from The PostgreSQL Global Development Group: org.postgresql.ds.PGSimpleDataSource
.
By reading the Tomcat documentation pages, JNDI Resources How-To and JNDI Datasource How-To, I came to realize that Tomcat allows us to use an alternate factory for these DataSource
objects in place of the default factory implementation bundled with Tomcat. Sounds like what I need.
PGObjectFactory
I discovered that the Postgres JDBC driver already comes bundled with such implementations:
PGObjectFactory
For simple JDBC connections.PGXADataSourceFactory
For XA-enabledDataSource
implementation, for distributed transactions.
By the way, there is a similar factory built into the driver for OSGi apps, PGDataSourceFactory
. I assume that is of no use to me with Tomcat.
So, the PGObjectFactory
class implements the interface javax.naming.spi.ObjectFactory
required by JNDI.
SPI
I am guessing that the spi
in that package name means the object factories load via the Java Service Provider Interface (SPI).
So I presume that need a SPI mapping file, as discussed in the Oracle Tutorial and in the Vaadin documentation. added a META-INF
folder to my Vaadin resources
folder, and created a services
folder further nested there. So in /resources/META-INF/services
I created a file named javax.naming.spi.ObjectFactory
containing a single line of text, the name of my desired object factory: org.postgresql.ds.common.PGObjectFactory
. I even checked inside the Postgres JDBC driver to verify physically the existence and the fully-qualified name of this class.
Question
➥ My question is: How do I tell Tomcat to use PGObjectFactory
rather than its default object factory for producing my DataSource
objects for producing connections to my Postgres database?
factory
attribute on <Resource>
element
I had hoped it would be as simple as adding a factory
attribute (factory="org.postgresql.ds.common.PGObjectFactory"
) to my <Resource>
element seen above. I got this idea from the Tomcat page, The Context Container. That page is quite confusing as it focuses on global resource, but I do not need or want to define this DataSource
globally. I need this DataSource
only for my one web app.
Adding that factory
attribute:
<?xml version="1.0" encoding="UTF-8"?>
<Context>
<!-- Domain: DEV, TEST, ACPT, ED, PROD -->
<Environment name = "work.basil.example.deployment-mode"
description = "Signals whether to run this web-app with development, testing, or production settings."
value = "DEV"
type = "java.lang.String"
override = "false"
/>
<Resource
name="jdbc/postgres"
auth="Container"
type="javax.sql.DataSource"
driverClassName="org.postgresql.Driver"
url="jdbc:postgresql://127.0.0.1:5432/mydb"
user="myuser"
password="mypasswd"
factory="org.postgresql.ds.common.PGObjectFactory"
/>
</Context>
…fails with my DataSource
object being null.
ctxInitial = new InitialContext();
DataSource dataSource = ( DataSource ) ctxInitial.lookup( "java:comp/env/jdbc/postgres" );
System.out.println( "dataSource = " + dataSource );
null
Removing that factory="org.postgresql.ds.common.PGObjectFactory"
attribute resolves the exception. But then I am back to getting a Tomcat BasicDataSource
rather than a Postgres PGSimpleDataSource
. Thus my Question here.
I know my Context
XML is being loaded successfully because I can access that Environment
entry’s value.
2nd experiment
I tried this from the top, days later.
I created a new "Plain Java Servlet" flavor Vaadin 14.0.9 project named "datasource-object-factory".
Here is my entire Vaadin web app code. The bottom half is the JNDI lookup.
package work.basil.example;
import com.vaadin.flow.component.ClickEvent;
import com.vaadin.flow.component.button.Button;
import com.vaadin.flow.component.notification.Notification;
import com.vaadin.flow.component.orderedlayout.VerticalLayout;
import com.vaadin.flow.router.Route;
import com.vaadin.flow.server.PWA;
import javax.naming.Context;
import javax.naming.InitialContext;
import javax.naming.NamingException;
import javax.sql.DataSource;
/**
* The main view contains a button and a click listener.
*/
@Route ( "" )
@PWA ( name = "Project Base for Vaadin", shortName = "Project Base" )
public class MainView extends VerticalLayout
{
public MainView ( )
{
Button button = new Button( "Click me" ,
event -> Notification.show( "Clicked!" ) );
Button lookupButton = new Button( "BASIL - Lookup DataSource" );
lookupButton.addClickListener( ( ClickEvent < Button > buttonClickEvent ) -> {
Notification.show( "BASIL - Starting lookup." );
System.out.println( "BASIL - Starting lookup." );
this.lookupDataSource();
Notification.show( "BASIL - Completed lookup." );
System.out.println( "BASIL - Completed lookup." );
} );
this.add( button );
this.add( lookupButton );
}
private void lookupDataSource ( )
{
Context ctxInitial = null;
try
{
ctxInitial = new InitialContext();
// Environment entry.
String deploymentMode = ( String ) ctxInitial.lookup( "java:comp/env/work.basil.example.deployment-mode" );
Notification.show( "BASIL - deploymentMode: " + deploymentMode );
System.out.println( "BASIL - deploymentMode = " + deploymentMode );
// DataSource resource entry.
DataSource dataSource = ( DataSource ) ctxInitial.lookup( "java:comp/env/jdbc/postgres" );
Notification.show( "BASIL - dataSource: " + dataSource );
System.out.println( "BASIL - dataSource = " + dataSource );
}
catch ( NamingException e )
{
Notification.show( "BASIL - NamingException: " + e );
System.out.println( "BASIL - NamingException: " + e );
e.printStackTrace();
}
}
}
To keep things simple, I did not designate a Tomcat "base" folder, instead going with defaults. I did not run from IntelliJ, instead moving my web app’s WAR file manually to the webapps
folder.
I downloaded a new version of Tomcat, version 9.0.27. I dragged in the Postgres JDBC jar to the /lib
folder. I used the BatChmod app to set the permissions of the Tomcat folder.
To the conf
folder, I created the Catalina
& localhost
folders. In there I created a file named datasource-object-factory.xml
with the same contents as seen above.
<?xml version="1.0" encoding="UTF-8"?>
<Context>
<!-- Domain: DEV, TEST, ACPT, ED, PROD -->
<Environment name = "work.basil.example.deployment-mode"
description = "Signals whether to run this web-app with development, testing, or production settings."
value = "DEV"
type = "java.lang.String"
override = "false"
/>
<Resource
factory="org.postgresql.ds.common.PGObjectFactory"
name="jdbc/postgres"
auth="Container"
type="javax.sql.DataSource"
driverClassName="org.postgresql.Driver"
url="jdbc:postgresql://127.0.0.1:5432/mydb"
user="myuser"
password="mypasswd"
/>
</Context>
I copied my web app’s datasource-object-factory.war
file to webapps
in Tomcat. Lastly, I run Tomcat's /bin/startup.sh
and watch the WAR file explode into a folder.
With the factory="org.postgresql.ds.common.PGObjectFactory"
attribute on my Resource
element, the resulting DataSource
is null
.
As with my first experiment, I can access the value of the <Environment>
, so I know my context-name XML file is being found and processes successfully via JNDI.
Here are the logs on a Google Drive:
factory
attribute due to a vague recollection of having seen that used somewhere. The page you linked does not say anything about afactory
attribute on theResource
element. So I do not understand why you said “what you did should work”. What I did is not discussed in that documentation. – Basil BourqueDataSource
object beingnull
. I also tried adding an SPI mapping for the object factory, but no help there. I revised my Question with these details. I appreciate your attention and help. – Basil Bourque