本站(springdoc.cn)中的内容来源于 spring.io ,原始版权归属于 spring.io。由 springdoc.cn 进行翻译,整理。可供个人学习、研究,未经许可,不得进行任何转载、商用或与之相关的行为。 商标声明:Spring 是 Pivotal Software, Inc. 在美国以及其他国家的商标。

1. Kotlin

Kotlin 是一种静态类型的语言,以JVM(和其他平台)为目标,可以编写简洁优雅的代码,同时与现有的Java编写的库有非常好的 互操作性

Spring框架为Kotlin提供了一流的支持,让开发者在编写Kotlin应用程序时几乎就像Spring框架是一个本地的Kotlin框架一样。除了Java之外,参考文档的大部分代码样本都是用Kotlin提供的。

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

如果你需要支持,请随时加入 Kotlin Slack 的 #spring 频道,或者在 Stackoverflow 上以 springkotlin 为标签提问。

1.1. 要求

Spring Framework 支持 Kotlin 1.3+,要求 classpath 上有 kotlin-stdlib(或其变体之一,如 kotlin-stdlib-jdk8)和 kotlin-reflect。如果你在 start.springboot.io 上启动一个Kotlin项目,它们会被默认提供。

目前还不支持 Kotlin 内联类(inline classes)
Jackson Kotlin module 对于使用Jackson的Kotlin类的序列化或反序列化JSON数据是必需的,所以如果你有这样的需要,请确保将 com.fasterxml.jackson.module:jackson-module-kotlin 依赖添加到你的项目。当在classpath中找到它时,它会自动注册。

1.2. 扩展

Kotlin extensions 提供了用额外功能扩展现有类的能力。Spring框架的Kotlin APIs使用这些扩展来为现有的Spring APIs添加新的Kotlin专用便利。

Spring Framework KDoc API 列出并记录了所有可用的Kotlin扩展和DSLs。

请记住,Kotlin 扩展需要被导入才能使用。例如,这意味着 GenericApplicationContext.registerBean Kotlin 扩展只有在 org.springframework.context.support.registerBean 被导入后才能使用。也就是说,与静态导入类似,在大多数情况下,IDE应该自动建议导入。

例如, Kotlin reified type parameters 为JVM 泛型类型擦除 提供了一种变通方法,而Spring框架提供了一些扩展来利用这一特性。这使得Kotlin API的 RestTemplate、Spring WebFlux 的新 WebClient 以及其他各种API都能得到更好的发挥。

其他库,如 Reactor 和 Spring Data,也为它们的API提供了Kotlin扩展,因此总体上有更好的Kotlin开发体验。

要在Java中检索一个 User 对象的列表,你通常会写如下:

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

有了 Kotlin 和 Spring 框架的扩展,你可以改写如下:

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

和Java一样,Kotlin中的 users 是强类型的,但Kotlin巧妙的类型推理使得语法更短。

1.3. Null-safety

Kotlin的关键特性之一是 null-safety,它在编译时干净地处理 null 值,而不是在运行时碰到著名的 NullPointerException。这使得应用程序通过 nullability 声明和表达 "值或无值" 语义变得更加安全,而不需要支付诸如 Optional 等 wrapper 的费用。(Kotlin 允许使用带有 nullable 值的功能结构。请看这个 关于Kotlin null-safety的综合指南)。

尽管Java不允许你在其类型系统中表达 null-safety,但Spring框架通过在 org.springframework.lang 包中声明的工具友好注解,为 整个Spring框架API提供了null-safety。默认情况下,Kotlin中使用的 Java API 中的类型被识别为 platform type,对于这些 platform type, null 值检查是放松的。Kotlin 对JSR-305注解 和Spring nullability注解的支持为Kotlin开发者提供了整个Spring框架API的null-safety,其优点是可以在编译时处理 null 相关问题。

Reactor 或 Spring Data 等库提供了 null-safe 的API来利用这一特性。

你可以通过添加 -Xjsr305 编译器标志和以下选项来配置JSR-305检查: -Xjsr305={strict|warn|ignore}

对于 1.1 以上版本的 kotlin,默认行为与 -Xjsr305=warn 相同。为了让从Spring API推断的Kotlin类型考虑到 Spring 框架 API的 null-safety,需要使用 strict 值,但在使用时应注意 Spring API 的 nullability 声明可能会发生变化,甚至在小版本之间,未来可能会增加更多的检查。

目前还不支持泛型参数、varargs 和数组元素的 nullability,但在即将发布的版本中应该会支持。请看这个 讨论,了解最新的信息。

1.4. 类和接口

Spring 框架支持各种 Kotlin 结构,例如通过主构造函数实例化 Kotlin 类,不可变的类数据绑定,以及带有默认值的函数可选参数。

