哈哈谢谢夸奖!😊 让我逐一解答并优化:

一、回答你的问题

1. ✅ 延迟初始化已实现

当前设计已经满足:用户不配置则首次访问时初始化,配置会被正确应用。

2. autoDecodePathParamsenableMethodOverride 的用途

autoDecodePathParams

// URL: /users/John%20Doe

// autoDecodePathParams = true (默认)
get("/users/:name") { ctx ->
    ctx.pathParam("name") // "John Doe" ✅ 自动解码
}

// autoDecodePathParams = false
get("/users/:name") { ctx ->
    ctx.pathParam("name") // "John%20Doe" ❌ 需要手动解码
}

enableMethodOverride

<!-- HTML 表单只支持 GET/POST,但你想发 DELETE 请求 -->

<!-- enableMethodOverride = true -->
<form method="POST" action="/users/123">
    <input type="hidden" name="_method" value="DELETE">
    <button type="submit">Delete User</button>
</form>

<!-- 或通过 Header -->
POST /users/123
X-HTTP-Method-Override: DELETE

<!-- 框架会把这个 POST 当作 DELETE 处理 -->

这两个配置项其实不是特别常用,可以考虑移除或标记为高级选项。我建议保留 autoDecodePathParams(默认 true),去掉 enableMethodOverride

3. 改进 logAllExceptions → 更精细的日志控制

/**
 * Error handling configuration.
 */
data class ErrorConfig(
    /** 
     * Whether to show detailed stack traces in error responses.
     * Should be false in production for security.
     */
    @JvmField
    var showStackTrace: Boolean = false,

    /** 
     * Whether to propagate exceptions from mounted sub-applications.
     */
    @JvmField
    var propagateSubAppExceptions: Boolean = true,

    /** 
     * Minimum HTTP status code to log exceptions.
     * - 400: Log all client and server errors (4xx and 5xx)
     * - 500: Log only server errors (5xx) - DEFAULT
     * - 600: Disable exception logging
     */
    @JvmField
    var logExceptionsFrom: Int = 500,

    /** 
     * Log level for exceptions: "ERROR", "WARN", "INFO", "DEBUG"
     */
    @JvmField
    var exceptionLogLevel: String = "ERROR",

    /**
     * Whether to include request details (path, method, headers) in error logs.
     */
    @JvmField
    var includeRequestDetailsInLogs: Boolean = true,
)

使用示例

// 只记录 5xx 错误(默认)
config.error {
    logExceptionsFrom = 500
}

// 记录所有 4xx 和 5xx
config.error {
    logExceptionsFrom = 400
}

// 关闭异常日志
config.error {
    logExceptionsFrom = 600
}

4. JacksonMapper 如何适配新的 JsonConfig

package site.daydream.colleen.json

import com.fasterxml.jackson.annotation.JsonInclude
import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.databind.SerializationFeature
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule
import com.fasterxml.jackson.module.kotlin.KotlinModule
import site.daydream.colleen.util.InputStreamFactory
import site.daydream.colleen.util.RingBufferInputStreamFactory
import java.io.InputStream
import java.io.OutputStream
import java.lang.reflect.Type
import java.util.function.Consumer
import java.util.stream.Stream

class JacksonMapper(
    objectMapper: ObjectMapper? = null,
    private val useVirtualThreads: Boolean = false,
) : JsonMapper {

    private val inputStreamFactory: InputStreamFactory by lazy {
        RingBufferInputStreamFactory(useVirtualThreads)
    }

    // ✅ 改为 internal,允许外部(Config)访问和配置
    internal val mapper = objectMapper ?: defaultMapper()

    override fun toJsonString(obj: Any, type: Type): String = when (obj) {
        is String -> obj
        else -> mapper.writeValueAsString(obj)
    }

    override fun toJsonStream(obj: Any, type: Type): InputStream = when (obj) {
        is String -> obj.byteInputStream()
        else -> inputStreamFactory.open { outputStream ->
            mapper.factory.createGenerator(outputStream).writeObject(obj)
        }
    }

    override fun writeToOutputStream(stream: Stream<*>, outputStream: OutputStream) {
        mapper.writer().writeValuesAsArray(outputStream).use { sequenceWriter ->
            stream.forEach { sequenceWriter.write(it) }
        }
    }

    override fun <T : Any> fromJsonString(json: String, targetType: Type): T =
        mapper.readValue(json, mapper.typeFactory.constructType(targetType))

    override fun <T : Any> fromJsonStream(json: InputStream, targetType: Type): T =
        mapper.readValue(json, mapper.typeFactory.constructType(targetType))

    override fun <T : Any> convertValue(obj: Any, targetType: Type): T {
        return mapper.convertValue(obj, mapper.typeFactory.constructType(targetType))
    }

    /** Update the current mapper and return self for easy chaining */
    fun updateMapper(updateFunction: Consumer<ObjectMapper>): JacksonMapper {
        updateFunction.accept(this.mapper)
        return this
    }

    companion object {
        @JvmStatic
        fun defaultMapper(): ObjectMapper = ObjectMapper()
            .registerModule(KotlinModule.Builder().build())
            .registerModule(JavaTimeModule())
            .setSerializationInclusion(JsonInclude.Include.NON_NULL)
            .disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)
    }
}

