微服务从零开始之留言板

目录

  • 概述
  • 需求分析
  • 领域对象设计
  • API 设计
  • Create Message
  • Request
  • Response
  • Retrieve Message
  • Update Message
  • Delete Message
  • Query Message
  • TDD - Test Driven Development
  • 测试方法 Test methods
  • 测试矩阵 Test Matrix
  • 留言板实现细节
  • 搭建骨架
  • 构建工具和插件
  • 我们需要哪些库
  • 日志库
  • 测试库
  • 框架及工具库
  • 度量相关库
  • 基本骨架
  • web.xml
  • 数据对象
  • 配置类
  • 数据库创建
  • MessageDb
    *留言板的主要实现
  • MessageContoller
  • MessageService
  • MessageDao
  • 参考

概述

以一个最简单的留言本为例, 麻雀虽小, 五脏俱全, 它基本上牵涉到了一个微服务的各个方面, 让我们看看如何从零开始,从无到有构建一个微服务

需求分析

让我们先从用户故事开始, 用例比较多, 可以分优先级分步实施

留言本看下来简单, 其实牵涉到 Web 开发的各个方面, 类似一个小微博

User story Priority
注册 B
登录 B
用户管理 B
访客留言 A
用户及访客评论 B
留言管理 C
评论管理 C

优先级可以参考时间管理的任务重要性划分

  • A 重要且紧急
  • B 重要不紧急
  • C 紧急不重要
  • D 不紧急不重要
Paste_Image.png

用户故事图脚本

[Guest]-(Post Message), 
[Guest]-(Query Message), 
[Guest]-(Sign Up), 
[User]-(Sign In), 
[User]-(Add Comments), 
[User]-(Update Message), 
[User]-(Query Message), 
[User]-(Update Self), 
[Admin]-(User Manage), 
[Admin]-(Manage Message), 
(Manage Message)>(Delete Message), 
(Manage Message)>(Delete Comment), 
(Manage Message)>(Archive Message), 
(User Manage)>(Approve Sign Up), 
(User Manage)>(CRUDQ User), 
(CRUDQ User)>(Reset Password), 
(CRUDQ User)>(Lock User)

领域对象设计

对于 guestbook 的最高优先级的用户故事是留言, 也就是 Post Message

让我们先从领域对象设计开始, 留言本的核心对象是 Message 和 Guest

Paste_Image.png

类图脚本如下

%2F%2F Cool Class Diagram, , [Message|id:String;title:String;content:String;tags:String;author: Author; createTime: timestamp], [Message]<>-[Author|id:String;name:String;email: Email;phoneNumber: PhoneNumber;createTime: timestamp]

对应于领域对象如下

  • 消息 Message
attribute type
id String(UUID)
title String
content String
author Author
tags String
createTime Timestamp
  • �留言者 Author
attribute type
id String(uuid)
name String
email String
phoneNumber String
createTime Timestamp

API 设计

对象的基本操作是典型的 CRUDQ, 即创建 Create, 获取 Retrieve, 修改 Update, 删除 Delete 和查询 Query

Operation API
Create Message POST /messages
Retrieve Message GET/messages/$id
Update Message PUT /messages/$id
Delete Message DELETE/messages/$id
Query Message GET /messages?$parameters

Create Message

POST /api/v1/messages

Request

{
"�title" : "String",
"content": "String",
"author": {
    "name" : "String",
    "email" : "Email",
    "phoneNumber": "PhoneNumber"
},
"tags" :  "String",
}

Response

{
"url": "http://guestbook.com/api/vi/messages/$id"
"title" : "String",
"content": "String",
"author": {
    "name" : "String",
    "email" : "Email",
    "phoneNumber": "PhoneNumber"
},
"tags" :  "String",
}

Retrieve Message

  • GET /api/v1/messages/:id

Update Message

  • PUT /api/v1/messages/:id

Delete Message

  • DELETE /api/v1/messages/:id

Query Message

  • GET /api/v1/messages

| Parameter | Type | Default | Comments |
|:----------|------|-----------|---------|---------|
| start | integer | 0 | start number |
| limit | integer | 20 | message count |
| order | string | asc | asc or desc |
| sortby | string | title | id, title, author, email, createtime |
| field | string | * | title, content, author name or email |
| keyword | string | n/a | |

TDD - Test Driven Development

测试驱动开发已经深入人心, 从下到上, 从单元测试到集成测试, 这些是质量的保证
一般来说, 测试的行覆盖率起码要在 80% 以上

测试金字塔我们都有所耳闻,case多了,速度慢了,逻辑越复杂,测试越脆弱,测试集的归类,统计很重要

Paste_Image.png

测试方法 Test methods

  • 单元测试 Unit Test: TestNG, Mockito, SpringTesting
  • 接收测试 API Test: HttpClient
  • 端到端集成测试 E2E Test: Selenium

测试矩阵 Test Matrix

Test Case Category Comments
消息创建 Message:Create UT,API
消息修改 Message:Update UT,API,E2E
消息删除 Message:Delete UT, API
消息简单查询 Message:Retrieve UT
消息复杂查询 Message:Query UT, API 分页, 排序,根据关键字查询

好了, 到此为止, 咱们已经搞清楚需求和领域对象了, 可以动手开始编程了

且慢, 想想这是不是就够了, 做了就要上线, 上线之后我们最关心什么

关注点 度量
功能是否正常完备 Function Metrics
用量如何 Usage Metrics
性能如何 Performance Metrics
有无异常 Exception Metrics
有无遭受攻击 Fraud attack Metrics
出现问题的修复时间 Fileover/Recover Metrics

让我们在编码实现的时候, 把这些记在心头.

留言板实现细节

以大家比较熟悉的 Java Web App 为例

技术选型如下

  • 前端框架: AngularJS
  • 后端框架: SpringMVC
  • 数据库: SQLite

搭建骨架

mvn archetype:generate -DgroupId=com.github.walterfan -DartifactId=guestbook  -DarchetypeArtifactId=maven-archetype-webapp -DinteractiveMode=false

这样 maven 就为你创建了一个 Java Web App 的骨架

guestbook//pom.xml
guestbook//src
guestbook//src/main
guestbook//src/main/resources
guestbook//src/main/webapp
guestbook//src/main/webapp/index.jsp
guestbook//src/main/webapp/WEB-INF
guestbook//src/main/webapp/WEB-INF/web.xml

Java Web App 的开发框架汗牛充栋, 比如 Struts2, Spring MVC, 还有最近比较流行的 DropWizard 和 Spring Boot

这两个框架都是众多开源项目的集大成者, 先不用这么重的东西来做留言本
这里就用Spring Boot 的核心项目 Spring MVC 来快速实现

具体实现下节细说, 这里讲几句题外话, 很多Java 开发者都有"好读书, 不求甚解"的坏毛病, 包括我在内, 从 C/C++ 世界转过来, 发现 Java 太方便了, 开发效率极高, 各种库和框架让人目不暇接, 很容易就迷失了

有时间还是可以看一看 HTTP 协议 和 Servlet JSR(Java Specification Requests)
最近一版是 JSR 340: Java Servlet 3.1 Specification

归根到底, 它是一个网络应用程序, 程序启动时侦听 80 或其他端口, 接收 HTTP Request, 解包交应用逻辑进行一些处理后以 HTTP Response 的编码返回.

只不过, 现在通过Servlet 容器把这些底层的脏活累活干了, 交到 Application 手里已经是标准的 HttpRequest 和 HttpResponse

构建工具和插件

Maven 是Java世界的标配, 近年来gradel 异军突起, 有待时间的检验

参见详情: https://github.com/walterfan/guestbook/blob/master/pom.xml

Maven 的插件也是林林总总, 不胜枚举, 这里只选用一些常用的

  • maven-surefire-plugin for uni test
  • Jacoco-maven-plugin for test coverage
  • maven-failsafe-plugin for integration test

我们需要哪些库

日志库

  • sl4j
  • logback

测试库

  • testng
  • mockito
  • spring test

框架及工具库

  • Spring MVC
  • Jackson
  • guava
  • commons lang3, io,

度量相关库

基本骨架

Spring MVC 原理回顾, DispatchServlet 是其核心

  • Controller
  • Service
  • Domain
  • Dao
  • Metrics: 度量相关代码

web.xml

<?xml version="1.0" ?>
<web-app xmlns="http://java.sun.com/xml/ns/javaee"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd"
         version="3.0">

<description>Micro Service</description>

<context-param>
  <param-name>contextClass</param-name>
  <param-value>org.springframework.web.context.support.AnnotationConfigWebApplicationContext</param-value>
</context-param>

<context-param>
  <param-name>contextConfigLocation</param-name>
  <param-value>com.github.walterfan.guestbook.MessageConfig</param-value>
</context-param>

<listener>
  <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>

<servlet>
  <servlet-name>mvc-servlet</servlet-name>
  <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
  <init-param>
    <param-name>contextConfigLocation</param-name>
    <param-value></param-value>
  </init-param>
  <load-on-startup>1</load-on-startup>
</servlet>

<servlet-mapping>
  <servlet-name>mvc-servlet</servlet-name>
  <url-pattern>/*</url-pattern>
</servlet-mapping>

</web-app>

数据对象

    1. class Message
package com.github.walterfan.guestbook.domain;


import org.hibernate.validator.constraints.NotBlank;

import javax.validation.constraints.NotNull;
import java.util.Date;


public class Message extends BaseObject {
    private String id;
    @NotBlank
    private String title;
    @NotBlank
    private String content;
    @NotNull
    private Author author;
    private String tags;
    private Date createTime;

    public String getId() {
        return id;
    }

    public void setId(String id) {
        this.id = id;
    }

    public String getTitle() {
        return title;
    }

    public void setTitle(String title) {
        this.title = title;
    }

    public String getContent() {
        return content;
    }

    public void setContent(String content) {
        this.content = content;
    }

    public Author getAuthor() {
        return author;
    }

    public void setAuthor(Author author) {
        this.author = author;
    }

    public String getTags() {
        return tags;
    }

    public void setTags(String tags) {
        this.tags = tags;
    }

    public Date getCreateTime() {
        return createTime;
    }

    public void setCreateTime(Date createTime) {
        this.createTime = createTime;
    }
}

配置类

  • 代替之前的spring bean xml 配置文件
    1. class MessageConfig
    
package com.github.walterfan.guestbook;

import com.github.walterfan.guestbook.controller.IndexController;
import com.github.walterfan.guestbook.controller.MessageController;
import com.github.walterfan.guestbook.dao.MessageDao;
import com.github.walterfan.guestbook.dao.MessageMapper;
import com.github.walterfan.guestbook.service.MessageService;
import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.mybatis.spring.SqlSessionTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.core.env.Environment;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.jdbc.datasource.SimpleDriverDataSource;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;

import javax.sql.DataSource;
import java.sql.Driver;

/**
 * Created by walter on 06/11/2016.
 */
@Configuration
@EnableWebMvc
@Import({
        IndexController.class,
        MessageController.class
})
public class MessageConfig {

    @Autowired
    private Environment env;

    @Bean
    public MessageService messageService() {
        return new MessageService();
    }

    @Bean
    public MessageProperties messageProperties()  {
        return new MessageProperties();
    }


    @Bean
    public DataSource dataSource() throws ClassNotFoundException {
        SimpleDriverDataSource dataSource = new SimpleDriverDataSource();
        dataSource.setDriverClass((Class<? extends Driver>)   Class.forName(messageProperties().getJdbcDriver()));
        dataSource.setUsername(messageProperties().getJdbcUserName());
        dataSource.setUrl(messageProperties().getJdbcUrl());
        dataSource.setPassword(messageProperties().getJdbcPassword());

        return dataSource;
    }

    @Bean
    public DataSourceTransactionManager transactionManager() throws ClassNotFoundException {
        return new DataSourceTransactionManager(dataSource());
    }


    @Bean
    public SqlSessionFactory sqlSessionFactory() throws Exception {
        SqlSessionFactoryBean sqlSessionFactory = new SqlSessionFactoryBean();
        sqlSessionFactory.setDataSource(dataSource());
        return (SqlSessionFactory) sqlSessionFactory.getObject();
    }

    @Bean
    public MessageDao messageDao() throws Exception {
        SqlSessionFactory sessionFactory = sqlSessionFactory();
        sessionFactory.getConfiguration().addMapper(MessageMapper.class);

        SqlSessionTemplate sessionTemplate = new SqlSessionTemplate(sqlSessionFactory());
        return sessionTemplate.getMapper(MessageMapper.class);
    }
}


数据库创建

我们可以用反射的方法直接生成创建,删除以及插入表数据的语句, 而不必自己手写SQL, Hibernate框架也用了类似的方法, 这里简单实现一个数据库创建初始化的工具

配置如下 jdbc.properties

jdbc.driverClass=org.sqlite.JDBC
jdbc.url=jdbc:sqlite:/workspace/walter/wfdb.s3db
#jdbc.driverClass=com.mysql.jdbc.Driver
#jdbc.url=jdbc:mysql://localhost/wfdb?useUnicode=true&characterEncoding=utf8
jdbc.username=walter
jdbc.password=pass

MessageDb

package com.github.walterfan.guestbook.db;

import com.github.walterfan.guestbook.domain.Message;

import java.sql.SQLException;
import java.util.Date;
import java.util.UUID;

import static java.lang.System.out;

/**
 * Created by walter on 07/11/2016.
 */
public class MessageDb {

    private final DbConn dbConn;

    private static String CHECK_SQL = "SELECT * FROM sqlite_master WHERE type='table' and name='%s'";

    public MessageDb() throws Exception {
        dbConn = new DbConn("jdbc.properties");
        dbConn.setDebug(true);
        dbConn.createConnection();
    }

    public void init() throws Exception {
        int ret = initTable();
        if(ret > 0) {
            initData();

        }
    }

    public int initTable() throws Exception {
        int ret = check(Message.class);
        if(ret > 0) {
            out.println("found table and drop it firstly ");
            dropTable(Message.class);
        }

        createTable(Message.class);
        return check(Message.class);



    }

    private int initData() throws Exception {
        String id = UUID.randomUUID().toString();
        Message msg = new Message();
        msg.setId(id);
        msg.setTitle("hello guest");
        msg.setContent("this is a test message");
        msg.setTags("test tag");
        msg.setCreateTime(new Date());
        String sql = DbHelper.makeInsertSql(msg);
        out.println("execute " + sql);
        dbConn.execute(sql);

        sql = DbHelper.makeQuerySql(msg.getClass(), String.format("id = '%s'", id));
        out.println("execute " + sql);
        return dbConn.execute(sql);
    }

    public int createTable(Class<?> clazz) throws Exception {

        String sql = DbHelper.makeCreateTableSql(clazz);
        out.println("execute " + sql);
        return dbConn.execute(sql);
    }

    public int dropTable(Class<?> clazz) throws Exception {

        String sql = DbHelper.makeDropTableSql(clazz);
        out.println("execute " + sql);
        return dbConn.execute(sql);
    }

    public void clean() throws SQLException {
        dbConn.commit();
        dbConn.closeConnection();
    }

    public int check(Class<?> clazz) throws Exception {
        String sql = String.format(CHECK_SQL, clazz.getSimpleName().toLowerCase());
        out.println("execute " + sql);
        return dbConn.execute(sql);

    }

    public static void main(String[] argv) throws Exception {
        MessageDb db = new MessageDb();
        db.init();
        db.clean();
    }

 }

其他代码参见 https://github.com/walterfan/guestbook/tree/master/src/main/java/com/github/walterfan/guestbook/db

执行结果如下:

execute SELECT * FROM sqlite_master WHERE type='table' and name='message'

