diff --git a/spring-boot-autoconfigure/pom.xml b/spring-boot-autoconfigure/pom.xml index 40e3303c5c..b36152d257 100644 --- a/spring-boot-autoconfigure/pom.xml +++ b/spring-boot-autoconfigure/pom.xml @@ -25,6 +25,16 @@ spring-boot + + com.atomikos + transactions-jdbc + true + + + com.atomikos + transactions-jta + true + com.fasterxml.jackson.core jackson-databind @@ -80,6 +90,11 @@ velocity true + + org.codehaus.btm + btm + true + org.codehaus.groovy groovy-templates diff --git a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/condition/ConditionalOnJndi.java b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/condition/ConditionalOnJndi.java new file mode 100644 index 0000000000..8015c01749 --- /dev/null +++ b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/condition/ConditionalOnJndi.java @@ -0,0 +1,39 @@ +/* + * 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.autoconfigure.condition; + +import javax.naming.InitialContext; + +import org.springframework.context.annotation.Conditional; + +/** + * {@link Conditional} that matches based on the availability of a JNDI + * {@link InitialContext} and the ability to lookup specific locations. + * + * @author Phillip Webb + * @since 1.2.0 + */ +@Conditional(OnJndiCondition.class) +public @interface ConditionalOnJndi { + + /** + * JNDI Locations, one of which must exist. If no locations are specific the condition + * matches solely based on the presence of an {@link InitialContext}. + */ + String[] value() default {}; + +} diff --git a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/condition/OnJndiCondition.java b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/condition/OnJndiCondition.java new file mode 100644 index 0000000000..3b85fb0dd1 --- /dev/null +++ b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/condition/OnJndiCondition.java @@ -0,0 +1,101 @@ +/* + * 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.autoconfigure.condition; + +import javax.naming.NamingException; + +import org.springframework.context.annotation.Condition; +import org.springframework.context.annotation.ConditionContext; +import org.springframework.core.annotation.AnnotationAttributes; +import org.springframework.core.type.AnnotatedTypeMetadata; +import org.springframework.jndi.JndiLocatorDelegate; +import org.springframework.jndi.JndiLocatorSupport; +import org.springframework.util.StringUtils; + +/** + * {@link Condition} that checks for JNDI locations. + * + * @author Phillip Webb + * @since 1.2.0 + * @see ConditionalOnJndi + */ +class OnJndiCondition extends SpringBootCondition { + + @Override + public ConditionOutcome getMatchOutcome(ConditionContext context, + AnnotatedTypeMetadata metadata) { + AnnotationAttributes annotationAttributes = AnnotationAttributes.fromMap(metadata + .getAnnotationAttributes(ConditionalOnJndi.class.getName())); + String[] locations = annotationAttributes.getStringArray("value"); + try { + return getMatchOutcome(locations); + } + catch (NoClassDefFoundError ex) { + return ConditionOutcome.noMatch("JNDI class not found"); + } + } + + private ConditionOutcome getMatchOutcome(String[] locations) { + if (!isJndiAvailable()) { + return ConditionOutcome.noMatch("JNDI environment is not available"); + } + if (locations.length == 0) { + return ConditionOutcome.match("JNDI environment is available"); + } + JndiLocator locator = getJndiLocator(locations); + String location = locator.lookupFirstLocation(); + if (location != null) { + return ConditionOutcome.match("JNDI location '" + location + + "' found from candidates " + + StringUtils.arrayToCommaDelimitedString(locations)); + } + return ConditionOutcome.noMatch("No JNDI location found from candidates " + + StringUtils.arrayToCommaDelimitedString(locations)); + } + + protected boolean isJndiAvailable() { + return JndiLocatorDelegate.isDefaultJndiEnvironmentAvailable(); + } + + protected JndiLocator getJndiLocator(String[] locations) { + return new JndiLocator(locations); + } + + protected static class JndiLocator extends JndiLocatorSupport { + + private String[] locations; + + public JndiLocator(String[] locations) { + this.locations = locations; + } + + public String lookupFirstLocation() { + for (String location : this.locations) { + try { + lookup(location); + return location; + } + catch (NamingException ex) { + // Swallow and continue + } + } + return null; + } + + } + +} diff --git a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jdbc/DataSourceAutoConfiguration.java b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jdbc/DataSourceAutoConfiguration.java index e07915bb24..5a68991a21 100644 --- a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jdbc/DataSourceAutoConfiguration.java +++ b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jdbc/DataSourceAutoConfiguration.java @@ -17,6 +17,7 @@ package org.springframework.boot.autoconfigure.jdbc; import javax.sql.DataSource; +import javax.sql.XADataSource; import org.springframework.beans.factory.BeanFactoryUtils; import org.springframework.beans.factory.NoSuchBeanDefinitionException; @@ -74,7 +75,7 @@ public class DataSourceAutoConfiguration { } @Conditional(DataSourceAutoConfiguration.EmbeddedDataSourceCondition.class) - @ConditionalOnMissingBean(DataSource.class) + @ConditionalOnMissingBean({ DataSource.class, XADataSource.class }) @Import(EmbeddedDataSourceConfiguration.class) protected static class EmbeddedConfiguration { @@ -92,7 +93,7 @@ public class DataSourceAutoConfiguration { } @Conditional(DataSourceAutoConfiguration.NonEmbeddedDataSourceCondition.class) - @ConditionalOnMissingBean(DataSource.class) + @ConditionalOnMissingBean({ DataSource.class, XADataSource.class }) protected static class NonEmbeddedConfiguration { @Autowired @@ -196,7 +197,8 @@ public class DataSourceAutoConfiguration { @Override public ConditionOutcome getMatchOutcome(ConditionContext context, AnnotatedTypeMetadata metadata) { - if (hasBean(context, DataSource.class)) { + if (hasBean(context, DataSource.class) + || hasBean(context, XADataSource.class)) { return ConditionOutcome .match("existing bean configured database detected"); } @@ -210,6 +212,7 @@ public class DataSourceAutoConfiguration { return BeanFactoryUtils.beanNamesForTypeIncludingAncestors( context.getBeanFactory(), type, true, false).length > 0; } + } } diff --git a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jdbc/DataSourceBuilder.java b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jdbc/DataSourceBuilder.java index cecc04a3c0..18d13e3f28 100644 --- a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jdbc/DataSourceBuilder.java +++ b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jdbc/DataSourceBuilder.java @@ -49,8 +49,6 @@ public class DataSourceBuilder { private ClassLoader classLoader; - private DriverClassNameProvider driverClassNameProvider = new DriverClassNameProvider(); - private Map properties = new HashMap(); public static DataSourceBuilder create() { @@ -76,9 +74,9 @@ public class DataSourceBuilder { private void maybeGetDriverClassName() { if (!this.properties.containsKey("driverClassName") && this.properties.containsKey("url")) { - String cls = this.driverClassNameProvider.getDriverClassName(this.properties - .get("url")); - this.properties.put("driverClassName", cls); + String url = this.properties.get("url"); + String driverClass = DatabaseDriver.fromJdbcUrl(url).getDriverClassName(); + this.properties.put("driverClassName", driverClass); } } diff --git a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jdbc/DataSourceProperties.java b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jdbc/DataSourceProperties.java index e497399285..fb8ee46c7e 100644 --- a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jdbc/DataSourceProperties.java +++ b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jdbc/DataSourceProperties.java @@ -16,6 +16,9 @@ package org.springframework.boot.autoconfigure.jdbc; +import java.util.LinkedHashMap; +import java.util.Map; + import org.springframework.beans.factory.BeanClassLoaderAware; import org.springframework.beans.factory.BeanCreationException; import org.springframework.beans.factory.InitializingBean; @@ -64,7 +67,7 @@ public class DataSourceProperties implements BeanClassLoaderAware, InitializingB private EmbeddedDatabaseConnection embeddedDatabaseConnection = EmbeddedDatabaseConnection.NONE; - private DriverClassNameProvider driverClassNameProvider = new DriverClassNameProvider(); + private Xa xa = new Xa(); @Override public void setBeanClassLoader(ClassLoader classLoader) { @@ -86,7 +89,7 @@ public class DataSourceProperties implements BeanClassLoaderAware, InitializingB String driverClassName = null; if (StringUtils.hasText(this.url)) { - driverClassName = this.driverClassNameProvider.getDriverClassName(this.url); + driverClassName = DatabaseDriver.fromJdbcUrl(this.url).getDriverClassName(); } if (!StringUtils.hasText(driverClassName)) { @@ -113,7 +116,7 @@ public class DataSourceProperties implements BeanClassLoaderAware, InitializingB "Cannot determine embedded database url for database type " + this.embeddedDatabaseConnection + ". If you want an embedded " - + "database please put a supported on on the classpath."); + + "database please put a supported one on the classpath."); } return url; } @@ -228,4 +231,39 @@ public class DataSourceProperties implements BeanClassLoaderAware, InitializingB return this.classLoader; } + public Xa getXa() { + return this.xa; + } + + public void setXa(Xa xa) { + this.xa = xa; + } + + /** + * XA Specific datasource settings. + */ + public static class Xa { + + private String dataSourceClassName; + + private Map properties = new LinkedHashMap(); + + public String getDataSourceClassName() { + return this.dataSourceClassName; + } + + public void setDataSourceClassName(String dataSourceClassName) { + this.dataSourceClassName = dataSourceClassName; + } + + public Map getProperties() { + return this.properties; + } + + public void setProperties(Map properties) { + this.properties = properties; + } + + } + } diff --git a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jdbc/DatabaseDriver.java b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jdbc/DatabaseDriver.java new file mode 100644 index 0000000000..026607762f --- /dev/null +++ b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jdbc/DatabaseDriver.java @@ -0,0 +1,137 @@ +/* + * 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.autoconfigure.jdbc; + +import org.springframework.util.Assert; +import org.springframework.util.StringUtils; + +/** + * Enumeration of common database drivers. + * + * @author Phillip Webb + * @author Maciej Walkowiak + * @since 1.2.0 + */ +enum DatabaseDriver { + + /** + * Unknown type. + */ + UNKNOWN(null), + + /** + * Apache Derby. + */ + DERBY("org.apache.derby.jdbc.EmbeddedDriver"), + + /** + * H2. + */ + H2("org.h2.Driver", "org.h2.jdbcx.JdbcDataSource"), + + /** + * HyperSQL DataBase. + */ + HSQLDB("org.hsqldb.jdbc.JDBCDriver", "org.hsqldb.jdbc.pool.JDBCXADataSource"), + + /** + * SQL Lite. + */ + SQLITE("org.sqlite.JDBC"), + + /** + * MySQL. + */ + MYSQL("com.mysql.jdbc.Driver", "org.mysql.jdbc.MySQLDataSource"), + + /** + * Maria DB. + */ + MARIADB("org.mariadb.jdbc.Driver", "org.mariadb.jdbc.MySQLDataSource"), + + /** + * Google App Engine. + */ + GOOGLE("com.google.appengine.api.rdbms.AppEngineDriver"), + + /** + * Oracle + */ + ORACLE("oracle.jdbc.OracleDriver", "oracle.jdbc.xa.OracleXADataSource"), + + /** + * Postres + */ + POSTGRESQL("org.postgresql.Driver", "org.postgresql.xa.PGXADataSource"), + + /** + * JTDS + */ + JTDS("net.sourceforge.jtds.jdbc.Driver"), + + /** + * SQL Server + */ + SQLSERVER("com.microsoft.sqlserver.jdbc.SQLServerDriver"); + + private final String driverClassName; + + private final String xaDataSourceClassName; + + private DatabaseDriver(String driverClassName) { + this(driverClassName, null); + } + + private DatabaseDriver(String driverClassName, String xaDataSourceClassName) { + this.driverClassName = driverClassName; + this.xaDataSourceClassName = xaDataSourceClassName; + } + + /** + * @return the driverClassName or {@code null} + */ + public String getDriverClassName() { + return this.driverClassName; + } + + /** + * @return the xaDataSourceClassName or {@code null} + */ + public String getXaDataSourceClassName() { + return this.xaDataSourceClassName; + } + + /** + * Find a {@link DatabaseDriver} for the given URL. + * @param url JDBC URL + * @return driver class name or {@link #UNKNOWN} if not found + */ + public static DatabaseDriver fromJdbcUrl(String url) { + if (StringUtils.hasLength(url)) { + Assert.isTrue(url.startsWith("jdbc"), "URL must start with 'jdbc'"); + String urlWithoutPrefix = url.substring("jdbc".length()).toLowerCase(); + for (DatabaseDriver driver : values()) { + String prefix = ":" + driver.name().toLowerCase() + ":"; + if (driver != UNKNOWN && urlWithoutPrefix.startsWith(prefix)) { + return driver; + } + } + } + return UNKNOWN; + } + +} diff --git a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jdbc/JndiDataSourceAutoConfiguration.java b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jdbc/JndiDataSourceAutoConfiguration.java index 735ab89357..2308938aea 100644 --- a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jdbc/JndiDataSourceAutoConfiguration.java +++ b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jdbc/JndiDataSourceAutoConfiguration.java @@ -36,7 +36,8 @@ import org.springframework.jdbc.datasource.lookup.JndiDataSourceLookup; * @since 1.2.0 */ @Configuration -@AutoConfigureBefore(DataSourceAutoConfiguration.class) +@AutoConfigureBefore({ XADataSourceAutoConfiguration.class, + DataSourceAutoConfiguration.class }) @ConditionalOnClass(DataSource.class) @ConditionalOnProperty(prefix = DataSourceProperties.PREFIX, name = "jndi-name") @EnableConfigurationProperties(DataSourceProperties.class) diff --git a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jdbc/XADataSourceAutoConfiguration.java b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jdbc/XADataSourceAutoConfiguration.java new file mode 100644 index 0000000000..1f5c00b844 --- /dev/null +++ b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jdbc/XADataSourceAutoConfiguration.java @@ -0,0 +1,119 @@ +/* + * 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.autoconfigure.jdbc; + +import javax.sql.DataSource; +import javax.sql.XADataSource; +import javax.transaction.TransactionManager; + +import org.springframework.beans.BeanUtils; +import org.springframework.beans.MutablePropertyValues; +import org.springframework.beans.factory.BeanClassLoaderAware; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.AutoConfigureAfter; +import org.springframework.boot.autoconfigure.AutoConfigureBefore; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.jta.JtaAutoConfiguration; +import org.springframework.boot.bind.RelaxedDataBinder; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.boot.jta.XADataSourceWrapper; +import org.springframework.context.annotation.Bean; +import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; +import org.springframework.util.StringUtils; + +/** + * {@link EnableAutoConfiguration Auto-configuration} for {@link DataSource} with XA. + * + * @author Phillip Webb + * @author Josh Long + * @since 1.2.0 + */ +@AutoConfigureBefore(DataSourceAutoConfiguration.class) +@AutoConfigureAfter(JtaAutoConfiguration.class) +@EnableConfigurationProperties(DataSourceProperties.class) +@ConditionalOnClass({ DataSource.class, TransactionManager.class }) +@ConditionalOnBean(XADataSourceWrapper.class) +@ConditionalOnMissingBean(DataSource.class) +public class XADataSourceAutoConfiguration implements BeanClassLoaderAware { + + @Autowired + private XADataSourceWrapper wrapper; + + @Autowired + private DataSourceProperties properties; + + @Autowired(required = false) + private XADataSource xaDataSource; + + private ClassLoader classLoader; + + @Bean + @ConfigurationProperties(prefix = DataSourceProperties.PREFIX) + public DataSource dataSource() throws Exception { + XADataSource xaDataSource = this.xaDataSource; + if (xaDataSource == null) { + xaDataSource = createXaDataSource(); + } + return this.wrapper.wrapDataSource(xaDataSource); + } + + @Override + public void setBeanClassLoader(ClassLoader classLoader) { + this.classLoader = classLoader; + } + + private XADataSource createXaDataSource() { + String className = this.properties.getXa().getDataSourceClassName(); + if (!StringUtils.hasLength(className)) { + className = DatabaseDriver.fromJdbcUrl(this.properties.getUrl()) + .getXaDataSourceClassName(); + } + Assert.state(StringUtils.hasLength(className), + "No XA DataSource class name specified"); + XADataSource dataSource = createXaDataSourceInstance(className); + bindXaProperties(dataSource, this.properties); + return dataSource; + } + + private XADataSource createXaDataSourceInstance(String className) { + try { + Class dataSourceClass = ClassUtils.forName(className, this.classLoader); + Object instance = BeanUtils.instantiate(dataSourceClass); + Assert.isInstanceOf(XADataSource.class, instance); + return (XADataSource) instance; + } + catch (Exception ex) { + throw new IllegalStateException( + "Unable to create XADataSource instance from '" + className + "'"); + } + } + + private void bindXaProperties(XADataSource target, DataSourceProperties properties) { + MutablePropertyValues values = new MutablePropertyValues(); + values.add("user", this.properties.getUsername()); + values.add("password", this.properties.getPassword()); + values.add("url", this.properties.getUrl()); + values.addPropertyValues(properties.getXa().getProperties()); + new RelaxedDataBinder(target).withAlias("user", "username").bind(values); + } + +} diff --git a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jms/JmsAnnotationDrivenConfiguration.java b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jms/JmsAnnotationDrivenConfiguration.java index 92cc4599f2..2feb63e04c 100644 --- a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jms/JmsAnnotationDrivenConfiguration.java +++ b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jms/JmsAnnotationDrivenConfiguration.java @@ -20,6 +20,7 @@ import javax.jms.ConnectionFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnJndi; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -29,6 +30,7 @@ import org.springframework.jms.annotation.JmsBootstrapConfiguration; import org.springframework.jms.config.DefaultJmsListenerContainerFactory; import org.springframework.jms.config.JmsListenerConfigUtils; import org.springframework.jms.support.destination.DestinationResolver; +import org.springframework.jms.support.destination.JndiDestinationResolver; import org.springframework.transaction.PlatformTransactionManager; /** @@ -67,6 +69,18 @@ class JmsAnnotationDrivenConfiguration { @EnableJms @ConditionalOnMissingBean(name = JmsListenerConfigUtils.JMS_LISTENER_ANNOTATION_PROCESSOR_BEAN_NAME) protected static class EnableJmsConfiguration { + } + + @ConditionalOnJndi + protected static class JndiConfiguration { + + @Bean + @ConditionalOnMissingBean + public DestinationResolver destinationResolver() { + JndiDestinationResolver resolver = new JndiDestinationResolver(); + resolver.setFallbackToDynamicDestination(true); + return resolver; + } } diff --git a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jms/JndiConnectionFactoryAutoConfiguration.java b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jms/JndiConnectionFactoryAutoConfiguration.java new file mode 100644 index 0000000000..9bb79bc389 --- /dev/null +++ b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jms/JndiConnectionFactoryAutoConfiguration.java @@ -0,0 +1,47 @@ +/* + * 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.autoconfigure.jms; + +import javax.jms.ConnectionFactory; +import javax.naming.NamingException; + +import org.springframework.boot.autoconfigure.AutoConfigureBefore; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.autoconfigure.condition.ConditionalOnJndi; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.jndi.JndiLocatorDelegate; + +/** + * {@link EnableAutoConfiguration Auto-configuration} for JMS provided from JNDI. + * + * @author Phillip Webb + * @since 1.2.0 + */ +@Configuration +@AutoConfigureBefore(JmsAutoConfiguration.class) +@ConditionalOnMissingBean(ConnectionFactory.class) +@ConditionalOnJndi("java:/JmsXA") +public class JndiConnectionFactoryAutoConfiguration { + + @Bean + public ConnectionFactory connectionFactory() throws NamingException { + return new JndiLocatorDelegate().lookup("java:/JmsXA", ConnectionFactory.class); + } + +} diff --git a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jms/activemq/ActiveMQAutoConfiguration.java b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jms/activemq/ActiveMQAutoConfiguration.java index d1f2b633d4..61fa5accac 100644 --- a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jms/activemq/ActiveMQAutoConfiguration.java +++ b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jms/activemq/ActiveMQAutoConfiguration.java @@ -19,19 +19,16 @@ package org.springframework.boot.autoconfigure.jms.activemq; import javax.jms.ConnectionFactory; import org.apache.activemq.ActiveMQConnectionFactory; -import org.apache.activemq.transport.vm.VMTransportFactory; +import org.springframework.boot.autoconfigure.AutoConfigureAfter; import org.springframework.boot.autoconfigure.AutoConfigureBefore; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; -import org.springframework.boot.autoconfigure.condition.ConditionOutcome; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; -import org.springframework.boot.autoconfigure.condition.SpringBootCondition; import org.springframework.boot.autoconfigure.jms.JmsAutoConfiguration; -import org.springframework.context.annotation.ConditionContext; -import org.springframework.context.annotation.Conditional; +import org.springframework.boot.autoconfigure.jta.JtaAutoConfiguration; +import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Import; -import org.springframework.core.type.AnnotatedTypeMetadata; /** * {@link EnableAutoConfiguration Auto-configuration} to integrate with an ActiveMQ @@ -39,67 +36,16 @@ import org.springframework.core.type.AnnotatedTypeMetadata; * embedded broker. * * @author Stephane Nicoll + * @author Phillip Webb * @since 1.1.0 */ @Configuration @AutoConfigureBefore(JmsAutoConfiguration.class) +@AutoConfigureAfter(JtaAutoConfiguration.class) @ConditionalOnClass({ ConnectionFactory.class, ActiveMQConnectionFactory.class }) @ConditionalOnMissingBean(ConnectionFactory.class) +@EnableConfigurationProperties(ActiveMQProperties.class) +@Import({ ActiveMQXAConnectionFactoryConfiguration.class, ActiveMQConnectionFactoryConfiguration.class }) public class ActiveMQAutoConfiguration { - @Configuration - @ConditionalOnClass(VMTransportFactory.class) - @Conditional(EmbeddedBrokerCondition.class) - @Import(ActiveMQConnectionFactoryConfiguration.class) - protected static class EmbeddedBroker { - } - - @Configuration - @Conditional(NonEmbeddedBrokerCondition.class) - @Import(ActiveMQConnectionFactoryConfiguration.class) - protected static class NetworkBroker { - } - - static abstract class BrokerTypeCondition extends SpringBootCondition { - private final boolean embedded; - - BrokerTypeCondition(boolean embedded) { - this.embedded = embedded; - } - - @Override - public ConditionOutcome getMatchOutcome(ConditionContext context, - AnnotatedTypeMetadata metadata) { - String brokerUrl = ActiveMQProperties.determineBrokerUrl(context - .getEnvironment()); - boolean match = brokerUrl.contains("vm://"); - boolean outcome = (match == this.embedded); - return new ConditionOutcome(outcome, buildMessage(brokerUrl, outcome)); - } - - protected String buildMessage(String brokerUrl, boolean outcome) { - String brokerType = this.embedded ? "Embedded" : "Network"; - String detected = outcome ? "detected" : "not detected"; - return brokerType + " ActiveMQ broker " + detected + " - brokerUrl '" - + brokerUrl + "'"; - } - - } - - static class EmbeddedBrokerCondition extends BrokerTypeCondition { - - EmbeddedBrokerCondition() { - super(true); - } - - } - - static class NonEmbeddedBrokerCondition extends BrokerTypeCondition { - - NonEmbeddedBrokerCondition() { - super(false); - } - - } - } diff --git a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jms/activemq/ActiveMQConnectionFactoryConfiguration.java b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jms/activemq/ActiveMQConnectionFactoryConfiguration.java index 65006e61c3..8e2c4ff9b4 100644 --- a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jms/activemq/ActiveMQConnectionFactoryConfiguration.java +++ b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jms/activemq/ActiveMQConnectionFactoryConfiguration.java @@ -18,27 +18,34 @@ package org.springframework.boot.autoconfigure.jms.activemq; import javax.jms.ConnectionFactory; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.apache.activemq.ActiveMQConnectionFactory; +import org.apache.activemq.pool.PooledConnectionFactory; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; /** - * Creates a {@link ConnectionFactory} based on {@link ActiveMQProperties}. + * Configuration for ActiveMQ {@link ConnectionFactory}. * * @author Greg Turnquist * @author Stephane Nicoll + * @author Phillip Webb * @since 1.1.0 */ @Configuration -@EnableConfigurationProperties(ActiveMQProperties.class) +@ConditionalOnMissingBean(ConnectionFactory.class) class ActiveMQConnectionFactoryConfiguration { - @Autowired - private ActiveMQProperties properties; - @Bean - public ConnectionFactory jmsConnectionFactory() { - return this.properties.createConnectionFactory(); + public ConnectionFactory jmsConnectionFactory(ActiveMQProperties properties) { + ActiveMQConnectionFactory connectionFactory = new ActiveMQConnectionFactoryFactory( + properties).createConnectionFactory(ActiveMQConnectionFactory.class); + if (properties.isPooled()) { + PooledConnectionFactory pool = new PooledConnectionFactory(); + pool.setConnectionFactory(connectionFactory); + return pool; + } + return connectionFactory; } + } diff --git a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jms/activemq/ActiveMQConnectionFactoryFactory.java b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jms/activemq/ActiveMQConnectionFactoryFactory.java new file mode 100644 index 0000000000..6d3f82074b --- /dev/null +++ b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jms/activemq/ActiveMQConnectionFactoryFactory.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.autoconfigure.jms.activemq; + +import org.apache.activemq.ActiveMQConnectionFactory; +import org.springframework.util.Assert; +import org.springframework.util.StringUtils; + +/** + * Factory to create a {@link ActiveMQConnectionFactory} instance from properties defined + * in {@link ActiveMQProperties}. + * + * @author Phillip Webb + * @since 1.2.0 + */ +class ActiveMQConnectionFactoryFactory { + + private static final String DEFAULT_EMBEDDED_BROKER_URL = "vm://localhost?broker.persistent=false"; + + private static final String DEFAULT_NETWORK_BROKER_URL = "tcp://localhost:61616"; + + private final ActiveMQProperties properties; + + public ActiveMQConnectionFactoryFactory(ActiveMQProperties properties) { + Assert.notNull(properties, "Properties must not be null"); + this.properties = properties; + } + + public T createConnectionFactory( + Class factoryClass) { + try { + return doCreateConnectionFactory(factoryClass); + } + catch (Exception ex) { + throw new IllegalStateException("Unable to create " + + "ActiveMQConnectionFactory", ex); + } + } + + private T doCreateConnectionFactory( + Class factoryClass) throws Exception { + String brokerUrl = determineBrokerUrl(); + String user = this.properties.getUser(); + String password = this.properties.getPassword(); + if (StringUtils.hasLength(user) && StringUtils.hasLength(password)) { + return factoryClass.getConstructor(String.class, String.class, String.class) + .newInstance(user, password, brokerUrl); + } + return factoryClass.getConstructor(String.class).newInstance(brokerUrl); + } + + String determineBrokerUrl() { + if (this.properties.getBrokerUrl() != null) { + return this.properties.getBrokerUrl(); + } + if (this.properties.isInMemory()) { + return DEFAULT_EMBEDDED_BROKER_URL; + } + return DEFAULT_NETWORK_BROKER_URL; + } + +} diff --git a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jms/activemq/ActiveMQProperties.java b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jms/activemq/ActiveMQProperties.java index de3db1caf2..0709c08668 100644 --- a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jms/activemq/ActiveMQProperties.java +++ b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jms/activemq/ActiveMQProperties.java @@ -16,15 +16,7 @@ package org.springframework.boot.autoconfigure.jms.activemq; -import javax.jms.ConnectionFactory; - -import org.apache.activemq.ActiveMQConnectionFactory; -import org.apache.activemq.pool.PooledConnectionFactory; -import org.springframework.boot.bind.RelaxedPropertyResolver; import org.springframework.boot.context.properties.ConfigurationProperties; -import org.springframework.core.env.Environment; -import org.springframework.core.env.PropertyResolver; -import org.springframework.util.StringUtils; /** * Configuration properties for ActiveMQ @@ -35,10 +27,6 @@ import org.springframework.util.StringUtils; @ConfigurationProperties(prefix = "spring.activemq") public class ActiveMQProperties { - public static final String DEFAULT_EMBEDDED_BROKER_URL = "vm://localhost?broker.persistent=false"; - - public static final String DEFAULT_NETWORK_BROKER_URL = "tcp://localhost:61616"; - private String brokerUrl; private boolean inMemory = true; @@ -93,52 +81,4 @@ public class ActiveMQProperties { this.password = password; } - /** - * Return a new {@link ConnectionFactory} from these properties. - */ - public ConnectionFactory createConnectionFactory() { - ConnectionFactory connectionFactory = createActiveMQConnectionFactory(); - if (isPooled()) { - PooledConnectionFactory pool = new PooledConnectionFactory(); - pool.setConnectionFactory(connectionFactory); - return pool; - } - return connectionFactory; - } - - private ConnectionFactory createActiveMQConnectionFactory() { - String brokerUrl = determineBrokerUrl(); - if (StringUtils.hasLength(this.user) && StringUtils.hasLength(this.password)) { - return new ActiveMQConnectionFactory(this.user, this.password, brokerUrl); - } - return new ActiveMQConnectionFactory(brokerUrl); - } - - String determineBrokerUrl() { - return determineBrokerUrl(this.brokerUrl, this.inMemory); - } - - /** - * Determine the broker url to use for the specified {@link Environment}. If no broker - * url is specified through configuration, a default broker is provided, that is - * {@value #DEFAULT_EMBEDDED_BROKER_URL} if the {@code inMemory} flag is {@code null} - * or {@code true}, {@value #DEFAULT_NETWORK_BROKER_URL} otherwise. - * @param environment the environment to extract configuration from - * @return the broker url to use - */ - public static String determineBrokerUrl(Environment environment) { - PropertyResolver resolver = new RelaxedPropertyResolver(environment, - "spring.activemq."); - String brokerUrl = resolver.getProperty("brokerUrl"); - Boolean inMemory = resolver.getProperty("inMemory", Boolean.class); - return determineBrokerUrl(brokerUrl, inMemory); - } - - private static String determineBrokerUrl(String brokerUrl, Boolean inMemory) { - if (brokerUrl != null) { - return brokerUrl; - } - boolean embedded = inMemory == null || inMemory; - return (embedded ? DEFAULT_EMBEDDED_BROKER_URL : DEFAULT_NETWORK_BROKER_URL); - } } diff --git a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jms/activemq/ActiveMQXAConnectionFactoryConfiguration.java b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jms/activemq/ActiveMQXAConnectionFactoryConfiguration.java new file mode 100644 index 0000000000..a30fd80e61 --- /dev/null +++ b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jms/activemq/ActiveMQXAConnectionFactoryConfiguration.java @@ -0,0 +1,50 @@ +/* + * 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.autoconfigure.jms.activemq; + +import javax.jms.ConnectionFactory; +import javax.transaction.TransactionManager; + +import org.apache.activemq.ActiveMQXAConnectionFactory; +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.jta.XAConnectionFactoryWrapper; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * Configuration for ActiveMQ XA {@link ConnectionFactory}. + * + * @author Phillip Webb + * @since 1.2.0 + */ +@Configuration +@ConditionalOnClass(TransactionManager.class) +@ConditionalOnBean(XAConnectionFactoryWrapper.class) +@ConditionalOnMissingBean(ConnectionFactory.class) +class ActiveMQXAConnectionFactoryConfiguration { + + @Bean + public ConnectionFactory jmsConnectionFactory(ActiveMQProperties properties, + XAConnectionFactoryWrapper wrapper) throws Exception { + ActiveMQXAConnectionFactory connectionFactory = new ActiveMQConnectionFactoryFactory( + properties).createConnectionFactory(ActiveMQXAConnectionFactory.class); + return wrapper.wrapConnectionFactory(connectionFactory); + } + +} diff --git a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jms/hornetq/HornetQAutoConfiguration.java b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jms/hornetq/HornetQAutoConfiguration.java index 7db9103fa3..8e86dea97a 100644 --- a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jms/hornetq/HornetQAutoConfiguration.java +++ b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jms/hornetq/HornetQAutoConfiguration.java @@ -16,48 +16,25 @@ package org.springframework.boot.autoconfigure.jms.hornetq; -import java.util.Collection; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - import javax.jms.ConnectionFactory; -import org.hornetq.api.core.TransportConfiguration; -import org.hornetq.api.core.client.HornetQClient; -import org.hornetq.api.core.client.ServerLocator; import org.hornetq.api.jms.HornetQJMSClient; -import org.hornetq.api.jms.JMSFactoryType; -import org.hornetq.core.remoting.impl.invm.InVMConnectorFactory; -import org.hornetq.core.remoting.impl.netty.NettyConnectorFactory; -import org.hornetq.core.remoting.impl.netty.TransportConstants; -import org.hornetq.jms.client.HornetQConnectionFactory; -import org.hornetq.jms.server.config.JMSConfiguration; -import org.hornetq.jms.server.config.JMSQueueConfiguration; -import org.hornetq.jms.server.config.TopicConfiguration; -import org.hornetq.jms.server.config.impl.JMSConfigurationImpl; -import org.hornetq.jms.server.config.impl.JMSQueueConfigurationImpl; -import org.hornetq.jms.server.config.impl.TopicConfigurationImpl; -import org.hornetq.jms.server.embedded.EmbeddedJMS; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.autoconfigure.AutoConfigureBefore; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; -import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression; -import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.autoconfigure.jms.JmsAutoConfiguration; import org.springframework.boot.context.properties.EnableConfigurationProperties; -import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; -import org.springframework.core.annotation.AnnotationAwareOrderComparator; -import org.springframework.util.ClassUtils; +import org.springframework.context.annotation.Import; /** - * {@link org.springframework.boot.autoconfigure.EnableAutoConfiguration - * Auto-configuration} to integrate with an HornetQ broker. If the necessary classes are - * present, embed the broker in the application by default. Otherwise, connect to a broker - * available on the local machine with the default settings. + * {@link EnableAutoConfiguration Auto-configuration} to integrate with an HornetQ broker. + * If the necessary classes are present, embed the broker in the application by default. + * Otherwise, connect to a broker available on the local machine with the default + * settings. * * @author Stephane Nicoll + * @author Phillip Webb * @since 1.1.0 * @see HornetQProperties */ @@ -65,151 +42,9 @@ import org.springframework.util.ClassUtils; @AutoConfigureBefore(JmsAutoConfiguration.class) @ConditionalOnClass({ ConnectionFactory.class, HornetQJMSClient.class }) @EnableConfigurationProperties(HornetQProperties.class) +@Import({ HornetQEmbeddedServerConfiguration.class, + HornetQXAConnectionFactoryConfiguration.class, + HornetQConnectionFactoryConfiguration.class }) public class HornetQAutoConfiguration { - private static final String EMBEDDED_JMS_CLASS = "org.hornetq.jms.server.embedded.EmbeddedJMS"; - - @Autowired - private HornetQProperties properties; - - /** - * Create the {@link ConnectionFactory} to use if none is provided. If no - * {@linkplain HornetQProperties#getMode() mode} has been explicitly set, start an - * embedded server unless it has been explicitly disabled, connect to a broker - * available on the local machine with the default settings otherwise. - */ - @Bean - @ConditionalOnMissingBean - public ConnectionFactory jmsConnectionFactory() { - HornetQMode mode = this.properties.getMode(); - if (mode == null) { - mode = deduceMode(); - } - if (mode == HornetQMode.EMBEDDED) { - return createEmbeddedConnectionFactory(); - } - return createNativeConnectionFactory(); - } - - /** - * Deduce the {@link HornetQMode} to use if none has been set. - */ - private HornetQMode deduceMode() { - if (this.properties.getEmbedded().isEnabled() - && ClassUtils.isPresent(EMBEDDED_JMS_CLASS, null)) { - return HornetQMode.EMBEDDED; - } - return HornetQMode.NATIVE; - } - - private ConnectionFactory createEmbeddedConnectionFactory() { - try { - TransportConfiguration transportConfiguration = new TransportConfiguration( - InVMConnectorFactory.class.getName(), this.properties.getEmbedded() - .generateTransportParameters()); - ServerLocator serviceLocator = HornetQClient - .createServerLocatorWithoutHA(transportConfiguration); - return new HornetQConnectionFactory(serviceLocator); - } - catch (NoClassDefFoundError ex) { - throw new IllegalStateException("Unable to create InVM " - + "HornetQ connection, ensure that hornet-jms-server.jar " - + "is in the classpath", ex); - } - } - - private ConnectionFactory createNativeConnectionFactory() { - Map params = new HashMap(); - params.put(TransportConstants.HOST_PROP_NAME, this.properties.getHost()); - params.put(TransportConstants.PORT_PROP_NAME, this.properties.getPort()); - TransportConfiguration transportConfiguration = new TransportConfiguration( - NettyConnectorFactory.class.getName(), params); - return HornetQJMSClient.createConnectionFactoryWithoutHA(JMSFactoryType.CF, - transportConfiguration); - } - - /** - * Configuration used to create the embedded HornetQ server. - */ - @Configuration - @ConditionalOnClass(name = EMBEDDED_JMS_CLASS) - @ConditionalOnExpression("${spring.hornetq.embedded.enabled:true}") - static class EmbeddedServerConfiguration { - - @Autowired - private HornetQProperties properties; - - @Autowired(required = false) - private List configurationCustomizers; - - @Autowired(required = false) - private List queuesConfiguration; - - @Autowired(required = false) - private List topicsConfiguration; - - @Bean - @ConditionalOnMissingBean - public org.hornetq.core.config.Configuration hornetQConfiguration() { - return new HornetQEmbeddedConfigurationFactory(this.properties) - .createConfiguration(); - } - - @Bean(initMethod = "start", destroyMethod = "stop") - @ConditionalOnMissingBean - public EmbeddedJMS hornetQServer( - org.hornetq.core.config.Configuration configuration, - JMSConfiguration jmsConfiguration) { - EmbeddedJMS server = new EmbeddedJMS(); - customize(configuration); - server.setConfiguration(configuration); - server.setJmsConfiguration(jmsConfiguration); - server.setRegistry(new HornetQNoOpBindingRegistry()); - return server; - } - - private void customize(org.hornetq.core.config.Configuration configuration) { - if (this.configurationCustomizers != null) { - AnnotationAwareOrderComparator.sort(this.configurationCustomizers); - for (HornetQConfigurationCustomizer customizer : this.configurationCustomizers) { - customizer.customize(configuration); - } - } - } - - @Bean - @ConditionalOnMissingBean - public JMSConfiguration hornetQJmsConfiguration() { - JMSConfiguration configuration = new JMSConfigurationImpl(); - addAll(configuration.getQueueConfigurations(), this.queuesConfiguration); - addAll(configuration.getTopicConfigurations(), this.topicsConfiguration); - addQueues(configuration, this.properties.getEmbedded().getQueues()); - addTopics(configuration, this.properties.getEmbedded().getTopics()); - return configuration; - } - - private void addAll(List list, Collection items) { - if (items != null) { - list.addAll(items); - } - } - - private void addQueues(JMSConfiguration configuration, String[] queues) { - boolean persistent = this.properties.getEmbedded().isPersistent(); - for (String queue : queues) { - configuration.getQueueConfigurations().add( - new JMSQueueConfigurationImpl(queue, null, persistent, "/queue/" - + queue)); - } - } - - private void addTopics(JMSConfiguration configuration, String[] topics) { - for (String topic : topics) { - configuration.getTopicConfigurations().add( - new TopicConfigurationImpl(topic, "/topic/" + topic)); - } - } - - } - } diff --git a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jms/hornetq/HornetQConnectionFactoryConfiguration.java b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jms/hornetq/HornetQConnectionFactoryConfiguration.java new file mode 100644 index 0000000000..64e361c30d --- /dev/null +++ b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jms/hornetq/HornetQConnectionFactoryConfiguration.java @@ -0,0 +1,56 @@ +/* + * 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.autoconfigure.jms.hornetq; + +import javax.jms.ConnectionFactory; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.hornetq.jms.client.HornetQConnectionFactory; +import org.hornetq.jms.server.embedded.EmbeddedJMS; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * Configuration for HornetQ {@link ConnectionFactory}. + * + * @author Phillip Webb + * @since 1.2.0 + */ +@Configuration +@ConditionalOnMissingBean(ConnectionFactory.class) +class HornetQConnectionFactoryConfiguration { + + private static Log logger = LogFactory + .getLog(HornetQEmbeddedServerConfiguration.class); + + // Ensure JMS is setup before XA + @Autowired(required = false) + private EmbeddedJMS embeddedJMS; + + @Bean + public ConnectionFactory jmsConnectionFactory(HornetQProperties properties) { + if (this.embeddedJMS != null && logger.isDebugEnabled()) { + logger.debug("Using embdedded HornetQ broker"); + } + return new HornetQConnectionFactoryFactory(properties) + .createConnectionFactory(HornetQConnectionFactory.class); + } + +} diff --git a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jms/hornetq/HornetQConnectionFactoryFactory.java b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jms/hornetq/HornetQConnectionFactoryFactory.java new file mode 100644 index 0000000000..496c73536a --- /dev/null +++ b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jms/hornetq/HornetQConnectionFactoryFactory.java @@ -0,0 +1,117 @@ +/* + * 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.autoconfigure.jms.hornetq; + +import java.lang.reflect.Constructor; +import java.util.HashMap; +import java.util.Map; + +import org.hornetq.api.core.TransportConfiguration; +import org.hornetq.api.core.client.HornetQClient; +import org.hornetq.api.core.client.ServerLocator; +import org.hornetq.core.remoting.impl.invm.InVMConnectorFactory; +import org.hornetq.core.remoting.impl.netty.NettyConnectorFactory; +import org.hornetq.core.remoting.impl.netty.TransportConstants; +import org.hornetq.jms.client.HornetQConnectionFactory; +import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; + +/** + * Factory to create a {@link HornetQConnectionFactory} instance from properties defined + * in {@link HornetQProperties}. + * + * @author Phillip Webb + * @author Stephane Nicoll + * @since 1.2.0 + */ +class HornetQConnectionFactoryFactory { + + static final String EMBEDDED_JMS_CLASS = "org.hornetq.jms.server.embedded.EmbeddedJMS"; + + private final HornetQProperties properties; + + public HornetQConnectionFactoryFactory(HornetQProperties properties) { + Assert.notNull(properties, "Properties must not be null"); + this.properties = properties; + } + + public T createConnectionFactory( + Class factoryClass) { + try { + return doCreateConnectionFactory(factoryClass); + } + catch (Exception ex) { + throw new IllegalStateException("Unable to create " + + "HornetQConnectionFactory", ex); + } + } + + private T doCreateConnectionFactory( + Class factoryClass) throws Exception { + HornetQMode mode = this.properties.getMode(); + if (mode == null) { + mode = deduceMode(); + } + if (mode == HornetQMode.EMBEDDED) { + return createEmbeddedConnectionFactory(factoryClass); + } + return createNativeConnectionFactory(factoryClass); + } + + /** + * Deduce the {@link HornetQMode} to use if none has been set. + */ + private HornetQMode deduceMode() { + if (this.properties.getEmbedded().isEnabled() + && ClassUtils.isPresent(EMBEDDED_JMS_CLASS, null)) { + return HornetQMode.EMBEDDED; + } + return HornetQMode.NATIVE; + } + + private T createEmbeddedConnectionFactory( + Class factoryClass) throws Exception { + try { + TransportConfiguration transportConfiguration = new TransportConfiguration( + InVMConnectorFactory.class.getName(), this.properties.getEmbedded() + .generateTransportParameters()); + ServerLocator serviceLocator = HornetQClient + .createServerLocatorWithoutHA(transportConfiguration); + return factoryClass.getConstructor(ServerLocator.class).newInstance( + serviceLocator); + } + catch (NoClassDefFoundError ex) { + throw new IllegalStateException("Unable to create InVM " + + "HornetQ connection, ensure that hornet-jms-server.jar " + + "is in the classpath", ex); + } + } + + private T createNativeConnectionFactory( + Class factoryClass) throws Exception { + Map params = new HashMap(); + params.put(TransportConstants.HOST_PROP_NAME, this.properties.getHost()); + params.put(TransportConstants.PORT_PROP_NAME, this.properties.getPort()); + TransportConfiguration transportConfiguration = new TransportConfiguration( + NettyConnectorFactory.class.getName(), params); + Constructor constructor = factoryClass.getConstructor(boolean.class, + TransportConfiguration[].class); + return constructor.newInstance(false, + new TransportConfiguration[] { transportConfiguration }); + } + +} diff --git a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jms/hornetq/HornetQEmbeddedServerConfiguration.java b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jms/hornetq/HornetQEmbeddedServerConfiguration.java new file mode 100644 index 0000000000..2a82e9b1df --- /dev/null +++ b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jms/hornetq/HornetQEmbeddedServerConfiguration.java @@ -0,0 +1,122 @@ +/* + * 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.autoconfigure.jms.hornetq; + +import java.util.Collection; +import java.util.List; + +import org.hornetq.jms.server.config.JMSConfiguration; +import org.hornetq.jms.server.config.JMSQueueConfiguration; +import org.hornetq.jms.server.config.TopicConfiguration; +import org.hornetq.jms.server.config.impl.JMSConfigurationImpl; +import org.hornetq.jms.server.config.impl.JMSQueueConfigurationImpl; +import org.hornetq.jms.server.config.impl.TopicConfigurationImpl; +import org.hornetq.jms.server.embedded.EmbeddedJMS; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.annotation.AnnotationAwareOrderComparator; + +/** + * Configuration used to create the embedded HornetQ server. + * + * @author Phillip Webb + * @author Stephane Nicoll + * @since 1.2.0 + */ +@Configuration +@ConditionalOnClass(name = HornetQConnectionFactoryFactory.EMBEDDED_JMS_CLASS) +@ConditionalOnProperty(prefix = "spring.hornetq.embedded", name = "enabled", havingValue = "true", matchIfMissing = true) +class HornetQEmbeddedServerConfiguration { + + @Autowired + private HornetQProperties properties; + + @Autowired(required = false) + private List configurationCustomizers; + + @Autowired(required = false) + private List queuesConfiguration; + + @Autowired(required = false) + private List topicsConfiguration; + + @Bean + @ConditionalOnMissingBean + public org.hornetq.core.config.Configuration hornetQConfiguration() { + return new HornetQEmbeddedConfigurationFactory(this.properties) + .createConfiguration(); + } + + @Bean(initMethod = "start", destroyMethod = "stop") + @ConditionalOnMissingBean + public EmbeddedJMS hornetQServer(org.hornetq.core.config.Configuration configuration, + JMSConfiguration jmsConfiguration) { + EmbeddedJMS server = new EmbeddedJMS(); + customize(configuration); + server.setConfiguration(configuration); + server.setJmsConfiguration(jmsConfiguration); + server.setRegistry(new HornetQNoOpBindingRegistry()); + return server; + } + + private void customize(org.hornetq.core.config.Configuration configuration) { + if (this.configurationCustomizers != null) { + AnnotationAwareOrderComparator.sort(this.configurationCustomizers); + for (HornetQConfigurationCustomizer customizer : this.configurationCustomizers) { + customizer.customize(configuration); + } + } + } + + @Bean + @ConditionalOnMissingBean + public JMSConfiguration hornetQJmsConfiguration() { + JMSConfiguration configuration = new JMSConfigurationImpl(); + addAll(configuration.getQueueConfigurations(), this.queuesConfiguration); + addAll(configuration.getTopicConfigurations(), this.topicsConfiguration); + addQueues(configuration, this.properties.getEmbedded().getQueues()); + addTopics(configuration, this.properties.getEmbedded().getTopics()); + return configuration; + } + + private void addAll(List list, Collection items) { + if (items != null) { + list.addAll(items); + } + } + + private void addQueues(JMSConfiguration configuration, String[] queues) { + boolean persistent = this.properties.getEmbedded().isPersistent(); + for (String queue : queues) { + configuration.getQueueConfigurations().add( + new JMSQueueConfigurationImpl(queue, null, persistent, "/queue/" + + queue)); + } + } + + private void addTopics(JMSConfiguration configuration, String[] topics) { + for (String topic : topics) { + configuration.getTopicConfigurations().add( + new TopicConfigurationImpl(topic, "/topic/" + topic)); + } + } + +} diff --git a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jms/hornetq/HornetQXAConnectionFactoryConfiguration.java b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jms/hornetq/HornetQXAConnectionFactoryConfiguration.java new file mode 100644 index 0000000000..ae7d5b1b6a --- /dev/null +++ b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jms/hornetq/HornetQXAConnectionFactoryConfiguration.java @@ -0,0 +1,63 @@ +/* + * 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.autoconfigure.jms.hornetq; + +import javax.jms.ConnectionFactory; +import javax.transaction.TransactionManager; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.hornetq.jms.client.HornetQXAConnectionFactory; +import org.hornetq.jms.server.embedded.EmbeddedJMS; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.jta.XAConnectionFactoryWrapper; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * Configuration for HornetQ XA {@link ConnectionFactory}. + * + * @author Phillip Webb + * @since 1.2.0 + */ +@Configuration +@ConditionalOnMissingBean(ConnectionFactory.class) +@ConditionalOnClass(TransactionManager.class) +@ConditionalOnBean(XAConnectionFactoryWrapper.class) +class HornetQXAConnectionFactoryConfiguration { + + private static Log logger = LogFactory + .getLog(HornetQEmbeddedServerConfiguration.class); + + // Ensure JMS is setup before XA + @Autowired(required = false) + private EmbeddedJMS embeddedJMS; + + @Bean + public ConnectionFactory jmsConnectionFactory(HornetQProperties properties, + XAConnectionFactoryWrapper wrapper) throws Exception { + if (this.embeddedJMS != null && logger.isDebugEnabled()) { + logger.debug("Using embdedded HornetQ broker with XA"); + } + return wrapper.wrapConnectionFactory(new HornetQConnectionFactoryFactory( + properties).createConnectionFactory(HornetQXAConnectionFactory.class)); + } + +} diff --git a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jta/AtomikosJtaConfiguration.java b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jta/AtomikosJtaConfiguration.java new file mode 100644 index 0000000000..65ee8083f5 --- /dev/null +++ b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jta/AtomikosJtaConfiguration.java @@ -0,0 +1,120 @@ +/* + * 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.autoconfigure.jta; + +import java.io.File; +import java.util.Properties; + +import javax.transaction.TransactionManager; +import javax.transaction.UserTransaction; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.ApplicationHome; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.boot.jta.XAConnectionFactoryWrapper; +import org.springframework.boot.jta.XADataSourceWrapper; +import org.springframework.boot.jta.atomikos.AtomikosDependsOnBeanFactoryPostProcessor; +import org.springframework.boot.jta.atomikos.AtomikosProperties; +import org.springframework.boot.jta.atomikos.AtomikosXAConnectionFactoryWrapper; +import org.springframework.boot.jta.atomikos.AtomikosXADataSourceWrapper; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.transaction.PlatformTransactionManager; +import org.springframework.transaction.jta.JtaTransactionManager; +import org.springframework.util.StringUtils; + +import com.atomikos.icatch.config.UserTransactionService; +import com.atomikos.icatch.config.UserTransactionServiceImp; +import com.atomikos.icatch.jta.UserTransactionManager; + +/** + * JTA Configuration for Atomikos. + * + * @author Josh Long + * @author Phillip Webb + * @since 1.2.0 + */ +@Configuration +@ConditionalOnClass(UserTransactionManager.class) +@ConditionalOnMissingBean(PlatformTransactionManager.class) +class AtomikosJtaConfiguration { + + @Autowired + private JtaProperties jtaProperties; + + @Bean + @ConditionalOnMissingBean + @ConfigurationProperties(prefix = JtaProperties.PREFIX) + public AtomikosProperties atomikosProperties() { + return new AtomikosProperties(); + } + + @Bean(initMethod = "init", destroyMethod = "shutdownForce") + @ConditionalOnMissingBean + public UserTransactionService userTransactionService( + AtomikosProperties atomikosProperties) { + Properties properties = new Properties(); + properties.setProperty("com.atomikos.icatch.log_base_dir", getLogBaseDir()); + properties.putAll(atomikosProperties.asProperties()); + return new UserTransactionServiceImp(properties); + } + + private String getLogBaseDir() { + if (StringUtils.hasLength(this.jtaProperties.getLogDir())) { + return this.jtaProperties.getLogDir(); + } + File home = new ApplicationHome().getDir(); + return new File(home, "transaction-logs").getAbsolutePath(); + } + + @Bean(initMethod = "init", destroyMethod = "close") + @ConditionalOnMissingBean + public UserTransactionManager atomikosTransactionManager( + UserTransactionService userTransactionService) throws Exception { + UserTransactionManager manager = new UserTransactionManager(); + manager.setStartupTransactionService(false); + manager.setForceShutdown(true); + return manager; + } + + @Bean + @ConditionalOnMissingBean + public XADataSourceWrapper xaDataSourceWrapper() { + return new AtomikosXADataSourceWrapper(); + } + + @Bean + @ConditionalOnMissingBean + public XAConnectionFactoryWrapper xaConnectionFactoryWrapper() { + return new AtomikosXAConnectionFactoryWrapper(); + } + + @Bean + @ConditionalOnMissingBean + public static AtomikosDependsOnBeanFactoryPostProcessor atomikosDependsOnBeanFactoryPostProcessor() { + return new AtomikosDependsOnBeanFactoryPostProcessor(); + } + + @Bean + public JtaTransactionManager transactionManager(UserTransaction userTransaction, + TransactionManager transactionManager) { + return new JtaTransactionManager(userTransaction, transactionManager); + } + +} diff --git a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jta/BitronixJtaConfiguration.java b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jta/BitronixJtaConfiguration.java new file mode 100644 index 0000000000..6a3407a5e4 --- /dev/null +++ b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jta/BitronixJtaConfiguration.java @@ -0,0 +1,109 @@ +/* + * 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.autoconfigure.jta; + +import java.io.File; + +import javax.transaction.TransactionManager; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.ApplicationHome; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.boot.jta.XAConnectionFactoryWrapper; +import org.springframework.boot.jta.XADataSourceWrapper; +import org.springframework.boot.jta.bitronix.BitronixDependentBeanFactoryPostProcessor; +import org.springframework.boot.jta.bitronix.BitronixXAConnectionFactoryWrapper; +import org.springframework.boot.jta.bitronix.BitronixXADataSourceWrapper; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.transaction.PlatformTransactionManager; +import org.springframework.transaction.jta.JtaTransactionManager; +import org.springframework.util.StringUtils; + +import bitronix.tm.TransactionManagerServices; +import bitronix.tm.jndi.BitronixContext; + +/** + * JTA Configuration for Bitronix. + * + * @author Josh Long + * @author Phillip Webb + * @since 1.2.0 + */ +@Configuration +@ConditionalOnClass(BitronixContext.class) +@ConditionalOnMissingBean(PlatformTransactionManager.class) +class BitronixJtaConfiguration { + + @Autowired + private JtaProperties jtaProperties; + + @Bean + @ConditionalOnMissingBean + @ConfigurationProperties(prefix = JtaProperties.PREFIX) + public bitronix.tm.Configuration bitronixConfiguration(JtaProperties xxx) { + bitronix.tm.Configuration config = TransactionManagerServices.getConfiguration(); + config.setServerId("spring-boot-jta-bitronix"); + File logBaseDir = getLogBaseDir(); + config.setLogPart1Filename(new File(logBaseDir, "part1.btm").getAbsolutePath()); + config.setLogPart2Filename(new File(logBaseDir, "part2.btm").getAbsolutePath()); + config.setDisableJmx(true); + return config; + } + + private File getLogBaseDir() { + if (StringUtils.hasLength(this.jtaProperties.getLogDir())) { + return new File(this.jtaProperties.getLogDir()); + } + File home = new ApplicationHome().getDir(); + return new File(home, "transaction-logs"); + } + + @Bean + @ConditionalOnMissingBean + public TransactionManager bitronixTransactionManager( + bitronix.tm.Configuration configuration) { + // Inject configuration to force ordering + return TransactionManagerServices.getTransactionManager(); + } + + @Bean + @ConditionalOnMissingBean + public XADataSourceWrapper xaDataSourceWrapper() { + return new BitronixXADataSourceWrapper(); + } + + @Bean + @ConditionalOnMissingBean + public XAConnectionFactoryWrapper xaConnectionFactoryWrapper() { + return new BitronixXAConnectionFactoryWrapper(); + } + + @Bean + @ConditionalOnMissingBean + public static BitronixDependentBeanFactoryPostProcessor atomikosDependsOnBeanFactoryPostProcessor() { + return new BitronixDependentBeanFactoryPostProcessor(); + } + + @Bean + public JtaTransactionManager transactionManager(TransactionManager transactionManager) { + return new JtaTransactionManager(transactionManager); + } + +} diff --git a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jta/JndiJtaConfiguration.java b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jta/JndiJtaConfiguration.java new file mode 100644 index 0000000000..92f7558942 --- /dev/null +++ b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jta/JndiJtaConfiguration.java @@ -0,0 +1,44 @@ +/* + * 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.autoconfigure.jta; + +import org.springframework.boot.autoconfigure.condition.ConditionalOnJndi; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.transaction.PlatformTransactionManager; +import org.springframework.transaction.jta.JtaTransactionManager; + +/** + * JTA Configuration for a JDNI managed {@link JtaTransactionManager}. + * + * @author Phillip Webb + * @since 1.2.0 + */ +@Configuration +@ConditionalOnJndi({ JtaTransactionManager.DEFAULT_USER_TRANSACTION_NAME, + "java:comp/TransactionManager", "java:appserver/TransactionManager", + "java:pm/TransactionManager", "java:/TransactionManager" }) +@ConditionalOnMissingBean(PlatformTransactionManager.class) +class JndiJtaConfiguration { + + @Bean + public JtaTransactionManager transactionManager() { + return new JtaTransactionManager(); + } + +} diff --git a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jta/JtaAutoConfiguration.java b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jta/JtaAutoConfiguration.java new file mode 100644 index 0000000000..4a0742d1bc --- /dev/null +++ b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jta/JtaAutoConfiguration.java @@ -0,0 +1,37 @@ +/* + * 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.autoconfigure.jta; + +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Import; + +/** + * {@link EnableAutoConfiguration Auto-configuration} for JTA. + * + * @author Josh Long + * @author Phillip Webb + * @since 1.2.0 + */ +@ConditionalOnClass(javax.transaction.Transaction.class) +@Import({ JndiJtaConfiguration.class, BitronixJtaConfiguration.class, + AtomikosJtaConfiguration.class }) +@EnableConfigurationProperties(JtaProperties.class) +public class JtaAutoConfiguration { + +} diff --git a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jta/JtaProperties.java b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jta/JtaProperties.java new file mode 100644 index 0000000000..02fda641e9 --- /dev/null +++ b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jta/JtaProperties.java @@ -0,0 +1,46 @@ +/* + * 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.autoconfigure.jta; + +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.transaction.jta.JtaTransactionManager; + +/** + * External configuration properties for a {@link JtaTransactionManager} created by + * Spring. All {@literal spring.jta.} properties are also applied to the appropriate + * vendor specific configuration. + * + * @author Josh Long + * @author Phillip Webb + * @since 1.2.0 + */ +@ConfigurationProperties(prefix = JtaProperties.PREFIX, ignoreUnknownFields = true) +public class JtaProperties { + + public static final String PREFIX = "spring.jta"; + + private String logDir; + + public void setLogDir(String logDir) { + this.logDir = logDir; + } + + public String getLogDir() { + return this.logDir; + } + +} diff --git a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/orm/jpa/EntityManagerFactoryBuilder.java b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/orm/jpa/EntityManagerFactoryBuilder.java index 1432c27d0f..af710e687d 100644 --- a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/orm/jpa/EntityManagerFactoryBuilder.java +++ b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/orm/jpa/EntityManagerFactoryBuilder.java @@ -89,6 +89,8 @@ public class EntityManagerFactoryBuilder { private Map properties = new HashMap(); + private boolean jta; + private Builder(DataSource dataSource) { this.dataSource = dataSource; } @@ -142,6 +144,21 @@ public class EntityManagerFactoryBuilder { return this; } + /** + * Configure if using a JTA {@link DataSource}, i.e. if + * {@link LocalContainerEntityManagerFactoryBean#setDataSource(DataSource) + * setDataSource} or + * {@link LocalContainerEntityManagerFactoryBean#setJtaDataSource(DataSource) + * setJtaDataSource} should be called on the + * {@link LocalContainerEntityManagerFactoryBean}. + * @param jta if the data source is JTA + * @return the builder for fluent usage + */ + public Builder jta(boolean jta) { + this.jta = jta; + return this; + } + public LocalContainerEntityManagerFactoryBean build() { LocalContainerEntityManagerFactoryBean entityManagerFactoryBean = new LocalContainerEntityManagerFactoryBean(); if (EntityManagerFactoryBuilder.this.persistenceUnitManager != null) { @@ -153,7 +170,14 @@ public class EntityManagerFactoryBuilder { } entityManagerFactoryBean .setJpaVendorAdapter(EntityManagerFactoryBuilder.this.jpaVendorAdapter); - entityManagerFactoryBean.setDataSource(this.dataSource); + + if (this.jta) { + entityManagerFactoryBean.setJtaDataSource(this.dataSource); + } + else { + entityManagerFactoryBean.setDataSource(this.dataSource); + } + entityManagerFactoryBean.setPackagesToScan(this.packagesToScan); entityManagerFactoryBean.getJpaPropertyMap().putAll( EntityManagerFactoryBuilder.this.properties.getProperties()); diff --git a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/orm/jpa/HibernateJpaAutoConfiguration.java b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/orm/jpa/HibernateJpaAutoConfiguration.java index 2cb64bcbd8..979ca1fa0b 100644 --- a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/orm/jpa/HibernateJpaAutoConfiguration.java +++ b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/orm/jpa/HibernateJpaAutoConfiguration.java @@ -22,6 +22,7 @@ import java.util.Map; import javax.persistence.EntityManager; import javax.sql.DataSource; +import org.hibernate.engine.transaction.jta.platform.internal.NoJtaPlatform; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.autoconfigure.AutoConfigureAfter; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; @@ -29,8 +30,9 @@ import org.springframework.boot.autoconfigure.condition.ConditionOutcome; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.autoconfigure.condition.SpringBootCondition; import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration; +import org.springframework.boot.autoconfigure.jta.JtaAutoConfiguration; import org.springframework.boot.autoconfigure.orm.jpa.HibernateJpaAutoConfiguration.HibernateEntityManagerCondition; -import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.boot.orm.jpa.hibernate.SpringJtaPlatform; import org.springframework.context.annotation.ConditionContext; import org.springframework.context.annotation.Conditional; import org.springframework.context.annotation.Configuration; @@ -39,29 +41,30 @@ import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean; import org.springframework.orm.jpa.vendor.AbstractJpaVendorAdapter; import org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter; import org.springframework.transaction.annotation.EnableTransactionManagement; +import org.springframework.transaction.jta.JtaTransactionManager; import org.springframework.util.ClassUtils; /** * {@link EnableAutoConfiguration Auto-configuration} for Hibernate JPA. * * @author Phillip Webb + * @author Josh Long */ @Configuration @ConditionalOnClass({ LocalContainerEntityManagerFactoryBean.class, EnableTransactionManagement.class, EntityManager.class }) @Conditional(HibernateEntityManagerCondition.class) -@AutoConfigureAfter(DataSourceAutoConfiguration.class) +@AutoConfigureAfter({ DataSourceAutoConfiguration.class, JtaAutoConfiguration.class }) public class HibernateJpaAutoConfiguration extends JpaBaseConfiguration { + private static final String JTA_PLATFORM = "hibernate.transaction.jta.platform"; + @Autowired private JpaProperties properties; @Autowired private DataSource dataSource; - @Autowired - private ConfigurableApplicationContext applicationContext; - @Override protected AbstractJpaVendorAdapter createJpaVendorAdapter() { return new HibernateJpaVendorAdapter(); @@ -74,6 +77,21 @@ public class HibernateJpaAutoConfiguration extends JpaBaseConfiguration { return vendorProperties; } + @Override + protected void customizeVendorProperties(Map vendorProperties) { + super.customizeVendorProperties(vendorProperties); + if (!vendorProperties.containsKey(JTA_PLATFORM)) { + JtaTransactionManager jtaTransactionManager = getJtaTransactionManager(); + if (jtaTransactionManager != null) { + vendorProperties.put(JTA_PLATFORM, new SpringJtaPlatform( + jtaTransactionManager)); + } + else { + vendorProperties.put(JTA_PLATFORM, NoJtaPlatform.INSTANCE); + } + } + } + static class HibernateEntityManagerCondition extends SpringBootCondition { private static String[] CLASS_NAMES = { diff --git a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/orm/jpa/JpaBaseConfiguration.java b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/orm/jpa/JpaBaseConfiguration.java index 0e4c3497a7..c0935824df 100644 --- a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/orm/jpa/JpaBaseConfiguration.java +++ b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/orm/jpa/JpaBaseConfiguration.java @@ -44,6 +44,7 @@ import org.springframework.orm.jpa.support.OpenEntityManagerInViewFilter; import org.springframework.orm.jpa.support.OpenEntityManagerInViewInterceptor; import org.springframework.orm.jpa.vendor.AbstractJpaVendorAdapter; import org.springframework.transaction.PlatformTransactionManager; +import org.springframework.transaction.jta.JtaTransactionManager; import org.springframework.web.servlet.config.annotation.InterceptorRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter; @@ -71,6 +72,9 @@ public abstract class JpaBaseConfiguration implements BeanFactoryAware { @Autowired private JpaProperties jpaProperties; + @Autowired(required = false) + private JtaTransactionManager jtaTransactionManager; + @Bean @ConditionalOnMissingBean(PlatformTransactionManager.class) public PlatformTransactionManager transactionManager() { @@ -103,14 +107,24 @@ public abstract class JpaBaseConfiguration implements BeanFactoryAware { @ConditionalOnMissingBean public LocalContainerEntityManagerFactoryBean entityManagerFactory( EntityManagerFactoryBuilder factoryBuilder) { + Map vendorProperties = getVendorProperties(); + customizeVendorProperties(vendorProperties); return factoryBuilder.dataSource(this.dataSource).packages(getPackagesToScan()) - .properties(getVendorProperties()).build(); + .properties(vendorProperties).jta(isJta()).build(); } protected abstract AbstractJpaVendorAdapter createJpaVendorAdapter(); protected abstract Map getVendorProperties(); + /** + * Customize vendor properties before they are used. Allows for post processing (for + * example to configure JTA specific settings). + * @param vendorProperties the vendor properties to customize + */ + protected void customizeVendorProperties(Map vendorProperties) { + } + protected EntityManagerFactoryBuilder.EntityManagerFactoryBeanCallback getVendorCallback() { return null; } @@ -127,6 +141,20 @@ public abstract class JpaBaseConfiguration implements BeanFactoryAware { LocalContainerEntityManagerFactoryBean entityManagerFactoryBean) { } + /** + * @return the jtaTransactionManager or {@code null} + */ + protected JtaTransactionManager getJtaTransactionManager() { + return this.jtaTransactionManager; + } + + /** + * Returns if a JTA {@link PlatformTransactionManager} is being used. + */ + protected final boolean isJta() { + return (this.jtaTransactionManager != null); + } + @Override public void setBeanFactory(BeanFactory beanFactory) throws BeansException { this.beanFactory = (ConfigurableListableBeanFactory) beanFactory; diff --git a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/orm/jpa/JpaProperties.java b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/orm/jpa/JpaProperties.java index 84437aba29..fa481f4b5f 100644 --- a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/orm/jpa/JpaProperties.java +++ b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/orm/jpa/JpaProperties.java @@ -25,7 +25,7 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.springframework.boot.autoconfigure.jdbc.EmbeddedDatabaseConnection; import org.springframework.boot.context.properties.ConfigurationProperties; -import org.springframework.boot.orm.jpa.SpringNamingStrategy; +import org.springframework.boot.orm.jpa.hibernate.SpringNamingStrategy; import org.springframework.orm.jpa.vendor.Database; import org.springframework.util.StringUtils; diff --git a/spring-boot-autoconfigure/src/main/resources/META-INF/spring.factories b/spring-boot-autoconfigure/src/main/resources/META-INF/spring.factories index 8f1ba96768..8fe6706801 100644 --- a/spring-boot-autoconfigure/src/main/resources/META-INF/spring.factories +++ b/spring-boot-autoconfigure/src/main/resources/META-INF/spring.factories @@ -20,11 +20,14 @@ org.springframework.boot.autoconfigure.hateoas.HypermediaAutoConfiguration,\ org.springframework.boot.autoconfigure.integration.IntegrationAutoConfiguration,\ org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration,\ org.springframework.boot.autoconfigure.jdbc.JndiDataSourceAutoConfiguration,\ +org.springframework.boot.autoconfigure.jdbc.XADataSourceAutoConfiguration,\ org.springframework.boot.autoconfigure.jdbc.DataSourceTransactionManagerAutoConfiguration,\ org.springframework.boot.autoconfigure.jms.JmsAutoConfiguration,\ org.springframework.boot.autoconfigure.jmx.JmxAutoConfiguration,\ +org.springframework.boot.autoconfigure.jms.JndiConnectionFactoryAutoConfiguration,\ org.springframework.boot.autoconfigure.jms.activemq.ActiveMQAutoConfiguration,\ org.springframework.boot.autoconfigure.jms.hornetq.HornetQAutoConfiguration,\ +org.springframework.boot.autoconfigure.jta.JtaAutoConfiguration,\ org.springframework.boot.autoconfigure.elasticsearch.ElasticsearchAutoConfiguration,\ org.springframework.boot.autoconfigure.elasticsearch.ElasticsearchDataAutoConfiguration,\ org.springframework.boot.autoconfigure.flyway.FlywayAutoConfiguration,\ diff --git a/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/condition/ConditionOnJndiTests.java b/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/condition/ConditionOnJndiTests.java new file mode 100644 index 0000000000..5d0f9aec29 --- /dev/null +++ b/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/condition/ConditionOnJndiTests.java @@ -0,0 +1,100 @@ +/* + * 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.autoconfigure.condition; + +import java.util.HashMap; +import java.util.Map; + +import org.junit.Test; +import org.springframework.core.type.AnnotatedTypeMetadata; + +import static org.hamcrest.Matchers.equalTo; +import static org.junit.Assert.assertThat; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; + +/** + * Tests for {@link OnJndiCondition}. + * + * @author Phillip Webb + */ +public class ConditionOnJndiTests { + + private MockableOnJndi condition = new MockableOnJndi(); + + @Test + public void jndiNotAvailable() { + this.condition.setJndiAvailable(false); + ConditionOutcome outcome = this.condition.getMatchOutcome(null, mockMetaData()); + assertThat(outcome.isMatch(), equalTo(false)); + } + + @Test + public void jndiLocationNotFound() { + ConditionOutcome outcome = this.condition.getMatchOutcome(null, + mockMetaData("java:/a")); + assertThat(outcome.isMatch(), equalTo(false)); + } + + @Test + public void jndiLocationFound() { + this.condition.setFoundLocation("java:/b"); + ConditionOutcome outcome = this.condition.getMatchOutcome(null, + mockMetaData("java:/a", "java:/b")); + assertThat(outcome.isMatch(), equalTo(true)); + } + + private AnnotatedTypeMetadata mockMetaData(String... value) { + AnnotatedTypeMetadata metadata = mock(AnnotatedTypeMetadata.class); + Map attributes = new HashMap(); + attributes.put("value", value); + given(metadata.getAnnotationAttributes(ConditionalOnJndi.class.getName())) + .willReturn(attributes); + return metadata; + } + + private static class MockableOnJndi extends OnJndiCondition { + + private boolean jndiAvailable = true; + + private String foundLocation; + + @Override + protected boolean isJndiAvailable() { + return this.jndiAvailable; + } + + @Override + protected JndiLocator getJndiLocator(String[] locations) { + return new JndiLocator(locations) { + @Override + public String lookupFirstLocation() { + return MockableOnJndi.this.foundLocation; + } + }; + } + + public void setJndiAvailable(boolean jndiAvailable) { + this.jndiAvailable = jndiAvailable; + } + + public void setFoundLocation(String foundLocation) { + this.foundLocation = foundLocation; + } + } + +} diff --git a/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jdbc/DriverClassNameProviderTests.java b/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jdbc/DatabaseDriverTests.java similarity index 63% rename from spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jdbc/DriverClassNameProviderTests.java rename to spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jdbc/DatabaseDriverTests.java index 861941007c..53987b2b8c 100644 --- a/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jdbc/DriverClassNameProviderTests.java +++ b/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jdbc/DatabaseDriverTests.java @@ -20,47 +20,46 @@ import org.junit.Rule; import org.junit.Test; import org.junit.rules.ExpectedException; +import static org.hamcrest.Matchers.equalTo; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertThat; /** - * Tests for {@link DriverClassNameProvider}. + * Tests for {@link DatabaseDriver}. * + * @author Phillip Webb * @author Maciej Walkowiak */ -public class DriverClassNameProviderTests { - - private DriverClassNameProvider provider = new DriverClassNameProvider(); +public class DatabaseDriverTests { @Rule public ExpectedException thrown = ExpectedException.none(); @Test public void classNameForKnownDatabase() { - String driverClassName = this.provider - .getDriverClassName("jdbc:postgresql://hostname/dbname"); + String driverClassName = DatabaseDriver.fromJdbcUrl( + "jdbc:postgresql://hostname/dbname").getDriverClassName(); assertEquals("org.postgresql.Driver", driverClassName); } @Test - public void nullForUnknownDatabase() { - String driverClassName = this.provider - .getDriverClassName("jdbc:unknowndb://hostname/dbname"); + public void nullClassNameForUnknownDatabase() { + String driverClassName = DatabaseDriver.fromJdbcUrl( + "jdbc:unknowndb://hostname/dbname").getDriverClassName(); assertNull(driverClassName); } @Test - public void failureOnNullJdbcUrl() { - this.thrown.expect(IllegalArgumentException.class); - this.thrown.expectMessage("JdbcUrl must not be null"); - this.provider.getDriverClassName(null); + public void unknownOnNullJdbcUrl() { + assertThat(DatabaseDriver.fromJdbcUrl(null), equalTo(DatabaseDriver.UNKNOWN)); } @Test public void failureOnMalformedJdbcUrl() { this.thrown.expect(IllegalArgumentException.class); - this.thrown.expectMessage("JdbcUrl must start with"); - this.provider.getDriverClassName("malformed:url"); + this.thrown.expectMessage("URL must start with"); + DatabaseDriver.fromJdbcUrl("malformed:url"); } } diff --git a/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jdbc/XADataSourceAutoConfigurationTests.java b/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jdbc/XADataSourceAutoConfigurationTests.java new file mode 100644 index 0000000000..0defecd364 --- /dev/null +++ b/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jdbc/XADataSourceAutoConfigurationTests.java @@ -0,0 +1,128 @@ +/* + * 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.autoconfigure.jdbc; + +import javax.sql.DataSource; +import javax.sql.XADataSource; + +import org.hsqldb.jdbc.pool.JDBCXADataSource; +import org.junit.Test; +import org.springframework.boot.jta.XADataSourceWrapper; +import org.springframework.boot.test.EnvironmentTestUtils; +import org.springframework.context.ApplicationContext; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import static org.hamcrest.Matchers.equalTo; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertThat; +import static org.mockito.Mockito.mock; + +/** + * Tests for {@link XADataSourceAutoConfiguration}. + * + * @author Phillip Webb + */ +public class XADataSourceAutoConfigurationTests { + + @Test + public void wrapExistingXaDataSource() throws Exception { + ApplicationContext context = createContext(WrapExisting.class); + context.getBean(DataSource.class); + XADataSource source = context.getBean(XADataSource.class); + MockXADataSourceWrapper wrapper = context.getBean(MockXADataSourceWrapper.class); + assertThat(wrapper.getXaDataSource(), equalTo(source)); + } + + @Test + public void createFromUrl() throws Exception { + ApplicationContext context = createContext(FromProperties.class, + "spring.datasource.url:jdbc:hsqldb:mem:test", + "spring.datasource.username:un"); + context.getBean(DataSource.class); + MockXADataSourceWrapper wrapper = context.getBean(MockXADataSourceWrapper.class); + JDBCXADataSource dataSource = (JDBCXADataSource) wrapper.getXaDataSource(); + assertNotNull(dataSource); + assertThat(dataSource.getUrl(), equalTo("jdbc:hsqldb:mem:test")); + assertThat(dataSource.getUser(), equalTo("un")); + } + + @Test + public void createFromClass() throws Exception { + ApplicationContext context = createContext( + FromProperties.class, + "spring.datasource.xa.data-source-class:org.hsqldb.jdbc.pool.JDBCXADataSource", + "spring.datasource.xa.properties.database-name:test"); + context.getBean(DataSource.class); + MockXADataSourceWrapper wrapper = context.getBean(MockXADataSourceWrapper.class); + JDBCXADataSource dataSource = (JDBCXADataSource) wrapper.getXaDataSource(); + assertNotNull(dataSource); + assertThat(dataSource.getDatabaseName(), equalTo("test")); + + } + + private ApplicationContext createContext(Class configuration, String... env) { + AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(); + EnvironmentTestUtils.addEnvironment(context, env); + context.register(configuration, XADataSourceAutoConfiguration.class); + context.refresh(); + return context; + } + + @Configuration + static class WrapExisting { + + @Bean + public MockXADataSourceWrapper wrapper() { + return new MockXADataSourceWrapper(); + } + + @Bean + public XADataSource xaDataSource() { + return mock(XADataSource.class); + } + + } + + @Configuration + static class FromProperties { + + @Bean + public MockXADataSourceWrapper wrapper() { + return new MockXADataSourceWrapper(); + } + + } + + private static class MockXADataSourceWrapper implements XADataSourceWrapper { + + private XADataSource dataSource; + + @Override + public DataSource wrapDataSource(XADataSource dataSource) { + this.dataSource = dataSource; + return mock(DataSource.class); + } + + public XADataSource getXaDataSource() { + return this.dataSource; + } + + } + +} diff --git a/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jms/JmsAutoConfigurationTests.java b/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jms/JmsAutoConfigurationTests.java index c69d1d524f..c1bcba116a 100644 --- a/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jms/JmsAutoConfigurationTests.java +++ b/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jms/JmsAutoConfigurationTests.java @@ -24,7 +24,6 @@ import org.junit.Test; import org.springframework.beans.BeansException; import org.springframework.beans.factory.config.BeanPostProcessor; import org.springframework.boot.autoconfigure.jms.activemq.ActiveMQAutoConfiguration; -import org.springframework.boot.autoconfigure.jms.activemq.ActiveMQProperties; import org.springframework.boot.test.EnvironmentTestUtils; import org.springframework.context.annotation.AnnotationConfigApplicationContext; import org.springframework.context.annotation.Bean; @@ -49,6 +48,10 @@ import static org.junit.Assert.assertTrue; */ public class JmsAutoConfigurationTests { + private static final String ACTIVEMQ_EMBEDDED_URL = "vm://localhost?broker.persistent=false"; + + private static final String ACTIVEMQ_NETWORK_URL = "tcp://localhost:61616"; + private AnnotationConfigApplicationContext context; @Test @@ -61,7 +64,7 @@ public class JmsAutoConfigurationTests { .getBean(JmsMessagingTemplate.class); assertEquals(jmsTemplate.getConnectionFactory(), connectionFactory); assertEquals(jmsTemplate, messagingTemplate.getJmsTemplate()); - assertEquals(ActiveMQProperties.DEFAULT_EMBEDDED_BROKER_URL, + assertEquals(ACTIVEMQ_EMBEDDED_URL, ((ActiveMQConnectionFactory) jmsTemplate.getConnectionFactory()) .getBrokerURL()); assertTrue("listener container factory should be created by default", @@ -158,7 +161,7 @@ public class JmsAutoConfigurationTests { assertNotNull(jmsTemplate); assertNotNull(connectionFactory); assertEquals(jmsTemplate.getConnectionFactory(), connectionFactory); - assertEquals(ActiveMQProperties.DEFAULT_NETWORK_BROKER_URL, + assertEquals(ACTIVEMQ_NETWORK_URL, ((ActiveMQConnectionFactory) jmsTemplate.getConnectionFactory()) .getBrokerURL()); } @@ -188,8 +191,7 @@ public class JmsAutoConfigurationTests { assertEquals(jmsTemplate.getConnectionFactory(), pool); ActiveMQConnectionFactory factory = (ActiveMQConnectionFactory) pool .getConnectionFactory(); - assertEquals(ActiveMQProperties.DEFAULT_EMBEDDED_BROKER_URL, - factory.getBrokerURL()); + assertEquals(ACTIVEMQ_EMBEDDED_URL, factory.getBrokerURL()); } @Test @@ -204,8 +206,7 @@ public class JmsAutoConfigurationTests { assertEquals(jmsTemplate.getConnectionFactory(), pool); ActiveMQConnectionFactory factory = (ActiveMQConnectionFactory) pool .getConnectionFactory(); - assertEquals(ActiveMQProperties.DEFAULT_NETWORK_BROKER_URL, - factory.getBrokerURL()); + assertEquals(ACTIVEMQ_NETWORK_URL, factory.getBrokerURL()); } @Test @@ -257,6 +258,7 @@ public class JmsAutoConfigurationTests { @Configuration protected static class TestConfiguration2 { + @Bean ConnectionFactory connectionFactory() { return new ActiveMQConnectionFactory() { @@ -265,10 +267,12 @@ public class JmsAutoConfigurationTests { } }; } + } @Configuration protected static class TestConfiguration3 { + @Bean JmsTemplate jmsTemplate(ConnectionFactory connectionFactory) { JmsTemplate jmsTemplate = new JmsTemplate(connectionFactory); @@ -280,6 +284,7 @@ public class JmsAutoConfigurationTests { @Configuration protected static class TestConfiguration4 implements BeanPostProcessor { + @Override public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException { @@ -295,6 +300,7 @@ public class JmsAutoConfigurationTests { throws BeansException { return bean; } + } @Configuration diff --git a/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jms/activemq/ActiveMQPropertiesTests.java b/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jms/activemq/ActiveMQPropertiesTests.java index 699bcdc5c1..68ae0ed1ae 100644 --- a/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jms/activemq/ActiveMQPropertiesTests.java +++ b/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jms/activemq/ActiveMQPropertiesTests.java @@ -17,67 +17,50 @@ package org.springframework.boot.autoconfigure.jms.activemq; import org.junit.Test; -import org.springframework.boot.test.EnvironmentTestUtils; -import org.springframework.core.env.StandardEnvironment; import static org.junit.Assert.assertEquals; /** - * Tests for {@link ActiveMQProperties}. + * Tests for {@link ActiveMQProperties} and ActiveMQConnectionFactoryFactory. * * @author Stephane Nicoll */ public class ActiveMQPropertiesTests { + private static final String DEFAULT_EMBEDDED_BROKER_URL = "vm://localhost?broker.persistent=false"; + + private static final String DEFAULT_NETWORK_BROKER_URL = "tcp://localhost:61616"; + private final ActiveMQProperties properties = new ActiveMQProperties(); - private final StandardEnvironment environment = new StandardEnvironment(); - - @Test - public void determineBrokerUrlDefault() { - assertEquals(ActiveMQProperties.DEFAULT_EMBEDDED_BROKER_URL, - ActiveMQProperties.determineBrokerUrl(this.environment)); - } - - @Test - public void determineBrokerUrlVmBrokerUrl() { - EnvironmentTestUtils.addEnvironment(this.environment, - "spring.activemq.brokerUrl:vm://localhost?persistent=true"); - assertEquals("vm://localhost?persistent=true", - ActiveMQProperties.determineBrokerUrl(this.environment)); - } - - @Test - public void determineBrokerUrlInMemoryFlag() { - EnvironmentTestUtils.addEnvironment(this.environment, - "spring.activemq.inMemory:false"); - assertEquals(ActiveMQProperties.DEFAULT_NETWORK_BROKER_URL, - ActiveMQProperties.determineBrokerUrl(this.environment)); - } - @Test public void getBrokerUrlIsInMemoryByDefault() { - assertEquals(ActiveMQProperties.DEFAULT_EMBEDDED_BROKER_URL, - this.properties.determineBrokerUrl()); + assertEquals(DEFAULT_EMBEDDED_BROKER_URL, new ActiveMQConnectionFactoryFactory( + this.properties).determineBrokerUrl()); } @Test public void getBrokerUrlUseExplicitBrokerUrl() { this.properties.setBrokerUrl("vm://foo-bar"); - assertEquals("vm://foo-bar", this.properties.determineBrokerUrl()); + assertEquals("vm://foo-bar", + new ActiveMQConnectionFactoryFactory(this.properties) + .determineBrokerUrl()); } @Test public void getBrokerUrlWithInMemorySetToFalse() { this.properties.setInMemory(false); - assertEquals(ActiveMQProperties.DEFAULT_NETWORK_BROKER_URL, - this.properties.determineBrokerUrl()); + assertEquals(DEFAULT_NETWORK_BROKER_URL, new ActiveMQConnectionFactoryFactory( + this.properties).determineBrokerUrl()); } @Test public void getExplicitBrokerUrlAlwaysWins() { this.properties.setBrokerUrl("vm://foo-bar"); this.properties.setInMemory(false); - assertEquals("vm://foo-bar", this.properties.determineBrokerUrl()); + assertEquals("vm://foo-bar", + new ActiveMQConnectionFactoryFactory(this.properties) + .determineBrokerUrl()); } + } diff --git a/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jms/hornetq/HornetQAutoConfigurationTests.java b/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jms/hornetq/HornetQAutoConfigurationTests.java index f304de8248..d96d2a3cd6 100644 --- a/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jms/hornetq/HornetQAutoConfigurationTests.java +++ b/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jms/hornetq/HornetQAutoConfigurationTests.java @@ -43,11 +43,13 @@ import org.junit.Test; import org.junit.rules.TemporaryFolder; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.autoconfigure.jms.JmsAutoConfiguration; +import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.boot.test.EnvironmentTestUtils; import org.springframework.context.ApplicationContext; import org.springframework.context.annotation.AnnotationConfigApplicationContext; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; import org.springframework.jms.core.JmsTemplate; import org.springframework.jms.core.MessageCreator; import org.springframework.jms.core.SessionCallback; @@ -183,10 +185,8 @@ public class HornetQAutoConfigurationTests { @Test public void embeddedServiceWithCustomJmsConfiguration() { - load(CustomJmsConfiguration.class, "spring.hornetq.embedded.queues=Queue1,Queue2"); // Ignored - // with - // custom - // config + // Ignored with custom config + load(CustomJmsConfiguration.class, "spring.hornetq.embedded.queues=Queue1,Queue2"); DestinationChecker checker = new DestinationChecker(this.context); checker.checkQueue("custom", true); // See CustomJmsConfiguration @@ -317,7 +317,7 @@ public class HornetQAutoConfigurationTests { String... environment) { AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext(); applicationContext.register(config); - applicationContext.register(HornetQAutoConfiguration.class, + applicationContext.register(HornetQAutoConfigurationWithoutXA.class, JmsAutoConfiguration.class); EnvironmentTestUtils.addEnvironment(applicationContext, environment); applicationContext.refresh(); @@ -417,4 +417,11 @@ public class HornetQAutoConfigurationTests { } } + @Configuration + @EnableConfigurationProperties(HornetQProperties.class) + @Import({ HornetQEmbeddedServerConfiguration.class, + HornetQConnectionFactoryConfiguration.class }) + protected static class HornetQAutoConfigurationWithoutXA { + } + } diff --git a/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jta/JtaAutoConfigurationTests.java b/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jta/JtaAutoConfigurationTests.java new file mode 100644 index 0000000000..f07097a6b2 --- /dev/null +++ b/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jta/JtaAutoConfigurationTests.java @@ -0,0 +1,107 @@ +/* + * 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.autoconfigure.jta; + +import javax.transaction.TransactionManager; +import javax.transaction.UserTransaction; + +import org.junit.After; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.springframework.beans.factory.NoSuchBeanDefinitionException; +import org.springframework.boot.jta.XAConnectionFactoryWrapper; +import org.springframework.boot.jta.XADataSourceWrapper; +import org.springframework.boot.jta.atomikos.AtomikosDependsOnBeanFactoryPostProcessor; +import org.springframework.boot.jta.atomikos.AtomikosProperties; +import org.springframework.boot.jta.bitronix.BitronixDependentBeanFactoryPostProcessor; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.transaction.PlatformTransactionManager; +import org.springframework.transaction.jta.JtaTransactionManager; + +import com.atomikos.icatch.config.UserTransactionService; +import com.atomikos.icatch.jta.UserTransactionManager; + +import static org.mockito.Mockito.mock; + +/** + * Tests for {@link JtaAutoConfiguration}. + * + * @author Josh Long + * @author Phillip Webb + */ +public class JtaAutoConfigurationTests { + + @Rule + public ExpectedException thrown = ExpectedException.none(); + + private AnnotationConfigApplicationContext context; + + @After + public void closeContext() { + if (this.context != null) { + this.context.close(); + } + } + + @Test + public void customPatformTransactionManager() throws Exception { + this.context = new AnnotationConfigApplicationContext( + CustomTransactionManagerConfig.class, JtaAutoConfiguration.class); + this.thrown.expect(NoSuchBeanDefinitionException.class); + this.context.getBean(JtaTransactionManager.class); + } + + @Test + public void atomikosSanityCheck() throws Exception { + this.context = new AnnotationConfigApplicationContext(JtaProperties.class, + AtomikosJtaConfiguration.class); + this.context.getBean(AtomikosProperties.class); + this.context.getBean(UserTransactionService.class); + this.context.getBean(UserTransactionManager.class); + this.context.getBean(UserTransaction.class); + this.context.getBean(XADataSourceWrapper.class); + this.context.getBean(XAConnectionFactoryWrapper.class); + this.context.getBean(AtomikosDependsOnBeanFactoryPostProcessor.class); + this.context.getBean(JtaTransactionManager.class); + } + + @Test + public void bitronixSanityCheck() throws Exception { + this.context = new AnnotationConfigApplicationContext(JtaProperties.class, + BitronixJtaConfiguration.class); + this.context.getBean(bitronix.tm.Configuration.class); + this.context.getBean(TransactionManager.class); + this.context.getBean(XADataSourceWrapper.class); + this.context.getBean(XAConnectionFactoryWrapper.class); + this.context.getBean(BitronixDependentBeanFactoryPostProcessor.class); + this.context.getBean(JtaTransactionManager.class); + } + + @Configuration + public static class CustomTransactionManagerConfig { + + @Bean + public PlatformTransactionManager transactionManager() { + return mock(PlatformTransactionManager.class); + } + + } + +} diff --git a/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/orm/jpa/AbstractJpaAutoConfigurationTests.java b/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/orm/jpa/AbstractJpaAutoConfigurationTests.java index 1e6734d164..d68309630f 100644 --- a/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/orm/jpa/AbstractJpaAutoConfigurationTests.java +++ b/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/orm/jpa/AbstractJpaAutoConfigurationTests.java @@ -17,11 +17,12 @@ package org.springframework.boot.autoconfigure.orm.jpa; import java.lang.reflect.Field; -import java.util.Collections; +import java.util.HashMap; import java.util.Map; import javax.sql.DataSource; +import org.hibernate.engine.transaction.jta.platform.internal.NoJtaPlatform; import org.junit.After; import org.junit.Rule; import org.junit.Test; @@ -228,8 +229,10 @@ public abstract class AbstractJpaAutoConfigurationTests { factoryBean.setJpaVendorAdapter(adapter); factoryBean.setDataSource(dataSource); factoryBean.setPersistenceUnitName("manually-configured"); - factoryBean.setJpaPropertyMap(Collections.singletonMap("configured", - "manually")); + Map properties = new HashMap(); + properties.put("configured", "manually"); + properties.put("hibernate.transaction.jta.platform", NoJtaPlatform.INSTANCE); + factoryBean.setJpaPropertyMap(properties); return factoryBean; } } diff --git a/spring-boot-dependencies/pom.xml b/spring-boot-dependencies/pom.xml index 9a5707e86e..92840a05e5 100644 --- a/spring-boot-dependencies/pom.xml +++ b/spring-boot-dependencies/pom.xml @@ -48,6 +48,8 @@ 5.9.1 1.8.2 + 3.9.3 + 2.1.4 3.0.2 1.9.2 3.2.1 @@ -85,6 +87,7 @@ 1.2.2 0.9.1 1.2 + 1.1 4.11 3.0.8 1.2.17 @@ -257,6 +260,21 @@ spring-boot-starter-jetty 1.2.0.BUILD-SNAPSHOT + + org.springframework.boot + spring-boot-starter-jta-atomikos + 1.2.0.BUILD-SNAPSHOT + + + org.springframework.boot + spring-boot-starter-jta-bitronix + 1.2.0.BUILD-SNAPSHOT + + + org.springframework.boot + spring-boot-starter-jta-arjuna + 1.2.0.BUILD-SNAPSHOT + org.springframework.boot spring-boot-starter-log4j @@ -350,6 +368,21 @@ logback-classic ${logback.version} + + com.atomikos + transactions-jdbc + ${atomikos.version} + + + com.atomikos + transactions-jms + ${atomikos.version} + + + com.atomikos + transactions-jta + ${atomikos.version} + com.codahale.metrics metrics-graphite @@ -370,6 +403,11 @@ metrics-servlets ${codahale-metrics.version} + + org.codehaus.btm + btm + ${bitronix.version} + org.codehaus.janino janino @@ -482,6 +520,11 @@ jstl ${jstl.version} + + javax.transaction + jta + ${jta.version} + jaxen jaxen diff --git a/spring-boot-docs/src/main/asciidoc/appendix-application-properties.adoc b/spring-boot-docs/src/main/asciidoc/appendix-application-properties.adoc index a9c14a45c4..6cd7748494 100644 --- a/spring-boot-docs/src/main/asciidoc/appendix-application-properties.adoc +++ b/spring-boot-docs/src/main/asciidoc/appendix-application-properties.adoc @@ -205,6 +205,10 @@ content into your application; rather pick only the properties that you need. spring.jpa.hibernate.ddl-auto= # defaults to create-drop for embedded dbs spring.data.jpa.repositories.enabled=true # if spring data repository support is enabled + # JTA ({sc-spring-boot-autoconfigure}/jta/JtaAutoConfiguration.{sc-ext}[JtaAutoConfiguration]) + spring.jta.log-dir= # transaction log dir + spring.jta.*= # technology specific configuration + # SOLR ({sc-spring-boot-autoconfigure}/solr/SolrProperties.{sc-ext}[SolrProperties}]) spring.data.solr.host=http://127.0.0.1:8983/solr spring.data.solr.zkHost= diff --git a/spring-boot-docs/src/main/asciidoc/spring-boot-features.adoc b/spring-boot-docs/src/main/asciidoc/spring-boot-features.adoc index 1774bddbf6..e79ad9b97a 100644 --- a/spring-boot-docs/src/main/asciidoc/spring-boot-features.adoc +++ b/spring-boot-docs/src/main/asciidoc/spring-boot-features.adoc @@ -1874,6 +1874,83 @@ own beans: +[[boot-features-jta]] +== Distributed Transactions with JTA +Spring Boot supports distributed JTA transactions across multiple XA resources using +either an http://www.atomikos.com/[Atomkos] or +http://docs.codehaus.org/display/BTM/Home[Bitronix] embedded transaction manager. JTA +transactions are also supported when deploying to a suitable Java EE Application Server. + +When a JTA environment is detected, Spring's `JtaTransactionManager` will be used to manage +transactions. Auto-configured JMS, DataSource and JPA beans will be upgraded to support +XA transactions. You can use standard Spring idioms such as `@Transactional` to +participate in a distributed transaction. + + + +=== Using an Atomikos transaction manager +Atomikos is a popular open source transaction manager which can be embedded into your +Spring Boot application. You can use the `spring-boot-starter-jta-atomikos` Starter POM to +pull in the appropriate Atomikos libraries. Spring Boot will auto-configure Atomikos and +ensure that appropriate `depends-on` settings are applied to your Spring Beans for correct +startup and shutdown ordering. + +By default Atomikos transaction logs will be written to a `transaction-logs` folder in +your application home directory (the directory in which your application jar file +resides). You can customize this directory by setting a `spring.jta.log-dir` property in +your `application.properties` file. Properties starting `spring.jta.` can also be used to +customize the Atomikos `UserTransactionServiceIml`. See the +{dc-spring-boot}/jta/atomikos/AtomikosProperties.{dc-ext}[`AtomikosProperties` javadoc] +for complete details. + + + +=== Using a Bitronix transaction manager +Bitronix is another popular open source JTA transaction manager implementation. You can +use the `spring-boot-starter-jta-bitronix` starter POM to add the appropriate Birtronix +dependencies to your project. As with Atomikos, Spring Boot will automatically configure +Bitronix and post-process your beans to ensure that startup and shutdown ordering is +correct. + +By default Bitronix transaction log files (`part1.btm` and `part2.btm`) will be written to +a `transaction-logs` folder in your application home directory. You can customize this +directory by using the `spring.jta.log-dir` property. Properties starting `spring.jta.` +are also bound to the `bitronix.tm.Configuration` bean, allowing for complete +customization. See the http://btm.codehaus.org/api/2.0.1/bitronix/tm/Configuration.html[Bitronix +documentation] for details. + + + +=== Using a Java EE managed transaction manager +If you are packaging your Spring Boot application as a `war` or `ear` file and deploying +it to a Java EE application server, you can use your application servers built-in +transaction manager. Spring Boot will attempt to auto-configure a transaction manager by +looking at common JNDI locations (`java:comp/UserTransaction`, +`java:comp/TransactionManager` etc). If you are using a transaction service provided by +your application server, you will generally also want to ensure that all resources are +managed by the server and exposed over JNDI. Spring Boot will attempt to auto-configure +JMS by looking for a `ConnectionFactory` at the JNDI path `java:/JmsXA` and you can use +the <> +to configure your `DataSource`. + + + +=== Supporting an alternative embedded transaction manager +The {sc-spring-boot}/jta/XAConnectionFactoryWrapper.{sc-ext}[`XAConnectionFactoryWrapper`] +and {sc-spring-boot}/jta/XADataSourceWrapper.{sc-ext}[`XADataSourceWrapper`] interfaces +can be used to support alternative embedded transaction managers. The interfaces are +responsible for wrapping `XAConnectionFactory` and `XADataSource` beans and exposing them +as regular `ConnectionFactory` and `DataSource` beans which will transparently enroll in +the distributed transaction. DataSource and JMS auto-configuration will use JTA variants +as long as you have a `JtaTransactionManager` bean and appropriate XA wrapper beans +registered within your `ApplicationContext` + +The {sc-spring-boot}/jta/BitronixXAConnectionFactoryWrapper.{sc-ext}[BitronixXAConnectionFactoryWrapper] +and {sc-spring-boot}/jta/BitronixXADataSourceWrapper.{sc-ext}[BitronixXADataSourceWrapper] +provide good examples of how to write XA wrappers. + + + [[boot-features-integration]] == Spring Integration Spring Integration provides abstractions over messaging and also other transports such as diff --git a/spring-boot-docs/src/main/asciidoc/using-spring-boot.adoc b/spring-boot-docs/src/main/asciidoc/using-spring-boot.adoc index 57a5c0480b..9965370a0c 100644 --- a/spring-boot-docs/src/main/asciidoc/using-spring-boot.adoc +++ b/spring-boot-docs/src/main/asciidoc/using-spring-boot.adoc @@ -255,6 +255,12 @@ and Hibernate. |`spring-boot-starter-jdbc` |JDBC Database support. +|`spring-boot-starter-jta-atomikos` +|Support for JTA distributed transactions via Atomikos. + +|`spring-boot-starter-jta-bitronix` +|Support for JTA distributed transactions via Bitronix. + |`spring-boot-starter-mobile` |Support for `spring-mobile` diff --git a/spring-boot-samples/pom.xml b/spring-boot-samples/pom.xml index e4cddbedf6..5404d60bae 100644 --- a/spring-boot-samples/pom.xml +++ b/spring-boot-samples/pom.xml @@ -38,6 +38,9 @@ spring-boot-sample-hornetq spring-boot-sample-integration spring-boot-sample-jetty + spring-boot-sample-jta-atomikos + spring-boot-sample-jta-bitronix + spring-boot-sample-jta-jndi spring-boot-sample-liquibase spring-boot-sample-parent-context spring-boot-sample-profile diff --git a/spring-boot-samples/spring-boot-sample-jta-atomikos/pom.xml b/spring-boot-samples/spring-boot-sample-jta-atomikos/pom.xml new file mode 100644 index 0000000000..bc7c05ce6d --- /dev/null +++ b/spring-boot-samples/spring-boot-sample-jta-atomikos/pom.xml @@ -0,0 +1,60 @@ + + + 4.0.0 + + + org.springframework.boot + spring-boot-samples + 1.2.0.BUILD-SNAPSHOT + + spring-boot-sample-jta-atomikos + Spring Boot Atomikos JTA Sample + Spring Boot Atomikos JTA Sample + http://projects.spring.io/spring-boot/ + + Pivotal Software, Inc. + http://www.spring.io + + + ${basedir}/../.. + + + + org.springframework + spring-jms + + + org.springframework.boot + spring-boot-starter-data-jpa + + + org.springframework.boot + spring-boot-starter-jta-atomikos + + + org.springframework.boot + spring-boot-starter-hornetq + + + org.hornetq + hornetq-jms-server + + + com.h2database + h2 + + + org.springframework.boot + spring-boot-starter-test + test + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + diff --git a/spring-boot-samples/spring-boot-sample-jta-atomikos/src/main/java/sample/atomikos/Account.java b/spring-boot-samples/spring-boot-sample-jta-atomikos/src/main/java/sample/atomikos/Account.java new file mode 100644 index 0000000000..d40c546003 --- /dev/null +++ b/spring-boot-samples/spring-boot-sample-jta-atomikos/src/main/java/sample/atomikos/Account.java @@ -0,0 +1,43 @@ +/* + * 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 sample.atomikos; + +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.Id; + +@Entity +public class Account { + + @Id + @GeneratedValue + private Long id; + + private String username; + + Account() { + } + + public Account(String username) { + this.username = username; + } + + public String getUsername() { + return this.username; + } + +} diff --git a/spring-boot-samples/spring-boot-sample-jta-atomikos/src/main/java/sample/atomikos/AccountRepository.java b/spring-boot-samples/spring-boot-sample-jta-atomikos/src/main/java/sample/atomikos/AccountRepository.java new file mode 100644 index 0000000000..6b88cb4cb4 --- /dev/null +++ b/spring-boot-samples/spring-boot-sample-jta-atomikos/src/main/java/sample/atomikos/AccountRepository.java @@ -0,0 +1,23 @@ +/* + * 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 sample.atomikos; + +import org.springframework.data.jpa.repository.JpaRepository; + +public interface AccountRepository extends JpaRepository { + +} diff --git a/spring-boot-samples/spring-boot-sample-jta-atomikos/src/main/java/sample/atomikos/AccountService.java b/spring-boot-samples/spring-boot-sample-jta-atomikos/src/main/java/sample/atomikos/AccountService.java new file mode 100644 index 0000000000..5f84d308f2 --- /dev/null +++ b/spring-boot-samples/spring-boot-sample-jta-atomikos/src/main/java/sample/atomikos/AccountService.java @@ -0,0 +1,47 @@ +/* + * 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 sample.atomikos; + +import javax.transaction.Transactional; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.jms.core.JmsTemplate; +import org.springframework.stereotype.Service; + +@Service +@Transactional +public class AccountService { + + private final JmsTemplate jmsTemplate; + + private final AccountRepository accountRepository; + + @Autowired + public AccountService(JmsTemplate jmsTemplate, AccountRepository accountRepository) { + this.jmsTemplate = jmsTemplate; + this.accountRepository = accountRepository; + } + + public void createAccountAndNotify(String username) { + this.jmsTemplate.convertAndSend("accounts", username); + this.accountRepository.save(new Account(username)); + if ("error".equals(username)) { + throw new RuntimeException("Simulated error"); + } + } + +} diff --git a/spring-boot-samples/spring-boot-sample-jta-atomikos/src/main/java/sample/atomikos/Messages.java b/spring-boot-samples/spring-boot-sample-jta-atomikos/src/main/java/sample/atomikos/Messages.java new file mode 100644 index 0000000000..d6f185f28b --- /dev/null +++ b/spring-boot-samples/spring-boot-sample-jta-atomikos/src/main/java/sample/atomikos/Messages.java @@ -0,0 +1,30 @@ +/* + * 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 sample.atomikos; + +import org.springframework.jms.annotation.JmsListener; +import org.springframework.stereotype.Component; + +@Component +public class Messages { + + @JmsListener(destination = "accounts") + public void onMessage(String content) { + System.out.println("----> " + content); + } + +} diff --git a/spring-boot-samples/spring-boot-sample-jta-atomikos/src/main/java/sample/atomikos/SampleAtomikosApplication.java b/spring-boot-samples/spring-boot-sample-jta-atomikos/src/main/java/sample/atomikos/SampleAtomikosApplication.java new file mode 100644 index 0000000000..e0094a3e4a --- /dev/null +++ b/spring-boot-samples/spring-boot-sample-jta-atomikos/src/main/java/sample/atomikos/SampleAtomikosApplication.java @@ -0,0 +1,50 @@ +/* + * 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 sample.atomikos; + +import java.io.Closeable; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.context.ApplicationContext; +import org.springframework.context.annotation.ComponentScan; +import org.springframework.context.annotation.Configuration; + +@Configuration +@ComponentScan +@EnableAutoConfiguration +public class SampleAtomikosApplication { + + public static void main(String[] args) throws Exception { + ApplicationContext context = SpringApplication.run( + SampleAtomikosApplication.class, args); + AccountService service = context.getBean(AccountService.class); + AccountRepository repository = context.getBean(AccountRepository.class); + service.createAccountAndNotify("josh"); + System.out.println("Count is " + repository.count()); + try { + service.createAccountAndNotify("error"); + } + catch (Exception ex) { + System.out.println(ex.getMessage()); + } + System.out.println("Count is " + repository.count()); + Thread.sleep(100); + ((Closeable) context).close(); + } + +} diff --git a/spring-boot-samples/spring-boot-sample-jta-atomikos/src/main/resources/application.properties b/spring-boot-samples/spring-boot-sample-jta-atomikos/src/main/resources/application.properties new file mode 100644 index 0000000000..62806e0ea8 --- /dev/null +++ b/spring-boot-samples/spring-boot-sample-jta-atomikos/src/main/resources/application.properties @@ -0,0 +1,5 @@ +spring.hornetq.mode=embedded +spring.hornetq.embedded.enabled=true +spring.hornetq.embedded.queues=accounts + +logging.level.com.atomikos=WARN diff --git a/spring-boot-samples/spring-boot-sample-jta-atomikos/src/test/java/sample/atomikos/SampleAtomikosApplicationTests.java b/spring-boot-samples/spring-boot-sample-jta-atomikos/src/test/java/sample/atomikos/SampleAtomikosApplicationTests.java new file mode 100644 index 0000000000..3950c3dfa8 --- /dev/null +++ b/spring-boot-samples/spring-boot-sample-jta-atomikos/src/test/java/sample/atomikos/SampleAtomikosApplicationTests.java @@ -0,0 +1,71 @@ +/* + * 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 sample.atomikos; + +import org.hamcrest.Matcher; +import org.hamcrest.core.SubstringMatcher; +import org.junit.Rule; +import org.junit.Test; +import org.springframework.boot.test.OutputCapture; + +import static org.hamcrest.Matchers.containsString; +import static org.junit.Assert.assertThat; + +/** + * Basic integration tests for demo application. + * + * @author Phillip Webb + */ +public class SampleAtomikosApplicationTests { + + @Rule + public OutputCapture outputCapture = new OutputCapture(); + + @Test + public void testTransactionRollback() throws Exception { + SampleAtomikosApplication.main(new String[] {}); + String expected = ""; + expected += "----> josh\n"; + expected += "Count is 1\n"; + expected += "Simulated error\n"; + expected += "Count is 1\n"; + assertThat(this.outputCapture.toString(), containsString(expected)); + assertThat(this.outputCapture.toString(), containsStringOnce("---->")); + } + + private Matcher containsStringOnce(String s) { + return new SubstringMatcher(s) { + + @Override + protected String relationship() { + return "containing once"; + } + + @Override + protected boolean evalSubstringOf(String s) { + int i = 0; + while (s.contains(this.substring)) { + s = s.substring(s.indexOf(this.substring) + this.substring.length()); + i++; + } + return i == 1; + } + + }; + } + +} diff --git a/spring-boot-samples/spring-boot-sample-jta-bitronix/pom.xml b/spring-boot-samples/spring-boot-sample-jta-bitronix/pom.xml new file mode 100644 index 0000000000..a88a235c53 --- /dev/null +++ b/spring-boot-samples/spring-boot-sample-jta-bitronix/pom.xml @@ -0,0 +1,61 @@ + + + 4.0.0 + + + org.springframework.boot + spring-boot-samples + 1.2.0.BUILD-SNAPSHOT + + spring-boot-sample-jta-bitronix + Spring Boot Bitronix JTA Sample + Spring Boot Bitronix JTA Sample + http://projects.spring.io/spring-boot/ + + Pivotal Software, Inc. + http://www.spring.io + + + ${basedir}/../.. + + + + org.springframework + spring-jms + + + org.springframework.boot + spring-boot-starter-data-jpa + + + org.springframework.boot + spring-boot-starter-jta-bitronix + + + org.springframework.boot + spring-boot-starter-hornetq + + + org.hornetq + hornetq-jms-server + + + com.h2database + h2 + + + org.springframework.boot + spring-boot-starter-test + test + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + diff --git a/spring-boot-samples/spring-boot-sample-jta-bitronix/src/main/java/sample/bitronix/Account.java b/spring-boot-samples/spring-boot-sample-jta-bitronix/src/main/java/sample/bitronix/Account.java new file mode 100644 index 0000000000..183e4c595a --- /dev/null +++ b/spring-boot-samples/spring-boot-sample-jta-bitronix/src/main/java/sample/bitronix/Account.java @@ -0,0 +1,43 @@ +/* + * 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 sample.bitronix; + +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.Id; + +@Entity +public class Account { + + @Id + @GeneratedValue + private Long id; + + private String username; + + Account() { + } + + public Account(String username) { + this.username = username; + } + + public String getUsername() { + return this.username; + } + +} diff --git a/spring-boot-samples/spring-boot-sample-jta-bitronix/src/main/java/sample/bitronix/AccountRepository.java b/spring-boot-samples/spring-boot-sample-jta-bitronix/src/main/java/sample/bitronix/AccountRepository.java new file mode 100644 index 0000000000..53978fa7d0 --- /dev/null +++ b/spring-boot-samples/spring-boot-sample-jta-bitronix/src/main/java/sample/bitronix/AccountRepository.java @@ -0,0 +1,23 @@ +/* + * 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 sample.bitronix; + +import org.springframework.data.jpa.repository.JpaRepository; + +public interface AccountRepository extends JpaRepository { + +} diff --git a/spring-boot-samples/spring-boot-sample-jta-bitronix/src/main/java/sample/bitronix/AccountService.java b/spring-boot-samples/spring-boot-sample-jta-bitronix/src/main/java/sample/bitronix/AccountService.java new file mode 100644 index 0000000000..752adf7611 --- /dev/null +++ b/spring-boot-samples/spring-boot-sample-jta-bitronix/src/main/java/sample/bitronix/AccountService.java @@ -0,0 +1,47 @@ +/* + * 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 sample.bitronix; + +import javax.transaction.Transactional; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.jms.core.JmsTemplate; +import org.springframework.stereotype.Service; + +@Service +@Transactional +public class AccountService { + + private final JmsTemplate jmsTemplate; + + private final AccountRepository accountRepository; + + @Autowired + public AccountService(JmsTemplate jmsTemplate, AccountRepository accountRepository) { + this.jmsTemplate = jmsTemplate; + this.accountRepository = accountRepository; + } + + public void createAccountAndNotify(String username) { + this.jmsTemplate.convertAndSend("accounts", username); + this.accountRepository.save(new Account(username)); + if ("error".equals(username)) { + throw new RuntimeException("Simulated error"); + } + } + +} diff --git a/spring-boot-samples/spring-boot-sample-jta-bitronix/src/main/java/sample/bitronix/Messages.java b/spring-boot-samples/spring-boot-sample-jta-bitronix/src/main/java/sample/bitronix/Messages.java new file mode 100644 index 0000000000..cdaddd76b6 --- /dev/null +++ b/spring-boot-samples/spring-boot-sample-jta-bitronix/src/main/java/sample/bitronix/Messages.java @@ -0,0 +1,30 @@ +/* + * 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 sample.bitronix; + +import org.springframework.jms.annotation.JmsListener; +import org.springframework.stereotype.Component; + +@Component +public class Messages { + + @JmsListener(destination = "accounts") + public void onMessage(String content) { + System.out.println("----> " + content); + } + +} diff --git a/spring-boot-samples/spring-boot-sample-jta-bitronix/src/main/java/sample/bitronix/SampleBitronixApplication.java b/spring-boot-samples/spring-boot-sample-jta-bitronix/src/main/java/sample/bitronix/SampleBitronixApplication.java new file mode 100644 index 0000000000..62fb930224 --- /dev/null +++ b/spring-boot-samples/spring-boot-sample-jta-bitronix/src/main/java/sample/bitronix/SampleBitronixApplication.java @@ -0,0 +1,50 @@ +/* + * 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 sample.bitronix; + +import java.io.Closeable; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.context.ApplicationContext; +import org.springframework.context.annotation.ComponentScan; +import org.springframework.context.annotation.Configuration; + +@Configuration +@ComponentScan +@EnableAutoConfiguration +public class SampleBitronixApplication { + + public static void main(String[] args) throws Exception { + ApplicationContext context = SpringApplication.run( + SampleBitronixApplication.class, args); + AccountService service = context.getBean(AccountService.class); + AccountRepository repository = context.getBean(AccountRepository.class); + service.createAccountAndNotify("josh"); + System.out.println("Count is " + repository.count()); + try { + service.createAccountAndNotify("error"); + } + catch (Exception ex) { + System.out.println(ex.getMessage()); + } + System.out.println("Count is " + repository.count()); + Thread.sleep(100); + ((Closeable) context).close(); + } + +} diff --git a/spring-boot-samples/spring-boot-sample-jta-bitronix/src/main/resources/application.properties b/spring-boot-samples/spring-boot-sample-jta-bitronix/src/main/resources/application.properties new file mode 100644 index 0000000000..ca501b18cc --- /dev/null +++ b/spring-boot-samples/spring-boot-sample-jta-bitronix/src/main/resources/application.properties @@ -0,0 +1,3 @@ +spring.hornetq.mode=embedded +spring.hornetq.embedded.enabled=true +spring.hornetq.embedded.queues=accounts diff --git a/spring-boot-samples/spring-boot-sample-jta-bitronix/src/test/java/sample/bitronix/SampleBitronixApplicationTests.java b/spring-boot-samples/spring-boot-sample-jta-bitronix/src/test/java/sample/bitronix/SampleBitronixApplicationTests.java new file mode 100644 index 0000000000..096d1719e2 --- /dev/null +++ b/spring-boot-samples/spring-boot-sample-jta-bitronix/src/test/java/sample/bitronix/SampleBitronixApplicationTests.java @@ -0,0 +1,73 @@ +/* + * 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 sample.bitronix; + +import org.hamcrest.Matcher; +import org.hamcrest.core.SubstringMatcher; +import org.junit.Rule; +import org.junit.Test; +import org.springframework.boot.test.OutputCapture; + +import sample.bitronix.SampleBitronixApplication; + +import static org.hamcrest.Matchers.containsString; +import static org.junit.Assert.assertThat; + +/** + * Basic integration tests for demo application. + * + * @author Phillip Webb + */ +public class SampleBitronixApplicationTests { + + @Rule + public OutputCapture outputCapture = new OutputCapture(); + + @Test + public void testTransactionRollback() throws Exception { + SampleBitronixApplication.main(new String[] {}); + String expected = ""; + expected += "----> josh\n"; + expected += "Count is 1\n"; + expected += "Simulated error\n"; + expected += "Count is 1\n"; + assertThat(this.outputCapture.toString(), containsString(expected)); + assertThat(this.outputCapture.toString(), containsStringOnce("---->")); + } + + private Matcher containsStringOnce(String s) { + return new SubstringMatcher(s) { + + @Override + protected String relationship() { + return "containing once"; + } + + @Override + protected boolean evalSubstringOf(String s) { + int i = 0; + while (s.contains(this.substring)) { + s = s.substring(s.indexOf(this.substring) + this.substring.length()); + i++; + } + return i == 1; + } + + }; + } + +} diff --git a/spring-boot-samples/spring-boot-sample-jta-jndi/README.adoc b/spring-boot-samples/spring-boot-sample-jta-jndi/README.adoc new file mode 100644 index 0000000000..3e4addb82e --- /dev/null +++ b/spring-boot-samples/spring-boot-sample-jta-jndi/README.adoc @@ -0,0 +1,148 @@ +## Introduction + +This application is intended to run inside of a Java EE application server such as +JBoss Wildfly. It demonstrates Spring Boot's auto-configuration defaulting for a +container-managed `TransactionManager` and `DataSource`. This example unfortunately +requires a fully configured Wildfly installation. You'll need to configure a PostgreSQL +XA `DataSource` and an XA `ConnectionFactory` in the Java EE application server's +JNDI machinery. + +## Setup + +### Postgres +We will use postgres as the underlying database, v9.3.5 or above is recommend. Follow +the installation instructions from http://www.postgresql.org/[postgresql.org] or use +a package manager to install the appropriate binaries. + +Once installed you will need to initialize and start the server. + +[source,indent=0] +---- + $ initdb /usr/local/var/postgres -E utf8 + $ pg_ctl -D /usr/local/var/postgres -l /usr/local/var/postgres/server.log start +---- + +With the server running you can create a user and a database: + +[source,indent=0] +---- + $ createuser springboot + $ createdb bootdemo +---- + +Finally you can type `psql bootdemo` to configure a password: + +[source,indent=0] +---- + ALTER USER springboot WITH PASSWORD 'springboot'; + \q +---- + + +### WildFly 8.1 +Download an install WildFly 8.1 from http://wildfly.org/downloads/[wildfly.org]. Once +installed you will need to add a management user by running `$JBOSS_HOME/bin/add-user.sh` +(see the WildFly documentation for details). + +You will also need to add a postgresql module. The following commands setup the basic +structure: + +[source,indent=0] +---- + $ cd $JBOSS_HOME + mkdir -p modules/org/postgresql/main + wget http://jdbc.postgresql.org/download/postgresql-9.3-1102.jdbc41.jar + mv postgresql-9.3-1102.jdbc41.jar modules/org/postgresql/main +---- + +You can then add the following to `$JBOSS_HOME/modules/org/postgresql/main/module.xml`: + +[source,indent=0] +---- + + + + + + + + + + +---- + +## Configuration +A custom WildFly configuration is required for the XA `DataSource` and `ConnectionFactory` +elements. The `$JBOSS_HOME/standalone/configuration/standalone-full.xml` is a good +starting point, copy this file to +`$JBOSS_HOME/standalone/configuration/standalone-boot-demo.xml` then make the following +changes. + +### DataSource +You need to register a PostgreSQL XA `Driver` and then configure an `xa-datasource`. + +Here's a complete listing of the `xa-datasource` contribution to the `datasources` +element, and the `driver` contribution to the `drivers` element to configure a PostgreSQL +DB connection to localhost. +https://access.redhat.com/documentation/en-US/JBoss_Enterprise_Application_Platform/6/html-single/Administration_and_Configuration_Guide/index.html#Install_a_JDBC_Driver_with_the_Management_Console[You can learn more from the documentation]. + +[source,xml,indent=0,subs="verbatim,attributes"] +---- + + ... + + jdbc:postgresql://localhost:5432/crm + postgres + + 10 + 20 + true + + + springboot + springboot + + + + ... + + org.postgresql.xa.PGXADataSource + + + +---- + +### JMS Destination +You will also need to configure a `javax.jms.Destination` by contributing the following to +the `hornetq-server` element: + +[source,xml,indent=0,subs="verbatim,attributes"] +---- + + + + + ... + +---- + + +## Running and deploying the sample +Run Wildfly with the following command: + +[source,indent=0] +---- + $JBOSS_HOME/bin/standalone.sh -c standalone-boot-demo.xml +---- + +Once running you can deploy the application by copying +`target/spring-boot-sample-jta-jndi.war` to `$JBOSS_HOME/standalone/deployments`. + +Open a browser to http://localhost:8080/spring-boot-sample-jta-jndi to trigger the +sample. You should see the current count (it will increment by one on each refresh). If +you check the logs you should see a `----> Josh` message and some counts. Notice how the +`error` message triggers an exception with causes both the database insert and the JMS +message to be rolled back. diff --git a/spring-boot-samples/spring-boot-sample-jta-jndi/pom.xml b/spring-boot-samples/spring-boot-sample-jta-jndi/pom.xml new file mode 100644 index 0000000000..73a54e864e --- /dev/null +++ b/spring-boot-samples/spring-boot-sample-jta-jndi/pom.xml @@ -0,0 +1,61 @@ + + + 4.0.0 + + + org.springframework.boot + spring-boot-samples + 1.2.0.BUILD-SNAPSHOT + + spring-boot-sample-jta-jndi + Spring Boot JNDI JTA Sample + war + Spring Boot JNDI JTA Sample + http://projects.spring.io/spring-boot/ + + Pivotal Software, Inc. + http://www.spring.io + + + ${basedir}/../.. + + + + org.springframework + spring-jms + + + org.springframework.boot + spring-boot-starter-data-jpa + + + org.springframework.boot + spring-boot-starter-web + + + org.springframework.boot + spring-boot-starter-tomcat + + + + + javax.servlet + javax.servlet-api + provided + + + javax.jms + jms-api + provided + + + org.springframework.boot + spring-boot-starter-test + test + + + + spring-boot-sample-jta-jndi + + diff --git a/spring-boot-samples/spring-boot-sample-jta-jndi/src/main/java/sample/jndi/Account.java b/spring-boot-samples/spring-boot-sample-jta-jndi/src/main/java/sample/jndi/Account.java new file mode 100644 index 0000000000..c55d0e30cf --- /dev/null +++ b/spring-boot-samples/spring-boot-sample-jta-jndi/src/main/java/sample/jndi/Account.java @@ -0,0 +1,43 @@ +/* + * 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 sample.jndi; + +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.Id; + +@Entity +public class Account { + + @Id + @GeneratedValue + private Long id; + + private String username; + + Account() { + } + + public Account(String username) { + this.username = username; + } + + public String getUsername() { + return this.username; + } + +} diff --git a/spring-boot-samples/spring-boot-sample-jta-jndi/src/main/java/sample/jndi/AccountRepository.java b/spring-boot-samples/spring-boot-sample-jta-jndi/src/main/java/sample/jndi/AccountRepository.java new file mode 100644 index 0000000000..350e87af25 --- /dev/null +++ b/spring-boot-samples/spring-boot-sample-jta-jndi/src/main/java/sample/jndi/AccountRepository.java @@ -0,0 +1,23 @@ +/* + * 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 sample.jndi; + +import org.springframework.data.repository.CrudRepository; + +public interface AccountRepository extends CrudRepository { + +} diff --git a/spring-boot-samples/spring-boot-sample-jta-jndi/src/main/java/sample/jndi/AccountService.java b/spring-boot-samples/spring-boot-sample-jta-jndi/src/main/java/sample/jndi/AccountService.java new file mode 100644 index 0000000000..f297d1572a --- /dev/null +++ b/spring-boot-samples/spring-boot-sample-jta-jndi/src/main/java/sample/jndi/AccountService.java @@ -0,0 +1,47 @@ +/* + * 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 sample.jndi; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.jms.core.JmsTemplate; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@Transactional +public class AccountService { + + private final JmsTemplate jmsTemplate; + + private final AccountRepository accountRepository; + + @Autowired + public AccountService(JmsTemplate jmsTemplate, AccountRepository accountRepository) { + this.jmsTemplate = jmsTemplate; + this.accountRepository = accountRepository; + } + + public void createAccountAndNotify(String username) { + this.jmsTemplate.convertAndSend("java:/jms/queue/bootdemo", username); + Account entity = new Account(username); + this.accountRepository.save(entity); + if ("error".equals(username)) { + throw new RuntimeException("Simulated error"); + } + } + +} diff --git a/spring-boot-samples/spring-boot-sample-jta-jndi/src/main/java/sample/jndi/Messages.java b/spring-boot-samples/spring-boot-sample-jta-jndi/src/main/java/sample/jndi/Messages.java new file mode 100644 index 0000000000..59bb4e639d --- /dev/null +++ b/spring-boot-samples/spring-boot-sample-jta-jndi/src/main/java/sample/jndi/Messages.java @@ -0,0 +1,30 @@ +/* + * 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 sample.jndi; + +import org.springframework.jms.annotation.JmsListener; +import org.springframework.stereotype.Component; + +@Component +public class Messages { + + @JmsListener(destination = "java:/jms/queue/bootdemo") + public void onMessage(String content) { + System.out.println("----> " + content); + } + +} diff --git a/spring-boot-samples/spring-boot-sample-jta-jndi/src/main/java/sample/jndi/SampleJndiApplication.java b/spring-boot-samples/spring-boot-sample-jta-jndi/src/main/java/sample/jndi/SampleJndiApplication.java new file mode 100644 index 0000000000..2f42628517 --- /dev/null +++ b/spring-boot-samples/spring-boot-sample-jta-jndi/src/main/java/sample/jndi/SampleJndiApplication.java @@ -0,0 +1,28 @@ +/* + * 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 sample.jndi; + +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.context.annotation.ComponentScan; +import org.springframework.context.annotation.Configuration; + +@Configuration +@EnableAutoConfiguration +@ComponentScan +public class SampleJndiApplication { + +} diff --git a/spring-boot-samples/spring-boot-sample-jta-jndi/src/main/java/sample/jndi/SampleJndiInitializer.java b/spring-boot-samples/spring-boot-sample-jta-jndi/src/main/java/sample/jndi/SampleJndiInitializer.java new file mode 100644 index 0000000000..3314d0e126 --- /dev/null +++ b/spring-boot-samples/spring-boot-sample-jta-jndi/src/main/java/sample/jndi/SampleJndiInitializer.java @@ -0,0 +1,29 @@ +/* + * 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 sample.jndi; + +import org.springframework.boot.builder.SpringApplicationBuilder; +import org.springframework.boot.context.web.SpringBootServletInitializer; + +public class SampleJndiInitializer extends SpringBootServletInitializer { + + @Override + protected SpringApplicationBuilder configure(SpringApplicationBuilder application) { + return application.sources(SampleJndiApplication.class); + } + +} diff --git a/spring-boot-samples/spring-boot-sample-jta-jndi/src/main/java/sample/jndi/WebController.java b/spring-boot-samples/spring-boot-sample-jta-jndi/src/main/java/sample/jndi/WebController.java new file mode 100644 index 0000000000..6b444578f6 --- /dev/null +++ b/spring-boot-samples/spring-boot-sample-jta-jndi/src/main/java/sample/jndi/WebController.java @@ -0,0 +1,51 @@ +/* + * 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 sample.jndi; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +public class WebController { + + private final AccountService service; + + private final AccountRepository repository; + + @Autowired + public WebController(AccountService service, AccountRepository repository) { + this.service = service; + this.repository = repository; + } + + @RequestMapping("/") + public String hello() { + System.out.println("Count is " + this.repository.count()); + this.service.createAccountAndNotify("josh"); + try { + this.service.createAccountAndNotify("error"); + } + catch (Exception ex) { + System.out.println(ex.getMessage()); + } + long count = this.repository.count(); + System.out.println("Count is " + count); + return "Count is " + count; + } + +} diff --git a/spring-boot-samples/spring-boot-sample-jta-jndi/src/main/resources/application.properties b/spring-boot-samples/spring-boot-sample-jta-jndi/src/main/resources/application.properties new file mode 100644 index 0000000000..a4720b1e4d --- /dev/null +++ b/spring-boot-samples/spring-boot-sample-jta-jndi/src/main/resources/application.properties @@ -0,0 +1,5 @@ +spring.jpa.generate-ddl=true +spring.datasource.jndi-name=java:jboss/datasources/bootdemo + +# Workaround SPR-12118 +spring.jpa.open-in-view=false diff --git a/spring-boot-starters/pom.xml b/spring-boot-starters/pom.xml index 6f45c9b2e0..efe5a5b2b4 100644 --- a/spring-boot-starters/pom.xml +++ b/spring-boot-starters/pom.xml @@ -36,6 +36,8 @@ spring-boot-starter-integration spring-boot-starter-jdbc spring-boot-starter-jetty + spring-boot-starter-jta-atomikos + spring-boot-starter-jta-bitronix spring-boot-starter-logging spring-boot-starter-log4j spring-boot-starter-mobile diff --git a/spring-boot-starters/spring-boot-starter-jta-atomikos/pom.xml b/spring-boot-starters/spring-boot-starter-jta-atomikos/pom.xml new file mode 100644 index 0000000000..fe36be77a5 --- /dev/null +++ b/spring-boot-starters/spring-boot-starter-jta-atomikos/pom.xml @@ -0,0 +1,38 @@ + + + 4.0.0 + + org.springframework.boot + spring-boot-starters + 1.2.0.BUILD-SNAPSHOT + + spring-boot-starter-jta-atomikos + Spring Boot Atomikos JTA Starter + Spring Boot Atomikos JTA Starter + http://projects.spring.io/spring-boot/ + + Pivotal Software, Inc. + http://www.spring.io + + + ${basedir}/../.. + + + + org.springframework.boot + spring-boot-starter + + + com.atomikos + transactions-jms + + + com.atomikos + transactions-jta + + + com.atomikos + transactions-jdbc + + + diff --git a/spring-boot-starters/spring-boot-starter-jta-bitronix/pom.xml b/spring-boot-starters/spring-boot-starter-jta-bitronix/pom.xml new file mode 100644 index 0000000000..ced6f76870 --- /dev/null +++ b/spring-boot-starters/spring-boot-starter-jta-bitronix/pom.xml @@ -0,0 +1,39 @@ + + + 4.0.0 + + org.springframework.boot + spring-boot-starters + 1.2.0.BUILD-SNAPSHOT + + spring-boot-starter-jta-bitronix + Spring Boot Bitronix JTA Starter + Spring Boot Bitronix JTA Starter + http://projects.spring.io/spring-boot/ + + Pivotal Software, Inc. + http://www.spring.io + + + ${basedir}/../.. + + + + javax.jms + jms-api + + + javax.transaction + jta + + + org.codehaus.btm + btm + + + org.springframework.boot + spring-boot-starter + + + diff --git a/spring-boot/pom.xml b/spring-boot/pom.xml index 86d4e5f2df..7036d0cb55 100644 --- a/spring-boot/pom.xml +++ b/spring-boot/pom.xml @@ -34,11 +34,31 @@ logback-classic true + + com.atomikos + transactions-jms + true + + + com.atomikos + transactions-jta + true + + + com.atomikos + transactions-jdbc + true + com.fasterxml.jackson.core jackson-databind true + + javax.jms + jms-api + true + javax.servlet javax.servlet-api @@ -69,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/XAConnectionFactoryWrapper.java b/spring-boot/src/main/java/org/springframework/boot/jta/XAConnectionFactoryWrapper.java new file mode 100644 index 0000000000..4680fb5c9a --- /dev/null +++ b/spring-boot/src/main/java/org/springframework/boot/jta/XAConnectionFactoryWrapper.java @@ -0,0 +1,41 @@ +/* + * 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; + +import javax.jms.ConnectionFactory; +import javax.jms.XAConnectionFactory; +import javax.transaction.TransactionManager; + +/** + * Strategy interface used to wrap a JMS {@link XAConnectionFactory} enrolling it with a + * JTA {@link TransactionManager}. + * + * @author Phillip Webb + * @since 1.2.0 + */ +public interface XAConnectionFactoryWrapper { + + /** + * Wrap the specific {@link XAConnectionFactory} and enroll it with a JTA + * {@link TransactionManager}. + * @param connectionFactory the connection factory to wrap + * @return the wrapped connection factory + */ + ConnectionFactory wrapConnectionFactory(XAConnectionFactory connectionFactory) + throws Exception; + +} diff --git a/spring-boot/src/main/java/org/springframework/boot/jta/XADataSourceWrapper.java b/spring-boot/src/main/java/org/springframework/boot/jta/XADataSourceWrapper.java new file mode 100644 index 0000000000..1ee8d818b7 --- /dev/null +++ b/spring-boot/src/main/java/org/springframework/boot/jta/XADataSourceWrapper.java @@ -0,0 +1,40 @@ +/* + * 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; + +import javax.sql.DataSource; +import javax.sql.XADataSource; +import javax.transaction.TransactionManager; + +/** + * Strategy interface used to wrap a JMS {@link XADataSource} enrolling it with a JTA + * {@link TransactionManager}. + * + * @author Phillip Webb + * @since 1.2.0 + */ +public interface XADataSourceWrapper { + + /** + * Wrap the specific {@link XADataSource} and enroll it with a JTA + * {@link TransactionManager}. + * @param dataSource the data source to wrap + * @return the wrapped data source + */ + DataSource wrapDataSource(XADataSource dataSource) throws Exception; + +} diff --git a/spring-boot/src/main/java/org/springframework/boot/jta/atomikos/AtomikosConnectionFactoryBean.java b/spring-boot/src/main/java/org/springframework/boot/jta/atomikos/AtomikosConnectionFactoryBean.java new file mode 100644 index 0000000000..9559ed1b5f --- /dev/null +++ b/spring-boot/src/main/java/org/springframework/boot/jta/atomikos/AtomikosConnectionFactoryBean.java @@ -0,0 +1,54 @@ +/* + * 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.atomikos; + +import org.springframework.beans.factory.BeanNameAware; +import org.springframework.beans.factory.DisposableBean; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.util.StringUtils; + +/** + * Spring friendly version of {@link com.atomikos.jms.AtomikosConnectionFactoryBean}. + * + * @author Phillip Webb + * @since 1.2.0 + */ +public class AtomikosConnectionFactoryBean extends + com.atomikos.jms.AtomikosConnectionFactoryBean implements BeanNameAware, + InitializingBean, DisposableBean { + + private String beanName; + + @Override + public void setBeanName(String name) { + this.beanName = name; + } + + @Override + public void afterPropertiesSet() throws Exception { + if (!StringUtils.hasLength(getUniqueResourceName())) { + setUniqueResourceName(this.beanName); + } + init(); + } + + @Override + public void destroy() throws Exception { + close(); + } + +} diff --git a/spring-boot/src/main/java/org/springframework/boot/jta/atomikos/AtomikosDataSourceBean.java b/spring-boot/src/main/java/org/springframework/boot/jta/atomikos/AtomikosDataSourceBean.java new file mode 100644 index 0000000000..d601aaf37a --- /dev/null +++ b/spring-boot/src/main/java/org/springframework/boot/jta/atomikos/AtomikosDataSourceBean.java @@ -0,0 +1,53 @@ +/* + * 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.atomikos; + +import org.springframework.beans.factory.BeanNameAware; +import org.springframework.beans.factory.DisposableBean; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.util.StringUtils; + +/** + * Spring friendly version of {@link com.atomikos.jdbc.AtomikosDataSourceBean}. + * + * @author Phillip Webb + * @since 1.2.0 + */ +public class AtomikosDataSourceBean extends com.atomikos.jdbc.AtomikosDataSourceBean + implements BeanNameAware, InitializingBean, DisposableBean { + + private String beanName; + + @Override + public void setBeanName(String name) { + this.beanName = name; + } + + @Override + public void afterPropertiesSet() throws Exception { + if (!StringUtils.hasLength(getUniqueResourceName())) { + setUniqueResourceName(this.beanName); + } + init(); + } + + @Override + public void destroy() throws Exception { + close(); + } + +} diff --git a/spring-boot/src/main/java/org/springframework/boot/jta/atomikos/AtomikosDependsOnBeanFactoryPostProcessor.java b/spring-boot/src/main/java/org/springframework/boot/jta/atomikos/AtomikosDependsOnBeanFactoryPostProcessor.java new file mode 100644 index 0000000000..4d0fdc22dd --- /dev/null +++ b/spring-boot/src/main/java/org/springframework/boot/jta/atomikos/AtomikosDependsOnBeanFactoryPostProcessor.java @@ -0,0 +1,113 @@ +/* + * 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.atomikos; + +import java.util.Arrays; +import java.util.Collections; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Set; + +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.beans.factory.config.BeanFactoryPostProcessor; +import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; +import org.springframework.core.Ordered; + +import com.atomikos.icatch.jta.UserTransactionManager; + +/** + * {@link BeanFactoryPostProcessor} to automatically setup the recommended + * {@link BeanDefinition#setDependsOn(String[]) dependsOn} settings for correct Atomikos + * ordering. + * + * @author Phillip Webb + * @since 1.2.0 + */ +public class AtomikosDependsOnBeanFactoryPostProcessor 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( + UserTransactionManager.class, true, false); + for (String transactionManager : transactionManagers) { + addTransactionManagerDependencies(beanFactory, transactionManager); + } + addMessageDrivenContainerDependencies(beanFactory, transactionManagers); + } + + private void addTransactionManagerDependencies( + ConfigurableListableBeanFactory beanFactory, String transactionManager) { + BeanDefinition bean = beanFactory.getBeanDefinition(transactionManager); + Set dependsOn = new LinkedHashSet(asList(bean.getDependsOn())); + int initialSize = dependsOn.size(); + addDependencies(beanFactory, "javax.jms.ConnectionFactory", dependsOn); + addDependencies(beanFactory, "javax.sql.DataSource", dependsOn); + if (dependsOn.size() != initialSize) { + bean.setDependsOn(dependsOn.toArray(new String[dependsOn.size()])); + } + } + + private void addMessageDrivenContainerDependencies( + ConfigurableListableBeanFactory beanFactory, String[] transactionManagers) { + String[] messageDrivenContainers = getBeanNamesForType(beanFactory, + "com.atomikos.jms.extra.MessageDrivenContainer"); + for (String messageDrivenContainer : messageDrivenContainers) { + BeanDefinition bean = beanFactory.getBeanDefinition(messageDrivenContainer); + Set dependsOn = new LinkedHashSet(asList(bean.getDependsOn())); + dependsOn.addAll(asList(transactionManagers)); + bean.setDependsOn(dependsOn.toArray(new String[dependsOn.size()])); + } + } + + private void addDependencies(ConfigurableListableBeanFactory beanFactory, + String type, Set dependsOn) { + dependsOn.addAll(asList(getBeanNamesForType(beanFactory, type))); + } + + private String[] getBeanNamesForType(ConfigurableListableBeanFactory beanFactory, + String type) { + try { + return beanFactory.getBeanNamesForType(Class.forName(type), true, false); + } + catch (ClassNotFoundException ex) { + // Ignore + } + return NO_BEANS; + } + + private List asList(String[] array) { + return (array == null ? Collections. emptyList() : Arrays.asList(array)); + } + + @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/atomikos/AtomikosLoggingLevel.java b/spring-boot/src/main/java/org/springframework/boot/jta/atomikos/AtomikosLoggingLevel.java new file mode 100644 index 0000000000..39e5fca759 --- /dev/null +++ b/spring-boot/src/main/java/org/springframework/boot/jta/atomikos/AtomikosLoggingLevel.java @@ -0,0 +1,43 @@ +/* + * 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.atomikos; + +/** + * Logging levels supported by Atomikos. + * + * @author Phillip Webb + * @see AtomikosProperties + * @since 1.2.0 + */ +public enum AtomikosLoggingLevel { + + /** + * Debug Level. + */ + DEBUG, + + /** + * Info Level. + */ + INFO, + + /** + * Warning Level. + */ + WARN + +} diff --git a/spring-boot/src/main/java/org/springframework/boot/jta/atomikos/AtomikosProperties.java b/spring-boot/src/main/java/org/springframework/boot/jta/atomikos/AtomikosProperties.java new file mode 100644 index 0000000000..5222846035 --- /dev/null +++ b/spring-boot/src/main/java/org/springframework/boot/jta/atomikos/AtomikosProperties.java @@ -0,0 +1,237 @@ +/* + * 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.atomikos; + +import java.util.Map; +import java.util.Properties; +import java.util.TreeMap; + +/** + * Bean friendly variant of Atomikos configuration + * properties. Allows for setter based configuration and is amiable to relaxed data + * binding. + * + * @author Phillip Webb + * @see #asProperties() + * @since 1.2.0 + */ +public class AtomikosProperties { + + private final Map values = new TreeMap(); + + /** + * Specifies the transaction manager implementation that should be started. There is + * no default value and this must be set. Generally, + * {@literal com.atomikos.icatch.standalone.UserTransactionServiceFactory} is the + * value you should set. + * @param service the service + */ + public void setService(String service) { + set("service", service); + } + + /** + * Specifies the maximum timeout (in milliseconds) that can be allowed for + * transactions. Defaults to {@literal 300000}. This means that calls to + * UserTransaction.setTransactionTimeout() with a value higher than configured here + * will be max'ed to this value. + * @param maxTimeout the max timeout + */ + public void setMaxTimeout(long maxTimeout) { + set("max_timeout", maxTimeout); + } + + /** + * The default timeout for JTA transactions (optional, defaults to {@literal 10000} + * ms). + * @param defaultJtaTimeout the default JTA timeout + */ + public void setDefaultJtaTimeout(long defaultJtaTimeout) { + set("default_jta_timeout", defaultJtaTimeout); + } + + /** + * Specifies the maximum number of active transactions. Defaults to {@literal 50}. A + * negative value means infinite amount. You will get an {@code IllegalStateException} + * with error message "Max number of active transactions reached" if you call + * {@code UserTransaction.begin()} while there are already n concurrent transactions + * running, n being this value. + * @param maxActivities the max activities + */ + public void setMaxActives(int maxActivities) { + set("max_actives", maxActivities); + } + + /** + * Specifies if disk logging should be enabled or not. Defaults to true. It is useful + * for JUnit testing, or to profile code without seeing the transaction manager's + * activity as a hot spot but this should never be disabled on production or data + * integrity cannot be guaranteed. + * @param enableLogging if logging is enabled + */ + public void setEnableLogging(boolean enableLogging) { + set("enable_logging", enableLogging); + } + + /** + * Specifies the transaction manager's unique name. Defaults to the machine's IP + * address. If you plan to run more than one transaction manager against one database + * you must set this property to a unique value or you might run into duplicate + * transaction ID (XID) problems that can be quite subtle (example: + * {@literal http://fogbugz.atomikos.com/default.asp?community.6.2225.7}). If multiple + * instances need to use the same properties file then the easiest way to ensure + * uniqueness for this property is by referencing a system property specified at VM + * startup. + * @param uniqueName the unique name + */ + public void setTransactionManagerUniqueName(String uniqueName) { + set("tm_unique_name", uniqueName); + } + + /** + * Specifies if subtransactions should be joined when possible. Defaults to true. When + * false, no attempt to call {@code XAResource.start(TM_JOIN)} will be made for + * different but related subtransctions. This setting has no effect on resource access + * within one and the same transaction. If you don't use subtransactions then this + * setting can be ignored. + * @param serialJtaTransactions if serial JTA transaction are supported + */ + public void setSerialJtaTransactions(boolean serialJtaTransactions) { + set("serial_jta_transactions", serialJtaTransactions); + } + + /** + * Specifies whether VM shutdown should trigger forced shutdown of the transaction + * core. Defaults to false. + * @param forceShutdownOnVmExit + */ + public void setForceShutdownOnVmExit(boolean forceShutdownOnVmExit) { + set("force_shutdown_on_vm_exit", forceShutdownOnVmExit); + } + + /** + * Specifies the transactions log file base name. Defaults to {@literal tmlog}. The + * transactions logs are stored in files using this name appended with a number and + * the extension {@literal .log}. At checkpoint, a new transactions log file is + * created and the number is incremented. + * @param logBaseName the log base name + */ + public void setLogBaseName(String logBaseName) { + set("log_base_name", logBaseName); + } + + /** + * Specifies the directory in which the log files should be stored. Defaults to the + * current working directory. This directory should be a stable storage like a SAN, + * RAID or at least backed up location. The transactions logs files are as important + * as the data themselves to guarantee consistency in case of failures. + * @param logBaseDir the log base dir + */ + public void setLogBaseDir(String logBaseDir) { + set("log_base_dir", logBaseDir); + } + + /** + * Specifies the interval between checkpoints. A checkpoint reduces the log file size + * at the expense of adding some overhead in the runtime. Defaults to {@literal 500}. + * @param checkpointInterval the checkpoint interval + */ + public void setCheckpointInterval(long checkpointInterval) { + set("checkpoint_interval", checkpointInterval); + } + + /** + * Specifies the console log level. Defaults to {@link AtomikosLoggingLevel#WARN}. + * @param consoleLogLevel the console log level + */ + public void setConsoleLogLevel(AtomikosLoggingLevel consoleLogLevel) { + set("console_log_level", consoleLogLevel); + } + + /** + * Specifies the directory in which to store the debug log files. Defaults to the + * current working directory. + * @param outputDir the output dir + */ + public void setOutputDir(String outputDir) { + set("output_dir", outputDir); + } + + /** + * Specifies the debug logs file name. Defaults to {@literal tm.out}. + * @param consoleFileName the console file name + */ + public void setConsoleFileName(String consoleFileName) { + set("console_file_name", consoleFileName); + } + + /** + * Specifies how many debug logs files can be created. Defaults to {@literal 1}. + * @param consoleFileCount the console file count + */ + public void setConsoleFileCount(int consoleFileCount) { + set("console_file_count", consoleFileCount); + } + + /** + * Specifies how many bytes can be stored at most in debug logs files. Defaults to + * {@literal -1}. Negative values means unlimited. + * @param consoleFileLimit the console file limit + */ + public void setConsoleFileLimit(int consoleFileLimit) { + set("console_file_limit", consoleFileLimit); + } + + /** + * Specifies whether or not to use different (and concurrent) threads for two-phase + * commit on the participating resources. Setting this to {@literal true} implies that + * the commit is more efficient since waiting for acknowledgements is done in + * parallel. Defaults to {@literal true}. If you set this to {@literal false}, then + * commits will happen in the order that resources are accessed within the + * transaction. + * @param threadedTwoPhaseCommit if threaded two phase commits should be used + */ + public void setThreadedTwoPhaseCommit(boolean threadedTwoPhaseCommit) { + set("threaded_2pc", threadedTwoPhaseCommit); + } + + private void set(String key, Object value) { + set("com.atomikos.icatch.", key, value); + } + + private void set(String keyPrefix, String key, Object value) { + if (value != null) { + this.values.put(keyPrefix + key, value.toString()); + } + else { + this.values.remove(keyPrefix + key); + } + } + + /** + * Returns the properties as a {@link Properties} object that can be used with + * Atomikos. + * @return the properties + */ + public Properties asProperties() { + Properties properties = new Properties(); + properties.putAll(this.values); + return properties; + } + +} diff --git a/spring-boot/src/main/java/org/springframework/boot/jta/atomikos/AtomikosXAConnectionFactoryWrapper.java b/spring-boot/src/main/java/org/springframework/boot/jta/atomikos/AtomikosXAConnectionFactoryWrapper.java new file mode 100644 index 0000000000..c370bb945b --- /dev/null +++ b/spring-boot/src/main/java/org/springframework/boot/jta/atomikos/AtomikosXAConnectionFactoryWrapper.java @@ -0,0 +1,40 @@ +/* + * 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.atomikos; + +import javax.jms.ConnectionFactory; +import javax.jms.XAConnectionFactory; + +import org.springframework.boot.jta.XAConnectionFactoryWrapper; + +/** + * {@link XAConnectionFactoryWrapper} that uses an {@link AtomikosConnectionFactoryBean} + * to wrap a {@link XAConnectionFactory}. + * + * @author Phillip Webb + * @since 1.2.0 + */ +public class AtomikosXAConnectionFactoryWrapper implements XAConnectionFactoryWrapper { + + @Override + public ConnectionFactory wrapConnectionFactory(XAConnectionFactory connectionFactory) { + AtomikosConnectionFactoryBean bean = new AtomikosConnectionFactoryBean(); + bean.setXaConnectionFactory(connectionFactory); + return bean; + } + +} diff --git a/spring-boot/src/main/java/org/springframework/boot/jta/atomikos/AtomikosXADataSourceWrapper.java b/spring-boot/src/main/java/org/springframework/boot/jta/atomikos/AtomikosXADataSourceWrapper.java new file mode 100644 index 0000000000..28fc7f778b --- /dev/null +++ b/spring-boot/src/main/java/org/springframework/boot/jta/atomikos/AtomikosXADataSourceWrapper.java @@ -0,0 +1,40 @@ +/* + * 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.atomikos; + +import javax.sql.XADataSource; + +import org.springframework.boot.jta.XADataSourceWrapper; + +/** + * {@link XADataSourceWrapper} that uses an {@link AtomikosDataSourceBean} to wrap a + * {@link XADataSource}. + * + * @author Phillip Webb + * @since 1.2.0 + */ +public class AtomikosXADataSourceWrapper implements XADataSourceWrapper { + + @Override + public AtomikosDataSourceBean wrapDataSource(XADataSource dataSource) + throws Exception { + AtomikosDataSourceBean bean = new AtomikosDataSourceBean(); + bean.setXaDataSource(dataSource); + return bean; + } + +} 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/BitronixXAConnectionFactoryWrapper.java b/spring-boot/src/main/java/org/springframework/boot/jta/bitronix/BitronixXAConnectionFactoryWrapper.java new file mode 100644 index 0000000000..6a539c9a0c --- /dev/null +++ b/spring-boot/src/main/java/org/springframework/boot/jta/bitronix/BitronixXAConnectionFactoryWrapper.java @@ -0,0 +1,40 @@ +/* + * 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.jms.XAConnectionFactory; + +import org.springframework.boot.jta.XAConnectionFactoryWrapper; + +/** + * {@link XAConnectionFactoryWrapper} that uses a Bitronix + * {@link PoolingConnectionFactoryBean} to wrap a {@link XAConnectionFactory}. + * + * @author Phillip Webb + * @since 1.2.0 + */ +public class BitronixXAConnectionFactoryWrapper implements XAConnectionFactoryWrapper { + + @Override + public ConnectionFactory wrapConnectionFactory(XAConnectionFactory connectionFactory) { + PoolingConnectionFactoryBean pool = new PoolingConnectionFactoryBean(); + pool.setConnectionFactory(connectionFactory); + return pool; + } + +} diff --git a/spring-boot/src/main/java/org/springframework/boot/jta/bitronix/BitronixXADataSourceWrapper.java b/spring-boot/src/main/java/org/springframework/boot/jta/bitronix/BitronixXADataSourceWrapper.java new file mode 100644 index 0000000000..2edd1a07bf --- /dev/null +++ b/spring-boot/src/main/java/org/springframework/boot/jta/bitronix/BitronixXADataSourceWrapper.java @@ -0,0 +1,39 @@ +/* + * 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.sql.XADataSource; + +import org.springframework.boot.jta.XADataSourceWrapper; + +/** + * {@link XADataSourceWrapper} that uses a Bitronix {@link PoolingDataSourceBean} to wrap + * a {@link XADataSource}. + * + * @author Phillip Webb + * @since 1.2.0 + */ +public class BitronixXADataSourceWrapper implements XADataSourceWrapper { + + @Override + public PoolingDataSourceBean wrapDataSource(XADataSource dataSource) throws Exception { + PoolingDataSourceBean pool = new PoolingDataSourceBean(); + pool.setDataSource(dataSource); + return pool; + } + +} 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/main/java/org/springframework/boot/jta/package-info.java b/spring-boot/src/main/java/org/springframework/boot/jta/package-info.java new file mode 100644 index 0000000000..3ca0cc266b --- /dev/null +++ b/spring-boot/src/main/java/org/springframework/boot/jta/package-info.java @@ -0,0 +1,21 @@ +/* + * 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. + */ + +/** + * Support for the Java Transaction API. + */ +package org.springframework.boot.jta; + diff --git a/spring-boot/src/main/java/org/springframework/boot/orm/jpa/SpringNamingStrategy.java b/spring-boot/src/main/java/org/springframework/boot/orm/jpa/SpringNamingStrategy.java index c0f133e720..a6917f94ce 100644 --- a/spring-boot/src/main/java/org/springframework/boot/orm/jpa/SpringNamingStrategy.java +++ b/spring-boot/src/main/java/org/springframework/boot/orm/jpa/SpringNamingStrategy.java @@ -16,32 +16,17 @@ package org.springframework.boot.orm.jpa; -import org.hibernate.cfg.ImprovedNamingStrategy; import org.hibernate.cfg.NamingStrategy; -import org.hibernate.internal.util.StringHelper; -import org.springframework.util.Assert; -import org.springframework.util.StringUtils; /** * Hibernate {@link NamingStrategy} that follows Spring recommended naming conventions. - * Naming conventions implemented here are identical to {@link ImprovedNamingStrategy} - * with the exception that foreign key columns include the referenced column name. * * @author Phillip Webb - * @see "http://stackoverflow.com/questions/7689206/ejb3namingstrategy-vs-improvednamingstrategy-foreign-key-naming" + * @deprecated Since 1.2.0 in favor of + * {@link org.springframework.boot.orm.jpa.hibernate.SpringNamingStrategy} */ -public class SpringNamingStrategy extends ImprovedNamingStrategy { - - @Override - public String foreignKeyColumnName(String propertyName, String propertyEntityName, - String propertyTableName, String referencedColumnName) { - String name = propertyTableName; - if (propertyName != null) { - name = StringHelper.unqualify(propertyName); - } - Assert.state(StringUtils.hasLength(name), - "Unable to generate foreignKeyColumnName"); - return columnName(name) + "_" + referencedColumnName; - } +@Deprecated +public class SpringNamingStrategy extends + org.springframework.boot.orm.jpa.hibernate.SpringNamingStrategy { } diff --git a/spring-boot/src/main/java/org/springframework/boot/orm/jpa/hibernate/SpringJtaPlatform.java b/spring-boot/src/main/java/org/springframework/boot/orm/jpa/hibernate/SpringJtaPlatform.java new file mode 100644 index 0000000000..66fd6000ad --- /dev/null +++ b/spring-boot/src/main/java/org/springframework/boot/orm/jpa/hibernate/SpringJtaPlatform.java @@ -0,0 +1,60 @@ +/* + * 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.orm.jpa.hibernate; + +import javax.transaction.TransactionManager; +import javax.transaction.UserTransaction; + +import org.hibernate.engine.transaction.jta.platform.internal.AbstractJtaPlatform; +import org.springframework.transaction.jta.JtaTransactionManager; +import org.springframework.util.Assert; + +/** + * Generic Hibernate {@link AbstractJtaPlatform} implementation that simply resolves the + * JTA {@link UserTransaction} and {@link TransactionManager} from the Spring-configured + * {@link JtaTransactionManager} implementation. + * + * @author Josh Long + * @author Phillip Webb + * @since 1.2.0 + */ +public class SpringJtaPlatform extends AbstractJtaPlatform { + + private static final long serialVersionUID = 1L; + + private final JtaTransactionManager transactionManager; + + public SpringJtaPlatform(JtaTransactionManager transactionManager) { + Assert.notNull(transactionManager, "TransactionManager must not be null"); + this.transactionManager = transactionManager; + } + + protected boolean hasTransactionManager() { + return true; + } + + @Override + protected TransactionManager locateTransactionManager() { + return this.transactionManager.getTransactionManager(); + } + + @Override + protected UserTransaction locateUserTransaction() { + return this.transactionManager.getUserTransaction(); + } + +} diff --git a/spring-boot/src/main/java/org/springframework/boot/orm/jpa/hibernate/SpringNamingStrategy.java b/spring-boot/src/main/java/org/springframework/boot/orm/jpa/hibernate/SpringNamingStrategy.java new file mode 100644 index 0000000000..92239e4800 --- /dev/null +++ b/spring-boot/src/main/java/org/springframework/boot/orm/jpa/hibernate/SpringNamingStrategy.java @@ -0,0 +1,48 @@ +/* + * 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.orm.jpa.hibernate; + +import org.hibernate.cfg.ImprovedNamingStrategy; +import org.hibernate.cfg.NamingStrategy; +import org.hibernate.internal.util.StringHelper; +import org.springframework.util.Assert; +import org.springframework.util.StringUtils; + +/** + * Hibernate {@link NamingStrategy} that follows Spring recommended naming conventions. + * Naming conventions implemented here are identical to {@link ImprovedNamingStrategy} + * with the exception that foreign key columns include the referenced column name. + * + * @author Phillip Webb + * @see "http://stackoverflow.com/questions/7689206/ejb3namingstrategy-vs-improvednamingstrategy-foreign-key-naming" + * @since 1.2.0 + */ +public class SpringNamingStrategy extends ImprovedNamingStrategy { + + @Override + public String foreignKeyColumnName(String propertyName, String propertyEntityName, + String propertyTableName, String referencedColumnName) { + String name = propertyTableName; + if (propertyName != null) { + name = StringHelper.unqualify(propertyName); + } + Assert.state(StringUtils.hasLength(name), + "Unable to generate foreignKeyColumnName"); + return columnName(name) + "_" + referencedColumnName; + } + +} diff --git a/spring-boot/src/test/java/org/springframework/boot/jta/atomikos/AtomikosConnectionFactoryBeanTests.java b/spring-boot/src/test/java/org/springframework/boot/jta/atomikos/AtomikosConnectionFactoryBeanTests.java new file mode 100644 index 0000000000..55b7f0bcf7 --- /dev/null +++ b/spring-boot/src/test/java/org/springframework/boot/jta/atomikos/AtomikosConnectionFactoryBeanTests.java @@ -0,0 +1,61 @@ +/* + * 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.atomikos; + +import javax.jms.JMSException; + +import org.junit.Test; + +import static org.hamcrest.Matchers.equalTo; +import static org.junit.Assert.assertThat; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; + +/** + * Tests for {@link AtomikosConnectionFactoryBean}. + * + * @author Phillip Webb + */ +public class AtomikosConnectionFactoryBeanTests { + + @Test + public void beanMethods() throws Exception { + MockAtomikosConnectionFactoryBean bean = spy(new MockAtomikosConnectionFactoryBean()); + bean.setBeanName("bean"); + bean.afterPropertiesSet(); + assertThat(bean.getUniqueResourceName(), equalTo("bean")); + verify(bean).init(); + verify(bean, never()).close(); + bean.destroy(); + verify(bean).close(); + } + + private static class MockAtomikosConnectionFactoryBean extends + AtomikosConnectionFactoryBean { + + @Override + public synchronized void init() throws JMSException { + } + + @Override + public synchronized void close() { + } + + } + +} diff --git a/spring-boot/src/test/java/org/springframework/boot/jta/atomikos/AtomikosDataSourceBeanTests.java b/spring-boot/src/test/java/org/springframework/boot/jta/atomikos/AtomikosDataSourceBeanTests.java new file mode 100644 index 0000000000..10348aa5bf --- /dev/null +++ b/spring-boot/src/test/java/org/springframework/boot/jta/atomikos/AtomikosDataSourceBeanTests.java @@ -0,0 +1,60 @@ +/* + * 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.atomikos; + +import org.junit.Test; + +import com.atomikos.jdbc.AtomikosSQLException; + +import static org.hamcrest.Matchers.equalTo; +import static org.junit.Assert.assertThat; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; + +/** + * Tests for {@link AtomikosDataSourceBean}. + * + * @author Phillip Webb + */ +public class AtomikosDataSourceBeanTests { + + @Test + public void beanMethods() throws Exception { + MockAtomikosDataSourceBean bean = spy(new MockAtomikosDataSourceBean()); + bean.setBeanName("bean"); + bean.afterPropertiesSet(); + assertThat(bean.getUniqueResourceName(), equalTo("bean")); + verify(bean).init(); + verify(bean, never()).close(); + bean.destroy(); + verify(bean).close(); + } + + private static class MockAtomikosDataSourceBean extends AtomikosDataSourceBean { + + @Override + public synchronized void init() throws AtomikosSQLException { + } + + @Override + public synchronized void close() { + } + + } + +} diff --git a/spring-boot/src/test/java/org/springframework/boot/jta/atomikos/AtomikosDependsOnBeanFactoryPostProcessorTests.java b/spring-boot/src/test/java/org/springframework/boot/jta/atomikos/AtomikosDependsOnBeanFactoryPostProcessorTests.java new file mode 100644 index 0000000000..485f46d953 --- /dev/null +++ b/spring-boot/src/test/java/org/springframework/boot/jta/atomikos/AtomikosDependsOnBeanFactoryPostProcessorTests.java @@ -0,0 +1,99 @@ +/* + * 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.atomikos; + +import java.util.Arrays; +import java.util.HashSet; + +import javax.jms.ConnectionFactory; +import javax.sql.DataSource; + +import org.junit.Test; +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import com.atomikos.icatch.jta.UserTransactionManager; +import com.atomikos.jms.extra.MessageDrivenContainer; + +import static org.hamcrest.Matchers.equalTo; +import static org.junit.Assert.assertThat; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.mock; + +/** + * Tests for {@link AtomikosDependsOnBeanFactoryPostProcessor}. + * + * @author Phillip Webb + */ +public class AtomikosDependsOnBeanFactoryPostProcessorTests { + + private AnnotationConfigApplicationContext context; + + @Test + public void setsDependsOn() { + this.context = new AnnotationConfigApplicationContext(Config.class); + assertDependsOn("dataSource"); + assertDependsOn("connectionFactory"); + assertDependsOn("userTransactionManager", "dataSource", "connectionFactory"); + assertDependsOn("messageDrivenContainer", "userTransactionManager"); + this.context.close(); + } + + private void assertDependsOn(String bean, String... expected) { + BeanDefinition definition = this.context.getBeanDefinition(bean); + if (definition.getDependsOn() == null) { + assertTrue("No dependsOn expected for " + bean, expected.length == 0); + return; + } + HashSet dependsOn = new HashSet(Arrays.asList(definition + .getDependsOn())); + assertThat(dependsOn, equalTo(new HashSet(Arrays.asList(expected)))); + } + + @Configuration + static class Config { + + @Bean + public DataSource dataSource() { + return mock(DataSource.class); + } + + @Bean + public ConnectionFactory connectionFactory() { + return mock(ConnectionFactory.class); + } + + @Bean + public UserTransactionManager userTransactionManager() { + return mock(UserTransactionManager.class); + } + + @Bean + public MessageDrivenContainer messageDrivenContainer() { + return mock(MessageDrivenContainer.class); + } + + @Bean + public static AtomikosDependsOnBeanFactoryPostProcessor atomikosPostProcessor() { + return new AtomikosDependsOnBeanFactoryPostProcessor(); + } + + } + +} diff --git a/spring-boot/src/test/java/org/springframework/boot/jta/atomikos/AtomikosPropertiesTests.java b/spring-boot/src/test/java/org/springframework/boot/jta/atomikos/AtomikosPropertiesTests.java new file mode 100644 index 0000000000..3060a60cb3 --- /dev/null +++ b/spring-boot/src/test/java/org/springframework/boot/jta/atomikos/AtomikosPropertiesTests.java @@ -0,0 +1,77 @@ +/* + * 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.atomikos; + +import org.junit.Test; + +import static org.hamcrest.Matchers.equalTo; +import static org.junit.Assert.assertThat; + +/** + * Tests for ;@link AtomikosProperties}. + * + * @author Phillip Webb + */ +public class AtomikosPropertiesTests { + + private AtomikosProperties properties = new AtomikosProperties(); + + @Test + public void testProperties() { + this.properties.setService("service"); + this.properties.setMaxTimeout(1L); + this.properties.setDefaultJtaTimeout(2L); + this.properties.setMaxActives(3); + this.properties.setEnableLogging(true); + this.properties.setTransactionManagerUniqueName("uniqueName"); + this.properties.setSerialJtaTransactions(true); + this.properties.setForceShutdownOnVmExit(true); + this.properties.setLogBaseName("logBaseName"); + this.properties.setLogBaseDir("logBaseDir"); + this.properties.setCheckpointInterval(4); + this.properties.setConsoleLogLevel(AtomikosLoggingLevel.WARN); + this.properties.setOutputDir("outputDir"); + this.properties.setConsoleFileName("consoleFileName"); + this.properties.setConsoleFileCount(5); + this.properties.setConsoleFileLimit(6); + this.properties.setThreadedTwoPhaseCommit(true); + + assertThat(this.properties.asProperties().size(), equalTo(17)); + assertProperty("com.atomikos.icatch.service", "service"); + assertProperty("com.atomikos.icatch.max_timeout", "1"); + assertProperty("com.atomikos.icatch.default_jta_timeout", "2"); + assertProperty("com.atomikos.icatch.max_actives", "3"); + assertProperty("com.atomikos.icatch.enable_logging", "true"); + assertProperty("com.atomikos.icatch.tm_unique_name", "uniqueName"); + assertProperty("com.atomikos.icatch.serial_jta_transactions", "true"); + assertProperty("com.atomikos.icatch.force_shutdown_on_vm_exit", "true"); + assertProperty("com.atomikos.icatch.log_base_name", "logBaseName"); + assertProperty("com.atomikos.icatch.log_base_dir", "logBaseDir"); + assertProperty("com.atomikos.icatch.checkpoint_interval", "4"); + assertProperty("com.atomikos.icatch.console_log_level", "WARN"); + assertProperty("com.atomikos.icatch.output_dir", "outputDir"); + assertProperty("com.atomikos.icatch.console_file_name", "consoleFileName"); + assertProperty("com.atomikos.icatch.console_file_count", "5"); + assertProperty("com.atomikos.icatch.console_file_limit", "6"); + assertProperty("com.atomikos.icatch.threaded_2pc", "true"); + } + + private void assertProperty(String key, String value) { + assertThat(this.properties.asProperties().getProperty(key), equalTo(value)); + } + +} diff --git a/spring-boot/src/test/java/org/springframework/boot/jta/atomikos/AtomikosXAConnectionFactoryWrapperTests.java b/spring-boot/src/test/java/org/springframework/boot/jta/atomikos/AtomikosXAConnectionFactoryWrapperTests.java new file mode 100644 index 0000000000..3745522fd6 --- /dev/null +++ b/spring-boot/src/test/java/org/springframework/boot/jta/atomikos/AtomikosXAConnectionFactoryWrapperTests.java @@ -0,0 +1,46 @@ +/* + * 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.atomikos; + +import javax.jms.ConnectionFactory; +import javax.jms.XAConnectionFactory; + +import org.junit.Test; + +import static org.hamcrest.Matchers.instanceOf; +import static org.hamcrest.Matchers.sameInstance; +import static org.junit.Assert.assertThat; +import static org.mockito.Mockito.mock; + +/** + * Tests for {@link AtomikosXAConnectionFactoryWrapper}. + * + * @author Phillip Webb + */ +public class AtomikosXAConnectionFactoryWrapperTests { + + @Test + public void wrap() { + XAConnectionFactory connectionFactory = mock(XAConnectionFactory.class); + AtomikosXAConnectionFactoryWrapper wrapper = new AtomikosXAConnectionFactoryWrapper(); + ConnectionFactory wrapped = wrapper.wrapConnectionFactory(connectionFactory); + assertThat(wrapped, instanceOf(AtomikosConnectionFactoryBean.class)); + assertThat(((AtomikosConnectionFactoryBean) wrapped).getXaConnectionFactory(), + sameInstance(connectionFactory)); + } + +} diff --git a/spring-boot/src/test/java/org/springframework/boot/jta/atomikos/AtomikosXADataSourceWrapperTests.java b/spring-boot/src/test/java/org/springframework/boot/jta/atomikos/AtomikosXADataSourceWrapperTests.java new file mode 100644 index 0000000000..c2d6e56a5f --- /dev/null +++ b/spring-boot/src/test/java/org/springframework/boot/jta/atomikos/AtomikosXADataSourceWrapperTests.java @@ -0,0 +1,46 @@ +/* + * 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.atomikos; + +import javax.sql.DataSource; +import javax.sql.XADataSource; + +import org.junit.Test; + +import static org.hamcrest.Matchers.instanceOf; +import static org.hamcrest.Matchers.sameInstance; +import static org.junit.Assert.assertThat; +import static org.mockito.Mockito.mock; + +/** + * Tests for {@link AtomikosXADataSourceWrapper}. + * + * @author Phillip Webb + */ +public class AtomikosXADataSourceWrapperTests { + + @Test + public void wrap() throws Exception { + XADataSource dataSource = mock(XADataSource.class); + AtomikosXADataSourceWrapper wrapper = new AtomikosXADataSourceWrapper(); + DataSource wrapped = wrapper.wrapDataSource(dataSource); + assertThat(wrapped, instanceOf(AtomikosDataSourceBean.class)); + assertThat(((AtomikosDataSourceBean) wrapped).getXaDataSource(), + sameInstance(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/BitronixXAConnectionFactoryWrapperTests.java b/spring-boot/src/test/java/org/springframework/boot/jta/bitronix/BitronixXAConnectionFactoryWrapperTests.java new file mode 100644 index 0000000000..fca543971b --- /dev/null +++ b/spring-boot/src/test/java/org/springframework/boot/jta/bitronix/BitronixXAConnectionFactoryWrapperTests.java @@ -0,0 +1,46 @@ +/* + * 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.jms.XAConnectionFactory; + +import org.junit.Test; + +import static org.hamcrest.Matchers.instanceOf; +import static org.hamcrest.Matchers.sameInstance; +import static org.junit.Assert.assertThat; +import static org.mockito.Mockito.mock; + +/** + * Tests for {@link BitronixXAConnectionFactoryWrapper}. + * + * @author Phillip Webb + */ +public class BitronixXAConnectionFactoryWrapperTests { + + @Test + public void wrap() { + XAConnectionFactory connectionFactory = mock(XAConnectionFactory.class); + BitronixXAConnectionFactoryWrapper wrapper = new BitronixXAConnectionFactoryWrapper(); + ConnectionFactory wrapped = wrapper.wrapConnectionFactory(connectionFactory); + assertThat(wrapped, instanceOf(PoolingConnectionFactoryBean.class)); + assertThat(((PoolingConnectionFactoryBean) wrapped).getConnectionFactory(), + sameInstance(connectionFactory)); + } + +} diff --git a/spring-boot/src/test/java/org/springframework/boot/jta/bitronix/BitronixXADataSourceWrapperTests.java b/spring-boot/src/test/java/org/springframework/boot/jta/bitronix/BitronixXADataSourceWrapperTests.java new file mode 100644 index 0000000000..abea432440 --- /dev/null +++ b/spring-boot/src/test/java/org/springframework/boot/jta/bitronix/BitronixXADataSourceWrapperTests.java @@ -0,0 +1,46 @@ +/* + * 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.sql.DataSource; +import javax.sql.XADataSource; + +import org.junit.Test; + +import static org.hamcrest.Matchers.instanceOf; +import static org.hamcrest.Matchers.sameInstance; +import static org.junit.Assert.assertThat; +import static org.mockito.Mockito.mock; + +/** + * Tests for {@link BitronixXADataSourceWrapper}. + * + * @author Phillip Webb + */ +public class BitronixXADataSourceWrapperTests { + + @Test + public void wrap() throws Exception { + XADataSource dataSource = mock(XADataSource.class); + BitronixXADataSourceWrapper wrapper = new BitronixXADataSourceWrapper(); + DataSource wrapped = wrapper.wrapDataSource(dataSource); + assertThat(wrapped, instanceOf(PoolingDataSourceBean.class)); + assertThat(((PoolingDataSourceBean) wrapped).getDataSource(), + sameInstance(dataSource)); + } + +} 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(); + } + +}