1.科特林

Kotlin是一种针对 JVM(和其他平台)的静态类型语言,它允许编写简洁优雅的代码,同时提供 与用 Java 编写的现有库的非常好的互操作性。

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

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

如果您需要支持,请随意加入Kotlin Slack的#spring 频道, 或者使用Stackoverflowspringkotlin的标签 提出问题。

1.1。要求

Spring Framework 支持 Kotlin 1.3+ 并要求 kotlin-stdlib (或其变体之一,例如kotlin-stdlib-jdk8)并kotlin-reflect 出现在类路径中。如果您在 start.spring.io上引导 Kotlin 项目,则默认提供它们。

Jackson Kotlin 模块是 使用Jackson序列化或反序列化 Kotlin 类的 JSON 数据所必需的,因此如果您有此类需要,请确保将 com.fasterxml.jackson.module:jackson-module-kotlin依赖项添加到您的项目中。在类路径中找到它时会自动注册。

1.2. 扩展

Kotlin扩展提供了使用附加功能扩展现有类的能力。Spring Framework Kotlin API 使用这些扩展为现有 Spring API 添加新的 Kotlin 特定便利。

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

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

例如,Kotlin reified 类型参数 为 JVM泛型类型擦除提供了一种解决方法,并且 Spring 框架提供了一些扩展来利用这个特性。这为来自 Spring WebFluxRestTemplate的新API 和各种其他 API提供了更好的 Kotlin API 。WebClient

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

要在 Java 中检索User对象列表,您通常会编写以下代码:

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

使用 Kotlin 和 Spring Framework 扩展,您可以改为编写以下代码:

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

与 Java 一样,usersKotlin 是强类型的,但 Kotlin 聪明的类型推断允许更短的语法。

1.3. 零安全

Kotlin 的关键特性之一是null-safety,它在编译时干净地处理值,而不是在运行时null碰到著名 的值。NullPointerException这通过可空性声明和表达“值或无值”语义使应用程序更安全,而无需支付包装器的成本,例如Optional. (Kotlin 允许使用具有可为空值的函数构造。请参阅 Kotlin null-safety 综合指南。)

尽管 Java 不允许您在其类型系统中表达 null 安全性,但 Spring Framework 通过在包 中声明的工具友好的注释提供了整个 Spring Framework API 的 null 安全性。org.springframework.lang默认情况下,来自 Kotlin 中使用的 Java API 的类型被识别为 平台类型,对其进行空检查。 Kotlin 对 JSR-305 注释null和 Spring 可空性注释的支持为 Kotlin 开发人员提供了整个 Spring Framework API 的空安全性,并具有在编译时 处理相关问题的优势。

Reactor 或 Spring Data 等库提供了 null-safe API 来利用此功能。

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

对于 kotlin 1.1+ 版本,默认行为与-Xjsr305=warn. 该strict值需要在从 Spring API 推断的 Kotlin 类型中考虑 Spring Framework API 空安全性,但应该在知道 Spring API 可空性声明甚至在次要版本之间演变并且将来可能会添加更多检查的情况下使用该值.

尚不支持泛型类型参数、可变参数和数组元素可空性,但应该会在即将发布的版本中提供。 有关最新信息, 请参阅此讨论。

1.4. 类和接口

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

Kotlin 参数名称是通过一个专用的 来识别的KotlinReflectionParameterNameDiscoverer,它允许查找接口方法参数名称,而无需在-parameters 编译期间启用 Java 8 编译器标志。

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

1.5。注释

Spring 框架还利用Kotlin 空安全性 来确定是否需要 HTTP 参数,而无需显式定义required属性。这意味着@RequestParam name: String?被视为不需要,相反,@RequestParam name: String被视为需要。@HeaderSpring Messaging注解也支持此功能。

以类似的方式,使用 、 或 的 Spring bean 注入@Autowired使用@Bean@Inject信息来确定是否需要 bean。

例如,@Autowired lateinit var thing: Thing意味着Thing必须在应用程序上下文中注册一个类型的 bean,@Autowired lateinit var thing: Thing? 如果这样的 bean 不存在,则不会引发错误。

遵循相同的原则,@Bean fun play(toy: Toy, car: Car?) = Baz(toy, Car)意味着Toy必须在应用程序上下文中注册类型的 bean,而类型的 beanCar可能存在也可能不存在。相同的行为适用于自动装配的构造函数参数。

