在实际项目中一台主机可能会启动两个相同的服务,所以需要配置的动态端口。
SpringBoot动态端口是在配置文件中配置:
server:
port: 0
但是这样配置有缺点:每次启动端口都会变化,注册中心来不及更新导致会有较长时间无法服务。
所以我想在配置文件中指定多个的端口,如:
server:
port: 0 # 备用选项为随机分配
ports: 8161,8162
1.为了实现这样的支持,我实现了两个工具类:
package com.xxx.commons;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.context.embedded.ConfigurableEmbeddedServletContainer;
import org.springframework.boot.context.embedded.EmbeddedServletContainerCustomizer;
import org.springframework.core.env.Environment;
import org.springframework.util.StringUtils;
import javax.annotation.Resource;
import java.io.IOException;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.Socket;
/**
* 动态配置端口
*
* @author xhy
* @date 2022/7/24
*/
@Slf4j
public class DynamicPortConfigUtil implements EmbeddedServletContainerCustomizer {
@Resource
private Environment environment;
@Override
public void customize(ConfigurableEmbeddedServletContainer container) {
try {
String serverPorts = environment.getProperty("server.ports");
if (!StringUtils.isEmpty(serverPorts)) {
String[] split = serverPorts.split(",");
for (String s : split) {
int port = Integer.parseInt(s.trim());
if (isPortAvailable(port)) {
//环境属性保持一致
System.getProperties().put("server.port", port);
/**
* 如果使用了spring cloud config,那么${server.port}会被提前替换为yml里面定义的值
* 此时可以自定义一个${dynamicPort}
*/
System.getProperties().put("dynamicPort", port);
//设置启动端口
container.setPort(port);
log.info("Succeed bind port {}", port);
return;
} else {
log.info("The port {} is occupied ", port);
}
}
log.info("Configuring customized dynamic ports has failed and trying use default server.port {}", environment.getProperty("server.port"));
}
} catch (Exception e) {
log.error("Error occurred while config port form properties", e);
throw new RuntimeException(e);
}
}
private static boolean isPortAvailable(int port) {
int i = 1;
while (i <= 3) {
try {
checkPortAvailable("0.0.0.0", port);
String hostAddress = null;
try {
hostAddress = InetAddress.getLocalHost().getHostAddress();
} catch (Exception e) {
e.printStackTrace();
}
if (hostAddress != null) {
checkPortAvailable(hostAddress, port);
}
return true;
} catch (Exception e) {
log.info("[{}] bind port {},{},wait retrying ", i, port, e.getMessage());
//第2次尝试干掉被占用的端口
if (i == 2) {
try {
log.info("[{}] trying kill port {}", i, port);
/**
* 非强制杀死进程,只会杀死TCP:CLOSE_WAIT进程。(单机集群环境下 TCP:LISTEN 的进程显然不允许被杀死)
*/
KillPortUtils.KillResult result = KillPortUtils.kill(false, port);//isForce 一定传false,否则导致正常运行的服务宕机
if (KillPortUtils.KillResult.AVAILABLE.equals(result)) {
return true;
}
} catch (IOException e1) {
log.info("[{}] killing fail port {} {}", i, port, e1.getMessage());
}
}
try {
Thread.sleep(1000);
} catch (InterruptedException e1) {
e1.printStackTrace();
}
} finally {
i++;
}
}
return false;
}
private static void checkPortAvailable(String host, int port) throws Exception {
Socket s = new Socket();
s.bind(new InetSocketAddress(host, port));
s.close();
}
}
package com.giigle.commons;
import com.google.common.collect.Sets;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections.CollectionUtils;
import org.springframework.util.StringUtils;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* 关闭端口
*
* @author xhy
* @date 2022/7/24 11:06
*/
@Slf4j
public class KillPortUtils {
/**
* 结束占用端口的进程
* isForce = true 时
* return = KILLED(成功杀死进程),FAIL(杀死进程失败或不存在该进程)
* isForce = false 时
* return = AVAILABLE(成功杀死CLOSE_WAIT进程,由于不存在TCP:LISTEN 的进程,所以可以尝试重复占用该端口),KILLED(成功杀死TCP:CLOSE_WAIT的进程),FAIL(杀死进程失败或不存在该进程)
*/
public static KillResult kill(boolean isForce, int port) throws IOException {
Set<Integer> ports = new HashSet<>();
// 将要关闭的端口添加到set中
ports.add(port);
// 判断linux环境
Boolean isLinux = isOSLinux();
// 查询端口命令 linux 与 windows区分
String[] command = isLinux ? new String[]{"/bin/sh", "-c", "netstat -anp |grep " + port} : new String[]{"cmd /c netstat -ano | findstr " + port};
// 读取内容
List<String> read = execAndRead(command, isLinux, ports);
if (read.size() == 0) {
log.info("未查询到端口被占用");
return KillResult.FAIL;
} else {
// 关闭端口
return kill(isForce, read, isLinux, port);
}
}
// 执行命令并且读取结果
private static List<String> execAndRead(String[] command, Boolean isLinux, Set<Integer> ports) throws IOException {
// 读取结果
List<String> lines = new ArrayList<String>();
if (!isLinux) {
// PowerShell session = PowerShell.openSession();
// PowerShellResponse powerShellResponse = session.executeCommand(command);
// String outPut = powerShellResponse.getCommandOutput();
// if (StringUtils.isBlank(outPut)) {
// log.error("未查询到端口被占用");
// session.close();
// return read;
// }
// // 获取换行符
// String lineSeparator = System.lineSeparator();
// // 换行
// String[] lineArray = outPut.split(lineSeparator);
// if (lineArray.length > 0) {
// for (int i = 0; i < lineArray.length; i++) {
// String line = lineArray[i];
// // 验证端口
// boolean validPort = validPort(line, isLinux, ports);
// if (validPort) {
// // 添加内容
// read.add(line);
// }
// }
// }
// session.close();
} else {
Runtime runtime = Runtime.getRuntime();
//查找进程号
log.info("执行命令:{}", command[2]);
Process p = runtime.exec(command);
InputStream inputStream = p.getInputStream();
BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream, StandardCharsets.UTF_8));
try {
String line;
int i = 1;
while ((line = reader.readLine()) != null) {
// 验证端口
log.info("执行结果[{}]:{}", i++, line);
boolean validPort = validPort(line, isLinux, ports);
if (validPort) {
// 添加内容
lines.add(line);
}
}
inputStream.close();
reader.close();
} catch (Exception e) {
log.error(e.getMessage(), e);
} finally {
inputStream.close();
reader.close();
}
}
return lines;
}
public static void main(String[] args) {
String str = "tcp 0 0 172.16.48.63:43746 172.16.48.63:8161 ESTABLISHED 5604/java";
boolean b = validPort(str, true, Sets.newHashSet(8161));
System.out.println(b);
}
/**
* 验证此行是否为指定的端口,因为 findstr命令会是把包含的找出来,例如查找80端口,但是会把8099查找出来
*
* @param str
* @return
*/
private static boolean validPort(String str, Boolean isLinux, Set<Integer> ports) {
String find;
// linux TCP 0.0.0.0:12349 0.0.0.0:0 LISTENING 30700
// windows tcp 0 0 0.0.0.0:8888 0.0.0.0:* LISTEN 2319/python
String reg = isLinux ? ":[0-9]+" : "^ *[a-zA-Z]+ +\\S+";
// 匹配正则
Pattern pattern = Pattern.compile(reg);
Matcher matcher = pattern.matcher(str);
if (matcher.find()) {
// 获取匹配内容
find = matcher.group(0);
// 处理数据
int spStart = find.lastIndexOf(":");
// 截取掉冒号
find = find.substring(spStart + 1);
int port = 0;
try {
port = Integer.parseInt(find);
// 端口在其中 则通过验证
if (ports.contains(port)) {
return true;
}
} catch (NumberFormatException e) {
log.warn(e.getMessage());
}
}
return false;
}
/**
* 更换为一个Set,去掉重复的pid值
*
* @param data
*/
private static KillResult kill(boolean isForce, List<String> data, Boolean isLinux, int port) throws IOException {
// linux tcp6 0 0 :::9011 :::* LISTEN 22760/java
// windows tcp 0 0 0.0.0.0:8888 0.0.0.0:* LISTEN 2319/python
Set<Integer> pids = new HashSet<>();
boolean existListen = false;
for (String line : data) {
if (isLinux && !isForce) {
if (line.contains("LISTEN") || line.contains("ESTABLISHED")) {
existListen = true;
}
//isForce=false 只能杀死端口状态CLOSE_WAIT的进程
if (!line.contains("CLOSE_WAIT")) {
continue;
}
}
// 去除前后空格
line = line.trim();
// 获取最后一个空格下标
int offset = line.lastIndexOf(" ");
// 截取最后的内容 如 30700 或者 2319/python
String spid = line.substring(offset);
// 替换其中的空格
spid = spid.replaceAll(" ", "");
// 如果存在/
int lastSlashIndex = spid.lastIndexOf("/");
if (lastSlashIndex != -1) {
// 处理/
spid = spid.substring(0, lastSlashIndex);
}
try {
int pid;
pid = Integer.parseInt(spid);
pids.add(pid);
} catch (NumberFormatException e) {
log.error(e.getMessage(), e);
}
}
KillResult killResult;
if (CollectionUtils.isNotEmpty(pids)) {
log.info("需要关闭的pid:" + pids);
if (killWithPid(pids, isLinux)) {
killResult = existListen ? KillResult.KILLED : KillResult.AVAILABLE;
} else {
killResult = KillResult.FAIL;
}
} else {
log.info("未查询到可以被结束的进程");
//没有可结束的进程只能返回AVAILABLE、FAIL
killResult = existListen ? KillResult.FAIL : KillResult.AVAILABLE;
}
if (KillResult.AVAILABLE.equals(killResult)) {
log.info("由于不存在 TCP:LISTEN 的进程,所以可以尝试强行占用{}端口", port);
}
return killResult;
}
/**
* 一次性杀除所有的端口
*
* @param pids
*/
private static boolean killWithPid(Set<Integer> pids, Boolean isLinux) throws IOException {
if (isLinux) {
int count = 0;
for (Integer pid : pids) {
String commond = "kill -9 " + pid;
log.info("执行命令:" + commond);
Process process = Runtime.getRuntime().exec(commond);
InputStream inputStream = process.getInputStream();
String txt = readTxt(inputStream, "GBK");
if (StringUtils.isEmpty(txt)) {
log.info("执行结果:成功");
} else {
log.info("执行结果:" + txt);
}
count++;
}
return count > 0;
} else {
// PowerShell session = PowerShell.openSession();
// for (Integer pid : pids) {
// String commond = "taskkill /F /pid " + pid;
// log.error("关闭端口命令:" + commond);
// PowerShellResponse powerShellResponse = session.executeCommand(commond);
// String outPut = powerShellResponse.getCommandOutput();
// log.error("关闭端口结果:" + outPut);
// }
// session.close();
return false;
}
}
private static List<String> read(String outPut, Boolean isLinux, Set<Integer> ports) throws IOException {
List<String> data = new ArrayList<>();
// 获取换行符
String lineSeparator = System.lineSeparator();
// 换行
String[] lineArray = outPut.split(lineSeparator);
if (lineArray.length > 0) {
for (int i = 0; i < lineArray.length; i++) {
String line = lineArray[i];
// 验证端口
boolean validPort = validPort(line, isLinux, ports);
if (validPort) {
// 添加内容
data.add(line);
}
}
}
return data;
}
private static boolean isOSLinux() {
Properties prop = System.getProperties();
String os = prop.getProperty("os.name");
return os != null && os.toLowerCase().contains("linux");
}
private static String readTxt(InputStream in, String charset) throws IOException {
BufferedReader reader = new BufferedReader(new InputStreamReader(in, charset));
StringBuilder sb = new StringBuilder();
String line;
while ((line = reader.readLine()) != null) {
sb.append(line);
}
reader.close();
return sb.toString();
}
/**
* 结束占用端口的进程
* isForce = true 时
* return = KILLED(成功杀死进程),FAIL(杀死进程失败或不存在该进程)
* isForce = false 时
* return = AVAILABLE(成功杀死CLOSE_WAIT进程,由于不存在TCP:LISTEN 的进程,所以可以尝试重复占用该端口),KILLED(成功杀死TCP:CLOSE_WAIT的进程),FAIL(杀死进程失败或不存在该进程)
*/
public enum KillResult {
AVAILABLE,
KILLED,
FAIL;
}
}
2.将这两个类添加到你的项目之后,再配置一个BeanConfig就可以了。
package com.xxx.config;
import com.giigle.commons.DefaultGlobalExceptionHandler;
import com.giigle.commons.DynamicPortConfigUtil;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class BeanConfig {
@Bean
public DynamicPortConfigUtil dynamicPortConfigUtil(){
return new DynamicPortConfigUtil();
}
}
3.eureka显示动态配置的端口配置如下:
eureka:
instance:
instance-id: ${spring.cloud.client.ipAddress}:${dynamicPort}
如果你的项目使用spring cloud config ,则yml中的${server.port}会被直接静态替换(本项目中是0)
所以可以使用 ${dynamicPort} ,这个变量已经在 DynamicPortConfigUtil 工具类中配置好了,可以直接使用。