0%

SpringFramework官方文档翻译-语言支持

语言支持

Kotlin

Kotlin是一种静态类型语言,它以 JVM(和其他平台)为目标,允许编写简洁优雅的 code,同时提供非常好的互通性与用 Java 编写的现有 libraries。

Spring Framework 为 Kotlin 提供 first-class 支持,让开发人员可以编写 Kotlin applications,就好像 Spring Framework 是本机 Kotlin framework 一样。

了解 Spring 和 Kotlin 的最简单方法是遵循这个全面的教程。如果您需要支持,请随意加入Kotlin Slack的#spring channel 或在堆栈溢出上使用springkotlin作为标记提问。

要求

Spring Framework 支持 Kotlin 1.1 并且需要kotlin-stdlib(或其中一个变体,例如kotlin-stdlib-jre8用于 Kotlin 1.1 或kotlin-stdlib-jdk8用于 Kotlin 1.2)和kotlin-reflect出现在 classpath 上。如果您在start.spring.io上引导 Kotlin 项目,则默认提供它们。

Extensions

Kotlin extensions提供了扩展现有 classes 并具有附加功能的能力。 Spring Framework Kotlin API 使用这些 extensions 为现有的 Spring API 添加新的 Kotlin-specific 便利。

Spring Framework KDoc API lists 和文档都可用 Kotlin extensions 和 DSL。

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

例如,Kotlin reified 类型参数为 JVM 泛型类型擦除提供了一种解决方法,而 Spring Framework 提供了一些 extensions 来利用此 feature。这允许更好的 Kotlin API RestTemplate,来自 Spring WebFlux 的新WebClient,以及各种其他 API。

其他 libraries,例如 Reactor 和 Spring Data,也为其 API 提供 Kotlin extensions,从而提供更好的 Kotlin 开发体验。

要在 Java 中检索User objects 列表,通常会编写以下内容:

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

使用 Kotlin 和 Spring Framework extensions,您可以编写以下内容:

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

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

Null-safety

Kotlin 的 key features 之一是null-safety,它在编译 time 时干净地处理null值,而不是在运行时碰到着名的NullPointerException。这使得 applications 通过可空性声明更安全,并且表达“value 或 no value”语义而无需支付包装器的成本,例如Optional。 (Kotlin 允许使用具有可空值的函数结构.请参阅Kotlin 综合指南 null-safety .)

虽然 Java 不允许你在 type-system 中表达 null-safety,但 Spring Framework 通过org.springframework.lang包中声明的 tooling-friendly 注释提供了整个 Spring Framework API 的 null-safety。默认情况下,Kotlin 中使用的 Java API 类型被识别为平台类型,其中 null-checks 被放宽。 Kotlin 支持 JSR-305 annotations和 Spring nullability annotations 为 Kotlin 开发人员提供了整个 Spring Framework API 的 null-safety,其优点是可以在编译 time 时处理null -related 问题。

_Reactor 或 Spring Data 等库提供 null-safe API 来利用此 feature。

您可以通过使用以下选项添加-Xjsr305编译器 flag 来配置 JSR-305 检查:-Xjsr305={strict|warn|ignore}

对于 kotlin 版本 1.1,默认行为与-Xjsr305=warn相同。 value 需要在从 Spring API 中推断的 Kotlin 类型中考虑 Spring Framework API null-safety,但应该知道 Spring API 可空性声明甚至可以在次要版本之间发展,并且将来可能会添加更多检查) 。

尚未支持泛型类型 arguments,varargs 和 array 元素的可空性,但应该在即将发布的版本中。有关 up-to-date 信息,请参阅这个讨论

Classes和Interfaces

Spring Framework 支持各种 Kotlin 构造,例如通过主构造函数实例化 Kotlin classes,使用默认值的不可变 classes 数据 binding 和 function 可选参数。

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

在 classpath 中找到时,会自动注册序列化或反序列化 JSON 数据所需的Jackson Kotlin 模块,如果在没有 Jackson Kotlin 模块的情况下检测到 Jackson 和 Kotlin,则会记录警告消息。

您可以将 configuration classes 声明为top level 或嵌套但不是内部,因为后者需要_eference to outer class。

