Netkiller Spring Cloud 手札

目录

1. 自述

1. 1. 写给读者

2. 2. 作者简介

3. 3. 如何获得文档

4. 4. 打赏（Donations）

5. 5. 联系方式

2. I. Spring Boot

1. 1. Spring 开发环境

1. 1. Java 开发环境

2. 2. 安装 Spring Tool Suite

3. 3. Dashboard

4. 4. Spring Initializr - Bootstrap your application

2. 2. Spring Boot Quick start

1. 1. 创建项目

2. 2. pom.xml

3. 3. Controller

4. 4. Springboot with Maven

1. resource

2. Maven run

3. Spring Boot maven 插件 build-image

4. 生成项目信息

3. 3. SpringApplication

1. 1. 运行 Spring boot 项目

1. Linux systemd

2. 传统 init.d 脚本

3. 编译用于Tomcat的 War

2. 2. @SpringBootApplication

1. 排除 @EnableAutoConfiguration 加载项

3. 3. 获取 Resources 目录中的静态文件

4. 4. @EnableAutoConfiguration

5. 5. @ComponentScan

6. 6. @EntityScan 实体扫描

7. 7. @EnableJpaRepositories

8. 8. 启动和销毁

9. 9. 打印环境变量

10. 10. CharacterEncodingFilter

11. 11. 隐藏 Banner

12. 12. 实体与仓库扫描

13. 13. 列出 Beans

14. 14. Tomcat 端口

15. 15. 配置项设定

16. 16. spring.profiles.active

17. 17. @Profile("dev") / @ActiveProfiles("dev")

18. 18. 设置默认时区

4. 4. 如何优雅停止 Springboot 运行

1. 1. 准备工作

2. 2. kill 命令演示

3. 3. 容器中如何优雅关闭 Springboot

4. 4. 写入PID文件

5. 5. Properties 配置文件

1. 1. application.properties 配置文件

1. application.properties 参考

2. 启动指定参数

1. --spring.config.location 指定配置文件

2. --spring.profiles.active 切换配置文件

3. 加载排除

4. PID FILE

5. banner 关闭

6. server

1. 优雅关闭

2. 端口配置

3. Session 配置

4. cookie

5. error 路径

6. 压缩传输

7. ssl

7. logging

8. 内嵌 tomcat server

2. server.tomcat.basedir

3. access.log

4. charset

9. servlet

1. 上传限制

2. server.servlet.context-path

10. JSON 输出与日期格式化

11. SMTP 相关配置

12. Redis

13. MongoDB

14. MySQL

15. Oracle

16. default_schema

17. datasource

18. velocity

19. Security 相关配置

20. MVC 配置

21. Kafka 相关配置

2. 2. Properties 文件

1. 禁用命令行注入环境变量

2. @Value 注解

3. @EnableConfigurationProperties 引用自定义 *.properties 配置文件

4. 手工载入 *.properties 文件

5. spring.profiles.active 参数切换配置文件

6. SpringApplicationBuilder.properties() 方法添加配置项

3. 3. 参数引用

4. 4. 默认值

5. 5. 产生随机数

1. 随机数

6. 6. 多行字符串

7. 7. 注入多值属性 arrays, list, set

8. 8. containsProperty 读取配置文件

9. 9. @PropertySource 注解载入 properties 文件

10. 10. List 列表类型

11. 11. Map类型

12. 12. Binder

13. 13. 加密 application.properties 中的敏感内容

6. 6. Spring boot with Logging

1. 1. 配置日志文件

1. 日志输出级别

2. Spring boot 2.1 以后的版本不打印 Mapped 日志问题

3. 禁止控制台输出日志

4. 定制日志格式

5. 彩色输出

2. 2. 打印日志

1. lombok

3. 3. logback 配置详解

1. 标准输出

2. 禁止 logback 日志输出

3. 指定Class过滤日志

4. configuration 属性配置

5. contextName 设置上下文名称

6. property 设置变量

7. encoder 日志格式设置

8. RollingFileAppender

1. 按日期分割日志

2. 按照文件尺寸分割日志

9. 日志过滤

10. 标准输出

11. MDC

12. 日志写入 MongoDB

13. 日志发送给 logstash

1. logstash 配置

2. Java 项目

3. 通过 tags 区分日志文件

1. logstash pipeline 配置

2. logback-spring.xml 配置

3. 打印日志

14. fluentd

1. Maven 依赖

2. 安装 fluent-bit

3. 配置 logback-spring.xml

4. 查看 fluent 输出

15. Loki4j Logback

1. Maven

2. logback.xml

4. 4. Log4j2 + Gelf + Logstash

1. Maven 配置

2. log4j2.xml 配置

3. Java 测试代码

4. Logstash 配置

5. 测试结果

6. Log4j2 更多技巧

1. 多环境配置

2. 控制 class 日志输出级别

3. 日志输出级别

4. 读取系统变量/环境变量

5. 读取 spring 配置

6. 变量默认值

5. 5. 日志报警

1. Logstash 配置

2. 监控 SpringBootApplication 的启动和退出

6. 6. Spring boot with ELK(Elasticsearch + Logstash + Kibana)

1. TCP 方案

2. Redis 方案

3. Kafka 方案

4. Other

7. 7. Spring boot with Undertow

1. 1. Maven 依赖

2. 2. Application

3. 3. 相关配置

8. 8. Spring boot with Jetty

9. 9. Spring boot with HTTP2 SSL

1. 1. 生成自签名证书

2. 2. application.properties 配置文件

3. 3. 启动 Spring boot

4. 4. restTemplate 调用实例

5. 5. HTTP2

10. 10. Spring boot with Webpage

1. 1. Maven

2. 2. application.properties

3. 3. Application

4. 4. IndexController

5. 5. src/main/webapp/WEB-INF/jsp/index.jsp

6. 6. 集成模板引擎

11. 11. Spring boot with Velocity template

1. 1. Maven

2. 2. Resource

3. 3. Application

4. 4. RestController

5. 5. Test

12. 12. Spring boot with Thymeleaf

1. 1. Maven

2. 2. application.properties

3. 3. Controller

4. 4. HTML5 Template

13. 13. Spring boot with MongoDB

1. 1. Maven

2. 2. Application

3. 3. MongoTemplate

4. 4. Repository

14. 14. Spring boot with MySQL

1. 1. Maven

2. 2. Resource

3. 3. Application

4. 4. JdbcTemplate

5. 5. CrudRepository

15. 15. Spring boot with Oracle

1. 1. Maven

2. 2. application.properties

3. 3. Application

4. 4. CrudRepository

5. 5. JdbcTemplate

6. 6. Controller

16. 16. Spring boot with PostgreSQL

1. 1. pom.xml

2. 2. application.properties

3. 3. Application

4. 4. CrudRepository

5. 5. JdbcTemplate

6. 6. Controller

7. 7. Test

17. 17. Spring boot with Elasticsearch

1. 1. Maven

2. 2. Application

3. 3. application.properties

4. 4. Domain

5. 5. ElasticsearchRepository

18. 18. Spring boot with Elasticsearch TransportClient

1. 1. Maven

2. 2. Application

3. 3. application.properties

4. 4. ElasticsearchConfiguration

5. 5. RestController

19. 19. Spring boot with Apache Hive

1. 1. Maven

2. 2. application.properties

3. 3. Configuration

4. 4. CURD 操作实例

20. 20. Spring boot with Phoenix

1. 1. Maven

2. 2. application.properties

3. 3. Configuration

21. 21. Spring boot with Datasource

1. 1. Master / Slave 主从数据库数据源配置

1. application.properties

2. 配置主从数据源

3. 选择数据源

2. 2. 多数据源配置

3. 3. JPA 多数据源

22. 22. 连接池配置

1. 1. org.apache.tomcat.jdbc.pool.DataSource

2. 2. druid

1. 加密数据库密码

3. 3. c3p0 - JDBC3 Connection and Statement Pooling

4. 4. dbcp2

5. 5. bonecp

6. 6. HikariPool

23. 23. Spring boot with Queue

1. 1. Spring boot with RabbitMQ(AMQP)

1. maven

2. RabbitMQConfig

3. 生产者

4. 消费者

2. 2. Spring boot with Apache Kafka

1. 安装 kafka

2. maven

3. Spring boot Application

4. EnableKafka

5. KafkaListener

6. 测试

7. 完整的发布订阅实例

1. Consumer

2. Producer

3. Test

8. Spring cloud with Kafka

1. Application 主文件

2. 资源配置文件

1. application.properties

2. bootstrap.properties

3. Git 仓库

3. 启用 kafka

4. 消息发布主程序

24. 24. Spring boot with Scheduling

1. 1. Application.java

2. 2. Component

3. 3. 查看日志

4. 4. 计划任务控制开关

5. 5. @Scheduled 详解

1. 每3秒钟一运行一次

2. 凌晨23点运行

6. 6. Timer 例子

7. 7. ScheduledExecutorService 例子

25. 25. Spring boot with Swagger

1. 1. Swagger3

2. 2. Swagger2

1. Maven 文件

2. SpringApplication

3. EnableSwagger2

4. RestController

3. 3. @Api()

4. 4. @ApiOperation()

5. 5. @ApiResponses

6. 6. @ApiModel 实体类

26. 26. Spring boot with knife4j

1. 1. maven

2. 2. Knife4jConfiguration

3. 3. application.properties

27. 27. Spring boot with lombok

1. 1. @Builder

2. 2. @Slf4j 注解

28. 28. Spring boot with Container

1. 1. Spring boot with Docker

1. 通过 Docker 命令构建镜像

1. 手工编译镜像

2. Dockerfile 放在 src/main/docker/Dockerfile 下

3. 通过参数指定 Springboot 文件

4. SPRING_PROFILES_ACTIVE 指定配置文件

5. 推送镜像到仓库

2. 通过 Maven 构建 Docker 镜像

1. Maven + Dockerfile 方案一

2. Maven + Dockerfile 方案二

3. Maven 不使用 Dockerfile 文件

4. 推送镜像

3. [ERROR] No plugin found for prefix 'dockerfile' in the current project and in the plugin groups [org.apache.maven.plugins, org.codehaus.mojo] available from the repositories [local (/Users/neo/.m2/repository), central (https://repo.maven.apache.org/maven2)] -> [Help 1]

4. curl: (35) LibreSSL SSL_connect: SSL_ERROR_SYSCALL in connection to localhost:8888

2. 2. Spring boot with Docker stack

1. 编译 Docker 镜像

3. 3. Spring boot with Kubernetes

1. Kubernetes 编排脚本

2. 部署镜像

29. 29. Spring boot with command line

1. 1. Maven

2. 2. CommandLineRunner 例子

3. 3. ApplicationRunner 例子

30. 30. Spring Boot Actuator

1. 1. Maven 依赖

2. 2. 与 Spring Boot Actuator 有关的配置

1. 禁用HTTP端点

2. 安全配置

3. 修改 actuator 地址

4. 关机

3. 3. actuator 接口

4. 4. 健康状态

1. 健康状态

5. 5. info 配置信息

6. 6. beans 信息

7. 7. caches

8. 8. conditions

9. 9. configprops 配置文件

10. 10. env 环境变量

11. 11. logfile 日志

12. 12. threaddump 线程信息

13. 13. 计划任务

14. 14. metrics

15. 15. 控制器映射 URL

16. 16. 自定义监控指标

31. 31. String boot with RestTemplate

1. 1. RestTemplate Example

1. pom.xml

2. web.xml

3. springframework.xml

4. RestController

5. POJO

6. 在控制器中完整实例

7. 测试

2. 2. GET 操作

1. 返回字符串

2. 传递 GET 参数

3. 3. POST 操作

1. postForObject

1. 传递对象

2. 传递数据结构 MultiValueMap

2. postForEntity

4. 4. PUT 操作

5. 5. Delete 操作

6. 6. 上传文件

7. 7. HTTP Auth

1. Client

8. 8. PKCS12

9. 9. Timeout 超时设置

1. JRE 启动参数设置超时时间

2. RestTemplate timeout with SimpleClientHttpRequestFactory

3. @Configuration 方式

32. 32. SpringBootTest

1. 1. Maven 依赖

2. 2. 测试类

1. Junit基本注解介绍

3. 3.

1. Assert.assertEquals 判断相等

2. Assert.assertTrue

4. 4. JPA 测试

5. 5. TestRestTemplate

6. 6. Controller单元测试

7. 7. WebTestClient

33. 33. Spring boot with Aop

1. 1. Aspect

1. Maven

2. Pojo 类

3. Service 类

4. Aspect 类

5. 控制器

6. Application

7. 测试

34. 34. Spring boot with starter

1. 1. 实现 starter

1. Maven pom.xml 依赖包

2. 配置文件处理

3. 自动配置文件

4. 启用 starter 的自定义注解

2. 2. 引用 starter

1. Maven pom.xml 引入依赖

2. 通过注解配置 starter

3. 测试运行结果

35. 35. Spring boot with Monitor

1. 1. Spring boot with Grafana

1. Springboot 集成 InfluxDB

2. InfluxDB

2. 2. Spring Boot with Prometheus

1. Maven 依赖

2. application.properties 配置文件

3. 启动类

4. 测试

5. 控制器监控

6. 自定义埋点监控

1. 拦截器

2. 计数器元件

3. 配置类

4. 测试埋点效果

36. 37. Spring boot with Git version

1. 1. CommonRestController 公共控制器

2. 2. VersionRestController 测试控制器

3. 3. 创建 .gitattributes 文件

37. 38. Spring boot with Session share

1. 1. Redis

1. Maven

2. application.properties

3. Application

2. 2. 测试 Session

3. 3. JDBC

4. 4. Springboot 2.1

38. 39. Spring boot with Caching

1. 1. maven

1. Redis

2. 2. 启用 Cache

3. 3. @Cacheable 的用法

1. SpEL表达式

2. 排除 null 结果

4. 4. @CachePut 用法

5. 5. 清空缓存

6. 6. @Caching

7. 7. 解决Expire 和 TTL 过期时间

39. 40. Spring boot with Email

1. 1. Maven

2. 2. Resource

3. 3. POJO

4. 4. RestController

5. 5. Test

40. 41. Spring boot with Hessian

1. 1. Maven

2. 2. Application

3. 3. HessianServiceExporter

4. 4. Service

5. 5. RestController

41. 42. Spring boot with Async

1. 1. Callable 实现异步

2. 2. WebAsyncTask 实现异步

3. 3. DeferredResult 实现异步访问

4. 4. SimpleAsyncTaskExecutor

1. 配置线程池

5. 5. ThreadPoolTaskExecutor 自定义线程池

1. 最简单的配置

2. 队列

6. 6. 定义多个线程池

7. 7. 自定义线程池

1. ThreadPoolExecutor

2. 注入自定义线程池bean

8. 8. 设置线程名称

9. 9. 线程池监控

10. 10. 注意事项

42. 43. Springboot with Ethereum (web3j)

1. 1. Maven

2. 2. application.properties

3. 3. TestRestController

4. 4. 测试

43. 44. Java Record 新特性

1. 1. Record 替代 POJO 类

2. 2. Record 作为 Properties

3. 3. Record 作为实体类

4. 4. Record 作为 Service

5. 5. Record 作为 Controller

3. II. Spring Framework

1. 45. Spring MVC

1. 1. @EnableWebMvc

1. 1.1. CORS 跨域请求

2. 1.2. Spring MVC CORS with WebMvcConfigurerAdapter

2. 2. @Controller

1. 2.1. @RequestMapping

1. @RequestMapping("/")

2. 映射多个URL

3. 匹配通配符

4. headers

2. 2.2. @GetMapping

3. 2.3. @PostMapping

4. 2.4. @RequestBody

1. 原始数据

2. @RequestBody 传递 List

3. 传递 Map 数据

4. 获取 JSONObject 数据

5. 2.5. RequestMapping with Request Parameters - @RequestParam

1. HTTP GET

2. HTTP POST

3. @RequestParam 传递特殊字符串

4. 传递日期参数

5. 上传文件

6. @RequestParam - POST 数组

7. 默认值

8. 是否非必须

9. 用 Map 接收 From 数据

6. 2.6. @RequestHeader - 获取 HTTP Header 信息

1. @RequestHeader 从 Http 头中获取变量

7. 2.7. RequestMapping with Path Variables - @PathVariable

1. URL 参数传递

2. 默认值

3. URL 传递 Date 类型

4. 处理特殊字符

5. @PathVariable 注意事项

8. 2.8. @MatrixVariable注解，RFC3986定义URI的路径(Path)中可包含name-value片段

9. 2.9. @ModelAttribute

10. 2.10. @ResponseBody

1. 直接返回HTML

11. 2.11. @ResponseStatus 设置 HTTP 状态

12. 2.12. @CrossOrigin

1. maxAge

13. 2.13. @CookieValue - 获取 Cookie 值

14. 2.14. @SessionAttributes

15. 2.15. ModelAndView

1. 变量传递

2. ModelMap 传递多个变量

3. redirect

4. ArrayList

5. HashMap

6. 传递对象

16. 2.16. HttpServletRequest / HttpServletResponse

1. HttpServletResponse

2. HttpServletRequest

3. 3. @RestController

1. 3.1. 上传文件

2. 3.2. 返回实体

3. 3.3. JSON

4. 3.4. 处理原始 RAW JSON 数据

5. 3.5. 返回 JSON 对象 NULL 专为 "" 字符串

6. 3.6. XML

7. 3.7. 兼容传统 json 接口

8. 3.8. 上传文件

9. 3.9. Spring boot with csv

10. 3.10. Problem Details [RFC 7807]

1. ResponseEntity

1. status

11. 3.11. Jackson

1. Jackson 相关配置

2. @JsonIgnore 返回json是不含有该字段

3. @JsonFormat 格式化 json 时间格式

1. 日期格式化

2. 时区

3. 枚举

4. 枚举

4. @JsonComponent

5. Object to Json

6. Json To Object

12. 3.12. synchronized

13. 3.13. SSE

4. 4. View

1. 4.1. 配置静态文件目录

2. 4.2. 添加静态文件目录

3. 4.3. Using Spring’s form tag library

1. css

1. cssClass

2. cssStyle

3. cssErrorClass

2. cssClass

4. 4.4. Thymeleaf

1. Maven pom.xml

2. Spring 配置

3. controller

4. HTML5 Template

5. thymeleaf 渲染表格

6. URL 链接

7. 拆分字符串

8. 日期格式化

5. 4.5. FreeMarker

5. 5. Service

1. 5.1. Application

2. 5.2. 定义接口

3. 5.3. 实现接口

4. 5.4. 调用 Service

5. 5.5. context.getBean 调用 Service

6. 5.6. AopContext

7. 5.7. 变量共享

6. 6. i18n 国际化

1. 6.1. 在 appliction.properties 中配置启用 i18n

2. 6.2. 创建语言包文件

3. 6.3. 控制器重引用语言包

4. 6.4. 参数传递

7. 7. 校验器(Validator)

1. 7.1. 常规用法

1. 定义校验器

2. 获取 BindingResult 结果

3. 测试校验效果

2. 7.2. 自定义注解

1. 定义校验器注解接口

2. 实现接口

3. 注解用法

4. 测试注解

8. 8. Interceptor/Filter 拦截器/过滤

1. 8.1. Session 拦截

2. 8.2. Token 拦截

3. 8.3. 过滤器

4. 8.4. 拦截器获取PathVariable变量

9. 9. RestClient

1. 9.1. 创建 RestClient

2. 9.2. Get 操作

3. 9.3. Post Json

4. 9.4. HTTP Authorization Basic

5. 9.5.

10. 10. FAQ

1. 10.1. o.s.web.servlet.PageNotFound

2. 10.2. HTTP Status 500 - Handler processing failed; nested exception is java.lang.NoClassDefFoundError: javax/servlet/jsp/jstl/core/Config

3. 10.3. 同时使用 Thymeleaf 与 JSP

4. 10.4. 排除静态内容

5. 10.5. HTTP Status 406

6. 10.6. Caused by: java.lang.IllegalArgumentException: Not a managed type: class common.domain.Article

7. 10.7. {"error":"unauthorized","error_description":"Full authentication is required to access this resource"}

2. 46. WebFlux framework

1. 1. Getting Started

1. 1.1. Maven

2. 1.2. Application

3. 1.3. RestController

4. 1.4. 测试

2. 2. WebFlux 与 SprintMVC 有什么不同？

1. 2.1. 实验程序

2. 2.2. 实验结果

3. 3. WebFlux Router

1. 3.1. Component 原件

2. 3.2. 路由配置

3. 3.3. Thymeleaf

1. 模板引擎 Thymeleaf 依赖

2. application.properties 相关的配置

3. Webflux 控制器

4. Tymeleaf 视图

4. 3.4. Webflux Redis

1. Maven Redis 依赖

2. Redis 配置

3. Config

4. Service

5. 3.5. Webflux Mongdb

1. Maven 依赖

2. Repository

3. Service

4. 控制器

6. 3.6. Mono

7. 3.7. Flux 返回多条数据

8. 3.8. SSE

1. 一次性事件

2. 周期性事件

3. SSE 完整的例子

4. SSE Client 订阅实例

9. 3.9. WebClient

1. 配置 WebClient

2. @Controller/@RestController 实例

3. Get 请求实例

4. URI 参数

5. 查询参数

6. Post 操作演示

7. Post 表单数据

8. 上传文件

9. 设置 HTTP 头

10. websocket

11. 同步阻塞等待结果

4. 4. Webflux 安全

1. 4.1. Token 拦截器

2. 4.2. JWT

3. 4.3. spring-boot-starter-security

5. 5. 常见问题

1. 5.1. The Java/XML config for Spring MVC and Spring WebFlux cannot both be enabled, e.g. via @EnableWebMvc and @EnableWebFlux, in the same application.

2. 5.2. @EnableWebFluxSecurity 与 @EnableReactiveMethodSecurity 不生效

3. 5.3. webflux netty 不支持 Content-Type: application/x-www-form-urlencoded

4. III. Spring Data

1. 47. EntityManager

2. 48. Spring Data with JdbcTemplate

1. 1. execute

2. 2. queryForInt

3. 3. queryForLong

4. 4. queryForObject

1. 4.1. 返回整形与字符型

2. 4.2. 查询 Double 类型数据库

3. 4.3. 返回日期

4. 4.4. 返回结果集

5. 4.5. 通过 "?" 向SQL传递参数

6. 4.6. RowMapper 记录映射

5. 5. queryForList

1. 5.1. Iterator 用法

2. 5.2. for 循环

3. 5.3. forEach 用法

6. 6. queryForMap

7. 7. query

1. 7.1. ResultSet

2. 7.2. ResultSetExtractor

3. 7.3. RowMapper

8. 8. queryForRowSet

9. 9. update

10. 10. MapSqlParameterSource

11. 11. 实例参考

1. 11.1. 参数传递技巧

3. 49. Spring Data with MySQL

1. 1. 选择数据库表引擎

2. 2. 声明实体

1. 2.1. @Entity 声明实体

2. 2.2. @Table 定义表名

1. catalog

2. schema

3. uniqueConstraints

3. 2.3. @Id 定义主键

4. 2.4. @Column 定义字段：

1. 字段长度

2. 浮点型

3. 创建于更新控制

4. TEXT 类型

5. 整形数据类型

5. 2.5. 非数据库字段

6. 2.6. @Lob 注解属性将被持久化为 Blog 或 Clob 类型

7. 2.7. @NotNull 不能为空声明

8. 2.8. @Temporal 日期定义

9. 2.9. 创建日期

1. CreatedDate

2. 与时间日期有关的 hibernate 注解

1. 设置默认时间

2. 创建时间

3. 更新时间

3. 数据库级别的默认创建日期时间定义

4. 数据库级别的默认创建日期与更新时间定义

5. 最后修改时间

10. 2.10. @DateTimeFormat 处理日期时间格式

11. 2.11. Enum 枚举数据类型

1. 实体中处理 enum 类型，存储字符串

2. 实体中处理 enum 类型，存储序号

3. 数据库枚举类型

4. 自定义枚举value属性

12. 2.12. SET 数据结构

13. 2.13. JSON 数据类型

14. 2.14. 索引

1. 普通索引

2. 唯一索引

3. 复合索引

15. 2.15. 嵌入

1. @Embeddable / @Embedded

2. @AttributeOverrides 定义字段名称

3. 创建复合主键

16. 2.16. 外键

1. @JoinColumn

2. @OneToOne

1. 案例一

4. does not define an IdClass

5. 共享主键

6. null identifier

3. OneToMany 一对多

4. ManyToMany 多对多

5. 外键级联操作

1. CascadeType.PERSIST

2. CascadeType.REMOVE

3. 外键级联删除

4. MySQL ON DELETE CASCADE

6. @JoinTable

1. 多对多实例

7. @OrderBy

17. 2.17. 映射集合属性

1. 外键名称

18. 2.18. @JsonIgnore

19. 2.19. @EnableJpaAuditing 开启 JPA 审计功能

20. 2.20. 注释 @Comment

21. 2.21. @Pattern 数据匹配

22. 2.22. 实体继承

3. 3. Repository

1. 3.1. CrudRepository

1. 批量保存

2. 3.2. JpaRepository

3. 3.3. PagingAndSortingRepository

1. Pageable

2. 解决 PagingAndSortingRepository 没有 save 等方法的问题

3. @PageableDefault 分页

4. 3.4. findByXXX

1. 传 Boolean 参数

2. Eunm 传递枚举参数

5. 3.5. count 操作

6. 3.6. delete 删除操作

7. 3.7. OrderBy

8. 3.8. GreaterThan

9. 3.9. Sort 排序操作操作

10. 3.10. Pageable 翻页操作

1. PageRequest.of

11. 3.11. @DynamicInsert 与 @DynamicUpdate

12. 3.12. 继承已存在的 Repository

4. 4. TransactionTemplate

5. 5. JPQL @Query

1. 5.1. @Modifying 更新/删除

2. 5.2. 事务 @Transactional

1. 删除更新需要 @Transactional 注解

2. 回滚操作

3. private、default、protected 和 final 不支持事物

4. Service 注意事项

5. 需要 @Service 注解配合使用

3. 5.3. 参数传递

4. 5.4. 原生 SQL

5. 5.5. @Query 与 Pageagble

6. 5.6. 返回指定字段

7. 5.7. 返回指定的模型

8. 5.8. Collection

9. 5.9. 原生SQL查询

10. 5.10. Sort

11. 5.11. 锁 @Lock

4. 50. Spring Data with Redis

1. 1. 集成 Redis XML 方式

1. 1.1. pom.xml

2. 1.2. springframework-servlet.xml

3. 1.3. Controller

4. 1.4. index.jsp

5. 1.5. 测试

2. 2. RedisTemplate

1. 2.1. stringRedisTemplate 基本用法

2. 2.2. 设置缓存时间

3. 2.3. 字符串截取

4. 2.4. 追加字符串

5. 2.5. 设置键的字符串值并返回其旧值

6. 2.6. increment

7. 2.7. 删除 key

8. 2.8. 返回字符串长度

9. 2.9. 如果key不存便缓存。

10. 2.10. 缓存多个值 /获取多个值 multiSet / multiGet

11. 2.11. List

1. rightPush

2. rightPushAll

3. rightPushIfPresent

4. leftPush

5. leftPushAll

6. range

12. 2.12. SET 数据类型

1. 返回集合中的所有成员

2. 取出一个成员

3. 随机获取无序集合中的一个元素

4. 随机获取 n 个成员（存在重复数据）

5. 随机获取 n 个不重复成员

6. 在两个 SET 间移动数据

7. 成员删除

8. 返回集合数量

9. 判断元素是否在集合成员中

10. 对比两个集合求交集

11. 对比两个集合求交集，然后存储到新的 key 中

12. 合并两个集合，并去处重复数据

13. 合并两个集合去重复后保存到新的 key 中

14. 计算两个合集的差集

15. 计算两个合集的差集，然后保存到新的 key 中

16. 遍历 SET 集合

13. 2.13. 有序的 set 集合

14. 2.14. Hash

1. put

2. putAll

3. 从键中的哈希获取给定hashKey的值

4. delete

5. 确定哈希hashKey是否存在

6. 从哈希中获取指定的多个 hashKey 的值

7. 只有hashKey不存在时才能添加值

8. 获取整个Hash

9. 获取所有key

10. 通过 hashKey 获取所有值

11. 值加法操作

12. 遍历 Hash 表

15. 2.15. 过期时间未执行

16. 2.16. setBit / getBit 二进制位操作

17. 2.17. 存储 Json 对象

1. 集成 RedisTemplate 定义新类 JsonRedisTemplate

2. 配置 Redis

3. 测试

3. 3. Spring Data Redis - Repository Examples

1. 3.1. @EnableRedisRepositories 启动 Redis 仓库

2. 3.2. 定义 Domain 类

3. 3.3. Repository 接口

4. 3.4. 测试代码

5. 51. Spring Data with MongoDB

1. 1. Example Spring Data MongoDB

1. 1.1. pom.xml

2. 1.2. springframework-servlet.xml

3. 1.3. POJO

4. 1.4. Controller

5. 1.5. 查看测试结果

6. 1.6. 条件查询

2. 2. MongoDB 多数据源

1. 2.1. Maven

2. 2.2. Application 禁止自动配置 MongoDB

3. 2.3. application.properties 新增配置项

4. 2.4. MongoDB 配置类

5. 2.5. 创建 Document 关系映射类

6. 2.6. 测试控制器

7. 2.7. 测试

3. 3. @Document

1. 3.1. 指定表名

2. 3.2. @Id

3. 3.3. @Version

4. 3.4. @Field 定义字段名

5. 3.5. @Indexed

1. 普通索引

2. 唯一索引

3. 索引排序方式

4. 稀疏索引

5. 索引过期时间设置

6. 3.6. @CompoundIndex 复合索引

1. 普通复合索引

2. 唯一复合索引

7. 3.7. @TextIndexed

8. 3.8. @GeoSpatialIndex 地理位置索引

9. 3.9. @Transient 丢弃数据，不存到 mongodb

10. 3.10. @DBRef 做外外键引用

1. Article 类

2. Hypermedia 类

3. MongoRepository

4. RestController

5. 运行结果

11. 3.11. @DateTimeFormat

12. 3.12. @NumberFormat

13. 3.13. 在 @Document 中使用 Enum 类型

14. 3.14. 在 @Document 中定义数据结构 List/Map

15. 3.15. GeoJson 数据类型

4. 4. MongoRepository

1. 4.1. 扫描仓库接口

2. 4.2. findAll()

3. 4.3. deleteAll()

4. 4.4. save()

5. 4.5. count()

6. 4.6. exists() 判断是否存在

7. 4.7. existsById()

8. 4.8. findByXXXX

9. 4.9. findAll with OrderBy

1. order by boolean 布尔型数据排序

10. 4.10. findAll with Sort

11. 4.11. FindAll with Pageable

1. PageRequest - springboot 1.x 旧版本

12. 4.12. StartingWith 和 EndingWith

13. 4.13. Between

14. 4.14. Before / After

15. 4.15. @Query

5. 5. mongoTemplate

1. 5.1. Save 保存

2. 5.2. Insert

3. 5.3. updateFirst 修改符合条件第一条记录

4. 5.4. updateMulti 修改符合条件的所有

5. 5.5. 查找并保存

6. 5.6. upsert - 修改符合条件时如果不存在则添加

7. 5.7. 删除

8. 5.8. 查找一条数据

9. 5.9. 查找所有数据

10. 5.10. Query

1. 翻页

2. between

11. 5.11. Criteria

1. is

2. Regex 正则表达式搜索

3. lt 和 gt

4. exists()

5. 包含

12. 5.12. Update

1. set

2. 追加数据

3. 更新数据

4. 删除数据

5. inc

6. update.addToSet

13. 5.13. BasicUpdate

14. 5.14. Sort

15. 5.15. Query + PageRequest

16. 5.16. newAggregation

17. 5.17. 创建索引

18. 5.18. 子对象操作

1. List 类型

6. 6. GeoJson 反序列化

7. 7. FAQ

1. 7.1. location object expected, location array not in correct format; nested exception is com.mongodb.MongoWriteException: location object expected, location array not in correct format

6. 52. Spring Data with Elasticsearch

1. 1. 内嵌 Elasticsearch

1. 1.1. Maven

2. 1.2. src/main/resources/application.properties

3. 1.3. Domain Class

4. 1.4. ElasticsearchRepository

5. 1.5. SearchRestController

6. 1.6. 测试

2. 2. 集群模式

3. 3. Document

4. 4. Elasticsearch 删除操作

5. 5. FAQ

1. 5.1. java.lang.IllegalStateException: Received message from unsupported version: [2.0.0] minimal compatible version is: [5.0.0]

7. 53. Spring boot with Data restful

1. 1. Maven

8. 54. Apache ShardingSphere

1. 1. 微服务集群环境，雪花算法出现重复ID

1. 1.1. 方案一、配置实现

2. 1.2. 方案二、代码实现

5. IV. Spring Security

1. 55. Spring Security

1. 1. Spring boot with Spring security

1. 1.1. Maven

2. 1.2. Reource

3. 1.3. Application

4. 1.4. WebSecurityConfigurer

5. 1.5. RestController

6. 1.6. 测试

7. 1.7. Spring + Security + MongoDB

1. Account

2. AccountRepository

3. WebSecurityConfiguration

2. 2. Spring Security with HTTP Auth

1. 2.1. 默认配置

2. 2.2. 设置用户名和密码

3. 2.3. 禁用 Security

4. 2.4. 设置角色

3. 3. Spring Boot with Web Security(2.x)

1. 3.1. SecurityFilterChain

4. 4. Spring Boot with Web Security(2.x)

1. 4.1. EnableWebSecurity

2. 4.2. Web静态资源

3. 4.3. 正则匹配

4. 4.4. 登陆页面，失败页面，登陆中页面

5. 4.5. CORS

6. 4.6. X-Frame-Options 安全

5. 5. 访问控制列表（Access Control List，ACL）

1. 5.1. antMatchers

2. 5.2. HTTP Auth

3. 5.3. Rest

4. 5.4. hasRole

5. 5.5. hasAnyRole()

6. 5.6. withUser

1. 添加用户

2. 添加多个用户，并指定角色

3. 获取当前用户

6. 6. @PreAuthorize

1. 6.1. hasRole

2. 6.2. hasAnyRole

3. 6.3. 从 HttpServletRequest 返回的 request 变量中判断角色

4. 6.4. getAuthentication() 获得角色

5. 6.5. UserDetailsService

7. 7. Springboot 3 Security + OncePerRequestFilter

1. 7.1. OncePerRequestFilter

2. 7.2. SecurityConfiguration

3. 7.3.

4. 7.4.

6. V. Spring Cloud

1. 57. Spring Cloud

1. 1. Spring Cloud 相关的 application.properties 配置

1. 启用或禁用 bootstrap

2. bootstrap.properties 配置文件

2. 58. Spring Cloud Config

1. 1. Maven 项目 pom.xml 文件

2. 2. Server

1. Maven config 模块

2. Application

3. application.properties

4. Git 仓库

5. 测试服务器

3. 3. Client

1. Maven pom.xml

2. Application

3. bootstrap.properties

4. 测试 client

4. 4. Config 高级配置

1. 仓库配置

1. 分支

2. basedir

3. HTTP Auth

4. 本地git仓库

5. native 本地配置

2. Config server 用户认证

1. Server 配置

1. application.properties

2. Maven

3. 测试是否生效

2. Client 配置

3. 加密敏感数据

4. Spring Cloud Config JDBC Backend

1. Maven pom.xml

2. 数据库表结构

3. Config 服务器

4. application.properties

5. 5. Old

1. Server (Camden.SR5)

2. Client (Camden.SR5)

3. 59. Spring Cloud Consol

1. 1. Spring Cloud Consul 配置

2. 2. Maven 父项目

3. 3. Consul 服务生产者

1. Maven

2. application.properties

3. SpringApplication

4. TestController

4. 4. Consul 服务消费者

1. Maven

2. application.properties

3. SpringApplication

4. TestController

5. 5. Openfeign

1. Maven

2. application.properties

3. SpringApplication

4. Feign 接口

5. TestController

4. 60. Spring Cloud Netflix

1. 1. Eureka Server

1. Maven

2. Application

3. application.properties

4. 检查注册服务器

2. 2. Eureka Client

1. Maven

2. Application

3. RestController

4. application.properties

5. 测试

3. 3. Feign client

1. Maven

2. Application

3. interface

4. application.properties

5. 测试

6. fallback

4. 4. 为 Eureka Server 增加用户认证

1. Maven

2. application.properties

3. Eureka Client

4. Feign Client

5. 5. Eureka 配置项

1. /eureka/apps

2. Eureka instance 配置项

3. Eureka client 配置项

4. Eureka Server配置项

6. 6. ribbon

2. LoadBalancerClient 实例

1. application.properties

2. LoadBalancerClient 获取服务器列表

3. Ribbon 相关配置

1. 内置负载均衡策略

7. 7. 获取 EurekaClient 信息

8. 8. Zuul

1. Maven

2. EnableZuulProxy

3. application.yml

4. 负载均衡配置

5. 61. Openfeign

1. 1. Openfeign 扫描包配置

2. 2. 用户认证

3. 3. 应用实例

4. 4. 配置连接方式

1. httpclient

2. okhttp

5. 5. 配置手册

6. 62. Spring Cloud Gateway

1. 1. Gateway 例子

1. Maven

2. SpringApplication

3. application.yml

4. RouteLocator 方式

2. 2. 路由配置

1. 转发操作

2. URL 参数

7. 63. Spring Cloud Sleuth

1. 1. logback 安装

8. 64. Spring Cloud with Kubernetes

1. 1. Config

1. Maven 依赖

2. Spring Cloud 配置文件

3. 程序文件

1. SpringBootApplication 启动文件

2. 配置类

3. 控制器

4. Kubernetes 编排脚本

5. 测试

2. 2. 注册发现

1. Maven 父项目

2. provider

1. Maven 依赖

2. Springboot 启动类

3. 控制器

4. application.properties 配置文件

5. Kubernetes provider 编排脚本

3. consumer

1. Maven 依赖

2. Springboot 启动类

3. 控制器

4. FeignClient 接口

5. application.properties 配置文件

6. Kubernetes consumer 编排脚本

4. 测试

9. 65. Spring Cloud Alibaba

1. 1. 安装 Nacos

1. Docker 安装 Nacos

2. Kubernetes 安装 Nacos

3. IP限制，白名单

4. 防火墙配置

2. 2. Kubernetes 部署微服务

1. pom.xml 中加入 docker 插件

2. 容器启动脚本

3. 构建 docker 镜像

4. 编排 kubernetes 容器

5. 启动指定 nacos

3. 3. Nacos 配置中心/注册中心代码实例

1. Maven

2. SpringBootApplication

3. ConfigController

4. 配置文件

4. 4. FAQ

1. 禁用 Nacos

2. 禁止注册

3. Failed to bind properties under 'server.tomcat.basedir' to java.io.File:

4. 不读取 bootstrap.yaml 文件

5. WARN [com.alibaba.nacos.client.naming:177] [,] - out of date data received, old-t: 1665711914993, new-t: 1665711902390

6. User limit of inotify instances reached or too many open files

7. 开启权限

8. ERROR Whitelabel

10. 66. FAQ

1. 1. Cannot execute request on any known server

2. 2. @EnableDiscoveryClient与@EnableEurekaClient 区别

3. 3. Feign请求超时

4. 4. 已停止的微服务节点注销慢或不注销

5. 5. Feign 启动出错 PathVariable annotation was empty on param 0.

6. 6. Feign 提示 Consider defining a bean of type 'common.feign.Cms' in your configuration.

7. 7. Load balancer does not have available server for client

8. 8. Eureka Client (Dalston.SR1)

1. Maven

2. Application

3. RestController

4. application.properties

5. 测试

9. 9. Config Server(1.3.1.RELEASE)

1. Server

1. Maven

2. Application

3. application.properties

4. Git 仓库

5. 测试服务器

2. Client

1. Maven pom.xml

2. Application

3. bootstrap.properties

4. 测试 client

10. 10. feign.RetryableException: Read timed out executing

7. 67. Tomcat Spring 运行环境

1. 1. Maven

2. 2. Spring MVC configuration

3. 3. Tomcat

4. 4. 集成 Mybatis

1. 4.1. pom.xml

2. 4.2. properties

3. 4.3. dataSource

4. 4.4. SqlSessionFactory

5. 4.5. Mapper 扫描

6. 4.6. Mapper 单一class映射

7. 4.7. Service

8. 4.8. 测试实例

8. 68. 杂项 Miscellaneous

1. 1. URL 拼装/解析

2. 2. ServletUriComponentsBuilder

3. 3. URL 路径相关

9. 70. FAQ

1. 1. org.hibernate.dialect.Oracle10gDialect does not support identity key generation

2. 2. No identifier specified for entity

3. 3. Could not read document: Invalid UTF-8 middle byte 0xd0

4. 4. java.sql.SQLRecoverableException: IO Error: The Network Adapter could not establish the connection

5. 5. Field javaMailSender in cn.netkiller.rest.EmailRestController required a bean of type 'org.springframework.mail.javamail.JavaMailSender' that could not be found.

6. 6. org.postgresql.util.PSQLException: FATAL: no pg_hba.conf entry for host "172.16.0.3", user "test", database "test ", SSL off

7. 7. Spring boot 怎样显示执行的SQL语句

8. 8. Cannot determine embedded database driver class for database type NONE

9. 9. Spring boot / Spring cloud 时区差8个小时

10. 10. @Value 取不到值

11. 11. Spring boot 2.1.0

12. 12. Field authenticationManager in cn.netkiller.oauth2.config.AuthorizationServerConfigurer required a bean of type 'org.springframework.security.authentication.AuthenticationManager' that could not be found.

13. 13. 打印 Bean 信息

14. 14. The dependencies of some of the beans in the application context form a cycle

15. 15. no main manifest attribute, in /srv/job-admin.jar

10. A. 附录

1. 1. Springboot example

范例清单

1. 11.1. Spring boot with Velocity template (pom.xml)

2. 15.1. Example Spring boot with Oracle

3. 3. RedisTemplate

4. 23.1. Spring boot with Apache kafka.

5. 23.2. Spring boot with Apache kafka.

6. 23.3. Test Spring Kafka

7. 40.1. Spring boot with Email (pom.xml)

8. 50.1. Spring Data Redis Example

9. 51.1. Spring Data MongoDB - springframework-servlet.xml

10. 66.1. Share feign interface.

11. 67.1. MyBatis

12. 69.1. MyBatis

Netkiller Spring Cloud 手札

Spring Cloud Cookbook（2024版）

Mr. Neo Chan, 陈景峯(BG7NYT)

中国广东省深圳市望海路半岛城邦三期

518067

+86 13113668890

<netkiller@msn.com>

电子书最近一次更新于 2024-01-31 09:14:01

版权 © 2015-2024 Neo Chan

版权声明

转载请与作者联系，转载时请务必标明文章原始出处和作者信息及本声明。

http://www.netkiller.cn

http://netkiller.github.io

http://netkiller.sourceforge.net

微信公众号: netkiller

微信：13113668890 请注明“读者”

QQ：13721218 请注明“读者”

QQ群：128659835 请注明“读者”

知乎专栏

2017-11

我的系列文档

编程语言

Netkiller Architect 手札

Netkiller Developer 手札

Netkiller Java 手札

Netkiller Spring 手札

Netkiller Android 手札

Netkiller Python 手札

Netkiller Testing 手札

Netkiller Cryptography 手札

Netkiller Perl 手札

Netkiller Docbook 手札

Netkiller Project 手札

Netkiller Database 手札

Netkiller PHP 手札

致读者

Netkiller 系列手札 已经被 Github 收录，并备份保存在北极地下250米深的代码库中，备份会保留1000年。

Preserving open source software for future generations

The world is powered by open source software. It is a hidden cornerstone of modern civilization, and the shared heritage of all humanity.

The GitHub Arctic Code Vault is a data repository preserved in the Arctic World Archive (AWA), a very-long-term archival facility 250 meters deep in the permafrost of an Arctic mountain.

We are collaborating with the Bodleian Library in Oxford, the Bibliotheca Alexandrina in Egypt, and Stanford Libraries in California to store copies of 17,000 of GitHub’s most popular and most-depended-upon projects—open source’s “greatest hits”—in their archives, in museum-quality cases, to preserve them for future generations.

https://archiveprogram.github.com/arctic-vault/

自述

《Netkiller 系列 手札》是一套免费系列电子书，netkiller 是nickname 从1999 开使用至今，“手札” 是札记，手册的含义。

2003年之前我还是以文章形式在BBS上发表各类技术文章，后来发现文章不够系统，便尝试写长篇技术文章加上章节目录等等。随着内容增加，不断修订，开始发布第一版，第二版......

IT知识变化非常快，而且具有时效性，这样发布非常混乱，经常有读者发现第一版例子已经过时，但他不知道我已经发布第二版。

我便有一种想法，始终维护一个文档，不断更新，使他保持较新的版本不过时。

第一部电子书是《PostgreSQL 实用实例参考》开始我使用 Microsoft Office Word 慢慢随着文档尺寸增加 Word 开始表现出力不从心。

我看到PostgreSQL 中文手册使用SGML编写文档，便开始学习Docbook SGML。使用Docbook写的第一部电子书是《Netkiller Postfix Integrated Solution》这是Netkiller 系列手札的原型。

至于“手札”一词的来历，是因为我爱好摄影，经常去一个台湾摄影网站，名字就叫“摄影家手札”。

由于硬盘损坏数据丢失 《Netkiller Postfix Integrated Solution》 的 SGML文件已经不存在; Docbook SGML存在很多缺陷UTF-8支持不好，转而使用Docbook XML.

目前技术书籍的价格一路飙升，动则￥80，￥100，少则￥50，￥60. 技术书籍有时效性，随着技术的革新或淘汰，大批书记成为废纸垃圾。并且这些书技术内容雷同，相互抄袭，质量越来越差，甚至里面给出的例子错误百出，只能购买影印版，或者翻译的版本。

在这种背景下我便萌生了自己写书的想法，资料主要来源是我的笔记与例子。我并不想出版，只为分享，所有我制作了基于CC License 发行的系列电子书。

本书注重例子，少理论（捞干货），只要你对着例子一步一步操作，就会成功，会让你有成就感并能坚持学下去，因为很多人遇到障碍就会放弃，其实我就是这种人，只要让他看到希望，就能坚持下去。

1. 写给读者

为什么写这篇文章

有很多想法,工作中也用不到所以未能实现，所以想写出来,和大家分享.有一点写一点,写得也不好,只要能看懂就行,就当学习笔记了.

开始零零碎碎写过一些文档，也向维基百科供过稿，但维基经常被ZF封锁，后来发现sf.net可以提供主机存放文档，便做了迁移。并开始了我的写作生涯。

这篇文档是作者20年来对工作的总结,是作者一点一滴的积累起来的，有些笔记已经丢失，所以并不完整。

因为工作太忙整理比较缓慢。目前的工作涉及面比较窄所以新文档比较少。

我现在花在技术上的时间越来越少，兴趣转向摄影，无线电。也想写写摄影方面的心得体会。

写作动力:

曾经在网上看到外国开源界对中国的评价，中国人对开源索取无度，但贡献却微乎其微.这句话一直记在我心中，发誓要为中国开源事业做我仅有的一点微薄贡献

另外写文档也是知识积累，还可以增加在圈内的影响力.

人跟动物的不同,就是人类可以把自己学习的经验教给下一代人.下一代在上一代的基础上再创新,不断积累才有今天.

所以我把自己的经验写出来,可以让经验传承

没有内容的章节:

目前我自己一人维护所有文档，写作时间有限，当我发现一个好主题就会加入到文档中，待我有时间再完善章节，所以你会发现很多章节是空无内容的.

文档目前几乎是流水帐试的写作，维护量很大，先将就着看吧.

我想到哪写到哪,你会发现文章没一个中心,今天这里写点,明天跳过本章写其它的.

文中例子绝对多,对喜欢复制然后粘贴朋友很有用,不用动手写,也省时间.

理论的东西,网上大把,我这里就不写了,需要可以去网上查.

我爱写错别字,还有一些是打错的,如果发现请指正.

文中大部分试验是在Debian/Ubuntu/Redhat AS上完成.

写给读者

至读者：

我不知道什么时候，我不再更新文档或者退出IT行业去从事其他工作，我必须给这些文档找一个归宿，让他能持续更新下去。

我想捐赠给某些基金会继续运转，或者建立一个团队维护它。

我用了20年时间坚持不停地写作，持续更新，才有今天你看到的《Netkiller 手扎》系列文档，在中国能坚持20年，同时没有任何收益的技术类文档，是非常不容易的。

有很多时候想放弃，看到外国读者的支持与国内社区的影响，我坚持了下来。

中国开源事业需要各位参与，不要成为局外人，不要让外国人说：中国对开源索取无度，贡献却微乎其微。

我们参与内核的开发还比较遥远，但是进个人能力，写一些文档还是可能的。

系列文档

下面是我多年积累下来的经验总结，整理成文档供大家参考:

Netkiller Architect 手札

Netkiller Developer 手札

Netkiller PHP 手札

Netkiller Python 手札

Netkiller Testing 手札

Netkiller Cryptography 手札

Netkiller Linux 手札

Netkiller FreeBSD 手札

Netkiller Shell 手札

Netkiller Security 手札

Netkiller Web 手札

Netkiller Monitoring 手札

Netkiller Storage 手札

Netkiller Mail 手札

Netkiller Docbook 手札

Netkiller Version 手札

Netkiller Database 手札

Netkiller PostgreSQL 手札

Netkiller MySQL 手札

Netkiller NoSQL 手札

Netkiller LDAP 手札

Netkiller Network 手札

Netkiller Cisco IOS 手札

Netkiller H3C 手札

Netkiller Multimedia 手札

Netkiller Management 手札

Netkiller Spring 手札

Netkiller Perl 手札

Netkiller Amateur Radio 手札

2. 作者简介

陈景峯 (ㄔㄣ ㄐ丨ㄥ ㄈㄥ)

Nickname：netkiller | English name: Neo chen | Nippon name: ちんけいほう (音訳) | Korean name: 천징봉 | Thailand name: ภูมิภาพภูเขา | Vietnam: Trần Cảnh Phong

Callsign: BG7NYT | QTH: ZONE CQ24 ITU44 ShenZhen, China

程序猿，攻城狮，挨踢民工, Full Stack Developer, UNIX like Evangelist, 业余无线电爱好者（呼号：BG7NYT）,户外运动，山地骑行以及摄影爱好者。

《Netkiller 系列 手札》的作者

成长阶段

1981年1月19日(庚申年腊月十四)出生于黑龙江省青冈县建设乡双富大队第一小队

1989年9岁随父母迁居至黑龙江省伊春市，悲剧的天朝教育，不知道那门子归定，转学必须降一级，我本应该上一年级，但体制让我上学前班，那年多都10岁了

1995年小学毕业，体制规定借读要交3000两银子(我曾想过不升初中)，亲戚单位分楼告别平房，楼里没有地方放东西，把2麻袋书送给我，无意中发现一本电脑书BASIC语言，我竟然看懂了，对于电脑知识追求一发而不可收，后面顶零花钱，压岁钱主要用来买电脑书《MSDOS 6.22》《新编Unix实用大全》《跟我学Foxbase》。。。。。。

1996年第一次接触UNIX操作系统，BSD UNIX, Microsoft Xinux(盖茨亲自写的微软Unix，知道的人不多)

1997年自学Turbo C语言，苦于没有电脑，后来学校建了微机室才第一次使用QBASIC(DOS 6.22 自带命令)，那个年代只能通过软盘拷贝转播，Trubo C编译器始终没有搞到，

1997年第一次上Internet网速只有9600Bps, 当时全国兴起各种信息港域名格式是www.xxxx.info.net, 访问的第一个网站是NASA下载了很多火星探路者拍回的照片，还有“淞沪”sohu的前身

1998~2000年在哈尔滨学习计算机，充足的上机时间，但老师让我们练打字（明伦五笔/WT）打字不超过80个/每分钟还要强化训练，不过这个给我的键盘功夫打了好底。

1999年学校的电脑终于安装了光驱，在一张工具盘上终于找到了Turbo C, Borland C++与Quick Basic编译器，当时对VGA图形编程非常感兴趣，通过INT33中断控制鼠标，使用绘图函数模仿windows界面。还有操作 UCDOS 中文字库，绘制矢量与点阵字体。

2000年沉迷于Windows NT与Back Office各种技术，神马主域控制器，DHCP，WINS，IIS，域名服务器，Exchange邮件服务器，MS Proxy, NetMeeting...以及ASP+MS SQL开发；用56K猫下载了一张LINUX。ISO镜像，安装后我兴奋的24小时没有睡觉。

职业生涯

2001 年来深圳进城打工,成为一名外来务工者. 在一个4人公司做PHP开发，当时PHP的版本是2.0, 开始使用Linux Redhat 6.2.当时很多门户网站都是用FreeBSD,但很难搞到安装盘，在网易社区认识了一个网友,从广州给我寄了一张光盘，FreeBSD 3.2

2002 年我发现不能埋头苦干,还要学会"做人".后辗转广州工作了半年，考了一个Cisco CCNA认证。回到深圳重新开始，在车公庙找到一家工作做Java开发

2003 年这年最惨,公司拖欠工资16000元,打过两次官司2005才付清.

2004 年开始加入分布式计算团队,目前成绩，工作仍然是Java开发并且开始使用PostgreSQL数据库。

2004-10月开始玩户外和摄影

2005-6月成为中国无线电运动协会会员,呼号BG7NYT,进了一部Yaesu FT-60R手台。公司的需要转回PHP与MySQL，相隔几年发现PHP进步很大。在前台展现方面无人能敌，于是便前台使用PHP，后台采用Java开发。

2006 年单身生活了这么多年,终于找到归宿. 工作更多是研究PHP各种框架原理

2007 物价上涨,金融危机，休息了4个月（其实是找不到工作）,关外很难上439.460中继，搞了一台Yaesu FT-7800.

2008 终于找到英文学习方法， 《Netkiller Developer 手札》，《Netkiller Document 手札》

2008-8-8 08:08:08 结婚,后全家迁居湖南省常德市

2009《Netkiller Database 手札》,2009-6-13学车，年底拿到C1驾照

2010 对电子打击乐产生兴趣，计划学习爵士鼓。由于我对Linux热爱，我轻松的接管了公司的运维部，然后开发运维两把抓。我印象最深刻的是公司一次上架10个机柜，我们用买服务器纸箱的钱改善伙食。我将40多台服务器安装BOINC做压力测试，获得了中国第二的名次。

2011 平凡的一年，户外运动停止，电台很少开，中继很少上，摄影主要是拍女儿与家人，年末买了一辆山地车

2012 对油笔画产生了兴趣，活动基本是骑行银湖山绿道，

2013 开始学习民谣吉他，同时对电吉他也极有兴趣；最终都放弃了。这一年深圳开始推数字中继2013-7-6日入手Motorola MOTOTRBO XIR P8668，Netkiller 系列手札从Sourceforge向Github迁移；年底对MYSQL UDF，Engine与PHP扩展开发产生很浓的兴趣，拾起遗忘10+年的C，写了几个mysql扩展（图片处理，fifo管道与ZeroMQ），10月份入Toyota Rezi 2.5V并写了一篇《攻城狮的苦逼选车经历》

2014-9-8 在淘宝上买了一架电钢琴 Casio Privia PX-5S pro 开始陪女儿学习钢琴，由于这家钢琴是合成器电钢，里面有打击乐，我有对键盘鼓产生了兴趣。

2014-10-2号罗浮山两日游，对中国道教文化与音乐产生了兴趣，10月5号用了半天时间学会了简谱。10月8号入Canon 5D Mark III + Canon Speedlite 600EX-RT香港过关被查。

2014-12-20号对乐谱制作产生兴趣（https://github.com/SheetMusic/Piano），给女儿做了几首钢琴伴奏曲，MuseScore制谱然后生成MIDI与WAV文件。

2015-09-01 晚饭后拿起爵士鼓基础教程尝试在Casio Privia PX-5S pro演练，经过反复琢磨加上之前学钢琴的乐理知识，终于在02号晚上，打出了简单的基本节奏，迈出了第一步。

2016 对弓箭（复合弓）产生兴趣，无奈兲朝法律法规不让玩。每周游泳轻松1500米无压力，年底入 xbox one s 和 Yaesu FT-2DR, 同时开始关注功放音响这块

2017 7月9号入 Yamaha RX-V581 功放一台，连接Xbox打游戏爽翻了，入Kindle电子书，计划学习蝶泳，果断放弃运维和开发知识体系转攻区块链。

2018 从溪山美地搬到半岛城邦，丢弃了多年攒下的家底。11 月开始玩 MMDVM，使用 Yaesu FT-7800 发射，连接MMDVM中继板，树莓派，覆盖深圳湾，散步骑车通联两不误。

2019 卖了常德的房子，住了5次院，哮喘反复发作，决定停止电子书更新，兴趣转到知乎，B站

2020 准备找工作

职业生涯路上继续打怪升级

3. 如何获得文档

下载 Netkiller 手札 (epub,kindle,chm,pdf)

EPUB https://github.com/netkiller/netkiller.github.io/tree/master/download/epub

MOBI https://github.com/netkiller/netkiller.github.io/tree/master/download/mobi

PDF https://github.com/netkiller/netkiller.github.io/tree/master/download/pdf

CHM https://github.com/netkiller/netkiller.github.io/tree/master/download/chm

通过 GIT 镜像整个网站

https://github.com/netkiller/netkiller.github.com.git

$ git clone https://github.com/netkiller/netkiller.github.com.git

镜像下载

整站下载

wget -m http://www.netkiller.cn/index.html

指定下载

wget -m wget -m http://www.netkiller.cn/linux/index.html

Yum 下载文档

获得光盘介质，RPM包，DEB包，如有特别需要，请联系我

YUM 在线安装电子书

http://netkiller.sourceforge.net/pub/repo/

# cat >> /etc/yum.repos.d/netkiller.repo <<EOF

[netkiller]

name=Netkiller Free Books

baseurl=http://netkiller.sourceforge.net/pub/repo/

enabled=1

gpgcheck=0

gpgkey=

EOF

查找包

# yum search netkiller

netkiller-centos.x86_64 : Netkiller centos Cookbook

netkiller-cryptography.x86_64 : Netkiller cryptography Cookbook

netkiller-docbook.x86_64 : Netkiller docbook Cookbook

netkiller-linux.x86_64 : Netkiller linux Cookbook

netkiller-mysql.x86_64 : Netkiller mysql Cookbook

netkiller-php.x86_64 : Netkiller php Cookbook

netkiller-postgresql.x86_64 : Netkiller postgresql Cookbook

netkiller-python.x86_64 : Netkiller python Cookbook

netkiller-version.x86_64 : Netkiller version Cookbook

安装包

yum install netkiller-docbook

4. 打赏（Donations）

If you like this documents, please make a donation to support the authors' efforts. Thank you!

您可以通过微信，支付宝，贝宝给作者打赏。

银行(Bank)

招商银行(China Merchants Bank)

开户名：陈景峰

账号：9555500000007459

微信（Wechat）

支付宝（Alipay）

PayPal Donations

https://www.paypal.me/netkiller

5. 联系方式

主站 http://www.netkiller.cn/

备用 http://netkiller.github.io/

繁体网站 http://netkiller.sourceforge.net/

联系作者

Mobile: +86 13113668890

Email: netkiller@msn.com

QQ群: 128659835 请注明“读者”

QQ: 13721218

ICQ: 101888222

注：请不要问我安装问题！

博客 Blogger

知乎专栏 https://zhuanlan.zhihu.com/netkiller

LinkedIn: http://cn.linkedin.com/in/netkiller

OSChina: http://my.oschina.net/neochen/

Facebook: https://www.facebook.com/bg7nyt

Flickr: http://www.flickr.com/photos/bg7nyt/

Disqus: http://disqus.com/netkiller/

solidot: http://solidot.org/~netkiller/

SegmentFault: https://segmentfault.com/u/netkiller

Reddit: https://www.reddit.com/user/netkiller/

Digg: http://www.digg.com/netkiller

Twitter: http://twitter.com/bg7nyt

weibo: http://weibo.com/bg7nyt

Xbox club

我的 xbox 上的ID是 netkiller xbox，我创建了一个俱乐部 netkiller 欢迎加入。

Radio

CQ CQ CQ DE BG7NYT:

如果这篇文章对你有所帮助,请寄给我一张QSL卡片, qrz.cn or qrz.com or hamcall.net

Personal Amateur Radiostations of P.R.China

ZONE CQ24 ITU44 ShenZhen, China

Best Regards, VY 73! OP. BG7NYT

守听频率 DMR 438.460 -8 Color 12 Slot 2 Group 46001

守听频率 C4FM 439.360 -5 DN/VW

MMDVM Hotspot:

Callsign: BG7NYT QTH: Shenzhen, China

YSF: YSF80337 - CN China 1 - W24166/TG46001

DMR: BM_China_46001 - DMR Radio ID: 4600441

部分 I. Spring Boot

1. Spring boot with Redis

1.1. Spring boot with Redis

maven

<dependency>

<groupId>org.springframework.boot</groupId>

<artifactId>spring-boot-starter-data-redis</artifactId>

</dependency>

application.properties

spring.redis.database=10

spring.redis.host=localhost

spring.redis.port=6379

spring.redis.password=

spring.redis.pool.max-active=8

spring.redis.pool.max-wait=-1

spring.redis.pool.max-idle=8

spring.redis.pool.min-idle=0

spring.redis.timeout=0

JUnit

@RunWith(SpringJUnit4ClassRunner.class)

@SpringApplicationConfiguration(Application.class)

public class ApplicationTests {

@Autowired

private StringRedisTemplate stringRedisTemplate;

@Test

public void test() throws Exception {

// 保存字符串

stringRedisTemplate.opsForValue().set("neo", "chen");

Assert.assertEquals("chen", stringRedisTemplate.opsForValue().get("neo"));

}

}

Controller

stringRedisTemplate模板用于存储key,value为字符串的数据

@Autowired

private StringRedisTemplate stringRedisTemplate;

@RequestMapping("/test")

@ResponseBody

public String test() {

String message = "";

stringRedisTemplate.opsForValue().set("hello", "world");

message = stringRedisTemplate.opsForValue().get("hello");

return message;

}

等同于

@Autowired

private RedisTemplate<String, String> redisTemplate;

例 3. RedisTemplate

@Autowired

private RedisTemplate<String, String> redisTemplate;

public List<Protocol> getProtocol() {

List<Protocol> protocols = new ArrayList<Protocol>();

Gson gson = new Gson();

Type type = new TypeToken<List<Protocol>>(){}.getType();

redisTemplate.setKeySerializer(new StringRedisSerializer());

redisTemplate.setValueSerializer(new StringRedisSerializer());

String cacheKey = String.format("%s:%s", this.getClass().getName(), Thread.currentThread().getStackTrace()[1].getMethodName());

long expireTime = 5;

if(redisTemplate.hasKey(cacheKey)){

String cacheValue = redisTemplate.opsForValue().get(cacheKey);

System.out.println(cacheValue);

protocols = gson.fromJson(cacheValue, type);

}else{

Protocol protocol = new Protocol();

protocol.setRequest(new Date().toString());

protocols.add(protocol);

String jsonString = gson.toJson(protocols, type);

System.out.println( jsonString );

redisTemplate.opsForValue().set(cacheKey, jsonString);

redisTemplate.expire(cacheKey, expireTime, TimeUnit.SECONDS);

}

return protocols;

}

1.2.

获取过期时间

@Autowired

private RedisTemplate<String, String> redisTemplate;

Long ttl = redisTemplate.getExpire(String.format("lock:%s", device));

列表操作

ListOperations

public class Example {

// inject the actual template

@Autowired

private RedisTemplate<String, String> template;

// inject the template as ListOperations

// can also inject as Value, Set, ZSet, and HashOperations

@Resource(name="redisTemplate")

private ListOperations<String, String> listOps;

public void addLink(String userId, URL url) {

listOps.leftPush(userId, url.toExternalForm());

// or use template directly

redisTemplate.boundListOps(userId).leftPush(url.toExternalForm());

}

}

1.3. Redis Pub/Sub

Redis配置类

package cn.netkiller.wallet.config;

import org.springframework.context.annotation.Bean;

import org.springframework.context.annotation.Configuration;

import org.springframework.data.redis.connection.RedisConnectionFactory;

import org.springframework.data.redis.core.StringRedisTemplate;

import org.springframework.data.redis.listener.ChannelTopic;

import org.springframework.data.redis.listener.RedisMessageListenerContainer;

import org.springframework.data.redis.listener.adapter.MessageListenerAdapter;

import cn.netkiller.wallet.redis.RedisMessageSubscriber;

@Configuration

public class RedisConfig {

public RedisConfig() {

}

@Bean

public StringRedisTemplate stringRedisTemplate(RedisConnectionFactory connectionFactory) {

StringRedisTemplate redisTemplate = new StringRedisTemplate();

redisTemplate.setConnectionFactory(connectionFactory);

return redisTemplate;

}

@Bean

public MessageListenerAdapter messageListener() {

return new MessageListenerAdapter(new RedisMessageSubscriber());

}

@Bean

public ChannelTopic topic() {

return new ChannelTopic("demo");

}

@Bean

public RedisMessageListenerContainer redisContainer(RedisConnectionFactory connectionFactory, MessageListenerAdapter messageListener) {

RedisMessageListenerContainer container = new RedisMessageListenerContainer();

container.setConnectionFactory(connectionFactory);

container.addMessageListener(messageListener(), topic());

container.addMessageListener(messageListener(), new ChannelTopic("test"));

return container;

}

}

订阅和发布类

package cn.netkiller.wallet.redis;

import java.nio.charset.StandardCharsets;

import org.springframework.data.redis.connection.Message;

import org.springframework.data.redis.connection.MessageListener;

public class RedisMessageSubscriber implements MessageListener {

public void onMessage(final Message message, final byte[] pattern) {

System.out.println("Topic : " + new String(message.getChannel(), StandardCharsets.UTF_8));

System.out.println("Message : " + message.toString());

}

}

package cn.netkiller.wallet.redis;

import org.springframework.data.redis.core.StringRedisTemplate;

import org.springframework.data.redis.listener.ChannelTopic;

public class RedisMessagePublisher {

private final StringRedisTemplate redisTemplate;

private final ChannelTopic topic;

public RedisMessagePublisher(StringRedisTemplate redisTemplate, ChannelTopic topic) {

this.redisTemplate = redisTemplate;

this.topic = topic;

}

public void publish(String message) {

redisTemplate.convertAndSend(topic.getTopic(), message);

}

}

消息发布演示

@Autowired

private StringRedisTemplate stringRedisTemplate;

@GetMapping("/pub/demo")

public String pub() {

RedisMessagePublisher publisher = new RedisMessagePublisher(stringRedisTemplate, new ChannelTopic("demo"));

String message = "Message " + UUID.randomUUID();

publisher.publish(message);

return message;

}

@GetMapping("/pub/test")

public String pub(@RequestParam String message) {

RedisMessagePublisher publisher = new RedisMessagePublisher(stringRedisTemplate, new ChannelTopic("test"));

publisher.publish(message);

return message;

}

1.4. Spring Redis Lock

Maven 依赖

<dependency>

<groupId>org.springframework.boot</groupId>

<artifactId>spring-boot-starter-integration</artifactId>

</dependency>

<dependency>

<groupId>org.springframework.integration</groupId>

<artifactId>spring-integration-redis</artifactId>

</dependency>

配置锁

package cn.netkiller.config;

import org.springframework.context.annotation.Bean;

import org.springframework.context.annotation.Configuration;

import org.springframework.data.redis.connection.RedisConnectionFactory;

import org.springframework.integration.redis.util.RedisLockRegistry;

@Configuration

public class RedisLockRegistryConfiguration {

@Bean

public RedisLockRegistry redisLockRegistry(RedisConnectionFactory redisConnectionFactory) {

return new RedisLockRegistry(redisConnectionFactory, "netkiller-lock");

}

}

@Bean(destroyMethod = "destroy")

public RedisLockRegistry redisLockRegistry(RedisConnectionFactory redisConnectionFactory) {

return new RedisLockRegistry(redisConnectionFactory, "neo-lock",

TimeUnit.MINUTES.toMillis(10));

}

使用方法

@Autowired

private RedisLockRegistry redisLockRegistry;

Lock lock = redisLockRegistry.obtain(device);

if (lock.tryLock()) {

try {

// manipulate protected state

} finally {

lock.unlock();

}

} else {

// perform alternative actions

}

@Autowired

private RedisLockRegistry redisLockRegistry;

Lock lock = redisLockRegistry.obtain(key);

boolean locked = false;

try {

locked = lock.tryLock();

if (!locked) {

// 没有获取到锁的逻辑

}

// 获取锁的逻辑

} finally {

if (locked) {

lock.unlock();

}

}

如果没有上锁，上锁后返回 true 状态。如果已经上锁阻塞等待10秒，然后再返回锁状态

public boolean isLock(String device) {

Lock lock = redisLockRegistry.obtain(device);

boolean status = false;

try {

status = lock.tryLock(10, TimeUnit.SECONDS);

} catch (Exception e) {

log.info(e.getMessage());

}

log.warn("status: {} <<<<<<<<<<", status);

return status;

}

1.5. Sprint boot with Redisson

Springboot 3.x

Springboot 2.1

注意：排除 redisson-spring-data-23，引用 redisson-spring-data-21

<dependency>

<groupId>org.redisson</groupId>

<artifactId>redisson-spring-boot-starter</artifactId>

<version>3.14.0</version>

<exclusions>

<exclusion>

<groupId>org.redisson</groupId>

<artifactId>redisson-spring-data-23</artifactId>

</exclusion>

</exclusions>

</dependency>

<dependency>

<groupId>org.redisson</groupId>

<artifactId>redisson-spring-data-21</artifactId>

<version>3.14.0</version>

</dependency>

第 1 章 Spring 开发环境

1. Java 开发环境

[root@localhost ~]# dnf install java-latest-openjdk-devel

[root@localhost ~]# dnf install maven

2. 安装 Spring Tool Suite

https://spring.io/tools/sts/

环境 Eclipse Jee Neon

进入菜单 Help -> Marketpalce...

索搜 Spring Tool Suite 注意版本号

点击Confirm按钮

点击Finish按钮，等候漫长的下载，同时Progress窗口中显示 Installing Software，安装成功会提示重新启动Eclipse.

点击 Yes 按钮重启 Eclipse

3. Dashboard

进入菜单 Help -> Dashboard

4. Spring Initializr - Bootstrap your application

https://start.spring.io

第 2 章 Spring Boot Quick start

1. 创建项目

curl https://start.spring.io/starter.tgz  \

-d artifactId=creds-example-server \

-d dependencies=security,web \

-d language=java \

-d type=maven-project \

-d baseDir=example-server \

| tar -xzvf -

2. pom.xml

<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>

<groupId>api.netkiller.cn</groupId>

<artifactId>api.netkiller.cn</artifactId>

<version>0.0.1-SNAPSHOT</version>

<name>Skyline</name>

<description>skylinechencf@gmail.com</description>

<parent>

<groupId>org.springframework.boot</groupId>

<artifactId>spring-boot-starter-parent</artifactId>

<version>1.4.0.RELEASE</version>

</parent>

<dependencies>

<dependency>

<groupId>org.springframework.boot</groupId>

<artifactId>spring-boot-starter-web</artifactId>

</dependency>

</dependencies>

<build>

<sourceDirectory>src</sourceDirectory>

<plugins>

<plugin>

<artifactId>maven-compiler-plugin</artifactId>

<version>3.3</version>

<configuration>

<source />

<target />

</configuration>

</plugin>

</plugins>

</build>

</project>

3. Controller

package hello;

import org.springframework.boot.*;

import org.springframework.boot.autoconfigure.*;

import org.springframework.stereotype.*;

import org.springframework.web.bind.annotation.*;

@Controller

@EnableAutoConfiguration

public class SampleController {

@RequestMapping("/")

@ResponseBody

String home() {

return "Hello World!";

}

public static void main(String[] args) throws Exception {

SpringApplication.run(SampleController.class, args);

}

}

测试

curl http://127.0.0.1:8080/

4. Springboot with Maven

spring-boot-maven-plugin 插件

resource

将 resource 添加应用程序

<build>

<resources>

<resource>

<directory>src/main/java/resources</directory>

<filtering>true</filtering>

<excludes>

<exclude>*.jks</exclude>

</excludes>

</resource>

</resources>

<plugins>

<plugin>

<groupId>org.springframework.boot</groupId>

<artifactId>spring-boot-maven-plugin</artifactId>

<configuration>

<addResources>true</addResources>

</configuration>

</plugin>

</plugins>

</build>

Maven run

$ mvn spring-boot:run

$ mvn -P prod spring-boot:run

-P 指定 Maven 的 profile，如果指定 Springboot 的 profiles 请使用 -Drun.profiles=prod

$ mvn spring-boot:run -Drun.profiles=prod

打包后，使用jar包运行

$ mvn verify

$ mvn package

$ java -jar target/api.netkiller.cn-0.0.1-SNAPSHOT.jar

Spring Boot maven 插件 build-image

Spring Boot 构建 Docker 镜像，你不需要写 Dockerfile，plugin 帮你完成。

只需要简单的执行：

mvn spring-boot:build-image

执行完成后会看到成功提示信息：

[INFO] Successfully buit image 'docker.io/library/demo:0.0.1-SNAPSHOT'

运行容器测试：

docker run -p 8000:8080 -t demo:0.0.1-SNAPSHOT

注意：这里映射的本机端口是8000。

curl http://localhost:8000/

生成项目信息

mvn spring-boot:build-info

neo@MacBook-Pro-Neo ~/workspace/microservice/config % mvn spring-boot:build-info

第 3 章 SpringApplication

import org.springframework.boot.SpringApplication;

import org.springframework.boot.autoconfigure.EnableAutoConfiguration;

import org.springframework.boot.autoconfigure.SpringBootApplication;

import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;

import org.springframework.context.annotation.ComponentScan;

@SpringBootApplication

@EnableAutoConfiguration(exclude={DataSourceAutoConfiguration.class})

@ComponentScan({"cn.netkiller.controller"})

public class Application {

public static void main(String[] args) {

SpringApplication.run(Application.class, args);

}

}

1. 运行 Spring boot 项目

Linux systemd

/etc/systemd/system/spring.service

####################################################

# Homepage: http://netkiller.github.io

# Author: netkiller<netkiller@msn.com>

# Script: https://github.com/oscm/shell

# Date: 2015-11-03

####################################################

[Unit]

Description=Spring Boot Application

After=network.target

[Service]

User=www

Group=www

Type=oneshot

WorkingDirectory=/www/netkiller.cn/api.netkiller.cn

ExecStart=/usr/bin/java -jar your_jar_file.jar --spring.config.location=appliction-production.properties --spring.profiles.active=profile

#ExecStop=pkill -9 -f

RemainAfterExit=yes

[Install]

WantedBy=multi-user.target

传统 init.d 脚本

#!/bin/bash

##############################################

# Author: netkiller<netkiller@msn.com>

# Homepage: http://www.netkiller.cn

# Date: 2017-02-08

# $Author$

# $Id$

##############################################

# chkconfig: 345 100 02

# description: Spring boot application

# processname: springbootd

# File : springbootd

##############################################

BASEDIR="/www/netkiller.cn/api.netkiller.cn"

JAVA_HOME=/srv/java

JAVA_OPTS="-server -Xms2048m -Xmx8192m -Djava.security.egd=file:/dev/./urandom"

PACKAGE="api.netkiller.cn-0.0.2-release.jar"

CONFIG="--spring.config.location=$BASEDIR/application.properties"

USER=www

##############################################

NAME=springbootd

PROG="$JAVA_HOME/bin/java $JAVA_OPTS -jar $BASEDIR/$PACKAGE $CONFIG"

LOGFILE=/var/tmp/$NAME.log

PIDFILE=/var/tmp/$NAME.pid

ACCESS_LOG=/var/tmp/$NAME.access.log

##############################################

function log(){

echo "$(date -d "today" +"%Y-%m-%d %H:%M:%S") $1 $2" >> $LOGFILE

}

function start(){

if [ -f "$PIDFILE" ]; then

echo $PIDFILE

exit 2

fi

su - $USER -c "$PROG & echo \$! > $PIDFILE"

log info start

}

function stop(){

[ -f $PIDFILE ] && kill `cat $PIDFILE` && rm -rf $PIDFILE

log info stop

}

function status(){

ps aux | grep $PACKAGE | grep -v grep | grep -v status

log info status

}

function reset(){

pkill -f $PACKAGE

[ -f $PIDFILE ] && rm -rf $PIDFILE

log info reset

}

case "$1" in

start)

start

;;

stop)

stop

;;

status)

status

;;

restart)

stop

start

;;

log)

tail -f $LOGFILE

;;

reset)

reset

;;

*)

echo $"Usage: $0 {start|stop|status|restart|log|reset}"

esac

exit $?

编译用于Tomcat的 War

package demo;

import org.springframework.boot.SpringApplication;

import org.springframework.boot.autoconfigure.EnableAutoConfiguration;

import org.springframework.boot.builder.SpringApplicationBuilder;

import org.springframework.boot.context.web.SpringBootServletInitializer;

import org.springframework.context.annotation.ComponentScan;

import org.springframework.context.annotation.Configuration;

import org.springframework.web.bind.annotation.PathVariable;

import org.springframework.web.bind.annotation.RequestMapping;

import org.springframework.web.bind.annotation.RestController;

@Configuration

@ComponentScan

@EnableAutoConfiguration

public class Application extends SpringBootServletInitializer {

private static Class<Application> applicationClass = Application.class;

public static void main(String[] args) {

SpringApplication.run(applicationClass, args);

}

@Override

protected SpringApplicationBuilder configure(SpringApplicationBuilder application) {

return application.sources(applicationClass);

}

}

2. @SpringBootApplication

@SpringBootApplication 是 @Configuration, @EnableAutoConfiguration 跟 @ComponentScan 的集合。

@SpringBootApplication

排除 @EnableAutoConfiguration 加载项

@SpringBootApplication(exclude = DataSourceAutoConfiguration.class)

3. 获取 Resources 目录中的静态文件

package cn.netkiller;

import java.io.File;

import java.io.IOException;

import java.net.URL;

import org.springframework.core.io.ClassPathResource;

import org.springframework.web.bind.annotation.GetMapping;

import org.springframework.web.bind.annotation.RestController;

@RestController

public class TestController {

public TestController() {

// TODO Auto-generated constructor stub

}

@GetMapping("/test")

public String test() {

ClassPathResource resource = new ClassPathResource("test.ttf");

File file = null;

try {

file = resource.getFile();

} catch (IOException e) {

e.printStackTrace();

}

// InputStream inStream = new FileInputStream(file.getPath());

// BufferedReader br = new BufferedReader(new InputStreamReader(resource.getInputStream()));

return file.getPath();

}

@GetMapping("/test1")

public String test1() {

URL url = Thread.currentThread().getContextClassLoader().getResource("test.ttf");

return url.getPath();

}

}

4. @EnableAutoConfiguration

exclude 排除配置，下面例子是排除 DataSource配置

@EnableAutoConfiguration(exclude={DataSourceAutoConfiguration.class})

5. @ComponentScan

@ComponentScan 注入会扫描 @Controller 与 @RestController

@ComponentScan

@ComponentScan({"cn.netkiller.controller"})

@ComponentScan({"cn.netkiller.controller", "cn.netkiller.rest"})

6. @EntityScan 实体扫描

@EntityScan("common.domain")

7. @EnableJpaRepositories

扫描 Jpa 仓库

@EnableJpaRepositories("common.domain")

8. 启动和销毁

package cn.netkiller;

import jakarta.annotation.PostConstruct;

import jakarta.annotation.PreDestroy;

import org.slf4j.Logger;

import org.slf4j.LoggerFactory;

import org.springframework.beans.factory.annotation.Value;

import org.springframework.boot.SpringApplication;

import org.springframework.boot.autoconfigure.EnableAutoConfiguration;

import org.springframework.boot.autoconfigure.SpringBootApplication;

import org.springframework.data.jpa.repository.config.EnableJpaRepositories;

import org.springframework.scheduling.annotation.EnableAsync;

@SpringBootApplication

@EnableJpaRepositories

@EnableAutoConfiguration

@EnableAsync

public class Application {

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

@Value("${spring.application.name}")

public String name;

public static void main(String[] args) {

System.out.println("Watch interface start...");

SpringApplication.run(Application.class, args);

}

@PostConstruct

public void init() {

logger.info(String.format("==================== %s 系统启动 ====================", name));

}

@PreDestroy

public void destroy() {

logger.info(String.format("==================== %s 系统销毁 ====================", name));

}

}

9. 打印环境变量

package cn.netkiller;

import jakarta.annotation.PostConstruct;

import jakarta.annotation.PreDestroy;

import org.slf4j.Logger;

import org.slf4j.LoggerFactory;

import org.springframework.beans.factory.annotation.Value;

import org.springframework.boot.SpringApplication;

import org.springframework.boot.autoconfigure.EnableAutoConfiguration;

import org.springframework.boot.autoconfigure.SpringBootApplication;

import org.springframework.data.jpa.repository.config.EnableJpaRepositories;

import org.springframework.scheduling.annotation.EnableAsync;

@SpringBootApplication

@EnableJpaRepositories

@EnableAutoConfiguration

@EnableAsync

public class Application {

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

public static void main(String[] args) {

System.out.println("Watch interface start...");

SpringApplication.run(Application.class, args);

}

@Bean

ApplicationRunner applicationRunner(Environment environment) {

return args -> {

// log.info("our database URL connection will be " +

// environment.getProperty("spring.datasource.url"));

System.out.println(environment.getProperty("spring.application.name"));

};

}

}

10. CharacterEncodingFilter

public @Bean Filter characterEncodingFilter() {

CharacterEncodingFilter characterEncodingFilter = new CharacterEncodingFilter();

characterEncodingFilter.setEncoding("UTF-8");

characterEncodingFilter.setForceEncoding(true);

return characterEncodingFilter;

}

11. 隐藏 Banner

隐藏 Spring Boot Banner

. ____ _ __ _ _

/\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \

( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \

\\/ ___)| |_)| | | | | || (_| | ) ) ) )

' |____| .__|_| |_|_| |_\__, | / / / /

=========|_|==============|___/=/_/_/_/

:: Spring Boot :: (v2.3.1.RELEASE)

public static void main(String[] args) {

SpringApplication app = new SpringApplication(Application.class);

app.setShowBanner(false);

app.run(args);

}

12. 实体与仓库扫描

@EntityScan(basePackages = { "cn.netkiller.model" })

@EnableJpaRepositories(basePackages = { "cn.netkiller.repository" })

13. 列出 Beans

package cn.netkiller;

import java.util.Arrays;

import org.springframework.boot.SpringApplication;

import org.springframework.boot.autoconfigure.EnableAutoConfiguration;

import org.springframework.boot.autoconfigure.SpringBootApplication;

import org.springframework.context.ApplicationContext;

import org.springframework.context.annotation.ComponentScan;

import org.springframework.data.jpa.repository.config.EnableJpaRepositories;

import org.springframework.data.mongodb.repository.config.EnableMongoRepositories;

import org.springframework.scheduling.annotation.EnableScheduling;

@SpringBootApplication

@EnableAutoConfiguration

@ComponentScan

@EnableMongoRepositories

@EnableJpaRepositories

@EnableScheduling

public class Application {

public static void main(String[] args) {

//SpringApplication.run(Application.class, args);

ApplicationContext ctx = SpringApplication.run(Application.class, args);

System.out.println("Let's inspect the beans provided by Spring Boot:");

String[] beanNames = ctx.getBeanDefinitionNames();

Arrays.sort(beanNames);

for (String beanName : beanNames) {

System.out.println(beanName);

}

}

}

14. Tomcat 端口

@Configuration

public class TomcatConfiguration implements EmbeddedServletContainerCustomizer {

int ports[] = { 8080, 8081, 8082 };

@Override

public void customize(ConfigurableEmbeddedServletContainer configurableEmbeddedServletContainer) {

if (ports != null) {

// 判断如果是Tomcat才进行如下配置

if (configurableEmbeddedServletContainer instanceof TomcatEmbeddedServletContainerFactory) {

TomcatEmbeddedServletContainerFactory tomcat = (TomcatEmbeddedServletContainerFactory) configurableEmbeddedServletContainer;

for (int port : ports) {

// 一个Connecter监听一个端口,指定协议为HTTP/1.1

Connector httpConnector = new Connector("HTTP/1.1");

httpConnector.setPort(port);

tomcat.addAdditionalTomcatConnectors(httpConnector);

}

}

}

}

}

15. 配置项设定

public static void main(String[] args) {

SpringApplication.run(Backend.class,

"--spring.application.name=backend",

"--server.port=9000"

);

}

16. spring.profiles.active

在 Java 代码中激活 profile

直接指定环境变量来激活 profile：

System.setProperty("spring.profiles.active", "test");

在 Spring 容器中激活 profile：

AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext();

ctx.getEnvironment().setActiveProfiles("development");

ctx.register(SomeConfig.class, StandaloneDataConfig.class, JndiDataConfig.class);

ctx.refresh();

17. @Profile("dev") / @ActiveProfiles("dev")

不同环境运行不同的逻辑

@Configuration

public class DataSourceConfig {

@Bean

@Profile("dev")

public DataSource devDataSource() {

System.out.println(" dev DataSource !!");

BasicDataSource basicDataSource = new BasicDataSource();

basicDataSource.setDriverClassName("com.mysql.jdbc.Driver");

basicDataSource.setUrl("jdbc:mysql://localhost:3308/neo");

basicDataSource.setUsername("root");

basicDataSource.setPassword("123456");

return basicDataSource;

}

@Bean

@Profile("prod")

public DataSource prodDataSource() {

System.out.println(" prod DataSource !!");

BasicDataSource basicDataSource = new BasicDataSource();

basicDataSource.setDriverClassName("com.mysql.jdbc.Driver");

basicDataSource.setUrl("jdbc:mysql://localhost:3306/neo");

basicDataSource.setUsername("root");

basicDataSource.setPassword("123456");

return basicDataSource;

}

}

匹配多个环境

@Profile({"dev", "test", "grey", "prod"})

18. 设置默认时区

package cn.netkiller;

import org.mybatis.spring.annotation.MapperScan;

import org.springframework.boot.SpringApplication;

import org.springframework.boot.autoconfigure.SpringBootApplication;

import org.springframework.cloud.client.discovery.EnableDiscoveryClient;

import org.springframework.cloud.openfeign.EnableFeignClients;

import org.springframework.transaction.annotation.EnableTransactionManagement;

import javax.annotation.PostConstruct;

import java.util.TimeZone;

@SpringBootApplication

@EnableDiscoveryClient

@EnableTransactionManagement

@EnableFeignClients(basePackages = {"cn.netkiller.feign.*","cn.netkiller.openfeign.*"})

@MapperScan("cn.netkiller.dao")

public class Application {

@PostConstruct

void setDefaultTimezone() {

TimeZone.setDefault(TimeZone.getTimeZone("Asia/Shanghai"));

}

public static void main(String[] args) {

SpringApplication.run(Application.class, args);

}

}

第 4 章 如何优雅停止 Springboot 运行

1. 准备工作

@PreDestroy 会在系统关闭前执行

package cn.netkiller;

import javax.annotation.PreDestroy;

import org.springframework.context.annotation.Configuration;

@Configuration

public class ShutdownConfiguration {

public ShutdownConfiguration() {

// TODO Auto-generated constructor stub

}

@PreDestroy

public void preDestroy() {

System.out.println("==============================");

System.out.println("Destroying Spring");

System.out.println("==============================");

}

}

2. kill 命令演示

kill 命令本质是给进程发送终止信号，进程接收到终止信号后退出运行。

可以看到 Springboot 启动后，进程 PID 44559，现在使用 kill 命令杀死这个进程

当执行 kill 44559 你会看到下面的输出

neo@MacBook-Pro-Neo ~/workspace/microservice/test % java -jar target/test-0.0.1-SNAPSHOT.jar

Starting...

.   ____          _            __ _ _

/\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \

( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \

\\/  ___)| |_)| | | | | || (_| |  ) ) ) )

'  |____| .__|_| |_|_| |_\__, | / / / /

=========|_|==============|___/=/_/_/_/

:: Spring Boot ::                (v2.5.3)

2021-07-29 11:05:09.862  INFO 44559 --- [           main] cn.netkiller.Application                 : Starting Application v0.0.1-SNAPSHOT using Java 16.0.1 on MacBook-Pro-Neo.local with PID 44559 (/Users/neo/workspace/microservice/test/target/test-0.0.1-SNAPSHOT.jar started by neo in /Users/neo/workspace/microservice/test)

2021-07-29 11:05:09.865  INFO 44559 --- [           main] cn.netkiller.Application                 : No active profile set, falling back to default profiles: default

2021-07-29 11:05:11.363  WARN 44559 --- [           main] io.undertow.websockets.jsr               : UT026010: Buffer pool was not set on WebSocketDeploymentInfo, the default pool will be used

2021-07-29 11:05:11.399  INFO 44559 --- [           main] io.undertow.servlet                      : Initializing Spring embedded WebApplicationContext

2021-07-29 11:05:11.400  INFO 44559 --- [           main] w.s.c.ServletWebServerApplicationContext : Root WebApplicationContext: initialization completed in 1451 ms

2021-07-29 11:05:12.041  INFO 44559 --- [           main] o.s.b.a.e.web.EndpointLinksResolver      : Exposing 1 endpoint(s) beneath base path '/actuator'

2021-07-29 11:05:12.073  INFO 44559 --- [           main] io.undertow                              : starting server: Undertow - 2.2.9.Final

2021-07-29 11:05:12.085  INFO 44559 --- [           main] org.xnio                                 : XNIO version 3.8.4.Final

2021-07-29 11:05:12.099  INFO 44559 --- [           main] org.xnio.nio                             : XNIO NIO Implementation Version 3.8.4.Final

2021-07-29 11:05:12.197  INFO 44559 --- [           main] org.jboss.threads                        : JBoss Threads version 3.1.0.Final

2021-07-29 11:05:12.263  INFO 44559 --- [           main] o.s.b.w.e.undertow.UndertowWebServer     : Undertow started on port(s) 8080 (http)

2021-07-29 11:05:12.278  INFO 44559 --- [           main] cn.netkiller.Application                 : Started Application in 2.989 seconds (JVM running for 3.582)

2021-07-29 11:05:20.577  INFO 44559 --- [ionShutdownHook] io.undertow                              : stopping server: Undertow - 2.2.9.Final

==============================

Destroying Spring

==============================

而是用 kill -9 PID 就不会出现下面提示。

neo@MacBook-Pro-Neo ~/workspace/microservice/test % java -jar target/test-0.0.1-SNAPSHOT.jar

Starting...

.   ____          _            __ _ _

/\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \

( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \

\\/  ___)| |_)| | | | | || (_| |  ) ) ) )

'  |____| .__|_| |_|_| |_\__, | / / / /

=========|_|==============|___/=/_/_/_/

:: Spring Boot ::                (v2.5.3)

2021-07-29 11:08:10.857  INFO 44613 --- [           main] cn.netkiller.Application                 : Starting Application v0.0.1-SNAPSHOT using Java 16.0.1 on MacBook-Pro-Neo.local with PID 44613 (/Users/neo/workspace/microservice/test/target/test-0.0.1-SNAPSHOT.jar started by neo in /Users/neo/workspace/microservice/test)

2021-07-29 11:08:10.860  INFO 44613 --- [           main] cn.netkiller.Application                 : No active profile set, falling back to default profiles: default

2021-07-29 11:08:12.377  WARN 44613 --- [           main] io.undertow.websockets.jsr               : UT026010: Buffer pool was not set on WebSocketDeploymentInfo, the default pool will be used

2021-07-29 11:08:12.411  INFO 44613 --- [           main] io.undertow.servlet                      : Initializing Spring embedded WebApplicationContext

2021-07-29 11:08:12.411  INFO 44613 --- [           main] w.s.c.ServletWebServerApplicationContext : Root WebApplicationContext: initialization completed in 1466 ms

2021-07-29 11:08:13.046  INFO 44613 --- [           main] o.s.b.a.e.web.EndpointLinksResolver      : Exposing 1 endpoint(s) beneath base path '/actuator'

2021-07-29 11:08:13.081  INFO 44613 --- [           main] io.undertow                              : starting server: Undertow - 2.2.9.Final

2021-07-29 11:08:13.100  INFO 44613 --- [           main] org.xnio                                 : XNIO version 3.8.4.Final

2021-07-29 11:08:13.114  INFO 44613 --- [           main] org.xnio.nio                             : XNIO NIO Implementation Version 3.8.4.Final

2021-07-29 11:08:13.206  INFO 44613 --- [           main] org.jboss.threads                        : JBoss Threads version 3.1.0.Final

2021-07-29 11:08:13.275  INFO 44613 --- [           main] o.s.b.w.e.undertow.UndertowWebServer     : Undertow started on port(s) 8080 (http)

2021-07-29 11:08:13.290  INFO 44613 --- [           main] cn.netkiller.Application                 : Started Application in 3.195 seconds (JVM running for 3.808)

[1]    44613 killed     java -jar target/test-0.0.1-SNAPSHOT.jar

这是因为 kill 命令会给进程发送终止信号，进程会正常退出.

什么是正常退出呢？例如:

- 完成为运行的逻辑

- 将为写入磁盘的文件后写入后退出

- 执行完SQL并关闭数据库

- 写入缓存，并关闭 redis

- 完成用户请求，并关闭链接

这就是为什么当我们正常关闭程序需要等待很长时间，如果我们此时没有运行状态显示，也没有通过日志反应执行状态，就会认为程序死了。其实此时程序可能尽职尽责的在工作，将未完成的工作完成，然后一步步正常退出。

尤其是多线程的程序，退出时需要等待每个线程完成请求，需要很长时间，我们常常因为升级时间紧迫而使用 kill -9 强行杀死进程，这会带来很多问题。

kill -9 的弊端：

1. 程序执行一半被强行退出，用户端会出现 Timeout 超时

2. 文件写入一半被终止，如果是文本文件只有一半内容；如果是二进制文件会造成损坏

3. 数据库操作一组SQL，只执行了一半，会产生脏数据；如果使用事务处理会引起回滚；

Ctrl + C 与 kill 没有区别，也是给进程发送终止信号，现在我们来演示一下。

neo@MacBook-Pro-Neo ~/workspace/microservice/test % java -jar target/test-0.0.1-SNAPSHOT.jar

Starting...

.   ____          _            __ _ _

/\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \

( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \

\\/  ___)| |_)| | | | | || (_| |  ) ) ) )

'  |____| .__|_| |_|_| |_\__, | / / / /

=========|_|==============|___/=/_/_/_/

:: Spring Boot ::                (v2.5.3)

2021-07-29 11:04:42.657  INFO 44546 --- [           main] cn.netkiller.Application                 : Starting Application v0.0.1-SNAPSHOT using Java 16.0.1 on MacBook-Pro-Neo.local with PID 44546 (/Users/neo/workspace/microservice/test/target/test-0.0.1-SNAPSHOT.jar started by neo in /Users/neo/workspace/microservice/test)

2021-07-29 11:04:42.660  INFO 44546 --- [           main] cn.netkiller.Application                 : No active profile set, falling back to default profiles: default

2021-07-29 11:04:44.212  WARN 44546 --- [           main] io.undertow.websockets.jsr               : UT026010: Buffer pool was not set on WebSocketDeploymentInfo, the default pool will be used

2021-07-29 11:04:44.246  INFO 44546 --- [           main] io.undertow.servlet                      : Initializing Spring embedded WebApplicationContext

2021-07-29 11:04:44.246  INFO 44546 --- [           main] w.s.c.ServletWebServerApplicationContext : Root WebApplicationContext: initialization completed in 1502 ms

2021-07-29 11:04:44.857  INFO 44546 --- [           main] o.s.b.a.e.web.EndpointLinksResolver      : Exposing 1 endpoint(s) beneath base path '/actuator'

2021-07-29 11:04:44.889  INFO 44546 --- [           main] io.undertow                              : starting server: Undertow - 2.2.9.Final

2021-07-29 11:04:44.902  INFO 44546 --- [           main] org.xnio                                 : XNIO version 3.8.4.Final

2021-07-29 11:04:44.916  INFO 44546 --- [           main] org.xnio.nio                             : XNIO NIO Implementation Version 3.8.4.Final

2021-07-29 11:04:45.002  INFO 44546 --- [           main] org.jboss.threads                        : JBoss Threads version 3.1.0.Final

2021-07-29 11:04:45.068  INFO 44546 --- [           main] o.s.b.w.e.undertow.UndertowWebServer     : Undertow started on port(s) 8080 (http)

2021-07-29 11:04:45.084  INFO 44546 --- [           main] cn.netkiller.Application                 : Started Application in 3.149 seconds (JVM running for 3.748)

^C2021-07-29 11:04:47.082  INFO 44546 --- [ionShutdownHook] io.undertow                              : stopping server: Undertow - 2.2.9.Final

==============================

Destroying Spring

==============================

3. 容器中如何优雅关闭 Springboot

容器与进程模式并没有什么区别，我们给容器发送终止信号，容器会转发给 Springboot。

理论归理论，我们还是需要亲自实践，这样才能理解更深刻。

准备实验环境和素材，下面是 docker-compose.yaml 编排文件

version: '3.9'

services:

spring:

image: openjdk:latest

container_name: spring

restart: always

hostname: www.netkiller.cn

environment:

TZ: Asia/Shanghai

JAVA_OPTS: -Xms256m -Xmx512m -XX:MetaspaceSize=128m -XX:MaxMetaspaceSize=512m

ports:

- 8099:8080

volumes:

- ./test-0.0.1-SNAPSHOT.jar:/app/test-0.0.1-SNAPSHOT.jar

entrypoint: java -jar /app/test-0.0.1-SNAPSHOT.jar

command:

--spring.profiles.active=dev

--server.port=8080

实验步骤

- 运行容器：docker-compose up

- 观察容器：docker-compose logs -f

- 停止容器：

运行容器

[root@localhost netkiller.cn]# docker-compose up -d

Starting spring ... done

观察容器日志

[root@localhost netkiller.cn]# docker-compose logs -f

spring    | Starting...

spring    |

spring    |   .   ____          _            __ _ _

spring    |  /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \

spring    | ( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \

spring    |  \\/  ___)| |_)| | | | | || (_| |  ) ) ) )

spring    |   '  |____| .__|_| |_|_| |_\__, | / / / /

spring    |  =========|_|==============|___/=/_/_/_/

spring    |  :: Spring Boot ::                (v2.5.3)

spring    |

spring    | 2021-07-29 11:29:34.556  INFO 1 --- [           main] cn.netkiller.Application                 : Starting Application v0.0.1-SNAPSHOT using Java 16.0.2 on www.netkiller.cn with PID 1 (/app/test-0.0.1-SNAPSHOT.jar started by root in /)

spring    | 2021-07-29 11:29:34.559  INFO 1 --- [           main] cn.netkiller.Application                 : The following profiles are active: dev

spring    | 2021-07-29 11:29:35.903  WARN 1 --- [           main] io.undertow.websockets.jsr               : UT026010: Buffer pool was not set on WebSocketDeploymentInfo, the default pool will be used

spring    | 2021-07-29 11:29:35.921  INFO 1 --- [           main] io.undertow.servlet                      : Initializing Spring embedded WebApplicationContext

spring    | 2021-07-29 11:29:35.921  INFO 1 --- [           main] w.s.c.ServletWebServerApplicationContext : Root WebApplicationContext: initialization completed in 1274 ms

spring    | 2021-07-29 11:29:36.411  INFO 1 --- [           main] o.s.b.a.e.web.EndpointLinksResolver      : Exposing 1 endpoint(s) beneath base path '/actuator'

spring    | 2021-07-29 11:29:36.437  INFO 1 --- [           main] io.undertow                              : starting server: Undertow - 2.2.9.Final

spring    | 2021-07-29 11:29:36.444  INFO 1 --- [           main] org.xnio                                 : XNIO version 3.8.4.Final

spring    | 2021-07-29 11:29:36.451  INFO 1 --- [           main] org.xnio.nio                             : XNIO NIO Implementation Version 3.8.4.Final

spring    | 2021-07-29 11:29:36.511  INFO 1 --- [           main] org.jboss.threads                        : JBoss Threads version 3.1.0.Final

spring    | 2021-07-29 11:29:36.547  INFO 1 --- [           main] o.s.b.w.e.undertow.UndertowWebServer     : Undertow started on port(s) 8080 (http)

spring    | 2021-07-29 11:29:36.560  INFO 1 --- [           main] cn.netkiller.Application                 : Started Application in 2.48 seconds (JVM running for 2.923)

停止容器

[root@localhost netkiller.cn]# docker ps | grep spring

8901384d1973   openjdk:latest                "java -jar /app/test…"   3 minutes ago   Up About a minute   0.0.0.0:8099->8080/tcp, :::8099->8080/tcp                                              spring

[root@localhost netkiller.cn]# docker stop spring

spring

[root@localhost netkiller.cn]# docker ps | grep spring

在观察日志

spring    | 2021-07-29 11:31:31.807  INFO 1 --- [ionShutdownHook] io.undertow                              : stopping server: Undertow - 2.2.9.Final

spring    | ==============================

spring    | Destroying Spring

spring    | ==============================

spring exited with code 143

现在可以看到 Springboot 是正常退出的

下面我们再做一个实验 docker kill

[root@localhost netkiller.cn]# docker-compose start

Starting spring ... done

[root@localhost netkiller.cn]# docker-compose logs -f

[root@localhost netkiller.cn]# docker kill spring

spring

此时再观察日志，只输出了一行。

spring exited with code 137

结论，docker kill = kill -9

现在你应该明白什么时候该使用什么命令终止程序了吧，同时我们在写程序的时候，也应该将程序的运行状态反应出来，在我们停止程序运行的时候，可以去观察进程的状态，而不是半天没有反应，只能怀疑进程死了，必须执行B计划（kill -9）这会造成很多数据丢失的问题。

4. 写入PID文件

我们明白了 kill 的原理后，常常需要与 pid 打交道，使用 ps 命令是可以查看 pid 的，但是当我们运行多个实例的时候会常常搞混，所以最好的方式是让 springboot 把PID写入到文件中。

package cn.netkiller;

import org.springframework.boot.SpringApplication;

import org.springframework.boot.autoconfigure.SpringBootApplication;

import org.springframework.boot.context.ApplicationPidFileWriter;

@SpringBootApplication

public class Application {

public static void main(String[] args) {

System.out.println("Starting...");

SpringApplication springApplication = new SpringApplication(Application.class);

springApplication.addListeners(new ApplicationPidFileWriter());

springApplication.run(args);

}

}

程序运行后会在当前目录下产生一个 PID 文件

neo@MacBook-Pro-Neo ~/workspace/microservice/test % cat application.pid

44027

修改 pid 文件位置可以配置 application.properties

server.port=8080

spring.pid.file=/tmp/spring.pid

在启动的时候指定 pid 文件位置

SpringApplication application = new SpringApplication(Application.class);

application.addListeners(new ApplicationPidFileWriter("/tmp/app.pid"));

application.run();

最后说说容器，容器的进程ID永远是 1 所以配置与否自己斟酌。

[root@localhost netkiller.cn]# docker exec -it spring cat /tmp/spring.pid

1

第 5 章 Properties 配置文件

1. application.properties 配置文件

https://docs.spring.io/spring-boot/docs/current/reference/html/common-application-properties.html

https://docs.spring.io/spring-boot/docs/current/reference/html/application-properties.html

application.properties 参考

http://docs.spring.io/spring-boot/docs/current/reference/html/common-application-properties.html

# ===================================================================

# COMMON SPRING BOOT PROPERTIES

#

# This sample file is provided as a guideline. Do NOT copy it in its

# entirety to your own application.               ^^^

# ===================================================================

# ----------------------------------------

# CORE PROPERTIES

# ----------------------------------------

debug=false # Enable debug logs.

trace=false # Enable trace logs.

# LOGGING

logging.config= # Location of the logging configuration file. For instance, `classpath:logback.xml` for Logback.

logging.exception-conversion-word=%wEx # Conversion word used when logging exceptions.

logging.file= # Log file name (for instance, `myapp.log`). Names can be an exact location or relative to the current directory.

logging.file.max-history=0 # Maximum of archive log files to keep. Only supported with the default logback setup.

logging.file.max-size=10MB # Maximum log file size. Only supported with the default logback setup.

logging.level.*= # Log levels severity mapping. For instance, `logging.level.org.springframework=DEBUG`.

logging.path= # Location of the log file. For instance, `/var/log`.

logging.pattern.console= # Appender pattern for output to the console. Supported only with the default Logback setup.

logging.pattern.dateformat=yyyy-MM-dd HH:mm:ss.SSS # Appender pattern for log date format. Supported only with the default Logback setup.

logging.pattern.file= # Appender pattern for output to a file. Supported only with the default Logback setup.

logging.pattern.level=%5p # Appender pattern for log level. Supported only with the default Logback setup.

logging.register-shutdown-hook=false # Register a shutdown hook for the logging system when it is initialized.

# AOP

spring.aop.auto=true # Add @EnableAspectJAutoProxy.

spring.aop.proxy-target-class=true # Whether subclass-based (CGLIB) proxies are to be created (true), as opposed to standard Java interface-based proxies (false).

# IDENTITY (ContextIdApplicationContextInitializer)

spring.application.name= # Application name.

# ADMIN (SpringApplicationAdminJmxAutoConfiguration)

spring.application.admin.enabled=false # Whether to enable admin features for the application.

spring.application.admin.jmx-name=org.springframework.boot:type=Admin,name=SpringApplication # JMX name of the application admin MBean.

# AUTO-CONFIGURATION

spring.autoconfigure.exclude= # Auto-configuration classes to exclude.

# BANNER

spring.banner.charset=UTF-8 # Banner file encoding.

spring.banner.location=classpath:banner.txt # Banner text resource location.

spring.banner.image.location=classpath:banner.gif # Banner image file location (jpg or png can also be used).

spring.banner.image.width=76 # Width of the banner image in chars.

spring.banner.image.height= # Height of the banner image in chars (default based on image height).

spring.banner.image.margin=2 # Left hand image margin in chars.

spring.banner.image.invert=false # Whether images should be inverted for dark terminal themes.

# SPRING CORE

spring.beaninfo.ignore=true # Whether to skip search of BeanInfo classes.

# SPRING CACHE (CacheProperties)

spring.cache.cache-names= # Comma-separated list of cache names to create if supported by the underlying cache manager.

spring.cache.caffeine.spec= # The spec to use to create caches. See CaffeineSpec for more details on the spec format.

spring.cache.couchbase.expiration=0ms # Entry expiration. By default the entries never expire. Note that this value is ultimately converted to seconds.

spring.cache.ehcache.config= # The location of the configuration file to use to initialize EhCache.

spring.cache.infinispan.config= # The location of the configuration file to use to initialize Infinispan.

spring.cache.jcache.config= # The location of the configuration file to use to initialize the cache manager.

spring.cache.jcache.provider= # Fully qualified name of the CachingProvider implementation to use to retrieve the JSR-107 compliant cache manager. Needed only if more than one JSR-107 implementation is available on the classpath.

spring.cache.redis.cache-null-values=true # Allow caching null values.

spring.cache.redis.key-prefix= # Key prefix.

spring.cache.redis.time-to-live=0ms # Entry expiration. By default the entries never expire.

spring.cache.redis.use-key-prefix=true # Whether to use the key prefix when writing to Redis.

spring.cache.type= # Cache type. By default, auto-detected according to the environment.

# SPRING CONFIG - using environment property only (ConfigFileApplicationListener)

spring.config.additional-location= # Config file locations used in addition to the defaults.

spring.config.location= # Config file locations that replace the defaults.

spring.config.name=application # Config file name.

# HAZELCAST (HazelcastProperties)

spring.hazelcast.config= # The location of the configuration file to use to initialize Hazelcast.

# PROJECT INFORMATION (ProjectInfoProperties)

spring.info.build.location=classpath:META-INF/build-info.properties # Location of the generated build-info.properties file.

spring.info.git.location=classpath:git.properties # Location of the generated git.properties file.

# JMX

spring.jmx.default-domain= # JMX domain name.

spring.jmx.enabled=true # Expose management beans to the JMX domain.

spring.jmx.server=mbeanServer # MBeanServer bean name.

# Email (MailProperties)

spring.mail.default-encoding=UTF-8 # Default MimeMessage encoding.

spring.mail.host= # SMTP server host. For instance, `smtp.example.com`.

spring.mail.jndi-name= # Session JNDI name. When set, takes precedence over other Session settings.

spring.mail.password= # Login password of the SMTP server.

spring.mail.port= # SMTP server port.

spring.mail.properties.*= # Additional JavaMail Session properties.

spring.mail.protocol=smtp # Protocol used by the SMTP server.

spring.mail.-connection=false # Whether to  that the mail server is available on startup.

spring.mail.username= # Login user of the SMTP server.

# APPLICATION SETTINGS (SpringApplication)

spring.main.banner-mode=console # Mode used to display the banner when the application runs.

spring.main.sources= # Sources (class names, package names, or XML resource locations) to include in the ApplicationContext.

spring.main.web-application-type= # Flag to explicitly request a specific type of web application. If not set, auto-detected based on the classpath.

# FILE ENCODING (FileEncodingApplicationListener)

spring.mandatory-file-encoding= # Expected character encoding the application must use.

# INTERNATIONALIZATION (MessageSourceProperties)

spring.messages.always-use-message-format=false # Whether to always apply the MessageFormat rules, parsing even messages without arguments.

spring.messages.basename=messages # Comma-separated list of basenames (essentially a fully-qualified classpath location), each following the ResourceBundle convention with relaxed support for slash based locations.

spring.messages.cache-duration= # Loaded resource bundle files cache duration. When not set, bundles are cached forever. If a duration suffix is not specified, seconds will be used.

spring.messages.encoding=UTF-8 # Message bundles encoding.

spring.messages.fallback-to-system-locale=true # Whether to fall back to the system Locale if no files for a specific Locale have been found.

spring.messages.use-code-as-default-message=false # Whether to use the message code as the default message instead of throwing a "NoSuchMessageException". Recommended during development only.

# OUTPUT

spring.output.ansi.enabled=detect # Configures the ANSI output.

# PID FILE (ApplicationPidFileWriter)

spring.pid.fail-on-write-error= # Fails if ApplicationPidFileWriter is used but it cannot write the PID file.

spring.pid.file= # Location of the PID file to write (if ApplicationPidFileWriter is used).

# PROFILES

spring.profiles.active= # Comma-separated list of active profiles. Can be overridden by a command line switch.

spring.profiles.include= # Unconditionally activate the specified comma-separated list of profiles (or list of profiles if using YAML).

# QUARTZ SCHEDULER (QuartzProperties)

spring.quartz.jdbc.comment-prefix=-- # Prefix for single-line comments in SQL initialization scripts.

spring.quartz.jdbc.initialize-schema=embedded # Database schema initialization mode.

spring.quartz.jdbc.schema=classpath:org/quartz/impl/jdbcjobstore/tables_@@platform@@.sql # Path to the SQL file to use to initialize the database schema.

spring.quartz.job-store-type=memory # Quartz job store type.

spring.quartz.properties.*= # Additional Quartz Scheduler properties.

# REACTOR (ReactorCoreProperties)

spring.reactor.stacktrace-mode.enabled=false # Whether Reactor should collect stacktrace information at runtime.

# SENDGRID (SendGridAutoConfiguration)

spring.sendgrid.api-key= # SendGrid API key.

spring.sendgrid.proxy.host= # SendGrid proxy host.

spring.sendgrid.proxy.port= # SendGrid proxy port.

# ----------------------------------------

# WEB PROPERTIES

# ----------------------------------------

# EMBEDDED SERVER CONFIGURATION (ServerProperties)

server.address= # Network address to which the server should bind.

server.compression.enabled=false # Whether response compression is enabled.

server.compression.excluded-user-agents= # List of user-agents to exclude from compression.

server.compression.mime-types=text/html,text/xml,text/plain,text/css,text/javascript,application/javascript # Comma-separated list of MIME types that should be compressed.

server.compression.min-response-size=2048 # Minimum "Content-Length" value that is required for compression to be performed.

server.connection-timeout= # Time that connectors wait for another HTTP request before closing the connection. When not set, the connector's container-specific default is used. Use a value of -1 to indicate no (that is, an infinite) timeout.

server.error.include-exception=false # Include the "exception" attribute.

server.error.include-stacktrace=never # When to include a "stacktrace" attribute.

server.error.path=/error # Path of the error controller.

server.error.whitelabel.enabled=true # Whether to enable the default error page displayed in browsers in case of a server error.

server.http2.enabled=false # Whether to enable HTTP/2 support, if the current environment supports it.

server.jetty.acceptors=-1 # Number of acceptor threads to use. When the value is -1, the default, the number of acceptors is derived from the operating environment.

server.jetty.accesslog.append=false # Append to log.

server.jetty.accesslog.date-format=dd/MMM/yyyy:HH:mm:ss Z # Timestamp format of the request log.

server.jetty.accesslog.enabled=false # Enable access log.

server.jetty.accesslog.extended-format=false # Enable extended NCSA format.

server.jetty.accesslog.file-date-format= # Date format to place in log file name.

server.jetty.accesslog.filename= # Log filename. If not specified, logs redirect to "System.err".

server.jetty.accesslog.locale= # Locale of the request log.

server.jetty.accesslog.log-cookies=false # Enable logging of the request cookies.

server.jetty.accesslog.log-latency=false # Enable logging of request processing time.

server.jetty.accesslog.log-server=false # Enable logging of the request hostname.

server.jetty.accesslog.retention-period=31 # Number of days before rotated log files are deleted.

server.jetty.accesslog.time-zone=GMT # Timezone of the request log.

server.jetty.max-http-post-size=200000 # Maximum size in bytes of the HTTP post or put content.

server.jetty.selectors=-1 # Number of selector threads to use. When the value is -1, the default, the number of selectors is derived from the operating environment.

server.max-http-header-size=0 # Maximum size, in bytes, of the HTTP message header.

server.port=8080 # Server HTTP port.

server.server-header= # Value to use for the Server response header (if empty, no header is sent).

server.use-forward-headers= # Whether X-Forwarded-* headers should be applied to the HttpRequest.

server.servlet.context-parameters.*= # Servlet context init parameters.

server.servlet.context-path= # Context path of the application.

server.servlet.application-display-name=application # Display name of the application.

server.servlet.jsp.class-name=org.apache.jasper.servlet.JspServlet # The class name of the JSP servlet.

server.servlet.jsp.init-parameters.*= # Init parameters used to configure the JSP servlet.

server.servlet.jsp.registered=true # Whether the JSP servlet is registered.

server.servlet.path=/ # Path of the main dispatcher servlet.

server.servlet.session.cookie.comment= # Comment for the session cookie.

server.servlet.session.cookie.domain= # Domain for the session cookie.

server.servlet.session.cookie.http-only= # "HttpOnly" flag for the session cookie.

server.servlet.session.cookie.max-age= # Maximum age of the session cookie. If a duration suffix is not specified, seconds will be used.

server.servlet.session.cookie.name= # Session cookie name.

server.servlet.session.cookie.path= # Path of the session cookie.

server.servlet.session.cookie.secure= # "Secure" flag for the session cookie.

server.servlet.session.persistent=false # Whether to persist session data between restarts.

server.servlet.session.store-dir= # Directory used to store session data.

server.servlet.session.timeout= # Session timeout. If a duration suffix is not specified, seconds will be used.

server.servlet.session.tracking-modes= # Session tracking modes (one or more of the following: "cookie", "url", "ssl").

server.ssl.ciphers= # Supported SSL ciphers.

server.ssl.client-auth= # Whether client authentication is wanted ("want") or needed ("need"). Requires a trust store.

server.ssl.enabled= # Enable SSL support.

server.ssl.enabled-protocols= # Enabled SSL protocols.

server.ssl.key-alias= # Alias that identifies the key in the key store.

server.ssl.key-password= # Password used to access the key in the key store.

server.ssl.key-store= # Path to the key store that holds the SSL certificate (typically a jks file).

server.ssl.key-store-password= # Password used to access the key store.

server.ssl.key-store-provider= # Provider for the key store.

server.ssl.key-store-type= # Type of the key store.

server.ssl.protocol=TLS # SSL protocol to use.

server.ssl.trust-store= # Trust store that holds SSL certificates.

server.ssl.trust-store-password= # Password used to access the trust store.

server.ssl.trust-store-provider= # Provider for the trust store.

server.ssl.trust-store-type= # Type of the trust store.

server.tomcat.accept-count=100 # Maximum queue length for incoming connection requests when all possible request processing threads are in use.

server.tomcat.accesslog.buffered=true # Whether to buffer output such that it is flushed only periodically.

server.tomcat.accesslog.directory=logs # Directory in which log files are created. Can be absolute or relative to the Tomcat base dir.

server.tomcat.accesslog.enabled=false # Enable access log.

server.tomcat.accesslog.file-date-format=.yyyy-MM-dd # Date format to place in the log file name.

server.tomcat.accesslog.pattern=common # Format pattern for access logs.

server.tomcat.accesslog.prefix=access_log # Log file name prefix.

server.tomcat.accesslog.rename-on-rotate=false # Whether to defer inclusion of the date stamp in the file name until rotate time.

server.tomcat.accesslog.request-attributes-enabled=false # Set request attributes for the IP address, Hostname, protocol, and port used for the request.

server.tomcat.accesslog.rotate=true # Whether to enable access log rotation.

server.tomcat.accesslog.suffix=.log # Log file name suffix.

server.tomcat.additional-tld-skip-patterns= # Comma-separated list of additional patterns that match jars to ignore for TLD scanning.

server.tomcat.background-processor-delay=10 # Delay in seconds between the invocation of backgroundProcess methods.

server.tomcat.basedir= # Tomcat base directory. If not specified, a temporary directory is used.

server.tomcat.internal-proxies=10\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}|\\

192\\.168\\.\\d{1,3}\\.\\d{1,3}|\\

169\\.254\\.\\d{1,3}\\.\\d{1,3}|\\

127\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}|\\

172\\.1[6-9]{1}\\.\\d{1,3}\\.\\d{1,3}|\\

172\\.2[0-9]{1}\\.\\d{1,3}\\.\\d{1,3}|\\

172\\.3[0-1]{1}\\.\\d{1,3}\\.\\d{1,3} # Regular expression matching trusted IP addresses.

server.tomcat.max-connections=10000 # Maximum number of connections that the server will accept and process at any given time.

server.tomcat.max-http-header-size=0 # Maximum size in bytes of the HTTP message header.

server.tomcat.max-http-post-size=2097152 # Maximum size in bytes of the HTTP post content.

server.tomcat.max-threads=200 # Maximum amount of worker threads.

server.tomcat.min-spare-threads=10 # Minimum amount of worker threads.

server.tomcat.port-header=X-Forwarded-Port # Name of the HTTP header used to override the original port value.

server.tomcat.protocol-header= # Header that holds the incoming protocol, usually named "X-Forwarded-Proto".

server.tomcat.protocol-header-https-value=https # Value of the protocol header indicating whether the incoming request uses SSL.

server.tomcat.redirect-context-root=true # Whether requests to the context root should be redirected by appending a / to the path.

server.tomcat.remote-ip-header= # Name of the HTTP header from which the remote IP is extracted. For instance, `X-FORWARDED-FOR`.

server.tomcat.resource.cache-ttl= # Time-to-live of the static resource cache.

server.tomcat.uri-encoding=UTF-8 # Character encoding to use to decode the URI.

server.tomcat.use-relative-redirects= # Whether HTTP 1.1 and later location headers generated by a call to sendRedirect will use relative or absolute redirects.

server.undertow.accesslog.dir= # Undertow access log directory.

server.undertow.accesslog.enabled=false # Whether to enable the access log.

server.undertow.accesslog.pattern=common # Format pattern for access logs.

server.undertow.accesslog.prefix=access_log. # Log file name prefix.

server.undertow.accesslog.rotate=true # Whether to enable access log rotation.

server.undertow.accesslog.suffix=log # Log file name suffix.

server.undertow.buffer-size= # Size of each buffer, in bytes.

server.undertow.direct-buffers= # Allocate buffers outside the Java heap. The default is derived from the maximum amount of memory that is available to the JVM.

server.undertow.eager-filter-init=true # Whether servlet filters should be initialized on startup.

server.undertow.io-threads= # Number of I/O threads to create for the worker. The default is derived from the number of available processors.

server.undertow.max-http-post-size=-1 # Maximum size in bytes of the HTTP post content. When the value is -1, the default, the size is unlimited.

server.undertow.worker-threads= # Number of worker threads. The default is 8 times the number of I/O threads.

# FREEMARKER (FreeMarkerProperties)

spring.freemarker.allow-request-override=false # Whether HttpServletRequest attributes are allowed to override (hide) controller generated model attributes of the same name.

spring.freemarker.allow-session-override=false # Whether HttpSession attributes are allowed to override (hide) controller generated model attributes of the same name.

spring.freemarker.cache=false # Whether to enable template caching.

spring.freemarker.charset=UTF-8 # Template encoding.

spring.freemarker.check-template-location=true # Whether to check that the templates location exists.

spring.freemarker.content-type=text/html # Content-Type value.

spring.freemarker.enabled=true # Whether to enable MVC view resolution for this technology.

spring.freemarker.expose-request-attributes=false # Whether all request attributes should be added to the model prior to merging with the template.

spring.freemarker.expose-session-attributes=false # Whether all HttpSession attributes should be added to the model prior to merging with the template.

spring.freemarker.expose-spring-macro-helpers=true # Whether to expose a RequestContext for use by Spring's macro library, under the name "springMacroRequestContext".

spring.freemarker.prefer-file-system-access=true # Whether to prefer file system access for template loading. File system access enables hot detection of template changes.

spring.freemarker.prefix= # Prefix that gets prepended to view names when building a URL.

spring.freemarker.request-context-attribute= # Name of the RequestContext attribute for all views.

spring.freemarker.settings.*= # Well-known FreeMarker keys which are passed to FreeMarker's Configuration.

spring.freemarker.suffix=.ftl # Suffix that gets appended to view names when building a URL.

spring.freemarker.template-loader-path=classpath:/templates/ # Comma-separated list of template paths.

spring.freemarker.view-names= # White list of view names that can be resolved.

# GROOVY TEMPLATES (GroovyTemplateProperties)

spring.groovy.template.allow-request-override=false # Whether HttpServletRequest attributes are allowed to override (hide) controller generated model attributes of the same name.

spring.groovy.template.allow-session-override=false # Whether HttpSession attributes are allowed to override (hide) controller generated model attributes of the same name.

spring.groovy.template.cache=false # Whether to enable template caching.

spring.groovy.template.charset=UTF-8 # Template encoding.

spring.groovy.template.check-template-location=true # Whether to check that the templates location exists.

spring.groovy.template.configuration.*= # See GroovyMarkupConfigurer

spring.groovy.template.content-type=text/html # Content-Type value.

spring.groovy.template.enabled=true # Whether to enable MVC view resolution for this technology.

spring.groovy.template.expose-request-attributes=false # Whether all request attributes should be added to the model prior to merging with the template.

spring.groovy.template.expose-session-attributes=false # Whether all HttpSession attributes should be added to the model prior to merging with the template.

spring.groovy.template.expose-spring-macro-helpers=true # Whether to expose a RequestContext for use by Spring's macro library, under the name "springMacroRequestContext".

spring.groovy.template.prefix= # Prefix that gets prepended to view names when building a URL.

spring.groovy.template.request-context-attribute= # Name of the RequestContext attribute for all views.

spring.groovy.template.resource-loader-path=classpath:/templates/ # Template path.

spring.groovy.template.suffix=.tpl # Suffix that gets appended to view names when building a URL.

spring.groovy.template.view-names= # White list of view names that can be resolved.

# SPRING HATEOAS (HateoasProperties)

spring.hateoas.use-hal-as-default-json-media-type=true # Whether application/hal+json responses should be sent to requests that accept application/json.

# HTTP message conversion

spring.http.converters.preferred-json-mapper= # Preferred JSON mapper to use for HTTP message conversion. By default, auto-detected according to the environment.

# HTTP encoding (HttpEncodingProperties)

spring.http.encoding.charset=UTF-8 # Charset of HTTP requests and responses. Added to the "Content-Type" header if not set explicitly.

spring.http.encoding.enabled=true # Whether to enable http encoding support.

spring.http.encoding.force= # Whether to force the encoding to the configured charset on HTTP requests and responses.

spring.http.encoding.force-request= # Whether to force the encoding to the configured charset on HTTP requests. Defaults to true when "force" has not been specified.

spring.http.encoding.force-response= # Whether to force the encoding to the configured charset on HTTP responses.

spring.http.encoding.mapping= # Locale in which to encode mapping.

# MULTIPART (MultipartProperties)

spring.servlet.multipart.enabled=true # Whether to enable support of multipart uploads.

spring.servlet.multipart.file-size-threshold=0 # Threshold after which files are written to disk. Values can use the suffixes "MB" or "KB" to indicate megabytes or kilobytes, respectively.

spring.servlet.multipart.location= # Intermediate location of uploaded files.

spring.servlet.multipart.max-file-size=1MB # Max file size. Values can use the suffixes "MB" or "KB" to indicate megabytes or kilobytes, respectively.

spring.servlet.multipart.max-request-size=10MB # Max request size. Values can use the suffixes "MB" or "KB" to indicate megabytes or kilobytes, respectively.

spring.servlet.multipart.resolve-lazily=false # Whether to resolve the multipart request lazily at the time of file or parameter access.

# JACKSON (JacksonProperties)

spring.jackson.date-format= # Date format string or a fully-qualified date format class name. For instance, `yyyy-MM-dd HH:mm:ss`.

spring.jackson.default-property-inclusion= # Controls the inclusion of properties during serialization. Configured with one of the values in Jackson's JsonInclude.Include enumeration.

spring.jackson.deserialization.*= # Jackson on/off features that affect the way Java objects are deserialized.

spring.jackson.generator.*= # Jackson on/off features for generators.

spring.jackson.joda-date-time-format= # Joda date time format string. If not configured, "date-format" is used as a fallback if it is configured with a format string.

spring.jackson.locale= # Locale used for formatting.

spring.jackson.mapper.*= # Jackson general purpose on/off features.

spring.jackson.parser.*= # Jackson on/off features for parsers.

spring.jackson.property-naming-strategy= # One of the constants on Jackson's PropertyNamingStrategy. Can also be a fully-qualified class name of a PropertyNamingStrategy subclass.

spring.jackson.serialization.*= # Jackson on/off features that affect the way Java objects are serialized.

spring.jackson.time-zone= #  Time zone used when formatting dates. For instance, "America/Los_Angeles" or "GMT+10".

# GSON (GsonProperties)

spring.gson.date-format= # Format to use when serializing Date objects.

spring.gson.disable-html-escaping= # Whether to disable the escaping of HTML characters such as '<', '>', etc.

spring.gson.disable-inner-class-serialization= # Whether to exclude inner classes during serialization.

spring.gson.enable-complex-map-key-serialization= # Whether to enable serialization of complex map keys (i.e. non-primitives).

spring.gson.exclude-fields-without-expose-annotation= # Whether to exclude all fields from consideration for serialization or deserialization that do not have the "Expose" annotation.

spring.gson.field-naming-policy= # Naming policy that should be applied to an object's field during serialization and deserialization.

spring.gson.generate-non-executable-json= # Whether to generate non executable JSON by prefixing the output with some special text.

spring.gson.lenient= # Whether to be lenient about parsing JSON that doesn't conform to RFC 4627.

spring.gson.long-serialization-policy= # Serialization policy for Long and long types.

spring.gson.pretty-printing= # Whether to output serialized JSON that fits in a page for pretty printing.

spring.gson.serialize-nulls= # Whether to serialize null fields.

# JERSEY (JerseyProperties)

spring.jersey.application-path= # Path that serves as the base URI for the application. If specified, overrides the value of "@ApplicationPath".

spring.jersey.filter.order=0 # Jersey filter chain order.

spring.jersey.init.*= # Init parameters to pass to Jersey through the servlet or filter.

spring.jersey.servlet.load-on-startup=-1 # Load on startup priority of the Jersey servlet.

spring.jersey.type=servlet # Jersey integration type.

# SPRING LDAP (LdapProperties)

spring.ldap.anonymous-read-only=false # Whether read-only operations should use an anonymous environment.

spring.ldap.base= # Base suffix from which all operations should originate.

spring.ldap.base-environment.*= # LDAP specification settings.

spring.ldap.password= # Login password of the server.

spring.ldap.urls= # LDAP URLs of the server.

spring.ldap.username= # Login username of the server.

# EMBEDDED LDAP (EmbeddedLdapProperties)

spring.ldap.embedded.base-dn= # List of base DNs.

spring.ldap.embedded.credential.username= # Embedded LDAP username.

spring.ldap.embedded.credential.password= # Embedded LDAP password.

spring.ldap.embedded.ldif=classpath:schema.ldif # Schema (LDIF) script resource reference.

spring.ldap.embedded.port=0 # Embedded LDAP port.

spring.ldap.embedded.validation.enabled=true # Whether to enable LDAP schema validation.

spring.ldap.embedded.validation.schema= # Path to the custom schema.

# MUSTACHE TEMPLATES (MustacheAutoConfiguration)

spring.mustache.allow-request-override=false # Whether HttpServletRequest attributes are allowed to override (hide) controller generated model attributes of the same name.

spring.mustache.allow-session-override=false # Whether HttpSession attributes are allowed to override (hide) controller generated model attributes of the same name.

spring.mustache.cache=false # Whether to enable template caching.

spring.mustache.charset=UTF-8 # Template encoding.

spring.mustache.check-template-location=true # Whether to check that the templates location exists.

spring.mustache.content-type=text/html # Content-Type value.

spring.mustache.enabled=true # Whether to enable MVC view resolution for this technology.

spring.mustache.expose-request-attributes=false # Whether all request attributes should be added to the model prior to merging with the template.

spring.mustache.expose-session-attributes=false # Whether all HttpSession attributes should be added to the model prior to merging with the template.

spring.mustache.expose-spring-macro-helpers=true # Whether to expose a RequestContext for use by Spring's macro library, under the name "springMacroRequestContext".

spring.mustache.prefix=classpath:/templates/ # Prefix to apply to template names.

spring.mustache.request-context-attribute= # Name of the RequestContext attribute for all views.

spring.mustache.suffix=.mustache # Suffix to apply to template names.

spring.mustache.view-names= # White list of view names that can be resolved.

# SPRING MVC (WebMvcProperties)

spring.mvc.async.request-timeout= # Amount of time before asynchronous request handling times out.

spring.mvc.contentnegotiation.favor-parameter=false # Whether a request parameter ("format" by default) should be used to determine the requested media type.

spring.mvc.contentnegotiation.favor-path-extension=false # Whether the path extension in the URL path should be used to determine the requested media type.

spring.mvc.contentnegotiation.media-types.*= # Map file extensions to media types for content negotiation. For instance, yml to text/yaml.

spring.mvc.contentnegotiation.parameter-name= # Query parameter name to use when "favor-parameter" is enabled.

spring.mvc.date-format= # Date format to use. For instance, `dd/MM/yyyy`.

spring.mvc.dispatch-trace-request=false # Whether to dispatch TRACE requests to the FrameworkServlet doService method.

spring.mvc.dispatch-options-request=true # Whether to dispatch OPTIONS requests to the FrameworkServlet doService method.

spring.mvc.favicon.enabled=true # Whether to enable resolution of favicon.ico.

spring.mvc.formcontent.putfilter.enabled=true # Whether to enable Spring's HttpPutFormContentFilter.

spring.mvc.ignore-default-model-on-redirect=true # Whether the content of the "default" model should be ignored during redirect scenarios.

spring.mvc.locale= # Locale to use. By default, this locale is overridden by the "Accept-Language" header.

spring.mvc.locale-resolver=accept-header # Define how the locale should be resolved.

spring.mvc.log-resolved-exception=false # Whether to enable warn logging of exceptions resolved by a "HandlerExceptionResolver".

spring.mvc.message-codes-resolver-format= # Formatting strategy for message codes. For instance, `PREFIX_ERROR_CODE`.

spring.mvc.pathmatch.use-registered-suffix-pattern=false # Whether suffix pattern matching should work only against extensions registered with "spring.mvc.contentnegotiation.media-types.*".

spring.mvc.pathmatch.use-suffix-pattern=false # Whether to use suffix pattern match (".*") when matching patterns to requests.

spring.mvc.servlet.load-on-startup=-1 # Load on startup priority of the dispatcher servlet.

spring.mvc.static-path-pattern=/** # Path pattern used for static resources.

spring.mvc.throw-exception-if-no-handler-found=false # Whether a "NoHandlerFoundException" should be thrown if no Handler was found to process a request.

spring.mvc.view.prefix= # Spring MVC view prefix.

spring.mvc.view.suffix= # Spring MVC view suffix.

# SPRING RESOURCES HANDLING (ResourceProperties)

spring.resources.add-mappings=true # Whether to enable default resource handling.

spring.resources.cache.cachecontrol.cache-private= # Indicate that the response message is intended for a single user and must not be stored by a shared cache.

spring.resources.cache.cachecontrol.cache-public= # Indicate that any cache may store the response.

spring.resources.cache.cachecontrol.max-age= # Maximum time the response should be cached, in seconds if no duration suffix is not specified.

spring.resources.cache.cachecontrol.must-revalidate= # Indicate that once it has become stale, a cache must not use the response without re-validating it with the server.

spring.resources.cache.cachecontrol.no-cache= # Indicate that the cached response can be reused only if re-validated with the server.

spring.resources.cache.cachecontrol.no-store= # Indicate to not cache the response in any case.

spring.resources.cache.cachecontrol.no-transform= # Indicate intermediaries (caches and others) that they should not transform the response content.

spring.resources.cache.cachecontrol.proxy-revalidate= # Same meaning as the "must-revalidate" directive, except that it does not apply to private caches.

spring.resources.cache.cachecontrol.s-max-age= # Maximum time the response should be cached by shared caches, in seconds if no duration suffix is not specified.

spring.resources.cache.cachecontrol.stale-if-error= # Maximum time the response may be used when errors are encountered, in seconds if no duration suffix is not specified.

spring.resources.cache.cachecontrol.stale-while-revalidate= # Maximum time the response can be served after it becomes stale, in seconds if no duration suffix is not specified.

spring.resources.cache.period= # Cache period for the resources served by the resource handler. If a duration suffix is not specified, seconds will be used.

spring.resources.chain.cache=true # Whether to enable caching in the Resource chain.

spring.resources.chain.enabled= # Whether to enable the Spring Resource Handling chain. By default, disabled unless at least one strategy has been enabled.

spring.resources.chain.gzipped=false # Whether to enable resolution of already gzipped resources.

spring.resources.chain.html-application-cache=false # Whether to enable HTML5 application cache manifest rewriting.

spring.resources.chain.strategy.content.enabled=false # Whether to enable the content Version Strategy.

spring.resources.chain.strategy.content.paths=/** # Comma-separated list of patterns to apply to the content Version Strategy.

spring.resources.chain.strategy.fixed.enabled=false # Whether to enable the fixed Version Strategy.

spring.resources.chain.strategy.fixed.paths=/** # Comma-separated list of patterns to apply to the fixed Version Strategy.

spring.resources.chain.strategy.fixed.version= # Version string to use for the fixed Version Strategy.

spring.resources.static-locations=classpath:/META-INF/resources/,classpath:/resources/,classpath:/static/,classpath:/public/ # Locations of static resources.

# SPRING SESSION (SessionProperties)

spring.session.store-type= # Session store type.

spring.session.timeout= # Session timeout. If a duration suffix is not specified, seconds will be used.

spring.session.servlet.filter-order=-2147483598 # Session repository filter order.

spring.session.servlet.filter-dispatcher-types=async,error,request # Session repository filter dispatcher types.

# SPRING SESSION HAZELCAST (HazelcastSessionProperties)

spring.session.hazelcast.flush-mode=on-save # Sessions flush mode.

spring.session.hazelcast.map-name=spring:session:sessions # Name of the map used to store sessions.

# SPRING SESSION JDBC (JdbcSessionProperties)

spring.session.jdbc.cleanup-cron=0 * * * * * # Cron expression for expired session cleanup job.

spring.session.jdbc.initialize-schema=embedded # Database schema initialization mode.

spring.session.jdbc.schema=classpath:org/springframework/session/jdbc/schema-@@platform@@.sql # Path to the SQL file to use to initialize the database schema.

spring.session.jdbc.table-name=SPRING_SESSION # Name of the database table used to store sessions.

# SPRING SESSION MONGODB (MongoSessionProperties)

spring.session.mongodb.collection-name=sessions # Collection name used to store sessions.

# SPRING SESSION REDIS (RedisSessionProperties)

spring.session.redis.cleanup-cron=0 * * * * * # Cron expression for expired session cleanup job.

spring.session.redis.flush-mode=on-save # Sessions flush mode.

spring.session.redis.namespace=spring:session # Namespace for keys used to store sessions.

# THYMELEAF (ThymeleafAutoConfiguration)

spring.thymeleaf.cache=true # Whether to enable template caching.

spring.thymeleaf.check-template=true # Whether to check that the template exists before rendering it.

spring.thymeleaf.check-template-location=true # Whether to check that the templates location exists.

spring.thymeleaf.enabled=true # Whether to enable Thymeleaf view resolution for Web frameworks.

spring.thymeleaf.enable-spring-el-compiler=false # Enable the SpringEL compiler in SpringEL expressions.

spring.thymeleaf.encoding=UTF-8 # Template files encoding.

spring.thymeleaf.excluded-view-names= # Comma-separated list of view names (patterns allowed) that should be excluded from resolution.

spring.thymeleaf.mode=HTML # Template mode to be applied to templates. See also Thymeleaf's TemplateMode enum.

spring.thymeleaf.prefix=classpath:/templates/ # Prefix that gets prepended to view names when building a URL.

spring.thymeleaf.reactive.chunked-mode-view-names= # Comma-separated list of view names (patterns allowed) that should be the only ones executed in CHUNKED mode when a max chunk size is set.

spring.thymeleaf.reactive.full-mode-view-names= # Comma-separated list of view names (patterns allowed) that should be executed in FULL mode even if a max chunk size is set.

spring.thymeleaf.reactive.max-chunk-size=0 # Maximum size of data buffers used for writing to the response, in bytes.

spring.thymeleaf.reactive.media-types= # Media types supported by the view technology.

spring.thymeleaf.servlet.content-type=text/html # Content-Type value written to HTTP responses.

spring.thymeleaf.suffix=.html # Suffix that gets appended to view names when building a URL.

spring.thymeleaf.template-resolver-order= # Order of the template resolver in the chain.

spring.thymeleaf.view-names= # Comma-separated list of view names (patterns allowed) that can be resolved.

# SPRING WEBFLUX (WebFluxProperties)

spring.webflux.date-format= # Date format to use. For instance, `dd/MM/yyyy`.

spring.webflux.static-path-pattern=/** # Path pattern used for static resources.

# SPRING WEB SERVICES (WebServicesProperties)

spring.webservices.path=/services # Path that serves as the base URI for the services.

spring.webservices.servlet.init= # Servlet init parameters to pass to Spring Web Services.

spring.webservices.servlet.load-on-startup=-1 # Load on startup priority of the Spring Web Services servlet.

spring.webservices.wsdl-locations= # Comma-separated list of locations of WSDLs and accompanying XSDs to be exposed as beans.

# ----------------------------------------

# SECURITY PROPERTIES

# ----------------------------------------

# SECURITY (SecurityProperties)

spring.security.filter.order=-100 # Security filter chain order.

spring.security.filter.dispatcher-types=async,error,request # Security filter chain dispatcher types.

spring.security.user.name=user # Default user name.

spring.security.user.password= # Password for the default user name.

spring.security.user.roles= # Granted roles for the default user name.

# SECURITY OAUTH2 CLIENT (OAuth2ClientProperties)

spring.security.oauth2.client.provider.*= # OAuth provider details.

spring.security.oauth2.client.registration.*= # OAuth client registrations.

# ----------------------------------------

# DATA PROPERTIES

# ----------------------------------------

# FLYWAY (FlywayProperties)

spring.flyway.baseline-description= #

spring.flyway.baseline-on-migrate= #

spring.flyway.baseline-version=1 # Version to start migration

spring.flyway.check-location=true # Whether to check that migration scripts location exists.

spring.flyway.clean-disabled= #

spring.flyway.clean-on-validation-error= #

spring.flyway.dry-run-output= #

spring.flyway.enabled=true # Whether to enable flyway.

spring.flyway.encoding= #

spring.flyway.error-handlers= #

spring.flyway.group= #

spring.flyway.ignore-future-migrations= #

spring.flyway.ignore-missing-migrations= #

spring.flyway.init-sqls= # SQL statements to execute to initialize a connection immediately after obtaining it.

spring.flyway.installed-by= #

spring.flyway.locations=classpath:db/migration # The locations of migrations scripts.

spring.flyway.mixed= #

spring.flyway.out-of-order= #

spring.flyway.password= # JDBC password to use if you want Flyway to create its own DataSource.

spring.flyway.placeholder-prefix= #

spring.flyway.placeholder-replacement= #

spring.flyway.placeholder-suffix= #

spring.flyway.placeholders.*= #

spring.flyway.repeatable-sql-migration-prefix= #

spring.flyway.schemas= # schemas to update

spring.flyway.skip-default-callbacks= #

spring.flyway.skip-default-resolvers= #

spring.flyway.sql-migration-prefix=V #

spring.flyway.sql-migration-separator= #

spring.flyway.sql-migration-suffix=.sql #

spring.flyway.sql-migration-suffixes= #

spring.flyway.table= #

spring.flyway.target= #

spring.flyway.undo-sql-migration-prefix= #

spring.flyway.url= # JDBC url of the database to migrate. If not set, the primary configured data source is used.

spring.flyway.user= # Login user of the database to migrate.

spring.flyway.validate-on-migrate= #

# LIQUIBASE (LiquibaseProperties)

spring.liquibase.change-log=classpath:/db/changelog/db.changelog-master.yaml # Change log configuration path.

spring.liquibase.check-change-log-location=true # Whether to check that the change log location exists.

spring.liquibase.contexts= # Comma-separated list of runtime contexts to use.

spring.liquibase.default-schema= # Default database schema.

spring.liquibase.drop-first=false # Whether to first drop the database schema.

spring.liquibase.enabled=true # Whether to enable Liquibase support.

spring.liquibase.labels= # Comma-separated list of runtime labels to use.

spring.liquibase.parameters.*= # Change log parameters.

spring.liquibase.password= # Login password of the database to migrate.

spring.liquibase.rollback-file= # File to which rollback SQL is written when an update is performed.

spring.liquibase.url= # JDBC URL of the database to migrate. If not set, the primary configured data source is used.

spring.liquibase.user= # Login user of the database to migrate.

# COUCHBASE (CouchbaseProperties)

spring.couchbase.bootstrap-hosts= # Couchbase nodes (host or IP address) to bootstrap from.

spring.couchbase.bucket.name=default # Name of the bucket to connect to.

spring.couchbase.bucket.password=  # Password of the bucket.

spring.couchbase.env.endpoints.key-value=1 # Number of sockets per node against the key/value service.

spring.couchbase.env.endpoints.queryservice.min-endpoints=1 # Minimum number of sockets per node.

spring.couchbase.env.endpoints.queryservice.max-endpoints=1 # Maximum number of sockets per node.

spring.couchbase.env.endpoints.viewservice.min-endpoints=1 # Minimum number of sockets per node.

spring.couchbase.env.endpoints.viewservice.max-endpoints=1 # Maximum number of sockets per node.

spring.couchbase.env.ssl.enabled= # Whether to enable SSL support. Enabled automatically if a "keyStore" is provided unless specified otherwise.

spring.couchbase.env.ssl.key-store= # Path to the JVM key store that holds the certificates.

spring.couchbase.env.ssl.key-store-password= # Password used to access the key store.

spring.couchbase.env.timeouts.connect=5000ms # Bucket connections timeouts.

spring.couchbase.env.timeouts.key-value=2500ms # Blocking operations performed on a specific key timeout.

spring.couchbase.env.timeouts.query=7500ms # N1QL query operations timeout.

spring.couchbase.env.timeouts.socket-connect=1000ms # Socket connect connections timeout.

spring.couchbase.env.timeouts.view=7500ms # Regular and geospatial view operations timeout.

# DAO (PersistenceExceptionTranslationAutoConfiguration)

spring.dao.exceptiontranslation.enabled=true # Whether to enable the PersistenceExceptionTranslationPostProcessor.

# CASSANDRA (CassandraProperties)

spring.data.cassandra.cluster-name= # Name of the Cassandra cluster.

spring.data.cassandra.compression=none # Compression supported by the Cassandra binary protocol.

spring.data.cassandra.connect-timeout= # Socket option: connection time out.

spring.data.cassandra.consistency-level= # Queries consistency level.

spring.data.cassandra.contact-points=localhost # Cluster node addresses.

spring.data.cassandra.fetch-size= # Queries default fetch size.

spring.data.cassandra.keyspace-name= # Keyspace name to use.

spring.data.cassandra.load-balancing-policy= # Class name of the load balancing policy.

spring.data.cassandra.port= # Port of the Cassandra server.

spring.data.cassandra.password= # Login password of the server.

spring.data.cassandra.pool.heartbeat-interval=30s # Heartbeat interval after which a message is sent on an idle connection to make sure it's still alive. If a duration suffix is not specified, seconds will be used.

spring.data.cassandra.pool.idle-timeout=120s # Idle timeout before an idle connection is removed. If a duration suffix is not specified, seconds will be used.

spring.data.cassandra.pool.max-queue-size=256 # Maximum number of requests that get queued if no connection is available.

spring.data.cassandra.pool.pool-timeout=5000ms # Pool timeout when trying to acquire a connection from a host's pool.

spring.data.cassandra.read-timeout= # Socket option: read time out.

spring.data.cassandra.reconnection-policy= # Reconnection policy class.

spring.data.cassandra.repositories.type=auto # Type of Cassandra repositories to enable.

spring.data.cassandra.retry-policy= # Class name of the retry policy.

spring.data.cassandra.serial-consistency-level= # Queries serial consistency level.

spring.data.cassandra.schema-action=none # Schema action to take at startup.

spring.data.cassandra.ssl=false # Enable SSL support.

spring.data.cassandra.username= # Login user of the server.

# DATA COUCHBASE (CouchbaseDataProperties)

spring.data.couchbase.auto-index=false # Automatically create views and indexes.

spring.data.couchbase.consistency=read-your-own-writes # Consistency to apply by default on generated queries.

spring.data.couchbase.repositories.type=auto # Type of Couchbase repositories to enable.

# ELASTICSEARCH (ElasticsearchProperties)

spring.data.elasticsearch.cluster-name=elasticsearch # Elasticsearch cluster name.

spring.data.elasticsearch.cluster-nodes= # Comma-separated list of cluster node addresses.

spring.data.elasticsearch.properties.*= # Additional properties used to configure the client.

spring.data.elasticsearch.repositories.enabled=true # Whether to enable Elasticsearch repositories.

# DATA LDAP

spring.data.ldap.repositories.enabled=true # Whether to enable LDAP repositories.

# MONGODB (MongoProperties)

spring.data.mongodb.authentication-database= # Authentication database name.

spring.data.mongodb.database= # Database name.

spring.data.mongodb.field-naming-strategy= # Fully qualified name of the FieldNamingStrategy to use.

spring.data.mongodb.grid-fs-database= # GridFS database name.

spring.data.mongodb.host= # Mongo server host. Cannot be set with URI.

spring.data.mongodb.password= # Login password of the mongo server. Cannot be set with URI.

spring.data.mongodb.port= # Mongo server port. Cannot be set with URI.

spring.data.mongodb.repositories.type=auto # Type of Mongo repositories to enable.

spring.data.mongodb.uri=mongodb://localhost/ # Mongo database URI. Cannot be set with host, port and credentials.

spring.data.mongodb.username= # Login user of the mongo server. Cannot be set with URI.

# DATA REDIS

spring.data.redis.repositories.enabled=true # Whether to enable Redis repositories.

# NEO4J (Neo4jProperties)

spring.data.neo4j.auto-index=none # Auto index mode.

spring.data.neo4j.embedded.enabled=true # Whether to enable embedded mode if the embedded driver is available.

spring.data.neo4j.open-in-view=true # Register OpenSessionInViewInterceptor. Binds a Neo4j Session to the thread for the entire processing of the request.

spring.data.neo4j.password= # Login password of the server.

spring.data.neo4j.repositories.enabled=true # Whether to enable Neo4j repositories.

spring.data.neo4j.uri= # URI used by the driver. Auto-detected by default.

spring.data.neo4j.username= # Login user of the server.

# DATA REST (RepositoryRestProperties)

spring.data.rest.base-path= # Base path to be used by Spring Data REST to expose repository resources.

spring.data.rest.default-media-type= # Content type to use as a default when none is specified.

spring.data.rest.default-page-size= # Default size of pages.

spring.data.rest.detection-strategy=default # Strategy to use to determine which repositories get exposed.

spring.data.rest.enable-enum-translation= # Whether to enable enum value translation through the Spring Data REST default resource bundle.

spring.data.rest.limit-param-name= # Name of the URL query string parameter that indicates how many results to return at once.

spring.data.rest.max-page-size= # Maximum size of pages.

spring.data.rest.page-param-name= # Name of the URL query string parameter that indicates what page to return.

spring.data.rest.return-body-on-create= # Whether to return a response body after creating an entity.

spring.data.rest.return-body-on-update= # Whether to return a response body after updating an entity.

spring.data.rest.sort-param-name= # Name of the URL query string parameter that indicates what direction to sort results.

# SOLR (SolrProperties)

spring.data.solr.host=http://127.0.0.1:8983/solr # Solr host. Ignored if "zk-host" is set.

spring.data.solr.repositories.enabled=true # Whether to enable Solr repositories.

spring.data.solr.zk-host= # ZooKeeper host address in the form HOST:PORT.

# DATA WEB (SpringDataWebProperties)

spring.data.web.pageable.default-page-size=20 # Default page size.

spring.data.web.pageable.max-page-size=2000 # Maximum page size to be accepted.

spring.data.web.pageable.one-indexed-parameters=false # Whether to expose and assume 1-based page number indexes.

spring.data.web.pageable.page-parameter=page # Page index parameter name.

spring.data.web.pageable.prefix= # General prefix to be prepended to the page number and page size parameters.

spring.data.web.pageable.qualifier-delimiter=_ # Delimiter to be used between the qualifier and the actual page number and size properties.

spring.data.web.pageable.size-parameter=size # Page size parameter name.

spring.data.web.sort.sort-parameter=sort # Sort parameter name.

# DATASOURCE (DataSourceAutoConfiguration & DataSourceProperties)

spring.datasource.continue-on-error=false # Whether to stop if an error occurs while initializing the database.

spring.datasource.data= # Data (DML) script resource references.

spring.datasource.data-username= # Username of the database to execute DML scripts (if different).

spring.datasource.data-password= # Password of the database to execute DML scripts (if different).

spring.datasource.dbcp2.*= # Commons DBCP2 specific settings

spring.datasource.driver-class-name= # Fully qualified name of the JDBC driver. Auto-detected based on the URL by default.

spring.datasource.generate-unique-name=false # Whether to generate a random datasource name.

spring.datasource.hikari.*= # Hikari specific settings

spring.datasource.initialization-mode=embedded # Initialize the datasource with available DDL and DML scripts.

spring.datasource.jmx-enabled=false # Whether to enable JMX support (if provided by the underlying pool).

spring.datasource.jndi-name= # JNDI location of the datasource. Class, url, username & password are ignored when set.

spring.datasource.name= # Name of the datasource. Default to "db" when using an embedded database.

spring.datasource.password= # Login password of the database.

spring.datasource.platform=all # Platform to use in the DDL or DML scripts (such as schema-${platform}.sql or data-${platform}.sql).

spring.datasource.schema= # Schema (DDL) script resource references.

spring.datasource.schema-username= # Username of the database to execute DDL scripts (if different).

spring.datasource.schema-password= # Password of the database to execute DDL scripts (if different).

spring.datasource.separator=; # Statement separator in SQL initialization scripts.

spring.datasource.sql-script-encoding= # SQL scripts encoding.

spring.datasource.tomcat.*= # Tomcat datasource specific settings

spring.datasource.type= # Fully qualified name of the connection pool implementation to use. By default, it is auto-detected from the classpath.

spring.datasource.url= # JDBC URL of the database.

spring.datasource.username= # Login username of the database.

spring.datasource.xa.data-source-class-name= # XA datasource fully qualified name.

spring.datasource.xa.properties= # Properties to pass to the XA data source.

# JEST (Elasticsearch HTTP client) (JestProperties)

spring.elasticsearch.jest.connection-timeout=3s # Connection timeout.

spring.elasticsearch.jest.multi-threaded=true # Whether to enable connection requests from multiple execution threads.

spring.elasticsearch.jest.password= # Login password.

spring.elasticsearch.jest.proxy.host= # Proxy host the HTTP client should use.

spring.elasticsearch.jest.proxy.port= # Proxy port the HTTP client should use.

spring.elasticsearch.jest.read-timeout=3s # Read timeout.

spring.elasticsearch.jest.uris=http://localhost:9200 # Comma-separated list of the Elasticsearch instances to use.

spring.elasticsearch.jest.username= # Login username.

# H2 Web Console (H2ConsoleProperties)

spring.h2.console.enabled=false # Whether to enable the console.

spring.h2.console.path=/h2-console # Path at which the console is available.

spring.h2.console.settings.trace=false # Whether to enable trace output.

spring.h2.console.settings.web-allow-others=false # Whether to enable remote access.

# InfluxDB (InfluxDbProperties)

spring.influx.password= # Login password.

spring.influx.url= # URL of the InfluxDB instance to which to connect.

spring.influx.user= # Login user.

# JOOQ (JooqProperties)

spring.jooq.sql-dialect= # SQL dialect to use. Auto-detected by default.

# JDBC (JdbcProperties)

spring.jdbc.template.fetch-size=-1 # Number of rows that should be fetched from the database when more rows are needed.

spring.jdbc.template.max-rows=-1 # Maximum number of rows.

spring.jdbc.template.query-timeout= # Query timeout. Default is to use the JDBC driver's default configuration. If a duration suffix is not specified, seconds will be used.

# JPA (JpaBaseConfiguration, HibernateJpaAutoConfiguration)

spring.data.jpa.repositories.enabled=true # Whether to enable JPA repositories.

spring.jpa.database= # Target database to operate on, auto-detected by default. Can be alternatively set using the "databasePlatform" property.

spring.jpa.database-platform= # Name of the target database to operate on, auto-detected by default. Can be alternatively set using the "Database" enum.

spring.jpa.generate-ddl=false # Whether to initialize the schema on startup.

spring.jpa.hibernate.ddl-auto= # DDL mode. This is actually a shortcut for the "hibernate.hbm2ddl.auto" property. Defaults to "create-drop" when using an embedded database and no schema manager was detected. Otherwise, defaults to "none".

spring.jpa.hibernate.naming.implicit-strategy= # Fully qualified name of the implicit naming strategy.

spring.jpa.hibernate.naming.physical-strategy= # Fully qualified name of the physical naming strategy.

spring.jpa.hibernate.use-new-id-generator-mappings= # Whether to use Hibernate's newer IdentifierGenerator for AUTO, TABLE and SEQUENCE.

spring.jpa.mapping-resources= # Mapping resources (equivalent to "mapping-file" entries in persistence.xml).

spring.jpa.open-in-view=true # Register OpenEntityManagerInViewInterceptor. Binds a JPA EntityManager to the thread for the entire processing of the request.

spring.jpa.properties.*= # Additional native properties to set on the JPA provider.

spring.jpa.show-sql=false # Whether to enable logging of SQL statements.

# JTA (JtaAutoConfiguration)

spring.jta.enabled=true # Whether to enable JTA support.

spring.jta.log-dir= # Transaction logs directory.

spring.jta.transaction-manager-id= # Transaction manager unique identifier.

# ATOMIKOS (AtomikosProperties)

spring.jta.atomikos.connectionfactory.borrow-connection-timeout=30 # Timeout, in seconds, for borrowing connections from the pool.

spring.jta.atomikos.connectionfactory.ignore-session-transacted-flag=true # Whether to ignore the transacted flag when creating session.

spring.jta.atomikos.connectionfactory.local-transaction-mode=false # Whether local transactions are desired.

spring.jta.atomikos.connectionfactory.maintenance-interval=60 # The time, in seconds, between runs of the pool's maintenance thread.

spring.jta.atomikos.connectionfactory.max-idle-time=60 # The time, in seconds, after which connections are cleaned up from the pool.

spring.jta.atomikos.connectionfactory.max-lifetime=0 # The time, in seconds, that a connection can be pooled for before being destroyed. 0 denotes no limit.

spring.jta.atomikos.connectionfactory.max-pool-size=1 # The maximum size of the pool.

spring.jta.atomikos.connectionfactory.min-pool-size=1 # The minimum size of the pool.

spring.jta.atomikos.connectionfactory.reap-timeout=0 # The reap timeout, in seconds, for borrowed connections. 0 denotes no limit.

spring.jta.atomikos.connectionfactory.unique-resource-name=jmsConnectionFactory # The unique name used to identify the resource during recovery.

spring.jta.atomikos.connectionfactory.xa-connection-factory-class-name= # Vendor-specific implementation of XAConnectionFactory.

spring.jta.atomikos.connectionfactory.xa-properties= # Vendor-specific XA properties.

spring.jta.atomikos.datasource.borrow-connection-timeout=30 # Timeout, in seconds, for borrowing connections from the pool.

spring.jta.atomikos.datasource.concurrent-connection-validation= # Whether to use concurrent connection validation.

spring.jta.atomikos.datasource.default-isolation-level= # Default isolation level of connections provided by the pool.

spring.jta.atomikos.datasource.login-timeout= # Timeout, in seconds, for establishing a database connection.

spring.jta.atomikos.datasource.maintenance-interval=60 # The time, in seconds, between runs of the pool's maintenance thread.

spring.jta.atomikos.datasource.max-idle-time=60 # The time, in seconds, after which connections are cleaned up from the pool.

spring.jta.atomikos.datasource.max-lifetime=0 # The time, in seconds, that a connection can be pooled for before being destroyed. 0 denotes no limit.

spring.jta.atomikos.datasource.max-pool-size=1 # The maximum size of the pool.

spring.jta.atomikos.datasource.min-pool-size=1 # The minimum size of the pool.

spring.jta.atomikos.datasource.reap-timeout=0 # The reap timeout, in seconds, for borrowed connections. 0 denotes no limit.

spring.jta.atomikos.datasource.-query= # SQL query or statement used to validate a connection before returning it.

spring.jta.atomikos.datasource.unique-resource-name=dataSource # The unique name used to identify the resource during recovery.

spring.jta.atomikos.datasource.xa-data-source-class-name= # Vendor-specific implementation of XAConnectionFactory.

spring.jta.atomikos.datasource.xa-properties= # Vendor-specific XA properties.

spring.jta.atomikos.properties.allow-sub-transactions=true # Specify whether sub-transactions are allowed.

spring.jta.atomikos.properties.checkpoint-interval=500 # Interval between checkpoints, expressed as the number of log writes between two checkpoints.

spring.jta.atomikos.properties.default-jta-timeout=10000ms # Default timeout for JTA transactions.

spring.jta.atomikos.properties.default-max-wait-time-on-shutdown=9223372036854775807 # How long should normal shutdown (no-force) wait for transactions to complete.

spring.jta.atomikos.properties.enable-logging=true # Whether to enable disk logging.

spring.jta.atomikos.properties.force-shutdown-on-vm-exit=false # Whether a VM shutdown should trigger forced shutdown of the transaction core.

spring.jta.atomikos.properties.log-base-dir= # Directory in which the log files should be stored.

spring.jta.atomikos.properties.log-base-name=tmlog # Transactions log file base name.

spring.jta.atomikos.properties.max-actives=50 # Maximum number of active transactions.

spring.jta.atomikos.properties.max-timeout=300000ms # Maximum timeout that can be allowed for transactions.

spring.jta.atomikos.properties.recovery.delay=10000ms # Delay between two recovery scans.

spring.jta.atomikos.properties.recovery.forget-orphaned-log-entries-delay=86400000ms # Delay after which recovery can cleanup pending ('orphaned') log entries.

spring.jta.atomikos.properties.recovery.max-retries=5 # Number of retry attempts to commit the transaction before throwing an exception.

spring.jta.atomikos.properties.recovery.retry-interval=10000ms # Delay between retry attempts.

spring.jta.atomikos.properties.serial-jta-transactions=true # Whether sub-transactions should be joined when possible.

spring.jta.atomikos.properties.service= # Transaction manager implementation that should be started.

spring.jta.atomikos.properties.threaded-two-phase-commit=false # Whether to use different (and concurrent) threads for two-phase commit on the participating resources.

spring.jta.atomikos.properties.transaction-manager-unique-name= # The transaction manager's unique name.

# BITRONIX

spring.jta.bitronix.connectionfactory.acquire-increment=1 # Number of connections to create when growing the pool.

spring.jta.bitronix.connectionfactory.acquisition-interval=1 # Time, in seconds, to wait before trying to acquire a connection again after an invalid connection was acquired.

spring.jta.bitronix.connectionfactory.acquisition-timeout=30 # Timeout, in seconds, for acquiring connections from the pool.

spring.jta.bitronix.connectionfactory.allow-local-transactions=true # Whether the transaction manager should allow mixing XA and non-XA transactions.

spring.jta.bitronix.connectionfactory.apply-transaction-timeout=false # Whether the transaction timeout should be set on the XAResource when it is enlisted.

spring.jta.bitronix.connectionfactory.automatic-enlisting-enabled=true # Whether resources should be enlisted and delisted automatically.

spring.jta.bitronix.connectionfactory.cache-producers-consumers=true # Whether producers and consumers should be cached.

spring.jta.bitronix.connectionfactory.class-name= # Underlying implementation class name of the XA resource.

spring.jta.bitronix.connectionfactory.defer-connection-release=true # Whether the provider can run many transactions on the same connection and supports transaction interleaving.

spring.jta.bitronix.connectionfactory.disabled= # Whether this resource is disabled, meaning it's temporarily forbidden to acquire a connection from its pool.

spring.jta.bitronix.connectionfactory.driver-properties= # Properties that should be set on the underlying implementation.

spring.jta.bitronix.connectionfactory.failed= # Mark this resource producer as failed.

spring.jta.bitronix.connectionfactory.ignore-recovery-failures=false # Whether recovery failures should be ignored.

spring.jta.bitronix.connectionfactory.max-idle-time=60 # The time, in seconds, after which connections are cleaned up from the pool.

spring.jta.bitronix.connectionfactory.max-pool-size=10 # The maximum size of the pool. 0 denotes no limit.

spring.jta.bitronix.connectionfactory.min-pool-size=0 # The minimum size of the pool.

spring.jta.bitronix.connectionfactory.password= # The password to use to connect to the JMS provider.

spring.jta.bitronix.connectionfactory.share-transaction-connections=false #  Whether connections in the ACCESSIBLE state can be shared within the context of a transaction.

spring.jta.bitronix.connectionfactory.-connections=true # Whether connections should be ed when acquired from the pool.

spring.jta.bitronix.connectionfactory.two-pc-ordering-position=1 # The position that this resource should take during two-phase commit (always first is Integer.MIN_VALUE, always last is Integer.MAX_VALUE).

spring.jta.bitronix.connectionfactory.unique-name=jmsConnectionFactory # The unique name used to identify the resource during recovery.

spring.jta.bitronix.connectionfactory.use-tm-join=true # Whether TMJOIN should be used when starting XAResources.

spring.jta.bitronix.connectionfactory.user= # The user to use to connect to the JMS provider.

spring.jta.bitronix.datasource.acquire-increment=1 # Number of connections to create when growing the pool.

spring.jta.bitronix.datasource.acquisition-interval=1 # Time, in seconds, to wait before trying to acquire a connection again after an invalid connection was acquired.

spring.jta.bitronix.datasource.acquisition-timeout=30 # Timeout, in seconds, for acquiring connections from the pool.

spring.jta.bitronix.datasource.allow-local-transactions=true # Whether the transaction manager should allow mixing XA and non-XA transactions.

spring.jta.bitronix.datasource.apply-transaction-timeout=false # Whether the transaction timeout should be set on the XAResource when it is enlisted.

spring.jta.bitronix.datasource.automatic-enlisting-enabled=true # Whether resources should be enlisted and delisted automatically.

spring.jta.bitronix.datasource.class-name= # Underlying implementation class name of the XA resource.

spring.jta.bitronix.datasource.cursor-holdability= # The default cursor holdability for connections.

spring.jta.bitronix.datasource.defer-connection-release=true # Whether the database can run many transactions on the same connection and supports transaction interleaving.

spring.jta.bitronix.datasource.disabled= # Whether this resource is disabled, meaning it's temporarily forbidden to acquire a connection from its pool.

spring.jta.bitronix.datasource.driver-properties= # Properties that should be set on the underlying implementation.

spring.jta.bitronix.datasource.enable-jdbc4-connection-= # Whether Connection.isValid() is called when acquiring a connection from the pool.

spring.jta.bitronix.datasource.failed= # Mark this resource producer as failed.

spring.jta.bitronix.datasource.ignore-recovery-failures=false # Whether recovery failures should be ignored.

spring.jta.bitronix.datasource.isolation-level= # The default isolation level for connections.

spring.jta.bitronix.datasource.local-auto-commit= # The default auto-commit mode for local transactions.

spring.jta.bitronix.datasource.login-timeout= # Timeout, in seconds, for establishing a database connection.

spring.jta.bitronix.datasource.max-idle-time=60 # The time, in seconds, after which connections are cleaned up from the pool.

spring.jta.bitronix.datasource.max-pool-size=10 # The maximum size of the pool. 0 denotes no limit.

spring.jta.bitronix.datasource.min-pool-size=0 # The minimum size of the pool.

spring.jta.bitronix.datasource.prepared-statement-cache-size=0 # The target size of the prepared statement cache. 0 disables the cache.

spring.jta.bitronix.datasource.share-transaction-connections=false #  Whether connections in the ACCESSIBLE state can be shared within the context of a transaction.

spring.jta.bitronix.datasource.-query= # SQL query or statement used to validate a connection before returning it.

spring.jta.bitronix.datasource.two-pc-ordering-position=1 # The position that this resource should take during two-phase commit (always first is Integer.MIN_VALUE, and always last is Integer.MAX_VALUE).

spring.jta.bitronix.datasource.unique-name=dataSource # The unique name used to identify the resource during recovery.

spring.jta.bitronix.datasource.use-tm-join=true # Whether TMJOIN should be used when starting XAResources.

spring.jta.bitronix.properties.allow-multiple-lrc=false # Whether to allow multiple LRC resources to be enlisted into the same transaction.

spring.jta.bitronix.properties.asynchronous2-pc=false # Whether to enable asynchronously execution of two phase commit.

spring.jta.bitronix.properties.background-recovery-interval-seconds=60 # Interval in seconds at which to run the recovery process in the background.

spring.jta.bitronix.properties.current-node-only-recovery=true # Whether to recover only the current node.

spring.jta.bitronix.properties.debug-zero-resource-transaction=false # Whether to log the creation and commit call stacks of transactions executed without a single enlisted resource.

spring.jta.bitronix.properties.default-transaction-timeout=60 # Default transaction timeout, in seconds.

spring.jta.bitronix.properties.disable-jmx=false # Whether to enable JMX support.

spring.jta.bitronix.properties.exception-analyzer= # Set the fully qualified name of the exception analyzer implementation to use.

spring.jta.bitronix.properties.filter-log-status=false # Whether to enable filtering of logs so that only mandatory logs are written.

spring.jta.bitronix.properties.force-batching-enabled=true #  Whether disk forces are batched.

spring.jta.bitronix.properties.forced-write-enabled=true # Whether logs are forced to disk.

spring.jta.bitronix.properties.graceful-shutdown-interval=60 # Maximum amount of seconds the TM waits for transactions to get done before aborting them at shutdown time.

spring.jta.bitronix.properties.jndi-transaction-synchronization-registry-name= # JNDI name of the TransactionSynchronizationRegistry.

spring.jta.bitronix.properties.jndi-user-transaction-name= # JNDI name of the UserTransaction.

spring.jta.bitronix.properties.journal=disk # Name of the journal. Can be 'disk', 'null', or a class name.

spring.jta.bitronix.properties.log-part1-filename=btm1.tlog # Name of the first fragment of the journal.

spring.jta.bitronix.properties.log-part2-filename=btm2.tlog # Name of the second fragment of the journal.

spring.jta.bitronix.properties.max-log-size-in-mb=2 # Maximum size in megabytes of the journal fragments.

spring.jta.bitronix.properties.resource-configuration-filename= # ResourceLoader configuration file name.

spring.jta.bitronix.properties.server-id= # ASCII ID that must uniquely identify this TM instance. Defaults to the machine's IP address.

spring.jta.bitronix.properties.skip-corrupted-logs=false # Skip corrupted transactions log entries.

spring.jta.bitronix.properties.warn-about-zero-resource-transaction=true # Whether to log a warning for transactions executed without a single enlisted resource.

# NARAYANA (NarayanaProperties)

spring.jta.narayana.default-timeout=60s # Transaction timeout. If a duration suffix is not specified, seconds will be used.

spring.jta.narayana.expiry-scanners=com.arjuna.ats.internal.arjuna.recovery.ExpiredTransactionStatusManagerScanner # Comma-separated list of expiry scanners.

spring.jta.narayana.log-dir= # Transaction object store directory.

spring.jta.narayana.one-phase-commit=true # Whether to enable one phase commit optimization.

spring.jta.narayana.periodic-recovery-period=120s # Interval in which periodic recovery scans are performed. If a duration suffix is not specified, seconds will be used.

spring.jta.narayana.recovery-backoff-period=10s # Back off period between first and second phases of the recovery scan. If a duration suffix is not specified, seconds will be used.

spring.jta.narayana.recovery-db-pass= # Database password to be used by the recovery manager.

spring.jta.narayana.recovery-db-user= # Database username to be used by the recovery manager.

spring.jta.narayana.recovery-jms-pass= # JMS password to be used by the recovery manager.

spring.jta.narayana.recovery-jms-user= # JMS username to be used by the recovery manager.

spring.jta.narayana.recovery-modules= # Comma-separated list of recovery modules.

spring.jta.narayana.transaction-manager-id=1 # Unique transaction manager id.

spring.jta.narayana.xa-resource-orphan-filters= # Comma-separated list of orphan filters.

# EMBEDDED MONGODB (EmbeddedMongoProperties)

spring.mongodb.embedded.features=sync_delay # Comma-separated list of features to enable.

spring.mongodb.embedded.storage.database-dir= # Directory used for data storage.

spring.mongodb.embedded.storage.oplog-size= # Maximum size of the oplog, in megabytes.

spring.mongodb.embedded.storage.repl-set-name= # Name of the replica set.

spring.mongodb.embedded.version=3.2.2 # Version of Mongo to use.

# REDIS (RedisProperties)

spring.redis.cluster.max-redirects= # Maximum number of redirects to follow when executing commands across the cluster.

spring.redis.cluster.nodes= # Comma-separated list of "host:port" pairs to bootstrap from.

spring.redis.database=0 # Database index used by the connection factory.

spring.redis.url= # Connection URL. Overrides host, port, and password. User is ignored. Example: redis://user:password@example.com:6379

spring.redis.host=localhost # Redis server host.

spring.redis.jedis.pool.max-active=8 # Maximum number of connections that can be allocated by the pool at a given time. Use a negative value for no limit.

spring.redis.jedis.pool.max-idle=8 # Maximum number of "idle" connections in the pool. Use a negative value to indicate an unlimited number of idle connections.

spring.redis.jedis.pool.max-wait=-1ms # Maximum amount of time a connection allocation should block before throwing an exception when the pool is exhausted. Use a negative value to block indefinitely.

spring.redis.jedis.pool.min-idle=0 # Target for the minimum number of idle connections to maintain in the pool. This setting only has an effect if it is positive.

spring.redis.lettuce.pool.max-active=8 # Maximum number of connections that can be allocated by the pool at a given time. Use a negative value for no limit.

spring.redis.lettuce.pool.max-idle=8 # Maximum number of "idle" connections in the pool. Use a negative value to indicate an unlimited number of idle connections.

spring.redis.lettuce.pool.max-wait=-1ms # Maximum amount of time a connection allocation should block before throwing an exception when the pool is exhausted. Use a negative value to block indefinitely.

spring.redis.lettuce.pool.min-idle=0 # Target for the minimum number of idle connections to maintain in the pool. This setting only has an effect if it is positive.

spring.redis.lettuce.shutdown-timeout=100ms # Shutdown timeout.

spring.redis.password= # Login password of the redis server.

spring.redis.port=6379 # Redis server port.

spring.redis.sentinel.master= # Name of the Redis server.

spring.redis.sentinel.nodes= # Comma-separated list of "host:port" pairs.

spring.redis.ssl=false # Whether to enable SSL support.

spring.redis.timeout= # Connection timeout.

# TRANSACTION (TransactionProperties)

spring.transaction.default-timeout= # Default transaction timeout. If a duration suffix is not specified, seconds will be used.

spring.transaction.rollback-on-commit-failure= # Whether to roll back on commit failures.

# ----------------------------------------

# INTEGRATION PROPERTIES

# ----------------------------------------

# ACTIVEMQ (ActiveMQProperties)

spring.activemq.broker-url= # URL of the ActiveMQ broker. Auto-generated by default.

spring.activemq.close-timeout=15s # Time to wait before considering a close complete.

spring.activemq.in-memory=true # Whether the default broker URL should be in memory. Ignored if an explicit broker has been specified.

spring.activemq.non-blocking-redelivery=false # Whether to stop message delivery before re-delivering messages from a rolled back transaction. This implies that message order is not preserved when this is enabled.

spring.activemq.password= # Login password of the broker.

spring.activemq.send-timeout=0ms # Time to wait on message sends for a response. Set it to 0 to wait forever.

spring.activemq.user= # Login user of the broker.

spring.activemq.packages.trust-all= # Whether to trust all packages.

spring.activemq.packages.trusted= # Comma-separated list of specific packages to trust (when not trusting all packages).

spring.activemq.pool.block-if-full=true # Whether to block when a connection is requested and the pool is full. Set it to false to throw a "JMSException" instead.

spring.activemq.pool.block-if-full-timeout=-1ms # Blocking period before throwing an exception if the pool is still full.

spring.activemq.pool.enabled=false # Whether a PooledConnectionFactory should be created, instead of a regular ConnectionFactory.

spring.activemq.pool.idle-timeout=30s # Connection idle timeout.

spring.activemq.pool.max-connections=1 # Maximum number of pooled connections.

spring.activemq.pool.maximum-active-session-per-connection=500 # Maximum number of active sessions per connection.

spring.activemq.pool.time-between-expiration-check=-1ms # Time to sleep between runs of the idle connection eviction thread. When negative, no idle connection eviction thread runs.

spring.activemq.pool.use-anonymous-producers=true # Whether to use only one anonymous "MessageProducer" instance. Set it to false to create one "MessageProducer" every time one is required.

# ARTEMIS (ArtemisProperties)

spring.artemis.embedded.cluster-password= # Cluster password. Randomly generated on startup by default.

spring.artemis.embedded.data-directory= # Journal file directory. Not necessary if persistence is turned off.

spring.artemis.embedded.enabled=true # Whether to enable embedded mode if the Artemis server APIs are available.

spring.artemis.embedded.persistent=false # Whether to enable persistent store.

spring.artemis.embedded.queues= # Comma-separated list of queues to create on startup.

spring.artemis.embedded.server-id= # Server ID. By default, an auto-incremented counter is used.

spring.artemis.embedded.topics= # Comma-separated list of topics to create on startup.

spring.artemis.host=localhost # Artemis broker host.

spring.artemis.mode= # Artemis deployment mode, auto-detected by default.

spring.artemis.password= # Login password of the broker.

spring.artemis.port=61616 # Artemis broker port.

spring.artemis.user= # Login user of the broker.

# SPRING BATCH (BatchProperties)

spring.batch.initialize-schema=embedded # Database schema initialization mode.

spring.batch.job.enabled=true # Execute all Spring Batch jobs in the context on startup.

spring.batch.job.names= # Comma-separated list of job names to execute on startup (for instance, `job1,job2`). By default, all Jobs found in the context are executed.

spring.batch.schema=classpath:org/springframework/batch/core/schema-@@platform@@.sql # Path to the SQL file to use to initialize the database schema.

spring.batch.table-prefix= # Table prefix for all the batch meta-data tables.

# SPRING INTEGRATION (IntegrationProperties)

spring.integration.jdbc.initialize-schema=embedded # Database schema initialization mode.

spring.integration.jdbc.schema=classpath:org/springframework/integration/jdbc/schema-@@platform@@.sql # Path to the SQL file to use to initialize the database schema.

# JMS (JmsProperties)

spring.jms.jndi-name= # Connection factory JNDI name. When set, takes precedence to others connection factory auto-configurations.

spring.jms.listener.acknowledge-mode= # Acknowledge mode of the container. By default, the listener is transacted with automatic acknowledgment.

spring.jms.listener.auto-startup=true # Start the container automatically on startup.

spring.jms.listener.concurrency= # Minimum number of concurrent consumers.

spring.jms.listener.max-concurrency= # Maximum number of concurrent consumers.

spring.jms.pub-sub-domain=false # Whether the default destination type is topic.

spring.jms.template.default-destination= # Default destination to use on send and receive operations that do not have a destination parameter.

spring.jms.template.delivery-delay= # Delivery delay to use for send calls.

spring.jms.template.delivery-mode= # Delivery mode. Enables QoS (Quality of Service) when set.

spring.jms.template.priority= # Priority of a message when sending. Enables QoS (Quality of Service) when set.

spring.jms.template.qos-enabled= # Whether to enable explicit QoS (Quality of Service) when sending a message.

spring.jms.template.receive-timeout= # Timeout to use for receive calls.

spring.jms.template.time-to-live= # Time-to-live of a message when sending. Enables QoS (Quality of Service) when set.

# APACHE KAFKA (KafkaProperties)

spring.kafka.admin.client-id= # ID to pass to the server when making requests. Used for server-side logging.

spring.kafka.admin.fail-fast=false # Whether to fail fast if the broker is not available on startup.

spring.kafka.admin.properties.*= # Additional admin-specific properties used to configure the client.

spring.kafka.admin.ssl.key-password= # Password of the private key in the key store file.

spring.kafka.admin.ssl.keystore-location= # Location of the key store file.

spring.kafka.admin.ssl.keystore-password= # Store password for the key store file.

spring.kafka.admin.ssl.keystore-type= # Type of the key store.

spring.kafka.admin.ssl.protocol= # SSL protocol to use.

spring.kafka.admin.ssl.truststore-location= # Location of the trust store file.

spring.kafka.admin.ssl.truststore-password= # Store password for the trust store file.

spring.kafka.admin.ssl.truststore-type= # Type of the trust store.

spring.kafka.bootstrap-servers= # Comma-delimited list of host:port pairs to use for establishing the initial connections to the Kafka cluster. Applies to all components unless overridden.

spring.kafka.client-id= # ID to pass to the server when making requests. Used for server-side logging.

spring.kafka.consumer.auto-commit-interval= # Frequency with which the consumer offsets are auto-committed to Kafka if 'enable.auto.commit' is set to true.

spring.kafka.consumer.auto-offset-reset= # What to do when there is no initial offset in Kafka or if the current offset no longer exists on the server.

spring.kafka.consumer.bootstrap-servers= # Comma-delimited list of host:port pairs to use for establishing the initial connections to the Kafka cluster. Overrides the global property, for consumers.

spring.kafka.consumer.client-id= # ID to pass to the server when making requests. Used for server-side logging.

spring.kafka.consumer.enable-auto-commit= # Whether the consumer's offset is periodically committed in the background.

spring.kafka.consumer.fetch-max-wait= # Maximum amount of time the server blocks before answering the fetch request if there isn't sufficient data to immediately satisfy the requirement given by "fetch.min.bytes".

spring.kafka.consumer.fetch-min-size= # Minimum amount of data, in bytes, the server should return for a fetch request.

spring.kafka.consumer.group-id= # Unique string that identifies the consumer group to which this consumer belongs.

spring.kafka.consumer.heartbeat-interval= # Expected time between heartbeats to the consumer coordinator.

spring.kafka.consumer.key-deserializer= # Deserializer class for keys.

spring.kafka.consumer.max-poll-records= # Maximum number of records returned in a single call to poll().

spring.kafka.consumer.properties.*= # Additional consumer-specific properties used to configure the client.

spring.kafka.consumer.ssl.key-password= # Password of the private key in the key store file.

spring.kafka.consumer.ssl.keystore-location= # Location of the key store file.

spring.kafka.consumer.ssl.keystore-password= # Store password for the key store file.

spring.kafka.consumer.ssl.keystore-type= # Type of the key store.

spring.kafka.consumer.ssl.protocol= # SSL protocol to use.

spring.kafka.consumer.ssl.truststore-location= # Location of the trust store file.

spring.kafka.consumer.ssl.truststore-password= # Store password for the trust store file.

spring.kafka.consumer.ssl.truststore-type= # Type of the trust store.

spring.kafka.consumer.value-deserializer= # Deserializer class for values.

spring.kafka.jaas.control-flag=required # Control flag for login configuration.

spring.kafka.jaas.enabled=false # Whether to enable JAAS configuration.

spring.kafka.jaas.login-module=com.sun.security.auth.module.Krb5LoginModule # Login module.

spring.kafka.jaas.options= # Additional JAAS options.

spring.kafka.listener.ack-count= # Number of records between offset commits when ackMode is "COUNT" or "COUNT_TIME".

spring.kafka.listener.ack-mode= # Listener AckMode. See the spring-kafka documentation.

spring.kafka.listener.ack-time= # Time between offset commits when ackMode is "TIME" or "COUNT_TIME".

spring.kafka.listener.client-id= # Prefix for the listener's consumer client.id property.

spring.kafka.listener.concurrency= # Number of threads to run in the listener containers.

spring.kafka.listener.idle-event-interval= # Time between publishing idle consumer events (no data received).

spring.kafka.listener.log-container-config= # Whether to log the container configuration during initialization (INFO level).

spring.kafka.listener.monitor-interval= # Time between checks for non-responsive consumers. If a duration suffix is not specified, seconds will be used.

spring.kafka.listener.no-poll-threshold= # Multiplier applied to "pollTimeout" to determine if a consumer is non-responsive.

spring.kafka.listener.poll-timeout= # Timeout to use when polling the consumer.

spring.kafka.listener.type=single # Listener type.

spring.kafka.producer.acks= # Number of acknowledgments the producer requires the leader to have received before considering a request complete.

spring.kafka.producer.batch-size= # Default batch size in bytes.

spring.kafka.producer.bootstrap-servers= # Comma-delimited list of host:port pairs to use for establishing the initial connections to the Kafka cluster. Overrides the global property, for producers.

spring.kafka.producer.buffer-memory= # Total bytes of memory the producer can use to buffer records waiting to be sent to the server.

spring.kafka.producer.client-id= # ID to pass to the server when making requests. Used for server-side logging.

spring.kafka.producer.compression-type= # Compression type for all data generated by the producer.

spring.kafka.producer.key-serializer= # Serializer class for keys.

spring.kafka.producer.properties.*= # Additional producer-specific properties used to configure the client.

spring.kafka.producer.retries= # When greater than zero, enables retrying of failed sends.

spring.kafka.producer.ssl.key-password= # Password of the private key in the key store file.

spring.kafka.producer.ssl.keystore-location= # Location of the key store file.

spring.kafka.producer.ssl.keystore-password= # Store password for the key store file.

spring.kafka.producer.ssl.keystore-type= # Type of the key store.

spring.kafka.producer.ssl.protocol= # SSL protocol to use.

spring.kafka.producer.ssl.truststore-location= # Location of the trust store file.

spring.kafka.producer.ssl.truststore-password= # Store password for the trust store file.

spring.kafka.producer.ssl.truststore-type= # Type of the trust store.

spring.kafka.producer.transaction-id-prefix= # When non empty, enables transaction support for producer.

spring.kafka.producer.value-serializer= # Serializer class for values.

spring.kafka.properties.*= # Additional properties, common to producers and consumers, used to configure the client.

spring.kafka.ssl.key-password= # Password of the private key in the key store file.

spring.kafka.ssl.keystore-location= # Location of the key store file.

spring.kafka.ssl.keystore-password= # Store password for the key store file.

spring.kafka.ssl.keystore-type= # Type of the key store.

spring.kafka.ssl.protocol= # SSL protocol to use.

spring.kafka.ssl.truststore-location= # Location of the trust store file.

spring.kafka.ssl.truststore-password= # Store password for the trust store file.

spring.kafka.ssl.truststore-type= # Type of the trust store.

spring.kafka.template.default-topic= # Default topic to which messages are sent.

# RABBIT (RabbitProperties)

spring.rabbitmq.addresses= # Comma-separated list of addresses to which the client should connect.

spring.rabbitmq.cache.channel.checkout-timeout= # Duration to wait to obtain a channel if the cache size has been reached.

spring.rabbitmq.cache.channel.size= # Number of channels to retain in the cache.

spring.rabbitmq.cache.connection.mode=channel # Connection factory cache mode.

spring.rabbitmq.cache.connection.size= # Number of connections to cache.

spring.rabbitmq.connection-timeout= # Connection timeout. Set it to zero to wait forever.

spring.rabbitmq.dynamic=true # Whether to create an AmqpAdmin bean.

spring.rabbitmq.host=localhost # RabbitMQ host.

spring.rabbitmq.listener.direct.acknowledge-mode= # Acknowledge mode of container.

spring.rabbitmq.listener.direct.auto-startup=true # Whether to start the container automatically on startup.

spring.rabbitmq.listener.direct.consumers-per-queue= # Number of consumers per queue.

spring.rabbitmq.listener.direct.default-requeue-rejected= # Whether rejected deliveries are re-queued by default.

spring.rabbitmq.listener.direct.idle-event-interval= # How often idle container events should be published.

spring.rabbitmq.listener.direct.prefetch= # Number of messages to be handled in a single request. It should be greater than or equal to the transaction size (if used).

spring.rabbitmq.listener.direct.retry.enabled=false # Whether publishing retries are enabled.

spring.rabbitmq.listener.direct.retry.initial-interval=1000ms # Duration between the first and second attempt to deliver a message.

spring.rabbitmq.listener.direct.retry.max-attempts=3 # Maximum number of attempts to deliver a message.

spring.rabbitmq.listener.direct.retry.max-interval=10000ms # Maximum duration between attempts.

spring.rabbitmq.listener.direct.retry.multiplier=1 # Multiplier to apply to the previous retry interval.

spring.rabbitmq.listener.direct.retry.stateless=true # Whether retries are stateless or stateful.

spring.rabbitmq.listener.simple.acknowledge-mode= # Acknowledge mode of container.

spring.rabbitmq.listener.simple.auto-startup=true # Whether to start the container automatically on startup.

spring.rabbitmq.listener.simple.concurrency= # Minimum number of listener invoker threads.

spring.rabbitmq.listener.simple.default-requeue-rejected= # Whether rejected deliveries are re-queued by default.

spring.rabbitmq.listener.simple.idle-event-interval= # How often idle container events should be published.

spring.rabbitmq.listener.simple.max-concurrency= # Maximum number of listener invoker threads.

spring.rabbitmq.listener.simple.prefetch= # Number of messages to be handled in a single request. It should be greater than or equal to the transaction size (if used).

spring.rabbitmq.listener.simple.retry.enabled=false # Whether publishing retries are enabled.

spring.rabbitmq.listener.simple.retry.initial-interval=1000ms # Duration between the first and second attempt to deliver a message.

spring.rabbitmq.listener.simple.retry.max-attempts=3 # Maximum number of attempts to deliver a message.

spring.rabbitmq.listener.simple.retry.max-interval=10000ms # Maximum duration between attempts.

spring.rabbitmq.listener.simple.retry.multiplier=1 # Multiplier to apply to the previous retry interval.

spring.rabbitmq.listener.simple.retry.stateless=true # Whether retries are stateless or stateful.

spring.rabbitmq.listener.simple.transaction-size= # Number of messages to be processed in a transaction. That is, the number of messages between acks. For best results, it should be less than or equal to the prefetch count.

spring.rabbitmq.listener.type=simple # Listener container type.

spring.rabbitmq.password=guest # Login to authenticate against the broker.

spring.rabbitmq.port=5672 # RabbitMQ port.

spring.rabbitmq.publisher-confirms=false # Whether to enable publisher confirms.

spring.rabbitmq.publisher-returns=false # Whether to enable publisher returns.

spring.rabbitmq.requested-heartbeat= # Requested heartbeat timeout; zero for none. If a duration suffix is not specified, seconds will be used.

spring.rabbitmq.ssl.algorithm= # SSL algorithm to use. By default, configured by the Rabbit client library.

spring.rabbitmq.ssl.enabled=false # Whether to enable SSL support.

spring.rabbitmq.ssl.key-store= # Path to the key store that holds the SSL certificate.

spring.rabbitmq.ssl.key-store-password= # Password used to access the key store.

spring.rabbitmq.ssl.key-store-type=PKCS12 # Key store type.

spring.rabbitmq.ssl.trust-store= # Trust store that holds SSL certificates.

spring.rabbitmq.ssl.trust-store-password= # Password used to access the trust store.

spring.rabbitmq.ssl.trust-store-type=JKS # Trust store type.

spring.rabbitmq.ssl.validate-server-certificate=true # Whether to enable server side certificate validation.

spring.rabbitmq.ssl.verify-hostname= # Whether to enable hostname verification. Requires AMQP client 4.8 or above and defaults to true when a suitable client version is used.

spring.rabbitmq.template.exchange= # Name of the default exchange to use for send operations.

spring.rabbitmq.template.mandatory= # Whether to enable mandatory messages.

spring.rabbitmq.template.receive-timeout= # Timeout for `receive()` operations.

spring.rabbitmq.template.reply-timeout= # Timeout for `sendAndReceive()` operations.

spring.rabbitmq.template.retry.enabled=false # Whether publishing retries are enabled.

spring.rabbitmq.template.retry.initial-interval=1000ms # Duration between the first and second attempt to deliver a message.

spring.rabbitmq.template.retry.max-attempts=3 # Maximum number of attempts to deliver a message.

spring.rabbitmq.template.retry.max-interval=10000ms # Maximum duration between attempts.

spring.rabbitmq.template.retry.multiplier=1 # Multiplier to apply to the previous retry interval.

spring.rabbitmq.template.routing-key= # Value of a default routing key to use for send operations.

spring.rabbitmq.username=guest # Login user to authenticate to the broker.

spring.rabbitmq.virtual-host= # Virtual host to use when connecting to the broker.

# ----------------------------------------

# ACTUATOR PROPERTIES

# ----------------------------------------

# MANAGEMENT HTTP SERVER (ManagementServerProperties)

management.server.add-application-context-header=false # Add the "X-Application-Context" HTTP header in each response.

management.server.address= # Network address to which the management endpoints should bind. Requires a custom management.server.port.

management.server.port= # Management endpoint HTTP port (uses the same port as the application by default). Configure a different port to use management-specific SSL.

management.server.servlet.context-path= # Management endpoint context-path (for instance, `/management`). Requires a custom management.server.port.

management.server.ssl.ciphers= # Supported SSL ciphers. Requires a custom management.port.

management.server.ssl.client-auth= # Whether client authentication is wanted ("want") or needed ("need"). Requires a trust store. Requires a custom management.server.port.

management.server.ssl.enabled= # Whether to enable SSL support. Requires a custom management.server.port.

management.server.ssl.enabled-protocols= # Enabled SSL protocols. Requires a custom management.server.port.

management.server.ssl.key-alias= # Alias that identifies the key in the key store. Requires a custom management.server.port.

management.server.ssl.key-password= # Password used to access the key in the key store. Requires a custom management.server.port.

management.server.ssl.key-store= # Path to the key store that holds the SSL certificate (typically a jks file). Requires a custom management.server.port.

management.server.ssl.key-store-password= # Password used to access the key store. Requires a custom management.server.port.

management.server.ssl.key-store-provider= # Provider for the key store. Requires a custom management.server.port.

management.server.ssl.key-store-type= # Type of the key store. Requires a custom management.server.port.

management.server.ssl.protocol=TLS # SSL protocol to use. Requires a custom management.server.port.

management.server.ssl.trust-store= # Trust store that holds SSL certificates. Requires a custom management.server.port.

management.server.ssl.trust-store-password= # Password used to access the trust store. Requires a custom management.server.port.

management.server.ssl.trust-store-provider= # Provider for the trust store. Requires a custom management.server.port.

management.server.ssl.trust-store-type= # Type of the trust store. Requires a custom management.server.port.

# CLOUDFOUNDRY

management.cloudfoundry.enabled=true # Whether to enable extended Cloud Foundry actuator endpoints.

management.cloudfoundry.skip-ssl-validation=false # Whether to skip SSL verification for Cloud Foundry actuator endpoint security calls.

# ENDPOINTS GENERAL CONFIGURATION

management.endpoints.enabled-by-default= # Whether to enable or disable all endpoints by default.

# ENDPOINTS JMX CONFIGURATION (JmxEndpointProperties)

management.endpoints.jmx.domain=org.springframework.boot # Endpoints JMX domain name. Fallback to 'spring.jmx.default-domain' if set.

management.endpoints.jmx.exposure.include=* # Endpoint IDs that should be included or '*' for all.

management.endpoints.jmx.exposure.exclude= # Endpoint IDs that should be excluded or '*' for all.

management.endpoints.jmx.static-names= # Additional static properties to append to all ObjectNames of MBeans representing Endpoints.

management.endpoints.jmx.unique-names=false # Whether to ensure that ObjectNames are modified in case of conflict.

# ENDPOINTS WEB CONFIGURATION (WebEndpointProperties)

management.endpoints.web.exposure.include=health,info # Endpoint IDs that should be included or '*' for all.

management.endpoints.web.exposure.exclude= # Endpoint IDs that should be excluded or '*' for all.

management.endpoints.web.base-path=/actuator # Base path for Web endpoints. Relative to server.servlet.context-path or management.server.servlet.context-path if management.server.port is configured.

management.endpoints.web.path-mapping= # Mapping between endpoint IDs and the path that should expose them.

# ENDPOINTS CORS CONFIGURATION (CorsEndpointProperties)

management.endpoints.web.cors.allow-credentials= # Whether credentials are supported. When not set, credentials are not supported.

management.endpoints.web.cors.allowed-headers= # Comma-separated list of headers to allow in a request. '*' allows all headers.

management.endpoints.web.cors.allowed-methods= # Comma-separated list of methods to allow. '*' allows all methods. When not set, defaults to GET.

management.endpoints.web.cors.allowed-origins= # Comma-separated list of origins to allow. '*' allows all origins. When not set, CORS support is disabled.

management.endpoints.web.cors.exposed-headers= # Comma-separated list of headers to include in a response.

management.endpoints.web.cors.max-age=1800s # How long the response from a pre-flight request can be cached by clients. If a duration suffix is not specified, seconds will be used.

# AUDIT EVENTS ENDPOINT (AuditEventsEndpoint)

management.endpoint.auditevents.cache.time-to-live=0ms # Maximum time that a response can be cached.

management.endpoint.auditevents.enabled=true # Whether to enable the auditevents endpoint.

# BEANS ENDPOINT (BeansEndpoint)

management.endpoint.beans.cache.time-to-live=0ms # Maximum time that a response can be cached.

management.endpoint.beans.enabled=true # Whether to enable the beans endpoint.

# CONDITIONS REPORT ENDPOINT (ConditionsReportEndpoint)

management.endpoint.conditions.cache.time-to-live=0ms # Maximum time that a response can be cached.

management.endpoint.conditions.enabled=true # Whether to enable the conditions endpoint.

# CONFIGURATION PROPERTIES REPORT ENDPOINT (ConfigurationPropertiesReportEndpoint, ConfigurationPropertiesReportEndpointProperties)

management.endpoint.configprops.cache.time-to-live=0ms # Maximum time that a response can be cached.

management.endpoint.configprops.enabled=true # Whether to enable the configprops endpoint.

management.endpoint.configprops.keys-to-sanitize=password,secret,key,token,.*credentials.*,vcap_services,sun.java.command # Keys that should be sanitized. Keys can be simple strings that the property ends with or regular expressions.

# ENVIRONMENT ENDPOINT (EnvironmentEndpoint, EnvironmentEndpointProperties)

management.endpoint.env.cache.time-to-live=0ms # Maximum time that a response can be cached.

management.endpoint.env.enabled=true # Whether to enable the env endpoint.

management.endpoint.env.keys-to-sanitize=password,secret,key,token,.*credentials.*,vcap_services,sun.java.command # Keys that should be sanitized. Keys can be simple strings that the property ends with or regular expressions.

# FLYWAY ENDPOINT (FlywayEndpoint)

management.endpoint.flyway.cache.time-to-live=0ms # Maximum time that a response can be cached.

management.endpoint.flyway.enabled=true # Whether to enable the flyway endpoint.

# HEALTH ENDPOINT (HealthEndpoint, HealthEndpointProperties)

management.endpoint.health.cache.time-to-live=0ms # Maximum time that a response can be cached.

management.endpoint.health.enabled=true # Whether to enable the health endpoint.

management.endpoint.health.roles= # Roles used to determine whether or not a user is authorized to be shown details. When empty, all authenticated users are authorized.

management.endpoint.health.show-details=never # When to show full health details.

# HEAP DUMP ENDPOINT (HeapDumpWebEndpoint)

management.endpoint.heapdump.cache.time-to-live=0ms # Maximum time that a response can be cached.

management.endpoint.heapdump.enabled=true # Whether to enable the heapdump endpoint.

# HTTP TRACE ENDPOINT (HttpTraceEndpoint)

management.endpoint.httptrace.cache.time-to-live=0ms # Maximum time that a response can be cached.

management.endpoint.httptrace.enabled=true # Whether to enable the httptrace endpoint.

# INFO ENDPOINT (InfoEndpoint)

info= # Arbitrary properties to add to the info endpoint.

management.endpoint.info.cache.time-to-live=0ms # Maximum time that a response can be cached.

management.endpoint.info.enabled=true # Whether to enable the info endpoint.

# JOLOKIA ENDPOINT (JolokiaProperties)

management.endpoint.jolokia.config.*= # Jolokia settings. Refer to the documentation of Jolokia for more details.

management.endpoint.jolokia.enabled=true # Whether to enable the jolokia endpoint.

# LIQUIBASE ENDPOINT (LiquibaseEndpoint)

management.endpoint.liquibase.cache.time-to-live=0ms # Maximum time that a response can be cached.

management.endpoint.liquibase.enabled=true # Whether to enable the liquibase endpoint.

# LOG FILE ENDPOINT (LogFileWebEndpoint, LogFileWebEndpointProperties)

management.endpoint.logfile.cache.time-to-live=0ms # Maximum time that a response can be cached.

management.endpoint.logfile.enabled=true # Whether to enable the logfile endpoint.

management.endpoint.logfile.external-file= # External Logfile to be accessed. Can be used if the logfile is written by output redirect and not by the logging system itself.

# LOGGERS ENDPOINT (LoggersEndpoint)

management.endpoint.loggers.cache.time-to-live=0ms # Maximum time that a response can be cached.

management.endpoint.loggers.enabled=true # Whether to enable the loggers endpoint.

# REQUEST MAPPING ENDPOINT  (MappingsEndpoint)

management.endpoint.mappings.cache.time-to-live=0ms # Maximum time that a response can be cached.

management.endpoint.mappings.enabled=true # Whether to enable the mappings endpoint.

# METRICS ENDPOINT (MetricsEndpoint)

management.endpoint.metrics.cache.time-to-live=0ms # Maximum time that a response can be cached.

management.endpoint.metrics.enabled=true # Whether to enable the metrics endpoint.

# PROMETHEUS ENDPOINT (PrometheusScrapeEndpoint)

management.endpoint.prometheus.cache.time-to-live=0ms # Maximum time that a response can be cached.

management.endpoint.prometheus.enabled=true # Whether to enable the prometheus endpoint.

# SCHEDULED TASKS ENDPOINT (ScheduledTasksEndpoint)

management.endpoint.scheduledtasks.cache.time-to-live=0ms # Maximum time that a response can be cached.

management.endpoint.scheduledtasks.enabled=true # Whether to enable the scheduledtasks endpoint.

# SESSIONS ENDPOINT (SessionsEndpoint)

management.endpoint.sessions.enabled=true # Whether to enable the sessions endpoint.

# SHUTDOWN ENDPOINT (ShutdownEndpoint)

management.endpoint.shutdown.enabled=false # Whether to enable the shutdown endpoint.

# THREAD DUMP ENDPOINT (ThreadDumpEndpoint)

management.endpoint.threaddump.cache.time-to-live=0ms # Maximum time that a response can be cached.

management.endpoint.threaddump.enabled=true # Whether to enable the threaddump endpoint.

# HEALTH INDICATORS

management.health.db.enabled=true # Whether to enable database health check.

management.health.cassandra.enabled=true # Whether to enable Cassandra health check.

management.health.couchbase.enabled=true # Whether to enable Couchbase health check.

management.health.couchbase.timeout=1000ms # Timeout for getting the Bucket information from the server.

management.health.defaults.enabled=true # Whether to enable default health indicators.

management.health.diskspace.enabled=true # Whether to enable disk space health check.

management.health.diskspace.path= # Path used to compute the available disk space.

management.health.diskspace.threshold=0 # Minimum disk space, in bytes, that should be available.

management.health.elasticsearch.enabled=true # Whether to enable Elasticsearch health check.

management.health.elasticsearch.indices= # Comma-separated index names.

management.health.elasticsearch.response-timeout=100ms # Time to wait for a response from the cluster.

management.health.influxdb.enabled=true # Whether to enable InfluxDB health check.

management.health.jms.enabled=true # Whether to enable JMS health check.

management.health.ldap.enabled=true # Whether to enable LDAP health check.

management.health.mail.enabled=true # Whether to enable Mail health check.

management.health.mongo.enabled=true # Whether to enable MongoDB health check.

management.health.neo4j.enabled=true # Whether to enable Neo4j health check.

management.health.rabbit.enabled=true # Whether to enable RabbitMQ health check.

management.health.redis.enabled=true # Whether to enable Redis health check.

management.health.solr.enabled=true # Whether to enable Solr health check.

management.health.status.http-mapping= # Mapping of health statuses to HTTP status codes. By default, registered health statuses map to sensible defaults (for example, UP maps to 200).

management.health.status.order=DOWN,OUT_OF_SERVICE,UP,UNKNOWN # Comma-separated list of health statuses in order of severity.

# HTTP TRACING (HttpTraceProperties)

management.trace.http.enabled=true # Whether to enable HTTP request-response tracing.

management.trace.http.include=request-headers,response-headers,cookies,errors # Items to be included in the trace.

# INFO CONTRIBUTORS (InfoContributorProperties)

management.info.build.enabled=true # Whether to enable build info.

management.info.defaults.enabled=true # Whether to enable default info contributors.

management.info.env.enabled=true # Whether to enable environment info.

management.info.git.enabled=true # Whether to enable git info.

management.info.git.mode=simple # Mode to use to expose git information.

# METRICS

management.metrics.binders.files.enabled=true # Whether to enable files metrics.

management.metrics.binders.jvm.enabled=true # Whether to enable JVM metrics.

management.metrics.binders.logback.enabled=true # Whether to enable Logback metrics.

management.metrics.binders.processor.enabled=true # Whether to enable processor metrics.

management.metrics.binders.uptime.enabled=true # Whether to enable uptime metrics.

management.metrics.distribution.percentiles-histogram.*= # Whether meter IDs starting-with the specified name should be publish percentile histograms.

management.metrics.distribution.percentiles.*= # Specific computed non-aggregable percentiles to ship to the backend for meter IDs starting-with the specified name.

management.metrics.distribution.sla.*= # Specific SLA boundaries for meter IDs starting-with the specified name. The longest match wins, the key `all` can also be used to configure all meters.

management.metrics.enable.*= # Whether meter IDs starting-with the specified name should be enabled. The longest match wins, the key `all` can also be used to configure all meters.

management.metrics.export.atlas.batch-size=10000 # Number of measurements per request to use for this backend. If more measurements are found, then multiple requests will be made.

management.metrics.export.atlas.config-refresh-frequency=10s # Frequency for refreshing config settings from the LWC service.

management.metrics.export.atlas.config-time-to-live=150s # Time to live for subscriptions from the LWC service.

management.metrics.export.atlas.config-uri=http://localhost:7101/lwc/api/v1/expressions/local-dev # URI for the Atlas LWC endpoint to retrieve current subscriptions.

management.metrics.export.atlas.connect-timeout=1s # Connection timeout for requests to this backend.

management.metrics.export.atlas.enabled=true # Whether exporting of metrics to this backend is enabled.

management.metrics.export.atlas.eval-uri=http://localhost:7101/lwc/api/v1/evaluate # URI for the Atlas LWC endpoint to evaluate the data for a subscription.

management.metrics.export.atlas.lwc-enabled=false # Whether to enable streaming to Atlas LWC.

management.metrics.export.atlas.meter-time-to-live=15m # Time to live for meters that do not have any activity. After this period the meter will be considered expired and will not get reported.

management.metrics.export.atlas.num-threads=2 # Number of threads to use with the metrics publishing scheduler.

management.metrics.export.atlas.read-timeout=10s # Read timeout for requests to this backend.

management.metrics.export.atlas.step=1m # Step size (i.e. reporting frequency) to use.

management.metrics.export.atlas.uri=http://localhost:7101/api/v1/publish # URI of the Atlas server.

management.metrics.export.datadog.api-key= # Datadog API key.

management.metrics.export.datadog.application-key= # Datadog application key. Not strictly required, but improves the Datadog experience by sending meter descriptions, types, and base units to Datadog.

management.metrics.export.datadog.batch-size=10000 # Number of measurements per request to use for this backend. If more measurements are found, then multiple requests will be made.

management.metrics.export.datadog.connect-timeout=1s # Connection timeout for requests to this backend.

management.metrics.export.datadog.descriptions=true # Whether to publish descriptions metadata to Datadog. Turn this off to minimize the amount of metadata sent.

management.metrics.export.datadog.enabled=true # Whether exporting of metrics to this backend is enabled.

management.metrics.export.datadog.host-tag=instance # Tag that will be mapped to "host" when shipping metrics to Datadog.

management.metrics.export.datadog.num-threads=2 # Number of threads to use with the metrics publishing scheduler.

management.metrics.export.datadog.read-timeout=10s # Read timeout for requests to this backend.

management.metrics.export.datadog.step=1m # Step size (i.e. reporting frequency) to use.

management.metrics.export.datadog.uri=https://app.datadoghq.com # URI to ship metrics to. If you need to publish metrics to an internal proxy en-route to Datadog, you can define the location of the proxy with this.

management.metrics.export.ganglia.addressing-mode=multicast # UDP addressing mode, either unicast or multicast.

management.metrics.export.ganglia.duration-units=milliseconds # Base time unit used to report durations.

management.metrics.export.ganglia.enabled=true # Whether exporting of metrics to Ganglia is enabled.

management.metrics.export.ganglia.host=localhost # Host of the Ganglia server to receive exported metrics.

management.metrics.export.ganglia.port=8649 # Port of the Ganglia server to receive exported metrics.

management.metrics.export.ganglia.protocol-version=3.1 # Ganglia protocol version. Must be either 3.1 or 3.0.

management.metrics.export.ganglia.rate-units=seconds # Base time unit used to report rates.

management.metrics.export.ganglia.step=1m # Step size (i.e. reporting frequency) to use.

management.metrics.export.ganglia.time-to-live=1 # Time to live for metrics on Ganglia. Set the multi-cast Time-To-Live to be one greater than the number of hops (routers) between the hosts.

management.metrics.export.graphite.duration-units=milliseconds # Base time unit used to report durations.

management.metrics.export.graphite.enabled=true # Whether exporting of metrics to Graphite is enabled.

management.metrics.export.graphite.host=localhost # Host of the Graphite server to receive exported metrics.

management.metrics.export.graphite.port=2004 # Port of the Graphite server to receive exported metrics.

management.metrics.export.graphite.protocol=pickled # Protocol to use while shipping data to Graphite.

management.metrics.export.graphite.rate-units=seconds # Base time unit used to report rates.

management.metrics.export.graphite.step=1m # Step size (i.e. reporting frequency) to use.

management.metrics.export.graphite.tags-as-prefix= # For the default naming convention, turn the specified tag keys into part of the metric prefix.

management.metrics.export.influx.auto-create-db=true # Whether to create the Influx database if it does not exist before attempting to publish metrics to it.

management.metrics.export.influx.batch-size=10000 # Number of measurements per request to use for this backend. If more measurements are found, then multiple requests will be made.

management.metrics.export.influx.compressed=true # Whether to enable GZIP compression of metrics batches published to Influx.

management.metrics.export.influx.connect-timeout=1s # Connection timeout for requests to this backend.

management.metrics.export.influx.consistency=one # Write consistency for each point.

management.metrics.export.influx.db=mydb # Tag that will be mapped to "host" when shipping metrics to Influx.

management.metrics.export.influx.enabled=true # Whether exporting of metrics to this backend is enabled.

management.metrics.export.influx.num-threads=2 # Number of threads to use with the metrics publishing scheduler.

management.metrics.export.influx.password= # Login password of the Influx server.

management.metrics.export.influx.read-timeout=10s # Read timeout for requests to this backend.

management.metrics.export.influx.retention-duration= # Time period for which Influx should retain data in the current database.

management.metrics.export.influx.retention-shard-duration= # Time range covered by a shard group.

management.metrics.export.influx.retention-policy= # Retention policy to use (Influx writes to the DEFAULT retention policy if one is not specified).

management.metrics.export.influx.retention-replication-factor= # How many copies of the data are stored in the cluster.

management.metrics.export.influx.step=1m # Step size (i.e. reporting frequency) to use.

management.metrics.export.influx.uri=http://localhost:8086 # URI of the Influx server.

management.metrics.export.influx.user-name= # Login user of the Influx server.

management.metrics.export.jmx.domain=metrics # Metrics JMX domain name.

management.metrics.export.jmx.enabled=true # Whether exporting of metrics to JMX is enabled.

management.metrics.export.jmx.step=1m # Step size (i.e. reporting frequency) to use.

management.metrics.export.newrelic.account-id= # New Relic account ID.

management.metrics.export.newrelic.api-key= # New Relic API key.

management.metrics.export.newrelic.batch-size=10000 # Number of measurements per request to use for this backend. If more measurements are found, then multiple requests will be made.

management.metrics.export.newrelic.connect-timeout=1s # Connection timeout for requests to this backend.

management.metrics.export.newrelic.enabled=true # Whether exporting of metrics to this backend is enabled.

management.metrics.export.newrelic.num-threads=2 # Number of threads to use with the metrics publishing scheduler.

management.metrics.export.newrelic.read-timeout=10s # Read timeout for requests to this backend.

management.metrics.export.newrelic.step=1m # Step size (i.e. reporting frequency) to use.

management.metrics.export.newrelic.uri=https://insights-collector.newrelic.com # URI to ship metrics to.

management.metrics.export.prometheus.descriptions=true # Whether to enable publishing descriptions as part of the scrape payload to Prometheus. Turn this off to minimize the amount of data sent on each scrape.

management.metrics.export.prometheus.enabled=true # Whether exporting of metrics to Prometheus is enabled.

management.metrics.export.prometheus.step=1m # Step size (i.e. reporting frequency) to use.

management.metrics.export.signalfx.access-token= # SignalFX access token.

management.metrics.export.signalfx.batch-size=10000 # Number of measurements per request to use for this backend. If more measurements are found, then multiple requests will be made.

management.metrics.export.signalfx.connect-timeout=1s # Connection timeout for requests to this backend.

management.metrics.export.signalfx.enabled=true # Whether exporting of metrics to this backend is enabled.

management.metrics.export.signalfx.num-threads=2 # Number of threads to use with the metrics publishing scheduler.

management.metrics.export.signalfx.read-timeout=10s # Read timeout for requests to this backend.

management.metrics.export.signalfx.source= # Uniquely identifies the app instance that is publishing metrics to SignalFx. Defaults to the local host name.

management.metrics.export.signalfx.step=10s # Step size (i.e. reporting frequency) to use.

management.metrics.export.signalfx.uri=https://ingest.signalfx.com # URI to ship metrics to.

management.metrics.export.simple.enabled=true # Whether, in the absence of any other exporter, exporting of metrics to an in-memory backend is enabled.

management.metrics.export.simple.mode=cumulative # Counting mode.

management.metrics.export.simple.step=1m # Step size (i.e. reporting frequency) to use.

management.metrics.export.statsd.enabled=true # Whether exporting of metrics to StatsD is enabled.

management.metrics.export.statsd.flavor=datadog # StatsD line protocol to use.

management.metrics.export.statsd.host=localhost # Host of the StatsD server to receive exported metrics.

management.metrics.export.statsd.max-packet-length=1400 # Total length of a single payload should be kept within your network's MTU.

management.metrics.export.statsd.polling-frequency=10s # How often gauges will be polled. When a gauge is polled, its value is recalculated and if the value has changed (or publishUnchangedMeters is true), it is sent to the StatsD server.

management.metrics.export.statsd.port=8125 # Port of the StatsD server to receive exported metrics.

management.metrics.export.statsd.publish-unchanged-meters=true # Whether to send unchanged meters to the StatsD server.

management.metrics.export.wavefront.api-token= # API token used when publishing metrics directly to the Wavefront API host.

management.metrics.export.wavefront.batch-size=10000 # Number of measurements per request to use for this backend. If more measurements are found, then multiple requests will be made.

management.metrics.export.wavefront.connect-timeout=1s # Connection timeout for requests to this backend.

management.metrics.export.wavefront.enabled=true # Whether exporting of metrics to this backend is enabled.

management.metrics.export.wavefront.global-prefix= # Global prefix to separate metrics originating from this app's white box instrumentation from those originating from other Wavefront integrations when viewed in the Wavefront UI.

management.metrics.export.wavefront.num-threads=2 # Number of threads to use with the metrics publishing scheduler.

management.metrics.export.wavefront.read-timeout=10s # Read timeout for requests to this backend.

management.metrics.export.wavefront.source= # Unique identifier for the app instance that is the source of metrics being published to Wavefront. Defaults to the local host name.

management.metrics.export.wavefront.step=10s # Step size (i.e. reporting frequency) to use.

management.metrics.export.wavefront.uri=https://longboard.wavefront.com # URI to ship metrics to.

management.metrics.use-global-registry=true # Whether auto-configured MeterRegistry implementations should be bound to the global static registry on Metrics.

management.metrics.web.client.max-uri-tags=100 # Maximum number of unique URI tag values allowed. After the max number of tag values is reached, metrics with additional tag values are denied by filter.

management.metrics.web.client.requests-metric-name=http.client.requests # Name of the metric for sent requests.

management.metrics.web.server.auto-time-requests=true # Whether requests handled by Spring MVC or WebFlux should be automatically timed.

management.metrics.web.server.max-uri-tags=100 # Maximum number of unique URI tag values allowed. After the max number of tag values is reached, metrics with additional tag values are denied by filter.

management.metrics.web.server.requests-metric-name=http.server.requests # Name of the metric for received requests.

# ----------------------------------------

# DEVTOOLS PROPERTIES

# ----------------------------------------

# DEVTOOLS (DevToolsProperties)

spring.devtools.livereload.enabled=true # Whether to enable a livereload.com-compatible server.

spring.devtools.livereload.port=35729 # Server port.

spring.devtools.restart.additional-exclude= # Additional patterns that should be excluded from triggering a full restart.

spring.devtools.restart.additional-paths= # Additional paths to watch for changes.

spring.devtools.restart.enabled=true # Whether to enable automatic restart.

spring.devtools.restart.exclude=META-INF/maven/**,META-INF/resources/**,resources/**,static/**,public/**,templates/**,**/*Test.class,**/*Tests.class,git.properties,META-INF/build-info.properties # Patterns that should be excluded from triggering a full restart.

spring.devtools.restart.log-condition-evaluation-delta=true # Whether to log the condition evaluation delta upon restart.

spring.devtools.restart.poll-interval=1s # Amount of time to wait between polling for classpath changes.

spring.devtools.restart.quiet-period=400ms # Amount of quiet time required without any classpath changes before a restart is triggered.

spring.devtools.restart.trigger-file= # Name of a specific file that, when changed, triggers the restart check. If not specified, any classpath file change triggers the restart.

# REMOTE DEVTOOLS (RemoteDevToolsProperties)

spring.devtools.remote.context-path=/.~~spring-boot!~ # Context path used to handle the remote connection.

spring.devtools.remote.proxy.host= # The host of the proxy to use to connect to the remote application.

spring.devtools.remote.proxy.port= # The port of the proxy to use to connect to the remote application.

spring.devtools.remote.restart.enabled=true # Whether to enable remote restart.

spring.devtools.remote.secret= # A shared secret required to establish a connection (required to enable remote support).

spring.devtools.remote.secret-header-name=X-AUTH-TOKEN # HTTP header used to transfer the shared secret.

# ----------------------------------------

# TESTING PROPERTIES

# ----------------------------------------

spring..database.replace=any # Type of existing DataSource to replace.

spring..mockmvc.print=default # MVC Print option.

启动指定参数

配置资源文件位置，默认application.properties是放在jar包中的，通过spring.config.location可以制定外部配置文件，这样更便于运维。

--spring.config.location 指定配置文件

java -jar demo.jar --spring.config.location=/opt/config/application.properties

--spring.profiles.active 切换配置文件

java -jar demo.jar --spring.profiles.active=dev

src/main/resources 准备三个环境的配置文件

application-dev.properties

application-pro.properties

application-tes.properties

加载排除

spring.autoconfigure.exclude=org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration

PID FILE

spring.pid.fail-on-write-error= # Fail if ApplicationPidFileWriter is used but it cannot write the PID file.

spring.pid.file= # Location of the PID file to write (if ApplicationPidFileWriter is used).

banner 关闭

spring.main.banner-mode=off

server

优雅关闭

server.shutdown=graceful

# timeout-per-shutdown-phase 默认缓冲 30秒,超时会强行关闭

spring.lifecycle.timeout-per-shutdown-phase=60s

启动之后使用 kill / kill -2 杀掉进城，注意不能使用 kill -9

neo@MacBook-Pro-M2 engineering % java -jar target/engineering-0.0.1-SNAPSHOT.jar

neo@MacBook-Pro-M2 engineering % ps ax | grep engineering

57854 s005  S+     0:12.45 /usr/bin/java -jar target/engineering-0.0.1-SNAPSHOT.jar

53810 s006  Ss+    0:00.12 /opt/homebrew/bin/fish --init-command=source '/Applications/IntelliJ IDEA CE.app/Contents/plugins/terminal/fish/init.fish' --login -i

56253 s007  Ss+    0:00.10 /opt/homebrew/bin/fish

neo@MacBook-Pro-M2 engineering % kill -2 57854

日志输出 Graceful shutdown complete 字样

2023-04-24T17:34:30.535+08:00  INFO 57854 --- [ionShutdownHook] o.s.b.w.e.undertow.UndertowWebServer     : Commencing graceful shutdown. Waiting for active requests to complete

2023-04-24T17:34:30.554+08:00  INFO 57854 --- [ionShutdownHook] o.s.b.w.e.undertow.UndertowWebServer     : Graceful shutdown complete

端口配置

server.port=8080 # 监听端口

server.address= # 绑定的地址

随机产生端口

# 方法一

server.port=0

# 方法二

server.port=${random.int[1000,1999]}

Session 配置

server.session.persistent 重启时是否持久化session，默认false

server.session.timeout session的超时时间

server.session.tracking-modes 设定Session的追踪模式(cookie, url, ssl).

server.session.timeout=1800 #session有效时长

cookie

server.session.cookie.comment 指定session cookie的comment

server.session.cookie.domain 指定session cookie的domain

server.session.cookie.http-only 否开启HttpOnly.

server.session.cookie.max-age 设定session cookie的最大age.

server.session.cookie.name 设定Session cookie 的名称.

server.session.cookie.path 设定session cookie的路径.

server.session.cookie.secure 设定session cookie的“Secure” flag.

案例

server.session.cookie.name=PHPSESSID

server.session.cookie.domain=.example.com

server.session.cookie.http-only=true

server.session.cookie.path=/

error 路径

server.error.path=/error

压缩传输

server.compression.enabled=true #是否开启压缩，默认为false.

server.compression.excluded-user-agents #指定不压缩的user-agent，多个以逗号分隔，默认值为:text/html,text/xml,text/plain,text/css

server.compression.mime-types #指定要压缩的MIME type，多个以逗号分隔.

server.compression.min-response-size #执行压缩的阈值，默认为2048

server.compression.enabled=true

server.compression.mime-types=application/json,application/xml,text/html,text/xml,text/plain,text/css,application/javascript

server.compression.min-response-size=1024

ssl

server.ssl.ciphers 是否支持SSL ciphers.

server.ssl.client-auth 设定client authentication是wanted 还是 needed.

server.ssl.enabled 是否开启ssl，默认: true

server.ssl.key-alias 设定key store中key的别名.

server.ssl.key-password 访问key store中key的密码.

server.ssl.key-store 设定持有SSL certificate的key store的路径，通常是一个.jks文件.

server.ssl.key-store-password设定访问key store的密码.

server.ssl.key-store-provider设定key store的提供者.

server.ssl.key-store-type 设定key store的类型.

server.ssl.protocol 使用的SSL协议，默认: TLS

server.ssl.trust-store 持有SSL certificates的Trust

store.

server.ssl.trust-store-password访问trust store的密码.

server.ssl.trust-store-provider设定trust store的提供者.

server.ssl.trust-store-type 指定trust store的类型.

生成证书

server.ssl.* #ssl相关配置

keytool -genkey -alias springboot -keyalg RSA -keystore /www/ssl/spring

设置密码 123456

配置 application.properties

server.ssl.enabled 启动tomcat ssl配置

server.ssl.keyAlias 别名

server.ssl.keyPassword 密码

server.ssl.keyStore 位置

server.port=8443

server.ssl.enabled=true

server.ssl.keyAlias=springboot

server.ssl.keyPassword=123456

server.ssl.keyStore=/www/ssl/spring

logging

logging.file.path=/tmp # 日志目录默认为 /tmp

logging.file.name=your.log # 日志文件名称，默认为spring.log

java -jar spring-boot-app.jar --logging.file.name=/tmp/springboot.log

内嵌 tomcat server

连接数配置

server.tomcat.max-threads=2048 # 最大线程数

server.tomcat.basedir

设置 Tomcat 工作目录，默认 /tmp/tomcat-docbase.7057591687859485145.7000 通过下面配置修改

server.tomcat.basedir=/tmp/your_project

access.log

如果前端有 nginx 代理这个配置可以禁用

server.tomcat.accesslog.enabled=true

server.tomcat.accesslog.directory=/tmp/logs

server.tomcat.accesslog.pattern=common

server.tomcat.accesslog.prefix=www.netkiller.cn.access

server.tomcat.accesslog.suffix=.log

charset

Spring boot 默认并非UTF-8 所以下面配置必设，否则将会出现

spring.messages.encoding=UTF-8

server.tomcat.uri-encoding=UTF-8

spring.http.encoding.charset=UTF-8

spring.http.encoding.enabled=true

spring.http.encoding.force=true

servlet

上传限制

spring.servlet.multipart.max-file-size=2MB

spring.servlet.multipart.max-request-size=10MB

spring.http.multipart.enabled=false

server.servlet.context-path

server.servlet.context-path=/your

JSON 输出与日期格式化

# Pretty-print JSON responses

spring.jackson.serialization.indent_output=true

#序列化时间格式

spring.jackson.date-format=yyyy-MM-dd HH:mm:ss

spring.mvc.date-format=yyyy-MM-dd HH:mm:ss

#mvc序列化时候时区选择

spring.jackson.time-zone=GMT+8

SMTP 相关配置

spring.mail.host=smtp.netkiller.cn

#spring.mail.username=

#spring.mail.password=

#spring.mail.properties.mail.smtp.auth=true

#spring.mail.properties.mail.smtp.starttls.enable=true

#spring.mail.properties.mail.smtp.starttls.required=true

mail.smtp.debug=true

Redis

Springboot 1.5

# REDIS (RedisProperties)

# Redis数据库索引（默认为0）

spring.redis.database=0

# Redis服务器地址

spring.redis.host=localhost

# Redis服务器连接端口

spring.redis.port=6379

# Redis服务器连接密码（默认为空）

spring.redis.password=

# 连接池最大连接数（使用负值表示没有限制）

spring.redis.pool.max-active=8

# 连接池最大阻塞等待时间（使用负值表示没有限制）

spring.redis.pool.max-wait=-1

# 连接池中的最大空闲连接

spring.redis.pool.max-idle=8

# 连接池中的最小空闲连接

spring.redis.pool.min-idle=0

# 连接超时时间（毫秒）

spring.redis.timeout=0

Springboot 2.0

# REDIS (RedisProperties)

# Redis数据库索引（默认为0）

spring.redis.database=0

# Redis服务器地址

spring.redis.host=192.168.30.103

# Redis服务器连接端口

spring.redis.port=6379

# Redis服务器连接密码（默认为空）

spring.redis.password=

# 连接超时时间（毫秒）

spring.redis.timeout=0

# 连接池最大连接数（使用负值表示没有限制）

spring.redis.jedis.pool.max-active=8

# 连接池最大阻塞等待时间（使用负值表示没有限制）

spring.redis.jedis.pool.max-wait=-1

# 连接池中的最大空闲连接

spring.redis.jedis.pool.max-idle=8

# 连接池中的最小空闲连接

spring.redis.jedis.pool.min-idle=0

MongoDB

格式：mongodb://用户名:密码@主机地址/数据库

spring.data.mongodb.uri=mongodb://user:passw0rd@mdb.netkiller.cn/

spring.data.mongodb.repositories.enabled=true

MySQL

spring.datasource.driver-class-name=com.mysql.jdbc.Driver

spring.datasource.url=jdbc:mysql://主机地址:端口号/数据库

spring.datasource.username=用户名

spring.datasource.password=密码

spring.jpa.database=MYSQL # 启用JPA支持

Oracle

spring.datasource.driver-class-name=oracle.jdbc.OracleDriver

spring.datasource.url=jdbc:oracle:thin:@//odb.netkiller.cn:1521/orcl

spring.datasource.username=orcl

spring.datasource.password=passw0rd

spring.datasource.connection--query="SELECT 1 FROM DUAL"

spring.jpa.database-platform=org.hibernate.dialect.Oracle10gDialect

spring.jpa.show-sql=true

#spring.jpa.hibernate.ddl-auto=none

#spring.jpa.hibernate.ddl-auto=create-drop

spring.datasource.max-active=50

spring.datasource.initial-size=5

spring.datasource.max-idle=10

spring.datasource.min-idle=5

spring.datasource.-while-idle=true

spring.datasource.-on-borrow=false

spring.datasource.validation-query=SELECT 1 FROM DUAL

spring.datasource.time-between-eviction-runs-millis=5000

spring.datasource.min-evictable-idle-time-millis=60000

default_schema

spring.jpa.properties.hibernate.default_schema=schema

datasource

启用/禁用 导入 schema.sql 和 data.sql / data-${platform}.sql 其中 platform 是 spring.datasource.platform 所定义的平台

spring.datasource.initialize=false

spring.datasource.platform=MYSQL

velocity

spring.velocity.resourceLoaderPath=classpath:/templates/

spring.velocity.prefix=

spring.velocity.suffix=.vm

spring.velocity.cache=false

spring.velocity.check-template-location=true

spring.velocity.content-type=text/html

spring.velocity.charset=UTF-8

spring.velocity.properties.input.encoding=UTF-8

spring.velocity.properties.output.encoding=UTF-8

禁用 velocity 模板引擎

spring.velocity.enabled=false

spring.velocity.check-template-location=false

Security 相关配置

security.user.name=user

security.user.password=password

security.user.role=USER

Web 安全

# X-Frame-Options: DENY

security.headers.frame=false

security.headers.cache

security.headers.content-type

security.headers.hsts

security.headers.xss

参考 https://github.com/spring-projects/spring-boot/blob/master/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/SecurityProperties.java#L171

MVC 配置

是否支持favicon.ico，默认为: true

spring.mvc.favicon.enabled=false

Kafka 相关配置

# APACHE KAFKA (KafkaProperties)

spring.kafka.admin.client-id= # ID to pass to the server when making requests. Used for server-side logging.

spring.kafka.admin.fail-fast=false # Whether to fail fast if the broker is not available on startup.

spring.kafka.admin.properties.*= # Additional admin-specific properties used to configure the client.

spring.kafka.admin.ssl.key-password= # Password of the private key in the key store file.

spring.kafka.admin.ssl.keystore-location= # Location of the key store file.

spring.kafka.admin.ssl.keystore-password= # Store password for the key store file.

spring.kafka.admin.ssl.keystore-type= # Type of the key store.

spring.kafka.admin.ssl.protocol= # SSL protocol to use.

spring.kafka.admin.ssl.truststore-location= # Location of the trust store file.

spring.kafka.admin.ssl.truststore-password= # Store password for the trust store file.

spring.kafka.admin.ssl.truststore-type= # Type of the trust store.

spring.kafka.bootstrap-servers= # Comma-delimited list of host:port pairs to use for establishing the initial connection to the Kafka cluster.

spring.kafka.client-id= # ID to pass to the server when making requests. Used for server-side logging.

spring.kafka.consumer.auto-commit-interval= # Frequency with which the consumer offsets are auto-committed to Kafka if 'enable.auto.commit' is set to true.

spring.kafka.consumer.auto-offset-reset= # What to do when there is no initial offset in Kafka or if the current offset no longer exists on the server.

spring.kafka.consumer.bootstrap-servers= # Comma-delimited list of host:port pairs to use for establishing the initial connection to the Kafka cluster.

spring.kafka.consumer.client-id= # ID to pass to the server when making requests. Used for server-side logging.

spring.kafka.consumer.enable-auto-commit= # Whether the consumer's offset is periodically committed in the background.

spring.kafka.consumer.fetch-max-wait= # Maximum amount of time the server blocks before answering the fetch request if there isn't sufficient data to immediately satisfy the requirement given by "fetch.min.bytes".

spring.kafka.consumer.fetch-min-size= # Minimum amount of data, in bytes, the server should return for a fetch request.

spring.kafka.consumer.group-id= # Unique string that identifies the consumer group to which this consumer belongs.

spring.kafka.consumer.heartbeat-interval= # Expected time between heartbeats to the consumer coordinator.

spring.kafka.consumer.key-deserializer= # Deserializer class for keys.

spring.kafka.consumer.max-poll-records= # Maximum number of records returned in a single call to poll().

spring.kafka.consumer.properties.*= # Additional consumer-specific properties used to configure the client.

spring.kafka.consumer.ssl.key-password= # Password of the private key in the key store file.

spring.kafka.consumer.ssl.keystore-location= # Location of the key store file.

spring.kafka.consumer.ssl.keystore-password= # Store password for the key store file.

spring.kafka.consumer.ssl.keystore-type= # Type of the key store.

spring.kafka.consumer.ssl.protocol= # SSL protocol to use.

spring.kafka.consumer.ssl.truststore-location= # Location of the trust store file.

spring.kafka.consumer.ssl.truststore-password= # Store password for the trust store file.

spring.kafka.consumer.ssl.truststore-type= # Type of the trust store.

spring.kafka.consumer.value-deserializer= # Deserializer class for values.

spring.kafka.jaas.control-flag=required # Control flag for login configuration.

spring.kafka.jaas.enabled=false # Whether to enable JAAS configuration.

spring.kafka.jaas.login-module=com.sun.security.auth.module.Krb5LoginModule # Login module.

spring.kafka.jaas.options= # Additional JAAS options.

spring.kafka.listener.ack-count= # Number of records between offset commits when ackMode is "COUNT" or "COUNT_TIME".

spring.kafka.listener.ack-mode= # Listener AckMode. See the spring-kafka documentation.

spring.kafka.listener.ack-time= # Time between offset commits when ackMode is "TIME" or "COUNT_TIME".

spring.kafka.listener.client-id= # Prefix for the listener's consumer client.id property.

spring.kafka.listener.concurrency= # Number of threads to run in the listener containers.

spring.kafka.listener.idle-event-interval= # Time between publishing idle consumer events (no data received).

spring.kafka.listener.log-container-config= # Whether to log the container configuration during initialization (INFO level).

spring.kafka.listener.monitor-interval= # Time between checks for non-responsive consumers. If a duration suffix is not specified, seconds will be used.

spring.kafka.listener.no-poll-threshold= # Multiplier applied to "pollTimeout" to determine if a consumer is non-responsive.

spring.kafka.listener.poll-timeout= # Timeout to use when polling the consumer.

spring.kafka.listener.type=single # Listener type.

spring.kafka.producer.acks= # Number of acknowledgments the producer requires the leader to have received before considering a request complete.

spring.kafka.producer.batch-size= # Default batch size in bytes.

spring.kafka.producer.bootstrap-servers= # Comma-delimited list of host:port pairs to use for establishing the initial connection to the Kafka cluster.

spring.kafka.producer.buffer-memory= # Total bytes of memory the producer can use to buffer records waiting to be sent to the server.

spring.kafka.producer.client-id= # ID to pass to the server when making requests. Used for server-side logging.

spring.kafka.producer.compression-type= # Compression type for all data generated by the producer.

spring.kafka.producer.key-serializer= # Serializer class for keys.

spring.kafka.producer.properties.*= # Additional producer-specific properties used to configure the client.

spring.kafka.producer.retries= # When greater than zero, enables retrying of failed sends.

spring.kafka.producer.ssl.key-password= # Password of the private key in the key store file.

spring.kafka.producer.ssl.keystore-location= # Location of the key store file.

spring.kafka.producer.ssl.keystore-password= # Store password for the key store file.

spring.kafka.producer.ssl.keystore-type= # Type of the key store.

spring.kafka.producer.ssl.protocol= # SSL protocol to use.

spring.kafka.producer.ssl.truststore-location= # Location of the trust store file.

spring.kafka.producer.ssl.truststore-password= # Store password for the trust store file.

spring.kafka.producer.ssl.truststore-type= # Type of the trust store.

spring.kafka.producer.transaction-id-prefix= # When non empty, enables transaction support for producer.

spring.kafka.producer.value-serializer= # Serializer class for values.

spring.kafka.properties.*= # Additional properties, common to producers and consumers, used to configure the client.

spring.kafka.ssl.key-password= # Password of the private key in the key store file.

spring.kafka.ssl.keystore-location= # Location of the key store file.

spring.kafka.ssl.keystore-password= # Store password for the key store file.

spring.kafka.ssl.keystore-type= # Type of the key store.

spring.kafka.ssl.protocol= # SSL protocol to use.

spring.kafka.ssl.truststore-location= # Location of the trust store file.

spring.kafka.ssl.truststore-password= # Store password for the trust store file.

spring.kafka.ssl.truststore-type= # Type of the trust store.

spring.kafka.template.default-topic= # Default topic to which messages are sent.

2. Properties 文件

禁用命令行注入环境变量

SpringApplication.setAddCommandLineProperties(false)

@Value 注解

application.properties

server.name=Linux

server.host=192.168.0.1,172.16.0.1

@Value("${server.name}")

private String name;

@EnableConfigurationProperties 引用自定义 *.properties 配置文件

Application.java 配置NetkillerProperties.java是 @ComponentScan 扫描范围，可以不用声明下面注解。

@EnableConfigurationProperties(NetkillerProperties.class)

import org.springframework.boot.SpringApplication;

import org.springframework.boot.autoconfigure.EnableAutoConfiguration;

import org.springframework.boot.autoconfigure.SpringBootApplication;

import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;

import org.springframework.boot.context.properties.EnableConfigurationProperties;

import org.springframework.context.annotation.Bean;

import org.springframework.context.annotation.ComponentScan;

import org.springframework.context.annotation.Configuration;

import org.springframework.data.authentication.UserCredentials;

import org.springframework.data.mongodb.MongoDbFactory;

import org.springframework.data.mongodb.core.MongoTemplate;

import org.springframework.data.mongodb.core.SimpleMongoDbFactory;

import org.springframework.data.mongodb.repository.config.EnableMongoRepositories;

import com.mongodb.Mongo;

import pojo.NetkillerProperties;

@Configuration

@SpringBootApplication

@EnableConfigurationProperties(NetkillerProperties.class)

@EnableAutoConfiguration(exclude = { DataSourceAutoConfiguration.class })

@ComponentScan({ "web", "rest" })

@EnableMongoRepositories

public class Application {

@SuppressWarnings("deprecation")

public @Bean MongoDbFactory mongoDbFactory() throws Exception {

UserCredentials userCredentials = new UserCredentials("finance", "your_password");

return new SimpleMongoDbFactory(new Mongo("mdb.netkiller.cn"), "finance", userCredentials);

}

public @Bean MongoTemplate mongoTemplate() throws Exception {

return new MongoTemplate(mongoDbFactory());

}

public static void main(String[] args) {

SpringApplication.run(Application.class, args);

}

}

NetkillerProperties.java

package pojo;

import org.springframework.boot.context.properties.ConfigurationProperties;

import org.springframework.context.annotation.Configuration;

@ConfigurationProperties(prefix="netkiller")

public class NetkillerProperties {

private String name;

private String email;

private String home;

public String getName() {

return name;

}

public void setName(String name) {

this.name = name;

}

public String getEmail() {

return email;

}

public void setEmail(String email) {

this.email = email;

}

public String getHome() {

return home;

}

public void setHome(String home) {

this.home = home;

}

@Override

public String toString() {

return "NetkillerProperties [name=" + name + ", email=" + email + ", home=" + home + "]";

}

}

IndexController.java

package web;

import org.springframework.beans.factory.annotation.Autowired;

import org.springframework.stereotype.Controller;

import org.springframework.web.bind.annotation.RequestMapping;

import org.springframework.web.bind.annotation.ResponseBody;

import domain.City;

import pojo.NetkillerProperties;

import repository.CityRepository;

@Controller

public class IndexController {

@Autowired

private CityRepository repository;

@Autowired

private NetkillerProperties propertie;

@RequestMapping("/index")

@ResponseBody

public String index() {

//public ModelAndView index() {

String message = "Hello";

//return new ModelAndView("home/welcome", "variable", message);

return message;

}

@RequestMapping("/config")

@ResponseBody

public String config() {

return propertie.toString();

}

}

src/main/resource/application.properties

netkiller.name=Neo

netkiller.email=netkiller@msn.com

netkiller.home=http://www.netkiller.cn

@ConfigurationProperties 默认配置是 application.properties

你可以通过 locations 指向特定配置文件

@ConfigurationProperties(prefix = "message.api",locations = "classpath:config/message.properties")

@EnableConfigurationProperties 可以导入多个配置文件

@EnableConfigurationProperties({NetkillerProperties.class, NeoProperties.class})

手工载入 *.properties 文件

@RequestMapping("/config")

@ResponseBody

public void config() {

try {

Properties properties = PropertiesLoaderUtils.loadProperties(new ClassPathResource("/config.properties"));

for(String key : properties.stringPropertyNames()) {

String value = properties.getProperty(key);

System.out.println(key + " => " + value);

}

} catch (IOException e) {

// TODO Auto-generated catch block

e.printStackTrace();

}

}

spring.profiles.active 参数切换配置文件

首先我们准备三个配置文件

src/main/resource/application-development.properties

src/main/resource/application-testing.properties

src/main/resource/application-production.properties

使用下面--spring.profiles.active参数切换运行环境配置文件

java -jar application.jar --spring.profiles.active=development

java -jar application.jar --spring.profiles.active=testing

java -jar application.jar --spring.profiles.active=production

分别为三个环境打包

mvn clean package -Pdevelopment

mvn clean package -Ptesting

mvn clean package -Pproduction

SpringApplicationBuilder.properties() 方法添加配置项

public static void main(String[] args) {

new SpringApplicationBuilder(Application.class).properties("spring.config.name=client").run(args);

}

3. 参数引用

book.name=SpringCloud

book.author=netkiller

book.title=《${book.name}》作者 ${book.author}

4. 默认值

默认值 ${变量:默认值}

@Value("${server.name:Windows}") 如果application.properties没有配置server.name那么默认值将是 Windows

private String name;

@Value("${some.key:my default value}")

private String stringWithDefaultValue;

@Value("${some.key:true}")

private boolean booleanWithDefaultValue;

@Value("${some.key:42}")

private int intWithDefaultValue;

@Value("${some.key:one,two,three}")

private String[] stringArrayWithDefaults;

@Value("${some.key:1,2,3}")

private int[] intArrayWithDefaults;

// Using SpEL

@Value("#{systemProperties['some.key'] ?: 'my default system property value'}")

private String spelWithDefaultValue;

Null 默认值

@Value("${app.name:@null}") // app.name = null

private String name;

5. 产生随机数

my.secret=${random.value}

my.number=${random.int}

my.bignumber=${random.long}

my.uuid=${random.uuid}

my.number.less.than.ten=${random.int(10)}

my.number.in.range=${random.int[1024,65536]}

# 随机字符串

cn.netkiller.blog.value=${random.value}

# 随机整数

cn.netkiller.blog.number=${random.int}

# 随机长整数

cn.netkiller.blog.bignumber=${random.long}

# 随机10以内的数

cn.netkiller.blog.1=${random.int(10)}

# 随机10-20之间的数值

cn.netkiller.blog.2=${random.int[10,20]}

随机数

import lombok.extern.slf4j.Slf4j;

import org.junit.Test;

import org.junit.runner.RunWith;

import org.springframework.beans.factory.annotation.Autowired;

import org.springframework.boot.env.RandomValuePropertySource;

import org.springframework.boot.test.context.SpringBootTest;

import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;

import java.util.List;

@Slf4j

@RunWith(SpringJUnit4ClassRunner.class)

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.NONE)

public class RandomValuePropertySourceTest {

@Test

public void testRandomValuePropertySource() {

// 自定义的一个随机值属性源,起名叫做 myRandom

RandomValuePropertySource random = new RandomValuePropertySource("myRandom");

// 随机生成一个整数

log.info("random int:{}", random.getProperty("random.int"));

// 随机生成一个整数，指定上边界，不大于等于1

log.info("random int(1):{}", random.getProperty("random.int(1)"));

// 随机生成一个整数，指定上边界，不大于等于5

log.info("random int(5):{}", random.getProperty("random.int(5)"));

// 随机生成一个整数，使用区间[0,1),前闭后开=>只能是1

// 注意区间的表示法：使用()包围,2个字符

log.info("random int(0,1):{}", random.getProperty("random.int(0,1)"));

// 随机生成一个整数，使用区间[1,3),前闭后开=>只能是1或者2

// 注意区间的表示法：使用空格包围,2个字符，前后各一个空格

log.info("random int(1,3):{}", random.getProperty("random.int 1,3 "));

// 随机生成一个整数，使用区间[3,4),前闭后开=>只能是3

// 注意区间的表示法：使用汉字包围,2个字符，前后各一个汉字自负

log.info("random int(3,4):{}", random.getProperty("random.int底3,4顶"));

// 随机生成一个整数，使用区间[5,6),前闭后开=>只能是5

// 注意区间的表示法：使用英文字母包围,2个字符，前后各一个英文字母

log.info("random int(5,6):{}", random.getProperty("random.intL5,6U"));

// 随机生成一个整数，使用区间[5,6),前闭后开=>只能是5

// 注意区间的表示法：使用数字包围,2个字符，前一个数字5，后一个数字6

log.info("random int(5,6):{}", random.getProperty("random.int55,66"));

// 随机生成一个长整数

log.info("random long:{}", random.getProperty("random.long"));

// 随机生成一个整数，使用区间[100,101),前闭后开=>只能是100

log.info("random long(100,101):{}", random.getProperty("random.long(100,101)"));

// 随机生成一个 uuid

log.info("random uuid:{}", random.getProperty("random.uuid"));

}

}

6. 多行字符串

text.content=Bright moonlight in front of bed\

ground frost\

up at the bright moon\

down at home

7. 注入多值属性 arrays, list, set

处理逗号分割得值

@Value("#{'${server.host}'.split(',')}")

private List<String> host;

my.numbers=1,2,3,4,5,6,1,2

自定义列表属性分隔符

my.lists=aa;bb;cc;dd

public test(@Value("#{'${my.lists}'.split(';')}") List<String> list) {

this.list = list;

log.debug("list", list);

}

8. containsProperty 读取配置文件

this.environment.containsProperty("spring.jpa.database-platform")

9. @PropertySource 注解载入 properties 文件

@PropertySource("classpath:/config.properties}")

忽略FileNotFoundException，当配置文件不存在系统抛出FileNotFoundException并终止程序运行，ignoreResourceNotFound=true 会跳过使程序能够正常运行

@PropertySource(value="classpath:config.properties", ignoreResourceNotFound=true)

载入多个配置文件

@PropertySources({

@PropertySource("classpath:config.properties"),

@PropertySource("classpath:db.properties")

})

test.properties

name=Neo

age=30

package cn.netkiller.web;

import java.util.Date;

import javax.servlet.http.HttpSession;

import org.springframework.beans.factory.annotation.Autowired;

import org.springframework.beans.factory.annotation.Value;

import org.springframework.context.annotation.PropertySource;

import org.springframework.core.env.Environment;

import org.springframework.stereotype.Controller;

import org.springframework.web.bind.annotation.RequestMapping;

import org.springframework.web.bind.annotation.ResponseBody;

@Controller

@PropertySource("classpath:test.properties")

public class TestController {

@Autowired

Environment environment;

@Value("${age}")

private String age;

public TestController() {

// TODO Auto-generated constructor stub

}

// 环境变量方式

@RequestMapping("/test/env")

@ResponseBody

public String env() {

String message = environment.getProperty("name");

return message;

}

@RequestMapping("/test/age")

@ResponseBody

public String age() {

String message = age;

return message;

}

}

10. List 列表类型

List类型在properties文件中使用[]来定义列表类型，比如：

sms.url[0]=http://api1.example.com

sms.url[1]=http://api2.example.com

netkiller.book[0].title=Netkiller Linux 手札

netkiller.book[0].author=netkiller

netkiller.book[1].title=Netkiller Spring 手札

netkiller.book[1].author=netkiller

注意：在Spring Boot 2.0中对于List类型数组下标的配置必须是连续的，否则会抛出UnboundConfigurationPropertiesException异常，所以如下配置是不允许的：

foo[0]=a

foo[2]=b

使用逗号分割的配置方式，上面与下面的配置是等价的：

sms.url[0]=http://api1.example.com,http://api2.example.com

在yaml文件中使用可以使用如下配置：

email:

to:

address:

- neo@netkiller.cn

- jam@netkiller.cn

逗号分割的方式：

email:

to:

address: neo@netkiller.cn, jam@netkiller.cn

命令行传递 List 数据

java -jar -D"api.url[0]=http://api1.example.com" \

-D"api.url[1]=http://api2.example.com" \

api.netkiller.cn-v1.0.jar

逗号分割的方式，比如：

java -jar -Dapi.url=http://api1.example.com,http://api2.example.com demo.jar

11. Map类型

Map类型在properties和yaml中的标准配置方式如下：

properties格式：

netkiller.key=value

yaml格式：

netkiller:

key: value

举例：

user:

name: neo

gender: male

age: 30

注意：如果Map类型的key包含字母数字和-以外的字符，需要用[]括起来，比如：

user:

name:

'[first.name]': neo

'[last#name]': chen

12. Binder

cn.netkiller.author=bar

cn.netkiller.journal[0]=Spring Boot

cn.netkiller.journal[1]=Spring Cloud

cn.netkiller.books[0].title=Netkiller Spring Boot 手札

cn.netkiller.books[0].url=http://www.netkiller.cn/spring/

cn.netkiller.books[1].title=Netkiller Java 手札

cn.netkiller.books[1].url=http://www.netkiller.cn/linux/

@Data

@ConfigurationProperties(prefix = "cn.netkiller")

public class NetkillerProperties {

public String author;

}

@SpringBootApplication

public class Application {

public static void main(String[] args) {

ApplicationContext context = SpringApplication.run(Application.class, args);

Binder binder = Binder.get(context.getEnvironment());

NetkillerProperties prop = binder.bind("cn.netkiller", Bindable.of(NetkillerProperties.class)).get();

System.out.println(prop.author);

List<String> journal = binder.bind("cn.netkiller.journal", Bindable.listOf(String.class)).get();

System.out.println(journal);

List<Book> books = binder.bind("cn.netkiller.book", Bindable.listOf(Book.class)).get();

System.out.println(books);

}

}

13. 加密 application.properties 中的敏感内容

http://www.jasypt.org

Maven 配置

<!-- https://mvnrepository.com/artifact/com.github.ulisesbocchio/jasypt-spring-boot-starter -->

<dependency>

<groupId>com.github.ulisesbocchio</groupId>

<artifactId>jasypt-spring-boot-starter</artifactId>

<version>3.0.4</version>

</dependency>

生成加密信息

package cn.netkiller.controller;

import org.jasypt.encryption.StringEncryptor;

import org.springframework.beans.factory.annotation.Autowired;

import org.springframework.beans.factory.annotation.Value;

import org.springframework.web.bind.annotation.GetMapping;

import org.springframework.web.bind.annotation.RequestParam;

import org.springframework.web.bind.annotation.RestController;

@RestController

public class PasswordController {

@Autowired

private StringEncryptor encryptor;

@Value("${test.password}")

private String cleartext;

public PasswordController() {

// TODO Auto-generated constructor stub

}

@GetMapping("/password")

public String password(@RequestParam("text") String text) {

return encryptor.encrypt(text);

}

@GetMapping("/cleartext")

public String getPassword() {

return this.cleartext;

}

}

启动 Springboot 应用

java -jar your_springboot_application.jar --jasypt.encryptor.password=123456

将文本 neo 加密

neo@MacBook-Pro-Neo ~ % curl http://localhost:8080/password\?text\=neo

YrEdNoIyJlRoO+QhHGGwhxorlrc1e0B6Sk2iWwWMeUFd5AeCh3uAuxFr0FhEi3di

修改 application.properties 配置文件

test.password=ENC(YrEdNoIyJlRoO+QhHGGwhxorlrc1e0B6Sk2iWwWMeUFd5AeCh3uAuxFr0FhEi3di)

重启 Springboot 项目，检验加密是否生效

neo@MacBook-Pro-Neo ~ % curl http://localhost:8080/cleartext

neo

测试环境可以将 jasypt.encryptor.password 放入配置文件，无需每次启动加入该参数。

jasypt.encryptor.password=123456

test.password=ENC(cH0s45ZDOHtbCNgVGgs0etnigdfZgvrnhdFokG9ysnvy4DK0jZFPGOqe7Myow64y)

BasicTextEncryptor 加密文本内容

package cn.netkiller;

import org.jasypt.util.text.BasicTextEncryptor;

public class Password {

public Password() {

// TODO Auto-generated constructor stub

}

public static void main(String[] args) {

// TODO Auto-generated method stub

BasicTextEncryptor textEncryptor = new BasicTextEncryptor();

textEncryptor.setPassword("123456");

String username = textEncryptor.encrypt("root");

String password = textEncryptor.encrypt("123456");

System.out.println("username:" + username);

System.out.println("password:" + password);

}

}

第 6 章 Spring boot with Logging

通过命令行改变日志的输出级别

java -jar app.jar --debug

在application.properties中配置

debug=true

application.yml

debug=true

相同的方式使能TRACE级别的日志

java -jar app.jar --trace

application.properties

trace=true

application.yml

trace=true

1. 配置日志文件

一般的日志需求可以通过配置 application.properties实现。

Spring Boot中 日志默认是输出到控制台的，这样是为了方便开发人员，但是在生产环境中应该输出到日志文件中。

配置如下

- logging.file.path: 指定日志文件的路径

- logging.file.name: 日志的文件名(默认为spring.log)

- logging.pattern.console: 控制台的输出格式

- logging.pattern.file: 日志文件的输出格式

- logging.pattern.level: 定义渲染不同级别日志的格式。默认是%5p.

提示

注意：这两个属性不能同时配置，只需要配置一个即可。

提示

旧版本

logging.file，设置文件，可以是绝对路径，也可以是相对路径。如：logging.file=my.log

logging.path，设置目录，如果 logging.file 没有设置，会在该目录下创建spring.log文件作为默认日志文件。

logging.file=target/spring.log

#logging.path=

如果仍不能满足可以使用 logback.xml 配置日志。

logging.path=/tmp

logging.config=classpath:logback.xml

日志输出级别

几种常见的日志级别由低到高分为：TRACE < DEBUG < INFO < WARN < ERROR < FATAL

显示所有DEBUG信息

logging.level.root=DEBUG

仅仅显示 springframework 调试信息

logging.level.org.springframework.web=DEBUG

仅仅显示 cn.netkiller.web.TestController 调试信息

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

log.debug(message);

logging.level.cn.netkiller.web.TestController=DEBUG

YAML 配置文件写法

logging:

level:

root: info

cn.netkiller.test: debug

cn.netkiller.sharding.MonthShardingAlgorithm: DEBUG

Spring boot 2.1 以后的版本不打印 Mapped 日志问题

logging.level.org.springframework.web=trace

禁止控制台输出日志

禁止控制台日志输出，同时将日志写入日志文件.

src/main/resources/application.properties

logging.file.path=/tmp

logging.file.name=/tmp/spring.log

logging.level.root=INFO

logging.level.org.springframework.web=DEBUG

logging.level.org.hibernate=ERROR

src/main/resources/logback.xml

$ cat src/main/resources/logback.xml

<?xml version="1.0" encoding="UTF-8"?>

<configuration>

<include resource="org/springframework/boot/logging/logback/defaults.xml" />

<include resource="org/springframework/boot/logging/logback/file-appender.xml" />

<root level="INFO">

<appender-ref ref="FILE" />

</root>

</configuration>

使用 java -jar project-version-xxx.jar 启动后控制不会再输出日志

定制日志格式

定制日志格式有两个配置

- logging.pattern.console：控制台的输出格式

- logging.pattern.file：日志文件的输出格式

分别是控制台的输出格式和文件中的日志输出格式

举例

logging.pattern.console=%d{yyyy/MM/dd-HH:mm:ss} [%thread] %-5level %logger- %msg%n

logging.pattern.file=%d{yyyy/MM/dd-HH:mm} [%thread] %-5level %logger- %msg%n

格式说明

%d{HH:mm:ss.SSS} 日志输出时间

%thread 输出日志的进程名字，这在Web应用以及异步任务处理中很有用

%-5level 日志级别，并且使用5个字符靠左对齐

%logger 日志输出者的名字

%msg 日志消息

%n 平台的换行符

彩色输出

spring.main.banner-mode=off

spring.output.ansi.enabled=ALWAYS

logging.pattern.console=%clr(%d{yy-MM-dd E HH:mm:ss.SSS}){blue} %clr(%-5p) %clr(${PID}){faint} %clr(---){faint} %clr([%8.15t]){cyan} %clr(%-40.40logger{0}){blue} %clr(:){red} %clr(%m){faint}%n

2. 打印日志

日志的用法，首先开发中我们根据实际的需要打印不同级别的日志。

package cn.netkiller.web;

import org.slf4j.Logger;

import org.slf4j.LoggerFactory;

import org.springframework.stereotype.Controller;

import org.springframework.web.bind.annotation.RequestMapping;

import org.springframework.web.bind.annotation.ResponseBody;

@Controller

public class TestController {

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

@RequestMapping("/test/log")

@ResponseBody

public String log() {

String message = "Test";

log.debug(message);

log.info(message);

log.warn(message);

log.error(message);

log.trace(message);

return message;

}

}

然后通过application.properties配置那些需要显示，那些不需要，以及显示的级别是什么。

lombok

<dependency>

<groupId>org.projectlombok</groupId>

<artifactId>lombok</artifactId>

</dependency>

@Slf4j

class DemoApplicationTests {

@Test

public void test(){

log.debug("输出DEBUG日志.......");

}

}

3. logback 配置详解

配置文件名默认是：logback-spring.xml，使用其他文件名通过下面配置项指定即可。

logging.config=classpath:logback.xml

标准输出

基本配置

<?xml version="1.0" encoding="UTF-8"?>

<configuration>

<appender name="stdout" class="ch.qos.logback.core.ConsoleAppender">

<encoder>

<pattern>%d{yy-MMMM-dd HH:mm:ss:SSS} %5p %t %c{2}:%L - %m%n</pattern>

</encoder>

</appender>

<root level="INFO">

<appender-ref ref="stdout"/>

</root>

</configuration>

禁止 logback 日志输出

<statusListener class="ch.qos.logback.core.status.NopStatusListener" />

指定Class过滤日志

<logger name="cn.netkiller.controller"/>

<logger name="cn.netkiller.controller.HomeController" level="WARN" additivity="false">

<appender-ref ref="console"/>

</logger>

configuration 属性配置

scan:当此属性设置为true时，配置文件如果发生改变，将会被重新加载，默认值为true。

scanPeriod:设置监测配置文件是否有修改的时间间隔，如果没有给出时间单位，默认单位是毫秒。当scan为true时，此属性生效。默认的时间间隔为1分钟。

debug:当此属性设置为true时，将打印出logback内部日志信息，实时查看logback运行状态。默认值为false。

contextName 设置上下文名称

每个logger都关联到logger上下文，默认上下文名称为“default”。但可以使用设置成其他名字，用于区分不同应用程序的记录。设置后可以通过%contextName来打印日志上下文名称。

<contextName>logback</contextName>

property 设置变量

用来定义变量值的标签， 有两个属性，name和value；其中name的值是变量的名称，value的值时变量定义的值。通过定义的值会被插入到logger上下文中。定义变量后，可以使“${}”来使用变量。

<property name="log.path" value="/tmp" />

encoder 日志格式设置

<encoder>表示对日志进行编码：

%d{HH: mm:ss.SSS}——日志输出时间

%thread——输出日志的进程名字，这在Web应用以及异步任务处理中很有用

%-5level——日志级别，并且使用5个字符靠左对齐

%logger{36}——日志输出者的名字

%msg——日志消息

%n——平台的换行符

RollingFileAppender

上例中<fileNamePattern>${log.path}/logback.%d{yyyy-MM-dd}.log</fileNamePattern>定义了日志的切分方式——把每一天的日志归档到一个文件中，同理，可以使用%d{yyyy-MM-dd_HH-mm}来定义精确到分的日志切分方式。

<maxHistory>30</maxHistory>表示只保留最近30天的日志，以防止日志填满整个磁盘空间。

<totalSizeCap>1GB</totalSizeCap>用来指定日志文件的上限大小，例如设置为1GB的话，那么到了这个值，就会删除旧的日志。

按日期分割日志

<?xml version="1.0" encoding="UTF-8"?>

<configuration  scan="true" scanPeriod="60 seconds" debug="false">

<contextName>logback</contextName>

<property name="log.path" value="target" />

<!--输出到控制台-->

<appender name="console" class="ch.qos.logback.core.ConsoleAppender">

<encoder>

<pattern>%d{HH:mm:ss.SSS} %contextName [%thread] %-5level %logger{36} - %msg%n</pattern>

</encoder>

</appender>

<!--输出到文件-->

<appender name="file" class="ch.qos.logback.core.rolling.RollingFileAppender">

<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">

<fileNamePattern>${log.path}/spring.%d{yyyy-MM-dd}.log</fileNamePattern>

</rollingPolicy>

<encoder>

<pattern>%d{HH:mm:ss.SSS} %contextName [%thread] %-5level %logger{36} - %msg%n</pattern>

</encoder>

</appender>

<root level="info">

<appender-ref ref="console" />

<appender-ref ref="file" />

</root>

</configuration>

按照文件尺寸分割日志

按日期分割文件

<?xml version="1.0" encoding="UTF-8"?>

<configuration>

<include resource="org/springframework/boot/logging/logback/defaults.xml" />

<include resource="org/springframework/boot/logging/logback/file-appender.xml" />

<appender name="dailyRollingFileAppender" class="ch.qos.logback.core.rolling.RollingFileAppender">

<File>logs/spring.log</File>

<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">

<!-- daily rollover -->

<FileNamePattern>spring.%d{yyyy-MM-dd}.log</FileNamePattern>

<!-- keep 30 days' worth of history -->

<maxHistory>60</maxHistory>

</rollingPolicy>

<encoder>

<Pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{35} - %msg %n</Pattern>

</encoder>

</appender>

<root level="INFO">

<appender-ref ref="FILE" />

<appender-ref ref="dailyRollingFileAppender" />

</root>

</configuration>

通过级别分割日志将 info, error, debug 分割到指定文件中。

<configuration scan="true" scanPeriod="10 seconds">

<!-- 控制台日志输出-->

<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">

<encoder>

<pattern>%d %p (%file:%line\)- %m%n</pattern>

<charset>UTF-8</charset>

</encoder>

</appender>

<!-- info日志输出-->

<appender name="INFO_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">

<encoder>

<pattern>%d %p (%file:%line\)- %m%n</pattern>

<charset>UTF-8</charset>

</encoder>

<filter class="ch.qos.logback.classic.filter.ThresholdFilter">

<level>INFO</level>

</filter>

<File>${LOG_PATH}/www.netkiller.cn.info.log</File>

<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">

<fileNamePattern>${LOG_PATH}/www.netkiller.cn.info-%d{yyyyMMdd}.log.%i

</fileNamePattern>

<timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">

<maxFileSize>10MB</maxFileSize>

</timeBasedFileNamingAndTriggeringPolicy>

<maxHistory>30</maxHistory>

</rollingPolicy>

<layout class="ch.qos.logback.classic.PatternLayout">

<Pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} -%msg%n

</Pattern>

</layout>

</appender>

<!-- debug 日志输出-->

<appender name="DEBUG_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">

<encoder>

<pattern>%d %p (%file:%line\)- %m%n</pattern>

<charset>UTF-8</charset>

</encoder>

<filter class="ch.qos.logback.classic.filter.ThresholdFilter">

<level>DEBUG</level>

</filter>

<File>${LOG_PATH}/www.netkiller.cn.debug.log</File>

<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">

<fileNamePattern>${LOG_PATH}/www.netkiller.cn.debug-%d{yyyyMMdd}.log.%i

</fileNamePattern>

<timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">

<maxFileSize>10MB</maxFileSize>

</timeBasedFileNamingAndTriggeringPolicy>

<maxHistory>30</maxHistory>

</rollingPolicy>

<layout class="ch.qos.logback.classic.PatternLayout">

<Pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} -%msg%n

</Pattern>

</layout>

</appender>

<!--error 日志输出配置 -->

<appender name="ERROR_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">

<encoder>

<pattern>%d %p (%file:%line\)- %m%n</pattern>

<charset>UTF-8</charset>

</encoder>

<filter class="ch.qos.logback.classic.filter.ThresholdFilter">

<level>ERROR</level>

</filter>

<File>${LOG_PATH}/www.netkiller.cn.error.log</File>

<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">

<fileNamePattern>${LOG_PATH}/www.netkiller.cn.error-%d{yyyyMMdd}.log.%i</fileNamePattern>

<timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">

<maxFileSize>10MB</maxFileSize>

</timeBasedFileNamingAndTriggeringPolicy>

<maxHistory>30</maxHistory>

</rollingPolicy>

<layout class="ch.qos.logback.classic.PatternLayout">

<Pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} -%msg%n</Pattern>

</layout>

</appender>

<root level="DEBUG">

<!--

<appender-ref ref="STDOUT" />

<appender-ref ref="INFO_FILE" />

<appender-ref ref="ERROR_FILE" />

<appender-ref ref="DEBUG_FILE" />

-->

<appender-ref ref="ERROR_FILE" />

<appender-ref ref="INFO_FILE" />

<appender-ref ref="DEBUG_FILE" />

</root>

</configuration>

日志过滤

将特定日志输出保存到指定位置

package cn.netkiller;

import org.slf4j.Logger;

import org.slf4j.LoggerFactory;

import org.slf4j.Marker;

import org.slf4j.MarkerFactory;

public class Application {

public static void main(String[] args) {

final Logger logger = LoggerFactory.getLogger(Application.class);

Marker notifyAdmin = MarkerFactory.getMarker("netkiller");

logger.info("AAAAAAAAA");

logger.info(notifyAdmin, "BBBBBBBBB");

logger.error(notifyAdmin, "This is a serious an error requiring the admin's attention", new Exception("Just testing"));

}

}

匹配到 marker 的日志才输出，通过 RollingFileAppender 可以保存到指定文件。

<?xml version="1.0" encoding="UTF-8"?>

<configuration>

<include resource="org/springframework/boot/logging/logback/defaults.xml" />

<include resource="org/springframework/boot/logging/logback/file-appender.xml" />

<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">

<filter class="ch.qos.logback.core.filter.EvaluatorFilter">

<evaluator class="ch.qos.logback.classic.boolex.OnMarkerEvaluator">

<marker>netkiller</marker>

</evaluator>

<onMatch>ACCEPT</onMatch>

<onMismatch>DENY</onMismatch>

</filter>

<encoder>

<pattern>%date{yyyy-MM-dd HH:mm:ss} %-4relative [%thread] %-5level %logger{35} : %msg %n</pattern>

</encoder>

</appender>

<root level="INFO">

<appender-ref ref="STDOUT" />

<appender-ref ref="FILE" />

</root>

</configuration>

标准输出

<?xml version="1.0" encoding="UTF-8"?>

<configuration>

<include resource="org/springframework/boot/logging/logback/defaults.xml" />

<include resource="org/springframework/boot/logging/logback/file-appender.xml" />

<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">

<encoder>

<pattern>%date{yyyy-MM-dd HH:mm:ss} %-4relative [%thread] %-5level %logger{35} : %msg %n</pattern>

</encoder>

</appender>

<root level="INFO">

<appender-ref ref="STDOUT" />

<appender-ref ref="FILE" />

</root>

</configuration>

MDC

每个 userId 生成一个日志文件

<?xml version="1.0" encoding="UTF-8"?>

<configuration>

<include resource="org/springframework/boot/logging/logback/defaults.xml" />

<include resource="org/springframework/boot/logging/logback/file-appender.xml" />

<property name="log.pattern" value="%d{yyyy-MM-dd HH:mm:ss} - [%25.25(%thread)] - [%-5level] - %-30.30(%logger{30}) : %msg%n" />

<appender name="siftingAppender" class="ch.qos.logback.classic.sift.SiftingAppender">

<discriminator>

<key>userId</key>

<defaultValue>unknown</defaultValue>

</discriminator>

<sift>

<appender name="${userId}" class="ch.qos.logback.core.rolling.RollingFileAppender">

<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">

<fileNamePattern>${log.path}/${userId}.%d{yyyy-MM-dd}.log</fileNamePattern>

</rollingPolicy>

<encoder>

<pattern>${log.pattern}</pattern>

</encoder>

</appender>

</sift>

</appender>

<root level="INFO">

<appender-ref ref="siftingAppender" />

</root>

</configuration>

package cn.netkiller.log;

import org.slf4j.Logger;

import org.slf4j.LoggerFactory;

import org.slf4j.MDC;

public class LogTest {

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

public LogTest() {

// TODO Auto-generated constructor stub

}

public static void main(String[] args) {

MDC.put("userId","0001");

logger.info("0001用户");

MDC.clear();

MDC.put("userId","0002");

logger.info("0002用户");

MDC.clear();

}

}

日志写入 MongoDB

日志发送给 logstash

https://github.com/logfellow/logstash-logback-encoder/

logstash 配置

配置 logstash 增加 tcp 接收输入端。

input {

tcp {

mode => "server"

host => "127.0.0.1"

port => 4567

codec => json_lines

}

}

Java 项目

Maven 配置文件 pom.xml 中添加

<dependency>

<groupId>net.logstash.logback</groupId>

<artifactId>logstash-logback-encoder</artifactId>

<version>7.2</version>

</dependency>

然后再resources添加logback.xml文件

<?xml version="1.0" encoding="UTF-8"?>

<configuration scan="true" scanPeriod="60 seconds" debug="true">

<include resource="org/springframework/boot/logging/logback/defaults.xml" />

<include resource="org/springframework/boot/logging/logback/console-appender.xml" />

<include resource="org/springframework/boot/logging/logback/file-appender.xml" />

<appender name="logstash" class="net.logstash.logback.appender.LogstashTcpSocketAppender">

<destination>127.0.0.1:4567</destination>

<encoder class="net.logstash.logback.encoder.LogstashEncoder">

<providers>

<timestamp />

<logLevel />

<threadName />

<loggerName />

<message />

</providers>

</encoder>

</appender>

<root level="info">

<appender-ref ref="CONSOLE" />

<appender-ref ref="logstash" />

</root>

</configuration>

通过 tags 区分日志文件

logstash pipeline 配置

[root@netkiller ~]# cat /etc/logstash/conf.d/file.conf

input {

tcp {

port => 4567

codec => json_lines

}

}

filter {

ruby {

code => "event.set('datetime', event.get('@timestamp').time.localtime.strftime('%Y-%m-%d %H:%M:%S'))"

}

}

output {

if "finance" in [tags] {

file {

path => "/opt/log/%{app}.finance.%{+yyyy}-%{+MM}-%{+dd}.log"

codec => line { format => "[%{datetime}] %{level} %{message} %{tags}"}

}

} else if "market" in [tags] {

file {

path => "/opt/log/%{app}.market.%{+yyyy}-%{+MM}-%{+dd}.log"

codec => line { format => "[%{datetime}] %{level} %{message} %{tags}"}

}

} else {

file {

path => "/opt/log/%{app}.unknow.%{+yyyy}-%{+MM}-%{+dd}.log"

codec => line { format => "[%{datetime}] %{level} %{message} %{tags}"}

}

}

}

logback-spring.xml 配置

<?xml version="1.0" encoding="UTF-8"?>

<configuration scan="false" scanPeriod="60 seconds" debug="false">

<include resource="org/springframework/boot/logging/logback/defaults.xml" />

<include resource="org/springframework/boot/logging/logback/console-appender.xml" />

<include resource="org/springframework/boot/logging/logback/file-appender.xml" />

<logger name="org.springframework.web" level="INFO" />

<logger name="org.springboot.sample" level="TRACE" />

<property name="log.pattern" value="%date{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{35}.%method: %msg%n" />

<springProperty scope="context" name="app" source="spring.application.name" defaultValue="spring-boot-fusion" />

<property name="log.path" value="/tmp" />

<appender name="siftingAppender" class="ch.qos.logback.classic.sift.SiftingAppender">

<discriminator>

<key>userId</key>

<defaultValue>unknown</defaultValue>

</discriminator>

<sift>

<appender name="${userId}" class="ch.qos.logback.core.rolling.RollingFileAppender">

<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">

<fileNamePattern>${log.path}/${userId}.%d{yyyy-MM-dd}.log</fileNamePattern>

</rollingPolicy>

<encoder>

<pattern>${log.pattern}</pattern>

</encoder>

</appender>

</sift>

</appender>

<springProfile name="prod">

<appender name="logstash" class="net.logstash.logback.appender.LogstashTcpSocketAppender">

<destination>172.18.200.10:4567</destination>

<keepAliveDuration>5 minutes</keepAliveDuration>

<reconnectionDelay>3 second</reconnectionDelay>

<writeBufferSize>8192</writeBufferSize>

<includeCallerData>true</includeCallerData>

<encoder class="net.logstash.logback.encoder.LogstashEncoder">

<shortenedLoggerNameLength>36</shortenedLoggerNameLength>

<timestampPattern>yyyy-MM-dd HH:mm:ss.Asia/Shanghai</timestampPattern>

<timeZone>Asia/Shanghai</timeZone>

<fieldNames>

<timestamp>@timestamp</timestamp>

<version>@version</version>

<message>message</message>

<logger>logger_name</logger>

<!-- <thread>thread_name</thread> -->

<level>level</level>

<thread>[ignore]</thread>

<levelValue>[ignore]</levelValue>

</fieldNames>

</encoder>

<filter class="ch.qos.logback.core.filter.EvaluatorFilter">

<evaluator class="ch.qos.logback.classic.boolex.OnMarkerEvaluator">

<marker>finance</marker>

<marker>market</marker>

<marker>customer</marker>

</evaluator>

<onMatch>ACCEPT</onMatch>

<onMismatch>DENY</onMismatch>

</filter>

</appender>

</springProfile>

<root level="info">

<springProfile name="dev">

<appender-ref ref="CONSOLE" />

</springProfile>

<springProfile name="test">

<appender-ref ref="CONSOLE" />

<appender-ref ref="FILE" />

</springProfile>

<springProfile name="prod">

<appender-ref ref="CONSOLE" />

<appender-ref ref="logstash" />

</springProfile>

</root>

</configuration>

打印日志

package cn.netkiller.controller;

import java.util.concurrent.TimeUnit;

import org.slf4j.Logger;

import org.slf4j.LoggerFactory;

import org.slf4j.MDC;

import org.slf4j.Marker;

import org.slf4j.MarkerFactory;

import org.springframework.beans.factory.annotation.Autowired;

import org.springframework.data.redis.core.RedisTemplate;

import org.springframework.jdbc.core.JdbcTemplate;

import org.springframework.web.bind.annotation.GetMapping;

import org.springframework.web.bind.annotation.RequestParam;

import org.springframework.web.bind.annotation.RestController;

import cn.netkiller.LogMarker;

import lombok.extern.slf4j.Slf4j;

@RestController

@Slf4j

public class HomeController {

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

public HomeController() {

// TODO Auto-generated constructor stub

}

@GetMapping("/")

public String index() {

Marker finance = MarkerFactory.getMarker(LogMarker.finance.toString());

Marker customer = MarkerFactory.getMarker(LogMarker.customer.toString());

Marker market = MarkerFactory.getMarker(LogMarker.market.toString());

logger.info("AAAAAAAAA");

logger.info(finance, "test");

logger.info(finance, "finance");

logger.info(customer, "customer");

logger.info(market, "market");

MDC.put("userId", "0001");

logger.info("0001用户");

MDC.clear();

MDC.put("userId", "0002");

logger.info("0002用户");

MDC.clear();

return "Hello world!!!";

}

}

fluentd

Maven 依赖

<dependency>

<groupId>org.fluentd</groupId>

<artifactId>fluent-logger</artifactId>

<version>0.3.4</version>

</dependency>

<dependency>

<groupId>com.sndyuk</groupId>

<artifactId>logback-more-appenders</artifactId>

<version>1.8.7</version>

</dependency>

安装 fluent-bit

dnf install -y fluent-bit

启动 fluent-bit

[root@netkiller ~]# fluent-bit -i forward -o stdout

Fluent Bit v1.9.7

* Copyright (C) 2015-2022 The Fluent Bit Authors

* Fluent Bit is a CNCF sub-project under the umbrella of Fluentd

* https://fluentbit.io

[2022/09/24 23:25:25] [ info] [fluent bit] version=1.9.7, commit=, pid=1191240

[2022/09/24 23:25:25] [ info] [storage] version=1.2.0, type=memory-only, sync=normal, checksum=disabled, max_chunks_up=128

[2022/09/24 23:25:25] [ info] [cmetrics] version=0.3.5

[2022/09/24 23:25:25] [ info] [input:forward:forward.0] listening on 0.0.0.0:24224

[2022/09/24 23:25:25] [ info] [sp] stream processor started

[2022/09/24 23:25:25] [ info] [output:stdout:stdout.0] worker #0 started

配置 logback-spring.xml

<?xml version="1.0" encoding="UTF-8"?>

<configuration debug="true">

<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">

<encoder>

<pattern>%date - %level - [%thread] - %logger - [%file:%line] - %msg%n</pattern>

</encoder>

</appender>

<appender name="FLUENT" class="ch.qos.logback.more.appenders.DataFluentAppender">

<tag>development</tag>

<label>normal</label>

<remoteHost>localhost</remoteHost>

<port>24224</port>

<maxQueueSize>20</maxQueueSize>

</appender>

<logger name="cn.netkiller.log" level="DEBUG"/>

<root level="DEBUG">

<appender-ref ref="STDOUT" />

<appender-ref ref="FLUENT" />

</root>

</configuration>

查看 fluent 输出

[0] development.normal: [1664033186.000000000, {"level"=>"INFO", "logger"=>"cn.netkiller.Application", "thread"=>"main", "message"=>"Starting Application using Java 18 on MacBook-Pro-Neo.local with PID 85696 (/Users/neo/workspace/bottleneck/target/classes started by neo in /Users/neo/workspace/bottleneck)"}]

[1] development.normal: [1664033186.000000000, {"level"=>"INFO", "logger"=>"cn.netkiller.Application", "thread"=>"main", "message"=>"The following 1 profile is active: "prod""}]

[0] development.normal: [1664033187.000000000, {"level"=>"INFO", "logger"=>"org.springframework.data.repository.config.RepositoryConfigurationDelegate", "thread"=>"main", "message"=>"Multiple Spring Data modules found, entering strict repository configuration mode"}]

[1] development.normal: [1664033187.000000000, {"level"=>"INFO", "logger"=>"org.springframework.data.repository.config.RepositoryConfigurationDelegate", "thread"=>"main", "message"=>"Bootstrapping Spring Data Redis repositories in DEFAULT mode."}]

[2] development.normal: [1664033187.000000000, {"level"=>"INFO", "logger"=>"org.springframework.data.repository.config.RepositoryConfigurationDelegate", "thread"=>"main", "message"=>"Finished Spring Data repository scanning in 6 ms. Found 0 Redis repository interfaces."}]

[0] development.normal: [1664033188.000000000, {"level"=>"INFO", "logger"=>"org.springframework.boot.web.embedded.tomcat.TomcatWebServer", "thread"=>"main", "message"=>"Tomcat initialized with port(s): 8080 (http)"}]

[1] development.normal: [1664033188.000000000, {"level"=>"INFO", "logger"=>"org.apache.catalina.core.StandardService", "thread"=>"main", "message"=>"Starting service [Tomcat]"}]

[2] development.normal: [1664033188.000000000, {"level"=>"INFO", "logger"=>"org.apache.catalina.core.StandardEngine", "thread"=>"main", "message"=>"Starting Servlet engine: [Apache Tomcat/9.0.65]"}]

[3] development.normal: [1664033188.000000000, {"level"=>"INFO", "logger"=>"org.apache.catalina.core.ContainerBase.[Tomcat].[localhost].[/]", "thread"=>"main", "message"=>"Initializing Spring embedded WebApplicationContext"}]

[4] development.normal: [1664033188.000000000, {"level"=>"INFO", "logger"=>"org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext", "thread"=>"main", "message"=>"Root WebApplicationContext: initialization completed in 2133 ms"}]

[0] development.normal: [1664033189.000000000, {"level"=>"INFO", "logger"=>"org.springframework.boot.actuate.endpoint.web.EndpointLinksResolver", "thread"=>"main", "message"=>"Exposing 14 endpoint(s) beneath base path '/actuator'"}]

[0] development.normal: [1664033189.000000000, {"level"=>"INFO", "logger"=>"org.springframework.boot.web.embedded.tomcat.TomcatWebServer", "thread"=>"main", "message"=>"Tomcat started on port(s): 8080 (http) with context path ''"}]

[1] development.normal: [1664033189.000000000, {"level"=>"INFO", "logger"=>"cn.netkiller.Application", "thread"=>"main", "message"=>"Started Application in 4.224 seconds (JVM running for 4.918)"}]

Loki4j Logback

https://loki4j.github.io/loki-logback-appender/

Maven

<dependency>

<groupId>com.github.loki4j</groupId>

<artifactId>loki-logback-appender</artifactId>

<version>1.3.2</version>

</dependency>

logback.xml

<appender name="LOKI" class="com.github.loki4j.logback.Loki4jAppender">

<http>

<url>http://localhost:3100/loki/api/v1/push</url>

</http>

<format>

<label>

<pattern>app=my-app,host=${HOSTNAME},level=%level</pattern>

</label>

<message>

<pattern>l=%level h=${HOSTNAME} c=%logger{20} t=%thread | %msg %ex</pattern>

</message>

<sortByTime>true</sortByTime>

</format>

</appender>

<root level="DEBUG">

<appender-ref ref="LOKI" />

</root>

4. Log4j2 + Gelf + Logstash

https://logging.paluch.biz/examples/log4j-2.x.html

Maven 配置

<dependency>

<groupId>org.springframework.boot</groupId>

<artifactId>spring-boot-starter-web</artifactId>

<!-- Exclude the Tomcat dependency -->

<exclusions>

<exclusion>

<groupId>org.springframework.boot</groupId>

<artifactId>spring-boot-starter-tomcat</artifactId>

</exclusion>

<!-- 禁用 logback -->

<exclusion>

<groupId>org.springframework.boot</groupId>

<artifactId>spring-boot-starter-logging</artifactId>

</exclusion>

</exclusions>

</dependency>

<!-- 添加Log4j2 依赖 -->

<dependency>

<groupId>org.springframework.boot</groupId>

<artifactId>spring-boot-starter-log4j2</artifactId>

</dependency>

<dependency>

<groupId>biz.paluch.logging</groupId>

<artifactId>logstash-gelf</artifactId>

<version>1.15.0</version>

</dependency>

log4j2.xml 配置

<?xml version="1.0" encoding="UTF-8"?>

<Configuration>

<properties>

<property name="log.pattern">[%d{yyyy-MM-dd HH:mm:ss}] [${hostName}] [%p] [%t] %l - %m%n</property>

<property name="log.dir">/tmp/logs</property>

<property name="log.level">info</property>

</properties>

<Appenders>

<!-- 控制台 -->

<Console name="Console" target="SYSTEM_OUT">

<PatternLayout pattern="${log.pattern}" />

</Console>

<!-- INFO级别日志 -->

<RollingFile name="RollingFileInfo" fileName="${log.dir}/info.log" filePattern="${log.dir}/info.%d{yyyy-MM-dd}.log">

<Filters>

<ThresholdFilter level="INFO" />

<ThresholdFilter level="WARN" onMatch="DENY" onMismatch="NEUTRAL" />

</Filters>

<PatternLayout pattern="${log.pattern}" />

<Policies>

<TimeBasedTriggeringPolicy interval="1" modulate="false" />

</Policies>

</RollingFile>

<!-- WARN级别日志 -->

<RollingFile name="RollingFileWarn" fileName="${log.dir}/warn.log" filePattern="${log.dir}/warn.%d{yyyy-MM-dd}.log">

<Filters>

<ThresholdFilter level="WARN" />

<ThresholdFilter level="ERROR" onMatch="DENY" onMismatch="NEUTRAL" />

</Filters>

<PatternLayout pattern="${log.pattern}" />

<Policies>

<TimeBasedTriggeringPolicy interval="1" modulate="false" />

</Policies>

</RollingFile>

<!-- ERROR级别日志 -->

<RollingFile name="RollingFileError" fileName="${log.dir}/error.log" filePattern="${log.dir}/error.%d{yyyy-MM-dd}.log">

<Filters>

<ThresholdFilter level="ERROR" />

</Filters>

<PatternLayout pattern="${log.pattern}" />

<Policies>

<TimeBasedTriggeringPolicy interval="1" modulate="false" />

</Policies>

</RollingFile>

<Gelf name="Gelf" host="udp:172.18.200.10" port="12201" version="1.1" extractStackTrace="true" filterStackTrace="true" mdcProfiling="true" includeFullMdc="true" maximumMessageSize="8192" originHost="%host{fqdn}">

<Field name="timestamp" pattern="%d{yyyy-MM-dd HH:mm:ss.SSS}" />

<Field name="logger" pattern="%logger" />

<Field name="level" pattern="%level" />

<Field name="class" pattern="%C{1}" />

<Field name="method" pattern="%M" />

<Field name="line" pattern="%L" />

<Field name="marker" pattern="%marker" />

<Filters>

<MarkerFilter marker="finance" onMatch="ACCEPT" onMismatch="NEUTRAL" />

<MarkerFilter marker="market" onMatch="ACCEPT" onMismatch="DENY" />

</Filters>

</Gelf>

</Appenders>

<Loggers>

<Root level="${sys:log.level}">

<AppenderRef ref="Console" />

<AppenderRef ref="Gelf" />

<!-- <AppenderRef ref="RollingFileInfo" /> <AppenderRef ref="RollingFileWarn" /> <AppenderRef ref="RollingFileError" /> -->

</Root>

</Loggers>

</Configuration>

Java 测试代码

@GetMapping("/log")

public String log() {

Marker finance = MarkerFactory.getMarker(LogMarker.finance.toString());

Marker customer = MarkerFactory.getMarker(LogMarker.customer.toString());

Marker market = MarkerFactory.getMarker(LogMarker.market.toString());

logger.info("常规日志");

logger.info(finance, "test");

logger.info(finance, "finance");

logger.info(customer, "customer");

logger.info(market, "market");

return "OK!!!\r\n";

}

@GetMapping("/log/marker")

public String marker(@RequestParam("marker") String marker, @RequestParam("msg") String msg) {

logger.info(MarkerFactory.getMarker(marker), msg);

msg += "\r\n";

return msg;

}

Logstash 配置

[root@netkiller log]# cat /etc/logstash/conf.d/file.conf

input {

tcp {

port => 4567

codec => json_lines

}

gelf {

port => 12201

use_udp => true

#use_tcp => true

}

}

filter {

ruby {

code => "event.set('datetime', event.get('@timestamp').time.localtime.strftime('%Y-%m-%d %H:%M:%S'))"

}

}

output {

file {

path => "/opt/log/%{marker}.%{+yyyy}-%{+MM}-%{+dd}.log"

codec => line { format => "[%{datetime}] %{level} %{message}"}

}

file {

path => "/opt/log/origin.%{+yyyy}-%{+MM}-%{+dd}.log.gz"

codec => json_lines

gzip => true

}

}

测试结果

[root@netkiller log]# ls

finance.2022-11-16.log  market.2022-11-16.log  origin.2022-11-16.log.gz

[root@netkiller log]# cat finance.2022-11-16.log

[2022-11-16 15:02:36] INFO test

[2022-11-16 15:02:36] INFO finance

[2022-11-16 15:21:34] INFO test

[2022-11-16 15:21:34] INFO finance

[root@netkiller log]# cat market.2022-11-16.log

[2022-11-16 15:02:36] INFO market

[2022-11-16 15:21:34] INFO market

[root@netkiller log]# zcat origin.2022-11-16.log.gz |jq

{

"datetime": "2022-11-16 15:21:34",

"timestamp": "2022-11-16 15:21:34.185",

"message": "market",

"host": "macbook-pro-neo.local",

"level": "INFO",

"line": 53,

"@version": "1",

"@timestamp": "2022-11-16T07:21:34.185Z",

"marker": "market",

"logger": "cn.netkiller.controller.HomeController",

"version": "1.1",

"method": "log",

"class": "HomeController",

"source_host": "172.18.5.142",

"facility": "logstash-gelf"

}

{

"datetime": "2022-11-16 15:21:34",

"timestamp": "2022-11-16 15:21:34.143",

"message": "test",

"host": "macbook-pro-neo.local",

"level": "INFO",

"line": 49,

"@version": "1",

"@timestamp": "2022-11-16T07:21:34.143Z",

"marker": "finance",

"logger": "cn.netkiller.controller.HomeController",

"version": "1.1",

"method": "log",

"class": "HomeController",

"source_host": "172.18.5.142",

"facility": "logstash-gelf"

}

{

"datetime": "2022-11-16 15:21:34",

"timestamp": "2022-11-16 15:21:34.184",

"message": "finance",

"host": "macbook-pro-neo.local",

"level": "INFO",

"line": 51,

"@version": "1",

"@timestamp": "2022-11-16T07:21:34.184Z",

"marker": "finance",

"logger": "cn.netkiller.controller.HomeController",

"version": "1.1",

"method": "log",

"class": "HomeController",

"source_host": "172.18.5.142",

"facility": "logstash-gelf"

}

Log4j2 更多技巧

多环境配置

方案一

logging:

config: classpath:log4j2-${spring.profiles.active}.xml

方案二

@SpringBootApplication

public class Application implements CommandLineRunner {

@Autowired

private Environment env;

public static void main(String[] args) {

SpringApplication.run(Application.class, args);

}

@Override

public void run(String... param) {

if (Arrays.asList(env.getActiveProfiles()).contains("dev")) {

Configurator.initialize(null, "/path/to/log4j2-dev.xml");

} else {

Configurator.initialize(null, "/path/to/log4j2.xml");

}

}

}

控制 class 日志输出级别

#日志配置 无特殊需求无需更改

logging:

config:  classpath:log4j2.xml

level:

root: INFO

javax.activation: info

org.apache.catalina: INFO

org.apache.commons.beanutils.converters: INFO

org.apache.coyote.http11.Http11Processor: INFO

org.apache.http: INFO

org.apache.tomcat: INFO

org.springframework: INFO

com.chinamobile.cmss.bdpaas.resource.monitor: DEBUG

日志输出级别

<Loggers>

<Logger name="com.ensd.service.sharding.MonthShardingAlgorithm" level="ERROR" />

<Root level="${sys:log.level}">

<AppenderRef ref="Console"/>

<AppenderRef ref="File"/>

<AppenderRef ref="Logstash"/>

</Root>

</Loggers>

读取系统变量/环境变量

${sys:catalina.home}/logs

${env:log.home}/logs

读取 spring 配置

引入依赖

<dependency>

<groupId>org.apache.logging.log4j</groupId>

<artifactId>log4j-spring-boot</artifactId>

</dependency>

例子

<Configuration name="ConfigTest" status="ERROR" monitorInterval="5">

<properties>

<property name="applicationName">${spring:spring.application.name}</property>

</properties>

<Appenders>

<SpringProfile name="dev | staging">

<Console name="Out">

<PatternLayout pattern="%m%n"/>

</Console>

</SpringProfile>

<SpringProfile name="prod">

<List name="Out">

</List>

</SpringProfile>

</Appenders>

<Loggers>

<Logger name="org.apache.test" level="trace" additivity="false">

<AppenderRef ref="Out"/>

</Logger>

<Root level="error">

<AppenderRef ref="Out"/>

</Root>

</Loggers>

</Configuration>

读取方法 ${spring:spring.profiles.active}

<?xml version="1.0" encoding="UTF-8"?>

<Configuration>

<properties>

<property name="log.pattern">[%d{yyyy-MM-dd HH:mm:ss}] [${hostName}] [%p] [%t] %l - %m%n</property>

<property name="log.home">/tmp/logs</property>

<property name="log.level">info</property>

</properties>

<Appenders>

<Console name="Console" target="SYSTEM_OUT">

<PatternLayout pattern="${log.pattern}" />

</Console>

<Gelf name="dev" host="udp:172.18.200.10" port="12201" version="1.1" extractStackTrace="true" filterStackTrace="true" mdcProfiling="true" includeFullMdc="true" maximumMessageSize="8192" originHost="%host{fqdn}">

<Field name="timestamp" pattern="%d{yyyy-MM-dd HH:mm:ss.SSS}" />

<Field name="logger" pattern="%logger" />

<Field name="level" pattern="%level" />

<Field name="class" pattern="%C{1}" />

<Field name="method" pattern="%M" />

<Field name="line" pattern="%L" />

<Field name="marker" pattern="%marker" />

<Filters>

<MarkerFilter marker="finance" onMatch="ACCEPT" onMismatch="NEUTRAL" />

<MarkerFilter marker="market" onMatch="ACCEPT" onMismatch="DENY" />

</Filters>

</Gelf>

</Appenders>

<Loggers>

<Root level="${sys:log.level}">

<AppenderRef ref="Console" />

<AppenderRef ref="${spring:spring.profiles.active:-dev}" />

</Root>

</Loggers>

</Configuration>

Spring 2.1.4 无法获取配置，解决方法使用 sys，同时启动的时候增加系统配置项 java -Dspring.application.name=netkiller -Dspring.profiles.active=dev -jar netkiller.jar

<property name="service">${sys:spring.application.name}</property>

<property name="environment">${sys:spring.profiles.active}</property>

变量默认值

格式是 ${变量名:-默认值}

<property name="service">${sys:spring.application.name:-dev}</property> <property name="environment">${sys:spring.profiles.active:-dev}</property>

5. 日志报警

Logstash 配置

将 ERROR 和 WARN 级别的日志发送到钉钉群

[root@netkiller ~]# cat /etc/logstash/conf.d/file.conf

input {

tcp {

port => 4567

codec => json_lines

}

gelf {

port => 12201

use_udp => true

#use_tcp => true

}

}

filter {

ruby {

code => "event.set('datetime', event.get('@timestamp').time.localtime.strftime('%Y-%m-%d %H:%M:%S'))"

}

}

output {

file {

path => "/opt/log/%{marker}.%{+yyyy}-%{+MM}-%{+dd}.log"

codec => line { format => "[%{datetime}] %{level} %{message}"}

}

file {

path => "/opt/log/origin.%{+yyyy}-%{+MM}-%{+dd}.log.gz"

codec => json_lines

gzip => true

}

if "ERROR" in [level] or "WARN" in [level] {

http {

url => "https://oapi.dingtalk.com/robot/send?access_token=56c27cb734a56cf549f6977ecc2761c4a16473db02d9d2881d008f9a239ba3e0"

http_method => "post"

content_type => "application/json; charset=utf-8"

format => "message"

message => '{"msgtype":"text","text":{"content":"Monitor: %{host}  - %{message}"}}'

}

}

}

监控 SpringBootApplication 的启动和退出

neo@MacBook-Pro-M2 ~ % cat workspace/bottleneck/src/main/java/cn/netkiller/Application.java

package cn.netkiller;

import org.slf4j.Logger;

import org.slf4j.LoggerFactory;

import org.slf4j.MarkerFactory;

import org.springframework.boot.SpringApplication;

import org.springframework.boot.autoconfigure.SpringBootApplication;

import org.springframework.cloud.client.discovery.EnableDiscoveryClient;

import jakarta.annotation.PostConstruct;

import jakarta.annotation.PreDestroy;

@EnableDiscoveryClient

@SpringBootApplication

public class Application {

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

@PostConstruct

public void init() {

System.out.printf("==================== init ====================");

logger.warn(MarkerFactory.getMarker("finance"), "XXX 系统启动");

}

@PreDestroy

public void destroy() {

System.out.printf("==================== destroy ====================");

logger.error(MarkerFactory.getMarker("finance"), "XXX 系统销毁");

}

public static void main(String[] args) {

System.out.println("Netkiller bottleneck tool!");

SpringApplication.run(Application.class, args);

}

}

@PostConstruct 可以监控 启动情况

@PreDestroy 可以监控 退出情况

6. Spring boot with ELK(Elasticsearch + Logstash + Kibana)

将 Spring boot 日志写入 ELK 有多种实现方式，这里仅提供三种方案：

1. Spring boot -> logback -> Tcp/IP -> logstash -> elasticsearch
这种方式实现非常方便不需要而外包或者软件

2. Spring boot -> logback -> Redis -> logstash -> elasticsearch
利用 Redis 提供的发布订阅功能将日志投递到 elasticsearch

3. Spring boot -> logback -> Kafka -> logstash -> elasticsearch
Kafka 方法适合大数据的情况。

TCP 方案

logstash 配置

input {

tcp {

host => "172.16.1.16"

port => 9250

mode => "server"

tags => ["tags"]

codec => json_lines  //可能需要更新logstash插件

}

}

output {

stdout{codec =>rubydebug}

elasticsearch {

hosts => ["localhost:9200"]  //这块配置需要带端口号

flush_size => 1000

}

}

Spring boot logback.xml 配置

<?xml version="1.0" encoding="UTF-8"?>

<configuration>

<property resource="properties/logback-variables.properties" />

<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">

<encoder charset="UTF-8">

<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger - %msg%n

</pattern>

</encoder>

</appender>

<appender name="LOGSTASH" class="net.logstash.logback.appender.LogstashTcpSocketAppender">

<destination>172.16.1.16:9250</destination>

<encoder charset="UTF-8" class="net.logstash.logback.encoder.LogstashEncoder" />

</appender>

<!--<appender name="async" class="ch.qos.logback.classic.AsyncAppender">-->

<!--<appender-ref ref="stash" />-->

<!--</appender>-->

<root level="info">                    <!-- 设置日志级别 -->

<appender-ref ref="STDOUT" />

<appender-ref ref="LOGSTASH" />

</root>

</configuration>

Redis 方案

https://github.com/kmtong/logback-redis-appender

Maven pom.xml 增加 Logback Redis 依赖

<!-- https://mvnrepository.com/artifact/com.cwbase/logback-redis-appender -->

<dependency>

<groupId>com.cwbase</groupId>

<artifactId>logback-redis-appender</artifactId>

<version>1.1.5</version>

</dependency>

Spring boot logback.xml 配置

<?xml version="1.0" encoding="UTF-8"?>

<configuration>

<include resource="org/springframework/boot/logging/logback/defaults.xml" />

<include resource="org/springframework/boot/logging/logback/file-appender.xml" />

<property name="type.name" value="test" />

<appender name="LOGSTASH" class="com.cwbase.logback.RedisAppender">

<source>spring-application</source>

<type>${type.name}</type>

<host>localhost</host>

<key>logstash:redis</key>

<tags>test-2</tags>

<mdc>true</mdc>

<location>true</location>

<callerStackIndex>0</callerStackIndex>

<!--additionalField添加附加字段 用于head插件显示 -->

<additionalField>

<key>MyKey</key>

<value>MyValue</value>

</additionalField>

<additionalField>

<key>MySecondKey</key>

<value>MyOtherValue</value>

</additionalField>

</appender>

<root level="INFO">

<appender-ref ref="FILE" />

<appender-ref ref="LOGSTASH" />

</root>

</configuration>

logstash 配置

input {

redis {

host => 'localhost'

data_type => 'list'

port => "6379"

key => 'logstash:redis' #自定义

type => 'redis-input'   #自定义

}

}

output {

elasticsearch {

host => "localhost"

codec => "json"

protocol => "http"

}

}

Kafka 方案

Other

第 7 章 Spring boot with Undertow

Undertow 是红帽公司开发的一款基于 NIO 的高性能 Web 嵌入式服务器

1. Maven 依赖

<dependencies>

<dependency>

<groupId>org.springframework.boot</groupId>

<artifactId>spring-boot-starter-web</artifactId>

<exclusions>

<!-- Exclude the Tomcat dependency -->

<exclusion>

<groupId>org.springframework.boot</groupId>

<artifactId>spring-boot-starter-tomcat</artifactId>

</exclusion>

</exclusions>

</dependency>

<!-- Use Undertow instead -->

<dependency>

<groupId>org.springframework.boot</groupId>

<artifactId>spring-boot-starter-undertow</artifactId>

</dependency>

</dependencies>

2. Application

import org.springframework.boot.SpringApplication;

import org.springframework.boot.autoconfigure.SpringBootApplication;

import org.springframework.web.bind.annotation.GetMapping;

import org.springframework.web.bind.annotation.RestController;

@RestController

@SpringBootApplication

public class Application {

public static void main(String[] args) {

SpringApplication.run(Application.class);

}

@GetMapping(value = "/undertow/test")

public String undertow() {

return "hello undertow";

}

}

3. 相关配置

Undertow 日志配置

#存放目录

server.undertow.accesslog.dir=

# 是否启用日志

server.undertow.accesslog.enabled=false

# 日志格式

server.undertow.accesslog.pattern=common

# 日志文件名前缀

server.undertow.accesslog.prefix=access_log

# 日志文件名后缀

server.undertow.accesslog.suffix=log

HTTP 相关配置

# HTTP POST请求最大的大小

server.undertow.max-http-post-size=0

# 设置IO线程数, 它主要执行非阻塞的任务,它们会负责多个连接, 默认设置每个CPU核心一个线程

server.undertow.io-threads=4

# 阻塞任务线程池, 当执行类似servlet请求阻塞操作, undertow会从这个线程池中取得线程,它的值设置取决于系统的负载

server.undertow.worker-threads=20

# 以下的配置会影响buffer,这些buffer会用于服务器连接的IO操作,有点类似netty的池化内存管理

# 每块buffer的空间大小,越小的空间被利用越充分

server.undertow.buffer-size=1024

# 每个区分配的buffer数量 , 所以pool的大小是buffer-size * buffers-per-region

server.undertow.buffers-per-region=1024

# 是否分配的直接内存

server.undertow.direct-buffers=true

第 8 章 Spring boot with Jetty

使用 Jetty 替代 Tomcat

<dependency>

<groupId>org.springframework.boot</groupId>

<artifactId>spring-boot-starter-web</artifactId>

<exclusions>

<!-- Exclude the Tomcat dependency -->

<exclusion>

<groupId>org.springframework.boot</groupId>

<artifactId>spring-boot-starter-tomcat</artifactId>

</exclusion>

</exclusions>

</dependency>

<!-- Use Jetty instead -->

<dependency>

<groupId>org.springframework.boot</groupId>

<artifactId>spring-boot-starter-jetty</artifactId>

</dependency>

第 9 章 Spring boot with HTTP2 SSL

1. 生成自签名证书

keytool -genkey -alias www.netkiller.cn -keyalg RSA -keystore /www/netkiller.cn/www.netkiller.cn.keystore

导入证书（Windows）

keytool -selfcert -alias www.netkiller.cn -keystore www.netkiller.cn.keystore

keytool -export -alias www.netkiller.cn -keystore www.netkiller.cn.keystore -storepass passw0rd -rfc -file www.netkiller.cn.cer

找到 Java 安装路径

[root@localhost ~]# alternatives --list

libnssckbi.so.x86_64    auto    /usr/lib64/pkcs11/p11-kit-trust.so

python                  auto    /usr/libexec/no-python

cifs-idmap-plugin       auto    /usr/lib64/cifs-utils/cifs_idmap_sss.so

ifup                    auto    /usr/libexec/nm-ifup

ld                      auto    /usr/bin/ld.bfd

python3                 auto    /usr/bin/python3.6

dockerd                 auto    /usr/bin/dockerd-ce

java                    manual  /usr/lib/jvm/java-14-openjdk-14.0.2.12-1.rolling.el8.x86_64/bin/java

jre_openjdk             auto    /usr/lib/jvm/java-1.8.0-openjdk-1.8.0.262.b10-0.el8_2.x86_64/jre

jre_14                  auto    /usr/lib/jvm/java-14-openjdk-14.0.2.12-1.rolling.el8.x86_64

jre_14_openjdk          auto    /usr/lib/jvm/jre-14-openjdk-14.0.2.12-1.rolling.el8.x86_64

javac                   auto    /usr/lib/jvm/java-1.8.0-openjdk-1.8.0.262.b10-0.el8_2.x86_64/bin/javac

java_sdk_openjdk        auto    /usr/lib/jvm/java-1.8.0-openjdk-1.8.0.262.b10-0.el8_2.x86_64

java_sdk_14             auto    /usr/lib/jvm/java-14-openjdk-14.0.2.12-1.rolling.el8.x86_64

java_sdk_14_openjdk     auto    /usr/lib/jvm/java-14-openjdk-14.0.2.12-1.rolling.el8.x86_64

jre_1.8.0               auto    /usr/lib/jvm/java-1.8.0-openjdk-1.8.0.262.b10-0.el8_2.x86_64/jre

jre_1.8.0_openjdk       auto    /usr/lib/jvm/jre-1.8.0-openjdk-1.8.0.262.b10-0.el8_2.x86_64

java_sdk_1.8.0          auto    /usr/lib/jvm/java-1.8.0-openjdk-1.8.0.262.b10-0.el8_2.x86_64

java_sdk_1.8.0_openjdk  auto    /usr/lib/jvm/java-1.8.0-openjdk-1.8.0.262.b10-0.el8_2.x86_64

mvn                     auto    /usr/share/maven/bin/mvn

导入证书（JVM）

keytool -importcert -alias www.netkiller.cn -file www.netkiller.cn.cer -keystore /srv/java/jre/lib/security/cacerts

2. application.properties 配置文件

配置Tomcat HTTPS 端口 8443（由于JVM不能fork和setuid，所以无法向nginx,apache httpd 那样设置 80 端口，除非你使用root用户运行，但这样做是不安全的。）

server.port=8443

server.ssl.enabled=true

server.ssl.key-store=/www/netkiller.cn/www.netkiller.cn.keystore

server.ssl.key-store-password=passw0rd

server.ssl.key-store-type=JKS

server.ssl.key-alias=www.netkiller.cn

keystore 文件可以放到 classpath 中，首先将证书文件放到 src/main/resources 目录中，然后配置 application.properties 如下：

server.port=8443

server.ssl.enabled=true

server.ssl.key-store=classpath:www.netkiller.cn.keystore

server.ssl.key-store-password=passw0rd

server.ssl.key-store-type=JKS

server.ssl.key-alias=www.netkiller.cn

3. 启动 Spring boot

/srv/java/bin/java -server -Xms2048m -Xmx8192m -Djava.security.egd=file:/dev/./urandom -jar /www/netkiller.cn/www.netkiller.cn/www.netkiller.cn-0.0.1.war

4. restTemplate 调用实例

String url = "https://www.netkiller.cn:8443/public/test/version.json";

ResponseEntity<RestResponse<String>> result = restTemplate.exchange(url, HttpMethod.GET, null, new ParameterizedTypeReference<RestResponse<String>>() {});

5. HTTP2

启用 HTTP2 必须使用 Tomcat 9 以上， Springboot 2.1

创建证书

keytool -genkey -alias localhost -storetype PKCS12 -keyalg RSA -keysize 2048 -storepass passw0rd -keystore localhost.p12 -dname "CN=localhost, OU=netkiller, O=netkiller.cn, L=Guangdong, ST=Shenzhen, C=CN"

keytool -selfcert -alias localhost -storepass passw0rd -keystore localhost.p12

keytool -export -alias localhost -keystore localhost.p12 -storepass passw0rd -rfc -file localhost.cer

keytool -importcert -trustcacerts -alias localhost -file localhost.cer -storepass passw0rd -keystore /etc/pki/java/cacerts

如果你是自己安装的JDK，需要找到cacerts安装路径

keytool -importcert -trustcacerts -alias localhost -file localhost.cer -storepass passw0rd -keystore /srv/java/jre/lib/security/cacerts

MacOS 添加方法，当提示你输入密码的时候，输入：changeit

iMac:resources neo$ sudo keytool -importcert -trustcacerts -alias localhost -file localhost.cer -cacerts

Password:

输入密钥库口令:

所有者: CN=localhost, OU=netkiller, O=netkiller.cn, L=Guangdong, ST=Shenzhen, C=CN

发布者: CN=localhost, OU=netkiller, O=netkiller.cn, L=Guangdong, ST=Shenzhen, C=CN

序列号: ffd28d78add2b56c

生效时间: Mon Sep 07 16:55:39 CST 2020, 失效时间: Sun Dec 06 16:55:39 CST 2020

证书指纹:

SHA1: A0:DB:69:34:66:EA:16:A3:AF:65:31:F9:5D:6E:C0:70:CA:5F:0E:22

SHA256: 2C:04:B7:BB:28:25:B5:E6:7C:0F:73:4B:02:38:6E:04:80:42:E2:F7:61:5C:91:4D:A8:EA:5E:20:2E:82:4F:0C

签名算法名称: SHA256withRSA

主体公共密钥算法: 2048 位 RSA 密钥

版本: 3

扩展:

#1: ObjectId: 2.5.29.14 Criticality=false

SubjectKeyIdentifier [

KeyIdentifier [

0000: 4E 30 9A EC C1 9D FB C2   CC 55 B2 6D 0D F4 01 CE  N0.......U.m....

0010: 13 C6 62 38                                        ..b8

]

]

是否信任此证书? [否]:  Y

证书已添加到密钥库中

iMac:resources neo$ keytool -list -cacerts -alias localhost

输入密钥库口令:

localhost, 2020年9月8日, trustedCertEntry,

证书指纹 (SHA-256): 2C:04:B7:BB:28:25:B5:E6:7C:0F:73:4B:02:38:6E:04:80:42:E2:F7:61:5C:91:4D:A8:EA:5E:20:2E:82:4F:0C

配置启用 http2

server:

port: 8443

servlet:

context-path: /

ssl:

enabled: true

key-store: classpath:ssl/localhost.p12

key-store-type: PKCS12

key-store-password: 123456

http2:

enabled: true

我的配置

spring.application.name=web

server.port=8443

#server.servlet.context-path=/

server.ssl.enabled=true

server.ssl.key-store=classpath:localhost.p12

server.ssl.key-store-type=PKCS12

server.ssl.key-store-password=123456

server.http2.enabled=true

使用 curl 访问可以看到 HTTP/2 字样，表示成功

neo@MacBook-Pro ~ % curl -i -k https://localhost:8443/ping

HTTP/2 200

content-type: text/plain;charset=UTF-8

content-length: 4

date: Tue, 09 Apr 2019 08:41:29 GMT

Pong%

第 10 章 Spring boot with Webpage

ViewResolver

1. Maven

<dependency>

<groupId>javax.servlet</groupId>

<artifactId>javax.servlet-api</artifactId>

</dependency>

<dependency>

<groupId>javax.servlet</groupId>

<artifactId>jstl</artifactId>

</dependency>

<dependency>

<groupId>org.springframework.boot</groupId>

<artifactId>spring-boot-starter-tomcat</artifactId>

</dependency>

<dependency>

<groupId>org.apache.tomcat.embed</groupId>

<artifactId>tomcat-embed-jasper</artifactId>

</dependency>

2. application.properties

spring.mvc.view.prefix=/WEB-INF/jsp/

spring.mvc.view.suffix=.jsp

3. Application

package cn.netkiller;

import org.springframework.boot.SpringApplication;

import org.springframework.boot.autoconfigure.EnableAutoConfiguration;

import org.springframework.boot.autoconfigure.SpringBootApplication;

import org.springframework.context.annotation.ComponentScan;

import org.springframework.scheduling.annotation.EnableScheduling;

@SpringBootApplication

@EnableAutoConfiguration

@ComponentScan

@EnableScheduling

public class Application {

public static void main(String[] args) {

SpringApplication.run(Application.class, args);

}

}

4. IndexController

package cn.netkiller.web;

import java.util.HashMap;

import java.util.Map;

import org.springframework.beans.factory.annotation.Autowired;

import org.springframework.stereotype.Controller;

import org.springframework.web.bind.annotation.PathVariable;

import org.springframework.web.bind.annotation.RequestMapping;

import org.springframework.web.bind.annotation.RequestParam;

import org.springframework.web.bind.annotation.ResponseBody;

import org.springframework.web.client.RestTemplate;

import org.springframework.web.servlet.ModelAndView;

@Controller

public class IndexController {

@RequestMapping("/welcome")

@ResponseBody

public String welcome() {

String message = "Welcome";

return message;

}

@RequestMapping("/index")

public ModelAndView index() {

String message = "Helloworld";

return new ModelAndView("index").addObject("message", message);

}

}

5. src/main/webapp/WEB-INF/jsp/index.jsp

<%@ page language="java" contentType="text/html; charset=UTF-8"

pageEncoding="UTF-8"%>

<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">

<html>

<head>

<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">

<title>Home</title>

</head>

<body>

${message}

</body>

</html>

6. 集成模板引擎

如果你需要使用其他模板引擎可以采用 Bean 注解方式。

package cn.netkiller.config;

import org.springframework.context.annotation.Bean;

import org.springframework.context.annotation.Configuration;

import org.springframework.web.servlet.config.annotation.DefaultServletHandlerConfigurer;

import org.springframework.web.servlet.config.annotation.EnableWebMvc;

import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;

import org.springframework.web.servlet.view.InternalResourceViewResolver;

@Configuration

@EnableWebMvc

public class WebMvcConfig extends WebMvcConfigurerAdapter {

@Override

public void configureDefaultServletHandling(DefaultServletHandlerConfigurer configurer) {

configurer.enable();

}

@Bean

public InternalResourceViewResolver viewResolver() {

InternalResourceViewResolver resolver = new InternalResourceViewResolver();

resolver.setPrefix("WEB-INF/jsp/");

resolver.setSuffix(".jsp");

return resolver;

}

}

第 11 章 Spring boot with Velocity template

1. Maven

<dependency>

<groupId>org.springframework.boot</groupId>

<artifactId>spring-boot-starter-velocity</artifactId>

</dependency>

<dependency>

<groupId>org.apache.velocity</groupId>

<artifactId>velocity</artifactId>

</dependency>

例 11.1. Spring boot with Velocity template (pom.xml)

<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>

<groupId>netkiller.cn</groupId>

<artifactId>api.netkiller.cn</artifactId>

<version>0.0.1-SNAPSHOT</version>

<packaging>jar</packaging>

<name>api.netkiller.cn</name>

<url>http://maven.apache.org</url>

<properties>

<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>

<java.version>1.8</java.version>

</properties>

<parent>

<groupId>org.springframework.boot</groupId>

<artifactId>spring-boot-starter-parent</artifactId>

<version>2.3.1.RELEASE</version>

</parent>

<dependencies>

<dependency>

<groupId>org.springframework.boot</groupId>

<artifactId>spring-boot-starter-web</artifactId>

</dependency>

<!-- <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> -->

<dependency>

<groupId>org.springframework.boot</groupId>

<artifactId>spring-boot-starter-data-jpa</artifactId>

</dependency>

<dependency>

<groupId>org.springframework.boot</groupId>

<artifactId>spring-boot-starter-jdbc</artifactId>

</dependency>

<dependency>

<groupId>org.springframework.boot</groupId>

<artifactId>spring-boot-starter-data-redis</artifactId>

</dependency>

<dependency>

<groupId>org.springframework.boot</groupId>

<artifactId>spring-boot-starter-data-mongodb</artifactId>

</dependency>

<dependency>

<groupId>org.springframework.boot</groupId>

<artifactId>spring-boot-starter-amqp</artifactId>

</dependency>

<dependency>

<groupId>org.springframework.boot</groupId>

<artifactId>spring-boot-devtools</artifactId>

</dependency>

<dependency>

<groupId>org.springframework.boot</groupId>

<artifactId>spring-boot-starter-test</artifactId>

<scope>test</scope>

</dependency>

<dependency>

<groupId>org.springframework.data</groupId>

<artifactId>spring-data-mongodb</artifactId>

</dependency>

<dependency>

<groupId>org.springframework.data</groupId>

<artifactId>spring-data-oracle</artifactId>

<version>1.0.0.RELEASE</version>

</dependency>

<dependency>

<groupId>com.oracle</groupId>

<artifactId>ojdbc6</artifactId>

<!-- <version>12.1.0.1</version> -->

<version>11.2.0.3</version>

<scope>system</scope>

<systemPath>${basedir}/lib/ojdbc6.jar</systemPath>

</dependency>

<dependency>

<groupId>mysql</groupId>

<artifactId>mysql-connector-java</artifactId>

</dependency>

<dependency>

<groupId>org.springframework.boot</groupId>

<artifactId>spring-boot-starter-mail</artifactId>

</dependency>

<dependency>

<groupId>org.springframework.boot</groupId>

<artifactId>spring-boot-starter-velocity</artifactId>

</dependency>

<dependency>

<groupId>org.apache.velocity</groupId>

<artifactId>velocity</artifactId>

</dependency>

<dependency>

<groupId>com.google.code.gson</groupId>

<artifactId>gson</artifactId>

<scope>compile</scope>

</dependency>

<dependency>

<groupId>junit</groupId>

<artifactId>junit</artifactId>

<scope>test</scope>

</dependency>

</dependencies>

<build>

<sourceDirectory>src</sourceDirectory>

<plugins>

<plugin>

<groupId>org.springframework.boot</groupId>

<artifactId>spring-boot-maven-plugin</artifactId>

</plugin>

<plugin>

<artifactId>maven-compiler-plugin</artifactId>

<version>3.3</version>

<configuration>

<source />

<target />

</configuration>

</plugin>

<plugin>

<artifactId>maven-war-plugin</artifactId>

<version>2.6</version>

<configuration>

<warSourceDirectory>WebContent</warSourceDirectory>

<failOnMissingWebXml>false</failOnMissingWebXml>

</configuration>

</plugin>

</plugins>

</build>

</project>

2. Resource

src/main/resources/application.properties

spring.velocity.resourceLoaderPath=classpath:/templates/

spring.velocity.prefix=

spring.velocity.suffix=.vm

spring.velocity.cache=false

spring.velocity.check-template-location=true

spring.velocity.content-type=text/html

spring.velocity.charset=UTF-8

spring.velocity.properties.input.encoding=UTF-8

spring.velocity.properties.output.encoding=UTF-8

src/main/resources/templates/email.vm

<html>

<body>

<h3>${title}!</h3>

<p>${body}</p>

</body>

</html>

3. Application

package api;

import org.springframework.boot.SpringApplication;

import org.springframework.boot.autoconfigure.EnableAutoConfiguration;

import org.springframework.boot.autoconfigure.SpringBootApplication;

import org.springframework.boot.context.properties.EnableConfigurationProperties;

import org.springframework.context.annotation.Bean;

import org.springframework.context.annotation.ComponentScan;

import org.springframework.data.jpa.repository.config.EnableJpaRepositories;

import org.springframework.data.mongodb.repository.config.EnableMongoRepositories;

import org.springframework.web.servlet.config.annotation.CorsRegistry;

import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;

import api.ApplicationConfiguration;

@SpringBootApplication

@EnableConfigurationProperties(ApplicationConfiguration.class)

@EnableAutoConfiguration

@ComponentScan({ "api.web", "api.rest", "api.service" })

@EnableMongoRepositories

@EnableJpaRepositories

public class Application {

public static void main(String[] args) {

SpringApplication.run(Application.class, args);

}

}

4. RestController

package api.rest;

import java.io.File;

import java.util.HashMap;

import java.util.Map;

import javax.mail.internet.MimeMessage;

import org.apache.velocity.app.VelocityEngine;

import org.springframework.beans.factory.annotation.Autowired;

import org.springframework.core.io.FileSystemResource;

import org.springframework.http.HttpStatus;

import org.springframework.http.ResponseEntity;

import org.springframework.mail.SimpleMailMessage;

import org.springframework.mail.javamail.JavaMailSender;

import org.springframework.mail.javamail.MimeMessageHelper;

import org.springframework.ui.velocity.VelocityEngineUtils;

import org.springframework.web.bind.annotation.RequestBody;

import org.springframework.web.bind.annotation.RequestMapping;

import org.springframework.web.bind.annotation.RequestMethod;

import org.springframework.web.bind.annotation.ResponseStatus;

import org.springframework.web.bind.annotation.RestController;

import api.pojo.Email;

@RestController

@RequestMapping("/v1/email")

public class EmailRestController extends CommonRestController {

@Autowired

private JavaMailSender javaMailSender;

@Autowired

private VelocityEngine velocityEngine;

@RequestMapping("version")

@ResponseStatus(HttpStatus.OK)

public String version() {

return "[OK] Welcome to withdraw Restful version 1.0";

}

@RequestMapping(value = "send", method = RequestMethod.POST, produces = { "application/xml", "application/json" })

public ResponseEntity<Email> sendSimpleMail(@RequestBody Email email) {

SimpleMailMessage message = new SimpleMailMessage();

message.setFrom(email.getFrom());

message.setTo(email.getTo());

message.setSubject(email.getSubject());

message.setText(email.getText());

javaMailSender.send(message);

email.setStatus(true);

return new ResponseEntity<Email>(email, HttpStatus.OK);

}

@RequestMapping(value = "attachments", method = RequestMethod.POST, produces = { "application/xml", "application/json" })

public ResponseEntity<Email> attachments(@RequestBody Email email) throws Exception {

MimeMessage mimeMessage = javaMailSender.createMimeMessage();

MimeMessageHelper mimeMessageHelper = new MimeMessageHelper(mimeMessage, true);

mimeMessageHelper.setFrom(email.getFrom());

mimeMessageHelper.setTo(email.getTo());

mimeMessageHelper.setSubject(email.getSubject());

mimeMessageHelper.setText("<html><body><img src=\"cid:banner\" >" + email.getText() + "</body></html>", true);

FileSystemResource file = new FileSystemResource(new File("banner.jpg"));

mimeMessageHelper.addInline("banner", file);

FileSystemResource fileSystemResource = new FileSystemResource(new File("Attachment.jpg"));

mimeMessageHelper.addAttachment("Attachment.jpg", fileSystemResource);

javaMailSender.send(mimeMessage);

email.setStatus(true);

return new ResponseEntity<Email>(email, HttpStatus.OK);

}

@RequestMapping(value = "template", method = RequestMethod.POST, produces = { "application/xml", "application/json" })

public ResponseEntity<Email> template(@RequestBody Email email) throws Exception {

Map<String, Object> model = new HashMap<String, Object>();

model.put("title", email.getSubject());

model.put("body", email.getText());

String text = VelocityEngineUtils.mergeTemplateIntoString(velocityEngine, "email.vm", "UTF-8", model);

System.out.println(text);

MimeMessage mimeMessage = javaMailSender.createMimeMessage();

MimeMessageHelper mimeMessageHelper = new MimeMessageHelper(mimeMessage, true);

mimeMessageHelper.setFrom(email.getFrom());

mimeMessageHelper.setTo(email.getTo());

mimeMessageHelper.setSubject(email.getSubject());

mimeMessageHelper.setText(text, true);

javaMailSender.send(mimeMessage);

email.setStatus(true);

return new ResponseEntity<Email>(email, HttpStatus.OK);

}

}

5. Test

$ curl -i -H "Accept: application/json" -H "Content-Type: application/json" -X POST -d '{"from":"www@netkiller.cn", "to":"21214094@qq.com","subject":"Hello","text":"Hello world!!!"}' http://172.16.0.20:8080/v1/email/template.json

第 12 章 Spring boot with Thymeleaf

1. Maven

<dependency>

<groupId>org.springframework.boot</groupId>

<artifactId>spring-boot-starter-thymeleaf</artifactId>

</dependency>

2. application.properties

创建目录 src/main/resources/templates/ 用户存放模板文件

spring.thymeleaf.prefix=classpath:/templates/

spring.thymeleaf.suffix=.html

spring.thymeleaf.mode=HTML5

spring.thymeleaf.encoding=UTF-8

spring.thymeleaf.content-type=text/html

spring.thymeleaf.cache=false

3. Controller

package cn.netkiller.controller;

import org.springframework.stereotype.Controller;

import org.springframework.ui.ModelMap;

import org.springframework.web.bind.annotation.PathVariable;

import org.springframework.web.bind.annotation.RequestMapping;

import org.springframework.web.bind.annotation.RequestMethod;

@Controller

@RequestMapping("/")

public class HelloController {

@GetMapping("/hello")

// 如果此处使用 @ResponseBody，将会返回 "hello" 字符串，而不是模板

public String test() {

return "hello";

}

@RequestMapping("/hello1")

public String hello1(Map<String, Object> map) {

// 传递参数测试

map.put("name", "Neo");

return "thymeleaf";

}

@RequestMapping("/hello2")

public ModelAndView hello2() {

ModelAndView mv = new ModelAndView();

mv.addObject("name", "Amy");

mv.setViewName("thymeleaf");

return mv;

}

@RequestMapping(value = "/{name}", method = RequestMethod.GET)

public String getMovie(@PathVariable String name, ModelMap model) {

model.addAttribute("name", name);

return "hello";

}

}

4. HTML5 Template

在 src/main/resources/templates/ 目录下创建模板文件 hello.html

<!DOCTYPE html>

<html lang="en" xmlns:th="http://www.thymeleaf.org">

<head>

<meta charset="UTF-8" />

<title>Spring MVC + Thymeleaf Example</title>

</head>

<body>

<h1>Welcome to Thymeleaf</h1>

<span th:text="${name}"></span>

</body>

</html>

第 13 章 Spring boot with MongoDB

1. Maven

pom.xml

<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>

<groupId>netkiller.cn</groupId>

<artifactId>api.netkiller.cn</artifactId>

<version>0.0.1-SNAPSHOT</version>

<packaging>jar</packaging>

<name>api.netkiller.cn</name>

<url>http://maven.apache.org</url>

<properties>

<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>

</properties>

<parent>

<groupId>org.springframework.boot</groupId>

<artifactId>spring-boot-starter-parent</artifactId>

<version>2.0.2.RELEASE</version>

</parent>

<dependencies>

<dependency>

<groupId>org.springframework.boot</groupId>

<artifactId>spring-boot-starter-web</artifactId>

</dependency>

<!--

<dependency>

<groupId>org.springframework.boot</groupId>

<artifactId>spring-boot-starter-security</artifactId>

</dependency>

<dependency>

<groupId>org.springframework.boot</groupId>

<artifactId>spring-boot-starter-data-jpa</artifactId>

</dependency>

<dependency>

<groupId>org.springframework.boot</groupId>

<artifactId>spring-boot-starter-jdbc</artifactId>

</dependency>

-->

<dependency>

<groupId>org.springframework.boot</groupId>

<artifactId>spring-boot-starter-data-redis</artifactId>

</dependency>

<dependency>

<groupId>org.springframework.boot</groupId>

<artifactId>spring-boot-starter-data-mongodb</artifactId>

</dependency>

<dependency>

<groupId>org.springframework.boot</groupId>

<artifactId>spring-boot-starter-amqp</artifactId>

</dependency>

<dependency>

<groupId>org.springframework.boot</groupId>

<artifactId>spring-boot-devtools</artifactId>

</dependency>

<dependency>

<groupId>org.springframework.boot</groupId>

<artifactId>spring-boot-starter-test</artifactId>

<scope>test</scope>

</dependency>

<dependency>

<groupId>org.springframework.data</groupId>

<artifactId>spring-data-mongodb</artifactId>

</dependency>

<dependency>

<groupId>com.google.code.gson</groupId>

<artifactId>gson</artifactId>

<scope>compile</scope>

</dependency>

<dependency>

<groupId>junit</groupId>

<artifactId>junit</artifactId>

<scope>test</scope>

</dependency>

</dependencies>

<build>

<sourceDirectory>src</sourceDirectory>

<plugins>

<plugin>

<groupId>org.springframework.boot</groupId>

<artifactId>spring-boot-maven-plugin</artifactId>

</plugin>

<plugin>

<artifactId>maven-compiler-plugin</artifactId>

<version>3.3</version>

<configuration>

<source />

<target />

</configuration>

</plugin>

<plugin>

<artifactId>maven-war-plugin</artifactId>

<version>2.6</version>

<configuration>

<warSourceDirectory>WebContent</warSourceDirectory>

<failOnMissingWebXml>false</failOnMissingWebXml>

</configuration>

</plugin>

</plugins>

</build>

</project>

2. Application

Application.java

import org.springframework.boot.SpringApplication;

import org.springframework.boot.autoconfigure.EnableAutoConfiguration;

import org.springframework.boot.autoconfigure.SpringBootApplication;

import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;

import org.springframework.context.annotation.Bean;

import org.springframework.context.annotation.ComponentScan;

import org.springframework.context.annotation.Configuration;

import org.springframework.data.authentication.UserCredentials;

import org.springframework.data.mongodb.MongoDbFactory;

import org.springframework.data.mongodb.core.MongoTemplate;

import org.springframework.data.mongodb.core.SimpleMongoDbFactory;

import org.springframework.data.mongodb.repository.config.EnableMongoRepositories;

import com.mongodb.Mongo;

@Configuration

@SpringBootApplication

@EnableAutoConfiguration(exclude = { DataSourceAutoConfiguration.class })

@ComponentScan({ "web", "rest" })

@EnableMongoRepositories

public class Application {

@SuppressWarnings("deprecation")

public @Bean MongoDbFactory mongoDbFactory() throws Exception {

UserCredentials userCredentials = new UserCredentials("finance", "En7d0l0wssXQ8owzedjb82I0BMd4pFoZ");

return new SimpleMongoDbFactory(new Mongo("db.netkiller.cn"), "finance", userCredentials);

}

public @Bean MongoTemplate mongoTemplate() throws Exception {

return new MongoTemplate(mongoDbFactory());

}

public static void main(String[] args) {

SpringApplication.run(Application.class, args);

}

}

3. MongoTemplate

package web;

import org.springframework.beans.factory.annotation.Autowired;

import org.springframework.jdbc.core.JdbcTemplate;

import org.springframework.stereotype.Controller;

import org.springframework.web.bind.annotation.RequestMapping;

import org.springframework.web.bind.annotation.RequestParam;

import org.springframework.web.bind.annotation.ResponseBody;

import api.domain.City;

import api.domain.Article;

import api.ApplicationConfiguration;

import api.repository.CityRepository;

import api.repository.ArticleRepository;

import api.service.TestService;

@Controller

public class IndexController {

@Autowired

private CityRepository repository;

@Autowired

private TestService testService;

@Autowired

private ApplicationConfiguration propertie;

@RequestMapping("/repository")

@ResponseBody

public String repository() {

repository.deleteAll();

// save a couple of city

repository.save(new City("Shenzhen", "China"));

repository.save(new City("Beijing", "China"));

System.out.println("--------------------------------");

// fetch all city

for (City city : repository.findAll()) {

System.out.println(city);

}

// fetch an individual city

System.out.println("--------------------------------");

System.out.println(repository.findByName("Shenzhen"));

System.out.println("--------------------------------");

for (City city : repository.findByCountry("China")) {

System.out.println(city);

}

String message = "Hello";

return message;

}

}

4. Repository

在上一节 MongoTemplate 中，继续添加下面代码。

package api;

import org.springframework.boot.SpringApplication;

import org.springframework.boot.autoconfigure.EnableAutoConfiguration;

import org.springframework.boot.autoconfigure.SpringBootApplication;

import org.springframework.context.annotation.Bean;

import org.springframework.context.annotation.ComponentScan;

import org.springframework.web.servlet.config.annotation.CorsRegistry;

import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;

@SpringBootApplication

@EnableAutoConfiguration(exclude = { DataSourceAutoConfiguration.class })

@ComponentScan({ "api.web", "api.rest","api.service" })

@EnableMongoRepositories

public class Application {

public @Bean WebMvcConfigurer corsConfigurer() {

return new WebMvcConfigurerAdapter() {

@Override

public void addCorsMappings(CorsRegistry registry) {

registry.addMapping("/**");

}

};

}

public static void main(String[] args) {

SpringApplication.run(Application.class, args);

}

}

Resource: src/main/resources/application.properties

spring.data.mongodb.uri=mongodb://finance:XQ8os82I0pFoZBMd4@mdb.netkiller.cn/finance

spring.data.mongodb.repositories.enabled=true

CityRepository.java

package repository;

import java.util.List;

import org.springframework.data.domain.Page;

import org.springframework.data.domain.Pageable;

import org.springframework.data.mongodb.repository.MongoRepository;

import domain.City;

public interface CityRepository extends MongoRepository<City, String> {

public Page<City> findAll(Pageable pageable);

public City findByNameAndCountry(String name, String country);

public City findByName(String name);

public List<City> findByCountry(String country);

}

City.java

package domain;

import org.springframework.data.annotation.Id;

import org.springframework.data.mongodb.core.mapping.Document;

@Document(collection = "city")

public class City {

@Id

private String id;

public String name;

public String country;

public City(String name, String country){

this.setName(name);

this.setCountry(country);

}

public String getName() {

return name;

}

public void setName(String name) {

this.name = name;

}

public String getCountry() {

return country;

}

public void setCountry(String country) {

this.country = country;

}

@Override

public String toString() {

return "City [id=" + id + ", name=" + name + ", country=" + country + "]";

}

}

在 IndexController 中调用 CityRepository

package web;

import org.springframework.beans.factory.annotation.Autowired;

import org.springframework.stereotype.Controller;

import org.springframework.web.bind.annotation.RequestMapping;

import org.springframework.web.bind.annotation.ResponseBody;

import repository.CityRepository;

import domain.City;

import repository.CityRepository;

@Controller

public class IndexController {

@Autowired

private CityRepository repository;

@RequestMapping("/index")

@ResponseBody

public ModelAndView index() {

String message = "Hello";

return new ModelAndView("home/welcome", "variable", message);

}

@RequestMapping("/curd")

@ResponseBody

public String curd() {

repository.deleteAll();

// save a couple of city

repository.save(new City("Shenzhen", "China"));

repository.save(new City("Beijing", "China"));

System.out.println("--------------------------------");

// fetch all city

for (City city : repository.findAll()) {

System.out.println(city);

}

// fetch an individual city

System.out.println("--------------------------------");

System.out.println(repository.findByName("Shenzhen"));

System.out.println("--------------------------------");

for (City city : repository.findByCountry("China")) {

System.out.println(city);

}

String message = "Hello";

return message;

}

}

第 14 章 Spring boot with MySQL

1. Maven

pom.xml

<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>

<groupId>netkiller.cn</groupId>

<artifactId>api.netkiller.cn</artifactId>

<version>0.0.1-SNAPSHOT</version>

<packaging>jar</packaging>

<name>api.netkiller.cn</name>

<url>http://maven.apache.org</url>

<properties>

<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>

</properties>

<parent>

<groupId>org.springframework.boot</groupId>

<artifactId>spring-boot-starter-parent</artifactId>

<version>2.0.2.RELEASE</version>

</parent>

<dependencies>

<dependency>

<groupId>org.springframework.boot</groupId>

<artifactId>spring-boot-starter-web</artifactId>

</dependency>

<!--

<dependency>

<groupId>org.springframework.boot</groupId>

<artifactId>spring-boot-starter-security</artifactId>

</dependency>

-->

<dependency>

<groupId>org.springframework.boot</groupId>

<artifactId>spring-boot-starter-data-jpa</artifactId>

</dependency>

<dependency>

<groupId>org.springframework.boot</groupId>

<artifactId>spring-boot-starter-jdbc</artifactId>

</dependency>

<dependency>

<groupId>org.springframework.boot</groupId>

<artifactId>spring-boot-starter-data-redis</artifactId>

</dependency>

<dependency>

<groupId>org.springframework.boot</groupId>

<artifactId>spring-boot-starter-data-mongodb</artifactId>

</dependency>

<dependency>

<groupId>org.springframework.boot</groupId>

<artifactId>spring-boot-starter-amqp</artifactId>

</dependency>

<dependency>

<groupId>org.springframework.boot</groupId>

<artifactId>spring-boot-devtools</artifactId>

</dependency>

<dependency>

<groupId>org.springframework.boot</groupId>

<artifactId>spring-boot-starter-test</artifactId>

<scope>test</scope>

</dependency>

<dependency>

<groupId>org.springframework.data</groupId>

<artifactId>spring-data-mongodb</artifactId>

</dependency>

<dependency>

<groupId>mysql</groupId>

<artifactId>mysql-connector-java</artifactId>

</dependency>

<dependency>

<groupId>com.google.code.gson</groupId>

<artifactId>gson</artifactId>

<scope>compile</scope>

</dependency>

<dependency>

<groupId>junit</groupId>

<artifactId>junit</artifactId>

<scope>test</scope>

</dependency>

</dependencies>

<build>

<sourceDirectory>src</sourceDirectory>

<plugins>

<plugin>

<groupId>org.springframework.boot</groupId>

<artifactId>spring-boot-maven-plugin</artifactId>

</plugin>

<plugin>

<artifactId>maven-compiler-plugin</artifactId>

<version>3.3</version>

<configuration>

<source />

<target />

</configuration>

</plugin>

<plugin>

<artifactId>maven-war-plugin</artifactId>

<version>2.6</version>

<configuration>

<warSourceDirectory>WebContent</warSourceDirectory>

<failOnMissingWebXml>false</failOnMissingWebXml>

</configuration>

</plugin>

</plugins>

</build>

</project>

2. Resource

src/main/resources/application.properties

spring.datasource.driver-class-name=com.mysql.jdbc.Driver

spring.datasource.url=jdbc:mysql://192.168.6.1:3306/test

spring.datasource.username=root

spring.datasource.password=password

spring.jpa.database=MYSQL

spring.jpa.show-sql=true

spring.jpa.hibernate.ddl-auto=update

#spring.jpa.hibernate.ddl-auto=create-drop

spring.datasource.url=jdbc:mysql://localhost:3306/test

spring.datasource.username=root

spring.datasource.password=pasword

spring.datasource.driver-class-name=com.mysql.jdbc.Driver

spring.datasource.max-idle=10

spring.datasource.max-wait=10000

spring.datasource.min-idle=5

spring.datasource.initial-size=5

spring.datasource.validation-query=SELECT 1

spring.datasource.test-on-borrow=false

spring.datasource.test-while-idle=true

spring.datasource.time-between-eviction-runs-millis=18800

spring.datasource.jdbc-interceptors=ConnectionState;SlowQueryReport(threshold=0)

3. Application

package api;

import org.springframework.boot.SpringApplication;

import org.springframework.boot.autoconfigure.EnableAutoConfiguration;

import org.springframework.boot.autoconfigure.SpringBootApplication;

import org.springframework.boot.context.properties.EnableConfigurationProperties;

import org.springframework.context.annotation.Bean;

import org.springframework.context.annotation.ComponentScan;

import api.ApplicationConfiguration;

@SpringBootApplication

@EnableConfigurationProperties(ApplicationConfiguration.class)

@EnableAutoConfiguration

@ComponentScan({ "api.web", "api.rest","api.service" })

@EnableMongoRepositories

public class Application {

public static void main(String[] args) {

SpringApplication.run(Application.class, args);

}

}

4. JdbcTemplate

package api.web;

import org.springframework.beans.factory.annotation.Autowired;

import org.springframework.jdbc.core.JdbcTemplate;

import org.springframework.stereotype.Controller;

import org.springframework.web.bind.annotation.RequestMapping;

import org.springframework.web.bind.annotation.RequestParam;

import org.springframework.web.bind.annotation.ResponseBody;

import api.domain.City;

import api.domain.Article;

import api.ApplicationConfiguration;

import api.repository.CityRepository;

import api.repository.ArticleRepository;

import api.service.TestService;

@Controller

public class IndexController {

@Autowired

private CityRepository repository;

@Autowired

private TestService testService;

@Autowired

private ApplicationConfiguration propertie;

@Autowired

private JdbcTemplate jdbcTemplate;

@RequestMapping(value = "/article")

public @ResponseBody String dailyStats(@RequestParam Integer id) {

String query = "SELECT id, title, content from article where id = " + id;

return jdbcTemplate.queryForObject(query, (resultSet, i) -> {

System.out.println(resultSet.getLong(1)+","+ resultSet.getString(2)+","+ resultSet.getString(3));

return (resultSet.getLong(1)+","+ resultSet.getString(2)+","+ resultSet.getString(3));

});

}

}

5. CrudRepository

ArticleRepository

package api.repository;

import org.springframework.data.domain.Page;

import org.springframework.data.domain.Pageable;

import org.springframework.data.repository.CrudRepository;

import org.springframework.stereotype.Repository;

import api.domain.Article;

@Repository

public interface ArticleRepository extends CrudRepository<Article, Long> {

Page<Article> findAll(Pageable pageable);

}

Article.java

package api.domain;

import java.io.Serializable;

import javax.persistence.Column;

import javax.persistence.Entity;

import javax.persistence.Id;

import javax.persistence.GeneratedValue;

import javax.persistence.GenerationType;

import javax.persistence.Table;

@Entity

@Table(name = "article")

public class Article implements Serializable {

private static final long serialVersionUID = 7998903421265538801L;

@Id

@GeneratedValue(strategy=GenerationType.IDENTITY)

@Column(name = "id", unique=true, nullable=false, insertable=true, updatable = false)

private Long id;

private String title;

private String content;

public Article(){

}

public Article(String title, String content) {

this.title = title;

this.content = content;

}

public Long getId() {

return id;

}

public void setId(long id) {

this.id = id;

}

public String getTitle() {

return title;

}

public void setTitle(String title) {

this.title = title;

}

public String getContent() {

return content;

}

public void setContent(String content) {

this.content = content;

}

@Override

public String toString() {

return "Article [id=" + id + ", title=" + title + ", content=" + content + "]";

}

}

package api.web;

import org.springframework.beans.factory.annotation.Autowired;

import org.springframework.jdbc.core.JdbcTemplate;

import org.springframework.stereotype.Controller;

import org.springframework.web.bind.annotation.RequestMapping;

import org.springframework.web.bind.annotation.RequestParam;

import org.springframework.web.bind.annotation.ResponseBody;

import api.domain.City;

import api.domain.Article;

import api.ApplicationConfiguration;

import api.repository.CityRepository;

import api.repository.ArticleRepository;

import api.service.TestService;

@Controller

public class IndexController {

@Autowired

private CityRepository repository;

@Autowired

private TestService testService;

@Autowired

private ApplicationConfiguration propertie;

@Autowired

private ArticleRepository articleRepository;

@RequestMapping("/save")

@ResponseBody

public String save() {

articleRepository.save(new Article("Neo", "Chen"));

return "OK";

}

@RequestMapping("/mysql")

@ResponseBody

public String mysql() {

for (Article article : articleRepository.findAll()) {

System.out.println(article);

}

return "OK";

}

}

第 15 章 Spring boot with Oracle

1. Maven

首先到oracle官网，根据你的Oracle数据库，下载ojdbc6.jar(Oracle 11) 或者 ojdbc7.jar (Oracle 12)

<dependency>

<groupId>com.oracle</groupId>

<artifactId>ojdbc7</artifactId>

<version>12.1.0.1</version>

</dependency>

mvn install:install-file -DgroupId=com.oracle -DartifactId=ojdbc6 -Dversion=11.2.0.3 -Dpackaging=jar -Dfile=ojdbc6.jar -DgeneratePom=true

mvn install:install-file -Dfile=ojdbc7.jar -DgroupId=com.oracle -DartifactId=ojdbc7 -Dversion=12.1.0.1 -Dpackaging=jar

另一种方案是在项目根目录下创建一个/lib文件夹，将下载的驱动放入该文件夹中。然后pom.xml 加入下面代码

ojdbc6.jar 例子

<dependency>

<groupId>com.oracle</groupId>

<artifactId>ojdbc6</artifactId>

<version>11.2.0.3</version>

<scope>system</scope>

<systemPath>${basedir}/lib/ojdbc6.jar</systemPath>

</dependency>

ojdbc7.jar 例子

<dependency>

<groupId>com.oracle</groupId>

<artifactId>ojdbc7</artifactId>

<version>12.1.0.1</version>

<scope>system</scope>

<systemPath>${basedir}/lib/ojdbc7.jar</systemPath>

</dependency>

例 15.1. Example Spring boot with Oracle

<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>

<groupId>netkiller.cn</groupId>

<artifactId>api.netkiller.cn</artifactId>

<version>0.0.1-SNAPSHOT</version>

<packaging>jar</packaging>

<name>api.netkiller.cn</name>

<url>http://maven.apache.org</url>

<properties>

<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>

<java.version>1.8</java.version>

</properties>

<parent>

<groupId>org.springframework.boot</groupId>

<artifactId>spring-boot-starter-parent</artifactId>

<version>2.0.2.RELEASE</version>

</parent>

<dependencies>

<dependency>

<groupId>org.springframework.boot</groupId>

<artifactId>spring-boot-starter-web</artifactId>

</dependency>

<!-- <dependency>

<groupId>org.springframework.boot</groupId>

<artifactId>spring-boot-starter-security</artifactId>

</dependency> -->

<dependency>

<groupId>org.springframework.boot</groupId>

<artifactId>spring-boot-starter-data-jpa</artifactId>

</dependency>

<dependency>

<groupId>org.springframework.boot</groupId>

<artifactId>spring-boot-starter-jdbc</artifactId>

</dependency>

<dependency>

<groupId>org.springframework.boot</groupId>

<artifactId>spring-boot-starter-data-redis</artifactId>

</dependency>

<dependency>

<groupId>org.springframework.boot</groupId>

<artifactId>spring-boot-starter-data-mongodb</artifactId>

</dependency>

<dependency>

<groupId>org.springframework.boot</groupId>

<artifactId>spring-boot-starter-amqp</artifactId>

</dependency>

<dependency>

<groupId>org.springframework.boot</groupId>

<artifactId>spring-boot-devtools</artifactId>

</dependency>

<dependency>

<groupId>org.springframework.boot</groupId>

<artifactId>spring-boot-starter-test</artifactId>

<scope>test</scope>

</dependency>

<dependency>

<groupId>org.springframework.data</groupId>

<artifactId>spring-data-mongodb</artifactId>

</dependency>

<dependency>

<groupId>org.springframework.data</groupId>

<artifactId>spring-data-oracle</artifactId>

<version>1.0.0.RELEASE</version>

</dependency>

<dependency>

<groupId>com.oracle</groupId>

<artifactId>ojdbc6</artifactId>

<!-- <version>12.1.0.1</version> -->

<version>11.2.0.3</version>

<scope>system</scope>

<systemPath>${basedir}/lib/ojdbc6.jar</systemPath>

</dependency>

<dependency>

<groupId>mysql</groupId>

<artifactId>mysql-connector-java</artifactId>

</dependency>

<dependency>

<groupId>com.google.code.gson</groupId>

<artifactId>gson</artifactId>

<scope>compile</scope>

</dependency>

<dependency>

<groupId>junit</groupId>

<artifactId>junit</artifactId>

<scope>test</scope>

</dependency>

</dependencies>

<build>

<sourceDirectory>src</sourceDirectory>

<plugins>

<plugin>

<groupId>org.springframework.boot</groupId>

<artifactId>spring-boot-maven-plugin</artifactId>

</plugin>

<plugin>

<artifactId>maven-compiler-plugin</artifactId>

<version>3.3</version>

<configuration>

<source />

<target />

</configuration>

</plugin>

<plugin>

<artifactId>maven-war-plugin</artifactId>

<version>2.6</version>

<configuration>

<warSourceDirectory>WebContent</warSourceDirectory>

<failOnMissingWebXml>false</failOnMissingWebXml>

</configuration>

</plugin>

</plugins>

</build>

</project>

2. application.properties

spring.datasource.driver-class-name=oracle.jdbc.OracleDriver

spring.datasource.url=jdbc:oracle:thin:@//192.168.4.9:1521/orcl.example.com

spring.datasource.username=www

spring.datasource.password=123123

spring.jpa.database-platform=org.hibernate.dialect.Oracle10gDialect

spring.jpa.show-sql=true

#spring.jpa.hibernate.ddl-auto=update

spring.jpa.hibernate.ddl-auto=create-drop

其他配置

spring.datasource.connection-test-query="SELECT 1 FROM DUAL"

spring.datasource.test-while-idle=true

spring.datasource.test-on-borrow=true

Oracle RAC

jdbc:oracle:thin@(DESCRIPTION=

(LOAD_BALANCE=on)

(ADDRESS=(PROTOCOL=TCP)(HOST=racnode1) (PORT=1521))

(ADDRESS=(PROTOCOL=TCP)(HOST=racnode2) (PORT=1521))

(CONNECT_DATA=(SERVICE_NAME=service_name)))

jdbc:oracle:thin:@(DESCRIPTION=

(ADDRESS=(PROTOCOL=TCP)(HOST=125.22.42.68)(PORT=1521))

(LOAD_BALANCE=on)

(FAILOVER=ON)

(CONNECT_DATA=

(SERVER=DEDICATED)

(SERVICE_NAME=service_name)

(FAILOVER_MODE=(TYPE=SESSION)(METHOD=BASIC))

)

)

3. Application

package api;

import org.springframework.boot.SpringApplication;

import org.springframework.boot.autoconfigure.EnableAutoConfiguration;

import org.springframework.boot.autoconfigure.SpringBootApplication;

import org.springframework.boot.context.properties.EnableConfigurationProperties;

import org.springframework.context.annotation.Bean;

import org.springframework.context.annotation.ComponentScan;

import org.springframework.data.jpa.repository.config.EnableJpaRepositories;

import org.springframework.data.mongodb.repository.config.EnableMongoRepositories;

import org.springframework.web.servlet.config.annotation.CorsRegistry;

import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;

import api.ApplicationConfiguration;

@SpringBootApplication

@EnableConfigurationProperties(ApplicationConfiguration.class)

@EnableAutoConfiguration

@ComponentScan({ "api.web", "api.rest", "api.service" })

@EnableMongoRepositories

@EnableJpaRepositories

public class Application {

public @Bean WebMvcConfigurer corsConfigurer() {

return new WebMvcConfigurerAdapter() {

@Override

public void addCorsMappings(CorsRegistry registry) {

registry.addMapping("/**");

}

};

}

public static void main(String[] args) {

SpringApplication.run(Application.class, args);

}

}

4. CrudRepository

package api.repository;

import java.util.List;

import org.springframework.data.domain.Page;

import org.springframework.data.domain.Pageable;

import org.springframework.data.jpa.repository.Query;

import org.springframework.data.repository.CrudRepository;

import org.springframework.data.repository.query.Param;

import org.springframework.stereotype.Repository;

import api.domain.Article;

@Repository

public interface ArticleRepository extends CrudRepository<Article, Long> {

Page<Article> findAll(Pageable pageable);

Article findByTitle(String title);

//@Query("select id,title,content from Article where id > ?1")

//public List<Article> findBySearch(@Param("id")long id);

}

package api.domain;

import java.io.Serializable;

import javax.persistence.Column;

import javax.persistence.Entity;

import javax.persistence.Id;

import javax.persistence.SequenceGenerator;

import javax.persistence.GeneratedValue;

import javax.persistence.GenerationType;

import javax.persistence.Table;

@Entity

@Table(name = "article")

public class Article implements Serializable {

private static final long serialVersionUID = 7998903421265538801L;

@Id

@Column(name = "ID")

@GeneratedValue(strategy=GenerationType.SEQUENCE, generator = "id_Sequence")

@SequenceGenerator(name = "id_Sequence", sequenceName = "ID_SEQ")

private Long id;

private String title;

private String content;

public Article(){

}

public Article(String title, String content) {

this.title = title;

this.content = content;

}

public Long getId() {

return id;

}

public void setId(Long id) {

this.id = id;

}

public String getTitle() {

return title;

}

public void setTitle(String title) {

this.title = title;

}

public String getContent() {

return content;

}

public void setContent(String content) {

this.content = content;

}

@Override

public String toString() {

return "Article [id=" + id + ", title=" + title + ", content=" + content + "]";

}

}

5. JdbcTemplate

@Autowired

private JdbcTemplate jdbcTemplate;

@RequestMapping(value = "/article")

public @ResponseBody String dailyStats(@RequestParam Integer id) {

String query = "SELECT id, title, content from article where id = " + id;

return jdbcTemplate.queryForObject(query, (resultSet, i) -> {

System.out.println(resultSet.getLong(1)+","+ resultSet.getString(2)+","+ resultSet.getString(3));

return (resultSet.getLong(1)+","+ resultSet.getString(2)+","+ resultSet.getString(3));

});

}

6. Controller

package api.web;

import java.util.List;

import org.springframework.beans.factory.annotation.Autowired;

import org.springframework.jdbc.core.JdbcTemplate;

import org.springframework.stereotype.Controller;

import org.springframework.web.bind.annotation.RequestMapping;

import org.springframework.web.bind.annotation.RequestParam;

import org.springframework.web.bind.annotation.ResponseBody;

import api.domain.Article;

import api.repository.ArticleRepository;

@Controller

public class IndexController {

@Autowired

private ArticleRepository articleRepository;

@RequestMapping("/mysql")

@ResponseBody

public String mysql() {

repository.deleteAll();

return "Deleted"

}

@RequestMapping("/mysql")

@ResponseBody

public String mysql() {

articleRepository.save(new Article("Neo", "Chen"));

for (Article article : articleRepository.findAll()) {

System.out.println(article);

}

Article tmp = articleRepository.findByTitle("Neo");

return tmp.getTitle();

}

/*

@RequestMapping("/search")

@ResponseBody

public String search() {

/*for (Article article : articleRepository.findBySearch(1)) {

System.out.println(article);

}*/

List<Article> tmp = articleRepository.findBySearch(1L);

tmp.forEach((temp) -> {

System.out.println(temp.toString());

});

return tmp.get(0).getTitle();

}

*/

@Autowired

private JdbcTemplate jdbcTemplate;

@RequestMapping(value = "/article")

public @ResponseBody String dailyStats(@RequestParam Integer id) {

String query = "SELECT id, title, content from article where id = " + id;

return jdbcTemplate.queryForObject(query, (resultSet, i) -> {

System.out.println(resultSet.getLong(1)+","+ resultSet.getString(2)+","+ resultSet.getString(3));

return (resultSet.getLong(1)+","+ resultSet.getString(2)+","+ resultSet.getString(3));

});

}

}

第 16 章 Spring boot with PostgreSQL

1. pom.xml

<dependency>

<groupId>org.springframework.boot</groupId>

<artifactId>spring-boot-starter-data-jpa</artifactId>

</dependency>

<dependency>

<groupId>org.springframework.boot</groupId>

<artifactId>spring-boot-starter-web</artifactId>

</dependency>

<!-- https://mvnrepository.com/artifact/org.postgresql/postgresql -->

<dependency>

<groupId>org.postgresql</groupId>

<artifactId>postgresql</artifactId>

<version>9.4.1212</version>

</dependency>

2. application.properties

spring.datasource.url=jdbc:postgresql://localhost:5432/your-database

spring.datasource.username=postgres

spring.datasource.password=postgres

spring.jpa.database=POSTGRESQL

spring.jpa.show-sql=true

spring.jpa.hibernate.ddl-auto=create-drop

spring.jpa.generate-ddl=true

3. Application

package cn.netkiller;

import org.springframework.boot.SpringApplication;

import org.springframework.boot.autoconfigure.EnableAutoConfiguration;

import org.springframework.boot.autoconfigure.SpringBootApplication;

import org.springframework.context.annotation.ComponentScan;

import org.springframework.data.jpa.repository.config.EnableJpaRepositories;

import org.springframework.data.mongodb.repository.config.EnableMongoRepositories;

import org.springframework.scheduling.annotation.EnableScheduling;

@SpringBootApplication

@EnableAutoConfiguration

@ComponentScan

@EnableMongoRepositories

@EnableJpaRepositories

@EnableScheduling

public class Application {

public static void main(String[] args) {

SpringApplication.run(Application.class, args);

}

}

4. CrudRepository

Model Class

package cn.netkiller.model;

import java.io.Serializable;

import javax.persistence.Column;

import javax.persistence.Entity;

import javax.persistence.GeneratedValue;

import javax.persistence.GenerationType;

import javax.persistence.Id;

import javax.persistence.Table;

@Entity

@Table(name = "customer")

public class Customer implements Serializable {

private static final long serialVersionUID = -3009077722242246666L;

@Id

@GeneratedValue(strategy = GenerationType.AUTO)

private long id;

@Column(name = "firstname")

private String firstName;

@Column(name = "lastname")

private String lastName;

protected Customer() {

}

public Customer(String firstName, String lastName) {

this.firstName = firstName;

this.lastName = lastName;

}

@Override

public String toString() {

return String.format("Customer[id=%d, firstName='%s', lastName='%s']", id, firstName, lastName);

}

}

CrudRepository

package cn.netkiller.repository;

import java.util.List;

import org.springframework.data.repository.CrudRepository;

import cn.netkiller.model.Customer;

public interface CustomerRepository extends CrudRepository<Customer, Long>{

List<Customer> findByFirstName(String firstName);

List<Customer> findByLastName(String lastName);

}

5. JdbcTemplate

@Autowired

private JdbcTemplate jdbcTemplate;

@RequestMapping(value = "/jdbc")

public @ResponseBody String dailyStats(@RequestParam Integer id) {

String query = "SELECT id, firstname, lastname from customer where id = " + id;

return jdbcTemplate.queryForObject(query, (resultSet, i) -> {

System.out.println(resultSet.getLong(1)+","+ resultSet.getString(2)+","+ resultSet.getString(3));

return (resultSet.getLong(1)+","+ resultSet.getString(2)+","+ resultSet.getString(3));

});

}

6. Controller

package cn.netkiller.web;

import org.springframework.beans.factory.annotation.Autowired;

import org.springframework.jdbc.core.JdbcTemplate;

import org.springframework.stereotype.Controller;

import org.springframework.web.bind.annotation.RequestMapping;

import org.springframework.web.bind.annotation.RequestParam;

import org.springframework.web.bind.annotation.ResponseBody;

import cn.netkiller.model.Customer;

import cn.netkiller.repository.CustomerRepository;

@Controller

@RequestMapping("/test/pgsql")

public class TestPostgreSQLController {

@Autowired

private CustomerRepository customerRepository;

@RequestMapping("/save")

public @ResponseBody String process() {

customerRepository.save(new Customer("Neo", "Chan"));

customerRepository.save(new Customer("Luke", "Liu"));

customerRepository.save(new Customer("Ran", "Guo"));

customerRepository.save(new Customer("Joey", "Chen"));

customerRepository.save(new Customer("Larry", "Huang"));

return "Done";

}

@RequestMapping("/findall")

public @ResponseBody String findAll() {

String result = "<html>";

for (Customer cust : customerRepository.findAll()) {

result += "<div>" + cust.toString() + "</div>";

}

return result + "</html>";

}

@RequestMapping("/findbyid")

public @ResponseBody String findById(@RequestParam("id") long id) {

String result = "";

result = customerRepository.findOne(id).toString();

return result;

}

@RequestMapping("/findbylastname")

public @ResponseBody String fetchDataByLastName(@RequestParam("lastname") String lastName) {

String result = "<html>";

for (Customer cust : customerRepository.findByLastName(lastName)) {

result += "<div>" + cust.toString() + "</div>";

}

return result + "</html>";

}

@Autowired

private JdbcTemplate jdbcTemplate;

@RequestMapping(value = "/jdbc")

public @ResponseBody String dailyStats(@RequestParam Integer id) {

String query = "SELECT id, firstname, lastname from customer where id = " + id;

return jdbcTemplate.queryForObject(query, (resultSet, i) -> {

System.out.println(resultSet.getLong(1)+","+ resultSet.getString(2)+","+ resultSet.getString(3));

return (resultSet.getLong(1)+","+ resultSet.getString(2)+","+ resultSet.getString(3));

});

}

}

7. Test

curl http://127.0.0.1:7000/test/pgsql/save

curl http://127.0.0.1:7000/test/pgsql/findall

curl http://127.0.0.1:7000/test/pgsql/findbyid?id=1

curl http://127.0.0.1:7000/test/pgsql/jdbc?id=1

第 17 章 Spring boot with Elasticsearch

1. Maven

<?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/ma ... gt%3B

<modelVersion>4.0.0</modelVersion>

<groupId>cn.netkiller.springboot</groupId>

<artifactId>elasticsearch</artifactId>

<version>0.0.1-SNAPSHOT</version>

<name>elasticsearch</name>

<parent>

<groupId>org.springframework.boot</groupId>

<artifactId>spring-boot-starter-parent</artifactId>

<version>2.3.1.RELEASE</version>

</parent>

<dependencies>

<dependency>

<groupId>org.springframework.boot</groupId>

<artifactId>spring-boot-starter-data-elasticsearch</artifactId>

</dependency>

<dependency>

<groupId>org.springframework.boot</groupId>

<artifactId>spring-boot-starter-web</artifactId>

</dependency>

<dependency>

<groupId>junit</groupId>

<artifactId>junit</artifactId>

<version>4.12</version>

</dependency>

</dependencies>

</project>

2. Application

package cn.netkiller;

import org.springframework.boot.SpringApplication;

import org.springframework.boot.autoconfigure.EnableAutoConfiguration;

import org.springframework.boot.autoconfigure.SpringBootApplication;

import org.springframework.context.annotation.ComponentScan;

import org.springframework.data.jpa.repository.config.EnableJpaRepositories;

import org.springframework.data.mongodb.repository.config.EnableMongoRepositories;

import org.springframework.scheduling.annotation.EnableScheduling;

@SpringBootApplication

@EnableAutoConfiguration

@ComponentScan

@EnableMongoRepositories

@EnableJpaRepositories

@EnableScheduling

public class Application {

public static void main(String[] args) {

SpringApplication.run(Application.class, args);

}

}

3. application.properties

spring.data.elasticsearch.repositories.enabled=true

spring.data.elasticsearch.cluster-nodes=127.0.0.1:9300

4. Domain

@Document(indexName = "province", type = "city")

public class City implements Serializable {

private static final long serialVersionUID = -1L;

private Long id;

private String name;

private String description;

public Long getId() {

return id;

}

public void setId(Long id) {

this.id = id;

}

public String getName() {

return name;

}

public void setName(String name) {

this.name = name;

}

public String getDescription() {

return description;

}

public void setDescription(String description) {

this.description = description;

}

}

5. ElasticsearchRepository

public interface CityRepository extends ElasticsearchRepository<City, Long> {

List<City> findByNameLike(String name);

Page<City> findByDescription(String description, Pageable page);

Page<City> findByDescriptionNot(String description, Pageable page);

Page<City> findByDescriptionLike(String description, Pageable page);

}

第 18 章 Spring boot with Elasticsearch TransportClient

Spring data 目前还不支持 Elasticsearch 5.5.x 所以需要通过注入 TransportClient 这就意味着使用 5.5.x 版本你无法使用 ElasticsearchRepository 这种特性，只能通过官方的 TransportClient 操作 Elasticsearch。

1. Maven

Elasticsearch 依赖下来四个包

<dependency>

<groupId>org.elasticsearch</groupId>

<artifactId>elasticsearch</artifactId>

<version>5.5.1</version>

</dependency>

<dependency>

<groupId>org.elasticsearch.client</groupId>

<artifactId>transport</artifactId>

<version>5.5.1</version>

</dependency>

<dependency>

<groupId>org.apache.logging.log4j</groupId>

<artifactId>log4j-api</artifactId>

<version>2.8.2</version>

</dependency>

<dependency>

<groupId>org.apache.logging.log4j</groupId>

<artifactId>log4j-core</artifactId>

<version>2.8.2</version>

</dependency>

下面是我的完整例子

<?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>

<groupId>com.example</groupId>

<artifactId>api</artifactId>

<version>0.0.1-SNAPSHOT</version>

<packaging>jar</packaging>

<name>api</name>

<description>Demo project for Spring Boot</description>

<parent>

<groupId>org.springframework.boot</groupId>

<artifactId>spring-boot-starter-parent</artifactId>

<version>2.3.1.RELEASE</version>

<relativePath /> <!-- lookup parent from repository -->

</parent>

<properties>

<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>

<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>

<java.version>1.8</java.version>

</properties>

<dependencies>

<dependency>

<groupId>org.springframework.boot</groupId>

<artifactId>spring-boot-starter-data-jpa</artifactId>

</dependency>

<dependency>

<groupId>org.springframework.boot</groupId>

<artifactId>spring-boot-starter-jdbc</artifactId>

</dependency>

<dependency>

<groupId>org.springframework.boot</groupId>

<artifactId>spring-boot-starter-security</artifactId>

</dependency>

<dependency>

<groupId>org.springframework.boot</groupId>

<artifactId>spring-boot-starter-web</artifactId>

</dependency>

<!-- <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId>

</dependency> -->

<dependency>

<groupId>mysql</groupId>

<artifactId>mysql-connector-java</artifactId>

<scope>runtime</scope>

</dependency>

<dependency>

<groupId>org.springframework.boot</groupId>

<artifactId>spring-boot-starter-test</artifactId>

<scope>test</scope>

</dependency>

<dependency>

<groupId>org.springframework.boot</groupId>

<artifactId>spring-boot-starter-data-redis</artifactId>

</dependency>

<!-- https://mvnrepository.com/artifact/javax.persistence/persistence-api -->

<dependency>

<groupId>javax.persistence</groupId>

<artifactId>persistence-api</artifactId>

<version>1.0.2</version>

</dependency>

<!-- https://mvnrepository.com/artifact/org.json/json -->

<dependency>

<groupId>org.json</groupId>

<artifactId>json</artifactId>

</dependency>

<dependency>

<groupId>org.elasticsearch</groupId>

<artifactId>elasticsearch</artifactId>

<version>5.5.1</version>

</dependency>

<dependency>

<groupId>org.elasticsearch.client</groupId>

<artifactId>transport</artifactId>

<version>5.5.1</version>

</dependency>

<dependency>

<groupId>org.apache.logging.log4j</groupId>

<artifactId>log4j-api</artifactId>

<version>2.8.2</version>

</dependency>

<dependency>

<groupId>org.apache.logging.log4j</groupId>

<artifactId>log4j-core</artifactId>

<version>2.8.2</version>

</dependency>

</dependencies>

<build>

<plugins>

<plugin>

<groupId>org.springframework.boot</groupId>

<artifactId>spring-boot-maven-plugin</artifactId>

</plugin>

<plugin>

<groupId>org.apache.maven.plugins</groupId>

<artifactId>maven-surefire-plugin</artifactId>

<configuration>

<skip>true</skip>

</configuration>

</plugin>

</plugins>

</build>

</project>

2. Application

package cn.netkiller;

import org.springframework.boot.SpringApplication;

import org.springframework.boot.autoconfigure.EnableAutoConfiguration;

import org.springframework.boot.autoconfigure.SpringBootApplication;

import org.springframework.context.annotation.ComponentScan;

import org.springframework.data.jpa.repository.config.EnableJpaRepositories;

import org.springframework.data.mongodb.repository.config.EnableMongoRepositories;

import org.springframework.scheduling.annotation.EnableScheduling;

@SpringBootApplication

@EnableAutoConfiguration

@ComponentScan

@EnableMongoRepositories

@EnableJpaRepositories

@EnableScheduling

public class Application {

public static void main(String[] args) {

SpringApplication.run(Application.class, args);

}

}

3. application.properties

注意：Elasticsearch 连接地址是 9300， 而不是 9200

spring.data.elasticsearch.cluster-nodes=172.0.0.1:9300

spring.data.elasticsearch.local=false

spring.data.elasticsearch.properties.transport.tcp.connect_timeout=60s

4. ElasticsearchConfiguration

package com.example.api.config;

import java.net.InetAddress;

import java.net.UnknownHostException;

import org.elasticsearch.client.transport.TransportClient;

import org.elasticsearch.common.transport.InetSocketTransportAddress;

import org.elasticsearch.transport.client.PreBuiltTransportClient;

import org.elasticsearch.common.settings.Settings;

import org.slf4j.Logger;

import org.slf4j.LoggerFactory;

import org.springframework.beans.factory.DisposableBean;

import org.springframework.beans.factory.FactoryBean;

import org.springframework.beans.factory.InitializingBean;

import org.springframework.beans.factory.annotation.Value;

import org.springframework.context.annotation.Configuration;

@Configuration

public class ElasticsearchConfiguration implements FactoryBean<TransportClient>, InitializingBean, DisposableBean {

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

@Value("${spring.data.elasticsearch.cluster-nodes}")

private String clusterNodes;

private TransportClient transportClient;

private PreBuiltTransportClient preBuiltTransportClient;

@Override

public void destroy() throws Exception {

try {

logger.info("Closing elasticSearch client");

if (transportClient != null) {

transportClient.close();

}

} catch (final Exception e) {

logger.error("Error closing ElasticSearch client: ", e);

}

}

@Override

public TransportClient getObject() throws Exception {

return transportClient;

}

@Override

public Class<TransportClient> getObjectType() {

return TransportClient.class;

}

@Override

public boolean isSingleton() {

return false;

}

@Override

public void afterPropertiesSet() throws Exception {

buildClient();

}

protected void buildClient() {

try {

preBuiltTransportClient = new PreBuiltTransportClient(settings());

String InetSocket[] = clusterNodes.split(":");

String address = InetSocket[0];

Integer port = Integer.valueOf(InetSocket[1]);

transportClient = preBuiltTransportClient.addTransportAddress(new InetSocketTransportAddress(InetAddress.getByName(address), port));

} catch (UnknownHostException e) {

logger.error(e.getMessage());

}

}

/**

* 初始化默认的client

*/

private Settings settings() {

// Settings settings = Settings.builder().put("cluster.name", clusterName).put("client.transport.sniff", true).build();

Settings settings = Settings.builder().put("cluster.name", "elasticsearch").build();

return settings;

}

}

5. RestController

package com.example.api.restful;

import org.elasticsearch.action.get.GetResponse;

import org.elasticsearch.client.transport.TransportClient;

import org.springframework.beans.factory.annotation.Autowired;

import org.springframework.web.bind.annotation.PathVariable;

import org.springframework.web.bind.annotation.RequestMapping;

import org.springframework.web.bind.annotation.RestController;

@RestController

@RequestMapping("/restful/search")

public class SearchRestController {

@Autowired

private TransportClient client;

@RequestMapping(value = "/client/{articleId}")

public GetResponse test(@PathVariable String articleId) {

GetResponse response = client.prepareGet("information", "article", articleId).get();

return response;

}

}

使用 Curl 测试 restful 接口

MacBook-Pro:~ neo$ curl -k https://test:test@localhost:8443/restful/search/client/1093.json

{"fields":{},"id":"1093","type":"article","source":{"@timestamp":"2017-07-31T05:41:00.248Z","author":"test","@version":"1","description":"test","ctime":"2017-07-31T05:40:35.000Z","id":1093,"source":"test","title":"test11111","content":"<p>test</p><p>aaaaaaaaaaaaaa</p>"},"version":3,"index":"information","sourceAsBytes":"eyJAdGltZXN0YW1wIjoiMjAxNy0wNy0zMVQwNTo0MTowMC4yNDhaIiwiYXV0aG9yIjoidGVzdCIsIkB2ZXJzaW9uIjoiMSIsImRlc2NyaXB0aW9uIjoidGVzdCIsImN0aW1lIjoiMjAxNy0wNy0zMVQwNTo0MDozNS4wMDBaIiwiaWQiOjEwOTMsInNvdXJjZSI6InRlc3QiLCJ0aXRsZSI6InRlc3QxMTExMSIsImNvbnRlbnQiOiI8cD50ZXN0PC9wPjxwPmFhYWFhYWFhYWFhYWFhPC9wPiJ9","sourceInternal":{"childResources":[]},"sourceAsString":"{\"@timestamp\":\"2017-07-31T05:41:00.248Z\",\"author\":\"test\",\"@version\":\"1\",\"description\":\"test\",\"ctime\":\"2017-07-31T05:40:35.000Z\",\"id\":1093,\"source\":\"test\",\"title\":\"test11111\",\"content\":\"<p>test</p><p>aaaaaaaaaaaaaa</p>\"}","sourceEmpty":false,"sourceAsMap":{"@timestamp":"2017-07-31T05:41:00.248Z","author":"test","@version":"1","description":"test","ctime":"2017-07-31T05:40:35.000Z","id":1093,"source":"test","title":"test11111","content":"<p>test</p><p>aaaaaaaaaaaaaa</p>"},"exists":true,"sourceAsBytesRef":{"childResources":[]},"fragment":false}

第 19 章 Spring boot with Apache Hive

1. Maven

<dependency>

<groupId>org.springframework.boot</groupId>

<artifactId>spring-boot-starter-jdbc</artifactId>

</dependency>

<dependency>

<groupId>org.springframework.data</groupId>

<artifactId>spring-data-hadoop</artifactId>

<version>2.5.0.RELEASE</version>

</dependency>

<!-- https://mvnrepository.com/artifact/org.apache.hive/hive-jdbc -->

<dependency>

<groupId>org.apache.hive</groupId>

<artifactId>hive-jdbc</artifactId>

<version>2.3.0</version>

<exclusions>

<exclusion>

<groupId>org.eclipse.jetty.aggregate</groupId>

<artifactId>*</artifactId>

</exclusion>

</exclusions>

</dependency>

<dependency>

<groupId>org.apache.tomcat</groupId>

<artifactId>tomcat-jdbc</artifactId>

<version>8.5.20</version>

</dependency>

2. application.properties

hive 数据源配置项

hive.url=jdbc:hive2://172.16.0.10:10000/default

hive.driver-class-name=org.apache.hive.jdbc.HiveDriver

hive.username=hadoop

hive.password=

用户名是需要具有 hdfs 写入权限，密码可以不用写

如果使用 yaml 格式 application.yml 配置如下

hive:

url: jdbc:hive2://172.16.0.10:10000/default

driver-class-name: org.apache.hive.jdbc.HiveDriver

type: com.alibaba.druid.pool.DruidDataSource

username: hive

password: hive

3. Configuration

package cn.netkiller.config;

import org.apache.tomcat.jdbc.pool.DataSource;

import org.slf4j.Logger;

import org.slf4j.LoggerFactory;

import org.springframework.beans.factory.annotation.Autowired;

import org.springframework.beans.factory.annotation.Qualifier;

import org.springframework.context.annotation.Bean;

import org.springframework.context.annotation.Configuration;

import org.springframework.core.env.Environment;

import org.springframework.jdbc.core.JdbcTemplate;

@Configuration

public class HiveConfig {

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

@Autowired

private Environment env;

@Bean(name = "hiveJdbcDataSource")

@Qualifier("hiveJdbcDataSource")

public DataSource dataSource() {

DataSource dataSource = new DataSource();

dataSource.setUrl(env.getProperty("hive.url"));

dataSource.setDriverClassName(env.getProperty("hive.driver-class-name"));

dataSource.setUsername(env.getProperty("hive.username"));

dataSource.setPassword(env.getProperty("hive.password"));

logger.debug("Hive DataSource");

return dataSource;

}

@Bean(name = "hiveJdbcTemplate")

public JdbcTemplate hiveJdbcTemplate(@Qualifier("hiveJdbcDataSource") DataSource dataSource) {

return new JdbcTemplate(dataSource);

}

}

你也可以使用 DruidDataSource

package cn.netkiller.api.config;

@Configuration

public class HiveDataSource {

@Autowired

private Environment env;

@Bean(name = "hiveJdbcDataSource")

@Qualifier("hiveJdbcDataSource")

public DataSource dataSource() {

DruidDataSource dataSource = new DruidDataSource();

dataSource.setUrl(env.getProperty("hive.url"));

dataSource.setDriverClassName(env.getProperty("hive.driver-class-name"));

dataSource.setUsername(env.getProperty("hive.username"));

dataSource.setPassword(env.getProperty("hive.password"));

return dataSource;

}

@Bean(name = "hiveJdbcTemplate")

public JdbcTemplate hiveJdbcTemplate(@Qualifier("hiveJdbcDataSource") DataSource dataSource) {

return new JdbcTemplate(dataSource);

}

}

4. CURD 操作实例

Hive 数据库的增删插改操作与其他数据库没有什么不同。

package cn.netkiller.web;

import java.util.Iterator;

import java.util.List;

import java.util.Map;

import org.slf4j.Logger;

import org.slf4j.LoggerFactory;

import org.springframework.beans.factory.annotation.Autowired;

import org.springframework.beans.factory.annotation.Qualifier;

import org.springframework.jdbc.core.JdbcTemplate;

import org.springframework.stereotype.Controller;

import org.springframework.web.bind.annotation.RequestMapping;

import org.springframework.web.servlet.ModelAndView;

@Controller

@RequestMapping("/hive")

public class HiveController {

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

@Autowired

@Qualifier("hiveJdbcTemplate")

private JdbcTemplate hiveJdbcTemplate;

@RequestMapping("/create")

public ModelAndView create() {

StringBuffer sql = new StringBuffer("create table IF NOT EXISTS ");

sql.append("HIVE_TEST");

sql.append("(KEY INT, VALUE STRING)");

sql.append("PARTITIONED BY (CTIME DATE)"); // 分区存储

sql.append("ROW FORMAT DELIMITED FIELDS TERMINATED BY '\t' LINES TERMINATED BY '\n' "); // 定义分隔符

sql.append("STORED AS TEXTFILE"); // 作为文本存储

logger.info(sql.toString());

hiveJdbcTemplate.execute(sql.toString());

return new ModelAndView("index");

}

@RequestMapping("/insert")

public String insert() {

hiveJdbcTemplate.execute("insert into hive_test(key, value) values('Neo','Chen')");

return "Done";

}

@RequestMapping("/select")

public String select() {

String sql = "select * from HIVE_TEST";

List<Map<String, Object>> rows = hiveJdbcTemplate.queryForList(sql);

Iterator<Map<String, Object>> it = rows.iterator();

while (it.hasNext()) {

Map<String, Object> row = it.next();

System.out.println(String.format("%s\t%s", row.get("key"), row.get("value")));

}

return "Done";

}

@RequestMapping("/delete")

public String delete() {

StringBuffer sql = new StringBuffer("DROP TABLE IF EXISTS ");

sql.append("HIVE_TEST");

logger.info(sql.toString());

hiveJdbcTemplate.execute(sql.toString());

return "Done";

}

}

第 20 章 Spring boot with Phoenix

1. Maven

<properties>

<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>

</properties>

<dependencies>

<dependency>

<groupId>org.springframework.boot</groupId>

<artifactId>spring-boot-starter-jdbc</artifactId>

</dependency>

<dependency>

<groupId>org.springframework.data</groupId>

<artifactId>spring-data-hadoop</artifactId>

<version>2.4.0.RELEASE</version>

</dependency>

<!-- https://mvnrepository.com/artifact/org.apache.phoenix/phoenix-queryserver-client -->

<dependency>

<groupId>org.apache.phoenix</groupId>

<artifactId>phoenix-queryserver-client</artifactId>

<version>4.12.0-HBase-1.3</version>

</dependency>

<dependency>

<groupId>com.alibaba</groupId>

<artifactId>druid</artifactId>

<version>1.1.0</version>

</dependency>

</dependencies>

2. application.properties

phoenix 数据源配置项

phoenix:

enable: true

url: jdbc:phoenix:172.16.0.20

type: com.alibaba.druid.pool.DruidDataSource

driver-class-name: org.apache.phoenix.jdbc.PhoenixDriver

username: //phoenix的用户名默认为空

password: //phoenix的密码默认为空

default-auto-commit: true

3. Configuration

package cn.netkiller.api.config;

@Configuration

public class PhoenixDataSource {

@Autowired

private Environment env;

@Bean(name = "phoenixJdbcDataSource")

@Qualifier("phoenixJdbcDataSource")

public DataSource dataSource() {

DruidDataSource dataSource = new DruidDataSource();

dataSource.setUrl(env.getProperty("phoenix.url"));

dataSource.setDriverClassName(env.getProperty("phoenix.driver-class-name"));

dataSource.setUsername(env.getProperty("phoenix.username"));

dataSource.setPassword(env.getProperty("phoenix.password"));

dataSource.setDefaultAutoCommit(Boolean.valueOf(env.getProperty("phoenix.default-auto-commit")));

return dataSource;

}

@Bean(name = "phoenixJdbcTemplate")

public JdbcTemplate phoenixJdbcTemplate(@Qualifier("phoenixJdbcDataSource") DataSource dataSource) {

return new JdbcTemplate(dataSource);

}

}

第 21 章 Spring boot with Datasource

数据源配置

1. Master / Slave 主从数据库数据源配置

application.properties

spring.datasource.master.driverClassName = com.mysql.cj.jdbc.Driver

spring.datasource.master.url=jdbc:mysql://192.168.1.240:3306/test?useUnicode=true&characterEncoding=UTF-8&serverTimezone=UTC&useSSL=false

spring.datasource.master.username = root

spring.datasource.master.password = password

spring.datasource.slave.driverClassName = com.mysql.cj.jdbc.Driver

spring.datasource.slave.url=jdbc:mysql://192.168.1.250:3306/test?useUnicode=true&characterEncoding=UTF-8&serverTimezone=UTC&useSSL=false

spring.datasource.slave.username = root

spring.datasource.slave.password = password

spring.jpa.database-platform=org.hibernate.dialect.MySQL5Dialect

配置主从数据源

package cn.netkiller.config;

import javax.sql.DataSource;

import org.springframework.beans.factory.annotation.Qualifier;

import org.springframework.boot.autoconfigure.jdbc.DataSourceProperties;

import org.springframework.boot.context.properties.ConfigurationProperties;

import org.springframework.context.annotation.Bean;

import org.springframework.context.annotation.Configuration;

import org.springframework.context.annotation.Primary;

import org.springframework.jdbc.core.JdbcTemplate;

@Configuration

public class MultiDataSourceConfig {

@Bean

@Primary

@ConfigurationProperties("spring.datasource.master")

public DataSourceProperties masterDataSourceProperties() {

return new DataSourceProperties();

}

@Bean("Master")

@Primary

@ConfigurationProperties("spring.datasource.master")

public DataSource masterDataSource() {

return masterDataSourceProperties().initializeDataSourceBuilder().build();

}

@Bean("masterJdbcTemplate")

@Primary

public JdbcTemplate masterJdbcTemplate(@Qualifier("Master") DataSource Master) {

return new JdbcTemplate(Master);

}

@Bean

@ConfigurationProperties("spring.datasource.slave")

public DataSourceProperties slaveDataSourceProperties() {

return new DataSourceProperties();

}

@Bean(name = "Slave")

@ConfigurationProperties("spring.datasource.slave")

public DataSource slaveDataSource() {

return slaveDataSourceProperties().initializeDataSourceBuilder().build();

}

@Bean("slaveJdbcTemplate")

public JdbcTemplate slaveJdbcTemplate(@Qualifier("Slave") DataSource Master) {

return new JdbcTemplate(Master);

}

}

选择数据源

// 默认是 Master

@Autowired

private JdbcTemplate jdbcTemplate;

// 或者这样写

@Qualifier("masterJdbcTemplate")

@Autowired

private JdbcTemplate masterJdbcTemplate;

// 下面是 Slave 数据源

@Qualifier("slaveJdbcTemplate")

@Autowired

private JdbcTemplate slaveJdbcTemplate;

2. 多数据源配置

package cn.netkiller.project.config;

import javax.sql.DataSource;

import org.springframework.beans.factory.annotation.Qualifier;

import org.springframework.boot.context.properties.ConfigurationProperties;

import org.springframework.boot.jdbc.DataSourceBuilder;

import org.springframework.context.annotation.Bean;

import org.springframework.context.annotation.Configuration;

import org.springframework.context.annotation.Primary;

import org.springframework.jdbc.core.JdbcTemplate;

import org.springframework.jdbc.datasource.DataSourceTransactionManager;

import org.springframework.transaction.PlatformTransactionManager;

@Configuration

public class DataSourceConfig {

@Bean

@Primary

@ConfigurationProperties("spring.datasource")

public DataSourceProperties defaultDataSourceProperties() {

return new DataSourceProperties();

}

@Bean

@Primary

@ConfigurationProperties("spring.datasource")

public DataSource defaultDataSource() {

return defaultDataSourceProperties().initializeDataSourceBuilder().build();

}

@Bean("JdbcTemplate")

@Primary

public JdbcTemplate defaultJdbcTemplate(@Qualifier("defaultDataSource") DataSource Master) {

return new JdbcTemplate(Master);

}

@Bean

// @Primary

@ConfigurationProperties("spring.datasource.master")

public DataSourceProperties masterDataSourceProperties() {

return new DataSourceProperties();

}

@Bean("Master")

// @Primary

@ConfigurationProperties("spring.datasource.master")

public DataSource masterDataSource() {

return masterDataSourceProperties().initializeDataSourceBuilder().build();

}

@Bean("masterJdbcTemplate")

// @Primary

public JdbcTemplate masterJdbcTemplate(@Qualifier("Master") DataSource Master) {

return new JdbcTemplate(Master);

}

@Bean

@ConfigurationProperties("spring.datasource.slave")

public DataSourceProperties slaveDataSourceProperties() {

return new DataSourceProperties();

}

@Bean(name = "Slave")

@ConfigurationProperties("spring.datasource.slave")

public DataSource slaveDataSource() {

return slaveDataSourceProperties().initializeDataSourceBuilder().build();

}

@Bean("slaveJdbcTemplate")

public JdbcTemplate slaveJdbcTemplate(@Qualifier("Slave") DataSource Master) {

return new JdbcTemplate(Master);

}

@Bean(name = "wwwDataSource")

@Qualifier("wwwDataSource")

@ConfigurationProperties(prefix = "spring.datasource.www")

public DataSource wwwDataSource() {

return DataSourceBuilder.create().build();

}

@Bean(name = "apiDataSource")

@Qualifier("apiDataSource")

@ConfigurationProperties(prefix = "spring.datasource.api")

public DataSource apiDataSource() {

return DataSourceBuilder.create().build();

}

@Bean(name = "cmsDataSource")

@Qualifier("cmsDataSource")

//@Primary

@ConfigurationProperties(prefix = "spring.datasource.cms")

public DataSource cmsDataSource() {

return DataSourceBuilder.create().build();

}

@Bean

PlatformTransactionManager transactionManager() {

return new DataSourceTransactionManager(apiDataSource());

}

@Bean(name = "wwwJdbcTemplate")

public JdbcTemplate wwwJdbcTemplate(@Qualifier("wwwDataSource") DataSource dataSource) {

return new JdbcTemplate(dataSource);

}

@Bean(name = "appJdbcTemplate")

public JdbcTemplate appJdbcTemplate(@Qualifier("apiDataSource") DataSource dataSource) {

return new JdbcTemplate(dataSource);

}

@Bean(name = "cmsJdbcTemplate")

public JdbcTemplate cmsJdbcTemplate(@Qualifier("cmsDataSource") DataSource dataSource) {

return new JdbcTemplate(dataSource);

}

}

对应 application.properties 的配置方法

spring.datasource.www.driver-class-name=com.mysql.jdbc.Driver

spring.datasource.www.url=jdbc:mysql://localhost:3306/www?useUnicode=true&characterEncoding=UTF-8&serverTimezone=UTC&useSSL=false

spring.datasource.www.username=www

spring.datasource.www.password=passw0rd

spring.datasource.www.max-idle=10

spring.datasource.www.max-wait=10000

spring.datasource.www.min-idle=5

spring.datasource.www.initial-size=5

spring.datasource.www.validation-query=SELECT 1

spring.datasource.www.test-on-borrow=false

spring.datasource.www.test-while-idle=true

spring.datasource.www.time-between-eviction-runs-millis=18800

spring.datasource.www.jdbc-interceptors=ConnectionState;SlowQueryReport(threshold=0)

spring.datasource.api.driver-class-name=com.mysql.jdbc.Driver

spring.datasource.api.url=jdbc:mysql://localhost:3306/api?useUnicode=true&characterEncoding=UTF-8&serverTimezone=UTC&useSSL=false

spring.datasource.api.username=api

spring.datasource.api.password=passw0rd

spring.datasource.api.max-idle=10

spring.datasource.api.max-wait=10000

spring.datasource.api.min-idle=5

spring.datasource.api.initial-size=5

spring.datasource.api.validation-query=SELECT 1

spring.datasource.api.test-on-borrow=false

spring.datasource.api.test-while-idle=true

spring.datasource.api.time-between-eviction-runs-millis=18800

spring.datasource.api.jdbc-interceptors=ConnectionState;SlowQueryReport(threshold=0)

选择数据库

@Autowired

@Qualifier("apiJdbcTemplate")

JdbcTemplate jdbcTempalte;

3. JPA 多数据源

多个 JPA 数据配置非常简单，请参考下面的例子。注意两点，第一点是设置Repository的位置：

@EnableJpaRepositories(

entityManagerFactoryRef="entityManagerFactoryPrimary",

transactionManagerRef="transactionManagerPrimary",

basePackages= { "cn.netkiller.repository.primary" }) //设置Repository所在位置

第二点是设置 Domain 位置，与 Repository 成套出现

public LocalContainerEntityManagerFactoryBean entityManagerFactoryPrimary (EntityManagerFactoryBuilder builder) {

return builder

.dataSource(primaryDataSource)

.properties(getVendorProperties(primaryDataSource))

.packages("cn.netkiller.domain.primary") //设置实体类所在包

.persistenceUnit("primaryPersistenceUnit")

.build();

}

首先配置第一组数据源。

package cn.netkiller.project.config;

@Configuration

@EnableTransactionManagement

@EnableJpaRepositories(

entityManagerFactoryRef="entityManagerFactoryPrimary",

transactionManagerRef="transactionManagerPrimary",

basePackages= { "cn.netkiller.repository.primary" }) //设置Repository所在位置

public class PrimaryConfig {

@Autowired @Qualifier("primaryDataSource")

private DataSource primaryDataSource;

@Primary

@Bean(name = "entityManagerPrimary")

public EntityManager entityManager(EntityManagerFactoryBuilder builder) {

return entityManagerFactoryPrimary(builder).getObject().createEntityManager();

}

@Primary

@Bean(name = "entityManagerFactoryPrimary")

public LocalContainerEntityManagerFactoryBean entityManagerFactoryPrimary (EntityManagerFactoryBuilder builder) {

return builder

.dataSource(primaryDataSource)

.properties(getVendorProperties(primaryDataSource))

.packages("cn.netkiller.domain.primary") //设置实体类所在包

.persistenceUnit("primaryPersistenceUnit")

.build();

}

@Autowired

private JpaProperties jpaProperties;

private Map<String, String> getVendorProperties(DataSource dataSource) {

return jpaProperties.getHibernateProperties(dataSource);

}

@Primary

@Bean(name = "transactionManagerPrimary")

public PlatformTransactionManager transactionManagerPrimary(EntityManagerFactoryBuilder builder) {

return new JpaTransactionManager(entityManagerFactoryPrimary(builder).getObject());

}

}

设置第二组数据源

package cn.netkiller.project.config;

@Configuration

@EnableTransactionManagement

@EnableJpaRepositories(

entityManagerFactoryRef="entityManagerFactorySecondary",

transactionManagerRef="transactionManagerSecondary",

basePackages= { "cn.netkiller.repository.secondary" }) //设置Repository所在位置

public class SecondaryConfig {

@Autowired @Qualifier("secondaryDataSource")

private DataSource secondaryDataSource;

@Bean(name = "entityManagerSecondary")

public EntityManager entityManager(EntityManagerFactoryBuilder builder) {

return entityManagerFactorySecondary(builder).getObject().createEntityManager();

}

@Bean(name = "entityManagerFactorySecondary")

public LocalContainerEntityManagerFactoryBean entityManagerFactorySecondary (EntityManagerFactoryBuilder builder) {

return builder

.dataSource(secondaryDataSource)

.properties(getVendorProperties(secondaryDataSource))

.packages("cn.netkiller.repository.domain.secondary") //设置Domain实体类所在位置

.persistenceUnit("secondaryPersistenceUnit")

.build();

}

@Autowired

private JpaProperties jpaProperties;

private Map<String, String> getVendorProperties(DataSource dataSource) {

return jpaProperties.getHibernateProperties(dataSource);

}

@Bean(name = "transactionManagerSecondary")

PlatformTransactionManager transactionManagerSecondary(EntityManagerFactoryBuilder builder) {

return new JpaTransactionManager(entityManagerFactorySecondary(builder).getObject());

}

}

第 22 章 连接池配置

Connection and Statement Pooling

注意：下面的实例仅限 Spring boot 2.0.2.RELEASE

1. org.apache.tomcat.jdbc.pool.DataSource

默认连接池，可以忽略配置

spring.datasource.type = org.apache.tomcat.jdbc.pool.DataSource

2. druid

pom.xml

<dependency>

<groupId>com.alibaba</groupId>

<artifactId>druid</artifactId>

<version>1.2.6</version>

</dependency>

application.properties

spring.jpa.database=MYSQL

spring.datasource.type=com.alibaba.druid.pool.DruidDataSource

#驱动配置信息

spring.datasource.driver-class-name=com.mysql.jdbc.Driver

#基本连接信息

spring.datasource.username = root

spring.datasource.password = root

spring.datasource.url=jdbc:mysql://192.168.153.23:3306/mytest?useUnicode=true&characterEncoding=utf-8

#连接池属性

spring.datasource.druid.initial-size=15

spring.datasource.druid.max-active=100

spring.datasource.druid.min-idle=15

spring.datasource.druid.max-wait=60000

spring.datasource.druid.time-between-eviction-runs-millis=60000

spring.datasource.druid.min-evictable-idle-time-millis=300000

spring.datasource.druid.test-on-borrow=false

spring.datasource.druid.test-on-return=false

spring.datasource.druid.test-while-idle=true

spring.datasource.druid.validation-query=SELECT 1

spring.datasource.druid.validation-query-timeout=1000

spring.datasource.druid.keep-alive=true

spring.datasource.druid.remove-abandoned=true

spring.datasource.druid.remove-abandoned-timeout=180

spring.datasource.druid.log-abandoned=true

spring.datasource.druid.pool-prepared-statements=true

spring.datasource.druid.max-pool-prepared-statement-per-connection-size=20

spring.datasource.druid.filters=stat,wall,slf4j

spring.datasource.druid.use-global-data-source-stat=true

spring.datasource.druid.preparedStatement=true

spring.datasource.druid.maxOpenPreparedStatements=100

spring.datasource.druid.connect-properties.mergeSql=true

spring.datasource.druid.connect-properties.slowSqlMillis=5000

加密数据库密码

创建私钥、公钥和密码

neo@MacBook-Pro-Neo ~ % java -cp ~/.m2/repository/com/alibaba/druid/1.2.6/druid-1.2.6.jar com.alibaba.druid.filter.config.ConfigTools you_password

privateKey:MIIBVAIBADANBgkqhkiG9w0BAQEFAASCAT4wggE6AgEAAkEA0dcAPh18Jqob83AdY9SeO1mP1UOvivoDenH7nHOXk2ti5B4Q5A/6MYLnbLANHLTFACJe7+4ZaFK0qbzixdIqkQIDAQABAkAA9YbRjIczcootlPTkw/VOr7hmc5h0bfOGM7SVk2+Ci8RFHtzQw9MGHvOCX3NHYA6fqmT4oer/z+GljuF4YeqZAiEA/N6Jvw1Wxq8EC+4EpRsjCg1eYbOV2kYYBKip0lfDzVkCIQDUcBURmKSDpLPOE+jq4SBZXV3HTJs5IfmgtTzGWZIH+QIhAOVTQOMGSttXH7ld+9JsgON96kl6330bsm6PM6vyMj3JAiAUvgjgmfXeQLOpuHnyjR66ewpQDmPNlUqpbWjMuSwwCQIgSEXSKgW+Fd2aIjB6TrXjUTRsJy3B7OwB3Jfdu4GSioc=

publicKey:MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBANHXAD4dfCaqG/NwHWPUnjtZj9VDr4r6A3px+5xzl5NrYuQeEOQP+jGC52ywDRy0xQAiXu/uGWhStKm84sXSKpECAwEAAQ==

password:BMxzWjQmHsQwzNmWPPBn94Vdz8Czi6fDIOHJcqXBGzkAiKsI5cb2NMa/wtmZY2AEXinaivtiJvqYMwWUPVzRYg==

spring.datasource.name=druidDataSource

spring.datasource.type=com.alibaba.druid.pool.DruidDataSource

spring.datasource.url=jdbc:mysql://localhost:3306/test?characterEncoding=utf-8&allowMultiQueries=true&autoReconnect=true

#---------密码加密------------------------

spring.datasource.username=netkiller

spring.datasource.password=BMxzWjQmHsQwzNmWPPBn94Vdz8Czi6fDIOHJcqXBGzkAiKsI5cb2NMa/wtmZY2AEXinaivtiJvqYMwWUPVzRYg==

#---------启用ConfigFilter支持-----------

spring.datasource.druid.filter.config.enabled=true

#---------设置公钥------------------------

spring.datasource.druid.publicKey=MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBANHXAD4dfCaqG/NwHWPUnjtZj9VDr4r6A3px+5xzl5NrYuQeEOQP+jGC52ywDRy0xQAiXu/uGWhStKm84sXSKpECAwEAAQ==

#---------设置连接属性---------------------

spring.datasource.druid.connection-properties=config.decrypt=true;config.decrypt.key=${spring.datasource.druid.publicKey}

@RunWith(SpringRunner.class)

@SpringBootTest

public class ApplicationTests {

@Test

public void druidEncrypt() throws Exception {

//密码明文

String password = "123456";

String[] keyPair = ConfigTools.genKeyPair(512);

//私钥

String privateKey = keyPair[0];

//公钥

String publicKey = keyPair[1];

//用私钥加密后的密文

password = ConfigTools.encrypt(privateKey, password);

System.out.println("privateKey:" + privateKey);

System.out.println("publicKey:" + publicKey);

System.out.println("password:" + password);

//用公钥解密密码

String decryptPassword = ConfigTools.decrypt(publicKey, password);

System.out.println("解密后:" + decryptPassword);

}

}

3. c3p0 - JDBC3 Connection and Statement Pooling

pom.xml

<dependency>

<groupId>org.hibernate</groupId>

<artifactId>hibernate-c3p0</artifactId>

<version>4.3.6.Final</version>

</dependency>

<dependency>

<groupId>c3p0</groupId>

<artifactId>c3p0</artifactId>

<version>0.9.1.2</version>

</dependency>

application.properties

spring.datasource.type=com.mchange.v2.c3p0.ComboPooledDataSource

spring.datasource.initialSize=5

spring.datasource.minIdle=5

spring.datasource.maxActive=20

spring.datasource.maxWait=60000

spring.datasource.timeBetweenEvictionRunsMillis=60000

spring.datasource.minEvictableIdleTimeMillis=300000

spring.datasource.validationQuery=SELECT 1 FROM DUAL

spring.datasource.testWhileIdle=true

spring.datasource.testOnBorrow=false

spring.datasource.testOnReturn=false

spring.datasource.poolPreparedStatements=true

spring.datasource.maxPoolPreparedStatementPerConnectionSize=20

spring.datasource.filters=stat,wall,log4j

spring.datasource.connectionProperties=druid.stat.mergeSql=true;druid.stat.slowSqlMillis=5000

#spring.datasource.useGlobalDataSourceStat=true

spring.datasource.driver-class-name=com.mysql.jdbc.Driver

spring.datasource.url=jdbc:mysql://192.168.6.1:3306/test

spring.datasource.username=inf

spring.datasource.password=inf

4. dbcp2

spring.datasource.type = org.apache.commons.dbcp2.BasicDataSource

5. bonecp

spring.datasource.type = com.jolbox.bonecp.BoneCPDataSource

6. HikariPool

# Hikari will use the above plus the following to setup connection pooling

spring.datasource.type=com.zaxxer.hikari.HikariDataSource

#最小空闲连接，默认值10，小于0或大于maximum-pool-size，都会重置为maximum-pool-size

spring.datasource.hikari.minimum-idle=5

#最大连接数，小于等于0会被重置为默认值10；大于零小于1会被重置为minimum-idle的值

spring.datasource.hikari.maximum-pool-size=15

#自动提交从池中返回的连接，默认值为true

spring.datasource.hikari.auto-commit=true

#空闲连接超时时间，默认值600000（10分钟），大于等于max-lifetime且max-lifetime>0，会被重置为0；不等于0且小于10秒，会被重置为10秒。

#只有空闲连接数大于最大连接数且空闲时间超过该值，才会被释放

spring.datasource.hikari.idle-timeout=30000

#连接池名称，默认HikariPool-1

spring.datasource.hikari.pool-name=Hikari

#连接最大存活时间.不等于0且小于30秒，会被重置为默认值30分钟.设置应该比mysql设置的超时时间短；单位ms

spring.datasource.hikari.max-lifetime=55000

#连接超时时间:毫秒，小于250毫秒，会被重置为默认值30秒

spring.datasource.hikari.connection-timeout=30000

#连接测试查询

spring.datasource.hikari.connection-test-query=SELECT 1

第 23 章 Spring boot with Queue

1. Spring boot with RabbitMQ(AMQP)

maven

<dependency>

<groupId>org.springframework.boot</groupId>

<artifactId>spring-boot-starter-amqp</artifactId>

</dependency>

RabbitMQConfig

@Configuration

public class RabbitMQConfig {

public final static String QUEUE_NAME = "spring-boot-queue";

public final static String EXCHANGE_NAME = "spring-boot-exchange";

public final static String ROUTING_KEY = "spring-boot-key";

// 创建队列

@Bean

public Queue queue() {

return new Queue(QUEUE_NAME);

}

// 创建一个 topic 类型的交换器

@Bean

public TopicExchange exchange() {

return new TopicExchange(EXCHANGE_NAME);

}

// 使用路由键（ROUTING_KEY）把队列（Queue）绑定到交换器（Exchange）

@Bean

public Binding binding(Queue queue, TopicExchange exchange) {

return BindingBuilder.bind(queue).to(exchange).with(ROUTING_KEY);

}

@Bean

public ConnectionFactory connectionFactory() {

CachingConnectionFactory connectionFactory = new CachingConnectionFactory("127.0.0.1", 5672);

connectionFactory.setUsername("guest");

connectionFactory.setPassword("guest");

return connectionFactory;

}

@Bean

public RabbitTemplate rabbitTemplate(ConnectionFactory connectionFactory) {

return new RabbitTemplate(connectionFactory);

}

}

生产者

@RestController

public class ProducerController {

@Autowired

private RabbitTemplate rabbitTemplate;

@GetMapping("/sendMessage")

public String sendMessage() {

new Thread(() -> {

for (int i = 0; i < 100; i++) {

String value = new DateTime().toString("yyyy-MM-dd HH:mm:ss");

System.out.println("send message {}", value);

rabbitTemplate.convertAndSend(RabbitMQConfig.EXCHANGE_NAME, RabbitMQConfig.ROUTING_KEY, value);

}

}).start();

return "ok";

}

}

消费者

@Component

public class Consumer {

@RabbitListener(queues = RabbitMQConfig.QUEUE_NAME)

public void consumeMessage(String message) {

System.out.println("consume message {}", message);

}

}

2. Spring boot with Apache Kafka

Spring boot 1.5.1

安装 kafka

一下安装仅仅适合开发环境，生产环境请使用这个脚本安装 https://github.com/oscm/shell/tree/master/mq/kafka

cd /usr/local/src

wget http://apache.communilink.net/kafka/0.10.2.0/kafka_2.12-0.10.2.0.tgz

tar zxvf kafka_2.12-0.10.2.0.tgz

mv kafka_2.12-0.10.2.0 /srv/

cp /srv/kafka_2.12-0.10.2.0/config/server.properties{,.original}

echo "advertised.host.name=localhost" >> /srv/kafka_2.12-0.10.2.0/config/server.properties

ln -s /srv/kafka_2.12-0.10.2.0 /srv/kafka

启动 Kafka 服务

/srv/kafka/bin/zookeeper-server-start.sh config/zookeeper.properties

/srv/kafka/bin/kafka-server-start.sh /srv/kafka/config/server.properties

-daemon 表示守护进程方式在后台启动

/srv/kafka/bin/zookeeper-server-start.sh -daemon config/zookeeper.properties

/srv/kafka/bin/kafka-server-start.sh -daemon /srv/kafka/config/server.properties

停止 Kafka 服务

/srv/kafka/bin/kafka-server-stop.sh

/srv/kafka/bin/zookeeper-server-stop.sh

maven

<dependency>

<groupId>org.springframework.kafka</groupId>

<artifactId>spring-kafka</artifactId>

</dependency>

<?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>

<groupId>cn.netkiller</groupId>

<artifactId>deploy</artifactId>

<version>0.0.1-SNAPSHOT</version>

<packaging>war</packaging>

<name>deploy.netkiller.cn</name>

<description>Deploy project for Spring Boot</description>

<parent>

<groupId>org.springframework.boot</groupId>

<artifactId>spring-boot-starter-parent</artifactId>

<version>1.5.1.RELEASE</version>

<relativePath /> <!-- lookup parent from repository -->

</parent>

<properties>

<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>

<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>

<java.version>1.8</java.version>

</properties>

<dependencies>

<!-- <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-actuator</artifactId> </dependency> -->

<!-- <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jpa</artifactId> </dependency> -->

<!-- <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-mongodb</artifactId> </dependency> -->

<!-- <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency> -->

<dependency>

<groupId>org.springframework.boot</groupId>

<artifactId>spring-boot-starter-data-redis</artifactId>

</dependency>

<dependency>

<groupId>org.springframework.session</groupId>

<artifactId>spring-session-data-redis</artifactId>

</dependency>

<dependency>

<groupId>org.springframework.boot</groupId>

<artifactId>spring-boot-starter-cache</artifactId>

</dependency>

<dependency>

<groupId>org.springframework.boot</groupId>

<artifactId>spring-boot-starter-security</artifactId>

</dependency>

<dependency>

<groupId>org.springframework.boot</groupId>

<artifactId>spring-boot-starter-web</artifactId>

</dependency>

<dependency>

<groupId>org.springframework.boot</groupId>

<artifactId>spring-boot-starter-websocket</artifactId>

</dependency>

<dependency>

<groupId>org.webjars</groupId>

<artifactId>webjars-locator</artifactId>

</dependency>

<dependency>

<groupId>org.webjars</groupId>

<artifactId>sockjs-client</artifactId>

<version>1.0.2</version>

</dependency>

<dependency>

<groupId>org.webjars</groupId>

<artifactId>stomp-websocket</artifactId>

<version>2.3.3</version>

</dependency>

<dependency>

<groupId>org.webjars</groupId>

<artifactId>bootstrap</artifactId>

<version>3.3.7</version>

</dependency>

<dependency>

<groupId>org.webjars</groupId>

<artifactId>jquery</artifactId>

<version>3.1.0</version>

</dependency>

<dependency>

<groupId>org.springframework.boot</groupId>

<artifactId>spring-boot-starter-test</artifactId>

<scope>test</scope>

</dependency>

<dependency>

<groupId>org.springframework.boot</groupId>

<artifactId>spring-boot-starter-mail</artifactId>

</dependency>

<dependency>

<groupId>org.apache.tomcat.embed</groupId>

<artifactId>tomcat-embed-jasper</artifactId>

<scope>provided</scope>

</dependency>

<dependency>

<groupId>javax.servlet</groupId>

<artifactId>jstl</artifactId>

</dependency>

<dependency>

<groupId>mysql</groupId>

<artifactId>mysql-connector-java</artifactId>

</dependency>

<dependency>

<groupId>com.google.code.gson</groupId>

<artifactId>gson</artifactId>

<!-- <version>2.7</version> -->

</dependency>

<dependency>

<groupId>com.caucho</groupId>

<artifactId>hessian</artifactId>

<version>4.0.38</version>

</dependency>

<dependency>

<groupId>org.springframework.kafka</groupId>

<artifactId>spring-kafka</artifactId>

</dependency>

<dependency>

<groupId>junit</groupId>

<artifactId>junit</artifactId>

<scope>test</scope>

</dependency>

</dependencies>

<build>

<plugins>

<plugin>

<groupId>org.springframework.boot</groupId>

<artifactId>spring-boot-maven-plugin</artifactId>

<configuration>

<mainClass>cn.netkiller.Application</mainClass>

</configuration>

</plugin>

<plugin>

<groupId>org.apache.maven.plugins</groupId>

<artifactId>maven-surefire-plugin</artifactId>

<configuration>

<skip>true</skip>

</configuration>

</plugin>

</plugins>

</build>

<repositories>

<repository>

<id>spring-snapshots</id>

<name>Spring Snapshots</name>

<url>https://repo.spring.io/snapshot</url>

<snapshots>

<enabled>true</enabled>

</snapshots>

</repository>

<repository>

<id>spring-milestones</id>

<name>Spring Milestones</name>

<url>https://repo.spring.io/milestone</url>

<snapshots>

<enabled>false</enabled>

</snapshots>

</repository>

</repositories>

<pluginRepositories>

<pluginRepository>

<id>spring-snapshots</id>

<name>Spring Snapshots</name>

<url>https://repo.spring.io/snapshot</url>

<snapshots>

<enabled>true</enabled>

</snapshots>

</pluginRepository>

<pluginRepository>

<id>spring-milestones</id>

<name>Spring Milestones</name>

<url>https://repo.spring.io/milestone</url>

<snapshots>

<enabled>false</enabled>

</snapshots>

</pluginRepository>

</pluginRepositories>

</project>

Spring boot Application

package cn.netkiller;

import org.springframework.boot.SpringApplication;

import org.springframework.boot.autoconfigure.EnableAutoConfiguration;

import org.springframework.boot.autoconfigure.SpringBootApplication;

import org.springframework.context.annotation.ComponentScan;

import org.springframework.scheduling.annotation.EnableScheduling;

@SpringBootApplication

@EnableAutoConfiguration

@ComponentScan

@EnableScheduling

public class Application {

public static void main(String[] args) {

SpringApplication.run(Application.class, args);

}

}

EnableKafka

package cn.netkiller.kafka;

import org.apache.kafka.clients.consumer.ConsumerConfig;

import org.apache.kafka.common.serialization.IntegerDeserializer;

import org.apache.kafka.common.serialization.StringDeserializer;

import org.springframework.context.annotation.Bean;

import org.springframework.context.annotation.Configuration;

import org.springframework.kafka.annotation.EnableKafka;

import org.springframework.kafka.config.ConcurrentKafkaListenerContainerFactory;

import org.springframework.kafka.config.KafkaListenerContainerFactory;

import org.springframework.kafka.core.ConsumerFactory;

import org.springframework.kafka.core.DefaultKafkaConsumerFactory;

import org.springframework.kafka.listener.ConcurrentMessageListenerContainer;

import java.util.HashMap;

import java.util.Map;

@Configuration

@EnableKafka

public class KafkaConsumerConfig {

public KafkaConsumerConfig() {

// TODO Auto-generated constructor stub

}

@Bean

KafkaListenerContainerFactory<ConcurrentMessageListenerContainer<String, String>> kafkaListenerContainerFactory() {

ConcurrentKafkaListenerContainerFactory<String, String> factory = new ConcurrentKafkaListenerContainerFactory<String, String>();

factory.setConsumerFactory(consumerFactory());

// factory.setConcurrency(1);

// factory.getContainerProperties().setPollTimeout(3000);

return factory;

}

@Bean

public ConsumerFactory<String, String> consumerFactory() {

return new DefaultKafkaConsumerFactory<String, String>(consumerConfigs());

}

@Bean

public Map<String, Object> consumerConfigs() {

Map<String, Object> propsMap = new HashMap<String, Object>();

propsMap.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, "www.netkiller.cn:9092");

propsMap.put(ConsumerConfig.GROUP_ID_CONFIG, "test-consumer-group");

propsMap.put(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, "latest");

propsMap.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG, true);

propsMap.put(ConsumerConfig.AUTO_COMMIT_INTERVAL_MS_CONFIG, "100");

propsMap.put(ConsumerConfig.SESSION_TIMEOUT_MS_CONFIG, "15000");

propsMap.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, IntegerDeserializer.class);

propsMap.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class);

return propsMap;

}

@Bean

public Listener listener() {

return new Listener();

}

}

KafkaListener

package cn.netkiller.kafka;

import org.apache.kafka.clients.consumer.ConsumerRecord;

import org.springframework.kafka.annotation.KafkaListener;

import java.util.concurrent.CountDownLatch;

import java.util.logging.Logger;

public class Listener {

public Listener() {

// TODO Auto-generated constructor stub

}

protected Logger logger = Logger.getLogger(Listener.class.getName());

public CountDownLatch getCountDownLatch1() {

return countDownLatch1;

}

private CountDownLatch countDownLatch1 = new CountDownLatch(1);

@KafkaListener(topics = "test")

public void listen(ConsumerRecord<?, ?> record) {

logger.info("Received message: " + record.toString());

System.out.println("Received message: " + record);

countDownLatch1.countDown();

}

}

测试

$ cd /srv/kafka

$ bin/kafka-console-producer.sh --broker-list 47.89.35.55:9092 --topic test

This is test message.

每输入一行回车后发送到你的Spring boot kafka 程序

完整的发布订阅实例

上面的例子仅仅是做了一个热身，现在我们将实现 一个完整的例子。

Consumer

例 23.1. Spring boot with Apache kafka.

SpringApplication

package cn.netkiller;

import org.springframework.boot.SpringApplication;

import org.springframework.boot.autoconfigure.EnableAutoConfiguration;

import org.springframework.boot.autoconfigure.SpringBootApplication;

import org.springframework.context.annotation.ComponentScan;

//import org.springframework.data.jpa.repository.config.EnableJpaRepositories;

//import org.springframework.data.mongodb.repository.config.EnableMongoRepositories;

import org.springframework.scheduling.annotation.EnableScheduling;

@SpringBootApplication

@EnableAutoConfiguration

@ComponentScan

// @EnableMongoRepositories

// @EnableJpaRepositories

@EnableScheduling

public class Application {

public static void main(String[] args) {

SpringApplication.run(Application.class, args);

}

}

Consumer configuration

package cn.netkiller.kafka.config;

import java.util.HashMap;

import java.util.Map;

import org.apache.kafka.clients.consumer.ConsumerConfig;

import org.apache.kafka.common.serialization.IntegerDeserializer;

import org.apache.kafka.common.serialization.StringDeserializer;

import org.springframework.beans.factory.annotation.Value;

import org.springframework.context.annotation.Bean;

import org.springframework.context.annotation.Configuration;

import org.springframework.kafka.annotation.EnableKafka;

import org.springframework.kafka.config.ConcurrentKafkaListenerContainerFactory;

import org.springframework.kafka.core.ConsumerFactory;

import org.springframework.kafka.core.DefaultKafkaConsumerFactory;

import cn.netkiller.kafka.consumer.Consumer;

@Configuration

@EnableKafka

public class ConsumerConfiguration {

public ConsumerConfiguration() {

// TODO Auto-generated constructor stub

}

@Bean

public Map<String, Object> consumerConfigs() {

HashMap<String, Object> props = new HashMap<>();

// list of host:port pairs used for establishing the initial connections

// to the Kakfa cluster

props.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, "www.netkiller.cn:9092");

props.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, IntegerDeserializer.class);

props.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class);

// consumer groups allow a pool of processes to divide the work of

// consuming and processing records

props.put(ConsumerConfig.GROUP_ID_CONFIG, "helloworld");

return props;

}

@Bean

public ConsumerFactory<String, String> consumerFactory() {

return new DefaultKafkaConsumerFactory<String, String>(consumerConfigs());

}

@Bean

public ConcurrentKafkaListenerContainerFactory<String, String> kafkaListenerContainerFactory() {

ConcurrentKafkaListenerContainerFactory<String, String> factory = new ConcurrentKafkaListenerContainerFactory<String, String>();

factory.setConsumerFactory(consumerFactory());

return factory;

}

@Bean

public Consumer receiver() {

return new Consumer();

}

}

Consumer

package cn.netkiller.kafka.consumer;

import java.util.concurrent.CountDownLatch;

import org.slf4j.Logger;

import org.slf4j.LoggerFactory;

import org.springframework.kafka.annotation.KafkaListener;

public class Consumer {

public Consumer() {

// TODO Auto-generated constructor stub

}

private static final Logger logger = LoggerFactory

.getLogger(Consumer.class);

private CountDownLatch latch = new CountDownLatch(1);

@KafkaListener(topics = "helloworld.t")

public void receiveMessage(String message) {

logger.info("received message='{}'", message);

latch.countDown();

}

public CountDownLatch getLatch() {

return latch;

}

}

Producer

例 23.2. Spring boot with Apache kafka.

Producer configuration

package cn.netkiller.kafka.config;

import java.util.HashMap;

import java.util.Map;

import org.apache.kafka.clients.producer.ProducerConfig;

import org.apache.kafka.common.serialization.IntegerSerializer;

import org.apache.kafka.common.serialization.StringSerializer;

import org.springframework.beans.factory.annotation.Value;

import org.springframework.context.annotation.Bean;

import org.springframework.context.annotation.Configuration;

import org.springframework.kafka.core.DefaultKafkaProducerFactory;

import org.springframework.kafka.core.KafkaTemplate;

import org.springframework.kafka.core.ProducerFactory;

import cn.netkiller.kafka.producer.Producer;

@Configuration

public class ProducerConfiguration {

public ProducerConfiguration() {

// TODO Auto-generated constructor stub

}

@Bean

public Map<String, Object> producerConfigs() {

HashMap<String, Object> props = new HashMap<>();

// list of host:port pairs used for establishing the initial connections

// to the Kakfa cluster

props.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, "www.netkiller.cn:9092");

props.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, IntegerSerializer.class);

props.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, StringSerializer.class);

// value to block, after which it will throw a TimeoutException

props.put(ProducerConfig.MAX_BLOCK_MS_CONFIG, 5000);

return props;

}

@Bean

public ProducerFactory<String, String> producerFactory() {

return new DefaultKafkaProducerFactory<String, String>(producerConfigs());

}

@Bean

public KafkaTemplate<String, String> kafkaTemplate() {

return new KafkaTemplate<String, String>(producerFactory());

}

@Bean

public Producer sender() {

return new Producer();

}

}

Producer

package cn.netkiller.kafka.producer;

import org.slf4j.Logger;

import org.slf4j.LoggerFactory;

import org.springframework.beans.factory.annotation.Autowired;

import org.springframework.kafka.core.KafkaTemplate;

import org.springframework.kafka.support.SendResult;

import org.springframework.util.concurrent.ListenableFuture;

import org.springframework.util.concurrent.ListenableFutureCallback;

public class Producer {

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

/*

* public Sender() { // TODO Auto-generated constructor stub }

*/

@Autowired

private KafkaTemplate<String, String> kafkaTemplate;

public void sendMessage(String topic, String message) {

// the KafkaTemplate provides asynchronous send methods returning a

// Future

ListenableFuture<SendResult<String, String>> future = kafkaTemplate.send(topic, message);

// you can register a callback with the listener to receive the result

// of the send asynchronously

future.addCallback(new ListenableFutureCallback<SendResult<String, String>>() {

@Override

public void onSuccess(SendResult<String, String> result) {

logger.info("sent message='{}' with offset={}", message, result.getRecordMetadata().offset());

}

@Override

public void onFailure(Throwable ex) {

logger.error("unable to send message='{}'", message, ex);

}

});

// alternatively, to block the sending thread, to await the result,

// invoke the future’s get() method

}

}

Controller

package cn.netkiller.web;

import java.util.concurrent.TimeUnit;

import org.slf4j.Logger;

import org.slf4j.LoggerFactory;

import org.springframework.beans.factory.annotation.Autowired;

import org.springframework.stereotype.Controller;

import org.springframework.web.bind.annotation.RequestMapping;

import org.springframework.web.bind.annotation.ResponseBody;

import cn.netkiller.kafka.consumer.Consumer;

import cn.netkiller.kafka.producer.Producer;

@Controller

@RequestMapping("/test")

public class KafkaTestController {

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

public KafkaTestController() {

// TODO Auto-generated constructor stub

}

@Autowired

private Producer sender;

@Autowired

private Consumer receiver;

@RequestMapping("/ping")

@ResponseBody

public String ping() {

String message = "PONG";

return message;

}

@RequestMapping("/kafka/send")

@ResponseBody

public String testReceiver() throws Exception {

sender.sendMessage("helloworld.t", "Hello Spring Kafka!");

receiver.getLatch().await(10000, TimeUnit.MILLISECONDS);

logger.info(receiver.getLatch().getCount() + "");

return "OK";

}

}

Test

例 23.3. Test Spring Kafka

SpringBootTest

package cn.netkiller;

import static org.assertj.core.api.Assertions.assertThat;

import java.util.concurrent.TimeUnit;

import org.junit.Test;

import org.junit.runner.RunWith;

import org.springframework.beans.factory.annotation.Autowired;

import org.springframework.boot.test.context.SpringBootTest;

import org.springframework.test.context.junit4.SpringRunner;

import cn.netkiller.kafka.consumer.Consumer;

import cn.netkiller.kafka.producer.Producer;

@RunWith(SpringRunner.class)

@SpringBootTest

public class SpringKafkaApplicationTests {

public SpringKafkaApplicationTests() {

// TODO Auto-generated constructor stub

}

@Autowired

private Producer sender;

@Autowired

private Consumer receiver;

@Test

public void testReceiver() throws Exception {

sender.sendMessage("helloworld.t", "Hello Spring Kafka!");

receiver.getLatch().await(10000, TimeUnit.MILLISECONDS);

assertThat(receiver.getLatch().getCount()).isEqualTo(0);

}

}

Spring cloud with Kafka

Application 主文件

package schedule;

import org.springframework.boot.SpringApplication;

import org.springframework.boot.autoconfigure.SpringBootApplication;

import org.springframework.boot.autoconfigure.domain.EntityScan;

import org.springframework.cloud.netflix.eureka.EnableEurekaClient;

import org.springframework.scheduling.annotation.EnableScheduling;

@SpringBootApplication

@EnableScheduling

@EnableEurekaClient

@EntityScan("common.domain")

public class Application {

public static void main(String[] args) {

System.out.println("Service Schedule Starting...");

SpringApplication.run(Application.class, args);

}

}

资源配置文件

application.properties

只需要两行，其余所有配置均放在配置中心。

# ==============================

spring.application.name=schedule

eureka.client.serviceUrl.defaultZone=http://eureka:s3cr3t@172.16.0.10:8761/eureka/

# ==============================

bootstrap.properties

配置中心服务器相关配置

#spring.application.name=schedule

spring.cloud.config.profile=development

spring.cloud.config.label=master

spring.cloud.config.uri=http://172.16.0.10:8888

management.security.enabled=false

spring.cloud.config.username=cfg

spring.cloud.config.password=s3cr3t

Git 仓库

在 git 仓库中加入 spring.kafka.bootstrap_servers 配置项

spring.kafka.bootstrap_servers=172.16.0.10:9092

启用 kafka

使用 @EnableKafka 启用 Kafka 不需要其他@Bean等。这个配置文件可以省略，可以将 @EnableKafka 放到 Application.java 中。我还是喜欢独立配置。

package schedule.config;

@Configuration

@EnableKafka

public class KafkaConfiguration {

}

消息发布主程序

package schedule.task;

import java.text.ParseException;

import java.text.SimpleDateFormat;

import java.util.Calendar;

import java.util.Date;

import java.util.List;

import org.slf4j.Logger;

import org.slf4j.LoggerFactory;

import org.springframework.beans.factory.annotation.Autowired;

import org.springframework.beans.factory.annotation.Value;

import org.springframework.data.redis.core.RedisTemplate;

import org.springframework.kafka.core.KafkaTemplate;

import org.springframework.kafka.support.SendResult;

import org.springframework.scheduling.annotation.Scheduled;

import org.springframework.stereotype.Component;

import org.springframework.util.concurrent.ListenableFuture;

import org.springframework.util.concurrent.ListenableFutureCallback;

import org.springframework.web.client.RestTemplate;

import com.fasterxml.jackson.core.JsonProcessingException;

import com.fasterxml.jackson.databind.ObjectMapper;

import schedule.repository.CmsTrashRepository;

import schedule.repository.ArticleRepository;

import common.domain.Article;

import common.domain.CmsTrash;

import common.pojo.ResponseRestful;

@Component

public class CFPushTasks {

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

private static final String TOPIC = "test";

private static final SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");

private static final ObjectMapper mapper = new ObjectMapper();

@Autowired

private ArticleRepository articleRepository;

@Autowired

private CmsTrashRepository cmsTrashRepository;

@Autowired

private KafkaTemplate<String, String> kafkaTemplate;

@Autowired

private RedisTemplate<String, String> redisTemplate;

@Value("${cf.cms.site_id}")

private int siteId;

public CFPushTasks() {

}

private Date getDate() {

Calendar calendar = Calendar.getInstance();

calendar.add(Calendar.MINUTE, -1);

Date date = calendar.getTime();

return date;

}

private boolean setPostionDate(String key, Date value) {

String cacheKey = String.format("schedule:CFPushTasks:%s", key);

String date = simpleDateFormat.format(value);

logger.info("setPostion({},{})", cacheKey, date);

redisTemplate.opsForValue().set(cacheKey, date);

if (value == this.getPostionDate(cacheKey)) {

return true;

}

return false;

}

private Date getPostionDate(String key) {

String cacheKey = String.format("schedule:CFPushTasks:%s", key);

Date date = null;

if (redisTemplate.hasKey(cacheKey)) {

try {

date = simpleDateFormat.parse(redisTemplate.opsForValue().get(cacheKey));

} catch (ParseException e) {

// TODO Auto-generated catch block

// e.printStackTrace();

logger.warn(e.getMessage());

}

}

logger.debug("getPostion({}) => {}", cacheKey, date);

return date;

}

private boolean setPostionId(String key, int id) {

String cacheKey = String.format("schedule:CFPushTasks:PostionId:%s", key);

logger.info("setPostionId({},{})", cacheKey, id);

redisTemplate.opsForValue().set(cacheKey, String.valueOf(id));

if (id == this.getPostionId(cacheKey)) {

return true;

}

return false;

}

private int getPostionId(String key) {

String cacheKey = String.format("schedule:CFPushTasks:PostionId:%s", key);

int id = 0;

if (redisTemplate.hasKey(cacheKey)) {

id = Integer.valueOf(redisTemplate.opsForValue().get(cacheKey));

}

logger.debug("getPostion({}) => {}", cacheKey, id);

return id;

}

@Scheduled(fixedRate = 1000 * 50)

public void insert() {

Iterable<Article> articles = null;

int id = this.getPostionId("insert");

if (id == 0) {

articles = articleRepository.findBySiteId(this.siteId);

} else {

articles = articleRepository.findBySiteIdAndIdGreaterThan(this.siteId, id);

}

if (articles != null) {

for (Article article : articles) {

ResponseRestful responseRestful = new ResponseRestful(true, this.getPostionId("insert"), "INSERT", article);

String jsonString;

try {

jsonString = mapper.writeValueAsString(responseRestful);

this.send(TOPIC, jsonString);

if (!this.setPostionId("insert", article.getId())) {

return;

}

} catch (JsonProcessingException e) {

// TODO Auto-generated catch block

e.printStackTrace();

}

}

}

}

@Scheduled(fixedRate = 1000 * 50)

public void update() {

String message = "Hello";

this.send(TOPIC, message);

}

@Scheduled(fixedRate = 1000 * 50)

public void delete() {

Date date = this.getPostionDate("delete");

Iterable<CmsTrash> cmsTrashs;

if (date == null) {

cmsTrashs = cmsTrashRepository.findBySiteIdAndTypeOrderByCtime(this.siteId, "delete");

} else {

cmsTrashs = cmsTrashRepository.findBySiteIdAndTypeAndCtimeGreaterThanOrderByCtime(this.siteId, "delete", date);

}

if (cmsTrashs != null) {

for (CmsTrash cmsTrash : cmsTrashs) {

ResponseRestful responseRestful = new ResponseRestful(true, this.getPostionId("delete"), "DELETE", cmsTrash);

String jsonString;

try {

jsonString = mapper.writeValueAsString(responseRestful);

this.send(TOPIC, jsonString);

this.setPostionId("delete", cmsTrash.getId());

if (!this.setPostionDate("delete", cmsTrash.getCtime())) {

return;

} else {

}

} catch (JsonProcessingException e) {

// TODO Auto-generated catch block

e.printStackTrace();

}

}

}

}

private void send(String topic, String message) {

ListenableFuture<SendResult<String, String>> future = kafkaTemplate.send(topic, message);

future.addCallback(new ListenableFutureCallback<SendResult<String, String>>() {

@Override

public void onSuccess(SendResult<String, String> result) {

logger.debug("sent message='{}' with offset={}", message, result.getRecordMetadata().offset());

}

@Override

public void onFailure(Throwable ex) {

logger.error("unable to send message='{}'", message, ex);

}

});

}

private void post(ResponseRestful responseRestful) {

RestTemplate restTemplate = new RestTemplate();

String response = restTemplate.postForObject("http://localhost:8440/test/cf/post.json", responseRestful, String.class);

// logger.info(article.toString());

if (response != null) {

logger.info(response);

}

}

}

第 24 章 Spring boot with Scheduling

项目中经常会用到计划任务，spring Boot 中通过@EnableScheduling启用计划任务，再通过@Scheduled注解配置计划任务如果运行。

1. Application.java

Application.java

启用计划任务, 在Spring Boot启动类加上注解@EnableScheduling，表示该项目启用计划任务

package api;

import org.springframework.boot.SpringApplication;

import org.springframework.boot.autoconfigure.EnableAutoConfiguration;

import org.springframework.boot.autoconfigure.SpringBootApplication;

import org.springframework.context.annotation.Bean;

import org.springframework.context.annotation.ComponentScan;

import org.springframework.data.jpa.repository.config.EnableJpaRepositories;

import org.springframework.data.mongodb.repository.config.EnableMongoRepositories;

import org.springframework.scheduling.annotation.EnableScheduling;

import org.springframework.web.servlet.config.annotation.CorsRegistry;

import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;

@SpringBootApplication

@EnableAutoConfiguration

@ComponentScan({ "api.config", "api.web", "api.rest", "api.service","api.schedule" })

@EnableMongoRepositories

@EnableJpaRepositories

@EnableScheduling

public class Application {

public static void main(String[] args) {

SpringApplication.run(Application.class, args);

}

}

开启计划任务 @EnableScheduling

确保你的计划任务在 @ComponentScan 包中。

2. Component

在计划任务方法上加上@Scheduled注解，表示该方法是一个计划任务，项目启动后会去扫描该注解的方法并加入计划任务列表。

package api.schedule;

import java.text.SimpleDateFormat;

import java.util.Date;

import org.slf4j.Logger;

import org.slf4j.LoggerFactory;

import org.springframework.scheduling.annotation.Scheduled;

import org.springframework.stereotype.Component;

@Component

public class ScheduledTasks {

private static final Logger log = LoggerFactory.getLogger(ScheduledTasks.class);

private static final SimpleDateFormat dateFormat = new SimpleDateFormat("HH:mm:ss");

public final static long ONE_DAY = 24 * 60 * 60 * 1000;

public final static long ONE_HOUR = 60 * 60 * 1000;

public ScheduledTasks() {

// TODO Auto-generated constructor stub

}

@Scheduled(fixedRate = 5000) //5秒运行一次调度任务

public void echoCurrentTime() {

log.info("The time is now {}", dateFormat.format(new Date()));

}

@Scheduled(fixedRate = ONE_DAY)

public void scheduledTask() {

System.out.println("每隔一天执行一次调度任务");

}

@Scheduled(fixedDelay = ONE_HOUR)

public void scheduleTask2() {

System.out.println("运行完后隔一小时就执行任务");

}

@Scheduled(initialDelay = 1000, fixedRate = 5000)

public void doSomething() {

// something that should execute periodically

}

@Scheduled(cron = "0 0/1 * * * ? ")

public void ScheduledTask3() {

System.out.println(" 每隔一分钟执行一次任务");

}

}

3. 查看日志

tail -f spring.log

4. 计划任务控制开关

matchIfMissing = true， 如果改属性条目不存在返回 true

@ConditionalOnProperty("batch.metrics.export.influxdb.enabled")

# mybean.enabled = true

@ConditionalOnProperty(value='mybean.enabled')

@ConditionalOnProperty(value = "endpoints.hal.enabled", matchIfMissing = true)

# server.host = localhost

@ConditionalOnProperty(name="server.host", havingValue="localhost")

@ConditionalOnExpression("'${server.host}'=='localhost'")

# spring.rabbitmq.dynamic = true

@ConditionalOnProperty(prefix = "spring.rabbitmq", name = "dynamic", matchIfMissing = true)

@ConditionalOnProperty(prefix = "extension.security.cors", name = "enabled", matchIfMissing = false)

@ConditionalOnProperty(prefix = "camunda.bpm.job-execution", name = "enabled", havingValue = "true", matchIfMissing = true)

# spring.social.auto-connection-views = true

@ConditionalOnProperty(prefix = "spring.social.", value = "auto-connection-views")

使用案例

package mis.schedule;

import java.text.SimpleDateFormat;

import java.util.Date;

import org.slf4j.Logger;

import org.slf4j.LoggerFactory;

import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;

import org.springframework.scheduling.annotation.Scheduled;

import org.springframework.stereotype.Component;

@ConditionalOnProperty("mis.schedule.enabled")

@Component

public class ScheduledTasks {

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

private static final SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-mm-dd HH:mm:ss");

public final static long ONE_DAY = 24 * 60 * 60 * 1000;

public final static long ONE_HOUR = 60 * 60 * 1000;

public final static long ONE_SECOND = 1000;

public ScheduledTasks() {

// TODO Auto-generated constructor stub

}

@Scheduled(fixedDelay = ONE_SECOND)

public void scheduleTaskSplitLine() {

logger.info("==================== {} ====================", dateFormat.format(new Date()));

}

}

application.properties 配置如下

mis.schedule.enabled=true

5. @Scheduled 详解

@Scheduled参数说明

@Scheduled注解有一些参数，用于配置计划任务执行频率，执行时段等。

cron ：cron表达式，e.g. {@code "0 * * * * ？"}从前到后依次表示秒 分 时 日 月 年

zone：设置时区，指明计划任务运行在哪个时区下，默认为空，采用操作系统默认时区

fixedDelay：同一个计划任务两次执行间隔固定时间，单位毫秒，上次执行结束到下次开始执行的时间，以long类型复制

fixedDelayString：同一个计划任务两次执行间隔固定时间，单位毫秒，上次执行结束到下次开始执行的时间，以String类型赋值

fixedRate：以一个固定频率执行，单位毫秒，表示每隔多久执行一次，以long类型赋值

fixedRateString：以一个固定频率执行，单位毫秒，表示每隔多久执行一次，以String类型赋值

initialDelay：延迟启动计划任务，单位毫秒，表示执行第一次计划任务前先延迟一段时间，以long类型赋值

initialDelayString：延迟启动计划任务，单位毫秒，表示执行第一次计划任务前先延迟一段时间，以String赋值

cron表达式使用空格分隔的时间元素。

字段   允许值   允许的特殊字符

秒 0-59 , - * /

分 0-59 , - * /

小时 0-23 , - * /

日期 1-31 , - * ? / L W C

月份 1-12 或者 JAN-DEC , - * /

星期 1-7 或者 SUN-SAT , - * ? / L C #

年（可选） 留空, 1970-2099 , - * /

按顺序依次为

秒（0~59）

分钟（0~59）

小时（0~23）

天（月）（0~31，但是你需要考虑你月的天数）

月（0~11）

天（星期）（1~7 1=SUN 或 SUN，MON，TUE，WED，THU，FRI，SAT）

7.年份（1970－2099）

其中每个元素可以是一个值(如6),一个连续区间(9-12),一个间隔时间(8-18/4)(/表示每隔4小时),一个列表(1,3,5),通配符。由于"月份中的日期"和"星期中的日期"这两个元素互斥的,必须要对其中一个设置?.

0 0 10,14,16 * * ? 每天上午10点，下午2点，4点

0 0/30 9-17 * * ?   朝九晚五工作时间内每半小时

0 0 12 ? * WED 表示每个星期三中午12点

"0 0 12 * * ?" 每天中午12点触发

"0 15 10 ? * *" 每天上午10:15触发

"0 15 10 * * ?" 每天上午10:15触发

"0 15 10 * * ? *" 每天上午10:15触发

"0 15 10 * * ? 2005" 2005年的每天上午10:15触发

"0 * 14 * * ?" 在每天下午2点到下午2:59期间的每1分钟触发

"0 0/5 14 * * ?" 在每天下午2点到下午2:55期间的每5分钟触发

"0 0/5 14,18 * * ?" 在每天下午2点到2:55期间和下午6点到6:55期间的每5分钟触发

"0 0-5 14 * * ?" 在每天下午2点到下午2:05期间的每1分钟触发

"0 10,44 14 ? 3 WED" 每年三月的星期三的下午2:10和2:44触发

"0 15 10 ? * MON-FRI" 周一至周五的上午10:15触发

"0 15 10 15 * ?" 每月15日上午10:15触发

"0 15 10 L * ?" 每月最后一日的上午10:15触发

"0 15 10 ? * 6L" 每月的最后一个星期五上午10:15触发

"0 15 10 ? * 6L 2002-2005" 2002年至2005年的每月的最后一个星期五上午10:15触发

"0 15 10 ? * 6#3" 每月的第三个星期五上午10:15触发

有些子表达式能包含一些范围或列表

例如：子表达式（天（星期））可以为 “MON-FRI”，“MON，WED，FRI”，“MON-WED,SAT”

“*”字符代表所有可能的值

因此，“*”在子表达式（月）里表示每个月的含义，“*”在子表达式（天（星期））表示星期的每一天

“/”字符用来指定数值的增量

例如：在子表达式（分钟）里的“0/15”表示从第0分钟开始，每15分钟

在子表达式（分钟）里的“3/20”表示从第3分钟开始，每20分钟（它和“3，23，43”）的含义一样

“？”字符仅被用于天（月）和天（星期）两个子表达式，表示不指定值

当2个子表达式其中之一被指定了值以后，为了避免冲突，需要将另一个子表达式的值设为“？”

“L” 字符仅被用于天（月）和天（星期）两个子表达式，它是单词“last”的缩写

但是它在两个子表达式里的含义是不同的。

在天（月）子表达式中，“L”表示一个月的最后一天

在天（星期）自表达式中，“L”表示一个星期的最后一天，也就是SAT

如果在“L”前有具体的内容，它就具有其他的含义了

例如：“6L”表示这个月的倒数第６天，“ＦＲＩＬ”表示这个月的最一个星期五

注意：在使用“L”参数时，不要指定列表或范围，因为这会导致问题

每3秒钟一运行一次

@Component

@EnableScheduling

public class MyTask {

@Scheduled(cron="*/3 * * * * *")

public void myTaskMethod(){

//do something

}

}

凌晨23点运行

@Scheduled(cron = "0 0 23 * * ?")

private void cleanNewToday() {

long begin = System.currentTimeMillis();

redisTemplate.delete("news:today");

long end = System.currentTimeMillis();

logger.info("Schedule clean redis {} 耗时 {} 秒", "cleanNewFlash()", (end-begin) / 1000 );

}

6. Timer 例子

package cn.netkiller.schedule;

import java.util.Date;

import java.util.Timer;

import java.util.TimerTask;

public class TimerTest {

public static void main(String[] args) {

// TODO Auto-generated method stub

TimerTask timerTask = new TimerTask() {

@Override

public void run() {

System.out.println("task  run:" + new Date());

}

};

Timer timer = new Timer();

// 每3秒执行一次

timer.schedule(timerTask, 10, 3000);

}

}

7. ScheduledExecutorService 例子

package cn.netkiller.schedule;

import java.util.Date;

import java.util.concurrent.Executors;

import java.util.concurrent.ScheduledExecutorService;

import java.util.concurrent.TimeUnit;

public class ScheduledExecutorServiceTest {

public static void main(String[] args) {

// TODO Auto-generated method stub

ScheduledExecutorService service = Executors.newSingleThreadScheduledExecutor();

// 参数：执行命令，初始执行的延时时间，任务执行间隔，间隔时间单位

service.scheduleAtFixedRate(() -> System.out.println("ScheduledExecutorService " + new Date()), 0, 3, TimeUnit.SECONDS);

}

}

第 25 章 Spring boot with Swagger

1. Swagger3

<dependency>

<groupId>io.springfox</groupId>

<artifactId>springfox-boot-starter</artifactId>

<version>3.0.0</version>

</dependency>

2. Swagger2

Maven 文件

<?xml version="1.0"?>

<project xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd" xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">

<modelVersion>4.0.0</modelVersion>

<parent>

<groupId>cn.netkiller</groupId>

<artifactId>parent</artifactId>

<version>0.0.1-SNAPSHOT</version>

</parent>

<groupId>cn.netkiller</groupId>

<artifactId>swagger2</artifactId>

<version>0.0.1-SNAPSHOT</version>

<name>swagger2</name>

<url>http://www.netkiller.cn</url>

<properties>

<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>

</properties>

<dependencies>

<dependency>

<groupId>org.springframework.boot</groupId>

<artifactId>spring-boot-starter-web</artifactId>

</dependency>

<dependency>

<groupId>io.springfox</groupId>

<artifactId>springfox-swagger2</artifactId>

<version>2.9.2</version>

</dependency>

<dependency>

<groupId>io.springfox</groupId>

<artifactId>springfox-swagger-ui</artifactId>

<version>2.9.2</version>

</dependency>

<dependency>

<groupId>junit</groupId>

<artifactId>junit</artifactId>

<scope>test</scope>

</dependency>

</dependencies>

</project>

SpringApplication

package cn.netkiller.swagger2;

import org.springframework.boot.SpringApplication;

import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication

public class Application {

public static void main(String[] args) {

System.out.println("Swagger2!");

SpringApplication.run(Application.class, args);

}

}

EnableSwagger2

package cn.netkiller.swagger2;

import org.springframework.context.annotation.Bean;

import org.springframework.context.annotation.Configuration;

import com.google.common.base.Predicate;

import springfox.documentation.builders.ApiInfoBuilder;

import springfox.documentation.service.ApiInfo;

import springfox.documentation.service.Contact;

import springfox.documentation.spi.DocumentationType;

import springfox.documentation.spring.web.plugins.Docket;

import springfox.documentation.swagger2.annotations.EnableSwagger2;

import static springfox.documentation.builders.PathSelectors.regex;

import static com.google.common.base.Predicates.or;

@Configuration

@EnableSwagger2

public class Swagger2Configuration {

@Bean

public Docket postsApi() {

return new Docket(DocumentationType.SWAGGER_2).groupName("public").apiInfo(apiInfo()).select().paths(postPaths()).build();

}

private Predicate<String> postPaths() {

return or(regex("/api/.*"), regex("/public/api/.*"));

}

private ApiInfo apiInfo() {

return new ApiInfoBuilder().title("Open API").description("Open API reference for developers").termsOfServiceUrl("http://api.netkiller.cn").contact(new Contact("Neo Chen", "http://www.netkiller.cn", "netkiller@msn.com")).license("Mit License").licenseUrl("").version("1.0").build();

}

}

RestController

package cn.netkiller.swagger2;

import org.springframework.web.bind.annotation.RequestMapping;

import org.springframework.web.bind.annotation.RequestMethod;

import org.springframework.web.bind.annotation.RestController;

@RestController

public class HelloController {

@RequestMapping(method = RequestMethod.GET, value = "/api/hello")

public String sayHello() {

return "Swagger Hello World";

}

}

3. @Api()

用于类；表示标识这个类是swagger的资源tags,value 是说明，可以使用tags替代, tags如果有多个值，生成多个list

@Api(value="用户控制器",tags={"用户操作接口"})

@RestController

public class UserController {

}

4. @ApiOperation()

用于方法；表示一个http请求的操作，value用于方法描述，notes用于提示内容，tags可以重新分组

@ApiImplicitParams() 请求参数，包含多个 @ApiImplicitParam

@ApiImplicitParam()

name–参数ming

value–参数说明

dataType–数据类型

paramType–参数类型

example–举例说明

package cn.netkiller.swagger2;

import org.springframework.web.bind.annotation.RequestMapping;

import org.springframework.web.bind.annotation.RequestMethod;

import org.springframework.web.bind.annotation.RequestParam;

import org.springframework.web.bind.annotation.RestController;

import io.swagger.annotations.Api;

import io.swagger.annotations.ApiImplicitParam;

import io.swagger.annotations.ApiImplicitParams;

import io.swagger.annotations.ApiOperation;

@Api(value = "test", tags = "test")

@RestController

@RequestMapping("/api/test")

public class TestController {

@ApiOperation(value = "接口说明", notes = "接口说明")

@ApiImplicitParams({ @ApiImplicitParam(name = "id", value = "唯一ID", dataType = "Integer"), @ApiImplicitParam(name = "name", value = "名字") })

@RequestMapping(value = "/name", method = { RequestMethod.GET, RequestMethod.POST })

public String test(@RequestParam(value = "id", required = true) String id, @RequestParam(value = "name", required = true) String name) {

return String.format("%s:%s", id, name);

}

@ApiOperation(value = "getGreeting", notes="get greeting",nickname = "getGreeting")

@RequestMapping(method = RequestMethod.GET, value = "/api/javainuse")

public <Hello> sayHello() {

ArrayList<Hello> arrayList= new ArrayList<>();

arrayList.add(new Hello());

return arrayList;

}

}

5. @ApiResponses

@ApiOperation(value = "getGreeting", nickname = "getGreeting")

@ApiResponses(value = {

@ApiResponse(code = 500, message = "Server error"),

@ApiResponse(code = 404, message = "Service not found"),

@ApiResponse(code = 200, message = "Successful retrieval",

response = Hello.class, responseContainer = "List") })

@RequestMapping(method = RequestMethod.GET, value = "/api/javainuse")

public <Hello> sayHello() {

ArrayList<Hello> arrayList= new ArrayList<>();

arrayList.add(new Hello());

return arrayList;

}

6. @ApiModel 实体类

@ApiModel()用于类 ；表示对类进行说明，用于参数用实体类接收

value–表示对象名,description–描述,都可省略

@ApiModelProperty()用于方法，字段； 表示对model属性的说明或者数据操作更改

value 字段说明

name 重写属性名字

dataType 重写属性类型

required 是否必填

example 举例说明

hidden 隐藏

package cn.netkiller.swagger2;

import java.io.Serializable;

import java.util.List;

import io.swagger.annotations.ApiModel;

import io.swagger.annotations.ApiModelProperty;

@ApiModel(value = "User", description = "通用用户对象")

public class User implements Serializable {

private static final long serialVersionUID = 1L;

@ApiModelProperty(value = "用户名", name = "username", example = "neo")

private String username = "Neo";

private String password = "passw0rd";

private String nickname = "netkiller";

@ApiModelProperty(value = "状态", name = "state", example = "false", required = true)

private boolean state = false;

@ApiModelProperty(value = "字符串数组", hidden = true)

private String[] array;

private List<String> list;

public String getUsername() {

return username;

}

public void setUsername(String username) {

this.username = username;

}

public String getPassword() {

return password;

}

public void setPassword(String password) {

this.password = password;

}

public String getNickname() {

return nickname;

}

public void setNickname(String nickname) {

this.nickname = nickname;

}

public boolean isState() {

return state;

}

public void setState(boolean state) {

this.state = state;

}

public String[] getArray() {

return array;

}

public void setArray(String[] array) {

this.array = array;

}

public List<String> getList() {

return list;

}

public void setList(List<String> list) {

this.list = list;

}

}

package cn.netkiller.swagger2;

import java.io.Serializable;

import java.util.List;

import org.springframework.web.bind.annotation.PostMapping;

import org.springframework.web.bind.annotation.RequestBody;

import org.springframework.web.bind.annotation.RestController;

import io.swagger.annotations.Api;

import io.swagger.annotations.ApiOperation;

import io.swagger.annotations.ApiParam;

@Api(value = "测试", tags = "test")

@RestController

@RequestMapping("/api/test")

public class TestController {

@ApiOperation("更改用户信息")

@PostMapping("/user/info")

public User userInfo(@RequestBody @ApiParam(name = "用户对象", value = "传入json格式", required = true) User user) {

return user;

}

}

第 26 章 Spring boot with knife4j

1. maven

<dependency>

<groupId>com.github.xiaoymin</groupId>

<artifactId>knife4j-openapi3-jakarta-spring-boot-starter</artifactId>

<version>4.4.0</version>

</dependency>

2. Knife4jConfiguration

package cn.netkiller.config;

import io.swagger.v3.oas.models.ExternalDocumentation;

import io.swagger.v3.oas.models.OpenAPI;

import io.swagger.v3.oas.models.info.Contact;

import io.swagger.v3.oas.models.info.Info;

import io.swagger.v3.oas.models.info.License;

import org.springdoc.core.models.GroupedOpenApi;

import org.springframework.context.annotation.Bean;

import org.springframework.context.annotation.Configuration;

@Configuration

public class Knife4jConfiguration {

@Bean

public GroupedOpenApi adminApi() {      // 创建了一个api接口的分组

return GroupedOpenApi.builder()

.group("default")         // 分组名称

.pathsToMatch("/**")  // 接口请求路径规则

.build();

}

@Bean

public OpenAPI openAPI() {

return new OpenAPI()

.info(new Info()

.title("netkiller")

.description("接口描述")

.version("v1.0.0")

.contact(new Contact().name("neo").email("netkiller@msn.com"))

.license(new License().name("Apache 2.0").url("https://www.netkiller.cn/docs"))

).externalDocs(new ExternalDocumentation()

.description("外部文档")

.url("https://www.netkiller.cn/docs"));

}

}

3. application.properties

# springdoc-openapi项目配置

springdoc.swagger-ui.path=/swagger-ui.html

springdoc.swagger-ui.tags-sorter=alpha

springdoc.swagger-ui.operations-sorter=alpha

springdoc.api-docs.path=/v3/api-docs

# knife4j的增强配置，不需要增强可以不配

knife4j.enable=true

knife4j.setting.language=zh_cn

第 27 章 Spring boot with lombok

<dependency>

<groupId>org.projectlombok</groupId>

<artifactId>lombok</artifactId>

<optional>true</optional>

</dependency>

常用的 lombok 注解:

@EqualsAndHashCode：实现equals()方法和hashCode()方法 @ToString：实现toString()方法

@Data  ：注解在类上；提供类所有属性的 getting 和 setting 方法，此外还提供了equals、canEqual、hashCode、toString 方法

@Setter：注解在属性上；为属性提供 setting 方法

@Getter：注解在属性上；为属性提供 getting 方法

@Log4j ：注解在类上；为类提供一个 属性名为log 的 log4j 日志对象

@NoArgsConstructor：注解在类上；为类提供一个无参的构造方法

@AllArgsConstructor：注解在类上；为类提供一个全参的构造方法

@Cleanup：关闭流 @Synchronized：对象同步 @SneakyThrows：抛出异常

1. @Builder

package cn.netkiller.graphql.domain;

import lombok.Builder;

import lombok.Data;

@Builder

@Data

public class Author {

private Integer id;

private String name;

private Integer age;

public Author() {

// TODO Auto-generated constructor stub

}

@Override

public String toString() {

return "Author [id=" + id + ", name=" + name + ", age=" + age + "]";

}

}

Author author = Author.builder().id(1).name("Neo Chen").age(40).build();

2. @Slf4j 注解

如果不想每次都写

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

可以用注解 @Slf4j

package cn.netkiller.service;

import org.springframework.web.bind.annotation.GetMapping;

import org.springframework.web.bind.annotation.RestController;

import lombok.extern.slf4j.Slf4j;

@RestController

@Slf4j

public class HelloController {

// private static final Log log = LogFactory.getLog(HelloController.class);

@GetMapping("/")

public String hello() {

Log.info("@Slf4j Test OK");

return "Hello World";

}

}

第 28 章 Spring boot with Container

1. Spring boot with Docker

通过 Docker 命令构建镜像

手工编译镜像

在项目根目录创建 Dockerfile 文件

% cat Dockerfile

FROM openjdk

VOLUME /tmp

COPY target/*.jar app.jar

ENTRYPOINT ["java","-Djava.security.egd=file:/dev/./urandom","-jar","/app.jar"]

编译镜像

mvn package

docker build -t netkiller/docker .

% docker images | grep netkiller

netkiller/docker                           latest              ed359b6ffcad        16 seconds ago      105MB

% docker run -ti --entrypoint /bin/sh netkiller/docker

sh-4.2# ls

app.jar  bin  boot  dev  etc  home  lib  lib64 media  mnt  opt  proc  root  run  sbin srv  sys  tmp  usr  var

sh-4.2#

启动镜像测试

docker run -p 8080:8080 netkiller/docker

neo@MacBook-Pro ~ % curl http://localhost:8080

Hello Docker World

Dockerfile 放在 src/main/docker/Dockerfile 下

% cat src/main/docker/Dockerfile

FROM openjdk

VOLUME /tmp

COPY target/*.jar app.jar

ENTRYPOINT ["java","-Djava.security.egd=file:/dev/./urandom","-jar","/app.jar"]

mvn package

docker rmi netkiller/docker -f

docker build -t netkiller/docker -f src/main/docker/Dockerfile .

docker run -p 8080:8080 netkiller/docker

neo@MacBook-Pro ~ % curl http://localhost:8080

Hello Docker World

通过参数指定 Springboot 文件

% cat src/main/docker/Dockerfile

FROM openjdk:8-jdk-alpine

VOLUME /tmp

ARG JAR_FILE

COPY ${JAR_FILE} app.jar

ENTRYPOINT ["java","-Djava.security.egd=file:/dev/./urandom","-jar","/app.jar"]

mvn package

docker rmi netkiller/docker -f

docker build --build-arg JAR_FILE=target/*.jar -t netkiller/docker -f src/main/docker/Dockerfile .

docker run -p 8080:8080 netkiller/docker

SPRING_PROFILES_ACTIVE 指定配置文件

% docker run -e "SPRING_PROFILES_ACTIVE=prod" -p 8080:8080 netkiller/docker

推送镜像到仓库

neo@MacBook-Pro ~ % docker push netkiller/docker

The push refers to repository [docker.io/netkiller/docker]

100ff47f36fe: Pushed

a7aafc769de1: Mounted from library/openjdk

2666aafcfdd9: Mounted from library/openjdk

c4a7cf6a6169: Mounted from library/openjdk

latest: digest: sha256:3078fea95c633f007be33b829efae0ff8e9d78ad463925af7d07752c95eb43a3 size: 1165

通过 Maven 构建 Docker 镜像

Maven + Dockerfile 方案一

项目地址 https://github.com/spotify/dockerfile-maven

<build>

<plugins>

<plugin>

<groupId>org.springframework.boot</groupId>

<artifactId>spring-boot-maven-plugin</artifactId>

<configuration>

<mainClass>cn.netkiller.docker.Application</mainClass>

</configuration>

</plugin>

<plugin>

<groupId>com.spotify</groupId>

<artifactId>dockerfile-maven-plugin</artifactId>

<version>1.4.10</version>

<executions>

<execution>

<id>default</id>

<goals>

<goal>build</goal>

<goal>push</goal>

</goals>

</execution>

</executions>

<configuration>

<dockerfile>${project.basedir}/src/main/docker/Dockerfile</dockerfile>

<repository>${docker.image.prefix}/${project.artifactId}</repository>

<tag>${project.version}</tag>

<buildArgs>

<JAR_FILE>target/${project.build.finalName}.jar</JAR_FILE>

</buildArgs>

<resources>

<resource>

<targetPath>/</targetPath>

<directory>${project.build.directory}</directory>

<include>${project.build.finalName}.jar</include>

</resource>

</resources>

</configuration>

</plugin>

</plugins>

</build>

neo@MacBook-Pro ~/git/springcloud/docker % mvn dockerfile:build

[INFO] Scanning for projects...

[INFO]

[INFO] ------------------------< cn.netkiller:docker >-------------------------

[INFO] Building docker 0.0.1-SNAPSHOT

[INFO] --------------------------------[ jar ]---------------------------------

[INFO]

[INFO] --- dockerfile-maven-plugin:1.4.10:build (default-cli) @ docker ---

[INFO] dockerfile: /Users/neo/git/springcloud/docker/src/main/docker/Dockerfile

[INFO] contextDirectory: /Users/neo/git/springcloud/docker

[INFO] Building Docker context /Users/neo/git/springcloud/docker

[INFO] Path(dockerfile): /Users/neo/git/springcloud/docker/src/main/docker/Dockerfile

[INFO] Path(contextDirectory): /Users/neo/git/springcloud/docker

[INFO]

[INFO] Image will be built as netkiller/docker:0.0.1-SNAPSHOT

[INFO]

[INFO] Step 1/7 : FROM openjdk

[INFO]

[INFO] Pulling from library/openjdk

[INFO] Digest: sha256:38ec2c78a60ec4d5773c93534e433237be154ff5afa476965a68837b43ef2f19

[INFO] Status: Image is up to date for openjdk:latest

[INFO]  ---> b697a97ee8e1

[INFO] Step 2/7 : MAINTAINER Netkiller <netkiller@msn.com>

[INFO]

[INFO]  ---> Using cache

[INFO]  ---> e6fd68ec1ce8

[INFO] Step 3/7 : VOLUME /tmp

[INFO]

[INFO]  ---> Using cache

[INFO]  ---> 78b146e1a8a0

[INFO] Step 4/7 : ARG JAR_FILE

[INFO]

[INFO]  ---> Using cache

[INFO]  ---> 2c60b65d49dc

[INFO] Step 5/7 : COPY ${JAR_FILE} app.jar

[INFO]

[INFO]  ---> Using cache

[INFO]  ---> 3186f0425f1d

[INFO] Step 6/7 : CMD ["java", "-version"]

[INFO]

[INFO]  ---> Using cache

[INFO]  ---> d14b8d6360fe

[INFO] Step 7/7 : ENTRYPOINT ["java","-Djava.security.egd=file:/dev/./urandom","-jar","/app.jar"]

[INFO]

[INFO]  ---> Using cache

[INFO]  ---> 68e424cf5eab

[INFO] Successfully built 68e424cf5eab

[INFO] Successfully tagged netkiller/docker:0.0.1-SNAPSHOT

[INFO]

[INFO] Detected build of image with id 68e424cf5eab

[INFO] Building jar: /Users/neo/git/springcloud/docker/target/docker-0.0.1-SNAPSHOT-docker-info.jar

[INFO] Successfully built netkiller/docker:0.0.1-SNAPSHOT

[INFO] ------------------------------------------------------------------------

[INFO] BUILD SUCCESS

[INFO] ------------------------------------------------------------------------

[INFO] Total time:  9.413 s

[INFO] Finished at: 2019-04-13T05:39:07+08:00

[INFO] ------------------------------------------------------------------------

Maven + Dockerfile 方案二

<build>

<plugins>

...

<plugin>

<groupId>com.spotify</groupId>

<artifactId>docker-maven-plugin</artifactId>

<version>VERSION GOES HERE</version>

<configuration>

<imageName>example</imageName>

<dockerDirectory>docker</dockerDirectory>

<resources>

<resource>

<targetPath>/</targetPath>

<directory>${project.build.directory}</directory>

<include>${project.build.finalName}.jar</include>

</resource>

</resources>

</configuration>

</plugin>

...

</plugins>

</build>

Maven 不使用 Dockerfile 文件

项目地址 https://github.com/spotify/docker-maven-plugin

<plugin>

<groupId>com.spotify</groupId>

<artifactId>docker-maven-plugin</artifactId>

<version>1.2.0</version>

<configuration>

<imageName>${docker.image.prefix}/${project.artifactId}</imageName>

<baseImage>openjdk</baseImage>

<tag>${project.version}</tag>

<maintainer>${docker.maintainer}</maintainer>

<volumes>/tmp</volumes>

<workdir>/</workdir>

<cmd>["java", "-version"]</cmd>

<entryPoint>["java", "-Djava.security.egd=file:/dev/./urandom", "-jar", "/${project.build.finalName}.jar"]</entryPoint>

<!-- copy the service's jar file from target into the root directory of the image -->

<resources>

<resource>

<targetPath>/</targetPath>

<directory>${project.build.directory}</directory>

<include>${project.build.finalName}.jar</include>

</resource>

</resources>

</configuration>

</plugin>

构建镜像 mvn clean package docker:build

neo@MacBook-Pro ~/git/springcloud/webflux % mvn docker:build

[INFO] Scanning for projects...

[INFO]

[INFO] ------------------------< cn.netkiller:webflux >------------------------

[INFO] Building webflux 0.0.1-SNAPSHOT

[INFO] --------------------------------[ jar ]---------------------------------

[INFO]

[INFO] --- docker-maven-plugin:1.2.0:build (default-cli) @ webflux ---

[INFO] Using authentication suppliers: [ConfigFileRegistryAuthSupplier]

[INFO] Copying /Users/neo/git/springcloud/webflux/target/webflux-0.0.1-SNAPSHOT.jar -> /Users/neo/git/springcloud/webflux/target/docker/webflux-0.0.1-SNAPSHOT.jar

[INFO] Building image netkiller/webflux

Step 1/7 : FROM openjdk

---> b697a97ee8e1

Step 2/7 : MAINTAINER netkiller

---> Using cache

---> c275f5dc2815

Step 3/7 : WORKDIR /

---> Using cache

---> 27815e0b4455

Step 4/7 : ADD /webflux-0.0.1-SNAPSHOT.jar //

---> Using cache

---> 78b0fe2a827d

Step 5/7 : ENTRYPOINT ["java", "-Djava.security.egd=file:/dev/./urandom", "-jar", "/webflux-0.0.1-SNAPSHOT.jar"]

---> Using cache

---> 66d5499c8ba3

Step 6/7 : CMD ["java", "-version"]

---> Using cache

---> 080a1468d88b

Step 7/7 : VOLUME /tmp

---> Using cache

---> 60debfac7b7c

ProgressMessage{id=null, status=null, stream=null, error=null, progress=null, progressDetail=null}

Successfully built 60debfac7b7c

Successfully tagged netkiller/webflux:latest

[INFO] Built netkiller/webflux

[INFO] ------------------------------------------------------------------------

[INFO] BUILD SUCCESS

[INFO] ------------------------------------------------------------------------

[INFO] Total time:  4.485 s

[INFO] Finished at: 2019-04-13T05:41:41+08:00

[INFO] ------------------------------------------------------------------------

推送镜像

neo@MacBook-Pro ~ % vim /usr/local/Cellar/maven/3.6.0/libexec/conf/settings.xml

<!-- servers

| This is a list of authentication profiles, keyed by the server-id used within the system.

| Authentication profiles can be used whenever maven must make a connection to a remote server.

|-->

<servers>

<!-- server

| Specifies the authentication information to use when connecting to a particular server, identified by

| a unique name within the system (referred to by the 'id' attribute below).

|

| NOTE: You should either specify username/password OR privateKey/passphrase, since these pairings are

|       used together.

|

<server>

<id>deploymentRepo</id>

<username>repouser</username>

<password>repopwd</password>

</server>

-->

<!-- Another sample, using keys to authenticate.

<server>

<id>siteServer</id>

<privateKey>/path/to/private/key</privateKey>

<passphrase>optional; leave empty if not used.</passphrase>

</server>

-->

<server>

<id>docker-hub</id>

<username>netkiller</username>

<password>******</password>

<configuration>

<email>netkiller@msn.com</email>

</configuration>

</server>

</servers>

****** 修改为你的密码

查看 Docker Registry 地址

neo@MacBook-Pro ~ % docker info | grep Registry

Registry: https://index.docker.io/v1/

maven docker 插件配置

<plugin>

<groupId>com.spotify</groupId>

<artifactId>docker-maven-plugin</artifactId>

<version>1.2.0</version>

<configuration>

<imageName>${docker.image.prefix}/${project.artifactId}</imageName>

<baseImage>openjdk</baseImage>

<tag>${project.version}</tag>

<maintainer>${docker.maintainer}</maintainer>

<volumes>/tmp</volumes>

<workdir>/srv</workdir>

<cmd>["java", "-version"]</cmd>

<entryPoint>["java", "-Djava.security.egd=file:/dev/./urandom", "-jar", "/srv/${project.build.finalName}.jar"]</entryPoint>

<!-- copy the service's jar file from target into the root directory of the image -->

<resources>

<resource>

<targetPath>/</targetPath>

<directory>${project.build.directory}</directory>

<include>${project.build.finalName}.jar</include>

</resource>

</resources>

<image>${docker.image.prefix}/${project.artifactId}</image>

<newName>${docker.image.prefix}/${project.artifactId}:${project.version}</newName>

<serverId>docker-hub</serverId>

<registryUrl>https://index.docker.io/v1/</registryUrl>

</configuration>

</plugin>

docker:build -DpushImage or docker:push

使用加密的密码

neo@MacBook-Pro ~ % mvn --encrypt-master-password

Master password:

{r7kkN/XCOXYHqwRqE30k6Bz+pNGsB7/UogGTqqo+G2A=}

vim /usr/local/Cellar/maven/3.6.0/libexec/conf/settings.xml

<servers>

<server>

<id>docker-hub</id>

<username>netkiller</username>

<password>{r7kkN/XCOXYHqwRqE30k6Bz+pNGsB7/UogGTqqo+G2A=}</password>

</server>

</servers>

vim ~/.m2/settings-security.xml

<settingsSecurity>

<master>{r7kkN/XCOXYHqwRqE30k6Bz+pNGsB7/UogGTqqo+G2A=}</master>

</settingsSecurity>

[ERROR] No plugin found for prefix 'dockerfile' in the current project and in the plugin groups [org.apache.maven.plugins, org.codehaus.mojo] available from the repositories [local (/Users/neo/.m2/repository), central (https://repo.maven.apache.org/maven2)] -> [Help 1]

在maven的conf/setting.xml中要加入:

neo@MacBook-Pro ~ % mvn -version

Apache Maven 3.6.0 (97c98ec64a1fdfee7767ce5ffb20918da4f719f3; 2018-10-25T02:41:47+08:00)

Maven home: /usr/local/Cellar/maven/3.6.0/libexec

Java version: 12, vendor: Oracle Corporation, runtime: /Library/Java/JavaVirtualMachines/jdk-12.jdk/Contents/Home

Default locale: en_CN, platform encoding: UTF-8

OS name: "mac os x", version: "10.14.5", arch: "x86_64", family: "mac"

vim /usr/local/Cellar/maven/3.6.0/libexec/conf/settings.xml

<pluginGroups>

<pluginGroup>com.spotify</pluginGroup>

</pluginGroups>

curl: (35) LibreSSL SSL_connect: SSL_ERROR_SYSCALL in connection to localhost:8888

iMac:config neo$ curl -k -i -H HOST:sss https://config:s3cr3t@localhost:8888/netkiller-dev.json

curl: (35) LibreSSL SSL_connect: SSL_ERROR_SYSCALL in connection to localhost:8888

检查发现 8888 端口已经启动，SSL证书读不到

iMac:config neo$ openssl s_client -connect localhost:8888

CONNECTED(00000005)

140735970464712:error:140790E5:SSL routines:SSL23_WRITE:ssl handshake failure:/BuildRoot/Library/Caches/com.apple.xbs/Sources/libressl/libressl-22.50.3/libressl/ssl/s23_lib.c:124:

---

no peer certificate available

---

No client certificate CA names sent

---

SSL handshake has read 0 bytes and written 318 bytes

---

New, (NONE), Cipher is (NONE)

Secure Renegotiation IS NOT supported

Compression: NONE

Expansion: NONE

No ALPN negotiated

---

我开始怀疑是泛域名问题

keytool -genkey -alias *.netkiller.cn -storetype PKCS12 -keyalg RSA -keysize 2048 -storepass passw0rd -keystore allhost.p12 -dname "CN=*.netkiller.cn, OU=netkiller, O=netkiller.cn, L=Guangdong, ST=Shenzhen, C=CN"

keytool -selfcert -alias *.netkiller.cn -storepass passw0rd -keystore allhost.p12

测试后发现跟证书无关。

经过曲折的排查发现绑定了地址，在本地启动是正常的，一旦放入 Docker 容器就无法工作。

#server.address=localhost

server.port=8888

server.ssl.enabled=true

server.ssl.key-store-type=PKCS12

server.ssl.key-store=classpath:localhost.p12

server.ssl.key-store-password=123456

#server.ssl.key-store=classpath:allhost.p12

#server.ssl.key-store-password=passw0rd

server.http2.enabled=true

#logging.file=target/spring.log

spring.application.name=config-server

spring.profiles.active=native

spring.security.user.name=config

spring.security.user.password=s3cr3t

#spring.cloud.config.server.git.uri=/opt/config

spring.cloud.config.server.native.search-locations=classpath:/shared

去掉 server.address=localhost 即可，在 build docker 镜像，然后启动容器。可以正常获取证书

iMac:config neo$ openssl s_client -connect localhost:8888

CONNECTED(00000005)

depth=0 C = CN, ST = Shenzhen, L = Guangdong, O = netkiller.cn, OU = netkiller, CN = localhost

verify error:num=18:self signed certificate

verify return:1

depth=0 C = CN, ST = Shenzhen, L = Guangdong, O = netkiller.cn, OU = netkiller, CN = localhost

verify return:1

---

Certificate chain

0 s:/C=CN/ST=Shenzhen/L=Guangdong/O=netkiller.cn/OU=netkiller/CN=localhost

i:/C=CN/ST=Shenzhen/L=Guangdong/O=netkiller.cn/OU=netkiller/CN=localhost

---

Server certificate

-----BEGIN CERTIFICATE-----

MIIDijCCAnKgAwIBAgIJAP/SjXit0rVsMA0GCSqGSIb3DQEBCwUAMHMxCzAJBgNV

BAYTAkNOMREwDwYDVQQIEwhTaGVuemhlbjESMBAGA1UEBxMJR3Vhbmdkb25nMRUw

EwYDVQQKEwxuZXRraWxsZXIuY24xEjAQBgNVBAsTCW5ldGtpbGxlcjESMBAGA1UE

AxMJbG9jYWxob3N0MB4XDTIwMDkwNzA4NTUzOVoXDTIwMTIwNjA4NTUzOVowczEL

MAkGA1UEBhMCQ04xETAPBgNVBAgTCFNoZW56aGVuMRIwEAYDVQQHEwlHdWFuZ2Rv

bmcxFTATBgNVBAoTDG5ldGtpbGxlci5jbjESMBAGA1UECxMJbmV0a2lsbGVyMRIw

EAYDVQQDEwlsb2NhbGhvc3QwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIB

AQCTZUtf/siYQr3MBstphQsBceRxvflDm2C4ztZ8OemDzH2avhI7edD6rzrJQ0V2

1n1XlTRgwoYqoTgeIdQ1DbzgrCliBYy+3E9vcp8WWzYz9o2YZRphYUr37iWonP+b

ZkLqzmRLuASRNZ8sBrwD7Mvs5IXfJZQ8wruO0V4oJQ5NOzcxDmbA0WGJn/0QZDKN

/tR7Rw3g9B96fFYGI/T7g4nuteEiUqQ9GJ1gx3utBd31Z1m8cV59ZsWd+Y2P14LO

W+YxkpB56OZKWWr1ExxQdZmLIME+D0d40M8At6rCAvclMKa7dva6+ZRxPlizVkQS

L4JNT1WOMtVaUUhFX5xlhsBtAgMBAAGjITAfMB0GA1UdDgQWBBROMJrswZ37wsxV

sm0N9AHOE8ZiODANBgkqhkiG9w0BAQsFAAOCAQEAPIgc6ZcQueQTEym36gx2IRWT

wLVQEabyS4/xeu89aRfbGDOavBajNwStqGdWUE8PRb95bhfvziZ61c6gBO9IE23j

GOmIQTW5RvZL6HLJgqR3LngZUiV/Ugwuno5Uo8IN25duq993tNmdCG8YeBtfuy/j

OFRrn96OT/Trj04NfYmC7nqBThyNmLPY5Oeo0XkhIAqqcLJE8/SJ9zd16vmgVhPM

UlsFJcZoL1uhbNXQuLPv8id8tntH+Lli39RVwd56CgTW7k9YFfFNV0mCeWBsAYl3

74R8l4ClV15o31wH/qPLg0F6uE/M/xsz56WIu2e5Oa30issz0DjYrG9GiQ2kDA==

-----END CERTIFICATE-----

subject=/C=CN/ST=Shenzhen/L=Guangdong/O=netkiller.cn/OU=netkiller/CN=localhost

issuer=/C=CN/ST=Shenzhen/L=Guangdong/O=netkiller.cn/OU=netkiller/CN=localhost

---

No client certificate CA names sent

---

SSL handshake has read 2631 bytes and written 512 bytes

---

New, TLSv1/SSLv3, Cipher is ECDHE-RSA-AES256-GCM-SHA384

Server public key is 2048 bit

Secure Renegotiation IS supported

Compression: NONE

Expansion: NONE

No ALPN negotiated

SSL-Session:

Protocol  : TLSv1.2

Cipher    : ECDHE-RSA-AES256-GCM-SHA384

Session-ID: 856BA1E0CFE8AC65AEC838C0A4DA0503C7A05F0BA803B127D3B1EBBB8FF1A344

Session-ID-ctx:

Master-Key: 2DCA2747330C8008958B1A4F3EF340044FE69455EA730DA0E30DF97A13E6EB7BCABDFDF5CA0FA5B278701EA25D694CAB

TLS session ticket lifetime hint: 86400 (seconds)

TLS session ticket:

0000 - 93 c1 d2 63 4d ef 37 a9-47 d1 72 2e ee 07 5a e2   ...cM.7.G.r...Z.

0010 - b7 40 aa 89 db 70 64 88-86 ad 65 2e e9 f8 2a de   .@...pd...e...*.

0020 - 02 03 7f d3 5d 22 c2 e1-48 5a 43 59 7d 0f ef cc   ....]"..HZCY}...

0030 - cc fa 08 f9 bd 23 70 bb-82 8b d8 29 c8 42 e8 ed   .....#p....).B..

0040 - 12 6d ae 99 c8 74 c0 87-d9 a0 c0 27 ae 92 d9 71   .m...t.....'...q

0050 - ab 14 da d1 c6 9f 6f ba-7b 2f 6a 39 af c3 81 09   ......o.{/j9....

0060 - bd 8a ac 55 d0 9f e4 32-d7 a6 1f 10 29 0d 07 f0   ...U...2....)...

0070 - 09 d2 54 35 a8 d5 9e 9c-e1 5b 7b dd cc de eb 2a   ..T5.....[{....*

0080 - 94 f9 56 41 df 14 85 37-b3 c1 28 be fe 1b ae 64   ..VA...7..(....d

0090 - 68 c9 b3 12 8b 78 28 d4-16 f3 28 3e 0e c3 e2 e3   h....x(...(>....

00a0 - 0d d5 42 46 37 3a 62 11-38 d4 68 59 77 01 2f 12   ..BF7:b.8.hYw./.

00b0 - 29 b1 3f ab 3d c2 0b be-f0 df 87 43 ae 89 99 35   ).?.=......C...5

00c0 - 19 eb fc 00 38 fa cc 5e-bb 0c 81 7f ae ee 8f 0e   ....8..^........

00d0 - c5 82 00 4f bc f4 c6 a7-b0 3e 27 a8 0a 7e 57 a0   ...O.....>'..~W.

00e0 - b8 c9 4a 04 49 61 db 62-cd bc a2 3d c4 32 a0 74   ..J.Ia.b...=.2.t

00f0 - 11 0a ee c0 99 58 7a ce-99 30 7f a2 90 a0 50 30   .....Xz..0....P0

0100 - fe df 5e 57 d5 e3 fb 6f-20 64 eb 8e ef da 95 6b   ..^W...o d.....k

0110 - 5c 20 38 62 75 5b d0 b6-4a 38 12 4b 8e be 6c 03   \ 8bu[..J8.K..l.

0120 - 14 b1 e9 05 cf b7 8c 12-e4 b6 2e 84 c3 14 57 4b   ..............WK

0130 - 56 a6 47 f6 2f 06 81 12-a5 d8 88 8e 2f dd 40 43   V.G./......./.@C

0140 - 31 c3 0b 85 7d 26 ef b2-4d 9d aa 40 f4 e4 1c bd   1...}&..M..@....

0150 - 03 8e 61 b6 da d0 05 49-32 7a 26 44 7c 8e 69 c5   ..a....I2z&D|.i.

0160 - 9c 41 30 e3 0f 08 8f 57-1e 70 13 ff c2 cc f2 53   .A0....W.p.....S

0170 - 44 ed d2 9f c0 1c 5a 49-1a e3 88 94 84 15 7d c1   D.....ZI......}.

0180 - a7 e5 fc 39 70 92 c1 6f-77 64 dc 93 aa af 81 ad   ...9p..owd......

0190 - 64 50 c6 f9 3e da 4f 62-60 21 df 78 98 ca 78 6e   dP..>.Ob`!.x..xn

01a0 - d0 43 14 12 54 ae 4b e0-f4 4b 70 06 1e 26 6a 17   .C..T.K..Kp..&j.

01b0 - af b2 7c 76 75 ce 4f 60-79 5d a8 4d 8f e7 22 75   ..|vu.O`y].M.."u

01c0 - 5b 65 db 42 5e b5 c0 05-9e ef f1 38 e4 e8 b0 a2   [e.B^......8....

01d0 - 89 60 fa 43 18 e3 89 e9-4d d2 52 87 8c a3 73 16   .`.C....M.R...s.

01e0 - f6 9b d4 0f 72 b3 22 e1-86 87 b1 85 c4 b0 b6 36   ....r."........6

01f0 - 1f 83 1f 87 76 28 20 9f-64 ca f0 1e 11 da 0b bf   ....v( .d.......

0200 - 75 df a9 77 48 84 6d a1-5e 2d 3c f7 d6 df 3e d8   u..wH.m.^-<...>.

0210 - 6e 18 6f 53 eb c1 86 9e-cb a8 e1 19 e7 f4 5c b9   n.oS..........\.

0220 - 58 c9 d4 38 b1 4a 3b ff-a0 16 34 2f 69 67 28 b4   X..8.J;...4/ig(.

0230 - e9 72 f8 97 75 6d a0 15-5c 16 cf 28 33 2f c1 37   .r..um..\..(3/.7

0240 - ca 09 07 2b 5f 5f e7 6b-94 19 9c 95 5c 2c d1 54   ...+__.k....\,.T

0250 - 69 3f cd d5 63 9f 75 6c-26 53 cd 57 3a 9b 7b 02   i?..c.ul&S.W:.{.

0260 - 6e 79 5c e5 36 9d 90 1a-d2 8a 0b b2 6f 03 5a fd   ny\.6.......o.Z.

0270 - b0 3b d1 b8 68 be 1f 99-05 e2 52 a5 96 99 bd bf   .;..h.....R.....

0280 - bd 84 06 b9 ed fb bb 2e-fd 9b 14 1b ca 7c 07 eb   .............|..

0290 - a6 ff 07 ce d3 6b 48 26-b2 f0 67 c2 96 6d 4b 00   .....kH&..g..mK.

02a0 - 77 d3 59 e0 fc 48 19 29-23 1a 9a 30 b6 3f 2a 12   w.Y..H.)#..0.?*.

02b0 - 80 b4 f7 5e 33 85 42 da-c2 b9 42 dd 30 73 f1 15   ...^3.B...B.0s..

02c0 - f2 16 49 f7 24 39 77 61-e4 90 7c 32 f1 e9 0e fb   ..I.$9wa..|2....

02d0 - 7b a7 02 db 91 3a 16 8c-85 d2 2a 38 ad 3c a8 a9   {....:....*8.<..

02e0 - 0b a8 3f 5b 49 92 de 45-41 74 60 dd 41 66 8f ac   ..?[I..EAt`.Af..

02f0 - d2 23 60 25 99 6f 73 8b-8c f1 88 6c 67 36 b7 e0   .#`%.os....lg6..

0300 - 60 d1 2a 77 b4 3e 29 bb-90 dc 7f f2 30 2e e7 de   `.*w.>).....0...

0310 - dd 48 f6 dc 59 30 89 fe-1f 90 ac a6 10 42 96 ab   .H..Y0.......B..

0320 - a7 84 34 2c 2e 54 d1 1b-65 48 a9 47 63 3f ff 2a   ..4,.T..eH.Gc?.*

0330 - a1 66 b7 6d d6 f7 d3 11-d3 6a 21 33 a4 99 5c a4   .f.m.....j!3..\.

0340 - e3 a1 b8 5a 1b 7a d9 45-89 fa 12 ee 5f 5b 69 6e   ...Z.z.E...._[in

0350 - 7b 77 ba c9 3a 3c 09 b0-db 16 ad ac 66 6e 36 5a   {w..:<......fn6Z

0360 - 48 c9 9a e7 6c a7 2f 10-31 33 9c 3f e1 18 9c af   H...l./.13.?....

0370 - dc a1 f9 26 50 2a 66 e8-62 da fb 51 ad dc d6 72   ...&P*f.b..Q...r

0380 - ca 53 4c 7b 72 e6 2b ee-f9 fd 97 f3 c4 67 dc c6   .SL{r.+......g..

0390 - f1 38 d1 58 d5 df 02 a5-1c f0 3d 5b 6d 01 be ff   .8.X......=[m...

03a0 - a7 d1 0b 68 04 22 2b ab-ee a6 0a c3 98 80 04 bf   ...h."+.........

03b0 - 99 8b 9b 67 6e d3 fc 25-ab 87 01 74 8c 29 c8 8b   ...gn..%...t.)..

03c0 - 10 f0 b5 24 a9 71 e9 66-a4 65 cf a8 ee 2f ab 4c   ...$.q.f.e.../.L

03d0 - 0a c0 08 87 1e 34 84 c1-a6 fe 7b 55 42 bb b2 0c   .....4....{UB...

03e0 - 46 c4 1a 77 df cb 9c 8f-9f de 9d 57 8a 5c e1 12   F..w.......W.\..

03f0 - 43 8e f3 fe 09 63 7f 47-c0 31 bc 51 f1 59 2e fb   C....c.G.1.Q.Y..

0400 - 89 f7 16 99 20 eb 52 e3-5f 11 70 4a c4 9e 19 5d   .... .R._.pJ...]

0410 - 29 11 23 f6 9b f9 d1 2f-6c f9 55 54 53 c5 65 6a   ).#..../l.UTS.ej

0420 - c7 b0 26 cc 42 b6 8d c3-19 d8 f0 57 7d 55 59 65   ..&.B......W}UYe

0430 - 6c 39 8c a0 69 51 d2 3d-d4 d4 71 c5 7f 6e eb f3   l9..iQ.=..q..n..

0440 - 46 45 2a 73 a6 1c cb ec-47 35 13 05 81 53 02 6f   FE*s....G5...S.o

0450 - f1 ae 8c 27 a2 b7 05 0d-e3 f9 20 46 1d 4a d6 ce   ...'...... F.J..

0460 - b6 19 72 0f 3f 60 1e 65-57 5c 55 a3 b5 4d f1 05   ..r.?`.eW\U..M..

0470 - 2b 41 a2 47 2e a9 63 42-be 37 e1 d2 28 92         +A.G..cB.7..(.

Start Time: 1600656460

Timeout   : 300 (sec)

Verify return code: 18 (self signed certificate)

---

closed

工作正常

iMac:config neo$ curl -k -i https://config:s3cr3t@192.168.3.85:8888/netkiller-dev.json

HTTP/2 200

set-cookie: JSESSIONID=75D0C2900D87C789DF596220FA77012D; Path=/; Secure; HttpOnly

x-content-type-options: nosniff

x-xss-protection: 1; mode=block

cache-control: no-cache, no-store, max-age=0, must-revalidate

pragma: no-cache

expires: 0

strict-transport-security: max-age=31536000 ; includeSubDomains

x-frame-options: DENY

content-type: application/json

content-length: 100

date: Mon, 21 Sep 2020 02:51:11 GMT

{"sms":{"gateway":{"url":"https://sms.netkiller.cn/v1","username":"netkiller","password":"123456"}}}

2. Spring boot with Docker stack

编译 Docker 镜像

iMac:config neo$ mvn docker:build

[INFO] Scanning for projects...

[INFO]

[INFO] ------------------------< cn.netkiller:config >-------------------------

[INFO] Building config 0.0.1-SNAPSHOT

[INFO] --------------------------------[ jar ]---------------------------------

[INFO]

[INFO] --- docker-maven-plugin:1.2.2:build (default-cli) @ config ---

[INFO] Using authentication suppliers: [ConfigFileRegistryAuthSupplier, FixedRegistryAuthSupplier]

[INFO] Copying /Users/neo/workspace/Microservice/config/target/config-0.0.1-SNAPSHOT.jar -> /Users/neo/workspace/Microservice/config/target/docker/srv/config-0.0.1-SNAPSHOT.jar

[INFO] Building image netkiller/config

Step 1/7 : FROM openjdk

---> b2324c52d969

Step 2/7 : WORKDIR /srv

---> Using cache

---> f7c1730935c6

Step 3/7 : ADD /srv/config-0.0.1-SNAPSHOT.jar /srv/

---> Using cache

---> 8b5a053550ba

Step 4/7 : EXPOSE 8888

---> Running in 7f4e35b3564f

Removing intermediate container 7f4e35b3564f

---> a968ea58ba64

Step 5/7 : ENTRYPOINT ["java", "-jar", "-Djava.security.egd=file:/dev/./urandom", "/srv/config-0.0.1-SNAPSHOT.jar"]

---> Running in 6b110b5d16b7

Removing intermediate container 6b110b5d16b7

---> a8ab10c1c186

Step 6/7 : CMD ["java", "-version"]

---> Running in 4f2dc6e08404

Removing intermediate container 4f2dc6e08404

---> a74bbf7b6c30

Step 7/7 : VOLUME /tmp

---> Running in 0a3836ea768f

Removing intermediate container 0a3836ea768f

---> 5e13d81a9dea

ProgressMessage{id=null, status=null, stream=null, error=null, progress=null, progressDetail=null}

Successfully built 5e13d81a9dea

Successfully tagged netkiller/config:latest

[INFO] Built netkiller/config

[INFO] Tagging netkiller/config with 0.0.1-SNAPSHOT

[INFO] Tagging netkiller/config with latest

[INFO] ------------------------------------------------------------------------

[INFO] BUILD SUCCESS

[INFO] ------------------------------------------------------------------------

[INFO] Total time:  20.516 s

[INFO] Finished at: 2020-09-20T21:49:28+08:00

[INFO] ------------------------------------------------------------------------

初始化 Swarm

iMac:springboot neo$ docker swarm init

Swarm initialized: current node (qvqez97c8ja014ktmroy9sw47) is now a manager.

To add a worker to this swarm, run the following command:

docker swarm join --token SWMTKN-1-49w6mcdjvj9nhb1go4wiazygupvj6qmjy7mgdb7x5bzqspldss-6yfvnij63it1qbs2nwvqw6xv0 192.168.65.3:2377

To add a manager to this swarm, run 'docker swarm join-token manager' and follow the instructions.

创建 docker-compose.yml 文件

version: '3.8'

services:

config:

image: netkiller/config:latest

ports:

- "8888"

volumes:

- /tmp/config:/tmp

deploy:

replicas: 1

restart_policy:

condition: on-failure

resources:

limits:

cpus: "0.1"

memory: 50M

部署服务

iMac:springboot neo$ docker stack deploy -c docker-compose.yml springboot

Creating network springboot_default

Creating service springboot_config

查看部署情况

iMac:springboot neo$ docker stack ls

NAME                SERVICES            ORCHESTRATOR

springboot          1                   Swarm

iMac:springboot neo$ docker stack services springboot

ID                  NAME                MODE                REPLICAS            IMAGE                     PORTS

viavpkzk6lvo        springboot_config   replicated          0/1                 netkiller/config:latest   *:30001->8888/tcp

查看服务运行状态

iMac:springboot neo$ docker stack ps springboot

ID                  NAME                  IMAGE                     NODE                DESIRED STATE       CURRENT STATE             ERROR               PORTS

mr30ujsdbti4        springboot_config.1   netkiller/config:latest   docker-desktop      Running             Preparing 4 minutes ago

3. Spring boot with Kubernetes

首先你需要构建 docker 镜像，并且 push 到 registry 参考这里

Kubernetes 编排脚本

创建密钥

kubectl create secret docker-registry docker-hub \

--docker-server=https://index.docker.io/v1/ \

--docker-username=netkiller \

--docker-password=passw0rd \

--docker-email=netkiller@msn.com

查看是否创建成功

iMac:spring neo$ kubectl get secret

NAME                  TYPE                                  DATA   AGE

default-token-fhfn8   kubernetes.io/service-account-token   3      2d23h

docker-hub            kubernetes.io/dockerconfigjson        1      15s

springboot.yml 编排脚本

apiVersion: v1

kind: Service

metadata:

name: springboot

namespace: default

labels:

app: springboot

spec:

type: NodePort

ports:

- port: 8888

nodePort: 30000

selector:

app: springboot

---

apiVersion: apps/v1

kind: Deployment

metadata:

name: springboot

spec:

replicas: 3

selector:

matchLabels:

app: springboot

template:

metadata:

labels:

app: springboot

spec:

containers:

- name: springboot

image: netkiller/config:latest

imagePullPolicy: IfNotPresent

ports:

- containerPort: 8888

imagePullSecrets:

- name: docker-hub

部署镜像

iMac:spring neo$ kubectl create -f springboot.yml

deployment.apps/springboot created

iMac:spring neo$ kubectl expose deployment springboot --type="LoadBalancer"

service/springboot exposed

iMac:spring neo$ minikube service list

|----------------------|---------------------------|--------------|---------------------------|

|      NAMESPACE       |           NAME            | TARGET PORT  |            URL            |

|----------------------|---------------------------|--------------|---------------------------|

| default              | kubernetes                | No node port |

| default              | springboot                |         8888 | http://192.168.64.2:30000 |

| kube-system          | kube-dns                  | No node port |

| kube-system          | registry                  | No node port |

| kubernetes-dashboard | dashboard-metrics-scraper | No node port |

| kubernetes-dashboard | kubernetes-dashboard      | No node port |

|----------------------|---------------------------|--------------|---------------------------|

iMac:spring neo$ minikube service springboot --url

http://192.168.64.2:30000

http://192.168.64.2:30000 是访问地址，Kubernetes 会负载均衡到后面的三个 pod 上。

iMac:config neo$ curl -k https://config:s3cr3t@192.168.64.2:30000/netkiller-dev.json

{"sms":{"gateway":{"url":"https://sms.netkiller.cn/v1","username":"netkiller","password":"123456"}}}

删除服务

iMac:spring neo$ kubectl delete -f springboot.yml

service "springboot" deleted

deployment.apps "springboot" deleted

第 29 章 Spring boot with command line

1. Maven

开发命令行程序通常我们不需要 Tomcat，所以不需要引入 spring-boot-starter-web 依赖，spring-boot-starter 依赖不含Tomcat。

<?xml version="1.0"?>

<project xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd" xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">

<modelVersion>4.0.0</modelVersion>

<parent>

<groupId>cn.netkiller</groupId>

<artifactId>parent</artifactId>

<version>0.0.1-SNAPSHOT</version>

</parent>

<groupId>cn.netkiller</groupId>

<artifactId>command</artifactId>

<version>0.0.1-SNAPSHOT</version>

<name>Command Line</name>

<url>http://maven.apache.org</url>

<properties>

<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>

</properties>

<dependencies>

<dependency>

<groupId>org.springframework.boot</groupId>

<artifactId>spring-boot-starter</artifactId>

</dependency>

<dependency>

<groupId>junit</groupId>

<artifactId>junit</artifactId>

<scope>test</scope>

</dependency>

</dependencies>

<build>

<plugins>

<plugin>

<groupId>org.springframework.boot</groupId>

<artifactId>spring-boot-maven-plugin</artifactId>

</plugin>

</plugins>

</build>

</project>

2. CommandLineRunner 例子

package cn.netkiller.cmd;

import org.slf4j.Logger;

import org.slf4j.LoggerFactory;

import org.springframework.boot.CommandLineRunner;

import org.springframework.boot.SpringApplication;

import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication

public class Application implements CommandLineRunner {

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

public static void main(String[] args) {

SpringApplication.run(Application.class, args);

}

@Override

public void run(String... args) throws Exception {

logger.info("服务已启动，执行command line runner。");

for (int i = 0; i < args.length; ++i) {

logger.info("args[{}]: {}", i, args[i]);

}

}

}

% java -jar target/command-0.0.1-SNAPSHOT.jar --host=ww.netkiller.cn java spring boot --help -v

3. ApplicationRunner 例子

package cn.netkiller.component;

import java.util.Arrays;

import org.slf4j.Logger;

import org.slf4j.LoggerFactory;

import org.springframework.beans.factory.annotation.Value;

import org.springframework.boot.ApplicationArguments;

import org.springframework.boot.ApplicationRunner;

import org.springframework.core.annotation.Order;

import org.springframework.core.io.ClassPathResource;

import org.springframework.core.io.Resource;

import org.springframework.stereotype.Component;

@Component

@Order(1)

public class Command implements ApplicationRunner {

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

@Override

public void run(ApplicationArguments args) throws Exception {

System.out.println("==ApplicationRunner=====" + Arrays.asList(args.getSourceArgs()));

System.out.println("==getOptionNames========" + args.getOptionNames());

System.out.println("==getOptionValues=======" + args.getOptionValues("foo"));

System.out.println("==getOptionValues=======" + args.getOptionValues("developer.name"));

// System.exit(0);

}

}

第 30 章 Spring Boot Actuator

健康检查、审计、统计和监控

1. Maven 依赖

<dependency>

<groupId>org.springframework.boot</groupId>

<artifactId>spring-boot-starter-actuator</artifactId>

</dependency>

2. 与 Spring Boot Actuator 有关的配置

application.properties

跨域配置

management.endpoints.web.cors.allowed-origins=https://example.com

management.endpoints.web.cors.allowed-methods=GET,POST

禁用HTTP端点

如果您不想通过HTTP公开端点，则可以将管理端口设置为-1，如以下示例所示：

management.server.port=-1

安全配置

security.basic.enabled=true

security.basic.path=/admin    #针对/admin路径进行认证

security.user.name=admin     #认证使用的用户名

security.user.password=password   #认证使用的密码

management.security.roles=SUPERUSER

management.port=11111   #actuator暴露接口使用的端口，为了和api接口使用的端口进行分离

management.context-path=/admin   #actuator暴露接口的前缀

management.security.enabled=true   #actuator是否需要安全保证

endpoints.metrics.sensitive=false   #actuator的metrics接口是否需要安全保证

endpoints.metrics.enabled=true

endpoints.health.sensitive=false  #actuator的health接口是否需要安全保证

endpoints.health.enabled=true

修改 actuator 地址

management:

endpoints:

web:

base-path: /monitor

关机

配置文件中加入

management.endpoint.shutdown.enabled=true

curl -X POST www.netkiller.cn:8080/actuator/shutdown

3. actuator 接口

常规接口

neo@MacBook-Pro ~ % curl -s http://www.netkiller.cn:8080/actuator | jq

{

"_links": {

"self": {

"href": "http://www.netkiller.cn:8080/actuator",

"templated": false

},

"health": {

"href": "http://www.netkiller.cn:8080/actuator/health",

"templated": false

},

"health-component": {

"href": "http://www.netkiller.cn:8080/actuator/health/{component}",

"templated": true

},

"health-component-instance": {

"href": "http://www.netkiller.cn:8080/actuator/health/{component}/{instance}",

"templated": true

},

"info": {

"href": "http://www.netkiller.cn:8080/actuator/info",

"templated": false

}

}

}

暴漏所有接口

management:

endpoints:

web:

exposure:

include: "*"

再次访问 /actuator

neo@MacBook-Pro-Neo ~/w/Architect (master)> curl -s https://www.netkiller.cn/actuator/ | jq

{

"_links": {

"self": {

"href": "http://pre.ejiayou.com/ensd-channel-service/monitor",

"templated": false

},

"archaius": {

"href": "https://www.netkiller.cn/actuator/archaius",

"templated": false

},

"nacosconfig": {

"href": "https://www.netkiller.cn/actuator/nacosconfig",

"templated": false

},

"nacosdiscovery": {

"href": "https://www.netkiller.cn/actuator/nacosdiscovery",

"templated": false

},

"auditevents": {

"href": "https://www.netkiller.cn/actuator/auditevents",

"templated": false

},

"beans": {

"href": "https://www.netkiller.cn/actuator/beans",

"templated": false

},

"caches-cache": {

"href": "https://www.netkiller.cn/actuator/caches/{cache}",

"templated": true

},

"caches": {

"href": "https://www.netkiller.cn/actuator/caches",

"templated": false

},

"health-component": {

"href": "https://www.netkiller.cn/actuator/health/{component}",

"templated": true

},

"health": {

"href": "https://www.netkiller.cn/actuator/health",

"templated": false

},

"health-component-instance": {

"href": "https://www.netkiller.cn/actuator/health/{component}/{instance}",

"templated": true

},

"conditions": {

"href": "https://www.netkiller.cn/actuator/conditions",

"templated": false

},

"configprops": {

"href": "https://www.netkiller.cn/actuator/configprops",

"templated": false

},

"env-toMatch": {

"href": "https://www.netkiller.cn/actuator/env/{toMatch}",

"templated": true

},

"env": {

"href": "https://www.netkiller.cn/actuator/env",

"templated": false

},

"info": {

"href": "https://www.netkiller.cn/actuator/info",

"templated": false

},

"loggers-name": {

"href": "https://www.netkiller.cn/actuator/loggers/{name}",

"templated": true

},

"loggers": {

"href": "https://www.netkiller.cn/actuator/loggers",

"templated": false

},

"heapdump": {

"href": "https://www.netkiller.cn/actuator/heapdump",

"templated": false

},

"threaddump": {

"href": "https://www.netkiller.cn/actuator/threaddump",

"templated": false

},

"metrics": {

"href": "https://www.netkiller.cn/actuator/metrics",

"templated": false

},

"metrics-requiredMetricName": {

"href": "https://www.netkiller.cn/actuator/metrics/{requiredMetricName}",

"templated": true

},

"scheduledtasks": {

"href": "https://www.netkiller.cn/actuator/scheduledtasks",

"templated": false

},

"httptrace": {

"href": "https://www.netkiller.cn/actuator/httptrace",

"templated": false

},

"mappings": {

"href": "https://www.netkiller.cn/actuator/mappings",

"templated": false

},

"refresh": {

"href": "https://www.netkiller.cn/actuator/refresh",

"templated": false

},

"features": {

"href": "https://www.netkiller.cn/actuator/features",

"templated": false

},

"service-registry": {

"href": "https://www.netkiller.cn/actuator/service-registry",

"templated": false

}

}

}

Spring boot 2.3.0

neo@MacBook-Pro-M2 ~ % curl -s http://www.netkiller.cn:8080/actuator |jq

{

"_links": {

"self": {

"href": "http://www.netkiller.cn:8080/actuator",

"templated": false

},

"beans": {

"href": "http://www.netkiller.cn:8080/actuator/beans",

"templated": false

},

"caches-cache": {

"href": "http://www.netkiller.cn:8080/actuator/caches/{cache}",

"templated": true

},

"caches": {

"href": "http://www.netkiller.cn:8080/actuator/caches",

"templated": false

},

"health": {

"href": "http://www.netkiller.cn:8080/actuator/health",

"templated": false

},

"health-path": {

"href": "http://www.netkiller.cn:8080/actuator/health/{*path}",

"templated": true

},

"info": {

"href": "http://www.netkiller.cn:8080/actuator/info",

"templated": false

},

"conditions": {

"href": "http://www.netkiller.cn:8080/actuator/conditions",

"templated": false

},

"configprops": {

"href": "http://www.netkiller.cn:8080/actuator/configprops",

"templated": false

},

"configprops-prefix": {

"href": "http://www.netkiller.cn:8080/actuator/configprops/{prefix}",

"templated": true

},

"env": {

"href": "http://www.netkiller.cn:8080/actuator/env",

"templated": false

},

"env-toMatch": {

"href": "http://www.netkiller.cn:8080/actuator/env/{toMatch}",

"templated": true

},

"logfile": {

"href": "http://www.netkiller.cn:8080/actuator/logfile",

"templated": false

},

"loggers": {

"href": "http://www.netkiller.cn:8080/actuator/loggers",

"templated": false

},

"loggers-name": {

"href": "http://www.netkiller.cn:8080/actuator/loggers/{name}",

"templated": true

},

"heapdump": {

"href": "http://www.netkiller.cn:8080/actuator/heapdump",

"templated": false

},

"threaddump": {

"href": "http://www.netkiller.cn:8080/actuator/threaddump",

"templated": false

},

"metrics-requiredMetricName": {

"href": "http://www.netkiller.cn:8080/actuator/metrics/{requiredMetricName}",

"templated": true

},

"metrics": {

"href": "http://www.netkiller.cn:8080/actuator/metrics",

"templated": false

},

"scheduledtasks": {

"href": "http://www.netkiller.cn:8080/actuator/scheduledtasks",

"templated": false

},

"mappings": {

"href": "http://www.netkiller.cn:8080/actuator/mappings",

"templated": false

}

}

}

4. 健康状态

curl www.netkiller.cn:8080/actuator/health

neo@MacBook-Pro ~ % curl -s http://www.netkiller.cn:8080/actuator/health | jq

{

"status": "UP"

}

健康状态

详细的健康状态信息

management.endpoint.health.show-details=always

neo@MacBook-Pro ~ % curl -s http://www.netkiller.cn:8080/actuator/health | jq

{

"status": "UP",

"details": {

"diskSpace": {

"status": "UP",

"details": {

"total": 250790436864,

"free": 23556112384,

"threshold": 10485760

}

}

}

}

5. info 配置信息

返回 application.properties 文件中定义的 info 配置信息，如:

# info端点信息配置

info.app.name=spring-boot-example

info.app.version=v1.0.0

neo@MacBook-Pro ~ % curl -s http://www.netkiller.cn:8080/actuator/info | jq

{

"app": {

"name": "spring-boot-example",

"version": "v1.0.0"

}

}

6. beans 信息

neo@MacBook-Pro-M2 ~ % curl -s http://www.netkiller.cn:8080/actuator/beans |jq

{

"contexts": {

"watch-production": {

"beans": {

"spring.jpa-org.springframework.boot.autoconfigure.orm.jpa.JpaProperties": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.boot.autoconfigure.orm.jpa.JpaProperties",

"dependencies": []

},

"applicationTaskExecutor": {

"aliases": [

"taskExecutor"

],

"scope": "singleton",

"type": "org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor",

"resource": "class path resource [org/springframework/boot/autoconfigure/task/TaskExecutorConfigurations$TaskExecutorConfiguration.class]",

"dependencies": [

"org.springframework.boot.autoconfigure.task.TaskExecutorConfigurations$TaskExecutorConfiguration",

"taskExecutorBuilder"

]

},

"healthEndpointGroups": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.boot.actuate.autoconfigure.health.AutoConfiguredHealthEndpointGroups",

"resource": "class path resource [org/springframework/boot/actuate/autoconfigure/health/HealthEndpointConfiguration.class]",

"dependencies": [

"org.springframework.boot.actuate.autoconfigure.health.HealthEndpointConfiguration",

"org.springframework.boot.web.servlet.context.AnnotationConfigServletWebServerApplicationContext@18e36d14",

"management.endpoint.health-org.springframework.boot.actuate.autoconfigure.health.HealthEndpointProperties"

]

},

"webConversionServiceProvider": {

"aliases": [],

"scope": "singleton",

"type": "org.springdoc.core.providers.WebConversionServiceProvider",

"resource": "class path resource [org/springdoc/core/configuration/SpringDocConfiguration$WebConversionServiceConfiguration.class]",

"dependencies": [

"org.springdoc.core.configuration.SpringDocConfiguration$WebConversionServiceConfiguration"

]

},

"management.endpoint.health-org.springframework.boot.actuate.autoconfigure.health.HealthEndpointProperties": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.boot.actuate.autoconfigure.health.HealthEndpointProperties",

"dependencies": []

},

"org.springframework.boot.autoconfigure.web.servlet.MultipartAutoConfiguration": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.boot.autoconfigure.web.servlet.MultipartAutoConfiguration",

"dependencies": [

"spring.servlet.multipart-org.springframework.boot.autoconfigure.web.servlet.MultipartProperties"

]

},

"hikariDataSourceMeterBinder": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.boot.actuate.autoconfigure.metrics.jdbc.DataSourcePoolMetricsAutoConfiguration$HikariDataSourceMetricsConfiguration$HikariDataSourceMeterBinder",

"resource": "class path resource [org/springframework/boot/actuate/autoconfigure/metrics/jdbc/DataSourcePoolMetricsAutoConfiguration$HikariDataSourceMetricsConfiguration.class]",

"dependencies": [

"org.springframework.boot.actuate.autoconfigure.metrics.jdbc.DataSourcePoolMetricsAutoConfiguration$HikariDataSourceMetricsConfiguration"

]

},

"jdbcTemplate": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.jdbc.core.JdbcTemplate",

"resource": "class path resource [org/springframework/boot/autoconfigure/jdbc/JdbcTemplateConfiguration.class]",

"dependencies": [

"dataSourceScriptDatabaseInitializer",

"org.springframework.boot.autoconfigure.jdbc.JdbcTemplateConfiguration",

"dataSource",

"spring.jdbc-org.springframework.boot.autoconfigure.jdbc.JdbcProperties"

]

},

"org.springframework.boot.actuate.autoconfigure.endpoint.web.WebEndpointAutoConfiguration": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.boot.actuate.autoconfigure.endpoint.web.WebEndpointAutoConfiguration",

"dependencies": [

"org.springframework.boot.web.servlet.context.AnnotationConfigServletWebServerApplicationContext@18e36d14",

"management.endpoints.web-org.springframework.boot.actuate.autoconfigure.endpoint.web.WebEndpointProperties"

]

},

"webEndpointPathMapper": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.boot.actuate.autoconfigure.endpoint.web.MappingWebEndpointPathMapper",

"resource": "class path resource [org/springframework/boot/actuate/autoconfigure/endpoint/web/WebEndpointAutoConfiguration.class]",

"dependencies": [

"org.springframework.boot.actuate.autoconfigure.endpoint.web.WebEndpointAutoConfiguration"

]

},

"org.springframework.boot.autoconfigure.orm.jpa.HibernateJpaConfiguration": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.boot.autoconfigure.orm.jpa.HibernateJpaConfiguration",

"dependencies": [

"dataSource",

"spring.jpa-org.springframework.boot.autoconfigure.orm.jpa.JpaProperties",

"org.springframework.beans.factory.support.DefaultListableBeanFactory@37efd131",

"spring.jpa.hibernate-org.springframework.boot.autoconfigure.orm.jpa.HibernateProperties"

]

},

"org.springframework.boot.actuate.autoconfigure.cache.CachesEndpointAutoConfiguration": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.boot.actuate.autoconfigure.cache.CachesEndpointAutoConfiguration",

"dependencies": []

},

"org.springframework.boot.autoconfigure.aop.AopAutoConfiguration$AspectJAutoProxyingConfiguration": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.boot.autoconfigure.aop.AopAutoConfiguration$AspectJAutoProxyingConfiguration",

"dependencies": []

},

"org.springframework.boot.autoconfigure.transaction.TransactionAutoConfiguration": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.boot.autoconfigure.transaction.TransactionAutoConfiguration",

"dependencies": []

},

"org.springframework.boot.autoconfigure.transaction.TransactionAutoConfiguration$EnableTransactionManagementConfiguration$CglibAutoProxyConfiguration": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.boot.autoconfigure.transaction.TransactionAutoConfiguration$EnableTransactionManagementConfiguration$CglibAutoProxyConfiguration",

"dependencies": []

},

"org.springframework.boot.autoconfigure.web.reactive.function.client.ClientHttpConnectorAutoConfiguration": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.boot.autoconfigure.web.reactive.function.client.ClientHttpConnectorAutoConfiguration",

"dependencies": []

},

"org.springdoc.webmvc.core.configuration.MultipleOpenApiSupportConfiguration": {

"aliases": [],

"scope": "singleton",

"type": "org.springdoc.webmvc.core.configuration.MultipleOpenApiSupportConfiguration",

"dependencies": []

},

"org.springframework.boot.actuate.autoconfigure.observation.web.client.WebClientObservationConfiguration": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.boot.actuate.autoconfigure.observation.web.client.WebClientObservationConfiguration",

"dependencies": []

},

"management.simple.metrics.export-org.springframework.boot.actuate.autoconfigure.metrics.export.simple.SimpleProperties": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.boot.actuate.autoconfigure.metrics.export.simple.SimpleProperties",

"dependencies": []

},

"beanNameViewResolver": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.web.servlet.view.BeanNameViewResolver",

"resource": "class path resource [org/springframework/boot/autoconfigure/web/servlet/error/ErrorMvcAutoConfiguration$WhitelabelErrorViewConfiguration.class]",

"dependencies": [

"org.springframework.boot.autoconfigure.web.servlet.error.ErrorMvcAutoConfiguration$WhitelabelErrorViewConfiguration"

]

},

"reactiveRedisTemplate": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.data.redis.core.ReactiveRedisTemplate",

"resource": "class path resource [org/springframework/boot/autoconfigure/data/redis/RedisReactiveAutoConfiguration.class]",

"dependencies": [

"org.springframework.boot.autoconfigure.data.redis.RedisReactiveAutoConfiguration",

"redisConnectionFactory",

"org.springframework.boot.web.servlet.context.AnnotationConfigServletWebServerApplicationContext@18e36d14"

]

},

"swaggerResourceResolver": {

"aliases": [],

"scope": "singleton",

"type": "org.springdoc.webmvc.ui.SwaggerResourceResolver",

"resource": "class path resource [org/springdoc/webmvc/ui/SwaggerConfig.class]",

"dependencies": [

"org.springdoc.webmvc.ui.SwaggerConfig",

"org.springdoc.core.properties.SwaggerUiConfigProperties"

]

},

"org.springframework.boot.actuate.autoconfigure.endpoint.web.servlet.WebMvcEndpointManagementContextConfiguration": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.boot.actuate.autoconfigure.endpoint.web.servlet.WebMvcEndpointManagementContextConfiguration",

"dependencies": []

},

"viewResolver": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.web.servlet.view.ContentNegotiatingViewResolver",

"resource": "class path resource [org/springframework/boot/autoconfigure/web/servlet/WebMvcAutoConfiguration$WebMvcAutoConfigurationAdapter.class]",

"dependencies": [

"org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguration$WebMvcAutoConfigurationAdapter",

"org.springframework.beans.factory.support.DefaultListableBeanFactory@37efd131"

]

},

"toolsController": {

"aliases": [],

"scope": "singleton",

"type": "cn.netkiller.controller.ToolsController",

"resource": "URL [jar:nested:/app/watch-1.0-SNAPSHOT.jar/!BOOT-INF/classes/!/cn/netkiller/controller/ToolsController.class]",

"dependencies": [

"aliyunService",

"psychoanalysisService",

"baiduService",

"audioService",

"mqttService",

"config"

]

},

"reactorResourceFactory": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.http.client.ReactorResourceFactory",

"resource": "class path resource [org/springframework/boot/autoconfigure/reactor/netty/ReactorNettyConfigurations$ReactorResourceFactoryConfiguration.class]",

"dependencies": [

"org.springframework.boot.autoconfigure.reactor.netty.ReactorNettyConfigurations$ReactorResourceFactoryConfiguration",

"spring.reactor.netty-org.springframework.boot.autoconfigure.reactor.netty.ReactorNettyProperties"

]

},

"projectingArgumentResolverBeanPostProcessor": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.data.web.config.ProjectingArgumentResolverRegistrar$ProjectingArgumentResolverBeanPostProcessor",

"resource": "class path resource [org/springframework/data/web/config/ProjectingArgumentResolverRegistrar.class]",

"dependencies": []

},

"tomcatServletWebServerFactoryCustomizer": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.boot.autoconfigure.web.servlet.TomcatServletWebServerFactoryCustomizer",

"resource": "class path resource [org/springframework/boot/autoconfigure/web/servlet/ServletWebServerFactoryAutoConfiguration.class]",

"dependencies": [

"org.springframework.boot.autoconfigure.web.servlet.ServletWebServerFactoryAutoConfiguration",

"server-org.springframework.boot.autoconfigure.web.ServerProperties"

]

},

"server-org.springframework.boot.autoconfigure.web.ServerProperties": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.boot.autoconfigure.web.ServerProperties",

"dependencies": []

},

"redisConnectionDetails": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.boot.autoconfigure.data.redis.PropertiesRedisConnectionDetails",

"resource": "class path resource [org/springframework/boot/autoconfigure/data/redis/RedisAutoConfiguration.class]",

"dependencies": [

"org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration",

"spring.data.redis-org.springframework.boot.autoconfigure.data.redis.RedisProperties"

]

},

"messageConverters": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.boot.autoconfigure.http.HttpMessageConverters",

"resource": "class path resource [org/springframework/boot/autoconfigure/http/HttpMessageConvertersAutoConfiguration.class]",

"dependencies": [

"org.springframework.boot.autoconfigure.http.HttpMessageConvertersAutoConfiguration"

]

},

"websocketServletWebServerCustomizer": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.boot.autoconfigure.websocket.servlet.TomcatWebSocketServletWebServerCustomizer",

"resource": "class path resource [org/springframework/boot/autoconfigure/websocket/servlet/WebSocketServletAutoConfiguration$TomcatWebSocketConfiguration.class]",

"dependencies": [

"org.springframework.boot.autoconfigure.websocket.servlet.WebSocketServletAutoConfiguration$TomcatWebSocketConfiguration"

]

},

"configurationPropertiesReportEndpointWebExtension": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.boot.actuate.context.properties.ConfigurationPropertiesReportEndpointWebExtension",

"resource": "class path resource [org/springframework/boot/actuate/autoconfigure/context/properties/ConfigurationPropertiesReportEndpointAutoConfiguration.class]",

"dependencies": [

"org.springframework.boot.actuate.autoconfigure.context.properties.ConfigurationPropertiesReportEndpointAutoConfiguration",

"configurationPropertiesReportEndpoint",

"management.endpoint.configprops-org.springframework.boot.actuate.autoconfigure.context.properties.ConfigurationPropertiesReportEndpointProperties"

]

},

"redisCustomConversions": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.data.redis.core.convert.RedisCustomConversions",

"dependencies": []

},

"org.springframework.boot.autoconfigure.sql.init.SqlInitializationAutoConfiguration": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.boot.autoconfigure.sql.init.SqlInitializationAutoConfiguration",

"dependencies": []

},

"dataSourceScriptDatabaseInitializer": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.boot.autoconfigure.sql.init.SqlDataSourceScriptDatabaseInitializer",

"resource": "class path resource [org/springframework/boot/autoconfigure/sql/init/DataSourceInitializationConfiguration.class]",

"dependencies": [

"org.springframework.boot.autoconfigure.sql.init.DataSourceInitializationConfiguration",

"dataSource",

"spring.sql.init-org.springframework.boot.autoconfigure.sql.init.SqlInitializationProperties"

]

},

"meterRegistryPostProcessor": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.boot.actuate.autoconfigure.metrics.MeterRegistryPostProcessor",

"resource": "class path resource [org/springframework/boot/actuate/autoconfigure/metrics/MetricsAutoConfiguration.class]",

"dependencies": [

"org.springframework.boot.web.servlet.context.AnnotationConfigServletWebServerApplicationContext@18e36d14"

]

},

"org.springdoc.core.configuration.SpringDocSortConfiguration": {

"aliases": [],

"scope": "singleton",

"type": "org.springdoc.core.configuration.SpringDocSortConfiguration",

"dependencies": []

},

"org.springframework.boot.autoconfigure.jdbc.JdbcClientAutoConfiguration": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.boot.autoconfigure.jdbc.JdbcClientAutoConfiguration",

"dependencies": []

},

"endpointMediaTypes": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.boot.actuate.endpoint.web.EndpointMediaTypes",

"resource": "class path resource [org/springframework/boot/actuate/autoconfigure/endpoint/web/WebEndpointAutoConfiguration.class]",

"dependencies": [

"org.springframework.boot.actuate.autoconfigure.endpoint.web.WebEndpointAutoConfiguration"

]

},

"jvmCompilationMetrics": {

"aliases": [],

"scope": "singleton",

"type": "io.micrometer.core.instrument.binder.jvm.JvmCompilationMetrics",

"resource": "class path resource [org/springframework/boot/actuate/autoconfigure/metrics/JvmMetricsAutoConfiguration.class]",

"dependencies": [

"org.springframework.boot.actuate.autoconfigure.metrics.JvmMetricsAutoConfiguration"

]

},

"cacheMetricsRegistrar": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.boot.actuate.metrics.cache.CacheMetricsRegistrar",

"resource": "class path resource [org/springframework/boot/actuate/autoconfigure/metrics/cache/CacheMetricsRegistrarConfiguration.class]",

"dependencies": [

"org.springframework.boot.actuate.autoconfigure.metrics.cache.CacheMetricsRegistrarConfiguration"

]

},

"org.springframework.boot.actuate.autoconfigure.health.HealthEndpointWebExtensionConfiguration": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.boot.actuate.autoconfigure.health.HealthEndpointWebExtensionConfiguration",

"dependencies": []

},

"org.springframework.boot.autoconfigure.web.reactive.function.client.WebClientAutoConfiguration": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.boot.autoconfigure.web.reactive.function.client.WebClientAutoConfiguration",

"dependencies": []

},

"jdbcConnectionDetailsHikariBeanPostProcessor": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.boot.autoconfigure.jdbc.HikariJdbcConnectionDetailsBeanPostProcessor",

"resource": "class path resource [org/springframework/boot/autoconfigure/jdbc/DataSourceConfiguration$Hikari.class]",

"dependencies": []

},

"org.springdoc.core.configuration.SpringDocKotlinConfiguration": {

"aliases": [],

"scope": "singleton",

"type": "org.springdoc.core.configuration.SpringDocKotlinConfiguration",

"dependencies": []

},

"spring.ssl-org.springframework.boot.autoconfigure.ssl.SslProperties": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.boot.autoconfigure.ssl.SslProperties",

"dependencies": []

},

"repositoryTagsProvider": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.boot.actuate.metrics.data.DefaultRepositoryTagsProvider",

"resource": "class path resource [org/springframework/boot/actuate/autoconfigure/metrics/data/RepositoryMetricsAutoConfiguration.class]",

"dependencies": [

"org.springframework.boot.actuate.autoconfigure.metrics.data.RepositoryMetricsAutoConfiguration"

]

},

"dbHealthContributor": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.boot.actuate.jdbc.DataSourceHealthIndicator",

"resource": "class path resource [org/springframework/boot/actuate/autoconfigure/jdbc/DataSourceHealthContributorAutoConfiguration.class]",

"dependencies": [

"org.springframework.boot.actuate.autoconfigure.jdbc.DataSourceHealthContributorAutoConfiguration",

"dataSource",

"management.health.db-org.springframework.boot.actuate.autoconfigure.jdbc.DataSourceHealthIndicatorProperties"

]

},

"dataSourcePoolMetadataMeterBinder": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.boot.actuate.autoconfigure.metrics.jdbc.DataSourcePoolMetricsAutoConfiguration$DataSourcePoolMetadataMetricsConfiguration$DataSourcePoolMetadataMeterBinder",

"resource": "class path resource [org/springframework/boot/actuate/autoconfigure/metrics/jdbc/DataSourcePoolMetricsAutoConfiguration$DataSourcePoolMetadataMetricsConfiguration.class]",

"dependencies": [

"org.springframework.boot.actuate.autoconfigure.metrics.jdbc.DataSourcePoolMetricsAutoConfiguration$DataSourcePoolMetadataMetricsConfiguration",

"dataSource"

]

},

"webServerFactoryCustomizerBeanPostProcessor": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.boot.web.server.WebServerFactoryCustomizerBeanPostProcessor",

"dependencies": []

},

"metricsHttpClientUriTagFilter": {

"aliases": [],

"scope": "singleton",

"type": "io.micrometer.core.instrument.config.MeterFilter$9",

"resource": "class path resource [org/springframework/boot/actuate/autoconfigure/observation/web/client/HttpClientObservationsAutoConfiguration$MeterFilterConfiguration.class]",

"dependencies": [

"org.springframework.boot.actuate.autoconfigure.observation.web.client.HttpClientObservationsAutoConfiguration$MeterFilterConfiguration",

"management.observations-org.springframework.boot.actuate.autoconfigure.observation.ObservationProperties",

"management.metrics-org.springframework.boot.actuate.autoconfigure.metrics.MetricsProperties"

]

},

"timedAspect": {

"aliases": [],

"scope": "singleton",

"type": "io.micrometer.core.aop.TimedAspect",

"resource": "class path resource [org/springframework/boot/actuate/autoconfigure/metrics/MetricsAspectsAutoConfiguration.class]",

"dependencies": [

"org.springframework.boot.actuate.autoconfigure.metrics.MetricsAspectsAutoConfiguration",

"simpleMeterRegistry"

]

},

"org.springframework.boot.autoconfigure.websocket.servlet.WebSocketServletAutoConfiguration": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.boot.autoconfigure.websocket.servlet.WebSocketServletAutoConfiguration",

"dependencies": []

},

"management.health.diskspace-org.springframework.boot.actuate.autoconfigure.system.DiskSpaceHealthIndicatorProperties": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.boot.actuate.autoconfigure.system.DiskSpaceHealthIndicatorProperties",

"dependencies": []

},

"org.springframework.boot.autoconfigure.gson.GsonAutoConfiguration": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.boot.autoconfigure.gson.GsonAutoConfiguration",

"dependencies": []

},

"controllerEndpointHandlerMapping": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.boot.actuate.endpoint.web.servlet.ControllerEndpointHandlerMapping",

"resource": "class path resource [org/springframework/boot/actuate/autoconfigure/endpoint/web/servlet/WebMvcEndpointManagementContextConfiguration.class]",

"dependencies": [

"org.springframework.boot.actuate.autoconfigure.endpoint.web.servlet.WebMvcEndpointManagementContextConfiguration",

"controllerEndpointDiscoverer",

"management.endpoints.web.cors-org.springframework.boot.actuate.autoconfigure.endpoint.web.CorsEndpointProperties",

"management.endpoints.web-org.springframework.boot.actuate.autoconfigure.endpoint.web.WebEndpointProperties"

]

},

"webFluxSupportConverter": {

"aliases": [],

"scope": "singleton",

"type": "org.springdoc.core.converters.WebFluxSupportConverter",

"resource": "class path resource [org/springdoc/core/configuration/SpringDocConfiguration$SpringDocWebFluxSupportConfiguration.class]",

"dependencies": [

"org.springdoc.core.configuration.SpringDocConfiguration$SpringDocWebFluxSupportConfiguration",

"springdocObjectMapperProvider"

]

},

"management.endpoint.env-org.springframework.boot.actuate.autoconfigure.env.EnvironmentEndpointProperties": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.boot.actuate.autoconfigure.env.EnvironmentEndpointProperties",

"dependencies": []

},

"org.springframework.boot.autoconfigure.jdbc.JdbcTemplateAutoConfiguration": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.boot.autoconfigure.jdbc.JdbcTemplateAutoConfiguration",

"dependencies": []

},

"org.springframework.boot.actuate.autoconfigure.management.ThreadDumpEndpointAutoConfiguration": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.boot.actuate.autoconfigure.management.ThreadDumpEndpointAutoConfiguration",

"dependencies": []

},

"org.springframework.boot.autoconfigure.availability.ApplicationAvailabilityAutoConfiguration": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.boot.autoconfigure.availability.ApplicationAvailabilityAutoConfiguration",

"dependencies": []

},

"config": {

"aliases": [],

"scope": "singleton",

"type": "cn.netkiller.config.Config",

"resource": "URL [jar:nested:/app/watch-1.0-SNAPSHOT.jar/!BOOT-INF/classes/!/cn/netkiller/config/Config.class]",

"dependencies": []

},

"jdbcClient": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.jdbc.core.simple.DefaultJdbcClient",

"resource": "class path resource [org/springframework/boot/autoconfigure/jdbc/JdbcClientAutoConfiguration.class]",

"dependencies": [

"dataSourceScriptDatabaseInitializer",

"org.springframework.boot.autoconfigure.jdbc.JdbcClientAutoConfiguration",

"namedParameterJdbcTemplate"

]

},

"observabilitySchedulingConfigurer": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.boot.actuate.autoconfigure.scheduling.ScheduledTasksObservabilityAutoConfiguration$ObservabilitySchedulingConfigurer",

"resource": "class path resource [org/springframework/boot/actuate/autoconfigure/scheduling/ScheduledTasksObservabilityAutoConfiguration.class]",

"dependencies": [

"org.springframework.boot.actuate.autoconfigure.scheduling.ScheduledTasksObservabilityAutoConfiguration",

"observationRegistry"

]

},

"metricsEndpoint": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.boot.actuate.metrics.MetricsEndpoint",

"resource": "class path resource [org/springframework/boot/actuate/autoconfigure/metrics/MetricsEndpointAutoConfiguration.class]",

"dependencies": [

"org.springframework.boot.actuate.autoconfigure.metrics.MetricsEndpointAutoConfiguration",

"simpleMeterRegistry"

]

},

"management.observations-org.springframework.boot.actuate.autoconfigure.observation.ObservationProperties": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.boot.actuate.autoconfigure.observation.ObservationProperties",

"dependencies": []

},

"org.springframework.boot.autoconfigure.aop.AopAutoConfiguration": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.boot.autoconfigure.aop.AopAutoConfiguration",

"dependencies": []

},

"spring.cache-org.springframework.boot.autoconfigure.cache.CacheProperties": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.boot.autoconfigure.cache.CacheProperties",

"dependencies": []

},

"org.springframework.boot.actuate.autoconfigure.info.InfoContributorAutoConfiguration": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.boot.actuate.autoconfigure.info.InfoContributorAutoConfiguration",

"dependencies": []

},

"org.springframework.boot.actuate.autoconfigure.observation.ObservationAutoConfiguration$ObservedAspectConfiguration": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.boot.actuate.autoconfigure.observation.ObservationAutoConfiguration$ObservedAspectConfiguration",

"dependencies": []

},

"metricsObservationHandlerGrouping": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.boot.actuate.autoconfigure.observation.ObservationHandlerGrouping",

"resource": "class path resource [org/springframework/boot/actuate/autoconfigure/observation/ObservationAutoConfiguration$OnlyMetricsConfiguration.class]",

"dependencies": [

"org.springframework.boot.actuate.autoconfigure.observation.ObservationAutoConfiguration$OnlyMetricsConfiguration"

]

},

"org.springframework.boot.autoconfigure.web.servlet.ServletWebServerFactoryAutoConfiguration": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.boot.autoconfigure.web.servlet.ServletWebServerFactoryAutoConfiguration",

"dependencies": []

},

"spring.jpa.hibernate-org.springframework.boot.autoconfigure.orm.jpa.HibernateProperties": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.boot.autoconfigure.orm.jpa.HibernateProperties",

"dependencies": []

},

"environmentEndpoint": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.boot.actuate.env.EnvironmentEndpoint",

"resource": "class path resource [org/springframework/boot/actuate/autoconfigure/env/EnvironmentEndpointAutoConfiguration.class]",

"dependencies": [

"org.springframework.boot.actuate.autoconfigure.env.EnvironmentEndpointAutoConfiguration",

"environment",

"management.endpoint.env-org.springframework.boot.actuate.autoconfigure.env.EnvironmentEndpointProperties"

]

},

"kotlinCoroutinesReturnTypeParser": {

"aliases": [],

"scope": "singleton",

"type": "org.springdoc.core.parsers.KotlinCoroutinesReturnTypeParser",

"resource": "class path resource [org/springdoc/core/configuration/SpringDocKotlinConfiguration.class]",

"dependencies": [

"org.springdoc.core.configuration.SpringDocKotlinConfiguration"

]

},

"springDocCustomizers": {

"aliases": [],

"scope": "singleton",

"type": "org.springdoc.core.customizers.SpringDocCustomizers",

"resource": "class path resource [org/springdoc/core/configuration/SpringDocConfiguration.class]",

"dependencies": [

"org.springdoc.core.configuration.SpringDocConfiguration"

]

},

"jacksonCodecCustomizer": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.boot.autoconfigure.http.codec.CodecsAutoConfiguration$JacksonCodecConfiguration$$Lambda/0x00007ff400a35068",

"resource": "class path resource [org/springframework/boot/autoconfigure/http/codec/CodecsAutoConfiguration$JacksonCodecConfiguration.class]",

"dependencies": [

"org.springframework.boot.autoconfigure.http.codec.CodecsAutoConfiguration$JacksonCodecConfiguration",

"jacksonObjectMapper"

]

},

"conventionErrorViewResolver": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.boot.autoconfigure.web.servlet.error.DefaultErrorViewResolver",

"resource": "class path resource [org/springframework/boot/autoconfigure/web/servlet/error/ErrorMvcAutoConfiguration$DefaultErrorViewResolverConfiguration.class]",

"dependencies": [

"org.springframework.boot.autoconfigure.web.servlet.error.ErrorMvcAutoConfiguration$DefaultErrorViewResolverConfiguration"

]

},

"org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguration$EnableWebMvcConfiguration": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguration$EnableWebMvcConfiguration",

"dependencies": [

"spring.mvc-org.springframework.boot.autoconfigure.web.servlet.WebMvcProperties",

"spring.web-org.springframework.boot.autoconfigure.web.WebProperties",

"org.springframework.beans.factory.support.DefaultListableBeanFactory@37efd131",

"org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguration$WebMvcAutoConfigurationAdapter",

"swaggerWebMvcConfigurer",

"org.springframework.data.web.config.SpringDataWebConfiguration",

"endpointObjectMapperWebMvcConfigurer"

]

},

"org.springdoc.core.configuration.SpringDocConfiguration$OpenApiResourceAdvice": {

"aliases": [],

"scope": "singleton",

"type": "org.springdoc.core.configuration.SpringDocConfiguration$OpenApiResourceAdvice",

"dependencies": [

"org.springdoc.core.configuration.SpringDocConfiguration"

]

},

"org.springframework.boot.actuate.autoconfigure.metrics.LogbackMetricsAutoConfiguration": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.boot.actuate.autoconfigure.metrics.LogbackMetricsAutoConfiguration",

"dependencies": []

},

"org.springframework.boot.actuate.autoconfigure.observation.web.client.HttpClientObservationsAutoConfiguration$MeterFilterConfiguration": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.boot.actuate.autoconfigure.observation.web.client.HttpClientObservationsAutoConfiguration$MeterFilterConfiguration",

"dependencies": []

},

"localeCharsetMappingsCustomizer": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.boot.autoconfigure.web.servlet.HttpEncodingAutoConfiguration$LocaleCharsetMappingsCustomizer",

"resource": "class path resource [org/springframework/boot/autoconfigure/web/servlet/HttpEncodingAutoConfiguration.class]",

"dependencies": [

"org.springframework.boot.autoconfigure.web.servlet.HttpEncodingAutoConfiguration"

]

},

"org.springframework.boot.actuate.autoconfigure.logging.LoggersEndpointAutoConfiguration": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.boot.actuate.autoconfigure.logging.LoggersEndpointAutoConfiguration",

"dependencies": []

},

"jsonMixinModuleEntries": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.boot.jackson.JsonMixinModuleEntries",

"resource": "class path resource [org/springframework/boot/autoconfigure/jackson/JacksonAutoConfiguration$JacksonMixinConfiguration.class]",

"dependencies": [

"org.springframework.boot.web.servlet.context.AnnotationConfigServletWebServerApplicationContext@18e36d14"

]

},

"formContentFilter": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.boot.web.servlet.filter.OrderedFormContentFilter",

"resource": "class path resource [org/springframework/boot/autoconfigure/web/servlet/WebMvcAutoConfiguration.class]",

"dependencies": [

"org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguration"

]

},

"mockController": {

"aliases": [],

"scope": "singleton",

"type": "cn.netkiller.controller.MockController",

"resource": "URL [jar:nested:/app/watch-1.0-SNAPSHOT.jar/!BOOT-INF/classes/!/cn/netkiller/controller/MockController.class]",

"dependencies": []

},

"pictureService": {

"aliases": [],

"scope": "singleton",

"type": "cn.netkiller.service.PictureService",

"resource": "URL [jar:nested:/app/watch-1.0-SNAPSHOT.jar/!BOOT-INF/classes/!/cn/netkiller/service/PictureService.class]",

"dependencies": [

"pictureRepository",

"mqttService",

"baiduService",

"sessionStatusService"

]

},

"defaultViewResolver": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.web.servlet.view.InternalResourceViewResolver",

"resource": "class path resource [org/springframework/boot/autoconfigure/web/servlet/WebMvcAutoConfiguration$WebMvcAutoConfigurationAdapter.class]",

"dependencies": [

"org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguration$WebMvcAutoConfigurationAdapter"

]

},

"keyValueMappingContext": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.data.redis.core.mapping.RedisMappingContext",

"dependencies": [

"redisMappingConfiguration#0"

]

},

"org.springframework.boot.autoconfigure.transaction.TransactionAutoConfiguration$EnableTransactionManagementConfiguration": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.boot.autoconfigure.transaction.TransactionAutoConfiguration$EnableTransactionManagementConfiguration",

"dependencies": []

},

"routerFunctionMapping": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.web.servlet.function.support.RouterFunctionMapping",

"resource": "class path resource [org/springframework/boot/autoconfigure/web/servlet/WebMvcAutoConfiguration$EnableWebMvcConfiguration.class]",

"dependencies": [

"org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguration$EnableWebMvcConfiguration",

"mvcConversionService",

"mvcResourceUrlProvider"

]

},

"jacksonObjectMapperBuilder": {

"aliases": [],

"scope": "prototype",

"type": "org.springframework.http.converter.json.Jackson2ObjectMapperBuilder",

"resource": "class path resource [org/springframework/boot/autoconfigure/jackson/JacksonAutoConfiguration$JacksonObjectMapperBuilderConfiguration.class]",

"dependencies": [

"org.springframework.boot.autoconfigure.jackson.JacksonAutoConfiguration$JacksonObjectMapperBuilderConfiguration",

"org.springframework.boot.web.servlet.context.AnnotationConfigServletWebServerApplicationContext@18e36d14",

"standardJacksonObjectMapperBuilderCustomizer"

]

},

"org.springframework.boot.autoconfigure.dao.PersistenceExceptionTranslationAutoConfiguration": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.boot.autoconfigure.dao.PersistenceExceptionTranslationAutoConfiguration",

"dependencies": []

},

"cacheManager": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.data.redis.cache.RedisCacheManager",

"resource": "class path resource [org/springframework/boot/autoconfigure/cache/RedisCacheConfiguration.class]",

"dependencies": [

"org.springframework.boot.autoconfigure.cache.RedisCacheConfiguration",

"spring.cache-org.springframework.boot.autoconfigure.cache.CacheProperties",

"cacheManagerCustomizers",

"redisConnectionFactory",

"org.springframework.boot.web.servlet.context.AnnotationConfigServletWebServerApplicationContext@18e36d14"

]

},

"spring.task.scheduling-org.springframework.boot.autoconfigure.task.TaskSchedulingProperties": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.boot.autoconfigure.task.TaskSchedulingProperties",

"dependencies": []

},

"spring.reactor-org.springframework.boot.autoconfigure.reactor.ReactorProperties": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.boot.autoconfigure.reactor.ReactorProperties",

"dependencies": []

},

"org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration",

"dependencies": []

},

"healthEndpointWebExtension": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.boot.actuate.health.HealthEndpointWebExtension",

"resource": "class path resource [org/springframework/boot/actuate/autoconfigure/health/HealthEndpointWebExtensionConfiguration.class]",

"dependencies": [

"org.springframework.boot.actuate.autoconfigure.health.HealthEndpointWebExtensionConfiguration",

"healthContributorRegistry",

"healthEndpointGroups",

"management.endpoint.health-org.springframework.boot.actuate.autoconfigure.health.HealthEndpointProperties"

]

},

"multipartResolver": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.web.multipart.support.StandardServletMultipartResolver",

"resource": "class path resource [org/springframework/boot/autoconfigure/web/servlet/MultipartAutoConfiguration.class]",

"dependencies": [

"org.springframework.boot.autoconfigure.web.servlet.MultipartAutoConfiguration"

]

},

"sessionStatusService": {

"aliases": [],

"scope": "singleton",

"type": "cn.netkiller.service.SessionStatusService",

"resource": "URL [jar:nested:/app/watch-1.0-SNAPSHOT.jar/!BOOT-INF/classes/!/cn/netkiller/service/SessionStatusService.class]",

"dependencies": [

"sessionStatusRepository"

]

},

"org.springframework.boot.actuate.autoconfigure.endpoint.web.WebEndpointAutoConfiguration$WebEndpointServletConfiguration": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.boot.actuate.autoconfigure.endpoint.web.WebEndpointAutoConfiguration$WebEndpointServletConfiguration",

"dependencies": []

},

"requestMappingHandlerMapping": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping",

"resource": "class path resource [org/springframework/boot/autoconfigure/web/servlet/WebMvcAutoConfiguration$EnableWebMvcConfiguration.class]",

"dependencies": [

"org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguration$EnableWebMvcConfiguration",

"mvcContentNegotiationManager",

"mvcConversionService",

"mvcResourceUrlProvider"

]

},

"webExposeExcludePropertyEndpointFilter": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.boot.actuate.autoconfigure.endpoint.expose.IncludeExcludeEndpointFilter",

"resource": "class path resource [org/springframework/boot/actuate/autoconfigure/endpoint/web/WebEndpointAutoConfiguration.class]",

"dependencies": [

"org.springframework.boot.actuate.autoconfigure.endpoint.web.WebEndpointAutoConfiguration"

]

},

"lifecycleProcessor": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.context.support.DefaultLifecycleProcessor",

"resource": "class path resource [org/springframework/boot/autoconfigure/context/LifecycleAutoConfiguration.class]",

"dependencies": [

"org.springframework.boot.autoconfigure.context.LifecycleAutoConfiguration",

"spring.lifecycle-org.springframework.boot.autoconfigure.context.LifecycleProperties"

]

},

"requestMappingHandlerAdapter": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter",

"resource": "class path resource [org/springframework/boot/autoconfigure/web/servlet/WebMvcAutoConfiguration$EnableWebMvcConfiguration.class]",

"dependencies": [

"org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguration$EnableWebMvcConfiguration",

"mvcContentNegotiationManager",

"mvcConversionService",

"mvcValidator"

]

},

"tomcatMetricsBinder": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.boot.actuate.metrics.web.tomcat.TomcatMetricsBinder",

"resource": "class path resource [org/springframework/boot/actuate/autoconfigure/metrics/web/tomcat/TomcatMetricsAutoConfiguration.class]",

"dependencies": [

"org.springframework.boot.actuate.autoconfigure.metrics.web.tomcat.TomcatMetricsAutoConfiguration",

"simpleMeterRegistry"

]

},

"org.springdoc.core.configuration.SpringDocConfiguration$SpringDocSpringDataWebPropertiesProvider": {

"aliases": [],

"scope": "singleton",

"type": "org.springdoc.core.configuration.SpringDocConfiguration$SpringDocSpringDataWebPropertiesProvider",

"dependencies": []

},

"org.springframework.boot.actuate.autoconfigure.metrics.jdbc.DataSourcePoolMetricsAutoConfiguration$DataSourcePoolMetadataMetricsConfiguration": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.boot.actuate.autoconfigure.metrics.jdbc.DataSourcePoolMetricsAutoConfiguration$DataSourcePoolMetadataMetricsConfiguration",

"dependencies": []

},

"org.springframework.boot.actuate.autoconfigure.endpoint.web.ServletEndpointManagementContextConfiguration$WebMvcServletEndpointManagementContextConfiguration": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.boot.actuate.autoconfigure.endpoint.web.ServletEndpointManagementContextConfiguration$WebMvcServletEndpointManagementContextConfiguration",

"dependencies": []

},

"org.springframework.data.jpa.repository.support.JpaEvaluationContextExtension": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.data.jpa.repository.support.JpaEvaluationContextExtension",

"dependencies": []

},

"org.springframework.boot.autoconfigure.context.LifecycleAutoConfiguration": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.boot.autoconfigure.context.LifecycleAutoConfiguration",

"dependencies": []

},

"org.springframework.boot.actuate.autoconfigure.logging.LogFileWebEndpointAutoConfiguration": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.boot.actuate.autoconfigure.logging.LogFileWebEndpointAutoConfiguration",

"dependencies": []

},

"spring.info-org.springframework.boot.autoconfigure.info.ProjectInfoProperties": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.boot.autoconfigure.info.ProjectInfoProperties",

"dependencies": []

},

"org.springframework.boot.autoconfigure.jdbc.DataSourceTransactionManagerAutoConfiguration": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.boot.autoconfigure.jdbc.DataSourceTransactionManagerAutoConfiguration",

"dependencies": []

},

"org.springframework.boot.actuate.autoconfigure.metrics.MetricsEndpointAutoConfiguration": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.boot.actuate.autoconfigure.metrics.MetricsEndpointAutoConfiguration",

"dependencies": []

},

"infoEndpoint": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.boot.actuate.info.InfoEndpoint",

"resource": "class path resource [org/springframework/boot/actuate/autoconfigure/info/InfoEndpointAutoConfiguration.class]",

"dependencies": [

"org.springframework.boot.actuate.autoconfigure.info.InfoEndpointAutoConfiguration"

]

},

"org.springframework.boot.actuate.autoconfigure.observation.web.servlet.WebMvcObservationAutoConfiguration$MeterFilterConfiguration": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.boot.actuate.autoconfigure.observation.web.servlet.WebMvcObservationAutoConfiguration$MeterFilterConfiguration",

"dependencies": []

},

"org.springframework.boot.autoconfigure.jdbc.DataSourceTransactionManagerAutoConfiguration$JdbcTransactionManagerConfiguration": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.boot.autoconfigure.jdbc.DataSourceTransactionManagerAutoConfiguration$JdbcTransactionManagerConfiguration",

"dependencies": []

},

"org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration",

"dependencies": []

},

"lettuceMetrics": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.boot.actuate.autoconfigure.metrics.redis.LettuceMetricsAutoConfiguration$$Lambda/0x00007ff40055ba28",

"resource": "class path resource [org/springframework/boot/actuate/autoconfigure/metrics/redis/LettuceMetricsAutoConfiguration.class]",

"dependencies": [

"org.springframework.boot.actuate.autoconfigure.metrics.redis.LettuceMetricsAutoConfiguration",

"simpleMeterRegistry",

"micrometerOptions"

]

},

"metricsRepositoryMethodInvocationListenerBeanPostProcessor": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.boot.actuate.autoconfigure.metrics.data.MetricsRepositoryMethodInvocationListenerBeanPostProcessor",

"resource": "class path resource [org/springframework/boot/actuate/autoconfigure/metrics/data/RepositoryMetricsAutoConfiguration.class]",

"dependencies": []

},

"org.springdoc.core.configuration.SpringDocConfiguration$QuerydslProvider": {

"aliases": [],

"scope": "singleton",

"type": "org.springdoc.core.configuration.SpringDocConfiguration$QuerydslProvider",

"dependencies": []

},

"redisCacheMeterBinderProvider": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.boot.actuate.metrics.cache.RedisCacheMeterBinderProvider",

"resource": "class path resource [org/springframework/boot/actuate/autoconfigure/metrics/cache/CacheMeterBinderProvidersConfiguration$RedisCacheMeterBinderProviderConfiguration.class]",

"dependencies": [

"org.springframework.boot.actuate.autoconfigure.metrics.cache.CacheMeterBinderProvidersConfiguration$RedisCacheMeterBinderProviderConfiguration"

]

},

"classLoaderMetrics": {

"aliases": [],

"scope": "singleton",

"type": "io.micrometer.core.instrument.binder.jvm.ClassLoaderMetrics",

"resource": "class path resource [org/springframework/boot/actuate/autoconfigure/metrics/JvmMetricsAutoConfiguration.class]",

"dependencies": [

"org.springframework.boot.actuate.autoconfigure.metrics.JvmMetricsAutoConfiguration"

]

},

"observationRestTemplateCustomizer": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.boot.actuate.metrics.web.client.ObservationRestTemplateCustomizer",

"resource": "class path resource [org/springframework/boot/actuate/autoconfigure/observation/web/client/RestTemplateObservationConfiguration.class]",

"dependencies": [

"org.springframework.boot.actuate.autoconfigure.observation.web.client.RestTemplateObservationConfiguration",

"observationRegistry",

"management.observations-org.springframework.boot.actuate.autoconfigure.observation.ObservationProperties"

]

},

"knife4j-com.github.xiaoymin.knife4j.spring.configuration.Knife4jProperties": {

"aliases": [],

"scope": "singleton",

"type": "com.github.xiaoymin.knife4j.spring.configuration.Knife4jProperties",

"dependencies": []

},

"org.springframework.boot.autoconfigure.context.ConfigurationPropertiesAutoConfiguration": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.boot.autoconfigure.context.ConfigurationPropertiesAutoConfiguration",

"dependencies": []

},

"org.springframework.boot.autoconfigure.ssl.SslAutoConfiguration": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.boot.autoconfigure.ssl.SslAutoConfiguration",

"dependencies": [

"spring.ssl-org.springframework.boot.autoconfigure.ssl.SslProperties"

]

},

"org.springframework.boot.autoconfigure.internalCachingMetadataReaderFactory": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.core.type.classreading.CachingMetadataReaderFactory",

"dependencies": []

},

"knife4jJakartaOperationCustomizer": {

"aliases": [],

"scope": "singleton",

"type": "com.github.xiaoymin.knife4j.spring.extension.Knife4jJakartaOperationCustomizer",

"resource": "class path resource [com/github/xiaoymin/knife4j/spring/configuration/Knife4jAutoConfiguration.class]",

"dependencies": [

"com.github.xiaoymin.knife4j.spring.configuration.Knife4jAutoConfiguration"

]

},

"fileWatcher": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.boot.autoconfigure.ssl.FileWatcher",

"resource": "class path resource [org/springframework/boot/autoconfigure/ssl/SslAutoConfiguration.class]",

"dependencies": [

"org.springframework.boot.autoconfigure.ssl.SslAutoConfiguration"

]

},

"org.springframework.boot.autoconfigure.jackson.JacksonAutoConfiguration$ParameterNamesModuleConfiguration": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.boot.autoconfigure.jackson.JacksonAutoConfiguration$ParameterNamesModuleConfiguration",

"dependencies": []

},

"sortResolver": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.data.web.SortHandlerMethodArgumentResolver",

"resource": "class path resource [org/springframework/data/web/config/SpringDataWebConfiguration.class]",

"dependencies": [

"org.springframework.data.web.config.SpringDataWebConfiguration"

]

},

"org.springframework.boot.autoconfigure.sql.init.DataSourceInitializationConfiguration": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.boot.autoconfigure.sql.init.DataSourceInitializationConfiguration",

"dependencies": []

},

"baiduService": {

"aliases": [],

"scope": "singleton",

"type": "cn.netkiller.service.BaiduService$$SpringCGLIB$$0",

"resource": "URL [jar:nested:/app/watch-1.0-SNAPSHOT.jar/!BOOT-INF/classes/!/cn/netkiller/service/BaiduService.class]",

"dependencies": [

"sessionStatusService",

"psychoanalysisService"

]

},

"org.springframework.boot.actuate.autoconfigure.audit.AuditEventsEndpointAutoConfiguration": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.boot.actuate.autoconfigure.audit.AuditEventsEndpointAutoConfiguration",

"dependencies": []

},

"errorAttributes": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.boot.web.servlet.error.DefaultErrorAttributes",

"resource": "class path resource [org/springframework/boot/autoconfigure/web/servlet/error/ErrorMvcAutoConfiguration.class]",

"dependencies": [

"org.springframework.boot.autoconfigure.web.servlet.error.ErrorMvcAutoConfiguration"

]

},

"observationWebClientCustomizer": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.boot.actuate.metrics.web.reactive.client.ObservationWebClientCustomizer",

"resource": "class path resource [org/springframework/boot/actuate/autoconfigure/observation/web/client/WebClientObservationConfiguration.class]",

"dependencies": [

"org.springframework.boot.actuate.autoconfigure.observation.web.client.WebClientObservationConfiguration",

"observationRegistry",

"management.observations-org.springframework.boot.actuate.autoconfigure.observation.ObservationProperties",

"management.metrics-org.springframework.boot.actuate.autoconfigure.metrics.MetricsProperties"

]

},

"beanNameHandlerMapping": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.web.servlet.handler.BeanNameUrlHandlerMapping",

"resource": "class path resource [org/springframework/boot/autoconfigure/web/servlet/WebMvcAutoConfiguration$EnableWebMvcConfiguration.class]",

"dependencies": [

"org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguration$EnableWebMvcConfiguration",

"mvcConversionService",

"mvcResourceUrlProvider"

]

},

"org.springframework.boot.actuate.autoconfigure.health.HealthEndpointConfiguration": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.boot.actuate.autoconfigure.health.HealthEndpointConfiguration",

"dependencies": []

},

"org.springframework.boot.actuate.autoconfigure.metrics.cache.CacheMeterBinderProvidersConfiguration$RedisCacheMeterBinderProviderConfiguration": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.boot.actuate.autoconfigure.metrics.cache.CacheMeterBinderProvidersConfiguration$RedisCacheMeterBinderProviderConfiguration",

"dependencies": []

},

"org.springframework.boot.autoconfigure.info.ProjectInfoAutoConfiguration": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.boot.autoconfigure.info.ProjectInfoAutoConfiguration",

"dependencies": [

"spring.info-org.springframework.boot.autoconfigure.info.ProjectInfoProperties"

]

},

"psychoanalysisService": {

"aliases": [],

"scope": "singleton",

"type": "cn.netkiller.service.PsychoanalysisService$$SpringCGLIB$$0",

"resource": "URL [jar:nested:/app/watch-1.0-SNAPSHOT.jar/!BOOT-INF/classes/!/cn/netkiller/service/PsychoanalysisService.class]",

"dependencies": [

"psychoanalysisRepository",

"chatGPT"

]

},

"org.springframework.boot.autoconfigure.web.servlet.ServletWebServerFactoryConfiguration$EmbeddedTomcat": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.boot.autoconfigure.web.servlet.ServletWebServerFactoryConfiguration$EmbeddedTomcat",

"dependencies": []

},

"org.springframework.boot.actuate.autoconfigure.management.HeapDumpWebEndpointAutoConfiguration": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.boot.actuate.autoconfigure.management.HeapDumpWebEndpointAutoConfiguration",

"dependencies": []

},

"org.springframework.boot.autoconfigure.jdbc.DataSourceConfiguration$Hikari": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.boot.autoconfigure.jdbc.DataSourceConfiguration$Hikari",

"dependencies": []

},

"springDataWebPropertiesProvider": {

"aliases": [],

"scope": "singleton",

"type": "org.springdoc.core.providers.SpringDataWebPropertiesProvider",

"resource": "class path resource [org/springdoc/core/configuration/SpringDocConfiguration$SpringDocSpringDataWebPropertiesProvider.class]",

"dependencies": [

"org.springdoc.core.configuration.SpringDocConfiguration$SpringDocSpringDataWebPropertiesProvider"

]

},

"responseBuilder": {

"aliases": [],

"scope": "singleton",

"type": "org.springdoc.core.service.GenericResponseService",

"resource": "class path resource [org/springdoc/webmvc/core/configuration/SpringDocWebMvcConfiguration.class]",

"dependencies": [

"org.springdoc.webmvc.core.configuration.SpringDocWebMvcConfiguration",

"operationBuilder",

"genericReturnTypeParser",

"kotlinCoroutinesReturnTypeParser",

"org.springdoc.core.properties.SpringDocConfigProperties",

"propertyResolverUtils"

]

},

"org.springframework.data.web.config.SpringDataJacksonConfiguration": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.data.web.config.SpringDataJacksonConfiguration",

"dependencies": []

},

"org.springframework.boot.actuate.autoconfigure.metrics.cache.CacheMetricsRegistrarConfiguration": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.boot.actuate.autoconfigure.metrics.cache.CacheMetricsRegistrarConfiguration",

"dependencies": [

"simpleMeterRegistry",

"redisCacheMeterBinderProvider",

"cacheManager"

]

},

"management.metrics-org.springframework.boot.actuate.autoconfigure.metrics.MetricsProperties": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.boot.actuate.autoconfigure.metrics.MetricsProperties",

"dependencies": []

},

"swaggerWelcome": {

"aliases": [],

"scope": "singleton",

"type": "org.springdoc.webmvc.ui.SwaggerWelcomeWebMvc",

"resource": "class path resource [org/springdoc/webmvc/ui/SwaggerConfig.class]",

"dependencies": [

"org.springdoc.webmvc.ui.SwaggerConfig",

"org.springdoc.core.properties.SwaggerUiConfigProperties",

"org.springdoc.core.properties.SpringDocConfigProperties",

"org.springdoc.core.properties.SwaggerUiConfigParameters",

"springWebProvider"

]

},

"flashMapManager": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.web.servlet.support.SessionFlashMapManager",

"resource": "class path resource [org/springframework/boot/autoconfigure/web/servlet/WebMvcAutoConfiguration$EnableWebMvcConfiguration.class]",

"dependencies": [

"org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguration$EnableWebMvcConfiguration"

]

},

"org.springframework.boot.autoconfigure.task.TaskSchedulingConfigurations$TaskSchedulerBuilderConfiguration": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.boot.autoconfigure.task.TaskSchedulingConfigurations$TaskSchedulerBuilderConfiguration",

"dependencies": []

},

"pictureRepository": {

"aliases": [],

"scope": "singleton",

"type": "cn.netkiller.repository.PictureRepository",

"resource": "cn.netkiller.repository.PictureRepository defined in @EnableJpaRepositories declared on Application",

"dependencies": [

"jpa.named-queries#2",

"jpa.PictureRepository.fragments#0",

"jpaSharedEM_entityManagerFactory",

"jpaMappingContext"

]

},

"requestBuilder": {

"aliases": [],

"scope": "singleton",

"type": "org.springdoc.webmvc.core.service.RequestService",

"resource": "class path resource [org/springdoc/webmvc/core/configuration/SpringDocWebMvcConfiguration.class]",

"dependencies": [

"org.springdoc.webmvc.core.configuration.SpringDocWebMvcConfiguration",

"parameterBuilder",

"requestBodyBuilder",

"operationBuilder",

"localSpringDocParameterNameDiscoverer"

]

},

"redisHealthContributor": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.boot.actuate.data.redis.RedisReactiveHealthIndicator",

"resource": "class path resource [org/springframework/boot/actuate/autoconfigure/data/redis/RedisReactiveHealthContributorAutoConfiguration.class]",

"dependencies": [

"org.springframework.boot.actuate.autoconfigure.data.redis.RedisReactiveHealthContributorAutoConfiguration"

]

},

"org.springframework.boot.autoconfigure.netty.NettyAutoConfiguration": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.boot.autoconfigure.netty.NettyAutoConfiguration",

"dependencies": [

"spring.netty-org.springframework.boot.autoconfigure.netty.NettyProperties"

]

},

"healthHttpCodeStatusMapper": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.boot.actuate.health.SimpleHttpCodeStatusMapper",

"resource": "class path resource [org/springframework/boot/actuate/autoconfigure/health/HealthEndpointConfiguration.class]",

"dependencies": [

"org.springframework.boot.actuate.autoconfigure.health.HealthEndpointConfiguration",

"management.endpoint.health-org.springframework.boot.actuate.autoconfigure.health.HealthEndpointProperties"

]

},

"psychoanalysisRepository": {

"aliases": [],

"scope": "singleton",

"type": "cn.netkiller.repository.PsychoanalysisRepository",

"resource": "cn.netkiller.repository.PsychoanalysisRepository defined in @EnableJpaRepositories declared on Application",

"dependencies": [

"jpa.named-queries#4",

"jpa.PsychoanalysisRepository.fragments#0",

"jpaSharedEM_entityManagerFactory",

"jpaMappingContext"

]

},

"defaultCodecCustomizer": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.boot.autoconfigure.http.codec.CodecsAutoConfiguration$DefaultCodecsConfiguration$$Lambda/0x00007ff400a34e48",

"resource": "class path resource [org/springframework/boot/autoconfigure/http/codec/CodecsAutoConfiguration$DefaultCodecsConfiguration.class]",

"dependencies": [

"org.springframework.boot.autoconfigure.http.codec.CodecsAutoConfiguration$DefaultCodecsConfiguration",

"spring.codec-org.springframework.boot.autoconfigure.codec.CodecProperties"

]

},

"statusController": {

"aliases": [],

"scope": "singleton",

"type": "cn.netkiller.controller.StatusController",

"resource": "URL [jar:nested:/app/watch-1.0-SNAPSHOT.jar/!BOOT-INF/classes/!/cn/netkiller/controller/StatusController.class]",

"dependencies": [

"sessionStatusRepository"

]

},

"org.springframework.boot.actuate.autoconfigure.metrics.jdbc.DataSourcePoolMetricsAutoConfiguration": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.boot.actuate.autoconfigure.metrics.jdbc.DataSourcePoolMetricsAutoConfiguration",

"dependencies": []

},

"metricsRepositoryMethodInvocationListener": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.boot.actuate.metrics.data.MetricsRepositoryMethodInvocationListener",

"resource": "class path resource [org/springframework/boot/actuate/autoconfigure/metrics/data/RepositoryMetricsAutoConfiguration.class]",

"dependencies": [

"org.springframework.boot.actuate.autoconfigure.metrics.data.RepositoryMetricsAutoConfiguration",

"repositoryTagsProvider"

]

},

"uptimeMetrics": {

"aliases": [],

"scope": "singleton",

"type": "io.micrometer.core.instrument.binder.system.UptimeMetrics",

"resource": "class path resource [org/springframework/boot/actuate/autoconfigure/metrics/SystemMetricsAutoConfiguration.class]",

"dependencies": [

"org.springframework.boot.actuate.autoconfigure.metrics.SystemMetricsAutoConfiguration"

]

},

"controllerExposeExcludePropertyEndpointFilter": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.boot.actuate.autoconfigure.endpoint.expose.IncludeExcludeEndpointFilter",

"resource": "class path resource [org/springframework/boot/actuate/autoconfigure/endpoint/web/WebEndpointAutoConfiguration.class]",

"dependencies": [

"org.springframework.boot.actuate.autoconfigure.endpoint.web.WebEndpointAutoConfiguration"

]

},

"pathMappedEndpoints": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.boot.actuate.endpoint.web.PathMappedEndpoints",

"resource": "class path resource [org/springframework/boot/actuate/autoconfigure/endpoint/web/WebEndpointAutoConfiguration.class]",

"dependencies": [

"org.springframework.boot.actuate.autoconfigure.endpoint.web.WebEndpointAutoConfiguration",

"servletEndpointDiscoverer",

"webEndpointDiscoverer",

"controllerEndpointDiscoverer"

]

},

"jvmThreadMetrics": {

"aliases": [],

"scope": "singleton",

"type": "io.micrometer.core.instrument.binder.jvm.JvmThreadMetrics",

"resource": "class path resource [org/springframework/boot/actuate/autoconfigure/metrics/JvmMetricsAutoConfiguration.class]",

"dependencies": [

"org.springframework.boot.actuate.autoconfigure.metrics.JvmMetricsAutoConfiguration"

]

},

"scheduledTasksEndpoint": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.boot.actuate.scheduling.ScheduledTasksEndpoint",

"resource": "class path resource [org/springframework/boot/actuate/autoconfigure/scheduling/ScheduledTasksEndpointAutoConfiguration.class]",

"dependencies": [

"org.springframework.boot.actuate.autoconfigure.scheduling.ScheduledTasksEndpointAutoConfiguration"

]

},

"indexPageTransformer": {

"aliases": [],

"scope": "singleton",

"type": "org.springdoc.webmvc.ui.SwaggerIndexPageTransformer",

"resource": "class path resource [org/springdoc/webmvc/ui/SwaggerConfig.class]",

"dependencies": [

"org.springdoc.webmvc.ui.SwaggerConfig",

"org.springdoc.core.properties.SwaggerUiConfigProperties",

"org.springdoc.core.properties.SwaggerUiOAuthProperties",

"org.springdoc.core.properties.SwaggerUiConfigParameters",

"swaggerWelcome",

"springdocObjectMapperProvider"

]

},

"org.springframework.data.web.config.ProjectingArgumentResolverRegistrar": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.data.web.config.ProjectingArgumentResolverRegistrar",

"dependencies": []

},

"heapDumpWebEndpoint": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.boot.actuate.management.HeapDumpWebEndpoint",

"resource": "class path resource [org/springframework/boot/actuate/autoconfigure/management/HeapDumpWebEndpointAutoConfiguration.class]",

"dependencies": [

"org.springframework.boot.actuate.autoconfigure.management.HeapDumpWebEndpointAutoConfiguration"

]

},

"managementServletContext": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.boot.actuate.autoconfigure.web.servlet.ServletManagementContextAutoConfiguration$$Lambda/0x00007ff400ae9a58",

"resource": "class path resource [org/springframework/boot/actuate/autoconfigure/web/servlet/ServletManagementContextAutoConfiguration.class]",

"dependencies": [

"org.springframework.boot.actuate.autoconfigure.web.servlet.ServletManagementContextAutoConfiguration",

"management.endpoints.web-org.springframework.boot.actuate.autoconfigure.endpoint.web.WebEndpointProperties"

]

},

"spring.gson-org.springframework.boot.autoconfigure.gson.GsonProperties": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.boot.autoconfigure.gson.GsonProperties",

"dependencies": []

},

"fileDescriptorMetrics": {

"aliases": [],

"scope": "singleton",

"type": "io.micrometer.core.instrument.binder.system.FileDescriptorMetrics",

"resource": "class path resource [org/springframework/boot/actuate/autoconfigure/metrics/SystemMetricsAutoConfiguration.class]",

"dependencies": [

"org.springframework.boot.actuate.autoconfigure.metrics.SystemMetricsAutoConfiguration"

]

},

"org.springframework.boot.autoconfigure.aop.AopAutoConfiguration$AspectJAutoProxyingConfiguration$CglibAutoProxyConfiguration": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.boot.autoconfigure.aop.AopAutoConfiguration$AspectJAutoProxyingConfiguration$CglibAutoProxyConfiguration",

"dependencies": []

},

"org.springframework.boot.actuate.autoconfigure.health.ReactiveHealthEndpointConfiguration": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.boot.actuate.autoconfigure.health.ReactiveHealthEndpointConfiguration",

"dependencies": []

},

"org.springframework.boot.actuate.autoconfigure.metrics.cache.CacheMeterBinderProvidersConfiguration": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.boot.actuate.autoconfigure.metrics.cache.CacheMeterBinderProvidersConfiguration",

"dependencies": []

},

"org.springframework.boot.actuate.autoconfigure.observation.ObservationAutoConfiguration$MeterObservationHandlerConfiguration$OnlyMetricsMeterObservationHandlerConfiguration": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.boot.actuate.autoconfigure.observation.ObservationAutoConfiguration$MeterObservationHandlerConfiguration$OnlyMetricsMeterObservationHandlerConfiguration",

"dependencies": []

},

"environmentEndpointWebExtension": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.boot.actuate.env.EnvironmentEndpointWebExtension",

"resource": "class path resource [org/springframework/boot/actuate/autoconfigure/env/EnvironmentEndpointAutoConfiguration.class]",

"dependencies": [

"org.springframework.boot.actuate.autoconfigure.env.EnvironmentEndpointAutoConfiguration",

"environmentEndpoint",

"management.endpoint.env-org.springframework.boot.actuate.autoconfigure.env.EnvironmentEndpointProperties"

]

},

"restClientBuilderConfigurer": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.boot.autoconfigure.web.client.RestClientBuilderConfigurer",

"resource": "class path resource [org/springframework/boot/autoconfigure/web/client/RestClientAutoConfiguration.class]",

"dependencies": [

"org.springframework.boot.autoconfigure.web.client.RestClientAutoConfiguration"

]

},

"mvcValidator": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.boot.autoconfigure.validation.ValidatorAdapter",

"resource": "class path resource [org/springframework/boot/autoconfigure/web/servlet/WebMvcAutoConfiguration$EnableWebMvcConfiguration.class]",

"dependencies": [

"org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguration$EnableWebMvcConfiguration"

]

},

"conditionsReportEndpoint": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.boot.actuate.autoconfigure.condition.ConditionsReportEndpoint",

"resource": "class path resource [org/springframework/boot/actuate/autoconfigure/condition/ConditionsReportEndpointAutoConfiguration.class]",

"dependencies": [

"org.springframework.boot.actuate.autoconfigure.condition.ConditionsReportEndpointAutoConfiguration",

"org.springframework.boot.web.servlet.context.AnnotationConfigServletWebServerApplicationContext@18e36d14"

]

},

"applicationAvailability": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.boot.availability.ApplicationAvailabilityBean",

"resource": "class path resource [org/springframework/boot/autoconfigure/availability/ApplicationAvailabilityAutoConfiguration.class]",

"dependencies": [

"org.springframework.boot.autoconfigure.availability.ApplicationAvailabilityAutoConfiguration"

]

},

"org.springdoc.webmvc.ui.SwaggerConfig": {

"aliases": [],

"scope": "singleton",

"type": "org.springdoc.webmvc.ui.SwaggerConfig",

"dependencies": []

},

"mvcResourceUrlProvider": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.web.servlet.resource.ResourceUrlProvider",

"resource": "class path resource [org/springframework/boot/autoconfigure/web/servlet/WebMvcAutoConfiguration$EnableWebMvcConfiguration.class]",

"dependencies": [

"org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguration$EnableWebMvcConfiguration"

]

},

"org.springframework.boot.actuate.autoconfigure.web.server.ManagementContextAutoConfiguration$SameManagementContextConfiguration$EnableSameManagementContextConfiguration": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.boot.actuate.autoconfigure.web.server.ManagementContextAutoConfiguration$SameManagementContextConfiguration$EnableSameManagementContextConfiguration",

"dependencies": []

},

"reactorClientHttpConnectorFactory": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.boot.autoconfigure.web.reactive.function.client.ReactorClientHttpConnectorFactory",

"resource": "class path resource [org/springframework/boot/autoconfigure/web/reactive/function/client/ClientHttpConnectorFactoryConfiguration$ReactorNetty.class]",

"dependencies": [

"org.springframework.boot.autoconfigure.web.reactive.function.client.ClientHttpConnectorFactoryConfiguration$ReactorNetty",

"reactorResourceFactory"

]

},

"org.springframework.boot.autoconfigure.task.TaskExecutorConfigurations$SimpleAsyncTaskExecutorBuilderConfiguration": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.boot.autoconfigure.task.TaskExecutorConfigurations$SimpleAsyncTaskExecutorBuilderConfiguration",

"dependencies": [

"spring.task.execution-org.springframework.boot.autoconfigure.task.TaskExecutionProperties"

]

},

"servletEndpointRegistrar": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.boot.actuate.endpoint.web.ServletEndpointRegistrar",

"resource": "class path resource [org/springframework/boot/actuate/autoconfigure/endpoint/web/ServletEndpointManagementContextConfiguration$WebMvcServletEndpointManagementContextConfiguration.class]",

"dependencies": [

"org.springframework.boot.actuate.autoconfigure.endpoint.web.ServletEndpointManagementContextConfiguration$WebMvcServletEndpointManagementContextConfiguration",

"management.endpoints.web-org.springframework.boot.actuate.autoconfigure.endpoint.web.WebEndpointProperties",

"servletEndpointDiscoverer",

"dispatcherServletRegistration"

]

},

"dumpEndpoint": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.boot.actuate.management.ThreadDumpEndpoint",

"resource": "class path resource [org/springframework/boot/actuate/autoconfigure/management/ThreadDumpEndpointAutoConfiguration.class]",

"dependencies": [

"org.springframework.boot.actuate.autoconfigure.management.ThreadDumpEndpointAutoConfiguration"

]

},

"springdocObjectMapperProvider": {

"aliases": [],

"scope": "singleton",

"type": "org.springdoc.core.providers.ObjectMapperProvider",

"resource": "class path resource [org/springdoc/core/configuration/SpringDocConfiguration.class]",

"dependencies": [

"org.springdoc.core.configuration.SpringDocConfiguration",

"org.springdoc.core.properties.SpringDocConfigProperties"

]

},

"org.springframework.boot.actuate.autoconfigure.endpoint.web.ServletEndpointManagementContextConfiguration": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.boot.actuate.autoconfigure.endpoint.web.ServletEndpointManagementContextConfiguration",

"dependencies": []

},

"org.springframework.boot.autoconfigure.data.redis.RedisReactiveAutoConfiguration": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.boot.autoconfigure.data.redis.RedisReactiveAutoConfiguration",

"dependencies": []

},

"knife4j.basic-com.github.xiaoymin.knife4j.spring.configuration.Knife4jHttpBasic": {

"aliases": [],

"scope": "singleton",

"type": "com.github.xiaoymin.knife4j.spring.configuration.Knife4jHttpBasic",

"dependencies": []

},

"hikariPoolDataSourceMetadataProvider": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.boot.autoconfigure.jdbc.metadata.DataSourcePoolMetadataProvidersConfiguration$HikariPoolDataSourceMetadataProviderConfiguration$$Lambda/0x00007ff4005b7c00",

"resource": "class path resource [org/springframework/boot/autoconfigure/jdbc/metadata/DataSourcePoolMetadataProvidersConfiguration$HikariPoolDataSourceMetadataProviderConfiguration.class]",

"dependencies": [

"org.springframework.boot.autoconfigure.jdbc.metadata.DataSourcePoolMetadataProvidersConfiguration$HikariPoolDataSourceMetadataProviderConfiguration"

]

},

"org.springframework.boot.autoconfigure.task.TaskSchedulingConfigurations$SimpleAsyncTaskSchedulerBuilderConfiguration": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.boot.autoconfigure.task.TaskSchedulingConfigurations$SimpleAsyncTaskSchedulerBuilderConfiguration",

"dependencies": [

"spring.task.scheduling-org.springframework.boot.autoconfigure.task.TaskSchedulingProperties"

]

},

"org.springframework.boot.autoconfigure.cache.CacheAutoConfiguration$CacheManagerEntityManagerFactoryDependsOnPostProcessor": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.boot.autoconfigure.cache.CacheAutoConfiguration$CacheManagerEntityManagerFactoryDependsOnPostProcessor",

"dependencies": []

},

"org.springframework.boot.autoconfigure.task.TaskExecutorConfigurations$TaskExecutorBuilderConfiguration": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.boot.autoconfigure.task.TaskExecutorConfigurations$TaskExecutorBuilderConfiguration",

"dependencies": []

},

"org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration",

"dependencies": []

},

"org.springframework.boot.actuate.autoconfigure.observation.ObservationAutoConfiguration$MeterObservationHandlerConfiguration": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.boot.actuate.autoconfigure.observation.ObservationAutoConfiguration$MeterObservationHandlerConfiguration",

"dependencies": []

},

"nettyWebServerFactoryCustomizer": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.boot.autoconfigure.web.embedded.NettyWebServerFactoryCustomizer",

"resource": "class path resource [org/springframework/boot/autoconfigure/web/embedded/EmbeddedWebServerFactoryCustomizerAutoConfiguration$NettyWebServerFactoryCustomizerConfiguration.class]",

"dependencies": [

"org.springframework.boot.autoconfigure.web.embedded.EmbeddedWebServerFactoryCustomizerAutoConfiguration$NettyWebServerFactoryCustomizerConfiguration",

"environment",

"server-org.springframework.boot.autoconfigure.web.ServerProperties"

]

},

"org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguration$WebMvcAutoConfigurationAdapter": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguration$WebMvcAutoConfigurationAdapter",

"dependencies": [

"spring.web-org.springframework.boot.autoconfigure.web.WebProperties",

"spring.mvc-org.springframework.boot.autoconfigure.web.servlet.WebMvcProperties",

"org.springframework.beans.factory.support.DefaultListableBeanFactory@37efd131"

]

},

"org.springframework.data.web.config.SpringDataWebConfiguration": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.data.web.config.SpringDataWebConfiguration",

"dependencies": [

"org.springframework.boot.web.servlet.context.AnnotationConfigServletWebServerApplicationContext@18e36d14"

]

},

"errorPageRegistrarBeanPostProcessor": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.boot.web.server.ErrorPageRegistrarBeanPostProcessor",

"dependencies": []

},

"mvcConversionService": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.boot.autoconfigure.web.format.WebConversionService",

"resource": "class path resource [org/springframework/boot/autoconfigure/web/servlet/WebMvcAutoConfiguration$EnableWebMvcConfiguration.class]",

"dependencies": [

"org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguration$EnableWebMvcConfiguration"

]

},

"redisTemplate": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.data.redis.core.RedisTemplate",

"resource": "class path resource [org/springframework/boot/autoconfigure/data/redis/RedisAutoConfiguration.class]",

"dependencies": [

"org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration",

"redisConnectionFactory"

]

},

"memberService": {

"aliases": [],

"scope": "singleton",

"type": "cn.netkiller.service.MemberService",

"resource": "URL [jar:nested:/app/watch-1.0-SNAPSHOT.jar/!BOOT-INF/classes/!/cn/netkiller/service/MemberService.class]",

"dependencies": []

},

"spring.codec-org.springframework.boot.autoconfigure.codec.CodecProperties": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.boot.autoconfigure.codec.CodecProperties",

"dependencies": []

},

"controllerEndpointDiscoverer": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.boot.actuate.endpoint.web.annotation.ControllerEndpointDiscoverer",

"resource": "class path resource [org/springframework/boot/actuate/autoconfigure/endpoint/web/WebEndpointAutoConfiguration.class]",

"dependencies": [

"org.springframework.boot.actuate.autoconfigure.endpoint.web.WebEndpointAutoConfiguration"

]

},

"diskSpaceHealthIndicator": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.boot.actuate.system.DiskSpaceHealthIndicator",

"resource": "class path resource [org/springframework/boot/actuate/autoconfigure/system/DiskSpaceHealthContributorAutoConfiguration.class]",

"dependencies": [

"org.springframework.boot.actuate.autoconfigure.system.DiskSpaceHealthContributorAutoConfiguration",

"management.health.diskspace-org.springframework.boot.actuate.autoconfigure.system.DiskSpaceHealthIndicatorProperties"

]

},

"jvmMemoryMetrics": {

"aliases": [],

"scope": "singleton",

"type": "io.micrometer.core.instrument.binder.jvm.JvmMemoryMetrics",

"resource": "class path resource [org/springframework/boot/actuate/autoconfigure/metrics/JvmMetricsAutoConfiguration.class]",

"dependencies": [

"org.springframework.boot.actuate.autoconfigure.metrics.JvmMetricsAutoConfiguration"

]

},

"storyService": {

"aliases": [],

"scope": "singleton",

"type": "cn.netkiller.service.StoryService$$SpringCGLIB$$0",

"resource": "URL [jar:nested:/app/watch-1.0-SNAPSHOT.jar/!BOOT-INF/classes/!/cn/netkiller/service/StoryService.class]",

"dependencies": [

"pictureRepository",

"mqttService",

"baiduService",

"sessionStatusService",

"pictureService",

"psychoanalysisService",

"stableDiffusionService",

"audioService",

"warningService",

"chatService",

"chatGPT",

"aliyunService"

]

},

"modelConverterRegistrar": {

"aliases": [],

"scope": "singleton",

"type": "org.springdoc.core.converters.ModelConverterRegistrar",

"resource": "class path resource [org/springdoc/core/configuration/SpringDocConfiguration.class]",

"dependencies": [

"org.springdoc.core.configuration.SpringDocConfiguration",

"org.springdoc.core.properties.SpringDocConfigProperties"

]

},

"restClientSsl": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.boot.autoconfigure.web.client.AutoConfiguredRestClientSsl",

"resource": "class path resource [org/springframework/boot/autoconfigure/web/client/RestClientAutoConfiguration.class]",

"dependencies": [

"org.springframework.boot.autoconfigure.web.client.RestClientAutoConfiguration",

"sslBundleRegistry"

]

},

"org.springframework.boot.actuate.autoconfigure.condition.ConditionsReportEndpointAutoConfiguration": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.boot.actuate.autoconfigure.condition.ConditionsReportEndpointAutoConfiguration",

"dependencies": []

},

"com.github.xiaoymin.knife4j.spring.configuration.Knife4jAutoConfiguration": {

"aliases": [],

"scope": "singleton",

"type": "com.github.xiaoymin.knife4j.spring.configuration.Knife4jAutoConfiguration$$SpringCGLIB$$0",

"dependencies": [

"knife4j-com.github.xiaoymin.knife4j.spring.configuration.Knife4jProperties",

"environment"

]

},

"logFileWebEndpoint": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.boot.actuate.logging.LogFileWebEndpoint",

"resource": "class path resource [org/springframework/boot/actuate/autoconfigure/logging/LogFileWebEndpointAutoConfiguration.class]",

"dependencies": [

"org.springframework.boot.actuate.autoconfigure.logging.LogFileWebEndpointAutoConfiguration",

"management.endpoint.logfile-org.springframework.boot.actuate.autoconfigure.logging.LogFileWebEndpointProperties"

]

},

"mvcPathMatcher": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.util.AntPathMatcher",

"resource": "class path resource [org/springframework/boot/autoconfigure/web/servlet/WebMvcAutoConfiguration$EnableWebMvcConfiguration.class]",

"dependencies": [

"org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguration$EnableWebMvcConfiguration"

]

},

"org.springframework.boot.actuate.autoconfigure.metrics.redis.LettuceMetricsAutoConfiguration": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.boot.actuate.autoconfigure.metrics.redis.LettuceMetricsAutoConfiguration",

"dependencies": []

},

"handlerExceptionResolver": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.web.servlet.handler.HandlerExceptionResolverComposite",

"resource": "class path resource [org/springframework/boot/autoconfigure/web/servlet/WebMvcAutoConfiguration$EnableWebMvcConfiguration.class]",

"dependencies": [

"org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguration$EnableWebMvcConfiguration",

"mvcContentNegotiationManager"

]

},

"routerFunctionProvider": {

"aliases": [],

"scope": "singleton",

"type": "org.springdoc.webmvc.core.providers.RouterFunctionWebMvcProvider",

"resource": "class path resource [org/springdoc/webmvc/core/configuration/SpringDocWebMvcConfiguration$SpringDocWebMvcRouterConfiguration.class]",

"dependencies": [

"org.springdoc.webmvc.core.configuration.SpringDocWebMvcConfiguration$SpringDocWebMvcRouterConfiguration"

]

},

"redisKeyValueTemplate": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.data.redis.core.RedisKeyValueTemplate",

"dependencies": [

"redisKeyValueAdapter",

"keyValueMappingContext"

]

},

"basicErrorController": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.boot.autoconfigure.web.servlet.error.BasicErrorController",

"resource": "class path resource [org/springframework/boot/autoconfigure/web/servlet/error/ErrorMvcAutoConfiguration.class]",

"dependencies": [

"org.springframework.boot.autoconfigure.web.servlet.error.ErrorMvcAutoConfiguration",

"errorAttributes"

]

},

"pingHealthContributor": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.boot.actuate.health.PingHealthIndicator",

"resource": "class path resource [org/springframework/boot/actuate/autoconfigure/health/HealthContributorAutoConfiguration.class]",

"dependencies": [

"org.springframework.boot.actuate.autoconfigure.health.HealthContributorAutoConfiguration"

]

},

"org.springdoc.core.configuration.SpringDocConfiguration$SpringDocWebFluxSupportConfiguration": {

"aliases": [],

"scope": "singleton",

"type": "org.springdoc.core.configuration.SpringDocConfiguration$SpringDocWebFluxSupportConfiguration",

"dependencies": []

},

"mappingsEndpoint": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.boot.actuate.web.mappings.MappingsEndpoint",

"resource": "class path resource [org/springframework/boot/actuate/autoconfigure/web/mappings/MappingsEndpointAutoConfiguration.class]",

"dependencies": [

"org.springframework.boot.actuate.autoconfigure.web.mappings.MappingsEndpointAutoConfiguration",

"org.springframework.boot.web.servlet.context.AnnotationConfigServletWebServerApplicationContext@18e36d14"

]

},

"sortOpenAPIConverter": {

"aliases": [],

"scope": "singleton",

"type": "org.springdoc.core.converters.SortOpenAPIConverter",

"resource": "class path resource [org/springdoc/core/configuration/SpringDocSortConfiguration.class]",

"dependencies": [

"org.springdoc.core.configuration.SpringDocSortConfiguration",

"springdocObjectMapperProvider"

]

},

"healthEndpointGroupMembershipValidator": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.boot.actuate.autoconfigure.health.HealthEndpointConfiguration$HealthEndpointGroupMembershipValidator",

"resource": "class path resource [org/springframework/boot/actuate/autoconfigure/health/HealthEndpointConfiguration.class]",

"dependencies": [

"org.springframework.boot.actuate.autoconfigure.health.HealthEndpointConfiguration",

"management.endpoint.health-org.springframework.boot.actuate.autoconfigure.health.HealthEndpointProperties",

"healthContributorRegistry"

]

},

"org.springframework.boot.autoconfigure.http.codec.CodecsAutoConfiguration$JacksonCodecConfiguration": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.boot.autoconfigure.http.codec.CodecsAutoConfiguration$JacksonCodecConfiguration",

"dependencies": []

},

"org.springframework.boot.autoconfigure.cache.CacheAutoConfiguration": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.boot.autoconfigure.cache.CacheAutoConfiguration",

"dependencies": []

},

"org.springframework.boot.autoconfigure.http.GsonHttpMessageConvertersConfiguration": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.boot.autoconfigure.http.GsonHttpMessageConvertersConfiguration",

"dependencies": []

},

"spring.datasource-org.springframework.boot.autoconfigure.jdbc.DataSourceProperties": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.boot.autoconfigure.jdbc.DataSourceProperties",

"dependencies": []

},

"namedParameterJdbcTemplate": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate",

"resource": "class path resource [org/springframework/boot/autoconfigure/jdbc/NamedParameterJdbcTemplateConfiguration.class]",

"dependencies": [

"dataSourceScriptDatabaseInitializer",

"org.springframework.boot.autoconfigure.jdbc.NamedParameterJdbcTemplateConfiguration",

"jdbcTemplate"

]

},

"simpleAsyncTaskExecutorBuilder": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.boot.task.SimpleAsyncTaskExecutorBuilder",

"resource": "class path resource [org/springframework/boot/autoconfigure/task/TaskExecutorConfigurations$SimpleAsyncTaskExecutorBuilderConfiguration.class]",

"dependencies": [

"org.springframework.boot.autoconfigure.task.TaskExecutorConfigurations$SimpleAsyncTaskExecutorBuilderConfiguration"

]

},

"spring.web-org.springframework.boot.autoconfigure.web.WebProperties": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.boot.autoconfigure.web.WebProperties",

"dependencies": []

},

"mvcViewResolver": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.web.servlet.view.ViewResolverComposite",

"resource": "class path resource [org/springframework/boot/autoconfigure/web/servlet/WebMvcAutoConfiguration$EnableWebMvcConfiguration.class]",

"dependencies": [

"org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguration$EnableWebMvcConfiguration",

"mvcContentNegotiationManager"

]

},

"simpleConfig": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.boot.actuate.autoconfigure.metrics.export.simple.SimplePropertiesConfigAdapter",

"resource": "class path resource [org/springframework/boot/actuate/autoconfigure/metrics/export/simple/SimpleMetricsExportAutoConfiguration.class]",

"dependencies": [

"org.springframework.boot.actuate.autoconfigure.metrics.export.simple.SimpleMetricsExportAutoConfiguration",

"management.simple.metrics.export-org.springframework.boot.actuate.autoconfigure.metrics.export.simple.SimpleProperties"

]

},

"org.springframework.boot.actuate.autoconfigure.observation.web.client.RestClientObservationConfiguration": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.boot.actuate.autoconfigure.observation.web.client.RestClientObservationConfiguration",

"dependencies": []

},

"org.springframework.boot.actuate.autoconfigure.web.servlet.ServletManagementContextAutoConfiguration": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.boot.actuate.autoconfigure.web.servlet.ServletManagementContextAutoConfiguration",

"dependencies": []

},

"observationRegistry": {

"aliases": [],

"scope": "singleton",

"type": "io.micrometer.observation.SimpleObservationRegistry",

"resource": "class path resource [org/springframework/boot/actuate/autoconfigure/observation/ObservationAutoConfiguration.class]",

"dependencies": [

"org.springframework.boot.actuate.autoconfigure.observation.ObservationAutoConfiguration"

]

},

"mvcUriComponentsContributor": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.web.method.support.CompositeUriComponentsContributor",

"resource": "class path resource [org/springframework/boot/autoconfigure/web/servlet/WebMvcAutoConfiguration$EnableWebMvcConfiguration.class]",

"dependencies": [

"org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguration$EnableWebMvcConfiguration",

"mvcConversionService",

"requestMappingHandlerAdapter"

]

},

"webClientBuilder": {

"aliases": [],

"scope": "prototype",

"type": "org.springframework.web.reactive.function.client.WebClient$Builder",

"resource": "class path resource [org/springframework/boot/autoconfigure/web/reactive/function/client/WebClientAutoConfiguration.class]",

"dependencies": []

},

"org.springframework.boot.actuate.autoconfigure.web.server.ManagementContextAutoConfiguration": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.boot.actuate.autoconfigure.web.server.ManagementContextAutoConfiguration",

"dependencies": []

},

"endpointObjectMapper": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.boot.actuate.autoconfigure.endpoint.jackson.JacksonEndpointAutoConfiguration$$Lambda/0x00007ff400a51c30",

"resource": "class path resource [org/springframework/boot/actuate/autoconfigure/endpoint/jackson/JacksonEndpointAutoConfiguration.class]",

"dependencies": [

"org.springframework.boot.actuate.autoconfigure.endpoint.jackson.JacksonEndpointAutoConfiguration"

]

},

"jpaSharedEM_entityManagerFactory": {

"aliases": [],

"scope": "singleton",

"type": "jdk.proxy2.$Proxy155",

"dependencies": [

"entityManagerFactory"

]

},

"application": {

"aliases": [],

"scope": "singleton",

"type": "cn.netkiller.Application$$SpringCGLIB$$0",

"dependencies": []

},

"httpMessageConvertersRestClientCustomizer": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.boot.autoconfigure.web.client.HttpMessageConvertersRestClientCustomizer",

"resource": "class path resource [org/springframework/boot/autoconfigure/web/client/RestClientAutoConfiguration.class]",

"dependencies": [

"org.springframework.boot.autoconfigure.web.client.RestClientAutoConfiguration"

]

},

"redisMappingConfiguration#0": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.data.redis.core.convert.MappingConfiguration",

"dependencies": [

"redisIndexConfiguration#0",

"redisKeyspaceConfiguration#0"

]

},

"taskExecutorBuilder": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.boot.task.TaskExecutorBuilder",

"resource": "class path resource [org/springframework/boot/autoconfigure/task/TaskExecutorConfigurations$TaskExecutorBuilderConfiguration.class]",

"dependencies": [

"org.springframework.boot.autoconfigure.task.TaskExecutorConfigurations$TaskExecutorBuilderConfiguration",

"spring.task.execution-org.springframework.boot.autoconfigure.task.TaskExecutionProperties"

]

},

"org.springframework.boot.autoconfigure.jdbc.JdbcTemplateConfiguration": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.boot.autoconfigure.jdbc.JdbcTemplateConfiguration",

"dependencies": []

},

"queryDslQuerydslPredicateOperationCustomizer": {

"aliases": [],

"scope": "singleton",

"type": "org.springdoc.core.customizers.QuerydslPredicateOperationCustomizer",

"resource": "class path resource [org/springdoc/core/configuration/SpringDocConfiguration$QuerydslProvider.class]",

"dependencies": [

"org.springdoc.core.configuration.SpringDocConfiguration$QuerydslProvider",

"org.springdoc.core.properties.SpringDocConfigProperties"

]

},

"org.springframework.boot.actuate.autoconfigure.endpoint.jackson.JacksonEndpointAutoConfiguration": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.boot.actuate.autoconfigure.endpoint.jackson.JacksonEndpointAutoConfiguration",

"dependencies": []

},

"multipleOpenApiResource": {

"aliases": [],

"scope": "singleton",

"type": "org.springdoc.webmvc.api.MultipleOpenApiWebMvcResource",

"resource": "class path resource [org/springdoc/webmvc/core/configuration/MultipleOpenApiSupportConfiguration.class]",

"dependencies": [

"org.springdoc.webmvc.core.configuration.MultipleOpenApiSupportConfiguration",

"adminApi",

"requestBuilder",

"responseBuilder",

"operationBuilder",

"org.springdoc.core.properties.SpringDocConfigProperties",

"springDocProviders",

"springDocCustomizers"

]

},

"platformTransactionManagerCustomizers": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.boot.autoconfigure.transaction.TransactionManagerCustomizers",

"resource": "class path resource [org/springframework/boot/autoconfigure/transaction/TransactionManagerCustomizationAutoConfiguration.class]",

"dependencies": [

"org.springframework.boot.autoconfigure.transaction.TransactionManagerCustomizationAutoConfiguration"

]

},

"requestBodyBuilder": {

"aliases": [],

"scope": "singleton",

"type": "org.springdoc.core.service.RequestBodyService",

"resource": "class path resource [org/springdoc/core/configuration/SpringDocConfiguration.class]",

"dependencies": [

"org.springdoc.core.configuration.SpringDocConfiguration",

"parameterBuilder"

]

},

"endpointCachingOperationInvokerAdvisor": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.boot.actuate.endpoint.invoker.cache.CachingOperationInvokerAdvisor",

"resource": "class path resource [org/springframework/boot/actuate/autoconfigure/endpoint/EndpointAutoConfiguration.class]",

"dependencies": [

"org.springframework.boot.actuate.autoconfigure.endpoint.EndpointAutoConfiguration",

"environment"

]

},

"defaultServletHandlerMapping": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.web.servlet.HandlerMapping",

"resource": "class path resource [org/springframework/boot/autoconfigure/web/servlet/WebMvcAutoConfiguration$EnableWebMvcConfiguration.class]",

"dependencies": [

"org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguration$EnableWebMvcConfiguration"

]

},

"swaggerWebMvcConfigurer": {

"aliases": [],

"scope": "singleton",

"type": "org.springdoc.webmvc.ui.SwaggerWebMvcConfigurer",

"resource": "class path resource [org/springdoc/webmvc/ui/SwaggerConfig.class]",

"dependencies": [

"org.springdoc.webmvc.ui.SwaggerConfig",

"org.springdoc.core.properties.SwaggerUiConfigParameters",

"indexPageTransformer",

"swaggerResourceResolver"

]

},

"persistenceExceptionTranslationPostProcessor": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.dao.annotation.PersistenceExceptionTranslationPostProcessor",

"resource": "class path resource [org/springframework/boot/autoconfigure/dao/PersistenceExceptionTranslationAutoConfiguration.class]",

"dependencies": [

"environment"

]

},

"management.health.db-org.springframework.boot.actuate.autoconfigure.jdbc.DataSourceHealthIndicatorProperties": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.boot.actuate.autoconfigure.jdbc.DataSourceHealthIndicatorProperties",

"dependencies": []

},

"observationRegistryPostProcessor": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.boot.actuate.autoconfigure.observation.ObservationRegistryPostProcessor",

"resource": "class path resource [org/springframework/boot/actuate/autoconfigure/observation/ObservationAutoConfiguration.class]",

"dependencies": []

},

"spring.data.web-org.springframework.boot.autoconfigure.data.web.SpringDataWebProperties": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.boot.autoconfigure.data.web.SpringDataWebProperties",

"dependencies": []

},

"characterEncodingFilter": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.boot.web.servlet.filter.OrderedCharacterEncodingFilter",

"resource": "class path resource [org/springframework/boot/autoconfigure/web/servlet/HttpEncodingAutoConfiguration.class]",

"dependencies": [

"org.springframework.boot.autoconfigure.web.servlet.HttpEncodingAutoConfiguration"

]

},

"sortCustomizer": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.boot.autoconfigure.data.web.SpringDataWebAutoConfiguration$$Lambda/0x00007ff400a7b1f8",

"resource": "class path resource [org/springframework/boot/autoconfigure/data/web/SpringDataWebAutoConfiguration.class]",

"dependencies": [

"org.springframework.boot.autoconfigure.data.web.SpringDataWebAutoConfiguration"

]

},

"webEndpointDiscoverer": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.boot.actuate.endpoint.web.annotation.WebEndpointDiscoverer",

"resource": "class path resource [org/springframework/boot/actuate/autoconfigure/endpoint/web/WebEndpointAutoConfiguration.class]",

"dependencies": [

"org.springframework.boot.actuate.autoconfigure.endpoint.web.WebEndpointAutoConfiguration",

"endpointOperationParameterMapper",

"endpointMediaTypes"

]

},

"org.springframework.boot.autoconfigure.web.servlet.DispatcherServletAutoConfiguration$DispatcherServletRegistrationConfiguration": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.boot.autoconfigure.web.servlet.DispatcherServletAutoConfiguration$DispatcherServletRegistrationConfiguration",

"dependencies": []

},

"preserveErrorControllerTargetClassPostProcessor": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.boot.autoconfigure.web.servlet.error.ErrorMvcAutoConfiguration$PreserveErrorControllerTargetClassPostProcessor",

"resource": "class path resource [org/springframework/boot/autoconfigure/web/servlet/error/ErrorMvcAutoConfiguration.class]",

"dependencies": []

},

"org.springframework.boot.autoconfigure.jackson.JacksonAutoConfiguration$JacksonObjectMapperBuilderConfiguration": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.boot.autoconfigure.jackson.JacksonAutoConfiguration$JacksonObjectMapperBuilderConfiguration",

"dependencies": []

},

"org.springframework.boot.autoconfigure.web.reactive.function.client.ClientHttpConnectorFactoryConfiguration$ReactorNetty": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.boot.autoconfigure.web.reactive.function.client.ClientHttpConnectorFactoryConfiguration$ReactorNetty",

"dependencies": []

},

"openAPIBuilder": {

"aliases": [],

"scope": "prototype",

"type": "org.springdoc.core.service.OpenAPIService",

"resource": "class path resource [org/springdoc/core/configuration/SpringDocConfiguration.class]",

"dependencies": [

"org.springdoc.core.configuration.SpringDocConfiguration",

"securityParser",

"org.springdoc.core.properties.SpringDocConfigProperties",

"propertyResolverUtils"

]

},

"logbackMetrics": {

"aliases": [],

"scope": "singleton",

"type": "io.micrometer.core.instrument.binder.logging.LogbackMetrics",

"resource": "class path resource [org/springframework/boot/actuate/autoconfigure/metrics/LogbackMetricsAutoConfiguration.class]",

"dependencies": [

"org.springframework.boot.actuate.autoconfigure.metrics.LogbackMetricsAutoConfiguration"

]

},

"management.info-org.springframework.boot.actuate.autoconfigure.info.InfoContributorProperties": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.boot.actuate.autoconfigure.info.InfoContributorProperties",

"dependencies": []

},

"org.springframework.boot.actuate.autoconfigure.metrics.data.RepositoryMetricsAutoConfiguration": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.boot.actuate.autoconfigure.metrics.data.RepositoryMetricsAutoConfiguration",

"dependencies": [

"management.metrics-org.springframework.boot.actuate.autoconfigure.metrics.MetricsProperties"

]

},

"propertySourcesPlaceholderConfigurer": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.context.support.PropertySourcesPlaceholderConfigurer",

"resource": "class path resource [org/springframework/boot/autoconfigure/context/PropertyPlaceholderAutoConfiguration.class]",

"dependencies": []

},

"org.springframework.boot.autoconfigure.transaction.TransactionManagerCustomizationAutoConfiguration": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.boot.autoconfigure.transaction.TransactionManagerCustomizationAutoConfiguration",

"dependencies": []

},

"org.springframework.boot.autoconfigure.transaction.jta.JtaAutoConfiguration": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.boot.autoconfigure.transaction.jta.JtaAutoConfiguration",

"dependencies": []

},

"management.endpoints.web.cors-org.springframework.boot.actuate.autoconfigure.endpoint.web.CorsEndpointProperties": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.boot.actuate.autoconfigure.endpoint.web.CorsEndpointProperties",

"dependencies": []

},

"stringHttpMessageConverter": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.http.converter.StringHttpMessageConverter",

"resource": "class path resource [org/springframework/boot/autoconfigure/http/HttpMessageConvertersAutoConfiguration$StringHttpMessageConverterConfiguration.class]",

"dependencies": [

"org.springframework.boot.autoconfigure.http.HttpMessageConvertersAutoConfiguration$StringHttpMessageConverterConfiguration",

"environment"

]

},

"org.springframework.boot.autoconfigure.data.redis.RedisRepositoriesAutoConfiguration": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.boot.autoconfigure.data.redis.RedisRepositoriesAutoConfiguration",

"dependencies": []

},

"org.springframework.boot.autoconfigure.web.embedded.EmbeddedWebServerFactoryCustomizerAutoConfiguration$TomcatWebServerFactoryCustomizerConfiguration": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.boot.autoconfigure.web.embedded.EmbeddedWebServerFactoryCustomizerAutoConfiguration$TomcatWebServerFactoryCustomizerConfiguration",

"dependencies": []

},

"chatGPT": {

"aliases": [],

"scope": "singleton",

"type": "cn.netkiller.component.ChatGPT$$SpringCGLIB$$0",

"resource": "URL [jar:nested:/app/watch-1.0-SNAPSHOT.jar/!BOOT-INF/classes/!/cn/netkiller/component/ChatGPT.class]",

"dependencies": []

},

"jsonComponentModule": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.boot.jackson.JsonComponentModule",

"resource": "class path resource [org/springframework/boot/autoconfigure/jackson/JacksonAutoConfiguration.class]",

"dependencies": [

"org.springframework.boot.autoconfigure.jackson.JacksonAutoConfiguration"

]

},

"org.springdoc.core.configuration.SpringDocUIConfiguration": {

"aliases": [],

"scope": "singleton",

"type": "org.springdoc.core.configuration.SpringDocUIConfiguration",

"dependencies": []

},

"org.springframework.boot.actuate.autoconfigure.web.mappings.MappingsEndpointAutoConfiguration$ServletWebConfiguration$SpringMvcConfiguration": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.boot.actuate.autoconfigure.web.mappings.MappingsEndpointAutoConfiguration$ServletWebConfiguration$SpringMvcConfiguration",

"dependencies": []

},

"entityManagerFactoryBuilder": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.boot.orm.jpa.EntityManagerFactoryBuilder",

"resource": "class path resource [org/springframework/boot/autoconfigure/orm/jpa/HibernateJpaConfiguration.class]",

"dependencies": [

"org.springframework.boot.autoconfigure.orm.jpa.HibernateJpaConfiguration",

"jpaVendorAdapter"

]

},

"org.springframework.boot.autoconfigure.web.embedded.EmbeddedWebServerFactoryCustomizerAutoConfiguration": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.boot.autoconfigure.web.embedded.EmbeddedWebServerFactoryCustomizerAutoConfiguration",

"dependencies": []

},

"audioService": {

"aliases": [],

"scope": "singleton",

"type": "cn.netkiller.component.AudioService",

"resource": "URL [jar:nested:/app/watch-1.0-SNAPSHOT.jar/!BOOT-INF/classes/!/cn/netkiller/component/AudioService.class]",

"dependencies": []

},

"mappingJackson2HttpMessageConverter": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.http.converter.json.MappingJackson2HttpMessageConverter",

"resource": "class path resource [org/springframework/boot/autoconfigure/http/JacksonHttpMessageConvertersConfiguration$MappingJackson2HttpMessageConverterConfiguration.class]",

"dependencies": [

"org.springframework.boot.autoconfigure.http.JacksonHttpMessageConvertersConfiguration$MappingJackson2HttpMessageConverterConfiguration",

"jacksonObjectMapper"

]

},

"org.springframework.boot.actuate.autoconfigure.env.EnvironmentEndpointAutoConfiguration": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.boot.actuate.autoconfigure.env.EnvironmentEndpointAutoConfiguration",

"dependencies": []

},

"org.springframework.boot.autoconfigure.jdbc.DataSourceJmxConfiguration": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.boot.autoconfigure.jdbc.DataSourceJmxConfiguration",

"dependencies": []

},

"org.springframework.boot.actuate.autoconfigure.data.redis.RedisReactiveHealthContributorAutoConfiguration": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.boot.actuate.autoconfigure.data.redis.RedisReactiveHealthContributorAutoConfiguration",

"dependencies": [

"redisConnectionFactory"

]

},

"stableDiffusionService": {

"aliases": [],

"scope": "singleton",

"type": "cn.netkiller.service.StableDiffusionService$$SpringCGLIB$$0",

"resource": "URL [jar:nested:/app/watch-1.0-SNAPSHOT.jar/!BOOT-INF/classes/!/cn/netkiller/service/StableDiffusionService.class]",

"dependencies": [

"aliyunService",

"pictureService",

"sessionStatusService",

"mqttService"

]

},

"spring.reactor.netty-org.springframework.boot.autoconfigure.reactor.netty.ReactorNettyProperties": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.boot.autoconfigure.reactor.netty.ReactorNettyProperties",

"dependencies": []

},

"healthStatusAggregator": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.boot.actuate.health.SimpleStatusAggregator",

"resource": "class path resource [org/springframework/boot/actuate/autoconfigure/health/HealthEndpointConfiguration.class]",

"dependencies": [

"org.springframework.boot.actuate.autoconfigure.health.HealthEndpointConfiguration",

"management.endpoint.health-org.springframework.boot.actuate.autoconfigure.health.HealthEndpointProperties"

]

},

"org.springframework.boot.actuate.autoconfigure.metrics.SystemMetricsAutoConfiguration": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.boot.actuate.autoconfigure.metrics.SystemMetricsAutoConfiguration",

"dependencies": []

},

"management.server-org.springframework.boot.actuate.autoconfigure.web.server.ManagementServerProperties": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.boot.actuate.autoconfigure.web.server.ManagementServerProperties",

"dependencies": []

},

"servletWebServerFactoryCustomizer": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.boot.autoconfigure.web.servlet.ServletWebServerFactoryCustomizer",

"resource": "class path resource [org/springframework/boot/autoconfigure/web/servlet/ServletWebServerFactoryAutoConfiguration.class]",

"dependencies": [

"org.springframework.boot.autoconfigure.web.servlet.ServletWebServerFactoryAutoConfiguration",

"server-org.springframework.boot.autoconfigure.web.ServerProperties"

]

},

"mvcUrlPathHelper": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.web.util.UrlPathHelper",

"resource": "class path resource [org/springframework/boot/autoconfigure/web/servlet/WebMvcAutoConfiguration$EnableWebMvcConfiguration.class]",

"dependencies": [

"org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguration$EnableWebMvcConfiguration"

]

},

"transactionTemplate": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.transaction.support.TransactionTemplate",

"resource": "class path resource [org/springframework/boot/autoconfigure/transaction/TransactionAutoConfiguration$TransactionTemplateConfiguration.class]",

"dependencies": [

"org.springframework.boot.autoconfigure.transaction.TransactionAutoConfiguration$TransactionTemplateConfiguration",

"transactionManager"

]

},

"jpa.named-queries#4": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.data.repository.core.support.PropertiesBasedNamedQueries",

"dependencies": []

},

"nullableKotlinRequestParameterCustomizer": {

"aliases": [],

"scope": "singleton",

"type": "org.springdoc.core.configuration.SpringDocKotlinConfiguration$$Lambda/0x00007ff400aa6460",

"resource": "class path resource [org/springdoc/core/configuration/SpringDocKotlinConfiguration.class]",

"dependencies": [

"org.springdoc.core.configuration.SpringDocKotlinConfiguration"

]

},

"org.springframework.boot.actuate.autoconfigure.metrics.startup.StartupTimeMetricsListenerAutoConfiguration": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.boot.actuate.autoconfigure.metrics.startup.StartupTimeMetricsListenerAutoConfiguration",

"dependencies": []

},

"servletMappingDescriptionProvider": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.boot.actuate.web.mappings.servlet.ServletsMappingDescriptionProvider",

"resource": "class path resource [org/springframework/boot/actuate/autoconfigure/web/mappings/MappingsEndpointAutoConfiguration$ServletWebConfiguration.class]",

"dependencies": [

"org.springframework.boot.actuate.autoconfigure.web.mappings.MappingsEndpointAutoConfiguration$ServletWebConfiguration"

]

},

"org.springframework.boot.autoconfigure.task.TaskSchedulingConfigurations$ThreadPoolTaskSchedulerBuilderConfiguration": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.boot.autoconfigure.task.TaskSchedulingConfigurations$ThreadPoolTaskSchedulerBuilderConfiguration",

"dependencies": []

},

"org.springdoc.core.configuration.SpringDocConfiguration$WebConversionServiceConfiguration": {

"aliases": [],

"scope": "singleton",

"type": "org.springdoc.core.configuration.SpringDocConfiguration$WebConversionServiceConfiguration",

"dependencies": []

},

"org.springdoc.webmvc.core.configuration.SpringDocWebMvcConfiguration$SpringDocWebMvcActuatorConfiguration": {

"aliases": [],

"scope": "singleton",

"type": "org.springdoc.webmvc.core.configuration.SpringDocWebMvcConfiguration$SpringDocWebMvcActuatorConfiguration",

"dependencies": []

},

"stringRedisTemplate": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.data.redis.core.StringRedisTemplate",

"resource": "class path resource [org/springframework/boot/autoconfigure/data/redis/RedisAutoConfiguration.class]",

"dependencies": [

"org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration",

"redisConnectionFactory"

]

},

"org.springframework.boot.actuate.autoconfigure.scheduling.ScheduledTasksObservabilityAutoConfiguration": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.boot.actuate.autoconfigure.scheduling.ScheduledTasksObservabilityAutoConfiguration",

"dependencies": []

},

"org.springframework.boot.actuate.autoconfigure.observation.ObservationAutoConfiguration": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.boot.actuate.autoconfigure.observation.ObservationAutoConfiguration",

"dependencies": []

},

"spring.sql.init-org.springframework.boot.autoconfigure.sql.init.SqlInitializationProperties": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.boot.autoconfigure.sql.init.SqlInitializationProperties",

"dependencies": []

},

"reactiveHealthContributorRegistry": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.boot.actuate.autoconfigure.health.AutoConfiguredReactiveHealthContributorRegistry",

"resource": "class path resource [org/springframework/boot/actuate/autoconfigure/health/ReactiveHealthEndpointConfiguration.class]",

"dependencies": [

"org.springframework.boot.actuate.autoconfigure.health.ReactiveHealthEndpointConfiguration",

"diskSpaceHealthIndicator",

"pingHealthContributor",

"dbHealthContributor",

"redisHealthContributor",

"healthEndpointGroups"

]

},

"adminApi": {

"aliases": [],

"scope": "singleton",

"type": "org.springdoc.core.models.GroupedOpenApi",

"resource": "class path resource [cn/netkiller/config/Knife4jConfiguration.class]",

"dependencies": [

"knife4jConfiguration"

]

},

"jpa.named-queries#1": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.data.repository.core.support.PropertiesBasedNamedQueries",

"dependencies": []

},

"org.springframework.boot.actuate.autoconfigure.metrics.JvmMetricsAutoConfiguration": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.boot.actuate.autoconfigure.metrics.JvmMetricsAutoConfiguration",

"dependencies": []

},

"jpa.named-queries#0": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.data.repository.core.support.PropertiesBasedNamedQueries",

"dependencies": []

},

"org.springframework.boot.autoconfigure.reactor.ReactorAutoConfiguration": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.boot.autoconfigure.reactor.ReactorAutoConfiguration",

"dependencies": [

"spring.reactor-org.springframework.boot.autoconfigure.reactor.ReactorProperties"

]

},

"jpa.named-queries#3": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.data.repository.core.support.PropertiesBasedNamedQueries",

"dependencies": []

},

"org.springframework.boot.autoconfigure.jackson.JacksonAutoConfiguration$Jackson2ObjectMapperBuilderCustomizerConfiguration": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.boot.autoconfigure.jackson.JacksonAutoConfiguration$Jackson2ObjectMapperBuilderCustomizerConfiguration",

"dependencies": []

},

"org.springframework.boot.autoconfigure.http.HttpMessageConvertersAutoConfiguration$StringHttpMessageConverterConfiguration": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.boot.autoconfigure.http.HttpMessageConvertersAutoConfiguration$StringHttpMessageConverterConfiguration",

"dependencies": []

},

"jpa.named-queries#2": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.data.repository.core.support.PropertiesBasedNamedQueries",

"dependencies": []

},

"standardJacksonObjectMapperBuilderCustomizer": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.boot.autoconfigure.jackson.JacksonAutoConfiguration$Jackson2ObjectMapperBuilderCustomizerConfiguration$StandardJackson2ObjectMapperBuilderCustomizer",

"resource": "class path resource [org/springframework/boot/autoconfigure/jackson/JacksonAutoConfiguration$Jackson2ObjectMapperBuilderCustomizerConfiguration.class]",

"dependencies": [

"org.springframework.boot.autoconfigure.jackson.JacksonAutoConfiguration$Jackson2ObjectMapperBuilderCustomizerConfiguration",

"spring.jackson-org.springframework.boot.autoconfigure.jackson.JacksonProperties"

]

},

"taskSchedulerBuilder": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.boot.task.TaskSchedulerBuilder",

"resource": "class path resource [org/springframework/boot/autoconfigure/task/TaskSchedulingConfigurations$TaskSchedulerBuilderConfiguration.class]",

"dependencies": [

"org.springframework.boot.autoconfigure.task.TaskSchedulingConfigurations$TaskSchedulerBuilderConfiguration",

"spring.task.scheduling-org.springframework.boot.autoconfigure.task.TaskSchedulingProperties"

]

},

"org.springframework.boot.autoconfigure.task.TaskExecutionAutoConfiguration": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.boot.autoconfigure.task.TaskExecutionAutoConfiguration",

"dependencies": []

},

"org.springframework.boot.autoconfigure.web.servlet.DispatcherServletAutoConfiguration": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.boot.autoconfigure.web.servlet.DispatcherServletAutoConfiguration",

"dependencies": []

},

"spring.data.redis-org.springframework.boot.autoconfigure.data.redis.RedisProperties": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.boot.autoconfigure.data.redis.RedisProperties",

"dependencies": []

},

"simpleMeterRegistry": {

"aliases": [],

"scope": "singleton",

"type": "io.micrometer.core.instrument.simple.SimpleMeterRegistry",

"resource": "class path resource [org/springframework/boot/actuate/autoconfigure/metrics/export/simple/SimpleMetricsExportAutoConfiguration.class]",

"dependencies": [

"org.springframework.boot.actuate.autoconfigure.metrics.export.simple.SimpleMetricsExportAutoConfiguration",

"simpleConfig",

"micrometerClock"

]

},

"org.springdoc.core.configuration.SpringDocPageableConfiguration": {

"aliases": [],

"scope": "singleton",

"type": "org.springdoc.core.configuration.SpringDocPageableConfiguration",

"dependencies": []

},

"entityManagerFactory": {

"aliases": [],

"scope": "singleton",

"type": "jdk.proxy2.$Proxy148",

"resource": "class path resource [org/springframework/boot/autoconfigure/orm/jpa/HibernateJpaConfiguration.class]",

"dependencies": [

"cacheManager",

"dataSourceScriptDatabaseInitializer",

"org.springframework.boot.autoconfigure.orm.jpa.HibernateJpaConfiguration",

"entityManagerFactoryBuilder",

"persistenceManagedTypes"

]

},

"webClientSsl": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.boot.autoconfigure.web.reactive.function.client.AutoConfiguredWebClientSsl",

"resource": "class path resource [org/springframework/boot/autoconfigure/web/reactive/function/client/WebClientAutoConfiguration.class]",

"dependencies": [

"org.springframework.boot.autoconfigure.web.reactive.function.client.WebClientAutoConfiguration",

"reactorClientHttpConnectorFactory",

"sslBundleRegistry"

]

},

"startupTimeMetrics": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.boot.actuate.metrics.startup.StartupTimeMetricsListener",

"resource": "class path resource [org/springframework/boot/actuate/autoconfigure/metrics/startup/StartupTimeMetricsListenerAutoConfiguration.class]",

"dependencies": [

"org.springframework.boot.actuate.autoconfigure.metrics.startup.StartupTimeMetricsListenerAutoConfiguration",

"simpleMeterRegistry"

]

},

"observedAspect": {

"aliases": [],

"scope": "singleton",

"type": "io.micrometer.observation.aop.ObservedAspect",

"resource": "class path resource [org/springframework/boot/actuate/autoconfigure/observation/ObservationAutoConfiguration$ObservedAspectConfiguration.class]",

"dependencies": [

"org.springframework.boot.actuate.autoconfigure.observation.ObservationAutoConfiguration$ObservedAspectConfiguration",

"observationRegistry"

]

},

"org.springframework.boot.actuate.autoconfigure.metrics.web.tomcat.TomcatMetricsAutoConfiguration": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.boot.actuate.autoconfigure.metrics.web.tomcat.TomcatMetricsAutoConfiguration",

"dependencies": []

},

"org.springframework.boot.autoconfigure.jdbc.metadata.DataSourcePoolMetadataProvidersConfiguration": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.boot.autoconfigure.jdbc.metadata.DataSourcePoolMetadataProvidersConfiguration",

"dependencies": []

},

"org.springframework.boot.actuate.autoconfigure.observation.web.servlet.WebMvcObservationAutoConfiguration": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.boot.actuate.autoconfigure.observation.web.servlet.WebMvcObservationAutoConfiguration",

"dependencies": []

},

"spring.mvc-org.springframework.boot.autoconfigure.web.servlet.WebMvcProperties": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.boot.autoconfigure.web.servlet.WebMvcProperties",

"dependencies": []

},

"knife4j.setting-com.github.xiaoymin.knife4j.spring.configuration.Knife4jSetting": {

"aliases": [],

"scope": "singleton",

"type": "com.github.xiaoymin.knife4j.spring.configuration.Knife4jSetting",

"dependencies": []

},

"org.springdoc.core.configuration.SpringDocConfiguration": {

"aliases": [],

"scope": "singleton",

"type": "org.springdoc.core.configuration.SpringDocConfiguration",

"dependencies": []

},

"configurationPropertiesReportEndpoint": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.boot.actuate.context.properties.ConfigurationPropertiesReportEndpoint",

"resource": "class path resource [org/springframework/boot/actuate/autoconfigure/context/properties/ConfigurationPropertiesReportEndpointAutoConfiguration.class]",

"dependencies": [

"org.springframework.boot.actuate.autoconfigure.context.properties.ConfigurationPropertiesReportEndpointAutoConfiguration",

"management.endpoint.configprops-org.springframework.boot.actuate.autoconfigure.context.properties.ConfigurationPropertiesReportEndpointProperties"

]

},

"multipartConfigElement": {

"aliases": [],

"scope": "singleton",

"type": "jakarta.servlet.MultipartConfigElement",

"resource": "class path resource [org/springframework/boot/autoconfigure/web/servlet/MultipartAutoConfiguration.class]",

"dependencies": [

"org.springframework.boot.autoconfigure.web.servlet.MultipartAutoConfiguration"

]

},

"requestContextFilter": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.boot.web.servlet.filter.OrderedRequestContextFilter",

"resource": "class path resource [org/springframework/boot/autoconfigure/web/servlet/WebMvcAutoConfiguration$WebMvcAutoConfigurationAdapter.class]",

"dependencies": []

},

"org.springframework.boot.autoconfigure.orm.jpa.JpaBaseConfiguration$PersistenceManagedTypesConfiguration": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.boot.autoconfigure.orm.jpa.JpaBaseConfiguration$PersistenceManagedTypesConfiguration",

"dependencies": []

},

"mqttService": {

"aliases": [],

"scope": "singleton",

"type": "cn.netkiller.utils.MqttService",

"resource": "URL [jar:nested:/app/watch-1.0-SNAPSHOT.jar/!BOOT-INF/classes/!/cn/netkiller/utils/MqttService.class]",

"dependencies": []

},

"operationBuilder": {

"aliases": [],

"scope": "singleton",

"type": "org.springdoc.core.service.OperationService",

"resource": "class path resource [org/springdoc/core/configuration/SpringDocConfiguration.class]",

"dependencies": [

"org.springdoc.core.configuration.SpringDocConfiguration",

"parameterBuilder",

"requestBodyBuilder",

"securityParser",

"propertyResolverUtils"

]

},

"org.springframework.boot.actuate.autoconfigure.web.mappings.MappingsEndpointAutoConfiguration$ServletWebConfiguration": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.boot.actuate.autoconfigure.web.mappings.MappingsEndpointAutoConfiguration$ServletWebConfiguration",

"dependencies": []

},

"beansEndpoint": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.boot.actuate.beans.BeansEndpoint",

"resource": "class path resource [org/springframework/boot/actuate/autoconfigure/beans/BeansEndpointAutoConfiguration.class]",

"dependencies": [

"org.springframework.boot.actuate.autoconfigure.beans.BeansEndpointAutoConfiguration",

"org.springframework.boot.web.servlet.context.AnnotationConfigServletWebServerApplicationContext@18e36d14"

]

},

"lettuceClientResources": {

"aliases": [],

"scope": "singleton",

"type": "io.lettuce.core.resource.DefaultClientResources",

"resource": "class path resource [org/springframework/boot/autoconfigure/data/redis/LettuceConnectionConfiguration.class]",

"dependencies": [

"org.springframework.boot.autoconfigure.data.redis.LettuceConnectionConfiguration"

]

},

"pageableResolver": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.data.web.PageableHandlerMethodArgumentResolver",

"resource": "class path resource [org/springframework/data/web/config/SpringDataWebConfiguration.class]",

"dependencies": [

"org.springframework.data.web.config.SpringDataWebConfiguration"

]

},

"exchangeStrategiesCustomizer": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.boot.autoconfigure.web.reactive.function.client.WebClientCodecCustomizer",

"resource": "class path resource [org/springframework/boot/autoconfigure/web/reactive/function/client/WebClientAutoConfiguration$WebClientCodecsConfiguration.class]",

"dependencies": [

"org.springframework.boot.autoconfigure.web.reactive.function.client.WebClientAutoConfiguration$WebClientCodecsConfiguration"

]

},

"localeResolver": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.web.servlet.i18n.AcceptHeaderLocaleResolver",

"resource": "class path resource [org/springframework/boot/autoconfigure/web/servlet/WebMvcAutoConfiguration$EnableWebMvcConfiguration.class]",

"dependencies": [

"org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguration$EnableWebMvcConfiguration"

]

},

"handlerFunctionAdapter": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.web.servlet.function.support.HandlerFunctionAdapter",

"resource": "class path resource [org/springframework/boot/autoconfigure/web/servlet/WebMvcAutoConfiguration$EnableWebMvcConfiguration.class]",

"dependencies": [

"org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguration$EnableWebMvcConfiguration"

]

},

"localSpringDocParameterNameDiscoverer": {

"aliases": [],

"scope": "singleton",

"type": "org.springdoc.core.discoverer.SpringDocParameterNameDiscoverer",

"resource": "class path resource [org/springdoc/core/configuration/SpringDocConfiguration.class]",

"dependencies": [

"org.springdoc.core.configuration.SpringDocConfiguration"

]

},

"schemaPropertyDeprecatingConverter": {

"aliases": [],

"scope": "singleton",

"type": "org.springdoc.core.converters.SchemaPropertyDeprecatingConverter",

"resource": "class path resource [org/springdoc/core/configuration/SpringDocConfiguration.class]",

"dependencies": [

"org.springdoc.core.configuration.SpringDocConfiguration"

]

},

"pictureController": {

"aliases": [],

"scope": "singleton",

"type": "cn.netkiller.controller.PictureController",

"resource": "URL [jar:nested:/app/watch-1.0-SNAPSHOT.jar/!BOOT-INF/classes/!/cn/netkiller/controller/PictureController.class]",

"dependencies": [

"pictureService",

"jdbcTemplate"

]

},

"org.springframework.boot.actuate.autoconfigure.web.server.ManagementContextAutoConfiguration$SameManagementContextConfiguration": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.boot.actuate.autoconfigure.web.server.ManagementContextAutoConfiguration$SameManagementContextConfiguration",

"dependencies": [

"environment"

]

},

"org.springframework.boot.autoconfigure.jackson.JacksonAutoConfiguration$JacksonMixinConfiguration": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.boot.autoconfigure.jackson.JacksonAutoConfiguration$JacksonMixinConfiguration",

"dependencies": []

},

"org.springframework.boot.autoconfigure.jdbc.NamedParameterJdbcTemplateConfiguration": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.boot.autoconfigure.jdbc.NamedParameterJdbcTemplateConfiguration",

"dependencies": []

},

"org.springframework.boot.autoconfigure.web.reactive.function.client.WebClientAutoConfiguration$WebClientCodecsConfiguration": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.boot.autoconfigure.web.reactive.function.client.WebClientAutoConfiguration$WebClientCodecsConfiguration",

"dependencies": []

},

"spring.transaction-org.springframework.boot.autoconfigure.transaction.TransactionProperties": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.boot.autoconfigure.transaction.TransactionProperties",

"dependencies": []

},

"observationRestClientCustomizer": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.boot.actuate.metrics.web.client.ObservationRestClientCustomizer",

"resource": "class path resource [org/springframework/boot/actuate/autoconfigure/observation/web/client/RestClientObservationConfiguration.class]",

"dependencies": [

"org.springframework.boot.actuate.autoconfigure.observation.web.client.RestClientObservationConfiguration",

"observationRegistry",

"management.observations-org.springframework.boot.actuate.autoconfigure.observation.ObservationProperties"

]

},

"warningService": {

"aliases": [],

"scope": "singleton",

"type": "cn.netkiller.service.WarningService$$SpringCGLIB$$0",

"resource": "URL [jar:nested:/app/watch-1.0-SNAPSHOT.jar/!BOOT-INF/classes/!/cn/netkiller/service/WarningService.class]",

"dependencies": [

"mqttService"

]

},

"jvmHeapPressureMetrics": {

"aliases": [],

"scope": "singleton",

"type": "io.micrometer.core.instrument.binder.jvm.JvmHeapPressureMetrics",

"resource": "class path resource [org/springframework/boot/actuate/autoconfigure/metrics/JvmMetricsAutoConfiguration.class]",

"dependencies": [

"org.springframework.boot.actuate.autoconfigure.metrics.JvmMetricsAutoConfiguration"

]

},

"org.springframework.boot.autoconfigure.web.servlet.HttpEncodingAutoConfiguration": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.boot.autoconfigure.web.servlet.HttpEncodingAutoConfiguration",

"dependencies": [

"server-org.springframework.boot.autoconfigure.web.ServerProperties"

]

},

"welcomePageNotAcceptableHandlerMapping": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.boot.autoconfigure.web.servlet.WelcomePageNotAcceptableHandlerMapping",

"resource": "class path resource [org/springframework/boot/autoconfigure/web/servlet/WebMvcAutoConfiguration$EnableWebMvcConfiguration.class]",

"dependencies": [

"org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguration$EnableWebMvcConfiguration",

"org.springframework.boot.web.servlet.context.AnnotationConfigServletWebServerApplicationContext@18e36d14",

"mvcConversionService",

"mvcResourceUrlProvider"

]

},

"org.springframework.boot.actuate.autoconfigure.observation.web.client.HttpClientObservationsAutoConfiguration": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.boot.actuate.autoconfigure.observation.web.client.HttpClientObservationsAutoConfiguration",

"dependencies": []

},

"endpointOperationParameterMapper": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.boot.actuate.endpoint.invoke.convert.ConversionServiceParameterValueMapper",

"resource": "class path resource [org/springframework/boot/actuate/autoconfigure/endpoint/EndpointAutoConfiguration.class]",

"dependencies": [

"org.springframework.boot.actuate.autoconfigure.endpoint.EndpointAutoConfiguration"

]

},

"org.springframework.boot.autoconfigure.web.embedded.EmbeddedWebServerFactoryCustomizerAutoConfiguration$NettyWebServerFactoryCustomizerConfiguration": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.boot.autoconfigure.web.embedded.EmbeddedWebServerFactoryCustomizerAutoConfiguration$NettyWebServerFactoryCustomizerConfiguration",

"dependencies": []

},

"org.springframework.boot.actuate.autoconfigure.health.HealthContributorAutoConfiguration": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.boot.actuate.autoconfigure.health.HealthContributorAutoConfiguration",

"dependencies": []

},

"sslBundleRegistry": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.boot.ssl.DefaultSslBundleRegistry",

"resource": "class path resource [org/springframework/boot/autoconfigure/ssl/SslAutoConfiguration.class]",

"dependencies": [

"org.springframework.boot.autoconfigure.ssl.SslAutoConfiguration"

]

},

"jpaVendorAdapter": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter",

"resource": "class path resource [org/springframework/boot/autoconfigure/orm/jpa/HibernateJpaConfiguration.class]",

"dependencies": [

"org.springframework.boot.autoconfigure.orm.jpa.HibernateJpaConfiguration"

]

},

"reactiveStringRedisTemplate": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.data.redis.core.ReactiveStringRedisTemplate",

"resource": "class path resource [org/springframework/boot/autoconfigure/data/redis/RedisReactiveAutoConfiguration.class]",

"dependencies": [

"org.springframework.boot.autoconfigure.data.redis.RedisReactiveAutoConfiguration",

"redisConnectionFactory"

]

},

"org.springframework.boot.actuate.autoconfigure.info.InfoEndpointAutoConfiguration": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.boot.actuate.autoconfigure.info.InfoEndpointAutoConfiguration",

"dependencies": []

},

"org.springframework.boot.autoconfigure.task.TaskExecutorConfigurations$ThreadPoolTaskExecutorBuilderConfiguration": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.boot.autoconfigure.task.TaskExecutorConfigurations$ThreadPoolTaskExecutorBuilderConfiguration",

"dependencies": []

},

"cacheAutoConfigurationValidator": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.boot.autoconfigure.cache.CacheAutoConfiguration$CacheManagerValidator",

"resource": "class path resource [org/springframework/boot/autoconfigure/cache/CacheAutoConfiguration.class]",

"dependencies": [

"org.springframework.boot.autoconfigure.cache.CacheAutoConfiguration",

"spring.cache-org.springframework.boot.autoconfigure.cache.CacheProperties"

]

},

"servletWebChildContextFactory": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.boot.actuate.autoconfigure.web.ManagementContextFactory",

"resource": "class path resource [org/springframework/boot/actuate/autoconfigure/web/servlet/ServletManagementContextAutoConfiguration.class]",

"dependencies": []

},

"countedAspect": {

"aliases": [],

"scope": "singleton",

"type": "io.micrometer.core.aop.CountedAspect",

"resource": "class path resource [org/springframework/boot/actuate/autoconfigure/metrics/MetricsAspectsAutoConfiguration.class]",

"dependencies": [

"org.springframework.boot.actuate.autoconfigure.metrics.MetricsAspectsAutoConfiguration",

"simpleMeterRegistry"

]

},

"additionalModelsConverter": {

"aliases": [],

"scope": "singleton",

"type": "org.springdoc.core.converters.AdditionalModelsConverter",

"resource": "class path resource [org/springdoc/core/configuration/SpringDocConfiguration.class]",

"dependencies": [

"org.springdoc.core.configuration.SpringDocConfiguration",

"springdocObjectMapperProvider"

]

},

"jacksonGeoModule": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.data.geo.GeoModule",

"resource": "class path resource [org/springframework/data/web/config/SpringDataJacksonConfiguration.class]",

"dependencies": [

"org.springframework.data.web.config.SpringDataJacksonConfiguration"

]

},

"meterRegistryCloser": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.boot.actuate.autoconfigure.metrics.MetricsAutoConfiguration$MeterRegistryCloser",

"resource": "class path resource [org/springframework/boot/actuate/autoconfigure/metrics/MetricsAutoConfiguration.class]",

"dependencies": [

"org.springframework.boot.actuate.autoconfigure.metrics.MetricsAutoConfiguration"

]

},

"mvcContentNegotiationManager": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.web.accept.ContentNegotiationManager",

"resource": "class path resource [org/springframework/boot/autoconfigure/web/servlet/WebMvcAutoConfiguration$EnableWebMvcConfiguration.class]",

"dependencies": [

"org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguration$EnableWebMvcConfiguration"

]

},

"org.springframework.boot.actuate.autoconfigure.metrics.CompositeMeterRegistryAutoConfiguration": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.boot.actuate.autoconfigure.metrics.CompositeMeterRegistryAutoConfiguration",

"dependencies": []

},

"org.springframework.boot.actuate.autoconfigure.metrics.jdbc.DataSourcePoolMetricsAutoConfiguration$HikariDataSourceMetricsConfiguration": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.boot.actuate.autoconfigure.metrics.jdbc.DataSourcePoolMetricsAutoConfiguration$HikariDataSourceMetricsConfiguration",

"dependencies": []

},

"org.springframework.boot.autoconfigure.task.TaskSchedulingAutoConfiguration": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.boot.autoconfigure.task.TaskSchedulingAutoConfiguration",

"dependencies": []

},

"httpRequestHandlerAdapter": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.web.servlet.mvc.HttpRequestHandlerAdapter",

"resource": "class path resource [org/springframework/boot/autoconfigure/web/servlet/WebMvcAutoConfiguration$EnableWebMvcConfiguration.class]",

"dependencies": [

"org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguration$EnableWebMvcConfiguration"

]

},

"org.springframework.boot.actuate.autoconfigure.web.mappings.MappingsEndpointAutoConfiguration": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.boot.actuate.autoconfigure.web.mappings.MappingsEndpointAutoConfiguration",

"dependencies": []

},

"java.lang.Object": {

"aliases": [],

"scope": "singleton",

"type": "java.lang.Object",

"dependencies": []

},

"org.springframework.boot.actuate.autoconfigure.observation.web.client.RestTemplateObservationConfiguration": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.boot.actuate.autoconfigure.observation.web.client.RestTemplateObservationConfiguration",

"dependencies": []

},

"spring.servlet.multipart-org.springframework.boot.autoconfigure.web.servlet.MultipartProperties": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.boot.autoconfigure.web.servlet.MultipartProperties",

"dependencies": []

},

"sslPropertiesSslBundleRegistrar": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.boot.autoconfigure.ssl.SslPropertiesBundleRegistrar",

"resource": "class path resource [org/springframework/boot/autoconfigure/ssl/SslAutoConfiguration.class]",

"dependencies": [

"org.springframework.boot.autoconfigure.ssl.SslAutoConfiguration",

"fileWatcher"

]

},

"org.springdoc.webmvc.core.configuration.SpringDocWebMvcConfiguration$SpringDocWebMvcRouterConfiguration": {

"aliases": [],

"scope": "singleton",

"type": "org.springdoc.webmvc.core.configuration.SpringDocWebMvcConfiguration$SpringDocWebMvcRouterConfiguration",

"dependencies": []

},

"defaultMeterObservationHandler": {

"aliases": [],

"scope": "singleton",

"type": "io.micrometer.core.instrument.observation.DefaultMeterObservationHandler",

"resource": "class path resource [org/springframework/boot/actuate/autoconfigure/observation/ObservationAutoConfiguration$MeterObservationHandlerConfiguration$OnlyMetricsMeterObservationHandlerConfiguration.class]",

"dependencies": [

"org.springframework.boot.actuate.autoconfigure.observation.ObservationAutoConfiguration$MeterObservationHandlerConfiguration$OnlyMetricsMeterObservationHandlerConfiguration",

"simpleMeterRegistry"

]

},

"resourceHandlerMapping": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.web.servlet.handler.SimpleUrlHandlerMapping",

"resource": "class path resource [org/springframework/boot/autoconfigure/web/servlet/WebMvcAutoConfiguration$EnableWebMvcConfiguration.class]",

"dependencies": [

"org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguration$EnableWebMvcConfiguration",

"mvcContentNegotiationManager",

"mvcConversionService",

"mvcResourceUrlProvider"

]

},

"simpleControllerHandlerAdapter": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.web.servlet.mvc.SimpleControllerHandlerAdapter",

"resource": "class path resource [org/springframework/boot/autoconfigure/web/servlet/WebMvcAutoConfiguration$EnableWebMvcConfiguration.class]",

"dependencies": [

"org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguration$EnableWebMvcConfiguration"

]

},

"org.springframework.boot.actuate.autoconfigure.metrics.export.simple.SimpleMetricsExportAutoConfiguration": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.boot.actuate.autoconfigure.metrics.export.simple.SimpleMetricsExportAutoConfiguration",

"dependencies": []

},

"propertyResolverUtils": {

"aliases": [],

"scope": "singleton",

"type": "org.springdoc.core.utils.PropertyResolverUtils",

"resource": "class path resource [org/springdoc/core/configuration/SpringDocConfiguration.class]",

"dependencies": [

"org.springdoc.core.configuration.SpringDocConfiguration",

"org.springframework.beans.factory.support.DefaultListableBeanFactory@37efd131",

"messageSource",

"org.springdoc.core.properties.SpringDocConfigProperties"

]

},

"org.springframework.boot.autoconfigure.jdbc.DataSourceJmxConfiguration$Hikari": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.boot.autoconfigure.jdbc.DataSourceJmxConfiguration$Hikari",

"dependencies": [

"dataSource"

]

},

"org.springdoc.core.properties.SwaggerUiConfigProperties": {

"aliases": [],

"scope": "singleton",

"type": "org.springdoc.core.properties.SwaggerUiConfigProperties",

"dependencies": []

},

"cachesEndpointWebExtension": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.boot.actuate.cache.CachesEndpointWebExtension",

"resource": "class path resource [org/springframework/boot/actuate/autoconfigure/cache/CachesEndpointAutoConfiguration.class]",

"dependencies": [

"org.springframework.boot.actuate.autoconfigure.cache.CachesEndpointAutoConfiguration",

"cachesEndpoint"

]

},

"org.springframework.boot.actuate.autoconfigure.context.properties.ConfigurationPropertiesReportEndpointAutoConfiguration": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.boot.actuate.autoconfigure.context.properties.ConfigurationPropertiesReportEndpointAutoConfiguration",

"dependencies": []

},

"simpleAsyncTaskSchedulerBuilder": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.boot.task.SimpleAsyncTaskSchedulerBuilder",

"resource": "class path resource [org/springframework/boot/autoconfigure/task/TaskSchedulingConfigurations$SimpleAsyncTaskSchedulerBuilderConfiguration.class]",

"dependencies": [

"org.springframework.boot.autoconfigure.task.TaskSchedulingConfigurations$SimpleAsyncTaskSchedulerBuilderConfiguration"

]

},

"spring.lifecycle-org.springframework.boot.autoconfigure.context.LifecycleProperties": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.boot.autoconfigure.context.LifecycleProperties",

"dependencies": []

},

"org.springframework.boot.autoconfigure.http.codec.CodecsAutoConfiguration$DefaultCodecsConfiguration": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.boot.autoconfigure.http.codec.CodecsAutoConfiguration$DefaultCodecsConfiguration",

"dependencies": []

},

"org.springframework.boot.actuate.autoconfigure.metrics.cache.CacheMetricsAutoConfiguration": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.boot.actuate.autoconfigure.metrics.cache.CacheMetricsAutoConfiguration",

"dependencies": []

},

"org.springframework.boot.autoconfigure.data.redis.LettuceConnectionConfiguration": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.boot.autoconfigure.data.redis.LettuceConnectionConfiguration",

"dependencies": [

"spring.data.redis-org.springframework.boot.autoconfigure.data.redis.RedisProperties",

"redisConnectionDetails"

]

},

"badgesController": {

"aliases": [],

"scope": "singleton",

"type": "cn.netkiller.controller.BadgesController",

"resource": "URL [jar:nested:/app/watch-1.0-SNAPSHOT.jar/!BOOT-INF/classes/!/cn/netkiller/controller/BadgesController.class]",

"dependencies": [

"baiduService",

"sessionStatusService",

"storyService"

]

},

"healthContributorRegistry": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.boot.actuate.autoconfigure.health.AutoConfiguredHealthContributorRegistry",

"resource": "class path resource [org/springframework/boot/actuate/autoconfigure/health/HealthEndpointConfiguration.class]",

"dependencies": [

"org.springframework.boot.actuate.autoconfigure.health.HealthEndpointConfiguration",

"org.springframework.boot.web.servlet.context.AnnotationConfigServletWebServerApplicationContext@18e36d14",

"healthEndpointGroups",

"diskSpaceHealthIndicator",

"pingHealthContributor",

"dbHealthContributor",

"redisHealthContributor"

]

},

"micrometerOptions": {

"aliases": [],

"scope": "singleton",

"type": "io.lettuce.core.metrics.MicrometerOptions",

"resource": "class path resource [org/springframework/boot/actuate/autoconfigure/metrics/redis/LettuceMetricsAutoConfiguration.class]",

"dependencies": [

"org.springframework.boot.actuate.autoconfigure.metrics.redis.LettuceMetricsAutoConfiguration"

]

},

"management.endpoint.configprops-org.springframework.boot.actuate.autoconfigure.context.properties.ConfigurationPropertiesReportEndpointProperties": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.boot.actuate.autoconfigure.context.properties.ConfigurationPropertiesReportEndpointProperties",

"dependencies": []

},

"parameterNamesModule": {

"aliases": [],

"scope": "singleton",

"type": "com.fasterxml.jackson.module.paramnames.ParameterNamesModule",

"resource": "class path resource [org/springframework/boot/autoconfigure/jackson/JacksonAutoConfiguration$ParameterNamesModuleConfiguration.class]",

"dependencies": [

"org.springframework.boot.autoconfigure.jackson.JacksonAutoConfiguration$ParameterNamesModuleConfiguration"

]

},

"propertiesObservationFilter": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.boot.actuate.autoconfigure.observation.PropertiesObservationFilterPredicate",

"resource": "class path resource [org/springframework/boot/actuate/autoconfigure/observation/ObservationAutoConfiguration.class]",

"dependencies": [

"org.springframework.boot.actuate.autoconfigure.observation.ObservationAutoConfiguration",

"management.observations-org.springframework.boot.actuate.autoconfigure.observation.ObservationProperties"

]

},

"sessionStatusRepository": {

"aliases": [],

"scope": "singleton",

"type": "cn.netkiller.repository.SessionStatusRepository",

"resource": "cn.netkiller.repository.SessionStatusRepository defined in @EnableJpaRepositories declared on Application",

"dependencies": [

"jpa.named-queries#3",

"jpa.SessionStatusRepository.fragments#0",

"jpaSharedEM_entityManagerFactory",

"jpaMappingContext"

]

},

"persistenceManagedTypes": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.orm.jpa.persistenceunit.SimplePersistenceManagedTypes",

"resource": "class path resource [org/springframework/boot/autoconfigure/orm/jpa/JpaBaseConfiguration$PersistenceManagedTypesConfiguration.class]",

"dependencies": [

"org.springframework.beans.factory.support.DefaultListableBeanFactory@37efd131",

"org.springframework.boot.web.servlet.context.AnnotationConfigServletWebServerApplicationContext@18e36d14"

]

},

"micrometerClock": {

"aliases": [],

"scope": "singleton",

"type": "io.micrometer.core.instrument.Clock$1",

"resource": "class path resource [org/springframework/boot/actuate/autoconfigure/metrics/MetricsAutoConfiguration.class]",

"dependencies": [

"org.springframework.boot.actuate.autoconfigure.metrics.MetricsAutoConfiguration"

]

},

"org.springframework.boot.actuate.autoconfigure.beans.BeansEndpointAutoConfiguration": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.boot.actuate.autoconfigure.beans.BeansEndpointAutoConfiguration",

"dependencies": []

},

"responseSupportConverter": {

"aliases": [],

"scope": "singleton",

"type": "org.springdoc.core.converters.ResponseSupportConverter",

"resource": "class path resource [org/springdoc/core/configuration/SpringDocConfiguration.class]",

"dependencies": [

"org.springdoc.core.configuration.SpringDocConfiguration",

"springdocObjectMapperProvider"

]

},

"pageableOpenAPIConverter": {

"aliases": [],

"scope": "singleton",

"type": "org.springdoc.core.converters.PageableOpenAPIConverter",

"resource": "class path resource [org/springdoc/core/configuration/SpringDocPageableConfiguration.class]",

"dependencies": [

"org.springdoc.core.configuration.SpringDocPageableConfiguration",

"springdocObjectMapperProvider"

]

},

"redisReferenceResolver": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.data.redis.core.convert.ReferenceResolverImpl",

"dependencies": [

"redisTemplate"

]

},

"propertiesMeterFilter": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.boot.actuate.autoconfigure.metrics.PropertiesMeterFilter",

"resource": "class path resource [org/springframework/boot/actuate/autoconfigure/metrics/MetricsAutoConfiguration.class]",

"dependencies": [

"org.springframework.boot.actuate.autoconfigure.metrics.MetricsAutoConfiguration",

"management.metrics-org.springframework.boot.actuate.autoconfigure.metrics.MetricsProperties"

]

},

"org.springframework.boot.autoconfigure.web.client.RestTemplateAutoConfiguration": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.boot.autoconfigure.web.client.RestTemplateAutoConfiguration",

"dependencies": []

},

"gsonBuilder": {

"aliases": [],

"scope": "singleton",

"type": "com.google.gson.GsonBuilder",

"resource": "class path resource [org/springframework/boot/autoconfigure/gson/GsonAutoConfiguration.class]",

"dependencies": [

"org.springframework.boot.autoconfigure.gson.GsonAutoConfiguration",

"standardGsonBuilderCustomizer"

]

},

"aliyunService": {

"aliases": [],

"scope": "singleton",

"type": "cn.netkiller.service.AliyunService",

"resource": "URL [jar:nested:/app/watch-1.0-SNAPSHOT.jar/!BOOT-INF/classes/!/cn/netkiller/service/AliyunService.class]",

"dependencies": []

},

"diskSpaceMetrics": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.boot.actuate.metrics.system.DiskSpaceMetricsBinder",

"resource": "class path resource [org/springframework/boot/actuate/autoconfigure/metrics/SystemMetricsAutoConfiguration.class]",

"dependencies": [

"org.springframework.boot.actuate.autoconfigure.metrics.SystemMetricsAutoConfiguration",

"management.metrics-org.springframework.boot.actuate.autoconfigure.metrics.MetricsProperties"

]

},

"securityParser": {

"aliases": [],

"scope": "singleton",

"type": "org.springdoc.core.service.SecurityService",

"resource": "class path resource [org/springdoc/core/configuration/SpringDocConfiguration.class]",

"dependencies": [

"org.springdoc.core.configuration.SpringDocConfiguration",

"propertyResolverUtils"

]

},

"org.springframework.data.jpa.util.JpaMetamodelCacheCleanup": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.data.jpa.util.JpaMetamodelCacheCleanup",

"dependencies": []

},

"homeController": {

"aliases": [],

"scope": "singleton",

"type": "cn.netkiller.controller.HomeController",

"resource": "URL [jar:nested:/app/watch-1.0-SNAPSHOT.jar/!BOOT-INF/classes/!/cn/netkiller/controller/HomeController.class]",

"dependencies": []

},

"org.springdoc.core.properties.SpringDocConfigProperties": {

"aliases": [],

"scope": "singleton",

"type": "org.springdoc.core.properties.SpringDocConfigProperties",

"dependencies": []

},

"org.springdoc.core.properties.SwaggerUiConfigParameters": {

"aliases": [],

"scope": "singleton",

"type": "org.springdoc.core.properties.SwaggerUiConfigParameters",

"dependencies": [

"org.springdoc.core.properties.SwaggerUiConfigProperties"

]

},

"org.springframework.boot.autoconfigure.web.servlet.error.ErrorMvcAutoConfiguration": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.boot.autoconfigure.web.servlet.error.ErrorMvcAutoConfiguration",

"dependencies": [

"server-org.springframework.boot.autoconfigure.web.ServerProperties"

]

},

"emBeanDefinitionRegistrarPostProcessor": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.data.jpa.repository.support.EntityManagerBeanDefinitionRegistrarPostProcessor",

"dependencies": []

},

"servletEndpointDiscoverer": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.boot.actuate.endpoint.web.annotation.ServletEndpointDiscoverer",

"resource": "class path resource [org/springframework/boot/actuate/autoconfigure/endpoint/web/WebEndpointAutoConfiguration$WebEndpointServletConfiguration.class]",

"dependencies": [

"org.springframework.boot.actuate.autoconfigure.endpoint.web.WebEndpointAutoConfiguration$WebEndpointServletConfiguration",

"org.springframework.boot.web.servlet.context.AnnotationConfigServletWebServerApplicationContext@18e36d14"

]

},

"metricsHttpServerUriTagFilter": {

"aliases": [],

"scope": "singleton",

"type": "io.micrometer.core.instrument.config.MeterFilter$9",

"resource": "class path resource [org/springframework/boot/actuate/autoconfigure/observation/web/servlet/WebMvcObservationAutoConfiguration$MeterFilterConfiguration.class]",

"dependencies": [

"org.springframework.boot.actuate.autoconfigure.observation.web.servlet.WebMvcObservationAutoConfiguration$MeterFilterConfiguration",

"management.observations-org.springframework.boot.actuate.autoconfigure.observation.ObservationProperties",

"management.metrics-org.springframework.boot.actuate.autoconfigure.metrics.MetricsProperties"

]

},

"eagerTaskExecutorMetrics": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.boot.LazyInitializationExcludeFilter$$Lambda/0x00007ff400afb340",

"resource": "class path resource [org/springframework/boot/actuate/autoconfigure/metrics/task/TaskExecutorMetricsAutoConfiguration.class]",

"dependencies": []

},

"org.springframework.boot.autoconfigure.websocket.servlet.WebSocketServletAutoConfiguration$TomcatWebSocketConfiguration": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.boot.autoconfigure.websocket.servlet.WebSocketServletAutoConfiguration$TomcatWebSocketConfiguration",

"dependencies": []

},

"org.springframework.boot.autoconfigure.http.HttpMessageConvertersAutoConfiguration": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.boot.autoconfigure.http.HttpMessageConvertersAutoConfiguration",

"dependencies": []

},

"redisConverter": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.data.redis.core.convert.MappingRedisConverter",

"dependencies": [

"keyValueMappingContext",

"redisReferenceResolver",

"redisCustomConversions"

]

},

"knife4jOpenApiCustomizer": {

"aliases": [],

"scope": "singleton",

"type": "com.github.xiaoymin.knife4j.spring.extension.Knife4jOpenApiCustomizer",

"resource": "class path resource [com/github/xiaoymin/knife4j/spring/configuration/Knife4jAutoConfiguration.class]",

"dependencies": [

"com.github.xiaoymin.knife4j.spring.configuration.Knife4jAutoConfiguration"

]

},

"healthEndpoint": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.boot.actuate.health.HealthEndpoint",

"resource": "class path resource [org/springframework/boot/actuate/autoconfigure/health/HealthEndpointConfiguration.class]",

"dependencies": [

"org.springframework.boot.actuate.autoconfigure.health.HealthEndpointConfiguration",

"healthContributorRegistry",

"healthEndpointGroups",

"management.endpoint.health-org.springframework.boot.actuate.autoconfigure.health.HealthEndpointProperties"

]

},

"spring.task.execution-org.springframework.boot.autoconfigure.task.TaskExecutionProperties": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.boot.autoconfigure.task.TaskExecutionProperties",

"dependencies": []

},

"viewControllerHandlerMapping": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.web.servlet.HandlerMapping",

"resource": "class path resource [org/springframework/boot/autoconfigure/web/servlet/WebMvcAutoConfiguration$EnableWebMvcConfiguration.class]",

"dependencies": [

"org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguration$EnableWebMvcConfiguration",

"mvcConversionService",

"mvcResourceUrlProvider"

]

},

"org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration$PooledDataSourceConfiguration": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration$PooledDataSourceConfiguration",

"dependencies": []

},

"openApiResource": {

"aliases": [],

"scope": "singleton",

"type": "org.springdoc.webmvc.api.OpenApiWebMvcResource",

"resource": "class path resource [org/springdoc/webmvc/core/configuration/SpringDocWebMvcConfiguration.class]",

"dependencies": [

"org.springdoc.webmvc.core.configuration.SpringDocWebMvcConfiguration",

"requestBuilder",

"responseBuilder",

"operationBuilder",

"org.springdoc.core.properties.SpringDocConfigProperties",

"springDocProviders",

"springDocCustomizers"

]

},

"themeResolver": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.web.servlet.theme.FixedThemeResolver",

"resource": "class path resource [org/springframework/boot/autoconfigure/web/servlet/WebMvcAutoConfiguration$EnableWebMvcConfiguration.class]",

"dependencies": [

"org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguration$EnableWebMvcConfiguration"

]

},

"org.springframework.boot.actuate.autoconfigure.availability.AvailabilityHealthContributorAutoConfiguration": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.boot.actuate.autoconfigure.availability.AvailabilityHealthContributorAutoConfiguration",

"dependencies": []

},

"org.springframework.boot.autoconfigure.jdbc.metadata.DataSourcePoolMetadataProvidersConfiguration$HikariPoolDataSourceMetadataProviderConfiguration": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.boot.autoconfigure.jdbc.metadata.DataSourcePoolMetadataProvidersConfiguration$HikariPoolDataSourceMetadataProviderConfiguration",

"dependencies": []

},

"mvcPatternParser": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.web.util.pattern.PathPatternParser",

"resource": "class path resource [org/springframework/boot/autoconfigure/web/servlet/WebMvcAutoConfiguration$EnableWebMvcConfiguration.class]",

"dependencies": [

"org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguration$EnableWebMvcConfiguration"

]

},

"org.springdoc.webmvc.core.configuration.SpringDocWebMvcConfiguration": {

"aliases": [],

"scope": "singleton",

"type": "org.springdoc.webmvc.core.configuration.SpringDocWebMvcConfiguration",

"dependencies": []

},

"org.springframework.boot.autoconfigure.orm.jpa.HibernateJpaAutoConfiguration": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.boot.autoconfigure.orm.jpa.HibernateJpaAutoConfiguration",

"dependencies": []

},

"org.springframework.boot.actuate.autoconfigure.health.HealthEndpointWebExtensionConfiguration$MvcAdditionalHealthEndpointPathsConfiguration": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.boot.actuate.autoconfigure.health.HealthEndpointWebExtensionConfiguration$MvcAdditionalHealthEndpointPathsConfiguration",

"dependencies": []

},

"org.springframework.boot.sql.init.dependency.DatabaseInitializationDependencyConfigurer$DependsOnDatabaseInitializationPostProcessor": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.boot.sql.init.dependency.DatabaseInitializationDependencyConfigurer$DependsOnDatabaseInitializationPostProcessor",

"dependencies": []

},

"dispatcherServlet": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.web.servlet.DispatcherServlet",

"resource": "class path resource [org/springframework/boot/autoconfigure/web/servlet/DispatcherServletAutoConfiguration$DispatcherServletConfiguration.class]",

"dependencies": [

"org.springframework.boot.autoconfigure.web.servlet.DispatcherServletAutoConfiguration$DispatcherServletConfiguration",

"spring.mvc-org.springframework.boot.autoconfigure.web.servlet.WebMvcProperties"

]

},

"org.springframework.boot.actuate.autoconfigure.metrics.task.TaskExecutorMetricsAutoConfiguration": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.boot.actuate.autoconfigure.metrics.task.TaskExecutorMetricsAutoConfiguration",

"dependencies": [

"applicationTaskExecutor",

"simpleMeterRegistry"

]

},

"restClientBuilder": {

"aliases": [],

"scope": "prototype",

"type": "org.springframework.web.client.RestClient$Builder",

"resource": "class path resource [org/springframework/boot/autoconfigure/web/client/RestClientAutoConfiguration.class]",

"dependencies": []

},

"springWebProvider": {

"aliases": [],

"scope": "singleton",

"type": "org.springdoc.webmvc.core.providers.SpringWebMvcProvider",

"resource": "class path resource [org/springdoc/webmvc/core/configuration/SpringDocWebMvcConfiguration.class]",

"dependencies": [

"org.springdoc.webmvc.core.configuration.SpringDocWebMvcConfiguration"

]

},

"jvmInfoMetrics": {

"aliases": [],

"scope": "singleton",

"type": "io.micrometer.core.instrument.binder.jvm.JvmInfoMetrics",

"resource": "class path resource [org/springframework/boot/actuate/autoconfigure/metrics/JvmMetricsAutoConfiguration.class]",

"dependencies": [

"org.springframework.boot.actuate.autoconfigure.metrics.JvmMetricsAutoConfiguration"

]

},

"org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguration": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguration",

"dependencies": []

},

"parameterBuilder": {

"aliases": [],

"scope": "singleton",

"type": "org.springdoc.core.service.GenericParameterService",

"resource": "class path resource [org/springdoc/core/configuration/SpringDocConfiguration.class]",

"dependencies": [

"org.springdoc.core.configuration.SpringDocConfiguration",

"propertyResolverUtils",

"springdocObjectMapperProvider"

]

},

"org.springframework.boot.actuate.autoconfigure.jdbc.DataSourceHealthContributorAutoConfiguration": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.boot.actuate.autoconfigure.jdbc.DataSourceHealthContributorAutoConfiguration",

"dependencies": []

},

"webEndpointServletHandlerMapping": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.boot.actuate.endpoint.web.servlet.WebMvcEndpointHandlerMapping",

"resource": "class path resource [org/springframework/boot/actuate/autoconfigure/endpoint/web/servlet/WebMvcEndpointManagementContextConfiguration.class]",

"dependencies": [

"org.springframework.boot.actuate.autoconfigure.endpoint.web.servlet.WebMvcEndpointManagementContextConfiguration",

"webEndpointDiscoverer",

"servletEndpointDiscoverer",

"controllerEndpointDiscoverer",

"endpointMediaTypes",

"management.endpoints.web.cors-org.springframework.boot.actuate.autoconfigure.endpoint.web.CorsEndpointProperties",

"management.endpoints.web-org.springframework.boot.actuate.autoconfigure.endpoint.web.WebEndpointProperties",

"environment"

]

},

"threadPoolTaskExecutorBuilder": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.boot.task.ThreadPoolTaskExecutorBuilder",

"resource": "class path resource [org/springframework/boot/autoconfigure/task/TaskExecutorConfigurations$ThreadPoolTaskExecutorBuilderConfiguration.class]",

"dependencies": [

"org.springframework.boot.autoconfigure.task.TaskExecutorConfigurations$ThreadPoolTaskExecutorBuilderConfiguration",

"spring.task.execution-org.springframework.boot.autoconfigure.task.TaskExecutionProperties"

]

},

"processorMetrics": {

"aliases": [],

"scope": "singleton",

"type": "io.micrometer.core.instrument.binder.system.ProcessorMetrics",

"resource": "class path resource [org/springframework/boot/actuate/autoconfigure/metrics/SystemMetricsAutoConfiguration.class]",

"dependencies": [

"org.springframework.boot.actuate.autoconfigure.metrics.SystemMetricsAutoConfiguration"

]

},

"dispatcherServletMappingDescriptionProvider": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.boot.actuate.web.mappings.servlet.DispatcherServletsMappingDescriptionProvider",

"resource": "class path resource [org/springframework/boot/actuate/autoconfigure/web/mappings/MappingsEndpointAutoConfiguration$ServletWebConfiguration$SpringMvcConfiguration.class]",

"dependencies": [

"org.springframework.boot.actuate.autoconfigure.web.mappings.MappingsEndpointAutoConfiguration$ServletWebConfiguration$SpringMvcConfiguration"

]

},

"org.springframework.boot.actuate.autoconfigure.scheduling.ScheduledTasksEndpointAutoConfiguration": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.boot.actuate.autoconfigure.scheduling.ScheduledTasksEndpointAutoConfiguration",

"dependencies": []

},

"chatService": {

"aliases": [],

"scope": "singleton",

"type": "cn.netkiller.service.ChatService",

"resource": "URL [jar:nested:/app/watch-1.0-SNAPSHOT.jar/!BOOT-INF/classes/!/cn/netkiller/service/ChatService.class]",

"dependencies": [

"chatRepository"

]

},

"spring.jdbc-org.springframework.boot.autoconfigure.jdbc.JdbcProperties": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.boot.autoconfigure.jdbc.JdbcProperties",

"dependencies": []

},

"org.springframework.boot.autoconfigure.cache.RedisCacheConfiguration": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.boot.autoconfigure.cache.RedisCacheConfiguration",

"dependencies": []

},

"psychoanalysisController": {

"aliases": [],

"scope": "singleton",

"type": "cn.netkiller.controller.PsychoanalysisController",

"resource": "URL [jar:nested:/app/watch-1.0-SNAPSHOT.jar/!BOOT-INF/classes/!/cn/netkiller/controller/PsychoanalysisController.class]",

"dependencies": [

"psychoanalysisService"

]

},

"org.springframework.boot.actuate.autoconfigure.metrics.integration.IntegrationMetricsAutoConfiguration": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.boot.actuate.autoconfigure.metrics.integration.IntegrationMetricsAutoConfiguration",

"dependencies": []

},

"transactionExecutionListeners": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.boot.autoconfigure.transaction.ExecutionListenersTransactionManagerCustomizer",

"resource": "class path resource [org/springframework/boot/autoconfigure/transaction/TransactionManagerCustomizationAutoConfiguration.class]",

"dependencies": [

"org.springframework.boot.autoconfigure.transaction.TransactionManagerCustomizationAutoConfiguration"

]

},

"redisConnectionFactory": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory",

"resource": "class path resource [org/springframework/boot/autoconfigure/data/redis/LettuceConnectionConfiguration.class]",

"dependencies": [

"org.springframework.boot.autoconfigure.data.redis.LettuceConnectionConfiguration",

"lettuceClientResources"

]

},

"springDocProviders": {

"aliases": [],

"scope": "singleton",

"type": "org.springdoc.core.providers.SpringDocProviders",

"resource": "class path resource [org/springdoc/core/configuration/SpringDocConfiguration.class]",

"dependencies": [

"org.springdoc.core.configuration.SpringDocConfiguration",

"springdocObjectMapperProvider"

]

},

"transactionManager": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.orm.jpa.JpaTransactionManager",

"resource": "class path resource [org/springframework/boot/autoconfigure/orm/jpa/HibernateJpaConfiguration.class]",

"dependencies": [

"org.springframework.boot.autoconfigure.orm.jpa.HibernateJpaConfiguration"

]

},

"spring.netty-org.springframework.boot.autoconfigure.netty.NettyProperties": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.boot.autoconfigure.netty.NettyProperties",

"dependencies": []

},

"errorPageCustomizer": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.boot.autoconfigure.web.servlet.error.ErrorMvcAutoConfiguration$ErrorPageCustomizer",

"resource": "class path resource [org/springframework/boot/autoconfigure/web/servlet/error/ErrorMvcAutoConfiguration.class]",

"dependencies": [

"org.springframework.boot.autoconfigure.web.servlet.error.ErrorMvcAutoConfiguration",

"dispatcherServletRegistration"

]

},

"loggersEndpoint": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.boot.actuate.logging.LoggersEndpoint",

"resource": "class path resource [org/springframework/boot/actuate/autoconfigure/logging/LoggersEndpointAutoConfiguration.class]",

"dependencies": [

"org.springframework.boot.actuate.autoconfigure.logging.LoggersEndpointAutoConfiguration",

"springBootLoggingSystem"

]

},

"standardGsonBuilderCustomizer": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.boot.autoconfigure.gson.GsonAutoConfiguration$StandardGsonBuilderCustomizer",

"resource": "class path resource [org/springframework/boot/autoconfigure/gson/GsonAutoConfiguration.class]",

"dependencies": [

"org.springframework.boot.autoconfigure.gson.GsonAutoConfiguration",

"spring.gson-org.springframework.boot.autoconfigure.gson.GsonProperties"

]

},

"org.springframework.boot.autoconfigure.web.servlet.error.ErrorMvcAutoConfiguration$DefaultErrorViewResolverConfiguration": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.boot.autoconfigure.web.servlet.error.ErrorMvcAutoConfiguration$DefaultErrorViewResolverConfiguration",

"dependencies": [

"org.springframework.boot.web.servlet.context.AnnotationConfigServletWebServerApplicationContext@18e36d14",

"spring.web-org.springframework.boot.autoconfigure.web.WebProperties"

]

},

"tomcatWebServerFactoryCustomizer": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.boot.autoconfigure.web.embedded.TomcatWebServerFactoryCustomizer",

"resource": "class path resource [org/springframework/boot/autoconfigure/web/embedded/EmbeddedWebServerFactoryCustomizerAutoConfiguration$TomcatWebServerFactoryCustomizerConfiguration.class]",

"dependencies": [

"org.springframework.boot.autoconfigure.web.embedded.EmbeddedWebServerFactoryCustomizerAutoConfiguration$TomcatWebServerFactoryCustomizerConfiguration",

"environment",

"server-org.springframework.boot.autoconfigure.web.ServerProperties"

]

},

"org.springframework.boot.actuate.autoconfigure.observation.ObservationAutoConfiguration$OnlyMetricsConfiguration": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.boot.actuate.autoconfigure.observation.ObservationAutoConfiguration$OnlyMetricsConfiguration",

"dependencies": []

},

"org.springframework.boot.actuate.autoconfigure.endpoint.EndpointAutoConfiguration": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.boot.actuate.autoconfigure.endpoint.EndpointAutoConfiguration",

"dependencies": []

},

"threadPoolTaskSchedulerBuilder": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.boot.task.ThreadPoolTaskSchedulerBuilder",

"resource": "class path resource [org/springframework/boot/autoconfigure/task/TaskSchedulingConfigurations$ThreadPoolTaskSchedulerBuilderConfiguration.class]",

"dependencies": [

"org.springframework.boot.autoconfigure.task.TaskSchedulingConfigurations$ThreadPoolTaskSchedulerBuilderConfiguration",

"spring.task.scheduling-org.springframework.boot.autoconfigure.task.TaskSchedulingProperties"

]

},

"org.springframework.boot.actuate.autoconfigure.data.redis.RedisHealthContributorAutoConfiguration": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.boot.actuate.autoconfigure.data.redis.RedisHealthContributorAutoConfiguration",

"dependencies": []

},

"viewNameTranslator": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.web.servlet.view.DefaultRequestToViewNameTranslator",

"resource": "class path resource [org/springframework/boot/autoconfigure/web/servlet/WebMvcAutoConfiguration$EnableWebMvcConfiguration.class]",

"dependencies": [

"org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguration$EnableWebMvcConfiguration"

]

},

"org.springframework.boot.autoconfigure.http.JacksonHttpMessageConvertersConfiguration": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.boot.autoconfigure.http.JacksonHttpMessageConvertersConfiguration",

"dependencies": []

},

"polymorphicModelConverter": {

"aliases": [],

"scope": "singleton",

"type": "org.springdoc.core.converters.PolymorphicModelConverter",

"resource": "class path resource [org/springdoc/core/configuration/SpringDocConfiguration.class]",

"dependencies": [

"org.springdoc.core.configuration.SpringDocConfiguration",

"springdocObjectMapperProvider"

]

},

"org.springframework.boot.actuate.autoconfigure.system.DiskSpaceHealthContributorAutoConfiguration": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.boot.actuate.autoconfigure.system.DiskSpaceHealthContributorAutoConfiguration",

"dependencies": []

},

"fileSupportConverter": {

"aliases": [],

"scope": "singleton",

"type": "org.springdoc.core.converters.FileSupportConverter",

"resource": "class path resource [org/springdoc/core/configuration/SpringDocConfiguration.class]",

"dependencies": [

"org.springdoc.core.configuration.SpringDocConfiguration",

"springdocObjectMapperProvider"

]

},

"org.springframework.boot.actuate.autoconfigure.metrics.MetricsAutoConfiguration": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.boot.actuate.autoconfigure.metrics.MetricsAutoConfiguration",

"dependencies": []

},

"cachesEndpoint": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.boot.actuate.cache.CachesEndpoint",

"resource": "class path resource [org/springframework/boot/actuate/autoconfigure/cache/CachesEndpointAutoConfiguration.class]",

"dependencies": [

"org.springframework.boot.actuate.autoconfigure.cache.CachesEndpointAutoConfiguration",

"cacheManager"

]

},

"org.springframework.boot.autoconfigure.http.codec.CodecsAutoConfiguration": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.boot.autoconfigure.http.codec.CodecsAutoConfiguration",

"dependencies": []

},

"dispatcherServletRegistration": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.boot.autoconfigure.web.servlet.DispatcherServletRegistrationBean",

"resource": "class path resource [org/springframework/boot/autoconfigure/web/servlet/DispatcherServletAutoConfiguration$DispatcherServletRegistrationConfiguration.class]",

"dependencies": [

"org.springframework.boot.autoconfigure.web.servlet.DispatcherServletAutoConfiguration$DispatcherServletRegistrationConfiguration",

"dispatcherServlet",

"spring.mvc-org.springframework.boot.autoconfigure.web.servlet.WebMvcProperties"

]

},

"pageableCustomizer": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.boot.autoconfigure.data.web.SpringDataWebAutoConfiguration$$Lambda/0x00007ff400a7e000",

"resource": "class path resource [org/springframework/boot/autoconfigure/data/web/SpringDataWebAutoConfiguration.class]",

"dependencies": [

"org.springframework.boot.autoconfigure.data.web.SpringDataWebAutoConfiguration"

]

},

"dataSource": {

"aliases": [],

"scope": "singleton",

"type": "com.zaxxer.hikari.HikariDataSource",

"resource": "class path resource [org/springframework/boot/autoconfigure/jdbc/DataSourceConfiguration$Hikari.class]",

"dependencies": [

"org.springframework.boot.autoconfigure.jdbc.DataSourceConfiguration$Hikari",

"spring.datasource-org.springframework.boot.autoconfigure.jdbc.DataSourceProperties",

"jdbcConnectionDetails"

]

},

"knife4jConfiguration": {

"aliases": [],

"scope": "singleton",

"type": "cn.netkiller.config.Knife4jConfiguration$$SpringCGLIB$$0",

"resource": "URL [jar:nested:/app/watch-1.0-SNAPSHOT.jar/!BOOT-INF/classes/!/cn/netkiller/config/Knife4jConfiguration.class]",

"dependencies": []

},

"openAPI": {

"aliases": [],

"scope": "prototype",

"type": "io.swagger.v3.oas.models.OpenAPI",

"resource": "class path resource [cn/netkiller/config/Knife4jConfiguration.class]",

"dependencies": [

"knife4jConfiguration"

]

},

"healthEndpointGroupsBeanPostProcessor": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.boot.actuate.autoconfigure.health.HealthEndpointConfiguration$HealthEndpointGroupsBeanPostProcessor",

"resource": "class path resource [org/springframework/boot/actuate/autoconfigure/health/HealthEndpointConfiguration.class]",

"dependencies": []

},

"management.endpoints.web-org.springframework.boot.actuate.autoconfigure.endpoint.web.WebEndpointProperties": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.boot.actuate.autoconfigure.endpoint.web.WebEndpointProperties",

"dependencies": []

},

"org.springframework.boot.autoconfigure.task.TaskExecutorConfigurations$TaskExecutorConfiguration": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.boot.autoconfigure.task.TaskExecutorConfigurations$TaskExecutorConfiguration",

"dependencies": []

},

"jpaMappingContext": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.data.jpa.mapping.JpaMetamodelMappingContext",

"dependencies": []

},

"tomcatServletWebServerFactory": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.boot.web.embedded.tomcat.TomcatServletWebServerFactory",

"resource": "class path resource [org/springframework/boot/autoconfigure/web/servlet/ServletWebServerFactoryConfiguration$EmbeddedTomcat.class]",

"dependencies": [

"org.springframework.boot.autoconfigure.web.servlet.ServletWebServerFactoryConfiguration$EmbeddedTomcat"

]

},

"org.springframework.boot.autoconfigure.http.JacksonHttpMessageConvertersConfiguration$MappingJackson2HttpMessageConverterConfiguration": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.boot.autoconfigure.http.JacksonHttpMessageConvertersConfiguration$MappingJackson2HttpMessageConverterConfiguration",

"dependencies": []

},

"spring.jackson-org.springframework.boot.autoconfigure.jackson.JacksonProperties": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.boot.autoconfigure.jackson.JacksonProperties",

"dependencies": []

},

"org.springframework.boot.autoconfigure.reactor.netty.ReactorNettyConfigurations$ReactorResourceFactoryConfiguration": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.boot.autoconfigure.reactor.netty.ReactorNettyConfigurations$ReactorResourceFactoryConfiguration",

"dependencies": []

},

"chatRepository": {

"aliases": [],

"scope": "singleton",

"type": "cn.netkiller.repository.ChatRepository",

"resource": "cn.netkiller.repository.ChatRepository defined in @EnableJpaRepositories declared on Application",

"dependencies": [

"jpa.named-queries#1",

"jpa.ChatRepository.fragments#0",

"jpaSharedEM_entityManagerFactory",

"jpaMappingContext"

]

},

"org.springframework.boot.autoconfigure.jackson.JacksonAutoConfiguration": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.boot.autoconfigure.jackson.JacksonAutoConfiguration",

"dependencies": []

},

"error": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.boot.autoconfigure.web.servlet.error.ErrorMvcAutoConfiguration$StaticView",

"resource": "class path resource [org/springframework/boot/autoconfigure/web/servlet/error/ErrorMvcAutoConfiguration$WhitelabelErrorViewConfiguration.class]",

"dependencies": [

"org.springframework.boot.autoconfigure.web.servlet.error.ErrorMvcAutoConfiguration$WhitelabelErrorViewConfiguration"

]

},

"org.springframework.boot.autoconfigure.data.web.SpringDataWebAutoConfiguration": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.boot.autoconfigure.data.web.SpringDataWebAutoConfiguration",

"dependencies": [

"spring.data.web-org.springframework.boot.autoconfigure.data.web.SpringDataWebProperties"

]

},

"memberRepository": {

"aliases": [],

"scope": "singleton",

"type": "cn.netkiller.repository.MemberRepository",

"resource": "cn.netkiller.repository.MemberRepository defined in @EnableJpaRepositories declared on Application",

"dependencies": [

"jpa.named-queries#0",

"jpa.MemberRepository.fragments#0",

"jpaSharedEM_entityManagerFactory",

"jpaMappingContext"

]

},

"org.springframework.boot.autoconfigure.jackson.JacksonAutoConfiguration$JacksonObjectMapperConfiguration": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.boot.autoconfigure.jackson.JacksonAutoConfiguration$JacksonObjectMapperConfiguration",

"dependencies": []

},

"swaggerConfigResource": {

"aliases": [],

"scope": "singleton",

"type": "org.springdoc.webmvc.ui.SwaggerConfigResource",

"resource": "class path resource [org/springdoc/webmvc/ui/SwaggerConfig.class]",

"dependencies": [

"org.springdoc.webmvc.ui.SwaggerConfig",

"swaggerWelcome"

]

},

"org.springframework.boot.actuate.autoconfigure.health.HealthEndpointAutoConfiguration": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.boot.actuate.autoconfigure.health.HealthEndpointAutoConfiguration",

"dependencies": []

},

"org.springframework.boot.autoconfigure.web.servlet.error.ErrorMvcAutoConfiguration$WhitelabelErrorViewConfiguration": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.boot.autoconfigure.web.servlet.error.ErrorMvcAutoConfiguration$WhitelabelErrorViewConfiguration",

"dependencies": []

},

"delegatingMethodParameterCustomizer": {

"aliases": [],

"scope": "singleton",

"type": "org.springdoc.core.customizers.DataRestDelegatingMethodParameterCustomizer",

"resource": "class path resource [org/springdoc/core/configuration/SpringDocPageableConfiguration.class]",

"dependencies": [

"org.springdoc.core.configuration.SpringDocPageableConfiguration"

]

},

"jsonMixinModule": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.boot.jackson.JsonMixinModule",

"resource": "class path resource [org/springframework/boot/autoconfigure/jackson/JacksonAutoConfiguration$JacksonMixinConfiguration.class]",

"dependencies": [

"org.springframework.boot.autoconfigure.jackson.JacksonAutoConfiguration$JacksonMixinConfiguration",

"org.springframework.boot.web.servlet.context.AnnotationConfigServletWebServerApplicationContext@18e36d14",

"jsonMixinModuleEntries"

]

},

"org.springframework.boot.autoconfigure.web.servlet.DispatcherServletAutoConfiguration$DispatcherServletConfiguration": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.boot.autoconfigure.web.servlet.DispatcherServletAutoConfiguration$DispatcherServletConfiguration",

"dependencies": []

},

"jdbcConnectionDetails": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.boot.autoconfigure.jdbc.PropertiesJdbcConnectionDetails",

"resource": "class path resource [org/springframework/boot/autoconfigure/jdbc/DataSourceAutoConfiguration$PooledDataSourceConfiguration.class]",

"dependencies": [

"org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration$PooledDataSourceConfiguration",

"spring.datasource-org.springframework.boot.autoconfigure.jdbc.DataSourceProperties"

]

},

"genericReturnTypeParser": {

"aliases": [],

"scope": "singleton",

"type": "org.springdoc.core.configuration.SpringDocConfiguration$1",

"resource": "class path resource [org/springdoc/core/configuration/SpringDocConfiguration.class]",

"dependencies": [

"org.springdoc.core.configuration.SpringDocConfiguration"

]

},

"jvmGcMetrics": {

"aliases": [],

"scope": "singleton",

"type": "io.micrometer.core.instrument.binder.jvm.JvmGcMetrics",

"resource": "class path resource [org/springframework/boot/actuate/autoconfigure/metrics/JvmMetricsAutoConfiguration.class]",

"dependencies": [

"org.springframework.boot.actuate.autoconfigure.metrics.JvmMetricsAutoConfiguration"

]

},

"offsetResolver": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.data.web.OffsetScrollPositionHandlerMethodArgumentResolver",

"resource": "class path resource [org/springframework/data/web/config/SpringDataWebConfiguration.class]",

"dependencies": [

"org.springframework.data.web.config.SpringDataWebConfiguration"

]

},

"org.springframework.boot.autoconfigure.transaction.TransactionAutoConfiguration$TransactionTemplateConfiguration": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.boot.autoconfigure.transaction.TransactionAutoConfiguration$TransactionTemplateConfiguration",

"dependencies": []

},

"org.springframework.boot.autoconfigure.web.client.RestClientAutoConfiguration": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.boot.autoconfigure.web.client.RestClientAutoConfiguration",

"dependencies": []

},

"healthEndpointWebMvcHandlerMapping": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.boot.actuate.endpoint.web.servlet.AdditionalHealthEndpointPathsWebMvcHandlerMapping",

"resource": "class path resource [org/springframework/boot/actuate/autoconfigure/health/HealthEndpointWebExtensionConfiguration$MvcAdditionalHealthEndpointPathsConfiguration.class]",

"dependencies": [

"org.springframework.boot.actuate.autoconfigure.health.HealthEndpointWebExtensionConfiguration$MvcAdditionalHealthEndpointPathsConfiguration",

"webEndpointDiscoverer",

"healthEndpointGroups"

]

},

"welcomePageHandlerMapping": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.boot.autoconfigure.web.servlet.WelcomePageHandlerMapping",

"resource": "class path resource [org/springframework/boot/autoconfigure/web/servlet/WebMvcAutoConfiguration$EnableWebMvcConfiguration.class]",

"dependencies": [

"org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguration$EnableWebMvcConfiguration",

"org.springframework.boot.web.servlet.context.AnnotationConfigServletWebServerApplicationContext@18e36d14",

"mvcConversionService",

"mvcResourceUrlProvider"

]

},

"redisKeyValueAdapter": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.data.redis.core.RedisKeyValueAdapter",

"dependencies": [

"redisTemplate",

"redisConverter"

]

},

"servletExposeExcludePropertyEndpointFilter": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.boot.actuate.autoconfigure.endpoint.expose.IncludeExcludeEndpointFilter",

"resource": "class path resource [org/springframework/boot/actuate/autoconfigure/endpoint/web/ServletEndpointManagementContextConfiguration.class]",

"dependencies": [

"org.springframework.boot.actuate.autoconfigure.endpoint.web.ServletEndpointManagementContextConfiguration",

"management.endpoints.web-org.springframework.boot.actuate.autoconfigure.endpoint.web.WebEndpointProperties"

]

},

"filterMappingDescriptionProvider": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.boot.actuate.web.mappings.servlet.FiltersMappingDescriptionProvider",

"resource": "class path resource [org/springframework/boot/actuate/autoconfigure/web/mappings/MappingsEndpointAutoConfiguration$ServletWebConfiguration.class]",

"dependencies": [

"org.springframework.boot.actuate.autoconfigure.web.mappings.MappingsEndpointAutoConfiguration$ServletWebConfiguration"

]

},

"springdocBeanFactoryPostProcessor": {

"aliases": [],

"scope": "singleton",

"type": "org.springdoc.core.configurer.SpringdocBeanFactoryConfigurer",

"resource": "class path resource [org/springdoc/core/configuration/SpringDocConfiguration.class]",

"dependencies": []

},

"cacheManagerCustomizers": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.boot.autoconfigure.cache.CacheManagerCustomizers",

"resource": "class path resource [org/springframework/boot/autoconfigure/cache/CacheAutoConfiguration.class]",

"dependencies": [

"org.springframework.boot.autoconfigure.cache.CacheAutoConfiguration"

]

},

"org.springframework.boot.actuate.autoconfigure.web.exchanges.HttpExchangesEndpointAutoConfiguration": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.boot.actuate.autoconfigure.web.exchanges.HttpExchangesEndpointAutoConfiguration",

"dependencies": []

},

"jacksonObjectMapper": {

"aliases": [],

"scope": "singleton",

"type": "com.fasterxml.jackson.databind.ObjectMapper",

"resource": "class path resource [org/springframework/boot/autoconfigure/jackson/JacksonAutoConfiguration$JacksonObjectMapperConfiguration.class]",

"dependencies": [

"org.springframework.boot.autoconfigure.jackson.JacksonAutoConfiguration$JacksonObjectMapperConfiguration",

"jacksonObjectMapperBuilder"

]

},

"webMvcObservationFilter": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.boot.web.servlet.FilterRegistrationBean",

"resource": "class path resource [org/springframework/boot/actuate/autoconfigure/observation/web/servlet/WebMvcObservationAutoConfiguration.class]",

"dependencies": [

"org.springframework.boot.actuate.autoconfigure.observation.web.servlet.WebMvcObservationAutoConfiguration",

"observationRegistry",

"management.observations-org.springframework.boot.actuate.autoconfigure.observation.ObservationProperties"

]

},

"org.springdoc.core.properties.SwaggerUiOAuthProperties": {

"aliases": [],

"scope": "singleton",

"type": "org.springdoc.core.properties.SwaggerUiOAuthProperties",

"dependencies": []

},

"management.endpoint.logfile-org.springframework.boot.actuate.autoconfigure.logging.LogFileWebEndpointProperties": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.boot.actuate.autoconfigure.logging.LogFileWebEndpointProperties",

"dependencies": []

},

"org.springframework.boot.actuate.autoconfigure.metrics.MetricsAspectsAutoConfiguration": {

"aliases": [],

"scope": "singleton",

"type": "org.springframework.boot.actuate.autoconfigure.metrics.MetricsAspectsAutoConfiguration",

"dependencies": []

},

"gson": {

"aliases": [],

"scope": "singleton",

"type": "com.google.gson.Gson",

"resource": "class path resource [org/springframework/boot/autoconfigure/gson/GsonAutoConfiguration.class]",

"dependencies": [

"org.springframework.boot.autoconfigure.gson.GsonAutoConfiguration",

"gsonBuilder"

]

}

}

}

}

}

7. caches

neo@MacBook-Pro-M2 ~ % curl -s http://www.netkiller.cn:8080/actuator/caches | jq

{

"cacheManagers": {

"cacheManager": {

"caches": {

"censor": {

"target": "org.springframework.data.redis.cache.DefaultRedisCacheWriter"

},

"translate": {

"target": "org.springframework.data.redis.cache.DefaultRedisCacheWriter"

}

}

}

}

}

neo@MacBook-Pro-M2 ~ % curl -s http://www.netkiller.cn:8080/actuator/caches/censor |jq

{

"target": "org.springframework.data.redis.cache.DefaultRedisCacheWriter",

"name": "censor",

"cacheManager": "cacheManager"

}

8. conditions

neo@MacBook-Pro-M2 ~ % curl -s http://www.netkiller.cn:8080/actuator/conditions | jq | more

{

"contexts": {

"watch-production": {

"positiveMatches": {

"Knife4jAutoConfiguration": [

{

"condition": "OnPropertyCondition",

"message": "@ConditionalOnProperty (knife4j.enable=true) matched"

}

],

"Knife4jAutoConfiguration#knife4jJakartaOperationCustomizer": [

{

"condition": "OnBeanCondition",

"message": "@ConditionalOnMissingBean (types: com.github.xiaoymin.knife4j.spring.extension.Knife4jJakartaOperationCustomizer; SearchStrategy: all) did not find any beans"

}

],

"Knife4jAutoConfiguration#knife4jOpenApiCustomizer": [

{

"condition": "OnBeanCondition",

"message": "@ConditionalOnMissingBean (types: com.github.xiaoymin.knife4j.spring.extension.Knife4jOpenApiCustomizer; SearchStrategy: all) did not find any beans"

}

],

"SpringDocConfiguration": [

{

"condition": "OnWebApplicationCondition",

"message": "@ConditionalOnWebApplication (required) found 'session' scope"

},

{

"condition": "OnPropertyCondition",

"message": "@ConditionalOnProperty (springdoc.api-docs.enabled) matched"

}

],

"SpringDocConfiguration#fileSupportConverter": [

{

"condition": "OnBeanCondition",

"message": "@ConditionalOnMissingBean (types: org.springdoc.core.converters.FileSupportConverter; SearchStrategy: all) did not find any beans"

}

],

"SpringDocConfiguration#openAPIBuilder": [

{

"condition": "OnBeanCondition",

"message": "@ConditionalOnMissingBean (types: org.springdoc.core.service.OpenAPIService; SearchStrategy: all) did not find any beans"

}

],

"SpringDocConfiguration#operationBuilder": [

{

"condition": "OnBeanCondition",

"message": "@ConditionalOnMissingBean (types: org.springdoc.core.service.OperationService; SearchStrategy: all) did not find any beans"

}

],

"SpringDocConfiguration#parameterBuilder": [

{

"condition": "OnBeanCondition",

"message": "@ConditionalOnMissingBean (types: org.springdoc.core.service.GenericParameterService; SearchStrategy: all) did not find any beans"

}

],

9. configprops 配置文件

查看 spring.task.execution 配置

neo@MacBook-Pro-M2 ~ % curl -s http://www.netkiller.cn:8080/actuator/configprops/spring.task.execution | jq

{

"contexts": {

"watch-production": {

"beans": {

"spring.task.execution-org.springframework.boot.autoconfigure.task.TaskExecutionProperties": {

"prefix": "spring.task.execution",

"properties": {

"pool": {

"queueCapacity": "******",

"coreSize": "******",

"maxSize": "******",

"allowCoreThreadTimeout": "******",

"keepAlive": "******"

},

"simple": {},

"threadNamePrefix": "******",

"shutdown": {

"awaitTermination": "******",

"awaitTerminationPeriod": "******"

}

},

"inputs": {

"pool": {

"queueCapacity": {

"value": "******",

"origin": "class path resource [application.properties] from watch-1.0-SNAPSHOT.jar - 25:43"

},

"coreSize": {

"value": "******",

"origin": "class path resource [application.properties] from watch-1.0-SNAPSHOT.jar - 23:38"

},

"maxSize": {

"value": "******",

"origin": "class path resource [application.properties] from watch-1.0-SNAPSHOT.jar - 24:37"

},

"allowCoreThreadTimeout": {

"value": "******",

"origin": "class path resource [application.properties] from watch-1.0-SNAPSHOT.jar - 27:54"

},

"keepAlive": {

"value": "******",

"origin": "class path resource [application.properties] from watch-1.0-SNAPSHOT.jar - 26:39"

}

},

"simple": {},

"threadNamePrefix": {

"value": "******",

"origin": "class path resource [application.properties] from watch-1.0-SNAPSHOT.jar - 30:42"

},

"shutdown": {

"awaitTermination": {

"value": "******",

"origin": "class path resource [application.properties] from watch-1.0-SNAPSHOT.jar - 28:50"

},

"awaitTerminationPeriod": {

"value": "******",

"origin": "class path resource [application.properties] from watch-1.0-SNAPSHOT.jar - 29:57"

}

}

}

}

}

}

}

}

查看所有配置

neo@MacBook-Pro-M2 ~ % curl -s http://www.netkiller.cn:8080/actuator/configprops | jq

{

"contexts": {

"watch-production": {

"beans": {

"spring.transaction-org.springframework.boot.autoconfigure.transaction.TransactionProperties": {

"prefix": "spring.transaction",

"properties": {},

"inputs": {}

},

"spring.jpa-org.springframework.boot.autoconfigure.orm.jpa.JpaProperties": {

"prefix": "spring.jpa",

"properties": {

"mappingResources": [],

"showSql": "******",

"openInView": "******",

"generateDdl": "******",

"properties": {}

},

"inputs": {

"mappingResources": [],

"showSql": {},

"openInView": {

"value": "******",

"origin": "class path resource [application.properties] from watch-1.0-SNAPSHOT.jar - 41:25"

},

"generateDdl": {},

"properties": {}

}

},

"management.observations-org.springframework.boot.actuate.autoconfigure.observation.ObservationProperties": {

"prefix": "management.observations",

"properties": {

"keyValues": {},

"http": {

"client": {

"requests": {

"name": "******"

}

},

"server": {

"requests": {

"name": "******"

}

}

},

"enable": {}

},

"inputs": {

"keyValues": {},

"http": {

"client": {

"requests": {

"name": {}

}

},

"server": {

"requests": {

"name": {}

}

}

},

"enable": {}

}

},

"management.endpoints.web-org.springframework.boot.actuate.autoconfigure.endpoint.web.WebEndpointProperties": {

"prefix": "management.endpoints.web",

"properties": {

"pathMapping": {},

"exposure": {

"include": [

"******"

],

"exclude": []

},

"basePath": "******",

"discovery": {

"enabled": "******"

}

},

"inputs": {

"pathMapping": {},

"exposure": {

"include": [

{

"value": "******",

"origin": "class path resource [application.properties] from watch-1.0-SNAPSHOT.jar - 20:43"

}

],

"exclude": []

},

"basePath": {},

"discovery": {

"enabled": {}

}

}

},

"spring.cache-org.springframework.boot.autoconfigure.cache.CacheProperties": {

"prefix": "spring.cache",

"properties": {

"caffeine": {},

"infinispan": {},

"cacheNames": [],

"couchbase": {},

"jcache": {},

"type": "******",

"redis": {

"timeToLive": "******",

"cacheNullValues": "******",

"useKeyPrefix": "******",

"enableStatistics": "******"

}

},

"inputs": {

"caffeine": {},

"infinispan": {},

"cacheNames": [],

"couchbase": {},

"jcache": {},

"type": {

"value": "******",

"origin": "class path resource [application.properties] from watch-1.0-SNAPSHOT.jar - 60:19"

},

"redis": {

"timeToLive": {

"value": "******",

"origin": "class path resource [application.properties] from watch-1.0-SNAPSHOT.jar - 61:33"

},

"cacheNullValues": {

"value": "******",

"origin": "class path resource [application.properties] from watch-1.0-SNAPSHOT.jar - 62:38"

},

"useKeyPrefix": {},

"enableStatistics": {}

}

}

},

"spring.jdbc-org.springframework.boot.autoconfigure.jdbc.JdbcProperties": {

"prefix": "spring.jdbc",

"properties": {

"template": {

"fetchSize": "******",

"maxRows": "******"

}

},

"inputs": {

"template": {

"fetchSize": {},

"maxRows": {}

}

}

},

"spring.data.redis-org.springframework.boot.autoconfigure.data.redis.RedisProperties": {

"prefix": "spring.data.redis",

"properties": {

"database": "******",

"password": "******",

"port": "******",

"jedis": {

"pool": {

"maxIdle": "******",

"minIdle": "******",

"maxActive": "******",

"maxWait": "******"

}

},

"host": "******",

"ssl": {

"enabled": "******"

},

"lettuce": {

"shutdownTimeout": "******",

"pool": {

"maxIdle": "******",

"minIdle": "******",

"maxActive": "******",

"maxWait": "******"

},

"cluster": {

"refresh": {

"dynamicRefreshSources": "******",

"adaptive": "******"

}

}

},

"timeout": "******"

},

"inputs": {

"database": {

"value": "******",

"origin": "class path resource [application-prod.properties] from watch-1.0-SNAPSHOT.jar - 11:28"

},

"password": {

"value": "******",

"origin": "class path resource [application-prod.properties] from watch-1.0-SNAPSHOT.jar - 10:28"

},

"port": {

"value": "******",

"origin": "class path resource [application-prod.properties] from watch-1.0-SNAPSHOT.jar - 9:24"

},

"jedis": {

"pool": {

"maxIdle": {},

"minIdle": {},

"maxActive": {},

"maxWait": {}

}

},

"host": {

"value": "******",

"origin": "class path resource [application-prod.properties] from watch-1.0-SNAPSHOT.jar - 8:24"

},

"ssl": {

"enabled": {}

},

"lettuce": {

"shutdownTimeout": {},

"pool": {

"maxIdle": {

"value": "******",

"origin": "class path resource [application.properties] from watch-1.0-SNAPSHOT.jar - 58:41"

},

"minIdle": {

"value": "******",

"origin": "class path resource [application.properties] from watch-1.0-SNAPSHOT.jar - 59:41"

},

"maxActive": {

"value": "******",

"origin": "class path resource [application.properties] from watch-1.0-SNAPSHOT.jar - 56:43"

},

"maxWait": {

"value": "******",

"origin": "class path resource [application.properties] from watch-1.0-SNAPSHOT.jar - 57:41"

}

},

"cluster": {

"refresh": {

"dynamicRefreshSources": {},

"adaptive": {}

}

}

},

"timeout": {

"value": "******",

"origin": "class path resource [application-prod.properties] from watch-1.0-SNAPSHOT.jar - 12:27"

}

}

},

"spring.jackson-org.springframework.boot.autoconfigure.jackson.JacksonProperties": {

"prefix": "spring.jackson",

"properties": {

"serialization": {},

"visibility": {},

"parser": {},

"datatype": {

"jsonNode": {},

"enum": {}

},

"deserialization": {},

"generator": {},

"mapper": {}

},

"inputs": {

"serialization": {},

"visibility": {},

"parser": {},

"datatype": {

"jsonNode": {},

"enum": {}

},

"deserialization": {},

"generator": {},

"mapper": {}

}

},

"spring.gson-org.springframework.boot.autoconfigure.gson.GsonProperties": {

"prefix": "spring.gson",

"properties": {},

"inputs": {}

},

"management.health.db-org.springframework.boot.actuate.autoconfigure.jdbc.DataSourceHealthIndicatorProperties": {

"prefix": "management.health.db",

"properties": {

"ignoreRoutingDataSources": "******"

},

"inputs": {

"ignoreRoutingDataSources": {}

}

},

"spring.data.web-org.springframework.boot.autoconfigure.data.web.SpringDataWebProperties": {

"prefix": "spring.data.web",

"properties": {

"pageable": {

"pageParameter": "******",

"sizeParameter": "******",

"oneIndexedParameters": "******",

"prefix": "******",

"qualifierDelimiter": "******",

"defaultPageSize": "******",

"maxPageSize": "******"

},

"sort": {

"sortParameter": "******"

}

},

"inputs": {

"pageable": {

"pageParameter": {},

"sizeParameter": {},

"oneIndexedParameters": {},

"prefix": {},

"qualifierDelimiter": {},

"defaultPageSize": {},

"maxPageSize": {}

},

"sort": {

"sortParameter": {}

}

}

},

"org.springdoc.core.properties.SpringDocConfigProperties": {

"prefix": "springdoc",

"properties": {

"removeBrokenReferenceDefinitions": "******",

"writerWithOrderByKeys": "******",

"disableI18n": "******",

"showLoginEndpoint": "******",

"nullableRequestParameterEnabled": "******",

"enableJavadoc": "******",

"useFqn": "******",

"useManagementPort": "******",

"showSpringCloudFunctions": "******",

"enableDataRest": "******",

"cache": {

"disabled": "******"

},

"preLoadingEnabled": "******",

"enableKotlin": "******",

"enableGroovy": "******",

"defaultProducesMediaType": "******",

"modelConverters": {

"deprecatingConverter": {

"enabled": "******"

},

"pageableConverter": {

"enabled": "******"

},

"polymorphicConverter": {

"enabled": "******"

}

},

"enableHateoas": "******",

"groupConfigs": [

{

"pathsToMatch": [

"******"

],

"group": "******",

"displayName": "******"

}

],

"enableSpringSecurity": "******",

"writerWithDefaultPrettyPrinter": "******",

"showOauth2Endpoints": "******",

"showActuator": "******",

"defaultFlatParamObject": "******",

"defaultSupportFormData": "******",

"apiDocs": {

"path": "******",

"enabled": "******",

"resolveSchemaProperties": "******",

"groups": {

"enabled": "******"

}

},

"autoTagClasses": "******",

"webjars": {

"prefix": "******"

},

"modelAndViewAllowed": "******",

"defaultConsumesMediaType": "******",

"sortConverter": {

"enabled": "******"

}

},

"inputs": {

"removeBrokenReferenceDefinitions": {},

"writerWithOrderByKeys": {},

"disableI18n": {},

"showLoginEndpoint": {},

"nullableRequestParameterEnabled": {},

"enableJavadoc": {},

"useFqn": {},

"useManagementPort": {},

"showSpringCloudFunctions": {},

"enableDataRest": {},

"cache": {

"disabled": {}

},

"preLoadingEnabled": {},

"enableKotlin": {},

"enableGroovy": {},

"defaultProducesMediaType": {},

"modelConverters": {

"deprecatingConverter": {

"enabled": {}

},

"pageableConverter": {

"enabled": {}

},

"polymorphicConverter": {

"enabled": {}

}

},

"enableHateoas": {},

"groupConfigs": [

{

"pathsToMatch": [

{}

],

"group": {},

"displayName": {}

}

],

"enableSpringSecurity": {},

"writerWithDefaultPrettyPrinter": {},

"showOauth2Endpoints": {},

"showActuator": {},

"defaultFlatParamObject": {},

"defaultSupportFormData": {},

"apiDocs": {

"path": {

"value": "******",

"origin": "class path resource [application.properties] from watch-1.0-SNAPSHOT.jar - 38:25"

},

"enabled": {},

"resolveSchemaProperties": {},

"groups": {

"enabled": {}

}

},

"autoTagClasses": {},

"webjars": {

"prefix": {}

},

"modelAndViewAllowed": {},

"defaultConsumesMediaType": {},

"sortConverter": {

"enabled": {}

}

}

},

"spring.jpa.hibernate-org.springframework.boot.autoconfigure.orm.jpa.HibernateProperties": {

"prefix": "spring.jpa.hibernate",

"properties": {

"naming": {},

"ddlAuto": "******"

},

"inputs": {

"naming": {},

"ddlAuto": {

"value": "******",

"origin": "class path resource [application.properties] from watch-1.0-SNAPSHOT.jar - 43:31"

}

}

},

"spring.info-org.springframework.boot.autoconfigure.info.ProjectInfoProperties": {

"prefix": "spring.info",

"properties": {

"build": {

"location": {},

"encoding": "******"

},

"git": {

"location": {},

"encoding": "******"

}

},

"inputs": {

"build": {

"location": {},

"encoding": {}

},

"git": {

"location": {},

"encoding": {}

}

}

},

"spring.datasource-org.springframework.boot.autoconfigure.jdbc.DataSourceProperties": {

"prefix": "spring.datasource",

"properties": {

"password": "******",

"embeddedDatabaseConnection": "******",

"driverClassName": "******",

"generateUniqueName": "******",

"xa": {

"properties": {}

},

"url": "******",

"username": "******"

},

"inputs": {

"password": {

"value": "******",

"origin": "class path resource [application-prod.properties] from watch-1.0-SNAPSHOT.jar - 6:28"

},

"embeddedDatabaseConnection": {},

"driverClassName": {

"value": "******",

"origin": "class path resource [application-prod.properties] from watch-1.0-SNAPSHOT.jar - 3:37"

},

"generateUniqueName": {},

"xa": {

"properties": {}

},

"url": {

"value": "******",

"origin": "class path resource [application-prod.properties] from watch-1.0-SNAPSHOT.jar - 4:23"

},

"username": {

"value": "******",

"origin": "class path resource [application-prod.properties] from watch-1.0-SNAPSHOT.jar - 5:28"

}

}

},

"org.springdoc.core.properties.SwaggerUiConfigProperties": {

"prefix": "springdoc.swagger-ui",

"properties": {

"path": "******",

"operationsSorter": "******",

"useRootPath": "******",

"groupsOrder": "******",

"tagsSorter": "******",

"disableSwaggerDefaultUrl": "******",

"csrf": {

"enabled": "******",

"useLocalStorage": "******",

"useSessionStorage": "******",

"cookieName": "******",

"localStorageKey": "******",

"sessionStorageKey": "******",

"headerName": "******"

},

"syntaxHighlight": {},

"version": "******",

"enabled": "******"

},

"inputs": {

"path": {

"value": "******",

"origin": "class path resource [application.properties] from watch-1.0-SNAPSHOT.jar - 35:27"

},

"operationsSorter": {

"value": "******",

"origin": "class path resource [application.properties] from watch-1.0-SNAPSHOT.jar - 37:40"

},

"useRootPath": {},

"groupsOrder": {},

"tagsSorter": {

"value": "******",

"origin": "class path resource [application.properties] from watch-1.0-SNAPSHOT.jar - 36:34"

},

"disableSwaggerDefaultUrl": {},

"csrf": {

"enabled": {},

"useLocalStorage": {},

"useSessionStorage": {},

"cookieName": {},

"localStorageKey": {},

"sessionStorageKey": {},

"headerName": {}

},

"syntaxHighlight": {},

"version": {},

"enabled": {}

}

},

"management.endpoint.health-org.springframework.boot.actuate.autoconfigure.health.HealthEndpointProperties": {

"prefix": "management.endpoint.health",

"properties": {

"logging": {

"slowIndicatorThreshold": "******"

},

"showDetails": "******",

"status": {

"order": [],

"httpMapping": {}

},

"roles": [],

"group": {}

},

"inputs": {

"logging": {

"slowIndicatorThreshold": {}

},

"showDetails": {},

"status": {

"order": [],

"httpMapping": {}

},

"roles": [],

"group": {}

}

},

"spring.reactor.netty-org.springframework.boot.autoconfigure.reactor.netty.ReactorNettyProperties": {

"prefix": "spring.reactor.netty",

"properties": {},

"inputs": {}

},

"spring.lifecycle-org.springframework.boot.autoconfigure.context.LifecycleProperties": {

"prefix": "spring.lifecycle",

"properties": {

"timeoutPerShutdownPhase": "******"

},

"inputs": {

"timeoutPerShutdownPhase": {}

}

},

"spring.web-org.springframework.boot.autoconfigure.web.WebProperties": {

"prefix": "spring.web",

"properties": {

"localeResolver": "******",

"resources": {

"staticLocations": [

"******",

"******",

"******",

"******"

],

"addMappings": "******",

"chain": {

"cache": "******",

"compressed": "******",

"strategy": {

"fixed": {

"enabled": "******",

"paths": [

"******"

]

},

"content": {

"enabled": "******",

"paths": [

"******"

]

}

}

},

"cache": {

"cachecontrol": {},

"useLastModified": "******"

}

}

},

"inputs": {

"localeResolver": {},

"resources": {

"staticLocations": [

{},

{},

{},

{}

],

"addMappings": {},

"chain": {

"cache": {},

"compressed": {},

"strategy": {

"fixed": {

"enabled": {},

"paths": [

{}

]

},

"content": {

"enabled": {},

"paths": [

{}

]

}

}

},

"cache": {

"cachecontrol": {},

"useLastModified": {}

}

}

}

},

"management.metrics-org.springframework.boot.actuate.autoconfigure.metrics.MetricsProperties": {

"prefix": "management.metrics",

"properties": {

"system": {

"diskspace": {

"paths": [

"******"

]

}

},

"data": {

"repository": {

"metricName": "******",

"autotime": {

"enabled": "******",

"percentilesHistogram": "******"

}

}

},

"web": {

"client": {

"maxUriTags": "******"

},

"server": {

"maxUriTags": "******"

}

},

"enable": {},

"useGlobalRegistry": "******",

"distribution": {

"percentilesHistogram": {},

"percentiles": {},

"slo": {},

"minimumExpectedValue": {},

"maximumExpectedValue": {},

"expiry": {},

"bufferLength": {}

},

"tags": {}

},

"inputs": {

"system": {

"diskspace": {

"paths": [

{}

]

}

},

"data": {

"repository": {

"metricName": {},

"autotime": {

"enabled": {},

"percentilesHistogram": {}

}

}

},

"web": {

"client": {

"maxUriTags": {}

},

"server": {

"maxUriTags": {}

}

},

"enable": {},

"useGlobalRegistry": {},

"distribution": {

"percentilesHistogram": {},

"percentiles": {},

"slo": {},

"minimumExpectedValue": {},

"maximumExpectedValue": {},

"expiry": {},

"bufferLength": {}

},

"tags": {}

}

},

"spring.mvc-org.springframework.boot.autoconfigure.web.servlet.WebMvcProperties": {

"prefix": "spring.mvc",

"properties": {

"contentnegotiation": {

"favorParameter": "******",

"mediaTypes": {}

},

"servlet": {

"path": "******",

"loadOnStartup": "******"

},

"format": {},

"staticPathPattern": "******",

"dispatchOptionsRequest": "******",

"dispatchTraceRequest": "******",

"problemdetails": {

"enabled": "******"

},

"logResolvedException": "******",

"webjarsPathPattern": "******",

"async": {},

"view": {},

"publishRequestHandledEvents": "******",

"logRequestDetails": "******",

"pathmatch": {

"matchingStrategy": "******"

},

"throwExceptionIfNoHandlerFound": "******"

},

"inputs": {

"contentnegotiation": {

"favorParameter": {},

"mediaTypes": {}

},

"servlet": {

"path": {},

"loadOnStartup": {}

},

"format": {},

"staticPathPattern": {},

"dispatchOptionsRequest": {},

"dispatchTraceRequest": {},

"problemdetails": {

"enabled": {}

},

"logResolvedException": {},

"webjarsPathPattern": {},

"async": {},

"view": {},

"publishRequestHandledEvents": {},

"logRequestDetails": {},

"pathmatch": {

"matchingStrategy": {}

},

"throwExceptionIfNoHandlerFound": {}

}

},

"knife4j.setting-com.github.xiaoymin.knife4j.spring.configuration.Knife4jSetting": {

"prefix": "knife4j.setting",

"properties": {

"enableDebug": "******",

"enableOpenApi": "******",

"enableHomeCustom": "******",

"enableFooter": "******",

"enableFilterMultipartApiMethodType": "******",

"language": "******",

"enableReloadCacheParameter": "******",

"enableDynamicParameter": "******",

"enableHostText": "******",

"enableRequestCache": "******",

"enableFooterCustom": "******",

"customCode": "******",

"swaggerModelName": "******",

"enableResponseCode": "******",

"enableDocumentManage": "******",

"enableAfterScript": "******",

"enableSwaggerModels": "******",

"enableFilterMultipartApis": "******",

"enableHost": "******",

"enableVersion": "******",

"enableSearch": "******",

"enableGroup": "******"

},

"inputs": {

"enableDebug": {},

"enableOpenApi": {},

"enableHomeCustom": {},

"enableFooter": {},

"enableFilterMultipartApiMethodType": {},

"language": {

"value": "******",

"origin": "class path resource [application.properties] from watch-1.0-SNAPSHOT.jar - 33:26"

},

"enableReloadCacheParameter": {},

"enableDynamicParameter": {},

"enableHostText": {},

"enableRequestCache": {},

"enableFooterCustom": {},

"customCode": {},

"swaggerModelName": {},

"enableResponseCode": {},

"enableDocumentManage": {},

"enableAfterScript": {},

"enableSwaggerModels": {},

"enableFilterMultipartApis": {},

"enableHost": {},

"enableVersion": {},

"enableSearch": {},

"enableGroup": {}

}

},

"management.info-org.springframework.boot.actuate.autoconfigure.info.InfoContributorProperties": {

"prefix": "management.info",

"properties": {

"git": {

"mode": "******"

}

},

"inputs": {

"git": {

"mode": {}

}

}

},

"spring.netty-org.springframework.boot.autoconfigure.netty.NettyProperties": {

"prefix": "spring.netty",

"properties": {},

"inputs": {}

},

"spring.task.execution-org.springframework.boot.autoconfigure.task.TaskExecutionProperties": {

"prefix": "spring.task.execution",

"properties": {

"pool": {

"queueCapacity": "******",

"coreSize": "******",

"maxSize": "******",

"allowCoreThreadTimeout": "******",

"keepAlive": "******"

},

"simple": {},

"threadNamePrefix": "******",

"shutdown": {

"awaitTermination": "******",

"awaitTerminationPeriod": "******"

}

},

"inputs": {

"pool": {

"queueCapacity": {

"value": "******",

"origin": "class path resource [application.properties] from watch-1.0-SNAPSHOT.jar - 25:43"

},

"coreSize": {

"value": "******",

"origin": "class path resource [application.properties] from watch-1.0-SNAPSHOT.jar - 23:38"

},

"maxSize": {

"value": "******",

"origin": "class path resource [application.properties] from watch-1.0-SNAPSHOT.jar - 24:37"

},

"allowCoreThreadTimeout": {

"value": "******",

"origin": "class path resource [application.properties] from watch-1.0-SNAPSHOT.jar - 27:54"

},

"keepAlive": {

"value": "******",

"origin": "class path resource [application.properties] from watch-1.0-SNAPSHOT.jar - 26:39"

}

},

"simple": {},

"threadNamePrefix": {

"value": "******",

"origin": "class path resource [application.properties] from watch-1.0-SNAPSHOT.jar - 30:42"

},

"shutdown": {

"awaitTermination": {

"value": "******",

"origin": "class path resource [application.properties] from watch-1.0-SNAPSHOT.jar - 28:50"

},

"awaitTerminationPeriod": {

"value": "******",

"origin": "class path resource [application.properties] from watch-1.0-SNAPSHOT.jar - 29:57"

}

}

}

},

"management.server-org.springframework.boot.actuate.autoconfigure.web.server.ManagementServerProperties": {

"prefix": "management.server",

"properties": {

"basePath": "******"

},

"inputs": {

"basePath": {}

}

},

"knife4j-com.github.xiaoymin.knife4j.spring.configuration.Knife4jProperties": {

"prefix": "knife4j",

"properties": {

"cors": "******",

"production": "******",

"enable": "******",

"setting": {

"customCode": "******",

"language": "******",

"enableSwaggerModels": "******",

"swaggerModelName": "******",

"enableReloadCacheParameter": "******",

"enableAfterScript": "******",

"enableDocumentManage": "******",

"enableVersion": "******",

"enableRequestCache": "******",

"enableFilterMultipartApis": "******",

"enableFilterMultipartApiMethodType": "******",

"enableHost": "******",

"enableHostText": "******",

"enableDynamicParameter": "******",

"enableDebug": "******",

"enableFooter": "******",

"enableFooterCustom": "******",

"enableSearch": "******",

"enableOpenApi": "******",

"enableHomeCustom": "******",

"enableGroup": "******",

"enableResponseCode": "******"

}

},

"inputs": {

"cors": {},

"production": {},

"enable": {

"value": "******",

"origin": "class path resource [application.properties] from watch-1.0-SNAPSHOT.jar - 32:16"

},

"setting": {

"customCode": {},

"language": {

"value": "******",

"origin": "class path resource [application.properties] from watch-1.0-SNAPSHOT.jar - 33:26"

},

"enableSwaggerModels": {},

"swaggerModelName": {},

"enableReloadCacheParameter": {},

"enableAfterScript": {},

"enableDocumentManage": {},

"enableVersion": {},

"enableRequestCache": {},

"enableFilterMultipartApis": {},

"enableFilterMultipartApiMethodType": {},

"enableHost": {},

"enableHostText": {},

"enableDynamicParameter": {},

"enableDebug": {},

"enableFooter": {},

"enableFooterCustom": {},

"enableSearch": {},

"enableOpenApi": {},

"enableHomeCustom": {},

"enableGroup": {},

"enableResponseCode": {}

}

}

},

"spring.ssl-org.springframework.boot.autoconfigure.ssl.SslProperties": {

"prefix": "spring.ssl",

"properties": {

"bundle": {

"pem": {},

"jks": {},

"watch": {

"file": {

"quietPeriod": "******"

}

}

}

},

"inputs": {

"bundle": {

"pem": {},

"jks": {},

"watch": {

"file": {

"quietPeriod": {}

}

}

}

}

},

"spring.codec-org.springframework.boot.autoconfigure.codec.CodecProperties": {

"prefix": "spring.codec",

"properties": {

"logRequestDetails": "******"

},

"inputs": {

"logRequestDetails": {}

}

},

"management.endpoint.configprops-org.springframework.boot.actuate.autoconfigure.context.properties.ConfigurationPropertiesReportEndpointProperties": {

"prefix": "management.endpoint.configprops",

"properties": {

"showValues": "******",

"roles": []

},

"inputs": {

"showValues": {},

"roles": []

}

},

"management.simple.metrics.export-org.springframework.boot.actuate.autoconfigure.metrics.export.simple.SimpleProperties": {

"prefix": "management.simple.metrics.export",

"properties": {

"mode": "******",

"enabled": "******",

"step": "******"

},

"inputs": {

"mode": {},

"enabled": {},

"step": {}

}

},

"spring.task.scheduling-org.springframework.boot.autoconfigure.task.TaskSchedulingProperties": {

"prefix": "spring.task.scheduling",

"properties": {

"pool": {

"size": "******"

},

"simple": {},

"threadNamePrefix": "******",

"shutdown": {

"awaitTermination": "******"

}

},

"inputs": {

"pool": {

"size": {}

},

"simple": {},

"threadNamePrefix": {},

"shutdown": {

"awaitTermination": {}

}

}

},

"spring.reactor-org.springframework.boot.autoconfigure.reactor.ReactorProperties": {

"prefix": "spring.reactor",

"properties": {

"contextPropagation": "******"

},

"inputs": {

"contextPropagation": {}

}

},

"management.endpoints.web.cors-org.springframework.boot.actuate.autoconfigure.endpoint.web.CorsEndpointProperties": {

"prefix": "management.endpoints.web.cors",

"properties": {

"allowedHeaders": [],

"allowedMethods": [],

"allowedOrigins": [],

"maxAge": "******",

"exposedHeaders": [],

"allowedOriginPatterns": []

},

"inputs": {

"allowedHeaders": [],

"allowedMethods": [],

"allowedOrigins": [],

"maxAge": {},

"exposedHeaders": [],

"allowedOriginPatterns": []

}

},

"management.health.diskspace-org.springframework.boot.actuate.autoconfigure.system.DiskSpaceHealthIndicatorProperties": {

"prefix": "management.health.diskspace",

"properties": {

"path": "******",

"threshold": "******"

},

"inputs": {

"path": {},

"threshold": {}

}

},

"org.springdoc.core.properties.SwaggerUiOAuthProperties": {

"prefix": "springdoc.swagger-ui.oauth",

"properties": {

"configParameters": {}

},

"inputs": {

"configParameters": {}

}

},

"spring.sql.init-org.springframework.boot.autoconfigure.sql.init.SqlInitializationProperties": {

"prefix": "spring.sql.init",

"properties": {

"mode": "******",

"separator": "******",

"platform": "******",

"continueOnError": "******"

},

"inputs": {

"mode": {},

"separator": {},

"platform": {},

"continueOnError": {}

}

},

"management.endpoint.env-org.springframework.boot.actuate.autoconfigure.env.EnvironmentEndpointProperties": {

"prefix": "management.endpoint.env",

"properties": {

"showValues": "******",

"roles": []

},

"inputs": {

"showValues": {},

"roles": []

}

},

"management.endpoint.logfile-org.springframework.boot.actuate.autoconfigure.logging.LogFileWebEndpointProperties": {

"prefix": "management.endpoint.logfile",

"properties": {},

"inputs": {}

},

"knife4j.basic-com.github.xiaoymin.knife4j.spring.configuration.Knife4jHttpBasic": {

"prefix": "knife4j.basic",

"properties": {

"enable": "******"

},

"inputs": {

"enable": {}

}

},

"server-org.springframework.boot.autoconfigure.web.ServerProperties": {

"prefix": "server",

"properties": {

"maxHttpRequestHeaderSize": "******",

"reactive": {

"session": {

"timeout": "******"

}

},

"undertow": {

"maxHttpPostSize": "******",

"eagerFilterInit": "******",

"maxHeaders": "******",

"maxCookies": "******",

"allowEncodedSlash": "******",

"decodeUrl": "******",

"urlCharset": "******",

"alwaysSetKeepAlive": "******",

"preservePathOnForward": "******",

"accesslog": {

"enabled": "******",

"pattern": "******",

"prefix": "******",

"suffix": "******",

"dir": "******",

"rotate": "******"

},

"threads": {},

"options": {

"socket": {},

"server": {}

}

},

"port": "******",

"tomcat": {

"accesslog": {

"enabled": "******",

"pattern": "******",

"directory": "******",

"prefix": "******",

"suffix": "******",

"checkExists": "******",

"rotate": "******",

"renameOnRotate": "******",

"maxDays": "******",

"fileDateFormat": "******",

"ipv6Canonical": "******",

"requestAttributesEnabled": "******",

"buffered": "******"

},

"threads": {

"max": "******",

"minSpare": "******"

},

"backgroundProcessorDelay": "******",

"maxHttpFormPostSize": "******",

"maxSwallowSize": "******",

"redirectContextRoot": "******",

"useRelativeRedirects": "******",

"uriEncoding": "******",

"maxConnections": "******",

"acceptCount": "******",

"processorCache": "******",

"maxKeepAliveRequests": "******",

"additionalTldSkipPatterns": [],

"relaxedPathChars": [],

"relaxedQueryChars": [],

"rejectIllegalHeader": "******",

"resource": {

"allowCaching": "******"

},

"mbeanregistry": {

"enabled": "******"

},

"remoteip": {

"internalProxies": "******",

"protocolHeaderHttpsValue": "******",

"hostHeader": "******",

"portHeader": "******"

},

"maxHttpResponseHeaderSize": "******"

},

"servlet": {

"contextParameters": {},

"applicationDisplayName": "******",

"registerDefaultServlet": "******"

},

"jetty": {

"accesslog": {

"enabled": "******",

"format": "******",

"retentionPeriod": "******",

"append": "******"

},

"threads": {

"acceptors": "******",

"selectors": "******",

"max": "******",

"min": "******",

"idleTimeout": "******"

},

"maxHttpFormPostSize": "******",

"maxHttpResponseHeaderSize": "******",

"maxConnections": "******"

},

"error": {

"path": "******",

"includeException": "******",

"includeStacktrace": "******",

"includeMessage": "******",

"includeBindingErrors": "******",

"whitelabel": {

"enabled": "******"

}

},

"shutdown": "******",

"netty": {

"h2cMaxContentLength": "******",

"initialBufferSize": "******",

"maxInitialLineLength": "******",

"validateHeaders": "******"

}

},

"inputs": {

"maxHttpRequestHeaderSize": {},

"reactive": {

"session": {

"timeout": {}

}

},

"undertow": {

"maxHttpPostSize": {},

"eagerFilterInit": {},

"maxHeaders": {},

"maxCookies": {},

"allowEncodedSlash": {},

"decodeUrl": {},

"urlCharset": {},

"alwaysSetKeepAlive": {},

"preservePathOnForward": {},

"accesslog": {

"enabled": {},

"pattern": {},

"prefix": {},

"suffix": {},

"dir": {},

"rotate": {}

},

"threads": {},

"options": {

"socket": {},

"server": {}

}

},

"port": {

"value": "******",

"origin": "\"server.port\" from property source \"commandLineArgs\""

},

"tomcat": {

"accesslog": {

"enabled": {},

"pattern": {},

"directory": {},

"prefix": {},

"suffix": {},

"checkExists": {},

"rotate": {},

"renameOnRotate": {},

"maxDays": {},

"fileDateFormat": {},

"ipv6Canonical": {},

"requestAttributesEnabled": {},

"buffered": {}

},

"threads": {

"max": {},

"minSpare": {}

},

"backgroundProcessorDelay": {},

"maxHttpFormPostSize": {},

"maxSwallowSize": {},

"redirectContextRoot": {},

"useRelativeRedirects": {},

"uriEncoding": {},

"maxConnections": {

"value": "******",

"origin": "class path resource [application.properties] from watch-1.0-SNAPSHOT.jar - 13:31"

},

"acceptCount": {

"value": "******",

"origin": "class path resource [application.properties] from watch-1.0-SNAPSHOT.jar - 15:28"

},

"processorCache": {},

"maxKeepAliveRequests": {},

"additionalTldSkipPatterns": [],

"relaxedPathChars": [],

"relaxedQueryChars": [],

"rejectIllegalHeader": {},

"resource": {

"allowCaching": {}

},

"mbeanregistry": {

"enabled": {}

},

"remoteip": {

"internalProxies": {},

"protocolHeaderHttpsValue": {},

"hostHeader": {},

"portHeader": {}

},

"maxHttpResponseHeaderSize": {}

},

"servlet": {

"contextParameters": {},

"applicationDisplayName": {},

"registerDefaultServlet": {}

},

"jetty": {

"accesslog": {

"enabled": {},

"format": {},

"retentionPeriod": {},

"append": {}

},

"threads": {

"acceptors": {},

"selectors": {},

"max": {},

"min": {},

"idleTimeout": {}

},

"maxHttpFormPostSize": {},

"maxHttpResponseHeaderSize": {},

"maxConnections": {}

},

"error": {

"path": {},

"includeException": {},

"includeStacktrace": {},

"includeMessage": {},

"includeBindingErrors": {},

"whitelabel": {

"enabled": {}

}

},

"shutdown": {},

"netty": {

"h2cMaxContentLength": {},

"initialBufferSize": {},

"maxInitialLineLength": {},

"validateHeaders": {}

}

}

},

"spring.servlet.multipart-org.springframework.boot.autoconfigure.web.servlet.MultipartProperties": {

"prefix": "spring.servlet.multipart",

"properties": {

"fileSizeThreshold": "******",

"maxFileSize": "******",

"maxRequestSize": "******",

"strictServletCompliance": "******",

"enabled": "******",

"resolveLazily": "******"

},

"inputs": {

"fileSizeThreshold": {},

"maxFileSize": {

"value": "******",

"origin": "class path resource [application.properties] from watch-1.0-SNAPSHOT.jar - 12:40"

},

"maxRequestSize": {

"value": "******",

"origin": "class path resource [application.properties] from watch-1.0-SNAPSHOT.jar - 11:43"

},

"strictServletCompliance": {},

"enabled": {},

"resolveLazily": {}

}

},

"dataSource": {

"prefix": "spring.datasource.hikari",

"properties": {

"validationTimeout": "******",

"hikariPoolMXBean": {},

"minimumIdle": "******",

"password": "******",

"metricsTrackerFactory": {},

"dataSourceProperties": {},

"loginTimeout": "******",

"autoCommit": "******",

"connectionTimeout": "******",

"poolName": "******",

"initializationFailTimeout": "******",

"readOnly": "******",

"registerMbeans": "******",

"healthCheckProperties": {},

"isolateInternalQueries": "******",

"leakDetectionThreshold": "******",

"maxLifetime": "******",

"keepaliveTime": "******",

"allowPoolSuspension": "******",

"idleTimeout": "******",

"connectionTestQuery": "******",

"driverClassName": "******",

"jdbcUrl": "******",

"maximumPoolSize": "******",

"username": "******"

},

"inputs": {

"validationTimeout": {},

"hikariPoolMXBean": {},

"minimumIdle": {

"value": "******",

"origin": "class path resource [application.properties] from watch-1.0-SNAPSHOT.jar - 48:39"

},

"password": {},

"metricsTrackerFactory": {},

"dataSourceProperties": {},

"loginTimeout": {},

"autoCommit": {

"value": "******",

"origin": "class path resource [application.properties] from watch-1.0-SNAPSHOT.jar - 50:38"

},

"connectionTimeout": {

"value": "******",

"origin": "class path resource [application.properties] from watch-1.0-SNAPSHOT.jar - 53:45"

},

"poolName": {},

"initializationFailTimeout": {},

"readOnly": {},

"registerMbeans": {},

"healthCheckProperties": {},

"isolateInternalQueries": {},

"leakDetectionThreshold": {},

"maxLifetime": {

"value": "******",

"origin": "class path resource [application.properties] from watch-1.0-SNAPSHOT.jar - 52:39"

},

"keepaliveTime": {},

"allowPoolSuspension": {},

"idleTimeout": {

"value": "******",

"origin": "class path resource [application.properties] from watch-1.0-SNAPSHOT.jar - 51:39"

},

"connectionTestQuery": {

"value": "******",

"origin": "class path resource [application.properties] from watch-1.0-SNAPSHOT.jar - 54:48"

},

"driverClassName": {},

"jdbcUrl": {},

"maximumPoolSize": {

"value": "******",

"origin": "class path resource [application.properties] from watch-1.0-SNAPSHOT.jar - 49:44"

},

"username": {}

}

}

}

}

}

}

10. env 环境变量

neo@MacBook-Pro-M2 ~ % curl -s http://www.netkiller.cn:8080/actuator/env |jq

{

"activeProfiles": [

"prod"

],

"propertySources": [

{

"name": "server.ports",

"properties": {

"local.server.port": {

"value": "******"

}

}

},

{

"name": "commandLineArgs",

"properties": {

"logging.file.name": {

"value": "******"

},

"server.port": {

"value": "******"

},

"spring.profiles.active": {

"value": "******"

}

}

},

{

"name": "servletContextInitParams",

"properties": {}

},

{

"name": "systemProperties",

"properties": {

"java.specification.version": {

"value": "******"

},

"sun.jnu.encoding": {

"value": "******"

},

"java.class.path": {

"value": "******"

},

"java.vm.vendor": {

"value": "******"

},

"sun.arch.data.model": {

"value": "******"

},

"java.vendor.url": {

"value": "******"

},

"catalina.useNaming": {

"value": "******"

},

"user.timezone": {

"value": "******"

},

"org.jboss.logging.provider": {

"value": "******"

},

"java.vm.specification.version": {

"value": "******"

},

"os.name": {

"value": "******"

},

"sun.java.launcher": {

"value": "******"

},

"sun.boot.library.path": {

"value": "******"

},

"sun.java.command": {

"value": "******"

},

"jdk.debug": {

"value": "******"

},

"sun.cpu.endian": {

"value": "******"

},

"user.home": {

"value": "******"

},

"user.language": {

"value": "******"

},

"java.specification.vendor": {

"value": "******"

},

"java.version.date": {

"value": "******"

},

"java.home": {

"value": "******"

},

"file.separator": {

"value": "******"

},

"java.vm.compressedOopsMode": {

"value": "******"

},

"line.separator": {

"value": "******"

},

"java.specification.name": {

"value": "******"

},

"java.vm.specification.vendor": {

"value": "******"

},

"FILE_LOG_CHARSET": {

"value": "******"

},

"java.awt.headless": {

"value": "******"

},

"java.protocol.handler.pkgs": {

"value": "******"

},

"sun.management.compiler": {

"value": "******"

},

"java.runtime.version": {

"value": "******"

},

"user.name": {

"value": "******"

},

"stdout.encoding": {

"value": "******"

},

"path.separator": {

"value": "******"

},

"os.version": {

"value": "******"

},

"java.runtime.name": {

"value": "******"

},

"file.encoding": {

"value": "******"

},

"java.vm.name": {

"value": "******"

},

"LOG_FILE": {

"value": "******"

},

"java.vendor.url.bug": {

"value": "******"

},

"java.io.tmpdir": {

"value": "******"

},

"catalina.home": {

"value": "******"

},

"com.zaxxer.hikari.pool_number": {

"value": "******"

},

"java.version": {

"value": "******"

},

"user.dir": {

"value": "******"

},

"os.arch": {

"value": "******"

},

"java.vm.specification.name": {

"value": "******"

},

"PID": {

"value": "******"

},

"CONSOLE_LOG_CHARSET": {

"value": "******"

},

"catalina.base": {

"value": "******"

},

"native.encoding": {

"value": "******"

},

"java.library.path": {

"value": "******"

},

"stderr.encoding": {

"value": "******"

},

"java.vm.info": {

"value": "******"

},

"java.vendor": {

"value": "******"

},

"java.vm.version": {

"value": "******"

},

"sun.io.unicode.encoding": {

"value": "******"

},

"java.class.version": {

"value": "******"

},

"LOGGED_APPLICATION_NAME": {

"value": "******"

}

}

},

{

"name": "systemEnvironment",

"properties": {

"HOME": {

"value": "******",

"origin": "System Environment Property \"HOME\""

},

"PATH": {

"value": "******",

"origin": "System Environment Property \"PATH\""

},

"JAVA_VERSION": {

"value": "******",

"origin": "System Environment Property \"JAVA_VERSION\""

},

"JAVA_HOME": {

"value": "******",

"origin": "System Environment Property \"JAVA_HOME\""

},

"TZ": {

"value": "******",

"origin": "System Environment Property \"TZ\""

},

"JAVA_OPTS": {

"value": "******",

"origin": "System Environment Property \"JAVA_OPTS\""

},

"LANG": {

"value": "******",

"origin": "System Environment Property \"LANG\""

},

"HOSTNAME": {

"value": "******",

"origin": "System Environment Property \"HOSTNAME\""

}

}

},

{

"name": "Config resource 'class path resource [application-prod.properties]' via location 'optional:classpath:/'",

"properties": {

"spring.application.name": {

"value": "******",

"origin": "class path resource [application-prod.properties] from watch-1.0-SNAPSHOT.jar - 1:25"

},

"spring.datasource.driver-class-name": {

"value": "******",

"origin": "class path resource [application-prod.properties] from watch-1.0-SNAPSHOT.jar - 3:37"

},

"spring.datasource.url": {

"value": "******",

"origin": "class path resource [application-prod.properties] from watch-1.0-SNAPSHOT.jar - 4:23"

},

"spring.datasource.username": {

"value": "******",

"origin": "class path resource [application-prod.properties] from watch-1.0-SNAPSHOT.jar - 5:28"

},

"spring.datasource.password": {

"value": "******",

"origin": "class path resource [application-prod.properties] from watch-1.0-SNAPSHOT.jar - 6:28"

},

"spring.data.redis.host": {

"value": "******",

"origin": "class path resource [application-prod.properties] from watch-1.0-SNAPSHOT.jar - 8:24"

},

"spring.data.redis.port": {

"value": "******",

"origin": "class path resource [application-prod.properties] from watch-1.0-SNAPSHOT.jar - 9:24"

},

"spring.data.redis.password": {

"value": "******",

"origin": "class path resource [application-prod.properties] from watch-1.0-SNAPSHOT.jar - 10:28"

},

"spring.data.redis.database": {

"value": "******",

"origin": "class path resource [application-prod.properties] from watch-1.0-SNAPSHOT.jar - 11:28"

},

"spring.data.redis.timeout": {

"value": "******",

"origin": "class path resource [application-prod.properties] from watch-1.0-SNAPSHOT.jar - 12:27"

},

"aliyun.oss.access_key_id": {

"value": "******",

"origin": "class path resource [application-prod.properties] from watch-1.0-SNAPSHOT.jar - 14:26"

},

"aliyun.oss.secret_access_key": {

"value": "******",

"origin": "class path resource [application-prod.properties] from watch-1.0-SNAPSHOT.jar - 15:30"

},

"aliyun.oss.endpoint": {

"value": "******",

"origin": "class path resource [application-prod.properties] from watch-1.0-SNAPSHOT.jar - 16:21"

},

"aliyun.oss.bucket_name": {

"value": "******",

"origin": "class path resource [application-prod.properties] from watch-1.0-SNAPSHOT.jar - 17:24"

},

"aliyun.oss.callback_url": {

"value": "******",

"origin": "class path resource [application-prod.properties] from watch-1.0-SNAPSHOT.jar - 18:25"

},

"mqtt.broker": {

"value": "******",

"origin": "class path resource [application-prod.properties] from watch-1.0-SNAPSHOT.jar - 20:13"

},

"mqtt.username": {

"value": "******",

"origin": "class path resource [application-prod.properties] from watch-1.0-SNAPSHOT.jar - 21:15"

},

"mqtt.password": {

"value": "******",

"origin": "class path resource [application-prod.properties] from watch-1.0-SNAPSHOT.jar - 22:15"

},

"mqtt.topic.prefix": {

"value": "******",

"origin": "class path resource [application-prod.properties] from watch-1.0-SNAPSHOT.jar - 23:19"

},

"baidu.url": {

"value": "******",

"origin": "class path resource [application-prod.properties] from watch-1.0-SNAPSHOT.jar - 25:11"

},

"baidu.appid": {

"value": "******",

"origin": "class path resource [application-prod.properties] from watch-1.0-SNAPSHOT.jar - 26:13"

},

"baidu.apikey": {

"value": "******",

"origin": "class path resource [application-prod.properties] from watch-1.0-SNAPSHOT.jar - 27:14"

},

"baidu.secretkey": {

"value": "******",

"origin": "class path resource [application-prod.properties] from watch-1.0-SNAPSHOT.jar - 28:17"

},

"baidu.appsecret": {

"value": "******",

"origin": "class path resource [application-prod.properties] from watch-1.0-SNAPSHOT.jar - 29:17"

},

"baidu.censor.appid": {

"value": "******",

"origin": "class path resource [application-prod.properties] from watch-1.0-SNAPSHOT.jar - 30:20"

},

"baidu.censor.appsecret": {

"value": "******",

"origin": "class path resource [application-prod.properties] from watch-1.0-SNAPSHOT.jar - 31:24"

},

"xfyun.appid": {

"value": "******",

"origin": "class path resource [application-prod.properties] from watch-1.0-SNAPSHOT.jar - 33:13"

},

"xfyun.stt.apikey": {

"value": "******",

"origin": "class path resource [application-prod.properties] from watch-1.0-SNAPSHOT.jar - 34:18"

},

"xfyun.stt.length": {

"value": "******",

"origin": "class path resource [application-prod.properties] from watch-1.0-SNAPSHOT.jar - 35:18"

},

"xfyun.tts.apikey": {

"value": "******",

"origin": "class path resource [application-prod.properties] from watch-1.0-SNAPSHOT.jar - 36:18"

},

"xfyun.tts.apisecret": {

"value": "******",

"origin": "class path resource [application-prod.properties] from watch-1.0-SNAPSHOT.jar - 37:21"

},

"xfyun.tts.vcn": {

"value": "******",

"origin": "class path resource [application-prod.properties] from watch-1.0-SNAPSHOT.jar - 42:15"

},

"xfyun.tts.pitch": {

"value": "******",

"origin": "class path resource [application-prod.properties] from watch-1.0-SNAPSHOT.jar - 43:17"

},

"xfyun.tts.speed": {

"value": "******",

"origin": "class path resource [application-prod.properties] from watch-1.0-SNAPSHOT.jar - 44:17"

}

}

},

{

"name": "Config resource 'class path resource [application.properties]' via location 'optional:classpath:/'",

"properties": {

"spring.application.name": {

"value": "******",

"origin": "class path resource [application.properties] from watch-1.0-SNAPSHOT.jar - 2:25"

},

"spring.profiles.active": {

"value": "******",

"origin": "class path resource [application.properties] from watch-1.0-SNAPSHOT.jar - 3:24"

},

"server.port": {

"value": "******",

"origin": "class path resource [application.properties] from watch-1.0-SNAPSHOT.jar - 10:13"

},

"spring.servlet.multipart.max-request-size": {

"value": "******",

"origin": "class path resource [application.properties] from watch-1.0-SNAPSHOT.jar - 11:43"

},

"spring.servlet.multipart.max-file-size": {

"value": "******",

"origin": "class path resource [application.properties] from watch-1.0-SNAPSHOT.jar - 12:40"

},

"server.tomcat.max-connections": {

"value": "******",

"origin": "class path resource [application.properties] from watch-1.0-SNAPSHOT.jar - 13:31"

},

"server.tomcat.max-threads": {

"value": "******",

"origin": "class path resource [application.properties] from watch-1.0-SNAPSHOT.jar - 14:27"

},

"server.tomcat.accept-count": {

"value": "******",

"origin": "class path resource [application.properties] from watch-1.0-SNAPSHOT.jar - 15:28"

},

"server.tomcat.min-spare-threads": {

"value": "******",

"origin": "class path resource [application.properties] from watch-1.0-SNAPSHOT.jar - 16:33"

},

"endpoints.metrics.enabled": {

"value": "******",

"origin": "class path resource [application.properties] from watch-1.0-SNAPSHOT.jar - 18:27"

},

"management.endpoints.jmx.exposure.include": {

"value": "******",

"origin": "class path resource [application.properties] from watch-1.0-SNAPSHOT.jar - 19:43"

},

"management.endpoints.web.exposure.include": {

"value": "******",

"origin": "class path resource [application.properties] from watch-1.0-SNAPSHOT.jar - 20:43"

},

"management.endpoints.health.show-details": {

"value": "******",

"origin": "class path resource [application.properties] from watch-1.0-SNAPSHOT.jar - 21:42"

},

"spring.task.execution.pool.core-size": {

"value": "******",

"origin": "class path resource [application.properties] from watch-1.0-SNAPSHOT.jar - 23:38"

},

"spring.task.execution.pool.max-size": {

"value": "******",

"origin": "class path resource [application.properties] from watch-1.0-SNAPSHOT.jar - 24:37"

},

"spring.task.execution.pool.queue-capacity": {

"value": "******",

"origin": "class path resource [application.properties] from watch-1.0-SNAPSHOT.jar - 25:43"

},

"spring.task.execution.pool.keep-alive": {

"value": "******",

"origin": "class path resource [application.properties] from watch-1.0-SNAPSHOT.jar - 26:39"

},

"spring.task.execution.pool.allow-core-thread-timeout": {

"value": "******",

"origin": "class path resource [application.properties] from watch-1.0-SNAPSHOT.jar - 27:54"

},

"spring.task.execution.shutdown.await-termination": {

"value": "******",

"origin": "class path resource [application.properties] from watch-1.0-SNAPSHOT.jar - 28:50"

},

"spring.task.execution.shutdown.await-termination-period": {

"value": "******",

"origin": "class path resource [application.properties] from watch-1.0-SNAPSHOT.jar - 29:57"

},

"spring.task.execution.thread-name-prefix": {

"value": "******",

"origin": "class path resource [application.properties] from watch-1.0-SNAPSHOT.jar - 30:42"

},

"knife4j.enable": {

"value": "******",

"origin": "class path resource [application.properties] from watch-1.0-SNAPSHOT.jar - 32:16"

},

"knife4j.setting.language": {

"value": "******",

"origin": "class path resource [application.properties] from watch-1.0-SNAPSHOT.jar - 33:26"

},

"springdoc.swagger-ui.path": {

"value": "******",

"origin": "class path resource [application.properties] from watch-1.0-SNAPSHOT.jar - 35:27"

},

"springdoc.swagger-ui.tags-sorter": {

"value": "******",

"origin": "class path resource [application.properties] from watch-1.0-SNAPSHOT.jar - 36:34"

},

"springdoc.swagger-ui.operations-sorter": {

"value": "******",

"origin": "class path resource [application.properties] from watch-1.0-SNAPSHOT.jar - 37:40"

},

"springdoc.api-docs.path": {

"value": "******",

"origin": "class path resource [application.properties] from watch-1.0-SNAPSHOT.jar - 38:25"

},

"spring.jpa.open-in-view": {

"value": "******",

"origin": "class path resource [application.properties] from watch-1.0-SNAPSHOT.jar - 41:25"

},

"spring.jpa.hibernate.ddl-auto": {

"value": "******",

"origin": "class path resource [application.properties] from watch-1.0-SNAPSHOT.jar - 43:31"

},

"spring.datasource.driver-class-name": {

"value": "******",

"origin": "class path resource [application.properties] from watch-1.0-SNAPSHOT.jar - 45:37"

},

"spring.datasource.hikari.minimum-idle": {

"value": "******",

"origin": "class path resource [application.properties] from watch-1.0-SNAPSHOT.jar - 48:39"

},

"spring.datasource.hikari.maximum-pool-size": {

"value": "******",

"origin": "class path resource [application.properties] from watch-1.0-SNAPSHOT.jar - 49:44"

},

"spring.datasource.hikari.auto-commit": {

"value": "******",

"origin": "class path resource [application.properties] from watch-1.0-SNAPSHOT.jar - 50:38"

},

"spring.datasource.hikari.idle-timeout": {

"value": "******",

"origin": "class path resource [application.properties] from watch-1.0-SNAPSHOT.jar - 51:39"

},

"spring.datasource.hikari.max-lifetime": {

"value": "******",

"origin": "class path resource [application.properties] from watch-1.0-SNAPSHOT.jar - 52:39"

},

"spring.datasource.hikari.connection-timeout": {

"value": "******",

"origin": "class path resource [application.properties] from watch-1.0-SNAPSHOT.jar - 53:45"

},

"spring.datasource.hikari.connection-test-query": {

"value": "******",

"origin": "class path resource [application.properties] from watch-1.0-SNAPSHOT.jar - 54:48"

},

"spring.data.redis.lettuce.pool.max-active": {

"value": "******",

"origin": "class path resource [application.properties] from watch-1.0-SNAPSHOT.jar - 56:43"

},

"spring.data.redis.lettuce.pool.max-wait": {

"value": "******",

"origin": "class path resource [application.properties] from watch-1.0-SNAPSHOT.jar - 57:41"

},

"spring.data.redis.lettuce.pool.max-idle": {

"value": "******",

"origin": "class path resource [application.properties] from watch-1.0-SNAPSHOT.jar - 58:41"

},

"spring.data.redis.lettuce.pool.min-idle": {

"value": "******",

"origin": "class path resource [application.properties] from watch-1.0-SNAPSHOT.jar - 59:41"

},

"spring.cache.type": {

"value": "******",

"origin": "class path resource [application.properties] from watch-1.0-SNAPSHOT.jar - 60:19"

},

"spring.cache.redis.time-to-live": {

"value": "******",

"origin": "class path resource [application.properties] from watch-1.0-SNAPSHOT.jar - 61:33"

},

"spring.cache.redis.cache-null-values": {

"value": "******",

"origin": "class path resource [application.properties] from watch-1.0-SNAPSHOT.jar - 62:38"

},

"stablediffusion.url": {

"value": "******",

"origin": "class path resource [application.properties] from watch-1.0-SNAPSHOT.jar - 64:21"

},

"stablediffusion.negative_prompt": {

"value": "******",

"origin": "class path resource [application.properties] from watch-1.0-SNAPSHOT.jar - 66:0"

},

"stablediffusion.sampler_index": {

"value": "******",

"origin": "class path resource [application.properties] from watch-1.0-SNAPSHOT.jar - 66:31"

},

"stablediffusion.seed": {

"value": "******",

"origin": "class path resource [application.properties] from watch-1.0-SNAPSHOT.jar - 67:22"

},

"stablediffusion.steps": {

"value": "******",

"origin": "class path resource [application.properties] from watch-1.0-SNAPSHOT.jar - 68:23"

},

"stablediffusion.width": {

"value": "******",

"origin": "class path resource [application.properties] from watch-1.0-SNAPSHOT.jar - 69:23"

},

"stablediffusion.height": {

"value": "******",

"origin": "class path resource [application.properties] from watch-1.0-SNAPSHOT.jar - 70:24"

},

"stablediffusion.cfg_scale": {

"value": "******",

"origin": "class path resource [application.properties] from watch-1.0-SNAPSHOT.jar - 71:27"

},

"chatgpt.url": {

"value": "******",

"origin": "class path resource [application.properties] from watch-1.0-SNAPSHOT.jar - 73:13"

},

"chatgpt.username": {

"value": "******",

"origin": "class path resource [application.properties] from watch-1.0-SNAPSHOT.jar - 74:18"

},

"chatgpt.password": {

"value": "******",

"origin": "class path resource [application.properties] from watch-1.0-SNAPSHOT.jar - 75:18"

}

}

}

]

}

11. logfile 日志

neo@MacBook-Pro-M2 ~ % curl -s http://www.netkiller.cn:8080/actuator/logfile | tail -n 5

2024-01-01T10:26:57.967+08:00  INFO 1 --- [watch-production] [http-nio-8080-exec-3] c.netkiller.service.SessionStatusService   : SessionStatus(id=15476, session=06c27cc8-dbc6-4386-bd69-e555fa3715cf, progress=5.创建会话, description=画一张猫, ctime=null)

2024-01-01T10:26:58.091+08:00  INFO 1 --- [watch-production] [http-nio-8080-exec-3] cn.netkiller.service.BaiduService          : {"refresh_token":"25.3fe5248bc2006526a59a10973eaa4320.315360000.2019436018.282335-45847061","expires_in":2592000,"session_key":"9mzdA5O3tx06PF7hFuzwL5FsbQDWq0LxF4mpdWWl5josf1ZE0IvZOXsrDS471dAz8F6aq7YbivAAZ7WsrliSyZ2Fwtr2iA==","access_token":"24.89b1691bb1d8e1437671365521b43a2d.2592000.1706668018.282335-45847061","scope":"public brain_all_scope brain_nlp_sentiment_classify brain_nlp_emotion solution_face brain_mt_texttrans wise_adapt lebo_resource_base lightservice_public hetu_basic lightcms_map_poi kaidian_kaidian ApsMisTest_Test\u6743\u9650 vis-classify_flower lpq_\u5f00\u653e cop_helloScope ApsMis_fangdi_permission smartapp_snsapi_base smartapp_mapp_dev_manage iop_autocar oauth_tp_app smartapp_smart_game_openapi oauth_sessionkey smartapp_swanid_verify smartapp_opensource_openapi smartapp_opensource_recapi fake_face_detect_\u5f00\u653eScope vis-ocr_\u865a\u62df\u4eba\u7269\u52a9\u7406 idl-video_\u865a\u62df\u4eba\u7269\u52a9\u7406 smartapp_component smartapp_search_plugin avatar_video_test b2b_tp_openapi b2b_tp_openapi_online smartapp_gov_aladin_to_xcx","session_secret":"0bca5fb59addf566eb2e9038afb09883"}

2024-01-01T10:26:58.468+08:00  INFO 1 --- [watch-production] [http-nio-8080-exec-3] cn.netkiller.service.BaiduService          : Translate: {"result":{"from":"zh","trans_result":[{"dst":"Draw a cat","src":"画一张猫"}],"to":"en"},"log_id":1741647171642597969}

2024-01-01T10:26:58.499+08:00  INFO 1 --- [watch-production] [http-nio-8080-exec-3] c.netkiller.service.SessionStatusService   : SessionStatus(id=15477, session=06c27cc8-dbc6-4386-bd69-e555fa3715cf, progress=6.翻译成功, description=Draw a cat, ctime=null)

neo@MacBook-Pro-M2 ~ % curl -s http://www.netkiller.cn:8080/actuator/loggers/cn.netkiller.component

{"effectiveLevel":"INFO"}%

neo@MacBook-Pro-M2 ~ % curl -s http://www.netkiller.cn:8080/actuator/loggers | tail -n 5

{"levels":["OFF","ERROR","WARN","INFO","DEBUG","TRACE"],"loggers":{"ROOT":{"configuredLevel":"INFO","effectiveLevel":"INFO"},"_org":{"effectiveLevel":"INFO"},"_org.springframework":{"effectiveLevel":"INFO"},"_org.springframework.web":{"effectiveLevel":"INFO"},"_org.springframework.web.servlet":{"effectiveLevel":"INFO"},"_org.springframework.web.servlet.HandlerMapping":{"effectiveLevel":"INFO"},"_org.springframework.web.servlet.HandlerMapping.Mappings":{"effectiveLevel":"INFO"},"cn":{"effectiveLevel":"INFO"},"cn.netkiller":{"effectiveLevel":"INFO"},"cn.netkiller.Application":{"effectiveLevel":"INFO"},"cn.netkiller.ai":{"effectiveLevel":"INFO"},"cn.netkiller.ai.xfyun":{"effectiveLevel":"INFO"},"cn.netkiller.ai.xfyun.SpeechRecognizerService":{"effectiveLevel":"INFO"},"cn.netkiller.ai.xfyun.SpeechSynthesizerService":{"effectiveLevel":"INFO"},"cn.netkiller.component":{"effectiveLevel":"INFO"},"cn.netkiller.component.AudioService":

12. threaddump 线程信息

neo@MacBook-Pro-M2 ~ % curl -s http://www.netkiller.cn:8080/actuator/threaddump | jq | grep threadName

"threadName": "Reference Handler",

"threadName": "Finalizer",

"threadName": "Signal Dispatcher",

"threadName": "Notification Thread",

"threadName": "Common-Cleaner",

"threadName": "Cleaner-0",

"threadName": "Catalina-utility-1",

"threadName": "Catalina-utility-2",

"threadName": "container-0",

"threadName": "lettuce-timer-3-1",

"threadName": "mysql-cj-abandoned-connection-cleanup",

"threadName": "HikariPool-1 housekeeper",

"threadName": "http-nio-8080-exec-1",

"threadName": "http-nio-8080-exec-2",

"threadName": "http-nio-8080-exec-3",

"threadName": "http-nio-8080-exec-4",

"threadName": "http-nio-8080-exec-5",

"threadName": "http-nio-8080-exec-7",

"threadName": "http-nio-8080-exec-8",

"threadName": "http-nio-8080-exec-10",

"threadName": "http-nio-8080-Poller",

"threadName": "http-nio-8080-Acceptor",

"threadName": "DestroyJavaVM",

"threadName": "lettuce-epollEventLoop-4-1",

"threadName": "lettuce-eventExecutorLoop-1-1",

"threadName": "lettuce-eventExecutorLoop-1-2",

"threadName": "lettuce-eventExecutorLoop-1-3",

"threadName": "lettuce-eventExecutorLoop-1-4",

"threadName": "task-11",

"threadName": "task-12",

"threadName": "task-13",

"threadName": "task-14",

"threadName": "task-15",

"threadName": "task-16",

"threadName": "task-17",

"threadName": "task-18",

"threadName": "task-19",

"threadName": "task-20",

"threadName": "MQTT Rec: netkiller-1704016635545",

"threadName": "MQTT Snd: netkiller-1704016635545",

"threadName": "MQTT Call: netkiller-1704016635545",

"threadName": "MQTT Ping: netkiller-1704016635545",

"threadName": "http-nio-8080-exec-14",

"threadName": "http-nio-8080-exec-16",

"threadName": "boundedElastic-evictor-1",

"threadName": "lettuce-epollEventLoop-4-2",

13. 计划任务

http://www.netkiller.cn:8080/actuator/scheduledtasks

14. metrics

neo@MacBook-Pro-Neo ~/w/Architect (master)> curl -s https://www.netkiller.cn/actuator/metrics/ | jq

{

"names": [

"jvm.threads.states",

"process.files.max",

"jvm.gc.memory.promoted",

"hikaricp.connections.max",

"hikaricp.connections.min",

"jvm.memory.committed",

"system.load.average.1m",

"http.server.requests",

"jvm.memory.used",

"jvm.gc.max.data.size",

"jdbc.connections.max",

"jdbc.connections.min",

"hikaricp.connections.usage",

"jvm.gc.pause",

"system.cpu.count",

"hikaricp.connections.timeout",

"tomcat.global.sent",

"jvm.buffer.memory.used",

"tomcat.sessions.created",

"jvm.memory.max",

"jvm.threads.daemon",

"hikaricp.connections.acquire",

"system.cpu.usage",

"jvm.gc.memory.allocated",

"tomcat.global.request.max",

"tomcat.global.request",

"tomcat.sessions.expired",

"jvm.threads.live",

"jvm.threads.peak",

"tomcat.global.received",

"process.uptime",

"tomcat.sessions.rejected",

"process.cpu.usage",

"tomcat.threads.config.max",

"jvm.classes.loaded",

"jvm.classes.unloaded",

"tomcat.global.error",

"tomcat.sessions.active.current",

"tomcat.sessions.alive.max",

"jvm.gc.live.data.size",

"log4j2.events",

"hikaricp.connections.idle",

"tomcat.threads.current",

"hikaricp.connections.pending",

"process.files.open",

"jvm.buffer.count",

"hikaricp.connections",

"jvm.buffer.total.capacity",

"tomcat.sessions.active.max",

"hikaricp.connections.active",

"hikaricp.connections.creation",

"tomcat.threads.busy",

"process.start.time"

]

}

neo@MacBook-Pro-Neo ~/w/Architect (master)> curl -s https://www.netkiller.cn/actuator/metrics/tomcat.threads.config.max |jq

{

"name": "tomcat.threads.config.max",

"description": null,

"baseUnit": "threads",

"measurements": [

{

"statistic": "VALUE",

"value": 4096

}

],

"availableTags": [

{

"tag": "name",

"values": [

"http-nio-8080"

]

}

]

}

neo@MacBook-Pro-Neo ~/w/Architect (master)> curl -s https://www.netkiller.cn/actuator/metrics/tomcat.threads.current |jq

{

"name": "tomcat.threads.current",

"description": null,

"baseUnit": "threads",

"measurements": [

{

"statistic": "VALUE",

"value": 24

}

],

"availableTags": [

{

"tag": "name",

"values": [

"http-nio-8080"

]

}

]

}

15. 控制器映射 URL

neo@MacBook-Pro-M2 ~ % curl -s http://www.netkiller.cn:8080/actuator/mappings |jq

16. 自定义监控指标

package cn.netkiller.config;

import org.springframework.boot.actuate.endpoint.annotation.Endpoint;

import org.springframework.boot.actuate.endpoint.annotation.ReadOperation;

import org.springframework.context.annotation.Configuration;

import java.util.*;

@Configuration

@Endpoint(id = "netkiller")

public class TestEndpoint {

@ReadOperation

public Map<String, Object> threadPoolsMetric() {

Map<String, Object> metricMap = new HashMap<>();

List<Map> threadPools = new ArrayList<>();

Map<String, Object> poolInfo = new HashMap<>();

poolInfo.put("thread.pool.name", "netkiller");

poolInfo.put("thread.pool.core.size", 100);

poolInfo.put("thread.pool.time", new Date());

threadPools.add(poolInfo);

metricMap.put("netkiller", threadPools);

return metricMap;

}

}

验证

neo@MacBook-Pro-M2 ~> curl -s http://www.netkiller.cn:8080/actuator/netkiller | jq

{

"netkiller": [

{

"thread.pool.time": "2023-04-24T09:08:14.407+00:00",

"thread.pool.core.size": 100,

"thread.pool.name": "netkiller"

}

]

}

第 31 章 String boot with RestTemplate

RestTemplate - Spring Restful

RestTemplate 是 Spring Restful Client 用于调用restful接口

首先我要禁告各位，Spring发展过程中，每个版本都有一定差异。如果你做实验失败后在网上搜索答案，切记看一下版本号还有文章帖子的发布时间。否则你可能按照Spring3配置方法去Spring4。

@RestController 默认返回 @ResponseBody， 所以@ResponseBody可加可不加

1. RestTemplate Example

pom.xml

Maven 增加 jackson 开发包

<dependency>

<groupId>com.fasterxml.jackson.dataformat</groupId>

<artifactId>jackson-dataformat-xml</artifactId>

</dependency>

<dependency>

<groupId>com.fasterxml.jackson.core</groupId>

<artifactId>jackson-core</artifactId>

</dependency>

<dependency>

<groupId>com.fasterxml.jackson.core</groupId>

<artifactId>jackson-databind</artifactId>

</dependency>

<dependency>

<groupId>com.fasterxml.jackson.core</groupId>

<artifactId>jackson-annotations</artifactId>

</dependency>

web.xml

url-pattern匹配中增加*.xml跟*.json

<servlet>

<servlet-name>springframework</servlet-name>

<servlet-class>

org.springframework.web.servlet.DispatcherServlet

</servlet-class>

<load-on-startup>1</load-on-startup>

</servlet>

<servlet-mapping>

<servlet-name>springframework</servlet-name>

<url-pattern>/welcome.jsp</url-pattern>

<url-pattern>/welcome.html</url-pattern>

<url-pattern>*.json</url-pattern>

<url-pattern>*.xml</url-pattern>

<url-pattern>*.html</url-pattern>

</servlet-mapping>

springframework.xml

<?xml version="1.0" encoding="UTF-8"?>

<beans xmlns="http://www.springframework.org/schema/beans" xmlns:mvc="http://www.springframework.org/schema/mvc" xmlns:context="http://www.springframework.org/schema/context" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:p="http://www.springframework.org/schema/p" xmlns:mongo="http://www.springframework.org/schema/data/mongo" xmlns:tx="http://www.springframework.org/schema/tx"

xsi:schemaLocation="

http://www.springframework.org/schema/beans

http://www.springframework.org/schema/beans/spring-beans.xsd

http://www.springframework.org/schema/mvc

http://www.springframework.org/schema/mvc/spring-mvc.xsd

http://www.springframework.org/schema/context

http://www.springframework.org/schema/context/spring-context.xsd

http://www.springframework.org/schema/data/mongo

http://www.springframework.org/schema/data/mongo/spring-mongo-1.5.xsd

">

<mvc:resources location="/images/" mapping="/images/**" />

<mvc:resources location="/css/" mapping="/css/**" />

<mvc:resources location="/js/" mapping="/js/**" />

<mvc:resources location="/zt/" mapping="/zt/**" />

<mvc:resources location="/sm/" mapping="/sm/**" />

<mvc:resources location="/module/" mapping="/module/**" />

<context:component-scan base-package="cn.netkiller.controller">

</context:component-scan>

<context:annotation-config />

<mvc:annotation-driven />

<bean id="viewResolver" class="org.springframework.web.servlet.view.UrlBasedViewResolver">

<property name="viewClass" value="org.springframework.web.servlet.view.JstlView" />

<property name="prefix" value="/WEB-INF/jsp/" />

<property name="suffix" value=".jsp" />

</bean>

<bean id="configuracion" class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer">

<property name="location" value="classpath:resources/development.properties" />

</bean>

<!-- Redis Connection Factory -->

<bean id="jedisConnFactory" class="org.springframework.data.redis.connection.jedis.JedisConnectionFactory" p:host-name="192.168.2.1" p:port="6379" p:use-pool="true" />

<!-- redis template definition -->

<bean id="redisTemplate" class="org.springframework.data.redis.core.RedisTemplate" p:connection-factory-ref="jedisConnFactory" />

<mongo:db-factory id="mongoDbFactory" host="${mongo.host}" port="${mongo.port}" dbname="${mongo.database}" />

<!-- username="${mongo.username}" password="${mongo.password}" -->

<bean id="mongoTemplate" class="org.springframework.data.mongodb.core.MongoTemplate">

<constructor-arg name="mongoDbFactory" ref="mongoDbFactory" />

</bean>

<mongo:mapping-converter id="converter" db-factory-ref="mongoDbFactory" />

<bean id="gridFsTemplate" class="org.springframework.data.mongodb.gridfs.GridFsTemplate">

<constructor-arg ref="mongoDbFactory" />

<constructor-arg ref="converter" />

</bean>

</beans>

RestController

package cn.netkiller.controller;

import org.springframework.web.bind.annotation.PathVariable;

import org.springframework.web.bind.annotation.RequestHeader;

import org.springframework.web.bind.annotation.RequestMapping;

import org.springframework.web.bind.annotation.RequestMethod;

import org.springframework.web.bind.annotation.ResponseStatus;

import org.springframework.web.bind.annotation.RestController;

import org.springframework.http.HttpStatus;

import org.springframework.http.ResponseEntity;

import org.springframework.web.bind.annotation.ResponseBody;

import cn.netkiller.pojo.Message;

@RestController

@RequestMapping("/rest")

public class TestRestController {

public TrackerRestController() {

// TODO Auto-generated constructor stub

}

@RequestMapping("welcome")

@ResponseStatus(HttpStatus.OK)

public String welcome() {

return "Welcome to RestTemplate Example.";

}

@RequestMapping(value = "test", method = RequestMethod.GET, produces = { "application/xml", "application/json" })

@ResponseStatus(HttpStatus.OK)

public @ResponseBody Message test(@RequestHeader(value = "accept") String accept) {

Message message = new Message();

message.setTitle("test");

message.setText("Helloworld!!!");

System.out.println("accept: " + accept);

System.out.println(message.toString());

return message;

}

@RequestMapping("message/{name}")

public ResponseEntity<Message> message(@PathVariable String name) {

Message msg = new Message();

msg.setTitle(name);

return new ResponseEntity<Message>(msg, HttpStatus.OK);

}

@RequestMapping(value = "create", method = RequestMethod.POST, produces = { "application/xml", "application/json" })

public ResponseEntity<Tracker> create(@RequestBody Tracker tracker) {

this.mongoTemplate.insert(tracker);

return new ResponseEntity<Tracker>(tracker, HttpStatus.OK);

}

@RequestMapping(value = "read", method = RequestMethod.GET, produces = { "application/xml", "application/json" })

@ResponseStatus(HttpStatus.OK)

public ArrayList<Tracker> read() {

ArrayList<Tracker> trackers =  (ArrayList<Tracker>) mongoTemplate.findAll(Tracker.class);

return trackers;

}

}

POJO

package cn.netkiller.pojo;

import javax.xml.bind.annotation.XmlAttribute;

import javax.xml.bind.annotation.XmlElement;

import javax.xml.bind.annotation.XmlRootElement;

@XmlRootElement

public class Message {

String title;

String text;

public Message() {

// TODO Auto-generated constructor stub

}

//@XmlElement

@XmlAttribute

public String getTitle() {

return title;

}

public void setTitle(String title) {

this.title = title;

}

//@XmlElement

@XmlAttribute

public String getText() {

return text;

}

public void setText(String text) {

this.text = text;

}

@Override

public String toString() {

return "Message [title=" + title + ", text=" + text + "]";

}

}

在控制器中完整实例

package api.web;

import java.util.HashMap;

import java.util.List;

import java.util.Map;

import org.springframework.beans.factory.annotation.Autowired;

import org.springframework.stereotype.Controller;

import org.springframework.web.bind.annotation.PathVariable;

import org.springframework.web.bind.annotation.RequestMapping;

import org.springframework.web.bind.annotation.RequestParam;

import org.springframework.web.bind.annotation.ResponseBody;

import org.springframework.web.client.RestTemplate;

import api.domain.City;

import api.repository.CityRepository;

@Controller

public class IndexController {

@Autowired

private CityRepository repository;

// Spring RESTFul Client

@RequestMapping("/restful/get")

@ResponseBody

public String restfulGet() {

RestTemplate restTemplate = new RestTemplate();

String text = restTemplate.getForObject("http://inf.netkiller.cn/detail/html/2/2/42564.html", String.class);

return text;

}

@RequestMapping("/restful/get/{id}")

@ResponseBody

private static String restfulGetId(@PathVariable String id) {

final String uri = "http://inf.netkiller.cn/detail/html/{tid}/{cid}/{id}.html";

Map<String, String> params = new HashMap<String, String>();

params.put("tid", "2");

params.put("cid", "2");

params.put("id", id);

RestTemplate restTemplate = new RestTemplate();

String result = restTemplate.getForObject(uri, String.class, params);

return (result);

}

@RequestMapping("/restful/post/{id}")

@ResponseBody

private static String restfullPost(@PathVariable String id) {

final String uri = "http://inf.netkiller.cn/detail/html/{tid}/{cid}/{id}.html";

Map<String, String> params = new HashMap<String, String>();

params.put("tid", "2");

params.put("cid", "2");

params.put("id", id);

City city = new City("Shenzhen", "Guangdong");

RestTemplate restTemplate = new RestTemplate();

String result = restTemplate.postForObject(uri, city, String.class, params);

return result;

}

@RequestMapping("/restful/put/{id}")

private static void restfulPut(@PathVariable String id) {

final String uri = "http://inf.netkiller.cn/detail/html/{tid}/{cid}/{id}.html";

Map<String, String> params = new HashMap<String, String>();

params.put("id", id);

City city = new City("Shenzhen", "Guangdong");

RestTemplate restTemplate = new RestTemplate();

restTemplate.put(uri, city, params);

}

@RequestMapping("/restful/delete/{id}")

private static void restfulDelete(@PathVariable String id) {

final String uri = "http://inf.netkiller.cn/detail/html/{tid}/{cid}/{id}.html";

Map<String, String> params = new HashMap<String, String>();

params.put("id", id);

RestTemplate restTemplate = new RestTemplate();

restTemplate.delete(uri, params);

}

}

测试

neo@netkiller:~/www.netkiller.cn$ curl http://172.16.0.1:8080/spring4/rest/welcome.html

Welcome to RestTemplate Example.

neo@netkiller:~/www.netkiller.cn$ curl http://172.16.0.1:8080/spring4/rest/test.json

{"title":"test","text":"Helloworld!!!"}

neo@netkiller:~/www.netkiller.cn$ curl http://172.16.0.1:8080/spring4/rest/test.xml

<Message xmlns=""><title>test</title><text>Helloworld!!!</text></Message>

neo@netkiller:~/www.netkiller.cn$ curl -i -H "Accept: application/json" -H "Content-Type: application/json" -X POST -d '{"login":"neo", "unique":"356770257607079474","hostname":"www.example.com","referrer":"http://www.netkiller.cn","href":"http://www.netkiller.cn"}' http://172.16.0.1:8080/spring4/rest/create.json

HTTP/1.1 200 OK

Server: Apache-Coyote/1.1

Content-Type: application/json

Transfer-Encoding: chunked

Date: Tue, 21 Jun 2016 03:08:26 GMT

{"name":"neo","unique":"356770257607079474","hostname":"www.netkiller.cn","referrer":"http://www.netkiller.cn","href":"http://www.netkiller.cn"}

2. GET 操作

返回字符串

@RequestMapping("/restful/get")

@ResponseBody

public String restfulGet() {

RestTemplate restTemplate = new RestTemplate();

String text = restTemplate.getForObject("http://inf.netkiller.cn/detail/html/2/2/42564.html", String.class);

return text;

}

传递 GET 参数

@RequestMapping("/restful/get/{id}")

@ResponseBody

private static String restfulGetId(@PathVariable String id) {

final String uri = "http://inf.netkiller.cn/detail/html/{tid}/{cid}/{id}.html";

Map<String, String> params = new HashMap<String, String>();

params.put("tid", "2");

params.put("cid", "2");

params.put("id", id);

RestTemplate restTemplate = new RestTemplate();

String result = restTemplate.getForObject(uri, String.class, params);

return (result);

}

3. POST 操作

postForObject

传递对象

@RequestMapping("/restful/post/{id}")

@ResponseBody

private static String restfullPost(@PathVariable String id) {

final String uri = "http://inf.netkiller.cn/detail/html/{tid}/{cid}/{id}.html";

Map<String, String> params = new HashMap<String, String>();

params.put("tid", "2");

params.put("cid", "2");

params.put("id", id);

City city = new City("Shenzhen", "Guangdong");

RestTemplate restTemplate = new RestTemplate();

String result = restTemplate.postForObject(uri, city, String.class, params);

return result;

}

传递数据结构 MultiValueMap

@RequestMapping("/findByMobile")

public String findByMobile() {

System.out.println("****************************findByMobile******************************");

final String uri = "http://www.netkiller.cn/account/getMemberByMobile.json";

MultiValueMap<String, String> map = new LinkedMultiValueMap<String, String>();

try {

map.add("prefix", "86");

map.add("mobile", "13698041116");

map.add("_pretty_", "false");

} catch (Exception e) {

e.printStackTrace();

}

RestTemplate restTemplate = new RestTemplate();

String result = restTemplate.postForObject(uri, map, String.class);

System.out.println(map.toString());

System.out.println(result);

return result;

}

postForEntity

@RequestMapping("/findByMobile")

@ResponseBody

public String findByMobile() {

System.out.println("****************************findByMobile******************************");

final String uri = "https://www.netkiller.cn/account/getMemberByMobile";

MultiValueMap<String, String> map = new LinkedMultiValueMap<String, String>();

try {

map.add("prefix", "86");

map.add("mobile", "13698041116");

map.add("args", "[]");

map.add("_pretty_", "false");

} catch (Exception e) {

e.printStackTrace();

}

RestTemplate restTemplate = new RestTemplate();

ResponseEntity<String> response = restTemplate.postForEntity(uri, map, String.class);

System.out.println(map.toString());

System.out.println();

return response.getBody();

}

4. PUT 操作

@RequestMapping("/restful/put/{id}")

private static void restfulPut(@PathVariable String id) {

final String uri = "http://inf.netkiller.cn/detail/html/{tid}/{cid}/{id}.html";

Map<String, String> params = new HashMap<String, String>();

params.put("id", id);

City city = new City("Shenzhen", "Guangdong");

RestTemplate restTemplate = new RestTemplate();

restTemplate.put(uri, city, params);

}

5. Delete 操作

@RequestMapping("/restful/delete/{id}")

private static void restfulDelete(@PathVariable String id) {

final String uri = "http://inf.netkiller.cn/detail/html/{tid}/{cid}/{id}.html";

Map<String, String> params = new HashMap<String, String>();

params.put("id", id);

RestTemplate restTemplate = new RestTemplate();

restTemplate.delete(uri, params);

}

6. 上传文件

package cn.netkiller.file;

import org.springframework.core.io.FileSystemResource;

import org.springframework.core.io.Resource;

import org.springframework.http.*;

import org.springframework.util.LinkedMultiValueMap;

import org.springframework.util.MultiValueMap;

import org.springframework.web.client.RestTemplate;

import java.io.File;

import java.io.IOException;

import java.nio.file.Files;

import java.nio.file.Path;

public class UploadClient {

public static void main(String[] args) throws IOException {

MultiValueMap<String, Object> bodyMap = new LinkedMultiValueMap<>();

bodyMap.add("user-file", getUserFileResource());

HttpHeaders headers = new HttpHeaders();

headers.setContentType(MediaType.MULTIPART_FORM_DATA);

HttpEntity<MultiValueMap<String, Object>> requestEntity = new HttpEntity<>(bodyMap, headers);

RestTemplate restTemplate = new RestTemplate();

ResponseEntity<String> response = restTemplate.exchange("http://localhost:8080/upload", HttpMethod.POST, requestEntity, String.class);

System.out.println("response status: " + response.getStatusCode());

System.out.println("response body: " + response.getBody());

}

public static Resource getUserFileResource() throws IOException {

//todo replace tempFile with a real file

Path tempFile = Files.createTempFile("hello", ".txt");

Files.write(tempFile, "Helloworld, http://www.netkiller.cn".getBytes());

System.out.println("uploading: " + tempFile);

File file = tempFile.toFile();

//to upload in-memory bytes use ByteArrayResource instead

return new FileSystemResource(file);

}

}

7. HTTP Auth

Client

HttpClient client = new HttpClient();

UsernamePasswordCredentials credentials = new UsernamePasswordCredentials("your_user","your_password");

client.getState().setCredentials(new AuthScope("thehost", 9090, AuthScope.ANY_REALM), credentials);

CommonsClientHttpRequestFactory commons = new CommonsClientHttpRequestFactory(client);

RestTemplate template = new RestTemplate(commons);

Example results = template.getForObject("http://www.netkiller.cn:9090/foo.json", Example.class);

8. PKCS12

package example.controller;

import java.io.File;

import java.io.FileInputStream;

import java.io.IOException;

import java.security.KeyManagementException;

import java.security.KeyStore;

import java.security.KeyStoreException;

import java.security.NoSuchAlgorithmException;

import java.security.UnrecoverableKeyException;

import java.security.cert.CertificateException;

import javax.net.ssl.HostnameVerifier;

import javax.net.ssl.SSLContext;

import org.apache.http.conn.ssl.NoopHostnameVerifier;

import org.apache.http.conn.ssl.SSLConnectionSocketFactory;

import org.apache.http.impl.client.CloseableHttpClient;

import org.apache.http.impl.client.HttpClients;

import org.apache.http.ssl.SSLContextBuilder;

import org.apache.http.ssl.SSLContexts;

import org.springframework.beans.factory.annotation.Autowired;

import org.springframework.http.HttpEntity;

import org.springframework.http.HttpHeaders;

import org.springframework.http.HttpMethod;

import org.springframework.http.MediaType;

import org.springframework.http.ResponseEntity;

import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;

import org.springframework.security.oauth2.client.OAuth2RestOperations;

import org.springframework.security.oauth2.common.OAuth2AccessToken;

import org.springframework.stereotype.Controller;

import org.springframework.web.bind.annotation.GetMapping;

import org.springframework.web.bind.annotation.ResponseBody;

import org.springframework.web.client.RestTemplate;

@Controller

public class TestController {

@Autowired

private OAuth2RestOperations restTemplate;

@GetMapping("/")

@ResponseBody

public String index() {

OAuth2AccessToken token = restTemplate.getAccessToken();

System.out.println(token.getValue());

String tmp = restTemplate.getForObject("http://api.alpha.netkiller.cn/", String.class);

System.out.println(tmp);

return tmp;

}

@GetMapping("/ssl")

@ResponseBody

public String ssl() throws KeyManagementException, NoSuchAlgorithmException, KeyStoreException {

String url = "https://api.alpha.netkiller.cn/";

SSLContext sslcontext = SSLContexts.custom().loadTrustMaterial(null, (chain, authType) -> true).build();

SSLConnectionSocketFactory sslsf = new SSLConnectionSocketFactory(sslcontext, new String[] { "TLSv1" }, null, new NoopHostnameVerifier());

CloseableHttpClient httpClient = HttpClients.custom().setSSLSocketFactory(sslsf).build();

HttpComponentsClientHttpRequestFactory httpComponentsClientHttpRequestFactory = new HttpComponentsClientHttpRequestFactory(httpClient);

httpComponentsClientHttpRequestFactory.setConnectTimeout(60000);

httpComponentsClientHttpRequestFactory.setReadTimeout(180000);

final RestTemplate restTemplate = new RestTemplate(httpComponentsClientHttpRequestFactory);

HttpHeaders headers = new HttpHeaders();

headers.setContentType(MediaType.APPLICATION_JSON);

headers.set("Authorization", "Bearer " + this.restTemplate.getAccessToken().getValue());

HttpEntity<String> entity = new HttpEntity<String>(headers);

ResponseEntity<String> response = restTemplate.exchange(url, HttpMethod.GET, entity, String.class);

String str = response.getBody();

return str;

}

@GetMapping("/pkcs12")

@ResponseBody

public String PKCS12(String url, String data) throws KeyStoreException, NoSuchAlgorithmException, CertificateException, IOException, KeyManagementException, UnrecoverableKeyException {

KeyStore keyStore = KeyStore.getInstance("PKCS12");

FileInputStream instream = new FileInputStream(new File("/opt/xxx.p12"));

keyStore.load(instream, "netkiller".toCharArray());

// Trust own CA and all self-signed certs

SSLContext sslcontext = SSLContextBuilder.create().loadKeyMaterial(keyStore, "netkiller".toCharArray()).build();

// Allow TLSv1 protocol only

HostnameVerifier hostnameVerifier = NoopHostnameVerifier.INSTANCE;

SSLConnectionSocketFactory sslsf = new SSLConnectionSocketFactory(sslcontext, new String[] { "TLSv1" }, null, hostnameVerifier);

CloseableHttpClient httpclient = HttpClients.custom().setSSLSocketFactory(sslsf).build();

HttpComponentsClientHttpRequestFactory clientHttpRequestFactory = new HttpComponentsClientHttpRequestFactory(httpclient);

RestTemplate restTemplate = new RestTemplate(clientHttpRequestFactory);

HttpHeaders httpHeaders = new HttpHeaders();

httpHeaders.add("Connection", "keep-alive");

httpHeaders.add("Accept", "*/*");

httpHeaders.add("Content-Type", "application/x-www-form-urlencoded;charset=UTF-8");

httpHeaders.add("Host", "api.netkiller.cn");

httpHeaders.add("X-Requested-With", "XMLHttpRequest");

httpHeaders.add("Cache-Control", "max-age=0");

httpHeaders.add("User-Agent", "Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 6.0) ");

HttpEntity<String> httpEntity = new HttpEntity<String>(httpHeaders);

ResponseEntity<String> response = restTemplate.exchange(url, HttpMethod.POST, httpEntity, String.class);

return response.getBody();

}

}

9. Timeout 超时设置

JRE 启动参数设置超时时间

-Dsun.net.client.defaultConnectTimeout=<TimeoutInMiliSec>

-Dsun.net.client.defaultReadTimeout=<TimeoutInMiliSec>

RestTemplate timeout with SimpleClientHttpRequestFactory

//Create resttemplate

RestTemplate restTemplate = new RestTemplate(getClientHttpRequestFactory());

//Override timeouts in request factory

private SimpleClientHttpRequestFactory getClientHttpRequestFactory()

{

SimpleClientHttpRequestFactory clientHttpRequestFactory = new SimpleClientHttpRequestFactory();

// or

// HttpComponentsClientHttpRequestFactory clientHttpRequestFactory = new HttpComponentsClientHttpRequestFactory();

//Connect timeout

clientHttpRequestFactory.setConnectTimeout(10_000);

//Read timeout

clientHttpRequestFactory.setReadTimeout(10_000);

return clientHttpRequestFactory;

}

@Configuration 方式

注意下面使用了 Java 11 语法 var factory = new SimpleClientHttpRequestFactory();

package cn.netkiller.consul.consumer;

import org.springframework.context.annotation.Bean;

import org.springframework.context.annotation.Configuration;

import org.springframework.http.client.SimpleClientHttpRequestFactory;

import org.springframework.web.client.RestTemplate;

@Configuration

public class RestTemplateConfiguration {

@Bean

public RestTemplate restTemplate() {

var factory = new SimpleClientHttpRequestFactory();

factory.setConnectTimeout(3000);

factory.setReadTimeout(3000);

return new RestTemplate(factory);

}

}

第 32 章 SpringBootTest

1. Maven 依赖

<dependency>

<groupId>org.springframework.boot</groupId>

<artifactId>spring-boot-starter-test</artifactId>

<scope>test</scope>

</dependency>

2. 测试类

创建测试类，在测试类的类头部添加：@RunWith(SpringRunner.class)和@SpringBootTest注解，在测试方法的前添加@Test，最后选择方法右键run运行。

@RunWith(SpringRunner.class)

@SpringBootTest

public class WalletTest {

@Autowired

WalletService walletService;

public WalletTest() {

// TODO Auto-generated constructor stub

}

@Test

public void test() throws Exception {

Assert.assertEquals(5,5);

}

}

Junit基本注解介绍

@RunWith

在JUnit中有很多个Runner，他们负责调用你的测试代码，每一个Runner都有各自的特殊功能，你要根据需要选择不同的Runner来运行你的测试代码。

如果我们只是简单的做普通Java测试，不涉及Spring Web项目，你可以省略@RunWith注解，这样系统会自动使用默认Runner来运行你的代码。

//在所有测试方法前执行一次，一般在其中写上整体初始化的代码

@BeforeClass

//在所有测试方法后执行一次，一般在其中写上销毁和释放资源的代码

@AfterClass

//在每个测试方法前执行，一般用来初始化方法（比如我们在测试别的方法时，类中与其他测试方法共享的值已经被改变，为了保证测试结果的有效性，我们会在@Before注解的方法中重置数据）

@Before

//在每个测试方法后执行，在方法执行完成后要做的事情

@After

// 测试方法执行超过1000毫秒后算超时，测试将失败

@Test(timeout = 1000)

// 测试方法期望得到的异常类，如果方法执行没有抛出指定的异常，则测试失败

@Test(expected = Exception.class)

// 执行测试时将忽略掉此方法，如果用于修饰类，则忽略整个类

@Ignore("not ready yet")

@Test

3.

Assert.assertEquals 判断相等

Assert.assertTrue

4. JPA 测试

@RunWith(SpringJUnit4ClassRunner.class)

@SpringApplicationConfiguration(Application.class)

public class ApplicationTests {

@Autowired

private UserRepository userRepository;

@Autowired

private MessageRepository messageRepository;

@Test

public void test() throws Exception {

userRepository.save(new User("Neo", 10));

userRepository.save(new User("Jam", 20));

userRepository.save(new User("Tom", 30));

userRepository.save(new User("Sam", 40));

userRepository.save(new User("Leo", 50));

Assert.assertEquals(5, userRepository.findAll().size());

messageRepository.save(new Message("Neo", "How are you?"));

messageRepository.save(new Message("Jam", "Hi!"));

messageRepository.save(new Message("Sam", "What's going on?"));

Assert.assertEquals(3, messageRepository.findAll().size());

}

}

5. TestRestTemplate

package cn.netkiller.rest;

import java.net.URI;

import java.net.URISyntaxException;

import org.junit.Assert;

import org.junit.Test;

import org.junit.runner.RunWith;

import org.springframework.beans.factory.annotation.Autowired;

import org.springframework.boot.test.context.SpringBootTest;

import org.springframework.boot.test.context.SpringBootTest.WebEnvironment;

import org.springframework.boot.test.web.client.TestRestTemplate;

import org.springframework.boot.web.server.LocalServerPort;

import org.springframework.http.HttpEntity;

import org.springframework.http.HttpHeaders;

import org.springframework.http.ResponseEntity;

import org.springframework.test.context.junit4.SpringRunner;

import cn.netkiller.rest.model.Employee;

@RunWith(SpringRunner.class)

@SpringBootTest(webEnvironment=WebEnvironment.RANDOM_PORT)

public class SpringBootDemoApplicationTests

{

@Autowired

private TestRestTemplate restTemplate;

@LocalServerPort

int randomServerPort;

@Test

public void testAddEmployeeSuccess() throws URISyntaxException

{

final String baseUrl = "http://localhost:"+randomServerPort+"/employees/";

URI uri = new URI(baseUrl);

Employee employee = new Employee(null, "Adam", "Gilly", "test@email.com");

HttpHeaders headers = new HttpHeaders();

headers.set("X-COM-PERSIST", "true");

HttpEntity<Employee> request = new HttpEntity<>(employee, headers);

ResponseEntity<String> result = this.restTemplate.postForEntity(uri, request, String.class);

//Verify request succeed

Assert.assertEquals(201, result.getStatusCodeValue());

}

@Test

public void testAddEmployeeMissingHeader() throws URISyntaxException

{

final String baseUrl = "http://localhost:"+randomServerPort+"/employees/";

URI uri = new URI(baseUrl);

Employee employee = new Employee(null, "Adam", "Gilly", "test@email.com");

HttpHeaders headers = new HttpHeaders();

HttpEntity<Employee> request = new HttpEntity<>(employee, headers);

ResponseEntity<String> result = this.restTemplate.postForEntity(uri, request, String.class);

//Verify bad request and missing header

Assert.assertEquals(400, result.getStatusCodeValue());

Assert.assertEquals(true, result.getBody().contains("Missing request header"));

}

}

6. Controller单元测试

创建测试类，在测试类的类头部添加：@RunWith(SpringRunner.class)、@SpringBootTest、@ AutoConfigureMockMvc注解，在测试方法的前添加@Test，最后选择方法右键run运行。

使用@Autowired 注入MockMvc，在方法中使用 mvc测试功能。示例：

@RunWith(SpringRunner.class)

@SpringBootTest

@AutoConfigureMockMvc

public class StudentControllerTest {

@Autowired

private MockMvc mvc;

@Test

public void getAll() throws Exception {

mvc.perform(MockMvcRequestBuilders.get("/student/getAll")).andExpect(MockMvcResultMatchers.model().attributeExists("students"));

}

@Test

public void save() throws Exception {

Student student = new Student();

student.setAge(12);

student.setId("1003");

student.setName("Neo");

mvc.perform(MockMvcRequestBuilders.post("/student/save", student));

}

@Test

public void delete() throws Exception {

mvc.perform(MockMvcRequestBuilders.delete("/student/delete?id=1002"));

}

@Test

public void index() throws Exception {

mvc.perform(MockMvcRequestBuilders.get("/student/index")).andReturn();

}

}

7. WebTestClient

package cn.netkiller.webflux;

import org.junit.Before;

import org.junit.Test;

import org.junit.runner.RunWith;

import org.springframework.boot.test.context.SpringBootTest;

import org.springframework.test.context.junit4.SpringRunner;

import org.springframework.test.web.reactive.server.WebTestClient;

@RunWith(SpringRunner.class)

@SpringBootTest

public class WebfluxApplicationTests {

@Test

public void contextLoads() {

}

private WebTestClient webTestClient;

@Before

public void setUp() {

this.webTestClient = WebTestClient.bindToServer().baseUrl("http://localhost:8080").build();

}

@Test

public void sample() throws Exception {

this.webTestClient.get().uri("/").exchange().expectStatus().isOk().expectBody(String.class).isEqualTo("Hello world!");

}

@Test

public void client() {

}

}

第 33 章 Spring boot with Aop

1. Aspect

Maven

<dependency>

<groupId>org.springframework.boot</groupId>

<artifactId>spring-boot-starter-aop</artifactId>

</dependency>

Pojo 类

package cn.netkiller.aop.pojo;

import lombok.Data;

@Data

public class Employee {

private String id;

private String name;

public Employee() {

// TODO Auto-generated constructor stub

}

}

Service 类

package cn.netkiller.aop.service;

import org.springframework.stereotype.Service;

import cn.netkiller.aop.pojo.Employee;

@Service

public class EmployeeService {

public EmployeeService() {

// TODO Auto-generated constructor stub

}

public Employee createEmployee(String id, String name) {

Employee emp = new Employee();

emp.setName(name);

emp.setId(id);

return emp;

}

public void deleteEmployee(String id) {

}

}

Aspect 类

package cn.netkiller.aop.aspect;

import org.aspectj.lang.JoinPoint;

import org.aspectj.lang.annotation.After;

import org.aspectj.lang.annotation.Aspect;

import org.aspectj.lang.annotation.Before;

import org.springframework.stereotype.Component;

@Aspect

@Component

public class EmployeeServiceAspect {

public EmployeeServiceAspect() {

}

@Before(value = "execution(* cn.netkiller.aop.service.EmployeeService.*(..)) and args(id, name)")

public void beforeAdvice(JoinPoint joinPoint, String id, String name) {

System.out.println("Before method:" + joinPoint.getSignature());

System.out.println("Creating Employee with id: " + id + ", name: " + name);

}

@After(value = "execution(* cn.netkiller.aop.service.EmployeeService.*(..)) and args(id,name)")

public void afterAdvice(JoinPoint joinPoint, String id, String name) {

System.out.println("After method:" + joinPoint.getSignature());

System.out.println("Successfully created Employee with id: " + id + ", name: " + name);

}

}

控制器

package cn.netkiller.aop.controller;

import org.springframework.beans.factory.annotation.Autowired;

import org.springframework.web.bind.annotation.RequestMapping;

import org.springframework.web.bind.annotation.RequestMethod;

import org.springframework.web.bind.annotation.RequestParam;

import org.springframework.web.bind.annotation.RestController;

import cn.netkiller.aop.pojo.Employee;

import cn.netkiller.aop.service.EmployeeService;

@RestController

public class EmployeeController {

public EmployeeController() {

// TODO Auto-generated constructor stub

}

@Autowired

private EmployeeService employeeService;

@RequestMapping(value = "/add/employee", method = RequestMethod.GET)

public Employee addEmployee(@RequestParam("id") String id, @RequestParam("name") String name) {

return employeeService.createEmployee(id, name);

}

@RequestMapping(value = "/remove/employee", method = RequestMethod.GET)

public String removeEmployee(@RequestParam("id") String id) {

employeeService.deleteEmployee(id);

return "Employee removed";

}

}

Application

package cn.netkiller.aop;

import org.springframework.boot.SpringApplication;

import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication

public class Application {

public static void main(String[] args) {

System.out.println("Hello World!");

SpringApplication.run(Application.class, args);

}

}

测试

触发 Aspect

neo@MacBook-Pro ~ % curl http://localhost:8080/add/employee\?id\=1\&name\=neo

{"id":"1","name":"neo"}

控制台输出效果

Before method:Employee cn.netkiller.aop.service.EmployeeService.createEmployee(String,String)

Creating Employee with id: 1, name: neo

After method:Employee cn.netkiller.aop.service.EmployeeService.createEmployee(String,String)

Successfully created Employee with id: 1, name: neo

第 34 章 Spring boot with starter

spring-boot-starter-xxxxx 是 Spring boot 子模块，开发中我们可以根据自己的需求开引用所需的功能，这样不必引用所有的 Spring boot 依赖包。

我们也可以开发自己的 starter 模块和自定义注解，将我们的项目化整为零，模块化，随时根据项目的需要引用，并且可以使用自定义注解启用它们。

1. 实现 starter

Maven pom.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>

<groupId>cn.netkiller</groupId>

<artifactId>spring-boot-starter-customize</artifactId>

<version>0.0.1-SNAPSHOT</version>

<packaging>jar</packaging>

<name>Spring Boot Starter Project</name>

<parent>

<groupId>cn.netkiller</groupId>

<artifactId>parent</artifactId>

<version>0.0.1-SNAPSHOT</version>

</parent>

<properties>

<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>

<start-class>cn.netkiller.starter.App</start-class>

<java.version>11</java.version>

<lombok.version>1.16.18</lombok.version>

</properties>

<dependencies>

<dependency>

<groupId>org.springframework.boot</groupId>

<artifactId>spring-boot-starter</artifactId>

</dependency>

<dependency>

<groupId>org.projectlombok</groupId>

<artifactId>lombok</artifactId>

<version>${lombok.version}</version>

<scope>provided</scope>

</dependency>

<dependency>

<groupId>org.springframework.boot</groupId>

<artifactId>spring-boot-starter-test</artifactId>

<scope>test</scope>

</dependency>

</dependencies>

<build>

<plugins>

<plugin>

<groupId>org.springframework.boot</groupId>

<artifactId>spring-boot-maven-plugin</artifactId>

</plugin>

</plugins>

</build>

</project>

配置文件处理

application.properties 加入短信网关的配置项

sms.gateway.url=https://sms.netkiller.cn/v1

sms.gateway.username=netkiller

sms.gateway.password=passw0rd

SmsProperties 用于读取前缀为 sms.gateway 的配置项。

package cn.netkiller.autoconfigure;

import org.springframework.boot.context.properties.ConfigurationProperties;

import lombok.Data;

@ConfigurationProperties(prefix = "sms.gateway")

@Data

public class SmsProperties {

private String url;

private String username;

private String password;

public String getUrl() {

return url;

}

public void setUrl(String url) {

this.url = url;

}

public String getUsername() {

return username;

}

public void setUsername(String username) {

this.username = username;

}

public String getPassword() {

return password;

}

public void setPassword(String password) {

this.password = password;

}

@Override

public String toString() {

return "SmsProperties [url=" + url + ", username=" + username + ", password=" + password + "]";

}

}

自动配置文件

package cn.netkiller.autoconfigure;

import org.springframework.beans.factory.annotation.Autowired;

import org.springframework.boot.context.properties.EnableConfigurationProperties;

import org.springframework.context.annotation.Bean;

import org.springframework.context.annotation.Configuration;

import cn.netkiller.sms.SmsSender;

@EnableConfigurationProperties(value = SmsProperties.class)

@Configuration

public class SmsAutoConfiguration {

@Autowired

private SmsProperties smsProperties;

@Bean

public SmsSender send() {

return new SmsSender(this.smsProperties);

}

}

启用 starter 的自定义注解

package cn.netkiller.autoconfigure;

import java.lang.annotation.Documented;

import java.lang.annotation.Retention;

import java.lang.annotation.Target;

import java.lang.annotation.ElementType;

import java.lang.annotation.RetentionPolicy;

import org.springframework.context.annotation.Import;

@Target({ ElementType.TYPE })

@Retention(RetentionPolicy.RUNTIME)

@Documented

@Import({ SmsAutoConfiguration.class })

public @interface EnableSms {

}

2. 引用 starter

Maven pom.xml 引入依赖

<dependency>

<groupId>cn.netkiller</groupId>

<artifactId>spring-boot-starter-customize</artifactId>

<version>0.0.1-SNAPSHOT</version>

</dependency>

完整的 pom.xml 文件

<?xml version="1.0"?>

<project xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd" xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">

<modelVersion>4.0.0</modelVersion>

<parent>

<groupId>cn.netkiller</groupId>

<artifactId>parent</artifactId>

<version>0.0.1-SNAPSHOT</version>

</parent>

<groupId>cn.netkiller</groupId>

<artifactId>spring-boot-starter-customize-test</artifactId>

<version>0.0.1-SNAPSHOT</version>

<name>spring-boot-starter-customize-test</name>

<url>http://maven.apache.org</url>

<properties>

<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>

</properties>

<dependencies>

<dependency>

<groupId>cn.netkiller</groupId>

<artifactId>spring-boot-starter-customize</artifactId>

<version>0.0.1-SNAPSHOT</version>

</dependency>

</dependencies>

</project>

通过注解配置 starter

@EnableSms 启用自动配置短信发送模块

package cn.netkiller.starter.customize.test;

import org.springframework.boot.SpringApplication;

import org.springframework.boot.autoconfigure.SpringBootApplication;

import org.springframework.context.ConfigurableApplicationContext;

import cn.netkiller.autoconfigure.EnableSms;

import cn.netkiller.sms.SmsSender;

@SpringBootApplication

@EnableSms

public class Application {

public static void main(String[] args) {

System.out.println("Hello World!");

ConfigurableApplicationContext applicationContext = SpringApplication.run(Application.class, args);

SmsSender smsSender = applicationContext.getBean(SmsSender.class);

smsSender.send("验证码发送成功！");

}

}

测试运行结果

Hello World!

.   ____          _            __ _ _

/\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \

( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \

\\/  ___)| |_)| | | | | || (_| |  ) ) ) )

'  |____| .__|_| |_|_| |_\__, | / / / /

=========|_|==============|___/=/_/_/_/

:: Spring Boot ::        (v2.3.2.RELEASE)

2020-08-02 20:51:54.564  INFO 43216 --- [           main] c.n.starter.customize.test.Application   : Starting Application on MacBook-Pro-Neo.local with PID 43216 (/Users/neo/git/springcloud/spring-boot-starter-customize-test/target/classes started by neo in /Users/neo/git/springcloud/spring-boot-starter-customize-test)

2020-08-02 20:51:54.567  INFO 43216 --- [           main] c.n.starter.customize.test.Application   : No active profile set, falling back to default profiles: default

2020-08-02 20:51:55.349  INFO 43216 --- [           main] c.n.starter.customize.test.Application   : Started Application in 1.539 seconds (JVM running for 1.942)

SmsProperties [url=https://sms.netkiller.cn/v1, username=netkiller, password=passw0rd]

验证码发送成功！

第 35 章 Spring boot with Monitor

1. Spring boot with Grafana

Springboot 集成 InfluxDB

Springboot 集成 InfluxDB 非常简单，先引入依赖即可，记得需要同时引入 actuator

<dependency>

<groupId>org.springframework.boot</groupId>

<artifactId>spring-boot-starter-actuator</artifactId>

</dependency>

<dependency>

<groupId>io.micrometer</groupId>

<artifactId>micrometer-registry-influx</artifactId>

</dependency>

配置文件 application.yaml 如下

spring:

application:

name: springboot-with-influxdb

server:

port: 8080

management:

metrics:

export:

influx:

enabled: true

db: springboot

uri: http://localhost:8086

user-name:

password:

connect-timeout: 1s

read-timeout: 10s

auto-create-db: true

step: 1m

num-threads: 2

consistency: one

compressed: true

batch-size: 1000

InfluxDB

配置好 Springboot 后，启动应用，稍后Springboot 就会将数据源源不断地写入到 InfluxDB 中。

> show measurements

name: measurements

name

----

jvm_buffer_count

jvm_buffer_memory_used

jvm_buffer_total_capacity

jvm_classes_loaded

jvm_classes_unloaded

jvm_gc_live_data_size

jvm_gc_max_data_size

jvm_gc_memory_allocated

jvm_gc_memory_promoted

jvm_gc_pause

jvm_memory_committed

jvm_memory_max

jvm_memory_used

jvm_threads_daemon

jvm_threads_live

jvm_threads_peak

jvm_threads_states

logback_events

process_cpu_usage

process_files_max

process_files_open

process_start_time

process_uptime

system_cpu_count

system_cpu_usage

system_load_average_1m

tomcat_sessions_active_current

tomcat_sessions_active_max

tomcat_sessions_alive_max

tomcat_sessions_created

tomcat_sessions_expired

tomcat_sessions_rejected

visits

查看数据

select * from process_cpu_usage

第 36 章 SpringBoot Admin

依赖

<dependency>

<groupId>de.codecentric</groupId>

<artifactId>spring-boot-admin-starter-server</artifactId>

<version>2.1.6</version>

</dependency>

启用 Springboot Admin

@EnableAdminServer

public class SpringBootAdminApplication {

public static void main(String[] args) {

SpringApplication.run(SpringBootAdminApplication.class, args);

}

}

Nginx 跨域

server {

listen       192.168.30.11:80;

listen       192.168.30.11:443 ssl http2;

server_name  api.netkiller.cn;

ssl_certificate "/etc/pki/nginx/server.crt";

ssl_certificate_key "/etc/pki/nginx/private/server.key";

ssl_session_cache shared:SSL:1m;

ssl_session_timeout  10m;

ssl_ciphers PROFILE=SYSTEM;

ssl_prefer_server_ciphers on;

access_log /var/log/nginx/api.netkiller.cn.access.log;

error_log /var/log/nginx/api.netkiller.cn.error.log;

error_page 497 https://$host$uri?$args;

if ($scheme = http) {

return 301 https://$server_name$request_uri;

}

location / {

add_header Content-Security-Policy "upgrade-insecure-requests;connect-src *";

proxy_set_header X-Forwarded-Proto https;

proxy_set_header Host $host;

proxy_set_header X-Real-IP $remote_addr;

proxy_set_header REMOTE-HOST $remote_addr;

proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;

proxy_pass http://192.168.30.10:8088;

}

error_page 404 /404.html;

location = /40x.html {

}

error_page 500 502 503 504 /50x.html;

location = /50x.html {

}

}

2. Spring Boot with Prometheus

Maven 依赖

<dependencies>

<dependency>

<groupId>org.springframework.boot</groupId>

<artifactId>spring-boot-starter-webflux</artifactId>

</dependency>

<dependency>

<groupId>io.micrometer</groupId>

<artifactId>micrometer-registry-prometheus</artifactId>

</dependency>

</dependencies>

application.properties 配置文件

开启 metrics

spring.application.name=springboot-with-prometheus

#management.endpoints.web.exposure.include=*

management.endpoints.web.exposure.include=prometheus

management.metrics.tags.application=${spring.application.name}

启动类

package cn.netkiller.welcome;

import java.net.InetAddress;

import java.net.UnknownHostException;

import org.reactivestreams.Publisher;

import org.springframework.beans.factory.annotation.Value;

import org.springframework.boot.SpringApplication;

import org.springframework.boot.actuate.autoconfigure.metrics.MeterRegistryCustomizer;

import org.springframework.boot.autoconfigure.SpringBootApplication;

import org.springframework.context.annotation.Bean;

import org.springframework.web.bind.annotation.GetMapping;

import org.springframework.web.bind.annotation.ResponseBody;

import org.springframework.web.bind.annotation.RestController;

import io.micrometer.core.instrument.MeterRegistry;

import reactor.core.publisher.Mono;

@SpringBootApplication

@RestController

public class Application {

@GetMapping("/")

@ResponseBody

public Publisher<String> index() {

return Mono.just("Hello world! \r\n");

}

@GetMapping("/address")

@ResponseBody

public Publisher<String> address() throws UnknownHostException {

InetAddress addr = InetAddress.getLocalHost();

return Mono.just(String.format("Address %s, Hostname %s \r\n", addr.getHostAddress(), addr.getHostName()));

}

@Bean

MeterRegistryCustomizer<MeterRegistry> configurer(@Value("${spring.application.name}") String applicationName) {

return (registry) -> registry.config().commonTags("application", applicationName);

}

public static void main(String[] args) {

System.out.println("Welcome!");

SpringApplication.run(Application.class, args);

}

}

测试

启动 Springboot

Welcome!

.   ____          _            __ _ _

/\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \

( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \

\\/  ___)| |_)| | | | | || (_| |  ) ) ) )

'  |____| .__|_| |_|_| |_\__, | / / / /

=========|_|==============|___/=/_/_/_/

:: Spring Boot ::        (v2.3.4.RELEASE)

2020-10-28 21:57:54.110  INFO 64079 --- [           main] cn.netkiller.welcome.Application         : Starting Application on MacBook-Pro-Neo.local with PID 64079 (/Users/neo/workspace/microservice/welcome/target/classes started by neo in /Users/neo/workspace/microservice/welcome)

2020-10-28 21:57:54.114  INFO 64079 --- [           main] cn.netkiller.welcome.Application         : No active profile set, falling back to default profiles: default

2020-10-28 21:57:55.877  INFO 64079 --- [           main] o.s.b.a.e.web.EndpointLinksResolver      : Exposing 14 endpoint(s) beneath base path '/actuator'

2020-10-28 21:57:56.364  INFO 64079 --- [           main] o.s.b.web.embedded.netty.NettyWebServer  : Netty started on port(s): 8080

2020-10-28 21:57:56.380  INFO 64079 --- [           main] cn.netkiller.welcome.Application         : Started Application in 2.773 seconds (JVM running for 3.439)

获取监控数据

neo@MacBook-Pro-Neo ~ % curl http://localhost:8080/actuator/prometheus

# HELP jvm_threads_states_threads The current number of threads having NEW state

# TYPE jvm_threads_states_threads gauge

jvm_threads_states_threads{application="springboot-with-prometheus",state="terminated",} 0.0

jvm_threads_states_threads{application="springboot-with-prometheus",state="blocked",} 0.0

jvm_threads_states_threads{application="springboot-with-prometheus",state="waiting",} 2.0

jvm_threads_states_threads{application="springboot-with-prometheus",state="timed-waiting",} 2.0

jvm_threads_states_threads{application="springboot-with-prometheus",state="runnable",} 7.0

jvm_threads_states_threads{application="springboot-with-prometheus",state="new",} 0.0

# HELP jvm_gc_memory_allocated_bytes_total Incremented for an increase in the size of the young generation memory pool after one GC to before the next

# TYPE jvm_gc_memory_allocated_bytes_total counter

jvm_gc_memory_allocated_bytes_total{application="springboot-with-prometheus",} 1.9922944E7

# HELP system_cpu_usage The "recent cpu usage" for the whole system

# TYPE system_cpu_usage gauge

system_cpu_usage{application="springboot-with-prometheus",} 0.0

# HELP jvm_memory_used_bytes The amount of used memory

# TYPE jvm_memory_used_bytes gauge

jvm_memory_used_bytes{application="springboot-with-prometheus",area="heap",id="G1 Old Gen",} 1.1322368E7

jvm_memory_used_bytes{application="springboot-with-prometheus",area="heap",id="G1 Eden Space",} 1.6777216E7

jvm_memory_used_bytes{application="springboot-with-prometheus",area="nonheap",id="Metaspace",} 3.1712968E7

jvm_memory_used_bytes{application="springboot-with-prometheus",area="heap",id="G1 Survivor Space",} 1487328.0

jvm_memory_used_bytes{application="springboot-with-prometheus",area="nonheap",id="CodeHeap 'non-nmethods'",} 1277184.0

jvm_memory_used_bytes{application="springboot-with-prometheus",area="nonheap",id="CodeHeap 'non-profiled nmethods'",} 1413760.0

jvm_memory_used_bytes{application="springboot-with-prometheus",area="nonheap",id="Compressed Class Space",} 4253200.0

jvm_memory_used_bytes{application="springboot-with-prometheus",area="nonheap",id="CodeHeap 'profiled nmethods'",} 7536256.0

# HELP jvm_memory_committed_bytes The amount of memory in bytes that is committed for the Java virtual machine to use

# TYPE jvm_memory_committed_bytes gauge

jvm_memory_committed_bytes{application="springboot-with-prometheus",area="heap",id="G1 Old Gen",} 2.5165824E7

jvm_memory_committed_bytes{application="springboot-with-prometheus",area="heap",id="G1 Eden Space",} 2.8311552E7

jvm_memory_committed_bytes{application="springboot-with-prometheus",area="nonheap",id="Metaspace",} 3.3161216E7

jvm_memory_committed_bytes{application="springboot-with-prometheus",area="heap",id="G1 Survivor Space",} 2097152.0

jvm_memory_committed_bytes{application="springboot-with-prometheus",area="nonheap",id="CodeHeap 'non-nmethods'",} 2555904.0

jvm_memory_committed_bytes{application="springboot-with-prometheus",area="nonheap",id="CodeHeap 'non-profiled nmethods'",} 2555904.0

jvm_memory_committed_bytes{application="springboot-with-prometheus",area="nonheap",id="Compressed Class Space",} 4849664.0

jvm_memory_committed_bytes{application="springboot-with-prometheus",area="nonheap",id="CodeHeap 'profiled nmethods'",} 7602176.0

# HELP system_load_average_1m The sum of the number of runnable entities queued to available processors and the number of runnable entities running on the available processors averaged over a period of time

# TYPE system_load_average_1m gauge

system_load_average_1m{application="springboot-with-prometheus",} 2.263671875

# HELP process_files_open_files The open file descriptor count

# TYPE process_files_open_files gauge

process_files_open_files{application="springboot-with-prometheus",} 89.0

# HELP jvm_classes_unloaded_classes_total The total number of classes unloaded since the Java virtual machine has started execution

# TYPE jvm_classes_unloaded_classes_total counter

jvm_classes_unloaded_classes_total{application="springboot-with-prometheus",} 0.0

# HELP jvm_buffer_total_capacity_bytes An estimate of the total capacity of the buffers in this pool

# TYPE jvm_buffer_total_capacity_bytes gauge

jvm_buffer_total_capacity_bytes{application="springboot-with-prometheus",id="direct",} 1.6777223E7

jvm_buffer_total_capacity_bytes{application="springboot-with-prometheus",id="mapped - 'non-volatile memory'",} 0.0

jvm_buffer_total_capacity_bytes{application="springboot-with-prometheus",id="mapped",} 0.0

# HELP jvm_gc_live_data_size_bytes Size of old generation memory pool after a full GC

# TYPE jvm_gc_live_data_size_bytes gauge

jvm_gc_live_data_size_bytes{application="springboot-with-prometheus",} 0.0

# HELP jvm_gc_pause_seconds Time spent in GC pause

# TYPE jvm_gc_pause_seconds summary

jvm_gc_pause_seconds_count{action="end of minor GC",application="springboot-with-prometheus",cause="G1 Evacuation Pause",} 1.0

jvm_gc_pause_seconds_sum{action="end of minor GC",application="springboot-with-prometheus",cause="G1 Evacuation Pause",} 0.008

# HELP jvm_gc_pause_seconds_max Time spent in GC pause

# TYPE jvm_gc_pause_seconds_max gauge

jvm_gc_pause_seconds_max{action="end of minor GC",application="springboot-with-prometheus",cause="G1 Evacuation Pause",} 0.008

# HELP process_files_max_files The maximum file descriptor count

# TYPE process_files_max_files gauge

process_files_max_files{application="springboot-with-prometheus",} 10240.0

# HELP jvm_threads_live_threads The current number of live threads including both daemon and non-daemon threads

# TYPE jvm_threads_live_threads gauge

jvm_threads_live_threads{application="springboot-with-prometheus",} 11.0

# HELP process_start_time_seconds Start time of the process since unix epoch.

# TYPE process_start_time_seconds gauge

process_start_time_seconds{application="springboot-with-prometheus",} 1.603893473057E9

# HELP jvm_classes_loaded_classes The number of classes that are currently loaded in the Java virtual machine

# TYPE jvm_classes_loaded_classes gauge

jvm_classes_loaded_classes{application="springboot-with-prometheus",} 6965.0

# HELP jvm_buffer_memory_used_bytes An estimate of the memory that the Java virtual machine is using for this buffer pool

# TYPE jvm_buffer_memory_used_bytes gauge

jvm_buffer_memory_used_bytes{application="springboot-with-prometheus",id="direct",} 1.6777224E7

jvm_buffer_memory_used_bytes{application="springboot-with-prometheus",id="mapped - 'non-volatile memory'",} 0.0

jvm_buffer_memory_used_bytes{application="springboot-with-prometheus",id="mapped",} 0.0

# HELP process_cpu_usage The "recent cpu usage" for the Java Virtual Machine process

# TYPE process_cpu_usage gauge

process_cpu_usage{application="springboot-with-prometheus",} 0.0

# HELP jvm_buffer_count_buffers An estimate of the number of buffers in the pool

# TYPE jvm_buffer_count_buffers gauge

jvm_buffer_count_buffers{application="springboot-with-prometheus",id="direct",} 4.0

jvm_buffer_count_buffers{application="springboot-with-prometheus",id="mapped - 'non-volatile memory'",} 0.0

jvm_buffer_count_buffers{application="springboot-with-prometheus",id="mapped",} 0.0

# HELP jvm_gc_max_data_size_bytes Max size of old generation memory pool

# TYPE jvm_gc_max_data_size_bytes gauge

jvm_gc_max_data_size_bytes{application="springboot-with-prometheus",} 2.147483648E9

# HELP jvm_threads_peak_threads The peak live thread count since the Java virtual machine started or peak was reset

# TYPE jvm_threads_peak_threads gauge

jvm_threads_peak_threads{application="springboot-with-prometheus",} 11.0

# HELP logback_events_total Number of error level events that made it to the logs

# TYPE logback_events_total counter

logback_events_total{application="springboot-with-prometheus",level="debug",} 0.0

logback_events_total{application="springboot-with-prometheus",level="trace",} 0.0

logback_events_total{application="springboot-with-prometheus",level="info",} 3.0

logback_events_total{application="springboot-with-prometheus",level="error",} 0.0

logback_events_total{application="springboot-with-prometheus",level="warn",} 0.0

# HELP jvm_gc_memory_promoted_bytes_total Count of positive increases in the size of the old generation memory pool before GC to after GC

# TYPE jvm_gc_memory_promoted_bytes_total counter

jvm_gc_memory_promoted_bytes_total{application="springboot-with-prometheus",} 1924096.0

# HELP jvm_threads_daemon_threads The current number of live daemon threads

# TYPE jvm_threads_daemon_threads gauge

jvm_threads_daemon_threads{application="springboot-with-prometheus",} 9.0

# HELP process_uptime_seconds The uptime of the Java virtual machine

# TYPE process_uptime_seconds gauge

process_uptime_seconds{application="springboot-with-prometheus",} 35.375

# HELP jvm_memory_max_bytes The maximum amount of memory in bytes that can be used for memory management

# TYPE jvm_memory_max_bytes gauge

jvm_memory_max_bytes{application="springboot-with-prometheus",area="heap",id="G1 Old Gen",} 2.147483648E9

jvm_memory_max_bytes{application="springboot-with-prometheus",area="heap",id="G1 Eden Space",} -1.0

jvm_memory_max_bytes{application="springboot-with-prometheus",area="nonheap",id="Metaspace",} -1.0

jvm_memory_max_bytes{application="springboot-with-prometheus",area="heap",id="G1 Survivor Space",} -1.0

jvm_memory_max_bytes{application="springboot-with-prometheus",area="nonheap",id="CodeHeap 'non-nmethods'",} 5840896.0

jvm_memory_max_bytes{application="springboot-with-prometheus",area="nonheap",id="CodeHeap 'non-profiled nmethods'",} 1.22908672E8

jvm_memory_max_bytes{application="springboot-with-prometheus",area="nonheap",id="Compressed Class Space",} 1.073741824E9

jvm_memory_max_bytes{application="springboot-with-prometheus",area="nonheap",id="CodeHeap 'profiled nmethods'",} 1.22908672E8

# HELP system_cpu_count The number of processors available to the Java virtual machine

# TYPE system_cpu_count gauge

system_cpu_count{application="springboot-with-prometheus",} 8.0

控制器监控

@RestController

@RequestMapping("/app")

public class AppController {

private final Counter counter;

public AppController(final MeterRegistry registry) {

this.counter = registry.counter("greeting");

}

@RequestMapping("/greeting")

public String greeting() {

this.counter.increment();

return "hello world #" + this.counter.count();

}

}

自定义埋点监控

prometheus 监控指标有如下几种类型

- Counter 类型代表数据递增的指标，即只增不减，除非监控系统重置

- Guage 类型代表数据可以任意变化的指标，即可增可减

- Histogram 由bucket{le=""}，bucket{le="+Inf"},sum，count 组成，用于一段时间范围内对数据进行采样，并能够对其指定区间以及总数进行统计，通常它采集的数据展示为直方图。

- Summary 由{quantile="<φ>"}，sum，count 组成，用于一段时间内数据采样结果（通常是请求持续时间或响应大小），它直接存储了 quantile 数据，而不是根据统计区间计算出来的。

拦截器

package cn.netkiller.welcome.config;

import javax.servlet.http.HttpServletRequest;

import javax.servlet.http.HttpServletResponse;

import org.springframework.beans.factory.annotation.Autowired;

import org.springframework.web.servlet.HandlerInterceptor;

import cn.netkiller.welcome.component.RestfulApiCounter;

public class PrometheusInterceptor implements HandlerInterceptor {

@Autowired

private RestfulApiCounter restfulApiCounter;

public PrometheusInterceptor() {

}

@Override

public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {

restfulApiCounter.increment();

}

}

计数器元件

package cn.netkiller.welcome.component;

import org.springframework.stereotype.Component;

import io.micrometer.core.instrument.Counter;

import io.micrometer.core.instrument.MeterRegistry;

@Component

public class RestfulApiCounter {

private final Counter counter;

public RestfulApiCounter(MeterRegistry registry) {

this.counter = registry.counter("restful_api_requests_total");

}

public void increment() {

this.counter.increment();

}

}

配置类

package cn.netkiller.welcome.config;

import org.springframework.context.annotation.Bean;

import org.springframework.context.annotation.Configuration;

import org.springframework.web.servlet.config.annotation.InterceptorRegistry;

import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration

public class IntercepterConfiguration implements WebMvcConfigurer {

@Bean

public PrometheusInterceptor prometheusInterceptor() {

return new PrometheusInterceptor();

}

@Override

public void addInterceptors(InterceptorRegistry registry) {

registry.addInterceptor(prometheusInterceptor()).addPathPatterns("/**");

}

}

测试埋点效果

neo@MacBook-Pro-Neo ~ % curl -s http://localhost:8080/actuator/prometheus | grep restful

# HELP restful_api_requests_total

# TYPE restful_api_requests_total counter

restful_api_requests_total{application="springboot-with-prometheus",} 0.0

neo@MacBook-Pro-Neo ~ % curl http://localhost:8080/

Hello world!

neo@MacBook-Pro-Neo ~ % curl http://localhost:8080/address

Address 127.0.0.1, Hostname MacBook-Pro-Neo.local

neo@MacBook-Pro-Neo ~ % curl -s http://localhost:8080/actuator/prometheus | grep restful

# HELP restful_api_requests_total

# TYPE restful_api_requests_total counter

restful_api_requests_total{application="springboot-with-prometheus",} 2.0

第 37 章 Spring boot with Git version

Spring boot 每次升级打包发给运维操作，常常运维操作不当致使升级失败，开发怎样确认线上的jar/war包与升级包一致呢？

请看下面的解决方案

1. CommonRestController 公共控制器

所有 RestController将会集成 CommonRestController

package cn.netkiller.api.rest;

import org.springframework.http.HttpStatus;

import org.springframework.security.core.annotation.AuthenticationPrincipal;

import org.springframework.security.core.userdetails.UserDetails;

import org.springframework.web.bind.annotation.RequestMapping;

import org.springframework.web.bind.annotation.ResponseStatus;

public class CommonRestController {

@RequestMapping("ping")

@ResponseStatus(HttpStatus.OK)

public String welcome() {

return "PONG";

}

@RequestMapping("commit")

public String commit() {

return "$Id$";

}

@RequestMapping("auth")

@ResponseStatus(HttpStatus.OK)

public String auth(@AuthenticationPrincipal final UserDetails user) {

return String.format("%s: %s %s", user.getUsername(), user.getPassword(), user.getAuthorities());

}

}

2. VersionRestController 测试控制器

我们创建一个RestController并继承CommonRestController用来测试

package cn.netkiller.api.rest;

@RestController

@RequestMapping("/public/version")

public class VersionRestController extends CommonRestController {

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

public VersionRestController() {

// TODO Auto-generated constructor stub

}

@RequestMapping("welcome")

@ResponseStatus(HttpStatus.OK)

public String welcome() {

return "Welcome to RestTemplate version 1.0.";

}

}

3. 创建 .gitattributes 文件

# vim .gitattributes

src/main/java/cn/netkiller/api/rest/CommonRestController.java ident

使用curl命令调用commit接口可以显示当前war/jar最后一次提交的版本号码（你同样可以使用IE浏览器）

curl https://api.netkiller.cn/public/version/commit.json

$Id: 929bc9e4c90b4d68c25dc693618f23b33fd6ba0f $

第 38 章 Spring boot with Session share

1. Redis

Maven

增加下面代码到pom.xml

<dependency>

<groupId>org.springframework.boot</groupId>

<artifactId>spring-boot-starter-data-redis</artifactId>

</dependency>

<dependency>

<groupId>org.springframework.session</groupId>

<artifactId>spring-session-data-redis</artifactId>

</dependency>

pom.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>

<groupId>cn.netkiller</groupId>

<artifactId>deploy</artifactId>

<version>0.0.1-SNAPSHOT</version>

<packaging>jar</packaging>

<name>deploy.netkiller.cn</name>

<description>Deploy project for Spring Boot</description>

<parent>

<groupId>org.springframework.boot</groupId>

<artifactId>spring-boot-starter-parent</artifactId>

<version>2.3.1.RELEASE</version>

<relativePath /> <!-- lookup parent from repository -->

</parent>

<properties>

<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>

<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>

<java.version>1.8</java.version>

</properties>

<dependencies>

<!-- <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-actuator</artifactId> </dependency> -->

<!-- <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jpa</artifactId> </dependency> -->

<!-- <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-mongodb</artifactId> </dependency> -->

<dependency>

<groupId>org.springframework.boot</groupId>

<artifactId>spring-boot-starter-data-redis</artifactId>

</dependency>

<dependency>

<groupId>org.springframework.boot</groupId>

<artifactId>spring-boot-starter-data-redis</artifactId>

</dependency>

<dependency>

<groupId>org.springframework.session</groupId>

<artifactId>spring-session-data-redis</artifactId>

</dependency>

<!-- <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-jdbc</artifactId> </dependency> -->

<dependency>

<groupId>org.springframework.boot</groupId>

<artifactId>spring-boot-starter-security</artifactId>

</dependency>

<dependency>

<groupId>org.springframework.boot</groupId>

<artifactId>spring-boot-starter-web</artifactId>

</dependency>

<dependency>

<groupId>org.springframework.boot</groupId>

<artifactId>spring-boot-starter-websocket</artifactId>

</dependency>

<dependency>

<groupId>org.webjars</groupId>

<artifactId>webjars-locator</artifactId>

</dependency>

<dependency>

<groupId>org.webjars</groupId>

<artifactId>sockjs-client</artifactId>

<version>1.0.2</version>

</dependency>

<dependency>

<groupId>org.webjars</groupId>

<artifactId>stomp-websocket</artifactId>

<version>2.3.3</version>

</dependency>

<dependency>

<groupId>org.webjars</groupId>

<artifactId>bootstrap</artifactId>

<version>3.3.7</version>

</dependency>

<dependency>

<groupId>org.webjars</groupId>

<artifactId>jquery</artifactId>

<version>3.1.0</version>

</dependency>

<dependency>

<groupId>org.springframework.boot</groupId>

<artifactId>spring-boot-starter-test</artifactId>

<scope>test</scope>

</dependency>

<dependency>

<groupId>org.apache.tomcat.embed</groupId>

<artifactId>tomcat-embed-jasper</artifactId>

<scope>provided</scope>

</dependency>

<dependency>

<groupId>javax.servlet</groupId>

<artifactId>jstl</artifactId>

</dependency>

<dependency>

<groupId>mysql</groupId>

<artifactId>mysql-connector-java</artifactId>

</dependency>

<dependency>

<groupId>junit</groupId>

<artifactId>junit</artifactId>

<scope>test</scope>

</dependency>

</dependencies>

<build>

<plugins>

<plugin>

<groupId>org.springframework.boot</groupId>

<artifactId>spring-boot-maven-plugin</artifactId>

</plugin>

</plugins>

</build>

<repositories>

<repository>

<id>spring-snapshots</id>

<name>Spring Snapshots</name>

<url>https://repo.spring.io/snapshot</url>

<snapshots>

<enabled>true</enabled>

</snapshots>

</repository>

<repository>

<id>spring-milestones</id>

<name>Spring Milestones</name>

<url>https://repo.spring.io/milestone</url>

<snapshots>

<enabled>false</enabled>

</snapshots>

</repository>

</repositories>

<pluginRepositories>

<pluginRepository>

<id>spring-snapshots</id>

<name>Spring Snapshots</name>

<url>https://repo.spring.io/snapshot</url>

<snapshots>

<enabled>true</enabled>

</snapshots>

</pluginRepository>

<pluginRepository>

<id>spring-milestones</id>

<name>Spring Milestones</name>

<url>https://repo.spring.io/milestone</url>

<snapshots>

<enabled>false</enabled>

</snapshots>

</pluginRepository>

</pluginRepositories>

</project>

application.properties

spring.session.store-type=redis 将Session 存储在Redis中

spring.redis.database=0

spring.redis.host=192.168.4.1

spring.redis.port=6379

#spring.redis.password=

spring.redis.pool.max-active=8

spring.redis.pool.max-wait=30

spring.redis.pool.max-idle=8

spring.redis.pool.min-idle=0

spring.redis.timeout=10

spring.session.store-type=redis

Application

package cn.netkiller;

import org.springframework.boot.SpringApplication;

import org.springframework.boot.autoconfigure.EnableAutoConfiguration;

import org.springframework.boot.autoconfigure.SpringBootApplication;

import org.springframework.context.annotation.ComponentScan;

import org.springframework.data.jpa.repository.config.EnableJpaRepositories;

import org.springframework.data.mongodb.repository.config.EnableMongoRepositories;

import org.springframework.scheduling.annotation.EnableScheduling;

import org.springframework.session.data.redis.config.annotation.web.http.EnableRedisHttpSession;

@SpringBootApplication

@EnableAutoConfiguration

@ComponentScan

@EnableMongoRepositories

@EnableJpaRepositories

@EnableScheduling

public class Application {

public static void main(String[] args) {

SpringApplication.run(Application.class, args);

}

}

RedisHttpSessionConfig.java

package cn.netkiller.config;

import org.springframework.context.annotation.Configuration;

import org.springframework.session.data.redis.config.annotation.web.http.EnableRedisHttpSession;

@Configuration

@EnableRedisHttpSession

public class RedisHttpSessionConfig {

public RedisHttpSessionConfig() {

// TODO Auto-generated constructor stub

}

}

2. 测试 Session

package cn.netkiller.web;

import java.util.Date;

import javax.servlet.http.HttpSession;

import org.springframework.stereotype.Controller;

import org.springframework.web.bind.annotation.RequestMapping;

import org.springframework.web.bind.annotation.ResponseBody;

@Controller

public class TestController {

public TestController() {

// TODO Auto-generated constructor stub

}

@RequestMapping("/session/set")

@ResponseBody

public String set(HttpSession session) {

String key = "test";

session.setAttribute(key, new Date());

return key;

}

@RequestMapping("/session/get")

@ResponseBody

public String get(HttpSession session) {

String value = (String) session.getAttribute("test").toString();

return value;

}

}

keys spring:session:* 查看 Session Key

$ telnet 192.168.4.1 6379

Connecting to 192.168.4.1:6379...

Connection established.

To escape to local shell, press 'Ctrl+Alt+]'.

keys spring:session:*

*7

$68

spring:session:sessions:expires:a510f46f-0a2f-4649-af05-34bd750562c1

$40

spring:session:expirations:1476100200000

$40

spring:session:expirations:1476098400000

$60

spring:session:sessions:f6494a2f-591e-42ba-b381-ce2596f4046d

$60

spring:session:sessions:a510f46f-0a2f-4649-af05-34bd750562c1

$112

spring:session:index:org.springframework.session.FindByIndexNameSessionRepository.PRINCIPAL_NAME_INDEX_NAME:user

$60

spring:session:sessions:627018c8-243e-43ac-87b9-fc07f130c899

3. JDBC

spring.session.store-type=jdbc

spring.session.jdbc.table-name=SESSIONS

4. Springboot 2.1

<dependency>

<groupId>org.springframework.boot</groupId>

<artifactId>spring-boot-starter-redis</artifactId>

</dependency>

<dependency>

<groupId>org.springframework.session</groupId>

<artifactId>spring-session-data-redis</artifactId>

</dependency>

开启Redis共享SESSION @EnableRedisHttpSession

package cn.netkiller.oauth2;

import org.springframework.boot.SpringApplication;

import org.springframework.boot.autoconfigure.EnableAutoConfiguration;

import org.springframework.boot.autoconfigure.SpringBootApplication;

import org.springframework.session.data.redis.config.annotation.web.http.EnableRedisHttpSession;

@SpringBootApplication

@EnableAutoConfiguration

@EnableRedisHttpSession

public class Application {

public static void main(String[] args) {

SpringApplication.run(Application.class, args);

}

}

application.properties中配置redis服务器

spring.redis.host=localhost

spring.redis.port=6379

第 39 章 Spring boot with Caching

https://docs.spring.io/spring-boot/docs/current/reference/html/io.html#io.caching.provider.redis

1. maven

<dependency>

<groupId>org.springframework.boot</groupId>

<artifactId>spring-boot-starter-cache</artifactId>

</dependency>

Redis

使用 Redis

<dependency>

<groupId>org.springframework.boot</groupId>

<artifactId>spring-boot-starter-cache</artifactId>

</dependency>

<dependency>

<groupId>org.springframework.boot</groupId>

<artifactId>spring-boot-starter-data-redis</artifactId>

</dependency>

spring.data.redis.host=cch.netkiller.cn

spring.data.redis.port=6379

spring.data.redis.password=passw0rd

spring.data.redis.database=10

spring.data.redis.timeout=30000

spring.data.redis.lettuce.pool.max-active=8

spring.data.redis.lettuce.pool.max-wait=-1

spring.data.redis.lettuce.pool.max-idle=8

spring.data.redis.lettuce.pool.min-idle=0

spring.cache.type=redis

spring.cache.redis.time-to-live=3600000

spring.cache.redis.cache-null-values=true

2. 启用 Cache

添加 @EnableCaching

package hello;

import org.springframework.boot.SpringApplication;

import org.springframework.boot.autoconfigure.SpringBootApplication;

import org.springframework.cache.annotation.EnableCaching;

@SpringBootApplication

@EnableCaching

public class Application {

public static void main(String[] args) {

SpringApplication.run(Application.class, args);

}

}

3. @Cacheable 的用法

@Cacheable(value="users", key="#id")

public User find(Integer id) {

return null;

}

引用对象

@Cacheable(value="users", key="#user.id")

public User find(User user) {

return null;

}

条件判断

@Cacheable(value="messagecache", key="#id", condition="id < 10")

public String getMessage(int id){

return "hello"+id;

}

@Cacheable(value="test",condition="#userName.length()>2")

@Cacheable(value={"users"}, key="#user.id", condition="#user.id%2==0")

#p0 参数索引，p0表示第一个参数

@Cacheable(value="users", key="#p0")

public User find(Integer id) {

return null;

}

@Cacheable(value="users", key="#p0.id")

public User find(User user) {

return null;

}

@Cacheable 如果没有任何参数将会自动生成 key ，前提是必须设置 @CacheConfig(cacheNames = "test")

@GetMapping("/cache/auto")

@Cacheable()

public Attribute auto() {

Attribute attribute = new Attribute();

attribute.setName("sdfsdf");

return attribute;

}

127.0.0.1:6379> keys *

1) "test::SimpleKey []"

SpEL表达式

@GetMapping("/cache/expire")

@Cacheable("test1#${select.cache.timeout:1000}")

public String expire() {

return "Test";

}

@GetMapping("/cache/expire")

@Cacheable("test1#${select.cache.timeout:1000}#${select.cache.refresh:600}")

public String expire() {

return "Test";

}

排除 null 结果

使用 unless 排除 null 结果

@Cacheable(value = "translate", key = "#chinese", unless="#result == null")

public String translate(String chinese) {

}

通过配置文件设置spring.cache.redis.cache-null-values

spring.cache.redis.cache-null-values=false

4. @CachePut 用法

@CachePut 每次都会执行方法，都会将结果存入指定key的缓存中，@CachePut 不会判断是否 key 已经存在，二是始终覆盖。

@CachePut("users")

public User find(Integer id) {

return null;

}

5. 清空缓存

缓存返回结果

@Cacheable("cacheable")

@RequestMapping("/test/cacheable")

@ResponseBody

public String cacheable() {

Date date = new Date();

String message = date.toString();

return message;

}

5秒钟清楚一次缓存

@Scheduled(fixedDelay = 5000)

@CacheEvict(allEntries = true, value = "cacheable")

public void expire() {

Date date = new Date();

String message = date.toString();

System.out.println(message);

}

6. @Caching

@Caching(

cacheable = {

@Cacheable(value = "emp",key = "#p0"),

...

},

put = {

@CachePut(value = "emp",key = "#p0"),

...

},

evict = {

@CacheEvict(value = "emp",key = "#p0"),

....

}

)

public User save(User user) {

....

}

7. 解决Expire 和 TTL 过期时间

Springboot 1.x

@Bean

public CacheManager cacheManager(RedisTemplate redisTemplate) {

RedisCacheManager cacheManager = new RedisCacheManager(redisTemplate);

cacheManager.setDefaultExpiration(60); //缓存默认 60 秒

Map<String, Long> expiresMap = new HashMap<>();

expiresMap.put("Product", 5L);  //设置 key = Product 时 5秒缓存。你可以添加很多规则。

cacheManager.setExpires(expiresMap);

return cacheManager;

}

Springboot 2.x

package api.config;

import java.time.Duration;

import java.util.HashMap;

import java.util.Map;

import org.springframework.cache.CacheManager;

import org.springframework.cache.interceptor.KeyGenerator;

import org.springframework.context.annotation.Bean;

import org.springframework.context.annotation.Configuration;

import org.springframework.data.redis.cache.RedisCacheConfiguration;

import org.springframework.data.redis.cache.RedisCacheManager;

import org.springframework.data.redis.cache.RedisCacheWriter;

import org.springframework.data.redis.connection.RedisConnectionFactory;

import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;

import org.springframework.data.redis.serializer.RedisSerializationContext;

import com.fasterxml.jackson.annotation.JsonAutoDetect;

import com.fasterxml.jackson.annotation.PropertyAccessor;

import com.fasterxml.jackson.databind.ObjectMapper;

@Configuration

public class CachingConfigurer {

public CachingConfigurer() {

// TODO Auto-generated constructor stub

}

@Bean

public KeyGenerator simpleKeyGenerator() {

return (o, method, objects) -> {

StringBuilder stringBuilder = new StringBuilder();

stringBuilder.append(o.getClass().getSimpleName());

stringBuilder.append(".");

stringBuilder.append(method.getName());

stringBuilder.append("[");

for (Object obj : objects) {

stringBuilder.append(obj.toString());

}

stringBuilder.append("]");

return stringBuilder.toString();

};

}

@Bean

public CacheManager cacheManager(RedisConnectionFactory redisConnectionFactory) {

return new RedisCacheManager(RedisCacheWriter.nonLockingRedisCacheWriter(redisConnectionFactory),

this.redisCacheConfiguration(600), // 默认配置

this.initialCacheConfigurations()); // 指定key过期时间配置

}

private Map<String, RedisCacheConfiguration> initialCacheConfigurations() {

Map<String, RedisCacheConfiguration> redisCacheConfigurationMap = new HashMap<>();

redisCacheConfigurationMap.put("UserInfoList", this.redisCacheConfiguration(3000));

redisCacheConfigurationMap.put("UserInfoListAnother", this.redisCacheConfiguration(18000));

return redisCacheConfigurationMap;

}

private RedisCacheConfiguration redisCacheConfiguration(Integer seconds) {

Jackson2JsonRedisSerializer<Object> jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer<>(Object.class);

ObjectMapper om = new ObjectMapper();

om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);

om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);

jackson2JsonRedisSerializer.setObjectMapper(om);

RedisCacheConfiguration redisCacheConfiguration = RedisCacheConfiguration.defaultCacheConfig();

redisCacheConfiguration = redisCacheConfiguration.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(jackson2JsonRedisSerializer)).entryTtl(Duration.ofSeconds(seconds));

return redisCacheConfiguration;

}

}

@Cacheable(value = "DefaultKey", keyGenerator = "simpleKeyGenerator") // 600秒，使用默认策略

@Cacheable(value = "UserInfoList", keyGenerator = "simpleKeyGenerator") // 3000秒

@Cacheable(value = "UserInfoListAnother", keyGenerator = "simpleKeyGenerator") // 18000秒

127.0.0.1:6379> keys *

1) "test2::SimpleKey []"

127.0.0.1:6379> ttl "test2::SimpleKey []"

(integer) 584

第 40 章 Spring boot with Email

1. Maven

<dependency>

<groupId>org.springframework.boot</groupId>

<artifactId>spring-boot-starter-mail</artifactId>

</dependency>

例 40.1. Spring boot with Email (pom.xml)

<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>

<groupId>netkiller.cn</groupId>

<artifactId>api.netkiller.cn</artifactId>

<version>0.0.1-SNAPSHOT</version>

<packaging>jar</packaging>

<name>api.netkiller.cn</name>

<url>http://maven.apache.org</url>

<properties>

<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>

<java.version>1.8</java.version>

</properties>

<parent>

<groupId>org.springframework.boot</groupId>

<artifactId>spring-boot-starter-parent</artifactId>

<version>2.3.1.RELEASE</version>

</parent>

<dependencies>

<dependency>

<groupId>org.springframework.boot</groupId>

<artifactId>spring-boot-starter-web</artifactId>

</dependency>

<!-- <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> -->

<dependency>

<groupId>org.springframework.boot</groupId>

<artifactId>spring-boot-starter-data-jpa</artifactId>

</dependency>

<dependency>

<groupId>org.springframework.boot</groupId>

<artifactId>spring-boot-starter-jdbc</artifactId>

</dependency>

<dependency>

<groupId>org.springframework.boot</groupId>

<artifactId>spring-boot-starter-data-redis</artifactId>

</dependency>

<dependency>

<groupId>org.springframework.boot</groupId>

<artifactId>spring-boot-starter-data-mongodb</artifactId>

</dependency>

<dependency>

<groupId>org.springframework.boot</groupId>

<artifactId>spring-boot-starter-amqp</artifactId>

</dependency>

<dependency>

<groupId>org.springframework.boot</groupId>

<artifactId>spring-boot-devtools</artifactId>

</dependency>

<dependency>

<groupId>org.springframework.boot</groupId>

<artifactId>spring-boot-starter-test</artifactId>

<scope>test</scope>

</dependency>

<dependency>

<groupId>org.springframework.data</groupId>

<artifactId>spring-data-mongodb</artifactId>

</dependency>

<dependency>

<groupId>org.springframework.data</groupId>

<artifactId>spring-data-oracle</artifactId>

<version>1.0.0.RELEASE</version>

</dependency>

<dependency>

<groupId>com.oracle</groupId>

<artifactId>ojdbc6</artifactId>

<!-- <version>12.1.0.1</version> -->

<version>11.2.0.3</version>

<scope>system</scope>

<systemPath>${basedir}/lib/ojdbc6.jar</systemPath>

</dependency>

<dependency>

<groupId>mysql</groupId>

<artifactId>mysql-connector-java</artifactId>

</dependency>

<dependency>

<groupId>org.springframework.boot</groupId>

<artifactId>spring-boot-starter-mail</artifactId>

</dependency>

<dependency>

<groupId>com.google.code.gson</groupId>

<artifactId>gson</artifactId>

<scope>compile</scope>

</dependency>

<dependency>

<groupId>junit</groupId>

<artifactId>junit</artifactId>

<scope>test</scope>

</dependency>

</dependencies>

<build>

<sourceDirectory>src</sourceDirectory>

<plugins>

<plugin>

<groupId>org.springframework.boot</groupId>

<artifactId>spring-boot-maven-plugin</artifactId>

</plugin>

<plugin>

<artifactId>maven-compiler-plugin</artifactId>

<version>3.3</version>

<configuration>

<source />

<target />

</configuration>

</plugin>

<plugin>

<artifactId>maven-war-plugin</artifactId>

<version>2.6</version>

<configuration>

<warSourceDirectory>WebContent</warSourceDirectory>

<failOnMissingWebXml>false</failOnMissingWebXml>

</configuration>

</plugin>

</plugins>

</build>

</project>

2. Resource

application.properties

Postfix / Exam4 / Sendmail 邮件服务器配置

spring.mail.host=smtp.163.com

SMTP 配置

spring.mail.host=smtp.163.com

spring.mail.username=openunix@163.com

spring.mail.password=your_password

spring.mail.properties.mail.smtp.auth=true

#spring.mail.properties.mail.smtp.starttls.enable=true

#spring.mail.properties.mail.smtp.starttls.required=true

3. POJO

package api.pojo;

public class Email {

public String from;

public String to;

public String subject;

public String text;

public boolean status;

public String getFrom() {

return from;

}

public void setFrom(String from) {

this.from = from;

}

public String getTo() {

return to;

}

public void setTo(String to) {

this.to = to;

}

public String getSubject() {

return subject;

}

public void setSubject(String subject) {

this.subject = subject;

}

public String getText() {

return text;

}

public void setText(String text) {

this.text = text;

}

public boolean isStatus() {

return status;

}

public void setStatus(boolean status) {

this.status = status;

}

@Override

public String toString() {

return "Email [from=" + from + ", to=" + to + ", subject=" + subject + ", text=" + text + "]";

}

public Email() {

}

public Email(String from, String to, String subject, String text) {

super();

this.from = from;

this.to = to;

this.subject = subject;

this.text = text;

}

}

4. RestController

package api.rest;

import java.io.File;

import javax.mail.internet.MimeMessage;

import org.springframework.beans.factory.annotation.Autowired;

import org.springframework.core.io.FileSystemResource;

import org.springframework.http.HttpStatus;

import org.springframework.http.ResponseEntity;

import org.springframework.mail.SimpleMailMessage;

import org.springframework.mail.javamail.JavaMailSender;

import org.springframework.mail.javamail.MimeMessageHelper;

import org.springframework.web.bind.annotation.RequestBody;

import org.springframework.web.bind.annotation.RequestMapping;

import org.springframework.web.bind.annotation.RequestMethod;

import org.springframework.web.bind.annotation.ResponseStatus;

import org.springframework.web.bind.annotation.RestController;

import api.pojo.Email;

@RestController

@RequestMapping("/v1/email")

public class EmailRestController extends CommonRestController {

@Autowired

private JavaMailSender javaMailSender;

@RequestMapping("version")

@ResponseStatus(HttpStatus.OK)

public String version() {

return "[OK] Welcome to withdraw Restful version 1.0";

}

@RequestMapping(value = "send", method = RequestMethod.POST, produces = { "application/xml", "application/json" })

public ResponseEntity<Email> sendSimpleMail(@RequestBody Email email) {

SimpleMailMessage message = new SimpleMailMessage();

message.setFrom(email.getFrom());

message.setTo(email.getTo());

message.setSubject(email.getSubject());

message.setText(email.getText());

javaMailSender.send(message);

email.setStatus(true);

return new ResponseEntity<Email>(email, HttpStatus.OK);

}

@RequestMapping(value = "attachments", method = RequestMethod.POST, produces = { "application/xml", "application/json" })

public ResponseEntity<Email> attachments(@RequestBody Email email) throws Exception {

MimeMessage mimeMessage = javaMailSender.createMimeMessage();

MimeMessageHelper mimeMessageHelper = new MimeMessageHelper(mimeMessage, true);

mimeMessageHelper.setFrom(email.getFrom());

mimeMessageHelper.setTo(email.getTo());

mimeMessageHelper.setSubject(email.getSubject());

mimeMessageHelper.setText("<html><body><img src=\"cid:banner\" >" + email.getText() + "</body></html>", true);

FileSystemResource file = new FileSystemResource(new File("banner.jpg"));

mimeMessageHelper.addInline("banner", file);

FileSystemResource fileSystemResource = new FileSystemResource(new File("Attachment.jpg"));

mimeMessageHelper.addAttachment("Attachment.jpg", fileSystemResource);

javaMailSender.send(mimeMessage);

email.setStatus(true);

return new ResponseEntity<Email>(email, HttpStatus.OK);

}

// 如果你不想使用 application.properties 中的 spring.mail.host 配置，想自行配置SMTP主机可以参考下面例子

@RequestMapping(value = "sendmail", method = RequestMethod.POST, produces = { "application/xml", "application/json" })

public ResponseEntity<Email> sendmail(@RequestBody Email email) {

JavaMailSenderImpl javaMailSender = new JavaMailSenderImpl();

javaMailSender.setHost(email.getHost());

SimpleMailMessage message = new SimpleMailMessage();

message.setFrom(email.getFrom());

message.setTo(email.getTo());

message.setSubject(email.getSubject());

message.setText(email.getText());

try{

javaMailSender.send(message);

email.setStatus(true);

}catch(Exception e){

email.setText(e.getMessage());

email.setStatus(false);

}

return new ResponseEntity<Email>(email, HttpStatus.OK);

}

}

5. Test

$ curl -i -H "Accept: application/json" -H "Content-Type: application/json" -X POST -d '{"from":"root@netkiller.cn", "to":"21214094@qq.com","subject":"Hello","text":"Hello world!!!"}' http://172.16.0.20:8080/v1/email/send.json

HTTP/1.1 200 OK

Server: Apache-Coyote/1.1

Content-Type: application/json;charset=UTF-8

Transfer-Encoding: chunked

Date: Wed, 10 Aug 2016 06:38:00 GMT

{"from":"root@netkiller.cn","to":"21214094@qq.com","subject":"Hello","text":"Hello world!!!","status":true}

第 41 章 Spring boot with Hessian

1. Maven

<dependency>

<groupId>com.caucho</groupId>

<artifactId>hessian</artifactId>

<version>4.0.38</version>

</dependency>

2. Application

package cn.netkiller;

import org.springframework.boot.SpringApplication;

import org.springframework.boot.autoconfigure.EnableAutoConfiguration;

import org.springframework.boot.autoconfigure.SpringBootApplication;

import org.springframework.context.annotation.ComponentScan;

//import org.springframework.data.jpa.repository.config.EnableJpaRepositories;

//import org.springframework.data.mongodb.repository.config.EnableMongoRepositories;

import org.springframework.scheduling.annotation.EnableScheduling;

@SpringBootApplication

@EnableAutoConfiguration

@ComponentScan

// @EnableMongoRepositories

// @EnableJpaRepositories

@EnableScheduling

public class Application {

public static void main(String[] args) {

SpringApplication.run(Application.class, args);

}

}

3. HessianServiceExporter

package cn.netkiller.config;

import org.springframework.beans.factory.annotation.Autowired;

import org.springframework.context.annotation.Bean;

import org.springframework.context.annotation.Configuration;

import org.springframework.remoting.caucho.HessianProxyFactoryBean;

//import org.springframework.remoting.caucho.HessianProxyFactoryBean;

import org.springframework.remoting.caucho.HessianServiceExporter;

import cn.netkiller.service.HelloWorldService;

@Configuration

public class HessionConfig {

@Autowired

private HelloWorldService helloWorldService;

@Bean(name = "/HelloWorldService")

public HessianServiceExporter hessianServiceExporter() {

HessianServiceExporter exporter = new HessianServiceExporter();

exporter.setService(helloWorldService);

exporter.setServiceInterface(HelloWorldService.class);

return exporter;

}

@Bean

public HessianProxyFactoryBean helloClient() {

HessianProxyFactoryBean factory = new HessianProxyFactoryBean();

factory.setServiceUrl("http://localhost:7000/HelloWorldService");

factory.setServiceInterface(HelloWorldService.class);

return factory;

}

}

4. Service

package cn.netkiller.service;

public interface HelloWorldService {

String sayHello(String name);

}

package cn.netkiller.service.impl;

import org.springframework.stereotype.Component;

import cn.netkiller.service.HelloWorldService;

@Component

public class HelloWorldServiceImpl implements HelloWorldService {

@Override

public String sayHello(String name) {

return "Hello World! " + name;

}

}

5. RestController

package cn.netkiller.rest.hession;

import org.springframework.beans.factory.annotation.Autowired;

import org.springframework.web.bind.annotation.RequestMapping;

import org.springframework.web.bind.annotation.RestController;

import cn.netkiller.service.HelloWorldService;

@RestController

@RequestMapping("/public/hession")

public class TestRestController {

@Autowired

HelloWorldService helloWorldService;

@RequestMapping("/hello")

public String test() {

return helloWorldService.sayHello("Spring boot with Hessian.");

}

}

第 42 章 Spring boot with Async

1. Callable 实现异步

@GetMapping("/email")

public Callable<String> order() {

System.out.println("主线程开始：" + Thread.currentThread().getName());

Callable<String> result = () -> {

System.out.println("副线程开始：" + Thread.currentThread().getName());

Thread.sleep(1000);

System.out.println("副线程返回：" + Thread.currentThread().getName());

return "success";

};

System.out.println("主线程返回：" + Thread.currentThread().getName());

return result;

}

2. WebAsyncTask 实现异步

@GetMapping("/webAsyncTask")

public WebAsyncTask<String> webAsyncTask() {

log.info("外部线程：" + Thread.currentThread().getName());

WebAsyncTask<String> result = new WebAsyncTask<>(60 * 1000L, new Callable<String>() {

@Override

public String call() {

log.info("内部线程：" + Thread.currentThread().getName());

return "success";

}

});

result.onTimeout(new Callable<String>() {

@Override

public String call() {

log.info("timeout callback");

return "timeout callback";

}

});

result.onCompletion(new Runnable() {

@Override

public void run() {

log.info("finish callback");

}

});

return result;

}

3. DeferredResult 实现异步访问

private DeferredResult<String> deferredResult = new DeferredResult<String>();

@ResponseBody

@GetMapping("/receive")

public DeferredResult<String> receive() throws Exception {

return deferredResult;

}

@ResponseBody

@GetMapping("/send")

public void send() throws Exception {

deferredResult.setResult("Helloworld!!!");

}

private final List<DeferredResult<String>> deferredResultList = new ArrayList<DeferredResult<String>>();

@ResponseBody

@GetMapping("/receive")

public DeferredResult<String> receive() throws Exception {

DeferredResult<String> deferredResult = new DeferredResult<>();

//先存起来，等待触发

deferredResultList.add(deferredResult);

return deferredResult;

}

@ResponseBody

@GetMapping("/send")

public void send() throws Exception {

// 让所有hold住的请求给与响应

deferredResultList.forEach(d -> d.setResult("say hello to all"));

}

DeferredResult 与 Callback 配合使用，用来获取 Callback 返回值

@GetMapping("/tts")

@Operation(summary = "音频合成")

@ResponseBody

public DeferredResult<ResponseJson> test(@RequestParam("text") String text, @RequestParam("filename") String filename) {

DeferredResult<ResponseJson> deferredResult = new DeferredResult<ResponseJson>();

speechSynthesizerService.tts(text, new XfyunCallback() {

@Override

public void onCallback(String sid, String text) {

String audio = aliyunService.uploadMp3FromBase64(text, filename.concat(".mp3"));

ResponseJson response = new ResponseJson(true, ResponseJson.Code.SUCCESS, "", audio);

deferredResult.setResult(response);

}

});

return deferredResult;

}

4. SimpleAsyncTaskExecutor

启用异步执行 @EnableAsync

@EnableAsync

@SpringBootApplication

public class ThreadPoolApplication {

public static void main(String[] args) {

SpringApplication.run(ThreadPoolApplication.class, args);

}

}

编写异步执行代码

@Component

@Slf4j

public class AsyncTask {

@Async

public void  asyncRun() throws InterruptedException {

Thread.sleep(10);

log.info(Thread.currentThread().getName()+":处理完成");

}

}

配置线程池

默认线程池的配置很简单，配置参数如下：

spring.task.execution.pool.core-size：线程池创建时的初始化线程数，默认为8

spring.task.execution.pool.max-size：线程池的最大线程数，默认为int最大值

spring.task.execution.pool.queue-capacity：用来缓冲执行任务的队列，默认为int最大值

spring.task.execution.pool.keep-alive：线程终止前允许保持空闲的时间

spring.task.execution.pool.allow-core-thread-timeout：是否允许核心线程超时

spring.task.execution.shutdown.await-termination：是否等待剩余任务完成后才关闭应用

spring.task.execution.shutdown.await-termination-period：等待剩余任务完成的最大时间

spring.task.execution.thread-name-prefix：线程名的前缀，设置好了之后可以方便我们在日志中查看处理任务所在的线程池

具体配置含义如下：

spring.task.execution.pool.core-size=8

spring.task.execution.pool.max-size=20

spring.task.execution.pool.queue-capacity=10

spring.task.execution.pool.keep-alive=60s

spring.task.execution.pool.allow-core-thread-timeout=true

spring.task.execution.shutdown.await-termination=true

spring.task.execution.shutdown.await-termination-period=60

spring.task.execution.thread-name-prefix=task-

spring:

task:

execution:

thread-name-prefix: task- # 线程池的线程名的前缀。默认为 task- ，建议根据自己应用来设置

pool: # 线程池相关

core-size: 8 # 核心线程数，线程池创建时候初始化的线程数。默认为 8 。

max-size: 20 # 最大线程数，线程池最大的线程数，只有在缓冲队列满了之后，才会申请超过核心线程数的线程。默认为 Integer.MAX_VALUE

keep-alive: 60s # 允许线程的空闲时间，当超过了核心线程之外的线程，在空闲时间到达之后会被销毁。默认为 60 秒

queue-capacity: 200 # 缓冲队列大小，用来缓冲执行任务的队列的大小。默认为 Integer.MAX_VALUE 。

allow-core-thread-timeout: true # 是否允许核心线程超时，即开启线程池的动态增长和缩小。默认为 true 。

shutdown:

await-termination: true # 应用关闭时，是否等待定时任务执行完成。默认为 false ，建议设置为 true

await-termination-period: 60 # 等待任务完成的最大时长，单位为秒。默认为 0 ，根据自己应用来设置

5. ThreadPoolTaskExecutor 自定义线程池

Bean 注入配置线程池

import org.springframework.boot.SpringApplication;

import org.springframework.boot.autoconfigure.SpringBootApplication;

import org.springframework.context.annotation.Bean;

import org.springframework.scheduling.annotation.EnableAsync;

import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;

import java.util.concurrent.Executor;

@SpringBootApplication

@EnableAsync

public class Application {

public static void main(String[] args) {

// close the application context to shut down the custom ExecutorService

SpringApplication.run(Application.class, args).close();

}

@Bean

public Executor asyncExecutor() {

ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();

executor.setCorePoolSize(2);

executor.setMaxPoolSize(2);

executor.setQueueCapacity(500);

executor.setThreadNamePrefix("Netkiller -");

executor.initialize();

return executor;

}

@Bean("thread")

public Executor taskExecutor() {

ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();

// 设置核心线程数

executor.setCorePoolSize(5);

// 设置最大线程数

executor.setMaxPoolSize(10);

// 设置队列容量

executor.setQueueCapacity(20);

// 设置线程活跃时间（秒）

executor.setKeepAliveSeconds(60);

// 设置线程名称

executor.setThreadNamePrefix("hello-");

// 设置拒绝策略

executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());

// 等待所有任务结束后再关闭线程池

executor.setWaitForTasksToCompleteOnShutdown(true);

return executor;

}

}

设置线程池参数

最简单的配置

@SpringBootApplication

@EnableAsync

public class Application {

public static void main(String[] args) {

SpringApplication.run(Application.class, args);

}

}

@Component

public class Task {

@Async

public void doTaskOne() throws Exception {

// 业务逻辑

}

@Async

public void doTaskTwo() throws Exception {

// 业务逻辑

}

@Async("asyncExecutor")

public void doTaskThree() throws Exception {

// 业务逻辑

}

}

队列

线程池能接受多少队列？

下面配置是 executor.setQueueCapacity(10); 也就是 10个，但是实测结果跟你想的不同

package cn.netkiller.config;

import org.springframework.context.annotation.Bean;

import org.springframework.context.annotation.Configuration;

import org.springframework.scheduling.annotation.EnableAsync;

import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;

import java.util.concurrent.ThreadPoolExecutor;

@Configuration

@EnableAsync

public class ThreadPoolTaskExecutorConfiguration {

@Bean("asyncExecutor")

public ThreadPoolTaskExecutor executor() {

ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();

executor.setThreadGroupName("job");

executor.setThreadNamePrefix("async-job-");

executor.setCorePoolSize(5);

executor.setMaxPoolSize(10);

executor.setQueueCapacity(10);

executor.setKeepAliveSeconds(60);

executor.setRejectedExecutionHandler(new ThreadPoolExecutor.AbortPolicy());

executor.setAwaitTerminationSeconds(60);

executor.setWaitForTasksToCompleteOnShutdown(true);

executor.initialize();

return executor;

}

}

实测结果是，首次执行可以容纳 20 个线程，20个线程执行完毕之后，再添加任务，就只接受 10 个，超过的部分会跑出异常

Executor [java.util.concurrent.ThreadPoolExecutor@7e729046[Running, pool size = 10, active threads = 10, queued tasks = 10, completed tasks = 0]] did not accept task: org.springframework.aop.interceptor.AsyncExecutionInterceptor$$Lambda$1775/0x0000000801b6afb0@20eaccc2

这是因为线程池可以容纳 10 个任务，队列还能排队 10 个任务。

6. 定义多个线程池

package cn.netkiller.wallet.config;

import org.springframework.context.annotation.Configuration;

import org.springframework.scheduling.annotation.EnableAsync;

@Configuration

@EnableAsync

public class ExecutorConfiguration {

/** Set the ThreadPoolExecutor's core pool size. */

private int corePoolSize = 10;

/** Set the ThreadPoolExecutor's maximum pool size. */

private int maxPoolSize = 200;

/** Set the capacity for the ThreadPoolExecutor's BlockingQueue. */

private int queueCapacity = 10;

@Bean

public Executor OneAsync() {

ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();

executor.setCorePoolSize(corePoolSize);

executor.setMaxPoolSize(maxPoolSize);

executor.setQueueCapacity(queueCapacity);

executor.setThreadNamePrefix("MySimpleExecutor-");

executor.initialize();

return executor;

}

@Bean

public Executor TwoAsync() {

ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();

executor.setCorePoolSize(corePoolSize);

executor.setMaxPoolSize(maxPoolSize);

executor.setQueueCapacity(queueCapacity);

executor.setThreadNamePrefix("MyExecutor-");

// rejection-policy：当pool已经达到max size的时候，如何处理新任务

// CALLER_RUNS：不在新线程中执行任务，而是有调用者所在的线程来执行

executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());

executor.initialize();

return executor;

}

}

@Service

public class DemoAsyncServiceImpl implements DemoAsyncService {

public static Random random =new Random();

@Async("OneAsync")

public Future<String> doTaskOne() throws Exception {

System.out.println("开始做任务一");

long start = System.currentTimeMillis();

Thread.sleep(random.nextInt(10000));

long end = System.currentTimeMillis();

System.out.println("完成任务一，耗时：" + (end - start) + "毫秒");

return new AsyncResult<>("任务一完成");

}

@Async("TwoAsync")

public Future<String> doTaskTwo() throws Exception {

System.out.println("开始做任务二");

long start = System.currentTimeMillis();

Thread.sleep(random.nextInt(10000));

long end = System.currentTimeMillis();

System.out.println("完成任务二，耗时：" + (end - start) + "毫秒");

return new AsyncResult<>("任务二完成");

}

@Async

public Future<String> doTaskThree() throws Exception {

System.out.println("开始做任务三");

long start = System.currentTimeMillis();

Thread.sleep(random.nextInt(10000));

long end = System.currentTimeMillis();

System.out.println("完成任务三，耗时：" + (end - start) + "毫秒");

return new AsyncResult<>("任务三完成");

}

}

7. 自定义线程池

自定义线程池

ThreadPoolExecutor

@Bean("queueThreadPool")

public ThreadPoolExecutor queueThreadPool() {

ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(

5,

10,

60,

TimeUnit.SECONDS,

new LinkedBlockingDeque<>(10),

Executors.defaultThreadFactory(),

new ThreadPoolExecutor.AbortPolicy());

return threadPoolExecutor;

}

注入自定义线程池bean

// 注入自定义线程池bean

@Autowired

private ThreadPoolExecutor threadPoolExecutor;

threadPoolExecutor.execute(new Runnable() {

@Override

public void run() {

System.out.println("=======");

}

});

8. 设置线程名称

public static Random random = new Random();

@Async("jobExecutor")

public void doAsyncTask() throws InterruptedException {

Thread.currentThread().setName("测试线程-" + random.nextInt(1000));

System.out.println("开始做任务一");

long start = System.currentTimeMillis();

Thread.sleep(random.nextInt(100000));

long end = System.currentTimeMillis();

System.out.println("完成任务一，耗时：" + (end - start) + "毫秒");

}

9. 线程池监控

监控指标

neo@MacBook-Pro-M2 ~> curl -s http://www.netkiller.cn:8080/actuator/metrics | jq | grep executor

"executor.active",

"executor.completed",

"executor.pool.core",

"executor.pool.max",

"executor.pool.size",

"executor.queue.remaining",

"executor.queued",

获取指标

neo@MacBook-Pro-M2 ~> curl -s http://www.netkiller.cn:8080/actuator/metrics/executor.active | jq

{

"name": "executor.active",

"description": "The approximate number of threads that are actively executing tasks",

"baseUnit": "threads",

"measurements": [

{

"statistic": "VALUE",

"value": 0

}

],

"availableTags": [

{

"tag": "name",

"values": [

"asyncExecutor"

]

}

]

}

@Autowired

ThreadPoolTaskExecutor threadPoolTaskExecutor;

@GetMapping("/pool")

public String pool() {

int activeCount = threadPoolTaskExecutor.getActiveCount();

long completedTaskCount = threadPoolTaskExecutor.getThreadPoolExecutor().getCompletedTaskCount();

long taskCount = threadPoolTaskExecutor.getThreadPoolExecutor().getTaskCount();

int queue = threadPoolTaskExecutor.getThreadPoolExecutor().getQueue().size();

String monitor = String.format("Task: %d, Queue: %d, Active: %d, Completed: %d\n", taskCount, queue, activeCount, completedTaskCount);

log.info(monitor);

return monitor;

}

10. 注意事项

再 Service 中 通过 this 访问另一个 @Async 方法，异步会失效，必须使用 @@Autowired 入住。

第 43 章 Springboot with Ethereum (web3j)

1. Maven

<dependency>

<groupId>org.web3j</groupId>

<artifactId>web3j-spring-boot-starter</artifactId>

<version>1.6.0</version>

</dependency>

2. application.properties

web3j.client-address=https://ropsten.infura.io/CsS9shwaAab0z7B4LP2d

web3j.admin-client=true

3. TestRestController

package cn.netkiller.wallet.restful;

import java.io.IOException;

import org.slf4j.Logger;

import org.slf4j.LoggerFactory;

import org.springframework.beans.factory.annotation.Autowired;

import org.springframework.web.bind.annotation.GetMapping;

import org.springframework.web.bind.annotation.RestController;

import org.web3j.protocol.Web3j;

import org.web3j.protocol.core.methods.response.Web3ClientVersion;

@RestController

public class TestRestController {

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

@Autowired

private Web3j web3j;

public TestRestController() {

// TODO Auto-generated constructor stub

}

@GetMapping("/version")

public String version() throws IOException {

Web3ClientVersion web3ClientVersion = web3j.web3ClientVersion().send();

String clientVersion = web3ClientVersion.getWeb3ClientVersion();

logger.info(clientVersion);

return clientVersion;

}

}

4. 测试

neo@MacBook-Pro ~ % curl http://localhost:8080/version

Geth/v1.8.3-stable/linux-amd64/go1.10

第 44 章 Java Record 新特性

1. Record 替代 POJO 类

POJO类：

@Data

public class Point {

private String x;

private String y;

}

record 实现方式

public record Point(String x, String y) {

}

2. Record 作为 Properties

@ConfigurationProperties(prefix = "user")

public record UserProperties(String firstName, String lastName) {

}

3. Record 作为实体类

package cn.netkiller.record;

import jakarta.persistence.*;

@Entity

@Table(name = "member")

public record Member(

@Id

@GeneratedValue(strategy = GenerationType.IDENTITY)

@Column(name = "id", unique = true, nullable = false, insertable = true, updatable = false)

Long id,

String name

) {

}

4. Record 作为 Service

@Service

public record PersonService(PersonRepository personRepository){

//保存person

public Person save(Person person){

Person person = new Person(person.firstName(), person.lastName(), person.age());

return personRepository.save(person);

}

//按照lastName查询people，返回值只有firstName和lastName

public List<PersonOnlyWithName> findByLastName(String lastName){

return personRepository.findByLastName(lastName);

}

}

public interface PersonRepository extends JpaRepository<Person, Long> {

List<PersonOnlyWithName> findByLastName(String lastName);

}

5. Record 作为 Controller

@RestController

@RequestMapping("/people")

public record PersonController(PersonService personService) {

@PostMapping

public ResponseEntity<Person> save(@RequestBody Person person){

return ResponseEntity.ok(personService.save(person));

}

@GetMapping("/findByLastName")

public ResponseEntity<List<PersonOnlyWithName>> findByLastName(String lastName){

return ResponseEntity.ok(personService.findByLastName(lastName));

}

}

2. Springboot 接入阿里云

2.1. 阿里云 OSS - STS进行临时授权访问获取 HTTPS 地址

默认获取URL是 HTTP，我们的需求是需要获取HTTPS地址，当然你可以使用字符串替换操作，将 http 替换成 https，但这并不是最有解决方案。

package cn.netkiller.aliyun;

import java.net.URL;

import java.util.Date;

import com.aliyun.oss.ClientBuilderConfiguration;

import com.aliyun.oss.OSS;

import com.aliyun.oss.OSSClientBuilder;

import com.aliyun.oss.common.comm.Protocol;

public class App {

public static void main(String[] args) {

System.out.println("Hello World!");

// yourEndpoint填写Bucket所在地域对应的Endpoint。以华东1（杭州）为例，Endpoint填写为https://oss-cn-hangzhou.aliyuncs.com。

String endpoint = "oss-cn-shanghai.aliyuncs.com";

// 从STS服务获取的临时访问密钥（AccessKey ID和AccessKey Secret）。

String accessKeyId = "DYmJeLTAI5tm1ZaCEB9nUxAP";

String accessKeySecret = "QkXusBiLMoMIsW3JJhG0D5NOFBEh5a";

// 从STS服务获取的安全令牌（SecurityToken）。

String securityToken = "yourSecurityToken";

// 填写Bucket名称，例如examplebucket。

String bucketName = "production";

// 填写Object完整路径，例如exampleobject.txt。Object完整路径中不能包含Bucket名称。

String objectName = "exampleobject.txt";

ClientBuilderConfiguration clientBuilderConfiguration = new ClientBuilderConfiguration();

clientBuilderConfiguration.setProtocol(Protocol.HTTPS);

// 创建OSSClient实例。

OSS ossClient = new OSSClientBuilder().build(endpoint, accessKeyId, accessKeySecret, securityToken, clientBuilderConfiguration);

// 设置签名URL过期时间为3600秒（1小时）。

Date expiration = new Date(new Date().getTime() + 3600 * 1000);

// 生成以GET方法访问的签名URL，访客可以直接通过浏览器访问相关内容。

URL url = ossClient.generatePresignedUrl(bucketName, objectName, expiration);

System.out.println(url);

System.out.println(url.toString().replace(bucketName + "." + endpoint, "oss.netkiller.cn"));

// 关闭OSSClient。

ossClient.shutdown();

}

}

部分 II. Spring Framework

第 45 章 Spring MVC

Spring MVC 有两种启动模式，一种是传统Tomcat，需要配置很多XML文件。另一种方式是采用 Spring Boot 需要些一个Java程序，不需要写xml文件，这个程序会帮助你处理启动所需的一切，并且采用嵌入方式启动 Tomcat 或者 Jetty.

两种方式各有优缺点，Tomcat 方式配置繁琐，但是可以使用虚拟机，同一个IP地址使用不同域名访问，出现不同的内容。而Spring Boot一个应用一个容器一个端口，比不得不通过端口来区分应用。

1. @EnableWebMvc

package cn.netkiller.config;

import org.springframework.context.annotation.Bean;

import org.springframework.context.annotation.Configuration;

import org.springframework.web.servlet.config.annotation.DefaultServletHandlerConfigurer;

import org.springframework.web.servlet.config.annotation.EnableWebMvc;

import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;

import org.springframework.web.servlet.view.InternalResourceViewResolver;

@Configuration

@EnableWebMvc

public class WebMvcConfig extends WebMvcConfigurerAdapter {

@Override

public void configureDefaultServletHandling(DefaultServletHandlerConfigurer configurer) {

configurer.enable();

}

@Bean

public InternalResourceViewResolver viewResolver() {

InternalResourceViewResolver resolver = new InternalResourceViewResolver();

resolver.setPrefix("WEB-INF/jsp/");

resolver.setSuffix(".jsp");

return resolver;

}

}

1.1. CORS 跨域请求

@Configuration

public class CorsConfiguration

{

@Bean

public WebMvcConfigurer corsConfigurer()

{

return new WebMvcConfigurerAdapter() {

@Override

public void addCorsMappings(CorsRegistry registry) {

registry.addMapping("/**");

}

};

}

}

@Bean

public WebMvcConfigurer corsConfigurer() {

return new WebMvcConfigurerAdapter() {

@Override

public void addCorsMappings(CorsRegistry registry) {

registry.addMapping("/**").allowedOrigins("*");

}

};

}

1.2. Spring MVC CORS with WebMvcConfigurerAdapter

@Configuration

@EnableWebMvc

public class CorsConfiguration extends WebMvcConfigurerAdapter

{

@Override

public void addCorsMappings(CorsRegistry registry) {

registry.addMapping("/**").allowedMethods("GET", "POST");

}

}

@Configuration

@EnableWebMvc

public class AppConfig extends WebMvcConfigurerAdapter {

@Override

public void addCorsMappings(CorsRegistry registry) {

registry.addMapping("/info/**")

.allowedOrigins("http://localhost:8080", "http://localhost:8000")

.allowedMethods("POST", "GET",  "PUT", "OPTIONS", "DELETE")

.allowedHeaders("X-Auth-Token", "Content-Type")

.exposedHeaders("custom-header1", "custom-header2")

.allowCredentials(false)

.maxAge(4800);

}

}

2. @Controller

package cn.netkiller.controller;

import org.springframework.stereotype.Controller;

import org.springframework.web.bind.annotation.RequestMapping;

import org.springframework.web.servlet.ModelAndView;

@Controller

public class Welcome {

@RequestMapping("/welcome")

public ModelAndView helloWorld() {

String message = "Helloworld!!!";

return new ModelAndView("welcome", "message", message);

}

}

2.1. @RequestMapping

@RequestMapping("/welcome")

@RequestMapping(value = "/list", method =

RequestMethod.GET)

@RequestMapping("/")

package com.cf88.controller;

import org.springframework.stereotype.Controller;

import org.springframework.ui.ModelMap;

import org.springframework.web.bind.annotation.PathVariable;

import org.springframework.web.bind.annotation.RequestMapping;

import org.springframework.web.bind.annotation.RequestMethod;

@Controller

@RequestMapping("/")

public class HelloController {

@RequestMapping(value = "/{name}", method = RequestMethod.GET)

public String getMovie(@PathVariable String name, ModelMap model) {

model.addAttribute("name", name);

return "hello";

}

}

同时支持多种操作方法

@RequestMapping(value = "/name", method = { RequestMethod.GET, RequestMethod.POST })

映射多个URL

@RequestMapping({ "/news/zh-cn", "/news/zh-tw" })

@ResponseBody

public String getNewsByPath() {

return "Hello";

}

匹配通配符

@Controller

@RequestMapping("/test/*")

public class TestController {

@RequestMapping

public String default() {

return "OK";

}

}

headers

@RequestMapping(value = "/news/json", method = GET, headers = "Accept=application/json")

@ResponseBody

public String getFoosAsJsonFromBrowser() {

return "{...}";

}

curl -H "Accept:application/json,text/html"

http://localhost:8080/spring/news/json.html

2.2. @GetMapping

@GetMapping 等效与 @RequestMapping

@RequestMapping(value = "/news/list", method = GET)

范例

import org.springframework.web.bind.annotation.GetMapping;

@GetMapping("/finance/list")

public String financeList() {

return financeService.financeList();

}

@GetMapping(value = "/user",produces = MediaType.APPLICATION_JSON_UTF8_VALUE)

2.3. @PostMapping

@GetMapping 等效与 @RequestMapping

@RequestMapping(value = "/news/list", method = RequestMethod.POST)

范例

import org.springframework.web.bind.annotation.PostMapping;

@PostMapping("/finance/list")

public String financeList() {

return financeService.financeList();

}

Content-Type: multipart/form-data

@PostMapping(path = "/upload", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)

2.4. @RequestBody

原始数据

处理 raw 原始数据，例如提交的时 application/json, application/xml等

@RequestMapping(value = "/something", method = RequestMethod.PUT)

public void handle(@RequestBody String body, Writer writer) throws IOException {

writer.write(body);

}

@RequestBody 传递 List

package cn.netkiller.api.restful;

import java.util.List;

import org.springframework.web.bind.annotation.PathVariable;

import org.springframework.web.bind.annotation.RequestBody;

import org.springframework.web.bind.annotation.RequestMapping;

import org.springframework.web.bind.annotation.RequestMethod;

import org.springframework.web.bind.annotation.RestController;

@RestController

public class TestRestController {

@RequestMapping(value = "/test/list/{siteId}", method = RequestMethod.POST)

public List<String> ping(@PathVariable("siteId") int siteId, @RequestBody List<String> tags) {

System.out.println(String.format("%d, %s", siteId, tags));

return (tags);

}

}

$ curl -H "Content-Type: application/json" -X POST -d '["Neo","Netkiller"]' http://localhost:8440/test/list/22.json

["Neo","Netkiller"]

传递 Map 数据

@PostMapping("/finance/list")

public String financeList(@RequestBody Map<String,String> map) {

return financeService.financeList(map);

}

% curl -H "Content-Type: application/json" -X POST -d '{"date":"2017-11-08"}' http://localhost:8440/finance/list.json

获取 JSONObject 数据

@PostMapping(value = "/{device}/post")

public Mono<String> post(@PathVariable String device, @RequestBody JSONObject jsonObject) {

log.info(jsonObject.toString());

return Mono.just(jsonObject.toString());

}

2.5. RequestMapping with Request Parameters - @RequestParam

@RequestParam 用来处理 HTTP GET/POST 请求的变量

import

org.springframework.web.bind.annotation.RequestParam;

HTTP GET

@RequestMapping("/request/param")

@ResponseBody

public String getBarBySimplePathWithRequestParam(@RequestParam("id") long id) {

return "Get a specific Bar with id=" + id;

}

http://localhost:8080/Spring/request/param.html?id=100

HTTP POST

package cn.netkiller.controller;

import javax.servlet.http.HttpServletRequest;

import javax.servlet.http.HttpSession;

import org.springframework.stereotype.Controller;

import org.springframework.ui.ModelMap;

import org.springframework.web.bind.annotation.RequestMapping;

import org.springframework.web.bind.annotation.RequestMethod;

import org.springframework.web.bind.annotation.RequestParam;

import org.springframework.web.bind.annotation.ResponseBody;

import org.springframework.web.servlet.ModelAndView;

@Controller

public class Http {

@RequestMapping("/http/form")

public ModelAndView createCustomer(){

ModelMap model = new ModelMap();

model.addAttribute("email", "netkiller@msn.com");

model.addAttribute("phone", "13113668890");

return new ModelAndView("http/form", model);

}

@RequestMapping(value= "/http/post", method = RequestMethod.POST)

public ModelAndView saveCustomer(HttpServletRequest request,

@RequestParam(value="Email", required=false) String email,

@RequestParam(value="Password", required=false) String password,

@RequestParam(value="Phone", required=false) String phone){

ModelMap model = new ModelMap();

model.addAttribute("email", email);

model.addAttribute("password", password);

model.addAttribute("phone", phone);

return new ModelAndView("http/post", model);

}

}

http/form.jsp

<%@ page language="java" contentType="text/html; charset=UTF-8"

pageEncoding="UTF-8"%>

<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">

<html>

<head>

<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">

<title>Insert title here</title>

</head>

<body>

<form method="POST"

action="http://localhost:8080/Spring/http/post.html" id="Register"

name="Register">

Email: <input class="register" type="text" id="Email" name="Email" value="${email}" /> <br />

Password： <input class="register" type="password" id="Password" name="Password" value="" /><br />

Phone： <input class="register" type="text" id="Phone" name="Phone" value="${phone}" /> <br />

<input type="submit" id="btnRegister" name="btnRegister" value="Register" style="cursor: pointer" />

</form>

</body>

</html>

http/post.jsp

<%@ page language="java" contentType="text/html; charset=UTF-8"

pageEncoding="UTF-8"%>

<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">

<html>

<head>

<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">

<title>Insert title here</title>

</head>

<body>

${email}<br>

${password} <br>

${phone} <br>

</body>

</html>

@RequestParam 传递特殊字符串

URL 中 “+” 有特殊意义，表示空格。

如果 @RequestParam 传递参数含有空格可以这样处理。

@RequestMapping("/RequestParam")

@ResponseBody

public String query(@RequestParam("code") String code) {

return code.replace(" ", "+");

}

传递日期参数

@RequestMapping("/range")

public ModelAndView range(@RequestParam("beginDate") @DateTimeFormat(pattern = "yyyy-MM-dd") Date beginDate, @RequestParam("endDate") @DateTimeFormat(pattern = "yyyy-MM-dd") Date endDate) {

log.info("===== Begin ===== {}", beginDate);

// 你的逻辑

log.info("===== End ===== {}", endDate);

return new ModelAndView("activity/index", "message", "操作成功");

}

上传文件

package cn.netkiller.restful;

import java.io.IOException;

import org.slf4j.Logger;

import org.slf4j.LoggerFactory;

import org.springframework.web.bind.annotation.GetMapping;

import org.springframework.web.bind.annotation.PathVariable;

import org.springframework.web.bind.annotation.PostMapping;

import org.springframework.web.bind.annotation.RequestMapping;

import org.springframework.web.bind.annotation.RequestParam;

import org.springframework.web.bind.annotation.RestController;

import org.springframework.web.client.RestTemplate;

import org.springframework.web.multipart.MultipartFile;

@RestController

@RequestMapping("/upload")

public class UploadRestController {

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

public UploadRestController() {

// TODO Auto-generated constructor stub

}

@PostMapping("/add")

public String fileUpload(@RequestParam("file") MultipartFile multipartFile) throws IOException {

String name = multipartFile.getOriginalFilename();

System.out.println("File name: " + name);

// todo save to a file via multipartFile.getInputStream()

byte[] bytes = multipartFile.getBytes();

System.out.println("File uploaded content:\n" + new String(bytes));

return "file uploaded";

}

}

操作演示，首先创建一个文件

echo "Helloworld!!!" > hello.txt

上传该文件

neo@MacBook-Pro /tmp % curl "http://localhost:8080/upload/add" \

-X POST \

-H "Content-Type: multipart/form-data" \

-F file=@"hello.txt"

file uploaded

@RequestParam - POST 数组

HTTP 头

picture[]: gather/293a93baa02cb18a840631bac1f9eeb20b7d436f.jpeg

picture[]: gather/be7572e4df527b4389d605766ea65aafcf2d822a.jpg

@PostMapping("/save")

public String save(@RequestParam(value = "picture[]", required = true) String[] picture) {

return String.join(",", picture);

}

默认值

@RequestParam(name = "name", defaultValue = "xxx") String name

是否非必须

使用 required = false 标注参数是非必须的

@RequestParam(name = "age", required = false) Integer age

用 Map 接收 From 数据

@PostMapping("/token")

@ResponseBody

public String token(@RequestParam Map<String, String> params) {

log.debug(params.toString());

String token = jwtTokeComponent.getTestToken(params.get("appId"), params.get("appKey"), params.get("subject"), params.get("audience"));

return token;

}

2.6. @RequestHeader - 获取 HTTP Header 信息

@RequestMapping("/displayHeaderInfo")

public void displayHeaderInfo(@RequestHeader("Accept-Encoding") String encoding,

@RequestHeader("Keep-Alive") long keepAlive)  {

//...

}

获取用户当前语言

@GetMapping("/lang")

public String language(@RequestHeader("Accept-Language") String locale ) {

System.out.println(locale);

return locale;

}

下面代码可以获得相同效果

@GetMapping("/lang")

public String language(Locale locale) {

System.out.println(locale);

return locale;

}

@GetMapping("/lang")

public String language() {

String locale = LocaleContextHolder.getLocale().toString();

System.out.println(locale);

return locale;

}

@RequestHeader 从 Http 头中获取变量

@PostMapping(value = "/token")

public TokenResponse token(@RequestParam String symbol, @RequestHeader String token) {

TokenResponse tokenResponse = walletService.getTokenBySymbol(symbol);

return tokenResponse;

}

2.7. RequestMapping with Path Variables - @PathVariable

PATHINFO 变量可通过 @Pathvariable注解绑定它传过来的值到方法的参数上。

URL 参数传递

需求，我们需要通过URL传递参数，所传递的值是分类ID与文章ID，例如 /news/1.html, /news/1/1.html。

package cn.netkiller.controller;

import org.springframework.stereotype.Controller;

import org.springframework.ui.ModelMap;

import org.springframework.web.bind.annotation.PathVariable;

import org.springframework.web.bind.annotation.RequestMapping;

import org.springframework.web.servlet.ModelAndView;

@Controller

public class Pathinfo {

@RequestMapping("/pathinfo/{id}")

public ModelAndView urlTestId(@PathVariable String id) {

return new ModelAndView("pathinfo/param", "id", id);

}

@RequestMapping("/pathinfo/{cid}/{id}")

public ModelAndView urlTestId(@PathVariable String cid, @PathVariable String id) {

ModelMap model = new ModelMap();

model.addAttribute("cid", cid);

model.addAttribute("id", id);

return new ModelAndView("pathinfo/param", model);

}

}

jsp测试文件

<%@ page language="java" contentType="text/html; charset=UTF-8"

pageEncoding="UTF-8"%>

<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">

<html>

<head>

<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">

<title>Insert title here</title>

</head>

<body>

${ cid } <br>

${ id } <br>

</body>

</html>

默认值

required 设置参数不是必须的

@PathVariable(required = false) String id

设置多个映射

@RequestMapping(value = {"/organization/{pageNumber}", "/organization"} , method = RequestMethod.GET)

public String list(@PathVariable(required = false) Integer pageNumber, ModelMap modelMap){

...

}

URL 传递 Date 类型

http://localhost:7000/history/2016-09-28%2000:00:00/

@RequestMapping("/history/{datetime}")

public String history(@PathVariable @DateTimeFormat(pattern="yyyy-MM-dd HH:mm:ss") Date datetime) throws Exception {

System.out.println(datetime)

return null;

}

处理特殊字符

http://www.netkiller.cn/release/1.0.1

@RequestMapping(value = "/release/{version:[a-zA-Z0-9\\.]+}", method = RequestMethod.GET)

public @ResponseBody

String release(@PathVariable String version) {

log.debug("version: ", version);

return version;

}

http://www.netkiller.cn/release/1.0.1/other

@RequestMapping(value="/release/{version:.+}",method=RequestMethod.GET)

public void download(HttpSession

session,@PathVariable("version")String version){

return version;

}

@PathVariable 注意事项

@PathVariable 参数传统需要注意，参数中不能携带 “/”,斜杠会被视为目录。

@RequestMapping("/PathVariable/{code}.html")

@ResponseBody

public String urlTestId(@PathVariable String code) {

return code;

}

2.8. @MatrixVariable注解，RFC3986定义URI的路径(Path)中可包含name-value片段

/*

请求为/netkiller/color=123,321

那么color值为123,321

*/

@RequestMapping(path="/netkiller/{id}", method=RequestMethod.GET)

public String test1(@MatrixVariable Integer[] color){}

/*

请求为/netkiller/color=123;color=321

那么color值为123,321

*/

@RequestMapping(path="/netkiller/{id}", method=RequestMethod.GET)

public String test1(@MatrixVariable Integer[] color){}

2.9. @ModelAttribute

@ModelAttribute 处理 HTML FORM POST 提交

package cn.netkiller.pojo;

import java.util.List;

public class Deploy {

private String group;

private String envionment;

private String project;

private List<String> arguments;

public Deploy() {

// TODO Auto-generated constructor stub

}

// Getters & Setters

}

@RequestMapping(value="/deploy/post",  method = RequestMethod.POST)

public ModelAndView post(@ModelAttribute("deploy")Deploy deploy, BindingResult result) {

if (result.hasErrors()) {

System.out.println(result.toString());

}

System.out.println(deploy.toString());

return new ModelAndView("output").addObject("output", deploy.toString());

}

2.10. @ResponseBody

import org.springframework.web.bind.annotation.ResponseBody;

直接返回HTML

package cn.netkiller.controller;

import org.springframework.stereotype.Controller;

import org.springframework.ui.ModelMap;

import org.springframework.web.bind.annotation.PathVariable;

import org.springframework.web.bind.annotation.RequestMapping;

import org.springframework.web.bind.annotation.ResponseBody;

import org.springframework.web.servlet.ModelAndView;

@Controller

public class Pathinfo {

@RequestMapping(value = "/news/shenzhen/{numericId:[\\d]+}")

@ResponseBody

public String getNewsWithPathVariable(@PathVariable final long numericId) {

return "Get a specific Bar with id=" + numericId;

}

}

2.11. @ResponseStatus 设置 HTTP 状态

@RequestMapping(value = "/", method = RequestMethod.POST)

@ResponseStatus(HttpStatus.CREATED)

public String create(@RequestBody MultiValueMap<String, String> map) {

return "OK";

}

2.12. @CrossOrigin

@CrossOrigin(origins = "http://localhost:9000")

@GetMapping("/greeting")

public Greeting greeting(@RequestParam(required=false, defaultValue="World") String name) {

System.out.println("==== in greeting ====");

return new Greeting(counter.incrementAndGet(), String.format(template,name));

}

@CrossOrigin(origins = "*", allowedHeaders = "*")

@RestController

public class HomeController

{

@GetMapping(path="/")

public String home() {

return "home";

}

}

全局放行所有轻松，方法权限单独控制

@RestController

@CrossOrigin(origins = "*", allowedHeaders = "*")

public class HomeController

{

@CrossOrigin(origins = "http://example.com")

@GetMapping(path="/")

public String home() {

return "home";

}

}

maxAge

@CrossOrigin(origins = {"http://localhost:8585"}, maxAge = 4800, allowCredentials = "false")

@RestController

@RequestMapping("/info")

public class PersonController {

@Autowired

private PersonService service;

@CrossOrigin(origins = {"http://localhost:8080"}, maxAge = 6000)

@RequestMapping("home")

public List<Person> show() {

List<Person> list = service.getAllPerson();

return list;

}

}

2.13. @CookieValue - 获取 Cookie 值

@RequestMapping("/sessionInfo")

public void sessionInfo(@CookieValue("JSESSIONID") String cookie)  {

//...

}

2.14. @SessionAttributes

@SessionAttributes: 该注解用来绑定HttpSession中的attribute对象的值，便于在方法中的参数里使用。 该注解有value、types两个属性，可以通过名字和类型指定要使用的attribute 对象；

@Controller

@RequestMapping("/editProfile")

@SessionAttributes("profile")

public class ProfileForm {

// ...

}

@Controller

@SessionAttributes("myRequestObject")

public class MyController {

...

}

2.15. ModelAndView

变量传递

@RequestMapping("/testString")

public ModelAndView helloWorld() {

String message = "Helloworld!!!";

return new

ModelAndView("welcome", "message", message);

}

public ModelAndView handleRequestInternal() {

ModelAndView mav = new ModelAndView("test");//

实例化一个VIew的ModelAndView实例

mav.addObject("variable", "Hello World!");//

添加一个带名的model对象

return mav;

}

ModelMap 传递多个变量

传递多个字符串

@RequestMapping("/testModelMap")

public ModelAndView testModelMap() {

ModelMap model = new ModelMap();

model.addAttribute("username", "Neo");

model.addAttribute("password", "Netkiller");

return new ModelAndView("test/modelmap", model);

}

推荐使用ModelMap

@RequestMapping("/testMapString")

public ModelAndView testMapString() {

Map<String,String> data = new HashMap<String,String>();

data.put("username","Neo");

data.put("password","Netkiller");

return new ModelAndView("test/modelmap",data);

}

<%@ page language="java" contentType="text/html; charset=ISO-8859-1"

pageEncoding="ISO-8859-1"%>

<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">

<html>

<head>

<meta http-equiv="Content-Type" content="text/html; charset=ISO-8859-1">

<title>Insert title here</title>

</head>

<body>

${username}<br>

${password}<br>

</body>

</html>

redirect

@RequestMapping("/testRedirect")

public ModelAndView testRedirect(){

RedirectView view = new RedirectView("testMapString.html");

return new ModelAndView(view);

}

ArrayList

@RequestMapping(value = "testList")

public ModelAndView testList() {

ModelAndView mav = new ModelAndView();

mav.setViewName("/test/list");

// List

List<String> list = new ArrayList<String>();

list.add("java");

list.add("c++");

list.add("oracle");

mav.addObject("bookList", list);

return mav;

}

<%@ page language="java" contentType="text/html; charset=ISO-8859-1"

pageEncoding="ISO-8859-1"%>

<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c"%>

<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">

<html>

<head>

<meta http-equiv="Content-Type" content="text/html; charset=ISO-8859-1">

<title>Insert title here</title>

</head>

<body>

${bookList}

<br>

<c:forEach items="${bookList}" var="node">

<c:out value="${node}"></c:out><br>

</c:forEach>

</body>

</html>

HashMap

@RequestMapping("/testMap")

public ModelAndView testMap() {

ModelAndView mav = new ModelAndView();

mav.setViewName("test/map"); // 返回的文件名

// Map

Map<String, String> map = new HashMap<String, String>();

map.put("Java", "http://www.netkiller.cn/java");

map.put("PHP", "http://www.netkiller.cn/php");

map.put("Home", "http://www.netkiller.cn");

mav.addObject("channel", map);

return mav;

}

<%@ page language="java" contentType="text/html; charset=utf-8"

pageEncoding="utf-8"%>

<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c"%>

<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">

<html>

<head>

<meta http-equiv="Content-Type" content="text/html; charset=ISO-8859-1">

<title>Insert title here</title>

</head>

<body>

<c:forEach items="${channel}" var="node">

<a href="<c:out value="${node.value}"></c:out>"><c:out value="${node.key}"></c:out></a>

<br/>

</c:forEach>

</body>

</html>

传递对象

@RequestMapping("/testObject")

public ModelAndView testObject() {

ModelMap model = new ModelMap();

User user = new User("neo", "passw0rd");

model.addAttribute("user", user);

return new ModelAndView("test/object", model);

}

package cn.netkiller;

public class User {

public String username;

public String password;

public User(String username, String password){

this.username = username;

this.password = password;

}

public String getUsername(){

return this.username;

}

public String getPassword(){

return this.password;

}

}

<%@ page language="java" contentType="text/html; charset=ISO-8859-1"

pageEncoding="ISO-8859-1"%>

<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">

<html>

<head>

<meta http-equiv="Content-Type" content="text/html; charset=ISO-8859-1">

<title>Insert title here</title>

</head>

<body>

Username: ${user.username}<br>

Password: ${user.password}<br>

</body>

</html>

2.16. HttpServletRequest / HttpServletResponse

HttpServletResponse

HttpServletResponse 实例

package cn.netkiller.api.rest;

import com.google.zxing.BarcodeFormat;

import com.google.zxing.EncodeHintType;

import com.google.zxing.MultiFormatWriter;

import com.google.zxing.WriterException;

import com.google.zxing.common.BitMatrix;

import api.util.MatrixToImageWriter;

import org.slf4j.Logger;

import org.slf4j.LoggerFactory;

import org.springframework.stereotype.Controller;

import org.springframework.web.bind.annotation.*;

import org.springframework.web.servlet.ModelAndView;

import javax.servlet.ServletOutputStream;

import javax.servlet.http.HttpServletResponse;

import java.io.IOException;

import java.util.Hashtable;

@Controller

@RequestMapping("/public/QRCode")

public class QRCodeController {

private static final Logger log = LoggerFactory.getLogger(QRCodeController.class);

@RequestMapping("/create/{code}" )

@ResponseBody

public void create(@PathVariable String code, HttpServletResponse httpServletResponse) throws WriterException, IOException {

log.info(code);

if (code != null && !"".equals(code)){

ServletOutputStream stream = null;

try {

String text = code; // 二维码内容

int width = 300; // 二维码图片宽度

int height = 300; // 二维码图片高度

String format = "gif"; // 二维码的图片格式

Hashtable<EncodeHintType, String> hints = new Hashtable<EncodeHintType, String>();

hints.put(EncodeHintType.CHARACTER_SET, "utf-8");   // 内容所使用字符集编码

BitMatrix bitMatrix = new MultiFormatWriter().encode(text, BarcodeFormat.QR_CODE, width, height, hints);

// 生成二维码

stream = httpServletResponse.getOutputStream();

MatrixToImageWriter.writeToStream(bitMatrix, format, stream);

}catch (WriterException e) {

e.printStackTrace();

} finally {

if (stream != null) {

stream.flush();

stream.close();

}

}

}

}

@RequestMapping("show")

@ResponseBody

public ModelAndView show(){

return new ModelAndView("/qrcode/qrcode");

}

}

HttpServletRequest

package com.example.demo.controller;

import java.io.IOException;

import java.util.Enumeration;

import java.util.HashMap;

import java.util.Map;

import javax.servlet.http.HttpServletRequest;

import org.springframework.web.bind.annotation.GetMapping;

import org.springframework.web.bind.annotation.PostMapping;

import org.springframework.web.bind.annotation.RequestHeader;

import org.springframework.web.bind.annotation.RequestMapping;

import org.springframework.web.bind.annotation.RestController;

@RestController

@RequestMapping("/api/test")

public class TestController {

public TestController() {

// TODO Auto-generated constructor stub

}

@GetMapping("/get")

public String get(@RequestHeader String lang) throws IOException {

System.out.println(lang);

return lang;

}

@PostMapping("/post")

public String post(@RequestHeader String lang) throws IOException {

System.out.println(lang);

return lang;

}

@GetMapping("/list")

public Map<String, String> x(HttpServletRequest request) throws IOException {

return getHeadersInfo(request);

}

private Map<String, String> getHeadersInfo(HttpServletRequest request) {

Map<String, String> map = new HashMap<String, String>();

Enumeration<?> headerNames = request.getHeaderNames();

while (headerNames.hasMoreElements()) {

String key = (String) headerNames.nextElement();

String value = request.getHeader(key);

map.put(key, value);

}

return map;

}

}

3. @RestController

3.1. 上传文件

@PostMapping("/{device}/question/voice")

public String questionVoice(@PathVariable String device, @RequestParam("file") MultipartFile file, @RequestParam("session") String session) {

if (file.isEmpty()) {

logger.error("上传失败，请选择文件");

}

String fileName = file.getOriginalFilename();

String filePath = "/tmp/".concat(session.concat(".mp3"));

File saveAs = new File(filePath);

try {

file.transferTo(saveAs);

logger.info("上传成功 " + fileName);

} catch (IOException e) {

logger.error(e.toString(), e);

return "上传失败";

}

return "上传成功";

}

3.2. 返回实体

@RequestMapping("/get/{id}")

public Member getStatistics(@PathVariable long id) {

Member statistics = memberRepostitory.findOne(id);

if (statistics == null) {

statistics = new Member();

}

return statistics;

}

3.3. JSON

MediaType.APPLICATION_JSON_VALUE 执行结果反馈json数据

@RestController

@RequestMapping("/api/persons")

public class MainController {

@RequestMapping(

value = "/detail/{id}",

method = RequestMethod.GET,

produces = MediaType.APPLICATION_JSON_VALUE

)

public ResponseEntity<Persons> getUserDetail(@PathVariable Long id) {

Persons user = personsRepository.findById(id);

return new ResponseEntity<>(user, HttpStatus.OK);

}

}

3.4. 处理原始 RAW JSON 数据

@RequestMapping(value = "/create", method = RequestMethod.POST, produces = { "application/xml", "application/json" })

public ResponseEntity<Member> create(@RequestBody Member member) {

memberRepository.save(member);

return new ResponseEntity<Member>(member, HttpStatus.OK);

}

3.5. 返回 JSON 对象 NULL 专为 "" 字符串

package api.config;

import java.io.IOException;

import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;

import org.springframework.context.annotation.Bean;

import org.springframework.context.annotation.Configuration;

import org.springframework.context.annotation.Primary;

import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder;

import com.fasterxml.jackson.core.JsonGenerator;

import com.fasterxml.jackson.core.JsonProcessingException;

import com.fasterxml.jackson.databind.JsonSerializer;

import com.fasterxml.jackson.databind.ObjectMapper;

import com.fasterxml.jackson.databind.SerializerProvider;

@Configuration

public class JacksonConfig {

@Bean

@Primary

@ConditionalOnMissingBean(ObjectMapper.class)

public ObjectMapper jacksonObjectMapper(Jackson2ObjectMapperBuilder builder) {

ObjectMapper objectMapper = builder.createXmlMapper(false).build();

objectMapper.getSerializerProvider().setNullValueSerializer(new JsonSerializer<Object>() {

@Override

public void serialize(Object o, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) throws IOException, JsonProcessingException {

jsonGenerator.writeString("");

}

});

return objectMapper;

}

}

3.6. XML

restful 将同时支持 json 和 xml 数据传递

package com.example.api.restful;

import java.util.List;

import org.springframework.beans.factory.annotation.Autowired;

import org.springframework.data.domain.PageRequest;

import org.springframework.http.HttpStatus;

import org.springframework.http.ResponseEntity;

import org.springframework.web.bind.annotation.PathVariable;

import org.springframework.web.bind.annotation.RequestMapping;

import org.springframework.web.bind.annotation.RequestMethod;

import org.springframework.web.bind.annotation.RestController;

import com.example.api.domain.RecentRead;

import com.example.api.repository.RecentReadRepostitory;

@RestController

@RequestMapping("/restful/article")

public class ArticleRestController {

@Autowired

private RecentReadRepostitory recentReadRepostitory;

@RequestMapping(value = "/recent/read/add/{memberId}/{articleId}", method = RequestMethod.GET, produces = { "application/xml", "application/json" })

public ResponseEntity<RecentRead> recentAdd(@PathVariable long memberId, @PathVariable long articleId) {

RecentRead recentRead = new RecentRead();

recentRead.setMemberId(memberId);

recentRead.setArticleId(articleId);

recentReadRepostitory.save(recentRead);

return new ResponseEntity<RecentRead>(recentRead, HttpStatus.OK);

}

@RequestMapping(value="/recent/read/list/{id}", produces = { "application/xml", "application/json" })

public List<RecentRead> recentList(@PathVariable long id) {

int page = 0;

int limit = 20;

List<RecentRead> recentRead = recentReadRepostitory.findByMemberId(id, new PageRequest(page, limit));

return recentRead;

}

}

3.7. 兼容传统 json 接口

开发中发现很多人不适应新的接口方式，有时候只能妥协，这些顽固不化的人需要这样的数据库格式

{

"status":true,

"reason":"登录成功",

"code":1,

"data":{

"id":2,

"name":null,

"sex":null,

"age":0,

"wechat":null,

"mobile":"13113668890",

"picture":null,

"ipAddress":"0:0:0:0:0:0:0:1"

}

}

返回数据必须放在 data 字典中, 而我通常是采用 http status code 来返回状态，返回结果是对象。实现上面的需求我们需要加入一个data成员变量，因为我们不清楚最终要返回什么对象。所以声明为 java.lang.Object

package com.example.api.pojo;

import java.io.Serializable;

public class RestfulResponse implements Serializable {

/**

*

*/

private static final long serialVersionUID = -4045645995352698349L;

private boolean status;

private String reason;

private int code;

private Object data;

public RestfulResponse(boolean status, int code, String reason, Object data) {

this.status = status;

this.code = code;

this.reason = reason;

this.data = data;

}

public boolean isStatus() {

return status;

}

public void setStatus(boolean status) {

this.status = status;

}

public String getReason() {

return reason;

}

public void setReason(String reason) {

this.reason = reason;

}

public int getCode() {

return code;

}

public void setCode(int code) {

this.code = code;

}

public Object getData() {

return data;

}

public void setData(Object data) {

this.data = data;

}

@Override

public String toString() {

return "RestfulResponse [status=" + status + ", reason=" + reason + ", code=" + code + ", data=" + data + "]";

}

}

Service

public RestfulResponse bindWechat(String mobile, String wechat) {

Member member = memberRepository.findByMobile(mobile);

member.setWechat(wechat);

memberRepository.save(member);

return new RestfulResponse(true, 1, "微信绑定成功", member);

}

Controller

@RequestMapping("/login/sms/{mobile}/{code}")

public RestfulResponse sms(@PathVariable String mobile, @PathVariable String wechat) {

return memberService.bindWechat(mobile, wechat);

}

3.8. 上传文件

spring.servlet.multipart.max-file-size=128KB

spring.servlet.multipart.max-request-size=128KB

spring.http.multipart.enabled=false

RestController

package api.restful;

import java.io.File;

import java.io.IOException;

import java.nio.file.Files;

import java.nio.file.Path;

import java.nio.file.Paths;

import java.util.ArrayList;

import java.util.List;

import org.springframework.web.bind.annotation.PostMapping;

import org.springframework.web.bind.annotation.RequestMapping;

import org.springframework.web.bind.annotation.RequestParam;

import org.springframework.web.bind.annotation.RestController;

import org.springframework.web.multipart.MultipartFile;

import org.springframework.web.servlet.mvc.support.RedirectAttributes;

import api.pojo.RestfulResponse;

@RestController

@RequestMapping("/upload")

public class UploadRestController {

private final static String FOLDER = "/tmp";

public UploadRestController() {

}

@PostMapping("/single")

public RestfulResponse upload(@RequestParam("file") MultipartFile file, RedirectAttributes redirectAttributes) {

if (file.isEmpty()) {

return new RestfulResponse(false, 0, "Please select a file to upload", "");

}

try {

byte[] bytes = file.getBytes();

Path path = Paths.get(FOLDER + "/" + file.getOriginalFilename());

Files.write(path, bytes);

return new RestfulResponse(true, 0, "", path.toString());

} catch (Exception e) {

return new RestfulResponse(false, 0, e.getMessage(), null);

}

}

@PostMapping(value = "/group")

public RestfulResponse group(@RequestParam("files") MultipartFile[] files) {

List<String> filelist = new ArrayList<String>();

try {

for (MultipartFile file : files) {

File tmpfile = new File(FOLDER + "/" + file.getOriginalFilename());

file.transferTo(tmpfile);

filelist.add(tmpfile.getPath());

}

return new RestfulResponse(true, 0, null, filelist);

} catch (Exception e) {

return new RestfulResponse(false, 0, e.getMessage(), null);

}

}

}

由于上传文件名可能存在空格等特殊字符，这里使用UUID替代文件名

@PostMapping(value = "/file")

public RestfulResponse file(@RequestParam("file") MultipartFile[] files) {

List<Object> filelist = new ArrayList<Object>();

try {

for (MultipartFile file : files) {

UUID uuid = UUID.randomUUID();

String filename = String.format("%s/%s.%s", folder, uuid.toString(), this.getExtensionName(filename));

File tmpfile = new File(filename);

String filepath = tmpfile.getPath();

System.out.println(filepath);

file.transferTo(tmpfile);

filelist.add(tmpfile.toString());

}

return new RestfulResponse(true, 0, null, filelist);

} catch (Exception e) {

return new RestfulResponse(false, 0, e.getMessage(), null);

}

}

private String getExtensionName(String filename) {

if ((filename != null) && (filename.length() > 0)) {

int dot = filename.lastIndexOf('.');

if ((dot > -1) && (dot < (filename.length() - 1))) {

return filename.substring(dot + 1);

}

}

return filename;

}

获取文件名及后缀信息

MultipartFile file = new MultipartFile();

String file = file.getOriginalFilename()

获取文件名

MultipartFile file = new MultipartFile();

String fileName = file.getOriginalFilename().substring(0,file.getOriginalFilename().lastIndexOf("."))

获取文件后缀

MultipartFile file = new MultipartFile();

String fileSuffix = file.getOriginalFilename().substring(file.getOriginalFilename().lastIndexOf("."))

获取文件类型

MultipartFile file = new MultipartFile();

String fileType = file.getContentType()

获取文件大小

MultipartFile file = new MultipartFile();

String fileSize = file.getSize()

3.9. Spring boot with csv

下面是一个导出 CSV 文件的例子

@GetMapping("/export")

public void export(HttpServletResponse response) throws IOException {

response.setContentType("application/csv");

// response.setContentType("application/csv;charset=gb18030");

response.setHeader("Content-Disposition", "attachment; filename=\"file.csv\"");

BufferedWriter writer = new BufferedWriter(response.getWriter());

// 需要写入 utf8bom 头否则会出现中文乱码

// byte[] uft8bom = { (byte) 0xef, (byte) 0xbb, (byte) 0xbf };

String bom = new String(new byte[] { (byte) 0xEF, (byte) 0xBB, (byte) 0xBF });

writer.write(bom);

writer.write("A,B,C");

writer.newLine();

tableRepository.findAll().forEach(table -> {

try {

String tmp = String.format("%s,%s,%s", table.getId(), table.getMethod(), table.getMoney());

writer.write(tmp);

writer.newLine();

} catch (IOException e) {

// TODO Auto-generated catch block

e.printStackTrace();

}

});

writer.flush();

writer.close();

}

3.10. Problem Details [RFC 7807]

HTTP RFC 7807 规范：https://www.rfc-editor.org/rfc/rfc7807。这个规范里定义了HTTP API的“问题细节”（Problem Details）内容。用它来携带HTTP错误返回信息，避免自定义新的错误返回格式。

HTTP/1.1 403 Forbidden

Content-Type: application/problem+json

Content-Language: en

{

"status": 403,

"type": "https://bankname.com/common-problems/low-balance",

"title": "You not have enough balance",

"detail": "Your current balance is 30 and you are transterring 50",

"instance": "/account-transfer-service"

}

type： 问题的类型；

title： 问题类型描述；

status： HTTP状态码；

detail： 问题实例描述；

instance： URI的内容应该用来描述问题实例，但不是必须的。

@GetMapping("/ProblemDetail/v1/{id}")

public ResponseEntity config(@PathVariable("id") Long id) {

if (id < 100) {

return ResponseEntity.ok(new Member(id, "netkiller"));

} else {

ProblemDetail pd = ProblemDetail.forStatusAndDetail(HttpStatus.NOT_FOUND, "Member id '" + id + "' does no exist");

pd.setType(URI.create("https://www.netkiller.cn/errors/not-found"));

pd.setTitle("Record Not Found");

pd.setProperty("hostname", "www.netkiller.cn");

return ResponseEntity.status(404).body(pd);

}

}

@GetMapping(path = "/ProblemDetail/v2/{id}")

public ResponseEntity getEmployeeById_V3(@PathVariable("id") Long id) {

try {

//somthing threw this exception

throw new NullPointerException("Something was expected but it was null");

} catch (NullPointerException npe) {

ProblemDetail pd = ProblemDetail

.forStatusAndDetail(HttpStatus.INTERNAL_SERVER_ERROR,

"Null Pointer Exception");

pd.setType(URI.create("https://www.netkiller.cn/errors/npe"));

pd.setTitle("Null Pointer Exception");

pd.setProperty("hostname", "www.netkiller.cn");

throw new ErrorResponseException(HttpStatus.NOT_FOUND, pd, npe);

}

}

ResponseEntity

@PostMapping(path = "/foo", params = {"id", "name=John"})

public ResponseEntity<String> handlePostRequest() {

// 处理请求

return ResponseEntity.ok("Success");

}

status

return ResponseEntity

.status(HttpStatus.CREATED)

.header("Location", locationUri)

.body("Employee created successfully. Location: " + locationUri);

3.11. Jackson

Jackson 相关配置

#序列化时间格式

spring.jackson.date-format=yyyy-MM-dd HH:mm:ss

spring.mvc.date-format=yyyy-MM-dd HH:mm:ss

#mvc序列化时候时区选择

spring.jackson.time-zone=GMT+8

@JsonIgnore 返回json是不含有该字段

@JsonIgnore

private String entityName = this.getClass().getSimpleName();

@JsonFormat 格式化 json 时间格式

日期格式化

默认 json 中的时间格式是这样的

"createDate":"2018-09-11T07:34:20.106+0000","updateDate":"2018-09-11T07:34:20.106+0000"

@JsonFormat 可以格式化 json 返回的时间格式。

@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")

格式化后

"createDate":"2018-09-11 07:42:44","updateDate":"2018-09-11 07:42:44"

解决时区问题，MongoDb 默认使用UTC,显示时间相差8小时

@JsonFormat(timezone = "GMT+8", pattern = "yyyy-MM-dd HH:mm:ss")

private Date createdDate = new Date();

时区

public class Test {

@JsonFormat(shape=JsonFormat.Shape.STRING, pattern="yyyy-MMM-dd HH:mm:ss z", timezone="EST")

@JsonProperty("pubDate")

private Date recentBookPubDate;

}

枚举

public class Test {

@JsonFormat(shape=JsonFormat.Shape.NUMBER)

@JsonProperty("birthDate")

private Date birthDate;

}

{

"birthDate" : 1528702883858

}

枚举

package cn.netkiller;

import com.fasterxml.jackson.annotation.JsonFormat;

@JsonFormat(shape=JsonFormat.Shape.NUMBER)

enum Code {

BLOCKING,

CRITICAL,

MEDIUM,

LOW;

}

@JsonFormat(shape=JsonFormat.Shape.STRING)

enum Lang {

Java,

PHP,

Python

}

@JsonComponent

package cn.netkiller.json;

public class Member {

private String name;

public Member() {

// TODO Auto-generated constructor stub

}

public Member(String name) {

// TODO Auto-generated constructor stub

this.name = name;

}

public String getName() {

return name;

}

public void setName(String name) {

this.name = name;

}

@Override

public String toString() {

return "Member [name=" + name + "]";

}

}

package cn.netkiller.json;

import java.io.IOException;

import org.springframework.boot.jackson.JsonComponent;

import com.fasterxml.jackson.core.JsonGenerator;

import com.fasterxml.jackson.core.JsonParser;

import com.fasterxml.jackson.core.JsonProcessingException;

import com.fasterxml.jackson.core.TreeNode;

import com.fasterxml.jackson.databind.DeserializationContext;

import com.fasterxml.jackson.databind.JsonDeserializer;

import com.fasterxml.jackson.databind.JsonSerializer;

import com.fasterxml.jackson.databind.SerializerProvider;

import com.fasterxml.jackson.databind.node.TextNode;

@JsonComponent

public class Json {

public Json() {

// TODO Auto-generated constructor stub

}

public static class MemberJsonSerializer extends JsonSerializer<Member> {

@Override

public void serialize(Member value, JsonGenerator gen, SerializerProvider serializers) throws IOException {

// TODO Auto-generated method stub

gen.writeStartObject();

gen.writeStringField("member", value.toString());

gen.writeEndObject();

}

}

public static class MemberJsonDeserializer extends JsonDeserializer<Member> {

@Override

public Member deserialize(JsonParser p, DeserializationContext ctxt) throws IOException, JsonProcessingException {

// TODO Auto-generated method stub

TreeNode treeNode = p.getCodec().readTree(p);

TextNode member = (TextNode) treeNode.get("member");

return new Member(member.asText());

}

}

}

package cn.netkiller.json.controller;

import cn.netkiller.json.Member;

import org.springframework.beans.factory.annotation.Autowired;

import org.springframework.web.bind.annotation.GetMapping;

import org.springframework.web.bind.annotation.RestController;

import com.fasterxml.jackson.core.JsonProcessingException;

import com.fasterxml.jackson.databind.JsonMappingException;

import com.fasterxml.jackson.databind.ObjectMapper;

/**

*

* @author neo

*/

@RestController

public class SimpleController {

@Autowired

public ObjectMapper objectMapper;

@GetMapping("/")

public String home() throws JsonMappingException, JsonProcessingException {

String json = "{\"name\":\"netkiller\"}";

Member member = objectMapper.readValue(json, Member.class);

System.out.println(member.getName());

return member.getName();

}

}

Object to Json

ObjectMapper mapper = new ObjectMapper();

User user = new User();

//Object to JSON in file

mapper.writeValue(new File("c:\\user.json"), user);

//Object to JSON in String

String jsonInString = mapper.writeValueAsString(user);

//Convert object to JSON string and pretty print

String jsonInString = mapper.writerWithDefaultPrettyPrinter().writeValueAsString(user);

Notification notification = new Notification(status, time, summary + info);

ObjectMapper objectMapper = new ObjectMapper();

String json = objectMapper.writeValueAsString(notification);

Json To Object

ObjectMapper mapper = new ObjectMapper();

String jsonInString = "{'name' : 'mkyong'}";

//JSON from file to Object

User user = mapper.readValue(new File("c:\\user.json"), User.class);

//JSON from String to Object

User user = mapper.readValue(jsonInString, User.class);

3.12. synchronized

避免接口无序执行，被同时多次执行，同一时间只能有一个请求，请求完毕之后才能进行下一次请求。

@GetMapping("/lock/{id}")

public String lock1(@PathVariable("id") String id) throws InterruptedException {

synchronized (id.intern()) {

log.info(Thread.currentThread().getName() + " 上锁");

Thread.sleep(10000);

log.info(Thread.currentThread().getName() + " 解锁");

}

return Thread.currentThread().getName();

}

使用 ConcurrentHashMap 数据共享

private final Map<String, Object> share = new ConcurrentHashMap<>();

@GetMapping("/share/{id}")

public Map<String, Object> shareTest(@PathVariable("id") String id) throws InterruptedException {

//        share.computeIfAbsent(id, k -> new Object());

share.computeIfAbsent(id, key -> {

return new Date();

});

synchronized (share) {

log.info(Thread.currentThread().getName() + " 上锁");

Thread.sleep(1000);

log.info(Thread.currentThread().getName() + " 解锁");

}

return share;

}

3.13. SSE

@GetMapping("/mvc/sse")

public SseEmitter streamSseMvc() {

SseEmitter emitter = new SseEmitter();

ExecutorService sseMvcExecutor = Executors.newSingleThreadExecutor();

sseMvcExecutor.execute(() -> {

try {

for (int i = 0; true; i++) {

SseEventBuilder event = SseEmitter.event()

.data("SSE MVC - " + LocalTime.now().toString())

.id(String.valueOf(i))

.name("sse event - mvc");

emitter.send(event);

Thread.sleep(1000);

}

} catch (Exception ex) {

emitter.completeWithError(ex);

}

});

return emitter;

}

4. View

4.1. 配置静态文件目录

#静态资源访问路径

spring.mvc.static-path-pattern=/**

#静态资源映射路径

spring.resources.static-locations=classpath:/

4.2. 添加静态文件目录

package cn.netkiller.demo.config;

import org.springframework.context.annotation.Configuration;

import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;

import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration

public class MyWebMvcConfigurer implements WebMvcConfigurer {

@Override

public void addResourceHandlers(ResourceHandlerRegistry registry) {

registry.addResourceHandler("/images/**").addResourceLocations("classpath:/images/");

}

}

4.3. Using Spring’s form tag library

css

cssClass

cssClass 使用该属性指定表单元素CSS样式名，相当于HTML元素的class属性

<form:input path="userName" cssClass="inputStyle"/>

cssStyle

cssStyle 直接通过该属性指定样式，相当于HTML元素的style属性

<form:input path="userName" cssStyle="width:100px"/>

cssErrorClass

cssError Class表示表单元素发生错误时对应的样式

<form:input path="userName" cssClass="userNameClass" cssErrorClass= "userNameClassError"/>

cssClass

4.4. Thymeleaf

http://thymeleaf.org/

Maven pom.xml

<dependency>

<groupId>org.springframework.boot</groupId>

<artifactId>spring-boot-starter-thymeleaf</artifactId>

</dependency>

Spring 配置

<!-- **************************************************************** -->

<!-- THYMELEAF-SPECIFIC ARTIFACTS -->

<!-- TemplateResolver <- TemplateEngine <- ViewResolver -->

<!-- **************************************************************** -->

<bean id="templateResolver"

class="org.thymeleaf.templateresolver.ServletContextTemplateResolver">

<property name="prefix" value="/WEB-INF/templates/" />

<property name="suffix" value=".html" />

<property name="templateMode" value="HTML5" />

</bean>

<bean id="templateEngine" class="org.thymeleaf.spring4.SpringTemplateEngine">

<property name="templateResolver" ref="templateResolver" />

</bean>

<bean class="org.thymeleaf.spring4.view.ThymeleafViewResolver">

<property name="templateEngine" ref="templateEngine" />

</bean>

controller

package cn.netkiller.controller;

import org.springframework.stereotype.Controller;

import org.springframework.ui.ModelMap;

import org.springframework.web.bind.annotation.PathVariable;

import org.springframework.web.bind.annotation.RequestMapping;

import org.springframework.web.bind.annotation.RequestMethod;

@Controller

@RequestMapping("/")

public class HelloController {

@RequestMapping(value = "/{name}", method = RequestMethod.GET)

public String getMovie(@PathVariable String name, ModelMap model) {

model.addAttribute("name", name);

return "hello";

}

}

HTML5 Template

<!DOCTYPE html>

<html>

<head>

<meta charset="UTF-8" />

<title>Spring MVC + Thymeleaf Example</title>

</head>

<body>

Hello, <span th:text="${name}" />!

</body>

</html>

thymeleaf 渲染表格

@RequestMapping("/list")

public ModelAndView list() {

Iterable<User> users = userRepository.findAll();

ModelAndView mv = new ModelAndView();

mv.addObject("users", users);

mv.setViewName("table");

return mv;

}

模板文件

<!DOCTYPE html>

<html lang="en" xmlns:th="http://www.thymeleaf.org">

<head>

<meta charset="UTF-8" />

<title>用户登记</title>

</head>

<body>

<h1>Welcome to Thymeleaf</h1>

<table border="1" width="100%">

<tr>

<td>ID</td>

<td>姓名</td>

<td>联系方式</td>

<td>详细地址</td>

<td>图片</td>

</tr>

<tr th:each="user : ${users}">

<td th:text="${user.id}"></td>

<td th:text="${user.name}"></td>

<td th:text="${user.tel}"></td>

<td th:text="${user.address}"></td>

<td th:text="${user.picture}"></td>

</tr>

</table>

</body>

</html>

URL 链接

<span th:text="${number+1}"></span> /

<span th:text="${totalPages}"></span>

<a href="#"

th:href="@{/api/user/browse?sort=id,desc&size=10(page=${number-1})}">上一页</a>

<a href="#"

th:href="@{/api/user/browse?sort=id,desc&size=10(page=${number+1})}">下一页</a>

拼接 URL 的方法

<img src="#" th:src="${'https://img.netkiller.cn/' + pic}" height="128" th:target="_blank" />

拆分字符串

pictures 是一个以逗号分割得字符串。我们需要拆分并逐条显示。

<div th:unless="${picture == null}">

<a th:each="pic : ${#strings.arraySplit(pictures, ',')}" href="#" th:href="${pic}"> <img src="#" th:src="${pic}" height="64" /></a>

</div>

日期格式化

<span th:text="${#dates.format(createDate, 'yyyy-MM-dd HH:mm')}"></span

// java.util.Date 处理

${#dates.day(date)}

${#dates.month(date)}

${#dates.monthName(date)}

${#dates.monthNameShort(date)}

${#dates.year(date)}

${#dates.dayOfWeek(date)}

${#dates.dayOfWeekName(date)}

${#dates.dayOfWeekNameShort(date)}

${#dates.hour(date)}

${#dates.minute(date)}

${#dates.second(date)}

${#dates.millisecond(date)}

// java.time 时间处理

${#temporals.day(date)}

${#temporals.month(date)}

${#temporals.monthName(date)}

${#temporals.monthNameShort(date)}

${#temporals.year(date)}

${#temporals.dayOfWeek(date)}

${#temporals.dayOfWeekName(date)}

${#temporals.dayOfWeekNameShort(date)}

${#temporals.hour(date)}

${#temporals.minute(date)}

${#temporals.second(date)}

${#temporals.millisecond(date)}

// 处理天实例

<p th:text="${#dates.day(standardDate)}"></p>

<p th:text="${#temporals.day(localDateTime)}"></p>

<p th:text="${#temporals.day(localDate)}"></p>

// 处理周实例

<p th:text="${#dates.dayOfWeekName(standardDate)}"></p>

<p th:text="${#temporals.dayOfWeekName(localDateTime)}"></p>

<p th:text="${#temporals.dayOfWeekName(localDate)}"></p>

// 处理秒实例

<p th:text="${#dates.second(standardDate)}"></p>

<p th:text="${#temporals.second(localDateTime)}"></p>

4.5. FreeMarker

http://freemarker.org/

5. Service

5.1. Application

@ComponentScan({ "web", "rest","service" }) 一定要包含 Service 目录。否则无法实现 @Autowired自动装配。你可以直接@ComponentScan扫描所有目录。

import org.springframework.boot.SpringApplication;

import org.springframework.boot.autoconfigure.EnableAutoConfiguration;

import org.springframework.boot.autoconfigure.SpringBootApplication;

import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;

import org.springframework.boot.context.properties.EnableConfigurationProperties;

import org.springframework.context.annotation.Bean;

import org.springframework.context.annotation.ComponentScan;

import org.springframework.context.annotation.Configuration;

import org.springframework.data.authentication.UserCredentials;

import org.springframework.data.mongodb.MongoDbFactory;

import org.springframework.data.mongodb.core.MongoTemplate;

import org.springframework.data.mongodb.core.SimpleMongoDbFactory;

import org.springframework.data.mongodb.repository.config.EnableMongoRepositories;

import com.mongodb.Mongo;

import pojo.ApplicationConfiguration;

@Configuration

@SpringBootApplication

@EnableConfigurationProperties(ApplicationConfiguration.class)

@EnableAutoConfiguration(exclude = { DataSourceAutoConfiguration.class })

@ComponentScan({ "web", "rest","service" })

@EnableMongoRepositories

public class Application {

@SuppressWarnings("deprecation")

public @Bean MongoDbFactory mongoDbFactory() throws Exception {

UserCredentials userCredentials = new UserCredentials("finance", "your_password");

return new SimpleMongoDbFactory(new Mongo("mdb.netkiller.cn"), "finance", userCredentials);

}

public @Bean MongoTemplate mongoTemplate() throws Exception {

return new MongoTemplate(mongoDbFactory());

}

public static void main(String[] args) {

SpringApplication.run(Application.class, args);

}

}

5.2. 定义接口

TestService 接口

package service;

public interface TestService {

public String getName();

public String toString();

public String helloUser(String user);

}

5.3. 实现接口

实现 TestService 接口

package service.impl;

import org.springframework.stereotype.Component;

import service.TestService;

@Component

public class TestServiceImpl implements TestService {

public String name = "Test";

public void TestService() {

}

@Override

public String helloUser(String user) {

return "hello " + user;

}

public String getName() {

return this.name;

}

@Override

public String toString() {

return "TestServiceImpl [config=" + this.name + "]";

}

}

5.4. 调用 Service

控制器中调用 Service

package web;

import org.springframework.beans.factory.annotation.Autowired;

import org.springframework.stereotype.Controller;

import org.springframework.web.bind.annotation.RequestMapping;

import org.springframework.web.bind.annotation.ResponseBody;

import domain.City;

import pojo.ApplicationConfiguration;

import repository.CityRepository;

import service.TestService;

@Controller

public class IndexController {

@Autowired

private TestService testService;

@RequestMapping("/service")

@ResponseBody

public String service() {

return testService.helloUser("Neo");

}

}

5.5. context.getBean 调用 Service

@SpringBootApplication

public class DemoApplication {

public static void main(String[] args) {

ConfigurableApplicationContext context = SpringApplication.run(DemoApplication.class, args);

TestService bean = context.getBean(TestService.class);

bean.test1();

bean.test2("xsx");

bean.test3("xsx`", 1);

bean.test4("xsx2", 1, 2, 3, 4);

}

}

5.6. AopContext

@Servcie

public class UserService {

public void save(User user) {

((UserService)AopContext.currentProxy()).save(user);

}

@Transactional(rollbackFor=Exception.class)

public void save(User user) {

...

...

}

}

5.7. 变量共享

Service 的变量是共享的，这是与 new Object 的区别。

package cn.netkiller.service;

import cn.netkiller.domain.Chat;

import cn.netkiller.repository.ChatRepository;

import lombok.extern.slf4j.Slf4j;

import org.springframework.beans.factory.annotation.Autowired;

import org.springframework.scheduling.annotation.Async;

import org.springframework.stereotype.Service;

@Service

@Slf4j

public class ChatService {

private String test;

@Async

public void test1() {

this.test = "Test 1";

}

@Async

public void test2() {

this.test = "Test 2";

}

@Async

public void test() {

log.info(this.test);

}

}

@Autowired

private ChatService chatService;

@GetMapping("test")

private Mono<String> test() {

chatService.test();

return Mono.just("OK");

}

@GetMapping("/test1")

public Mono<String> test1() {

String test = "测试";

chatService.test1();

return Mono.just(test);

}

@GetMapping("/test2")

public Mono<String> test2() {

chatService.test2();

return Mono.just("OK");

}

2024-01-01T14:09:10.022+08:00  INFO 59782 --- [watch-development] [         task-1] cn.netkiller.service.ChatService           : null

2024-01-01T14:09:24.694+08:00  INFO 59782 --- [watch-development] [         task-3] cn.netkiller.service.ChatService           : Test 1

2024-01-01T14:10:04.394+08:00  INFO 59782 --- [watch-development] [         task-8] cn.netkiller.service.ChatService           : Test 2

6. i18n 国际化

6.1. 在 appliction.properties 中配置启用 i18n

spring.messages.basename=message

spring.messages.encoding=UTF-8

6.2. 创建语言包文件

创建默认语言包文件 message.properties，当匹配不到语言时使用默认配置

member.name=Name

message_en_US.properties

member.name=Name

message_zh_CN.properties

member.name=姓名

注意：Eclipse 需要安装 properties 编辑工具，否则中文会自动转换成UTF8编码，无法直接阅读。

6.3. 控制器重引用语言包

RestController

@RestController

public class HomeController {

@Autowired

private MessageSource messageSource;

@GetMapping("/lang")

public String language() {

String message = messageSource.getMessage("member.name", null, LocaleContextHolder.getLocale());

return message;

}

}

Controller

package cn.netkiller.controller;

import org.springframework.stereotype.Controller;

import org.springframework.beans.factory.annotation.Autowired;

import org.springframework.context.i18n.LocaleContextHolder;

import org.springframework.context.MessageSource;

import org.springframework.web.bind.annotation.RequestMapping;

import org.springframework.web.bind.annotation.RequestMethod;

import org.springframework.ui.Model;

import java.util.Locale;

@Controller

public class HomeController {

@Autowired

private MessageSource messageSource;

@RequestMapping(value = "/", method = RequestMethod.GET)

public String index(Locale locale, Model model){

// add parametrized message from controller

String welcome = messageSource.getMessage("welcome.message", new Object[]{"Neo Chan"}, locale);

model.addAttribute("message", welcome);

// obtain locale from LocaleContextHolder

Locale currentLocale = LocaleContextHolder.getLocale();

model.addAttribute("locale", currentLocale);

model.addAttribute("startMeeting", "10:30");

return "index";

}

}

6.4. 参数传递

有时定义语言包会出现一种情况，一个句子中可能存在变量。例如：

恭喜你 XXXX 您已成为我们的会员

这样的需求，如果丁一两个key处理起来会非常麻烦。这里可以定义一个变量，通过参数传递来修改一句话中间的部分。

welcome=Welcom to {0}

@GetMapping("/lang/args")

public String welcome() {

String[] args = { "China" };

String message = messageSource.getMessage("welcome", args, LocaleContextHolder.getLocale());

return message;

}

参数以此类推 {0}, {1} ...... {n}

String welcome = messageSource.getMessage("welcome.message", new Object[]{"Neo chen"}, locale);

7. 校验器(Validator)

常见的校验注解

@Null 被注释的元素必须为 null

@NotNull 被注释的元素必须不为 null

@AssertTrue 被注释的元素必须为 true

@AssertFalse 被注释的元素必须为 false

@Min(value) 被注释的元素必须是一个数字，其值必须大于等于指定的最小值

@Max(value) 被注释的元素必须是一个数字，其值必须小于等于指定的最大值

@DecimalMin(value) 被注释的元素必须是一个数字，其值必须大于等于指定的最小值

@DecimalMax(value) 被注释的元素必须是一个数字，其值必须小于等于指定的最大值

@Size(max=, min=) 被注释的元素的大小必须在指定的范围内

@Digits (integer, fraction) 被注释的元素必须是一个数字，其值必须在可接受的范围内

@Past 被注释的元素必须是一个过去的日期

@Future 被注释的元素必须是一个将来的日期

@Pattern(regex=,flag=) 被注释的元素必须符合指定的正则表达式

Hibernate Validator提供的校验注解：

@NotBlank(message =) 验证字符串非null，且长度必须大于0

@Email 被注释的元素必须是电子邮箱地址

@Length(min=,max=) 被注释的字符串的大小必须在指定的范围内

@NotEmpty 被注释的字符串的必须非空

@Range(min=,max=,message=) 被注释的元素必须在合适的范围内

7.1. 常规用法

定义校验器

package web.domain;

import javax.validation.constraints.Email;

import javax.validation.constraints.Min;

import javax.validation.constraints.NotNull;

import javax.validation.constraints.Size;

public class User {

private Long id;

@NotNull(message = "用户账号不能为空")

@Size(min = 6, max = 11, message = "账号长度必须是6-11个字符")

private String username;

@NotNull(message = "用户密码不能为空")

@Size(min = 6, max = 8, message = "密码长度必须是6-8个字符")

private String password;

@NotNull(message = "用户邮箱不能为空")

@Email(message = "邮箱格式不正确")

private String email;

// 不允许为空，并且年龄的最小值为18

@NotNull

@Min(18)

private Integer age;

public User() {

// TODO Auto-generated constructor stub

}

public Long getId() {

return id;

}

public void setId(Long id) {

this.id = id;

}

public String getUsername() {

return username;

}

public void setUsername(String username) {

this.username = username;

}

public String getPassword() {

return password;

}

public void setPassword(String password) {

this.password = password;

}

public String getEmail() {

return email;

}

public void setEmail(String email) {

this.email = email;

}

public Integer getAge() {

return age;

}

public void setAge(Integer age) {

this.age = age;

}

@Override

public String toString() {

return "User [id=" + id + ", username=" + username + ", password=" + password + ", email=" + email + ", age=" + age + "]";

}

}

获取 BindingResult 结果

package web.restful;

import javax.validation.Valid;

import org.springframework.validation.BindingResult;

import org.springframework.validation.ObjectError;

import org.springframework.web.bind.annotation.PostMapping;

import org.springframework.web.bind.annotation.RequestBody;

import org.springframework.web.bind.annotation.RequestMapping;

import org.springframework.web.bind.annotation.RestController;

import web.domain.User;

@RestController

@RequestMapping("/restful")

public class TestRestController {

@RequestMapping("/test")

public String home() {

return "OK";

}

@PostMapping("/validation")

public String addUser(@RequestBody @Valid User user, BindingResult bindingResult) {

// 如果有参数校验失败，返回错误信息

if (bindingResult.hasErrors()) {

System.out.println(user.toString());

System.out.println(bindingResult.getErrorCount());

System.out.println(bindingResult.getAllErrors());

}

for (ObjectError error : bindingResult.getAllErrors()) {

return error.getDefaultMessage();

}

return user.toString();

}

}

测试校验效果

neo@MacBook-Pro-Neo ~/workspace/Management % curl -H "Content-Type: application/json" -d '{"id":100000, "username":"netkiller", "password":"123456", "email":"netkillermsn.com"}' curl http://localhost:8080/restful/validation

邮箱格式不正确

neo@MacBook-Pro-Neo ~/workspace/Management % curl -H "Content-Type: application/json" -d '{"id":100000, "username":"netkiller", "password":"123456", "email":"netkiller@msn.com"}' curl http://localhost:8080/restful/validation

must not be null

neo@MacBook-Pro-Neo ~/workspace/Management % curl -H "Content-Type: application/json" -d '{"id":100000, "username":"netkiller", "password":"123456", "email":"netkiller@msn.com", "age":20}' curl http://localhost:8080/restful/validation

User [id=100000, username=netkiller, password=123456, email=netkiller@msn.com, age=20]

7.2. 自定义注解

下面实现一个手机号码检查的注解。

@Retention ：用来说明该注解类的生命周期。它有以下三个参数：

RetentionPolicy.SOURCE : 注解只保留在源文件中

RetentionPolicy.CLASS : 注解保留在class文件中，在加载到JVM虚拟机时丢弃

RetentionPolicy.RUNTIME : 注解保留在程序运行期间，此时可以通过反射获得定义在某个类上的所有注解。

@Target : 用来说明该注解可以被声明在那些元素之前。

ElementType.TYPE：说明该注解只能被声明在一个类前。

ElementType.FIELD：说明该注解只能被声明在一个类的字段前。

ElementType.METHOD：说明该注解只能被声明在一个类的方法前。

ElementType.PARAMETER：说明该注解只能被声明在一个方法参数前。

ElementType.CONSTRUCTOR：说明该注解只能声明在一个类的构造方法前。

ElementType.LOCAL_VARIABLE：说明该注解只能声明在一个局部变量前。

ElementType.ANNOTATION_TYPE：说明该注解只能声明在一个注解类型前。

ElementType.PACKAGE：说明该注解只能声明在一个包名前。

@Constraint来限定自定义注解的方法

定义校验器注解接口

package cn.netkiller.web.annotation;

import java.lang.annotation.Documented;

import java.lang.annotation.Retention;

import java.lang.annotation.Target;

import java.lang.annotation.RetentionPolicy;

import java.lang.annotation.ElementType;

import javax.validation.Constraint;

import javax.validation.Payload;

import cn.netkiller.web.annotation.impl.MobileValidator;

@Target({ ElementType.METHOD, ElementType.FIELD, ElementType.ANNOTATION_TYPE, ElementType.CONSTRUCTOR, ElementType.PARAMETER })

@Retention(RetentionPolicy.RUNTIME)

@Constraint(validatedBy = MobileValidator.class)

@Documented

// 注解的实现类。

public @interface Mobile {

// 校验错误的默认信息

String message() default "手机号码格式不正确！";

// 是否强制校验

boolean isRequired() default true;

Class<?>[] groups() default {};

Class<? extends Payload>[] payload() default {};

}

实现接口

package cn.netkiller.web.annotation.impl;

import java.util.regex.Matcher;

import java.util.regex.Pattern;

import javax.validation.ConstraintValidator;

import javax.validation.ConstraintValidatorContext;

import org.springframework.util.StringUtils;

import cn.netkiller.web.annotation.Mobile;

public class MobileValidator implements ConstraintValidator<Mobile, String> {

public MobileValidator() {

// TODO Auto-generated constructor stub

}

private boolean required = false;

@Override

public void initialize(Mobile constraintAnnotation) {

required = constraintAnnotation.isRequired();

}

@Override

public boolean isValid(String phone, ConstraintValidatorContext constraintValidatorContext) {

Pattern mobile_pattern = Pattern.compile("1\\d{10}");

// System.out.println(phone);

// 是否为手机号的实现

if (required) {

if (StringUtils.isEmpty(phone)) {

return false;

}

Matcher m = mobile_pattern.matcher(phone);

return m.matches();

} else {

return StringUtils.isEmpty(phone);

}

}

}

注解用法

package cn.netkiller.web.domain;

import java.util.Date;

import javax.validation.constraints.Email;

import javax.validation.constraints.Future;

import javax.validation.constraints.Min;

import javax.validation.constraints.NotNull;

import javax.validation.constraints.Size;

import cn.netkiller.web.annotation.Mobile;

public class User {

private Long id;

@NotNull(message = "用户账号不能为空")

@Size(min = 6, max = 11, message = "账号长度必须是6-11个字符")

private String username;

@NotNull(message = "用户密码不能为空")

@Size(min = 6, max = 8, message = "密码长度必须是6-8个字符")

private String password;

@NotNull(message = "用户邮箱不能为空")

@Email(message = "邮箱格式不正确")

private String email;

// 这里是新添加的注解奥

@Mobile(message = "手机号码格式错误！！！")

private String phone;

// 不允许为空，并且年龄的最小值为18

@NotNull

@Min(18)

private Integer age;

@Future

private Date createTime;

public User() {

// TODO Auto-generated constructor stub

}

public Long getId() {

return id;

}

public void setId(Long id) {

this.id = id;

}

public String getUsername() {

return username;

}

public void setUsername(String username) {

this.username = username;

}

public String getPassword() {

return password;

}

public void setPassword(String password) {

this.password = password;

}

public String getEmail() {

return email;

}

public void setEmail(String email) {

this.email = email;

}

public Integer getAge() {

return age;

}

public void setAge(Integer age) {

this.age = age;

}

public String getPhone() {

return phone;

}

public void setPhone(String phone) {

this.phone = phone;

}

@Override

public String toString() {

return "User [id=" + id + ", username=" + username + ", password=" + password + ", email=" + email + ", phone=" + phone + ", age=" + age + "]";

}

}

测试注解

neo@MacBook-Pro-Neo ~ % curl -H "Content-Type: application/json" -d '{"id":100000, "username":"netkiller", "password":"123456", "email":"netkiller@msn.com", "age":20, "phone":"BB"}' curl http://localhost:8080/restful/validation

手机号码格式错误！！！

neo@MacBook-Pro-Neo ~ % curl -H "Content-Type: application/json" -d '{"id":100000, "username":"netkiller", "password":"123456", "email":"netkiller@msn.com", "age":20, "phone":"2433"}' curl http://localhost:8080/restful/validation

手机号码格式错误！！！

neo@MacBook-Pro-Neo ~ % curl -H "Content-Type: application/json" -d '{"id":100000, "username":"netkiller", "password":"123456", "email":"netkiller@msn.com", "age":20, "phone":"130"}' curl http://localhost:8080/restful/validation

手机号码格式错误！！！%

neo@MacBook-Pro-Neo ~ % curl -H "Content-Type: application/json" -d '{"id":100000, "username":"netkiller", "password":"123456", "email":"netkiller@msn.com", "age":20, "phone":"13022223333"}' curl http://localhost:8080/restful/validation

User [id=100000, username=netkiller, password=123456, email=netkiller@msn.com, phone=13022223333, age=20]%

8. Interceptor/Filter 拦截器/过滤

8.1. Session 拦截

WebMvcConfigurerAdapter

package mis.config;

import mis.interceptor.SpringMVCInterceptor;

import org.springframework.boot.autoconfigure.EnableAutoConfiguration;

import org.springframework.context.annotation.Configuration;

import org.springframework.web.servlet.config.annotation.InterceptorRegistry;

import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;

@Configuration

public class WebAppConfig extends WebMvcConfigurerAdapter {

/**

* 配置拦截器

*/

@Override

public void addInterceptors(InterceptorRegistry registry) {

registry.addInterceptor(new SpringMVCInterceptor()).addPathPatterns("/**");

}

}

HandlerInterceptor

package mis.interceptor;

import org.springframework.util.StringUtils;

import org.springframework.web.servlet.HandlerInterceptor;

import org.springframework.web.servlet.ModelAndView;

import javax.servlet.http.HttpServletRequest;

import javax.servlet.http.HttpServletResponse;

public class SpringMVCInterceptor implements HandlerInterceptor {

@Override

public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object o) throws Exception {

if(request.getServletPath().startsWith("/index") || request.getServletPath().startsWith("/login")) {

return true;

}

String username = (String)request.getSession().getAttribute("userName");

if (StringUtils.isEmpty(username)){

response.sendRedirect(request.getContextPath() + "/index");

return false;

}

return true;

}

@Override

public void postHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, ModelAndView modelAndView) throws Exception {

}

@Override

public void afterCompletion(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, Exception e) throws Exception {

}

}

8.2. Token 拦截

package cn.netkiller.annotation;

import java.lang.annotation.ElementType;

import java.lang.annotation.Retention;

import java.lang.annotation.RetentionPolicy;

import java.lang.annotation.Target;

@Target({ElementType.METHOD, ElementType.TYPE})

@Retention(RetentionPolicy.RUNTIME)

public @interface TokenPass {

boolean required() default true;

}

package cn.netkiller.annotation;

import java.lang.annotation.ElementType;

import java.lang.annotation.Retention;

import java.lang.annotation.RetentionPolicy;

import java.lang.annotation.Target;

@Target({ElementType.METHOD, ElementType.TYPE})

@Retention(RetentionPolicy.RUNTIME)

public @interface TokenVerification {

boolean required() default true;

}

package cn.netkiller.component;

import cn.netkiller.annotation.TokenPass;

import cn.netkiller.annotation.TokenVerification;

import jakarta.servlet.http.HttpServletRequest;

import jakarta.servlet.http.HttpServletResponse;

import lombok.extern.slf4j.Slf4j;

import org.springframework.stereotype.Component;

import org.springframework.web.method.HandlerMethod;

import org.springframework.web.servlet.HandlerInterceptor;

import org.springframework.web.servlet.ModelAndView;

import java.lang.reflect.Method;

@Slf4j

@Component

public class TokenHandlerInterceptor implements HandlerInterceptor {

// 返回 true 表示继续向下执行，返回 false 表示中断后续操作

@Override

public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {

String token = request.getHeader("token");// 从 http 请求头中取出 token

// 如果不是映射到方法直接通过

if (!(handler instanceof HandlerMethod handlerMethod)) {

return true;

}

Method method = handlerMethod.getMethod();

//检查方法是否有TokenPass注解，有则跳过认证，直接通过

if (method.isAnnotationPresent(TokenPass.class)) {

TokenPass tokenPass = method.getAnnotation(TokenPass.class);

if (tokenPass.required()) {

return true;

}

}

//检查 TokenVerification 需要用户权限的注解

if (method.isAnnotationPresent(TokenVerification.class)) {

TokenVerification tokenVerification = method.getAnnotation(TokenVerification.class);

if (tokenVerification.required()) {

// 执行认证

if (token == null) {

throw new RuntimeException("无 token，请重新登录");

}

// token 校验逻辑写在这里

}

}

throw new RuntimeException("没有权限");

}

// 目标方法执行后, 该方法在控制器处理请求方法调用之后、解析视图之前执行, 可以通过此方法对请求域中的模型和视图做进一步修改

@Override

public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {

log.debug("postHandle");

}

// 页面渲染后, 该方法在视图渲染结束后执行, 可以通过此方法实现资源清理、记录日志信息等工作

@Override

public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {

log.debug("afterCompletion");

}

}

package cn.netkiller.config;

import cn.netkiller.component.TokenHandlerInterceptor;

import jakarta.annotation.Resource;

import org.springframework.context.annotation.Configuration;

import org.springframework.web.servlet.config.annotation.InterceptorRegistry;

import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration

public class TokenWebMvcConfigurer implements WebMvcConfigurer {

@Resource

private TokenHandlerInterceptor tokenHandlerInterceptor;

@Override

public void addInterceptors(InterceptorRegistry registry) {

//注册拦截器,并设置拦截的请求路径

//addPathPatterns为拦截此请求路径的请求

//excludePathPatterns为不拦截此路径的请求

registry.addInterceptor(tokenHandlerInterceptor)

.addPathPatterns("/mock/*")

.excludePathPatterns("/device/*")

.excludePathPatterns("/callback/*");

}

}

package cn.netkiller.controller;

import org.springframework.web.bind.annotation.ControllerAdvice;

import org.springframework.web.bind.annotation.ExceptionHandler;

import org.springframework.web.bind.annotation.ResponseBody;

import java.util.Map;

@ControllerAdvice

public class GloablExceptionHandler {

@ResponseBody

@ExceptionHandler(Exception.class)

public Object handleException(Exception e) {

String msg = e.getMessage();

if (msg == null || msg.equals("")) {

msg = "服务器出错";

}

return Map.of("message", msg, "status", 500);

}

}

package cn.netkiller.controller;

import lombok.extern.slf4j.Slf4j;

import org.reactivestreams.Publisher;

import org.springframework.web.bind.annotation.GetMapping;

import org.springframework.web.bind.annotation.PostMapping;

import org.springframework.web.bind.annotation.ResponseBody;

import org.springframework.web.bind.annotation.RestController;

import reactor.core.publisher.Mono;

@RestController

@RequestMapping("/mock")

@Slf4j

public class HomeController {

//无需 token 可以访问

@TokenPass

@GetMapping("/neo")

public String neo() {

return "https://www.netkiller.cn";

}

// 需要 Token 才能访问

@TokenVerification

@GetMapping("/netkiller")

public String netkiller() {

return "https://www.netkiller.cn";

}

}

neo@MacBook-Pro-M2 ~> curl -X 'GET' 'http://localhost:8080/mock/neo'

https://www.netkiller.cn⏎

neo@MacBook-Pro-M2 ~> curl -X 'GET' 'http://localhost:8080/mock/netkiller'

{"message":"无 token，请重新登录","status":500}⏎

neo@MacBook-Pro-M2 ~> curl -X 'GET' 'http://localhost:8080/mock/flux' -H 'token: 8A8691CF-DC81-4477-84D8-DC5CDDF98568'

https://www.netkiller.cn⏎

8.3. 过滤器

package cn.netkiller.config;

import jakarta.servlet.*;

import lombok.extern.slf4j.Slf4j;

import org.springframework.stereotype.Component;

import java.io.IOException;

@Component

@Slf4j

public class MyFilter implements Filter {

@Override

public void init(FilterConfig filterConfig) throws ServletException {

log.info(" myfilter init");

}

@Override

public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {

log.info("myfilter execute");

}

@Override

public void destroy() {

log.info("myfilter destroy");

}

}

package cn.netkiller.config.filter;

import org.springframework.beans.factory.annotation.Autowired;

import org.springframework.beans.factory.annotation.Qualifier;

import org.springframework.lang.NonNull;

import org.springframework.stereotype.Component;

import org.springframework.web.filter.OncePerRequestFilter;

import org.springframework.web.servlet.HandlerExceptionResolver;

import javax.servlet.FilterChain;

import javax.servlet.ServletException;

import javax.servlet.http.HttpServletRequest;

import javax.servlet.http.HttpServletResponse;

import java.io.IOException;

@Component

public class CustomExceptionFilter extends OncePerRequestFilter {

@Autowired

@Qualifier("handlerExceptionResolver")

private HandlerExceptionResolver resolver;

@Override

protected void doFilterInternal(@NonNull HttpServletRequest request, @NonNull HttpServletResponse response, @NonNull FilterChain filterChain) throws ServletException, IOException {

try {

filterChain.doFilter(request, response);

} catch (Exception e) {

resolver.resolveException(request, response, null, e);

}

}

}

8.4. 拦截器获取PathVariable变量

Map<String, String> pathVars = (Map<String, String>) request.getAttribute(HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE);

log.info(pathVars.toString());

9. RestClient

9.1. 创建 RestClient

Bean 注入方式

@Value("${REMOTE_BASE_URI:http://localhost:3000}")

String baseURI;

@Bean

RestClient restClient() {

return RestClient.create(baseURI);

}

RestClient defaultClient = RestClient.create();

RestClient customClient = RestClient.builder()

.requestFactory(new HttpComponentsClientHttpRequestFactory())

.messageConverters(converters -> converters.add(new MyCustomMessageConverter()))

.baseUrl("https://example.com")

.defaultUriVariables(Map.of("variable", "foo"))

.defaultHeader("My-Header", "Foo")

.requestInterceptor(myCustomInterceptor)

.requestInitializer(myCustomInitializer)

.build();

9.2. Get 操作

restClient.get()

.uri("/employees")

//...

restClient.get()

.uri("/employees/{id}", id)

//...

List<Employee> employeeList = restClient.get()

.uri("/employees")

.accept(MediaType.APPLICATION_JSON)

.retrieve()

.body(List.class);

ResponseEntity<List> responseEntity = restClient.get()

.uri("/employees")

.accept(MediaType.APPLICATION_JSON)

.retrieve()

.toEntity(List.class);

9.3. Post Json

@Cacheable(value = "translate", key = "#chinese", unless = "#result == null")

public String translate(String chinese) {

String english = null;

RestClient restClient = RestClient.builder().baseUrl(url).build();

String accessToken = this.getAccessToken();

HashMap<String, String> data = new LinkedHashMap<String, String>() {{

put("q", chinese);

put("from", "zh");

put("to", "en");

}};

ResponseEntity<Translate> response = restClient.post()

.uri("/rpc/2.0/mt/texttrans/v1-?access_token={access_token}", Map.of("access_token", accessToken))

.contentType(APPLICATION_JSON)

.body(data)

.retrieve()

.toEntity(Translate.class);

if (response.getStatusCode() == HttpStatus.OK) {

Translate translate = response.getBody();

if (translate.getResult() != null) {

english = translate.getResult().getTrans_result().get(0).get("dst");

}

log.info("Translate english: {}", english);

} else {

log.info("Translate: " + response);

}

return english;

}

9.4. HTTP Authorization Basic

@GetMapping("/{device}/test")

public String get(@PathVariable String device) throws InterruptedException {

String username = System.getProperty("username", "admin");

String password = System.getProperty("password", "uPQKFe98IwZCzgVGjbWIQRyRyyecb2Ha");

Base64.Encoder encoder = Base64.getEncoder();

String authorization = encoder.encodeToString((username + ":" + password).getBytes(StandardCharsets.UTF_8));

RestClient restClient = RestClient.builder()

.baseUrl("http://gpt.netkiller.cn:8080")

.defaultHeader("Authorization", "Basic " + authorization)

.build();

String question = "test";

String result = restClient.get().uri(uriBuilder -> uriBuilder

.path("/ask/cache_chatgpt")

.queryParam("question", question)

.build()).retrieve().body(String.class);

return result;

}

9.5.

ResponseEntity<JsonObject> responseEntity = restClient.get()

.uri(uriBuilder -> uriBuilder

.path("/articles/1.html")

.queryParam("question", URLEncoder.encode(question, StandardCharsets.UTF_8))

.build())

.retrieve()

.onStatus(status -> status.value() == 404, (request, response) -> {

throw new ArticleNotFoundException(response)

}).toEntity(JsonObject.class);

10. FAQ

10.1. o.s.web.servlet.PageNotFound

解决方法，加入下面代码到 dispatcher-servlet.xml 文件中

<mvc:annotation-driven />

dispatcher-servlet.xml

<?xml version="1.0" encoding="UTF-8"?>

<beans xmlns="http://www.springframework.org/schema/beans"

xmlns:mvc="http://www.springframework.org/schema/mvc" xmlns:context="http://www.springframework.org/schema/context"

xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"

xsi:schemaLocation="

http://www.springframework.org/schema/beans

http://www.springframework.org/schema/beans/spring-beans.xsd

http://www.springframework.org/schema/mvc

http://www.springframework.org/schema/mvc/spring-mvc.xsd

http://www.springframework.org/schema/context

http://www.springframework.org/schema/context/spring-context.xsd">

<context:component-scan base-package="cn.netkiller.controller" />

<mvc:annotation-driven />

<bean id="viewResolver"

class="org.springframework.web.servlet.view.UrlBasedViewResolver">

<property name="viewClass"

value="org.springframework.web.servlet.view.JstlView" />

<property name="prefix" value="/WEB-INF/jsp/" />

<property name="suffix" value=".jsp" />

</bean>

</beans>

10.2. HTTP Status 500 - Handler processing failed; nested exception is java.lang.NoClassDefFoundError: javax/servlet/jsp/jstl/core/Config

pom.xml 文件中加入依赖包

<dependency>

<groupId>javax.servlet</groupId>

<artifactId>jstl</artifactId>

<version>1.2</version>

</dependency>

10.3. 同时使用 Thymeleaf 与 JSP

Using both Thymeleaf and JSP

<bean

class="org.springframework.web.servlet.view.InternalResourceViewResolver">

<property name="viewClass"

value="org.springframework.web.servlet.view.JstlView" />

<property name="prefix" value="/WEB-INF/jsp/" />

<!-- <property name="suffix" value=".jsp" /> -->

<property name="viewNames" value="*.jsp" />

</bean>

<bean id="templateResolver"

class="org.thymeleaf.templateresolver.ServletContextTemplateResolver">

<property name="prefix" value="/WEB-INF/templates/" />

<!-- <property name="suffix" value=".html" /> -->

<property name="templateMode" value="HTML5" />

</bean>

<bean id="templateEngine" class="org.thymeleaf.spring4.SpringTemplateEngine">

<property name="templateResolver" ref="templateResolver" />

</bean>

<bean class="org.thymeleaf.spring4.view.ThymeleafViewResolver">

<property name="templateEngine" ref="templateEngine" />

<property name="viewNames" value="*.html" />

</bean>

@RequestMapping("/thymeleaf")

public String thymeleafView(){

return "thymeleaf.html";

}

@RequestMapping("/jsp")

public String jspView(){

return "jstl.jsp";

}

<property name="viewNames" value="*thymeleaf/*" />

@RequestMapping(value="/test")

public ModelAndView dboxPrint(Model model){

ModelAndView modelAndView = new ModelAndView("thymeleaf/test");

return modelAndView;

}

10.4. 排除静态内容

方法一，排除静态内容如 images, css, js 等等

<servlet>

<servlet-name>springframework</servlet-name>

<servlet-class>

org.springframework.web.servlet.DispatcherServlet

</servlet-class>

<load-on-startup>1</load-on-startup>

</servlet>

<servlet-mapping>

<servlet-name>default</servlet-name>

<url-pattern>/images/*</url-pattern>

<url-pattern>*.css</url-pattern>

<url-pattern>/js/*.js</url-pattern>

</servlet-mapping>

<servlet-mapping>

<servlet-name>springframework</servlet-name>

<url-pattern>/welcome.jsp</url-pattern>

<url-pattern>/welcome.html</url-pattern>

<url-pattern>*.html</url-pattern>

</servlet-mapping>

方法二

<mvc:resources location="/images/" mapping="/images/**" />

<mvc:resources location="/css/" mapping="/css/**" />

<mvc:resources location="/js/" mapping="/js/**" />

10.5. HTTP Status 406

配置 url-pattern 增加需要传递给Spring的扩展名

<servlet>

<servlet-name>springframework</servlet-name>

<servlet-class>

org.springframework.web.servlet.DispatcherServlet

</servlet-class>

<load-on-startup>1</load-on-startup>

</servlet>

<servlet-mapping>

<servlet-name>springframework</servlet-name>

<url-pattern>/welcome.jsp</url-pattern>

<url-pattern>/welcome.html</url-pattern>

<url-pattern>*.json</url-pattern>

<url-pattern>*.xml</url-pattern>

<url-pattern>*.html</url-pattern>

</servlet-mapping>

10.6. Caused by: java.lang.IllegalArgumentException: Not a managed type: class common.domain.Article

背景描述：Springboot 入口文件 Application.java 的包是 package api; 为了让 domain,pojo 共用，于是将 domain 放到Maven module下命令为 common。启动后出现这个故障。

解决方案增加 @EntityScan("common.domain") 即可。

package api;

import org.springframework.boot.SpringApplication;

import org.springframework.boot.autoconfigure.SpringBootApplication;

import org.springframework.boot.autoconfigure.domain.EntityScan;

import org.springframework.cloud.netflix.eureka.EnableEurekaClient;

import org.springframework.scheduling.annotation.EnableScheduling;

@SpringBootApplication

@EnableScheduling

@EnableEurekaClient

@EntityScan("common.domain")

public class Application {

public static void main(String[] args) {

System.out.println( "Service Api Starting..." );

SpringApplication.run(Application.class, args);

}

}

10.7. {"error":"unauthorized","error_description":"Full authentication is required to access this resource"}

Oauth @RestController 一切正常， @Controller 提示如下

{"error":"unauthorized","error_description":"Full authentication is required to access this resource"}

程序如下

package api.controller;

import org.springframework.stereotype.Controller;

import org.springframework.web.bind.annotation.GetMapping;

import org.springframework.web.bind.annotation.RequestMapping;

@Controller

@RequestMapping("/")

public class IndexController {

public IndexController() {

// TODO Auto-generated constructor stub

}

@GetMapping("/")

public String index() {

return "Helloworld!!!";

}

@GetMapping("/about")

public String test() {

return "Helloworld!!!";

}

}

分析 @Controller 不允许直接返回字符串，必须使用 @ResponseBody 或者 ModelAndView，下改后问题解决。

package api.controller;

import org.springframework.stereotype.Controller;

import org.springframework.web.bind.annotation.GetMapping;

import org.springframework.web.bind.annotation.RequestMapping;

import org.springframework.web.bind.annotation.ResponseBody;

@Controller

@RequestMapping("/web")

public class IndexController {

public IndexController() {

// TODO Auto-generated constructor stub

}

@GetMapping("/")

@ResponseBody

public String index() {

return "Helloworld!!!";

}

@GetMapping("/about")

@ResponseBody

public String test() {

return "Helloworld!!!";

}

}

同时 @EnableWebSecurity 需要忽略 @Controller 的映射 URL

@Configuration

@EnableWebSecurity

public class WebSecurityConfiguration extends WebSecurityConfigurerAdapter {

@Override

public void configure(WebSecurity web) throws Exception {

web.ignoring().antMatchers("/**/json").antMatchers("/about", "/", "/css/**");

}

}

第 46 章 WebFlux framework

1. Getting Started

1.1. Maven

<?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>cn.netkiller</groupId>

<artifactId>webflux</artifactId>

<version>0.0.1-SNAPSHOT</version>

<name>webflux</name>

<description>Demo webflux project for Spring Boot</description>

<properties>

<java.version>11</java.version>

</properties>

<dependencies>

<dependency>

<groupId>org.springframework.boot</groupId>

<artifactId>spring-boot-starter-webflux</artifactId>

</dependency>

<dependency>

<groupId>org.springframework.boot</groupId>

<artifactId>spring-boot-starter-test</artifactId>

<scope>test</scope>

</dependency>

<dependency>

<groupId>io.projectreactor</groupId>

<artifactId>reactor-test</artifactId>

<scope>test</scope>

</dependency>

<dependency>

<groupId>org.springframework.restdocs</groupId>

<artifactId>spring-restdocs-mockmvc</artifactId>

<scope>test</scope>

</dependency>

</dependencies>

<build>

<plugins>

<plugin>

<groupId>org.springframework.boot</groupId>

<artifactId>spring-boot-maven-plugin</artifactId>

</plugin>

</plugins>

</build>

</project>

1.2. Application

package cn.netkiller.webflux;

import org.springframework.boot.SpringApplication;

import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication

public class WebfluxApplication {

public static void main(String[] args) {

SpringApplication.run(WebfluxApplication.class, args);

}

}

1.3. RestController

package cn.netkiller.webflux;

import org.reactivestreams.Publisher;

import org.springframework.web.bind.annotation.GetMapping;

import org.springframework.web.bind.annotation.ResponseBody;

import org.springframework.web.bind.annotation.RestController;

import reactor.core.publisher.Mono;

@RestController

public class TestController {

public TestController() {

}

@GetMapping("/")

@ResponseBody

public Publisher<String> index() {

return Mono.just("Hello world!");

}

}

1.4. 测试

neo@MacBook-Pro ~/webflux % mvn spring-boot:run

neo@MacBook-Pro ~ % curl http://localhost:8080

Hello world!%

2. WebFlux 与 SprintMVC 有什么不同？

2.1. 实验程序

package cn.netkiller.controller;

import java.util.concurrent.TimeUnit;

import org.slf4j.Logger;

import org.slf4j.LoggerFactory;

import org.springframework.web.bind.annotation.GetMapping;

import org.springframework.web.bind.annotation.RestController;

import lombok.extern.slf4j.Slf4j;

import reactor.core.publisher.Mono;

@RestController

@Slf4j

public class WebFluxController {

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

public WebFluxController() {

}

// 阻塞5秒钟

private String job() {

try {

TimeUnit.SECONDS.sleep(5);

} catch (InterruptedException e) {

}

return "Hellowoard!!!";

}

// SpringMVC 方式

@GetMapping("/SpringMVC")

private String springmvc() {

logger.info("start");

String result = job();

logger.info("done");

return result;

}

// WebFlux 方式

@GetMapping("/WebFlux")

private Mono<String> webflux() {

logger.info("start");

Mono<String> result = Mono.fromSupplier(() -> job());

logger.info("done");

return result;

}

}

2.2. 实验结果

neo@MacBook-Pro-Neo ~> time curl http://localhost:8080/SpringMVC

Hellowoard!!!

________________________________________________________

Executed in    5.02 secs      fish           external

usr time    4.98 millis  242.00 micros    4.74 millis

sys time    5.48 millis  993.00 micros    4.49 millis

2023-02-24T14:13:07.063+08:00 TRACE 1552 --- [  XNIO-1 task-2] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped to cn.netkiller.controller.WebFluxController#springmvc()

2023-02-24T14:13:07.077+08:00  INFO 1552 --- [  XNIO-1 task-2] c.n.controller.WebFluxController         : start

2023-02-24T14:13:12.082+08:00  INFO 1552 --- [  XNIO-1 task-2] c.n.controller.WebFluxController         : done

从省输出日志可以看到 start 2023-02-24T14:13:07, done 2023-02-24T14:13:12 程序运行被阻塞了 5秒钟

neo@MacBook-Pro-Neo ~> time curl http://localhost:8080/WebFlux

Hellowoard!!!

________________________________________________________

Executed in    5.02 secs      fish           external

usr time    5.19 millis  228.00 micros    4.96 millis

sys time    6.05 millis  854.00 micros    5.20 millis

2023-02-24T14:14:54.720+08:00 TRACE 1583 --- [  XNIO-1 task-2] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped to cn.netkiller.controller.WebFluxController#webflux()

2023-02-24T14:14:54.729+08:00  INFO 1583 --- [  XNIO-1 task-2] c.n.controller.WebFluxController         : start

2023-02-24T14:14:54.731+08:00  INFO 1583 --- [  XNIO-1 task-2] c.n.controller.WebFluxController         : done

2023-02-24T14:14:59.753+08:00 TRACE 1583 --- [  XNIO-1 task-3] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped to cn.netkiller.controller.WebFluxController#webflux()

再看 webflux 的表现，start 2023-02-24T14:14:54, done 2023-02-24T14:14:54 执行时间不到一秒钟。

3. WebFlux Router

3.1. Component 原件

package cn.netkiller.webflux.component;

import org.springframework.http.MediaType;

import org.springframework.stereotype.Component;

import org.springframework.web.reactive.function.BodyInserters;

import org.springframework.web.reactive.function.server.ServerRequest;

import org.springframework.web.reactive.function.server.ServerResponse;

import reactor.core.publisher.Mono;

@Component

public class HelloWorldHandler {

public HelloWorldHandler() {

}

public Mono<ServerResponse> helloWorld(ServerRequest request) {

return ServerResponse.ok().contentType(MediaType.TEXT_PLAIN).body(BodyInserters.fromObject("Hello World!!!"));

}

}

3.2. 路由配置

package cn.netkiller.webflux.config;

import org.springframework.context.annotation.Bean;

import org.springframework.context.annotation.Configuration;

import org.springframework.http.MediaType;

import org.springframework.web.reactive.function.server.RequestPredicates;

import org.springframework.web.reactive.function.server.RouterFunction;

import org.springframework.web.reactive.function.server.RouterFunctions;

import org.springframework.web.reactive.function.server.ServerResponse;

import cn.netkiller.webflux.component.HelloWorldHandler;

@Configuration

public class WebFluxRouter {

public WebFluxRouter() {

}

@Bean

public RouterFunction<ServerResponse> routeHelloWorld(HelloWorldHandler helloWorldHandler) {

return RouterFunctions.route(RequestPredicates.GET("/hello").and(RequestPredicates.accept(MediaType.TEXT_PLAIN)), helloWorldHandler::helloWorld);

}

}

3.3. Thymeleaf

模板引擎 Thymeleaf 依赖

<dependency>

<groupId>org.springframework.boot</groupId>

<artifactId>spring-boot-starter-thymeleaf</artifactId>

</dependency>

application.properties 相关的配置

spring.thymeleaf.cache=true # Enable template caching.

spring.thymeleaf.check-template=true # Check that the template exists before rendering it.

spring.thymeleaf.check-template-location=true # Check that the templates location exists.

spring.thymeleaf.enabled=true # Enable Thymeleaf view resolution for Web frameworks.

spring.thymeleaf.encoding=UTF-8 # Template files encoding.

spring.thymeleaf.excluded-view-names= # Comma-separated list of view names that should be excluded from resolution.

spring.thymeleaf.mode=HTML5 # Template mode to be applied to templates. See also StandardTemplateModeHandlers.

spring.thymeleaf.prefix=classpath:/templates/ # Prefix that gets prepended to view names when building a URL.

spring.thymeleaf.reactive.max-chunk-size= # Maximum size of data buffers used for writing to the response, in bytes.

spring.thymeleaf.reactive.media-types= # Media types supported by the view technology.

spring.thymeleaf.servlet.content-type=text/html # Content-Type value written to HTTP responses.

spring.thymeleaf.suffix=.html # Suffix that gets appended to view names when building a URL.

spring.thymeleaf.template-resolver-order= # Order of the template resolver in the chain.

spring.thymeleaf.view-names= # Comma-separated list of view names that can be resolved.

Webflux 控制器

@GetMapping("/welcome")

public Mono<String> hello(final Model model) {

model.addAttribute("name", "Neo");

model.addAttribute("city", "深圳");

String path = "hello";

return Mono.create(monoSink -> monoSink.success(path));

}

@GetMapping("/list")

public String listPage(final Model model) {

final Flux<City> citys = cityService.findAllCity();

model.addAttribute("cityLists", citys);

return "cityList";

}

Tymeleaf 视图

welcome.html

<!DOCTYPE html>

<html lang="zh-CN">

<head>

<meta charset="UTF-8"/>

<title>欢迎页面</title>

</head>

<body>

<h1 >你好，欢迎来自<p th:text="${city}"></p>的<p th:text="${name}"></p></h1>

</body>

</html>

cityList.html

<!DOCTYPE html>

<html lang="zh-CN">

<head>

<meta charset="UTF-8"/>

<title>城市列表</title>

</head>

<body>

<div>

<table>

<legend>

<strong>城市列表</strong>

</legend>

<thead>

<tr>

<th>城市编号</th>

<th>省份编号</th>

<th>名称</th>

<th>描述</th>

</tr>

</thead>

<tbody>

<tr th:each="city : ${cityLists}">

<td th:text="${city.id}"></td>

<td th:text="${city.provinceId}"></td>

<td th:text="${city.name}"></td>

<td th:text="${city.description}"></td>

</tr>

</tbody>

</table>

</div>

</body>

</html>

3.4. Webflux Redis

Maven Redis 依赖

<dependency>

<groupId>org.springframework.boot</groupId>

<artifactId>spring-boot-starter-data-redis-reactive</artifactId>

</dependency>

Redis 配置

server:

port: 8080

spring:

application:

name: webflux

redis:

host: 127.0.0.1

port: 6379

password: pwd2020

timeout: 5000

lettuce:

pool:

max-active: 200

max-idle: 20

min-idle: 5

max-wait: 1000

Config

@Bean

public ReactiveRedisTemplate<String, String> reactiveRedisTemplate(ReactiveRedisConnectionFactory factory) {

ReactiveRedisTemplate<String, String> reactiveRedisTemplate = new ReactiveRedisTemplate<>(factory,RedisSerializationContext.string());

return reactiveRedisTemplate;

}

Service

@Service

public class RedisServiceImpl implements RedisService {

@Autowired

private ReactiveRedisTemplate<String, String> redisTemplate;

@Override

public Mono<String> get(String key) {

ReactiveValueOperations<String, String> operations = redisTemplate.opsForValue();

return operations.get(key);

}

@Override

public Mono<String> set(String key,User user) {

ReactiveValueOperations<String, String> operations = redisTemplate.opsForValue();

return operations.getAndSet(key, JSON.toJSONString(user));

}

@Override

public Mono<Boolean> delete(String key) {

ReactiveValueOperations<String, String> operations = redisTemplate.opsForValue();

return operations.delete(key);

}

@Override

public Mono<String> update(String key,User user) {

ReactiveValueOperations<String, String> operations = redisTemplate.opsForValue();

return operations.getAndSet(key, JSON.toJSONString(user));

}

@Override

public Flux<String> all(String key) {

ReactiveListOperations<String, String> operations = redisTemplate.opsForList();

return operations.range(key, 0, -1);

}

@Override

public Mono<Long> push(String key,List<String> list) {

ReactiveListOperations<String, String> operations = redisTemplate.opsForList();

return operations.leftPushAll(key, list);

}

@Override

public Flux<String> find(String key) {

ReactiveValueOperations<String, String> operations = redisTemplate.opsForValue();

return redisTemplate.keys(key).flatMap(keyId ->operations.get(keyId));

}

}

@RestController

@RequestMapping("/user")

public class UserController {

public final static String USER_KEY="user";

@Autowired

private RedisService redisService;

@GetMapping("/get/{key}")

public Mono<String> getUserByKey(@PathVariable("id")String key){

return redisService.get(key);

}

@GetMapping("/add")

public Mono<String> add(User user){

user = new User();

user.setAccount("neo");

user.setPassword("123456");

user.setNickname("netkiller");

user.setEmail("netkiller@msn.com");

user.setPhone("");

user.setGender(true);

user.setBirthday("1980-01-30");

user.setProvince("广东省");

user.setCity("深圳市");

user.setCounty("南山区");

user.setAddress("");

user.setState("Enabled");

System.out.println(JSON.toJSONString(user));

return redisService.set("neo",user);

}

@GetMapping("/addlist")

public Mono<Long> addlist(){

List<String> list=new ArrayList<String>();

User user = new User();

user.setAccount("neo");

user.setPassword("123456");

user.setNickname("netkiller");

user.setEmail("netkiller@msn.com");

user.setPhone("");

user.setGender(true);

user.setBirthday("1980-01-30");

user.setProvince("广东省");

user.setCity("深圳市");

user.setCounty("南山区");

user.setAddress("");

user.setState("Enabled");

//添加第一条数据

list.add(JSON.toJSONString(user));

//添加第二条数据

list.add(JSON.toJSONString(user));

//添加第三条数据

list.add(JSON.toJSONString(user));

return redisService.addlist("list", list);

}

@GetMapping(value="/findAll",produces = MediaType.APPLICATION_STREAM_JSON_VALUE)

public Flux<String> findAll(){

return redisService.all("list").delayElements(Duration.ofSeconds(2));

}

@GetMapping("/getUsers")

public Flux<String> findUsers() {

return redisService.find("*").delayElements(Duration.ofSeconds(2));

}

}

3.5. Webflux Mongdb

Maven 依赖

<dependency>

<groupId>org.springframework.boot</groupId>

<artifactId>spring-boot-starter-data-mongodb-reactive</artifactId>

</dependency>

Repository

import org.springframework.data.mongodb.repository.ReactiveMongoRepository;

import cn.netkiller.entity.User;

public interface UserRepository extends ReactiveMongoRepository<User, Long>{

}

Service

@Service

public class MongoServiceImpl implements MongoService {

@Autowired

private UserRepository userRepository;

@Override

public Mono<User> getById(Long id) {

return userRepository.findById(id);

}

@Override

public Mono<User> addUser(User user) {

return userRepository.save(user);

}

@Override

public Mono<Boolean> deleteById(Long id) {

userRepository.deleteById(id);

return Mono.create(userMonoSink -> userMonoSink.success());

}

@Override

public Mono<User> updateById(User user) {

return userRepository.save(user);

}

@Override

public Flux<User> findAllUser() {

return userRepository.findAll();

}

}

控制器

@RestController

@RequestMapping("/usermg")

public class UserMongoController {

@Autowired

private MongoService mongoService;

@GetMapping("/add")

public Mono<User> add(User user) {

user = new User();

User user = new User();

user.setAccount("neo");

user.setPassword("123456");

user.setNickname("netkiller");

user.setEmail("netkiller@msn.com");

user.setPhone("");

user.setGender(true);

user.setBirthday("1980-01-30");

user.setProvince("广东省");

user.setCity("深圳市");

user.setCounty("南山区");

user.setAddress("");

user.setState("Enabled");

System.out.println(JSON.toJSONString(user));

return mongoService.addUser(user);

}

/**

* 注意这里 produces = MediaType.APPLICATION_STREAM_JSON_VALUE 必须这样设置

*/

@GetMapping(value="/findAll",produces = MediaType.APPLICATION_STREAM_JSON_VALUE)

public Flux<User> findAll(){

return mongoService.findAllUser().delayElements(Duration.ofSeconds(1));

}

}

produces 如果不是application/stream+json则调用端无法滚动得到结果，将一直阻塞等待数据流结束或超时。

3.6. Mono

Mono(返回0或1个元素)/Flux(返回0-n个元素)

@GetMapping("mono")

public Mono<Object> mono() {

return Mono.create(monoSink -> {

log.info("创建 Mono");

monoSink.success("hello webflux");

})

.doOnSubscribe(subscription -> { //当订阅者去订阅发布者的时候，该方法会调用

log.info("doOnSubscribe={}", subscription);

}).doOnNext(next -> { //当订阅者收到数据时，改方法会调用

log.info("doOnNext={}", next);

});

}

从 Supplier 创建 Mono

@GetMapping("/get")

private Mono<String> get() {

log.info("start");

Mono<String> result = Mono.fromSupplier(() -> {

try {

TimeUnit.SECONDS.sleep(5);

} catch (InterruptedException e) {

}

return "netkiller";

});

log.info("end");

return result;

}

3.7. Flux 返回多条数据

返回 List

@GetMapping("flux")

public Flux<Picture> flux() {

List<Picture> list = new ArrayList<Picture>();

IntStream.range(1, 10).forEach(i -> {

Picture picture = new Picture();

picture.setId(Long.valueOf(i));

picture.setImage("https://www.netkiller.cn/images/" + i + ".png");

list.add(picture);

});

return Flux.fromIterable(list);

}

返回 Map

@GetMapping("map")

public Flux<Map.Entry<String, String>> map() {

Map<String, String> map = new HashMap<>();

IntStream.range(1, 10).forEach(i -> {

map.put("key" + i, "value" + i);

});

return Flux.fromIterable(map.entrySet());

}

从 Stream 返回 Flux

@GetMapping(path = "/sse", produces = MediaType.TEXT_EVENT_STREAM_VALUE)

private Flux<String> getWords() {

Stream<String> items = Arrays.asList("alpha", "bravo", "charlie").stream();

return Flux.fromStream(items);

}

3.8. SSE

一次性事件

@GetMapping(path = "/sse", produces = MediaType.TEXT_EVENT_STREAM_VALUE)

public Flux<String> createConnectionAndSendEvents() {

return Flux.just("Alpha", "Omega");

}

curl 访问 SSE 需要设置HTTP头 -H "Accept: text/event-stream"

neo@MacBook-Pro-M2 ~ % curl -H "Accept: text/event-stream" -X 'GET' 'http://localhost:8080/mock/sse'

data:Alpha

data:Omega

提示

Safari 浏览器不支持 SSE推送，微软的 Egde 支持。

周期性事件

每间隔一秒发送一次数据

@GetMapping(path = "/sse", produces = MediaType.TEXT_EVENT_STREAM_VALUE)

private Flux<String> getWords() {

String[] WORDS = "The quick brown fox jumps over the lazy dog.".split(" ");

return Flux

.zip(Flux.just(WORDS), Flux.interval(Duration.ofSeconds(1)))

.map(Tuple2::getT1);

}

@GetMapping("/random")

public Flux<ServerSentEvent<Integer>> randomNumbers() {

return Flux.interval(Duration.ofSeconds(1)).map(seq -> Tuples.of(seq, ThreadLocalRandom.current().nextInt())).map(data -> ServerSentEvent.<Integer>builder().event("random").id(Long.toString(data.getT1())).data(data.getT2()).build());

}

@GetMapping(path = "/stream-flux", produces = MediaType.TEXT_EVENT_STREAM_VALUE)

public Flux<String> streamFlux() {

return Flux.interval(Duration.ofSeconds(1))

.map(sequence -> "Flux - " + LocalTime.now().toString());

}

SSE 完整的例子

package cn.netkiller.webflux.controller;

import java.time.Duration;

import java.util.concurrent.ThreadLocalRandom;

import org.springframework.http.MediaType;

import org.springframework.http.codec.ServerSentEvent;

import org.springframework.web.bind.annotation.GetMapping;

import org.springframework.web.bind.annotation.RequestMapping;

import org.springframework.web.bind.annotation.RestController;

import reactor.core.publisher.Flux;

import reactor.util.function.Tuples;

@RestController

@RequestMapping("/sse")

public class SseController {

private int count_down = 10;

public SseController() {

}

@GetMapping(value = "/launch", produces = MediaType.TEXT_EVENT_STREAM_VALUE)

public Flux<ServerSentEvent<Object>> countDown() {

return Flux.interval(Duration.ofSeconds(1)).map(seq -> Tuples.of(seq, getCountDownSec())).map(data -> ServerSentEvent.<Object>builder().event("launch").id(Long.toString(data.getT1())).data(data.getT2().toString()).build());

}

private String getCountDownSec() {

if (count_down > 0) {

count_down--;

return "倒计时：" + count_down;

}

return "发射";

}

@GetMapping("/range")

public Flux<Object> range() {

return Flux.range(10, 1).map(seq -> Tuples.of(seq, getCountDownSec())).map(data -> ServerSentEvent.<Object>builder().event("launch").id(Long.toString(data.getT1())).data(data.getT2().toString()).build());

}

// WebFlux 服务器推送(SSE - >Server Send Event)

@GetMapping(value = "/sse", produces = MediaType.TEXT_EVENT_STREAM_VALUE)

private Flux<String> flux() {

Flux<String> result = Flux.fromStream(IntStream.range(1, 10).mapToObj(i -> {

try {

TimeUnit.SECONDS.sleep(1);

} catch (InterruptedException e) {

}

logger.info("sse " + i);

return "flux data -- " + i;

}));

return result;

}

}

运行结果

id:0

event:launch

data:倒计时：9

id:1

event:launch

data:倒计时：8

id:2

event:launch

data:倒计时：7

id:3

event:launch

data:倒计时：6

id:4

event:launch

data:倒计时：5

id:5

event:launch

data:倒计时：4

id:6

event:launch

data:倒计时：3

id:7

event:launch

data:倒计时：2

id:8

event:launch

data:倒计时：1

id:9

event:launch

data:倒计时：0

id:10

event:launch

data:发射

SSE Client 订阅实例

@GetMapping("/server")

public Flux<ServerSentEvent<String>> streamEvents() {

return Flux.interval(Duration.ofSeconds(1))

.map(sequence -> ServerSentEvent.<String>builder()

.id(String.valueOf(sequence))

.event("test-event")

.data("LocalTime: " + LocalTime.now())

.build());

}

@GetMapping("/client")

public void consumeServerSentEvent() {

WebClient client = WebClient.create("http://localhost:8080");

ParameterizedTypeReference<ServerSentEvent<String>> type

= new ParameterizedTypeReference<ServerSentEvent<String>>() {

};

Flux<ServerSentEvent<String>> eventStream = client.get()

.uri("mock/server")

.retrieve()

.bodyToFlux(type);

eventStream.subscribe(

content -> log.info("Time: {} - event: name[{}], id [{}], content[{}] ",

LocalTime.now(), content.event(), content.id(), content.data()),

error -> log.error("Error receiving SSE: {}", error),

() -> log.info("Completed!!!"));

}

3.9. WebClient

配置 WebClient

@Configuration

public class WebConfig {

@Bean

public WebClient webClient() {

WebClient webClient = WebClient.builder()

.baseUrl("http://localhost:8080")

.defaultCookie("cookie-name", "cookie-value")

.defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)

.build();

}

}

@Controller/@RestController 实例

@GetMapping("webclient")

public Mono<String> webclient() {

WebClient webClient = WebClient.create("http://localhost:8080");

Mono<String> response = webClient

.get().uri("/mock/mono")

.retrieve()

.bodyToMono(String.class);

response.subscribe(System.out::println);

return response;

}

会返结果

public Mono<ResponseEntity<Employee>> createEmployee(Employee newEmployee) {

return webClient.post()

.uri("/employees")

.contentType(MediaType.APPLICATION_JSON)

.bodyValue(newEmployee)

.retrieve()

.toEntity(Employee.class);

}

@PostMapping("/create")

public Mono<ResponseEntity<?>> createEmployee(@RequestBody Employee newEmployee) {

return employeeService.createEmployee(newEmployee)

.map(responseEntity -> {

if (responseEntity.getStatusCode().is2xxSuccessful()) {

return ResponseEntity.ok(responseEntity.getBody());

} else {

return ResponseEntity.status(responseEntity.getStatusCode())

.body("Failed to create employee");

}

})

.onErrorResume(exception -> {

return Mono.just(ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)

.body("Internal Server Error: " + exception.getMessage()));

});

}

Get 请求实例

WebClient.create("http://localhost:8080")

.get()

.uri("/students")

.retrieve()

.bodyToFlux(Student.class);

URI 参数

字符串拼接方式

WebClient.create("http://localhost:8080")

.get()

.uri("/students/" + studentId)

.retrieve()

.bodyToMono(Student.class);

通过 uriBuilder 组装 Uri 参数

String endpoint = "/employees";

UriComponentsBuilder uriBuilder = UriComponentsBuilder.fromPath(endpoint)

.queryParam("param1", "value1")

.queryParam("param2", "value2");

webClient.post()

.uri(uriBuilder.build().toUri())

.bodyValue(new Employee(...))

.retrieve()

.bodyToMono(Employee.class);

WebClient.create("http://localhost:8080")

.get()

.uri(uriBuilder -> uriBuilder

.path("/student/{studentId}")

.build(studentId))

.retrieve()

.bodyToMono(Student.class);

WebClient.create("http://localhost:8080")

.get()

.uri(uriBuilder -> uriBuilder

.path("/student/{studentId}/assignments/{assignmentId}")

.build(studentId, assignmentId))

.retrieve()

.bodyToMono(Student.class);

uriTemplate 组装 Uri 参数

UriTemplate uriTemplate = new UriTemplate(

"/student/{studentId}/assignments/{assignmentId}");

WebClient.create("http://localhost:8080")

.get()

.uri(uriTemplate.expand(studentId, assignmentId))

.retrieve()

.bodyToMono(Student.class);

查询参数

http://localhost:8080/students?firstName=Jon&year=1996

String firstName = "Jon";

String year = "1996";

WebClient.create("http://localhost:8080")

.get()

.uri(uriBuilder -> uriBuilder.path("/students")

.queryParam("firstName", firstName)

.queryParam("year", year)

.build())

.retrieve()

.bodyToMono(Student.class);

http://localhost:8080/students?year=1995,1996,1997

WebClient.create("http://localhost:8080")

.get()

.uri(uriBuilder -> uriBuilder.path("/students")

.queryParam("year", String.join(",", "1995", "1996", "1997"))

.build())

.retrieve()

.bodyToMono(Student.class);

"/products/?category=Phones&category=Tablets"

webClient.get()

.uri(uriBuilder - > uriBuilder

.path("/products/")

.queryParam("category", "Phones", "Tablets")

.build())

.retrieve()

.bodyToMono(String.class)

.onErrorResume(e -> Mono.empty())

.block();

Post 操作演示

Employee newEmployee = ...;  //Create a new employee object

webClient.post()

.uri("/employees")

.bodyValue(BodyInserters.fromValue(newEmployee))

.retrieve()

.toEntity(Employee.class)   //Change here

.subscribe(

responseEntity -> {

// Handle success response here

HttpStatusCode status = responseEntity.getStatusCode();

URI location = responseEntity.getHeaders().getLocation();

Employee createdEmployee = responseEntity.getBody();    // Response body

// handle response as necessary

},

error -> {

// Handle the error here

if (error instanceof WebClientResponseException) {

WebClientResponseException ex = (WebClientResponseException) error;

HttpStatusCode status = ex.getStatusCode();

System.out.println("Error Status Code: " + status.value());

//...

} else {

// Handle other types of errors

System.err.println("An unexpected error occurred: " + error.getMessage());

}

}

);

Post 表单数据

@Service

public class EmployeeService {

private final WebClient webClient;

@Autowired

public EmployeeService(WebClient webClient) {

this.webClient = webClient;

}

public Mono<Employee> createEmployee(Map<String, String> formParams) {

return webClient.post()

.uri("/employees")

.body(BodyInserters.fromFormData("id", formParams.get("id"))

.with("name", formParams.get("name"))

.with("status", formParams.get("status")))

.retrieve()

.onStatus(HttpStatus::is4xxClientError, clientResponse -> {

// Handle 4xx client errors here

})

.onStatus(HttpStatus::is5xxServerError, clientResponse -> {

// Handle 5xx server errors here

})

.toEntity(Employee.class)

.flatMap(responseEntity -> Mono.justOrEmpty(responseEntity.getBody()));

}

}

WebClient client = WebClient.create("https://www.netkiller.cn");

FormInserter formInserter = fromMultipartData("name","neo")

.with("age",19)

.with("map",ImmutableMap.of("sex","F"))

.with("file",new File("/tmp/netkiler.doc"));

Mono<String> result = client.post()

.uri("/article/index/{id}.html", 256)

.contentType(MediaType.APPLICATION_JSON)

.body(formInserter)

//.bodyValue(ImmutableMap.of("name","neo"))

.retrieve()

.bodyToMono(String.class);

result.subscribe(System.err::println);

上传文件

MultipartBodyBuilder builder = new MultipartBodyBuilder();

builder.part("file", new FileSystemResource("/tmp/file.txt"));

builder.part("id", "190001", MediaType.TEXT_PLAIN);

builder.part("name", "Lokesh", MediaType.TEXT_PLAIN);

builder.part("status", "active", MediaType.TEXT_PLAIN);

Then we can submit the multipart form data by using the method BodyInserters.fromMultipartData(builder.build()) and send a normal request as in the previous examples.

webClient.post()

.uri("/employees")

.contentType(MediaType.MULTIPART_FORM_DATA)

.body(BodyInserters.fromMultipartData(builder.build()))

.retrieve()

.toEntity(Employee.class)

.doOnError(WriteTimeoutException.class, ex -> {

System.err.println("WriteTimeout");

})

.subscribe(responseEntity -> {

System.out.println("Status: " + responseEntity.getStatusCode().value());

System.out.println("Location URI: " + responseEntity.getHeaders().getLocation().toString());

System.out.println("Created New Employee : " + responseEntity.getBody());

});

设置 HTTP 头

webClient.get()

.uri("/employees")

.bodyValue(new Employee(...))

.header("Authorization", "Bearer auth-token")

.header("User-Agent", "Mobile App 1.0")

.retrieve()

WebClient.builder()

.defaultCookie("session","f1d83210-0fc9-4689-82ab-05df70da3367")

.defaultUriVariables(ImmutableMap.of("name","kl"))

.defaultHeader("header","neo")

.defaultHeaders(httpHeaders -> {

httpHeaders.add("header1","neo");

httpHeaders.add("header2","chen");

})

.defaultCookies(cookie ->{

cookie.add("cookie1","neo");

cookie.add("cookie2","netkiller");

})

.baseUrl("https://www.netkiller.cn")

.build();

websocket

WebSocketClient client = new ReactorNettyWebSocketClient();

URI url = new URI("ws://localhost:8080/path");

client.execute(url, session ->

session.receive()

.doOnNext(System.out::println)

.then());

同步阻塞等待结果

WebClient client =  WebClient.create("http://www.kailing.pub");

String result = client .get()

.uri("/article/index/arcid/{id}.html", 256)

.retrieve()

.bodyToMono(String.class)

.block();

System.err.println(result);

避免单独阻塞每个同步响应

WebClient client =  WebClient.create("http://www.kailing.pub");

Mono<String> result1Mono = client .get()

.uri("/article/index/arcid/{id}.html", 255)

.retrieve()

.bodyToMono(String.class);

Mono<String> result2Mono = client .get()

.uri("/article/index/arcid/{id}.html", 254)

.retrieve()

.bodyToMono(String.class);

Map<String,String>  map = Mono.zip(result1Mono, result2Mono, (result1, result2) -> {

Map<String, String> arrayList = new HashMap<>();

arrayList.put("result1", result1);

arrayList.put("result2", result2);

return arrayList;

}).block();

System.err.println(map.toString());

4. Webflux 安全

4.1. Token 拦截器

package cn.netkiller.config;

import lombok.extern.slf4j.Slf4j;

import org.springframework.core.annotation.Order;

import org.springframework.http.HttpStatus;

import org.springframework.http.MediaType;

import org.springframework.http.server.reactive.ServerHttpResponse;

import org.springframework.stereotype.Component;

import org.springframework.web.server.ServerWebExchange;

import org.springframework.web.server.WebFilter;

import org.springframework.web.server.WebFilterChain;

import reactor.core.publisher.Mono;

@Component

@Order

@Slf4j

public class TokenWebFilter implements WebFilter {

@Override

public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {

log.info(exchange.getRequest().getURI().toString());

if (!exchange.getRequest().getHeaders().containsKey("token")) {

ServerHttpResponse response = exchange.getResponse();

response.setStatusCode(HttpStatus.FORBIDDEN);

response.getHeaders().setContentType(MediaType.APPLICATION_JSON);

return response.writeWith(Mono.just(response.bufferFactory().wrap("{\"msg\":\"no token\"}".getBytes())));

} else {

exchange.getAttributes().put("auth", "true");

return chain.filter(exchange).doFinally(s -> {

log.info("request after, url:{}, statusCode:{}", exchange.getRequest().getURI(), exchange.getResponse().getStatusCode());

});

}

}

}

4.2. JWT

iss (issuer)：签发人

exp (expiration time)：过期时间

sub (subject)：主题

aud (audience)：受众

nbf (Not Before)：生效时间

iat (Issued At)：签发时间

jti (JWT ID)：编号

package cn.netkiller.config;

import cn.netkiller.component.JwtTokeComponent;

import cn.netkiller.utils.ResponseJson;

import com.google.gson.Gson;

import lombok.extern.slf4j.Slf4j;

import org.springframework.beans.factory.annotation.Autowired;

import org.springframework.core.annotation.Order;

import org.springframework.core.io.buffer.DataBuffer;

import org.springframework.http.HttpStatus;

import org.springframework.http.MediaType;

import org.springframework.http.server.reactive.ServerHttpResponse;

import org.springframework.stereotype.Component;

import org.springframework.util.AntPathMatcher;

import org.springframework.web.server.ServerWebExchange;

import org.springframework.web.server.WebFilter;

import org.springframework.web.server.WebFilterChain;

import reactor.core.publisher.Mono;

@Component

@Order

@Slf4j

public class TokenWebFilter implements WebFilter {

private static final String[] patterns = {"/token", "/verifier", "/mock/*", "/swagger/*", "/badges/**"};

private final AntPathMatcher pathMatcher = new AntPathMatcher();

@Autowired

private JwtTokeComponent jwtTokeComponent;

public TokenWebFilter() {

}

@Override

public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {

exchange.getFormData().subscribe(System.out::println);

exchange.getFormData().doOnNext(n -> {

n.forEach((k, v) -> {

log.info("K: {}, V: {}", k, v);

});

});

log.info("No Token");

String path = exchange.getRequest().getPath().toString();

for (String pattern : patterns) {

if (pathMatcher.match(pattern, path)) {

log.info("Permit Pattern '" + pattern + "' matches path '" + path + "'");

return chain.filter(exchange);

}

}

if (!exchange.getRequest().getHeaders().containsKey("token")) {

ServerHttpResponse response = exchange.getResponse();

response.setStatusCode(HttpStatus.FORBIDDEN);

response.getHeaders().setContentType(MediaType.APPLICATION_JSON);

Mono<DataBuffer> message = Mono.just(response.bufferFactory().wrap(new ResponseJson(false, ResponseJson.Code.TokenException, "请提供 Token", null).toString().getBytes()));

return response.writeWith(message);

} else {

String token = exchange.getRequest().getHeaders().getFirst("token");

log.info("token: " + token);

ResponseJson jwt = jwtTokeComponent.verifier(token);

log.info("jwt: " + jwt.isStatus());

if (jwt.isStatus()) {

return chain.filter(exchange);

} else {

ServerHttpResponse response = exchange.getResponse();

response.setStatusCode(HttpStatus.FORBIDDEN);

response.getHeaders().setContentType(MediaType.APPLICATION_JSON);

Gson gson = new Gson();

String jsonString = gson.toJson(jwt);

Mono<DataBuffer> message = Mono.just(response.bufferFactory().wrap(jsonString.getBytes()));

return response.writeWith(message);

}

}

}

}

package cn.netkiller.component;

import cn.netkiller.utils.ResponseJson;

import com.auth0.jwt.JWT;

import com.auth0.jwt.algorithms.Algorithm;

import com.auth0.jwt.exceptions.*;

import com.auth0.jwt.interfaces.DecodedJWT;

import com.auth0.jwt.interfaces.JWTVerifier;

import lombok.NoArgsConstructor;

import lombok.extern.slf4j.Slf4j;

import org.springframework.beans.factory.annotation.Value;

import org.springframework.stereotype.Component;

import reactor.core.publisher.Mono;

import java.util.Calendar;

import java.util.Date;

import java.util.List;

@Slf4j

@Component

@NoArgsConstructor

public class JwtTokeComponent {

@Value("${app.expires}")

private int expires;

@Value("${app.audience}")

private String audience;

@Value("${app.id}")

private String appId;

@Value("${app.key}")

private String appKey;

@Value("${app.secret}")

private String secret;

@Value("${app.subject}")

private String subject;

@Value("#{'${app.role}'.split(',')}")

private List role;

public ResponseJson verifier(String token) {

ResponseJson response;

try {

DecodedJWT jwt = this.verify(token);

response = new ResponseJson(true, ResponseJson.Code.SUCCESS, "Token 校验成功", jwt);

} catch (SignatureVerificationException e) {

response = new ResponseJson(false, ResponseJson.Code.TokenException, e.getMessage(), "Token 签名失败");

} catch (TokenExpiredException e) {

response = new ResponseJson(false, ResponseJson.Code.TokenExpiredException, e.getMessage(), "Token 过期");

} catch (AlgorithmMismatchException e) {

response = new ResponseJson(false, ResponseJson.Code.AlgorithmMismatchException, e.getMessage(), "Token 签名算法异常");

} catch (JWTVerificationException e) {

response = new ResponseJson(false, ResponseJson.Code.TokenException, e.getMessage(), "Token 校验失败");

} catch (Exception e) {

response = new ResponseJson(false, ResponseJson.Code.Exception, e.getMessage(), "Token 异常");

}

log.error(response.toString());

return response;

}

public Mono<String> getToken(String appId, String appKey) {

Calendar instance = Calendar.getInstance();

instance.add(Calendar.DATE, expires);

//        instance.add(Calendar.SECOND, 30);

try {

//            Algorithm algorithm = Algorithm.RSA256(rsaPublicKey, rsaPrivateKey);

Algorithm algorithm = Algorithm.HMAC256(secret);

String token = JWT.create()

.withJWTId(appKey)

.withIssuer(appId)

.withIssuedAt(new Date())

.withSubject(subject)

.withKeyId(appKey)

.withAudience(audience)

.withClaim("role", role)

.withExpiresAt(instance.getTime())

.sign(algorithm);

return Mono.just(token);

} catch (JWTCreationException exception) {

log.error(exception.getMessage());

}

return Mono.empty();

}

public DecodedJWT verify(String token) {

Algorithm algorithm = Algorithm.HMAC256(secret);

JWTVerifier verifier = JWT.require(algorithm)

// specify an specific claim validations

.withIssuer(appId)

.withJWTId(appKey)

.withSubject(subject)

.withAudience(audience)

// reusable verifier instance

.build();

DecodedJWT decodedJWT = verifier.verify(token);

log.info(decodedJWT.getClaims().toString());

return decodedJWT;

}

}

4.3. spring-boot-starter-security

<dependency>

<groupId>org.springframework.boot</groupId>

<artifactId>spring-boot-starter-security</artifactId>

</dependency>

默认用户名是 user，密码会打印到终端

2024-01-03T18:52:01.027+08:00  WARN 33537 --- [watch-development] [           main] .s.s.UserDetailsServiceAutoConfiguration :

Using generated security password: 823fcdcc-e2dd-4967-84b4-546db9175357

This generated password is for development use only. Your security configuration must be updated before running your application in production.

2024-01-03T18:52:01.081+08:00  INFO 33537 --- [watch-development] [           main] o.s.b.a.e.web.EndpointLinksResolver      : Exposing 13 endpoint(s) beneath base path '/actuator'

访问方式

neo@MacBook-Pro-M2 ~> curl -X 'GET' 'http://user:823fcdcc-e2dd-4967-84b4-546db9175357@localhost:8080/mock/mono'

hello webflux⏎

修改密码，在配置文件中增加

#

spring.security.user.name=user

spring.security.user.password=123456

#

package cn.netkiller.config;

import org.springframework.context.annotation.Bean;

import org.springframework.context.annotation.Configuration;

import org.springframework.core.Ordered;

import org.springframework.core.annotation.Order;

import org.springframework.security.config.annotation.method.configuration.EnableReactiveMethodSecurity;

import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity;

import org.springframework.security.config.web.server.ServerHttpSecurity;

import org.springframework.security.core.userdetails.MapReactiveUserDetailsService;

import org.springframework.security.core.userdetails.User;

import org.springframework.security.core.userdetails.UserDetails;

import org.springframework.security.web.server.SecurityWebFilterChain;

@Configuration

@EnableWebFluxSecurity

@EnableReactiveMethodSecurity

public class SecurityConfig {

//    @Autowired

//    private TokenWebFilter tokenWebFilter;

@Bean

public MapReactiveUserDetailsService userDetailsService() {

UserDetails user = User.withDefaultPasswordEncoder()

.username("user")

.password("user")

.roles("USER")

.build();

return new MapReactiveUserDetailsService(user);

}

@Order(Ordered.HIGHEST_PRECEDENCE)

@Bean

SecurityWebFilterChain filterChain(ServerHttpSecurity httpSecurity) throws Exception {

httpSecurity.csrf().disable()

.authorizeExchange(exchanges -> exchanges

.pathMatchers("/").permitAll()

.pathMatchers("/token", "/verifier").permitAll()

.pathMatchers("/mock/*").permitAll()

.pathMatchers("/ping", "/version").permitAll()

.pathMatchers("/badges/**", "/chat/**", "/status/**", "/picture/**").permitAll()

.anyExchange().authenticated()

);

//                .addFilterBefore(tokenWebFilter, SecurityWebFiltersOrder.FIRST);

//                .httpBasic(withDefaults())

//                .formLogin(withDefaults());

return httpSecurity.build();

}

}

5. 常见问题

5.1. The Java/XML config for Spring MVC and Spring WebFlux cannot both be enabled, e.g. via @EnableWebMvc and @EnableWebFlux, in the same application.

是用 @EnableWebFlux 注解是，出现错误。这是因为 Mvc 与 Webflux 不能同时启用，通过下面配置可以解决。

spring.main.web-application-type=reactive

5.2. @EnableWebFluxSecurity 与 @EnableReactiveMethodSecurity 不生效

Mvc 不能与 Webflux 同时存在，默认系统是 Mvc 导致 Security 在 Webflux 下工作不正常。

去掉 Mvc 依赖

<dependency>

<groupId>org.springframework.boot</groupId>

<artifactId>spring-boot-starter-web</artifactId>

</dependency>

删除 @EnableWebSecurity 注解

@EnableWebMvc

@EnableWebSecurity

增加配置

spring.main.web-application-type=reactive

package cn.netkiller.config;

import org.springframework.context.annotation.Bean;

import org.springframework.context.annotation.Configuration;

import org.springframework.core.Ordered;

import org.springframework.core.annotation.Order;

import org.springframework.security.config.annotation.method.configuration.EnableReactiveMethodSecurity;

import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity;

import org.springframework.security.config.web.server.ServerHttpSecurity;

import org.springframework.security.core.userdetails.MapReactiveUserDetailsService;

import org.springframework.security.core.userdetails.User;

import org.springframework.security.core.userdetails.UserDetails;

import org.springframework.security.web.server.SecurityWebFilterChain;

import static org.springframework.security.config.Customizer.withDefaults;

@Configuration

@EnableWebFluxSecurity

@EnableReactiveMethodSecurity

public class SecurityConfig {

@Bean

public MapReactiveUserDetailsService userDetailsService() {

UserDetails user = User.withDefaultPasswordEncoder()

.username("user")

.password("user")

.roles("USER")

.build();

return new MapReactiveUserDetailsService(user);

}

@Order(Ordered.HIGHEST_PRECEDENCE)

@Bean

SecurityWebFilterChain filterChain(ServerHttpSecurity httpSecurity) throws Exception {

httpSecurity

.authorizeExchange(exchanges -> exchanges

.anyExchange().authenticated()

)

.httpBasic(withDefaults())

.formLogin(withDefaults());

return httpSecurity.build();

}

}

5.3. webflux netty 不支持 Content-Type: application/x-www-form-urlencoded

Post 数据无法使用 @RequestParam 读取，只能用于读取 URL 上的 GET 数据

[UnsupportedMediaTypeStatusException: 415 UNSUPPORTED_MEDIA_TYPE "Content type 'application/x-www-form-urlencoded' not supported"]

部分 III. Spring Data

第 47 章 EntityManager

@Repository

@Transactional(readOnly = true)

class AccountServiceImpl implements AccountService {

@PersistenceContext

private EntityManager em;

@Override

@Transactional

public Account save(Account account) {

if (account.getId() == null) {

em.persist(account);

return account;

} else {

return em.merge(account);

}

}

@Override

public List<Account> findByCustomer(Customer customer) {

TypedQuery query = em.createQuery("select a from Account a where a.customer = ?1", Account.class);

query.setParameter(1, customer);

return query.getResultList();

}

@Override

public List<Customer> findAll(int page, int pageSize) {

TypedQuery query = em.createQuery("select c from Customer c", Customer.class);

query.setFirstResult(page * pageSize);

query.setMaxResults(pageSize);

return query.getResultList();

}

}

第 48 章 Spring Data with JdbcTemplate

1. execute

jdbcTemplate.execute("CREATE TABLE USER (id integer, name varchar(100))");

2. queryForInt

int count = jdbcTemplate.queryForInt("SELECT COUNT(*) FROM USER");

3. queryForLong

lang count = jdbcTemplate.queryForLong("SELECT COUNT(*) FROM USER");

4. queryForObject

4.1. 返回整形与字符型

Integer age = queryForObject("select age from emp", Integer.class);

String name = queryForObject("select name from emp",String.class);

4.2. 查询 Double 类型数据库

private double getSumByMemberId(int memberId) {

double result = 0.0d;

String sql = "SELECT sum(o.price::NUMERIC) as total FROM public.order o group by member_id ="+ memberId;

try {

result = jdbcTemplate.queryForObject(sql, Double.class);

} catch (org.springframework.dao.EmptyResultDataAccessException e) {

log.info("{} {}", MemberId, e.toString());

}

return result;

}

4.3. 返回日期

注意 Date 是 java.util 不是 java.sql

private static final Logger log = LoggerFactory.getLogger(ScheduledTasks.class);

private static final SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-mm-dd HH:mm:ss");

@Autowired

private JdbcTemplate jdbcTemplate;

@Scheduled(initialDelay = 1000, fixedRate = 60000)

public void currentDate() {

Date date = jdbcTemplate.queryForObject("select sysdate from dual", Date.class);

log.info("The oracle sysdate is {}", dateFormat.format(date));

}

4.4. 返回结果集

@Autowired

private JdbcTemplate jdbcTemplate;

@RequestMapping(value = "/article")

public @ResponseBody String dailyStats(@RequestParam Integer id) {

String query = "SELECT id, title, content from article where id = " + id;

return jdbcTemplate.queryForObject(query, (resultSet, i) -> {

System.out.println(resultSet.getLong(1) + "," + resultSet.getString(2) + "," + resultSet.getString(3));

return (resultSet.getLong(1) + "," + resultSet.getString(2) + "," + resultSet.getString(3));

});

}

4.5. 通过 "?" 向SQL传递参数

package com.example.api.restful;

import org.springframework.beans.factory.annotation.Autowired;

import org.springframework.jdbc.core.JdbcTemplate;

import org.springframework.web.bind.annotation.PathVariable;

import org.springframework.web.bind.annotation.RequestMapping;

import org.springframework.web.bind.annotation.RequestMethod;

import org.springframework.web.bind.annotation.RestController;

import com.example.api.pojo.ResponseRestful;

@RestController

@RequestMapping("/restful/cms")

public class CmsRestController {

@Autowired

private JdbcTemplate jdbcTemplate;

@RequestMapping(value = "/article/update/count/{articleId}", method = RequestMethod.GET, produces = { "application/xml", "application/json" })

public ResponseRestful updateCount(@PathVariable int articleId) {

String sql = "SELECT count(*) FROM cms.article WHERE id > ?";

int count = jdbcTemplate.queryForObject(sql, new Object[] { articleId }, Integer.class);

return new ResponseRestful(true, 1, "文章更新", count);

}

}

4.6. RowMapper 记录映射

package cn.netkiller.model;

import java.sql.ResultSet;

import java.sql.SQLException;

import org.springframework.jdbc.core.RowMapper;

public class CustomerRowMapper implements RowMapper

{

public Object mapRow(ResultSet rs, int rowNum) throws SQLException {

Customer customer = new Customer();

customer.setId(rs.getInt("ID"));

customer.setName(rs.getString("NAME"));

customer.setAge(rs.getInt("AGE"));

return customer;

}

}

public Customer findByCustomerId(int id){

String sql = "SELECT * FROM CUSTOMER WHERE ID = ?";

Customer customer = (Customer)getJdbcTemplate().queryForObject(

sql, new Object[] { id }, new CustomerRowMapper());

return customer;

}

Member member = this.jdbcTemplate.queryForObject("select first_name, last_name from member where id = ?",new Object[]{112L},

new RowMapper<Member>() {

public Actor mapRow(ResultSet rs, int rowNum) throws SQLException {

Member member = new Member();

member.setFirstName(rs.getString("first_name"));

member.setLastName(rs.getString("last_name"));

return member;

}

});

5. queryForList

List rows = jdbcTemplate.queryForList("SELECT * FROM USER");

Iterator it = rows.iterator();

while(it.hasNext()) {

Map userMap = (Map) it.next();

System.out.print(userMap.get("id") + "\t");

System.out.print(userMap.get("name") + "\t");

System.out.print(userMap.get("sex") + "\t");

System.out.println(userMap.get("age") + "\t");

}

5.1. Iterator 用法

List<Map<String, Object>> rows = jdbcTemplate.queryForList("select * from user_token where address=? and contract_address not in (select contract_address from token)", new Object[] { address });

Iterator<Map<String, Object>> it = rows.iterator();

while (it.hasNext()) {

Map<String, Object> userMap = (Map<String, Object>) it.next();

String contractAddress = (String) userMap.get("contract_address");

String symbol = (String) userMap.get("symbol");

int decimals = (int) userMap.get("decimals");

}

5.2. for 循环

@RequestMapping("/article/tag/{siteId}")

public ResponseRestful tag(@PathVariable int siteId) {

List<Tag> tags = new ArrayList<Tag>();

List<Map<String, Object>> rows = new ArrayList<Map<String, Object>>();

String sql = "SELECT id,name FROM cms.tag WHERE site_id = ?";

rows = jdbcTemplate.queryForList(sql, new Object[] { siteId });

for (Map<String, Object> row : rows) {

Tag tag = new Tag();

tag.setId((Integer) row.get("id"));

tag.setName((String) row.get("name"));

tags.add(tag);

}

logger.info("tag {} SQL: {}", siteId, sql);

return new ResponseRestful(true, tags.size(), "标签", tags);

}

5.3. forEach 用法

jdbcTemplate.queryForList("select id from public.contract").forEach(item -> {

logger.debug(item.toString());

});

6. queryForMap

Map<String, Object> map = this.jdbcTemplate.queryForMap("SELECT * FROM USERS WHERE USERNAME=?", "username");

System.out.println(map.get("USERNAME"));

7. query

7.1. ResultSet

HashMap<String,String> member = jdbcTemplate.query("select name,age from member where id=1", (ResultSet rs) -> {

HashMap<String,String> results = new HashMap<>();

while (rs.next()) {

results.put(rs.getString("name"), rs.getString("age"));

}

return results;

});

7.2. ResultSetExtractor

ResultSetExtractor

HashMap<String,String> member = jdbcTemplate.query("select name,age from member where id=1", new ResultSetExtractor<Map>(){

@Override

public Map extractData(ResultSet rs) throws SQLException,DataAccessException {

HashMap<String,String> mapResult= new HashMap<String,String>();

while(rs.next()){

mapResult.put(rs.getString("name"),rs.getString("age"));

}

return mapResult;

}

});

7.3. RowMapper

List<Actor> actors = this.jdbcTemplate.query("select first_name, last_name from actor",new RowMapper<Actor>() {

public Actor mapRow(ResultSet rs, int rowNum) throws SQLException {

Actor actor = new Actor();

actor.setFirstName(rs.getString("first_name"));

actor.setLastName(rs.getString("last_name"));

return actor;

}

});

public List<Actor> findAllActors() {

return this.jdbcTemplate.query( "select first_name, last_name from actor", new ActorMapper());

}

private static final class ActorMapper implements RowMapper<Actor> {

public Actor mapRow(ResultSet rs, int rowNum) throws SQLException {

Actor actor = new Actor();

actor.setFirstName(rs.getString("first_name"));

actor.setLastName(rs.getString("last_name"));

return actor;

}

}

返回第一条数据，事实上只有一条。

public Token getTokenBySymbol(String symbol) {

List<Token> response = jdbcTemplate.query("select * from token where symbol ='" + symbol + "'", new RowMapper<Token>() {

public Token mapRow(ResultSet result, int rowNum) throws SQLException {

Token Token = new Token();

Token.setContractAddress(result.getString(""));

Token.setName(result.getString("name"));

Token.setSymbol(result.getString("symbol"));

Token.setDecimals(result.getInt("decimals"));

return Token;

}

});

if (response.size() == 1) {

return response.get(0);

}

return null;

}

8. queryForRowSet

SqlRowSet rs = jdbcTemplate.queryForRowSet("select * from test");

9. update

@RequestMapping(value="/comment/add/{siteId}/{articleId}", method = RequestMethod.POST)

public ResponseRestful commentAdd(@PathVariable("siteId") int siteId, @PathVariable("articleId") int articleId, @RequestBody Comment comment) {

String sql = "insert into cms.comment("

+ "article_id, "

+ "ctime,"

+ "content,"

+ "member_id,"

+ "nickname,"

+ "picture"

+ ") values(?,?,now(),?,?,?,?,?)";

int count = jdbcTemplate.update(sql,

comment.getArticleId(),

comment.getContent(),

comment.getMemberId(),

comment.getNickname(),

comment.getPicture()

);

return new ResponseRestful(true, count, "评论添加成功", comment);

}

10. MapSqlParameterSource

new MapSqlParameterSource("symbol", symbol)

11. 实例参考

11.1. 参数传递技巧

public List<PendingTransaction> getPendingTransaction(String address, String contractAddress) {

List<PendingTransaction> pendingTransactions = new ArrayList<PendingTransaction>();

String sql;

Object[] param;

if (contractAddress == null || contractAddress.equals("")) {

sql = "select * from pending_transaction where from_address = ? and contract_address IS NULL";

param = new Object[] { address };

} else {

sql = "select * from pending_transaction where from_address = ? and contract_address = ?";

param = new Object[] { address, contractAddress };

}

List<Map<String, Object>> rows = jdbcTemplate.queryForList(sql, param);

for (Map<String, Object> row : rows) {

PendingTransaction pendingTransaction = new PendingTransaction();

pendingTransaction.setHash((String) row.get("hash"));

pendingTransaction.setFrom((String) row.get("from_address"));

pendingTransaction.setTo((String) row.get("to_address"));

pendingTransaction.setValue((String) row.get("value"));

pendingTransaction.setGas((String) row.get("gas"));

pendingTransaction.setSymbol((String) row.get("symbol"));

pendingTransaction.setContractAddress((String) row.get("contractAddress"));

pendingTransactions.add(pendingTransaction);

}

logger.info("PendingTransaction:" + pendingTransactions.toString());

return pendingTransactions;

}

第 49 章 Spring Data with MySQL

1. 选择数据库表引擎

正常创建表会使用数据库默认引擎，有时数据库默认引擎并不是我们需要的，通过下面配置可以指定表引擎

# Spring boot 1.x.x

spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQLInnoDBDialect

# Spring boot 2.0.2

spring.jpa.hibernate.use-new-id-generator-mappings=true

spring.jpa.database-platform=org.hibernate.dialect.MySQL5InnoDBDialect

2. 声明实体

2.1. @Entity 声明实体

声明 Class 即是数据库表

@Entity

@Table

public class Your_table {

...

...

}

2.2. @Table 定义表名

catalog

@Table(name="CUSTOMERS",catalog="hibernate")

schema

配置Schema

@Table(name="tabname", schema="public")

uniqueConstraints

唯一索引

@Table(name="CUSTOMERS",uniqueConstraints={@UniqueConstraint(columnNames={"name","email"})})

定义多组唯一索引

uniqueConstraints={@UniqueConstraint(columnNames={"name","email"}),@UniqueConstraint(columnNames={"name","age"})}

2.3. @Id 定义主键

ID 字段，数据库中的主键。

@Id

@GeneratedValue(strategy = GenerationType.IDENTITY)

@Column(name = "id", unique = true, nullable = false, insertable = true, updatable = false)

private int id;

@GeneratedValue 主键生成策略：

@GeneratedValue(strategy= GenerationType.IDENTITY) 该注解由数据库自动生成，AUTO_INCREMENT 自增主键，在 mysql 数据库中使用最频繁，oracle 不支持。

@GeneratedValue(strategy= GenerationType.AUTO) 主键由程序控制，默认的主键生成策略，oracle 默认是序列化的方式，mysql 默认是主键自增的方式。

@GeneratedValue(strategy= GenerationType.SEQUENCE) 根据底层数据库的序列来生成主键，条件是数据库支持序列，Oracle支持，Mysql不支持。

@GeneratedValue(strategy= GenerationType.TABLE) 使用一个特定的数据库表格来保存主键，较少使用。

Long = bigint

package cn.netkiller.domain;

import jakarta.persistence.*;

import lombok.Data;

import java.io.Serial;

import java.io.Serializable;

import java.util.Date;

@Entity

@Table

@Data

public class Picture implements Serializable {

@Serial

public static final long serialVersionUID = 1L;

@Id

@GeneratedValue(strategy = GenerationType.IDENTITY)

@Column(name = "id", unique = true, nullable = false, insertable = true, updatable = false)

private Long id;

private String device;

private String model;

private String session;

private String prompt;

private String thumbnail;

private String image;

private String story;

private boolean share;

private int likes;

private int favorites;

private int forward;

private Date ctime;

private Date mtime;

}

CREATE TABLE `picture` (

`id` bigint NOT NULL AUTO_INCREMENT,

`ctime` datetime(6) DEFAULT NULL,

`favorites` int NOT NULL,

`image` varchar(255) DEFAULT NULL,

`likes` int NOT NULL,

`mtime` datetime(6) DEFAULT NULL,

`prompt` varchar(255) DEFAULT NULL,

`session` varchar(255) DEFAULT NULL,

`share` bit(1) NOT NULL,

`story` varchar(255) DEFAULT NULL,

`thumbnail` varchar(255) DEFAULT NULL,

`device` varchar(255) DEFAULT NULL,

`model` varchar(255) DEFAULT NULL,

`forward` int NOT NULL

PRIMARY KEY (`id`)

) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci

字符串做主键

package api.domain;

import javax.persistence.Entity;

import javax.persistence.Id;

import javax.persistence.Table;

@Entity

@Table()

public class TransactionsPostion {

@Id

private String address;

private String startblock;

private String endblock;

public TransactionsPostion() {

// TODO Auto-generated constructor stub

}

public String getAddress() {

return address;

}

public void setAddress(String address) {

this.address = address;

}

public String getStartblock() {

return startblock;

}

public void setStartblock(String startblock) {

this.startblock = startblock;

}

public String getEndblock() {

return endblock;

}

public void setEndblock(String endblock) {

this.endblock = endblock;

}

}

对应数据库表

CREATE TABLE "transactions_postion" (

"address" varchar(255) NOT NULL,

"endblock" varchar(255) DEFAULT NULL,

"startblock" varchar(255) DEFAULT NULL,

PRIMARY KEY ("address")

) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4

2.4. @Column 定义字段：

unique 属性表示该字段是否为唯一标识，默认为false。如果表中有一个字段需要唯一标识，则既可以使用该标记，也可以使用@Table标记中的@UniqueConstraint。

nullable 属性表示该字段是否可以为null值，默认为true。

insertable 属性表示在使用“INSERT”脚本插入数据时，是否需要插入该字段的值。

updatable 属性表示在使用“UPDATE”脚本插入数据时，是否需要更新该字段的值。insertable和updatable属性一般多用于只读的属性，例如主键和外键等。这些字段的值通常是自动生成的。

columnDefinition属性表示创建表时，该字段创建的SQL语句，一般用于通过Entity生成表定义时使用。

table 属性表示当映射多个表时，指定表的表中的字段。默认值为主表的表名。

length 属性表示字段的长度，当字段的类型为varchar时，该属性才有效，默认为255个字符。

precision 属性和scale属性表示精度，当字段类型为double时，precision表示数值的总长度，scale表示小数点所占的位数。

scale int 列的精度，仅对十进制数值有效，表示小数位的总位数。默认为0。

字段长度

字段长度定义

@Column(name="name", length=80, nullable=true)

浮点型

@Column(precision=18, scale=5)

private BigDecimal principal;

@Column(name="Price", columnDefinition="Decimal(10,2) default '100.00'")

创建于更新控制

@Column(name = "ctime", nullable = false, insertable = false, updatable = false)

TEXT 类型

private String subject;

@Column(columnDefinition = "TEXT")

private String content;

整形数据类型

无符号整形

package com.example.api.domain.elasticsearch;

import java.util.Date;

import javax.persistence.Column;

import javax.persistence.Entity;

import javax.persistence.Id;

import javax.persistence.Table;

@Entity

@Table

public class Member {

@Id

private int id;

@Column(columnDefinition = "INT(10) UNSIGNED NOT NULL")

private int age;

@Column(insertable = false, updatable = false, columnDefinition = "TIMESTAMP DEFAULT CURRENT_TIMESTAMP")

private Date ctime;

@Column(nullable = true, insertable = false, updatable = false, columnDefinition = "TIMESTAMP NULL DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP")

private Date mtime;

@Column(columnDefinition = "enum('Y','N') DEFAULT 'N'")

private boolean status;

}

CREATE TABLE `member` (

`id` int(11) NOT NULL,

`age` int(10) unsigned NOT NULL,

`ctime` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,

`mtime` timestamp NULL DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP,

`status` enum('Y','N') DEFAULT 'N',

PRIMARY KEY (`id`)

) ENGINE=InnoDB DEFAULT CHARSET=utf8

2.5. 非数据库字段

@Transient 该注解标注的字段不会被应射到数据库当中

2.6. @Lob 注解属性将被持久化为 Blog 或 Clob 类型

Clob（Character Large Ojects）类型是长字符串类型，具体的java.sql.Clob, Character[], char[] 和 java.lang.String 将被持久化为 Clob 类型。

Blob（Binary Large Objects）类型是字节类型，具体的java.sql.Blob, Byte[], byte[] 和 serializable type 将被持久化为 Blob 类型。

@Lob 持久化为Blob或者Clob类型,根据get方法的返回值不同,自动进行Clob和Blob的转换。

因为这两种类型的数据一般占用的内存空间比较大，所以通常使用延迟加载的方式，与@Basic标记同时使用，设置加载方式为FetchType.LAZY。

@Lob

@Basic(fetch = FetchType.LAZY)

@Column(name="content", columnDefinition="CLOB", nullable=true)

public String getContent() {

return content;

}

2.7. @NotNull 不能为空声明

@NotNull

public String username;

2.8. @Temporal 日期定义

@Entity

public class Article {

@Id

@GeneratedValue

Integer id;

@Temporal(TemporalType.DATE)

Date publicationDate;

@Temporal(TemporalType.TIME)

Date publicationTime;

@Temporal(TemporalType.TIMESTAMP)

Date creationDateTime;

}

2.9. 创建日期

@Column(name = "create_at")

@CreatedDate

private Timestamp create_date;

CreatedDate

Spring 提供了 import org.springframework.data.annotation.CreatedDate;

但是这些只能作用于实体类。

@CreatedDate

private Date createdDateTime;

与时间日期有关的 hibernate 注解

设置默认时间

@Column(insertable = false)

@org.hibernate.annotations.ColumnDefault("1.00")

@org.hibernate.annotations.Generated(

org.hibernate.annotations.GenerationTime.INSERT

)

protected Date lastModified;

创建时间

@Temporal(TemporalType.TIMESTAMP)

@Column(updatable = false)

@org.hibernate.annotations.CreationTimestamp

protected Date createdDate;

更新时间

@Column(name="update_time")

@org.hibernate.annotations.UpdateTimestamp

@Temporal(TemporalType.TIMESTAMP)

private Date updateTime;

@Temporal(TemporalType.TIMESTAMP)

@Column(insertable = false, updatable = false)

@org.hibernate.annotations.Generated(

org.hibernate.annotations.GenerationTime.ALWAYS

)

数据库级别的默认创建日期时间定义

package cn.netkiller.api.domain.elasticsearch;

import java.util.Date;

import javax.persistence.Column;

import javax.persistence.Entity;

import javax.persistence.Id;

import javax.persistence.Table;

@Entity

@Table

public class ElasticsearchTrash {

@Id

private int id;

@Column(columnDefinition = "TIMESTAMP DEFAULT CURRENT_TIMESTAMP")

private Date ctime;

public int getId() {

return id;

}

public void setId(int id) {

this.id = id;

}

public Date getCtime() {

return ctime;

}

public void setCtime(Date ctime) {

this.ctime = ctime;

}

}

对应数据库DDL

CREATE TABLE `elasticsearch_trash` (

`id` int(11) NOT NULL,

`ctime` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,

PRIMARY KEY (`id`)

) ENGINE=InnoDB DEFAULT CHARSET=utf8;

数据库级别的默认创建日期与更新时间定义

需求是这样的：

1. 创建时间与更新时间只能由数据库产生，不允许在实体类中产生，因为每个节点的时间/时区不一定一直。另外防止人为插入自定义时间时间。

2. 插入记录的时候创建默认时间，创建时间不能为空，时间一旦插入不允许日后在实体类中修改。

3. 记录创建后更新日志字段为默认为 null 表示该记录没有被修改过。一旦数据被修改，修改日期字段将记录下最后的修改时间。

4. 甚至你可以通过触发器实现一个history 表，用来记录数据的历史修改，详细请参考作者另一部电子书《Netkiller Architect 手札》数据库设计相关章节。

package cn.netkiller.api.domain.elasticsearch;

import java.util.Date;

import javax.persistence.Column;

import javax.persistence.Entity;

import javax.persistence.Id;

import javax.persistence.Table;

import javax.validation.constraints.Null;

@Entity

@Table

public class ElasticsearchTrash {

@Id

private int id;

// 创建时间

@Column(insertable = false, updatable = false, columnDefinition = "TIMESTAMP DEFAULT CURRENT_TIMESTAMP")

private Date ctime;

// 修改时间

@Column(nullable = true, insertable = false, updatable = false, columnDefinition = "TIMESTAMP NULL DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP")

private Date mtime;

public int getId() {

return id;

}

public void setId(int id) {

this.id = id;

}

public Date getCtime() {

return ctime;

}

public void setCtime(Date ctime) {

this.ctime = ctime;

}

public Date getMtime() {

return mtime;

}

public void setMtime(Date mtime) {

this.mtime = mtime;

}

}

对应数据库DDL

CREATE TABLE `elasticsearch_trash` (

`id` int(11) NOT NULL,

`ctime` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,

`mtime` timestamp NULL DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP,

PRIMARY KEY (`id`)

) ENGINE=InnoDB DEFAULT CHARSET=utf8

最后修改时间

需求：记录最后一次修改时间

package cn.netkiller.api.domain.elasticsearch;

import java.util.Date;

import javax.persistence.Column;

import javax.persistence.Entity;

import javax.persistence.Id;

import javax.persistence.Table;

@Entity

@Table

public class ElasticsearchTrash {

@Id

private int id;

@Column(columnDefinition = "TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP")

private Date lastModified;

}

产生DDL语句如下

CREATE TABLE `elasticsearch_trash` (

`id` int(11) NOT NULL,

`last_modified` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,

PRIMARY KEY (`id`)

) ENGINE=InnoDB DEFAULT CHARSET=utf8;

2.10. @DateTimeFormat 处理日期时间格式

public java.sql.Date createdate; 创建日期 YYYY-MM-DD

public java.util.Date finisheddate; 创建日期时间 YYYY-MM-DD HH:MM:SS

Json默认为 yyyy-MM-ddTHH:mm:ss 注意日期与时间中间的T，修改日期格式将T去掉

@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")

@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")

private Date createDate;

/**

* 日期 DATE  YYYY-MM-DD

*/

@Column(name = "create_date")

@JsonFormat(shape= JsonFormat.Shape.STRING,pattern="yyyy-MM-dd HH:mm:ss",timezone="GMT+8")

private Date date;

下面我们实际演示一下，例如默认返回 "ctime": "2024-01-25T08:07:39.000+00:00" 这样的格式

HTTP/1.1 200

Content-Type: application/json

Transfer-Encoding: chunked

Date: Thu, 25 Jan 2024 08:10:32 GMT

Connection: close

{

"status": true,

"code": "SUCCESS",

"data": [

{

"id": 3918,

"session": "27310934-4159-4bc9-8142-67a3780faf35",

"progress": "1.音频上传",

"description": "test.amr",

"ctime": "2024-01-25T08:07:16.000+00:00"

},

{

"id": 3919,

"session": "27310934-4159-4bc9-8142-67a3780faf35",

"progress": "2.音频转换",

"description": "AMR 转 PCM",

"ctime": "2024-01-25T08:07:16.000+00:00"

},

{

"id": 3920,

"session": "27310934-4159-4bc9-8142-67a3780faf35",

"progress": "3.语音识别",

"description": "小明是个调皮的小男孩，他觉得地板上有很多污垢，于是他拿起了牙刷用它来刷地板，虽然效果不佳，但是小明却觉得是一种有趣的尝试。",

"ctime": "2024-01-25T08:07:36.000+00:00"

},

{

"id": 3921,

"session": "27310934-4159-4bc9-8142-67a3780faf35",

"progress": "4.内容合规",

"description": "合规：小明是个调皮的小男孩，他觉得地板上有很多污垢，于是他拿起了牙刷用它来刷地板，虽然效果不佳，但是小明却觉得是一种有趣的尝试。",

"ctime": "2024-01-25T08:07:36.000+00:00"

},

{

"id": 3922,

"session": "27310934-4159-4bc9-8142-67a3780faf35",

"progress": "5.故事创作",

"description": "小明，一个满腔调皮的小男孩，发现家里仿佛被污秽覆盖。他拿起牙刷，一面一面地在地板上划过，结果却不尽人意。然而，这个看似毫无预期的尝试，他自己却获得了无比的乐趣。\n\n问题：小明的行为可能使他的生活环境变得更糟糕，你会如何帮助他改变这种状况？",

"ctime": "2024-01-25T08:07:36.000+00:00"

},

{

"id": 3923,

"session": "27310934-4159-4bc9-8142-67a3780faf35",

"progress": "6.语音合成",

"description": "http://oss.test.netkiller.cn/2024/01/25/27310934-4159-4bc9-8142-67a3780faf35.mp3",

"ctime": "2024-01-25T08:07:39.000+00:00"

}

],

"reason": "操作成功"

}

加入 @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") 注解

package cn.netkiller.domain;

import com.fasterxml.jackson.annotation.JsonFormat;

import jakarta.persistence.*;

import lombok.Data;

import org.hibernate.annotations.Comment;

import org.hibernate.annotations.DynamicUpdate;

import java.io.Serial;

import java.io.Serializable;

import java.util.Date;

@Entity

@Table

@DynamicUpdate

@Data

public class SessionStatus implements Serializable {

@Serial

public static final long serialVersionUID = 1L;

@Id

@GeneratedValue(strategy = GenerationType.IDENTITY)

@Column(name = "id", unique = true, nullable = false, insertable = false, updatable = false, columnDefinition = "int unsigned")

@Comment("主键")

private Long id;

@Comment("会话主键")

private String session;

@Comment("进度")

private String progress;

@Lob

@Basic(fetch = FetchType.LAZY)

@Column(nullable = true, columnDefinition = "text")

@Comment("描述")

private String description;

@Column(insertable = false, updatable = false, columnDefinition = "TIMESTAMP DEFAULT CURRENT_TIMESTAMP")

@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")

@Comment("创建时间")

private Date ctime;

}

日期被格式化为 "ctime": "2024-01-25 08:07:16"

HTTP/1.1 200

Content-Type: application/json

Transfer-Encoding: chunked

Date: Thu, 25 Jan 2024 08:12:10 GMT

Connection: close

{

"status": true,

"code": "SUCCESS",

"data": [

{

"id": 3918,

"session": "27310934-4159-4bc9-8142-67a3780faf35",

"progress": "1.音频上传",

"description": "test.amr",

"ctime": "2024-01-25 08:07:16"

},

{

"id": 3919,

"session": "27310934-4159-4bc9-8142-67a3780faf35",

"progress": "2.音频转换",

"description": "AMR 转 PCM",

"ctime": "2024-01-25 08:07:16"

},

{

"id": 3920,

"session": "27310934-4159-4bc9-8142-67a3780faf35",

"progress": "3.语音识别",

"description": "小明是个调皮的小男孩，他觉得地板上有很多污垢，于是他拿起了牙刷用它来刷地板，虽然效果不佳，但是小明却觉得是一种有趣的尝试。",

"ctime": "2024-01-25 08:07:36"

},

{

"id": 3921,

"session": "27310934-4159-4bc9-8142-67a3780faf35",

"progress": "4.内容合规",

"description": "合规：小明是个调皮的小男孩，他觉得地板上有很多污垢，于是他拿起了牙刷用它来刷地板，虽然效果不佳，但是小明却觉得是一种有趣的尝试。",

"ctime": "2024-01-25 08:07:36"

},

{

"id": 3922,

"session": "27310934-4159-4bc9-8142-67a3780faf35",

"progress": "5.故事创作",

"description": "小明，一个满腔调皮的小男孩，发现家里仿佛被污秽覆盖。他拿起牙刷，一面一面地在地板上划过，结果却不尽人意。然而，这个看似毫无预期的尝试，他自己却获得了无比的乐趣。\n\n问题：小明的行为可能使他的生活环境变得更糟糕，你会如何帮助他改变这种状况？",

"ctime": "2024-01-25 08:07:36"

},

{

"id": 3923,

"session": "27310934-4159-4bc9-8142-67a3780faf35",

"progress": "6.语音合成",

"description": "http://oss.test.netkiller.cn/2024/01/25/27310934-4159-4bc9-8142-67a3780faf35.mp3",

"ctime": "2024-01-25 08:07:39"

}

],

"reason": "操作成功"

}

2.11. Enum 枚举数据类型

Enum 枚举数据类型 MySQL 特殊数据类型

实体中处理 enum 类型，存储字符串

@Enumerated(EnumType.STRING) 注解可以使其成功字符串类型。

public enum StatisticsType {

LIKE, COMMENT, BROWSE;

}

@Enumerated(EnumType.STRING)

private StatisticsType type;

SQL

CREATE TABLE `statistics_history` (

`id` bigint(20) NOT NULL AUTO_INCREMENT,

`member_id` bigint(20) NOT NULL,

`statistics_id` bigint(20) NOT NULL,

`type` varchar(255) DEFAULT NULL,

PRIMARY KEY (`id`)

) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8;

实体中处理 enum 类型，存储序号

@Enumerated(value = EnumType.ORDINAL) //ORDINAL序数

在实体中处理枚举类型适用于所有数据库，Spring data 将枚举视为 String 类型。

package cn.netkiller.api.domain;

import java.io.Serializable;

import javax.persistence.Column;

import javax.persistence.Entity;

import javax.persistence.GeneratedValue;

import javax.persistence.GenerationType;

import javax.persistence.Id;

import javax.persistence.Table;

@Entity

@Table(name = "statistics_history")

public class StatisticsHistory implements Serializable {

private static final long serialVersionUID = 1L;

@Id

@GeneratedValue(strategy = GenerationType.IDENTITY)

@Column(name = "id",unique = true, nullable = false, insertable = true, updatable = false)

private long id;

private long memberId;

private long statisticsId;

public enum StatisticsType {

LIKE, COMMENT, BROWSE;

}

private StatisticsType type;

public Long getId() {

return id;

}

public void setId(Long id) {

this.id = id;

}

public long getMemberId() {

return memberId;

}

public void setMemberId(long memberId) {

this.memberId = memberId;

}

public long getStatisticsId() {

return statisticsId;

}

public void setStatisticsId(long statisticsId) {

this.statisticsId = statisticsId;

}

public StatisticsType getType() {

return type;

}

public void setType(StatisticsType type) {

this.type = type;

}

}

默认 enum 类型创建数据库等效 int(11)

CREATE TABLE `statistics_history` (

`id` bigint(20) NOT NULL AUTO_INCREMENT,

`member_id` bigint(20) NOT NULL,

`statistics_id` bigint(20) NOT NULL,

`type` int(11) DEFAULT NULL,

PRIMARY KEY (`id`)

) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8;

SELECT * FROM test.statistics;

数据库枚举类型

在枚举中处理类型虽然可以适用于所有数据库，但有时我们希望适用数据库的枚举类型（例如MySQL），数据库中得枚举类型要比字符串效率更高

package cn.netkiller.api.domain.elasticsearch;

import java.util.Date;

import javax.persistence.Column;

import javax.persistence.Entity;

import javax.persistence.Id;

import javax.persistence.Table;

@Entity

@Table

public class NetkillerTrash {

@Id

private int id;

@Column(columnDefinition = "enum('Y','N') DEFAULT 'N'")

private boolean status;

public int getId() {

return id;

}

public void setId(int id) {

this.id = id;

}

public boolean isStatus() {

return status;

}

public void setStatus(boolean status) {

this.status = status;

}

}

实际对应的数据库DLL

CREATE TABLE `netkiller_trash` (

`id` int(11) NOT NULL,

`status` enum('Y','N') DEFAULT 'N',

PRIMARY KEY (`id`)

) ENGINE=InnoDB DEFAULT CHARSET=utf8

自定义枚举value属性

public enum Gender {

MALE("男士"),

FEMALE("女士");

private final String value;

private Gender(String value) {

this.value = value;

}

// value 转枚举

public static Gender fromValue(String value) {

for (Gender gender : values()) {

if (gender.toValue().equals(value)) {

return gender;

}

}

return null;

}

// 枚举转 value

public String toValue() {

return value;

}

}

创建 Gender 的自定义转换器

// 实现 AttributeConverter 接口

java复制代码public class GenderConverter implements AttributeConverter<Gender, String> {

@Override

public String convertToDatabaseColumn(Gender gender) {

return gender.toValue();

}

@Override

public Gender convertToEntityAttribute(String value) {

return Gender.fromValue(value);

}

}

在实体中，枚举字段加 @Convert 注解

@Convert(converter = GenderConverter.class)

@Column(name = "gender")

private Gender gender;

2.12. SET 数据结构

package common.domain;

import java.util.Date;

import java.util.Map;

import javax.persistence.CascadeType;

import javax.persistence.Column;

import javax.persistence.Convert;

import javax.persistence.Entity;

import javax.persistence.GeneratedValue;

import javax.persistence.GenerationType;

import javax.persistence.Id;

import javax.persistence.JoinColumn;

import javax.persistence.ManyToOne;

import org.springframework.format.annotation.DateTimeFormat;

import com.fasterxml.jackson.annotation.JsonFormat;

import common.type.OptionConverter;

@Entity

public class ItemPool {

@Id

@GeneratedValue(strategy = GenerationType.IDENTITY)

@Column(name = "id", unique = true, nullable = false, insertable = false, updatable = false)

public int id;

@ManyToOne(cascade = { CascadeType.PERSIST, CascadeType.REMOVE })

@JoinColumn(name = "site_id", referencedColumnName = "id")

private Site site;

public String question;

@Column(columnDefinition = "json DEFAULT NULL")

@Convert(converter = OptionConverter.class)

public Map<String, String> options;

@Column(columnDefinition = "SET('A','B','C','D','E','F','G') DEFAULT NULL COMMENT '答案'")

public String answer;

@ManyToOne(cascade = { CascadeType.PERSIST, CascadeType.REMOVE })

@JoinColumn(name = "category_id", referencedColumnName = "id")

private Category category;

@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")

@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")

@Column(columnDefinition = "TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间'")

public Date ctime;

@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")

@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")

@Column(columnDefinition = "TIMESTAMP NULL DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT '更改时间'")

public Date mtime;

}

定义 SET 如下，在JAVA中 SET被映射为逗号分隔的字符串（String），所以操作起来并无不同。使用字符串"A,B,C"存储即可，取出也同样是字符串。

@Column(columnDefinition = "SET('A','B','C','D','E','F','G') DEFAULT NULL COMMENT '答案'")

接入后查看

mysql> select answer from item_pool;

+--------+

| answer |

+--------+

| A,B,C  |

+--------+

1 row in set (0.00 sec)

完美实现

2.13. JSON 数据类型

MySQL 5.7 中增加了 json 数据类型，下面是一个例子：

CREATE TABLE `test` (

`id` int(10) unsigned NOT NULL AUTO_INCREMENT,

`your` json DEFAULT NULL,

PRIMARY KEY (`id`)

) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8

我们需要在 Java 实体中定义 json 数据库结构，我搜索遍了整个互联网（Google,Bing,Baidu......），没有找到解决方案，功夫不负有心人，反复尝试后终于成功。记住我是第一个这样用的 ：） 。

package common.domain;

import java.util.Date;

import java.util.Map;

import javax.persistence.CascadeType;

import javax.persistence.Column;

import javax.persistence.Convert;

import javax.persistence.Entity;

import javax.persistence.GeneratedValue;

import javax.persistence.GenerationType;

import javax.persistence.Id;

import javax.persistence.JoinColumn;

import javax.persistence.ManyToOne;

import org.springframework.format.annotation.DateTimeFormat;

import com.fasterxml.jackson.annotation.JsonFormat;

import common.type.OptionConverter;

@Entity

public class ItemPool {

@Id

@GeneratedValue(strategy = GenerationType.IDENTITY)

@Column(name = "id", unique = true, nullable = false, insertable = false, updatable = false)

public int id;

@ManyToOne(cascade = { CascadeType.PERSIST, CascadeType.REMOVE })

@JoinColumn(name = "site_id", referencedColumnName = "id")

private Site site;

public String name;

@Column(columnDefinition = "json DEFAULT NULL")

@Convert(converter = OptionConverter.class)

public Map<String, String> options;

@ManyToOne(cascade = { CascadeType.PERSIST, CascadeType.REMOVE })

@JoinColumn(name = "category_id", referencedColumnName = "id")

private Category category;

@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")

@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")

@Column(columnDefinition = "TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间'")

public Date ctime;

@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")

@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")

@Column(columnDefinition = "TIMESTAMP NULL DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT '更改时间'")

public Date mtime;

}

类型转换 Class

package common.type;

import java.util.Map;

import javax.persistence.AttributeConverter;

import com.google.gson.Gson;

import com.google.gson.reflect.TypeToken;

public class OptionConverter implements AttributeConverter<Map<String, String>, String> {

Gson json = new Gson();

@Override

public String convertToDatabaseColumn(Map<String, String> items) {

return json.toJson(items, new TypeToken<Map<String, String>>() {

}.getType());

}

@Override

public Map<String, String> convertToEntityAttribute(String str) {

return json.fromJson(str, new TypeToken<Map<String, String>>() {

}.getType());

}

}

通过 @Column(columnDefinition = "json DEFAULT NULL") 定义数据库为 JSON 数据类型

数据存储与取出通过 @Convert(converter = OptionConverter.class) 做转换

这里我需要使用 Map 数据结构 public Map<String, String> options;， 你可以根据你的实际需要定义数据类型 Class

启动 Spring 项目后创建 Schema 如下：

CREATE TABLE `item_pool` (

`id` int(11) NOT NULL AUTO_INCREMENT,

`ctime` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '????',

`mtime` timestamp NULL DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT '????',

`name` varchar(255) DEFAULT NULL,

`category_id` int(11) DEFAULT NULL,

`site_id` int(11) DEFAULT NULL,

PRIMARY KEY (`id`),

KEY `FKgwuxedi20fxclobkk2po053hj` (`category_id`),

KEY `FKiujumwssofow95st51ukklpgv` (`site_id`),

CONSTRAINT `FKgwuxedi20fxclobkk2po053hj` FOREIGN KEY (`category_id`) REFERENCES `category` (`id`),

CONSTRAINT `FKiujumwssofow95st51ukklpgv` FOREIGN KEY (`site_id`) REFERENCES `site` (`id`)

) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8

我们做个简单的测试, 创建仓库。

package common.repository;

import org.springframework.data.repository.CrudRepository;

import org.springframework.stereotype.Repository;

import common.domain.ItemPool;

@Repository

public interface ItemPoolRepository extends CrudRepository<ItemPool, Integer> {

}

package cn.netkiller.api.restful;

import java.util.LinkedHashMap;

import java.util.List;

import java.util.Map;

import org.slf4j.Logger;

import org.slf4j.LoggerFactory;

import org.springframework.beans.factory.annotation.Autowired;

import org.springframework.web.bind.annotation.GetMapping;

import org.springframework.web.bind.annotation.PathVariable;

import org.springframework.web.bind.annotation.PostMapping;

import org.springframework.web.bind.annotation.RequestBody;

import org.springframework.web.bind.annotation.RequestMapping;

import org.springframework.web.bind.annotation.RequestMethod;

import org.springframework.web.bind.annotation.RestController;

import common.domain.ItemPool;

import common.repository.ItemPoolRepository;

@RestController

public class TestRestController {

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

@Autowired

private ItemPoolRepository itemPoolRepository;

@GetMapping("/test/json/data/type")

public void jsonType() {

ItemPool itemPool = new ItemPool();

itemPool.name = "Which is Operstion System?";

Map<String, String> opt = new LinkedHashMap<String, String>();

opt.put("A", "Linux");

opt.put("B", "Java");

itemPool.options = opt;

itemPoolRepository.save(itemPool);

itemPool = null;

itemPool = itemPoolRepository.findOne(1);

System.out.println(itemPool.toString());

}

}

只能用完美来形容

mysql> select options from item_pool;

+-----------------------------+

| options                     |

+-----------------------------+

| {"A": "Linux", "B": "Java"} |

+-----------------------------+

1 row in set (0.00 sec)

2.14. 索引

普通索引

@Table(indexes = { @Index(name = "name", columnList = "name DESC"), @Index(name = "path", columnList = "path") })

package common.domain;

import java.util.Date;

import java.util.Set;

import javax.persistence.CascadeType;

import javax.persistence.Column;

import javax.persistence.Entity;

import javax.persistence.FetchType;

import javax.persistence.GeneratedValue;

import javax.persistence.GenerationType;

import javax.persistence.Id;

import javax.persistence.Index;

import javax.persistence.JoinColumn;

import javax.persistence.ManyToOne;

import javax.persistence.OneToMany;

import javax.persistence.Table;

import org.springframework.format.annotation.DateTimeFormat;

import com.fasterxml.jackson.annotation.JsonFormat;

import com.fasterxml.jackson.annotation.JsonIgnore;

@Entity

@Table(indexes = { @Index(name = "name", columnList = "name DESC"), @Index(name = "path", columnList = "path") })

public class Category {

@Id

@GeneratedValue(strategy = GenerationType.IDENTITY)

@Column(name = "id", unique = true, nullable = false, insertable = true, updatable = false)

public int id;

public String name;

public String description;

public String path;

@Column(columnDefinition = "enum('Enabled','Disabled') DEFAULT 'Enabled' COMMENT '状态'")

public String status;

@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")

@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")

@Column(columnDefinition = "TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间'")

public Date ctime;

@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")

@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")

@Column(columnDefinition = "TIMESTAMP NULL DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT '更改时间'")

public Date mtime;

@ManyToOne(cascade = { CascadeType.PERSIST, CascadeType.REMOVE })

@JoinColumn(name = "pid", referencedColumnName = "id")

private Category categorys;

@JsonIgnore

@OneToMany(cascade = CascadeType.ALL, mappedBy = "category", fetch = FetchType.EAGER)

private Set<Category> category;

public int getId() {

return id;

}

public void setId(int id) {

this.id = id;

}

public String getName() {

return name;

}

public void setName(String name) {

this.name = name;

}

public String getDescription() {

return description;

}

public void setDescription(String description) {

this.description = description;

}

public String getPath() {

return path;

}

public void setPath(String path) {

this.path = path;

}

public String getStatus() {

return status;

}

public void setStatus(String status) {

this.status = status;

}

public Date getCtime() {

return ctime;

}

public void setCtime(Date ctime) {

this.ctime = ctime;

}

public Date getMtime() {

return mtime;

}

public void setMtime(Date mtime) {

this.mtime = mtime;

}

public Category getCategorys() {

return categorys;

}

public void setCategorys(Category categorys) {

this.categorys = categorys;

}

public Set<Category> getCategory() {

return category;

}

public void setCategory(Set<Category> category) {

this.category = category;

}

@Override

public String toString() {

return "Category [id=" + id + ", name=" + name + ", description=" + description + ", path=" + path + ", status="

+ status + ", ctime=" + ctime + ", mtime=" + mtime + ", categorys=" + categorys + ", category="

+ category + "]";

}

}

唯一索引

针对字段做唯一索引

@Column(unique = true)

复合索引

创建由多个字段组成的复合索引

package cn.netkiller.api.model;

import java.io.Serializable;

import java.util.Date;

import javax.persistence.CascadeType;

import javax.persistence.Column;

import javax.persistence.Entity;

import javax.persistence.GeneratedValue;

import javax.persistence.GenerationType;

import javax.persistence.Id;

import javax.persistence.JoinColumn;

import javax.persistence.ManyToOne;

import javax.persistence.Table;

import javax.persistence.Temporal;

import javax.persistence.TemporalType;

import javax.persistence.UniqueConstraint;

import com.fasterxml.jackson.annotation.JsonFormat;

@Entity

@Table(name = "comment", uniqueConstraints = { @UniqueConstraint(columnNames = { "member_id", "articleId" }) })

public class Comment implements Serializable {

/**

*

*/

private static final long serialVersionUID = -1484408775034277681L;

@Id

@GeneratedValue(strategy = GenerationType.IDENTITY)

@Column(name = "id", unique = true, nullable = false, insertable = true, updatable = false)

private int id;

@ManyToOne(cascade = { CascadeType.ALL })

@JoinColumn(name = "member_id")

private Member member;

private int articleId;

private String message;

@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")

@Temporal(TemporalType.TIMESTAMP)

@Column(updatable = false)

@org.hibernate.annotations.CreationTimestamp

protected Date createDate;

public int getId() {

return id;

}

public void setId(int id) {

this.id = id;

}

public Member getMember() {

return member;

}

public void setMember(Member member) {

this.member = member;

}

public int getArticleId() {

return articleId;

}

public void setArticleId(int articleId) {

this.articleId = articleId;

}

public String getMessage() {

return message;

}

public void setMessage(String message) {

this.message = message;

}

public Date getCreateDate() {

return createDate;

}

public void setCreateDate(Date createDate) {

this.createDate = createDate;

}

}

CREATE TABLE `comment` (

`id` int(11) NOT NULL AUTO_INCREMENT,

`article_id` int(11) NOT NULL,

`create_date` datetime DEFAULT NULL,

`message` varchar(255) DEFAULT NULL,

`member_id` int(11) DEFAULT NULL,

PRIMARY KEY (`id`),

UNIQUE KEY `UK5qxfiu92nwlvgli7bl3evl11m` (`member_id`,`article_id`),

CONSTRAINT `FKmrrrpi513ssu63i2783jyiv9m` FOREIGN KEY (`member_id`) REFERENCES `member` (`id`)

) ENGINE=InnoDB DEFAULT CHARSET=utf8;

2.15. 嵌入

@Embeddable / @Embedded

package cn.netkiller.domain.demo;

import jakarta.persistence.Embeddable;

@Embeddable

public class Address {

private String city;

private String district;

private String street;

private String community;

}

package cn.netkiller.domain.demo;

import jakarta.persistence.*;

@Entity

public class Company {

@Id

@GeneratedValue(strategy = GenerationType.IDENTITY)

private long id;

private String name;

@Embedded

private Address address;

}

CREATE TABLE `company` (

`id` bigint NOT NULL AUTO_INCREMENT,

`city` varchar(255) DEFAULT NULL,

`community` varchar(255) DEFAULT NULL,

`district` varchar(255) DEFAULT NULL,

`street` varchar(255) DEFAULT NULL,

`name` varchar(255) DEFAULT NULL,

PRIMARY KEY (`id`)

) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci

@AttributeOverrides 定义字段名称

@Data

@Embeddable

public class Address {

private String province;

private String city;

private String street;

}

@Data

@Entity

@Table

public class Company {

@Id

@GeneratedValue

private Long id;

@Column

private String name;

// 公司地址

private Address address;

// 注册地址

@AttributeOverrides({

@AttributeOverride(name = "city", column = @Column(name= "location_city")),

@AttributeOverride(name = "province", column=@Column(name="location_province")),

@AttributeOverride(name = "street", column = @Column(name="location_street"))

})

private Address locationAddress;

}

创建复合主键

定义实体

package cn.netkiller.wallet.domain;

import java.io.Serializable;

import javax.persistence.Column;

import javax.persistence.Embeddable;

import javax.persistence.EmbeddedId;

import javax.persistence.Entity;

@Entity

public class UserToken {

@EmbeddedId

@Column(unique = true, nullable = false, insertable = true, updatable = false)

private UserTokenPrimaryKey primaryKey;

private String name;

private String symbol;

private int decimals;

public UserToken() {

// TODO Auto-generated constructor stub

}

public UserTokenPrimaryKey getPrimaryKey() {

return primaryKey;

}

public void setPrimaryKey(UserTokenPrimaryKey primaryKey) {

this.primaryKey = primaryKey;

}

public String getName() {

return name;

}

public void setName(String name) {

this.name = name;

}

public String getSymbol() {

return symbol;

}

public void setSymbol(String symbol) {

this.symbol = symbol;

}

public int getDecimals() {

return decimals;

}

public void setDecimals(int decimals) {

this.decimals = decimals;

}

@Override

public String toString() {

return "UserToken [primaryKey=" + primaryKey + ", name=" + name + ", symbol=" + symbol + ", decimals=" + decimals + "]";

}

@Embeddable

public static class UserTokenPrimaryKey implements Serializable {

private static final long serialVersionUID = 1242827922377178368L;

private String address;

private String contractAddress;

public UserTokenPrimaryKey() {

}

public UserTokenPrimaryKey(String address, String contractAddress) {

this.address = address;

this.contractAddress = contractAddress;

}

public String getAddress() {

return address;

}

public void setAddress(String address) {

this.address = address;

}

public String getContractAddress() {

return contractAddress;

}

public void setContractAddress(String contractAddress) {

this.contractAddress = contractAddress;

}

@Override

public String toString() {

return "UserTokenPrimaryKey [address=" + address + ", contractAddress=" + contractAddress + "]";

}

}

}

实际效果

CREATE TABLE "user_has_token" (

"address" varchar(255) NOT NULL,

"contract_address" varchar(255) NOT NULL,

"decimals" int(11) NOT NULL,

"name" varchar(255) DEFAULT NULL,

"symbol" varchar(255) DEFAULT NULL,

PRIMARY KEY ("address","contract_address")

) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4

package cn.netkiller.wallet.repository;

import java.util.List;

import org.springframework.data.jpa.repository.JpaRepository;

import org.springframework.data.jpa.repository.Query;

import org.springframework.data.repository.query.Param;

import cn.netkiller.wallet.domain.UserToken;

import cn.netkiller.wallet.domain.UserToken.UserTokenPrimaryKey;;

public interface UserTokenRepository extends JpaRepository<UserToken, UserTokenPrimaryKey> {

UserToken findOneByPrimaryKey(UserTokenPrimaryKey primaryKey);

@Query("select ut from UserToken ut where ut.primaryKey.address=:address")

List<UserToken> getByAddress(@Param("address") String address);

@Query("select ut from UserToken ut where ut.primaryKey.address=:address and ut.primaryKey.contractAddress=:contractAddress")

List<UserToken> findByPrimaryKey(@Param("address") String address, @Param("contractAddress") String contractAddress);

}

2.16. 外键

@JoinColumn

@JoinColumn与@Column注释类似，它的定义如下代码所示。

@Target({METHOD, FIELD}) @Retention(RUNTIME)

public @interface JoinColumn {

String name() default "";

String referencedColumnName() default "";

boolean unique() default false;

boolean nullable() default true;

boolean insertable() default true;

boolean updatable() default true;

String columnDefinition() default "";

String table() default "";

}

定义外键名称 @ForeignKey(name = "picture_id")

@Id

@OneToOne(cascade = CascadeType.ALL, orphanRemoval = true)

@JoinColumn(name = "id", foreignKey = @ForeignKey(name = "picture_id"))

private Picture picture;

CREATE TABLE `picture_psychoanalysis` (

`analysis` text COMMENT '心里分析',

`ctime` timestamp NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',

`emotion` varchar(255) DEFAULT NULL COMMENT '感谢|愉快|抱怨|愤怒|喜爱|厌恶|恐惧|悲伤',

`mtime` timestamp NULL DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT '修改时间',

`replies` text COMMENT '建议回复话术',

`sentiment` varchar(255) DEFAULT NULL COMMENT '负向情绪|中性情绪|正向情绪',

`id` bigint unsigned NOT NULL,

PRIMARY KEY (`id`),

CONSTRAINT `picture_id` FOREIGN KEY (`id`) REFERENCES `picture` (`id`)

) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='文生图心里分析'

@OneToOne

一对一表结构，如下面ER图所示，users表是用户表里面有登陆信息，profile 保存的时死人信息，这样的目的是我们尽量减少users表的字段，在频繁操作该表的时候性能比较好，另外一个目的是为了横向水平扩展。

+----------+             +------------+

| users    |             | profile    |

+----------+             +------------+

| id       | <---1:1---o | id         |

| name     |             | sex        |

| password |             | email      |

+----------+             +------------+

package cn.netkiller.api.domain.test;

import java.io.Serializable;

import javax.persistence.Entity;

import javax.persistence.GeneratedValue;

import javax.persistence.GenerationType;

import javax.persistence.Id;

import javax.persistence.Table;

@Entity

@Table(name = "users")

public class Users implements Serializable {

@Id

@GeneratedValue(strategy = GenerationType.AUTO)

private int id;

private String name;

private String password;

public int getId() {

return id;

}

public void setId(int id) {

this.id = id;

}

public String getName() {

return name;

}

public void setName(String name) {

this.name = name;

}

public String getPassword() {

return password;

}

public void setPassword(String password) {

this.password = password;

}

@Override

public String toString() {

return "Users [id=" + id + ", name=" + name + ", password=" + password + "]";

}

}

package cn.netkiller.api.domain.test;

import java.io.Serializable;

import javax.persistence.Entity;

import javax.persistence.Id;

import javax.persistence.JoinColumn;

import javax.persistence.OneToOne;

import javax.persistence.Table;

@Entity

@Table(name = "profile")

public class Profile implements Serializable {

/**

*

*/

private static final long serialVersionUID = -2500499458196257167L;

@Id

@OneToOne

@JoinColumn(name = "id")

private Users users;

private int age;

private String sex;

private String email;

public Users getUsers() {

return users;

}

public void setUsers(Users users) {

this.users = users;

}

public int getAge() {

return age;

}

public void setAge(int age) {

this.age = age;

}

public String getSex() {

return sex;

}

public void setSex(String sex) {

this.sex = sex;

}

public String getEmail() {

return email;

}

public void setEmail(String email) {

this.email = email;

}

@Override

public String toString() {

return "Profile [users=" + users + ", age=" + age + ", sex=" + sex + ", email=" + email + "]";

}

}

CREATE TABLE `users` (

`id` INT(11) NOT NULL AUTO_INCREMENT,

`name` VARCHAR(255) NULL DEFAULT NULL,

`password` VARCHAR(255) NULL DEFAULT NULL,

PRIMARY KEY (`id`)

)

COLLATE='utf8_general_ci'

ENGINE=InnoDB;

CREATE TABLE `profile` (

`age` INT(11) NOT NULL,

`email` VARCHAR(255) NULL DEFAULT NULL,

`sex` VARCHAR(255) NULL DEFAULT NULL,

`id` INT(11) NOT NULL,

PRIMARY KEY (`id`),

CONSTRAINT `FK6x079ilawxjrfsljwyyi5ujjq` FOREIGN KEY (`id`) REFERENCES `users` (`id`)

)

COLLATE='utf8_general_ci'

ENGINE=InnoDB;

如果第二张表关联的并非主表的PK（主键）需要使用 referencedColumnName 指定。

@JoinColumn(name = "member_id",referencedColumnName="member_id")

案例一

package cn.netkiller.domain.demo;

import jakarta.persistence.Entity;

import jakarta.persistence.GeneratedValue;

import jakarta.persistence.GenerationType;

import jakarta.persistence.Id;

@Entity

public class Book {

@Id

@GeneratedValue(strategy = GenerationType.IDENTITY)

private long id;

private String name;

}

package cn.netkiller.domain.demo;

import jakarta.persistence.*;

@Entity

public class BookDetail {

@Id

@GeneratedValue(strategy = GenerationType.IDENTITY)

private long id;

private long numberOfPages;

@OneToOne

private Book book;

}

CREATE TABLE `book` (

`id` bigint NOT NULL AUTO_INCREMENT,

`name` varchar(255) DEFAULT NULL,

PRIMARY KEY (`id`)

) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci

CREATE TABLE `book_detail` (

`id` bigint NOT NULL AUTO_INCREMENT,

`number_of_pages` bigint NOT NULL,

`book_id` bigint DEFAULT NULL,

PRIMARY KEY (`id`),

UNIQUE KEY `UK_29qtqq9pgixv8kqlt0woj1hyp` (`book_id`),

CONSTRAINT `FKl1hmgccsvfwcxhem3qw6l7gpm` FOREIGN KEY (`book_id`) REFERENCES `book` (`id`)

) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci

package cn.netkiller.domain.demo;

import jakarta.persistence.*;

@Entity

public class Book {

@Id

@GeneratedValue(strategy = GenerationType.IDENTITY)

private long id;

private String name;

@OneToOne(cascade = CascadeType.ALL)

@JoinColumn(name = "book_detail")

private BookDetail bookDetail;

}

package cn.netkiller.domain.demo;

import jakarta.persistence.*;

@Entity

public class BookDetail {

@Id

@GeneratedValue(strategy = GenerationType.IDENTITY)

private long id;

private long numberOfPages;

@OneToOne(cascade = CascadeType.ALL, mappedBy = "bookDetail")

private Book book;

}

CREATE TABLE `book` (

`id` bigint NOT NULL AUTO_INCREMENT,

`name` varchar(255) DEFAULT NULL,

`book_detail` bigint DEFAULT NULL,

PRIMARY KEY (`id`),

UNIQUE KEY `UK_d8im4vqhlm2eo0mj9lwjvib94` (`book_detail`),

CONSTRAINT `FKagqqxsh6783b9dd9197ow49a5` FOREIGN KEY (`book_detail`) REFERENCES `book_detail` (`id`)

) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci

CREATE TABLE `book_detail` (

`id` bigint NOT NULL AUTO_INCREMENT,

`number_of_pages` bigint NOT NULL,

PRIMARY KEY (`id`)

) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci

package cn.netkiller.domain.demo;

import jakarta.persistence.Entity;

import jakarta.persistence.GeneratedValue;

import jakarta.persistence.GenerationType;

import jakarta.persistence.Id;

@Entity

public class Users {

@Id

@GeneratedValue(strategy = GenerationType.IDENTITY)

private long id;

private String name;

}

package cn.netkiller.domain.demo;

import jakarta.persistence.Entity;

import jakarta.persistence.Id;

import jakarta.persistence.OneToOne;

@Entity

public class Profile {

@Id

@OneToOne

private Users users;

private int age;

}

CREATE TABLE `users` (

`id` bigint NOT NULL AUTO_INCREMENT,

`name` varchar(255) DEFAULT NULL,

PRIMARY KEY (`id`)

) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci

CREATE TABLE `profile` (

`age` int NOT NULL,

`users_id` bigint NOT NULL,

PRIMARY KEY (`users_id`),

CONSTRAINT `FKi6d1noonlpe6oyk6pnwc1q49e` FOREIGN KEY (`users_id`) REFERENCES `users` (`id`)

) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci

指定一对一字段名，默认是表名+PK，例如上面的例子 users_id，如果我们希望自定义字段名，可以使用 @JoinColumn(name = "id")

package cn.netkiller.domain.demo;

import jakarta.persistence.Entity;

import jakarta.persistence.Id;

import jakarta.persistence.JoinColumn;

import jakarta.persistence.OneToOne;

import lombok.Data;

@Entity

@Data

public class Profile {

@Id

@OneToOne

@JoinColumn(name = "id")

private Users users;

private int age;

private boolean sex;

}

效果展示

CREATE TABLE `profile` (

`age` int NOT NULL,

`sex` bit(1) NOT NULL,

`id` bigint NOT NULL,

PRIMARY KEY (`id`),

CONSTRAINT `FK6x079ilawxjrfsljwyyi5ujjq` FOREIGN KEY (`id`) REFERENCES `users` (`id`)

) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci

does not define an IdClass

如果一对一的关系中，我们希望两端都是用 id 字段，而@OneToOne 一端是对象，必须定义一个 @Id，这时加入 @MapsId 可以解决 does not define an IdClass 错误，这时一段表中没有 @Id

package cn.netkiller.domain.demo;

import jakarta.persistence.*;

import lombok.Data;

import org.hibernate.annotations.Comment;

import org.hibernate.annotations.DynamicInsert;

import org.hibernate.annotations.DynamicUpdate;

@Entity

@Table

@Data

@DynamicInsert

@DynamicUpdate

@Comment("用户表")

public class Users {

@Id

@GeneratedValue(strategy = GenerationType.IDENTITY)

private long id;

private String username;

private String password;

private boolean status;

}

package cn.netkiller.domain.demo;

import jakarta.persistence.Table;

import jakarta.persistence.*;

import lombok.Data;

import org.hibernate.annotations.*;

@Entity

@Table

@Data

@DynamicInsert

@DynamicUpdate

@Comment("用户信息表")

public class Profile {

@Id

@Column(name = "id")

private Long id;

@MapsId

@OneToOne

@JoinColumn(name = "id")

@OnDelete(action = OnDeleteAction.CASCADE)

private Users users;

private int age;

private boolean sex;

}

package cn.netkiller.repository.demo;

import cn.netkiller.domain.demo.Profile;

import org.springframework.data.jpa.repository.JpaRepository;

import org.springframework.stereotype.Repository;

@Repository

public interface TestRepository extends JpaRepository<Profile, Long> {

}

CREATE TABLE `users` (

`id` bigint NOT NULL AUTO_INCREMENT,

`password` varchar(255) DEFAULT NULL,

`status` bit(1) NOT NULL,

`username` varchar(255) DEFAULT NULL,

PRIMARY KEY (`id`)

) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='用户表'

CREATE TABLE `profile` (

`id` bigint NOT NULL,

`age` int NOT NULL,

`sex` bit(1) NOT NULL,

PRIMARY KEY (`id`),

CONSTRAINT `FK6x079ilawxjrfsljwyyi5ujjq` FOREIGN KEY (`id`) REFERENCES `users` (`id`) ON DELETE CASCADE

) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='用户信息表'

共享主键

package cn.netkiller.domain.demo;

import jakarta.persistence.*;

import lombok.Data;

import org.hibernate.annotations.Comment;

import org.hibernate.annotations.DynamicInsert;

import org.hibernate.annotations.DynamicUpdate;

@Entity

@Table

@Data

@DynamicInsert

@DynamicUpdate

@Comment("用户表")

public class Users {

@Id

@GeneratedValue(strategy = GenerationType.IDENTITY)

private long id;

private String username;

private String password;

private boolean status;

@OneToOne(mappedBy = "users", cascade = CascadeType.ALL)

@PrimaryKeyJoinColumn

private Profile profile;

}

package cn.netkiller.domain.demo;

import jakarta.persistence.Table;

import jakarta.persistence.*;

import lombok.Data;

import org.hibernate.annotations.*;

@Entity

@Table

@Data

@DynamicInsert

@DynamicUpdate

@Comment("用户信息表")

public class Profile {

@Id

@Column(name = "id")

private Long id;

@MapsId

@OneToOne

@JoinColumn(name = "id")

@OnDelete(action = OnDeleteAction.CASCADE)

private Users users;

private int age;

private boolean sex;

}

CREATE TABLE `users` (

`id` bigint NOT NULL AUTO_INCREMENT,

`password` varchar(255) DEFAULT NULL,

`status` bit(1) NOT NULL,

`username` varchar(255) DEFAULT NULL,

PRIMARY KEY (`id`)

) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='用户表'

CREATE TABLE `profile` (

`id` bigint NOT NULL,

`age` int NOT NULL,

`sex` bit(1) NOT NULL,

PRIMARY KEY (`id`),

CONSTRAINT `FK6x079ilawxjrfsljwyyi5ujjq` FOREIGN KEY (`id`) REFERENCES `users` (`id`) ON DELETE CASCADE

) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='用户信息表'

null identifier

@OneToOne 保存提示 null identifier，经过排查需要配置

package cn.netkiller.domain;

import jakarta.persistence.CascadeType;

import jakarta.persistence.ForeignKey;

import jakarta.persistence.Table;

import jakarta.persistence.*;

import lombok.Data;

import org.hibernate.annotations.*;

import java.io.Serial;

import java.io.Serializable;

import java.util.Date;

@Entity

@Table

@DynamicUpdate

@DynamicInsert

@Data

@Comment("文生图心里分析")

public class PicturePsychoanalysis implements Serializable {

@Serial

public static final long serialVersionUID = 1L;

@Comment("Robert Plutchik 情感轮盘")

@Column(columnDefinition = "json")

public String plutchik;

@Id

@Column(name = "id")

@Comment("Picture Id 一对一关系")

private Long id;

@MapsId

@OneToOne(cascade = CascadeType.MERGE)

@JoinColumn(name = "id", insertable = true, updatable = false, columnDefinition = "bigint unsigned", foreignKey = @ForeignKey(name = "picture_id"))

@OnDelete(action = OnDeleteAction.CASCADE)

private Picture picture;

@Comment("负向情绪|中性情绪|正向情绪")

private String sentiment;

@Comment("感谢|愉快|抱怨|愤怒|喜爱|厌恶|恐惧|悲伤")

private String emotion;

@Lob

@Basic(fetch = FetchType.LAZY)

@Column(nullable = true, columnDefinition = "text")

@Comment("建议回复话术")

private String replies;

@Lob

@Basic(fetch = FetchType.LAZY)

@Column(nullable = true, columnDefinition = "text")

@Comment("心里分析")

private String analysis;

//    public JSONObject plutchik;

@Column(insertable = false, updatable = false, columnDefinition = "TIMESTAMP DEFAULT CURRENT_TIMESTAMP")

@Comment("创建时间")

private Date ctime;

@Column(nullable = true, insertable = false, updatable = false, columnDefinition = "TIMESTAMP NULL DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP")

@Comment("修改时间")

private Date mtime;

}

讲 @OneToOne 增加 @OneToOne(cascade = CascadeType.MERGE) 参数，如果 CascadeType.ALL 需要改为 CascadeType.MERGE

@Id

@Column(name = "id")

@Comment("Picture Id 一对一关系")

private Long id;

@MapsId

@OneToOne(cascade = CascadeType.MERGE)

@JoinColumn(name = "id", insertable = true, updatable = false, columnDefinition = "bigint unsigned", foreignKey = @ForeignKey(name = "picture_id"))

@OnDelete(action = OnDeleteAction.CASCADE)

private Picture picture;

OneToMany 一对多

我们要实现一个一对多实体关系，ER 图如下

+----------+          +------------+

| Classes  |          | Student    |

+----------+          +------------+

| id       | <---+    | id         |

| name     |     |    | name       |

+----------+     +--o | classes_id |

+------------+

classes 表需要 OneToMany 注解，Student 表需要 ManyToOne 注解，这样就建立起了表与表之间的关系

package cn.netkiller.api.domain.test;

import java.io.Serializable;

import java.util.Set;

import javax.persistence.CascadeType;

import javax.persistence.Entity;

import javax.persistence.GeneratedValue;

import javax.persistence.GenerationType;

import javax.persistence.Id;

import javax.persistence.OneToMany;

import javax.persistence.Table;

@Entity

@Table(name="classes")

public class Classes implements Serializable{

/**

*

*/

private static final long serialVersionUID = -5422905745519948312L;

@Id

@GeneratedValue(strategy=GenerationType.AUTO)

private int id;

private String name;

@OneToMany(cascade=CascadeType.ALL,mappedBy="classes")

private Set<Student> students;

public int getId() {

return id;

}

public void setId(int id) {

this.id = id;

}

public String getName() {

return name;

}

public void setName(String name) {

this.name = name;

}

public Set<Student> getStudents() {

return students;

}

public void setStudents(Set<Student> students) {

this.students = students;

}

@Override

public String toString() {

return "classes [id=" + id + ", name=" + name + ", students=" + students + "]";

}

}

package cn.netkiller.api.domain.test;

import java.io.Serializable;

import javax.persistence.CascadeType;

import javax.persistence.Entity;

import javax.persistence.GeneratedValue;

import javax.persistence.GenerationType;

import javax.persistence.Id;

import javax.persistence.JoinColumn;

import javax.persistence.ManyToOne;

import javax.persistence.Table;

@Entity

@Table(name = "student")

public class Student implements Serializable{

/**

*

*/

private static final long serialVersionUID = 6737037465677800326L;

@Id

@GeneratedValue(strategy = GenerationType.AUTO)

private int id;

private String name;

// 若有多个cascade，可以是：{CascadeType.PERSIST,CascadeType.MERGE}

@ManyToOne(cascade = { CascadeType.ALL })

@JoinColumn(name = "classes_id")

private Classes classes;

public int getId() {

return id;

}

public void setId(int id) {

this.id = id;

}

public String getName() {

return name;

}

public void setName(String name) {

this.name = name;

}

public Classes getClasses() {

return classes;

}

public void setClasses(Classes classes) {

this.classes = classes;

}

@Override

public String toString() {

return "Student [id=" + id + ", name=" + name + ", classes=" + classes + "]";

}

}

最终 SQL 表如下

CREATE TABLE `classes` (

`id` INT(11) NOT NULL AUTO_INCREMENT,

`name` VARCHAR(255) NULL DEFAULT NULL,

PRIMARY KEY (`id`)

)

COLLATE='utf8_general_ci'

ENGINE=InnoDB;

CREATE TABLE `student` (

`id` INT(11) NOT NULL AUTO_INCREMENT,

`name` VARCHAR(255) NULL DEFAULT NULL,

`class_id` INT(11) NULL DEFAULT NULL,

PRIMARY KEY (`id`),

INDEX `FKnsl7w2nw6o6eq53hqlxfcijpm` (`class_id`),

CONSTRAINT `FKnsl7w2nw6o6eq53hqlxfcijpm` FOREIGN KEY (`class_id`) REFERENCES `classes` (`id`)

)

COLLATE='utf8_general_ci'

ENGINE=InnoDB;

Classes classes=new Classes();

classes.setName("One");

Student st1=new Student();

st1.setSname("jason");

st1.setClasses(classes);

studentRepostitory.save(st1);

Student st2=new Student();

st2.setSname("neo");

st2.setClasses(classes);

studentRepostitory.save(st2);

ManyToMany 多对多

用户与角色就是一个多对多的关系，多对多是需要中间表做关联的。所以我方需要一个 user_has_role 表。

+----------+          +---------------+            +--------+

| users    |          | user_has_role |            | role   |

+----------+          +---------------+            +--------+

| id       | <------o | user_id       |      /---> | id     |

| name     |          | role_id       | o---+      | name   |

| password |          |               |            |        |

+----------+          +---------------+            +--------+

创建 User 表

package cn.netkiller.api.domain.test;

import java.io.Serializable;

import java.util.Set;

import javax.persistence.Entity;

import javax.persistence.FetchType;

import javax.persistence.GeneratedValue;

import javax.persistence.GenerationType;

import javax.persistence.Id;

import javax.persistence.JoinTable;

import javax.persistence.ManyToMany;

import javax.persistence.Table;

import javax.persistence.JoinColumn;

@Entity

@Table(name = "users")

public class Users implements Serializable {

/**

*

*/

private static final long serialVersionUID = -2480194112597046349L;

@Id

@GeneratedValue(strategy = GenerationType.AUTO)

private int id;

private String name;

private String password;

@ManyToMany(fetch = FetchType.EAGER)

@JoinTable(name = "user_has_role", joinColumns = { @JoinColumn(name = "user_id", referencedColumnName = "id") }, inverseJoinColumns = { @JoinColumn(name = "role_id", referencedColumnName = "id") })

private Set<Roles> roles;

public int getId() {

return id;

}

public void setId(int id) {

this.id = id;

}

public String getName() {

return name;

}

public void setName(String name) {

this.name = name;

}

public String getPassword() {

return password;

}

public void setPassword(String password) {

this.password = password;

}

public Set<Roles> getRoles() {

return roles;

}

public void setRoles(Set<Roles> roles) {

this.roles = roles;

}

@Override

public String toString() {

return "Users [id=" + id + ", name=" + name + ", password=" + password + ", roles=" + roles + "]";

}

}

创建 Role 表

package cn.netkiller.api.domain.test;

import java.io.Serializable;

import java.util.Set;

import javax.persistence.Entity;

import javax.persistence.GeneratedValue;

import javax.persistence.GenerationType;

import javax.persistence.Id;

import javax.persistence.ManyToMany;

import javax.persistence.Table;

@Entity

@Table(name = "roles")

public class Roles implements Serializable {

private static final long serialVersionUID = 6737037465677800326L;

@Id

@GeneratedValue(strategy = GenerationType.AUTO)

private int id;

private String name;

@ManyToMany(mappedBy = "roles")

private Set<Users> users;

public int getId() {

return id;

}

public void setId(int id) {

this.id = id;

}

public String getName() {

return name;

}

public void setName(String name) {

this.name = name;

}

public Set<Users> getUsers() {

return users;

}

public void setUsers(Set<Users> users) {

this.users = users;

}

@Override

public String toString() {

return "Roles [id=" + id + ", name=" + name + ", users=" + users + "]";

}

}

最终产生数据库表如下

CREATE TABLE `users` (

`id` INT(11) NOT NULL AUTO_INCREMENT,

`name` VARCHAR(255) NULL DEFAULT NULL,

`password` VARCHAR(255) NULL DEFAULT NULL,

PRIMARY KEY (`id`)

)

COLLATE='utf8_general_ci'

ENGINE=InnoDB;

CREATE TABLE `roles` (

`id` INT(11) NOT NULL AUTO_INCREMENT,

`name` VARCHAR(255) NULL DEFAULT NULL,

PRIMARY KEY (`id`)

)

COLLATE='utf8_general_ci'

ENGINE=InnoDB;

CREATE TABLE `user_has_role` (

`user_id` INT(11) NOT NULL,

`role_id` INT(11) NOT NULL,

PRIMARY KEY (`user_id`, `role_id`),

INDEX `FKsvvq61v3koh04fycopbjx72hj` (`role_id`),

CONSTRAINT `FK2dl1ftxlkldulcp934i3125qo` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`),

CONSTRAINT `FKsvvq61v3koh04fycopbjx72hj` FOREIGN KEY (`role_id`) REFERENCES `roles` (`id`)

)

COLLATE='utf8_general_ci'

ENGINE=InnoDB;

外键级联操作

cascade 属性： 指定级联操作的行为(可多选)

CascadeType.PERSIST：级联新增（又称级联保存）：对A对象保存时也会对B对象进行保存。并且，只有A类新增时，会级联B对象新增。若B对象在数据库存在则抛异常。对应EntityManager的presist方法。

CascadeType.MERGE：级联合并（级联更新）：指A类新增或者变化，会级联B对象（新增或者变化）。对应EntityManager的merge方法。

CascadeType.REMOVE：级联删除：只有A类删除时，会级联删除B类,即在设置的那一端进行删除时，另一端才会级联删除。对应EntityManager的remove方法。

CascadeType.REFRESH：级联刷新：获取A对象时也重新获取最新的B对象。对EntityManager的refresh(object)方法。即会重新查询数据库里的最新数据（用的比较少）

CascadeType.DETACH：级联分离。

CascadeType.ALL：级联所有操作。

CascadeType.PERSIST

@OneToMany(mappedBy = "boss", cascade = CascadeType.PERSIST)

private List<Staff> staffList;

CascadeType.REMOVE

@OneToMany(mappedBy = "boss", cascade = CascadeType.REMOVE)

private List<Staff> staffList;

外键级联删除

orphanRemoval 是 JPA 定义，并不是数据库原生

@Id

@OneToOne(cascade = CascadeType.ALL, orphanRemoval = true)

@JoinColumn(name = "id", foreignKey = @ForeignKey(name = "picture_id"))

private Picture picture;

orphanRemoval = true 可以实现数据级联删除

package cn.netkiller.api.domain;

import java.io.Serializable;

import java.util.Set;

import javax.persistence.CascadeType;

import javax.persistence.Column;

import javax.persistence.Entity;

import javax.persistence.Id;

import javax.persistence.OneToMany;

import javax.persistence.GeneratedValue;

import javax.persistence.GenerationType;

import javax.persistence.Table;

import com.fasterxml.jackson.annotation.JsonIgnore;

@Entity

@Table(name = "member")

public class Member implements Serializable {

/**

*

*/

private static final long serialVersionUID = 1L;

@Id

@GeneratedValue(strategy = GenerationType.IDENTITY)

@Column(name = "id", unique = true, nullable = false, insertable = true, updatable = false)

private int id;

private String name;

private String sex;

private int age;

private String wechat;

@Column(unique = true)

private String mobile;

private String picture;

private String ipAddress;

@JsonIgnore

@OneToMany(cascade = CascadeType.ALL, orphanRemoval = true, mappedBy = "member")

private Set<Comment> comment;

@JsonIgnore

@OneToMany(cascade = CascadeType.ALL, orphanRemoval = true, mappedBy = "member")

private Set<StatisticsHistory> statisticsHistory;

public Member() {

}

public Member(int id) {

this.id = id;

}

public int getId() {

return id;

}

public void setId(int id) {

this.id = id;

}

public String getName() {

return name;

}

public void setName(String name) {

this.name = name;

}

public String getSex() {

return sex;

}

public void setSex(String sex) {

this.sex = sex;

}

public int getAge() {

return age;

}

public void setAge(int age) {

this.age = age;

}

public String getWechat() {

return wechat;

}

public void setWechat(String wechat) {

this.wechat = wechat;

}

public String getMobile() {

return mobile;

}

public void setMobile(String mobile) {

this.mobile = mobile;

}

public String getPicture() {

return picture;

}

public void setPicture(String picture) {

this.picture = picture;

}

public String getIpAddress() {

return ipAddress;

}

public void setIpAddress(String ipAddress) {

this.ipAddress = ipAddress;

}

@Override

public String toString() {

return "Member [id=" + id + ", name=" + name + ", sex=" + sex + ", age=" + age + ", wechat=" + wechat + ", mobile=" + mobile + ", picture=" + picture + ", ipAddress=" + ipAddress + "]";

}

}

MySQL ON DELETE CASCADE

@OnDelete(action = OnDeleteAction.CASCADE)

package cn.netkiller.domain;

import jakarta.persistence.*;

import lombok.Data;

import org.hibernate.annotations.Comment;

import org.hibernate.annotations.DynamicUpdate;

import org.hibernate.annotations.OnDelete;

import org.hibernate.annotations.OnDeleteAction;

import java.io.Serial;

import java.io.Serializable;

import java.util.Date;

@Entity

@Table

@DynamicUpdate

@Data

@Comment("文生图心里分析")

public class PicturePsychoanalysis implements Serializable {

@Serial

public static final long serialVersionUID = 1L;

@Id

@OneToOne()

@JoinColumn(name = "id", foreignKey = @ForeignKey(name = "picture_id"))

@OnDelete(action = OnDeleteAction.CASCADE)

private Picture picture;

//    @Column(unique = true, nullable = false, insertable = true, updatable = false)

//    @Comment("会话")

//    private String session;

@Comment("负向情绪|中性情绪|正向情绪")

private String sentiment;

@Comment("感谢|愉快|抱怨|愤怒|喜爱|厌恶|恐惧|悲伤")

private String emotion;

@Lob

@Basic(fetch = FetchType.LAZY)

@Column(nullable = true, columnDefinition = "text")

@Comment("建议回复话术")

private String replies;

@Lob

@Basic(fetch = FetchType.LAZY)

@Column(nullable = true, columnDefinition = "text")

@Comment("心里分析")

private String analysis;

@Column(insertable = false, updatable = false, columnDefinition = "TIMESTAMP DEFAULT CURRENT_TIMESTAMP")

@Comment("创建时间")

private Date ctime;

@Column(nullable = true, insertable = false, updatable = false, columnDefinition = "TIMESTAMP NULL DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP")

@Comment("修改时间")

private Date mtime;

}

CREATE TABLE `picture_psychoanalysis` (

`analysis` text COMMENT '心里分析',

`ctime` timestamp NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',

`emotion` varchar(255) DEFAULT NULL COMMENT '感谢|愉快|抱怨|愤怒|喜爱|厌恶|恐惧|悲伤',

`mtime` timestamp NULL DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT '修改时间',

`replies` text COMMENT '建议回复话术',

`sentiment` varchar(255) DEFAULT NULL COMMENT '负向情绪|中性情绪|正向情绪',

`id` bigint unsigned NOT NULL,

PRIMARY KEY (`id`),

CONSTRAINT `picture_id` FOREIGN KEY (`id`) REFERENCES `picture` (`id`) ON DELETE CASCADE

) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='文生图心里分析'

@JoinTable

@JoinTable(name = "table")

@OneToMany(cascade = CascadeType.ALL)

@JoinTable

private List<UserProfile> userProfile;

@JoinTable(name = "cust_user",

joinColumns = @JoinColumn(name = "user_id", referencedColumnName = "id")

)

@JoinTable(name = "cust_user",

inverseJoinColumns = @JoinColumn(name = "user_ext_id", referencedColumnName = "id")

)

@JoinTable(name = "cust_user",

joinColumns = @JoinColumn(name = "user_id", referencedColumnName = "id"),

inverseJoinColumns = @JoinColumn(name = "user_ext_id", referencedColumnName = "id"),

uniqueConstraints = {

@UniqueConstraint(name = "unique_user_id", columnNames = {"user_id"}),

@UniqueConstraint(name = "unique_user_ext_id", columnNames = {"user_ext_id"})

}

)

多对多实例

package cn.netkiller.domain;

import jakarta.persistence.*;

import lombok.Data;

import java.io.Serializable;

@Entity

@Table

@Data

public class Consumer implements Serializable {

public static final long serialVersionUID = 7998903421265538801L;

public String firstName;

public String lastName;

@Id

@GeneratedValue(strategy = GenerationType.IDENTITY)

@Column(name = "id", unique = true, nullable = false, insertable = false, updatable = false, columnDefinition = "int unsigned")

public Integer id;

@OneToOne(cascade = CascadeType.ALL, orphanRemoval = true)

@JoinTable(name = "consumer_has_device",

joinColumns =

{@JoinColumn(name = "consumer_id", referencedColumnName = "id")},

inverseJoinColumns =

{@JoinColumn(name = "device_id", referencedColumnName = "id")})

private Device device;

public Consumer() {

}

}

package cn.netkiller.domain;

import jakarta.persistence.*;

import lombok.Data;

import org.hibernate.annotations.Comment;

import org.hibernate.annotations.DynamicUpdate;

import java.io.Serial;

import java.io.Serializable;

import java.util.Date;

@Entity

@Table

@DynamicUpdate

@Data

@Comment("设备表")

public class Device implements Serializable {

@Serial

public static final long serialVersionUID = 1L;

@Id

@GeneratedValue(strategy = GenerationType.IDENTITY)

@Column(name = "id", unique = true, nullable = false, insertable = false, updatable = false, columnDefinition = "int unsigned")

@Comment("主键")

private Integer id;

@Comment("设备名称")

private String name;

@Comment("型号")

private String model;

@Comment("版本")

private String fireware;

@Comment("版本")

private String version;

@Column(unique = true, nullable = false, insertable = true, updatable = false)

@Comment("序列号")

private String sn;

@Comment("ip")

private String ip;

@Comment("mac")

private String mac;

@Temporal(TemporalType.TIMESTAMP)

@Comment("最后一次登陆时间")

private Date lastTime;

@Column(columnDefinition = "enum('Y','N') DEFAULT 'N'")

@Comment("设备状态")

private boolean status;

@Column(insertable = false, updatable = false, columnDefinition = "TIMESTAMP DEFAULT CURRENT_TIMESTAMP")

@Comment("创建时间")

private Date ctime;

@Column(nullable = true, insertable = false, updatable = false, columnDefinition = "TIMESTAMP NULL DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP")

@Comment("修改时间")

private Date mtime;

}

CREATE TABLE `consumer` (

`id` int unsigned NOT NULL AUTO_INCREMENT,

`first_name` varchar(255) DEFAULT NULL,

`last_name` varchar(255) DEFAULT NULL,

PRIMARY KEY (`id`)

) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci

CREATE TABLE `device` (

`id` int unsigned NOT NULL AUTO_INCREMENT COMMENT '主键',

`ctime` timestamp NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',

`fireware` varchar(255) DEFAULT NULL COMMENT '版本',

`ip` varchar(255) DEFAULT NULL COMMENT 'ip',

`last_time` datetime(6) DEFAULT NULL COMMENT '最后一次登陆时间',

`mac` varchar(255) DEFAULT NULL COMMENT 'mac',

`model` varchar(255) DEFAULT NULL COMMENT '型号',

`mtime` timestamp NULL DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT '修改时间',

`name` varchar(255) DEFAULT NULL COMMENT '设备名称',

`sn` varchar(255) NOT NULL COMMENT '序列号',

`status` enum('Y','N') DEFAULT 'N' COMMENT '设备状态',

`version` varchar(255) DEFAULT NULL COMMENT '版本',

PRIMARY KEY (`id`),

UNIQUE KEY `UK_bg7pgyvfwv0q65tmquumxff3d` (`sn`)

) ENGINE=InnoDB AUTO_INCREMENT=12 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='设备表'

CREATE TABLE `consumer_has_device` (

`device_id` int unsigned DEFAULT NULL,

`consumer_id` int unsigned NOT NULL,

PRIMARY KEY (`consumer_id`),

UNIQUE KEY `UK_ibck20j1s6ch97lncg99uvpgh` (`device_id`),

CONSTRAINT `FKcottusf6sx3bnouahp29vjdwk` FOREIGN KEY (`device_id`) REFERENCES `device` (`id`),

CONSTRAINT `FKq7u4eyw8pmfkwg4yymjljx8ra` FOREIGN KEY (`consumer_id`) REFERENCES `consumer` (`id`)

) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci

@OrderBy

// JPA 默认根据 Student 的 ID 主键对 studentList 集合数据进行递增排序

@OneToMany(cascade = CascadeType.ALL)

@OrderBy

private List<Student> studentList;

// 手动指定 id 字段的排序方式，ASC 递增排序，DESC 递减排序

@OneToMany(cascade = CascadeType.ALL)

@OrderBy("id desc")

private List<Student> studentList;

// 手动指定按照 salary 属性进行递减排序

@OneToMany(cascade = CascadeType.ALL)

@OrderBy("salary desc")

private List<Student> studentList;

// 手动指定按照多个属性进行排序

// 下面将根据 sex 和 salay 进行递增排序

@OneToMany(cascade = CascadeType.ALL)

@OrderBy("sex,salary")

private List<Student> studentList;

// 下面将根据 sex 递增排序，salary 递增排序

@OneToMany(cascade = CascadeType.ALL)

@OrderBy("sex asc,salary asc")

private List<Student> studentList;

// 下面将根据 sex 递增排序，salary 递减排序

@OneToMany(cascade = CascadeType.ALL)

@OrderBy("sex asc,salary desc")

private List<Student> studentList;

2.17. 映射集合属性

@ElementCollection 很像 @OneToMany，甚至有些场景可以相互替代，@ElementCollection 更适合 List/Set/Map/Array 等数据类型，而 @OneToMany 是对应一个实体。

import javax.persistence.*;

import java.util.*;

import javax.persistence.*;

@Entity

public class Employee {

@Id

@GeneratedValue(strategy = GenerationType.AUTO)

private int id;

private String name;

@ElementCollection

private Set<Address> address = new HashSet<Address>();

}

import javax.persistence.*;

@Embeddable

public class Address {

private String province;

private String city;

private String state;

}

Set 集合

@ElementCollection

private final Set<String> address = new HashSet<String>();

List 集合

@ElementCollection(targetClass = String.class) //指定集合中元素的类型

@CollectionTable(name = "school_inf", joinColumns = @JoinColumn(name="pid",nullable = false)) //表示外键不能为空

@Column(name = "school_name") //指定表中保存集合元素的列名

@OrderColumn(name = "list_order") //索引列

private List<String> schools = new ArrayList<String>();

数组集合

@Entity

@Table

public class Student {

@Id @Column

@GeneratedValue(strategy = GenerationType.IDENTITY)

private Integer sid;

private String name;

private Integer age;

@ElementCollection(targetClass=String.class) //集合中元素的类型

@CollectionTable(name = "school", joinColumns = @JoinColumn(name="sid",nullable = false))//指定外键的名称为sid,并且不能为空

@Column(name = "school_name") //指定schools属性，在表中的列名

@OrderColumn(name = "array_order")

private String[] schools = new String[3];

}

Map 集合

@Entity

@Table(name = "student")

public class Student {

@Id @Column

@GeneratedValue(strategy = GenerationType.IDENTITY)

private Integer sid;

private String name;

private Integer age;

@ElementCollection(targetClass = Float.class) //对于Map类型的属性：指定的是Value的类型

@CollectionTable(name = "score_info", joinColumns = @JoinColumn(name="sid", nullable = false))

@MapKeyClass(String.class) // 指定Map中key的类型

@MapKeyColumn(name="suject") //指定索引列，也就是key的列名

@Column(name = "score") //映射保存Map, Value的列名

private Map<String, Float> scores = new HashMap<String, Float>(); //科目和成绩

}

外键名称

@ElementCollection

@CollectionTable(joinColumns = @JoinColumn(name = "pid", nullable = false))

private Set<Status> address = new HashSet<Status>();

2.18. @JsonIgnore

当尸体返回 Json 数据结构是，将不包含 @JsonIgnore 定义变量。

@JsonIgnore

@OneToMany(mappedBy = "owner")

private List<Pet> pets;

2.19. @EnableJpaAuditing 开启 JPA 审计功能

@SpringBootApplication

@EnableJpaAuditing

public class Application {

public static void main(String[] args) throws Exception {

SpringApplication.run(Application .class, args);

}

}

在需要审计实体中加入 @EntityListeners(AuditingEntityListener.class)

@EntityListeners(AuditingEntityListener.class)

public class Member  implements Serializable {

private static final long serialVersionUID = -6163675075289529459L;

@JsonIgnore

String entityName = this.getClass().getSimpleName();

@CreatedBy

String createdBy;

@LastModifiedBy

String modifiedBy;

/**

* 实体创建时间

*/

@Temporal(TemporalType.TIMESTAMP)

@CreatedDate

protected Date dateCreated = new Date();

/**

* 实体修改时间

*/

@Temporal(TemporalType.TIMESTAMP)

@LastModifiedDate

protected Date dateModified = new Date();

#省略getter setter

}

2.20. 注释 @Comment

@Column(columnDefinition = "int unsigned NOT NULL DEFAULT '0'")

@Comment("点赞")

private int likes;

@Column(columnDefinition = "int unsigned NOT NULL DEFAULT '0'")

@Comment("收藏")

private int favorites;

@Column(columnDefinition = "int unsigned NOT NULL DEFAULT '0'")

@Comment("转发")

private int forward;

2.21. @Pattern 数据匹配

/**

* 性别 CHAR(1) 0:女 1:男

*/

@Pattern(regexp = "[01]")

@Column(name = "gender",columnDefinition = "char(1)")

private String gender;

/**

* 身份证号 CHAR(18)

*/

@Pattern(regexp = "^([1-6][1-9]|50)\\d{4}(18|19|20)\\d{2}((0[1-9])|10|11|12)(([0-2][1-9])|10|20|30|31)\\d{3}[0-9Xx]$")

@Column(name = "identityCard",columnDefinition = "char(18)")

private String identityCard;

/**

* 所属部门 CHAR(2)  01：金融一部, 02：金融二部, 03：创新中心

*/

@Pattern(regexp = "(01|02|03)")

@Column(name = "department",columnDefinition = "char(2)")

private String department;

2.22. 实体继承

B、C 类继承 A 所有属性，并且主键均为数据库（auto_increment）

@MappedSuperclass

@(strategy = InheritanceType.TABLE_PER_CLASS)

public class A{

@Id

@GeneratedValue(strategy=GenerationType.IDENTITY)

private int id;

}

@Entity

@Table(name="b")

public class B extends A{

}

@Entity

@Table(name="c")

public class C extends A{

}

3. Repository

Repository： 仅仅是一个标识，没有任何方法，方便Spring自动扫描识别

CrudRepository： 继承Repository，实现了一组CRUD相关的方法

PagingAndSortingRepository： 继承CrudRepository，实现了一组分页排序相关的方法

JpaRepository： 继承PagingAndSortingRepository，实现一组JPA规范相关的方法

Spring Data JPA 为此提供了一些表达条件查询的关键字：

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 findByFirstnameIs,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 ages) … where x.age in ?1

NotIn findByAgeNotIn(Collection age) … 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)

常用如下：

And --- 等价于 SQL 中的 and 关键字，比如 findByUsernameAndPassword(String user, Striang pwd)

Or --- 等价于 SQL 中的 or 关键字，比如 findByUsernameOrAddress(String user, String addr)

Between --- 等价于 SQL 中的 between 关键字，比如 findBySalaryBetween(int max, int min)

LessThan --- 等价于 SQL 中的 "<"，比如 findBySalaryLessThan(int max)

GreaterThan --- 等价于 SQL 中的">"，比如 findBySalaryGreaterThan(int min)

IsNull --- 等价于 SQL 中的 "is null"，比如 findByUsernameIsNull()

IsNotNull --- 等价于 SQL 中的 "is not null"，比如 findByUsernameIsNotNull()

NotNull --- 与 IsNotNull 等价

Like --- 等价于 SQL 中的 "like"，比如 findByUsernameLike(String user)

NotLike --- 等价于 SQL 中的 "not like"，比如 findByUsernameNotLike(String user)

OrderBy ---等价于 SQL 中的 "order by"，比如 findByUsernameOrderBySalaryAsc(String user)

Not --- 等价于 SQL 中的 "！ ="，比如 findByUsernameNot(String user)

In --- 等价于 SQL 中的 "in"，比如 findByUsernameIn(Collection<String> userList) ，方法的参数可以是 Collection 类型，也可以是数组或者不定长参数

NotIn --- 等价于 SQL 中的 "not in"，比如 findByUsernameNotIn(Collection<String> userList) ，方法的参数可以是 Collection 类型，也可以是数组或者不定长

3.1. CrudRepository

CrudRepository 接口提供了最基本的对实体类的添删改查操作

T save(T entity); //保存单个实体

Iterable<T> save(Iterable<? extends T> entities);//保存集合

T findOne(ID id); //根据id查找实体

boolean exists(ID id); //根据id判断实体是否存在

Iterable<T> findAll(); //查询所有实体,不用或慎用!

long count(); //查询实体数量

void delete(ID id); //根据Id删除实体

void delete(T entity); //删除一个实体

void delete(Iterable<? extends T> entities); //删除一个实体的集合

void deleteAll(); //删除所有实体,不用或慎用!

批量保存

List<Book> books = new ArrayList<>();

books.add(new Book("Book A", new BookDetail(1)));

books.add(new Book("Book B", new BookDetail(2)));

books.add(new Book("Book C", new BookDetail(3)));

bookRepository.save(books);

3.2. JpaRepository

https://docs.spring.io/spring-data/jpa/docs/current/api/org/springframework/data/jpa/repository/JpaRepository.html

Modifier and Type Method and Description

void deleteAllInBatch()

Deletes all entities in a batch call.

void deleteInBatch(Iterable<T> entities)

Deletes the given entities in a batch which means it will create a single Query.

List<T> findAll()

<S extends T>

List<S> findAll(Example<S> example)

<S extends T>

List<S> findAll(Example<S> example, Sort sort)

List<T> findAll(Sort sort)

List<T> findAllById(Iterable<ID> ids)

void flush()

Flushes all pending changes to the database.

T getOne(ID id)

Returns a reference to the entity with the given identifier.

<S extends T>

List<S> saveAll(Iterable<S> entities)

<S extends T>

S saveAndFlush(S entity)

Saves an entity and flushes changes instantly.

3.3. PagingAndSortingRepository

Pageable

接口实现 PagingAndSortingRepository

package api.repository.h5;

import org.springframework.data.repository.PagingAndSortingRepository;

import api.domain.User;

public interface GatherRepository extends PagingAndSortingRepository<User, Integer> {

}

控制器添加 Pageable pageable 参数

@RequestMapping("/browse")

public ModelAndView browse(Pageable pageable) {

Page<User> users = userRepository.findAll(pageable);

System.out.println(users.toString());

ModelAndView mv = new ModelAndView();

mv.addObject("users", users.getContent());

mv.addObject("number", users.getNumber());

mv.addObject("size", users.getSize());

mv.addObject("totalPages", users.getTotalPages());

mv.setViewName("table");

return mv;

}

解决 PagingAndSortingRepository 没有 save 等方法的问题

如果 Repository 继承了 PagingAndSortingRepository 你会发 CrudRepository 中的 save 等方法不能使用了，我的解决方法是写两个 Repository

一个 CURD 的 ChatRepository 放在 cn.netkiller.repository

package cn.netkiller.repository;

import cn.netkiller.domain.Chat;

import org.springframework.data.repository.CrudRepository;

import org.springframework.stereotype.Repository;

import java.util.List;

@Repository

public interface ChatRepository extends CrudRepository<Chat, String> {

List<Chat> findAllBySession(String session);

Chat findOneBySession(String session);

}

另一个分页的 PagingAndSortingRepository 放在 cn.netkiller.repository.pageable

package cn.netkiller.repository.pageable;

import cn.netkiller.domain.Chat;

import org.springframework.data.domain.Page;

import org.springframework.data.domain.Pageable;

import org.springframework.data.repository.PagingAndSortingRepository;

import org.springframework.stereotype.Repository;

@Repository

public interface ChatPageableRepository extends PagingAndSortingRepository<Chat, String> {

Page<Chat> findAllByDevice(String device, Pageable pageable);

}

@PageableDefault 分页

@RequestMapping(value = "/list", method=RequestMethod.GET)

public Page<Blog> getEntryByPageable1(@PageableDefault( sort = { "id" }, direction = Sort.Direction.DESC)

Pageable pageable) {

return blogRepository.findAll(pageable);

}

@RequestMapping(value = "/blog", method=RequestMethod.GET)

public Page<Blog> getEntryByPageable(@PageableDefault(value = 15, sort = { "id" }, direction = Sort.Direction.DESC)

Pageable pageable) {

return blogRepository.findAll(pageable);

}

@RequestMapping(value = "/list", method=RequestMethod.GET)

public Page<Blog> getEntryByPageable2(@PageableDefault Pageable pageable) {

return blogRepository.findAll(pageable);

}

@ModelAttribute("users")

public Page<User> users(@PageableDefault(size = 5) Pageable pageable) {

return userManagement.findAll(pageable);

}

我们只需要在方法的参数中直接定义一个pageable类型的参数，当Spring发现这个参数时，Spring会自动的根据request的参数来组装该pageable对象，Spring支持的request参数如下：

page，第几页，从0开始，默认为第0页

size，每一页的大小，默认为20

sort，排序相关的信息，以property,property(,ASC|DESC)的方式组织，例如sort=firstname&sort=lastname,desc表示在按firstname正序排列基础上按lastname倒序排列

这样，我们就可以通过url的参数来进行多样化、个性化的查询，而不需要为每一种情况来写不同的方法了。

通过url来定制pageable很方便，但唯一的缺点是不太美观，因此我们需要为pageable设置一个默认配置，这样很多情况下我们都能够通过一个简洁的url来获取信息了。

Spring提供了@PageableDefault帮助我们个性化的设置pageable的默认配置。例如@PageableDefault(value = 15, sort = { "id" }, direction = Sort.Direction.DESC)表示默认情况下我们按照id倒序排列，每一页的大小为15。

3.4. findByXXX

@Autowired

private ArticleRepository articleRepository;

@RequestMapping("/mysql")

@ResponseBody

public String mysql() {

articleRepository.save(new Article("Neo", "Chen"));

for (Article article : articleRepository.findAll()) {

System.out.println(article);

}

Article tmp = articleRepository.findByTitle("Neo");

return tmp.getTitle();

}

@RequestMapping("/search")

@ResponseBody

public String search() {

for (Article article : articleRepository.findBySearch(1)) { System.out.println(article); }

List<Article> tmp = articleRepository.findBySearch(1L);

tmp.forEach((temp) -> {

System.out.println(temp.toString());

});

return tmp.get(0).getTitle();

}

传 Boolean 参数

package cn.netkiller.wallet.repository.fcoin;

import java.util.List;

import org.springframework.data.domain.Pageable;

import org.springframework.data.repository.CrudRepository;

import cn.netkiller.wallet.domain.fcoin.Fcoin;;

public interface FcoinRepository extends CrudRepository<Fcoin, String> {

Fcoin findOneByAddress(String address);

int countByAirdropFalse();

List<Fcoin> findByAirdrop(boolean airdrop, Pageable pageable);

}

Eunm 传递枚举参数

package cn.netkiller.api.repository;

import org.springframework.data.repository.CrudRepository;

import cn.netkiller.api.domain.StatisticsHistory;

public interface StatisticsHistoryRepostitory extends CrudRepository<StatisticsHistory, Long> {

public StatisticsHistory findByMemberIdAndStatisticsIdAndType(long member_id, long statistics_id,

StatisticsHistory.StatisticsType type);

}

@Autowired

private StatisticsHistoryRepostitory statisticsHistoryRepostitory;

statisticsHistoryRepostitory.findByMemberIdAndStatisticsIdAndType(uid, id, type);

3.5. count 操作

public interface UserRepository extends CrudRepository<User, Long> {

Long countByFirstName(String firstName);

}

3.6. delete 删除操作

@Transactional

Long deleteByFirstName(String firstName);

@Transactional

List<User> removeByFirstName(String firstName);

3.7. OrderBy

public List<StudentEntity> findAllByOrderByIdAsc();

public List<StudentEntity> findAllByOrderByIdDesc();

List<RecentRead> findByMemberIdOrderByIdDesc(int memberId, Pageable pageable);

3.8. GreaterThan

package schedule.repository;

import java.util.Date;

import org.springframework.data.repository.CrudRepository;

import common.domain.CmsTrash;

public interface CmsTrashRepository extends CrudRepository<CmsTrash, Integer> {

Iterable<CmsTrash> findBySiteIdAndTypeOrderByCtimeASC(int siteId, String string);

Iterable<CmsTrash> findBySiteIdAndTypeAndCtimeGreaterThanOrderByCtimeASC(int siteId, String string, Date date);

}

3.9. Sort 排序操作操作

List<UserModel> findByName(String name, Sort sort);

Sort sort = new Sort(Direction.DESC, "id");

repostitory.findByName("Neo", sort);

userRepository.findAll(Sort.by(Sort.Direction.ASC, "name"));

userRepository.findAll(Sort.by("LENGTH(name)"));

3.10. Pageable 翻页操作

Page 返回数据和页码等数据

PageRequest(int page, int size, Sort sort)  Deprecated.

use PageRequest.of(int, int, Sort) instead.

package cn.netkiller.repository;

import cn.netkiller.domain.Picture;

import org.springframework.data.domain.Page;

import org.springframework.data.domain.Pageable;

import org.springframework.data.jpa.repository.JpaRepository;

import org.springframework.stereotype.Repository;

import java.util.Optional;

@Repository

public interface PictureRepository extends JpaRepository<Picture, Long> {

Picture findAllBySession(String session);

Optional<Picture> findOneBySession(String session);

Page<Picture> findAll(Pageable pageable);

}

public Page<Picture> page(Pageable pageable) {

return pictureRepository.findAll(pageable);

}

@GetMapping("/{device}/page")

public Mono<Page<Picture>> page(@PathVariable String device, Pageable pageable) {

return Mono.just(pictureService.page(pageable));

}

排序 /picture/test/page?sort=id,desc

每页返回数量 /picture/test/page?size=10

返回第二页5条数据 /picture/test/page?size=5&page=1

返回第二页5条数据，ID倒序排序 /picture/test/page?size=5&page=1&sort=id,desc

curl -X 'GET' \

'http://localhost:8080/picture/test/page?page=0&size=1&sort=id' \

-H 'accept: */*'

PageRequest.of

package cn.netkiller.api.repository;

import java.util.List;

import org.springframework.data.domain.Pageable;

import org.springframework.data.repository.CrudRepository;

import cn.netkiller.api.domain.RecentRead;

public interface RecentReadRepostitory extends CrudRepository<RecentRead, Long> {

List<RecentRead> findByMemberId(long id, Pageable pageable);

}

Top 10 实例

@RequestMapping("/recent/read/list/{id}")

public List<RecentRead> recentList(@PathVariable long id) {

int page = 0;

int limit = 10;

List<RecentRead> recentRead = recentReadRepostitory.findByMemberId(id, new PageRequest(page, limit));

return recentRead;

}

翻页返回数据可以选择 Iterable/List 或者 Page。

Iterable/List 只返回数据，不含页码等数据

注意 PageRequest(int page, int size) 在新版 Spring boot 2.x 中已经废弃请使用 PageRequest.of(page, size) 替代

List<Fcoin> fcoins = fcoinRepository.findByAirdrop(false, PageRequest.of(0, size));

3.11. @DynamicInsert 与 @DynamicUpdate

@DynamicUpdate 只更新修改的字段

3.12. 继承已存在的 Repository

public interface MemberRepository extends JpaRepository<User, Integer>, UserRepository {

...

}

4. TransactionTemplate

@Autowired

private TransactionTemplate transactionTemplate;

...

...

public void save(final User user) {

transactionTemplate.execute((status) => {

doSomeThing(user);

doSomeThing1();

doSomeThing2();

doSomeThingN();

return Boolean.TRUE;

})

}

5. JPQL @Query

5.1. @Modifying 更新/删除

更新/删除操作需要加上 @Modifying 注解

@Modifying

@Query("update Money m set m.isDeleted=?2 where  m.money=?1")

void updateStateByMoney(Long money, Byte state);

@Modifying(clearAutomatically=true, flushAutomatically = true)

5.2. 事务 @Transactional

下面介绍一下@Transactional注解的参数以及使用：

事物传播行为介绍:

@Transactional(propagation=Propagation.REQUIRED) ：如果有事务, 那么加入事务, 没有的话新建一个(默认情况下)

@Transactional(propagation=Propagation.NOT_SUPPORTED) ：容器不为这个方法开启事务

@Transactional(propagation=Propagation.REQUIRES_NEW) ：不管是否存在事务,都创建一个新的事务,原来的挂起,新的执行完毕,继续执行老的事务

@Transactional(propagation=Propagation.MANDATORY) ：必须在一个已有的事务中执行,否则抛出异常

@Transactional(propagation=Propagation.NEVER) ：必须在一个没有的事务中执行,否则抛出异常(与Propagation.MANDATORY相反)

@Transactional(propagation=Propagation.SUPPORTS) ：如果其他bean调用这个方法,在其他bean中声明事务,那就用事务.如果其他bean没有声明事务,那就不用事务.

事物超时设置:

@Transactional(timeout=30) //默认是30秒

事务隔离级别:

@Transactional(isolation = Isolation.READ_UNCOMMITTED)：读取未提交数据(会出现脏读, 不可重复读) 基本不使用

@Transactional(isolation = Isolation.READ_COMMITTED)：读取已提交数据(会出现不可重复读和幻读)

@Transactional(isolation = Isolation.REPEATABLE_READ)：可重复读(会出现幻读)

@Transactional(isolation = Isolation.SERIALIZABLE)：串行化 　MYSQL: 默认为REPEATABLE_READ级别 　SQLSERVER: 默认为READ_COMMITTED

@Transactional注解中常用参数说明

注意的几点:

@Transactional 只能被应用到public方法上, 对于其它非public的方法,如果标记了@Transactional也不会报错,但方法没有事务功能.

用 spring 事务管理器,由spring来负责数据库的打开,提交,回滚.默认遇到运行期例外(throw new RuntimeException("注释");)会回滚，即遇到不受检查（unchecked）的例外时回滚；而遇到需要捕获的例外(throw new Exception("注释");)不会回滚,即遇到受检查的例外（就是非运行时抛出的异常，编译器会检查到的异常叫受检查例外或说受检查异常）时，需我们指定方式来让事务回滚要想所有异常都回滚,要加上 @Transactional( rollbackFor={Exception.class,其它异常}) .如果让unchecked例外不回滚： @Transactional(notRollbackFor=RunTimeException.class)

@Transactional 注解应该只被应用到 public 可见度的方法上。 如果你在 protected、private 或者 package-visible 的方法上使用 @Transactional 注解，它也不会报错， 但是这个被注解的方法将不会展示已配置的事务设置。

@Transactional 注解可以被应用于接口定义和接口方法、类定义和类的 public 方法上。然而，请注意仅仅 @Transactional 注解的出现不足于开启事务行为，它仅仅 是一种元数据，能够被可以识别 @Transactional 注解和上述的配置适当的具有事务行为的beans所使用。上面的例子中，其实正是 元素的出现 开启 了事务行为。

Spring团队的建议是你在具体的类（或类的方法）上使用 @Transactional 注解，而不要使用在类所要实现的任何接口上。你当然可以在接口上使用 @Transactional 注解，但是这将只能当你设置了基于接口的代理时它才生效。因为注解是不能继承的，这就意味着如果你正在使用基于类的代理时，那么事务的设置将不能被基于类的代理所识别，而且对象也将不会被事务代理所包装（将被确认为严重的）。因此，请接受Spring团队的建议并且在具体的类上使用 @Transactional 注解。

删除更新需要 @Transactional 注解

package cn.netkiller.api.repository;

import javax.transaction.Transactional;

import org.springframework.data.domain.Page;

import org.springframework.data.domain.Pageable;

import org.springframework.data.jpa.repository.Modifying;

import org.springframework.data.jpa.repository.Query;

import org.springframework.data.repository.CrudRepository;

import org.springframework.data.repository.query.Param;

import org.springframework.stereotype.Repository;

import cn.netkiller.api.domain.RecentRead;

@Repository

public interface RecentReadRepostitory extends CrudRepository<RecentRead, Integer> {

Page<RecentRead> findByMemberIdOrderByIdDesc(int memberId, Pageable pageable);

int countByMemberId(int memberId);

@Transactional

@Modifying

@Query("DELETE FROM RecentRead r WHERE r.memberId = ?1 AND r.articleId = ?2")

void deleteByMemberIdAndArticleId(int memberId, int articleId);

@Transactional

@Modifying

@Query("delete from RecentRead where member_id = :member_id")

public void deleteByMemberId(@Param("member_id") int memberId);

int countByMemberIdAndArticleId(int memberId, int articleId);

}

回滚操作

// 指定Exception回滚

@Transactional(rollbackFor=Exception.class)

public void methodName() {

// 不会回滚

throw new Exception("...");

}

//指定Exception回滚，但其他异常不回滚

@Transactional(noRollbackFor=Exception.class)

public ItimDaoImpl getItemDaoImpl() {

// 会回滚

throw new RuntimeException("注释");

}

@Service

public class UserService {

@Autowired

private UserRepostitory userRepostitory;

@Transactional

public void add(User user) {

try {

userRepostitory.save(user);

} catch (Exception e) {

log.error(e.getMessage(), e);

}

// 不会回滚

}

@Transactional

public void add(User user) throws Exception {

try {

userRepostitory.delete(user);

} catch (Exception e) {

log.error(e.getMessage(), e);

// 抛出异常才会回滚

throw new Exception(e);

}

}

}

private、default、protected 和 final 不支持事物

@Transactional 必须与 public 一起使用，不能定义为 private、default、protected

@Service

public class UserService {

@Transactional

private void add(User user) {

save(user);

}

}

Service 注意事项

this 调用不支持事物

@Service

public class UserService {

@Autowired

private UserRepostitory userRepostitory;

@Transactional

public void add(User user) {

userRepostitory.save(user);

this.update(user);

}

@Transactional

public void update(User user) {

userRepostitory.update(user);

}

}

解决方案

@Servcie

public class UserService {

@Autowired

prvate ProfileService profileService;

public void save(User user) {

profileService.save(user);

}

}

@Servcie

public class ProfileService {

@Transactional(rollbackFor=Exception.class)

public void save(User user) {

}

}

需要 @Service 注解配合使用

@Transactional 需要在 @Controller、@Service、@Component、@Repository 等注解下才能使用

// @Servcie

public class UserService {

@Autowired

prvate ProfileService profileService;

@Transactional

public void save(User user) {

profileService.save(user);

}

}

屏蔽 @Servcie 后观察 save 的 @Transactional 是不生效的。

@Service

public class UserService {

@Autowired

private UserRepostitory userRepostitory;

@Autowired

private RoleService roleService;

@Transactional

public void add(User user) throws Exception {

userRepostitory.save(user);

new Thread(() -> {

roleService.doOtherThing();

}).start();

}

}

@Service

public class RoleService {

@Transactional

public void doOtherThing() {

...

...

}

}

5.3. 参数传递

package api.repository.oracle;

import org.springframework.data.domain.Page;

import org.springframework.data.domain.Pageable;

import org.springframework.data.jpa.repository.Query;

import org.springframework.data.repository.CrudRepository;

import org.springframework.data.repository.query.Param;

import org.springframework.stereotype.Repository;

import api.domain.oracle.Member;

@Repository

public interface MemberRepository extends CrudRepository<Member, Long> {

public Page<Member> findAll(Pageable pageable);

// public Member findByBillno(String billno);

public Member findById(String id);

@Query("SELECT m FROM Member m WHERE m.status = 'Y' AND m.id = :id")

public Member findFinishById(@Param("id") String id);

}

import org.springframework.data.jpa.repository.JpaRepository;

import org.springframework.data.jpa.repository.Query;

import org.springframework.data.repository.query.Param;

public interface PersonRepository extends JpaRepository<Person, Long> {

@Query("SELECT p FROM Person p WHERE LOWER(p.lastName) = LOWER(:lastName)")

public List<Person> find(@Param("lastName") String lastName);

}

5.4. 原生 SQL

public interface UserRepository extends JpaRepository<User, Long> {

@Query(value = "SELECT * FROM USERS WHERE EMAIL_ADDRESS = ?0", nativeQuery = true)

User findByEmailAddress(String emailAddress);

}

insert ignore

@Modifying

@Query(value = "insert ignore into emp(create, modified, user_id, user_name, user_nickname, user_mail) values(?1, ?2, ?3, ?4, ?5, ?6)", nativeQuery = true)

void insertIgnoreEmployee(Timestamp create, Timestamp modified, String userId, String name, String nickname, String mail);

5.5. @Query 与 Pageagble

https://docs.spring.io/spring-data/jpa/docs/current/reference/html/#_native_queries

@Query(value = "SELECT u FROM User u ORDER BY id")

Page<User> findAllUsersWithPagination(Pageable pageable);

package api.domain;

import java.io.Serializable;

import javax.persistence.Column;

import javax.persistence.Entity;

import javax.persistence.Id;

import javax.persistence.Index;

import javax.persistence.Table;

@Entity

@Table(indexes = { @Index(name = "address", columnList = "from_address,to_address"), @Index(name = "contractAddress", columnList = "contractAddress") })

public class TransactionHistory implements Serializable {

private static final long serialVersionUID = 6710992220657056861L;

@Id

@Column(name = "blockNumber", unique = true, nullable = false, insertable = true, updatable = false)

private int blockNumber;

private String timeStamp;

private String hash;

@Column(name = "from_address")

private String from;

@Column(name = "to_address")

private String to;

private String value;

private String gas;

private String gasPrice;

private String isError;

private String contractAddress;

private String gasUsed;

private String symbol;

public TransactionHistory() {

// TODO Auto-generated constructor stub

}

public int getBlockNumber() {

return blockNumber;

}

public void setBlockNumber(int blockNumber) {

this.blockNumber = blockNumber;

}

public String getTimeStamp() {

return timeStamp;

}

public void setTimeStamp(String timeStamp) {

this.timeStamp = timeStamp;

}

public String getHash() {

return hash;

}

public void setHash(String hash) {

this.hash = hash;

}

public String getFrom() {

return from;

}

public void setFrom(String from) {

this.from = from;

}

public String getTo() {

return to;

}

public void setTo(String to) {

this.to = to;

}

public String getValue() {

return value;

}

public void setValue(String value) {

this.value = value;

}

public String getGas() {

return gas;

}

public void setGas(String gas) {

this.gas = gas;

}

public String getGasPrice() {

return gasPrice;

}

public void setGasPrice(String gasPrice) {

this.gasPrice = gasPrice;

}

public String getIsError() {

return isError;

}

public void setIsError(String isError) {

this.isError = isError;

}

public String getContractAddress() {

return contractAddress;

}

public void setContractAddress(String contractAddress) {

this.contractAddress = contractAddress;

}

public String getGasUsed() {

return gasUsed;

}

public void setGasUsed(String gasUsed) {

this.gasUsed = gasUsed;

}

public static long getSerialversionuid() {

return serialVersionUID;

}

public String getSymbol() {

return symbol;

}

public void setSymbol(String symbol) {

this.symbol = symbol;

}

@Override

public String toString() {

return "TransactionHistory [blockNumber=" + blockNumber + ", timeStamp=" + timeStamp + ", hash=" + hash + ", from=" + from + ", to=" + to + ", value=" + value + ", gas=" + gas + ", gasPrice=" + gasPrice + ", isError=" + isError + ", contractAddress=" + contractAddress + ", gasUsed=" + gasUsed + ", symbol=" + symbol + "]";

}

}

package api.repository;

import org.springframework.data.domain.Page;

import org.springframework.data.domain.Pageable;

import org.springframework.data.jpa.repository.Query;

import org.springframework.data.repository.CrudRepository;

import org.springframework.data.repository.query.Param;

import org.springframework.stereotype.Repository;

import api.domain.TransactionHistory;

@Repository

public interface TransactionHistoryRepository extends CrudRepository<TransactionHistory, Integer> {

@Query(value = "SELECT * FROM transaction_history th WHERE (th.from_address = :address or th.to_address = :address) and contract_address is NULL",

countQuery = "SELEÇT count(*) FROM transaction_history th WHERE (th.from_address = :address or th.to_address = :address) and contract_address is NULL",

nativeQuery = true)

public Page<TransactionHistory> findEthByAddress(@Param("address") String address, Pageable pageable);

}

5.6. 返回指定字段

通过实体返回数据有时结果集非常庞大，可能会影响性能，这时我们只需要返回指定字段即可。

@Query(value = "select u.userName, ui.name, ui.gender, ui.description from UserInfo ui, User u where u.id = ui.userId")

public List<Object> getCustomField();

5.7. 返回指定的模型

临时写一个新的模型

public class MyModel implements Serializable {

private String userName;

private String name;

private String gender;

private String description;

public MyModel() {};

public MyModel(String userName, String name, String gender, String description) {

this.userName = userName;

this.name = name;

this.gender = gender;

this.description = description;

}

}

使用构造方法赋值

@Query(value = "select new cn.netkiller.model.MyModel(u.userName, ui.name, ui.gender, ui.description) from UserInfo ui, User u where u.id = ui.userId")

public List<MyModel> getAllRecord();

5.8. Collection

返回集合

@Query("SELECT u FROM User u WHERE u.status = 1")

Collection<User> findAllActiveUsers();

处理子查询 IN

@Query(value = "SELECT u FROM User u WHERE u.name IN :names")

List<User> findUserByNameList(@Param("names") Collection<String> names);

5.9. 原生SQL查询

这里的nativeQuery=true代表在执行这个方法的时候使用原生sql语句，直接写数据库中的实际表名和表中的字段名，而不是实体表名。

@Modifying

@Query(nativeQuery = true, value = "UPDATE project p, (SELECT MIN(start) AS start, MAX(finish) AS finish FROM project WHERE parent_id = :id) t SET p.start = t.start, p.finish = t.finish WHERE p.id = :id")

public void updateStartAndFinishById(@Param("id") Long id);

在什么情况下使用呢？例如上面，同时操作两张表，做更新，如果不使用 nativeQuery = true 无法实现。

5.10. Sort

@Query(value = "SELECT u FROM User u")

List<User> findAllUsers(Sort sort);

5.11. 锁 @Lock

interface UserRepository extends Repository<User, Long> {

// Plain query method

@Lock(LockModeType.READ)

List<User> findByLastname(String lastname);

第 50 章 Spring Data with Redis

1. 集成 Redis XML 方式

1.1. pom.xml

<dependency>

<groupId>org.springframework.boot</groupId>

<artifactId>spring-boot-starter-redis</artifactId>

</dependency>

1.2. springframework-servlet.xml

<!-- Redis Connection Factory -->

<bean id="jedisConnFactory"

class="org.springframework.data.redis.connection.jedis.JedisConnectionFactory"

p:host-name="192.168.2.1" p:port="6379" p:use-pool="true" />

<!-- redis redisTemplate definition -->

<bean id="redisTemplate" class="org.springframework.data.redis.core.RedisTemplate"

p:connection-factory-ref="jedisConnFactory" />

例 50.1. Spring Data Redis Example

<?xml version="1.0" encoding="UTF-8"?>

<beans xmlns="http://www.springframework.org/schema/beans"

xmlns:mvc="http://www.springframework.org/schema/mvc" xmlns:context="http://www.springframework.org/schema/context"

xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:p="http://www.springframework.org/schema/p"

xsi:schemaLocation="

http://www.springframework.org/schema/beans

http://www.springframework.org/schema/beans/spring-beans.xsd

http://www.springframework.org/schema/mvc

http://www.springframework.org/schema/mvc/spring-mvc.xsd

http://www.springframework.org/schema/context

http://www.springframework.org/schema/context/spring-context.xsd">

<mvc:resources location="/images/" mapping="/images/**" />

<mvc:resources location="/css/" mapping="/css/**" />

<context:component-scan base-package="cn.netkiller.controller" />

<mvc:annotation-driven />

<bean

class="org.springframework.web.servlet.view.InternalResourceViewResolver">

<property name="viewClass"

value="org.springframework.web.servlet.view.JstlView" />

<property name="prefix" value="/WEB-INF/jsp/" />

<property name="suffix" value=".jsp" />

<!-- <property name="viewNames" value="*.jsp" /> -->

</bean>

<bean id="configuracion"

class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer">

<property name="location" value="classpath:resources/development.properties" />

</bean>

<bean id="dataSource"

class="org.springframework.jdbc.datasource.DriverManagerDataSource">

<property name="driverClassName" value="${jdbc.driverClassName}" />

<property name="url" value="${jdbc.url}" />

<property name="username" value="${jdbc.username}" />

<property name="password" value="${jdbc.password}" />

</bean>

<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">

<property name="dataSource" ref="dataSource" />

</bean>

<bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">

<property name="basePackage" value="cn.netkiller.mapper" />

</bean>

<bean id="userService" class="cn.netkiller.service.UserService">

</bean>

<!-- Redis Connection Factory -->

<bean id="jedisConnFactory"

class="org.springframework.data.redis.connection.jedis.JedisConnectionFactory"

p:host-name="192.168.2.1" p:port="6379" p:use-pool="true" />

<!-- redis redisTemplate definition -->

<bean id="redisTemplate" class="org.springframework.data.redis.core.RedisTemplate"

p:connection-factory-ref="jedisConnFactory" />

</beans>

1.3. Controller

package cn.netkiller.controller;

import javax.annotation.Resource;

import org.springframework.beans.factory.annotation.Autowired;

import org.springframework.data.redis.core.ListOperations;

import org.springframework.data.redis.core.RedisTemplate;

import org.springframework.data.redis.serializer.StringRedisSerializer;

import org.springframework.stereotype.Controller;

import org.springframework.web.bind.annotation.RequestMapping;

import org.springframework.web.servlet.ModelAndView;

import cn.netkiller.model.User;

@Controller

public class CacheController {

// inject the actual redisTemplate

@Autowired

private RedisTemplate<String, String> redisTemplate;

// inject the redisTemplate as ListOperations

@Resource(name = "redisTemplate")

private ListOperations<String, String> listOps;

@RequestMapping("/cache")

public ModelAndView cache() {

String message = "";

User user = new User();

user.setId("1");

user.setName("Neo");

user.setAge(30);

String key = "user";

listOps.leftPush(key, user.toString());

message = listOps.leftPop(key);

redisTemplate.setKeySerializer(new StringRedisSerializer());

redisTemplate.setValueSerializer(new StringRedisSerializer());

redisTemplate.opsForValue().set("key", user.toString());

return new ModelAndView("index/index", "variable", message);

}

}

1.4. index.jsp

<%@ page language="java" contentType="text/html; charset=ISO-8859-1"

pageEncoding="ISO-8859-1"%>

<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">

<html>

<head>

<meta http-equiv="Content-Type" content="text/html; charset=ISO-8859-1">

<title>Insert title here</title>

</head>

<body>

<br>

<div style="text-align:center">

<h2>

${variable}

</h2>

</div>

</body>

</html>

1.5. 测试

请求URL http://your.domain.com/your.html

[root@master ~]# redis-cli

redis 127.0.0.1:6379> keys *

1) "\xac\xed\x00\x05t\x00\x04user"

2) "key"

redis 127.0.0.1:6379> get key

"\xac\xed\x00\x05t\x00\x1dUser [id=1, name=Neo, age=30]"

提示

Spring Redis 默认使用 Byte数据类型存储Key，在redis-cli中会看到 "\xac\xed\x00\x05t\x00\x04" 前缀不方便get操作，所以我们会设置使用字符串，通过 redisTemplate.setKeySerializer(new StringRedisSerializer()); 实现

2. RedisTemplate

2.1. stringRedisTemplate 基本用法

stringRedisTemplate.opsForValue().set("test", "100",60*10,TimeUnit.SECONDS); //向redis里存入数据和设置缓存时间

stringRedisTemplate.opsForValue().get("test") //根据key获取缓存中的val

stringRedisTemplate.getExpire("test") //根据key获取过期时间

stringRedisTemplate.getExpire("test",TimeUnit.SECONDS) //根据key获取过期时间并换算成指定单位

stringRedisTemplate.delete("test"); //根据key删除缓存

stringRedisTemplate.hasKey("546545"); //检查key是否存在，返回boolean值

stringRedisTemplate.expire("test",1000 , TimeUnit.MILLISECONDS); //设置过期时间

2.2. 设置缓存时间

例子：设置 name 缓存 10 秒

redisTemplate.opsForValue().set("name","neo",10, TimeUnit.SECONDS);

redisTemplate.opsForValue().get("name")

结果：由于设置的是10秒失效，十秒之内查询有结果，十秒之后返回为null

2.3. 字符串截取

设置：redisTemplate.opsForValue().set("hello","Helloworld");

代码：System.out.println(redisTemplate.opsForValue().get("hello",0,5));

结果：Hellow

代码：System.out.println(redisTemplate.opsForValue().get("hello",0,-1));

结果：Helloworld

代码：System.out.println(redisTemplate.opsForValue().get("hello",-3,-1));

结果：rld

2.4. 追加字符串

redisTemplate.opsForValue().append("hello","Hello");

System.out.println(redisTemplate.opsForValue().get("hello"));

redisTemplate.opsForValue().append("hello","world");

System.out.println(redisTemplate.opsForValue().get("hello")); // 结果：Helloworld

2.5. 设置键的字符串值并返回其旧值

redisTemplate.opsForValue().set("name","neo");

System.out.println(redisTemplate.opsForValue().getAndSet("name","Jerry"));

// 结果 neo

2.6. increment

stringRedisTemplate.opsForValue().set("test", "100"); //向redis里存入数据

stringRedisTemplate.boundValueOps("test").increment(-50); //val做-60操作

stringRedisTemplate.boundValueOps("test").increment(100); //val +100

stringRedisTemplate.opsForValue().get("test") //根据key获取缓存中的val

2.7. 删除 key

private void cleanNewToday() {

long begin = System.currentTimeMillis();

redisTemplate.delete("news:today");

long end = System.currentTimeMillis();

logger.info("Schedule clean redis {} 耗时 {} 秒", "cleanNewFlash()", (end-begin) / 1000 );

}

2.8. 返回字符串长度

redisTemplate.opsForValue().set("key","hello world");

System.out.println(redisTemplate.opsForValue().size("key"));

2.9. 如果key不存便缓存。

System.out.println(redisTemplate.opsForValue().setIfAbsent("name","neo")); // name 之前已经存在 false

System.out.println(redisTemplate.opsForValue().setIfAbsent("age","11")); // age 之前不存在 true

setIfAbsent 实现分布式锁

boolean static = Boolean.TRUE.equals(redisTemplate.opsForValue().setIfAbsent("lock:order", 1, 1, TimeUnit.DAYS));

2.10. 缓存多个值 /获取多个值 multiSet / multiGet

Map<String,String> maps = new HashMap<String, String>();

maps.put("multi1","multi1");

maps.put("multi2","multi2");

maps.put("multi3","multi3");

redisTemplate.opsForValue().multiSet(maps);

List<String> keys = new ArrayList<String>();

keys.add("multi1");

keys.add("multi2");

keys.add("multi3");

System.out.println(redisTemplate.opsForValue().multiGet(keys));

输出结果

[multi1, multi2, multi3]

为多个键分别设置它们的值，如果存在则返回false，不存在返回true

Map<String,String> maps = new HashMap<String, String>();

maps.put("multi11","multi11");

maps.put("multi22","multi22");

maps.put("multi33","multi33");

Map<String,String> maps2 = new HashMap<String, String>();

maps2.put("multi1","multi1");

maps2.put("multi2","multi2");

maps2.put("multi3","multi3");

System.out.println(redisTemplate.opsForValue().multiSetIfAbsent(maps)); // 返回 true

System.out.println(redisTemplate.opsForValue().multiSetIfAbsent(maps2)); // 返回 false

2.11. List

rightPush

ListOperations<String, Object> list = redisTemplate.opsForList();

list.rightPush("books", "Linux");

list.rightPush("books", "Java");

System.out.println(list.range("books", 0, 1));

System.out.println(redisTemplate.opsForList().size("list"));

rightPushAll

String[] stringarrays = new String[]{"1","2","3"};

redisTemplate.opsForList().rightPushAll("listarrayright",stringarrays);

System.out.println(redisTemplate.opsForList().range("listarrayright",0,-1));

List<Object> strings = new ArrayList<Object>();

strings.add("1");

strings.add("2");

strings.add("3");

redisTemplate.opsForList().rightPushAll("listcollectionright", strings);

System.out.println(redisTemplate.opsForList().range("listcollectionright",0,-1));

rightPushIfPresent

System.out.println("========== KEY 不存在===========");

System.out.println(redisTemplate.opsForList().rightPushIfPresent("rightPushIfPresent","aa"));

System.out.println(redisTemplate.opsForList().rightPushIfPresent("rightPushIfPresent","bb"));

System.out.println("========== KEY 已经存在===========");

System.out.println(redisTemplate.opsForList().rightPushIfPresent("rightPushIfPresent","aa"));

System.out.println(redisTemplate.opsForList().rightPushIfPresent("rightPushIfPresent","bb"));

leftPush

redisTemplate.opsForList().leftPush("list","java");

redisTemplate.opsForList().leftPush("list","python");

redisTemplate.opsForList().leftPush("list","c++");

leftPushAll

批量把一个数组插入到列表中

String[] stringarrays = new String[]{"1","2","3"};

redisTemplate.opsForList().leftPushAll("listarray",stringarrays);

批量把一个集合插入到列表中

使用：List<Object> strings = new ArrayList<Object>();

strings.add("1");

strings.add("2");

strings.add("3");

redisTemplate.opsForList().leftPushAll("listcollection", strings);

System.out.println(redisTemplate.opsForList().range("listcollection",0,-1));

结果:[3, 2, 1]

range

System.out.println(redisTemplate.opsForList().range("listarray",0,-1));

// 结果:[3, 2, 1]

2.12. SET 数据类型

Redis的Set是无序集合并且集合成员是唯一的，这就意味着集合中不能出现重复的数据。

stringRedisTemplate.opsForSet().add("test", "1","2","3"); //向指定key中存放set集合

stringRedisTemplate.opsForSet().isMember("test", "1") //根据key查看集合中是否存在指定数据

stringRedisTemplate.opsForSet().members("test"); //根据key获取set集合

//添加 一个 set 集合

SetOperations<String, Object> set = redisTemplate.opsForSet();

set.add("Member", "neo");

set.add("Member", "36");

set.add("Member", "178cm");

//输出 set 集合

System.out.println(set.members("Member"));

package cn.netkiller.api.restful;

import java.util.Set;

import org.springframework.beans.factory.annotation.Autowired;

import org.springframework.data.redis.core.RedisTemplate;

import org.springframework.web.bind.annotation.PathVariable;

import org.springframework.web.bind.annotation.RequestMapping;

import org.springframework.web.bind.annotation.RestController;

import common.pojo.ResponseRestful;

@RestController

@RequestMapping("/news")

public class NewsRestController {

@Autowired

private RedisTemplate<String, String> redisTemplate;

@RequestMapping(value = "/flash/{count}")

public ResponseRestful flash(@PathVariable("count") long count) {

if(count == 0L) {

count=10L;

}

Set<String> news = this.redisTemplate.opsForZSet().reverseRange("news:flash", 0, count);

if (news == null) {

return new ResponseRestful(false, 10, "没有查询到结果", news);

}

return new ResponseRestful(true, 0, "返回数据: " + news.size() + " 条", news);

}

public void addRecentUser(long userId, String name) {

String key = RedisKeyGenerator.genRecentBrowsingPositionsKey(String.valueOf(userId));

// 获取已缓存的最近浏览的职位

ZSetOperations<String, String> zSetOperations = redisTempalte.opsForZSet();

//zset内部是按分数来排序的，这里用当前时间做分数

zSetOperations.add(key, name, System.currentTimeMillis());

zSetOperations.removeRange(key, 0, -6);

}

}

返回集合中的所有成员

System.out.println(redisTemplate.opsForSet().members("setTest"));

取出一个成员

System.out.println(redisTemplate.opsForSet().pop("setTest"));

随机获取无序集合中的一个元素

System.out.println("Random member: " + redisTemplate.opsForSet().randomMember("setTest"));

随机获取 n 个成员（存在重复数据）

System.out.println("Random member: " + redisTemplate.opsForSet().randomMembers("setTest",5));

// 结果 Random member: [ccc, ddd, ddd, ddd, aaa]

随机获取 n 个不重复成员

System.out.println("Random members: " + redisTemplate.opsForSet().distinctRandomMembers("setTest",5));

//结果 Random members: [aaa, bbb, ddd, ccc]

在两个 SET 间移动数据

redisTemplate.opsForSet().move("key1","aaa","key2");

System.out.println(redisTemplate.opsForSet().members("key1"));

System.out.println(redisTemplate.opsForSet().members("key2"));

成员删除

String[] arrays = new String[]{"Java","PHP"};

System.out.println(redisTemplate.opsForSet().remove("setTest",arrays));

返回集合数量

System.out.println(redisTemplate.opsForSet().size("setTest"));

判断元素是否在集合成员中

ystem.out.println(redisTemplate.opsForSet().isMember("setTest","Linux"));

对比两个集合求交集

System.out.println(redisTemplate.opsForSet().members("key"));

System.out.println(redisTemplate.opsForSet().members("otherKey"));

System.out.println(redisTemplate.opsForSet().intersect("key","otherKey"));

List<String> library2 = new ArrayList<String>();

library2.add("Linux");

library2.add("FreeBSD");

System.out.println(redisTemplate.opsForSet().intersect("library1",library2));

对比两个集合求交集，然后存储到新的 key 中

System.out.println(redisTemplate.opsForSet().intersectAndStore("key","otherKey","destKey"));

List<String> otherKey = new ArrayList<String>();

otherKey.add("《Netkiller Java 手札》");

otherKey.add("《Netkiller Spring Cloud 手札》");

System.out.println(redisTemplate.opsForSet().intersectAndStore("key",otherKey,"destKey"));

合并两个集合，并去处重复数据

System.out.println(redisTemplate.opsForSet().union("setTest1","setTest2"));

List<String> otherKey = new ArrayList<String>();

otherKey.add("《Netkiller Java 手札》");

otherKey.add("《Netkiller Spring Cloud 手札》");

System.out.println(redisTemplate.opsForSet().union("setTest",otherKey));

合并两个集合去重复后保存到新的 key 中

System.out.println(redisTemplate.opsForSet().unionAndStore("key","otherKey","destKey"));

System.out.println(redisTemplate.opsForSet().unionAndStore("key",otherKey,"destKey"));

计算两个合集的差集

System.out.println(redisTemplate.opsForSet().difference("key","otherKey"));

List<String> otherKey = new ArrayList<String>();

otherKey.add("setTest2");

otherKey.add("setTest3");

System.out.println(redisTemplate.opsForSet().difference("key",otherKey));

计算两个合集的差集，然后保存到新的 key 中

System.out.println(redisTemplate.opsForSet().differenceAndStore("key","otherKey","destKey"));

遍历 SET 集合

Cursor<Object> curosr = redisTemplate.opsForSet().scan("setTest", ScanOptions.NONE);

while(curosr.hasNext()){

System.out.println(curosr.next());

}

2.13. 有序的 set 集合

//添加有序的 set 集合

ZSetOperations<String, Object> zset = redisTemplate.opsForZSet();

zset.add("zMember", "neo", 0);

zset.add("zMember", "36", 1);

zset.add("zMember", "178cm", 2);

//输出有序 set 集合

System.out.println(zset.rangeByScore("zMember", 0, 2));

2.14. Hash

put

redisTemplate.opsForHash().put("redisHash","name","neo");

redisTemplate.opsForHash().put("redisHash","age",30);

redisTemplate.opsForHash().put("redisHash","nickname","netkiller");

putAll

HashOperations<String, Object, Object>  hash = redisTemplate.opsForHash();

Map<String,Object> map = new HashMap<String,Object>();

map.put("name", "neo");

map.put("age", "36");

hash.putAll("member", map);

System.out.println(hash.entries("member"));

从键中的哈希获取给定hashKey的值

System.out.println(redisTemplate.opsForHash().get("redisHash","age"));

delete

删除指定的哈希 hashKeys

System.out.println(redisTemplate.opsForHash().delete("redisHash","name"));

确定哈希hashKey是否存在

确定哈希hashKey是否存在

System.out.println(redisTemplate.opsForHash().hasKey("redisHash","age"));

从哈希中获取指定的多个 hashKey 的值

List<Object> keys = new ArrayList<Object>();

keys.add("name");

keys.add("age");

System.out.println(redisTemplate.opsForHash().multiGet("redisHash",keys))

只有hashKey不存在时才能添加值

System.out.println(redisTemplate.opsForHash().putIfAbsent("redisHash","age",30));

获取整个Hash

System.out.println(redisTemplate.opsForHash().entries("redisHash"));

获取所有key

System.out.println(redisTemplate.opsForHash().keys("redisHash1"));

通过 hashKey 获取所有值

System.out.println(redisTemplate.opsForHash().values("redisHash"));

值加法操作

System.out.println(redisTemplate.opsForHash().increment("redisHash","age",1)

遍历 Hash 表

Cursor<Map.Entry<Object, Object>> curosr = redisTemplate.opsForHash().scan("redisHash", ScanOptions.ScanOptions.NONE);

while(curosr.hasNext()){

Map.Entry<Object, Object> entry = curosr.next();

System.out.println(entry.getKey()+":"+entry.getValue());

}

2.15. 过期时间未执行

Spring Redis 中设置过期时间方法如下

设置 key

redisTemplate.opsForValue().setIfAbsent("key", "value");

设置过期时间

redisTemplate.expire("key", 30000, TimeUnit.MILLISECONDS);

释放 key

redisTemplate.delete("key");

这样存在一个问题，当程序运行一半被强行终止，可能导致setIfAbsent运行完成，但是expire未被执行，这样 key 便永远不会释放。解决方案如下，使用RedisCallback执行原生 Redis 命令。

String result = redisTemplate.execute(new RedisCallback<String>() {

@Override

public String doInRedis(RedisConnection connection) throws DataAccessException {

JedisCommands commands = (JedisCommands) connection.getNativeConnection();

return commands.set(key, value, "NX", "PX", expire);

}

});

2.16. setBit / getBit 二进制位操作

setBit Boolean setBit(K key, long offset, boolean value);

offset 二进制位置(从左向右数)

value 位 ture 表示 0，false 表示 1

// 'a' 的ASCII码是 97 转换为二进制是：01100001

// 'b' 的ASCII码是 98 转换为二进制是：01100010

// 'c' 的ASCII码是 99 转换为二进制是：01100011

redisTemplate.opsForValue().set("bitTest","a");

redisTemplate.opsForValue().setBit("bitTest",7, false); // 01100011

redisTemplate.opsForValue().setBit("bitTest",8, true); // 01100010

System.out.println(redisTemplate.opsForValue().get("bitTest"));

redisTemplate.opsForValue().setBit("bitTest",8, false); // 01100011

System.out.println(redisTemplate.opsForValue().get("bitTest"));

getBit Boolean getBit(K key, long offset); 获取键对应值的ascii码的在offset处位值

System.out.println(redisTemplate.opsForValue().getBit("bitTest",7));

2.17. 存储 Json 对象

集成 RedisTemplate 定义新类 JsonRedisTemplate

package cn.netkiller.wallet.redis;

import org.springframework.data.redis.connection.RedisConnectionFactory;

import org.springframework.data.redis.core.RedisTemplate;

import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;

import org.springframework.data.redis.serializer.RedisSerializer;

import org.springframework.data.redis.serializer.StringRedisSerializer;

import com.fasterxml.jackson.databind.ObjectMapper;

public class JsonRedisTemplate extends RedisTemplate<String, Object> {

public JsonRedisTemplate(RedisConnectionFactory connectionFactory, ObjectMapper objectMapper, Class<?> valueType) {

RedisSerializer<String> stringSerializer = new StringRedisSerializer();

super.setKeySerializer(stringSerializer);

super.setHashKeySerializer(stringSerializer);

super.setHashValueSerializer(stringSerializer);

Jackson2JsonRedisSerializer<?> jsonRedisSerializer = new Jackson2JsonRedisSerializer<>(valueType);

jsonRedisSerializer.setObjectMapper(objectMapper);

super.setValueSerializer(jsonRedisSerializer);

super.setConnectionFactory(connectionFactory);

super.afterPropertiesSet();

}

}

配置 Redis

package cn.netkiller.wallet.config;

import org.springframework.context.annotation.Bean;

import org.springframework.context.annotation.Configuration;

import org.springframework.data.redis.connection.RedisConnectionFactory;

import org.springframework.data.redis.core.StringRedisTemplate;

import org.springframework.data.redis.listener.ChannelTopic;

import org.springframework.data.redis.listener.RedisMessageListenerContainer;

import org.springframework.data.redis.listener.adapter.MessageListenerAdapter;

import com.fasterxml.jackson.databind.ObjectMapper;

import cn.netkiller.wallet.redis.JsonRedisTemplate;

import cn.netkiller.wallet.redis.RedisMessageSubscriber;

@Configuration

public class RedisConfig {

public RedisConfig() {

}

@Bean

public StringRedisTemplate stringRedisTemplate(RedisConnectionFactory connectionFactory) {

StringRedisTemplate redisTemplate = new StringRedisTemplate();

redisTemplate.setConnectionFactory(connectionFactory);

return redisTemplate;

}

@Bean

public MessageListenerAdapter messageListener() {

return new MessageListenerAdapter(new RedisMessageSubscriber());

}

@Bean

public ChannelTopic topic() {

return new ChannelTopic("demo");

}

@Bean

public RedisMessageListenerContainer redisContainer(RedisConnectionFactory connectionFactory, MessageListenerAdapter messageListener) {

RedisMessageListenerContainer container = new RedisMessageListenerContainer();

container.setConnectionFactory(connectionFactory);

container.addMessageListener(messageListener(), topic());

container.addMessageListener(messageListener(), new ChannelTopic("test"));

return container;

}

@Bean

public ObjectMapper objectMapper() {

return new ObjectMapper();

}

@Bean

public JsonRedisTemplate jsonRedisTemplate(RedisConnectionFactory connectionFactory, ObjectMapper objectMapper) {

return new JsonRedisTemplate(connectionFactory, objectMapper, Object.class);

}

}

测试

package cn.netkiller.wallet.restful;

import java.io.IOException;

import java.util.UUID;

import org.slf4j.Logger;

import org.slf4j.LoggerFactory;

import org.springframework.beans.factory.annotation.Autowired;

import org.springframework.data.redis.core.StringRedisTemplate;

import org.springframework.data.redis.listener.ChannelTopic;

import org.springframework.web.bind.annotation.GetMapping;

import org.springframework.web.bind.annotation.RequestParam;

import org.springframework.web.bind.annotation.RestController;

import cn.netkiller.wallet.pojo.RestfulResponse;

import cn.netkiller.wallet.redis.JsonRedisTemplate;

import cn.netkiller.wallet.redis.RedisMessagePublisher;

@RestController

public class TestRestController {

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

@Autowired

private StringRedisTemplate stringRedisTemplate;

@Autowired

private JsonRedisTemplate jsonRedisTemplate;

public TestRestController() {

}

@GetMapping("/version")

public String version() throws IOException {

Web3ClientVersion web3ClientVersion = web3j.web3ClientVersion().send();

String clientVersion = web3ClientVersion.getWeb3ClientVersion();

logger.info(clientVersion);

return clientVersion;

}

@GetMapping("/pub/demo")

public String pub() {

RedisMessagePublisher publisher = new RedisMessagePublisher(stringRedisTemplate, new ChannelTopic("demo"));

String message = "Message " + UUID.randomUUID();

publisher.publish(message);

return message;

}

@GetMapping("/pub/test")

public String pub(@RequestParam String message) {

RedisMessagePublisher publisher = new RedisMessagePublisher(stringRedisTemplate, new ChannelTopic("test"));

publisher.publish(message);

return message;

}

@GetMapping("/pub/json")

public RestfulResponse pubJson() {

RestfulResponse restfulResponse = new RestfulResponse(true, 0, null, null);

jsonRedisTemplate.opsForValue().set("test", restfulResponse);

jsonRedisTemplate.convertAndSend("test", restfulResponse);

return restfulResponse;

}

}

3. Spring Data Redis - Repository Examples

3.1. @EnableRedisRepositories 启动 Redis 仓库

package api.config;

import org.springframework.context.annotation.Configuration;

import org.springframework.data.redis.repository.configuration.EnableRedisRepositories;

@Configuration

@EnableRedisRepositories

public class CachingConfigurer {

}

3.2. 定义 Domain 类

package api.domain;

import java.util.List;

import org.springframework.data.annotation.Id;

import org.springframework.data.annotation.Reference;

import org.springframework.data.redis.core.RedisHash;

import org.springframework.data.redis.core.index.Indexed;

@RedisHash("persons")

public class Person {

public enum Gender {

FEMALE, MALE

}

@Id

private String id;

@Indexed

private String firstname;

@Indexed

private String lastname;

private Gender gender;

private Address address;

@Reference

private List<Person> children;

public Person() {

// TODO Auto-generated constructor stub

}

public String getId() {

return id;

}

public void setId(String id) {

this.id = id;

}

public String getFirstname() {

return firstname;

}

public void setFirstname(String firstname) {

this.firstname = firstname;

}

public String getLastname() {

return lastname;

}

public void setLastname(String lastname) {

this.lastname = lastname;

}

public Gender getGender() {

return gender;

}

public void setGender(Gender gender) {

this.gender = gender;

}

public Address getAddress() {

return address;

}

public void setAddress(Address address) {

this.address = address;

}

public List<Person> getChildren() {

return children;

}

public void setChildren(List<Person> children) {

this.children = children;

}

@Override

public String toString() {

return "Person [id=" + id + ", firstname=" + firstname + ", lastname=" + lastname + ", gender=" + gender + ", address=" + address + ", children=" + children + "]";

}

}

package api.domain;

import org.springframework.data.geo.Point;

import org.springframework.data.redis.core.index.GeoIndexed;

import org.springframework.data.redis.core.index.Indexed;

public class Address {

private @Indexed String city;

private String country;

private @GeoIndexed Point location;

public Address(String city, String country, Point location) {

this.city = city;

this.country = country;

this.location = location;

}

public String getCity() {

return city;

}

public void setCity(String city) {

this.city = city;

}

public String getCountry() {

return country;

}

public void setCountry(String country) {

this.country = country;

}

public Point getLocation() {

return location;

}

public void setLocation(Point location) {

this.location = location;

}

}

3.3. Repository 接口

package api.repository;

import java.util.List;

import org.springframework.data.domain.Page;

import org.springframework.data.domain.Pageable;

import org.springframework.data.geo.Circle;

import org.springframework.data.repository.CrudRepository;

import api.domain.Person;

public interface PersonRepository extends CrudRepository<Person, String> {

List<Person> findByLastname(String lastname);

Page<Person> findPersonByLastname(String lastname, Pageable page);

List<Person> findByFirstnameAndLastname(String firstname, String lastname);

List<Person> findByFirstnameOrLastname(String firstname, String lastname);

List<Person> findByAddress_City(String city);

List<Person> findByAddress_LocationWithin(Circle circle);

}

3.4. 测试代码

package api.restful;

import java.util.Arrays;

import org.slf4j.Logger;

import org.slf4j.LoggerFactory;

import org.springframework.beans.factory.annotation.Autowired;

import org.springframework.data.geo.Point;

import org.springframework.web.bind.annotation.GetMapping;

import org.springframework.web.bind.annotation.RequestHeader;

import org.springframework.web.bind.annotation.RequestMapping;

import org.springframework.web.bind.annotation.RestController;

import api.domain.Person;

import api.domain.Address;

import api.repository.PersonRepository;

@RestController

@RequestMapping("/test")

public class TestRestController {

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

@Autowired

private PersonRepository personRepository;

public TestRestController() {

}

@GetMapping("/redis")

public Person redis() {

Person children = new Person();

children.setFirstname("Lisa");

children.setLastname("Chen");

children.setGender(Person.Gender.FEMALE);

Person person = new Person();

person.setFirstname("Neo");

person.setLastname("Chen");

person.setGender(Person.Gender.MALE);

// List<Person> childrens = new ArrayList<Person>();

person.setChildren(Arrays.asList(children));

Point point = new Point(Double.valueOf("28.352734"), Double.valueOf("32.807382"));

Address address = new Address("Shenzhen", "China", point);

person.setAddress(address);

personRepository.save(person);

return person;

}

}

第 51 章 Spring Data with MongoDB

https://docs.spring.io/spring-data/mongodb/docs/current/reference/html/

1. Example Spring Data MongoDB

1.1. pom.xml

注意Spring4 与 1.9.1.RELEASE有兼容性问题，日志提示 Error creating bean with name 'mongoTemplate' defined in ServletContext resource

<dependency>

<groupId>org.springframework.data</groupId>

<artifactId>spring-data-mongodb</artifactId>

<version>1.8.1.RELEASE</version>

</dependency>

1.2. springframework-servlet.xml

<mongo:db-factory id="mongoDbFactory" host="${mongo.host}" port="${mongo.port}" dbname="${mongo.database}" />

<!-- username="${mongo.username}" password="${mongo.password}" -->

<bean id="mongoTemplate" class="org.springframework.data.mongodb.core.MongoTemplate">

<constructor-arg name="mongoDbFactory" ref="mongoDbFactory"/>

</bean>

<mongo:mapping-converter id="converter" db-factory-ref="mongoDbFactory"/>

<bean id="gridFsTemplate" class="org.springframework.data.mongodb.gridfs.GridFsTemplate">

<constructor-arg ref="mongoDbFactory"/>

<constructor-arg ref="converter"/>

</bean>

例 51.1. Spring Data MongoDB - springframework-servlet.xml

<?xml version="1.0" encoding="UTF-8"?>

<beans xmlns="http://www.springframework.org/schema/beans"

xmlns:mvc="http://www.springframework.org/schema/mvc"

xmlns:context="http://www.springframework.org/schema/context"

xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"

xmlns:p="http://www.springframework.org/schema/p"

xmlns:mongo="http://www.springframework.org/schema/data/mongo"

xmlns:tx="http://www.springframework.org/schema/tx" xsi:schemaLocation="

http://www.springframework.org/schema/beans

http://www.springframework.org/schema/beans/spring-beans.xsd

http://www.springframework.org/schema/mvc

http://www.springframework.org/schema/mvc/spring-mvc.xsd

http://www.springframework.org/schema/context

http://www.springframework.org/schema/context/spring-context.xsd

http://www.springframework.org/schema/data/mongo

http://www.springframework.org/schema/data/mongo/spring-mongo-1.5.xsd

">

<mvc:resources location="/images/" mapping="/images/**" />

<mvc:resources location="/css/" mapping="/css/**" />

<mvc:resources location="/js/" mapping="/js/**" />

<mvc:resources location="/zt/" mapping="/zt/**" />

<mvc:resources location="/sm/" mapping="/sm/**" />

<mvc:resources location="/module/" mapping="/module/**" />

<context:component-scan base-package="cn.netkiller.controller" />

<!-- <context:property-placeholder location="classpath:resources/development.properties" /> -->

<mvc:annotation-driven />

<bean id="viewResolver" class="org.springframework.web.servlet.view.UrlBasedViewResolver">

<property name="viewClass" value="org.springframework.web.servlet.view.JstlView" />

<property name="prefix" value="/WEB-INF/jsp/" />

<property name="suffix" value=".jsp" />

</bean>

<bean id="configuracion" class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer">

<property name="location" value="classpath:resources/development.properties" />

</bean>

<!-- MongoDB Connection Factory -->

<mongo:db-factory id="mongoDbFactory" host="${mongo.host}" port="${mongo.port}" dbname="${mongo.database}" />

<!-- username="${mongo.username}" password="${mongo.password}" -->

<!-- MongoDB template definition -->

<bean id="mongoTemplate" class="org.springframework.data.mongodb.core.MongoTemplate">

<constructor-arg name="mongoDbFactory" ref="mongoDbFactory"/>

</bean>

<!-- MongoDB GridFS template definition -->

<mongo:mapping-converter id="converter" db-factory-ref="mongoDbFactory"/>

<bean id="gridFsTemplate" class="org.springframework.data.mongodb.gridfs.GridFsTemplate">

<constructor-arg ref="mongoDbFactory"/>

<constructor-arg ref="converter"/>

</bean>

<!-- Redis Connection Factory -->

<bean id="jedisConnFactory" class="org.springframework.data.redis.connection.jedis.JedisConnectionFactory" p:host-name="192.168.2.1" p:port="6379" p:use-pool="true" />

<!-- redis template definition -->

<bean id="redisTemplate" class="org.springframework.data.redis.core.RedisTemplate" p:connection-factory-ref="jedisConnFactory" />

</beans>

development.properties 配置内容

mongo.host=192.168.4.1

mongo.port=27017

mongo.username=test

mongo.password=passw0rd

mongo.database=website

1.3. POJO

package cn.netkiller.pojo;

import org.springframework.data.annotation.Id;

import org.springframework.data.mongodb.core.mapping.Document;

@Document(collection = "tracker")

public class Tracker {

@Id

private String id;

private String name;

private String unique;

private String hostname;

private String referrer;

private String href;

public Tracker() {

// TODO Auto-generated constructor stub

}

public String getName() {

return name;

}

public void setName(String name) {

this.name = name;

}

public String getUnique() {

return unique;

}

public void setUnique(String unique) {

this.unique = unique;

}

public String getHostname() {

return hostname;

}

public void setHostname(String hostname) {

this.hostname = hostname;

}

public String getReferrer() {

return referrer;

}

public void setReferrer(String referrer) {

this.referrer = referrer;

}

public String getHref() {

return href;

}

public void setHref(String href) {

this.href = href;

}

@Override

public String toString() {

return "Tracker [id=" + id + ", name=" + name + ", unique=" + unique + ", hostname=" + hostname + ", referrer=" + referrer + ", href=" + href + "]";

}

}

1.4. Controller

package cn.netkiller.controller;

import cn.netkiller.pojo.Tracker;

import org.springframework.beans.factory.annotation.Autowired;

import org.springframework.data.mongodb.core.MongoTemplate;

import org.springframework.stereotype.*;

import org.springframework.web.bind.annotation.*;

@Controller

public class TrackerController {

@Autowired

private MongoTemplate mongoTemplate;

public TrackerController() {

}

@RequestMapping("/tracker/test")

@ResponseBody

String hello() {

return "Hello World!";

}

@RequestMapping("/tracker")

@ResponseBody

String execute() {

Tracker tracker = new Tracker();

tracker.setName("test");

tracker.setUnique("111223456");

tracker.setHostname("www.example.com");

tracker.setHref("http://example.com/test.html");

tracker.setReferrer("http://example.com/");

this.mongoTemplate.insert(tracker);

return tracker.toString();

}

}

1.5. 查看测试结果

> db.tracker.find();

{ "_id" : ObjectId("5757c0b92c526a6bda5eea3a"), "_class" : "cn.netkiller.repositories.Tracker", "name" : "test", "unique" : "111223456", "hostname" : "www.example.com", "referrer" : "http://example.com/", "href" : "http://example.com/test.html" }

1.6. 条件查询

@RequestMapping("/read/name/{name}")

public ArrayList<Tracker> sort(@PathVariable String name) {

Query query = new Query(Criteria.where("name").is(name));

ArrayList<Tracker> trackers =  (ArrayList<Tracker>) mongoTemplate.find(query, Tracker.class);

return trackers;

}

2. MongoDB 多数据源

2.1. Maven

<dependency>

<groupId>org.springframework.boot</groupId>

<artifactId>spring-boot-starter-data-mongodb</artifactId>

</dependency>

2.2. Application 禁止自动配置 MongoDB

exclude = { MongoAutoConfiguration.class, MongoDataAutoConfiguration.class }

package cn.netkiller;

import org.springframework.boot.SpringApplication;

import org.springframework.boot.autoconfigure.SpringBootApplication;

import org.springframework.boot.autoconfigure.data.mongo.MongoDataAutoConfiguration;

import org.springframework.boot.autoconfigure.mongo.MongoAutoConfiguration;

import org.springframework.boot.context.ApplicationPidFileWriter;

@SpringBootApplication(exclude = { MongoAutoConfiguration.class, MongoDataAutoConfiguration.class })

public class Application {

public static void main(String[] args) {

System.out.println("Starting...");

SpringApplication springApplication = new SpringApplication(Application.class);

springApplication.addListeners(new ApplicationPidFileWriter());

springApplication.run(args);

}

}

2.3. application.properties 新增配置项

mongodb.primary.uri=mongodb://netkiller:chen@192.168.30.10:27017/news

mongodb.secondary.uri=mongodb://netkiller:chen@192.168.30.5:27017/member

2.4. MongoDB 配置类

package cn.netkiller.config;

import org.springframework.data.mongodb.MongoDatabaseFactory;

import org.springframework.data.mongodb.core.SimpleMongoClientDatabaseFactory;

import com.mongodb.ConnectionString;

public abstract class AbstractMongoConfigure {

public MongoDatabaseFactory mongoDatabaseFactory(String uri) {

ConnectionString connectionString = new ConnectionString(uri);

return new SimpleMongoClientDatabaseFactory(connectionString);

}

}

配置多数据源

package cn.netkiller.config;

import org.springframework.beans.factory.annotation.Qualifier;

import org.springframework.boot.autoconfigure.mongo.MongoProperties;

import org.springframework.boot.context.properties.ConfigurationProperties;

import org.springframework.context.annotation.Bean;

import org.springframework.context.annotation.Configuration;

import org.springframework.context.annotation.Primary;

import org.springframework.data.mongodb.core.MongoTemplate;

import org.springframework.data.mongodb.repository.config.EnableMongoRepositories;

@Configuration

@ConfigurationProperties(prefix = "mongodb")

@EnableMongoRepositories(basePackages = { "cn.netkiller.repository" }, mongoTemplateRef = MultipleMongoConfigure.primaryMongoTemplate)

public class MultipleMongoConfigure extends AbstractMongoConfigure {

protected static final String primaryMongoTemplate = "primaryMongoTemplate";

protected static final String secondaryMongoTemplate = "secondaryMongoTemplate";

private MongoProperties primary = new MongoProperties();

private MongoProperties secondary = new MongoProperties();

public MongoProperties getPrimary() {

return primary;

}

public void setPrimary(MongoProperties primary) {

this.primary = primary;

}

public MongoProperties getSecondary() {

return secondary;

}

public void setSecondary(MongoProperties secondary) {

this.secondary = secondary;

}

public MultipleMongoConfigure() {

}

@Primary

@Bean(name = MultipleMongoConfigure.primaryMongoTemplate)

@Qualifier(value = MultipleMongoConfigure.primaryMongoTemplate)

public MongoTemplate primaryMongoTemplate() throws Exception {

String uri = this.getPrimary().getUri();

return new MongoTemplate(mongoDatabaseFactory(uri));

}

@Bean(name = "secondaryMongoTemplate")

@Qualifier("secondaryMongoTemplate")

public MongoTemplate secondaryMongoTemplate() throws Exception {

String uri = this.getSecondary().getUri();

return new MongoTemplate(mongoDatabaseFactory(uri));

}

}

2.5. 创建 Document 关系映射类

package cn.netkiller.domain;

import java.io.Serializable;

import java.util.Date;

import org.springframework.data.mongodb.core.mapping.Document;

import org.springframework.data.mongodb.core.mapping.MongoId;

import com.fasterxml.jackson.annotation.JsonFormat;

@Document

public class User implements Serializable {

private static final long serialVersionUID = -3258839839160856613L;

// private Long id;

@MongoId

private String id;

private String useruame;

private String password;

private String name;

private String sex;

private Integer age;

@JsonFormat(pattern = "yyyy-MM-dd", timezone = "GMT+8")

private Date birthday;

public String getId() {

return id;

}

public void setId(String id) {

this.id = id;

}

public String getUseruame() {

return useruame;

}

public void setUseruame(String useruame) {

this.useruame = useruame;

}

public String getPassword() {

return password;

}

public void setPassword(String password) {

this.password = password;

}

public String getName() {

return name;

}

public void setName(String name) {

this.name = name;

}

public String getSex() {

return sex;

}

public void setSex(String sex) {

this.sex = sex;

}

public Integer getAge() {

return age;

}

public void setAge(Integer age) {

this.age = age;

}

public Date getBirthday() {

return birthday;

}

public void setBirthday(Date birthday) {

this.birthday = birthday;

}

}

2.6. 测试控制器

package cn.netkiller.controller;

import org.springframework.beans.factory.annotation.Autowired;

import org.springframework.beans.factory.annotation.Qualifier;

import org.springframework.data.mongodb.core.MongoTemplate;

import org.springframework.web.bind.annotation.GetMapping;

import org.springframework.web.bind.annotation.RestController;

import cn.netkiller.domain.User;

import cn.netkiller.service.UserService;

@RestController

public class TestMongoController {

@Autowired

@Qualifier(value = "primaryMongoTemplate")

private MongoTemplate primaryMongoTemplate;

@Autowired

@Qualifier(value = "secondaryMongoTemplate")

private MongoTemplate secondaryMongoTemplate;

public TestMongoController() {

// TODO Auto-generated constructor stub

}

@GetMapping("/mongo/primary/save")

public String primarysave() {

User user = new User();

user.setUseruame("netkiller");

user.setPassword("123456");

primaryMongoTemplate.save(user);

return "Success\r\n";

}

@GetMapping("/mongo/secondary/save")

public String secondaryMongoTemplate() {

User user = new User();

user.setUseruame("netkiller");

user.setPassword("123456");

secondaryMongoTemplate.save(user);

return "Success\r\n";

}

}

2.7. 测试

启动 Springboot 可以看到下面👇日志，两个MongoDB都链接成功。

2021-10-14 19:29:50.037  INFO 93698 --- [           main] org.mongodb.driver.cluster               : Cluster created with settings {hosts=[192.168.30.10:27017], mode=SINGLE, requiredClusterType=UNKNOWN, serverSelectionTimeout='30000 ms'}

2021-10-14 19:29:50.156  INFO 93698 --- [168.30.10:27017] org.mongodb.driver.connection            : Opened connection [connectionId{localValue:1, serverValue:126}] to 192.168.30.10:27017

2021-10-14 19:29:50.157  INFO 93698 --- [168.30.10:27017] org.mongodb.driver.cluster               : Monitor thread successfully connected to server with description ServerDescription{address=192.168.30.10:27017, type=STANDALONE, state=CONNECTED, ok=true, minWireVersion=0, maxWireVersion=13, maxDocumentSize=16777216, logicalSessionTimeoutMinutes=30, roundTripTimeNanos=31466638}

2021-10-14 19:29:50.157  INFO 93698 --- [168.30.10:27017] org.mongodb.driver.connection            : Opened connection [connectionId{localValue:2, serverValue:127}] to 192.168.30.10:27017

2021-10-14 19:29:50.266  INFO 93698 --- [           main] org.mongodb.driver.cluster               : Cluster created with settings {hosts=[192.168.30.5:27017], mode=SINGLE, requiredClusterType=UNKNOWN, serverSelectionTimeout='30000 ms'}

2021-10-14 19:29:50.272  INFO 93698 --- [.168.30.5:27017] org.mongodb.driver.connection            : Opened connection [connectionId{localValue:3, serverValue:969}] to 192.168.30.5:27017

2021-10-14 19:29:50.272  INFO 93698 --- [.168.30.5:27017] org.mongodb.driver.connection            : Opened connection [connectionId{localValue:4, serverValue:968}] to 192.168.30.5:27017

2021-10-14 19:29:50.272  INFO 93698 --- [.168.30.5:27017] org.mongodb.driver.cluster               : Monitor thread successfully connected to server with description ServerDescription{address=192.168.30.5:27017, type=STANDALONE, state=CONNECTED, ok=true, minWireVersion=0, maxWireVersion=13, maxDocumentSize=16777216, logicalSessionTimeoutMinutes=30, roundTripTimeNanos=2345376}

neo@MacBook-Pro-Neo ~ % curl http://localhost:8080/mongo/primary/save

Success

neo@MacBook-Pro-Neo ~ % curl http://localhost:8080/mongo/secondary/save

Success

现在去两个 MongoDB 数据查看输入是否保存成功。

在使用 curl 调用的时候，日志会显示链接两个 MongoDB 的状态。

2021-10-14 19:34:27.795  INFO 93698 --- [  XNIO-1 task-1] org.mongodb.driver.connection            : Opened connection [connectionId{localValue:5, serverValue:970}] to 192.168.30.5:27017

2021-10-14 19:34:31.096  INFO 93698 --- [  XNIO-1 task-1] org.mongodb.driver.connection            : Opened connection [connectionId{localValue:6, serverValue:130}] to 192.168.30.10:27017

3. @Document

复杂的 @Document 数据类型定义

package cn.netkiller.domain;

import java.util.Date;

import java.util.List;

import java.util.Map;

import org.springframework.data.annotation.Id;

import org.springframework.data.mongodb.core.mapping.Document;

@Document

public class MultilevelDirectSellingTradingRebate {

public enum Type {

POINT, CASH, GIFT

}

public enum Rebate {

DIRECT, INDIRECT

}

public enum Status {

New, Rejected, Approved

}

@Id

private String id;

public String name;

public Date beginDate;

public Date endDate;

public double lowAmount;

public double highAmount;

public Type type;

public Status status = Status.New;

public List<Map<String, Map<?, ?>>> product;

@Override

public String toString() {

return "MultilevelDirectSellingTradingRebate [id=" + id + ", name=" + name + ", beginDate=" + beginDate

+ ", endDate=" + endDate + ", lowAmount=" + lowAmount + ", highAmount=" + highAmount + ", type=" + type

+ ", status=" + status + ", product=" + product + "]";

}

}

3.1. 指定表名

默认使用 class 作为表名

@Document

public class Multilevel {

...

...

}

指定特别表名

@Document(collection = "author")

3.2. @Id

@Id

private String id;

3.3. @Version

@Version

private Long version;

3.4. @Field 定义字段名

@Field("url")

private String link;

3.5. @Indexed

https://docs.spring.io/spring-data/mongodb/docs/current/api/org/springframework/data/mongodb/core/index/Indexed.html

索引

普通索引

@Indexed

唯一索引

@Indexed(unique=true)

索引排序方式

@Indexed(name = "first_name_index", direction = IndexDirection.DESCENDING)

稀疏索引

稀疏索引允许唯一索引存在多个 null 值

@Indexed(unique = true, sparse = true)

private String uuid;

@Indexed(unique = true, sparse = true)

private String transactionId = null;

索引过期时间设置

@Indexed(name = "expire_after_seconds_index", expireAfterSeconds = 10)

private LocalDateTime updateDate;

3.6. @CompoundIndex 复合索引

普通复合索引

@Document

@CompoundIndexes({

@CompoundIndex(name = "email_age", def = "{'email.id' : 1, 'age': 1}")

})

public class User {

//

}

@Document

@CompoundIndexes({

@CompoundIndex(def = "{'firstName':1, 'salary':-1}", name = "compound_index_1"),

@CompoundIndex(def = "{'secondName':1, 'profession':1}", name = "compound_index_2")

})

public class Person {

@Id private String id;

private String firstName;

private String secondName;

private LocalDateTime dateOfBirth;

private Address address;

private String profession;

private int salary;

// constructor

// getters and setters

}

唯一复合索引

唯一复合索引：楼层和房号不能相同，不然就是同一个房间了

@CompoundIndexes({

@CompoundIndex(name = "floor_num", def = "{'floor' : 1, 'num': 1}",unique=true)

})

不允许同名

@CompoundIndexes({ @CompoundIndex(name = "username", def = "{'firstname' : 1, 'lastname': 1}", unique = true) })

3.7. @TextIndexed

@Document(language = "spanish")

class SomeEntity {

@TextIndexed String foo;

@Language String lang;

Nested nested;

}

class Nested {

@TextIndexed(weight=5) String bar;

String roo;

}

3.8. @GeoSpatialIndex 地理位置索引

点数据索引

@GeoSpatialIndexed

private GeoJsonPoint location; // GPS 定位信息

2D 数据索引

@GeoSpatialIndexed(type = GeoSpatialIndexType.GEO_2DSPHERE)

3.9. @Transient 丢弃数据，不存到 mongodb

public class User {

@Transient

private Integer age;

// standard getter and setter

}

3.10. @DBRef 做外外键引用

Article 类

package cn.netkiller.api.domain;

import java.util.List;

import org.springframework.data.mongodb.core.mapping.DBRef;

import org.springframework.data.mongodb.core.mapping.Document;

@Document

public class Article {

private String title; // 名称

private String description; // 描述

private String tag; // 类型

@DBRef

private List<Hypermedia> hypermedia; // 图片，视频

public Article() {

// TODO Auto-generated constructor stub

}

public String getTitle() {

return title;

}

public void setTitle(String title) {

this.title = title;

}

public String getDescription() {

return description;

}

public void setDescription(String description) {

this.description = description;

}

public String getTag() {

return tag;

}

public void setTag(String tag) {

this.tag = tag;

}

public List<Hypermedia> getHypermedia() {

return hypermedia;

}

public void setHypermedia(List<Hypermedia> hypermedia) {

this.hypermedia = hypermedia;

}

@Override

public String toString() {

return "Article [title=" + title + ", description=" + description + ", tag=" + tag + ", hypermedia=" + hypermedia + "]";

}

}

Hypermedia 类

package api.domain;

import org.springframework.data.annotation.Id;

import org.springframework.data.mongodb.core.mapping.Document;

@Document

public class Hypermedia {

@Id

private String id;

private String hash;

private String name;

private String size;

public Hypermedia() {

// TODO Auto-generated constructor stub

}

public Hypermedia(String hash, String name, String size) {

this.hash = hash;

this.name = name;

this.size = size;

}

public String getId() {

return id;

}

public void setId(String id) {

this.id = id;

}

public String getHash() {

return hash;

}

public void setHash(String hash) {

this.hash = hash;

}

public String getName() {

return name;

}

public void setName(String name) {

this.name = name;

}

public String getSize() {

return size;

}

public void setSize(String size) {

this.size = size;

}

@Override

public String toString() {

return "Hypermedia [id=" + id + ", hash=" + hash + ", name=" + name + ", size=" + size + "]";

}

}

如果你只查询 Article 表，不会单独查询 Hypermedia，返回结果可以掩藏 Id ，不写 get/set 方法即可。

package cn.netkiller.api.domain;

import org.springframework.data.annotation.Id;

import org.springframework.data.mongodb.core.mapping.Document;

@Document

public class Hypermedia {

@Id

private String id;

private String hash;

private String name;

private String size;

public Hypermedia() {

// TODO Auto-generated constructor stub

}

public Hypermedia(String hash, String name, String size) {

this.hash = hash;

this.name = name;

this.size = size;

}

public String getHash() {

return hash;

}

public void setHash(String hash) {

this.hash = hash;

}

public String getName() {

return name;

}

public void setName(String name) {

this.name = name;

}

public String getSize() {

return size;

}

public void setSize(String size) {

this.size = size;

}

@Override

public String toString() {

return "Hypermedia [hash=" + hash + ", name=" + name + ", size=" + size + "]";

}

}

MongoRepository

package cn.netkiller.api.repository;

import org.springframework.data.mongodb.repository.MongoRepository;

import api.domain.Article;

public interface ArticleRepository extends MongoRepository<Article, String> {

}

package cn.netkiller.api.repository;

import org.springframework.data.mongodb.repository.MongoRepository;

import api.domain.Hypermedia;

public interface HypermediaRepository extends MongoRepository<Hypermedia, String> {

}

RestController

package cn.netkiller.api.restful;

import java.util.ArrayList;

import java.util.List;

import org.springframework.beans.factory.annotation.Autowired;

import org.springframework.web.bind.annotation.GetMapping;

import org.springframework.web.bind.annotation.RequestMapping;

import org.springframework.web.bind.annotation.RestController;

import api.domain.Article;

import api.domain.Hypermedia;

import api.repository.ArticleRepository;

import api.repository.HypermediaRepository;

@RestController

@RequestMapping("/article")

public class ArticleRestController {

@Autowired

private ArticleRepository articleRepository;

@Autowired

private HypermediaRepository hypermediaRepository;

public ArticleRestController() {

// TODO Auto-generated constructor stub

}

@GetMapping("/save")

public Article save() {

Article article = new Article();

article.setTitle("标题");

article.setDescription("摘要");

article.setTag("标签");

Hypermedia hypermedia = new Hypermedia("AAA", "BBB", "CCC");

hypermediaRepository.save(hypermedia);

List<Hypermedia> hypermedias = new ArrayList<Hypermedia>();

hypermedias.add(hypermedia);

article.setHypermedia(hypermedias);

articleRepository.save(article);

System.out.println(article);

return article;

}

}

运行结果

neo@MacBook-Pro ~ % curl -s -H "Accept: application/json" -H "Content-Type: application/json" -H "Authorization: Bearer ${TOKEN}" -X GET ${URL}/article/save | jq

{

"title": "标题",

"description": "摘要",

"tag": "标签",

"hypermedia": [

{

"hash": "AAA",

"name": "BBB",

"size": "CCC"

}

]

}

MongoDB 结果

db.getCollection('article').find({})

/* 1 */

{

"_id" : ObjectId("5bab66f8c92782395817cb05"),

"title" : "标题",

"description" : "摘要",

"tag" : "标签",

"hypermedia" : [

{

"$ref" : "hypermedia",

"$id" : ObjectId("5bab66f8c92782395817cb04")

}

],

"_class" : "cn.netkiller.api.domain.Article"

}

db.getCollection('hypermedia').find({})

/* 1 */

{

"_id" : ObjectId("5bab66b9c927823951f4f5fe"),

"hash" : "AAA",

"name" : "BBB",

"size" : "CCC",

"_class" : "api.domain.Hypermedia"

}

3.11. @DateTimeFormat

@DateTimeFormat( pattern = "yyyy-MM-dd" )

private Date birthday

@DateTimeFormat(iso = DateTimeFormat.ISO.NONE)

private final Calendar datetime;

@DateTimeFormat(pattern="yyyy-MM-dd HH:mm:ss")

private Date date;

@DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME)

private Date createdDate = new Date();

3.12. @NumberFormat

@NumberFormat(style=Style.CURRENCY)

private double money;

3.13. 在 @Document 中使用 Enum 类型

public enum Type {

POINT, CASH, GIFT

}

public enum Rebate {

DIRECT, INDIRECT

}

public enum Status {

New, Rejected, Approved

}

枚举类型的赋值方法

MultilevelDirectSellingTradingRebate multilevelDirectSellingTradingRebate = new MultilevelDirectSellingTradingRebate();

multilevelDirectSellingTradingRebate.name = "TEST";

multilevelDirectSellingTradingRebate.beginDate = new Date();

multilevelDirectSellingTradingRebate.endDate = new Date();

multilevelDirectSellingTradingRebate.lowAmount = 1.5d;

multilevelDirectSellingTradingRebate.highAmount = 100d;

multilevelDirectSellingTradingRebate.type = Type.CASH;

3.14. 在 @Document 中定义数据结构 List/Map

public List<Map<String, Map<?, ?>>> product;

下面是数据集结构的赋值例子

Map<Enum<Rebate>, Double> rebate = new HashMap<Enum<Rebate>, Double>();

rebate.put(Rebate.DIRECT, 10.05d);

rebate.put(Rebate.INDIRECT, 6.05d);

Map<String, Map<?, ?>> prod1 = new HashMap<String, Map<?, ?>>();

prod1.put("USDRMB", rebate);

List<Map<String, Map<?, ?>>> products = new ArrayList<Map<String, Map<?, ?>>>();

products.add(prod1);

multilevelDirectSellingTradingRebate.product = products;

3.15. GeoJson 数据类型

@GeoSpatialIndexed

private GeoJsonPoint location; // GPS 地址位置

location = new GeoJsonPoint(Double.valueOf(longitude), Double.valueOf(latitude));

4. MongoRepository

4.1. 扫描仓库接口

默认不需要设置，除非你的包不在当前包下，或者命令不是 repository。

@EnableMongoRepositories(basePackages = "cn.netkiller.repository")

4.2. findAll()

@RequestMapping(value = "read", method = RequestMethod.GET, produces = { "application/xml", "application/json" })

@ResponseStatus(HttpStatus.OK)

public List<Withdraw> read() {

return repository.findAll();

}

4.3. deleteAll()

repository.deleteAll();

4.4. save()

repository.save(new City("Shenzhen", "China"));

4.5. count()

@RequestMapping("count")

public long count() {

return repository.count();

}

4.6. exists() 判断是否存在

boolean isExists = userRepository.exists(user.getId());

4.7. existsById()

memberRepository.existsById(id);

4.8. findByXXXX

List<User> findByName(String name);

List<User> users = userRepository.findByName("Eric");

4.9. findAll with OrderBy

order by boolean 布尔型数据排序

因为 boolean 数据 true = 1, false = 0 所以 ASC false 会排列在前面。所有很多时候而我们需要 DESC 排序

List<ShippingAddress> shippingAddress = shippingAddressRepository.findAllByMemberIdOrderByDefaultsDesc(memberId);

4.10. findAll with Sort

List<User> users = userRepository.findAll(new Sort(Sort.Direction.ASC, "name"));

4.11. FindAll with Pageable

Pageable pageable = PageRequest.of(0, 1);

Page<User> page = userRepository.findAll(pageable);

List<User> users = pages.getContent();

PageRequest - springboot 1.x 旧版本

Page<User> findByLastname(String lastname, Pageable pageable);

@RequestMapping(value = "read/{size}/{page}", method = RequestMethod.GET, produces = { "application/xml", "application/json" })

@ResponseStatus(HttpStatus.OK)

public List<Withdraw> readPage(@PathVariable int size, @PathVariable int page){

PageRequest pageRequest = new PageRequest(page-1,size);

return repository.findAll(pageRequest).getContent();

}

URL翻页参数，每次返回10条记录

第一页 http://localhost:8080/v1/withdraw/read/10/1.json

第二页 http://localhost:8080/v1/withdraw/read/10/2.json

...

第五页 http://localhost:8080/v1/withdraw/read/10/5.json

4.12. StartingWith 和 EndingWith

List<User> findByNameStartingWith(String regexp);

List<User> findByNameEndingWith(String regexp);

List<User> users = userRepository.findByNameStartingWith("N");

List<User> users = userRepository.findByNameEndingWith("o");

4.13. Between

数值范围

List<User> findByAgeBetween(int ageGT, int ageLT);

List<User> users = userRepository.findByAgeBetween(20, 50);

日期范围，取值 e.g. 2018-07-04 00:00:00 and 2018-07-04 23:59:59

List<Member> findByCreatedDateBetween(DateTime start, DateTime end);

List<Member> findByCreatedDate(@Temporal(TemporalType.DATE) Date date);

4.14. Before / After

List<Assets> findAllByUpdateDateBefore(Date yesterday);

List<Assets> findAllByUpdateDateBeforeAndStatus(Date yesterday, String status);

List<Assets> findAllByUpdateDateAfter(Date yesterday);

4.15. @Query

public interface PersonRepository extends MongoRepository<Person, String> {

@Query("{ 'name' : ?0 }")

List<Person> findWithQuery(String userId);

}

@Query(value = "{'statusHistories':{$elemMatch:{'status':{$in:['PROCESSABLE']}}},'created' : { '$gt' : { '$date' : ':?0' } , '$lt' : { '$date' : ':?1'}}}", count = true)

Long countMe(@Param("dateFrom") Date datefrom, @Param("dateTo") Date dateTo);

5. mongoTemplate

导入与模板相关的包

import org.springframework.data.mongodb.core.MongoTemplate;

import org.springframework.data.mongodb.core.query.Criteria;

import org.springframework.data.mongodb.core.query.Query;

import org.springframework.data.mongodb.core.query.Update;

注入 MongoTemplate 对象

@Autowired

private MongoTemplate mongoTemplate;

5.1. Save 保存

User user = new User();

user.setName("Netkiller");

mongoTemplate.save(user, "user");

更新数据

user = mongoTemplate.findOne(Query.query(Criteria.where("name").is("Jam")), User.class);

user.setName("Neo");

mongoTemplate.save(user, "user");

5.2. Insert

User user = new User();

user.setName("Neo");

mongoTemplate.insert(user, "user");

BSONObject personBsonObj = BasicDBObjectBuilder.start()

.add("name","Neo Chen")

.add("age",27)

.add("address",null).get();

mongoTemplate.insert(personBsonObj,"personCollection");

document in the db:

db.personCollection.findOne().pretty();

{"age":21,"name":"John Doe";"address":null}*

5.3. updateFirst 修改符合条件第一条记录

updateFirst 修改符合条件第一条记录

Query query = new Query();

query.addCriteria(Criteria.where("name").is("Neo"));

Update update = new Update();

update.set("name", "Netkiller");

mongoTemplate.updateFirst(query, update, User.class);

5.4. updateMulti 修改符合条件的所有

更新所有数据

Query query = new Query();

query.addCriteria(Criteria.where("name").is("Neo"));

Update update = new Update();

update.set("name", "Jerry");

mongoTemplate.updateMulti(query, update, User.class);

5.5. 查找并保存

Query query = new Query();

query.addCriteria(Criteria.where("name").is("Luck"));

Update update = new Update();

update.set("name", "Lisa");

User user = mongoTemplate.findAndModify(query, update, User.class);

5.6. upsert - 修改符合条件时如果不存在则添加

Query query = new Query();

query.addCriteria(Criteria.where("name").is("Green"));

Update update = new Update();

update.set("name", "Tom");

mongoTemplate.upsert(query, update, User.class);

mongoTemplate.upsert(new Query(Criteria.where("age").is("18")), new Update().set("name", "neo"), collectionName);

5.7. 删除

User user = new User();

user.setId("5bbf091efd9557069c4a25c5")

mongoTemplate.remove(user, "user");

5.8. 查找一条数据

public Person findOneByName(String name) {

Query query = new Query();

query.addCriteria(Criteria.where("name").is(name));

return mongoTemplate.findOne(query, Person.class);

}

5.9. 查找所有数据

public List<Person> findByName(String name) {

Query query = new Query();

query.addCriteria(Criteria.where("name").is(name));

return mongoTemplate.find(query, Person.class);

}

5.10. Query

翻页

public List<Person> getAllPersonPaginated(int pageNumber, int pageSize) {

Query query = new Query();

query.skip(pageNumber * pageSize);

query.limit(pageSize);

return mongoTemplate.find(query, Person.class);

}

between

实现一个区间条件 new Criteria("createdDate").gte(beginDate).lte(endDate)

public boolean AccountDeposit(Date beginDate, Date endDate) {

MatchOperation matchOperation = match(new Criteria("createdDate").gte(beginDate).lte(endDate));

GroupOperation groupOperation = group("loginname").sum("amount").as("amount");

SortOperation sortOperation = sort(new Sort(Direction.ASC, "loginname"));

Aggregation aggregation = newAggregation(matchOperation, groupOperation, sortOperation);

AggregationResults<AccountSettlementDetails> results = mongoTemplate.aggregate(aggregation, AccountSettlementDetails.class, AccountSettlementDetails.class);

if (results.getMappedResults() != null) {

log.info(results.getRawResults().get("result").toString());

for (AccountSettlementDetails settlementDetails : results.getMappedResults()) {

log.info("{}", settlementDetails.toString());

}

}

return true;

}

5.11. Criteria

is

Query query = new Query();

query.addCriteria(Criteria.where("name").is("Neo"));

List<User> users = mongoTemplate.find(query, User.class);

Regex 正则表达式搜索

查询以N开头的名字

Query query = new Query();

query.addCriteria(Criteria.where("name").regex("^N"));

List<User> users = mongoTemplate.find(query,User.class);

查询以o结尾的名字

Query query = new Query();

query.addCriteria(Criteria.where("name").regex("o$"));

List<User> users = mongoTemplate.find(query, User.class);

lt 和 gt

查询年龄小于 < 30 并 > 20 的用户

Query query = new Query();

query.addCriteria(Criteria.where("age").lt(30).gt(20));

List<User> users = mongoTemplate.find(query,User.class);

查找日期范围

Date start = DateUtil.convertStringToDateTime("2014-02-10 20:38:44");

Date end = DateUtil.convertStringToDateTime("2014-02-10 20:38:50");

Query query = new Query();

Criteria criteria = Criteria.where("delflag").is(false);

criteria.and("modifyDate").gte(start).lte(end);

query.addCriteria(criteria);

query.limit(10);

exists()

Query query = new Query();

query.addCriteria(

new Criteria().andOperator(

Criteria.where("field1").exists(true),

Criteria.where("field1").ne(false)

)

);

List<Foo> result = mongoTemplate.find(query, Foo.class);

System.out.println("query - " + query.toString());

for (Foo foo : result) {

System.out.println("result - " + foo);

}

包含

public List<Person> findByFavoriteBooks(String favoriteBook) {

Query query = new Query();

query.addCriteria(Criteria.where("favoriteBooks").in(favoriteBook));

return mongoTemplate.find(query, Person.class);

}

5.12. Update

set

Update update = new Update();

update.set("name", "Netkiller");

追加数据

Query query = Query.query(Criteria.where("id").is("5bbf091efd9557069c4a25c5"));

Update update = new Update().push("author", new Author("neo", "chen"));

mongoTemplate.updateFirst(query, update, Article.class);

更新数据

Query query = Query.query(Criteria.where("classId").is("1").and("Students.studentId").is("1"));

Update update = Update.update("Students.$.name", "lisa");

mongoTemplate.upsert(query, update, "class");

删除数据

Query query = Query.query(Criteria.where("classId").is("1").and("Students.studentId").is("3"));

Update update = new Update();

update.unset("Students.$");

mongoTemplate.updateFirst(query, update, "class");

inc

public void updateMultiplePersonAge() {

Query query = new Query();

Update update = new Update().inc("age", 1);

mongoTemplate.findAndModify(query, update, Person.class);;

}

update.addToSet

Query query = Query.query(Criteria.where("classId").is("1"));

Student student = new Student("1", "lisa", 3, "girl");

Update update = new Update();

update.addToSet("Students", student);

mongoTemplate.upsert(query, update, "class");

5.13. BasicUpdate

BasicUpdate 是底层更新可操作，需要手动实现$set等语句

BasicDBObject basicDBObject = new BasicDBObject();

basicDBObject.put("$set", new BasicDBObject("date","2018-09-09"));

Update update = new BasicUpdate(basicDBObject);

mongoTemplate.updateFirst(new Query(Criteria.where("nickname").is("netkiller")), update,collectionName);

5.14. Sort

按照年龄排序

Query query = new Query();

query.with(new Sort(Sort.Direction.ASC, "age"));

List<User> users = mongoTemplate.find(query,User.class);

5.15. Query + PageRequest

final Pageable pageableRequest = new PageRequest(0, 2);

Query query = new Query();

query.with(pageableRequest);

5.16. newAggregation

MultilevelDirectSellingAccountRewardsSettlementDetails multilevelDirectSellingAccountRewardsSettlementDetails = new MultilevelDirectSellingAccountRewardsSettlementDetails();

multilevelDirectSellingAccountRewardsSettlementDetails.setLoginname("111");

multilevelDirectSellingAccountRewardsSettlementDetails.setPhone("111");

multilevelDirectSellingAccountRewardsSettlementDetails.setRecommenderLoginname("111");

multilevelDirectSellingAccountRewardsSettlementDetails.setRecommenderPhone("111");

multilevelDirectSellingAccountRewardsSettlementDetails.setRecommenderName("Neo");

multilevelDirectSellingAccountRewardsSettlementDetails.setRecommenderType("客户");

multilevelDirectSellingAccountRewardsSettlementDetails.setAmount(5.02);

multilevelDirectSellingAccountRewardsSettlementDetails.setCreatedDate(new Date());

multilevelDirectSellingAccountRewardsSettlementDetailsRepository.save(multilevelDirectSellingAccountRewardsSettlementDetails);

Date beginDate = this.getToday("00:00:00");

Date endDate = this.getToday("23:59:59");

log.info(beginDate.toString() + " ~ " + endDate.toString());

GroupOperation groupOperation = group("loginname").sum("amount").as("amount");

MatchOperation matchOperation = match(new Criteria("createdDate").gte(beginDate).lte(endDate));

SortOperation sortOperation = sort(new Sort(Direction.ASC, "loginname"));

Aggregation aggregation = newAggregation(matchOperation, groupOperation, sortOperation);

AggregationResults<MultilevelDirectSellingAccountRewardsSettlementDetails> results = mongoTemplate.aggregate(aggregation, MultilevelDirectSellingAccountRewardsSettlementDetails.class, MultilevelDirectSellingAccountRewardsSettlementDetails.class);

System.out.println(results.getRawResults().get("result").toString());

5.17. 创建索引

mongoOps.indexOps(User.class).ensureIndex(new Index().on("name", Direction.ASC));

5.18. 子对象操作

List 类型

package cn.netkiller.api.domain;

import java.util.List;

import javax.persistence.Id;

import org.springframework.data.mongodb.core.mapping.Document;

@Document

public class Article {

@Id

private String id;

private String title;

private String description;

List<Author> author;

public static class Author {

private String id;

private String firstname;

private String lastname;

public Author(String firstname, String lastname) {

this.firstname = firstname;

this.lastname = lastname;

}

}

}

更新

db.getCollection('foo').update({"author.firstname":"neo"},{"$set":{"author.$.firstname":"netkiller"}})

更新数据

Query query = Query.query(Criteria.where("author.firstname").is("neo"));

Update update = new Update().set("author.$.firstname", "netkiller");

mongoTemplate.updateFirst(query, update, Article.class);

追加数据

Query query = Query.query(Criteria.where("id").is("5bbf091efd9557069c4a25c5"));

Update update = new Update().push("author", new Author("neo", "chen"));

mongoTemplate.updateFirst(query, update, Article.class);

删除数据

Query query = Query.query(Criteria.where("id").is("5bbf091efd9557069c4a25c5"));

Update update = new Update().pull("author", new Author("jerry", "lee"));

mongoTemplate.updateFirst(query, update, Article.class);

6. GeoJson 反序列化

正常情况下是不需要做反序列化操作的。如花你想测试，打印一些信息可以这样做。

package cn.netkiller.api.config;

import java.io.IOException;

import org.springframework.data.mongodb.core.geo.GeoJsonPoint;

import com.fasterxml.jackson.core.JsonParser;

import com.fasterxml.jackson.databind.DeserializationContext;

import com.fasterxml.jackson.databind.JsonDeserializer;

import com.fasterxml.jackson.databind.JsonNode;

public class GeoJsonDeserializer extends JsonDeserializer<GeoJsonPoint> {

@Override

public GeoJsonPoint deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) throws IOException {

final JsonNode tree = jsonParser.getCodec().readTree(jsonParser);

final String type = tree.get("type").asText();

final JsonNode coordsNode = tree.get("coordinates");

System.out.println(tree.toString());

System.out.println(type);

System.out.println(coordsNode.toString());

double x = 0;

double y = 0;

if ("Point".equalsIgnoreCase(type)) {

x = coordsNode.get(0).asDouble();

y = coordsNode.get(1).asDouble();

} else {

System.out.println(String.format("No logic present to deserialize %s ", tree.asText()));

}

final GeoJsonPoint point = new GeoJsonPoint(x, y);

return point;

}

}

使用 @JsonDeserialize 指定反序列化 Class

@Document

public class Address {

@Id

private String id;

// @GeoSpatialIndexed

@JsonDeserialize(using = GeoJsonDeserializer.class)

private GeoJsonPoint location; // GPS 定位信息

}

7. FAQ

7.1. location object expected, location array not in correct format; nested exception is com.mongodb.MongoWriteException: location object expected, location array not in correct format

GeoJsonPoint 可能设置了索引，且有些数据部正确。

第 52 章 Spring Data with Elasticsearch

1. 内嵌 Elasticsearch

内嵌 Elasticsearch 应用，你不需要一个 Elasticsearch 服务器，启动 Spring boot 即可使用 Elasticsearch 服务。

1.1. Maven

需要下面两个依赖

<dependency>

<groupId>org.springframework.boot</groupId>

<artifactId>spring-boot-starter-data-elasticsearch</artifactId>

</dependency>

<!-- com.sun.jna for elasticsearch -->

<dependency>

<groupId>com.sun.jna</groupId>

<artifactId>jna</artifactId>

<version>3.0.9</version>

</dependency>

<?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>

<groupId>com.example</groupId>

<artifactId>api</artifactId>

<version>0.0.1-SNAPSHOT</version>

<packaging>jar</packaging>

<name>api</name>

<description>Demo project for Spring Boot</description>

<parent>

<groupId>org.springframework.boot</groupId>

<artifactId>spring-boot-starter-parent</artifactId>

<version>1.5.6.RELEASE</version>

<relativePath /> <!-- lookup parent from repository -->

</parent>

<properties>

<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>

<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>

<java.version>1.8</java.version>

</properties>

<dependencies>

<dependency>

<groupId>org.springframework.boot</groupId>

<artifactId>spring-boot-starter-data-jpa</artifactId>

</dependency>

<dependency>

<groupId>org.springframework.boot</groupId>

<artifactId>spring-boot-starter-jdbc</artifactId>

</dependency>

<dependency>

<groupId>org.springframework.boot</groupId>

<artifactId>spring-boot-starter-security</artifactId>

</dependency>

<dependency>

<groupId>org.springframework.boot</groupId>

<artifactId>spring-boot-starter-web</artifactId>

</dependency>

<dependency>

<groupId>org.springframework.boot</groupId>

<artifactId>spring-boot-starter-test</artifactId>

</dependency>

<dependency>

<groupId>mysql</groupId>

<artifactId>mysql-connector-java</artifactId>

<scope>runtime</scope>

</dependency>

<dependency>

<groupId>org.springframework.boot</groupId>

<artifactId>spring-boot-starter-test</artifactId>

<scope>test</scope>

</dependency>

<dependency>

<groupId>org.springframework.boot</groupId>

<artifactId>spring-boot-starter-data-redis</artifactId>

</dependency>

<dependency>

<groupId>org.springframework.boot</groupId>

<artifactId>spring-boot-starter-data-elasticsearch</artifactId>

</dependency>

<!-- https://mvnrepository.com/artifact/javax.persistence/persistence-api -->

<dependency>

<groupId>javax.persistence</groupId>

<artifactId>persistence-api</artifactId>

<version>1.0.2</version>

</dependency>

<!-- https://mvnrepository.com/artifact/org.json/json -->

<dependency>

<groupId>org.json</groupId>

<artifactId>json</artifactId>

</dependency>

<!-- com.sun.jna for elasticsearch -->

<dependency>

<groupId>com.sun.jna</groupId>

<artifactId>jna</artifactId>

<version>3.0.9</version>

</dependency>

</dependencies>

<build>

<plugins>

<plugin>

<groupId>org.springframework.boot</groupId>

<artifactId>spring-boot-maven-plugin</artifactId>

</plugin>

<plugin>

<groupId>org.apache.maven.plugins</groupId>

<artifactId>maven-surefire-plugin</artifactId>

<configuration>

<skip>true</skip>

</configuration>

</plugin>

</plugins>

</build>

</project>

1.2. src/main/resources/application.properties

spring.data.elasticsearch.repositories.enabled=true

#spring.data.elasticsearch.cluster-name=elasticsearch

#spring.data.elasticsearch.cluster-nodes=119.29.241.95:9200

spring.data.elasticsearch.local=false

spring.data.elasticsearch.properties.transport.tcp.connect_timeout=60s

spring.data.elasticsearch.properties.host=127.0.0.1

spring.data.elasticsearch.properties.port=9200

spring.data.elasticsearch.properties.path.home=/tmp

1.3. Domain Class

package com.example.api.domain.elasticsearch;

import java.io.Serializable;

import java.util.Date;

import javax.persistence.Id;

import org.springframework.data.annotation.CreatedDate;

import org.springframework.data.elasticsearch.annotations.DateFormat;

import org.springframework.data.elasticsearch.annotations.Document;

import org.springframework.data.elasticsearch.annotations.Field;

import org.springframework.data.elasticsearch.annotations.FieldIndex;

import org.springframework.data.elasticsearch.annotations.FieldType;

import com.fasterxml.jackson.annotation.JsonFormat;

@Document(indexName = "information", type = "article")

public class Article implements Serializable {

/**

*

*/

private static final long serialVersionUID = 8789505663320446079L;

@Id

private int id;

private String title;

private String description;

private String author;

private String source;

private String content;

@JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyyMMdd'T'HHmmss.SSS'Z'")

@Field(type = FieldType.Date, format = DateFormat.basic_date_time, index = FieldIndex.not_analyzed)

@CreatedDate

private Date ctime;

public int getId() {

return id;

}

public void setId(int id) {

this.id = id;

}

public String getTitle() {

return title;

}

public void setTitle(String title) {

this.title = title;

}

public String getDescription() {

return description;

}

public void setDescription(String description) {

this.description = description;

}

public Date getCtime() {

return ctime;

}

public void setCtime(Date ctime) {

this.ctime = ctime;

}

public String getAuthor() {

return author;

}

public void setAuthor(String author) {

this.author = author;

}

public String getSource() {

return source;

}

public void setSource(String source) {

this.source = source;

}

public String getContent() {

return content;

}

public void setContent(String content) {

this.content = content;

}

@Override

public String toString() {

return "Article [id=" + id + ", title=" + title + ", description=" + description + ", author=" + author + ", source=" + source + ", content=" + content + ", ctime=" + ctime + "]";

}

}

1.4. ElasticsearchRepository

package com.example.api.repository.elasticsearch;

import org.springframework.data.domain.Page;

import org.springframework.data.domain.Pageable;

import org.springframework.data.elasticsearch.repository.ElasticsearchRepository;

import org.springframework.stereotype.Repository;

import com.example.api.domain.elasticsearch.Article;

@Repository

public interface ArticleElasticsearchRepository extends ElasticsearchRepository<Article, Integer> {

Page<Article> findByTitleLike(String title, Pageable page);

Page<Article> findByDescription(String description, Pageable pageable);

Page<Article> findByDescriptionNot(String description, Pageable pageable);

Page<Article> findByDescriptionLike(String description, Pageable pageable);

}

1.5. SearchRestController

package com.example.api.restful;

import org.springframework.beans.factory.annotation.Autowired;

import org.springframework.data.domain.Page;

import org.springframework.data.domain.Pageable;

import org.springframework.data.domain.Sort;

import org.springframework.data.web.PageableDefault;

import org.springframework.web.bind.annotation.PathVariable;

import org.springframework.web.bind.annotation.RequestMapping;

import org.springframework.web.bind.annotation.RestController;

import com.example.api.domain.elasticsearch.Article;

import com.example.api.repository.elasticsearch.ArticleElasticsearchRepository;

@RestController

@RequestMapping("/restful/search")

public class SearchRestController {

@Autowired

private ArticleElasticsearchRepository articleElasticsearchRepository;

@RequestMapping(value = "/article/create")

public  Article create() {

Article article = new Article();

article.setId(1);

article.setTitle("sssss");

article.setContent("test");

return articleElasticsearchRepository.save(article);

}

@RequestMapping(value = "/article/{articleId}")

public  Article get(@PathVariable int articleId) {

return articleElasticsearchRepository.findOne(articleId);

}

}

1.6. 测试

MacBook-Pro:~ neo$ curl http://test:test@localhost:8443/restful/search/article/create.json

{"id":1,"title":"sssss","description":null,"author":null,"source":null,"content":"test","ctime":null}

MacBook-Pro:~ neo$ curl http://test:test@localhost:8443/restful/search/article/1.json

{"id":1,"title":"sssss","description":null,"author":null,"source":null,"content":"test","ctime":null}

2. 集群模式

查看 cluster.name 配置项

root@netkiller ~ % grep ^cluster.name /etc/elasticsearch/elasticsearch.yml

cluster.name: elasticsearch

src/main/resources/application.properties

spring.data.elasticsearch.cluster-name=elasticsearch

spring.data.elasticsearch.cluster-nodes=172.16.0.100:9200

spring.data.elasticsearch.local=false

spring.data.elasticsearch.repositories.enabled=true

3. Document

@Document(indexName = "customer", type = "external", shards = 1, replicas = 0, refreshInterval = "-1")

4. Elasticsearch 删除操作

package com.example.api.schedule;

import org.elasticsearch.action.delete.DeleteResponse;

import org.elasticsearch.client.transport.TransportClient;

import org.elasticsearch.rest.RestStatus;

import org.slf4j.Logger;

import org.slf4j.LoggerFactory;

import org.springframework.beans.factory.annotation.Autowired;

import org.springframework.scheduling.annotation.Scheduled;

import org.springframework.stereotype.Component;

import com.example.api.domain.elasticsearch.ElasticsearchTrash;

import com.example.api.repository.elasticsearch.ElasticsearchTrashRepository;

@Component

public class ScheduledTasks {

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

@Autowired

private TransportClient client;

@Autowired

private ElasticsearchTrashRepository alasticsearchTrashRepository;

public ScheduledTasks() {

}

@Scheduled(fixedRate = 1000 * 60) // 60秒运行一次调度任务

public void cleanTrash() {

for (ElasticsearchTrash elasticsearchTrash : alasticsearchTrashRepository.findAll()) {

DeleteResponse response = client.prepareDelete("information", "article", elasticsearchTrash.getId() + "").get();

RestStatus status = response.status();

logger.info("delete {} {}", elasticsearchTrash.getId(), status.toString());

if (status == RestStatus.OK || status == RestStatus.NOT_FOUND) {

alasticsearchTrashRepository.delete(elasticsearchTrash);

}

}

}

}

5. FAQ

5.1. java.lang.IllegalStateException: Received message from unsupported version: [2.0.0] minimal compatible version is: [5.0.0]

spring-boot-starter-data-elasticsearch 目前还不支持 5.0.0 版本

第 53 章 Spring boot with Data restful

spring-boot-starter-data-rest 能够提供将 Repository, CrudRepository 等接口直接提供给用户访问

1. Maven

<dependency>

<groupId>org.springframework.boot</groupId>

<artifactId>spring-boot-starter-data-rest</artifactId>

</dependency>

第 54 章 Apache ShardingSphere

1. 微服务集群环境，雪花算法出现重复ID

Caused by: com.mysql.jdbc.exceptions.jdbc4.MySQLIntegrityConstraintViolationException: Duplicate entry '854658443787632640' for key 'PRIMARY'

# 指定 工作机器数量 最大是2的10次方 ， 即小于 1024 就可以

spring.shardingsphere.sharding.tables.shard.key-generator.props.worker.id=1000

max-vibration-offset

# 最大容忍的时钟回拨毫秒数, 雪花算法依据时间戳来生成的，一旦时间戳回拨就会造成 id 重复的可能

spring.shardingsphere.sharding.tables.shard.key-generator.max.tolerate.time.difference.milliseconds=5

1.1. 方案一、配置实现

随机指定 worker.id，这样在kubernetes集群环境，每次启动pod，worker.id 都会自动变化。

spring.shardingsphere.sharding.tables.test.key-generator.props.worker.id=${random.int[1,1024]}

查看当前 worker.id

package cn.netkiller.controller.test;//package cn.netkiller.controller;

import org.springframework.beans.factory.annotation.Value;

import org.springframework.cloud.context.config.annotation.RefreshScope;

import org.springframework.web.bind.annotation.GetMapping;

import org.springframework.web.bind.annotation.RestController;

@RefreshScope

@RestController

public class TestRestController {

@Value("${spring.shardingsphere.sharding.tables.test.key-generator.props.worker.id}")

public String workerId;

public TestRestController() {

}

@GetMapping("/workerId")

public String snow() {

return this.workerId;

}

}

1.2. 方案二、代码实现

package cn.netkiller.config;

import org.springframework.context.annotation.Configuration;

import java.net.Inet4Address;

import java.net.InetAddress;

import java.net.UnknownHostException;

/**

* 动态指定sharding jdbc 的 work.id 雪花算法中的属性，然后通过 System.setProperty() 设置环境变量

* workId 可以用主机名、IP地址、Mac地址，最大值 1L << 100，就是1024，即 0<= workId < 1024

* {@link SnowflakeShardingKeyGenerator#getWorkerId()}

*/

@Configuration

public class SnowFlakeWordIdConfiguration {

static {

try {

InetAddress ip4 = Inet4Address.getLocalHost();

String addressIp = ip4.getHostAddress();

System.setProperty("workerId", (Math.abs(addressIp.hashCode()) % 1024) + "");

} catch (UnknownHostException e) {

throw new RuntimeException(e);

}

}

}

配置文件添加 key-generator.props.worker.id 设置 ${workerId} 变量

key-generator:

column: id

props:

worker:

id: ${workerId}

type: SNOWFLAKE

部分 IV. Spring Security

第 55 章 Spring Security

1. Spring boot with Spring security

1.1. Maven

<dependency>

<groupId>org.springframework.boot</groupId>

<artifactId>spring-boot-starter-security</artifactId>

</dependency>

<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>

<groupId>netkiller.cn</groupId>

<artifactId>api.netkiller.cn</artifactId>

<version>0.0.1-SNAPSHOT</version>

<packaging>jar</packaging>

<name>api.netkiller.cn</name>

<url>http://maven.apache.org</url>

<properties>

<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>

<java.version>1.8</java.version>

</properties>

<parent>

<groupId>org.springframework.boot</groupId>

<artifactId>spring-boot-starter-parent</artifactId>

<version>2.0.2.RELEASE</version>

</parent>

<dependencies>

<dependency>

<groupId>org.springframework.boot</groupId>

<artifactId>spring-boot-starter-web</artifactId>

</dependency>

<dependency>

<groupId>org.springframework.boot</groupId>

<artifactId>spring-boot-starter-data-jpa</artifactId>

</dependency>

<dependency>

<groupId>org.springframework.boot</groupId>

<artifactId>spring-boot-starter-jdbc</artifactId>

</dependency>

<dependency>

<groupId>org.springframework.boot</groupId>

<artifactId>spring-boot-starter-data-redis</artifactId>

</dependency>

<dependency>

<groupId>org.springframework.boot</groupId>

<artifactId>spring-boot-starter-data-mongodb</artifactId>

</dependency>

<dependency>

<groupId>org.springframework.boot</groupId>

<artifactId>spring-boot-starter-amqp</artifactId>

</dependency>

<dependency>

<groupId>org.springframework.boot</groupId>

<artifactId>spring-boot-starter-security</artifactId>

</dependency>

<dependency>

<groupId>org.springframework.boot</groupId>

<artifactId>spring-boot-devtools</artifactId>

</dependency>

<dependency>

<groupId>org.springframework.boot</groupId>

<artifactId>spring-boot-starter-test</artifactId>

<scope>test</scope>

</dependency>

<dependency>

<groupId>org.springframework.data</groupId>

<artifactId>spring-data-mongodb</artifactId>

</dependency>

<dependency>

<groupId>org.springframework.data</groupId>

<artifactId>spring-data-oracle</artifactId>

<version>1.0.0.RELEASE</version>

</dependency>

<dependency>

<groupId>com.oracle</groupId>

<artifactId>ojdbc6</artifactId>

<!-- <version>12.1.0.1</version> -->

<version>11.2.0.3</version>

<scope>system</scope>

<systemPath>${basedir}/lib/ojdbc6.jar</systemPath>

</dependency>

<dependency>

<groupId>mysql</groupId>

<artifactId>mysql-connector-java</artifactId>

</dependency>

<dependency>

<groupId>org.springframework.boot</groupId>

<artifactId>spring-boot-starter-mail</artifactId>

</dependency>

<dependency>

<groupId>org.springframework.boot</groupId>

<artifactId>spring-boot-starter-velocity</artifactId>

</dependency>

<dependency>

<groupId>org.apache.velocity</groupId>

<artifactId>velocity</artifactId>

</dependency>

<dependency>

<groupId>com.google.code.gson</groupId>

<artifactId>gson</artifactId>

<scope>compile</scope>

</dependency>

<dependency>

<groupId>junit</groupId>

<artifactId>junit</artifactId>

<scope>test</scope>

</dependency>

</dependencies>

<build>

<sourceDirectory>src</sourceDirectory>

<plugins>

<plugin>

<groupId>org.springframework.boot</groupId>

<artifactId>spring-boot-maven-plugin</artifactId>

</plugin>

<plugin>

<artifactId>maven-compiler-plugin</artifactId>

<version>3.3</version>

<configuration>

<source />

<target />

</configuration>

</plugin>

<plugin>

<artifactId>maven-war-plugin</artifactId>

<version>2.6</version>

<configuration>

<warSourceDirectory>WebContent</warSourceDirectory>

<failOnMissingWebXml>false</failOnMissingWebXml>

</configuration>

</plugin>

</plugins>

</build>

</project>

1.2. Reource

src/main/resources/application.properties

添加默认用户，角色user,用户名neo,密码password

security.user.name=neo

security.user.password=password

security.user.role=USER

现在启动Application，然后尝试访问url，这时会弹出对话框，提示用户用户输入用户名与密码。使用上面的密码便可登陆。

1.3. Application

package api;

import org.springframework.boot.SpringApplication;

import org.springframework.boot.autoconfigure.EnableAutoConfiguration;

import org.springframework.boot.autoconfigure.SpringBootApplication;

import org.springframework.context.annotation.Bean;

import org.springframework.context.annotation.ComponentScan;

import org.springframework.data.jpa.repository.config.EnableJpaRepositories;

import org.springframework.data.mongodb.repository.config.EnableMongoRepositories;

import org.springframework.web.servlet.config.annotation.CorsRegistry;

import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;

@SpringBootApplication

@EnableAutoConfiguration

@ComponentScan({ "api.config", "api.web", "api.rest", "api.service" })

@EnableMongoRepositories

@EnableJpaRepositories

public class Application {

public @Bean WebMvcConfigurer corsConfigurer() {

return new WebMvcConfigurerAdapter() {

@Override

public void addCorsMappings(CorsRegistry registry) {

registry.addMapping("/**");

}

};

}

public static void main(String[] args) {

SpringApplication.run(Application.class, args);

}

}

1.4. WebSecurityConfigurer

注意WebSecurityConfigurer必须在 ComponentScan 的扫描范围

package api.config;

import org.springframework.context.annotation.Configuration;

import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;

import org.springframework.security.config.annotation.web.builders.HttpSecurity;

import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;

import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;

@Configuration

@EnableWebSecurity

public class WebSecurityConfigurer extends WebSecurityConfigurerAdapter {

@Override

protected void configure(AuthenticationManagerBuilder auth) throws Exception {

auth.inMemoryAuthentication().

withUser("user1").password("secret1").roles("USER")

.and().

withUser("user2").password("secret2").roles("USER")

.and().

withUser("admin").password("secret").roles("ADMIN");

}

@Override

protected void configure(HttpSecurity http) throws Exception {

http.authorizeRequests().anyRequest().fullyAuthenticated();

http.httpBasic();

http.csrf().disable();

}

}

1.5. RestController

@RestController

@RequestMapping("/service")

public class UserService {

@RequestMapping(value = "/echo/{in}", method = RequestMethod.GET)

public String echo(@PathVariable(value = "in") final String in, @AuthenticationPrincipal final UserDetails user) {

return "Hello " + user.getUsername() + ", you said: " + in;

}

}

1.6. 测试

curl -u user:password http://172.16.0.20:8080/index.html

curl http://user:password@172.16.0.20:8080/index.html

1.7. Spring + Security + MongoDB

MongoDB 为 Security 用户认证提供数据存储。

Account

package mis.domain;

import org.springframework.data.annotation.Id;

import org.springframework.data.mongodb.core.index.Indexed;

public class Administrator {

@Id

private String id;

@Indexed(unique = true)

private String username;

private String password;

private String authority;

public Administrator() {

// TODO Auto-generated constructor stub

}

public Administrator(String username, String password) {

this.username = username;

this.password = password;

}

public String getId() {

return id;

}

public void setId(String id) {

this.id = id;

}

public String getUsername() {

return username;

}

public void setUsername(String username) {

this.username = username;

}

public String getPassword() {

return password;

}

public void setPassword(String password) {

this.password = password;

}

public String getAuthority() {

return authority;

}

public void setAuthority(String authority) {

this.authority = authority;

}

@Override

public String toString() {

return "User [id=" + id + ", username=" + username + ", password=" + password + ", authority=" + authority + "]";

}

}

AccountRepository

package mis.repository;

import org.springframework.data.mongodb.repository.MongoRepository;

import mis.domain.Administrator;

public interface AdministratorRepository extends MongoRepository<Administrator, String> {

public Administrator findByUsername(String username);

}

WebSecurityConfiguration

@EnableWebSecurity

public class SecurityConfig {

@Bean

public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {

http

// 配置授权请求规则

.authorizeRequests()

// 任何请求都需要认证

.anyRequest()

.authenticated()

// 使用and()方法连接多个配置

.and()

// 开启HTTP基本认证功能

.httpBasic();

return http.build();

}

}

Springboot 2.x

package mis.config;

import org.springframework.beans.factory.annotation.Autowired;

import org.springframework.context.annotation.Bean;

import org.springframework.context.annotation.Configuration;

import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;

import org.springframework.security.config.annotation.authentication.configurers.GlobalAuthenticationConfigurerAdapter;

import org.springframework.security.config.annotation.web.builders.HttpSecurity;

import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;

import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;

import org.springframework.security.core.authority.AuthorityUtils;

import org.springframework.security.core.userdetails.User;

import org.springframework.security.core.userdetails.UserDetails;

import org.springframework.security.core.userdetails.UserDetailsService;

import org.springframework.security.core.userdetails.UsernameNotFoundException;

import mis.domain.Administrator;

import mis.repository.AdministratorRepository;

@Configuration

class GlobalAuthenticationConfigurer extends GlobalAuthenticationConfigurerAdapter {

@Autowired

AdministratorRepository administratorRepository;

@Override

public void init(AuthenticationManagerBuilder auth) throws Exception {

auth.userDetailsService(userDetailsService());

}

@Bean

UserDetailsService userDetailsService() {

return new UserDetailsService() {

@Override

public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {

Administrator administrator = administratorRepository.findByUsername(username);

if (administrator != null) {

return new User(administrator.getUsername(), administrator.getPassword(), AuthorityUtils.createAuthorityList(administrator.getAuthority()));

} else {

throw new UsernameNotFoundException("could not find the administrator '" + username + "'");

}

}

};

}

}

@Configuration

@EnableWebSecurity

public class WebSecurityConfigurer extends WebSecurityConfigurerAdapter {

public WebSecurityConfigurer() {

// TODO Auto-generated constructor stub

}

@Override

protected void configure(HttpSecurity http) throws Exception {

// http.authorizeRequests().anyRequest().fullyAuthenticated().and().httpBasic().and().csrf().disable();

// http.authorizeRequests().antMatchers("/", "/index.html", "/css/**",

// "/js/**","/static/**","/setup.html").permitAll().anyRequest().authenticated().and().formLogin().loginPage("/login.html").permitAll().and().logout().permitAll().and().httpBasic();

// http.authorizeRequests().antMatchers("/**"

// ).permitAll().and().httpBasic();

http.authorizeRequests().antMatchers("/ping", "/v1/*/ping", "/v1/public/**").permitAll().anyRequest().authenticated().and().rememberMe().and().httpBasic().and().csrf().disable();

}

}

2. Spring Security with HTTP Auth

2.1. 默认配置

如果在 maven 中引入了 spring security当你启动 springboot 的时候会提示

Using generated security password: 1cd27b90-1208-4be2-ae8e-0f564ee427b8

默认用户名是 user 可以这样访问

neo@MacBook-Pro ~ % curl -s http://user:1cd27b90-1208-4be2-ae8e-0f564ee427b8@localhost:8080/member/json

{"status":false,"reason":"","code":0,"data":{}}

2.2. 设置用户名和密码

spring.security.user.name=test

spring.security.user.password=test

spring.security.user.role=USER

注意 Springboot 1.x

# security.basic.enabled=false

security.user.name=test

security.user.password=passw0rdf

security.user.role=USER

2.3. 禁用 Security

方法一

@EnableAutoConfiguration(exclude = {

org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration.class

})

@SpringBootApplication(exclude = { SecurityAutoConfiguration.class })

public class Application {

public static void main(String[] args) {

System.out.println("Web Starting...");

SpringApplication.run(Application.class, args);

}

}

Springboot 1.x 可以在 appliction.properties 中加入

security.basic.enabled=false

2.4. 设置角色

spring:

security:

user:

name: kaven

password: itkaven

roles:

- USER

- ADMIN

3. Spring Boot with Web Security(2.x)

3.1. SecurityFilterChain

return http.authorizeHttpRequests(authorize -> {

try {

authorize

// 放行登录接口

.requestMatchers("/", "/ping").permitAll()

.requestMatchers("/token").permitAll()

// 放行资源目录

.requestMatchers("/static/**", "/resources/**").permitAll()

// 其余的都需要权限校验

.anyRequest().authenticated()

// 防跨站请求伪造

.and().csrf(csrf -> csrf.disable());

} catch (Exception e) {

throw new RuntimeException(e);

}

}

).build();

开启 httpBasic 认证

// 使用@EnableWebSecurity注解开启Spring Security功能

@EnableWebSecurity

public class SecurityConfig {

// 定义一个SecurityFilterChain bean，用于配置安全过滤器链

@Bean

public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {

http

// 配置授权请求规则

.authorizeRequests()

// 任何请求都需要认证

.anyRequest()

.authenticated()

// 使用and()方法连接多个配置

.and()

// 开启HTTP基本认证功能

.httpBasic();

return http.build();

}

}

4. Spring Boot with Web Security(2.x)

4.1. EnableWebSecurity

package cn.netkiller.config;

import org.springframework.beans.factory.annotation.Autowired;

import org.springframework.context.annotation.Configuration;

import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;

import org.springframework.security.config.annotation.web.builders.HttpSecurity;

import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;

import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;

@Configuration

@EnableWebSecurity

public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

public WebSecurityConfig() {

// TODO Auto-generated constructor stub

}

@Override

protected void configure(HttpSecurity http) throws Exception {

http

.authorizeRequests()

.antMatchers("/", "/about.html", "/doc/**").permitAll()

.anyRequest().authenticated()

.and()

.formLogin()

.loginPage("/login.html")

.permitAll()

.and()

.logout()

.permitAll();

}

@Autowired

public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {

auth

.inMemoryAuthentication()

.withUser("user").password("password").roles("USER")

.and()

.withUser("admin").password("admin").roles("ADMIN");

}

}

4.2. Web静态资源

用于Web静态资源的权限控制

package com.example.api.config;

import org.springframework.context.annotation.Configuration;

import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;

import org.springframework.security.config.annotation.web.builders.HttpSecurity;

import org.springframework.security.config.annotation.web.builders.WebSecurity;

import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;

import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;

@Configuration

@EnableWebSecurity

public class WebSecurityConfigurer extends WebSecurityConfigurerAdapter {

@Override

public void configure(WebSecurity web) throws Exception {

web.ignoring().antMatchers("/static/**", "/**/*.jsp");

}

protected void registerAuthentication(AuthenticationManagerBuilder auth) throws Exception {

auth.inMemoryAuthentication().withUser("user1").password("secret1").roles("USER").and().withUser("user2").password("secret2").roles("USER").and().withUser("admin").password("secret").roles("ADMIN");

}

@Override

protected void configure(HttpSecurity http) throws Exception {

http.authorizeRequests().anyRequest().fullyAuthenticated();

http.httpBasic();

http.csrf().disable();

}

}

启动 Springboot 可以看到类似日志

2018-10-12 18:01:40.692  INFO 4722 --- [           main] o.s.s.web.DefaultSecurityFilterChain     : Creating filter chain: Ant [pattern='/**/json'], []

2018-10-12 18:01:40.692  INFO 4722 --- [           main] o.s.s.web.DefaultSecurityFilterChain     : Creating filter chain: Ant [pattern='/about'], []

2018-10-12 18:01:40.692  INFO 4722 --- [           main] o.s.s.web.DefaultSecurityFilterChain     : Creating filter chain: Ant [pattern='/test/hello'], []

2018-10-12 18:01:40.693  INFO 4722 --- [           main] o.s.s.web.DefaultSecurityFilterChain     : Creating filter chain: Ant [pattern='/web/**'], []

4.3. 正则匹配

@Override

public void configure(WebSecurity web) throws Exception {

web.ignoring().regexMatchers(XXXXX);

}

4.4. 登陆页面，失败页面，登陆中页面

@Override

protected void configure(HttpSecurity http) throws Exception {

http.authorizeRequests().antMatchers("/**").hasRole("USER").and().formLogin().usernameParameter("username") // default is username

.passwordParameter("password") // default is password

.loginPage("/authentication/login") // default is /login with an HTTP get

.failureUrl("/authentication/login?failed") // default is /login?error

.loginProcessingUrl("/authentication/login/process"); // default is /login

}

4.5. CORS

@EnableWebSecurity

public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

@Override

protected void configure(HttpSecurity http) throws Exception {

http.cors().and()

//other config

}

@Bean

CorsConfigurationSource corsConfigurationSource()

{

CorsConfiguration configuration = new CorsConfiguration();

configuration.setAllowedOrigins(Arrays.asList("https://example.com"));

configuration.setAllowedMethods(Arrays.asList("GET","POST"));

UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();

source.registerCorsConfiguration("/**", configuration);

return source;

}

}

4.6. X-Frame-Options 安全

X-Frame-Options: SAMEORIGIN

@EnableWebSecurity

public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

@Override

protected void configure(HttpSecurity http) throws Exception {

http

// ...

.headers()

.frameOptions().sameOrigin()

.httpStrictTransportSecurity().disable();

}

}

安全配置 X-FRAME-OPTIONS 指定允许iframe访问的域名

package cn.netkiller.api.config;

import org.springframework.context.annotation.Configuration;

import org.springframework.security.config.annotation.web.builders.HttpSecurity;

import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;

import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;

import org.springframework.security.web.header.writers.StaticHeadersWriter;

@Configuration

@EnableWebSecurity

public class WebSecurityConfigurer extends WebSecurityConfigurerAdapter {

@Override

protected void configure(HttpSecurity http) throws Exception {

http.headers().frameOptions().disable().addHeaderWriter(new StaticHeadersWriter("X-FRAME-OPTIONS", "ALLOW-FROM netkiller.cn")).and().

csrf().disable()

.authorizeRequests()

.antMatchers("/","/ping","/v1/*/ping","/public/**","/your/**" ).permitAll()

.antMatchers("/v1/**").authenticated().

anyRequest().permitAll().and().

httpBasic();

}

}

5. 访问控制列表（Access Control List，ACL）

5.1. antMatchers

/** 表示放行所有请求URL

http.authorizeRequests().antMatchers("/**" ).permitAll();

匹配精确的URL地址 "/","/products","/product/show/*","/css/**"

@Override

protected void configure(HttpSecurity httpSecurity) throws Exception {

httpSecurity

.authorizeRequests().antMatchers("/","/products","/product/show/*","/css/**").permitAll()

.anyRequest().authenticated()

.and()

.formLogin().loginPage("/login").permitAll()

.and()

.logout().permitAll();

httpSecurity.csrf().disable();

httpSecurity.headers().frameOptions().disable();

}

5.2. HTTP Auth

@Override

protected void configure(HttpSecurity http) throws Exception {

http.authorizeRequests().antMatchers("/ping","/v1/*/ping","/v1/public/**" ).permitAll()

.anyRequest().authenticated()

.and().rememberMe().and().httpBasic()

.and().csrf().disable();

}

5.3. Rest

protected void configure(HttpSecurity http) throws Exception {

http

.csrf().disable()

.authorizeRequests()

.antMatchers(HttpMethod.POST, "/api/**").authenticated()

.antMatchers(HttpMethod.PUT, "/api/**").authenticated()

.antMatchers(HttpMethod.DELETE, "/api/**").authenticated()

.anyRequest().permitAll()

.and()

.httpBasic().and()

.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);

}

5.4. hasRole

@Override

protected void configure(HttpSecurity http) throws Exception {

http.authorizeRequests()

.antMatchers("/", "/member").access("hasRole('USER') or hasRole('ADMIN') or hasRole('DBA')")

.and().formLogin().loginPage("/login")

.usernameParameter("sso").passwordParameter("password")

.and().exceptionHandling().accessDeniedPage("/403");

}

5.5. hasAnyRole()

@Autowired

private AccessDeniedHandler accessDeniedHandler;

@Override

protected void configure(HttpSecurity http) throws Exception {

http.csrf().disable()

.authorizeRequests()

.antMatchers("/", "/home", "/about").permitAll()

.antMatchers("/admin/**").hasAnyRole("ADMIN")

.antMatchers("/user/**").hasAnyRole("USER")

.anyRequest().authenticated()

.and()

.formLogin()

.loginPage("/login")

.permitAll()

.and()

.logout()

.permitAll()

.and()

.exceptionHandling().accessDeniedHandler(accessDeniedHandler);

}

5.6. withUser

添加用户

@Autowired

public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {

auth

.inMemoryAuthentication()

.withUser("user").password("password").roles("USER");

}

添加多个用户，并指定角色

添加多个用户

@Autowired

public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {

auth.inMemoryAuthentication()

.withUser("user").password("password").roles("USER")

.and()

.withUser("admin").password("password").roles("ADMIN");

}

@Autowired

public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {

auth

.inMemoryAuthentication()

.withUser("user").password("password").roles("USER")

.and()

.withUser("admin").password("admin").roles("ADMIN")

.and()

.withUser("admin").password("super").roles("ADMIN","SYS","DBA")

;

}

获取当前用户

Authentication authentication = SecurityContextHolder.getContext().getAuthentication();

String currentPrincipalName = authentication.getName();

6. @PreAuthorize

6.1. hasRole

@PreAuthorize("hasRole('ROLE_ADMIN')")

@GetMapping("/user/{id}")

public String getUser(@PathVariable("id") String id) {

...

}

6.2. hasAnyRole

@PreAuthorize("hasAnyRole('ROLE_ADMIN','ROLE_MANAGER')")

@GetMapping("/users")

public String getUsers() {

...

}

6.3. 从 HttpServletRequest 返回的 request 变量中判断角色

@GetMapping("/users")

public String getUsers(HttpServletRequest request) {

if (request.isUserInRole("ROLE_ADMIN")) {

...

}

}

6.4. getAuthentication() 获得角色

Authentication auth = SecurityContextHolder.getContext().getAuthentication();

if (auth != null && auth.getAuthorities().stream().anyMatch(a -> a.getAuthority().equals("ADMIN"))) {

...

}

6.5. UserDetailsService

@GetMapping("/users")

public String getUsers() {

UserDetails details = userDetailsService.loadUserByUsername("mike");

if (details != null && details.getAuthorities().stream()

.anyMatch(a -> a.getAuthority().equals("ADMIN"))) {

// ...

}

}

7. Springboot 3 Security + OncePerRequestFilter

7.1. OncePerRequestFilter

package cn.netkiller.config;

import cn.netkiller.annotation.TokenPass;

import cn.netkiller.component.JwtTokeComponent;

import cn.netkiller.utils.ResponseJson;

import jakarta.servlet.FilterChain;

import jakarta.servlet.ServletException;

import jakarta.servlet.http.HttpServletRequest;

import jakarta.servlet.http.HttpServletResponse;

import lombok.extern.slf4j.Slf4j;

import org.springframework.beans.factory.annotation.Autowired;

import org.springframework.context.ApplicationContext;

import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;

import org.springframework.security.core.GrantedAuthority;

import org.springframework.security.core.authority.SimpleGrantedAuthority;

import org.springframework.security.core.context.SecurityContextHolder;

import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;

import org.springframework.stereotype.Component;

import org.springframework.web.filter.OncePerRequestFilter;

import org.springframework.web.method.HandlerMethod;

import org.springframework.web.servlet.HandlerExceptionResolver;

import org.springframework.web.servlet.HandlerExecutionChain;

import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;

import java.io.IOException;

import java.lang.reflect.Method;

import java.util.HashSet;

import java.util.Objects;

import java.util.Set;

@Component

@Slf4j

public class SecurityTokenAuthenticationFilter extends OncePerRequestFilter {

@Autowired

private ApplicationContext applicationContext;

@Autowired

private JwtTokeComponent jwtTokeComponent;

@Autowired

//    @Qualifier("handlerExceptionResolver")

private HandlerExceptionResolver handlerExceptionResolver;

@Override

protected boolean shouldNotFilter(HttpServletRequest request) throws ServletException {

String url = request.getRequestURL().toString();

log.info(request.getRequestURL().toString());

//        return super.shouldNotFilter(request);

return url.contains("/exclude");

}

/**

* token过滤器配置

*

* @param request

* @param response

* @param filterChain

* @throws ServletException

* @throws IOException

*/

@Override

protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {

final String token = request.getHeader("token");

if (token == null || token.isEmpty()) {

// 没有携带 token 则 放行

filterChain.doFilter(request, response);

return;

}

log.info("doFilterInternal: " + request.getRequestURI());

log.info("doFilterInternal: " + request.getHttpServletMapping().getPattern());

try {

//            if (token == null) {

//                throw new RuntimeException("无 token");

//            }

RequestMappingHandlerMapping requestMappingHandlerMapping = (RequestMappingHandlerMapping) applicationContext.getBean("requestMappingHandlerMapping");

// Map<RequestMappingInfo, HandlerMethod> handlerMethods = req2HandlerMapping.getHandlerMethods();

HandlerExecutionChain handlerExecutionChain = requestMappingHandlerMapping.getHandler(request);

if (Objects.nonNull(handlerExecutionChain)) {

HandlerMethod handlerMethod = (HandlerMethod) handlerExecutionChain.getHandler();

//                if (handlerMethod.getBeanType().getName().startsWith(MY_CONTROLLER_PACKAGE_NAME)) {

//                    log.info(handlerMethod.getBeanType().getSimpleName());

//                }

Method method = handlerMethod.getMethod();

//检查方法是否有TokenPass注解，有则跳过认证，直接通过

if (method.isAnnotationPresent(TokenPass.class)) {

TokenPass tokenPass = method.getAnnotation(TokenPass.class);

if (tokenPass.required()) {

filterChain.doFilter(request, response);

return;

}

}

//检查 TokenVerification 需要用户权限的注解

//        if (method.isAnnotationPresent(TokenVerification.class)) {

//            TokenVerification tokenVerification = method.getAnnotation(TokenVerification.class);

//            if (tokenVerification.required()) {

//                //

//            }

//        }

// token 校验逻辑写在这里

//        log.info("token: " + token);

ResponseJson jwt = jwtTokeComponent.verifier(token);

//        log.info("jwt: " + jwt.isStatus());

if (!jwt.isStatus()) {

throw new RuntimeException(jwt.getReason());

}

// 执行认证

Set<GrantedAuthority> authorities = new HashSet<>();

authorities.add(new SimpleGrantedAuthority("ROLE_USER"));

authorities.add(new SimpleGrantedAuthority("ROLE_WATCH"));

authorities.add(new SimpleGrantedAuthority("ROLE_PICTURE"));

UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken("netkiller", null, authorities);

authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));

SecurityContextHolder.getContext().setAuthentication(authenticationToken);

log.info(authenticationToken.toString());

}

} catch (Exception e) {

log.info(e.getMessage());

handlerExceptionResolver.resolveException(request, response, null, e);

return;

}

/**

* 将请求转发给过滤器链下一个filter

*/

filterChain.doFilter(request, response);

}

}

7.2. SecurityConfiguration

package cn.netkiller.config;

import org.springframework.beans.factory.annotation.Autowired;

import org.springframework.context.annotation.Bean;

import org.springframework.context.annotation.Configuration;

import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;

import org.springframework.security.config.annotation.web.builders.HttpSecurity;

import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;

import org.springframework.security.config.annotation.web.configuration.WebSecurityCustomizer;

import org.springframework.security.web.SecurityFilterChain;

import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

/**

* @author Neo

* @description Security 配置类

* @date 2023-01-26 21:18

*/

@Configuration

@EnableWebSecurity

@EnableMethodSecurity

public class SecurityConfiguration {

@Autowired

private SecurityTokenAuthenticationFilter securityTokenAuthenticationFilter;

//    @Value("${spring.profiles.active}")

//    private String env;

@Bean

public WebSecurityCustomizer ignoringCustomizer() {

return (web) -> web.ignoring().requestMatchers("/token", "/version");

}

@Bean

public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {

//        http.authorizeRequests().anyRequest().permitAll();

http.csrf(csrf -> csrf.disable())

.authorizeHttpRequests(authorize -> {

authorize

.requestMatchers("/", "/ping", "/exclude", "/mock/**", "/test/**").permitAll()

.requestMatchers("/token").permitAll()

.requestMatchers("/picture/**", "/chat/**", "/badges/**", "/album/**", "/book/**").permitAll()

.requestMatchers("/static/**", "/resources/**").permitAll()

.anyRequest().authenticated();

}

)

.addFilterBefore(securityTokenAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);

return http.build();

}

}

7.3.

7.4.

第 56 章 Spring Authorization Server

Spring Authorization Server 是 Spring Security OAuth 替代品。

1. Oauth2 协议

授权模式

oauth2.0提供了四种授权模式，开发者可以根据自己的业务情况自由选择。

授权码授权模式（Authorization Code Grant）

隐式授权模式（Implicit Grant）

密码授权模式（Resource Owner Password Credentials Grant）

客户端凭证授权模式（Client Credentials Grant）

token

access_token：访问令牌，必选项。

token_type：令牌类型，该值大小写不敏感，必选项。

expires_in：过期时间，单位为秒。如果省略该参数，必须其他方式设置过期时间。

refresh_token：更新令牌，用来获取下一次的访问令牌，可选项。

scope：权限范围，如果与客户端申请的范围一致，此项可省略。

grant_type

client_credentials

grant_type = 'client_credentials' 模式不需要用户去资源服务器登录并授权, 因为客户端(client)已经有了访问资源服务器的凭证(credentials).

所以当用户访问时,由client直接向资源服务器获取access_token并访问资源即可.

授权码授权模式（Authorization Code Grant）

（A）用户访问客户端，客户端将用户引导向认证服务器。

（B）用户选择是否给予客户端授权。

（C）如用户给予授权，认证服务器将用户引导向客户端指定的redirection uri，同时加上授权码code。

（D）客户端收到code后，通过后台的服务器向认证服务器发送code和redirection uri。

（E）认证服务器验证code和redirection uri，确认无误后，响应客户端访问令牌（access token）和刷新令牌（refresh token）。

请求示例

（A）步骤：客户端申请认证的URI

https://www.example.com/oauth/authorize?response_type=code&client_id=CLIENT_ID&redirect_uri=CALLBACK_URL&scope=read&state=xxx

参数说明：

response_type：授权类型，必选项，此处的值固定为"code"

client_id：客户端的ID，必选项

redirect_uri：重定向URI，必选项

scope：申请的权限范围，可选项

state：任意值，认证服务器会原样返回,用于抵制CSRF(跨站请求伪造)攻击。

（C）步骤：服务器回应客户端的URI

https://client.example.com/cb?code=SplxlOBeZQQYbYS6WxSbIA&state=xxx

参数说明：

code：授权码，必选项。授权码有效期通常设为10分钟，一次性使用。该码与客户端ID、重定向URI以及用户，是一一对应关系。

state：原样返回客户端传的该参数的值。

（D）步骤：客户端向认证服务器申请令牌

https://www.example.com/oauth/token?client_id=CLIENT_ID&grant_type=authorization_code&code=AUTHORIZATION_CODE&redirect_uri=CALLBACK_URL

参数说明：

client_id：表示客户端ID，必选项。

grant_type：表示使用的授权模式，必选项，此处的值固定为"authorization_code"。

code：表示上一步获得的授权码，必选项。

redirect_uri：表示重定向URI，必选项，且必须与A步骤中的该参数值保持一致。

注意：协议里没有提及client_secret参数，建议可以使用此参数进行客户端的二次验证。

（E）步骤：响应（D）步骤的数据

{ "access_token":"2YotnFZFEjr1zCsicMWpAA", "token_type":"example", "expires_in":3600, "refresh_token":"tGzv3JOkF0XG5Qx2TlKWIA", "example_parameter":"example_value" }

参数说明：

access_token：访问令牌，必选项。

token_type：令牌类型，该值大小写不敏感，必选项。

expires_in：过期时间，单位为秒。如果省略该参数，必须其他方式设置过期时间。

refresh_token：更新令牌，用来获取下一次的访问令牌，可选项。

scope：权限范围，如果与客户端申请的范围一致，此项可省略。

使用场景

授权码模式是最常见的一种授权模式，在oauth2.0内是最安全和最完善的。

适用于所有有Server端的应用，如Web站点、有Server端的手机客户端。

可以得到较长期限授权。

密码模式（Resource Owner Password Credentials Grant）

密码模式（Resource Owner Password Credentials Grant）

流程介绍

（A）用户向客户端提供用户名和密码。

（B）客户端将用户名和密码发给认证服务器，向后者请求令牌。

（C）认证服务器确认无误后，向客户端提供访问令牌。

请求示例

（B）步骤：客户端发出https请求

https://www.example.com/token?grant_type=password&username=USERNAME&password=PASSWORD&client_id=CLIENT_ID

参数说明

grant_type：授权类型，此处的值固定为"password"，必选项。

username：用户名，必选项。

password：用户的密码，必选项。

scope：权限范围，可选项。

（C）步骤：向客户端响应（B）步骤的数据

{ "access_token":"2YotnFZFEjr1zCsicMWpAA", "token_type":"example", "expires_in":3600, "refresh_token":"tGzv3JOkF0XG5Qx2TlKWIA" }

参数说明

access_token：访问令牌，必选项。

token_type：令牌类型，该值大小写不敏感，必选项。

expires_in：过期时间，单位为秒。如果省略该参数，必须其他方式设置过期时间。

refresh_token：更新令牌，用来获取下一次的访问令牌，可选项。

使用场景

这种模式适用于用户对应用程序高度信任的情况。比如是用户操作系统的一部分。

认证服务器只有在其他授权模式无法执行的情况下，才能考虑使用这种模式。

客户端凭证模式（Client Credentials Grant）

客户端凭证模式（Client Credentials Grant）

流程介绍

（A）客户端向认证服务器进行身份认证，并要求一个访问令牌。

（B）认证服务器确认无误后，向客户端提供访问令牌。

请求示例

（A）步骤：客户端发送https请求

https://www.example.com/token?grant_type=client_credentials&client_id=CLIENT_ID

参数说明

granttype：表示授权类型，此处的值固定为"client_credentials"，必选项。　　scope：表示权限范围，可选项。

（B）步骤：向客户端响应（A）步骤的数据

{ "access_token":"2YotnFZFEjr1zCsicMWpAA", "token_type":"example", "expires_in":3600, "example_parameter":"example_value" }

参数说明：

access_token：访问令牌，必选项。

token_type：令牌类型，该值大小写不敏感，必选项。

expires_in：过期时间，单位为秒。如果省略该参数，必须其他方式设置过期时间。

example_parameter：其它参数，可选项。

使用场景

客户端模式应用于应用程序想要以自己的名义与授权服务器以及资源服务器进行互动。

例如使用了第三方的静态文件服务

刷新 TOKEN 方式

刷新TOKEN

从上面的四种授权流程可以看出，最终的目的是要获取用户的授权令牌（access_token）。而且授权令牌（access_token）的权限也非常之大，

所以在协议中明确表示要设置授权令牌（access_token）的有效期。那么当授权令牌（access_token）过期要怎么办呢，协议里提出了一个刷新token的流程。

流程介绍

（A）--（D）通过授权流程获取access_token，并调用业务api接口。

（F）当调用业务api接口时响应“Invalid Token Error”时。

（G）调用刷新access_token接口，使用参数refresh_token（如果平台方提供，否则需要用户重新进行授权流程）。

（H）响应最新的access_token及refresh_token。

请求示例

（G）步骤：客户端调用刷新token接口

https://www.example.com/v1/oauth/token?grant_type=refresh_token&client_id=CLIENT_ID&client_secret=CLIENT_SECRET&refresh_token=REFRESH_TOKEN

参数说明

client_id：客户端的ID，必选项。

client_secret：客户端的密钥，必选项。

grant_type：表示使用的授权模式，此处的值固定为"refreshtoken"，必选项。　　refresh_token：表示早前收到的更新令牌，必选项。

（H）步骤：响应客户端数据

{ "access_token":"2YotnFZFEjr1zCsicMWpAA", "token_type":"example", "expires_in":3600, "refresh_token":"tGzv3JOkF0XG5Qx2TlKWIA", "example_parameter":"example_value" }

参数说明

access_token：访问令牌，必选项。

token_type：令牌类型，该值大小写不敏感，必选项。

expires_in：过期时间，单位为秒。如果省略该参数，必须其他方式设置过期时间。

refresh_token：更新令牌，用来获取下一次的访问令牌，可选项。

scope：权限范围，如果与客户端申请的范围一致，此项可省略。

说明：建议将access_token和refresh_token的过期时间保存下来，每次调用平台方的业务api前先对access_token和refresh_token进行一下时间判断，如果过期则执行刷新access_token或重新授权操作。refersh_token如果过期就只能让用户重新授权。

好，到此oauth2.0的四种授权流程及令牌的刷新流程已经介绍完了，下面来从oauth2.0的安全性上来介绍一下。

2. Maven 依赖

<dependency>

<groupId>org.springframework.security.experimental</groupId>

<artifactId>spring-security-oauth2-authorization-server</artifactId>

<version>0.0.1</version>

</dependency>

3. Spring cloud with Oauth2

authorization_code

验证服务器器

<dependency>

<groupId>org.springframework.boot</groupId>

<artifactId>spring-boot-starter-web</artifactId>

</dependency>

<dependency>

<groupId>org.springframework.cloud</groupId>

<artifactId>spring-cloud-starter-oauth2</artifactId>

</dependency>

<dependency>

<groupId>org.springframework.cloud</groupId>

<artifactId>spring-cloud-starter-security</artifactId>

</dependency>

<dependency>

<groupId>org.springframework.boot</groupId>

<artifactId>spring-boot-starter-thymeleaf</artifactId>

</dependency>

测试

http://localhost:8080/oauth/authorize?response_type=code&client_id=sso&redirect_uri=http://localhost:8082/callback&scope=read

http://localhost:8080/oauth/token?client_id=sso&grant_type=authorization_code&redirect_uri=http://localhost:8082/callback&code=ZzLi3w

localhost:8082/admin&code=ZzLi3w

localhost:8080/oauth/authorize?response_type=code&client_id=client&redirect_uri=http://www.netkiller.cn&state=123

localhost:8080/oauth/authorize?response_type=code&client_id=sso&redirect_uri=http://localhost:8082/test&scope=app

http://localhost:8080/oauth/token?client_id=sso&grant_type=authorization_code&redirect_uri=http://localhost:8082&code=8z5J9L

http://localhost:8080/oauth/authorize?response_type=code&client_id=sso&redirect_uri=http://localhost:8080&scope=app

http://www.netkiller.cn/?code=eX6IMV&state=123

http://localhost:18084/oauth/token?client_id=client&grant_type=authorization_code&redirect_uri=http://api.netkiller.cn&code=bzxoHn

Spring boot with Oauth2 - Password

下面例子由三个项目组成，分别是 tools, server, client。

其中 tools 是密码生成工具

https://tools.ietf.org/html/rfc6749

Maven

<?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>

<groupId>cn.netkiller</groupId>

<artifactId>oauth</artifactId>

<version>0.0.1-SNAPSHOT</version>

<packaging>pom</packaging>

<name>oauth</name>

<url>http://maven.apache.org</url>

<properties>

<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>

</properties>

<parent>

<groupId>org.springframework.boot</groupId>

<artifactId>spring-boot-starter-parent</artifactId>

<version>2.0.2.RELEASE</version>

<relativePath /> <!-- lookup parent from repository -->

</parent>

<dependencies>

<dependency>

<groupId>org.springframework.boot</groupId>

<artifactId>spring-boot-starter-actuator</artifactId>

</dependency>

<dependency>

<groupId>org.springframework.boot</groupId>

<artifactId>spring-boot-starter-data-jpa</artifactId>

</dependency>

<dependency>

<groupId>org.springframework.boot</groupId>

<artifactId>spring-boot-starter-jdbc</artifactId>

</dependency>

<dependency>

<groupId>mysql</groupId>

<artifactId>mysql-connector-java</artifactId>

<scope>runtime</scope>

</dependency>

<dependency>

<groupId>org.springframework.boot</groupId>

<artifactId>spring-boot-starter-web</artifactId>

</dependency>

<dependency>

<groupId>org.springframework.boot</groupId>

<artifactId>spring-boot-starter-test</artifactId>

<scope>test</scope>

</dependency>

<!-- Security -->

<dependency>

<groupId>org.springframework.boot</groupId>

<artifactId>spring-boot-starter-security</artifactId>

</dependency>

<dependency>

<groupId>org.springframework.security.oauth</groupId>

<artifactId>spring-security-oauth2</artifactId>

</dependency>

</dependencies>

<modules>

<module>server</module>

<module>client</module>

</modules>

<build>

<plugins>

<plugin>

<groupId>org.springframework.boot</groupId>

<artifactId>spring-boot-maven-plugin</artifactId>

</plugin>

</plugins>

</build>

</project>

Password tools

Maven

<?xml version="1.0"?>

<project xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd" xmlns="http://maven.apache.org/POM/4.0.0"

xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">

<modelVersion>4.0.0</modelVersion>

<parent>

<groupId>cn.netkiller</groupId>

<artifactId>oauth</artifactId>

<version>0.0.1-SNAPSHOT</version>

</parent>

<artifactId>tools</artifactId>

<version>0.0.1-SNAPSHOT</version>

<name>tools</name>

<url>http://maven.apache.org</url>

<properties>

<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>

</properties>

<dependencies>

<dependency>

<groupId>junit</groupId>

<artifactId>junit</artifactId>

<version>3.8.1</version>

<scope>test</scope>

</dependency>

</dependencies>

</project>

下面的代码用于生成 Spring security 所用的密码

package cn.netkiller.oauth.tools;

import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;

/**

* Hello world!

*

*/

public class App {

public static void main(String[] args) {

int i = 0;

while (i < 10) {

String password = "123456";

BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();

String hashedPassword = passwordEncoder.encode(password);

System.out.println(hashedPassword);

i++;

}

}

}

运行后生成类似的密码

$2a$10$IrDG5Yr3CGorEg9gKG8smeRLnNUieCVyBCJHA80U6h20HDFCxd43W

$2a$10$wseh5xFv1L3a3uHQId0MAOqAN0rKKcuX.RDaBPQ.pV/hsr80TqwxO

$2a$10$xP3Gc/5/PN03BdkDfhUjAemTRVaiwr0lsaqPqD18UI.ho9nRC/ebW

$2a$10$S.wLZ6e5YvmQA6mkX8yXWOdJbvahtDOesRu0ZwPOzAPCwpo7eDAsi

$2a$10$Jo/yuWyiAZ2Lj8.ywoPl7OeOJYuP7RVq8l.qc/zOwtW8MTFp3NYGO

$2a$10$eEvvjPok0fRK.DU6yF0qI.aucuiWr3y5G93SLq9/76ovcOwIuQAuS

$2a$10$BWEkANxbgwATNQCEI9/uNevNEUNlomGY7cZ2CQVm.qCRcnyukT.Si

$2a$10$69wSpyJQvjzJY7ou5PFWlOlEIecQukHV9WEq0nebsz5V6IZKfOVv2

$2a$10$Cyj3hM39V34r5pMeQ.Y9peuUqYMBSvsJ7GTBgp4.stWaTtWMboYGS

$2a$10$0/o4cRN2.tmnc58sH.N4WOsreVI6sWlPl4CCBrmUfJ332TMfRzA42

Server

Maven

<?xml version="1.0"?>

<project xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd" xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">

<modelVersion>4.0.0</modelVersion>

<parent>

<groupId>cn.netkiller</groupId>

<artifactId>oauth</artifactId>

<version>0.0.1-SNAPSHOT</version>

</parent>

<groupId>cn.netkiller</groupId>

<artifactId>server</artifactId>

<name>server</name>

<description>Example Spring Boot REST project</description>

<url>http://maven.apache.org</url>

<properties>

<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>

<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>

<java.version>1.8</java.version>

</properties>

<dependencies>

</dependencies>

</project>

application.properties

server.port=8000

server.contextPath=/api

logging.level.com.gigy=DEBUG

# Data source properties

spring.datasource.driver-class-name=com.mysql.jdbc.Driver

spring.datasource.url=jdbc:mysql://127.0.0.1:3306/netkiller?useSSL=false

spring.datasource.username=netkiller

spring.datasource.password=123123

spring.datasource.max-idle=10

spring.datasource.max-wait=10000

spring.datasource.min-idle=5

spring.datasource.initial-size=5

spring.datasource.validation-query=SELECT 1

spring.datasource.test-on-borrow=false

spring.datasource.test-while-idle=true

spring.datasource.time-between-eviction-runs-millis=18800

spring.datasource.jdbc-interceptors=ConnectionState;SlowQueryReport(threshold=0)

spring.jpa.database=MYSQL

spring.jpa.show-sql=true

#spring.jpa.hibernate.ddl-auto=update

spring.jpa.hibernate.ddl-auto=create-drop

#spring.jpa.hibernate.ddl-auto=validate

#spring.jpa.show-sql=true

EnableAuthorizationServer

package cn.netkiller.oauth.server.config;

import org.springframework.beans.factory.annotation.Autowired;

import org.springframework.beans.factory.annotation.Qualifier;

import org.springframework.beans.factory.annotation.Value;

import org.springframework.context.annotation.Bean;

import org.springframework.context.annotation.Configuration;

import org.springframework.security.authentication.AuthenticationManager;

import org.springframework.security.core.userdetails.UserDetailsService;

import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;

import org.springframework.security.crypto.password.PasswordEncoder;

import org.springframework.security.oauth2.config.annotation.configurers.ClientDetailsServiceConfigurer;

import org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerConfigurerAdapter;

import org.springframework.security.oauth2.config.annotation.web.configuration.EnableAuthorizationServer;

import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerEndpointsConfigurer;

@Configuration

@EnableAuthorizationServer

public class AuthorizationServerConfigurer extends AuthorizationServerConfigurerAdapter {

@Autowired

@Qualifier("userDetailsService")

private UserDetailsService userDetailsService;

@Autowired

private AuthenticationManager authenticationManager;

@Value("${oauth.tokenTimeout:3600}")

private int expiration;

@Bean

public PasswordEncoder passwordEncoder() {

return new BCryptPasswordEncoder();

}

@Override

public void configure(AuthorizationServerEndpointsConfigurer configurer) throws Exception {

configurer.authenticationManager(authenticationManager);

configurer.userDetailsService(userDetailsService);

}

@Override

public void configure(ClientDetailsServiceConfigurer clients) throws Exception {

clients.inMemory().withClient("api").secret("secret").accessTokenValiditySeconds(expiration).scopes("read", "write").authorizedGrantTypes("password", "refresh_token").resourceIds("resource");

}

}

Spring boot 2.0

@Override

public void configure(ClientDetailsServiceConfigurer clients) throws Exception {

clients.inMemory().withClient("api").secret(passwordEncoder().encode("secret")).accessTokenValiditySeconds(expiration).scopes("read", "write").authorizedGrantTypes("password", "refresh_token").resourceIds("resource");

}

EnableResourceServer

package cn.netkiller.oauth.server.config;

import org.springframework.context.annotation.Bean;

import org.springframework.context.annotation.Configuration;

import org.springframework.security.authentication.AuthenticationManager;

import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;

import org.springframework.security.config.annotation.web.builders.HttpSecurity;

import org.springframework.security.config.annotation.web.builders.WebSecurity;

import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;

import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;

import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer;

@Configuration

@EnableWebSecurity

@EnableResourceServer

@EnableGlobalMethodSecurity(prePostEnabled = true)

public class WebSecurityConfiguration extends WebSecurityConfigurerAdapter {

/**

* Constructor disables the default security settings

*/

public WebSecurityConfiguration() {

super(true);

}

@Override

public void configure(WebSecurity web) throws Exception {

web.ignoring().antMatchers("/login");

}

@Override

public void configure(HttpSecurity http) throws Exception {

http.antMatcher("/api/**").authorizeRequests().anyRequest().authenticated();

}

@Bean

@Override

public AuthenticationManager authenticationManagerBean() throws Exception {

return super.authenticationManagerBean();

}

}

Entity Table

package cn.netkiller.oauth.server.model;

import java.util.ArrayList;

import java.util.Collection;

import java.util.List;

import javax.persistence.Column;

import javax.persistence.Entity;

import javax.persistence.GeneratedValue;

import javax.persistence.GenerationType;

import javax.persistence.Id;

import javax.persistence.Table;

import org.springframework.security.core.GrantedAuthority;

import org.springframework.security.core.userdetails.UserDetails;

@Entity

@Table(name = "users")

public class User implements UserDetails {

static final long serialVersionUID = 1L;

@Id

@GeneratedValue(strategy = GenerationType.AUTO)

@Column(name = "user_id", nullable = false, updatable = false)

private Long id;

@Column(name = "username", nullable = false, unique = true)

private String username;

@Column(name = "password", nullable = false)

private String password;

@Column(name = "enabled", nullable = false)

private boolean enabled;

public Collection<? extends GrantedAuthority> getAuthorities() {

List<GrantedAuthority> authorities = new ArrayList<GrantedAuthority>();

return authorities;

}

public boolean isAccountNonExpired() {

return true;

}

public boolean isAccountNonLocked() {

// we never lock accounts

return true;

}

public boolean isCredentialsNonExpired() {

// credentials never expire

return true;

}

public boolean isEnabled() {

return enabled;

}

public String getPassword() {

return password;

}

public String getUsername() {

return username;

}

}

UserRepository

package cn.netkiller.oauth.server.repository;

import org.springframework.data.jpa.repository.JpaRepository;

import cn.netkiller.oauth.server.model.User;

public interface UserRepository extends JpaRepository<User, Long> {

/**

* Find a user by username

*

* @param username

*            the user's username

* @return user which contains the user with the given username or null.

*/

User findOneByUsername(String username);

}

UserService

package cn.netkiller.oauth.server.service;

import org.springframework.beans.factory.annotation.Autowired;

import org.springframework.security.core.userdetails.UserDetails;

import org.springframework.security.core.userdetails.UserDetailsService;

import org.springframework.security.core.userdetails.UsernameNotFoundException;

import org.springframework.stereotype.Service;

import cn.netkiller.oauth.server.repository.UserRepository;

@Service("userDetailsService")

public class UserService implements UserDetailsService {

@Autowired

private UserRepository userRepository;

public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {

return userRepository.findOneByUsername(username);

}

}

TestRestController

package cn.netkiller.oauth.server.controller;

import org.springframework.http.HttpStatus;

import org.springframework.http.ResponseEntity;

import org.springframework.security.access.prepost.PreAuthorize;

import org.springframework.web.bind.annotation.RequestMapping;

import org.springframework.web.bind.annotation.RequestMethod;

import org.springframework.web.bind.annotation.RestController;

@RestController

@RequestMapping("/test")

public class TestRestController {

@RequestMapping(value = "hello", method = RequestMethod.GET)

public ResponseEntity<String> hello() {

return new ResponseEntity<String>("Hello world !", HttpStatus.OK);

}

@PreAuthorize("#oauth2.hasScope('write')")

@RequestMapping(value = "set/{string}", method = RequestMethod.GET)

public ResponseEntity<String> set(String string) {

return new ResponseEntity<String>(string, HttpStatus.OK);

}

}

数据库初始化

在 src/main/resources 目录创建 data.sql 文件

INSERT INTO users (user_id, username, password, enabled) VALUES

('1', 'netkiller@msn.com', '$2a$10$Cyj3hM39V34r5pMeQ.Y9peuUqYMBSvsJ7GTBgp4.stWaTtWMboYGS', true);

此处密码 $2a$10$Cyj3hM39V34r5pMeQ.Y9peuUqYMBSvsJ7GTBgp4.stWaTtWMboYGS 请使用上面提供的工具生成。

Test

启动 Spring boot Server 项目

mvn spring-boot:run

启动后 Spring boot 会导入 data.sql 文件

mysql> select * from users where username="netkiller@msn.com";

+---------+---------+--------------------------------------------------------------+-------------------+

| user_id | enabled | password                                                     | username          |

+---------+---------+--------------------------------------------------------------+-------------------+

|       4 |        | $2a$10$Cyj3hM39V34r5pMeQ.Y9peuUqYMBSvsJ7GTBgp4.stWaTtWMboYGS | netkiller@msn.com |

+---------+---------+--------------------------------------------------------------+-------------------+

1 row in set (0.00 sec)

MacBook-Pro:Application neo$ curl -X POST --user 'api:secret' -d 'grant_type=password&username=netkiller@msn.com&password=123456' http://localhost:8000/api/oauth/token

{"access_token":"5bc0ee89-cd6d-47be-b31f-89c9e028159b","token_type":"bearer","refresh_token":"5107c09b-de85-4faf-8396-941572cf30d2","expires_in":3599,"scope":"read write"}MacBook-Pro:Application neo$

MacBook-Pro:Application neo$ curl -H "Accept: application/json" -H "Content-Type: application/json" -H "Authorization: Bearer 5bc0ee89-cd6d-47be-b31f-89c9e028159b" -X GET http://localhost:8000/api/test/hello

Hello world !

Spring boot with Oauth2 RestTemplate

Maven

<dependency>

<groupId>org.springframework.security.oauth</groupId>

<artifactId>spring-security-oauth2</artifactId>

</dependency>

OAuth2ClientConfiguration.java

package cn.netkiller.config;

import static java.util.Arrays.asList;

import org.springframework.beans.factory.annotation.Value;

import org.springframework.context.annotation.Bean;

import org.springframework.context.annotation.Configuration;

import org.springframework.security.oauth2.client.DefaultOAuth2ClientContext;

import org.springframework.security.oauth2.client.OAuth2RestOperations;

import org.springframework.security.oauth2.client.OAuth2RestTemplate;

import org.springframework.security.oauth2.client.resource.OAuth2ProtectedResourceDetails;

import org.springframework.security.oauth2.client.token.AccessTokenRequest;

import org.springframework.security.oauth2.client.token.DefaultAccessTokenRequest;

import org.springframework.security.oauth2.client.token.grant.password.ResourceOwnerPasswordResourceDetails;

import org.springframework.security.oauth2.config.annotation.web.configuration.EnableOAuth2Client;

@EnableOAuth2Client

@Configuration

public class OAuth2ClientConfiguration {

@Value("${oauth.resource:http://localhost:8080}")

private String baseUrl;

@Value("${oauth.authorize:http://localhost:8080/oauth/authorize}")

private String authorizeUrl;

@Value("${oauth.token:http://localhost:8080/oauth/token}")

private String tokenUrl;

@Bean

protected OAuth2ProtectedResourceDetails resource() {

ResourceOwnerPasswordResourceDetails resource = new ResourceOwnerPasswordResourceDetails();

resource.setAccessTokenUri(tokenUrl);

resource.setClientId("api");

resource.setClientSecret("secret");

resource.setGrantType("password");

resource.setScope(asList("read", "write"));

resource.setUsername("netkiller@msn.com");

resource.setPassword("123456");

return resource;

}

@Bean

public OAuth2RestOperations restTemplate() {

AccessTokenRequest accessTokenRequest = new DefaultAccessTokenRequest();

return new OAuth2RestTemplate(resource(), new DefaultOAuth2ClientContext(accessTokenRequest));

}

}

Application.java

package cn.netkiller;

import org.springframework.boot.SpringApplication;

import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication

public class Application {

public static void main(String[] args) {

System.out.println("Spring boot with Oauth2RestTemplate!");

SpringApplication.run(Application.class, args);

}

}

application.properties

security.basic.enabled=false

Controller

package cn.netkiller.controller;

import org.springframework.beans.factory.annotation.Autowired;

import org.springframework.security.oauth2.client.OAuth2RestOperations;

import org.springframework.security.oauth2.common.OAuth2AccessToken;

import org.springframework.stereotype.Controller;

import org.springframework.web.bind.annotation.GetMapping;

import org.springframework.web.bind.annotation.ResponseBody;

@Controller

public class TestController {

@Autowired

private OAuth2RestOperations restTemplate;

@GetMapping("/")

@ResponseBody

public String index() {

OAuth2AccessToken token = restTemplate.getAccessToken();

System.out.println(token.getValue());

String tmp = restTemplate.getForObject("http://api.netkiller.cn/test.json", String.class);

System.out.println(tmp);

return tmp;

}

}

Test

neo@MacBook-Pro ~/deployment % curl http://localhost:8080

OK

Spring boot with Oauth2 jwt

Maven

<dependency>

<groupId>org.springframework.boot</groupId>

<artifactId>spring-boot-starter-security</artifactId>

</dependency>

<dependency>

<groupId>org.springframework.security.oauth</groupId>

<artifactId>spring-security-oauth2</artifactId>

</dependency>

<dependency>

<groupId>org.springframework.security</groupId>

<artifactId>spring-security-jwt</artifactId>

</dependency>

如果是 Springboot 2.0.4 需要制定版本号

<dependency>

<groupId>org.springframework.security.oauth</groupId>

<artifactId>spring-security-oauth2</artifactId>

<version>2.3.3.RELEASE</version>

</dependency>

<dependency>

<groupId>org.springframework.security</groupId>

<artifactId>spring-security-jwt</artifactId>

<version>1.0.9.RELEASE</version>

</dependency>

<dependency>

<groupId>javax.xml.bind</groupId>

<artifactId>jaxb-api</artifactId>

</dependency>

<dependency>

<groupId>com.sun.xml.bind</groupId>

<artifactId>jaxb-impl</artifactId>

<version>2.3.0</version>

</dependency>

<dependency>

<groupId>com.sun.xml.bind</groupId>

<artifactId>jaxb-core</artifactId>

<version>2.3.0</version>

</dependency>

<dependency>

<groupId>com.sun.activation</groupId>

<artifactId>javax.activation</artifactId>

<version>1.2.0</version>

</dependency>

Authorization Server

package api.config;

import org.springframework.beans.factory.annotation.Autowired;

import org.springframework.beans.factory.annotation.Qualifier;

import org.springframework.beans.factory.annotation.Value;

import org.springframework.context.annotation.Bean;

import org.springframework.context.annotation.Configuration;

import org.springframework.context.annotation.Primary;

import org.springframework.security.authentication.AuthenticationManager;

import org.springframework.security.core.userdetails.UserDetailsService;

import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;

import org.springframework.security.crypto.password.PasswordEncoder;

import org.springframework.security.oauth2.config.annotation.configurers.ClientDetailsServiceConfigurer;

import org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerConfigurerAdapter;

import org.springframework.security.oauth2.config.annotation.web.configuration.EnableAuthorizationServer;

import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerEndpointsConfigurer;

import org.springframework.security.oauth2.provider.token.DefaultTokenServices;

import org.springframework.security.oauth2.provider.token.TokenStore;

import org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter;

import org.springframework.security.oauth2.provider.token.store.JwtTokenStore;

@Configuration

@EnableAuthorizationServer

public class AuthorizationServerConfigurer extends AuthorizationServerConfigurerAdapter {

@Autowired

@Qualifier("userDetailsService")

private UserDetailsService userDetailsService;

@Autowired

private AuthenticationManager authenticationManager;

@Value("${oauth.tokenTimeout:3600}")

private int expiration;

@Bean

public PasswordEncoder passwordEncoder() {

return new BCryptPasswordEncoder();

}

// @Override

// public void configure(AuthorizationServerSecurityConfigurer oauthServer) throws Exception {

// oauthServer.tokenKeyAccess("isAnonymous() || hasAuthority('ROLE_TRUSTED_CLIENT')");

// oauthServer.checkTokenAccess("hasAuthority('ROLE_TRUSTED_CLIENT')");

// }

@Override

public void configure(ClientDetailsServiceConfigurer clients) throws Exception {

clients.inMemory().withClient("api").secret(passwordEncoder().encode("secret")).accessTokenValiditySeconds(expiration).refreshTokenValiditySeconds(expiration).scopes("read", "write").authorizedGrantTypes("password", "refresh_token").authorities("USER").resourceIds("blockchain");

}

@Override

public void configure(AuthorizationServerEndpointsConfigurer configurer) throws Exception {

configurer.tokenStore(tokenStore()).accessTokenConverter(accessTokenConverter());

configurer.authenticationManager(authenticationManager);

configurer.userDetailsService(userDetailsService);

}

@Bean

public TokenStore tokenStore() {

return new JwtTokenStore(accessTokenConverter());

}

@Bean

public JwtAccessTokenConverter accessTokenConverter() {

JwtAccessTokenConverter converter = new JwtAccessTokenConverter();

converter.setSigningKey("123");

return converter;

}

@Bean

@Primary

public DefaultTokenServices tokenServices() {

DefaultTokenServices defaultTokenServices = new DefaultTokenServices();

defaultTokenServices.setTokenStore(tokenStore());

defaultTokenServices.setSupportRefreshToken(true);

return defaultTokenServices;

}

}

Resource Server

package api.config;

import org.springframework.context.annotation.Bean;

import org.springframework.context.annotation.Configuration;

import org.springframework.context.annotation.Primary;

import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer;

import org.springframework.security.oauth2.config.annotation.web.configuration.ResourceServerConfigurerAdapter;

import org.springframework.security.oauth2.config.annotation.web.configurers.ResourceServerSecurityConfigurer;

import org.springframework.security.oauth2.provider.token.DefaultTokenServices;

import org.springframework.security.oauth2.provider.token.TokenStore;

import org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter;

import org.springframework.security.oauth2.provider.token.store.JwtTokenStore;

@Configuration

@EnableResourceServer

public class ResourceServerConfigurer extends ResourceServerConfigurerAdapter {

public ResourceServerConfigurer() {

// TODO Auto-generated constructor stub

}

@Override

public void configure(ResourceServerSecurityConfigurer resources) {

resources.resourceId("blockchain");

resources.tokenServices(tokenServices());

}

@Bean

public TokenStore tokenStore() {

return new JwtTokenStore(accessTokenConverter());

}

@Bean

public JwtAccessTokenConverter accessTokenConverter() {

JwtAccessTokenConverter converter = new JwtAccessTokenConverter();

converter.setSigningKey("123");

return converter;

}

@Bean

@Primary

public DefaultTokenServices tokenServices() {

DefaultTokenServices defaultTokenServices = new DefaultTokenServices();

defaultTokenServices.setTokenStore(tokenStore());

return defaultTokenServices;

}

}

Web Security

package api.config;

import org.springframework.context.annotation.Bean;

import org.springframework.context.annotation.Configuration;

import org.springframework.http.HttpMethod;

import org.springframework.security.authentication.AuthenticationManager;

import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;

import org.springframework.security.config.annotation.web.builders.HttpSecurity;

import org.springframework.security.config.annotation.web.builders.WebSecurity;

import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;

import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;

@Configuration

@EnableWebSecurity

@EnableGlobalMethodSecurity(prePostEnabled = true)

public class WebSecurityConfiguration extends WebSecurityConfigurerAdapter {

@Override

public void configure(WebSecurity web) throws Exception {

web.ignoring().antMatchers("/login");

}

@Override

public void configure(HttpSecurity http) throws Exception {

// http.antMatcher("/**").authorizeRequests().anyRequest().authenticated();

http.authorizeRequests().antMatchers(HttpMethod.OPTIONS).permitAll().anyRequest().authenticated().and().httpBasic().and().csrf().disable();

}

@Bean

@Override

public AuthenticationManager authenticationManagerBean() throws Exception {

return super.authenticationManagerBean();

}

}

插入数据

INSERT INTO `user` (username, password, enabled) VALUES ('netkiller@msn.com', '$2a$10$Cyj3hM39V34r5pMeQ.Y9peuUqYMBSvsJ7GTBgp4.stWaTtWMboYGS', true);

使用 CURL 测试 JWT

neo@MacBook-Pro ~ % curl -X POST --user 'api:secret' -d 'grant_type=password&username=netkiller@msn.com&password=123456' http://localhost:8080/oauth/token

{"access_token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOlsicmVzb3VyY2UiXSwiZXhwIjoxNTM1MTE4MzYxLCJ1c2VyX25hbWUiOiJuZXRraWxsZXJAbXNuLmNvbSIsImp0aSI6ImM5YzA4ODczLWZlMTctNGM2My05MDA1LTIzZGJlNTE1NTQ0YiIsImNsaWVudF9pZCI6ImFwaSIsInNjb3BlIjpbInJlYWQiLCJ3cml0ZSJdfQ.ydxJQKtdBNWHELL8_aXVoZETNMygXs8T1HQ5XWDBELA","token_type":"bearer","refresh_token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOlsicmVzb3VyY2UiXSwidXNlcl9uYW1lIjoibmV0a2lsbGVyQG1zbi5jb20iLCJzY29wZSI6WyJyZWFkIiwid3JpdGUiXSwiYXRpIjoiYzljMDg4NzMtZmUxNy00YzYzLTkwMDUtMjNkYmU1MTU1NDRiIiwiZXhwIjoxNTM1MTE4MzYxLCJqdGkiOiI3ODJiNmY2NC0xYzdiLTQ0YWYtOThlZC1iZDk3Y2QzYzE1NjEiLCJjbGllbnRfaWQiOiJhcGkifQ.16QUHOrVPUBF97E_972AcLA83zEK7UzaI424PeAmX6E","expires_in":3599,"scope":"read write","jti":"c9c08873-fe17-4c63-9005-23dbe515544b"}

复制 access_token 粘贴到 Authorization: Bearer 后面

neo@MacBook-Pro ~ % curl -H "Accept: application/json" -H "Content-Type: application/json" -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOlsiYmxvY2tjaGFpbiJdLCJleHAiOjE1MzUxMTkyMTUsInVzZXJfbmFtZSI6Im5ldGtpbGxlckBtc24uY29tIiwianRpIjoiZTNjYmIxNTItYmFkZS00MGM1LTkxOGItZjJjMzZiYTBiMTE2IiwiY2xpZW50X2lkIjoiYXBpIiwic2NvcGUiOlsicmVhZCIsIndyaXRlIl19.ophWAvIT1ZmWCIQyAgyuzmQ98TJsN49OeR3CBSJ_X54" -X GET http://localhost:8080/test/mongo

{"id":"5b800709e18a211e83c7f355","name":"Netkiller"}

测试 Shell

URL=http://localhost:8080

TOKEN=$(curl -s  -X POST --user 'api:secret' -d 'grant_type=password&username=netkiller@msn.com&password=123456' ${URL}/oauth/token | sed 's/.*"access_token":"\([^"]*\)".*/\1/g')

curl -s -k -H "Accept: application/json" -H "Content-Type: application/json" -H "Authorization: Bearer ${TOKEN}" -X GET ${URL}/test/hello.json

refresh_token

curl -s  -X POST --user 'api:secret' -d 'grant_type=refresh_token&client_id=api&client_secret=secret&refresh_token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOlsiYmxvY2tjaGFpbiJdLCJ1c2VyX25hbWUiOiJibG9ja2NoYWluIiwic2NvcGUiOlsicmVhZCIsIndyaXRlIl0sImF0aSI6ImU2OGE4ODUzLWJlODUtNGUwNy1hYzE0LTM0MDI1ZDBiZGY0ZiIsImV4cCI6MTU0MDM4MzA5NCwianRpIjoiY2JjNTk2ZWMtOTkyZi00NmQ2LWFiZGYtMTcyYWFiZmEwNDFiIiwiY2xpZW50X2lkIjoiYXBpIn0.PqsGl5Dm8WBqwbhHf-LqmUP1toCpsFS4gLeOP2bMwR4' ${URL}/oauth/token

Spring boot with Oauth2 jwt 非对称证书

创建证书

创建证书 keytool -genkeypair -alias jwt -keyalg RSA -keypass passw0rd -keystore jwt.jks -storepass passw0rd

neo@MacBook-Pro /tmp/oauth % keytool -genkeypair -alias jwt -keyalg RSA -keypass passw0rd -keystore jwt.jks -storepass passw0rd

What is your first and last name?

[Unknown]:  Neo Chen

What is the name of your organizational unit?

[Unknown]:  netkiller.cn

What is the name of your organization?

[Unknown]:  netkiller.cn

What is the name of your City or Locality?

[Unknown]:  Shenzhen

What is the name of your State or Province?

[Unknown]:  Guangdong

What is the two-letter country code for this unit?

[Unknown]:  CN

Is CN=Neo Chen, OU=netkiller.cn, O=netkiller.cn, L=Shenzhen, ST=Guangdong, C=CN correct?

[no]:  yes

该命令将生成一个名为jwt.jks的文件，其中包含我们的密钥 - 公钥和私钥。 还要牢记keypass和storepass密码。

导出公钥，接下来，我们需要从刚刚生成的JKS中导出我们的公钥，我们可以使用下面的命令来实现：

neo@MacBook-Pro /tmp/oauth % keytool -list -rfc --keystore jwt.jks | openssl x509 -inform pem -pubkey -out certificate.crt > public.crt

Enter keystore password:  passw0rd

公钥内容

-----BEGIN PUBLIC KEY-----

MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAj6ePdDwBrHKX3kNFnbve

T1rTTbyA9GjaiZNwj2X4Y0In7RCFl8auXXBn2DxztQMGqHY2Ydc3/26Gu9Vri441

r8/RInA6UpzzDRl5SeYYTobcgfIVpfQ0hTX0xzuMDVLVoLibGfcvGy7ZkrJjQFX8

lIaO84K8KP/yzma5622XJ+f5hkXmTX5e0tXGDCPjVO1dSrouPWqhcbM0Kf6y3RdE

JkNRTHLky6afx8MNobakz1Ab9K7cjD8De6LwScwMQMFU46traN/3Fw0lZFxKkpay

+sEUHvHDUYWTuVovUmfiKMX8fj5QCm4imPdA3pF/jjM+xeeVcTID3qffDGOKrGTF

HQIDAQAB

-----END PUBLIC KEY-----

复制 jwt.jks 和 public.crt 到 src/main/resources 目录下

Authorization Server

@Bean

public JwtAccessTokenConverter accessTokenConverter() {

JwtAccessTokenConverter converter = new JwtAccessTokenConverter();

KeyStoreKeyFactory keyStoreKeyFactory = new KeyStoreKeyFactory(new ClassPathResource("jwt.jks"), "passw0rd".toCharArray());

converter.setKeyPair(keyStoreKeyFactory.getKeyPair("passw0rd"));

return converter;

}

Resource Server

@Bean

public JwtAccessTokenConverter accessTokenConverter() {

JwtAccessTokenConverter converter = new JwtAccessTokenConverter();

String publicKey = "-----BEGIN PUBLIC KEY-----\n" +

"MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAj6ePdDwBrHKX3kNFnbve\n" +

"T1rTTbyA9GjaiZNwj2X4Y0In7RCFl8auXXBn2DxztQMGqHY2Ydc3/26Gu9Vri441\n" +

"r8/RInA6UpzzDRl5SeYYTobcgfIVpfQ0hTX0xzuMDVLVoLibGfcvGy7ZkrJjQFX8\n" +

"lIaO84K8KP/yzma5622XJ+f5hkXmTX5e0tXGDCPjVO1dSrouPWqhcbM0Kf6y3RdE\n" +

"JkNRTHLky6afx8MNobakz1Ab9K7cjD8De6LwScwMQMFU46traN/3Fw0lZFxKkpay\n" +

"+sEUHvHDUYWTuVovUmfiKMX8fj5QCm4imPdA3pF/jjM+xeeVcTID3qffDGOKrGTF\n" +

"HQIDAQAB\n" +

"-----END PUBLIC KEY-----";

converter.setVerifierKey(publicKey);

return converter;

}

Apple iOS 访问 Oauth2

//

//  AppDelegate.m

//

//  Created by Apple on 2018/9/8.

//  Copyright © 2018年 Apple. All rights reserved.

//

#import "AppDelegate.h"

#import "YTKNetworkConfig.h"

#import "AFOAuth2Manager.h"

#import <AFNetworking.h>

#import "NSAppConfig.h"

@interface AppDelegate ()

@property (nonatomic, strong) AFOAuthCredential *credential;

@property (nonatomic, strong) AFOAuth2Manager *OAuth2Manager;

@end

@implementation AppDelegate

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {

//授权

NSURL *url = [NSURL URLWithString:BASE_URL];

AFOAuth2Manager *OAuth2Manager = [[AFOAuth2Manager alloc]initWithBaseURL:url clientID:CLIENTID secret:SECRET];

self.OAuth2Manager = OAuth2Manager;

[OAuth2Manager authenticateUsingOAuthWithURLString:OAUTH_TOKEN

username:OAUTH_USERNAME

password:OAUTH_PASSWORD

scope:@""

success:^(AFOAuthCredential *credential) {

// NSLog(@"********请求OauthToek成功***********");

self.credential = credential;

[self testPost];

[self testGet];

}

failure:^(NSError *error) {

//  NSLog(@"********请求OauthToekn失败begin***********\r\n%@\r\n*********请求OauthToekn失败end************",error);

}];

//    YTKNetworkConfig *config = [YTKNetworkConfig sharedConfig]; config.baseUrl = @"http://yuantiku.com";

//

// Override point for customization after application launch.

return YES;

}

- (void)testGet{

NSURL *baseURL = [NSURL URLWithString:@"http://192.168.0.185:8080"];

AFHTTPSessionManager *manager =

[[AFHTTPSessionManager alloc] initWithBaseURL:baseURL];

[manager.requestSerializer setAuthorizationHeaderFieldWithCredential:self.credential];

[manager GET:@"/test/hello"

parameters:nil

progress:nil

success:^(NSURLSessionDataTask * _Nonnull task, id  _Nullable responseObject) {

NSLog(@"Success: %@", responseObject);

}

failure:^(NSURLSessionDataTask * _Nullable task, NSError * _Nonnull error) {

NSLog(@"Failure: %@", error);

}];

}

- (void)testPost{

NSURL *baseURL = [NSURL URLWithString:@"http://192.168.0.185:8080"];

AFHTTPSessionManager *manager =

[[AFHTTPSessionManager alloc] initWithBaseURL:baseURL];

manager.responseSerializer = [AFJSONResponseSerializer serializer];

manager.requestSerializer = [AFJSONRequestSerializer serializer];

[manager.requestSerializer setAuthorizationHeaderFieldWithCredential:self.credential];

NSDictionary *dic = @{@"mobile":@"13113676543",

@"password":@"123456",

@"role":@"Organization"

};

[manager POST:@"/member/create"

parameters:dic

progress:nil

success:^(NSURLSessionDataTask * _Nonnull task, id  _Nullable responseObject) {

NSLog(@"Success: %@", responseObject);

}

failure:^(NSURLSessionDataTask * _Nullable task, NSError * _Nonnull error) {

NSLog(@"Failure: %@ %@", error,task);

}];

}

- (void)applicationWillResignActive:(UIApplication *)application {

// Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state.

// Use this method to pause ongoing tasks, disable timers, and invalidate graphics rendering callbacks. Games should use this method to pause the game.

}

- (void)applicationDidEnterBackground:(UIApplication *)application {

// Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later.

// If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits.

}

- (void)applicationWillEnterForeground:(UIApplication *)application {

// Called as part of the transition from the background to the active state; here you can undo many of the changes made on entering the background.

}

- (void)applicationDidBecomeActive:(UIApplication *)application {

// Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface.

}

- (void)applicationWillTerminate:(UIApplication *)application {

// Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:.

}

@end

Post 出去的数据是 Raw 格式。

AFHTTPRequestOperationManager *manager = [AFHTTPRequestOperationManager manager];

//申明返回的结果是json类型

manager.responseSerializer = [AFJSONResponseSerializer serializer];

//申明请求的数据是json类型

manager.requestSerializer=[AFJSONRequestSerializer serializer];

//如果报接受类型不一致请替换一致text/html或别的

manager.responseSerializer.acceptableContentTypes = [NSSet setWithObject:@"text/html"];

//传入的参数

NSDictionary *parameters = @{@"1":@"XXXX",@"2":@"XXXX",@"3":@"XXXXX"};

//你的接口地址

NSString *url=@"http://xxxxx";

//发送请求

[manager POST:url parameters:parameters success:^(AFHTTPRequestOperation *operation, id responseObject) {

NSLog(@"JSON: %@", responseObject);

} failure:^(AFHTTPRequestOperation *operation, NSError *error) {

NSLog(@"Error: %@", error);

}];

Oauth2 客户端

环境：Java11 + Springboot 2.1.3

<?xml version="1.0"?>

<project xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd" xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">

<modelVersion>4.0.0</modelVersion>

<parent>

<groupId>cn.netkiller</groupId>

<artifactId>parent</artifactId>

<version>0.0.1-SNAPSHOT</version>

</parent>

<groupId>cn.netkiller</groupId>

<artifactId>oauth2-client</artifactId>

<version>0.0.1-SNAPSHOT</version>

<name>oauth2-client</name>

<url>http://maven.apache.org</url>

<properties>

<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>

</properties>

<dependencies>

<dependency>

<groupId>org.springframework.boot</groupId>

<artifactId>spring-boot-starter-oauth2-client</artifactId>

</dependency>

<dependency>

<groupId>org.springframework.boot</groupId>

<artifactId>spring-boot-starter-web</artifactId>

</dependency>

<dependency>

<groupId>junit</groupId>

<artifactId>junit</artifactId>

<scope>test</scope>

</dependency>

</dependencies>

</project>

application.yml

server:

port: 8080

APP-CLIENT-ID: 180579ba67875ffc18e3

APP-CLIENT-SECRET: 8175af8f5691e2f3b6007b9597d8c1e3499e15a0

spring:

#  redis:

#    host: localhost

security:

oauth2:

client:

provider:

netkiller:

authorization-uri: http://localhost:8080/oauth/authorize

token-uri: http://localhost:8080/oauth/token

user-info-uri: http://localhost:8080/user

user-name-attribute: name

#            token-endpoint-url: http://localhost:8080/oauth/check_token

yahoo-oidc:

issuer-uri: https://api.login.yahoo.com

registration:

netkiller:

client-id: sso

client-secret: secret

client-name: Neo

provider: netkiller

scope: read

authorization-grant-type: authorization_code

redirect-uri: http://localhost:${server.port}/login/oauth2/code/netkiller

github-client-1:

client-id: ${APP-CLIENT-ID}

client-secret: ${APP-CLIENT-SECRET}

client-name: Github user

provider: github

scope: user

redirect-uri: http://localhost:${server.port}/login/oauth2/code/github

github-client-2:

client-id: ${APP-CLIENT-ID}

client-secret: ${APP-CLIENT-SECRET}

client-name: Github email

provider: github

scope: user:email

redirect-uri: http://localhost:${server.port}/login/oauth2/code/github

yahoo-oidc:

client-id: ${YAHOO-CLIENT-ID}

client-secret: ${YAHOO-CLIENT-SECRET}

github-repos:

client-id: ${APP-CLIENT-ID}

client-secret: ${APP-CLIENT-SECRET}

scope: public_repo

redirect-uri: "{baseUrl}/github-repos"

provider: github

client-name: GitHub Repositories

logging:

level:

#    org.springframework.web: DEBUG

org.springframework.security: DEBUG

SpringApplication

package cn.netkiller.oauth2.client;

import org.springframework.boot.SpringApplication;

import org.springframework.boot.autoconfigure.EnableAutoConfiguration;

import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication

@EnableAutoConfiguration

public class Application {

public static void main(String[] args) {

System.out.println("Oauth2 Client!");

SpringApplication.run(Application.class, args);

}

}

WebSecurityConfigurer

package cn.netkiller.oauth2.client;

import org.springframework.context.annotation.Configuration;

import org.springframework.security.config.annotation.web.builders.HttpSecurity;

import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;

import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;

@Configuration

@EnableWebSecurity

public class WebSecurityConfigurer extends WebSecurityConfigurerAdapter {

@Override

protected void configure(HttpSecurity http) throws Exception {

http.authorizeRequests().anyRequest().authenticated().and().oauth2Login();

}

}

TestController

package cn.netkiller.oauth2.client;

import java.security.Principal;

import org.springframework.beans.factory.annotation.Autowired;

import org.springframework.security.oauth2.client.registration.ClientRegistration;

import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository;

import org.springframework.web.bind.annotation.RequestMapping;

import org.springframework.web.bind.annotation.RestController;

@RestController

public class TestController {

@Autowired

private ClientRegistrationRepository clientRegistrationRepository;

@RequestMapping("/")

public Principal email(Principal principal) {

System.out.println("Hello " + principal.getName());

return principal;

}

@RequestMapping("/index")

public ClientRegistration index() {

ClientRegistration clientRegistration = this.clientRegistrationRepository.findByRegistrationId("netkiller");

return clientRegistration;

}

}

Android Oauth2 + Jwt example

package cn.netkiller.okhttp.pojo;

public class Oauth {

private String access_token;

private String token_type;

private String refresh_token;

private String expires_in;

private String scope;

private String jti;

public String getAccess_token() {

return access_token;

}

public void setAccess_token(String access_token) {

this.access_token = access_token;

}

public String getToken_type() {

return token_type;

}

public void setToken_type(String token_type) {

this.token_type = token_type;

}

public String getRefresh_token() {

return refresh_token;

}

public void setRefresh_token(String refresh_token) {

this.refresh_token = refresh_token;

}

public String getExpires_in() {

return expires_in;

}

public void setExpires_in(String expires_in) {

this.expires_in = expires_in;

}

public String getScope() {

return scope;

}

public void setScope(String scope) {

this.scope = scope;

}

public String getJti() {

return jti;

}

public void setJti(String jti) {

this.jti = jti;

}

@Override

public String toString() {

return "Oauth{" +

"access_token='" + access_token + '\'' +

", token_type='" + token_type + '\'' +

", refresh_token='" + refresh_token + '\'' +

", expires_in='" + expires_in + '\'' +

", scope='" + scope + '\'' +

", jti='" + jti + '\'' +

'}';

}

}

Activity 文件

package cn.netkiller.okhttp;

import android.support.v7.app.AppCompatActivity;

import android.os.Bundle;

import android.util.Log;

import android.widget.TextView;

import com.google.gson.Gson;

import java.io.IOException;

import cn.netkiller.okhttp.pojo.Oauth;

import okhttp3.Authenticator;

import okhttp3.Call;

import okhttp3.Callback;

import okhttp3.Credentials;

import okhttp3.FormBody;

import okhttp3.OkHttpClient;

import okhttp3.Request;

import okhttp3.RequestBody;

import okhttp3.Response;

import okhttp3.Route;

public class Oauth2jwtActivity extends AppCompatActivity {

private TextView token;

@Override

protected void onCreate(Bundle savedInstanceState) {

super.onCreate(savedInstanceState);

setContentView(R.layout.activity_oauth2jwt);

token = (TextView) findViewById(R.id.token);

try {

get();

} catch (IOException e) {

e.printStackTrace();

}

}

public static Oauth accessToken() throws IOException {

OkHttpClient client = new OkHttpClient.Builder().authenticator(

new Authenticator() {

public Request authenticate(Route route, Response response) {

String credential = Credentials.basic("api", "secret");

return response.request().newBuilder().header("Authorization", credential).build();

}

}

).build();

String url = "https://api.netkiller.cn/oauth/token";

RequestBody formBody = new FormBody.Builder()

.add("grant_type", "password")

.add("username", "blockchain")

.add("password", "123456")

.build();

Request request = new Request.Builder()

.url(url)

.post(formBody)

.build();

Response response = client.newCall(request).execute();

if (!response.isSuccessful()) {

throw new IOException("服务器端错误: " + response);

}

Gson gson = new Gson();

Oauth oauth = gson.fromJson(response.body().string(), Oauth.class);

Log.i("oauth", oauth.toString());

return oauth;

}

public void get() throws IOException {

OkHttpClient client = new OkHttpClient.Builder().authenticator(

new Authenticator() {

public Request authenticate(Route route, Response response) throws IOException {

return response.request().newBuilder().header("Authorization", "Bearer " + accessToken().getAccess_token()).build();

}

}

).build();

String url = "https://api.netkiller.cn/misc/compatibility";

Request request = new Request.Builder()

.url(url)

.build();

client.newCall(request).enqueue(new Callback() {

@Override

public void onFailure(Call call, IOException e) {

call.cancel();

}

@Override

public void onResponse(Call call, Response response) throws IOException {

final String myResponse = response.body().string();

runOnUiThread(new Runnable() {

@Override

public void run() {

Log.i("oauth", myResponse);

token.setText(myResponse);

}

});

}

});

}

}

RestTemplate 使用 HttpClient

Maven

<?xml version="1.0"?>

<project xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd" xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">

<modelVersion>4.0.0</modelVersion>

<parent>

<groupId>cn.netkiller</groupId>

<artifactId>oauth</artifactId>

<version>0.0.1-SNAPSHOT</version>

</parent>

<groupId>cn.netkiller</groupId>

<artifactId>client</artifactId>

<version>0.0.1-SNAPSHOT</version>

<name>client</name>

<url>http://maven.apache.org</url>

<properties>

<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>

</properties>

<dependencies>

<dependency>

<groupId>org.apache.httpcomponents</groupId>

<artifactId>httpclient</artifactId>

<version>4.5.3</version>

</dependency>

</dependencies>

</project>

SpringBootApplication

package cn.netkiller.oauth.client;

import org.springframework.boot.SpringApplication;

import org.springframework.boot.autoconfigure.SpringBootApplication;

/**

* Hello world!

*

*/

@SpringBootApplication

public class Application {

public static void main(String[] args) {

System.out.println("Hello World!");

SpringApplication.run(Application.class, args);

}

}

ClientRestController

package cn.netkiller.oauth.client.controller;

import org.apache.http.impl.client.DefaultHttpClient;

import org.springframework.beans.factory.annotation.Autowired;

import org.springframework.beans.factory.annotation.Value;

import org.springframework.http.HttpEntity;

import org.springframework.http.HttpHeaders;

import org.springframework.http.HttpMethod;

import org.springframework.http.MediaType;

import org.springframework.http.ResponseEntity;

import org.springframework.security.oauth2.client.OAuth2RestOperations;

import org.springframework.web.bind.annotation.RequestMapping;

import org.springframework.web.bind.annotation.RestController;

import org.springframework.web.client.RestTemplate;

import com.fasterxml.jackson.databind.JsonNode;

@RestController

public class ClientRestController {

@RequestMapping("/token")

public String token() {

HttpHeaders headers = new HttpHeaders();

headers.setContentType(MediaType.APPLICATION_JSON);

headers.set("Authorization","Bearer "+"6296057a-ed06-4899-9adc-1993ba7a4946");

HttpEntity<String> entity = new HttpEntity<String>(headers);

ResponseEntity<String> response = restTemplate.exchange("http://localhost:8000/api/test/hello.json",HttpMethod.GET,entity,String.class);

System.out.println(response.getBody());

return response.getStatusCode().toString() + " " + response.getBody();

}

}

Test

首先获取 Token

MacBook-Pro:Application neo$ curl -X POST --user 'api:secret' -d 'grant_type=password&username=netkiller@msn.com&password=123456' http://localhost:8000/api/oauth/token

{"access_token":"6296057a-ed06-4899-9adc-1993ba7a4946","token_type":"bearer","refresh_token":"b22d70db-3253-4f5f-9b6a-8714da23e14d","expires_in":2642,"scope":"read write"}

将Token写入到 http 头

headers.set("Authorization","Bearer "+"6296057a-ed06-4899-9adc-1993ba7a4946");

启动 client 项目

mvn spring-boot:run

curl 测试

MacBook-Pro:Application neo$ curl http://localhost:8080/client/token.json

200 Hello world !

自签名证书信任问题

unable to find valid certification path to requested target

package example.controller;

import java.security.KeyManagementException;

import java.security.KeyStoreException;

import java.security.NoSuchAlgorithmException;

import javax.net.ssl.SSLContext;

import org.apache.http.conn.ssl.NoopHostnameVerifier;

import org.apache.http.conn.ssl.SSLConnectionSocketFactory;

import org.apache.http.impl.client.CloseableHttpClient;

import org.apache.http.impl.client.HttpClients;

import org.apache.http.ssl.SSLContexts;

import org.springframework.beans.factory.annotation.Autowired;

import org.springframework.http.HttpEntity;

import org.springframework.http.HttpHeaders;

import org.springframework.http.HttpMethod;

import org.springframework.http.MediaType;

import org.springframework.http.ResponseEntity;

import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;

import org.springframework.security.oauth2.client.OAuth2RestOperations;

import org.springframework.security.oauth2.common.OAuth2AccessToken;

import org.springframework.stereotype.Controller;

import org.springframework.web.bind.annotation.GetMapping;

import org.springframework.web.bind.annotation.ResponseBody;

import org.springframework.web.client.RestTemplate;

@Controller

public class TestController {

@Autowired

private OAuth2RestOperations restTemplate;

@GetMapping("/ssl")

@ResponseBody

public String ssl() throws KeyManagementException, NoSuchAlgorithmException, KeyStoreException {

String url = "https://api.netkiller.cn/news/list.json";

SSLContext sslcontext = SSLContexts.custom().loadTrustMaterial(null, (chain, authType) -> true).build();

SSLConnectionSocketFactory sslsf = new SSLConnectionSocketFactory(sslcontext, new String[] { "TLSv1" }, null, new NoopHostnameVerifier());

CloseableHttpClient httpClient = HttpClients.custom().setSSLSocketFactory(sslsf).build();

HttpComponentsClientHttpRequestFactory httpComponentsClientHttpRequestFactory = new HttpComponentsClientHttpRequestFactory(httpClient);

httpComponentsClientHttpRequestFactory.setConnectTimeout(60000);

httpComponentsClientHttpRequestFactory.setReadTimeout(180000);

final RestTemplate restTemplate = new RestTemplate(httpComponentsClientHttpRequestFactory);

HttpHeaders headers = new HttpHeaders();

headers.setContentType(MediaType.APPLICATION_JSON);

headers.set("Authorization", "Bearer " + this.restTemplate.getAccessToken().getValue());

HttpEntity<String> entity = new HttpEntity<String>(headers);

ResponseEntity<String> response = restTemplate.exchange(url, HttpMethod.GET, entity, String.class);

return response.getBody();

}

}

Principal

@RestController

public class ResourceController {

@GetMapping("/getResource")

public String getResource(Principal principal) {

return "SUCCESS, 当前用户：" + principal.getName();

}

@RequestMapping("/user")

public Principal user(Principal principal) {

return principal;

}

}

SecurityContextHolder 对象

@RequestMapping(value = "principal", method = RequestMethod.GET)

public Object getPrincipal() {

Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal();

return principal;

}

// 查询用户角色

@RequestMapping(value = "roles", method = RequestMethod.GET)

public Object getRoles() {

return SecurityContextHolder.getContext().getAuthentication().getAuthorities();

}

资源服务器配置

ResourceServerConfigurerAdapter

access()

@Override

public void configure(HttpSecurity http) throws Exception {

http

.httpBasic()

.and()

.authorizeRequests()

.antMatchers("/home")

.permitAll()

.and()

.authorizeRequests()

.antMatchers("/user")

.access("#oauth2.hasScope('read')")

.and()

.authorizeRequests()

.anyRequest()

.authenticated()

;

}

@Override

public void configure(HttpSecurity http) throws Exception {

super.configure(http);

http

.authorizeRequests()

.antMatchers("/login").permitAll()

.antMatchers("/info","/news")

.access("#oauth2.hasScope('read') and hasRole('USER')");

}

@Override

public void configure(ResourceServerSecurityConfigurer resources) throws Exception {

TokenStore tokenStore = new JdbcTokenStore(dataSource);

resources.resourceId("user-services").tokenStore(tokenStore).stateless(true);

}

@Override

public void configure(HttpSecurity http) throws Exception {

http

.sessionManagement()

.sessionCreationPolicy(SessionCreationPolicy.NEVER)

.and()

.requestMatchers().antMatchers("/user/**")

.and()

.authorizeRequests()

.antMatchers(HttpMethod.GET, "/user/**").access("#oauth2.hasScope('read')")

.antMatchers(HttpMethod.POST, "/user/**").access("#oauth2.hasScope('write')")

.antMatchers(HttpMethod.PATCH, "/user/**").access("#oauth2.hasScope('write')")

.antMatchers(HttpMethod.PUT, "/user/**").access("#oauth2.hasScope('write')")

.antMatchers(HttpMethod.DELETE, "/user/**").access("#oauth2.hasScope('write')")

.and()

.headers().addHeaderWriter(new HeaderWriter() {

@Override

public void writeHeaders(HttpServletRequest request, HttpServletResponse response) {

response.addHeader("Access-Control-Allow-Origin", "*");

if (request.getMethod().equals("OPTIONS")) {

response.setHeader("Access-Control-Allow-Methods", request.getHeader("Access-Control-Request-Method"));

response.setHeader("Access-Control-Allow-Headers", request.getHeader("Access-Control-Request-Headers"));

}

}

});

}

Client

Overriding Spring Boot 2.0 Auto-configuration

@Configuration

public class OAuth2LoginConfig {

@Bean

public ClientRegistrationRepository clientRegistrationRepository() {

return new InMemoryClientRegistrationRepository(this.googleClientRegistration());

}

private ClientRegistration googleClientRegistration() {

return ClientRegistration.withRegistrationId("google")

.clientId("google-client-id")

.clientSecret("google-client-secret")

.clientAuthenticationMethod(ClientAuthenticationMethod.BASIC)

.authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)

.redirectUriTemplate("{baseUrl}/login/oauth2/code/{registrationId}")

.scope("openid", "profile", "email", "address", "phone")

.authorizationUri("https://accounts.google.com/o/oauth2/v2/auth")

.tokenUri("https://www.googleapis.com/oauth2/v4/token")

.userInfoUri("https://www.googleapis.com/oauth2/v3/userinfo")

.userNameAttributeName(IdTokenClaimNames.SUB)

.jwkSetUri("https://www.googleapis.com/oauth2/v3/certs")

.clientName("Google")

.build();

}

}

Oauth2 常见问题

修改 /oauth/token 路径

@Override

public void configure(AuthorizationServerEndpointsConfigurer configurer) throws Exception {

configurer.authenticationManager(authenticationManager);

configurer.userDetailsService(userDetailsService);

configurer.pathMapping("/oauth/token","/oauth/token3"); //可以修改默认/oauth/token路径为 /oauth/token3

}

password 认证方式静态配置用户列表

@Override

public void configure(AuthorizationServerEndpointsConfigurer configurer) throws Exception {

configurer.authenticationManager(authenticationManager);

configurer.userDetailsService(userDetailsService());

}

@Bean

public UserDetailsService userDetailsService() {

Map<String, String[]> users = new HashMap<String, String[]>() {

{

put("user", new String[] { "ROLE_USER" });

put("admin", new String[] { "ROLE_USER", "ROLE_ADMIN" });

put("client", new String[] { "ROLE_CLIENT" });

put("trust", new String[] { "ROLE_TRUSTED_CLIENT" });

}

};

String password = passwordEncoder().encode("123456"); // 设置默认密码

// TODO 这里需要自行定义访问数据库的扩展

return new UserDetailsService() {

@Override

public UserDetails loadUserByUsername(String name) throws UsernameNotFoundException {

String[] authList = users.containsKey(name) ? users.get(name) : new String[] { "ROLE_USER" };

User user = new User(name, password, AuthorityUtils.createAuthorityList(authList));

return user;

}

};

}

部分 V. Spring Cloud

1. Spring Cloud Stream

第 57 章 Spring Cloud

1. Spring Cloud 相关的 application.properties 配置

启用或禁用 bootstrap

spring.cloud.bootstrap.enabled=false

spring.cloud.bootstrap.location=classpath:bootstrap.properties

bootstrap.properties 配置文件

bootstrap.properties 是优先级最高配置文件，一般用于 Spring Cloud 配置中心。

bootstrap.yml是由spring.cloud.bootstrap.name（默认:”bootstrap”）或者spring.cloud.bootstrap.location 设置（默认空）。

如果激活 profile（spring.profiles.active=development）对应配置 bootstrap-development.properties

spring.cloud.config.allowOverride=true（允许本地配置覆盖远程配置）。

spring.cloud.config.overrideNone=true 覆盖任何本地属性

spring.cloud.config.overrideSystemProperties=false 仅仅系统属性和环境变量

第 58 章 Spring Cloud Config

1. Maven 项目 pom.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>

<groupId>cn.netkiller</groupId>

<artifactId>microservice</artifactId>

<version>0.0.1-SNAPSHOT</version>

<packaging>pom</packaging>

<name>microservice</name>

<url>http://www.netkiller.cn</url>

<description>Demo project for Spring Boot</description>

<organization>

<name>Netkiller Spring Cloud 手札</name>

<url>http://www.netkiller.cn</url>

</organization>

<developers>

<developer>

<name>Neo</name>

<email>netkiller@msn.com</email>

<organization>Netkiller Spring Cloud 手札</organization>

<organizationUrl>http://www.netkiller.cn</organizationUrl>

<roles>

<role>Author</role>

</roles>

</developer>

</developers>

<properties>

<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>

<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>

<java.version>14</java.version>

<maven.compiler.source>${java.version}</maven.compiler.source>

<maven.compiler.target>${java.version}</maven.compiler.target>

<maven.compiler.release>${java.version}</maven.compiler.release>

</properties>

<parent>

<groupId>org.springframework.boot</groupId>

<artifactId>spring-boot-starter-parent</artifactId>

<version>2.3.3.RELEASE</version>

<relativePath />

</parent>

<repositories>

<repository>

<id>alimaven</id>

<name>Maven Aliyun Mirror</name>

<url>http://maven.aliyun.com/nexus/content/repositories/central/</url>

<releases>

<enabled>true</enabled>

</releases>

<snapshots>

<enabled>false</enabled>

</snapshots>

</repository>

</repositories>

<dependencyManagement>

<dependencies>

<dependency>

<groupId>org.springframework.cloud</groupId>

<artifactId>spring-cloud-dependencies</artifactId>

<version>Hoxton.SR8</version>

<type>pom</type>

<scope>import</scope>

</dependency>

</dependencies>

</dependencyManagement>

<dependencies>

<dependency>

<groupId>org.springframework.cloud</groupId>

<artifactId>spring-cloud-starter-config</artifactId>

</dependency>

<dependency>

<groupId>org.springframework.boot</groupId>

<artifactId>spring-boot-starter-actuator</artifactId>

</dependency>

<dependency>

<groupId>org.springframework.boot</groupId>

<artifactId>spring-boot-starter-test</artifactId>

<scope>test</scope>

</dependency>

<dependency>

<groupId>junit</groupId>

<artifactId>junit</artifactId>

<scope>test</scope>

</dependency>

<dependency>

<groupId>org.projectlombok</groupId>

<artifactId>lombok</artifactId>

</dependency>

</dependencies>

<modules>

<module>eureka</module>

<module>gateway</module>

<module>config</module>

<module>webflux</module>

<module>openfeign</module>

<module>restful</module>

</modules>

</project>

2. Server

Maven config 模块

<?xml version="1.0"?>

<project xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd" xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">

<modelVersion>4.0.0</modelVersion>

<parent>

<groupId>cn.netkiller</groupId>

<artifactId>microservice</artifactId>

<version>0.0.1-SNAPSHOT</version>

</parent>

<groupId>cn.netkiller</groupId>

<artifactId>config</artifactId>

<version>0.0.1-SNAPSHOT</version>

<name>config</name>

<url>http://www.netkiller.cn</url>

<description>Config project for Spring cloud</description>

<properties>

<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>

<java.version>11</java.version>

</properties>

<dependencies>

<dependency>

<groupId>org.springframework.cloud</groupId>

<artifactId>spring-cloud-config-server</artifactId>

</dependency>

<!-- <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-config-monitor</artifactId> </dependency> -->

<!-- <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> -->

</dependencies>

<build>

<plugins>

<plugin>

<groupId>org.springframework.boot</groupId>

<artifactId>spring-boot-maven-plugin</artifactId>

<configuration>

<mainClass>cn.netkiller.config.Application</mainClass>

</configuration>

</plugin>

<plugin>

<artifactId>maven-surefire-plugin</artifactId>

<configuration>

<skip>true</skip>

</configuration>

</plugin>

</plugins>

</build>

</project>

Application

Application

package cn.netkiller.config;

import org.springframework.boot.SpringApplication;

import org.springframework.boot.autoconfigure.SpringBootApplication;

import org.springframework.cloud.config.server.EnableConfigServer;

@EnableConfigServer

@SpringBootApplication

public class Application {

public static void main(String[] args) {

System.out.println("Config Server Starting...");

SpringApplication.run(Application.class, args);

}

}

application.properties

server.port=8888

spring.cloud.config.server.git.uri=https://github.com/netkiller/config.git

Git 仓库

克隆仓库

git clone https://github.com/netkiller/config.git

创建配置文件 server-development.properties

vim server-development.properties

test.a=KKOOKK

message=Hello world

提交配置文件

git commit -a

git push

测试服务器

neo@netkiller $ curl http://localhost:8888/server-development.json

{"message":"Hello world","test":{"a":"KKOOKK"}}

3. Client

Maven pom.xml

<?xml version="1.0"?>

<project xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd" xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">

<modelVersion>4.0.0</modelVersion>

<parent>

<groupId>cn.netkiller</groupId>

<artifactId>microservice</artifactId>

<version>0.0.1-SNAPSHOT</version>

</parent>

<groupId>cn.netkiller</groupId>

<artifactId>restful</artifactId>

<version>0.0.1-SNAPSHOT</version>

<name>restful</name>

<url>http://maven.apache.org</url>

<properties>

<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>

</properties>

<dependencies>

<dependency>

<groupId>org.springframework.cloud</groupId>

<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>

</dependency>

<dependency>

<groupId>org.springframework.boot</groupId>

<artifactId>spring-boot-starter-web</artifactId>

</dependency>

</dependencies>

<build>

<plugins>

<plugin>

<groupId>org.springframework.boot</groupId>

<artifactId>spring-boot-maven-plugin</artifactId>

<configuration>

<mainClass>cn.netkiller.Application</mainClass>

</configuration>

</plugin>

<plugin>

<artifactId>maven-surefire-plugin</artifactId>

<configuration>

<skip>true</skip>

</configuration>

</plugin>

</plugins>

</build>

</project>

Application

package cn.netkiller.cloud.client;

import org.springframework.beans.factory.annotation.Value;

import org.springframework.boot.SpringApplication;

import org.springframework.boot.autoconfigure.SpringBootApplication;

import org.springframework.cloud.context.config.annotation.RefreshScope;

import org.springframework.web.bind.annotation.RequestMapping;

import org.springframework.web.bind.annotation.RestController;

@SpringBootApplication

public class Application {

public static void main(String[] args) {

SpringApplication.run(Application.class, args);

}

}

@RefreshScope

@RestController

class MessageRestController {

@Value("${message:Hello default}")

private String message;

@RequestMapping("/message")

String getMessage() {

return this.message;

}

}

注意 @RefreshScope 注解

bootstrap.properties

spring.application.name=server-development

spring.cloud.config.uri=http://localhost:8888

management.security.enabled=false

测试 client

neo@netkiller $ curl http://localhost:8080/message.json

Hello world

4. Config 高级配置

仓库配置

配置文件格式

/{application}/{profile}[/{label}]

/{application}-{profile}.yml

/{label}/{application}-{profile}.yml

/{application}-{profile}.properties

/{label}/{application}-{profile}.properties

{application} 映射到客户端的"spring.application.name" 或 “spring.cloud.config.name”;

{profile}映射到客户端上的"spring.profiles.active" 或 “spring.cloud.config.profile”;

{label} 是可选的 git 标签，默认 master；

nickname: netkilleriMac:Java neo$ curl http://localhost:8769/webflux-dev.json

{"name":"Neo","nickname":"netkiller"}

iMac:Java neo$ curl http://localhost:8769/webflux-dev.properties

name: Neo

nickname: netkiller

iMac:Java neo$ curl http://localhost:8769/webflux-dev.yml

name: Neo

nickname: netkiller

iMac:Java neo$ curl http://localhost:8769/webflux-dev.yaml

name: Neo

nickname: netkiller

分支

label 是指 git 的分支

spring.cloud.config.label=mybranch

basedir

spring.cloud.config.server.git.basedir=api/configs

HTTP Auth

spring.application.name=config-server

spring.cloud.config.server.git.uri=https://netkiller:xxxxxx@github.com/xyz/microservices-configs.git

spring.application.name=config-server

spring.cloud.config.server.git.uri=https://github.com/xyz/microservices-configs.git

spring.cloud.config.server.git.username=netkiller

spring.cloud.config.server.git.password=password

本地git仓库

创建本地仓库

mkdir ~/config

cd config

git init

git config --global user.email "neo.chen@live.com"

git config --global user.name "Neo Chen"

创建测试配置文件

# cat app-test.properties

name=neo

age=10

提交配置文件

git add app-test.properties

git commit -a

检查文件是否提交成功

[root@netkiller config]# git log

commit aee6c35bacf1740004e02f8ecdcf2fd322422405

Author: Neo Chen <neo.chen@live.com>

Date:   Thu Nov 2 14:18:48 2017 +0800

new file:   app-test.properties

配置 Spring cloud config 服务器，修改 application.properties 文件

server.port=8888

#spring.cloud.config.server.git.uri=/opt/config

spring.cloud.config.server.git.uri= file://${user.home}/config

security.user.name=cfg

security.user.password=s3cr3t

## Spring cloud GIT Repository file

${user.home}/config/root-server.properties

检验配置中心

[root@netkiller config]# curl http://cfg:test@localhost:8888/app-test.properties

age: 10

name: neo

native 本地配置

载入本地配置文件 resources/shared/config-client-dev.yml

server:

port: 8762

foo: foo version 1

配置中心服务端 resources/application.yml 配置文件

server:

port: 8769

spring:

application:

name: config-server

profiles:

active: native

cloud:

config:

server:

native:

search-locations: classpath:/shared

测试配置文件

iMac:Java neo$ curl http://localhost:8769/config-client-dev.json

{"server":{"port":8762},"foo":"foo version 1"}

Config server 用户认证

Server 配置

application.properties

server.port=8888

spring.cloud.config.server.git.uri=ssh://localhost/config-repo

spring.cloud.config.server.git.clone-on-start=true

security.user.name=cfg

security.user.password=s3cr3t

Maven

<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>

<groupId>cn.netkiller</groupId>

<artifactId>config</artifactId>

<version>0.0.1-SNAPSHOT</version>

<packaging>jar</packaging>

<name>config</name>

<url>http://maven.apache.org</url>

<properties>

<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>

<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>

<java.version>1.8</java.version>

</properties>

<parent>

<groupId>org.springframework.boot</groupId>

<artifactId>spring-boot-starter-parent</artifactId>

<version>1.5.7.RELEASE</version>

<relativePath />

</parent>

<dependencyManagement>

<dependencies>

<dependency>

<groupId>org.springframework.cloud</groupId>

<artifactId>spring-cloud-config</artifactId>

<version>1.3.1.RELEASE</version>

<type>pom</type>

<scope>import</scope>

</dependency>

</dependencies>

</dependencyManagement>

<dependencies>

<dependency>

<groupId>org.springframework.cloud</groupId>

<artifactId>spring-cloud-config-server</artifactId>

</dependency>

<dependency>

<groupId>org.springframework.boot</groupId>

<artifactId>spring-boot-starter-security</artifactId>

</dependency>

<dependency>

<groupId>org.springframework.boot</groupId>

<artifactId>spring-boot-starter-test</artifactId>

<scope>test</scope>

</dependency>

</dependencies>

<build>

<plugins>

<plugin>

<groupId>org.springframework.boot</groupId>

<artifactId>spring-boot-maven-plugin</artifactId>

</plugin>

</plugins>

</build>

</project>

测试是否生效

neo@MacBook-Pro ~/deployment % curl http://cfg:s3cr3t@localhost:8888/neo-development.json

{"message":"Hello world","test":{"name":"neo"}}

Client 配置

bootstrap.properties:

spring.application.name=project

spring.profiles.active=development

spring.cloud.config.uri=http://localhost:8888

spring.cloud.config.username=cfg

spring.cloud.config.password=s3cr3t

加密敏感数据

Config server 创建证书

keytool -genkeypair -alias config-server-key \

-keyalg RSA -keysize 4096 -sigalg SHA512withRSA \

-dname 'CN=Config Server,OU=Spring Cloud,O=Netkiller' \

-keypass s3cr3t -keystore config-server.jks \

-storepass passw0rd

application.properties 中配置证书

# spring.cloud.config.server.encrypt.enabled=true

encrypt.key-store.location=classpath:/config-server.jks

encrypt.key-store.alias=config-server-key

encrypt.key-store.secret=s3cr3t

encrypt.key-store.password=passw0rd

测试加密

curl -X POST --data-urlencode mypassword http://localhost:8888/encrypt

$ PASSWORD=$(curl -X POST --data-urlencode passw0rd http://cfg:s3cr3t@localhost:8888/encrypt)

$ echo "user.password=$PASSWORD" >> api-interface-development.properties

$ git commit -am 'Added encrypted password'

# 刷新配置

$ curl -X POST http://cfg:s3cr3t@localhost:8888/refresh

Spring Cloud Config JDBC Backend

Maven pom.xml

<parent>

<groupId>org.springframework.boot</groupId>

<artifactId>spring-boot-starter-parent</artifactId>

<version>1.5.11.RELEASE</version>

<relativePath/>

</parent>

<dependencies>

<dependency>

<groupId>org.springframework.cloud</groupId>

<artifactId>spring-cloud-config-server</artifactId>

</dependency>

<dependency>

<groupId>org.springframework.boot</groupId>

<artifactId>spring-boot-starter-jdbc</artifactId>

</dependency>

<dependency>

<groupId>org.flywaydb</groupId>

<artifactId>flyway-core</artifactId>

<version>5.0.3</version>

</dependency>

<dependency>

<groupId>mysql</groupId>

<artifactId>mysql-connector-java</artifactId>

<version>5.1.21</version>

</dependency>

</dependencies>

<dependencyManagement>

<dependencies>

<dependency>

<groupId>org.springframework.cloud</groupId>

<artifactId>spring-cloud-dependencies</artifactId>

<version>Edgware.SR3</version>

<type>pom</type>

<scope>import</scope>

</dependency>

</dependencies>

</dependencyManagement>

数据库表结构

CREATE TABLE `properties` (

`key` varchar(50) NOT NULL,

`value` varchar(500) NOT NULL,

`application` varchar(50) NOT NULL,

`profile` varchar(50) NOT NULL,

`label` varchar(50) NOT NULL,

PRIMARY KEY (`KEY`, `APPLICATION`, `PROFILE`, `LABEL`)

) ENGINE=InnoDB DEFAULT CHARSET=utf8;

Config 服务器

@EnableConfigServer

@SpringBootApplication

public class Application {

//@Autowired

//JdbcTemplate jdbcTemplate;

public static void main(String[] args) {

SpringApplication.run(Application.class, args);

// 测试用数据，仅用于本文测试使用

JdbcTemplate jdbcTemplate = context.getBean(JdbcTemplate.class);

jdbcTemplate.execute("delete from properties");

jdbcTemplate.execute("INSERT INTO properties VALUES('neo.message', 'helloworld', 'api', 'stage', 'master')");

jdbcTemplate.execute("INSERT INTO properties VALUES('neo.message', 'helloworld', 'new', 'online', 'master')");

jdbcTemplate.execute("INSERT INTO properties VALUES('neo.message', 'helloworld', 'test', 'online', 'develop')");

jdbcTemplate.execute("INSERT INTO properties VALUES('neo.message', 'helloworld', 'cms', 'online', 'master')");

}

}

application.properties

spring.profiles.active=jdbc 将配置中心的存储实现切换到jdbc的方式, 必须设置。

server.port=8888

spring.profiles.active=jdbc

spring.datasource.driver-class-name=com.mysql.jdbc.Driver

spring.datasource.url=jdbc:mysql://localhost:3306/config-server-db

spring.datasource.username=root

spring.datasource.password=xxxx

# or

spring.datasource.driver-class-name=org.postgresql.Driver

spring.datasource.url= jdbc:postgresql://localhost:5432/configdb

spring.datasource.username=xxxxxx

spring.datasource.password=xxxxxx

5. Old

Server (Camden.SR5)

Maven pom.xml 请使用最新版本 1.3.1, 下面的 maven 是早期 Camden.SR5 版本的配置

<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>

<groupId>netkiller.cn</groupId>

<artifactId>cloud</artifactId>

<version>0.0.1-SNAPSHOT</version>

<name>Neo</name>

<description>http://www.netkiller.cn</description>

<packaging>jar</packaging>

<parent>

<groupId>org.springframework.boot</groupId>

<artifactId>spring-boot-starter-parent</artifactId>

<version>1.5.3.RELEASE</version>

<relativePath />

</parent>

<properties>

<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>

<java.version>1.8</java.version>

</properties>

<dependencies>

<dependency>

<groupId>org.springframework.cloud</groupId>

<artifactId>spring-cloud-config-server</artifactId>

</dependency>

<dependency>

<groupId>org.springframework.boot</groupId>

<artifactId>spring-boot-starter-test</artifactId>

<scope>test</scope>

</dependency>

</dependencies>

<dependencyManagement>

<dependencies>

<dependency>

<groupId>org.springframework.cloud</groupId>

<artifactId>spring-cloud-dependencies</artifactId>

<version>Camden.SR5</version>

<type>pom</type>

<scope>import</scope>

</dependency>

</dependencies>

</dependencyManagement>

<build>

<plugins>

<plugin>

<groupId>org.springframework.boot</groupId>

<artifactId>spring-boot-maven-plugin</artifactId>

</plugin>

</plugins>

</build>

</project>

Client (Camden.SR5)

Maven pom.xml Camden.SR5 为早期版本，尽可以使用新版

<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>

<groupId>netkiller.cn</groupId>

<artifactId>cloud</artifactId>

<version>0.0.1-SNAPSHOT</version>

<parent>

<groupId>org.springframework.boot</groupId>

<artifactId>spring-boot-starter-parent</artifactId>

<version>1.5.2.RELEASE</version>

<relativePath />

</parent>

<properties>

<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>

<java.version>1.8</java.version>

</properties>

<dependencies>

<dependency>

<groupId>org.springframework.cloud</groupId>

<artifactId>spring-cloud-starter-config</artifactId>

</dependency>

<dependency>

<groupId>org.springframework.boot</groupId>

<artifactId>spring-boot-starter-actuator</artifactId>

</dependency>

<dependency>

<groupId>org.springframework.boot</groupId>

<artifactId>spring-boot-starter-web</artifactId>

</dependency>

<dependency>

<groupId>org.springframework.boot</groupId>

<artifactId>spring-boot-starter-test</artifactId>

<scope>test</scope>

</dependency>

</dependencies>

<dependencyManagement>

<dependencies>

<dependency>

<groupId>org.springframework.cloud</groupId>

<artifactId>spring-cloud-dependencies</artifactId>

<version>Camden.SR5</version>

<type>pom</type>

<scope>import</scope>

</dependency>

</dependencies>

</dependencyManagement>

<build>

<plugins>

<plugin>

<groupId>org.springframework.boot</groupId>

<artifactId>spring-boot-maven-plugin</artifactId>

</plugin>

</plugins>

</build>

</project>

第 59 章 Spring Cloud Consol

1. Spring Cloud Consul 配置

核心参数

配置项 默认值

spring.cloud.consul.enabled true

spring.cloud.consul.host localhost

spring.cloud.consul.port 8500

服务发现参数

配置项 默认值

spring.cloud.consul.discovery.acl-token

spring.cloud.consul.discovery.catalog-services-watch-delay 10

spring.cloud.consul.discovery.catalog-services-watch-timeout 2

spring.cloud.consul.discovery.datacenters

spring.cloud.consul.discovery.default-query-tag

spring.cloud.consul.discovery.default-zone-metadata-name zone

spring.cloud.consul.discovery.deregister true

spring.cloud.consul.discovery.enabled true

spring.cloud.consul.discovery.fail-fast true

spring.cloud.consul.discovery.health-check-critical-timeout

spring.cloud.consul.discovery.health-check-interval 10s

spring.cloud.consul.discovery.health-check-path /actuator/health

spring.cloud.consul.discovery.health-check-timeout

spring.cloud.consul.discovery.health-check-tls-skip-verify

spring.cloud.consul.discovery.health-check-url

spring.cloud.consul.discovery.heartbeat.enabled false

spring.cloud.consul.discovery.heartbeat.interval-ratio

spring.cloud.consul.discovery.heartbeat.ttl-unit s

spring.cloud.consul.discovery.heartbeat.ttl-value 30

spring.cloud.consul.discovery.hostname

spring.cloud.consul.discovery.instance-group

spring.cloud.consul.discovery.instance-id 默认为服务名+环境+端口号

spring.cloud.consul.discovery.instance-zone

spring.cloud.consul.discovery.ip-address

spring.cloud.consul.discovery.lifecycle.enabled true

spring.cloud.consul.discovery.management-port

spring.cloud.consul.discovery.management-suffix management

spring.cloud.consul.discovery.management-tags

spring.cloud.consul.discovery.port

spring.cloud.consul.discovery.prefer-agent-address false

spring.cloud.consul.discovery.prefer-ip-address false

spring.cloud.consul.discovery.query-passing false

spring.cloud.consul.discovery.register true

spring.cloud.consul.discovery.register-health-check true

spring.cloud.consul.discovery.scheme http

spring.cloud.consul.discovery.server-list-query-tags

spring.cloud.consul.discovery.service-name

spring.cloud.consul.discovery.tags

spring.cloud.consul.discovery.serviceName 是指注册到 Consul 的服务名称，后期客户端会根据这个名称来进行服务调用。

配置服务参数

配置项 默认值

spring.cloud.consul.config.enabled true

spring.cloud.consul.config.prefix config

spring.cloud.consul.config.default-context application

spring.cloud.consul.config.profile-separator ,

spring.cloud.consul.config.data-key data

spring.cloud.consul.config.format KEY_VALUE, PROPERTIES, YAML, FILES

spring.cloud.consul.config.name ${spring.application.name}

spring.cloud.consul.config.acl-token

spring.cloud.consul.config.fail-fast false

spring.cloud.consul.config.watch.enabled true

spring.cloud.consul.config.watch.wait-time 55

spring.cloud.consul.config.watch.delay 1000

2. Maven 父项目

<?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>

<groupId>cn.netkiller</groupId>

<artifactId>parent</artifactId>

<version>0.0.1-SNAPSHOT</version>

<packaging>pom</packaging>

<name>demo</name>

<description>Demo project for Spring Boot</description>

<url>http://www.netkiller.cn</url>

<organization>

<name>Netkiller Spring Cloud 手札</name>

<url>http://www.netkiller.cn</url>

</organization>

<developers>

<developer>

<name>Neo</name>

<email>netkiller@msn.com</email>

<organization>Netkiller Spring Cloud 手札</organization>

<organizationUrl>http://www.netkiller.cn</organizationUrl>

<roles>

<role>Author</role>

</roles>

</developer>

</developers>

<!--使用aliyun镜像 -->

<repositories>

<repository>

<id>alimaven</id>

<name>Maven Aliyun Mirror</name>

<url>http://maven.aliyun.com/nexus/content/repositories/central/</url>

<releases>

<enabled>true</enabled>

</releases>

<snapshots>

<enabled>false</enabled>

</snapshots>

</repository>

</repositories>

<properties>

<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>

<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>

<java.version>1.8</java.version>

<spring-cloud.version>Greenwich.SR1</spring-cloud.version>

</properties>

<parent>

<groupId>org.springframework.boot</groupId>

<artifactId>spring-boot-starter-parent</artifactId>

<version>2.1.3.RELEASE</version>

<relativePath />

</parent>

<dependencyManagement>

<dependencies>

<dependency>

<groupId>org.springframework.cloud</groupId>

<artifactId>spring-cloud-dependencies</artifactId>

<version>${spring-cloud.version}</version>

<type>pom</type>

<scope>import</scope>

</dependency>

</dependencies>

</dependencyManagement>

<dependencies>

<dependency>

<groupId>org.springframework.boot</groupId>

<artifactId>spring-boot-starter-test</artifactId>

<scope>test</scope>

</dependency>

<dependency>

<groupId>junit</groupId>

<artifactId>junit</artifactId>

<scope>test</scope>

</dependency>

</dependencies>

<modules>

<module>consol-producer</module>

<module>consol-consumer</module>

<module>consol-config</module>

<module>openfeign</module>

</modules>

<build>

<plugins>

<plugin>

<groupId>org.springframework.boot</groupId>

<artifactId>spring-boot-maven-plugin</artifactId>

</plugin>

</plugins>

</build>

</project>

3. Consul 服务生产者

Maven

<?xml version="1.0"?>

<project xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd" xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">

<modelVersion>4.0.0</modelVersion>

<parent>

<groupId>cn.netkiller</groupId>

<artifactId>parent</artifactId>

<version>0.0.1-SNAPSHOT</version>

</parent>

<!-- <groupId>cn.netkiller</groupId> -->

<artifactId>consul-producer</artifactId>

<!-- <version>0.0.1-SNAPSHOT</version> -->

<name>consul-producer</name>

<url>http://www.netkiller.cn</url>

<description>Spring Cloud Consul Sample</description>

<properties>

<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>

<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>

<java.version>11</java.version>

</properties>

<dependencies>

<dependency>

<groupId>org.springframework.boot</groupId>

<artifactId>spring-boot-starter-actuator</artifactId>

</dependency>

<dependency>

<groupId>org.springframework.cloud</groupId>

<artifactId>spring-cloud-starter-consul-discovery</artifactId>

</dependency>

<dependency>

<groupId>org.springframework.boot</groupId>

<artifactId>spring-boot-starter-web</artifactId>

</dependency>

<dependency>

<groupId>org.projectlombok</groupId>

<artifactId>lombok</artifactId>

<!-- Only needed at compile time -->

<optional>true</optional>

</dependency>

<dependency>

<groupId>junit</groupId>

<artifactId>junit</artifactId>

<!-- <version>3.8.1</version> -->

<scope>test</scope>

</dependency>

</dependencies>

<build>

<plugins>

<plugin>

<groupId>org.springframework.boot</groupId>

<artifactId>spring-boot-maven-plugin</artifactId>

<!-- <configuration> <mainClass>cn.netkiller.config.Application</mainClass> </configuration> -->

</plugin>

<plugin>

<artifactId>maven-surefire-plugin</artifactId>

<configuration>

<skip>true</skip>

</configuration>

</plugin>

</plugins>

</build>

</project>

application.properties

server.port=8080

spring.application.name=spring-cloud-consul-producer

spring.cloud.consul.host=192.168.4.217

spring.cloud.consul.port=8500

logging.level.org.springframework.cloud.consul=DEBUG

SpringApplication

package cn.netkiller.consul;

import org.springframework.boot.SpringApplication;

import org.springframework.boot.autoconfigure.SpringBootApplication;

import org.springframework.cloud.client.discovery.EnableDiscoveryClient;

@SpringBootApplication

@EnableDiscoveryClient

public class Application {

public static void main(String[] args) {

System.out.println("Hello World!");

SpringApplication.run(Application.class, args);

}

}

TestController

package cn.netkiller.consul.controller;

import org.springframework.web.bind.annotation.GetMapping;

import org.springframework.web.bind.annotation.RequestMapping;

import org.springframework.web.bind.annotation.RequestParam;

import org.springframework.web.bind.annotation.RestController;

@RestController

public class TestController {

public TestController() {

// TODO Auto-generated constructor stub

}

@GetMapping("/hello")

public String provider() {

return "Helloworld!!!";

}

@RequestMapping("/hi")

public String hi(@RequestParam(name = "name") String name) {

return "hi " + name + "!";

}

}

4. Consul 服务消费者

Maven

<?xml version="1.0"?>

<project xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd" xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">

<modelVersion>4.0.0</modelVersion>

<parent>

<groupId>cn.netkiller</groupId>

<artifactId>parent</artifactId>

<version>0.0.1-SNAPSHOT</version>

</parent>

<groupId>cn.netkiller</groupId>

<artifactId>consul-consumer</artifactId>

<version>0.0.1-SNAPSHOT</version>

<name>consul-consumer</name>

<url>http://www.netkiller.cn</url>

<properties>

<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>

<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>

<java.version>11</java.version>

</properties>

<dependencies>

<dependency>

<groupId>org.springframework.cloud</groupId>

<artifactId>spring-cloud-starter-consul-discovery</artifactId>

</dependency>

<dependency>

<groupId>org.springframework.boot</groupId>

<artifactId>spring-boot-starter-web</artifactId>

</dependency>

<dependency>

<groupId>junit</groupId>

<artifactId>junit</artifactId>

<version>3.8.1</version>

<scope>test</scope>

</dependency>

</dependencies>

<build>

<plugins>

<plugin>

<groupId>org.springframework.boot</groupId>

<artifactId>spring-boot-maven-plugin</artifactId>

<!-- <configuration> <mainClass>cn.netkiller.config.Application</mainClass> </configuration> -->

</plugin>

<plugin>

<artifactId>maven-surefire-plugin</artifactId>

<configuration>

<skip>true</skip>

</configuration>

</plugin>

</plugins>

</build>

</project>

application.properties

server.port=8082

spring.application.name=spring-cloud-consul-consumer

spring.cloud.consul.host=192.168.4.217

spring.cloud.consul.port=8500

#设置不需要注册到 consul 中

spring.cloud.consul.discovery.register=false

SpringApplication

package cn.netkiller.consul.consumer;

import org.springframework.boot.SpringApplication;

import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication

public class Application {

public static void main(String[] args) {

System.out.println("Consol Consumer!");

SpringApplication.run(Application.class, args);

}

}

TestController

package cn.netkiller.consul.consumer;

import org.springframework.beans.factory.annotation.Autowired;

import org.springframework.cloud.client.ServiceInstance;

import org.springframework.cloud.client.discovery.DiscoveryClient;

import org.springframework.cloud.client.loadbalancer.LoadBalancerClient;

import org.springframework.web.bind.annotation.RequestMapping;

import org.springframework.web.bind.annotation.RestController;

import org.springframework.web.client.RestTemplate;

@RestController

public class TestController {

public ConsumerController() {

// TODO Auto-generated constructor stub

}

@Autowired

private LoadBalancerClient loadBalancerClient;

@Autowired

private DiscoveryClient discoveryClient;

/**

* 获取所有服务

*/

@RequestMapping("/services")

public Object services() {

return discoveryClient.getInstances("spring-cloud-consul-producer");

}

/**

* 从所有服务中选择一个服务（轮询）

*/

@RequestMapping("/discover")

public Object discover() {

return loadBalancerClient.choose("spring-cloud-consul-producer").getUri().toString();

}

@RequestMapping("/call")

public String call() {

ServiceInstance serviceInstance = loadBalancerClient.choose("spring-cloud-consul-producer");

System.out.println("服务地址：" + serviceInstance.getUri());

System.out.println("服务名称：" + serviceInstance.getServiceId());

String callServiceResult = new RestTemplate().getForObject(serviceInstance.getUri().toString() + "/hello", String.class);

System.out.println(callServiceResult);

return callServiceResult;

}

}

5. Openfeign

Maven

<?xml version="1.0"?>

<project xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd" xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">

<modelVersion>4.0.0</modelVersion>

<parent>

<groupId>cn.netkiller</groupId>

<artifactId>parent</artifactId>

<version>0.0.1-SNAPSHOT</version>

</parent>

<groupId>cn.netkiller</groupId>

<artifactId>openfeign</artifactId>

<version>0.0.1-SNAPSHOT</version>

<name>openfeign</name>

<url>http://www.netkiller.cn</url>

<properties>

<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>

<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>

<java.version>11</java.version>

</properties>

<dependencies>

<dependency>

<groupId>org.springframework.cloud</groupId>

<artifactId>spring-cloud-starter-consul-discovery</artifactId>

</dependency>

<dependency>

<groupId>org.springframework.cloud</groupId>

<artifactId>spring-cloud-starter-openfeign</artifactId>

</dependency>

<dependency>

<groupId>org.springframework.boot</groupId>

<artifactId>spring-boot-starter-web</artifactId>

</dependency>

<dependency>

<groupId>junit</groupId>

<artifactId>junit</artifactId>

<version>3.8.1</version>

<scope>test</scope>

</dependency>

</dependencies>

<build>

<plugins>

<plugin>

<groupId>org.springframework.boot</groupId>

<artifactId>spring-boot-maven-plugin</artifactId>

<!-- <configuration> <mainClass>cn.netkiller.config.Application</mainClass> </configuration> -->

</plugin>

<plugin>

<artifactId>maven-surefire-plugin</artifactId>

<configuration>

<skip>true</skip>

</configuration>

</plugin>

</plugins>

</build>

</project>

application.properties

server.port=8083

spring.application.name=spring-cloud-consul-openfeign

spring.cloud.consul.host=192.168.4.217

spring.cloud.consul.port=8500

spring.cloud.consul.discovery.register=false

SpringApplication

package cn.netkiller.openfeign;

import org.springframework.boot.SpringApplication;

import org.springframework.boot.autoconfigure.SpringBootApplication;

import org.springframework.cloud.client.discovery.EnableDiscoveryClient;

import org.springframework.cloud.openfeign.EnableFeignClients;

@SpringBootApplication

@EnableDiscoveryClient

@EnableFeignClients

public class Application {

public static void main(String[] args) {

System.out.println("openfeign!");

SpringApplication.run(Application.class, args);

}

}

Feign 接口

package cn.netkiller.openfeign;

import org.springframework.cloud.openfeign.FeignClient;

import org.springframework.web.bind.annotation.RequestMapping;

import org.springframework.web.bind.annotation.RequestMethod;

import org.springframework.web.bind.annotation.RequestParam;

@FeignClient(value = "spring-cloud-consul-producer", fallback = FeignFallback.class)

public interface TestFeign {

@RequestMapping(value = "/hi", method = RequestMethod.GET)

String hi(@RequestParam(value = "name") String name);

}

package cn.netkiller.openfeign;

public class FeignFallback implements TestFeign {

@Override

public String hi(String name) {

return "sorry,熔断介入";

}

}

TestController

package cn.netkiller.openfeign;

import org.springframework.beans.factory.annotation.Autowired;

import org.springframework.web.bind.annotation.RequestMapping;

import org.springframework.web.bind.annotation.RequestParam;

import org.springframework.web.bind.annotation.RestController;

@RestController

public class TestController {

@Autowired

TestFeign testFeign;

@RequestMapping("/feign")

public String testFeign(@RequestParam(name = "name") String name) {

return testFeign.hi(name);

}

}

第 60 章 Spring Cloud Netflix

1. Eureka Server

Maven

<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>

<groupId>cn.netkiller.spring.cloud</groupId>

<artifactId>netflix.eureka.server</artifactId>

<version>0.0.1-SNAPSHOT</version>

<packaging>jar</packaging>

<name>eureka.server</name>

<url>http://maven.apache.org</url>

<properties>

<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>

</properties>

<parent>

<groupId>org.springframework.boot</groupId>

<artifactId>spring-boot-starter-parent</artifactId>

<version>1.5.7.RELEASE</version>

<relativePath />

</parent>

<dependencyManagement>

<dependencies>

<dependency>

<groupId>org.springframework.cloud</groupId>

<artifactId>spring-cloud-netflix</artifactId>

<version>1.3.5.RELEASE</version>

<type>pom</type>

<scope>import</scope>

</dependency>

</dependencies>

</dependencyManagement>

<dependencies>

<dependency>

<groupId>org.springframework.boot</groupId>

<artifactId>spring-boot-starter-test</artifactId>

<scope>test</scope>

</dependency>

<dependency>

<groupId>org.springframework.cloud</groupId>

<artifactId>spring-cloud-starter-eureka-server</artifactId>

</dependency>

</dependencies>

<build>

<plugins>

<plugin>

<groupId>org.apache.maven.plugins</groupId>

<artifactId>maven-surefire-plugin</artifactId>

<configuration>

<skip>true</skip>

</configuration>

</plugin>

</plugins>

</build>

</project>

Application

package cn.netkiller.spring.cloud.netflix.eureka.server;

import org.springframework.boot.SpringApplication;

import org.springframework.boot.autoconfigure.SpringBootApplication;

import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer;

@SpringBootApplication

@EnableEurekaServer

public class Application {

public static void main(String[] args) {

System.out.println("Hello World!");

// new SpringApplicationBuilder(Application.class).web(true).run(args);

SpringApplication.run(Application.class, args);

}

}

application.properties

server.port=8761

eureka.client.register-with-eureka=false

eureka.client.fetch-registry=false

eureka.client.serviceUrl.defaultZone=http://localhost:${server.port}/eureka/

logging.level.com.netflix.eureka=OFF

logging.level.com.netflix.discovery=OFF

检查注册服务器

http://localhost:8761

2. Eureka Client

Maven

<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>

<groupId>cn.netkiller.spring.cloud</groupId>

<artifactId>eureka.client</artifactId>

<version>0.0.1-SNAPSHOT</version>

<packaging>jar</packaging>

<name>eureka.client</name>

<url>http://maven.apache.org</url>

<properties>

<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>

</properties>

<parent>

<groupId>org.springframework.boot</groupId>

<artifactId>spring-boot-starter-parent</artifactId>

<version>1.5.7.RELEASE</version>

<relativePath />

</parent>

<dependencyManagement>

<dependencies>

<dependency>

<groupId>org.springframework.cloud</groupId>

<artifactId>spring-cloud-netflix</artifactId>

<version>1.3.5.RELEASE</version>

<type>pom</type>

<scope>import</scope>

</dependency>

</dependencies>

</dependencyManagement>

<dependencies>

<dependency>

<groupId>org.springframework.cloud</groupId>

<artifactId>spring-cloud-starter-config</artifactId>

</dependency>

<dependency>

<groupId>org.springframework.cloud</groupId>

<artifactId>spring-cloud-starter-eureka</artifactId>

</dependency>

</dependencies>

<build>

<plugins>

<plugin>

<groupId>org.apache.maven.plugins</groupId>

<artifactId>maven-surefire-plugin</artifactId>

<configuration>

<skip>true</skip>

</configuration>

</plugin>

</plugins>

</build>

</project>

Application

package cn.netkiller.spring.cloud.eureka.client;

import org.springframework.boot.SpringApplication;

import org.springframework.boot.autoconfigure.EnableAutoConfiguration;

import org.springframework.cloud.netflix.eureka.EnableEurekaClient;

import org.springframework.context.annotation.Configuration;

import org.springframework.web.bind.annotation.RequestMapping;

import org.springframework.web.bind.annotation.RestController;

@Configuration

@EnableAutoConfiguration

@EnableEurekaClient

@RestController

public class Application {

@RequestMapping("/")

public String home() {

return "Hello World";

}

public static void main(String[] args) {

SpringApplication.run(Application.class, args);

}

}

RestController

package cn.netkiller.spring.cloud.eureka.client;

import java.util.List;

import org.slf4j.Logger;

import org.slf4j.LoggerFactory;

import org.springframework.beans.factory.annotation.Autowired;

import org.springframework.cloud.client.ServiceInstance;

import org.springframework.cloud.client.discovery.DiscoveryClient;

import org.springframework.web.bind.annotation.PathVariable;

import org.springframework.web.bind.annotation.RequestMapping;

import org.springframework.web.bind.annotation.RequestMethod;

import org.springframework.web.bind.annotation.RequestParam;

import org.springframework.web.bind.annotation.RestController;

@RestController

public class TestRestController {

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

@RequestMapping("/")

public String version() {

logger.info("Hello!!!");

return "Version: v1.0.0";

}

@RequestMapping(value = "/add", method = RequestMethod.GET)

public Integer add(@RequestParam Integer a, @RequestParam Integer b) {

Integer r = a + b;

return r;

}

@RequestMapping("/greeting")

public String greeting() {

return "GREETING";

}

}

application.properties

spring.application.name=test-service

server.port=8080

eureka.client.serviceUrl.defaultZone=http://localhost:8761/eureka/

测试

首先确认客户端已经注册到 http://localhost:8761/

你可以启动很多 Eureka 客户端，相同的 spring.application.name 会归为一组，为用户提供负载均衡。

neo@MacBook-Pro ~ % curl http://localhost:8080/

Hello World

add 接口测试

curl http://localhost:8080/add.json?a=5&b=3

8

3. Feign client

Maven

<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>

<groupId>cn.netkiller.spring.cloud.netflix</groupId>

<artifactId>feign.client</artifactId>

<version>0.0.1-SNAPSHOT</version>

<packaging>jar</packaging>

<name>feign.client</name>

<url>http://maven.apache.org</url>

<properties>

<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>

</properties>

<parent>

<groupId>org.springframework.boot</groupId>

<artifactId>spring-boot-starter-parent</artifactId>

<version>1.5.3.RELEASE</version>

<relativePath />

</parent>

<dependencies>

<dependency>

<groupId>org.springframework.boot</groupId>

<artifactId>spring-boot-starter-test</artifactId>

<scope>test</scope>

</dependency>

<dependency>

<groupId>org.springframework.boot</groupId>

<artifactId>spring-boot-starter-web</artifactId>

</dependency>

<dependency>

<groupId>org.springframework.cloud</groupId>

<artifactId>spring-cloud-starter-eureka</artifactId>

<version>1.3.1.RELEASE</version>

</dependency>

<dependency>

<groupId>org.springframework.cloud</groupId>

<artifactId>spring-cloud-starter-feign</artifactId>

<version>1.3.1.RELEASE</version>

</dependency>

</dependencies>

<build>

<plugins>

<plugin>

<groupId>org.apache.maven.plugins</groupId>

<artifactId>maven-surefire-plugin</artifactId>

<configuration>

<skip>true</skip>

</configuration>

</plugin>

</plugins>

</build>

</project>

Application

package cn.netkiller.spring.cloud.netflix.feign.client;

import org.springframework.beans.factory.annotation.Autowired;

import org.springframework.boot.SpringApplication;

import org.springframework.boot.autoconfigure.SpringBootApplication;

import org.springframework.cloud.netflix.eureka.EnableEurekaClient;

import org.springframework.cloud.netflix.feign.EnableFeignClients;

import org.springframework.web.bind.annotation.RequestMapping;

import org.springframework.web.bind.annotation.RestController;

@SpringBootApplication

@EnableEurekaClient

@EnableFeignClients

@RestController

public class Application {

@Autowired

private GreetingClient greetingClient;

@RequestMapping("/get-greeting")

public String greeting() {

return greetingClient.greeting();

}

public static void main(String[] args) {

SpringApplication.run(Application.class, args);

System.out.println("Hello World!");

}

}

interface

package cn.netkiller.spring.cloud.netflix.feign.client;

import org.springframework.cloud.netflix.feign.FeignClient;

import org.springframework.web.bind.annotation.RequestMapping;

@FeignClient("test-service")

public interface GreetingClient {

@RequestMapping("/greeting")

String greeting();

}

@FeignClient("test-service") 是 Eureka Client application.properties 中的 spring.application.name 配置项

@RequestMapping("/greeting") 是 Eureka Client RestController 中的 @RequestMapping

application.properties

spring.application.name=spring-cloud-eureka-feign-client

server.port=8088

#eureka.client.register-with-eureka=false

#eureka.client.fetch-registry=false

eureka.client.serviceUrl.defaultZone=http://localhost:8761/eureka/

feign.compression.response.enabled=true

feign.compression.request.enabled=true

feign.compression.request.mime-types=text/xml,application/xml,application/json

feign.compression.request.min-request-size=2048

测试

$ curl -s http://localhost:8088/get-greeting.json

GREETING

fallback

@FeignClient(value = "restful-api-service", fallback = UserServiceFeignClientFallback.class)

public interface UserServiceFeignClient {

@RequestMapping(value = "/api/user/{id}", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_VALUE, consumes = MediaType.APPLICATION_JSON_VALUE)

User getUser(@PathVariable("id") int id);

@RequestMapping(value = "/api/user/search/findByName?name={name}", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_VALUE, consumes = MediaType.APPLICATION_JSON_VALUE)

User findUserByName(@PathVariable("name") String name);

@RequestMapping(value = "/api/user/search/findByAddress?address={address}", method = RequestMethod.GET)

String findUserByAddress(@PathVariable("address") String address);

}

@Component

public class UserServiceFeignClientFallback implements UserServiceFeignClient {

@Override

public User getUser(int id) {

return new User("getUser.Fallback", "feignClient return");

}

@Override

public User findUserByName(String name) {

return new User("findUserByName.Fallback", "feignClient return");

}

@Override

public String findUserByAddress(String address) {

return "fallback";

}

}

4. 为 Eureka Server 增加用户认证

Maven

<dependency>

<groupId>org.springframework.boot</groupId>

<artifactId>spring-boot-starter-security</artifactId>

</dependency>

application.properties

security.user.name=eureka

security.user.password=s3cr3t

Eureka Client

spring.application.name=restful-api-service

eureka.client.serviceUrl.defaultZone=http://eureka:s3cr3t@localhost:8761/eureka/

Feign Client

eureka.client.serviceUrl.defaultZone=http://eureka:s3cr3t@localhost:8761/eureka/

5. Eureka 配置项

/eureka/apps

neo@MacBook-Pro-Neo ~ % curl http://localhost:8761/eureka/apps

<applications>

<versions__delta>1</versions__delta>

<apps__hashcode></apps__hashcode>

</applications>

Spring Cloud Eureka 配置参数说明

Eureka Client 配置项（eureka.client.*）

org.springframework.cloud.netflix.eureka.EurekaClientConfigBean

参数名称 说明 默认值

eureka.client.enabled

用于指示Eureka客户端已启用的标志

true

eureka.client.registry-fetch-interval-seconds

指示从eureka服务器获取注册表信息的频率（s）

30

eureka.client.instance-info-replication-interval-seconds

更新实例信息的变化到Eureka服务端的间隔时间，（s）

30

eureka.client.initial-instance-info-replication-interval-seconds

初始化实例信息到Eureka服务端的间隔时间，（s）

40

eureka.client.eureka-service-url-poll-interval-seconds

询问Eureka Server信息变化的时间间隔（s），默认为300秒 300

eureka.client.eureka-server-read-timeout-seconds

读取Eureka Server 超时时间（s），默认8秒

8

eureka.client.eureka-server-connect-timeout-seconds

连接Eureka Server 超时时间（s），默认5秒

5

eureka.client.eureka-server-total-connections

获取从eureka客户端到所有eureka服务器的连接总数,默认200个

200

eureka.client.eureka-server-total-connections-per-host

获取从eureka客户端到eureka服务器主机允许的连接总数，默认50个

50

eureka.client.eureka-connection-idle-timeout-seconds

连接到 Eureka Server 空闲连接的超时时间（s），默认30

30

eureka.client.registry-refresh-single-vip-address

指示客户端是否仅对单个VIP的注册表信息感兴趣，默认为null

null

eureka.client.heartbeat-executor-thread-pool-size

心跳保持线程池初始化线程数，默认2个 2

eureka.client.heartbeat-executor-exponential-back-off-bound

心跳超时重试延迟时间的最大乘数值，默认10

10

eureka.client.serviceUrl.defaultZone

可用区域映射到与eureka服务器通信的完全限定URL列表。每个值可以是单个URL或逗号分隔的备用位置列表。(http://${eureka.instance.hostname}:${server.port}/eureka/)

eureka.client.use-dns-for-fetching-service-urls

指示eureka客户端是否应使用DNS机制来获取要与之通信的eureka服务器列表。当DNS名称更新为具有其他服务器时，eureka客户端轮询eurekaServiceUrlPollIntervalSeconds中指定的信息后立即使用该信息。

false

eureka.client.register-with-eureka

指示此实例是否应将其信息注册到eureka服务器以供其他服务发现，默认为false

True

eureka.client.prefer-same-zone-eureka

实例是否使用同一zone里的eureka服务器，默认为true，理想状态下，eureka客户端与服务端是在同一zone下

true

eureka.client.log-delta-diff

是否记录eureka服务器和客户端之间在注册表的信息方面的差异，默认为false

false

eureka.client.disable-delta

指示eureka客户端是否禁用增量提取

false

eureka.client.fetch-remote-regions-registry

逗号分隔的区域列表，提取eureka注册表信息

eureka.client.on-demand-update-status-change

客户端的状态更新到远程服务器上，默认为true

true

eureka.client.allow-redirects

指示服务器是否可以将客户端请求重定向到备份服务器/集群。如果设置为false，则服务器将直接处理请求。如果设置为true，则可以将HTTP重定向发送到具有新服务器位置的客户端。

false

eureka.client.availability-zones.*

获取此实例所在区域的可用区域列表（在AWS数据中心中使用）。更改在运行时在registryFetchIntervalSeconds指定的下一个注册表获取周期生效。

eureka.client.backup-registry-impl

获取实现BackupRegistry的实现的名称，该实现仅在eureka客户端启动时第一次作为后备选项获取注册表信息。 对于需要额外的注册表信息弹性的应用程序，可能需要这样做，否则它将无法运行。

eureka.client.cache-refresh-executor-exponential-back-off-bound

在发生一系列超时的情况下，它是重试延迟的最大乘数值。

10

eureka.client.cache-refresh-executor-thread-pool-size

缓存刷新线程池初始化线程数量

2

eureka.client.client-data-accept

客户端数据接收的名称 full

eureka.client.decoder-name

解码器名称

eureka.client.dollar-replacement

eureka服务器序列化/反序列化的信息中获取“$”符号的替换字符串。默认为“_-”

eureka.client.encoder-name

编码器名称

eureka.client.escape-char-replacement

eureka服务器序列化/反序列化的信息中获取“_”符号的的替换字符串。默认为“__“

eureka.client.eureka-server-d-n-s-name

获取要查询的DNS名称来获得eureka服务器，此配置只有在eureka服务器ip地址列表是在DNS中才会用到。默认为null

null

eureka.client.eureka-server-port

获取eureka服务器的端口，此配置只有在eureka服务器ip地址列表是在DNS中才会用到。默认为null

null

eureka.client.eureka-server-u-r-l-context

表示eureka注册中心的路径，如果配置为eureka，则为http://ip:port/eureka/，

在eureka的配置文件中加入此配置表示eureka作为客户端向注册中心注册，从而构成eureka集群。此配置只有在eureka服务器ip地址列表是在DNS中才会用到，默认为null

null

eureka.client.fetch-registry

客户端是否获取eureka服务器注册表上的注册信息，默认为true

true

eureka.client.filter-only-up-instances

是否过滤掉非up实例，默认为true

true

eureka.client.g-zip-content

当服务端支持压缩的情况下，是否支持从服务端获取的信息进行压缩。默认为true

eureka.client.property-resolver

属性解析器

eureka.client.proxy-host

获取eureka server 的代理主机名

null

eureka.client.proxy-password

获取eureka server 的代理主机密码

null

eureka.client.proxy-port

获取eureka server 的代理主机端口

null

eureka.client.proxy-user-name

获取eureka server 的代理用户名

null

eureka.client.region

获取此实例所在的区域（在AWS数据中心中使用）。

us-east-1

eureka.client.should-enforce-registration-at-init

client 在初始化阶段是否强行注册到注册中心

false

eureka.client.should-unregister-on-shutdown

client在shutdown情况下，是否显示从注册中心注销

true

服务实例配置项（eureka.instance.*）

org.springframework.cloud.netflix.eureka.EurekaInstanceConfigBean

参数名称 说明 默认值

eureka.instance.appname

注册到注册中心的应用名称

unknown

eureka.instance.a-s-g-name

注册到注册中心的应用所属分组名称（AWS服务器） null

eureka.instance.app-group-name

注册到注册中心的应用所属分组名称

null

eureka.instance.data-center-info

指定服务实例所属数据中心

eureka.instance.instance-enabled-onit

指示是否应在eureka注册后立即启用实例以获取流量

false

eureka.instance.non-secure-port

http通信端口

80

eureka.instance.non-secure-port-enabled

是否启用HTTP通信端口 ture

eureka.instance.secure-port

HTTPS通信端口

443

eureka.instance.secure-port-enabled

是否启用HTTPS通信端口 false

eureka.instance.secure-virtual-host-name

服务实例安全主机名称（HTTPS） unknown

eureka.instance.virtual-host-name

该服务实例非安全注解名称（HTTP） unknown

eureka.instance.secure-health-check-url

该服务实例安全健康检查地址（URL），绝对地址

eureka.instance.lease-renewal-interval-in-seconds

该服务实例向注册中心发送心跳间隔（s）

30

eureka.instance.lease-expiration-duration-in-seconds

指示eureka服务器在删除此实例之前收到最后一次心跳之后等待的时间（s）

90

eureka.instance.metadata-map.*

eureka.instance.ip-address

该服务实例的IP地址 null

eureka.instance.prefer-ip-address

是否优先使用服务实例的IP地址，相较于hostname false

eureka.instance.status-page-url

该服务实例的状态检查地址（url），绝对地址 null

eureka.instance.status-page-url-path

该服务实例的状态检查地址，相对地址

/actuator/info

eureka.instance.home-page-url

该服务实例的主页地址（url），绝对地址

eureka.instance.home-page-url-path

该服务实例的主页地址，相对地址

/

eureka.instance.health-check-url

该服务实例的健康检查地址（url），绝对地址

null

eureka.instance.health-check-url-path

该服务实例的健康检查地址，相对地址

/actuator/health

eureka.instance.instance-id

该服务实例在注册中心的唯一实例ID

eureka.instance.hostname

该服务实例所在主机名

eureka.instance.namespace

获取用于查找属性的命名空间。 在Spring Cloud中被忽略。

eureka

eureka.instance.environment

该服务实例环境配置

eureka.instance.default-address-resolution-order

默认地址解析顺序

eureka.instance.initial-status

该服务实例注册到Eureka Server 的初始状态 up

eureka.instance.registry.default-open-for-traffic-count

【Eureka Server 端属性】默认开启通信的数量

1

eureka.instance.registry.expected-number-of-renews-per-min

【Eureka Server 端属性】每分钟续约次数

1

Eureka Server 配置项（eureka.server.*）

org.springframework.cloud.netflix.eureka.server.EurekaServerConfigBean

参数名称 说明 默认值

eureka.server.enable-self-preservation

启用自我保护机制，默认为true true

eureka.server.eviction-interval-timer-in-ms

清除无效服务实例的时间间隔（ms），默认1分钟

60000

eureka.server.delta-retention-timer-interval-in-ms

清理无效增量信息的时间间隔（ms），默认30秒

30000

eureka.server.disable-delta

禁用增量获取服务实例信息 false

eureka.server.log-identity-headers

是否记录登录日志 true

eureka.server.rate-limiter-burst-size

限流大小

10

eureka.server.rate-limiter-enabled

是否启用限流 false

eureka.server.rate-limiter-full-fetch-average-rate

平均请求速率

100

eureka.server.rate-limiter-throttle-standard-clients

是否对标准客户端进行限流 false

eureka.server.rate-limiter-registry-fetch-average-rate

服务注册与拉取的平均速率

500

eureka.server.rate-limiter-privileged-clients

信任的客户端列表

eureka.server.renewal-percent-threshold

15分钟内续约服务的比例小于0.85，则开启自我保护机制，再此期间不会清除已注册的任何服务（即便是无效服务）

0.85

eureka.server.renewal-threshold-update-interval-ms

更新续约阈值的间隔（分钟），默认15分钟

15

eureka.server.response-cache-auto-expiration-in-seconds

注册信息缓存有效时长（s），默认180秒

180

eureka.server.response-cache-update-interval-ms

注册信息缓存更新间隔（s），默认30秒

30

eureka.server.retention-time-in-m-s-in-delta-queue

保留增量信息时长（分钟），默认3分钟

3

eureka.server.sync-when-timestamp-differs

当时间戳不一致时，是否进行同步 true

eureka.server.use-read-only-response-cache

是否使用只读缓存策略 true

自定义工具设置

eureka.server.json-codec-name

Json编解码器名称

eureka.server.property-resolver

属性解析器名称

eureka.server.xml-codec-name

Xml编解码器名称

Eureka Server 集群配置

eureka.server.enable-replicated-request-compression

复制数据请求时，数据是否压缩 false

eureka.server.batch-replication

节点之间数据复制是否采用批处理 false

eureka.server.max-elements-in-peer-replication-pool

备份池最大备份事件数量，默认1000

1000

eureka.server.max-elements-in-status-replication-pool

状态备份池最大备份事件数量，默认1000

1000

eureka.server.max-idle-thread-age-in-minutes-for-peer-replication

节点之间信息同步线程最大空闲时间（分钟）

15

eureka.server.max-idle-thread-in-minutes-age-for-status-replication

节点之间状态同步线程最大空闲时间（分钟）

10

eureka.server.max-threads-for-peer-replication

节点之间信息同步最大线程数量

20

eureka.server.max-threads-for-status-replication

节点之间状态同步最大线程数量

1

eureka.server.max-time-for-replication

节点之间信息复制最大通信时长（ms）

30000

eureka.server.min-available-instances-for-peer-replication

集群中服务实例最小数量，-1 表示单节点

-1

eureka.server.min-threads-for-peer-replication

节点之间信息复制最小线程数量

5

eureka.server.min-threads-for-status-replication

节点之间信息状态同步最小线程数量

1

eureka.server.number-of-replication-retries

节点之间数据复制时，可重试次数

5

eureka.server.peer-eureka-nodes-update-interval-ms

节点更新数据间隔时长（分钟）

10

eureka.server.peer-eureka-status-refresh-time-interval-ms

节点之间状态刷新间隔时长（ms）

30000

eureka.server.peer-node-connect-timeout-ms

节点之间连接超时时长（ms）

200

eureka.server.peer-node-connection-idle-timeout-seconds

节点之间连接后，空闲时长（s）

30

eureka.server.peer-node-read-timeout-ms

几点之间数据读取超时时间（ms）

200

eureka.server.peer-node-total-connections

集群中节点连接总数

1000

eureka.server.peer-node-total-connections-per-host

节点之间连接，单机最大连接数量

500

eureka.server.registry-sync-retries

节点启动时，尝试获取注册信息的次数

500

eureka.server.registry-sync-retry-wait-ms

节点启动时，尝试获取注册信息的间隔时长（ms）

30000

eureka.server.wait-time-in-ms-when-sync-empty

在Eureka服务器获取不到集群里对等服务器上的实例时，需要等待的时间（分钟）

5

Eureka instance 配置项

#服务注册中心实例的主机名

eureka.instance.hostname=localhost

#注册在Eureka服务中的应用组名

eureka.instance.app-group-name=

#注册在的Eureka服务中的应用名称

eureka.instance.appname=

#该实例注册到服务中心的唯一ID

eureka.instance.instance-id=

#该实例的IP地址

eureka.instance.ip-address=

#该实例，相较于hostname是否优先使用IP

eureka.instance.prefer-ip-address=false

#用于AWS平台自动扩展的与此实例关联的组名，

eureka.instance.a-s-g-name=

#部署此实例的数据中心

eureka.instance.data-center-info=

#默认的地址解析顺序

eureka.instance.default-address-resolution-order=

#该实例的环境配置

eureka.instance.environment=

#初始化该实例，注册到服务中心的初始状态

eureka.instance.initial-status=up

#表明是否只要此实例注册到服务中心，立马就进行通信

eureka.instance.instance-enabled-onit=false

#该服务实例的命名空间,用于查找属性

eureka.instance.namespace=eureka

#该服务实例的子定义元数据，可以被服务中心接受到

eureka.instance.metadata-map.test = test

#服务中心删除此服务实例的等待时间(秒为单位),时间间隔为最后一次服务中心接受到的心跳时间

eureka.instance.lease-expiration-duration-in-seconds=90

#该实例给服务中心发送心跳的间隔时间，用于表明该服务实例可用

eureka.instance.lease-renewal-interval-in-seconds=30

#该实例，注册服务中心，默认打开的通信数量

eureka.instance.registry.default-open-for-traffic-count=1

#每分钟续约次数

eureka.instance.registry.expected-number-of-renews-per-min=1

#该实例健康检查url,绝对路径

eureka.instance.health-check-url=

#该实例健康检查url,相对路径

eureka.instance.health-check-url-path=/health

#该实例的主页url,绝对路径

eureka.instance.home-page-url=

#该实例的主页url,相对路径

eureka.instance.home-page-url-path=/

#该实例的安全健康检查url,绝对路径

eureka.instance.secure-health-check-url=

#https通信端口

eureka.instance.secure-port=443

#https通信端口是否启用

eureka.instance.secure-port-enabled=false

#http通信端口

eureka.instance.non-secure-port=80

#http通信端口是否启用

eureka.instance.non-secure-port-enabled=true

#该实例的安全虚拟主机名称(https)

eureka.instance.secure-virtual-host-name=unknown

#该实例的虚拟主机名称(http)

eureka.instance.virtual-host-name=unknown

#该实例的状态呈现url,绝对路径

eureka.instance.status-page-url=

#该实例的状态呈现url,相对路径

eureka.instance.status-page-url-path=/status

Eureka client 配置项

#该客户端是否可用

eureka.client.enabled=true

#实例是否在eureka服务器上注册自己的信息以供其他服务发现，默认为true

eureka.client.register-with-eureka=false

#此客户端是否获取eureka服务器注册表上的注册信息，默认为true

eureka.client.fetch-registry=false

#是否过滤掉，非UP的实例。默认为true

eureka.client.filter-only-up-instances=true

#与Eureka注册服务中心的通信zone和url地址

eureka.client.serviceUrl.defaultZone=http://${eureka.instance.hostname}:${server.port}/eureka/

#client连接Eureka服务端后的空闲等待时间，默认为30 秒

eureka.client.eureka-connection-idle-timeout-seconds=30

#client连接eureka服务端的连接超时时间，默认为5秒

eureka.client.eureka-server-connect-timeout-seconds=5

#client对服务端的读超时时长

eureka.client.eureka-server-read-timeout-seconds=8

#client连接all eureka服务端的总连接数，默认200

eureka.client.eureka-server-total-connections=200

#client连接eureka服务端的单机连接数量，默认50

eureka.client.eureka-server-total-connections-per-host=50

#执行程序指数回退刷新的相关属性，是重试延迟的最大倍数值，默认为10

eureka.client.cache-refresh-executor-exponential-back-off-bound=10

#执行程序缓存刷新线程池的大小，默认为5

eureka.client.cache-refresh-executor-thread-pool-size=2

#心跳执行程序回退相关的属性，是重试延迟的最大倍数值，默认为10

eureka.client.heartbeat-executor-exponential-back-off-bound=10

#心跳执行程序线程池的大小,默认为5

eureka.client.heartbeat-executor-thread-pool-size=5

# 询问Eureka服务url信息变化的频率（s），默认为300秒

eureka.client.eureka-service-url-poll-interval-seconds=300

#最初复制实例信息到eureka服务器所需的时间（s），默认为40秒

eureka.client.initial-instance-info-replication-interval-seconds=40

#间隔多长时间再次复制实例信息到eureka服务器，默认为30秒

eureka.client.instance-info-replication-interval-seconds=30

#从eureka服务器注册表中获取注册信息的时间间隔（s），默认为30秒

eureka.client.registry-fetch-interval-seconds=30

# 获取实例所在的地区。默认为us-east-1

eureka.client.region=us-east-1

#实例是否使用同一zone里的eureka服务器，默认为true，理想状态下，eureka客户端与服务端是在同一zone下

eureka.client.prefer-same-zone-eureka=true

# 获取实例所在的地区下可用性的区域列表，用逗号隔开。（AWS）

eureka.client.availability-zones.china=defaultZone,defaultZone1,defaultZone2

#eureka服务注册表信息里的以逗号隔开的地区名单，如果不这样返回这些地区名单，则客户端启动将会出错。默认为null

eureka.client.fetch-remote-regions-registry=

#服务器是否能够重定向客户端请求到备份服务器。 如果设置为false，服务器将直接处理请求，如果设置为true，它可能发送HTTP重定向到客户端。默认为false

eureka.client.allow-redirects=false

#客户端数据接收

eureka.client.client-data-accept=

#增量信息是否可以提供给客户端看，默认为false

eureka.client.disable-delta=false

#eureka服务器序列化/反序列化的信息中获取“_”符号的的替换字符串。默认为“__“

eureka.client.escape-char-replacement=__

#eureka服务器序列化/反序列化的信息中获取“$”符号的替换字符串。默认为“_-”

eureka.client.dollar-replacement="_-"

#当服务端支持压缩的情况下，是否支持从服务端获取的信息进行压缩。默认为true

eureka.client.g-zip-content=true

#是否记录eureka服务器和客户端之间在注册表的信息方面的差异，默认为false

eureka.client.log-delta-diff=false

# 如果设置为true,客户端的状态更新将会点播更新到远程服务器上，默认为true

eureka.client.on-demand-update-status-change=true

#此客户端只对一个单一的VIP注册表的信息感兴趣。默认为null

eureka.client.registry-refresh-single-vip-address=

#client是否在初始化阶段强行注册到服务中心，默认为false

eureka.client.should-enforce-registration-at-init=false

#client在shutdown的时候是否显示的注销服务从服务中心，默认为true

eureka.client.should-unregister-on-shutdown=true

# 获取eureka服务的代理主机，默认为null

eureka.client.proxy-host=

#获取eureka服务的代理密码，默认为null

eureka.client.proxy-password=

# 获取eureka服务的代理端口, 默认为null

eureka.client.proxy-port=

# 获取eureka服务的代理用户名，默认为null

eureka.client.proxy-user-name=

#属性解释器

eureka.client.property-resolver=

#获取实现了eureka客户端在第一次启动时读取注册表的信息作为回退选项的实现名称

eureka.client.backup-registry-impl=

#这是一个短暂的×××的配置，如果最新的×××是稳定的，则可以去除，默认为null

eureka.client.decoder-name=

#这是一个短暂的编码器的配置，如果最新的编码器是稳定的，则可以去除，默认为null

eureka.client.encoder-name=

#是否使用DNS机制去获取服务列表，然后进行通信。默认为false

eureka.client.use-dns-for-fetching-service-urls=false

#获取要查询的DNS名称来获得eureka服务器，此配置只有在eureka服务器ip地址列表是在DNS中才会用到。默认为null

eureka.client.eureka-server-d-n-s-name=

#获取eureka服务器的端口，此配置只有在eureka服务器ip地址列表是在DNS中才会用到。默认为null

eureka.client.eureka-server-port=

#表示eureka注册中心的路径，如果配置为eureka，则为http://x.x.x.x:x/eureka/，在eureka的配置文件中加入此配置表示eureka作为客户端向注册中心注册，从而构成eureka集群。此配置只有在eureka服务器ip地址列表是在DNS中才会用到，默认为null

eureka.client.eureka-server-u-r-l-context=

Eureka Server配置项

#服务端开启自我保护模式。无论什么情况，服务端都会保持一定数量的服务。避免client与server的网络问题，而出现大量的服务被清除。

eureka.server.enable-self-preservation=true

#开启清除无效服务的定时任务，时间间隔。默认1分钟

eureka.server.eviction-interval-timer-in-ms= 60000

#间隔多长时间，清除过期的delta数据

eureka.server.delta-retention-timer-interval-in-ms=0

#过期数据，是否也提供给client

eureka.server.disable-delta=false

#eureka服务端是否记录client的身份header

eureka.server.log-identity-headers=true

#请求频率限制器

eureka.server.rate-limiter-burst-size=10

#是否开启请求频率限制器

eureka.server.rate-limiter-enabled=false

#请求频率的平均值

eureka.server.rate-limiter-full-fetch-average-rate=100

#是否对标准的client进行频率请求限制。如果是false，则只对非标准client进行限制

eureka.server.rate-limiter-throttle-standard-clients=false

#注册服务、拉去服务列表数据的请求频率的平均值

eureka.server.rate-limiter-registry-fetch-average-rate=500

#设置信任的client list

eureka.server.rate-limiter-privileged-clients=

#在设置的时间范围类，期望与client续约的百分比。

eureka.server.renewal-percent-threshold=0.85

#多长时间更新续约的阈值

eureka.server.renewal-threshold-update-interval-ms=0

#对于缓存的注册数据，多长时间过期

eureka.server.response-cache-auto-expiration-in-seconds=180

#多长时间更新一次缓存中的服务注册数据

eureka.server.response-cache-update-interval-ms=0

#缓存增量数据的时间，以便在检索的时候不丢失信息

eureka.server.retention-time-in-m-s-in-delta-queue=0

#当时间戳不一致的时候，是否进行同步

eureka.server.sync-when-timestamp-differs=true

#是否采用只读缓存策略，只读策略对于缓存的数据不会过期。

eureka.server.use-read-only-response-cache=true

################server 自定义实现的配置#####################33

#json的转换的实现类名

eureka.server.json-codec-name=

#PropertyResolver

eureka.server.property-resolver=

#eureka server xml的编解码实现名称

eureka.server.xml-codec-name=

################server node 与 node 之间关联的配置#####################33

#发送复制数据是否在request中，总是压缩

eureka.server.enable-replicated-request-compression=false

#指示群集节点之间的复制是否应批处理以提高网络效率。

eureka.server.batch-replication=false

#允许备份到备份池的最大复制事件数量。而这个备份池负责除状态更新的其他事件。可以根据内存大小，超时和复制流量，来设置此值得大小

eureka.server.max-elements-in-peer-replication-pool=10000

#允许备份到状态备份池的最大复制事件数量

eureka.server.max-elements-in-status-replication-pool=10000

#多个服务中心相互同步信息线程的最大空闲时间

eureka.server.max-idle-thread-age-in-minutes-for-peer-replication=15

#状态同步线程的最大空闲时间

eureka.server.max-idle-thread-in-minutes-age-for-status-replication=15

#服务注册中心各个instance相互复制数据的最大线程数量

eureka.server.max-threads-for-peer-replication=20

#服务注册中心各个instance相互复制状态数据的最大线程数量

eureka.server.max-threads-for-status-replication=1

#instance之间复制数据的通信时长

eureka.server.max-time-for-replication=30000

#正常的对等服务instance最小数量。-1表示服务中心为单节点。

eureka.server.min-available-instances-for-peer-replication=-1

#instance之间相互复制开启的最小线程数量

eureka.server.min-threads-for-peer-replication=5

#instance之间用于状态复制，开启的最小线程数量

eureka.server.min-threads-for-status-replication=1

#instance之间复制数据时可以重试的次数

eureka.server.number-of-replication-retries=5

#eureka节点间间隔多长时间更新一次数据。默认10分钟。

eureka.server.peer-eureka-nodes-update-interval-ms=600000

#eureka服务状态的相互更新的时间间隔。

eureka.server.peer-eureka-status-refresh-time-interval-ms=0

#eureka对等节点间连接超时时间

eureka.server.peer-node-connect-timeout-ms=200

#eureka对等节点连接后的空闲时间

eureka.server.peer-node-connection-idle-timeout-seconds=30

#节点间的读数据连接超时时间

eureka.server.peer-node-read-timeout-ms=200

#eureka server 节点间连接的总共最大数量

eureka.server.peer-node-total-connections=1000

#eureka server 节点间连接的单机最大数量

eureka.server.peer-node-total-connections-per-host=10

#在服务节点启动时，eureka尝试获取注册信息的次数

eureka.server.registry-sync-retries=

#在服务节点启动时，eureka多次尝试获取注册信息的间隔时间

eureka.server.registry-sync-retry-wait-ms=

#当eureka server启动的时候，不能从对等节点获取instance注册信息的情况，应等待多长时间。

eureka.server.wait-time-in-ms-when-sync-empty=0

################server 与 remote 关联的配置#####################33

#过期数据，是否也提供给远程region

eureka.server.disable-delta-for-remote-regions=false

#回退到远程区域中的应用程序的旧行为 (如果已配置) 如果本地区域中没有该应用程序的实例, 则将被禁用。

eureka.server.disable-transparent-fallback-to-other-region=false

#指示在服务器支持的情况下, 是否必须为远程区域压缩从尤里卡服务器获取的内容。

eureka.server.g-zip-content-from-remote-region=true

#连接eureka remote note的连接超时时间

eureka.server.remote-region-connect-timeout-ms=1000

#remote region 应用白名单

eureka.server.remote-region-app-whitelist.

#连接eureka remote note的连接空闲时间

eureka.server.remote-region-connection-idle-timeout-seconds=30

#执行remote region 获取注册信息的请求线程池大小

eureka.server.remote-region-fetch-thread-pool-size=20

#remote region 从对等eureka加点读取数据的超时时间

eureka.server.remote-region-read-timeout-ms=1000

#从remote region 获取注册信息的时间间隔

eureka.server.remote-region-registry-fetch-interval=30

#remote region 连接eureka节点的总连接数量

eureka.server.remote-region-total-connections=1000

#remote region 连接eureka节点的单机连接数量

eureka.server.remote-region-total-connections-per-host=50

#remote region抓取注册信息的存储文件，而这个可靠的存储文件需要全限定名来指定

eureka.server.remote-region-trust-store=

#remote region 储存的文件的密码

eureka.server.remote-region-trust-store-password=

#remote region url.多个逗号隔开

eureka.server.remote-region-urls=

#remote region url.多个逗号隔开

eureka.server.remote-region-urls-with-name.

################server 与 ASG/AWS/EIP/route52 之间关联的配置#####################33

#缓存ASG信息的过期时间。

eureka.server.a-s-g-cache-expiry-timeout-ms=0

#查询ASG信息的超时时间

eureka.server.a-s-g-query-timeout-ms=300

#服务更新ASG信息的频率

eureka.server.a-s-g-update-interval-ms=0

#AWS访问ID

eureka.server.a-w-s-access-id=

#AWS安全密钥

eureka.server.a-w-s-secret-key=

#AWS绑定策略

eureka.server.binding-strategy=eip

#用于从第三方AWS 帐户描述自动扩展分组的角色的名称。

eureka.server.list-auto-scaling-groups-role-name=

#是否应该建立连接引导

eureka.server.prime-aws-replica-connections=true

#服务端尝试绑定候选EIP的次数

eureka.server.e-i-p-bind-rebind-retries=3

#服务端绑定EIP的时间间隔.如果绑定就检查;如果绑定失效就重新绑定。当且仅当已经绑定的情况

eureka.server.e-i-p-binding-retry-interval-ms=10

#服务端绑定EIP的时间间隔.当且仅当服务为绑定的情况

eureka.server.e-i-p-binding-retry-interval-ms-when-unbound=

#服务端尝试绑定route53的次数

eureka.server.route53-bind-rebind-retries=3

#服务端间隔多长时间尝试绑定route53

eureka.server.route53-binding-retry-interval-ms=30

#

eureka.server.route53-domain-t-t-l=10

6. ribbon

@Configuration

public class RibbonConfigure {

@LoadBalanced

@Bean

public RestTemplate restTemplate(){

return new RestTemplate();

}

//指定Ribbon使用随机策略

@Bean

public IRule loadBalanceRule(){

//return new RandomRule();

List<Integer> ports = new ArrayList<>();

ports.add(8081);

return new CustomRule(ports);

}

}

LoadBalancerClient 实例

application.properties

web.ribbon.listOfServers=localhost:7900,localhost:7901,localhost:7902

LoadBalancerClient 获取服务器列表

package cn.netkiller.openfeign.controller;

import org.springframework.beans.factory.annotation.Autowired;

import org.springframework.cloud.client.ServiceInstance;

import org.springframework.cloud.client.loadbalancer.LoadBalancerClient;

import org.springframework.web.bind.annotation.GetMapping;

import org.springframework.web.bind.annotation.RestController;

@RestController

public class TestController {

@Autowired

private LoadBalancerClient loadBalancerClient;

@GetMapping("/lb")

public String LoadBalancer() {

ServiceInstance serviceInstance = this.loadBalancerClient.choose("web");

System.out.println("Server: " + serviceInstance.getServiceId() + ":" + serviceInstance.getHost() + ":"

+ serviceInstance.getPort());

return serviceInstance.toString();

}

}

Ribbon 相关配置

spring.cloud.loadbalancer.ribbon.enabled=false

内置负载均衡策略

provider.ribbon.NFLoadBalancerRuleClassName=com.netflix.loadbalancer.RandomRule

RoundRobinRule

轮询策略。Ribbon 默认采用的策略。若经过一轮轮询没有找到可用的 provider，其最多 轮询 10 轮。若最终还没有找到，则返回 null。

RandomRule

随机策略，从所有可用的 provider 中随机选择一个。

RetryRule

重试策略。先按照 RoundRobinRule 策略获取 provider，若获取失败，则在指定的时限内重试。默认的时限为 500 毫秒。

BestAvailableRule

最可用策略。选择并发量最小的 provider，即连接的消费者数量最少的 provider。

AvailabilityFilteringRule

可用过滤算法。该算法规则是:过滤掉处于熔断状态的 provider 与已经超过连接极限的 provider，对剩余 provider 采用轮询策略。

ZoneAvoidanceRule

zone 回避策略。根据 provider 所在 zone 及 provider 的可用性，对 provider 进行选择。

WeightedResponseTimeRule

“权重响应时间”策略。根据每个 provider 的平均响应时间计算其权重，响应时间越快权重越大，被选中的机率就越高。在刚启动时采用轮询策略。后面就会根据权重进行选择了。

7. 获取 EurekaClient 信息

package cn.netkiller.sample;

import com.netflix.discovery.EurekaClient;

import org.springframework.beans.factory.annotation.Autowired;

import org.springframework.beans.factory.annotation.Value;

import org.springframework.boot.SpringApplication;

import org.springframework.boot.autoconfigure.SpringBootApplication;

import org.springframework.cloud.netflix.eureka.EnableEurekaClient;

import org.springframework.context.annotation.Lazy;

import org.springframework.web.bind.annotation.GetMapping;

import org.springframework.web.bind.annotation.RequestParam;

import org.springframework.web.bind.annotation.RestController;

import reactor.core.publisher.Mono;

@SpringBootApplication

@EnableEurekaClient

@RestController

public class WebFluxApplication {

@Autowired

@Lazy

private EurekaClient eurekaClient;

@Value("${spring.application.name}")

private String appName;

public static void main(String[] args) {

SpringApplication.run(WebFluxApplication.class, args);

}

@GetMapping("/client")

public Mono<String> greeting() {

String idInEureka = eurekaClient.getApplication(appName).getInstances().get(0).getId();

return Mono.just(String.format("Hello from '%s'!", idInEureka));

}

@GetMapping("/client2")

public Mono<String> greetingWithParam(@RequestParam(value = "id") Long id) {

String idInEureka = eurekaClient.getApplication(appName).getInstances().get(0).getId();

return Mono.just(String.format("Hello with param from '%s'!", idInEureka));

}

}

8. Zuul

Maven

<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>

<groupId>cn.netkiller</groupId>

<artifactId>zuul</artifactId>

<version>0.0.1-SNAPSHOT</version>

<packaging>jar</packaging>

<name>zuul</name>

<url>http://maven.apache.org</url>

<properties>

<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>

</properties>

<parent>

<groupId>org.springframework.boot</groupId>

<artifactId>spring-boot-starter-parent</artifactId>

<version>1.5.6.RELEASE</version>

<relativePath /> <!-- lookup parent from repository -->

</parent>

<dependencyManagement>

<dependencies>

<dependency>

<groupId>org.springframework.cloud</groupId>

<artifactId>spring-cloud-dependencies</artifactId>

<version>Dalston.SR2</version>

<type>pom</type>

<scope>import</scope>

</dependency>

</dependencies>

</dependencyManagement>

<dependencies>

<dependency>

<groupId>org.springframework.cloud</groupId>

<artifactId>spring-cloud-starter-zuul</artifactId>

</dependency>

</dependencies>

</project>

EnableZuulProxy

package cn.netkiller.zuul;

import org.springframework.boot.SpringApplication;

import org.springframework.boot.autoconfigure.SpringBootApplication;

import org.springframework.cloud.netflix.zuul.EnableZuulProxy;

@SpringBootApplication

@EnableZuulProxy

public class Application {

public static void main(String[] args) {

SpringApplication.run(Application.class, args);

}

}

application.yml

server:

port: 8765

logging:

level:

ROOT: INFO

org.springframework.web: DEBUG

zuul:

routes:

restful:

path: /restful/**

url: http://api:password@api.netkiller.com:8080/restful

负载均衡配置

zuul:

routes:

httpbin:

path: /**

serviceId: httpslb

httpslb:

ribbon:

listOfServers: api1.netkiller.org, api2.netkiller.cn

第 61 章 Openfeign

1. Openfeign 扫描包配置

@EnableFeignClients(basePackages = {"cn.netkiller.openfeign"})

2. 用户认证

@Configuration

@EnableFeignClients

public class FeignConfiguration {

@Bean

public Contract feignContract() {

return new feign.Contract.Default();

}

@Bean

public BasicAuthRequestInterceptor basicAuthRequestInterceptor() {

return new BasicAuthRequestInterceptor("user", "password");

}

}

3. 应用实例

@FeignClient(name="myServiceName", url="localhost:8888")

public interface OpenfeignService {

@RequestMapping("/")

public String getName();

}

4. 配置连接方式

系统默认是 httpclient

httpclient

<dependency>

<groupId>io.github.openfeign</groupId>

<artifactId>feign-httpclient</artifactId>

</dependency>

httpclient

feign.httpclient.enabled=true

feign.httpclient.max-connections=1000

feign.httpclient.max-connections-per-route=50

okhttp

Maven 依赖

<dependency>

<groupId>io.github.openfeign</groupId>

<artifactId>feign-okhttp</artifactId>

</dependency>

启用 okhttp

feign.httpclient.enabled=false

feign.okhttp.enabled=true

feign.httpclient.max-connections=1000

feign.httpclient.max-connections-per-route=50

5. 配置手册

https://docs.spring.io/spring-cloud-openfeign/docs/current/reference/html/appendix.html

第 62 章 Spring Cloud Gateway

SpringCloud Gateway是基于WebFlux框架实现的网关服务器

gateway网关路由配置有两种方式

1. 通过@Bean自定义RouteLocator，在启动主类Application中配置

2. 在配置文件yml中配置

这两种方式都可以实现网关路由，还可以同时使用，写在配置配置文件中对于运维更友好。

1. Gateway 例子

Maven

<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>

<groupId>cn.netkiller</groupId>

<artifactId>gateway</artifactId>

<version>0.0.1-SNAPSHOT</version>

<packaging>jar</packaging>

<name>gateway</name>

<url>http://www.netkiller.cn</url>

<properties>

<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>

<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>

<java.version>11</java.version>

<spring-cloud.version>Greenwich.SR1</spring-cloud.version>

</properties>

<parent>

<groupId>org.springframework.boot</groupId>

<artifactId>spring-boot-starter-parent</artifactId>

<version>2.1.3.RELEASE</version>

<relativePath />

</parent>

<dependencyManagement>

<dependencies>

<dependency>

<groupId>org.springframework.cloud</groupId>

<artifactId>spring-cloud-dependencies</artifactId>

<version>${spring-cloud.version}</version>

<type>pom</type>

<scope>import</scope>

</dependency>

</dependencies>

</dependencyManagement>

<dependencies>

<dependency>

<groupId>org.springframework.boot</groupId>

<artifactId>spring-boot-starter-actuator</artifactId>

</dependency>

<dependency>

<groupId>org.springframework.cloud</groupId>

<artifactId>spring-cloud-starter-gateway</artifactId>

</dependency>

</dependencies>

</project>

SpringApplication

package cn.netkiller.gateway;

import org.springframework.boot.SpringApplication;

import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication

public class Application {

public static void main(String[] args) {

SpringApplication.run(Application.class, args);

}

}

application.yml

resources/application.yml

server:

port: 8080

spring:

application:

name: spring-cloud-gateway

cloud:

gateway:

routes:

- id: linux

uri: http://www.netkiller.cn

predicates:

- Path=/linux

logging:

level:

org.springframework.cloud.gateway: TRACE

org.springframework.http.server.reactive: DEBUG

org.springframework.web.reactive: DEBUG

reactor.ipc.netty: DEBUG

RouteLocator 方式

package com.springcloud.gateway;

import org.springframework.boot.SpringApplication;

import org.springframework.boot.autoconfigure.SpringBootApplication;

import org.springframework.cloud.gateway.route.RouteLocator;

import org.springframework.cloud.gateway.route.builder.RouteLocatorBuilder;

import org.springframework.context.annotation.Bean;

@SpringBootApplication

public class GatewayApplication {

public static void main(String[] args) {

SpringApplication.run(GatewayApplication.class, args);

}

@Bean

public RouteLocator customRouteLocator(RouteLocatorBuilder builder) {

return builder.routes()

.route("path_route", r -> r.path("/linux")

.uri("http://www.netkiller.cn"))

.build();

}

}

2. 路由配置

转发操作

@Bean

public RouteLocator customRouteLocator(RouteLocatorBuilder builder) {

return builder.routes().route("path_route", r -> r.path("/ch/history/").uri("https://new.qq.com")).build();

}

URL 参数

@Bean

public RouteLocator customRouteLocator(RouteLocatorBuilder builder) {

return builder.routes().route("query_route", r -> r.query("id", "1000").uri("https://news.netkiller.cn/news")).build();

}

curl http://localhost:8080/?id=1000

传递参数

@Bean

public RouteLocator customRouteLocator(RouteLocatorBuilder builder) {

return builder.routes().route("query_route", r -> r.query("q").uri("https://cn.bing.com/search")).build();

}

http://localhost:8080/?q=netkiller

@Bean

public RouteLocator customRouteLocator(RouteLocatorBuilder builder) {

return builder.routes().route("query_route", r -> r.query("q").and().path("/search").uri("https://cn.bing.com"))

.build();

}

http://localhost:8080/search?q=netkiller

2. Spring Cloud Bus

第 63 章 Spring Cloud Sleuth

分布式链路追踪工具

<dependency>

<groupId>org.springframework.cloud</groupId>

<artifactId>spring-cloud-starter-sleuth</artifactId>

</dependency>

字段定义

字段 描述

trace 从客户发起请求(request)抵达被追踪系统的边界开始，到被追踪系统向客户返回响应(response)为止的整个过程

span 每个trace中会调用若干个服务，为了记录调用了哪些服务，以及每次调用的消耗时间等信息，在每次调用服务时，埋入一个调用记录

X-B3-ParentSpanId 标识当前工作单元所属的上一个工作单元

X-B3-Sampled 是否采样，1表示需要被输出，0表示不需要被输出

X-B3-TraceId 一条请求链路(trace)的唯一标识，必须值

X-Span-Name 工作单元的名称

X-B3-SpanId 一个工作单元(span)的唯一标识，必须值

1. logback 安装

logging.level.org.springframework.web=DEBUG

spring.sleuth.traceId128=true

spring.sleuth.sampler.probability=1.0

logging.pattern.level=[%X{traceId}/%X{spanId}] %-5p [%t] %C{2} - %m%n

第 64 章 Spring Cloud with Kubernetes

1. Config

Spring Cloud 使用 Kubernetes 提供的 Config Maps 作为配置中心。这样的好处是我们无需再启动一个 config 服务。

Maven 依赖

父项目

<?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>

<groupId>cn.netkiller</groupId>

<artifactId>kubernetes</artifactId>

<version>0.0.1-SNAPSHOT</version>

<packaging>pom</packaging>

<name>kubernetes</name>

<url>http://www.netkiller.cn</url>

<description>Spring Cloud with Kubernetes</description>

<organization>

<name>Netkiller Spring Cloud 手札</name>

<url>http://www.netkiller.cn</url>

</organization>

<developers>

<developer>

<name>Neo</name>

<email>netkiller@msn.com</email>

<organization>Netkiller Spring Cloud 手札</organization>

<organizationUrl>http://www.netkiller.cn</organizationUrl>

<roles>

<role>Author</role>

</roles>

</developer>

</developers>

<properties>

<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>

<spring-cloud.version>Hoxton.SR8</spring-cloud.version>

<docker.registry>registry.netkiller.cn:5000</docker.registry>

<docker.registry.name>netkiller</docker.registry.name>

<docker.image>mcr.microsoft.com/java/jre:15-zulu-alpine</docker.image>

</properties>

<parent>

<groupId>org.springframework.boot</groupId>

<artifactId>spring-boot-starter-parent</artifactId>

<version>2.3.4.RELEASE</version>

<relativePath />

</parent>

<dependencyManagement>

<dependencies>

<dependency>

<groupId>org.springframework.cloud</groupId>

<artifactId>spring-cloud-dependencies</artifactId>

<version>${spring-cloud.version}</version>

<type>pom</type>

<scope>import</scope>

</dependency>

</dependencies>

</dependencyManagement>

<dependencies>

<dependency>

<groupId>org.springframework.boot</groupId>

<artifactId>spring-boot-starter-actuator</artifactId>

</dependency>

<dependency>

<groupId>org.springframework.boot</groupId>

<artifactId>spring-boot-starter-test</artifactId>

<scope>test</scope>

</dependency>

</dependencies>

<modules>

<module>service</module>

<module>ConfigMaps</module>

</modules>

</project>

项目模块

<?xml version="1.0"?>

<project xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd" xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">

<modelVersion>4.0.0</modelVersion>

<parent>

<groupId>cn.netkiller</groupId>

<artifactId>kubernetes</artifactId>

<version>0.0.1-SNAPSHOT</version>

</parent>

<groupId>cn.netkiller</groupId>

<artifactId>configmaps</artifactId>

<version>0.0.1-SNAPSHOT</version>

<name>configmaps</name>

<url>http://maven.apache.org</url>

<properties>

<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>

</properties>

<dependencies>

<dependency>

<groupId>org.springframework.boot</groupId>

<artifactId>spring-boot-starter-web</artifactId>

</dependency>

<dependency>

<groupId>org.springframework.cloud</groupId>

<artifactId>spring-cloud-starter-kubernetes-config</artifactId>

</dependency>

</dependencies>

<build>

<plugins>

<plugin>

<groupId>org.springframework.boot</groupId>

<artifactId>spring-boot-maven-plugin</artifactId>

<executions>

<execution>

<goals>

<goal>repackage</goal>

</goals>

</execution>

</executions>

</plugin>

<plugin>

<groupId>com.spotify</groupId>

<artifactId>docker-maven-plugin</artifactId>

<version>1.2.2</version>

<configuration>

<imageName>${docker.registry}/${docker.registry.name}/${project.artifactId}</imageName>

<baseImage>${docker.image}</baseImage>

<maintainer>netkiller@msn.com</maintainer>

<volumes>/tmp</volumes>

<workdir>/srv</workdir>

<exposes>8080</exposes>

<env>

<JAVA_OPTS>-server -Xms128m -Xmx256m</JAVA_OPTS>

</env>

<entryPoint>["sh", "-c", "java ${JAVA_OPTS} -jar /srv/${project.build.finalName}.jar ${SPRING_OPTS}"]</entryPoint>

<resources>

<resource>

<targetPath>/srv</targetPath>

<directory>${project.build.directory}</directory>

<include>${project.build.finalName}.jar</include>

</resource>

</resources>

<!-- <image>${docker.image.prefix}/${project.artifactId}</image> -->

<!-- <newName>${docker.image.prefix}/${project.artifactId}:${project.version}</newName> -->

<!-- <serverId>docker-hub</serverId> -->

<registryUrl>http://${docker.registry}/v2/</registryUrl>

<imageTags>

<!-- <imageTag>${project.version}</imageTag> -->

<imageTag>latest</imageTag>

</imageTags>

</configuration>

</plugin>

</plugins>

</build>

</project>

Spring Cloud 配置文件

src/main/resources/bootstrap.yml

spring:

application:

name: spring-cloud-kubernetes-configmaps

profiles:

active: dev

cloud:

kubernetes:

reload:

enabled: true

mode: polling

period: 5000

config:

sources:

- name: ${spring.application.name}

group: cn.netkiller

namespace: default

management:

security:

enabled: false

#context-path: /

endpoints:

web:

exposure:

include: refresh

程序文件

SpringBootApplication 启动文件

package cn.netkiller.config;

import org.springframework.boot.SpringApplication;

import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication

public class App {

public static void main(String[] args) {

System.out.println("Hello World!");

SpringApplication.run(App.class, args);

}

}

配置类

package cn.netkiller.config;

import org.springframework.boot.context.properties.ConfigurationProperties;

import org.springframework.context.annotation.Configuration;

@Configuration

@ConfigurationProperties(prefix = "greeting")

public class KeyValueConfig {

private String message = "This is a default message";

public String getMessage() {

return this.message;

}

public void setMessage(String message) {

this.message = message;

}

}

控制器

package cn.netkiller.config;

import java.text.SimpleDateFormat;

import java.util.Date;

import org.springframework.beans.factory.annotation.Autowired;

import org.springframework.boot.context.properties.EnableConfigurationProperties;

import org.springframework.cloud.context.config.annotation.RefreshScope;

import org.springframework.web.bind.annotation.GetMapping;

import org.springframework.web.bind.annotation.RestController;

@RestController

@EnableConfigurationProperties(KeyValueConfig.class)

@RefreshScope

public class TestController {

@Autowired

private KeyValueConfig keyValueConfig;

@GetMapping("/")

public String index() {

return "Hello world\r\n";

}

@GetMapping("/hello")

public String hello() {

return keyValueConfig.getMessage() + " [" + new SimpleDateFormat().format(new Date()) + "]";

}

}

Kubernetes 编排脚本

config.yaml

apiVersion: v1

kind: Service

metadata:

annotations:

fabric8.io/git-commit: 729badc5e8578b67c1f9387ac0d1949b0646a991

fabric8.io/git-branch: master

fabric8.io/git-url: https://netkiller@github.com/netkiller/spring-cloud-kubernetes.git

fabric8.io/scm-url: https://github.com/spring-projects/spring-boot/kubernetes/ConfigMaps

fabric8.io/scm-tag: HEAD

prometheus.io/port: "9779"

prometheus.io/scrape: "true"

labels:

expose: "true"

app: ConfigMaps

provider: fabric8

version: 0.0.1-SNAPSHOT

group: cn.netkiller

name: config

spec:

ports:

- name: http

port: 8080

protocol: TCP

targetPort: 8080

selector:

app: ConfigMaps

provider: fabric8

group: cn.netkiller

type: NodePort

---

kind: ConfigMap

apiVersion: v1

metadata:

name: spring-cloud-kubernetes-configmaps

data:

application.yml: |-

greeting:

message: Say Hello to the World

farewell:

message: Say Goodbye

---

spring:

profiles: development

greeting:

message: Say Hello to the Developers

farewell:

message: Say Goodbye to the Developers

---

spring:

profiles: production

greeting:

message: Say Hello to the Ops

---

apiVersion: apps/v1

kind: Deployment

metadata:

annotations:

fabric8.io/git-commit: 729badc5e8578b67c1f9387ac0d1949b0646a991

fabric8.io/git-branch: master

fabric8.io/git-url: https://netkiller@github.com/netkiller/spring-cloud-kubernetes.git

fabric8.io/scm-url: https://github.com/spring-projects/spring-boot/kubernetes/ConfigMaps

fabric8.io/scm-tag: HEAD

labels:

app: ConfigMaps

provider: fabric8

version: 0.0.1-SNAPSHOT

group: cn.netkiller

name: config

spec:

replicas: 1

revisionHistoryLimit: 2

selector:

matchLabels:

app: ConfigMaps

provider: fabric8

group: cn.netkiller

template:

metadata:

annotations:

fabric8.io/git-commit: 729badc5e8578b67c1f9387ac0d1949b0646a991

fabric8.io/git-branch: master

fabric8.io/scm-tag: HEAD

fabric8.io/git-url: https://netkiller@github.com/netkiller/spring-cloud-kubernetes.git

fabric8.io/scm-url: https://github.com/spring-projects/spring-boot/kubernetes/ConfigMaps

labels:

app: ConfigMaps

provider: fabric8

version: 0.0.1-SNAPSHOT

group: cn.netkiller

spec:

containers:

- env:

- name: KUBERNETES_NAMESPACE

valueFrom:

fieldRef:

fieldPath: metadata.namespace

image: registry.netkiller.cn:5000/netkiller/configmaps:latest

#imagePullPolicy: IfNotPresent

name: spring-boot

ports:

- containerPort: 8080

name: http

protocol: TCP

- containerPort: 9779

name: prometheus

protocol: TCP

- containerPort: 8778

name: jolokia

protocol: TCP

securityContext:

privileged: false

测试

编译并构建 Docker 镜像

iMac:ConfigMaps neo$ mvn package docker:build

查看镜像是否正确产生

iMac:ConfigMaps neo$ docker images

REPOSITORY                                        TAG                 IMAGE ID            CREATED             SIZE

registry.netkiller.cn:5000/netkiller/configmaps   latest              93280aec434f        4 minutes ago       219MB

上传镜像到私有库

iMac:ConfigMaps neo$ docker push registry.netkiller.cn:5000/netkiller/configmaps

The push refers to repository [registry.netkiller.cn:5000/netkiller/configmaps]

f56e553c8b82: Pushed

7c1edc21f93f: Layer already exists

50644c29ef5a: Layer already exists

latest: digest: sha256:3ef48e858254ee4d578fe1737fd948b2679c33d28d0dc573cf1e8076d0a054a1 size: 952

开启 Spring cloud 访问 Kubernetes Config Maps 的权限。

kubectl create clusterrolebinding permissive-binding \

--clusterrole=cluster-admin \

--user=admin \

--user=kubelet \

--group=system:serviceaccounts

部署镜像

iMac:ConfigMaps neo$ kubectl create -f config.yaml

service/config created

configmap/spring-cloud-kubernetes-configmaps created

deployment.apps/config created

查看服务地址

iMac:ConfigMaps neo$ minikube service list

|----------------------|---------------------------|--------------|----------------------------|

|      NAMESPACE       |           NAME            | TARGET PORT  |            URL             |

|----------------------|---------------------------|--------------|----------------------------|

| default              | config                    | http/8080    | http://192.168.64.12:30662 |

| default              | kubernetes                | No node port |

| kube-system          | kube-dns                  | No node port |

| kubernetes-dashboard | dashboard-metrics-scraper | No node port |

| kubernetes-dashboard | kubernetes-dashboard      | No node port |

|----------------------|---------------------------|--------------|----------------------------|

访问地址，从 Config Maps 中获取配置项。

iMac:ConfigMaps neo$ curl http://192.168.64.12:30662/hello

Say Hello to the World [10/7/20, 11:25 AM]

修改配置增加了=*=，然后使用 kubectl apply -f config.yaml 刷新

iMac:ConfigMaps neo$ curl http://192.168.64.12:30662/hello

Say Hello to the World=*= [10/7/20, 11:26 AM]

配置刷新成功

2. 注册发现

有了 Kubernetes 注册发现，我们就可以抛弃 Eureka Server。

Maven 父项目

<?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>

<groupId>cn.netkiller</groupId>

<artifactId>kubernetes</artifactId>

<version>0.0.1-SNAPSHOT</version>

<packaging>pom</packaging>

<name>kubernetes</name>

<url>http://www.netkiller.cn</url>

<description>Spring Cloud with Kubernetes</description>

<organization>

<name>Netkiller Spring Cloud 手札</name>

<url>http://www.netkiller.cn</url>

</organization>

<developers>

<developer>

<name>Neo</name>

<email>netkiller@msn.com</email>

<organization>Netkiller Spring Cloud 手札</organization>

<organizationUrl>http://www.netkiller.cn</organizationUrl>

<roles>

<role>Author</role>

</roles>

</developer>

</developers>

<properties>

<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>

<spring-cloud.version>Hoxton.SR8</spring-cloud.version>

<docker.registry>registry.netkiller.cn:5000</docker.registry>

<docker.registry.name>netkiller</docker.registry.name>

<docker.image>mcr.microsoft.com/java/jre:15-zulu-alpine</docker.image>

</properties>

<parent>

<groupId>org.springframework.boot</groupId>

<artifactId>spring-boot-starter-parent</artifactId>

<version>2.3.4.RELEASE</version>

<relativePath />

</parent>

<dependencyManagement>

<dependencies>

<dependency>

<groupId>org.springframework.cloud</groupId>

<artifactId>spring-cloud-dependencies</artifactId>

<version>${spring-cloud.version}</version>

<type>pom</type>

<scope>import</scope>

</dependency>

</dependencies>

</dependencyManagement>

<dependencies>

<dependency>

<groupId>org.springframework.boot</groupId>

<artifactId>spring-boot-starter-actuator</artifactId>

</dependency>

<dependency>

<groupId>org.springframework.boot</groupId>

<artifactId>spring-boot-starter-test</artifactId>

<scope>test</scope>

</dependency>

<dependency>

<groupId>org.projectlombok</groupId>

<artifactId>lombok</artifactId>

</dependency>

</dependencies>

<modules>

<module>service</module>

<module>ConfigMaps</module>

<module>provider</module>

<module>consumer</module>

</modules>

</project>

provider

Maven 依赖

<?xml version="1.0"?>

<project xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd" xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">

<modelVersion>4.0.0</modelVersion>

<parent>

<groupId>cn.netkiller</groupId>

<artifactId>kubernetes</artifactId>

<version>0.0.1-SNAPSHOT</version>

</parent>

<groupId>cn.netkiller</groupId>

<artifactId>provider</artifactId>

<version>0.0.1-SNAPSHOT</version>

<name>provider</name>

<url>http://maven.apache.org</url>

<properties>

<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>

</properties>

<dependencies>

<dependency>

<groupId>org.springframework.cloud</groupId>

<artifactId>spring-cloud-starter-kubernetes</artifactId>

</dependency>

<dependency>

<groupId>org.springframework.boot</groupId>

<artifactId>spring-boot-starter-web</artifactId>

</dependency>

<dependency>

<groupId>org.springframework.boot</groupId>

<artifactId>spring-boot-starter-webflux</artifactId>

</dependency>

</dependencies>

<build>

<plugins>

<plugin>

<groupId>org.springframework.boot</groupId>

<artifactId>spring-boot-maven-plugin</artifactId>

</plugin>

<plugin>

<artifactId>maven-surefire-plugin</artifactId>

<configuration>

<skip>true</skip>

</configuration>

</plugin>

<plugin>

<groupId>com.spotify</groupId>

<artifactId>docker-maven-plugin</artifactId>

<version>1.2.2</version>

<configuration>

<imageName>${docker.registry}/${docker.registry.name}/${project.artifactId}</imageName>

<baseImage>${docker.image}</baseImage>

<maintainer>netkiller@msn.com</maintainer>

<volumes>/tmp</volumes>

<workdir>/srv</workdir>

<exposes>8080</exposes>

<env>

<JAVA_OPTS>-server -Xms128m -Xmx256m</JAVA_OPTS>

</env>

<entryPoint>["sh", "-c", "java ${JAVA_OPTS} -jar /srv/${project.build.finalName}.jar ${SPRING_OPTS}"]</entryPoint>

<resources>

<resource>

<targetPath>/srv</targetPath>

<directory>${project.build.directory}</directory>

<include>${project.build.finalName}.jar</include>

</resource>

</resources>

<!-- <image>${docker.image.prefix}/${project.artifactId}</image> -->

<!-- <newName>${docker.image.prefix}/${project.artifactId}:${project.version}</newName> -->

<!-- <serverId>docker-hub</serverId> -->

<registryUrl>http://${docker.registry}/v2/</registryUrl>

<imageTags>

<!-- <imageTag>${project.version}</imageTag> -->

<imageTag>latest</imageTag>

</imageTags>

</configuration>

</plugin>

</plugins>

</build>

</project>

Springboot 启动类

注意：这里必须使用 @EnableDiscoveryClient 注解，不能使用 @EnableEurekaClient。

他们的区别是 @EnableEurekaClient 只能注册到 Eureka Server，而 @EnableDiscoveryClient 不仅可以注册进 Eureka Server 还能注册到 ZooKeeper，Consul，Kubernetes 等等......

package cn.netkiller.provider;

import org.springframework.boot.SpringApplication;

import org.springframework.boot.autoconfigure.SpringBootApplication;

import org.springframework.cloud.client.discovery.EnableDiscoveryClient;

@SpringBootApplication

@EnableDiscoveryClient

public class ProviderApplication {

public static void main(String[] args) {

System.out.println("Hello World!");

SpringApplication.run(ProviderApplication.class, args);

}

}

控制器

package cn.netkiller.provider.controller;

import java.net.InetAddress;

import java.net.UnknownHostException;

import java.util.List;

import org.springframework.beans.factory.annotation.Autowired;

import org.springframework.cloud.client.discovery.DiscoveryClient;

import org.springframework.web.bind.annotation.GetMapping;

import org.springframework.web.bind.annotation.RestController;

import lombok.extern.slf4j.Slf4j;

@RestController

@Slf4j

public class ProviderController {

@Autowired

private DiscoveryClient discoveryClient;

@GetMapping("/")

public String index() {

return "Hello world!!!";

}

@GetMapping("/ping")

public String ping() {

try {

return InetAddress.getLocalHost().getHostName();

} catch (UnknownHostException e) {

return "Pong";

}

}

@GetMapping("/services")

public List<String> services() {

return this.discoveryClient.getServices();

}

}

@GetMapping("/services") 可以返回注册中心已经注册的服务。

application.properties 配置文件

src/main/resource/application.properties 配置文件

spring.application.name=provider

server.port=8080

Pod 启动后会以 spring.application.name 设置的名字注册到注册中心，Openfeign 将改名字访问微服务。

Kubernetes provider 编排脚本

apiVersion: v1

kind: Service

metadata:

name: provider

labels:

app.kubernetes.io/name: provider

spec:

type: ClusterIP

ports:

- port: 8080

targetPort: 8080

protocol: TCP

name: http

selector:

app.kubernetes.io/name: provider

---

apiVersion: apps/v1

kind: Deployment

metadata:

name: provider

labels:

app.kubernetes.io/name: provider

spec:

replicas: 1

selector:

matchLabels:

app.kubernetes.io/name: provider

template:

metadata:

labels:

app.kubernetes.io/name: provider

app.kubernetes.io/instance: sad-markhor

spec:

containers:

- name: provider

image: registry.netkiller.cn:5000/netkiller/provider

#imagePullPolicy: IfNotPresent

ports:

- name: http

containerPort: 8080

protocol: TCP

env:

- name: "KUBERNETES_NAMESPACE"

valueFrom:

fieldRef:

fieldPath: "metadata.namespace"

consumer

Maven 依赖

<?xml version="1.0"?>

<project xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd" xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">

<modelVersion>4.0.0</modelVersion>

<parent>

<groupId>cn.netkiller</groupId>

<artifactId>kubernetes</artifactId>

<version>0.0.1-SNAPSHOT</version>

</parent>

<groupId>cn.netkiller</groupId>

<artifactId>consumer</artifactId>

<version>0.0.1-SNAPSHOT</version>

<name>consumer</name>

<url>http://maven.apache.org</url>

<properties>

<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>

</properties>

<dependencies>

<dependency>

<groupId>org.springframework.cloud</groupId>

<artifactId>spring-cloud-starter-kubernetes</artifactId>

</dependency>

<dependency>

<groupId>org.springframework.cloud</groupId>

<artifactId>spring-cloud-starter-openfeign</artifactId>

</dependency>

<dependency>

<groupId>org.springframework.cloud</groupId>

<artifactId>spring-cloud-starter-kubernetes-ribbon</artifactId>

</dependency>

<dependency>

<groupId>org.springframework.boot</groupId>

<artifactId>spring-boot-starter-web</artifactId>

</dependency>

<dependency>

<groupId>org.springframework.boot</groupId>

<artifactId>spring-boot-starter-webflux</artifactId>

</dependency>

</dependencies>

<build>

<plugins>

<plugin>

<groupId>org.springframework.boot</groupId>

<artifactId>spring-boot-maven-plugin</artifactId>

</plugin>

<plugin>

<artifactId>maven-surefire-plugin</artifactId>

<configuration>

<skip>true</skip>

</configuration>

</plugin>

<plugin>

<groupId>com.spotify</groupId>

<artifactId>docker-maven-plugin</artifactId>

<version>1.2.2</version>

<configuration>

<imageName>${docker.registry}/${docker.registry.name}/${project.artifactId}</imageName>

<baseImage>${docker.image}</baseImage>

<maintainer>netkiller@msn.com</maintainer>

<volumes>/tmp</volumes>

<workdir>/srv</workdir>

<exposes>8080</exposes>

<env>

<JAVA_OPTS>-server -Xms128m -Xmx256m</JAVA_OPTS>

</env>

<entryPoint>["sh", "-c", "java ${JAVA_OPTS} -jar /srv/${project.build.finalName}.jar ${SPRING_OPTS}"]</entryPoint>

<resources>

<resource>

<targetPath>/srv</targetPath>

<directory>${project.build.directory}</directory>

<include>${project.build.finalName}.jar</include>

</resource>

</resources>

<!-- <image>${docker.image.prefix}/${project.artifactId}</image> -->

<!-- <newName>${docker.image.prefix}/${project.artifactId}:${project.version}</newName> -->

<!-- <serverId>docker-hub</serverId> -->

<registryUrl>http://${docker.registry}/v2/</registryUrl>

<imageTags>

<!-- <imageTag>${project.version}</imageTag> -->

<imageTag>latest</imageTag>

</imageTags>

</configuration>

</plugin>

</plugins>

</build>

</project>

Springboot 启动类

package cn.netkiller.consumer;

import org.springframework.boot.SpringApplication;

import org.springframework.boot.autoconfigure.SpringBootApplication;

import org.springframework.cloud.client.discovery.EnableDiscoveryClient;

import org.springframework.cloud.openfeign.EnableFeignClients;

@SpringBootApplication

@EnableDiscoveryClient

@EnableFeignClients

public class ConsumerApplication {

public static void main(String[] args) {

System.out.println("Hello World!");

SpringApplication.run(ConsumerApplication.class, args);

}

}

控制器

package cn.netkiller.consumer.controller;

import java.util.List;

import org.springframework.beans.factory.annotation.Autowired;

import org.springframework.cloud.client.ServiceInstance;

import org.springframework.cloud.client.discovery.DiscoveryClient;

import org.springframework.web.bind.annotation.GetMapping;

import org.springframework.web.bind.annotation.RestController;

import cn.netkiller.consumer.feign.ProviderClient;

import lombok.extern.slf4j.Slf4j;

@RestController

@Slf4j

public class ConsumerController {

@Autowired

private DiscoveryClient discoveryClient;

@Autowired

private ProviderClient providerClient;

@GetMapping("/")

public String index() {

return "Consumer OK\r\n";

}

@GetMapping("/service")

public Object getClient() {

return discoveryClient.getServices();

}

@GetMapping("/instance")

public List<ServiceInstance> getInstance(String instanceId) {

return discoveryClient.getInstances(instanceId);

}

@GetMapping("/ping")

public String ping() {

return OperationResponse.builder().status(true).data(providerClient.ping()).build();

}

}

@GetMapping("/ping") 将经过注册中心，获取到可用的服务，运行后从微服务返回结果。

package cn.netkiller.consumer.controller;

public class OperationResponse {

public boolean status = false;

public String data = "";

public static OperationResponse builder() {

// TODO Auto-generated method stub

return new OperationResponse();

}

public OperationResponse status(boolean status) {

this.status = status;

return this;

}

public OperationResponse data(String data) {

this.data = data;

return this;

}

public String build() {

return String.format("Status: %s, Data: %s", this.status, this.data);

}

}

FeignClient 接口

package cn.netkiller.consumer.feign;

import org.springframework.cloud.openfeign.FeignClient;

import org.springframework.stereotype.Component;

import org.springframework.web.bind.annotation.GetMapping;

@FeignClient(name = "provider", fallback = ProviderClientFallback.class)

public interface ProviderClient {

@GetMapping("/ping")

String ping();

}

@Component

class ProviderClientFallback implements ProviderClient {

@Override

public String ping() {

return "Error";

}

}

application.properties 配置文件

src/main/resource/application.properties 配置文件

spring.application.name=consumer

server.port=8080

provider.ribbon.KubernetesNamespace=default

provider.ribbon.NFLoadBalancerRuleClassName=com.netflix.loadbalancer.RandomRule

Kubernetes consumer 编排脚本

apiVersion: v1

kind: Service

metadata:

name: consumer

labels:

app.kubernetes.io/name: consumer

spec:

type: NodePort

ports:

- port: 8080

nodePort: 30080

protocol: TCP

name: http

selector:

app.kubernetes.io/name: consumer

---

apiVersion: apps/v1

kind: Deployment

metadata:

name: consumer

labels:

app.kubernetes.io/name: consumer

spec:

replicas: 1

selector:

matchLabels:

app.kubernetes.io/name: consumer

template:

metadata:

labels:

app.kubernetes.io/name: consumer

spec:

containers:

- name: consumer

image: registry.netkiller.cn:5000/netkiller/consumer

#imagePullPolicy: IfNotPresent

ports:

- name: http

containerPort: 8080

protocol: TCP

测试

编译，打包，构建Docker镜像

iMac:provider neo$ mvn package docker:build

iMac:consumer neo$ mvn package docker:build

推送镜像

iMac:spring-cloud-kubernetes neo$ docker images | grep registry.netkiller.cn

registry.netkiller.cn:5000/netkiller/consumer     latest              63921dc9b81d        27 seconds ago      238MB

registry.netkiller.cn:5000/netkiller/provider     latest              fbcc6b3a91ef        4 minutes ago       221MB

registry.netkiller.cn:5000/netkiller/configmaps   latest              93280aec434f        23 hours ago        219MB

iMac:spring-cloud-kubernetes neo$ docker push registry.netkiller.cn:5000/netkiller/consumer

The push refers to repository [registry.netkiller.cn:5000/netkiller/consumer]

51c839989a66: Pushed

7c1edc21f93f: Pushed

50644c29ef5a: Pushed

latest: digest: sha256:2591686cfc63888f3b97ca1e950205b58a47b3d3c618242465baa0796cf7e056 size: 952

iMac:spring-cloud-kubernetes neo$ docker push registry.netkiller.cn:5000/netkiller/provider

The push refers to repository [registry.netkiller.cn:5000/netkiller/provider]

47eb1c86b415: Pushed

7c1edc21f93f: Mounted from netkiller/consumer

50644c29ef5a: Mounted from netkiller/consumer

latest: digest: sha256:42cdeb63cd67a5edf9e769538878a0c8f18048bcde1ef9ba22d58766afa2d52d size: 952

iMac:spring-cloud-kubernetes neo$ curl -s http://registry.netkiller.cn:5000/v2/_catalog | jq

{

"repositories": [

"netkiller/configmaps",

"netkiller/consumer",

"netkiller/provider"

]

}

将 provider 和 consumer 应用部署到 Kubernetes

iMac:spring-cloud-kubernetes neo$ kubectl create -f provider/src/main/kubernetes/provider.yaml

service/provider created

deployment.apps/provider created

iMac:spring-cloud-kubernetes neo$ kubectl create -f consumer/src/main/kubernetes/consumer.yaml

service/consumer created

deployment.apps/consumer created

查看 consumer 端口

iMac:spring-cloud-kubernetes neo$ minikube service list

|----------------------|---------------------------|--------------|----------------------------|

|      NAMESPACE       |           NAME            | TARGET PORT  |            URL             |

|----------------------|---------------------------|--------------|----------------------------|

| default              | config                    | http/8080    | http://192.168.64.12:30662 |

| default              | consumer                  | http/8080    | http://192.168.64.12:30080 |

| default              | kubernetes                | No node port |

| default              | provider                  | No node port |

| kube-system          | kube-dns                  | No node port |

| kubernetes-dashboard | dashboard-metrics-scraper | No node port |

| kubernetes-dashboard | kubernetes-dashboard      | No node port |

|----------------------|---------------------------|--------------|----------------------------|

测试 provider 是否工作正常

$ curl -s http://10.10.0.121:8080/ping

provider-54875bf44-4gf49

$ curl -s http://10.10.0.121:8080/services |jq

[

"config",

"kubernetes",

"provider"

]

查看 consumer 是否已经注册成功

iMac:spring-cloud-kubernetes neo$ curl -s http://192.168.64.12:30080/service |jq

[

"config",

"consumer",

"kubernetes",

"provider"

]

iMac:spring-cloud-kubernetes neo$ curl http://192.168.64.12:30080/ping

Status: true, Data: provider-54875bf44-4gf49

增加 provider 节点，然后反复请求可以看到返回不同节点的主机名

iMac:spring-cloud-kubernetes neo$ kubectl scale deployment provider --replicas=3

deployment.apps/provider scaled

iMac:spring-cloud-kubernetes neo$ curl -s http://192.168.64.12:30080/ping

Status: true, Data: provider-54875bf44-8vs72

iMac:spring-cloud-kubernetes neo$ curl -s http://192.168.64.12:30080/ping

Status: true, Data: provider-54875bf44-4gf49i

测试完毕销毁服务

iMac:spring-cloud-kubernetes neo$ kubectl delete -f consumer/src/main/kubernetes/consumer.yaml

service "consumer" deleted

deployment.apps "consumer" deleted

iMac:spring-cloud-kubernetes neo$ kubectl delete -f provider/src/main/kubernetes/provider.yaml

service "provider" deleted

deployment.apps "provider" deleted

spring.cloud.kubernetes.discovery.enabled=false

第 65 章 Spring Cloud Alibaba

1. 安装 Nacos

Docker 安装 Nacos

安装 netkiller-devops 库

pip install netkiller-devops

创建 docker.py 编排文件

#!/usr/bin/env python3

from netkiller.docker import *

volume = Volumes()

volume.create('mysql')

mysql = Services('mysql')

mysql.image('mysql:5.7').container_name('mysql').restart('always').hostname('db.netkiller.cn').env_file(os.getcwd()+'/nacos/env/mysql.env')

mysql.ports(['3306:3306']).volumes([

'mysql:/var/lib/mysql'

]).command([

'--socket=/var/lib/mysql/mysql.sock',

'--default-authentication-plugin=mysql_native_password',

'--character-set-server=utf8mb4',

'--collation-server=utf8mb4_general_ci',

'--explicit_defaults_for_timestamp=true',

'--lower_case_table_names=1',

'--max_execution_time=0'

])

nacos = Services('nacos')

nacos.container_name('nacos').env_file(os.getcwd()+'/nacos/env/nacos-mysql.env')

# .environment([

# 'PREFER_HOST_MODE=hostname',

# 'MODE=standalone'

# ])

nacos.image('nacos/nacos-server').volumes([

'../nacos/logs/:/home/nacos/logs',

'../nacos/init.d/custom.properties:/home/nacos/init.d/custom.properties'

]).ports([

"8848:8848",

"9848:9848",

'9555:9555'

]).depends_on('mysql').restart('on-failure')

experiment = Composes('experiment')

experiment.version('3.9')

experiment.volumes(volume)

experiment.services(mysql)

experiment.services(nacos)

if __name__ == '__main__':

try:

docker = Docker()

docker.sysctl([{'vm.max_map_count':'262144'}])

docker.environment(experiment)

docker.main()

except KeyboardInterrupt:

print ("Crtl+C Pressed. Shutting down.")

查看帮助信息

[root@localhost ~]# python3 docker.py

Python controls the docker manager.

Usage: docker.py [options] up|rm|start|stop|restart|logs|top|images|exec <service>

Options:

-h, --help            show this help message and exit

--debug               debug mode

-e development|testing|production, --environment=development|testing|production

environment

-d, --daemon          run as daemon

--logfile=LOGFILE     logs file.

-l, --list            print service of environment

-f, --follow          following logging

-c, --compose         show docker compose

--export              export docker compose

Homepage: http://www.netkiller.cn Author: Neo <netkiller@msn.com>

启动 nacos

[root@localhost ~]# python3 docker.py -e experiment up nacos

mysql is up-to-date

Starting nacos ... done

[root@localhost ~]# python3 docker.py -e experiment ps

Name                   Command               State                                                         Ports

--------------------------------------------------------------------------------------------------------------------------------------------------------------------------

mysql           docker-entrypoint.sh --soc ...   Up      0.0.0.0:3306->3306/tcp,:::3306->3306/tcp, 33060/tcp

nacos           bin/docker-startup.sh            Up      0.0.0.0:8848->8848/tcp,:::8848->8848/tcp, 0.0.0.0:9555->9555/tcp,:::9555->9555/tcp,

0.0.0.0:9848->9848/tcp,:::9848->9848/tcp

查看启动端口

[root@localhost ~]# ss -lnt | grep -E "(8848|9848)"

LISTEN 0      1024         0.0.0.0:8848       0.0.0.0:*

LISTEN 0      1024         0.0.0.0:9848       0.0.0.0:*

LISTEN 0      1024            [::]:8848          [::]:*

LISTEN 0      1024            [::]:9848          [::]:*

测试配置中心

[root@localhost ~]# curl -X POST "http://127.0.0.1:8848/nacos/v1/cs/configs?dataId=nacos.cfg.dataId&group=test&content=helloWorld"

true

[root@localhost ~]# curl -X GET "http://127.0.0.1:8848/nacos/v1/cs/configs?dataId=nacos.cfg.dataId&group=test"

helloWorld

登陆 Web 界面 http://192.168.30.12:8848/nacos/ 默认的账号密码是：nacos/nacos

Kubernetes 安装 Nacos

创建 nacos 数据库用户

CREATE USER 'nacos'@'%' IDENTIFIED BY 'nacos';

GRANT ALL PRIVILEGES ON nacos.* TO 'nacos'@'%';

SHOW GRANTS FOR 'nacos'@'%';

前往 https://github.com/alibaba/nacos/blob/master/distribution/conf/nacos-mysql.sql 下来SQL文件，恢复到 nacos 数据中。

import sys

sys.path.insert(0, '/Users/neo/workspace/devops')

from netkiller.kubernetes import *

namespace = 'default'

config = ConfigMap('nacos')

config.apiVersion('v1')

config.metadata().name('nacos').namespace(namespace)

config.data({

'mysql.host': "rm-bp1g441na9an26wsb.mysql.rds.aliyuncs.com",

'mysql.port': "3306",

'mysql.dbname': "nacos",

'mysql.user': "nacos",

'mysql.password': "nacos"

})

# config.debug()

statefulSet = StatefulSet()

statefulSet = StatefulSet()

statefulSet.apiVersion('apps/v1')

statefulSet.metadata().name('nacos').labels(

{'app': 'nacos'}).namespace(namespace)

statefulSet.spec().replicas(3)

statefulSet.spec().serviceName('nacos')

statefulSet.spec().selector({'matchLabels': {'app': 'nacos'}})

statefulSet.spec().template().metadata().labels({'app': 'nacos'})

statefulSet.spec().template().metadata().annotations(

{'pod.alpha.kubernetes.io/initialized': "true"})

# statefulSet.spec().template().spec().affinity().nodeAffinity({

#     'requiredDuringSchedulingIgnoredDuringExecution': [

#         {'labelSelector': {

#             'matchExpressions': [

#                 {'key': 'app',

#                  'operator': 'In',

#                  'values': ['nacos']

#                  }]

#             },

#         'topologyKey': "kubernetes.io/hostname"

#         }

#     ]

# })

statefulSet.spec().template().spec().containers().name('nacos').imagePullPolicy(Define.containers.imagePullPolicy.IfNotPresent).image(

'nacos/nacos-server:latest').resources(

# {'requests': {

# # 'cpu':'200m',

# 'memory': "2Gi"}}

).ports([

{'name':'client','containerPort': 8848},

{'name':'client-rpc','containerPort': 9848},

{'name':'raft-rpc','containerPort': 9849}

]).env([

{'name': 'TZ', 'value': 'Asia/Shanghai'},

{'name': 'LANG', 'value': 'en_US.UTF-8'},

{'name': 'NACOS_REPLICAS', 'value': '1'},

# {'name': 'SPRING_DATASOURCE_PLATFORM', 'value': 'mysql'},

# {'name': 'MYSQL_SERVICE_HOST', 'value': 'mysql-0.mysql.default.svc.cluster.local'},

{'name': 'MYSQL_SERVICE_HOST', 'valueFrom':{'configMapKeyRef':{'name': 'nacos','key': 'mysql.host'}}},

{'name': 'MYSQL_SERVICE_PORT', 'valueFrom':{'configMapKeyRef':{'name': 'nacos','key': 'mysql.port'}}},

{'name': 'MYSQL_SERVICE_DB_NAME', 'valueFrom':{'configMapKeyRef':{'name': 'nacos','key': 'mysql.dbname'}}},

{'name': 'MYSQL_SERVICE_USER', 'valueFrom':{'configMapKeyRef':{'name': 'nacos','key': 'mysql.user'}}},

{'name': 'MYSQL_SERVICE_PASSWORD', 'valueFrom':{'configMapKeyRef':{'name': 'nacos','key': 'mysql.password'}}},

# {'name': 'MYSQL_SERVICE_DB_PARAM', 'value': 'characterEncoding=utf8&connectTimeout=1000&socketTimeout=3000&autoReconnect=true&useSSL=false&serverTimezone=GMT%2B8'},

{'name': 'NACOS_SERVER_PORT', 'value': '8848'},

{'name': 'NACOS_APPLICATION_PORT', 'value': '8848'},

{'name': 'PREFER_HOST_MODE', 'value': 'hostname'},

{'name': 'NACOS_SERVERS', 'value': 'nacos-0.nacos.default.svc.cluster.local:8848 nacos-1.nacos.default.svc.cluster.local:8848 nacos-2.nacos.default.svc.cluster.local:8848'},

# {'name': 'JVM_XMX', 'value': '4g'},

# {'name': 'NACOS_DEBUG', 'value': 'true'},

# {'name': 'TOMCAT_ACCESSLOG_ENABLED', 'value': 'true'},

])

# statefulSet.debug()

service = Service()

service.metadata().name('nacos')

service.metadata().namespace(namespace)

service.metadata().labels({'app':'nacos'})

service.spec().selector({'app': 'nacos'})

service.spec().type('ClusterIP')

service.spec().ports([

{'name': 'server', 'protocol': 'TCP', 'port': 8848, 'targetPort': 8848},

{'name': 'client-rpc', 'protocol': 'TCP', 'port': 9848, 'targetPort': 9848},

{'name': 'raft-rpc', 'protocol': 'TCP', 'port': 9555, 'targetPort': 9555}

])

# service.debug()

ingress = Ingress()

ingress.apiVersion('networking.k8s.io/v1')

ingress.metadata().name('nacos')

ingress.metadata().namespace(namespace)

# ingress.metadata().annotations({'kubernetes.io/ingress.class': 'nginx'})

ingress.spec().rules([{

'host': 'nacos.netkiller.com',

'http':{

'paths': [{

'pathType': Define.Ingress.pathType.Prefix,

'path': '/nacos',

'backend':{

'service':{

'name':'nacos',

'port':{'number': 8848}

}

}}]

}}])

# ingress.debug()

kubernetes = Kubernetes('/Volumes/Data/kubeconfig')

compose = Compose('nacos')

compose.add(config)

compose.add(statefulSet)

compose.add(service)

compose.add(ingress)

kubernetes.compose(compose)

kubernetes.main()

IP限制，白名单

location /nacos {

allow 192.168.0.0/24;

allow 172.18.0.0/16;

allow 202.104.66.10;

deny all;

proxy_set_header Host $host;

proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;

proxy_set_header X-Forwarded-Proto $scheme;

proxy_pass http://192.168.0.10:8848;

}

防火墙配置

配置防火墙，限制 8848 端口的访问策略，防止本地或其他服务注册到 Nacos 中。

$ iptables -A INPUT -s 172.18.5.0/24 -p tcp --dport 8848 -j REJECT

172.18.5.0/24 是办公网络，添加上面IP规则，可以防止开发人的电脑注册到测试环境。

删除规则

删除方法一

$ iptables -L -n --line-number | grep 8848

8    REJECT     tcp  --  172.18.5.0/24        0.0.0.0/0            tcp dpt:8848 reject-with icmp-port-unreachable

134  ACCEPT     tcp  --  0.0.0.0/0            172.17.0.119         tcp dpt:8848

$ iptables -D INPUT 8

删除方法二

$ iptables -D INPUT -s 172.18.5.0/24 -p tcp --dport 8848 -j REJECT

2. Kubernetes 部署微服务

pom.xml 中加入 docker 插件

<?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>

<groupId>com.example</groupId>

<artifactId>demo</artifactId>

<version>0.0.1-SNAPSHOT</version>

<packaging>jar</packaging>

<name>demo</name>

<description>Demo project for Spring Boot</description>

<parent>

<groupId>org.springframework.boot</groupId>

<artifactId>spring-boot-starter-parent</artifactId>

<version>2.6.3</version>

<relativePath /> <!-- lookup parent from repository -->

</parent>

<properties>

<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>

<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>

<java.version>1.8</java.version>

<sonar.projectKey>netkiller.cn_java_AX0HsoVkT19KeT2iVgUT</sonar.projectKey>

<sonar.qualitygate.wait>true</sonar.qualitygate.wait>

<docker.registry>registry.netkiller.cn/netkiller.cn</docker.registry>

</properties>

<repositories>

<repository>

<id>gitlab-maven</id>

<url>${env.CI_API_V4_URL}/projects/${env.CI_PROJECT_ID}/packages/maven</url>

</repository>

</repositories>

<distributionManagement>

<repository>

<id>gitlab-maven</id>

<url>${CI_API_V4_URL}/projects/${env.CI_PROJECT_ID}/packages/maven</url>

</repository>

<snapshotRepository>

<id>gitlab-maven</id>

<url>${CI_API_V4_URL}/projects/${env.CI_PROJECT_ID}/packages/maven</url>

</snapshotRepository>

</distributionManagement>

<dependencies>

<dependency>

<groupId>org.springframework.boot</groupId>

<artifactId>spring-boot-starter-web</artifactId>

</dependency>

<dependency>

<groupId>org.springframework.boot</groupId>

<artifactId>spring-boot-starter-test</artifactId>

<scope>test</scope>

</dependency>

<dependency>

<groupId>junit</groupId>

<artifactId>junit</artifactId>

<!-- <version>4.13.2</version> -->

<scope>test</scope>

</dependency>

</dependencies>

<build>

<plugins>

<plugin>

<groupId>org.springframework.boot</groupId>

<artifactId>spring-boot-maven-plugin</artifactId>

</plugin>

<plugin>

<groupId>com.spotify</groupId>

<artifactId>docker-maven-plugin</artifactId>

<version>1.2.2</version>

<configuration>

<imageName>${docker.registry}/${project.artifactId}</imageName>

<baseImage>openjdk:8-alpine</baseImage>

<maintainer>netkiller@msn.com</maintainer>

<volumes>/srv</volumes>

<workdir>/srv</workdir>

<env>

<JAVA_OPTS>-server -Xms512m -Xmx4096m -Djava.security.egd=file:/dev/./urandom</JAVA_OPTS>

</env>

<exposes>8080</exposes>

<entryPoint>["sh", "-c", "/srv/docker-entrypoint.sh"]</entryPoint>

<resources>

<resource>

<targetPath>/srv</targetPath>

<directory>${project.build.directory}</directory>

<include>${project.build.finalName}.jar</include>

</resource>

<resource>

<targetPath>/srv</targetPath>

<directory>.</directory>

<include>docker-entrypoint.sh</include>

</resource>

</resources>

<registryUrl>http://${docker.registry}/v2/</registryUrl>

<imageTags>

<imageTag>${project.version}</imageTag>

<imageTag>latest</imageTag>

</imageTags>

</configuration>

</plugin>

</plugins>

</build>

</project>

容器启动脚本

在项目目录创建 docker-entrypoint.sh 文件

#!/bin/sh

if [ ! -z $1 ]; then

MODULE=$1

shift

fi

if [ -z $JAVA_OPTS ]; then

JAVA_OPTS='-Xms1024m -Xmx4096m -XX:MetaspaceSize=128m -XX:MaxMetaspaceSize=512m -Djava.security.egd=file:/dev/./urandom -Duser.timezone=GMT+8 -Dfile.encoding=utf-8'

fi

if [ -z $MODULE ]; then

echo "MODULE environment is not set"

exit 127

else

PACKAGE=/srv/$MODULE.jar

fi

DEBUG='-Xdebug -Xrunjdwp:transport=dt_socket,suspend=n,server=y,address=5555'

SKYWALKING="-javaagent:/srv/skywalking/agent/skywalking-agent.jar -Dskywalking.collector.backend_service=oap.netkiller.cn:11800 -Dskywalking.agent.service_name=${MODULE}"

exec java ${JAVA_OPTS}  -jar ${PACKAGE} $@

暂时 DEBUG，SKYWALKING 没有使用，放在一遍不碍事。脚本的用法

./docker-entrypoint.sh your_module --server.port=8080

构建 docker 镜像

运行 mvn 命令构建 docker 镜像

neo@Netkiller-iMac ~/w/java.netkiller.cn (master)> mvn package docker:build docker:push

不出预料，你会看到下面输出

[INFO] Building image registry.netkiller.cn/netkiller.cn/demo

Step 1/9 : FROM openjdk:8-alpine

---> a3562aa0b991

Step 2/9 : MAINTAINER netkiller@msn.com

---> Using cache

---> b4a79be602ae

Step 3/9 : ENV JAVA_OPTS -server -Xms512m -Xmx4096m -Djava.security.egd=file:/dev/./urandom

---> Using cache

---> 9d685ea4a0d3

Step 4/9 : WORKDIR /srv

---> Using cache

---> e2feea451bb1

Step 5/9 : ADD /srv/demo-0.0.1-SNAPSHOT.jar /srv/

---> 7ad53fb991b8

Step 6/9 : ADD /srv/docker-entrypoint.sh /srv/

---> 39def6507064

Step 7/9 : EXPOSE 8080

---> Running in 338a99e6ec36

Removing intermediate container 338a99e6ec36

---> f192b73ab3b9

Step 8/9 : ENTRYPOINT ["sh", "-c", "/srv/docker-entrypoint.sh"]

---> Running in 5bda82acd305

Removing intermediate container 5bda82acd305

---> 85c1b2615a97

Step 9/9 : VOLUME /srv

---> Running in 27d71c55bf7e

Removing intermediate container 27d71c55bf7e

---> 64e0d8992fdd

ProgressMessage{id=null, status=null, stream=null, error=null, progress=null, progressDetail=null}

Successfully built 64e0d8992fdd

Successfully tagged registry.netkiller.cn/netkiller.cn/demo:latest

[INFO] Built registry.netkiller.cn/netkiller.cn/demo

[INFO] Tagging registry.netkiller.cn/netkiller.cn/demo with 0.0.1-SNAPSHOT

[INFO] Tagging registry.netkiller.cn/netkiller.cn/demo with latest

查看镜像

neo@Netkiller-iMac ~/w/java.netkiller.cn (master)> docker image ls | grep netkiller

registry.netkiller.cn/netkiller.cn/demo                       0.0.1-SNAPSHOT   64e0d8992fdd   3 minutes ago    122MB

registry.netkiller.cn/netkiller.cn/demo                       latest           64e0d8992fdd   3 minutes ago    122MB

编排 kubernetes 容器

from netkiller.kubernetes import *

namespace = 'default'

compose = Compose('development')

module = 'demo'

# version = '0.0.1-SNAPSHOT'

version = 'latest'

deployment = Deployment()

deployment.apiVersion('apps/v1')

deployment.metadata().name(module).labels({'app': module}).namespace(namespace)

deployment.spec().replicas(1)

deployment.spec().selector({'matchLabels': {'app': module}})

deployment.spec().template().metadata().labels({'app': module})

deployment.spec().template().spec().containers().name(module).image(

'registry.netkiller.cn/netkiller.cn/cloud.netkiller.cn:%s' % version).ports([{

'containerPort': 8080

}]).env([

{'name': 'TZ', 'value': 'Asia/Shanghai'},

{'name': 'LANG', 'value': 'en_US.UTF-8'},

]).args([module,'--server.port=8080'])

# deployment.debug()

# deployment.json()

service = Service()

service.metadata().name(module)

service.metadata().namespace(namespace)

service.spec().selector({'app': module})

service.spec().type('NodePort')

service.spec().ports([{

'name': 'http',

'protocol': 'TCP',

'port': 8080,

'targetPort': 8080

}])

compose.add(deployment)

compose.add(service)

print("=" * 40, "Compose", "=" * 40)

compose.debug()

compose.delete()

compose.create()⏎

查看容器运行状态

neo@Netkiller-iMac ~/w/java.netkiller.cn (master)> kubectl get pods

NAME                       READY   STATUS             RESTARTS   AGE

nginx-88c84c4d8-8pmzp      1/1     Running            1          3d20h

demo-76b7598b76-5hstp      1/1     Running            0          5h43m

busybox                    0/1     CrashLoopBackOff   52         4h44m

启动指定 nacos

容器中不方便修改配置文件，我们可以使用环境变量覆盖配置

JAVA_OPTS=-Dspring.cloud.nacos.username=nacos \

-Dspring.cloud.nacos.password=nacos \

-Dspring.cloud.nacos.config.server-addr=mse-032dbef0-nacos-ans.mse.aliyuncs.com:8848 \

-Dspring.cloud.nacos.discovery.server-addr=mse-032dbef0-nacos-ans.mse.aliyuncs.com:8848 \

相当于

java -Dspring.cloud.nacos.username=nacos \

-Dspring.cloud.nacos.password=nacos \

-Dspring.cloud.nacos.config.server-addr=mse-032dbef0-nacos-ans.mse.aliyuncs.com:8848 \

-Dspring.cloud.nacos.discovery.server-addr=mse-032dbef0-nacos-ans.mse.aliyuncs.com:8848 \

-jar netkiller.jar

3. Nacos 配置中心/注册中心代码实例

Maven

<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 https://maven.apache.org/xsd/maven-4.0.0.xsd">

<modelVersion>4.0.0</modelVersion>

<groupId>cn.netkiller</groupId>

<artifactId>bottleneck</artifactId>

<version>0.0.1-SNAPSHOT</version>

<name>bottleneck</name>

<description>bottleneck</description>

<organization>

<name>Netkiller Spring Cloud 手札</name>

<url>https://www.netkiller.cn</url>

</organization>

<developers>

<developer>

<name>Neo</name>

<email>netkiller@msn.com</email>

<organization>Netkiller Spring Cloud 手札</organization>

<organizationUrl>https://www.netkiller.cn</organizationUrl>

<roles>

<role>Author</role>

</roles>

</developer>

</developers>

<parent>

<groupId>org.springframework.boot</groupId>

<artifactId>spring-boot-starter-parent</artifactId>

<version>2.7.3</version>

<relativePath />

</parent>

<properties>

<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>

</properties>

<dependencies>

<dependency>

<groupId>org.springframework.boot</groupId>

<artifactId>spring-boot-starter-web</artifactId>

<!-- Exclude the Tomcat dependency -->

<exclusions>

<exclusion>

<groupId>org.springframework.boot</groupId>

<artifactId>spring-boot-starter-tomcat</artifactId>

</exclusion>

</exclusions>

</dependency>

<!-- Use Undertow instead Tomcat -->

<dependency>

<groupId>org.springframework.boot</groupId>

<artifactId>spring-boot-starter-undertow</artifactId>

</dependency>

<dependency>

<groupId>org.springframework.boot</groupId>

<artifactId>spring-boot-starter-webflux</artifactId>

</dependency>

<dependency>

<groupId>org.springframework.boot</groupId>

<artifactId>spring-boot-starter-jdbc</artifactId>

</dependency>

<dependency>

<groupId>org.springframework.boot</groupId>

<artifactId>spring-boot-starter-actuator</artifactId>

</dependency>

<dependency>

<groupId>org.springframework.boot</groupId>

<artifactId>spring-boot-starter-test</artifactId>

<scope>test</scope>

</dependency>

<dependency>

<groupId>mysql</groupId>

<artifactId>mysql-connector-java</artifactId>

</dependency>

<dependency>

<groupId>org.springframework.boot</groupId>

<artifactId>spring-boot-starter-data-redis</artifactId>

<exclusions>

<exclusion>

<groupId>io.lettuce</groupId>

<artifactId>lettuce-core</artifactId>

</exclusion>

</exclusions>

</dependency>

<dependency>

<groupId>redis.clients</groupId>

<artifactId>jedis</artifactId>

</dependency>

<dependency>

<groupId>org.springframework.data</groupId>

<artifactId>spring-data-commons</artifactId>

</dependency>

<dependency>

<groupId>org.projectlombok</groupId>

<artifactId>lombok</artifactId>

</dependency>

<dependency>

<groupId>org.springframework.boot</groupId>

<artifactId>spring-boot-starter-data-elasticsearch</artifactId>

</dependency>

<dependency>

<groupId>com.alibaba.cloud</groupId>

<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>

<version>2.2.9.RELEASE</version>

</dependency>

<dependency>

<groupId>com.alibaba.cloud</groupId>

<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>

<version>2.2.9.RELEASE</version>

</dependency>

<!-- https://mvnrepository.com/artifact/org.fluentd/fluent-logger -->

<dependency>

<groupId>org.fluentd</groupId>

<artifactId>fluent-logger</artifactId>

<version>0.3.4</version>

</dependency>

<dependency>

<groupId>com.sndyuk</groupId>

<artifactId>logback-more-appenders</artifactId>

<version>1.8.7</version>

</dependency>

<dependency>

<groupId>org.springframework.cloud</groupId>

<artifactId>spring-cloud-context</artifactId>

<version>3.1.4</version>

</dependency>

<dependency>

<groupId>org.springframework.cloud</groupId>

<artifactId>spring-cloud-commons</artifactId>

<version>3.1.4</version>

</dependency>

<dependency>

<groupId>org.springframework.cloud</groupId>

<artifactId>spring-cloud-starter-loadbalancer</artifactId>

<version>3.1.4</version>

</dependency>

<dependency>

<groupId>org.springframework.cloud</groupId>

<artifactId>spring-cloud-starter-bootstrap</artifactId>

<version>3.1.4</version>

</dependency>

</dependencies>

<build>

<plugins>

<plugin>

<groupId>org.springframework.boot</groupId>

<artifactId>spring-boot-maven-plugin</artifactId>

<configuration>

<mainClass>cn.netkiller.Application</mainClass>

</configuration>

</plugin>

<plugin>

<artifactId>maven-surefire-plugin</artifactId>

<configuration>

<skip>true</skip>

</configuration>

</plugin>

<plugin>

<groupId>com.spotify</groupId>

<artifactId>docker-maven-plugin</artifactId>

<version>1.2.2</version>

<configuration>

<imageName>netkiller/${project.artifactId}</imageName>

<baseImage>openjdk:19-alpine</baseImage>

<maintainer>netkiller@msn.com</maintainer>

<volumes>/tmp</volumes>

<workdir>/srv</workdir>

<exposes>8080</exposes>

<env>

<JAVA_OPTS>-server -Xms128m -Xmx2048m</JAVA_OPTS>

</env>

<entryPoint>["sh", "-c", "java ${JAVA_OPTS} -jar /srv/${project.build.finalName}.jar ${SPRING_OPTS}"]</entryPoint>

<resources>

<resource>

<targetPath>/srv</targetPath>

<directory>${project.build.directory}</directory>

<include>${project.build.finalName}.jar</include>

</resource>

</resources>

<image>netkiller/${project.artifactId}</image>

<newName>netkiller/${project.artifactId}:${project.version}</newName>

<!-- <serverId>docker-hub</serverId> -->

<registryUrl>http://${docker.registry}/v2/</registryUrl>

<imageTags>

<imageTag>undertow</imageTag>

<!-- <imageTag>tomcat</imageTag> <imageTag>${project.version}</imageTag> <imageTag>latest</imageTag> -->

</imageTags>

</configuration>

</plugin>

</plugins>

</build>

</project>

SpringBootApplication

package cn.netkiller;

import org.springframework.boot.SpringApplication;

import org.springframework.boot.autoconfigure.SpringBootApplication;

import org.springframework.cloud.client.discovery.EnableDiscoveryClient;

@EnableDiscoveryClient

@SpringBootApplication

public class Application {

public static void main(String[] args) {

System.out.println("Netkiller bottleneck tool!");

SpringApplication.run(Application.class, args);

}

}

ConfigController

package cn.netkiller.controller;

import org.springframework.beans.factory.annotation.Value;

import org.springframework.cloud.context.config.annotation.RefreshScope;

import org.springframework.web.bind.annotation.GetMapping;

import org.springframework.web.bind.annotation.RestController;

@RefreshScope

@RestController

public class ConfigController {

public ConfigController() {

// TODO Auto-generated constructor stub

}

@Value("${key}")

public String name;

@GetMapping("/config")

public String config() {

String name = this.name;

return name;

}

}

配置文件

#debug=true

server.port=8080

#server.tomcat.max-connections=10000

#server.tomcat.max-threads=4096

#server.tomcat.accept-count=1000

#server.tomcat.min-spare-threads=100

server.undertow.max-http-post-size=0

server.undertow.io-threads=16

server.undertow.worker-threads=4096

server.undertow.buffer-size=1024

server.undertow.buffers-per-region=1024

server.undertow.direct-buffers=true

spring.application.name=bottleneck

spring.profiles.active=dev

#spring.profiles.active=test

##spring.profiles.active=prod

#

#logging.file.path=/tmp

##logging.file.name=spring.log

#

endpoints.metrics.enabled=true

management.endpoints.jmx.exposure.include=*

management.endpoints.web.exposure.include=*

management.endpoints.health.show-details=always

#

spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver

spring.datasource.url=jdbc:mysql://172.18.200.5:3306/test?useUnicode=true&characterEncoding=UTF-8

spring.datasource.username=root

spring.datasource.password=passw0rd

spring.datasource.type=com.zaxxer.hikari.HikariDataSource

spring.datasource.hikari.minimum-idle=5

spring.datasource.hikari.maximum-pool-size=200

spring.datasource.hikari.auto-commit=true

spring.datasource.hikari.idle-timeout=30000

spring.datasource.hikari.pool-name=Hikari

spring.datasource.hikari.max-lifetime=55000

spring.datasource.hikari.connection-timeout=30000

spring.datasource.hikari.connection-test-query=SELECT 1

spring.redis.host=172.18.200.5

spring.redis.port=6379

spring.redis.password=passw0rd

spring.redis.database=0

spring.redis.jedis.pool.max-active=1000

spring.redis.jedis.pool.max-idle=80

spring.redis.jedis.pool.min-idle=20

spring.redis.jedis.pool.max-wait=-1

spring.cloud.nacos.server-addr=nacos.netkiller.cn:8848

spring.cloud.nacos.username=neo

spring.cloud.nacos.password=netkiller

#spring.cloud.nacos.config.enable-remote-sync-config=true

spring.cloud.nacos.config.namespace=${spring.profiles.active}

spring.cloud.nacos.config.group=DEFAULT_GROUP

spring.cloud.nacos.config.prefix=${spring.application.name}

spring.cloud.nacos.config.file-extension=yaml

spring.cloud.nacos.discovery.service=${spring.application.name:DEFAULT-SERVICE-NAME}

spring.cloud.nacos.discovery.namespace=${spring.profiles.active}

4. FAQ

禁用 Nacos

当 Maven 引入了 nacos 依赖，启动就会要求配置 Nacos，可以通过下面方法禁用 Nacos

spring.cloud.nacos.config.enabled=false

spring.cloud.nacos.discovery.enabled=false

spring.cloud.nacos.config.refresh-enabled=false

spring.cloud.nacos.discovery.instance-enabled=false

禁止注册

有时我们希望只获取配置，并不希望服务注册

register-enabled: false

spring:

profiles:

active: dev

application:

name: netkiller

main:

allow-bean-definition-overriding: true

cloud:

nacos:

username: dev

password: passw0rd

config:

server-addr: http://${spring.profiles.active}.netkiller.cn:8848

file-extension: yaml

enabled: true

namespace: ${spring.profiles.active}

enable-remote-sync-config: true

discovery:

server-addr: http://${spring.profiles.active}.netkiller.cn:8848

namespace: ${spring.profiles.active}

register-enabled: true

Failed to bind properties under 'server.tomcat.basedir' to java.io.File:

环境 Docker 安装 nacos 提示 server.tomcat.basedir 错误

nacos  | ***************************

nacos  | APPLICATION FAILED TO START

nacos  | ***************************

nacos  |

nacos  | Description:

nacos  |

nacos  | Failed to bind properties under 'server.tomcat.basedir' to java.io.File:

nacos  |

nacos  |     Property: server.tomcat.basedir

nacos  |     Value:

nacos  |     Origin: InputStream resource [resource loaded through InputStream] - 34:0

nacos  |     Reason: failed to convert java.lang.String to java.io.File (caused by java.lang.IllegalStateException: Could not retrieve file for class path resource []: class path resource [] cannot be resolved to absolute file path because it does not reside in the file system: jar:file:/home/nacos/target/nacos-server.jar!/BOOT-INF/classes!/)

nacos  |

nacos  | Action:

nacos  |

nacos  | Update your application's configuration

解决方案

进入容器复制 application.properties 文件到本地，修改 server.tomcat.basedir= 配置，改为 server.tomcat.basedir=. 然后在挂载到容器卷中。

[root@cloud ops.sfzito.com]# docker run -it --entrypoint sh  nacos/nacos-server

sh-4.2# cat /home/nacos/conf/application.properties

# spring

server.servlet.contextPath=${SERVER_SERVLET_CONTEXTPATH:/nacos}

server.contextPath=/nacos

server.port=${NACOS_APPLICATION_PORT:8848}

spring.datasource.platform=${SPRING_DATASOURCE_PLATFORM:""}

nacos.cmdb.dumpTaskInterval=3600

nacos.cmdb.eventTaskInterval=10

nacos.cmdb.labelTaskInterval=300

nacos.cmdb.loadDataAtStart=false

db.num=${MYSQL_DATABASE_NUM:1}

db.url.0=jdbc:mysql://${MYSQL_SERVICE_HOST}:${MYSQL_SERVICE_PORT:3306}/${MYSQL_SERVICE_DB_NAME}?${MYSQL_SERVICE_DB_PARAM:characterEncoding=utf8&connectTimeout=1000&socketTimeout=3000&autoReconnect=true&useSSL=false}

db.url.1=jdbc:mysql://${MYSQL_SERVICE_HOST}:${MYSQL_SERVICE_PORT:3306}/${MYSQL_SERVICE_DB_NAME}?${MYSQL_SERVICE_DB_PARAM:characterEncoding=utf8&connectTimeout=1000&socketTimeout=3000&autoReconnect=true&useSSL=false}

db.user=${MYSQL_SERVICE_USER}

db.password=${MYSQL_SERVICE_PASSWORD}

### The auth system to use, currently only 'nacos' and 'ldap' is supported:

nacos.core.auth.system.type=${NACOS_AUTH_SYSTEM_TYPE:nacos}

### worked when nacos.core.auth.system.type=nacos

### The token expiration in seconds:

nacos.core.auth.plugin.nacos.token.expire.seconds=${NACOS_AUTH_TOKEN_EXPIRE_SECONDS:18000}

### The default token:

nacos.core.auth.plugin.nacos.token.secret.key=${NACOS_AUTH_TOKEN:SecretKey012345678901234567890123456789012345678901234567890123456789}

### Turn on/off caching of auth information. By turning on this switch, the update of auth information would have a 15 seconds delay.

nacos.core.auth.caching.enabled=${NACOS_AUTH_CACHE_ENABLE:false}

nacos.core.auth.enable.userAgentAuthWhite=${NACOS_AUTH_USER_AGENT_AUTH_WHITE_ENABLE:false}

nacos.core.auth.server.identity.key=${NACOS_AUTH_IDENTITY_KEY:serverIdentity}

nacos.core.auth.server.identity.value=${NACOS_AUTH_IDENTITY_VALUE:security}

server.tomcat.accesslog.enabled=${TOMCAT_ACCESSLOG_ENABLED:false}

server.tomcat.accesslog.pattern=%h %l %u %t "%r" %s %b %D

# default current work dir

server.tomcat.basedir=

## spring security config

### turn off security

nacos.security.ignore.urls=${NACOS_SECURITY_IGNORE_URLS:/,/error,/**/*.css,/**/*.js,/**/*.html,/**/*.map,/**/*.svg,/**/*.png,/**/*.ico,/console-fe/public/**,/v1/auth/**,/v1/console/health/**,/actuator/**,/v1/console/server/**}

# metrics for elastic search

management.metrics.export.elastic.enabled=false

management.metrics.export.influx.enabled=false

nacos.naming.distro.taskDispatchThreadCount=10

nacos.naming.distro.taskDispatchPeriod=200

nacos.naming.distro.batchSyncKeyCount=1000

nacos.naming.distro.initDataRatio=0.9

nacos.naming.distro.syncRetryDelay=5000

nacos.naming.data.warmup=true

不读取 bootstrap.yaml 文件

Nacos 是一个独立项目，并不依赖 Spring Cloud，如果只是想使用配置中心，在 Springboot 中引入 Nacos 依赖，你会发现 Springboot 不读取 bootstrap.yaml。

解决方法

<dependency>

<groupId>org.springframework.cloud</groupId>

<artifactId>spring-cloud-starter-bootstrap</artifactId>

<version>3.1.4</version>

</dependency>

WARN [com.alibaba.nacos.client.naming:177] [,] - out of date data received, old-t: 1665711914993, new-t: 1665711902390

出错信息如下，故障是因为每个节点的时间不同步造成的。

[2022-10-14 09:44:51.289 [com.alibaba.nacos.naming.push.receiver] WARN [com.alibaba.nacos.client.naming:177] [,] - out of date data received, old-t: 1665711914993, new-t: 1665711902390

解决方法，安装时间同步软件。例如 NTP

User limit of inotify instances reached or too many open files

nacos  | 09:22:23.431 [main] ERROR org.springframework.boot.SpringApplication - Application run failed

nacos  | com.alibaba.nacos.api.exception.runtime.NacosRuntimeException: ErrCode:500, ErrMsg:User limit of inotify instances reached or too many open files

nacos  | at com.alibaba.nacos.core.listener.StartingApplicationListener.loadPreProperties(StartingApplicationListener.java:161)

nacos  | at com.alibaba.nacos.core.listener.StartingApplicationListener.environmentPrepared(StartingApplicationListener.java:100)

nacos  | at com.alibaba.nacos.core.code.SpringApplicationRunListener.environmentPrepared(SpringApplicationRunListener.java:60)

nacos  | at org.springframework.boot.SpringApplicationRunListeners.lambda$environmentPrepared$2(SpringApplicationRunListeners.java:66)

nacos  | at java.util.ArrayList.forEach(ArrayList.java:1259)

nacos  | at org.springframework.boot.SpringApplicationRunListeners.doWithListeners(SpringApplicationRunListeners.java:120)

nacos  | at org.springframework.boot.SpringApplicationRunListeners.doWithListeners(SpringApplicationRunListeners.java:114)

nacos  | at org.springframework.boot.SpringApplicationRunListeners.environmentPrepared(SpringApplicationRunListeners.java:65)

nacos  | at org.springframework.boot.SpringApplication.prepareEnvironment(SpringApplication.java:343)

nacos  | at org.springframework.boot.SpringApplication.run(SpringApplication.java:301)

nacos  | at org.springframework.boot.SpringApplication.run(SpringApplication.java:1317)

nacos  | at org.springframework.boot.SpringApplication.run(SpringApplication.java:1306)

nacos  | at com.alibaba.nacos.Nacos.main(Nacos.java:35)

nacos  | at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)

nacos  | at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)

nacos  | at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)

nacos  | at java.lang.reflect.Method.invoke(Method.java:498)

nacos  | at org.springframework.boot.loader.MainMethodRunner.run(MainMethodRunner.java:49)

nacos  | at org.springframework.boot.loader.Launcher.launch(Launcher.java:108)

nacos  | at org.springframework.boot.loader.Launcher.launch(Launcher.java:58)

nacos  | at org.springframework.boot.loader.PropertiesLauncher.main(PropertiesLauncher.java:467)

nacos  | Caused by: com.alibaba.nacos.api.exception.NacosException: java.io.IOException: User limit of inotify instances reached or too many open files

nacos  | at com.alibaba.nacos.sys.file.WatchFileCenter$WatchDirJob.<init>(WatchFileCenter.java:184)

nacos  | at com.alibaba.nacos.sys.file.WatchFileCenter.registerWatcher(WatchFileCenter.java:92)

nacos  | at com.alibaba.nacos.core.listener.StartingApplicationListener.registerWatcher(StartingApplicationListener.java:167)

nacos  | at com.alibaba.nacos.core.listener.StartingApplicationListener.loadPreProperties(StartingApplicationListener.java:159)

nacos  | ... 20 common frames omitted

nacos  | Caused by: java.io.IOException: User limit of inotify instances reached or too many open files

nacos  | at sun.nio.fs.LinuxWatchService.<init>(LinuxWatchService.java:64)

nacos  | at sun.nio.fs.LinuxFileSystem.newWatchService(LinuxFileSystem.java:47)

nacos  | at com.alibaba.nacos.sys.file.WatchFileCenter$WatchDirJob.<init>(WatchFileCenter.java:179)

nacos  | ... 23 common frames omitted

nacos  | 09:22:23.435 [Thread-4] WARN com.alibaba.nacos.common.executor.ThreadPoolManager - [ThreadPoolManager] Start destroying ThreadPool

nacos  | 09:22:23.435 [Thread-4] WARN com.alibaba.nacos.common.executor.ThreadPoolManager - [ThreadPoolManager] Destruction of the end

加大 fs.inotify.max_user_instances 配置即可，默认是 128

sysctl fs.inotify.max_user_watches

sudo sysctl -w fs.inotify.max_user_watches=50889300

sysctl fs.inotify.max_user_instances

sudo sysctl -w fs.inotify.max_user_instances=65535

开启权限

nacos.core.auth.enabled= true

在容器中设置开启验证：NACOS_AUTH_ENABLE=true

ERROR Whitelabel

当Nacos开启了认证配置nacos.core.auth.enabled=true时，当前账号没有该命名空间的权限，就会出现下面的错误提示。

2022-11-07 18:42:04,370 [,,,] INFO com.alibaba.nacos.client.config.impl.LocalConfigInfoProcessor:212 [main] - LOCAL_SNAPSHOT_PATH:/root/nacos/config

Mon, Nov 7 2022 6:42:04 pm 2022-11-07 18:42:04,393 [,,,] ERROR com.alibaba.nacos.client.config.impl.ClientWorker:304 [main] - [fixed-nacos.default.svc.cluster.local_8848-test] [sub-server-error] no right, dataId=netkiller, group=DEFAULT_GROUP, tenant=test

Mon, Nov 7 2022 6:42:04 pm 2022-11-07 18:42:04,393 [,,,] ERROR com.alibaba.cloud.nacos.client.NacosPropertySourceBuilder:104 [main] - get data from Nacos error,dataId:netkiller

Mon, Nov 7 2022 6:42:04 pm com.alibaba.nacos.api.exception.NacosException: <html><body><h1>Whitelabel Error Page</h1><p>This application has no explicit mapping for /error, so you are seeing this as a fallback.</p><div id='created'>Mon Nov 07 18:42:04 CST 2022</div><div>There was an unexpected error (type=Forbidden, status=403).</div></body></html>

解决方案，在权限控制->权限管理中「添加权限」可以解决。

spring:

cloud:

nacos:

discovery:

server-addr: 127.0.0.1:8848

metadata:

preserved.heart.beat.interval: 1000 # 客户端上报心跳的间隔时间。（单位:毫秒）

preserved.heart.beat.timeout: 3000 # 不发送心跳后，从健康到不健康的时间。（单位:毫秒）

preserved.ip.delete.timeout: 3000 # 不发送心跳后，被nacos下掉该实例的时间。（单位:毫秒）

Spring cloud 网关 Gateway 的 ribbion 配置

#ribbon config,Interval to refresh the server list from the source

ribbon:

ServerListRefreshInterval: 3000

最终 Openfeign 获得注册服务的时间是 Nacos + Gateway = 6ms

第 66 章 FAQ

1. Cannot execute request on any known server

com.netflix.discovery.shared.transport.TransportException: Cannot execute request on any known server

解决方法，禁用 CSRF

package cn.netkiller.eureka.config;

import org.springframework.context.annotation.Configuration;

import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;

import org.springframework.security.config.annotation.web.builders.HttpSecurity;

import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;

import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;

@Configuration

@EnableWebSecurity

public class SecurityConfigurerAdapter extends WebSecurityConfigurerAdapter {

@Override

protected void configure(HttpSecurity http) throws Exception {

http.csrf().disable();

super.configure(http);

}

@Override

protected void configure(AuthenticationManagerBuilder auth) throws Exception {

super.configure(auth);

}

}

2. @EnableDiscoveryClient与@EnableEurekaClient 区别

相同点：@EnableDiscoveryClient、@EnableEurekaClient  这二个注解作用，都可以让该服务注册到注册中心上去。

不同点：@EnableEurekaClient 只支持Eureka注册中心，@EnableDiscoveryClient 支持Eureka、Zookeeper、Consul 这三个注册中心。

3. Feign请求超时

方法一，修改配置是让Hystrix的超时时间改为5秒

hystrix.command.default.execution.isolation.thread.timeoutInMilliseconds: 5000

方法二，修改配置，禁用Hystrix的超时时间

hystrix.command.default.execution.timeout.enabled: false

方法三，修改配置，用于索性禁用feign的hystrix。

feign.hystrix.enabled: false

4. 已停止的微服务节点注销慢或不注销

由于 Eureka Server 清理无效节点周期长默认为90秒，可能会遇到微服务注销慢甚至不注销的问题。

Eureka Server 配置，注意仅适合开发环境。

# 设为false, 关闭自我保护， 从而保证会注销微服务

eureka.server.enable-self-preservation=false

# 清理间隔（单位毫秒，默认是60 * 1000）

eureka.server.eviction-interval-timer-in-ms=30000

Eureka Client

配置开启健康检查，续约更新时间和到期时间。

# 设为true，开启健康检查（需要spring-boot-starter-actuator 依赖）

eureka.client.healthcheck.enabled=ture

# 续约更新时间间隔（默认是30秒）

eureka.instance.lease-renewal-interval-in-seconds=20000

# 续约到期时间（默认90秒)

eureka.instance.lease-expiration-duration-in-seconds=30000

5. Feign 启动出错 PathVariable annotation was empty on param 0.

问题分析，@PathVariable 找不到对应的参数

package api.feign;

import java.util.List;

import java.util.Map;

import org.springframework.cloud.netflix.feign.FeignClient;

import org.springframework.web.bind.annotation.PathVariable;

import org.springframework.web.bind.annotation.RequestMapping;

@FeignClient("restful-api-service")

public interface Search {

@RequestMapping("/search/article/list")

public List<Map<String, Object>> list();

@RequestMapping("/search/article/{articleId}")

public Object read(@PathVariable String articleId);

}

解决方案

@RequestMapping("/search/article/{articleId}")

public Object read(@PathVariable("articleId") String articleId);

6. Feign 提示 Consider defining a bean of type 'common.feign.Cms' in your configuration.

背景：我们需要共用 Feign 接口，故将 Feign 放到共用的 common-version.jar 包中，供其他项目使用。

启动提示：Consider defining a bean of type 'common.feign.Cms' in your configuration.

注解加入包位置后解决

@EnableFeignClients("common.feign")

例 66.1. Share feign interface.

package cn.netkiller.feign;

import org.springframework.boot.SpringApplication;

import org.springframework.boot.autoconfigure.SpringBootApplication;

import org.springframework.cloud.netflix.eureka.EnableEurekaClient;

import org.springframework.cloud.netflix.feign.EnableFeignClients;

@SpringBootApplication

@EnableEurekaClient

@EnableFeignClients("common.feign")

public class Application {

public static void main(String[] args) {

System.out.println("Feign Starting...");

SpringApplication.run(Application.class, args);

}

}

7. Load balancer does not have available server for client

com.netflix.client.ClientException: Load balancer does not have available server for client: restful

8. Eureka Client (Dalston.SR1)

Maven

<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>

<groupId>cn.netkiller.spring.cloud</groupId>

<artifactId>eureka.client</artifactId>

<version>0.0.1-SNAPSHOT</version>

<packaging>jar</packaging>

<name>eureka.client</name>

<url>http://maven.apache.org</url>

<properties>

<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>

</properties>

<parent>

<groupId>org.springframework.boot</groupId>

<artifactId>spring-boot-starter-parent</artifactId>

<version>1.5.3.RELEASE</version>

<relativePath />

</parent>

<dependencyManagement>

<dependencies>

<dependency>

<groupId>org.springframework.cloud</groupId>

<artifactId>spring-cloud-dependencies</artifactId>

<version>Dalston.SR1</version>

<type>pom</type>

<scope>import</scope>

</dependency>

</dependencies>

</dependencyManagement>

<dependencies>

<dependency>

<groupId>org.springframework.boot</groupId>

<artifactId>spring-boot-starter-test</artifactId>

<scope>test</scope>

</dependency>

<dependency>

<groupId>org.springframework.cloud</groupId>

<artifactId>spring-cloud-starter-config</artifactId>

</dependency>

<dependency>

<groupId>org.springframework.cloud</groupId>

<artifactId>spring-cloud-starter-eureka</artifactId>

</dependency>

</dependencies>

<build>

<plugins>

<plugin>

<groupId>org.apache.maven.plugins</groupId>

<artifactId>maven-surefire-plugin</artifactId>

<configuration>

<skip>true</skip>

</configuration>

</plugin>

</plugins>

</build>

</project>

Application

package cn.netkiller.spring.cloud.eureka.client;

import org.springframework.boot.SpringApplication;

import org.springframework.boot.autoconfigure.SpringBootApplication;

import org.springframework.cloud.client.discovery.EnableDiscoveryClient;

@SpringBootApplication

@EnableDiscoveryClient

public class Application {

public static void main(String[] args) {

SpringApplication.run(Application.class, args);

}

}

RestController

package cn.netkiller.spring.cloud.eureka.client;

import java.util.List;

import org.slf4j.Logger;

import org.slf4j.LoggerFactory;

import org.springframework.beans.factory.annotation.Autowired;

import org.springframework.cloud.client.ServiceInstance;

import org.springframework.cloud.client.discovery.DiscoveryClient;

import org.springframework.web.bind.annotation.PathVariable;

import org.springframework.web.bind.annotation.RequestMapping;

import org.springframework.web.bind.annotation.RequestMethod;

import org.springframework.web.bind.annotation.RequestParam;

import org.springframework.web.bind.annotation.RestController;

@RestController

public class TestRestController {

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

@RequestMapping("/")

public String home() {

logger.info("Hello!!!");

return "Hello World";

}

@Autowired

private DiscoveryClient discoveryClient;

@RequestMapping("/service-instances/{applicationName}")

public List<ServiceInstance> serviceInstancesByApplicationName(@PathVariable String applicationName) {

return this.discoveryClient.getInstances(applicationName);

}

@RequestMapping(value = "/add", method = RequestMethod.GET)

public Integer add(@RequestParam Integer a, @RequestParam Integer b) {

@SuppressWarnings("deprecation")

ServiceInstance instance = discoveryClient.getLocalServiceInstance();

Integer r = a + b;

logger.info("/add, host:" + instance.getHost() + ", service_id:" + instance.getServiceId() + ", result:" + r);

return r;

}

@RequestMapping("/greeting")

public String greeting() {

return "GREETING";

}

}

application.properties

spring.application.name=test-service

server.port=8080

eureka.client.serviceUrl.defaultZone=http://localhost:8761/eureka/

测试

首先确认客户端已经注册到 http://localhost:8761/

$ curl http://localhost:8080/service-instances/test-service

[

{

"host": "Neo-Desktop",

"port": 8080,

"secure": false,

"uri": "http://Neo-Desktop:8080",

"serviceId": "TEST-SERVICE",

"metadata": {},

"instanceInfo": {

"instanceId": "Neo-Desktop:test-service:8080",

"app": "TEST-SERVICE",

"appGroupName": null,

"ipAddr": "172.25.10.150",

"sid": "na",

"homePageUrl": "http://Neo-Desktop:8080/",

"statusPageUrl": "http://Neo-Desktop:8080/info",

"healthCheckUrl": "http://Neo-Desktop:8080/health",

"secureHealthCheckUrl": null,

"vipAddress": "test-service",

"secureVipAddress": "test-service",

"countryId": 1,

"dataCenterInfo": {

"@class": "com.netflix.appinfo.InstanceInfo$DefaultDataCenterInfo",

"name": "MyOwn"

},

"hostName": "Neo-Desktop",

"status": "UP",

"leaseInfo": {

"renewalIntervalInSecs": 30,

"durationInSecs": 90,

"registrationTimestamp": 1497922681680,

"lastRenewalTimestamp": 1497922681680,

"evictionTimestamp": 0,

"serviceUpTimestamp": 1497922003783

},

"isCoordinatingDiscoveryServer": false,

"metadata": {},

"lastUpdatedTimestamp": 1497922681680,

"lastDirtyTimestamp": 1497922681025,

"actionType": "ADDED",

"asgName": null,

"overriddenStatus": "UNKNOWN"

}

}

]

add 接口测试

curl http://localhost:8080/add.json?a=5&b=3

8

9. Config Server(1.3.1.RELEASE)

Server

Maven

<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>

<groupId>cn.netkiller</groupId>

<artifactId>config</artifactId>

<version>0.0.1-SNAPSHOT</version>

<packaging>jar</packaging>

<name>config</name>

<url>http://maven.apache.org</url>

<properties>

<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>

<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>

<java.version>1.8</java.version>

</properties>

<parent>

<groupId>org.springframework.boot</groupId>

<artifactId>spring-boot-starter-parent</artifactId>

<version>1.5.6.RELEASE</version>

<relativePath />

</parent>

<dependencyManagement>

<dependencies>

<dependency>

<groupId>org.springframework.cloud</groupId>

<artifactId>spring-cloud-config</artifactId>

<version>1.3.1.RELEASE</version>

<type>pom</type>

<scope>import</scope>

</dependency>

</dependencies>

</dependencyManagement>

<dependencies>

<dependency>

<groupId>org.springframework.cloud</groupId>

<artifactId>spring-cloud-config-server</artifactId>

</dependency>

<dependency>

<groupId>org.springframework.boot</groupId>

<artifactId>spring-boot-starter-test</artifactId>

<scope>test</scope>

</dependency>

</dependencies>

<build>

<plugins>

<plugin>

<groupId>org.springframework.boot</groupId>

<artifactId>spring-boot-maven-plugin</artifactId>

</plugin>

</plugins>

</build>

</project>

Application

Application

package cn.netkiller.cloud;

import org.springframework.boot.SpringApplication;

import org.springframework.boot.autoconfigure.EnableAutoConfiguration;

import org.springframework.boot.autoconfigure.SpringBootApplication;

import org.springframework.cloud.client.discovery.EnableDiscoveryClient;

import org.springframework.cloud.config.server.EnableConfigServer;

import org.springframework.context.annotation.Configuration;

@Configuration

@EnableAutoConfiguration

@EnableDiscoveryClient

@EnableConfigServer

@SpringBootApplication

public class Application {

public static void main(String[] args) {

SpringApplication.run(Application.class, args);

}

}

application.properties

server.port=8888

spring.cloud.config.server.git.uri=https://github.com/netkiller/config.git

Git 仓库

克隆仓库

git clone https://github.com/netkiller/config.git

创建配置文件 server-development.properties

vim server-development.properties

test.a=KKOOKK

message=Hello world

提交配置文件

git commit -a

git push

测试服务器

neo@netkiller $ curl http://localhost:8888/server-development.json

{"message":"Hello world","test":{"a":"KKOOKK"}}

Client

Maven pom.xml

<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>

<groupId>netkiller.cn</groupId>

<artifactId>cloud</artifactId>

<version>0.0.1-SNAPSHOT</version>

<parent>

<groupId>org.springframework.boot</groupId>

<artifactId>spring-boot-starter-parent</artifactId>

<version>1.5.2.RELEASE</version>

<relativePath />

</parent>

<properties>

<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>

<java.version>1.8</java.version>

</properties>

<dependencyManagement>

<dependencies>

<dependency>

<groupId>org.springframework.cloud</groupId>

<artifactId>spring-cloud-config</artifactId>

<version>1.3.1.RELEASE</version>

<type>pom</type>

<scope>import</scope>

</dependency>

</dependencies>

</dependencyManagement>

<dependencies>

<dependency>

<groupId>org.springframework.cloud</groupId>

<artifactId>spring-cloud-starter-config</artifactId>

</dependency>

<dependency>

<groupId>org.springframework.boot</groupId>

<artifactId>spring-boot-starter-actuator</artifactId>

</dependency>

<dependency>

<groupId>org.springframework.boot</groupId>

<artifactId>spring-boot-starter-web</artifactId>

</dependency>

<dependency>

<groupId>org.springframework.boot</groupId>

<artifactId>spring-boot-starter-test</artifactId>

<scope>test</scope>

</dependency>

</dependencies>

<build>

<plugins>

<plugin>

<groupId>org.springframework.boot</groupId>

<artifactId>spring-boot-maven-plugin</artifactId>

</plugin>

</plugins>

</build>

</project>

Application

package cn.netkiller.cloud.client;

import org.springframework.beans.factory.annotation.Value;

import org.springframework.boot.SpringApplication;

import org.springframework.boot.autoconfigure.SpringBootApplication;

import org.springframework.cloud.context.config.annotation.RefreshScope;

import org.springframework.web.bind.annotation.RequestMapping;

import org.springframework.web.bind.annotation.RestController;

@SpringBootApplication

public class Application {

public static void main(String[] args) {

SpringApplication.run(Application.class, args);

}

}

@RefreshScope

@RestController

class MessageRestController {

@Value("${message:Hello default}")

private String message;

@RequestMapping("/message")

String getMessage() {

return this.message;

}

}

注意 @RefreshScope 注解

bootstrap.properties

spring.application.name=server-development

spring.cloud.config.uri=http://localhost:8888

management.security.enabled=false

测试 client

neo@netkiller $ curl http://localhost:8080/message.json

Hello world

10. feign.RetryableException: Read timed out executing

ribbon:

#请求连接的超时时长

ConnectTimeout: 60000

#请求处理的超时时长

ReadTimeout: 60000

第 67 章 Tomcat Spring 运行环境

1. Maven

<?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>

<groupId>com.example</groupId>

<artifactId>demo</artifactId>

<version>0.0.1-SNAPSHOT</version>

<packaging>jar</packaging>

<name>demo</name>

<description>Demo project for Spring Boot</description>

<parent>

<groupId>org.springframework.boot</groupId>

<artifactId>spring-boot-starter-parent</artifactId>

<version>1.3.0.RELEASE</version>

<relativePath/> <!-- lookup parent from repository -->

</parent>

<properties>

<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>

<java.version>1.8</java.version>

</properties>

<dependencies>

<dependency>

<groupId>org.springframework.boot</groupId>

<artifactId>spring-boot-starter-web</artifactId>

</dependency>

<dependency>

<groupId>org.springframework.boot</groupId>

<artifactId>spring-boot-starter-test</artifactId>

<scope>test</scope>

</dependency>

</dependencies>

<build>

<plugins>

<plugin>

<groupId>org.springframework.boot</groupId>

<artifactId>spring-boot-maven-plugin</artifactId>

</plugin>

</plugins>

</build>

</project>

2. Spring MVC configuration

<!-- **************************************************************** -->

<!--  RESOURCE FOLDERS CONFIGURATION                                  -->

<!--  Dispatcher configuration for serving static resources           -->

<!-- **************************************************************** -->

<mvc:resources location="/images/" mapping="/images/**" />

<mvc:resources location="/css/" mapping="/css/**" />

3. Tomcat

web.xml

<?xml version="1.0" encoding="UTF-8"?>

<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"

xmlns="http://xmlns.jcp.org/xml/ns/javaee"

xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd"

id="WebApp_ID" version="3.1">

<display-name>m.cf88.com</display-name>

<welcome-file-list>

<welcome-file>index.html</welcome-file>

<welcome-file>index.htm</welcome-file>

<welcome-file>index.jsp</welcome-file>

<welcome-file>default.html</welcome-file>

<welcome-file>default.htm</welcome-file>

<welcome-file>default.jsp</welcome-file>

</welcome-file-list>

<servlet>

<servlet-name>netkiller</servlet-name>

<servlet-class>

org.springframework.web.servlet.DispatcherServlet

</servlet-class>

<load-on-startup>1</load-on-startup>

</servlet>

<servlet-mapping>

<servlet-name>netkiller</servlet-name>

<url-pattern>/welcome.jsp</url-pattern>

<url-pattern>/welcome.html</url-pattern>

<url-pattern>*.html</url-pattern>

</servlet-mapping>

</web-app>

netkiller-servlet.xml

<beans xmlns="http://www.springframework.org/schema/beans"

xmlns:mvc="http://www.springframework.org/schema/mvc" xmlns:context="http://www.springframework.org/schema/context"

xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"

xsi:schemaLocation="

http://www.springframework.org/schema/beans

http://www.springframework.org/schema/beans/spring-beans.xsd

http://www.springframework.org/schema/mvc

http://www.springframework.org/schema/mvc/spring-mvc.xsd

http://www.springframework.org/schema/context

http://www.springframework.org/schema/context/spring-context.xsd">

<context:component-scan base-package="cn.netkiller.controller" />

<bean id="viewResolver"

class="org.springframework.web.servlet.view.UrlBasedViewResolver">

<property name="viewClass"

value="org.springframework.web.servlet.view.JstlView" />

<property name="prefix" value="/WEB-INF/jsp/" />

<property name="suffix" value=".jsp" />

</bean>

</beans>

4. 集成 Mybatis

4.1. pom.xml

<dependency>

<groupId>org.mybatis</groupId>

<artifactId>mybatis</artifactId>

<version>3.3.0</version>

</dependency>

<dependency>

<groupId>org.mybatis</groupId>

<artifactId>mybatis-spring</artifactId>

<version>1.2.3</version>

</dependency>

4.2. properties

<bean id="configuracion"

class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer">

<property name="location" value="classpath:resources/development.properties" />

</bean>

jdbc.driverClassName=oracle.jdbc.driver.OracleDriver

jdbc.url=jdbc:oracle:thin:@192.168.4.9:1521:orcl

#jdbc.url=jdbc:mysql://127.0.0.1:3306/mybatis

jdbc.username=test

jdbc.password=123456

<bean id="dataSource"

class="org.springframework.jdbc.datasource.DriverManagerDataSource">

<property name="driverClassName" value="${jdbc.driverClassName}" />

<property name="url" value="${jdbc.url}" />

<property name="username" value="${jdbc.username}" />

<property name="password" value="${jdbc.password}" />

</bean>

4.3. dataSource

<bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource">

<property name="driverClassName" value="${driver}" />

<property name="url" value="${url}" />

<property name="username" value="${username}" />

<property name="password" value="${password}" />

</bean>

4.4. SqlSessionFactory

创建SqlSessionFactory，需指定数据源，property名称必须为dataSource

<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">

<property name="dataSource" ref="dataSource" />

</bean>

4.5. Mapper 扫描

<bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">

<property name="basePackage" value="cn.netkiller.mappers" />

<property name="annotationClass" value="cn.netkiller.mappers.annotation.MybatisMapper"/>

<property name="sqlSessionFactoryBeanName" value="sqlSessionFactory"/>

</bean>

4.6. Mapper 单一class映射

创建数据映射器Mapper，属性mapperInterface的value必须为接口类

<bean id="userMapper" class="org.mybatis.spring.mapper.MapperFactoryBean">

<property name="mapperInterface" value="com.mybatis.demo.UserMapper" />

<property name="sqlSessionFactory" ref="sqlSessionFactory" />

</bean>

4.7. Service

<bean id="userService" class="cn.netkiller.service.UserService">

</bean>

4.8. 测试实例

例 67.1. MyBatis

建立映射

package cn.netkiller.mapper;

import org.apache.ibatis.annotations.Select;

import org.apache.ibatis.annotations.Param;

import cn.netkiller.model.User;

public interface UserMapper {

@Select("SELECT * FROM `user` WHERE id = #{id}")

public User findById(@Param("id") int id);

}

建立模型

package cn.netkiller.model;

public class User {

private String id;

private String name;

private int age;

public String getId() {

return id;

}

public void setId(String id) {

this.id = id;

}

public String getName() {

return name;

}

public void setName(String name) {

this.name = name;

}

public int getAge() {

return age;

}

public void setAge(int age) {

this.age = age;

}

@Override

public String toString() {

return "User [id=" + id + ", name=" + name + ", age=" + age + "]";

}

}

建立 service

package cn.netkiller.service;

import org.springframework.beans.factory.annotation.Autowired;

import org.springframework.stereotype.Service;

import cn.netkiller.mapper.UserMapper;

import cn.netkiller.model.User;

@Service

public class UserService {

@Autowired

private UserMapper userMapper;

public UserMapper getUserMapper() {

return userMapper;

}

public void setUserMapper(UserMapper userMapper) {

this.userMapper = userMapper;

}

public User findById(int id) {

return userMapper.findById(id);

}

}

建立控制器

package cn.netkiller.controller;

import org.springframework.beans.factory.annotation.Autowired;

import org.springframework.stereotype.Controller;

import org.springframework.ui.ModelMap;

import org.springframework.web.bind.annotation.RequestMapping;

import org.springframework.web.bind.annotation.ResponseBody;

import org.springframework.web.context.support.WebApplicationContextUtils;

import org.springframework.web.servlet.ModelAndView;

import org.springframework.web.servlet.view.RedirectView;

import cn.netkiller.mapper.UserMapper;

import cn.netkiller.model.User;

import cn.netkiller.service.UserService;

@Controller

public class Index {

@Autowired

private UserMapper userMapper;

@Autowired

private UserService userService;

@RequestMapping("/index")

// @ResponseBody

public ModelAndView index() {

String message = "Hello";

return new ModelAndView("index/index", "variable", message);

}

@RequestMapping("/user")

public ModelAndView user() {

User user = userService.findById(2);

String message = user.toString();

return new ModelAndView("index/index", "variable", message);

}

@RequestMapping("/member")

public ModelAndView member() {

User user = userMapper.findById(2);

String message = user.toString();

return new ModelAndView("index/index", "variable", message);

}

}

第 68 章 杂项 Miscellaneous

1. URL 拼装/解析

UriComponents https = UriComponentsBuilder.newInstance()

.scheme("https")

.host("www.netkiller.cn")

.port("8080")

.path("/article")

.queryParam("id", "9527")

.encode(StandardCharsets.UTF_8)

.build();

log.info(https.toUriString());

URL 解析

String httpUrl = "https://www.netkiller.cn:8080/article?id=9527";

UriComponents uriComponents = UriComponentsBuilder.fromHttpUrl(httpUrl).build();

提取协议头

String scheme = uriComponents.getScheme();

// scheme = https

System.out.println("scheme = " + scheme);

获取host操作。

String host = uriComponents.getHost();

// host = felord.cn

System.out.println("host = " + host);

提取 Port 端口。

int port = uriComponents.getPort();

// port = -1

System.out.println("port = " + port);

但是很奇怪的是上面的是 -1,很多人误以为会是80。其实 Http 协议确实是80，但是java.net.URL#getPort()规定，若 URL 的实例未申明（省略）端口号，则返回值为-1。所以当返回了-1就等同于80，但是 URL 中不直接体现它们。

提取 Path 路径

String path = uriComponents.getPath();

// path = /spring-security/{article}

System.out.println("path = " + path);

提取 Query 参数

String query = uriComponents.getQuery();

// query = version=1&timestamp=123123325

System.out.println("query = " + query);

更加合理的提取方式：

MultiValueMap<String, String> queryParams = uriComponents.getQueryParams();

// queryParams = {version=[1], timestamp=[123123325]}

System.out.println("queryParams = " + queryParams);

替换变量

UriComponents uriComponents = UriComponentsBuilder.newInstance()

.scheme("https")

.host("www.netkiller.cn")

.port("8080")

.path("/article/{category}")

.queryParam("id", "9527")

.encode(StandardCharsets.UTF_8)

.build();

UriComponents expand = uriComponents.expand("story");

log.info(expand.toUriString());

# https://www.netkiller.cn:8080/article/story?id=9527

UriComponents uriComponents = UriComponentsBuilder.newInstance()

.scheme("https")

.host("www.netkiller.cn")

.port("8080")

.path("/book/{chapter}/{section}")

.queryParam("id", "9527")

.encode(StandardCharsets.UTF_8)

.build();

UriComponents expand = uriComponents.expand(Map.of("chapter", "chapter1", "section", "section2"));

log.info(expand.toUriString());

# https://www.netkiller.cn:8080/book/chapter1/section2?id=9527

2. ServletUriComponentsBuilder

String locationUri = ServletUriComponentsBuilder

.fromCurrentRequest()

.path("/{id}")

.buildAndExpand(employeeId)

.toUriString();

3. URL 路径相关

过滤路径

PathPattern pattern = new PathPatternParser().parse("/test/**");

PathContainer pathContainer = exchange.getRequest().getPath().pathWithinApplication();

if (pattern.matches(pathContainer)) {

log.info("custom webFilter");

return chain.filter(exchange);

}

PathPatternParser pathPatternParser = new PathPatternParser();

List<String> paths = List.of("/token", "/verifier", "/mock/*");

List<PathPattern> parsedPatterns = new ArrayList<>();

for (String path : paths) {

PathPattern pathPattern = pathPatternParser.parse(path);

parsedPatterns.add(pathPattern);

}

PathContainer pathContainer = exchange.getRequest().getPath().pathWithinApplication();

for (PathPattern pattern : parsedPatterns) {

if (pattern.matches(pathContainer)) {

System.out.println("Path " + pathContainer + " matches pattern " + pattern.getPatternString());

}

}

return chain.filter(exchange);

第 69 章 Tomcat Spring 运行环境

1. Maven

<?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>

<groupId>com.example</groupId>

<artifactId>demo</artifactId>

<version>0.0.1-SNAPSHOT</version>

<packaging>jar</packaging>

<name>demo</name>

<description>Demo project for Spring Boot</description>

<parent>

<groupId>org.springframework.boot</groupId>

<artifactId>spring-boot-starter-parent</artifactId>

<version>1.3.0.RELEASE</version>

<relativePath/> <!-- lookup parent from repository -->

</parent>

<properties>

<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>

<java.version>1.8</java.version>

</properties>

<dependencies>

<dependency>

<groupId>org.springframework.boot</groupId>

<artifactId>spring-boot-starter-web</artifactId>

</dependency>

<dependency>

<groupId>org.springframework.boot</groupId>

<artifactId>spring-boot-starter-test</artifactId>

<scope>test</scope>

</dependency>

</dependencies>

<build>

<plugins>

<plugin>

<groupId>org.springframework.boot</groupId>

<artifactId>spring-boot-maven-plugin</artifactId>

</plugin>

</plugins>

</build>

</project>

2. Spring MVC configuration

<!-- **************************************************************** -->

<!--  RESOURCE FOLDERS CONFIGURATION                                  -->

<!--  Dispatcher configuration for serving static resources           -->

<!-- **************************************************************** -->

<mvc:resources location="/images/" mapping="/images/**" />

<mvc:resources location="/css/" mapping="/css/**" />

3. Tomcat

web.xml

<?xml version="1.0" encoding="UTF-8"?>

<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"

xmlns="http://xmlns.jcp.org/xml/ns/javaee"

xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd"

id="WebApp_ID" version="3.1">

<display-name>m.cf88.com</display-name>

<welcome-file-list>

<welcome-file>index.html</welcome-file>

<welcome-file>index.htm</welcome-file>

<welcome-file>index.jsp</welcome-file>

<welcome-file>default.html</welcome-file>

<welcome-file>default.htm</welcome-file>

<welcome-file>default.jsp</welcome-file>

</welcome-file-list>

<servlet>

<servlet-name>netkiller</servlet-name>

<servlet-class>

org.springframework.web.servlet.DispatcherServlet

</servlet-class>

<load-on-startup>1</load-on-startup>

</servlet>

<servlet-mapping>

<servlet-name>netkiller</servlet-name>

<url-pattern>/welcome.jsp</url-pattern>

<url-pattern>/welcome.html</url-pattern>

<url-pattern>*.html</url-pattern>

</servlet-mapping>

</web-app>

netkiller-servlet.xml

<beans xmlns="http://www.springframework.org/schema/beans"

xmlns:mvc="http://www.springframework.org/schema/mvc" xmlns:context="http://www.springframework.org/schema/context"

xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"

xsi:schemaLocation="

http://www.springframework.org/schema/beans

http://www.springframework.org/schema/beans/spring-beans.xsd

http://www.springframework.org/schema/mvc

http://www.springframework.org/schema/mvc/spring-mvc.xsd

http://www.springframework.org/schema/context

http://www.springframework.org/schema/context/spring-context.xsd">

<context:component-scan base-package="cn.netkiller.controller" />

<bean id="viewResolver"

class="org.springframework.web.servlet.view.UrlBasedViewResolver">

<property name="viewClass"

value="org.springframework.web.servlet.view.JstlView" />

<property name="prefix" value="/WEB-INF/jsp/" />

<property name="suffix" value=".jsp" />

</bean>

</beans>

4. 集成 Mybatis

pom.xml

<dependency>

<groupId>org.mybatis</groupId>

<artifactId>mybatis</artifactId>

<version>3.3.0</version>

</dependency>

<dependency>

<groupId>org.mybatis</groupId>

<artifactId>mybatis-spring</artifactId>

<version>1.2.3</version>

</dependency>

properties

<bean id="configuracion"

class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer">

<property name="location" value="classpath:resources/development.properties" />

</bean>

jdbc.driverClassName=oracle.jdbc.driver.OracleDriver

jdbc.url=jdbc:oracle:thin:@192.168.4.9:1521:orcl

#jdbc.url=jdbc:mysql://127.0.0.1:3306/mybatis

jdbc.username=test

jdbc.password=123456

<bean id="dataSource"

class="org.springframework.jdbc.datasource.DriverManagerDataSource">

<property name="driverClassName" value="${jdbc.driverClassName}" />

<property name="url" value="${jdbc.url}" />

<property name="username" value="${jdbc.username}" />

<property name="password" value="${jdbc.password}" />

</bean>

dataSource

<bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource">

<property name="driverClassName" value="${driver}" />

<property name="url" value="${url}" />

<property name="username" value="${username}" />

<property name="password" value="${password}" />

</bean>

SqlSessionFactory

创建SqlSessionFactory，需指定数据源，property名称必须为dataSource

<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">

<property name="dataSource" ref="dataSource" />

</bean>

Mapper 扫描

<bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">

<property name="basePackage" value="cn.netkiller.mappers" />

<property name="annotationClass" value="cn.netkiller.mappers.annotation.MybatisMapper"/>

<property name="sqlSessionFactoryBeanName" value="sqlSessionFactory"/>

</bean>

Mapper 单一class映射

创建数据映射器Mapper，属性mapperInterface的value必须为接口类

<bean id="userMapper" class="org.mybatis.spring.mapper.MapperFactoryBean">

<property name="mapperInterface" value="com.mybatis.demo.UserMapper" />

<property name="sqlSessionFactory" ref="sqlSessionFactory" />

</bean>

Service

<bean id="userService" class="cn.netkiller.service.UserService">

</bean>

测试实例

例 69.1. MyBatis

建立映射

package cn.netkiller.mapper;

import org.apache.ibatis.annotations.Select;

import org.apache.ibatis.annotations.Param;

import cn.netkiller.model.User;

public interface UserMapper {

@Select("SELECT * FROM `user` WHERE id = #{id}")

public User findById(@Param("id") int id);

}

建立模型

package cn.netkiller.model;

public class User {

private String id;

private String name;

private int age;

public String getId() {

return id;

}

public void setId(String id) {

this.id = id;

}

public String getName() {

return name;

}

public void setName(String name) {

this.name = name;

}

public int getAge() {

return age;

}

public void setAge(int age) {

this.age = age;

}

@Override

public String toString() {

return "User [id=" + id + ", name=" + name + ", age=" + age + "]";

}

}

建立 service

package cn.netkiller.service;

import org.springframework.beans.factory.annotation.Autowired;

import org.springframework.stereotype.Service;

import cn.netkiller.mapper.UserMapper;

import cn.netkiller.model.User;

@Service

public class UserService {

@Autowired

private UserMapper userMapper;

public UserMapper getUserMapper() {

return userMapper;

}

public void setUserMapper(UserMapper userMapper) {

this.userMapper = userMapper;

}

public User findById(int id) {

return userMapper.findById(id);

}

}

建立控制器

package cn.netkiller.controller;

import org.springframework.beans.factory.annotation.Autowired;

import org.springframework.stereotype.Controller;

import org.springframework.ui.ModelMap;

import org.springframework.web.bind.annotation.RequestMapping;

import org.springframework.web.bind.annotation.ResponseBody;

import org.springframework.web.context.support.WebApplicationContextUtils;

import org.springframework.web.servlet.ModelAndView;

import org.springframework.web.servlet.view.RedirectView;

import cn.netkiller.mapper.UserMapper;

import cn.netkiller.model.User;

import cn.netkiller.service.UserService;

@Controller

public class Index {

@Autowired

private UserMapper userMapper;

@Autowired

private UserService userService;

@RequestMapping("/index")

// @ResponseBody

public ModelAndView index() {

String message = "Hello";

return new ModelAndView("index/index", "variable", message);

}

@RequestMapping("/user")

public ModelAndView user() {

User user = userService.findById(2);

String message = user.toString();

return new ModelAndView("index/index", "variable", message);

}

@RequestMapping("/member")

public ModelAndView member() {

User user = userMapper.findById(2);

String message = user.toString();

return new ModelAndView("index/index", "variable", message);

}

}

第 70 章 FAQ

1. org.hibernate.dialect.Oracle10gDialect does not support identity key generation

@GeneratedValue(strategy=GenerationType.IDENTITY)

换成

@GeneratedValue(strategy=GenerationType.AUTO)

or

@Id

@Column(name = "ID")

@GeneratedValue(strategy=GenerationType.SEQUENCE, generator = "id_Sequence")

@SequenceGenerator(name = "id_Sequence", sequenceName = "ID_SEQ")

private int id;

2. No identifier specified for entity

在实体中使用

import javax.persistence.Id;

替换

import org.springframework.data.annotation.Id;

3. Could not read document: Invalid UTF-8 middle byte 0xd0

Spring 默认不支持 UTF-8

2016-08-17 16:04:53.148 WARN 7700 --- [nio-8080-exec-1] .w.s.m.s.DefaultHandlerExceptionResolver : Failed to read HTTP message: org.springframework.http.converter.HttpMessageNotReadableException:Could not read document: Invalid UTF-8 middle byte 0xd0 at [Source: java.io.PushbackInputStream@33aa54cc; line: 1, column: 38](through reference chain: api.domain.oracle.Withdraw["bankname"]); nested exception is com.fasterxml.jackson.databind.JsonMappingException: Invalid UTF-8 middle byte 0xd0 at [Source: java.io.PushbackInputStream@33aa54cc; line: 1, column: 38]

(through reference chain: api.domain.oracle.Withdraw["bankname"])

解决方案 application.properties 配置文件中加入如下配置：

spring.messages.encoding=UTF-8

server.tomcat.uri-encoding=UTF-8

spring.http.encoding.charset=UTF-8

spring.http.encoding.enabled=true

spring.http.encoding.force=true

4. java.sql.SQLRecoverableException: IO Error: The Network Adapter could not establish the connection

分析，Oracle 数据库无法连接，确认用户密码正确，日志提示 The Network Adapter could not establish the connection 看上去更像网络故障，同事还有下面两条日志。

Caused by: oracle.net.ns.NetException: The Network Adapter could not

establish the connection

Caused by: java.net.SocketTimeoutException: connect timed out

通过 ss 命令可以看到有tcp操作，可以排除不是网络故障。

[root@iZ62m7362hwZ ~]# ss -ant | grep 1521

TIME-WAIT 0 0 47.90.18.24:45780 15.84.21.59:1521

检查你的用户名与密码是否含有特殊字符，特殊字符需要使用转义字符"\".

spring.datasource.url=jdbc:oracle:thin:neo/\[y7\$ghM\~3b@(DESCRIPTION=(ADDRESS=(PROTOCOL=TCP)(HOST=215.184.211.50)(PORT=1521))(LOAD_BALANCE=YES)(FAILOVER=ON)(CONNECT_DATA=(SERVER=DEDICATED)(SERVICE_NAME=orcl)(FAILOVER_MODE=(TYPE=SESSION)(METHOD=BASIC))))

#spring.datasource.username=neo

#spring.datasource.password=[y7$ghM~3b

将用户名写入spring.datasource.url中，格式jdbc:oracle:thin:用户名/密码@(......)，禁用spring.datasource.username和spring.datasource.password两个配置项。

5. Field javaMailSender in cn.netkiller.rest.EmailRestController required a bean of type 'org.springframework.mail.javamail.JavaMailSender' that could not be found.

启动提示 'org.springframework.mail.javamail.JavaMailSender' that could not be found 这句话很误导人。实际上是 (spring.mail.host) did not find property 'host'

***************************

APPLICATION FAILED TO START

***************************

Description:

Field javaMailSender in cn.netkiller.rest.EmailRestController required a

bean of type 'org.springframework.mail.javamail.JavaMailSender' that

could not be found.

- Bean method 'mailSender' not loaded because AnyNestedCondition 0

matched 2 did not; NestedCondition on

MailSenderAutoConfiguration.MailSenderCondition.JndiNameProperty

@ConditionalOnProperty (spring.mail.jndi-name) did not find property

'jndi-name';

NestedCondition on

MailSenderAutoConfiguration.MailSenderCondition.HostProperty

@ConditionalOnProperty (spring.mail.host) did not find property

'host'

Action:

Consider revisiting the conditions above or defining a bean of type

'org.springframework.mail.javamail.JavaMailSender' in your

configuration.

解决方案，application.properties 增加 spring.mail.host=localhost

6. org.postgresql.util.PSQLException: FATAL: no pg_hba.conf entry for host "172.16.0.3", user "test", database "test ", SSL off

确认 pg_hba.conf 配置正确，并且 psql 可以正常链接，spring仍然报错

spring.datasource.url=jdbc:postgresql://47.90.18.244:5432/test

spring.datasource.username=test

spring.datasource.password=test

spring.jpa.show-sql=true

spring.jpa.hibernate.ddl-auto=create-drop

spring.jpa.generate-ddl=true

请检查 jdbc:postgresql://47.90.18.244:5432/test 后面test是否多了一个空格或者有特殊字符。删除test后面的空格可以解决

7. Spring boot 怎样显示执行的SQL语句

spring.jpa.show-sql=true

8. Cannot determine embedded database driver class for database type NONE

错误如下

***************************

APPLICATION FAILED TO START

***************************

Description:

Cannot determine embedded database driver class for database type NONE

Action:

If you want an embedded database please put a supported one on the classpath. If you have database settings to be loaded from a particular profile you may need to active it (no profiles are currently active).

背景：Maven 项目中并不包含任何与数据库有关的依赖。问题出在另一个公共包中如： common-version.jar

解决方案：排除不需要的包

<dependency>

<groupId>cn.netkiller</groupId>

<artifactId>common</artifactId>

<version>0.0.1-SNAPSHOT</version>

<exclusions>

<exclusion>

<groupId>mysql</groupId>

<artifactId>mysql-connector-java</artifactId>

</exclusion>

<exclusion>

<groupId>org.springframework.boot</groupId>

<artifactId>spring-boot-starter-data-jpa</artifactId>

</exclusion>

<exclusion>

<groupId>org.springframework.boot</groupId>

<artifactId>spring-boot-starter-jdbc</artifactId>

</exclusion>

</exclusions>

</dependency>

9. Spring boot / Spring cloud 时区差8个小时

经过检查：操作系统时区 CST，数据库是 SYSTEM，Spring boot 获取时间相差8个小时。

分析：认为是 @JsonFormat 格式化造成的。

解决方案：在 @JsonFormat 中增加时区设置。

@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")

@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "Asia/Shanghai")

public Date ctime;

期间尝试多种方式无效:

# 下面例子无效

spring.jackson.date-format=yyyy-MM-dd HH:mm:ss

spring.mvc.date-format=yyyy-MM-dd HH:mm:ss

spring.jackson.time-zone=GMT+8

# 下面方法无效

spring.datasource.url=jdbc:mysql://119.29.241.95:3306/5kwords?useSSL=false&serverTimezone=UTC

# 下面配置仍然无效

spring.jpa.properties.jadira.usertype.autoRegisterUserTypes = true

spring.jpa.properties.jadira.usertype.javaZone=Asia/Shanghai

spring.jpa.properties.jadira.usertype.databaseZone=Asia/Shanghai

根源在 Json 转化。

完成的例子

package common.domain;

import java.io.Serializable;

import java.util.Date;

import javax.persistence.Column;

import javax.persistence.Entity;

import javax.persistence.Id;

import javax.persistence.Table;

import org.springframework.format.annotation.DateTimeFormat;

import com.fasterxml.jackson.annotation.JsonFormat;

@Entity

@Table(name = "article", catalog = "cms")

public class Article implements Serializable {

private static final long serialVersionUID = 7603772682950271321L;

@Id

public int id;

public String title;

@Column(name = "short")

public String shortTitle;

public String description;

public String author;

public String star;

public String tags;

public boolean status;

public String content;

public int typeId;

public int siteId;

@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")

@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "Asia/Shanghai")

public Date ctime;

@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")

@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "America/Phoenix")

public Date mtime;

public int getId() {

return id;

}

public void setId(int id) {

this.id = id;

}

public String getTitle() {

return title;

}

public void setTitle(String title) {

this.title = title;

}

public String getDescription() {

return description;

}

public void setDescription(String description) {

this.description = description;

}

public Date getCtime() {

return ctime;

}

public void setCtime(Date ctime) {

this.ctime = ctime;

}

public String getShortTitle() {

return shortTitle;

}

public void setShortTitle(String shortTitle) {

this.shortTitle = shortTitle;

}

public String getAuthor() {

return author;

}

public void setAuthor(String author) {

this.author = author;

}

public String getStar() {

return star;

}

public void setStar(String star) {

this.star = star;

}

public String getTags() {

return tags;

}

public void setTags(String tags) {

this.tags = tags;

}

public boolean isStatus() {

return status;

}

public void setStatus(boolean status) {

this.status = status;

}

public String getContent() {

return content;

}

public void setContent(String content) {

this.content = content;

}

public int getTypeId() {

return typeId;

}

public void setTypeId(int typeId) {

this.typeId = typeId;

}

public int getSiteId() {

return siteId;

}

public void setSiteId(int siteId) {

this.siteId = siteId;

}

public Date getMtime() {

return mtime;

}

public void setMtime(Date mtime) {

this.mtime = mtime;

}

@Override

public String toString() {

return "Article [id=" + id + ", title=" + title + ", shortTitle=" + shortTitle + ", description=" + description + ", author=" + author + ", star=" + star + ", tags=" + tags + ", status=" + status + ", content=" + content + ", typeId=" + typeId + ", siteId=" + siteId + ", ctime=" + ctime + ", mtime=" + mtime + "]";

}

}

10. @Value 取不到值

在构造方法中引用@value为null，由于spring实例化顺序为先执行构造方法，再注入成员变量，所以取值为null。

调用spring组件时使用new对象，而不是@Autowired。

11. Spring boot 2.1.0

application.properties 需要加入下面参数，否则 @Bean 不允许。

spring.main.allow-bean-definition-overriding=true

另外 MySQL驱动必须使用最新的 com.mysql.cj.jdbc.Driver

12. Field authenticationManager in cn.netkiller.oauth2.config.AuthorizationServerConfigurer required a bean of type 'org.springframework.security.authentication.AuthenticationManager' that could not be found.

***************************

APPLICATION FAILED TO START

***************************

Description:

Field authenticationManager in cn.netkiller.oauth2.config.AuthorizationServerConfigurer required a bean of type 'org.springframework.security.authentication.AuthenticationManager' that could not be found.

The injection point has the following annotations:

- @org.springframework.beans.factory.annotation.Autowired(required=true)

Action:

Consider defining a bean of type 'org.springframework.security.authentication.AuthenticationManager' in your configuration.

注入 @Bean @Override public AuthenticationManager authenticationManagerBean() 即可解决

package cn.netkiller.oauth2.config;

import org.springframework.context.annotation.Bean;

import org.springframework.context.annotation.Configuration;

import org.springframework.security.authentication.AuthenticationManager;

import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;

import org.springframework.security.config.annotation.web.builders.HttpSecurity;

import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;

import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;

@EnableGlobalMethodSecurity(prePostEnabled = true)

@Configuration

@EnableWebSecurity

public class WebSecurityConfigurer extends WebSecurityConfigurerAdapter {

public WebSecurityConfigurer() {

// TODO Auto-generated constructor stub

}

// 此处必须声明这个bean类，否则无法注入AuthenticationManager

@Bean

@Override

public AuthenticationManager authenticationManagerBean() throws Exception {

return super.authenticationManagerBean();

}

protected void configure(HttpSecurity http) throws Exception {

http.formLogin().and().authorizeRequests().anyRequest().authenticated().and().logout().permitAll().and().csrf().disable();

}

}

13. 打印 Bean 信息

import java.util.Arrays;

import org.springframework.boot.CommandLineRunner;

import org.springframework.boot.SpringApplication;

import org.springframework.boot.autoconfigure.SpringBootApplication;

import org.springframework.context.ApplicationContext;

import org.springframework.context.annotation.Bean;

@SpringBootApplication

public class Application {

public static void main(String[] args) {

SpringApplication.run(Application.class, args);

}

@Bean

public CommandLineRunner commandLineRunner(ApplicationContext ctx) {

return args -> {

System.out.println("Let's inspect the beans provided by Spring Boot:");

String[] beanNames = ctx.getBeanDefinitionNames();

Arrays.sort(beanNames);

for (String beanName : beanNames) {

System.out.println(beanName);

}

};

}

}

14. The dependencies of some of the beans in the application context form a cycle

@Service 中的 @Autowired 出现了相互注入，引起循环。

api    | ***************************

api    | APPLICATION FAILED TO START

api    | ***************************

api    |

api    | Description:

api    |

api    | The dependencies of some of the beans in the application context form a cycle:

api    |

api    | ┌─────┐

api    | |  incarOrderFlowServiceImpl defined in URL [jar:file:/app/api.netkiller.cn.jar!/BOOT-INF/lib/api.netkiller.cn-1.0.0.jar!/com/zito/incar/service/impl/IncarOrderFlowServiceImpl.class]

api    | ↑     ↓

api    | |  incarOrderServiceImpl (field private cn.netkiller.service.IIncarAttachService cn.netkiller.service.impl.IncarOrderServiceImpl.iIncarAttachService)

api    | ↑     ↓

api    | |  incarAttachServiceImpl (field private cn.netkiller.service.IIncarOrderFlowService cn.netkiller.service.impl.IncarAttachServiceImpl.iIncarOrderFlowService)

api    | └─────┘

解决方案

增加 @Lazy 注解

@Slf4j

@Service

@Lazy

public class IncarOrderFlowServiceImpl implements IIncarOrderFlowService {

@Autowired

private ISysRoleService iSysRoleService;

@Autowired

private  IIncarOrderService iIncarOrderService;

...

...

...

}

15. no main manifest attribute, in /srv/job-admin.jar

[root@localhost cloud.netkiller.cn]# java -jar job-admin.jar

no main manifest attribute, in job-admin.jar

解压 jar 包，查看 META-INF/MANIFEST.MF 文件

[root@localhost ~]# unzip job-admin.jar

[root@localhost ~]# cat META-INF/MANIFEST.MF

Manifest-Version: 1.0

Archiver-Version: Plexus Archiver

Built-By: gitlab-runner

Created-By: Apache Maven 3.8.4

Build-Jdk: 1.8.0_312

解决方法，pom.xml 中增加 repackage 配置项

<plugins>

<plugin>

<groupId>org.springframework.boot</groupId>

<artifactId>spring-boot-maven-plugin</artifactId>

<version>${spring-boot.version}</version>

<executions>

<execution>

<goals>

<goal>repackage</goal>

</goals>

</execution>

</executions>

</plugin>

</plugins>

重新打包，再解压开查看 MANIFEST.MF 文件

[root@localhost ~]# cat META-INF/MANIFEST.MF

Manifest-Version: 1.0

Spring-Boot-Classpath-Index: BOOT-INF/classpath.idx

Archiver-Version: Plexus Archiver

Built-By: gitlab-runner

Spring-Boot-Layers-Index: BOOT-INF/layers.idx

Start-Class: cn.netkiller.admin.Application

Spring-Boot-Classes: BOOT-INF/classes/

Spring-Boot-Lib: BOOT-INF/lib/

Spring-Boot-Version: 2.6.2

Created-By: Apache Maven 3.8.4

Build-Jdk: 1.8.0_312

Main-Class: org.springframework.boot.loader.JarLauncher

附录 A. 附录

1. Springboot example

https://github.com/spring-projects/spring-boot/tree/master/spring-boot-samples