Web开发框架推导

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

假设咱们的「特定环境 」以下:web

  • 技术层面spring

    • 使用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之间的关系:
image1.png | center | 800x488
从上图能够看出,除了第一种状况(且这种状况不多),其它四种状况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开发(我会另开一篇讨论这个)!或者你有什么好的方案,欢迎指教?


公众号:ivaneye

相关文章
相关标签/搜索