From fc3519a080800e563b6fbb593f123d1a7107f887 Mon Sep 17 00:00:00 2001 From: John Blum Date: Fri, 31 Jul 2015 13:41:11 -0700 Subject: [PATCH] SGF-404 - Enable Expiration settings and policies to be specified per application domain object using an @Expiration annotation and a custom, SDG-provided CustomExpiry instance. Final touches on the SDG Expiration annotation based support. Added additional test cases to test the improper specification of Expiration annotation attribute settings and modified the SDG Reference Guide to discuss Spring Property Placeholder and SpEL support. (cherry picked from commit 278af87703dfbc341fef1690e7064d9de80fc295) Signed-off-by: John Blum --- src/asciidoc/reference/region.adoc | 98 ++++++++++++++++--- ...xpirationConfigurationIntegrationTest.java | 41 +++++++- 2 files changed, 122 insertions(+), 17 deletions(-) diff --git a/src/asciidoc/reference/region.adoc b/src/asciidoc/reference/region.adoc index 60d5888c..8491a518 100644 --- a/src/asciidoc/reference/region.adoc +++ b/src/asciidoc/reference/region.adoc @@ -627,7 +627,7 @@ settings on a Session-based application domain object like so... [source,java] ---- -@Expiration(timeout = 1800, action = ExpirationActionType.INVALIDATE) +@Expiration(timeout = "1800", action = "INVALIDATE") public static class SessionBasedApplicationDomainObject { } ---- @@ -637,9 +637,9 @@ and `@TimeToLiveExpiration` for Idle Timeout (TTI) and Time-to-Live (TTL) Expira [source,java] ---- -@TimeToLiveExpiration(timeout = 3600, action = ExpirationActionType.LOCAL_DESTROY) -@IdleTimeoutExpiration(timeout = 1800, action = ExpirationActionType.LOCAL_INVALIDATE) -@Expiration(timeout = 1800, action = ExpirationActionType.INVALIDATE) +@TimeToLiveExpiration(timeout = "3600", action = "LOCAL_DESTROY") +@IdleTimeoutExpiration(timeout = "1800", action = "LOCAL_INVALIDATE") +@Expiration(timeout = "1800", action = "INVALIDATE") public static class AnotherSessionBasedApplicationDomainObject { } ---- @@ -647,9 +647,11 @@ public static class AnotherSessionBasedApplicationDomainObject { Both `@IdleTimeoutExpiration` and `@TimeToLiveExpiration` take precedence over the generic `@Expiration` annotation when more than one Expiration annotation type is specified, as shown above. Though, neither `@IdleTimeoutExpiration` nor `@TimeToLiveExpiration` overrides the other; rather they may compliment each other when different Region Entry -Expiration types, such as TTL and TTI, are both configured. +Expiration types, such as TTL and TTI, are configured. -Also, all @Expiration-based annotations apply only to Region Entry values. Expiration for a "Region" is not covered +[NOTE] +==== +All @Expiration-based annotations apply only to Region Entry values. Expiration for a "Region" is not covered by Spring Data GemFire's Expiration annotation support. However, GemFire and Spring Data GemFire do allow you to set Region Expiration using the SDG XML namespace, like so... @@ -660,6 +662,7 @@ Region Expiration using the SDG XML namespace, like so... ---- +==== Spring Data GemFire's @Expiration annotation support is implemented with GemFire's http://gemfire.docs.pivotal.io/latest/javadocs/japi/com/gemstone/gemfire/cache/CustomExpiry.html[`CustomExpiry`] interface. See http://gemfire.docs.pivotal.io/latest/userguide/index.html#developing/expiration/configuring_data_expiration.html[GemFire's User Guide] for more details @@ -668,13 +671,13 @@ The Spring Data GemFire `AnnotationBasedExpiration` class (and `CustomExpiry` im for processing the SDG @Expiration annotations and applying the Expiration policy and settings appropriately for Region Entry Expiration on request. -To use Spring Data GemFire to configure specifically GemFire Regions to appropriately apply the Expiration policy +To use Spring Data GemFire to configure specific GemFire Regions to appropriately apply the Expiration policy and settings applied to your application domain objects annotated with @Expiration-based annotations, you must... -1. Define a Spring bean in the Spring ApplicationContext of type `AnnotationBasedExpiration` using a constructor -or one of the factory methods. When configuring Expiration for a specific Expiration type, such as Idle Timeout -or Time-to-Live, then you should use one of the convenient factory methods of the `AnnotationBasedExpiration` class, -like so... +1. Define a Spring bean in the Spring ApplicationContext of type `AnnotationBasedExpiration` using the appropriate +constructor or one of the convenient factory methods. When configuring Expiration for a specific Expiration type, +such as Idle Timeout or Time-to-Live, then you should use one of the factory methods of the `AnnotationBasedExpiration` +class, like so... + [source,xml] ---- @@ -693,10 +696,10 @@ along with the `` element to set TTI. ==== 2. (optional) Annotate your application domain objects that will be stored in the Region with Expiration policies -and custom settings with one of the Spring Data GemFire @Expiration annotations: `@Expiration`, `@IdleTimeoutExpiration` +and custom settings using one of Spring Data GemFire's @Expiration annotations: `@Expiration`, `@IdleTimeoutExpiration` and/or `@TimeToLiveExpiration` -3. (optional) In cases where certain application domain objects have not been annotated with Spring Data GemFire's +3. (optional) In cases where particular application domain objects have not been annotated with Spring Data GemFire's @Expiration annotations at all, but the GemFire Region is configured to use SDG's custom `AnnotationBasedExpiration` class to determine the Expiration policy and settings for objects stored in the Region, then it is possible to set "default" Expiration attributes on the `AnnotationBasedExpiration` bean by doing the following... @@ -718,6 +721,75 @@ Expiration attributes on the `AnnotationBasedExpiration` bean by doing the follo ---- +You may have noticed that the Spring Data GemFire's @Expiration annotations use String as the attributes type, rather +than and perhaps more appropriately being strongly typed, i.e. `int` for 'timeout' and SDG'S `ExpirationActionType` +for 'action'. Why is that? + +Well, enter one of Spring Data GemFire's other features, leveraging Spring's core infrastructure +for configuration convenience: Property Placeholders and Spring Expression Language (SpEL). + +For instance, a developer can specify both the Expiration 'timeout' and 'action' using Property Placeholders +in the @Expiration annotation attributes... + +[source,java] +---- +@TimeToLiveExpiration(timeout = "${gemfire.region.entry.expiration.ttl.timeout}" + action = "${gemfire.region.entry.expiration.ttl.action}") +public class ExampleApplicationDomainObject { +} +---- + +Then, in your Spring context XML or in JavaConfig, you would declare the following beans... + +[source,xml] +---- + + 600 + INVALIDATE + ... + + + +---- + +This is both convenient when multiple application domain objects might share similar Expiration policies and settings, +or when you wish to externalize the configuration. + +However, a developer may want more dynamic Expiration configuration determined by the state of the running system. +This is where the power of SpEL comes in and is the recommended approach. Not only can you refer to beans +in the Spring context and access bean properties, invoke methods, etc, the values for Expiration 'timeout' and 'action' +can be strongly typed. For example (building on the example above)... + +[source,xml] +---- + + 600 + #{T(org.springframework.data.gemfire.ExpirationActionType).DESTROY} + #{T(com.gemstone.gemfire.cache.ExpirationAction).INVALIDATE} + ... + + + +---- + +Then, on your application domain object... + +[source,java] +---- +@TimeToLiveExpiration(timeout = "@expirationSettings['gemfire.region.entry.expiration.ttl.timeout']" + action = "@expirationSetting['gemfire.region.entry.expiration.ttl.action']") +public class ExampleApplicationDomainObject { +} +---- + +You can imagine that the 'expirationSettings' bean could be a more interesting and useful object rather than a simple +instance of `java.util.Properties`. In this example, even the Properties ('expirationSettings') using using SpEL +to based the action value on the actual Expiration action enumerated types leading to more quickly identified failures +if the types ever change. + +All of this has been demonstrated and tested in the Spring Data GemFire test suite, by way of example. See the +https://github.com/spring-projects/spring-data-gemfire[source] for further details. + [[bootstrap:region:local]] == Local Region diff --git a/src/test/java/org/springframework/data/gemfire/support/AnnotationBasedExpirationConfigurationIntegrationTest.java b/src/test/java/org/springframework/data/gemfire/support/AnnotationBasedExpirationConfigurationIntegrationTest.java index 24db16ff..9b0346e1 100644 --- a/src/test/java/org/springframework/data/gemfire/support/AnnotationBasedExpirationConfigurationIntegrationTest.java +++ b/src/test/java/org/springframework/data/gemfire/support/AnnotationBasedExpirationConfigurationIntegrationTest.java @@ -19,6 +19,7 @@ package org.springframework.data.gemfire.support; import static org.hamcrest.CoreMatchers.equalTo; import static org.hamcrest.CoreMatchers.instanceOf; import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.CoreMatchers.isA; import static org.hamcrest.CoreMatchers.not; import static org.hamcrest.CoreMatchers.nullValue; import static org.hamcrest.CoreMatchers.sameInstance; @@ -29,11 +30,14 @@ import static org.mockito.Mockito.when; import javax.annotation.Resource; import org.junit.Before; +import org.junit.Rule; import org.junit.Test; +import org.junit.rules.ExpectedException; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.data.gemfire.test.GemfireTestApplicationContextInitializer; +import org.springframework.expression.EvaluationException; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; @@ -65,6 +69,9 @@ import com.gemstone.gemfire.cache.Region; @SuppressWarnings("unused") public class AnnotationBasedExpirationConfigurationIntegrationTest { + @Rule + public ExpectedException expectedException = ExpectedException.none(); + @Autowired @Qualifier("genericExpiration") private AnnotationBasedExpiration genericExpiration; @@ -125,7 +132,7 @@ public class AnnotationBasedExpirationConfigurationIntegrationTest { public void exampleRegionIdleTimeoutExpirationPolicy() { CustomExpiry expiration = example.getAttributes().getCustomEntryIdleTimeout(); - assertExpiration(expiration.getExpiry(mockRegionEntry(new ApplicationDomainObjectWithTimeToLiveGenericExpirationPolicies())), + assertExpiration(expiration.getExpiry(mockRegionEntry(new ApplicationDomainObjectWithTimeToLiveAndGenericExpirationPolicies())), 60, ExpirationAction.INVALIDATE); assertExpiration(expiration.getExpiry(mockRegionEntry(new ApplicationDomainObjectWithIdleTimeoutExpirationPolicy())), 120, ExpirationAction.INVALIDATE); @@ -143,7 +150,7 @@ public class AnnotationBasedExpirationConfigurationIntegrationTest { public void exampleRegionTimeToLiveExpirationPolicy() { CustomExpiry expiration = example.getAttributes().getCustomEntryTimeToLive(); - assertExpiration(expiration.getExpiry(mockRegionEntry(new ApplicationDomainObjectWithTimeToLiveGenericExpirationPolicies())), + assertExpiration(expiration.getExpiry(mockRegionEntry(new ApplicationDomainObjectWithTimeToLiveAndGenericExpirationPolicies())), 300, ExpirationAction.DESTROY); assertExpiration(defaultExpirationAttributes, expiration.getExpiry(mockRegionEntry(new ApplicationDomainObjectWithIdleTimeoutExpirationPolicy()))); assertExpiration(expiration.getExpiry(mockRegionEntry(new ApplicationDomainObjectWithGenericExpirationPolicy())), @@ -159,7 +166,7 @@ public class AnnotationBasedExpirationConfigurationIntegrationTest { @Test public void genericExpirationPolicy() { assertExpiration(genericExpiration.getExpiry(mockRegionEntry( - new ApplicationDomainObjectWithTimeToLiveGenericExpirationPolicies())), 60, ExpirationAction.INVALIDATE); + new ApplicationDomainObjectWithTimeToLiveAndGenericExpirationPolicies())), 60, ExpirationAction.INVALIDATE); assertThat(genericExpiration.getExpiry(mockRegionEntry( new ApplicationDomainObjectWithIdleTimeoutExpirationPolicy())), is(nullValue())); assertExpiration(genericExpiration.getExpiry(mockRegionEntry( @@ -174,9 +181,25 @@ public class AnnotationBasedExpirationConfigurationIntegrationTest { new RegionEntryGenericExpirationPolicy())), 60, ExpirationAction.DESTROY); } + @Test + public void invalidExpirationAction() { + expectedException.expect(EvaluationException.class); + expectedException.expectCause(isA(IllegalArgumentException.class)); + expectedException.expectMessage(String.format("'%1$s' is not resolvable as a valid ExpirationAction(Type)", + "@expirationProperties['gemfire.region.entry.expiration.invalid.action.string']")); + genericExpiration.getExpiry(mockRegionEntry(new RegionEntryWithInvalidExpirationAction())); + } + + @Test + public void invalidExpirationTimeout() { + expectedException.expect(IllegalArgumentException.class); + expectedException.expectCause(is(nullValue(Throwable.class))); + genericExpiration.getExpiry(mockRegionEntry(new RegionEntryWithInvalidExpirationTimeout())); + } + @Expiration(timeout = "60", action = "INVALIDATE") @TimeToLiveExpiration(timeout = "300", action = "DESTROY") - protected static class ApplicationDomainObjectWithTimeToLiveGenericExpirationPolicies { + protected static class ApplicationDomainObjectWithTimeToLiveAndGenericExpirationPolicies { } @IdleTimeoutExpiration(timeout = "120", action = "INVALIDATE") @@ -205,4 +228,14 @@ public class AnnotationBasedExpirationConfigurationIntegrationTest { protected static class RegionEntryGenericExpirationPolicy { } + @Expiration(timeout = "${gemfire.region.entry.expiration.timeout}", + action = "@expirationProperties['gemfire.region.entry.expiration.invalid.action.string']") + protected static class RegionEntryWithInvalidExpirationAction { + } + + @Expiration(timeout = "${gemfire.region.entry.expiration.invalid.timeout}", + action = "@expirationProperties['gemfire.region.entry.expiration.action.spring.type']") + protected static class RegionEntryWithInvalidExpirationTimeout { + } + }