Как создать 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 я пришел к такому подходу:
Создать
WebServerFactoryCustomizer
и создайте свой jndi-ресурсДобавьте прослушиватель жизненного цикла, чтобы включить именование.
Добавить зависимость 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.