曹工杂谈: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
曹工杂谈:Spring boot应用,自己动手用Netty替换底层Tomcat容器的更多相关文章
- 曹工说Spring Boot源码(9)-- Spring解析xml文件,到底从中得到了什么(context命名空间上)
写在前面的话 相关背景及资源: 曹工说Spring Boot源码(1)-- Bean Definition到底是什么,附spring思维导图分享 曹工说Spring Boot源码(2)-- Bean ...
- 曹工说Spring Boot源码(20)-- 码网灰灰,疏而不漏,如何记录Spring RedisTemplate每次操作日志
写在前面的话 相关背景及资源: 曹工说Spring Boot源码(1)-- Bean Definition到底是什么,附spring思维导图分享 曹工说Spring Boot源码(2)-- Bean ...
- 曹工说Spring Boot源码系列开讲了(1)-- Bean Definition到底是什么,附spring思维导图分享
写在前面的话&&About me 网上写spring的文章多如牛毛,为什么还要写呢,因为,很简单,那是人家写的:网上都鼓励你不要造轮子,为什么你还要造呢,因为,那不是你造的. 我不是要 ...
- 曹工说Spring Boot源码(2)-- Bean Definition到底是什么,咱们对着接口,逐个方法讲解
写在前面的话 相关背景及资源: 曹工说Spring Boot源码系列开讲了(1)-- Bean Definition到底是什么,附spring思维导图分享 工程代码地址 思维导图地址 工程结构图: 正 ...
- 曹工说Spring Boot源码(3)-- 手动注册Bean Definition不比游戏好玩吗,我们来试一下
写在前面的话 相关背景及资源: 曹工说Spring Boot源码系列开讲了(1)-- Bean Definition到底是什么,附spring思维导图分享 工程代码地址 思维导图地址 工程结构图: 大 ...
- 曹工说Spring Boot源码(4)-- 我是怎么自定义ApplicationContext,从json文件读取bean definition的?
写在前面的话 相关背景及资源: 曹工说Spring Boot源码系列开讲了(1)-- Bean Definition到底是什么,附spring思维导图分享 工程代码地址 思维导图地址 工程结构图: 大 ...
- 曹工说Spring Boot源码(5)-- 怎么从properties文件读取bean
写在前面的话 相关背景及资源: 曹工说Spring Boot源码(1)-- Bean Definition到底是什么,附spring思维导图分享 曹工说Spring Boot源码(2)-- Bean ...
- 曹工说Spring Boot源码(6)-- Spring怎么从xml文件里解析bean的
写在前面的话 相关背景及资源: 曹工说Spring Boot源码(1)-- Bean Definition到底是什么,附spring思维导图分享 曹工说Spring Boot源码(2)-- Bean ...
- 曹工说Spring Boot源码(7)-- Spring解析xml文件,到底从中得到了什么(上)
写在前面的话 相关背景及资源: 曹工说Spring Boot源码(1)-- Bean Definition到底是什么,附spring思维导图分享 曹工说Spring Boot源码(2)-- Bean ...
随机推荐
- python基础-函数作用域
函数 函数对象 函数是第一类对象 函数名可以被引用 函数名可以当作参数使用 函数名可以当作返回值使用 函数名可以当作容器类型的元素 函数嵌套 嵌套调用:在函数内部中调用函数 嵌套定义:在函数内部中定义 ...
- 命运Ⅰ&命运Ⅱ
upd:为啥下面的相关博文都是各种退役记(这TM怎么就相关了) 竟然被卡线了,16名,我这几次考试也是炸到了一定境界了... 前三次模拟总榜rk1,第一次分机房rk4,第二次分机房rk11,第三次分机 ...
- python学习之【第四篇】:Python中的列表及其所具有的方法
1.前言 列表是Python中最常用的数据类型之一,是以[ ]括起来,每个元素以逗号隔开,而且里面可以存放各种数据类型,而且列表是有序的,有索引值,可切片,方便取值. 2.创建列表 li = ['he ...
- Hibernate一对多、多对一的关系表达
一.关系表达: 1.一对多.多对一表的关系: 学生表: 班级表: 在学生表中,学生的学号是主键.在班级表中,班级号是主键,因此,学生表的外键是classno.因此,班级对应学生是一对多,学生对应班级是 ...
- c#数据结构之Array、ArrayList、List、LinkedList对比分析
一.前言: 在c#数据结构中,集合的应用非常广泛,无论是做BS架构还是CS架构开发,都离不开集合的使用,比如我们常见的集合包括:Array.ArrayList.List.LinkedList等.这一些 ...
- WordPress代码高亮插件SyntaxHighlighter终极使用详解
子曰: 工欲善其事,必先利其器.作为码农一枚,再加上站长这个已经不再光鲜的称呼,岂能没有一款经济实用.操作简单.而且功能必须强大.样式也必须好看的Wordpress代码高亮插件?!作为一个视代码如生命 ...
- 深入理解计算机系统 第三章 程序的机器级表示 Part1 第二遍
第一遍对应笔记链接 https://www.cnblogs.com/stone94/p/9905345.html 机器级代码 计算机系统使用了多种不同形式的抽象,利用更简单的抽象模型来隐藏实现的细节. ...
- Unity加载AB包
Unity制作游戏AB包 需要注意的是在游戏场景运行的情况下,不能编译AB包,不运行的情况下编译AB包需要使用Unity的扩展菜单功能,首先需要建立菜单用来编译AB包. 1.建立AB包的名字,首先选中 ...
- Java自动生成数据
最近在造数据库中的表数据,写了些数据生成类 可以随机生成姓名.性别,民族,出生日期,身份证号,手机号,邮箱,身高,文化程度,地址,单位,日期时间,编码等 package com.util.create ...
- # & 等特殊字符会导致传参失败
# & 等特殊字符会导致 post 传参失败 处理方法使用 encodeURIComponent 将字符串转化一下 实例 // toUpperCase() 转化为大写字母 var cateco ...