Add Bitronix support classes

Add support classes for the Bitronix JTA library, including:

- A Spring friendly ConnectionFactoryBean and DataSourceBean with
  support for setting a direct XA source.
- A PostProcessor to apply shutdown ordering automatically.

See gh-947
This commit is contained in:
Phillip Webb 2014-08-21 22:24:30 -07:00
parent 983ec0ebc8
commit b91274c0db
8 changed files with 632 additions and 0 deletions

View File

@ -49,6 +49,7 @@
<activemq.version>5.9.1</activemq.version>
<aspectj.version>1.8.2</aspectj.version>
<atomikos.version>3.9.3</atomikos.version>
<bitronix.version>2.1.4</bitronix.version>
<codahale-metrics.version>3.0.2</codahale-metrics.version>
<commons-beanutils.version>1.9.2</commons-beanutils.version>
<commons-collections.version>3.2.1</commons-collections.version>
@ -387,6 +388,11 @@
<artifactId>metrics-servlets</artifactId>
<version>${codahale-metrics.version}</version>
</dependency>
<dependency>
<groupId>org.codehaus.btm</groupId>
<artifactId>btm</artifactId>
<version>${bitronix.version}</version>
</dependency>
<dependency>
<groupId>org.codehaus.janino</groupId>
<artifactId>janino</artifactId>

View File

@ -89,6 +89,11 @@
<artifactId>tomcat-embed-jasper</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.codehaus.btm</groupId>
<artifactId>btm</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.codehaus.groovy</groupId>
<artifactId>groovy</artifactId>

View File

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

View File

@ -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<PoolingConnectionFactoryBean> source = new ThreadLocal<PoolingConnectionFactoryBean>();
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;
}
}
}

View File

@ -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<PoolingDataSourceBean> source = new ThreadLocal<PoolingDataSourceBean>();
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;
}
}
}

View File

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

View File

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

View File

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