Configuring Port Assignment Strategies in Spring Boot and Spring Cloud Applications
Spring Boot and Spring Cloud applications support both fixed and random port assignment. However, both methods present mangaement challenges. Fixed ports risk conflicts, especial in microservice architectures. Random ports, while avoiding conflicts, introduce unpredictability in service discovery and conneciton. This article details three port configuration strategies: fixed, random within a range, and incremental within a range, using properties file syntax.
1. Fixed Port Assignment
This is the standard approach, defining a specific port in the configuration.
# Standard fixed port configuration
server.port=8080
# Alternatively, read from an environment variable
server.port=${APP_SERVER_PORT}
2. Random Port Within a Specified Range
Spring Boot's built-in property randomizer allows for port selection within a defined interval.
# Port is randomly selected between 10000 and 19999
server.port=${random.int[10000,19999]}
3. Programmatic Port Selection Strategy
This approach uses a custom strategy to determine the port, often reading from or setting an environment variable programmatically.
3.1 Configuration File
# The port value is sourced from an environment variable
server.port=${CUSTOM_PORT_ENV}
3.2 Aplication Main Method Logic
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class ApplicationMain {
public static void main(String[] args) {
// Set the environment variable before the application context loads
System.setProperty("CUSTOM_PORT_ENV", ApplicationMain.determinePort());
SpringApplication.run(ApplicationMain.class, args);
}
private static String determinePort() {
// Implement custom logic to return a port string
// Example: Check a condition or read from an external source
// if (someCondition) {
// return "5555";
// }
return "44455";
}
}
4. Incrremental Port Assignment
This strategy finds the next available port within a given range, useful for local development to avoid conflicts. It extends the programmatic approach using a custom PropertySource.
4.1 Configuration File
# Custom property syntax: default start port is 8888, end is 9999
server.port=${portFinder.range:8888,9999}
4.2 Application Main Method with Listener
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ApplicationListener;
import org.springframework.boot.context.event.ApplicationEnvironmentPreparedEvent;
@SpringBootApplication
public class ApplicationMain {
public static void main(String[] args) {
SpringApplication app = new SpringApplication(ApplicationMain.class);
app.addListeners((ApplicationListener<ApplicationEnvironmentPreparedEvent>) event -> {
// Register the custom property source
event.getEnvironment().getPropertySources().addLast(new IncrementalPortFinder());
});
app.run(args);
}
}
4.3 Custom PropertySource Implementation
import org.springframework.core.env.PropertySource;
public class IncrementalPortFinder extends PropertySource<PortScanner> {
private static final String PROPERTY_SOURCE_NAME = "portFinder";
private static final String PREFIX = "portFinder.";
public IncrementalPortFinder() {
super(PROPERTY_SOURCE_NAME, new PortScanner());
}
@Override
public Object getProperty(String name) {
if (!name.startsWith(PREFIX)) {
return null;
}
return findAvailablePort(name.substring(PREFIX.length()));
}
private Integer findAvailablePort(String configValue) {
configValue = configValue.replace("range:", "");
String[] portLimits = configValue.split(",");
try {
if (portLimits.length == 1) {
// Scan from the given start port to 65535
return new PortScanner().findPort(Integer.parseInt(portLimits[0]));
} else {
// Scan within the specified start and end range
return new PortScanner().findPort(Integer.parseInt(portLimits[0]), Integer.parseInt(portLimits[1]));
}
} catch (Exception e) {
e.printStackTrace();
return 8080; // Fallback port
}
}
}
4.4 Core Port Scanning Logic
import lombok.extern.slf4j.Slf4j;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.Socket;
@Slf4j
public class PortScanner {
public int findPort(int startPort) {
return findPort(startPort, 65535);
}
public int findPort(int startPort, int endPort) {
log.info("Scanning for an available server port from {} to {}.", startPort, endPort);
for (int currentPort = startPort; currentPort <= endPort; currentPort++) {
if (!isPortInUse("localhost", currentPort)) {
log.info("Selected available port: {}.", currentPort);
return currentPort;
} else {
log.debug("Port {} is occupied, trying next port.", currentPort);
}
}
log.error("No available ports found in the specified range {} - {}.", startPort, endPort);
return startPort; // Return start port as a fallback
}
/**
* Checks if a given IP and port is already in use.
*/
private boolean isPortInUse(String host, int port) {
try (Socket socket = new Socket()) {
socket.connect(new InetSocketAddress(host, port), 100); // 100ms timeout
return true;
} catch (IOException e) {
return false; // Port is likely available
}
}
}