0

I'm implementing caching in my Spring Boot (v2.5.2) app using GemFire.

The class which needs to be cached -

import org.springframework.data.annotation.Id;
import org.springframework.data.gemfire.mapping.annotation.Region;

import com.auth0.jwk.Jwk;

@Region("jwks")
public class Key {

   @Id
   private String id;

   private Jwk jwk;

   @PersistenceConstructor
   public Key(String id, Jwk jwk){
       this.id = id;
       this.jwk = jwk;
   }

   // Getters and setters for variables
}

I get the following error while fetching the stored entity from cache -

org.apache.geode.SerializationException : While deserializing query result with root cause
java.lang.NoSuchMethodException: com.auth0.jwk.Jwk.<init>()
at java.lang.Class.getConstructors0(Class.java:3082)
at java.lang.Class.getDeclaredConstructor(Class.java:2187)
...

How can I deserialize this object when Jwk doesn't have a no-arg constructor?

Edit - build.gradle

implementation 'org.springframework.geode:spring-geode-starter'
implementation 'org.springframework.geode:spring-data-geode'

Also, updated the constructor as per suggested answer which continues to give the same error.

Error causing code -

Optional<Key> key = this.keyRepository.findById(id);
Vaishnavi Killekar
  • 457
  • 1
  • 8
  • 22

1 Answers1

1

First, please be precise when you ask questions.

Your Key class is not even valid Java. The Key class by itself would not even compile as is.

The constructors (JsonWebKey) in this case are 1) not named after the class (Key) and 2) you cannot have 2 no arg constructors.

I am assuming the second constructor would be:

public Key(String id, Jwk jwk) {
  this.id = id;
  this.jwk = jwk;
}

I am guessing either the constructors are misnamed or perhaps are part of some [static] inner class??

Never-the-less, assuming you are using Spring Boot for Apache Geode (and VMware Tanzu (Pivotal) GemFire), a.k.a. SBDG (see here; if you are not using SBDG, you should be!), and since SBDG enables GemFire/Geode PDX serialization by default, then SBDG with the help of Spring Data for Apache Geode (and VMware Tanzu GemFire), a.k.a. SDG (upon which SBDG is built, along with the core Spring Framework, Spring Data and Spring Boot) will handle these serialization concerns for you, with no extra effort on your part. See the corresponding SDG documentation.

By way of example, I wrote this test class, which resides in this package, to demonstrate.

By default, the test class is using PDX serialization configured with Spring using the test configuration. The Spring-based configuration class is here. The other test configuration classes are only enabled with the appropriate test Spring profile configured.

The model class for this test is the CompositeValue class in the model sub-package.

As you can see, the class has 2 primary constructors. I also declared a commented-out, default, public no-arg constructor, which I will explain further below.

This CompositeValue model class was designed very deliberately. You will notice that it is (mostly) immutable.

I use a Spring Data CrudRepository (see here) to save (persist/store) an instance of CompositeValue in a GemFire/Geode Region ("Values") and then retrieve it (findBy..). The "Values" Region is necessarily a PARTITION Region (see here), since a PARTITION Region stores values in serialized form, and in our case, PDX serialized.

Using the Spring configuration, the test runs and passes successfully! Spring is doing all the heavy lifting!

If you are NOT using Spring to its fullest extent, then you are (most likely) going to have problems unless you know what you are doing with GemFire/Geode.

Out-of-the-box, GemFire/Geode PDX serialization has certain limitations. See the documentation. Specifically, see here.

For instance, if you are using GemFire/Geode's ReflectionBasedAutoSerializer class (Javadoc, documentation; and I suspect you are) and not Spring, then it requires your application domain objects (model classes / entities) to have a default, public no-arg constructor.

This flies in the face of immutable, effectively immutable and mostly immutable classes since then you cannot appropriately initialize classes using constructors, which is crucial in a highly concurrent, multi-Threaded context, like GemFire/Geode.

You can see the effects of trying to use GemFire/Geode's ReflectionBasedAutoSerializer by enabling the "gemfire" profile in the example test class I wrote, for which the configuration is here.

The test will NOT pass without the commented-out, default, public no-arg constructor.

When using Apache Geode 1.13.4, I get the following error:

2021-11-01 11:53:08,720  WARN ode.pdx.internal.AutoSerializableManager: 274 - Class 
io.stackoverflow.questions.spring.geode.serialization.pdx.model.CompositeValue 
matched with '.*' cannot be auto-serialized due to missing public no-arg constructor.
Will attempt using Java serialization.

However, even with Java serialization (the GemFire/Geode backup serialization strategy), the test results in a failure:

