哇塞
基于你的约束和代码,我来提供一个优雅的解决方案。
一、核心配置设计
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 配置项(重命名建议)
~~
propagateExceptions~~ →propagateSubAppExceptions✅ 更清晰showStackTrace- 是否显示堆栈跟踪logAllExceptions- 是否记录所有异常exceptionLogLevel- 异常日志级别includeRequestDetailsInLogs- 日志中包含请求详情
RoutingConfig 配置项
caseSensitive- 路由大小写敏感ignoreTrailingSlash- 忽略尾部斜杠treatDotsAsNormalChar- 点号处理方式maxPathSegments- 最大路径段数(安全)autoDecodePathParams- 自动URL解码enableMethodOverride- HTTP方法覆盖
五、优势总结
✅ 延迟初始化 - jsonMapper 在首次访问时创建,此时所有配置已完成
✅ 保持兼容 - Colleen 类无需改动,现有用例无需修改
✅ 类型安全 - DSL 配置,IDE 自动补全
✅ 灵活性 - 既支持简单配置,也支持完全自定义
✅ 配置分层 - server/json/error/routing 职责清晰
✅ 合理默认值 - 开箱即用
✅ 生产就绪 - 包含安全相关配置项
这个方案既优雅又实用,符合 Kotlin 惯例,同时也参考了 Ktor、Spring Boot 等成熟框架的最佳实践!