Browse Source

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.
pull/27/head
Greg Turnquist 8 years ago committed by Jens Schauder
parent
commit
b044a13af1
  1. 5
      src/main/java/org/springframework/data/jdbc/repository/config/EnableJdbcRepositories.java
  2. 43
      src/main/java/org/springframework/data/jdbc/repository/config/JdbcConfiguration.java
  3. 170
      src/main/java/org/springframework/data/jdbc/repository/support/JdbcRepositoryFactoryBean.java
  4. 11
      src/test/java/org/springframework/data/jdbc/mybatis/MyBatisHsqlIntegrationTests.java
  5. 12
      src/test/java/org/springframework/data/jdbc/repository/JdbcRepositoryIdGenerationIntegrationTests.java
  6. 12
      src/test/java/org/springframework/data/jdbc/repository/JdbcRepositoryManipulateDbActionsIntegrationTests.java
  7. 15
      src/test/java/org/springframework/data/jdbc/repository/config/EnableJdbcRepositoriesIntegrationTests.java
  8. 353
      src/test/java/org/springframework/data/jdbc/repository/support/JdbcRepositoryFactoryBeanUnitTests.java

5
src/main/java/org/springframework/data/jdbc/repository/config/EnableJdbcRepositories.java

@ -32,18 +32,19 @@ import org.springframework.data.jdbc.repository.support.JdbcRepositoryFactoryBea @@ -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 {};

43
src/main/java/org/springframework/data/jdbc/repository/config/JdbcConfiguration.java

@ -0,0 +1,43 @@ @@ -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> namingStrategy,
Optional<ConversionCustomizer> conversionCustomizer) {
return new JdbcMappingContext(
namingStrategy.orElse(new DefaultNamingStrategy()),
conversionCustomizer.orElse(conversionService -> {}));
}
}

170
src/main/java/org/springframework/data/jdbc/repository/support/JdbcRepositoryFactoryBean.java

@ -16,35 +16,16 @@ @@ -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; @@ -55,141 +36,50 @@ import org.springframework.util.ClassUtils;
* @since 2.0
*/
public class JdbcRepositoryFactoryBean<T extends Repository<S, ID>, S, ID extends Serializable> //
extends TransactionalRepositoryFactoryBeanSupport<T, S, ID> {
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";
extends TransactionalRepositoryFactoryBeanSupport<T, S, ID> implements ApplicationEventPublisherAware {
private final ApplicationEventPublisher applicationEventPublisher;
private final ApplicationContext applicationContext;
JdbcRepositoryFactoryBean(Class<? extends T> repositoryInterface, ApplicationEventPublisher applicationEventPublisher,
ApplicationContext applicationContext) {
private ApplicationEventPublisher publisher;
private JdbcMappingContext mappingContext;
private DataAccessStrategy dataAccessStrategy;
JdbcRepositoryFactoryBean(Class<? extends T> 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;
}
/**
* <p>
* Create the {@link DataAccessStrategy}, by combining all applicable strategies into one.
* </p>
* <p>
* 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}.
* </p>
* Creates the actual {@link RepositoryFactorySupport} instance.
*
* @return
*/
private CascadingDataAccessStrategy createDataAccessStrategy(JdbcMappingContext context) {
DelegatingDataAccessStrategy delegatingDataAccessStrategy = new DelegatingDataAccessStrategy();
List<DataAccessStrategy> 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;
}
private Optional<DataAccessStrategy> createMyBatisDataAccessStrategy() {
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);
}
}
private Optional<DataAccessStrategy> createDefaultAccessStrategy(JdbcMappingContext context,
DelegatingDataAccessStrategy delegatingDataAccessStrategy) {
return Optional.of(new DefaultDataAccessStrategy(new SqlGeneratorSource(context), findOrCreateJdbcOperations(),
context, delegatingDataAccessStrategy));
}
private NamedParameterJdbcOperations findOrCreateJdbcOperations() {
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<NamedParameterJdbcOperations> getNamedParameterJdbcOperations() {
return getBean(NamedParameterJdbcOperations.class, NAMED_PARAMETER_JDBC_OPERATIONS_BEAN_NAME);
@Override
protected RepositoryFactorySupport doCreateRepositoryFactory() {
return new JdbcRepositoryFactory(publisher, mappingContext, dataAccessStrategy);
}
private Optional<JdbcOperations> getJdbcOperations() {
return getBean(JdbcOperations.class, JDBC_OPERATIONS_BEAN_NAME);
}
@Autowired
protected void setMappingContext(JdbcMappingContext mappingContext) {
private Optional<DataSource> getDataSource() {
return getBean(DataSource.class, DATA_SOURCE_BEAN_NAME);
super.setMappingContext(mappingContext);
this.mappingContext = mappingContext;
}
private Optional<NamingStrategy> getNamingStrategy() {
return getBean(NamingStrategy.class, NAMING_STRATEGY_BEAN_NAME);
@Autowired
public void setDataAccessStrategy(DataAccessStrategy dataAccessStrategy) {
this.dataAccessStrategy = dataAccessStrategy;
}
private Optional<ConversionCustomizer> getConversionCustomizer() {
return getBean(ConversionCustomizer.class, CONVERSION_CUSTOMIZER_BEAN_NAME);
}
private <R> Optional<R> getBean(Class<R> type, String name) {
Map<String, R> beansOfType = applicationContext.getBeansOfType(type);
if (beansOfType.size() == 1) {
return beansOfType.values().stream().findFirst();
}
@Override
public void afterPropertiesSet() {
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();
}
}

11
src/test/java/org/springframework/data/jdbc/mybatis/MyBatisHsqlIntegrationTests.java

@ -29,9 +29,8 @@ import org.mybatis.spring.SqlSessionFactoryBean; @@ -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; @@ -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 { @@ -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 { @@ -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();

12
src/test/java/org/springframework/data/jdbc/repository/JdbcRepositoryIdGenerationIntegrationTests.java

@ -26,16 +26,21 @@ import org.junit.ClassRule; @@ -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 { @@ -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 { @@ -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() {

12
src/test/java/org/springframework/data/jdbc/repository/JdbcRepositoryManipulateDbActionsIntegrationTests.java

@ -19,6 +19,7 @@ import static java.util.Arrays.*; @@ -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; @@ -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; @@ -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 { @@ -227,6 +234,11 @@ public class JdbcRepositoryManipulateDbActionsIntegrationTests {
};
}
@Bean
DefaultDataAccessStrategy defaultDataAccessStrategy(JdbcMappingContext context,
@Qualifier("namedParameterJdbcTemplate") NamedParameterJdbcOperations operations) {
return new DefaultDataAccessStrategy(new SqlGeneratorSource(context), operations, context);
}
}
}

15
src/test/java/org/springframework/data/jdbc/repository/config/EnableJdbcRepositoriesIntegrationTests.java

@ -22,12 +22,16 @@ import lombok.Data; @@ -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; @@ -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 { @@ -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);
}
}
}

353
src/test/java/org/springframework/data/jdbc/repository/support/JdbcRepositoryFactoryBeanUnitTests.java

@ -2,338 +2,101 @@ package org.springframework.data.jdbc.repository.support; @@ -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";
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";
ApplicationEventPublisher eventPublisher = mock(ApplicationEventPublisher.class);
ApplicationContext context = mock(ApplicationContext.class);
Map<String, DataSource> dataSources = new HashMap<>();
Map<String, JdbcOperations> jdbcOperations = new HashMap<>();
Map<String, NamedParameterJdbcOperations> namedJdbcOperations = new HashMap<>();
Map<String, SqlSessionFactory> sqlSessionFactories = new HashMap<>();
Map<String, ConversionCustomizer> 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);
}
@Test // DATAJDBC-100
public void exceptionWithUsefulMessage() {
JdbcRepositoryFactoryBean<DummyEntityRepository, DummyEntity, Long> factoryBean = //
new JdbcRepositoryFactoryBean<>(DummyEntityRepository.class, eventPublisher, context);
assertThatExceptionOfType(IllegalStateException.class) //
.isThrownBy(factoryBean::doCreateRepositoryFactory);
}
@Test // DATAJDBC-100
public void singleDataSourceGetsUsedForCreatingRepositoryFactory() {
DataSource expectedDataSource = mock(DataSource.class);
dataSources.put("arbitraryName", expectedDataSource);
JdbcRepositoryFactoryBean<DummyEntityRepository, DummyEntity, Long> factoryBean = //
new JdbcRepositoryFactoryBean<>(DummyEntityRepository.class, eventPublisher, context);
assertThat(factoryBean.doCreateRepositoryFactory()).is(using(expectedDataSource));
}
@Test // DATAJDBC-100
public void multipleDataSourcesGetDisambiguatedByName() {
DataSource expectedDataSource = mock(DataSource.class);
dataSources.put("dataSource", expectedDataSource);
dataSources.put("arbitraryName", mock(DataSource.class));
JdbcRepositoryFactoryBean<DummyEntityRepository, DummyEntity, Long> 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<DummyEntityRepository, DummyEntity, Long> 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<DummyEntityRepository, DummyEntity, Long> 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<DummyEntityRepository, DummyEntity, Long> 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<DummyEntityRepository, DummyEntity, Long> 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<DummyEntityRepository, DummyEntity, Long> 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<DummyEntityRepository, DummyEntity, Long> 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<DummyEntityRepository, DummyEntity, Long> 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<DummyEntityRepository, DummyEntity, Long> 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<DummyEntityRepository, DummyEntity, Long> 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 {
JdbcRepositoryFactoryBean<DummyEntityRepository, DummyEntity, Long> factoryBean;
String sqlSessionFactoryClassName = SqlSessionFactory.class.getName();
StubRepositoryFactorySupport factory;
@Mock ListableBeanFactory beanFactory;
@Mock Repository<?, ?> repository;
@Mock DataAccessStrategy dataAccessStrategy;
@Mock JdbcMappingContext mappingContext;
ShadowingClassLoader classLoader = new ShadowingClassLoader(this.getClass().getClassLoader()) {
@Before
public void setUp() {
@Override
public Class<?> loadClass(String name) throws ClassNotFoundException {
factory = Mockito.spy(new StubRepositoryFactorySupport(repository));
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<Duration, Long>() {
@Nullable
@Override
public Long convert(Duration duration) {
return duration.toHours();
}
});
});
JdbcRepositoryFactoryBean<DummyEntityRepository, DummyEntity, Long> 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<? super RepositoryFactorySupport> using(NamedParameterJdbcOperations expectedOperations) {
Predicate<RepositoryFactorySupport> 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);
// Setup standard configuration
factoryBean = new JdbcRepositoryFactoryBean<>(DummyEntityRepository.class);
}
private Condition<? super RepositoryFactorySupport> using(JdbcOperations expectedOperations) {
@Test
public void setsUpBasicInstanceCorrectly() {
Predicate<RepositoryFactorySupport> predicate = r -> extractNamedParameterJdbcOperations(r)
.getJdbcOperations() == expectedOperations;
factoryBean.setDataAccessStrategy(dataAccessStrategy);
factoryBean.setMappingContext(mappingContext);
factoryBean.afterPropertiesSet();
return new Condition<>(predicate, "uses " + expectedOperations);
assertThat(factoryBean.getObject()).isNotNull();
}
private Condition<? super RepositoryFactorySupport> using(DataSource expectedDataSource) {
Predicate<RepositoryFactorySupport> predicate = r -> {
@Test(expected = IllegalArgumentException.class)
public void requiresListableBeanFactory() {
NamedParameterJdbcOperations namedOperations = extractNamedParameterJdbcOperations(r);
JdbcTemplate jdbcOperations = (JdbcTemplate) namedOperations.getJdbcOperations();
return jdbcOperations.getDataSource() == expectedDataSource;
};
return new Condition<>(predicate, "using " + expectedDataSource);
factoryBean.setBeanFactory(mock(BeanFactory.class));
}
private static <T extends DataAccessStrategy> T findDataAccessStrategy(RepositoryFactorySupport r, Class<T> type) {
/**
* required to trick Mockito on invoking protected getRepository(Class<T> repositoryInterface, Optional<Object>
* customImplementation
*/
private static class StubRepositoryFactorySupport extends RepositoryFactorySupport {
DataAccessStrategy accessStrategy = (DataAccessStrategy) getField(r, ACCESS_STRATEGY_FIELD_NAME_IN_FACTORY);
return findDataAccessStrategy(accessStrategy, type);
}
private final Repository<?, ?> repository;
private static <T extends DataAccessStrategy> T findDataAccessStrategy(DataAccessStrategy accessStrategy,
Class<T> type) {
private StubRepositoryFactorySupport(Repository<?, ?> repository) {
this.repository = repository;
}
if (type.isInstance(accessStrategy))
return (T) accessStrategy;
@Override
public <T> T getRepository(Class<T> repositoryInterface, RepositoryComposition.RepositoryFragments fragments) {
return (T) repository;
}
if (accessStrategy instanceof DelegatingDataAccessStrategy) {
return findDataAccessStrategy((DataAccessStrategy) getField(accessStrategy, "delegate"), type);
@Override
public <T, ID> EntityInformation<T, ID> getEntityInformation(Class<T> domainClass) {
return null;
}
if (accessStrategy instanceof CascadingDataAccessStrategy) {
List<DataAccessStrategy> strategies = (List<DataAccessStrategy>) getField(accessStrategy, "strategies");
return strategies.stream() //
.map((DataAccessStrategy das) -> findDataAccessStrategy(das, type)) //
.filter(Objects::nonNull) //
.findFirst() //
.orElse(null);
@Override
protected Object getTargetRepository(RepositoryInformation metadata) {
return null;
}
return null;
@Override
protected Class<?> getRepositoryBaseClass(RepositoryMetadata metadata) {
return null;
}
}
private static class DummyEntity {
@Id private Long id;
}

Loading…
Cancel
Save