曹工雜談: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


免責聲明!

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



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