【譯】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.更多資源

通過如下鏈接來獲取關于本章討論的對應腳本語言的更多資源。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容