第11章 使用Kotlin集成SpringBoot開發Web服務端
《Kotlin極簡教程》正式上架:
點擊這里 > 去京東商城購買閱讀
點擊這里 > 去天貓商城購買閱讀
非常感謝您親愛的讀者,大家請多支持!!!有任何問題,歡迎隨時與我交流~
我們在前面第2章 “ 2.3 Web RESTFul HelloWorld ” 一節中,已經介紹了使用 Kotlin 結合 SpringBoot 開發一個RESTFul版本的 Hello World。當然,Kotlin與Spring家族的關系不止如此。在 Spring 5.0 M4 中引入了一個專門針對Kotlin的支持。
本章我們就一起來學習怎樣使用Kotlin集成SpringBoot、SpringMVC等框架來開發Web服務端應用,同時簡單介紹Spring 5.0對Kotlin的支持特性。
11.1 Spring Boot簡介
SpringBoot是伴隨著Spring4.0誕生的。從字面理解,Boot是引導的意思,SpringBoot幫助開發者快速搭建Spring框架、快速啟動一個Web容器等,使得基于Spring的開發過程更加簡易。 大部分Spring Boot Application只要一些極簡的配置,即可“一鍵運行”。
SpringBoot的特性如下:
- 創建獨立的Spring applications
- 能夠使用內嵌的Tomcat, Jetty or Undertow,不需要部署war
- 提供定制化的starter poms來簡化maven配置(gradle相同)
- 追求極致的自動配置Spring
- 提供一些生產環境的特性,比如特征指標,健康檢查和外部配置。
- 零代碼生成和零XML配置
Spring由于其繁瑣的配置,一度被人認為“配置地獄”,各種XML文件的配置,讓人眼花繚亂,而且如果出錯了也很難找出原因。而Spring Boot更多的是采用Java Config的方式對Spring進行配置。
11.2 系統架構技術棧
本節我們介紹使用 Kotlin 集成 Spring Boot 開發一個完整的博客站點的服務端Web 應用, 它支持 Markdown 寫文章, 文章列表分頁、搜索查詢等功能。
其系統架構技術棧如下表所示:
編程語言 | Java,Kotlin |
---|---|
數據庫 | MySQL , mysql-jdbc-driver, Spring data JPA, |
J2EE框架 | Spring Boot, Spring MVC |
視圖模板引擎 | Freemarker |
前端組件庫 | jquery,bootstrap, flat UI , Mditor , DataTables |
工程構建工具 | Gradle |
11.3 環境準備
11.3.1 創建工程
首先,我們使用SPRING INITIALIZR來創建一個模板工程。
第一步:訪問 http://start.spring.io/, 選擇生成一個Gradle項目,使用Kotlin語言,使用的Spring Boot版本是2.0.0 M2。
第二步: 配置項目基本信息。
Group: com.easy.kotlin
Artifact:chapter11_kotlin_springboot
以及項目名稱、項目描述、包名稱等其他的選項。選擇jar包方式打包,使用JDK1.8 。
第三步:選擇項目依賴。我們這里分別選擇了:Web、DevTools、JPA、MySQL、Actuator、Freemarker。
以上三步如下圖所示:
點擊生成項目,下載zip包,解壓后導入IDEA中,我們可以看到一個如下目錄結構的工程:
.
├── 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應用的入口啟動類。
11.3.2 Gradle配置文件說明
整個工程的Gradle構建配置文件build.gradle的內容如下:
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')
}
其中主要的配置項如下表說明:
配置項 | 功能說明 |
---|---|
spring-boot-gradle-plugin | SpringBoot集成Gradle的插件 |
kotlin-gradle-plugin | Kotlin集成Gradle的插件 |
kotlin-allopen | Kotlin全開放插件。Kotlin 里類默認都是final的,如果聲明的類需要被繼承則需要使用open 關鍵字來描述類,這個插件就是把Kotlin中的所有類都open打開,可被繼承 |
spring-boot-starter-actuator | SpringBoot的健康檢查監控組件啟動器 |
spring-boot-starter-data-jpa | JPA啟動器 |
spring-boot-starter-freemarker | 模板引擎freemarker啟動器 |
kotlin-stdlib-jre8 | Kotlin基于JRE8的標準庫 |
kotlin-reflect | Kotlin反射庫 |
spring-boot-devtools | SpringBoot開發者工具,例如:熱部署等 |
mysql-connector-java | Java的MySQL連接器庫 |
spring-boot-starter-test | 測試啟動器 |
11.4 數據庫層配置
上面的模板工程,我們來直接運行main函數,會發現啟動失敗,控制臺會輸出如下報錯信息:
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).
因為我們還沒有配置數據源。我們先在MySQL中新建一個schema:
CREATE SCHEMA `blog` DEFAULT CHARACTER SET utf8 ;
11.4.1 配置數據源
接著,我們在配置文件application.properties中配置MySQL數據源:
# datasource
# datasource: unicode編碼的支持,設定為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 中,為了支持中文的正確顯示(防止出現中文亂碼),我們需要設置一下編碼。
數據庫ORM(對象關系映射)層,我們使用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
再次運行啟動類,控制臺輸出啟動日志:
...
_ __ _ _ _
| |/ / | | | (_) _
| ' / ___ | |_| |_ _ __ _| |_
| < / _ \| __| | | '_ \ |_ _|
| . \ (_) | |_| | | | | | |_|
|_|\_\___/ \__|_|_|_| |_|
_____ _ ____ _
/ ____| (_) | _ \ | |
| (___ _ __ _ __ _ _ __ __ _| |_) | ___ ___ | |_
\___ \| '_ \| '__| | '_ \ / _` | _ < / _ \ / _ \| __|
____) | |_) | | | | | | | (_| | |_) | (_) | (_) | |_
|_____/| .__/|_| |_|_| |_|\__, |____/ \___/ \___/ \__|
| | __/ |
|_| |___/
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)
關于上面的日志,我們通過下面的表格作簡要說明:
日志內容 | 簡要說明 |
---|---|
LocalContainerEntityManagerFactoryBean : Initialized JPA EntityManagerFactory | 初始化JAP實體管理器工廠 |
EndpointHandlerMapping : Mapped "{[/application/beans ...等 | SpringBoot健康監控Endpoint 等REST接口 |
FreeMarkerAutoConfiguration | Freemarker模板引擎自動配置,默認視圖文件目錄是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] | 數據源Bean通過annotation注解注冊MBean到JMX實現監控其運行狀態 |
TomcatWebServer : Tomcat started on port(s): 8000 (http) | SpringBoot默認內嵌了Tomcat,端口我們可以在application.properties中配置 |
Started Chapter11KotlinSpringbootApplicationKt in 16.316 seconds (JVM running for 17.68) | SpringBoot應用啟動成功 |
11.5 Endpoint監控接口
我們來嘗試訪問: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.
提示沒有權限訪問。我們去控制臺日志可以看到下面這行輸出:
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.
意思是說,要訪問這些Endpoints需要權限,可以通過Spring Security來實現權限控制,或者把權限限制去掉:把management.security.enabled
設置為false
。
我們在application.properties里面添加配置:
management.security.enabled=false
重啟應用,再次訪問,我們可以看到如下輸出:
[
{
"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"
]
}
]
}
可以看出,我們一行代碼還沒寫,只是加了幾行配置,SpringBoot已經自動配置初始化了這么多的Bean。我們再訪問 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
}
}
從上面我們可以看到一些應用的健康狀態信息,例如:應用狀態、磁盤空間、數據庫狀態等信息。
11.6 數據庫實體類
我們在上面已經完成了MySQL數據源的配置,下面我們來寫一個實體類。新建package com.easy.kotlin.chapter11_kotlin_springboot.entity
,然后新建Article
實體類:
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)"
}
}
類似的實體類,我們在Java中需要生成一堆getter/setter方法;如果我們用Scala寫還需要加個 注解@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 = _
//用例狀態: -1未執行 0失敗 1成功
@BeanProperty
var state: Integer = _
...
}
我們這個是一個博客文章的簡單實體類。再次重啟運行應用,我們去MySQL的Schema: blog 里面去看,發現數據庫自動生成了 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 數據訪問層代碼
在Spring Data JPA中,我們只需要實現接口CrudRepository<T, ID>
即可獲得一個擁有基本CRUD操作的接口實現了:
interface ArticleRepository : CrudRepository<Article, Long>
JPA會自動實現ArticleRepository接口中的方法,不需要我們寫基本的CRUD操作代碼。它的常用的基本CRUD操作方法的簡單說明如下表:
方法 | 功能說明 |
---|---|
S save(S entity) | 保存給定的實體對象,我們可以使用這個保存之后返回的實例進行進一步操作(保存操作可能會更改實體實例) |
findById(ID id) | 根據主鍵id查詢 |
existsById(ID id) | 判斷是否存在該主鍵id的記錄 |
findAll() | 返回所有記錄 |
findAllById(Iterable<ID> ids) | 根據主鍵id集合批量查詢 |
count() | 返回總記錄條數 |
deleteById(ID id) | 根據主鍵id刪除 |
deleteAll() | 全部刪除 |
當然,如果我們需要自己去實現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語句。另外,JPA的EntityManager API 還提供了創建 Query 實例以執行原生 SQL 語句的createNativeQuery方法。
默認是非原生的JP QL查詢模式。如果我們想指定原生SQL查詢,只需要設置
nativeQuery=true
即可。
11.7.2 模糊查詢like寫法
另外,我們原生SQL模糊查詢like語法,我們在寫sql的時候是這樣寫的
like '%?%'
但是在JP QL中, 這樣寫
like %?1%
11.7.3 參數占位符
其中,查詢語句中的 ?1
是函數參數的占位符,1代表的是參數的位置。
11.7.4 JP QL中的SpEL
另外我們使用JPA的標準查詢(Criteria Query):
SELECT a FROM #{#entityName} a where a.content like %:content%
其中的#{#entityName}
是SpEL(Spring表達式語言),用來代替本來實體的名稱,而Spring data jpa會自動根據Article實體上對應的默認的 @Entity class Article
,或者指定@Entity(name = "Article") class Article
自動將實體名稱填入 JP QL語句中。
通過把實體類名稱抽象出來成為參數,幫助我們解決了項目中很多dao接口的方法除了實體類名稱不同,其他操作都相同的問題。
11.7.5 注解參數
我們使用@Param("content")
來指定參數名綁定,然后在JP QL語句中這樣引用:
:content
JP QL 語句中通過": 變量"的格式來指定參數,同時在方法的參數前面使用 @Param 將方法參數與 JP QL 中的命名參數對應。
11.8 控制器層
我們新建子目錄controller,然后在下面新建控制器類:
@Controller
class ArticleController {
}
我們首先,裝配數據訪問層的接口Bean:
@Autowired val articleRepository: ArticleRepository? = null
這個接口Bean的實例化由Spring data jpa完成。如果我們去 http://127.0.0.1:8000/application/beans 中查看這個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"
]
}
我們先來實現一個簡單的查詢所有記錄的REST接口。我們在ArticleRepository中重寫了findAll方法:
override fun findAll(): MutableList<Article>
然后,我們在控制器代碼中直接調用這個接口方法:
@GetMapping("listAllArticle")
@ResponseBody
fun listAllArticle(): MutableList<Article>? {
return articleRepository?.findAll()
}
其中,注解@ResponseBody表示把方法返回值直接綁定到響應體(response body)。
11.9 啟動初始化CommandLineRunner
為了方便測試用,我們在SpringBoot應用啟動的時候初始化幾條數據到數據庫里。Spring Boot 為我們提供了一個方法,通過實現接口 CommandLineRunner 來實現。這是一個函數式接口:
@FunctionalInterface
public interface CommandLineRunner {
void run(String... args) throws Exception;
}
我們只需要創建一個實現接口 CommandLineRunner 的類。很簡單,只需要一個類就可以,無需其他配置。 這里我們使用Kotlin的Lambda表達式來寫:
@Bean
fun init(repository: ArticleRepository) = CommandLineRunner {
val article: Article = Article()
article.author = "Kotlin"
article.title = "極簡Kotlin教程 ${Date()}"
article.content = "Easy Kotlin ${Date()}"
repository.save(article)
}
11.10 應用啟動類
我們在main函數中調用SpringApplication類的靜態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 = "極簡Kotlin教程 ${Date()}"
article.content = "Easy Kotlin ${Date()}"
repository.save(article)
}
}
fun main(args: Array<String>) {
SpringApplication.run(Chapter11KotlinSpringbootApplication::class.java, *args)
}
這里我們主要關注的是@SpringBootApplication注解,它包括三個注解,簡單說明如下表:
注解 | 功能說明 |
---|---|
@SpringBootConfiguration(它包括@Configuration) | 表示將該類作用springboot配置文件類。 |
@EnableAutoConfiguration | 表示SpringBoot程序啟動時,啟動Spring Boot默認的自動配置。 |
@ComponentScan | 表示程序啟動時自動掃描當前包及子包下所有類。 |
11.10.1 啟動運行
如果是在IDEA中運行,可以直接點擊main函數運行,如下圖所示:
如果想在命令行運行,直接在項目根目錄下運行命令:
$ gradle bootRun
我們可以看到控制臺的日志輸出:
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
我們在瀏覽器中直接訪問: http://127.0.0.1:8000/listAllArticle , 可以看到類似如下輸出:
[
{
"id": 1,
"version": 0,
"title": "極簡Kotlin教程",
"content": "Easy Kotlin ",
"author": "Kotlin",
"gmtCreated": 1500306475000,
"gmtModified": 1500306475000,
"deletedDate": 1500306475000
},
{
"id": 2,
"version": 0,
"title": "極簡Kotlin教程",
"content": "Easy Kotlin ",
"author": "Kotlin",
"gmtCreated": 1500306764000,
"gmtModified": 1500306764000,
"deletedDate": 1500306764000
}
]
至此,我們已經完成了一個簡單的REST接口從數據庫到后端的開發。
下面我們繼續來寫一個前端的文章列表頁面。
11.11 Model數據綁定
我們寫一個返回ModelAndView對象控制器類,其中數據模型Model中放入文章列表數據,代碼如下:
@GetMapping("listAllArticleView")
fun listAllArticleView(model: Model): ModelAndView {
model.addAttribute("articles", articleRepository?.findAll())
return ModelAndView("list")
}
其中,ModelAndView("list")
中的"list"表示視圖文件的所在目錄的相對路徑。SpringBoot的默認的視圖文件放在src/main/resources/templates目錄。
11.12 模板引擎視圖頁面
我們使用Freemarker模板引擎。我們在templates目錄下新建一個list.ftl文件,內容如下:
<html>
<head>
<title>Blog!!!</title>
</head>
<body>
<table>
<thead>
<th>序號</th>
<th>標題</th>
<th>作者</th>
<th>發表時間</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的循環指令,${}是 Freemarker引用變量的方式。
提示:關于Freemarker的詳細語法可參考 http://freemarker.org/ 。
11.13 運行測試
重啟應用,瀏覽器訪問 : http://127.0.0.1:8000/listAllArticleView ,我們可以看到頁面輸出:
到這里,我們已經完成了一個從數據庫到前端頁面的完整的一個極簡的Web應用。
當然,這樣的UI樣式未免太簡陋了一些。下面我們加入前端UI組件美化一下。
11.14 引入前端組件
我們使用基于Bootstrap的前端UI庫Flat UI。首先去Flat UI的首頁:http://www.bootcss.com/p/flat-ui/ 下載zip包,加壓后,放到我們的工程里,放置的目錄是:src/main/resources/static 。如下圖所示:
我們在list.ftl頭部引入靜態資源文件:
<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>
其中,我們的這個SpringBoot應用中默認的靜態資源的跟路徑是src/main/resources/static,然后我們的HTML代碼中引用的路徑是在此根目錄下的相對路徑。
提示:更多的關于Spring Boot靜態資源處理內容可以參考文章:
http://www.lxweimin.com/p/d127c4f78bb8
然后,我們再把我們的文章列表布局優化一下:
<div class="container">
<h1>我的博客</h1>
<table class="table table-responsive table-bordered">
<thead>
<th>序號</th>
<th>標題</th>
<th>作者</th>
<th>發表時間</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工程,在此訪問文章列表頁,我們將看到一個比剛才漂亮多了的頁面:
考慮到頭部的靜態資源文件基本都是公共的代碼,我們單獨抽取到一個head.ftl文件中, 然后在list.ftl中直接這樣引用:
<#include "head.ftl">
11.15 實現寫文章模塊
我們在列表頁上面添加一個“寫文章”的入口:
<a href="addArticleView" target="_blank" class="btn btn-primary pull-right add-article">寫文章</a>
其中,btn btn-primary pull-right 這三個css樣式類是Flat UI組件的。add-article是我們自定義的樣式類:
.add-article {
margin: 20px;
}
下面我們來寫新建文章的頁面。我們寫文章的跳轉頁面路徑是 <a href="addArticleView">
, 我們先來新建一個寫文章頁面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="文章標題">
</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">保存并發表</button>
</div>
</div>
</form>
</div>
</body>
</html>
然后,再添加控制器請求轉發。這里我們使用集成WebMvcConfigurerAdapter類,重寫實現addViewControllers方法的方式來添加一個不帶數據傳輸的,單純的請求轉發的跳轉View的RequestMapping Controller:
@Configuration
class WebMvcConfig : WebMvcConfigurerAdapter() {
// 注冊簡單請求轉發跳轉View的RequestMapping Controller
override fun addViewControllers(registry: ViewControllerRegistry?) {
//寫文章的RequestMapping
registry?.addViewController("addArticleView")?.setViewName("addArticleView")
}
}
這樣前端瀏覽器來的請求addArticle會直接映射轉發到視圖addArticle.ftl文件渲染解析。
重啟應用,進入到我們的寫文章的頁面,如下圖:
11.15.1 加上導航欄
為了方便頁面之間的跳轉,我們給我們的博客站點加上導航欄,我們新建一個navbar.ftl文件,內容如下:
<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">切換導航</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="#">關于</a></li>
<li class="dropdown">
<a href="http://www.lxweimin.com/nb/12976878" class="dropdown-toggle" data-toggle="dropdown">
Kotlin <b class="caret"></b>
</a>
<ul class="dropdown-menu">
<li><a href="http://www.lxweimin.com/nb/12976878" target="_blank">Kotlin極簡教程</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://www.lxweimin.com/nb/12066555" target="_blank">SpringBoot極簡教程</a></li>
</ul>
</li>
</ul>
</div>
</div>
</nav>
11.15.2 抽取公共模板文件
考慮到head.ftl、navbar.ftl都是公共的文件,我們把他們單獨放到一個common目錄下。
然后,我們分別在addArticleView.ftl、listAllArticleView.ftl引用如下:
<!DOCTYPE html>
<html>
<#include "common/head.ftl">
<body>
<#include "common/navbar.ftl">
加入了導航欄之后,我們的頁面現在更加美觀了:
11.15.3 寫文章的控制器層接口
控制器層保存接口:
@PostMapping("saveArticle")
@ResponseBody
fun saveArticle(article: Article): Article? {
article.gmtCreated = Date()
article.gmtModified = Date()
article.version = 0
return articleRepository?.save(article)
}
另外,為了支持較多內容的文章,我們把文章內容字段設置成LONGTEXT:
ALTER TABLE `blog`.`article`
CHANGE COLUMN `content` `content` LONGTEXT NULL DEFAULT NULL ;
11.15.4 前端 ajax 請求
我們在blog.js中加上ajax POST請求后端接口的邏輯:
$(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 文章詳情頁
保存成功后,我們默認跳轉該文章詳情頁。
后端控制器代碼:
@GetMapping("detailArticleView")
fun detailArticleView(id: Long, model: Model): ModelAndView {
model.addAttribute("article", articleRepository?.findById(id)?.get())
return ModelAndView("detailArticleView")
}
注意,這里的articleRepository?.findById(id) 方法返回的是Optional<Article>, 我們調用其get()方法,返回真正的Article實體對象。
前端視圖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>
我們在文章列表頁中,給每篇文章標題加上跳轉文章詳情的超鏈接:
<td><a target="_blank" href="detailArticleView?id=${article.id}">${article.title}</a></td>
現在我們的文章列表頁面如下:
點擊一篇文章標題,即可進入詳情頁:
11.16 添加Markdown支持
我們寫技術博客文章,最常用的就是使用Markdown了。我們來為我們的博客添加Markdown的支持。我們使用前端js組件Mditor來支持Markdown的編輯。 Mditor是一個簡潔、易于集成、方便擴展、期望舒服的編寫 markdown 的編輯器。
11.16.1 引入靜態資源
<link href="/mditor-master/dist/css/mditor.css" rel="stylesheet">
<script src="/mditor-master/dist/js/mditor.js"></script>
11.16.2 初始化Mditor
我們在寫文章的頁面addArticleView.ftl,初始化Mditor如下:
<script>
$(function () {
//寫文章 mditor
var mditor = Mditor.fromTextarea(document.getElementById('articleContentEditor'));
//是否打開分屏
mditor.split = true; //打開
//是否打開預覽
mditor.preivew = true; //打開
//是否全屏
mditor.fullscreen = false; //關閉
//獲取或設置編輯器的值
mditor.on('ready', function () {
mditor.value = '# ';
});
hljs.initHighlightingOnLoad();
//源碼高亮
$('pre code').each(function (i, block) {
hljs.highlightBlock(block);
});
})
</script>
另外,我們還使用了代碼高亮插件highlight.js 。
這樣,寫文章的頁面對應的textarea區域就變成了支持Markdown在線編輯+預覽的功能了:
11.16.3 文章詳情顯示Markdown渲染
下面我們來使我們的詳情頁也能支持Markdown的渲染顯示。
詳情頁的視圖文件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>
這里我們把文章內容放到一個隱藏的textarea的placeholder屬性中:
<textarea id="articleContentShow" placeholder="<#escape x as x?html>${article.content}</#escape>" style="display:
none"></textarea>
注意,這里我們作了字符的轉義escape,防止有特殊字符導致頁面顯示錯亂。
然后,我們在js中獲取這個內容:
<script>
$(function () {
// 文章詳情 mditor
var parser = new Mditor.Parser();
var articleContent = document.getElementById('articleContentShow').placeholder //直接取原本的字符串。不會被轉譯,默認html頁面中textarea區域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>
其中,我們是直接調用的Mditor.Parser()函數來解析Markdown字符文本的。
這樣我們的詳情頁也支持了Markdown的渲染顯示了:
11.17 文章列表分頁搜索
為了方便檢索我們的博客文章,我們再來給文章列表頁面添加分頁、搜索、排序等功能。我們使用前端組件DataTables來實現。
提示:更多關于DataTables,可參考: http://www.datatables.club/
11.17.1 引入靜態資源文件
<link href="/datatables/media/css/jquery.dataTables.css" rel="stylesheet">
<script src="/datatables/media/js/jquery.dataTables.js"></script>
11.17.2 給表格加上id
我們給表格加個屬性id="articlesDataTable" :
<table id="articlesDataTable" class="table table-responsive table-bordered">
<thead>
<th>序號</th>
<th>標題</th>
<th>作者</th>
<th>發表時間</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 調用DataTable函數
首先,我們配置一下DataTable的選項:
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: {//分頁的樣式內容
previous: "上一頁",
next: "下一頁",
first: "第一頁",
last: "最后"
}
},
zeroRecords: "沒有內容",//table tbody內容為空時,tbody的內容。
//下面三者構成了總體的左下角的內容。
info: "總計 _TOTAL_ 條,共 _PAGES_ 頁,_START_ - _END_ ",//左下角的信息顯示,大寫的詞為關鍵字。
infoEmpty: "0條記錄",//篩選為空時左下角的顯示。
infoFiltered: ""http://篩選之后的左下角篩選提示
}
然后把我們剛才添加了id的表格使用JQuery選擇器獲取對象,然后直接調用:
$('#articlesDataTable').DataTable(dataTableOptions)
再次看我們的文章列表頁:
已經具備了分頁、搜索、排序等功能了。
到這里,我們的這個較為完整的極簡博客站點應用基本就開發完成了。
11.18 Spring 5.0對Kotlin的支持
Kotlin 關鍵性能之一就是能與 Java 庫很好地互用。但要在 Spring 中編寫慣用的 Kotlin 代碼,還需要一段時間的發展。 Spring 對 Java 8 的新支持:函數式 Web 編程、bean 注冊 API , 這同樣可以在 Kotlin 中使用。
Kotlin 擴展是Kotlin 的編程利器。它能對現有的 API 實現非侵入式的擴展,從而向 Spring中加入 Kotlin 的專有的功能特性。
11.18.1 一種注冊 Bean 的新方法
Spring Framework 5.0 引入了一種注冊 Bean 的新方法,作為利用 XML 或者 JavaConfig 的 @Configuration 或者 @Bean 的替代方案。簡言之就是 Lambda 表達式。
例如用 Java 代碼我們會這樣寫:
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 函數式 API
Spring 5.0 中的 RouterFunctionDsl 可以讓我們使用干凈且優雅的 Kotlin 代碼來使用嶄新的 Spring Web 函數式 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 擴展
Reactor 是 Spring 5.0 中提供的響應式框架。而 reactor-kotlin 項目則是對 Reactor 中使用Kotlin 的支持。目前該項目正在早期階段。
11.18.4 基于 Kotlin腳本的 Gradle 構建配置
之前我們的 Gradle 構建配置文件都是用Groovy 來編寫的,這導致我們基于 Gradle 的 Kotlin 工程還要配置 Groovy 的語法的構建配置文件。
在gradle-script-kotlin 項目中,我們可以直接用 Kotlin 腳本來編寫 Gradle 的構建配置文件了。而且 IDE 還為我們提供了在編寫配置文件過程中的自動完成功能和重構功能的支持。
11.18.5 基于模板的 Kotlin 腳本
從 4.3 版本開始,Spring 提供了一個 ScriptTemplateView,用于利用支持 JSR-223 的腳本引擎來渲染模板。 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")}
"""
本章小結
本章我們較為細致完整地介紹了使用Kotlin集成SpringBoot進行服務后端開發,并結合簡單的前端開發,完成了一個極簡的技術博客Web站點。我們可以看到,使用Kotlin結合Spring Boot、Spring MVC、JPA等Java框架的無縫集成,關鍵是大大簡化了我們的代碼。同時,在本章最后我們簡單介紹了Spring 5.0中對Kotlin的支持諸多新特性,這些新特性令人驚喜。
使用Kotlin編寫Spring Boot應用程序越多,我們越覺得這兩種技術有著共同的目標,讓我們廣大程序員可以使用——
- 富有表達性
- 簡潔優雅
- 可讀
的代碼來更高效地編寫應用程序,而Spring Framework 5 Kotlin支持將這些技術以更加自然,簡單和強大的方式來展現給我們。
未來Spring Framework 5.0 和 Kotlin 結合的開發實踐更加值得我們期待。
在下一章中我們將一起學習Kotlin 集成 Gradle 開發的相關內容。
本章項目源碼: https://github.com/EasyKotlin/chapter11_kotlin_springboot
Kotlin 開發者社區
國內第一Kotlin 開發者社區公眾號,主要分享、交流 Kotlin 編程語言、Spring Boot、Android、React.js/Node.js、函數式編程、編程思想等相關主題。