Skip to content

Commit fd9b8fe

Browse files
committed
Merge branch 'gh-34658'
Closes gh-34658
2 parents 8e4b8a8 + 95f45ea commit fd9b8fe

File tree

84 files changed

+3074
-264
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

84 files changed

+3074
-264
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
/*
2+
* Copyright 2012-2023 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+
* https://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 org.springframework.boot.autoconfigure.service.connection;
18+
19+
import java.util.ArrayList;
20+
import java.util.List;
21+
import java.util.Objects;
22+
23+
import org.springframework.core.ResolvableType;
24+
import org.springframework.core.annotation.AnnotationAwareOrderComparator;
25+
import org.springframework.core.io.support.SpringFactoriesLoader;
26+
import org.springframework.core.style.ToStringCreator;
27+
28+
/**
29+
* A registry of {@link ConnectionDetailsFactory} instances.
30+
*
31+
* @author Moritz Halbritter
32+
* @author Andy Wilkinson
33+
* @author Phillip Webb
34+
* @since 3.1.0
35+
*/
36+
public class ConnectionDetailsFactories {
37+
38+
private List<FactoryDetails> registeredFactories = new ArrayList<>();
39+
40+
public ConnectionDetailsFactories() {
41+
this(SpringFactoriesLoader.forDefaultResourceLocation(ConnectionDetailsFactory.class.getClassLoader()));
42+
}
43+
44+
@SuppressWarnings("rawtypes")
45+
ConnectionDetailsFactories(SpringFactoriesLoader loader) {
46+
List<ConnectionDetailsFactory> factories = loader.load(ConnectionDetailsFactory.class);
47+
factories.stream().map(this::factoryDetails).filter(Objects::nonNull).forEach(this::register);
48+
}
49+
50+
@SuppressWarnings("unchecked")
51+
private FactoryDetails factoryDetails(ConnectionDetailsFactory<?, ?> factory) {
52+
ResolvableType connectionDetailsFactory = findConnectionDetailsFactory(
53+
ResolvableType.forClass(factory.getClass()));
54+
if (connectionDetailsFactory != null) {
55+
ResolvableType input = connectionDetailsFactory.getGeneric(0);
56+
ResolvableType output = connectionDetailsFactory.getGeneric(1);
57+
return new FactoryDetails(input.getRawClass(), (Class<? extends ConnectionDetails>) output.getRawClass(),
58+
factory);
59+
}
60+
return null;
61+
}
62+
63+
private ResolvableType findConnectionDetailsFactory(ResolvableType type) {
64+
try {
65+
ResolvableType[] interfaces = type.getInterfaces();
66+
for (ResolvableType iface : interfaces) {
67+
if (iface.getRawClass().equals(ConnectionDetailsFactory.class)) {
68+
return iface;
69+
}
70+
}
71+
}
72+
catch (TypeNotPresentException ex) {
73+
// A type referenced by the factory is not present. Skip it.
74+
}
75+
ResolvableType superType = type.getSuperType();
76+
return ResolvableType.NONE.equals(superType) ? null : findConnectionDetailsFactory(superType);
77+
}
78+
79+
private void register(FactoryDetails details) {
80+
this.registeredFactories.add(details);
81+
}
82+
83+
@SuppressWarnings("unchecked")
84+
public <S> ConnectionDetailsFactory<S, ConnectionDetails> getConnectionDetailsFactory(S source) {
85+
Class<S> input = (Class<S>) source.getClass();
86+
List<ConnectionDetailsFactory<S, ConnectionDetails>> matchingFactories = new ArrayList<>();
87+
for (FactoryDetails factoryDetails : this.registeredFactories) {
88+
if (factoryDetails.input.isAssignableFrom(input)) {
89+
matchingFactories.add((ConnectionDetailsFactory<S, ConnectionDetails>) factoryDetails.factory);
90+
}
91+
}
92+
if (matchingFactories.isEmpty()) {
93+
throw new ConnectionDetailsFactoryNotFoundException(source);
94+
}
95+
else {
96+
if (matchingFactories.size() == 1) {
97+
return matchingFactories.get(0);
98+
}
99+
AnnotationAwareOrderComparator.sort(matchingFactories);
100+
return new CompositeConnectionDetailsFactory<>(matchingFactories);
101+
}
102+
}
103+
104+
private record FactoryDetails(Class<?> input, Class<? extends ConnectionDetails> output,
105+
ConnectionDetailsFactory<?, ?> factory) {
106+
}
107+
108+
static class CompositeConnectionDetailsFactory<S> implements ConnectionDetailsFactory<S, ConnectionDetails> {
109+
110+
private final List<ConnectionDetailsFactory<S, ConnectionDetails>> delegates;
111+
112+
CompositeConnectionDetailsFactory(List<ConnectionDetailsFactory<S, ConnectionDetails>> delegates) {
113+
this.delegates = delegates;
114+
}
115+
116+
@Override
117+
@SuppressWarnings("unchecked")
118+
public ConnectionDetails getConnectionDetails(Object source) {
119+
for (ConnectionDetailsFactory<S, ConnectionDetails> delegate : this.delegates) {
120+
ConnectionDetails connectionDetails = delegate.getConnectionDetails((S) source);
121+
if (connectionDetails != null) {
122+
return connectionDetails;
123+
}
124+
}
125+
return null;
126+
}
127+
128+
@Override
129+
public String toString() {
130+
return new ToStringCreator(this).append("delegates", this.delegates).toString();
131+
}
132+
133+
List<ConnectionDetailsFactory<S, ConnectionDetails>> getDelegates() {
134+
return this.delegates;
135+
}
136+
137+
}
138+
139+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
/*
2+
* Copyright 2012-2023 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+
* https://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 org.springframework.boot.autoconfigure.service.connection;
18+
19+
/**
20+
* A factory to create {@link ConnectionDetails} from a given {@code source}.
21+
* Implementations should be registered in {@code META-INF/spring.factories}.
22+
*
23+
* @param <S> the source type accepted by the factory. Implementations are expected to
24+
* provide a valid {@code toString}.
25+
* @param <D> the type of {@link ConnectionDetails} produced by the factory
26+
* @author Moritz Halbritter
27+
* @author Andy Wilkinson
28+
* @author Phillip Webb
29+
* @since 3.1.0
30+
*/
31+
public interface ConnectionDetailsFactory<S, D extends ConnectionDetails> {
32+
33+
/**
34+
* Get the {@link ConnectionDetails} from the given {@code source}. May return
35+
* {@code null} if no details can be created.
36+
* @param source the source
37+
* @return the connection details or {@code null}
38+
*/
39+
D getConnectionDetails(S source);
40+
41+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
/*
2+
* Copyright 2012-2023 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+
* https://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 org.springframework.boot.autoconfigure.service.connection;
18+
19+
/**
20+
* {@link RuntimeException} thrown when a {@link ConnectionDetailsFactory} could not be
21+
* found.
22+
*
23+
* @author Moritz Halbritter
24+
* @author Andy Wilkinson
25+
* @author Phillip Webb
26+
* @since 3.1.0
27+
*/
28+
public class ConnectionDetailsFactoryNotFoundException extends RuntimeException {
29+
30+
public <S> ConnectionDetailsFactoryNotFoundException(S source) {
31+
super("No ConnectionDetailsFactory found for source '" + source + "'");
32+
}
33+
34+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
/*
2+
* Copyright 2012-2023 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+
* https://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 org.springframework.boot.autoconfigure.service.connection;
18+
19+
import org.assertj.core.api.InstanceOfAssertFactories;
20+
import org.junit.jupiter.api.Test;
21+
22+
import org.springframework.boot.autoconfigure.service.connection.ConnectionDetailsFactories.CompositeConnectionDetailsFactory;
23+
import org.springframework.core.Ordered;
24+
import org.springframework.core.test.io.support.MockSpringFactoriesLoader;
25+
26+
import static org.assertj.core.api.Assertions.assertThat;
27+
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
28+
29+
/**
30+
* Tests for {@link ConnectionDetailsFactories}.
31+
*
32+
* @author Moritz Halbritter
33+
* @author Andy Wilkinson
34+
* @author Phillip Webb
35+
*/
36+
class ConnectionDetailsFactoriesTests {
37+
38+
private final MockSpringFactoriesLoader loader = new MockSpringFactoriesLoader();
39+
40+
@Test
41+
void getConnectionDetailsFactoryShouldThrowWhenNoFactoryForSource() {
42+
ConnectionDetailsFactories factories = new ConnectionDetailsFactories(this.loader);
43+
assertThatExceptionOfType(ConnectionDetailsFactoryNotFoundException.class)
44+
.isThrownBy(() -> factories.getConnectionDetailsFactory("source"));
45+
}
46+
47+
@Test
48+
void getConnectionDetailsFactoryShouldReturnSingleFactoryWhenSourceHasOneMatch() {
49+
this.loader.addInstance(ConnectionDetailsFactory.class, new TestConnectionDetailsFactory());
50+
ConnectionDetailsFactories factories = new ConnectionDetailsFactories(this.loader);
51+
ConnectionDetailsFactory<String, ConnectionDetails> factory = factories.getConnectionDetailsFactory("source");
52+
assertThat(factory).isInstanceOf(TestConnectionDetailsFactory.class);
53+
}
54+
55+
@Test
56+
@SuppressWarnings("unchecked")
57+
void getConnectionDetailsFactoryShouldReturnCompositeFactoryWhenSourceHasMultipleMatches() {
58+
this.loader.addInstance(ConnectionDetailsFactory.class, new TestConnectionDetailsFactory(),
59+
new TestConnectionDetailsFactory());
60+
ConnectionDetailsFactories factories = new ConnectionDetailsFactories(this.loader);
61+
ConnectionDetailsFactory<String, ConnectionDetails> factory = factories.getConnectionDetailsFactory("source");
62+
assertThat(factory).asInstanceOf(InstanceOfAssertFactories.type(CompositeConnectionDetailsFactory.class))
63+
.satisfies((composite) -> assertThat(composite.getDelegates()).hasSize(2));
64+
}
65+
66+
@Test
67+
@SuppressWarnings("unchecked")
68+
void compositeFactoryShouldHaveOrderedDelegates() {
69+
TestConnectionDetailsFactory orderOne = new TestConnectionDetailsFactory(1);
70+
TestConnectionDetailsFactory orderTwo = new TestConnectionDetailsFactory(2);
71+
TestConnectionDetailsFactory orderThree = new TestConnectionDetailsFactory(3);
72+
this.loader.addInstance(ConnectionDetailsFactory.class, orderOne, orderThree, orderTwo);
73+
ConnectionDetailsFactories factories = new ConnectionDetailsFactories(this.loader);
74+
ConnectionDetailsFactory<String, ConnectionDetails> factory = factories.getConnectionDetailsFactory("source");
75+
assertThat(factory).asInstanceOf(InstanceOfAssertFactories.type(CompositeConnectionDetailsFactory.class))
76+
.satisfies((composite) -> assertThat(composite.getDelegates()).containsExactly(orderOne, orderTwo,
77+
orderThree));
78+
}
79+
80+
private static final class TestConnectionDetailsFactory
81+
implements ConnectionDetailsFactory<String, TestConnectionDetails>, Ordered {
82+
83+
private final int order;
84+
85+
private TestConnectionDetailsFactory() {
86+
this(0);
87+
}
88+
89+
private TestConnectionDetailsFactory(int order) {
90+
this.order = order;
91+
}
92+
93+
@Override
94+
public TestConnectionDetails getConnectionDetails(String source) {
95+
return new TestConnectionDetails();
96+
}
97+
98+
@Override
99+
public int getOrder() {
100+
return this.order;
101+
}
102+
103+
}
104+
105+
private static final class TestConnectionDetails implements ConnectionDetails {
106+
107+
private TestConnectionDetails() {
108+
}
109+
110+
}
111+
112+
}

spring-boot-project/spring-boot-docs/src/docs/asciidoc/howto/testing.adoc

Lines changed: 38 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,11 +29,46 @@ Testcontainers can be used in a Spring Boot test as follows:
2929
include::code:vanilla/MyIntegrationTests[]
3030

3131
This will start up a docker container running Neo4j (if Docker is running locally) before any of the tests are run.
32-
In most cases, you will need to configure the application using details from the running container, such as container IP or port.
32+
In most cases, you will need to configure the application to connect to the service running in the container.
3333

34-
This can be done with a static `@DynamicPropertySource` method that allows adding dynamic property values to the Spring Environment.
3534

36-
include::code:dynamicproperties/MyIntegrationTests[]
35+
[[howto.testing.testcontainers.service-connections]]
36+
==== Service Connections
37+
A service connection is a connection to any remote service.
38+
Spring Boot's auto-configuration can consume the details of a service connection and use them to establish a connection to a remote service.
39+
When doing so, the connection details take precedence over any connection-related configuration properties.
40+
41+
When using Testcontainers, connection details can be automatically created for a service running in a container by annotating the container field in the test class.
42+
43+
include::code:MyIntegrationTests[]
44+
45+
Thanks to `@Neo4jServiceConnection`, the above configuration allows Neo4j-related beans in the application to communicate with Neo4j running inside the Testcontainers-managed Docker container.
46+
This is done by automatically defining a `Neo4jConnectionDetails` bean which is then used by the Neo4j auto-configuration, overriding any connection-related configuration properties.
47+
48+
The following service connection annotations are provided by `spring-boot-test-autoconfigure`:
49+
50+
- `@CassandraServiceConnection`
51+
- `@CouchbaseServiceConnection`
52+
- `@ElasticsearchServiceConnection`
53+
- `@InfluxDbServiceConnection`
54+
- `@JdbcServiceConnection`
55+
- `@KafkaServiceConnection`
56+
- `@MongoServiceConnection`
57+
- `@Neo4jServiceConnection`
58+
- `@R2dbcServiceConnection`
59+
- `@RabbitServiceConnection`
60+
- `@RedisServiceConnection`
61+
62+
As with the earlier `@Neo4jConnectionDetails` example, each can be used on a container field. Doing so will automatically configure the application to connect to the service running in the container.
63+
64+
65+
66+
[[howto.testing.testcontainers.dynamic-properties]]
67+
==== Dynamic Properties
68+
A slightly more verbose but also more flexible alternative to service connections is `@DynamicPropertySource`.
69+
A static `@DynamicPropertySource` method allows adding dynamic property values to the Spring Environment.
70+
71+
include::code:/MyIntegrationTests[]
3772

3873
The above configuration allows Neo4j-related beans in the application to communicate with Neo4j running inside the Testcontainers-managed Docker container.
3974

0 commit comments

Comments
 (0)