SGF-897 - Adapt TransactionListener to enable auto transaction event publishing in the Spring ApplicationContext.

This commit is contained in:
John Blum
2019-11-12 16:09:41 -08:00
parent c1d99ac578
commit 1150e34148
10 changed files with 789 additions and 157 deletions

View File

@@ -885,7 +885,7 @@ public class CacheFactoryBean extends AbstractFactoryBeanSupport<GemFireCache>
* @return the copyOnRead
*/
public Boolean getCopyOnRead() {
return copyOnRead;
return this.copyOnRead;
}
/**
@@ -901,7 +901,7 @@ public class CacheFactoryBean extends AbstractFactoryBeanSupport<GemFireCache>
* @return the criticalHeapPercentage
*/
public Float getCriticalHeapPercentage() {
return criticalHeapPercentage;
return this.criticalHeapPercentage;
}
/**
@@ -956,7 +956,7 @@ public class CacheFactoryBean extends AbstractFactoryBeanSupport<GemFireCache>
* @return the evictionHeapPercentage
*/
public Float getEvictionHeapPercentage() {
return evictionHeapPercentage;
return this.evictionHeapPercentage;
}
/**
@@ -989,7 +989,7 @@ public class CacheFactoryBean extends AbstractFactoryBeanSupport<GemFireCache>
* @return the gatewayConflictResolver
*/
public GatewayConflictResolver getGatewayConflictResolver() {
return gatewayConflictResolver;
return this.gatewayConflictResolver;
}
/**
@@ -1003,7 +1003,7 @@ public class CacheFactoryBean extends AbstractFactoryBeanSupport<GemFireCache>
* @return the list of configured JndiDataSources.
*/
public List<JndiDataSource> getJndiDataSources() {
return jndiDataSources;
return this.jndiDataSources;
}
/**
@@ -1019,7 +1019,7 @@ public class CacheFactoryBean extends AbstractFactoryBeanSupport<GemFireCache>
* @return the lockLease
*/
public Integer getLockLease() {
return lockLease;
return this.lockLease;
}
/**
@@ -1035,7 +1035,7 @@ public class CacheFactoryBean extends AbstractFactoryBeanSupport<GemFireCache>
* @return the lockTimeout
*/
public Integer getLockTimeout() {
return lockTimeout;
return this.lockTimeout;
}
/**
@@ -1054,7 +1054,7 @@ public class CacheFactoryBean extends AbstractFactoryBeanSupport<GemFireCache>
* @return the messageSyncInterval
*/
public Integer getMessageSyncInterval() {
return messageSyncInterval;
return this.messageSyncInterval;
}
/**
@@ -1094,7 +1094,7 @@ public class CacheFactoryBean extends AbstractFactoryBeanSupport<GemFireCache>
* @return the pdxDiskStoreName
*/
public String getPdxDiskStoreName() {
return pdxDiskStoreName;
return this.pdxDiskStoreName;
}
/**
@@ -1111,7 +1111,7 @@ public class CacheFactoryBean extends AbstractFactoryBeanSupport<GemFireCache>
* @return the pdxIgnoreUnreadFields
*/
public Boolean getPdxIgnoreUnreadFields() {
return pdxIgnoreUnreadFields;
return this.pdxIgnoreUnreadFields;
}
/**
@@ -1127,7 +1127,7 @@ public class CacheFactoryBean extends AbstractFactoryBeanSupport<GemFireCache>
* @return the pdxPersistent
*/
public Boolean getPdxPersistent() {
return pdxPersistent;
return this.pdxPersistent;
}
/**
@@ -1144,7 +1144,7 @@ public class CacheFactoryBean extends AbstractFactoryBeanSupport<GemFireCache>
* @return the pdxReadSerialized
*/
public Boolean getPdxReadSerialized() {
return pdxReadSerialized;
return this.pdxReadSerialized;
}
/**
@@ -1162,7 +1162,7 @@ public class CacheFactoryBean extends AbstractFactoryBeanSupport<GemFireCache>
* @return the pdxSerializer
*/
public PdxSerializer getPdxSerializer() {
return pdxSerializer;
return this.pdxSerializer;
}
/**
@@ -1203,7 +1203,7 @@ public class CacheFactoryBean extends AbstractFactoryBeanSupport<GemFireCache>
* @return the searchTimeout
*/
public Integer getSearchTimeout() {
return searchTimeout;
return this.searchTimeout;
}
/**
@@ -1223,7 +1223,7 @@ public class CacheFactoryBean extends AbstractFactoryBeanSupport<GemFireCache>
* @see org.apache.geode.security.SecurityManager
*/
public SecurityManager getSecurityManager() {
return securityManager;
return this.securityManager;
}
/**
@@ -1241,14 +1241,14 @@ public class CacheFactoryBean extends AbstractFactoryBeanSupport<GemFireCache>
* @return the transactionListeners
*/
public List<TransactionListener> getTransactionListeners() {
return transactionListeners;
return this.transactionListeners;
}
/**
* Sets the TransactionWriter used to configure the Cache for handling transaction events, such as to veto
* Sets the {@link TransactionWriter} used to configure the cache for handling transaction events, such as to veto
* the transaction or update an external DB before the commit.
*
* @param transactionWriter the Pivotal GemFire TransactionWriter callback receiving transaction events.
* @param transactionWriter configured {@link TransactionWriter} callback receiving transaction events.
* @see org.apache.geode.cache.TransactionWriter
*/
public void setTransactionWriter(TransactionWriter transactionWriter) {
@@ -1256,10 +1256,13 @@ public class CacheFactoryBean extends AbstractFactoryBeanSupport<GemFireCache>
}
/**
* @return the transactionWriter
* Return the configured {@link TransactionWriter} used to process and handle transaction events.
*
* @return the configured {@link TransactionWriter}.
* @see org.apache.geode.cache.TransactionWriter
*/
public TransactionWriter getTransactionWriter() {
return transactionWriter;
return this.transactionWriter;
}
/**

View File

@@ -187,7 +187,8 @@ public class GemfireTransactionManager extends AbstractPlatformTransactionManage
}
catch (Exception cause) {
throw new NoTransactionException(
"No transaction is associated with the current thread; are multiple transaction managers present?", cause);
"No transaction is associated with the current thread. Are multiple transaction managers present?",
cause);
}
}
@@ -274,7 +275,8 @@ public class GemfireTransactionManager extends AbstractPlatformTransactionManage
/**
* Sets a reference to the {@link GemFireCache} for which this transaction manager
* will manage local cache transactions.
* manages local cache transactions.
*
* @param cache reference to the {@link GemFireCache}.
* @see org.apache.geode.cache.GemFireCache
*/
@@ -284,7 +286,7 @@ public class GemfireTransactionManager extends AbstractPlatformTransactionManage
/**
* Returns a reference to the {@link GemFireCache} for which this transaction manager
* will manage local cache transactions.
* manages local cache transactions.
*
* @return a reference to the {@link GemFireCache}.
* @see org.apache.geode.cache.GemFireCache
@@ -293,6 +295,14 @@ public class GemfireTransactionManager extends AbstractPlatformTransactionManage
return this.cache;
}
/**
* Returns a reference to the {@link CacheTransactionManager} used by Apache Geode to manage local,
* cache transactions.
*
* @return a reference to the {@link CacheTransactionManager}.
* @see org.apache.geode.cache.CacheTransactionManager
* @see #getCache()
*/
protected CacheTransactionManager getCacheTransactionManager() {
return getCache().getCacheTransactionManager();
}

