DATAGEODE-272 - 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 feed0a3b0d
commit 2f558d4c95
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;
}
/**
@@ -953,7 +953,7 @@ public class CacheFactoryBean extends AbstractFactoryBeanSupport<GemFireCache>
* @return the evictionHeapPercentage
*/
public Float getEvictionHeapPercentage() {
return evictionHeapPercentage;
return this.evictionHeapPercentage;
}
/**
@@ -986,7 +986,7 @@ public class CacheFactoryBean extends AbstractFactoryBeanSupport<GemFireCache>
* @return the gatewayConflictResolver
*/
public GatewayConflictResolver getGatewayConflictResolver() {
return gatewayConflictResolver;
return this.gatewayConflictResolver;
}
/**
@@ -1000,7 +1000,7 @@ public class CacheFactoryBean extends AbstractFactoryBeanSupport<GemFireCache>
* @return the list of configured JndiDataSources.
*/
public List<JndiDataSource> getJndiDataSources() {
return jndiDataSources;
return this.jndiDataSources;
}
/**
@@ -1016,7 +1016,7 @@ public class CacheFactoryBean extends AbstractFactoryBeanSupport<GemFireCache>
* @return the lockLease
*/
public Integer getLockLease() {
return lockLease;
return this.lockLease;
}
/**
@@ -1032,7 +1032,7 @@ public class CacheFactoryBean extends AbstractFactoryBeanSupport<GemFireCache>
* @return the lockTimeout
*/
public Integer getLockTimeout() {
return lockTimeout;
return this.lockTimeout;
}
/**
@@ -1051,7 +1051,7 @@ public class CacheFactoryBean extends AbstractFactoryBeanSupport<GemFireCache>
* @return the messageSyncInterval
*/
public Integer getMessageSyncInterval() {
return messageSyncInterval;
return this.messageSyncInterval;
}
/**
@@ -1091,7 +1091,7 @@ public class CacheFactoryBean extends AbstractFactoryBeanSupport<GemFireCache>
* @return the pdxDiskStoreName
*/
public String getPdxDiskStoreName() {
return pdxDiskStoreName;
return this.pdxDiskStoreName;
}
/**
@@ -1108,7 +1108,7 @@ public class CacheFactoryBean extends AbstractFactoryBeanSupport<GemFireCache>
* @return the pdxIgnoreUnreadFields
*/
public Boolean getPdxIgnoreUnreadFields() {
return pdxIgnoreUnreadFields;
return this.pdxIgnoreUnreadFields;
}
/**
@@ -1124,7 +1124,7 @@ public class CacheFactoryBean extends AbstractFactoryBeanSupport<GemFireCache>
* @return the pdxPersistent
*/
public Boolean getPdxPersistent() {
return pdxPersistent;
return this.pdxPersistent;
}
/**
@@ -1141,7 +1141,7 @@ public class CacheFactoryBean extends AbstractFactoryBeanSupport<GemFireCache>
* @return the pdxReadSerialized
*/
public Boolean getPdxReadSerialized() {
return pdxReadSerialized;
return this.pdxReadSerialized;
}
/**
@@ -1159,7 +1159,7 @@ public class CacheFactoryBean extends AbstractFactoryBeanSupport<GemFireCache>
* @return the pdxSerializer
*/
public PdxSerializer getPdxSerializer() {
return pdxSerializer;
return this.pdxSerializer;
}
/**
@@ -1200,7 +1200,7 @@ public class CacheFactoryBean extends AbstractFactoryBeanSupport<GemFireCache>
* @return the searchTimeout
*/
public Integer getSearchTimeout() {
return searchTimeout;
return this.searchTimeout;
}
/**
@@ -1220,7 +1220,7 @@ public class CacheFactoryBean extends AbstractFactoryBeanSupport<GemFireCache>
* @see org.apache.geode.security.SecurityManager
*/
public SecurityManager getSecurityManager() {
return securityManager;
return this.securityManager;
}
/**
@@ -1238,14 +1238,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 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) {
@@ -1253,10 +1253,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));
}
}