SpringBoot JNDI 数据源抛出 java.lang.ClassNotFoundException: org.apache.tomcat.dbcp.dbcp2.BasicDataSource

Posted

技术标签:

【中文标题】SpringBoot JNDI 数据源抛出 java.lang.ClassNotFoundException: org.apache.tomcat.dbcp.dbcp2.BasicDataSourceFactory【英文标题】:SpringBoot JNDI datasource throws java.lang.ClassNotFoundException: org.apache.tomcat.dbcp.dbcp2.BasicDataSourceFactory 【发布时间】:2017-01-10 03:07:18 【问题描述】:

之前有人问过类似的问题,我已经解决了所有这些问题,但无法解决问题。相关问题 - Q1,Q2,Q3,Q4,Q5,Q6

我有一个带有 Spring Boot 的 Spring Batch 项目并尝试使用数据库连接池。我正在使用版本 8.5.x 的嵌入式 tomcat 容器。

如果我使用 application.properties 指定数据源和池设置,一切正常。

但是当我尝试使用 JNDI 时,我得到了异常 -

Caused by: java.lang.ClassNotFoundException: org.apache.tomcat.dbcp.dbcp2.BasicDataSourceFactory

我在 Maven jar 中没有看到任何 jar 名称 tomcat-dbcp-**,所以我不确定是否需要包含任何新的依赖项或需要设置默认数据源工厂以及如何进行。

下面是我设置的 JNDI bean,Question。我已经取消了某些值。

@Bean
    public TomcatEmbeddedServletContainerFactory embeddedServletContainerFactory()
        return new TomcatEmbeddedServletContainerFactory() 

            @Override
            protected TomcatEmbeddedServletContainer getTomcatEmbeddedServletContainer(
                    Tomcat tomcat) 
                tomcat.enableNaming();
                return super.getTomcatEmbeddedServletContainer(tomcat);
            

            @Override
            protected void postProcessContext(Context context) 
                ContextResource resource = new ContextResource();
                resource.setName("jdbc/myDataSource");
                resource.setType(DataSource.class.getName());
                resource.setProperty("driverClassName", "com.ibm.db2.jcc.DB2Driver");
                resource.setProperty("url", "url");
                resource.setProperty("username", "user");
                resource.setProperty("password", "*****");
                context.getNamingResources().addResource(resource);
            
        ;
    

    @Lazy
    @Bean(destroyMethod="")
    public DataSource jndiDataSource() throws IllegalArgumentException, NamingException 
        JndiObjectFactoryBean bean = new JndiObjectFactoryBean();
        bean.setJndiName("java:comp/env/jdbc/myDataSource");
        bean.setProxyInterface(DataSource.class);
        bean.setLookupOnStartup(false);
        bean.afterPropertiesSet();
        return (DataSource)bean.getObject();
    

我的 pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <packaging>war</packaging>

    <groupId>***</groupId>
    <artifactId>***</artifactId>
    <version>1.0.0</version>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>1.4.0.RELEASE</version>
    </parent>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-batch</artifactId>
            <exclusions>
                <exclusion>
                    <groupId>org.springframework.boot</groupId>
                    <artifactId>spring-boot-starter-logging</artifactId>
                </exclusion>
            </exclusions>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-log4j</artifactId>
            <version>1.2.1.RELEASE</version>
        </dependency>   

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-tomcat</artifactId>
            <scope>provided</scope>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-jdbc</artifactId>
        </dependency>

        <dependency>
            <groupId>db2</groupId>
            <artifactId>db2jcc</artifactId>
            <version>4.0</version>
        </dependency>

        <dependency>
            <groupId>db2</groupId>
            <artifactId>db2jcc_license_cu</artifactId>
            <version>4.0</version>
        </dependency>
    </dependencies>

    <build>

        <plugins>

            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <version>1.4.0.RELEASE</version>
                <executions>
                    <execution>
                        <goals>
                            <goal>repackage</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.1</version>
                <configuration>
                <source>1.7</source>
                <target>1.7</target>
                </configuration>
            </plugin>
        </plugins>
    </build>

</project>

【问题讨论】:

我不确定原因,是上级要求我这样做的。可能他们不希望属性文件中包含用户名和密码。我觉得奇怪的是spring boot不包括tomcat-dbcp依赖项作为starter POM的一部分,而是使用DBCP DataSource工厂作为默认工厂。 如果我们也必须保持开放以部署到非嵌入式环境,是否有更好的方法来保护数据库连接信息?我们通过类文件面临的一个挑战是为各种暂存环境编辑数据库 URL 和用户,但使用 war 文件分发数据库信息似乎不是一个好主意。 您不必将其包含在文件中。您可以在启动应用程序时将其作为参数传递或作为环境变量传递。此外,如果您指定数据源 jndi 名称,它将被使用而不是本地配置的实例。我建议阅读 Spring Boot 如何加载/解析属性文件以及如何为环境指定属性,而不是侵入它。简而言之,您应该使用框架而不是围绕框架。 【参考方案1】:

到前面的帖子。我认为 hikari 的配置可能是这样的:

resource.setProperty("type", "com.zaxxer.hikari.HikariDataSource");
resource.setProperty("factory", "com.zaxxer.hikari.HikariJNDIFactory");

它让我更进一步。然后我面临这个异常导致另一个依赖问题(缺少 oracle jdbc 驱动程序):

Failed to load driver class oracle.jdbc.OracleDriver in either of HikariConfig class loader or Thread context classloader
    at com.zaxxer.hikari.HikariConfig.setDriverClassName(HikariConfig.java:486) ~[HikariCP-3.4.5.jar:na]

添加或删除>>>

dependencies 
    implementation 'org.springframework.boot:spring-boot-starter-web'
    implementation 'commons-codec:commons-codec'
    implementation 'org.springframework.boot:spring-boot-starter-data-jpa'

    providedRuntime 'org.springframework.boot:spring-boot-starter-tomcat'
>>>    providedRuntime 'com.zaxxer:HikariCP'
>>>    providedRuntime 'com.oracle.database.jdbc:ojdbc8'

    testImplementation 'org.springframework.boot:spring-boot-starter-test'
    testImplementation 'org.apache.httpcomponents:fluent-hc'

对我的 gradle buildfile 没有任何影响。我也尝试了 implementation 和 runtimeOnly 范围,但它似乎不影响问题。

如果有人对如何将 oracle jdbc 驱动程序(或基本上任何 jar 文件,最好使用 gradle)传播到 spring boot 嵌入式 tomcat 有任何想法,请分享您的智慧。

似乎与此有关:https://developpaper.com/maven-magic-hall-the-pits-on-which-oracle-jdbc-driver-depends/ 也许使用这个 maven 解决方法它可能会起作用,但我不想仅仅为此切换回 maven。

【讨论】:

【参考方案2】:

你有多种选择:

使用默认的 DBCP 2 数据源(您不想使用已过时且效率较低的 DBCP 1)。 使用 Tomcat JDBC 数据源。 使用任何其他数据源:例如 HikariCP。

1) 要使用 Apache JDBC 数据源,您不需要添加任何依赖项,因为它已在 Tomcat Spring Boot 启动器中提供,但您必须将默认工厂类更改为 org.apache.tomcat.jdbc.pool.DataSourceFactory 才能使用它。 您可以在资源声明中执行此操作: resource.setProperty("factory", "org.apache.tomcat.jdbc.pool.DataSourceFactory"); 我将在下面解释添加此行的位置。

2) 要使用 DBCP 2 数据源(默认情况下实际上是预期的),需要依赖项:

<dependency>
  <groupId>org.apache.tomcat</groupId>
  <artifactId>tomcat-dbcp</artifactId>
  <version>8.5.4</version>
</dependency>

当然,根据你的 Spring Boot Tomcat 嵌入式版本适配神器版本。

3) 要使用任何其他数据源,我将使用 HikariCP 进行说明,如果您的配置中不存在所需的依赖项,则添加所需的依赖项(如果您依赖 Spring Boot 的持久性启动器,则可能是 HikariCP),例如:

<dependency>
    <groupId>com.zaxxer</groupId>
    <artifactId>HikariCP</artifactId>
    <version>3.1.0</version>
</dependency>

并在资源声明中指定工厂:

resource.setProperty("factory", "com.zaxxer.hikari.HikariDataSource");

例如对于 PostgreSQL 和 DBCP 2 数据源,不要指定任何工厂,因为它是默认值:

    @Override 
    protected void postProcessContext(Context context)             
        ContextResource resource = new ContextResource();
        //...
        context.getNamingResources()
               .addResource(resource);          
    

这里是 Tomcat JDBC 和 HikariCP 数据源的变体。

postProcessContext() 中设置工厂属性,如前所述为 Tomcat JDBC ds :

    @Override 
    protected void postProcessContext(Context context) 
        ContextResource resource = new ContextResource();       
        //...
        resource.setProperty("factory", "org.apache.tomcat.jdbc.pool.DataSourceFactory");
        //...
        context.getNamingResources()
               .addResource(resource);          
    
;

对于 HikariCP:

    @Override 
    protected void postProcessContext(Context context) 
        ContextResource resource = new ContextResource();       
        //...
        resource.setProperty("factory", "com.zaxxer.hikari.HikariDataSource");
        //...
        context.getNamingResources()
               .addResource(resource);          
    
;

【讨论】:

【参考方案3】:

我通过在我的Resource 定义中设置factory 属性解决了这个问题。 resource.setProperty("factory", "org.apache.tomcat.jdbc.pool.DataSourceFactory");

@Bean
public TomcatEmbeddedServletContainerFactory embeddedServletContainerFactory()
    return new TomcatEmbeddedServletContainerFactory() 

        @Override
        protected TomcatEmbeddedServletContainer getTomcatEmbeddedServletContainer(
                Tomcat tomcat) 
            tomcat.enableNaming();
            return super.getTomcatEmbeddedServletContainer(tomcat);
        

        @Override
        protected void postProcessContext(Context context) 
            ContextResource resource = new ContextResource();
            resource.setName("jdbc/myDataSource");
            resource.setType(DataSource.class.getName());
            resource.setProperty("factory", "org.apache.tomcat.jdbc.pool.DataSourceFactory");
            resource.setProperty("driverClassName", "com.ibm.db2.jcc.DB2Driver");
            resource.setProperty("url", "url");
            resource.setProperty("username", "user");
            resource.setProperty("password", "*****");
            context.getNamingResources().addResource(resource);
        
    ;

根据 tomcat 8 文档,它应该通过查看 DataSource 类型来自动推断数据库池工厂类型,并且不知何故它默认为 DBCP 工厂,并且该类不在我的类路径中。

我想可以通过提供tomcat-dbcp-** jar 来解决这个问题,但我不确定如何使用 spring boot 或者即使 spring boot 可以做到这一点。

我觉得奇怪的是 Spring Boot 不包括 tomcat-dbcp 依赖项作为 starter POM 的一部分,而是使用 DBCP DataSource 工厂作为默认工厂。

【讨论】:

【参考方案4】:

“Starter POM”不再包含 jndi reltead 依赖项,如果您 正在使用 Tomcat/Jetty/etc... 与 JNDI 您现在需要自己直接添加此依赖项。

然后在 application.properties 文件中配置 JNDI spring.datasource.jndi-name=java:comp/env/jdbc/yourname

对于您的例外情况,您需要将 tomcat-dbcp 添加到您的 pom.xml 文件中。

但是你可以检查你的项目依赖,如果你使用 spring-boot-starter-jdbc 或 spring-boot-starter-data-jpa ‘starters’ 将自动获得对“tomcat-jdbc”的依赖。

【讨论】:

那些 jndi reltead 依赖项 是什么?另外,我想,我在 bean 定义中设置了相同的属性。

以上是关于SpringBoot JNDI 数据源抛出 java.lang.ClassNotFoundException: org.apache.tomcat.dbcp.dbcp2.BasicDataSource的主要内容,如果未能解决你的问题,请参考以下文章

Bitronix 配置与 tomcat 抛出 jndi 异常

Spring Boot - 外部 Tomcat - JNDI 数据源

在 Spring Boot 应用程序中查找 JNDI 数据源失败

使用 JNDI 配置数据源 使用外部 Tomcat 9 服务器:Spring Boot

Tomcat 7.0 for Hibernate 中的数据源 JNDI 配置

JNDI 与 spring mvc3 集成