Kotlin的参数名称是通过专门的 KotlinReflectionParameterNameDiscoverer 来识别的,它可以在编译过程中不需要启用Java 8 -parameters 编译器标志就可以找到接口方法的参数名称。(为了完整起见,我们还是建议在运行Kotlin编译器时使用其 -java-parameters 标志,以获得标准的Java参数曝光。)

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

1.5. 注解

Spring框架还利用了 KKotlin 的 null-safety 来确定一个HTTP参数是否是必需的,而不需要明确地定义 required 属性。这意味着 @RequestParam name: String? 被视为非必需,反之,@RequestParam name: String 被视为必需。这个特性在 Spring Messaging 的 @Header 注解上也得到了支持。

例如,@Autowired lateinit var thing: Thing 意味着必须在 application context 中注册一个 Thing 类型的 bean,而 @Autowired lateinit var thing: Thing? 如果这样的 bean 不存在,则不会引发错误。

遵循同样的原则,@Bean fun play(toy: Toy, car: Car?) = Baz(toy, Car) 意味着必须在 application context 中注册一个 Toy 类型的bean,而 Car 类型的 bean 可能存在也可能不存在。同样的行为也适用于自动注入的构造函数参数。

如果你在有属性或 primary constructor 参数的类上使用Bean验证,你可能需要使用 annotation use-site targets,如 @field:NotNull@get:Size(min=5, max=15),如 Stack Overflow 的这个回应 中所述。

1.6. Bean 定义(Definition) DSL

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

在Java中,你可以,例如,写下以下内容:

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扩展,你可以改写如下:

class Foo

class Bar(private val foo: Foo)

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

Bar 类有一个单一的构造函数时,你甚至可以只指定bean类,构造函数参数将按类型自动装配:

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

为了允许更多的声明性方法和更简洁的语法,Spring框架提供了一个 Kotlin bean definition DSL 它通过一个简洁的声明性API声明了一个 ApplicationContextInitializer,让你处理 profiles 和 Environment,以自定义 bean 的注册方法。

在下面的例子中,注意到:

  • 类型推理通常允许避免为 Bean 引用指定类型,如 ref("bazBean")

  • 可以使用 Kotlin 顶层函数,使用可调用的引用来声明Bean,如本例中的 Bean(::myRouter)

  • 当指定 bean<Bar>()bean(::myRouter) 时,参数是按类型自动装配的。

  • 只有当 foobar profile 处于 active 状态时,FooBar Bean才会被注册。

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() 函数在 application context 上注册 Bean,如下例所示:

val context = GenericApplicationContext().apply {
    myBeans.initialize(this)
    refresh()
}
Spring Boot 基于 JavaConfig, 尚未提供对 函数式 bean definition 的具体支持,但你可以通过 Spring Boot 的 ApplicationContextInitializer 支持实验性地使用函数式 bean definition。请看 这个 Stack Overflow 答案,了解更多细节和最新的信息。另请参见 Spring Fu 孵化器 中开发的实验性Kofu DSL。

1.7. Web

1.7.1. 路由(Router) DSL

Spring 框架带有一个 Kotlin 路由器 DSL,有3种类型:

这些 DSL 可以让你写出干净利落的 Kotlin 代码来构建一个 RouterFunction 实例,如下例所示:

@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项目

1.7.2. MockMvc DSL

通过 MockMvc Kotlin扩展提供了一 个Kotlin DSL,以便提供一个更成文的Kotlin API,并允许更好的发现性(不使用静态方法)。

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,通过使用脚本引擎来渲染模板。

通过利用 scripting-jsr223 的依赖,有可能使用这种功能来渲染基于Kotlin的模板,使用 kotlinx.html DSL 或 Kotlin 多行插值的 String

build.gradle.kts

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

配置通常是通过 ScriptTemplateConfigurerScriptTemplateViewResolver bean 来完成。

KotlinScriptConfiguration.kt

@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 示例项目。

1.7.4. Kotlin 多平台序列化

从Spring Framework 5.3开始,Spring MVC、Spring WebFlux 和 Spring Messaging(RSocket)都支持 Kotlin多平台序列化。内置支持目前针对 CBOR、JSON和 ProtoBuf 格式。

要启用它,请按照 这些说明 来添加相关的依赖和插件。对于 Spring MVC 和 WebFlux,如果 Kotlin 序列化和 Jackson 在 classpath 中,它们将被默认配置,因为Kotlin序列化被设计为只序列化带有 @Serializable 注解的Kotlin类。对于 Spring Messaging(RSocket),如果你想自动配置,请确保 Jackson、GSON 或 JSONB 都不在 classpath 中;如果需要 Jackson,请手动配置 KotlinSerializationJsonMessageConverter

1.8. Coroutines

Kotlin Coroutines 是Kotlin的轻量级线程,允许以命令式的方式编写非阻塞代码。在语言方面,suspending 函数为异步操作提供了一个抽象,而在库方面, kotlinx.coroutines 提供了像 async { } 这样的函数和 Flow 这样的类型。

