本文欲回答这样一个问题:在 「特定环境 」下,如何规划Web开发框架,使其能知足 「指望 」?java
假设咱们的「特定环境 」以下:web
技术层面spring
非技术层面数据库
咱们的 「指望 」是:后端
本文从一个空框架开始,逐步加入上面的约束,最终推导出符合指望的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四个包,职责以下:
分层后的框架逻辑清晰,且切分方式符合行业规约,更易于上手。
考虑到目前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,须要符合四个核心的约束:
绝大部分声称符合RESTful的应用都不是百分百符合这四个约束,特别是超媒体做为应用状态引擎(hypermedia as the engine of application state)这个约束。
肯定了以JSON的方式进行参数的传递后,就须要肯定如何来处理参数和返回结果?这涉及到几个问题:
这里选择了Mybatis做为持久化框架,咱们先从Mybatis的角度来回答上面的几个问题!
首先Mybatis做为框架,会生成几个文件:Model.java,Mapper.java和Mapper.xml!(这里不作过多解释!对Mybatis不熟悉的朋友请自行google!)这几个文件能够自动生成,也能够手写!
不管是自动生成仍是手写都有其优缺点:
先说自动生成的优缺点:
手动编写的优缺点:
一种优化方案是,第一次使用自动生成,后续手动修改。
可是结合前面的约束:
此方法并不适用。 此方法只对于改动不太频繁的项目还算适用,可是若是表结构改动较频繁,后续的每次修改仍是要手动修改,很是的麻烦(没法适应频繁的变动,快速迭代)。且只能第一次使用自动生成这个规定并无法强制实施,你无法保证谁不会误操做了自动生成(考虑开发人员资历较浅),致使手写的代码被覆盖了!
结合以上约束,为了尽可能避免错误,优先选择自动生成!再来尝试解决其短板,即生成的三个文件没法进行修改。是否有可行方案呢?
咱们先考虑几个问题:
为方便起见,咱们把入参称为Param,返回结果称为Result。咱们先回答第一个和第四个问题!
Controller须要对Param作哪些操做?
Controller须要对Result作哪些操做?
这些操做均可以方便的处理:
这些都是规约!
针对第二个和第三个问题,咱们先看Param、Result和Model之间的关系:
从上图能够看出,除了第一种状况(且这种状况不多),其它四种状况Param和Model实际是一个包含的关系。既然是一种包含的状况,那这种包含关系,在Java里咱们可使用继承来实现。也就是说可使Param extends Model,以这样的方式来复用Model的内容!
咱们来看以这种方式来实现Param和Result,如何来解决上面的问题!
尽可能以扩展规约的方式来处理问题,在不增长理解难度的状况下提升易用性和开发效率!
在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测试,缘由以下:
SpringBoot能够直接打包为jar包,直接运行启动。这很方便,可是若是想快速的横向扩容,配置文件就是一个问题。由于不一样机器上的配置并非彻底相同的。
有两个方案能够解决:
从便利性考虑,仍是选择配置服务器。
配置文件中均是开发环境配置,方便开发人员直接开发、测试。
在正式环境中,应用启动时会从配置服务器获取对应的配置,覆盖本地测试进行部署。
在结束以前,先问个问题?你是喜欢代码生成、仍是封装?
我我的更偏向代码生成,理由是:
基于上面的缘由,再考虑到其实咱们的框架都是符合规约的(RESTful,JSR303,覆写,Jackson),故对于标准CRUD,咱们能够一键生成!
其实到上面一节,整个框架应该已经符合预期了!可是为了获得超预期的效果,咱们来更进一步!
咱们先看目前的开发流程:
对于一个典型的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逻辑,咱们能够作到以下的开发流程:
对于不可重复生成的文件,咱们能够设置"存在即不覆盖",在最大限度的提升开发效率的前提下,下降误操做。
如上便是我基于约束所作的Web推导!目前的主要问题仍是在Model层面:
目前我的以为基于data的transform、filter、map操做更适合web开发(我会另开一篇讨论这个)!或者你有什么好的方案,欢迎指教?
公众号:ivaneye