第11章 使用Kotlin集成SpringBoot開發(fā)Web服務(wù)端
《Kotlin極簡(jiǎn)教程》正式上架:
點(diǎn)擊這里 > 去京東商城購(gòu)買閱讀
點(diǎn)擊這里 > 去天貓商城購(gòu)買閱讀
非常感謝您親愛(ài)的讀者,大家請(qǐng)多支持!?。∮腥魏螁?wèn)題,歡迎隨時(shí)與我交流~
我們?cè)谇懊娴?章 “ 2.3 Web RESTFul HelloWorld ” 一節(jié)中,已經(jīng)介紹了使用 Kotlin 結(jié)合 SpringBoot 開發(fā)一個(gè)RESTFul版本的 Hello World。當(dāng)然,Kotlin與Spring家族的關(guān)系不止如此。在 Spring 5.0 M4 中引入了一個(gè)專門針對(duì)Kotlin的支持。
本章我們就一起來(lái)學(xué)習(xí)怎樣使用Kotlin集成SpringBoot、SpringMVC等框架來(lái)開發(fā)Web服務(wù)端應(yīng)用,同時(shí)簡(jiǎn)單介紹Spring 5.0對(duì)Kotlin的支持特性。
11.1 Spring Boot簡(jiǎn)介
SpringBoot是伴隨著Spring4.0誕生的。從字面理解,Boot是引導(dǎo)的意思,SpringBoot幫助開發(fā)者快速搭建Spring框架、快速啟動(dòng)一個(gè)Web容器等,使得基于Spring的開發(fā)過(guò)程更加簡(jiǎn)易。 大部分Spring Boot Application只要一些極簡(jiǎn)的配置,即可“一鍵運(yùn)行”。
SpringBoot的特性如下:
- 創(chuàng)建獨(dú)立的Spring applications
- 能夠使用內(nèi)嵌的Tomcat, Jetty or Undertow,不需要部署war
- 提供定制化的starter poms來(lái)簡(jiǎn)化maven配置(gradle相同)
- 追求極致的自動(dòng)配置Spring
- 提供一些生產(chǎn)環(huán)境的特性,比如特征指標(biāo),健康檢查和外部配置。
- 零代碼生成和零XML配置
Spring由于其繁瑣的配置,一度被人認(rèn)為“配置地獄”,各種XML文件的配置,讓人眼花繚亂,而且如果出錯(cuò)了也很難找出原因。而Spring Boot更多的是采用Java Config的方式對(duì)Spring進(jìn)行配置。
11.2 系統(tǒng)架構(gòu)技術(shù)棧
本節(jié)我們介紹使用 Kotlin 集成 Spring Boot 開發(fā)一個(gè)完整的博客站點(diǎn)的服務(wù)端Web 應(yīng)用, 它支持 Markdown 寫文章, 文章列表分頁(yè)、搜索查詢等功能。
其系統(tǒng)架構(gòu)技術(shù)棧如下表所示:
| 編程語(yǔ)言 | Java,Kotlin |
|---|---|
| 數(shù)據(jù)庫(kù) | MySQL , mysql-jdbc-driver, Spring data JPA, |
| J2EE框架 | Spring Boot, Spring MVC |
| 視圖模板引擎 | Freemarker |
| 前端組件庫(kù) | jquery,bootstrap, flat UI , Mditor , DataTables |
| 工程構(gòu)建工具 | Gradle |
11.3 環(huán)境準(zhǔn)備
11.3.1 創(chuàng)建工程
首先,我們使用SPRING INITIALIZR來(lái)創(chuàng)建一個(gè)模板工程。
第一步:訪問(wèn) http://start.spring.io/, 選擇生成一個(gè)Gradle項(xiàng)目,使用Kotlin語(yǔ)言,使用的Spring Boot版本是2.0.0 M2。
第二步: 配置項(xiàng)目基本信息。
Group: com.easy.kotlin
Artifact:chapter11_kotlin_springboot
以及項(xiàng)目名稱、項(xiàng)目描述、包名稱等其他的選項(xiàng)。選擇jar包方式打包,使用JDK1.8 。
第三步:選擇項(xiàng)目依賴。我們這里分別選擇了:Web、DevTools、JPA、MySQL、Actuator、Freemarker。
以上三步如下圖所示:

點(diǎn)擊生成項(xiàng)目,下載zip包,解壓后導(dǎo)入IDEA中,我們可以看到一個(gè)如下目錄結(jié)構(gòu)的工程:
.
├── build
│ └── kotlin-build
│ └── caches
├── build.gradle
├── gradle
│ └── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── gradlew
├── gradlew.bat
└── src
├── main
│ ├── kotlin
│ │ └── com
│ │ └── easy
│ │ └── kotlin
│ │ └── chapter11_kotlin_springboot
│ │ └── Chapter11KotlinSpringbootApplication.kt
│ └── resources
│ ├── application.properties
│ ├── static
│ └── templates
└── test
└── kotlin
└── com
└── easy
└── kotlin
└── chapter11_kotlin_springboot
└── Chapter11KotlinSpringbootApplicationTests.kt
21 directories, 8 files
其中,Chapter11KotlinSpringbootApplication.kt是SpringBoot應(yīng)用的入口啟動(dòng)類。
11.3.2 Gradle配置文件說(shuō)明
整個(gè)工程的Gradle構(gòu)建配置文件build.gradle的內(nèi)容如下:
buildscript {
ext {
kotlinVersion = '1.1.3-2'
springBootVersion = '2.0.0.M2'
}
repositories {
mavenCentral()
maven { url "https://repo.spring.io/snapshot" }
maven { url "https://repo.spring.io/milestone" }
}
dependencies {
classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}")
classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:${kotlinVersion}")
classpath("org.jetbrains.kotlin:kotlin-allopen:${kotlinVersion}")
}
}
apply plugin: 'kotlin'
apply plugin: 'kotlin-spring'
apply plugin: 'eclipse'
apply plugin: 'org.springframework.boot'
apply plugin: 'io.spring.dependency-management'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = 1.8
compileKotlin {
kotlinOptions.jvmTarget = "1.8"
}
compileTestKotlin {
kotlinOptions.jvmTarget = "1.8"
}
repositories {
mavenCentral()
maven { url "https://repo.spring.io/snapshot" }
maven { url "https://repo.spring.io/milestone" }
}
dependencies {
compile('org.springframework.boot:spring-boot-starter-actuator')
compile('org.springframework.boot:spring-boot-starter-data-jpa')
compile('org.springframework.boot:spring-boot-starter-freemarker')
compile('org.springframework.boot:spring-boot-starter-web')
compile("org.jetbrains.kotlin:kotlin-stdlib-jre8:${kotlinVersion}")
compile("org.jetbrains.kotlin:kotlin-reflect:${kotlinVersion}")
runtime('org.springframework.boot:spring-boot-devtools')
runtime('mysql:mysql-connector-java')
testCompile('org.springframework.boot:spring-boot-starter-test')
}
其中主要的配置項(xiàng)如下表說(shuō)明:
| 配置項(xiàng) | 功能說(shuō)明 |
|---|---|
| spring-boot-gradle-plugin | SpringBoot集成Gradle的插件 |
| kotlin-gradle-plugin | Kotlin集成Gradle的插件 |
| kotlin-allopen | Kotlin全開放插件。Kotlin 里類默認(rèn)都是final的,如果聲明的類需要被繼承則需要使用open 關(guān)鍵字來(lái)描述類,這個(gè)插件就是把Kotlin中的所有類都o(jì)pen打開,可被繼承 |
| spring-boot-starter-actuator | SpringBoot的健康檢查監(jiān)控組件啟動(dòng)器 |
| spring-boot-starter-data-jpa | JPA啟動(dòng)器 |
| spring-boot-starter-freemarker | 模板引擎freemarker啟動(dòng)器 |
| kotlin-stdlib-jre8 | Kotlin基于JRE8的標(biāo)準(zhǔn)庫(kù) |
| kotlin-reflect | Kotlin反射庫(kù) |
| spring-boot-devtools | SpringBoot開發(fā)者工具,例如:熱部署等 |
| mysql-connector-java | Java的MySQL連接器庫(kù) |
| spring-boot-starter-test | 測(cè)試啟動(dòng)器 |
11.4 數(shù)據(jù)庫(kù)層配置
上面的模板工程,我們來(lái)直接運(yùn)行main函數(shù),會(huì)發(fā)現(xiàn)啟動(dòng)失敗,控制臺(tái)會(huì)輸出如下報(bào)錯(cuò)信息:
BeanCreationException: Error creating bean with name 'dataSource' defined in class path resource [org/springframework/boot/autoconfigure/jdbc/DataSourceConfiguration$Hikari.class]
...
***************************
APPLICATION FAILED TO START
***************************
Description:
Cannot determine embedded database driver class for database type NONE
Action:
If you want an embedded database please put a supported one on the classpath. If you have database settings to be loaded from a particular profile you may need to active it (no profiles are currently active).
因?yàn)槲覀冞€沒(méi)有配置數(shù)據(jù)源。我們先在MySQL中新建一個(gè)schema:
CREATE SCHEMA `blog` DEFAULT CHARACTER SET utf8 ;
11.4.1 配置數(shù)據(jù)源
接著,我們?cè)谂渲梦募pplication.properties中配置MySQL數(shù)據(jù)源:
# datasource
# datasource: unicode編碼的支持,設(shè)定為utf-8
spring.datasource.url=jdbc:mysql://localhost:3306/blog?zeroDateTimeBehavior=convertToNull&characterEncoding=utf8&characterSetResults=utf8
spring.datasource.username=root
spring.datasource.password=root
spring.datasource.testWhileIdle=true
spring.datasource.validationQuery=SELECT 1
其中,spring.datasource.url 中,為了支持中文的正確顯示(防止出現(xiàn)中文亂碼),我們需要設(shè)置一下編碼。
數(shù)據(jù)庫(kù)ORM(對(duì)象關(guān)系映射)層,我們使用spring-data-jpa :
spring.jpa.database=MYSQL
spring.jpa.show-sql=true
# Hibernate ddl auto (create, create-drop, update)
spring.jpa.hibernate.ddl-auto=update
# Naming strategy
spring.jpa.hibernate.naming-strategy=org.hibernate.cfg.ImprovedNamingStrategy
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQL5Dialect
再次運(yùn)行啟動(dòng)類,控制臺(tái)輸出啟動(dòng)日志:
...
_ __ _ _ _
| |/ / | | | (_) _
| ' / ___ | |_| |_ _ __ _| |_
| < / _ \| __| | | '_ \ |_ _|
| . \ (_) | |_| | | | | | |_|
|_|\_\___/ \__|_|_|_| |_|
_____ _ ____ _
/ ____| (_) | _ \ | |
| (___ _ __ _ __ _ _ __ __ _| |_) | ___ ___ | |_
\___ \| '_ \| '__| | '_ \ / _` | _ < / _ \ / _ \| __|
____) | |_) | | | | | | | (_| | |_) | (_) | (_) | |_
|_____/| .__/|_| |_|_| |_|\__, |____/ \___/ \___/ \__|
| | __/ |
|_| |___/
2017-07-17 21:10:48.741 INFO 5062 --- [ restartedMain] c.Chapter11KotlinSpringbootApplicationKt : Starting Chapter11KotlinSpringbootApplicationKt on 192.168.1.6 with PID 5062 ...
...
2017-07-17 21:19:40.548 INFO 5329 --- [ restartedMain] j.LocalContainerEntityManagerFactoryBean : Initialized JPA EntityManagerFactory for persistence unit 'default'
2017-07-17 21:11:03.017 INFO 5062 --- [ restartedMain] o.s.b.a.e.mvc.EndpointHandlerMapping : Mapped "{[/application/env || /application/env.json],methods=[GET],produces=[application/vnd.spring-boot.actuator.v2+json || application/json]}" onto public java.lang.Object org.springframework.boot.actuate.endpoint.mvc.EndpointMvcAdapter.invoke()
...
2017-07-17 21:11:03.026 INFO 5062 --- [ restartedMain] o.s.b.a.e.mvc.EndpointHandlerMapping : Mapped "{[/application/beans || /application/beans.json],methods=[GET],produces=[application/vnd.spring-boot.actuator.v2+json || application/json]}" onto public java.lang.Object org.springframework.boot.actuate.endpoint.mvc.EndpointMvcAdapter.invoke()
2017-07-17 21:11:03.028 INFO 5062 --- [ restartedMain] o.s.b.a.e.mvc.EndpointHandlerMapping : Mapped "{[/application/health || /application/health.json],methods=[GET],produces=[application/vnd.spring-boot.actuator.v2+json || application/json]}" onto public java.lang.Object org.springframework.boot.actuate.endpoint.mvc.HealthMvcEndpoint.invoke(javax.servlet.http.HttpServletRequest,java.security.Principal)
...
2017-07-17 21:11:03.060 INFO 5062 --- [ restartedMain] o.s.b.a.e.mvc.EndpointHandlerMapping : Mapped "{[/application/autoconfig || /application/autoconfig.json],methods=[GET],produces=[application/vnd.spring-boot.actuator.v2+json || application/json]}" onto public java.lang.Object org.springframework.boot.actuate.endpoint.mvc.EndpointMvcAdapter.invoke()
...
2017-07-17 21:11:03.478 INFO 5062 --- [ restartedMain] o.s.ui.freemarker.SpringTemplateLoader : SpringTemplateLoader for FreeMarker: using resource loader [org.springframework.boot.web.servlet.context.AnnotationConfigServletWebServerApplicationContext@6aab8872: startup date [Mon Jul 17 21:10:48 CST 2017]; root of context hierarchy] and template loader path [classpath:/templates/]
2017-07-17 21:11:03.479 INFO 5062 --- [ restartedMain] o.s.w.s.v.f.FreeMarkerConfigurer : ClassTemplateLoader for Spring macros added to FreeMarker configuration
2017-07-17 21:11:03.520 WARN 5062 --- [ restartedMain] o.s.b.a.f.FreeMarkerAutoConfiguration : Cannot find template location(s): [classpath:/templates/] (please add some templates, check your FreeMarker configuration, or set spring.freemarker.checkTemplateLocation=false)
2017-07-17 21:11:03.713 INFO 5062 --- [ restartedMain] o.s.b.d.a.OptionalLiveReloadServer : LiveReload server is running on port 35729
2017-07-17 21:11:03.871 INFO 5062 --- [ restartedMain] o.s.j.e.a.AnnotationMBeanExporter : Registering beans for JMX exposure on startup
2017-07-17 21:11:03.874 INFO 5062 --- [ restartedMain] o.s.j.e.a.AnnotationMBeanExporter : Bean with name 'dataSource' has been autodetected for JMX exposure
2017-07-17 21:11:03.886 INFO 5062 --- [ restartedMain] o.s.j.e.a.AnnotationMBeanExporter : Located MBean 'dataSource': registering with JMX server as MBean [com.zaxxer.hikari:name=dataSource,type=HikariDataSource]
2017-07-17 21:11:03.901 INFO 5062 --- [ restartedMain] o.s.c.support.DefaultLifecycleProcessor : Starting beans in phase 0
2017-07-17 21:11:04.232 INFO 5062 --- [ restartedMain] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat started on port(s): 8000 (http)
2017-07-17 21:11:04.240 INFO 5062 --- [ restartedMain] c.Chapter11KotlinSpringbootApplicationKt : Started Chapter11KotlinSpringbootApplicationKt in 16.316 seconds (JVM running for 17.68)
關(guān)于上面的日志,我們通過(guò)下面的表格作簡(jiǎn)要說(shuō)明:
| 日志內(nèi)容 | 簡(jiǎn)要說(shuō)明 |
|---|---|
| LocalContainerEntityManagerFactoryBean : Initialized JPA EntityManagerFactory | 初始化JAP實(shí)體管理器工廠 |
| EndpointHandlerMapping : Mapped "{[/application/beans ...等 | SpringBoot健康監(jiān)控Endpoint 等REST接口 |
| FreeMarkerAutoConfiguration | Freemarker模板引擎自動(dòng)配置,默認(rèn)視圖文件目錄是classpath:/templates/ |
| AnnotationMBeanExporter : Bean with name 'dataSource' has been autodetected for JMX exposure ... Located MBean 'dataSource': registering with JMX server as MBean [com.zaxxer.hikari:name=dataSource,type=HikariDataSource] | 數(shù)據(jù)源Bean通過(guò)annotation注解注冊(cè)MBean到JMX實(shí)現(xiàn)監(jiān)控其運(yùn)行狀態(tài) |
| TomcatWebServer : Tomcat started on port(s): 8000 (http) | SpringBoot默認(rèn)內(nèi)嵌了Tomcat,端口我們可以在application.properties中配置 |
| Started Chapter11KotlinSpringbootApplicationKt in 16.316 seconds (JVM running for 17.68) | SpringBoot應(yīng)用啟動(dòng)成功 |
11.5 Endpoint監(jiān)控接口
我們來(lái)嘗試訪問(wèn):http://127.0.0.1:8000/application/beans ,瀏覽器顯示如下信息:
Whitelabel Error Page
This application has no explicit mapping for /error, so you are seeing this as a fallback.
Mon Jul 17 21:30:35 CST 2017
There was an unexpected error (type=Unauthorized, status=401).
Full authentication is required to access this resource.
提示沒(méi)有權(quán)限訪問(wèn)。我們?nèi)タ刂婆_(tái)日志可以看到下面這行輸出:
s.b.a.e.m.MvcEndpointSecurityInterceptor : Full authentication is required to access actuator endpoints. Consider adding Spring Security or set 'management.security.enabled' to false.
意思是說(shuō),要訪問(wèn)這些Endpoints需要權(quán)限,可以通過(guò)Spring Security來(lái)實(shí)現(xiàn)權(quán)限控制,或者把權(quán)限限制去掉:把management.security.enabled設(shè)置為false。
我們?cè)赼pplication.properties里面添加配置:
management.security.enabled=false
重啟應(yīng)用,再次訪問(wèn),我們可以看到如下輸出:
[
{
"context": "application:8000",
"parent": null,
"beans": [
{
"bean": "chapter11KotlinSpringbootApplication",
"aliases": [
],
"scope": "singleton",
"type": "com.easy.kotlin.chapter11_kotlin_springboot.Chapter11KotlinSpringbootApplication$$EnhancerBySpringCGLIB$$353fd63e",
"resource": "null",
"dependencies": [
]
},
...
{
"bean": "org.springframework.boot.autoconfigure.orm.jpa.HibernateJpaAutoConfiguration",
"aliases": [
],
"scope": "singleton",
"type": "org.springframework.boot.autoconfigure.orm.jpa.HibernateJpaAutoConfiguration$$EnhancerBySpringCGLIB$$24d31c25",
"resource": "null",
"dependencies": [
"dataSource",
"spring.jpa-org.springframework.boot.autoconfigure.orm.jpa.JpaProperties"
]
},
{
"bean": "transactionManager",
"aliases": [
],
"scope": "singleton",
"type": "org.springframework.orm.jpa.JpaTransactionManager",
"resource": "class path resource [org/springframework/boot/autoconfigure/orm/jpa/HibernateJpaAutoConfiguration.class]",
"dependencies": [
]
},
...
{
"bean": "spring.devtools-org.springframework.boot.devtools.autoconfigure.DevToolsProperties",
"aliases": [
],
"scope": "singleton",
"type": "org.springframework.boot.devtools.autoconfigure.DevToolsProperties",
"resource": "null",
"dependencies": [
]
},
{
"bean": "org.springframework.orm.jpa.SharedEntityManagerCreator#0",
"aliases": [
],
"scope": "singleton",
"type": "com.sun.proxy.$Proxy86",
"resource": "null",
"dependencies": [
"entityManagerFactory"
]
}
]
}
可以看出,我們一行代碼還沒(méi)寫,只是加了幾行配置,SpringBoot已經(jīng)自動(dòng)配置初始化了這么多的Bean。我們?cè)僭L問(wèn) http://127.0.0.1:8000/application/health
{
"status": "UP",
"diskSpace": {
"status": "UP",
"total": 120108089344,
"free": 1724157952,
"threshold": 10485760
},
"db": {
"status": "UP",
"database": "MySQL",
"hello": 1
}
}
從上面我們可以看到一些應(yīng)用的健康狀態(tài)信息,例如:應(yīng)用狀態(tài)、磁盤空間、數(shù)據(jù)庫(kù)狀態(tài)等信息。
11.6 數(shù)據(jù)庫(kù)實(shí)體類
我們?cè)谏厦嬉呀?jīng)完成了MySQL數(shù)據(jù)源的配置,下面我們來(lái)寫一個(gè)實(shí)體類。新建package com.easy.kotlin.chapter11_kotlin_springboot.entity ,然后新建Article實(shí)體類:
package com.easy.kotlin.chapter11_kotlin_springboot.entity
import java.util.*
import javax.persistence.*
@Entity
class Article {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
var id: Long = -1
@Version
var version: Long = 0
var title: String = ""
var content: String = ""
var author: String = ""
var gmtCreated: Date = Date()
var gmtModified: Date = Date()
var isDeleted: Int = 0 //1 Yes 0 No
var deletedDate: Date = Date()
override fun toString(): String {
return "Article(id=$id, version=$version, title='$title', content='$content', author='$author', gmtCreated=$gmtCreated, gmtModified=$gmtModified, isDeleted=$isDeleted, deletedDate=$deletedDate)"
}
}
類似的實(shí)體類,我們?cè)贘ava中需要生成一堆getter/setter方法;如果我們用Scala寫還需要加個(gè) 注解@BeanProperty, 例如
package com.springboot.in.action.entity
import java.util.Date
import javax.persistence.{ Entity, GeneratedValue, GenerationType, Id }
import scala.language.implicitConversions
import scala.beans.BeanProperty
@Entity
class HttpApi {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
@BeanProperty
var id: Integer = _
@BeanProperty
var httpSuiteId: Integer = _
//用例名稱
@BeanProperty
var name: String = _
//用例狀態(tài): -1未執(zhí)行 0失敗 1成功
@BeanProperty
var state: Integer = _
...
}
我們這個(gè)是一個(gè)博客文章的簡(jiǎn)單實(shí)體類。再次重啟運(yùn)行應(yīng)用,我們?nèi)ySQL的Schema: blog 里面去看,發(fā)現(xiàn)數(shù)據(jù)庫(kù)自動(dòng)生成了 Table: article , 它的表字段信息如下:
| Field | Type | Null | Key | Default | Extra |
|---|---|---|---|---|---|
| id | bigint(20) | NO | PRI | NULL | auto_increment |
| author | varchar(255) | YES | NULL | ||
| content | varchar(255) | YES | NULL | ||
| deleted_date | datetime | YES | NULL | ||
| gmt_created | datetime | YES | NULL | ||
| gmt_modified | datetime | YES | NULL | ||
| is_deleted | int(11) | NO | NULL | ||
| title | varchar(255) | YES | NULL | ||
| version | bigint(20) | NO | NULL | - |
11.7 數(shù)據(jù)訪問(wèn)層代碼
在Spring Data JPA中,我們只需要實(shí)現(xiàn)接口CrudRepository<T, ID>即可獲得一個(gè)擁有基本CRUD操作的接口實(shí)現(xiàn)了:
interface ArticleRepository : CrudRepository<Article, Long>
JPA會(huì)自動(dòng)實(shí)現(xiàn)ArticleRepository接口中的方法,不需要我們寫基本的CRUD操作代碼。它的常用的基本CRUD操作方法的簡(jiǎn)單說(shuō)明如下表:
| 方法 | 功能說(shuō)明 |
|---|---|
| S save(S entity) | 保存給定的實(shí)體對(duì)象,我們可以使用這個(gè)保存之后返回的實(shí)例進(jìn)行進(jìn)一步操作(保存操作可能會(huì)更改實(shí)體實(shí)例) |
| findById(ID id) | 根據(jù)主鍵id查詢 |
| existsById(ID id) | 判斷是否存在該主鍵id的記錄 |
| findAll() | 返回所有記錄 |
| findAllById(Iterable<ID> ids) | 根據(jù)主鍵id集合批量查詢 |
| count() | 返回總記錄條數(shù) |
| deleteById(ID id) | 根據(jù)主鍵id刪除 |
| deleteAll() | 全部刪除 |
當(dāng)然,如果我們需要自己去實(shí)現(xiàn)SQL查詢邏輯,我們可以直接使用@Query注解。
interface ArticleRepository : CrudRepository<Article, Long> {
override fun findAll(): MutableList<Article>
@Query(value = "SELECT * FROM blog.article where title like %?1%", nativeQuery = true)
fun findByTitle(title: String): MutableList<Article>
@Query("SELECT a FROM #{#entityName} a where a.content like %:content%")
fun findByContent(@Param("content") content: String): MutableList<Article>
@Query(value = "SELECT * FROM blog.article where author = ?1", nativeQuery = true)
fun findByAuthor(author: String): MutableList<Article>
}
11.7.1 原生SQL查詢
其中,@Query注解里面的value的值就是我們要寫的 JP QL語(yǔ)句。另外,JPA的EntityManager API 還提供了創(chuàng)建 Query 實(shí)例以執(zhí)行原生 SQL 語(yǔ)句的createNativeQuery方法。
默認(rèn)是非原生的JP QL查詢模式。如果我們想指定原生SQL查詢,只需要設(shè)置
nativeQuery=true即可。
11.7.2 模糊查詢like寫法
另外,我們?cè)鶶QL模糊查詢like語(yǔ)法,我們?cè)趯憇ql的時(shí)候是這樣寫的
like '%?%'
但是在JP QL中, 這樣寫
like %?1%
11.7.3 參數(shù)占位符
其中,查詢語(yǔ)句中的 ?1 是函數(shù)參數(shù)的占位符,1代表的是參數(shù)的位置。
11.7.4 JP QL中的SpEL
另外我們使用JPA的標(biāo)準(zhǔn)查詢(Criteria Query):
SELECT a FROM #{#entityName} a where a.content like %:content%
其中的#{#entityName} 是SpEL(Spring表達(dá)式語(yǔ)言),用來(lái)代替本來(lái)實(shí)體的名稱,而Spring data jpa會(huì)自動(dòng)根據(jù)Article實(shí)體上對(duì)應(yīng)的默認(rèn)的 @Entity class Article ,或者指定@Entity(name = "Article") class Article 自動(dòng)將實(shí)體名稱填入 JP QL語(yǔ)句中。
通過(guò)把實(shí)體類名稱抽象出來(lái)成為參數(shù),幫助我們解決了項(xiàng)目中很多dao接口的方法除了實(shí)體類名稱不同,其他操作都相同的問(wèn)題。
11.7.5 注解參數(shù)
我們使用@Param("content") 來(lái)指定參數(shù)名綁定,然后在JP QL語(yǔ)句中這樣引用:
:content
JP QL 語(yǔ)句中通過(guò)": 變量"的格式來(lái)指定參數(shù),同時(shí)在方法的參數(shù)前面使用 @Param 將方法參數(shù)與 JP QL 中的命名參數(shù)對(duì)應(yīng)。
11.8 控制器層
我們新建子目錄controller,然后在下面新建控制器類:
@Controller
class ArticleController {
}
我們首先,裝配數(shù)據(jù)訪問(wèn)層的接口Bean:
@Autowired val articleRepository: ArticleRepository? = null
這個(gè)接口Bean的實(shí)例化由Spring data jpa完成。如果我們?nèi)?http://127.0.0.1:8000/application/beans 中查看這個(gè)Bean,我們可以看到信息如下:
{
"bean": "articleRepository",
"aliases": [
],
"scope": "singleton",
"type": "com.easy.kotlin.chapter11_kotlin_springboot.dao.ArticleRepository",
"resource": "null",
"dependencies": [
"(inner bean)#39c36d98",
"(inner bean)#19d60142",
"(inner bean)#1757cb01",
"(inner bean)#6dd045f0",
"jpaMappingContext"
]
}
我們先來(lái)實(shí)現(xiàn)一個(gè)簡(jiǎn)單的查詢所有記錄的REST接口。我們?cè)贏rticleRepository中重寫了findAll方法:
override fun findAll(): MutableList<Article>
然后,我們?cè)诳刂破鞔a中直接調(diào)用這個(gè)接口方法:
@GetMapping("listAllArticle")
@ResponseBody
fun listAllArticle(): MutableList<Article>? {
return articleRepository?.findAll()
}
其中,注解@ResponseBody表示把方法返回值直接綁定到響應(yīng)體(response body)。
11.9 啟動(dòng)初始化CommandLineRunner
為了方便測(cè)試用,我們?cè)赟pringBoot應(yīng)用啟動(dòng)的時(shí)候初始化幾條數(shù)據(jù)到數(shù)據(jù)庫(kù)里。Spring Boot 為我們提供了一個(gè)方法,通過(guò)實(shí)現(xiàn)接口 CommandLineRunner 來(lái)實(shí)現(xiàn)。這是一個(gè)函數(shù)式接口:
@FunctionalInterface
public interface CommandLineRunner {
void run(String... args) throws Exception;
}
我們只需要?jiǎng)?chuàng)建一個(gè)實(shí)現(xiàn)接口 CommandLineRunner 的類。很簡(jiǎn)單,只需要一個(gè)類就可以,無(wú)需其他配置。 這里我們使用Kotlin的Lambda表達(dá)式來(lái)寫:
@Bean
fun init(repository: ArticleRepository) = CommandLineRunner {
val article: Article = Article()
article.author = "Kotlin"
article.title = "極簡(jiǎn)Kotlin教程 ${Date()}"
article.content = "Easy Kotlin ${Date()}"
repository.save(article)
}
11.10 應(yīng)用啟動(dòng)類
我們?cè)趍ain函數(shù)中調(diào)用SpringApplication類的靜態(tài)run方法,我們的SpringBootApplication主類代碼如下:
package com.easy.kotlin.chapter11_kotlin_springboot
import com.easy.kotlin.chapter11_kotlin_springboot.dao.ArticleRepository
import com.easy.kotlin.chapter11_kotlin_springboot.entity.Article
import org.springframework.boot.CommandLineRunner
import org.springframework.boot.SpringApplication
import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.context.annotation.Bean
import java.util.*
@SpringBootApplication
class Chapter11KotlinSpringbootApplication {
@Bean
fun init(repository: ArticleRepository) = CommandLineRunner {
val article: Article = Article()
article.author = "Kotlin"
article.title = "極簡(jiǎn)Kotlin教程 ${Date()}"
article.content = "Easy Kotlin ${Date()}"
repository.save(article)
}
}
fun main(args: Array<String>) {
SpringApplication.run(Chapter11KotlinSpringbootApplication::class.java, *args)
}
這里我們主要關(guān)注的是@SpringBootApplication注解,它包括三個(gè)注解,簡(jiǎn)單說(shuō)明如下表:
| 注解 | 功能說(shuō)明 |
|---|---|
| @SpringBootConfiguration(它包括@Configuration) | 表示將該類作用springboot配置文件類。 |
| @EnableAutoConfiguration | 表示SpringBoot程序啟動(dòng)時(shí),啟動(dòng)Spring Boot默認(rèn)的自動(dòng)配置。 |
| @ComponentScan | 表示程序啟動(dòng)時(shí)自動(dòng)掃描當(dāng)前包及子包下所有類。 |
11.10.1 啟動(dòng)運(yùn)行
如果是在IDEA中運(yùn)行,可以直接點(diǎn)擊main函數(shù)運(yùn)行,如下圖所示:

如果想在命令行運(yùn)行,直接在項(xiàng)目根目錄下運(yùn)行命令:
$ gradle bootRun
我們可以看到控制臺(tái)的日志輸出:
2017-07-18 17:42:53.689 INFO 21239 --- [ restartedMain] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat started on port(s): 8000 (http)
Hibernate: insert into article (author, content, deleted_date, gmt_created, gmt_modified, is_deleted, title, version) values (?, ?, ?, ?, ?, ?, ?, ?)
2017-07-18 17:42:53.974 INFO 21239 --- [ restartedMain] c.Chapter11KotlinSpringbootApplicationKt : Started Chapter11KotlinSpringbootApplicationKt in 16.183 seconds (JVM running for 17.663)
<==========---> 83% EXECUTING [1m 43s]
> :bootRun
我們?cè)跒g覽器中直接訪問(wèn): http://127.0.0.1:8000/listAllArticle , 可以看到類似如下輸出:
[
{
"id": 1,
"version": 0,
"title": "極簡(jiǎn)Kotlin教程",
"content": "Easy Kotlin ",
"author": "Kotlin",
"gmtCreated": 1500306475000,
"gmtModified": 1500306475000,
"deletedDate": 1500306475000
},
{
"id": 2,
"version": 0,
"title": "極簡(jiǎn)Kotlin教程",
"content": "Easy Kotlin ",
"author": "Kotlin",
"gmtCreated": 1500306764000,
"gmtModified": 1500306764000,
"deletedDate": 1500306764000
}
]
至此,我們已經(jīng)完成了一個(gè)簡(jiǎn)單的REST接口從數(shù)據(jù)庫(kù)到后端的開發(fā)。
下面我們繼續(xù)來(lái)寫一個(gè)前端的文章列表頁(yè)面。
11.11 Model數(shù)據(jù)綁定
我們寫一個(gè)返回ModelAndView對(duì)象控制器類,其中數(shù)據(jù)模型Model中放入文章列表數(shù)據(jù),代碼如下:
@GetMapping("listAllArticleView")
fun listAllArticleView(model: Model): ModelAndView {
model.addAttribute("articles", articleRepository?.findAll())
return ModelAndView("list")
}
其中,ModelAndView("list")中的"list"表示視圖文件的所在目錄的相對(duì)路徑。SpringBoot的默認(rèn)的視圖文件放在src/main/resources/templates目錄。
11.12 模板引擎視圖頁(yè)面
我們使用Freemarker模板引擎。我們?cè)趖emplates目錄下新建一個(gè)list.ftl文件,內(nèi)容如下:
<html>
<head>
<title>Blog!!!</title>
</head>
<body>
<table>
<thead>
<th>序號(hào)</th>
<th>標(biāo)題</th>
<th>作者</th>
<th>發(fā)表時(shí)間</th>
<th>操作</th>
</thead>
<tbody>
<#-- 使用FTL指令 -->
<#list articles as article>
<tr>
<td>${article.id}</td>
<td>${article.title}</td>
<td>${article.author}</td>
<td>${article.gmtModified}</td>
<td><a href="#" target="_blank">編輯</a></td>
</tr>
</#list>
</tbody>
</table>
</body>
</html>
其中,<#list articles as article>是Freemarker的循環(huán)指令,${}是 Freemarker引用變量的方式。
提示:關(guān)于Freemarker的詳細(xì)語(yǔ)法可參考 http://freemarker.org/ 。
11.13 運(yùn)行測(cè)試
重啟應(yīng)用,瀏覽器訪問(wèn) : http://127.0.0.1:8000/listAllArticleView ,我們可以看到頁(yè)面輸出:

到這里,我們已經(jīng)完成了一個(gè)從數(shù)據(jù)庫(kù)到前端頁(yè)面的完整的一個(gè)極簡(jiǎn)的Web應(yīng)用。
當(dāng)然,這樣的UI樣式未免太簡(jiǎn)陋了一些。下面我們加入前端UI組件美化一下。
11.14 引入前端組件
我們使用基于Bootstrap的前端UI庫(kù)Flat UI。首先去Flat UI的首頁(yè):http://www.bootcss.com/p/flat-ui/ 下載zip包,加壓后,放到我們的工程里,放置的目錄是:src/main/resources/static 。如下圖所示:

我們?cè)趌ist.ftl頭部引入靜態(tài)資源文件:
<head>
<meta charset="utf-8">
<title>Blog</title>
<meta name="description"
content="Blog, using Flat UI Kit Free is a Twitter Bootstrap Framework design and Theme, this responsive framework includes a PSD and HTML version."/>
<meta name="viewport" content="width=1000, initial-scale=1.0, maximum-scale=1.0">
<!-- Loading Bootstrap -->
<link href="/flatui/dist/css/vendor/bootstrap/css/bootstrap.min.css" rel="stylesheet">
<!-- Loading Flat UI -->
<link href="/flatui/dist/css/flat-ui.css" rel="stylesheet">
<link href="/flatui/docs/assets/css/demo.css" rel="stylesheet">
<link rel="shortcut icon" href="/flatui/img/favicon.ico">
<script src="/flatui/dist/js/vendor/jquery.min.js"></script>
<script src="/flatui/dist/js/flat-ui.js"></script>
<script src="/flatui/dist/js/vendor/html5shiv.js"></script>
<script src="/flatui/dist/js/vendor/respond.min.js"></script>
<link rel="stylesheet" href="/blog/blog.css">
<script src="/blog/blog.js"></script>
</head>
其中,我們的這個(gè)SpringBoot應(yīng)用中默認(rèn)的靜態(tài)資源的跟路徑是src/main/resources/static,然后我們的HTML代碼中引用的路徑是在此根目錄下的相對(duì)路徑。
提示:更多的關(guān)于Spring Boot靜態(tài)資源處理內(nèi)容可以參考文章:
http://m.itdecent.cn/p/d127c4f78bb8
然后,我們?cè)侔盐覀兊奈恼铝斜聿季謨?yōu)化一下:
<div class="container">
<h1>我的博客</h1>
<table class="table table-responsive table-bordered">
<thead>
<th>序號(hào)</th>
<th>標(biāo)題</th>
<th>作者</th>
<th>發(fā)表時(shí)間</th>
<th>操作</th>
</thead>
<tbody>
<#-- 使用FTL指令 -->
<#list articles as article>
<tr>
<td>${article.id}</td>
<td>${article.title}</td>
<td>${article.author}</td>
<td>${article.gmtModified}</td>
<td><a href="#" target="_blank">編輯</a></td>
</tr>
</#list>
</tbody>
</table>
</div>
重新build工程,在此訪問(wèn)文章列表頁(yè),我們將看到一個(gè)比剛才漂亮多了的頁(yè)面:

考慮到頭部的靜態(tài)資源文件基本都是公共的代碼,我們單獨(dú)抽取到一個(gè)head.ftl文件中, 然后在list.ftl中直接這樣引用:
<#include "head.ftl">
11.15 實(shí)現(xiàn)寫文章模塊
我們?cè)诹斜眄?yè)上面添加一個(gè)“寫文章”的入口:
<a href="addArticleView" target="_blank" class="btn btn-primary pull-right add-article">寫文章</a>
其中,btn btn-primary pull-right 這三個(gè)css樣式類是Flat UI組件的。add-article是我們自定義的樣式類:
.add-article {
margin: 20px;
}
下面我們來(lái)寫新建文章的頁(yè)面。我們寫文章的跳轉(zhuǎn)頁(yè)面路徑是 <a href="addArticleView">, 我們先來(lái)新建一個(gè)寫文章頁(yè)面addArticleView.ftl:
<!DOCTYPE html>
<html>
<#include "head.ftl">
<body>
<div class="container">
<h2>寫文章</h2>
<form id="addArticleForm" class="form-horizontal">
<div class="form-group">
<input type="text" name="title" class="form-control" placeholder="文章標(biāo)題">
</div>
<div class="form-group">
<textarea id="articleContentEditor" type="text" name="content" class="form-control" rows="20"
placeholder=""></textarea>
</div>
<div class="form-group save-article">
<div class="col-sm-offset-2 col-sm-10">
<button type="submit" class="btn btn-primary" id="addArticleBtn">保存并發(fā)表</button>
</div>
</div>
</form>
</div>
</body>
</html>
然后,再添加控制器請(qǐng)求轉(zhuǎn)發(fā)。這里我們使用集成WebMvcConfigurerAdapter類,重寫實(shí)現(xiàn)addViewControllers方法的方式來(lái)添加一個(gè)不帶數(shù)據(jù)傳輸?shù)?,單純的?qǐng)求轉(zhuǎn)發(fā)的跳轉(zhuǎn)View的RequestMapping Controller:
@Configuration
class WebMvcConfig : WebMvcConfigurerAdapter() {
// 注冊(cè)簡(jiǎn)單請(qǐng)求轉(zhuǎn)發(fā)跳轉(zhuǎn)View的RequestMapping Controller
override fun addViewControllers(registry: ViewControllerRegistry?) {
//寫文章的RequestMapping
registry?.addViewController("addArticleView")?.setViewName("addArticleView")
}
}
這樣前端瀏覽器來(lái)的請(qǐng)求addArticle會(huì)直接映射轉(zhuǎn)發(fā)到視圖addArticle.ftl文件渲染解析。
重啟應(yīng)用,進(jìn)入到我們的寫文章的頁(yè)面,如下圖:

11.15.1 加上導(dǎo)航欄
為了方便頁(yè)面之間的跳轉(zhuǎn),我們給我們的博客站點(diǎn)加上導(dǎo)航欄,我們新建一個(gè)navbar.ftl文件,內(nèi)容如下:
<nav class="navbar navbar-default" role="navigation">
<div class="container-fluid">
<div class="navbar-header">
<button type="button" class="navbar-toggle" data-toggle="collapse"
data-target="#example-navbar-collapse">
<span class="sr-only">切換導(dǎo)航</span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
<a class="navbar-brand" href="#">我的博客</a>
</div>
<div class="collapse navbar-collapse" id="example-navbar-collapse">
<ul class="nav navbar-nav">
<li class=""><a href="listAllArticleView">文章列表</a></li>
<li class="active"><a href="addArticleView">寫文章</a></li>
<li><a href="#">關(guān)于</a></li>
<li class="dropdown">
<a href="http://m.itdecent.cn/nb/12976878" class="dropdown-toggle" data-toggle="dropdown">
Kotlin <b class="caret"></b>
</a>
<ul class="dropdown-menu">
<li><a href="http://m.itdecent.cn/nb/12976878" target="_blank">Kotlin極簡(jiǎn)教程</a></li>
<li class="divider"></li>
<li><a href="#">Java</a></li>
<li><a href="#">Scala</a></li>
<li><a href="#">Groovy</a></li>
<li class="divider"></li>
<li><a href="http://m.itdecent.cn/nb/12066555" target="_blank">SpringBoot極簡(jiǎn)教程</a></li>
</ul>
</li>
</ul>
</div>
</div>
</nav>
11.15.2 抽取公共模板文件
考慮到head.ftl、navbar.ftl都是公共的文件,我們把他們單獨(dú)放到一個(gè)common目錄下。
然后,我們分別在addArticleView.ftl、listAllArticleView.ftl引用如下:
<!DOCTYPE html>
<html>
<#include "common/head.ftl">
<body>
<#include "common/navbar.ftl">
加入了導(dǎo)航欄之后,我們的頁(yè)面現(xiàn)在更加美觀了:


