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 { + } + }