唯一改动val mapperinternal val mapper,允许 JsonConfig.applyTo() 访问。

二、完整的优化版配置

package site.daydream.colleen

import org.slf4j.Logger
import org.slf4j.LoggerFactory
import site.daydream.colleen.json.JacksonMapper
import site.daydream.colleen.json.JsonMapper
import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.databind.DeserializationFeature
import com.fasterxml.jackson.databind.SerializationFeature
import com.fasterxml.jackson.annotation.JsonInclude

inline val <reified T> T.logger: Logger
    get() = LoggerFactory.getLogger(T::class.java)

/**
 * Server configuration for network and connection settings.
 */
data class ServerConfig(
    @JvmField
    var host: String = "127.0.0.1",

    @JvmField
    var port: Int = 8080,

    /** Whether to use virtual threads (Java 21+) */
    @JvmField
    var useVirtualThreads: Boolean = true,

    /** Maximum worker threads, only used when useVirtualThreads = false (default: CPU count * 8) */
    @JvmField
    var maxThreads: Int = Runtime.getRuntime().availableProcessors() * 8,

    /** Maximum concurrent requests (default: no limit) */
    @JvmField
    var maxConcurrentRequests: Int = 0,

    /** Maximum size of the entire request in bytes (default: 30MB) */
    @JvmField
    var maxRequestSize: Long = 30 * 1024 * 1024,

    /** Maximum size of a single uploaded file in bytes (default: 10MB) */
    @JvmField
    var maxFileSize: Long = 10 * 1024 * 1024,

    /** File size threshold for writing to disk in bytes (default: 10KB) */
    @JvmField
    var fileSizeThreshold: Long = 10 * 1024,

    /** Graceful shutdown timeout in milliseconds */
    @JvmField
    var shutdownTimeout: Long = 30_000,

    /** Connection idle timeout in milliseconds (0 = infinite) */
    @JvmField
    var idleTimeout: Long = 30_000,

    /** Connection read timeout in milliseconds (0 = infinite) */
    @JvmField
    var readTimeout: Long = 30_000,

    /** Connection write timeout in milliseconds (0 = infinite) */
    @JvmField
    var writeTimeout: Long = 30_000,
)

/**
 * JSON serialization configuration.
 */
data class JsonConfig(
    /** Pretty print JSON output (default: false) */
    @JvmField
    var prettyPrint: Boolean = false,

    /** Include null values in JSON output (default: false) */
    @JvmField
    var includeNulls: Boolean = false,

    /** Fail on unknown properties during deserialization (default: false) */
    @JvmField
    var failOnUnknownProperties: Boolean = false,

    /** Write dates as timestamps (true) or ISO-8601 strings (false, default) */
    @JvmField
    var writeDatesAsTimestamps: Boolean = false,

    /** Date format pattern (e.g., "yyyy-MM-dd'T'HH:mm:ss", default: null = ISO-8601) */
    @JvmField
    var dateFormat: String? = null,

    /** Fail on empty beans (classes with no properties, default: false) */
    @JvmField
    var failOnEmptyBeans: Boolean = false,

    /** Accept single value as array during deserialization (default: false) */
    @JvmField
    var acceptSingleValueAsArray: Boolean = false,

    /** Write enums using toString() instead of name() (default: false) */
    @JvmField
    var writeEnumsUsingToString: Boolean = false,

    /** Read enums using toString() instead of name() (default: false) */
    @JvmField
    var readEnumsUsingToString: Boolean = false,
) {
    /**
     * Apply this configuration to a JacksonMapper's ObjectMapper.
     * Internal use only - called during initialization.
     */
    internal fun applyTo(jacksonMapper: JacksonMapper) {
        val mapper = jacksonMapper.mapper
        
        // Serialization features
        mapper.configure(SerializationFeature.INDENT_OUTPUT, prettyPrint)
        mapper.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, writeDatesAsTimestamps)
        mapper.configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, failOnEmptyBeans)
        mapper.configure(SerializationFeature.WRITE_ENUMS_USING_TO_STRING, writeEnumsUsingToString)

        // Deserialization features
        mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, failOnUnknownProperties)
        mapper.configure(DeserializationFeature.ACCEPT_SINGLE_VALUE_AS_ARRAY, acceptSingleValueAsArray)
        mapper.configure(DeserializationFeature.READ_ENUMS_USING_TO_STRING, readEnumsUsingToString)

        // Inclusion
        mapper.setSerializationInclusion(
            if (includeNulls) JsonInclude.Include.ALWAYS else JsonInclude.Include.NON_NULL
        )

        // Date format
        dateFormat?.let { mapper.dateFormat = java.text.SimpleDateFormat(it) }
    }
}

