语言

1. Kotlin

Kotlin 是一种静态类型语言,目标平台包括 JVM(以及其他平台),它允许编写简洁而优雅的代码,同时与用 Java 编写的现有库具有非常好的互操作性spring-doc.cadn.net.cn

Spring 框架为 Kotlin 提供了一流的支持,使开发者能够编写 Kotlin 应用程序,几乎就像 Spring 框架本身是一个原生的 Kotlin 框架一样。spring-doc.cadn.net.cn

使用 Kotlin 构建 Spring 应用程序最简单的方法是利用 Spring Boot 及其专门的 Kotlin 支持本综合教程 将教你如何使用start.spring.io构建基于 Kotlin 的 Spring Boot 应用程序。spring-doc.cadn.net.cn

从 Spring Framework 5.2 起,参考文档中的大部分代码示例除了提供 Java 版本外,还提供了 Kotlin 版本。spring-doc.cadn.net.cn

欢迎加入 Kotlin Slack 的 #spring 频道, 或在 Stackoverflow 上使用 kotlinhttps://stackoverflow.com/questions/tagged/spring+kotlin 标签提问, 如果您需要支持的话。spring-doc.cadn.net.cn

1.1. 要求

Spring Framework 支持 Kotlin 1.3,并要求类路径中存在 kotlin-stdlib (或其变体之一,例如 kotlin-stdlib-jdk8) 和 kotlin-reflect。如果您在 start.spring.io 上引导一个 Kotlin 项目,这些依赖将默认提供。spring-doc.cadn.net.cn

1.2. 扩展

Kotlin 扩展 提供了为现有类添加额外功能的能力。Spring Framework 的 Kotlin API 利用这些扩展,为现有的 Spring API 增加了 Kotlin 特有的便捷功能。spring-doc.cadn.net.cn

Spring Framework KDoc API 列出了 并记录了所有可用的 Kotlin 扩展和 DSL。spring-doc.cadn.net.cn

请记住,Kotlin 扩展必须被导入后才能使用。这意味着,例如,只有在导入了 GenericApplicationContext.registerBean 之后,org.springframework.context.support.registerBean 这个 Kotlin 扩展才可用。 话虽如此,与静态导入类似,在大多数情况下 IDE 应该会自动提示导入。

例如,Kotlin 的具体化类型参数为 JVM 的泛型类型擦除问题提供了一种变通方案, 而 Spring 框架也提供了一些扩展来充分利用这一特性。 这使得 Kotlin API(如 RestTemplate、Spring WebFlux 中全新的 WebClient 以及其他多种 API) 能够获得更好的使用体验。spring-doc.cadn.net.cn

其他库,例如 Reactor 和 Spring Data,也为其 API 提供了 Kotlin 扩展,从而整体上带来更佳的 Kotlin 开发体验。

要在 Java 中检索一个 User 对象列表,你通常会编写如下代码:spring-doc.cadn.net.cn

Flux<User> users  = client.get().retrieve().bodyToFlux(User.class)

借助 Kotlin 和 Spring Framework 扩展,你可以改为编写如下代码:spring-doc.cadn.net.cn

val users = client.get().retrieve().bodyToFlux<User>()
// or (both are equivalent)
val users : Flux<User> = client.get().retrieve().bodyToFlux()

与 Java 中一样,Kotlin 中的 users 是强类型的,但 Kotlin 的智能类型推断允许使用更简洁的语法。spring-doc.cadn.net.cn

1.3. 空安全

Kotlin 的关键特性之一是空安全(null-safety), 它在编译时就能清晰地处理null值,而不是在运行时遭遇著名的 NullPointerException。通过可空性声明,该特性使应用程序更加安全, 并能以“有值或无值”的语义表达,而无需付出使用包装器(如Optional)的代价。 (Kotlin 允许对可空值使用函数式构造。参见这份 Kotlin 空安全全面指南。)spring-doc.cadn.net.cn

尽管 Java 无法在其类型系统中表达空安全(null-safety),但 Spring 框架通过在 core.html#null-safety 包中声明的、对工具友好的注解,为整个 Spring 框架 API 提供了空安全性。 默认情况下,Kotlin 中使用的 Java API 类型会被识别为平台类型(platform types),对其空值检查会有所放宽。 Kotlin 对 JSR-305 注解的支持以及 Spring 的可空性注解为 Kotlin 开发者提供了整个 Spring 框架 API 的空安全性,其优势在于可以在编译时处理与 null 相关的问题。spring-doc.cadn.net.cn

诸如 Reactor 或 Spring Data 之类的库提供了空安全(null-safe)的 API 来利用此特性。

你可以通过添加 -Xjsr305 编译器标志并使用以下选项来配置 JSR-305 检查:-Xjsr305={strict|warn|ignore}spring-doc.cadn.net.cn

对于 Kotlin 1.1 及以上版本,默认行为与 -Xjsr305=warn 相同。 要使 Spring Framework API 的空安全特性在从 Spring API 推断出的 Kotlin 类型中生效,需要使用 strict 值,但应了解 Spring API 的可空性声明即使在次要版本之间也可能发生变化,并且未来可能会加入更多检查。spring-doc.cadn.net.cn

泛型类型参数、可变参数(varargs)和数组元素的可空性目前尚不支持, 但将在即将发布的版本中提供。请参阅此讨论 以获取最新信息。

1.4. 类和接口

Spring 框架支持多种 Kotlin 构造,例如通过主构造函数实例化 Kotlin 类、不可变类的数据绑定,以及带有默认值的函数可选参数。spring-doc.cadn.net.cn

Kotlin 参数名称通过专用的 KotlinReflectionParameterNameDiscoverer 被识别, 这使得在无需在编译期间启用 Java 8 的 -parameters 编译器标志的情况下,也能找到接口方法的参数名称。spring-doc.cadn.net.cn

当在类路径中发现 Jackson Kotlin 模块 时,该模块会自动注册(此模块是序列化或反序列化 JSON 数据所必需的)。如果检测到 Jackson 和 Kotlin,但未找到 Jackson Kotlin 模块,则会记录一条警告信息。spring-doc.cadn.net.cn

你可以将配置类声明为 顶层类或嵌套类,但不能是内部类, 因为后者需要对外部类的引用。spring-doc.cadn.net.cn

1.5. 注解

Spring 框架还利用了 Kotlin 的空安全(null-safety)特性, 无需显式定义 required 属性即可判断 HTTP 参数是否为必需。 这意味着 @RequestParam name: String? 被视为非必需参数, 而 @RequestParam name: String 则被视为必需参数。 此特性在 Spring Messaging 的 @Header 注解中也同样受支持。spring-doc.cadn.net.cn

类似地,Spring 使用 @Autowired@Bean@Inject 进行 Bean 注入时,会利用此信息来判断某个 Bean 是否为必需。spring-doc.cadn.net.cn

例如,@Autowired lateinit var thing: Thing 表示必须在应用上下文中注册一个类型为 Thing 的 bean,而 @Autowired lateinit var thing: Thing? 在该 bean 不存在时不会抛出错误。spring-doc.cadn.net.cn

遵循相同的原则,@Bean fun play(toy: Toy, car: Car?) = Baz(toy, Car) 表示 必须在应用上下文中注册一个类型为 Toy 的 Bean,而类型为 Car 的 Bean 可以存在,也可以不存在。同样的行为也适用于自动装配的构造函数参数。spring-doc.cadn.net.cn

如果你在具有属性或主构造函数参数的类上使用 Bean Validation,可能需要使用注解的使用位置目标(annotation use-site targets),例如 @field:NotNull@get:Size(min=5, max=15),如此 Stack Overflow 回答中所述。

1.6. Bean 定义 DSL

Spring Framework 支持使用 Lambda 表达式以函数式方式注册 Bean,作为 XML 或 Java 配置(@Configuration@Bean)的替代方案。简而言之,它允许你通过一个充当 FactoryBean 的 Lambda 表达式来注册 Bean。 这种机制非常高效,因为它不需要任何反射或 CGLIB 代理。spring-doc.cadn.net.cn

