jeecgboot / JeecgBoot

🔥「企业级低代码平台」前后端分离架构SpringBoot 2.x/3.x,SpringCloud,Ant Design&Vue3,Mybatis,Shiro,JWT。强大的代码生成器让前后端代码一键生成,无需写任何代码! 引领新的开发模式,引入AI模型能力 OnlineCoding->代码生成->手工MERGE,帮助Java项目解决70%重复工作,让开发更关注业务,既能快速提高效率,帮助公司节省成本,同时又不失灵活性。
http://www.jeecg.com
Apache License 2.0
40.7k stars 14.85k forks source link

【求助】通过切换端口不停机更新后端,重启服务时,websocket无法连接 #6994

Closed catlvsuger closed 3 months ago

catlvsuger commented 3 months ago
版本号:3.10
问题描述:通过切换端口不停机更新后端,比如默认端口8080,更新时判断8080是否被占用,被占用使用替代端口启动,启动后,再停止原8080服务,将新的服务切换到8080,websokect连接,会进入ShiroRealm验证,请求地址会变成htp://localhost:8080/jeecg-boot/error ,导致连接失败,第一次启动没问题,重启切换端口就会出现该问题
错误截图:

image

image

友情提示:

catlvsuger commented 3 months ago

启动代码:public static void main(String[] args) throws UnknownHostException { String[] newArgs; //减少更新停机时间,切换时间1s左右,带来问题就是端口写死 int defaultPort = 8080; int backupPort = 9999; String ip = InetAddress.getLocalHost().getHostAddress();

    if (!isPortInUse(defaultPort)) {
        newArgs = setNewArgs(args, defaultPort);
        ConfigurableApplicationContext application = SpringApplication.run(JeecgSystemApplication.class, newArgs);
        Environment env = application.getEnvironment();

// String port = env.getProperty("server.port"); String path = oConvertUtils.getString(env.getProperty("server.servlet.context-path")); logInfo(defaultPort, path, ip); }else { log.info("Port " + defaultPort + " is in use. Trying backup port " + backupPort); newArgs = setNewArgs(args, backupPort); log.info("Start new instance asynchronously "); ConfigurableApplicationContext application = SpringApplication.run(JeecgSystemApplication.class, newArgs); Environment env = application.getEnvironment(); String path = oConvertUtils.getString(env.getProperty("server.servlet.context-path")); logInfo(backupPort, path, ip); try { waitForHealthyInstance(backupPort, path); log.info("New instance on port " + backupPort + " is healthy. Proceeding with traffic switch."); // Shut down old instance String command = String.format("lsof -i :%d | grep LISTEN | awk '{print $2}' | xargs kill -9", defaultPort); try { Runtime.getRuntime().exec(new String[] { "sh", "-c", command }).waitFor(); } catch (IOException | InterruptedException e) { log.error("Error shutting down old instance", e); } // Wait for default port to be free and restart new instance on default port waitForPortRelease(defaultPort); // WebSocket.closeAllSessions(); restartOnDefaultPort(application, defaultPort);

        } catch (InterruptedException e) {
            log.error("Error during new instance startup", e);
        }
    }
}

private static void logInfo(int backupPort, String path, String ip) {
    log.info("\n----------------------------------------------------------\n\t" +
            "Application Jeecg-Boot is running! Access URLs:\n\t" +
            "Local: \t\thttp://localhost:" + backupPort + path + "/\n\t" +
            "External: \thttp://" + ip + ":" + backupPort + path + "/\n\t" +
            "Swagger文档: \thttp://" + ip + ":" + backupPort + path + "/doc.html\n" +
            "----------------------------------------------------------");
}

private static String[] setNewArgs(String[] args, int port) {
    String[] newArgs = new String[args.length + 1];
    System.arraycopy(args, 0, newArgs, 0, args.length);
    newArgs[newArgs.length - 1] = "--server.port=" + port;
    return newArgs;
}