Caused by: java.io.NotSerializableException: io.stackoverflow.questions.spring.geode.serialization.pdx.model.CompositeValue
    at java.io.ObjectOutputStream.writeObject0(ObjectOutputStream.java:1184)
    at java.io.ObjectOutputStream.writeObject(ObjectOutputStream.java:348)
    at org.apache.geode.internal.InternalDataSerializer.writeSerializableObject(InternalDataSerializer.java:2184)
    at org.apache.geode.internal.InternalDataSerializer.basicWriteObject(InternalDataSerializer.java:2058)
    at org.apache.geode.DataSerializer.writeObject(DataSerializer.java:2839)
    at org.apache.geode.internal.cache.CachedDeserializableFactory.calcSerializedSize(CachedDeserializableFactory.java:245)
    ... 60 common frames omitted

Well, the java.io.NotSerializableException is thrown because the CompositeValue class (deliberately) does not implement the java.io.Serialiable interface.

Why deliberately? Because you cannot implement java.io.Serializable on classes you do not own, which is true when using 3rd party libraries and their classes. Even though we own the CompositeValue in my case, I am making a point, because in your case, you don't own Jwk.

So, not only can we not use (mostly/effectively) immutable classes, we also cannot rely on default serialization mechanisms baked into GemFire/Geode.

Of course, we can handle this by implementing a custom PdxSerializer, the second strategy in the documentation (under "Procedure", Step 1, "Serializing your Domain Object with a PdxSerializer").

If we again change the active Spring profile to "gemfire-custom-pdxserializer" in the example test I wrote, then the test will pass.

But, it comes at a high price! See the necessary configuration to make this arrangement work.

In our case, we have only 1 such model / entity class to build a custom PdxSerializer for. However, imagine if we had hundreds of classes to handle.

To make matters worse, GemFire/Geode only allows a single PdxSerializer to be registered with a Singleton GemFire/Geode cache, which means you can only have 1. Now you must rely on the Composite Software Design Pattern to compose multiple PdxSerializers necessary to handle all your application domain model types requiring serialization. While this is elegant, you must build a custom PdxSerializer per application model / entity type. Of course, you could bake all type handling into 1 PdxSerializer implementation, but that would get ugly rather quickly!

Finally, your application model / entity types could implement GemFire/Geode's PdxSerializable interface (Javadoc). This is no better than java.io.Serializable and (again) does not work for types you don't own. It also couples your application to GemFire/Geode and is why I did NOT demonstrate this approach, as it should be considered an anti-pattern.

With SDG's MappingPdxSerializer, which SBDG's auto-configures for you (by default), you do not need to do any of the above. SBDG auto-configures PDX by default (making the SDG @EnablePdx annotation unnecessary) and there is no special requirement for your application domain object (model) / entity classes.

However, if you have more than 1 constructor in your entity class, then you will need to designate 1 constructor as the primary persistence constructor. In the CompositeValue class, this constructor was designated as the primary, persistence constructor using Spring Data's @PersistenceConstructor annotation, which SDG's MappingPdxSerializer takes into account when deserializing and constructing your application domain object model types.

If you only have 1 constructor in your class, then you do not even need to declare the @PersistenceConstructor annotation on the only constructor. That is if the other constructor in CompositeValue did not exist, then the @PersistenceConstructor annotation on this constructor would not be necessary. SD[G] can figure it out.

Feel free to play around with my example test for you learning purposes.

John Blum
  • 7,381
  • 1
  • 20
  • 30
  • I added the `@PersistenceConstructor` annotation on the parameterized constructor, but that did not resolve the issue. Is there anything else that can be done? – Vaishnavi Killekar Nov 03 '21 at 14:56
  • Well, if you are using the GemFire/Geode `ReflectionBasedAutoSerializer`, then of course the `@PersistentencConstructor` annotation from SD is not going to work. The Spring annotations only work when the SDG `@MappingPdxSerializer` is in play. – John Blum Nov 03 '21 at 17:16
  • Are you using SBDG? How have you configured serialization for the cache? – John Blum Nov 03 '21 at 17:17
  • I've added the dependencies in the question. Please refer them. I'm not using `ReflectionBasedAutoSerializer`. Currently, there is no explicit setup for deserialization. – Vaishnavi Killekar Nov 05 '21 at 06:27
  • I've added the constructor code I'm currently using along with the one which fetches it that throws the error - `org.springframework.data.mapping.model.MappingInstantiationException: Failed to instantiate com.auth0.jwk.Jwk using constructor NO_CONSTRUCTOR with arguments connection with root cause java.lang.NoSuchMethodException: com.auth0.jwk.Jwk.()` – Vaishnavi Killekar Nov 05 '21 at 08:26