哇塞

基于你的约束和代码,我来提供一个优雅的解决方案。

一、核心配置设计

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 */
    @JvmField
    var prettyPrint: Boolean = false,

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

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

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

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

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

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

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

    /** Read enums using toString() instead of name() */
    @JvmField
    var readEnumsUsingToString: Boolean = false,
) {
    /**
     * Apply this configuration to an ObjectMapper.
     * Internal use only - called during initialization.
     */
    internal fun applyTo(mapper: ObjectMapper) {
        // 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.
     */
    @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.
     */
    @JvmField
    var propagateSubAppExceptions: Boolean = true,

    /** 
     * Whether to log all exceptions (including handled ones).
     */
    @JvmField
    var logAllExceptions: Boolean = true,

    /** 
     * Log level for handled 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,
)

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

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

    /** 
     * Whether to treat "." as a normal character in path parameters.
     * If false, "/files/:name" won't match "/files/document.pdf" 
     * (the ".pdf" is treated as an extension)
     */
    @JvmField
    var treatDotsAsNormalChar: Boolean = false,

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

    /**
     * Whether to automatically decode path parameters (URL decoding).
     */
    @JvmField
    var autoDecodePathParams: Boolean = true,

    /**
     * Whether to enable method override via "_method" query parameter or header.
     * Useful for HTML forms that only support GET/POST.
     */
    @JvmField
    var enableMethodOverride: Boolean = false,
)

/**
 * 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.
     */
    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
    }

    /**
     * Set a custom JsonMapper using a builder (for advanced users).
     */
    fun jsonMapper(block: JacksonMapper.() -> Unit) {
        _jsonMapper = createDefaultJsonMapper().apply(block)
    }

    private fun createDefaultJsonMapper(): JacksonMapper {
        return JacksonMapper(useVirtualThreads = server.useVirtualThreads).also {
            json.applyTo(it.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: 使用默认配置
val app = Colleen()
app.start()

// 示例 2: 基础配置
val app = Colleen().apply {
    config.server {
        port = 8080
        useVirtualThreads = true
    }
    
    config.json {
        prettyPrint = true
        failOnUnknownProperties = false
    }
    
    config.error {
        showStackTrace = true // 开发环境
        logAllExceptions = true
    }
    
    config.routing {
        ignoreTrailingSlash = true
        caseSensitive = false
    }
}

// 示例 3: 高级 - 完全自定义 JsonMapper
val app = Colleen().apply {
    config.jsonMapper {
        updateMapper { mapper ->
            mapper.registerModule(CustomModule())
            mapper.configure(...)
        }
    }
}

// 示例 4: 高级 - 使用自己的 JsonMapper 实现
val app = Colleen().apply {
    config.jsonMapper(MyCustomJsonMapper())
}

// 示例 5: 生产环境配置
val app = Colleen().apply {
    config.server {
        host = "0.0.0.0"
        port = 8080
        useVirtualThreads = true
        maxRequestSize = 50 * 1024 * 1024 // 50MB
        shutdownTimeout = 60_000
    }
    
    config.json {
        prettyPrint = false
        includeNulls = false
        failOnUnknownProperties = false
    }
    
    config.error {
        showStackTrace = false // 生产环境不暴露堆栈
        propagateSubAppExceptions = true
        logAllExceptions = true
    }
    
    config.routing {
        caseSensitive = true
        ignoreTrailingSlash = true
        maxPathSegments = 50
    }
}

三、在 Colleen 类中的集成

class Colleen {
    @JvmField
    val config = Config()
    
    fun start() {
        // 在启动前冻结配置
        config.finalize()
        
        // 现在可以安全使用 config.jsonMapper
        // 它已经根据最终的 server.useVirtualThreads 配置初始化
        
        // ... 启动服务器
    }
}

四、配置项说明

JsonConfig 常见配置项(我已包含)

✅ prettyPrint - 格式化输出
✅ includeNulls - 是否包含 null 值
✅ failOnUnknownProperties - 反序列化时遇到未知字段是否报错
✅ writeDatesAsTimestamps - 日期格式(时间戳 vs ISO-8601)
✅ dateFormat - 自定义日期格式
✅ failOnEmptyBeans - 空对象是否报错
✅ acceptSingleValueAsArray - 单值作为数组
✅ writeEnumsUsingToString - 枚举序列化方式

ErrorConfig 配置项(重命名建议)

RoutingConfig 配置项

五、优势总结

延迟初始化 - jsonMapper 在首次访问时创建,此时所有配置已完成
保持兼容 - Colleen 类无需改动,现有用例无需修改
类型安全 - DSL 配置,IDE 自动补全
灵活性 - 既支持简单配置,也支持完全自定义
配置分层 - server/json/error/routing 职责清晰
合理默认值 - 开箱即用
生产就绪 - 包含安全相关配置项

这个方案既优雅又实用,符合 Kotlin 惯例,同时也参考了 Ktor、Spring Boot 等成熟框架的最佳实践!