Spring Framework 在以下 scope 内提供对 Coroutines 的支持:

  • 在 Spring MVC和 WebFlux 注解的 @Controller 中支持 DeferredFlow 返回值

  • 在 Spring MVC 和 WebFlux 注解的 @Controller 中 Suspending 函数支持

  • WebFlux clientserver functional API 的扩展。

  • WebFlux.fn coRouter { } DSL

  • 在 RSocket 的 @MessageMapping 注解的方法中支 Suspending 函数和 Flow

  • RSocketRequester 的扩展。

1.8.1. 依赖

kotlinx-coroutines-corekotlinx-coroutines-reactor 依赖项在 classpath 中时, coroutines 支持被启用:

build.gradle.kts

dependencies {

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

支持 1.4.0 及以上版本。

1.8.2. Reactive 是如何转化为 Coroutine 的?

对于返回值,从 Reactive 到 Coroutine API 的转换如下:

  • fun handler(): Mono<Void> 变成了 suspend fun handler()

  • fun handler(): Mono<T> 变成了 suspend fun handler(): Tsuspend fun handler(): T? 取决于 Mono 是否可以为空(具有更多静态类型的优势)。

  • fun handler(): Flux<T> 变成了 fun handler(): Flow<T>

对于输入参数:

  • 如果不需要 laziness,fun handler(mono: Mono<T>) 就变成了 fun handler(value: T),因为可以调用一个 suspending 函数来获得值参数。

  • 如果需要 laziness,fun handler(mono: Mono<T>) 会变成 fun handler(supplier: suspend () → T)fun handler(supplier: suspend () → T?)

Flow 是 Coroutine 世界中的 Flux 等价物,适用于热流或冷流,有限流或无限流,主要区别如下:

  • Flow 是基于推的,而 Flux 是推拉混合型的。

  • 背压是通过 suspending function 实现的。

  • Flow 只有 一个 suspending collect 方法,operators 是作为 扩展 实现的

  • 由于有了 Coroutine, Operators 很容易实现

  • 扩展允许向 Flow 添加自定义 operators。

  • Collect 操作是 suspending function。

  • map operator 支持异步操作(不需要 flatMap),因为它需要一个 suspending function 参数。

阅读这篇关于 使用 Spring、Coroutine 和 Kotlin Flow 进行 Reactive 的博文,了解更多细节,包括如何使用 Coroutine 并发运行代码。

1.8.3. Controller

下面是一个 Coroutine @RestController 的例子。

@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 的视图渲染。

@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 和相关 handler 定义的 Coroutine 路由的例子。

@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. 事务

从Spring Framework 5.2开始,通过 Reactive 事务管理的编程式变体支持 Coroutine 上的事务。

对于 suspending function,提供了一个 TransactionalOperator.executeAndAwait 扩展。

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 扩展。

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 项目的具体提示和建议。

1.9.1. 默认为 Finale

默认情况下, Kotlin的所有类都是 final。一个类上的 open 修饰符与Java的 final 相反: 它允许其他人从这个类中继承。这也适用于成员函数,因为它们需要被标记为 open 才能被重写。

虽然 Kotlin 的 JVM 友好型设计通常与 Spring 无摩擦,但如果不考虑这一事实,这一特定的 Kotlin 特性会阻止应用程序的启动。这是因为Spring Bean(如 @Configuration 注解的类,由于技术原因,默认需要在运行时进行继承)通常由 CGLIB 代理。解决办法是在被 CGLIB 代理的 Spring Bean 的每个类和成员函数上添加一个 open 关键字,这很快就会变得很痛苦,而且违背了Kotlin保持代码简洁和可预测的原则。

也可以通过使用 @Configuration(proxyBeanMethods = false) 来避免配置类的 CGLIB 代理。参见 proxyBeanMethods Javadoc 获取更多细节。

幸运的是,Kotlin 提供了一个 kotlin-spring 插件(kotlin-allopen 插件的预配置版本),它可以自动为那些被注解或元注解的类型打开(open)类和其成员函数:

  • @Component

  • @Async

  • @Transactional

  • @Cacheable

元注解支持意味着用 @Configuration@Controller、@RestController@Service@Repository 注解的类型会自动 open,因为这些注解是用 @Component 元注解的。

start.springboot.io 默认启用 kotlin-spring 插件。因此,在实践中,你可以像在 Java 中一样,不需要任何额外的 open 关键字来编写你的 Kotlin bean。

Spring Framework 文档中的 Kotlin 代码样本没有明确指定类和其成员函数的 open。这些示例是为使用 kotlin-allopen 插件的项目编写的,因为这是最常用的设置。

1.9.2. 使用不可变(Immutable)的类实例进行持久化(Persistence)

在Kotlin中,在主构造函数中声明只读属性是很方便的,也被认为是一种最佳做法,就像下面的例子:

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

你可以选择添加 data 关键字,使编译器自动从主构造函数中声明的所有属性中派生出以下成员:

  • equals()hashCode()

  • "User(name=John, age=42)" 形式的 toString()

  • componentN() 函数,这些函数按其声明顺序与属性相对应。

  • copy() 函数。

正如下面的例子所示,这允许轻松地改变个别属性,即使 Person 的属性是只读的:

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注解的类生成合成的无参数构造函数。

如果你需要为其他持久化技术利用这种机制,你可以配置 kotlin-noarg 插件。

从 Kay release train 开始,Spring Data 支持 Kotlin 不可变类实例,如果模块使用 Spring Data 对象映射(如 MongoDB、Redis、Cassandra 和其他),则不需要 kotlin-noarg 插件。

1.9.3. 注入依赖

我们的建议是尽量倾向于使用 val 只读的构造函数注入(并且尽可能不为空)。 属性、正如下面的例子所示:

@Component
class YourBean(
    private val mongoTemplate: MongoTemplate,
    private val solrClient: SolrClient
)
只有一个构造函数的类,其参数会自动装配。这就是为什么在上面的例子中不需要明确的 @Autowired 构造函数。

如果你真的需要使用字段注入,你可以使用 lateinit var 结构,如下例所示:

@Component
class YourBean {

    @Autowired
    lateinit var mongoTemplate: MongoTemplate

    @Autowired
    lateinit var solrClient: SolrClient
}

1.9.4. 注入配置属性

在Java中,你可以通过使用注解来注入配置属性(比如 @Value("${property}")))。然而,在Kotlin中,$ 是一个保留字符,用于 字符串插值

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

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