/**
 * Error handling configuration.
 */
data class ErrorConfig(
    /** 
     * Whether to show detailed stack traces in error responses.
     * Should be false in production for security reasons.
     */
    @JvmField
    var showStackTrace: Boolean = false,

    /** 
     * Whether to propagate exceptions from mounted sub-applications.
     * If true, exceptions from sub-apps bubble up to the parent handler.
     * If false, sub-apps handle their own exceptions independently.
     */
    @JvmField
    var propagateSubAppExceptions: Boolean = true,

    /** 
     * Minimum HTTP status code to log exceptions.
     * - 400: Log all client errors (4xx) and server errors (5xx)
     * - 500: Log only server errors (5xx) - DEFAULT
     * - 600: Disable exception logging completely
     */
    @JvmField
    var logExceptionsFrom: Int = 500,

    /** 
     * Log level for exceptions: "ERROR", "WARN", "INFO", "DEBUG"
     * Default: "ERROR"
     */
    @JvmField
    var exceptionLogLevel: String = "ERROR",

    /**
     * Whether to include request details (path, method, headers) in error logs.
     */
    @JvmField
    var includeRequestDetailsInLogs: Boolean = true,
)

/**
 * Routing configuration.
 */
data class RoutingConfig(
    /** 
     * Whether routing is case-sensitive.
     * If false, "/users" matches "/Users" and "/USERS"
     * Default: true
     */
    @JvmField
    var caseSensitive: Boolean = true,

    /** 
     * Whether to ignore trailing slashes in paths.
     * If true, "/users" matches "/users/"
     * Default: true
     */
    @JvmField
    var ignoreTrailingSlash: Boolean = true,

    /**
     * Whether to automatically decode path parameters (URL decoding).
     * If true, "/users/John%20Doe" → pathParam("name") returns "John Doe"
     * If false, you need to manually decode
     * Default: true
     */
    @JvmField
    var autoDecodePathParams: Boolean = true,

    /**
     * Maximum allowed path segments (protection against path traversal attacks).
     * 0 means no limit.
     * Default: 100
     */
    @JvmField
    var maxPathSegments: Int = 100,
)

/**
 * Main configuration class for Colleen Web Framework.
 */
data class Config(
    @JvmField
    var server: ServerConfig = ServerConfig(),

    @JvmField
    var json: JsonConfig = JsonConfig(),

    @JvmField
    var error: ErrorConfig = ErrorConfig(),

    @JvmField
    var routing: RoutingConfig = RoutingConfig(),
) {
    // Lazy initialization - only created when first accessed
    private var _jsonMapper: JsonMapper? = null

    /**
     * Get the configured JsonMapper.
     * Automatically initialized based on server and json configuration on first access.
     */
    val jsonMapper: JsonMapper
        get() = _jsonMapper ?: createDefaultJsonMapper().also { _jsonMapper = it }

    /**
     * Set a custom JsonMapper (for advanced users).
     * This overrides the default mapper created from JsonConfig.
     */
    fun jsonMapper(mapper: JsonMapper) {
        _jsonMapper = mapper
    }

    /**
     * Configure the JsonMapper using a builder (for advanced users).
     * This allows fine-tuning the JacksonMapper after JsonConfig is applied.
     * 
     * Example:
     * ```
     * config.jsonMapper {
     *     updateMapper { it.registerModule(CustomModule()) }
     * }
     * ```
     */
    fun jsonMapper(block: JacksonMapper.() -> Unit) {
        val mapper = _jsonMapper as? JacksonMapper ?: createDefaultJsonMapper()
        mapper.apply(block)
        _jsonMapper = mapper
    }

    private fun createDefaultJsonMapper(): JacksonMapper {
        return JacksonMapper(useVirtualThreads = server.useVirtualThreads).also { mapper ->
            json.applyTo(mapper)
        }
    }

    // DSL functions for configuration
    fun server(block: ServerConfig.() -> Unit) {
        server.apply(block)
    }

    fun json(block: JsonConfig.() -> Unit) {
        json.apply(block)
    }

    fun error(block: ErrorConfig.() -> Unit) {
        error.apply(block)
    }

    fun routing(block: RoutingConfig.() -> Unit) {
        routing.apply(block)
    }

    /**
     * Finalize configuration before server starts.
     * This ensures jsonMapper is initialized with final settings.
     * Called internally by Colleen.start()
     */
    internal fun finalize() {
        // Force jsonMapper initialization if not already set
        if (_jsonMapper == null) {
            _jsonMapper = createDefaultJsonMapper()
        }
    }
}

