哈哈谢谢夸奖!😊 让我逐一解答并优化:
一、回答你的问题
1. ✅ 延迟初始化已实现
当前设计已经满足:用户不配置则首次访问时初始化,配置会被正确应用。
2. autoDecodePathParams 和 enableMethodOverride 的用途
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 mapper → internal 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())
}
}
}总结改进点:
✅
logExceptionsFrom替代logAllExceptions,更灵活✅ 移除了不常用的
treatDotsAsNormalChar和enableMethodOverride✅ JacksonMapper 的
mapper改为internal✅
JsonConfig.applyTo()直接操作 JacksonMapper✅ 添加了详细的文档注释
✅ 建议添加 CORS 和 Compression 配置(可选)
这样的配置系统既强大又易用!🎉