问题
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:
- catalina.out
- catalina.2019-10-18.log
回答1:
Your resource configuration seems to need modification. As mentioned in Tomcat Documentation,
You can declare the characteristics of the resource to be returned for JNDI lookups of and elements in the web application deployment descriptor. You MUST also define the needed resource parameters as attributes of the Resource element, to configure the object factory to be used (if not known to Tomcat already), and the properties used to configure that object factory.
The reason you are getting null, is the object factory cannot determine the type of object it needs to create, refer PGObjectFactory code
public Object getObjectInstance ( Object obj , Name name , Context nameCtx ,
Hashtable < ?, ? > environment ) throws Exception
{
Reference ref = ( Reference ) obj;
String className = ref.getClassName();
// Old names are here for those who still use them
if (
className.equals( "org.postgresql.ds.PGSimpleDataSource" )
|| className.equals( "org.postgresql.jdbc2.optional.SimpleDataSource" )
|| className.equals( "org.postgresql.jdbc3.Jdbc3SimpleDataSource" )
)
{
return loadSimpleDataSource( ref );
} else if (
className.equals( "org.postgresql.ds.PGConnectionPoolDataSource" )
|| className.equals( "org.postgresql.jdbc2.optional.ConnectionPool" )
|| className.equals( "org.postgresql.jdbc3.Jdbc3ConnectionPool" )
)
{
return loadConnectionPool( ref );
} else if (
className.equals( "org.postgresql.ds.PGPoolingDataSource" )
|| className.equals( "org.postgresql.jdbc2.optional.PoolingDataSource" )
|| className.equals( "org.postgresql.jdbc3.Jdbc3PoolingDataSource" )
)
{
return loadPoolingDataSource( ref );
} else
{
return null;
}
}
The value 'javax.sql.DataSource' in the resource definition does not correspond to any of the class the object factory understands, use one of the classes the object factory understands, in your case 'org.postgresql.ds.PGSimpleDataSource'.
However, this will still not get you a valid data source, reason being, refer, in the same source code, the following sections:
private Object loadSimpleDataSource(Reference ref) {
PGSimpleDataSource ds = new PGSimpleDataSource();
return loadBaseDataSource(ds, ref);
}
and
protected Object loadBaseDataSource(BaseDataSource ds, Reference ref) {
ds.setFromReference(ref);
return ds;
}
The loadBaseDataSource
calls setFromReference
on the super class of all data sources, refer: BaseDataSource, section:
public void setFromReference ( Reference ref )
{
databaseName = getReferenceProperty( ref , "databaseName" );
String portNumberString = getReferenceProperty( ref , "portNumber" );
if ( portNumberString != null )
{
String[] ps = portNumberString.split( "," );
int[] ports = new int[ ps.length ];
for ( int i = 0 ; i < ps.length ; i++ )
{
try
{
ports[ i ] = Integer.parseInt( ps[ i ] );
}
catch ( NumberFormatException e )
{
ports[ i ] = 0;
}
}
setPortNumbers( ports );
} else
{
setPortNumbers( null );
}
setServerNames( getReferenceProperty( ref , "serverName" ).split( "," ) );
for ( PGProperty property : PGProperty.values() )
{
setProperty( property , getReferenceProperty( ref , property.getName() ) );
}
}
The above requires three properties viz. 'databaseName', 'portNumber' and 'serverName', so these properties also need to be on the resource definition.
Sum total, your resource declaration probably should look as follows:
<Resource
factory="org.postgresql.ds.common.PGObjectFactory"
name="jdbc/postgres"
auth="Application"
type="org.postgresql.ds.PGSimpleDataSource"
serverName="127.0.0.1"
portNumber="5432"
databaseName="mydb"
/>
You should then resolve the data source as you have already done and get the connection with getConnection(userName, pwd).
NOTE: You could also set 'userName' and 'password' property, defined in BaseDataSource
.
Putting that all together, we can revise your original example to look like the following. We use some of the DataSource configuration properties defined by the Postgres JDBC driver.
<?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"
/>
<!-- `DataSource` object for obtaining database connections to Postgres -->
<Resource
factory="org.postgresql.ds.common.PGObjectFactory"
type="org.postgresql.ds.PGSimpleDataSource"
auth="Container"
driverClassName="org.postgresql.Driver"
name="jdbc/postgres"
serverName="127.0.0.1"
portNumber="5432"
databaseName="myDb"
user="myuser"
password="mypasswd"
ssl="false"
/>
</Context>
来源:https://stackoverflow.com/questions/58385528/specify-a-datasource-factory-instead-of-tomcats-default