SpringBoot集成Socket服務后打包(war包)啟動時如何啟動Socket服務(web應用外部tomcat啟動)


 
1、首先知道SpringBoot打包為jar和war包是不一樣的(只討論SpringBoot環境下web應用打包)
    1.1、jar和war包的打開方式不一樣,雖然都依賴java環境,但是jar包只需要外部提供java環境,war需要外部提供java和servlet環境
    1.2、jar包和war包的啟動流程不一樣:
            ①、jar包的啟動時直接由項目的主函數開始啟動,此時會先初始化IOC容器,然后才會進行內置Servlet環境(一般為Tomcat)的啟動。
//jar包啟動入口
@SpringBootApplication
public class HelloWorldApplcation {

    public static void main(String[] args) {
        SpringApplication.run(HelloWorldApplcation.class, args);
    }

}

 

            ②、war包通常使用Tomcat進行部署啟動,在tomcat啟動war應用時,會先進行Servlet環境的初始化,之后才會進行到IOC容器的初始化,也就是說,在servlet初始化過程中是不能使用IOC依賴注入的。

public class ServletInitializer extends SpringBootServletInitializer {

    @Override
    protected SpringApplicationBuilder configure(SpringApplicationBuilder application) {
        
        return application.sources(HelloWorldApplcation.class);
    }

}

 

2、實現在springboot應用啟動的時候啟動Socket服務
    2.1、jar包情況下,將Socket服務交由IOC容器進行管理,然后在IOC容器創建完成后,獲取到已經初始化的Socket服務實例,然后開始Socket服務。在這樣的情況下,可以在Socket服務類上進行依賴注入,也就是說可以使用@Autowired
@SpringBootApplication
public class HelloWorldApplcation {

    public static void main(String[] args) {
        ApplicationContext applicationContext = SpringApplication.run(HelloWorldApplcation.class, args);
        SocketServer socketServer =applicationContext.getBean(SocketServer.class);
        socketServer.start();
    }

}

 

2.2、war包情況下,由於是先進行Servlet環境的初始化,然后再進行IOC容器的創建,有了這個先后順序,即知道,是不可能在Servlet創建時拿到WebApplicationContext對象的,下面是我的驗證

package com.geniuses.sewage_zero_straight.listener;

import com.geniuses.sewage_zero_straight.net.socket.SocketServer;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.context.WebApplicationContext;
import org.springframework.web.context.support.WebApplicationContextUtils;

import javax.servlet.ServletContextEvent;
import javax.servlet.ServletContextListener;
import javax.servlet.annotation.WebListener;

@Slf4j
@WebListener
public class SocketListener implements ServletContextListener {

    @Autowired
    private SocketServer socketServer;

    @Override
    public void contextInitialized(ServletContextEvent sce) {

        log.info("准備啟動Socket服務...");
        log.info("SocketServer:{}", socketServer);
        socketServer.start();
    }

    @Override
    public void contextDestroyed(ServletContextEvent sce) {

    }
}

 

package com.geniuses.sewage_zero_straight.config;

import lombok.extern.slf4j.Slf4j;
import org.springframework.web.WebApplicationInitializer;
import org.springframework.web.context.WebApplicationContext;
import org.springframework.web.context.support.WebApplicationContextUtils;

import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.annotation.HandlesTypes;


@Slf4j
@HandlesTypes({WebApplicationInitializer.class})
public class SocketConfig implements WebApplicationInitializer {

    @Override
    public void onStartup(ServletContext servletContext) throws ServletException {
        WebApplicationContext webApplicationContext = WebApplicationContextUtils.getWebApplicationContext(servletContext);
        log.info("application: {}", webApplicationContext);
        //log.info("SocketServer: {}", webApplicationContext.getBean(SocketServer.class));
    }
}

關於WebApplicationInitializer理解,參見: https://blog.csdn.net/zq17865815296/article/details/79464403
以上是為測試Servlet環境中能否拿到IOC容器和能否注入Bean,發現並不能拿到對象
 
我就開始想啊,要怎么才能拿到IOC容器對象嘞,隨后發現SpringBootServletInitializer便是創建IOC容器的起始點

 

 

//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by Fernflower decompiler)
//

package org.springframework.boot.web.servlet.support;

