Skip to content

Commit aece234

Browse files
committed
#331 Refactor Spring SQL init support to use AOP proxying
Rename DataSourceScriptDatabase* classes to SpringScriptDatabase*, apply AOP-based interception pattern consistent with Flyway/Liquibase extensions, and cache the preparer instance in the interceptor.
1 parent 767d5b3 commit aece234

5 files changed

Lines changed: 124 additions & 101 deletions

File tree

embedded-database-spring-test/src/main/java/io/zonky/test/db/config/EmbeddedDatabaseAutoConfiguration.java

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,9 @@
1818

1919
import io.zonky.test.db.flyway.FlywayDatabaseExtension;
2020
import io.zonky.test.db.flyway.FlywayPropertiesPostProcessor;
21-
import io.zonky.test.db.init.DataSourceScriptDatabaseExtension;
2221
import io.zonky.test.db.init.EmbeddedDatabaseInitializer;
2322
import io.zonky.test.db.init.ScriptDatabasePreparer;
23+
import io.zonky.test.db.init.SpringScriptDatabaseExtension;
2424
import io.zonky.test.db.liquibase.LiquibaseDatabaseExtension;
2525
import io.zonky.test.db.liquibase.LiquibasePropertiesPostProcessor;
2626
import io.zonky.test.db.provider.DatabaseProvider;
@@ -232,7 +232,7 @@ public DatabaseProviderFactory defaultDatabaseProviderFactory(AutowireCapableBea
232232
int concurrency = environment.getProperty("zonky.test.database.prefetching.concurrency", int.class, 3);
233233
int pipelineCacheSize = environment.getProperty("zonky.test.database.prefetching.pipeline-cache-size", int.class, 5);
234234
int maxPreparedTemplates = environment.getProperty("zonky.test.database.prefetching.max-prepared-templates", int.class, 10);
235-
int maxPreparedDatabases = (maxPreparedTemplates * 2/3 * 2) + pipelineCacheSize;
235+
int maxPreparedDatabases = (maxPreparedTemplates * 2 / 3 * 2) + pipelineCacheSize;
236236

237237
return new DatabaseProviderFactory(beanFactory)
238238
.customizeTemplating(builder -> builder
@@ -293,9 +293,9 @@ public BeanPostProcessor liquibasePropertiesPostProcessor() {
293293
@Bean
294294
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
295295
@ConditionalOnClass(name = "org.springframework.boot.jdbc.init.DataSourceScriptDatabaseInitializer")
296-
@ConditionalOnMissingBean(name = "dataSourceScriptDatabaseExtension")
297-
public DataSourceScriptDatabaseExtension dataSourceScriptDatabaseExtension(Environment environment) {
298-
return new DataSourceScriptDatabaseExtension(environment);
296+
@ConditionalOnMissingBean(name = "springScriptDatabaseExtension")
297+
public SpringScriptDatabaseExtension springScriptDatabaseExtension(Environment environment) {
298+
return new SpringScriptDatabaseExtension(environment);
299299
}
300300

301301
@Bean

embedded-database-spring-test/src/main/java/io/zonky/test/db/init/DataSourceScriptDatabaseExtension.java

Lines changed: 0 additions & 76 deletions
This file was deleted.
Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
/*
2+
* Copyright 2025 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package io.zonky.test.db.init;
18+
19+
import io.zonky.test.db.context.DatabaseContext;
20+
import io.zonky.test.db.util.AopProxyUtils;
21+
import io.zonky.test.db.util.ReflectionUtils;
22+
import org.aopalliance.aop.Advice;
23+
import org.aopalliance.intercept.MethodInterceptor;
24+
import org.aopalliance.intercept.MethodInvocation;
25+
import org.springframework.aop.Advisor;
26+
import org.springframework.aop.framework.Advised;
27+
import org.springframework.aop.framework.AopInfrastructureBean;
28+
import org.springframework.aop.framework.ProxyFactory;
29+
import org.springframework.aop.support.NameMatchMethodPointcutAdvisor;
30+
import org.springframework.beans.factory.config.BeanPostProcessor;
31+
import org.springframework.boot.jdbc.init.DataSourceScriptDatabaseInitializer;
32+
import org.springframework.core.Ordered;
33+
import org.springframework.core.env.Environment;
34+
35+
import javax.sql.DataSource;
36+
37+
public class SpringScriptDatabaseExtension implements BeanPostProcessor, Ordered {
38+
39+
private final boolean enabled;
40+
41+
public SpringScriptDatabaseExtension(Environment environment) {
42+
this.enabled = environment.getProperty("zonky.test.database.spring.optimized-sql-init.enabled", boolean.class, true);
43+
}
44+
45+
@Override
46+
public int getOrder() {
47+
return Ordered.HIGHEST_PRECEDENCE + 1;
48+
}
49+
50+
@Override
51+
public Object postProcessBeforeInitialization(Object bean, String beanName) {
52+
if (!enabled || bean instanceof AopInfrastructureBean) {
53+
return bean;
54+
}
55+
56+
if (bean instanceof DataSourceScriptDatabaseInitializer) {
57+
DataSourceScriptDatabaseInitializer initializer = (DataSourceScriptDatabaseInitializer) bean;
58+
DataSource dataSource = ReflectionUtils.getField(initializer, "dataSource");
59+
DatabaseContext context = AopProxyUtils.getDatabaseContext(dataSource);
60+
61+
if (context != null) {
62+
if (bean instanceof Advised && !((Advised) bean).isFrozen()) {
63+
((Advised) bean).addAdvisor(0, createAdvisor(initializer, context));
64+
return bean;
65+
} else {
66+
ProxyFactory proxyFactory = new ProxyFactory(bean);
67+
proxyFactory.addAdvisor(createAdvisor(initializer, context));
68+
proxyFactory.setProxyTargetClass(true);
69+
return proxyFactory.getProxy();
70+
}
71+
}
72+
}
73+
74+
return bean;
75+
}
76+
77+
@Override
78+
public Object postProcessAfterInitialization(Object bean, String beanName) {
79+
return bean;
80+
}
81+
82+
protected Advisor createAdvisor(DataSourceScriptDatabaseInitializer initializer, DatabaseContext context) {
83+
Advice advice = new SpringScriptDatabaseExtensionInterceptor(initializer, context);
84+
NameMatchMethodPointcutAdvisor advisor = new NameMatchMethodPointcutAdvisor(advice);
85+
advisor.setMappedNames("afterPropertiesSet", "initializeDatabase");
86+
return advisor;
87+
}
88+
89+
protected static class SpringScriptDatabaseExtensionInterceptor implements MethodInterceptor {
90+
91+
private final SpringScriptDatabasePreparer preparer;
92+
private final DatabaseContext context;
93+
94+
protected SpringScriptDatabaseExtensionInterceptor(DataSourceScriptDatabaseInitializer initializer,
95+
DatabaseContext context) {
96+
this.preparer = new SpringScriptDatabasePreparer(initializer);
97+
this.context = context;
98+
}
99+
100+
@Override
101+
public Object invoke(MethodInvocation invocation) throws Throwable {
102+
switch (invocation.getMethod().getName()) {
103+
case "afterPropertiesSet":
104+
context.apply(preparer);
105+
return null;
106+
case "initializeDatabase":
107+
context.apply(preparer);
108+
return true;
109+
default:
110+
return invocation.proceed();
111+
}
112+
}
113+
}
114+
}

embedded-database-spring-test/src/main/java/io/zonky/test/db/init/DataSourceScriptDatabasePreparer.java renamed to embedded-database-spring-test/src/main/java/io/zonky/test/db/init/SpringScriptDatabasePreparer.java

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,6 @@
2020
import io.zonky.test.db.preparer.DatabasePreparer;
2121
import io.zonky.test.db.util.ReflectionUtils;
2222
import org.springframework.boot.jdbc.init.DataSourceScriptDatabaseInitializer;
23-
2423
import org.springframework.util.ReflectionUtils.FieldFilter;
2524

2625
import javax.sql.DataSource;
@@ -33,19 +32,19 @@
3332

3433
import static org.springframework.util.ReflectionUtils.makeAccessible;
3534

36-
public class DataSourceScriptDatabasePreparer implements DatabasePreparer {
35+
public class SpringScriptDatabasePreparer implements DatabasePreparer {
3736

3837
private static final Set<String> EXCLUDED_FIELDS = new HashSet<>(Arrays.asList("dataSource", "resourceLoader"));
3938

4039
private static final FieldFilter FIELD_FILTER =
4140
field -> !Modifier.isStatic(field.getModifiers()) && !EXCLUDED_FIELDS.contains(field.getName());
4241

42+
private static final ThreadLocalDataSource threadLocalDataSource = new ThreadLocalDataSource();
43+
4344
private final DataSourceScriptDatabaseInitializer initializer;
44-
private final ThreadLocalDataSource threadLocalDataSource;
4545

46-
public DataSourceScriptDatabasePreparer(DataSourceScriptDatabaseInitializer initializer) {
46+
public SpringScriptDatabasePreparer(DataSourceScriptDatabaseInitializer initializer) {
4747
this.initializer = initializer;
48-
this.threadLocalDataSource = new ThreadLocalDataSource();
4948
ReflectionUtils.setField(initializer, "dataSource", threadLocalDataSource);
5049
}
5150

@@ -68,7 +67,7 @@ public void prepare(DataSource dataSource) {
6867
public boolean equals(Object o) {
6968
if (this == o) return true;
7069
if (o == null || getClass() != o.getClass()) return false;
71-
DataSourceScriptDatabasePreparer that = (DataSourceScriptDatabasePreparer) o;
70+
SpringScriptDatabasePreparer that = (SpringScriptDatabasePreparer) o;
7271
if (initializer.getClass() != that.initializer.getClass()) return false;
7372
AtomicBoolean equal = new AtomicBoolean(true);
7473
org.springframework.util.ReflectionUtils.doWithFields(initializer.getClass(),

spring-boot-4-stubs/src/main/java/org/springframework/boot/sql/init/DatabaseInitializationSettings.java

Lines changed: 0 additions & 14 deletions
This file was deleted.

0 commit comments

Comments
 (0)