Annotations

Spring Framework 还利用Kotlin null-safety来确定是否需要 HTTP 参数而无需显式定义required属性。这意味着@RequestParam name: String?被视为不需要,相反,@RequestParam name: String被视为必需。 Spring Messaging @Header annotation 也支持此 feature。

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

对于 example,@Autowired lateinit var thing: Thing表示必须在 application context 中注册Thing类型的 bean,而如果 bean 不存在则@Autowired lateinit var thing: Thing?不会引发错误。

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

如果对带有 properties 的 classes 或主构造函数参数使用 bean 验证,则可能需要使用annotation use-site 目标,如@field:NotNull@get:Size(min=5, max=15),如此 Stack Overflow 响应中所述。

Bean定义DSL

Spring Framework 5 引入了一种通过使用 lambdas 替代 XML 或 Java configuration(@Configuration@Bean)来以功能方式注册 beans 的新方法。简而言之,它允许您使用充当FactoryBean的 lambda 注册 beans。这种机制非常有效,因为它不需要任何反射或 CGLIB 代理。

在 Java 中,您可以为 example 编写以下内容:

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

在 Kotlin 中,使用 reified 类型参数和GenericApplicationContext Kotlin extensions,您可以改为编写以下内容:

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

在 order 中允许更具说明性的方法和更清晰的语法,Spring Framework 提供Kotlin bean 定义 DSL它通过一个干净的声明性 API 声明一个ApplicationContextInitializer,它允许你处理 profiles 和Environment来定制如何注册 beans。以下 example 创建Play profile:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
fun beans() = beans {
bean<UserHandler>()
bean<Routes>()
bean<WebHandler>("webHandler") {
RouterFunctions.toWebHandler(
ref<Routes>().router(),
HandlerStrategies.builder().viewResolver(ref()).build()
)
}
bean("messageSource") {
ReloadableResourceBundleMessageSource().apply {
setBasename("messages")
setDefaultEncoding("UTF-8")
}
}
bean {
val prefix = "classpath:/templates/"
val suffix = ".mustache"
val loader = MustacheResourceTemplateLoader(prefix, suffix)
MustacheViewResolver(Mustache.compiler().withLoader(loader)).apply {
setPrefix(prefix)
setSuffix(suffix)
}
}
profile("play") {
bean<Play>()
}
}

在前面的 example 中,bean<Routes>()使用构造函数的自动装配,而ref<Routes>()applicationContext.getBean(Routes::class.java)的快捷方式。

然后,您可以使用此beans() function 在 application context 上注册 beans,如下面的 example 所示:

1
2
3
4
val context = GenericApplicationContext().apply {
beans().initialize(this)
refresh()
}

这个 DSL 是程序化的,这意味着它允许 beans 通过if表达式,for循环或任何其他 Kotlin 结构自定义注册逻辑。

有关具体的 example,请参见spring-kotlin-functional beans 声明

Spring Boot 基于 Java configuration 和尚未提供功能 bean 定义的特定支持,但您可以通过 Spring Boot 的ApplicationContextInitializer支持实验性地使用函数 bean 定义。有关详细信息和 up-to-date 信息,请参阅这个 Stack Overflow 答案

Web

WebFlux功能DSL

Spring Framework 现在附带一个Kotlin 路由 DSL,它允许您使用WebFlux 功能 API来编写干净且惯用的 Kotlin code,如下面的 example 所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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 是程序化的,这意味着它允许 beans 通过if表达式,for循环或任何其他 Kotlin 结构自定义注册逻辑。当您需要根据动态数据(例如,来自数据库)注册 routes 时,这非常有用。

有关具体的 example,请参见MiXiT 项目 routes

Kotlin脚本模板

从 version 4.3 开始,Spring Framework 通过使用脚本引擎提供ScriptTemplateView来渲染模板。它支持JSR-223。通过将此 feature 扩展到 WebFlux 并支持i18n 和嵌套模板,Spring Framework 5 更进一步。

Kotlin 提供类似的支持并允许渲染 Kotlin-based 模板。有关详细信息,请参阅这个提交

