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
<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>
-
继承
veigar-parent
2.2.2. 依赖 veigar-core
若项目无法依赖 veigar-parent
(比如需要依赖其他 parent) ,那么通过以下方式可达到同样效果:
<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>
-
依赖
veigar-core
-
添加插件
spring-boot-maven-plugin
2.3. 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);
}
}
-
自己项目 package
-
告诉 spring boot 去扫描 veigar 相关的组件,这是必须的。
Note
|
需要注意的是, |
2.4. application.properties
spring.profiles.active = dev # (1)
app.basePackage = com.mycompany.myapp # (2)
server.port = 8080 # (3)
cors.allowedOrigins = * # (4)
-
开发环境下设置为
dev
, 生产环境下设置为prod
-
项目级别的 package(公司级别下一级),某些组件需要读取并使用这个参数
-
服务端口
-
CORS 允许的来源
2.5. 第一个接口
创建 src/main/java/com/mycompany/myapp/controller/TestController.java
├── myapp | ├── src | | └── main | | ├── java | | | └── com | | | └── mycompany | | | └── controller | | | └── TestCOntroller.java | | └── resources │ └── pom.xml
@RestController // (1)
public class TestController {
@GetMapping("/test") // (2)
public Object test() {
Map map.put("a", "b");
return map;
}
}
-
每个 RESTful 接口类都需要使用
@RestController
注解 -
每个 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 获取
GET /users?name=watertao HTTP/1.1
@GetMapping("/users")
public void test(
@RequestParam("name") String name // (1)
) {
// name = "watertao";
}
-
使用
@RequestParam
获取 query parameter
4.1.2. path variable 获取
GET /users/133 HTTP/1.1
@GetMapping("/users/{userId}") // (1)
public void test(
@PathVariable("userId") Integer userId // (2)
) {
// userId = 133;
}
-
URI 的定义中需要指定 path variable 参数名,本例中为
{userId}
-
使用
@PathVariable
获取 path variable, 注解的参数需要与 URI 中{userId}
内的定义相对应
4.1.3. request body 获取
POST /users HTTP/1.1 Content-Type: application/json;charset=UTF-8 { "name": "watertao" }
@PostMapping("/users")
public void test(
@RequestBody User user // (1)
) {
// user.getName() = "watertao"
}
-
使用
@RequestBody
注解告诉 spring boot 将 JSON 反序列化为对象
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
:
POST /users HTTP/1.1 Content-Type: application/json;charset=UTF-8 { ... }
4.1.5. 请求校验
veigar 集成了 Hibernator-validator 作为 bean validation 的实现。所以我们可以很方便的对请求体中的 JSON 进行验证。
public class User {
@NotEmpty // (1)
private String name;
public String getName() {...}
public void setName(String name) {...}
}
-
通过注解
@NotEmpty
确保name
属性不可为空
@PostMapping("/users")
public Object test(
@Valid @RequestBody User user // (1)
) {
}
-
通过添加注解
@Valid
告知 spring boot 对user
对象进行校验,若 JSON 中 name 属性为空,则会抛出校验异常
bean validation 以及 hibernate-validator 所支持的校验注解可参考:
bean validation
hibernate validator
4.1.6. CORS 跨域
在 application.properties
中添加以下配置可支持浏览器跨域访问:
cors.allowedOrigins = http://localhost:8000
通过逗号分隔,可以支持多个域:
cors.allowedOrigins = http://localhost:8000,http://10.10.10.10
或者通过 *
支持所有的域:
cors.allowedOrigins = *
但需要注意的是,如果客户端的请求中包含了 credential,那么就不可使用 *,必须指定一个确定的域。
除了域以外,veigar 还支持其他 CORS 相关的配置,但绝大部分情况下不必对其进行设置:
# 允许跨域访问的 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 序列化时更美观:
spring.jackson.serialization.indent_output = true
4.1.8. 日期
veigar 会将日期以 ISO-8601 兼容的格式来序列化日期,如 2019-01-09T10:41:44.000+0800
,我们可以通过以下参数设置时区及格式:
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:
logging.root.level = INFO # (1)
logging.encodePattern = %d{yyyy/MM/dd-HH:mm:ss SSS} %-5level - %msg %n # (2)
-
root 输出级别,缺省为
INFO
-
输出的 pattern,缺省为
%d{yyyy/MM/dd-HH:mm:ss SSS} %-5level - %msg %n
根据 application.properties
中的属性 spring.profiles.active
取值不同,日志输出的行为也会有所不同:
- dev
-
日志只会输出到控制台,不会输出到文件。
- prod
-
日志只会输出到文件,不会输出到控制台。
在这种模式下,veigar 还支持以下配置:
logging.path = /myapp/log # (1)
logging.file = myapp.log # (2)
logging.splitPattern = yyyy-MM-dd_HH # (3)
logging.maxHistory = 30 # (4)
-
日志文件输出的目录,缺省为 jar 包所在的目录
-
日志文件的文件名,缺省为 spring.log
-
日志文件按时间切割的模式,缺省为
yyyy-MM-dd
(即按天切割) -
日志文件保存的文件个数,缺省为 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 来替换默认风格。比如:
@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)
}
-
异常对应的状态码
-
状态码对应的标准描述语(与 HTTP 规范兼容)
-
自定义的异常描述
-
附加的异常描述补充
Note
|
对于客户端而言,状态码为 |
4.5. 消息国际化
若要在 veigar 项目中使用消息国际化的特性,需要在 src/main/resources/message 下创建不同语言的 消息资源文件,下面以中文和英文为例:
├── myapp | ├── src | | └── main | | ├── java | | └── resources | | └── message | | ├── message_en.properties // (1) | | └── message_zh.properties // (2) │ └── pom.xml
-
英文消息资源文件
-
中文消息资源文件
分别为两个资源文件添加属性名为 test.name
的消息:
test.name = I'm English,param_1 is {0} and param_2 is {1}
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)
}
}
-
注入 LocaleMessage bean
-
调用 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
的依赖;
<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 驱动的依赖:
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.47</version>
</dependency>
在 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)
-
连接池的最大连接数
做完了以上这些工作,我们就可以在项目中使用 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 {
...
}
-
@Component
注解是为了给 Mapper 定义一个 bean name,强烈建议设置成接口的全限定名,这么做可以避免不同 package 下相同类名的 Mapper 接口产生冲突。 -
@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 插件的依赖:
<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
<?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>
-
开发环境本地的 jdbc 驱动绝对路径
-
需要生成的表
我们可以复制以上内容到 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
|
4.6.6. 分页处理
veigar 使用 pagehelper
进行分页的处理,要使用该功能需要在 application.properties
中指定 sql 方言,缺省为 mysql
:
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)
}
}
-
在进行任意的 sql 查询之前,先通过
PageHelper.startPage
设置本次分页的起始页和页大小 -
执行 Mapper 的查询方法
-
用
PageInfo
类构建一个实例,传入上一步返回的结果集,最终获得的就是一个分页结果对象 -
除了
startPage
,PageHelper 还提供了offsetPage
, 前者是按页数作为第一个参数,后者是按照游标作为第一个参数
在调用了 PageHelper 类的 startPage
和 offsetPage
方法之后,紧接着后面的 mapper 查询就会被 PageHelper 进行二次处理,它会同时 发起一个 count 查询和数据查询,并且返回的结果集是一个被改造过的 List,该 List 具备分页相关的一些属性。因此我们 new PageInfo(list)
的 时候,传入的 list 参数必须是改造过的 List,否则无法正确读取分页信息。
4.6.7. 在日志中打印 sql
在 veigar 中打印 sql 需要在 application.properties
中将 Mapper 类的日志级别调整到 DEBUG, 比如:
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
的依赖:
<dependency>
<groupId>io.github.watertao</groupId>
<artifactId>veigar-session</artifactId>
<version>2.2.0</version>
</dependency>
同时还需要依赖一个会话序列化的实现组件,veigar 目前提供了两种方案:
4.7.1. 会话序列化至内存
对于简单的项目,我们完全可以将 session 保存在 jvm 内存中,采用这种方式需要添加依赖:
<dependency>
<groupId>io.github.watertao</groupId>
<artifactId>veigar-session-map</artifactId>
<version>2.2.0</version>
</dependency>
这种方式虽然简单,但会有两个弊端:
首先,负载均衡时无法做到多个应用间共享 session
其次,应用重启后,session 将丢失
4.7.2. 会话序列化至 redis
对于需要负载均衡的项目,我们往往会将会话保存在外部缓存中,比如 redis,采用这种方式需要添加依赖:
<dependency>
<groupId>io.github.watertao</groupId>
<artifactId>veigar-session-redis</artifactId>
<version>2.2.0</version>
</dependency>
同时我们需要在 application.properties
中添加 redis 的连接配置:
spring.redis.host = localhost
spring.redis.port = 6379
Note
|
需要注意的是,Session 的序列化实现组件只能依赖一个,也就是说不能同时依赖 |
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
组件:
<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;
}
}
-
定义用户登录时的请求
method
和uri
。 -
定义登录请求的报文结构,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
|
并非一定要通过 |
4.7.7. 权限校验
当一个请求发起时,如何判断当前用户是否具有访问的权限呢? 不同的项目往往有不同的权限处理逻辑,有的 是基于角色的,有的可能基于复杂的组织机构树,veigar 抽象并提供了一组接口用于实现不同项目自己的 权限判断逻辑。
首先我们需要实现 io.github.watertao.veigar.session.spi.Resource
的子类,该类用于描述 一个受保护的资源,通常我们可以认为在一个 RESTful 接口系统中,其 method
和 uri
可用于唯一 标识一个资源。下面是常见的资源实现类:
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
}
-
用于定位资源的 http method
-
用于定位资源的 uri pattern,之所以用 pattern,是因为有些资源会用到 path variable,比如
/users/2/address
,那么在不同的 user id 情况下,uri 是不一样的,所以我们在定义资源的时候, 建议定义成 pattern:/users/{userId}/address
。那么无论是/users/2/address
还是users/200/address
都可以识别为同一种资源。 -
代表访问该资源需要用到哪些权限
接着就需要实现权限判断的逻辑了,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
方法即可,该方法的目的就是根据请求的 method
和 uri
以及当前登录用户的会话对象,然后返回 Resource
对象。Resource 对象中最重要的是 attributes
属性,它代表了访问这个资源所需要具备的条件,它是一个字符串数组,我们应该还记得之前在用户认证 时提到的,每个用户登录成功后都会在 AuthenticationObject
中设置一个 attributes
属性,而 veigar 便是根据 AuthenticationObject 中的 attributes 和 Resource 中的 attributes 进行 匹配判断,只要存在交集便给予权限访问,否则便禁止。最常见的 attribute 就是角色。
Note
|
如果 |
4.8. 审计日志
有些项目需要对用户的操作进行留痕审查,比如查看谁在什么时候对系统做了什么操作。要使用审计日志,需要添加 组件 veigar-audit-log
:
<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)
// 将审计信息保存到数据库或文件
}
}
-
当前会话对象, Null 代表当前无登录用户
-
当前访问的资源, Null 代表当前资源并不受保护
-
http method
-
http uri
-
访问者 IP
-
请求体反序列化后的对象, 可空
-
响应内容,可空
-
操作异常,可空
-
请求耗时
默认情况下,veigar 不会记录状态码为 2xx 以外的请求,即操作失败的请求不做审计,因为该请求不会对 系统的状态造成变化。如果需要记录失败的请求可以在 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
以数据库和系统日志配置为例:
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
-
注意需要把 profile 设置为 prod,这样系统日志会输出到文件,而非控制台
以上配置将会覆盖 jar 包中 resources/application.properties 中相应的属性。
7. 应用的启动和关闭
每次通过 java -jar
命令启动应用或通过 ps
及 kill
命令关闭应用,即繁琐又容易出错。
所以建议在部署目录中创建 startup.sh
和 shutdown.sh
两个脚本用于启动和关闭。
├── myapp │ ├── myapp-x.x.x-SNAPSHOT.jar │ ├── startup.sh // (1) │ ├── shutdown.sh // (2) │ └── config │ └── application.properties
-
启动脚本
-
关闭脚本
#!/bin/bash
PROJECTDIR=$(cd $(dirname $0); pwd)
nohup java -jar myapp-x.x.x-SNAPSHOT.jar >/dev/null 2>&1 &
echo $! > $PROJECTDIR/.pid
#!/bin/bash
PROJECT_DIR=$(cd $(dirname $0); pwd)
kill $(cat $PROJECT_DIR/.pid)
Note
|
别忘了为这两个脚本添加执行权限。 |
8. 接口文档
8.1. swagger 接口文档
要使用 swagger 接口文档,需要添加组件 veigar-swagger
的依赖:
<dependency>
<groupId>io.github.watertao</groupId>
<artifactId>veigar-swagger</artifactId>
<version>2.2.0</version>
</dependency>
同时在 application.properties
中启用 swagger 相关的配置:
# 是否启用 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
中 添加以下内容:
<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>
-
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
-
主文档
-
通用部分,比如介绍动词,状态码,异常处理,会话管理等
-
自定义的样式,在 pom.xml 中需要配置
在主文档中引入前面单元测试生成的 snippets:
operation::get-xx[snippets='http-request,path-parameters,http-response,response-fields']
接着执行 mvn package 便会在 target/generated-docs 下生成 api-reference.html 。
9. 单元测试
(待补充)
10. 完整配置参考
# 运行模式,在开发环境设置为 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