曹工雜談: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即可。
實現大體思路
- 排除掉
tomcat
依賴 - 解決掉報錯,保證
spring mvc
的上下文正常啟動 - 啟動
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的使用,只能算相對勉強,很多細節性的東西沒考慮。
比如:
- 我這裡,是很粗暴地每次請求後,關閉了連線;
- 請求id在從worker執行緒,傳給dispatcherServlet的業務執行緒時,丟失了(主要是直接使用了netty的api,來生成執行緒池,難以控制);
- 我使用了這個技術的微服務,qps不算高,高了之後,會不會有大問題,暫時未知,需要進一步測試,但最近也忙,時間有限。
- channel的handler這裡,現在用的prototype的bean,如果換成單例bean,在高併發下會不會有問題呢,待驗證。
雖然問題很多,但是我覺得很難等到我全部完善了再分享,因為我個人能力有限(netty
功力不行,哈哈)。我能做的是,先分享,拋磚引玉,後續有時間了我也會慢慢優化。
程式碼地址:https://gitee.com/ckl111/Netty_Spring_MVC_Sample