import java.util.Collections;
import javax.servlet.ServletConfig;
import javax.servlet.ServletContext;
import javax.servlet.ServletContextEvent;
import javax.servlet.ServletException;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.builder.ParentContextApplicationContextInitializer;
import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.boot.context.event.ApplicationEnvironmentPreparedEvent;
import org.springframework.boot.web.servlet.context.AnnotationConfigServletWebServerApplicationContext;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextInitializer;
import org.springframework.context.ApplicationListener;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.core.env.ConfigurableEnvironment;
import org.springframework.util.Assert;
import org.springframework.web.WebApplicationInitializer;
import org.springframework.web.context.ConfigurableWebEnvironment;
import org.springframework.web.context.ContextLoaderListener;
import org.springframework.web.context.WebApplicationContext;

public abstract class SpringBootServletInitializer implements WebApplicationInitializer {
    protected Log logger;
    private boolean registerErrorPageFilter = true;

    public SpringBootServletInitializer() {
    }

    protected final void setRegisterErrorPageFilter(boolean registerErrorPageFilter) {
        this.registerErrorPageFilter = registerErrorPageFilter;
    }

    public void onStartup(ServletContext servletContext) throws ServletException {
        this.logger = LogFactory.getLog(this.getClass());
        //這里開始創建IOC容器
        WebApplicationContext rootAppContext = this.createRootApplicationContext(servletContext);
        if (rootAppContext != null) {
            servletContext.addListener(new ContextLoaderListener(rootAppContext) {
                public void contextInitialized(ServletContextEvent event) {
                }
            });
        } else {
            this.logger.debug("No ContextLoaderListener registered, as createRootApplicationContext() did not return an application context");
        }

    }

    //IOC容器創建方法
    protected WebApplicationContext createRootApplicationContext(ServletContext servletContext) {
        SpringApplicationBuilder builder = this.createSpringApplicationBuilder();
        builder.main(this.getClass());
        ApplicationContext parent = this.getExistingRootWebApplicationContext(servletContext);
        if (parent != null) {
            this.logger.info("Root context already created (using as parent).");
            servletContext.setAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE, (Object)null);
            builder.initializers(new ApplicationContextInitializer[]{new ParentContextApplicationContextInitializer(parent)});
        }

        builder.initializers(new ApplicationContextInitializer[]{new ServletContextApplicationContextInitializer(servletContext)});
        builder.contextClass(AnnotationConfigServletWebServerApplicationContext.class);
        builder = this.configure(builder);
        builder.listeners(new ApplicationListener[]{new SpringBootServletInitializer.WebEnvironmentPropertySourceInitializer(servletContext)});
        SpringApplication application = builder.build();
        if (application.getAllSources().isEmpty() && AnnotationUtils.findAnnotation(this.getClass(), Configuration.class) != null) {
            application.addPrimarySources(Collections.singleton(this.getClass()));
        }

        Assert.state(!application.getAllSources().isEmpty(), "No SpringApplication sources have been defined. Either override the configure method or add an @Configuration annotation");
        if (this.registerErrorPageFilter) {
            application.addPrimarySources(Collections.singleton(ErrorPageFilterConfiguration.class));
        }
        return this.run(application);
    }


    protected WebApplicationContext run(SpringApplication application) {
        return (WebApplicationContext)application.run(new String[0]);
    }



    }
}

 

這個時候我想到,如果不將SocketServer交由IOC進行管理,那么可以在實現的WebApplicationInitializer自定義類中啟動SocketServer

package com.geniuses.sewage_zero_straight.config;

import lombok.extern.slf4j.Slf4j;
import org.springframework.web.WebApplicationInitializer;
import org.springframework.web.context.WebApplicationContext;
import org.springframework.web.context.support.WebApplicationContextUtils;

import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.annotation.HandlesTypes;


/**
 *
 */
@Slf4j
@HandlesTypes({WebApplicationInitializer.class})
public class SocketConfig implements WebApplicationInitializer {

    @Override
    public void onStartup(ServletContext servletContext) throws ServletException {
       new Thread(new SocketRunnable(new SocketServer())).start();
    }
}

這里為什么要用一個新的線程來啟動Socket服務,因為SocketServer服務端的實現中,使用while循環進行等待客戶端socket進行連接,啟動時便開始等待(阻塞),如果不使用一個新的線程啟動,那么整個war應用服務啟動線程便會在此處阻塞。這樣雖然可以解決Socket服務隨着應用啟動而啟動,但是又出現了一個新的問題,那就是SocketServer無法使用@Autowired了,因為SocketServer沒有被IOC進行管理,無法進行依賴的注入。如果你不需要依賴,那么此法可行。

 

