Ohhnews

分类导航

$ cd ..
foojay原文

社区聚焦:BoxLang Express 为 JVM 带来 Node 风格 HTTP 开发体验

#boxlang express#jvm#http服务#中间件#开源项目

[LOADING...]

如果你在 Spring Boot、Micronaut 或 Jakarta EE 上花费了多年时间,那么“Web 框架”通常意味着 servlet 容器、嵌入式 Tomcat 或 Netty、构建步骤,以及一个 fat jar,之后才能真正响应请求。如果你可以跳过所有这些步骤,同时仍然停留在 JVM 上呢?

BoxLang Express 是一个完全由开发者 Robert Zehnder(KISDigital) 构建的全新社区模块,它做的正是这件事。它直接构建在 com.sun.net.httpserver.HttpServer 之上,这个类自 Java 6 起就存在于 JDK 中,因此无需配置嵌入式服务器,HTTP 层自身也不会在 classpath 中增加任何额外内容。boxlang server.bxs 就是完整的部署方式。

这也是对 JDK 中那个老类的一次真正现代化的改造:每个请求都在自己的虚拟线程上运行,因此一个慢处理程序或一次阻塞的下游调用不会像平台线程那样占用整个工作线程池。这就是 Project Loom 时代的并发模型,而且不需要任何框架要求你选择启用或重写配置。与此同时,由于 BoxLang 运行在 JVM 上并具备完整的 Java 互操作性,你现有的任何 Java 库都不会被排除在外。你可以直接从路由处理程序中使用 JDBC 驱动、日志框架和现有的业务类,而无需任何适配层。对于希望在小型服务、内部工具或原型上获得 Express 级迭代速度,又不想放弃生产环境中已经值得依赖的运行时和库的 JVM 开发者来说,这种组合在其他地方很难找到。

这正是我们希望在 BoxLang 持续演进过程中重点展示的社区贡献。

缘起

Robert 自己的描述最能说明问题:每当他需要一个快速的 HTTP 端点时,他都会怀念在 Node 中这一切有多快。

$ node
const app = require( 'express' )()
app.get( '/', ( req, res ) => res.send( 'Hello World' ) )
app.listen( 3000 )

BoxLang 没有对应的替代品。但它的底层是 JVM,而 JVM 自 Java 6 起就内置了 com.sun.net.httpserver.HttpServer,这是一个无需 servlet 容器的独立 HTTP 服务器。Robert 基于它构建了一个 15 秒即可上手的版本,BoxLang Express 就此诞生:

$ node
app = boxExpress()

app.get( "/", ( req, res ) => {
    res.send( "Hello World" )
} )

app.listen( 3000 )

路由、中间件、可挂载的子路由器、视图渲染——全部都以一个普通 BoxLang 进程运行。没有 servlet、没有 WAR,需要部署的仅仅是一个脚本。

核心功能

像安装其他模块一样,从 ForgeBox 安装它:

$ bash
box install boxlang-express

一旦 BoxLang 发现该模块,boxExpress() 就会在全局可用,无需任何与 require() 等价的导入。看到这套 API 时,任何发布过 Node 服务的人都会立刻感到熟悉:

  • 路由与参数——app.getapp.post:id 风格路径参数,以及查询参数解析,都与 Express 开发者已有的经验直接对应。
  • 中间件——采用相同的 ( req, res, next ) 形式,next( err ) 会直接跳转到错误处理中间件,检测方式也与 Express 相同。
  • 可挂载路由器——new bxModules.boxexpress.models.Router(),通过 app.use( "/api", apiRouter ) 挂载,包括路径作用域在内一应俱全。
  • 内置中间件工厂——boxExpressJSON()boxExpressUrlencoded()boxExpressStatic()boxExpressUpload(),以及用于基于 cookie 会话的 boxExpressSession()。每个中间件默认都设置了上限并做了加固,而不是留下容易误用的安全隐患。
  • 视图渲染——BoxLang 原生 .bxm 模板与内置的 Handlebars 引擎并存,按视图文件的扩展名自动选择引擎。
  • 静态文件与下载——res.sendFile()res.download(),并支持 ETag/Last-Modified 条件请求,让浏览器缓存开箱即用。

在平台确实存在差异的地方,它也给出了自己的倾向性做法。Node 通过事件循环让 CLI 进程保持运行;BoxLang 的 CLI 运行时没有事件循环,因此 listen() 默认会阻塞调用线程,而不要求每个脚本自己记得维护 keep-alive 循环。只需一个简单的 boxlang server.bxs,进程就会保持运行。

