如何使用嵌入式Tomcat将JNDI数据库连接与Spring Boot和Spring Data结合使用?

问题描述:

当我尝试通过嵌入式Tomcat服务器将JNDI数据源与Spring Boot和Spring Data JPA结合使用时,在使用SpringApplication.run运行应用程序时收到以下错误消息:

When I try to use a JNDI datasource with Spring Boot and Spring Data JPA using an embedded Tomcat server, I get the following error message when running the application with SpringApplication.run:

org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'entityManagerFactory' defined in class path resource [org/springframework/boot/autoconfigure/orm/jpa/HibernateJpaAutoConfiguration.class]: Instantiation of bean failed; 
nested exception is org.springframework.beans.factory.BeanDefinitionStoreException: Factory method [public org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean org.springframework.boot.autoconfigure.orm.jpa.JpaBaseConfiguration.entityManagerFactory(org.springframework.boot.autoconfigure.orm.jpa.EntityManagerFactoryBuilder)] threw exception;
nested exception is org.springframework.jndi.JndiLookupFailureException: JndiObjectTargetSource failed to obtain new target object; 
nested exception is javax.naming.NameNotFoundException: Name [comp/env/jdbc/myDataSource] is not bound in this Context. Unable to find [comp].

我使用唯一的区别是对org.springframework.boot:spring-boot-starter-data-jpa附加了Maven依赖

The only difference is the additional Maven dependency to org.springframework.boot:spring-boot-starter-data-jpa

这是一个示例项目: https://github.com/derkoe/spring-boot-sample-tomcat-jndi (这是解决方案中示例的修改版本).只需签出,构建并运行SampleTomcatJndiApplication.

Here is a sample project: https://github.com/derkoe/spring-boot-sample-tomcat-jndi (this is a modified version of the sample in the solution). Just check out, build and run SampleTomcatJndiApplication.

看来,用于查找数据库连接的JNDI上下文还不是Web应用程序中的那个.在Spring上下文和Tomcat服务器的初始化中,这似乎是一个排序问题.

It seems that the JNDI context used in looking up the database connection is not yet the one from the webapp. This seems to be an ordering problem in the initialization of the Spring context and the Tomcat server.

有什么办法解决这个问题吗?

Any ideas how to solve that?

Tomcat使用线程的上下文类加载器确定要执行查找的JNDI上下文.如果线程上下文类加载器不是Web应用程序类加载器,则JNDI上下文为空,因此查找失败.

Tomcat uses the thread's context class loader to determine the JNDI context to perform the lookup against. If the thread context class loader isn't the web app classloader then the JNDI context is empty, hence the lookup failure.

问题在于,启动期间执行的DataSource的JNDI查找是在主线程上执行的,而主线程的TCCL不是Tomcat的Web应用程序类加载器.您可以通过更新您的TomcatEmbeddedServletContainerFactory bean来设置线程上下文类加载器来解决此问题.我还没有说服自己,这不是一个可怕的骇客,但它确实有效……

The problem is that the JNDI lookup of the DataSource that's performed during startup is being performed on the main thread and the main thread's TCCL isn't Tomcat's web app classloader. You can work around this by updating your TomcatEmbeddedServletContainerFactory bean to set the thread context class loader. I've yet to convince myself that this isn't a horrible hack, but it works…

这是更新的bean:

@Bean
public TomcatEmbeddedServletContainerFactory tomcatFactory() {
    return new TomcatEmbeddedServletContainerFactory() {

        @Override
        protected TomcatEmbeddedServletContainer getTomcatEmbeddedServletContainer(
                Tomcat tomcat) {
            tomcat.enableNaming();
            TomcatEmbeddedServletContainer container = 
                    super.getTomcatEmbeddedServletContainer(tomcat);
            for (Container child: container.getTomcat().getHost().findChildren()) {
                if (child instanceof Context) {
                    ClassLoader contextClassLoader = 
                            ((Context)child).getLoader().getClassLoader();
                    Thread.currentThread().setContextClassLoader(contextClassLoader);
                    break;
                }
            }
            return container;
        }

        @Override
        protected void postProcessContext(Context context) {
            ContextResource resource = new ContextResource();
            resource.setName("jdbc/myDataSource");
            resource.setType(DataSource.class.getName());
            resource.setProperty("driverClassName", "your.db.Driver");
            resource.setProperty("url", "jdbc:yourDb");

            context.getNamingResources().addResource(resource);
        }
    };
}

getEmbeddedServletContainer提取上下文的类加载器,并将其设置为当前线程的上下文类加载器.调用super方法后 会发生这种情况.这种顺序很重要,因为对super方法的调用会创建并启动容器,并作为该创建的一部分来创建上下文的类加载器.

getEmbeddedServletContainer extracts the context's classloader and set it as the current thread's context class loader. This happens` after the call to the super method. This ordering is important as the call to the super method creates and starts the container and, as a part of that creation, creates the context's class loader.