三、其他优化建议

1. 考虑添加 CORS 配置(常见需求)

/**
 * CORS (Cross-Origin Resource Sharing) configuration.
 */
data class CorsConfig(
    /** Enable CORS (default: false) */
    @JvmField
    var enabled: Boolean = false,

    /** Allowed origins (default: "*" if enabled) */
    @JvmField
    var allowedOrigins: List<String> = listOf("*"),

    /** Allowed methods (default: common HTTP methods) */
    @JvmField
    var allowedMethods: List<String> = listOf("GET", "POST", "PUT", "DELETE", "OPTIONS", "PATCH"),

    /** Allowed headers (default: "*") */
    @JvmField
    var allowedHeaders: List<String> = listOf("*"),

    /** Whether to allow credentials (cookies, authorization headers) */
    @JvmField
    var allowCredentials: Boolean = false,

    /** Max age for preflight requests in seconds (default: 1 hour) */
    @JvmField
    var maxAge: Long = 3600,
)

2. 添加压缩配置(性能优化)

/**
 * Response compression configuration.
 */
data class CompressionConfig(
    /** Enable gzip compression (default: true) */
    @JvmField
    var enabled: Boolean = true,

    /** Minimum response size to compress in bytes (default: 1KB) */
    @JvmField
    var minSize: Long = 1024,

    /** Compression level 0-9 (default: 6, higher = better compression but slower) */
    @JvmField
    var level: Int = 6,

    /** MIME types to compress (default: text/*, application/json, etc.) */
    @JvmField
    var mimeTypes: List<String> = listOf(
        "text/*",
        "application/json",
        "application/javascript",
        "application/xml"
    ),
)

3. 改进 DSL 链式调用

当前的 DSL 函数可以返回 Config 本身以支持链式调用:

fun server(block: ServerConfig.() -> Unit): Config {
    server.apply(block)
    return this
}

fun json(block: JsonConfig.() -> Unit): Config {
    json.apply(block)
    return this
}

// 使用
val config = Config()
    .server { port = 8080 }
    .json { prettyPrint = true }
    .error { showStackTrace = false }

4. 配置验证(可选)

internal fun finalize() {
    validate()
    
    if (_jsonMapper == null) {
        _jsonMapper = createDefaultJsonMapper()
    }
}

private fun validate() {
    require(server.port in 1..65535) { "Port must be between 1 and 65535" }
    require(server.maxRequestSize > 0) { "maxRequestSize must be positive" }
    require(error.logExceptionsFrom in 0..600) { "logExceptionsFrom must be a valid HTTP status code or 600" }
    require(error.exceptionLogLevel in setOf("ERROR", "WARN", "INFO", "DEBUG")) {
        "exceptionLogLevel must be ERROR, WARN, INFO, or DEBUG"
    }
}

四、使用示例(完整版)

// 开发环境
val devApp = Colleen().apply {
    config.server {
        port = 8080
        useVirtualThreads = true
    }
    
    config.json {
        prettyPrint = true
        failOnUnknownProperties = false
    }
    
    config.error {
        showStackTrace = true
        logExceptionsFrom = 400  // 记录所有 4xx 和 5xx
    }
}

// 生产环境
val prodApp = Colleen().apply {
    config.server {
        host = "0.0.0.0"
        port = 8080
        maxRequestSize = 50 * 1024 * 1024
        shutdownTimeout = 60_000
    }
    
    config.json {
        prettyPrint = false
        includeNulls = false
    }
    
    config.error {
        showStackTrace = false
        logExceptionsFrom = 500  // 只记录 5xx
        includeRequestDetailsInLogs = false
    }
    
    config.routing {
        caseSensitive = true
        maxPathSegments = 50
    }
}

// 高级自定义
val customApp = Colleen().apply {
    config.jsonMapper {
        updateMapper { mapper ->
            mapper.registerModule(MyCustomModule())
        }
    }
}

总结改进点

  1. logExceptionsFrom 替代 logAllExceptions,更灵活

  2. ✅ 移除了不常用的 treatDotsAsNormalCharenableMethodOverride

  3. ✅ JacksonMapper 的 mapper 改为 internal

  4. JsonConfig.applyTo() 直接操作 JacksonMapper

  5. ✅ 添加了详细的文档注释

  6. ✅ 建议添加 CORS 和 Compression 配置(可选)

这样的配置系统既强大又易用!🎉