如果您对具有属性或主要构造函数参数的类使用 bean 验证,则可能需要使用 注解使用站点目标,例如@field:NotNull@get:Size(min=5, max=15)如此 Stack Overflow 响应中所述。

1.6. Bean 定义 DSL

Spring Framework 通过使用 lambdas 作为 XML 或 Java 配置(@Configuration@Bean)的替代方案,支持以功能方式注册 bean。简而言之,它允许您使用充当FactoryBean. 这种机制非常有效,因为它不需要任何反射或 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 中,使用具体的类型参数和GenericApplicationContextKotlin 扩展,您可以改为编写以下代码:

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 Framework 提供了一个Kotlin bean 定义 DSLApplicationContextInitializer通过一个干净的声明性 API 声明,它允许您处理配置文件并Environment自定义 bean 的注册方式。

在以下示例中,请注意:

  • 类型推断通常允许避免指定 bean 引用的类型,例如ref("bazBean")

  • 可以使用 Kotlin 顶级函数使用可调用引用来声明 bean,如bean(::myRouter)本例所示

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

  • 仅当配置文件处于活动状态FooBar时才会注册 beanfoobar

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,如以下示例所示:

val context = GenericApplicationContext().apply {
    myBeans.initialize(this)
    refresh()
}
Spring Boot 基于 JavaConfig, 目前还没有对功能 bean 定义提供具体的支持,但是您可以通过 Spring Boot 的ApplicationContextInitializer支持来实验性地使用功能 bean 定义。 有关更多详细信息和最新信息,请参阅此 Stack Overflow 答案。另见Spring Fu 孵化器中开发的实验性 Kofu DSL 。

1.7. 网络

1.7.1。路由器 DSL

Spring Framework 带有一个 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

Kotlin DSL 通过MockMvcKotlin 扩展提供,以提供更惯用的 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依赖关系,可以使用此类功能来渲染基于 Kotlinx.html DSL 或 Kotlin multiline interpolated的模板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-templateing示例项目。

1.7.4。Kotlin 多平台序列化

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

要启用它,请按照这些说明添加相关的依赖项和插件。使用 Spring MVC 和 WebFlux,如果 Kotlin 序列化和 Jackson 在类路径中,则默认配置它们,因为 Kotlin 序列化旨在仅序列化带有注释的 Kotlin 类@Serializable。使用 Spring Messaging (RSocket),如果需要自动配置,请确保 Jackson、GSON 或 JSONB 都不在类路径中,如果需要KotlinSerializationJsonMessageConverter手动配置 Jackson。

1.8. 协程

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

Spring Framework 在以下范围内为 Coroutines 提供支持:

1.8.1。依赖项

kotlinx-coroutines-core当和kotlinx-coroutines-reactor 依赖项在类路径中时启用协程支持:

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 如何转化为 Coroutines?

对于返回值,从 Reactive 到 Coroutines 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>

对于输入参数:

  • 如果不需要惰性,fun handler(mono: Mono<T>)就变成了,fun handler(value: T)因为可以调用挂起函数来获取 value 参数。

  • 如果需要懒惰​​,fun handler(mono: Mono<T>)则变为fun handler(supplier: suspend () → T)fun handler(supplier: suspend () → T?)

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

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

  • 背压是通过挂起函数实现的

  • Flow只有一个挂起collect方法,操作符作为扩展实现

  • 得益于协程,操作符很容易实现

  • 扩展允许添加自定义运算符Flow

  • 收集操作正在暂停功能

  • map运算符支持异步操作(不需要flatMap),因为它需要一个挂起函数参数

阅读这篇关于使用 Spring、Coroutines 和 Kotlin Flow实现响应式的博客文章以了解 更多详细信息,包括如何与 Coroutines 并发运行代码。

1.8.3. 控制器

这是一个 Coroutines 的例子@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还支持使用 a 进行视图渲染。

@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 和相关处理程序定义的协程路由器示例。

@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 提供的反应式事务管理的编程变体支持协程上的事务。

对于暂停功能,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 FlowFlow<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。最终默认

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

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

也可以通过使用@Configuration(proxyBeanMethods = false). 有关更多详细信息,请参阅proxyBeanMethodsJavadoc

幸运的是,Kotlin 提供了一个 kotlin-spring 插件(插件的预配置版本kotlin-allopen),它可以自动打开类及其成员函数,用于使用以下注释之一进行注释或元注释的类型:

  • @Component

  • @Async

  • @Transactional

  • @Cacheable

元注释支持意味着用@Configuration, @Controller, @RestController,@Service或注释的类型@Repository会自动打开,因为这些注释是用 元注释的@Component

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

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

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

在 Kotlin 中,在主构造函数中声明只读属性很方便,被认为是最佳实践,如下例所示:

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

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

  • equals()hashCode()

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

  • 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 插件。

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

1.9.3。注入依赖

我们的建议是尝试使用val只读(并且在可能的情况下不可为空)属性的构造函数注入,如以下示例所示:

@Component
class YourBean(
    private val mongoTemplate: MongoTemplate,
    private val solrClient: SolrClient
)
具有单个构造函数的类的参数会自动自动装配。@Autowired constructor这就是为什么在上面显示的示例中 不需要显式的原因。

如果确实需要使用字段注入,可以使用lateinit var构造,如下例所示:

@Component
class YourBean {

    @Autowired
    lateinit var mongoTemplate: MongoTemplate

    @Autowired
    lateinit var solrClient: SolrClient
}

1.9.4。注入配置属性

在 Java 中,您可以使用注解(例如@Value("${property}"))来注入配置属性。但是,在 Kotlin 中,$是用于 字符串插值的保留字符。

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

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

作为替代方案,您可以通过声明以下配置 bean 来自定义属性占位符前缀:

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

@LocalServerPort您可以使用配置 bean 自定义使用该语法的现有代码(例如 Spring Boot actuator 或) ${…​},如以下示例所示:

@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是将处理程序方法映射到特定路径和方法。在 Java 中,您可以为注解数组属性指定单个值,它会自动转换为数组。

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

但是,在 Kotlin 中,您必须编写@RequestMapping("/toys", method = [RequestMethod.GET]) or @RequestMapping(path = ["/toys"], method = [RequestMethod.GET])(方括号需要使用命名数组属性指定)。

此特定属性(最常见的属性)的替代方法method是使用快捷注释,例如@GetMapping@PostMapping等。

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

1.9.7。测试

本节介绍如何结合使用 Kotlin 和 Spring Framework 进行测试。推荐的测试框架是JUnit 5以及用于 模拟的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。

您还可以将默认行为更改为PER_CLASS感谢junit-platform.properties 具有junit.jupiter.testinstance.lifecycle.default = 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)
     }
  }
}
WebTestClientKotlin 中的类型推断问题

由于类型推断问题,您必须使用 KotlinexpectBody扩展(例如.expectBody<String>().isEqualTo("toys")),因为它为 Java API 的 Kotlin 问题提供了一种解决方法。

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

1.10。入门

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

1.10.1。start.spring.io

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

1.10.2。选择网络风格

Spring Framework 现在带有两个不同的 Web 堆栈:Spring MVCSpring WebFlux

如果您想创建将处理延迟、长期连接、流式传输场景的应用程序,或者如果您想使用 Web 功能 Kotlin DSL,建议使用 Spring WebFlux。

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

1.11。资源

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

1.11.1。例子

以下 Github 项目提供了可供您学习甚至扩展的示例:

2. Apache Groovy

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

Spring Framework 提供了一个专用ApplicationContext的支持基于 Groovy 的 Bean 定义 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 测试套件。如果您想查看任何其他受支持语言的等效示例,请查看源代码)。

下一个示例显示了MessengerGroovy 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...
    }
}

以下示例Messenger在 Groovy 中实现了该接口:

// 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 前导码。您还需要使用 SpringApplicationContext实现作为 IoC 容器。支持使用具有简单BeanFactory 实现的动态语言支持的 bean,但您必须管理 Spring 内部的管道才能这样做。

有关基于模式的配置的更多信息,请参阅基于 XML 模式的配置

最后,以下示例显示了影响将 Groovy 定义的Messenger实现注入到 DefaultBookingService类的实例中的 bean 定义:

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

bookingServicebean (a )现在DefaultBookingService可以正常使用它的私有messenger 成员变量,因为Messenger注入它的实例是一个Messenger实例。这里没有什么特别之处——只有普通的 Java 和普通的 Groovy。

希望前面的 XML 片段是不言自明的,但如果不是,请不要过分担心。继续阅读有关上述配置的原因和原因的详细信息。

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

本节准确描述了如何在任何受支持的动态语言中定义 Spring 管理的 bean。

请注意,本章并不试图解释支持的动态语言的语法和习语。例如,如果您想使用 Groovy 在应用程序中编写某些类,我们假设您已经了解 Groovy。如果您需要有关动态语言本身的更多详细信息,请参阅本章末尾的更多资源。

3.2.1。常见概念

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

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

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

  3. 使用 XML 配置中的适当元素定义动态语言支持的 bean <lang:language/> (您可以使用 Spring API 以编程方式定义此类 bean,尽管您必须查阅源代码以获取有关如何执行此操作的说明,如本章不包括此类高级配置)。请注意,这是一个迭代步骤。每个动态语言源文件至少需要一个 bean 定义(尽管多个 bean 定义可以引用同一个源文件)。

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

<lang:language/> 元素

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

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

  • <lang:groovy/>(时髦)

  • <lang:bsh/>(豆壳)

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

可用于配置的确切属性和子元素完全取决于 bean 是用哪种语言定义的(本章后面特定于语言的部分对此进行了详细说明)。

可刷新的豆子

Spring 中动态语言支持的一个(也许是唯一的)最引人注目的附加值是“可刷新 bean”特性。

可刷新 bean 是动态语言支持的 bean。通过少量配置,动态语言支持的 bean 可以监视其底层源文件资源的变化,然后在动态语言源文件发生变化时(例如,当您在文件系统)。

这使您可以将任意数量的动态语言源文件部署为应用程序的一部分,配置 Spring 容器以创建由动态语言源文件支持的 bean(使用本章中描述的机制),以及(稍后,随着需求的变化或其他一些外部因素起作用)编辑动态语言源文件,并让他们所做的任何更改反映在由更改的动态语言源文件支持的 bean 中。无需关闭正在运行的应用程序(或在 Web 应用程序的情况下重新部署)。如此修改的动态语言支持的 bean 从更改的动态语言源文件中获取新的状态和逻辑。

此功能默认关闭。

现在我们可以看一个例子,看看开始使用可刷新 bean 是多么容易。要打开可刷新 bean 功能,您必须在<lang:language/>bean 定义的元素上准确指定一个附加属性。因此,如果我们坚持本章前面的示例,下面的示例显示了我们将在 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>

这就是你所要做的。在 bean 定义上定义的refresh-check-delay属性 messenger是在对底层动态语言源文件进行任何更改之后刷新 bean 的毫秒数。refresh-check-delay您可以通过为属性分配负值来关闭刷新行为 。请记住,默认情况下,刷新行为是禁用的。如果您不想要刷新行为,请不要定义该属性。

如果我们然后运行以下应用程序,我们可以使用可刷新功能。(请原谅下一段代码中的“跳过箍到暂停执行”的恶作剧。)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());
    }
}

然后假设,出于本示例的目的,对实现getMessage() 方法的所有调用Messenger都必须更改,以便消息被引号括起来。以下清单显示了您(开发人员)在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. 在对源文件进行更改并保存并且程序恢复执行后,getMessage()在动态语言支持的 Messenger实现上调用方法的结果是'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/> 元素在某些场景中可能很有用。例如,我们可能希望快速将 SpringValidator实现添加到 Spring MVCController中。这只是使用内联源代码的片刻工作。(有关此类示例,请参阅脚本验证器。)

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

关于 Spring 的动态语言支持,有一件非常重要的事情需要注意。也就是说,您不能(当前)向动态语言支持的 bean 提供构造函数参数(因此,构造函数注入不适用于动态语言支持的 bean)。为了使构造函数和属性的这种特殊处理 100% 清楚,以下代码和配置的混合不起作用:

一种行不通的方法
// 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 注入是绝大多数开发人员所青睐的注入风格(我们将讨论这是否是一件好事留到另一天)。

3.2.2. 时髦的豆子

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

Groovy 主页包括以下描述:

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

如果您从头开始阅读本章,那么您已经 看到了一个 Groovy-dynamic-language-backed bean 的示例。现在考虑另一个示例(再次使用 Spring 测试套件中的示例):

package org.springframework.scripting;

public interface Calculator {

    int add(int x, int y);
}

以下示例Calculator在 Groovy 中实现了该接口:

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

class GroovyCalculator implements Calculator {

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

以下 bean 定义使用 Groovy 中定义的计算器:

<!-- 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 的过程中。例如,这个接口的实现可以调用任何需要的初始化方法,设置一些默认属性值,或者指定一个自定义的MetaClass. 以下清单显示了GroovyObjectCustomizer接口定义:

public interface GroovyObjectCustomizer {

    void customize(GroovyObject goo);
}

Spring 框架实例化您的 Groovy 支持的 bean 的实例,然后将创建的 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 参考手册的相关部分或在线搜索。很多文章都讨论了这个话题。实际上, GroovyObjectCustomizer如果您使用 Spring 命名空间支持,则使用 a 很容易,如以下示例所示:

<!-- 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"/>
您还可以在 SpringCompilationCustomizer的. 此外,您可以在级别为您的 bean 设置一个通用的自定义配置;这也导致共享使用,因此在有大量脚本 bean 的情况下是推荐的(避免每个 bean 一个孤立的实例)。 ImportCustomizerCompilerConfigurationGroovyObjectCustomizerGroovyClassLoaderConfigurableApplicationContext.setClassLoaderGroovyClassLoaderGroovyClassLoader

3.2.3。豆壳豆

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

BeanShell 主页包括以下描述:

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

与 Groovy 相比,BeanShell 支持的 bean 定义需要一些(小的)额外配置。Spring 中 BeanShell 动态语言支持的实现很有趣,因为 Spring 创建了一个 JDK 动态代理,该代理实现script-interfaces了元素属性值 中指定的所有接口<lang:bsh>(这就是为什么您必须在值中至少提供一个接口属性,因此,当您使用 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 控制器

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

请记住,在 Spring 等项目支持的轻量级架构模型中,您通常的目标是拥有一个非常薄的表示层,应用程序的所有丰富业务逻辑都包含在域和服务层类中。将 Spring MVC 控制器开发为支持动态语言的 bean,使您可以通过编辑和保存文本文件来更改表示层逻辑。对此类动态语言源文件的任何更改(取决于配置)都会自动反映在由动态语言源文件支持的 bean 中。

要实现对动态语言支持的 bean 的任何更改的自动“提取”,您必须启用“可刷新 bean”功能。有关此功能的完整处理,请参见 Refreshable Beans

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

// 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. 脚本验证器

另一个可能受益于动态语言支持的 bean 提供的灵活性的 Spring 应用程序开发领域是验证。与常规 Java 相比,使用松散类型的动态语言(也可能支持内联正则表达式)可以更容易地表达复杂的验证逻辑。

同样,将验证器开发为支持动态语言的 bean 使您可以通过编辑和保存一个简单的文本文件来更改验证逻辑。任何此类更改都会(取决于配置)自动反映在正在运行的应用程序的执行中,并且不需要重新启动应用程序。

要实现对动态语言支持的 bean 的任何更改的自动“拾取”,您必须启用“可刷新 bean”功能。有关此功能的完整详细处理,请参见 Refreshable Beans

以下示例显示了org.springframework.validation.Validator 使用 Groovy 动态语言实现的 Spring(有关该接口的讨论, 请参阅使用 Spring 的 Validator 接口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 — 建议脚本 Bean

您可以使用 Spring AOP 框架来建议脚本化 bean。Spring AOP 框架实际上并不知道所建议的 bean 可能是脚本 bean,因此您使用(或打算使用)的所有 AOP 用例和功能都与脚本 bean 一起工作。当您建议脚本化 bean 时,您不能使用基于类的代理。您必须使用基于接口的代理

您不仅限于建议脚本 bean。您还可以使用受支持的动态语言自己编写方面,并使用此类 bean 来建议其他 Spring bean。不过,这确实是对动态语言支持的高级使用。

3.4.2. 范围界定

如果不是很明显,脚本化的 bean 可以像任何其他 bean 一样被限定范围。scope各种元素上的属性<lang:language/>使您可以控制底层脚本 bean 的范围,就像它对常规 bean 所做的那样。(默认范围是singleton,与“常规” bean 一样。)

以下示例使用该属性来定义范围为原型scope的 Groovy bean :

<?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 框架中范围支持的完整讨论,请参阅IoC 容器中的Bean Scopes 。

3.4.3. XMLlang模式

Spring XML 配置中的lang元素处理将用动态语言(例如 Groovy 或 BeanShell)编写的对象公开为 Spring 容器中的 bean。

这些元素(以及动态语言支持)在 Dynamic Language Support中全面介绍。lang有关此支持和元素的完整详细信息,请参阅该部分。

要使用lang模式中的元素,您需要在 Spring XML 配置文件的顶部有以下序言。以下代码段中的文本引用了正确的架构,以便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. 更多资源

以下链接指向有关本章中引用的各种动态语言的更多资源:


1. see XML Configuration