Mostly fixed small things and a couple of sentences that were long enough to be hard to parse. Resolves gh-106.
386 lines
19 KiB
Plaintext
386 lines
19 KiB
Plaintext
[[geode-api-extensions]]
|
|
== Apache Geode API Extensions
|
|
:geode-name: Apache Geode
|
|
:images-dir: ./images
|
|
|
|
|
|
The Spring Boot for {geode-name} (SBDG) project includes the `org.springframework.geode:apache-geode-extensions` module
|
|
to make use of the {geode-name} {apache-geode-javadoc}[APIs] in a Spring and non-Spring application context more
|
|
effective. While this module is relatively new, it already contains several necessary API extensions.
|
|
|
|
{geode-name} {apache-geode-javadoc}[APIs] can be complex and difficult to use properly, especially without prior
|
|
knowledge or experience. Users often get things wrong, which is why Spring's APIs
|
|
for {geode-name} are invaluable. They lead users to do the right thing from the start, first and foremost.
|
|
|
|
However, this module does not exist only to help users use {geode-name}'s APIs more effectively. In fact, with
|
|
Spring's abstractions, it should not be necessary to use {geode-name} APIs at all -- for example, when using the Spring Cache
|
|
Abstraction for caching or the Spring Data Repository abstraction for DAO development. This module is necessary
|
|
to enable Spring for {geode-name} to overcome and adapt to the limitations of the API in a Spring application context.
|
|
That lets Spring for {geode-name} offer an experience familiar to Spring users.
|
|
|
|
In general, Spring shields users from design problems as well as changes in third party library APIs that could
|
|
adversely affect your Spring applications when integrating with these libraries. Spring's APIs provide a layer of
|
|
indirection along with enhanced capabilities (such as exception translation).
|
|
|
|
TIP: Spring Data for {geode-name} (SDG) also {spring-data-geode-docs-html}/#apis[offers] some relief when using
|
|
{geode-name}'s APIs.
|
|
|
|
[[geode-api-extensions-cacheresolver]]
|
|
=== `SimpleCacheResolver`
|
|
|
|
In some cases, it is necessary to acquire a reference to the cache instance in your application components at runtime.
|
|
For example, you might want to create a temporary `Region` on the fly in order to aggregate data for analysis.
|
|
|
|
Typically, you already know the type of cache your application is using since you must declare your application to be
|
|
either a client (i.e. `ClientCache`) in the {apache-geode-docs}/topologies_and_comm/cs_configuration/chapter_overview.html[client/server topology],
|
|
or a {apache-geode-docs}/topologies_and_comm/p2p_configuration/chapter_overview.html[peer member/node] in the cluster
|
|
(i.e. `Cache`) on startup. This is expressed in configuration when creating the cache instance required to interact with
|
|
the {geode-name} data management system. In most cases, your application will be a client and SBDG makes this decision
|
|
easy since it _auto-configures_ a `ClientCache` instance, <<geode-clientcache-applications,by default>>.
|
|
|
|
In a Spring context, the cache instance created by the framework is a managed bean in the Spring container. As such,
|
|
it is a simple matter to inject a reference to the _Singleton_ cache bean into any other managed application component.
|
|
|
|
.Autowired Cache Reference using Dependency Injection (DI)
|
|
[source,java]
|
|
----
|
|
@Service
|
|
class CacheMonitoringService {
|
|
|
|
@Autowired
|
|
ClientCache clientCache;
|
|
|
|
// use the clientCache object reference to monitor the cache as necessary
|
|
|
|
}
|
|
----
|
|
|
|
However, in cases where your application component or class is not managed by Spring and you need a reference to the
|
|
cache instance at runtime, SBDG provides the abstract `org.springframework.geode.cache.SimpleCacheResolver` class
|
|
(see {spring-boot-data-geode-javadoc}/org/springframework/geode/cache/SimpleCacheResolver.html[Javadoc]).
|
|
|
|
.`SimpleCacheResolver` API
|
|
[source, java ]
|
|
----
|
|
package org.springframework.geode.cache;
|
|
|
|
abstract class SimpleCacheResolver {
|
|
|
|
<T extends GemFireCache> T require() { }
|
|
|
|
<T extends GemFireCache> Optional<T> resolve() { }
|
|
|
|
Optional<ClientCache> resolveClientCache() { }
|
|
|
|
Optional<Cache> resolvePeerCache() { }
|
|
|
|
}
|
|
----
|
|
|
|
`SimpleCacheResolver` adheres to https://en.wikipedia.org/wiki/SOLID[SOLID OO Principles]. This class is abstract and
|
|
extensible so users can change the algorithm used to resolve client or peer cache instances as well as mock its methods
|
|
in _Unit Tests_.
|
|
|
|
Additionally, each method is precise. For example, `resolveClientCache()` will only resolve a reference to a cache if
|
|
the cache instance is a "client"! If a cache exists, but is a "peer" instance, then `resolveClientCache()` returns
|
|
`Optional.EMPTY`. The behavior of `resolvePeerCache()` is similar.
|
|
|
|
`require()` returns a non-`Optional` reference to a cache instance throwing an `IllegalStateException` if a cache
|
|
is not present.
|
|
|
|
[[geode-api-extensions-cacheutils]]
|
|
=== `CacheUtils`
|
|
|
|
Under-the-hood, `SimpleCacheResolver` delegates some of its functions to the
|
|
{spring-boot-data-geode-javadoc}/org/springframework/geode/util/CacheUtils.html[`CacheUtils`]
|
|
abstract utility class, which provides additional, convenient capabilities when using a cache.
|
|
|
|
While there are utility methods to determine whether a cache instance (i.e. `GemFireCache`) or _Region_ is a client
|
|
or a peer, one of the more useful functions is to extract all the values from a _Region_.
|
|
|
|
To extract all the values stored in a _Region_ call `CacheUtils.collectValues(:Region<?, T>)`. This method returns a
|
|
`Collection<T>` containing all the values stored in the given _Region_. The method is smart, and knows how to handle
|
|
the `Region` appropriately regardless of whether the `Region` is a client or peer `Region`. This distinction is
|
|
important since client `PROXY` _Regions_ store no values.
|
|
|
|
WARNING: Caution is advised when getting all values from a _Region_. While getting filtered reference values from a
|
|
non-transactional, reference data only [`REPLICATE`] _Region_ is quite useful, getting all values from a transactional,
|
|
[`PARTITION`] _Region_ can prove quite detrimental, especially in production. Getting all values from a _Region_ can be
|
|
useful during testing.
|
|
|
|
[[geode-api-extensions-membership]]
|
|
=== `MembershipListenerAdapter` & `MembershipEvent`
|
|
|
|
Another useful API hidden by {geode-name} is the membership events and listener interface. This API is especially useful
|
|
on the server-side when your Spring Boot application is serving as a peer member of an {geode-name} distributed system.
|
|
|
|
When a peer member is disconnected from the distributed system, perhaps due to a network failure, the member is forcibly
|
|
removed from the cluster. This node immediately enters a reconnecting state, trying to establish a connection back to
|
|
the cluster. Once reconnected, the peer member must rebuild all cache objects (i.e. `Cache`, `Regions`, `Indexes`,
|
|
`DiskStores`, etc). All previous cache objects are now invalid and their references stale.
|
|
|
|
As you can imagine, in a Spring context this is particularly problematic since most {geode-name} objects are _Singleton_
|
|
beans declared in and managed by the Spring container. Those beans may be injected and used in other framework and
|
|
application components. For instance, `Regions` are injected into SDG's `GemfireTemplate`, Spring Data _Repositories_
|
|
and possibly application-specific _Data Access Objects_ (https://en.wikipedia.org/wiki/Data_access_object[DAO]).
|
|
|
|
If references to those cache objects become stale on a forced disconnect event, then there is no way to auto-wire fresh
|
|
object references into the dependent application or framework components when the peer member is reconnected unless the
|
|
Spring `ApplicationContext` is "refreshed". In fact, there is no way to even know that this event has occurred since the
|
|
{geode-name} `MembershipListener` API and corresponding events are "internal".
|
|
|
|
NOTE: The Spring team have explored the idea of creating proxies for all types of cache objects (i.e. `Cache`, `Regions`,
|
|
`Indexes`, `DiskStores`, `AsyncEventQueues`, `GatewayReceivers`, `GatewaySenders`, etc) used by Spring. The proxies
|
|
would know how to obtain a "fresh" reference on a reconnect event. However, this turns out to be more problematic than
|
|
it is worth. It is simply easier to "refresh" the Spring `ApplicationContext`, although no less cheap. Neither way is
|
|
ideal. See https://jira.spring.io/browse/SGF-921[SGF-921] and https://jira.spring.io/browse/SGF-227[SGF-227]
|
|
for further details.
|
|
|
|
In the case where membership events are useful to the Spring Boot application, SBDG provides the following
|
|
{spring-boot-data-geode-javadoc}/org/springframework/geode/distributed/event/package-frame.html[API]:
|
|
|
|
* {spring-boot-data-geode-javadoc}/org/springframework/geode/distributed/event/MembershipListenerAdapter.html[`MembershipListenerAdapter`]
|
|
* {spring-boot-data-geode-javadoc}/org/springframework/geode/distributed/event/MembershipEvent.html[`MembershipEvent`]
|
|
|
|
The abstract `MembershipListenerAdapter` class implements {geode-name}'s clumsy
|
|
`org.apache.geode.distributed.internal.MembershipListener` interface to simplify the event handler method signatures by
|
|
using an appropriate `MembershipEvent` type to encapsulate the actors in the event.
|
|
|
|
The abstract `MembershipEvent` class is further subclassed to represent specific membership event types that occur
|
|
within the {geode-name} system:
|
|
|
|
* {spring-boot-data-geode-javadoc}/org/springframework/geode/distributed/event/support/MemberDepartedEvent.html[`MemberDepartedEvent`]
|
|
* {spring-boot-data-geode-javadoc}/org/springframework/geode/distributed/event/support/MemberJoinedEvent.html[`MemberJoinedEvent`]
|
|
* {spring-boot-data-geode-javadoc}/org/springframework/geode/distributed/event/support/MemberSuspectEvent.html[`MemberSuspectEvent`]
|
|
* {spring-boot-data-geode-javadoc}/org/springframework/geode/distributed/event/support/QuorumLostEvent.html[`QuorumLostEvent`]
|
|
|
|
The API is depicted in this UML diagram:
|
|
|
|
image::{images-dir}/membership-api-uml.png[]
|
|
|
|
The membership event type is further categorized with an appropriate enumerated value,
|
|
{spring-boot-data-geode-javadoc}/org/springframework/geode/distributed/event/MembershipEvent.Type.html[`MembershipEvent.Type`],
|
|
as a property of the `MembershipEvent` itself (see {spring-boot-data-geode-javadoc}/org/springframework/geode/distributed/event/MembershipEvent.html#getType--[`getType()`]).
|
|
|
|
The type hierarchy is useful in `instanceof` expressions while the `Enum` is useful in `switch` statements.
|
|
|
|
You can see 1 particular implementation of the `MembershipListenerAdapter` with the
|
|
{spring-boot-data-geode-javadoc}/org/springframework/geode/distributed/event/ApplicationContextMembershipListener.html[`ApplicationContextMembershipListener`] class,
|
|
which does exactly as we described above, handling forced-disconnect/auto-reconnect membership events inside a
|
|
Spring context in order to refresh the Spring `ApplicationContext`.
|
|
|
|
[[geode-api-extensions-pdx]]
|
|
=== PDX
|
|
|
|
{geode-name}'s PDX serialization framework is yet another API that falls short of a complete stack.
|
|
|
|
For instance, there is no easy or direct way to serialize an object as PDX bytes. It is also not possible to modify an
|
|
existing `PdxInstance` by adding or removing fields since it requires a new PDX type. In this case, you must create a
|
|
new `PdxInstance` and copy from the existing `PdxInstance`. Unfortunately, the {geode-name} API offers no assistance.
|
|
It is also not possible to use PDX in a client, local-only mode without a server since the PDX type registry is only
|
|
available and managed on servers in a cluster. All of this leaves much to be desired.
|
|
|
|
[[geode-api-extensions-pdx-builder]]
|
|
==== `PdxInstanceBuilder`
|
|
|
|
In such cases, SBDG conveniently provides the
|
|
{spring-boot-data-geode-javadoc}/org/springframework/geode/pdx/PdxInstanceBuilder.html[`PdxInstanceBuilder`] class,
|
|
appropriately named after the https://en.wikipedia.org/wiki/Builder_pattern[_Builder Software Design Pattern_].
|
|
The `PdxInstanceBuilder` also offers a fluent API for constructing `PdxInstances`.
|
|
|
|
.`PdxInstanceBuilder` API
|
|
[source,java]
|
|
----
|
|
class PdxInstanceBuilder {
|
|
|
|
PdxInstanceFactory copy(PdxInstance pdx);
|
|
|
|
Factory from(Object target);
|
|
|
|
}
|
|
----
|
|
|
|
For example, you could serialize an application domain object as PDX bytes with the following code:
|
|
|
|
.Serializing an Object to PDX
|
|
[source,java]
|
|
----
|
|
@Component
|
|
class CustomerSerializer {
|
|
|
|
PdxInstance serialize(Customer customer) {
|
|
|
|
return PdxInstanceBuilder.create()
|
|
.from(customer)
|
|
.create();
|
|
}
|
|
}
|
|
----
|
|
|
|
You could then modify the `PdxInstance` by copying from the original:
|
|
|
|
.Copy `PdxInstance`
|
|
[source,java]
|
|
----
|
|
@Component
|
|
class CustomerDecorator {
|
|
|
|
@Autowired
|
|
CustomerSerializer serializer;
|
|
|
|
PdxIntance decorate(Customer customer) {
|
|
|
|
PdxInstance pdxCustomer = serializer.serialize(customer);
|
|
|
|
return PdxInstanceBuilder.create()
|
|
.copy(pdxCustomer)
|
|
.writeBoolean("vip", isImportant(customer))
|
|
.create();
|
|
}
|
|
}
|
|
----
|
|
|
|
[[geode-api-extensions-pdx-wrapper]]
|
|
==== `PdxInstanceWrapper`
|
|
|
|
SBDG also provides the {spring-boot-data-geode-javadoc}/org/springframework/geode/pdx/PdxInstanceWrapper.html[`PdxInstanceWrapper`]
|
|
class to wrap an existing `PdxInstance` in order to provide more control during the conversion from PDX to JSON and from
|
|
JSON back into a POJO. Specifically, the wrapper gives users more control over the configuration of Jackson's
|
|
`ObjectMapper`.
|
|
|
|
The `ObjectMapper` constructed by {geode-name}'s own `PdxInstance` implementation (`PdxInstanceImpl`) is not
|
|
configurable nor was it configured correctly. And unfortunately, since `PdxInstance` is not extensible, the `getObject()`
|
|
method fails miserably when converting the JSON generated from PDX back into a POJO for any practical application domain
|
|
model type.
|
|
|
|
.Wrapping an existing `PdxInstance`
|
|
[source,java]
|
|
----
|
|
PdxInstanceWrapper wrapper = PdxInstanceWrapper.from(pdxInstance);
|
|
----
|
|
|
|
For all operations on `PdxInstance` except `getObject()`, the wrapper delegates to the underlying `PdxInstance` method
|
|
implementation called by the user.
|
|
|
|
In addition to the decorated `getObject()` method, the `PdxInstanceWrapper` provides a thorough implementation of the
|
|
`toString()` method. The state of the `PdxInstance` is output in a JSON-like String.
|
|
|
|
Finally, the `PdxInstanceWrapper` class adds a `getIdentifier()` method. Rather than put the burden on the user to have
|
|
to iterate the field names of the `PdxInstance` to determine whether a field is the identity field, and then call
|
|
`getField(..)` with the field name to get the ID (value), assuming an identity field was marked in the first place,
|
|
the `PdxInstanceWrapper` class provides the `getIdentifier()` method to return the ID of the `PdxInstance` directly.
|
|
|
|
The `getIdentifier()` method is smart in that it first iterates the fields of the `PdxInstance` asking if the field is
|
|
the identity field. If no field was marked as the "identity" field, then the algorithm searches for a field named "id".
|
|
If no field with the name "id" exists, then the algorithm searches for a metadata field called "@identifier", which
|
|
refers to the field that is the identity field of the `PdxInstance`.
|
|
|
|
The `@identifier` metadata field is useful in cases where the `PdxInstance` originated from JSON and the application
|
|
domain object uses a natural identifier, rather than a surrogate ID, such as `Book.isbn`.
|
|
|
|
NOTE: {geode-name}'s `JSONFormatter` is not capable of marking the identity field of a `PdxInstance` originating
|
|
from JSON.
|
|
|
|
WARNING: It is not currently possible to implement the `PdxInstance` interface and store instances of this type as a
|
|
value in a _Region_. {geode-name} naively assumes that all `PdxInstance` objects are an implementation created by
|
|
{geode-name} itself (i.e. `PdxInstanceImpl`), which has a tight coupling to the PDX type registry. An Exception is
|
|
thrown if you try to store instances of your own `PdxInstance` implementation.
|
|
|
|
[[geode-api-extensions-pdx-adapter]]
|
|
==== `ObjectPdxInstanceAdapter`
|
|
|
|
In rare cases, it might be necessary to treat an `Object` as a `PdxInstance` depending on the context without incurring
|
|
the overhead of serializing an `Object` to PDX. For such cases, SBDG offers the `ObjectPdxInstanceAdapter` class.
|
|
|
|
This might be true when calling a method with a parameter expecting an argument, or returning an instance, of type
|
|
`PdxInstance`, particularly when {geode-name}'s `read-serialized` PDX configuration property is set to `true`, and only
|
|
an object is available in the current context.
|
|
|
|
Under-the-hood, SBDG's `ObjectPdxInstanceAdapter` class uses Spring's
|
|
{spring-framework-javadoc}/org/springframework/beans/BeanWrapper.html[`BeanWrapper`] class along with _Java's
|
|
Introspection & Reflection_ functionality to adapt the given `Object` in order to access it using the full
|
|
{apache-geode-javadoc}/org/apache/geode/pdx/PdxInstance.html[`PdxInstance`] API. This includes the use of the
|
|
{apache-geode-javadoc}/org/apache/geode/pdx/WritablePdxInstance.html[`WritablePdxInstance`] API, obtained from
|
|
{apache-geode-javadoc}/org/apache/geode/pdx/PdxInstance.html#createWriter--[`PdxInstance.createWriter()`], to modify
|
|
the underlying `Object` as well.
|
|
|
|
Like the `PdxInstanceWrapper` class, `ObjectPdxInstanceAdapter` contains special logic to resolve the identity field
|
|
and ID of the `PdxInstance`, including consideration for Spring Data's
|
|
{spring-data-commons-javadoc}/org/springframework/data/annotation/Id.html[`@Id`] mapping annotation, which can be
|
|
introspected in this case given the underlying `Object` backing the `PdxInstance` is a POJO.
|
|
|
|
Clearly, the `ObjectPdxInstanceAdapter.getObject()` method will return the given, wrapped `Object` used to construct
|
|
the `ObjectPdxInstanceAdapter`, and is therefore, automatically "_deserializable_", as determined by the
|
|
{apache-geode-javadoc}/org/apache/geode/pdx/PdxInstance.html#isDeserializable--[`PdxInstance.isDeseriable()`] method,
|
|
which always returns true.
|
|
|
|
To adapt any `Object` as a `PdxInstance`, simply do:
|
|
|
|
.Adapt an `Object` as a `PdxInstance`
|
|
[source,java]
|
|
----
|
|
class OfflineObjectToPdxInstanceConverter {
|
|
|
|
@NonNull PdxInstance convert(@NonNull Object target) {
|
|
return ObjectPdxInstanceAdapter.from(target);
|
|
}
|
|
}
|
|
----
|
|
|
|
Once the adapter is created, you can use it to access data on the underlying `Object`.
|
|
|
|
For example, given a `Customer` class:
|
|
|
|
.`Customer` class
|
|
[source,java]
|
|
----
|
|
@Region("Customers")
|
|
class Customer {
|
|
|
|
@Id
|
|
private Long id;
|
|
|
|
String name;
|
|
|
|
// constructors, getters and setters omitted
|
|
|
|
}
|
|
----
|
|
|
|
Then accessing an instance of `Customer` using the `PdxInstance` API is as easy as:
|
|
|
|
.Accessing an `Object` using the `PdxInstance` API
|
|
[source,java]
|
|
----
|
|
class ObjectPdxInstanceAdapterTest {
|
|
|
|
@Test
|
|
public void getAndSetObjectProperties() {
|
|
|
|
Customer jonDoe = new Customer(1L, "Jon Doe");
|
|
|
|
PdxInstance adapter = ObjectPdxInstanceAdapter.from(jonDoe);
|
|
|
|
assertThat(jonDoe.getName()).isEqualTo("Jon Doe");
|
|
assertThat(adapter.getField("name")).isEqualTo("Jon Doe");
|
|
|
|
adapter.createWriter().setField("name", "Jane Doe");
|
|
|
|
assertThat(adapter.getField("name")).isEqualTo("Jane Doe");
|
|
assertThat(jonDoe.getName()).isEqualTo("Jane Doe");
|
|
}
|
|
}
|
|
----
|
|
|
|
[[geode-api-extensions-security]]
|
|
=== Security
|
|
|
|
For testing purposes, SBDG provides a test implementation of {geode-name}'s {apache-geode-javadoc}/org/apache/geode/security/SecurityManager.html[`SecurityManager`]
|
|
interface that simply expects the password to match the username (case-sensitive) when authenticating.
|
|
|
|
By default, all operations are authorized.
|
|
|
|
To match the expectations of SBDG's `TestSecurityManager`, SBDG additionally provides a test implementation of
|
|
{geode-name}'s {apache-geode-javadoc}/org/apache/geode/security/AuthInitialize.html[`AuthInitialize`] interface that
|
|
supplies matching credentials for both the username and password.
|