然后我又開始思考了,如果我需要進行依賴注入,由IOC進行SocketServer的管理,那么我要怎么樣才能獲取到WebApplicationContext對象,然后再通過該對象拿到SocketServer實例,然后再啟動Socket服務。如果我繼承了SpringBootServletInitializer,並重寫onStartup方法,再重寫的onStartup方法中進行Socket服務的啟動會怎么樣,最后發現Socket服務啟動了,但是進行了兩次onStartup方法的調用,第一次是自定義的SpringBootServletInitializer的實現類,第二次便是SpringBootServletInitializer類。即便只會啟動一次,這樣的代碼也具有着極強的侵入性,便放棄了。

@Override
public void onStartup(ServletContext servletContext) throws ServletException {
        this.logger = LogFactory.getLog(this.getClass());
        //這里開始創建IOC容器
        WebApplicationContext rootAppContext = this.createRootApplicationContext(servletContext);
    //在這里進行啟動Socket服務
    new Thread(new SocketRunnable(rootAppContext.getBean(SocketServer.class))).start();
        if (rootAppContext != null) {
            servletContext.addListener(new ContextLoaderListener(rootAppContext) {
                public void contextInitialized(ServletContextEvent event) {
                }
            });
        } else {
            this.logger.debug("No ContextLoaderListener registered, as createRootApplicationContext() did not return an application context");
        }

    }

 

然后我又開始思考,既然有ServletContextListener 監聽ServletContext對象的創建以及銷毀,那么是不是有監聽IOC容器的創建和銷毀嘞,隨后開始進行了百度
.................................................
是的,沒錯,這個真的有,詳情參見: https://blog.csdn.net/qq_40693828/article/details/81448934
感謝大佬
其實在這里想到了Javaweb整合spring的時候,好像有一個初始化IOC容器的監聽器來着,忘了,活該自己技術差......
在這里將SocketServer注入到SocketThread中,這里還有一個疑問,在這里使用@Autowired注入SocketThread不是很穩定,猜測可能與IOC的初始化時間前后有關系,不知道是為何
package com.geniuses.sewage_zero_straight.listener;

import com.geniuses.sewage_zero_straight.net.socket.SocketServer;
import com.geniuses.sewage_zero_straight.net.socket.SocketThread;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.web.context.ContextLoaderListener;
import org.springframework.web.context.WebApplicationContext;
import org.springframework.web.context.support.WebApplicationContextUtils;

import javax.servlet.ServletContext;
import javax.servlet.ServletContextEvent;
import javax.servlet.annotation.WebListener;

@Slf4j
@WebListener
public class MyContextLoaderListener extends ContextLoaderListener {

    @Override
    public void contextInitialized(ServletContextEvent event) {
        ServletContext servletContext = event.getServletContext();
        WebApplicationContext webApplicationContext = WebApplicationContextUtils.getWebApplicationContext(servletContext);
        log.info("webApplicationContext:{}",webApplicationContext);
        SocketThread socketThread = webApplicationContext.getBean(SocketThread.class);
        servletContext.setAttribute("SocketThread", socketThread);//在這里放進去之后......,好像也沒有必要放,可以從WebApplicationContext中獲取...
        log.info("socketThread:{}", socketThread);
        socketThread.start();
    }
}

 

在百度的過程中,突然看見一博客, https://www.jianshu.com/p/90063de8cf9b,感謝這位大神

在這里由SocketThread實現InitializingBean,實現afterPropertiesSet,這樣,在該類的依賴注入完畢之后,會自動調用afterPropertiesSet方法,這樣便可以在這里啟動socket服務。關於InitializingBean的介紹,參見上面的這個鏈接。

package com.geniuses.sewage_zero_straight.net.socket;

import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

@Slf4j
@Component
public class SocketThread extends Thread implements InitializingBean {

    @Autowired
    private SocketServer socketServer;

    @Override
    public void run() {
        log.info("當前線程名:{}", Thread.currentThread().getName());
        log.info("由當前線程開始啟動Socket服務...");
        socketServer.start();
    }

    @Override
    public void afterPropertiesSet() throws Exception {
        start();
    }
}

 

至此,我是不想再想了。
在這里感謝下面的幾篇博客:
自啟動監聽類(InitializingBean)介紹: https://www.jianshu.com/p/90063de8cf9b
springboot中啟動流程說明: https://www.jb51.net/article/142981.htm
SpringBoot中WebApplicationInitializer的理解: https://blog.csdn.net/zq17865815296/article/details/79464403
Spring中ContextLoaderListener作用: https://blog.csdn.net/qq_40693828/article/details/81448934
 
最后說一下我的開發環境:win7_64旗艦版、jdk1.8、idea、tomcat8.5


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM