forkme right red aa0000

1. Spring Roll

Build Status codecov JitPack

1.1. How to use?

1.1.1. gradle

build.gradle 文件的 repositories 末尾添加 JitPack 仓库:

allprojects {
    repositories {
        ...
        maven { url 'https://jitpack.io' }
    }
}

然后即可添加依赖,以 roll-test 模块为例:

dependencies {
    implementation 'com.github.alphahinex.spring-roll:roll-test:0.0.8.RELEASE'
}

其他构建工具配置情况,可参考 https://jitpack.io/#AlphaHinex/spring-roll

1.2. 主要设计目标

  • Spring Boot 为基础,采用当前主流且活跃的开源技术和框架,以简化开发步骤、提升开发效率和质量为目标,拥抱并响应变化为宗旨,稳定坚固可持续发展为愿景,对平台进行架构

  • 平台功能按模块进行划分及构建,除核心功能模块和公共模块外,各模块尽量保持独立,且可按需组合

  • 部署方式可集中可分布。集中式部署时为一个 web 应用(单体应用),分布式部署时为多个 web 应用(微服务)。分布式部署时通过网关保持部署方式对用户的透明,系统内部无需单点登录

  • 模块间需要依赖时,面向接口和服务编程,以便模块按不同需求提供不同实现

  • 屏蔽对具体数据库的依赖,表结构及初始化数据以 Database Change Log File 形式组织

  • 平台功能及特性须有对应的单元测试,及必要的集成测试及性能测试

1.3. 项目构建

项目构建依托 gradle 构建工具,gradlewgradle wrapper,作用是使不同的开发环境能够使用统一版本的构建工具进行构建, 避免版本不同带来的兼容性等问题。wrapper 中使用的 gradle 版本参见 gradle-wrapper.properties

1.3.1. 整体构建

$ ./gradlew build

1.3.2. 子项目独立构建

<module_name>:build,以 roll-utils 子项目为例

$ ./gradlew roll-utils:build

项目构建会对源码进行编译、进行代码质量检查、执行测试、构建 jar 包。构建的内容输出到各子项目的 build 路径内。

Windows 环境下使用时,需使用 gradlew.bat 批处理指令,构建命令也需相应变化,如整体构建命令为 gradlew build

1.4. 发布

1.4.1. 发布到 GitHub Packages

$ ./gradlew publish

注意,仅 release 版本可被其他项目依赖,snapshot 版本虽然可以发布上去,但无法被下载。

1.4.2. 发布到本地 Maven 库

$ ./gradlew publishToMavenLocal

1.5. 部署及运行

Spring boot 默认配置 tomcat 使用的字符集为 UTF-8(org.springframework.boot.web.embedded.tomcat.TomcatServletWebServerFactory#DEFAULT_CHARSET), 使用的 connector 为 Http11NioProtocol(org.springframework.boot.web.embedded.tomcat.TomcatServletWebServerFactory#DEFAULT_PROTOCOL)。

推荐使用 docker 容器 + Jar 包的形式部署及运行,避免因运行环境差异导致的各种问题。 通过 Docker Compose 编排和运行服务(单节点),可以在 docker-compose.yml 中调整所需服务,并运行

$ docker-compose up -d

2. Developer Guidelines

2.1. 开发环境

2.1.1. IntelliJ IDEA

可使用 IDEA 直接打开项目路径,自动导入 gradle 项目。

或者手动生成 idea 配置文件后,直接打开,如:

$ ./gradlew cleanIdea idea

在 IDEA 中需要配置为 gradle 项目才可部署至 tomcat 中开发

2.2. Spring Boot 开发运行及调试

Spring Boot 内置 tomcat 使用默认端口 8080

2.2.1. 直接运行

$ ./gradlew bootRun

bootRun 任务会将源码编译后启动运行。

2.2.2. 远程调试

$ ./gradlew bootRun --debug-jvm

IDEA 开启远程调试方式可参见 IntelliJ Remote Run/Debug Configuration

2.3. 测试

  • 单元测试 $ ./gradlew test

  • 测试及代码质量检查 $ ./gradlew check

  • 单元测试覆盖 bean 的方法:因为单体测试类可能会因为具体的测试功能需要不同的测试数据,之前的做法是在测试文件中添加一个 Mock 接口的实现类并通过 @Primary 将 Mock 的实现类设置为主要实现类,例如:

    @Primary
    public class MockDepartmentServiceImpl extends DepartmentServiceImpl implements DepartmentService {
        @Override
        List<Department> getAllDepartments() {
            ... ...
        }
    }

    但是这样处理后当出现有多个不同的测试数据需求时很难进行分别处理,如何解决这个问题呢? 单体测试可以使用从 Spring 上下文中获取具体实现类,通过新的实现类重写接口方法进行测试验证并在测试验证后将原有的实现类放回上下文中,具体操作如下:

    第一步:通过上下文获取目标接口的实现类,如:

    DepartmentExtService departmentExtService = ApplicationContextHolder.getBean("departmentExtService");

    第二步:通过 overrideSingleton 方法中 使用 @Override 方法重写该接口的方法返回所需测试数据,如:

    overrideSingleton("departmentExtService", new DepartmentExtService() {
        @Override
        List<Department> getAllDepartments() {
            ... ...
        }
    })

    第三步:在验证测试数据后,需要将原接口的实现类放回至 Spring 上下文中,这样就不会对其他的测试类造成影响,如:

    overrideSingleton("departmentExtService", departmentExtService);

    最后,还有一点需要说明,如果要覆盖的 bean 已被其他 bean 所引用,则需要在测试类结束前恢复 所有 相关类的实例,否则可能会对其他需要用到未覆盖前 bean 的行为的单元测试造成意外影响!

推送代码至远程仓库或创建 Pull Request 之前需确保所有测试及检查能够在本地通过

2.4. 开发规范

  • 统一使用 Version.HASH 作为 serialVersionUID

  • 单元测试:与被测试的类相同包名,被测试类名称为测试类名前缀, 基于 Junit 的测试以 Test 为后缀,基于 Spock 的测试以 Spec 为后缀。

  • 异常不允许直接 printStackTrace,应记录到日志中, 日志打印需要将堆栈进行输入, 避免只打印 e.getMessage()

    LOGGER.error("execute failed", e);
  • 记录日志时,应采用 Slf4j 推荐的方式,避免日志信息通过 + 拼接

    LOGGER.debug("{} concat {} is {}", "a", "b", "ab");
  • 当在日志消息中有非常消耗资源的操作时,可考虑先判断日志级别,如:

    if (LOGGER.isDebugEnabled()) {
        LOGGER.debug("hello {} world {}", cheap, veryExpensive());
    }

TO BE CONTINUED

2.5. 版本号规范

版本号参照 语义化版本 规范,采用四位数字,如:1.1522.49.327

  • 第一位表示`主版本`

  • 第二位表示`次版本`

  • 第三位表示`修订版`

  • 第四位表示`bug 修正` 等

第四位为可选位,没有第四位时,第三位也可以表示 bug 修正 等含义。

2.6. 版本发布流程

  1. 后端修改 build.gradle 中的 version 值,并同步修改 Version.java 中的 版本号

  2. 提交代码打 tag,并将 tag 推送远程仓库,如:

    $ git tag v0.4.0
    $ git push upstream v0.4.0
  3. 编写 Release Note

注意区分发布分支(master)和开发分支(develop),发布分支的内容自动合并至开发分支,在开发分支发布新版本后,将开发分支中的内容合并至发布分支

2.7. 开放问题

  • HTTP 500 状态码问题:通常系统异常时应该返回 500 状态码。 但在与 nginx 共同部署时,nginx 连接上游服务器超时时也会返回 500 状态。 当需要故障转移时,就会出现矛盾:因为连接某一台上游服务器超时,其他服务器仍然可能可以处理这个请求;但若请求会导致系统抛异常,其他服务器再处理这个请求应该也会得到同样的结果。 当前对这个问题的处理方式是:

    平台返回的系统异常和业务异常仍然使用 500 作为响应的状态码,但会增加一个特殊的响应头 X-SR-ERR-TYPE,并使用这个响应头的内容区分系统异常和业务异常。 系统异常值为 SR_SYS_ERR,业务异常值为 SR_BIZ_ERR

TO BE CONTINUED

2.8. 持续集成环境

平台使用 Travis CI 作为持续集成环境。 为保证代码质量,任何提交到 master 分支的代码和任何 Pull Request 都会触发持续集成环境对代码质量的检查。

Pull Request 的构建结果会直接在列表页和详细信息页面展现

Build Result

master 分支的构建结果会在项目首页展现

Build Status

测试覆盖率结果也会在项目首页展现

codecov

每位工程师都要为项目构建失败或覆盖率下降负责!

3. Reference

4. blocks

一些开箱即用的组件,可直接被业务利用的模块。

4.1. roll-export

通用导出功能,面向前端提供一个导出 URL,传入表头定义、业务查询数据接口及请求参数等信息, 可自动调用业务查询接口 RestController 中的方法,并将返回对象输出成一个统一格式的 Excel 文件,写入到 Response 里。

4.1.1. 导出接口

/export/excel/{title}

提供 GETPOST 两种格式。

GET 接口不支持 methodbizReqBody 参数,通过 Request Parameter 传入。

POST 接口通过 Request Body 传递入参。

详细接口信息可参见 swagger 文档:http://localhost:8080/swagger-ui.html

4.1.2. 参数表

参数名

Method

是否必填

描述

示例

title

POST、GET

路径变量,指定导出 Excel 文件的文件名(不包含扩展名,扩展名固定为 xlsx)。

TestExport

cols

POST、GET

列表中对表头的列定义,具体结构参见 ColumnDef。GET 时按 JSON 格式表示,需进行 URL Encode。

[{"label":"名称","prop":"name","showTitle":true}]

url

POST、GET

查询数据请求 url。可包含请求参数。GET 时需进行 URL Encode。

/test/query?a=1&b=2

tomcatUriEncoding

POST、GET

‍需匹配 Tomcat 中的 URIEncoding,以免乱码。缺省值为 UTF-8。独立运行的 Tomcat 默认 URIEncoding 为 ISO-8859-1,可在 server.xml 的 Connector 中进行设定。

UTF-8

method

POST

‍HTTP Method,默认为 GET,不区分大小写。

POST

bizReqBody

POST

‍业务请求的请求体。当查询数据请求为 POST 等,需要通过 Request Body 传递内容时,可以将数据对象放入此属性中传递给导出接口。格式不限。

{"name":"body name","des":"body des"}

GET 示例:

可以按此格式组装:

"/export/excel/" + title + "?cols=" + cols + "&url=" + url + "&tomcatUriEncoding=" + encode

URL Encode 之后如下:

/export/excel/%E4%B8%AD%E6%96%87?cols=%5B%7B%22display%22%3A%22%E5%90%8D%E7%A7%B0%22%2C%22name%22%3A%22name%22%2C%22showTitle%22%3Atrue%2C%22field%22%3A%22name%22%2C%22hidden%22%3Afalse%2C%22label%22%3A%22%E5%90%8D%E7%A7%B0%22%2C%22prop%22%3A%22name%22%2C%22title%22%3A%22%E5%90%8D%E7%A7%B0%22%7D%5D&url=%2Ftest%2Fquery&tomcatUriEncoding=utf-8

POST Request Body 示例:

{
   "cols":[
     {"prop":"name","label":"名称"},
     {"prop":"des","label":"描述"},
     {"label":"无prop","other":"props"}],
   "url":"/test/query/post/plant_name/plant_des",
   "bizReqBody":{"name":"body name","des":"body des"},
   "method":"POST",
   "tomcatUriEncoding":"utf-8"
}
ColumnDef 结构

ColumnDef 作为前端表格组件的结构体,支持了 EasyUI、QUI 和 ElementUI 的基本表头定义格式,主要包含三个表头属性及一个附加属性(内容解码器)。

含义

可用属性

类型

示例

显示名

display, title, label

表头属性

性别

属性名

name, field, prop

表头属性

gender

显示/隐藏,注意两个属性的含义是相反的

showTitle, hidden

表头属性

true, false

解码器定义,key/value 对

decoder

附加属性

{key: 'date', value: 'yy-MM-dd HH:mm:ss'}

4.1.3. 基本原理

根据传入的业务接口 url,找到对应的 RestController 方法进行调用,并对返回值进行 拆封解码 两步操作:

  1. 拆封:根据业务接口返回值结构,找到合适的 PaginationHandler 接口实现类,进行拆封。默认提供了一个 MapPaginationHandler,可按此新增其他实现。

  2. 解码:根据入参列定义中 decoder 属性的 key,找到合适的 DecodeHandler 接口的实现类,进行解码。默认提供了日期解码器(DateDecodeHandler)和默认的 toString 解码器(DefaultToStringDecodeHandler)。

具体用法可参见单元测试 ExportExcelControllerTest

暂不支持特殊表头格式的表格导出。

4.1.4. 配置项

提供了如下配置项,可以在 propertiesyml 中进行配置:

key

描述

默认值

roll.export.excel.page-number

默认在请求中添加一个代表当前页的参数,供分页查询使用,参数名默认为 pageNumber

pageNumber

roll.export.excel.page-size

默认在请求中添加一个代表每页数据总数的参数,供分页查询使用,参数名默认为 pageSize

pageSize

roll.export.excel.date-decoder-key

日期类型解码器标识,默认为 date

date

roll.export.excel.max-rows

通用导出功能导出的 Excel 最大行数,默认 5000,设置过大可能会导致导出时间过长或无响应

5000

4.2. roll-utils

工具类模块,扩展并封装一些常用工具,如集合、日期、字符串、JSON 等,可被其他模块依赖,且不依赖任何模块,也不依赖 Spring 。

5. dev-kits

开发工具库,方便开发及测试。

5.1. roll-dev-configs

开发配置库,包括依赖定义、质量检查配置及具体规则、脚手架等。

5.1.1. 脚手架

初始化一个项目

依照 Spring Roll 框架体系新建一个项目时,可使用 new-<group>_<artifact> 模式脚手架,如:

$ ./gradlew new-com.example_sr-demo

包括:

  1. 沿用依赖版本

  2. 模块定义方式

  3. 开发模式规范

  4. 质量控制体系

初始化新模块

例如要新增一个 test-init 模块,可以通过下面的任务初始化该模块的基本路径及文件:

$ ./gradlew init-test-init

初始化内容包括:

modules/test-init
modules/test-init/src/main/java/io/github/springroll/test/init
modules/test-init/src/main/resources
modules/test-init/src/test/groovy/io/github/springroll/test/init
modules/test-init/src/test/resources
modules/test-init/test-init.gradle
modules/test-init/README.md

其中 package 为 project_group + module_name

注意:初始化任务执行时会先将改模块根路径删除

5.2. roll-swagger

Swagger 配置模块,引入后启动应用环境,浏览器输入以下网址即可浏览 API 文档

常用注解如下,如若有具体需要请参考 swagger 的[官方文档]:

  • @Api 用在类上,说明该类的作用;

  • @ApiModel 用在类上,表示对类进行说明,用于实体类中的参数接收说明;

  • @ApiModelProperty 用于字段,表示对model属性的说明;

  • @ApiOperation 用在 Controller 里的方法上,说明方法的作用,每一个接口的定义;

  • @ApiParam 用于Controller中方法的参数说明;

  • @ApiImplicitParams 用在方法上,为请求参数进行说明,如下:@ApiImplicitParams({@ApiImplicitParam1,@ApiImplicitParam2,…​})

  • @ApiResponse 用于方法上,说明接口响应的一些信息,@ApiResponses`组装多个@ApiResponse`

@ApiOperation("''‍''…​")、@ApiModelProperty() 等注解中需要编写中文时,需在包含中文的字符串收位加 ''‍'' 零等宽字符,使checkStyle检查成功。

5.3. roll-test

测试套件,旨在方便各模块开发各类测试用例,提供基础支持。

6. incubating

孵化中组件

6.1. roll-dl

动态语言包,引入对动态语言(Groovy)的支持。

主要实现了使用 Groovy 动态脚本或类进行校验的场景的支持。

可通过 GroovyShellExecution 进行脚本的执行。

在 Spring 环境中,会将标记为 Scriptable 的 bean 集合以 applicationContext 为 key 加入到 Binding 中,供脚本调用。 没有实现此接口的类,原则上不可以在脚本中通过 applicationContext.beanName 的方式直接调用。 但若必需,可考虑从 ApplicationContextHolder 中直接获取。

针对单独脚本要用到的变量(非共享),可通过 execute(String scriptContent, Map<String, Object> scriptContext) 中的 scriptContext 进行传递, 并在调用时,通过 scriptContext.varName 进行调用。

GroovyShellExecution 执行时产生的异常,都被封装到了 GroovyScriptException 中。

7. raw-materials

原材料组件,指一些本身没有太多业务价值,需被依赖后进行扩展的内容。更多的作用是定义一些常量、规范、模型等。

7.1. roll-base

基础模块,包括平台的基础定义,基于 Spring 的基础组件等。

7.2. roll-base-jpa

为平台提供使用 JPA 的基础支持

7.3. roll-web

基于 Spring Web MVC 的基础 web 组件。主要围绕 Servlet 框架和 RESTFul 接口进行一些基础内容定义。

8. roll-application

Powered by Spring-roll

Web Application Module

此模块为所有模块中唯一的一个 web 应用,其他各个模块均以 jar 包的方式被本模块所依赖。

在开发及发布时,可根据实际情况调整本模块依赖的模块,以达到定制产品功能的目的。