From b044a13af1bb09eea624a2a28e89b8b677a0679f Mon Sep 17 00:00:00 2001 From: Greg Turnquist Date: Thu, 16 Nov 2017 12:48:03 -0600 Subject: [PATCH] DATAJDBC-151 - Reform Spring Data JDBC to stop autoconfiguration In the past, Spring Data JDBC performed autoconfiguration such as gleaning whether or not MyBatis is on the classpath, and also whether or not certain other beans exist. This commit removes such flexible settings and instead wires up a JdbcMappingContext seeking an optional NamingStrategy and optional ConversionCustomizer. The other required beans will alert the end user if they don't exist. All relevant test cases are updated to inject the proper components. All autoconfiguration is being moved outside Spring Data JDBC, to eventually join Spring Boot after being shook out as an independent module. --- .../config/EnableJdbcRepositories.java | 5 +- .../repository/config/JdbcConfiguration.java | 43 +++ .../support/JdbcRepositoryFactoryBean.java | 170 ++------- .../mybatis/MyBatisHsqlIntegrationTests.java | 11 +- ...epositoryIdGenerationIntegrationTests.java | 12 +- ...ryManipulateDbActionsIntegrationTests.java | 12 + ...nableJdbcRepositoriesIntegrationTests.java | 15 +- .../JdbcRepositoryFactoryBeanUnitTests.java | 357 +++--------------- 8 files changed, 179 insertions(+), 446 deletions(-) create mode 100644 src/main/java/org/springframework/data/jdbc/repository/config/JdbcConfiguration.java diff --git a/src/main/java/org/springframework/data/jdbc/repository/config/EnableJdbcRepositories.java b/src/main/java/org/springframework/data/jdbc/repository/config/EnableJdbcRepositories.java index e822e876..6556dc72 100644 --- a/src/main/java/org/springframework/data/jdbc/repository/config/EnableJdbcRepositories.java +++ b/src/main/java/org/springframework/data/jdbc/repository/config/EnableJdbcRepositories.java @@ -32,18 +32,19 @@ import org.springframework.data.jdbc.repository.support.JdbcRepositoryFactoryBea * repositories by default. * * @author Jens Schauder + * @author Greg Turnquist * @since 2.0 */ @Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) @Documented @Inherited -@Import(JdbcRepositoriesRegistrar.class) +@Import({JdbcRepositoriesRegistrar.class, JdbcConfiguration.class}) public @interface EnableJdbcRepositories { /** * Alias for the {@link #basePackages()} attribute. Allows for more concise annotation declarations e.g.: - * {@code @EnableJpaRepositories("org.my.pkg")} instead of {@code @EnableJpaRepositories(basePackages="org.my.pkg")}. + * {@code @EnableJdbcRepositories("org.my.pkg")} instead of {@code @EnableJdbcRepositories(basePackages="org.my.pkg")}. */ String[] value() default {}; diff --git a/src/main/java/org/springframework/data/jdbc/repository/config/JdbcConfiguration.java b/src/main/java/org/springframework/data/jdbc/repository/config/JdbcConfiguration.java new file mode 100644 index 00000000..865a68db --- /dev/null +++ b/src/main/java/org/springframework/data/jdbc/repository/config/JdbcConfiguration.java @@ -0,0 +1,43 @@ +/* + * Copyright 2017 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 + * + * http://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.jdbc.repository.config; + +import java.util.Optional; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.jdbc.mapping.model.ConversionCustomizer; +import org.springframework.data.jdbc.mapping.model.DefaultNamingStrategy; +import org.springframework.data.jdbc.mapping.model.JdbcMappingContext; +import org.springframework.data.jdbc.mapping.model.NamingStrategy; + +/** + * Beans that must be registered for Spring Data JDBC to work. + * + * @author Greg Turnquist + */ +@Configuration +public class JdbcConfiguration { + + @Bean + JdbcMappingContext jdbcMappingContext(Optional namingStrategy, + Optional conversionCustomizer) { + + return new JdbcMappingContext( + namingStrategy.orElse(new DefaultNamingStrategy()), + conversionCustomizer.orElse(conversionService -> {})); + } +} diff --git a/src/main/java/org/springframework/data/jdbc/repository/support/JdbcRepositoryFactoryBean.java b/src/main/java/org/springframework/data/jdbc/repository/support/JdbcRepositoryFactoryBean.java index cb717b33..3e420de8 100644 --- a/src/main/java/org/springframework/data/jdbc/repository/support/JdbcRepositoryFactoryBean.java +++ b/src/main/java/org/springframework/data/jdbc/repository/support/JdbcRepositoryFactoryBean.java @@ -16,35 +16,16 @@ package org.springframework.data.jdbc.repository.support; import java.io.Serializable; -import java.util.List; -import java.util.Map; -import java.util.Optional; -import java.util.stream.Collectors; -import java.util.stream.Stream; -import javax.sql.DataSource; - -import org.apache.ibatis.session.SqlSessionFactory; -import org.springframework.context.ApplicationContext; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.ApplicationEventPublisher; -import org.springframework.data.jdbc.core.CascadingDataAccessStrategy; +import org.springframework.context.ApplicationEventPublisherAware; import org.springframework.data.jdbc.core.DataAccessStrategy; -import org.springframework.data.jdbc.core.DefaultDataAccessStrategy; -import org.springframework.data.jdbc.core.DelegatingDataAccessStrategy; -import org.springframework.data.jdbc.core.SqlGeneratorSource; -import org.springframework.data.jdbc.mapping.model.ConversionCustomizer; -import org.springframework.data.jdbc.mapping.model.DefaultNamingStrategy; import org.springframework.data.jdbc.mapping.model.JdbcMappingContext; -import org.springframework.data.jdbc.mapping.model.NamingStrategy; -import org.springframework.data.jdbc.mybatis.MyBatisDataAccessStrategy; import org.springframework.data.repository.Repository; import org.springframework.data.repository.core.support.RepositoryFactorySupport; import org.springframework.data.repository.core.support.TransactionalRepositoryFactoryBeanSupport; -import org.springframework.data.util.Optionals; -import org.springframework.jdbc.core.JdbcOperations; -import org.springframework.jdbc.core.namedparam.NamedParameterJdbcOperations; -import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate; -import org.springframework.util.ClassUtils; +import org.springframework.util.Assert; /** * Special adapter for Springs {@link org.springframework.beans.factory.FactoryBean} interface to allow easy setup of @@ -55,141 +36,50 @@ import org.springframework.util.ClassUtils; * @since 2.0 */ public class JdbcRepositoryFactoryBean, S, ID extends Serializable> // - extends TransactionalRepositoryFactoryBeanSupport { + extends TransactionalRepositoryFactoryBeanSupport implements ApplicationEventPublisherAware { - private static final String NO_NAMED_PARAMETER_JDBC_OPERATION_ERROR_MESSAGE = // - "No unique NamedParameterJdbcOperation could be found, " // - + "nor JdbcOperations or DataSource to construct one from."; - - private static final String NAMED_PARAMETER_JDBC_OPERATIONS_BEAN_NAME = "namedParameterJdbcTemplate"; - private static final String JDBC_OPERATIONS_BEAN_NAME = "jdbcTemplate"; - private static final String DATA_SOURCE_BEAN_NAME = "dataSource"; - private static final String NAMING_STRATEGY_BEAN_NAME = "namingStrategy"; - private static final String SQL_SESSION_FACTORY_BEAN_NAME = "sqlSessionFactory"; - private static final String CONVERSION_CUSTOMIZER_BEAN_NAME = "conversionCustomizer"; - - private final ApplicationEventPublisher applicationEventPublisher; - private final ApplicationContext applicationContext; - - JdbcRepositoryFactoryBean(Class repositoryInterface, ApplicationEventPublisher applicationEventPublisher, - ApplicationContext applicationContext) { + private ApplicationEventPublisher publisher; + private JdbcMappingContext mappingContext; + private DataAccessStrategy dataAccessStrategy; + JdbcRepositoryFactoryBean(Class repositoryInterface) { super(repositoryInterface); - this.applicationEventPublisher = applicationEventPublisher; - this.applicationContext = applicationContext; } @Override - protected RepositoryFactorySupport doCreateRepositoryFactory() { - - final JdbcMappingContext context = new JdbcMappingContext(findOrCreateNamingStrategy(), findOrCreateConversionCustomizer()); - - return new JdbcRepositoryFactory(applicationEventPublisher, context, createDataAccessStrategy(context)); + public void setApplicationEventPublisher(ApplicationEventPublisher publisher) { + + super.setApplicationEventPublisher(publisher); + this.publisher = publisher; } /** - *

