【译】Spring官方文档:Spring 语言支持

Version 5.0.7.RELEASE

1.Kotlin

Kotlin是一门运行于JVM(或其他平台)之上的静态语言,它可以在提供和现有Java编写的库的良好交互性的同时,写出简明优雅的代码。
Spring框架为Kotlin提供第一手的支持从而使得开发者在开发Kotlin应用时会感觉Spring框架就像是用原生Kotlin编写的框架一样。
学习Spring + Kotlin的方式就是看这篇全面的教程。您可以顺便加入Kotlin Slack的#spring频道,或在需要支持的时候在Stackoverflow上使用springkotlin标签来提问。

1.1.系统需求

Spring框架支持Kotlin 1.1+,并且需要在classpath里提供kotlin-stdlib(或者其他对应的某个,例如kotlin-stdlib-jre8对应Kotlin 1.1,kotlin-stdlib-jdk8对应 Kotlin 1.2)和kotlin-reflect。如果是在start.spring.io上启动的Kotlin项目的话,这些依赖默认就会被提供。

1.2.扩展

Kotlin的扩展提供了给现有存在的类添加额外功能的能力。Spring框架的Kotlin API充分运用了扩展来给现有的Spring API提供新的Kotlin独有的便利。
Spring框架 KDoc API上列举并给出了所有可用的Kotlin扩展和DSL的文档。

要记住Kotlin的扩展在使用之前要先导入。举例来说,GenericApplicationContext.registerBean这个Kotlin扩展只有在org.springframework.context.support.registerBean被导入了才会起作用。也就是说,和静态导入类似,IDE应该在大多数情境下对这种导入进行自动提示。

例如,Kotin 具体类型参数提供了针对JVM泛型类型擦除的一种变通方案,并且Spring框架提供了很多运用了此功能的扩展。这使得Kotlin版的API,例如Spring WebFlux上的新的WebClient——RestTemplate,还有其他很多API,都变得更好了。

像Reactor和Spring Data等库的API也提供了Kotlin扩展,因此从整体上给予了Kotlin更好的开发体验。

想要在Java中获取一系列Foo对象,我们会自然地写成这样:

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

但在使用了Spring的Kotlin扩展后,我们可能会这样写:

val users = client.get().retrieve().bodyToFlux<User>()
// 或者(两者是等价的)
val users : Flux<User> = client.get().retrieve().bodyToFlux()

和在Java里一样,Kotlin中的users是强类型的,但Kotlin更聪明的类型推断可以简化语法。

1.3.空值安全

Kotlin的关键特征之一就是空值安全——在编译期间干净地解决null值问题,而不是在运行期间撞到著名的NullPointerException异常。使用可空声明和“值或非值”表达式可以让应用更加安全,而不用花费精力在Optional这种包装器上。(Kotlin允许在空值上使用函数;参考这篇Kotlin空值安全全面教程。)
尽管Java不能在其类型系统中表达空值安全,但Spring框架现在通过位于org.springframework.lang包下的工具友好注解提供了一套全SPring框架适用的空值安全API。默认情况下,Kotlin中使用的来自Java API的类型会被认为是平台类型的,这种类型不会执行严格的控制检查。Kotlin对JSR 305注解的支持 + Spring可空性注解通过在编译期间解决null相关问题可以给Kotlin开发者提供整个Spring框架的空值安全。

Reactor和Spring Data等库借助这一功能提供了空值安全API。

JSR 305检查可以通过添加 -Xjsr305编译器标识来配置,其选项为:-Xjsr305{strict|warn|ignore}
在Kotlin 1.1+中,默认的行为和-Xjsr305=warn是一致的。考虑到Kotlin从Spring API中推断的类型,想要启用Spring框架API的空值安全需要使用strict。但是心里需要明白,Spring API的可空性声明即使是小版本发布时也可能会更新,而且在未来有更多的检查会被添加进去。

目前尚不支持泛型类型参数、可变参数和数组元素的可空性,但会在即将发布的新版本里提供。访问相关讨论来获取最新消息。

1.4.接口和类

Spring框架支持多种Kotlin构造器,如通过主要构造器、不可变类数据绑定以及函数可选参数默认值等来实例化Kotlin类。
Kotlin的参数名称是使用一个专用的KotlinReflectionParameterNameDiscoverer来识别的,它可以在不启用Java 8 -parameters编译选项进行编译的情况下找到接口方法参数的名字。
在序列化/反序列化JSON数据时要用到的Jackson Kotlin模块,如果在classpath中能找到它就会自动启用,否则在Jackson和Kotlin都能检查到却找不到这个模块的时候,会通过日志记录一条警告。

1.5.注解

Spring框架同样也使用了Kotlin空值安全确认某个HTTP参数是否是必填的,而不需要定义required属性。这也意味着@RequestParam name: String?会被当做非必填项,反之@RequestParam name: String会被当做必填项。该功能同样支持Spring信息@Header注解。
在更简洁的风格中,使用@Autowired@Bean@Inject的Spring bean注入会使用该信息来确定某个bean是不是必须注入的。
例如,@Autowired lateinit var foo: Foo意味着Foo类型的bean必须注册到应用上下文里,而@Autowired lateinit var foo: Foo?在这个bean不存在时则不会抛出异常。
同理,@Bean fun baz(foo: Foo, bar: Bar?) = Baz(foo, bar)意味着Foo类型的bean必须注册到应用上下文里,但Bar类型的bean则可存在可不存在。同样的行为也存在于自动注入构造器参数里。

如果你正在使用具有属性的类或主要构造器参数中使用bean验证,那么你可能需要使用用方靶向注解(annotation use-site target),譬如@field:NotNull@get:Size(min=5, max=5),就像这篇Stack Overflow回复所讨论的。

1.6.定义bean的DSL

Spring 5框架提供了基于lambda的函数式的一种注册bean的方法作为XML或Java代码式配置(@Configuration@Bean)的替代方案。简单地说,它可以像FactoryBean那样借助lambda来注册bean。这种机制不需要任何反射或者CGLIB代理,故效率非常好。
我们先使用Java来实现一个例子:

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

但是在使用了Kotlin的具体类型参数和GenericApplicationContext后,可以简单地换成这种写法:

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

为了更加接近声明式的风格和更加简明的语法,Spring提供了Kotlin 定义bean 的DSL。它通过简洁的声明式API声明了一个ApplicationContextInitializer,用来处理自定义bean注册的profile和Enviroment

