1. 程式人生 > >曹工雜談:Spring boot應用,自己動手用Netty替換底層Tomcat容器

曹工雜談:Spring boot應用,自己動手用Netty替換底層Tomcat容器

前言

問:標題說的什麼意思?

答:簡單說,一個spring boot應用(我這裡,版本升到2.1.7.Release了,沒什麼問題),預設使用了tomcat作為底層容器來接收和處理連線。 我這裡,在依賴中排除了tomcat,使用Netty作為了替代品。優勢在於,啟動飛快,執行緒數量完全可控(多少個netty的boss、worker執行緒,多少個業務執行緒),如果能優化得好,效率會很高(我這個還有很多優化空間,見文末總結)

流程圖如下(中間的三個handler是自定義的):

這個東西,年初我就弄出來了,然後用在了某個我負責的微服務裡,之前一直想寫,但是一直沒把demo程式碼從微服務裡抽出來,然後就一直拖著。前一陣吧,把程式碼抽出來了,然後又覺得要優化下,不然有些低階問題怎麼辦?

前一陣抽了程式碼出來,然後想著優化下,結果忙起來搞忘了,而且優化無底洞啊,所以先不優化了,略微補了些註釋,就發上來了,希望大家看到後,多多批評指正。

先附上程式碼地址:https://gitee.com/ckl111/Netty_Spring_MVC_Sample/

啟動後,訪問:http://localhost:8081/test.do即可。

實現大體思路

  1. 排除掉tomcat依賴
  2. 解決掉報錯,保證spring mvc的上下文正常啟動
  3. 啟動netty容器,最後一個handler負責將servlet request交給dispatcherServlet處理

具體實現

解決dispatcherServlet不能正常工作的問題

問題1:缺少servletContext報錯

經過追蹤發現,這個servletContext來源於:org.springframework.web.context.support.GenericWebApplicationContext中的servletContext欄位

解決辦法:

META-INF/spring.factories中,定義了一個listener,來參與spring boot啟動時的生命週期:

org.springframework.boot.SpringApplicationRunListener=com.ceiec.router.config.MyListener

在我的自定義listener中,實現org.springframework.boot.SpringApplicationRunListener

,然後重寫如下方法:

package com.ceiec.router.config;

import com.ceiec.router.config.servletconfig.MyServletContext;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.SpringApplicationRunListener;
import org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.core.env.ConfigurableEnvironment;
import org.springframework.core.env.MapPropertySource;
import org.springframework.core.env.MutablePropertySources;
import org.springframework.core.env.PropertySource;

import javax.servlet.ServletContext;
import java.util.Map;

@Data
@Slf4j
public class MyListener implements SpringApplicationRunListener {


    public MyListener(SpringApplication application, String[] args) {
        super();
    }
    ...
    
    @Override
    public void contextPrepared(ConfigurableApplicationContext context) {
        // 這裡手動new一個servletContext,然後設定給spring上下文
        ServletContext servletContext = new MyServletContext();
        ServletWebServerApplicationContext applicationContext = (ServletWebServerApplicationContext) context;
        applicationContext.setServletContext(servletContext);
    }
  
  ...

}

自定義實現了com.ceiec.router.config.servletconfig.MyServletContext,這個很簡單,繼承spring test包中的org.springframework.mock.web.MockServletContext即可。

package com.ceiec.router.config.servletconfig;

import org.springframework.mock.web.MockServletContext;

import javax.servlet.Filter;
import javax.servlet.FilterRegistration;
import javax.servlet.Servlet;
import javax.servlet.ServletRegistration;

public class MyServletContext extends MockServletContext{

    @Override
    public ServletRegistration.Dynamic addServlet(String servletName, Servlet servlet) {
        return null;
    }

    @Override
    public FilterRegistration.Dynamic addFilter(String filterName, Filter filter){
        return null;
    }
}

問題2:

暫時沒有。之前的版本本來有一個問題,升到spring boot 2.1.7後,好像不需要了,先不管。

問題3:

怎麼保證少了tomcat後,dispatcherServlet還能用?準確地說,dispatcherServlet這個東西和tomcat是兩回事,以前寫struts 2的時候,也沒dispatcherServlet這個類,不是嗎?

所以,在spring boot啟動時,並不強依賴底層容器,dispatcherServlet 這個bean會自動裝配,裝配程式碼在