private static void waitForHealthyInstance(int port, String path) throws InterruptedException {
    log.info("Waiting for new instance to become healthy...");
    int retries = 10;
    int delay = 5000; // 5 seconds
    while (retries > 0) {
        try {
            String urlStr = "http://localhost:" + port  + path + "/sys/api/health";
            log.info("url:{}.", urlStr);
            URL url = new URL(urlStr); // Modify the health endpoint as needed
            HttpURLConnection connection = (HttpURLConnection) url.openConnection();
            connection.setRequestMethod("GET");
            connection.setConnectTimeout(3000);
            connection.connect();
            int responseCode = connection.getResponseCode();
            if (responseCode == 200) {
                log.info("New instance on port " + port + " is healthy.");
                return;
            }
        } catch (IOException e) {
            log.warn("Health check failed. Retrying...");
        }
        Thread.sleep(delay);
        retries--;
    }
    throw new IllegalStateException("New instance did not become healthy within the expected time.");
}

private static void waitForPortRelease(int port) throws InterruptedException {
    log.info("Waiting for port " + port + " to be released...");
    while (isPortInUse(port)) {
        Thread.sleep(100); // Check every 1 second
    }
    log.info("Port " + port + " is now free.");
}

private static void restartOnDefaultPort(ConfigurableApplicationContext application, int defaultPort) {
    log.info("Restarting application on default port " + defaultPort);
    // 获取当前运行的ServletWebServerApplicationContext
    ServletWebServerApplicationContext servletAppContext = (ServletWebServerApplicationContext) application;
    // 停止当前的Web服务器
    WebServer webServer = servletAppContext.getWebServer();
    if (webServer != null) {
        webServer.stop();
        WebSocket.closeAllSessions();
    }
    // 重新配置端口
    ServletWebServerFactory webServerFactory = getWebServerFactory(servletAppContext);
    ((TomcatServletWebServerFactory) webServerFactory).setPort(defaultPort);
    WebServer newWebServer = webServerFactory.getWebServer(invokeSelfInitialize((ServletWebServerApplicationContext) application));
    newWebServer.start();
    log.info("Restarting application on default port " + defaultPort + " is successful.");
}

private static ServletContextInitializer invokeSelfInitialize(ServletWebServerApplicationContext context) {
    try {
        Method method = ServletWebServerApplicationContext.class.getDeclaredMethod("getSelfInitializer");
        method.setAccessible(true);
        return (ServletContextInitializer) method.invoke(context);
    } catch (Throwable e) {
        throw new RuntimeException(e);
    }
}

private static boolean isPortInUse(int port) {
    try (ServerSocket serverSocket = new ServerSocket(port)) {
        return false;
    } catch (IOException e) {
        return true;
    }
}

private static ServletWebServerFactory getWebServerFactory(ConfigurableApplicationContext context) {
    String[] beanNames = context.getBeanFactory().getBeanNamesForType(ServletWebServerFactory.class);
    return context.getBeanFactory().getBean(beanNames[0], ServletWebServerFactory.class);
}
catlvsuger commented 3 months ago

先正常启动个服务8080,再启动个新的服务9999,新服务启动成功前把旧服务停掉,新的服务切换回8080,此时webSocket连接失败,webSocket请求 会进入 DispatcherServlet 在 ResourceHttpRequestHandler 返回 Resource not found, DispatcherServlet:1131 - Completed 404 NOT_FOUND tomcat日志 Processing ErrorPage[errorCode=0, location=/error] 回到 shiroRealm 的 doGetAuthenticationInfo,抛出异常 ShiroConfig 已经加了webSocket过滤,正常应该不会进去Shiro,在第二次启动时重新加载 filterChainDefinitionMap 也没有用...

zhangdaiscott commented 3 months ago

更新这个试试 https://github.com/jeecgboot/JeecgBoot/commit/d0f09480caa3c9a45ceedb83515aba662de1477c

catlvsuger commented 3 months ago

更新了还是一样,前端重连是成功的,进到 WebsocketFilter校验token通过后,filterChain.doFilter(servletRequest, servletResponse);走后续的 过滤器就出问题了