Web开发框架推导

本文欲回答这样一个问题:在 「特定环境 」下,如何规划Web开发框架,使其能满足 「期望 」?

假设我们的「特定环境 」如下:

  • 技术层面
    • 使用Java语言进行开发
    • 通过Maven构建
    • 基于SpringBoot
    • 使用IntellijIDEA作为IDE
    • 使用Mybatis作为持久层框架
    • 前后端分离
  • 非技术层面
    • 新项目,变化较频繁
    • 快速迭代
    • 开发人员资历较浅
    • 人员流动性较大

我们的 「期望 」是:

  • 快速上手:鉴于人员流动性较大、开发人员的资历较浅和项目的快速迭代需求,期望开发框架易于开发人员开发。易于入门,易于部署。
  • 符合行业规约:尽量不定义私有规范,使用行业标准,进一步降低学习难度
  • 快速开发:尽可能复用代码,尽可能自动化生成模板代码
  • 独立性:应用能独立运行,不过多的依赖其它应用或中间件。边界清晰,有利于理解、开发、测试和部署。反例:就是没有规划的RPC调用。
  • 易于测试:能方便的进行单元/集成测试,不影响真实数据
  • 易于部署:能方便的进行部署,便于快速的扩容
  • 异常可追踪:对异常,可快速定位到具体是哪个应用,哪个类,哪行代码的问题

本文从一个空框架开始,逐步加入上面的约束,最终推导出符合期望的Web框架!
本文提供的是一种思路!如有纰漏、或不同意见,欢迎讨论指正!

从「空框架」开始

我们从一个「空框架」开始我们的框架推导!所谓「空框架」是一个没有任何约束的接收HTTP的可运行代码,比如对任何请求都只返回Hello World的servlet!
这里我们基于Maven和SpringBoot快速搭建一个「空框架」!

代码结构如下(Maven构建约束):

intellijweb2
    src/main
        java
            com.ivaneye.intellijweb2
                TestController
        resources
            application.properties
            logback-spring.xml

代码如下:

package com.ivaneye.intellijweb2;
 
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.ResponseBody;
 
@Controller
@EnableAutoConfiguration
public class TestController {
 
    @RequestMapping("/")
    @ResponseBody
    public String home() {
        return "Hello World!";
    }
 
    public static void main(String[] args) throws Exception {
        SpringApplication.run(Main.class, args);
    }
}

启动后,当访问http://localhost:8080时,页面上将显示Hello world!字样!

我们完全可以基于这个「空框架」进行开发,但是这个「空框架」离我们的期望还很远。我们来一步步的改造!

分层架构

分层架构可以说是Web项目的默认架构风格,可以说是行业标准!所以我们首先引入分层架构这个约束!

分层架构有其优势和劣势:

  • 优势:通过将组件对系统的知识限制在单一层内,为整个系统的复杂性设置了边界,并且提高了底层独立性。使用层来封装遗留的服务,使新的服务免受遗留客户端的影响;通过将不常用的功能转移到一个共享的中间组件中,从而简化组件的实现。中间组件还能够通过支持跨多个网络和处理器的负载均衡,来改善系统的可伸缩性。

  • 劣势:增加了数据处理的开销和延迟,因此降低了用户可觉察的性能。可以通过在中间层使用共享缓存来弥补这一缺点。

Web里最常用的切分方式就是MVC模式!我们对我们的「空框架」引入MVC模式!
那我们这里是切分包?还是切分模块呢?考虑到最小影响原则,这里先切分包。如果有后续约束,再做进一步调整。

引入MVC模式后的代码结构:

intellijweb2
    src/main
        java
            com.ivaneye.intellijweb2
                controller
                    TestController
                model
                respository
                service
                Main
        resources
            application.properties
            logback-spring.xml

引入MVC模式后的代码:

package com.ivaneye.intellijweb2;
 
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.context.annotation.ComponentScan;
 
@EnableAutoConfiguration
@ComponentScan({"com.ivaneye.intellijweb2"})
public class Main {
 
    public static void main(String[] args) throws Exception {
        SpringApplication.run(Main.class, args);
    }
}
 
 
package com.ivaneye.intellijweb2.controller;
 
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.ResponseBody;
 
@Controller
public class TestController {
 
    @RequestMapping("/")
    @ResponseBody
    public String home() {
        return "Hello World!";
    }
}

这里暂时切分了Controller,Service,Model,Respository四个包,职责如下:

  • Controller:接收前台的请求,验证数据,组装需要的数据,委托Service执行具体业务逻辑,并将结果组装返回给前台

  • Service:处理核心业务逻辑,包含事务

  • Model:数据模型,与数据库表的对应类

  • Respository:数据操作类包,操作Model中的类,进行基本的CRUD操作