作为一种选择,你可以通过声明以下配置 bean 来定制属性占位符前缀:

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

你可以用配置 bean 来定制使用 ${…​} 语法的现有代码(如 Spring Boot Actuator 或 @LocalServerPort),如下例所示:

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

@Bean
fun defaultPropertyConfigurer() = PropertySourcesPlaceholderConfigurer()

1.9.5. 未检查(捕获)的异常

Java 和 Kotlin的异常处理 非常接近,主要区别在于Kotlin将所有的异常都视为未检查的异常。然而,当使用代理对象(例如用 @Transactional 注解的类或方法)时,抛出的受检查(需要捕获)的异常将被默认包装成 UndeclaredThrowableException

为了获得像 Java 中那样的原始异常抛出,方法应该用 @Throws 注解,以明确指定抛出的受检查的异常(例如 @Throws(IOException::class))。

1.9.6. 注解数组属性

Kotlin注解大多与Java注解相似,但数组属性(在Spring中被广泛使用)的行为是不同的。正如 Kotlin文档 中所解释的那样,你可以省略 value 属性的名称,与其他属性不同,你可以把它指定为一个 vararg 参数。

为了理解这意味着什么,请考虑以 @RequestMapping(这是最广泛使用的Spring注解之一)为例。这个Java注解的声明方式如下:

public @interface RequestMapping {

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

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

    RequestMethod[] method() default {};

    // ...
}

@RequestMapping 的典型用例是将一个 handler method 映射到一个特定的 path 和 method。在Java中,你可以为注解数组属性指定一个单一的值,它将被自动转换为一个数组。

这就是为什么我们可以写 @RequestMapping(value = "/toys", method = RequestMethod.GET)@RequestMapping(path = "/toys", method = RequestMethod.GET)

然而,在Kotlin中,你必须写 @RequestMapping("/toys", method = [RequestMethod.GET])@RequestMapping(path = ["/toys"], method = [RequestMethod.GET])(方括号内需要指定命名数组属性)。

这种特定方法属性的一个替代方法(最常见的)是使用一个快捷注解,如 @GetMapping@PostMapping 等。

如果没有指定 @RequestMapping method 属性,所有的HTTP方法都会被匹配,而不仅仅是 GET 方法。

1.9.7. 测试

本节讨论 Kotlin 和 Spring 框架的组合测试。推荐的测试框架是 JUnit 5 和用于模拟(mock)的 Mockk

如果你使用的是 Spring Boot,请看这个 相关文档
构造函数注入

正如 专门章节 所描述的,JUnit 5允许对Bean进行构造器注入,这在Kotlin中相当有用,以便使用 val 而不是 lateinit var。 你可以使用 @TestConstructor(autowireMode = AutowireMode.ALL) 来启用所有参数的自动装配。

@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很适合。

你也可以通过 junit-platform.properties 文件中的 junit.jupiter.testinstance.lifecycle.default = per_class 属性,将默认行为改为 PER_CLASS

下面的例子演示了对非静态方法的 @BeforeAll@AfterAll 注解:

@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创建类似规范的测试。下面的例子展示了如何做到这一点:

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)
     }
  }
}
Kotlin 中的 WebTestClient 类型推导问题

