io.github.watertao:veigar-framework

A scaffold for developing Restful API

License

License

GroupId

GroupId

io.github.watertao
ArtifactId

ArtifactId

veigar-framework
Last Version

Last Version

2.2.0
Release Date

Release Date

Type

Type

pom
Description

Description

A scaffold for developing Restful API
Project URL

Project URL

https://github.com/watertao/veigar
Source Code Management

Source Code Management

https://github.com/watertao/veigar.git

Download veigar-framework

How to add to project

<!-- https://jarcasting.com/artifacts/io.github.watertao/veigar-framework/ -->
<dependency>
    <groupId>io.github.watertao</groupId>
    <artifactId>veigar-framework</artifactId>
    <version>2.2.0</version>
    <type>pom</type>
</dependency>
// https://jarcasting.com/artifacts/io.github.watertao/veigar-framework/
implementation 'io.github.watertao:veigar-framework:2.2.0'
// https://jarcasting.com/artifacts/io.github.watertao/veigar-framework/
implementation ("io.github.watertao:veigar-framework:2.2.0")
'io.github.watertao:veigar-framework:pom:2.2.0'
<dependency org="io.github.watertao" name="veigar-framework" rev="2.2.0">
  <artifact name="veigar-framework" type="pom" />
</dependency>
@Grapes(
@Grab(group='io.github.watertao', module='veigar-framework', version='2.2.0')
)
libraryDependencies += "io.github.watertao" % "veigar-framework" % "2.2.0"
[io.github.watertao/veigar-framework "2.2.0"]

Dependencies

There are no dependencies for this project. It is a standalone project that does not depend on any other jars.

Project Modules

  • veigar-core
  • veigar-parent
  • veigar-audit-log
  • veigar-db
  • veigar-session
  • veigar-session-redis
  • veigar-session-map
  • veigar-auth
  • veigar-mbg-plugin
  • veigar-swagger

Veigar

一个用于开发 RESTful 接口的脚手架,基于 spring boot。

1. 贡献者

曾利(henry),吴涛(watertao)

2. 快速入门

2.1. 新建 Maven 项目

├── myapp
|   ├── src
|   |   └── main
|   |       ├── java
|   |       |   └── com
|   |       |       └── mycompany
|   |       |           └── BootstrapApplication.java
|   |       └── resources
|   |           └── application.properties
│   └── pom.xml

2.2. pom.xml

在 pom.xml 中引入 veigar 有两种方式:

2.2.1. 继承 veigar-parent

pom.xml
<parent>                                <!--(1)-->
  <groupId>io.github.watertao</groupId>
  <artifactId>veigar-parent</artifactId>
  <version>2.1.1</version>
</parent>
<groupId>com.mycompy</groupId>
<artifactId>myapp</artifactId>
<version>1.0.0-SNAPSHOT</version>
  1. 继承 veigar-parent

2.2.2. 依赖 veigar-core

若项目无法依赖 veigar-parent(比如需要依赖其他 parent) ,那么通过以下方式可达到同样效果:

pom.xml
<groupId>com.mycompy</groupId>
<artifactId>myapp</artifactId>
<version>1.0.0-SNAPSHOT</version>
<dependencies>
  <dependency>                          <!--(1)-->
    <groupId>io.github.watertao</groupId>
    <artifactId>veigar-core</artifactId>
    <version>2.1.1</version>
  </dependency>
</dependencies>
<build>
  <plugins>
    <plugin>                            <!--(2)-->
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-maven-plugin</artifactId>
      <executions>
        <execution>
          <id>repackage</id>
          <goals>
            <goal>repackage</goal>
          </goals>
        </execution>
      </executions>
      <configuration>
        <mainClass>${start-class}</mainClass>
      </configuration>
    </plugin>
  </plugins>
</build>
  1. 依赖 veigar-core

  2. 添加插件 spring-boot-maven-plugin

2.3. BootstrapApplication.java

BootstrapApplication.java
@SpringBootApplication(
   scanBasePackages={
      "com.mycompany",                  // (1)
      "io.github.watertao.veigar"       // (2)
   }
)
public class BootstrapApplication {
   public static void main(String[] args) {
      SpringApplication.run(BootstrapApplication.class, args);
   }
}
  1. 自己项目 package

  2. 告诉 spring boot 去扫描 veigar 相关的组件,这是必须的。

Note

需要注意的是,scanBasePackages 中自己项目的 package 需要尽量设置到公司级别,比如上例中的 com.mycompany ,因为这会影响到组件的扫描范围。

2.4. application.properties

application.properties
spring.profiles.active = dev            # (1)
app.basePackage = com.mycompany.myapp   # (2)
server.port = 8080                      # (3)
cors.allowedOrigins = *                 # (4)
  1. 开发环境下设置为 dev, 生产环境下设置为 prod

  2. 项目级别的 package(公司级别下一级),某些组件需要读取并使用这个参数

  3. 服务端口

  4. CORS 允许的来源

2.5. 第一个接口

创建 src/main/java/com/mycompany/myapp/controller/TestController.java

├── myapp
|   ├── src
|   |   └── main
|   |       ├── java
|   |       |   └── com
|   |       |       └── mycompany
|   |       |           └── controller
|   |       |               └── TestCOntroller.java
|   |       └── resources
│   └── pom.xml
TestController.java
@RestController                         // (1)
public class TestController {
    @GetMapping("/test")                // (2)
    public Object test() {
        Map map.put("a", "b");
        return map;
    }
}
  1. 每个 RESTful 接口类都需要使用 @RestController 注解

  2. 每个 RESTful 接口方法都需要使用 @RequestMapping 或其子注解

2.6. 启动项目

通过执行 spring-boot-maven-plugin 启动项目:

mvn spring-boot:run

3. 设计理念

veigar 是在 spring boot 基础上进一步封装了开发 RESTful 接口时常用的一些特性,用于简化开发框架的搭建过程。
目前 veigar 支持的组件包括:

组件名 作用 依赖

veigar-parent

用于简化 pom.xml 中对 veigar 的依赖

veigar-core

核心组件

veigar-session

若项目具有用户会话的概念,就需要用到此组件。而此组件一般不单独依赖,还需要额外依赖一个 veigar-session-* 来指定会话持久化到哪里

veigar-core

veigar-session-map

将会话保存在内存 Map 中

veigar-session

veigar-session-redis

将会话保存在 redis 中

veigar-session

veigar-db

若项目需要进行数据库访问,可以依赖此组件

veigar-core

veigar-auth

提供了用户认证相关的扩展点

veigar-session

veigar-audit-log

提供了用户日志审计相关的扩展点

veigar-session

veigar-mbg-plugin

提供了 mybatis generator 的插件

veigar-swagger

提供了 swagger 生成 API 文档的特性

4. 特性

4.1. 基本 RESTful 支持

4.1.1. query parameter 获取

HTTP REQUEST
GET /users?name=watertao HTTP/1.1
controller
@GetMapping("/users")
public void test(
  @RequestParam("name") String name       // (1)
) {
    // name = "watertao";
}
  1. 使用 @RequestParam 获取 query parameter

4.1.2. path variable 获取

HTTP REQUEST
GET /users/133 HTTP/1.1
controller
@GetMapping("/users/{userId}")              // (1)
public void test(
  @PathVariable("userId") Integer userId    // (2)
) {
    // userId = 133;
}
  1. URI 的定义中需要指定 path variable 参数名,本例中为 {userId}

  2. 使用 @PathVariable 获取 path variable, 注解的参数需要与 URI 中 {userId} 内的定义相对应

4.1.3. request body 获取

HTTP REQUEST
POST /users HTTP/1.1
Content-Type: application/json;charset=UTF-8

{
  "name": "watertao"
}
controller
@PostMapping("/users")
public void test(
  @RequestBody User user                    // (1)
) {
    // user.getName() = "watertao"
}
  1. 使用 @RequestBody 注解告诉 spring boot 将 JSON 反序列化为对象

User
public class User {
    private String name;
    public String getName() {...}
    public void setName(String name) {...}
}

4.1.4. Content-Type

veigar 只支持 JSON 格式,且字符集为 UTF-8 的请求。
所以,客户端在发起 RESTful 请求调用时,若请求体中包含了 JSON,则必须设置 Content-Type

HTTP REQUEST
POST /users HTTP/1.1
Content-Type: application/json;charset=UTF-8

{ ... }

4.1.5. 请求校验

veigar 集成了 Hibernator-validator 作为 bean validation 的实现。所以我们可以很方便的对请求体中的 JSON 进行验证。

User.java
public class User {
    @NotEmpty                                 // (1)
    private String name;

    public String getName() {...}

    public void setName(String name) {...}
}
  1. 通过注解 @NotEmpty 确保 name 属性不可为空

controller
@PostMapping("/users")
public Object test(
  @Valid @RequestBody User user             // (1)
) {

}
  1. 通过添加注解 @Valid 告知 spring boot 对 user 对象进行校验,若 JSON 中 name 属性为空,则会抛出校验异常

bean validation 以及 hibernate-validator 所支持的校验注解可参考:
bean validation
hibernate validator

4.1.6. CORS 跨域

application.properties 中添加以下配置可支持浏览器跨域访问:

application.properties
cors.allowedOrigins = http://localhost:8000

通过逗号分隔,可以支持多个域:

application.properties
cors.allowedOrigins = http://localhost:8000,http://10.10.10.10

或者通过 * 支持所有的域:

application.properties
cors.allowedOrigins = *

但需要注意的是,如果客户端的请求中包含了 credential,那么就不可使用 *,必须指定一个确定的域。

除了域以外,veigar 还支持其他 CORS 相关的配置,但绝大部分情况下不必对其进行设置:

application.properties
# 允许跨域访问的 method
cors.allowedMethods = POST,PUT,GET,PATCH,DELETE,OPTIONS
# 允许跨域访问时的请求头
cors.allowedHeaders = x-auth-token,if-modified-since
# 允许跨域访问时响应中可访问的头
cors.exposedHeaders = x-total-count,x-auth-token
# preflight (OPTIONS请求) 的缓存时长
cors.maxAge = 1728000

4.1.7. JSON pretty print

默认情况下,JSON 被序列化为单行,虽然紧凑,但对人类并不友好,我们可以通过配置以下参数让 json 序列化时更美观:

application.properties
spring.jackson.serialization.indent_output = true

4.1.8. 日期

veigar 会将日期以 ISO-8601 兼容的格式来序列化日期,如 2019-01-09T10:41:44.000+0800 ,我们可以通过以下参数设置时区及格式:

application.properties
spring.jackson.date-format = yyyy-MM-dd HH:mm:ss
spring.jackson.time-zone = GMT+8

4.2. 日志打印

veigar 使用 slf4j + logback 来输出日志。
application.properties 中可以通过以下配置设置 root 的输出级别以及输出 pattern:

application.properties
logging.root.level = INFO                                                   # (1)
logging.encodePattern = %d{yyyy/MM/dd-HH:mm:ss SSS} %-5level - %msg %n      # (2)
  1. root 输出级别,缺省为 INFO

  2. 输出的 pattern,缺省为 %d{yyyy/MM/dd-HH:mm:ss SSS} %-5level - %msg %n

根据 application.properties 中的属性 spring.profiles.active 取值不同,日志输出的行为也会有所不同:

dev

日志只会输出到控制台,不会输出到文件。

prod

日志只会输出到文件,不会输出到控制台。
在这种模式下,veigar 还支持以下配置:

application.properties
logging.path = /myapp/log                             # (1)
logging.file = myapp.log                              # (2)
logging.splitPattern = yyyy-MM-dd_HH                  # (3)
logging.maxHistory = 30                               # (4)
  1. 日志文件输出的目录,缺省为 jar 包所在的目录

  2. 日志文件的文件名,缺省为 spring.log

  3. 日志文件按时间切割的模式,缺省为 yyyy-MM-dd (即按天切割)

  4. 日志文件保存的文件个数,缺省为 30 个文件

日志输出 API 使用范例:

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class TestController {

  private static final Logger logger = LoggerFactory.getLogger(TestController.class);

  public void test() {
    logger.info("hello log");
  }

}

4.3. 请求日志

veigar 会打印所有 controller 的请求调用,看起来如下:

2019/01/10-13:13:19 141 INFO  - <--o POST /test
2019/01/10-13:13:19 142 INFO  - PAYLOAD: {"name":"test2"}
...
2019/01/10-13:13:19 142 INFO  - o--> COST: 1ms; PAYLOAD: {"name":"test2"}

如果觉得这种打印风格不满足需求,也可以实现 io.github.watertao.veigar.core.reqlog.RequestLogger 接口, 并将其注册为 spring bean 来替换默认风格。比如:

MyRequestLogger.java
@Component
public class MyRequestLogger implements RequestLogger {

    private static final Logger logger = LoggerFactory.getLogger(MyRequestLogger.class);

    @Override
    public void preLog(HttpServletRequest request, Object requestBody) {
        logger.info("REQUEST RECEIVED:");
        logger.info("{} {}", request.getMethod(), request.getRequestUri());
    }

    @Override
    public void postLog(Object result, Throwable exception, Long cost) {
        logger.info("RESPONSE: {}", toJson(result));
    }
    private String toJson(Object obj) {...}
}

那么请求日志的输出将会变成:

2019/01/10-13:13:19 141 INFO  - REQUEST RECEIVED:
2019/01/10-13:13:19 142 INFO  - POST /test
...
2019/01/10-13:13:19 142 INFO  - RESPONSE: {"name":"test2"}

4.4. 异常处理

在 veigar 项目中,我们不必在 controller 中捕获异常并将其转化为 json。我们只需要直接抛出异常即可, veigar 会将其转化为合适的 json。
veigar 提供了以下几个常用的运行时异常类:

异常类 推荐使用场景 HTTP 状态码

BadRequestException

当请求的参数有问题时,比如格式有误

400

ForbiddenException

当请求被禁止访问时,比如 A 分公司的用户想要访问 B 分公司的数据,若是业务要求禁止,那么就可以抛出此类异常

401

ConflictException

当资源与预期状态有冲突时,比如针对一个尚未测试的接口进行审核通过的请求调用,按照逻辑是不允许的,这时候就可以提示状态冲突。

409

NotFoundException

访问了一个不存在的资源,比如对一个 ID为3的接口进行修改操作,而实际上库里并不存在 ID 为 3 的接口

404

UnauthenticatedException

系统无法识别当前用户的时候。比如 session 过期,登录时密码错误等

403

HttpStatusException

如果以上异常都不满足场景时,可使用此异常,并设定一个状态码即可。

自定义

InternalServerException

提对于运行时产生的一些非预期异常,比如 NullPoint,数据库访问异常等,框架最终都被将其包装成此错误

500

任何异常最终都会被转成以下格式的 json:

{
    "status": 403,                      // (1)
    "error": "Forbidden",               // (2)
    "message": "未登录",                 // (3)
    "verbose": null                     // (4)
}
  1. 异常对应的状态码

  2. 状态码对应的标准描述语(与 HTTP 规范兼容)

  3. 自定义的异常描述

  4. 附加的异常描述补充

Note

对于客户端而言,状态码为 2xx ( 如 200 / 201 / 204 ) 的响应就代表着请求的处理是成功的,非 2xx 的响应即代表处理失败。

4.5. 消息国际化

若要在 veigar 项目中使用消息国际化的特性,需要在 src/main/resources/message 下创建不同语言的 消息资源文件,下面以中文和英文为例:

├── myapp
|   ├── src
|   |   └── main
|   |       ├── java
|   |       └── resources
|   |           └── message
|   |               ├── message_en.properties           // (1)
|   |               └── message_zh.properties           // (2)
│   └── pom.xml
  1. 英文消息资源文件

  2. 中文消息资源文件

分别为两个资源文件添加属性名为 test.name 的消息:

message_en.properties
test.name = I'm English,param_1 is {0} and param_2 is {1}
message_zh.properties
test.name = 我是中文的,参数1的值是 {0},参数2的值是 {1}

在需要国际化消息的地方,可以通过注入 io.github.watertao.veigar.core.message.LocaleMessage 来使用:

@Component
public class Test {

    @Autowired
    private LocaleMessage localeMessage;                                                    // (1)

    public void test() {
        System.out.println(localeMessage.m("test.name", new Object[] { "a", "b" }));        // (2)
    }

}
  1. 注入 LocaleMessage bean

  2. 调用 localeMessage 的 m 方法,将消息属性名作为参数传入即可

veigar 会根据 HTTP 请求头部中的 Accept-Language 来决定使用哪种语言的消息资源文件。
上例中若语言为

中文

输出为:

我是中文的,参数1的值是 a,参数2的值是 b
英文

输出为:

I'm English,param_1 is a and param_2 is b

4.6. 数据库访问

访问数据库是绝大部分项目的需求,我们需要添加组件 veigar-db 的依赖;

pom.xml
<dependency>
    <groupId>io.github.watertao</groupId>
    <artifactId>veigar-db</artifactId>
    <version>2.2.0</version>
</dependency>

veigar-db 使用 mybatis 作为 ORM 框架, 使用 druid 作为连接池。
下面以 mysql 为例,描述如何使项目支持数据库访问。
除了上面的 veigar-db ,我们还需要添加 jdbc 驱动的依赖:

pom.xml
<dependency>
     <groupId>mysql</groupId>
     <artifactId>mysql-connector-java</artifactId>
     <version>5.1.47</version>
</dependency>

application.properties 中添加数据库相关的配置:

application.properties
spring.datasource.driverClassName = com.mysql.jdbc.Driver
spring.datasource.url = jdbc:mysql://localhost:3306/test?characterEncoding=UTF-8&zeroDateTimeBehavior=convertToNull
spring.datasource.username = test
spring.datasource.password = test
spring.datasource.maxActive = 20          # (1)
  1. 连接池的最大连接数

做完了以上这些工作,我们就可以在项目中使用 mybatis 进行开发了。

4.6.1. Mapper 接口

Mapper 接口可以放在项目 package ( com.mycompany.myapp ) 下的任意目录中,veigar 通过 @Mapper 注解来识别 Mapper 接口:

package com.mycompany.myapp.mapper;

import org.apache.ibatis.annotations.Mapper;
import org.springframework.stereotype.Component;

@Component("com.mycompany.myapp.mapper.MyCustomMapper")         // (1)
@Mapper                                                         // (2)
public interface MyCustomMapper {
  ...
}
  1. @Component 注解是为了给 Mapper 定义一个 bean name,强烈建议设置成接口的全限定名,这么做可以避免不同 package 下相同类名的 Mapper 接口产生冲突。

  2. @Mapper 注解

4.6.2. Mapper XML 映射文件

映射文件 必须 放在 src/main/resources/mybatis/mapper 文件夹下:

├── myapp
|   ├── src
|   |   └── main
|   |       ├── java
|   |       └── resources
|   |           └── mybatis
|   |               └── mapper
|   |                   ├── Test1Mapper.xml
|   |                   └── Test2Mapper.xml
│   └── pom.xml

4.6.3. 在 service 中使用 mapper

veigar 会扫描带有 @Mapper 的接口,并将其注册为 bean,service 类中我们可以注入 mapper 进行 数据库访问:

@Service
public class TestService {
    @Autowired
    private TestMapper testMapper;
}

4.6.4. 数据库事务

veigar 使用了基于注解的事务,因此在 service 类中我们得给需要事务的方法添加 @Transactional 注解:

@Service
public class TestService {
    @Transactional
    public void doTest() {
        ...
    }
}

4.6.5. 自动生成 Mapper

对数据库表的简单增删改查,我们可以通过 Mybatis-generator ( mbg ) 来自动生成 Mapper 接口, Model 以及 映射文件。
首先需要在 pom.xml 中添加 mbg 插件的依赖:

pom.xml
<build>
 <plugins>
  <plugin>
   <groupId>org.mybatis.generator</groupId>
   <artifactId>mybatis-generator-maven-plugin</artifactId>
   <version>1.3.7</version>
   <configuration>
    <configurationFile>mbg/generatorConfig.xml</configurationFile>
   </configuration>
   <dependencies>
     <dependency>
       <groupId>io.github.watertao</groupId>
       <artifactId>veigar-mbg-plugin</artifactId>
       <version>2.2.0</version>
     </dependency>
   </dependencies>
  </plugin>
 </plugins>
</build>

在项目根目录下新建 mbg 相关目录及 generatorConfig.xml 配置文件:

├── myapp
|   ├── mbg
|   |   ├── output
|   |   └── generatorConfig.xml
|   ├── src
│   └── pom.xml
generatorConfig.xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE generatorConfiguration
  PUBLIC "-//mybatis.org//DTD MyBatis Generator Configuration 1.0//EN"
  "http://mybatis.org/dtd/mybatis-generator-config_1_0.dtd">
<generatorConfiguration>
  <properties resource="application.properties" ></properties>
  <classPathEntry location="/Users/watertao/.m2/repository/mysql/mysql-connector-java/5.1.47/mysql-connector-java-5.1.47.jar" />    <!--(1)-->
  <context id="mbgTables" targetRuntime="MyBatis3">
    <plugin type="org.mybatis.generator.plugins.MapperAnnotationPlugin"></plugin>
    <plugin type="io.github.watertao.veigar.mbgplugin.ComponentAnnotationPlugin"></plugin>
    <commentGenerator>
      <property name="suppressAllComments" value="true"/>
    </commentGenerator>
    <jdbcConnection driverClass="${spring.datasource.driverClassName}"
                    connectionURL="${spring.datasource.url}"
                    userId="${spring.datasource.username}"
                    password="${spring.datasource.password}">
    </jdbcConnection>
    <javaTypeResolver>
      <property name="forceBigDecimals" value="false"/>
    </javaTypeResolver>
    <javaModelGenerator targetPackage="${app.basePackage}.model" targetProject="mbg/output/">
      <property name="enableSubPackages" value="true"/>
      <property name="trimStrings" value="true"/>
    </javaModelGenerator>
    <sqlMapGenerator targetPackage="mapper" targetProject="mbg/output/">
      <property name="enableSubPackages" value="true"/>
    </sqlMapGenerator>
    <javaClientGenerator type="XMLMAPPER" targetPackage="${app.basePackage}.mapper.autogen" targetProject="mbg/output/">
      <property name="enableSubPackages" value="true"/>
    </javaClientGenerator>
    <table schema="test" tableName="test" enableSelectByExample="true" enableDeleteByExample="true"
           enableCountByExample="true" enableUpdateByExample="true">        <!--(2)-->
           <generatedKey sqlStatement="Mysql" column="id" identity="true"></generatedKey>
    </table>
  </context>
</generatorConfiguration>
  1. 开发环境本地的 jdbc 驱动绝对路径

  2. 需要生成的表

我们可以复制以上内容到 generatorConfig.xml 文件,设置好 jdbc 驱动的位置,添加所需要生成的表,接着执行以下命令:

mvn mybatis-generator:generate

执行成功后,在 myapp/mbg/output 目录下会生成相应的文件,然后我们将他们拷贝到 myapp/src/main 下对应的位置即可。

Tip

为了避免自动生成的文件覆盖掉手动修改过的文件,强烈建议将自动生成的和手工生成的分别放在不同的目录中,我们可以 在 com.mycompany.myapp.mapper 下建立 autogen 和 custom 包, 在 resources/mybatis/mapper 下建立 autogen 和 custom 文件夹,最终的目录结构看起来如下:

├── myapp
|   ├── mbg
|   ├── src
|   |   └── main
|   |       ├── java
|   |       |   └── com
|   |       |       └── mycompany
|   |       |           └── myapp
|   |       |               ├── mapper
|   |       |               |   ├── autogen    // (1)
|   |       |               |   └── custom     // (2)
|   |       |               └── model          // (3)
|   |       └── resources
|   |           ├── autogen                    // (4)
|   |           └── custom                     // (5)
│   └── pom.xml
  1. 存放 mbg 自动生成的 mapper 接口

  2. 存放手工编写的 mapper 接口

  3. 存放自动生成的 model

  4. 存放 mbg 自动生成的 mapper 映射文件

  5. 存放手工编写的 mapper 映射文件

4.6.6. 分页处理

veigar 使用 pagehelper 进行分页的处理,要使用该功能需要在 application.properties 中指定 sql 方言,缺省为 mysql

application.properties
pagehelper.helperDialect = mysql

目前支持的方言包括:db2,hsqldb,informix,mysql,oracle,sqlserver

在 service 中使用分页很简单:

public class TestService {
  @Autowired
  private UserMapper userMapper;

  public void test(int pageIdx, int pageSize, String name) {
    // 假设 pageIdx = 0, pageSize = 10
    PageHelper.startPage(pageIdx, pageSize);                  // (1)

    List<User> users = userMapper.findUsersByName(name);      // (2)

    PageInfo<User> pageInfo = new PageInfo<>(users);          // (3)

    // pageInfo.getTotal() = 满足条件的记录总数
    // pageInfo.getList() = 当前页返回的 10 条记录
    // ...

    PageHelper.offsetPage(pageIdx * pageSize, pageSize);      // (4)

  }

}
  1. 在进行任意的 sql 查询之前,先通过 PageHelper.startPage 设置本次分页的起始页和页大小

  2. 执行 Mapper 的查询方法

  3. PageInfo 类构建一个实例,传入上一步返回的结果集,最终获得的就是一个分页结果对象

  4. 除了 startPage ,PageHelper 还提供了 offsetPage, 前者是按页数作为第一个参数,后者是按照游标作为第一个参数

在调用了 PageHelper 类的 startPageoffsetPage 方法之后,紧接着后面的 mapper 查询就会被 PageHelper 进行二次处理,它会同时 发起一个 count 查询和数据查询,并且返回的结果集是一个被改造过的 List,该 List 具备分页相关的一些属性。因此我们 new PageInfo(list) 的 时候,传入的 list 参数必须是改造过的 List,否则无法正确读取分页信息。

4.6.7. 在日志中打印 sql

在 veigar 中打印 sql 需要在 application.properties 中将 Mapper 类的日志级别调整到 DEBUG, 比如:

application.properties
logging.level.com.mycompany.myapp.mapper = DEBUG

这会让 com.mycompany.myapp.mapper 包下所有的 Mapper 调用都打印出 sql:

2019/01/14-16:57:29 652 DEBUG - ==>  Preparing: SELECT count(0) FROM test t WHERE t.name LIKE ?
2019/01/14-16:57:29 652 DEBUG - ==> Parameters: 上海(String)
2019/01/14-16:57:29 653 DEBUG - <==      Total: 1
Note

由于开发环境的配置会被打包进 jar,所以在生产环境配置 config/application.properties 时,记得这点。若是发现在生产中并未对某个 logger 做 配置,但依然打印了日志,也许可能就是开发环境的配置在作祟。

4.7. 会话( Session )支持

如果项目涉及到用户,那么就需要支持会话,在 veigar 使用会话需要在 pom 中添加组件 veigar-session 的依赖:

pom.xml
<dependency>
    <groupId>io.github.watertao</groupId>
    <artifactId>veigar-session</artifactId>
    <version>2.2.0</version>
</dependency>

同时还需要依赖一个会话序列化的实现组件,veigar 目前提供了两种方案:

4.7.1. 会话序列化至内存

对于简单的项目,我们完全可以将 session 保存在 jvm 内存中,采用这种方式需要添加依赖:

pom.xml
<dependency>
    <groupId>io.github.watertao</groupId>
    <artifactId>veigar-session-map</artifactId>
    <version>2.2.0</version>
</dependency>

这种方式虽然简单,但会有两个弊端:
首先,负载均衡时无法做到多个应用间共享 session
其次,应用重启后,session 将丢失

4.7.2. 会话序列化至 redis

对于需要负载均衡的项目,我们往往会将会话保存在外部缓存中,比如 redis,采用这种方式需要添加依赖:

pom.xml
<dependency>
    <groupId>io.github.watertao</groupId>
    <artifactId>veigar-session-redis</artifactId>
    <version>2.2.0</version>
</dependency>

同时我们需要在 application.properties 中添加 redis 的连接配置:

application.properties
spring.redis.host = localhost
spring.redis.port = 6379
Note

需要注意的是,Session 的序列化实现组件只能依赖一个,也就是说不能同时依赖 veigar-session-mapveigar-session.redis

4.7.3. 识别会话令牌

veigar 支持客户端在请求中以三种方式携带令牌( 会话 ID ),按照优先级从高到低分别是:

query parameter
GET /test?auth_token=47844236-fdb6-494e-bd66-7607f8c9b1b6 HTTP/1.1
http header
GET /test HTTP/1.1
X-Auth-Token: 47844236-fdb6-494e-bd66-7607f8c9b1b6
cookie
GET /test HTTP/1.1
Cookie: auth_token=47844236-fdb6-494e-bd66-7607f8c9b1b6;

4.7.4. 创建会话

veigar 提供了一个创建会话的 API 方法:
io.github.watertao.veigar.session.api.AuthObjHolder.createSession()
由于 veigar 无法预知或假设项目的用户认证方式,所以开发人员需要实现自己的认证逻辑, 认证成功后可通过此 API 创建会话。

会话创建成功后, veigar 会在 http header 和 cookie 这两处设置令牌反馈给客户端:

HTTP/1.1 201 Created
X-Auth-Token: 47844236-fdb6-494e-bd66-7607f8c9b1b6
Set-Cookie: auth=47844236-fdb6-494e-bd66-7607f8c9b1b6; path=/; httpOnly;

客户端可任意选择一种方式获得会话令牌。

4.7.5. 获取当前登录用户

在开发接口的过程中,我们常常需要获得当前登录用户的信息,比如用户 ID,veigar 提供了以下接口帮助 开发人员快速从 session 中获得登录用户信息:
io.github.watertao.veigar.session.api.AuthObjectHolder.getAuthObj()
该方法返回的是 AuthenticationObject 的子类:

public abstract class AuthenticationObject {
  private String token;
  public String getToken() {
    return token;
  }
  public void setToken(String token) {
    this.token = token;
  }
  public abstract List<String> getAttributes();
}

该类仅定义了 token (令牌)和 attributes (用于权限判断,后面会提到)两个属性,通常我们 的项目会需要很多额外属性,比如用户 ID,所属部门等,那就需要在继承该类时,扩展这些属性,这些扩展 了的属性需要在用户认证时进行填充。

4.7.6. 用户认证

用户认证即登录,是一个识别用户身份的过程。不同的项目有不同的认证手段,最常见的是通过用户输入的用户 名密码与数据库中保存的进行比对判断,当然还有通过单点登录,第三方登录等方式进行用户的认证。但不管 采用哪种方式,别忘了认证通过后,必须为应用创建会话

veigar 提供了一个 Filter 抽象类用于简化某些场景下的登录逻辑,使用该 Filter 需要在 pom 中 依赖 veigar-auth 组件:

pom.xml
<dependency>
    <groupId>io.github.watertao</groupId>
    <artifactId>veigar-auth</artifactId>
    <version>2.2.0</version>
</dependency>

以基于用户名密码的认证方式为例:

@Component
public class MyAuthenticationFilter extends AuthenticationFilter {
  private static final String METHOD = "POST";
  private static final String URI = "/system/session";
  public MyAuthenticationFilter() {
    super(METHOD, URI);                     // (1)
  }
  @Override
  protected AuthenticationObject authenticate(HttpServletRequest request, HttpServletResponse response, Object requestBody) {
    AuthenticationRequest authRequest = (AuthenticationRequest)requestBody;
    String userName = authRequest.getName();
    String password = authRequest.getPassword();
    // 根据 userName 和 password 实现认证逻辑
    // 若认证成功需要创建 AuthenticationObject
    return authObj;
  }
  @Override
  protected Class getReqBindingClass() {    // (2)
    return AuthenticationRequest.class;
  }
}
  1. 定义用户登录时的请求 methoduri

  2. 定义登录请求的报文结构,veigar 会用此类型去尝试解析请求体中的 JSON,若未定义该方法,veigar 默认会使用 Map.class 去解析。

登录的逻辑实现位于抽象方法 authentication(Object request) 中,在该方法内我们可以用任意方式 去验证用户的身份,当验证通过后,我们需要创建一个 AuthenticationObject 的子类,为其填充上所需 的字段,然后再返回。 需要注意的是,AuthenticationObject 的 attributes 属性是特别重要的,用于 判断该用户是否有权限访问某个资源,我们可以将其想象成是 角色
在认证过程中发生了身份验证失败,建议抛出 UnauthenticatedException 异常。
当我们实现了上例中的这个用户认证 Filter 之后,就可以通过以下请求进行登录:

POST /system/session HTTP/1.1
Content-Type: application/json;charset=UTF-8

{
  "name": "watertao",
  "password": "111111"
}
Note

并非一定要通过 AuthenticationFilter 来实现登录逻辑,事实上完全可以编写自己的 Filter,甚至 Controller 来实现,只是在身份验证成功后,别忘了创建会话

4.7.7. 权限校验

当一个请求发起时,如何判断当前用户是否具有访问的权限呢? 不同的项目往往有不同的权限处理逻辑,有的 是基于角色的,有的可能基于复杂的组织机构树,veigar 抽象并提供了一组接口用于实现不同项目自己的 权限判断逻辑。

首先我们需要实现 io.github.watertao.veigar.session.spi.Resource 的子类,该类用于描述 一个受保护的资源,通常我们可以认为在一个 RESTful 接口系统中,其 methoduri 可用于唯一 标识一个资源。下面是常见的资源实现类:

public class MyResource implements Resource {
  private Integer id;
  private String method;                      // (1)
  private String uriPattern;                  // (2)
  private String name;
  private String remark;
  private List<String> attributes;            // (3)

  // setter & getter
}
  1. 用于定位资源的 http method

  2. 用于定位资源的 uri pattern,之所以用 pattern,是因为有些资源会用到 path variable,比如 /users/2/address ,那么在不同的 user id 情况下,uri 是不一样的,所以我们在定义资源的时候, 建议定义成 pattern: /users/{userId}/address。那么无论是 /users/2/address 还是 users/200/address 都可以识别为同一种资源。

  3. 代表访问该资源需要用到哪些权限

接着就需要实现权限判断的逻辑了,veigar 提供了一个 io.github.watertao.veigar.session.spi.SecurityHandler 接口:

@Component
public class HtRsrvSecurityHandler implements SecurityHandler {
  public HtRsrvResource identifyResource(String method, String uri, AuthenticationObject authObj) {
    // 根据本次请求的 method 和 uri 定位资源,并且根据项目自己的权限体系,设定 attributes
    return resource;
  }
}

需要做的很简单,实现 identifyResource 方法即可,该方法的目的就是根据请求的 methoduri 以及当前登录用户的会话对象,然后返回 Resource 对象。Resource 对象中最重要的是 attributes 属性,它代表了访问这个资源所需要具备的条件,它是一个字符串数组,我们应该还记得之前在用户认证 时提到的,每个用户登录成功后都会在 AuthenticationObject 中设置一个 attributes 属性,而 veigar 便是根据 AuthenticationObject 中的 attributes 和 Resource 中的 attributes 进行 匹配判断,只要存在交集便给予权限访问,否则便禁止。最常见的 attribute 就是角色。

Note

如果 SecurityHandler.identifyResource 返回了 null ,则代表该资源不受保护,可任意被访问(包括未登录), 若是返回的 resource 的 attributes 为 null 或 空数组,那么该资源就不可被任何人访问。

4.8. 审计日志

有些项目需要对用户的操作进行留痕审查,比如查看谁在什么时候对系统做了什么操作。要使用审计日志,需要添加 组件 veigar-audit-log

pom.xml
<dependency>
    <groupId>io.github.watertao</groupId>
    <artifactId>veigar-audit-log</artifactId>
    <version>2.2.0</version>
</dependency>

veigar 只会记录 method 为 POST/DELETE/PUT/PATCH 类型的请求,因为只有这些请求会 对系统的状态造成变化, 所以 GET 请求并不会记录。开发人员需要实现 io.github.watertao.veigar.auditlog.spi.AuditLogger 接口并将其注册为 Bean 即可:

@Component
public class MyAuditLogger implements AuditLogger {
  @Override
  public void log(
    AuthenticationObject authObj,           // (1)
    Resource resource,                      // (2)
    String reqVerb,                         // (3)
    String requestUri,                      // (4)
    String remoteIp,                        // (5)
    Object requestBody,                     // (6)
    Object responseBody,                    // (7)
    Throwable e,                            // (8)
    Long cost) {                            // (9)
    // 将审计信息保存到数据库或文件
  }
}
  1. 当前会话对象, Null 代表当前无登录用户

  2. 当前访问的资源, Null 代表当前资源并不受保护

  3. http method

  4. http uri

  5. 访问者 IP

  6. 请求体反序列化后的对象, 可空

  7. 响应内容,可空

  8. 操作异常,可空

  9. 请求耗时

默认情况下,veigar 不会记录状态码为 2xx 以外的请求,即操作失败的请求不做审计,因为该请求不会对 系统的状态造成变化。如果需要记录失败的请求可以在 application.properties 添加配置:

application.properties
auditLog.logFail = true

5. 构建部署包

通过以下命令可以构建用于部署的包:

mvn clean package

执行成功后,在 target 目录下会获得一个 jar 包:

├── myapp
|   ├── src
|   ├── target
|   |   └── myapp-x.x.x-SNAPSHOT.jar
│   └── pom.xml

这个 jar 包是 spring-boot-maven-plugin 插件通过 repackage 之后的可执行 jar,所以我们 可直接通过 java -jar 命令进行启动,在 linux 上的完整执行命令可参考:

6. 环境配置

开发环境与生产环境总是存在差别的,比如数据库的连接参数不同。 我们并不需要每次为了构建用于生产的 包而去修改 src/main/resources/application.properties 中的参数,因为 spring boot 提供 了配置文件外置覆盖的机制来解决这个问题。
在生产环境中,我们只需要建立一个与 jar 文件同级的 config 目录,并在 config 里放上 application.properties,该文件中的配置参数将会覆盖 jar 里面的 application.properties 中的:

├── myapp
|   ├── myapp-x.x.x-SNAPSHOT.jar
│   └── config
|       └── application.properties

以数据库和系统日志配置为例:

config/application.properties
spring.profiles.active = prod                 // (1)

spring.datasource.url = jdbc:mysql://3.3.3.3:3306/test?characterEncoding=UTF-8&zeroDateTimeBehavior=convertToNull
spring.datasource.username = test
spring.datasource.password = test

logging.path = /myapp/log
logging.file = myapp.log
logging.splitPattern = yyyy-MM-dd
logging.maxHistory = 30
  1. 注意需要把 profile 设置为 prod,这样系统日志会输出到文件,而非控制台

以上配置将会覆盖 jar 包中 resources/application.properties 中相应的属性。

7. 应用的启动和关闭

每次通过 java -jar 命令启动应用或通过 pskill 命令关闭应用,即繁琐又容易出错。
所以建议在部署目录中创建 startup.shshutdown.sh 两个脚本用于启动和关闭。

├── myapp
│   ├── myapp-x.x.x-SNAPSHOT.jar
│   ├── startup.sh                              // (1)
│   ├── shutdown.sh                             // (2)
│   └── config
│       └── application.properties
  1. 启动脚本

  2. 关闭脚本

startup.sh
#!/bin/bash
PROJECTDIR=$(cd $(dirname $0); pwd)
nohup java -jar myapp-x.x.x-SNAPSHOT.jar >/dev/null 2>&1 &
echo $! > $PROJECTDIR/.pid
shutdown.sh
#!/bin/bash
PROJECT_DIR=$(cd $(dirname $0); pwd)
kill $(cat $PROJECT_DIR/.pid)
Note

别忘了为这两个脚本添加执行权限。
chmod a+x startup.sh
chmod a+x shutdown.sh

8. 接口文档

8.1. swagger 接口文档

要使用 swagger 接口文档,需要添加组件 veigar-swagger 的依赖:

pom.xml
<dependency>
    <groupId>io.github.watertao</groupId>
    <artifactId>veigar-swagger</artifactId>
    <version>2.2.0</version>
</dependency>

同时在 application.properties 中启用 swagger 相关的配置:

application.properties
# 是否启用 swagger 接口文档
swagger.enable = true
# swagger 扫描哪些包下的 controller
swagger.api.basePackage = com.mycompany
# 接口文档的标题
swagger.api.title = Myapp Management Platform API Reference
# 接口的版本
swagger.api.version = 1.0.0

8.2. spring REST doc 接口文档

spring REST doc 是 spring 提供的文档生成组件,它基于单元测试,要使用该特性需要在 pom.xml 中 添加以下内容:

pom.xml
<dependencies>
  <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-test</artifactId>
  </dependency>
  <dependency>
    <groupId>org.springframework.restdocs</groupId>
    <artifactId>spring-restdocs-mockmvc</artifactId>
    <version>2.0.3.RELEASE</version>
    <scope>test</scope>
  </dependency>
</dependencies>

<build>
  <plugins>
    <plugin>
      <groupId>org.asciidoctor</groupId>
      <artifactId>asciidoctor-maven-plugin</artifactId>
      <version>1.5.3</version>
      <executions>
        <execution>
          <id>generate-docs</id>
          <phase>prepare-package</phase>
          <goals>
            <goal>process-asciidoc</goal>
          </goals>
          <configuration>
            <backend>html</backend>
            <doctype>book</doctype>
            <attributes>
              <stylesheet>asciidoc-style.css</stylesheet>             // (1)
            </attributes>
          </configuration>
        </execution>
      </executions>
      <dependencies>
        <dependency>
          <groupId>org.springframework.restdocs</groupId>
          <artifactId>spring-restdocs-asciidoctor</artifactId>
          <version>2.0.3.RELEASE</version>
        </dependency>
      </dependencies>
    </plugin>
  </plugins>
</build>
  1. veigar 提供了一个自定义的 css 样式,可从本项目源码的 assets 目录下获得,该 css 主要是对字体部分做了改动,以更适合中文显示。

编写单元测试:

@RunWith(SpringRunner.class)
@SpringBootTest
public class Test {

  @Rule
  public JunitRestDocumentation restDocumentation = new JunitRestDocumentation();

  protected MockMvc mockMvc;

  @Autowired
  private WebApplicationContext context;

  @Before
  public void setUp() {
    this.mockMvc = MockMvcBuilders.webAppContextSetup(this.context)
      .apply(documentationConfiguration(this.restDocumentation))
      .build();
  }

  @Test
  @Transactional
  @Rollback
  public void get() {

    this.mockMvc.perform(
      get("/a/b/{id}", 123)
      .accept(MediaType.APPLICATION_JSON)
    )
    .andExpect(status().isOk())
    .andDo(document("get-xx",
      pathParameters(
        parameterWithName("id").description("ID")
      ),
      responseFields(
        fieldWithPath("id").type(JsonFieldType.NUMBER).description("ID")
      )
    ));

  }

}

当单元测试执行通过后,在 target/generated-snippets 下会生成 adoc 文档片段,接着我们在 src/main/asciidoc 下建立 API 主文档:

├── myapp
│   ├── src
│   │   └── main
│   │       └── asciidoc
│   │           ├── api-reference.adoc        // (1)
│   │           ├── common.adoc               // (2)
│   │           └── asciidoc-style.css        // (3)
│   └── pom.xml
  1. 主文档

  2. 通用部分,比如介绍动词,状态码,异常处理,会话管理等

  3. 自定义的样式,在 pom.xml 中需要配置

在主文档中引入前面单元测试生成的 snippets:

api-reference.adoc
operation::get-xx[snippets='http-request,path-parameters,http-response,response-fields']

接着执行 mvn package 便会在 target/generated-docs 下生成 api-reference.html 。

9. 单元测试

(待补充)

10. 完整配置参考

application.properties
# 运行模式,在开发环境设置为 dev, 在生产模式设置为 prod
spring.profiles.active = dev
# base package
app.basePackage = com.mycompany.myapp
# 服务端口,缺省 8080
server.port = 8080

# CORS 允许的域,支持逗号分割多个域
cors.allowedOrigins = *
# 允许跨域访问的 method
cors.allowedMethods = POST,PUT,GET,PATCH,DELETE,OPTIONS
# 允许跨域访问时的请求头
cors.allowedHeaders = x-auth-token,if-modified-since
# 允许跨域访问时响应中可访问的头
cors.exposedHeaders = x-total-count,x-auth-token
# preflight (OPTIONS请求) 的缓存时长
cors.maxAge = 1728000

# 日期对象序列化为 JSON 时使用的时区,默认采用操作系统的
spring.jackson.time-zone = GMT+8
# 对象序列化为 JSON 时是否格式化显示,默认是单行的
spring.jackson.serialization.indent_output = true

# 是否启用 swagger 接口文档
swagger.enable = true
# swagger 扫描哪些包下的 controller
swagger.api.basePackage = com.mycompany
# 接口文档的标题
swagger.api.title = Myapp Management Platform API Reference
# 接口的版本
swagger.api.version = 1.0.0

# 系统日志 root 输出级别,缺省为 INFO
logging.root.level = INFO
# 系统日志输出 pattern,缺省为 %d{yyyy/MM/dd-HH:mm:ss SSS} %-5level - %msg %n
logging.encodePattern = %d{yyyy/MM/dd-HH:mm:ss SSS} %-5level - %msg %n
# 在运行模式为 dev 时,只向控制台输出日志,而为 prod 时,只向文件输出日志,当设置为 prod 时还支持以下配置
# 日志文件的输出目录,缺省输出到 jar 同级目录
logging.path = /myapp/log
# 日志文件的文件名,缺省为 spring.log
logging.file = myapp.log
# 日志文件按时间切割的模式,缺省为 yyyy-MM-dd
logging.splitPattern = yyyy-MM-dd
# 日志文件保留的个数,缺省为 30
logging.maxHistory = 30

# redis 配置
spring.redis.host = localhost
spring.redis.port = 6379

# 数据库配置
spring.datasource.driverClassName = com.mysql.jdbc.Driver
spring.datasource.url = jdbc:mysql://localhost:3306/test?characterEncoding=UTF-8&zeroDateTimeBehavior=convertToNull
spring.datasource.username = test
spring.datasource.password = test
spring.datasource.maxActive = 20

# 审计日志配置
# 是否记录失败的请求,缺省 false
auditLog.logFail = false
# 请求方 ip 是否需要从 Header 中获取,因为如果应用在负载均衡设备之后,通常负载均衡设备会将真实 IP 设置到 HTTP 头中转发过来,在此处可设置头名称
auditLog.ip.header =  X-Real-IP

Versions

Version
2.2.0
2.1.1