JavaEE 环境与程序设计 · 交互课程

贯穿示例 · 手写 HTTP 服务器(V1–V7)

从一个裸 Socket 逐步长出手写 Servlet、Filter、Listener,再到 Spring MVC 与 ORM 的框架仿真

贯穿示例 · 手写 HTTP 服务器(V1–V7)

贯穿式教学示例(一):把「Web 容器到底干了什么」这件事,用一个代码库的七次演进讲透。素材在本仓库同级的 HttpWebServerV1…HttpWebServerV7 七个工程里,下面每段代码都摘自对应版本的真实源码(有删节)。

这条线的意义:课程模块 1–3(WEB 基础 / Servlet、Filter、Listener / JSP)讲的是规范与概念,本示例则从零开始手写一遍——先没有 Tomcat,只有 java.net,看一个 HTTP 服务器是怎么一步步「长」出容器能力的。

一、总览:一条从裸 Socket 到框架仿真的演进链

版本新增能力对应课程模块
V1裸 ServerSocket,手写解析 HTTP 请求行与头,返回静态文件模块 1 · WEB 基础
V2结构化 Request/Response 对象 + Application 上下文 + Session/SessionManager模块 1 · WEB 基础
V3Servlet 抽象:GenericServlet/HttpServlet,URL→Servlet 映射与生命周期模块 2 · Servlet
V4Filter 链:LoggingFilter 包裹 Servlet,做请求前后处理模块 2 · Filter
V5Listener + Session 事件:SessionCounterListener 响应会话创建/销毁模块 2 · Listener
V6Spring MVC 仿真:前端控制器 MvcDispatcher + @Controller/@RequestMapping模块 5 · SpringMVC
V7ORM 层:MapperProxy 动态代理读注解生成 SQL(MyBatis 式)模块 8 · MyBatis

交互实验:手写 HTTP 服务器 V1–V7 演进(HttpServerTimeline)

从一个最简 socket 程序一步步长成 Tomcat:每升一版就补上一种 Web 能力。点时间线上的节点,看该版本新增了什么、代表类是什么——这正是模块1–3(Servlet / Filter / Listener)的活教材。

V1最简 socket 服务器V2V3V4V5V6V7

V1 · 最简 socket 服务器

新增能力:ServerSocket 监听 + 收请求/回响应,支持 request/response 内置对象

代表类:SimpleWebServerRequestResponse

第一个能跑的网络程序:绑定端口、读请求行、回一段 HTML。

到 V1 为止,已叠加 1 / 7 项核心能力——V7 凑齐 SSM 三件套雏形。

二、V1:没有容器,只有 Socket

V1 的 SimpleWebServer 是整条链的地基——一行规范都没有,全靠手写。它开一个 ServerSocket,循环 accept() 拿到连接,自己从输入流里读请求行(GET /path HTTP/1.1)和头,再决定返回哪个静态文件:

// HttpWebServerV1/src/SimpleWebServer.java(节选)
ServerSocket serverSocket = new ServerSocket(8080);
while (true) {
    Socket socket = serverSocket.accept();   // 阻塞等待一个连接
    // 读请求行 → 拆出 method / path / protocol
    // 读头直到空行 → 拼响应(状态行 + 头 + body)
}

要点:「HTTP 服务器」的本质就是「接受 TCP 连接、按文本协议收发字节」。V1 让你看清,所谓 Web 容器,最初不过是对这个循环的一层又一层封装。

三、V2:把请求/响应/会话对象化

V1 里请求头是一堆散落的字符串。V2 引入结构化模型:

// HttpWebServerV2/src/Application.java(节选)
public class Application {
    private Map<String, Object> attributes;   // ConcurrentHashMap
    private Map<String, String> initParameters;
    public Application() {
        this.attributes = new ConcurrentHashMap<>();
        this.serverInfo = "SimpleWebServer/1.0";
    }
}

四、V3:Servlet 抽象与生命周期

到了 V3,「按 URL 分发到不同处理逻辑」被抽象成 Servlet。GenericServlet 提供 init()/service()/destroy() 三段式生命周期,HttpServlet 再按方法拆出 doGet/doPost:

