diff --git a/spring-boot-dependencies/pom.xml b/spring-boot-dependencies/pom.xml index 1811d1b319..9cdc69299f 100644 --- a/spring-boot-dependencies/pom.xml +++ b/spring-boot-dependencies/pom.xml @@ -49,6 +49,7 @@ 5.9.1 1.8.2 3.9.3 + 2.1.4 3.0.2 1.9.2 3.2.1 @@ -387,6 +388,11 @@ metrics-servlets ${codahale-metrics.version} + + org.codehaus.btm + btm + ${bitronix.version} + org.codehaus.janino janino diff --git a/spring-boot/pom.xml b/spring-boot/pom.xml index d37027dc2c..7036d0cb55 100644 --- a/spring-boot/pom.xml +++ b/spring-boot/pom.xml @@ -89,6 +89,11 @@ tomcat-embed-jasper true + + org.codehaus.btm + btm + true + org.codehaus.groovy groovy diff --git a/spring-boot/src/main/java/org/springframework/boot/jta/bitronix/BitronixDependentBeanFactoryPostProcessor.java b/spring-boot/src/main/java/org/springframework/boot/jta/bitronix/BitronixDependentBeanFactoryPostProcessor.java new file mode 100644 index 0000000000..df800b196a --- /dev/null +++ b/spring-boot/src/main/java/org/springframework/boot/jta/bitronix/BitronixDependentBeanFactoryPostProcessor.java @@ -0,0 +1,85 @@ +/* + * Copyright 2012-2014 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.jta.bitronix; + +import javax.transaction.TransactionManager; + +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.config.BeanFactoryPostProcessor; +import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; +import org.springframework.core.Ordered; + +/** + * {@link BeanFactoryPostProcessor} to automatically register the recommended + * {@link ConfigurableListableBeanFactory#registerDependentBean(String, String) + * dependencies} for correct Bitronix shutdown ordering. With Bitronix it appears that + * ConnectionFactory and DataSource beans must be shutdown before the + * {@link TransactionManager}. + * + * @author Phillip Webb + * @since 1.2.0 + */ +public class BitronixDependentBeanFactoryPostProcessor implements + BeanFactoryPostProcessor, Ordered { + + private static final String[] NO_BEANS = {}; + + private int order = Ordered.LOWEST_PRECEDENCE; + + @Override + public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) + throws BeansException { + String[] transactionManagers = beanFactory.getBeanNamesForType( + TransactionManager.class, true, false); + for (String transactionManager : transactionManagers) { + addTransactionManagerDependencies(beanFactory, transactionManager); + } + } + + private void addTransactionManagerDependencies( + ConfigurableListableBeanFactory beanFactory, String transactionManager) { + for (String dependentBeanName : getBeanNamesForType(beanFactory, + "javax.jms.ConnectionFactory")) { + beanFactory.registerDependentBean(transactionManager, dependentBeanName); + } + for (String dependentBeanName : getBeanNamesForType(beanFactory, + "javax.sql.DataSource")) { + beanFactory.registerDependentBean(transactionManager, dependentBeanName); + } + } + + private String[] getBeanNamesForType(ConfigurableListableBeanFactory beanFactory, + String type) { + try { + return beanFactory.getBeanNamesForType(Class.forName(type), true, false); + } + catch (ClassNotFoundException ex) { + // Ignore + } + return NO_BEANS; + } + + @Override + public int getOrder() { + return this.order; + } + + public void setOrder(int order) { + this.order = order; + } + +} diff --git a/spring-boot/src/main/java/org/springframework/boot/jta/bitronix/PoolingConnectionFactoryBean.java b/spring-boot/src/main/java/org/springframework/boot/jta/bitronix/PoolingConnectionFactoryBean.java new file mode 100644 index 0000000000..215e82db2f --- /dev/null +++ b/spring-boot/src/main/java/org/springframework/boot/jta/bitronix/PoolingConnectionFactoryBean.java @@ -0,0 +1,141 @@ +/* + * Copyright 2012-2014 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.jta.bitronix; + +import java.util.Properties; + +import javax.jms.JMSException; +import javax.jms.XAConnection; +import javax.jms.XAConnectionFactory; + +import org.springframework.beans.factory.BeanNameAware; +import org.springframework.beans.factory.DisposableBean; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.util.StringUtils; + +import bitronix.tm.resource.common.ResourceBean; +import bitronix.tm.resource.common.XAStatefulHolder; +import bitronix.tm.resource.jms.PoolingConnectionFactory; + +/** + * Spring friendly version of {@link PoolingConnectionFactory}. Provides sensible defaults + * and also supports direct wrapping of a {@link XAConnectionFactory} instance. + * + * @author Phillip Webb + * @author Josh Long + * @since 1.2.0 + */ +public class PoolingConnectionFactoryBean extends PoolingConnectionFactory implements + BeanNameAware, InitializingBean, DisposableBean { + + private static ThreadLocal source = new ThreadLocal(); + + private String beanName; + + private XAConnectionFactory connectionFactory; + + public PoolingConnectionFactoryBean() { + setMaxPoolSize(10); + setTestConnections(true); + setAutomaticEnlistingEnabled(true); + setAllowLocalTransactions(true); + } + + @Override + public synchronized void init() { + source.set(this); + try { + super.init(); + } + finally { + source.remove(); + } + } + + @Override + public void setBeanName(String name) { + this.beanName = name; + } + + @Override + public void afterPropertiesSet() throws Exception { + if (!StringUtils.hasLength(getUniqueName())) { + setUniqueName(this.beanName); + } + init(); + } + + @Override + public void destroy() throws Exception { + close(); + } + + /** + * Set the {@link XAConnectionFactory} directly, instead of calling + * {@link #setClassName(String)}. + * @param connectionFactory the connection factory to use + */ + public void setConnectionFactory(XAConnectionFactory connectionFactory) { + this.connectionFactory = connectionFactory; + setClassName(DirectXAConnectionFactory.class.getName()); + setDriverProperties(new Properties()); + } + + protected final XAConnectionFactory getConnectionFactory() { + return this.connectionFactory; + } + + @Override + public XAStatefulHolder createPooledConnection(Object xaFactory, ResourceBean bean) + throws Exception { + if (xaFactory instanceof DirectXAConnectionFactory) { + xaFactory = ((DirectXAConnectionFactory) xaFactory).getConnectionFactory(); + } + return super.createPooledConnection(xaFactory, bean); + } + + /** + * A {@link XAConnectionFactory} implementation that delegates to the + * {@link ThreadLocal} {@link PoolingConnectionFactoryBean}. + * @see PoolingConnectionFactoryBean#setConnectionFactory(XAConnectionFactory) + */ + public static class DirectXAConnectionFactory implements XAConnectionFactory { + + private final XAConnectionFactory connectionFactory; + + public DirectXAConnectionFactory() { + this.connectionFactory = source.get().connectionFactory; + } + + @Override + public XAConnection createXAConnection() throws JMSException { + return this.connectionFactory.createXAConnection(); + } + + @Override + public XAConnection createXAConnection(String userName, String password) + throws JMSException { + return this.connectionFactory.createXAConnection(userName, password); + } + + public XAConnectionFactory getConnectionFactory() { + return this.connectionFactory; + } + + } + +} diff --git a/spring-boot/src/main/java/org/springframework/boot/jta/bitronix/PoolingDataSourceBean.java b/spring-boot/src/main/java/org/springframework/boot/jta/bitronix/PoolingDataSourceBean.java new file mode 100644 index 0000000000..c128ad8856 --- /dev/null +++ b/spring-boot/src/main/java/org/springframework/boot/jta/bitronix/PoolingDataSourceBean.java @@ -0,0 +1,161 @@ +/* + * Copyright 2012-2014 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.jta.bitronix; + +import java.io.PrintWriter; +import java.sql.SQLException; +import java.sql.SQLFeatureNotSupportedException; +import java.util.Properties; +import java.util.logging.Logger; + +import javax.sql.XAConnection; +import javax.sql.XADataSource; + +import org.springframework.beans.factory.BeanNameAware; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.util.StringUtils; + +import bitronix.tm.resource.common.ResourceBean; +import bitronix.tm.resource.common.XAStatefulHolder; +import bitronix.tm.resource.jdbc.PoolingDataSource; + +/** + * Spring friendly version of {@link PoolingDataSource}. Provides sensible defaults and + * also supports direct wrapping of a {@link XADataSource} instance. + * + * @author Phillip Webb + * @author Josh Long + * @since 1.2.0 + */ +public class PoolingDataSourceBean extends PoolingDataSource implements BeanNameAware, + InitializingBean { + + private static ThreadLocal source = new ThreadLocal(); + + private XADataSource dataSource; + + private String beanName; + + public PoolingDataSourceBean() { + super(); + setMaxPoolSize(10); + setAllowLocalTransactions(true); + setEnableJdbc4ConnectionTest(true); + } + + @Override + public synchronized void init() { + source.set(this); + try { + super.init(); + } + finally { + source.remove(); + } + } + + @Override + public void setBeanName(String name) { + this.beanName = name; + } + + @Override + public void afterPropertiesSet() throws Exception { + if (!StringUtils.hasLength(getUniqueName())) { + setUniqueName(this.beanName); + } + } + + /** + * Set the {@link XADataSource} directly, instead of calling + * {@link #setClassName(String)}. + * @param dataSource the data source to use + */ + public void setDataSource(XADataSource dataSource) { + this.dataSource = dataSource; + setClassName(DirectXADataSource.class.getName()); + setDriverProperties(new Properties()); + } + + protected final XADataSource getDataSource() { + return this.dataSource; + } + + @Override + public XAStatefulHolder createPooledConnection(Object xaFactory, ResourceBean bean) + throws Exception { + if (xaFactory instanceof DirectXADataSource) { + xaFactory = ((DirectXADataSource) xaFactory).getDataSource(); + } + return super.createPooledConnection(xaFactory, bean); + } + + /** + * A {@link XADataSource} implementation that delegates to the {@link ThreadLocal} + * {@link PoolingDataSourceBean}. + * @see PoolingDataSourceBean#setDataSource(XADataSource) + */ + public static class DirectXADataSource implements XADataSource { + + private final XADataSource dataSource; + + public DirectXADataSource() { + this.dataSource = source.get().dataSource; + } + + @Override + public PrintWriter getLogWriter() throws SQLException { + return this.dataSource.getLogWriter(); + } + + @Override + public XAConnection getXAConnection() throws SQLException { + return this.dataSource.getXAConnection(); + } + + @Override + public XAConnection getXAConnection(String user, String password) + throws SQLException { + return this.dataSource.getXAConnection(user, password); + } + + @Override + public void setLogWriter(PrintWriter out) throws SQLException { + this.dataSource.setLogWriter(out); + } + + @Override + public void setLoginTimeout(int seconds) throws SQLException { + this.dataSource.setLoginTimeout(seconds); + } + + @Override + public int getLoginTimeout() throws SQLException { + return this.dataSource.getLoginTimeout(); + } + + @Override + public Logger getParentLogger() throws SQLFeatureNotSupportedException { + return this.dataSource.getParentLogger(); + } + + public XADataSource getDataSource() { + return this.dataSource; + } + } + +} diff --git a/spring-boot/src/test/java/org/springframework/boot/jta/bitronix/BitronixDependentBeanFactoryPostProcessorTests.java b/spring-boot/src/test/java/org/springframework/boot/jta/bitronix/BitronixDependentBeanFactoryPostProcessorTests.java new file mode 100644 index 0000000000..8090459f90 --- /dev/null +++ b/spring-boot/src/test/java/org/springframework/boot/jta/bitronix/BitronixDependentBeanFactoryPostProcessorTests.java @@ -0,0 +1,80 @@ +/* + * Copyright 2012-2014 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.jta.bitronix; + +import javax.jms.ConnectionFactory; +import javax.sql.DataSource; + +import org.junit.Test; +import org.springframework.beans.factory.support.DefaultListableBeanFactory; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import bitronix.tm.BitronixTransactionManager; + +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; + +/** + * Tests for {@link BitronixDependentBeanFactoryPostProcessor}. + * + * @author Phillip Webb + */ +public class BitronixDependentBeanFactoryPostProcessorTests { + + private AnnotationConfigApplicationContext context; + + @Test + public void setsDependsOn() { + DefaultListableBeanFactory beanFactory = spy(new DefaultListableBeanFactory()); + this.context = new AnnotationConfigApplicationContext(beanFactory); + this.context.register(Config.class); + this.context.refresh(); + String name = "bitronixTransactionManager"; + verify(beanFactory).registerDependentBean(name, "dataSource"); + verify(beanFactory).registerDependentBean(name, "connectionFactory"); + this.context.close(); + } + + @Configuration + static class Config { + + @Bean + public DataSource dataSource() { + return mock(DataSource.class); + } + + @Bean + public ConnectionFactory connectionFactory() { + return mock(ConnectionFactory.class); + } + + @Bean + public BitronixTransactionManager bitronixTransactionManager() { + return mock(BitronixTransactionManager.class); + } + + @Bean + public static BitronixDependentBeanFactoryPostProcessor bitronixPostProcessor() { + return new BitronixDependentBeanFactoryPostProcessor(); + } + + } + +} diff --git a/spring-boot/src/test/java/org/springframework/boot/jta/bitronix/PoolingConnectionFactoryBeanTests.java b/spring-boot/src/test/java/org/springframework/boot/jta/bitronix/PoolingConnectionFactoryBeanTests.java new file mode 100644 index 0000000000..fa0225ac79 --- /dev/null +++ b/spring-boot/src/test/java/org/springframework/boot/jta/bitronix/PoolingConnectionFactoryBeanTests.java @@ -0,0 +1,76 @@ +/* + * Copyright 2012-2014 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.jta.bitronix; + +import javax.jms.XAConnectionFactory; + +import org.junit.Test; + +import static org.hamcrest.Matchers.equalTo; +import static org.junit.Assert.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; + +/** + * Tests for {@link PoolingConnectionFactoryBean}. + * + * @author Phillip Webb + */ +public class PoolingConnectionFactoryBeanTests { + + private PoolingConnectionFactoryBean bean = new PoolingConnectionFactoryBean() { + @Override + public synchronized void init() { + // Stub out for the tests + }; + }; + + @Test + public void sensbileDefaults() throws Exception { + assertThat(this.bean.getMaxPoolSize(), equalTo(10)); + assertThat(this.bean.getTestConnections(), equalTo(true)); + assertThat(this.bean.getAutomaticEnlistingEnabled(), equalTo(true)); + assertThat(this.bean.getAllowLocalTransactions(), equalTo(true)); + } + + @Test + public void setsUniqueNameIfNull() throws Exception { + this.bean.setBeanName("beanName"); + this.bean.afterPropertiesSet(); + assertThat(this.bean.getUniqueName(), equalTo("beanName")); + } + + @Test + public void doesNotSetUniqueNameIfNotNull() throws Exception { + this.bean.setBeanName("beanName"); + this.bean.setUniqueName("un"); + this.bean.afterPropertiesSet(); + assertThat(this.bean.getUniqueName(), equalTo("un")); + } + + @Test + public void setConnectionFactory() throws Exception { + XAConnectionFactory factory = mock(XAConnectionFactory.class); + this.bean.setConnectionFactory(factory); + this.bean.setBeanName("beanName"); + this.bean.afterPropertiesSet(); + this.bean.init(); + this.bean.createPooledConnection(factory, this.bean); + verify(factory).createXAConnection(); + } + +} diff --git a/spring-boot/src/test/java/org/springframework/boot/jta/bitronix/PoolingDataSourceBeanTests.java b/spring-boot/src/test/java/org/springframework/boot/jta/bitronix/PoolingDataSourceBeanTests.java new file mode 100644 index 0000000000..16cfa9f734 --- /dev/null +++ b/spring-boot/src/test/java/org/springframework/boot/jta/bitronix/PoolingDataSourceBeanTests.java @@ -0,0 +1,78 @@ +/* + * Copyright 2012-2014 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.jta.bitronix; + +import java.sql.Connection; + +import javax.sql.XAConnection; +import javax.sql.XADataSource; + +import org.junit.Test; + +import static org.hamcrest.Matchers.equalTo; +import static org.junit.Assert.assertThat; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; + +/** + * Tests for {@link PoolingDataSourceBean}. + * + * @author Phillip Webb + */ +public class PoolingDataSourceBeanTests { + + private PoolingDataSourceBean bean = new PoolingDataSourceBean(); + + @Test + public void sensbileDefaults() throws Exception { + assertThat(this.bean.getMaxPoolSize(), equalTo(10)); + assertThat(this.bean.getAutomaticEnlistingEnabled(), equalTo(true)); + assertThat(this.bean.isEnableJdbc4ConnectionTest(), equalTo(true)); + } + + @Test + public void setsUniqueNameIfNull() throws Exception { + this.bean.setBeanName("beanName"); + this.bean.afterPropertiesSet(); + assertThat(this.bean.getUniqueName(), equalTo("beanName")); + } + + @Test + public void doesNotSetUniqueNameIfNotNull() throws Exception { + this.bean.setBeanName("beanName"); + this.bean.setUniqueName("un"); + this.bean.afterPropertiesSet(); + assertThat(this.bean.getUniqueName(), equalTo("un")); + } + + @Test + public void setDataSource() throws Exception { + XADataSource dataSource = mock(XADataSource.class); + XAConnection xaConnection = mock(XAConnection.class); + Connection connection = mock(Connection.class); + given(dataSource.getXAConnection()).willReturn(xaConnection); + given(xaConnection.getConnection()).willReturn(connection); + this.bean.setDataSource(dataSource); + this.bean.setBeanName("beanName"); + this.bean.afterPropertiesSet(); + this.bean.init(); + this.bean.createPooledConnection(dataSource, this.bean); + verify(dataSource).getXAConnection(); + } + +}