如何使用嵌入式 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 数据源,需要一个依赖项:

&lt;dependency&gt; &lt;groupId&gt;org.apache.tomcat&lt;/groupId&gt; &lt;artifactId&gt;tomcat-dbcp&lt;/artifactId&gt; &lt;version&gt;8.5.4&lt;/version&gt; &lt;/dependency&gt;

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

若要使用 HikariCP,请添加所需的依赖项(如果您的配置中尚不存在)(如果您依赖 Spring Boot 的持久性启动器),例如:

&lt;dependency&gt; &lt;groupId&gt;com.zaxxer&lt;/groupId&gt; &lt;artifactId&gt;HikariCP&lt;/artifactId&gt; &lt;version&gt;3.1.0&lt;/version&gt; &lt;/dependency&gt;

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

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 容器

Spring Boot如何启动嵌入式Tomcat?

如何在嵌入式 tomcat 服务器上部署 Spring Boot Web 应用程序,来自 Spring Boot 本身

Spring Boot如何使用内嵌式的Tomcat和Jetty?

Spring Boot将WAR文件部署到Tomcat