diff --git a/spring-test/src/main/java/org/springframework/test/util/TestSocketUtils.java b/spring-test/src/main/java/org/springframework/test/util/TestSocketUtils.java new file mode 100644 index 0000000000..53c02be7cb --- /dev/null +++ b/spring-test/src/main/java/org/springframework/test/util/TestSocketUtils.java @@ -0,0 +1,139 @@ +/* + * Copyright 2002-2022 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.test.util; + +import java.net.InetAddress; +import java.net.ServerSocket; +import java.util.Random; + +import javax.net.ServerSocketFactory; + +import org.springframework.util.Assert; + +/** + * Simple utility for finding available TCP ports on {@code localhost} for use in + * integration testing scenarios. + * + *

This is a limited form of {@link org.springframework.util.SocketUtils} which + * has been deprecated since Spring Framework 5.3.16 and removed in Spring + * Framework 6.0. + * + *

{@code TestSocketUtils} can be used in integration tests which start an + * external server on an available random port. However, these utilities make no + * guarantee about the subsequent availability of a given port and are therefore + * unreliable. Instead of using {@code TestSocketUtils} to find an available local + * port for a server, it is recommended that you rely on a server's ability to + * start on a random ephemeral port that it selects or is assigned by the + * operating system. To interact with that server, you should query the server + * for the port it is currently using. + * + * @author Sam Brannen + * @author Ben Hale + * @author Arjen Poutsma + * @author Gunnar Hillert + * @author Gary Russell + * @author Chris Bono + * @since 5.3.24 + */ +public class TestSocketUtils { + + /** + * The minimum value for port ranges used when finding an available TCP port. + */ + static final int PORT_RANGE_MIN = 1024; + + /** + * The maximum value for port ranges used when finding an available TCP port. + */ + static final int PORT_RANGE_MAX = 65535; + + private static final int PORT_RANGE_PLUS_ONE = PORT_RANGE_MAX - PORT_RANGE_MIN + 1; + + private static final int MAX_ATTEMPTS = 1_000; + + private static final Random random = new Random(System.nanoTime()); + + private static final TestSocketUtils INSTANCE = new TestSocketUtils(); + + + /** + * Although {@code TestSocketUtils} consists solely of static utility methods, + * this constructor is intentionally {@code public}. + *

Rationale

+ *

Static methods from this class may be invoked from within XML + * configuration files using the Spring Expression Language (SpEL) and the + * following syntax. + *


+	 * <bean id="myBean" ... p:port="#{T(org.springframework.test.util.TestSocketUtils).findAvailableTcpPort()}" />
+	 * 
+ *

If this constructor were {@code private}, you would be required to supply + * the fully qualified class name to SpEL's {@code T()} function for each usage. + * Thus, the fact that this constructor is {@code public} allows you to reduce + * boilerplate configuration with SpEL as can be seen in the following example. + *


+	 * <bean id="socketUtils" class="org.springframework.test.util.TestSocketUtils" />
+	 * <bean id="myBean" ... p:port="#{socketUtils.findAvailableTcpPort()}" />
+	 * 
+ */ + public TestSocketUtils() { + } + + /** + * Find an available TCP port randomly selected from the range [1024, 65535]. + * @return an available TCP port number + * @throws IllegalStateException if no available port could be found + */ + public static int findAvailableTcpPort() { + return INSTANCE.findAvailableTcpPortInternal(); + } + + + /** + * Internal implementation of {@link #findAvailableTcpPort()}. + *

Package-private solely for testing purposes. + */ + int findAvailableTcpPortInternal() { + int candidatePort; + int searchCounter = 0; + do { + Assert.state(++searchCounter <= MAX_ATTEMPTS, () -> String.format( + "Could not find an available TCP port in the range [%d, %d] after %d attempts", + PORT_RANGE_MIN, PORT_RANGE_MAX, MAX_ATTEMPTS)); + candidatePort = PORT_RANGE_MIN + random.nextInt(PORT_RANGE_PLUS_ONE); + } + while (!isPortAvailable(candidatePort)); + + return candidatePort; + } + + /** + * Determine if the specified TCP port is currently available on {@code localhost}. + *

Package-private solely for testing purposes. + */ + boolean isPortAvailable(int port) { + try { + ServerSocket serverSocket = ServerSocketFactory.getDefault() + .createServerSocket(port, 1, InetAddress.getByName("localhost")); + serverSocket.close(); + return true; + } + catch (Exception ex) { + return false; + } + } + +} diff --git a/spring-test/src/test/java/org/springframework/test/util/TestSocketUtilsTests.java b/spring-test/src/test/java/org/springframework/test/util/TestSocketUtilsTests.java new file mode 100644 index 0000000000..8ebc85f85f --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/util/TestSocketUtilsTests.java @@ -0,0 +1,61 @@ +/* + * Copyright 2002-2022 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.test.util; + +import org.junit.jupiter.api.RepeatedTest; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalStateException; + +/** + * Unit tests for {@link TestSocketUtils}. + * + * @author Sam Brannen + * @author Gary Russell + * @since 5.3.24 + */ +class TestSocketUtilsTests { + + @Test + void canBeInstantiated() { + // Just making sure somebody doesn't try to make SocketUtils abstract, + // since that would be a breaking change due to the intentional public + // constructor. + new TestSocketUtils(); + } + + @RepeatedTest(10) + void findAvailableTcpPort() { + assertThat(TestSocketUtils.findAvailableTcpPort()) + .isBetween(TestSocketUtils.PORT_RANGE_MIN, TestSocketUtils.PORT_RANGE_MAX); + } + + @Test + void findAvailableTcpPortWhenNoAvailablePortFoundInMaxAttempts() { + TestSocketUtils socketUtils = new TestSocketUtils() { + @Override + boolean isPortAvailable(int port) { + return false; + } + }; + assertThatIllegalStateException() + .isThrownBy(socketUtils::findAvailableTcpPortInternal) + .withMessage("Could not find an available TCP port in the range [1024, 65535] after 1000 attempts"); + } + +}