本站(springdoc.cn)中的内容来源于 spring.io ,原始版权归属于 spring.io。由 springdoc.cn 进行翻译,整理。可供个人学习、研究,未经许可,不得进行任何转载、商用或与之相关的行为。 商标声明:Spring 是 Pivotal Software, Inc. 在美国以及其他国家的商标。 |
1. Kotlin
Spring框架为Kotlin提供了一流的支持,让开发者在编写Kotlin应用程序时几乎就像Spring框架是一个本地的Kotlin框架一样。除了Java之外,参考文档的大部分代码样本都是用Kotlin提供的。
用Kotlin构建Spring应用程序的最简单方法是利用Spring Boot及其 专门的Kotlin支持。 本综合教程 将教你如何使用 start.springboot.io 用Kotlin构建Spring Boot应用程序。
如果你需要支持,请随时加入 Kotlin Slack 的 #spring 频道,或者在 Stackoverflow 上以 spring
和 kotlin
为标签提问。
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种类型:
-
WebMvc.fn DSL 与 router { } 。
-
WebFlux.fn Reactive DSL 与 router { }
-
WebFlux.fn Coroutines DSL 和 coRouter { }
这些 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}")
}
配置通常是通过 ScriptTemplateConfigurer
和 ScriptTemplateViewResolver
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
中支持 Deferred 和 Flow 返回值 -
在 Spring MVC 和 WebFlux 注解的
@Controller
中 Suspending 函数支持 -
WebFlux.fn coRouter { } DSL
-
在 RSocket 的
@MessageMapping
注解的方法中支 Suspending 函数和Flow
。 -
RSocketRequester
的扩展。
1.8.1. 依赖
当 kotlinx-coroutines-core
和 kotlinx-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(): T
或suspend 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
只有 一个 suspendingcollect
方法,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
扩展。
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
扩展。
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. 测试
如果你使用的是 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)
}
}
}
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 MVC 和 Spring WebFlux。
如果你想创建处理延迟、长期连接、流媒体场景的应用程序,或者你想使用 web functional 的 Kotlin DSL,建议使用Spring WebFlux。
对于其他用例,特别是当你使用 JPA 等阻塞技术时,Spring MVC 及其基于注解的编程模型是推荐的选择。
1.11. 资源
我们为学习如何用 Kotlin 和 Spring 框架构建应用程序的人推荐以下资源:
-
Kotlin Slack (有一个专门的 #spring 频道)
1.11.1. 实例
以下Github项目提供了一些例子,你可以从中学习,甚至可以扩展:
-
spring-boot-kotlin-demo: 常规的Spring Boot和Spring Data JPA项目
-
mixit: Spring Boot 2、WebFlux和Reactive Spring Data MongoDB
-
spring-kotlin-functional: 独立的 WebFlux 和 functional bean definition DSL
-
spring-kotlin-fullstack: WebFlux Kotlin全栈实例,前端使用Kotlin2js而不是JavaScript或TypeScript
-
spring-petclinic-kotlin: Kotlin版本的 Spring PetClinic 示例应用程序
-
spring-kotlin-deepdive: 从Boot 1.0和Java到Boot 2.0和Kotlin的一步步迁移指南
-
spring-cloud-gcp-kotlin-app-sample: Spring Boot 与 Google Cloud Platform 的整合
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 关于基于 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 所涉及的步骤如下:
-
编写动态语言源代码的测试(自然)。
-
然后自己写动态语言的源代码。
-
通过在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 上触发刷新。
下面的列表显示了这个示例应用程序:
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
// 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;
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
// 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 接口进行验证):
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>