每个请求都在自己的虚拟线程上运行,因此慢处理程序不会阻塞应用的其他部分。另外,在调用 listen() 之前,路由表应已完整注册。

中间件:沿用你熟悉的方式

如果你写过 Express 中间件,那么这段代码读起来会完全符合你的预期。处理函数是 ( req, res, next ),错误处理函数则只是前面多了一个参数,检测方式与 Express 一致:四个参数而不是三个。

$ node
app.use( boxExpressJSON() )

app.post( "/echo", ( req, res ) => {
    res.json( { youSent: req.body } )
} )

app.use( ( err, req, res, next ) => {
    res.status( 500 ).json( { error: true, message: err.message } )
} )

boxExpressJSON() 默认将请求体限制在 100KB,上传中间件默认限制在 10MB,两者都可以通过 limit 选项覆盖。这种默认安全的姿态贯穿整个模块,而不是事后才补上。

挂载子路由的方式也与你在 Node 中的用法一致:

$ node
apiRouter = new boxexpress.models.Router()

apiRouter.get( "/ping", ( req, res ) => {
    res.json( { pong: true } )
} )

app.use( "/api", apiRouter )

在路由器内部,req.path 的作用域会被限定为 /ping,挂载前缀会在这里被剥除,并在调用过程中正确还原,与 express.Router() 的行为完全一致。

两套视图引擎,任你选择

并非每个 BoxLang 团队都希望在所有视图中写 #data.name#,因此 res.render() 同时支持 BoxLang 原生 .bxm 模板和内置的 Handlebars 引擎,按文件的扩展名来选择:

$ node
app.set( "views", expandPath( "./views" ) )

app.get( "/greet/:name", ( req, res ) => {
    res.render( "greeting", { name: req.params.name } )
} )

app.get( "/greet-hbs/:name", ( req, res ) => {
    res.render( "greeting.hbs", { data: { name: req.params.name } } )
} )
$ handlebars
<h1>Hello, {{data.name}}!</h1>

如果 Handlebars 是项目的主要模板格式,可以用 app.set( "view engine", "hbs" ) 为整个应用设置默认引擎;也可以按单个视图混合使用两种引擎。Handlebars 也无需单独安装,因为它已内置在模块中。

默认防御的设计

  • 静态文件和 sendFile() 的路径会在真正返回文件前,先解析为已经处理过符号链接的真实目录路径,因此公共目录中被放置的符号链接无法被用来越出该目录。
  • 上传的文件会以生成的名称写入磁盘,绝不会使用客户端提交的原始文件名,从而杜绝路径穿越与文件名冲突风险。
  • 调用方提供的文件名以前会被未转义地直接插入 Content-Disposition 响应头中;该问题已被发现并修复,因此现在无法再脱离引号包裹的 token,也无法注入额外的响应头。
  • 默认的 500 错误处理程序不再向未认证客户端回显原始异常信息——这一修复封堵了泄露内部文件路径和驱动错误的一个真实通道。真实错误信息仍会输出到 stdout,而主动设置 app.set( "env", "development" ) 后,仅会在本地调试时重新显示这条信息。

在一个年轻项目中不断发现并修复真实安全问题的这种迭代,正是你在基于它进行开发之前希望看到的。

Express 测试应用

你还可以查看 express-test:这是一个完全基于该模块构建的实时聊天应用,使用 Pulsely 处理发布/订阅消息,并在消息签名与发布前通过 bx-esapi 对用户输入进行清理。这个应用虽然很小,但它会像真实应用一样全面检验框架:路由、请求体、视图渲染,以及一个全程与实时后端通信的前端。

为什么这很重要

BoxLang Express 很好地证明了开放、JVM 原生语言所能带来的可能性。Robert 不需要等待核心团队的功能请求,也不需要 fork 任何仓库。他直接拿起了一直存在于 JDK 中的类,设计了 API,编写了代码,并将其发布到 ForgeBox 供所有人使用。现在,我们可以支持 Robert 和他的计划了!

如果你正在用 BoxLang 构建 HTTP 服务,并且希望在不需要 servlet 容器的情况下获得 Express 风格的使用体验,那就试试它吧。

$ bash
box install boxlang-express

你是否基于 BoxLang 构建了我们值得展示的项目?请告诉我们,我们很乐意聚焦这个社区正在打造的成果。

本文(社区聚焦:BoxLang Express 为 JVM 带来 Node 风格 HTTP)首发于 foojay