由于 类型推导问题,你必须使用 Kotlin expectBody 扩展(如 .expectBody<String>().isEqualTo("toys")),因为它为 Kotlin 与 Java API 的问题提供了一个变通方法。

另请参见相关的 SPR-16057 问题。

1.10. 入门

学习如何用 Kotlin 构建 Spring 应用程序的最简单方法是跟随 专门的教程

1.10.1. start.springboot.io

在 Kotlin 中启动一个新的 Spring 框架项目的最简单方法是在 start.springboot.io 上创建一个新的 Spring Boot 2 项目。

1.10.2. 选择 Web 风格

Spring 框架现在有两个不同的 Web Stack:Spring MVCSpring WebFlux

如果你想创建处理延迟、长期连接、流媒体场景的应用程序,或者你想使用 web functional 的 Kotlin DSL,建议使用Spring WebFlux。

对于其他用例,特别是当你使用 JPA 等阻塞技术时,Spring MVC 及其基于注解的编程模型是推荐的选择。

1.11. 资源

我们为学习如何用 Kotlin 和 Spring 框架构建应用程序的人推荐以下资源:

1.11.1. 实例

以下Github项目提供了一些例子,你可以从中学习,甚至可以扩展:

2. Apache Groovy

Groovy是一种功能强大、可选择类型的动态语言,具有静态类型和静态编译功能。它提供了一个简洁的语法,并能与任何现有的Java应用程序顺利整合。

Spring框架提供了一个专门的 ApplicationContext,支持基于 Groovy 的 Bean Definition DSL。更多细节,请看 Groovy Bean 定义 DSL

对 Groovy 的进一步支持,包括用 Groovy 编写的 Bean、可刷新的脚本 Bean 等等,都可以在 动态语言的支持 中找到。

3. 动态语言的支持

Spring为使用Spring的动态语言(如Groovy)定义的类和对象提供全面支持。这种支持让你可以用支持的动态语言编写任意数量的类,并让Spring容器透明地实例化、配置和依赖注入所产生的对象。

Spring的脚本支持主要针对Groovy和BeanShell。除了这些特别支持的语言外,JSR-223脚本机制还支持与任何具有JSR-223能力的语言提供者(截至Spring 4.2)进行整合,例如JRuby。

你可以在 场景 中找到这种动态语言支持可以立即发挥作用的完整工作实例。

3.1. 第一个例子

本章的主要内容是详细描述动态语言支持。在深入了解动态语言支持的所有来龙去脉之前,我们先看一个用动态语言定义的Bean的快速例子。这第一个Bean的动态语言是Groovy。(这个例子的基础来自Spring的测试套件。如果你想看其他支持的语言中的同等例子,请看一下源代码)。

下一个例子显示了 Messenger 接口,Groovy bean 将实现这个接口。请注意,这个接口是用纯 Java 定义的。被注入 Messenger 引用的依赖对象不知道底层实现是一个Groovy脚本。下面的列表显示了 Messenger 的接口:

package org.springframework.scripting;

public interface Messenger {

    String getMessage();
}

下面的例子定义了一个对 Messenger 接口有依赖的类:

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 接口:

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 in file 'Messenger.groovy'
class GroovyMessenger implements Messenger {

    String message
}

为了使用自定义动态语言标签来定义动态语言支持的Bean,你需要在Spring XML配置文件的顶部设置 XML Schem 序言。你还需要使用Spring ApplicationContext 实现作为你的IoC容器。支持用普通的 BeanFactory 实现来使用动态语言支持的Bean,但你必须管理 Spring 内部的 plumbing 来做到这一点。

关于基于 schema 的配置的更多信息,请参阅 基于XML schema 的配置

最后,下面的例子显示了将 Groovy 定义的 Messenger 实现注入到 DefaultBookingService 类的实例中的 bean definition 的效果:

<?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。

希望前面的XML片段是不言自明的,但如果不是这样,也不要过分担心。继续阅读,深入了解前述配置的原因和原理。

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

本节准确描述了如何用任何一种支持的动态语言定义Spring管理的Bean。

请注意,本章并不试图解释所支持的动态语言的语法和习语。例如,如果你想用Groovy来编写你应用程序中的某些类,我们假设你已经知道Groovy。如果你需要关于动态语言本身的更多细节,请看本章末尾的 更多资源

3.2.1. 常见的概念

使用动态语言支持的 Bean 所涉及的步骤如下:

  1. 编写动态语言源代码的测试(自然)。

  2. 然后自己写动态语言的源代码。

  3. 通过在XML配置中使用适当的 <lang:language/> 元素来定义你的动态语言支持的Bean(你可以通过使用Spring API以编程方式定义这种Bean,尽管你必须查阅源代码以了解如何做到这一点,因为本章并不涉及这种类型的高级配置)。请注意,这是一个反复的步骤。你需要为每个动态语言源文件提供至少一个Bean定义(尽管多个Bean定义可以引用同一个源文件)。