这可以实现一些有趣的用例 - 例如使用kotlinx.html DSL 编写 type-safe 模板或使用插值使用 Kotlin 多线String

这可以让您在支持的 IDE 中编写具有完全自动完成和重构支持的 Kotlin 模板,如下面的示例所示:

1
2
3
4
5
6
7
8
9
10
import io.spring.demo.*

"""
${include("header")}
<h1>${i18n("title")}</h1>
<ul>
${users.joinToLine{ "<li>${i18n("user")} ${it.firstname} ${it.lastname}</li>" }}
</ul>
${include("footer")}
"""

有关更多详细信息,请参阅kotlin-script-templating example 项目。

Kotlin中的Spring项目

本节提供了一些在 Kotlin 中开发 Spring 项目的具体提示和建议。

默认类型Final

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

虽然 Kotlin 的 JVM-friendly 设计通常与 Spring 没有摩擦,但如果不考虑这个事实,这个特定的 Kotlin feature 可以阻止应用程序启动。这是因为 Spring beans(例如由于技术原因需要在运行时继承的@Configuration classes)通常由 CGLIB 代理。解决方法是在 CGLIB 代理的每个 class 和 Spring beans 的成员 function 上添加一个open关键字(例如@Configuration classes),这很快就会变得很痛苦,并且违反了保持 code 简洁和可预测的 Kotlin 原则。

幸运的是,Kotlin 现在提供了一个kotlin-spring插件(kotlin-allopen插件的预配置 version),它自动打开 classes 及其成员函数,用于注释的类型或带有以下注释之一的 meta-annotated:

  • @Component
  • @Async
  • @Transactional
  • @Cacheable

Meta-annotations 支持意味着自动打开带有@Configuration@Controller@RestController@Service@Repository注释的类型,因为这些注释是 meta-annotated,带有@Component

start.spring.io默认启用它,因此,在实践中,您可以编写 Kotlin beans 而不需要任何其他open关键字,就像在 Java 中一样。

使用不可变的Class实例进行持久化

在 Kotlin 中,在主构造函数中声明 read-only properties 是一种方便且被认为是最佳实践,如下面的示例所示:

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

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

  • equals()hashCode()
  • 形式"User(name=John, age=42)"toString()
  • componentN()与声明 order 中的 properties 对应的函数
  • copy() function

如下面的 example 所示,即使Person properties 为 read-only,这也允许轻松更改单个 properties:

1
2
3
4
data class Person(val name: String, val age: Int)

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

Common 持久性技术(如 JPA)需要一个默认构造函数,以防止这种设计。幸运的是,这个“默认构造函数地狱”现在有一个解决方法,因为 Kotlin 提供了一个kotlin-jpa插件,为使用 JPA annotations 注释的 class 生成合成 no-arg 构造函数。

如果您需要将此类机制用于其他持久性技术,则可以配置kotlin-noarg插件。

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

注入依赖关系

我们的建议是尝试使用val read-only(和 non-nullable,如果可能的话)properties来支持构造函数注入,如下面的 example 所示:

1
2
3
4
5
@Component
class YourBean(
private val mongoTemplate: MongoTemplate,
private val solrClient: SolrClient
)

从 Spring Framework 4.3 开始,具有单个构造函数的 classes 的参数会自动自动装配,这就是为什么上面显示的 example 中不需要显式@Autowired constructor的原因。

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

1
2
3
4
5
6
7
8
9
@Component
class YourBean {

@Autowired
lateinit var mongoTemplate: MongoTemplate

@Autowired
lateinit var solrClient: SolrClient
}

注入配置属性值

在 Java 中,您可以使用 annotations(例如@Value("${property}"))inject configuration properties。但是,在 Kotlin 中,$是用于string 插值的保留字符。

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

或者,您可以通过声明以下 configuration beans 来自定义 properties 占位符前缀:

1
2
3
4
@Bean
fun propertyConfigurer() = PropertySourcesPlaceholderConfigurer().apply {
setPlaceholderPrefix("%{")
}

您可以使用 configuration beans 自定义使用${…}语法的现有 code(例如 Spring Boot 执行器或@LocalServerPort),如下面的 example 所示:

1
2
3
4
5
6
7
8
@Bean
fun kotlinPropertyConfigurer() = PropertySourcesPlaceholderConfigurer().apply {
setPlaceholderPrefix("%{")
setIgnoreUnresolvablePlaceholders(true)
}

@Bean
fun defaultPropertyConfigurer() = PropertySourcesPlaceholderConfigurer()

如果使用 Spring Boot,则可以使用@ConfigurationProperties而不是@Value 注释。但是,目前,这仅适用于lateinit或 nullable var properties(我们推荐前者),因为尚不支持由构造函数初始化的不可变 classes。有关详细信息,请参阅有关@ConfigurationProperties binding 为不可变的 POJO@ConfigurationProperties binding 接口的这些问题。

注解数组属性

Kotlin annotations 大多类似于 Java annotations,但 array 属性(在 Spring 中广泛使用)的行为有所不同。如Kotlin 文档中所述,您可以省略value属性 name,与其他属性不同,并将其指定为vararg参数。

要理解这意味着什么,请考虑@RequestMapping(这是使用最广泛的 Spring annotations 之一)作为 example。此 Java annotation 声明如下:

1
2
3
4
5
6
7
8
9
10
11
12
public @interface RequestMapping {

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

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

RequestMethod[] method() default {};

// ...
}

@RequestMapping的典型用例是将处理程序方法 map 映射到特定的路径和方法。在 Java 中,您可以为 annotation array 属性指定单个 value,并自动将其转换为 array。

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

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

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

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

测试

本节使用 Kotlin 和 Spring Framework 的组合进行测试。

PER_CLASS生命周期

