如何使用 JNDI 数据库连接 Spring Boot 和 Spring Data 使用嵌入式 Tomcat?
Howto use JNDI database connection with Spring Boot and Spring Data using embedded Tomcat?
当我尝试通过 Spring Boot 和 Spring Data JPA 使用嵌入式 Tomcat 服务器使用 JNDI 数据源时,在使用 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].
我使用How to create JNDI context in Spring Boot with Embedded Tomcat Container
的解决方案中描述的配置
唯一的区别是对 org.springframework.boot:spring-boot-starter-data-jpa
的额外 Maven 依赖
这是一个示例项目:https://github.com/derkoe/spring-boot-sample-tomcat-jndi(这是解决方案中示例的修改版本)。只需签出、构建并运行 SampleTomcatJndiApplication。
查找数据库连接时使用的JNDI上下文似乎还不是来自webapp的。这似乎是 Spring 上下文和 Tomcat 服务器初始化中的排序问题。
有什么解决办法吗?
Tomcat 使用线程的上下文 class 加载程序来确定要针对其执行查找的 JNDI 上下文。如果线程上下文 class 加载器不是 Web 应用程序 class 加载器,则 JNDI 上下文为空,因此查找失败。
问题是启动期间执行的 DataSource
的 JNDI 查找是在主线程上执行的,而主线程的 TCCL 不是 Tomcat 的 Web 应用程序 class装载机。您可以通过更新 TomcatEmbeddedServletContainerFactory
bean 来设置线程上下文 class 加载程序来解决这个问题。我还没有说服自己这不是一个可怕的黑客攻击,但它确实有效……
这是更新后的 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
提取上下文的 class 加载器并将其设置为当前线程的上下文 class 加载器。这种情况发生在调用 super 方法后 。这种顺序很重要,因为对 super 方法的调用会创建并启动容器,并且作为该创建的一部分,还会创建上下文的 class 加载程序。
基于@Andy Wilkinson 的精彩回答,我更新了 SpringBoot 2.5.3 的解决方案:
public TomcatServletWebServerFactory tomcatFactory() {
return new TomcatServletWebServerFactory() {
@Override
protected TomcatWebServer getTomcatWebServer(org.apache.catalina.startup.Tomcat tomcat) {
tomcat.enableNaming();
TomcatWebServer tomcatWebServer = super.getTomcatWebServer(tomcat);
Container context = tomcatWebServer.getTomcat().getHost().findChild(getContextPath());
ClassUtils.overrideThreadContextClassLoader(((Context) context).getLoader().getClassLoader());
return tomcatWebServer;
}
@Override
protected void postProcessContext(Context context) {
...
}
}
当我尝试通过 Spring Boot 和 Spring Data JPA 使用嵌入式 Tomcat 服务器使用 JNDI 数据源时,在使用 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].
我使用How to create JNDI context in Spring Boot with Embedded Tomcat Container
的解决方案中描述的配置唯一的区别是对 org.springframework.boot:spring-boot-starter-data-jpa
的额外 Maven 依赖这是一个示例项目:https://github.com/derkoe/spring-boot-sample-tomcat-jndi(这是解决方案中示例的修改版本)。只需签出、构建并运行 SampleTomcatJndiApplication。
查找数据库连接时使用的JNDI上下文似乎还不是来自webapp的。这似乎是 Spring 上下文和 Tomcat 服务器初始化中的排序问题。
有什么解决办法吗?
Tomcat 使用线程的上下文 class 加载程序来确定要针对其执行查找的 JNDI 上下文。如果线程上下文 class 加载器不是 Web 应用程序 class 加载器,则 JNDI 上下文为空,因此查找失败。
问题是启动期间执行的 DataSource
的 JNDI 查找是在主线程上执行的,而主线程的 TCCL 不是 Tomcat 的 Web 应用程序 class装载机。您可以通过更新 TomcatEmbeddedServletContainerFactory
bean 来设置线程上下文 class 加载程序来解决这个问题。我还没有说服自己这不是一个可怕的黑客攻击,但它确实有效……
这是更新后的 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
提取上下文的 class 加载器并将其设置为当前线程的上下文 class 加载器。这种情况发生在调用 super 方法后 。这种顺序很重要,因为对 super 方法的调用会创建并启动容器,并且作为该创建的一部分,还会创建上下文的 class 加载程序。
基于@Andy Wilkinson 的精彩回答,我更新了 SpringBoot 2.5.3 的解决方案:
public TomcatServletWebServerFactory tomcatFactory() {
return new TomcatServletWebServerFactory() {
@Override
protected TomcatWebServer getTomcatWebServer(org.apache.catalina.startup.Tomcat tomcat) {
tomcat.enableNaming();
TomcatWebServer tomcatWebServer = super.getTomcatWebServer(tomcat);
Container context = tomcatWebServer.getTomcat().getHost().findChild(getContextPath());
ClassUtils.overrideThreadContextClassLoader(((Context) context).getLoader().getClassLoader());
return tomcatWebServer;
}
@Override
protected void postProcessContext(Context context) {
...
}
}