Search code examples
springspring-dataspring-data-jpaspring-data-gemfiregeode

How to cache PageImpl with Spring Data Geode?


When trying to cache a PageImpl response from a Spring Data JpaRepository using Spring Data Geode, it fails to cache the result with the following error:

Caused by: org.springframework.beans.BeanInstantiationException: Failed to instantiate [org.springframework.data.domain.PageImpl]: No default constructor found; nested exception is java.lang.NoSuchMethodException: org.springframework.data.domain.PageImpl.<init>()
at org.springframework.beans.BeanUtils.instantiateClass(BeanUtils.java:127) ~[spring-beans-5.0.6.RELEASE.jar:5.0.6.RELEASE]
at org.springframework.data.convert.ReflectionEntityInstantiator.createInstance(ReflectionEntityInstantiator.java:64) ~[spring-data-commons-2.0.7.RELEASE.jar:2.0.7.RELEASE]
at org.springframework.data.convert.ClassGeneratingEntityInstantiator.createInstance(ClassGeneratingEntityInstantiator.java:86) ~[spring-data-commons-2.0.7.RELEASE.jar:2.0.7.RELEASE]
at org.springframework.data.gemfire.mapping.MappingPdxSerializer.fromData(MappingPdxSerializer.java:422) ~[spring-data-gemfire-2.0.7.RELEASE.jar:2.0.7.RELEASE]
at org.apache.geode.pdx.internal.PdxReaderImpl.basicGetObject(PdxReaderImpl.java:741) ~[geode-core-9.1.1.jar:?]
at org.apache.geode.pdx.internal.PdxReaderImpl.getObject(PdxReaderImpl.java:682) ~[geode-core-9.1.1.jar:?]
at org.apache.geode.internal.InternalDataSerializer.readPdxSerializable(InternalDataSerializer.java:3054) ~[geode-core-9.1.1.jar:?]

It looks like the MappingPdxSerializer looks for a default constructor but doesn't find it for a PageImpl class.

Here is maven pom for the dependencies I have:

<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.1.0.BUILD-SNAPSHOT</version>
    <relativePath/> <!-- lookup parent from repository -->
</parent>

<properties>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
    <java.version>1.8</java.version>
    <spring-cloud.version>Finchley.BUILD-SNAPSHOT</spring-cloud.version>
</properties>

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter</artifactId>
        <exclusions>
            <exclusion>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-logging</artifactId>
            </exclusion>
        </exclusions>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-log4j2</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-actuator</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-cache</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-jpa</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-rest</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-hateoas</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.data</groupId>
        <artifactId>spring-data-gemfire</artifactId>
    </dependency>
    <dependency>
        <groupId>javax.cache</groupId>
        <artifactId>cache-api</artifactId>
        <scope>runtime</scope>
    </dependency>
    <dependency>
        <groupId>org.apache.commons</groupId>
        <artifactId>commons-text</artifactId>
        <version>1.3</version>
    </dependency>
    <dependency>
        <groupId>commons-codec</groupId>
        <artifactId>commons-codec</artifactId>
        <version>1.11</version>
    </dependency>
    <dependency>
        <groupId>org.apache.commons</groupId>
        <artifactId>commons-lang3</artifactId>
        <version>3.6</version>
    </dependency>

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-configuration-processor</artifactId>
        <optional>true</optional>
    </dependency>

    <dependency>
        <groupId>org.postgresql</groupId>
        <artifactId>postgresql</artifactId>
        <version>42.2.2</version>
    </dependency>
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <optional>true</optional>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <scope>test</scope>
    </dependency>
</dependencies>

The JpaRepository I am using is:

@RepositoryRestResource
public interface RecordRepository extends JpaRepository<Record, Long>
{
    @Override
    @CacheEvict(cacheNames = { "Records" })
    <S extends Record> S save(S s);

    @Override
    @Cacheable(value = "Records")
    Optional<Record> findById(Long id);

    @Override
    @Cacheable(value = "Records", key = "#pageable.pageNumber + '.' + #pageable.pageSize + '.records'")
    Page<Record> findAll(Pageable pageable);

    @Override
    @Cacheable(value = "Records")
    Record getOne(Long aLong);
}

The code used to invoke a repository paged result is:

int PAGE=0,PAGE_SIZE=100;
        Page<Record> recordPage;
        do {
            recordPage = recordRepository.findAll(PageRequest.of(PAGE, PAGE_SIZE));
            log.info("Retrieved page: [{}]", recordPage);
        } while (recordPage.hasNext());

I feel like it maybe a possible bug with the MappingPdxSerializer, but I'm not 100% sure. Any help in resolving this issue would be awesome!

Thanks


Solution

  • Why do you feel this is a possible bug with Spring Data Geode's (SDG) o.s.d.g.mapping.MappingPdxSerializer?

    It is quite common, and even expected, that not all objects passed through SDG's MappingPdxSerializer will have a default (i.e. public, no-arg) constructor.

    When using such types in your application (e.g. like the SD PageImpl class) and an instance of that type is read from Apache Geode (e.g. get(key)), the object is de-serialized and reconstructed on the (Region) data access operation (providing Apache Geode's read-serialized configuration attribute is not set to true; which cause you other problems and not recommended in this case), then you need to register an EntityInstantiator that informs SDG's MappingPdxSerializer how to instantiate the object, using an appropriate constructor.

    The "appropriate" constructor is determined by the persistent entity's PreferredConstructor, which is evaluated during type evaluation by the SD Mapping Infrastructure, and can be specified with the @PersistenceContructor annotation, if necessary. This is useful in cases where you are using 1 of SD's canned EntityIntantiator types, e.g. ReflectionEntityInstantiator, and your application domain type has more than 1 non-default constructor.

    Therefore, you can register 1 or more EntityInstantiator objects per application domain object by type using the EntityIntantiatiors composite class, perhaps with a "mapping" between application domain object Class type (e.g. Page) and EntityInstantiator, and then register the EntityInstantiators on SDG's MappingPdxSerializer.

    Of course, you need to make sure that custom configured MappingPdxSerializer gets used by Apache Geode...

    @Configuration
    class ApacheGeodeConfiration {
    
      @Bean
      MappingPdxSerializer pdxSerializer() {
    
        Map<Class<?>, EntityInstantiator> customInstantiators = new HashMap<>();
    
        customInstantiators.put(Page.class, new MyPageEntityInstantiator());
        customInstantiators.put...
    
        MappingPdxSerializer pdxSerializer = 
          MappingPdxSerializer.newMappingPdxSerializer();
    
        pdxSerializer.setGemfireInstantiators(
          new EntityInstantiators(customInstantiators));
    
        return pdxSerializer;
      }
    
      @Bean
      CacheFactoryBean gemfireCache(MappingPdxSerializer pdxSerializer) {
    
        CacheFactoryBean gemfireCache = new CacheFactoryBean();
    
        gemfireCache.setPdxSerializer(pdxSerializer);
        gemfireCache.set...
    
        return gemfireCache;
      }
    
      ...
    }
    

    Hope this helps!

    -j