前两个步骤(测试和编写你的动态语言源文件)超出了本章的范围。请参阅你所选择的动态语言的语言规范和参考手册,然后继续开发你的动态语言源文件。不过你首先要阅读本章的其余部分,因为Spring的动态语言支持确实对你的动态语言源文件的内容做了一些(很小的)假设。

<lang:language/> 元素

上一节 列表中的最后一步涉及到定义动态语言支持的 bean 定义,为你要配置的每个 bean 定义一个(这与正常的 JavaBean 配置没有什么不同)。然而,你可以使用 <lang:language/> 元素来定义动态语言支持的 bean,而不是指定将被容器实例化和配置的类的全路径的类名。

每个被支持的语言都有一个相应的 <lang:language/> 元素:

  • <lang:groovy/> (Groovy)

  • <lang:bsh/> (BeanShell)

  • <lang:std/> (JSR-223, e.g. 例如用 JRuby)

可供配置的确切属性和子元素取决于Bean是用哪种语言定义的(本章后面的特定语言部分会详细介绍)。

可刷新的 Bean

Spring 的动态语言支持中最引人注目的增值功能之一(也许是唯一的)是 "可刷新的bean" 功能。

一个可刷新的Bean是一个动态语言支持的Bean。通过少量的配置,动态语言支持的Bean可以监控其底层源文件资源的变化,然后在动态语言源文件发生变化时(例如,当你编辑和保存文件系统上的变化时)重新加载自己。

这让你可以部署任何数量的动态语言源文件作为应用程序的一部分,配置Spring容器以创建由动态语言源文件支持的Bean(使用本章描述的机制),并且(以后,随着需求的变化或其他一些外部因素的出现)编辑动态语言源文件,并让他们所做的任何变化反映在由变化的动态语言源文件支持的Bean上。不需要关闭正在运行的应用程序(如果是Web应用程序,则需要重新部署)。经修正的动态语言支持的Bean会从改变的动态语言源文件中获取新的状态和逻辑。

该功能默认为关闭。

现在我们可以看一个例子,看看开始使用可刷新 Bean 是多么容易。要打开可刷新Bean的功能,你必须在Bean定义的 <lang:language/> 元素上准确地指定一个额外的属性。因此,如果我们坚持本章前面的 例子,下面的例子显示了我们在Spring XML配置中的变化,以实现可刷新Bean:

<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>

这真的是你所要做的一切。在 messenger Bean定义中定义的 refresh-check-delay 属性是指在对底层动态语言源文件进行任何修改后,Bean被刷新的毫秒数。你可以通过给 refresh-check-delay 属性分配一个负值来关闭刷新行为。记住,默认情况下,刷新行为是禁用的。如果你不想要刷新行为,不要定义该属性。

如果我们运行下面的应用程序,我们就可以行使可刷新功能。(请原谅这段代码中的 “jumping-through-hoops-to-pause-the-execution” 的诡计。) System.in.read() 的调用只是为了让程序的执行暂停,而你(这个场景中的开发者)去编辑底层的动态语言源文件,以便在程序恢复执行时在动态语言支持的 bean 上触发刷新。

下面的列表显示了这个示例应用程序:

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());
    }
}

那么,为了这个例子的目的,假设所有对 Messenger 实现的 getMessage() 方法的调用都必须被改变,使 message 被引号所包围。下面的列表显示了当程序的执行暂停时,你(开发者)应该对 Messenger.groovy 源文件进行的修改:

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。在对源文件进行修改并保存后,程序恢复执行,在动态语言支持的 Messenger 实现上调用 getMessage() 方法的结果是 'I Can Do The Frug'(注意包含额外的引号)。

如果对脚本的更改发生在 refresh-check-delay 值的窗口内,则不会触发刷新。在动态语言支持的Bean上调用方法之前,对脚本的改变实际上不会被发现。只有当一个方法在动态语言支持的Bean上被调用时,它才会检查其底层的脚本源是否有变化。任何与刷新脚本有关的异常(比如遇到编译错误或发现脚本文件被删除)都会导致一个致命的异常被传播到调用代码中。

前面描述的可刷新Bean行为并不适用于用 <lang:inline-script/> 元素符号定义的动态语言源文件(参见 内联动态语言源代码文件)。此外,它只适用于可以实际检测到底层源文件变化的 Bean(例如,通过检查文件系统上存在的动态语言源文件的最后修改日期的代码)。

内联动态语言源代码文件

动态语言支持也可以满足直接嵌入 Spring Bean 定义中的动态语言源文件。更具体地说,<lang:inline-script/> 元素可以让你在Spring配置文件中直接定义动态语言源。一个例子可以说明内联脚本功能是如何工作的:

