diff --git a/spring-data-geode/src/main/java/org/springframework/data/gemfire/config/annotation/AsCacheListener.java b/spring-data-geode/src/main/java/org/springframework/data/gemfire/config/annotation/AsCacheListener.java
new file mode 100644
index 000000000..43901a1e7
--- /dev/null
+++ b/spring-data-geode/src/main/java/org/springframework/data/gemfire/config/annotation/AsCacheListener.java
@@ -0,0 +1,57 @@
+/*
+ * Copyright 2016-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.config.annotation;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+import org.springframework.data.gemfire.eventing.config.CacheListenerEventType;
+import org.springframework.data.gemfire.eventing.config.RegionCacheListenerEventType;
+
+/**
+ * Used to declare a concrete method as a {@link org.apache.geode.cache.CacheListener} event handler
+ *
+ * @author Udo Kohlmeyer
+ * @see org.apache.geode.cache.CacheListener
+ * @see CacheListenerEventType
+ * @see org.apache.geode.cache.Region
+ * @see org.apache.geode.cache.RegionEvent
+ * @see org.apache.geode.cache.EntryEvent
+ * @since 2.4.0
+ */
+@Retention(RetentionPolicy.RUNTIME)
+@Target({ ElementType.METHOD })
+public @interface AsCacheListener {
+
+ /**
+ * An array of {@link CacheListenerEventType} that control what region CRUD events need to be observed
+ * {@link CacheListenerEventType} and {@link RegionCacheListenerEventType} cannot be set on the same method. As they
+ * are mutually exclusive and require that the implementing method uses {@link org.apache.geode.cache.RegionEvent} or
+ * {@link org.apache.geode.cache.EntryEvent}
+ */
+ CacheListenerEventType[] eventTypes() default {};
+
+ /**
+ * An array for {@link org.apache.geode.cache.Region} names which this {@link org.apache.geode.cache.CacheListener}
+ * will be link to. Not declaring any regions will result in the CacheListener to be configured against all defined
+ * regions.
+ */
+ String[] regions() default {};
+}
diff --git a/spring-data-geode/src/main/java/org/springframework/data/gemfire/config/annotation/AsCacheWriter.java b/spring-data-geode/src/main/java/org/springframework/data/gemfire/config/annotation/AsCacheWriter.java
new file mode 100644
index 000000000..d3a1f4ed0
--- /dev/null
+++ b/spring-data-geode/src/main/java/org/springframework/data/gemfire/config/annotation/AsCacheWriter.java
@@ -0,0 +1,57 @@
+/*
+ * Copyright 2016-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.config.annotation;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+import org.springframework.data.gemfire.eventing.config.CacheWriterEventType;
+import org.springframework.data.gemfire.eventing.config.RegionCacheWriterEventType;
+
+/**
+ * Used to declare a concrete method as a {@link org.apache.geode.cache.CacheWriter} event handler
+ *
+ * @author Udo Kohlmeyer
+ * @see org.apache.geode.cache.CacheWriter
+ * @see CacheWriterEventType
+ * @see org.apache.geode.cache.Region
+ * @see org.apache.geode.cache.RegionEvent
+ * @see org.apache.geode.cache.EntryEvent
+ * @since 2.4.0
+ */
+@Retention(RetentionPolicy.RUNTIME)
+@Target({ ElementType.METHOD })
+public @interface AsCacheWriter {
+
+ /**
+ * An array of {@link CacheWriterEventType} that control what region CRUD events need to be observed
+ * {@link CacheWriterEventType} and {@link RegionCacheWriterEventType} cannot be set on the same method. As they
+ * are mutually exclusive and require that the implementing method uses {@link org.apache.geode.cache.RegionEvent} or
+ * {@link org.apache.geode.cache.EntryEvent}
+ */
+ CacheWriterEventType[] eventTypes() default {};
+
+ /**
+ * An array for {@link org.apache.geode.cache.Region} names which this {@link org.apache.geode.cache.CacheWriter}
+ * will be link to. Not declaring any regions will result in the CacheWriter to be configured against all defined
+ * regions.
+ */
+ String[] regions() default {};
+}
diff --git a/spring-data-geode/src/main/java/org/springframework/data/gemfire/config/annotation/AsRegionEventHandler.java b/spring-data-geode/src/main/java/org/springframework/data/gemfire/config/annotation/AsRegionEventHandler.java
new file mode 100644
index 000000000..e8d50348c
--- /dev/null
+++ b/spring-data-geode/src/main/java/org/springframework/data/gemfire/config/annotation/AsRegionEventHandler.java
@@ -0,0 +1,70 @@
+/*
+ * Copyright 2016-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.config.annotation;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+import org.springframework.data.gemfire.eventing.config.CacheListenerEventType;
+import org.springframework.data.gemfire.eventing.config.CacheWriterEventType;
+import org.springframework.data.gemfire.eventing.config.RegionCacheListenerEventType;
+import org.springframework.data.gemfire.eventing.config.RegionCacheWriterEventType;
+
+/**
+ * Used to declare a concrete method as a {@link AsRegionEventHandler} event handler,
+ * which handles {@link org.apache.geode.cache.Region}
+ *
+ * @author Udo Kohlmeyer
+ * @see org.apache.geode.cache.CacheWriter
+ * @see org.apache.geode.cache.CacheListener
+ * @see org.apache.geode.cache.Region
+ * @see RegionCacheListenerEventType
+ * @see RegionCacheWriterEventType
+ * @see org.apache.geode.cache.RegionEvent
+ * @see org.apache.geode.cache.EntryEvent
+ * @since 2.4.0
+ */
+@Retention(RetentionPolicy.RUNTIME)
+@Target({ ElementType.METHOD })
+public @interface AsRegionEventHandler {
+
+ /**
+ * An array of {@link RegionCacheListenerEventType} that control what region events need to be observed
+ * {@link CacheListenerEventType} and {@link RegionCacheListenerEventType} cannot be set on the same method. As they
+ * are mutually exclusive and require that the implementing method uses {@link org.apache.geode.cache.RegionEvent} or
+ * {@link org.apache.geode.cache.EntryEvent}
+ */
+ RegionCacheListenerEventType[] regionListenerEventTypes() default {};
+
+ /**
+ * An array of {@link RegionCacheWriterEventType} that control what region events need to be observed.
+ * {@link CacheWriterEventType} and {@link RegionCacheWriterEventType} cannot be set on the same method. As they
+ * are mutually exclusive and require that the implementing method uses {@link org.apache.geode.cache.RegionEvent} or
+ * {@link org.apache.geode.cache.EntryEvent}
+ */
+ RegionCacheWriterEventType[] regionWriterEventTypes() default {};
+
+ /**
+ * An array for {@link org.apache.geode.cache.Region} names which this {@link org.apache.geode.cache.CacheWriter}
+ * will be link to. Not declaring any regions will result in the CacheListener to be configured against all defined
+ * regions.
+ */
+ String[] regions() default {};
+}
diff --git a/spring-data-geode/src/main/java/org/springframework/data/gemfire/config/annotation/EnableEventProcessing.java b/spring-data-geode/src/main/java/org/springframework/data/gemfire/config/annotation/EnableEventProcessing.java
new file mode 100644
index 000000000..04381fff6
--- /dev/null
+++ b/spring-data-geode/src/main/java/org/springframework/data/gemfire/config/annotation/EnableEventProcessing.java
@@ -0,0 +1,55 @@
+/*
+ * Copyright 2016-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.config.annotation;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Inherited;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+import org.springframework.context.annotation.Import;
+import org.springframework.data.gemfire.eventing.config.AsCacheListenerBeanPostProcessorRegistrar;
+import org.springframework.data.gemfire.eventing.config.AsCacheWriterBeanPostProcessorRegistrar;
+import org.springframework.data.gemfire.eventing.config.PojoCacheListenerWrapper;
+
+/**
+ * Enables GemFire annotated EventHandler implementations. These implementation will include {@link org.apache.geode.cache.CacheListener},
+ * {@link org.apache.geode.cache.CacheWriter}, {@link org.apache.geode.cache.TransactionListener} and {@link org.apache.geode.cache.CacheLoader}
+ *
+ * This annotation results in the container discovering any beans that are annotated with:
+ *
<
+ *
{code}@AsCacheListener{code},wraps them in a {@link PojoCacheListenerWrapper}
+ *
{code}@AsCacheWriter{code},wraps them in a {@link ComposableCacheWriterWrapper}
+ * and register them with the corresponding {@link org.apache.geode.cache.Region}.
+ *
+ *
+ * @author Udo Kohlmeyer
+ * @since 2.3.0
+ */
+@Target(ElementType.TYPE)
+@Retention(RetentionPolicy.RUNTIME)
+@Inherited
+@Documented
+@Import({ AsCacheListenerBeanPostProcessorRegistrar.class,
+ AsCacheWriterBeanPostProcessorRegistrar.class})
+@SuppressWarnings("unused")
+public @interface EnableEventProcessing {
+
+}
diff --git a/spring-data-geode/src/main/java/org/springframework/data/gemfire/config/support/CacheListenerPostProcessor.java b/spring-data-geode/src/main/java/org/springframework/data/gemfire/config/support/CacheListenerPostProcessor.java
new file mode 100644
index 000000000..3a18b5b10
--- /dev/null
+++ b/spring-data-geode/src/main/java/org/springframework/data/gemfire/config/support/CacheListenerPostProcessor.java
@@ -0,0 +1,122 @@
+/*
+ * Copyright 2016-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.config.support;
+
+import java.lang.annotation.Annotation;
+import java.lang.reflect.Method;
+import java.util.List;
+
+import org.apache.geode.cache.CacheEvent;
+import org.apache.geode.cache.CacheListener;
+import org.apache.geode.cache.EntryEvent;
+import org.apache.geode.cache.Region;
+import org.apache.geode.cache.RegionEvent;
+import org.springframework.beans.factory.config.BeanPostProcessor;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.core.annotation.AnnotationAttributes;
+import org.springframework.data.gemfire.config.annotation.AsCacheListener;
+import org.springframework.data.gemfire.config.annotation.AsRegionEventHandler;
+import org.springframework.data.gemfire.eventing.EventProcessorUtils;
+import org.springframework.data.gemfire.eventing.config.CacheListenerEventType;
+import org.springframework.data.gemfire.eventing.config.PojoCacheListenerWrapper;
+import org.springframework.data.gemfire.eventing.config.PojoRegionEventCacheListenerWrapper;
+import org.springframework.data.gemfire.eventing.config.RegionCacheListenerEventType;
+
+/**
+ * A {@link BeanPostProcessor} to create and register {@link CacheListener}, annotated with {@link AsCacheListener}
+ * and {@link AsRegionEventHandler} onto the configured {@link Region}s
+ *
+ * @author Udo Kohlmeyer
+ * @see BeanPostProcessor
+ * @see CacheListener
+ * @see Region
+ * @see AsCacheListener
+ * @see AsRegionEventHandler
+ * @see CallbackPostProcessor
+ * @since 2.4.0
+ */
+@Configuration
+public class CacheListenerPostProcessor extends CallbackPostProcessor {
+
+ @Override
+ protected Class getRegionEventHandlerClass() {
+ return AsRegionEventHandler.class;
+ }
+
+ @Override
+ protected Class getEventHandlerClass() {
+ return AsCacheListener.class;
+ }
+
+ @Override
+ protected void registerEventHandlers(Object bean, Class listenerAnnotationClazz,
+ Method method, AnnotationAttributes cacheListenerAttributes) {
+ if (listenerAnnotationClazz.isAssignableFrom(getEventHandlerClass())) {
+ registerCacheListenerEventHandler(bean, method, cacheListenerAttributes);
+ }
+ else if (listenerAnnotationClazz.isAssignableFrom(getRegionEventHandlerClass())) {
+ registerRegionEventHandler(bean, method, cacheListenerAttributes);
+ }
+ }
+
+ /**
+ * Lookup {@link CacheListenerEventType} from the {@link AsCacheListener} annotation and create a {@link PojoCacheListenerWrapper}
+ * of type {@link CacheListener} that would register itself onto a {@link Region} for the configured events
+ */
+ private void registerCacheListenerEventHandler(Object bean, Method method,
+ AnnotationAttributes cacheListenerAttributes) {
+ CacheListenerEventType[] eventTypes = (CacheListenerEventType[]) cacheListenerAttributes
+ .get("eventTypes");
+ registerEventHandlerToRegion(method, cacheListenerAttributes,
+ new PojoCacheListenerWrapper(method, bean, eventTypes), EntryEvent.class);
+ }
+
+ /**
+ * Lookup {@link RegionCacheListenerEventType} from the {@link AsRegionEventHandler} annotation and
+ * create a {@link PojoRegionEventCacheListenerWrapper}
+ * of type {@link CacheListener} that would register itself onto a {@link Region} for the configured
+ * {@link Region} specific events
+ */
+ private void registerRegionEventHandler(Object bean, Method method,
+ AnnotationAttributes cacheListenerAttributes) {
+ RegionCacheListenerEventType[] eventTypes = (RegionCacheListenerEventType[]) cacheListenerAttributes
+ .get("regionListenerEventTypes");
+ registerEventHandlerToRegion(method, cacheListenerAttributes,
+ new PojoRegionEventCacheListenerWrapper(method, bean, eventTypes), RegionEvent.class);
+ }
+
+ /**
+ * Validates the method parameters to be of the correct type dependent on the eventing Annotation. It then registers
+ * the defined {@link CacheListener} onto the defined set of {@link Region}.
+ *
+ * @param method - The event handler callback method for event handling type
+ * @param cacheListenerAttributes - A set of {@link Annotation} attributes used to get the region names configured
+ * on the annotation
+ * @param cacheListener - The {@link CacheListener} to be registered onto the {@link Region}
+ * @param eventClass - The expected method parameter type. Can be either {@link EntryEvent} or {@link RegionEvent}
+ */
+ private void registerEventHandlerToRegion(Method method,
+ AnnotationAttributes cacheListenerAttributes, CacheListener cacheListener, Class eventClass) {
+ List regions = getRegionsForEventRegistration(cacheListenerAttributes.getStringArray("regions"),
+ getBeanFactory());
+
+ EventProcessorUtils.validateEventHandlerMethodParameters(method, eventClass);
+ EventProcessorUtils.registerCacheListenerToRegions(regions, beanFactory, cacheListener);
+ }
+
+}
diff --git a/spring-data-geode/src/main/java/org/springframework/data/gemfire/config/support/CacheWriterPostProcessor.java b/spring-data-geode/src/main/java/org/springframework/data/gemfire/config/support/CacheWriterPostProcessor.java
new file mode 100644
index 000000000..50d526a88
--- /dev/null
+++ b/spring-data-geode/src/main/java/org/springframework/data/gemfire/config/support/CacheWriterPostProcessor.java
@@ -0,0 +1,134 @@
+/*
+ * Copyright 2016-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.config.support;
+
+import java.lang.annotation.Annotation;
+import java.lang.reflect.Method;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+
+import org.apache.geode.cache.CacheEvent;
+import org.apache.geode.cache.CacheWriter;
+import org.apache.geode.cache.Region;
+import org.springframework.beans.factory.config.BeanPostProcessor;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.core.annotation.AnnotationAttributes;
+import org.springframework.data.gemfire.config.annotation.AsCacheWriter;
+import org.springframework.data.gemfire.config.annotation.AsRegionEventHandler;
+import org.springframework.data.gemfire.eventing.EventProcessorUtils;
+import org.springframework.data.gemfire.eventing.config.ComposableCacheWriterWrapper;
+
+/**
+ * A {@link BeanPostProcessor} to create and register {@link org.apache.geode.cache.CacheWriter},
+ * annotated with {@link AsCacheWriter}
+ * and {@link AsRegionEventHandler} onto the configured {@link Region}s
+ *
+ * @author Udo Kohlmeyer
+ * @see Region
+ * @see CacheWriter
+ * @see AsCacheWriter
+ * @see BeanPostProcessor
+ * @see AsRegionEventHandler
+ * @see ComposableCacheWriterWrapper
+ * @since 2.4.0
+ */
+@Configuration
+public class CacheWriterPostProcessor> extends CallbackPostProcessor {
+
+ private final Map composableCacheWriterWrappers = new HashMap<>();
+ private List configuredRegions;
+
+ @Override
+ protected Class getRegionEventHandlerClass() {
+ return AsRegionEventHandler.class;
+ }
+
+ @Override
+ protected Class getEventHandlerClass() {
+ return AsCacheWriter.class;
+ }
+
+ @Override
+ protected void registerEventHandlers(Object bean, Class writerAnnotationClazz,
+ Method method, AnnotationAttributes cacheWriterAttributes) {
+
+ String[] regionNames = cacheWriterAttributes.getStringArray("regions");
+
+ List regions = regionNames.length > 0 ? Arrays.asList(regionNames) : getConfiguredRegions();
+
+ Optional eventTypes = Optional.ofNullable(getEventTypes(writerAnnotationClazz, cacheWriterAttributes));
+
+ eventTypes.ifPresent(events -> {
+ if (events.length > 0) {
+ for (String region : regions) {
+ registerCacheWriterEventHandler(bean, method, events, region);
+ }
+ }
+ });
+ }
+
+ private E[] getEventTypes(Class writerAnnotationClazz,
+ AnnotationAttributes cacheWriterAttributes) {
+ if (writerAnnotationClazz.isAssignableFrom(getEventHandlerClass())) {
+ return (E[]) cacheWriterAttributes.get("eventTypes");
+ }
+ else if (writerAnnotationClazz.isAssignableFrom(getRegionEventHandlerClass())) {
+ return (E[]) cacheWriterAttributes.get("regionWriterEventTypes");
+ }
+ return null;
+ }
+
+ private List getConfiguredRegions() {
+ if (configuredRegions == null) {
+ configuredRegions = getRegionsForEventRegistration(getBeanFactory());
+ }
+ return configuredRegions;
+ }
+
+
+ private void registerCacheWriterEventHandler(Object bean, Method method, E[] eventTypes, String region) {
+ ComposableCacheWriterWrapper composableCacheWriterWrapper =
+ composableCacheWriterWrappers.getOrDefault(region, new ComposableCacheWriterWrapper());
+
+ Optional> eventTypeForMethod = Optional
+ .ofNullable(getEventTypeForMethod(eventTypes));
+ eventTypeForMethod.ifPresent(eventTypeClass -> {
+ EventProcessorUtils.validateEventHandlerMethodParameters(method, eventTypeClass);
+ composableCacheWriterWrapper.addCacheWriter(method, bean, eventTypes);
+
+ composableCacheWriterWrappers.put(region, composableCacheWriterWrapper);
+
+ registerEventHandlerToRegion(region, composableCacheWriterWrapper);
+ });
+ }
+
+ /**
+ * Validates the method parameters to be of the correct type dependent on the eventing Annotation. It then registers
+ * the defined {@link CacheWriter} onto the defined set of {@link Region}.
+ *
+ * on the annotation
+ *
+ * @param cacheWriter - The {@link CacheWriter} to be registered onto the {@link Region}
+ */
+ private void registerEventHandlerToRegion(String regionName, CacheWriter cacheWriter) {
+ EventProcessorUtils.registerCacheWriterToRegion(regionName, beanFactory, cacheWriter);
+ }
+}
diff --git a/spring-data-geode/src/main/java/org/springframework/data/gemfire/config/support/CallbackPostProcessor.java b/spring-data-geode/src/main/java/org/springframework/data/gemfire/config/support/CallbackPostProcessor.java
new file mode 100644
index 000000000..4bdb799a1
--- /dev/null
+++ b/spring-data-geode/src/main/java/org/springframework/data/gemfire/config/support/CallbackPostProcessor.java
@@ -0,0 +1,177 @@
+package org.springframework.data.gemfire.config.support;
+
+import static java.util.Arrays.stream;
+import static org.springframework.data.gemfire.util.ArrayUtils.isEmpty;
+import static org.springframework.data.gemfire.util.ArrayUtils.nullSafeArray;
+import static org.springframework.data.gemfire.util.RuntimeExceptionFactory.newIllegalStateException;
+
+import java.lang.annotation.Annotation;
+import java.lang.reflect.Method;
+import java.lang.reflect.Modifier;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Optional;
+
+import org.apache.geode.cache.CacheEvent;
+import org.apache.geode.cache.EntryEvent;
+import org.apache.geode.cache.Region;
+import org.apache.geode.cache.RegionEvent;
+import org.springframework.beans.BeansException;
+import org.springframework.beans.factory.BeanFactory;
+import org.springframework.beans.factory.BeanFactoryAware;
+import org.springframework.beans.factory.config.BeanPostProcessor;
+import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
+import org.springframework.core.annotation.AnnotationAttributes;
+import org.springframework.core.annotation.AnnotationUtils;
+import org.springframework.data.gemfire.eventing.config.CacheListenerEventType;
+import org.springframework.data.gemfire.eventing.config.CacheWriterEventType;
+import org.springframework.data.gemfire.eventing.config.RegionCacheListenerEventType;
+import org.springframework.data.gemfire.eventing.config.RegionCacheWriterEventType;
+import org.springframework.util.Assert;
+import org.springframework.util.ObjectUtils;
+import org.springframework.util.ReflectionUtils;
+
+/**
+ *
+ */
+public abstract class CallbackPostProcessor implements BeanPostProcessor, BeanFactoryAware {
+
+ protected ConfigurableListableBeanFactory beanFactory;
+
+ @Override
+ public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
+
+ registerAnyDeclaredCallbackAnnotatedMethods(bean, getEventHandlerClass());
+ registerAnyDeclaredCallbackAnnotatedMethods(bean, getRegionEventHandlerClass());
+
+ return bean;
+ }
+
+ protected abstract Class getRegionEventHandlerClass();
+
+ protected abstract Class getEventHandlerClass();
+
+ protected void registerAnyDeclaredCallbackAnnotatedMethods(Object bean,
+ Class annotationClazz) {
+
+ Method[] declaredMethods = nullSafeArray(ReflectionUtils.getAllDeclaredMethods(bean.getClass()), Method.class);
+
+ stream(declaredMethods).forEach(method -> {
+
+ Optional optionalCallbackAnnotation = Optional.ofNullable(AnnotationUtils
+ .getAnnotation(method, annotationClazz));
+
+ optionalCallbackAnnotation.ifPresent(callback -> {
+
+ Assert.isTrue(Modifier.isPublic(method.getModifiers()), String
+ .format("The bean [%s] method [%s] annotated with [%s] must be public", bean.getClass().getName(),
+ method.getName(), annotationClazz.getName()));
+
+ AnnotationAttributes callbackAttributes = resolveAnnotationAttributes(callback);
+
+ registerEventHandlers(bean, annotationClazz, method, callbackAttributes);
+
+ });
+ });
+ }
+
+ protected abstract void registerEventHandlers(Object bean, Class annotationClazz,
+ Method method, AnnotationAttributes callbackAttributes);
+
+ /**
+ * Takes an array of Region names. If empty, returns all configured {@link Region} names, otherwise returns the input
+ * region name array
+ *
+ * @param beanFactory - A {@link org.springframework.data.gemfire.ConfigurableRegionFactoryBean}
+ * @return An array of {@link Region} names. If the input regions array is empty, the result will be an array with all
+ * configured {@link Region} names
+ */
+ protected List getRegionsForEventRegistration(ConfigurableListableBeanFactory beanFactory) {
+ List regionNames = new ArrayList<>();
+ stream(beanFactory.getBeanDefinitionNames()).forEach(beanName -> {
+ Object bean = beanFactory.getBean(beanName);
+ if (bean instanceof Region) {
+ Region region = (Region) bean;
+ regionNames.add(region.getName());
+ }
+ });
+ return regionNames;
+ }
+
+ private AnnotationAttributes resolveAnnotationAttributes(Annotation annotation) {
+ return AnnotationAttributes.fromMap(
+ AnnotationUtils.getAnnotationAttributes(annotation, false, true));
+ }
+
+ /**
+ * Takes an array of Region names. If empty, returns all configured {@link Region} names, otherwise returns the input
+ * region name array
+ *
+ * @param regions - An Array of {@link Region} names. This can be empty and thus defaults to all configured {@link Region}
+ * @param beanFactory - A {@link org.springframework.data.gemfire.ConfigurableRegionFactoryBean}
+ * @return An array of {@link Region} names. If the input regions array is empty, the result will be an array with all
+ * configured {@link Region} names
+ */
+ protected List getRegionsForEventRegistration(String[] regions,
+ ConfigurableListableBeanFactory beanFactory) {
+ if (isEmpty(regions)) {
+ return getRegionsForEventRegistration(beanFactory);
+ }
+ else {
+ return Arrays.asList(regions);
+ }
+ }
+
+ /**
+ * Sets a reference to the configured Spring {@link BeanFactory}.
+ *
+ * @param beanFactory configured Spring {@link BeanFactory}.
+ * @throws IllegalArgumentException if the given {@link BeanFactory} is not an instance of
+ * {@link ConfigurableListableBeanFactory}.
+ * @see BeanFactoryAware
+ * @see BeanFactory
+ */
+ @Override
+ @SuppressWarnings("all")
+ public final void setBeanFactory(BeanFactory beanFactory) throws BeansException {
+
+ Assert.isInstanceOf(ConfigurableListableBeanFactory.class, beanFactory, String
+ .format("BeanFactory [%1$s] must be an instance of %2$s", ObjectUtils.nullSafeClassName(beanFactory),
+ ConfigurableListableBeanFactory.class.getSimpleName()));
+
+ this.beanFactory = (ConfigurableListableBeanFactory) beanFactory;
+ }
+
+ /**
+ * Returns a reference to the containing Spring {@link BeanFactory}.
+ *
+ * @return a reference to the containing Spring {@link BeanFactory}.
+ * @throws IllegalStateException if the {@link BeanFactory} was not configured.
+ * @see BeanFactory
+ */
+ protected ConfigurableListableBeanFactory getBeanFactory() {
+ return Optional.ofNullable(this.beanFactory)
+ .orElseThrow(() -> newIllegalStateException("BeanFactory was not properly configured"));
+ }
+
+ /**
+ * Returns the correct Event type, either {@link EntryEvent} or {@link RegionEvent}, dependent on the eventType
+ * of either {@link RegionCacheWriterEventType}, {@link CacheWriterEventType}, {@link CacheListenerEventType}
+ * or {@link RegionCacheListenerEventType}
+ *
+ * @param eventTypes an array of event types
+ * @return a class type associated with the enum event type. Returns {@literal null} if not association is found
+ */
+ protected Class extends CacheEvent> getEventTypeForMethod(Enum[] eventTypes) {
+ for (Enum eventType : eventTypes) {
+ if (eventType instanceof CacheWriterEventType || eventType instanceof CacheListenerEventType) {
+ return EntryEvent.class;
+ }
+ if (eventType instanceof RegionCacheWriterEventType || eventType instanceof RegionCacheListenerEventType) {
+ return RegionEvent.class;
+ }
+ }
+ return null;
+ }
+}
diff --git a/spring-data-geode/src/main/java/org/springframework/data/gemfire/eventing/EventProcessorUtils.java b/spring-data-geode/src/main/java/org/springframework/data/gemfire/eventing/EventProcessorUtils.java
new file mode 100644
index 000000000..a0b9f4e4b
--- /dev/null
+++ b/spring-data-geode/src/main/java/org/springframework/data/gemfire/eventing/EventProcessorUtils.java
@@ -0,0 +1,96 @@
+/*
+ * Copyright 2016-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.eventing;
+
+import java.lang.reflect.Method;
+import java.util.List;
+import java.util.Optional;
+
+import org.apache.geode.cache.CacheEvent;
+import org.apache.geode.cache.CacheListener;
+import org.apache.geode.cache.CacheWriter;
+import org.apache.geode.cache.Region;
+import org.springframework.beans.factory.BeanFactory;
+
+/**
+ * @author Udo Kohlmeyer
+ * @see BeanFactory
+ * @see Region
+ * @see List
+ * @see CacheListener
+ * @see CacheWriter
+ * @see CacheEvent
+ * @see org.apache.geode.cache.RegionEvent
+ * @see org.apache.geode.cache.EntryEvent
+ * @see org.springframework.data.gemfire.eventing.config.ComposableCacheWriterWrapper
+ * @see org.springframework.data.gemfire.eventing.config.PojoCacheListenerWrapper
+ * @see org.springframework.data.gemfire.eventing.config.PojoRegionEventCacheListenerWrapper
+ * @see 2.4.0
+ */
+public class EventProcessorUtils {
+
+ /**
+ * Registers a {@link CacheListener} with a {@link Region}
+ * @param regions a {@link List} of {@link String}s of region names on which the {@link CacheListener} needs to be registered
+ * @param beanFactory the Spring {@link BeanFactory}
+ * @param cacheListener the {@link CacheListener} that needs to be registered onto the {@link Region}(s)
+ */
+ public static void registerCacheListenerToRegions(List regions, BeanFactory beanFactory,
+ CacheListener cacheListener) {
+ for (String regionName : regions) {
+ Optional regionBeanOptional = Optional.of(beanFactory.getBean(regionName, Region.class));
+
+ regionBeanOptional.ifPresent(region -> region.getAttributesMutator().addCacheListener(cacheListener));
+ }
+ }
+
+ /**
+ * Registers a {@link CacheWriter} with a {@link Region}
+ * @param regionName the region name on which the {@link CacheWriter} needs to be registered
+ * @param beanFactory the Spring {@link BeanFactory}
+ * @param cacheWriter the {@link CacheWriter} that needs to be registered
+ */
+ public static void registerCacheWriterToRegion(String regionName, BeanFactory beanFactory,
+ CacheWriter cacheWriter) {
+ Optional> regionBeanOptional = Optional.of(beanFactory.getBean(regionName, Region.class));
+
+ regionBeanOptional.ifPresent(region -> region.getAttributesMutator().setCacheWriter(cacheWriter));
+ }
+
+ /**
+ * The {@link Method} method needs to be validated to have only 1 parameter AND of either {@link org.apache.geode.cache.EntryEvent}
+ * or {@link org.apache.geode.cache.RegionEvent}
+ * @param method the method to be validated
+ * @param requireParameterType the required type of the method parameter
+ */
+ public static void validateEventHandlerMethodParameters(Method method,
+ Class extends CacheEvent> requireParameterType) {
+ Class>[] parameterTypes = method.getParameterTypes();
+ if (parameterTypes.length != 1) {
+ throw new IllegalArgumentException(String
+ .format("Callback Handler method: %s does not currently support more than one parameter",
+ method.getName()));
+ }
+ if (!parameterTypes[0].isAssignableFrom(requireParameterType)) {
+ throw new IllegalArgumentException(String
+ .format("Callback Handler: %s requires an %s parameter type", method.getName(),
+ requireParameterType.getName()));
+ }
+ }
+
+}
diff --git a/spring-data-geode/src/main/java/org/springframework/data/gemfire/eventing/config/AsCacheListenerBeanPostProcessorRegistrar.java b/spring-data-geode/src/main/java/org/springframework/data/gemfire/eventing/config/AsCacheListenerBeanPostProcessorRegistrar.java
new file mode 100644
index 000000000..4248d049b
--- /dev/null
+++ b/spring-data-geode/src/main/java/org/springframework/data/gemfire/eventing/config/AsCacheListenerBeanPostProcessorRegistrar.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright 2016-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.eventing.config;
+
+import org.springframework.beans.factory.support.BeanDefinitionBuilder;
+import org.springframework.beans.factory.support.BeanDefinitionReaderUtils;
+import org.springframework.beans.factory.support.BeanDefinitionRegistry;
+import org.springframework.context.annotation.ImportBeanDefinitionRegistrar;
+import org.springframework.core.type.AnnotationMetadata;
+import org.springframework.data.gemfire.config.support.CacheListenerPostProcessor;
+
+/**
+ * Spring {@link org.springframework.context.annotation.ImportBeanDefinitionRegistrar} to register the {@link CacheListenerPostProcessor}
+ *
+ * @author Udo Kohlmeyer
+ * @see org.springframework.beans.factory.BeanFactory
+ * @see org.springframework.beans.factory.support.BeanDefinitionBuilder
+ * @see org.springframework.beans.factory.support.BeanDefinitionRegistry
+ * @see org.springframework.context.annotation.ImportBeanDefinitionRegistrar
+ * @see CacheListenerPostProcessor
+ * @since 2.4.0
+ */
+public class AsCacheListenerBeanPostProcessorRegistrar implements ImportBeanDefinitionRegistrar {
+
+ @Override
+ public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
+
+ BeanDefinitionBuilder builder =
+ BeanDefinitionBuilder.genericBeanDefinition(CacheListenerPostProcessor.class);
+
+ BeanDefinitionReaderUtils.registerWithGeneratedName(builder.getBeanDefinition(), registry);
+ }
+}
diff --git a/spring-data-geode/src/main/java/org/springframework/data/gemfire/eventing/config/AsCacheWriterBeanPostProcessorRegistrar.java b/spring-data-geode/src/main/java/org/springframework/data/gemfire/eventing/config/AsCacheWriterBeanPostProcessorRegistrar.java
new file mode 100644
index 000000000..bbbd0220b
--- /dev/null
+++ b/spring-data-geode/src/main/java/org/springframework/data/gemfire/eventing/config/AsCacheWriterBeanPostProcessorRegistrar.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright 2016-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.eventing.config;
+
+import org.springframework.beans.factory.support.BeanDefinitionBuilder;
+import org.springframework.beans.factory.support.BeanDefinitionReaderUtils;
+import org.springframework.beans.factory.support.BeanDefinitionRegistry;
+import org.springframework.context.annotation.ImportBeanDefinitionRegistrar;
+import org.springframework.core.type.AnnotationMetadata;
+import org.springframework.data.gemfire.config.support.CacheWriterPostProcessor;
+
+/**
+ * Spring {@link ImportBeanDefinitionRegistrar} to register the {@link CacheWriterPostProcessor}
+ *
+ * @author Udo Kohlmeyer
+ * @see org.springframework.beans.factory.BeanFactory
+ * @see org.springframework.beans.factory.support.BeanDefinitionBuilder
+ * @see org.springframework.beans.factory.support.BeanDefinitionRegistry
+ * @see org.springframework.context.annotation.ImportBeanDefinitionRegistrar
+ * @see CacheWriterPostProcessor
+ * @since 2.4.0
+ */
+public class AsCacheWriterBeanPostProcessorRegistrar implements ImportBeanDefinitionRegistrar {
+
+ @Override
+ public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
+
+ BeanDefinitionBuilder builder =
+ BeanDefinitionBuilder.genericBeanDefinition(CacheWriterPostProcessor.class);
+
+ BeanDefinitionReaderUtils.registerWithGeneratedName(builder.getBeanDefinition(), registry);
+ }
+}
diff --git a/spring-data-geode/src/main/java/org/springframework/data/gemfire/eventing/config/CacheListenerEventType.java b/spring-data-geode/src/main/java/org/springframework/data/gemfire/eventing/config/CacheListenerEventType.java
new file mode 100644
index 000000000..2b0ce157c
--- /dev/null
+++ b/spring-data-geode/src/main/java/org/springframework/data/gemfire/eventing/config/CacheListenerEventType.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright 2016-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.eventing.config;
+
+/**
+ * An Enum that represents event types defined within {@link org.apache.geode.cache.CacheListener}. The event types
+ * defined are CRUD operation orientated. These do not reflect the events that {@link org.apache.geode.cache.Region} have.
+ *
+ * @author Udo Kohlmeyer
+ * @see org.apache.geode.cache.CacheListener
+ * @see org.apache.geode.cache.Region
+ * @since 2.4.0
+ */
+public enum CacheListenerEventType{
+ ALL(0b00001111),
+ AFTER_CREATE(0b00000001),
+ AFTER_UPDATE(0b00000010),
+ AFTER_DESTROY(0b00000100),
+ AFTER_INVALIDATE(0b00001000);
+
+ CacheListenerEventType(int mask){
+ this.mask = mask;
+ }
+
+ int mask;
+}
diff --git a/spring-data-geode/src/main/java/org/springframework/data/gemfire/eventing/config/CacheWriterEventType.java b/spring-data-geode/src/main/java/org/springframework/data/gemfire/eventing/config/CacheWriterEventType.java
new file mode 100644
index 000000000..eb7a0d753
--- /dev/null
+++ b/spring-data-geode/src/main/java/org/springframework/data/gemfire/eventing/config/CacheWriterEventType.java
@@ -0,0 +1,42 @@
+/*
+ * Copyright 2016-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.eventing.config;
+
+/**
+ * An Enum that represents event types defined within {@link org.apache.geode.cache.CacheWriter}. The event types
+ * defined are CRUD operation orientated. These do not reflect the managements events that {@link org.apache.geode.cache.Region} have.
+ * These events are handled by the {@link RegionCacheWriterEventType}.
+ *
+ * Due to constraints imposed by the {@link org.apache.geode.cache.Region} that only one {@link org.apache.geode.cache.CacheWriter}
+ * can be added to a region, the event mask described here must not conflict with the mask described in {@link RegionCacheWriterEventType}.
+ *
+ * @author Udo Kohlmeyer
+ * @see org.apache.geode.cache.CacheWriter
+ * @since 2.4.0
+ */
+public enum CacheWriterEventType {
+ ALL(0b00000111),
+ BEFORE_CREATE(0b00000001),
+ BEFORE_UPDATE(0b00000010),
+ BEFORE_DESTROY(0b00000100);
+
+ CacheWriterEventType(int mask){
+ this.mask = mask;
+ }
+
+ int mask;
+}
diff --git a/spring-data-geode/src/main/java/org/springframework/data/gemfire/eventing/config/ComposableCacheWriterWrapper.java b/spring-data-geode/src/main/java/org/springframework/data/gemfire/eventing/config/ComposableCacheWriterWrapper.java
new file mode 100644
index 000000000..56e857759
--- /dev/null
+++ b/spring-data-geode/src/main/java/org/springframework/data/gemfire/eventing/config/ComposableCacheWriterWrapper.java
@@ -0,0 +1,164 @@
+package org.springframework.data.gemfire.eventing.config;
+
+import java.lang.reflect.Method;
+import java.util.HashMap;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+
+import org.apache.geode.cache.CacheEvent;
+import org.apache.geode.cache.CacheWriterException;
+import org.apache.geode.cache.EntryEvent;
+import org.apache.geode.cache.RegionEvent;
+import org.apache.geode.cache.util.CacheWriterAdapter;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * A {@link ComposableCacheWriterWrapper} is a {@link org.apache.geode.cache.CacheWriter} that enables the ability to
+ * include multiple {@link PojoCacheWriterWrapper}s into a single {@link org.apache.geode.cache.CacheWriter}.
+ * {@link org.apache.geode.cache.Region}s don't allow for multiple {@link org.apache.geode.cache.CacheWriter}s to be
+ * registered against the same {@link org.apache.geode.cache.Region}.
+ *
+ * Given the callback event handling approach, using method annotations for {@link org.apache.geode.cache.CacheWriter} events of
+ * {@link RegionEvent} and {@link EntryEvent}, could possibly result into the creation of multiple {@link org.apache.geode.cache.CacheWriter}
+ * instances for the same region for different event types.
+ *
+ * @author Udo Kohlmeyer
+ * @see org.apache.geode.cache.CacheWriter
+ * @see CacheWriterAdapter
+ * @see PojoCacheWriterWrapper
+ * @since 2.4.0
+ */
+public class ComposableCacheWriterWrapper> extends CacheWriterAdapter {
+
+ private static final String BEFORE_CREATE = "beforeCreate";
+ private static final String BEFORE_UPDATE = "beforeUpdate";
+ private static final String BEFORE_DESTROY = "beforeDestroy";
+ private static final String BEFORE_REGION_DESTROY = "beforeRegionDestroy";
+ private static final String BEFORE_REGION_CLEAR = "beforeRegionClear";
+
+ private static Logger logger = LoggerFactory.getLogger(ComposableCacheWriterWrapper.class);
+
+ protected Map> cacheWriterWrapperList = new HashMap<>();
+
+ /**
+ * Registers a {@link org.springframework.data.gemfire.config.annotation.AsCacheWriter} or
+ * {@link org.springframework.data.gemfire.config.annotation.AsRegionEventHandler} method,
+ * as a {@link PojoCacheWriterWrapper} for execution.
+ *
+ * In the event that the events are not {@link org.apache.geode.cache.CacheWriter} events, no event handler will
+ * be registered *
+ *
+ * @param method the method that needs to be executed in an event handling callback
+ * @param targetInvocationClass the target class on which the method was registered
+ * @param eventTypes the event types that the method needs to be registered for.
+ */
+ public void addCacheWriter(Method method, Object targetInvocationClass, E[] eventTypes) {
+ int eventMask = createRegionEventTypeMask(eventTypes);
+ if (eventMask > 0) {
+ List regionEventCacheWriterList = cacheWriterWrapperList
+ .getOrDefault(eventMask, new LinkedList<>());
+
+ regionEventCacheWriterList.add(new PojoCacheWriterWrapper(method, targetInvocationClass));
+
+ cacheWriterWrapperList.put(eventMask, regionEventCacheWriterList);
+ }
+ }
+
+ /**
+ * Takes either a {@link RegionCacheWriterEventType} or {@link CacheWriterEventType} as an input.
+ * These enums contain an int that represents a filtering mask that will be applied. In order to create only 1
+ * {@link PojoCacheWriterWrapper} instance for each method, the masks need to be combined into a single value to
+ * represent all the events that the single event handling callback method needs cover.
+ *
+ * @param cacheWriterEventTypes an array of Enum EventTypes. Either {@link RegionCacheWriterEventType} or {@link CacheWriterEventType}
+ * @return an integer representing the combined masks of all registered event types
+ * @author Udo Kohlmeyer
+ * @since 2.4.0
+ */
+ public int createRegionEventTypeMask(E[] cacheWriterEventTypes) {
+ int mask = 0x0;
+ for (E cacheWriterEventType : cacheWriterEventTypes) {
+ if (cacheWriterEventType instanceof CacheWriterEventType) {
+ CacheWriterEventType writerEventType = (CacheWriterEventType) cacheWriterEventType;
+ mask |= writerEventType.mask;
+ }
+ else if (cacheWriterEventType instanceof RegionCacheWriterEventType) {
+ RegionCacheWriterEventType writerEventType = (RegionCacheWriterEventType) cacheWriterEventType;
+ mask |= writerEventType.mask;
+ }
+ }
+ return mask;
+ }
+
+ @Override
+ public void beforeCreate(EntryEvent event) {
+ logDebug(BEFORE_CREATE);
+
+ executeEventHandler(event, CacheWriterEventType.BEFORE_CREATE.mask);
+ }
+
+ @Override
+ public void beforeUpdate(EntryEvent event) {
+ logDebug(BEFORE_UPDATE);
+
+ executeEventHandler(event, CacheWriterEventType.BEFORE_UPDATE.mask);
+ }
+
+ @Override
+ public void beforeDestroy(EntryEvent event) {
+ logDebug(BEFORE_DESTROY);
+
+ executeEventHandler(event, CacheWriterEventType.BEFORE_DESTROY.mask);
+ }
+
+ @Override
+ public void beforeRegionDestroy(RegionEvent event) throws CacheWriterException {
+ logDebug(BEFORE_REGION_DESTROY);
+
+ executeEventHandler(event, RegionCacheWriterEventType.BEFORE_REGION_DESTROY.mask);
+ }
+
+ @Override
+ public void beforeRegionClear(RegionEvent event) throws CacheWriterException {
+ logDebug(BEFORE_REGION_CLEAR);
+
+ executeEventHandler(event, RegionCacheWriterEventType.BEFORE_REGION_CLEAR.mask);
+ }
+
+ /**
+ * This method takes a {@link CacheEvent}, which will either be a {@link RegionEvent} or a {@link EntryEvent},
+ * an eventType mask integer and tries to execute the event with registered {@link PojoCacheWriterWrapper}.
+ *
+ * Given that the registered {@link PojoCacheWriterWrapper} might be registered under a combined event mask, like
+ * {@link RegionCacheWriterEventType#ALL} or {@link CacheWriterEventType#ALL} each event received needs to be compared
+ * against each registered {@link PojoCacheWriterWrapper} entry to determine if the event needs to be executed.
+ *
+ * @param event either a {@link RegionEvent} or {@link EntryEvent} that needs to be actioned
+ * @param eventTypeInt a mask for the event that has just fired and needs to be handled
+ * @author Udo Kohlmeyer
+ * @see CacheEvent
+ * @see EntryEvent
+ * @see RegionEvent
+ * @since 2.4.0
+ */
+ private void executeEventHandler(CacheEvent event, int eventTypeInt) {
+
+ for (Map.Entry> cacheWriterWrappers : cacheWriterWrapperList
+ .entrySet()) {
+ if ((cacheWriterWrappers.getKey() & eventTypeInt) == eventTypeInt) {
+
+ for (PojoCacheWriterWrapper pojoCacheWriterWrapper : cacheWriterWrappers.getValue()) {
+ pojoCacheWriterWrapper.executeCacheWriter(event);
+ }
+ }
+ }
+ }
+
+ private void logDebug(String eventType) {
+ if (logger.isDebugEnabled()) {
+ logger.debug("Invoking " + eventType);
+ }
+ }
+}
diff --git a/spring-data-geode/src/main/java/org/springframework/data/gemfire/eventing/config/PojoCacheListenerWrapper.java b/spring-data-geode/src/main/java/org/springframework/data/gemfire/eventing/config/PojoCacheListenerWrapper.java
new file mode 100644
index 000000000..768400ff6
--- /dev/null
+++ b/spring-data-geode/src/main/java/org/springframework/data/gemfire/eventing/config/PojoCacheListenerWrapper.java
@@ -0,0 +1,106 @@
+/*
+ * Copyright 2016-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.eventing.config;
+
+import java.lang.reflect.Method;
+
+import org.apache.geode.cache.EntryEvent;
+import org.apache.geode.cache.util.CacheListenerAdapter;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.util.ReflectionUtils;
+
+/**
+ * Invokes a given {@link Object POJO} {@link Method} as a GemFire/Geode {@link org.apache.geode.cache.CacheListener}.
+ * This proxy will process events triggered from CRUD operations against the {@link org.apache.geode.cache.Region} data.
+ *
+ * @author Udo Kohlmeyer
+ * @see org.apache.geode.cache.CacheListener
+ * @see org.apache.geode.cache.util.CacheListenerAdapter
+ * @see org.apache.geode.cache.Region
+ * @see ReflectionUtils
+ * @since 2.4.0
+ */
+public class PojoCacheListenerWrapper extends CacheListenerAdapter {
+
+ private static final String AFTER_CREATE = "afterCreate";
+ private static final String AFTER_UPDATE = "afterUpdate";
+ private static final String AFTER_DESTROY = "afterDestroy";
+ private static final String AFTER_INVALIDATE = "afterInvalidate";
+
+ private static transient Logger logger = LoggerFactory.getLogger(PojoCacheListenerWrapper.class);
+ private final Method method;
+ private final Object targetInvocationObject;
+ private final int eventTypeMask;
+
+ public PojoCacheListenerWrapper(Method method, Object targetInvocationClass, CacheListenerEventType[] eventTypes) {
+ this.method = method;
+ this.targetInvocationObject = targetInvocationClass;
+ this.eventTypeMask = createEventTypeMask(eventTypes);
+ }
+
+ private static int createEventTypeMask(CacheListenerEventType[] eventTypes) {
+ int mask = 0x0;
+ for (CacheListenerEventType eventType : eventTypes) {
+ mask |= eventType.mask;
+ }
+ return mask;
+ }
+
+ @Override
+ public void afterCreate(EntryEvent event) {
+ logDebug(AFTER_CREATE);
+
+ executeEventHandler(event, CacheListenerEventType.AFTER_CREATE);
+ }
+
+ @Override
+ public void afterUpdate(EntryEvent event) {
+ logDebug(AFTER_UPDATE);
+
+ executeEventHandler(event, CacheListenerEventType.AFTER_UPDATE);
+ }
+
+ @Override
+ public void afterDestroy(EntryEvent event) {
+ logDebug(AFTER_DESTROY);
+
+ executeEventHandler(event, CacheListenerEventType.AFTER_DESTROY);
+ }
+
+ @Override
+ public void afterInvalidate(EntryEvent event) {
+ logDebug(AFTER_INVALIDATE);
+
+ executeEventHandler(event, CacheListenerEventType.AFTER_INVALIDATE);
+ }
+
+ private void executeEventHandler(EntryEvent event, CacheListenerEventType eventType) {
+
+ if ((eventTypeMask & eventType.mask) == eventType.mask) {
+
+ ReflectionUtils.invokeMethod(this.method, this.targetInvocationObject, event);
+
+ }
+ }
+
+ private void logDebug(String eventType) {
+ if (logger.isDebugEnabled()) {
+ logger.debug("Invoking " + eventType);
+ }
+ }
+}
diff --git a/spring-data-geode/src/main/java/org/springframework/data/gemfire/eventing/config/PojoCacheWriterWrapper.java b/spring-data-geode/src/main/java/org/springframework/data/gemfire/eventing/config/PojoCacheWriterWrapper.java
new file mode 100644
index 000000000..a760a3f77
--- /dev/null
+++ b/spring-data-geode/src/main/java/org/springframework/data/gemfire/eventing/config/PojoCacheWriterWrapper.java
@@ -0,0 +1,38 @@
+package org.springframework.data.gemfire.eventing.config;
+
+import java.lang.reflect.Method;
+
+import org.apache.geode.cache.CacheEvent;
+import org.springframework.util.ReflectionUtils;
+
+/**
+ * A simple wrapper method that wraps a {@link Method} and its target instance for simple usage in the {@link ComposableCacheWriterWrapper}
+ * for method execution on a {@link org.apache.geode.cache.Region}'s {@link org.apache.geode.cache.CacheWriter}.
+ *
+ * @author Udo Kohlmeyer
+ * @see org.apache.geode.cache.Region
+ * @see org.apache.geode.cache.CacheWriter
+ * @see ComposableCacheWriterWrapper
+ * @see ReflectionUtils
+ * @since 2.4.0
+ */
+public class PojoCacheWriterWrapper {
+
+ private final Method method;
+ private final Object targetInvocationObject;
+
+ public PojoCacheWriterWrapper(Method method, Object targetInvocationClass) {
+ this.method = method;
+ this.targetInvocationObject = targetInvocationClass;
+ }
+
+ /**
+ * Using Reflection the stored {@link Method} and target invocation instance is used to reflectively run the method.
+ *
+ * @param event the {@link org.apache.geode.cache.RegionEvent} or {@link org.apache.geode.cache.EntryEvent} that is
+ * required for the method to execute.
+ */
+ public void executeCacheWriter(CacheEvent event) {
+ ReflectionUtils.invokeMethod(method, targetInvocationObject, event);
+ }
+}
diff --git a/spring-data-geode/src/main/java/org/springframework/data/gemfire/eventing/config/PojoRegionEventCacheListenerWrapper.java b/spring-data-geode/src/main/java/org/springframework/data/gemfire/eventing/config/PojoRegionEventCacheListenerWrapper.java
new file mode 100644
index 000000000..61af8f224
--- /dev/null
+++ b/spring-data-geode/src/main/java/org/springframework/data/gemfire/eventing/config/PojoRegionEventCacheListenerWrapper.java
@@ -0,0 +1,106 @@
+/*
+ * Copyright 2016-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.eventing.config;
+
+import java.lang.reflect.Method;
+
+import org.apache.geode.cache.RegionEvent;
+import org.apache.geode.cache.util.CacheListenerAdapter;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.util.ReflectionUtils;
+
+/**
+ * Invokes a given {@link Object POJO} {@link Method} as a GemFire/Geode {@link org.apache.geode.cache.CacheListener}.
+ * This proxy will specifically handle Region type events.
+ *
+ * @author Udo Kohlmeyer
+ * @see org.apache.geode.cache.CacheListener
+ * @since 2.4.0
+ */
+public class PojoRegionEventCacheListenerWrapper extends CacheListenerAdapter {
+
+ private static final String AFTER_REGION_DESTROY = "afterRegionDestroy";
+ private static final String AFTER_REGION_CREATE = "afterRegionCreate";
+ private static final String AFTER_REGION_INVALIDATE = "afterRegionInvalidate";
+ private static final String AFTER_REGION_CLEAR = "afterRegionClear";
+ private static final String AFTER_REGION_LIVE = "afterRegionLive";
+
+ private static transient Logger logger = LoggerFactory.getLogger(PojoRegionEventCacheListenerWrapper.class);
+ private final Method method;
+ private final Object targetInvocationObject;
+ private final int eventTypeMask;
+
+ public PojoRegionEventCacheListenerWrapper(Method method, Object targetInvocationClass, RegionCacheListenerEventType[] regionEventTypes) {
+ this.method = method;
+ this.targetInvocationObject = targetInvocationClass;
+ this.eventTypeMask = createRegionEventTypeMask(regionEventTypes);
+ }
+
+ private static int createRegionEventTypeMask(RegionCacheListenerEventType[] regionEventTypes) {
+ int mask = 0x0;
+ for (RegionCacheListenerEventType eventType : regionEventTypes) {
+ mask |= eventType.mask;
+ }
+ return mask;
+ }
+
+ @Override public void afterRegionDestroy(RegionEvent event) {
+ logDebug(AFTER_REGION_DESTROY);
+
+ executeEventHandler(event, RegionCacheListenerEventType.AFTER_REGION_DESTROY);
+ }
+
+ @Override public void afterRegionCreate(RegionEvent event) {
+ logDebug(AFTER_REGION_CREATE);
+
+ executeEventHandler(event, RegionCacheListenerEventType.AFTER_REGION_CREATE);
+ }
+
+ @Override public void afterRegionInvalidate(RegionEvent event) {
+ logDebug(AFTER_REGION_INVALIDATE);
+
+ executeEventHandler(event, RegionCacheListenerEventType.AFTER_REGION_INVALIDATE);
+ }
+
+ @Override public void afterRegionClear(RegionEvent event) {
+ logDebug(AFTER_REGION_CLEAR);
+
+ executeEventHandler(event, RegionCacheListenerEventType.AFTER_REGION_CLEAR);
+ }
+
+ @Override public void afterRegionLive(RegionEvent event) {
+ logDebug(AFTER_REGION_LIVE);
+
+ executeEventHandler(event, RegionCacheListenerEventType.AFTER_REGION_LIVE);
+ }
+
+ private void executeEventHandler(RegionEvent regionEvent, RegionCacheListenerEventType eventType) {
+
+ if ((eventTypeMask & eventType.mask) == eventType.mask) {
+
+ ReflectionUtils.invokeMethod(this.method, this.targetInvocationObject, regionEvent);
+
+ }
+ }
+
+ private void logDebug(String eventType) {
+ if (logger.isDebugEnabled()) {
+ logger.debug("Invoking " + eventType);
+ }
+ }
+}
diff --git a/spring-data-geode/src/main/java/org/springframework/data/gemfire/eventing/config/RegionCacheListenerEventType.java b/spring-data-geode/src/main/java/org/springframework/data/gemfire/eventing/config/RegionCacheListenerEventType.java
new file mode 100644
index 000000000..c107b584b
--- /dev/null
+++ b/spring-data-geode/src/main/java/org/springframework/data/gemfire/eventing/config/RegionCacheListenerEventType.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright 2016-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.eventing.config;
+
+/**
+ * An Enum that represents region event types defined within {@link org.apache.geode.cache.CacheListener}. The region event
+ * types defined events triggered from Region management operations. These do not reflect the events
+ * that {@link org.apache.geode.cache.Region} have.
+ *
+ * @author Udo Kohlmeyer
+ * @since 2.4.0
+ */
+public enum RegionCacheListenerEventType {
+ ALL(0b0111111),
+ AFTER_REGION_CREATE(0b00000001),
+ AFTER_REGION_CLEAR(0b00000010),
+ AFTER_REGION_DESTROY(0b00000100),
+ AFTER_REGION_INVALIDATE(0b00001000),
+ AFTER_REGION_LIVE(0b00010000);
+
+ RegionCacheListenerEventType(int mask){
+ this.mask = mask;
+ }
+
+ int mask;
+}
diff --git a/spring-data-geode/src/main/java/org/springframework/data/gemfire/eventing/config/RegionCacheWriterEventType.java b/spring-data-geode/src/main/java/org/springframework/data/gemfire/eventing/config/RegionCacheWriterEventType.java
new file mode 100644
index 000000000..6294a17d0
--- /dev/null
+++ b/spring-data-geode/src/main/java/org/springframework/data/gemfire/eventing/config/RegionCacheWriterEventType.java
@@ -0,0 +1,41 @@
+/*
+ * Copyright 2016-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.eventing.config;
+
+/**
+ * An Enum that represents region event types defined within {@link org.apache.geode.cache.CacheWriter}. The region event
+ * types defined events triggered from Region management operations. These do not reflect CRUD operational events
+ * that {@link org.apache.geode.cache.Region} have. These events are represented by the enum {@link CacheWriterEventType}
+ *
+ * * Due to constraints imposed by the {@link org.apache.geode.cache.Region} that only one {@link org.apache.geode.cache.CacheWriter}
+ * * can be added to a region, the event mask described here must not conflict with the mask described in {@link CacheWriterEventType}.
+ *
+ * @author Udo Kohlmeyer
+ * @see org.apache.geode.cache.CacheWriter
+ * @since 2.4.0
+ */
+public enum RegionCacheWriterEventType {
+ ALL(0b0011000),
+ BEFORE_REGION_CLEAR(0b0001000),
+ BEFORE_REGION_DESTROY(0b0010000);
+
+ RegionCacheWriterEventType(int mask) {
+ this.mask = mask;
+ }
+
+ int mask;
+}
diff --git a/spring-data-geode/src/test/java/org/springframework/data/gemfire/config/annotation/AsCacheListenerCacheServerConfigurationTests.java b/spring-data-geode/src/test/java/org/springframework/data/gemfire/config/annotation/AsCacheListenerCacheServerConfigurationTests.java
new file mode 100644
index 000000000..e45f764a4
--- /dev/null
+++ b/spring-data-geode/src/test/java/org/springframework/data/gemfire/config/annotation/AsCacheListenerCacheServerConfigurationTests.java
@@ -0,0 +1,202 @@
+/*
+ * Copyright 2016-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.config.annotation;
+
+import org.apache.geode.cache.EntryEvent;
+import org.apache.geode.cache.GemFireCache;
+import org.apache.geode.cache.RegionEvent;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.data.gemfire.ReplicatedRegionFactoryBean;
+import org.springframework.data.gemfire.eventing.config.CacheListenerEventType;
+
+/**
+ * Tests for {@link org.springframework.data.gemfire.config.annotation.AsCacheListener} configured for a
+ * {@link org.apache.geode.cache.server.CacheServer}
+ *
+ * @author Udo Kohlmeyer
+ * @see org.junit.Test
+ * @see org.mockito.Mockito
+ * @see org.apache.geode.cache.server.CacheServer
+ * @see org.springframework.context.annotation.Configuration
+ * @see org.springframework.test.context.ContextConfiguration
+ * @see org.springframework.test.context.junit4.SpringRunner
+ * @since 2.3.0
+ */
+public class AsCacheListenerCacheServerConfigurationTests extends AsCacheListenerConfigurationTests {
+
+ protected static ReplicatedRegionFactoryBean createRegionFactoryBean(GemFireCache cache,
+ String regionName) {
+ ReplicatedRegionFactoryBean replicateRegion = new ReplicatedRegionFactoryBean<>();
+ replicateRegion.setName(regionName);
+ replicateRegion.setCache(cache);
+ return replicateRegion;
+ }
+
+ @Override
+ protected Class> getCacheListenerWithIncorrectRegionEventParameterConfiguration() {
+ return TestConfigurationWithIncorrectRegionEventParameter.class;
+ }
+
+ @Override
+ protected Class> getCacheListenerAnnotationSingleDefaultRegionsConfiguration() {
+ return TestConfigurationWithSimpleCacheListener.class;
+ }
+
+ @Override
+ protected Class getCacheListenerAnnotationWithInvalidRegion() {
+ return TestConfigurationWithInvalidRegion.class;
+ }
+
+ @Override
+ protected Class getCacheListenerAnnotationMultipleRegionsDefault() {
+ return TestConfigurationWith2RegionsAnd2CacheListenersDefaulted.class;
+ }
+
+ @Override
+ protected Class> getCacheListenerAnnotationSingleRegionAllEvents() {
+ return TestConfigurationWithSimpleCacheListenerAllEvents.class;
+ }
+
+ @Override
+ protected Class> getCacheListenerAnnotationAgainst2NamedRegions() {
+ return TestConfigurationWithSimpleCacheListenerWith2Regions.class;
+ }
+
+ @Configuration
+ @CacheServerApplication
+ @EnableEventProcessing
+ public static class TestConfigurationWithIncorrectRegionEventParameter {
+
+ @Bean("TestRegion1")
+ ReplicatedRegionFactoryBean getTestRegion(GemFireCache cache) {
+ return createRegionFactoryBean(cache, "TestRegion1");
+ }
+
+ @AsCacheListener(eventTypes = CacheListenerEventType.AFTER_CREATE)
+ public void afterCreateListener(RegionEvent event) {
+ }
+ }
+
+ @Configuration
+ @CacheServerApplication
+ @EnableEventProcessing
+ public static class TestConfigurationWithSimpleCacheListener {
+
+ @Bean("TestRegion1")
+ ReplicatedRegionFactoryBean getTestRegion(GemFireCache cache) {
+ return createRegionFactoryBean(cache, "TestRegion1");
+ }
+
+ @AsCacheListener(eventTypes = CacheListenerEventType.AFTER_CREATE)
+ public void afterCreateListener(EntryEvent event) {
+ recordEvent(event);
+ }
+
+ @AsCacheListener(eventTypes = CacheListenerEventType.AFTER_UPDATE)
+ public void afterUpdateListener(EntryEvent event) {
+ recordEvent(event);
+
+ }
+ }
+
+ @Configuration
+ @CacheServerApplication
+ @EnableEventProcessing
+ public static class TestConfigurationWithInvalidRegion {
+
+ @Bean("TestRegion1")
+ ReplicatedRegionFactoryBean getTestRegion(GemFireCache cache) {
+ return createRegionFactoryBean(cache, "TestRegion1");
+ }
+
+ @AsCacheListener(eventTypes = CacheListenerEventType.AFTER_CREATE, regions = "TestRegion2")
+ public void afterCreateListener(EntryEvent event) {
+ recordEvent(event);
+ }
+
+ }
+
+ @Configuration
+ @CacheServerApplication
+ @EnableEventProcessing
+ public static class TestConfigurationWithSimpleCacheListenerAllEvents {
+
+ @Bean("TestRegion1")
+ ReplicatedRegionFactoryBean getTestRegion(GemFireCache cache) {
+ return createRegionFactoryBean(cache, "TestRegion1");
+ }
+
+ @AsCacheListener(eventTypes = CacheListenerEventType.ALL)
+ public void afterCreateListener(EntryEvent event) {
+ recordEvent(event);
+ }
+ }
+
+ @Configuration
+ @CacheServerApplication
+ @EnableEventProcessing
+ public static class TestConfigurationWithSimpleCacheListenerWith2Regions {
+
+ @Bean("TestRegion1")
+ ReplicatedRegionFactoryBean getTestRegion1(GemFireCache cache) {
+ return createRegionFactoryBean(cache, "TestRegion1");
+ }
+
+ @Bean("TestRegion2")
+ ReplicatedRegionFactoryBean getTestRegion2(GemFireCache cache) {
+ return createRegionFactoryBean(cache, "TestRegion2");
+ }
+
+ @AsCacheListener(eventTypes = CacheListenerEventType.AFTER_CREATE, regions = "TestRegion1")
+ public void afterCreateListener(EntryEvent event) {
+ recordEvent(event);
+ }
+
+ @AsCacheListener(eventTypes = CacheListenerEventType.AFTER_CREATE, regions = "TestRegion2")
+ public void afterUpdateListener(EntryEvent event) {
+ recordEvent(event);
+ }
+ }
+
+ @Configuration
+ @CacheServerApplication
+ @EnableEventProcessing
+ public static class TestConfigurationWith2RegionsAnd2CacheListenersDefaulted {
+
+ @Bean("TestRegion1")
+ ReplicatedRegionFactoryBean getTestRegion1(GemFireCache cache) {
+ return createRegionFactoryBean(cache, "TestRegion1");
+ }
+
+ @Bean("TestRegion2")
+ ReplicatedRegionFactoryBean getTestRegion2(GemFireCache cache) {
+ return createRegionFactoryBean(cache, "TestRegion2");
+ }
+
+ @AsCacheListener(eventTypes = CacheListenerEventType.ALL)
+ public void afterCreateListener1(EntryEvent event) {
+ recordEvent(event);
+ }
+
+ @AsCacheListener(eventTypes = CacheListenerEventType.ALL)
+ public void afterCreateListener2(EntryEvent event) {
+ recordEvent(event);
+ }
+ }
+}
diff --git a/spring-data-geode/src/test/java/org/springframework/data/gemfire/config/annotation/AsCacheListenerClientCacheConfigurationTests.java b/spring-data-geode/src/test/java/org/springframework/data/gemfire/config/annotation/AsCacheListenerClientCacheConfigurationTests.java
new file mode 100644
index 000000000..e5a22c3bf
--- /dev/null
+++ b/spring-data-geode/src/test/java/org/springframework/data/gemfire/config/annotation/AsCacheListenerClientCacheConfigurationTests.java
@@ -0,0 +1,196 @@
+/*
+ * 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.config.annotation;
+
+import org.apache.geode.cache.EntryEvent;
+import org.apache.geode.cache.GemFireCache;
+import org.apache.geode.cache.RegionEvent;
+import org.apache.geode.cache.client.ClientRegionShortcut;
+import org.junit.Test;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.data.gemfire.client.ClientRegionFactoryBean;
+import org.springframework.data.gemfire.eventing.config.CacheListenerEventType;
+
+/**
+ * Tests for {@link AsCacheListener} configured for a {@link org.apache.geode.cache.client.ClientCache}
+ *
+ * @author Udo Kohlmeyer
+ * @see Test
+ * @see org.mockito.Mockito
+ * @see org.apache.geode.cache.client.ClientCache
+ * @see Configuration
+ * @see org.springframework.test.context.ContextConfiguration
+ * @see org.springframework.test.context.junit4.SpringRunner
+ * @since 2.3.0
+ */
+public class AsCacheListenerClientCacheConfigurationTests extends AsCacheListenerConfigurationTests {
+
+ protected static ClientRegionFactoryBean createRegionFactoryBean(GemFireCache cache,
+ String regionName) {
+ ClientRegionFactoryBean clientRegion = new ClientRegionFactoryBean<>();
+ clientRegion.setName(regionName);
+ clientRegion.setCache(cache);
+ clientRegion.setShortcut(ClientRegionShortcut.LOCAL);
+ return clientRegion;
+ }
+
+ @Override
+ protected Class> getCacheListenerWithIncorrectRegionEventParameterConfiguration() {
+ return TestConfigurationWithIncorrectEventParameter.class;
+ }
+
+ protected Class> getCacheListenerAnnotationSingleDefaultRegionsConfiguration() {
+ return TestConfigurationWithSimpleCacheListener.class;
+ }
+
+ protected Class getCacheListenerAnnotationWithInvalidRegion() {
+ return TestConfigurationWithInvalidRegion.class;
+ }
+
+ protected Class getCacheListenerAnnotationMultipleRegionsDefault() {
+ return TestConfigurationWith2RegionsAnd2CacheListenersDefaulted.class;
+ }
+
+ protected Class> getCacheListenerAnnotationSingleRegionAllEvents() {
+ return TestConfigurationWithSimpleCacheListenerAllEvents.class;
+ }
+
+ protected Class> getCacheListenerAnnotationAgainst2NamedRegions() {
+ return TestConfigurationWithSimpleCacheListenerWith2Regions.class;
+ }
+
+ @Configuration
+ @ClientCacheApplication
+ @EnableEventProcessing
+ public static class TestConfigurationWithIncorrectEventParameter {
+
+ @Bean("TestRegion")
+ ClientRegionFactoryBean getTestRegion(GemFireCache cache) {
+ return createRegionFactoryBean(cache, "TestRegion");
+ }
+
+ @AsCacheListener(eventTypes = CacheListenerEventType.AFTER_CREATE)
+ public void afterCreateListener(RegionEvent event) {
+ }
+ }
+
+ @Configuration
+ @ClientCacheApplication
+ @EnableEventProcessing
+ public static class TestConfigurationWithSimpleCacheListener {
+
+ @Bean("TestRegion1")
+ ClientRegionFactoryBean getTestRegion(GemFireCache cache) {
+ return createRegionFactoryBean(cache, "TestRegion1");
+ }
+
+ @AsCacheListener(eventTypes = CacheListenerEventType.AFTER_CREATE)
+ public void afterCreateListener(EntryEvent event) {
+ recordEvent(event);
+ }
+
+ @AsCacheListener(eventTypes = CacheListenerEventType.AFTER_UPDATE)
+ public void afterUpdateListener(EntryEvent event) {
+ recordEvent(event);
+ }
+ }
+
+ @Configuration
+ @ClientCacheApplication
+ @EnableEventProcessing
+ public static class TestConfigurationWithInvalidRegion {
+
+ @Bean("TestRegion")
+ ClientRegionFactoryBean getTestRegion(GemFireCache cache) {
+ return createRegionFactoryBean(cache, "TestRegion");
+ }
+
+ @AsCacheListener(eventTypes = CacheListenerEventType.AFTER_CREATE, regions = "TestRegion2")
+ public void afterCreateListener(EntryEvent event) {
+ recordEvent(event);
+ }
+ }
+
+ @Configuration
+ @ClientCacheApplication
+ @EnableEventProcessing
+ public static class TestConfigurationWithSimpleCacheListenerAllEvents {
+
+ @Bean("TestRegion1")
+ ClientRegionFactoryBean getTestRegion(GemFireCache cache) {
+ return createRegionFactoryBean(cache, "TestRegion1");
+ }
+
+ @AsCacheListener(eventTypes = CacheListenerEventType.ALL)
+ public void afterCreateListener(EntryEvent event) {
+ recordEvent(event);
+ }
+ }
+
+ @Configuration
+ @ClientCacheApplication
+ @EnableEventProcessing
+ public static class TestConfigurationWithSimpleCacheListenerWith2Regions {
+
+ @Bean("TestRegion1")
+ ClientRegionFactoryBean getTestRegion1(GemFireCache cache) {
+ return createRegionFactoryBean(cache, "TestRegion1");
+ }
+
+ @Bean("TestRegion2")
+ ClientRegionFactoryBean getTestRegion2(GemFireCache cache) {
+ return createRegionFactoryBean(cache, "TestRegion2");
+ }
+
+ @AsCacheListener(eventTypes = CacheListenerEventType.AFTER_CREATE, regions = "TestRegion1")
+ public void afterCreateListener(EntryEvent event) {
+ recordEvent(event);
+ }
+
+ @AsCacheListener(eventTypes = CacheListenerEventType.AFTER_CREATE, regions = "TestRegion2")
+ public void afterUpdateListener(EntryEvent event) {
+ recordEvent(event);
+ }
+ }
+
+ @Configuration
+ @ClientCacheApplication
+ @EnableEventProcessing
+ public static class TestConfigurationWith2RegionsAnd2CacheListenersDefaulted {
+
+ @Bean("TestRegion1")
+ ClientRegionFactoryBean getTestRegion1(GemFireCache cache) {
+ return createRegionFactoryBean(cache, "TestRegion1");
+ }
+
+ @Bean("TestRegion2")
+ ClientRegionFactoryBean getTestRegion2(GemFireCache cache) {
+ return createRegionFactoryBean(cache, "TestRegion2");
+ }
+
+ @AsCacheListener(eventTypes = CacheListenerEventType.ALL)
+ public void afterCreateListener1(EntryEvent event) {
+ recordEvent(event);
+ }
+
+ @AsCacheListener(eventTypes = CacheListenerEventType.ALL)
+ public void afterCreateListener2(EntryEvent event) {
+ recordEvent(event);
+ }
+ }
+}
diff --git a/spring-data-geode/src/test/java/org/springframework/data/gemfire/config/annotation/AsCacheListenerConfigurationTests.java b/spring-data-geode/src/test/java/org/springframework/data/gemfire/config/annotation/AsCacheListenerConfigurationTests.java
new file mode 100644
index 000000000..573df8452
--- /dev/null
+++ b/spring-data-geode/src/test/java/org/springframework/data/gemfire/config/annotation/AsCacheListenerConfigurationTests.java
@@ -0,0 +1,170 @@
+package org.springframework.data.gemfire.config.annotation;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Optional;
+
+import org.apache.geode.cache.EntryEvent;
+import org.apache.geode.cache.Operation;
+import org.apache.geode.cache.Region;
+import org.assertj.core.api.Assertions;
+import org.junit.After;
+import org.junit.Test;
+import org.springframework.beans.factory.BeanCreationException;
+import org.springframework.context.ConfigurableApplicationContext;
+import org.springframework.context.annotation.AnnotationConfigApplicationContext;
+
+public abstract class AsCacheListenerConfigurationTests {
+
+ protected static final List