// HttpWebServerV3/src/GenericServlet.java(节选)
public abstract class GenericServlet implements Servlet, ServletConfig {
    public abstract void init();                    // 由子类覆写
    public final void init(ServletConfig config) {
        this.config = config;
        this.initialized = true;
        init();                                     // 容器注册后只调用一次
    }
    public abstract void service(Request request, Response response);  // 按方法分发由子类实现
}

这一步对应课程模块 2 的核心:Servlet 是「容器托管的处理单元」,容器负责它的创建、复用与销毁。

交互实验:Servlet 生命周期(ServletLifecycleMachine)

点下面的「容器事件」按钮,看 Tomcat 按什么顺序加载类、new 实例、调用 init / service / destroy。 「客户端请求」可以连点多次(service 可重复);「卸载应用」之后一切结束。

未加载
→
已实例化
→
已初始化
→
服务中↻
→
已销毁

当前:未加载 —— 容器尚未加载该 Servlet 类

容器调用日志

  1. 尚未加载,等待容器处理

三个生命周期方法

方法调用次数做了什么
init()一次实例化后只执行一次:读配置、开数据库连接等一次性准备
service()可多次每次请求调用一次;按请求方法分发到 doGet(req,res) / doPost(req,res),可重复调用
destroy()一次卸载前只执行一次:释放资源(关连接等),之后对象不再被使用

web.xml 中声明并映射 Servlet

<servlet>
    <servlet-name>HelloServlet</servlet-name>
    <servlet-class>examples.HelloServlet</servlet-class>
</servlet>
<servlet-mapping>
    <servlet-name>HelloServlet</servlet-name>
    <url-pattern>/hello.svc</url-pattern>
</servlet-mapping>

HttpServletRequest / HttpServletResponse 常用方法

对象方法用途
requestgetParameter(name)取表单/查询参数(String,无此参数返回 null)
requestgetParameterValues(name)取同名多值参数(String[],如复选框)
requestgetSession()取当前会话(没有则创建),跨请求保存用户状态
responsesetContentType(...)设响应类型与编码,如 text/html;charset=UTF-8
responsegetWriter()取字符输出流,把 HTML/文本写入响应正文

Servlet 实例是「单例多线程」的:所有请求共享同一实例,service() 可能被多个线程并发调用。因此不要用实例变量保存本次请求的数据(会串数据),请求相关状态一律用局部变量;确需共享时用同步块或 ThreadLocal。

五、V4:Filter 链——横切关注点

日志、编码、鉴权这类「每个请求都要做一遍」的逻辑,不该塞进每个 Servlet。V4 用 Filter 把它们抽出来,LoggingFilter 包裹真正的处理逻辑,先记一行日志再放行:

// HttpWebServerV4/src/LoggingFilter.java(节选)
public void doFilter(Request req, Response res, FilterChain chain) {
    log(req.getMethod(), req.getPath());   // 放行前记 Method/Path
    chain.doFilter(req, res);              // 放行给下一个环节
    log(res.getStatus());                  // 回来时记状态码
}

交互实验:Filter 链执行顺序(FilterChainVisualizer)

请求向下依次穿过编码 → 权限 → 日志三个 Filter 才到达 Servlet;响应向上按相反顺序返回。 点「下一步」逐帧走查;切到「权限校验失败」看链如何被短路。

EncodingFilter(字符编码)
→
AuthFilter(权限验证)
→
LoggingFilter(日志记录)
→
HelloServlet(目标 Servlet)

↓ 向下放行EncodingFilter:设置请求/响应字符编码为 UTF-8,放行

链执行日志

  1. 1.EncodingFilter:设置请求/响应字符编码为 UTF-8,放行

chain.doFilter() 调用模式(对照 LoggingFilter.java)

public void doFilter(Request req, Response res, FilterChain chain) {
    // 前置处理:编码 / 权限校验 / 日志 …
    chain.doFilter(req, res);   // 放行:交给下一个 Filter 或目标 Servlet
    // 后置处理:仅当下游没有短路时才会执行到这里
}