<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 Controller 中快速添加一个Spring Validator 实现。这不过是使用内联源码的片刻之功。(关于这个例子,请看 脚本化的验证器 Validator)。

在动态语言支持的Bean背景下理解构造函数注入

关于Spring的动态语言支持,有一件非常重要的事情需要注意。也就是说,你不能(目前)为动态语言支持的Bean提供构造器参数(因此,构造器注入对动态语言支持的Bean是不可用的)。为了使构造函数和属性的这种特殊处理方式100%清晰,下面的代码和配置的混合物不起作用:

一个不能工作的方法
package org.springframework.scripting.groovy

import org.springframework.scripting.Messenger

// from the file 'Messenger.groovy'
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 注入是绝大多数开发者喜欢的注入方式(至于这是否是一件好事,我们留待以后讨论)。

3.2.2. Groovy Bean

本节介绍了如何在 Spring 中使用 Groovy 中定义的 bean。

Groovy 主页包括以下描述:

“Groovy 是 Java 2 平台的一种敏捷的动态语言,它具有人们非常喜欢的Python、Ruby和Smalltalk等语言的许多特性,使Java开发人员可以使用类似Java的语法”。

如果你直接从头开始阅读本章,你已经看到了一个Groovy-动态语言支持的Bean的 例子。现在考虑另一个例子(再次使用Spring测试套件中的一个例子):

package org.springframework.scripting;

public interface Calculator {

    int add(int x, int y);
}

下面的例子用 Groovy 实现了 Calculator 接口:

package org.springframework.scripting.groovy

// from the file 'calculator.groovy'
class GroovyCalculator implements Calculator {

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

下面的 bean 定义使用了 Groovy 中定义的 calculator:

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

最后,下面的小程序实践了前面的配置:

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。(关于更多有趣的例子,请看动态语言展示项目中更复杂的例子,或者看本章后面的例子 场景)。

你不能在每个Groovy源文件中定义一个以上的类。虽然这在Groovy中是完全合法的,但它(可以说)是一种不好的做法。为了保持方法的一致性,你应该(在Spring团队看来)尊重每个源文件一个(公共)类的标准Java惯例。

通过使用回调自定义Groovy对象

GroovyObjectCustomizer 接口是一个回调,可以让你在创建 Groovy 支持的 Bean 的过程中 hook 额外的创建逻辑。例如,这个接口的实现可以调用任何需要的初始化方法,设置一些默认的属性值,或者指定一个自定义的 MetaClass。下面的列表显示了 GroovyObjectCustomizer 接口的定义:

public interface GroovyObjectCustomizer {

    void customize(GroovyObject goo);
}

Spring 框架将你的 Groovy 支持的 Bean 实例化,然后将创建的 GroovyObject 传递给指定的 GroovyObjectCustomizer(如果已经定义了一个)。你可以对提供的 GroovyObject 引用做任何你喜欢的事情。我们希望大多数人想用这个回调来设置一个自定义的 MetaClass,下面的例子展示了如何做到这一点:

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 是很容易的,正如下面的例子所示:

<!-- 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 的功能,正如下面的例子所示:

<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 的 GroovyObjectCustomizer 相同的地方指定一个 Groovy CompilationCustomizer (比如 ImportCustomizer),甚至是一个完整的Groovy CompilerConfiguration 对象。此外,你可以在 ConfigurableApplicationContext.setClassLoader 级别为你的Bean设置一个带有自定义配置的普通 GroovyClassLoader;这也会导致共享 GroovyClassLoader 的使用,因此在有大量脚本Bean的情况下是值得推荐的(避免每个Bean有一个孤立的 GroovyClassLoader 实例)。

3.2.3. BeanShell Bean

本节介绍了如何在Spring中使用BeanShell Bean。

BeanShell 的主页 包括以下描述:

BeanShell是一个小型的、免费的、可嵌入的具有动态语言功能的Java源解释器。
特点,用Java编写。BeanShell动态地运行标准的Java语法,并以常见的脚本便利条件对其进行扩展,如松散类型、命令和方法。
用常见的脚本便利条件对其进行扩展,如松散类型、命令和方法关闭。
像Perl和JavaScript中的闭包。

与Groovy相比,BeanShell 支持的 Bean 定义需要一些(小的)额外配置。在 Spring 中实现 BeanShell 动态语言支持很有意思,因为 Spring 创建了一个JDK动态代理,实现了 <lang:bsh> 元素的 script-interfaces 属性值中指定的所有接口(这就是为什么你必须在属性值中至少提供一个接口,因此,当你使用 BeanShell 支持的Bean时,要对接口进行编程)。这意味着对 BeanShell 支持的对象的每个方法调用都要通过JDK动态代理调用机制。

现在我们可以展示一个使用基于 BeanShell 的 Bean 的完全工作的例子,该Bean实现了本章前面定义的 Messenger 接口。我们再次展示 Messenger 接口的定义:

package org.springframework.scripting;

public interface Messenger {

