Как создать JNDI-контекст в Spring Boot с помощью встроенного контейнера Tomcat

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);

                    }
                });
            }
        }
    };
}

}

Я использую весеннюю загрузку и пытаюсь запустить встроенный tomcat, который создает JNDI-контекст для моих источников данных:

    <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, я могу подключиться к приложению, которое вижу мой источник данных в MBeans (Catalina -> Ресурс -> Контекст -> "/" -> localhost -> javax.sql.DataSource -> jdbc/mydatasource)

У меня также есть MBeans, отображаемый через JConsole здесь (Tomcat -> DataSource -> / -> localhost -> javax.sql.DataSource -> jdbc/mydatasource)

Однако, когда я @ImportResource ищет то, что на самом деле ищет mydatasource через JNDI, он не находит его.

<bean id="myDS" class="org.springframework.jndi.JndiObjectFactoryBean">
    <property name="jndiName" value="java:comp/env/jdbc/mydatasource"/>
</bean>

Соответствующая часть моего импортированного XML-файла

Вы настраиваете ContextResource выше с теми же параметрами, которые я использовал в context.xml, который развертывается при развертывании приложения в контейнере Tomcat. Мои импортированные bean-компоненты и мое приложение работают правильно при развертывании в контейнере Tomcat.

Так что, похоже, у меня сейчас есть контекст, но не похоже, что это правильно. Я пробовал различные комбинации имени ресурса, но не могу создать "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

8 ответов

Решение

По умолчанию JNDI отключен во встроенном Tomcat, который вызывает NoInitialContextException, Вам нужно позвонить Tomcat.enableNaming() чтобы включить его. Самый простой способ сделать это с TomcatEmbeddedServletContainer подкласс:

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

        @Override
        protected TomcatEmbeddedServletContainer getTomcatEmbeddedServletContainer(
                Tomcat tomcat) {
            tomcat.enableNaming();
            return super.getTomcatEmbeddedServletContainer(tomcat);
        }
    };
}

Если вы используете этот подход, вы также можете зарегистрировать DataSource в JNDI, переопределив postProcessContext метод в вашем TomcatEmbeddedServletContainerFactory подкласс.

context.getNamingResources().addResource добавляет ресурс в java:comp/env контекст, поэтому имя ресурса должно быть jdbc/mydatasource не java:comp/env/mydatasource,

Tomcat использует загрузчик класса контекста потока, чтобы определить, с каким контекстом JNDI должен выполняться поиск. Вы связываете ресурс с JNDI-контекстом веб-приложения, поэтому вам нужно убедиться, что поиск выполняется, когда загрузчик классов веб-приложения является загрузчиком класса контекста потока. Вы должны быть в состоянии достичь этого, установив lookupOnStartup в false на jndiObjectFactoryBean, Вам также нужно будет установить 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>

Это создаст прокси для источника данных с фактическим поиском JNDI, выполняемым при первом использовании, а не во время запуска контекста приложения.

Описанный выше подход иллюстрируется в этом примере Spring Boot.

Недавно у меня было требование использовать JNDI со встроенным Tomcat в Spring Boot.
Фактические ответы дают некоторую подсказку, чтобы решить проблему, но этого было недостаточно, вероятно, не обновлено.

Вот мой вклад, протестированный с Spring Boot 2.0.3.RELEASE.

pom.xml

В соответствии с указанным вами источником данных вам может потребоваться предоставить библиотеку Tomcat-DBCP.
Например, при конфигурации по умолчанию создание экземпляра источника данных вызвало исключение:

Причина: 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.Tate.JT.JT. 114)
        at org.springframework.jndi.JndiObjectTargetSource.getTarget(JndiObjectTargetSource.java:140)
        ... пропущено 39 общих фреймов. Причиной является: java.lang.ClassNotFoundException: org.apache.tomcat.dbcp.acticd.jb.dll.URLClassLoader.findClass(URLClassLoader.java:381) в java.lang.ClassLoader.loadClass(ClassLoader.java:424) в sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:331) в java.lang.Classo.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 общих кадров опущено

В качестве обходного пути вы можете добавить эту зависимость:

<dependency>
  <groupId>org.apache.tomcat</groupId>
  <artifactId>tomcat-dbcp</artifactId>
  <version>8.5.4</version>
</dependency>   

Конечно, адаптируйте версию артефакта в соответствии с вашей версией Tomcat.

Конфигурация пружины

Вы должны настроить бин, который создает TomcatServletWebServerFactory пример.
Две вещи, которые нужно сделать:

  • включение именования JNDI, которое по умолчанию отключено

  • создание и добавление ресурсов JNDI в контексте сервера

Например, с источником данных базы данных PostgreSQL это выглядит так:

@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);          
        }
    };
}

Теперь вы сможете искать ресурсы JNDI в любом месте, используя стандартную InitialContext пример:

InitialContext initialContext = new InitialContext();
DataSource datasource = (DataSource) initialContext.lookup("java:comp/env/jdbc/myJndiResource");

Вы также можете использовать JndiObjectFactoryBean весны для поиска ресурса:

JndiObjectFactoryBean bean = new JndiObjectFactoryBean();
bean.setJndiName("java:comp/env/jdbc/myJndiResource");
bean.afterPropertiesSet();
DataSource object = (DataSource) bean.getObject();

Чтобы воспользоваться DI-контейнером, вы также можете сделать DataSource весенний боб:

@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();
}

И теперь вы можете внедрить DataSource в любые компоненты Spring, такие как:

@Autowired
private DataSource jndiDataSource;

Обратите внимание, что во многих примерах в Интернете поиск ресурса JNDI при запуске отключается:

