IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> Java知识库 -> SpringBoot动态端口配置 -> 正文阅读

[Java知识库]SpringBoot动态端口配置

在实际项目中一台主机可能会启动两个相同的服务,所以需要配置的动态端口。
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.xxx.commons;

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);
        }
    }

    public static void main(String[] args) throws IOException {
        int i = Integer.parseInt(args[0]);
        kill(false, i);
    }

    // 执行命令并且读取结果
    private static List<String> execAndRead(String[] command, Boolean isLinux, Set<Integer> ports) throws IOException {
        // 读取结果
        List<String> lines = new ArrayList<String>();
        if (!isLinux) {
        //自己实现

        } 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;
    }

    /**
     * 验证此行是否为指定的端口,因为 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);
        while (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.AVAILABLE : KillResult.KILLED;
            } else {
                killResult = KillResult.FAIL;
            }
        } else {
            log.info("未查询到可以被结束的进程");
            //没有可结束的进程只能返回AVAILABLE、FAIL
            killResult = !existListen ? KillResult.AVAILABLE : KillResult.FAIL;
        }
        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 {
//            自己实现;
            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?工具类中配置好了,可以直接使用。

  Java知识库 最新文章
计算距离春节还有多长时间
系统开发系列 之WebService(spring框架+ma
springBoot+Cache(自定义有效时间配置)
SpringBoot整合mybatis实现增删改查、分页查
spring教程
SpringBoot+Vue实现美食交流网站的设计与实
虚拟机内存结构以及虚拟机中销毁和新建对象
SpringMVC---原理
小李同学: Java如何按多个字段分组
打印票据--java
上一篇文章      下一篇文章      查看所有文章
加:2022-08-06 10:29:56  更:2022-08-06 10:31:51 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年11日历 -2024/11/23 13:18:42-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码