-
Notifications
You must be signed in to change notification settings - Fork 40.2k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Closes gh-28377
- Loading branch information
Showing
5 changed files
with
361 additions
and
6 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
150 changes: 150 additions & 0 deletions
150
.../java/org/springframework/boot/devtools/autoconfigure/DevToolsR2dbcAutoConfiguration.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,150 @@ | ||
/* | ||
* Copyright 2012-2021 the original author or authors. | ||
* | ||
* Licensed under the Apache License, Version 2.0 (the "License"); | ||
* you may not use this file except in compliance with the License. | ||
* You may obtain a copy of the License at | ||
* | ||
* https://www.apache.org/licenses/LICENSE-2.0 | ||
* | ||
* Unless required by applicable law or agreed to in writing, software | ||
* distributed under the License is distributed on an "AS IS" BASIS, | ||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
* See the License for the specific language governing permissions and | ||
* limitations under the License. | ||
*/ | ||
|
||
package org.springframework.boot.devtools.autoconfigure; | ||
|
||
import io.r2dbc.spi.Connection; | ||
import io.r2dbc.spi.ConnectionFactory; | ||
import org.reactivestreams.Publisher; | ||
import reactor.core.publisher.Mono; | ||
|
||
import org.springframework.beans.factory.DisposableBean; | ||
import org.springframework.beans.factory.annotation.AnnotatedBeanDefinition; | ||
import org.springframework.beans.factory.config.BeanDefinition; | ||
import org.springframework.boot.autoconfigure.AutoConfigureAfter; | ||
import org.springframework.boot.autoconfigure.EnableAutoConfiguration; | ||
import org.springframework.boot.autoconfigure.condition.ConditionMessage; | ||
import org.springframework.boot.autoconfigure.condition.ConditionOutcome; | ||
import org.springframework.boot.autoconfigure.condition.SpringBootCondition; | ||
import org.springframework.boot.autoconfigure.r2dbc.R2dbcAutoConfiguration; | ||
import org.springframework.boot.devtools.autoconfigure.DevToolsR2dbcAutoConfiguration.DevToolsConnectionFactoryCondition; | ||
import org.springframework.boot.r2dbc.EmbeddedDatabaseConnection; | ||
import org.springframework.context.ApplicationEventPublisher; | ||
import org.springframework.context.annotation.Bean; | ||
import org.springframework.context.annotation.ConditionContext; | ||
import org.springframework.context.annotation.Conditional; | ||
import org.springframework.context.annotation.Configuration; | ||
import org.springframework.context.annotation.ConfigurationCondition; | ||
import org.springframework.core.type.AnnotatedTypeMetadata; | ||
import org.springframework.core.type.MethodMetadata; | ||
|
||
/** | ||
* {@link EnableAutoConfiguration Auto-configuration} for DevTools-specific R2DBC | ||
* configuration. | ||
* | ||
* @author Phillip Webb | ||
* @since 2.5.6 | ||
*/ | ||
@AutoConfigureAfter(R2dbcAutoConfiguration.class) | ||
@Conditional({ OnEnabledDevToolsCondition.class, DevToolsConnectionFactoryCondition.class }) | ||
@Configuration(proxyBeanMethods = false) | ||
public class DevToolsR2dbcAutoConfiguration { | ||
|
||
@Bean | ||
InMemoryR2dbcDatabaseShutdownExecutor inMemoryR2dbcDatabaseShutdownExecutor( | ||
ApplicationEventPublisher eventPublisher, ConnectionFactory connectionFactory) { | ||
return new InMemoryR2dbcDatabaseShutdownExecutor(eventPublisher, connectionFactory); | ||
} | ||
|
||
final class InMemoryR2dbcDatabaseShutdownExecutor implements DisposableBean { | ||
|
||
private final ApplicationEventPublisher eventPublisher; | ||
|
||
private final ConnectionFactory connectionFactory; | ||
|
||
InMemoryR2dbcDatabaseShutdownExecutor(ApplicationEventPublisher eventPublisher, | ||
ConnectionFactory connectionFactory) { | ||
this.eventPublisher = eventPublisher; | ||
this.connectionFactory = connectionFactory; | ||
} | ||
|
||
@Override | ||
public void destroy() throws Exception { | ||
if (shouldShutdown()) { | ||
Mono.usingWhen(this.connectionFactory.create(), this::executeShutdown, this::closeConnection, | ||
this::closeConnection, this::closeConnection).block(); | ||
this.eventPublisher.publishEvent(new R2dbcDatabaseShutdownEvent(this.connectionFactory)); | ||
} | ||
} | ||
|
||
private boolean shouldShutdown() { | ||
try { | ||
return EmbeddedDatabaseConnection.isEmbedded(this.connectionFactory); | ||
} | ||
catch (Exception ex) { | ||
return false; | ||
} | ||
} | ||
|
||
private Mono<?> executeShutdown(Connection connection) { | ||
return Mono.from(connection.createStatement("SHUTDOWN").execute()); | ||
} | ||
|
||
private Publisher<Void> closeConnection(Connection connection) { | ||
return closeConnection(connection, null); | ||
} | ||
|
||
private Publisher<Void> closeConnection(Connection connection, Throwable ex) { | ||
return connection.close(); | ||
} | ||
|
||
} | ||
|
||
static class DevToolsConnectionFactoryCondition extends SpringBootCondition implements ConfigurationCondition { | ||
|
||
@Override | ||
public ConfigurationPhase getConfigurationPhase() { | ||
return ConfigurationPhase.REGISTER_BEAN; | ||
} | ||
|
||
@Override | ||
public ConditionOutcome getMatchOutcome(ConditionContext context, AnnotatedTypeMetadata metadata) { | ||
ConditionMessage.Builder message = ConditionMessage.forCondition("DevTools ConnectionFactory Condition"); | ||
String[] beanNames = context.getBeanFactory().getBeanNamesForType(ConnectionFactory.class, true, false); | ||
if (beanNames.length != 1) { | ||
return ConditionOutcome.noMatch(message.didNotFind("a single ConnectionFactory bean").atAll()); | ||
} | ||
BeanDefinition beanDefinition = context.getRegistry().getBeanDefinition(beanNames[0]); | ||
if (beanDefinition instanceof AnnotatedBeanDefinition | ||
&& isAutoConfigured((AnnotatedBeanDefinition) beanDefinition)) { | ||
return ConditionOutcome.match(message.foundExactly("auto-configured ConnectionFactory")); | ||
} | ||
return ConditionOutcome.noMatch(message.didNotFind("an auto-configured ConnectionFactory").atAll()); | ||
} | ||
|
||
private boolean isAutoConfigured(AnnotatedBeanDefinition beanDefinition) { | ||
MethodMetadata methodMetadata = beanDefinition.getFactoryMethodMetadata(); | ||
return methodMetadata != null && methodMetadata.getDeclaringClassName() | ||
.startsWith(R2dbcAutoConfiguration.class.getPackage().getName()); | ||
} | ||
|
||
} | ||
|
||
static class R2dbcDatabaseShutdownEvent { | ||
|
||
private final ConnectionFactory connectionFactory; | ||
|
||
R2dbcDatabaseShutdownEvent(ConnectionFactory connectionFactory) { | ||
this.connectionFactory = connectionFactory; | ||
} | ||
|
||
ConnectionFactory getConnectionFactory() { | ||
return this.connectionFactory; | ||
} | ||
|
||
} | ||
|
||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
203 changes: 203 additions & 0 deletions
203
.../org/springframework/boot/devtools/autoconfigure/DevToolsR2dbcAutoConfigurationTests.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,203 @@ | ||
/* | ||
* Copyright 2012-2021 the original author or authors. | ||
* | ||
* Licensed under the Apache License, Version 2.0 (the "License"); | ||
* you may not use this file except in compliance with the License. | ||
* You may obtain a copy of the License at | ||
* | ||
* https://www.apache.org/licenses/LICENSE-2.0 | ||
* | ||
* Unless required by applicable law or agreed to in writing, software | ||
* distributed under the License is distributed on an "AS IS" BASIS, | ||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
* See the License for the specific language governing permissions and | ||
* limitations under the License. | ||
*/ | ||
|
||
package org.springframework.boot.devtools.autoconfigure; | ||
|
||
import java.util.ArrayList; | ||
import java.util.Collection; | ||
import java.util.Collections; | ||
import java.util.List; | ||
import java.util.concurrent.atomic.AtomicReference; | ||
import java.util.function.Supplier; | ||
|
||
import io.r2dbc.spi.Connection; | ||
import io.r2dbc.spi.ConnectionFactory; | ||
import io.r2dbc.spi.ConnectionFactoryMetadata; | ||
import org.junit.jupiter.api.BeforeEach; | ||
import org.junit.jupiter.api.Nested; | ||
import org.junit.jupiter.api.Test; | ||
import org.reactivestreams.Publisher; | ||
|
||
import org.springframework.beans.factory.annotation.AnnotatedGenericBeanDefinition; | ||
import org.springframework.boot.autoconfigure.r2dbc.R2dbcAutoConfiguration; | ||
import org.springframework.boot.devtools.autoconfigure.DevToolsR2dbcAutoConfiguration.R2dbcDatabaseShutdownEvent; | ||
import org.springframework.boot.test.util.TestPropertyValues; | ||
import org.springframework.boot.testsupport.classpath.ClassPathExclusions; | ||
import org.springframework.context.ApplicationListener; | ||
import org.springframework.context.ConfigurableApplicationContext; | ||
import org.springframework.context.annotation.AnnotationConfigApplicationContext; | ||
import org.springframework.context.annotation.Bean; | ||
import org.springframework.context.annotation.Configuration; | ||
import org.springframework.util.ObjectUtils; | ||
|
||
import static org.assertj.core.api.Assertions.assertThat; | ||
|
||
/** | ||
* Tests for {@link DevToolsR2dbcAutoConfiguration}. | ||
* | ||
* @author Phillip Webb | ||
*/ | ||
class DevToolsR2dbcAutoConfigurationTests { | ||
|
||
static List<ConnectionFactory> shutdowns = Collections.synchronizedList(new ArrayList<>()); | ||
|
||
abstract static class Common { | ||
|
||
@BeforeEach | ||
void reset() { | ||
shutdowns.clear(); | ||
} | ||
|
||
@Test | ||
void autoConfiguredInMemoryConnectionFactoryIsShutdown() throws Exception { | ||
ConfigurableApplicationContext context = getContext(() -> createContext()); | ||
ConnectionFactory connectionFactory = context.getBean(ConnectionFactory.class); | ||
context.close(); | ||
assertThat(shutdowns).contains(connectionFactory); | ||
} | ||
|
||
@Test | ||
void nonEmbeddedConnectionFactoryIsNotShutdown() throws Exception { | ||
ConfigurableApplicationContext context = getContext(() -> createContext("r2dbc:h2:file:///testdb")); | ||
ConnectionFactory connectionFactory = context.getBean(ConnectionFactory.class); | ||
context.close(); | ||
assertThat(shutdowns).doesNotContain(connectionFactory); | ||
} | ||
|
||
@Test | ||
void singleManuallyConfiguredConnectionFactoryIsNotClosed() throws Exception { | ||
ConfigurableApplicationContext context = getContext( | ||
() -> createContext(SingleConnectionFactoryConfiguration.class)); | ||
ConnectionFactory connectionFactory = context.getBean(ConnectionFactory.class); | ||
context.close(); | ||
assertThat(shutdowns).doesNotContain(connectionFactory); | ||
} | ||
|
||
@Test | ||
void multipleConnectionFactoriesAreIgnored() throws Exception { | ||
ConfigurableApplicationContext context = getContext( | ||
() -> createContext(MultipleConnectionFactoriesConfiguration.class)); | ||
Collection<ConnectionFactory> connectionFactory = context.getBeansOfType(ConnectionFactory.class).values(); | ||
context.close(); | ||
assertThat(shutdowns).doesNotContainAnyElementsOf(connectionFactory); | ||
} | ||
|
||
@Test | ||
void emptyFactoryMethodMetadataIgnored() throws Exception { | ||
ConfigurableApplicationContext context = getContext(this::getEmptyFactoryMethodMetadataIgnoredContext); | ||
ConnectionFactory connectionFactory = context.getBean(ConnectionFactory.class); | ||
context.close(); | ||
assertThat(shutdowns).doesNotContain(connectionFactory); | ||
} | ||
|
||
private ConfigurableApplicationContext getEmptyFactoryMethodMetadataIgnoredContext() { | ||
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(); | ||
ConnectionFactory connectionFactory = new MockConnectionFactory(); | ||
AnnotatedGenericBeanDefinition beanDefinition = new AnnotatedGenericBeanDefinition( | ||
connectionFactory.getClass()); | ||
context.registerBeanDefinition("connectionFactory", beanDefinition); | ||
context.register(R2dbcAutoConfiguration.class, DevToolsR2dbcAutoConfiguration.class); | ||
context.refresh(); | ||
return context; | ||
} | ||
|
||
protected ConfigurableApplicationContext getContext(Supplier<ConfigurableApplicationContext> supplier) | ||
throws Exception { | ||
AtomicReference<ConfigurableApplicationContext> atomicReference = new AtomicReference<>(); | ||
Thread thread = new Thread(() -> { | ||
ConfigurableApplicationContext context = supplier.get(); | ||
atomicReference.getAndSet(context); | ||
}); | ||
thread.start(); | ||
thread.join(); | ||
return atomicReference.get(); | ||
} | ||
|
||
protected final ConfigurableApplicationContext createContext(Class<?>... classes) { | ||
return createContext(null, classes); | ||
} | ||
|
||
protected final ConfigurableApplicationContext createContext(String url, Class<?>... classes) { | ||
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(); | ||
if (!ObjectUtils.isEmpty(classes)) { | ||
context.register(classes); | ||
} | ||
context.register(R2dbcAutoConfiguration.class, DevToolsR2dbcAutoConfiguration.class); | ||
if (url != null) { | ||
TestPropertyValues.of("spring.r2dbc.url:" + url).applyTo(context); | ||
} | ||
context.addApplicationListener(ApplicationListener.forPayload(this::onEvent)); | ||
context.refresh(); | ||
return context; | ||
} | ||
|
||
private void onEvent(R2dbcDatabaseShutdownEvent event) { | ||
shutdowns.add(event.getConnectionFactory()); | ||
} | ||
|
||
} | ||
|
||
@Nested | ||
@ClassPathExclusions("r2dbc-pool*.jar") | ||
static class Embedded extends Common { | ||
|
||
} | ||
|
||
@Nested | ||
static class Pooled extends Common { | ||
|
||
} | ||
|
||
@Configuration(proxyBeanMethods = false) | ||
static class SingleConnectionFactoryConfiguration { | ||
|
||
@Bean | ||
ConnectionFactory connectionFactory() { | ||
return new MockConnectionFactory(); | ||
} | ||
|
||
} | ||
|
||
@Configuration(proxyBeanMethods = false) | ||
static class MultipleConnectionFactoriesConfiguration { | ||
|
||
@Bean | ||
ConnectionFactory connectionFactoryOne() { | ||
return new MockConnectionFactory(); | ||
} | ||
|
||
@Bean | ||
ConnectionFactory connectionFactoryTwo() { | ||
return new MockConnectionFactory(); | ||
} | ||
|
||
} | ||
|
||
private static class MockConnectionFactory implements ConnectionFactory { | ||
|
||
@Override | ||
public Publisher<? extends Connection> create() { | ||
return null; | ||
} | ||
|
||
@Override | ||
public ConnectionFactoryMetadata getMetadata() { | ||
return null; | ||
} | ||
|
||
} | ||
|
||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters