kotlin实现ajax,第13章 Kotlin 集成 SpringBoot 服务端开发(2)

13.2.10 搜索关键字管理

本节我们开发爬虫爬取的关键字管理的功能。

数据库实体类

首先,新建实体类SearchKeyWord 如下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(name = "keyWord", length = 50, nullable = false, unique = true)

var keyWord: String = ""

@Column(nullable = true)

var totalImage: Int? = 0

var gmtCreated: Date = Date()    var gmtModified: Date = Date()    var isDeleted: Int = 0  //1 Yes 0 No

var deletedDate: Date = Date()

}

其中,keyWord 是搜索关键字,有唯一性约束,同时我们给它建立了索引。

dao 层接口

我们来实现插入数据的 dao 层接口@Modifying

@Transactional

@Query(value = "INSERT INTO `search_key_word` (`deleted_date`, `gmt_created`, `gmt_modified`, `is_deleted`, `key_word`) VALUES (now(), now(), now(), '0', :keyWord) ON DUPLICATE KEY UPDATE `gmt_modified` = now()", nativeQuery = true)

fun saveOnNoDuplicateKey(@Param("keyWord") keyWord: String): Int

其中,ON DUPLICATE KEY UPDATE 这句表明当遇到重复的键值的时候,执行更新 gmt_modified = now() 的操作。这里nativeQuery = true ,表示使用的是原生 SQL 查询。

系统启动初始化动作

我们在应用启动类PictureCrawlerApplication 中添加初始化动作package com.easy.kotlin.picturecrawlerimport com.easy.kotlin.picturecrawler.dao.SearchKeyWordRepositoryimport com.easy.kotlin.picturecrawler.entity.SearchKeyWordimport org.springframework.beans.factory.annotation.Autowiredimport org.springframework.boot.CommandLineRunnerimport org.springframework.boot.SpringApplicationimport org.springframework.boot.autoconfigure.SpringBootApplicationimport org.springframework.core.Orderedimport org.springframework.core.annotation.Orderimport org.springframework.scheduling.annotation.EnableSchedulingimport org.springframework.stereotype.Componentimport java.io.File@SpringBootApplication@EnableSchedulingclass PictureCrawlerApplicationfun main(args: Array) {

SpringApplication.run(PictureCrawlerApplication::class.java, *args)}@Component@Order(value = Ordered.LOWEST_PRECEDENCE)class initSearchKeyWordRunner : CommandLineRunner {

@Autowired lateinit var searchKeyWordRepository: SearchKeyWordRepository    override fun run(vararg args: String) {        var keyWords = File("搜索关键词列表.data").readLines()

keyWords.forEach {            val SearchKeyWord = SearchKeyWord()

SearchKeyWord.keyWord = it

searchKeyWordRepository.saveOnNoDuplicateKey(it)

}

}

}

Spring Boot应用程序在启动后会去遍历 CommandLineRunner 接口的实例并运行它们的run方法。使用@Order注解来指定 CommandLineRunner 实例的运行顺序。

搜索查询接口

查询所有关键字记录接口如下@Query("SELECT a from #{#entityName} a where a.isDeleted=0 order by a.id desc")override fun findAll(pageable: Pageable): Page

模糊搜索关键字接口如下@Query("SELECT a from #{#entityName} a where a.isDeleted=0 and a.keyWord like %:searchText% order by a.id desc")fun search(@Param("searchText") searchText: String, pageable: Pageable): Page

模糊搜索 http 接口实现

跟搜索图片分类的逻辑类似,模糊搜索关键字的接口如下@RequestMapping(value = "searchKeyWordJson", 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 {        return getPageResult(page, size, searchText)

}    private fun getPageResult(page: Int, size: Int, searchText: String): Page {

val sort = Sort(Sort.Direction.DESC, "id")        // 注意:PageRequest.of(page,size,sort) page 默认是从0开始

val pageable = PageRequest.of(page, size, sort)        if (searchText == "") {            return searchKeyWordRepository.findAll(pageable)

} else {            return searchKeyWordRepository.search(searchText, pageable)

}

}

前端列表页面代码

search_keyword_view.ftl 模板页面代码如下

id="add_key_word_form_keyWord"

type="text"

class="form-control"

placeholder="输入爬虫抓取关键字">

class="btn btn-default"

type="button">

保存                        

search_keyword_table.js 代码如下$(function () {

$.extend($.fn.bootstrapTable.defaults, $.fn.bootstrapTable.locales['zh-CN'])    var searchText = $('.search').find('input').val()    var columns = []

columns.push(

{            title: 'ID',            field: 'id',            align: 'center',            valign: 'middle',            width: '10%',            formatter: function (value, row, index) {                return value

}

},

{            title: '关键字',            field: 'keyWord',            align: 'center',            valign: 'middle',            formatter: function (value, row, index) {                var html = "" + value + ""

return html

}

},

{            title: '图片总数',            field: 'totalImage',            align: 'center',            valign: 'middle',            formatter: function (value, row, index) {                var html = "" + row.totalImage + ""

return html

}

})

$('#search_keyword_table').bootstrapTable({        url: 'searchKeyWordJson',        sidePagination: "server",        queryParamsType: 'page,size',        contentType: "application/x-www-form-urlencoded",        method: 'get',        striped: false,     //是否显示行间隔色

cache: false,      //是否使用缓存,默认为true,所以一般情况下需要设置一下这个属性(*)

pagination: true,  //是否显示分页(*)

paginationLoop: true,        paginationHAlign: 'right', //right, left

paginationVAlign: 'bottom', //bottom, top, both

paginationDetailHAlign: 'left', //right, left

paginationPreText: ' 上一页',        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', // 所有记录 count

dataField: 'content', //后端 json 对应的表格List数据的 key

columns: 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()

}

})

$('#add_key_word_form_save_button').on('click', function () {        var keyWord = $('#add_key_word_form_keyWord').val()

$.ajax({            url: 'save_keyword',            type: 'get',            data: {keyWord: keyWord},            success: function (response) {                if (response == "1") {

alert("保存成功")

} else {

alert("保存失败")

}

},            error: function (error) {

alert(JSON.stringify(error))

}

})

})

})

添加爬取关键字

添加爬取关键字 http 接口代码如下@RequestMapping(value = "save_keyword", method = arrayOf(RequestMethod.GET,RequestMethod.POST))@ResponseBodyfun save(@RequestParam(value = "keyWord")keyWord:String): String {    if(keyWord==""){        return "0"

}else{

searchKeyWordRepository.saveOnNoDuplicateKey(keyWord)        return "1"

}

}

前端输入框表单代码

    

id="add_key_word_form_keyWord"

type="text"

class="form-control"

placeholder="输入爬虫抓取关键字">

class="btn btn-default"

type="button">

保存                        

对应的 js 代码如下$('#add_key_word_form_save_button').on('click', function () {    var keyWord = $('#add_key_word_form_keyWord').val()

$.ajax({        url: 'save_keyword',        type: 'get',        data: {keyWord: keyWord},        success: function (response) {            if (response == "1") {

alert("保存成功")

$('#search_keyword_table').bootstrapTable('refresh')

} else {

alert("数据不能为空")

}

},        error: function (error) {

alert(JSON.stringify(error))

}

})

})

其中, $('#search_keyword_table').bootstrapTable('refresh') 是当保存成功后,刷新表格内容。

定时更新该关键字的图片总数任务

最终的效果如下

AAffA0nNPuCLAAAAAElFTkSuQmCC

爬取关键字管理页面

AAffA0nNPuCLAAAAAElFTkSuQmCC

模糊搜索“秋”

更新 search_key_word 表 total_image 字段的 SQL 逻辑如下@Modifying@Transactional@Query("update search_key_word a set a.total_image = (select count(*) from image i where i.is_deleted=0 and i.category like concat('%',a.key_word,'%'))", nativeQuery = true)fun batchUpdateTotalImage()

表示该对应关键字包含的图片总数。

然后,我们用一个定时任务去执行它package com.easy.kotlin.picturecrawler.jobimport com.easy.kotlin.picturecrawler.dao.SearchKeyWordRepositoryimport kotlinx.coroutines.experimental.CommonPoolimport kotlinx.coroutines.experimental.launchimport kotlinx.coroutines.experimental.runBlockingimport org.springframework.beans.factory.annotation.Autowiredimport org.springframework.scheduling.annotation.Scheduledimport org.springframework.stereotype.Componentimport java.util.*@Componentclass BatchUpdateJob {    @Autowired lateinit var searchKeyWordRepository: SearchKeyWordRepository    @Scheduled(cron = "0 */5 * * * ?")

fun job() {

println("开始执行定时任务 batchUpdateTotalImage: ${Date()}")

searchKeyWordRepository.batchUpdateTotalImage()

}

}

13.2.11  使用协程实现异步爬虫任务