web.xml 中声明并映射 Filter

<filter>
    <filter-name>EncodingFilter</filter-name>
    <filter-class>examples.EncodingFilter</filter-class>
</filter>
<filter-mapping>
    <filter-name>EncodingFilter</filter-name>
    <url-pattern>/hello.svc</url-pattern>
</filter-mapping>
…(AuthFilter、LoggingFilter 同式声明,映射同一 url-pattern)
第 1 / 7 步

六、V5:Listener 与 Session 事件

「会话被创建/销毁时做点什么」这类事件驱动逻辑,交给 Listener。SessionCounterListener 在会话增减时维护一个在线计数——这正是 HttpSessionListener 的仿真:

// HttpWebServerV5/src/SessionCounterListener.java(节选)
private final AtomicInteger activeSessions = new AtomicInteger(0);

public void sessionCreated(HttpSessionEvent se) {
    int n = activeSessions.incrementAndGet();   // +1,并回写应用属性供页面展示
}
public void sessionDestroyed(HttpSessionEvent se) {
    int n = activeSessions.decrementAndGet();   // -1,同理回写
}

七、V6:Spring MVC 仿真——前端控制器

V6 把「分发」这件事从手写 if-else 升级成注解驱动的前端控制器。MvcDispatcher 扮演 DispatcherServlet:扫描带 @Controller 的类,用 @RequestMapping 建立 URL→方法映射;HelloController 只写业务:

// HttpWebServerV6/src/example/HelloController.java(节选)
@Controller
@RequestMapping("/springhello")
public class HelloController {
    @RequestMapping("/world")
    public String helloWorld(Request request, Response response) {
        return "hello";   // 返回视图名,由视图解析器渲染对应模板
    }
}
// HttpWebServerV6/src/com/simspring/bootstrap/MvcDispatcher.java(节选)
// 启动时扫描 @Controller,建立 URL → HandlerMethod 的映射表;
// 每次请求:查表 → 反射调用方法 → 渲染视图

这一步把课程模块 5 的「前端控制器 + 注解映射」从概念落到可运行的代码。

八、V7:ORM 层——动态代理生成 SQL

V7 补上数据访问层,思路与 MyBatis 一致:接口 + 注解 + 动态代理。MapperProxy 实现 InvocationHandler,运行时读取方法上的 @Select/@Insert/@Update/@Delete 注解,拼出 SQL 并执行:

// HttpWebServerV7/src/com/simspring/orm/MapperProxy.java(节选)
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    if (method.isAnnotationPresent(Select.class)) {
        String sql = method.getAnnotation(Select.class).value();
        // 按泛型返回类型区分 List / 单对象,交给 mockDatabase 执行
        return mockDatabase.executeSelectList(sql, elementType, args);
    }
    // … 同理处理 @Insert / @Update / @Delete
}

配合 SqlSessionFactory、RepositoryBeanPostProcessor,一个纯接口就能「凭空」得到可用的数据访问对象——这正是模块 8 MyBatis 的精髓。

九、小结:容器是「一层层包出来的」

能力V1→V7 里由谁提供
收发字节V1 裸 Socket
请求/会话模型V2 Request/Response/Session
处理单元 + 生命周期V3 Servlet
横切逻辑V4 Filter
事件响应V5 Listener
注解分发V6 Spring MVC 仿真
数据访问V7 ORM 动态代理

读完这条链,再回看 Tomcat 里 DispatcherServlet、Filter、Listener、MyBatis 那一堆概念,你会发现它们不是凭空出现的规范,而是「手写服务器写不下去时」自然长出来的分层。

学习小测验

Q1

V1 版本里,HTTP 服务器最底层的本质是( )

Q2

从 V2 开始引入 Application(带 attributes/initParameters)对象,它对应后来 Servlet 规范里的哪个概念?

Q3

V4 的 LoggingFilter 之所以能把「记日志」从每个 Servlet 里抽出来,靠的是( )

Q4

V7 的 MapperProxy 让一个纯接口方法(带 @Select)能直接执行 SQL,其核心机制是( )