Как создать контекст JNDI в Spring Загрузка с встроенным контейнером 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);
}
});
}
}
};
}
}
Я использую загрузку spring и пытаюсь запустить с помощью встроенного 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 → Resource → Context → "/" → 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 файла
Контекстный ресурс, который я настраиваю выше, имеет те же параметры, которые я использовал в контексте .xml, который развертывается при развертывании приложения в контейнере tomcat. Мой импортированный beans, и мое приложение работает правильно, когда оно развернуто в контейнер 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
Ответы
Ответ 1
По умолчанию 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
not 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>
Это создаст прокси для DataSource, при котором фактический поиск JNDI выполняется при первом использовании, а не при запуске контекста приложения.
Описанный выше подход иллюстрируется этим Spring образцом загрузки.
Ответ 2
В конце концов, я получил ответ благодаря 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
Ответ 3
Недавно у меня было требование использовать JNDI со встроенным Tomcat в Spring Boot.
Фактические ответы дают некоторые интересные подсказки для решения моей задачи, но этого было недостаточно, поскольку, вероятно, не обновлены для Spring Boot 2.
Вот мой вклад, протестированный с Spring Boot 2.0.3.RELEASE.
Указание источника данных, доступного в пути к классам во время выполнения
У вас есть несколько вариантов:
- использование источника данных DBCP 2 (вы не хотите использовать устаревший и менее эффективный DBCP 1).
- используя источник данных Tomcat JDBC.
- используя любой другой источник данных: например, HikariCP.
Если вы не укажете никого из них, при конфигурации по умолчанию создание источника данных вызовет исключение:
Caused by: javax.naming.NamingException: Could not create resource factory instance
at org.apache.naming.factory.ResourceFactory.getDefaultFactory(ResourceFactory.java:50)
at org.apache.naming.factory.FactoryBase.getObjectInstance(FactoryBase.java:90)
at javax.naming.spi.NamingManager.getObjectInstance(NamingManager.java:321)
at org.apache.naming.NamingContext.lookup(NamingContext.java:839)
at org.apache.naming.NamingContext.lookup(NamingContext.java:159)
at org.apache.naming.NamingContext.lookup(NamingContext.java:827)
at org.apache.naming.NamingContext.lookup(NamingContext.java:159)
at org.apache.naming.NamingContext.lookup(NamingContext.java:827)
at org.apache.naming.NamingContext.lookup(NamingContext.java:159)
at org.apache.naming.NamingContext.lookup(NamingContext.java:827)
at org.apache.naming.NamingContext.lookup(NamingContext.java:173)
at org.apache.naming.SelectorContext.lookup(SelectorContext.java:163)
at javax.naming.InitialContext.lookup(InitialContext.java:417)
at org.springframework.jndi.JndiTemplate.lambda$lookup$0(JndiTemplate.java:156)
at org.springframework.jndi.JndiTemplate.execute(JndiTemplate.java:91)
at org.springframework.jndi.JndiTemplate.lookup(JndiTemplate.java:156)
at org.springframework.jndi.JndiTemplate.lookup(JndiTemplate.java:178)
at org.springframework.jndi.JndiLocatorSupport.lookup(JndiLocatorSupport.java:96)
at org.springframework.jndi.JndiObjectLocator.lookup(JndiObjectLocator.java:114)
at org.springframework.jndi.JndiObjectTargetSource.getTarget(JndiObjectTargetSource.java:140)
... 39 common frames omitted
Caused by: java.lang.ClassNotFoundException: org.apache.tomcat.dbcp.dbcp2.BasicDataSourceFactory
at java.net.URLClassLoader.findClass(URLClassLoader.java:381)
at java.lang.ClassLoader.loadClass(ClassLoader.java:424)
at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:331)
at java.lang.ClassLoader.loadClass(ClassLoader.java:357)
at java.lang.Class.forName0(Native Method)
at java.lang.Class.forName(Class.java:264)
at org.apache.naming.factory.ResourceFactory.getDefaultFactory(ResourceFactory.java:47)
... 58 common frames omitted
-
Чтобы использовать источник данных 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.HikariDataSource");
Конфигурация источника данных/декларация
Вы должны настроить bean-компонент, который создает экземпляр TomcatServletWebServerFactory
.
Две вещи, которые нужно сделать:
-
включение именования 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 initialContext = new InitialContext();
DataSource datasource = (DataSource) initialContext.lookup("java:comp/env/jdbc/myJndiResource");
Вы также можете использовать JndiObjectFactoryBean
of Spring для поиска ресурса:
JndiObjectFactoryBean bean = new JndiObjectFactoryBean();
bean.setJndiName("java:comp/env/jdbc/myJndiResource");
bean.afterPropertiesSet();
DataSource object = (DataSource) bean.getObject();
Чтобы воспользоваться преимуществами контейнера DI, вы также можете сделать DataSource
компонентом Spring:
@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()
который выполняет поиск!
Ответ 4
Обратите внимание, что вместо
public TomcatEmbeddedServletContainerFactory tomcatFactory()
Мне пришлось использовать следующую сигнатуру метода
public EmbeddedServletContainerFactory embeddedServletContainerFactory()
Ответ 5
Вы пробовали @Lazy
загрузку источника данных? Поскольку вы инициализируете встроенный контейнер Tomcat в контексте Spring, вам нужно отложить инициализацию вашего DataSource
(пока не будут установлены JNDI-вары).
N.B. Я еще не успел протестировать этот код!
@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
везде, где используется DataSource. например.
@Lazy
@Autowired
private DataSource dataSource;
Ответ 6
В весенней загрузке 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 не работает, хотя. Постараюсь выяснить.