bean.setJndiName("java:comp/env/jdbc/myJndiResource");
bean.setProxyInterface(DataSource.class);
bean.setLookupOnStartup(false);
bean.afterPropertiesSet(); 

Но я думаю, что это беспомощно, так как он вызывает сразу после afterPropertiesSet() это делает поиск!

В конце концов, я получил ответ благодаря 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

В весенней загрузке 2.1 я нашел другое решение. Расширьте стандартный метод фабричного класса getTomcatWebServer. А затем верните его как боб из любого места.

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 не работает, хотя. Постараюсь выяснить.

В Spring boot v3 кажется, что предыдущие решения больше невозможны.

С документацией Spring я пришел к такому подходу:

  1. СоздатьWebServerFactoryCustomizerи создайте свой jndi-ресурс

  2. Добавьте прослушиватель жизненного цикла, чтобы включить именование.

  3. Добавить зависимость Tomcat JDBC

Настройщик:

      @Component
public class MyDatasourceJndiCustomizer implements WebServerFactoryCustomizer<TomcatServletWebServerFactory> {

  @Value("${mydatasource.jndi.name}")
  private String jndiName;

  @Value("${mydatasource.jndi.driver-class-name}")
  private String driverClassName;

  @Value("${mydatasource.jndi.url}")
  private String url;

  @Value("${mydatasource.username}")
  private String username;

  @Value("${jndi.password}")
  private String password;

  @Override
  public void customize(TomcatServletWebServerFactory server) {
    server.addContextCustomizers(new TomcatContextCustomizer() {
      @Override
      public void customize(Context context) {
        ContextResource resource = new ContextResource();
        resource.setName(jndiName);
        resource.setType(DataSource.class.getName());
        resource.setProperty("driverClassName", driverClassName);
        resource.setProperty("factory", "org.apache.tomcat.jdbc.pool.DataSourceFactory");
        resource.setProperty("url", url);
        resource.setProperty("username", username);
        resource.setProperty("password", password);
        context.getNamingResources()
          .addResource(resource);
      }

    });

    enableNaming(server);
  }


  private static void enableNaming(TomcatServletWebServerFactory server) {
    server.addContextLifecycleListeners(new NamingContextListener());
    
    // The following code is copied from Tomcat 
    System.setProperty("catalina.useNaming", "true");
    String value = "org.apache.naming";
    String oldValue = System.getProperty("java.naming.factory.url.pkgs");
    if (oldValue != null) {
      if (oldValue.contains(value)) {
        value = oldValue;
      } else {
        value = value + ":" + oldValue;
      }
    }

    System.setProperty("java.naming.factory.url.pkgs", value);
    value = System.getProperty("java.naming.factory.initial");
    if (value == null) {
      System.setProperty("java.naming.factory.initial", "org.apache.naming.java.javaURLContextFactory");
    }
  }

}

Зависимость:

          <dependency>
      <groupId>org.apache.tomcat</groupId>
      <artifactId>tomcat-jdbc</artifactId>
      <version>10.1.9</version>
    </dependency>

Пожалуйста, обратите внимание, вместо

public TomcatEmbeddedServletContainerFactory tomcatFactory()

Мне пришлось использовать следующий метод подписи

public EmbeddedServletContainerFactory embeddedServletContainerFactory() 

Ты пытался @Lazy загрузка источника данных? Поскольку вы инициализируете встроенный контейнер Tomcat в контексте Spring, вы должны отложить инициализацию вашего DataSource (пока не будут установлены JNDI vars).

NB У меня еще не было возможности протестировать этот код!

@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();
}

Вам также может понадобиться добавить @Lazy аннотации везде, где используется источник данных. например

@Lazy
@Autowired
private DataSource dataSource;

Пришлось решать эту проблему с нуля, так как ни один из примеров, которые мне попадались, мне не подошел. Похоже, это зависит от конфигурации встроенного приложения...

Цель: устаревшее приложение в виде военного файла с ресурсом JNDI , запускаемое со встроенным сервером.

Приведенный ниже фрагмент работает как для Spring Boot 2, так и для Spring Boot 3 , но будьте осторожны, поскольку Spring Boot 3 использует спецификацию Jakarta EE 9 (имеет новый пакет jakarta верхнего уровня ), которая может быть несовместима с вашим приложением (как в моем случае).

Основные болевые точки, отмеченные значком //важно:

      @Bean
public TomcatServletWebServerFactory servletContainerFactory() {
    return new TomcatServletWebServerFactory() {
        @Override
        protected TomcatWebServer getTomcatWebServer(Tomcat tomcat) {
            //add webapp
            try {
                //...app preparation here
                final Context context = tomcat.addWebapp(contextPath, application.getURL());
                context.setParentClassLoader(getClass().getClassLoader()); //important: helps the embedded app reach spring boot dependencies
            } catch (IOException e) {
                throw new RuntimeException(e);
            }

            //configure jndi
            tomcat.enableNaming(); //important: speaks for itself
            ContextResource resource = new ContextResource();
            resource.setType(DataSource.class.getName());
            resource.setName("jdbc/JNDI_NAME_HERE");
            resource.setProperty("factory", HikariJNDIFactory.class.getName());
            resource.setProperty("jdbcUrl", getUrl());
            resource.setProperty("driverClassName", getDriverClassName());
            tomcat.getServer().getGlobalNamingResources().addResource(resource); //important: solution for successful jndi lookup

            return super.getTomcatWebServer(tomcat);
        }
    };
}

Всего всего 3 шага и только 2 специально для jndi.

Другие вопросы по тегам