如何使用嵌入式 Tomcat 容器在 Spring Boot 中创建 JNDI 上下文
Posted
技术标签:
【中文标题】如何使用嵌入式 Tomcat 容器在 Spring Boot 中创建 JNDI 上下文【英文标题】:How to create JNDI context in Spring Boot with Embedded Tomcat Container 【发布时间】:2014-09-16 11:42:16 【问题描述】:import org.apache.catalina.Context;
import org.apache.catalina.deploy.ContextResource;
import org.apache.catalina.startup.Tomcat;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.boot.context.embedded.ConfigurableEmbeddedServletContainer;
import org.springframework.boot.context.embedded.EmbeddedServletContainerCustomizer;
import org.springframework.boot.context.embedded.tomcat.TomcatContextCustomizer;
import org.springframework.boot.context.embedded.tomcat.TomcatEmbeddedServletContainer;
import org.springframework.boot.context.embedded.tomcat.TomcatEmbeddedServletContainerFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.ImportResource;
@Configuration
@EnableAutoConfiguration
@ComponentScan
@ImportResource("classpath:applicationContext.xml")
public class Application
public static void main(String[] args) throws Exception
new SpringApplicationBuilder()
.showBanner(false)
.sources(Application.class)
.run(args);
@Bean
public TomcatEmbeddedServletContainerFactory tomcatFactory()
return new TomcatEmbeddedServletContainerFactory()
@Override
protected TomcatEmbeddedServletContainer getTomcatEmbeddedServletContainer(
Tomcat tomcat)
tomcat.enableNaming();
return super.getTomcatEmbeddedServletContainer(tomcat);
;
@Bean
public EmbeddedServletContainerCustomizer embeddedServletContainerCustomizer()
return new EmbeddedServletContainerCustomizer()
@Override
public void customize(ConfigurableEmbeddedServletContainer container)
if (container instanceof TomcatEmbeddedServletContainerFactory)
TomcatEmbeddedServletContainerFactory tomcatEmbeddedServletContainerFactory = (TomcatEmbeddedServletContainerFactory) container;
tomcatEmbeddedServletContainerFactory.addContextCustomizers(new TomcatContextCustomizer()
@Override
public void customize(Context context)
ContextResource mydatasource = new ContextResource();
mydatasource.setName("jdbc/mydatasource");
mydatasource.setAuth("Container");
mydatasource.setType("javax.sql.DataSource");
mydatasource.setScope("Sharable");
mydatasource.setProperty("driverClassName", "oracle.jdbc.driver.OracleDriver");
mydatasource.setProperty("url", "jdbc:oracle:thin:@mydomain.com:1522:myid");
mydatasource.setProperty("username", "myusername");
mydatasource.setProperty("password", "mypassword");
context.getNamingResources().addResource(mydatasource);
);
;
我正在使用 spring boot 并尝试使用为我的数据源创建 JNDI 上下文的嵌入式 tomcat 启动:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
<version>1.1.4.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-tomcat</artifactId>
<version>1.1.4.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-oracle</artifactId>
<version>1.0.0.RELEASE</version>
</dependency>
如果我删除 @ImportResource 我的应用程序启动就好了。我可以连接到 tomcat 实例。我可以检查我所有的执行器端点。使用 JConsole,我可以连接到应用程序,我可以在 MBean 中看到我的数据源(Catalina -> Resource -> Context -> "/" -> localhost -> javax.sql.DataSource -> jdbc/mydatasource)
我还通过 JConsole 在此处显示 MBean(Tomcat -> DataSource -> / -> localhost -> javax.sql.DataSource -> jdbc/mydatasource)
但是,当我 @ImportResource 实际通过 JNDI 查找 mydatasource 时,它没有找到它。
<bean id="myDS" class="org.springframework.jndi.JndiObjectFactoryBean">
<property name="jndiName" value="java:comp/env/jdbc/mydatasource"/>
</bean>
我导入的xml文件的相关部分
我在上面配置的 ContextResource 与我在将应用程序部署到 tomcat 容器时部署的 context.xml 中使用的参数完全相同。当部署到 tomcat 容器时,我导入的 bean 和我的应用程序可以正常工作。
所以看起来我现在有一个上下文,但似乎命名不正确。我尝试了资源名称的各种组合,但似乎无法在此上下文中生成“comp”绑定。
Caused by: javax.naming.NameNotFoundException: Name [comp/env/jdbc/mydatasource] is not bound in this Context. Unable to find [comp].
at org.apache.naming.NamingContext.lookup(NamingContext.java:819)
at org.apache.naming.NamingContext.lookup(NamingContext.java:167)
at org.apache.naming.SelectorContext.lookup(SelectorContext.java:156)
at javax.naming.InitialContext.lookup(InitialContext.java:392)
at org.springframework.jndi.JndiTemplate$1.doInContext(JndiTemplate.java:155)
at org.springframework.jndi.JndiTemplate.execute(JndiTemplate.java:87)
at org.springframework.jndi.JndiTemplate.lookup(JndiTemplate.java:152)
at org.springframework.jndi.JndiTemplate.lookup(JndiTemplate.java:179)
at org.springframework.jndi.JndiLocatorSupport.lookup(JndiLocatorSupport.java:95)
at org.springframework.jndi.JndiObjectLocator.lookup(JndiObjectLocator.java:106)
at org.springframework.jndi.JndiObjectFactoryBean.lookupWithFallback(JndiObjectFactoryBean.java:231)
at org.springframework.jndi.JndiObjectFactoryBean.afterPropertiesSet(JndiObjectFactoryBean.java:217)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.invokeInitMethods(AbstractAutowireCapableBeanFactory.java:1612)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.initializeBean(AbstractAutowireCapableBeanFactory.java:1549)
... 30 more
【问题讨论】:
【参考方案1】:默认情况下,嵌入式 Tomcat 中禁用 JNDI,这会导致 NoInitialContextException
。您需要调用Tomcat.enableNaming()
来启用它。最简单的方法是使用 TomcatEmbeddedServletContainer
子类:
@Bean
public TomcatEmbeddedServletContainerFactory tomcatFactory()
return new TomcatEmbeddedServletContainerFactory()
@Override
protected TomcatEmbeddedServletContainer getTomcatEmbeddedServletContainer(
Tomcat tomcat)
tomcat.enableNaming();
return super.getTomcatEmbeddedServletContainer(tomcat);
;
如果您采用这种方法,您还可以通过覆盖 TomcatEmbeddedServletContainerFactory
子类中的 postProcessContext
方法在 JNDI 中注册 DataSource
。
context.getNamingResources().addResource
将资源添加到 java:comp/env
上下文中,因此资源的名称应该是 jdbc/mydatasource
而不是 java:comp/env/mydatasource
。
Tomcat 使用线程上下文类加载器来确定应针对哪个 JNDI 上下文执行查找。您将资源绑定到 Web 应用程序的 JNDI 上下文中,因此您需要确保在 Web 应用程序的类加载器是线程上下文类加载器时执行查找。您应该能够通过在jndiObjectFactoryBean
上将lookupOnStartup
设置为false
来实现此目的。您还需要将expectedType
设置为javax.sql.DataSource
:
<bean class="org.springframework.jndi.JndiObjectFactoryBean">
<property name="jndiName" value="java:comp/env/jdbc/mydatasource"/>
<property name="expectedType" value="javax.sql.DataSource"/>
<property name="lookupOnStartup" value="false"/>
</bean>
这将为 DataSource 创建一个代理,在第一次使用时执行实际的 JNDI 查找,而不是在应用程序上下文启动期间执行。
this Spring Boot sample 中说明了上述方法。
【讨论】:
我认为你的提示让我更接近了。即使资源显示在 JConsole 中,我仍然无法找到它们。 我已经用更多细节更新了我的答案。请注意,您使用错误的名称将资源绑定到 JNDI。我还展示了如何调用 tomcat.enableNaming() 而不是复制代码。 我很难实现这一点,至少在使用 Graisl 3.0 时是这样。有更新的方法来执行此操作吗?你们勾勒的方式似乎不再有效 不是在postProcessContext方法中定义DataSource,而是可以使用Spring根据application.properties中的spring.datasource.*属性生成的那个吗? 这对我不起作用,我得到javax.naming.NoInitialContextException: Need to specify class name in environment or system property, or as an applet parameter, or in an application resource file: java.naming.factory.initial
我将我的 jndi 资源定义为其他文件中的 bean。【参考方案2】:
我最近需要在 Spring Boot 中使用带有嵌入式 Tomcat 的 JNDI。 实际答案提供了一些有趣的提示来解决我的任务,但这还不够,因为 Spring Boot 2 可能没有更新。
这是我使用 Spring Boot 2.0.3.RELEASE 测试的贡献。
在运行时指定类路径中可用的数据源
您有多种选择:
使用 DBCP 2 数据源(您不想使用已过时且效率较低的 DBCP 1)。 使用 Tomcat JDBC 数据源。 使用任何其他数据源:例如 HikariCP。如果您不指定其中任何一个,使用默认配置,数据源的实例化将引发异常:
原因:javax.naming.NamingException:无法创建资源工厂实例 在 org.apache.naming.factory.ResourceFactory.getDefaultFactory(ResourceFactory.java:50) 在 org.apache.naming.factory.FactoryBase.getObjectInstance(FactoryBase.java:90) 在 javax.naming.spi.NamingManager.getObjectInstance(NamingManager.java:321) 在 org.apache.naming.NamingContext.lookup(NamingContext.java:839) 在 org.apache.naming.NamingContext.lookup(NamingContext.java:159) 在 org.apache.naming.NamingContext.lookup(NamingContext.java:827) 在 org.apache.naming.NamingContext.lookup(NamingContext.java:159) 在 org.apache.naming.NamingContext.lookup(NamingContext.java:827) 在 org.apache.naming.NamingContext.lookup(NamingContext.java:159) 在 org.apache.naming.NamingContext.lookup(NamingContext.java:827) 在 org.apache.naming.NamingContext.lookup(NamingContext.java:173) 在 org.apache.naming.SelectorContext.lookup(SelectorContext.java:163) 在 javax.naming.InitialContext.lookup(InitialContext.java:417) 在 org.springframework.jndi.JndiTemplate.lambda$lookup$0(JndiTemplate.java:156) 在 org.springframework.jndi.JndiTemplate.execute(JndiTemplate.java:91) 在 org.springframework.jndi.JndiTemplate.lookup(JndiTemplate.java:156) 在 org.springframework.jndi.JndiTemplate.lookup(JndiTemplate.java:178) 在 org.springframework.jndi.JndiLocatorSupport.lookup(JndiLocatorSupport.java:96) 在 org.springframework.jndi.JndiObjectLocator.lookup(JndiObjectLocator.java:114) 在 org.springframework.jndi.JndiObjectTargetSource.getTarget(JndiObjectTargetSource.java:140) ...省略了39个常用框架 引起:java.lang.ClassNotFoundException:org.apache.tomcat.dbcp.dbcp2.BasicDataSourceFactory 在 java.net.URLClassLoader.findClass(URLClassLoader.java:381) 在 java.lang.ClassLoader.loadClass(ClassLoader.java:424) 在 sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:331) 在 java.lang.ClassLoader.loadClass(ClassLoader.java:357) 在 java.lang.Class.forName0(本机方法) 在 java.lang.Class.forName(Class.java:264) 在 org.apache.naming.factory.ResourceFactory.getDefaultFactory(ResourceFactory.java:47) ...省略了58个常用框架要使用 Apache JDBC 数据源,您无需添加任何依赖项,但必须将默认工厂类更改为 org.apache.tomcat.jdbc.pool.DataSourceFactory
。
您可以在资源声明中执行此操作:
resource.setProperty("factory", "org.apache.tomcat.jdbc.pool.DataSourceFactory");
我将在下面解释添加此行的位置。
要使用 DBCP 2 数据源,需要一个依赖项:
<dependency> <groupId>org.apache.tomcat</groupId> <artifactId>tomcat-dbcp</artifactId> <version>8.5.4</version> </dependency>
当然,根据你的 Spring Boot Tomcat 嵌入式版本适配神器版本。
若要使用 HikariCP,请添加所需的依赖项(如果您的配置中尚不存在)(如果您依赖 Spring Boot 的持久性启动器),例如:
<dependency> <groupId>com.zaxxer</groupId> <artifactId>HikariCP</artifactId> <version>3.1.0</version> </dependency>
并在资源声明中指定工厂:
resource.setProperty("factory", "com.zaxxer.hikari.HikariJNDIFactory");
数据源配置/声明
您必须自定义创建 TomcatServletWebServerFactory
实例的 bean。
两件事要做:
启用默认禁用的 JNDI 命名
在服务器上下文中创建和添加 JNDI 资源
例如,对于 PostgreSQL 和 DBCP 2 数据源,这样做:
@Bean
public TomcatServletWebServerFactory tomcatFactory()
return new TomcatServletWebServerFactory()
@Override
protected TomcatWebServer getTomcatWebServer(org.apache.catalina.startup.Tomcat tomcat)
tomcat.enableNaming();
return super.getTomcatWebServer(tomcat);
@Override
protected void postProcessContext(Context context)
// context
ContextResource resource = new ContextResource();
resource.setName("jdbc/myJndiResource");
resource.setType(DataSource.class.getName());
resource.setProperty("driverClassName", "org.postgresql.Driver");
resource.setProperty("url", "jdbc:postgresql://hostname:port/dbname");
resource.setProperty("username", "username");
resource.setProperty("password", "password");
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);
;
使用/注入数据源
您现在应该可以使用标准的InitialContext
实例在任何地方查找 JNDI 资源:
InitialContext initialContext = new InitialContext();
DataSource datasource = (DataSource) initialContext.lookup("java:comp/env/jdbc/myJndiResource");
你也可以使用Spring的JndiObjectFactoryBean
来查找资源:
JndiObjectFactoryBean bean = new JndiObjectFactoryBean();
bean.setJndiName("java:comp/env/jdbc/myJndiResource");
bean.afterPropertiesSet();
DataSource object = (DataSource) bean.getObject();
要利用 DI 容器,您还可以将 DataSource
设为 Spring bean:
@Bean(destroyMethod = "")
public DataSource jndiDataSource() throws IllegalArgumentException, NamingException
JndiObjectFactoryBean bean = new JndiObjectFactoryBean();
bean.setJndiName("java:comp/env/jdbc/myJndiResource");
bean.afterPropertiesSet();
return (DataSource) bean.getObject();
因此您现在可以在任何 Spring bean 中注入 DataSource,例如:
@Autowired
private DataSource jndiDataSource;
请注意,互联网上的许多示例似乎禁用了启动时对 JNDI 资源的查找:
bean.setJndiName("java:comp/env/jdbc/myJndiResource");
bean.setProxyInterface(DataSource.class);
bean.setLookupOnStartup(false);
bean.afterPropertiesSet();
但我认为它是无助的,因为它在 afterPropertiesSet()
之后调用它进行查找!
【讨论】:
首先,非常感谢。这不仅很好地描述了如何让 JNDI 与 spring boot 2 的嵌入式 tomcat 一起工作 - 它也是我发现的唯一类似的资源之一。 x100 谢谢。注意 - 我需要 setProxyInterface/setLookupOnState 调用来避免 InstanceAlreadyExistsException 异常。 @bwags 谢谢你的好话。我必须花费大约 1 天的时间才能使其正常工作:通常遗留的解决方法没有很好的文档记录......但很高兴这篇文章可以帮助其他人解决这个不常见的要求。关于你的最后一点,有趣的是,我没有通过删除这两个语句得到这个异常。 对我来说,它不适用于 Hikari,我不得不将工厂生产线更改为resource.setProperty("factory", "com.zaxxer.hikari.HikariJNDIFactory");
(而不是 HikariDataSource)
感谢您对所有内容的详细解释。这可以转换为博客,因为问题非常普遍,其他答案都没有帮助我。向你致敬!【参考方案3】:
感谢wikisona,我终于得到了答案,首先是豆子:
@Bean
public TomcatEmbeddedServletContainerFactory tomcatFactory()
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", "your.db.Driver");
resource.setProperty("url", "jdbc:yourDb");
context.getNamingResources().addResource(resource);
;
@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();
完整代码在这里:https://github.com/wilkinsona/spring-boot-sample-tomcat-jndi
【讨论】:
我在尝试使用上述代码时收到java.lang.ClassNotFoundException: org.apache.tomcat.dbcp.dbcp2.BasicDataSourceFactory
。如果我通过application.properties
定义tomcat 数据库池,一切正常。我使用了 Spring Boot 依赖项,但看起来我缺少 JNDI 的一些东西。它适用于嵌入式 tomcat 版本 8.5.x。
我通过添加行解决了问题,resource.setProperty("factory", "org.apache.tomcat.jdbc.pool.DataSourceFactory");
。 My SO Question 或者我需要添加单独的 dbcp 依赖项,但这在 spring boot 中看起来不是一个好主意。
@Sabir Khan 使用 Tomcat jdbc DS 是很可能的。但我不认为使用另一个 DS 是错误的。 Spring Boot 没有以开箱即用的方式涵盖这部分,因为在 Spring Boot 中完全不是直接使用 JNDI 的标准,但有时我们不想局限于 Tomcat jdbc DS 或不想使用它.【参考方案4】:
在 SpringBoot 2.1 中,我找到了另一个解决方案。 扩展标准工厂类方法getTomcatWebServer。然后从任何地方将其作为 bean 返回。
public class CustomTomcatServletWebServerFactory extends TomcatServletWebServerFactory
@Override
protected TomcatWebServer getTomcatWebServer(Tomcat tomcat)
System.setProperty("catalina.useNaming", "true");
tomcat.enableNaming();
return new TomcatWebServer(tomcat, getPort() >= 0);
@Component
public class TomcatConfiguration
@Bean
public ConfigurableServletWebServerFactory webServerFactory()
TomcatServletWebServerFactory factory = new CustomTomcatServletWebServerFactory();
return factory;
但从 context.xml 加载资源不起作用。将尝试找出答案。
【讨论】:
【参考方案5】:请注意,而不是
public TomcatEmbeddedServletContainerFactory tomcatFactory()
我不得不使用以下方法签名
public EmbeddedServletContainerFactory embeddedServletContainerFactory()
【讨论】:
【参考方案6】:您是否尝试过@Lazy
加载数据源?因为您是在 Spring 上下文中初始化嵌入式 Tomcat 容器,所以您必须延迟 DataSource
的初始化(直到设置 JNDI 变量)。
注意我还没有机会测试这段代码!
@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();
您可能还需要在使用 DataSource 的任何位置添加 @Lazy
注释。例如
@Lazy
@Autowired
private DataSource dataSource;
【讨论】:
它适用于我的情况。以上是关于如何使用嵌入式 Tomcat 容器在 Spring Boot 中创建 JNDI 上下文的主要内容,如果未能解决你的问题,请参考以下文章
Spring Boot使用嵌入式容器,自定义Filter如何配置?
Spring boot:无法启动嵌入式 Tomcat servlet 容器
如何在嵌入式 tomcat 服务器上部署 Spring Boot Web 应用程序,来自 Spring Boot 本身