手写一个迷你Tomcat——三步理解Servlet容器的核心原理
造过轮子的人学框架有多快?我自己写完IOC和AOP,Spring就是换个API。同样的道理,手写一个迷你Tomcat,Tomcat的源码你就看得懂了。
背景
我有一段时间想深入理解Tomcat的原理,看源码一头雾水——Connector、Container、Wrapper、Lifecycle、Pipeline、Valve,概念满天飞。
后来换了个思路:不看了,自己写一个。从最简单的HTTP服务器开始,一步步加功能,每一步都对照Tomcat的设计。
写完之后回头看Tomcat源码,发现它干的也就是这些事——只是做得更完整、更复杂。核心原理,就这么几个。
最终成果
一个能跑的迷你Servlet容器,三步演进:
第一步:HttpServer —— 能返回静态HTML 第二步:HttpServer1 —— 能动态加载并执行Servlet 第三步:HttpServer2 —— 加Facade门面模式,安全隔离完整代码12个类,总计不到600行。Tomcat的核心思想,全在里面了。
第一步:最简HTTP服务器
目标:浏览器发请求,服务器返回静态HTML文件。
publicclassHttpServer{publicstaticfinalStringWEB_ROOT=System.getProperty("user.dir")+File.separator+"webroot";publicvoidawait(){ServerSocketserverSocket=newServerSocket(80);while(!shutdown){Socketsocket=serverSocket.accept();InputStreaminput=socket.getInputStream();OutputStreamoutput=socket.getOutputStream();Requestrequest=newRequest(input);request.parse();// 解析HTTP请求Responseresponse=newResponse(output);response.setRequest(request);response.sendStaticResource();// 返回静态文件socket.close();}}}核心流程就四步:accept连接 → 解析请求 → 找文件 → 写回响应。
Request的parse方法做的事很暴力——把输入流读成字符串,从中抠出URI:
publicvoidparse(){byte[]buffer=newbyte[2048];inti=input.read(buffer);StringBufferrequest=newStringBuffer(2048);for(intj=0;j<i;j++){request.append((char)buffer[j]);}uri=parseUri(request.toString());}privateStringparseUri(StringrequestString){intindex1=requestString.indexOf(' ');intindex2=requestString.indexOf(' ',index1+1);returnrequestString.substring(index1+1,index2);}HTTP请求长这样:
GET /index.html HTTP/1.1 Host: localhost两个空格之间就是URI:/index.html。
Response把文件读出来写回Socket:
publicvoidsendStaticResource()throwsIOException{Filefile=newFile(Constants.WEB_ROOT,request.getUri());FileInputStreamfis=newFileInputStream(file);byte[]bytes=newbyte[1024];intch=fis.read(bytes,0,1024);while(ch!=-1){output.write(bytes,0,ch);ch=fis.read(bytes,0,1024);}}到这里,你已经理解了Tomcat的Connector + 静态资源处理器。Tomcat的DefaultServlet干的也是这个活——根据URI找文件,读出来写回去。
第二步:动态加载Servlet
目标:URL以/servlet/开头时,不是返回文件,而是动态加载并执行一个Servlet类。
这是最关键的一步。Tomcat之所以是"容器",就是因为它能动态加载和执行Servlet。
publicclassHttpServer1{publicvoidawait(){// ... accept连接,解析请求 ...if(request.getUri().startsWith("/servlet/")){ServletProcessor1processor=newServletProcessor1();processor.process(request,response);// 动态加载Servlet}else{StaticResourceProcessorprocessor=newStaticResourceProcessor();processor.process(request,response);// 返回静态文件}}}路由规则很简单:/servlet/xxx走Servlet处理器,其他走静态资源。
ServletProcessor1的核心——动态加载类并执行:
publicclassServletProcessor1{publicvoidprocess(Requestrequest,Responseresponse){Stringuri=request.getUri();StringservletName=uri.substring(uri.lastIndexOf("/")+1);// 1. 创建URLClassLoader,指向webroot目录URL[]urls=newURL[1];FileclassPath=newFile(Constants.WEB_ROOT);Stringrepository=newURL("file",null,classPath.getCanonicalPath()+File.separator).toString();urls[0]=newURL(null,repository,null);URLClassLoaderloader=newURLClassLoader(urls);// 2. 动态加载Servlet类ClassmyClass=loader.loadClass(servletName);// 3. 实例化并执行service方法Servletservlet=(Servlet)myClass.newInstance();servlet.service((ServletRequest)request,(ServletResponse)response);}}这三步就是Tomcat加载Servlet的精髓:
| 步骤 | 我们的代码 | Tomcat的实现 |
|---|---|---|
| 创建ClassLoader | URLClassLoader指向webroot | WebappClassLoader指向WEB-INF/classes和lib |
| 动态加载类 | loader.loadClass(servletName) | 同样,但加了缓存和热加载 |
| 实例化并执行 | newInstance() + service() | 用反射实例化,调用service() |
到这里,你已经理解了Tomcat的Servlet容器核心——Wrapper。Tomcat的StandardWrapper就是干这个的:加载Servlet类、实例化、调用service()。
第三步:Facade门面模式——安全隔离
目标:防止Servlet直接访问Request和Response的内部方法。
第二步有一个安全问题:ServletProcessor1把Request对象直接强转成ServletRequest传给了Servlet:
servlet.service((ServletRequest)request,(ServletResponse)response);问题在哪?Request类实现了ServletRequest接口,但它还有一个parse()方法。Servlet可以通过强转回去调用parse():
// Servlet里写这样的代码:if(requestinstanceofRequest){((Request)request).parse();// 灾难!重新解析了HTTP请求}这就是安全隐患——Servlet不应该能调用容器内部的方法。
Tomcat的解决方案:Facade门面模式。
publicclassServletProcessor2{publicvoidprocess(Requestrequest,Responseresponse){// ... 加载Servlet类 ...RequestFacaderequestFacade=newRequestFacade(request);ResponseFacaderesponseFacade=newResponseFacade(response);servlet.service((ServletRequest)requestFacade,(ServletResponse)responseFacade);}}RequestFacade的实现:
publicclassRequestFacadeimplementsServletRequest{privateServletRequestrequest=null;publicRequestFacade(Requestrequest){this.request=request;}publicObjectgetAttribute(Stringattribute){returnrequest.getAttribute(attribute);// 委托给真正的Request}publicStringgetParameter(Stringname){returnrequest.getParameter(name);}// 只有ServletRequest接口定义的方法,没有parse()}关键区别:
| 传给Servlet的对象 | Servlet能调用的方法 |
|---|---|
| Request(第二步) | parse()、getUri()、所有ServletRequest方法 + 内部方法 |
| RequestFacade(第三步) | 只有ServletRequest接口定义的方法 |
Servlet想强转?转不了——RequestFacade没有parse()方法。就算拿到RequestFacade实例,也调不到内部的Request。
这就是Tomcat安全设计的核心思想。Tomcat的org.apache.catalina.connector.RequestFacade和org.apache.catalina.connector.ResponseFacade就是这么干的,和我们的代码结构一模一样。
对照Tomcat
手写完这三步,再回头看Tomcat的架构:
Tomcat完整架构: Connector(连接器) ├── Http11NioProtocol(处理HTTP请求) ├── 解析请求 → org.apache.coyote.Request └── 交给Container处理 Container(容器) ├── Engine(全局引擎) │ ├── Host(虚拟主机,如localhost) │ │ ├── Context(Web应用,如/myapp) │ │ │ ├── Wrapper(Servlet实例) │ │ │ │ ├── 动态加载Servlet类 │ │ │ │ ├── 实例化并调用service() │ │ │ │ └── Facade门面模式隔离对应关系:
| 我们的代码 | Tomcat对应 |
|---|---|
| HttpServer.await() | Connector(接收HTTP连接) |
| Request.parse() | Coyote的HTTP解析器 |
| StaticResourceProcessor | DefaultServlet |
| ServletProcessor2 | StandardWrapper |
| URLClassLoader | WebappClassLoader |
| RequestFacade/ResponseFacade | RequestFacade/ResponseFacade |
/servlet/路由 | Context + Wrapper的URL映射 |
Tomcat多出来的东西(线程池、Pipeline-Valve、Lifecycle、Session管理、JNDI),都是在这些核心概念上的扩展。核心骨架我们已经写出来了。
这个练习教会我什么
1. Servlet容器本质上就是三件事
- 接收HTTP请求(ServerSocket + 解析)
- 路由到处理器(静态文件 or Servlet)
- 安全隔离(Facade门面模式)
Tomcat百万行代码,最终干的也是这三件事。
2. 动态类加载是容器的灵魂
URLClassLoader.loadClass()这一行代码,就是Tomcat能"热部署"的底层原因——不需要重启JVM,用新的ClassLoader加载新的class文件就行。
Spring的IoC容器、OSGi的模块化、Java的SPI机制,底层都是这个:动态加载类,用接口隔离实现。
3. 门面模式不是设计模式的考试题
很多人学门面模式就觉得是"简化接口",但在Servlet容器里,门面模式的目的是安全——防止外部代码访问内部实现。
Tomcat的Facade不是简化,是防御。这个认识不手写一遍代码,看设计模式的书是体会不到的。
和IOC/AOP的呼应
之前写过一篇《造过轮子的人学框架有多快》——自己写完IOC和AOP,Spring就是换个API。
这次又验证了一次:手写完迷你Tomcat,Tomcat源码你就看得懂了。
学框架最好的方式不是读文档、不是看视频,是自己造一个简化版的轮子。造完之后你会发现,那些"高大上"的框架,底层原理你早就在造轮子的时候弄明白了。
区别只在于:框架做得更完整、更健壮、处理了更多边界情况。但核心思想,你的轮子里已经有了。
代码结构
HttpServer/ ├── src/com/my/ │ ├── HttpServer.java # 第一步:静态HTTP服务器 │ ├── HttpServer1.java # 第二步:+ Servlet动态加载 │ ├── HttpServer2.java # 第三步:+ Facade门面模式 │ ├── Request.java # HTTP请求解析 │ ├── Response.java # HTTP响应输出 │ ├── RequestFacade.java # Request的门面 │ ├── ResponseFacade.java # Response的门面 │ ├── ServletProcessor1.java # Servlet处理器(无Facade) │ ├── ServletProcessor2.java # Servlet处理器(有Facade) │ ├── StaticResourceProcessor.java # 静态资源处理器 │ └── Constants.java # 常量定义 └── webroot/ # 静态文件和Servlet类总结
三步手写迷你Tomcat:
- HttpServer:ServerSocket + HTTP解析 + 返回静态文件——这就是Connector
- HttpServer1:URLClassLoader动态加载Servlet + service()调用——这就是Wrapper
- HttpServer2:RequestFacade/ResponseFacade门面模式——这就是安全隔离
写完这三步,Tomcat的Connector、Container、Wrapper、Facade,不再是概念,而是你亲手写过的代码。
造轮子的意义不是替代框架,是理解框架。理解了,用起来才心里有底。
感谢豆包、智谱、OpenCode在写作过程中的辅助。