在 Java 中,例如,你可以编写如下代码:spring-doc.cadn.net.cn

class Foo {}

class Bar {
    private final Foo foo;
    public Bar(Foo foo) {
        this.foo = foo;
    }
}

GenericApplicationContext context = new GenericApplicationContext();
context.registerBean(Foo.class);
context.registerBean(Bar.class, () -> new Bar(context.getBean(Foo.class)));

在 Kotlin 中,借助具体化的类型参数和 GenericApplicationContext 的 Kotlin 扩展, 你可以改写为如下代码:spring-doc.cadn.net.cn

class Foo

class Bar(private val foo: Foo)

val context = GenericApplicationContext().apply {
    registerBean<Foo>()
    registerBean { Bar(it.getBean()) }
}

当类 Bar 只有一个构造函数时,你甚至只需指定 bean 类, 构造函数参数将按类型自动装配:spring-doc.cadn.net.cn

val context = GenericApplicationContext().apply {
    registerBean<Foo>()
    registerBean<Bar>()
}

为了支持更声明式的编程方式和更简洁的语法,Spring Framework 提供了 Kotlin Bean 定义 DSL。 它通过一个清晰的声明式 API 声明一个 ApplicationContextInitializer, 使你可以处理 Profiles 和 Environment,以自定义 Bean 的注册方式。spring-doc.cadn.net.cn

在下面的示例中请注意:spring-doc.cadn.net.cn

  • 类型推断通常可以避免为 bean 引用(例如 ref("bazBean"))显式指定类型。spring-doc.cadn.net.cn

  • 可以使用 Kotlin 的顶层函数,通过可调用引用(例如本例中的 bean(::myRouter))来声明 Bean。spring-doc.cadn.net.cn

  • 指定 bean<Bar>()bean(::myRouter) 时,参数会按类型自动装配。spring-doc.cadn.net.cn

  • 仅当 FooBar 配置文件处于激活状态时,foobar Bean 才会被注册。spring-doc.cadn.net.cn

class Foo
class Bar(private val foo: Foo)
class Baz(var message: String = "")
class FooBar(private val baz: Baz)

val myBeans = beans {
    bean<Foo>()
    bean<Bar>()
    bean("bazBean") {
        Baz().apply {
            message = "Hello world"
        }
    }
    profile("foobar") {
        bean { FooBar(ref("bazBean")) }
    }
    bean(::myRouter)
}

fun myRouter(foo: Foo, bar: Bar, baz: Baz) = router {
    // ...
}
该 DSL 是程序化的,这意味着它允许通过 if 表达式、for 循环或任何其他 Kotlin 结构来实现自定义的 Bean 注册逻辑。

然后,您可以使用这个 beans() 函数在应用上下文中注册 bean,如下例所示:spring-doc.cadn.net.cn

val context = GenericApplicationContext().apply {
    myBeans.initialize(this)
    refresh()
}
Spring Boot 基于 JavaConfig, 目前尚未提供对函数式 Bean 定义的特定支持, 但你可以通过 Spring Boot 的 ApplicationContextInitializer 支持来实验性地使用函数式 Bean 定义。 有关更多详细信息和最新动态,请参阅 此 Stack Overflow 回答。 另请参阅在 Spring Fu 孵化器 中开发的实验性 Kofu DSL。

1.7. Web

1.7.1. 路由 DSL

Spring Framework 自带一个 Kotlin 路由 DSL,提供三种风格:spring-doc.cadn.net.cn

这些 DSL 允许你编写简洁且符合 Kotlin 习惯的代码,以构建一个 RouterFunction 实例,如下例所示:spring-doc.cadn.net.cn

@Configuration
class RouterRouterConfiguration {

    @Bean
    fun mainRouter(userHandler: UserHandler) = router {
        accept(TEXT_HTML).nest {
            GET("/") { ok().render("index") }
            GET("/sse") { ok().render("sse") }
            GET("/users", userHandler::findAllView)
        }
        "/api".nest {
            accept(APPLICATION_JSON).nest {
                GET("/users", userHandler::findAll)
            }
            accept(TEXT_EVENT_STREAM).nest {
                GET("/users", userHandler::stream)
            }
        }
        resources("/**", ClassPathResource("static/"))
    }
}
该 DSL 是程序化的,意味着它允许通过 if 表达式、for 循环或任何其他 Kotlin 结构来实现 Bean 的自定义注册逻辑。当你需要根据动态数据(例如来自数据库的数据)来注册路由时,这会非常有用。

参见MiXiT 项目以获取一个具体示例。spring-doc.cadn.net.cn

1.7.2. MockMvc DSL

通过 MockMvc 的 Kotlin 扩展提供了 Kotlin DSL,以提供更符合 Kotlin 习惯的 API,并提升可发现性(无需使用静态方法)。spring-doc.cadn.net.cn

val mockMvc: MockMvc = ...
mockMvc.get("/person/{name}", "Lee") {
    secure = true
    accept = APPLICATION_JSON
    headers {
        contentLanguage = Locale.FRANCE
    }
    principal = Principal { "foo" }
}.andExpect {
    status { isOk }
    content { contentType(APPLICATION_JSON) }
    jsonPath("$.name") { value("Lee") }
    content { json("""{"someBoolean": false}""", false) }
}.andDo {
    print()
}

1.7.3. Kotlin 脚本模板

Spring Framework 提供了一个 ScriptTemplateView ,它支持 JSR-223,以便使用脚本引擎渲染模板。spring-doc.cadn.net.cn

通过利用 kotlin-script-runtimescripting-jsr223-embeddable 依赖,可以使用此功能来渲染基于 Kotlin 的模板,例如使用 kotlinx.html DSL 或 Kotlin 多行插值 Stringspring-doc.cadn.net.cn

build.gradle.ktsspring-doc.cadn.net.cn

dependencies {
    compile("org.jetbrains.kotlin:kotlin-script-runtime:${kotlinVersion}")
    runtime("org.jetbrains.kotlin:kotlin-scripting-jsr223-embeddable:${kotlinVersion}")
}

配置通常通过 ScriptTemplateConfigurerScriptTemplateViewResolver 这两个 Bean 完成。spring-doc.cadn.net.cn

KotlinScriptConfiguration.ktspring-doc.cadn.net.cn

@Configuration
class KotlinScriptConfiguration {

    @Bean
    fun kotlinScriptConfigurer() = ScriptTemplateConfigurer().apply {
        engineName = "kotlin"
        setScripts("scripts/render.kts")
        renderFunction = "render"
        isSharedEngine = false
    }

    @Bean
    fun kotlinScriptViewResolver() = ScriptTemplateViewResolver().apply {
        setPrefix("templates/")
        setSuffix(".kts")
    }
}

有关更多详情,请参见 kotlin-script-templating 示例项目。spring-doc.cadn.net.cn

1.8. 协程

Kotlin 协程 是 Kotlin 的轻量级线程,允许以命令式方式编写非阻塞代码。在语言层面,挂起函数为异步操作提供了抽象;而在库层面,kotlinx.coroutines 提供了诸如 async { } 之类的函数以及如 Flow 之类的类型。spring-doc.cadn.net.cn

Spring Framework 在以下范围内提供对协程(Coroutines)的支持:spring-doc.cadn.net.cn

1.8.1. 依赖项

当类路径中包含 kotlinx-coroutines-corekotlinx-coroutines-reactor 依赖项时,协程支持将被启用:spring-doc.cadn.net.cn

build.gradle.ktsspring-doc.cadn.net.cn

dependencies {

    implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:${coroutinesVersion}")
    implementation("org.jetbrains.kotlinx:kotlinx-coroutines-reactor:${coroutinesVersion}")
}

支持版本 1.3.0 及以上。spring-doc.cadn.net.cn

1.8.2. 响应式如何转换为协程?

对于返回值,从响应式(Reactive)API 到协程(Coroutines)API 的转换如下所示:spring-doc.cadn.net.cn

  • fun handler(): Mono<Void> 变为 suspend fun handler()spring-doc.cadn.net.cn

  • fun handler(): Mono<T> 变为 suspend fun handler(): Tsuspend fun handler(): T?,具体取决于 Mono 是否可能为空(这样做的优势在于具有更强的静态类型安全性)spring-doc.cadn.net.cn

  • fun handler(): Flux<T> 变为 fun handler(): Flow<T>spring-doc.cadn.net.cn

对于输入参数:spring-doc.cadn.net.cn

  • 如果不需要延迟加载,fun handler(mono: Mono<T>) 就会变成 fun handler(value: T),因为挂起函数可以被调用来获取 value 参数。spring-doc.cadn.net.cn

  • 如果需要惰性求值,fun handler(mono: Mono<T>) 就变为 fun handler(supplier: suspend () → T)fun handler(supplier: suspend () → T?)spring-doc.cadn.net.cn

Flow 在协程世界中相当于 Flux,适用于热流或冷流、有限或无限流,主要区别如下:spring-doc.cadn.net.cn

阅读这篇关于使用 Spring、协程和 Kotlin Flow 实现响应式编程的博客文章, 以了解更多详情,包括如何使用协程并发运行代码。spring-doc.cadn.net.cn

1.8.3. 控制器

这是一个协程(Coroutines)@RestController 的示例。spring-doc.cadn.net.cn

@RestController
class CoroutinesRestController(client: WebClient, banner: Banner) {

    @GetMapping("/suspend")
    suspend fun suspendingEndpoint(): Banner {
        delay(10)
        return banner
    }

    @GetMapping("/flow")
    fun flowEndpoint() = flow {
        delay(10)
        emit(banner)
        delay(10)
        emit(banner)
    }

    @GetMapping("/deferred")
    fun deferredEndpoint() = GlobalScope.async {
        delay(10)
        banner
    }

    @GetMapping("/sequential")
    suspend fun sequential(): List<Banner> {
        val banner1 = client
                .get()
                .uri("/suspend")
                .accept(MediaType.APPLICATION_JSON)
                .awaitExchange()
                .awaitBody<Banner>()
        val banner2 = client
                .get()
                .uri("/suspend")
                .accept(MediaType.APPLICATION_JSON)
                .awaitExchange()
                .awaitBody<Banner>()
        return listOf(banner1, banner2)
    }

    @GetMapping("/parallel")
    suspend fun parallel(): List<Banner> = coroutineScope {
        val deferredBanner1: Deferred<Banner> = async {
            client
                    .get()
                    .uri("/suspend")
                    .accept(MediaType.APPLICATION_JSON)
                    .awaitExchange()
                    .awaitBody<Banner>()
        }
        val deferredBanner2: Deferred<Banner> = async {
            client
                    .get()
                    .uri("/suspend")
                    .accept(MediaType.APPLICATION_JSON)
                    .awaitExchange()
                    .awaitBody<Banner>()
        }
        listOf(deferredBanner1.await(), deferredBanner2.await())
    }

    @GetMapping("/error")
    suspend fun error() {
        throw IllegalStateException()
    }

    @GetMapping("/cancel")
    suspend fun cancel() {
        throw CancellationException()
    }

}

也支持使用 @Controller 进行视图渲染。spring-doc.cadn.net.cn

@Controller
class CoroutinesViewController(banner: Banner) {

    @GetMapping("/")
    suspend fun render(model: Model): String {
        delay(10)
        model["banner"] = banner
        return "index"
    }
}

1.8.4. WebFlux.fn

这是一个通过 coRouter { } DSL 定义的协程(Coroutines)路由及其相关处理器的示例。spring-doc.cadn.net.cn

@Configuration
class RouterConfiguration {

    @Bean
    fun mainRouter(userHandler: UserHandler) = coRouter {
        GET("/", userHandler::listView)
        GET("/api/user", userHandler::listApi)
    }
}
class UserHandler(builder: WebClient.Builder) {

    private val client = builder.baseUrl("...").build()

    suspend fun listView(request: ServerRequest): ServerResponse =
            ServerResponse.ok().renderAndAwait("users", mapOf("users" to
            client.get().uri("...").awaitExchange().awaitBody<User>()))

    suspend fun listApi(request: ServerRequest): ServerResponse =
                ServerResponse.ok().contentType(MediaType.APPLICATION_JSON).bodyAndAwait(
                client.get().uri("...").awaitExchange().awaitBody<User>())
}

1.8.5. 事务

协程(Coroutines)的事务支持通过 Spring Framework 5.2 起提供的响应式事务管理的编程式变体来实现。spring-doc.cadn.net.cn

对于挂起函数,提供了 TransactionalOperator.executeAndAwait 扩展。spring-doc.cadn.net.cn

import org.springframework.transaction.reactive.executeAndAwait

class PersonRepository(private val operator: TransactionalOperator) {

    suspend fun initDatabase() = operator.executeAndAwait {
        insertPerson1()
        insertPerson2()
    }

    private suspend fun insertPerson1() {
        // INSERT SQL statement
    }

    private suspend fun insertPerson2() {
        // INSERT SQL statement
    }
}

对于 Kotlin 的 Flow,提供了一个 Flow<T>.transactional 扩展。spring-doc.cadn.net.cn

import org.springframework.transaction.reactive.transactional

class PersonRepository(private val operator: TransactionalOperator) {

    fun updatePeople() = findPeople().map(::updatePerson).transactional(operator)

    private fun findPeople(): Flow<Person> {
        // SELECT SQL statement
    }

    private suspend fun updatePerson(person: Person): Person {
        // UPDATE SQL statement
    }
}

1.9. Kotlin 中的 Spring 项目

本节提供了一些在使用 Kotlin 开发 Spring 项目时值得参考的具体提示和建议。spring-doc.cadn.net.cn

1.9.1. 默认为 final

默认情况下,Kotlin 中的所有类都是 final。 类上的 open 修饰符与 Java 的 final 相反:它允许其他类继承此类。这也适用于成员函数,即它们需要被标记为 open 才能被重写。spring-doc.cadn.net.cn

虽然 Kotlin 面向 JVM 的设计通常与 Spring 框架配合顺畅,但如果忽视了这一特定的 Kotlin 特性,可能会导致应用程序无法启动。这是因为 Spring Bean(例如默认情况下出于技术原因需要在运行时被动态代理的 @Configuration 注解类)通常由 CGLIB 进行代理。解决方法是在每个由 CGLIB 代理的 Spring Bean 类及其成员函数上添加 open 关键字,但这很快会变得繁琐,并且违背了 Kotlin 保持代码简洁和可预测的原则。spring-doc.cadn.net.cn

也可以通过使用 @Configuration(proxyBeanMethods = false) 来避免配置类的 CGLIB 代理。 有关更多详细信息,请参阅 proxyBeanMethods Javadoc

幸运的是,Kotlin 提供了一个 kotlin-spring 插件(kotlin-allopen 插件的预配置版本),可自动为使用以下任一注解或元注解标注的类型及其成员函数打开类:spring-doc.cadn.net.cn

元注解支持意味着使用 @Configuration@Controller@RestController@Service@Repository 注解的类型会自动被注册为 Bean,因为这些注解本身都通过元注解方式标注了 @Componentspring-doc.cadn.net.cn

start.spring.io 默认启用了 kotlin-spring 插件。因此,在实际开发中,你可以像编写 Java Bean 一样编写 Kotlin Bean,而无需额外添加 open 关键字。spring-doc.cadn.net.cn

Spring Framework 文档中的 Kotlin 代码示例并未在类及其成员函数上显式指定 open 关键字。这些示例是为使用 kotlin-allopen 插件的项目编写的,因为这是最常用的配置。

1.9.2. 使用不可变类实例进行持久化

