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

6.3 Spring Boot集成mongodb开发

www说
关注TA
已关注
手记 302
粉丝 83
获赞 493

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.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_ &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



作者:陈光剑
链接:https://www.jianshu.com/p/28f73ce34fb2

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