Files
spring-boot-data-geode/spring-geode-docs/src/docs/asciidoc/_includes/geode-api-ext.adoc
Jay Bryant f614e6dbd4 Edit the intro to the Apache Geode API Extensions chapter in the reference documentation.
Mostly fixed small things and a couple of sentences that were long enough to be hard to parse.

Resolves gh-106.
2021-06-15 17:19:05 -07:00

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.