Web 主机
快速启动
cangjie
import spire_web_hosting.*
main() {
let builder = WebHost.createBuilder()
let host = builder.build()
host.run()
return 0
}
总结
- 只需要三行代码即可启动
web主机
,web主机
是对通用主机的扩展,因此拥有通用主机的所有能力。 - 启动 web 主机,会创建一个
http服务器
,监听 5000 端口来处理 http 请求,此时可以在浏览器上输入http://localhost:5000
请求管道
请求管道 是一个由多个**中间件(Middleware)**按顺序组成的处理链,负责处理 HTTP 请求并生成响应。每个中间件可以:
- 处理请求(如验证、日志记录)
- 修改请求或响应(如添加 HTTP 头)
- 直接返回响应(如静态文件中间件找到匹配文件时终止管道)
- 调用下一个中间件(通过
next()
传递)
管道顺序由注册顺序决定,典型流程:请求 → 中间件1 → 中间件2 → ... → 终结点(如 MVC)→ 响应
用 use
将多个请求委托链接在一起。 next 参数表示管道中的下一个委托。 可通过不调用 参数使管道短路。 通常可在 next 委托前后执行操作,如以下示例所示:
cangjie
import spire_web_hosting.*
main() {
let builder = WebHost.createBuilder()
let host = builder.build()
host.use{ context, next =>
println("middleware1: start")
next()
println("middleware1: endle")
}
host.use{ context, next =>
println("middleware2: start")
next()
println("middleware2: endle")
}
host.use{ context, next =>
println("middleware3: start")
next()
println("middleware3: endle")
}
host.run()
return 0
}
总结
WEB主机
除了实现了通用主机
之外,还在此基础上提供了请求管道
- 你可以在浏览器上输入
http://localhost:5000
查看输出结果 - 有了请求管道,我们就可以通过编写
中间件
来配置请求处理逻辑,并且中间件是可插拔的 - 应尽早在管道中调用异常处理委托,这样它们就能捕获在管道的后期阶段发生的异常。
自定义中间件
自定义中间件只需要实现IMiddleware
接口,中间件支持依赖注入,但只能依赖单例服务
。
异常处理中间件
让我来来写一个异常处理中间件,如果是已知的异常,那么打印异常消息,否则提示服务器错误。
cangjie
import spire_web_http.*
import spire_web_hosting.*
import spire_web_routing.*
import spire_extensions_injection.*
main() {
let builder = WebHost.createBuilder()
//注册路由中间件需要的服务
builder.services.addRouting()
let host = builder.build()
host.use(ExceptionHandlerMiddleware())
//添加终结点和终结点中间件,路由中间件
host.useEndpoints{ endpoints =>
//添加终结点1
endpoints.mapGet("hello1") { context =>
if (!context.request.isHttps) {
throw KnownException("必须使用https协议")
}
}
//添加终结点2
endpoints.mapGet("hello2") { context =>
if (!context.request.isHttps) {
throw Exception("必须使用https协议")
}
}
}
host.run()
return 0
}
public class KnownException <: Exception {
public init(message: String) {
super(message)
}
}
public class ExceptionHandlerMiddleware <: IMiddleware {
public func invoke(context: HttpContext, next: () -> Unit) {
try {
next()
} catch (known: KnownException) {
context.response.write("{success: false, message: \"${known.message}}\"")
context.response.status(StatusCodes.InternalServerError)
context.response.addHeader(HeaderNames.ContentType, "application/json;chatset=utf-8")
} catch (ex: Exception) {
context.response.write("{success: false, message: \"服务器错误\"}")
context.response.status(StatusCodes.InternalServerError)
context.response.addHeader(HeaderNames.ContentType, "application/json;chatset=utf-8")
}
}
}
总结
- 异常处理中间件一般要放到最上面,这样才能保住拦截到其他的中间件产生的异常信息
- 这里我们使用了路由中间件,中间件一般控制执行流程
简易 OpenApi
下面我们实现一个比较完整的中间件,里面运用到了 中间件
,容器
,选项
等技术。并使用扩展语法,便于使用者直接调用。
cangjie
import std.collection.*
import spire_web_http.*
import spire_web_hosting.*
import spire_web_routing.*
import spire_extensions_options.*
import spire_extensions_injection.*
main() {
let builder = WebHost.createBuilder()
//注册路由中间件需要的服务
builder.services.addRouting()
//注册OpenApi中间件需要的服务
builder.services.addOpenApi()
let host = builder.build()
//启用openapi中间件
if (host.environment.isDevelopment()) {
host.useOpenApi()
}
//添加终结点和终结点中间件,路由中间件
host.useEndpoints{ endpoints =>
endpoints.mapGet("/hello") { context =>
context.response.write("get")
}
endpoints.mapPost("/hello") { context =>
context.response.write("post")
}
}
host.run()
return 0
}
//openapi选项
public class OpenApiOptions {
public var path = "/openapi"
public var title = "spire OpenApi"
}
//openapi服务
public interface IOpenApiService {
func createDocument(): String
}
//openapi服务实现
public class OpenApiService <: IOpenApiService {
public OpenApiService(let _dataSource: EndpointDataSource, let _options :IOptions<OpenApiOptions>) {
}
public func createDocument() {
let sb = StringBuilder()
sb.append("<html>")
sb.append("<title>${_options.value.title}</title>")
sb.append("<body>")
for (pattern in _dataSource.endpoints |> filterMap{f => f as RouteEndpoint}) {
let methods = pattern.metadata.getMetadata<IHttpMethodMetadata>()
.flatMap{ f => String.join(f.httpMethods |> collectArray, delimiter: ",")}
sb.append("<a href=${pattern.routePattern}>${methods ?? ""}:${pattern.routePattern}</a><br/>")
}
sb.append("</body>")
sb.append("</html>")
return sb.toString()
}
}
//openapi中间件
public class OpenApiMiddleware <: IMiddleware {
public OpenApiMiddleware(let _service :IOpenApiService, let _options: IOptions<OpenApiOptions>) {
}
public func invoke(context: HttpContext, next: () -> Unit) {
if (_options.value.path == context.request.path.value) {
let document = _service.createDocument()
context.response.write(document)
} else {
next()
}
}
}
//扩展容器
extend ServiceCollection{
public func addOpenApi() {
addOpenApi{_ => }
}
public func addOpenApi(configureOptions: (OpenApiOptions)-> Unit) {
this.configure(configureOptions)
this.tryAddSingleton<IOpenApiService, OpenApiService>()
}
}
//扩展请求管道
extend ApplicationBuilder{
public func useOpenApi() {
this.use<OpenApiMiddleware>()
}
}
总结
EndpointDataSource
服务是由路由中间件注册的。- 一般我们只在开发环境才启用
OpenApi
中间件。
Http 服务器
web 主机在启动时会构建请求管道得到请求管道执行器,并启动 http 服务器来监听端口处理 http 请求。源码如下:
cangjie
class DefaultHttpRequestDistributor <: HttpRequestDistributor {
private let _app: RequestDelegate
private let _services: IServiceProvider
init(app: RequestDelegate, services: IServiceProvider) {
_app = app
_services = services
}
public func register(_: String, _: HttpRequestHandler): Unit {
}
public func register(_: String, _: (HttpContextBase) -> Unit): Unit {
}
public func distribute(_: String) {
let logger = _services.getOrThrow<ILoggerFactory>().createLogger("spire.hosting.lifetime")
return FuncHandler { context =>
try (requestScope = _services.createScope()) {
let contextImpl = HttpContextImpl(context, requestScope.services)
try {
setHttpContextAccessor(contextImpl)
_app(contextImpl)
} catch (ex: Exception) {
contextImpl.response.write("Internal Server Error")
contextImpl.response.status(StatusCodes.InternalServerError)
contextImpl.response.addHeader(HeaderNames.ContentType, "text/plain; charset=utf-8")
logger.error(ex, ex.message)
}
}
}
}
private func setHttpContextAccessor(context: HttpContextImpl): Unit {
if (let Some(contextAccessor) <- context.services.getOrDefault<IHttpContextAccessor>()) {
if (let internalContextAccessor: HttpContextAccessor <- contextAccessor) {
internalContextAccessor.setup(context)
}
}
}
}
总结
Web主机
会去实现HttpRequestDistributor
接口,将请求分发给handler
去处理,handler
就是最终构建的请求管道。- 每次监听到 http 请求都会创建一个作业域,我们称为
请求作用域
,并将子容器保存到HttpContext
上。 - 同时会判断容器中是否注册了
IHttpContextAccessor
,如果注册了IHttpContextAccessor
则将HttpContext
实列保存到IHttpContextAccessor
上。通过解析IHttpContextAccessor
服务,即可访问到当前HttpContext
实列
测试请求生命周期
我们可以编写一个测试案例来演示上面的源码要表达的含义
cangjie
import spire_web_http.*
import spire_web_hosting.*
import spire_web_routing.*
import spire_extensions_injection.*
main() {
let builder = WebHost.createBuilder()
builder.services.addRouting()
builder.services.addHttpContextAccessor()
builder.services.addScoped<Server, Server>()
let host = builder.build()
host.useEndpoints{ endpoints =>
// 添加终结点1
endpoints.mapGet("hello") { context =>
let server1 = context.services.getOrThrow<Server>()
let server2 = context.services.getOrThrow<Server>()
println("requestUrl:${server1.getRequestUrl()}")
// Server的生命周期注册为Scoped,在请求管道中,同一次请求共享同一个作用域,因此他们是同一个实例
println("server1 == server2:${refEq(server1, server2)}")
}
}
host.run()
return 0
}
public class Server {
public Server(
let _httpContextAccessor: IHttpContextAccessor) {
}
public func getRequestUrl() {
if (let Some(context) <-_httpContextAccessor.context) {
return context.request.getDisplayUrl()
}
throw UnsupportedException()
}
}
内置服务
通用主机为我们内置了一些服务,具体如下:
接口 | 用途 |
---|---|
ILoggerFactory | 用于创建和配置 ILogger 实例,管理日志记录器的生命周期和日志提供程序(如 Console、File 等)。 |
IConfiguration | 提供对应用程序配置(如 appsettings.json 、环境变量、命令行参数等)的键值对访问和读取功能。 |
IHostEnvironment | 提供当前宿主环境的信息(如 ApplicationName 、EnvironmentName 、ContentRootPath 等)。 |
IWebHostEnvironment | 提供当前宿主环境的信息(如 webRootPath )。 |
IHttpContextAccessor | 通过依赖注入的方式访问当前 HttpContext (需要手动注册)。 |