Version 5.0.7.RELEASE
1.Kotlin
Kotlin是一門運行于JVM(或其他平臺)之上的靜態(tài)語言,它可以在提供和現(xiàn)有Java編寫的庫的良好交互性的同時,寫出簡明優(yōu)雅的代碼。
Spring框架為Kotlin提供第一手的支持從而使得開發(fā)者在開發(fā)Kotlin應(yīng)用時會感覺Spring框架就像是用原生Kotlin編寫的框架一樣。
學(xué)習(xí)Spring + Kotlin的方式就是看這篇全面的教程。您可以順便加入Kotlin Slack的#spring頻道,或在需要支持的時候在Stackoverflow上使用spring
或kotlin
標(biāo)簽來提問。
1.1.系統(tǒng)需求
Spring框架支持Kotlin 1.1+,并且需要在classpath里提供kotlin-stdlib
(或者其他對應(yīng)的某個,例如kotlin-stdlib-jre8
對應(yīng)Kotlin 1.1,kotlin-stdlib-jdk8
對應(yīng) Kotlin 1.2)和kotlin-reflect
。如果是在start.spring.io上啟動的Kotlin項目的話,這些依賴默認(rèn)就會被提供。
1.2.擴(kuò)展
Kotlin的擴(kuò)展提供了給現(xiàn)有存在的類添加額外功能的能力。Spring框架的Kotlin API充分運用了擴(kuò)展來給現(xiàn)有的Spring API提供新的Kotlin獨有的便利。
Spring框架 KDoc API上列舉并給出了所有可用的Kotlin擴(kuò)展和DSL的文檔。
要記住Kotlin的擴(kuò)展在使用之前要先導(dǎo)入。舉例來說,
GenericApplicationContext.registerBean
這個Kotlin擴(kuò)展只有在org.springframework.context.support.registerBean
被導(dǎo)入了才會起作用。也就是說,和靜態(tài)導(dǎo)入類似,IDE應(yīng)該在大多數(shù)情境下對這種導(dǎo)入進(jìn)行自動提示。
例如,Kotin 具體類型參數(shù)提供了針對JVM泛型類型擦除的一種變通方案,并且Spring框架提供了很多運用了此功能的擴(kuò)展。這使得Kotlin版的API,例如Spring WebFlux上的新的WebClient
——RestTemplate
,還有其他很多API,都變得更好了。
像Reactor和Spring Data等庫的API也提供了Kotlin擴(kuò)展,因此從整體上給予了Kotlin更好的開發(fā)體驗。
想要在Java中獲取一系列Foo
對象,我們會自然地寫成這樣:
Flux<User> users = client.get().retrieve().bodyToFlux(User.class)
但在使用了Spring的Kotlin擴(kuò)展后,我們可能會這樣寫:
val users = client.get().retrieve().bodyToFlux<User>()
// 或者(兩者是等價的)
val users : Flux<User> = client.get().retrieve().bodyToFlux()
和在Java里一樣,Kotlin中的users
是強(qiáng)類型的,但Kotlin更聰明的類型推斷可以簡化語法。
1.3.空值安全
Kotlin的關(guān)鍵特征之一就是空值安全——在編譯期間干凈地解決null
值問題,而不是在運行期間撞到著名的NullPointerException
異常。使用可空聲明和“值或非值”表達(dá)式可以讓應(yīng)用更加安全,而不用花費精力在Optional
這種包裝器上。(Kotlin允許在空值上使用函數(shù);參考這篇Kotlin空值安全全面教程。)
盡管Java不能在其類型系統(tǒng)中表達(dá)空值安全,但Spring框架現(xiàn)在通過位于org.springframework.lang
包下的工具友好注解提供了一套全SPring框架適用的空值安全API。默認(rèn)情況下,Kotlin中使用的來自Java API的類型會被認(rèn)為是平臺類型的,這種類型不會執(zhí)行嚴(yán)格的控制檢查。Kotlin對JSR 305注解的支持 + Spring可空性注解通過在編譯期間解決null
相關(guān)問題可以給Kotlin開發(fā)者提供整個Spring框架的空值安全。
Reactor和Spring Data等庫借助這一功能提供了空值安全API。
JSR 305檢查可以通過添加 -Xjsr305
編譯器標(biāo)識來配置,其選項為:-Xjsr305{strict|warn|ignore}
。
在Kotlin 1.1+中,默認(rèn)的行為和-Xjsr305=warn
是一致的。考慮到Kotlin從Spring API中推斷的類型,想要啟用Spring框架API的空值安全需要使用strict
。但是心里需要明白,Spring API的可空性聲明即使是小版本發(fā)布時也可能會更新,而且在未來有更多的檢查會被添加進(jìn)去。
目前尚不支持泛型類型參數(shù)、可變參數(shù)和數(shù)組元素的可空性,但會在即將發(fā)布的新版本里提供。訪問相關(guān)討論來獲取最新消息。
1.4.接口和類
Spring框架支持多種Kotlin構(gòu)造器,如通過主要構(gòu)造器、不可變類數(shù)據(jù)綁定以及函數(shù)可選參數(shù)默認(rèn)值等來實例化Kotlin類。
Kotlin的參數(shù)名稱是使用一個專用的KotlinReflectionParameterNameDiscoverer
來識別的,它可以在不啟用Java 8 -parameters
編譯選項進(jìn)行編譯的情況下找到接口方法參數(shù)的名字。
在序列化/反序列化JSON數(shù)據(jù)時要用到的Jackson Kotlin模塊,如果在classpath中能找到它就會自動啟用,否則在Jackson和Kotlin都能檢查到卻找不到這個模塊的時候,會通過日志記錄一條警告。
1.5.注解
Spring框架同樣也使用了Kotlin空值安全確認(rèn)某個HTTP參數(shù)是否是必填的,而不需要定義required
屬性。這也意味著@RequestParam name: String?
會被當(dāng)做非必填項,反之@RequestParam name: String
會被當(dāng)做必填項。該功能同樣支持Spring信息@Header
注解。
在更簡潔的風(fēng)格中,使用@Autowired
、@Bean
、@Inject
的Spring bean注入會使用該信息來確定某個bean是不是必須注入的。
例如,@Autowired lateinit var foo: Foo
意味著Foo
類型的bean必須注冊到應(yīng)用上下文里,而@Autowired lateinit var foo: Foo?
在這個bean不存在時則不會拋出異常。
同理,@Bean fun baz(foo: Foo, bar: Bar?) = Baz(foo, bar)
意味著Foo
類型的bean必須注冊到應(yīng)用上下文里,但Bar
類型的bean則可存在可不存在。同樣的行為也存在于自動注入構(gòu)造器參數(shù)里。
如果你正在使用具有屬性的類或主要構(gòu)造器參數(shù)中使用bean驗證,那么你可能需要使用用方靶向注解(annotation use-site target),譬如
@field:NotNull
或@get:Size(min=5, max=5)
,就像這篇Stack Overflow回復(fù)所討論的。
1.6.定義bean的DSL
Spring 5框架提供了基于lambda的函數(shù)式的一種注冊bean的方法作為XML或Java代碼式配置(@Configuration
和@Bean
)的替代方案。簡單地說,它可以像FactoryBean
那樣借助lambda來注冊bean。這種機(jī)制不需要任何反射或者CGLIB代理,故效率非常好。
我們先使用Java來實現(xiàn)一個例子:
GenericApplicationContext context = new GenericApplicationContext();
context.registerBean(Foo.class);
context.registerBean(Bar.class, () -> new Bar(context.getBean(Foo.class))
但是在使用了Kotlin的具體類型參數(shù)和GenericApplicationContext
后,可以簡單地?fù)Q成這種寫法:
val context = GenericApplicationContext().apply {
registerBean<Foo>()
registerBean { Bar(it.getBean<Foo>()) }
}
為了更加接近聲明式的風(fēng)格和更加簡明的語法,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>()
可以使用構(gòu)造器實現(xiàn)自動注入,ref<Routes>()
是applicationContext.getBean(Routes::class.java)
的簡寫。
beans()
方法用來給應(yīng)用上下文注冊bean。
val context = GenericApplicationCotnext().apply {
beans().initialize(this)
refresh()
}
DSL是可編程的,也就是說可以使用使用
if
、for
循環(huán)和其他任何Kotlin功能來自定義bean的注冊邏輯。
參考Spring的Kotlin函數(shù)式bean聲明獲取更多示例。
Spring Boot是基于Java代碼式配置的,并且目前不特別提供函數(shù)式bean聲明的支持。但可以通過Spring Boot的
ApplicationContextInitializer
來實驗性地開啟函數(shù)式bean聲明的使用。參考這個Stack Overflow的回答來了解更多細(xì)節(jié)和最新信息。
1.7.Web
1.7.1.WebFlux函數(shù)式DSL
Spring框架現(xiàn)在提供了一種Kotlin路由DSL,使開發(fā)者可以將WebFlux函數(shù)式API替換為更加簡潔并符合習(xí)慣的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是可編程的,也就是說可以使用使用
if
、for
循環(huán)和其他任何Kotlin功能來自定義注冊邏輯。當(dāng)路由的注冊依賴于動態(tài)數(shù)據(jù)(例如來自數(shù)據(jù)庫的數(shù)據(jù))時,這種方式會非常有用。
參考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中進(jìn)一步擴(kuò)展了這項功能,并使之支持了i18n和模板嵌套。
Kotlin提供了類似的支持并且支持渲染基于Kotlin的模板,閱讀這個提交獲取詳情。
于是我們有了許多有趣的應(yīng)用場景——例如使用kotlinx.htmlDSL來編寫類型安全的模板,或簡單地以插值的形式使用Kotlin的多行String
。
這也使得在支持Kotlin的IDE中編寫Kotlin模板時,可以獲取全面的自動補(bǔ)全和重構(gòu)支持:
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腳本模板示例項目獲取詳細(xì)信息。
1.8.Spring 項目中的Kotlin
本節(jié)的重點是在使用Kotlin開發(fā)Spring項目時需要特別注意的地方和值得了解的建議。
1.8.1.默認(rèn)是final的
默認(rèn)情況下,Kotlin的所有類都是final
的。用在類上的open
標(biāo)識符和Java里的final
是正好相反的:加上它意味著其他類可以繼承這個類。open
同樣可以用于成員函數(shù),想要重寫成員函數(shù),就得給它們打上open
標(biāo)識符。
盡管Kotlin的JVM友好設(shè)計整體上對Spring是沒有反作用的,但如果沒有考慮到Kotlin的這項特性的話,那么在項目開始之初就可能就會遇到阻礙。這是因為Spring的bean通常是使用CGLIB來做代理的。例如@Configuration
類,由于技術(shù)原因會在運行時被繼承。變通的方案就是給每個通過CGLIB代理的類和成員方法都加上open
關(guān)鍵字,但很快這種做法就會變得讓人難以忍受,同事這也違背了Kotlin想要使代碼保持簡潔和可控的初衷。
幸運的是,Kotlin如今推出了一個叫做[kotlin-spring](https://kotlinlang.org/docs/reference/compiler-plugins.html#kotlin-spring-compiler-plugin)
的插件,它是kotlin-allopen
插件的一個預(yù)配置版本,會給添加了如下注解的類及它們的成員方法自動加上open
標(biāo)識符:
@Component
@Async
@Transactional
@Cacheable
元注解支持意味著使用@Configuration
、@Controller
、@RestController
、@Service
和@Repository
注解的類型同樣會自動添加open
,因為這些注解都被@Component
注解了。
start.spring.io默認(rèn)開啟這一功能,所以在實踐中您可以像寫Java代碼一樣編寫Kotlin bean,而不用添加額外的open
關(guān)鍵字。
1.8.2.在持久化時使用不可變類
在Kotlin的主構(gòu)造器中使用只讀屬性是非常方便的,并且應(yīng)該考慮作為一項最佳實踐:
class Person(val name: String, val age: Int)
你也可以選擇加上data
關(guān)鍵字來告訴編譯器自動使用使用主構(gòu)造器中的所有成員生產(chǎn)出:
- equals()/hashCode()這對函數(shù)
- 形如“User(name=John, age=42)”的toString()函數(shù)
- 根據(jù)屬性聲明的位置而產(chǎn)生的一批componentN()函數(shù)
- copy()函數(shù)
即使當(dāng)Person
的屬性是只讀的情況下,要改變某個單獨的屬性也是很容易的:
data class Person(val name: String, val age: Int)
val jack = Person(name = "Jack", age = 1)
val olderJack = jack.copy(age = 2)
通常的持久化技術(shù),像JPA,需要一個默認(rèn)構(gòu)造器來避免這種類型的設(shè)計。幸運的是,現(xiàn)在Kotlin提供了插件kotlin-jpa,用于解決這種“默認(rèn)構(gòu)造器地獄”。它可以給使用了JPA注解的類生成無參構(gòu)造器。
如果想在其他持久化框架中使用這種機(jī)制,您可以配置kotlin-noarg插件。
對于使用了Spring Data對象映射(如MongoDB、Redis、Cassandra等)的模塊,在無需
kotlin-noarg
插件的情況下也可以使用Kotlin不可變類。
1.8.3.依賴注入
我們推薦嘗試并喜歡上使用val
只讀(同時也可以加上不可空性)屬性進(jìn)行構(gòu)造器注入的方式。
@Component
class YourBean(
private val mongoTemplate: MongoTemplate,
private val solrClient: SolrClient
)
自Spring 4.3起,只有一個構(gòu)造器的類,構(gòu)造器的參數(shù)會自動注入。這也是為什么上述例子不需要聲明為
@Autowired constructor
。
如果真的需要使用字段注入,那就使用lateinit var
結(jié)構(gòu)。
@Component
class YourBean {
@Autowired
lateinit var mongoTemplate: MongoTemplate
@Autowired
lateinit var solrClient: SolrClient
}
1.8.4.配置屬性注入
在Java中,可以使用@Value("${property}")
之類的注解注入配置屬性。然而在Kotlin中,$
被用作字符串嵌入的保留字。
因此,如果想在Kotlin中使用@Value
注解,$
字符就需要轉(zhuǎn)義為@Value("\${property}")
。
作為替代,可以聲明如下bean自定義屬性替換前綴:
@Bean
fun propertyConfigurer() = PropertySourcesPlaceholderConfigurer().apply {
setPlaceholderPrefix("%{")
}
現(xiàn)有仍在使用${...}
的代碼(如Spring Boot actuator或者@LocalServerPort
),可以使用配置bean來自定義,代碼如下:
@Bean
fun kotlinPropertyConfigurer() = PropertySourcesPlaceholderConfigurer().apply {
setPlaceholderPrefix("%{")
setIgnoreUnresolvablePlaceholders(true)
}
@Bean
fun defaultPropertyConfigurer() = PropertySourcesPlaceholderConfigurer()
如果使用了Spring Boot,那么可以使用
@ConfigurationProperties
來代替@Value
注解。因為基于構(gòu)造器初始化的不可變類目前尚不支持,所以目前它只能用在lateinit
或可空的var
屬性(推薦使用這種形式)上。參考@ConfigurationProperties
針對不可變POJO的綁定和@ConfigurationProperties
針對接口的綁定這些問題獲取詳細(xì)信息。
1.8.5.注解上的數(shù)組屬性
Kotlin注解和Java注解十分相像,但廣泛運用于Spring的數(shù)組屬性的行為卻是不同的。如Kotlin文檔所述,不同于其他屬性,value
屬性名可以省略并指定為vararg
參數(shù)。
要理解這一點,我們來看一下使用最廣泛的Spring注解之一的@RequestMapping
。這個Java注解的定義是這樣的:
public @interface RequestMapping {
@AliasFor("path")
String[] value() default {};
@AliasFor("value")
String[] path() default {};
RequestMethod[] method() default {};
// ...
}
典型的@RequestMapping
的使用場景是映射特定路徑和請求方式的HTTP請求到對應(yīng)的處理方法上。在Java中,給數(shù)組屬性傳入單個值是可以的,并且它會自動轉(zhuǎn)化為一個數(shù)組。
這就是為什么我們可以直接寫@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])
(需要給數(shù)組屬性加上方括號)。
要換掉這個特殊的method
屬性(大多數(shù)情況下只有一個值)的方式,可以使用像@GetMapping
或者@PostMapping
這樣的快捷注解。
提示:如果
@RequestMapping
的method
注解沒有指定,所有的HTTP請求方法都將被匹配,而不單單是GET
方法。
1.8.6.測試
逐類生命周期
Kotlin允許使用反引號給測試方法指定更有意義的名字。并且自JUnit 5起,Kotlin測試類可以使用@TestInstance(TestInstance.Lifecycle.PER_CLASS)
注解使測試類只實例化一次,所以我們可以在非靜態(tài)方法上使用@BeforeAll
和@AfterAll
注解。這些都很適合Kotlin。
借助junit-platform.properties
文件的junit.jupiter.testinstance.lifecycle.default = per_class
配置項,我們現(xiàn)在可以將默認(rèn)行為改為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創(chuàng)建描述式測試。
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
擴(kuò)展(像.expectBody<String>().isEqualTo("foo")
)作為Kotlin在遇到Java API協(xié)作問題的變通。
同時請參考SPR-16057相關(guān)問題。
1.9.起步
1.9.1.start.spring.io
開發(fā)新的使用Kotlin的Spring 5項目的最簡單的方式就是在start.spring.io上創(chuàng)建一個新的Spring Boot 2項目。
如這篇博客所述,創(chuàng)建一個獨立的WebFlux項目也是可以的。
1.9.2.選擇web風(fēng)格
Spring框架現(xiàn)在提供兩套不同的web技術(shù)棧:Spring MVC和Spring WebFlux。
如果開發(fā)者想創(chuàng)建處理延遲、長連接、流等場景,或僅僅是想要使用web函數(shù)式KotlinDSL,那么推薦使用Spring WebFlux。
對于其他使用場景,特別是使用了JPA這樣的阻塞技術(shù),那么Spring MVC和它的基于注解的編程模型是一個完美有效且充分支持的選擇。
1.10.資源
- Kotlin語言參考手冊
- Kotlin Slack(有專用的#spring頻道)
- 具有
spring
和kotlin
標(biāo)簽的Stackoverflow - 在瀏覽器中嘗試Kotlin
- Kotlin 博客
- 了不起的Kotlin
1.10.1.教程
1.10.2.博客文章
- 使用Kotlin開發(fā)Spring Boot應(yīng)用
- 基于PostgreSQL、Spring Boot和Kotlin的空間信息系統(tǒng)
- Spring框架 5.0中的Kotlin支持介紹
- 函數(shù)式Spring框架 5 Kotlin API
1.10.3.示例
- spring-boot-kotlin-demo:常規(guī)Spring Boot + Spring Data JPA項目
- mixit:Spring Boot 2 + WebFlux + Reactive Spring Data MongoDB
- spring-kotlinfunctional:獨立WebFlux + 函數(shù)式bean定義DSL
- spring-kotlin-fullstack:使用Kotlin2js替換JavaScrip或TypeScript的WebFlux Kotlin 全棧示例
- spring-petclinic-kotlin:Spring PetClinic 示例項目的Kotlin版
- spring-kotlin-deepdive:一步步從Boot 1.0 + Java 遷移到Boot 2.0 + Kotlin
1.10.4.問題
這里是一系列關(guān)于Spring + Kotlin支持的現(xiàn)有問題。
Spring框架
Spring Boot
@ConfigurationProperties
針對不可變POJO的綁定@ConfigurationProperties
針對接口的綁定- 通過
SpringApplication
暴露函數(shù)式bean注冊API - 在Spring Boot API上添加空安全注解
- 使用Kotlin的bom給Kotlin提供依賴管理
Kotlin
- Spring框架支持的父問題
- Kotlin需要類型推斷而Java不需要
- 更好的泛型空安全支持
- 開放類的智能回退轉(zhuǎn)換
- 如果不是所有參數(shù)都是SAM參數(shù),就無法將參數(shù)作為函數(shù)傳入
- 給泛型類型參數(shù)添加JSR 305元注解
- 給庫添加一個方式避免Kotlin 1.0和1.1依賴混在一起
- 通過腳本變量直接支持JSR 223綁定
- 在Kotlin Eclipse插件中支持 all-open and no-arg 編譯器插件
2.Apache Groovy
Groovy是一門強(qiáng)大、可選參數(shù)類型的動態(tài)語言,并具有靜態(tài)類型和靜態(tài)編譯的能力。它提供了簡潔的語法,并且可以順利地整合進(jìn)任何現(xiàn)有的Java項目。
Spring框架提供了一個專用的ApplicationContext
用以支持基于Groovy的bean定義DSL。更多信息請參考《GroovyBean定義DSL》。
關(guān)于Groovy的更多支持,如基于Groovy編寫的bean、可刷新腳本bean等在下節(jié)《動態(tài)語言支持》中會介紹。
3.動態(tài)預(yù)言支持
3.1.簡介
Spring 2.0引入了在Spring中使用那些用動態(tài)語言(如JRuby)定義的類和對象的功能的全面支持。這使得您可以使用某種可支持的動態(tài)語言編寫任意數(shù)量的類,并讓Spring容器透明地給結(jié)果對象實例化、配置、注入。
目前支持的動態(tài)語言有:
- JRuby 1.5+
- Groovy 1.8+
- BeanShell 2.0
為什么只支持這些語言?
選擇這些語言的原因:1.這些語言在Java企業(yè)社區(qū)有很大的吸引力;2.在添加這些語言的支持時不需要求助于其他語言;3.Spring的開發(fā)者最熟悉這些語言。
有關(guān)這些動態(tài)語言支持可以立即發(fā)揮作用的完整可用的示例將在《場景》這一節(jié)討論。
3.2.第一個實例
本章節(jié)的大部分篇幅都集中在詳細(xì)討論動態(tài)語言的支持上。在深入研究這些動態(tài)語言支持的輸入和輸出之前,我們先來速覽一個使用動態(tài)語言定義bean的實例。定義這個bean的動態(tài)語言是Groovy(這個實例從Spring測試用例中抽出來的,如果你想看一下其他語言的等價實現(xiàn),請參考其源碼)。
下文是Groovy bean將要實現(xiàn)的Messenger
接口,注意它是使用純Java來定義的。其他需要注入Messenger
實例的對象并不知道其接口實現(xiàn)是基于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() {
// 注入進(jìn)來的Messenger對象的使用…
}
}
這里是使用Groovy對Messenger
接口的實現(xiàn):
// 來自“Messenger.groovy”文件
package org.springframework.scripting.groovy;
// 導(dǎo)入將要被實現(xiàn)的Messenger(使用Java編寫)接口
import org.springframework.scripting.Messenger
// 使用Groovy實現(xiàn)接口
class GroovyMessenger implements Messenger {
String message
}
最后給出的是會使Groovy所定義的Messenger
實現(xiàn)注入到DefaultBookingService
類起作用的bean配置代碼。
要使用自定義動態(tài)語言標(biāo)簽來定義bean,開發(fā)者需要在Spring XML配置文件的頂部注明對應(yīng)的XML Schema。此外,還得使用Spring
ApplicationContext
作為控制反轉(zhuǎn)容器的實現(xiàn)。雖然使用簡單的BeanFactory
實現(xiàn)也是支持的,但開發(fā)者需要管理Spring內(nèi)部的管道。
更多基于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實現(xiàn)的一個正常的bean -->
<bean id="bookingService" class="x.y.DefaultBookingService">
<property name="messenger" ref="messenger" />
</bean>
</beans>
因為被注入的Messenger
實例的確是實例化了,所以bookingService
bean(DefaultBookingService
的實現(xiàn))現(xiàn)在可以像平常一樣使用這個私有成員變量。在這里,只有普通的Java和普通的Groovy,沒有任何特別的地方。
希望上面的XML代碼段是不言自明的,如果不是的話也不要過分擔(dān)心。請繼續(xù)閱讀有關(guān)上述配置的原因和內(nèi)容的詳細(xì)信息。
3.3.定義動態(tài)語言實現(xiàn)的bean
本節(jié)會討論如何使用任意被支持的動態(tài)語言來定義Spring bean。
請記住本章并不試圖闡述被支持動態(tài)語言的語法和方言。比如說,如果你想在應(yīng)用上使用Groovy編寫某些類,那么前提就是你已經(jīng)學(xué)會使用Groovy了。如果需要更多關(guān)于動態(tài)語言本身的信息,請參考本章末尾的《更多資源》部分。
3.3.1.通用概念
使用動態(tài)語言bean需要如下步驟:
- 編寫動態(tài)語言源碼的測試(當(dāng)然了)
- 編寫動態(tài)語言源碼本身
- 在XML配置文件中使用適當(dāng)?shù)?code><lang:language/>定義這些動態(tài)語言bean(當(dāng)然也可以使用Spring API編程式地定義這些bean,不過你得自行查閱源碼尋找方法,因為本章不涉及這種進(jìn)階配置的介紹)。記住這是一個不斷迭代的步驟。每個動態(tài)語言源碼文件至少包含一個bean的定義(不過一個動態(tài)語言源碼文件可以被多個bean的定義引用)。
前兩個步驟(測試和編寫動態(tài)語言源碼)不在本章的范圍之內(nèi),請自行參閱相關(guān)語言說明或參考手冊進(jìn)行開發(fā)。不過你仍然可以先閱讀本章剩余部分,因為Spring的動態(tài)語言支持確實對動態(tài)語言的源碼文件做了一些(小的)假設(shè)。
<lang:language/> 標(biāo)簽
最后一步是定義每個你想要配置的動態(tài)語言bean的配置(和通常JavaBean的定義沒有什么不同)。然而,和以往通過指定想要由容器來實例化和配置的bean的完整類名的方式不同,我們得使用<lang:language/>
標(biāo)簽來定義這些bean。
每種被支持的語言都有對應(yīng)的<lang:language/>
標(biāo)簽:
-
<lang:groovy/>
(Groovy) -
<lang:bsh/>
(BeanShell) -
<lang:std/>
(JSR-223)
配置時對應(yīng)的屬性和子標(biāo)簽取決于定義這個bean所使用的語言(針對具體語言特性的全部內(nèi)幕將會在本節(jié)稍后提供)。
可刷新bean
Spring的動態(tài)語言支持中最吸引人的附加價值就是“可刷新bean”功能。
可刷新bean是帶有少量配置的動態(tài)語言bean。動態(tài)語言bean可以監(jiān)視其所屬源代碼資源的改動,并在它們發(fā)生改變的時候重新加載自己(例如在開發(fā)者在文件系統(tǒng)上編輯并保存了源文件)。
這使得開發(fā)者可以部署任意數(shù)量的動態(tài)語言源碼作為應(yīng)用的一部分,配置Spring容器創(chuàng)建基于這些動態(tài)語言源碼的bean(使用本章提到的機(jī)制),并在之后當(dāng)需求發(fā)生了變動或一些附加的要素添加進(jìn)來的時候,簡單地編輯動態(tài)語言源碼并讓所有的改動反映這些被修改的源碼文件對應(yīng)的bean上即可。不需要停止正在運行的應(yīng)用(如果是web應(yīng)用,也不需要重部署)。如此修改的動態(tài)語言bean將從更改的動態(tài)語言源文件中獲取新的狀態(tài)和邏輯。
請記住該功能默認(rèn)是關(guān)閉的。
讓我們通過一個示例來看一下開始使用可刷新bean是多么的簡單吧。想要開啟可刷新bean的功能,只需要給對應(yīng)bean的`<lang:language/>標(biāo)簽附加一個屬性即可。假設(shè)繼續(xù)使用之前的例子,這里給出使可刷新bean起作用的修改后的Spring XML 配置文件:
<beans>
<!-- 由于存在“refresh-check-delay”屬性,這個bean現(xiàn)在是可刷新的 -->
<lang:groovy id="messenger"
refresh-check-delay="5000" <!-- 如果有改動,會在五秒內(nèi)觸發(fā)刷新 -->
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
”屬性是檢測到對應(yīng)源碼修改后觸發(fā)刷新的毫秒數(shù)。給它傳入一個復(fù)制就可以關(guān)閉刷新行為。請記住,默認(rèn)情況下,刷新行為是關(guān)閉的。如果不需要刷新行為,簡單地不要定義該屬性就好。
現(xiàn)在運行這個應(yīng)用,我們就可以演練一下可刷新bean的功能了。請理解下面代碼中“跳轉(zhuǎn)到等待輸入階段以暫停執(zhí)行”的場景。調(diào)用System.in.read()
僅僅是為了讓項目暫停,從而使我們可以離開去修改對應(yīng)源碼文件從而在程序恢復(fù)執(zhí)行時觸發(fā)對應(yīng)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());
// 暫停執(zhí)行從而可以去對源碼文件進(jìn)行修改…
System.in.read();
System.out.println(messenger.getMessage());
}
}
接下來,假設(shè)在本示例中,Message
的getMessage()
方法需要變?yōu)檩敵鰡我柊南ⅰO挛氖俏覀冊诔绦驎和?zhí)行時對Messenger.groovy
源文件所做的改動。
package org.springframework.scripting
class GroovyMessenger implements Messenger {
private String message = "Bingo"
public String getMessage() {
// 修改方法實現(xiàn),將消息包裹在單引號里面
return "'" + this.message + "'"
}
public void setMessage(String message) {
this.message = message
}
}
程序在執(zhí)行的時候,在輸入暫停之前會輸出“I Can Do The Frug”。在修改并保存了源碼文件之后,再回復(fù)程序執(zhí)行,getMessage()
方法的調(diào)用結(jié)果就變成了“'I Can Do The Frug'”(注意多出了單引號)。
理解在'refresh-check-delay'
的時間之內(nèi),腳本更改將不會觸發(fā)刷新是很重要的。和這一點同樣重要的是理解在動態(tài)語言bean的方法被調(diào)用之前,腳本的改動實際上不會被“拾取”。 只有動態(tài)語言bean上的方法被調(diào)用的時候,系統(tǒng)才會檢查對應(yīng)的腳本源碼是否更改過了。任何和刷新腳本相關(guān)的異常(如遇到編譯異常,或?qū)?yīng)文件不存在)都會產(chǎn)生傳播到調(diào)用代碼時的致命異常。
上文提到的可刷新bean行為不適用于使用<lang:inline-script/>
標(biāo)簽(參考《內(nèi)聯(lián)動態(tài)語言源文件》)定義的腳本文件。它僅僅適用于那些源文件的更改可以清楚地被檢查到的bean。例如,使用代碼檢查存放于文件系統(tǒng)的動態(tài)語言源文件的最后編輯時間。
內(nèi)聯(lián)動態(tài)語言源文件
Spring同樣支持將動態(tài)語言文件直接嵌入到Spring bean的定義中去。特別地,<lang:inline-script />
標(biāo)簽可以直接在Spring的配置文件中編寫動態(tài)語言源碼。為了更清楚地理解,還是舉個例子吧:
<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>
如果我們先不考慮將動態(tài)語言源碼放到Spring的配置文件中是不是一種好的實踐的問題,那么<lang:inline-script />
標(biāo)簽在一些場景下就可以展現(xiàn)身手了。比如,我們可能會希望快速添加一個Spring Validator
的實現(xiàn)給Spring MVC 的Controller
。這不過是使用內(nèi)聯(lián)源碼的一種情景。(參考《腳本式驗證器》上的示例。)
理解動態(tài)語言bean的上下文構(gòu)造器注入
關(guān)于Spring的動態(tài)語言支持,有一點非常重要。也就是說,(當(dāng)前)不能夠給動態(tài)語言bean的構(gòu)造器傳遞參數(shù)(因此構(gòu)造器注入在動態(tài)語言bean上是不可用的)。為了完全搞懂這種針對構(gòu)造器和屬性的特殊處理,以下代碼和配置將不起作用。
// 來自文件 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">
<!-- 下面這個構(gòu)造器參數(shù)并不能注入到GroovyMessenger中 -->
<!-- 事實上,根據(jù)schema的定義,是不允許這樣寫的 -->
<constructor-arg value="不起作用" />
<!-- 只有屬性值可以注入到動態(tài)語言對象中 -->
<lang:property name="anotherMessage" value="直接傳給動態(tài)語言對象" />
</lang>
在實踐中這條限制并不如它起初看上去那么重要,因為setter注入模式被壓倒性數(shù)量的開發(fā)者所喜愛(讓我們改天再來討論這是不是一件好事)。
3.3.2.Groovy bean
待翻。
3.3.3.BeanShell bean
BeanShell庫依賴
Spring的BeanShell腳本支持需要在項目的classpath中引入如下庫:
- bsh-2.0b4.jar
來自BeanShell的主頁:
BeanShell是一個使用Java編寫的小型、免費、可嵌入的具有動態(tài)語言功能的Java源碼解釋器。BeanShell動態(tài)地執(zhí)行標(biāo)準(zhǔn)Java語法,并給它擴(kuò)展了腳本語言共通的方便特征,如弱類型、指令和方法閉包,就像Perl和JavaScript里的那樣。
和Groovy不同,BeanShell bean需要(少許)額外配置。Spring的BeanShell動態(tài)語言支持實現(xiàn)很有趣的一點在于:Spring創(chuàng)建了一個JDK動態(tài)代理來實現(xiàn)所有指定了'script-interfaces'
屬性的<lang:bsh>
標(biāo)簽所對應(yīng)的接口(這也是為什么你必須至少給這個屬性提供一個接口,并且在使用BeanShell bean時(相應(yīng)地)編寫接口)。這意味著BeanShell對象上的所有方法的調(diào)用都會經(jīng)過JDK動態(tài)代理調(diào)用機(jī)制。
我們來看一個使用BeanShell bean來實現(xiàn)之前章節(jié)定義的Messenger
接口(為了方便閱讀在下文給出接口代碼)的一個完全可用的例子。
package org.springframework.scripting;
public interface Messenger {
String getMessage();
}
這里是BeanShell對Messenger
接口的“實現(xiàn)”(不是嚴(yán)格意義的術(shù)語)。
String message;
String getMessage() {
return message;
}
void setMessage(String aMessage) {
message = aMessage;
}
接下來是在Spring XML中定義上述“類”的“實例”(同樣,不是嚴(yán)格意義的術(shù)語)。
<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.場景
當(dāng)然了,使用腳本語言定義Spring bean的可能場合多且有效。本節(jié)討論兩個可能的使用場景。
3.4.1.腳本式Spring MVC 控制器
在使用動態(tài)語言bean時,有一組類可能會從中受益,那就是Spring MVC 控制器。在純Spring MVC應(yīng)用中,web應(yīng)用的導(dǎo)航流很大程度上是封裝在Spring MVC控制器中的。由于需要修復(fù)問題或更改業(yè)務(wù)需求,所以導(dǎo)航流和其他web應(yīng)用展示層邏輯常常需要修改。通過修改一個或多個動態(tài)語言源文件就能看到它們在一個正在運行的項目中立即生效,可能更容易完成此類修改需求。
記住,在如Spring這種輕量級架構(gòu)模型項目中,開發(fā)者的通常的目標(biāo)是使用真的非常輕量的展示層,將應(yīng)用中所有豐富的業(yè)務(wù)邏輯放置到領(lǐng)域?qū)雍蜆I(yè)務(wù)層的類中。使用動態(tài)語言bean開發(fā)Spring MVC 控制器使開發(fā)者只需要編輯并保存文本文件就可以切換視圖層邏輯;在動態(tài)語言源文件中的任何修改將(取決于配置)自動映射到對應(yīng)的bean中去。
為了能夠自動“拾取”到動態(tài)語言bean上的任何更改,開發(fā)者需要啟動“可刷新bean”功能。參考《可刷新bean》獲得關(guān)于該功能的詳細(xì)信息。
下文是使用Groovy動態(tài)語言來實現(xiàn)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.腳本式校驗器
另一個使用動態(tài)語言bean可以提高Spring項目開發(fā)的靈活性的領(lǐng)域是校驗。也許使用弱類型的動態(tài)語言(也許還支持內(nèi)聯(lián)正則表達(dá)式)來提供復(fù)雜驗證邏輯相較于傳統(tǒng)的Java會更加方便。
再說一遍,使用動態(tài)語言bean是你可以僅僅通過編輯和保存文本文件就可以更改校驗邏輯;任何更改都將(取決于配置)自動映射到已經(jīng)啟動的應(yīng)用上,而無需重啟項目。
請記住,為了能夠自動“拾取”到動態(tài)語言bean上的任何更改,開發(fā)者需要啟動“可刷新bean”功能。參考《可刷新bean》獲得關(guān)于該功能的詳細(xì)信息。
下文是使用Groovy動態(tài)語言實現(xiàn)一個Spring org.springframework.validation.Validator
校驗器的例子。(參考《使用Spring Validator接口進(jìn)行校驗》獲取關(guān)于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.雜七雜八
本節(jié)包含于動態(tài)語言支持相關(guān)的零碎的事情。
3.5.1.AOP - 切入腳本式bean
使用Spring AOP 切入腳本式bean是可行的。Spring AOP框架事實上并不知道將要切入的某個bean是不是腳本式bean。所以在你的開發(fā)中,所有可能會用到或打算用的AOP的使用場景和功能都能工作在腳本式bean上。但也有一件(小)事情在切入腳本式bean時是要注意的,那就是不能使用基于類的代理,必須得使用(基于接口的代理)[https://docs.spring.io/spring-framework/docs/current/spring-framework-reference/core.html#aop-proxying]。
當(dāng)然不僅切入腳本式bean時沒有限制的,使用某種被支持的動態(tài)語言來編寫切面處理其本身從而切入其他Spring bean也是可以的。盡管這真得算得上是動態(tài)語言支持的高階用法了。
3.5.2.Scope
如果不是很明顯,腳本式bean當(dāng)然可以像任何其他bean一樣設(shè)置scope。各種<lang:language />
標(biāo)簽上的scope
屬性使你可以控制對應(yīng)的腳本式bean的scope,就像傳統(tǒng)的bean一樣。(默認(rèn)的scope是(單例的)[https://docs.spring.io/spring-framework/docs/current/spring-framework-reference/core.html#beans-factory-scopes-singleton],和“傳統(tǒng)的”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>
參考《(控制反轉(zhuǎn)容器)[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]》獲取更多關(guān)于Spring框架中scope支持的信息。
3.5.3.lang XML schema
Spring XML 配置文件中的lang
標(biāo)簽是用來處理使用如JRuby或Groovy這種動態(tài)語言編寫的對象作為Spring容器的bean的。
這些標(biāo)簽(和動態(tài)語言支持)在《動態(tài)語言支持》這一章進(jìn)行了全面講解。請閱讀這一章來學(xué)習(xí)動態(tài)語言支持和這些lang
標(biāo)簽本身的知識。
為了完整性,要想使用lang
schema,需要在XML 配置文件的頂部加入如下序文;下文的代碼段中引用了正確的schema,從而使您可以使用lang
命名空間下的標(biāo)簽。
<?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.更多資源
通過如下鏈接來獲取關(guān)于本章討論的對應(yīng)腳本語言的更多資源。