1. Kotlin

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

Spring框架为Kotlin提供了一流的支持,并允许开发人员编写Kotlin应用程序,就像Spring框架是原生Kotlin框架一样。除了Java之外,参考文档的大多数代码示例都是用Kotlin提供的。

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

如果您需要支持,请随时加入Kotlin Slack的#Spring频道,或在Stackoverflow上使用Springkotlin作为标签提出问题。

1.1. Requirements

Spring框架支持Kotlin 1.3+并要求类路径上存在kotlin-stdlib(或其变体之一,如kotlin-stdlib-jdk8)和kotlin-flect。如果您在start.spring.io上引导Kotlin项目,则默认情况下会提供它们。

Kotlin inline classes are not yet supported.
The Jackson Kotlin module is required for serializing or deserializing JSON data for Kotlin classes with Jackson, so make sure to add the com.fasterxml.jackson.module:jackson-module-kotlin dependency to your project if you have such need. It is automatically registered when found in the classpath.

1.2. Extensions

Kotlin扩展提供了使用附加功能扩展现有类的能力。Spring框架Kotlin API使用这些扩展将特定于Kotlin的新功能添加到现有的SpringAPI中。

Spring框架KDOC API列出并记录了所有可用的Kotlin扩展和DSL。

Keep in mind that Kotlin extensions need to be imported to be used. This means, for example, that the GenericApplicationContext.registerBean Kotlin extension is available only if org.springframework.context.support.registerBean is imported. That said, similar to static imports, an IDE should automatically suggest the import in most cases.
Other libraries, such as Reactor and Spring Data, also provide Kotlin extensions for their APIs, thus giving a better Kotlin development experience overall.

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

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

Libraries such as Reactor or Spring Data provide null-safe APIs to leverage this feature.

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

对于Kotlin版本1.1+,默认行为与-Xjsr305=warn相同。要在从Spring API推断的Kotlin类型中考虑到Spring框架API的空安全性,Strong值是必需的,但在使用时应了解到,即使在次要版本之间,Spring API的空性声明也可能发生变化,并且未来可能会添加更多检查。

Generic type arguments, varargs, and array elements nullability are not supported yet, but should be in an upcoming release. See this discussion for up-to-date information.

1.4. Classes and Interfaces

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

Kotlin参数名称是通过专用KotlinReflectionParameterNameDiscoverer,识别的,它允许查找接口方法参数名称,而不需要在编译期间启用Java8<代码>-参数 编译器标志。

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

1.5. Annotations

Spring框架还利用Kotlin空安全来确定是否需要HTTP参数,而不必显式定义必需的属性。这意味着@RequestParam name:字符串?被视为非必需,反之,@RequestParam name:字符串被视为必需。Spring Messaging@Header注释也支持此功能。

以类似的方式,带有@AuTower@Bean@Inject的Spring Bean注入使用该信息来确定是否需要Bean。

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

遵循相同的原则,@Bean Fun播放(玩具,汽车:Car?)=baz(玩具,汽车)意味着必须在应用程序上下文中注册Toy类型的Bean,而Car类型的Bean可能存在也可能不存在。同样的行为也适用于自动连接的构造函数参数。

If you use bean validation on classes with properties or a primary constructor parameters, you may need to use annotation use-site targets, such as @field:NotNull or @get:Size(min=5, max=15), as described in this Stack Overflow response.

1.6. Bean Definition DSL

通过使用lambdas替代XML或Java配置(@configuration@Bean),Spring框架支持以功能方式注册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中,使用具体化的类型参数和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框架提供了Kotlin Bean定义DSL,它通过一个干净的声明性API声明了一个ApplicationContextInitializer,它允许您处理配置文件和环境,以定制如何注册Bean。

在下面的示例中,请注意:

  • 类型推断通常允许避免为ref(“bazBean”)之类的Bean引用指定类型

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

  • 指定Bean<;Bar>;()Bean(::myRouter)时,参数按类型自动配置

  • 仅当foobar配置文件处于活动状态时,才会注册FooBarBean

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 {
    // ...
}

            
This DSL is programmatic, meaning it allows custom registration logic of beans through an if expression, a for loop, or any other Kotlin constructs.

然后,您可以使用此Beans()函数在应用程序上下文中注册Bean,如下面的示例所示:

val context = GenericApplicationContext().apply {
    myBeans.initialize(this)
    refresh()
}

            
Spring Boot is based on JavaConfig and does not yet provide specific support for functional bean definition, but you can experimentally use functional bean definitions through Spring Boot’s ApplicationContextInitializer support. See this Stack Overflow answer for more details and up-to-date information. See also the experimental Kofu DSL developed in Spring Fu incubator.

1.7. Web

1.7.1. Router DSL

Spring框架附带Kotlin路由器DSL,有3种风格可供选择:

  • 带有路由器的WebMvc.fn DSL{}

  • 带有路由器的WebFlos.fn被动DSL{}

  • 带有核心路由器的WebFlos.fn协程DSL

这些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/"))
    }
}

             
This DSL is programmatic, meaning that it allows custom registration logic of beans through an if expression, a for loop, or any other Kotlin constructs. That can be useful when you need to register routes depending on dynamic data (for example, from a database).

有关具体示例,请参阅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 Script Templates

Spring框架提供了一个ScriptTemplateView,它支持JSR-223通过使用脚本引擎呈现模板。

通过利用脚本-jsr223依赖关系,可以使用此类功能来呈现带有kotlinx.htmlDSL或kotlin多行内插字符串的基于kotlin的模板。

Build.gradle.kts

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

             

配置通常使用ScriptTemplateConfigurerScriptTemplateViewResolverBean完成。

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

从Spring Framework5.3开始,在Spring MVC、Spring WebFlux和Spring Messaging(RSocket)中支持Kotlin多平台序列化。内置支持目前面向CBOR、JSON和ProtoBuf格式。

要启用它,请按照说明添加相关的依赖项和插件。对于Spring MVC和WebFlux,如果Kotlin序列化和Jackson都在类路径中,那么它们都将在缺省情况下被配置,因为Kotlin序列化被设计成只序列化用@Serializable注释的Kotlin类。使用Spring Messaging(RSocket),如果您想要自动配置,确保jackson、gson或jsob都不在类路径中,如果需要手动配置KotlinSerializationJsonMessageConverter,请确保jackson、gson或jsonb都不在类路径中。

1.8. Coroutines

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

Spring框架在以下范围内提供对协程的支持:

1.8.1. Dependencies

当类路径中存在kotlinx-coroutines-corekotlinx-coroutines-ector依赖项时,启用协程支持:

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. How Reactive translates to Coroutines?

对于返回值,从反应式API到协程API的转换如下:

  • Fun Handler():mono<;void&>变成Suspend Fun Handler()

  • Fun Handler():mono<;T&>;变为Suspend Fun Handler():tSuspend Fun Handler():t?取决于Mono是否为空(优点是可以更静态地键入)

  • Fun Handler():Flux<;T&>;成为Flow<;T&>;

对于输入参数:

  • 如果不需要懒惰,Fun处理程序(mono:mono<;T>;)变成Fun处理程序(Value:t),因为可以调用挂起的函数来获取Value参数。

  • 如果需要懒惰,则有趣的处理程序(Mono:Mono;Mono<;T>;)变成有趣的处理程序(Supplier:Suspend()→T)或有趣的处理程序(Supplier:Suspend()→T?)

flow等同于协程世界中的Flux,适用于冷热流、有限流和无限流,主要区别如下:

  • 是基于推送的,而通量是推拉混合的

  • 通过挂起函数实现反压力

  • 只有一个单个挂起Collect方法,运算符被实现为扩展

  • 运算符易于实现,这要归功于协程

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

  • 收集操作正在暂停功能

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

阅读这篇关于应对Spring、协程和Kotlin flow的博客文章,了解更多详细信息,包括如何与协程并发运行代码。

1.8.3. Controllers

下面是一个协程@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
class CoroutinesViewController(banner: Banner) {

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

             

1.8.4. WebFlux.fn

以下是通过核心路由器{}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. Transactions

协程上的事务是通过Spring Framework5.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 } } 
             

对于KotlinFlow,提供了Flow<;T&>.Transaction扩展。

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. Spring Projects in Kotlin

这一节提供了一些值得用Kotlin开发Spring项目的具体提示和建议。

1.9.1. Final by Default

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

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

It is also possible to avoid CGLIB proxies for configuration classes by using @Configuration(proxyBeanMethods = false). See proxyBeanMethods Javadoc for more details.

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

  • @组件

  • @Async

  • @事务性

  • @cacheable

元批注支持是指使用@Configuration@Controller@RestController@Service@Repository批注的类型会自动打开,因为这些批注是用@Component元批注的。

start.spring.io默认情况下启用kotlin-Spring插件。因此,在实践中,您可以编写Kotlin Bean,而不需要任何额外的Open关键字,就像在Java中一样。

The Kotlin code samples in Spring Framework documentation do not explicitly specify open on the classes and their member functions. The samples are written for projects using the kotlin-allopen plugin, since this is the most commonly used setup.

1.9.2. Using Immutable Class Instances for Persistence

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

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

             

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

  • 等于()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插件。

As of the Kay release train, Spring Data supports Kotlin immutable class instances and does not require the kotlin-noarg plugin if the module uses Spring Data object mappings (such as MongoDB, Redis, Cassandra, and others).

1.9.3. Injecting Dependencies

我们的建议是尝试使用val只读(如果可能则不可为空)属性来支持构造函数注入,如下面的示例所示:

@Component
class YourBean(
    private val mongoTemplate: MongoTemplate,
    private val solrClient: SolrClient
)

             
Classes with a single constructor have their parameters automatically autowired. That’s why there is no need for an explicit @Autowired constructor in the example shown above.

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

@Component
class YourBean {

    @Autowired
    lateinit var mongoTemplate: MongoTemplate

    @Autowired
    lateinit var solrClient: SolrClient
}

             

1.9.4. Injecting Configuration Properties

在Java中,您可以通过使用注释(如@Value(“${Property}”))注入配置属性。然而,在Kotlin中,$是用于字符串内插的保留字符。

因此,如果您希望在Kotlin中使用@Value注释,则需要通过编写@Value(“\${Property}”)来转义$字符。

If you use Spring Boot, you should probably use @ConfigurationProperties instead of @Value annotations.

或者,您可以通过声明以下配置Bean来自定义属性占位符前缀:

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

             

您可以定制使用${…>的现有代码(如Spring Boot执行器或@LocalServerPort)​}语法和配置Bean,如下例所示:

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

@Bean
fun defaultPropertyConfigurer() = PropertySourcesPlaceholderConfigurer()

             

1.9.5. Checked Exceptions

Java和Kotlin异常处理非常接近,主要区别在于Kotlin将所有异常视为未检查的异常。但是,当使用代理对象(例如,用@Transaction注释的类或方法)时,默认情况下,抛出的已检查异常将包装在未声明的ThrowableException中。

要像Java一样抛出原始异常,方法应该用@throws注释,以显式指定抛出的已检查异常(例如@throws(IOException::Class))。

1.9.6. Annotation Array Attributes

Kotlin批注大部分类似于Java批注,但数组属性(在Spring中广泛使用)的行为不同。如Kotlin文档中所述,与其他属性不同,您可以省略属性名,并将其指定为vararg参数。

要理解这意味着什么,请以@Requestmap(这是最广泛使用的Spring注释之一)为例。该Java注释声明如下:

public @interface RequestMapping {

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

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

    RequestMethod[] method() default {};

    // ...
}

             

@Requestmap的典型用例是将处理程序方法映射到特定的路径和方法。在Java中,您可以为注释数组属性指定单个值,它会自动转换为数组。

这就是为什么可以编写@RequestMap(Value=“/Toys”,Method=RequestMethod.GET)@RequestMap(Path=“/Toys”,Method=RequestMethod.GET)的原因。

然而,在Kotlin中,您必须编写@RequestMap(“/Toys”,method=[RequestMethod.GET])@Requestmap(Path=[“/Toys”],Method=[RequestMethod.GET])(需要使用命名数组属性指定方括号)。

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

If the @RequestMapping method attribute is not specified, all HTTP methods will be matched, not only the GET method.

1.9.7. Testing

本节介绍结合使用Kotlin和Spring框架进行测试。推荐的测试框架是JUnit5和用于模拟的Mockk

If you are using Spring Boot, see this related documentation.
Constructor injection

专用部分所述,JUnit5允许构造函数注入Bean,这对Kotlin非常有用,以便使用val而不是lateinit var。您可以使用@TestConstructor(auTower ireMode=AuTower ireMode.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 Lifecycle

Kotlin允许您在反号之间指定有意义的测试函数名称(`)。从JUnit5开始,Kotlin测试类可以使用@TestInstance(TestInstance.Lifecycle.PER_CLASS)注释来实现测试类的单一实例化,这允许在非静态方法上使用<代码>@BeForeAll 和<代码>@After All注释,这非常适合于Kotlin。

您还可以将默认行为更改为PER_CLASS,这要归功于带有junit.jupiter.testinstance.lifecycle.default=PER_CLASS属性的JUNIT-Platform.Properties文件。

下面的示例演示非静态方法上的@BeForeAll@After All批注:

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

              
Specification-like Tests

您可以使用JUnit5和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)
     }
  }
}

              
WebTestClient Type Inference Issue in Kotlin

由于类型推断问题,您必须使用kotlinexpectBody扩展(如.expectBody<;String>;().isEqualTo(“toys”)),),因为它提供了使用Java API解决kotlin问题的方法。

另请参阅相关spr-16057问题。

1.10. Getting Started

学习如何使用Kotlin构建一个Spring应用程序的最简单方法是按照专门的教程

1.10.1. start.spring.io

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

1.10.2. Choosing the Web Flavor

Spring框架现在提供了两个不同的Web堆栈:Spring MVCSpring WebFlux

如果您想创建处理延迟、长连接、流场景的应用程序,或者如果您想使用Web功能Kotlin DSL,则推荐使用Spring WebFlux。

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

1.11. Resources

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

1.11.1. Examples

以下Github项目提供了一些您可以学习甚至可以推广的示例:

2. Apache Groovy

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

Spring框架提供了专用的ApplicationContext,它支持基于Groovy的Bean定义DSL。有关更多详细信息,请参阅Groovy Bean定义DSL

3. Dynamic Language Support

Spring为在Spring中使用通过动态语言(如Groovy)定义的类和对象提供了全面的支持。这种支持允许您用受支持的动态语言编写任意数量的类,并让Spring容器透明地实例化、配置和依赖注入结果对象。

Spring的脚本支持主要针对Groovy和BeanShell。除了那些特别支持的语言之外,JSR-223脚本机制还支持与任何支持JSR-223的语言提供者(从Spring4.2开始)集成,例如JRuby。

您可以找到这种动态语言支持在场景中立即有用的完整工作示例。

3.1. A First Example

本章的主要内容是详细描述动态语言支持。在深入研究动态语言支持的所有细节之前,我们先来看一个在动态语言中定义的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接口:

// 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模式前导。您还需要使用一个SpringApplicationContext实现作为您的IOC容器。支持将动态语言支持的Bean与简单的BeanFactory实现一起使用,但您必须管理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(一个DefaultBookingService)现在可以正常使用其私有Messenger成员变量,因为注入其中的Messenger实例是一个Messenger实例。这里没有什么特别的 - ,只是普通的JAVA和普通的Groovy。

希望前面的XML片段是不言而喻的,但如果不是这样,也不必过分担心。继续阅读,了解前面配置的原因和原因的详细信息。

3.2. Defining Beans that Are Backed by Dynamic Languages

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

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

3.2.1. Common Concepts

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

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

  2. 然后编写动态语言本身的源代码。

  3. 通过在XML配置中使用适当的<;lang:language/>;元素定义动态语言支持的Bean(您可以使用Spring API以编程方式定义此类Bean,尽管您必须参考源代码以了解如何执行此操作的说明,因为本章不讨论这种类型的高级配置)。请注意,这是一个迭代步骤。每个动态语言源文件至少需要一个Bean定义(尽管多个Bean定义可以引用同一个源文件)。

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

The <lang:language/> element

每种受支持的语言都有相应的<;lang:language/>;元素:

  • <;lang:groovy/>;(Groovy)

  • <;lang:bsh/>;(BeanShell)

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

可用于配置的确切属性和子元素取决于Bean是用哪种语言定义的(本章后面的语言特定部分详细说明了这一点)。

Refreshable Beans

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

可刷新的Bean是动态语言支持的Bean。只需少量配置,支持动态语言的Bean就可以监视其基础源文件资源中的更改,然后在动态语言源文件更改时(例如,当您编辑和保存对文件系统上的文件的更改时)重新加载自己。

这允许您将任意数量的动态语言源文件部署为应用程序的一部分,配置Spring容器以创建由动态语言源文件支持的Bean(使用本章中描述的机制),以及(稍后,当需求更改或其他外部因素进入播放时)编辑动态语言源文件,并使它们所做的任何更改反映在由更改后的动态语言源文件支持的Bean中。无需关闭正在运行的应用程序(如果是Web应用程序,则无需重新部署)。这样修改的动态语言支持的Bean从改变后的动态语言源文件中获取新的状态和逻辑。

This feature is off by default.

现在,我们可以通过一个示例来了解开始使用可刷新的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>
              

这真的是你要做的全部事情。在MessengerBean定义上定义的刷新-检查-延迟属性是在多少毫秒后使用对底层动态语言源文件所做的任何更改刷新Bean。您可以通过为刷新-检查-延迟属性赋一个负值来关闭刷新行为。请记住,默认情况下,刷新行为处于禁用状态。如果您不希望刷新行为,请不要定义该属性。

如果我们随后运行以下应用程序,我们就可以使用可刷新功能。(请原谅下一段代码中的“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()方法的所有调用,以便消息用引号括起来。下面的清单显示了暂停执行程序时您(开发人员)应该对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’(注意包含了额外的引号)。

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

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

Inline Dynamic Language Source Files

动态语言支持还可以迎合直接嵌入到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控制器添加一个SpringValidator实现。这只是使用内联源代码的片刻工作。(有关此类示例,请参阅脚本化验证器。)

Understanding Constructor Injection in the Context of Dynamic-language-backed Beans

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

An approach that cannot work
// 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. Groovy Beans

本节介绍如何在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接口:

// 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。(有关更有趣的示例,请参阅更复杂的示例的Dynamic Language Showcase项目,或参阅本章后面的示例场景)。

您不能为每个Groovy源文件定义多个类。虽然这在Groovy中是完全合法的,但它(可以说)是一种糟糕的做法。为了实现一致的方法,您应该(在Spring团队看来)遵守每个源文件一个(公共)类的标准Java约定。

Customizing Groovy Objects by Using a Callback

GroovyObjectCustomizer接口是一个回调函数,允许您将额外的创建逻辑挂接到创建Groovy支持的Bean的过程中。例如,此接口的实现可以调用任何必需的初始化方法、设置一些默认属性值或指定自定义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"/>
              
You may also specify a Groovy CompilationCustomizer (such as an ImportCustomizer) or even a full Groovy CompilerConfiguration object in the same place as Spring’s GroovyObjectCustomizer. Furthermore, you may set a common GroovyClassLoader with custom configuration for your beans at the ConfigurableApplicationContext.setClassLoader level; this also leads to shared GroovyClassLoader usage and is therefore recommendable in case of a large number of scripted beans (avoiding an isolated GroovyClassLoader instance per bean).

3.2.3. BeanShell Beans

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

BeanShell主页包含以下说明:

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动态代理,它实现了<;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. Scenarios

在脚本语言中定义Spring托管Bean会带来好处的可能场景有很多,而且是多种多样的。本节描述了Spring中动态语言支持的两种可能用例。

3.3.1. Scripted Spring MVC Controllers

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

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

To effect this automatic “pickup” of any changes to dynamic-language-backed beans, you have to enable the “refreshable beans” functionality. See Refreshable Beans for a full treatment of this feature.

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

// 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 jakarta.servlet.http.HttpServletRequest import jakarta.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. Scripted Validators

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

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

To effect the automatic “pickup” of any changes to dynamic-language-backed beans, you have to enable the 'refreshable beans' feature. See Refreshable Beans for a full and detailed treatment of this feature.

下面的示例显示了使用Groovy动态语言实现的Spring代码(有关<org.springframework.validation.Validator>Validator 接口的讨论,请参阅使用SpringValidator接口进行验证):

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. Additional Details

这最后一节包含一些与动态语言支持相关的其他详细信息。

3.4.1. AOP — Advising Scripted Beans

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

您并不局限于建议编写脚本的Bean。您还可以用支持的动态语言编写方面本身,并使用这样的Bean向其他SpringBean提供建议。不过,这确实是对动态语言支持的高级使用。

3.4.2. Scoping

如果不是一目了然,脚本化的Bean可以像任何其他Bean一样确定作用域。各种<;lang:language/>;元素上的Scope属性允许您控制底层脚本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框架中作用域支持的完整讨论,请参阅the IOC Container中的Bean作用域

3.4.3. The lang XML schema

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

动态语言支持全面介绍了这些元素(以及动态语言支持)。有关此支持和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. Further Resources

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