在 Kotlin 中,将只读属性声明在主构造函数中既方便,也被视为一种最佳实践,如下例所示:spring-doc.cadn.net.cn

class Person(val name: String, val age: Int)

您可以选择添加 the data keyword ,以使编译器自动从主构造函数中声明的所有属性派生以下成员:spring-doc.cadn.net.cn

如下例所示,即使 Person 属性是只读的,这也允许轻松修改各个属性:spring-doc.cadn.net.cn

data class Person(val name: String, val age: Int)

val jack = Person(name = "Jack", age = 1)
val olderJack = jack.copy(age = 2)

常见的持久化技术(如 JPA)需要一个默认构造函数,从而阻碍了这种设计方式。幸运的是,针对这种 “默认构造函数困境”存在一种变通方案, 因为 Kotlin 提供了一个 kotlin-jpa 插件,该插件可为标注了 JPA 注解的类生成合成的无参构造函数。spring-doc.cadn.net.cn

如果您需要为其他持久化技术利用此类机制,可以配置 kotlin-noarg 插件。spring-doc.cadn.net.cn

从 Kay 发布版本开始,Spring Data 支持 Kotlin 不可变类实例,并且如果模块使用 Spring Data 对象映射(例如 MongoDB、Redis、Cassandra 等),则不再需要 kotlin-noarg 插件。

1.9.3. 注入依赖

我们的建议是尽量优先使用构造函数注入,并配合 val 声明的只读属性(在可能的情况下使用非空类型),如下例所示:spring-doc.cadn.net.cn

@Component
class YourBean(
    private val mongoTemplate: MongoTemplate,
    private val solrClient: SolrClient
)
具有单个构造函数的类,其参数会自动进行自动装配(autowired)。 因此,在上面所示的示例中,无需显式使用 @Autowired constructor

如果你确实需要使用字段注入,可以使用 lateinit var 结构,如下例所示:spring-doc.cadn.net.cn

@Component
class YourBean {

    @Autowired
    lateinit var mongoTemplate: MongoTemplate

    @Autowired
    lateinit var solrClient: SolrClient
}

1.9.4. 注入配置属性

在 Java 中,你可以使用注解(例如 @Value("${property}"))来注入配置属性。 然而,在 Kotlin 中,$ 是一个保留字符,用于 字符串插值spring-doc.cadn.net.cn

因此,如果你想在 Kotlin 中使用 @Value 注解,需要通过编写 $ 来转义 @Value("\${property}") 字符。spring-doc.cadn.net.cn

如果您使用 Spring Boot,您可能应该使用 @ConfigurationProperties 而不是 @Value 注解。

或者,您可以通过声明以下配置 Bean 来自定义属性占位符的前缀:spring-doc.cadn.net.cn

@Bean
fun propertyConfigurer() = PropertySourcesPlaceholderConfigurer().apply {
    setPlaceholderPrefix("%{")
}

你可以使用配置 Bean 来自定义现有代码(例如 Spring Boot 的 Actuator 或 @LocalServerPort),这些代码使用了 ${…​} 语法,如下例所示:spring-doc.cadn.net.cn

@Bean
fun kotlinPropertyConfigurer() = PropertySourcesPlaceholderConfigurer().apply {
    setPlaceholderPrefix("%{")
    setIgnoreUnresolvablePlaceholders(true)
}

@Bean
fun defaultPropertyConfigurer() = PropertySourcesPlaceholderConfigurer()

1.9.5. 受检异常

Java 和 Kotlin 的异常处理 非常相似,主要区别在于 Kotlin 将所有异常都视为非检查型异常(unchecked exceptions)。然而,在使用代理对象时(例如带有 @Transactional 注解的类或方法),所抛出的检查型异常(checked exceptions)默认会被包装在 UndeclaredThrowableException 中。spring-doc.cadn.net.cn

若要在返回结果中获取与 Java 一致的原始异常,方法应使用 @Throws 注解显式声明所抛出的受检异常(例如 @Throws(IOException::class))。spring-doc.cadn.net.cn

1.9.6. 注解数组属性

Kotlin 注解与 Java 注解在大多数情况下是相似的,但数组属性(在 Spring 中被广泛使用)的行为有所不同。正如 Kotlin 官方文档 中所解释的那样,你可以省略 value 属性名(这与其他属性不同),并将其指定为一个 vararg 参数。spring-doc.cadn.net.cn

要理解其含义,可以以 @RequestMapping(这是 Spring 中最广泛使用的注解之一)为例。该 Java 注解的声明如下:spring-doc.cadn.net.cn

public @interface RequestMapping {

    @AliasFor("path")
    String[] value() default {};

    @AliasFor("value")
    String[] path() default {};

    RequestMethod[] method() default {};

    // ...
}

@RequestMapping 的典型用例是将处理方法映射到特定的路径和 HTTP 方法。在 Java 中,你可以为注解的数组属性指定单个值,该值会自动转换为数组。spring-doc.cadn.net.cn

这就是为什么我们可以写成 @RequestMapping(value = "/toys", method = RequestMethod.GET)@RequestMapping(path = "/toys", method = RequestMethod.GET)spring-doc.cadn.net.cn

然而,在 Kotlin 中,你必须写成 @RequestMapping("/toys", method = [RequestMethod.GET])@RequestMapping(path = ["/toys"], method = [RequestMethod.GET])(使用命名数组属性时需要指定方括号)。spring-doc.cadn.net.cn

针对这个特定的 method 属性(最常见的一种),可以使用快捷注解作为替代方案,例如 @GetMapping@PostMapping 等。spring-doc.cadn.net.cn

如果未指定 @RequestMappingmethod 属性,则将匹配所有 HTTP 方法,而不仅仅是 GET 方法。

1.9.7. 测试

本节介绍 Kotlin 与 Spring Framework 结合使用的测试方法。 推荐的测试框架是 JUnit 5,并搭配 Mockk 进行模拟。spring-doc.cadn.net.cn

如果你正在使用 Spring Boot,请参阅相关的文档
构造函数注入

正如在专用部分中所述, JUnit 5 允许对 Bean 进行构造函数注入,这在 Kotlin 中非常有用, 以便使用val而不是lateinit var。您可以使用 @TestConstructor(autowireMode = AutowireMode.ALL) 来为所有参数启用自动装配。spring-doc.cadn.net.cn

@SpringJUnitConfig(TestConfig::class)
@TestConstructor(autowireMode = AutowireMode.ALL)
class OrderServiceIntegrationTests(val orderService: OrderService,
                                   val customerService: CustomerService) {

    // tests that use the injected OrderService and CustomerService
}
PER_CLASS生命周期

