Support reactive repositories in ResourceReaderRepositoryPopulator.

We now support reactive repositories in addition to imperative repositories when populating repositories from a resource reader.

Instead of RepositoryInvokerFactory, we now use a RepositoryPersisterFactory to avoid introducing reactive support to RepositoryInvokerFactory.
This commit is contained in:
Mark Paluch
2022-04-28 15:48:36 +02:00
committed by Oliver Drotbohm
parent f28100663a
commit e5540b460a
3 changed files with 393 additions and 15 deletions

View File

@@ -15,35 +15,49 @@
*/
package org.springframework.data.repository.init;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import java.io.IOException;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.reactivestreams.Publisher;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.context.ApplicationEventPublisherAware;
import org.springframework.core.io.Resource;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.core.io.support.ResourcePatternResolver;
import org.springframework.data.repository.support.DefaultRepositoryInvokerFactory;
import org.springframework.data.repository.CrudRepository;
import org.springframework.data.repository.core.CrudMethods;
import org.springframework.data.repository.core.RepositoryInformation;
import org.springframework.data.repository.core.RepositoryMetadata;
import org.springframework.data.repository.core.support.DefaultCrudMethods;
import org.springframework.data.repository.reactive.ReactiveCrudRepository;
import org.springframework.data.repository.support.Repositories;
import org.springframework.data.repository.support.RepositoryInvoker;
import org.springframework.data.repository.support.RepositoryInvokerFactory;
import org.springframework.data.repository.util.ReactiveWrapperConverters;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.ReflectionUtils;
/**
* A {@link RepositoryPopulator} using a {@link ResourceReader} to read objects from the configured {@link Resource}s.
*
* @author Oliver Gierke
* @author Christoph Strobl
* @author Mark Paluch
* @since 1.4
*/
public class ResourceReaderRepositoryPopulator implements RepositoryPopulator, ApplicationEventPublisherAware {
private static final Log logger = LogFactory.getLog(ResourceReaderRepositoryPopulator.class);
private static final Log logger = LogFactory.getLog(ResourceReaderRepositoryPopulator.class);
private final ResourceReader reader;
private final @Nullable ClassLoader classLoader;
@@ -114,7 +128,7 @@ public class ResourceReaderRepositoryPopulator implements RepositoryPopulator, A
Assert.notNull(repositories, "Repositories must not be null!");
RepositoryInvokerFactory invokerFactory = new DefaultRepositoryInvokerFactory(repositories);
RepositoryPersisterFactory persisterFactory = new RepositoryPersisterFactory(repositories);
for (Resource resource : resources) {
@@ -125,13 +139,13 @@ public class ResourceReaderRepositoryPopulator implements RepositoryPopulator, A
if (result instanceof Collection) {
for (Object element : (Collection<?>) result) {
if (element != null) {
persist(element, invokerFactory);
persist(element, persisterFactory);
} else {
logger.info("Skipping null element found in unmarshal result!");
}
}
} else {
persist(result, invokerFactory);
persist(result, persisterFactory);
}
}
@@ -158,12 +172,172 @@ public class ResourceReaderRepositoryPopulator implements RepositoryPopulator, A
* Persists the given {@link Object} using a suitable repository.
*
* @param object must not be {@literal null}.
* @param invokerFactory must not be {@literal null}.
* @param persisterFactory must not be {@literal null}.
*/
private void persist(Object object, RepositoryInvokerFactory invokerFactory) {
private void persist(Object object, RepositoryPersisterFactory persisterFactory) {
RepositoryInvoker invoker = invokerFactory.getInvokerFor(object.getClass());
logger.debug(String.format("Persisting %s using repository %s", object, invoker));
invoker.invokeSave(object);
RepositoryPersister persister = persisterFactory.getPersisterFor(object.getClass());
logger.debug(String.format("Persisting %s using repository %s", object, persister));
persister.save(object);
}
/**
* Factory to create {@link RepositoryPersister} instances.
*/
static class RepositoryPersisterFactory {
private final Map<Class<?>, RepositoryPersister> persisters = new HashMap<>();
private final Repositories repositories;
public RepositoryPersisterFactory(Repositories repositories) {
this.repositories = repositories;
}
/**
* Obtain a {@link RepositoryPersister}.
*
* @param domainType
* @return
*/
public RepositoryPersister getPersisterFor(Class<?> domainType) {
return persisters.computeIfAbsent(domainType, this::createPersisterFor);
}
private RepositoryPersister createPersisterFor(Class<?> domainType) {
RepositoryInformation repositoryInformation = repositories.getRequiredRepositoryInformation(domainType);
Object repository = repositories.getRepositoryFor(domainType).orElseThrow(
() -> new IllegalArgumentException(String.format("No repository found for domain type: %s", domainType)));
if (repositoryInformation.isReactiveRepository()) {
return repository instanceof ReactiveCrudRepository ? new ReactiveCrudRepositoryPersister(repository)
: new ReflectiveReactivePersister(repositoryInformation, repository);
}
if (repository instanceof CrudRepository) {
return new CrudRepositoryPersister(repository);
}
return new ReflectivePersister(repositoryInformation, repository);
}
}
/**
* Interface defining a save method to persist an object within a repository.
*/
interface RepositoryPersister {
/**
* Saves the {@code object} in an appropriate repository.
*
* @param object
*/
void save(Object object);
}
/**
* Reflection variant of a {@link RepositoryPersister}.
*/
private static class ReflectivePersister implements RepositoryPersister {
private final CrudMethods methods;
private final Object repository;
public ReflectivePersister(RepositoryMetadata metadata, Object repository) {
this.methods = new DefaultCrudMethods(metadata);
this.repository = repository;
}
@Override
public void save(Object object) {
doPersist(object);
}
Object doPersist(Object object) {
Method method = methods.getSaveMethod()//
.orElseThrow(() -> new IllegalStateException("Repository doesn't have a save-method declared!"));
return ReflectionUtils.invokeMethod(method, repository, object);
}
@Override
public String toString() {
return repository.toString();
}
}
/**
* Reactive extension to save objects in a reactive repository.
*/
private static class ReflectiveReactivePersister extends ReflectivePersister {
public ReflectiveReactivePersister(RepositoryMetadata metadata, Object repository) {
super(metadata, repository);
}
@Override
public void save(Object object) {
Object wrapper = doPersist(object);
Publisher<?> publisher = ReactiveWrapperConverters.toWrapper(wrapper, Publisher.class);
if (!(publisher instanceof Mono)) {
publisher = Flux.from(publisher).collectList();
}
Mono.from(publisher).block();
}
}
/**
* {@link RepositoryPersister} to operate with {@link CrudRepository}.
*/
private static class CrudRepositoryPersister implements RepositoryPersister {
private final CrudRepository<Object, Object> repository;
@SuppressWarnings("unchecked")
public CrudRepositoryPersister(Object repository) {
Assert.isInstanceOf(CrudRepository.class, repository);
this.repository = (CrudRepository<Object, Object>) repository;
}
@Override
public void save(Object object) {
repository.save(object);
}
@Override
public String toString() {
return repository.toString();
}
}
/**
* {@link RepositoryPersister} to operate with {@link ReactiveCrudRepository}.
*/
private static class ReactiveCrudRepositoryPersister implements RepositoryPersister {
private final ReactiveCrudRepository<Object, Object> repository;
@SuppressWarnings("unchecked")
public ReactiveCrudRepositoryPersister(Object repository) {
Assert.isInstanceOf(ReactiveCrudRepository.class, repository);
this.repository = (ReactiveCrudRepository<Object, Object>) repository;
}
@Override
public void save(Object object) {
repository.save(object).block();
}
@Override
public String toString() {
return repository.toString();
}
}
}

View File

@@ -17,15 +17,13 @@ package org.springframework.data.repository.core.support;
import static org.mockito.Mockito.*;
import java.io.Serializable;
import org.springframework.data.mapping.context.SampleMappingContext;
import org.springframework.data.repository.Repository;
/**
* @author Oliver Gierke
*/
public class DummyRepositoryFactoryBean<T extends Repository<S, ID>, S, ID extends Serializable>
public class DummyRepositoryFactoryBean<T extends Repository<S, ID>, S, ID>
extends RepositoryFactoryBeanSupport<T, S, ID> {
private final T repository;

View File

@@ -0,0 +1,206 @@
/*
* Copyright 2012-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.repository.init;
import static org.mockito.ArgumentMatchers.*;
import static org.mockito.Mockito.*;
import io.reactivex.rxjava3.core.Single;
import reactor.core.publisher.Mono;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationEvent;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.Resource;
import org.springframework.data.repository.Repository;
import org.springframework.data.repository.core.support.DummyRepositoryFactoryBean;
import org.springframework.data.repository.core.support.RepositoryFactoryBeanSupport;
import org.springframework.data.repository.reactive.ReactiveCrudRepository;
import org.springframework.data.repository.reactive.RxJava3CrudRepository;
import org.springframework.data.repository.support.Repositories;
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
/**
* Unit tests for {@link ResourceReaderRepositoryPopulator} using reactive repositories.
*
* @author Mark Paluch
*/
@SpringJUnitConfig(classes = ReactiveResourceReaderRepositoryPopulatorUnitTests.ReactiveSampleConfiguration.class)
class ReactiveResourceReaderRepositoryPopulatorUnitTests {
@Autowired ReactivePersonRepository personRepository;
@Autowired ReactiveContactRepository contactRepository;
@Autowired RxJavaUserRepository userRepository;
@Autowired Repositories repositories;
ApplicationEventPublisher publisher;
ResourceReader reader;
Resource resource;
@BeforeEach
void setUp() {
this.reader = mock(ResourceReader.class);
this.publisher = mock(ApplicationEventPublisher.class);
this.resource = mock(Resource.class);
}
@Test
void storesSingleUsingReactiveRepositoryObjectCorrectly() throws Exception {
ReactivePerson reference = new ReactivePerson();
when(personRepository.save(reference)).thenReturn(Mono.just(reference));
setUpReferenceAndInitialize(reference);
verify(personRepository).save(reference);
}
@Test
void storesSingleUsingSimpleReactiveRepositoryObjectCorrectly() throws Exception {
ReactiveContact reference = new ReactiveContact();
when(contactRepository.save(reference)).thenReturn(Mono.just(reference));
setUpReferenceAndInitialize(reference);
verify(contactRepository).save(reference);
}
@Test
void storesSingleUsingRxJavaRepositoryObjectCorrectly() throws Exception {
ReactiveUser reference = new ReactiveUser();
when(userRepository.save(reference)).thenReturn(Single.just(reference));
setUpReferenceAndInitialize(reference);
verify(userRepository).save(reference);
}
@Test
void emitsRepositoriesPopulatedEventIfPublisherConfigured() throws Exception {
ReactivePerson reference = new ReactivePerson();
when(personRepository.save(reference)).thenReturn(Mono.just(reference));
RepositoryPopulator populator = setUpReferenceAndInitialize(reference, publisher);
ApplicationEvent event = new RepositoriesPopulatedEvent(populator, repositories);
verify(publisher, times(1)).publishEvent(event);
}
private RepositoryPopulator setUpReferenceAndInitialize(Object reference, ApplicationEventPublisher publish)
throws Exception {
when(reader.readFrom(any(), any())).thenReturn(reference);
ResourceReaderRepositoryPopulator populator = new ResourceReaderRepositoryPopulator(reader);
populator.setResources(resource);
populator.setApplicationEventPublisher(publisher);
populator.populate(repositories);
return populator;
}
private RepositoryPopulator setUpReferenceAndInitialize(Object reference) throws Exception {
return setUpReferenceAndInitialize(reference, null);
}
@Configuration
static class ReactiveSampleConfiguration {
@Autowired ApplicationContext context;
@Bean
Repositories repositories() {
return new Repositories(context);
}
@Bean
ReactivePersonRepository personRepository() {
return mock(ReactivePersonRepository.class);
}
@Bean
RepositoryFactoryBeanSupport<Repository<ReactivePerson, Object>, ReactivePerson, Object> personRepositoryFactory(
ReactivePersonRepository personRepository) {
DummyRepositoryFactoryBean<Repository<ReactivePerson, Object>, ReactivePerson, Object> factoryBean = new DummyRepositoryFactoryBean<>(
ReactivePersonRepository.class);
factoryBean.setCustomImplementation(personRepository);
return factoryBean;
}
@Bean
ReactiveContactRepository contactRepository() {
return mock(ReactiveContactRepository.class);
}
@Bean
RepositoryFactoryBeanSupport<Repository<ReactiveContact, Object>, ReactiveContact, Object> contactRepositoryFactory(
ReactiveContactRepository contactRepository) {
DummyRepositoryFactoryBean<Repository<ReactiveContact, Object>, ReactiveContact, Object> factoryBean = new DummyRepositoryFactoryBean<>(
ReactiveContactRepository.class);
factoryBean.setCustomImplementation(contactRepository);
return factoryBean;
}
@Bean
RxJavaUserRepository userRepository() {
return mock(RxJavaUserRepository.class);
}
@Bean
RepositoryFactoryBeanSupport<Repository<ReactiveUser, Object>, ReactiveUser, Object> userRepositoryFactory(
RxJavaUserRepository userRepository) {
DummyRepositoryFactoryBean<Repository<ReactiveUser, Object>, ReactiveUser, Object> factoryBean = new DummyRepositoryFactoryBean<>(
RxJavaUserRepository.class);
factoryBean.setCustomImplementation(userRepository);
return factoryBean;
}
}
static class ReactivePerson {
}
static class ReactiveContact {
}
static class ReactiveUser {
}
interface ReactivePersonRepository extends ReactiveCrudRepository<ReactivePerson, Object> {
}
interface ReactiveContactRepository extends Repository<ReactiveContact, Object> {
Mono<ReactiveContact> save(ReactiveContact contact);
}
interface RxJavaUserRepository extends RxJava3CrudRepository<ReactiveUser, Object> {
}
}