继续浏览精彩内容
慕课网APP
程序员的梦工厂
打开
继续
感谢您的支持,我会继续努力的
赞赏金额会直接到老师账户
将二维码发送给自己后长按识别
微信支付
支付宝支付

第13章 Kotlin 集成 SpringBoot 服务端开发(2)

慕码人8056858
关注TA
已关注
手记 1247
粉丝 350
获赞 1323

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<String>) {
    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<SearchKeyWord>

模糊搜索关键字接口如下

@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<SearchKeyWord>

模糊搜索 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<SearchKeyWord> {        return getPageResult(page, size, searchText)
    }    private fun getPageResult(page: Int, size: Int, searchText: String): Page<SearchKeyWord> {
        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 模板页面代码如下

<#include 'common/head.ftl'><#include 'common/nav.ftl'><form id="add_key_word_form">
    <div class="col-lg-3">
        <div class="input-group">
            <input name="keyWord"
                   id="add_key_word_form_keyWord"
                   type="text"
                   class="form-control"
                   placeholder="输入爬虫抓取关键字">
            <span class="input-group-btn">
                        <button id="add_key_word_form_save_button"
                                class="btn btn-default"
                                type="button">
                             保存                        </button>
            </span>
        </div><!-- /input-group -->
    </div><!-- /.col-lg-3 --></form><table id="search_keyword_table"></table><#include 'common/foot.ftl'><script src="search_keyword_table.js"></script>

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 = "<a href='sotu_view?keyWord=" + value + "' target='_blank'>" + value + "</a>"
                return html
            }
        },
        {            title: '图片总数',            field: 'totalImage',            align: 'center',            valign: 'middle',            formatter: function (value, row, index) {                var html = "<a href='sotu_view?keyWord=" + row.keyWord + "' target='_blank'>" + row.totalImage + "</a>"
                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"
    }
}

前端输入框表单代码

<form id="add_key_word_form">    <div class="col-lg-3">
        <div class="input-group">
            <input name="keyWord"
                   id="add_key_word_form_keyWord"
                   type="text"
                   class="form-control"
                   placeholder="输入爬虫抓取关键字">
            <span class="input-group-btn">
                        <button id="add_key_word_form_save_button"
                                class="btn btn-default"
                                type="button">
                             保存                        </button>
            </span>
        </div><!-- /input-group -->
    </div><!-- /.col-lg-3 --></form>

对应的 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') 是当保存成功后,刷新表格内容。

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

最终的效果如下

webp

爬取关键字管理页面

webp

模糊搜索“秋”

更新 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 = "<img onclick=downloadImage('" + value + "') width='100%' src='" + value + "'>"
            var html = '<img onclick="downBase64Image(this.src)" width="100%" src="data:image/jpg;base64,' + value + '"/>'
            return html
        }
    }

点击下载 js :

function downloadImage(src) {    var $a = $("<a></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 = $("<a></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 < rawLength; ++i) {
        uInt8Array[i] = raw.charCodeAt(i);
    }    return new Blob([uInt8Array], {type: contentType});
}

13.2.13 IDEA 的数据库客户端工具

webp

IDEA 的数据库客户端工具

本节完整的项目源码:https://github.com/EasySpringBoot/picture-crawler

本章小结

在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


打开App,阅读手记
0人推荐
发表评论
随时随地看视频慕课网APP