Kotlin 允许你在反引号(`)之间指定有意义的测试函数名称。 从 JUnit 5 开始,Kotlin 测试类可以使用 @TestInstance(TestInstance.Lifecycle.PER_CLASS) 注解来启用测试类的单例实例化,从而允许在非静态方法上使用 @BeforeAll@AfterAll 注解,这与 Kotlin 非常契合。spring-doc.cadn.net.cn

你也可以通过一个包含 PER_CLASS 属性的 junit-platform.properties 文件,将默认行为更改为 junit.jupiter.testinstance.lifecycle.default = per_classspring-doc.cadn.net.cn

以下示例演示了在非静态方法上使用 @BeforeAll@AfterAll 注解:spring-doc.cadn.net.cn

@TestInstance(TestInstance.Lifecycle.PER_CLASS)
class IntegrationTests {

  val application = Application(8181)
  val client = WebClient.create("http://localhost:8181")

  @BeforeAll
  fun beforeAll() {
    application.start()
  }

  @Test
  fun `Find all users on HTML page`() {
    client.get().uri("/users")
        .accept(TEXT_HTML)
        .retrieve()
        .bodyToMono<String>()
        .test()
        .expectNextMatches { it.contains("Foo") }
        .verifyComplete()
  }

  @AfterAll
  fun afterAll() {
    application.stop()
  }
}
类似规范的测试

你可以使用 JUnit 5 和 Kotlin 创建类似规范的测试。 以下示例展示了如何实现这一点:spring-doc.cadn.net.cn

class SpecificationLikeTests {

  @Nested
  @DisplayName("a calculator")
  inner class Calculator {
     val calculator = SampleCalculator()

     @Test
     fun `should return the result of adding the first number to the second number`() {
        val sum = calculator.sum(2, 4)
        assertEquals(6, sum)
     }

     @Test
     fun `should return the result of subtracting the second number from the first number`() {
        val subtract = calculator.subtract(4, 2)
        assertEquals(2, subtract)
     }
  }
}
WebTestClientKotlin 中的类型推断问题

由于一个类型推断问题,您必须使用 Kotlin 的 expectBody 扩展(例如 .expectBody<String>().isEqualTo("toys")), 因为它为 Java API 在 Kotlin 中存在的问题提供了一种变通方案。spring-doc.cadn.net.cn

另请参阅相关的 SPR-16057 问题。spring-doc.cadn.net.cn

1.10. 入门指南

学习如何使用 Kotlin 构建 Spring 应用程序最简单的方法是遵循专门的教程spring-doc.cadn.net.cn

1.10.1. start.spring.io

在 Kotlin 中启动一个新的 Spring Framework 项目的最简单方法是在 start.spring.io 上创建一个新的 Spring Boot 2 项目。spring-doc.cadn.net.cn

1.10.2. 选择 Web 风格

Spring Framework 现在包含两个不同的 Web 堆栈:Spring MVCSpring WebFluxspring-doc.cadn.net.cn

如果你希望构建能够处理延迟、长连接、流式场景的应用程序,或者希望使用 Web 函数式 Kotlin DSL,推荐使用 Spring WebFlux。spring-doc.cadn.net.cn

对于其他使用场景,特别是当你使用 JPA 等阻塞式技术时,推荐选择 Spring MVC 及其基于注解的编程模型。spring-doc.cadn.net.cn

1.11. 资源

我们推荐以下资源,供学习如何使用 Kotlin 和 Spring 框架构建应用程序的开发者参考:spring-doc.cadn.net.cn

1.11.1. 示例

以下 GitHub 项目提供了可供学习甚至可能进一步扩展的示例:spring-doc.cadn.net.cn

2. Apache Groovy

Groovy 是一种功能强大、可选类型且动态的语言,同时具备静态类型和静态编译能力。它语法简洁,并能与任何现有的 Java 应用程序无缝集成。spring-doc.cadn.net.cn

Spring 框架提供了一个专用的 ApplicationContext,支持基于 Groovy 的 Bean 定义 DSL。更多详情,请参阅 Groovy Bean 定义 DSLspring-doc.cadn.net.cn

对 Groovy 的进一步支持(包括使用 Groovy 编写的 Bean、可刷新的脚本 Bean 等)请参见 动态语言支持spring-doc.cadn.net.cn

3. 动态语言支持

Spring 提供了全面的支持,用于在 Spring 中使用通过动态语言(例如 Groovy)定义的类和对象。该支持允许你使用受支持的动态语言编写任意数量的类,并由 Spring 容器透明地实例化、配置和依赖注入所生成的对象。spring-doc.cadn.net.cn

Spring 的脚本支持主要面向 Groovy 和 BeanShell。除了这些明确支持的语言之外,Spring 还支持 JSR-223 脚本机制(从 Spring 4.2 起),可用于集成任何符合 JSR-223 标准的语言提供者,例如 JRuby。spring-doc.cadn.net.cn

你可以在应用场景中找到这些动态语言支持可立即发挥作用的完整示例。spring-doc.cadn.net.cn

3.1. 第一个示例

本章的大部分内容旨在详细描述动态语言支持。在深入探讨动态日消息支持的各种细节之前,我们先来看一个用动态语言定义的 Bean 的快速示例。此示例中的动态语言是 Groovy。(该示例基于 Spring 测试套件。如果你想查看其他受支持语言的等效示例,请参阅源代码。)spring-doc.cadn.net.cn

下一个示例展示了 Messenger 接口,Groovy bean 将实现该接口。请注意,此接口是用纯 Java 定义的。那些被注入了 Messenger 引用的依赖对象并不知道其底层实现实际上是一个 Groovy 脚本。以下代码清单展示了 Messenger 接口:spring-doc.cadn.net.cn

package org.springframework.scripting;

public interface Messenger {

    String getMessage();
}

以下示例定义了一个依赖于 Messenger 接口的类:spring-doc.cadn.net.cn

package org.springframework.scripting;

public class DefaultBookingService implements BookingService {

    private Messenger messenger;

    public void setMessenger(Messenger messenger) {
        this.messenger = messenger;
    }

    public void processBooking() {
        // use the injected Messenger object...
    }
}

以下示例使用 Groovy 实现了 Messenger 接口:spring-doc.cadn.net.cn

// from the file 'Messenger.groovy'
package org.springframework.scripting.groovy;

// import the Messenger interface (written in Java) that is to be implemented
import org.springframework.scripting.Messenger

// define the implementation in Groovy
class GroovyMessenger implements Messenger {

    String message
}

要使用自定义的动态语言标签来定义由动态语言支持的 Bean,您需要在 Spring XML 配置文件的顶部包含 XML Schema 声明。此外,您还需要使用 Spring 的 ApplicationContext 实现作为您的 IoC 容器。虽然也可以在普通的 BeanFactory 实现中使用由动态语言支持的 Bean,但您必须自行管理 Spring 内部组件的连接工作。spring-doc.cadn.net.cn

有关基于 schema 的配置的更多信息,请参阅基于 XML Schema 的配置spring-doc.cadn.net.cn

最后,以下示例展示了用于将 Groovy 定义的 Messenger 实现注入到 DefaultBookingService 类实例中的 bean 定义:spring-doc.cadn.net.cn

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:lang="http://www.springframework.org/schema/lang"
    xsi:schemaLocation="
        http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/lang https://www.springframework.org/schema/lang/spring-lang.xsd">

    <!-- this is the bean definition for the Groovy-backed Messenger implementation -->
    <lang:groovy id="messenger" script-source="classpath:Messenger.groovy">
        <lang:property name="message" value="I Can Do The Frug" />
    </lang:groovy>

    <!-- an otherwise normal bean that will be injected by the Groovy-backed Messenger -->
    <bean id="bookingService" class="x.y.DefaultBookingService">
        <property name="messenger" ref="messenger" />
    </bean>

</beans>

现在,bookingService bean(一个 DefaultBookingService)可以像平常一样使用其私有的 messenger 成员变量,因为注入到其中的 Messenger 实例就是一个 Messenger 实例。这里并没有什么特殊之处——只是普通的 Java 和普通的 Groovy。spring-doc.cadn.net.cn

希望前面的 XML 片段已经不言自明,但如果还不清楚,也无需过分担心。 请继续阅读,以深入了解上述配置背后的原因和细节。spring-doc.cadn.net.cn

3.2. 定义由动态语言支持的 Bean

本节详细描述了如何在任何受支持的动态语言中定义由 Spring 管理的 Bean。spring-doc.cadn.net.cn

请注意,本章并不试图解释所支持的动态语言的语法和惯用法。例如,如果你想使用 Groovy 编写应用程序中的某些类,我们假定你已经掌握了 Groovy。如果你需要进一步了解这些动态语言本身,请参阅本章末尾的更多资源spring-doc.cadn.net.cn

3.2.1. 通用概念

使用动态语言支持的 Bean 所涉及的步骤如下:spring-doc.cadn.net.cn

  1. 为动态语言源代码编写测试(自然而然地)。spring-doc.cadn.net.cn

  2. 然后编写动态语言本身的源代码。spring-doc.cadn.net.cn

  3. 通过在 XML 配置中使用相应的 <lang:language/> 元素来定义由动态语言支持的 Bean(您也可以通过编程方式使用 Spring API 来定义此类 Bean,但需要查阅源代码以了解具体实现方法,因为本章未涵盖此类高级配置)。请注意,这是一个迭代步骤:每个动态语言源文件至少需要一个 Bean 定义(尽管多个 Bean 定义可以引用同一个源文件)。spring-doc.cadn.net.cn

前两个步骤(测试和编写动态语言源文件)超出了本章的范围。请参阅您所选动态语言的语言规范和参考手册,并着手开发您的动态语言源文件。不过,您首先应阅读完本章其余部分,因为 Spring 对动态语言的支持确实对您的动态语言源文件内容做出了一些(较小的)假设。spring-doc.cadn.net.cn

<lang:language/> 元素

上一节前面部分中列出的最后一步 涉及定义动态语言支持的 Bean 定义,每个您想要配置的 Bean 各定义一个(这与普通的 JavaBean 配置并无不同)。 然而,您无需指定由容器实例化和配置的类的完整限定类名, 而是可以使用 <lang:language/> 元素来定义动态语言支持的 Bean。spring-doc.cadn.net.cn

每种受支持的语言都有一个对应的 <lang:language/> 元素:spring-doc.cadn.net.cn

可用于配置的确切属性和子元素取决于该 bean 是用哪种语言定义的(本章后面的语言特定部分将对此进行详细说明)。spring-doc.cadn.net.cn

可刷新 Bean

Spring 对动态语言支持最具吸引力(甚至可能是唯一最具吸引力)的价值之一就是“可刷新 Bean”特性。spring-doc.cadn.net.cn

可刷新的 Bean 是一种由动态语言支持的 Bean。通过少量配置,这种由动态语言支持的 Bean 就能监控其底层源文件资源的变化,并在动态语言源文件发生更改时(例如,当您在文件系统中编辑并保存该文件时)自动重新加载自身。spring-doc.cadn.net.cn

这使您能够将任意数量的动态语言源文件作为应用程序的一部分进行部署,配置 Spring 容器以创建由动态语言源文件支持的 Bean(使用本章所述的机制),并在之后(当需求发生变化或某些其他外部因素介入时)编辑动态语言源文件,使其所做的任何更改都能立即反映在由该已修改的动态语言源文件所支持的 Bean 中。无需关闭正在运行的应用程序(对于 Web 应用程序也无需重新部署)。经过这样修改的、由动态语言支持的 Bean 会自动从已更改的动态语言源文件中获取新的状态和逻辑。spring-doc.cadn.net.cn

此功能默认处于关闭状态。

现在,我们可以看一个示例,了解开始使用可刷新 Bean 有多么简单。要启用可刷新 Bean 功能,您必须在 Bean 定义的 <lang:language/> 元素上精确指定一个额外的属性。因此,如果我们继续沿用本章前面提到的示例,下面的示例展示了我们需要对 Spring XML 配置进行哪些更改,以实现可刷新 Bean:spring-doc.cadn.net.cn

<beans>

    <!-- this bean is now 'refreshable' due to the presence of the 'refresh-check-delay' attribute -->
    <lang:groovy id="messenger"
            refresh-check-delay="5000" <!-- switches refreshing on with 5 seconds between checks -->
            script-source="classpath:Messenger.groovy">
        <lang:property name="message" value="I Can Do The Frug" />
    </lang:groovy>

    <bean id="bookingService" class="x.y.DefaultBookingService">
        <property name="messenger" ref="messenger" />
    </bean>

</beans>

这确实就是你需要做的全部了。refresh-check-delay bean 定义中指定的 messenger 属性表示在底层动态语言源文件发生更改后,经过多少毫秒刷新该 bean。 你可以通过为 refresh-check-delay 属性赋予一个负值来关闭刷新行为。请注意,默认情况下,刷新行为是禁用的。如果你不希望启用刷新行为,请不要定义该属性。spring-doc.cadn.net.cn

如果我们随后运行以下应用程序,就可以测试可刷新功能。 (请原谅接下来这段代码中为了“暂停程序执行”而采取的繁琐操作。) 其中的 System.in.read() 调用只是为了在程序执行过程中暂停, 以便你(在此场景中作为开发者)可以去修改底层的动态语言源文件, 这样当程序恢复执行时,基于动态语言的 Bean 就会触发刷新。spring-doc.cadn.net.cn

以下列表展示了该示例应用程序:spring-doc.cadn.net.cn

import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import org.springframework.scripting.Messenger;

public final class Boot {

    public static void main(final String[] args) throws Exception {
        ApplicationContext ctx = new ClassPathXmlApplicationContext("beans.xml");
        Messenger messenger = (Messenger) ctx.getBean("messenger");
        System.out.println(messenger.getMessage());
        // pause execution while I go off and make changes to the source file...
        System.in.read();
        System.out.println(messenger.getMessage());
    }
}

因此,就本示例而言,假设对所有 getMessage() 实现类的 Messenger 方法的调用都需要进行修改,使得返回的消息被双引号包围。以下代码清单展示了当程序执行暂停时,您(开发者)应对 Messenger.groovy 源文件所做的修改:spring-doc.cadn.net.cn

package org.springframework.scripting

class GroovyMessenger implements Messenger {

    private String message = "Bingo"

    public String getMessage() {
        // change the implementation to surround the message in quotes
        return "'" + this.message + "'"
    }

    public void setMessage(String message) {
        this.message = message
    }
}

程序运行时,在输入暂停之前的输出将是 I Can Do The Frug。 在对源文件进行修改并保存后,程序继续执行,此时调用基于动态语言的 getMessage() 实现的 Messenger 方法所得到的结果是 'I Can Do The Frug'(请注意其中额外添加的引号)。spring-doc.cadn.net.cn

如果脚本的更改发生在 refresh-check-delay 值所设定的时间窗口内,则不会触发刷新。实际上,对脚本的更改并不会立即生效,而是在调用基于动态语言的 Bean 的某个方法时才会被检测到。只有当调用基于动态语言的 Bean 的方法时,它才会检查其底层脚本源是否发生了更改。任何与刷新脚本相关的异常(例如遇到编译错误或发现脚本文件已被删除)都会导致一个致命异常被传播到调用代码中。spring-doc.cadn.net.cn

前面描述的可刷新 Bean 行为不适用于使用 <lang:inline-script/> 元素定义的动态语言源文件(参见内联动态语言源文件)。此外,该行为仅适用于那些底层源文件的更改能够被实际检测到的 Bean(例如,通过检查文件系统中动态语言源文件的最后修改日期的代码)。spring-doc.cadn.net.cn

内联动态语言源文件

动态语言支持还可以处理直接嵌入在 Spring Bean 定义中的动态语言源文件。更具体地说,<lang:inline-script/> 元素允许你在 Spring 配置文件中直接定义动态语言源代码。下面的示例可以更清楚地说明内联脚本功能的工作方式:spring-doc.cadn.net.cn

<lang:groovy id="messenger">
    <lang:inline-script>

package org.springframework.scripting.groovy;

import org.springframework.scripting.Messenger

class GroovyMessenger implements Messenger {
    String message
}

    </lang:inline-script>
    <lang:property name="message" value="I Can Do The Frug" />
</lang:groovy>

如果我们暂且不考虑在 Spring 配置文件中定义动态语言源代码是否属于良好实践这一问题,<lang:inline-script/> 元素在某些场景下仍然很有用。例如,我们可能希望快速为 Spring MVC 的 Validator 添加一个 Spring Controller 实现。使用内联源代码只需片刻即可完成。(参见脚本化验证器中的示例。)spring-doc.cadn.net.cn

理解在动态语言支持的 Bean 上下文中的构造函数注入

关于 Spring 的动态语言支持,有一件非常重要的事情需要注意:目前你无法为基于动态语言的 bean 提供构造函数参数(因此,基于动态语言的 bean 不支持构造函数注入)。为了清晰地表明这种对构造函数和属性的特殊处理方式,以下代码与配置的混合示例是无效的:spring-doc.cadn.net.cn

一种行不通的方法
// from the file 'Messenger.groovy'
package org.springframework.scripting.groovy;

import org.springframework.scripting.Messenger

class GroovyMessenger implements Messenger {

    GroovyMessenger() {}

    // this constructor is not available for Constructor Injection
    GroovyMessenger(String message) {
        this.message = message;
    }

    String message

    String anotherMessage
}
<lang:groovy id="badMessenger"
    script-source="classpath:Messenger.groovy">
    <!-- this next constructor argument will not be injected into the GroovyMessenger -->
    <!-- in fact, this isn't even allowed according to the schema -->
    <constructor-arg value="This will not work" />

    <!-- only property values are injected into the dynamic-language-backed object -->
    <lang:property name="anotherMessage" value="Passed straight through to the dynamic-language-backed object" />

</lang>

实际上,这一限制并不像初看起来那么严重,因为绝大多数开发者都倾向于使用 setter 注入(至于这是否是一件好事,我们留待日后讨论)。spring-doc.cadn.net.cn

3.2.2. Groovy Bean

本节介绍如何在 Spring 中使用 Groovy 定义的 Bean。spring-doc.cadn.net.cn

Groovy 官网首页包含以下描述:spring-doc.cadn.net.cn

“Groovy 是一种面向 Java 2 平台的敏捷动态语言,它具备许多类似 Python、Ruby 和 Smalltalk 等语言广受欢迎的特性,并以类似 Java 的语法提供给 Java 开发者使用。”spring-doc.cadn.net.cn

如果你是从本章开头一直读到这里,那么你已经看过一个由 Groovy 动态语言支持的 Bean 的示例。现在再来看另一个示例(同样取自 Spring 测试套件中的例子):spring-doc.cadn.net.cn

package org.springframework.scripting;

public interface Calculator {

    int add(int x, int y);
}

以下示例使用 Groovy 实现了 Calculator 接口:spring-doc.cadn.net.cn

// from the file 'calculator.groovy'
package org.springframework.scripting.groovy

class GroovyCalculator implements Calculator {

    int add(int x, int y) {
        x + y
    }
}

以下 Bean 定义使用了在 Groovy 中定义的计算器:spring-doc.cadn.net.cn

<!-- from the file 'beans.xml' -->
<beans>
    <lang:groovy id="calculator" script-source="classpath:calculator.groovy"/>
</beans>

最后,以下小型应用程序演示了前述配置的使用:spring-doc.cadn.net.cn

package org.springframework.scripting;

import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

public class Main {

    public static void main(String[] args) {
        ApplicationContext ctx = new ClassPathXmlApplicationContext("beans.xml");
        Calculator calc = ctx.getBean("calculator", Calculator.class);
        System.out.println(calc.add(2, 8));
    }
}

运行上述程序所产生的输出结果(毫不意外地)是10。 (如需更有趣的示例,请参阅动态语言展示项目以了解更复杂的例子,或参见本章稍后部分的场景示例。)spring-doc.cadn.net.cn

每个 Groovy 源文件中不应定义多个类。尽管这在 Groovy 中是完全合法的,但(可以说)这是一种不良实践。为了保持一致的编码风格,您应当(按照 Spring 团队的观点)遵循标准的 Java 约定,即每个源文件只包含一个(public)类。spring-doc.cadn.net.cn

使用回调自定义 Groovy 对象

GroovyObjectCustomizer 接口是一个回调接口,允许你在创建基于 Groovy 的 Bean 的过程中插入额外的创建逻辑。例如,该接口的实现可以调用任何所需的初始化方法、设置某些默认属性值,或指定一个自定义的 MetaClass。以下代码展示了 GroovyObjectCustomizer 接口的定义:spring-doc.cadn.net.cn

public interface GroovyObjectCustomizer {

    void customize(GroovyObject goo);
}

Spring 框架会实例化一个由 Groovy 支持的 Bean 实例,然后将创建好的 GroovyObject 传递给指定的 GroovyObjectCustomizer(如果已定义)。你可以对提供的 GroovyObject 引用执行任意操作。我们预计大多数用户希望通过此回调设置一个自定义的 MetaClass,以下示例展示了如何实现这一点:spring-doc.cadn.net.cn

public final class SimpleMethodTracingCustomizer implements GroovyObjectCustomizer {

    public void customize(GroovyObject goo) {
        DelegatingMetaClass metaClass = new DelegatingMetaClass(goo.getMetaClass()) {

            public Object invokeMethod(Object object, String methodName, Object[] arguments) {
                System.out.println("Invoking '" + methodName + "'.");
                return super.invokeMethod(object, methodName, arguments);
            }
        };
        metaClass.initialize();
        goo.setMetaClass(metaClass);
    }

}

在Groovy中进行元编程的全面讨论超出了Spring参考手册的范围。请参阅Groovy参考手册的相关章节,或在网上进行搜索。有许多文章都探讨了这一主题。实际上,如果你使用Spring的命名空间支持,使用GroovyObjectCustomizer是非常简单的,如下例所示:spring-doc.cadn.net.cn

<!-- define the GroovyObjectCustomizer just like any other bean -->
<bean id="tracingCustomizer" class="example.SimpleMethodTracingCustomizer"/>

    <!-- ... and plug it into the desired Groovy bean via the 'customizer-ref' attribute -->
    <lang:groovy id="calculator"
        script-source="classpath:org/springframework/scripting/groovy/Calculator.groovy"
        customizer-ref="tracingCustomizer"/>

如果您不使用 Spring 的命名空间支持,仍然可以使用 GroovyObjectCustomizer 功能,如下例所示:spring-doc.cadn.net.cn

<bean id="calculator" class="org.springframework.scripting.groovy.GroovyScriptFactory">
    <constructor-arg value="classpath:org/springframework/scripting/groovy/Calculator.groovy"/>
    <!-- define the GroovyObjectCustomizer (as an inner bean) -->
    <constructor-arg>
        <bean id="tracingCustomizer" class="example.SimpleMethodTracingCustomizer"/>
    </constructor-arg>
</bean>

<bean class="org.springframework.scripting.support.ScriptFactoryPostProcessor"/>
您也可以在与 Spring 的 CompilationCustomizer 相同的位置指定一个 Groovy ImportCustomizer(例如 CompilerConfiguration), 甚至可以指定一个完整的 Groovy GroovyObjectCustomizer 对象。此外,您可以在 GroovyClassLoader 级别为您的 Bean 设置一个带有自定义配置的公共 ConfigurableApplicationContext.setClassLoader; 这同样会导致共享 GroovyClassLoader 的使用,因此在存在大量脚本化 Bean 的情况下推荐采用此方式(避免为每个 Bean 创建独立的 GroovyClassLoader 实例)。

3.2.3. BeanShell Beans

本节介绍如何在 Spring 中使用 BeanShell bean。spring-doc.cadn.net.cn

BeanShell is a small, free, embeddable Java source interpreter with dynamic language
features, written in Java. BeanShell dynamically runs standard Java syntax and
extends it with common scripting conveniences such as loose types, commands, and method
closures like those in Perl and JavaScript.

与 Groovy 不同,使用 BeanShell 支持的 Bean 定义需要一些(少量的)额外配置。Spring 中对 BeanShell 动态语言的支持实现方式非常有趣,因为 Spring 会创建一个 JDK 动态代理,该代理实现 script-interfaces 元素的 <lang:bsh> 属性值中指定的所有接口(这就是为什么你必须在该属性值中至少提供一个接口,并且在使用 BeanShell 支持的 Bean 时必须面向接口编程)。这意味着,对 BeanShell 支持的对象的每一次方法调用都会经过 JDK 动态代理的调用机制。spring-doc.cadn.net.cn

现在,我们可以展示一个完整且可运行的示例,该示例使用基于 BeanShell 的 bean 来实现本章前面定义的 Messenger 接口。我们再次展示 Messenger 接口的定义:spring-doc.cadn.net.cn

package org.springframework.scripting;

public interface Messenger {

    String getMessage();
}

以下示例展示了 Messenger 接口的 BeanShell “实现”(此处我们对该术语的使用较为宽松):spring-doc.cadn.net.cn

String message;

String getMessage() {
    return message;
}

void setMessage(String aMessage) {
    message = aMessage;
}

以下示例展示了定义上述“类”的一个“实例”的 Spring XML 配置(再次说明,这里我们对这些术语的使用非常宽松):spring-doc.cadn.net.cn

<lang:bsh id="messageService" script-source="classpath:BshMessenger.bsh"
    script-interfaces="org.springframework.scripting.Messenger">

    <lang:property name="message" value="Hello World!" />
</lang:bsh>

请参阅使用场景,了解一些你可能希望使用基于 BeanShell 的 bean 的场景。spring-doc.cadn.net.cn

3.3. 场景

在脚本语言中定义由 Spring 管理的 Bean 有许多不同且多样的适用场景。本节将介绍 Spring 动态语言支持的两个可能用例。spring-doc.cadn.net.cn

3.3.1. 基于脚本的 Spring MVC 控制器

有一类可以从使用动态语言支持的 Bean 中获益的类,那就是 Spring MVC 控制器。在纯 Spring MVC 应用程序中,Web 应用的导航流程在很大程度上由封装在 Spring MVC 控制器中的代码所决定。当 Web 应用的导航流程和其他表示层逻辑需要更新以应对支持问题或不断变化的业务需求时,通过编辑一个或多个动态语言源文件,并让这些更改立即反映到正在运行的应用程序状态中,可能会更容易实现所需的变更。spring-doc.cadn.net.cn

请记住,在 Spring 等项目所倡导的轻量级架构模型中,通常目标是让表示层非常薄,而将应用程序中所有核心业务逻辑都放在领域层和服务层的类中。将 Spring MVC 控制器开发为由动态语言支持的 Bean,可以让你通过编辑和保存文本文件来更改表示层逻辑。对这些动态语言源文件所做的任何更改(取决于配置)都会自动反映到由动态语言源文件支持的 Bean 中。spring-doc.cadn.net.cn

要实现对动态语言支持的 Bean 所做的任何更改的自动“拾取”,您必须启用“可刷新 Bean”(refreshable beans)功能。有关此功能的完整说明,请参阅可刷新 Bean

以下示例展示了使用 Groovy 动态语言实现的 org.springframework.web.servlet.mvc.Controllerspring-doc.cadn.net.cn

// from the file '/WEB-INF/groovy/FortuneController.groovy'
package org.springframework.showcase.fortune.web

import org.springframework.showcase.fortune.service.FortuneService
import org.springframework.showcase.fortune.domain.Fortune
import org.springframework.web.servlet.ModelAndView
import org.springframework.web.servlet.mvc.Controller

import javax.servlet.http.HttpServletRequest
import javax.servlet.http.HttpServletResponse

class FortuneController implements Controller {

    @Property FortuneService fortuneService

    ModelAndView handleRequest(HttpServletRequest request,
            HttpServletResponse httpServletResponse) {
        return new ModelAndView("tell", "fortune", this.fortuneService.tellFortune())
    }
}
<lang:groovy id="fortune"
        refresh-check-delay="3000"
        script-source="/WEB-INF/groovy/FortuneController.groovy">
    <lang:property name="fortuneService" ref="fortuneService"/>
</lang:groovy>

3.3.2. 脚本验证器

在 Spring 应用开发中,另一个可能受益于动态语言支持的 Bean 所提供的灵活性的领域是验证(validation)。与使用常规 Java 相比,使用松散类型的动态语言(该语言可能还支持内联正则表达式)来表达复杂的验证逻辑可能会更加简便。spring-doc.cadn.net.cn

同样,将验证器开发为基于动态语言的 Bean,可让您通过编辑并保存一个简单的文本文件来更改验证逻辑。任何此类更改(取决于配置)都会自动反映在正在运行的应用程序的执行中,而无需重新启动应用程序。spring-doc.cadn.net.cn

要实现对动态语言支持的 Bean 所做的任何更改的自动“拾取”,您必须启用“可刷新 Bean”(refreshable beans)功能。有关此功能的完整详细说明,请参阅可刷新 Bean

以下示例展示了一个使用 Groovy 动态语言实现的 Spring org.springframework.validation.Validator(有关 core.html#validator 接口的讨论,请参见使用 Spring 的 Validator 接口进行验证):spring-doc.cadn.net.cn

import org.springframework.validation.Validator
import org.springframework.validation.Errors
import org.springframework.beans.TestBean

class TestBeanValidator implements Validator {

    boolean supports(Class clazz) {
        return TestBean.class.isAssignableFrom(clazz)
    }

    void validate(Object bean, Errors errors) {
        if(bean.name?.trim()?.size() > 0) {
            return
        }
        errors.reject("whitespace", "Cannot be composed wholly of whitespace.")
    }
}

3.4. 更多详情

最后一节包含了一些与动态语言支持相关的附加细节。spring-doc.cadn.net.cn

3.4.1. AOP — 通知脚本化 Bean

你可以使用 Spring AOP 框架来为脚本化 Bean 提供通知(advice)。实际上,Spring AOP 框架并不知道被通知的 Bean 可能是一个脚本化 Bean,因此你所使用(或打算使用)的所有 AOP 用例和功能都可以与脚本化 Bean 一起正常工作。当你为脚本化 Bean 提供通知时,不能使用基于类的代理,而必须使用基于接口的代理spring-doc.cadn.net.cn

您不仅限于对脚本化 Bean 进行通知。您还可以使用受支持的动态语言直接编写切面本身,并使用此类 Bean 来通知其他 Spring Bean。 不过,这确实属于动态语言支持的高级用法。spring-doc.cadn.net.cn

3.4.2. 作用域

如果这一点尚不明显,脚本化 Bean 的作用域可以像其他任何 Bean 一样进行配置。各种 scope 元素上的 <lang:language/> 属性允许你控制底层脚本化 Bean 的作用域,其用法与普通 Bean 相同。(默认作用域是singleton,与“普通”Bean 一致。)spring-doc.cadn.net.cn

以下示例使用 scope 属性定义一个作用域为 原型(prototype) 的 Groovy Bean:spring-doc.cadn.net.cn

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:lang="http://www.springframework.org/schema/lang"
    xsi:schemaLocation="
        http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/lang https://www.springframework.org/schema/lang/spring-lang.xsd">

    <lang:groovy id="messenger" script-source="classpath:Messenger.groovy" scope="prototype">
        <lang:property name="message" value="I Can Do The RoboCop" />
    </lang:groovy>

    <bean id="bookingService" class="x.y.DefaultBookingService">
        <property name="messenger" ref="messenger" />
    </bean>

</beans>

有关 Spring 框架中作用域支持的完整讨论,请参见Bean 作用域一节,该节位于IoC 容器中。spring-doc.cadn.net.cn

3.4.3.langXML 模式

Spring XML 配置中的 lang 元素用于将使用动态语言(例如 Groovy 或 BeanShell)编写的对象暴露为 Spring 容器中的 bean。spring-doc.cadn.net.cn

这些元素(以及动态语言支持)在动态语言支持一节中有全面介绍。有关此支持功能及lang元素的完整详情,请参阅该部分。spring-doc.cadn.net.cn

要在 lang 命名空间中使用这些元素,您需要在 Spring XML 配置文件的顶部包含以下前导声明。以下代码片段中的文本引用了正确的 schema,从而使 lang 命名空间中的标签对您可用:spring-doc.cadn.net.cn

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:lang="http://www.springframework.org/schema/lang"
    xsi:schemaLocation="
        http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/lang https://www.springframework.org/schema/lang/spring-lang.xsd">

    <!-- bean definitions here -->

</beans>

3.5. 更多资源

以下链接指向本章中提到的各种动态语言的更多相关资源:spring-doc.cadn.net.cn