View File

@@ -13,7 +13,6 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.gemfire.transaction.config;
import java.lang.annotation.Documented;
@@ -24,6 +23,7 @@ import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.springframework.context.annotation.Import;
import org.springframework.data.gemfire.transaction.event.TransactionApplicationEvent;
/**
* The {@link EnableGemfireCacheTransactions} annotation enables Pivotal GemFire or Apache Geode Cache Transactions
@@ -49,4 +49,12 @@ import org.springframework.context.annotation.Import;
@SuppressWarnings("unused")
public @interface EnableGemfireCacheTransactions {
/**
* Configures whether {@link TransactionApplicationEvent} objects are automatically fired by the framework.
*
* @return a boolean value indicating whether transactional events are automatically fired by the framework
* without the need to manually publish transaction events. Defaults to {@literal false}.
*/
boolean enableAutoTransactionEventPublishing() default false;
}

View File

@@ -13,14 +13,31 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.gemfire.transaction.config;
import org.apache.geode.cache.GemFireCache;
import java.lang.annotation.Annotation;
import java.util.ArrayList;
import java.util.List;
import org.apache.geode.cache.GemFireCache;
import org.apache.geode.cache.TransactionListener;
import org.apache.geode.cache.TransactionWriter;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.ImportAware;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.AnnotationAttributes;
import org.springframework.core.annotation.Order;
import org.springframework.core.type.AnnotationMetadata;
import org.springframework.data.gemfire.CacheFactoryBean;
import org.springframework.data.gemfire.config.annotation.ClientCacheConfigurer;
import org.springframework.data.gemfire.config.annotation.PeerCacheConfigurer;
import org.springframework.data.gemfire.config.annotation.support.AbstractAnnotationConfigSupport;
import org.springframework.data.gemfire.transaction.GemfireTransactionManager;
import org.springframework.data.gemfire.transaction.event.ComposableTransactionWriter;
import org.springframework.data.gemfire.transaction.event.TransactionListenerAdapter;
import org.springframework.transaction.annotation.EnableTransactionManagement;
/**
@@ -30,16 +47,47 @@ import org.springframework.transaction.annotation.EnableTransactionManagement;
*
* @author John Blum
* @see org.apache.geode.cache.GemFireCache
* @see org.apache.geode.cache.TransactionListener
* @see org.apache.geode.cache.TransactionWriter
* @see org.springframework.context.ApplicationEventPublisher
* @see org.springframework.context.annotation.Bean
* @see org.springframework.context.annotation.Configuration
* @see org.springframework.context.annotation.ImportAware
* @see org.springframework.core.annotation.AnnotationAttributes
* @see org.springframework.core.type.AnnotatedTypeMetadata
* @see org.springframework.data.gemfire.CacheFactoryBean
* @see org.springframework.data.gemfire.config.annotation.ClientCacheConfigurer
* @see org.springframework.data.gemfire.config.annotation.PeerCacheConfigurer
* @see org.springframework.data.gemfire.config.annotation.support.AbstractAnnotationConfigSupport
* @see org.springframework.data.gemfire.transaction.GemfireTransactionManager
* @see org.springframework.data.gemfire.transaction.event.ComposableTransactionWriter
* @see org.springframework.data.gemfire.transaction.event.TransactionListenerAdapter
* @see org.springframework.transaction.annotation.EnableTransactionManagement
* @since 2.0.0
*/
@Configuration
@EnableTransactionManagement
@SuppressWarnings("unused")
public class GemfireCacheTransactionsConfiguration {
public class GemfireCacheTransactionsConfiguration extends AbstractAnnotationConfigSupport implements ImportAware {
private volatile boolean enableAutoTransactionEventPublishing;
@Override
protected Class<? extends Annotation> getAnnotationType() {
return EnableGemfireCacheTransactions.class;
}
@Override
public void setImportMetadata(AnnotationMetadata annotationMetadata) {
if (isAnnotationPresent(annotationMetadata)) {
AnnotationAttributes enableGemfireCacheTransactionsAttributes = getAnnotationAttributes(annotationMetadata);
this.enableAutoTransactionEventPublishing =
enableGemfireCacheTransactionsAttributes.getBoolean("enableAutoTransactionEventPublishing");
}
}
/**
* Declares and registers SDG's {@link GemfireTransactionManager} as the {@literal transactionManager}
@@ -54,4 +102,67 @@ public class GemfireCacheTransactionsConfiguration {
public GemfireTransactionManager transactionManager(GemFireCache gemfireCache) {
return new GemfireTransactionManager(gemfireCache);
}
@Bean
@Order(Ordered.LOWEST_PRECEDENCE)
public ClientCacheConfigurer registerTransactionListenerAdapterClientCacheConfigurer(
ApplicationEventPublisher applicationEventPublisher) {
return (beanName, bean) -> {
TransactionListenerAdapter transactionListener = newTransactionListenerAdapter(applicationEventPublisher);
registerGemFireCacheTransactionEventHandlers(bean, transactionListener);
};
}
@Bean
@Order(Ordered.LOWEST_PRECEDENCE)
public PeerCacheConfigurer registerTransactionListenerAdapterPeerCacheConfigurer(
ApplicationEventPublisher applicationEventPublisher) {
return (beanName, bean) -> {
TransactionListenerAdapter transactionListener = newTransactionListenerAdapter(applicationEventPublisher);
registerGemFireCacheTransactionEventHandlers(bean, transactionListener);
};
}
private TransactionListenerAdapter newTransactionListenerAdapter(
ApplicationEventPublisher applicationEventPublisher) {
return new TransactionListenerAdapter(applicationEventPublisher);
}
protected void registerGemFireCacheTransactionEventHandlers(CacheFactoryBean cacheFactoryBean,
TransactionListenerAdapter transactionListener) {
if (this.enableAutoTransactionEventPublishing) {
registerGemFireCacheTransactionListener(cacheFactoryBean, transactionListener);
registerGemFireCacheTransactionWriter(cacheFactoryBean, transactionListener);
}
}
private void registerGemFireCacheTransactionListener(CacheFactoryBean bean,
TransactionListener transactionListener) {
List<TransactionListener> transactionListeners = new ArrayList<>(bean.getTransactionListeners());
transactionListeners.add(transactionListener);
bean.setTransactionListeners(transactionListeners);
}
private void registerGemFireCacheTransactionWriter(CacheFactoryBean bean,
TransactionWriter transactionWriter) {
TransactionWriter existingTransactionWriter = bean.getTransactionWriter();
TransactionWriter compositeTransactionWriter =
ComposableTransactionWriter.compose(existingTransactionWriter, transactionWriter);
bean.setTransactionWriter(compositeTransactionWriter);
}
}

View File

@@ -0,0 +1,107 @@
/*
* Copyright 2019 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
*
* https://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.data.gemfire.transaction.event;
import java.util.Properties;
import org.apache.geode.cache.Cache;
import org.apache.geode.cache.TransactionEvent;
import org.apache.geode.cache.TransactionWriter;
import org.apache.geode.cache.TransactionWriterException;
import org.springframework.util.Assert;
/**
* An implementation of Apache Geode's {@link TransactionWriter} interface that uses the {@literal Composite Software Design}
* Pattern to compose multiple {@link TransactionWriter} objects into a single instance.
*
* @author John Blum
* @see org.apache.geode.cache.TransactionWriter
* @since 2.3.0
*/
public class ComposableTransactionWriter implements TransactionWriter {
/**
* Factory method used to construct and compose 2 {@link TransactionWriter} objects into a composite instance of
* {@link TransactionWriter} functioning as a single instance.
*
* @param transactionWriterOne first {@link TransactionWriter} in the composition.
* @param transactionWriterTwo second {@link TransactionWriter} in the composition.
* @return the first {@link TransactionWriter} if the second {@link TransactionWriter} is {@literal null}, or return
* the second {@link TransactionWriter} if the first {@link TransactionWriter} is {@literal null}, or return
* the composition of both {@link TransactionWriter} one and {@link TransactionWriter} two.
* @see org.apache.geode.cache.TransactionWriter
*/
public static TransactionWriter compose(TransactionWriter transactionWriterOne,
TransactionWriter transactionWriterTwo) {
return transactionWriterOne == null ? transactionWriterTwo
: transactionWriterTwo == null ? transactionWriterOne
: new ComposableTransactionWriter(transactionWriterOne, transactionWriterTwo);
}
private final TransactionWriter transactionWriterOne;
private final TransactionWriter transactionWriterTwo;
private ComposableTransactionWriter(TransactionWriter transactionWriterOne, TransactionWriter transactionWriterTwo) {
Assert.notNull(transactionWriterOne, "TransactionWriter one must not be null");
Assert.notNull(transactionWriterTwo, "TransactionWriter two must not be null");
this.transactionWriterOne = transactionWriterOne;
this.transactionWriterTwo = transactionWriterTwo;
}
/**
* @inheritDoc
*/
@Override
public void beforeCommit(TransactionEvent event) throws TransactionWriterException {
this.transactionWriterOne.beforeCommit(event);
this.transactionWriterTwo.beforeCommit(event);
}
/**
* @inheritDoc
*/
@Override
public void close() {
this.transactionWriterOne.close();
this.transactionWriterTwo.close();
}
/**
* @inheritDoc
*/
@Override
public void init(Properties properties) {
this.transactionWriterOne.init(properties);
this.transactionWriterTwo.init(properties);
}
/**
* @inheritDoc
*/
@Override
public void initialize(Cache cache, Properties properties) {
this.transactionWriterOne.initialize(cache, properties);
this.transactionWriterTwo.initialize(cache, properties);
}
}

View File

@@ -37,12 +37,37 @@ public class TransactionApplicationEvent extends ApplicationEvent {
protected static final String TIMESTAMP_PATTERN = "yyyy-MM-dd-hh:mm:ss.S";
/**
* Factory method used to construct a new instance of {@link TransactionApplicationEvent} initialized with
* the given {@link Object source}.
*
* @param source {@link Object} defined as the source of this {@link TransactionApplicationEvent}.
* @return a new instance of {@link TransactionApplicationEvent}.
* @see #TransactionApplicationEvent(Object)
*/
public static TransactionApplicationEvent of(Object source) {
return new TransactionApplicationEvent(source);
}
private String details;
/**
* Constructs a new instance of {@link TransactionApplicationEvent} initialized with the given {@link Object source}
* of this transaction event.
*
* @param source {@link Object} defined as the source of this {@link TransactionApplicationEvent}.
*/
public TransactionApplicationEvent(Object source) {
this(source, null);
}
/**
* Constructs a new instance of {@link TransactionApplicationEvent} initialized with the given {@link Object source}
* of this transaction event and {@link String details} describing the transaction event.
*
* @param source {@link Object} defined as the source of this {@link TransactionApplicationEvent}.
* @param details {@link String} describing the transaction event.
*/
public TransactionApplicationEvent(Object source, String details) {
super(source);
@@ -73,4 +98,9 @@ public class TransactionApplicationEvent extends ApplicationEvent {
.map(details -> String.format("%s - %s", getTimestampAsString(), details))
.orElse(String.format("%s[%s]", getClass().getSimpleName(), getTimestampAsString()));
}
public TransactionApplicationEvent with(String details) {
this.details = details;
return this;
}
}

View File

@@ -0,0 +1,95 @@
/*
* Copyright 2019 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
*
* https://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.data.gemfire.transaction.event;
import org.apache.geode.cache.TransactionEvent;
import org.apache.geode.cache.TransactionListener;
import org.apache.geode.cache.TransactionWriter;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.util.Assert;
/**
* The {@link TransactionListenerAdapter} class is an Apache Geode {@link TransactionListener}
* and {@link TransactionWriter} implementation that publishes the {@link TransactionEvent} to application components
* and beans declared in the Spring {@link ApplicationContext} using the {@link ApplicationEventPublisher}.
*
* @author John Blum
* @see org.apache.geode.cache.TransactionEvent
* @see org.apache.geode.cache.TransactionListener
* @see org.apache.geode.cache.TransactionWriter
* @see org.springframework.context.ApplicationContext
* @see org.springframework.context.ApplicationEventPublisher
* @since 2.3.0
*/
public class TransactionListenerAdapter implements TransactionListener, TransactionWriter {
private final ApplicationEventPublisher applicationEventPublisher;
/**
* Constructs a new instance of the {@link TransactionListenerAdapter} initialized with the required
* {@link ApplicationEventPublisher} to publish Apache Geode cache {@link TransactionEvent TransactionEvents}
* to application declared components and beans in a Spring {@link ApplicationContext}.
*
* @param applicationEventPublisher {@link ApplicationEventPublisher} used to publish Apache Geode cache
* {@link TransactionEvent TransactionEvents}.
* @throws IllegalArgumentException if the {@link ApplicationEventPublisher} is {@literal null}.
* @see org.springframework.context.ApplicationEventPublisher
*/
public TransactionListenerAdapter(ApplicationEventPublisher applicationEventPublisher) {
Assert.notNull(applicationEventPublisher, "ApplicationEventPublisher must not be null");
this.applicationEventPublisher = applicationEventPublisher;
}
/**
* @inheritDoc
*/
@Override
public void beforeCommit(TransactionEvent event) {
// NOTE: this will not work because Apache Geode's cache before commit transaction event is only triggered
// after Spring's AbstractPlatformTransaction.triggerBeforeCommit(:TransactionStatus) method, which is where
// all application @TransactionalEventListener(phase = TransactionPhase.BEFORE_COMMIT) annotated transaction
// event handler methods are invoked.
//this.applicationEventPublisher.publishEvent(TransactionApplicationEvent.of(event));
}
/**
* @inheritDoc
*/
@Override
public void afterCommit(TransactionEvent event) {
this.applicationEventPublisher.publishEvent(TransactionApplicationEvent.of(event));
}
/**
* @inheritDoc
*/
@Override
public void afterFailedCommit(TransactionEvent event) { }
/**
* @inheritDoc
*/
@Override
public void afterRollback(TransactionEvent event) {
this.applicationEventPublisher.publishEvent(TransactionApplicationEvent.of(event));
}
}

View File

@@ -0,0 +1,207 @@
/*
* Copyright 2019 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
*
* https://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.data.gemfire.transaction;
import static org.assertj.core.api.Assertions.assertThat;
import static org.springframework.data.gemfire.util.RuntimeExceptionFactory.newIllegalStateException;
import java.io.Serializable;
import org.junit.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.annotation.Id;
import org.springframework.data.gemfire.mapping.GemfireMappingContext;
import org.springframework.data.gemfire.mapping.annotation.Region;
import org.springframework.data.gemfire.repository.support.GemfireRepositoryFactoryBean;
import org.springframework.data.gemfire.transaction.event.TransactionApplicationEvent;
import org.springframework.data.repository.CrudRepository;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.Assert;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NonNull;
import lombok.RequiredArgsConstructor;
import lombok.ToString;
/**
* {@link AbstractTransactionalEventListenerIntegrationTests} is an abstract base class for writing transactional,
* Apache Geode cache event listener tests.
*
* @author John Blum
* @see org.springframework.context.ApplicationEventPublisher
* @see org.springframework.context.annotation.Bean
* @see org.springframework.context.annotation.Configuration
* @see org.springframework.data.gemfire.transaction.event.TransactionApplicationEvent
* @see org.springframework.transaction.annotation.Transactional
* @since 2.3.0
*/
@SuppressWarnings("unused")
public abstract class AbstractTransactionalEventListenerIntegrationTests {
protected static final String GEMFIRE_LOG_LEVEL = "error";
@Autowired
private CustomerService customerService;
protected abstract void assertTransactionEventListenerOnSuccess() throws Exception;
protected abstract void assertTransactionEventListenerOnFailure() throws Exception;
@Test
public void successfulEntityTransactionTriggersCommitTransactionEvents() throws Exception {
Customer jonDoe = this.customerService.save(Customer.newCustomer(1L, "Jon Doe"));
Customer jonDoeLoaded = this.customerService.findById(jonDoe.getId());
assertThat(jonDoeLoaded).isEqualTo(jonDoe);
assertTransactionEventListenerOnSuccess();
}
@Test(expected = IllegalStateException.class)
public void failingEntityTransactionTriggersRollbackTransactionEvent() throws Exception {
Customer janeDoe = Customer.newCustomer(2L, "Jane Doe");
try {
this.customerService.saveFailsAndRollsback(janeDoe);
}
catch (IllegalStateException expected) {
assertTransactionEventListenerOnFailure();
assertThat(expected).hasMessage("TEST");
assertThat(expected).hasNoCause();
try {
this.customerService.findById(janeDoe.getId());
}
catch (IllegalStateException alsoExpected) {
assertThat(alsoExpected).hasMessage("No Customer having ID [2] was found");
assertThat(alsoExpected).hasNoCause();
throw alsoExpected;
}
}
}
@Data
@EqualsAndHashCode
@Region("Customers")
@ToString(of = "name")
@RequiredArgsConstructor(staticName = "newCustomer")
static class Customer implements Serializable {
@Id @NonNull
private Long id;
@NonNull
private String name;
}
public interface CustomerRepository extends CrudRepository<Customer, Long> { }
@Configuration
static class CustomerRepositoryConfiguration {
@Bean
GemfireRepositoryFactoryBean<CustomerRepository, Customer, Long> customerRepositoryFactoryBean() {
GemfireRepositoryFactoryBean<CustomerRepository, Customer, Long> customerRepositoryFactoryBean
= new GemfireRepositoryFactoryBean<>(CustomerRepository.class);
customerRepositoryFactoryBean.setGemfireMappingContext(new GemfireMappingContext());
return customerRepositoryFactoryBean;
}
}
@Service
public static class CustomerService {
private final CustomerRepository customerRepository;
public CustomerService(CustomerRepository customerRepository) {
Assert.notNull(customerRepository, "CustomerRepository is required");
this.customerRepository = customerRepository;
}
public Customer findById(Long id) {
return this.customerRepository.findById(id)
.orElseThrow(() -> newIllegalStateException("No Customer having ID [%d] was found", id));
}
@Transactional
public Customer save(Customer customer) {
return this.customerRepository.save(customer);
}
@Transactional
public Customer saveFailsAndRollsback(Customer customer) {
this.customerRepository.save(customer);
throw newIllegalStateException("TEST");
}
}
@Service
public static class TransactionEventPublishingCustomerService extends CustomerService {
private final ApplicationEventPublisher eventPublisher;
public TransactionEventPublishingCustomerService(ApplicationEventPublisher eventPublisher,
CustomerRepository customerRepository) {
super(customerRepository);
Assert.notNull(eventPublisher, "ApplicationEventPublisher is required");
this.eventPublisher = eventPublisher;
}
@Override
@Transactional
public Customer save(Customer customer) {
customer = super.save(customer);
this.eventPublisher.publishEvent(new TransactionApplicationEvent(this,
String.valueOf(customer.getId())));
return customer;
}
@Override
@Transactional
public Customer saveFailsAndRollsback(Customer customer) {
this.eventPublisher.publishEvent(new TransactionApplicationEvent(this,
String.valueOf(customer.getId())));
return super.saveFailsAndRollsback(customer);
}
}
}

View File

@@ -0,0 +1,177 @@
/*
* Copyright 2019 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
*
* https://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.data.gemfire.transaction;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import java.util.ArrayList;
import java.util.EventListener;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.atomic.AtomicBoolean;
import org.junit.runner.RunWith;
import org.apache.geode.cache.RegionShortcut;
import org.apache.geode.cache.TransactionEvent;
import org.apache.geode.cache.TransactionWriter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Import;
import org.springframework.data.gemfire.config.annotation.EnableEntityDefinedRegions;
import org.springframework.data.gemfire.config.annotation.PeerCacheApplication;
import org.springframework.data.gemfire.config.annotation.PeerCacheConfigurer;
import org.springframework.data.gemfire.transaction.config.EnableGemfireCacheTransactions;
import org.springframework.data.gemfire.transaction.event.TransactionApplicationEvent;
import org.springframework.stereotype.Component;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.transaction.event.TransactionPhase;
import org.springframework.transaction.event.TransactionalEventListener;
/**
* Integration Tests for the Spring {@link TransactionalEventListener} in the context of Apache Geode
* cache transactions when auto-publishing of transaction events is enabled.
*
* @author John Blum
* @see org.junit.Test
* @see org.springframework.context.annotation.Bean
* @see org.springframework.context.annotation.Import
* @see org.springframework.data.gemfire.config.annotation.PeerCacheApplication
* @see org.springframework.data.gemfire.config.annotation.PeerCacheConfigurer
* @see org.springframework.data.gemfire.transaction.config.EnableGemfireCacheTransactions
* @see org.springframework.data.gemfire.transaction.event.TransactionApplicationEvent
* @see org.springframework.test.context.ContextConfiguration
* @see org.springframework.test.context.junit4.SpringRunner
* @see org.springframework.transaction.event.TransactionPhase
* @see org.springframework.transaction.event.TransactionalEventListener
* @since 2.3.0
*/
@RunWith(SpringRunner.class)
@ContextConfiguration
@SuppressWarnings("unused")
public class AutoTransactionalEventListenerIntegrationTests extends AbstractTransactionalEventListenerIntegrationTests {
@Autowired
private TestTransactionEventListener transactionEventListener;
@Autowired
@Qualifier("MockTransactionWriter")
private TransactionWriter mockTransactionWriter;
@Override
protected void assertTransactionEventListenerOnSuccess() throws Exception {
assertThat(this.transactionEventListener.isBeforeCommitInvoked()).isFalse();
assertThat(this.transactionEventListener.getAndClearTransactionPhases())
.containsExactly(TransactionPhase.AFTER_COMMIT);
verify(this.mockTransactionWriter, times(1)).beforeCommit(any(TransactionEvent.class));
}
@Override
protected void assertTransactionEventListenerOnFailure() {
assertThat(this.transactionEventListener.isBeforeCommitInvoked()).isFalse();
assertThat(this.transactionEventListener.getAndClearTransactionPhases())
.containsExactly(TransactionPhase.AFTER_ROLLBACK);
}
@PeerCacheApplication(logLevel = GEMFIRE_LOG_LEVEL)
@EnableEntityDefinedRegions(
basePackageClasses = Customer.class,
serverRegionShortcut = RegionShortcut.LOCAL
)
@EnableGemfireCacheTransactions(enableAutoTransactionEventPublishing = true)
@Import(CustomerRepositoryConfiguration.class)
static class TestConfiguration {
@Bean
CustomerService customerService(CustomerRepository customerRepository) {
return new CustomerService(customerRepository);
}
@Bean
TestTransactionEventListener testTransactionEventListener() {
return new TestTransactionEventListener();
}
@Bean("MockTransactionWriter")
TransactionWriter mockTransactionWriter() {
return mock(TransactionWriter.class);
}
@Bean
PeerCacheConfigurer transactionWriterRegisteringCacheConfigurer(
@Qualifier("MockTransactionWriter") TransactionWriter transactionWriter) {
return (beanName, bean) -> bean.setTransactionWriter(transactionWriter);
}
}
@Component
public static class TestTransactionEventListener implements EventListener {
private AtomicBoolean beforeCommitInvoked = new AtomicBoolean(false);
private List<TransactionPhase> transactionPhases = new ArrayList<>();
public List<TransactionPhase> getAndClearTransactionPhases() {
List<TransactionPhase> copy = new ArrayList<>(this.transactionPhases);
this.transactionPhases.clear();
return copy;
}
private boolean isBeforeCommitInvoked() {
return this.beforeCommitInvoked.getAndSet(false);
}
private void handleTransactionEvent(TransactionApplicationEvent event, TransactionPhase transactionPhase) {
Optional.ofNullable(event)
.map(TransactionApplicationEvent::getSource)
.filter(TransactionEvent.class::isInstance)
.ifPresent(transactionEvent -> this.transactionPhases.add(transactionPhase));
}
@TransactionalEventListener(phase = TransactionPhase.BEFORE_COMMIT)
public void handleTransactionBeforeCommit(TransactionApplicationEvent event) {
this.beforeCommitInvoked.set(true);
handleTransactionEvent(event, TransactionPhase.BEFORE_COMMIT);
}
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
public void handleTransactionAfterCommit(TransactionApplicationEvent event) {
handleTransactionEvent(event, TransactionPhase.AFTER_COMMIT);
}
@TransactionalEventListener(phase = TransactionPhase.AFTER_ROLLBACK)
public void handleTransactionAfterRollback(TransactionApplicationEvent event) {
handleTransactionEvent(event, TransactionPhase.AFTER_ROLLBACK);
}
}
}

View File

@@ -16,16 +16,13 @@
package org.springframework.data.gemfire.transaction;
import static org.assertj.core.api.Assertions.assertThat;
import static org.springframework.data.gemfire.util.RuntimeExceptionFactory.newIllegalStateException;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.apache.geode.cache.client.ClientRegionShortcut;
@@ -34,28 +31,16 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationEvent;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.context.annotation.Bean;
import org.springframework.data.annotation.Id;
import org.springframework.context.annotation.Import;
import org.springframework.data.gemfire.config.annotation.ClientCacheApplication;
import org.springframework.data.gemfire.config.annotation.EnableEntityDefinedRegions;
import org.springframework.data.gemfire.mapping.GemfireMappingContext;
import org.springframework.data.gemfire.mapping.annotation.Region;
import org.springframework.data.gemfire.repository.support.GemfireRepositoryFactoryBean;
import org.springframework.data.gemfire.transaction.config.EnableGemfireCacheTransactions;
import org.springframework.data.gemfire.transaction.event.TransactionApplicationEvent;
import org.springframework.data.repository.CrudRepository;
import org.springframework.stereotype.Component;
import org.springframework.stereotype.Service;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.transaction.event.TransactionPhase;
import org.springframework.transaction.event.TransactionalEventListener;
import org.springframework.util.Assert;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NonNull;
import lombok.RequiredArgsConstructor;
/**
* Integration Tests for the Spring {@link TransactionalEventListener} in the context of Apache Geode
@@ -66,9 +51,11 @@ import lombok.RequiredArgsConstructor;
* @see org.springframework.context.ApplicationEvent
* @see org.springframework.context.ApplicationEventPublisher
* @see org.springframework.context.annotation.Bean
* @see org.springframework.context.annotation.Import
* @see org.springframework.data.gemfire.config.annotation.ClientCacheApplication
* @see TransactionApplicationEvent
* @see org.springframework.data.gemfire.transaction.AbstractTransactionalEventListenerIntegrationTests
* @see org.springframework.data.gemfire.transaction.config.EnableGemfireCacheTransactions
* @see org.springframework.data.gemfire.transaction.event.TransactionApplicationEvent
* @see org.springframework.test.context.ContextConfiguration
* @see org.springframework.test.context.junit4.SpringRunner
* @see org.springframework.transaction.annotation.Transactional
@@ -79,24 +66,13 @@ import lombok.RequiredArgsConstructor;
@RunWith(SpringRunner.class)
@ContextConfiguration
@SuppressWarnings("unused")
public class TransactionalEventListenerIntegrationTests {
private static final String GEMFIRE_LOG_LEVEL = "error";
@Autowired
private CustomerService customerService;
public class TransactionalEventListenerIntegrationTests extends AbstractTransactionalEventListenerIntegrationTests {
@Autowired
private TestTransactionEventListener transactionEventListener;
@Test
public void successfulEntityTransactionTriggersCommitTransactionEvents() {
Customer jonDoe = this.customerService.save(Customer.newCustomer(1L, "Jon Doe"));
Customer jonDoeLoaded = this.customerService.findById(jonDoe.getId());
assertThat(jonDoeLoaded).isEqualTo(jonDoe);
@Override
protected void assertTransactionEventListenerOnSuccess() {
assertThat(this.transactionEventListener.getAndClearTransactionDetails()).containsExactly("1");
@@ -104,35 +80,13 @@ public class TransactionalEventListenerIntegrationTests {
.containsExactly(TransactionPhase.BEFORE_COMMIT, TransactionPhase.AFTER_COMMIT);
}
@Test(expected = IllegalStateException.class)
public void failingEntityTransactionTriggersRollbackTransactionEvent() {
@Override
protected void assertTransactionEventListenerOnFailure() {
Customer janeDoe = Customer.newCustomer(2L, "Jane Doe");
assertThat(this.transactionEventListener.getAndClearTransactionDetails()).containsExactly("2");
try {
this.customerService.saveFailsAndRollsback(janeDoe);
}
catch (IllegalStateException expected) {
assertThat(this.transactionEventListener.getAndClearTransactionDetails()).containsExactly("2");
assertThat(this.transactionEventListener.getAndClearTransactionPhases())
.containsExactly(TransactionPhase.AFTER_ROLLBACK);
assertThat(expected).hasMessage("TEST");
assertThat(expected).hasNoCause();
try {
this.customerService.findById(janeDoe.getId());
}
catch (IllegalStateException alsoExpected) {
assertThat(alsoExpected).hasMessage("No Customer having ID [2] was found");
assertThat(alsoExpected).hasNoCause();
throw alsoExpected;
}
}
assertThat(this.transactionEventListener.getAndClearTransactionPhases())
.containsExactly(TransactionPhase.AFTER_ROLLBACK);
}
@ClientCacheApplication(logLevel = GEMFIRE_LOG_LEVEL)
@@ -141,24 +95,14 @@ public class TransactionalEventListenerIntegrationTests {
clientRegionShortcut = ClientRegionShortcut.LOCAL
)
@EnableGemfireCacheTransactions
@Import(CustomerRepositoryConfiguration.class)
static class TestConfiguration {
@Bean
GemfireRepositoryFactoryBean<CustomerRepository, Customer, Long> customerRepositoryFactoryBean() {
GemfireRepositoryFactoryBean<CustomerRepository, Customer, Long> customerRepositoryFactoryBean
= new GemfireRepositoryFactoryBean<>(CustomerRepository.class);
customerRepositoryFactoryBean.setGemfireMappingContext(new GemfireMappingContext());
return customerRepositoryFactoryBean;
}
@Bean
CustomerService customerService(ApplicationEventPublisher eventPublisher,
CustomerRepository customerRepository) {
return new CustomerService(eventPublisher, customerRepository);
return new TransactionEventPublishingCustomerService(eventPublisher, customerRepository);
}
@Bean
@@ -167,66 +111,6 @@ public class TransactionalEventListenerIntegrationTests {
}
}
@Data
@EqualsAndHashCode
@Region("Customers")
@RequiredArgsConstructor(staticName = "newCustomer")
static class Customer implements Serializable {
@Id @NonNull
private Long id;
@NonNull
private String name;
}
public interface CustomerRepository extends CrudRepository<Customer, Long> { }
@Service
public static class CustomerService {
private ApplicationEventPublisher eventPublisher;
private final CustomerRepository customerRepository;
public CustomerService(ApplicationEventPublisher eventPublisher, CustomerRepository customerRepository) {
Assert.notNull(eventPublisher, "ApplicationEventPublisher is required");
Assert.notNull(customerRepository, "CustomerRepository is required");
this.eventPublisher = eventPublisher;
this.customerRepository = customerRepository;
}
public Customer findById(Long id) {
return this.customerRepository.findById(id)
.orElseThrow(() -> newIllegalStateException("No Customer having ID [%d] was found", id));
}
@Transactional
public Customer save(Customer customer) {
customer = this.customerRepository.save(customer);
this.eventPublisher.publishEvent(new TransactionApplicationEvent(this,
String.valueOf(customer.getId())));
return customer;
}
@Transactional
public Customer saveFailsAndRollsback(Customer customer) {
this.customerRepository.save(customer);
this.eventPublisher.publishEvent(new TransactionApplicationEvent(this,
String.valueOf(customer.getId())));
throw newIllegalStateException("TEST");
}
}
@Component
public static class TestTransactionEventListener {