6.3 Spring Boot集成mongodb开发

来源:互联网 发布:cf刷枪软件免费版 编辑:程序博客网 时间:2024/06/05 21:35

6.3 Spring Boot集成mongodb开发

本章我们通过SpringBoot集成mongodb,Java,Kotlin开发一个极简社区文章博客系统。

0 mongodb简介


Mongo 的主要目标是在键/值存储方式(提供了高性能和高度伸缩性)和传统的RDBMS 系统(具有丰富的功能)之间架起一座桥梁,它集两者的优势于一身。Mongo 的BSON 数据格式非常适合文档化格式的存储及查询。[1]

关于nosql和rdbms的对比以及选择,我参考了不少资料,关键一点在于:nosql可以轻易扩展表的列,对于业务快速变化的应用场景非常适合;rdbms则需要安装关系型数据库模式对业务进行建模,适合业务场景已经成熟的系统。我目前的这个项目——dailyReport,我暂时没法确定的是,对于一个report,它的属性应该有哪些:date、title、content、address、images等等,基于此我选择mongodb作为该项目的持久化存储。

1 系统基本功能

1.支持markdown编辑器
2.写文章,编辑文章,阅读文章基础博客功能
3.文章列表排序,搜索

2 系统技术框架

开发环境:

MacOS Sierra
IDEA 2017.1
JDK 1.8.0_40
mongod-3.2.4
Gradle 3.5-rc-2

后端:

开发语言:Java 混合Kotlin语言开发

开发框架:

kotlin,Version = '1.1.0'
SpringBoot,Version = '1.5.2.RELEASE'
Spring-data-mongodb

前端:

JavaScript、html、css
requirejs
jquery
bootstrap
dataTables
meditor

3 系统架构设计

领域模型


Blog

前后端分层


后端Controller

jsp

js

4 数据库环境配置

1.build.gradle配置

添加mongodb starter

compile('org.springframework.boot:spring-boot-starter-data-jpa')compile('org.springframework.boot:spring-boot-starter-data-mongodb')

添加mongo-java-driver

compile('org.mongodb:mongo-java-driver:3.4.2')

完整配置如下:

group = 'com.restfeel'version = '0.0.1-SNAPSHOT'description = ""apply {    plugin "kotlin"    plugin "kotlin-spring"    plugin "kotlin-jpa"    plugin "org.springframework.boot"    plugin 'java'    plugin 'eclipse'    plugin 'idea'    plugin 'war'    plugin 'maven'}sourceCompatibility = 1.8targetCompatibility = 1.8sourceSets {    main {        kotlin { srcDir "src/main/kotlin" }        java { srcDir "src/main/java" }    }    test {        kotlin { srcDir "src/test/kotlin" }        java { srcDir "src/test/java" }    }}jar {    baseName = 'restfeel'    version = '0.0.1'}buildscript {    ext {        kotlinVersion = '1.1.0'        springBootVersion = '1.5.2.RELEASE'    }    dependencies {        classpath "org.springframework.boot:spring-boot-gradle-plugin:$springBootVersion"        classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion"        classpath "org.jetbrains.kotlin:kotlin-noarg:$kotlinVersion"        classpath "org.jetbrains.kotlin:kotlin-allopen:$kotlinVersion"    }    repositories {        mavenLocal()        mavenCentral()        maven { url "http://oss.jfrog.org/artifactory/oss-release-local" }        maven { url "http://jaspersoft.artifactoryonline.com/jaspersoft/jaspersoft-repo/" }        maven { url "https://oss.sonatype.org/content/repositories/snapshots" }    }}dependencies {    compile("org.jetbrains.kotlin:kotlin-stdlib:$kotlinVersion")    compile("org.jetbrains.kotlin:kotlin-reflect:$kotlinVersion")    compile("com.fasterxml.jackson.module:jackson-module-kotlin:2.8.4")    compile('org.springframework.boot:spring-boot-starter')    compile('org.springframework.boot:spring-boot-starter-data-jpa')    compile('org.springframework.boot:spring-boot-starter-data-mongodb')    compile('org.springframework.boot:spring-boot-starter-actuator')    compile('org.springframework.boot:spring-boot-starter-web')    compile('org.springframework.boot:spring-boot-starter-security')    compile('org.springframework.boot:spring-boot-starter-remote-shell')    compile('org.springframework.boot:spring-boot-starter-aop')    providedCompile('org.springframework.boot:spring-boot-starter-tomcat')    compile('javax.servlet:jstl')    providedCompile('org.apache.tomcat.embed:tomcat-embed-jasper')    //thymeleaf//    compile("org.springframework.boot:spring-boot-starter-thymeleaf")    compile('org.hibernate:hibernate-validator:5.1.3.Final')    compile('org.mongodb:mongo-java-driver:3.4.2')    compile('org.hsqldb:hsqldb:2.3.2')    compile('org.apache.httpcomponents:httpclient:4.5.1')    compile('org.apache.httpcomponents:httpmime:4.5.1')    compile('org.apache.commons:commons-lang3:3.3.2')    compile('com.sendgrid:sendgrid-java:2.1.0')    compile('com.ryantenney.metrics:metrics-spring:3.0.0')    compile('net.sf.jasperreports:jasperreports:6.0.0') {        exclude module: 'jdtcore'        exclude module: 'jackson-annotations'    }    compile('com.mangofactory:swagger-springmvc:0.9.4')    compile('org.ajar:swagger-spring-mvc-ui:0.4')    compile('com.google.oauth-client:google-oauth-client:1.19.0')    compile('com.jayway.jsonpath:json-path:2.0.0')    compile('io.swagger:swagger-compat-spec-parser:1.0.12')    compile('org.raml:raml-parser:0.8.12') {        exclude module: 'slf4j-log4j12'        exclude module: 'log4j'    }    testCompile('org.springframework.boot:spring-boot-starter-test')    // https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-devtools    compile group: 'org.springframework.boot', name: 'spring-boot-devtools'}compileJava {    //options.fork = true    options.incremental = true}repositories {    mavenLocal()    mavenCentral()    maven { url "http://oss.jfrog.org/artifactory/oss-release-local" }    maven { url "http://jaspersoft.artifactoryonline.com/jaspersoft/jaspersoft-repo/" }    maven { url "https://oss.sonatype.org/content/repositories/snapshots" }}
2.实现AbstractMongoConfiguration类
package com.restfeel.configimport com.mongodb.Mongoimport com.mongodb.MongoClientimport com.mongodb.MongoCredentialimport com.mongodb.ServerAddressimport org.springframework.beans.factory.annotation.Autowiredimport org.springframework.context.annotation.Beanimport org.springframework.context.annotation.Configurationimport org.springframework.core.env.Environmentimport org.springframework.data.mongodb.config.AbstractMongoConfigurationimport org.springframework.data.mongodb.repository.config.EnableMongoRepositories/** * Created by jack on 2017/3/29. */@Configuration@EnableMongoRepositories(*arrayOf("com.restfeel.dao", "com.restfeel.service"))class PersistenceConfig : AbstractMongoConfiguration() {    @Autowired    private val env: Environment? = null    override fun getDatabaseName(): String {        return env!!.getProperty("mongodb.name")    }    @Bean    @Throws(Exception::class)    override fun mongo(): Mongo {        return MongoClient(listOf(ServerAddress(env!!.getProperty("mongodb.host"), env!!.getProperty("mongodb.port", Int::class.java))),                listOf(MongoCredential                        .createCredential(env!!.getProperty("mongodb.username"), env!!.getProperty("mongodb.name"),                                env!!.getProperty("mongodb.password").toCharArray())))    }//    override fun getMappingBasePackage(): String {//        return "com.restfiddle.dao"//    }    /**     * 这地方是配置扫描继承Repository类的所有接口类的路径的,路径配置错误,bean就不会创建了。     * 东海陈光剑 Jason Chen @蒋村花园如意苑 2017.3.30 01:41:35     */    override fun getMappingBasePackages(): Collection<String> {        return setOf("com.restfeel.dao", "com.restfeel.service")    }}

5 定义领域对象

领域模型类
package com.restfeel.entityimport org.bson.types.ObjectIdimport org.springframework.data.mongodb.core.mapping.Documentimport java.util.*import javax.persistence.GeneratedValueimport javax.persistence.GenerationTypeimport javax.persistence.Idimport javax.persistence.Version@Document(collection = "blog") // 如果不指定collection,默认遵从命名规则class Blog {    @Id    @GeneratedValue(strategy = GenerationType.AUTO)    var id: String = ObjectId.get().toString()    @Version    var version: Long = 0    var title: String = ""    var content: String = ""    var author: String = ""    var gmtCreated: Date = Date()    var gmtModified: Date = Date()    var isDeleted: Int = 0 //1 Yes 0 No    var deletedDate: Date = Date()    override fun toString(): String {        return "Blog(id='$id', version=$version, title='$title', content='$content', author='$author', gmtCreated=$gmtCreated, gmtModified=$gmtModified, isDeleted=$isDeleted, deletedDate=$deletedDate)"    }}

6 核心业务逻辑实现

BlogService代码:
package com.restfeel.serviceimport com.restfeel.entity.Blogimport org.springframework.data.mongodb.repository.MongoRepositoryimport org.springframework.data.mongodb.repository.Queryimport org.springframework.data.repository.query.Paraminterface BlogService : MongoRepository<Blog, String> {    @Query("{ 'title' : ?0 }")    fun findByTitle(@Param("title") title: String): Iterable<Blog>}

这里是精确匹配查询。我们一般在实际应用场景中会使用模糊查询。我们简单讲讲mongo的模糊查询。

LIKE模糊查询title包含A字母的数据(%A%)

SQL:

SELECT * FROM Blog WHERE title LIKE "%A%"

MongoDB:

db.Blog.find({title :/A/})

这是mongo里面的正则表达式。等同于

db.Blog.find({title :{$regex:"A"}})

LIKE模糊查询title以字母A开头的数据(A%)

SQL:

SELECT * FROM Blog WHERE title LIKE "A%"

MongoDB:

db.Blog.find({title :/^A/})

如果我们使用org.springframework.data.mongodb.repository.Query,不能直接这么写:{title :/^A/}。我们需要使用regex表达式来写。代码示例如下:

package com.restfeel.serviceimport com.restfeel.entity.Blogimport org.springframework.data.mongodb.repository.MongoRepositoryimport org.springframework.data.mongodb.repository.Queryimport org.springframework.data.repository.query.Paraminterface BlogService : MongoRepository<Blog, String> {//    @Query(value = "{ 'title' : ?0}")    @Query(value = "{ 'title' : {\$regex: ?0, \$options: 'i'}}")    fun findByTitle(@Param("title") title: String): Iterable<Blog>}

我们这里设置 $options 为 $i,意思是检索不区分大小写。

BlogController代码:
package com.restfeel.controllerimport com.restfeel.entity.Blogimport com.restfeel.service.BlogServiceimport org.springframework.boot.autoconfigure.EnableAutoConfigurationimport org.springframework.context.annotation.ComponentScanimport org.springframework.security.core.context.SecurityContextHolderimport org.springframework.security.core.userdetails.UserDetailsimport org.springframework.stereotype.Controllerimport org.springframework.transaction.annotation.Propagationimport org.springframework.transaction.annotation.Transactionalimport org.springframework.ui.Modelimport org.springframework.web.bind.annotation.GetMappingimport org.springframework.web.bind.annotation.PostMappingimport org.springframework.web.bind.annotation.RequestParamimport org.springframework.web.bind.annotation.ResponseBodyimport java.util.*import javax.servlet.http.HttpServletRequest/** * 文章列表,写文章的Controller * @author Jason Chen  2017/3/31 01:10:16 */@Controller@EnableAutoConfiguration@ComponentScan@Transactional(propagation = Propagation.REQUIRES_NEW)class BlogController(val blogService: BlogService) {    @GetMapping("/blogs.do")    fun listAll(model: Model): String {        val authentication = SecurityContextHolder.getContext().authentication        model.addAttribute("currentUser", if (authentication == null) null else authentication.principal as UserDetails)        val allblogs = blogService.findAll()        model.addAttribute("blogs", allblogs)        return "jsp/blog/list"    }    @PostMapping("/saveBlog")    @ResponseBody    fun saveBlog(blog: Blog, request: HttpServletRequest):Blog {        blog.author = (request.getSession().getAttribute("currentUser") as UserDetails).username        return blogService.save(blog)    }    @GetMapping("/goEditBlog")    fun goEditBlog(@RequestParam(value = "id") id: String, model: Model): String {        model.addAttribute("blog", blogService.findOne(id))        return "jsp/blog/edit"    }    @PostMapping("/editBlog")    @ResponseBody    fun editBlog(blog: Blog, request: HttpServletRequest) :Blog{        blog.author = (request.getSession().getAttribute("currentUser") as UserDetails).username        blog.gmtModified = Date()        blog.version = blog.version + 1        return blogService.save(blog)    }    @GetMapping("/blog")    fun blogDetail(@RequestParam(value = "id") id: String, model: Model): String {        model.addAttribute("blog", blogService.findOne(id))        return "jsp/blog/detail"    }    @GetMapping("/listblogs")    @ResponseBody    fun listblogs(model: Model) = blogService.findAll()    @GetMapping("/findBlogByTitle")    @ResponseBody    fun findBlogByTitle(@RequestParam(value = "title") title: String) = blogService.findByTitle(title)}

7 前端jsp设计

list.jsp

<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8" %><%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %><%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %><%@ taglib prefix="fn" uri="http://java.sun.com/jsp/jstl/functions" %><!DOCTYPE html><html lang="en"><head>    <jsp:include page="../header.jsp"></jsp:include></head><body><jsp:include page="../top-nav.jsp"></jsp:include><div class="col-sm-12">    <h2>文章列表</h2>    <div class="pull-right">        <a href="addBlog" class="btn btn-primary write-btn" target="_blank">写文章</a>    </div>    <table id="blogsTable" class="table table-hover">        <thead>        <tr>            <th>No</th>            <th>Title</th>            <th>Author</th>            <%--<th>Content</th>--%>            <th>CreateTime</th>        </tr>        </thead>        <tbody>        <c:forEach items="${blogs}" var="blog" varStatus="status">            <tr>                <td>${status.index+1}</td>                <td><a href="blog?id=${blog.id}" target="_blank">${blog.title}</a></td>                <td>${blog.author}</td>                    <%--<td>${fn: substring(blog.content,0,100)}</td>--%>                <td>${blog.gmtCreated}</td>            </tr>        </c:forEach>        </tbody>    </table></div><jsp:include page="../copyright.jsp"></jsp:include><script data-main="js/views/blog/config" src="js/libs/require/require.js"></script><script type="text/javascript">    require(['blog-list-view']);</script></body></html>

add.jsp

<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8" %><%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %><%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %><!DOCTYPE html><html lang="en"><head>    <jsp:include page="../header.jsp"></jsp:include></head><body><jsp:include page="../top-nav.jsp"></jsp:include><div class="col-sm-10 blog">    <h2>写文章</h2>    <form id="addBlogForm" class="form-horizontal">        <div class="form-group-lg">            <label></label>            <input type="text" name="title" class="form-control" placeholder="文章标题">        </div>        <div class="form-group-lg">            <label></label>            <textarea id="blogContentEditor" type="text" name="content" class="form-control" rows="100"                      placeholder=""></textarea>        </div>        <div class="form-group-lg">            <div class="col-sm-offset-2 col-sm-10">                <button type="submit" class="btn btn-primary rest-blog-submit-btn" id="addBlogBtn">保存并发表</button>            </div>        </div>    </form></div><jsp:include page="../copyright.jsp"></jsp:include><script data-main="js/views/blog/config" src="js/libs/require/require.js"></script><script type="text/javascript">    require(['blog-add-view']);</script></body></html>

detail.jsp

<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8" %><%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %><%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %><!DOCTYPE html><html lang="en"><head>    <jsp:include page="../header.jsp"></jsp:include></head><body><jsp:include page="../top-nav.jsp"></jsp:include><div class="container-fluid">    <div class="col-sm-10 blog">        <h1 class="center">${blog.title}</h1>        <input type="hidden" id="blogId" value="${blog.id}">        <div id="goEditBlog" class="btn-link pull-right">编辑</div>        <div class="rest-center">            作者: ${blog.author}            日期: <fmt:formatDate pattern="yyyy/MM/dd HH:mm:ss" value="${blog.gmtModified}"/>        </div>        <textarea id="blogContent" style="display: none"><c:out value="${blog.content}"                                                                escapeXml='false'></c:out></textarea>        <%--<textarea id="blogContent" rows="50" cols="150"><c:out value="${blog.content}" escapeXml='false'></c:out></textarea>--%>        <div class="markdown-body rest-blog-body"></div>    </div></div><jsp:include page="../copyright.jsp"></jsp:include><script data-main="js/views/blog/config" src="js/libs/require/require.js"></script><script type="text/javascript">    require(['blog-detail-view']);</script></body></html>

edit.jsp

<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8" %><%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %><%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %><!DOCTYPE html><html lang="en"><head>    <jsp:include page="../header.jsp"></jsp:include></head><body><jsp:include page="../top-nav.jsp"></jsp:include><div class="col-sm-10 blog">    <h2>写文章</h2>    <form id="editBlogForm" class="form-horizontal">        <input type="hidden" name="id" value="${blog.id}">        <div class="form-group-lg">            <label></label>            <input type="text" name="title" class="form-control" placeholder="文章标题" value="${blog.title}">        </div>        <div class="form-group-lg">            <label></label>            <textarea id="blogContentEditor" type="text" name="content" class="form-control" rows="100"                      placeholder=""><c:out value="${blog.content}" escapeXml='false'></c:out></textarea>        </div>        <div class="form-group-lg">            <div class="col-sm-offset-2 col-sm-10">                <button type="submit" class="btn btn-primary rest-blog-submit-btn" id="editBlogBtn">保存并发表</button>            </div>        </div>    </form></div><jsp:include page="../copyright.jsp"></jsp:include><script data-main="js/views/blog/config" src="js/libs/require/require.js"></script><script type="text/javascript">    require(['blog-edit-view']);</script></body></html>

8 前端js代码

我们采用requirejs管理js。js代码跟html代码隔离。
config.js

/** * 入口文件config.js。它一般用来对requirejs进行配置,并且载入真正的程序模块。 */require.config({    baseUrl: '/js',    paths: {        jquery: 'libs/jquery-2.1.4.min',        jqueryUi: 'libs/jquery-ui.min',        bootstarp: 'libs/bootstrap.min',        datatables: 'plugin/datatables/jquery.dataTables',        jsonview: 'plugin/jsonview/jquery.jsonview',        bootstrapDialog: 'plugin/bootstrap-dialog/bootstrap-dialog',        meditor: 'plugin/mditor-master/dist/js/mditor.min',    },    shim: {        'jqueryUi': {            deps: ['jquery']        },        'bootstarp': {            deps: ['jquery', 'jqueryUi']        },        'datatables': {            deps: ['jquery']        },        'jsonview': {            deps: ['jquery']        },        'bootstrapDialog': {            deps: ['jquery']        },        'meditor': {            deps: ['jquery']        }    }});

blog-add-view.js

/** * Created by jack on 2017/3/29. */define(function (require) {    "use strict";    require('meditor');    jQuery(function () {        //meditor        var mditor = Mditor.fromTextarea(document.getElementById('blogContentEditor'));        //是否打开分屏        mditor.split = true;    //打开        //是否打开预览        mditor.preivew = true;    //打开        //是否全屏        mditor.fullscreen = false;    //关闭        //获取或设置编辑器的值        mditor.on('ready', function () {            mditor.value = '#Restfeel';        });        //写文章        jQuery("#addBlogBtn").on("click", function () {            jQuery.ajax({                url: 'saveBlog',                type: 'POST',                data: $('#addBlogForm').serialize(),                async: false,                success: function (data) {                    if (data) {                        alert('保存成功');                        // location.href = 'blogs.do';                        window.opener = null;                        window.open('', '_self');                        window.close();                    } else {                        alert(data);                    }                },                error: function (data) {                    alert(data);                }            });        });    });});

blog-detail-view.js

/** * Created by jack on 2017/3/29. */define(function (require) {    "use strict";    require('meditor');    $(function () {        var parser = new Mditor.Parser();        // var blogContent = document.getElementById('blogContent').innerHTML;//这个遇到<>等特殊字符会被转译        var blogContent = document.getElementById('blogContent').value; //直接取原本的字符串。不会被转译        var html = parser.parse(blogContent);        $('.markdown-body').append(html);        //编辑文章        $('#goEditBlog').on('click',function () {            var blogId = $('#blogId').val();            location.href = 'goEditBlog?id=' + blogId;        });        //源码高亮        hljs.initHighlightingOnLoad();    });});

blog-edit-view.js

/** * Created by jack on 2017/3/29. */define(function (require) {    "use strict";    require('meditor');    jQuery(function () {        //meditor        var mditor = Mditor.fromTextarea(document.getElementById('blogContentEditor'));        //是否打开分屏        mditor.split = true;    //打开        //是否打开预览        mditor.preivew = true;    //打开        //是否全屏        mditor.fullscreen = false;    //关闭        //写文章        jQuery("#editBlogBtn").on("click", function () {            jQuery.ajax({                type: 'POST',                url: 'editBlog',                data: jQuery('#editBlogForm').serialize(),                //dataType: 'json',                async: false,                //在请求之前调用的函数                beforeSend: function () {                },                success: function (data) {                    if (data) {                        alert('保存成功');                        history.go(-1);                    } else {                        alert(data);                    }                },                //调用执行后调用的函数                complete: function (XMLHttpRequest, textStatus) {                },                error: function (data) {                    alert(data);                }            });        });    });});

blog-list-view.js

/** * Created by jack on 2017/3/29. */define(function (require) {    "use strict";    require('datatables');    $(function () {        // 文章列表        var aLengthMenu = [10, 20, 50, 100, 200];        var dataTableOptions = {            bDestroy: true,            paging: true,            lengthChange: true,            searching: true,            ordering: true,            order: [3, "desc"],            autoWidth: true,            processing: true,            stateSave: true,            responsive: true,            fixedHeader: false,            aLengthMenu: aLengthMenu,            language: {                search: "<div style='border-radius:10px;margin-left:auto;margin-right:2px;width:760px;'>_INPUT_ &nbsp;<span class='btn btn-primary'>搜索</span></div>",                paginate: {//分页的样式内容                    previous: "上一页",                    next: "下一页",                    first: "第一页",                    last: "最后"                }            },            zeroRecords: "没有内容",//table tbody内容为空时,tbody的内容。            //下面三者构成了总体的左下角的内容。            info: "总计 _TOTAL_ 条,共 _PAGES_ 页,_START_ - _END_ ",//左下角的信息显示,大写的词为关键字。            infoEmpty: "0条记录",//筛选为空时左下角的显示。            infoFiltered: ""//筛选之后的左下角筛选提示        }        $('#blogsTable').dataTable(dataTableOptions)    });});

9 运行效果

直接使用IDEA gradle插件,点击bootRun


我们先在Swagger里面测试一下模糊查询接口findBlogByTitle

http://127.0.0.1:5678/findBlogByTitle?title=Spring



分别测试写文章,文章列表,阅读文章页面:


写文章

阅读文章

文章列表

系统源代码

详见工程:
https://github.com/Jason-Chen-2017/restfeel

小结

我们采用SpringBoot集成mongodb,Java,Kotlin,jsp,jquery,bootstrap,requirejs等技术框架,架构层次分明,快速开发出了一个极简的社区文章博客系统。

参考资料:
1.http://baike.baidu.com/item/mongodb