Vert.x + Kotlin 手写 Web 框架:从 Tomcat + Spring MVC 到响应式核心

------仅用 Vert.x Core,从零实现请求抽象、正则路由与中间件管道

在经典的 Nginx → Tomcat → Spring MVC 架构中:

  • Tomcat 负责将原始 HTTP 字节流解析为结构化的请求对象;
  • Spring MVC 在此基础上,通过注解路由、拦截器链等机制,将请求分发给业务逻辑。

今天,我们仅使用 Vert.x Core (即 vertx-core),不依赖任何高层 Web 模块,亲手实现一个轻量但完整的 Web 框架。我们将逐步构建:

  • Request / Response:封装原始 HTTP 数据,提供清晰的编程接口;
  • 动态路由:通过正则表达式支持 /users/:id 这类路径;
  • 中间件系统:以可组合的方式插入通用逻辑,如日志、认证、CORS。

每一步都可运行、可验证,最终形成一个你完全理解其内部工作原理的 Web 内核


依赖说明

项目仅依赖以下两个库:

kotlin 复制代码
implementation("io.vertx:vertx-core:4.5.8")
implementation("org.jetbrains.kotlin:kotlin-stdlib")

不使用 vert.x-web,所有 HTTP 解析、路由匹配、响应构造均由我们自行实现。这让我们能清晰看到 Web 框架每一层的职责边界。


第一步:启动服务器 + 封装请求与响应

目标:

  • 启动原生 HTTP 服务器;
  • 定义 Request(≈ HttpServletRequest)和 Response(≈ HttpServletResponse);
  • 实现 /hello 静态路由。

核心数据结构

kotlin 复制代码
// Request.kt
data class Request(
    val method: String,
    val uri: String,
    val path: String,
    val headers: Map<String, String>,
    val query: Map<String, String>
)

// Response.kt
import io.vertx.core.http.HttpServerResponse

class Response(private val raw: HttpServerResponse) {
    fun text(content: String, statusCode: Int = 200) {
        raw
            .setStatusCode(statusCode)
            .putHeader("Content-Type", "text/plain; charset=utf-8")
            .end(content)
    }

    fun json(content: String, statusCode: Int = 200) {
        raw
            .setStatusCode(statusCode)
            .putHeader("Content-Type", "application/json; charset=utf-8")
            .end(content)
    }
}

URI 解析工具

kotlin 复制代码
// HttpUtils.kt
object HttpUtils {
    fun parsePath(uri: String): String =
        uri.split('?').first().ifEmpty { "/" }

    fun parseQuery(uri: String): Map<String, String> {
        val queryPart = uri.split('?').getOrNull(1) ?: return emptyMap()
        return queryPart.split('&').associate { param ->
            val parts = param.split('=', limit = 2)
            val key = java.net.URLDecoder.decode(parts.getOrElse(0) { "" }, "UTF-8")
            val value = java.net.URLDecoder.decode(parts.getOrElse(1) { "" }, "UTF-8")
            key to value
        }
    }
}

初始路由器

kotlin 复制代码
// Router.kt
import io.vertx.core.http.HttpServerRequest
import io.vertx.core.http.HttpServerResponse

class Router {
    private val routes = mutableMapOf<String, (Request, Response) -> Unit>()

    fun get(path: String, handler: (Request, Response) -> Unit) {
        routes[path] = handler
    }

    fun handle(req: HttpServerRequest, res: HttpServerResponse) {
        val path = HttpUtils.parsePath(req.uri())
        val handler = routes[path]

        if (handler != null) {
            val request = Request(
                method = req.method().name(),
                uri = req.uri(),
                path = path,
                headers = req.headers().entries().associate { it.key to it.value },
                query = HttpUtils.parseQuery(req.uri())
            )
            handler(request, Response(res))
        } else {
            res.setStatusCode(404).end("Not Found")
        }
    }
}

启动入口

kotlin 复制代码
// Main.kt
import io.vertx.core.Vertx

fun main() {
    val vertx = Vertx.vertx()
    val router = Router()

    router.get("/hello") { _, res -> res.text("Hello from Vert.x Core!") }

    vertx.createHttpServer()
        .requestHandler { httpReq -> router.handle(httpReq, httpReq.response()) }
        .listen(8080) { result ->
            if (result.succeeded()) println("✅ Server running on http://localhost:8080")
        }
}

运行后访问 http://localhost:8080/hello,即可看到响应。