type name tbl_name rootpage sql
table message message 48 CREATE TABLE message (id TEXT,title

found table and drop it firstly
execute DROP TABLE message
execute CREATE TABLE message (id TEXT,title TEXT,content TEXT,author TEXT,tags TEXT,createTime DATETIME)
execute SELECT * FROM sqlite_master WHERE type='table' and name='message'

type name tbl_name rootpage sql
table message message 48 CREATE TABLE message (id TEXT,title

execute insert into message(id,title,content,author,tags,createTime) values('fa2ba0d0-d843-48e1-804d-641647d33b5b','hello guest','this is a test message',null,'test tag','2016-11-17 23:08:34.959')
execute select * from message where id = 'fa2ba0d0-d843-48e1-804d-641647d33b5b'

id title content author tags createTime
fa2ba0d0-d843-48e1-804d-641647d33b5b hello guest this is a test message test tag 2016-11-17 23:08:34.959

�留言板的主要实现

按照标准的 SpringMVC 结构, 主要用五个类来搞定, 前面两个前面已经提过了, 所有代码请参见 https://github.com/walterfan/guestbook, 下面是所谓的 CRC( Class Responsibility Collaborator ) 类职责与协作者表格

Class Responsibility Collaborator
1. Message 留言数据对象 所有类
2. MessageConfig 留言板配置 MessageController, MessageService, MessageDao
3. MessageController 留言板控制器 MessageService
4. MessageService 留言板服务 MessageDao, MessageController
5. MessageDao 留言板数据存储接口 MessageService
6. MessageMapper 留言板数据存储实现 MessageService

注: 为简单起见, 这里只用了一个数据对象 Message , 也可以细分为

Class Responsibility Collaborator
MessageDto 数据传输对象 MessageService, MessageDao
MessageEntity 数据�实体对象 MessageService, MessageDao
MessageBo 数据�业务对象 MessageService
MessageRequest 数据�请求对象 MessageController
MessageResponse 数据�响应对象 MessageController

MessageContoller

  • 3 . class MessageController
package com.github.walterfan.guestbook.controller;

import com.github.walterfan.guestbook.domain.GenericQuery;
import com.github.walterfan.guestbook.domain.Message;
import com.github.walterfan.guestbook.service.MessageService;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import javax.validation.Valid;
import java.util.List;

@RestController
@RequestMapping(value = "/guestbook/api/v1/", produces = { "application/json" })
public class MessageController {


    protected final Logger logger = LoggerFactory.getLogger(getClass());

    @Autowired
    private MessageService messageService;


    @RequestMapping(value = "/messages", method = RequestMethod.POST)
    public Message createMessage(@Valid @RequestBody Message message) throws Exception {
        logger.info("got post request: " + message.toString());
        messageService.createMessage(message);
        return message;
    }

    @RequestMapping(value = {"/messages", "/"}, method = RequestMethod.GET)
    public List<Message> queryMessages(@RequestParam(value = "start",   required = false) Integer start,
                                       @RequestParam(value = "limit",   required = false) Integer limit,
                                       @RequestParam(value = "order",   required = false) String order,
                                       @RequestParam(value = "sortBy",  required = false) String sortBy,
                                       @RequestParam(value = "keyword", required = false) String keyword,
                                       @RequestParam(value = "fieldName",   required = false) String fieldName) {
        logger.info("query messages request");

        GenericQuery query = new GenericQuery();
        if(null != start) query.setStart(start);
        if(null != limit) query.setLimit(limit);
        if(null != order) {
            if("ASC".equalsIgnoreCase(order)) {
                query.setOrder(GenericQuery.OrderType.ASC);
            } else if("DESC".equalsIgnoreCase(order)) {
                query.setOrder(GenericQuery.OrderType.DESC);
            }
        }
        if(StringUtils.isNotBlank(sortBy)) query.setSortBy(sortBy);
        if(StringUtils.isNotBlank(fieldName)) query.setFieldName(fieldName);
        if(StringUtils.isNotBlank(keyword)) query.setKeyword(keyword);

        List<Message> messageList = messageService.queryMessage(query);
        return messageList;
    }

    @RequestMapping(value = "messages/{id}", method = RequestMethod.GET)
    public Message getMessage(@PathVariable("id") String id) throws Exception {
        return messageService.retrieveMessage(id);
    }


    @RequestMapping(value = "messages/{id}", method = RequestMethod.PUT)
    public Message updateMessage(@PathVariable("id") String id, @RequestBody Message message) {
        message.setId(id);
        messageService.updateMessage(message);
        return message;
    }

    @RequestMapping(value = "messages/{id}", method = RequestMethod.DELETE)
    public void deleteMessage(@PathVariable("id") String id) {
        messageService.deleteMessage(id);

    }
}


MessageService

  • 4 . class MessageService
package com.github.walterfan.guestbook.service;

import com.github.walterfan.guestbook.dao.MessageDao;
import com.github.walterfan.guestbook.domain.GenericQuery;
import com.github.walterfan.guestbook.domain.Message;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.List;
import java.util.UUID;


@Service
public class MessageService {

    @Autowired
    private MessageDao messageDao;

    public void createMessage(Message message) {
        UUID id = UUID.randomUUID();
        message.setId(id.toString());
        messageDao.createMessage(message);
    }

    public Message retrieveMessage(String id) {
        return messageDao.retrieveMessage(id);
    }

    public List<Message> queryMessage(GenericQuery query) {
        return messageDao.queryMessage(query);
    }

    public void updateMessage(Message message) {
        messageDao.updateMessage(message);
    }

    public void deleteMessage(String id) {
        messageDao.deleteMessage(id);

    }
}

MessageDao

  • 5 . class MessageDao
package com.github.walterfan.guestbook.dao;

import com.github.walterfan.guestbook.domain.GenericQuery;
import com.github.walterfan.guestbook.domain.Message;

import java.util.List;

public interface MessageDao {

    void createMessage(Message message);


    Message retrieveMessage(String id);

    void updateMessage(Message message);


    void deleteMessage(String id);


    List<Message> queryMessage(GenericQuery query);
}

  • 6 . MessageMapper
package com.github.walterfan.guestbook.dao;

import com.github.walterfan.guestbook.domain.GenericQuery;
import com.github.walterfan.guestbook.domain.Message;
import org.apache.ibatis.annotations.Delete;
import org.apache.ibatis.annotations.Insert;
import org.apache.ibatis.annotations.Select;
import org.apache.ibatis.annotations.Update;

import java.util.List;

public interface MessageMapper extends MessageDao {

    //#{author.id}
    @Insert("INSERT into message(id,title,content,tags, createTime) " +
            "VALUES(#{id}, #{title}, #{content}, #{tags}, #{createTime})")
    void createMessage(Message message);

    @Select("SELECT * FROM message WHERE id = #{id}")
    Message retrieveMessage(String id);

    @Update("UPDATE message SET title=#{title}, content =#{content}, tags=#{tags} , " +
            " WHERE id =#{id}")
    void updateMessage(Message message);

    @Delete("DELETE FROM message WHERE id =#{id}")
    void deleteMessage(String id);

    @Select("SELECT * FROM message ")
    List<Message> queryMessage(GenericQuery query);

}

至此, 一个最小的留言板微服务雏形已成, 可以快速看一下效果

mvn jetty:run
Paste_Image.png

好, 就此打住.

好的开始是成功的一半, 虽然我们只完成了整个项目的第一步, 也等于成功了一半.
之后, 让我们一步一步来分析和实现更多功能性和非功能性的需求吧.
即使这么小的一个留言板微服务, 也还有不少细节要仔细考虑和优化.

参考

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,311评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,339评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,671评论 0 342
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,252评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,253评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,031评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,340评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,973评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,466评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,937评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,039评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,701评论 4 323
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,254评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,259评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,485评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,497评论 2 354
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,786评论 2 345

推荐阅读更多精彩内容

  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,599评论 18 139
  • Spring Boot 参考指南 介绍 转载自:https://www.gitbook.com/book/qbgb...
    毛宇鹏阅读 46,748评论 6 342
  • 文章作者:Tyan博客:noahsnail.com 2.Introduction to the Spring Fr...
    SnailTyan阅读 5,375评论 7 56
  • 谈到宗教,人们总是热衷于讨论神。 神,亦或者说上帝,如来佛,他们到底存不存在?这是一个一直存在的争论。许多年来,人...
    半山徐风雾自清阅读 485评论 0 0
  • 我以为自己过的挺好,每天开开心心该干嘛干嘛也不觉得有什么不应该。大概也是看了大多心灵鸡汤式的文章,总是安慰自己平凡...
    阿慈i阅读 1,657评论 3 0