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].
I use the configuration described in the solution of How to create JNDI context in Spring Boot with Embedded Tomcat Container
The only difference is the additional Maven dependency to org.springframework.boot:spring-boot-starter-data-jpa
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.
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 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.
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…
Here's the updated 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
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.