javascript
第13章 Kotlin 集成 SpringBoot 服务端开发(1)
第13章 Kotlin 集成 SpringBoot 服務(wù)端開發(fā)
本章介紹Kotlin服務(wù)端開發(fā)的相關(guān)內(nèi)容。首先,我們簡單介紹一下Spring Boot服務(wù)端開發(fā)框架,快速給出一個 Restful Hello World的示例。然后,我們講下 Kotlin 集成 Spring Boot 進(jìn)行服務(wù)端開發(fā)的步驟,最后給出一個完整的 Web 應(yīng)用開發(fā)實(shí)例。
13.1 SpringBoot 快速開始 Restful Hello World
Spring Boot 大大簡化了使用 Spring 框架過程中的各種繁瑣的配置, 另外可以更加方便的整合常用的工具鏈 (比如 Redis, Email, kafka, ElasticSearch, MyBatis, JPA) 等, 而缺點(diǎn)是集成度較高(事物都是兩面性的),使用過程中不太容易了解底層,遇到問題了解決曲線比較陡峭。本節(jié)我們介紹怎樣快速開始SpringBoot服務(wù)端開發(fā)。
13.1.1 Spring Initializr
工欲善其事必先利其器。我們使用 https://start.spring.io/ 可以直接自動生成 SpringBoot項(xiàng)目腳手架。如下圖
start.spring.io點(diǎn)擊“Switch to the full version ” , 可以看到腳手架支持的工具鏈。
如果 https://start.spring.io/ 網(wǎng)絡(luò)連不上,我們也可以自己搭建本地的 Spring Initializr服務(wù)。步驟如下
即可看到啟動日志
...... s.b.c.e.t.TomcatEmbeddedServletContainer : Tomcat started on port(s): 8080 (http) i.s.i.service.InitializrService : Started InitializrService in 15.192 seconds (JVM running for 15.882)此時,我們本機(jī)瀏覽器訪問 http://127.0.0.1:8080/ ,即可看到腳手架initializr頁面。
13.1.2 創(chuàng)建SpringBoot項(xiàng)目
我們使用本地搭建的腳手架initializr, 頁面上表單選項(xiàng)如下
使用spring initializr創(chuàng)建SpringBoot項(xiàng)目首先 ,我們選擇生成的是一個使用Gradle 構(gòu)建的Kotlin項(xiàng)目,SpringBoot的版本號我們選擇2.0.0(SNAPSHOT) 。
在 Spring Boot Starters 和 dependencies 選項(xiàng)中,我們選擇 Web starter, 這個啟動器里面包含了基本夠用的Spring Web開發(fā)需要的東西:Tomcat 和 Spring MVC。
其余的項(xiàng)目元數(shù)據(jù)(Project Metadata)的配置(Bill Of Materials),我們可以從上面的圖中看到。然后,點(diǎn)擊“Generate Project” ,會自動下載一個項(xiàng)目的zip壓縮包。解壓導(dǎo)入IDEA中
導(dǎo)入IDEA因?yàn)槲覀兪褂玫氖荊radle構(gòu)建項(xiàng)目,所以需要配置一下Gradle環(huán)境,這里我們使用的是Local gradle distribution , 選擇對應(yīng)的本地的 gradle 軟件包目錄。
工程文件目錄樹
我們將得到如下一個樣板工程,工程文件目錄樹如下
kotlin-with-springboot$ tree . ├── build │ └── kotlin-build │ └── version.txt ├── build.gradle ├── gradle │ └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── kotlin-with-springboot.iml └── src├── main│ ├── java│ ├── kotlin│ │ └── com│ │ └── easy│ │ └── kotlin│ │ └── kotlinwithspringboot│ │ └── KotlinWithSpringbootApplication.kt│ └── resources│ ├── application.properties│ ├── static│ └── templates└── test├── java├── kotlin│ └── com│ └── easy│ └── kotlin│ └── kotlinwithspringboot│ └── KotlinWithSpringbootApplicationTests.kt└── resources23 directories, 10 files其中,src/main/kotlin 是Kotlin源碼放置目錄。src/main/resources目錄下面放置工程資源文件。application.properties 是工程全局的配置文件,static文件夾下面放置靜態(tài)資源文件,templates目錄下面放置視圖模板文件。
build.gradle 配置文件
我們使用 Gradle 來構(gòu)建項(xiàng)目。其中 build.gradle 配置文件類似 Maven中的pom.xml 配置文件。我們使用 Spring Initializr 自動生成的樣板項(xiàng)目的默認(rèn)配置如下
buildscript {ext {kotlinVersion = '1.1.51'springBootVersion = '2.0.0.BUILD-SNAPSHOT'}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'group = 'com.easy.kotlin' 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-web')compile("org.jetbrains.kotlin:kotlin-stdlib-jre8:${kotlinVersion}")compile("org.jetbrains.kotlin:kotlin-reflect:${kotlinVersion}")testCompile('org.springframework.boot:spring-boot-starter-test') }其中,
spring-boot-gradle-plugin 是SpringBoot 集成 Gradle 的插件;
kotlin-gradle-plugin 是 Kotlin 集成Gradle的插件;
kotlin-allopen 是 Kotlin 集成 Spring 框架,把類全部設(shè)置為 open 的插件。因?yàn)镵otlin 的所有類及其成員默認(rèn)情況下都是 final 的,也就是說你想要繼承一個類,就要不斷得寫各種 open。而使用Java寫的 Spring 框架中大量使用了繼承和覆寫,這個時候使用 kotlin-allopen 插件結(jié)合 kotlin-spring 插件,可以自動把 Spring 相關(guān)的所有注解的類設(shè)置為 open 。
spring-boot-starter-web 就是SpringBoot中提供的使用Spring框架進(jìn)行Web應(yīng)用開發(fā)的啟動器。
kotlin-stdlib-jre8 是Kotlin使用Java 8 的庫,kotlin-reflect 是 Kotlin 的反射庫。
項(xiàng)目的整體依賴如下圖所示
項(xiàng)目的整體依賴我們可以看出,spring-boot-starter-web 中已經(jīng)引入了我們所需要的 json 、tomcat 、validator 、webmvc (其中引入了Spring框架的核心web、context、aop、beans、expressions、core)等框架。
SpringBoot項(xiàng)目的入口類 KotlinWithSpringbootApplication
自動生成的 SpringBoot項(xiàng)目的入口類 KotlinWithSpringbootApplication如下
package com.easy.kotlin.kotlinwithspringbootimport org.springframework.boot.SpringApplication import org.springframework.boot.autoconfigure.SpringBootApplication@SpringBootApplication class KotlinWithSpringbootApplicationfun main(args: Array<String>) {SpringApplication.run(KotlinWithSpringbootApplication::class.java, *args) }其中,@SpringBootApplication注解是3個注解的組合,分別是@SpringBootConfiguration (背后使用的又是 @Configuration ),@EnableAutoConfiguration,@ComponentScan。由于這些注解一般都是一起使用,Spring Boot提供了這個@SpringBootApplication 統(tǒng)一的注解。這個注解的定義源碼如下
@Target({ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) @Documented @Inherited @SpringBootConfiguration @EnableAutoConfiguration @ComponentScan(excludeFilters = {@Filter(type = FilterType.CUSTOM,classes = {TypeExcludeFilter.class} ), @Filter(type = FilterType.CUSTOM,classes = {AutoConfigurationExcludeFilter.class} )} ) public @interface SpringBootApplication {... }main 函數(shù)中的 KotlinWithSpringbootApplication::class.java 是一個使用反射獲取KotlinWithSpringbootApplication類的Java Class引用。這也正是我們在依賴中引入 kotlin-reflect 包的用途所在。
寫 Hello World 控制器
下面我們來實(shí)現(xiàn)一個簡單的Hello World 控制器 。 首先新建 HelloWorldController Kotlin 類,代碼實(shí)現(xiàn)如下
package com.easy.kotlin.kotlinwithspringbootimport org.springframework.stereotype.Controller import org.springframework.web.bind.annotation.RequestMapping import org.springframework.web.bind.annotation.ResponseBody@Controller class HelloWorldController {@RequestMapping("/")@ResponseBodyfun home(): String {return "Hello World!"}}啟動運(yùn)行
系統(tǒng)默認(rèn)端口號是8080,我們在application.properties 中添加一行服務(wù)端口號的配置
server.port=8000然后,直接啟動入口類 KotlinWithSpringbootApplication , 可以看到啟動日志
...o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat started on port(s): 8000 (http) .e.k.k.KotlinWithSpringbootApplicationKt : Started KotlinWithSpringbootApplicationKt in 7.944 seconds (JVM running for 9.049)也可以點(diǎn)擊IDEA的Gradle工具欄里面的 Tasks - application - bootRun 執(zhí)行
Gradle工具欄 Tasks - application - bootRun啟動完畢后,我們直接在瀏覽器中打開 http://127.0.0.1:8000/ , 可以看到輸出了 Hello World!
Hello World!本節(jié)項(xiàng)目源碼:https://github.com/EasySpringBoot/kotlin-with-springboot
13.2 綜合實(shí)戰(zhàn):一個圖片爬蟲的Web應(yīng)用實(shí)例
上面我們已經(jīng)看到了使用Kotlin 集成 SpringBoot開發(fā)的基本步驟。本節(jié)我們給出一個使用MySQL數(shù)據(jù)庫、 Spring Data JPA ORM框架、Freemarker模板引擎的完整Web項(xiàng)目的實(shí)例。
13.2.1 系統(tǒng)技術(shù)棧
本節(jié)介紹使用Kotlin 集成 SpringBoot 開發(fā)一個完整的圖片爬蟲Web應(yīng)用,基本功能如下
- 定時抓取圖片搜索API的根據(jù)關(guān)鍵字搜索返回的圖片json信息,解析入庫
- Web頁面分頁展示圖片列表,支持收藏、刪除等功能
- 列表支持根據(jù)圖片分類進(jìn)行模糊搜索
涉及的主要技術(shù)棧如下
- 編程語言:Kotlin
- 數(shù)據(jù)庫層: MySQL、mysql-jdbc-driver 、JPA
- 企業(yè)級開發(fā)框架:Spring Boot、 Spring MVC
- 視圖層模板引擎: Freemarker
- 前端框架: jQuery 、 Bootstrap 、Bootstrap-table
- 工程構(gòu)建工具:Gradle
13.2.2 準(zhǔn)備工作
使用 Spring Initializr 創(chuàng)建項(xiàng)目
如下圖配置項(xiàng)目基本信息和依賴
使用 Spring Initializr 創(chuàng)建項(xiàng)目自動生成項(xiàng)目源碼工程,導(dǎo)入IDEA中,等待構(gòu)建完畢,我們將得到下面的工程目錄
picture-crawler$ tree . ├── build.gradle ├── gradle │ └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── picture-crawler.iml └── src├── main│ ├── java│ ├── kotlin│ │ └── com│ │ └── easy│ │ └── kotlin│ │ └── picturecrawler│ │ └── PictureCrawlerApplication.kt│ └── resources│ ├── application.properties│ ├── static│ └── templates└── test├── java├── kotlin│ └── com│ └── easy│ └── kotlin│ └── picturecrawler│ └── PictureCrawlerApplicationTests.kt└── resources21 directories, 9 files自動生成的 build.gradle 文件如下
buildscript {ext {kotlinVersion = '1.1.51'springBootVersion = '2.0.0.BUILD-SNAPSHOT'}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'group = 'com.easy.kotlin' 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-freemarker')compile('org.springframework.boot:spring-boot-starter-data-jpa')compile('org.springframework.boot:spring-boot-starter-quartz')compile('org.springframework.boot:spring-boot-starter-web')compile("org.jetbrains.kotlin:kotlin-stdlib-jre8:${kotlinVersion}")compile("org.jetbrains.kotlin:kotlin-reflect:${kotlinVersion}")runtime('mysql:mysql-connector-java')testCompile('org.springframework.boot:spring-boot-starter-test') }我們可以看到在 build.gradle 中新增了spring-boot-starter-freemarker 、 mybatis-spring-boot-starter 、 spring-boot-starter-quartz 、mysql-connector-java 等依賴。在這些starter中已經(jīng)封裝了這個工具鏈所需要的依賴庫。整個項(xiàng)目的依賴如下圖所示
整個項(xiàng)目的依賴目前我們的工程已經(jīng)具備了連接MySQL數(shù)據(jù)庫、解析Freemarker 的 .ftl 模板文件等的能力了。但是,此時如果啟動會報錯
BeanCreationException: Error creating bean with name 'dataSource' defined in class path resource [org/springframework/boot/autoconfigure/jdbc/DataSourceConfiguration$Hikari.class]創(chuàng)建 dataSource Bean失敗。因?yàn)?#xff0c;我們還沒有配置任何數(shù)據(jù)庫連接信息。下面我們來配置數(shù)據(jù)源 dataSource 。
13.2.3 配置數(shù)據(jù)源
Spring Boot 的數(shù)據(jù)源配置在 application.properties 中是以 spring.datasource 為前綴。例如,新建一個 wotu 庫
CREATE SCHEMA `wotu` DEFAULT CHARACTER SET utf8 ;我們配置數(shù)據(jù)庫的連接url 、用戶名 、 密碼信息如下
spring.datasource.url=jdbc:mysql://localhost:3306/wotu?zeroDateTimeBehavior=convertToNull&characterEncoding=utf8&characterSetResults=utf8&useSSL=false spring.datasource.username=root spring.datasource.password=rootspring.datasource.testWhileIdle=true spring.datasource.validationQuery=SELECT 1然后,再次啟動應(yīng)用,我們可以發(fā)現(xiàn)啟動成功。
13.2.4 數(shù)據(jù)庫表結(jié)構(gòu)設(shè)計
下面我們從數(shù)據(jù)庫層開始構(gòu)建我們的應(yīng)用。首先我們先設(shè)計數(shù)據(jù)庫的表結(jié)構(gòu)如下
CREATE TABLE `picture` (`id` bigint(20) NOT NULL AUTO_INCREMENT,`category` varchar(255) DEFAULT NULL,`deleted_date` datetime DEFAULT NULL,`gmt_created` datetime DEFAULT NULL,`gmt_modified` datetime DEFAULT NULL,`is_deleted` int(11) NOT NULL,`url` varchar(500) NOT NULL,`version` int(11) NOT NULL,`is_favorite` int(11) NOT NULL,PRIMARY KEY (`id`,`url`),KEY `url` (`id`,`url`) USING BTREE ) ENGINE=InnoDB DEFAULT CHARSET=utf8;因?yàn)槲覀兪褂玫氖?JPA,只需要寫好實(shí)體類代碼,啟動應(yīng)用即可自動創(chuàng)建表結(jié)構(gòu)到 MySQL 數(shù)據(jù)庫中。實(shí)體類代碼如下
package com.easy.kotlin.picturecrawler.entityimport java.util.* import javax.persistence.*@Entity class Image {@Id@GeneratedValue(strategy = GenerationType.IDENTITY)var id: Long = -1@Versionvar version: Int = 0var category: String = ""var isFavorite: Int = 0var url: String = ""var gmtCreated: Date = Date()var gmtModified: Date = Date()var isDeleted: Int = 0 //1 Yes 0 Novar deletedDate: Date = Date()override fun toString(): String {return "Image(id=$id, version=$version, category='$category', isFavorite=$isFavorite, url='$url', gmtCreated=$gmtCreated, gmtModified=$gmtModified, isDeleted=$isDeleted, deletedDate=$deletedDate)"} }ddl-auto 配置
我們再配置一下 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其中 spring.jpa.hibernate.ddl-auto 的值有:create、create-drop、update、validate、none,如下表分別作簡單說面
| create | 每次加載hibernate會自動創(chuàng)建表,以后啟動會覆蓋之前的表,所以這個值基本不用,嚴(yán)重會導(dǎo)致的數(shù)據(jù)的丟失。 |
| create-drop | 每次加載hibernate時根據(jù)model類生成表,但是sessionFactory一關(guān)閉,表就自動刪除,下一次啟動會重新創(chuàng)建。 |
| update | 加載hibernate時根據(jù)實(shí)體類model創(chuàng)建數(shù)據(jù)庫表,這是表名的依據(jù)是@Entity注解的值或者@Table注解的值,sessionFactory關(guān)閉表不會刪除,且下一次啟動會根據(jù)實(shí)體model更新結(jié)構(gòu)或者有新的實(shí)體類會創(chuàng)建新的表。 |
| validate | 啟動時驗(yàn)證表的結(jié)構(gòu),不會創(chuàng)建表 |
| none | 啟動時不做任何操作 |
所以,在開發(fā)項(xiàng)目的過程中,我們通常會選用 update 選項(xiàng)。
再次啟動應(yīng)用,啟動完畢后我們可以看到數(shù)據(jù)庫中已經(jīng)自動創(chuàng)建了 image 表
image 表結(jié)構(gòu)標(biāo)注索引
為了更高的性能,我們建立類別 category 字段和 url 索引。其中 url 是唯一索引
ALTER TABLE `sotu`.`image`ADD INDEX `idx_category` (`category` ASC),ADD UNIQUE INDEX `uk_url` (`url` ASC);而實(shí)際上,我們不需要去手工寫上面的 SQL 然后再去數(shù)據(jù)庫中執(zhí)行。我們只需要寫下面的實(shí)體類
package com.easy.kotlin.picturecrawler.entityimport java.util.* import javax.persistence.*@Entity @Table(indexes = arrayOf(Index(name = "idx_url", unique = true, columnList = "url"),Index(name = "idx_category", unique = false, columnList = "category"))) class Image {@Id@GeneratedValue(strategy = GenerationType.IDENTITY)var id: Long = -1@Versionvar version: Int = 0@Column(length = 255, unique = true, nullable = false)var category: String = ""var isFavorite: Int = 0@Column(length = 255, unique = true, nullable = false)var url: String = ""var gmtCreated: Date = Date()var gmtModified: Date = Date()var isDeleted: Int = 0 //1 Yes 0 Novar deletedDate: Date = Date()override fun toString(): String {return "Image(id=$id, version=$version, category='$category', isFavorite=$isFavorite, url='$url', gmtCreated=$gmtCreated, gmtModified=$gmtModified, isDeleted=$isDeleted, deletedDate=$deletedDate)"} }我們在@Table 注解里指定為 url、category 建立索引, 以及設(shè)定 url 唯一性約束 unique = true
@Table(indexes = arrayOf(Index(name = "idx_url", unique = true, columnList = "url"),Index(name = "idx_category", unique = false, columnList = "category")))啟動應(yīng)用的時候,JPA 會去解析我們的注解生成對應(yīng)的 SQL,并且自動去執(zhí)行相應(yīng)的 SQL。例如字段url 的唯一索引約束,我們可以在啟動日志中看到如下的輸出
Hibernate: alter table image drop index idx_url Hibernate: alter table image add constraint idx_url unique (url)其中,Index 是@Index 注解,當(dāng)做參數(shù)使用的時候不需要加@ 。
我們再舉個例子。實(shí)體類代碼如下
package com.easy.kotlin.picturecrawler.entityimport java.util.* import javax.persistence.*@Entity @Table(indexes = arrayOf(Index(name = "idx_key_word", columnList = "keyWord", unique = true))) class SearchKeyWord {@Id@GeneratedValue(strategy = GenerationType.IDENTITY)var id: Long = -1@Column(length = 50, unique = true, nullable = false)var keyWord: String = ""var gmtCreated: Date = Date()var gmtModified: Date = Date()var isDeleted: Int = 0 //1 Yes 0 Novar deletedDate: Date = Date() }重啟應(yīng)用,我們可以看到Hibernate 日志
Hibernate: create table search_key_word (id bigint not null auto_increment, deleted_date datetime, gmt_created datetime, gmt_modified datetime, is_deleted integer not null, key_word varchar(50) not null, primary key (id)) engine=MyISAM Hibernate: alter table search_key_word drop index UK_lvmjkr0dkesio7a33ejre5c26 Hibernate: alter table search_key_word add constraint UK_lvmjkr0dkesio7a33ejre5c26 unique (key_word)自動生成的表結(jié)構(gòu)如下
自動生成的 search_key_word 表結(jié)構(gòu)其中,@Column(length = 50, unique = true, nullable = false) 這一句指定了keyWord 字段的長度是50,有唯一約束,不可空。對應(yīng)生成的數(shù)據(jù)庫表字段 key_word 信息:Type 是 varchar(50) , Null 是 NO, Key 是唯一鍵 UNI 。
主鍵自動生成策略
我們使用@Id 注解來標(biāo)注主鍵字段
@Id @GeneratedValue(strategy = GenerationType.IDENTITY) var id: Long = -1其中的 @GeneratedValue(strategy = GenerationType.IDENTITY) 注解,我們重點(diǎn)介紹一下。這里的GenerationType是主鍵 ID 的生成規(guī)則。JPA提供的四種標(biāo)準(zhǔn)用法為 TABLE、SEQUENCE、IDENTITY、AUTO
| TABLE | 使用一個特定的數(shù)據(jù)庫表格來保存主鍵。 |
| SEQUENCE | 根據(jù)底層數(shù)據(jù)庫的序列來生成主鍵,條件是數(shù)據(jù)庫支持序列。 |
| IDENTITY | 主鍵由數(shù)據(jù)庫自動生成(主要是自動增長型) |
| AUTO | 主鍵由程序控制。 |
我們設(shè)計源碼目錄如下
├── src │ ├── main │ │ ├── java │ │ ├── kotlin │ │ │ └── com │ │ │ └── easy │ │ │ └── kotlin │ │ │ └── picturecrawler │ │ │ ├── PictureCrawlerApplication.kt │ │ │ ├── controller │ │ │ ├── dao │ │ │ ├── entity │ │ │ ├── job │ │ │ └── service ...其中,controller 放置 Controller 控制器代碼;
entity 放置對應(yīng)到數(shù)據(jù)庫表的實(shí)體類代碼;
dao 層放置數(shù)據(jù)訪問層邏輯代碼;
service 層放置業(yè)務(wù)邏輯實(shí)現(xiàn)代碼;
job 層放置定時任務(wù)代碼。
13.2.5 JSON 數(shù)據(jù)解析
我們的圖片搜索 API 返回的數(shù)據(jù)結(jié)構(gòu)是 JSON 格式的,內(nèi)容示例如下
{"queryEnc": "%E7%BE%8E%E5%A5%B3","queryExt": "美女","listNum": 3900,"displayNum": 415337,"gsm": "5a","bdFmtDispNum": "約415,000","bdSearchTime": "","isNeedAsyncRequest": 1,"bdIsClustered": "1","data": [{"adType": "0","hasAspData": "0","thumbURL": "http://img5.imgtn.bdimg.com/it/u=2817128514,340025963&fm=27&gp=0.jpg","middleURL": "http://img5.imgtn.bdimg.com/it/u=2817128514,340025963&fm=27&gp=0.jpg","largeTnImageUrl": "","hasLarge": 0,..."currentIndex": "","width": 800,"height": 958,"type": "jpg","is_gif": 0,..."bdImgnewsDate": "1970-01-01 08:00","fromPageTitle": "","fromPageTitleEnc": "性感美女",... }我們只需要取出其中的thumbURL 和 fromPageTitleEnc 兩個字段的值。我們使用 fastjson 來解析這個 json 字符串
try {val obj = JSON.parse(jsonstr) as Map<*, *>val dataArray = obj.get("data") as JSONArraydataArray.forEach {val category = (it as Map<*, *>).get("fromPageTitleEnc") as Stringval url = it.get("thumbURL") as Stringif (passFilter(url)) {val imageResult = ImageCategoryAndUrl(category = category, url = url)imageResultList.add(imageResult)}}} catch (ex: Exception) {}fun passFilter(imgUrl: String): Boolean {return imgUrl.endsWith(".jpg")&& !imgUrl.contains("baidu.com/")&& !imgUrl.contains("126.net")&& !imgUrl.contains("pconline.com")&& !imgUrl.contains("nipic.com")&& !imgUrl.contains("zol.com") }其中的ImageCategoryAndUrl 對象是我們定義的數(shù)據(jù)轉(zhuǎn)換對象
data class ImageCategoryAndUrl(val category: String, val url: String)搜索圖片的 Rest API Builder 類如下
object ImageSearchApiBuilder {fun build(word: String, page: Int): String {return "http://image.baidu.com/search/acjson?tn=resultjson_com&ipn=rj&fp=result&word=${word}&pn=${30 * page}&rn=30"} }我們來寫個單元測試
package com.easy.kotlin.picturecrawlerimport com.easy.kotlin.picturecrawler.api.ImageSearchApiBuilder import com.easy.kotlin.picturecrawler.service.JsonResultProcessor import org.junit.Test import org.junit.runner.RunWith import org.junit.runners.JUnit4@RunWith(JUnit4::class) class JsonResultProcessorTest {@Testfun testJsonResultProcessor() {val list = JsonResultProcessor.getImageCategoryAndUrlList(ImageSearchApiBuilder.build("美女", 1))println(list)} }輸出
[ImageCategoryAndUrl(category=性感美女寫真集, url=http://img1.imgtn.bdimg.com/it/u=3772875022,724775083&fm=27&gp=0.jpg), ImageCategoryAndUrl(category=美女寫真 性感美女_美女寫真 性感美女_美女寫真 性感美女, url=http://img0.imgtn.bdimg.com/it/u=3312193685,1215837845&fm=11&gp=0.jpg), ImageCategoryAndUrl(category=...13.2.6 數(shù)據(jù)入庫邏輯實(shí)現(xiàn)
現(xiàn)在我們已經(jīng)有了數(shù)據(jù)的表結(jié)構(gòu),實(shí)體類代碼; 同時也已經(jīng)有個業(yè)務(wù)源數(shù)據(jù)了。現(xiàn)在我們要做的是把爬到的圖片信息存儲到數(shù)據(jù)庫中。同時,重復(fù)的 url 信息我們不去重復(fù)存儲。
新建一個實(shí)現(xiàn) PagingAndSortingRepository<Image, Long> 的 ImageRepository 接口
interface ImageRepository : PagingAndSortingRepository<Image, Long>只要上面的一行代碼,我們就可以直接使用ImageRepository 的 CRUD 方法了。因?yàn)?JPA 框架會幫我們自動生成這些方法。這個PagingAndSortingRepository 是帶分頁功能的。它繼承了CrudRepository 接口
@NoRepositoryBean public interface PagingAndSortingRepository<T, ID> extends CrudRepository<T, ID> {Iterable<T> findAll(Sort sort);Page<T> findAll(Pageable pageable); }而在接口 CrudRepository 中定義了我們能夠直接使用的 CRUD 方法
@NoRepositoryBean public interface CrudRepository<T, ID> extends Repository<T, ID> {<S extends T> S save(S entity);<S extends T> Iterable<S> saveAll(Iterable<S> entities);Optional<T> findById(ID id);boolean existsById(ID id);Iterable<T> findAll();Iterable<T> findAllById(Iterable<ID> ids);long count();void deleteById(ID id);void delete(T entity);void deleteAll(Iterable<? extends T> entities);void deleteAll(); }我們?nèi)霂炀椭苯邮褂胹ave(S entity) 方法。但是為了保證重復(fù)的 url 不保存,需要寫個函數(shù)來判斷當(dāng)前 url 是否在數(shù)據(jù)庫中存在。我們直接使用 select count() 語句來判斷即可, 當(dāng)且僅當(dāng) select count() 出來的值等于 0 (表明數(shù)據(jù)庫中不存在此 url ),才進(jìn)行入庫動作。在ImageRepository 接口中直接聲明函數(shù)即可,代碼如下
@Query("select count(*) from #{#entityName} a where a.url = :url")fun countByUrl(@Param("url") url: String): Int入庫邏輯代碼如下
if (imageRepository.countByUrl(url) == 0) {val Image = Image()Image.category = categoryImage.url = urlimageRepository.save(Image) }13.2.7 定時調(diào)度任務(wù)執(zhí)行
為了簡單起見,我們直接使用 Spring 自帶的scheduling 包下面的@Schedules 注解來實(shí)現(xiàn)任務(wù)的定時執(zhí)行。需要注意的是,要在 SpringBoot 的啟動類上面添加注解
@SpringBootApplication @EnableScheduling class PictureCrawlerApplication我們的定時任務(wù)代碼如下
package com.easy.kotlin.picturecrawler.jobimport com.easy.kotlin.picturecrawler.service.CrawImageService import org.springframework.beans.factory.annotation.Autowired import org.springframework.scheduling.annotation.Scheduled import org.springframework.stereotype.Component import java.util.*@Component class ImageCrawlerJob {@Autowired lateinit var CrawImagesService: CrawImageService@Scheduled(cron = "0 */5 * * * ?")fun job() {println("開始執(zhí)行定時任務(wù): ${Date()}")CrawImagesService.doCrawJob()} }其中,@Scheduled(cron = "0 */5 * * * ?") 表示每隔5分鐘執(zhí)行一次圖片的抓取。
然后,我們重新啟動應(yīng)用,就會看到每隔5分鐘,我們的定時任務(wù)會去跑一次。
到目前為止,我們的原始數(shù)據(jù)已經(jīng)入庫。下面,我們將要進(jìn)行控制器層代碼和視圖展示層模板引擎的代碼的開發(fā)。最后是前端頁面展示部分的開發(fā)。
13.2.8 控制器層開發(fā)
下面我們實(shí)現(xiàn)一個分頁查詢接口 http://127.0.0.1:8000/sotuJson?page=10&size=3 ,返回的數(shù)據(jù)是 json 格式
{"content": [{"id": 5981,"version": 0,"category": "南非,動物世界,非洲地區(qū)旅游景點(diǎn),風(fēng)景名勝","url": "http://img0.imgtn.bdimg.com/it/u=2871771810,3599000038&fm=27&gp=0.jpg","gmtCreated": 1508858697000,"gmtModified": 1508858697000,"deletedDate": 1508858697000},...{"id": 5979,"version": 0,"category": "亞洲,藍(lán)色,明亮,商務(wù),特寫,地球,環(huán)境,地球形,光,地圖,材料","url": "http://img3.imgtn.bdimg.com/it/u=241353052,3712599419&fm=200&gp=0.jpg","gmtCreated": 1508858696000,"gmtModified": 1508858696000,"deletedDate": 1508858696000}],"pageable": {"sort": {"sorted": true,"unsorted": false},"offset": 30,"pageSize": 3,"pageNumber": 10,"paged": true,"unpaged": false},"last": false,"totalPages": 2004,"totalElements": 6011,"size": 3,"numberOfElements": 3,"sort": {"sorted": true,"unsorted": false},"number": 10,"first": false }實(shí)現(xiàn) findAll 函數(shù)
在 Spring Data JPA 中提供了基本的CRUD操作、分頁查詢、排序等。我們先來實(shí)現(xiàn) ImageRepository 接口中的 findAll 函數(shù)
@Query("SELECT a from #{#entityName} a where a.isDeleted=0 order by a.id desc") override fun findAll(pageable: Pageable): Page<Image>@Query 注解與 #{#entityName}
其中 @Query 是JPA中的查詢注解。JPA中可以執(zhí)行兩種方式的查詢,一種是使用JPQL,一種是使用Native SQL。其中JPQL是基于 Entity 對象(@Entity 注解標(biāo)注的對象)的查詢,可以消除不同數(shù)據(jù)庫SQL語句的差異;本地SQL是基于傳統(tǒng)的SQL查詢,是對JPQL查詢的補(bǔ)充。
這里的 JPQL 我們使用#{#entityName} 代替本來實(shí)體的名稱,而Spring Data JPA 會自動根據(jù) Image 實(shí)體上對應(yīng)的 @Entity(name = "Image") 或者是默認(rèn)的@Entity,來自動將實(shí)體名稱填入HQL 語句中。
實(shí)體類 Image 使用@Entity注解后,Spring Data JPA 的 EntityManager 會將實(shí)體類 Image 納入管理。默認(rèn)的 #{#entityName} 的值就是 Image ,如果指定其中的@Entity(name = "Image") name 的值,那么 #{#entityName} 就是指定的值。
在 JPQL 語句中
SELECT a from #{#entityName} a where a.isDeleted=0 order by a.id desc我們就可以像訪問Kotlin 類屬性一樣來訪問字段值。注意到,我們這里的a.isDeleted 是屬性名稱。
Pageable 參數(shù)
SpringData JPA 的 PagingAndSortingRepository接口已經(jīng)提供了對分頁的支持,查詢的時候我們只需要傳入一個 Pageable 類型的實(shí)現(xiàn)類。這個Pageable接口定義如下
package org.springframework.data.domain; import java.util.Optional; import org.springframework.util.Assert;public interface Pageable {static Pageable unpaged() {return Unpaged.INSTANCE;}default boolean isPaged() {return true;}default boolean isUnpaged() {return !isPaged();}int getPageNumber();int getPageSize();long getOffset();Sort getSort();default Sort getSortOr(Sort sort) {Assert.notNull(sort, "Fallback Sort must not be null!");return getSort().isSorted() ? getSort() : sort;}Pageable next();Pageable previousOrFirst();Pageable first();boolean hasPrevious();default Optional<Pageable> toOptional() {return isUnpaged() ? Optional.empty() : Optional.of(this);} }springData包中的 PageRequest類已經(jīng)實(shí)現(xiàn)了Pageable接口,我們可以像下面這樣直接使用
val sort = Sort(Sort.Direction.DESC, "id") val pageable = PageRequest.of(page, size, sort)其中,Direction 是 Sort 類中定義的注解
public static enum Direction {ASC, DESC; }Sort 類的構(gòu)造函數(shù)簽名是
public Sort(Direction direction, String... properties) {this(direction, properties == null ? new ArrayList<>() : Arrays.asList(properties)); }我們這里Sort(Sort.Direction.DESC, "id")傳入的是根據(jù) id 進(jìn)行降序排序。
Page<T> 返回類型
findAll 函數(shù)的返回類型是 Page<Image> , 這里的 Page 類型是 Spring Data JPA 的分頁結(jié)果的返回對象,Page 繼承了 Slice 。這兩個接口的定義如下
public interface Page<T> extends Slice<T> {static <T> Page<T> empty() {return empty(Pageable.unpaged());}static <T> Page<T> empty(Pageable pageable) {return new PageImpl<>(Collections.emptyList(), pageable, 0);}int getTotalPages();long getTotalElements();<U> Page<U> map(Function<? super T, ? extends U> converter); }public interface Slice<T> extends Streamable<T> {int getNumber();int getSize();int getNumberOfElements();List<T> getContent();boolean hasContent();Sort getSort();boolean isFirst();boolean isLast();boolean hasNext();boolean hasPrevious();default Pageable getPageable() {return PageRequest.of(getNumber(), getSize(), getSort());}Pageable nextPageable();Pageable previousPageable();<U> Slice<U> map(Function<? super T, ? extends U> converter); }這個分頁對象的數(shù)據(jù)結(jié)構(gòu)信息足夠我們在前端實(shí)現(xiàn)分頁的交互頁面時使用。
我們來實(shí)現(xiàn)分頁查詢所有 image 表記錄的REST API 接口。在 controller 包路徑下面新建 ImageController 類, 類上使用 @Controller注解。
package com.easy.kotlin.picturecrawler.controllerimport com.easy.kotlin.picturecrawler.dao.ImageRepository import com.easy.kotlin.picturecrawler.entity.Image import org.springframework.beans.factory.annotation.Autowired import org.springframework.data.domain.Page import org.springframework.data.domain.PageRequest import org.springframework.data.domain.Sort import org.springframework.stereotype.Controller import org.springframework.ui.Model import org.springframework.web.bind.annotation.RequestMapping import org.springframework.web.bind.annotation.RequestMethod import org.springframework.web.bind.annotation.RequestParam import org.springframework.web.bind.annotation.ResponseBody import org.springframework.web.servlet.ModelAndView import javax.servlet.http.HttpServletRequest@Controller class ImageController {@Autowiredlateinit var imageRepository: ImageRepository@RequestMapping(value = "sotuJson", method = arrayOf(RequestMethod.GET))@ResponseBodyfun sotuJson(@RequestParam(value = "page", defaultValue = "0") page: Int,@RequestParam(value = "size", defaultValue = "10") size: Int): Page<Image> {return getPageResult(page, size)}private fun getPageResult(page: Int, size: Int): Page<Image> {val sort = Sort(Sort.Direction.DESC, "id")val pageable = PageRequest.of(page, size, sort)return imageRepository.findAll(pageable)}... }其中
@Autowired lateinit var imageRepository: ImageRepository這里使用 lateinit 關(guān)鍵字來修飾我們需要裝配的 Bean ,表示 imageRepository 延遲初始化。
從上面的代碼可以看出,Kotlin 使用Spring MVC非常自然,跟使用原生 Java 代碼幾乎一樣順暢。有個稍微明顯的區(qū)別是 method = arrayOf(RequestMethod.GET) , 這里Kotlin 數(shù)組聲明的語法是使用 arrayOf() , 而這個在 Java 中只需要使用花括號 { } 括起來即可。
重新運(yùn)行應(yīng)用,瀏覽器訪問 http://127.0.0.1:8000/sotuJson?page=10&size=3 ,我們將看到分頁對象 Page 的 JSON 字符串格式的輸出結(jié)果。
模糊搜索分頁接口實(shí)現(xiàn)
下面我們來實(shí)現(xiàn)根據(jù) category 字段的值進(jìn)行模糊搜索的接口,并同時支持分頁。代碼如下
@Query("SELECT a from #{#entityName} a where a.isDeleted=0 and a.category like %:searchText% order by a.id desc") fun search(@Param("searchText") searchText: String, pageable: Pageable): Page<Image>其中 @Param("searchText") searchText: String 是搜索關(guān)鍵字參數(shù) @Param 注解指定了JPQL 中的參數(shù)名 searchText ,對應(yīng)到 JPQL 中的參數(shù)占位符寫作 :searchText ,我們注意到這里的模糊查詢的語法是
like %:searchText%對應(yīng)的 Controller 中的方法是
@RequestMapping(value = "sotuSearchJson", method = arrayOf(RequestMethod.GET)) @ResponseBody fun sotuSearchJson(@RequestParam(value = "page", defaultValue = "0") page: Int, @RequestParam(value = "size", defaultValue = "10") size: Int, @RequestParam(value = "searchText", defaultValue = "") searchText: String): Page<Image> {return getPageResult(page, size, searchText) }private fun getPageResult(page: Int, size: Int, searchText: String): Page<Image> {val sort = Sort(Sort.Direction.DESC, "id")val pageable = PageRequest.of(page, size, sort)if (searchText == "") {return imageRepository.findAll(pageable)} else {return imageRepository.search(searchText, pageable)} }這里需要注意的是 PageRequest.of(page,size,sort) page 取值默認(rèn)是從0開始 。
重新運(yùn)行應(yīng)用,瀏覽器訪問 http://127.0.0.1:8000/sotuSearchJson?page=10&size=3&searchText=秋天 ,我們可以看到輸出
{"content": [{"id": 17443,"version": 0,"category": "初秋岱廟","url": "http://img0.imgtn.bdimg.com/it/u=64076324,3274882882&fm=27&gp=0.jpg","gmtCreated": 1508924545000,"gmtModified": 1508924545000,"deletedDate": 1508924545000},{"id": 17280,"version": 0,"category": "初秋落葉信紙.doc","url": "http://img5.imgtn.bdimg.com/it/u=256290403,1153099708&fm=27&gp=0.jpg","gmtCreated": 1508924528000,"gmtModified": 1508924528000,"deletedDate": 1508924528000},{"id": 17130,"version": 0,"category": "初秋的小花圖片 12張 (天堂圖片網(wǎng))","url": "http://img3.imgtn.bdimg.com/it/u=1333940222,533390017&fm=11&gp=0.jpg","gmtCreated": 1508924510000,"gmtModified": 1508924510000,"deletedDate": 1508924510000}],"pageable": {"sort": {"sorted": true,"unsorted": false},"offset": 30,"pageSize": 3,"pageNumber": 10,"paged": true,"unpaged": false},"last": false,"totalElements": 148,"totalPages": 50,"size": 3,"number": 10,"numberOfElements": 3,"sort": {"sorted": true,"unsorted": false},"first": false }13.2.9 展示層模板引擎代碼
后端的數(shù)據(jù)接口我們已經(jīng)開發(fā)完畢,下面我們來把這些數(shù)據(jù)展示到前端頁面上。
我們使用的視圖層模板引擎是 Freemarker , 在 SpringBoot 中使用Freemarker,只需要加入 spring-boot-starter-freemarker 。其中,使用默認(rèn)的配置目錄 src/main/resources/templates , 模板文件以 .ftl 為后綴。
我們將前端依賴的外部庫靜態(tài)資源文件全部放到 src/main/resources/static/bower_components 文件夾下 。我們主要使用的是jquery.js 、bootstrap.js ,另外使用后端的分頁接口實(shí)現(xiàn)前端分頁的功能我們使用 bootstrap-table.js 庫來實(shí)現(xiàn)。前端模板文件以及 js 源碼文件的目錄結(jié)構(gòu)如下圖所示
前端模板文件以及 js 源碼文件的目錄結(jié)構(gòu)head.ftl
head.ftl 文件是公共文件頭部分,代碼如下
<!DOCTYPE html> <html> <head><meta http-equiv=content-type content=text/html;charset=utf-8><meta http-equiv=X-UA-Compatible content=IE=Edge><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no"><title>搜圖</title><link href="bower_components/bootstrap/dist/css/bootstrap-theme.css" rel="stylesheet"><link href="bower_components/bootstrap-table/src/bootstrap-table.css" rel="stylesheet"><link href="bower_components/bootstrap/dist/css/bootstrap.css" rel="stylesheet"><link href="bower_components/pnotify/src/pnotify.css" rel="stylesheet"><link href="app.css" rel="stylesheet"> </head> <body>foot.ftl
foot.ftl 是頁面公共底部。代碼如下
<script src="bower_components/jquery/dist/jquery.js"></script> <script src="bower_components/bootstrap/dist/js/bootstrap.js"></script> <script src="bower_components/bootstrap-table/src/bootstrap-table.js"></script> <script src="bower_components/bootstrap-table/src/locale/bootstrap-table-zh-CN.js"></script> <script src="bower_components/pnotify/src/pnotify.js"></script> <script src="app.js"></script> </body> </html>nav.ftl
nav.ftl 是導(dǎo)航欄部分的代碼,使用標(biāo)準(zhǔn)的 Bootstrap 樣式庫來實(shí)現(xiàn)
<nav class="navbar navbar-default" role="navigation"><div class="container-fluid"><div class="navbar-header"><a class="navbar-brand" href="#">搜圖</a></div><div><ul class="nav navbar-nav"><li class='<#if requestURI=="/sotu_view">active</#if>'><a href="sotu_view">美圖列表</a></li><li class='<#if requestURI=="/sotu_favorite_view">active</#if>'><a href="sotu_favorite_view">精選收藏</a><li class='<#if requestURI=="/search_keyword_view">active</#if>'><a href="search_keyword_view">搜索關(guān)鍵字</a></li><li class=""><a href="doCrawJob" target="_blank">執(zhí)行抓取</a></li><li class="dropdown"><a href="#" class="dropdown-toggle" data-toggle="dropdown">Kotlin <b class="caret"></b></a><ul class="dropdown-menu"><li><a href="http://www.jianshu.com/nb/12976878" target="_blank">Kotlin 極簡教程</a></li><li><a href="http://www.jianshu.com/nb/17117730" target="_blank">Kotlin 項(xiàng)目實(shí)戰(zhàn)開發(fā)</a></li><li><a href="#">SpringBoot</a></li><li><a href="#">Java</a></li><li class="divider"></li><li><a href="#">Scala</a></li><li class="divider"></li><li><a href="#">Groovy</a></li></ul></li><li class="nav-item"><a class="nav-link" href="#">關(guān)于</a></li></ul></div></div> </nav>其中這地方的代碼是實(shí)現(xiàn) Tab 切換的時候,active 狀態(tài)跟隨的交互
<li class='<#if requestURI=="/sotu_view">active</#if>'><a href="sotu_view">美圖列表</a></li><li class='<#if requestURI=="/sotu_favorite_view">active</#if>'><a href="sotu_favorite_view">精選收藏</a><li class='<#if requestURI=="/search_keyword_view">active</#if>'><a href="search_keyword_view">搜索關(guān)鍵字</a></li>requestURI 是后端的 Controller 獲取當(dāng)前請求傳給前端頁面的
@RequestMapping(value = *arrayOf("/", "sotu_view"), method = arrayOf(RequestMethod.GET)) fun sotuView(model: Model, request: HttpServletRequest): ModelAndView {model.addAttribute("requestURI", request.requestURI)return ModelAndView("sotu_view") }圖片列表頁面
新建 sotu_view.ftl 為圖片列表頁面
<#include 'common/head.ftl'> <#include 'common/nav.ftl'> <table id="sotu_table"></table> <#include 'common/foot.ftl'> <script src="sotu_table.js"></script>對應(yīng)的 ModelAndView 控制器代碼是
@RequestMapping(value = *arrayOf("/", "sotu_view"), method = arrayOf(RequestMethod.GET)) fun sotuView(model: Model, request: HttpServletRequest): ModelAndView {model.addAttribute("requestURI", request.requestURI)return ModelAndView("sotu_view") }表格后端分頁實(shí)現(xiàn)
新建 sotu_table.js , 我們在這里寫表格后端分頁實(shí)現(xiàn)的前端 js 代碼。主要是使用 bootstrap-table.js 中的 bootstrapTable 函數(shù)來完成
$.fn.bootstrapTable = function (option)sotu_table.js 的代碼如下
$(function () {$.extend($.fn.bootstrapTable.defaults, $.fn.bootstrapTable.locales['zh-CN'])var searchText = $('.search').find('input').val()var columns = []columns.push({title: '分類',field: 'category',align: 'center',valign: 'middle',width: '5%',formatter: function (value, row, index) {return value}}, {title: '美圖',field: 'url',align: 'center',valign: 'middle',formatter: function (value, row, index) {return ""}}, {title: ' 操作',field: 'id',align: 'center',width: '5%',formatter: function (value, row, index) {var html = ""html += "<div onclick='addFavorite(" + value + ")' name='addFavorite' id='addFavorite" + value + "' class='btn btn-default'>收藏</div><p>"html += "<div onclick='deleteById(" + value + ")' name='delete' id='delete" + value + "' class='btn btn-default'>刪除</div>"return html}})$('#sotu_table').bootstrapTable({url: 'sotuSearchJson',sidePagination: "server",queryParamsType: 'page,size',contentType: "application/x-www-form-urlencoded",method: 'get',striped: false, //是否顯示行間隔色buttonsAlign: 'right',smartDisplay: true,cache: false, //是否使用緩存,默認(rèn)為true,所以一般情況下需要設(shè)置一下這個屬性(*)pagination: true, //是否顯示分頁(*)paginationLoop: true,paginationHAlign: 'right', //right, leftpaginationVAlign: 'bottom', //bottom, top, bothpaginationDetailHAlign: 'left', //right, leftpaginationPreText: ' 上一頁',paginationNextText: '下一頁',search: true,searchText: searchText,searchTimeOut: 500,searchAlign: 'right',searchOnEnterKey: false,trimOnSearch: true,sortable: true, //是否啟用排序sortOrder: "desc", //排序方式sortName: "id",pageNumber: 1, //初始化加載第一頁,默認(rèn)第一頁pageSize: 10, //每頁的記錄行數(shù)(*)pageList: [8, 16, 32, 64, 128], // 可選的每頁數(shù)據(jù)totalField: 'totalElements', // 所有記錄 countdataField: 'content', //后端 json 對應(yīng)的表格List數(shù)據(jù)的 keycolumns: columns,queryParams: function (params) {return {size: params.pageSize,page: params.pageNumber - 1,sortName: params.sortName,sortOrder: params.sortOrder,searchText: params.searchText}},classes: 'table table-responsive full-width',})$(document).on('keydown', function (event) {// 鍵盤翻頁事件var e = event || window.event || arguments.callee.caller.arguments[0];if (e && e.keyCode == 38 || e && e.keyCode == 37) {//上,左// 上一頁$('.page-pre').click()}if (e && e.keyCode == 40 || e && e.keyCode == 39) {//下,右// 下一頁$('.page-next').click()}})var keyWord = getKeyWord()$('.search').find('input').val(keyWord)})function getKeyWord() {var url = decodeURI(location.href)var indexOfKeyWord = url.indexOf('?keyWord=')if (indexOfKeyWord != -1) {var start = indexOfKeyWord + '?keyWord='.lengthreturn url.substring(start)} else {return ""} }其中 url: 'sotuSearchJson' 后端的查詢接口實(shí)現(xiàn)代碼是
@RequestMapping(value = "sotuSearchJson", method = arrayOf(RequestMethod.GET)) @ResponseBody fun sotuSearchJson(@RequestParam(value = "page", defaultValue = "0") page: Int, @RequestParam(value = "size", defaultValue = "10") size: Int, @RequestParam(value = "searchText", defaultValue = "") searchText: String): Page<Image> {return getPageResult(page, size, searchText) }private fun getPageResult(page: Int, size: Int, searchText: String): Page<Image> {val sort = Sort(Sort.Direction.DESC, "id")// 注意:PageRequest.of(page,size,sort) page 默認(rèn)是從0開始val pageable = PageRequest.of(page, size, sort)if (searchText == "") {return imageRepository.findAll(pageable)} else {return imageRepository.search(searchText, pageable)} }其中,
dataField: 'content' 對應(yīng)的是 Page<Image> 中的 content 屬性的值;
totalField: 'totalElements' 對應(yīng)的是 Page<Image> 中 totalElements屬性的值。
columns: columns 是對應(yīng)到 content List 中的每個元素的對象屬性。例如
var columns = []columns.push({title: '分類',field: 'category',align: 'center',valign: 'middle',width: '5%',formatter: function (value, row, index) {return value},... }里面的field: 'category' 對應(yīng)的就是Image 實(shí)體類的category 屬性名稱。然后,我們在formatter: function (value, row, index) 函數(shù)中處理改單元格顯示的樣式 html 。
重新啟動運(yùn)行應(yīng)用,我們將看到分頁以及模糊搜索的效果
模糊搜索的效果 分頁的效果提示:Bootstrap-table完整的配置項(xiàng)在 bootstrap-table.js 源碼 (https://github.com/wenzhixin/bootstrap-table)中的BootstrapTable.DEFAULTS 這行代碼中
BootstrapTable.DEFAULTS = {classes: 'table table-hover',sortClass: undefined,locale: undefined,height: undefined,undefinedText: '-',sortName: undefined,sortOrder: 'asc',sortStable: false,rememberOrder: false,striped: false,columns: [[]],data: [],totalField: 'total',dataField: 'rows',method: 'get',url: undefined,ajax: undefined,cache: true,contentType: 'application/json',dataType: 'json',ajaxOptions: {},queryParams: function (params) {return params;},queryParamsType: 'limit', // undefinedresponseHandler: function (res) {return res;},pagination: false,onlyInfoPagination: false,paginationLoop: true,sidePagination: 'client', // client or servertotalRows: 0, // server side need to setpageNumber: 1,pageSize: 10,pageList: [10, 25, 50, 100],paginationHAlign: 'right', //right, leftpaginationVAlign: 'bottom', //bottom, top, bothpaginationDetailHAlign: 'left', //right, leftpaginationPreText: '?',paginationNextText: '?',search: false,searchOnEnterKey: false,strictSearch: false,searchAlign: 'right',selectItemName: 'btSelectItem',showHeader: true,... }收藏、刪除功能
下面我們來實(shí)現(xiàn)收藏圖片和刪除圖片功能。后端接口實(shí)現(xiàn)邏輯如下
@Modifying @Transactional @Query("update #{#entityName} a set a.isFavorite=1,a.gmtModified=now() where a.id=:id") fun addFavorite(@Param("id") id: Long)@Modifying @Transactional @Query("update #{#entityName} a set a.isDeleted=1 where a.id=:id") fun delete(@Param("id") id: Long)我們用 isFavorite=1來表示該圖片是被收藏的,isDeleted=1 表示該圖片被刪除。需要注意的是 JPA 中 update、delete 操作需要在對應(yīng)的函數(shù)上面添加@Modifying 和 @Transactional 注解。
控制層的 http 接口代碼如下
@RequestMapping(value = "addFavorite", method = arrayOf(RequestMethod.POST)) @ResponseBody fun addFavorite(@RequestParam(value = "id") id: Long): Boolean {imageRepository.addFavorite(id)return true }@RequestMapping(value = "delete", method = arrayOf(RequestMethod.POST)) @ResponseBody fun delete(@RequestParam(value = "id") id: Long): Boolean {imageRepository.delete(id)return true }前端 js 代碼如下
function addFavorite(id) {$.ajax({url: 'addFavorite',data: {id: id},dataType: 'json',type: 'post',success: function (resp) {// alert(JSON.stringify(resp))new PNotify({title: '收藏操作',styling: 'bootstrap3',text: JSON.stringify(resp),type: 'success',delay: 500,});},error: function (msg) {// alert(JSON.stringify(msg))new PNotify({title: '收藏操作',styling: 'bootstrap3',text: JSON.stringify(msg),type: 'error',delay: 500,});}}) }function deleteById(id) {$.ajax({url: 'delete',data: {id: id},dataType: 'json',type: 'post',success: function (resp) {// alert(JSON.stringify(resp))$('#sotu_favorite_table').bootstrapTable('refresh')$('#sotu_table').bootstrapTable('refresh')new PNotify({title: '刪除操作',styling: 'bootstrap3',text: JSON.stringify(resp),type: 'info',delay: 500,});},error: function (msg) {// alert(JSON.stringify(msg))new PNotify({title: '刪除操作',styling: 'bootstrap3',text: JSON.stringify(msg),type: 'error',delay: 500,});}}) }對應(yīng)的表格中的前端按鈕組件代碼在 sotu_table.js 中,關(guān)鍵片段如下
{title: ' 操作',field: 'id',align: 'center',width: '5%',formatter: function (value, row, index) {var html = ""html += "<div onclick='addFavorite(" + value + ")' name='addFavorite' id='addFavorite" + value + "' class='btn btn-default'>收藏</div><p>"html += "<div onclick='deleteById(" + value + ")' name='delete' id='delete" + value + "' class='btn btn-default'>刪除</div>"return html}}點(diǎn)擊圖片下載功能
在 sotu_table.js 中,我們實(shí)現(xiàn)點(diǎn)擊圖片自動觸發(fā)下載圖片到本地的功能。代碼如下
{title: '美圖',field: 'url',align: 'center',valign: 'middle',formatter: function (value, row, index) {return ""}}其中,downloadImage 函數(shù)實(shí)現(xiàn)如下
function downloadImage(src) {var $a = $("<a></a>").attr("href", src).attr("download", "sotu.png");$a[0].click(); }總結(jié)
以上是生活随笔為你收集整理的第13章 Kotlin 集成 SpringBoot 服务端开发(1)的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 【以太坊】ubuntu安装以太坊ethe
- 下一篇: 常用的css片段