javascript
第13章 Kotlin 集成 SpringBoot 服务端开发(1)
第13章 Kotlin 集成 SpringBoot 服務端開發
本章介紹Kotlin服務端開發的相關內容。首先,我們簡單介紹一下Spring Boot服務端開發框架,快速給出一個 Restful Hello World的示例。然后,我們講下 Kotlin 集成 Spring Boot 進行服務端開發的步驟,最后給出一個完整的 Web 應用開發實例。
13.1 SpringBoot 快速開始 Restful Hello World
Spring Boot 大大簡化了使用 Spring 框架過程中的各種繁瑣的配置, 另外可以更加方便的整合常用的工具鏈 (比如 Redis, Email, kafka, ElasticSearch, MyBatis, JPA) 等, 而缺點是集成度較高(事物都是兩面性的),使用過程中不太容易了解底層,遇到問題了解決曲線比較陡峭。本節我們介紹怎樣快速開始SpringBoot服務端開發。
13.1.1 Spring Initializr
工欲善其事必先利其器。我們使用 https://start.spring.io/ 可以直接自動生成 SpringBoot項目腳手架。如下圖
start.spring.io點擊“Switch to the full version ” , 可以看到腳手架支持的工具鏈。
如果 https://start.spring.io/ 網絡連不上,我們也可以自己搭建本地的 Spring Initializr服務。步驟如下
即可看到啟動日志
...... 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)此時,我們本機瀏覽器訪問 http://127.0.0.1:8080/ ,即可看到腳手架initializr頁面。
13.1.2 創建SpringBoot項目
我們使用本地搭建的腳手架initializr, 頁面上表單選項如下
使用spring initializr創建SpringBoot項目首先 ,我們選擇生成的是一個使用Gradle 構建的Kotlin項目,SpringBoot的版本號我們選擇2.0.0(SNAPSHOT) 。
在 Spring Boot Starters 和 dependencies 選項中,我們選擇 Web starter, 這個啟動器里面包含了基本夠用的Spring Web開發需要的東西:Tomcat 和 Spring MVC。
其余的項目元數據(Project Metadata)的配置(Bill Of Materials),我們可以從上面的圖中看到。然后,點擊“Generate Project” ,會自動下載一個項目的zip壓縮包。解壓導入IDEA中
導入IDEA因為我們使用的是Gradle構建項目,所以需要配置一下Gradle環境,這里我們使用的是Local gradle distribution , 選擇對應的本地的 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文件夾下面放置靜態資源文件,templates目錄下面放置視圖模板文件。
build.gradle 配置文件
我們使用 Gradle 來構建項目。其中 build.gradle 配置文件類似 Maven中的pom.xml 配置文件。我們使用 Spring Initializr 自動生成的樣板項目的默認配置如下
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 框架,把類全部設置為 open 的插件。因為Kotlin 的所有類及其成員默認情況下都是 final 的,也就是說你想要繼承一個類,就要不斷得寫各種 open。而使用Java寫的 Spring 框架中大量使用了繼承和覆寫,這個時候使用 kotlin-allopen 插件結合 kotlin-spring 插件,可以自動把 Spring 相關的所有注解的類設置為 open 。
spring-boot-starter-web 就是SpringBoot中提供的使用Spring框架進行Web應用開發的啟動器。
kotlin-stdlib-jre8 是Kotlin使用Java 8 的庫,kotlin-reflect 是 Kotlin 的反射庫。
項目的整體依賴如下圖所示
項目的整體依賴我們可以看出,spring-boot-starter-web 中已經引入了我們所需要的 json 、tomcat 、validator 、webmvc (其中引入了Spring框架的核心web、context、aop、beans、expressions、core)等框架。
SpringBoot項目的入口類 KotlinWithSpringbootApplication
自動生成的 SpringBoot項目的入口類 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 統一的注解。這個注解的定義源碼如下
@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 函數中的 KotlinWithSpringbootApplication::class.java 是一個使用反射獲取KotlinWithSpringbootApplication類的Java Class引用。這也正是我們在依賴中引入 kotlin-reflect 包的用途所在。
寫 Hello World 控制器
下面我們來實現一個簡單的Hello World 控制器 。 首先新建 HelloWorldController Kotlin 類,代碼實現如下
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!"}}啟動運行
系統默認端口號是8080,我們在application.properties 中添加一行服務端口號的配置
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)也可以點擊IDEA的Gradle工具欄里面的 Tasks - application - bootRun 執行
Gradle工具欄 Tasks - application - bootRun啟動完畢后,我們直接在瀏覽器中打開 http://127.0.0.1:8000/ , 可以看到輸出了 Hello World!
Hello World!本節項目源碼:https://github.com/EasySpringBoot/kotlin-with-springboot
13.2 綜合實戰:一個圖片爬蟲的Web應用實例
上面我們已經看到了使用Kotlin 集成 SpringBoot開發的基本步驟。本節我們給出一個使用MySQL數據庫、 Spring Data JPA ORM框架、Freemarker模板引擎的完整Web項目的實例。
13.2.1 系統技術棧
本節介紹使用Kotlin 集成 SpringBoot 開發一個完整的圖片爬蟲Web應用,基本功能如下
- 定時抓取圖片搜索API的根據關鍵字搜索返回的圖片json信息,解析入庫
- Web頁面分頁展示圖片列表,支持收藏、刪除等功能
- 列表支持根據圖片分類進行模糊搜索
涉及的主要技術棧如下
- 編程語言:Kotlin
- 數據庫層: MySQL、mysql-jdbc-driver 、JPA
- 企業級開發框架:Spring Boot、 Spring MVC
- 視圖層模板引擎: Freemarker
- 前端框架: jQuery 、 Bootstrap 、Bootstrap-table
- 工程構建工具:Gradle
13.2.2 準備工作
使用 Spring Initializr 創建項目
如下圖配置項目基本信息和依賴
使用 Spring Initializr 創建項目自動生成項目源碼工程,導入IDEA中,等待構建完畢,我們將得到下面的工程目錄
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中已經封裝了這個工具鏈所需要的依賴庫。整個項目的依賴如下圖所示
整個項目的依賴目前我們的工程已經具備了連接MySQL數據庫、解析Freemarker 的 .ftl 模板文件等的能力了。但是,此時如果啟動會報錯
BeanCreationException: Error creating bean with name 'dataSource' defined in class path resource [org/springframework/boot/autoconfigure/jdbc/DataSourceConfiguration$Hikari.class]創建 dataSource Bean失敗。因為,我們還沒有配置任何數據庫連接信息。下面我們來配置數據源 dataSource 。
13.2.3 配置數據源
Spring Boot 的數據源配置在 application.properties 中是以 spring.datasource 為前綴。例如,新建一個 wotu 庫
CREATE SCHEMA `wotu` DEFAULT CHARACTER SET utf8 ;我們配置數據庫的連接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然后,再次啟動應用,我們可以發現啟動成功。
13.2.4 數據庫表結構設計
下面我們從數據庫層開始構建我們的應用。首先我們先設計數據庫的表結構如下
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;因為我們使用的是 JPA,只需要寫好實體類代碼,啟動應用即可自動創建表結構到 MySQL 數據庫中。實體類代碼如下
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會自動創建表,以后啟動會覆蓋之前的表,所以這個值基本不用,嚴重會導致的數據的丟失。 |
| create-drop | 每次加載hibernate時根據model類生成表,但是sessionFactory一關閉,表就自動刪除,下一次啟動會重新創建。 |
| update | 加載hibernate時根據實體類model創建數據庫表,這是表名的依據是@Entity注解的值或者@Table注解的值,sessionFactory關閉表不會刪除,且下一次啟動會根據實體model更新結構或者有新的實體類會創建新的表。 |
| validate | 啟動時驗證表的結構,不會創建表 |
| none | 啟動時不做任何操作 |
所以,在開發項目的過程中,我們通常會選用 update 選項。
再次啟動應用,啟動完畢后我們可以看到數據庫中已經自動創建了 image 表
image 表結構標注索引
為了更高的性能,我們建立類別 category 字段和 url 索引。其中 url 是唯一索引
ALTER TABLE `sotu`.`image`ADD INDEX `idx_category` (`category` ASC),ADD UNIQUE INDEX `uk_url` (`url` ASC);而實際上,我們不需要去手工寫上面的 SQL 然后再去數據庫中執行。我們只需要寫下面的實體類
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 建立索引, 以及設定 url 唯一性約束 unique = true
@Table(indexes = arrayOf(Index(name = "idx_url", unique = true, columnList = "url"),Index(name = "idx_category", unique = false, columnList = "category")))啟動應用的時候,JPA 會去解析我們的注解生成對應的 SQL,并且自動去執行相應的 SQL。例如字段url 的唯一索引約束,我們可以在啟動日志中看到如下的輸出
Hibernate: alter table image drop index idx_url Hibernate: alter table image add constraint idx_url unique (url)其中,Index 是@Index 注解,當做參數使用的時候不需要加@ 。
我們再舉個例子。實體類代碼如下
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() }重啟應用,我們可以看到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)自動生成的表結構如下
自動生成的 search_key_word 表結構其中,@Column(length = 50, unique = true, nullable = false) 這一句指定了keyWord 字段的長度是50,有唯一約束,不可空。對應生成的數據庫表字段 key_word 信息:Type 是 varchar(50) , Null 是 NO, Key 是唯一鍵 UNI 。
主鍵自動生成策略
我們使用@Id 注解來標注主鍵字段
@Id @GeneratedValue(strategy = GenerationType.IDENTITY) var id: Long = -1其中的 @GeneratedValue(strategy = GenerationType.IDENTITY) 注解,我們重點介紹一下。這里的GenerationType是主鍵 ID 的生成規則。JPA提供的四種標準用法為 TABLE、SEQUENCE、IDENTITY、AUTO
| TABLE | 使用一個特定的數據庫表格來保存主鍵。 |
| SEQUENCE | 根據底層數據庫的序列來生成主鍵,條件是數據庫支持序列。 |
| IDENTITY | 主鍵由數據庫自動生成(主要是自動增長型) |
| AUTO | 主鍵由程序控制。 |
我們設計源碼目錄如下
├── src │ ├── main │ │ ├── java │ │ ├── kotlin │ │ │ └── com │ │ │ └── easy │ │ │ └── kotlin │ │ │ └── picturecrawler │ │ │ ├── PictureCrawlerApplication.kt │ │ │ ├── controller │ │ │ ├── dao │ │ │ ├── entity │ │ │ ├── job │ │ │ └── service ...其中,controller 放置 Controller 控制器代碼;
entity 放置對應到數據庫表的實體類代碼;
dao 層放置數據訪問層邏輯代碼;
service 層放置業務邏輯實現代碼;
job 層放置定時任務代碼。
13.2.5 JSON 數據解析
我們的圖片搜索 API 返回的數據結構是 JSON 格式的,內容示例如下
{"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 對象是我們定義的數據轉換對象
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 數據入庫邏輯實現
現在我們已經有了數據的表結構,實體類代碼; 同時也已經有個業務源數據了。現在我們要做的是把爬到的圖片信息存儲到數據庫中。同時,重復的 url 信息我們不去重復存儲。
新建一個實現 PagingAndSortingRepository<Image, Long> 的 ImageRepository 接口
interface ImageRepository : PagingAndSortingRepository<Image, Long>只要上面的一行代碼,我們就可以直接使用ImageRepository 的 CRUD 方法了。因為 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(); }我們入庫就直接使用save(S entity) 方法。但是為了保證重復的 url 不保存,需要寫個函數來判斷當前 url 是否在數據庫中存在。我們直接使用 select count() 語句來判斷即可, 當且僅當 select count() 出來的值等于 0 (表明數據庫中不存在此 url ),才進行入庫動作。在ImageRepository 接口中直接聲明函數即可,代碼如下
@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 定時調度任務執行
為了簡單起見,我們直接使用 Spring 自帶的scheduling 包下面的@Schedules 注解來實現任務的定時執行。需要注意的是,要在 SpringBoot 的啟動類上面添加注解
@SpringBootApplication @EnableScheduling class PictureCrawlerApplication我們的定時任務代碼如下
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("開始執行定時任務: ${Date()}")CrawImagesService.doCrawJob()} }其中,@Scheduled(cron = "0 */5 * * * ?") 表示每隔5分鐘執行一次圖片的抓取。
然后,我們重新啟動應用,就會看到每隔5分鐘,我們的定時任務會去跑一次。
到目前為止,我們的原始數據已經入庫。下面,我們將要進行控制器層代碼和視圖展示層模板引擎的代碼的開發。最后是前端頁面展示部分的開發。
13.2.8 控制器層開發
下面我們實現一個分頁查詢接口 http://127.0.0.1:8000/sotuJson?page=10&size=3 ,返回的數據是 json 格式
{"content": [{"id": 5981,"version": 0,"category": "南非,動物世界,非洲地區旅游景點,風景名勝","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": "亞洲,藍色,明亮,商務,特寫,地球,環境,地球形,光,地圖,材料","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 }實現 findAll 函數
在 Spring Data JPA 中提供了基本的CRUD操作、分頁查詢、排序等。我們先來實現 ImageRepository 接口中的 findAll 函數
@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中可以執行兩種方式的查詢,一種是使用JPQL,一種是使用Native SQL。其中JPQL是基于 Entity 對象(@Entity 注解標注的對象)的查詢,可以消除不同數據庫SQL語句的差異;本地SQL是基于傳統的SQL查詢,是對JPQL查詢的補充。
這里的 JPQL 我們使用#{#entityName} 代替本來實體的名稱,而Spring Data JPA 會自動根據 Image 實體上對應的 @Entity(name = "Image") 或者是默認的@Entity,來自動將實體名稱填入HQL 語句中。
實體類 Image 使用@Entity注解后,Spring Data JPA 的 EntityManager 會將實體類 Image 納入管理。默認的 #{#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 參數
SpringData JPA 的 PagingAndSortingRepository接口已經提供了對分頁的支持,查詢的時候我們只需要傳入一個 Pageable 類型的實現類。這個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類已經實現了Pageable接口,我們可以像下面這樣直接使用
val sort = Sort(Sort.Direction.DESC, "id") val pageable = PageRequest.of(page, size, sort)其中,Direction 是 Sort 類中定義的注解
public static enum Direction {ASC, DESC; }Sort 類的構造函數簽名是
public Sort(Direction direction, String... properties) {this(direction, properties == null ? new ArrayList<>() : Arrays.asList(properties)); }我們這里Sort(Sort.Direction.DESC, "id")傳入的是根據 id 進行降序排序。
Page<T> 返回類型
findAll 函數的返回類型是 Page<Image> , 這里的 Page 類型是 Spring Data JPA 的分頁結果的返回對象,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); }這個分頁對象的數據結構信息足夠我們在前端實現分頁的交互頁面時使用。
我們來實現分頁查詢所有 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 關鍵字來修飾我們需要裝配的 Bean ,表示 imageRepository 延遲初始化。
從上面的代碼可以看出,Kotlin 使用Spring MVC非常自然,跟使用原生 Java 代碼幾乎一樣順暢。有個稍微明顯的區別是 method = arrayOf(RequestMethod.GET) , 這里Kotlin 數組聲明的語法是使用 arrayOf() , 而這個在 Java 中只需要使用花括號 { } 括起來即可。
重新運行應用,瀏覽器訪問 http://127.0.0.1:8000/sotuJson?page=10&size=3 ,我們將看到分頁對象 Page 的 JSON 字符串格式的輸出結果。
模糊搜索分頁接口實現
下面我們來實現根據 category 字段的值進行模糊搜索的接口,并同時支持分頁。代碼如下
@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 是搜索關鍵字參數 @Param 注解指定了JPQL 中的參數名 searchText ,對應到 JPQL 中的參數占位符寫作 :searchText ,我們注意到這里的模糊查詢的語法是
like %:searchText%對應的 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 取值默認是從0開始 。
重新運行應用,瀏覽器訪問 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張 (天堂圖片網)","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 展示層模板引擎代碼
后端的數據接口我們已經開發完畢,下面我們來把這些數據展示到前端頁面上。
我們使用的視圖層模板引擎是 Freemarker , 在 SpringBoot 中使用Freemarker,只需要加入 spring-boot-starter-freemarker 。其中,使用默認的配置目錄 src/main/resources/templates , 模板文件以 .ftl 為后綴。
我們將前端依賴的外部庫靜態資源文件全部放到 src/main/resources/static/bower_components 文件夾下 。我們主要使用的是jquery.js 、bootstrap.js ,另外使用后端的分頁接口實現前端分頁的功能我們使用 bootstrap-table.js 庫來實現。前端模板文件以及 js 源碼文件的目錄結構如下圖所示
前端模板文件以及 js 源碼文件的目錄結構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 是導航欄部分的代碼,使用標準的 Bootstrap 樣式庫來實現
<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">搜索關鍵字</a></li><li class=""><a href="doCrawJob" target="_blank">執行抓取</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 項目實戰開發</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="#">關于</a></li></ul></div></div> </nav>其中這地方的代碼是實現 Tab 切換的時候,active 狀態跟隨的交互
<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">搜索關鍵字</a></li>requestURI 是后端的 Controller 獲取當前請求傳給前端頁面的
@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>對應的 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") }表格后端分頁實現
新建 sotu_table.js , 我們在這里寫表格后端分頁實現的前端 js 代碼。主要是使用 bootstrap-table.js 中的 bootstrapTable 函數來完成
$.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, //是否使用緩存,默認為true,所以一般情況下需要設置一下這個屬性(*)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, //初始化加載第一頁,默認第一頁pageSize: 10, //每頁的記錄行數(*)pageList: [8, 16, 32, 64, 128], // 可選的每頁數據totalField: 'totalElements', // 所有記錄 countdataField: 'content', //后端 json 對應的表格List數據的 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' 后端的查詢接口實現代碼是
@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 默認是從0開始val pageable = PageRequest.of(page, size, sort)if (searchText == "") {return imageRepository.findAll(pageable)} else {return imageRepository.search(searchText, pageable)} }其中,
dataField: 'content' 對應的是 Page<Image> 中的 content 屬性的值;
totalField: 'totalElements' 對應的是 Page<Image> 中 totalElements屬性的值。
columns: columns 是對應到 content List 中的每個元素的對象屬性。例如
var columns = []columns.push({title: '分類',field: 'category',align: 'center',valign: 'middle',width: '5%',formatter: function (value, row, index) {return value},... }里面的field: 'category' 對應的就是Image 實體類的category 屬性名稱。然后,我們在formatter: function (value, row, index) 函數中處理改單元格顯示的樣式 html 。
重新啟動運行應用,我們將看到分頁以及模糊搜索的效果
模糊搜索的效果 分頁的效果提示:Bootstrap-table完整的配置項在 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,... }收藏、刪除功能
下面我們來實現收藏圖片和刪除圖片功能。后端接口實現邏輯如下
@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 操作需要在對應的函數上面添加@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,});}}) }對應的表格中的前端按鈕組件代碼在 sotu_table.js 中,關鍵片段如下
{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.js 中,我們實現點擊圖片自動觸發下載圖片到本地的功能。代碼如下
{title: '美圖',field: 'url',align: 'center',valign: 'middle',formatter: function (value, row, index) {return ""}}其中,downloadImage 函數實現如下
function downloadImage(src) {var $a = $("<a></a>").attr("href", src).attr("download", "sotu.png");$a[0].click(); }總結
以上是生活随笔為你收集整理的第13章 Kotlin 集成 SpringBoot 服务端开发(1)的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 【以太坊】ubuntu安装以太坊ethe
- 下一篇: 常用的css片段