- * Create the {@link DataAccessStrategy}, by combining all applicable strategies into one. - *

- *

- * The challenge is that the {@link DefaultDataAccessStrategy} when used for reading needs a - * {@link DataAccessStrategy} for loading referenced entities (see. - * {@link DefaultDataAccessStrategy#getEntityRowMapper(Class)}. But it should use all configured - * {@link DataAccessStrategy}s for this. This creates a cyclic dependency. In order to build this the - * {@link DefaultDataAccessStrategy} gets passed in a {@link DelegatingDataAccessStrategy} which at the end gets set - * to the full {@link CascadingDataAccessStrategy}. - *

+ * Creates the actual {@link RepositoryFactorySupport} instance. + * + * @return */ - private CascadingDataAccessStrategy createDataAccessStrategy(JdbcMappingContext context) { - - DelegatingDataAccessStrategy delegatingDataAccessStrategy = new DelegatingDataAccessStrategy(); - - List accessStrategies = Stream.of( // - createMyBatisDataAccessStrategy(), // - createDefaultAccessStrategy(context, delegatingDataAccessStrategy) // - ) // - .filter(Optional::isPresent) // - .map(Optional::get) // - .collect(Collectors.toList()); - - CascadingDataAccessStrategy strategy = new CascadingDataAccessStrategy(accessStrategies); - delegatingDataAccessStrategy.setDelegate(strategy); - - return strategy; + @Override + protected RepositoryFactorySupport doCreateRepositoryFactory() { + return new JdbcRepositoryFactory(publisher, mappingContext, dataAccessStrategy); } - private Optional createMyBatisDataAccessStrategy() { + @Autowired + protected void setMappingContext(JdbcMappingContext mappingContext) { - String myBatisSqlSessionFactoryClassName = "org.apache.ibatis.session.SqlSessionFactory"; - ClassLoader classLoader = this.getClass().getClassLoader(); - - if (!ClassUtils.isPresent(myBatisSqlSessionFactoryClassName, classLoader)) { - return Optional.empty(); - } - - try { - - return getBean(classLoader.loadClass(myBatisSqlSessionFactoryClassName), SQL_SESSION_FACTORY_BEAN_NAME) - // note that the cast to SqlSessionFactory happens in a lambda, which is basically a separate class - // thus it won't get loaded if this code path doesn't get executed. - .map(ssf -> new MyBatisDataAccessStrategy((SqlSessionFactory) ssf)); - } catch (ClassNotFoundException e) { - throw new IllegalStateException("Detected MyBatis on classpath but failed to load the class " + myBatisSqlSessionFactoryClassName); - } + super.setMappingContext(mappingContext); + this.mappingContext = mappingContext; } - private Optional createDefaultAccessStrategy(JdbcMappingContext context, - DelegatingDataAccessStrategy delegatingDataAccessStrategy) { - - return Optional.of(new DefaultDataAccessStrategy(new SqlGeneratorSource(context), findOrCreateJdbcOperations(), - context, delegatingDataAccessStrategy)); + @Autowired + public void setDataAccessStrategy(DataAccessStrategy dataAccessStrategy) { + this.dataAccessStrategy = dataAccessStrategy; } - private NamedParameterJdbcOperations findOrCreateJdbcOperations() { + @Override + public void afterPropertiesSet() { - return Optionals.firstNonEmpty( // - this::getNamedParameterJdbcOperations, // - () -> getJdbcOperations().map(NamedParameterJdbcTemplate::new), // - () -> getDataSource().map(NamedParameterJdbcTemplate::new)) // - .orElseThrow(() -> new IllegalStateException(NO_NAMED_PARAMETER_JDBC_OPERATION_ERROR_MESSAGE)); - } - - private NamingStrategy findOrCreateNamingStrategy() { - return getNamingStrategy().orElse(new DefaultNamingStrategy()); - } - - private ConversionCustomizer findOrCreateConversionCustomizer() { - return getConversionCustomizer().orElse(conversionService->{}); - } - - private Optional getNamedParameterJdbcOperations() { - return getBean(NamedParameterJdbcOperations.class, NAMED_PARAMETER_JDBC_OPERATIONS_BEAN_NAME); - } - - private Optional getJdbcOperations() { - return getBean(JdbcOperations.class, JDBC_OPERATIONS_BEAN_NAME); - } - - private Optional getDataSource() { - return getBean(DataSource.class, DATA_SOURCE_BEAN_NAME); - } - - private Optional getNamingStrategy() { - return getBean(NamingStrategy.class, NAMING_STRATEGY_BEAN_NAME); - } - - private Optional getConversionCustomizer() { - return getBean(ConversionCustomizer.class, CONVERSION_CUSTOMIZER_BEAN_NAME); - } - - private Optional getBean(Class type, String name) { - - Map beansOfType = applicationContext.getBeansOfType(type); - - if (beansOfType.size() == 1) { - return beansOfType.values().stream().findFirst(); - } - - return Optional.ofNullable(beansOfType.get(name)); + Assert.notNull(this.dataAccessStrategy, "DataAccessStrategy must not be null!"); + Assert.notNull(this.mappingContext, "MappingContext must not be null!"); + super.afterPropertiesSet(); } } diff --git a/src/test/java/org/springframework/data/jdbc/mybatis/MyBatisHsqlIntegrationTests.java b/src/test/java/org/springframework/data/jdbc/mybatis/MyBatisHsqlIntegrationTests.java index babb18f2..683f06ea 100644 --- a/src/test/java/org/springframework/data/jdbc/mybatis/MyBatisHsqlIntegrationTests.java +++ b/src/test/java/org/springframework/data/jdbc/mybatis/MyBatisHsqlIntegrationTests.java @@ -29,9 +29,8 @@ import org.mybatis.spring.SqlSessionFactoryBean; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Import; -import org.springframework.data.jdbc.repository.JdbcRepositoryIdGenerationIntegrationTests.TestConfiguration; import org.springframework.data.jdbc.repository.config.EnableJdbcRepositories; -import org.springframework.data.jdbc.repository.support.JdbcRepositoryFactory; +import org.springframework.data.jdbc.testing.TestConfiguration; import org.springframework.data.repository.CrudRepository; import org.springframework.jdbc.datasource.embedded.EmbeddedDatabase; import org.springframework.test.context.ContextConfiguration; @@ -43,6 +42,7 @@ import org.springframework.transaction.annotation.Transactional; * Tests the integration with Mybatis. * * @author Jens Schauder + * @author Greg Turnquist */ @ContextConfiguration @Transactional @@ -53,8 +53,6 @@ public class MyBatisHsqlIntegrationTests { @EnableJdbcRepositories(considerNestedRepositories = true) static class Config { - @Autowired JdbcRepositoryFactory factory; - @Bean Class testClass() { return MyBatisHsqlIntegrationTests.class; @@ -75,6 +73,11 @@ public class MyBatisHsqlIntegrationTests { return sqlSessionFactoryBean; } + + @Bean + MyBatisDataAccessStrategy dataAccessStrategy(SqlSessionFactory factory) { + return new MyBatisDataAccessStrategy(factory); + } } @ClassRule public static final SpringClassRule classRule = new SpringClassRule(); diff --git a/src/test/java/org/springframework/data/jdbc/repository/JdbcRepositoryIdGenerationIntegrationTests.java b/src/test/java/org/springframework/data/jdbc/repository/JdbcRepositoryIdGenerationIntegrationTests.java index d4892638..8b69f31d 100644 --- a/src/test/java/org/springframework/data/jdbc/repository/JdbcRepositoryIdGenerationIntegrationTests.java +++ b/src/test/java/org/springframework/data/jdbc/repository/JdbcRepositoryIdGenerationIntegrationTests.java @@ -26,16 +26,21 @@ import org.junit.ClassRule; import org.junit.Rule; import org.junit.Test; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Import; import org.springframework.data.annotation.Id; +import org.springframework.data.jdbc.core.DefaultDataAccessStrategy; +import org.springframework.data.jdbc.core.SqlGeneratorSource; import org.springframework.data.jdbc.mapping.model.DefaultNamingStrategy; +import org.springframework.data.jdbc.mapping.model.JdbcMappingContext; import org.springframework.data.jdbc.mapping.model.NamingStrategy; import org.springframework.data.jdbc.repository.config.EnableJdbcRepositories; import org.springframework.data.jdbc.repository.support.JdbcRepositoryFactory; import org.springframework.data.repository.CrudRepository; +import org.springframework.jdbc.core.namedparam.NamedParameterJdbcOperations; import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.junit4.rules.SpringClassRule; @@ -61,6 +66,11 @@ public class JdbcRepositoryIdGenerationIntegrationTests { return JdbcRepositoryIdGenerationIntegrationTests.class; } + @Bean + DefaultDataAccessStrategy defaultDataAccessStrategy(JdbcMappingContext context, + @Qualifier("namedParameterJdbcTemplate") NamedParameterJdbcOperations operations) { + return new DefaultDataAccessStrategy(new SqlGeneratorSource(context), operations, context); + } } @ClassRule public static final SpringClassRule classRule = new SpringClassRule(); @@ -122,7 +132,7 @@ public class JdbcRepositoryIdGenerationIntegrationTests { @Configuration @ComponentScan("org.springframework.data.jdbc.testing") @EnableJdbcRepositories(considerNestedRepositories = true) - public static class TestConfiguration { + static class TestConfiguration { @Bean Class testClass() { diff --git a/src/test/java/org/springframework/data/jdbc/repository/JdbcRepositoryManipulateDbActionsIntegrationTests.java b/src/test/java/org/springframework/data/jdbc/repository/JdbcRepositoryManipulateDbActionsIntegrationTests.java index c84173d7..47aad67d 100644 --- a/src/test/java/org/springframework/data/jdbc/repository/JdbcRepositoryManipulateDbActionsIntegrationTests.java +++ b/src/test/java/org/springframework/data/jdbc/repository/JdbcRepositoryManipulateDbActionsIntegrationTests.java @@ -19,6 +19,7 @@ import static java.util.Arrays.*; import static org.assertj.core.api.Assertions.*; import junit.framework.AssertionFailedError; + import lombok.Data; import lombok.Getter; import lombok.RequiredArgsConstructor; @@ -31,18 +32,23 @@ import org.junit.ClassRule; import org.junit.Rule; import org.junit.Test; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.context.ApplicationListener; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Import; import org.springframework.data.annotation.Id; import org.springframework.data.annotation.PersistenceConstructor; +import org.springframework.data.jdbc.core.DefaultDataAccessStrategy; +import org.springframework.data.jdbc.core.SqlGeneratorSource; import org.springframework.data.jdbc.core.conversion.DbAction; import org.springframework.data.jdbc.mapping.event.BeforeDelete; import org.springframework.data.jdbc.mapping.event.BeforeSave; +import org.springframework.data.jdbc.mapping.model.JdbcMappingContext; import org.springframework.data.jdbc.repository.config.EnableJdbcRepositories; import org.springframework.data.jdbc.testing.TestConfiguration; import org.springframework.data.repository.CrudRepository; +import org.springframework.jdbc.core.namedparam.NamedParameterJdbcOperations; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.junit4.rules.SpringClassRule; import org.springframework.test.context.junit4.rules.SpringMethodRule; @@ -52,6 +58,7 @@ import org.springframework.test.context.junit4.rules.SpringMethodRule; * executed against the database. * * @author Jens Schauder + * @author Greg Turnquist */ @ContextConfiguration public class JdbcRepositoryManipulateDbActionsIntegrationTests { @@ -227,6 +234,11 @@ public class JdbcRepositoryManipulateDbActionsIntegrationTests { }; } + @Bean + DefaultDataAccessStrategy defaultDataAccessStrategy(JdbcMappingContext context, + @Qualifier("namedParameterJdbcTemplate") NamedParameterJdbcOperations operations) { + return new DefaultDataAccessStrategy(new SqlGeneratorSource(context), operations, context); + } } } diff --git a/src/test/java/org/springframework/data/jdbc/repository/config/EnableJdbcRepositoriesIntegrationTests.java b/src/test/java/org/springframework/data/jdbc/repository/config/EnableJdbcRepositoriesIntegrationTests.java index 3fe18ad4..af534c2d 100644 --- a/src/test/java/org/springframework/data/jdbc/repository/config/EnableJdbcRepositoriesIntegrationTests.java +++ b/src/test/java/org/springframework/data/jdbc/repository/config/EnableJdbcRepositoriesIntegrationTests.java @@ -22,12 +22,16 @@ import lombok.Data; import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.ComponentScan; import org.springframework.data.annotation.Id; -import org.springframework.data.jdbc.repository.JdbcRepositoryIntegrationTests; +import org.springframework.data.jdbc.core.DefaultDataAccessStrategy; +import org.springframework.data.jdbc.core.SqlGeneratorSource; +import org.springframework.data.jdbc.mapping.model.JdbcMappingContext; import org.springframework.data.jdbc.repository.config.EnableJdbcRepositoriesIntegrationTests.TestConfiguration; import org.springframework.data.repository.CrudRepository; +import org.springframework.jdbc.core.namedparam.NamedParameterJdbcOperations; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; @@ -35,6 +39,7 @@ import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; * Tests the {@link EnableJdbcRepositories} annotation. * * @author Jens Schauder + * @author Greg Turnquist */ @RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration(classes = TestConfiguration.class) @@ -69,6 +74,12 @@ public class EnableJdbcRepositoriesIntegrationTests { Class testClass() { return EnableJdbcRepositoriesIntegrationTests.class; } - } + + @Bean + DefaultDataAccessStrategy defaultDataAccessStrategy(JdbcMappingContext context, + @Qualifier("namedParameterJdbcTemplate") NamedParameterJdbcOperations operations) { + return new DefaultDataAccessStrategy(new SqlGeneratorSource(context), operations, context); + } + } } diff --git a/src/test/java/org/springframework/data/jdbc/repository/support/JdbcRepositoryFactoryBeanUnitTests.java b/src/test/java/org/springframework/data/jdbc/repository/support/JdbcRepositoryFactoryBeanUnitTests.java index de2c8556..4d479579 100644 --- a/src/test/java/org/springframework/data/jdbc/repository/support/JdbcRepositoryFactoryBeanUnitTests.java +++ b/src/test/java/org/springframework/data/jdbc/repository/support/JdbcRepositoryFactoryBeanUnitTests.java @@ -2,338 +2,101 @@ package org.springframework.data.jdbc.repository.support; import static org.assertj.core.api.Assertions.*; import static org.mockito.Mockito.*; -import static org.springframework.test.util.ReflectionTestUtils.*; -import java.time.Duration; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Objects; -import java.util.function.Predicate; - -import javax.sql.DataSource; - -import org.apache.ibatis.session.SqlSessionFactory; -import org.assertj.core.api.Condition; +import org.junit.Before; import org.junit.Test; -import org.springframework.context.ApplicationContext; -import org.springframework.context.ApplicationEventPublisher; -import org.springframework.core.convert.ConversionService; -import org.springframework.core.convert.converter.Converter; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.junit.MockitoJUnitRunner; +import org.springframework.beans.factory.BeanFactory; +import org.springframework.beans.factory.ListableBeanFactory; import org.springframework.data.annotation.Id; -import org.springframework.data.jdbc.core.CascadingDataAccessStrategy; import org.springframework.data.jdbc.core.DataAccessStrategy; -import org.springframework.data.jdbc.core.DefaultDataAccessStrategy; -import org.springframework.data.jdbc.core.DelegatingDataAccessStrategy; -import org.springframework.data.jdbc.mapping.model.ConversionCustomizer; import org.springframework.data.jdbc.mapping.model.JdbcMappingContext; -import org.springframework.data.jdbc.mybatis.MyBatisDataAccessStrategy; import org.springframework.data.repository.CrudRepository; +import org.springframework.data.repository.Repository; +import org.springframework.data.repository.core.EntityInformation; +import org.springframework.data.repository.core.RepositoryInformation; +import org.springframework.data.repository.core.RepositoryMetadata; +import org.springframework.data.repository.core.support.RepositoryComposition; import org.springframework.data.repository.core.support.RepositoryFactorySupport; -import org.springframework.instrument.classloading.ShadowingClassLoader; -import org.springframework.jdbc.core.JdbcOperations; -import org.springframework.jdbc.core.JdbcTemplate; -import org.springframework.jdbc.core.namedparam.NamedParameterJdbcOperations; -import org.springframework.lang.Nullable; -import org.springframework.util.ReflectionUtils; /** * Tests the dependency injection for {@link JdbcRepositoryFactoryBean}. * * @author Jens Schauder + * @author Greg Turnquist */ +@RunWith(MockitoJUnitRunner.class) public class JdbcRepositoryFactoryBeanUnitTests { - static final String EXPECTED_JDBC_OPERATIONS_BEAN_NAME = "jdbcTemplate"; - static final String EXPECTED_NAMED_PARAMETER_JDBC_OPERATIONS_BEAN_NAME = "namedParameterJdbcTemplate"; + JdbcRepositoryFactoryBean factoryBean; - static final String ACCESS_STRATEGY_FIELD_NAME_IN_FACTORY = "accessStrategy"; - static final String OPERATIONS_FIELD_NAME_IN_DEFAULT_ACCESS_STRATEGY = "operations"; - private static final String MAPPING_CONTEXT_FIELD_NAME_IN_FACTORY = "context"; + StubRepositoryFactorySupport factory; + @Mock ListableBeanFactory beanFactory; + @Mock Repository repository; + @Mock DataAccessStrategy dataAccessStrategy; + @Mock JdbcMappingContext mappingContext; - ApplicationEventPublisher eventPublisher = mock(ApplicationEventPublisher.class); - ApplicationContext context = mock(ApplicationContext.class); + @Before + public void setUp() { - Map dataSources = new HashMap<>(); - Map jdbcOperations = new HashMap<>(); - Map namedJdbcOperations = new HashMap<>(); - Map sqlSessionFactories = new HashMap<>(); + factory = Mockito.spy(new StubRepositoryFactorySupport(repository)); - Map conversionCustomizers = new HashMap<>(); - - { - - when(context.getBeansOfType(DataSource.class)).thenReturn(dataSources); - when(context.getBeansOfType(JdbcOperations.class)).thenReturn(jdbcOperations); - when(context.getBeansOfType(NamedParameterJdbcOperations.class)).thenReturn(namedJdbcOperations); - when(context.getBeansOfType(SqlSessionFactory.class)).thenReturn(sqlSessionFactories); - when(context.getBeansOfType(ConversionCustomizer.class)).thenReturn(conversionCustomizers); + // Setup standard configuration + factoryBean = new JdbcRepositoryFactoryBean<>(DummyEntityRepository.class); } - @Test // DATAJDBC-100 - public void exceptionWithUsefulMessage() { + @Test + public void setsUpBasicInstanceCorrectly() { - JdbcRepositoryFactoryBean factoryBean = // - new JdbcRepositoryFactoryBean<>(DummyEntityRepository.class, eventPublisher, context); - - assertThatExceptionOfType(IllegalStateException.class) // - .isThrownBy(factoryBean::doCreateRepositoryFactory); + factoryBean.setDataAccessStrategy(dataAccessStrategy); + factoryBean.setMappingContext(mappingContext); + factoryBean.afterPropertiesSet(); + assertThat(factoryBean.getObject()).isNotNull(); } - @Test // DATAJDBC-100 - public void singleDataSourceGetsUsedForCreatingRepositoryFactory() { + @Test(expected = IllegalArgumentException.class) + public void requiresListableBeanFactory() { - DataSource expectedDataSource = mock(DataSource.class); - dataSources.put("arbitraryName", expectedDataSource); - - JdbcRepositoryFactoryBean factoryBean = // - new JdbcRepositoryFactoryBean<>(DummyEntityRepository.class, eventPublisher, context); - - assertThat(factoryBean.doCreateRepositoryFactory()).is(using(expectedDataSource)); + factoryBean.setBeanFactory(mock(BeanFactory.class)); } - @Test // DATAJDBC-100 - public void multipleDataSourcesGetDisambiguatedByName() { + /** + * required to trick Mockito on invoking protected getRepository(Class repositoryInterface, Optional + * customImplementation + */ + private static class StubRepositoryFactorySupport extends RepositoryFactorySupport { - DataSource expectedDataSource = mock(DataSource.class); - dataSources.put("dataSource", expectedDataSource); - dataSources.put("arbitraryName", mock(DataSource.class)); + private final Repository repository; - JdbcRepositoryFactoryBean factoryBean = // - new JdbcRepositoryFactoryBean<>(DummyEntityRepository.class, eventPublisher, context); - - assertThat(factoryBean.doCreateRepositoryFactory()).is(using(expectedDataSource)); - } - - @Test // DATAJDBC-100 - public void singleJdbcOperationsUsedForCreatingRepositoryFactory() { - - JdbcOperations expectedOperations = mock(JdbcOperations.class); - jdbcOperations.put("arbitraryName", expectedOperations); - - JdbcRepositoryFactoryBean factoryBean = // - new JdbcRepositoryFactoryBean<>(DummyEntityRepository.class, eventPublisher, context); - - assertThat(factoryBean.doCreateRepositoryFactory()).is(using(expectedOperations)); - } - - @Test // DATAJDBC-100 - public void multipleJdbcOperationsGetDisambiguatedByName() { - - JdbcOperations expectedOperations = mock(JdbcOperations.class); - jdbcOperations.put(EXPECTED_JDBC_OPERATIONS_BEAN_NAME, expectedOperations); - jdbcOperations.put("arbitraryName", mock(JdbcOperations.class)); - - JdbcRepositoryFactoryBean factoryBean = // - new JdbcRepositoryFactoryBean<>(DummyEntityRepository.class, eventPublisher, context); - - assertThat(factoryBean.doCreateRepositoryFactory()).is(using(expectedOperations)); - } - - @Test // DATAJDBC-100 - public void singleNamedJdbcOperationsUsedForCreatingRepositoryFactory() { - - NamedParameterJdbcOperations expectedOperations = mock(NamedParameterJdbcOperations.class); - namedJdbcOperations.put("arbitraryName", expectedOperations); - - JdbcRepositoryFactoryBean factoryBean = // - new JdbcRepositoryFactoryBean<>(DummyEntityRepository.class, eventPublisher, context); - - assertThat(factoryBean.doCreateRepositoryFactory()).is(using(expectedOperations)); - } - - @Test // DATAJDBC-100 - public void multipleNamedJdbcOperationsGetDisambiguatedByName() { - - NamedParameterJdbcOperations expectedOperations = mock(NamedParameterJdbcOperations.class); - namedJdbcOperations.put(EXPECTED_NAMED_PARAMETER_JDBC_OPERATIONS_BEAN_NAME, expectedOperations); - namedJdbcOperations.put("arbitraryName", mock(NamedParameterJdbcOperations.class)); - - JdbcRepositoryFactoryBean factoryBean = // - new JdbcRepositoryFactoryBean<>(DummyEntityRepository.class, eventPublisher, context); - - assertThat(factoryBean.doCreateRepositoryFactory()).is(using(expectedOperations)); - } - - @Test // DATAJDBC-100 - public void namedParameterJdbcOperationsTakePrecedenceOverDataSource() { - - NamedParameterJdbcOperations expectedOperations = mock(NamedParameterJdbcOperations.class); - namedJdbcOperations.put("arbitraryName", expectedOperations); - dataSources.put("arbitraryName", mock(DataSource.class)); - - JdbcRepositoryFactoryBean factoryBean = // - new JdbcRepositoryFactoryBean<>(DummyEntityRepository.class, eventPublisher, context); - - assertThat(factoryBean.doCreateRepositoryFactory()).is(using(expectedOperations)); - } - - @Test // DATAJDBC-100 - public void jdbcOperationsTakePrecedenceOverDataSource() { - - JdbcOperations expectedOperations = mock(JdbcOperations.class); - jdbcOperations.put("arbitraryName", expectedOperations); - dataSources.put("arbitraryName", mock(DataSource.class)); - - JdbcRepositoryFactoryBean factoryBean = // - new JdbcRepositoryFactoryBean<>(DummyEntityRepository.class, eventPublisher, context); - - assertThat(factoryBean.doCreateRepositoryFactory()).is(using(expectedOperations)); - } - - @Test // DATAJDBC-100 - public void namedParameterJdbcOperationsTakePrecedenceOverJdbcOperations() { - - NamedParameterJdbcOperations expectedOperations = mock(NamedParameterJdbcOperations.class); - namedJdbcOperations.put("arbitraryName", expectedOperations); - jdbcOperations.put("arbitraryName", mock(JdbcOperations.class)); - - JdbcRepositoryFactoryBean factoryBean = // - new JdbcRepositoryFactoryBean<>(DummyEntityRepository.class, eventPublisher, context); - - assertThat(factoryBean.doCreateRepositoryFactory()).is(using(expectedOperations)); - } - - @Test // DATAJDBC-123 - public void withoutSqlSessionFactoryThereIsNoMyBatisIntegration() { - - dataSources.put("anyname", mock(DataSource.class)); - sqlSessionFactories.clear(); - - JdbcRepositoryFactoryBean factoryBean = // - new JdbcRepositoryFactoryBean<>(DummyEntityRepository.class, eventPublisher, context); - - RepositoryFactorySupport factory = factoryBean.doCreateRepositoryFactory(); - - assertThat(findDataAccessStrategy(factory, MyBatisDataAccessStrategy.class)).isNull(); - } - - @Test // DATAJDBC-123 - public void withSqlSessionFactoryThereIsMyBatisIntegration() { - - dataSources.put("anyname", mock(DataSource.class)); - sqlSessionFactories.put("anyname", mock(SqlSessionFactory.class)); - - JdbcRepositoryFactoryBean factoryBean = // - new JdbcRepositoryFactoryBean<>(DummyEntityRepository.class, eventPublisher, context); - - RepositoryFactorySupport factory = factoryBean.doCreateRepositoryFactory(); - - assertThat(findDataAccessStrategy(factory, MyBatisDataAccessStrategy.class)).isNotNull(); - } - - @Test // DATAJDBC-136 - public void canBeLoadedWithoutMyBatis() throws Exception { - - String sqlSessionFactoryClassName = SqlSessionFactory.class.getName(); - - ShadowingClassLoader classLoader = new ShadowingClassLoader(this.getClass().getClassLoader()) { - - @Override - public Class loadClass(String name) throws ClassNotFoundException { - - if (name.equals(sqlSessionFactoryClassName)) { - throw new ClassNotFoundException("%s is configured not to get loaded by this classloader"); - } - return super.loadClass(name); - } - }; - - Class loadedClass = classLoader.loadClass(JdbcRepositoryFactoryBean.class.getName()); - - assertThat(loadedClass).isNotNull(); - ReflectionUtils.getAllDeclaredMethods(loadedClass); - } - - @Test // DATAJDBC-147 - public void registersConversionsCorrectly() { - - dataSources.put("anyname", mock(DataSource.class)); - conversionCustomizers.put("anyname", cs -> { - - cs.addConverter(new Converter() { - - @Nullable - @Override - public Long convert(Duration duration) { - return duration.toHours(); - } - }); - }); - - JdbcRepositoryFactoryBean factoryBean = // - new JdbcRepositoryFactoryBean<>(DummyEntityRepository.class, eventPublisher, context); - - RepositoryFactorySupport factory = factoryBean.doCreateRepositoryFactory(); - - JdbcMappingContext mappingContext = (JdbcMappingContext) getField(factory, MAPPING_CONTEXT_FIELD_NAME_IN_FACTORY); - ConversionService conversions = mappingContext.getConversions(); - - assertThat(conversions.convert(Duration.ofDays(3), Long.class)).isEqualTo(72L); - } - - private Condition using(NamedParameterJdbcOperations expectedOperations) { - - Predicate predicate = r -> extractNamedParameterJdbcOperations(r) == expectedOperations; - return new Condition<>(predicate, "uses " + expectedOperations); - } - - private NamedParameterJdbcOperations extractNamedParameterJdbcOperations(RepositoryFactorySupport r) { - - DefaultDataAccessStrategy defaultDataAccessStrategy = findDataAccessStrategy(r, DefaultDataAccessStrategy.class); - return (NamedParameterJdbcOperations) getField(defaultDataAccessStrategy, - OPERATIONS_FIELD_NAME_IN_DEFAULT_ACCESS_STRATEGY); - } - - private Condition using(JdbcOperations expectedOperations) { - - Predicate predicate = r -> extractNamedParameterJdbcOperations(r) - .getJdbcOperations() == expectedOperations; - - return new Condition<>(predicate, "uses " + expectedOperations); - } - - private Condition using(DataSource expectedDataSource) { - - Predicate predicate = r -> { - - NamedParameterJdbcOperations namedOperations = extractNamedParameterJdbcOperations(r); - JdbcTemplate jdbcOperations = (JdbcTemplate) namedOperations.getJdbcOperations(); - return jdbcOperations.getDataSource() == expectedDataSource; - }; - - return new Condition<>(predicate, "using " + expectedDataSource); - } - - private static T findDataAccessStrategy(RepositoryFactorySupport r, Class type) { - - DataAccessStrategy accessStrategy = (DataAccessStrategy) getField(r, ACCESS_STRATEGY_FIELD_NAME_IN_FACTORY); - return findDataAccessStrategy(accessStrategy, type); - } - - private static T findDataAccessStrategy(DataAccessStrategy accessStrategy, - Class type) { - - if (type.isInstance(accessStrategy)) - return (T) accessStrategy; - - if (accessStrategy instanceof DelegatingDataAccessStrategy) { - return findDataAccessStrategy((DataAccessStrategy) getField(accessStrategy, "delegate"), type); + private StubRepositoryFactorySupport(Repository repository) { + this.repository = repository; } - if (accessStrategy instanceof CascadingDataAccessStrategy) { - List strategies = (List) getField(accessStrategy, "strategies"); - return strategies.stream() // - .map((DataAccessStrategy das) -> findDataAccessStrategy(das, type)) // - .filter(Objects::nonNull) // - .findFirst() // - .orElse(null); + @Override + public T getRepository(Class repositoryInterface, RepositoryComposition.RepositoryFragments fragments) { + return (T) repository; } - return null; - } + @Override + public EntityInformation getEntityInformation(Class domainClass) { + return null; + } + @Override + protected Object getTargetRepository(RepositoryInformation metadata) { + return null; + } + + @Override + protected Class getRepositoryBaseClass(RepositoryMetadata metadata) { + return null; + } + } + private static class DummyEntity { @Id private Long id; }