上面我们的定时任务都是同步的。当我们想用 http 接口去触发任务执行的时候,可能并不想一直等待,这个时候可以使用异步的方式。这里我们使用 Kotlin 提供的轻量级线程——协程来实现。在常用的并发模型中,多进程、多线程、分布式是最普遍的,不过近些年来逐渐有一些语言以first-class或者library的形式提供对基于协程的并发模型的支持。其中比较典型的有Scheme、Lua、Python、Perl、Go等以first-class的方式提供对协程的支持。同样地,Kotlin也支持协程。(关于协程的更多介绍,可参考《Kotlin 极简教程》第9章 轻量级线程:协程 )

我们在 build.gradle 中添加kotlinx-coroutines-core 依赖compile group: 'org.jetbrains.kotlinx', name: 'kotlinx-coroutines-core', version: '0.19.2'

然后把我们的定时任务代码改写为@Componentclass BatchUpdateJob {    @Autowired lateinit var searchKeyWordRepository: SearchKeyWordRepository    @Scheduled(cron = "0 */5 * * * ?")

fun job() {

doBatchUpdate()

}    fun doBatchUpdate() = runBlocking {

launch(CommonPool) {

println("开始执行定时任务 batchUpdateTotalImage: ${Date()}")

searchKeyWordRepository.batchUpdateTotalImage()

}

}

}

同样的爬虫抓取图片的任务也可以改写成fun doCrawJob() = runBlocking {    val list = searchKeyWordRepository.findAll()    for (i in 1..1000) {

list.forEach {

launch(CommonPool) {

saveImage(it.keyWord, i)

}

}

}

}

其中,launch函数会以非阻塞(non-blocking)当前线程的方式,启动一个新的协程后台任务,并返回一个Job类型的对象作为当前协程的引用。我们把真正要执行的代码逻辑放到 launch(CommonPool) { } 中。这样我们就可以手动启动任务异步执行了。

13.2.12  图片存入数据库并在前端展现

数据库实体类: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

@Version

var 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 No

var deletedDate: Date = Date()    @Lob

var imageBlob: ByteArray = byteArrayOf()    /* 0-Baidu  1-Gank */

var sourceType: Int = 0

override fun toString(): String {        return "Image(id=$id, version=$version, category='$category', isFavorite=$isFavorite, url='$url', gmtCreated=$gmtCreated, gmtModified=$gmtModified, isDeleted=$isDeleted, deletedDate=$deletedDate)"

}

}

其中 @Lob  var imageBlob: ByteArray = byteArrayOf() 这个字段存储图片的 Base64内容。

图片比特流数组存入数据库代码val image = Image()

image.category = "干货集中营福利"image.url = url

image.sourceType = 1image.imageBlob = getByteArray(url)

logger.info("Image = ${Image}")

imageRepository.save(Image)

其中的getByteArray(url) 函数实现代码如下private fun getByteArray(url: String): ByteArray {        val urlObj = URL(url)        return urlObj.readBytes()

}

前端 html 展示图片代码:{

title: '图片',

field: 'imageBlob',

align: 'center',

valign: 'middle',

formatter: function (value, row, index) {

            // var html = ""

            var html = ''

return html

}

}

点击下载 js :function downloadImage(src) {    var $a = $("").attr("href", src).attr("download", "sotu.png");

$a[0].click();

}function downBase64Image(url) {    var blob = base64Img2Blob(url);

url = window.URL.createObjectURL(blob);    var $a = $("").attr("href", url).attr("download", "sotu.png");

$a[0].click();

}function base64Img2Blob(code) {    var parts = code.split(';base64,');    var contentType = parts[0].split(':')[1];    var raw = window.atob(parts[1]);    var rawLength = raw.length;    var uInt8Array = new Uint8Array(rawLength);    for (var i = 0; i 

uInt8Array[i] = raw.charCodeAt(i);

}    return new Blob([uInt8Array], {type: contentType});

}

13.2.13 IDEA 的数据库客户端工具

AAffA0nNPuCLAAAAAElFTkSuQmCC

IDEA 的数据库客户端工具

本章小结

在Spring Framework 5.0中已经添加了对 Kotlin 的支持。使用 Kotlin 集成 SpringBoot 开发非常流畅自然,几乎不需要任何迁移成本。所以,Kotlin 在未来的 Java 服务端领域也必将受到越来越多的程序员的关注。

Kotlin 开发者社区

国内第一Kotlin 开发者社区公众号,主要分享、交流 Kotlin 编程语言、Spring Boot、Android、React.js/Node.js、函数式编程、编程思想等相关主题。

作者:一个会写诗的程序员

链接:https://www.jianshu.com/p/9a6ea6201baa

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值