    String getMessage();
}

下面的例子显示了 Messenger 接口的 BeanShell "实现"(我们在这里宽泛地使用这个术语):

String message;

String getMessage() {
    return message;
}

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

下面的例子显示了定义上述 "类" 的 "实例" 的Spring XML(同样,我们在这里非常宽泛地使用这些术语):

<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 的一些场景,请参见 场景

3.3. 场景

用脚本语言来定义Spring管理的Bean,可能有很多不同的场景。本节描述了Spring中动态语言支持的两种可能的用例。

3.3.1. 脚本化的 Spring MVC Controller

有一组类可以从使用动态语言支持的Bean中受益,这就是Spring MVC controller。在纯粹的Spring MVC应用中,Web应用的导航流在很大程度上是由Spring MVC controller 中封装的代码决定的。由于Web应用的导航流和其他表现层逻辑需要更新,以应对支持问题或不断变化的业务需求,通过编辑一个或多个动态语言源文件,并看到这些变化立即反映在运行中的应用状态中,可能更容易实现任何此类所需变化。

请记住,在Spring等项目所推崇的轻量级架构模型中,你的目标通常是拥有一个非常薄的表现层,应用程序的所有主要业务逻辑都包含在 domain 和服务层类中。将 Spring MVC Controller 开发成动态语言支持的Bean,可以让你通过编辑和保存文本文件来改变表现层逻辑。对这种动态语言源文件的任何改变(取决于配置)都会自动反映在由动态语言源文件支持的Bean上。

为了实现自动 "拾取" 动态语言支持的Bean的任何变化,你必须启用 "可刷新的Bean" 功能。请参阅 可刷新的 Bean 以了解对该功能的全面处理。

下面的例子显示了一个通过使用 Groovy 动态语言实现的 org.springframework.web.servlet.mvc.Controller

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 jakarta.servlet.http.HttpServletRequest
import jakarta.servlet.http.HttpServletResponse

// from the file '/WEB-INF/groovy/FortuneController.groovy'
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. 脚本化的验证器 Validator

在Spring的应用开发中,另一个可能受益于动态语言支持的Bean所带来的灵活性的领域是验证领域。使用松散类型的动态语言(可能还支持内联正则表达式),而不是常规的Java,可以更容易地表达复杂的验证逻辑。

同样,把验证器开发成动态语言支持的Bean可以让你通过编辑和保存一个简单的文本文件来改变验证逻辑。任何这样的变化(取决于配置)都会自动反映在运行中的应用程序的执行中,不需要重新启动应用程序。

为了实现对动态语言支持的Bean的任何修改的自动 "拾取",你必须启用 "可刷新的Bean" 功能。参见 可刷新的 Bean ,以了解对这一特性的全面和详细的处理。

T下面的例子展示了一个通过使用Groovy动态语言实现的Spring org.springframework.validation.Validator (关于 Validator 接口的讨论,请参见 使用 Spring 的 Validator 接口进行验证):

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. 其他细节

这最后一节包含了一些与动态语言支持有关的额外细节。

3.4.1. AOP — Advise 脚本化的Bean

你可以使用 Spring AOP 框架来 advise 脚本化Bean。Spring AOP框架实际上并不知道被 advise 的Bean可能是一个脚本Bean,所以所有你使用(或旨在使用)的AOP用例和功能都可以与脚本Bean一起使用。当你 advise 脚本Bean时,你不能使用基于类的代理。你必须使用 基于接口的代理

你并不局限于为脚本Bean提供 advise。你也可以用支持的动态语言来编写切面,并使用这些Bean来 advise 其他Spring Bean。不过这确实是对动态语言支持的高级使用。

3.4.2. Scope

如果它不是很明显的话,脚本Bean可以像其他Bean一样被定义 scope。各种 <lang:language/> 元素上的 scope 属性可以让你控制底层脚本Bean的scope,就像它对普通Bean一样。(默认的 scope 是 singleton,就像 "普通" Bean一样)。

下面的例子使用 scope 属性来定义一个Groovy Bean,它的 scope 是 prototype

<?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>

请参阅 IoC容器 中的 Bean Scope,以全面讨论Spring框架中的 Scope 支持。

3.4.3. lang XML schema

Spring XML配置中的 lang 元素涉及到将用动态语言(如 Groovy 或 BeanShell)编写的对象作为Spring容器中的Bean公开。

这些元素(和动态语言支持)在 动态语言支持 中得到了全面的阐述。关于这种支持和 lang 元素的全部细节,请参见该部分。

为了使用 lang schema 中的元素,你需要在你的Spring XML配置文件的顶部有以下序言。以下片段中的文字引用了正确的 schema,以便 lang 命名空间中的标记可以被你使用:

<?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. 更多资源

下面的链接是关于本章中提到的各种动态语言的进一步资源: