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 系统架构设计
领域模型
前后端分层
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.8 targetCompatibility = 1.8 sourceSets { 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.config import com.mongodb.Mongo import com.mongodb.MongoClient import com.mongodb.MongoCredential import com.mongodb.ServerAddress import org.springframework.beans.factory.annotation.Autowired import org.springframework.context.annotation.Bean import org.springframework.context.annotation.Configuration import org.springframework.core.env.Environment import org.springframework.data.mongodb.config.AbstractMongoConfiguration import 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.entity import org.bson.types.ObjectId import org.springframework.data.mongodb.core.mapping.Document import java.util.* import javax.persistence.GeneratedValue import javax.persistence.GenerationType import javax.persistence.Id import 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.service import com.restfeel.entity.Blog import org.springframework.data.mongodb.repository.MongoRepository import org.springframework.data.mongodb.repository.Query import org.springframework.data.repository.query.Param interface 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.service import com.restfeel.entity.Blog import org.springframework.data.mongodb.repository.MongoRepository import org.springframework.data.mongodb.repository.Query import org.springframework.data.repository.query.Param interface 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.controller import com.restfeel.entity.Blog import com.restfeel.service.BlogService import org.springframework.boot.autoconfigure.EnableAutoConfiguration import org.springframework.context.annotation.ComponentScan import org.springframework.security.core.context.SecurityContextHolder import org.springframework.security.core.userdetails.UserDetails import org.springframework.stereotype.Controller import org.springframework.transaction.annotation.Propagation import org.springframework.transaction.annotation.Transactional import org.springframework.ui.Model import org.springframework.web.bind.annotation.GetMapping import org.springframework.web.bind.annotation.PostMapping import org.springframework.web.bind.annotation.RequestParam import org.springframework.web.bind.annotation.ResponseBody import 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_ <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
作者:陈光剑
链接:https://www.jianshu.com/p/28f73ce34fb2