优化阅读格式

This commit is contained in:
heibaiying
2019-07-31 17:39:13 +08:00
parent 3020ff8efa
commit 246e5cfca1
55 changed files with 10521 additions and 10482 deletions

View File

@ -1,5 +1,6 @@
# spring boot actuator
# spring boot actuator
## 目录<br/>
<a href="#一用例涉及到的概念综述">一、用例涉及到的概念综述</a><br/>
&nbsp;&nbsp;&nbsp;&nbsp;<a href="#11-端点">1.1 端点</a><br/>
&nbsp;&nbsp;&nbsp;&nbsp;<a href="#12-启用端点">1.2 启用端点</a><br/>
@ -20,315 +21,315 @@
## 一、用例涉及到的概念综述
### 1.1 端点
执行器端点endpoints可用于监控应用及与应用进行交互Spring Boot包含很多内置的端点你也可以添加自己的。例如health端点提供了应用的基本健康信息。 端点暴露的方式取决于你采用的技术类型大部分应用选择HTTP监控端点的ID映射到一个URL。例如health端点默认映射到/health。
下面的端点都是可用的:
| ID | 描述 | 是否敏感 |
| ----------- | ------------------------------------------------------------ | -------- |
| actuator | 为其他端点提供基于超文本的导航页面需要添加Spring HATEOAS依赖 | true |
| autoconfig | 显示一个自动配置类的报告,该报告展示所有自动配置候选者及它们被应用或未被应用的原因 | true |
| beans | 显示一个应用中所有Spring Beans的完整列表 | true |
| configprops | 显示一个所有@ConfigurationProperties的集合列表 | true |
| dump | 执行一个线程转储 | true |
| env | 暴露来自Spring ConfigurableEnvironment的属性 | true |
| flyway | 显示数据库迁移路径,如果有的话 | true |
| health | 展示应用的健康信息(当使用一个未认证连接访问时显示一个简单的'status',使用认证连接访问则显示全部信息详情) | false |
| info | 示任意的应用信息 | false |
| liquibase | 展示任何Liquibase数据库迁移路径如果有的话 | true |
| metrics | 展示当前应用的'metrics'信息 | true |
| mappings | 显示一个所有@RequestMapping路径的集合列表 | true |
| shutdown | 允许应用以优雅的方式关闭(默认情况下不启用) | true |
| trace | 显示trace信息默认为最新的100条HTTP请求 | true |
如果使用Spring MVC你还可以使用以下端点
| ID | 描述 | 是否敏感 |
| -------- | ------------------------------------------------------------ | -------- |
| docs | 展示Actuator的文档包括示例请求和响应需添加spring-boot-actuator-docs依赖 | false |
| heapdump | 返回一个GZip压缩的hprof堆转储文件 | true |
| jolokia | 通过HTTP暴露JMX beans依赖Jolokia | true |
| logfile | 返回日志文件内容如果设置logging.file或logging.path属性支持使用HTTP Range头接收日志文件内容的部分信息 | |
根据端点暴露的方式sensitive属性可用做安全提示例如在使用HTTP访问敏感sensitive端点时需要提供用户名/密码如果没有启用web安全可能会简化为禁止访问该端点
### 1.2 启用端点
默认情况下除了以外的所有端点shutdown都已启用。要配置端点的启用请使用其management.endpoint.<id>.enabled属性。以下示例启用shutdown端点
```properties
management.endpoint.shutdown.enabled = true
```
### 1.3 暴露端点
由于端点可能包含敏感信息,因此应仔细考虑何时公开它们。下表显示了内置端点的默认曝光情况:
| ID | JMX | Web |
| -------------- | ----- | ---- |
| auditevents | 是 | 没有 |
| beans | 是 | 没有 |
| conditions | 是 | 没有 |
| configprops | 是 | 没有 |
| env | 是 | 没有 |
| flyway | 是 | 没有 |
| health | 是 | 是 |
| heapdump | N / A | 没有 |
| httptrace | 是 | 没有 |
| info | 是 | 是 |
| jolokia | N / A | 没有 |
| logfile | N / A | 没有 |
| loggers | 是 | 没有 |
| liquibase | 是 | 没有 |
| metrics | 是 | 没有 |
| mappings | | 没有 |
| prometheus | N / A | 没有 |
| scheduledtasks | 是 | 没有 |
| sessions | 是 | 没有 |
| shutdown | 是 | 没有 |
| threaddump | 是 | 没有 |
**可以选择是否暴露端点include或者排除端点exclude,其中排除优先于暴露:**
| 属性 | 默认 |
| ----------------------------------------- | ------------ |
| management.endpoints.jmx.exposure.exclude | |
| management.endpoints.jmx.exposure.include | * |
| management.endpoints.web.exposure.exclude | |
| management.endpoints.web.exposure.include | info, health |
### 1.4 健康检查信息
您可以使用健康信息来检查正在运行的应用程序的状态。health端点公开的信息取决于management.endpoint.health.show-details可以使用以下值之一配置的属性
| 名称 | 描述 |
| --------------- | ------------------------------------------------------------ |
| never | 细节永远不会显示。 |
| when-authorized | 详细信息仅向授权用户显示。授权角色可以使用配置 management.endpoint.health.roles。 |
| always | 详细信息显示给所有用户。 |
## 二、项目说明
#### 1.1 项目结构说明
1. CustomHealthIndicator 自定义健康指标
2. CustomHealthAggregator自定义健康聚合规则
3. CustomEndPoint自定义端点。
<div align="center"> <img src="https://github.com/heibaiying/spring-samples-for-all/blob/master/pictures/spring-boot-actuator.png"/> </div>
#### 1.2 主要依赖
```xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
```
#### 1.3 项目配置
```yaml
management:
endpoints:
web:
exposure:
# 这里用* 代表暴露所有端点只是为了观察效果,实际中按照需进行端点暴露
include: "*"
endpoint:
health:
# 详细信息显示给所有用户。
show-details: always
health:
status:
http-mapping:
# 自定义健康检查返回状态码对应的http状态码
FATAL: 503
```
#### 1.4 查看监控状态
导入actuator 的 start 并进行配置后访问http://127.0.0.1:8080/actuator/health 就可以看到对应的项目监控状态。
<div align="center"> <img src="https://github.com/heibaiying/spring-samples-for-all/blob/master/pictures/health.png"/> </div>
需要注意的是这里的监控状态根据实际项目所用到的技术不同而不同。因为以下HealthIndicators情况在适当时由Spring Boot自动配置的
| 名称 | 描述 |
| ------------------------------------------------------------ | -------------------------------- |
| [CassandraHealthIndicator](https://github.com/spring-projects/spring-boot/tree/v2.0.1.RELEASE/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/cassandra/CassandraHealthIndicator.java) | 检查Cassandra数据库是否启动。 |
| [DiskSpaceHealthIndicator](https://github.com/spring-projects/spring-boot/tree/v2.0.1.RELEASE/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/system/DiskSpaceHealthIndicator.java) | 检查磁盘空间不足。 |
| [DataSourceHealthIndicator](https://github.com/spring-projects/spring-boot/tree/v2.0.1.RELEASE/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/jdbc/DataSourceHealthIndicator.java) | 检查是否可以获得连接DataSource。 |
| [ElasticsearchHealthIndicator](https://github.com/spring-projects/spring-boot/tree/v2.0.1.RELEASE/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/elasticsearch/ElasticsearchHealthIndicator.java) | 检查Elasticsearch集群是否启动。 |
| [InfluxDbHealthIndicator](https://github.com/spring-projects/spring-boot/tree/v2.0.1.RELEASE/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/influx/InfluxDbHealthIndicator.java) | 检查InfluxDB服务器是否启动。 |
| [JmsHealthIndicator](https://github.com/spring-projects/spring-boot/tree/v2.0.1.RELEASE/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/jms/JmsHealthIndicator.java) | 检查JMS代理是否启动。 |
| [MailHealthIndicator](https://github.com/spring-projects/spring-boot/tree/v2.0.1.RELEASE/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/mail/MailHealthIndicator.java) | 检查邮件服务器是否启动。 |
| [MongoHealthIndicator](https://github.com/spring-projects/spring-boot/tree/v2.0.1.RELEASE/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/mongo/MongoHealthIndicator.java) | 检查Mongo数据库是否启动。 |
| [Neo4jHealthIndicator](https://github.com/spring-projects/spring-boot/tree/v2.0.1.RELEASE/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/neo4j/Neo4jHealthIndicator.java) | 检查Neo4j服务器是否启动。 |
| [RabbitHealthIndicator](https://github.com/spring-projects/spring-boot/tree/v2.0.1.RELEASE/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/amqp/RabbitHealthIndicator.java) | 检查Rabbit服务器是否启动。 |
| [RedisHealthIndicator](https://github.com/spring-projects/spring-boot/tree/v2.0.1.RELEASE/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/redis/RedisHealthIndicator.java) | 检查Redis服务器是否启动。 |
| [SolrHealthIndicator](https://github.com/spring-projects/spring-boot/tree/v2.0.1.RELEASE/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/solr/SolrHealthIndicator.java) | 检查Solr服务器是否已启动。 |
## 三、自定义健康检查指标
```java
/**
* @author : heibaiying
* @description : 自定义健康检查指标
*/
@Component
public class CustomHealthIndicator implements HealthIndicator {
@Override
public Health health() {
double random = Math.random();
// 这里用随机数模拟健康检查的结果
if (random > 0.5) {
return Health.status("FATAL").withDetail("error code", "某健康专项检查失败").build();
} else {
return Health.up().withDetail("success code", "自定义检查一切正常").build();
}
}
}
```
自定义检查通过的情况下:
<div align="center"> <img src="https://github.com/heibaiying/spring-samples-for-all/blob/master/pictures/actuator-health-up.png"/> </div>
自定义检查失败的情况:
<div align="center"> <img src="https://github.com/heibaiying/spring-samples-for-all/blob/master/pictures/health-fatal-200.png"/> </div>
这里我们可以看到自定义检查不论是否通过都不会影响整体的status,两种情况下都是status都是“up”。如果我们想通过自定义的检查检查去影响最终的检查结果比如我们健康检查针对的是支付业务在支付业务的不可用的情况下我们就认为整个服务是不可用的。这个时候就需要实现自定义实现健康状态的聚合。
## 四、自定义健康状态聚合规则
```java
/**
* @author : heibaiying
* @description : 对所有的自定义健康指标进行聚合,按照自定义规则返回总和健康状态
*/
@Component
public class CustomHealthAggregator implements HealthAggregator {
@Override
public Health aggregate(Map<String, Health> healths) {
for (Health health : healths.values()) {
// 聚合规则可以自定义,这里假设我们自定义的监控状态中有一项FATAL,就认为整个服务都是不可用的,否则认为整个服务是可用的
if (health.getStatus().getCode().equals("FATAL")) {
return Health.status("FATAL").withDetail("error code", "综合判断后服务宕机").build();
}
}
return Health.up().build();
}
}
```
当我们自定义健康检查不通过时候的结果如下:
<div align="center"> <img src="https://github.com/heibaiying/spring-samples-for-all/blob/master/pictures/actuator-heath-503.png"/> </div>
这里需要注意的是返回我们自定义的聚合状态的时候状态码也变成了503,这是我们在配置文件中进行定义的:
```properties
management.health.status.http-mapping.FATAL = 503
```
下表显示了内置状态的默认状态映射:
| Status | Mapping |
| -------------- | -------------------------------------------- |
| DOWN | SERVICE_UNAVAILABLE (503) |
| OUT_OF_SERVICE | SERVICE_UNAVAILABLE (503) |
| UP | No mapping by default, so http status is 200 |
| UNKNOWN | No mapping by default, so http status is 200 |
## 五、@Endpoint自定义端点
#### 5.1 自定义端点
spring boot 支持使用@Endpoint来自定义端点暴露应用信息。这里我们采用第三方sigar来暴露服务所在硬件的监控信息。
Sigar是Hyperic-hq产品的基础包是Hyperic HQ主要的数据收集组件。Sigar.jar的底层是用C语言编写的它通过本地方法来调用操作系统API来获取系统相关数据 [jar包下载地址](https://sourceforge.net/projects/sigar/)。
Sigar为不同平台提供了不同的库文件,下载后需要将库文件放到服务所在主机的对应位置:
- Windows下配置根据自己的操作系统版本选择sigar-amd64-winnt.dll或sigar-x86-winnt.dll拷贝到C:\Windows\System32中
- Linux下配置将libsigar-amd64-linux.so或libsigar-x86-linux.so拷贝到/usr/lib64或/lib64或/lib或/usr/lib目录下如果不起作用还需要sudochmod 744修改libsigar-amd64-linux.so文件权限
```java
@Endpoint(id = "customEndPoint")
@Component
public class CustomEndPoint {
@ReadOperation
public Map<String, Object> getCupInfo() throws SigarException {
Map<String, Object> cupInfoMap = new LinkedHashMap<>();
Sigar sigar = new Sigar();
CpuInfo infoList[] = sigar.getCpuInfoList();
CpuPerc[] cpuList = sigar.getCpuPercList();
for (int i = 0; i < infoList.length; i++) {
CpuInfo info = infoList[i];
cupInfoMap.put("CPU " + i + " 的总量MHz", info.getMhz()); // CPU的总量MHz
cupInfoMap.put("CPU " + i + " 生产商", info.getVendor()); // 获得CPU的生产商Intel
cupInfoMap.put("CPU " + i + " 类别", info.getModel()); // 获得CPU的类别Core
cupInfoMap.put("CPU " + i + " 缓存数量", info.getCacheSize()); // 缓冲存储器数量
cupInfoMap.put("CPU " + i + " 用户使用率", CpuPerc.format(cpuList[i].getUser())); // 用户使用率
cupInfoMap.put("CPU " + i + " 系统使用率", CpuPerc.format(cpuList[i].getSys())); // 系统使用
cupInfoMap.put("CPU " + i + " 当前等待率", CpuPerc.format(cpuList[i].getWait())); // 当前等待
cupInfoMap.put("CPU " + i + " 当前错误率", CpuPerc.format(cpuList[i].getNice())); // 当前错误
cupInfoMap.put("CPU " + i + " 当前空闲率", CpuPerc.format(cpuList[i].getIdle())); // 当前空闲
cupInfoMap.put("CPU " + i + " 总的使用率", CpuPerc.format(cpuList[i].getCombined()));// 总的使用率
}
return cupInfoMap;
}
}
```
其中可用的方法注解由http操作决定
| operation | HTTP方法 |
| ---------------- | -------- |
| @ReadOperation | GET |
| @WriteOperation | POST |
| @DeleteOperation | DELETE |
#### 5.2 访问自定义端点http://127.0.0.1:8080/actuator/customEndPoint
<div align="center"> <img src="https://github.com/heibaiying/spring-samples-for-all/blob/master/pictures/actuator-customEndPoint.png"/> </div>
关于 Sigar 的 更多监控参数可以参考博客:[java读取计算机CPU、内存等信息Sigar使用](https://blog.csdn.net/wudiazu/article/details/73829324)
Sigar 下载包中也提供了各种参数的参考用例:
## 一、用例涉及到的概念综述
### 1.1 端点
执行器端点endpoints可用于监控应用及与应用进行交互Spring Boot 包含很多内置的端点你也可以添加自己的。例如health 端点提供了应用的基本健康信息。 端点暴露的方式取决于你采用的技术类型,大部分应用选择 HTTP 监控,端点的 ID 映射到一个 URL。例如health 端点默认映射到/health。
下面的端点都是可用的:
| ID | 描述 | 是否敏感 |
| ----------- | ------------------------------------------------------------ | -------- |
| actuator | 为其他端点提供基于超文本的导航页面,需要添加 Spring HATEOAS 依赖 | true |
| autoconfig | 显示一个自动配置类的报告,该报告展示所有自动配置候选者及它们被应用或未被应用的原因 | true |
| beans | 显示一个应用中所有 Spring Beans 的完整列表 | true |
| configprops | 显示一个所有@ConfigurationProperties 的集合列表 | true |
| dump | 执行一个线程转储 | true |
| env | 暴露来自 Spring ConfigurableEnvironment 的属性 | true |
| flyway | 显示数据库迁移路径,如果有的话 | true |
| health | 展示应用的健康信息(当使用一个未认证连接访问时显示一个简单的'status',使用认证连接访问则显示全部信息详情) | false |
| info | 显示任意的应用信息 | false |
| liquibase | 示任何 Liquibase 数据库迁移路径,如果有的话 | true |
| metrics | 展示当前应用的'metrics'信息 | true |
| mappings | 显示一个所有@RequestMapping 路径的集合列表 | true |
| shutdown | 允许应用以优雅的方式关闭(默认情况下不启用) | true |
| trace | 显示 trace 信息(默认为最新的 100 条 HTTP 请求) | true |
如果使用 Spring MVC你还可以使用以下端点
| ID | 描述 | 是否敏感 |
| -------- | ------------------------------------------------------------ | -------- |
| docs | 展示 Actuator 的文档,包括示例请求和响应,需添加 spring-boot-actuator-docs 依赖 | false |
| heapdump | 返回一个 GZip 压缩的 hprof 堆转储文件 | true |
| jolokia | 通过 HTTP 暴露 JMX beans依赖 Jolokia | true |
| logfile | 返回日志文件内容(如果设置 logging.file 或 logging.path 属性),支持使用 HTTP Range 头接收日志文件内容的部分信息 | |
根据端点暴露的方式sensitive 属性可用做安全提示,例如,在使用 HTTP 访问敏感sensitive端点时需要提供用户名/密码(如果没有启用 web 安全,可能会简化为禁止访问该端点)。
### 1.2 启用端点
默认情况下,除了以外的所有端点 shutdown 都已启用。要配置端点的启用,请使用其 management.endpoint.<id>.enabled 属性。以下示例启用 shutdown 端点:
```properties
management.endpoint.shutdown.enabled = true
```
### 1.3 暴露端点
由于端点可能包含敏感信息,因此应仔细考虑何时公开它们。下表显示了内置端点的默认曝光情况:
| ID | JMX | Web |
| -------------- | ----- | ---- |
| auditevents | 是 | 没有 |
| beans | 是 | 没有 |
| conditions | 是 | 没有 |
| configprops | 是 | 没有 |
| env | 是 | 没有 |
| flyway | 是 | 没有 |
| health | 是 | |
| heapdump | N / A | 没有 |
| httptrace | | 没有 |
| info | 是 | |
| jolokia | N / A | 没有 |
| logfile | N / A | 没有 |
| loggers | | 没有 |
| liquibase | 是 | 没有 |
| metrics | 是 | 没有 |
| mappings | 是 | 没有 |
| prometheus | N / A | 没有 |
| scheduledtasks | 是 | 没有 |
| sessions | 是 | 没有 |
| shutdown | 是 | 没有 |
| threaddump | 是 | 没有 |
**可以选择是否暴露端点include或者排除端点exclude,其中排除优先于暴露:**
| 属性 | 默认 |
| ----------------------------------------- | ------------ |
| management.endpoints.jmx.exposure.exclude | |
| management.endpoints.jmx.exposure.include | * |
| management.endpoints.web.exposure.exclude | |
| management.endpoints.web.exposure.include | info, health |
### 1.4 健康检查信息
您可以使用健康信息来检查正在运行的应用程序的状态。health 端点公开的信息取决于 management.endpoint.health.show-details 可以使用以下值之一配置的属性:
| 名称 | 描述 |
| --------------- | ------------------------------------------------------------ |
| never | 细节永远不会显示。 |
| when-authorized | 详细信息仅向授权用户显示。授权角色可以使用配置 management.endpoint.health.roles。 |
| always | 详细信息显示给所有用户。 |
## 二、项目说明
#### 1.1 项目结构说明
1. CustomHealthIndicator 自定义健康指标;
2. CustomHealthAggregator自定义健康聚合规则
3. CustomEndPoint自定义端点。
<div align="center"> <img src="https://github.com/heibaiying/spring-samples-for-all/blob/master/pictures/spring-boot-actuator.png"/> </div>
#### 1.2 主要依赖
```xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
```
#### 1.3 项目配置
```yaml
management:
endpoints:
web:
exposure:
# 这里用* 代表暴露所有端点只是为了观察效果,实际中按照需进行端点暴露
include: "*"
endpoint:
health:
# 详细信息显示给所有用户。
show-details: always
health:
status:
http-mapping:
# 自定义健康检查返回状态码对应的 http 状态码
FATAL: 503
```
#### 1.4 查看监控状态
导入 actuator 的 start 并进行配置后,访问 http://127.0.0.1:8080/actuator/health 就可以看到对应的项目监控状态。
<div align="center"> <img src="https://github.com/heibaiying/spring-samples-for-all/blob/master/pictures/health.png"/> </div>
需要注意的是这里的监控状态根据实际项目所用到的技术不同而不同。因为以下 HealthIndicators 情况在适当时由 Spring Boot 自动配置的:
| 名称 | 描述 |
| ------------------------------------------------------------ | -------------------------------- |
| [CassandraHealthIndicator](https://github.com/spring-projects/spring-boot/tree/v2.0.1.RELEASE/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/cassandra/CassandraHealthIndicator.java) | 检查 Cassandra 数据库是否启动。 |
| [DiskSpaceHealthIndicator](https://github.com/spring-projects/spring-boot/tree/v2.0.1.RELEASE/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/system/DiskSpaceHealthIndicator.java) | 检查磁盘空间不足。 |
| [DataSourceHealthIndicator](https://github.com/spring-projects/spring-boot/tree/v2.0.1.RELEASE/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/jdbc/DataSourceHealthIndicator.java) | 检查是否可以获得连接 DataSource。 |
| [ElasticsearchHealthIndicator](https://github.com/spring-projects/spring-boot/tree/v2.0.1.RELEASE/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/elasticsearch/ElasticsearchHealthIndicator.java) | 检查 Elasticsearch 集群是否启动。 |
| [InfluxDbHealthIndicator](https://github.com/spring-projects/spring-boot/tree/v2.0.1.RELEASE/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/influx/InfluxDbHealthIndicator.java) | 检查 InfluxDB 服务器是否启动。 |
| [JmsHealthIndicator](https://github.com/spring-projects/spring-boot/tree/v2.0.1.RELEASE/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/jms/JmsHealthIndicator.java) | 检查 JMS 代理是否启动。 |
| [MailHealthIndicator](https://github.com/spring-projects/spring-boot/tree/v2.0.1.RELEASE/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/mail/MailHealthIndicator.java) | 检查邮件服务器是否启动。 |
| [MongoHealthIndicator](https://github.com/spring-projects/spring-boot/tree/v2.0.1.RELEASE/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/mongo/MongoHealthIndicator.java) | 检查 Mongo 数据库是否启动。 |
| [Neo4jHealthIndicator](https://github.com/spring-projects/spring-boot/tree/v2.0.1.RELEASE/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/neo4j/Neo4jHealthIndicator.java) | 检查 Neo4j 服务器是否启动。 |
| [RabbitHealthIndicator](https://github.com/spring-projects/spring-boot/tree/v2.0.1.RELEASE/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/amqp/RabbitHealthIndicator.java) | 检查 Rabbit 服务器是否启动。 |
| [RedisHealthIndicator](https://github.com/spring-projects/spring-boot/tree/v2.0.1.RELEASE/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/redis/RedisHealthIndicator.java) | 检查 Redis 服务器是否启动。 |
| [SolrHealthIndicator](https://github.com/spring-projects/spring-boot/tree/v2.0.1.RELEASE/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/solr/SolrHealthIndicator.java) | 检查 Solr 服务器是否启动。 |
## 三、自定义健康检查指标
```java
/**
* @author : heibaiying
* @description : 自定义健康检查指标
*/
@Component
public class CustomHealthIndicator implements HealthIndicator {
@Override
public Health health() {
double random = Math.random();
// 这里用随机数模拟健康检查的结果
if (random > 0.5) {
return Health.status("FATAL").withDetail("error code", "某健康专项检查失败").build();
} else {
return Health.up().withDetail("success code", "自定义检查一切正常").build();
}
}
}
```
自定义检查通过的情况下:
<div align="center"> <img src="https://github.com/heibaiying/spring-samples-for-all/blob/master/pictures/actuator-health-up.png"/> </div>
自定义检查失败的情况:
<div align="center"> <img src="https://github.com/heibaiying/spring-samples-for-all/blob/master/pictures/health-fatal-200.png"/> </div>
这里我们可以看到自定义检查不论是否通过都不会影响整体的 status,两种情况下都是 status 都是“up”。如果我们想通过自定义的检查检查去影响最终的检查结果比如我们健康检查针对的是支付业务在支付业务的不可用的情况下我们就认为整个服务是不可用的。这个时候就需要实现自定义实现健康状态的聚合。
## 四、自定义健康状态聚合规则
```java
/**
* @author : heibaiying
* @description : 对所有的自定义健康指标进行聚合,按照自定义规则返回总和健康状态
*/
@Component
public class CustomHealthAggregator implements HealthAggregator {
@Override
public Health aggregate(Map<String, Health> healths) {
for (Health health : healths.values()) {
// 聚合规则可以自定义,这里假设我们自定义的监控状态中有一项 FATAL,就认为整个服务都是不可用的,否则认为整个服务是可用的
if (health.getStatus().getCode().equals("FATAL")) {
return Health.status("FATAL").withDetail("error code", "综合判断后服务宕机").build();
}
}
return Health.up().build();
}
}
```
当我们自定义健康检查不通过时候的结果如下:
<div align="center"> <img src="https://github.com/heibaiying/spring-samples-for-all/blob/master/pictures/actuator-heath-503.png"/> </div>
这里需要注意的是返回我们自定义的聚合状态的时候,状态码也变成了 503,这是我们在配置文件中进行定义的:
```properties
management.health.status.http-mapping.FATAL = 503
```
下表显示了内置状态的默认状态映射:
| Status | Mapping |
| -------------- | -------------------------------------------- |
| DOWN | SERVICE_UNAVAILABLE (503) |
| OUT_OF_SERVICE | SERVICE_UNAVAILABLE (503) |
| UP | No mapping by default, so http status is 200 |
| UNKNOWN | No mapping by default, so http status is 200 |
## 五、@Endpoint自定义端点
#### 5.1 自定义端点
spring boot 支持使用@Endpoint 来自定义端点暴露应用信息。这里我们采用第三方 sigar 来暴露服务所在硬件的监控信息。
Sigar 是 Hyperic-hq 产品的基础包,是 Hyperic HQ 主要的数据收集组件。Sigar.jar 的底层是用 C 语言编写的,它通过本地方法来调用操作系统 API 来获取系统相关数据 [jar 包下载地址](https://sourceforge.net/projects/sigar/)。
Sigar 为不同平台提供了不同的库文件,下载后需要将库文件放到服务所在主机的对应位置:
- Windows 下配置:根据自己的操作系统版本选择 sigar-amd64-winnt.dll 或 sigar-x86-winnt.dll 拷贝到 C:\Windows\System32 中
- Linux 下配置:将 libsigar-amd64-linux.so 或 libsigar-x86-linux.so 拷贝到/usr/lib64 或/lib64 或/lib 或/usr/lib 目录下,如果不起作用,还需要 sudochmod 744 修改 libsigar-amd64-linux.so 文件权限
```java
@Endpoint(id = "customEndPoint")
@Component
public class CustomEndPoint {
@ReadOperation
public Map<String, Object> getCupInfo() throws SigarException {
Map<String, Object> cupInfoMap = new LinkedHashMap<>();
Sigar sigar = new Sigar();
CpuInfo infoList[] = sigar.getCpuInfoList();
CpuPerc[] cpuList = sigar.getCpuPercList();
for (int i = 0; i < infoList.length; i++) {
CpuInfo info = infoList[i];
cupInfoMap.put("CPU " + i + " 的总量 MHz", info.getMhz()); // CPU 的总量 MHz
cupInfoMap.put("CPU " + i + " 生产商", info.getVendor()); // 获得 CPU 的生产商Intel
cupInfoMap.put("CPU " + i + " 类别", info.getModel()); // 获得 CPU 的类别Core
cupInfoMap.put("CPU " + i + " 缓存数量", info.getCacheSize()); // 缓冲存储器数量
cupInfoMap.put("CPU " + i + " 用户使用率", CpuPerc.format(cpuList[i].getUser())); // 用户使用率
cupInfoMap.put("CPU " + i + " 系统使用率", CpuPerc.format(cpuList[i].getSys())); // 系统使用率
cupInfoMap.put("CPU " + i + " 当前等待率", CpuPerc.format(cpuList[i].getWait())); // 当前等待
cupInfoMap.put("CPU " + i + " 当前错误率", CpuPerc.format(cpuList[i].getNice())); // 当前错误
cupInfoMap.put("CPU " + i + " 当前空闲率", CpuPerc.format(cpuList[i].getIdle())); // 当前空闲
cupInfoMap.put("CPU " + i + " 总的使用率", CpuPerc.format(cpuList[i].getCombined()));// 总的使用
}
return cupInfoMap;
}
}
```
其中可用的方法注解由 http 操作决定:
| operation | HTTP 方法 |
| ---------------- | -------- |
| @ReadOperation | GET |
| @WriteOperation | POST |
| @DeleteOperation | DELETE |
#### 5.2 访问自定义端点http://127.0.0.1:8080/actuator/customEndPoint
<div align="center"> <img src="https://github.com/heibaiying/spring-samples-for-all/blob/master/pictures/actuator-customEndPoint.png"/> </div>
关于 Sigar 的 更多监控参数可以参考博客:[java 读取计算机 CPU、内存等信息Sigar 使用)](https://blog.csdn.net/wudiazu/article/details/73829324)
Sigar 下载包中也提供了各种参数的参考用例:
<div align="center"> <img src="https://github.com/heibaiying/spring-samples-for-all/blob/master/pictures/sigar.png"/> </div>

View File

@ -1,5 +1,6 @@
# spring-boot 基础
# spring-boot 基础
## 目录<br/>
<a href="#一说明">一、说明</a><br/>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href="#11-项目结构说明">1.1 项目结构说明</a><br/>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href="#12-项目依赖">1.2 项目依赖</a><br/>
@ -14,248 +15,248 @@
## 一、说明
#### 1.1 项目结构说明
1. 本项目搭建一个简单的hello spring 的 web工程简单说明spring-boot 的开箱即用的特性
2. 模板引擎采用freemaker 和 thymeleaf 作为示例分别对应模板文件makershow.ftl 和 leafShow.html
3. spring boot 2.x 默认是不支持jsp的需要额外的配置关于使用jsp的整合可以参考[spring-boot-jsp](https://github.com/heibaiying/spring-samples-for-all/tree/master/spring-boot/spring-boot-jsp)项目。
<div align="center"> <img src="https://github.com/heibaiying/spring-samples-for-all/blob/master/pictures/spring-boot-base.png"/> </div>
#### 1.2 项目依赖
导入相关的starter(启动器)
```xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.1.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.heibaiying</groupId>
<artifactId>spring-boot-base</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>spring-boot-base</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<!--模板引擎-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-freemarker</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<!--web 启动器-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!--lombok 插件-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<!--测试相关依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<!--Spring Boot的Maven插件Spring Boot Maven plugin能够以Maven的方式为应用提供Spring Boot的支持-->
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
```
1. spring boot 项目默认继承自spring-boot-starter-parent而spring-boot-starter-parent继承自spring-boot-dependencies, spring-boot-dependencies中定义了关于spring boot 依赖的各种jar包的版本是spring boot 的版本管理中心。
<div align="center"> <img src="https://github.com/heibaiying/spring-samples-for-all/blob/master/pictures/spring-boot-dependencies.png"/> </div>
2. 关于spring boot 2.x官方支持的所有starter 可以参见官方文档 [Table 13.1. Spring Boot application starters](https://docs.spring.io/spring-boot/docs/2.1.1.RELEASE/reference/htmlsingle/#using-boot-starter)
## 二、spring boot 主启动类
如果采用IDEA 或者 Spring Tool Suite (STS) 等开发工具创建的spring boot 工程,会默认创建启动类,如果没有创建,需要手动创建启动类
```java
package com.heibaiying.springbootbase;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class SpringBootBaseApplication {
// 启动类默认开启包扫描,扫描与主程序所在包及其子包,对于本工程而言 默认扫描 com.heibaiying.springbootbase
public static void main(String[] args) {
SpringApplication.run(SpringBootBaseApplication.class, args);
}
}
```
@SpringBootApplication 注解是一个复合注解,里面包含了@ComponentScan注解,默认开启包扫描,扫描与主程序所在包及其子包,对于本工程而言 默认扫描 com.heibaiying.springbootbase
```java
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@SpringBootConfiguration
@EnableAutoConfiguration
@ComponentScan(excludeFilters = {
@Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class),
@Filter(type = FilterType.CUSTOM, classes = AutoConfigurationExcludeFilter.class) })
public @interface SpringBootApplication {
...
}
```
## 三、开箱即用的web工程
在springbootBaseApplication.java 的同级目录创建controller文件夹并在其中创建RestfulController.java,启动项目访问localhost:8080/restful/programmers 即可看到项目搭建成功。
```java
/**
* @author : heibaiying
* @description : restful 控制器
*/
@RestController
@RequestMapping("restful")
public class RestfulController {
@GetMapping("programmers")
private List<Programmer> getProgrammers() {
List<Programmer> programmers = new ArrayList<>();
programmers.add(new Programmer("xiaoming", 12, 100000.00f, LocalDate.of(2019, Month.AUGUST, 2)));
programmers.add(new Programmer("xiaohong", 23, 900000.00f, LocalDate.of(2013, Month.FEBRUARY, 2)));
return programmers;
}
}
```
这里之所以能够开箱即用是因为我们在项目中导入spring-boot-starter-web启动器@SpringBootApplication 复合注解中默认开启了@EnableAutoConfiguration注解允许开启自动化配置spring在检查导入starter-web的依赖后就会开启web的自动化配置。
## 四、模板引擎
这里我们在一个项目中同时导入了freemaker 和 thymeleaf的starter虽然并不推荐但是在同一个项目中是可以混用这两种模板引擎的
#### 4.1 freemarker
```java
/**
* @author : heibaiying
* @description : 跳转渲染模板引擎 默认模板的存放位置为classpath:templates
*/
@Controller
@RequestMapping("freemarker")
public class FreeMarkerController {
@RequestMapping("show")
private String programmerShow(ModelMap modelMap){
List<Programmer> programmerList=new ArrayList<>();
programmerList.add(new Programmer("xiaoming",12,100000.00f,LocalDate.of(2019,Month.AUGUST,2)));
programmerList.add(new Programmer("xiaohong",23,900000.00f,LocalDate.of(2013,Month.FEBRUARY,2)));
modelMap.addAttribute("programmers",programmerList);
return "markerShow";
}
}
```
```html
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>freemarker模板引擎</title>
</head>
<body>
<ul>
<#list programmers as programmer>
<li>姓名: ${programmer.name} 年龄: ${programmer.age}</li>
</#list>
</ul>
</body>
</html>
```
#### 4.2 thymeleaf
```java
/**
* @author : heibaiying
* @description : 跳转渲染模板引擎 默认模板的存放位置为classpath:templates
*/
@Controller
@RequestMapping("thymeleaf")
public class ThymeleafController {
@RequestMapping("show")
private String programmerShow(ModelMap modelMap) {
List<Programmer> programmerList = new ArrayList<>();
programmerList.add(new Programmer("xiaoming", 12, 100000.00f, LocalDate.of(2019, Month.AUGUST, 2)));
programmerList.add(new Programmer("xiaohong", 23, 900000.00f, LocalDate.of(2013, Month.FEBRUARY, 2)));
modelMap.addAttribute("programmers", programmerList);
return "leafShow";
}
}
```
```html
<!doctype html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>thymeleaf模板引擎</title>
</head>
<body>
<ul th:each="programmer:${programmers}">
<li>
姓名:<span th:text="${programmer.name}"></span>
薪水:<span th:text="${programmer.salary}"></span>
</li>
</ul>
</body>
</html>
```
#### 4.3 文档说明
freemarker提供了完善的中文文档地址 http://freemarker.foofun.cn/
thymeleaf官方英文文档地址[thymeleaf 3.0.11RELEASE](https://www.thymeleaf.org/doc/tutorials/3.0/usingthymeleaf.pdf)
## 一、说明
#### 1.1 项目结构说明
1. 本项目搭建一个简单的 hello spring 的 web 工程,简单说明 spring-boot 的开箱即用的特性;
2. 模板引擎采用 freemaker 和 thymeleaf 作为示例,分别对应模板文件 makershow.ftl 和 leafShow.html
3. spring boot 2.x 默认是不支持 jsp 的,需要额外的配置,关于使用 jsp 的整合可以参考[spring-boot-jsp](https://github.com/heibaiying/spring-samples-for-all/tree/master/spring-boot/spring-boot-jsp) 项目。
<div align="center"> <img src="https://github.com/heibaiying/spring-samples-for-all/blob/master/pictures/spring-boot-base.png"/> </div>
#### 1.2 项目依赖
导入相关的 starter(启动器)
```xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.1.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.heibaiying</groupId>
<artifactId>spring-boot-base</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>spring-boot-base</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<!--模板引擎-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-freemarker</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<!--web 启动器-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!--lombok 插件-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<!--测试相关依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<!--Spring Boot 的 Maven 插件Spring Boot Maven plugin能够以 Maven 的方式为应用提供 Spring Boot 的支持-->
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
```
1. spring boot 项目默认继承自 spring-boot-starter-parent而 spring-boot-starter-parent 继承自 spring-boot-dependencies, spring-boot-dependencies 中定义了关于 spring boot 依赖的各种 jar 包的版本,是 spring boot 的版本管理中心。
<div align="center"> <img src="https://github.com/heibaiying/spring-samples-for-all/blob/master/pictures/spring-boot-dependencies.png"/> </div>
2. 关于 spring boot 2.x 官方支持的所有 starter 可以参见官方文档 [Table 13.1. Spring Boot application starters](https://docs.spring.io/spring-boot/docs/2.1.1.RELEASE/reference/htmlsingle/#using-boot-starter)
## 二、spring boot 主启动类
如果采用 IDEA 或者 Spring Tool Suite (STS) 等开发工具创建的 spring boot 工程,会默认创建启动类,如果没有创建,需要手动创建启动类
```java
package com.heibaiying.springbootbase;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class SpringBootBaseApplication {
// 启动类默认开启包扫描,扫描与主程序所在包及其子包,对于本工程而言 默认扫描 com.heibaiying.springbootbase
public static void main(String[] args) {
SpringApplication.run(SpringBootBaseApplication.class, args);
}
}
```
@SpringBootApplication 注解是一个复合注解,里面包含了@ComponentScan 注解,默认开启包扫描,扫描与主程序所在包及其子包,对于本工程而言 默认扫描 com.heibaiying.springbootbase
```java
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@SpringBootConfiguration
@EnableAutoConfiguration
@ComponentScan(excludeFilters = {
@Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class),
@Filter(type = FilterType.CUSTOM, classes = AutoConfigurationExcludeFilter.class) })
public @interface SpringBootApplication {
...
}
```
## 三、开箱即用的web工程
在 springbootBaseApplication.java 的同级目录创建 controller 文件夹,并在其中创建 RestfulController.java,启动项目访问 localhost:8080/restful/programmers 即可看到项目搭建成功。
```java
/**
* @author : heibaiying
* @description : restful 控制器
*/
@RestController
@RequestMapping("restful")
public class RestfulController {
@GetMapping("programmers")
private List<Programmer> getProgrammers() {
List<Programmer> programmers = new ArrayList<>();
programmers.add(new Programmer("xiaoming", 12, 100000.00f, LocalDate.of(2019, Month.AUGUST, 2)));
programmers.add(new Programmer("xiaohong", 23, 900000.00f, LocalDate.of(2013, Month.FEBRUARY, 2)));
return programmers;
}
}
```
这里之所以能够开箱即用,是因为我们在项目中导入 spring-boot-starter-web 启动器,而@SpringBootApplication 复合注解中默认开启了@EnableAutoConfiguration 注解允许开启自动化配置spring 在检查导入 starter-web 的依赖后就会开启 web 的自动化配置。
## 四、模板引擎
这里我们在一个项目中同时导入了 freemaker 和 thymeleaf 的 starter虽然并不推荐但是在同一个项目中是可以混用这两种模板引擎的
#### 4.1 freemarker
```java
/**
* @author : heibaiying
* @description : 跳转渲染模板引擎 默认模板的存放位置为 classpath:templates
*/
@Controller
@RequestMapping("freemarker")
public class FreeMarkerController {
@RequestMapping("show")
private String programmerShow(ModelMap modelMap){
List<Programmer> programmerList=new ArrayList<>();
programmerList.add(new Programmer("xiaoming",12,100000.00f,LocalDate.of(2019,Month.AUGUST,2)));
programmerList.add(new Programmer("xiaohong",23,900000.00f,LocalDate.of(2013,Month.FEBRUARY,2)));
modelMap.addAttribute("programmers",programmerList);
return "markerShow";
}
}
```
```html
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>freemarker 模板引擎</title>
</head>
<body>
<ul>
<#list programmers as programmer>
<li>姓名: ${programmer.name} 年龄: ${programmer.age}</li>
</#list>
</ul>
</body>
</html>
```
#### 4.2 thymeleaf
```java
/**
* @author : heibaiying
* @description : 跳转渲染模板引擎 默认模板的存放位置为 classpath:templates
*/
@Controller
@RequestMapping("thymeleaf")
public class ThymeleafController {
@RequestMapping("show")
private String programmerShow(ModelMap modelMap) {
List<Programmer> programmerList = new ArrayList<>();
programmerList.add(new Programmer("xiaoming", 12, 100000.00f, LocalDate.of(2019, Month.AUGUST, 2)));
programmerList.add(new Programmer("xiaohong", 23, 900000.00f, LocalDate.of(2013, Month.FEBRUARY, 2)));
modelMap.addAttribute("programmers", programmerList);
return "leafShow";
}
}
```
```html
<!doctype html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>thymeleaf 模板引擎</title>
</head>
<body>
<ul th:each="programmer:${programmers}">
<li>
姓名:<span th:text="${programmer.name}"></span>
薪水:<span th:text="${programmer.salary}"></span>
</li>
</ul>
</body>
</html>
```
#### 4.3 文档说明
freemarker提供了完善的中文文档地址 http://freemarker.foofun.cn/
thymeleaf官方英文文档地址[thymeleaf 3.0.11RELEASE](https://www.thymeleaf.org/doc/tutorials/3.0/usingthymeleaf.pdf)
注:我在本仓库中也上传了一份[thymeleaf 中文文档gangzi828(刘明刚 译)](https://github.com/heibaiying/spring-samples-for-all/tree/master/referenced%20documents),翻译的版本为 3.0.5RELEASE

View File

@ -1,5 +1,6 @@
# spring boot data jpa
# spring boot data jpa
## 目录<br/>
<a href="#一说明">一、说明</a><br/>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href="#11-项目结构">1.1 项目结构</a><br/>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href="#12-项目主要依赖">1.2 项目主要依赖</a><br/>
@ -12,194 +13,194 @@
## 一、说明
#### 1.1 项目结构
<div align="center"> <img src="https://github.com/heibaiying/spring-samples-for-all/blob/master/pictures/spring-boot-data-jpa.png"/> </div>
#### 1.2 项目主要依赖
```xml
<dependencies>
<!-- data-jpa -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<!--引入mysql驱动-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.13</version>
</dependency>
<!--单元测试包-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
```
## 二、data jpa 的使用
#### 2.1 在application.yml 中配置数据源
```yaml
spring:
datasource:
url: jdbc:mysql://127.0.0.1:3306/mysql?characterEncoding=UTF-8&serverTimezone=UTC&useSSL=false
username: root
password: root
driver-class-name: com.mysql.cj.jdbc.Driver
jpa:
hibernate:
ddl-auto: update
#Hibernate默认创建的表是myisam引擎可以用以下方式指定为使用innodb创建表
database-platform: org.hibernate.dialect.MySQL57Dialect
show-sql: true
```
#### 2.2 新建查询接口
```java
/**
* @author : heibaiying
* @description : 查询接口继承自CrudRepository,CrudRepository 默认定义了部分增删改查方法
*/
public interface ProgRepository extends CrudRepository<Programmer, Integer> {
/*
* 方法名遵循命名规范的查询 更多命名规范可以参考官方文档所列出的这张表格
*/
List<Programmer> findAllByName(String name);
/*
*分页排序查询
*/
Page<Programmer> findAll(Pageable pageable);
/*
* 占位符查询
*/
@Query(value = "select u from Programmer u where u.name = ?1 or u.salary = ?2")
List<Programmer> findByConditionAndOrder(String name, float salary, Sort.Order order);
/*
* 传入参数名称
*/
@Query("select u from Programmer u where u.name = :name or u.age = :age")
Programmer findByParam(@Param("name") String name,
@Param("age") int age);
}
```
关于查询方法遵循的命名规范和关键词见下表:
| Keyword | Sample | JPQL snippet |
| ------------------- | ------------------------------------------------------------ | ------------------------------------------------------------ |
| `And` | `findByLastnameAndFirstname` | `… where x.lastname = ?1 and x.firstname = ?2` |
| `Or` | `findByLastnameOrFirstname` | `… where x.lastname = ?1 or x.firstname = ?2` |
| `Is,Equals` | `findByFirstname`,`findByFirstnameIs`,<br>`findByFirstnameEquals` | `… where x.firstname = ?1` |
| `Between` | `findByStartDateBetween` | `… where x.startDate between ?1 and ?2` |
| `LessThan` | `findByAgeLessThan` | `… where x.age < ?1` |
| `LessThanEqual` | `findByAgeLessThanEqual` | `… where x.age <= ?1` |
| `GreaterThan` | `findByAgeGreaterThan` | `… where x.age > ?1` |
| `GreaterThanEqual` | `findByAgeGreaterThanEqual` | `… where x.age >= ?1` |
| `After` | `findByStartDateAfter` | `… where x.startDate > ?1` |
| `Before` | `findByStartDateBefore` | `… where x.startDate < ?1` |
| `IsNull` | `findByAgeIsNull` | `… where x.age is null` |
| `IsNotNull,NotNull` | `findByAge(Is)NotNull` | `… where x.age not null` |
| `Like` | `findByFirstnameLike` | `… where x.firstname like ?1` |
| `NotLike` | `findByFirstnameNotLike` | `… where x.firstname not like ?1` |
| `StartingWith` | `findByFirstnameStartingWith` | `… where x.firstname like ?1`(parameter bound with appended `%`) |
| `EndingWith` | `findByFirstnameEndingWith` | `… where x.firstname like ?1`(parameter bound with prepended `%`) |
| `Containing` | `findByFirstnameContaining` | `… where x.firstname like ?1`(parameter bound wrapped in `%`) |
| `OrderBy` | `findByAgeOrderByLastnameDesc` | `… where x.age = ?1 order by x.lastname desc` |
| `Not` | `findByLastnameNot` | `… where x.lastname <> ?1` |
| `In` | `findByAgeIn(Collection<Age> ages)` | `… where x.age in ?1` |
| `NotIn` | `findByAgeNotIn(Collection<Age> ages)` | `… where x.age not in ?1` |
| `True` | `findByActiveTrue()` | `… where x.active = true` |
| `False` | `findByActiveFalse()` | `… where x.active = false` |
| `IgnoreCase` | `findByFirstnameIgnoreCase` | `… where UPPER(x.firstame) = UPPER(?1)` |
#### 2.3 测试类
```java
@RunWith(SpringRunner.class)
@SpringBootTest
public class DataJPATests {
@Autowired
private ProgRepository repository;
/**
* 保存数据测试
*/
@Test
public void save() {
// 保存单条数据
repository.save(new Programmer("pro01", 12, 2121.34f, new Date()));
// 保存多条数据
List<Programmer> programmers = new ArrayList<>();
programmers.add(new Programmer("pro02", 22, 3221.34f, new Date()));
programmers.add(new Programmer("pro03", 32, 3321.34f, new Date()));
programmers.add(new Programmer("pro04", 44, 4561.34f, new Date()));
programmers.add(new Programmer("pro01", 44, 4561.34f, new Date()));
repository.saveAll(programmers);
}
/**
* 查询数据测试
*/
@Test
public void get() {
// 遵循命名规范的查询
List<Programmer> programmers = repository.findAllByName("pro01");
programmers.forEach(System.out::println);
// 传入参数名称
Programmer param = repository.findByParam("pro02", 22);
System.out.println("findByParam:" + param);
// 占位符查询
List<Programmer> byCondition = repository.findByConditionAndOrder("pro03", 3321.34f, Sort.Order.asc("salary"));
System.out.println("byCondition:" + byCondition);
//条件与分页查询 需要注意的是这里的页数是从第0页开始计算的
Page<Programmer> page = repository.findAll(PageRequest.of(0, 10, Sort.Direction.DESC, "salary"));
page.get().forEach(System.out::println);
}
/**
* 更新数据测试
*/
@Test
public void update() {
// 保存主键相同的数据就认为是更新操作
repository.save(new Programmer(1, "updatePro01", 12, 2121.34f, new Date()));
Optional<Programmer> programmer = repository.findById(1);
Assert.assertEquals(programmer.get().getName(), "updatePro01");
}
/**
* 删除数据测试
*/
@Test
public void delete() {
Optional<Programmer> programmer = repository.findById(2);
if (programmer.isPresent()) {
repository.deleteById(2);
}
Assert.assertFalse(programmer.isPresent());
}
}
```
## 一、说明
#### 1.1 项目结构
<div align="center"> <img src="https://github.com/heibaiying/spring-samples-for-all/blob/master/pictures/spring-boot-data-jpa.png"/> </div>
#### 1.2 项目主要依赖
```xml
<dependencies>
<!-- data-jpa -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<!--引入 mysql 驱动-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.13</version>
</dependency>
<!--单元测试包-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
```
## 二、data jpa 的使用
#### 2.1 在application.yml 中配置数据源
```yaml
spring:
datasource:
url: jdbc:mysql://127.0.0.1:3306/mysql?characterEncoding=UTF-8&serverTimezone=UTC&useSSL=false
username: root
password: root
driver-class-name: com.mysql.cj.jdbc.Driver
jpa:
hibernate:
ddl-auto: update
#Hibernate 默认创建的表是 myisam 引擎,可以用以下方式指定为使用 innodb 创建表
database-platform: org.hibernate.dialect.MySQL57Dialect
show-sql: true
```
#### 2.2 新建查询接口
```java
/**
* @author : heibaiying
* @description : 查询接口继承自 CrudRepository,CrudRepository 默认定义了部分增删改查方法
*/
public interface ProgRepository extends CrudRepository<Programmer, Integer> {
/*
* 方法名遵循命名规范的查询 更多命名规范可以参考官方文档所列出的这张表格
*/
List<Programmer> findAllByName(String name);
/*
*分页排序查询
*/
Page<Programmer> findAll(Pageable pageable);
/*
* 占位符查询
*/
@Query(value = "select u from Programmer u where u.name = ?1 or u.salary = ?2")
List<Programmer> findByConditionAndOrder(String name, float salary, Sort.Order order);
/*
* 传入参数名称
*/
@Query("select u from Programmer u where u.name = :name or u.age = :age")
Programmer findByParam(@Param("name") String name,
@Param("age") int age);
}
```
关于查询方法遵循的命名规范和关键词见下表:
| Keyword | Sample | JPQL snippet |
| ------------------- | ------------------------------------------------------------ | ------------------------------------------------------------ |
| `And` | `findByLastnameAndFirstname` | `… where x.lastname = ?1 and x.firstname = ?2` |
| `Or` | `findByLastnameOrFirstname` | `… where x.lastname = ?1 or x.firstname = ?2` |
| `Is,Equals` | `findByFirstname`,`findByFirstnameIs`,<br>`findByFirstnameEquals` | `… where x.firstname = ?1` |
| `Between` | `findByStartDateBetween` | `… where x.startDate between ?1 and ?2` |
| `LessThan` | `findByAgeLessThan` | `… where x.age < ?1` |
| `LessThanEqual` | `findByAgeLessThanEqual` | `… where x.age <= ?1` |
| `GreaterThan` | `findByAgeGreaterThan` | `… where x.age > ?1` |
| `GreaterThanEqual` | `findByAgeGreaterThanEqual` | `… where x.age >= ?1` |
| `After` | `findByStartDateAfter` | `… where x.startDate > ?1` |
| `Before` | `findByStartDateBefore` | `… where x.startDate < ?1` |
| `IsNull` | `findByAgeIsNull` | `… where x.age is null` |
| `IsNotNull,NotNull` | `findByAge(Is)NotNull` | `… where x.age not null` |
| `Like` | `findByFirstnameLike` | `… where x.firstname like ?1` |
| `NotLike` | `findByFirstnameNotLike` | `… where x.firstname not like ?1` |
| `StartingWith` | `findByFirstnameStartingWith` | `… where x.firstname like ?1`(parameter bound with appended `%`) |
| `EndingWith` | `findByFirstnameEndingWith` | `… where x.firstname like ?1`(parameter bound with prepended `%`) |
| `Containing` | `findByFirstnameContaining` | `… where x.firstname like ?1`(parameter bound wrapped in `%`) |
| `OrderBy` | `findByAgeOrderByLastnameDesc` | `… where x.age = ?1 order by x.lastname desc` |
| `Not` | `findByLastnameNot` | `… where x.lastname <> ?1` |
| `In` | `findByAgeIn(Collection<Age> ages)` | `… where x.age in ?1` |
| `NotIn` | `findByAgeNotIn(Collection<Age> ages)` | `… where x.age not in ?1` |
| `True` | `findByActiveTrue()` | `… where x.active = true` |
| `False` | `findByActiveFalse()` | `… where x.active = false` |
| `IgnoreCase` | `findByFirstnameIgnoreCase` | `… where UPPER(x.firstame) = UPPER(?1)` |
#### 2.3 测试类
```java
@RunWith(SpringRunner.class)
@SpringBootTest
public class DataJPATests {
@Autowired
private ProgRepository repository;
/**
* 保存数据测试
*/
@Test
public void save() {
// 保存单条数据
repository.save(new Programmer("pro01", 12, 2121.34f, new Date()));
// 保存多条数据
List<Programmer> programmers = new ArrayList<>();
programmers.add(new Programmer("pro02", 22, 3221.34f, new Date()));
programmers.add(new Programmer("pro03", 32, 3321.34f, new Date()));
programmers.add(new Programmer("pro04", 44, 4561.34f, new Date()));
programmers.add(new Programmer("pro01", 44, 4561.34f, new Date()));
repository.saveAll(programmers);
}
/**
* 查询数据测试
*/
@Test
public void get() {
// 遵循命名规范的查询
List<Programmer> programmers = repository.findAllByName("pro01");
programmers.forEach(System.out::println);
// 传入参数名称
Programmer param = repository.findByParam("pro02", 22);
System.out.println("findByParam:" + param);
// 占位符查询
List<Programmer> byCondition = repository.findByConditionAndOrder("pro03", 3321.34f, Sort.Order.asc("salary"));
System.out.println("byCondition:" + byCondition);
//条件与分页查询 需要注意的是这里的页数是从第 0 页开始计算的
Page<Programmer> page = repository.findAll(PageRequest.of(0, 10, Sort.Direction.DESC, "salary"));
page.get().forEach(System.out::println);
}
/**
* 更新数据测试
*/
@Test
public void update() {
// 保存主键相同的数据就认为是更新操作
repository.save(new Programmer(1, "updatePro01", 12, 2121.34f, new Date()));
Optional<Programmer> programmer = repository.findById(1);
Assert.assertEquals(programmer.get().getName(), "updatePro01");
}
/**
* 删除数据测试
*/
@Test
public void delete() {
Optional<Programmer> programmer = repository.findById(2);
if (programmer.isPresent()) {
repository.deleteById(2);
}
Assert.assertFalse(programmer.isPresent());
}
}
```

View File

@ -1,5 +1,6 @@
# spring boot 整合 druid+mybatis
# spring boot 整合 druid+mybatis
## 目录<br/>
<a href="#一说明">一、说明</a><br/>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href="#11-项目结构">1.1 项目结构</a><br/>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href="#12-项目主要依赖">1.2 项目主要依赖</a><br/>
@ -13,197 +14,197 @@
## 一、说明
#### 1.1 项目结构
1. 项目查询用的表对应的建表语句放置在resources的sql文件夹下
2. 为了使用druid控制台的功能项目以web的方式构建。
<div align="center"> <img src="https://github.com/heibaiying/spring-samples-for-all/blob/master/pictures/spring-boot-druid-mybatis.png"/> </div>
#### 1.2 项目主要依赖
需要说明的是按照spring 官方对于自定义的starter 命名规范的推荐:
- 官方的starter命名spring-boot-starter-XXXX
- 其他第三方starter命名XXXX-spring-boot-starte
所以mybatis的starter命名为mybatis-spring-boot-starter如果有自定义starter需求也需要按照此命名规则进行命名。
```xml
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>1.3.2</version>
</dependency>
<!--引入mysql驱动-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.13</version>
</dependency>
<!--druid 依赖-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.1.10</version>
</dependency>
```
spring boot 与 mybatis 版本的对应关系:
| MyBatis-Spring-Boot-Starter | [MyBatis-Spring](http://www.mybatis.org/spring/index.html#Requirements) | Spring Boot |
| --------------------------- | ------------------------------------------------------------ | ------------- |
| **1.3.x (1.3.1)** | 1.3 or higher | 1.5 or higher |
| **1.2.x (1.2.1)** | 1.3 or higher | 1.4 or higher |
| **1.1.x (1.1.1)** | 1.3 or higher | 1.3 or higher |
| **1.0.x (1.0.2)** | 1.2 or higher | 1.3 or higher |
## 二、整合 druid + mybatis
#### 2.1 在application.yml 中配置数据源
本用例采用druid作为数据库连接池虽然druid性能略逊于Hikari但是提供了更为全面的监控管理可以按照实际需求选用druid或者Hikari。关于Hikari数据源的配置可以参考[spring-boot-mybatis项目](https://github.com/heibaiying/spring-samples-for-all/tree/master/spring-boot/spring-boot-mybatis)
```yaml
spring:
datasource:
url: jdbc:mysql://127.0.0.1:3306/mysql?characterEncoding=UTF-8&serverTimezone=UTC&useSSL=false
username: root
password: root
driver-class-name: com.mysql.cj.jdbc.Driver
# 使用 druid 作为连接池 更多配置的说明可以参见 druid starter 中文文档 https://github.com/alibaba/druid/tree/master/druid-spring-boot-starter
type: com.alibaba.druid.pool.DruidDataSource
druid:
# 初始化时建立物理连接的个数。初始化发生在显示调用init方法或者第一次getConnection时
initialSize: 5
# 最小连接池数量
minIdle: 5
# 最大连接池数量
maxActive: 10
# 获取连接时最大等待时间单位毫秒。配置了maxWait之后缺省启用公平锁并发效率会有所下降如果需要可以通过配置useUnfairLock属性为true使用非公平锁。
maxWait: 60000
# Destroy线程会检测连接的间隔时间如果连接空闲时间大于等于minEvictableIdleTimeMillis则关闭物理连接。
timeBetweenEvictionRunsMillis: 60000
# 连接保持空闲而不被驱逐的最小时间
minEvictableIdleTimeMillis: 300000
# 用来检测连接是否有效的sql 因数据库方言而差, 例如 oracle 应该写成 SELECT 1 FROM DUAL
validationQuery: SELECT 1
# 建议配置为true不影响性能并且保证安全性。申请连接的时候检测如果空闲时间大于timeBetweenEvictionRunsMillis执行validationQuery检测连接是否有效。
testWhileIdle: true
# 申请连接时执行validationQuery检测连接是否有效做了这个配置会降低性能。
testOnBorrow: false
# 归还连接时执行validationQuery检测连接是否有效做了这个配置会降低性能。
testOnReturn: false
# 是否自动回收超时连接
removeAbandoned: true
# 超时时间(以秒数为单位)
remove-abandoned-timeout: 180
# druid 监控的配置 如果不使用 druid 的监控功能的话 以下配置就不是必须的
# 本项目监控台访问地址: http://localhost:8080/druid/login.html
# WebStatFilter用于采集web-jdbc关联监控的数据。
# 更多配置可参见: https://github.com/alibaba/druid/wiki/%E9%85%8D%E7%BD%AE_%E9%85%8D%E7%BD%AEWebStatFilter
web-stat-filter:
# 是否开启 WebStatFilter 默认是true
enabled: true
# 需要拦截的url
url-pattern: /*
# 排除静态资源的请求
exclusions: "*.js,*.gif,*.jpg,*.png,*.css,*.ico,/druid/*"
# Druid内置提供了一个StatViewServlet用于展示Druid的统计信息。
# 更多配置可参见:https://github.com/alibaba/druid/wiki/%E9%85%8D%E7%BD%AE_StatViewServlet%E9%85%8D%E7%BD%AE
stat-view-servlet:
#是否启用StatViewServlet 默认值true
enabled: true
# 需要拦截的url
url-pattern: /druid/*
# 允许清空统计数据
reset-enable: true
login-username: druid
login-password: druid
# mybatis 相关配置
mybatis:
configuration:
# 当没有为参数提供特定的 JDBC 类型时,为空值指定 JDBC 类型。
# oracle数据库建议配置为JdbcType.NULL, 默认是Other
jdbc-type-for-null: 'null'
# 是否打印sql语句 调试的时候可以开启
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
```
#### 2.2 新建查询接口和controller
```java
@Mapper
public interface ProgrammerDao {
@Select("select * from programmer")
List<Programmer> selectAll();
@Insert("insert into programmer (name, age, salary, birthday) VALUES (#{name}, #{age}, #{salary}, #{birthday})")
void save(Programmer programmer);
@Select("select * from programmer where name = #{id}")
Programmer selectById(int id);
@Update("update programmer set name=#{name},age=#{age},salary=#{salary},birthday=#{birthday} where id=#{id}")
int modify(Programmer programmer);
@Delete(" delete from programmer where id = #{id}")
void delete(int id);
}
```
```xml
@RestController
public class ProgrammerController {
@Autowired
private ProgrammerDao programmerDao;
@GetMapping("/programmers")
public List<Programmer> get() {
return programmerDao.selectAll();
}
}
```
#### 2.3 关于druid监控数据的外部化调用
```java
/**
* @author : heibaiying
* @description :在 Spring Boot 中可以通过 HTTP 接口将 Druid 监控数据以JSON 的形式暴露出去,
* 实际使用中你可以根据你的需要自由地对监控数据、暴露方式进行扩展。
*/
@RestController
public class DruidStatController {
@GetMapping("/stat")
public Object druidStat() {
// DruidStatManagerFacade#getDataSourceStatDataList 该方法可以获取所有数据源的监控数据
return DruidStatManagerFacade.getInstance().getDataSourceStatDataList();
}
}
```
<div align="center"> <img src="https://github.com/heibaiying/spring-samples-for-all/blob/master/pictures/druid-status.png"/> </div>
#### 2.4 druid 控制台的使用,默认访问地址 http://localhost:8080/druid/login.html
## 一、说明
#### 1.1 项目结构
1. 项目查询用的表对应的建表语句放置在 resources 的 sql 文件夹下;
2. 为了使用 druid 控制台的功能,项目以 web 的方式构建。
<div align="center"> <img src="https://github.com/heibaiying/spring-samples-for-all/blob/master/pictures/spring-boot-druid-mybatis.png"/> </div>
#### 1.2 项目主要依赖
需要说明的是按照 spring 官方对于自定义的 starter 命名规范的推荐:
- 官方的 starter 命名spring-boot-starter-XXXX
- 其他第三方 starter 命名:XXXX-spring-boot-starte
所以 mybatis 的 starter 命名为 mybatis-spring-boot-starter如果有自定义 starter 需求,也需要按照此命名规则进行命名。
```xml
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>1.3.2</version>
</dependency>
<!--引入mysql驱动-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.13</version>
</dependency>
<!--druid 依赖-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.1.10</version>
</dependency>
```
spring boot 与 mybatis 版本的对应关系:
| MyBatis-Spring-Boot-Starter | [MyBatis-Spring](http://www.mybatis.org/spring/index.html#Requirements) | Spring Boot |
| --------------------------- | ------------------------------------------------------------ | ------------- |
| **1.3.x (1.3.1)** | 1.3 or higher | 1.5 or higher |
| **1.2.x (1.2.1)** | 1.3 or higher | 1.4 or higher |
| **1.1.x (1.1.1)** | 1.3 or higher | 1.3 or higher |
| **1.0.x (1.0.2)** | 1.2 or higher | 1.3 or higher |
## 二、整合 druid + mybatis
#### 2.1 在application.yml 中配置数据源
本用例采用 druid 作为数据库连接池,虽然 druid 性能略逊于 Hikari但是提供了更为全面的监控管理可以按照实际需求选用 druid 或者 Hikari。关于 Hikari 数据源的配置可以参考[spring-boot-mybatis 项目](https://github.com/heibaiying/spring-samples-for-all/tree/master/spring-boot/spring-boot-mybatis)
```yaml
spring:
datasource:
url: jdbc:mysql://127.0.0.1:3306/mysql?characterEncoding=UTF-8&serverTimezone=UTC&useSSL=false
username: root
password: root
driver-class-name: com.mysql.cj.jdbc.Driver
# 使用 druid 作为连接池 更多配置的说明可以参见 druid starter 中文文档 https://github.com/alibaba/druid/tree/master/druid-spring-boot-starter
type: com.alibaba.druid.pool.DruidDataSource
druid:
# 初始化时建立物理连接的个数。初始化发生在显示调用 init 方法,或者第一次 getConnection 时
initialSize: 5
# 最小连接池数量
minIdle: 5
# 最大连接池数量
maxActive: 10
# 获取连接时最大等待时间,单位毫秒。配置了 maxWait 之后,缺省启用公平锁,并发效率会有所下降,如果需要可以通过配置 useUnfairLock 属性为 true 使用非公平锁。
maxWait: 60000
# Destroy 线程会检测连接的间隔时间,如果连接空闲时间大于等于 minEvictableIdleTimeMillis 则关闭物理连接。
timeBetweenEvictionRunsMillis: 60000
# 连接保持空闲而不被驱逐的最小时间
minEvictableIdleTimeMillis: 300000
# 用来检测连接是否有效的 sql 因数据库方言而差, 例如 oracle 应该写成 SELECT 1 FROM DUAL
validationQuery: SELECT 1
# 建议配置为 true不影响性能并且保证安全性。申请连接的时候检测如果空闲时间大于 timeBetweenEvictionRunsMillis执行 validationQuery 检测连接是否有效。
testWhileIdle: true
# 申请连接时执行 validationQuery 检测连接是否有效,做了这个配置会降低性能。
testOnBorrow: false
# 归还连接时执行 validationQuery 检测连接是否有效,做了这个配置会降低性能。
testOnReturn: false
# 是否自动回收超时连接
removeAbandoned: true
# 超时时间 (以秒数为单位)
remove-abandoned-timeout: 180
# druid 监控的配置 如果不使用 druid 的监控功能的话 以下配置就不是必须的
# 本项目监控台访问地址: http://localhost:8080/druid/login.html
# WebStatFilter 用于采集 web-jdbc 关联监控的数据。
# 更多配置可参见: https://github.com/alibaba/druid/wiki/%E9%85%8D%E7%BD%AE_%E9%85%8D%E7%BD%AEWebStatFilter
web-stat-filter:
# 是否开启 WebStatFilter 默认是 true
enabled: true
# 需要拦截的 url
url-pattern: /*
# 排除静态资源的请求
exclusions: "*.js,*.gif,*.jpg,*.png,*.css,*.ico,/druid/*"
# Druid 内置提供了一个 StatViewServlet 用于展示 Druid 的统计信息。
# 更多配置可参见:https://github.com/alibaba/druid/wiki/%E9%85%8D%E7%BD%AE_StatViewServlet%E9%85%8D%E7%BD%AE
stat-view-servlet:
#是否启用 StatViewServlet 默认值 true
enabled: true
# 需要拦截的 url
url-pattern: /druid/*
# 允许清空统计数据
reset-enable: true
login-username: druid
login-password: druid
# mybatis 相关配置
mybatis:
configuration:
# 当没有为参数提供特定的 JDBC 类型时,为空值指定 JDBC 类型。
# oracle 数据库建议配置为 JdbcType.NULL, 默认是 Other
jdbc-type-for-null: 'null'
# 是否打印 sql 语句 调试的时候可以开启
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
```
#### 2.2 新建查询接口和controller
```java
@Mapper
public interface ProgrammerDao {
@Select("select * from programmer")
List<Programmer> selectAll();
@Insert("insert into programmer (name, age, salary, birthday) VALUES (#{name}, #{age}, #{salary}, #{birthday})")
void save(Programmer programmer);
@Select("select * from programmer where name = #{id}")
Programmer selectById(int id);
@Update("update programmer set name=#{name},age=#{age},salary=#{salary},birthday=#{birthday} where id=#{id}")
int modify(Programmer programmer);
@Delete(" delete from programmer where id = #{id}")
void delete(int id);
}
```
```xml
@RestController
public class ProgrammerController {
@Autowired
private ProgrammerDao programmerDao;
@GetMapping("/programmers")
public List<Programmer> get() {
return programmerDao.selectAll();
}
}
```
#### 2.3 关于druid监控数据的外部化调用
```java
/**
* @author : heibaiying
* @description :在 Spring Boot 中可以通过 HTTP 接口将 Druid 监控数据以 JSON 的形式暴露出去,
* 实际使用中你可以根据你的需要自由地对监控数据、暴露方式进行扩展。
*/
@RestController
public class DruidStatController {
@GetMapping("/stat")
public Object druidStat() {
// DruidStatManagerFacade#getDataSourceStatDataList 该方法可以获取所有数据源的监控数据
return DruidStatManagerFacade.getInstance().getDataSourceStatDataList();
}
}
```
<div align="center"> <img src="https://github.com/heibaiying/spring-samples-for-all/blob/master/pictures/druid-status.png"/> </div>
#### 2.4 druid 控制台的使用,默认访问地址 http://localhost:8080/druid/login.html
<div align="center"> <img src="https://github.com/heibaiying/spring-samples-for-all/blob/master/pictures/spring-boot-druid%20%E6%8E%A7%E5%88%B6%E5%8F%B0.png"/> </div>

View File

@ -1,5 +1,6 @@
# spring boot 整合 dubbo
# spring boot 整合 dubbo
## 目录<br/>
<a href="#一-项目结构说明">一、 项目结构说明</a><br/>
<a href="#二关键依赖">二、关键依赖</a><br/>
<a href="#三公共模块boot-dubbo-common">三、公共模块boot-dubbo-common</a><br/>
@ -16,349 +17,349 @@
## 一、 项目结构说明
1.1 按照dubbo 文档推荐的服务最佳实践,建议将服务接口、服务模型、服务异常等均放在 API 包中所以项目采用maven多模块的构建方式在spring-boot-dubbo下构建三个子模块
1. boot-dubbo-common 是公共模块用于存放公共的接口和bean,被boot-dubbo-provider和boot-dubbo-consumer在pom.xml中引用
2. boot-dubbo-provider 是服务的提供者,提供商品的查询服务
3. boot-dubbo-consumer是服务的消费者调用provider提供的查询服务。
1.2 本项目dubbo的搭建采用zookeeper作为注册中心 关于zookeeper的安装和基本操作可以参见我的手记 [Zookeeper 基础命令与Java客户端](https://github.com/heibaiying/LearningNotes/blob/master/notes/%E4%B8%AD%E9%97%B4%E4%BB%B6/ZooKeeper/ZooKeeper%E9%9B%86%E7%BE%A4%E6%90%AD%E5%BB%BA%E4%B8%8EJava%E5%AE%A2%E6%88%B7%E7%AB%AF.md)
<div align="center"> <img src="https://github.com/heibaiying/spring-samples-for-all/blob/master/pictures/spring-boot-dubbo.png"/> </div>
## 二、关键依赖
在父工程的项目中统一导入依赖dubbo的starter父工程的pom.xml如下
```xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<packaging>pom</packaging>
<modules>
<module>boot-dubbo-common</module>
<module>boot-dubbo-consumer</module>
<module>boot-dubbo-provider</module>
</modules>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.1.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.heibaiying</groupId>
<artifactId>spring-boot-dubbo</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>spring-boot-dubbo</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-freemarker</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!--引入dubbo start依赖-->
<dependency>
<groupId>com.alibaba.boot</groupId>
<artifactId>dubbo-spring-boot-starter</artifactId>
<version>0.2.0</version>
</dependency>
</dependencies>
</project>
```
## 三、公共模块boot-dubbo-common
- api 下为公共的调用接口;
- bean 下为公共的实体类。
<div align="center"> <img src="https://github.com/heibaiying/spring-samples-for-all/blob/master/pictures/boot-dubbo-common.png"/> </div>
## 四、 服务提供者boot-dubbo-provider
<div align="center"> <img src="https://github.com/heibaiying/spring-samples-for-all/blob/master/pictures/boot-dubbo-provider.png"/> </div>
#### 4.1 提供方配置
```yaml
dubbo:
application:
name: boot-duboo-provider
# 指定注册协议和注册地址 dubbo推荐使用zookeeper作为注册中心并且在start依赖中引入了zookeeper的java客户端Curator
registry:
protocol: zookeeper
address: 127.0.0.1:2181
protocol.name: dubbo
```
#### 4.2 使用注解@Service暴露服务
需要注意的是这里的@Service注解不是spring的注解而是dubbo的注解 com.alibaba.dubbo.config.annotation.Service
```java
package com.heibaiying.dubboprovider.service;
import com.alibaba.dubbo.config.annotation.Service;
import com.heibaiying.api.IProductService;
import com.heibaiying.bean.Product;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
/**
* @author : heibaiying
* @description : 产品提供接口实现类
*/
@Service(timeout = 5000)
public class ProductService implements IProductService {
private static List<Product> productList = new ArrayList<>();
static {
for (int i = 0; i < 20; i++) {
productList.add(new Product(i, "产品" + i, i / 2 == 0, new Date(), 66.66f * i));
}
}
public Product queryProductById(int id) {
for (Product product : productList) {
if (product.getId() == id) {
return product;
}
}
return null;
}
public List<Product> queryAllProducts() {
return productList;
}
}
```
## 五、服务消费者boot-dubbo-consumer
<div align="center"> <img src="https://github.com/heibaiying/spring-samples-for-all/blob/master/pictures/boot-dubbo-consumer1.png"/> </div>
#### 1.消费方的配置
```yaml
dubbo:
application:
name: boot-duboo-provider
# 指定注册协议和注册地址 dubbo推荐使用zookeeper作为注册中心并且在start依赖中引入了zookeeper的java客户端Curator
registry:
protocol: zookeeper
address: 127.0.0.1:2181
protocol.name: dubbo
# 关闭所有服务的启动时检查 (没有提供者时报错)视实际情况设置
consumer:
check: false
server:
port: 8090
```
#### 2.使用注解@Reference引用远程服务
```java
package com.heibaiying.dubboconsumer.controller;
import com.alibaba.dubbo.config.annotation.Reference;
import com.heibaiying.api.IProductService;
import com.heibaiying.bean.Product;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import java.util.List;
@Controller
@RequestMapping("sell")
public class SellController {
// dubbo远程引用注解
@Reference
private IProductService productService;
@RequestMapping
public String productList(Model model) {
List<Product> products = productService.queryAllProducts();
model.addAttribute("products", products);
return "products";
}
@RequestMapping("product/{id}")
public String productDetail(@PathVariable int id, Model model) {
Product product = productService.queryProductById(id);
model.addAttribute("product", product);
return "product";
}
}
```
## 六、项目构建的说明
因为在项目中consumer和provider模块均依赖公共模块,所以在构建consumer和provider项目前需要将common 模块安装到本地仓库,**依次**对**父工程**和**common模块**执行:
```shell
mvn install -Dmaven.test.skip = true
```
consumer中 pom.xml如下
```xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<artifactId>spring-boot-dubbo</artifactId>
<groupId>com.heibaiying</groupId>
<version>0.0.1-SNAPSHOT</version>
</parent>
<artifactId>boot-dubbo-consumer</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>boot-dubbo-consumer</name>
<description>dubbo project for Spring Boot</description>
<properties>
<java.version>1.8</java.version>
</properties>
<!--引入对公共模块的依赖-->
<dependencies>
<dependency>
<groupId>com.heibaiying</groupId>
<artifactId>boot-dubbo-common</artifactId>
<version>0.0.1-SNAPSHOT</version>
<scope>compile</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
```
provider中 pom.xml如下
```xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<artifactId>spring-boot-dubbo</artifactId>
<groupId>com.heibaiying</groupId>
<version>0.0.1-SNAPSHOT</version>
</parent>
<artifactId>boot-dubbo-provider</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>boot-dubbo-provider</name>
<description>dubbo project for Spring Boot</description>
<properties>
<java.version>1.8</java.version>
</properties>
<!--引入对公共模块的依赖-->
<dependencies>
<dependency>
<groupId>com.heibaiying</groupId>
<artifactId>boot-dubbo-common</artifactId>
<version>0.0.1-SNAPSHOT</version>
<scope>compile</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
```
## 七、关于dubbo新版本管理控制台的安装说明
安装:
```sh
git clone https://github.com/apache/incubator-dubbo-ops.git /var/tmp/dubbo-ops
cd /var/tmp/dubbo-ops
mvn clean package
```
配置:
```sh
配置文件为:
dubbo-admin-backend/src/main/resources/application.properties
主要的配置有 默认的配置就是127.0.0.1:2181
dubbo.registry.address=zookeeper://127.0.0.1:2181
```
启动:
```sh
mvn --projects dubbo-admin-backend spring-boot:run
```
访问:
```
http://127.0.0.1:8080
## 一、 项目结构说明
1.1 按照 dubbo 文档推荐的服务最佳实践,建议将服务接口、服务模型、服务异常等均放在 API 包中,所以项目采用 maven 多模块的构建方式,在 spring-boot-dubbo 下构建三个子模块:
1. boot-dubbo-common 是公共模块,用于存放公共的接口和 bean,被 boot-dubbo-provider 和 boot-dubbo-consumer 在 pom.xml 中引用;
2. boot-dubbo-provider 是服务的提供者,提供商品的查询服务
3. boot-dubbo-consumer 是服务的消费者,调用 provider 提供的查询服务
1.2 本项目 dubbo 的搭建采用 zookeeper 作为注册中心, 关于 zookeeper 的安装和基本操作可以参见我的手记 [Zookeeper 基础命令与 Java 客户端](https://github.com/heibaiying/LearningNotes/blob/master/notes/%E4%B8%AD%E9%97%B4%E4%BB%B6/ZooKeeper/ZooKeeper%E9%9B%86%E7%BE%A4%E6%90%AD%E5%BB%BA%E4%B8%8EJava%E5%AE%A2%E6%88%B7%E7%AB%AF.md)
<div align="center"> <img src="https://github.com/heibaiying/spring-samples-for-all/blob/master/pictures/spring-boot-dubbo.png"/> </div>
## 二、关键依赖
在父工程的项目中统一导入依赖 dubbo 的 starter父工程的 pom.xml 如下
```xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<packaging>pom</packaging>
<modules>
<module>boot-dubbo-common</module>
<module>boot-dubbo-consumer</module>
<module>boot-dubbo-provider</module>
</modules>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.1.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.heibaiying</groupId>
<artifactId>spring-boot-dubbo</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>spring-boot-dubbo</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-freemarker</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!--引入 dubbo start 依赖-->
<dependency>
<groupId>com.alibaba.boot</groupId>
<artifactId>dubbo-spring-boot-starter</artifactId>
<version>0.2.0</version>
</dependency>
</dependencies>
</project>
```
## 三、公共模块boot-dubbo-common
- api 下为公共的调用接口;
- bean 下为公共的实体类。
<div align="center"> <img src="https://github.com/heibaiying/spring-samples-for-all/blob/master/pictures/boot-dubbo-common.png"/> </div>
## 四、 服务提供者boot-dubbo-provider
<div align="center"> <img src="https://github.com/heibaiying/spring-samples-for-all/blob/master/pictures/boot-dubbo-provider.png"/> </div>
#### 4.1 提供方配置
```yaml
dubbo:
application:
name: boot-duboo-provider
# 指定注册协议和注册地址 dubbo 推荐使用 zookeeper 作为注册中心,并且在 start 依赖中引入了 zookeeper 的 java 客户端 Curator
registry:
protocol: zookeeper
address: 127.0.0.1:2181
protocol.name: dubbo
```
#### 4.2 使用注解@Service暴露服务
需要注意的是这里的@Service 注解不是 spring 的注解,而是 dubbo 的注解 com.alibaba.dubbo.config.annotation.Service
```java
package com.heibaiying.dubboprovider.service;
import com.alibaba.dubbo.config.annotation.Service;
import com.heibaiying.api.IProductService;
import com.heibaiying.bean.Product;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
/**
* @author : heibaiying
* @description : 产品提供接口实现类
*/
@Service(timeout = 5000)
public class ProductService implements IProductService {
private static List<Product> productList = new ArrayList<>();
static {
for (int i = 0; i < 20; i++) {
productList.add(new Product(i, "产品" + i, i / 2 == 0, new Date(), 66.66f * i));
}
}
public Product queryProductById(int id) {
for (Product product : productList) {
if (product.getId() == id) {
return product;
}
}
return null;
}
public List<Product> queryAllProducts() {
return productList;
}
}
```
## 五、服务消费者boot-dubbo-consumer
<div align="center"> <img src="https://github.com/heibaiying/spring-samples-for-all/blob/master/pictures/boot-dubbo-consumer1.png"/> </div>
#### 1.消费方的配置
```yaml
dubbo:
application:
name: boot-duboo-provider
# 指定注册协议和注册地址 dubbo 推荐使用 zookeeper 作为注册中心,并且在 start 依赖中引入了 zookeeper 的 java 客户端 Curator
registry:
protocol: zookeeper
address: 127.0.0.1:2181
protocol.name: dubbo
# 关闭所有服务的启动时检查 (没有提供者时报错)视实际情况设置
consumer:
check: false
server:
port: 8090
```
#### 2.使用注解@Reference引用远程服务
```java
package com.heibaiying.dubboconsumer.controller;
import com.alibaba.dubbo.config.annotation.Reference;
import com.heibaiying.api.IProductService;
import com.heibaiying.bean.Product;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import java.util.List;
@Controller
@RequestMapping("sell")
public class SellController {
// dubbo 远程引用注解
@Reference
private IProductService productService;
@RequestMapping
public String productList(Model model) {
List<Product> products = productService.queryAllProducts();
model.addAttribute("products", products);
return "products";
}
@RequestMapping("product/{id}")
public String productDetail(@PathVariable int id, Model model) {
Product product = productService.queryProductById(id);
model.addAttribute("product", product);
return "product";
}
}
```
## 六、项目构建的说明
因为在项目中consumer 和 provider 模块均依赖公共模块,所以在构建 consumer 和 provider 项目前需要将 common 模块安装到本地仓库,**依次**对**父工程**和**common 模块**执行:
```shell
mvn install -Dmaven.test.skip = true
```
consumer 中 pom.xml 如下
```xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<artifactId>spring-boot-dubbo</artifactId>
<groupId>com.heibaiying</groupId>
<version>0.0.1-SNAPSHOT</version>
</parent>
<artifactId>boot-dubbo-consumer</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>boot-dubbo-consumer</name>
<description>dubbo project for Spring Boot</description>
<properties>
<java.version>1.8</java.version>
</properties>
<!--引入对公共模块的依赖-->
<dependencies>
<dependency>
<groupId>com.heibaiying</groupId>
<artifactId>boot-dubbo-common</artifactId>
<version>0.0.1-SNAPSHOT</version>
<scope>compile</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
```
provider 中 pom.xml 如下
```xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<artifactId>spring-boot-dubbo</artifactId>
<groupId>com.heibaiying</groupId>
<version>0.0.1-SNAPSHOT</version>
</parent>
<artifactId>boot-dubbo-provider</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>boot-dubbo-provider</name>
<description>dubbo project for Spring Boot</description>
<properties>
<java.version>1.8</java.version>
</properties>
<!--引入对公共模块的依赖-->
<dependencies>
<dependency>
<groupId>com.heibaiying</groupId>
<artifactId>boot-dubbo-common</artifactId>
<version>0.0.1-SNAPSHOT</version>
<scope>compile</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
```
## 七、关于dubbo新版本管理控制台的安装说明
安装:
```sh
git clone https://github.com/apache/incubator-dubbo-ops.git /var/tmp/dubbo-ops
cd /var/tmp/dubbo-ops
mvn clean package
```
配置:
```sh
配置文件为:
dubbo-admin-backend/src/main/resources/application.properties
主要的配置有 默认的配置就是 127.0.0.1:2181
dubbo.registry.address=zookeeper://127.0.0.1:2181
```
启动:
```sh
mvn --projects dubbo-admin-backend spring-boot:run
```
访问:
```
http://127.0.0.1:8080
```

View File

@ -1,5 +1,6 @@
# spring boot 内置容器 整合 jsp
# spring boot 内置容器 整合 jsp
## 目录<br/>
<a href="#一说明">一、说明</a><br/>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href="#11-项目结构">1.1 项目结构</a><br/>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href="#12-项目主要依赖">1.2 项目主要依赖</a><br/>
@ -12,85 +13,85 @@
## 一、说明
#### 1.1 项目结构
<div align="center"> <img src="https://github.com/heibaiying/spring-samples-for-all/blob/master/pictures/spring-boot-jsp.png"/> </div>
#### 1.2 项目主要依赖
```xml
<!--整合 jsp 依赖包-->
<dependency>
<groupId>org.apache.tomcat.embed</groupId>
<artifactId>tomcat-embed-jasper</artifactId>
<scope>provided</scope>
</dependency>
<!--jsp jstl 标签支持-->
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>jstl</artifactId>
</dependency>
```
## 二、整合 jsp
#### 2.1 导入整合的依赖
```xml
<!--整合 jsp 依赖包-->
<dependency>
<groupId>org.apache.tomcat.embed</groupId>
<artifactId>tomcat-embed-jasper</artifactId>
<scope>provided</scope>
</dependency>
<!--jsp jstl 标签支持-->
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>jstl</artifactId>
</dependency>
```
#### 2.2 在application.yml 中指定访问视图文件的前缀和后缀
```yml
spring:
mvc:
view:
prefix: /WEB-INF/jsp/
suffix: .jsp
```
#### 2.3 新建controller和show.jsp 测试整合是否成功
```java
@Controller
@RequestMapping("index")
public class JspController {
@RequestMapping
public String jsp(Model model){
Programmer programmer = new Programmer("heibai", 21, 1298.31f, LocalDate.now());
model.addAttribute("programmer",programmer);
return "show";
}
}
```
```jsp
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
<title>programmer</title>
<link rel="stylesheet" href="${pageContext.request.contextPath}/css/show.css">
</head>
<body>
<ul>
<li>姓名: ${programmer.name}</li>
<li>年龄: ${programmer.age}</li>
</ul>
</body>
</html>
```
## 一、说明
#### 1.1 项目结构
<div align="center"> <img src="https://github.com/heibaiying/spring-samples-for-all/blob/master/pictures/spring-boot-jsp.png"/> </div>
#### 1.2 项目主要依赖
```xml
<!--整合 jsp 依赖包-->
<dependency>
<groupId>org.apache.tomcat.embed</groupId>
<artifactId>tomcat-embed-jasper</artifactId>
<scope>provided</scope>
</dependency>
<!--jsp jstl 标签支持-->
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>jstl</artifactId>
</dependency>
```
## 二、整合 jsp
#### 2.1 导入整合的依赖
```xml
<!--整合 jsp 依赖包-->
<dependency>
<groupId>org.apache.tomcat.embed</groupId>
<artifactId>tomcat-embed-jasper</artifactId>
<scope>provided</scope>
</dependency>
<!--jsp jstl 标签支持-->
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>jstl</artifactId>
</dependency>
```
#### 2.2 在application.yml 中指定访问视图文件的前缀和后缀
```yml
spring:
mvc:
view:
prefix: /WEB-INF/jsp/
suffix: .jsp
```
#### 2.3 新建controller和show.jsp 测试整合是否成功
```java
@Controller
@RequestMapping("index")
public class JspController {
@RequestMapping
public String jsp(Model model){
Programmer programmer = new Programmer("heibai", 21, 1298.31f, LocalDate.now());
model.addAttribute("programmer",programmer);
return "show";
}
}
```
```jsp
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
<title>programmer</title>
<link rel="stylesheet" href="${pageContext.request.contextPath}/css/show.css">
</head>
<body>
<ul>
<li>姓名: ${programmer.name}</li>
<li>年龄: ${programmer.age}</li>
</ul>
</body>
</html>
```

View File

@ -1,5 +1,6 @@
# spring boot 整合 kafka
# spring boot 整合 kafka
## 目录<br/>
<a href="#一kafka的相关概念">一、kafka的相关概念</a><br/>
&nbsp;&nbsp;&nbsp;&nbsp;<a href="#1主题和分区">1.主题和分区</a><br/>
&nbsp;&nbsp;&nbsp;&nbsp;<a href="#2分区复制">2.分区复制</a><br/>
@ -25,374 +26,374 @@
## 一、kafka的相关概念
### 1.主题和分区
kafka 的消息通过主题进行分类。一个主题可以被分为若干个分区一个分区就是一个提交日志。消息以追加的方式写入分区然后以先入先出的顺序读取。kafka通过分区来实现数据的冗余和伸缩性分区可以分布在不同的服务器上也就是说一个主题可以横跨多个服务器以此来提供比单个服务器更强大的性能类比HDFS分布式文件系统
注意:由于一个主题包含多个分区,因此无法在整个主题范围内保证消息的顺序性,**但可以保证消息在单个分区内的顺序性**。
<div align="center"> <img src="https://github.com/heibaiying/spring-samples-for-all/blob/master/pictures/kafka主题和分区.png"/> </div>
### 2.分区复制
每个主题被分为若干个分区,每个分区有多个副本。那些副本被保存在 broker 上,每个 broker 可以保存成百上千个属于不同主题和分区的副本。副本有以下两种类型
- 首领副本 每个分区都有一个首领副本 。 为了保证一致性,所有生产者请求和消费者请求都会经过这个副本
- 跟随者副本 首领以外的副本都是跟随者副本。跟随者副本不处理来自客户端的请求,它们唯一的任务就是从首领那里复制消息,保持与首领一致的状态。如果首领发生崩渍,其中的一个跟随者会被提升为新首领。
### 3. 生产者
- 默认情况下生产者在把消息均衡地分布到在主题的所有分区上,而并不关心特定消息会被写到那个分区;
- 如果指定消息键,则通过消息键的散列来实现分区;
- 也可以通过消息键和分区器来实现把消息直接写到指定的分区这个需要自定义分区器需要实现Partitioner 接口并重写其中的partition方法。
### 4. 消费者
消费者是**消费者群组**的一部分。也就是说,会有一个或者多个消费者共同读取一个主题,群组保证每个分区只能被一个消费者使用。
**一个分区只能被同一个消费者群组里面的一个消费者读取,但可以被不同消费者群组里面的多个消费者读取。多个消费者群组可以共同读取同一个主题,彼此之间互不影响**
<div align="center"> <img src="https://github.com/heibaiying/spring-samples-for-all/blob/master/pictures/kafka消费者.png"/> </div>
### 5.broker和集群
一个独立的kafka服务器被称为broker。broker 接收来自生产者的消息为消息设置偏移量并提交消息到磁盘保存。broker为消费者提供服务对读取分区的请求做出响应返回已经提交到磁盘的消息。
broker是集群的组成部分。每一个集群都有一个broker同时充当了集群控制器的角色自动从集群的活跃成员中选举出来。控制器负责管理工作包括将分区分配给broker和监控broker。**在集群中一个分区从属一个broker,该broker被称为分区的首领**。一个分区可以分配给多个broker,这个时候会发生分区复制。这种复制机制为分区提供了消息冗余如果有一个broker失效其他broker可以接管领导权。
<div align="center"> <img src="https://github.com/heibaiying/spring-samples-for-all/blob/master/pictures/kafka集群复制.png"/> </div>
更多kafka 的说明可以参考我的个人笔记:[《Kafka权威指南》读书笔记](https://github.com/heibaiying/LearningNotes/blob/master/notes/%E4%B8%AD%E9%97%B4%E4%BB%B6/Kafka/%E3%80%8AKafka%E6%9D%83%E5%A8%81%E6%8C%87%E5%8D%97%E3%80%8B%E8%AF%BB%E4%B9%A6%E7%AC%94%E8%AE%B0.md#53-%E5%A4%8D%E5%88%B6)
## 二、项目说明
#### 1.1 项目结构说明
本项目提供kafka发送简单消息、对象消息、和多消费者组消费消息三种情况下的sample。
1. kafkaSimpleConsumer 用于普通消息监听;
2. kafkaBeanConsumer 用于对象消息监听;
3. kafkaGroupConsumer 用于多消费者组和多消费者对主题分区消息监听的情况。
<div align="center"> <img src="https://github.com/heibaiying/spring-samples-for-all/blob/master/pictures/spring-boot-kafka.png"/> </div>
#### 1.2 主要依赖
```xml
<dependency>
<groupId>org.springframework.kafka</groupId>
<artifactId>spring-kafka</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.kafka</groupId>
<artifactId>spring-kafka-test</artifactId>
<scope>test</scope>
</dependency>
```
## 二、 整合 kafka
#### 2.1 kafka基本配置
```yaml
spring:
kafka:
# 以逗号分隔的地址列表用于建立与Kafka集群的初始连接(kafka 默认的端口号为9092)
bootstrap-servers: 127.0.0.1:9092
producer:
# 发生错误后,消息重发的次数。
retries: 0
#当有多个消息需要被发送到同一个分区时,生产者会把它们放在同一个批次里。该参数指定了一个批次可以使用的内存大小,按照字节数计算。
batch-size: 16384
# 设置生产者内存缓冲区的大小。
buffer-memory: 33554432
# 键的序列化方式
key-serializer: org.apache.kafka.common.serialization.StringSerializer
# 值的序列化方式
value-serializer: org.apache.kafka.common.serialization.StringSerializer
# acks=0 生产者在成功写入消息之前不会等待任何来自服务器响应。
# acks=1 只要集群的首领节点收到消息,生产者会收到一个来自服务器成功响应。
# acks=all :只有当所有参与复制的节点全部收到消息时,生产者才会收到一个来自服务器的成功响应。
acks: 1
consumer:
# 自动提交的时间间隔 在spring boot 2.X 版本中这里采用的是值的类型为Duration 需要符合特定的格式如1S,1M,2H,5D
auto-commit-interval: 1S
# 该属性指定了消费者在读取一个没有偏移量的分区或者偏移量无效的情况下该作何处理:
# latest默认值在偏移量无效的情况下,消费者将从最新的记录开始读取数据(在消费者启动之后生成的记录
# earliest :在偏移量无效的情况下,消费者将从起始位置读取分区的记录
auto-offset-reset: earliest
# 是否自动提交偏移量默认值是true,为了避免出现重复数据和数据丢失可以把它设置为false,然后手动提交偏移量
enable-auto-commit: true
# 键的反序列化方式
key-deserializer: org.apache.kafka.common.serialization.StringDeserializer
# 值的反序列化方式
value-deserializer: org.apache.kafka.common.serialization.StringDeserializer
listener:
# 在侦听器容器中运行的线程数。
concurrency: 5
```
这里需要说明的是:
在spring boot 2.X 版本 auto-commit-interval自动提交的时间间隔采用的是值的类型为Duration Duration 是 jdk 1.8 版本之后引入的类,在其源码中我们可以看到对于其字符串的表达需要符合一定的规范,即数字+单位如下的写法1s 1.5s 0s 0.001S 1h 2d 在yaml 中都是有效的。如果传入无效的字符串则spring boot 在启动阶段解析配置文件的时候就会抛出异常。
```java
public final class Duration
implements TemporalAmount, Comparable<Duration>, Serializable {
/**
* The pattern for parsing.
*/
private static final Pattern PATTERN =
Pattern.compile("([-+]?)P(?:([-+]?[0-9]+)D)?" +
"(T(?:([-+]?[0-9]+)H)?(?:([-+]?[0-9]+)M)?(?:([-+]?[0-9]+)(?:[.,]([0-9]{0,9}))?S)?)?", Pattern.CASE_INSENSITIVE);
........
}
```
#### 2.2 KafkaTemplate实现消息发送
```java
@Component
@Slf4j
public class KafKaCustomrProducer {
@Autowired
private KafkaTemplate kafkaTemplate;
public void sendMessage(String topic, Object object) {
/*
* 这里的ListenableFuture类是spring对java原生Future的扩展增强,是一个泛型接口,用于监听异步方法的回调
* 而对于kafka send 方法返回值而言,这里的泛型所代表的实际类型就是 SendResult<K, V>,而这里K,V的泛型实际上
* 被用于ProducerRecord<K, V> producerRecord,即生产者发送消息的key,value 类型
*/
ListenableFuture<SendResult<String, Object>> future = kafkaTemplate.send(topic, object);
future.addCallback(new ListenableFutureCallback<SendResult<String, Object>>() {
@Override
public void onFailure(Throwable throwable) {
log.info("发送消息失败:" + throwable.getMessage());
}
@Override
public void onSuccess(SendResult<String, Object> sendResult) {
System.out.println("发送结果:" + sendResult.toString());
}
});
}
}
```
#### 2.3 @KafkaListener注解实现消息的监听
```java
@Component
@Slf4j
public class KafkaSimpleConsumer {
// 简单消费者
@KafkaListener(groupId = "simpleGroup", topics = Topic.SIMPLE)
public void consumer1_1(ConsumerRecord<String, Object> record, @Header(KafkaHeaders.RECEIVED_TOPIC) String topic, Consumer consumer) {
System.out.println("消费者收到消息:" + record.value() + "; topic:" + topic);
/*
* 如果需要手工提交异步 consumer.commitSync();
* 手工同步提交 consumer.commitAsync()
*/
}
}
```
#### 2.4 测试整合结果
```java
@Slf4j
@RestController
public class SendMsgController {
@Autowired
private KafKaCustomrProducer producer;
@Autowired
private KafkaTemplate kafkaTemplate;
/***
* 发送消息体为基本类型的消息
*/
@GetMapping("sendSimple")
public void sendSimple() {
producer.sendMessage(Topic.SIMPLE, "hello spring boot kafka");
}
}
```
## 三、关于多消费者组的测试
#### 3.1 创建多分区主题
```java
/**
* @author : heibaiying
* @description : kafka配置类
*/
@Configuration
public class KafkaConfig {
@Bean
public NewTopic groupTopic() {
// 指定主题名称,分区数量,和复制因子
return new NewTopic(Topic.GROUP, 10, (short) 2);
}
}
```
#### 3.2 多消费者组对同一主题的监听
1. 消费者1-1 监听主题的 0、1 分区
2. 消费者1-2 监听主题的 2、3 分区
3. 消费者1-3 监听主题的 0、1 分区
4. 消费者2-1 监听主题的所有分区
```java
/**
* @author : heibaiying
* @description : kafka 消费者组
* <p>
* 多个消费者群组可以共同读取同一个主题,彼此之间互不影响。
*/
@Component
@Slf4j
public class KafkaGroupConsumer {
// 分组1 中的消费者1
@KafkaListener(id = "consumer1-1", groupId = "group1", topicPartitions =
{@TopicPartition(topic = Topic.GROUP, partitions = {"0", "1"})
})
public void consumer1_1(ConsumerRecord<String, Object> record) {
System.out.println("consumer1-1 收到消息:" + record.value());
}
// 分组1 中的消费者2
@KafkaListener(id = "consumer1-2", groupId = "group1", topicPartitions =
{@TopicPartition(topic = Topic.GROUP, partitions = {"2", "3"})
})
public void consumer1_2(ConsumerRecord<String, Object> record) {
System.out.println("consumer1-2 收到消息:" + record.value());
}
// 分组1 中的消费者3
@KafkaListener(id = "consumer1-3", groupId = "group1", topicPartitions =
{@TopicPartition(topic = Topic.GROUP, partitions = {"0", "1"})
})
public void consumer1_3(ConsumerRecord<String, Object> record) {
System.out.println("consumer1-3 收到消息:" + record.value());
}
// 分组2 中的消费者
@KafkaListener(id = "consumer2-1", groupId = "group2", topics = Topic.GROUP)
public void consumer2_1(ConsumerRecord<String, Object> record) {
System.err.println("consumer2-1 收到消息:" + record.value());
}
}
```
#### 3.2 发送消息时候指定主题的具体分区
```java
/***
* 多消费者组、组中多消费者对同一主题的消费情况
*/
@GetMapping("sendGroup")
public void sendGroup() {
for (int i = 0; i < 4; i++) {
// 第二个参数指定分区,第三个参数指定消息键 分区优先
ListenableFuture<SendResult<String, Object>> future = kafkaTemplate.send(Topic.GROUP, i % 4, "key", "hello group " + i);
future.addCallback(new ListenableFutureCallback<SendResult<String, Object>>() {
@Override
public void onFailure(Throwable throwable) {
log.info("发送消息失败:" + throwable.getMessage());
}
@Override
public void onSuccess(SendResult<String, Object> sendResult) {
System.out.println("发送结果:" + sendResult.toString());
}
});
}
}
```
测试结果:
```yaml
# 主要看每次发送结果中的 partition 属性代表四次消息分别发送到了主题的0,1,2,3分区
发送结果:SendResult [producerRecord=ProducerRecord(topic=spring.boot.kafka.newGroup, partition=1, headers=RecordHeaders(headers = [], isReadOnly = true), key=key, value=hello group 1, timestamp=null), recordMetadata=spring.boot.kafka.newGroup-1@13]
发送结果:SendResult [producerRecord=ProducerRecord(topic=spring.boot.kafka.newGroup, partition=0, headers=RecordHeaders(headers = [], isReadOnly = true), key=key, value=hello group 0, timestamp=null), recordMetadata=spring.boot.kafka.newGroup-0@19]
发送结果:SendResult [producerRecord=ProducerRecord(topic=spring.boot.kafka.newGroup, partition=3, headers=RecordHeaders(headers = [], isReadOnly = true), key=key, value=hello group 3, timestamp=null), recordMetadata=spring.boot.kafka.newGroup-3@13]
发送结果:SendResult [producerRecord=ProducerRecord(topic=spring.boot.kafka.newGroup, partition=2, headers=RecordHeaders(headers = [], isReadOnly = true), key=key, value=hello group 2, timestamp=null), recordMetadata=spring.boot.kafka.newGroup-2@13]
# 消费者组2 接收情况
consumer2-1 收到消息:hello group 1
consumer2-1 收到消息:hello group 0
consumer2-1 收到消息:hello group 2
consumer2-1 收到消息:hello group 3
# 消费者1-1接收情况
consumer1-1 收到消息:hello group 1
consumer1-1 收到消息:hello group 0
# 消费者1-3接收情况
consumer1-3 收到消息:hello group 1
consumer1-3 收到消息:hello group 0
# 消费者1-2接收情况
consumer1-2 收到消息:hello group 3
consumer1-2 收到消息:hello group 2
```
#### 3.4 测试结果
1. 和kafka 原本机制一样,多消费者组之间对于同一个主题的消费彼此之间互不影响;
2. 和kafka原本机制不一样的是这里我们消费者1-1和消费1-3共同属于同一个消费者组并且监听同样的分区按照原本kafka的机制群组保证每个分区只能被同一个消费者组的一个消费者使用但是按照spring的声明方式实现的消息监听这里被两个消费者都监听到了。
## 四、序列化与反序列化
用例采用的是第三方fastjson将实体类序列化为json后发送。实现如下
```java
/***
* 发送消息体为bean的消息
*/
@GetMapping("sendBean")
public void sendBean() {
Programmer programmer = new Programmer("xiaoming", 12, 21212.33f, new Date());
producer.sendMessage(Topic.BEAN, JSON.toJSON(programmer).toString());
}
```
```java
@Component
@Slf4j
public class KafkaBeanConsumer {
@KafkaListener(groupId = "beanGroup",topics = Topic.BEAN)
public void consumer(ConsumerRecord<String, Object> record) {
System.out.println("消费者收到消息:" + JSON.parseObject(record.value().toString(), Programmer.class));
}
}
```
## 一、kafka的相关概念
### 1.主题和分区
kafka 的消息通过主题进行分类。一个主题可以被分为若干个分区一个分区就是一个提交日志。消息以追加的方式写入分区然后以先入先出的顺序读取。kafka 通过分区来实现数据的冗余和伸缩性,分区可以分布在不同的服务器上,也就是说一个主题可以横跨多个服务器,以此来提供比单个服务器更强大的性能(类比 HDFS 分布式文件系统)。
注意:由于一个主题包含多个分区,因此无法在整个主题范围内保证消息的顺序性,**但可以保证消息在单个分区内的顺序性**。
<div align="center"> <img src="https://github.com/heibaiying/spring-samples-for-all/blob/master/pictures/kafka主题和分区.png"/> </div>
### 2.分区复制
每个主题被分为若干个分区,每个分区有多个副本。那些副本被保存在 broker 上,每个 broker 可以保存成百上千个属于不同主题和分区的副本。副本有以下两种类型
- 首领副本 每个分区都有一个首领副本 。 为了保证一致性,所有生产者请求和消费者请求都会经过这个副本。
- 跟随者副本 首领以外的副本都是跟随者副本。跟随者副本不处理来自客户端的请求,它们唯一的任务就是从首领那里复制消息,保持与首领一致的状态。如果首领发生崩渍,其中的一个跟随者会被提升为新首领
### 3. 生产者
- 默认情况下生产者在把消息均衡地分布到在主题的所有分区上,而并不关心特定消息会被写到那个分区;
- 如果指定消息键,则通过对消息键的散列来实现分区;
- 也可以通过消息键和分区器来实现把消息直接写到指定的分区,这个需要自定义分区器,需要实现 Partitioner 接口,并重写其中的 partition 方法。
### 4. 消费者
消费者是**消费者群组**的一部分。也就是说,会有一个或者多个消费者共同读取一个主题,群组保证每个分区只能被一个消费者使用。
**一个分区只能被同一个消费者群组里面的一个消费者读取,但可以被不同消费者群组里面的多个消费者读取。多个消费者群组可以共同读取同一个主题,彼此之间互不影响**
<div align="center"> <img src="https://github.com/heibaiying/spring-samples-for-all/blob/master/pictures/kafka消费者.png"/> </div>
### 5.broker和集群
一个独立的 kafka 服务器被称为 broker。broker 接收来自生产者的消息为消息设置偏移量并提交消息到磁盘保存。broker 为消费者提供服务,对读取分区的请求做出响应,返回已经提交到磁盘的消息。
broker 是集群的组成部分。每一个集群都有一个 broker 同时充当了集群控制器的角色(自动从集群的活跃成员中选举出来)。控制器负责管理工作,包括将分区分配给 broker 和监控 broker。**在集群中,一个分区从属一个 broker,该 broker 被称为分区的首领**。一个分区可以分配给多个 broker,这个时候会发生分区复制。这种复制机制为分区提供了消息冗余,如果有一个 broker 失效,其他 broker 可以接管领导权。
<div align="center"> <img src="https://github.com/heibaiying/spring-samples-for-all/blob/master/pictures/kafka集群复制.png"/> </div>
更多 kafka 的说明可以参考我的个人笔记:[《Kafka 权威指南》读书笔记](https://github.com/heibaiying/LearningNotes/blob/master/notes/%E4%B8%AD%E9%97%B4%E4%BB%B6/Kafka/%E3%80%8AKafka%E6%9D%83%E5%A8%81%E6%8C%87%E5%8D%97%E3%80%8B%E8%AF%BB%E4%B9%A6%E7%AC%94%E8%AE%B0.md#53-%E5%A4%8D%E5%88%B6)
## 二、项目说明
#### 1.1 项目结构说明
本项目提供 kafka 发送简单消息、对象消息、和多消费者组消费消息三种情况下的 sample。
1. kafkaSimpleConsumer 用于普通消息的监听;
2. kafkaBeanConsumer 用于对象消息监听;
3. kafkaGroupConsumer 用于多消费者组和多消费者对主题分区消息监听的情况。
<div align="center"> <img src="https://github.com/heibaiying/spring-samples-for-all/blob/master/pictures/spring-boot-kafka.png"/> </div>
#### 1.2 主要依赖
```xml
<dependency>
<groupId>org.springframework.kafka</groupId>
<artifactId>spring-kafka</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.kafka</groupId>
<artifactId>spring-kafka-test</artifactId>
<scope>test</scope>
</dependency>
```
## 二、 整合 kafka
#### 2.1 kafka基本配置
```yaml
spring:
kafka:
# 以逗号分隔的地址列表,用于建立与 Kafka 集群的初始连接 (kafka 默认的端口号为 9092)
bootstrap-servers: 127.0.0.1:9092
producer:
# 发生错误后,消息重发的次数。
retries: 0
#当有多个消息需要被发送到同一个分区时,生产者会把它们放在同一个批次里。该参数指定了一个批次可以使用的内存大小,按照字节数计算。
batch-size: 16384
# 设置生产者内存缓冲区的大小。
buffer-memory: 33554432
# 键的序列化方式
key-serializer: org.apache.kafka.common.serialization.StringSerializer
# 值的序列化方式
value-serializer: org.apache.kafka.common.serialization.StringSerializer
# acks=0 生产者在成功写入消息之前不会等待任何来自服务器的响应。
# acks=1 只要集群的首领节点收到消息,生产者就会收到一个来自服务器成功响应。
# acks=all :只有当所有参与复制的节点全部收到消息,生产者会收到一个来自服务器成功响应。
acks: 1
consumer:
# 自动提交的时间间隔 在 spring boot 2.X 版本中这里采用的是值的类型为 Duration 需要符合特定的格式,如 1S,1M,2H,5D
auto-commit-interval: 1S
# 该属性指定了消费者在读取一个没有偏移量的分区或者偏移量无效的情况下该作何处理:
# latest默认值在偏移量无效的情况下消费者将从最新的记录开始读取数据在消费者启动之后生成的记录
# earliest 在偏移量无效的情况下,消费者将从起始位置读取分区的记录
auto-offset-reset: earliest
# 是否自动提交偏移量,默认值是 true,为了避免出现重复数据和数据丢失,可以把它设置为 false,然后手动提交偏移量
enable-auto-commit: true
# 键的反序列化方式
key-deserializer: org.apache.kafka.common.serialization.StringDeserializer
# 值的反序列化方式
value-deserializer: org.apache.kafka.common.serialization.StringDeserializer
listener:
# 在侦听器容器中运行的线程数。
concurrency: 5
```
这里需要说明的是:
在 spring boot 2.X 版本 auto-commit-interval自动提交的时间间隔采用的是值的类型为 Duration Duration 是 jdk 1.8 版本之后引入的类,在其源码中我们可以看到对于其字符串的表达需要符合一定的规范,即数字 + 单位,如下的写法 1s 1.5s 0s 0.001S 1h 2d 在 yaml 中都是有效的。如果传入无效的字符串,则 spring boot 在启动阶段解析配置文件的时候就会抛出异常。
```java
public final class Duration
implements TemporalAmount, Comparable<Duration>, Serializable {
/**
* The pattern for parsing.
*/
private static final Pattern PATTERN =
Pattern.compile("([-+]?)P(?:([-+]?[0-9]+)D)?" +
"(T(?:([-+]?[0-9]+)H)?(?:([-+]?[0-9]+)M)?(?:([-+]?[0-9]+)(?:[.,]([0-9]{0,9}))?S)?)?", Pattern.CASE_INSENSITIVE);
........
}
```
#### 2.2 KafkaTemplate实现消息发送
```java
@Component
@Slf4j
public class KafKaCustomrProducer {
@Autowired
private KafkaTemplate kafkaTemplate;
public void sendMessage(String topic, Object object) {
/*
* 这里的 ListenableFuture 类是 spring 对 java 原生 Future 的扩展增强,是一个泛型接口,用于监听异步方法的回调
* 而对于 kafka send 方法返回值而言,这里的泛型所代表的实际类型就是 SendResult<K, V>,而这里 K,V 的泛型实际上
* 被用于 ProducerRecord<K, V> producerRecord,即生产者发送消息的 key,value 类型
*/
ListenableFuture<SendResult<String, Object>> future = kafkaTemplate.send(topic, object);
future.addCallback(new ListenableFutureCallback<SendResult<String, Object>>() {
@Override
public void onFailure(Throwable throwable) {
log.info("发送消息失败:" + throwable.getMessage());
}
@Override
public void onSuccess(SendResult<String, Object> sendResult) {
System.out.println("发送结果:" + sendResult.toString());
}
});
}
}
```
#### 2.3 @KafkaListener注解实现消息的监听
```java
@Component
@Slf4j
public class KafkaSimpleConsumer {
// 简单消费者
@KafkaListener(groupId = "simpleGroup", topics = Topic.SIMPLE)
public void consumer1_1(ConsumerRecord<String, Object> record, @Header(KafkaHeaders.RECEIVED_TOPIC) String topic, Consumer consumer) {
System.out.println("消费者收到消息:" + record.value() + "; topic:" + topic);
/*
* 如果需要手工提交异步 consumer.commitSync();
* 手工同步提交 consumer.commitAsync()
*/
}
}
```
#### 2.4 测试整合结果
```java
@Slf4j
@RestController
public class SendMsgController {
@Autowired
private KafKaCustomrProducer producer;
@Autowired
private KafkaTemplate kafkaTemplate;
/***
* 发送消息体为基本类型的消息
*/
@GetMapping("sendSimple")
public void sendSimple() {
producer.sendMessage(Topic.SIMPLE, "hello spring boot kafka");
}
}
```
## 三、关于多消费者组的测试
#### 3.1 创建多分区主题
```java
/**
* @author : heibaiying
* @description : kafka 配置类
*/
@Configuration
public class KafkaConfig {
@Bean
public NewTopic groupTopic() {
// 指定主题名称,分区数量,和复制因子
return new NewTopic(Topic.GROUP, 10, (short) 2);
}
}
```
#### 3.2 多消费者组对同一主题的监听
1. 消费者 1-1 监听主题的 0、1 分区
2. 消费者 1-2 监听主题的 2、3 分区
3. 消费者 1-3 监听主题的 0、1 分区
4. 消费者 2-1 监听主题的所有分区
```java
/**
* @author : heibaiying
* @description : kafka 消费者组
* <p>
* 多个消费者群组可以共同读取同一个主题,彼此之间互不影响。
*/
@Component
@Slf4j
public class KafkaGroupConsumer {
// 分组 1 中的消费者 1
@KafkaListener(id = "consumer1-1", groupId = "group1", topicPartitions =
{@TopicPartition(topic = Topic.GROUP, partitions = {"0", "1"})
})
public void consumer1_1(ConsumerRecord<String, Object> record) {
System.out.println("consumer1-1 收到消息:" + record.value());
}
// 分组 1 中的消费者 2
@KafkaListener(id = "consumer1-2", groupId = "group1", topicPartitions =
{@TopicPartition(topic = Topic.GROUP, partitions = {"2", "3"})
})
public void consumer1_2(ConsumerRecord<String, Object> record) {
System.out.println("consumer1-2 收到消息:" + record.value());
}
// 分组 1 中的消费者 3
@KafkaListener(id = "consumer1-3", groupId = "group1", topicPartitions =
{@TopicPartition(topic = Topic.GROUP, partitions = {"0", "1"})
})
public void consumer1_3(ConsumerRecord<String, Object> record) {
System.out.println("consumer1-3 收到消息:" + record.value());
}
// 分组 2 中的消费者
@KafkaListener(id = "consumer2-1", groupId = "group2", topics = Topic.GROUP)
public void consumer2_1(ConsumerRecord<String, Object> record) {
System.err.println("consumer2-1 收到消息:" + record.value());
}
}
```
#### 3.2 发送消息时候指定主题的具体分区
```java
/***
* 多消费者组、组中多消费者对同一主题的消费情况
*/
@GetMapping("sendGroup")
public void sendGroup() {
for (int i = 0; i < 4; i++) {
// 第二个参数指定分区,第三个参数指定消息键 分区优先
ListenableFuture<SendResult<String, Object>> future = kafkaTemplate.send(Topic.GROUP, i % 4, "key", "hello group " + i);
future.addCallback(new ListenableFutureCallback<SendResult<String, Object>>() {
@Override
public void onFailure(Throwable throwable) {
log.info("发送消息失败:" + throwable.getMessage());
}
@Override
public void onSuccess(SendResult<String, Object> sendResult) {
System.out.println("发送结果:" + sendResult.toString());
}
});
}
}
```
测试结果:
```yaml
# 主要看每次发送结果中的 partition 属性代表四次消息分别发送到了主题的0,1,2,3分区
发送结果:SendResult [producerRecord=ProducerRecord(topic=spring.boot.kafka.newGroup, partition=1, headers=RecordHeaders(headers = [], isReadOnly = true), key=key, value=hello group 1, timestamp=null), recordMetadata=spring.boot.kafka.newGroup-1@13]
发送结果:SendResult [producerRecord=ProducerRecord(topic=spring.boot.kafka.newGroup, partition=0, headers=RecordHeaders(headers = [], isReadOnly = true), key=key, value=hello group 0, timestamp=null), recordMetadata=spring.boot.kafka.newGroup-0@19]
发送结果:SendResult [producerRecord=ProducerRecord(topic=spring.boot.kafka.newGroup, partition=3, headers=RecordHeaders(headers = [], isReadOnly = true), key=key, value=hello group 3, timestamp=null), recordMetadata=spring.boot.kafka.newGroup-3@13]
发送结果:SendResult [producerRecord=ProducerRecord(topic=spring.boot.kafka.newGroup, partition=2, headers=RecordHeaders(headers = [], isReadOnly = true), key=key, value=hello group 2, timestamp=null), recordMetadata=spring.boot.kafka.newGroup-2@13]
# 消费者组2 接收情况
consumer2-1 收到消息:hello group 1
consumer2-1 收到消息:hello group 0
consumer2-1 收到消息:hello group 2
consumer2-1 收到消息:hello group 3
# 消费者1-1接收情况
consumer1-1 收到消息:hello group 1
consumer1-1 收到消息:hello group 0
# 消费者1-3接收情况
consumer1-3 收到消息:hello group 1
consumer1-3 收到消息:hello group 0
# 消费者1-2接收情况
consumer1-2 收到消息:hello group 3
consumer1-2 收到消息:hello group 2
```
#### 3.4 测试结果
1. 和 kafka 原本的机制一样,多消费者组之间对于同一个主题的消费彼此之间互不影响;
2. kafka 原本机制一样的是,这里我们消费者 1-1 和消费 1-3 共同属于同一个消费者组,并且监听同样的分区,按照原本 kafka 的机制,群组保证每个分区只能被同一个消费者组的一个消费者使用,但是按照 spring 的声明方式实现的消息监听,这里被两个消费者都监听到了。
## 四、序列化与反序列化
用例采用的是第三方 fastjson 将实体类序列化为 json 后发送。实现如下:
```java
/***
* 发送消息体为 bean 的消息
*/
@GetMapping("sendBean")
public void sendBean() {
Programmer programmer = new Programmer("xiaoming", 12, 21212.33f, new Date());
producer.sendMessage(Topic.BEAN, JSON.toJSON(programmer).toString());
}
```
```java
@Component
@Slf4j
public class KafkaBeanConsumer {
@KafkaListener(groupId = "beanGroup",topics = Topic.BEAN)
public void consumer(ConsumerRecord<String, Object> record) {
System.out.println("消费者收到消息:" + JSON.parseObject(record.value().toString(), Programmer.class));
}
}
```

View File

@ -1,5 +1,6 @@
# spring boot 整合 mecached
# spring boot 整合 mecached
## 目录<br/>
<a href="#一说明">一、说明</a><br/>
&nbsp;&nbsp;&nbsp;&nbsp;<a href="#11--XMemcached客户端说明">1.1 XMemcached客户端说明</a><br/>
&nbsp;&nbsp;&nbsp;&nbsp;<a href="#12-项目结构说明">1.2 项目结构说明</a><br/>
@ -15,149 +16,149 @@
## 一、说明
### 1.1 XMemcached客户端说明
spring boot 官方并没有提供关于 memcached 的starter所以我们这里还是采用XMemcached作为客户端进行整合。 XMemcached是基于java nio的memcached高性能客户端支持完整的memcached协议支持客户端分布并且提供了一致性哈希(consistent hash)算法的实现。
### 1.2 项目结构说明
memcached的整合配置位于config文件夹下。
<div align="center"> <img src="https://github.com/heibaiying/spring-samples-for-all/blob/master/pictures/spring-boot-memcached.png"/> </div>
### 1.3 主要依赖
```xml
<!--memcached java 客户端-->
<dependency>
<groupId>com.googlecode.xmemcached</groupId>
<artifactId>xmemcached</artifactId>
<version>2.4.5</version>
</dependency>
```
## 二、spring boot 整合 memcached
#### 2.1 单机配置
```java
@Bean
public MemcachedClient memcachedClient() {
XMemcachedClientBuilder builder = new XMemcachedClientBuilder("192.168.200.201:11211");
MemcachedClient memcachedClient = null;
try {
memcachedClient = builder.build();
} catch (IOException e) {
e.printStackTrace();
}
return memcachedClient;
}
```
#### 2.2 集群配置
```java
@Bean
public MemcachedClient memcachedClientForCluster() {
List<InetSocketAddress> addressList = new ArrayList<InetSocketAddress>();
addressList.add(new InetSocketAddress("192.168.200.201", 11211));
addressList.add(new InetSocketAddress("192.168.200.201", 11212));
// 赋予权重
int[] weights = {1, 2};
XMemcachedClientBuilder builder = new XMemcachedClientBuilder(addressList, weights);
// 设置连接池大小
builder.setConnectionPoolSize(10);
// 协议工厂
builder.setCommandFactory(new TextCommandFactory());
// 分布策略一致性哈希KetamaMemcachedSessionLocator或者ArraySessionLocator(默认)
builder.setSessionLocator(new KetamaMemcachedSessionLocator());
// 设置序列化器
builder.setTranscoder(new SerializingTranscoder());
MemcachedClient memcachedClient = null;
try {
memcachedClient = builder.build();
} catch (IOException e) {
e.printStackTrace();
}
return memcachedClient;
}
```
#### 2.3 存储基本类型测试用例
xmemcached单机版本和集群版本注入的实例是相同的。
```java
/**
* @author : heibaiying
* @description : Memcached 操作基本对象
*/
@RunWith(SpringRunner.class)
@SpringBootTest
public class MemSamples {
@Autowired
private MemcachedClient memcachedClient;
@Test
public void operate() throws InterruptedException, MemcachedException, TimeoutException {
memcachedClient.set("hello", 0, "Hello,cluster xmemcached");
String value = memcachedClient.get("hello");
System.out.println("hello=" + value);
memcachedClient.delete("hello");
value = memcachedClient.get("hello");
System.out.println("hello=" + value);
}
}
```
#### 2.5 存储实体对象测试用例
```java
/**
* @author : heibaiying
* @description :Memcached 序列化与反序列化
*/
@RunWith(SpringRunner.class)
@SpringBootTest
public class MemObjectSamples {
@Autowired
private MemcachedClient memcachedClient;
@Test
public void operate() throws InterruptedException, MemcachedException, TimeoutException {
memcachedClient.set("programmer", 0, new Programmer("xiaoming", 12, 5000.21f, new Date()));
Programmer programmer = memcachedClient.get("programmer");
System.out.println("hello ," + programmer.getName());
memcachedClient.delete("programmer");
programmer = memcachedClient.get("programmer");
Assert.assertNull(programmer);
}
}
```
## 附memcached 基本命令
| 命令 | 格式 | 说明 |
| --------------- | -------------------------------------------------- | ------------------------------------- |
| 新增 set | set key flags exTime length -> value | 无论什么情况,都可以插入 |
| 新增 add | add key flags exTime length -> value | 只有当key不存在的情况下才可以插入 |
| 替换 replace | replace key flags exTime length -> value | 只修改已存在key的value值 |
| 追加内容append | append key flags exTime length -> value | length表示追加的长度而不是总长度 |
| 前面追加prepend | prepend key flags exTime length -> value | length表示追加的长度而不是总长度 |
| 查询操作 get | get key | |
| 检查更新 cas | cas key flags exTime length version -> value | 版本正确才更新 |
| 详细获取 gets | gets key | 返回的最后一个数代表 key 的 CAS 令牌 |
| 删除 delete | delete key | 将数据打一个删除标记 |
| 自增 incr | incr key 增加偏移量 | incr和decr只能操作能转换为数字的Value |
| 自减 decr | decr key 减少偏移量 | desr不能将数字减少至0以下 |
## 一、说明
### 1.1 XMemcached客户端说明
spring boot 官方并没有提供关于 memcached 的 starter所以我们这里还是采用 XMemcached 作为客户端进行整合。 XMemcached 是基于 java nio 的 memcached 高性能客户端,支持完整的 memcached 协议,支持客户端分布并且提供了一致性哈希 (consistent hash) 算法的实现。
### 1.2 项目结构说明
memcached 的整合配置位于 config 文件夹下。
<div align="center"> <img src="https://github.com/heibaiying/spring-samples-for-all/blob/master/pictures/spring-boot-memcached.png"/> </div>
### 1.3 主要依赖
```xml
<!--memcached java 客户端-->
<dependency>
<groupId>com.googlecode.xmemcached</groupId>
<artifactId>xmemcached</artifactId>
<version>2.4.5</version>
</dependency>
```
## 二、spring boot 整合 memcached
#### 2.1 单机配置
```java
@Bean
public MemcachedClient memcachedClient() {
XMemcachedClientBuilder builder = new XMemcachedClientBuilder("192.168.200.201:11211");
MemcachedClient memcachedClient = null;
try {
memcachedClient = builder.build();
} catch (IOException e) {
e.printStackTrace();
}
return memcachedClient;
}
```
#### 2.2 集群配置
```java
@Bean
public MemcachedClient memcachedClientForCluster() {
List<InetSocketAddress> addressList = new ArrayList<InetSocketAddress>();
addressList.add(new InetSocketAddress("192.168.200.201", 11211));
addressList.add(new InetSocketAddress("192.168.200.201", 11212));
// 赋予权重
int[] weights = {1, 2};
XMemcachedClientBuilder builder = new XMemcachedClientBuilder(addressList, weights);
// 设置连接池大小
builder.setConnectionPoolSize(10);
// 协议工厂
builder.setCommandFactory(new TextCommandFactory());
// 分布策略,一致性哈希 KetamaMemcachedSessionLocator 或者 ArraySessionLocator(默认)
builder.setSessionLocator(new KetamaMemcachedSessionLocator());
// 设置序列化器
builder.setTranscoder(new SerializingTranscoder());
MemcachedClient memcachedClient = null;
try {
memcachedClient = builder.build();
} catch (IOException e) {
e.printStackTrace();
}
return memcachedClient;
}
```
#### 2.3 存储基本类型测试用例
xmemcached 单机版本和集群版本注入的实例是相同的。
```java
/**
* @author : heibaiying
* @description : Memcached 操作基本对象
*/
@RunWith(SpringRunner.class)
@SpringBootTest
public class MemSamples {
@Autowired
private MemcachedClient memcachedClient;
@Test
public void operate() throws InterruptedException, MemcachedException, TimeoutException {
memcachedClient.set("hello", 0, "Hello,cluster xmemcached");
String value = memcachedClient.get("hello");
System.out.println("hello=" + value);
memcachedClient.delete("hello");
value = memcachedClient.get("hello");
System.out.println("hello=" + value);
}
}
```
#### 2.5 存储实体对象测试用例
```java
/**
* @author : heibaiying
* @description :Memcached 序列化与反序列化
*/
@RunWith(SpringRunner.class)
@SpringBootTest
public class MemObjectSamples {
@Autowired
private MemcachedClient memcachedClient;
@Test
public void operate() throws InterruptedException, MemcachedException, TimeoutException {
memcachedClient.set("programmer", 0, new Programmer("xiaoming", 12, 5000.21f, new Date()));
Programmer programmer = memcachedClient.get("programmer");
System.out.println("hello ," + programmer.getName());
memcachedClient.delete("programmer");
programmer = memcachedClient.get("programmer");
Assert.assertNull(programmer);
}
}
```
## 附memcached 基本命令
| 命令 | 格式 | 说明 |
| --------------- | -------------------------------------------------- | ------------------------------------- |
| 新增 set | set key flags exTime length -> value | 无论什么情况,都可以插入 |
| 新增 add | add key flags exTime length -> value | 只有当 key 不存在的情况下,才可以插入 |
| 替换 replace | replace key flags exTime length -> value | 只修改已存在 key 的 value 值 |
| 追加内容 append | append key flags exTime length -> value | length 表示追加的长度而不是总长度 |
| 前面追加 prepend | prepend key flags exTime length -> value | length 表示追加的长度而不是总长度 |
| 查询操作 get | get key | |
| 检查更新 cas | cas key flags exTime length version -> value | 版本正确才更新 |
| 详细获取 gets | gets key | 返回的最后一个数代表 key 的 CAS 令牌 |
| 删除 delete | delete key | 将数据打一个删除标记 |
| 自增 incr | incr key 增加偏移量 | incr 和 decr 只能操作能转换为数字的 Value |
| 自减 decr | decr key 减少偏移量 | desr 不能将数字减少至 0 以下 |
| 清库 | flush_all | |

View File

@ -1,5 +1,6 @@
# spring boot 整合 mongodb
# spring boot 整合 mongodb
## 目录<br/>
<a href="#一说明">一、说明</a><br/>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href="#11-用例结构">1.1 用例结构</a><br/>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href="#12-项目主要依赖">1.2 项目主要依赖</a><br/>
@ -12,183 +13,183 @@
## 一、说明
#### 1.1 用例结构
1. 本用例提供mongdb的简单整合用例
2. 提供用MongoTemplate的方式操作mongdb见测试用例MongoOriginalTests.java
3. 提供基于spring data jpa 的方式操作mongodb(推荐)见测试用例MongoJPATests.java
<div align="center"> <img src="https://github.com/heibaiying/spring-samples-for-all/blob/master/pictures/spring-boot-mongodb.png"/> </div>
#### 1.2 项目主要依赖
```xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-mongodb</artifactId>
</dependency>
```
## 二、整合 mongodb
#### 2.1 在application.yml 中配置mongodb数据源
```yaml
spring:
data:
mongodb:
database: spring
uri: mongodb://192.168.0.108:27017
```
#### 2.2 基于MongoTemplate实现对mongodb的操作
```java
@RunWith(SpringRunner.class)
@SpringBootTest
public class MongoOriginalTests {
@Autowired
private MongoTemplate mongoTemplate;
@Test
public void insert() {
// 单条插入
mongoTemplate.insert(new Programmer("xiaoming", 12, 5000.21f, new Date()));
List<Programmer> programmers = new ArrayList<Programmer>();
// 批量插入
programmers.add(new Programmer("xiaohong", 21, 52200.21f, new Date()));
programmers.add(new Programmer("xiaolan", 34, 500.21f, new Date()));
mongoTemplate.insert(programmers, Programmer.class);
}
// 条件查询
@Test
public void select() {
Criteria criteria = new Criteria();
criteria.andOperator(where("name").is("xiaohong"), where("age").is(21));
Query query = new Query(criteria);
Programmer one = mongoTemplate.findOne(query, Programmer.class);
System.out.println(one);
}
// 更新数据
@Test
public void MUpdate() {
UpdateResult updateResult = mongoTemplate.updateMulti(query(where("name").is("xiaoming")), update("age", 35), Programmer.class);
System.out.println("更新记录数:" + updateResult.getModifiedCount());
}
// 删除指定数据
@Test
public void delete() {
DeleteResult result = mongoTemplate.remove(query(where("name").is("xiaolan")), Programmer.class);
System.out.println("影响记录数" + result.getDeletedCount());
System.out.println("是否成功:" + result.wasAcknowledged());
}
}
```
#### 2.3 使用 data jpa 方式操作mongodb (推荐使用)
1.新建查询结构,查询方法按照支持的关键字命名
```java
public interface ProgrammerRepository extends MongoRepository<Programmer, String> {
void deleteAllByName(String name);
Programmer findAllByName(String names);
Programmer findByNameAndAge(String name, int age);
}
```
2.测试
```java
@RunWith(SpringRunner.class)
@SpringBootTest
public class MongoJPATests {
@Autowired
private ProgrammerRepository repository;
@Test
public void insert() {
// 单条插入
repository.save(new Programmer("python", 23, 21832.34f, new Date()));
// 批量插入
List<Programmer> programmers = new ArrayList<Programmer>();
programmers.add(new Programmer("java", 21, 52200.21f, new Date()));
programmers.add(new Programmer("Go", 34, 500.21f, new Date()));
repository.saveAll(programmers);
}
// 条件查询
@Test
public void select() {
Programmer java = repository.findByNameAndAge("java", 21);
Assert.assertEquals(java.getSalary(), 52200.21f, 0.01);
}
// 更新数据
@Test
public void MUpdate() {
repository.save(new Programmer("Go", 8, 500.21f, new Date()));
Programmer go = repository.findAllByName("Go");
Assert.assertEquals(go.getAge(), 8);
}
// 删除指定数据
@Test
public void delete() {
repository.deleteAllByName("python");
Optional<Programmer> python = repository.findById("python");
Assert.assertFalse(python.isPresent());
}
}
```
查询方法支持的关键字如下,更多命名规范可以参见Spring Data MongoDB官方文档[Query Methods](https://docs.spring.io/spring-data/mongodb/docs/2.1.3.RELEASE/reference/html/#mongodb.repositories.queries)
| Keyword | Sample | Logical result |
| ------------------------------------ | ------------------------------------------------------------ | ------------------------------------------------------------ |
| `After` | `findByBirthdateAfter(Date date)` | `{"birthdate" : {"$gt" : date}}` |
| `GreaterThan` | `findByAgeGreaterThan(int age)` | `{"age" : {"$gt" : age}}` |
| `GreaterThanEqual` | `findByAgeGreaterThanEqual(int age)` | `{"age" : {"$gte" : age}}` |
| `Before` | `findByBirthdateBefore(Date date)` | `{"birthdate" : {"$lt" : date}}` |
| `LessThan` | `findByAgeLessThan(int age)` | `{"age" : {"$lt" : age}}` |
| `LessThanEqual` | `findByAgeLessThanEqual(int age)` | `{"age" : {"$lte" : age}}` |
| `Between` | `findByAgeBetween(int from, int to)` | `{"age" : {"$gt" : from, "$lt" : to}}` |
| `In` | `findByAgeIn(Collection ages)` | `{"age" : {"$in" : [ages…]}}` |
| `NotIn` | `findByAgeNotIn(Collection ages)` | `{"age" : {"$nin" : [ages…]}}` |
| `IsNotNull`, `NotNull` | `findByFirstnameNotNull()` | `{"firstname" : {"$ne" : null}}` |
| `IsNull`, `Null` | `findByFirstnameNull()` | `{"firstname" : null}` |
| `Like`, `StartingWith`, `EndingWith` | `findByFirstnameLike(String name)` | `{"firstname" : name} (name as regex)` |
| `NotLike`, `IsNotLike` | `findByFirstnameNotLike(String name)` | `{"firstname" : { "$not" : name }} (name as regex)` |
| `Containing` on String | `findByFirstnameContaining(String name)` | `{"firstname" : name} (name as regex)` |
| `NotContaining` on String | `findByFirstnameNotContaining(String name)` | `{"firstname" : { "$not" : name}} (name as regex)` |
| `Containing` on Collection | `findByAddressesContaining(Address address)` | `{"addresses" : { "$in" : address}}` |
| `NotContaining` on Collection | `findByAddressesNotContaining(Address address)` | `{"addresses" : { "$not" : { "$in" : address}}}` |
| `Regex` | `findByFirstnameRegex(String firstname)` | `{"firstname" : {"$regex" : firstname }}` |
| `(No keyword)` | `findByFirstname(String name)` | `{"firstname" : name}` |
| `Not` | `findByFirstnameNot(String name)` | `{"firstname" : {"$ne" : name}}` |
| `Near` | `findByLocationNear(Point point)` | `{"location" : {"$near" : [x,y]}}` |
| `Near` | `findByLocationNear(Point point, Distance max)` | `{"location" : {"$near" : [x,y], "$maxDistance" : max}}` |
| `Near` | `findByLocationNear(Point point, Distance min, Distance max)` | `{"location" : {"$near" : [x,y], "$minDistance" : min, "$maxDistance" : max}}` |
| `Within` | `findByLocationWithin(Circle circle)` | `{"location" : {"$geoWithin" : {"$center" : [ [x, y], distance]}}}` |
| `Within` | `findByLocationWithin(Box box)` | `{"location" : {"$geoWithin" : {"$box" : [ [x1, y1], x2, y2]}}}` |
| `IsTrue`, `True` | `findByActiveIsTrue()` | `{"active" : true}` |
| `IsFalse`, `False` | `findByActiveIsFalse()` | `{"active" : false}` |
| `Exists` | `findByLocationExists(boolean exists)` | `{"location" : {"$exists" : exists }}` |
## 一、说明
#### 1.1 用例结构
1. 本用例提供 mongdb 的简单整合用例;
2. 提供用 MongoTemplate 的方式操作 mongdb见测试用例 MongoOriginalTests.java
3. 提供基于 spring data jpa 的方式操作 mongodb(推荐),见测试用例 MongoJPATests.java
<div align="center"> <img src="https://github.com/heibaiying/spring-samples-for-all/blob/master/pictures/spring-boot-mongodb.png"/> </div>
#### 1.2 项目主要依赖
```xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-mongodb</artifactId>
</dependency>
```
## 二、整合 mongodb
#### 2.1 在application.yml 中配置mongodb数据源
```yaml
spring:
data:
mongodb:
database: spring
uri: mongodb://192.168.0.108:27017
```
#### 2.2 基于MongoTemplate实现对mongodb的操作
```java
@RunWith(SpringRunner.class)
@SpringBootTest
public class MongoOriginalTests {
@Autowired
private MongoTemplate mongoTemplate;
@Test
public void insert() {
// 单条插入
mongoTemplate.insert(new Programmer("xiaoming", 12, 5000.21f, new Date()));
List<Programmer> programmers = new ArrayList<Programmer>();
// 批量插入
programmers.add(new Programmer("xiaohong", 21, 52200.21f, new Date()));
programmers.add(new Programmer("xiaolan", 34, 500.21f, new Date()));
mongoTemplate.insert(programmers, Programmer.class);
}
// 条件查询
@Test
public void select() {
Criteria criteria = new Criteria();
criteria.andOperator(where("name").is("xiaohong"), where("age").is(21));
Query query = new Query(criteria);
Programmer one = mongoTemplate.findOne(query, Programmer.class);
System.out.println(one);
}
// 更新数据
@Test
public void MUpdate() {
UpdateResult updateResult = mongoTemplate.updateMulti(query(where("name").is("xiaoming")), update("age", 35), Programmer.class);
System.out.println("更新记录数:" + updateResult.getModifiedCount());
}
// 删除指定数据
@Test
public void delete() {
DeleteResult result = mongoTemplate.remove(query(where("name").is("xiaolan")), Programmer.class);
System.out.println("影响记录数:" + result.getDeletedCount());
System.out.println("是否成功" + result.wasAcknowledged());
}
}
```
#### 2.3 使用 data jpa 方式操作mongodb (推荐使用)
1.新建查询结构,查询方法按照支持的关键字命名
```java
public interface ProgrammerRepository extends MongoRepository<Programmer, String> {
void deleteAllByName(String name);
Programmer findAllByName(String names);
Programmer findByNameAndAge(String name, int age);
}
```
2.测试
```java
@RunWith(SpringRunner.class)
@SpringBootTest
public class MongoJPATests {
@Autowired
private ProgrammerRepository repository;
@Test
public void insert() {
// 单条插入
repository.save(new Programmer("python", 23, 21832.34f, new Date()));
// 批量插入
List<Programmer> programmers = new ArrayList<Programmer>();
programmers.add(new Programmer("java", 21, 52200.21f, new Date()));
programmers.add(new Programmer("Go", 34, 500.21f, new Date()));
repository.saveAll(programmers);
}
// 条件查询
@Test
public void select() {
Programmer java = repository.findByNameAndAge("java", 21);
Assert.assertEquals(java.getSalary(), 52200.21f, 0.01);
}
// 更新数据
@Test
public void MUpdate() {
repository.save(new Programmer("Go", 8, 500.21f, new Date()));
Programmer go = repository.findAllByName("Go");
Assert.assertEquals(go.getAge(), 8);
}
// 删除指定数据
@Test
public void delete() {
repository.deleteAllByName("python");
Optional<Programmer> python = repository.findById("python");
Assert.assertFalse(python.isPresent());
}
}
```
查询方法支持的关键字如下,更多命名规范可以参见 Spring Data MongoDB 官方文档[Query Methods](https://docs.spring.io/spring-data/mongodb/docs/2.1.3.RELEASE/reference/html/#mongodb.repositories.queries)
| Keyword | Sample | Logical result |
| ------------------------------------ | ------------------------------------------------------------ | ------------------------------------------------------------ |
| `After` | `findByBirthdateAfter(Date date)` | `{"birthdate" : {"$gt" : date}}` |
| `GreaterThan` | `findByAgeGreaterThan(int age)` | `{"age" : {"$gt" : age}}` |
| `GreaterThanEqual` | `findByAgeGreaterThanEqual(int age)` | `{"age" : {"$gte" : age}}` |
| `Before` | `findByBirthdateBefore(Date date)` | `{"birthdate" : {"$lt" : date}}` |
| `LessThan` | `findByAgeLessThan(int age)` | `{"age" : {"$lt" : age}}` |
| `LessThanEqual` | `findByAgeLessThanEqual(int age)` | `{"age" : {"$lte" : age}}` |
| `Between` | `findByAgeBetween(int from, int to)` | `{"age" : {"$gt" : from, "$lt" : to}}` |
| `In` | `findByAgeIn(Collection ages)` | `{"age" : {"$in" : [ages…]}}` |
| `NotIn` | `findByAgeNotIn(Collection ages)` | `{"age" : {"$nin" : [ages…]}}` |
| `IsNotNull`, `NotNull` | `findByFirstnameNotNull()` | `{"firstname" : {"$ne" : null}}` |
| `IsNull`, `Null` | `findByFirstnameNull()` | `{"firstname" : null}` |
| `Like`, `StartingWith`, `EndingWith` | `findByFirstnameLike(String name)` | `{"firstname" : name} (name as regex)` |
| `NotLike`, `IsNotLike` | `findByFirstnameNotLike(String name)` | `{"firstname" : { "$not" : name }} (name as regex)` |
| `Containing` on String | `findByFirstnameContaining(String name)` | `{"firstname" : name} (name as regex)` |
| `NotContaining` on String | `findByFirstnameNotContaining(String name)` | `{"firstname" : { "$not" : name}} (name as regex)` |
| `Containing` on Collection | `findByAddressesContaining(Address address)` | `{"addresses" : { "$in" : address}}` |
| `NotContaining` on Collection | `findByAddressesNotContaining(Address address)` | `{"addresses" : { "$not" : { "$in" : address}}}` |
| `Regex` | `findByFirstnameRegex(String firstname)` | `{"firstname" : {"$regex" : firstname }}` |
| `(No keyword)` | `findByFirstname(String name)` | `{"firstname" : name}` |
| `Not` | `findByFirstnameNot(String name)` | `{"firstname" : {"$ne" : name}}` |
| `Near` | `findByLocationNear(Point point)` | `{"location" : {"$near" : [x,y]}}` |
| `Near` | `findByLocationNear(Point point, Distance max)` | `{"location" : {"$near" : [x,y], "$maxDistance" : max}}` |
| `Near` | `findByLocationNear(Point point, Distance min, Distance max)` | `{"location" : {"$near" : [x,y], "$minDistance" : min, "$maxDistance" : max}}` |
| `Within` | `findByLocationWithin(Circle circle)` | `{"location" : {"$geoWithin" : {"$center" : [ [x, y], distance]}}}` |
| `Within` | `findByLocationWithin(Box box)` | `{"location" : {"$geoWithin" : {"$box" : [ [x1, y1], x2, y2]}}}` |
| `IsTrue`, `True` | `findByActiveIsTrue()` | `{"active" : true}` |
| `IsFalse`, `False` | `findByActiveIsFalse()` | `{"active" : false}` |
| `Exists` | `findByLocationExists(boolean exists)` | `{"location" : {"$exists" : exists }}` |

View File

@ -1,5 +1,6 @@
# spring boot 整合 mybatis
# spring boot 整合 mybatis
## 目录<br/>
<a href="#一说明">一、说明</a><br/>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href="#11-项目结构">1.1 项目结构</a><br/>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href="#12-项目主要依赖">1.2 项目主要依赖</a><br/>
@ -12,250 +13,250 @@
## 一、说明
#### 1.1 项目结构
1. 项目查询用的表对应的建表语句放置在resources的sql文件夹下
2. 关于mybatis sql的写法提供两种方式:
xml 写法对应的类为ProgrammerMapper.java 和 programmerMapper.xml用MybatisXmlTest进行测试
注解写法对应的类为Programmer.java 用MybatisAnnotationTest进行测试。
<div align="center"> <img src="https://github.com/heibaiying/spring-samples-for-all/blob/master/pictures/spring-boot-mybatis.png"/> </div>
#### 1.2 项目主要依赖
需要说明的是按照spring 官方对应自定义的starter 命名规范的推荐:
- 官方的starter命名spring-boot-starter-XXXX
- 其他第三方starter命名XXXX-spring-boot-starte
所以mybatis的starter命名为mybatis-spring-boot-starter如果有自定义starter需求也需要按照此命名规则进行命名。
```xml
<!--spring 1.5 x 以上版本对应 mybatis 1.3.x (1.3.1)
关于更多spring-boot 与 mybatis 的版本对应可以参见 <a href="http://www.mybatis.org/spring-boot-starter/mybatis-spring-boot-autoconfigure/">-->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>1.3.2</version>
</dependency>
<!--引入mysql驱动-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.13</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
```
spring boot 与 mybatis 版本的对应关系:
| MyBatis-Spring-Boot-Starter | [MyBatis-Spring](http://www.mybatis.org/spring/index.html#Requirements) | Spring Boot |
| --------------------------- | ------------------------------------------------------------ | ------------- |
| **1.3.x (1.3.1)** | 1.3 or higher | 1.5 or higher |
| **1.2.x (1.2.1)** | 1.3 or higher | 1.4 or higher |
| **1.1.x (1.1.1)** | 1.3 or higher | 1.3 or higher |
| **1.0.x (1.0.2)** | 1.2 or higher | 1.3 or higher |
## 二、整合 mybatis
#### 2.1 在application.yml 中配置数据源
spring boot 2.x 版本默认采用Hikari作为数据库连接池Hikari是目前java平台性能最好的连接池性能好于druid。
```yaml
spring:
datasource:
url: jdbc:mysql://127.0.0.1:3306/mysql?characterEncoding=UTF-8&serverTimezone=UTC&useSSL=false
username: root
password: root
driver-class-name: com.mysql.cj.jdbc.Driver
# 如果不想配置对数据库连接池做特殊配置的话,以下关于连接池的配置就不是必须的
# spring-boot 2 默认采用高性能的 Hikari 作为连接池 更多配置可以参考 https://github.com/brettwooldridge/HikariCP#configuration-knobs-baby
type: com.zaxxer.hikari.HikariDataSource
hikari:
# 池中维护的最小空闲连接数
minimum-idle: 10
# 池中最大连接数,包括闲置和使用中的连接
maximum-pool-size: 20
# 此属性控制从池返回的连接的默认自动提交行为。默认为true
auto-commit: true
# 允许最长空闲时间
idle-timeout: 30000
# 此属性表示连接池的用户定义名称主要显示在日志记录和JMX管理控制台中以标识池和池配置。 默认值:自动生成
pool-name: custom-hikari
#此属性控制池中连接的最长生命周期值0表示无限生命周期默认1800000即30分钟
max-lifetime: 1800000
# 数据库连接超时时间,默认30秒30000
connection-timeout: 30000
# 连接测试sql 这个地方需要根据数据库方言差异而配置 例如 oracle 就应该写成 select 1 from dual
connection-test-query: SELECT 1
# mybatis 相关配置
mybatis:
# 指定 sql xml 文件的位置
mapper-locations: classpath*:mappers/*.xml
configuration:
# 当没有为参数提供特定的 JDBC 类型时,为空值指定 JDBC 类型。
# oracle数据库建议配置为JdbcType.NULL, 默认是Other
jdbc-type-for-null: 'null'
# 是否打印sql语句 调试的时候可以开启
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
```
#### 2.2 xml方式的sql语句
新建 ProgrammerMapper.java 和 programmerMapper.xml及其测试类
```java
@Mapper
public interface ProgrammerMapper {
void save(Programmer programmer);
Programmer selectById(int id);
int modify(Programmer programmer);
void delete(int id);
}
```
```xml
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.heibaiying.springboot.dao.ProgrammerMapper">
<insert id="save">
insert into programmer (name, age, salary, birthday) VALUES (#{name}, #{age}, #{salary}, #{birthday})
</insert>
<select id="selectById" resultType="com.heibaiying.springboot.bean.Programmer">
select * from programmer where name = #{id}
</select>
<update id="modify">
update programmer set name=#{name},age=#{age},salary=#{salary},birthday=#{birthday} where id=#{id}
</update>
<delete id="delete">
delete from programmer where id = #{id}
</delete>
</mapper>
```
测试类
```java
/***
* @description: xml Sql测试类
*/
@RunWith(SpringRunner.class)
@SpringBootTest
public class MybatisXmlTest {
@Autowired
private ProgrammerMapper mapper;
@Test
public void save() {
mapper.save(new Programmer("xiaominng", 12, 3467.34f, new Date()));
mapper.save(new Programmer("xiaominng", 12, 3467.34f, new Date()));
}
@Test
public void modify() {
mapper.modify(new Programmer(1, "xiaohong", 112, 347.34f, new Date()));
}
@Test
public void selectByCondition() {
Programmer programmers = mapper.selectById(1);
System.out.println(programmers);
}
@Test
public void delete() {
mapper.delete(2);
Programmer programmers = mapper.selectById(2);
Assert.assertNull(programmers);
}
}
```
#### 2.3 注解方式的sql语句
```java
@Mapper
public interface ProgrammerDao {
@Insert("insert into programmer (name, age, salary, birthday) VALUES (#{name}, #{age}, #{salary}, #{birthday})")
void save(Programmer programmer);
@Select("select * from programmer where name = #{id}")
Programmer selectById(int id);
@Update("update programmer set name=#{name},age=#{age},salary=#{salary},birthday=#{birthday} where id=#{id}")
int modify(Programmer programmer);
@Delete(" delete from programmer where id = #{id}")
void delete(int id);
}
```
测试类
```java
/***
* @description: 注解Sql测试类
*/
@RunWith(SpringRunner.class)
@SpringBootTest
public class MybatisAnnotationTest {
@Autowired
private ProgrammerDao programmerDao;
@Test
public void save() {
programmerDao.save(new Programmer("xiaominng", 12, 3467.34f, new Date()));
programmerDao.save(new Programmer("xiaominng", 12, 3467.34f, new Date()));
}
@Test
public void modify() {
programmerDao.modify(new Programmer(1, "xiaolan", 21, 347.34f, new Date()));
}
@Test
public void selectByCondition() {
Programmer programmers = programmerDao.selectById(1);
System.out.println(programmers);
}
@Test
public void delete() {
programmerDao.delete(3);
Programmer programmers = programmerDao.selectById(3);
Assert.assertNull(programmers);
}
}
```
## 一、说明
#### 1.1 项目结构
1. 项目查询用的表对应的建表语句放置在 resources 的 sql 文件夹下;
2. 关于 mybatis sql 的写法提供两种方式:
xml 写法:对应的类为 ProgrammerMapper.java 和 programmerMapper.xml用 MybatisXmlTest 进行测试;
注解写法:对应的类为 Programmer.java ,用 MybatisAnnotationTest 进行测试。
<div align="center"> <img src="https://github.com/heibaiying/spring-samples-for-all/blob/master/pictures/spring-boot-mybatis.png"/> </div>
#### 1.2 项目主要依赖
需要说明的是按照 spring 官方对应自定义的 starter 命名规范的推荐:
- 官方的 starter 命名spring-boot-starter-XXXX
- 其他第三方 starter 命名:XXXX-spring-boot-starte
所以 mybatis 的 starter 命名为 mybatis-spring-boot-starter如果有自定义 starter 需求,也需要按照此命名规则进行命名。
```xml
<!--spring 1.5 x 以上版本对应 mybatis 1.3.x (1.3.1)
关于更多 spring-boot 与 mybatis 的版本对应可以参见 <a href="http://www.mybatis.org/spring-boot-starter/mybatis-spring-boot-autoconfigure/">-->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>1.3.2</version>
</dependency>
<!--引入mysql驱动-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.13</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
```
spring boot 与 mybatis 版本的对应关系:
| MyBatis-Spring-Boot-Starter | [MyBatis-Spring](http://www.mybatis.org/spring/index.html#Requirements) | Spring Boot |
| --------------------------- | ------------------------------------------------------------ | ------------- |
| **1.3.x (1.3.1)** | 1.3 or higher | 1.5 or higher |
| **1.2.x (1.2.1)** | 1.3 or higher | 1.4 or higher |
| **1.1.x (1.1.1)** | 1.3 or higher | 1.3 or higher |
| **1.0.x (1.0.2)** | 1.2 or higher | 1.3 or higher |
## 二、整合 mybatis
#### 2.1 在application.yml 中配置数据源
spring boot 2.x 版本默认采用 Hikari 作为数据库连接池Hikari 是目前 java 平台性能最好的连接池,性能好于 druid。
```yaml
spring:
datasource:
url: jdbc:mysql://127.0.0.1:3306/mysql?characterEncoding=UTF-8&serverTimezone=UTC&useSSL=false
username: root
password: root
driver-class-name: com.mysql.cj.jdbc.Driver
# 如果不想配置对数据库连接池做特殊配置的话,以下关于连接池的配置就不是必须的
# spring-boot 2 默认采用高性能的 Hikari 作为连接池 更多配置可以参考 https://github.com/brettwooldridge/HikariCP#configuration-knobs-baby
type: com.zaxxer.hikari.HikariDataSource
hikari:
# 池中维护的最小空闲连接数
minimum-idle: 10
# 池中最大连接数,包括闲置和使用中的连接
maximum-pool-size: 20
# 此属性控制从池返回的连接的默认自动提交行为。默认为 true
auto-commit: true
# 允许最长空闲时间
idle-timeout: 30000
# 此属性表示连接池的用户定义名称,主要显示在日志记录和 JMX 管理控制台中,以标识池和池配置。 默认值:自动生成
pool-name: custom-hikari
#此属性控制池中连接的最长生命周期,值 0 表示无限生命周期,默认 1800000 即 30 分钟
max-lifetime: 1800000
# 数据库连接超时时间,默认 30 秒,即 30000
connection-timeout: 30000
# 连接测试 sql 这个地方需要根据数据库方言差异而配置 例如 oracle 就应该写成 select 1 from dual
connection-test-query: SELECT 1
# mybatis 相关配置
mybatis:
# 指定 sql xml 文件的位置
mapper-locations: classpath*:mappers/*.xml
configuration:
# 当没有为参数提供特定的 JDBC 类型时,为空值指定 JDBC 类型。
# oracle 数据库建议配置为 JdbcType.NULL, 默认是 Other
jdbc-type-for-null: 'null'
# 是否打印 sql 语句 调试的时候可以开启
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
```
#### 2.2 xml方式的sql语句
新建 ProgrammerMapper.java 和 programmerMapper.xml及其测试类
```java
@Mapper
public interface ProgrammerMapper {
void save(Programmer programmer);
Programmer selectById(int id);
int modify(Programmer programmer);
void delete(int id);
}
```
```xml
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.heibaiying.springboot.dao.ProgrammerMapper">
<insert id="save">
insert into programmer (name, age, salary, birthday) VALUES (#{name}, #{age}, #{salary}, #{birthday})
</insert>
<select id="selectById" resultType="com.heibaiying.springboot.bean.Programmer">
select * from programmer where name = #{id}
</select>
<update id="modify">
update programmer set name=#{name},age=#{age},salary=#{salary},birthday=#{birthday} where id=#{id}
</update>
<delete id="delete">
delete from programmer where id = #{id}
</delete>
</mapper>
```
测试类
```java
/***
* @description: xml Sql 测试类
*/
@RunWith(SpringRunner.class)
@SpringBootTest
public class MybatisXmlTest {
@Autowired
private ProgrammerMapper mapper;
@Test
public void save() {
mapper.save(new Programmer("xiaominng", 12, 3467.34f, new Date()));
mapper.save(new Programmer("xiaominng", 12, 3467.34f, new Date()));
}
@Test
public void modify() {
mapper.modify(new Programmer(1, "xiaohong", 112, 347.34f, new Date()));
}
@Test
public void selectByCondition() {
Programmer programmers = mapper.selectById(1);
System.out.println(programmers);
}
@Test
public void delete() {
mapper.delete(2);
Programmer programmers = mapper.selectById(2);
Assert.assertNull(programmers);
}
}
```
#### 2.3 注解方式的sql语句
```java
@Mapper
public interface ProgrammerDao {
@Insert("insert into programmer (name, age, salary, birthday) VALUES (#{name}, #{age}, #{salary}, #{birthday})")
void save(Programmer programmer);
@Select("select * from programmer where name = #{id}")
Programmer selectById(int id);
@Update("update programmer set name=#{name},age=#{age},salary=#{salary},birthday=#{birthday} where id=#{id}")
int modify(Programmer programmer);
@Delete(" delete from programmer where id = #{id}")
void delete(int id);
}
```
测试类
```java
/***
* @description: 注解 Sql 测试类
*/
@RunWith(SpringRunner.class)
@SpringBootTest
public class MybatisAnnotationTest {
@Autowired
private ProgrammerDao programmerDao;
@Test
public void save() {
programmerDao.save(new Programmer("xiaominng", 12, 3467.34f, new Date()));
programmerDao.save(new Programmer("xiaominng", 12, 3467.34f, new Date()));
}
@Test
public void modify() {
programmerDao.modify(new Programmer(1, "xiaolan", 21, 347.34f, new Date()));
}
@Test
public void selectByCondition() {
Programmer programmers = programmerDao.selectById(1);
System.out.println(programmers);
}
@Test
public void delete() {
programmerDao.delete(3);
Programmer programmers = programmerDao.selectById(3);
Assert.assertNull(programmers);
}
}
```

View File

@ -1,5 +1,6 @@
# spring boot 整合 rabbitmq
# spring boot 整合 rabbitmq
## 目录<br/>
<a href="#一-项目结构说明">一、 项目结构说明</a><br/>
<a href="#二关键依赖">二、关键依赖</a><br/>
<a href="#三公共模块rabbitmq-common">三、公共模块rabbitmq-common</a><br/>
@ -16,391 +17,391 @@
## 一、 项目结构说明
1.1 之前关于spring 整合 rabbitmq 我们采用的是单项目的方式为了使得用例更具有实际意义这里采用maven多模块的构建方式在spring-boot-rabbitmq下构建三个子模块
1. rabbitmq-common 是公共模块用于存放公共的接口、配置和bean,被rabbitmq-producer和rabbitmq-consumer在pom.xml中引用
2. rabbitmq-producer 是消息的生产者模块
3. rabbitmq-consumer是消息的消费者模块。
1.2 关于rabbitmq安装、交换机、队列、死信队列等基本概念可以参考我的手记[《RabbitMQ实战指南》读书笔记](https://github.com/heibaiying/LearningNotes/blob/master/notes/%E4%B8%AD%E9%97%B4%E4%BB%B6/RabbitMQ/%E3%80%8ARabbitMQ%E5%AE%9E%E6%88%98%E6%8C%87%E5%8D%97%E3%80%8B%E8%AF%BB%E4%B9%A6%E7%AC%94%E8%AE%B0.md),里面有详细的配图说明。
<div align="center"> <img src="https://github.com/heibaiying/spring-samples-for-all/blob/master/pictures/spring-boot-rabbitmq.png"/> </div>
## 二、关键依赖
在父工程的项目中统一导入依赖rabbitmq的starter(spring-boot-starter-amqp)父工程的pom.xml如下
```xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<packaging>pom</packaging>
<modules>
<module>rabbitmq-consumer</module>
<module>rabbitmq-producer</module>
<module>rabbitmq-common</module>
</modules>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.1.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.heibaiying</groupId>
<artifactId>spring-boot-rabbitmq</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>spring-boot-rabbitmq</name>
<description>RabbitMQ project for Spring Boot</description>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
</project>
```
## 三、公共模块rabbitmq-common
- bean 下为公共的实体类。
- constant 下为公共配置,用静态常量引用。(这里我使用静态常量是为了方便引用,实际中也可以按照情况,抽取为公共配置文件)
<div align="center"> <img src="https://github.com/heibaiying/spring-samples-for-all/blob/master/pictures/rabbitmq-common.png"/> </div>
```java
package com.heibaiying.constant;
/**
* @author : heibaiying
* @description : rabbit 公用配置信息
*/
public class RabbitInfo {
// queue 配置
public static final String QUEUE_NAME = "spring.boot.simple.queue";
public static final String QUEUE_DURABLE = "true";
// exchange 配置
public static final String EXCHANGE_NAME = "spring.boot.simple.exchange";
public static final String EXCHANGE_TYPE = "topic";
// routing key
public static final String ROUTING_KEY = "springboot.simple.*";
}
```
## 四、服务消费者rabbitmq-consumer
<div align="center"> <img src="https://github.com/heibaiying/spring-samples-for-all/blob/master/pictures/rabbitmq-consumer.png"/> </div>
#### 4.1 消息消费者配置
```yaml
spring:
rabbitmq:
addresses: 127.0.0.1:5672
# RabbitMQ 默认的用户名和密码都是guest 而虚拟主机名称是 "/"
# 如果配置其他虚拟主机地址,需要预先用管控台或者图形界面创建 图形界面地址 http://主机地址:15672
username: guest
password: guest
virtual-host: /
listener:
simple:
# 为了保证信息能够被正确消费,建议签收模式设置为手工签收,并在代码中实现手工签收
acknowledge-mode: manual
# 侦听器调用者线程的最小数量
concurrency: 10
# 侦听器调用者线程的最大数量
max-concurrency: 50
```
#### 4.2 使用注解@RabbitListener和@RabbitHandler创建消息监听者
1. 使用注解创建的交换机、队列、和绑定关系会在项目初始化的时候自动创建,但是不会重复创建;
2. 这里我们创建两个消息监听器,分别演示消息是基本类型和消息是对象时的配置区别。
```java
/**
* @author : heibaiying
* @description : 消息是对象的消费者
*/
@Component
@Slf4j
public class RabbitmqBeanConsumer {
@RabbitListener(bindings = @QueueBinding(
value = @Queue(value = RabbitBeanInfo.QUEUE_NAME, durable = RabbitBeanInfo.QUEUE_DURABLE),
exchange = @Exchange(value = RabbitBeanInfo.EXCHANGE_NAME, type = RabbitBeanInfo.EXCHANGE_TYPE),
key = RabbitBeanInfo.ROUTING_KEY)
)
@RabbitHandler
public void onMessage(@Payload Programmer programmer, @Headers Map<String, Object> headers, Channel channel) throws Exception {
log.info("programmer:{} ", programmer);
Long deliveryTag = (Long) headers.get(AmqpHeaders.DELIVERY_TAG);
channel.basicAck(deliveryTag, false);
}
}
```
```java
@Component
@Slf4j
public class RabbitmqConsumer {
@RabbitListener(bindings = @QueueBinding(
value = @Queue(value = RabbitInfo.QUEUE_NAME, durable = RabbitInfo.QUEUE_DURABLE),
exchange = @Exchange(value = RabbitInfo.EXCHANGE_NAME, type = RabbitInfo.EXCHANGE_TYPE),
key = RabbitInfo.ROUTING_KEY)
)
@RabbitHandler
public void onMessage(Message message, Channel channel) throws Exception {
MessageHeaders headers = message.getHeaders();
// 获取消息头信息和消息体
log.info("msgInfo:{} ; payload:{} ", headers.get("msgInfo"), message.getPayload());
// DELIVERY_TAG 代表 RabbitMQ 向该Channel投递的这条消息的唯一标识ID是一个单调递增的正整数
Long deliveryTag = (Long) headers.get(AmqpHeaders.DELIVERY_TAG);
// 第二个参数代表是否一次签收多条,当该参数为 true 时,则可以一次性确认 DELIVERY_TAG 小于等于传入值的所有消息
channel.basicAck(deliveryTag, false);
}
}
```
## 五、 消息生产者rabbitmq-producer
<div align="center"> <img src="https://github.com/heibaiying/spring-samples-for-all/blob/master/pictures/rabbitmq-producer.png"/> </div>
#### 5.1 消息生产者配置
```yaml
spring:
rabbitmq:
addresses: 127.0.0.1:5672
# RabbitMQ 默认的用户名和密码都是guest 而虚拟主机名称是 "/"
# 如果配置其他虚拟主机地址,需要预先用管控台或者图形界面创建 图形界面地址 http://主机地址:15672
username: guest
password: guest
virtual-host: /
# 是否启用发布者确认 具体确认回调实现见代码
publisher-confirms: true
# 是否启用发布者返回 具体返回回调实现见代码
publisher-returns: true
# 是否启用强制消息 保证消息的有效监听
template.mandatory: true
server:
port: 8090
```
#### 5.2 创建消息生产者
```java
/**
* @author : heibaiying
* @description : 消息生产者
*/
@Component
@Slf4j
public class RabbitmqProducer {
@Autowired
private RabbitTemplate rabbitTemplate;
public void sendSimpleMessage(Map<String, Object> headers, Object message,
String messageId, String exchangeName, String key) {
// 自定义消息头
MessageHeaders messageHeaders = new MessageHeaders(headers);
// 创建消息
Message<Object> msg = MessageBuilder.createMessage(message, messageHeaders);
/* 确认的回调 确认消息是否到达 Broker 服务器 其实就是是否到达交换器
如果发送时候指定的交换器不存在 ack就是false 代表消息不可达 */
rabbitTemplate.setConfirmCallback((correlationData, ack, cause) -> {
log.info("correlationData{} , ack:{}", correlationData.getId(), ack);
if (!ack) {
System.out.println("进行对应的消息补偿机制");
}
});
/* 消息失败的回调
* 例如消息已经到达交换器上,但路由键匹配任何绑定到该交换器的队列,会触发这个回调,此时 replyText: NO_ROUTE
*/
rabbitTemplate.setReturnCallback((message1, replyCode, replyText, exchange, routingKey) -> {
log.info("message:{}; replyCode: {}; replyText: {} ; exchange:{} ; routingKey:{}",
message1, replyCode, replyText, exchange, routingKey);
});
// 在实际中ID 应该是全局唯一 能够唯一标识消息 消息不可达的时候触发ConfirmCallback回调方法时可以获取该值进行对应的错误处理
CorrelationData correlationData = new CorrelationData(messageId);
rabbitTemplate.convertAndSend(exchangeName, key, msg, correlationData);
}
}
```
#### 5.3 以单元测试的方式发送消息
```java
@RunWith(SpringRunner.class)
@SpringBootTest
public class RabbitmqProducerTests {
@Autowired
private RabbitmqProducer producer;
/***
* 发送消息体为简单数据类型的消息
*/
@Test
public void send() {
Map<String, Object> heads = new HashMap<>();
heads.put("msgInfo", "自定义消息头信息");
// 模拟生成消息ID,在实际中应该是全局唯一的 消息不可达时候可以在setConfirmCallback回调中取得可以进行对应的重发或错误处理
String id = String.valueOf(Math.round(Math.random() * 10000));
producer.sendSimpleMessage(heads, "hello Spring", id, RabbitInfo.EXCHANGE_NAME, "springboot.simple.abc");
}
/***
* 发送消息体为bean的消息
*/
@Test
public void sendBean() {
String id = String.valueOf(Math.round(Math.random() * 10000));
Programmer programmer = new Programmer("xiaoMing", 12, 12123.45f, new Date());
producer.sendSimpleMessage(null, programmer, id, RabbitBeanInfo.EXCHANGE_NAME, RabbitBeanInfo.ROUTING_KEY);
}
}
```
## 六、项目构建的说明
因为在项目中consumer和producer模块均依赖公共模块,所以在构建consumer和producer项目前需要将common 模块安装到本地仓库,**依次**对**父工程**和**common模块**执行:
```shell
mvn install -Dmaven.test.skip = true
```
consumer中 pom.xml如下
```xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.heibaiying</groupId>
<artifactId>spring-boot-rabbitmq</artifactId>
<version>0.0.1-SNAPSHOT</version>
</parent>
<artifactId>rabbitmq-consumer</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>rabbitmq-consumer</name>
<description>RabbitMQ consumer project for Spring Boot</description>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>com.heibaiying</groupId>
<artifactId>rabbitmq-common</artifactId>
<version>0.0.1-SNAPSHOT</version>
<scope>compile</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
```
producer中 pom.xml如下
```xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.heibaiying</groupId>
<artifactId>spring-boot-rabbitmq</artifactId>
<version>0.0.1-SNAPSHOT</version>
</parent>
<artifactId>rabbitmq-producer</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>rabbitmq-producer</name>
<description>RabbitMQ producer project for Spring Boot</description>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>com.heibaiying</groupId>
<artifactId>rabbitmq-common</artifactId>
<version>0.0.1-SNAPSHOT</version>
<scope>compile</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
## 一、 项目结构说明
1.1 之前关于 spring 整合 rabbitmq 我们采用的是单项目的方式,为了使得用例更具有实际意义,这里采用 maven 多模块的构建方式,在 spring-boot-rabbitmq 下构建三个子模块:
1. rabbitmq-common 是公共模块,用于存放公共的接口、配置和 bean,被 rabbitmq-producer 和 rabbitmq-consumer 在 pom.xml 中引用;
2. rabbitmq-producer 是消息的生产者模块
3. rabbitmq-consumer 是消息的消费者模块
1.2 关于 rabbitmq 安装、交换机、队列、死信队列等基本概念可以参考我的手记[《RabbitMQ 实战指南》读书笔记](https://github.com/heibaiying/LearningNotes/blob/master/notes/%E4%B8%AD%E9%97%B4%E4%BB%B6/RabbitMQ/%E3%80%8ARabbitMQ%E5%AE%9E%E6%88%98%E6%8C%87%E5%8D%97%E3%80%8B%E8%AF%BB%E4%B9%A6%E7%AC%94%E8%AE%B0.md),里面有详细的配图说明。
<div align="center"> <img src="https://github.com/heibaiying/spring-samples-for-all/blob/master/pictures/spring-boot-rabbitmq.png"/> </div>
## 二、关键依赖
在父工程的项目中统一导入依赖 rabbitmq 的 starter(spring-boot-starter-amqp),父工程的 pom.xml 如下
```xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<packaging>pom</packaging>
<modules>
<module>rabbitmq-consumer</module>
<module>rabbitmq-producer</module>
<module>rabbitmq-common</module>
</modules>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.1.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.heibaiying</groupId>
<artifactId>spring-boot-rabbitmq</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>spring-boot-rabbitmq</name>
<description>RabbitMQ project for Spring Boot</description>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
</project>
```
## 三、公共模块rabbitmq-common
- bean 下为公共的实体类。
- constant 下为公共配置,用静态常量引用。(这里我使用静态常量是为了方便引用,实际中也可以按照情况,抽取为公共配置文件)
<div align="center"> <img src="https://github.com/heibaiying/spring-samples-for-all/blob/master/pictures/rabbitmq-common.png"/> </div>
```java
package com.heibaiying.constant;
/**
* @author : heibaiying
* @description : rabbit 公用配置信息
*/
public class RabbitInfo {
// queue 配置
public static final String QUEUE_NAME = "spring.boot.simple.queue";
public static final String QUEUE_DURABLE = "true";
// exchange 配置
public static final String EXCHANGE_NAME = "spring.boot.simple.exchange";
public static final String EXCHANGE_TYPE = "topic";
// routing key
public static final String ROUTING_KEY = "springboot.simple.*";
}
```
## 四、服务消费者rabbitmq-consumer
<div align="center"> <img src="https://github.com/heibaiying/spring-samples-for-all/blob/master/pictures/rabbitmq-consumer.png"/> </div>
#### 4.1 消息消费者配置
```yaml
spring:
rabbitmq:
addresses: 127.0.0.1:5672
# RabbitMQ 默认的用户名和密码都是 guest 而虚拟主机名称是 "/"
# 如果配置其他虚拟主机地址,需要预先用管控台或者图形界面创建 图形界面地址 http://主机地址:15672
username: guest
password: guest
virtual-host: /
listener:
simple:
# 为了保证信息能够被正确消费,建议签收模式设置为手工签收,并在代码中实现手工签收
acknowledge-mode: manual
# 侦听器调用者线程的最小数量
concurrency: 10
# 侦听器调用者线程的最大数量
max-concurrency: 50
```
#### 4.2 使用注解@RabbitListener和@RabbitHandler创建消息监听者
1. 使用注解创建的交换机、队列、和绑定关系会在项目初始化的时候自动创建,但是不会重复创建;
2. 这里我们创建两个消息监听器,分别演示消息是基本类型和消息是对象时的配置区别。
```java
/**
* @author : heibaiying
* @description : 消息是对象的消费者
*/
@Component
@Slf4j
public class RabbitmqBeanConsumer {
@RabbitListener(bindings = @QueueBinding(
value = @Queue(value = RabbitBeanInfo.QUEUE_NAME, durable = RabbitBeanInfo.QUEUE_DURABLE),
exchange = @Exchange(value = RabbitBeanInfo.EXCHANGE_NAME, type = RabbitBeanInfo.EXCHANGE_TYPE),
key = RabbitBeanInfo.ROUTING_KEY)
)
@RabbitHandler
public void onMessage(@Payload Programmer programmer, @Headers Map<String, Object> headers, Channel channel) throws Exception {
log.info("programmer:{} ", programmer);
Long deliveryTag = (Long) headers.get(AmqpHeaders.DELIVERY_TAG);
channel.basicAck(deliveryTag, false);
}
}
```
```java
@Component
@Slf4j
public class RabbitmqConsumer {
@RabbitListener(bindings = @QueueBinding(
value = @Queue(value = RabbitInfo.QUEUE_NAME, durable = RabbitInfo.QUEUE_DURABLE),
exchange = @Exchange(value = RabbitInfo.EXCHANGE_NAME, type = RabbitInfo.EXCHANGE_TYPE),
key = RabbitInfo.ROUTING_KEY)
)
@RabbitHandler
public void onMessage(Message message, Channel channel) throws Exception {
MessageHeaders headers = message.getHeaders();
// 获取消息头信息和消息体
log.info("msgInfo:{} ; payload:{} ", headers.get("msgInfo"), message.getPayload());
// DELIVERY_TAG 代表 RabbitMQ 向该 Channel 投递的这条消息的唯一标识 ID是一个单调递增的正整数
Long deliveryTag = (Long) headers.get(AmqpHeaders.DELIVERY_TAG);
// 第二个参数代表是否一次签收多条,当该参数为 true 时,则可以一次性确认 DELIVERY_TAG 小于等于传入值的所有消息
channel.basicAck(deliveryTag, false);
}
}
```
## 五、 消息生产者rabbitmq-producer
<div align="center"> <img src="https://github.com/heibaiying/spring-samples-for-all/blob/master/pictures/rabbitmq-producer.png"/> </div>
#### 5.1 消息生产者配置
```yaml
spring:
rabbitmq:
addresses: 127.0.0.1:5672
# RabbitMQ 默认的用户名和密码都是 guest 而虚拟主机名称是 "/"
# 如果配置其他虚拟主机地址,需要预先用管控台或者图形界面创建 图形界面地址 http://主机地址:15672
username: guest
password: guest
virtual-host: /
# 是否启用发布者确认 具体确认回调实现见代码
publisher-confirms: true
# 是否启用发布者返回 具体返回回调实现见代码
publisher-returns: true
# 是否启用强制消息 保证消息的有效监听
template.mandatory: true
server:
port: 8090
```
#### 5.2 创建消息生产者
```java
/**
* @author : heibaiying
* @description : 消息生产者
*/
@Component
@Slf4j
public class RabbitmqProducer {
@Autowired
private RabbitTemplate rabbitTemplate;
public void sendSimpleMessage(Map<String, Object> headers, Object message,
String messageId, String exchangeName, String key) {
// 自定义消息头
MessageHeaders messageHeaders = new MessageHeaders(headers);
// 创建消息
Message<Object> msg = MessageBuilder.createMessage(message, messageHeaders);
/* 确认的回调 确认消息是否到达 Broker 服务器 其实就是是否到达交换器
如果发送时候指定的交换器不存在 ack 就是 false 代表消息不可达 */
rabbitTemplate.setConfirmCallback((correlationData, ack, cause) -> {
log.info("correlationData{} , ack:{}", correlationData.getId(), ack);
if (!ack) {
System.out.println("进行对应的消息补偿机制");
}
});
/* 消息失败的回调
* 例如消息已经到达交换器上,但路由键匹配任何绑定到该交换器的队列,会触发这个回调,此时 replyText: NO_ROUTE
*/
rabbitTemplate.setReturnCallback((message1, replyCode, replyText, exchange, routingKey) -> {
log.info("message:{}; replyCode: {}; replyText: {} ; exchange:{} ; routingKey:{}",
message1, replyCode, replyText, exchange, routingKey);
});
// 在实际中 ID 应该是全局唯一 能够唯一标识消息 消息不可达的时候触发 ConfirmCallback 回调方法时可以获取该值,进行对应的错误处理
CorrelationData correlationData = new CorrelationData(messageId);
rabbitTemplate.convertAndSend(exchangeName, key, msg, correlationData);
}
}
```
#### 5.3 以单元测试的方式发送消息
```java
@RunWith(SpringRunner.class)
@SpringBootTest
public class RabbitmqProducerTests {
@Autowired
private RabbitmqProducer producer;
/***
* 发送消息体为简单数据类型的消息
*/
@Test
public void send() {
Map<String, Object> heads = new HashMap<>();
heads.put("msgInfo", "自定义消息头信息");
// 模拟生成消息 ID,在实际中应该是全局唯一的 消息不可达时候可以在 setConfirmCallback 回调中取得,可以进行对应的重发或错误处理
String id = String.valueOf(Math.round(Math.random() * 10000));
producer.sendSimpleMessage(heads, "hello Spring", id, RabbitInfo.EXCHANGE_NAME, "springboot.simple.abc");
}
/***
* 发送消息体为 bean 的消息
*/
@Test
public void sendBean() {
String id = String.valueOf(Math.round(Math.random() * 10000));
Programmer programmer = new Programmer("xiaoMing", 12, 12123.45f, new Date());
producer.sendSimpleMessage(null, programmer, id, RabbitBeanInfo.EXCHANGE_NAME, RabbitBeanInfo.ROUTING_KEY);
}
}
```
## 六、项目构建的说明
因为在项目中consumer 和 producer 模块均依赖公共模块,所以在构建 consumer 和 producer 项目前需要将 common 模块安装到本地仓库,**依次**对**父工程**和**common 模块**执行:
```shell
mvn install -Dmaven.test.skip = true
```
consumer 中 pom.xml 如下
```xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.heibaiying</groupId>
<artifactId>spring-boot-rabbitmq</artifactId>
<version>0.0.1-SNAPSHOT</version>
</parent>
<artifactId>rabbitmq-consumer</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>rabbitmq-consumer</name>
<description>RabbitMQ consumer project for Spring Boot</description>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>com.heibaiying</groupId>
<artifactId>rabbitmq-common</artifactId>
<version>0.0.1-SNAPSHOT</version>
<scope>compile</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
```
producer 中 pom.xml 如下
```xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.heibaiying</groupId>
<artifactId>spring-boot-rabbitmq</artifactId>
<version>0.0.1-SNAPSHOT</version>
</parent>
<artifactId>rabbitmq-producer</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>rabbitmq-producer</name>
<description>RabbitMQ producer project for Spring Boot</description>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>com.heibaiying</groupId>
<artifactId>rabbitmq-common</artifactId>
<version>0.0.1-SNAPSHOT</version>
<scope>compile</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
```

View File

@ -1,5 +1,6 @@
# spring boot 整合 redis
# spring boot 整合 redis
## 目录<br/>
<a href="#一说明">一、说明</a><br/>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href="#11-项目结构">1.1 项目结构</a><br/>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href="#12-项目主要依赖">1.2 项目主要依赖</a><br/>
@ -34,445 +35,445 @@
## 一、说明
#### 1.1 项目结构
1. RedisConfig.java实现了redisTemplate 序列化与反序列化的配置;
2. RedisOperation和RedisObjectOperation分别封装了对基本类型和对象的操作。
<div align="center"> <img src="https://github.com/heibaiying/spring-samples-for-all/blob/master/pictures/spring-boot-redis.png"/> </div>
#### 1.2 项目主要依赖
```xml
<!--redis starter -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!--jackson 序列化包 -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.9.8</version>
</dependency>
```
## 二、整合 Redis
#### 2.1 在application.yml 中配置redis数据源
```yaml
spring:
redis:
host: 127.0.0.1
port: 6379
# 默认采用的也是 0 号数据库 redis官方在4.0之后版本就不推荐采用单节点多数据库(db1-db15)的方式存储数据,如果有需要应该采用集群方式构建
database: 0
# 如果是集群节点 采用如下配置指定节点
#spring.redis.cluster.nodes
```
#### 2.2 封装redis基本操作
需要说明的是spring boot 提供了两个template 用于操作redis:
- StringRedisTemplate由于redis在大多数使用情况下都是操作字符串类型的存储所以spring boot 将对字符串的操作单独封装在StringRedisTemplate
- RedisTemplate<Object, Object> redis 用于操作任意类型的template。
```java
/**
* @author : heibaiying
* @description : redis 基本操作
*/
@Component
public class RedisOperation {
@Autowired
private StringRedisTemplate redisTemplate;
/***
* 操作普通字符串
*/
public void StringSet(String key, String value) {
ValueOperations<String, String> valueOperations = redisTemplate.opsForValue();
valueOperations.set(key, value);
}
/***
* 操作列表
*/
public void ListSet(String key, List<String> values) {
ListOperations<String, String> listOperations = redisTemplate.opsForList();
values.forEach(value -> listOperations.leftPush(key, value));
}
/***
* 操作集合
*/
public void SetSet(String key, Set<String> values) {
SetOperations<String, String> setOperations = redisTemplate.opsForSet();
values.forEach(value -> setOperations.add(key, value));
}
/***
* 获取字符串
*/
public String StringGet(String key) {
ValueOperations<String, String> valueOperations = redisTemplate.opsForValue();
return valueOperations.get(key);
}
/***
* 列表弹出元素
*/
public String ListLeftPop(String key) {
ListOperations<String, String> listOperations = redisTemplate.opsForList();
return listOperations.leftPop(key, 2, TimeUnit.SECONDS);
}
/***
* 集合弹出元素
*/
public String SetPop(String key) {
SetOperations<String, String> setOperations = redisTemplate.opsForSet();
return setOperations.pop(key);
}
}
```
```java
/**
* @author : heibaiying
* @description : redis 基本操作
*/
@Component
public class RedisObjectOperation {
@Autowired
private RedisTemplate<Object, Object> objectRedisTemplate;
/***
* 操作对象
*/
public void ObjectSet(Object key, Object value) {
ValueOperations<Object, Object> valueOperations = objectRedisTemplate.opsForValue();
valueOperations.set(key, value);
}
/***
* 操作元素为对象列表
*/
public void ListSet(Object key, List<Object> values) {
ListOperations<Object, Object> listOperations = objectRedisTemplate.opsForList();
values.forEach(value -> listOperations.leftPush(key, value));
}
/***
* 操作元素为对象集合
*/
public void SetSet(Object key, Set<Object> values) {
SetOperations<Object, Object> setOperations = objectRedisTemplate.opsForSet();
values.forEach(value -> setOperations.add(key, value));
}
/***
* 获取对象
*/
public Object ObjectGet(Object key) {
ValueOperations<Object, Object> valueOperations = objectRedisTemplate.opsForValue();
return valueOperations.get(key);
}
/***
* 列表弹出元素
*/
public Object ListLeftPop(Object key) {
ListOperations<Object, Object> listOperations = objectRedisTemplate.opsForList();
return listOperations.leftPop(key, 2, TimeUnit.SECONDS);
}
/***
* 集合弹出元素
*/
public Object SetPop(Object key) {
SetOperations<Object, Object> setOperations = objectRedisTemplate.opsForSet();
return setOperations.pop(key);
}
}
```
#### 2.3 redisTemplate 序列化为json格式与反序列化
这里需要说明的spring boot 的 redisTemplate 本身是实现了对象的序列化与反序列化的是支持直接存取对象的。但是这里的序列化默认采用的是JdkSerializationRedisSerializer.serialize()序列化为二进制码这个本身是不影响redisTemplate 的操作的因为redisTemplate在取出数据的时候会自动进行反序列化。
但是如果我们在命令行中使用get命令去获取数据时候得到的是一串不直观的二进制码所以我们尽量将其序列化为直观的json格式存储。
```java
/**
* @author : heibaiying
* @description : 自定义序列化器
* 不定义的话默认采用的是serializer.JdkSerializationRedisSerializer.serialize()序列化为二进制字节码 存储在数据库中不直观
*/
@Configuration
public class RedisConfig {
@Bean
public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
RedisTemplate<Object, Object> redisTemplate = new RedisTemplate<>();
redisTemplate.setConnectionFactory(redisConnectionFactory);
// 使用Jackson2JsonRedisSerialize 需要导入依赖 com.fasterxml.jackson.core jackson-databind
Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
ObjectMapper objectMapper = new ObjectMapper();
// 第个参数表示: 表示所有访问者都受到影响 包括 字段, getter / isGetter,settercreator
// 第二个参数表示: 所有类型的访问修饰符都是可接受的,不论是公有还有私有表变量都会被序列化
objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
objectMapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
jackson2JsonRedisSerializer.setObjectMapper(objectMapper);
// 设置key,value 序列化规则
redisTemplate.setValueSerializer(jackson2JsonRedisSerializer);
redisTemplate.setKeySerializer(new StringRedisSerializer());
redisTemplate.afterPropertiesSet();
return redisTemplate;
}
}
```
#### 2.4 测试
```java
@RunWith(SpringRunner.class)
@SpringBootTest
public class RedisTests {
@Autowired
private RedisOperation redisOperation;
@Test
public void StringOperation() {
redisOperation.StringSet("hello", "redis");
String s = redisOperation.StringGet("hello");
Assert.assertEquals(s, "redis");
}
@Test
public void ListOperation() {
redisOperation.ListSet("skill", Arrays.asList("java", "oracle", "vue"));
String s = redisOperation.ListLeftPop("skill");
Assert.assertEquals(s, "vue");
}
/*
* 需要注意的是Redis的集合set不仅不允许有重复元素并且集合中的元素是无序的
* 不能通过索引下标获取元素。哪怕你在java中传入的集合是有序的newLinkedHashSet但是实际在Redis存储的还是无序的集合
*/
@Test
public void SetOperation() {
redisOperation.SetSet("skillSet", Sets.newLinkedHashSet("java", "oracle", "vue"));
String s = redisOperation.SetPop("skillSet");
Assert.assertNotNull(s);
}
}
```
<br/>
## 附Redis的数据结构和操作命令
### 1.1 预备
#### 1.1.1 全局命令
1. 查看所有键: **keys \***
2. 查看键总数:**dbsize**
3. 检查键是否存在:**exists key**
4. 删除键:**del key [key ...]** 支持删除多个键
5. 键过期:**expire key seconds**
ttl命令会返回键的剩余过期时间 它有3种返回值
- 大于等于0的整数 键剩余的过期时间。
- -1没设置过期时间。
- -2 键不存在
6. 键的数据结构 **type key**
#### 1.1.2 数据结构和内部编码
type命令实际返回的就是当前键的数据结构类型 它们分别是:**string**(字符串) 、 **hash**(哈希) 、 **list**(列表) 、 **set**(集合) 、 **zset**(有序集合)
#### 1.1.3 单线程架构
1. 纯内存访问, Redis将所有数据放在内存中 内存的响应时长大约为100纳秒 这是Redis达到每秒万级别访问的重要基础。
2. 非阻塞I/O Redis使用epoll作为I/O多路复用技术的实现 再加上Redis自身的事件处理模型将epoll中的连接、 读写、 关闭都转换为事件, 不在网络I/O上浪费过多的时间 如图2-6所示
3. 单线程避免了线程切换和竞态产生的消耗。
### 1.2 字符串
| 作用 | 格式 | 参数或示例 |
| ---------------------- | ------------------------------------------------------------ | ------------------------------------------------------------ |
| 设置值 | set key value \[ex seconds]\[px milliseconds][nx\|xx] setnx setex | ex seconds 为键设置秒级过期时间。 <br/>px milliseconds 为键设置毫秒级过期时间。<br/>nx 键必须不存在, 才可以设置成功, 用于添加。<br/>xx 与nx相反 键必须存在, 才可以设置成功, 用于更新。 |
| 获取值 | get key | r如果获取的键不存在 则返回nil(空) |
| 批量设置 | mset key value [key value ...] | mset a 1 b 2 c 3 d 4 |
| 批量获取值 | mget key [key ...] | mget a b c d |
| 计数 | incr key decr key incrby key increment指定数值自增<br/>decrby key decrement指定数值自减<br/>incrbyfloat key increment (浮点数自增) | 值不是整数, 返回错误。 值是整数, 返回自增或自减后的结果。<br/>键不存在创建键并按照值为0自增或自减 返回结果为1。 |
| 追加值 | append key value | 向字符串的默认追加值 |
| 字符串长度 | strlen key | 获取字符串长度,中文占用三个字节 |
| 设置并返回原值 | getset key value | |
| 设置指定位置的租字符串 | setrange key offeset value | |
| 获取部分字符串 | getrange key start end | |
### 1.3 哈希
| 作用 | 格式 | 参数或示例 |
| ------------------------- | ------------------------------------------------------------ | ------------------------------------------------------------ |
| 设置值 | hset key field value | hset user:1 name tom<br/>hset user:1 age 12 |
| 获取值 | hget key field | hget user:1 name |
| 删除field | hdel key field [field ...] | |
| 计算field个数 | hlen key | |
| 批量设置或获取field-value | hmget key field [field]<br/>hmset key field value [field value...] | hmset user:1 name mike age 12 city tianjin<br/>hmget user:1 name city |
| 判断field是否存在 | hexists key field | |
| 获取所有field | hkeys key | |
| 获取所有value | hvals key | |
| 获取所有的filed-value | hgetall key | 如果哈希元素个数比较多, 会存在阻塞Redis的可能。<br/>获取全部 可以使用hscan命令 该命令会渐进式遍历哈希类型 |
| 计数 | hincrby key field<br/>hincrbyfloat key field | |
### 1.4 列表
| 作用 | 格式 | 参数或示例 |
| -------- | ------------------------------------------------------------ | ------------------------------------------------------------ |
| | 左侧插入lpush key value [value ...] 右侧插入rpush key value [value ...] 某个指定元素前后插入linsert key before\|after pivot value | |
| | 获取指定范围内的元素列表lrange key start end 获取列表指定索引下标的元素lindex key index 获取列表指定长度llen key | lrange listkey 0 -1 |
| | 从列表左侧弹出元素lpop key 从列表右侧弹出元素rpop key 删除指定元素lrem key count value 截取列表ltrim key start end | count>0 从左到右, 删除最多count个元素。<br/>count<0 从右到左 删除最多count绝对值个元素。<br/>count=0 删除所有 |
| 改 | 修改指定索引下标的元素lset key index newValue | |
| 阻塞操作 | blpop key [key ...] timeout brpop key [key ...] timeout | key[key...] 多个列表的键。 timeout 阻塞时间\|等待时间(单位: 秒) |
### 1.5 集合
集合set 类型也是用来保存多个的字符串元素, 但和列表类型不一样的是, **集合中不允许有重复元素** 并且集合中的元素是无序的, **不能通过索引下标获取元素**
**集合内操作**
| 作用 | 格式 | 参数或示例 |
| -------------------- | ------------------------------ | ----------------------------------------- |
| 添加元素 | sadd key element [element ...] | 返回结果为添加成功的元素个数 |
| 删除元素 | srem key element [element ...] | 返回结果为成功删除的元素个数 |
| 计算元素个数 | scard key | |
| 判断元素是否在集合中 | sismember key element | |
| 随机返回 | srandmember key [count] | 随机从集合返回指定个数元素count 默认为1 |
| 从集合随机弹出元素 | spop key | srandmember 不会从集合中删除元素spop 会 |
| 获取集合中所有元素 | smembers key | 可用sscan 代替 |
**集合间操作**
| 作用 | 格式 |
| ---------------------------- | ------------------------------------------------------------ |
| 求多个集合的集 | sinter key [key ...] |
| 求多个集合的集 | suinon key [key ...] |
| 求多个集合的差集 | sdiff key [key ...] |
| 将交集、并集、差集的结果保存 | sinterstore destination key [key ...] <br/>suionstore destination key [key ...]<br/>sdiffstore destination key [key ...] |
### 1.6 有序集合
有序集合中的元素可以排序。 但是它和列表使用索引下标作为排序依据不同的是, 它给每个元素设置一个分数score 作为排序的依据。
**集合内操作**
| 作用 | 格式 | 参数或示例 |
| ------------------------ | ------------------------------------------------------------ | ------------------------------------------------------------ |
| 添加成员 | zadd key score member [score member ...] | nx member必须不存在 才可设置成功, 用于添加。<br> xx member必须存在 才可以设置成功, 用于更新。<br/>ch 返回此次操作后, 有序集合元素和分数发生变化的个数<br/>incr 对score做增加 相当于后面介绍的zincrby。 |
| 计算成员个数 | zcard key | |
| 计算某个成员的分数 | zscore key member | |
| 计算某个成员的排名 | zrank key member zrevrank key member | zrank是从分数从低到高返回排名 zrevrank反之。 |
| 删除成员 | zrem key member [member ...] | |
| 增加成员分数 | zincrby key increment member | zincrby user:ranking 9 tom |
| 返回指定排名范围的成员 | zrange key start end [withscores] zrange key start end [withscores] | zrange是从低到高返回 zrevrange反之。 |
| 返回指定分数范围内的成员 | zrangebyscore key min max \[withscores][limit offset count] zrevrangebyscore key max min \[withscores][limit offset count] | 其中zrangebyscore按照分数从低到高返回 zrevrangebyscore反之。 [limit offset count]选项可以限制输出的起始位置和个数: 同时min和max还支持开区间小括号 和闭区间(中括号) -inf和+inf分别代表无限小和无限大 |
| 删除指定排名内的升序元素 | zremrangerank key start end | |
| 删除指定分数范围的成员 | zremrangebyscore key min max | |
**集合间操作**
| 作用 | 格式 |
| ---- | ------------------------------------------------------------ |
| 集 | zinterstore destination numkeys key \[key ...] [weights weight [weight ...]] \[aggregate sum\|min\|max] |
| 并集 | zunionstore destination numkeys key \[key ...] [weights weight [weight ...]] \[aggregate sum\|min\|max] |
- destination 交集计算结果保存到这个键
- numkeys 需要做交集计算键的个数。
- key[key...] 需要做交集计算的键。
- weights weight[weight...] 每个键的权重, 在做交集计算时, 每个键中的每个member会将自己分数乘以这个权重 每个键的权重默认是1。
- aggregate sum|min|max 计算成员交集后, 分值可以按照sum 、min最小值 、 max最大值 做汇总, 默认值是sum。
### 1.7 键管理
#### 1.7.1 单个键管理
##### 1.键重命名
**rename key newkey**
为了防止被强行rename Redis提供了renamenx命令 确保只有newKey不存在时候才被覆盖。
##### 2. 随机返回键
**random key**
##### 3.键过期
- expire key seconds 键在seconds秒后过期。
- expireat key timestamp 键在秒级时间戳timestamp后过期。
- pexpire key milliseconds 键在milliseconds毫秒后过期
- pexpireat key milliseconds-timestamp键在毫秒级时间戳timestamp后过期
注意:
1. 如果expire key的键不存在 返回结果为0
2. 如果设置过期时间为负值, 键会立即被删除, 犹如使用del命令一样
3. persist key t命令可以将键的过期时间清除
4. 对于字符串类型键, 执行set命令会去掉过期时间 这个问题很容易在开发中被忽视
5. Redis不支持二级数据结构例如哈希、 列表) 内部元素的过期功能, 例如不能对列表类型的一个元素做过期时间设置
6. setex命令作为set+expire的组合 不但是原子执行, 同时减少了一次网络通讯的时间
#### 1.7.2 键遍历
##### 1. 全量键遍历
**keys pattern**
##### 2. 渐进式遍历
scan cursor \[match pattern] \[count number]
- cursor是必需参数 实际上cursor是一个游标 第一次遍历从0开始 每次scan遍历完都会返回当前游标的值 直到游标值为0 表示遍历结束
- match pattern是可选参数, 它的作用的是做模式的匹配, 这点和keys的模式匹配很像。
- count number是可选参数 它的作用是表明每次要遍历的键个数, 默认值是10 此参数可以适当增大。
#### 1.7.3 数据库管理
##### 1.切换数据库
**select dbIndex**
##### 2.flushdb/flushall
## 一、说明
#### 1.1 项目结构
1. RedisConfig.java 实现了 redisTemplate 序列化与反序列化的配置;
2. RedisOperation 和 RedisObjectOperation 分别封装了对基本类型和对象的操作。
<div align="center"> <img src="https://github.com/heibaiying/spring-samples-for-all/blob/master/pictures/spring-boot-redis.png"/> </div>
#### 1.2 项目主要依赖
```xml
<!--redis starter -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!--jackson 序列化包 -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.9.8</version>
</dependency>
```
## 二、整合 Redis
#### 2.1 在application.yml 中配置redis数据源
```yaml
spring:
redis:
host: 127.0.0.1
port: 6379
# 默认采用的也是 0 号数据库 redis 官方在 4.0 之后版本就不推荐采用单节点多数据库 (db1-db15) 的方式存储数据,如果有需要应该采用集群方式构建
database: 0
# 如果是集群节点 采用如下配置指定节点
#spring.redis.cluster.nodes
```
#### 2.2 封装redis基本操作
需要说明的是 spring boot 提供了两个 template 用于操作 redis:
- StringRedisTemplate由于 redis 在大多数使用情况下都是操作字符串类型的存储,所以 spring boot 将对字符串的操作单独封装在 StringRedisTemplate
- RedisTemplate<Object, Object> redis 用于操作任意类型的 template
```java
/**
* @author : heibaiying
* @description : redis 基本操作
*/
@Component
public class RedisOperation {
@Autowired
private StringRedisTemplate redisTemplate;
/***
* 操作普通字符串
*/
public void StringSet(String key, String value) {
ValueOperations<String, String> valueOperations = redisTemplate.opsForValue();
valueOperations.set(key, value);
}
/***
* 操作列表
*/
public void ListSet(String key, List<String> values) {
ListOperations<String, String> listOperations = redisTemplate.opsForList();
values.forEach(value -> listOperations.leftPush(key, value));
}
/***
* 操作集合
*/
public void SetSet(String key, Set<String> values) {
SetOperations<String, String> setOperations = redisTemplate.opsForSet();
values.forEach(value -> setOperations.add(key, value));
}
/***
* 获取字符串
*/
public String StringGet(String key) {
ValueOperations<String, String> valueOperations = redisTemplate.opsForValue();
return valueOperations.get(key);
}
/***
* 列表弹出元素
*/
public String ListLeftPop(String key) {
ListOperations<String, String> listOperations = redisTemplate.opsForList();
return listOperations.leftPop(key, 2, TimeUnit.SECONDS);
}
/***
* 集合弹出元素
*/
public String SetPop(String key) {
SetOperations<String, String> setOperations = redisTemplate.opsForSet();
return setOperations.pop(key);
}
}
```
```java
/**
* @author : heibaiying
* @description : redis 基本操作
*/
@Component
public class RedisObjectOperation {
@Autowired
private RedisTemplate<Object, Object> objectRedisTemplate;
/***
* 操作对象
*/
public void ObjectSet(Object key, Object value) {
ValueOperations<Object, Object> valueOperations = objectRedisTemplate.opsForValue();
valueOperations.set(key, value);
}
/***
* 操作元素为对象列表
*/
public void ListSet(Object key, List<Object> values) {
ListOperations<Object, Object> listOperations = objectRedisTemplate.opsForList();
values.forEach(value -> listOperations.leftPush(key, value));
}
/***
* 操作元素为对象集合
*/
public void SetSet(Object key, Set<Object> values) {
SetOperations<Object, Object> setOperations = objectRedisTemplate.opsForSet();
values.forEach(value -> setOperations.add(key, value));
}
/***
* 获取对象
*/
public Object ObjectGet(Object key) {
ValueOperations<Object, Object> valueOperations = objectRedisTemplate.opsForValue();
return valueOperations.get(key);
}
/***
* 列表弹出元素
*/
public Object ListLeftPop(Object key) {
ListOperations<Object, Object> listOperations = objectRedisTemplate.opsForList();
return listOperations.leftPop(key, 2, TimeUnit.SECONDS);
}
/***
* 集合弹出元素
*/
public Object SetPop(Object key) {
SetOperations<Object, Object> setOperations = objectRedisTemplate.opsForSet();
return setOperations.pop(key);
}
}
```
#### 2.3 redisTemplate 序列化为json格式与反序列化
这里需要说明的 spring boot 的 redisTemplate 本身是实现了对象的序列化与反序列化的,是支持直接存取对象的。但是这里的序列化默认采用的是 JdkSerializationRedisSerializer.serialize() 序列化为二进制码,这个本身是不影响 redisTemplate 的操作的,因为 redisTemplate 在取出数据的时候会自动进行反序列化。
但是如果我们在命令行中使用 get 命令去获取数据时候,得到的是一串不直观的二进制码,所以我们尽量将其序列化为直观的 json 格式存储。
```java
/**
* @author : heibaiying
* @description : 自定义序列化器
* 不定义的话默认采用的是 serializer.JdkSerializationRedisSerializer.serialize() 序列化为二进制字节码 存储在数据库中不直观
*/
@Configuration
public class RedisConfig {
@Bean
public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
RedisTemplate<Object, Object> redisTemplate = new RedisTemplate<>();
redisTemplate.setConnectionFactory(redisConnectionFactory);
// 使用 Jackson2JsonRedisSerialize 需要导入依赖 com.fasterxml.jackson.core jackson-databind
Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
ObjectMapper objectMapper = new ObjectMapper();
// 第一个参数表示: 表示所有访问者都受到影响 包括 字段, getter / isGetter,settercreator
// 第个参数表示: 所有类型的访问修饰符都是可接受的,不论是公有还有私有表变量都会被序列化
objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
objectMapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
jackson2JsonRedisSerializer.setObjectMapper(objectMapper);
// 设置 key,value 序列化规则
redisTemplate.setValueSerializer(jackson2JsonRedisSerializer);
redisTemplate.setKeySerializer(new StringRedisSerializer());
redisTemplate.afterPropertiesSet();
return redisTemplate;
}
}
```
#### 2.4 测试
```java
@RunWith(SpringRunner.class)
@SpringBootTest
public class RedisTests {
@Autowired
private RedisOperation redisOperation;
@Test
public void StringOperation() {
redisOperation.StringSet("hello", "redis");
String s = redisOperation.StringGet("hello");
Assert.assertEquals(s, "redis");
}
@Test
public void ListOperation() {
redisOperation.ListSet("skill", Arrays.asList("java", "oracle", "vue"));
String s = redisOperation.ListLeftPop("skill");
Assert.assertEquals(s, "vue");
}
/*
* 需要注意的是 Redis 的集合set不仅不允许有重复元素并且集合中的元素是无序的
* 不能通过索引下标获取元素。哪怕你在 java 中传入的集合是有序的 newLinkedHashSet但是实际在 Redis 存储的还是无序的集合
*/
@Test
public void SetOperation() {
redisOperation.SetSet("skillSet", Sets.newLinkedHashSet("java", "oracle", "vue"));
String s = redisOperation.SetPop("skillSet");
Assert.assertNotNull(s);
}
}
```
<br/>
## 附Redis的数据结构和操作命令
### 1.1 预备
#### 1.1.1 全局命令
1. 查看所有键: **keys \***
2. 查看键总数:**dbsize**
3. 检查键是否存在:**exists key**
4. 删除键:**del key [key ...]** 支持删除多个键
5. 键过期:**expire key seconds**
ttl 命令会返回键的剩余过期时间, 它有 3 种返回值:
- 大于等于 0 的整数: 键剩余的过期时间。
- -1 键没设置过期时间。
- -2不存在
6. 键的数据结构 **type key**
#### 1.1.2 数据结构和内部编码
type 命令实际返回的就是当前键的数据结构类型, 它们分别是:**string**(字符串) 、 **hash**(哈希) 、 **list**(列表) 、 **set**(集合) 、 **zset**(有序集合)
#### 1.1.3 单线程架构
1. 纯内存访问, Redis 将所有数据放在内存中, 内存的响应时长大约为 100 纳秒, 这是 Redis 达到每秒万级别访问的重要基础。
2. 非阻塞 I/O Redis 使用 epoll 作为 I/O 多路复用技术的实现, 再加上 Redis 自身的事件处理模型将 epoll 中的连接、 读写、 关闭都转换为事件, 不在网络 I/O 上浪费过多的时间, 如图 2-6 所示。
3. 单线程避免了线程切换和竞态产生的消耗
### 1.2 字符串
| 作用 | 格式 | 参数或示例 |
| ---------------------- | ------------------------------------------------------------ | ------------------------------------------------------------ |
| 设置值 | set key value \[ex seconds]\[px milliseconds][nx\|xx] setnx setex | ex seconds 为键设置秒级过期时间。 <br/>px milliseconds 为键设置毫秒级过期时间。<br/>nx 键必须不存在, 才可以设置成功, 用于添加。<br/>xx 与 nx 相反, 键必须存在, 才可以设置成功, 用于更新。 |
| 获取值 | get key | r 如果获取的键不存在 ,则返回 nil(空) |
| 批量设置 | mset key value [key value ...] | mset a 1 b 2 c 3 d 4 |
| 批量获取值 | mget key [key ...] | mget a b c d |
| 计数 | incr key decr key incrby key increment指定数值自增<br/>decrby key decrement指定数值自减<br/>incrbyfloat key increment (浮点数自增) | 值不是整数, 返回错误。 值是整数, 返回自增或自减后的结果。<br/>键不存在,创建键,并按照值为 0 自增或自减, 返回结果为 1。 |
| 追加值 | append key value | 向字符串的默认追加值 |
| 字符串长度 | strlen key | 获取字符串长度,中文占用三个字节 |
| 设置并返回原值 | getset key value | |
| 设置指定位置的租字符串 | setrange key offeset value | |
| 获取部分字符串 | getrange key start end | |
### 1.3 哈希
| 作用 | 格式 | 参数或示例 |
| ------------------------- | ------------------------------------------------------------ | ------------------------------------------------------------ |
| 设置值 | hset key field value | hset user:1 name tom<br/>hset user:1 age 12 |
| 获取值 | hget key field | hget user:1 name |
| 删除 field | hdel key field [field ...] | |
| 计算 field 个数 | hlen key | |
| 批量设置或获取 field-value | hmget key field [field]<br/>hmset key field value [field value...] | hmset user:1 name mike age 12 city tianjin<br/>hmget user:1 name city |
| 判断 field 是否存在 | hexists key field | |
| 获取所有 field | hkeys key | |
| 获取所有 value | hvals key | |
| 获取所有的 filed-value | hgetall key | 如果哈希元素个数比较多, 会存在阻塞 Redis 的可能。<br/>获取全部 可以使用 hscan 命令, 该命令会渐进式遍历哈希类型 |
| 计数 | hincrby key field<br/>hincrbyfloat key field | |
### 1.4 列表
| 作用 | 格式 | 参数或示例 |
| -------- | ------------------------------------------------------------ | ------------------------------------------------------------ |
| 增 | 左侧插入lpush key value [value ...] 右侧插入rpush key value [value ...] 某个指定元素前后插入linsert key before\|after pivot value | |
| | 获取指定范围内的元素列表lrange key start end 获取列表指定索引下标的元素lindex key index 获取列表指定长度llen key | lrange listkey 0 -1 |
| | 从列表左侧弹出元素lpop key 从列表右侧弹出元素rpop key 删除指定元素lrem key count value 截取列表ltrim key start end | count>0 从左到右, 删除最多 count 个元素。<br/>count<0 从右到左 删除最多 count 绝对值个元素。<br/>count=0 删除所有 |
| | 修改指定索引下标的元素lset key index newValue | |
| 阻塞操作 | blpop key [key ...] timeout brpop key [key ...] timeout | key[key...] 多个列表的键。 timeout 阻塞时间\|等待时间(单位: 秒) |
### 1.5 集合
集合set 类型也是用来保存多个的字符串元素, 但和列表类型不一样的是, **集合中不允许有重复元素** 并且集合中的元素是无序的, **不能通过索引下标获取元素**
**集合内操作**
| 作用 | 格式 | 参数或示例 |
| -------------------- | ------------------------------ | ----------------------------------------- |
| 添加元素 | sadd key element [element ...] | 返回结果为添加成功的元素个数 |
| 删除元素 | srem key element [element ...] | 返回结果为成功删除的元素个数 |
| 计算元素个数 | scard key | |
| 判断元素是否在集合中 | sismember key element | |
| 随机返回 | srandmember key [count] | 随机从集合返回指定个数元素count 默认为 1 |
| 从集合随机弹出元素 | spop key | srandmember 不会从集合中删除元素spop 会 |
| 获取集合中所有元素 | smembers key | 可用 sscan 代替 |
**集合间操作**
| 作用 | 格式 |
| ---------------------------- | ------------------------------------------------------------ |
| 求多个集合的交集 | sinter key [key ...] |
| 求多个集合的集 | suinon key [key ...] |
| 求多个集合的集 | sdiff key [key ...] |
| 将交集、并集、差集的结果保存 | sinterstore destination key [key ...] <br/>suionstore destination key [key ...]<br/>sdiffstore destination key [key ...] |
### 1.6 有序集合
有序集合中的元素可以排序。 但是它和列表使用索引下标作为排序依据不同的是, 它给每个元素设置一个分数score 作为排序的依据。
**集合内操作**
| 作用 | 格式 | 参数或示例 |
| ------------------------ | ------------------------------------------------------------ | ------------------------------------------------------------ |
| 添加成员 | zadd key score member [score member ...] | nx member 必须不存在, 才可设置成功, 用于添加。<br> xx member 必须存在, 才可以设置成功, 用于更新。<br/>ch 返回此次操作后, 有序集合元素和分数发生变化的个数<br/>incr 对 score 做增加, 相当于后面介绍的 zincrby。 |
| 计算成员个数 | zcard key | |
| 计算某个成员的分数 | zscore key member | |
| 计算某个成员的排名 | zrank key member zrevrank key member | zrank 是从分数从低到高返回排名, zrevrank 反之。 |
| 删除成员 | zrem key member [member ...] | |
| 增加成员分数 | zincrby key increment member | zincrby user:ranking 9 tom |
| 返回指定排名范围的成员 | zrange key start end [withscores] zrange key start end [withscores] | zrange 是从低到高返回, zrevrange 反之。 |
| 返回指定分数范围的成员 | zrangebyscore key min max \[withscores][limit offset count] zrevrangebyscore key max min \[withscores][limit offset count] | 其中 zrangebyscore 按照分数从低到高返回, zrevrangebyscore 反之。 [limit offset count]选项可以限制输出的起始位置和个数: 同时 min 和 max 还支持开区间(小括号) 和闭区间(中括号) -inf 和 +inf 分别代表无限小和无限大 |
| 删除指定排名内的升序元素 | zremrangerank key start end | |
| 删除指定分数范围的成员 | zremrangebyscore key min max | |
**集合间操作**
| 作用 | 格式 |
| ---- | ------------------------------------------------------------ |
| 交集 | zinterstore destination numkeys key \[key ...] [weights weight [weight ...]] \[aggregate sum\|min\|max] |
| 集 | zunionstore destination numkeys key \[key ...] [weights weight [weight ...]] \[aggregate sum\|min\|max] |
- destination 交集计算结果保存到这个键。
- numkeys 需要做交集计算键的个数
- key[key...] 需要做交集计算的键。
- weights weight[weight...] 每个键的权重, 在做交集计算时, 每个键中的每个 member 会将自己分数乘以这个权重, 每个键的权重默认是 1。
- aggregate sum|min|max 计算成员交集后, 分值可以按照 sum 、min最小值 、 max最大值 做汇总, 默认值是 sum。
### 1.7 键管理
#### 1.7.1 单个键管理
##### 1.键重命名
**rename key newkey**
为了防止被强行 rename Redis 提供了 renamenx 命令, 确保只有 newKey 不存在时候才被覆盖。
##### 2. 随机返回键
**random key**
##### 3.键过期
- expire key seconds 键在 seconds 秒后过期。
- expireat key timestamp 键在秒级时间戳 timestamp 后过期。
- pexpire key milliseconds 键在 milliseconds 毫秒后过期。
- pexpireat key milliseconds-timestamp 键在毫秒级时间戳 timestamp 后过期
注意:
1. 如果 expire key 的键不存在, 返回结果为 0
2. 如果设置过期时间为负值, 键会立即被删除, 犹如使用 del 命令一样
3. persist key t 命令可以将键的过期时间清除
4. 对于字符串类型键, 执行 set 命令会去掉过期时间, 这个问题很容易在开发中被忽视
5. Redis 不支持二级数据结构(例如哈希、 列表) 内部元素的过期功能, 例如不能对列表类型的一个元素做过期时间设置
6. setex 命令作为 set+expire 的组合, 不但是原子执行, 同时减少了一次网络通讯的时间
#### 1.7.2 键遍历
##### 1. 全量键遍历
**keys pattern**
##### 2. 渐进式遍历
scan cursor \[match pattern] \[count number]
- cursor 是必需参数, 实际上 cursor 是一个游标, 第一次遍历从 0 开始, 每次 scan 遍历完都会返回当前游标的值, 直到游标值为 0 表示遍历结束。
- match pattern 是可选参数, 它的作用的是做模式的匹配, 这点和 keys 的模式匹配很像
- count number 是可选参数, 它的作用是表明每次要遍历的键个数, 默认值是 10 此参数可以适当增大。
#### 1.7.3 数据库管理
##### 1.切换数据库
**select dbIndex**
##### 2.flushdb/flushall
flushdb/flushall 命令用于清除数据库, 两者的区别的是 flushdb 只清除当前数据库, flushall 会清除所有数据库。

View File

@ -1,5 +1,6 @@
# spring boot 整合 servlet
# spring boot 整合 servlet
## 目录<br/>
<a href="#一说明">一、说明</a><br/>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href="#11-项目结构说明">1.1 项目结构说明</a><br/>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href="#12-项目依赖">1.2 项目依赖</a><br/>
@ -14,231 +15,231 @@
## 一、说明
#### 1.1 项目结构说明
1. 项目提供与servlet整合的两种方式一种是servlet3.0 原生的注解方式,一种是采用spring 注册方式;
2. servlet、过滤器、监听器分别位于servlet、filter、listen 下其中以Annotation命名结尾的代表是servlet注解方式实现采用spring注册方式则在ServletConfig中进行注册
3. 为了说明外置容器对servlet注解的自动发现机制项目采用外置容器构建关于spring boot 整合外置容器的详细说明可以参考[spring-boot-tomcat](https://github.com/heibaiying/spring-samples-for-all/tree/master/spring-boot/spring-boot-tomcat)
<div align="center"> <img src="https://github.com/heibaiying/spring-samples-for-all/blob/master/pictures/spring-boot-servlet.png"/> </div>
#### 1.2 项目依赖
```xml
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<!--排除依赖 使用外部tomcat容器启动-->
<exclusions>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-tomcat</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<!--使用外置容器时候SpringBootServletInitializer 依赖此包 -->
<groupId>javax.servlet</groupId>
<artifactId>servlet-api</artifactId>
<version>2.5</version>
<scope>provided</scope>
</dependency>
<!--servlet api 注解依赖包-->
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
</dependency>
</dependencies>
```
## 二、采用spring 注册方式整合 servlet
#### 2.1 新建过滤器、监听器和servlet
```java
/**
* @author : heibaiying
* @description : 自定义过滤器
*/
public class CustomFilter implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
request.setAttribute("filterParam","我是filter传递的参数");
chain.doFilter(request,response);
response.getWriter().append(" CustomFilter ");
}
@Override
public void destroy() {
}
}
```
```java
/**
* @author : heibaiying
* @description : 自定义监听器
*/
public class CustomListen implements ServletContextListener {
//Web应用程序初始化过程正在启动的通知
@Override
public void contextInitialized(ServletContextEvent sce) {
System.out.println("容器初始化启动");
}
/* 通知servlet上下文即将关闭
* 这个地方如果我们使用的是spring boot 内置的容器 是监听不到销毁过程,所以我们使用了外置 tomcat 容器
*/
@Override
public void contextDestroyed(ServletContextEvent sce) {
System.out.println("容器即将销毁");
}
}
```
```java
/**
* @author : heibaiying
* @description : 自定义servlet
*/
public class CustomServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
System.out.println("doGet 执行:" + req.getAttribute("filterParam"));
resp.getWriter().append("CustomServlet");
}
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
doGet(req, resp);
}
}
```
#### 2.2 注册过滤器、监听器和servlet
```java
/**
* @author : heibaiying
*/
@Configuration
public class ServletConfig {
@Bean
public ServletRegistrationBean registrationBean() {
return new ServletRegistrationBean<HttpServlet>(new CustomServlet(), "/servlet");
}
@Bean
public FilterRegistrationBean filterRegistrationBean() {
FilterRegistrationBean bean = new FilterRegistrationBean<Filter>();
bean.setFilter(new CustomFilter());
bean.addUrlPatterns("/servlet");
return bean;
}
@Bean
public ServletListenerRegistrationBean listenerRegistrationBean() {
return new ServletListenerRegistrationBean<ServletContextListener>(new CustomListen());
}
}
```
## 三、采用注解方式整合 servlet
#### 3.1 新建过滤器、监听器和servlet,分别使用@WebFilter、@WebListener、@WebServlet注解标注
```java
/**
* @author : heibaiying
* @description : 自定义过滤器
*/
@WebFilter(urlPatterns = "/servletAnn")
public class CustomFilterAnnotation implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
chain.doFilter(request,response);
response.getWriter().append(" CustomFilter Annotation");
}
@Override
public void destroy() {
}
}
```
```java
/**
* @author : heibaiying
* @description :自定义监听器
*/
@WebListener
public class CustomListenAnnotation implements ServletContextListener {
//Web应用程序初始化过程正在启动的通知
@Override
public void contextInitialized(ServletContextEvent sce) {
System.out.println("容器初始化启动 Annotation");
}
/* 通知servlet上下文即将关闭
* 这个地方如果我们使用的是spring boot 内置的容器 是监听不到销毁过程,所以我们使用了外置 tomcat 容器
*/
@Override
public void contextDestroyed(ServletContextEvent sce) {
System.out.println("容器即将销毁 Annotation");
}
}
```
```java
/**
* @author : heibaiying
* @description : 自定义servlet
*/
@WebServlet(urlPatterns = "/servletAnn")
public class CustomServletAnnotation extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
resp.getWriter().append("CustomServlet Annotation");
}
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
doGet(req, resp);
}
}
```
#### 3.2 使注解生效
1. 如果是置容器,需要在启动类上添加@ServletComponentScan("com.heibaiying.springbootservlet") ,指定扫描的包目录;
## 一、说明
#### 1.1 项目结构说明
1. 项目提供与 servlet 整合的两种方式,一种是 servlet3.0 原生的注解方式,一种是采用 spring 注册的方式;
2. servlet、过滤器、监听器分别位于 servlet、filter、listen 下,其中以 Annotation 命名结尾的代表是 servlet 注解方式实现,采用 spring 注册方式则在 ServletConfig 中进行注册
3. 为了说明外置容器对 servlet 注解的自动发现机制,项目采用外置容器构建,关于 spring boot 整合外置容器的详细说明可以参考[spring-boot-tomcat](https://github.com/heibaiying/spring-samples-for-all/tree/master/spring-boot/spring-boot-tomcat)
<div align="center"> <img src="https://github.com/heibaiying/spring-samples-for-all/blob/master/pictures/spring-boot-servlet.png"/> </div>
#### 1.2 项目依赖
```xml
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<!--排除依赖 使用外部 tomcat 容器启动-->
<exclusions>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-tomcat</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<!--使用外置容器时候 SpringBootServletInitializer 依赖此包 -->
<groupId>javax.servlet</groupId>
<artifactId>servlet-api</artifactId>
<version>2.5</version>
<scope>provided</scope>
</dependency>
<!--servlet api 注解依赖包-->
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
</dependency>
</dependencies>
```
## 二、采用spring 注册方式整合 servlet
#### 2.1 新建过滤器、监听器和servlet
```java
/**
* @author : heibaiying
* @description : 自定义过滤器
*/
public class CustomFilter implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
request.setAttribute("filterParam","我是 filter 传递的参数");
chain.doFilter(request,response);
response.getWriter().append(" CustomFilter ");
}
@Override
public void destroy() {
}
}
```
```java
/**
* @author : heibaiying
* @description : 自定义监听器
*/
public class CustomListen implements ServletContextListener {
//Web 应用程序初始化过程正在启动的通知
@Override
public void contextInitialized(ServletContextEvent sce) {
System.out.println("容器初始化启动");
}
/* 通知 servlet 上下文即将关闭
* 这个地方如果我们使用的是 spring boot 内置的容器 是监听不到销毁过程,所以我们使用了外置 tomcat 容器
*/
@Override
public void contextDestroyed(ServletContextEvent sce) {
System.out.println("容器即将销毁");
}
}
```
```java
/**
* @author : heibaiying
* @description : 自定义 servlet
*/
public class CustomServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
System.out.println("doGet 执行:" + req.getAttribute("filterParam"));
resp.getWriter().append("CustomServlet");
}
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
doGet(req, resp);
}
}
```
#### 2.2 注册过滤器、监听器和servlet
```java
/**
* @author : heibaiying
*/
@Configuration
public class ServletConfig {
@Bean
public ServletRegistrationBean registrationBean() {
return new ServletRegistrationBean<HttpServlet>(new CustomServlet(), "/servlet");
}
@Bean
public FilterRegistrationBean filterRegistrationBean() {
FilterRegistrationBean bean = new FilterRegistrationBean<Filter>();
bean.setFilter(new CustomFilter());
bean.addUrlPatterns("/servlet");
return bean;
}
@Bean
public ServletListenerRegistrationBean listenerRegistrationBean() {
return new ServletListenerRegistrationBean<ServletContextListener>(new CustomListen());
}
}
```
## 三、采用注解方式整合 servlet
#### 3.1 新建过滤器、监听器和servlet,分别使用@WebFilter、@WebListener、@WebServlet注解标注
```java
/**
* @author : heibaiying
* @description : 自定义过滤器
*/
@WebFilter(urlPatterns = "/servletAnn")
public class CustomFilterAnnotation implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
chain.doFilter(request,response);
response.getWriter().append(" CustomFilter Annotation");
}
@Override
public void destroy() {
}
}
```
```java
/**
* @author : heibaiying
* @description :自定义监听器
*/
@WebListener
public class CustomListenAnnotation implements ServletContextListener {
//Web 应用程序初始化过程正在启动的通知
@Override
public void contextInitialized(ServletContextEvent sce) {
System.out.println("容器初始化启动 Annotation");
}
/* 通知 servlet 上下文即将关闭
* 这个地方如果我们使用的是 spring boot 内置的容器 是监听不到销毁过程,所以我们使用了外置 tomcat 容器
*/
@Override
public void contextDestroyed(ServletContextEvent sce) {
System.out.println("容器即将销毁 Annotation");
}
}
```
```java
/**
* @author : heibaiying
* @description : 自定义 servlet
*/
@WebServlet(urlPatterns = "/servletAnn")
public class CustomServletAnnotation extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
resp.getWriter().append("CustomServlet Annotation");
}
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
doGet(req, resp);
}
}
```
#### 3.2 使注解生效
1. 如果是内置容器,需要在启动类上添加@ServletComponentScan("com.heibaiying.springbootservlet") ,指定扫描的包目录;
2. 如果是置容器,需要进行任何配置,依靠容器内建的 discovery 机制自动发现,需要说明的是这里的容器必须支持 servlet3.0tomcat 从 7.0 版本开始支持 Servlet3.0)。

View File

@ -1,267 +1,267 @@
# spring-boot 集成 Swagger2 打造在线接口文档
## 目录<br/>
<a href="#一Springfox-与-Swagger-简介">一、Springfox 与 Swagger 简介</a><br/>
&nbsp;&nbsp;&nbsp;&nbsp;<a href="#11-Springfox">1.1 Springfox</a><br/>
&nbsp;&nbsp;&nbsp;&nbsp;<a href="#12-Swagger">1.2 Swagger</a><br/>
&nbsp;&nbsp;&nbsp;&nbsp;<a href="#13-OpenApiSwaggerSpringfox的关系">1.3 OpenApi、Swagger、Springfox的关系</a><br/>
<a href="#二spring-boot-集成-swagger-20">二、spring boot 集成 swagger 2.0</a><br/>
&nbsp;&nbsp;&nbsp;&nbsp;<a href="#21-导入项目相关依赖">2.1 导入项目相关依赖</a><br/>
&nbsp;&nbsp;&nbsp;&nbsp;<a href="#22--进行swagger个性化配置并用EnableSwagger2开启Swagger支持">2.2 进行swagger个性化配置、并用@EnableSwagger2开启Swagger支持</a><br/>
&nbsp;&nbsp;&nbsp;&nbsp;<a href="#23-swagger注解的使用和说明">2.3 swagger注解的使用和说明</a><br/>
&nbsp;&nbsp;&nbsp;&nbsp;<a href="#24-swagger-ui-可视化接口文档">2.4 swagger-ui 可视化接口文档</a><br/>
&nbsp;&nbsp;&nbsp;&nbsp;<a href="#25-利用swagger-ui进行接口测试">2.5 利用swagger-ui进行接口测试</a><br/>
## 正文<br/>
## 一、Springfox 与 Swagger 简介
### 1.1 Springfox
Springfox 是一个开源的API Doc的框架 它的前身是swagger-springmvc能够完美的支持springmvc可以将spring 接口方法自动转换为接口文档。 目前spring fox 正致力于对更多JSON API规范和标准的扩展和支持例如[swagger](http://swagger.io/)[RAML](http://raml.org/)和[jsonapi](http://jsonapi.org/)。
### 1.2 Swagger
Swagger是一个规范和完整的框架用于生成、描述、调用和可视化 RESTful 风格的 Web 服务支持从整个API生命周期从设计和文档到测试和部署的开发。
swagger 是一个综合的开源项目,包含[swagger-core](https://github.com/swagger-api/swagger-core)、[swagger-ui](https://github.com/swagger-api/swagger-ui)、[swagger-codegen](https://github.com/swagger-api/swagger-codegen)、[swagger-editor](https://github.com/swagger-api/swagger-editor)等多个子项目。
+ **swagger-core**Swagger CoreOpenAPI规范以前称为Swagger规范的**Java实现**。
+ **swagger-ui**依据可视化文档提供与API资源的可视化交互。
+ **swagger-codegen**:开源的代码生成器,根据Swagger定义的RESTful API可以自动建立服务端和客户端的连接。
+ **swagger-editor**开源的api文档编辑器。
下图为swagger-ui 提供的文档可视化界面示例:
<div align="center"> <img src="https://github.com/heibaiying/spring-samples-for-all/blob/master/pictures/Swagger_UI.png"/> </div>
### 1.3 OpenApi、Swagger、Springfox的关系
**Swagger Core 是 OpenApi 规范以前称为Swagger规范的Java 实现,而 Springfox 提供 Swagger 与 spring 的集成支持**<br/>
## 二、spring boot 集成 swagger 2.0
### 2.1 导入项目相关依赖
```xml
<!--swagger2-->
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger2</artifactId>
<version>2.9.2</version>
</dependency>
<!--swagger-ui -->
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger-ui</artifactId>
<version>2.9.2</version>
</dependency>
```
### 2.2 进行swagger个性化配置、并用@EnableSwagger2开启Swagger支持
这里需要说明的是swagger虽然是一个非常直观易用的接口调试插件但是有可能导致接口信息泄露的危险所以建议在开发环境和测试环境开启在生产环境关闭。这里一共给出三种Swagger开关切换的方法
1. 如下面代码所示在配置文件中配置自定义的开关参数并在创建Docket时候在链式调用的enable()方法中传入;
2.`SwaggerConfig`配置类上添加`@Profile({"dev","test"}) `注解指明在开发环境和测试环境下激活此配置类打包或者部署时候使用spring.profiles.active指明环境即可
3. 在配置文件中配置自定义的开关参数,并在`SwaggerConfig`配置类上添加`@ConditionalOnProperty(name = "swagger.enable", havingValue = "true") `,指明配置类的生效条件
注:@ConditionalOnProperty 注解说明
@ConditionalOnProperty注解能够控制某个@configuration修饰的配置类是否生效。具体操作是通过namehavingValue属性来实现name对应application.propertiesyml中的某个属性值如果该值为空则返回false如果值不为空则将该值与havingValue指定的值进行比较如果一样则返回true否则返回false。如果返回值为false则该configuration不生效为true则生效。
```java
/**
* @author : heibaiying
* @description : Swagger 配置类
*/
@Configuration
@EnableSwagger2
public class SwaggerConfig {
@Value("${swagger.enable}")
private boolean swaggerEnable;
/***
* 配置swagger
* 开发和测试环境下可以开启swagger辅助进行调试,而生产环境下可以关闭或者进行相应的权限控制,防止接口信息泄露
*/
@Bean
public Docket createRestApi() {
return new Docket(DocumentationType.SWAGGER_2)
.enable(swaggerEnable)
.apiInfo(apiInfo())
.select()
.apis(RequestHandlerSelectors.basePackage("com.heibaiying.springboot.controller"))
.paths(PathSelectors.any())
.paths(doFilteringRules())
.build();
}
/***
* 接口文档的描述信息
*/
private ApiInfo apiInfo() {
return new ApiInfoBuilder()
.title("spring boot swagger2 用例")
.description("描述")
.licenseUrl("https://mit-license.org/")
.version("1.0")
.build();
}
/**
* 可以使用正则定义url过滤规则
*/
private Predicate<String> doFilteringRules() {
return not(
regex("/ignore/*")
);
}
}
```
application.properties :
```properties
#swagger启用开关
swagger.enable = true
```
### 2.3 swagger注解的使用和说明
```java
@Slf4j
@Api(value = "产品接口", description = "产品信息接口")
@RestController
public class ProductController {
/***
* 一个标准的swagger注解
*/
@ApiOperation(notes = "查询所有产品", value = "产品查询接口")
@ApiImplicitParams(
@ApiImplicitParam(name = "id", value = "产品编号", paramType = "path", defaultValue = "1")
)
@ApiResponses(value = {
@ApiResponse(code = 200, message = "请求成功"),
@ApiResponse(code = 400, message = "无效的请求"),
@ApiResponse(code = 401, message = "未经过授权认证"),
@ApiResponse(code = 403, message = "已经过授权认证,但是没有该资源对应的访问权限"),
@ApiResponse(code = 404, message = "服务器找不到给定的资源,商品不存在"),
@ApiResponse(code = 500, message = "服务器错误")
})
@GetMapping(value = "/product/{id}", produces = "application/json")
public ResponseEntity<Product> getProduct(@PathVariable long id) {
Product product = new Product(id, "product" + id, new Date());
return ResponseEntity.ok(product);
}
/***
* 如果用实体类接收参数,则用实体类对应的属性名称指定参数
*/
@ApiOperation(notes = "保存产品", value = "产品保存接口")
@ApiImplicitParams({
@ApiImplicitParam(name = "id", value = "产品编号", paramType = "body", defaultValue = "1"),
@ApiImplicitParam(name = "name", value = "产品名称", paramType = "body"),
@ApiImplicitParam(name = "date", value = "产品生产日期", paramType = "body")
}
)
@PostMapping(value = "/product")
public ResponseEntity<Void> saveProduct(@RequestBody Product product) {
System.out.println(product);
return ResponseEntity.ok().build();
}
/***
* 在配置类中指明了该接口不被扫描到,可以在配置类中使用正则指定某一类符合规则的接口不被扫描到
*/
@ApiOperation(notes = "该接口会被忽略", value = "产品保存接口")
@PostMapping(value = "/ignore")
public ResponseEntity<Product> ignore() {
return ResponseEntity.ok().build();
}
/**
* 不加上任何swagger相关的注解也会被扫描到 如果不希望被扫描到,需要用 @ApiIgnore 修饰
*/
@PostMapping(value = "/normal")
public ResponseEntity<Void> normal() {
return ResponseEntity.ok().build();
}
@ApiIgnore
@PostMapping(value = "/apiIgnore")
public ResponseEntity<Void> apiIgnore() {
return ResponseEntity.ok().build();
}
}
```
swagger 为了最大程度防止对逻辑代码的侵入,基本都是依靠注解来完成文档描述。常用注解如下:
| Annotation | Attribute | Target Property | Description |
| ---------------- | ------------ | ------------------------- | ------------------------------------------------------------ |
| RequestHeader | defaultValue | Parameter#defaultValue | e.g. `@RequestHeader(defaultValue="${param1.defaultValue}")` |
| ApiModelProperty | value | ModelProperty#description | e.g. `@ApiModelProperty(value="${property1.description}")` |
| ApiModelProperty | description | ModelProperty#description | e.g. `@ApiModelProperty(notes="${property1.description}")` |
| ApiParam | value | Parameter#description | e.g. `@ApiParam(value="${param1.description}")` |
| ApiImplicitParam | value | Parameter#description | e.g. `@ApiImplicitParam(value="${param1.description}")` |
| ApiOperation | notes | Operation#notes | e.g. `@ApiOperation(notes="${operation1.description}")` |
| ApiOperation | summary | Operation#summary | e.g. `@ApiOperation(value="${operation1.summary}")` |
| RequestParam | defaultValue | Parameter#defaultValue | e.g. `@RequestParam(defaultValue="${param1.defaultValue}")` |
1. `@Api`Api 用在类上,说明该类的作用;
2. `@ApiOperation`:用在方法上,说明方法的作用;
3. `@ApiParam`:用在参数上,说明参数的作用;
4. `@ApiImplicitParams`:用在方法上说明方法参数的作用;
5. `@ApiImplicitParam`:用在@ApiImplicitParams注解中,描述每个具体参数;
6. `@ApiResponses`:一组@ApiResponse的配置
7. `@ApiResponse`:请求返回的配置;
8. `@ResponseHeader`:响应头的配置;
9. `@ApiModel`描述一个Model的信息一般用在post创建的时候使用@RequestBody接收参数的场景
10. `@ApiModelProperty`描述model的属性。
11. `@ApiIgnore`可以用于类、方法、属性代表该方法、类、属性不被swagger的文档所管理。
### 2.4 swagger-ui 可视化接口文档
接口文档访问地址http://localhost:8080/swagger-ui.html ,文档主界面如下:
<div align="center"> <img src="https://github.com/heibaiying/spring-samples-for-all/blob/master/pictures/swagger-ui-index.png"/> </div>
### 2.5 利用swagger-ui进行接口测试
​点击对应接口的`try it out`按钮可以进行接口测试,此时可以输入对应的参数的值,然后点击下方的`Execute`按钮发送请求。<div align="center"> <img src="https://github.com/heibaiying/spring-samples-for-all/blob/master/pictures/swagger-try-it.png"/> </div>
<div align="center"> <img src="https://github.com/heibaiying/spring-samples-for-all/blob/master/pictures/swagger-execute.png"/> </div>
post方法可以直接修改model 对应的 json数据 ,然后点击下方的`Execute`按钮发送请求。
<div align="center"> <img src="https://github.com/heibaiying/spring-samples-for-all/blob/master/pictures/swagger-post-try.png"/> </div>
# spring-boot 集成 Swagger2 打造在线接口文档
## 目录<br/>
<a href="#一Springfox-与-Swagger-简介">一、Springfox 与 Swagger 简介</a><br/>
&nbsp;&nbsp;&nbsp;&nbsp;<a href="#11-Springfox">1.1 Springfox</a><br/>
&nbsp;&nbsp;&nbsp;&nbsp;<a href="#12-Swagger">1.2 Swagger</a><br/>
&nbsp;&nbsp;&nbsp;&nbsp;<a href="#13-OpenApiSwaggerSpringfox的关系">1.3 OpenApi、Swagger、Springfox的关系</a><br/>
<a href="#二spring-boot-集成-swagger-20">二、spring boot 集成 swagger 2.0</a><br/>
&nbsp;&nbsp;&nbsp;&nbsp;<a href="#21-导入项目相关依赖">2.1 导入项目相关依赖</a><br/>
&nbsp;&nbsp;&nbsp;&nbsp;<a href="#22--进行swagger个性化配置并用EnableSwagger2开启Swagger支持">2.2 进行swagger个性化配置、并用@EnableSwagger2开启Swagger支持</a><br/>
&nbsp;&nbsp;&nbsp;&nbsp;<a href="#23-swagger注解的使用和说明">2.3 swagger注解的使用和说明</a><br/>
&nbsp;&nbsp;&nbsp;&nbsp;<a href="#24-swagger-ui-可视化接口文档">2.4 swagger-ui 可视化接口文档</a><br/>
&nbsp;&nbsp;&nbsp;&nbsp;<a href="#25-利用swagger-ui进行接口测试">2.5 利用swagger-ui进行接口测试</a><br/>
## 正文<br/>
## 一、Springfox 与 Swagger 简介
### 1.1 Springfox
Springfox 是一个开源的 API Doc 的框架, 它的前身是 swagger-springmvc能够完美的支持 springmvc可以将 spring 接口方法自动转换为接口文档。 目前 spring fox 正致力于对更多 JSON API 规范和标准的扩展和支持,例如:[swagger](http://swagger.io/)[RAML](http://raml.org/) 和[jsonapi](http://jsonapi.org/)。
### 1.2 Swagger
Swagger 是一个规范和完整的框架,用于生成、描述、调用和可视化 RESTful 风格的 Web 服务,支持从整个 API 生命周期(从设计和文档到测试和部署)的开发。
swagger 是一个综合的开源项目,包含[swagger-core](https://github.com/swagger-api/swagger-core)、[swagger-ui](https://github.com/swagger-api/swagger-ui)、[swagger-codegen](https://github.com/swagger-api/swagger-codegen)、[swagger-editor](https://github.com/swagger-api/swagger-editor) 等多个子项目。
+ **swagger-core**Swagger CoreOpenAPI 规范(以前称为 Swagger 规范)的**Java 实现**。
+ **swagger-ui**:依据可视化文档,提供与 API 资源的可视化交互。
+ **swagger-codegen**:开源的代码生成器,根据 Swagger 定义的 RESTful API 可以自动建立服务端和客户端的连接。
+ **swagger-editor**:开源的 api 文档编辑器。
下图为 swagger-ui 提供的文档可视化界面示例:
<div align="center"> <img src="https://github.com/heibaiying/spring-samples-for-all/blob/master/pictures/Swagger_UI.png"/> </div>
### 1.3 OpenApi、Swagger、Springfox的关系
**Swagger Core 是 OpenApi 规范(以前称为 Swagger 规范)的 Java 实现,而 Springfox 提供 Swagger 与 spring 的集成支持**<br/>
## 二、spring boot 集成 swagger 2.0
### 2.1 导入项目相关依赖
```xml
<!--swagger2-->
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger2</artifactId>
<version>2.9.2</version>
</dependency>
<!--swagger-ui -->
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger-ui</artifactId>
<version>2.9.2</version>
</dependency>
```
### 2.2 进行swagger个性化配置、并用@EnableSwagger2开启Swagger支持
这里需要说明的是 swagger 虽然是一个非常直观易用的接口调试插件,但是有可能导致接口信息泄露的危险,所以建议在开发环境和测试环境开启,在生产环境关闭。这里一共给出三种 Swagger 开关切换的方法:
1. 如下面代码所示,在配置文件中配置自定义的开关参数,并在创建 Docket 时候,在链式调用的 enable() 方法中传入;
2. `SwaggerConfig` 配置类上添加 `@Profile({"dev","test"}) ` 注解,指明在开发环境和测试环境下激活此配置类,打包或者部署时候使用 spring.profiles.active 指明环境即可;
3. 在配置文件中配置自定义的开关参数,并在 `SwaggerConfig` 配置类上添加 `@ConditionalOnProperty(name = "swagger.enable", havingValue = "true") `,指明配置类的生效条件
注:@ConditionalOnProperty 注解说明
@ConditionalOnProperty 注解能够控制某个@configuration 修饰的配置类是否生效。具体操作是通过 namehavingValue 属性来实现name 对应 application.propertiesyml中的某个属性值如果该值为空则返回 false如果值不为空则将该值与 havingValue 指定的值进行比较,如果一样则返回 true否则返回 false。如果返回值为 false则该 configuration 不生效;为 true 则生效。
```java
/**
* @author : heibaiying
* @description : Swagger 配置类
*/
@Configuration
@EnableSwagger2
public class SwaggerConfig {
@Value("${swagger.enable}")
private boolean swaggerEnable;
/***
* 配置 swagger
* 开发和测试环境下可以开启 swagger 辅助进行调试,而生产环境下可以关闭或者进行相应的权限控制,防止接口信息泄露
*/
@Bean
public Docket createRestApi() {
return new Docket(DocumentationType.SWAGGER_2)
.enable(swaggerEnable)
.apiInfo(apiInfo())
.select()
.apis(RequestHandlerSelectors.basePackage("com.heibaiying.springboot.controller"))
.paths(PathSelectors.any())
.paths(doFilteringRules())
.build();
}
/***
* 接口文档的描述信息
*/
private ApiInfo apiInfo() {
return new ApiInfoBuilder()
.title("spring boot swagger2 用例")
.description("描述")
.licenseUrl("https://mit-license.org/")
.version("1.0")
.build();
}
/**
* 可以使用正则定义 url 过滤规则
*/
private Predicate<String> doFilteringRules() {
return not(
regex("/ignore/*")
);
}
}
```
application.properties :
```properties
#swagger启用开关
swagger.enable = true
```
### 2.3 swagger注解的使用和说明
```java
@Slf4j
@Api(value = "产品接口", description = "产品信息接口")
@RestController
public class ProductController {
/***
* 一个标准的 swagger 注解
*/
@ApiOperation(notes = "查询所有产品", value = "产品查询接口")
@ApiImplicitParams(
@ApiImplicitParam(name = "id", value = "产品编号", paramType = "path", defaultValue = "1")
)
@ApiResponses(value = {
@ApiResponse(code = 200, message = "请求成功"),
@ApiResponse(code = 400, message = "无效的请求"),
@ApiResponse(code = 401, message = "未经过授权认证"),
@ApiResponse(code = 403, message = "已经过授权认证,但是没有该资源对应的访问权限"),
@ApiResponse(code = 404, message = "服务器找不到给定的资源,商品不存在"),
@ApiResponse(code = 500, message = "服务器错误")
})
@GetMapping(value = "/product/{id}", produces = "application/json")
public ResponseEntity<Product> getProduct(@PathVariable long id) {
Product product = new Product(id, "product" + id, new Date());
return ResponseEntity.ok(product);
}
/***
* 如果用实体类接收参数,则用实体类对应的属性名称指定参数
*/
@ApiOperation(notes = "保存产品", value = "产品保存接口")
@ApiImplicitParams({
@ApiImplicitParam(name = "id", value = "产品编号", paramType = "body", defaultValue = "1"),
@ApiImplicitParam(name = "name", value = "产品名称", paramType = "body"),
@ApiImplicitParam(name = "date", value = "产品生产日期", paramType = "body")
}
)
@PostMapping(value = "/product")
public ResponseEntity<Void> saveProduct(@RequestBody Product product) {
System.out.println(product);
return ResponseEntity.ok().build();
}
/***
* 在配置类中指明了该接口不被扫描到,可以在配置类中使用正则指定某一类符合规则的接口不被扫描到
*/
@ApiOperation(notes = "该接口会被忽略", value = "产品保存接口")
@PostMapping(value = "/ignore")
public ResponseEntity<Product> ignore() {
return ResponseEntity.ok().build();
}
/**
* 不加上任何 swagger 相关的注解也会被扫描到 如果不希望被扫描到,需要用 @ApiIgnore 修饰
*/
@PostMapping(value = "/normal")
public ResponseEntity<Void> normal() {
return ResponseEntity.ok().build();
}
@ApiIgnore
@PostMapping(value = "/apiIgnore")
public ResponseEntity<Void> apiIgnore() {
return ResponseEntity.ok().build();
}
}
```
swagger 为了最大程度防止对逻辑代码的侵入,基本都是依靠注解来完成文档描述。常用注解如下:
| Annotation | Attribute | Target Property | Description |
| ---------------- | ------------ | ------------------------- | ------------------------------------------------------------ |
| RequestHeader | defaultValue | Parameter#defaultValue | e.g. `@RequestHeader(defaultValue="${param1.defaultValue}")` |
| ApiModelProperty | value | ModelProperty#description | e.g. `@ApiModelProperty(value="${property1.description}")` |
| ApiModelProperty | description | ModelProperty#description | e.g. `@ApiModelProperty(notes="${property1.description}")` |
| ApiParam | value | Parameter#description | e.g. `@ApiParam(value="${param1.description}")` |
| ApiImplicitParam | value | Parameter#description | e.g. `@ApiImplicitParam(value="${param1.description}")` |
| ApiOperation | notes | Operation#notes | e.g. `@ApiOperation(notes="${operation1.description}")` |
| ApiOperation | summary | Operation#summary | e.g. `@ApiOperation(value="${operation1.summary}")` |
| RequestParam | defaultValue | Parameter#defaultValue | e.g. `@RequestParam(defaultValue="${param1.defaultValue}")` |
1. `@Api`Api 用在类上,说明该类的作用;
2. `@ApiOperation`:用在方法上,说明方法的作用;
3. `@ApiParam`:用在参数上,说明参数的作用;
4. `@ApiImplicitParams`:用在方法上说明方法参数的作用;
5. `@ApiImplicitParam`:用在@ApiImplicitParams 注解中,描述每个具体参数;
6. `@ApiResponses`:一组@ApiResponse 的配置;
7. `@ApiResponse`:请求返回的配置;
8. `@ResponseHeader`:响应头的配置;
9. `@ApiModel`:描述一个 Model 的信息(一般用在 post 创建的时候,使用@RequestBody 接收参数的场景);
10. `@ApiModelProperty`:描述 model 的属性。
11. `@ApiIgnore`:可以用于类、方法、属性,代表该方法、类、属性不被 swagger 的文档所管理。
### 2.4 swagger-ui 可视化接口文档
接口文档访问地址http://localhost:8080/swagger-ui.html ,文档主界面如下:
<div align="center"> <img src="https://github.com/heibaiying/spring-samples-for-all/blob/master/pictures/swagger-ui-index.png"/> </div>
### 2.5 利用swagger-ui进行接口测试
​点击对应接口的 `try it out` 按钮可以进行接口测试,此时可以输入对应的参数的值,然后点击下方的 `Execute` 按钮发送请求。<div align="center"> <img src="https://github.com/heibaiying/spring-samples-for-all/blob/master/pictures/swagger-try-it.png"/> </div>
<div align="center"> <img src="https://github.com/heibaiying/spring-samples-for-all/blob/master/pictures/swagger-execute.png"/> </div>
post 方法可以直接修改 model 对应的 json 数据 ,然后点击下方的 `Execute` 按钮发送请求。
<div align="center"> <img src="https://github.com/heibaiying/spring-samples-for-all/blob/master/pictures/swagger-post-try.png"/> </div>

View File

@ -1,5 +1,6 @@
# spring boot 整合 tomcat
# spring boot 整合 tomcat
## 目录<br/>
<a href="#一说明">一、说明</a><br/>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href="#11-项目结构说明">1.1 项目结构说明</a><br/>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href="#12-项目主要依赖">1.2 项目主要依赖</a><br/>
@ -11,102 +12,102 @@
## 一、说明
#### 1.1 项目结构说明
spring boot 整合 tomcat 后支持jsp 的使用内置容器默认是不支持jsp所以项目整合后采用jspController 跳转到show.jsp测试整合是否成功。
<div align="center"> <img src="https://github.com/heibaiying/spring-samples-for-all/blob/master/pictures/spring-boot-tomcat.png"/> </div>
#### 1.2 项目主要依赖
```xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<!--排除内置容器依赖 使用外部tomcat容器启动-->
<exclusions>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-tomcat</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<!--使用外置容器时候SpringBootServletInitializer 依赖此包 -->
<groupId>javax.servlet</groupId>
<artifactId>servlet-api</artifactId>
<version>2.5</version>
<scope>provided</scope>
</dependency>
```
## 二、整合 tomcat
#### 2.1 修改启动类继承自SpringBootServletInitializer并覆盖重写其中configure方法
```java
/**
* 如果用外置tomcat,启动报错java.lang.NoClassDefFoundError: javax/el/ELManager
* 是因为tomcat 7.0 el-api包中没有ELManager类 , 切换tomcat 为8.0 以上版本即可
*/
@SpringBootApplication
public class SpringBootTomcatApplication extends SpringBootServletInitializer {
@Override
protected SpringApplicationBuilder configure(SpringApplicationBuilder application) {
//传入SpringBoot应用的主程序
return application.sources(SpringBootTomcatApplication.class);
}
public static void main(String[] args) {
SpringApplication.run(SpringBootTomcatApplication.class, args);
}
}
```
#### 2.2 在application.yml 中指定访问视图文件的前缀和后缀
```yml
spring:
mvc:
view:
prefix: /WEB-INF/jsp/
suffix: .jsp
```
#### 2.3 新建controller和show.jsp 测试整合是否成功
```java
@Controller
@RequestMapping("index")
public class JspController {
@RequestMapping
public String jsp(Model model){
Programmer programmer = new Programmer("heibai", 21, 1298.31f, LocalDate.now());
model.addAttribute("programmer",programmer);
return "show";
}
}
```
```jsp
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
<title>programmer</title>
<link rel="stylesheet" href="${pageContext.request.contextPath}/css/show.css">
</head>
<body>
<ul>
<li>姓名: ${programmer.name}</li>
<li>年龄: ${programmer.age}</li>
</ul>
</body>
</html>
```
## 一、说明
#### 1.1 项目结构说明
spring boot 整合 tomcat 后支持 jsp 的使用(内置容器默认是不支持 jsp所以项目整合后采用 jspController 跳转到 show.jsp 测试整合是否成功。
<div align="center"> <img src="https://github.com/heibaiying/spring-samples-for-all/blob/master/pictures/spring-boot-tomcat.png"/> </div>
#### 1.2 项目主要依赖
```xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<!--排除内置容器依赖 使用外部 tomcat 容器启动-->
<exclusions>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-tomcat</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<!--使用外置容器时候 SpringBootServletInitializer 依赖此包 -->
<groupId>javax.servlet</groupId>
<artifactId>servlet-api</artifactId>
<version>2.5</version>
<scope>provided</scope>
</dependency>
```
## 二、整合 tomcat
#### 2.1 修改启动类继承自SpringBootServletInitializer并覆盖重写其中configure方法
```java
/**
* 如果用外置 tomcat,启动报错 java.lang.NoClassDefFoundError: javax/el/ELManager
* 是因为 tomcat 7.0 el-api 包中没有 ELManager 类 , 切换 tomcat 为 8.0 以上版本即可
*/
@SpringBootApplication
public class SpringBootTomcatApplication extends SpringBootServletInitializer {
@Override
protected SpringApplicationBuilder configure(SpringApplicationBuilder application) {
//传入 SpringBoot 应用的主程序
return application.sources(SpringBootTomcatApplication.class);
}
public static void main(String[] args) {
SpringApplication.run(SpringBootTomcatApplication.class, args);
}
}
```
#### 2.2 在application.yml 中指定访问视图文件的前缀和后缀
```yml
spring:
mvc:
view:
prefix: /WEB-INF/jsp/
suffix: .jsp
```
#### 2.3 新建controller和show.jsp 测试整合是否成功
```java
@Controller
@RequestMapping("index")
public class JspController {
@RequestMapping
public String jsp(Model model){
Programmer programmer = new Programmer("heibai", 21, 1298.31f, LocalDate.now());
model.addAttribute("programmer",programmer);
return "show";
}
}
```
```jsp
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
<title>programmer</title>
<link rel="stylesheet" href="${pageContext.request.contextPath}/css/show.css">
</head>
<body>
<ul>
<li>姓名: ${programmer.name}</li>
<li>年龄: ${programmer.age}</li>
</ul>
</body>
</html>
```

View File

@ -1,196 +1,196 @@
# spring boot websocket
## 目录<br/>
<a href="#一说明">一、说明</a><br/>
&nbsp;&nbsp;&nbsp;&nbsp;<a href="#11-项目结构说明">1.1 项目结构说明</a><br/>
&nbsp;&nbsp;&nbsp;&nbsp;<a href="#12-主要依赖">1.2 主要依赖</a><br/>
<a href="#二spring-boot-websocket">二、spring boot websocket</a><br/>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href="#21-创建消息处理类ChatSocket使用ServerEndpoint声明websocket服务">2.1 创建消息处理类ChatSocket使用@ServerEndpoint声明websocket服务</a><br/>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href="#22-配置ServerEndpointExporterServerEndpointExporter会在运行时候自动注册我们用ServerEndpoint声明的websocket服务">2.2 配置ServerEndpointExporterServerEndpointExporter会在运行时候自动注册我们用@ServerEndpoint声明的websocket服务</a><br/>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href="#23-前端websocket的实现">2.3 前端websocket的实现</a><br/>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href="#24-简单登录的实现">2.4 简单登录的实现</a><br/>
## 正文<br/>
## 一、说明
### 1.1 项目结构说明
1. 项目模拟一个简单的群聊功能为区分不同的聊天客户端登录时候将临时用户名存储在session当中
2. 关于websocket的主要配置在websocket文件夹下
3. 模板引擎采用freemaker
4. 项目以web的方式构建。
<div align="center"> <img src="https://github.com/heibaiying/spring-samples-for-all/blob/master/pictures/spring-boot-websocket.png"/> </div>
### 1.2 主要依赖
```xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!--spring boot webSocket 的依赖包 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-freemarker</artifactId>
</dependency>
```
## 二、spring boot websocket
#### 2.1 创建消息处理类ChatSocket使用@ServerEndpoint声明websocket服务
```java
@ServerEndpoint(value = "/socket/{username}")
@Component
public class ChatSocket {
/**
* 建立连接时候触发
*/
@OnOpen
public void onOpen(Session session, @PathParam("username") String username) {
// 这个方法是线程不安全的
Constant.nameAndSession.putIfAbsent(username, session);
}
/**
* 关闭连接时候触发
*/
@OnClose
public void onClose(Session session, @PathParam("username") String username) {
Constant.nameAndSession.remove(username);
}
/**
* 处理消息
*/
@OnMessage
public void onMessage(Session session, String message, @PathParam("username") String username) throws UnsupportedEncodingException {
// 防止中文乱码
String msg = URLDecoder.decode(message, "utf-8");
// 简单模拟群发消息
Constant.nameAndSession.forEach((s, webSocketSession)
-> {
try {
webSocketSession.getBasicRemote().sendText(username + " : " + msg);
} catch (IOException e) {
e.printStackTrace();
}
});
}
}
```
#### 2.2 配置ServerEndpointExporterServerEndpointExporter会在运行时候自动注册我们用@ServerEndpoint声明的websocket服务。
```java
@Configuration
public class WebSocketConfig {
/***
* 检测{@link javax.websocket.server.ServerEndpointConfig}和{@link ServerEndpoint} 类型的bean
* 并在运行时使用标准Java WebSocket时注册。
* 我们在{@link com.heibaiying.springboot.websocket.WebSocketConfig}中就是使用@ServerEndpoint去声明websocket服务
*/
@Bean
public ServerEndpointExporter serverEndpointExporter() {
return new ServerEndpointExporter();
}
}
```
#### 2.3 前端websocket的实现
```jsp
<!doctype html>
<html lang="en">
<head>
<title>${Session["username"]}您好!欢迎进入群聊大厅!</title>
</head>
<body>
<h5>${Session["username"]}您好!欢迎进入群聊大厅!</h5>
<input id="message" type="text">
<button id="btn">发送消息</button>
<div id="show">
</div>
<script>
let btn = document.getElementById("btn");
let message = document.getElementById("message");
let show = document.getElementById("show");
let ws = new WebSocket("ws://localhost:8080/socket/${Session["username"]}");
ws.onmessage = function (evt) {
let node = document.createElement("div");
node.innerHTML = "<h5>" + evt.data + "</h5>";
show.appendChild(node);
};
btn.addEventListener("click", function () {
let data = message.value;
console.log(data);
if (data) {
ws.send(encodeURI(data));
} else {
alert("请输入消息后发送");
}
message.value = "";
});
// 关闭页面时候关闭ws
window.addEventListener("beforeunload", function (event) {
ws.close();
});
</script>
</body>
</html>
```
#### 2.4 简单登录的实现
```java
<!doctype html>
<html lang="en">
<head>
<title>Title</title>
</head>
<body>
<form action="/login" method="post">
<input name="username" type="text">
<button id="btn">输入临时用户名后登录</button>
</form>
</body>
</html>
```
```java
/**
* @description : 简单登录
*/
@Controller
public class LoginController {
@PostMapping("login")
public String login(String username, HttpSession session) {
session.setAttribute(Constant.USER_NAME, username);
return "chat";
}
@GetMapping
public String index() {
return "index";
}
}
```
# spring boot websocket
## 目录<br/>
<a href="#一说明">一、说明</a><br/>
&nbsp;&nbsp;&nbsp;&nbsp;<a href="#11-项目结构说明">1.1 项目结构说明</a><br/>
&nbsp;&nbsp;&nbsp;&nbsp;<a href="#12-主要依赖">1.2 主要依赖</a><br/>
<a href="#二spring-boot-websocket">二、spring boot websocket</a><br/>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href="#21-创建消息处理类ChatSocket使用ServerEndpoint声明websocket服务">2.1 创建消息处理类ChatSocket使用@ServerEndpoint声明websocket服务</a><br/>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href="#22-配置ServerEndpointExporterServerEndpointExporter会在运行时候自动注册我们用ServerEndpoint声明的websocket服务">2.2 配置ServerEndpointExporterServerEndpointExporter会在运行时候自动注册我们用@ServerEndpoint声明的websocket服务</a><br/>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href="#23-前端websocket的实现">2.3 前端websocket的实现</a><br/>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href="#24-简单登录的实现">2.4 简单登录的实现</a><br/>
## 正文<br/>
## 一、说明
### 1.1 项目结构说明
1. 项目模拟一个简单的群聊功能,为区分不同的聊天客户端,登录时候将临时用户名存储在 session 当中;
2. 关于 websocket 的主要配置在 websocket 文件夹下;
3. 模板引擎采用 freemaker
4. 项目以 web 的方式构建。
<div align="center"> <img src="https://github.com/heibaiying/spring-samples-for-all/blob/master/pictures/spring-boot-websocket.png"/> </div>
### 1.2 主要依赖
```xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!--spring boot webSocket 的依赖包 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-freemarker</artifactId>
</dependency>
```
## 二、spring boot websocket
#### 2.1 创建消息处理类ChatSocket使用@ServerEndpoint声明websocket服务
```java
@ServerEndpoint(value = "/socket/{username}")
@Component
public class ChatSocket {
/**
* 建立连接时候触发
*/
@OnOpen
public void onOpen(Session session, @PathParam("username") String username) {
// 这个方法是线程不安全的
Constant.nameAndSession.putIfAbsent(username, session);
}
/**
* 关闭连接时候触发
*/
@OnClose
public void onClose(Session session, @PathParam("username") String username) {
Constant.nameAndSession.remove(username);
}
/**
* 处理消息
*/
@OnMessage
public void onMessage(Session session, String message, @PathParam("username") String username) throws UnsupportedEncodingException {
// 防止中文乱码
String msg = URLDecoder.decode(message, "utf-8");
// 简单模拟群发消息
Constant.nameAndSession.forEach((s, webSocketSession)
-> {
try {
webSocketSession.getBasicRemote().sendText(username + " : " + msg);
} catch (IOException e) {
e.printStackTrace();
}
});
}
}
```
#### 2.2 配置ServerEndpointExporterServerEndpointExporter会在运行时候自动注册我们用@ServerEndpoint声明的websocket服务。
```java
@Configuration
public class WebSocketConfig {
/***
* 检测{@link javax.websocket.server.ServerEndpointConfig}和{@link ServerEndpoint} 类型的 bean
* 并在运行时使用标准 Java WebSocket 时注册。
* 我们在{@link com.heibaiying.springboot.websocket.WebSocketConfig}中就是使用@ServerEndpoint 去声明 websocket 服务
*/
@Bean
public ServerEndpointExporter serverEndpointExporter() {
return new ServerEndpointExporter();
}
}
```
#### 2.3 前端websocket的实现
```jsp
<!doctype html>
<html lang="en">
<head>
<title>${Session["username"]}您好!欢迎进入群聊大厅!</title>
</head>
<body>
<h5>${Session["username"]}您好!欢迎进入群聊大厅!</h5>
<input id="message" type="text">
<button id="btn">发送消息</button>
<div id="show">
</div>
<script>
let btn = document.getElementById("btn");
let message = document.getElementById("message");
let show = document.getElementById("show");
let ws = new WebSocket("ws://localhost:8080/socket/${Session["username"]}");
ws.onmessage = function (evt) {
let node = document.createElement("div");
node.innerHTML = "<h5>" + evt.data + "</h5>";
show.appendChild(node);
};
btn.addEventListener("click", function () {
let data = message.value;
console.log(data);
if (data) {
ws.send(encodeURI(data));
} else {
alert("请输入消息后发送");
}
message.value = "";
});
// 关闭页面时候关闭 ws
window.addEventListener("beforeunload", function (event) {
ws.close();
});
</script>
</body>
</html>
```
#### 2.4 简单登录的实现
```java
<!doctype html>
<html lang="en">
<head>
<title>Title</title>
</head>
<body>
<form action="/login" method="post">
<input name="username" type="text">
<button id="btn">输入临时用户名后登录</button>
</form>
</body>
</html>
```
```java
/**
* @description : 简单登录
*/
@Controller
public class LoginController {
@PostMapping("login")
public String login(String username, HttpSession session) {
session.setAttribute(Constant.USER_NAME, username);
return "chat";
}
@GetMapping
public String index() {
return "index";
}
}
```

View File

@ -1,5 +1,6 @@
# spring-boot-yml-profile
# spring-boot-yml-profile
## 目录<br/>
<a href="#一项目结构">一、项目结构</a><br/>
<a href="#二常用-yaml-语法讲解">二、常用 yaml 语法讲解</a><br/>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href="#21-基本规则">2.1 基本规则</a><br/>
@ -16,178 +17,178 @@
<a href="#六优先级的说明">六、优先级的说明</a><br/>
## 正文<br/>
## 一、项目结构
<div align="center"> <img src="https://github.com/heibaiying/spring-samples-for-all/blob/master/pictures/spring-boot-yml-profile.png"/> </div>
## 二、常用 yaml 语法讲解
项目中的yml配置文件如下
```yaml
programmer:
name: xiaoming-DEV
married: false
hireDate: 2018/12/23
salary: 66666.88
random: ${random.int[1024,65536]}
skill: {java: master, jquery: proficiency}
company: [baidu,tengxun,alibaba]
school:
name: unviersity
location: shanghai
```
#### 2.1 基本规则
1. 大小写敏感
2. 使用缩进表示层级关系
3. 缩进长度没有限制,只要元素对齐就表示这些元素属于一个层级。
4. 使用#表示注释
5. 字符串默认不用加单双引号,但单引号和双引号都可以使用,双引号不会对特殊字符转义
6. YAML中提供了多种常量结构包括整数浮点数字符串NULL日期布尔时间。
#### 2.2 对象的写法
```yaml
key: value
```
#### 2.3 map的写法
```yaml
# 写法一 同一缩进的所有键值对属于一个map
key:
key1: value1
key2: value2
# 写法二
{key1: value1, key2: value2}
```
#### 2.3 数组的写法
```yaml
# 写法一 使用一个短横线加一个空格代表一个数组项
- a
- b
- c
# 写法二
[a,b,c]
```
#### 2.5 单双引号
单引号和双引号都可以使用,双引号不会对特殊字符转义。
```yaml
s1: '内容\n字符串'
s2: "内容\n字符串"
转换后:
{ s1: '内容\\n字符串', s2: '内容\n字符串' }
```
#### 2.6 特殊符号
--- YAML可以在同一个文件中使用---表示一个文档的开始。
## 三、spring boot 与 yaml
#### 3.1 spring boot 支持使用 ${app.name} 引用预先定义的值
```properties
appName: MyApp
appDescription: ${app.name} is a Spring Boot application
```
#### 3.2 spring boot 支持使用 ${random.xxx} 配置随机值
```properties
my.secret: ${random.value}
my.number: ${random.int}
my.bignumber: ${random.long}
my.number.less.than.ten: ${random.int(10)}
my.number.in.range: ${random.int[1024,65536]}
```
## 四、@ConfigurationProperties实现属性绑定
```java
@Component
@ConfigurationProperties(prefix = "programmer")
@Data
@ToString
public class Programmer {
private String name;
private int age;
private boolean married;
private Date hireDate;
private float salary;
private int random;
private Map<String, String> skill;
private List company;
private School school;
}
```
Spring Boot将环境属性绑定到@ConfigurationProperties beans时会使用一些宽松的规则称之为松散绑定。所以Environment属性名和bean属性名不需要精确匹配。常见的示例中有用的包括虚线分割比如context-path绑定到contextPath将环境属性转为大写字母比如PORT绑定port
需要注意的是`@Value`是不支持松散绑定的,所以建议除非有特殊的需求,否则在`ConfigurationProperties``value` 配置属性的时候最好都保持属性和变量的一致,以免造成不必要的勿扰。
## 五、多配置文件
多配置文件可以在同一个yml中使用 --- 分割为多个配置或者遵循application-xxx.yml 的方式命名拆分为多个文件并在主配置文件application.yml 中确定激活哪个配置文件,当然也可在命令行中确定,命令行的优先级大于配置文件。
<div align="center"> <img src="https://github.com/heibaiying/spring-samples-for-all/blob/master/pictures/profile.png"/> </div>
```yaml
# 配置文件中激活配置
spring:
profiles:
active: dev
```
```shell
# 命令行参数激活配置
--spring.profiles.active=dev
```
## 六、优先级的说明
Spring Boot设计了一个非常特别的PropertySource顺序以允许对属性值进行合理的覆盖属性会以如下的顺序进行设值
1. home目录下的devtools全局设置属性~/.spring-boot-devtools.properties如果devtools激活
2. 测试用例上的@TestPropertySource注解
3. 测试用例上的@SpringBootTest#properties注解
4. 命令行参数
5. 来自SPRING_APPLICATION_JSON的属性环境变量或系统属性中内嵌的内联JSON
6. ServletConfig初始化参数。
7. ServletContext初始化参数
8. 来自于java:comp/env的JNDI属性
9. Java系统属性System.getProperties()
10. 操作系统环境变量
11. RandomValuePropertySource只包含random.*中的属性。
12. 没有打进jar包的Profile-specific应用属性application-{profile}.propertiesYAML变量
13. 打进jar包中的Profile-specific应用属性application-{profile}.propertiesYAML变量
14. 没有打进jar的应用配置application.propertiesYAML变量
15. 打进jar包中的应用配置application.properties和YAML变量
16. @Configuration类上的@PropertySource注解
17. 默认属性使用SpringApplication.setDefaultProperties指定
这里做一下说明上文第12,14 点没有打进jar包的文件指的是在启动时候通过`spring.config.location`参数指定的外部配置文件外部配置文件的优先级应该是大于jar中的配置文件。
对上面的配置中常用的规则可以精简如下:
## 一、项目结构
<div align="center"> <img src="https://github.com/heibaiying/spring-samples-for-all/blob/master/pictures/spring-boot-yml-profile.png"/> </div>
## 二、常用 yaml 语法讲解
项目中的 yml 配置文件如下:
```yaml
programmer:
name: xiaoming-DEV
married: false
hireDate: 2018/12/23
salary: 66666.88
random: ${random.int[1024,65536]}
skill: {java: master, jquery: proficiency}
company: [baidu,tengxun,alibaba]
school:
name: unviersity
location: shanghai
```
#### 2.1 基本规则
1. 大小写敏感
2. 使用缩进表示层级关系
3. 缩进长度没有限制,只要元素对齐就表示这些元素属于一个层级。
4. 使用#表示注释
5. 字符串默认不用加单双引号,但单引号和双引号都可以使用,双引号不会对特殊字符转义。
6. YAML 中提供了多种常量结构包括整数浮点数字符串NULL日期布尔时间
#### 2.2 对象的写法
```yaml
key: value
```
#### 2.3 map的写法
```yaml
# 写法一 同一缩进的所有键值对属于一个map
key:
key1: value1
key2: value2
# 写法二
{key1: value1, key2: value2}
```
#### 2.3 数组的写法
```yaml
# 写法一 使用一个短横线加一个空格代表一个数组项
- a
- b
- c
# 写法二
[a,b,c]
```
#### 2.5 单双引号
单引号和双引号都可以使用,双引号不会对特殊字符转义。
```yaml
s1: '内容\n 字符串'
s2: "内容\n 字符串"
转换后:
{ s1: '内容\\n 字符串', s2: '内容\n 字符串' }
```
#### 2.6 特殊符号
--- YAML 可以在同一个文件中,使用---表示一个文档的开始。
## 三、spring boot 与 yaml
#### 3.1 spring boot 支持使用 ${app.name} 引用预先定义的值
```properties
appName: MyApp
appDescription: ${app.name} is a Spring Boot application
```
#### 3.2 spring boot 支持使用 ${random.xxx} 配置随机值
```properties
my.secret: ${random.value}
my.number: ${random.int}
my.bignumber: ${random.long}
my.number.less.than.ten: ${random.int(10)}
my.number.in.range: ${random.int[1024,65536]}
```
## 四、@ConfigurationProperties实现属性绑定
```java
@Component
@ConfigurationProperties(prefix = "programmer")
@Data
@ToString
public class Programmer {
private String name;
private int age;
private boolean married;
private Date hireDate;
private float salary;
private int random;
private Map<String, String> skill;
private List company;
private School school;
}
```
Spring Boot 将环境属性绑定到@ConfigurationProperties beans 时会使用一些宽松的规则,称之为松散绑定。所以 Environment 属性名和 bean 属性名不需要精确匹配。常见的示例中有用的包括虚线分割比如context-path 绑定到 contextPath将环境属性转为大写字母比如PORT 绑定 port
需要注意的是 `@Value` 是不支持松散绑定的,所以建议除非有特殊的需求,否则在 `ConfigurationProperties``value` 配置属性的时候最好都保持属性和变量的一致,以免造成不必要的勿扰。
## 五、多配置文件
多配置文件可以在同一个 yml 中使用 --- 分割为多个配置,或者遵循 application-xxx.yml 的方式命名拆分为多个文件,并在主配置文件 application.yml 中确定激活哪个配置文件,当然也可在命令行中确定,命令行的优先级大于配置文件。
<div align="center"> <img src="https://github.com/heibaiying/spring-samples-for-all/blob/master/pictures/profile.png"/> </div>
```yaml
# 配置文件中激活配置
spring:
profiles:
active: dev
```
```shell
# 命令行参数激活配置
--spring.profiles.active=dev
```
## 六、优先级的说明
Spring Boot 设计了一个非常特别的 PropertySource 顺序,以允许对属性值进行合理的覆盖,属性会以如下的顺序进行设值:
1. home 目录下的 devtools 全局设置属性(~/.spring-boot-devtools.properties如果 devtools 激活)。
2. 测试用例上的@TestPropertySource 注解
3. 测试用例上的@SpringBootTest#properties 注解。
4. 命令行参数
5. 来自 SPRING_APPLICATION_JSON 的属性(环境变量或系统属性中内嵌的内联 JSON
6. ServletConfig 初始化参数
7. ServletContext 初始化参数。
8. 来自于 java:comp/env 的 JNDI 属性
9. Java 系统属性System.getProperties()
10. 操作系统环境变量
11. RandomValuePropertySource只包含 random.*中的属性
12. 没有打进 jar 包的 Profile-specific 应用属性application-{profile}.properties 和 YAML 变量)
13. 打进 jar 包中的 Profile-specific 应用属性application-{profile}.propertiesYAML 变量)
14. 没有打进 jar 包的应用配置application.propertiesYAML 变量)。
15. 打进 jar 包中的应用配置application.propertiesYAML 变量)。
16. @Configuration 类上的@PropertySource 注解
17. 默认属性(使用 SpringApplication.setDefaultProperties 指定)
这里做一下说明,上文第 12,14 点没有打进 jar 包的文件指的是在启动时候通过 `spring.config.location` 参数指定的外部配置文件,外部配置文件的优先级应该是大于 jar 中的配置文件。
对上面的配置中常用的规则可以精简如下:
**命令行 > application-{profile}.yml > application.yml > 默认属性**

View File

@ -15,7 +15,7 @@
### 1.1 项目说明
本用例基于spring boot + druid + mybatis 配置多数据源,并采用 **JTA 实现分布式事务**
本用例基于 spring boot + druid + mybatis 配置多数据源,并采用 **JTA 实现分布式事务**
### 1.2 项目结构
@ -31,9 +31,9 @@
### 2.1 导入基本依赖
除了mybatis 、durid 等依赖外我们依靠切面来实现动态数据源的切换所以还需要导入aop依赖。
除了 mybatis 、durid 等依赖外,我们依靠切面来实现动态数据源的切换,所以还需要导入 aop 依赖。
最主要的是还要导入spring-boot-starter-jta-atomikosSpring Boot通过[Atomkos](http://www.atomikos.com/)或[Bitronix](http://docs.codehaus.org/display/BTM/Home)的内嵌事务管理器支持跨多个XA资源的分布式JTA事务当发现JTA环境时Spring Boot将使用SpringJtaTransactionManager来管理事务。自动配置的JMSDataSourceJPA beans将被升级以支持XA事务。
最主要的是还要导入 spring-boot-starter-jta-atomikosSpring Boot 通过[Atomkos](http://www.atomikos.com/) 或[Bitronix](http://docs.codehaus.org/display/BTM/Home) 的内嵌事务管理器支持跨多个 XA 资源的分布式 JTA 事务,当发现 JTA 环境时Spring Boot 将使用 SpringJtaTransactionManager 来管理事务。自动配置的 JMSDataSourceJPA beans 将被升级以支持 XA 事务。
```xml
<!--mybatis starter-->
@ -80,7 +80,7 @@
**注意**Spring Boot 2.X 版本不再支持配置继承,多数据源的话每个数据源的所有配置都需要单独配置,否则配置不会生效。
这里我用的本地数据库mysqlmysql02,配置如下:
这里我用的本地数据库 mysqlmysql02,配置如下:
```yml
spring:
@ -92,29 +92,29 @@ spring:
password: root
driver-class-name: com.mysql.cj.jdbc.Driver
# 初始化时建立物理连接的个数。初始化发生在显示调用init方法或者第一次getConnection时
# 初始化时建立物理连接的个数。初始化发生在显示调用 init 方法,或者第一次 getConnection
initialSize: 5
# 最小连接池数量
minIdle: 5
# 最大连接池数量
maxActive: 10
# 获取连接时最大等待时间单位毫秒。配置了maxWait之后缺省启用公平锁并发效率会有所下降如果需要可以通过配置useUnfairLock属性为true使用非公平锁。
# 获取连接时最大等待时间,单位毫秒。配置了 maxWait 之后,缺省启用公平锁,并发效率会有所下降,如果需要可以通过配置 useUnfairLock 属性为 true 使用非公平锁。
maxWait: 60000
# Destroy线程会检测连接的间隔时间如果连接空闲时间大于等于minEvictableIdleTimeMillis则关闭物理连接。
# Destroy 线程会检测连接的间隔时间,如果连接空闲时间大于等于 minEvictableIdleTimeMillis 则关闭物理连接。
timeBetweenEvictionRunsMillis: 60000
# 连接保持空闲而不被驱逐的最小时间
minEvictableIdleTimeMillis: 300000
# 用来检测连接是否有效的sql 因数据库方言而异, 例如 oracle 应该写成 SELECT 1 FROM DUAL
# 用来检测连接是否有效的 sql 因数据库方言而异, 例如 oracle 应该写成 SELECT 1 FROM DUAL
validationQuery: SELECT 1
# 建议配置为true不影响性能并且保证安全性。申请连接的时候检测如果空闲时间大于timeBetweenEvictionRunsMillis执行validationQuery检测连接是否有效。
# 建议配置为 true不影响性能并且保证安全性。申请连接的时候检测如果空闲时间大于 timeBetweenEvictionRunsMillis执行 validationQuery 检测连接是否有效。
testWhileIdle: true
# 申请连接时执行validationQuery检测连接是否有效做了这个配置会降低性能。
# 申请连接时执行 validationQuery 检测连接是否有效,做了这个配置会降低性能。
testOnBorrow: false
# 归还连接时执行validationQuery检测连接是否有效做了这个配置会降低性能。
# 归还连接时执行 validationQuery 检测连接是否有效,做了这个配置会降低性能。
testOnReturn: false
# 是否自动回收超时连接
removeAbandoned: true
# 超时时间(以秒数为单位)
# 超时时间 (以秒数为单位)
remove-abandoned-timeout: 1800
db2:
@ -123,45 +123,45 @@ spring:
password: root
driver-class-name: com.mysql.cj.jdbc.Driver
# 初始化时建立物理连接的个数。初始化发生在显示调用init方法或者第一次getConnection时
# 初始化时建立物理连接的个数。初始化发生在显示调用 init 方法,或者第一次 getConnection
initialSize: 6
# 最小连接池数量
minIdle: 6
# 最大连接池数量
maxActive: 10
# 获取连接时最大等待时间单位毫秒。配置了maxWait之后缺省启用公平锁并发效率会有所下降如果需要可以通过配置useUnfairLock属性为true使用非公平锁。
# 获取连接时最大等待时间,单位毫秒。配置了 maxWait 之后,缺省启用公平锁,并发效率会有所下降,如果需要可以通过配置 useUnfairLock 属性为 true 使用非公平锁。
maxWait: 60000
# Destroy线程会检测连接的间隔时间如果连接空闲时间大于等于minEvictableIdleTimeMillis则关闭物理连接。
# Destroy 线程会检测连接的间隔时间,如果连接空闲时间大于等于 minEvictableIdleTimeMillis 则关闭物理连接。
timeBetweenEvictionRunsMillis: 60000
# 连接保持空闲而不被驱逐的最小时间
minEvictableIdleTimeMillis: 300000
# 用来检测连接是否有效的sql 因数据库方言而异, 例如 oracle 应该写成 SELECT 1 FROM DUAL
# 用来检测连接是否有效的 sql 因数据库方言而异, 例如 oracle 应该写成 SELECT 1 FROM DUAL
validationQuery: SELECT 1
# 建议配置为true不影响性能并且保证安全性。申请连接的时候检测如果空闲时间大于timeBetweenEvictionRunsMillis执行validationQuery检测连接是否有效。
# 建议配置为 true不影响性能并且保证安全性。申请连接的时候检测如果空闲时间大于 timeBetweenEvictionRunsMillis执行 validationQuery 检测连接是否有效。
testWhileIdle: true
# 申请连接时执行validationQuery检测连接是否有效做了这个配置会降低性能。
# 申请连接时执行 validationQuery 检测连接是否有效,做了这个配置会降低性能。
testOnBorrow: false
# 归还连接时执行validationQuery检测连接是否有效做了这个配置会降低性能。
# 归还连接时执行 validationQuery 检测连接是否有效,做了这个配置会降低性能。
testOnReturn: false
# 是否自动回收超时连接
removeAbandoned: true
# 超时时间(以秒数为单位)
# 超时时间 (以秒数为单位)
remove-abandoned-timeout: 1800
# WebStatFilter用于采集web-jdbc关联监控的数据。
# WebStatFilter 用于采集 web-jdbc 关联监控的数据。
web-stat-filter:
# 是否开启 WebStatFilter 默认是true
# 是否开启 WebStatFilter 默认是 true
enabled: true
# 需要拦截的url
# 需要拦截的 url
url-pattern: /*
# 排除静态资源的请求
exclusions: "*.js,*.gif,*.jpg,*.png,*.css,*.ico,/druid/*"
# Druid内置提供了一个StatViewServlet用于展示Druid的统计信息。
# Druid 内置提供了一个 StatViewServlet 用于展示 Druid 的统计信息。
stat-view-servlet:
#是否启用StatViewServlet 默认值true
#是否启用 StatViewServlet 默认值 true
enabled: true
# 需要拦截的url
# 需要拦截的 url
url-pattern: /druid/*
# 允许清空统计数据
reset-enable: true
@ -188,9 +188,9 @@ public class DruidMybatisMultiApplication {
#### 2. 创建多数据源配置类`DataSourceFactory.java`, 手动配置多数据源
+ 这里我们创建druid数据源的时候创建的是`DruidXADataSource`,它继承自`DruidDataSource `并支持XA分布式事务
+ 使用 `AtomikosDataSourceBean` 包装我们创建的`DruidXADataSource`,使得数据源能够被 JTA 事务管理器管理;
+ 这里我们使用的sqlSessionTemplate是我们重写的`CustomSqlSessionTemplate`,原生的sqlSessionTemplate是不能实现在一个事务中实现数据源切换的。为了不占用篇幅我会在后文再给出详细的原因分析
+ 这里我们创建 druid 数据源的时候,创建的是 `DruidXADataSource`,它继承自 `DruidDataSource ` 并支持 XA 分布式事务;
+ 使用 `AtomikosDataSourceBean` 包装我们创建的 `DruidXADataSource`,使得数据源能够被 JTA 事务管理器管理;
+ 这里我们使用的 sqlSessionTemplate 是我们重写的 `CustomSqlSessionTemplate`,原生的 sqlSessionTemplate 是不能实现在一个事务中实现数据源切换的。(为了不占用篇幅,我会在后文再给出详细的原因分析)
```java
/**
@ -207,7 +207,7 @@ public class DataSourceFactory {
/***
* 创建 DruidXADataSource 1 用@ConfigurationProperties自动配置属性
* 创建 DruidXADataSource 1 用@ConfigurationProperties 自动配置属性
*/
@Bean
@ConfigurationProperties("spring.datasource.druid.db1")
@ -225,7 +225,7 @@ public class DataSourceFactory {
}
/**
* 创建支持XA事务的Atomikos数据源1
* 创建支持 XA 事务的 Atomikos 数据源 1
*/
@Bean
public DataSource dataSourceOne(DataSource druidDataSourceOne) {
@ -237,7 +237,7 @@ public class DataSourceFactory {
}
/**
* 创建支持XA事务的Atomikos数据源2
* 创建支持 XA 事务的 Atomikos 数据源 2
*/
@Bean
public DataSource dataSourceTwo(DataSource druidDataSourceTwo) {
@ -249,8 +249,8 @@ public class DataSourceFactory {
/**
* @param dataSourceOne 数据源1
* @return 数据源1的会话工厂
* @param dataSourceOne 数据源 1
* @return 数据源 1 的会话工厂
*/
@Bean
public SqlSessionFactory sqlSessionFactoryOne(DataSource dataSourceOne)
@ -260,8 +260,8 @@ public class DataSourceFactory {
/**
* @param dataSourceTwo 数据源2
* @return 数据源2的会话工厂
* @param dataSourceTwo 数据源 2
* @return 数据源 2 的会话工厂
*/
@Bean
public SqlSessionFactory sqlSessionFactoryTwo(DataSource dataSourceTwo)
@ -271,10 +271,10 @@ public class DataSourceFactory {
/***
* sqlSessionTemplateSpring事务管理一起使用以确保使用的实际SqlSession是与当前Spring事务关联的,
* 此外它还管理会话生命周期包括根据Spring事务配置根据需要关闭提交或回滚会话
* @param sqlSessionFactoryOne 数据源1
* @param sqlSessionFactoryTwo 数据源2
* sqlSessionTemplateSpring 事务管理一起使用,以确保使用的实际 SqlSession 是与当前 Spring 事务关联的,
* 此外它还管理会话生命周期,包括根据 Spring 事务配置根据需要关闭,提交或回滚会话
* @param sqlSessionFactoryOne 数据源 1
* @param sqlSessionFactoryTwo 数据源 2
*/
@Bean
public CustomSqlSessionTemplate sqlSessionTemplate(SqlSessionFactory sqlSessionFactoryOne,
@ -297,13 +297,13 @@ public class DataSourceFactory {
SqlSessionFactoryBean factoryBean = new SqlSessionFactoryBean();
factoryBean.setDataSource(dataSource);
factoryBean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources(MAPPER_LOCATION));
// 其他可配置项(包括是否打印sql,是否开启驼峰命名等)
// 其他可配置项 (包括是否打印 sql,是否开启驼峰命名等)
org.apache.ibatis.session.Configuration configuration = new org.apache.ibatis.session.Configuration();
configuration.setMapUnderscoreToCamelCase(true);
configuration.setLogImpl(StdOutImpl.class);
factoryBean.setConfiguration(configuration);
/* *
* 采用个如下方式配置属性的时候一定要保证已经进行数据源的配置(setDataSource)和数据源和MapperLocation配置(setMapperLocations)
* 采用个如下方式配置属性的时候一定要保证已经进行数据源的配置 (setDataSource) 和数据源和 MapperLocation 配置 (setMapperLocations)
* factoryBean.getObject().getConfiguration().setMapUnderscoreToCamelCase(true);
* factoryBean.getObject().getConfiguration().setLogImpl(StdOutImpl.class);
**/
@ -314,7 +314,7 @@ public class DataSourceFactory {
#### 3. 自定义sqlSessionTemplate的主要实现逻辑
这里主要覆盖重写了sqlSessionTemplategetSqlSessionFactory从ThreadLocal去获取实际使用的数据源AOP切面将实际使用的数据源存入ThreadLocal
这里主要覆盖重写了 sqlSessionTemplategetSqlSessionFactory ThreadLocal 去获取实际使用的数据源AOP 切面将实际使用的数据源存入 ThreadLocal
```java
/***
@ -340,12 +340,12 @@ public SqlSessionFactory getSqlSessionFactory() {
/**
* 这个方法的实现和父类的实现是基本一致的唯一不同的就是在getSqlSession方法传参中获取会话工厂的方式
* 这个方法的实现和父类的实现是基本一致的,唯一不同的就是在 getSqlSession 方法传参中获取会话工厂的方式
*/
private class SqlSessionInterceptor implements InvocationHandler {
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
//在getSqlSession传参时候用我们重写的getSqlSessionFactory获取当前数据源对应的会话工厂
//在 getSqlSession 传参时候,用我们重写的 getSqlSessionFactory 获取当前数据源对应的会话工厂
final SqlSession sqlSession = getSqlSession(
CustomSqlSessionTemplate.this.getSqlSessionFactory(),
CustomSqlSessionTemplate.this.executorType,
@ -375,7 +375,7 @@ private class SqlSessionInterceptor implements InvocationHandler {
#### 4. 使用AOP动态切换数据源将当前使用的数据源名称保存到线程隔离的ThreadLocal中
这里我们直接对dao层接口进行切面如果第一个参数指明需要使用哪一个数据源就使用对应的数据源如果没有指定就使用默认的数据源。
这里我们直接对 dao 层接口进行切面,如果第一个参数指明需要使用哪一个数据源,就使用对应的数据源,如果没有指定,就使用默认的数据源。
注:使用切面来切换数据源是一种实现思路,而具体如何定义切入点可以按照自己的实际情况来定,你可以使用第一个参数指明数据源,也可以自定义注解来指定数据源,这个按照自己的实际使用方便来实现即可。
@ -436,7 +436,7 @@ public class DataSourceContextHolder {
```java
/**
* @author : heibaiying
* @description : JTA事务配置
* @description : JTA 事务配置
*/
@Configuration
@EnableTransactionManagement
@ -474,9 +474,9 @@ public class XATransactionManagerConfig {
### 3.1 测试数据库整合结果
这里我在mysqlmysql02的表中分别插入了一条数据
这里我在 mysqlmysql02 的表中分别插入了一条数据:
mysql数据库
mysql 数据库:
<div align="center"> <img src="https://github.com/heibaiying/spring-samples-for-all/blob/master/pictures/mysql01.png"/> </div>
@ -494,7 +494,7 @@ mysql02 数据库:
### 3.2 测试单数据库事务
这里因为没有复杂的业务逻辑,我直接将@Transactional加载controller层实际中最好加到service层
这里因为没有复杂的业务逻辑,我直接将@Transactional 加载 controller 层,实际中最好加到 service
```java
/**
@ -567,15 +567,15 @@ public class XATransactionController {
### 3.4 测试druid数据源是否整合成功
访问 http://localhost:8080/druid/index.html 可以在数据源监控页面看到两个数据源已配置成功同时配置都与我们在yml配置文件中的一致。
访问 http://localhost:8080/druid/index.html ,可以在数据源监控页面看到两个数据源已配置成功,同时配置都与我们在 yml 配置文件中的一致。
数据源1
数据源 1
<div align="center"> <img src="https://github.com/heibaiying/spring-samples-for-all/blob/master/pictures/druid-mysql01.png"/> </div>
数据源2
数据源 2
<div align="center"> <img src="https://github.com/heibaiying/spring-samples-for-all/blob/master/pictures/durud-mysql02.png"/> </div>
@ -593,11 +593,11 @@ url 监控情况:
### 4.1 XA 与 JTA
XA是由X/Open组织提出的分布式事务的规范。XA规范主要定义了(全局)事务管理器(Transaction Manager)(局部)资源管理器(Resource Manager)之间的接口。XA接口是双向的系统接口在事务管理器Transaction Manager以及一个或多个资源管理器Resource Manager之间形成通信桥梁。XA之所以需要引入事务管理器是因为在分布式系统中从理论上讲两台机器理论上无 法达到一致的状态,需要引入一个单点进行协调。
事务管理器控制着全局事务,管理事务生命周期,并协调资源。资源管理器负责控制和管理实际资源(如数据库或 JMS队列
XA 是由 X/Open 组织提出的分布式事务的规范。XA 规范主要定义了 (全局) 事务管理器 (Transaction Manager)(局部) 资源管理器 (Resource Manager) 之间的接口。XA 接口是双向的系统接口在事务管理器Transaction Manager以及一个或多个资源管理器Resource Manager之间形成通信桥梁。XA 之所以需要引入事务管理器是因为,在分布式系统中,从理论上讲,两台机器理论上无 法达到一致的状态,需要引入一个单点进行协调。
事务管理器控制着全局事务,管理事务生命周期,并协调资源。资源管理器负责控制和管理实际资源(如数据库或 JMS 队列)。
下图说明了事务管理器、资源管理器,与应用程序之间的关系。
**而 JTA 就是 XA 规范在java语言上的实现。JTA 采用两阶段提交实现分布式事务。**
**而 JTA 就是 XA 规范在 java 语言上的实现。JTA 采用两阶段提交实现分布式事务。**
<div align="center"> <img src="https://github.com/heibaiying/spring-samples-for-all/blob/master/pictures/XA.gif"/> </div>
@ -605,16 +605,16 @@ XA是由X/Open组织提出的分布式事务的规范。XA规范主要定义了(
### 4.2 两阶段提交
分布式事务必须满足传统事务的特性,即原子性,一致性,分离性和持久性。但是分布式事务处理过程中,某些节点(Server)可能发生故障,或 者由于网络发生故障而无法访问到某些节点。为了防止分布式系统部分失败时产生数据的不一致性。在分布式事务的控制中采用了两阶段提交协议Two- Phase Commit Protocol。即事务的提交分为两个阶段
分布式事务必须满足传统事务的特性,即原子性,一致性,分离性和持久性。但是分布式事务处理过程中,某些节点 (Server) 可能发生故障,或 者由于网络发生故障而无法访问到某些节点。为了防止分布式系统部分失败时产生数据的不一致性。在分布式事务的控制中采用了两阶段提交协议Two- Phase Commit Protocol。即事务的提交分为两个阶段
+ 预提交阶段(Pre-Commit Phase)
+ 预提交阶段 (Pre-Commit Phase)
+ 决策后阶段Post-Decision Phase
两阶段提交用来协调参与一个更新中的多个服务器的活动,以防止分布式系统部分失败时产生数据的不一致性。例如,如果一个更新操作要求位于三个不同结点上的记录被改变,且其中只要有一个结点失败,另外两个结点必须检测到这个失败并取消它们所做的改变。为了支持两阶段提交,一个分布式更新事务中涉及到的服务器必须能够相互通信。一般来说一个服务器会被指定为"控制"或"提交"服务器并监控来自其它服务器的信息。
在分布式更新期间,各服务器首先标志它们已经完成(但未提交)指定给它们的分布式事务的那一部分,并准备提交(以使它们的更新部分成为永久性的)。这是 两阶段提交的第一阶段。如果有一结点不能响应,那么控制服务器要指示其它结点撤消分布式事务的各个部分的影响。如果所有结点都回答准备好提交,控制服务器 则指示它们提交并等待它们的响应。等待确认信息阶段是第二阶段。在接收到可以提交指示后,每个服务器提交分布式事务中属于自己的那一部分,并给控制服务器 发回提交完成信息。
在一个分布式事务中必须有一个场地的Server作为协调者(coordinator),它能向 其它场地的Server发出请求并对它们的回答作出响应由它来控制一个分布式事务的提交或撤消。该分布式事务中涉及到的其它场地的Server称为参与者(Participant)。
在一个分布式事务中,必须有一个场地的 Server 作为协调者 (coordinator),它能向 其它场地的 Server 发出请求,并对它们的回答作出响应,由它来控制一个分布式事务的提交或撤消。该分布式事务中涉及到的其它场地的 Server 称为参与者 (Participant)。
<div align="center"> <img src="https://github.com/heibaiying/spring-samples-for-all/blob/master/pictures/commit.png"/> </div>
@ -626,7 +626,7 @@ XA是由X/Open组织提出的分布式事务的规范。XA规范主要定义了(
(1) 协调者准备局部(即在本地)提交并在日志中写入"预提交"日志项,并包含有该事务的所有参与者的名字。
(2) 协调者询问参与者能否提交该事务。一个参与者可能由于多种原因不能提交。例如该Server提供的约束条(Constraints)的延迟检查不符合 限制条件时不能提交参与者本身的Server进程或硬件发生故障不能提交或者协调者访问不到某参与者网络故障这时协调者都认为是收到了一个 否定的回答。
(2) 协调者询问参与者能否提交该事务。一个参与者可能由于多种原因不能提交。例如,该 Server 提供的约束条 (Constraints) 的延迟检查不符合 限制条件时,不能提交;参与者本身的 Server 进程或硬件发生故障,不能提交;或者协调者访问不到某参与者(网络故障),这时协调者都认为是收到了一个 否定的回答。
(3) 如果参与者能够提交,则在其本身的日志中写入"准备提交"日志项,该日志项立即写入硬盘,然后给协调者发回,已准备好提交"的回答。
@ -652,19 +652,19 @@ XA是由X/Open组织提出的分布式事务的规范。XA规范主要定义了(
### 5.1 事务下多数据源无法切换
这里是主要是对上文提到为什么不重写sqlSessionTemplate会导致在事务下数据源切换失败的补充我们先看看sqlSessionTemplate源码中关于该类的定义
这里是主要是对上文提到为什么不重写 sqlSessionTemplate 会导致在事务下数据源切换失败的补充,我们先看看 sqlSessionTemplate 源码中关于该类的定义:
> sqlSessionTemplateSpring事务管理一起使用以确保使用的实际SqlSession是与当前Spring事务关联的,此外它还管理会话生命周期包括根据Spring事务配置根据需要关闭提交或回滚会话
> sqlSessionTemplateSpring 事务管理一起使用,以确保使用的实际 SqlSession 是与当前 Spring 事务关联的,此外它还管理会话生命周期,包括根据 Spring 事务配置根据需要关闭,提交或回滚会话
<div align="center"> <img src="https://github.com/heibaiying/spring-samples-for-all/blob/master/pictures/sqlSessionTemplate.png"/> </div>
这里最主要的是说明sqlSession是与当前是spring 事务是关联的。
这里最主要的是说明 sqlSession 是与当前是 spring 事务是关联的。
#### 1. sqlSession与事务关联导致问题
对于mybatis 来说是默认开启一级缓存的一级缓存是session级别的对于同一个session如果是相同的查询语句并且查询参数都相同第二次的查询就直接从一级缓存中获取。
对于 mybatis 来说,是默认开启一级缓存的,一级缓存是 session 级别的,对于同一个 session 如果是相同的查询语句并且查询参数都相同,第二次的查询就直接从一级缓存中获取。
这也就是说对于如下的情况由于sqlSession是与事务绑定的如果使用原生sqlSessionTemplate则第一次查询和第二次查询都是用的同一个sqlSession那么第二个查询数据库2的查询语句根本不会执行,会直接从一级缓存中获取查询结果。两次查询得到都是第一次查询的结果。
这也就是说,对于如下的情况,由于 sqlSession 是与事务绑定的,如果使用原生 sqlSessionTemplate则第一次查询和第二次查询都是用的同一个 sqlSession那么第二个查询数据库 2 的查询语句根本不会执行,会直接从一级缓存中获取查询结果。两次查询得到都是第一次查询的结果。
```java
@GetMapping("ts/db/programmers")
@ -682,7 +682,7 @@ public List<Programmer> getAllProgrammers() {
先说一下为什么会出现连接的复用:
我们可以在spring的源码中看到spring在通过`DataSourceUtils`类中去获取新的连接`doGetConnection`的时候,会通过`TransactionSynchronizationManager.getResource(dataSource)`方法去判断当前数据源是否有可用的连接,如果有就直接返回,如果没有就通过`fetchConnection`方法去获取。
我们可以在 spring 的源码中看到 spring 在通过 `DataSourceUtils` 类中去获取新的连接 `doGetConnection` 的时候,会通过 `TransactionSynchronizationManager.getResource(dataSource)` 方法去判断当前数据源是否有可用的连接,如果有就直接返回,如果没有就通过 `fetchConnection` 方法去获取。
```java
public static Connection doGetConnection(DataSource dataSource) throws SQLException {
@ -734,37 +734,37 @@ public static Connection doGetConnection(DataSource dataSource) throws SQLExcept
}
```
这里主要的问题是`TransactionSynchronizationManager.getResource(dataSource)`dataSource参数是在哪里进行注入的这里可以沿着调用堆栈往上寻找可以看到是在这个参数是`SpringManagedTransaction`类中获取连接的时候传入的。
这里主要的问题是 `TransactionSynchronizationManager.getResource(dataSource)`dataSource 参数是在哪里进行注入的,这里可以沿着调用堆栈往上寻找,可以看到是在这个参数是 `SpringManagedTransaction` 类中获取连接的时候传入的。
<div align="center"> <img src="https://github.com/heibaiying/spring-samples-for-all/blob/master/pictures/opneConnection.png"/> </div>
`SpringManagedTransaction`这类中的dataSource是如何得到赋值的这里可以进入这个类中查看只有在创建这个类的时候通过构造器为dataSource赋值那么是哪个方法创建了`SpringManagedTransaction`?
`SpringManagedTransaction` 这类中的 dataSource 是如何得到赋值的,这里可以进入这个类中查看,只有在创建这个类的时候通过构造器为 dataSource 赋值,那么是哪个方法创建了 `SpringManagedTransaction`?
<div align="center"> <img src="https://github.com/heibaiying/spring-samples-for-all/blob/master/pictures/springManagerTransaction.png"/> </div>
在构造器上打一个断点,沿着调用的堆栈往上寻找可以看到是`DefaultSqlSessionFactory`在创建`SpringManagedTransaction`中传入的,**这个数据源就是创建sqlSession`sqlSessionFactory`中数据源**。
在构造器上打一个断点,沿着调用的堆栈往上寻找可以看到是 `DefaultSqlSessionFactory` 在创建 `SpringManagedTransaction` 中传入的,**这个数据源就是创建 sqlSession`sqlSessionFactory` 中数据源**。
<div align="center"> <img src="https://github.com/heibaiying/spring-samples-for-all/blob/master/pictures/DefaultSqlSessionFactory.png"/> </div>
**这里说明连接的复用是与我们创建sqlSession时候传入的sqlSessionFactory是否是同一个有关**
**这里说明连接的复用是与我们创建 sqlSession 时候传入的 sqlSessionFactory 是否是同一个有关**
<div align="center"> <img src="https://github.com/heibaiying/spring-samples-for-all/blob/master/pictures/getsqlSession.png"/> </div>
所以我们才重写了sqlSessionTemplate中的`getSqlSession`方法获取sqlSession时候传入正在使用的数据源对应的`sqlSessionFactory`,这样即便在同一个的事务中,由于传入的`sqlSessionFactory`中不同,就不会出现连接复用。
所以我们才重写了 sqlSessionTemplate 中的 `getSqlSession` 方法,获取 sqlSession 时候传入正在使用的数据源对应的 `sqlSessionFactory`,这样即便在同一个的事务中,由于传入的 `sqlSessionFactory` 中不同,就不会出现连接复用。
<div align="center"> <img src="https://github.com/heibaiying/spring-samples-for-all/blob/master/pictures/customSqlSessionTemplate.png"/> </div>
关于mybati-spring 的更多事务处理机制,推荐阅读博客[mybatis-spring事务处理机制分析](https://my.oschina.net/fifadxj/blog/785621)
关于 mybati-spring 的更多事务处理机制,推荐阅读博客[mybatis-spring 事务处理机制分析](https://my.oschina.net/fifadxj/blog/785621)
### 5.2 出现org.apache.ibatis.binding.BindingExceptionInvalid bound statement (not found)异常
出现这个异常的的原因是在创建SqlSessionFactory的时候`setMapperLocations`配置好之前调用了`factoryBean.getObject()`方法
出现这个异常的的原因是在创建 SqlSessionFactory 的时候,在 `setMapperLocations` 配置好之前调用了 `factoryBean.getObject()` 方法
```java
//一个错误的示范
@ -779,7 +779,7 @@ private SqlSessionFactory createSqlSessionFactory(DataSource dataSource) throws
}
```
上面这段代码没有任何编译问题导致这个错误不容易发现但是在调用sql时候就会出现异常。原因是`factoryBean.getObject()`方法被调用时就已经创建了SqlSessionFactory并且SqlSessionFactory只会被创建一次。此时还没有指定sql 文件的位置导致mybatis无法将接口与xml中的sql语句进行绑定所以出现BindingExceptionInvalid 绑定异常。
上面这段代码没有任何编译问题,导致这个错误不容易发现,但是在调用 sql 时候就会出现异常。原因是 `factoryBean.getObject()` 方法被调用时就已经创建了 SqlSessionFactory并且 SqlSessionFactory 只会被创建一次。此时还没有指定 sql 文件的位置,导致 mybatis 无法将接口与 xml 中的 sql 语句进行绑定,所以出现 BindingExceptionInvalid 绑定异常。
```java
@Override
@ -792,7 +792,7 @@ private SqlSessionFactory createSqlSessionFactory(DataSource dataSource) throws
}
```
正常绑定的情况下我们是可以在sqlSessionFactory中查看到绑定好的查询接口
正常绑定的情况下,我们是可以在 sqlSessionFactory 中查看到绑定好的查询接口:
<div align="center"> <img src="https://github.com/heibaiying/spring-samples-for-all/blob/master/pictures/sqlSessionFactory.png"/> </div>