分层后的框架逻辑清晰,且切分方式符合行业规约,更易于上手。

前后端分离

考虑到,目前Web开发流行前后端分离,为了适应潮流,引入前后端分离的约束。

为了适应前后端分离,后端不负责页面的渲染,只接收和返回JSON数据。SpringBoot对此有直接的支持,直接将@Controller改为@RestController即可!

相关代码:

package com.ivaneye.intellijweb2.controller;
 
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
 
@RestController
public class TestController {
 
    @RequestMapping("/")
    public String home() {
        return "Hello World!";
    }
}

整个URL符合RESTful,即符合行业规约!至于REST相关内容另行讨论。

实际上完整的RESTful应用不只是URL符合RESTful,需要符合四个核心的约束:

  • 资源的识别(identification of resources)

  • 通过表述操作资源(manipulation of resources through representations)

  • 自描述的消息(self-descriptive messages)

  • 超媒体作为应用状态引擎(hypermedia as the engine of application state)

绝大部分声称符合RESTful的应用都不是百分百符合这四个约束,特别是超媒体作为应用状态引擎(hypermedia as the engine of application state)这个约束。

基于注解的数据处理

确定了以JSON的方式进行参数的传递后,就需要确定如何来处理参数和返回结果?这涉及到几个问题:

  • Controller如何接收参数?

  • Controller如何返回结果?

  • Controller如何将数据传递给Respository进行持久化处理?

  • Respository又如何将数据从数据库中查出来返回给Controller?

这里选择了Mybatis作为持久化框架,我们先从Mybatis的角度来回答上面的几个问题!

首先Mybatis作为框架,会生成几个文件:Model.java,Mapper.java和Mapper.xml!(这里不做过多解释!对Mybatis不熟悉的朋友请自行google!)这几个文件可以自动生成,也可以手写!

不论是自动生成还是手写都有其优缺点:

  • 先说自动生成的优缺点

    • 优点就是在修改表结构以后,直接一条命令就可以自动生成新文件。

    • 缺点就是这三个文件不能修改,如果修改了就不能再次自动生成了,否则会被覆盖。

  • 手动编写的优缺点

    • 优点是完全自主控制,可复用Model,在里面添加注解,实现数据验证、主键加解密、字典自动查询等逻辑。

    • 缺点就是表结构调整后,需要手动修改需要调整的文件。一是繁琐,二是没有编译期校验,如果手误写错了,直到运行期才可能发现

一种优化方案是,第一次使用自动生成,后续手动修改。

但是结合前面的约束:

  • 新项目,变化较频繁
  • 快速迭代
  • 开发人员资历较浅

此方法并不适用。 此方法只对于改动不太频繁的项目还算适用,但是如果表结构改动较频繁,后续的每次修改还是要手动修改,非常的麻烦(无法适应频繁的变更,快速迭代)。且只能第一次使用自动生成这个规定并没法强制实施,你没法保证谁不会误操作了自动生成(考虑开发人员资历较浅),导致手写的代码被覆盖了!

结合以上约束,为了尽量避免错误,优先选择自动生成!再来尝试解决其短板,即生成的三个文件无法进行修改。是否有可行方案呢?

我们先考虑几个问题:

  1. Controller需要对页面传过来的参数做哪些操作

  2. 页面传来的参数和Model是一个什么关系

  3. 从Controller返回给页面的数据又和Model是什么关系

  4. Controller对返回给页面的数据又要做哪些操作

为方便起见,我们把入参称为Param,返回结果称为Result。我们先回答第一个和第四个问题!

  • Controller需要对Param做哪些操作
    • 把从页面传递过来的flat数据transform为对象(这是面向对象语言的一种典型做法,我目前更偏向函数式做法,另开一篇讨论)

    • 对数据做校验:类型对不对、格式对不对、是否为空等等等等

    • 解密:有些字段数据可能是加过密的,比如主键,在transform的过程中需要对这些字段进行解密处理

  • Controller需要对Result做哪些操作
    • 加密:对需要加密的字段进行加密操作,比如主键

    • 字典转换:有些字段是code码,页面需要code码对应的值,方便人类阅读。这里需要根据这些code码从字典中获取对应的值(你可以在数据库查询的时候,直接关联字典表查询,但是这样会带来两个麻烦,一个是model中需要包含字典value字段,就没法自动生成了。第二个就是,一般字典会放在内存中,关联表查询相对内存取数据,性能上会有劣势)

    • 字典列表:和字典转换类似,有些页面需要字典列表数据,需要获取这些数据到前台供用户选择

这些操作都可以方便的处理:

  • SpringMVC已经提供了数据绑定功能,将数据绑定到对象上

  • JSR303基于注解进行校验

  • 加解密、字典都可以通过自定义注解处理(扩展Jackson的注解处理即可。Jackson的注解只在方法上生效,本以为是个问题,却助我构思了一个方案:一个结合了自动生成的方便性和手写的灵活性的方案!!!!)

这些都是规约!

针对第二个和第三个问题,我们先看Param、Result和Model之间的关系:


50e52024-4589-4069-be6b-297b5d4ebe53-image1.png

从上图可以看出,除了第一种情况(且这种情况很少),其它四种情况Param和Model实际是一个包含的关系。既然是一种包含的情况,那这种包含关系,在Java里我们可以使用继承来实现。也就是说可以使Param extends Model,以这样的方式来复用Model的内容!
我们来看以这种方式来实现Param和Result,如何来解决上面的问题!

  • 首先,因为Param和Result都继承了Model,所以Model是不需要做任何改动的,就可以无限次的自动生成

  • 其次,数据验证、加解密的注解是可以添加到方法上的。我们对需要这些注解的字段,在Param/Result里覆盖Model里的get/set方法,在其上添加注解,就可以使用基于注解的数据验证和加解密

  • 假设数据字段有了修改,重新生成后,由于有@Override注解,在编译期就可以定位到需要修改的get/set方法,结合IDE可以快速修复

  • 如果是新增字段,则直接重新生成Mybatis的三个文件即可,原有代码不受任何影响

尽量以扩展规约的方式来处理问题,在不增加理解难度的情况下提高易用性和开发效率!

数据返回

在RESTful约束中,推荐使用HTTP的标准响应来处理返回数据。SpringMVC中也提供了标准响应的支持。

ResponseEntity.ok("body");
ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("");

但是由于HTTP的标准状态码太少了,见下表:

代码 消息 描述
100 Continue 只有请求的一部分已经被服务器接收,但只要它没有被拒绝,客户端应继续该请求。
101 Switching Protocols 服务器切换协议。
200 OK 请求成功。
201 Created 该请求是完整的,并创建一个新的资源。
202 Accepted 该请求被接受处理,但是该处理是不完整的。
203 Non-authoritative Information
204 No Content
205 Reset Content
206 Partial Content
300 Multiple Choices 链接列表。用户可以选择一个链接,进入到该位置。最多五个地址
301 Moved Permanently 所请求的页面已经转移到一个新的 URL。
302 Found 所请求的页面已经临时转移到一个新的 URL。
303 See Other 所请求的页面可以在另一个不同的 URL 下被找到。
304 Not Modified
305 Use Proxy
306 Unused 在以前的版本中使用该代码。现在已不再使用它,但代码仍被保留。
307 Temporary Redirect 所请求的页面已经临时转移到一个新的 URL。
400 Bad Request 服务器不理解请求。
401 Unauthorized 所请求的页面需要用户名和密码。
402 Payment Required 你还不能使用该代码。
403 Forbidden 禁止访问所请求的页面。
404 Not Found 服务器无法找到所请求的页面。
405 Method Not Allowed 在请求中指定的方法是不允许的。
406 Not Acceptable 服务器只生成一个不被客户端接受的响应。
407 Proxy Authentication Required 在请求送达之前,您必须使用代理服务器的验证。
408 Request Timeout 请求需要的时间比服务器能够等待的时间长,超时。
409 Conflict 请求因为冲突无法完成。
410 Gone 所请求的页面不再可用。
411 Length Required "Content-Length" 未定义。服务器无法处理客户端发送的不带 Content-Length 的请求信息。
412 Precondition Failed 请求中给出的先决条件被服务器评估为 false。
413 Request Entity Too Large 服务器不接受该请求,因为请求实体过大。
414 Request-url Too Long 服务器不接受该请求,因为 URL 太长。当你转换一个 “post” 请求为一个带有长的查询信息的 “get” 请求时发生。
415 Unsupported Media Type 服务器不接受该请求,因为媒体类型不被支持。
417 Expectation Failed
500 Internal Server Error 未完成的请求。服务器遇到了一个意外的情况。
501 Not Implemented 未完成的请求。服务器不支持所需的功能。
502 Bad Gateway 未完成的请求。服务器从上游服务器收到无效响应。
503 Service Unavailable 未完成的请求。服务器暂时超载或死机。
504 Gateway Timeout 网关超时。
505 HTTP Version Not Supported 服务器不支持“HTTP协议”版本。

这些标准的状态码无法详细的表示一个项目中的所有情况。且目前SpringMVC不支持自定义状态码。就是类似这样的代码:

ResponseEntity.status(10001).body("");

虽然不报错,但是无法正常响应,后台会报类似“非标准状态码”的错误!
所以我自定义了一个对象Result,用来完成类似ResponseEntity的工作。Result的结构如下:

public class Result {
    private int code;//200为正常,其它为相关业务报错
    private String msg;//对应的错误信息,200为ok
    private Object body;//返回的业务对象
}

提供类似:

Result.ok("body")
Result.error(e);
Result.error(CommonConstants.SERVER_ERROR, e.getMessage());

这样的构造方法,方便使用。

异常处理

异常处理在上面数据返回里涉及了一点(就是Result的构造以及业务的各种场景处理)。这里详细说明。
约束中需要能方便的追踪异常!
Java里提供了CheckedException和UnCheckedException,而对于我们实际使用来说,还是需要区分业务场景。

  • 异常是业务异常还是非业务异常?
    • 这里的业务异常指的是:由于不符合业务需求而导致的异常,比如:用户没登录,必要字段没填写导致校验失败,订单的数量超出了库存。

    • 非业务异常则指的是:和业务场景不相关的异常。例如:数据库连接失败了,网络连接失败。

表现到代码上,对于业务异常我们可以定义BusinessException来表示,所有继承了BusinessException的异常,都是业务异常,而其它异常就是非业务异常。

  • 更进一步,业务异常也可以分为:
    • 通用业务异常,例如:用户没有登录,必要字段没填写导致校验失败;

    • 和特定业务异常,例如:订单的数量超出库存了。

这两种异常,我们可以通过异常码来区分,例如:100开头的为通用业务异常,300开头的为订单异常,400开头的为产品异常,依此类推。
同时异常的Code和Msg与Result对应,方便构建Result.error(e);直接返回。
再进一步,目前的应用都是分布式的,甚至是微服务架构!我们是否可以通过异常能快速的定位到是哪个应用的哪个模块里的哪个代码出问题了呢?
一种可行方案还是通过异常码来处理:以三位数字为间隔,来区分应用+模块+代码,例如:001002301,可以理解为异常是001机器上的,002应用,抛出的301(订单相关)异常。

独立性

当系统变得越来越大后,难免不会出现系统内不同应用之间的相互调用;如果是微服务的话,那么服务间的相互调用是很常见的。如果处理不当,会使得各应用之间相互依赖,无法独立的运行。导致开发、测试、部署都很麻烦。
为了避免这样的问题出现,结合如下两个约束:

  • 符合行业规约

  • 独立性

故使用RESTful方式,作为应用间通信的方式。这也是微服务推荐的通信方式!
应用间调用会出现Model的依赖,故这里将Model从包提升为模块。方便后续如果有其它应用要依赖时,可直接依赖Model模块,而不是整个应用。

调整后代码结构如下:

intellijweb2
    intellijweb2-web
        src/main
            java
                com.ivaneye.intellijweb2
                    controller
                        TestController
                    respository
                    service
                    Main
            resources
                application.properties
                logback-spring.xml
    intellijweb2-model
        src/main
                java
                    com.ivaneye.intellijweb2
                        model
                        param
                        result

将model包移动到了intellijweb2-model模块中,同时新增了param和result包!

测试

SpringBoot本身提供了较为完善的测试功能。包括单元测试、Mocker、Spy等。
基于如下几个考虑:

  • 易于测试:我接触的很多开发人员是不喜欢写测试的。如果测试代码不易编写,那就更不愿意写了。
  • 不影响环境:我期望的是在发布时是包含测试的,测试不通过即不能发布。也就是说在部署时测试,会使用正式环境的库表数据,所以在测试时不能影响到这些数据。
  • 小范围测试:以最少的代码,覆盖最核心的代码逻辑

故决定只对Service测试,原因如下:

  • 在上面的分层架构里描述了各层的职责,可以看出,核心业务都在Service层,Controller和Model都没有业务逻辑,只是一些标准化代码,没必要测试
  • SpringBoot对Controller的测试是在不同的线程内,不支持事务,如果在正式环境测试的话,会影响正式库数据

部署

SpringBoot可以直接打包为jar包,直接运行启动。这很方便,但是如果想快速的横向扩容,配置文件就是一个问题。因为不同机器上的配置并不是完全相同的。
有两个方案可以解决:

  • Docker

  • 配置服务器