你已用纯 Vert.x Core 实现了最基础的 Web 请求处理流程。


第二步:动态路由 ------ 用正则匹配路径参数

目标:

  • 支持 /users/:id 这样的路径模板;
  • 自动提取 :id 对应的值;
  • 无需第三方路由库,完全手动实现。

路由条目:模板 → 正则 → 参数名

kotlin 复制代码
// RouteEntry.kt
import java.util.regex.Pattern

data class RouteEntry(
    val template: String,
    private val pattern: Pattern,
    private val paramNames: List<String>,
    val handler: (Request, Response) -> Unit
) {
    companion object {
        fun compile(template: String, handler: (Request, Response) -> Unit): RouteEntry {
            val segments = template.trim('/').split('/')
            val regexParts = mutableListOf<String>()
            val names = mutableListOf<String>()

            for (segment in segments) {
                if (segment.startsWith(":")) {
                    names += segment.substring(1)
                    regexParts += "([^/]+)"  // 匹配任意非斜杠字符
                } else {
                    regexParts += Pattern.quote(segment)  // 转义静态段
                }
            }

            val regex = "^/${regexParts.joinToString("/")}$"
            return RouteEntry(template, Pattern.compile(regex), names, handler)
        }
    }

    fun match(path: String): Map<String, String>? {
        val matcher = pattern.matcher(path)
        if (!matcher.matches()) return null

        return paramNames.withIndex().associate { (i, name) ->
            name to (matcher.group(i + 1) ?: "")
        }
    }
}

更新 Request 与 Router

kotlin 复制代码
// Request.kt(更新)
data class Request(
    val method: String,
    val uri: String,
    val path: String,
    val headers: Map<String, String>,
    val query: Map<String, String>,
    val params: Map<String, String> = emptyMap()  // 新增路径参数
)

// Router.kt(更新)
class Router {
    private val routes = mutableListOf<RouteEntry>()

    fun get(template: String, handler: (Request, Response) -> Unit) {
        routes += RouteEntry.compile(template, handler)
    }

    fun handle(req: HttpServerRequest, res: HttpServerResponse) {
        val rawUri = req.uri()
        val path = HttpUtils.parsePath(rawUri)

        for (entry in routes) {
            val params = entry.match(path)
            if (params != null) {
                val request = Request(
                    method = req.method().name(),
                    uri = rawUri,
                    path = path,
                    headers = req.headers().entries().associate { it.key to it.value },
                    query = HttpUtils.parseQuery(rawUri),
                    params = params
                )
                entry.handler(request, Response(res))
                return
            }
        }

        res.setStatusCode(404).end("Not Found")
    }
}

注册动态路由

kotlin 复制代码
router.get("/users/:id") { req, res ->
    res.json("""{"id": "${req.params["id"]}", "name": "User"}""")
}

验证:

bash 复制代码
curl http://localhost:8080/users/42
# → {"id": "42", "name": "User"}

你现在的路由系统已支持 RESTful 风格,且完全由正则驱动,逻辑透明。


第三步:中间件系统 ------ 可组合的通用逻辑层

目标:

  • 在请求到达业务逻辑前/后插入通用处理;
  • 支持日志、CORS、认证等;
  • 中间件可按需组合,顺序可控。

中间件定义

kotlin 复制代码
// Middleware.kt
typealias Middleware = (Request, Response, () -> Unit) -> Unit

中间件接收请求、响应和"继续执行"的回调。调用 next() 表示放行,否则可提前终止流程(如返回 401)。

内置中间件

kotlin 复制代码
// Middlewares.kt
object Middlewares {
    val logger: Middleware = { req, _, next ->
        val start = System.currentTimeMillis()
        println("[${req.method}] ${req.path}")
        next()
        println("  → ${System.currentTimeMillis() - start}ms")
    }

    fun bearerAuth(expectedToken: String): Middleware = { req, res, next ->
        val auth = req.headers["authorization"]
        if (auth?.startsWith("Bearer ") == true && auth.substring(7) == expectedToken) {
            next()
        } else {
            res.text("Unauthorized", 401)
        }
    }
}

路由器集成中间件

kotlin 复制代码
// Router.kt(最终版)
class Router {
    private val middlewares = mutableListOf<Middleware>()
    private val routes = mutableListOf<RouteEntry>()

    fun use(middleware: Middleware) {
        middlewares.add(middleware)
    }

    fun get(template: String, handler: (Request, Response) -> Unit) {
        routes += RouteEntry.compile(template, handler)
    }

    fun handle(req: HttpServerRequest, res: HttpServerResponse) {
        val rawUri = req.uri()
        val path = HttpUtils.parsePath(rawUri)

        val matched = routes.firstOrNull { it.match(path) != null }
        if (matched == null) {
            res.setStatusCode(404).end("Not Found")
            return
        }

        val params = matched.match(path)!!
        val request = Request(
            method = req.method().name(),
            uri = rawUri,
            path = path,
            headers = req.headers().entries().associate { it.key to it.value },
            query = HttpUtils.parseQuery(rawUri),
            params = params
        )
        val response = Response(res)

        // 构建中间件调用链(洋葱模型)
        var next: () -> Unit = { matched.handler(request, response) }
        for (i in middlewares.lastIndex downTo 0) {
            val currentNext = next
            next = { middlewares[i](request, response, currentNext) }
        }
        next()
    }
}

使用中间件

kotlin 复制代码
val router = Router()
router.use(Middlewares.logger)
router.use(Middlewares.bearerAuth("my-token"))

router.get("/profile") { _, res -> res.text("Hello, authenticated user!") }

验证:

bash 复制代码
# 无 token → 401
curl http://localhost:8080/profile

# 有 token → 200 + 日志
curl -H "Authorization: Bearer my-token" http://localhost:8080/profile

控制台输出:

bash 复制代码
[GET] /profile
  → 2ms

中间件机制让你能横向切分关注点,业务逻辑不再混杂日志或安全代码。


未来拓展方向

当前框架已具备生产级内核的雏形。接下来可考虑:

  1. 请求体解析

    通过 req.handler { buffer -> ... } 读取 Body,支持 JSON 解码。

  2. 多方法支持

    扩展 post(), put(),在 RouteEntry 中记录 HTTP 方法。

  3. 类型安全参数

    支持 :id(int),自动校验并转换为 Int

  4. 全局异常处理

    捕获处理器中的未处理异常,返回 500,防止服务器崩溃。

  5. Kotlin 协程集成

    提供 suspend 版本的处理器,用 awaitResult 包装异步操作。

  6. 静态文件服务

    /static/** 路径返回文件内容。

  7. 测试支持

    利用 Vertx 的嵌入式能力编写集成测试。


结语:理解比使用更重要

你已用 纯 Vert.x Core 实现了 Web 框架的四大核心组件:

组件 作用 Spring 对应
Request 封装客户端请求数据 HttpServletRequest
Response 构造并发送 HTTP 响应 HttpServletResponse
Router 路径匹配 → 处理函数 HandlerMapping
Middleware 插入通用逻辑(日志、认证等) Filter

没有魔法,没有注解,没有黑盒。每一行代码都有明确职责,每一层抽象都清晰可见。

相关推荐
星辰徐哥4 小时前
Spring Boot 微服务架构设计与实现
spring boot·后端·微服务
星辰徐哥4 小时前
Spring Boot 数据导入导出与报表生成
spring boot·后端·ui
明夜之约4 小时前
Spring Boot 自动装配源码
java·spring boot·后端
Leaton Lee4 小时前
Spring Boot分层架构详解:从Controller到Service再到Mapper的完整流程
java·spring boot·后端·架构
Micro麦可乐4 小时前
Spring Boot 实战:从零设计一个短链系统(含完整代码与数据库设计)
数据库·spring boot·后端·哈希算法·雪花算法·短链系统
Jinkxs4 小时前
Resilience4j- 与 Spring Boot 快速集成:自动配置与基础注解使用
java·spring boot·后端
毕设源码_郑学姐4 小时前
计算机毕业设计springboot网络相册设计与实现 基于Spring Boot框架的在线相册管理系统开发与应用 Spring Boot驱动的网络影集设计与实践
spring boot·后端·课程设计
辣机小司4 小时前
【踩坑记录:Spring Boot 配置文件读取值不一致?警惕 YAML 的“八进制陷阱”与 SnakeYAML 版本之谜】
java·spring boot·后端·yaml·踩坑记录
码农阿豪4 小时前
从零到一:Spring Boot快速接入金仓数据库实战
数据库·spring boot·后端
追逐时光者4 小时前
一个基于 .NET 与 Avalonia 构建、面向 TrinityCore 的开源 WoW 数据库编辑器
后端·.net