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等技术框架,架构层次分明,快速开发出了一个极简的社区文章博客系统。