11.15.3 寫文章的控制器層接口
控制器層保存接口:
@PostMapping("saveArticle")
@ResponseBody
fun saveArticle(article: Article): Article? {
article.gmtCreated = Date()
article.gmtModified = Date()
article.version = 0
return articleRepository?.save(article)
}
另外,為了支持較多內(nèi)容的文章,我們把文章內(nèi)容字段設(shè)置成LONGTEXT:
ALTER TABLE `blog`.`article`
CHANGE COLUMN `content` `content` LONGTEXT NULL DEFAULT NULL ;
11.15.4 前端 ajax 請(qǐng)求
我們?cè)赽log.js中加上ajax POST請(qǐng)求后端接口的邏輯:
$(function () {
$('#addArticleBtn').on('click', function () {
saveArticle()
})
function saveArticle() {
$.ajax({
url: "saveArticle",
data: $('#addArticleForm').serialize(),
type: "POST",
async: false,
success: function (resp) {
if (resp) {
saveArticleSuccess(resp)
} else {
saveArticleFail()
}
},
error: function () {
saveArticleFail()
}
})
}
function saveArticleSuccess(resp) {
alert('保存成功: ' + JSON.stringify(resp))
window.open('detailArticleView?id=' + resp.id)
}
function saveArticleFail() {
alert("保存失敗!")
}
})
11.15.5 文章詳情頁(yè)
保存成功后,我們默認(rèn)跳轉(zhuǎn)該文章詳情頁(yè)。
后端控制器代碼:
@GetMapping("detailArticleView")
fun detailArticleView(id: Long, model: Model): ModelAndView {
model.addAttribute("article", articleRepository?.findById(id)?.get())
return ModelAndView("detailArticleView")
}
注意,這里的articleRepository?.findById(id) 方法返回的是Optional<Article>, 我們調(diào)用其get()方法,返回真正的Article實(shí)體對(duì)象。
前端視圖detailArticleView.ftl代碼:
<!DOCTYPE html>
<html>
<#include "common/head.ftl">
<body>
<#include "common/navbar.ftl">
<div class="container">
<h3>${article.title}</h3>
<h6>${article.author}</h6>
<h6>${article.gmtModified}</h6>
<div>${article.content}</div>
</div>
</body>
</html>
我們?cè)谖恼铝斜眄?yè)中,給每篇文章標(biāo)題加上跳轉(zhuǎn)文章詳情的超鏈接:
<td><a target="_blank" href="detailArticleView?id=${article.id}">${article.title}</a></td>
現(xiàn)在我們的文章列表頁(yè)面如下:

點(diǎn)擊一篇文章標(biāo)題,即可進(jìn)入詳情頁(yè):

11.16 添加Markdown支持
我們寫技術(shù)博客文章,最常用的就是使用Markdown了。我們來(lái)為我們的博客添加Markdown的支持。我們使用前端js組件Mditor來(lái)支持Markdown的編輯。 Mditor是一個(gè)簡(jiǎn)潔、易于集成、方便擴(kuò)展、期望舒服的編寫 markdown 的編輯器。
11.16.1 引入靜態(tài)資源
<link href="/mditor-master/dist/css/mditor.css" rel="stylesheet">
<script src="/mditor-master/dist/js/mditor.js"></script>
11.16.2 初始化Mditor
我們?cè)趯懳恼碌捻?yè)面addArticleView.ftl,初始化Mditor如下:
<script>
$(function () {
//寫文章 mditor
var mditor = Mditor.fromTextarea(document.getElementById('articleContentEditor'));
//是否打開分屏
mditor.split = true; //打開
//是否打開預(yù)覽
mditor.preivew = true; //打開
//是否全屏
mditor.fullscreen = false; //關(guān)閉
//獲取或設(shè)置編輯器的值
mditor.on('ready', function () {
mditor.value = '# ';
});
hljs.initHighlightingOnLoad();
//源碼高亮
$('pre code').each(function (i, block) {
hljs.highlightBlock(block);
});
})
</script>
另外,我們還使用了代碼高亮插件highlight.js 。
這樣,寫文章的頁(yè)面對(duì)應(yīng)的textarea區(qū)域就變成了支持Markdown在線編輯+預(yù)覽的功能了:

11.16.3 文章詳情顯示Markdown渲染
下面我們來(lái)使我們的詳情頁(yè)也能支持Markdown的渲染顯示。
詳情頁(yè)的視圖文件detailArticleView.ftl如下:
<!DOCTYPE html>
<html>
<#include "common/head.ftl">
<body>
<#include "common/navbar.ftl">
<div class="container">
<h3>${article.title}</h3>
<h6>${article.author}</h6>
<h6>${article.gmtModified}</h6>
<textarea id="articleContentShow" placeholder="<#escape x as x?html>${article.content}</#escape>" style="display:
none"></textarea>
<div id="article-content" class="markdown-body"></div>
</div>
</body>
</html>
這里我們把文章內(nèi)容放到一個(gè)隱藏的textarea的placeholder屬性中:
<textarea id="articleContentShow" placeholder="<#escape x as x?html>${article.content}</#escape>" style="display:
none"></textarea>
注意,這里我們作了字符的轉(zhuǎn)義escape,防止有特殊字符導(dǎo)致頁(yè)面顯示錯(cuò)亂。
然后,我們?cè)趈s中獲取這個(gè)內(nèi)容:
<script>
$(function () {
// 文章詳情 mditor
var parser = new Mditor.Parser();
var articleContent = document.getElementById('articleContentShow').placeholder //直接取原本的字符串。不會(huì)被轉(zhuǎn)譯,默認(rèn)html頁(yè)面中textarea區(qū)域text需要escape編碼
articleContent = unescape(articleContent);//unescape解碼
var html = parser.parse(articleContent);
$('#article-content').append(html);
hljs.initHighlightingOnLoad();
//源碼高亮
$('pre code').each(function (i, block) {
hljs.highlightBlock(block);
});
})
</script>
其中,我們是直接調(diào)用的Mditor.Parser()函數(shù)來(lái)解析Markdown字符文本的。
這樣我們的詳情頁(yè)也支持了Markdown的渲染顯示了:

11.17 文章列表分頁(yè)搜索
為了方便檢索我們的博客文章,我們?cè)賮?lái)給文章列表頁(yè)面添加分頁(yè)、搜索、排序等功能。我們使用前端組件DataTables來(lái)實(shí)現(xiàn)。
提示:更多關(guān)于DataTables,可參考: http://www.datatables.club/
11.17.1 引入靜態(tài)資源文件
<link href="/datatables/media/css/jquery.dataTables.css" rel="stylesheet">
<script src="/datatables/media/js/jquery.dataTables.js"></script>
11.17.2 給表格加上id
我們給表格加個(gè)屬性id="articlesDataTable" :
<table id="articlesDataTable" class="table table-responsive table-bordered">
<thead>
<th>序號(hào)</th>
<th>標(biāo)題</th>
<th>作者</th>
<th>發(fā)表時(shí)間</th>
<th>操作</th>
</thead>
<tbody>
<#-- 使用FTL指令 -->
<#list articles as article>
<tr>
<td>${article.id}</td>
<td><a target="_blank" href="detailArticleView?id=${article.id}">${article.title}</a></td>
<td>${article.author}</td>
<td>${article.gmtModified}</td>
<td><a href="#" target="_blank">編輯</a></td>
</tr>
</#list>
</tbody>
</table>
11.17.3 調(diào)用DataTable函數(shù)
首先,我們配置一下DataTable的選項(xiàng):
var aLengthMenu = [7, 10, 20, 50, 100, 200]
var dataTableOptions = {
"bDestroy": true,
dom: 'lfrtip',
"paging": true,
"lengthChange": true,
"searching": true,
"ordering": true,
"info": true,
"autoWidth": true,
"processing": true,
"stateSave": true,
responsive: true,
fixedHeader: false,
order: [[1, "desc"]],
"aLengthMenu": aLengthMenu,
language: {
"search": "<div style='border-radius:10px;margin-left:auto;margin-right:2px;width:760px;'>_INPUT_ <span class='btn btn-primary'><span class='fa fa-search'></span> 搜索</span></div>",
paginate: {//分頁(yè)的樣式內(nèi)容
previous: "上一頁(yè)",
next: "下一頁(yè)",
first: "第一頁(yè)",
last: "最后"
}
},
zeroRecords: "沒(méi)有內(nèi)容",//table tbody內(nèi)容為空時(shí),tbody的內(nèi)容。
//下面三者構(gòu)成了總體的左下角的內(nèi)容。
info: "總計(jì) _TOTAL_ 條,共 _PAGES_ 頁(yè),_START_ - _END_ ",//左下角的信息顯示,大寫的詞為關(guān)鍵字。
infoEmpty: "0條記錄",//篩選為空時(shí)左下角的顯示。
infoFiltered: ""http://篩選之后的左下角篩選提示
}
然后把我們剛才添加了id的表格使用JQuery選擇器獲取對(duì)象,然后直接調(diào)用:
$('#articlesDataTable').DataTable(dataTableOptions)
再次看我們的文章列表頁(yè):

已經(jīng)具備了分頁(yè)、搜索、排序等功能了。
到這里,我們的這個(gè)較為完整的極簡(jiǎn)博客站點(diǎn)應(yīng)用基本就開發(fā)完成了。
11.18 Spring 5.0對(duì)Kotlin的支持
Kotlin 關(guān)鍵性能之一就是能與 Java 庫(kù)很好地互用。但要在 Spring 中編寫慣用的 Kotlin 代碼,還需要一段時(shí)間的發(fā)展。 Spring 對(duì) Java 8 的新支持:函數(shù)式 Web 編程、bean 注冊(cè) API , 這同樣可以在 Kotlin 中使用。
Kotlin 擴(kuò)展是Kotlin 的編程利器。它能對(duì)現(xiàn)有的 API 實(shí)現(xiàn)非侵入式的擴(kuò)展,從而向 Spring中加入 Kotlin 的專有的功能特性。
11.18.1 一種注冊(cè) Bean 的新方法
Spring Framework 5.0 引入了一種注冊(cè) Bean 的新方法,作為利用 XML 或者 JavaConfig 的 @Configuration 或者 @Bean 的替代方案。簡(jiǎn)言之就是 Lambda 表達(dá)式。
例如用 Java 代碼我們會(huì)這樣寫:
GenericApplicationContext context = new GenericApplicationContext();
context.registerBean(Foo.class);
context.registerBean(Bar.class, () -> new
Bar(context.getBean(Foo.class))
);
而使用 Kotlin 我們可以將代碼寫成這樣:
val context = GenericApplicationContext {
registerBean<foo>()
registerBean { Bar(it.getBean<foo>()) }
}
11.18.2 Spring Web 函數(shù)式 API
Spring 5.0 中的 RouterFunctionDsl 可以讓我們使用干凈且優(yōu)雅的 Kotlin 代碼來(lái)使用嶄新的 Spring Web 函數(shù)式 API:
fun route(request: ServerRequest) = RouterFunctionDsl {
accept(TEXT_HTML).apply {
(GET("/user/") or GET("/users/")) { findAllView() }
GET("/user/{login}") { findViewById() }
}
accept(APPLICATION_JSON).apply {
(GET("/api/user/") or GET("/api/users/")) { findAll() }
POST("/api/user/") { create() }
POST("/api/user/{login}") { findOne() }
}
} (request)
11.18.3 Reactor Kotlin 擴(kuò)展
Reactor 是 Spring 5.0 中提供的響應(yīng)式框架。而 reactor-kotlin 項(xiàng)目則是對(duì) Reactor 中使用Kotlin 的支持。目前該項(xiàng)目正在早期階段。
11.18.4 基于 Kotlin腳本的 Gradle 構(gòu)建配置
之前我們的 Gradle 構(gòu)建配置文件都是用Groovy 來(lái)編寫的,這導(dǎo)致我們基于 Gradle 的 Kotlin 工程還要配置 Groovy 的語(yǔ)法的構(gòu)建配置文件。
在gradle-script-kotlin 項(xiàng)目中,我們可以直接用 Kotlin 腳本來(lái)編寫 Gradle 的構(gòu)建配置文件了。而且 IDE 還為我們提供了在編寫配置文件過(guò)程中的自動(dòng)完成功能和重構(gòu)功能的支持。
11.18.5 基于模板的 Kotlin 腳本
從 4.3 版本開始,Spring 提供了一個(gè) ScriptTemplateView,用于利用支持 JSR-223 的腳本引擎來(lái)渲染模板。 Kotlin 1.1-M04 提供了這樣的支持,并支持渲染基于 Kotlin 的模板,類似下面這樣:
import io.spring.demo.User
import io.spring.demo.joinToLine
"""
${include("header", bindings)}
<h1>Title : $title</h1>
<ul>
${(users as List<User>).joinToLine{ "<li>User ${it.firstname} ${it.lastname}</li>" }}
</ul>
${include("footer")}
"""
本章小結(jié)
本章我們較為細(xì)致完整地介紹了使用Kotlin集成SpringBoot進(jìn)行服務(wù)后端開發(fā),并結(jié)合簡(jiǎn)單的前端開發(fā),完成了一個(gè)極簡(jiǎn)的技術(shù)博客Web站點(diǎn)。我們可以看到,使用Kotlin結(jié)合Spring Boot、Spring MVC、JPA等Java框架的無(wú)縫集成,關(guān)鍵是大大簡(jiǎn)化了我們的代碼。同時(shí),在本章最后我們簡(jiǎn)單介紹了Spring 5.0中對(duì)Kotlin的支持諸多新特性,這些新特性令人驚喜。
使用Kotlin編寫Spring Boot應(yīng)用程序越多,我們?cè)接X(jué)得這兩種技術(shù)有著共同的目標(biāo),讓我們廣大程序員可以使用——
- 富有表達(dá)性
- 簡(jiǎn)潔優(yōu)雅
- 可讀
的代碼來(lái)更高效地編寫應(yīng)用程序,而Spring Framework 5 Kotlin支持將這些技術(shù)以更加自然,簡(jiǎn)單和強(qiáng)大的方式來(lái)展現(xiàn)給我們。
未來(lái)Spring Framework 5.0 和 Kotlin 結(jié)合的開發(fā)實(shí)踐更加值得我們期待。
在下一章中我們將一起學(xué)習(xí)Kotlin 集成 Gradle 開發(fā)的相關(guān)內(nèi)容。
本章項(xiàng)目源碼: https://github.com/EasyKotlin/chapter11_kotlin_springboot
Kotlin 開發(fā)者社區(qū)
國(guó)內(nèi)第一Kotlin 開發(fā)者社區(qū)公眾號(hào),主要分享、交流 Kotlin 編程語(yǔ)言、Spring Boot、Android、React.js/Node.js、函數(shù)式編程、編程思想等相關(guān)主題。