Kotlin 允许您在 non-static 方法的反引号(). As of JUnit 5, Kotlin test classes can use the @TestInstance(TestInstance.Lifecycle.PERCLASS)annotation to enable a single instantiation of test classes, which allows the use of @BeforeAll and @AfterAll`annotations 之间指定有意义的测试函数名称,这非常适合 Kotlin。

由于带有junit.jupiter.testinstance.lifecycle.default = per_class property 的junit-platform.properties文件,您现在可以将默认行为更改为PER_CLASS

non-static 方法的以下 example @BeforeAll@AfterAll 注解:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
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测试

您可以使用 JUnit 5 和 Kotlin 创建 specification-like 测试。以下 example 显示了如何执行此操作:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class SpecificationLikeTests {

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

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

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

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

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

入门

本节介绍了开始使用 Kotlin 和 Spring Framework 组合的项目的最快方法。

使用start.spring.io

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

您还可以创建独立的 WebFlux 项目,如这篇博文中所述。

选择Web风格

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

如果你想创建能够处理延迟,long-lived 连接,流媒体场景或者你想使用 web 功能 Kotlin DSL 的 applications,建议使用 Spring WebFlux。

对于其他用例,特别是如果您使用阻塞技术(如 JPA),Spring MVC 及其 annotation-based 编程 model 是完全有效且完全支持的选择。

资源

对于学习如何使用 Kotlin 和 Spring Framework build applications 的人们,我们建议使用以下资源:

教程

我们建议使用以下教程:

博客文章

以下博客文章提供了更多详细信息:

例子

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

问题

以下列表对与 Spring 和 Kotlin 支持相关的待处理问题进行了分类:

ApacheGroovy

Groovy 是一种功能强大,可选的类型和动态语言,具有 static-typing 和静态编译功能。它提供了简洁的语法,并可与任何现有的 Java application 平滑集成。

Spring Framework 提供了一个支持 Groovy-based Bean Definition DSL 的专用ApplicationContext。有关更多详细信息,请参阅Groovy Bean 定义 DSL

进一步支持 Groovy,包括用 Groovy 编写的 beans,可刷新的脚本 beans 等等,可在动态语言支持中使用。

动态语言支持

Spring 2.0 引入了对使用 classes 和 objects 的全面支持,这些 classes 和 objects 是使用动态语言(如 JRuby)和 Spring 定义的。此支持允许您以受支持的动态语言编写任意数量的 classes,并使 Spring 容器透明地实例化,配置和依赖 inject 得到的 objects。

Spring 目前支持以下动态语言:

  • JRuby 1.5
  • Groovy 1.8
  • BeanShell 2.0

为什么只有这些语言?

我们选择支持这些语言,因为:

  • 这些语言在 Java 企业社区中具有很大的吸引力。
  • 在 time 添加此支持时,未向其他语言发出请求
  • Spring 开发人员最熟悉它们。

您可以找到这个动态语言支持在方案中可以立即使用的完整工作示例。

第一个例子

本章的大部分内容涉及详细描述动态语言支持。在深入研究动态语言支持的所有细节之前,我们先看一下动态语言中定义的 bean 的快速示例。第一个 bean 的动态语言是 Groovy。 (这个 example 的基础来自 Spring 测试套件.如果你想在任何其他支持的语言中看到相同的例子,请看一下 source code)。

下一个 example 显示了接口,Groovy bean 将实现该接口。请注意,此接口是以普通 Java 定义的。注入了Messenger的 reference 的依赖 objects 不知道底层的实现是一个 Groovy 脚本。以下清单显示了Messenger接口:

1
2
3
4
5
6
7
package org.springframework.scripting;

public interface Messenger {

String getMessage();

}

以下 example 定义了一个与Messenger接口有依赖关系的 class:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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...
}

}

以下 example 在 Groovy 中实现Messenger接口:

1
2
3
4
5
6
7
8
9
10
11
12
// 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

}

要使用自定义动态语言标记来定义 dynamic-language-backed beans,您需要在 Spring XML configuration 文件的顶部放置 XML Schema 前导码。您还需要使用 Spring ApplicationContext implementation 作为 IoC 容器。支持使用带有普通BeanFactory implementation 的 dynamic-language-backed beans,但您必须管理 Spring 内部的管道才能执行此操作。

有关 schema-based configuration 的更多信息,请参阅XML Schema-based configuration

最后,以下 example 显示 bean 定义,它们将 Groovy-defined Messenger 实现注入到DefaultBookingService class 的实例中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<?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 http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/lang http://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(a DefaultBookingService)现在可以正常使用其私有messenger成员变量,因为注入其中的Messenger实例是Messenger实例。这里没有什么特别的东西 - 只是简单的 Java 和普通的 Groovy。

希望前面的 XML 片段是 self-explanatory,但如果不是,请不要过分担心。继续阅读上述 configuration 的原因和原因的 in-depth 详细信息。

定义由动态语言支持的Beans

本节将准确描述如何在任何支持的动态语言中定义 Spring-managed beans。

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

通用概念

使用 dynamic-language-backed beans 所涉及的步骤如下:

  • 编写动态语言 source code(自然)的测试。
  • 然后编写动态语言 source code 本身。
  • 通过在 XML configuration 中使用适当的<lang:language/>元素来定义 dynamic-language-backed beans(您可以使用 Spring API 以编程方式定义此类 beans,尽管您将不得不查阅 source code 以获取有关如何执行此操作的说明,因为本章未涵盖这种类型的高级 configuration)。请注意,这是一个迭代的 step。每个动态语言源文件至少需要一个 bean 定义(尽管多个 bean 定义可以引用相同的动态语言源文件)。

前两个步骤(测试和编写动态语言源 files)超出了本章的范围。请参阅所选动态语言的语言规范和 reference 手册,并使用 developing 动态语言源 files 进行破解。但是,您首先要阅读本章的内容,因为 Spring 的动态语言支持确实对动态语言源 files 的内容做了一些(小的)假设。

元素

前一节列表中的最后 step 涉及定义 dynamic-language-backed bean 定义,每个 bean 用于您要配置的一个 bean(这与普通的 JavaBean configuration 没有区别)。但是,您可以使用<lang:language/>元素来定义动态 language-backed bean,而不是指定要由容器实例化和配置的 class 的完全限定类名。

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

  • <lang:groovy/>(Groovy)
  • <lang:bsh/>(BeanShell)
  • <lang:std/>(JSR-223)

configuration 可用的确切属性和 child 元素取决于 bean 已定义的语言(本章后面的 language-specific 部分详细说明了这一点)。

可刷新的Beans

其中一个(也许是单个)最引人注目的 value 在 Spring 中增加了动态语言支持,是“可刷新的 bean”feature。

可刷新的 bean 是 dynamic-language-backed bean。通过少量 configuration,dynamic-language-backed bean 可以监视其底层源文件资源的更改,然后在更改动态语言源文件时重新加载自身(例如,当您编辑并保存对文件系统上文件的更改时)。

这允许您将任意数量的动态语言源 files 部署为 application 的一部分,配置 Spring 容器以创建由动态语言源 files 支持的 beans(使用本章中描述的机制),以及(稍后,随着需求的变化或其他外部因素发挥作用)编辑动态语言源文件,并将它们所做的任何更改反映在由更改的动态语言源文件支持的 bean 中。无需关闭运行 application(或在 web application 的情况下重新部署)。如此修改的 dynamic-language-backed bean 从更改的动态语言源文件中获取新的 state 和逻辑。

默认情况下,此 feature 处于关闭状态。

现在我们可以看一个 example,看看开始使用可刷新的 beans 是多么容易。要打开可刷新的 beans feature,您必须在 bean 定义的<lang:language/>元素上指定一个额外的属性。因此,如果我们坚持本章前面的这个例子,下面的 example 将显示我们将在 Spring XML configuration 中更改的内容,以实现可刷新的 beans:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<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属性指定负 value 来关闭刷新行为。请记住,默认情况下,禁用刷新行为。如果您不想要刷新行为,请不要定义该属性。

如果我们然后运行以下 application,我们可以运行可刷新的 feature(请在下一个 code.)中找到“jumping-through-hoops-to-pause-the-execution”shenanigans。只有System.in.read()调用才会执行,以便程序的执行暂停而你(此处的开发人员)方案)关闭并编辑底层动态语言源文件,以便在程序恢复执行时,dynamic-language-backed bean 上的刷新触发器。

以下清单显示了此 sample application:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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());
}
}

然后,为了此 example 的目的,假设必须更改Messenger 实现的getMessage()方法的所有 calls,以使消息被引号括起来。以下清单显示了在程序执行暂停时您(开发人员)应对Messenger.groovy源文件所做的更改:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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。在完成对源文件的更改并保存并且程序继续执行之后,在 dynamic-language-backed Messenger implementation 上调用getMessage()方法的结果是'I Can Do The Frug'(注意包含附加引号)。

如果更改发生在refresh-check-delay value 的窗口内,则对脚本的更改不会触发刷新。在 dynamic-language-backed bean 上调用方法之前,实际上不会对脚本进行更改。只有在 dynamic-language-backed bean 上调用方法时,它才会检查其底层脚本源是否已更改。任何与刷新脚本相关的 exceptions(例如遇到编译错误或发现脚本文件已被删除)都会导致致命 exception 传播到调用 code。

前面描述的可刷新 bean 行为不适用于使用<lang:inline-script/>元素表示法定义的动态语言源 files(请参阅内联动态语言源文件。此外,它仅适用于实际可以检测到对基础源文件的更改的 beans(例如,通过 code 检查文件系统上存在的动态语言源文件的上次修改的 date)。

内联动态语言源文件

动态语言支持还可以满足直接嵌入 Spring bean 定义中的动态语言源 files。更具体地说,<lang:inline-script/>元素允许您在 Spring configuration 文件中立即定义动态语言源。 示例可能会澄清内联脚本 feature 的工作原理:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<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 configuration 文件中定义动态语言源的优良做法的问题放在一边,那么<lang:inline-script/>元素在某些情况下会很有用。例如,我们可能希望快速将 Spring Validator implementation 添加到 Spring MVC Controller。这只是 moment 使用内联源的工作。 (对于这样的 example.),见脚本验证器

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

关于 Spring 的动态语言支持,有一件非常重要的事情需要注意。也就是说,您不能(当前)向 dynamic-language-backed beans 提供构造函数 arguments(因此,constructor-injection 不适用于 dynamic-language-backed beans)。为了使构造函数和 properties 的这种特殊处理 100%清晰,以下 code 和 configuration 的混合不起作用:

示例 1.无法使用的方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 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

}
1
2
3
4
5
6
7
8
9
10
<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>

实际上,这种限制并不像它最初出现时那么重要,因为二手注入是绝大多数开发人员所青睐的注入风格(我们将讨论是否对另一天来说是好事)。

GroovyBeans

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

Groovy library 依赖项

Spring 中的 Groovy 脚本支持要求以下 libraries 位于 application 的 classpath 中:

  • groovy-1.8.jar
  • asm-3.2.jar
  • antlr-2.7.7.jar

Groovy 主页包括以下描述:

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

如果您已经从顶部开始阅读本章,那么您已经现在考虑另一个 example(再次使用 Spring 测试套件中的 example):

1
2
3
4
5
6
7
package org.springframework.scripting;

public interface Calculator {

int add(int x, int y);

}

以下 example 在 Groovy 中实现Calculator接口:

1
2
3
4
5
6
7
8
9
10
// from the file 'calculator.groovy'
package org.springframework.scripting.groovy

class GroovyCalculator implements Calculator {

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

}

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

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

最后,以下小应用程序执行前面的 configuration:

1
2
3
4
5
6
7
8
9
10
11
12
13
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 = (Calculator) ctx.getBean("calculator");
System.out.println(calc.add(2, 8));
}
}

运行上述程序产生的结果是(不出所料)10。 (有关更有趣的示例,请参阅动态语言展示项目以获取更复杂的 example 或参见本章后面的示例方案

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

使用回调自定义GroovyObjects

GroovyObjectCustomizer接口是一个回调,它允许你将_更多创建逻辑添加到_创建 Groovy-backed bean 的 process 中。对于 example,此接口的 implementations 可以调用任何所需的初始化方法,设置一些默认的 property 值,或指定自定义MetaClass。以下清单显示了GroovyObjectCustomizer接口定义:

1
2
3
4
public interface GroovyObjectCustomizer {

void customize(GroovyObject goo);
}

Spring Framework 实例化 Groovy-backed bean 的实例,然后将创建的GroovyObject传递给指定的GroovyObjectCustomizer(如果已定义的话)。您可以使用提供的GroovyObject reference 执行任何操作。我们希望大多数人都希望使用此回调设置自定义MetaClass,以下 example 显示了如何执行此操作:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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 中对 meta-programming 的完整讨论超出了 Spring reference 手册的范围。请参阅 Groovy reference 手册的相关部分或在线搜索。很多文章都论述了这个主题。实际上,如果使用 Spring 名称空间支持,则使用GroovyObjectCustomizer很容易,如下面的 example 所示:

1
2
3
4
5
6
7
<!-- 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功能,如下面的 example 所示:

1
2
3
4
5
6
7
8
9
<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 Framework 4.3.3 开始,您也可以在与 Spring 的GroovyObjectCustomizer相同的位置指定 Groovy CompilationCustomizer(例如ImportCustomizer)甚至是完整的 Groovy CompilerConfiguration object。

BeanShell-Beans

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

BeanShell library 依赖项

Spring 中的 BeanShell 脚本支持要求以下 libraries 位于 application 的 classpath 中:

  • bsh-2.0b4.jar

BeanShell 主页包括以下描述:{。 450}

“BeanShell 是一个小型,免费,可嵌入的 Java 源代码 - 解释器,带有动态语言 features,用 Java 编写.BeanShell 动态执行标准 Java 语法并使用 common 脚本编写方便性扩展它,例如松散类型,命令和方法闭包,如 Perl 和 JavaScript 中的那些“。

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

现在我们可以展示一个完全工作的 example,使用 BeanShell-based bean 实现本章前面定义的Messenger接口。我们再次展示了Messenger接口的定义:

1
2
3
4
5
6
7
package org.springframework.scripting;

public interface Messenger {

String getMessage();

}

以下 example 显示了Messenger接口的 BeanShell“implementation”(我们在这里使用松散的术语):

1
2
3
4
5
6
7
8
9
String message;

String getMessage() {
return message;
}

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

下面的 example 显示了定义上述“class”的“实例”的 Spring XML(同样,我们在这里非常松散地使用这些术语):

1
2
3
4
5
<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-based beans 的某些情况,请参阅方案

方案

在脚本语言中定义 Spring 托管 beans 会有益的可能场景是多种多样的。本节介绍了 Spring 中动态语言支持的两种可能用例。

脚本SpringMVC控制器

可以从使用 dynamic-language-backed beans 中受益的一组 classes 是 Spring MVC 控制器。在纯 Spring MVC applications 中,通过 web application 的导航流程在很大程度上取决于封装在 Spring MVC 控制器中的 code。由于需要更新 web application 的导航流和其他表示层逻辑以响应支持问题或更改业务需求,因此通过编辑一个或多个动态语言源 files 并查看这些所需的更改可能更容易实现更改立即反映在 running application 的 state 中。

请记住,在由 Spring 等项目支持的轻量级体系结构 model 中,您通常希望拥有一个非常精简的表示层,其中 application 的所有内容业务逻辑都包含在域和服务层 classes 中。 Developing Spring MVC 控制器为 dynamic-language-backed beans 允许您通过编辑和保存文本 files 来更改表示层逻辑。对此类动态语言源 files 的任何更改(取决于 configuration)都会自动反映在动态语言源 files 支持的 beans 中。

要实现对 dynamic-language-backed beans 的任何更改的自动“拾取”,您必须启用“可刷新的 beans”功能。有关此 feature 的完整处理,请参阅可刷新的 Beans

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 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())
}

}
1
2
3
4
5
<lang:groovy id="fortune"
refresh-check-delay="3000"
script-source="/WEB-INF/groovy/FortuneController.groovy">
<lang:property name="fortuneService" ref="fortuneService"/>
</lang:groovy>

脚本验证器

使用 Spring 进行 application 开发的另一个领域可能会受益于 dynamic-language-backed beans 提供的灵活性。与常规 Java 相比,通过使用松散类型的动态语言(也可能支持内联正则表达式)来表达复杂的验证逻辑可能更容易。

同样,developing validators as dynamic-language-backed beans 允许您通过编辑和保存简单的文本文件来更改验证逻辑。任何此类更改(根据 configuration)自动反映在 running application 的执行中,并且不需要重新启动 application。

要实现对 dynamic-language-backed beans 的任何更改的自动“拾取”,您必须启用’refreshable beans’feature。有关此 feature 的详细信息,请参阅可刷新的 Beans

以下 example 显示了使用 Groovy 动态语言实现的 Spring org.springframework.validation.Validator(有关Validator接口的讨论,请参阅使用 Spring 的 Validator 接口进行验证

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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.")
}

}

额外细节

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

AOP-通知脚本Beans

您可以使用 Spring AOP framework 来建议脚本 beans。 Spring AOP framework 实际上并不知道被建议的 bean 可能是脚本化的 bean,因此您使用(或旨在使用)的所有 AOP 用例和功能都与脚本 beans 一起使用。当您建议脚本 beans 时,您不能使用 class-based 代理。你必须使用interface-based 代理

您不仅限于为脚本 beans 提供建议。您还可以使用受支持的动态语言编写方面本身,并使用此类 beans 来建议其他 Spring beans。这确实是动态语言支持的高级用法。

作用域

如果不是很明显,脚本 beans 的范围可以与任何其他 bean 相同。各种<lang:language/>元素上的scope属性允许您控制底层脚本 bean 的范围,就像使用常规 bean 一样。 (默认范围是singleton,因为它与“常规”beans.)一样

以下 example 使用scope属性来定义作为原型的 Groovy bean:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<?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 http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/lang http://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 Framework 中作用域支持的完整讨论,请参阅IoC 容器中的Bean 范围

langXMLschema

Spring XML configuration 中的lang元素处理将以动态语言(如 JRuby 或 Groovy)编写的 objects 暴露为 Spring 容器中的 beans。

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

要使用lang schema 中的元素,您需要在 Spring XML configuration 文件的顶部添加以下前导码。以下代码段中的文本引用了正确的 schema,以便lang命名空间中的标记可供您使用:

1
2
3
4
5
6
7
8
9
10
<?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 http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/lang http://www.springframework.org/schema/lang/spring-lang.xsd">

<!-- bean definitions here -->

</beans>

更多资源

以下链接将介绍有关本章所述各种动态语言的更多信息: