У меня есть приложение Spring Boot 2.3, использующее Spring JPA, Spring HATEAOS, Hibernate, Spring Cache, Spring Data REST и т. Д. c. У меня около 460 тестов junit, разбитых примерно на 70 файлов. Каждый файл выглядит так:
@RunWith(SpringRunner.class)
@ContextConfiguration
@SpringBootTest(classes = TestApplication.class)
@TestPropertySource(locations = "classpath:application-test.properties")
@Transactional
@Log4j2
public class SalesOrderTests {
@Before
@WithMockAdminUser
public void beforeTests() {
testDbUtils.truncateDB();
TenantContext.setCurrentTenantId("test");
when(tenantRepository.findByTenantId("test")).thenReturn(testDbUtils.fakeTenant());
}
// MANY TESTS
Mine - это мультитенантное приложение; в моем тесте я использую тот же Db logi c production, чтобы быть уверенным, что все будет работать. Я использую подход отдельной базы данных для каждого клиента на Mysql.
Поэтому у меня есть поставщик подключения с несколькими арендаторами:
*
@Component
@Log4j2
public class MultiTenantConnectionProviderImpl implements MultiTenantConnectionProvider {
private static final long serialVersionUID = 3193007611085791247L;
@Autowired
private ConnectionPoolManager connectionPoolManager;
@Override
public void releaseAnyConnection(Connection connection) throws SQLException {
connection.close();
}
@Override
public Connection getAnyConnection() throws SQLException {
return connectionPoolManager.getConnection(TenantContext.TENANT_DEFAULT);
}
@Override
public Connection getConnection(String tenantId) throws SQLException {
log.trace("getConnection " + tenantId);
Connection connection = connectionPoolManager.getConnection(tenantId);
return connection;
}
@Override
public void releaseConnection(String tenantId, Connection connection) throws SQLException {
log.trace("releaseConnection " + tenantId);
connection.close();
}
@Override
public boolean supportsAggressiveRelease() {
return false;
}
@Override
public boolean isUnwrappableAs(Class unwrapType) {
return false;
}
@Override
public <T> T unwrap(Class<T> unwrapType) {
return null;
}
}
, которые получают подключения от созданного мной диспетчера пула подключений :
@Component
@Profile({"dev", "stage", "prod"})
@Log4j2
public class ConnectionPoolManagerImpl implements ConnectionPoolManager {
@Autowired
private DataSourceCache dataSourceCache;
@Autowired
private TenantDbCache tenantDbCache;
@Autowired
private DatabaseInstanceJdbcRepository databaseInstanceJdbcRepository;
@Autowired
private EncryptUtil encryptUtil;
@Autowired
private Environment env;
@Autowired
private DataSource primaryDataSource;
@PostConstruct
public void init() {
try {
log.info("Caching datasource connections...");
Set<String> databaseIds = databaseInstanceJdbcRepository.findInstanceIds();
databaseIds.forEach(instanceId -> dataSourceCache.addDataSource(instanceId, createDataSource(instanceId)));
log.info("Cached {} datasources", databaseIds.size());
} catch (Exception e) {
log.error("Error trying to cache datasources.", e);
}
}
@Override
public Connection getConnection(@NotNull String tenantId) {
try {
// Default connection (in memory)
if (tenantId.equalsIgnoreCase(TenantContext.TENANT_DEFAULT)) {
return primaryDataSource.getConnection();
}
DataSource dataSource = getDataSource(tenantId);
if (dataSource != null) {
Connection connection = dataSource.getConnection();
connection.setCatalog("test_" + tenantId);
return connection;
}
} catch (SQLException e) {
throw new DbException(ExceptionCode.DB_ERROR, e, ExceptionUtils.getRootCauseMessage(e));
}
throw new DbException(ExceptionCode.DB_ERROR, String.format("Connection not found for %s", tenantId));
}
@Override
public DataSource getDataSource(@NotNull String tenantId) {
log.trace("Required connection for tenantid {}", tenantId);
// Default in memory connection for default tenant
if (tenantId.equalsIgnoreCase(TenantContext.TENANT_DEFAULT)) {
return primaryDataSource;
}
String instanceId = tenantDbCache.getDbInstance(tenantId);
Optional<DataSource> optionalDataSource = dataSourceCache.getDatasource(instanceId);
if (!optionalDataSource.isPresent()) {
DataSource dataSource = createDataSource(instanceId);
//add the datasource to the cache
dataSourceCache.addDataSource(tenantId, dataSource);
return dataSource;
} else {
return optionalDataSource.get();
}
}
private DataSource createDataSource(@NotNull String dbInstanceIdentifier) {
Optional<DatabaseInstance> optionalDatabaseInstance = databaseInstanceJdbcRepository.findByIdentifier(dbInstanceIdentifier);
if (optionalDatabaseInstance.isPresent()) {
DatabaseInstance databaseInstance = optionalDatabaseInstance.get();
HikariConfig hikari = new HikariConfig();
String driver = "jdbc:mysql://";
String options = "?useLegacyDatetimeCode=false&serverTimezone=UTC&useUnicode=yes&characterEncoding=UTF-8&useSSL=false&allowLoadLocalInfile=true";
hikari.setJdbcUrl(driver + databaseInstance.getHost() + ":" + databaseInstance.getPort() + options);
log.info("Creating pool for {}", databaseInstance.getHost());
hikari.setUsername(databaseInstance.getUsername());
hikari.setPassword(encryptUtil.decrypt(databaseInstance.getPassword()));
//OTHER HIKARI SETTINGS...
String connectionPoolName = "JPAHikari_" + databaseInstance.getIdentifier();
hikari.setPoolName(connectionPoolName);
DataSource dataSource = new HikariDataSource(hikari);
return dataSource;
} else {
throw new DbException(ExceptionCode.DB_ERROR, String.format("Db not found for %s", dbInstanceIdentifier));
}
}
и, в конце концов, у меня конфигурация Hibernate:
@Configuration
@Profile("test")
@EnableJpaRepositories(
basePackages = "com.test.server",
entityManagerFactoryRef = "entityManagerFactory",
transactionManagerRef = "transactionManager"
)
@Log4j2
public class HibernateConfig {
@Autowired
private Environment env;
@Bean
public LocalContainerEntityManagerFactoryBean entityManagerFactory(MultiTenantConnectionProvider multiTenantConnectionProviderImpl,
CurrentTenantIdentifierResolver currentTenantIdentifierResolverImpl) throws InstantiationException, IllegalAccessException, ClassNotFoundException,
InvocationTargetException, NoSuchMethodException {
Map<String, Object> properties = new HashMap<>();
properties.put(AvailableSettings.MULTI_TENANT, MultiTenancyStrategy.DATABASE);
properties.put(AvailableSettings.MULTI_TENANT_CONNECTION_PROVIDER, multiTenantConnectionProviderImpl);
properties.put(AvailableSettings.MULTI_TENANT_IDENTIFIER_RESOLVER, currentTenantIdentifierResolverImpl);
LocalContainerEntityManagerFactoryBean em = new LocalContainerEntityManagerFactoryBean();
em.setPersistenceUnitName("testPU");
em.setDataSource(dataSource());
em.setPackagesToScan("com.test.server.model");
em.setJpaPropertyMap(properties);
HibernateJpaVendorAdapter vendorAdapter = new HibernateJpaVendorAdapter();
em.setJpaVendorAdapter(vendorAdapter);
properties = new HashMap<>();
properties.put("hibernate.hbm2ddl.auto", env.getProperty("hibernate.hbm2ddl.auto"));
properties.put("hibernate.generate_statistics", env.getProperty("hibernate.generate_statistics"));
properties.put("hibernate.dialect", env.getProperty("hibernate.dialect"));
properties.put("hibernate.show_sql", env.getProperty("hibernate.show_sql"));
properties.put("hibernate.implicit_naming_strategy", env.getProperty("hibernate.implicit_naming_strategy"));
properties.put("hibernate.jdbc.fetch_size", env.getProperty("hibernate.jdbc.fetch_size"));
properties.put("hibernate.jdbc.batch_size", env.getProperty("hibernate.jdbc.batch_size"));
properties.put("hibernate.max_fetch_depth", env.getProperty("hibernate.max_fetch_depth"));
properties.put("hibernate.cache.use_second_level_cache", env.getProperty("hibernate.cache.use_second_level_cache"));
properties.put("hibernate.jdbc.time_zone", env.getProperty("hibernate.jdbc.time_zone"));
properties.put("hibernate.globally_quoted_identifiers", env.getProperty("hibernate.globally_quoted_identifiers"));
properties.put("hibernate.globally_quoted_identifiers_skip_column_definitions",
env.getProperty("hibernate.globally_quoted_identifiers_skip_column_definitions"));
properties.put("hibernate.hbm2ddl.delimiter", ";");
properties.put("hibernate.ejb.interceptor", hibernateInterceptor());
em.setJpaPropertyMap(properties);
em.afterPropertiesSet();
return em;
}
@Bean
public HibernateInterceptor hibernateInterceptor() {
return new HibernateInterceptor();
}
@Primary
@Bean
public DataSource dataSource() throws InstantiationException, IllegalAccessException, ClassNotFoundException, IllegalArgumentException,
InvocationTargetException, NoSuchMethodException, SecurityException {
final SimpleDriverDataSource dataSource = new SimpleDriverDataSource();
dataSource.setDriver(((Driver) Class.forName(env.getProperty("primary.datasource.driver-class-name")).getDeclaredConstructor().newInstance()));
dataSource.setUrl(env.getProperty("primary.datasource.url"));
dataSource.setUsername(env.getProperty("primary.datasource.username"));
dataSource.setPassword(env.getProperty("primary.datasource.password"));
return dataSource;
}
@Primary
@Bean
public PlatformTransactionManager transactionManager(MultiTenantConnectionProvider multiTenantConnectionProviderImpl,
CurrentTenantIdentifierResolver currentTenantIdentifierResolverImpl) throws InstantiationException, IllegalAccessException, ClassNotFoundException,
InvocationTargetException, NoSuchMethodException {
JpaTransactionManager transactionManager = new JpaTransactionManager();
transactionManager.setEntityManagerFactory(entityManagerFactory(multiTenantConnectionProviderImpl, currentTenantIdentifierResolverImpl).getObject());
return transactionManager;
}
@PreDestroy
public void onDestroy() {
log.debug("On destroy");
}
}
Он работает, и мое приложение prod не имеет проблем, к сожалению, каждый раз, когда я запускаю все свои тесты вместе, кажется, создается новый экземпляр HibernateConfig
и ConnectionPoolManagerImpl
, поэтому новые соединения открываются с Mysql, пока я не получу сообщение «слишком много соединений». Читая другие сообщения, я вижу, что контекст не кэшируется между разными файлами тестов, потому что я использую @MockBean
. Я вижу смысл, но:
- Я предполагаю, есть ли способ сохранить HibernateConfig и связанные bean-компоненты между тестами, избегая их воссоздания
- Есть способ уничтожить свойство пула HikariCp если мои beans не кэшированы, чтобы не исчерпать ресурсы