Specify a `DataSource` factory instead of Tomcat's default

元气小坏坏 提交于 2019-12-08 12:13:44

问题


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-enabled DataSource 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

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!