从便利性考虑,还是选择配置服务器。
配置文件中均是开发环境配置,方便开发人员直接开发、测试。
在正式环境中,应用启动时会从配置服务器获取对应的配置,覆盖本地测试进行部署。

代码生成OR封装

在结束之前,先问个问题?你是喜欢代码生成、还是封装?

  • 代码生成就类似Mybatis这样生成了对应的文件,逻辑透明。你可以去改

  • 封装就类似Hibernate,你写个对象,然后对对象操作就行了,底层数据库操作由Hibernate来处理

我个人更偏向代码生成,理由是:

  • 简单:易于使用,易于上手

  • 行业标准:生成的代码是行业标准代码,只要熟悉Mybatis,Spring就可以直接上手(而Mybatis和Spring目前是互联网标配)。如果公司内部进行一些封装,那么新手需要先理解这些封装,增加了学习成本。

基于上面的原因,再考虑到其实我们的框架都是符合规约的(RESTful,JSR303,覆写,Jackson),故对于标准CRUD,我们可以一键生成!

一键生成

其实到上面一节,整个框架应该已经符合预期了!但是为了得到超预期的效果,我们来更进一步!

我们先看目前的开发流程:

  • 设计数据表

  • 生成Model,Mapper

  • 编写Param,Result

  • 编写Respository

  • 编写Service

  • 编写Controller

  • 编写测试

  • 执行测试

  • 提交代码

对于一个典型的CRUD操作,这里有多少重复代码呢?
篇幅有限,举个简单的例子:现在需要编写Order和User的新增逻辑,Controller的代码是什么样的?

Controller:

package ${package.Controller};

import ...

@Api(tags = "${table.controllerName}")
@RestController
@RequestMapping("$!{cfg.basePath}")
public class ${table.controllerName} extends ${superControllerClass}{

    @Autowired
    private ${table.serviceImplName} ${instanceName}Service;

    private Logger logger = LoggerFactory.getLogger(${table.controllerName}.class);

    @ApiOperation(value = "创建${entity}")
    @RequestMapping(value = "/$!{cfg.version}/${table.entityPath}", method = RequestMethod.POST)
    public Result create(@RequestBody @Validated(Create.class) ${entity}Param param, BindingResult bindingResult) {
        try {
            //验证失败
            if (bindingResult.hasErrors()) {
                throw new ValidException(bindingResult.getFieldError().getDefaultMessage());
            }
            Long recId = ${instanceName}Service.create(param);
            return Result.ok(recId);
        } catch (BusinessException e) {
            logger.error("create ${entity} Error!", e);
            return Result.error(e);
        } catch (Exception e) {
            logger.error("create ${entity} Error!", e);
            return Result.error(CommonConstants.SERVER_ERROR, e.getMessage());
        }
    }
}

如上的模板是否能符合OrderController和UserController?再往后看Service,Param,Result等是否都可以用类似的模板来统一处理?
所以,我们完全可以对相应的代码进行自动生成,尽可能的降低模板代码的手动编写。对于标准的CRUD逻辑,我们可以做到如下的开发流程:

  • 设计数据表
  • 生成CRUD,包括测试(我们测试的是Service,想想测试代码和Controller代码有多少区别?)
  • 执行测试
  • 提交代码

对于不可重复生成的文件,我们可以设置"存在即不覆盖",在最大限度的提高开发效率的前提下,降低误操作。

总结

如上即是我基于约束所做的Web推导!目前的主要问题还是在Model层面:

  • 数据表映射为Model是否是合理的?
  • 基于Model的操作是否合适?
  • 基于上面Param、Result和Model的关系图来看,实际上Param、Result和Model大部分情况下都不是契合的!把这些Param、Result限制在Model上是否合适?数据结构是否清晰?

目前个人觉得基于data的transform、filter、map操作更适合web开发(我会另开一篇讨论这个)!或者你有什么好的方案,欢迎指教?

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

推荐阅读更多精彩内容

  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 171,887评论 25 707
  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,642评论 18 139
  • 手贱,看了不该看的,又是一晚失眠,明知你这么渣,我已经完全没了底线
    滚你丫阅读 143评论 0 0
  • 和心爱的人儿在一起 无论干什么 都很开心 一起起床 一起上班 一起回家 一起买菜 一起做饭 一起看电视 一起听音乐...
    太不專業阅读 210评论 0 0
  • 原计划公务员,结果都没准备。因为总有事情打扰。 1.帮蔡老师整理调研组人员名单 2.下午修笔记本电脑,与连成聊了很...
    炎丽阅读 260评论 0 0