org.springframework.boot.autoconfigure.web.servlet.DispatcherServletAutoConfiguration.DispatcherServletConfiguration

    @Configuration
    @Conditional(DefaultDispatcherServletCondition.class)
    @ConditionalOnClass(ServletRegistration.class)
    @EnableConfigurationProperties({ HttpProperties.class, WebMvcProperties.class })
    protected static class DispatcherServletConfiguration {

        private final HttpProperties httpProperties;

        private final WebMvcProperties webMvcProperties;

        //這裡自動裝配DispatcherServlet
        @Bean(name = DEFAULT_DISPATCHER_SERVLET_BEAN_NAME)
        public DispatcherServlet dispatcherServlet() {
            DispatcherServlet dispatcherServlet = new DispatcherServlet();
            dispatcherServlet.setDispatchOptionsRequest(
                                this.webMvcProperties.isDispatchOptionsRequest());
            dispatcherServlet.setDispatchTraceRequest(
              this.webMvcProperties.isDispatchTraceRequest());
            return dispatcherServlet;
        }

問題4:

自動裝配DispatcherServlet後,處理請求時報錯:

解決方式是,啟動完成後,給dispatcherServlet設定這個field的值,同時,初始化我們的servlet(這裡提一句,還記得servlet的生命週期嗎,就是那個東西):

import org.springframework.mock.web.MockServletConfig;
/**
 * 從spring上下文獲取 DispatcherServlet,設定其欄位config為mockServletConfig
 */
DispatcherServlet dispatcherServlet = applicationContext.getBean(DispatcherServlet.class);
MockServletConfig myServletConfig = new MockServletConfig();
MyReflectionUtils.setFieldValue(dispatcherServlet,"config",myServletConfig);

/**
 * 初始化servlet
 */
try {
  dispatcherServlet.init();
} catch (ServletException e) {
  log.error("e:{}",e);
}

netty處理過程

大致流程

這裡,我們再將總共流程圖貼一下:

中間的三個handler,是我們自定義的。每個handler具體做的事情,寫得比較清楚了。具體看下面的com.ceiec.router.netty.DispatcherServletChannelInitializer:

    public class DispatcherServletChannelInitializer extends ChannelInitializer<SocketChannel> {

    //可以使用單獨的執行緒池,來處理業務請求
    private static DefaultEventLoopGroup eventExecutors = new DefaultEventLoopGroup(4,new NamedThreadFactory("business_servlet"));

    @Override
    public void initChannel(SocketChannel channel) throws Exception {
        ChannelPipeline pipeline = channel.pipeline();

        // 對通訊資料進行編解碼
        pipeline.addLast(new HttpServerCodec());

        // 把多個HTTP請求中的資料組裝成一個
        pipeline.addLast(new HttpObjectAggregator(65536));

        // 用於處理大的資料流
        pipeline.addLast(new ChunkedWriteHandler());

        /**
         * 生成servlet使用的request
         */
        pipeline.addLast("GenerateServletRequestHandler", new GenerateServletRequestHandler());

        /**
         * 過濾器處理器,模擬servlet中的 filter 鏈
         */
        FilterNettyHandler filterNettyHandler = SpringContextUtils.getApplicationContext().getBean(FilterNettyHandler.class);
        pipeline.addLast("FilterNettyHandler", filterNettyHandler);

        /**
         * 真正的業務handler,轉交給:spring mvc的dispatcherServlet 處理
         */
        DispatcherServletHandler dispatcherServletHandler = SpringContextUtils.getApplicationContext().getBean(DispatcherServletHandler.class);
        //pipeline.addLast("dispatcherServletHandler", dispatcherServletHandler);
        // 使用下面的過載方法,第一個引數為執行緒池,則這裡會非同步執行我們的業務邏輯,正常也應該這樣,避免長時間阻塞io執行緒
        pipeline.addLast(eventExecutors,"handler", new ServletNettyHandler(dispatcherServlet));
    }


}

原始netty的http請求,轉成servlet http請求

其中,GenerateServletRequestHandler完成這部分工作,傳遞給下一個handler的,就是MockHttpServletRequest型別:

 @Override
    protected void channelRead0(ChannelHandlerContext channelHandlerContext, FullHttpRequest fullHttpRequest) throws Exception {
        if (!fullHttpRequest.decoderResult().isSuccess()) {
            sendError(channelHandlerContext, BAD_REQUEST);
            return;
        }

        // 設定請求的會話id
        String token = UUID.randomUUID().toString().replace("-", "");
        MDC.put(SESSION_KEY, token);

        String remoteIP = getRemoteIP(fullHttpRequest, channelHandlerContext);
        MockHttpServletRequest servletRequest = createServletRequest(fullHttpRequest);
        String s = fullHttpRequest.content().toString(CharsetUtil.UTF_8);

        log.info("{},request:{},param:{}", remoteIP, fullHttpRequest.uri(), s);
        try {
            channelHandlerContext.fireChannelRead(servletRequest);
        } finally {
            // 刪除SessionId
            MDC.remove(SESSION_KEY);
        }

    }

模擬servlet filter chain對請求進行處理

這裡說下,為什麼要使用spring來管理它,且型別為prototype,因為:每次請求進來,都會去呼叫

com.ceiec.router.netty.DispatcherServletChannelInitializer#initChannel,在那裡面是如下的從spring上下文獲取的方式來拿到FilterNettyHandler的。

@Override
    public void initChannel(SocketChannel channel) throws Exception {
        ChannelPipeline pipeline = channel.pipeline();
        ... 
        /**
         * 過濾器處理器,模擬servlet中的 filter 鏈
         */
        FilterNettyHandler filterNettyHandler = SpringContextUtils.getApplicationContext().getBean(FilterNettyHandler.class);
        pipeline.addLast("FilterNettyHandler", filterNettyHandler);
    }
package com.ceiec.router.netty.handler;

import com.ceiec.router.netty.DispatcherServletChannelInitializer;
import com.ceiec.router.netty.filter.ApplicationFilterChain;
import com.ceiec.router.netty.filter.ApplicationFilterFactory;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Scope;
import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.mock.web.MockHttpServletResponse;
import org.springframework.stereotype.Component;

/**
 * desc: 模擬servlet的filter鏈
 * netty handler鏈的初始化在{@link DispatcherServletChannelInitializer#initChannel(io.netty.channel.socket.SocketChannel)}
 * @author: ckl
 * creat_date: 2019/12/10 0010
 * creat_time: 10:14
 **/
@Slf4j
@Component
@Scope(scopeName = "prototype")
public class FilterNettyHandler extends SimpleChannelInboundHandler<MockHttpServletRequest> {


    @Override
    protected void channelRead0(ChannelHandlerContext ctx, MockHttpServletRequest httpServletRequest) throws Exception {
        MockHttpServletResponse httpServletResponse = new MockHttpServletResponse();
        ApplicationFilterChain filterChain = ApplicationFilterFactory.createFilterChain(ctx,httpServletRequest);
        if (filterChain == null) {
            return;
        }

        filterChain.doFilter(httpServletRequest, httpServletResponse);
    }
}

handler最後一棒:將請求交給dispatcherServlet處理

package com.ceiec.router.netty.handler;

import com.ceiec.router.netty.DispatcherServletChannelInitializer;
import com.ceiec.router.netty.filter.RequestResponseWrapper;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelFutureListener;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.handler.codec.http.DefaultHttpResponse;
import io.netty.handler.codec.http.HttpResponse;
import io.netty.handler.codec.http.HttpResponseStatus;
import io.netty.handler.stream.ChunkedStream;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Scope;
import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.mock.web.MockHttpServletResponse;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.DispatcherServlet;

import java.io.ByteArrayInputStream;
import java.io.InputStream;

import static io.netty.handler.codec.http.HttpVersion.HTTP_1_1;

/**
 *
 * desc:
 * 請求交給,Spring的dispatcherServlet處理
 * netty handler鏈的初始化在{@link DispatcherServletChannelInitializer#initChannel(io.netty.channel.socket.SocketChannel)}
 * @author: caokunliang
 * creat_date: 2019/8/21 0021
 * creat_time: 15:46
 **/
@Slf4j
@Component
@Scope(scopeName = "prototype")
public class DispatcherServletHandler extends SimpleChannelInboundHandler<RequestResponseWrapper> {

    @Autowired
    private DispatcherServlet dispatcherServlet;


    @Override
    protected void channelRead0(ChannelHandlerContext channelHandlerContext, RequestResponseWrapper requestResponseWrapper) throws Exception {
        MockHttpServletRequest servletRequest = (MockHttpServletRequest) requestResponseWrapper.getServletRequest();
        MockHttpServletResponse servletResponse = (MockHttpServletResponse) requestResponseWrapper.getServletResponse();
        //這裡呼叫dispatcherServlet的service,最終會呼叫controller的方法,響應流會寫入到servletResponse中
        dispatcherServlet.service(servletRequest, servletResponse);

        HttpResponseStatus status = HttpResponseStatus.valueOf(servletResponse.getStatus());
        HttpResponse response = new DefaultHttpResponse(HTTP_1_1, status);

        for (String name : servletResponse.getHeaderNames()) {
            response.headers().add(name, servletResponse.getHeader(name));
        }

        response.headers().add("Content-Type","application/json;charset=UTF-8");

        // Write the initial line and the header.
        channelHandlerContext.write(response);

        InputStream contentStream = new ByteArrayInputStream(servletResponse.getContentAsByteArray());

        ChunkedStream stream = new ChunkedStream(contentStream);
        ChannelFuture writeFuture = channelHandlerContext.writeAndFlush(stream);
        writeFuture.addListener(ChannelFutureListener.CLOSE);
    }


}

總結

大概就上面這些東西了,整體來說,有很多需要優化的東西。但我本身對netty的使用,只能算相對勉強,很多細節性的東西沒考慮。

比如:

  1. 我這裡,是很粗暴地每次請求後,關閉了連線;
  2. 請求id在從worker執行緒,傳給dispatcherServlet的業務執行緒時,丟失了(主要是直接使用了netty的api,來生成執行緒池,難以控制);
  3. 我使用了這個技術的微服務,qps不算高,高了之後,會不會有大問題,暫時未知,需要進一步測試,但最近也忙,時間有限。
  4. channel的handler這裡,現在用的prototype的bean,如果換成單例bean,在高併發下會不會有問題呢,待驗證。

雖然問題很多,但是我覺得很難等到我全部完善了再分享,因為我個人能力有限(netty功力不行,哈哈)。我能做的是,先分享,拋磚引玉,後續有時間了我也會慢慢優化。
程式碼地址:https://gitee.com/ckl111/Netty_Spring_MVC_Sample