指定 `DataSource` 工厂而不是 Tomcat 的默认工厂

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

tl;博士

如何告诉 Tomcat 9 使用 Postgres-specific object factory for producing DataSource object in response to JNDI 查询?

详情

我可以通过定义一个与我的上下文同名的 XML 文件轻松获得 DataSource object from Apache Tomcat 9。例如,对于名为 clepsydra 的网络应用程序,我创建了这个文件:

<?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>

我将该文件放在我的 Tomcat“基本”文件夹中,在 conf 文件夹中,在我用引擎名称 Catalina 和主机名称 localhost 创建的文件夹中。 Tomcat 将设置输入资源工厂以 return DataSource 的一个实例。我可以通过 JNDI 访问该实例:

Context ctxInitial = new InitialContext();
DataSource dataSource = 
        ( DataSource ) ctxInitial.lookup( "java:comp/env/jdbc/postgres" )
;

我确实意识到该查找字符串中的 postgres 可能更特定于特定应用程序。但是让我们使用 postgres 进行相同的演示。

我要org.postgresql.ds.PGSimpleDataSource,不要org.apache.tomcat.dbcp.dbcp2.BasicDataSource

此设置正在使用 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

通过阅读 Tomcat 文档页面,JNDI Resources How-To and JNDI Datasource How-To,我开始意识到 Tomcat 允许我们为这些 DataSource 对象使用备用工厂来代替与 Tomcat 捆绑在一起的默认工厂实现。听起来像我需要的。

PGObjectFactory

我发现 Postgres JDBC 驱动程序已经捆绑了这样的实现:

顺便说一句,OSGi 应用程序的驱动程序中内置了一个类似的工厂,PGDataSourceFactory。我认为 Tomcat.

对我没有用

因此,PGObjectFactory class 实现了 JNDI 所需的接口 javax.naming.spi.ObjectFactory

SPI

我猜测该包名称中的 spi 表示对象工厂通过 Java Service Provider Interface (SPI) 加载。

所以我推测需要一个 SPI 映射文件,如 Oracle Tutorial and in the Vaadin documentation 中所讨论的那样。在我的 Vaadin resources 文件夹中添加了一个 META-INF 文件夹,并进一步嵌套在那里创建了一个 services 文件夹。因此,在 /resources/META-INF/services 中,我创建了一个名为 javax.naming.spi.ObjectFactory 的文件,其中包含一行文本,即我想要的对象工厂的名称:org.postgresql.ds.common.PGObjectFactory。我什至检查了 Postgres JDBC 驱动程序以物理验证此 class.

的存在和完全限定名称

问题

➥ 我的问题是:如何 告诉 Tomcat 使用 PGObjectFactory 而不是其默认对象工厂 来生成我的 DataSource用于生成与我的 Postgres 数据库的连接的对象?

factory <Resource> 元素上的属性

我曾希望它会像在上面看到的 <Resource> 元素中添加 factory 属性 (factory="org.postgresql.ds.common.PGObjectFactory") 一样简单。我从 Tomcat 页面 The Context Container 得到了这个想法。该页面非常混乱,因为它专注于全局资源,但我不需要或不想在全局范围内定义此 DataSource。我只需要这个 DataSource 用于我的一个网络应用程序。

添加 factory 属性:

<?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>

…失败,我的 DataSource 对象为空。

ctxInitial = new InitialContext();
DataSource dataSource = ( DataSource ) ctxInitial.lookup( "java:comp/env/jdbc/postgres" );
System.out.println( "dataSource = " + dataSource );

null

删除 factory="org.postgresql.ds.common.PGObjectFactory" 属性可解决异常。但后来我又回到了 Tomcat BasicDataSource 而不是 Postgres PGSimpleDataSource。因此我的问题在这里。

我知道我的 Context XML 正在成功加载,因为我可以访问 Environment 条目的值。

第二次实验

几天后,我从顶部尝试了这个。

我创建了一个名为 "datasource-object-factory" 的新 "Plain Java Servlet" 风味 Vaadin 14.0.9 项目。

这是我的整个 Vaadin 网络应用程序代码。下半部分是 JNDI 查找。

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();
        }
    }
}

为简单起见,我没有指定 Tomcat "base" 文件夹,而是使用默认值。我没有从 IntelliJ 运行,而是手动将我的网络应用程序的 WAR 文件移动到 webapps 文件夹。

我下载了 Tomcat 的新版本,版本 9.0.27。我将 Postgres JDBC jar 拖到 /lib 文件夹中。我使用 BatChmod 应用程序设置 Tomcat 文件夹的权限。

我在 conf 文件夹中创建了 Catalinalocalhost 文件夹。在那里我创建了一个名为 datasource-object-factory.xml 的文件,其内容与上面相同。

<?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>

我将网络应用程序的 datasource-object-factory.war 文件复制到 Tomcat 中的 webapps。最后,我 运行 Tomcat 的 /bin/startup.sh 看着 WAR 文件分解成一个文件夹。

我的 Resource 元素上的 factory="org.postgresql.ds.common.PGObjectFactory" 属性,生成的 DataSourcenull

与我的第一个实验一样,我可以访问 <Environment> 的值,所以我知道我的上下文名称 XML 文件正在被找到并通过 JNDI 成功处理。

这是 Google 驱动器上的日志:

您的资源配置似乎需要修改。如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.

你得到 null 的原因是对象工厂无法确定它需要创建的对象的类型,参考 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;
    }
}

资源定义中的值 'javax.sql.DataSource' 不对应于对象工厂理解的任何 class,使用对象工厂理解的 classes 之一,在你的案例 'org.postgresql.ds.PGSimpleDataSource'.

但是,这仍然无法为您提供有效的数据源,原因是,在相同的源代码中,请参考以下部分:

private Object loadSimpleDataSource(Reference ref) {
    PGSimpleDataSource ds = new PGSimpleDataSource();
    return loadBaseDataSource(ds, ref);
}

protected Object loadBaseDataSource(BaseDataSource ds, Reference ref) {
    ds.setFromReference(ref);

    return ds;
}

loadBaseDataSource在所有数据源的超class上调用setFromReference,参考:BaseDataSource,部分:

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() ) );
    }
}

以上需要三个属性,即。 'databaseName'、'portNumber' 和 'serverName',因此这些属性也需要在资源定义中。

总而言之,您的资源声明可能应如下所示:

<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"
            />

然后您应该像您已经完成的那样解析数据源并使用 getConnection(userName, pwd) 获取连接。

注意:您还可以设置 'userName' 和 'password' 属性,在 BaseDataSource.

中定义

综上所述,我们可以将您的原始示例修改为如下所示。我们使用了一些 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>