fun beans() = beans {
    bean<UserHandler>()
    bean<Routes>()
    bean<WebHandler>("webHandler") {
        RouterFunctions.toWebHandler(
            ref<Routes>().router(),
            HandlerStrategies.builder().viewResolver(ref()).build()
        )
    }
    bean("messageSource") {
        ReloadableResourceBundleMessageSource().appy {
            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("foo") {
        bean<Foo>()
    }
}

这这段代码中,bean<Routes>()可以使用构造器实现自动注入,ref<Routes>()applicationContext.getBean(Routes::class.java)的简写。
beans()方法用来给应用上下文注册bean。

val context = GenericApplicationCotnext().apply {
    beans().initialize(this)
    refresh()
}

DSL是可编程的,也就是说可以使用使用iffor循环和其他任何Kotlin功能来自定义bean的注册逻辑。

参考Spring的Kotlin函数式bean声明获取更多示例。

Spring Boot是基于Java代码式配置的,并且目前不特别提供函数式bean声明的支持。但可以通过Spring Boot的ApplicationContextInitializer来实验性地开启函数式bean声明的使用。参考这个Stack Overflow的回答来了解更多细节和最新信息。

1.7.Web

1.7.1.WebFlux函数式DSL

Spring框架现在提供了一种Kotlin路由DSL,使开发者可以将WebFlux函数式API替换为更加简洁并符合习惯的Kotlin代码:

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是可编程的,也就是说可以使用使用iffor循环和其他任何Kotlin功能来自定义注册逻辑。当路由的注册依赖于动态数据(例如来自数据库的数据)时,这种方式会非常有用。

参考MiXiT项目的路由获取具体示例。

1.7.2.Kotlin脚本模板

自4.3起,Spring框架提供了一种[ScriptTemplateView](https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/web/servlet/view/script/ScriptTemplateView.html),它可以使用支持JSR-223的脚本引擎来渲染模板。Spring 5在WebFlux中进一步扩展了这项功能,并使之支持了i18n和模板嵌套
Kotlin提供了类似的支持并且支持渲染基于Kotlin的模板,阅读这个提交获取详情。
于是我们有了许多有趣的应用场景——例如使用kotlinx.htmlDSL来编写类型安全的模板,或简单地以插值的形式使用Kotlin的多行String
这也使得在支持Kotlin的IDE中编写Kotlin模板时,可以获取全面的自动补全和重构支持:

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脚本模板示例项目获取详细信息。

1.8.Spring 项目中的Kotlin

本节的重点是在使用Kotlin开发Spring项目时需要特别注意的地方和值得了解的建议。

1.8.1.默认是final的

默认情况下,Kotlin的所有类都是final。用在类上的open标识符和Java里的final是正好相反的:加上它意味着其他类可以继承这个类。open同样可以用于成员函数,想要重写成员函数,就得给它们打上open标识符。
尽管Kotlin的JVM友好设计整体上对Spring是没有反作用的,但如果没有考虑到Kotlin的这项特性的话,那么在项目开始之初就可能就会遇到阻碍。这是因为Spring的bean通常是使用CGLIB来做代理的。例如@Configuration类,由于技术原因会在运行时被继承。变通的方案就是给每个通过CGLIB代理的类和成员方法都加上open关键字,但很快这种做法就会变得让人难以忍受,同事这也违背了Kotlin想要使代码保持简洁和可控的初衷。
幸运的是,Kotlin如今推出了一个叫做[kotlin-spring](https://kotlinlang.org/docs/reference/compiler-plugins.html#kotlin-spring-compiler-plugin)的插件,它是kotlin-allopen插件的一个预配置版本,会给添加了如下注解的类及它们的成员方法自动加上open标识符:

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

元注解支持意味着使用@Configuration@Controller@RestController@Service@Repository注解的类型同样会自动添加open,因为这些注解都被@Component注解了。
start.spring.io默认开启这一功能,所以在实践中您可以像写Java代码一样编写Kotlin bean,而不用添加额外的open关键字。

1.8.2.在持久化时使用不可变类

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

对于使用了Spring Data对象映射(如MongoDB、Redis、Cassandra等)的模块,在无需kotlin-noarg插件的情况下也可以使用Kotlin不可变类。

1.8.3.依赖注入

我们推荐尝试并喜欢上使用val只读(同时也可以加上不可空性)属性进行构造器注入的方式。

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

自Spring 4.3起,只有一个构造器的类,构造器的参数会自动注入。这也是为什么上述例子不需要声明为@Autowired constructor

如果真的需要使用字段注入,那就使用lateinit var结构。

@Component
class YourBean {

    @Autowired
    lateinit var mongoTemplate: MongoTemplate

    @Autowired
    lateinit var solrClient: SolrClient
}

1.8.4.配置属性注入

在Java中,可以使用@Value("${property}")之类的注解注入配置属性。然而在Kotlin中,$被用作字符串嵌入的保留字。
因此,如果想在Kotlin中使用@Value注解,$字符就需要转义为@Value("\${property}")
作为替代,可以声明如下bean自定义属性替换前缀:

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

现有仍在使用${...}的代码(如Spring Boot actuator或者@LocalServerPort),可以使用配置bean来自定义,代码如下:

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

@Bean
fun defaultPropertyConfigurer() = PropertySourcesPlaceholderConfigurer()

如果使用了Spring Boot,那么可以使用@ConfigurationProperties来代替@Value注解。因为基于构造器初始化的不可变类目前尚不支持,所以目前它只能用在lateinit或可空的var属性(推荐使用这种形式)上。参考@ConfigurationProperties针对不可变POJO的绑定@ConfigurationProperties针对接口的绑定这些问题获取详细信息。

1.8.5.注解上的数组属性

Kotlin注解和Java注解十分相像,但广泛运用于Spring的数组属性的行为却是不同的。如Kotlin文档所述,不同于其他属性,value属性名可以省略并指定为vararg参数。
要理解这一点,我们来看一下使用最广泛的Spring注解之一的@RequestMapping。这个Java注解的定义是这样的:

public @interface RequestMapping {

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

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

    RequestMethod[] method() default {};

    // ...
}

典型的@RequestMapping的使用场景是映射特定路径和请求方式的HTTP请求到对应的处理方法上。在Java中,给数组属性传入单个值是可以的,并且它会自动转化为一个数组。
这就是为什么我们可以直接写@RequestMapping(value = "/foo", method= RequestMethod.GET)@RequestMapping(path = "/foo", method = RequestMethod.GET)
然而,在Kotlin 1.2+,我们得写成@RequestMapping("/foo", method = [RequestMethod.GET])或者@RequestMapping(path = ["/foo"], method = [RequestMethod.GET])(需要给数组属性加上方括号)。
要换掉这个特殊的method属性(大多数情况下只有一个值)的方式,可以使用像@GetMapping或者@PostMapping这样的快捷注解。

提示:如果@RequestMappingmethod注解没有指定,所有的HTTP请求方法都将被匹配,而不单单是GET方法。

1.8.6.测试

逐类生命周期

Kotlin允许使用反引号给测试方法指定更有意义的名字。并且自JUnit 5起,Kotlin测试类可以使用@TestInstance(TestInstance.Lifecycle.PER_CLASS)注解使测试类只实例化一次,所以我们可以在非静态方法上使用@BeforeAll@AfterAll注解。这些都很适合Kotlin。
借助junit-platform.properties文件的junit.jupiter.testinstance.lifecycle.default = per_class配置项,我们现在可以将默认行为改为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()
  }
}

描述式测试

可以使用Kotlin和JUnit 5创建描述式测试。

class SpecificationLikeTests {

    @Tested
    @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类型推断在Kotlin中的问题

由于一个类型推断问题,所以确保使用Kotlin的expectBody扩展(像.expectBody<String>().isEqualTo("foo"))作为Kotlin在遇到Java API协作问题的变通。
同时请参考SPR-16057相关问题。

1.9.起步

1.9.1.start.spring.io

开发新的使用Kotlin的Spring 5项目的最简单的方式就是在start.spring.io上创建一个新的Spring Boot 2项目。
这篇博客所述,创建一个独立的WebFlux项目也是可以的。

1.9.2.选择web风格

Spring框架现在提供两套不同的web技术栈:Spring MVCSpring WebFlux
如果开发者想创建处理延迟、长连接、流等场景,或仅仅是想要使用web函数式KotlinDSL,那么推荐使用Spring WebFlux。
对于其他使用场景,特别是使用了JPA这样的阻塞技术,那么Spring MVC和它的基于注解的编程模型是一个完美有效且充分支持的选择。

1.10.资源

1.10.1.教程

1.10.2.博客文章

1.10.3.示例

1.10.4.问题

这里是一系列关于Spring + Kotlin支持的现有问题。

Spring框架

Spring Boot

Kotlin

2.Apache Groovy

Groovy是一门强大、可选参数类型的动态语言,并具有静态类型和静态编译的能力。它提供了简洁的语法,并且可以顺利地整合进任何现有的Java项目。
Spring框架提供了一个专用的ApplicationContext用以支持基于Groovy的bean定义DSL。更多信息请参考《GroovyBean定义DSL》。
关于Groovy的更多支持,如基于Groovy编写的bean、可刷新脚本bean等在下节《动态语言支持》中会介绍。

3.动态预言支持

3.1.简介

Spring 2.0引入了在Spring中使用那些用动态语言(如JRuby)定义的类和对象的功能的全面支持。这使得您可以使用某种可支持的动态语言编写任意数量的类,并让Spring容器透明地给结果对象实例化、配置、注入。
目前支持的动态语言有:

  • JRuby 1.5+
  • Groovy 1.8+
  • BeanShell 2.0

为什么只支持这些语言?
选择这些语言的原因:1.这些语言在Java企业社区有很大的吸引力;2.在添加这些语言的支持时不需要求助于其他语言;3.Spring的开发者最熟悉这些语言。

有关这些动态语言支持可以立即发挥作用的完整可用的示例将在《场景》这一节讨论。

3.2.第一个实例

本章节的大部分篇幅都集中在详细讨论动态语言的支持上。在深入研究这些动态语言支持的输入和输出之前,我们先来速览一个使用动态语言定义bean的实例。定义这个bean的动态语言是Groovy(这个实例从Spring测试用例中抽出来的,如果你想看一下其他语言的等价实现,请参考其源码)。
下文是Groovy bean将要实现的Messenger接口,注意它是使用纯Java来定义的。其他需要注入Messenger实例的对象并不知道其接口实现是基于Groovy的。

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() {
        // 注入进来的Messenger对象的使用…
    }

}

这里是使用Groovy对Messenger接口的实现:

// 来自“Messenger.groovy”文件
package org.springframework.scripting.groovy;

// 导入将要被实现的Messenger(使用Java编写)接口
import org.springframework.scripting.Messenger

// 使用Groovy实现接口
class GroovyMessenger implements Messenger {

    String message

}

最后给出的是会使Groovy所定义的Messenger实现注入到DefaultBookingService类起作用的bean配置代码。

要使用自定义动态语言标签来定义bean,开发者需要在Spring XML配置文件的顶部注明对应的XML Schema。此外,还得使用Spring ApplicationContext作为控制反转容器的实现。虽然使用简单的BeanFactory实现也是支持的,但开发者需要管理Spring内部的管道。
更多基于schema的配置信息,请参阅基于XML Schema的配置方式

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

    <!-- 定义Groovy的Messenger bean -->
    <lang:groovy id="messenger" script-source="classpath:Messenger.groovy">
        <lang:property name="message" value="I Can Do The Frug" />
    </lang:groovy>

    <!-- 被注入了Groovy实现的一个正常的bean -->
    <bean id="bookingService" class="x.y.DefaultBookingService">
        <property name="messenger" ref="messenger" />
    </bean>

</beans>

因为被注入的Messenger实例的确是实例化了,所以bookingService bean(DefaultBookingService的实现)现在可以像平常一样使用这个私有成员变量。在这里,只有普通的Java和普通的Groovy,没有任何特别的地方。
希望上面的XML代码段是不言自明的,如果不是的话也不要过分担心。请继续阅读有关上述配置的原因和内容的详细信息。

3.3.定义动态语言实现的bean

本节会讨论如何使用任意被支持的动态语言来定义Spring bean。
请记住本章并不试图阐述被支持动态语言的语法和方言。比如说,如果你想在应用上使用Groovy编写某些类,那么前提就是你已经学会使用Groovy了。如果需要更多关于动态语言本身的信息,请参考本章末尾的《更多资源》部分。

3.3.1.通用概念

使用动态语言bean需要如下步骤:

  • 编写动态语言源码的测试(当然了)
  • 编写动态语言源码本身
  • 在XML配置文件中使用适当的<lang:language/>定义这些动态语言bean(当然也可以使用Spring API编程式地定义这些bean,不过你得自行查阅源码寻找方法,因为本章不涉及这种进阶配置的介绍)。记住这是一个不断迭代的步骤。每个动态语言源码文件至少包含一个bean的定义(不过一个动态语言源码文件可以被多个bean的定义引用)。

前两个步骤(测试和编写动态语言源码)不在本章的范围之内,请自行参阅相关语言说明或参考手册进行开发。不过你仍然可以先阅读本章剩余部分,因为Spring的动态语言支持确实对动态语言的源码文件做了一些(小的)假设。

<lang:language/> 标签

最后一步是定义每个你想要配置的动态语言bean的配置(和通常JavaBean的定义没有什么不同)。然而,和以往通过指定想要由容器来实例化和配置的bean的完整类名的方式不同,我们得使用<lang:language/>标签来定义这些bean。
每种被支持的语言都有对应的<lang:language/>标签:

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

配置时对应的属性和子标签取决于定义这个bean所使用的语言(针对具体语言特性的全部内幕将会在本节稍后提供)。

可刷新bean

Spring的动态语言支持中最吸引人的附加价值就是“可刷新bean”功能。
可刷新bean是带有少量配置的动态语言bean。动态语言bean可以监视其所属源代码资源的改动,并在它们发生改变的时候重新加载自己(例如在开发者在文件系统上编辑并保存了源文件)。
这使得开发者可以部署任意数量的动态语言源码作为应用的一部分,配置Spring容器创建基于这些动态语言源码的bean(使用本章提到的机制),并在之后当需求发生了变动或一些附加的要素添加进来的时候,简单地编辑动态语言源码并让所有的改动反映这些被修改的源码文件对应的bean上即可。不需要停止正在运行的应用(如果是web应用,也不需要重部署)。如此修改的动态语言bean将从更改的动态语言源文件中获取新的状态和逻辑。

请记住该功能默认是关闭的。

让我们通过一个示例来看一下开始使用可刷新bean是多么的简单吧。想要开启可刷新bean的功能,只需要给对应bean的`<lang:language/>标签附加一个属性即可。假设继续使用之前的例子,这里给出使可刷新bean起作用的修改后的Spring XML 配置文件:

<beans>

    <!-- 由于存在“refresh-check-delay”属性,这个bean现在是可刷新的 -->
    <lang:groovy id="messenger"
            refresh-check-delay="5000" <!-- 如果有改动,会在五秒内触发刷新 -->
            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”上定义的“refresh-check-delay”属性是检测到对应源码修改后触发刷新的毫秒数。给它传入一个复制就可以关闭刷新行为。请记住,默认情况下,刷新行为是关闭的。如果不需要刷新行为,简单地不要定义该属性就好。
现在运行这个应用,我们就可以演练一下可刷新bean的功能了。请理解下面代码中“跳转到等待输入阶段以暂停执行”的场景。调用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());
        // 暂停执行从而可以去对源码文件进行修改…
        System.in.read();
        System.out.println(messenger.getMessage());
    }
}

接下来,假设在本示例中,MessagegetMessage()方法需要变为输出单引号包裹的消息。下文是我们在程序暂停执行时对Messenger.groovy源文件所做的改动。

package org.springframework.scripting

class GroovyMessenger implements Messenger {

    private String message = "Bingo"

    public String getMessage() {
        // 修改方法实现,将消息包裹在单引号里面
        return "'" + this.message + "'"
    }

    public void setMessage(String message) {
        this.message = message
    }
}

程序在执行的时候,在输入暂停之前会输出“I Can Do The Frug”。在修改并保存了源码文件之后,再回复程序执行,getMessage()方法的调用结果就变成了“'I Can Do The Frug'”(注意多出了单引号)。
理解在'refresh-check-delay'的时间之内,脚本更改将不会触发刷新是很重要的。和这一点同样重要的是理解在动态语言bean的方法被调用之前,脚本的改动实际上不会被“拾取”。 只有动态语言bean上的方法被调用的时候,系统才会检查对应的脚本源码是否更改过了。任何和刷新脚本相关的异常(如遇到编译异常,或对应文件不存在)都会产生传播到调用代码时的致命异常。
上文提到的可刷新bean行为不适用于使用<lang:inline-script/>标签(参考《内联动态语言源文件》)定义的脚本文件。它仅仅适用于那些源文件的更改可以清楚地被检查到的bean。例如,使用代码检查存放于文件系统的动态语言源文件的最后编辑时间。

内联动态语言源文件

Spring同样支持将动态语言文件直接嵌入到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 Validator 的实现给Spring MVC 的Controller。这不过是使用内联源码的一种情景。(参考《脚本式验证器》上的示例。)

理解动态语言bean的上下文构造器注入

关于Spring的动态语言支持,有一点非常重要。也就是说,(当前)不能够给动态语言bean的构造器传递参数(因此构造器注入在动态语言bean上是不可用的)。为了完全搞懂这种针对构造器和属性的特殊处理,以下代码和配置将起作用。

// 来自文件 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">

    <!-- 下面这个构造器参数并不能注入到GroovyMessenger中 -->
    <!-- 事实上,根据schema的定义,是不允许这样写的 -->
    <constructor-arg value="不起作用" />

    <!-- 只有属性值可以注入到动态语言对象中 -->
    <lang:property name="anotherMessage" value="直接传给动态语言对象" />

</lang>

在实践中这条限制并不如它起初看上去那么重要,因为setter注入模式被压倒性数量的开发者所喜爱(让我们改天再来讨论这是不是一件好事)。

3.3.2.Groovy bean

待翻。

3.3.3.BeanShell bean

BeanShell库依赖
Spring的BeanShell脚本支持需要在项目的classpath中引入如下库:

  • bsh-2.0b4.jar

来自BeanShell的主页:

BeanShell是一个使用Java编写的小型、免费、可嵌入的具有动态语言功能的Java源码解释器。BeanShell动态地执行标准Java语法,并给它扩展了脚本语言共通的方便特征,如弱类型、指令和方法闭包,就像Perl和JavaScript里的那样。
和Groovy不同,BeanShell bean需要(少许)额外配置。Spring的BeanShell动态语言支持实现很有趣的一点在于:Spring创建了一个JDK动态代理来实现所有指定了'script-interfaces'属性的<lang:bsh>标签所对应的接口(这也是为什么你必须至少给这个属性提供一个接口,并且在使用BeanShell bean时(相应地)编写接口)。这意味着BeanShell对象上的所有方法的调用都会经过JDK动态代理调用机制。
我们来看一个使用BeanShell bean来实现之前章节定义的Messenger接口(为了方便阅读在下文给出接口代码)的一个完全可用的例子。

package org.springframework.scripting;

public interface Messenger {

    String getMessage();

}

这里是BeanShell对Messenger接口的“实现”(不是严格意义的术语)。

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.4.场景

当然了,使用脚本语言定义Spring bean的可能场合多且有效。本节讨论两个可能的使用场景。

3.4.1.脚本式Spring MVC 控制器

在使用动态语言bean时,有一组类可能会从中受益,那就是Spring MVC 控制器。在纯Spring MVC应用中,web应用的导航流很大程度上是封装在Spring MVC控制器中的。由于需要修复问题或更改业务需求,所以导航流和其他web应用展示层逻辑常常需要修改。通过修改一个或多个动态语言源文件就能看到它们在一个正在运行的项目中立即生效,可能更容易完成此类修改需求。
记住,在如Spring这种轻量级架构模型项目中,开发者的通常的目标是使用真的非常轻量的展示层,将应用中所有丰富的业务逻辑放置到领域层和业务层的类中。使用动态语言bean开发Spring MVC 控制器使开发者只需要编辑并保存文本文件就可以切换视图层逻辑;在动态语言源文件中的任何修改将(取决于配置)自动映射到对应的bean中去。

为了能够自动“拾取”到动态语言bean上的任何更改,开发者需要启动“可刷新bean”功能。参考《可刷新bean》获得关于该功能的详细信息。

下文是使用Groovy动态语言来实现org.springframework.web.servlet.mvc.Controller的例子。

// 来自文件 '/WEB-INF/groovy/FortuneController.groovy'
package org.springframework.showcase.fortune.web

import org.springframework.showcase.fortune.service.FortuneService
import org.springframework.showcase.fortune.domain.Fortune
import org.springframework.web.servlet.ModelAndView
import org.springframework.web.servlet.mvc.Controller

import javax.servlet.http.HttpServletRequest
import javax.servlet.http.HttpServletResponse

class FortuneController implements Controller {

    @Property FortuneService fortuneService

    ModelAndView handleRequest(HttpServletRequest request,
            HttpServletResponse httpServletResponse) {
        return new ModelAndView("tell", "fortune", this.fortuneService.tellFortune())
    }

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

3.4.2.脚本式校验器

另一个使用动态语言bean可以提高Spring项目开发的灵活性的领域是校验。也许使用弱类型的动态语言(也许还支持内联正则表达式)来提供复杂验证逻辑相较于传统的Java会更加方便。
再说一遍,使用动态语言bean是你可以仅仅通过编辑和保存文本文件就可以更改校验逻辑;任何更改都将(取决于配置)自动映射到已经启动的应用上,而无需重启项目。

请记住,为了能够自动“拾取”到动态语言bean上的任何更改,开发者需要启动“可刷新bean”功能。参考《可刷新bean》获得关于该功能的详细信息。

下文是使用Groovy动态语言实现一个Spring org.springframework.validation.Validator校验器的例子。(参考《使用Spring Validator接口进行校验》获取关于Validator接口的更多信息。)

import org.springframework.validation.Validator
import org.springframework.validation.Errors
import org.springframework.beans.TestBean

class TestBeanValidator implements Validator {

    boolean supports(Class clazz) {
        return TestBean.class.isAssignableFrom(clazz)
    }

    void validate(Object bean, Errors errors) {
        if(bean.name?.trim()?.size() > 0) {
            return
        }
        errors.reject("whitespace", "Cannot be composed wholly of whitespace.")
    }

}

3.5.杂七杂八

本节包含于动态语言支持相关的零碎的事情。

3.5.1.AOP - 切入脚本式bean

使用Spring AOP 切入脚本式bean是可行的。Spring AOP框架事实上并不知道将要切入的某个bean是不是脚本式bean。所以在你的开发中,所有可能会用到或打算用的AOP的使用场景和功能都能工作在脚本式bean上。但也有一件(小)事情在切入脚本式bean时是要注意的,那就是不能使用基于类的代理,必须得使用(基于接口的代理)[https://docs.spring.io/spring-framework/docs/current/spring-framework-reference/core.html#aop-proxying]
当然不仅切入脚本式bean时没有限制的,使用某种被支持的动态语言来编写切面处理其本身从而切入其他Spring bean也是可以的。尽管这真得算得上是动态语言支持的高阶用法了。

3.5.2.Scope

如果不是很明显,脚本式bean当然可以像任何其他bean一样设置scope。各种<lang:language />标签上的scope属性使你可以控制对应的脚本式bean的scope,就像传统的bean一样。(默认的scope是(单例的)[https://docs.spring.io/spring-framework/docs/current/spring-framework-reference/core.html#beans-factory-scopes-singleton],和“传统的”bean一样。)
下文是使用scope属性定义一个Groovy bean的scope为(prototype)[https://docs.spring.io/spring-framework/docs/current/spring-framework-reference/core.html#beans-factory-scopes-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 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>

参考《(控制反转容器)[https://docs.spring.io/spring-framework/docs/current/spring-framework-reference/core.html#beans]》里的《(Bean scope)[https://docs.spring.io/spring-framework/docs/current/spring-framework-reference/core.html#beans]》获取更多关于Spring框架中scope支持的信息。

3.5.3.lang XML schema

Spring XML 配置文件中的lang标签是用来处理使用如JRuby或Groovy这种动态语言编写的对象作为Spring容器的bean的。
这些标签(和动态语言支持)在《动态语言支持》这一章进行了全面讲解。请阅读这一章来学习动态语言支持和这些lang标签本身的知识。
为了完整性,要想使用lang schema,需要在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 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的配置 -->

</beans>

3.6.更多资源

通过如下链接来获取关于本章讨论的对应脚本语言的更多资源。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,117评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,328评论 1 293
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,839评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,007评论 0 206
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,384评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,629评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,880评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,593评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,313评论 1 243
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,575评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,066评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,392评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,052评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,082评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,844评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,662评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,575评论 2 270

推荐阅读更多精彩内容