提交 10f4d34f 编写于 作者: 沉默王二's avatar 沉默王二 💬

2022年05月21日 Spring Boot 系列专栏更新

上级 70a878d5
......@@ -277,12 +277,16 @@
- [一分钟快速搭建Spring Boot项目](docs/springboot/initializr.md)
- [Spring Boot 整合 MySQL 和 Druid](docs/springboot/mysql-druid.md)
- [Spring Boot 整合 JPA](docs/springboot/jpa.md)
- [Spring Boot 整合 Thymeleaf 模板引擎](docs/springboot/thymeleaf.md)
- [Spring Boot 如何开启事务支持?](docs/springboot/transaction.md)
- [Spring Boot 中使用过滤器、拦截器、监听器](docs/springboot/Filter-Interceptor-Listener.md)
- [Spring Boot 整合 Redis 实现缓存](docs/redis/redis-springboot.md)
- [Spring Boot 整合 MyBatis-Plus AutoGenerator 生成编程喵项目骨架代码](docs/kaiyuan/auto-generator.md)
- [Spring Boot 整合 Logback 定制日志框架](docs/springboot/logback.md)
- [Spring Boot 整合 Swagger-UI 实现在线API文档](docs/springboot/swagger.md)
- [Spring Boot 整合 Knife4j,美化强化丑陋的Swagger](docs/gongju/knife4j.md)
- [Spring Boot 整合 Spring Task 实现定时任务](docs/springboot/springtask.md)
- [Spring Boot 为什么不需要额外安装Tomcat?](docs/springboot/tomcat.md)
- [Spring Boot 整合 MyBatis-Plus AutoGenerator 生成编程喵项目骨架代码](docs/kaiyuan/auto-generator.md)
## 辅助工具/轮子
......@@ -309,13 +313,14 @@
- [全文搜索引擎Elasticsearch入门教程](docs/elasticsearch/rumen.md)
- [可能是把ZooKeeper概念讲的最清楚的一篇文章](docs/zookeeper/jibenjieshao.md)
- [微服务网关:从对比到选型,由理论到实践](docs/microservice/api-wangguan.md)
## 高性能
### 消息队列
- [RabbitMQ入门教程(概念、应用场景、安装、使用)](docs/mq/rabbitmq-rumen.md)
- [MQ:怎么确保消息100%不丢失?](docs/mq/100-budiushi.md)
- [怎么确保消息100%不丢失?](docs/mq/100-budiushi.md)
## 高可用
......@@ -326,10 +331,12 @@
## MySQL
- [如何保障MySQL和Redis的数据一致性?](docs/mysql/redis-shuju-yizhixing.md)
## Redis
- [Redis入门(适合新手)](docs/redis/rumen.md)
- [简单聊聊缓存雪崩、穿透、击穿](docs/redis/xuebeng-chuantou-jichuan.md)
- [聊聊缓存雪崩、穿透、击穿](docs/redis/xuebeng-chuantou-jichuan.md)
......
......@@ -688,13 +688,25 @@ export const sidebarConfig = defineSidebarConfig({
text: "整合JPA",
link: "springboot/jpa",
},
{
text: "整合Thymeleaf",
link: "springboot/thymeleaf",
},
{
text: "开启事务支持",
link: "springboot/transaction",
},
{
text: "过滤器、拦截器、监听器",
link: "Filter-Interceptor-Listener",
},
{
text: "整合Redis实现缓存",
link: "redis/redis-springboot",
},
{
text: "整合MyBatis-Plus AutoGenerator",
link: "kaiyuan/auto-generator",
text: "整合Logback",
link: "springboot/logback"
},
{
text: "整合Swagger-UI",
......@@ -708,7 +720,11 @@ export const sidebarConfig = defineSidebarConfig({
text: "整合SpringTask",
link: "springboot/springtask"
},
"springboot/tomcat",
{
text: "整合MyBatis-Plus AutoGenerator",
link: "kaiyuan/auto-generator",
},
],
},
{
......@@ -736,21 +752,31 @@ export const sidebarConfig = defineSidebarConfig({
text: "3.6 分布式",
collapsable: true,
children: [
"elasticsearch/rumen",
"zookeeper/jibenjieshao",
{
text: "Elasticsearch入门",
link: "elasticsearch/rumen"
},
{
text: "聊聊ZooKeeper",
link: "zookeeper/jibenjieshao"
},
{
text: "聊聊微服务网关",
link: "microservice/api-wangguan"
},
],
},
{
text: "3.7 高性能",
text: "3.7 消息队列",
collapsable: true,
children: [
{
text: "消息队列",
collapsable: true,
children: [
"mq/rabbitmq-rumen",
"mq/100-budiushi",
],
text: "RabbitMQ入门",
link: "mq/rabbitmq-rumen"
},
{
text: "如何保障消息不丢失",
link: "mq/100-budiushi"
},
],
},
......@@ -760,12 +786,28 @@ export const sidebarConfig = defineSidebarConfig({
text: "四、数据库",
collapsable: true,
children: [
{
text: "MySQL",
collapsable: true,
children: [
{
text: "MySQL和Redis数据一致性",
link: "mysql/redis-shuju-yizhixing"
},
],
},
{
text: "Redis",
collapsable: true,
children: [
"redis/rumen",
"redis/xuebeng-chuantou-jichuan",
{
text: "Redis入门",
link: "redis/rumen"
},
{
text: "缓存雪崩、穿透、击穿",
link: "redis/xuebeng-chuantou-jichuan"
},
],
},
{
......
......@@ -275,12 +275,15 @@ headerDepth: 1
- [一分钟快速搭建Spring Boot项目](springboot/initializr.md)
- [Spring Boot 整合 MySQL 和 Druid](springboot/mysql-druid.md)
- [Spring Boot 整合 JPA](springboot/jpa.md)
- [Spring Boot 整合 Thymeleaf 模板引擎](springboot/thymeleaf.md)
- [Spring Boot 如何开启事务支持?](springboot/transaction.md)
- [Spring Boot 中使用过滤器、拦截器、监听器](springboot/Filter-Interceptor-Listener.md)
- [Spring Boot 整合 Redis 实现缓存](redis/redis-springboot.md)
- [Spring Boot 整合 MyBatis-Plus AutoGenerator生成项目骨架代码](kaiyuan/auto-generator.md)
- [Spring Boot 整合 Logback 定制日志框架](springboot/logback.md)
- [Spring Boot 整合 Swagger-UI 实现在线API文档](springboot/swagger.md)
- [Spring Boot 整合 Knife4j,美化强化丑陋的Swagger](gongju/knife4j.md)
- [Spring Boot 整合 Spring Task 实现定时任务](springboot/springtask.md)
- [Spring Boot 为什么不需要额外安装Tomcat?](springboot/tomcat.md)
- [Spring Boot 整合 MyBatis-Plus AutoGenerator 生成编程喵项目骨架代码](kaiyuan/auto-generator.md)
## 辅助工具/轮子
......@@ -307,6 +310,7 @@ headerDepth: 1
- [全文搜索引擎Elasticsearch入门教程](elasticsearch/rumen.md)
- [可能是把ZooKeeper概念讲的最清楚的一篇文章](zookeeper/jibenjieshao.md)
- [微服务网关:从对比到选型,由理论到实践](microservice/api-wangguan.md)
### 高性能
......@@ -325,10 +329,12 @@ headerDepth: 1
### MySQL
- [如何保障MySQL和Redis的数据一致性?](mysql/redis-shuju-yizhixing.md)
### Redis
- [Redis入门(适合新手)](redis/rumen.md)
- [简单聊聊缓存雪崩、穿透、击穿](redis/xuebeng-chuantou-jichuan.md)
- [聊聊缓存雪崩、穿透、击穿](redis/xuebeng-chuantou-jichuan.md)
......
---
category:
- Java企业级开发
tag:
- 微服务网关
title: 微服务网关:从对比到选型,由理论到实践
---
> 整理:沉默王二,戳[转载链接](https://mp.weixin.qq.com/s/b2xf4GiJksBQqjHVpsh49Q),作者:楼仔,戳[原文链接](https://mp.weixin.qq.com/s/0Qupyl5eCyQGFEAIY6GxoQ)。
微服务近几年非常火,围绕微服务的技术生态也比较多,比如微服务网关、Docker、Kubernetes等。
我是于2019年开始接触微服务网关,当时和公司的一位同事一起开发,由于技术能力有限,我只负责网关后台,后续微服务网关的迭代,我其实没有参与,不过后来抽空看了微服务网关前台的代码,所以对这套微服务网关的实现原理算是基本掌握。
最近在写技术栈相关的文章,刚好写到微服务网关,就把之前学习的知识进行简单总结,同时也把市面上常用的微服务网关进行梳理,一方面便于后续技术选型,另一方面也算是给自己一个交代。下面是文章目录:
![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/microservice/api-wangguan-f910e9f2-0374-4afb-aef0-3ea47c9b7f67.png)
## API网关基础
### 什么是API网关
**API网关是一个服务器,是系统的唯一入口。** 从面向对象设计的角度看,它与外观模式类似。
API网关封装了系统内部架构,为每个客户端提供一个定制的API。它可能还具有其它职责,如身份验证、监控、负载均衡、缓存、协议转换、限流熔断、静态响应处理。
**API网关方式的核心要点是,所有的客户端和消费端都通过统一的网关接入微服务**,在网关层处理所有的非业务功能。通常,网关也是提供REST/HTTP的访问API。
### 网关的主要功能
微服务网关作为微服务后端服务的统一入口,它可以统筹管理后端服务,主要分为数据平面和控制平面:
- 数据平面主要功能是接入用户的HTTP请求和微服务被拆分后的聚合。使用微服务网关统一对外暴露后端服务的API和契约,路由和过滤功能正是网关的核心能力模块。另外,微服务网关可以实现拦截机制和专注跨横切面的功能,包括协议转换、安全认证、熔断限流、灰度发布、日志管理、流量监控等。
- 控制平面主要功能是对后端服务做统一的管控和配置管理。例如,可以控制网关的弹性伸缩;可以统一下发配置;可以对网关服务添加标签;可以在微服务网关上通过配置Swagger功能统一将后端服务的API契约暴露给使用方,完成文档服务,提高工作效率和降低沟通成本。
![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/microservice/api-wangguan-f6ea867d-ffdf-4da2-a89d-fccbe6326110.png)
- **路由功能**:路由是微服务网关的核心能力。通过路由功能微服务网关可以将请求转发到目标微服务。在微服务架构中,网关可以结合注册中心的动态服务发现,实现对后端服务的发现,调用方只需要知道网关对外暴露的服务API就可以透明地访问后端微服务。
- **负载均衡**:API网关结合负载均衡技术,利用Eureka或者Consul等服务发现工具,通过轮询、指定权重、IP地址哈希等机制实现下游服务的负载均衡。
- **统一鉴权**:一般而言,无论对内网还是外网的接口都需要做用户身份认证,而用户认证在一些规模较大的系统中都会采用统一的单点登录(Single Sign On)系统,如果每个微服务都要对接单点登录系统,那么显然比较浪费资源且开发效率低。API网关是统一管理安全性的绝佳场所,可以将认证的部分抽取到网关层,微服务系统无须关注认证的逻辑,只关注自身业务即可。
- **协议转换**:API网关的一大作用在于构建异构系统,API网关作为单一入口,通过协议转换整合后台基于REST、AMQP、Dubbo等不同风格和实现技术的微服务,面向Web Mobile、开放平台等特定客户端提供统一服务。
- **指标监控**:网关可以统计后端服务的请求次数,并且可以实时地更新当前的流量健康状态,可以对URL粒度的服务进行延迟统计,也可以使用Hystrix Dashboard查看后端服务的流量状态及是否有熔断发生。
- **限流熔断**:在某些场景下需要控制客户端的访问次数和访问频率,一些高并发系统有时还会有限流的需求。在网关上可以配置一个阈值,当请求数超过阈值时就直接返回错误而不继续访问后台服务。当出现流量洪峰或者后端服务出现延迟或故障时,网关能够主动进行熔断,保护后端服务,并保持前端用户体验良好。
- **黑白名单**:微服务网关可以使用系统黑名单,过滤HTTP请求特征,拦截异常客户端的请求,例如DDoS攻击等侵蚀带宽或资源迫使服务中断等行为,可以在网关层面进行拦截过滤。比较常见的拦截策略是根据IP地址增加黑名单。在存在鉴权管理的路由服务中可以通过设置白名单跳过鉴权管理而直接访问后端服务资源。
- **灰度发布**:微服务网关可以根据HTTP请求中的特殊标记和后端服务列表元数据标识进行流量控制,实现在用户无感知的情况下完成灰度发布。
- **流量染色**:和灰度发布的原理相似,网关可以根据HTTP请求的Host、Head、Agent等标识对请求进行染色,有了网关的流量染色功能,我们可以对服务后续的调用链路进行跟踪,对服务延迟及服务运行状况进行进一步的链路分析。
- **文档中心**:网关结合Swagger,可以将后端的微服务暴露给网关,网关作为统一的入口给接口的使用方提供查看后端服务的API规范,不需要知道每一个后端微服务的Swagger地址,这样网关起到了对后端API聚合的效果。
- **日志审计**:微服务网关可以作为统一的日志记录和收集器,对服务URL粒度的日志请求信息和响应信息进行拦截。
## API网关选型
### 常用API网关
先简单看一下市面上常用的API网关:
![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/microservice/api-wangguan-ddaf1b1e-60b2-4cff-bab2-2b7b32f07f72.png)
#### Nginx
Nginx是一个高性能的HTTP和反向代理服务器。**Nginx一方面可以做反向代理,另外一方面可以做静态资源服务器,接口使用Lua动态语言可以完成灵活的定制功能。**
Nginx 在启动后,会有一个 Master 进程和多个 Worker 进程,Master 进程和 Worker 进程之间是通过进程间通信进行交互的,如图所示。Worker 工作进程的阻塞点是在像 select()、epoll_wait() 等这样的 I/O 多路复用函数调用处,以等待发生数据可读 / 写事件。Nginx 采用了异步非阻塞的方式来处理请求,也就是说,Nginx 是可以同时处理成千上万个请求的。
#### Zuul
Zuul 是 Netflix 开源的一个API网关组件,它可以和 Eureka、Ribbon、Hystrix 等组件配合使用。社区活跃,融合于 SpringCloud 完整生态,是构建微服务体系前置网关服务的最佳选型之一。
Zuul 的核心是一系列的过滤器,这些过滤器可以完成以下功能:
- **统一鉴权 + 动态路由 + 负载均衡 + 压力测试**
- **审查与监控**:与边缘位置追踪有意义的数据和统计结果,从而带来精确的生产视图。
- **多区域弹性**:跨越 AWS Region 进行请求路由,旨在实现 ELB(Elastic Load Balancing,弹性负载均衡)使用的多样化,以及让系统的边缘更贴近系统的使用者。
Zuul 目前有两个大的版本:**Zuul1 和 Zuul2**
Zuul1 是基于 Servlet 框架构建,如图所示,采用的是阻塞和多线程方式,即一个线程处理一次连接请求,这种方式在内部延迟严重、设备故障较多情况下会引起存活的连接增多和线程增加的情况发生。
![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/microservice/api-wangguan-93bef98d-7d71-48d1-943a-b8ede72432c8.png)
Netflix 发布的 Zuul2 有重大的更新,它运行在异步和无阻塞框架上,每个 CPU 核一个线程,处理所有的请求和响应,请求和响应的生命周期是通过事件和回调来处理的,这种方式减少了线程数量,因此开销较小。
![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/microservice/api-wangguan-90dfae10-7cbd-4099-a5df-ea09a1435bc0.png)
#### Spring Cloud GetWay
Spring Cloud Gateway 是Spring Cloud的一个全新的API网关项目,目的是为了替换掉Zuul1,它基于Spring5.0 + SpringBoot2.0 + WebFlux(基于⾼性能的Reactor模式响应式通信框架Netty,异步⾮阻塞模型)等技术开发,性能⾼于Zuul,官⽅测试,**Spring Cloud GateWay是Zuul的1.6倍**,旨在为微服务架构提供⼀种简单有效的统⼀的API路由管理⽅式。
Spring Cloud Gateway可以与Spring Cloud Discovery Client(如Eureka)、Ribbon、Hystrix等组件配合使用,**实现路由转发、负载均衡、熔断、鉴权、路径重写、⽇志监控等,并且Gateway还内置了限流过滤器,实现了限流的功能。**
![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/microservice/api-wangguan-71c6dfa6-dcf3-43ab-98bf-34d7ef588f02.png)
#### Kong
Kong是一款基于OpenResty(Nginx + Lua模块)编写的高可用、易扩展的,由Mashape公司开源的API Gateway项目。**Kong是基于NGINX和Apache Cassandra或PostgreSQL构建的**,能提供易于使用的RESTful API来操作和配置API管理系统,所以它可以水平扩展多个Kong服务器,通过前置的负载均衡配置把请求均匀地分发到各个Server,来应对大批量的网络请求。
![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/microservice/api-wangguan-e2113ab8-0fc2-4df5-91be-60154e557cc0.png)
Kong主要有三个组件:
- Kong Server :基于Nginx的服务器,用来接收API请求。
- Apache Cassandra/PostgreSQL :用来存储操作数据。
- Kong dashboard:官方推荐UI管理工具,也可以使用 restfull 方式管理admin api。
Kong采用插件机制进行功能定制,插件集(可以是0或N个)在API请求响应循环的生命周期中被执行。插件使用Lua编写,目前已有几个基础功能:**HTTP基本认证、密钥认证、CORS(Cross-Origin Resource Sharing,跨域资源共享)、TCP、UDP、文件日志、API请求限流、请求转发以及Nginx监控。**
![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/microservice/api-wangguan-c6c7c5e3-214b-472e-b998-8adc820aa922.png)
Kong网关具有以下的特性:
- 可扩展性: 通过简单地添加更多的服务器,可以轻松地进行横向扩展,这意味着您的平台可以在一个较低负载的情况下处理任何请求;
- 模块化: 可以通过添加新的插件进行扩展,这些插件可以通过RESTful Admin API轻松配置;
- 在任何基础架构上运行: Kong网关可以在任何地方都能运行。您可以在云或内部网络环境中部署Kong,包括单个或多个数据中心设置,以及public,private 或invite-only APIs。
#### Traefik
Træfɪk 是一个为了让部署微服务更加便捷而诞生的现代HTTP反向代理、负载均衡工具。 它支持多种后台 (Docker, Swarm, Kubernetes, Marathon, Mesos, Consul, Etcd, Zookeeper, BoltDB, Rest API, file…) 来自动化、动态的应用它的配置文件设置。
![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/microservice/api-wangguan-553541d9-870d-45a8-8026-a1274d809e5e.png)
重要特性:
- 它非常快,无需安装其他依赖,通过Go语言编写的单一可执行文件;
- **多种后台支持:Docker, Swarm, Kubernetes, Marathon, Mesos, Consul, Etcd;**
- 支持支持Rest API、Websocket、HTTP/2、Docker镜像;
- 监听后台变化进而自动化应用新的配置文件设置;
- 配置文件热更新,无需重启进程;
- 后端断路器、负载均衡、容错机制;
- 清爽的前端页面,可监控服务指标。
> 关于Traefik的更多内容,可以查看官网:https://traefik.cn/
### API网关对比
![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/microservice/api-wangguan-ad51c257-fe2d-435d-a043-92a70957756e.png)
![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/microservice/api-wangguan-ae017d9d-db0d-4322-805e-5230189561f5.png)
![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/microservice/api-wangguan-daf82b5b-011d-488d-9d60-c1221bd5b0ec.png)
上面是网关对比截图,偷个懒,大家主要关注Kong、Traefik和Zuul即可:
-**开源社区活跃度**来看,无疑是Kong和Traefik较好;
-**成熟度**来看,较好的是Kong、Tyk、Traefik;
-**性能**来看,Kong要比其他几个领先一些;
-**架构优势**的扩展性来看,Kong、Tyk有丰富的插件,Ambassador也有插件但不多,而Zuul是完全需要自研,但Zuul由于与Spring Cloud深度集成,使用度也很高,近年来Istio服务网格的流行,Ambassador因为能够和Istio无缝集成也是相当大的优势。
下面是其它网友的思考结论,可供参考:
- **性能**:Nginx+Lua形式必然是高于Java语言实现的网关的,Java技术栈里面Zuul1.0是基于Servlet实现的,剩下都是基于webflux实现,性能是高于基于Servlet实现的。**在性能方面我觉得选择网关可能不算那么重要,多加几台机器就可以搞定。**
- **可维护性和扩展性**:Nginx+Lua这个组合掌握的人不算多,如果团队有大神,大佬们就随意了,当没看到这段话,对于一般团队来说的话,选择自己团队擅长的语言更重要。Java技术栈下的3种网关,对于Zuul和Spring Cloud Getway需要或多或少要搞一些集成和配置页面来维护,但是对于Soul我就无脑看看文章,需要哪个搬哪个好了,尤其是可以无脑对接Dubbo美滋滋,此外Soul2.0以后版本可以摆脱ZK,在我心里再无诟病,我就喜欢无脑操作。
- **高可用**:对于网关高可用基本都是统一的策略都是采用多机器部署的方式,前面挂一个负载,对于而外需要用的一些组件大家注意一下。
## 基于Traefik自研的微服务网关
这个是我司自研的微服务网关,基于Traefik进行开发,下面从技术选型、网关框架、网关后台、协议转换进行讲解,绝对干货!
### 技术栈选型
- **Traefik**:一款开源的反向代理与负载均衡工具,它最大的优点是能够与常见的微服务系统直接整合,可以实现自动化动态配置。traefik较为轻量,非常易于使用和设置,性能比较好,已在全球范围内用于生产环境。
- **Etcd**:一个Go言编写的分布式、高可用的一致性键值存储系统,用于提供可靠的分布式键值存储、配置共享和服务发现等功能。
- **Go**:并发能力强,性能媲美C,处理能力是PHP的4倍,效率高,语法简单,易上手,开发效率接近PHP。
![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/microservice/api-wangguan-3567ace8-c7f3-4b5d-bf0f-9bb85fa8c439.png)
### 网关框架
整个网关框架分为3块:
- **网关后台**(hal-fe和hal-admin):用于应用、服务和插件的配置,然后将配置信息发布到ETCD;
- **Traefik**:读取ETCD配置,根据配置信息对请求进行路由分发,如果需要鉴权,会直接通过hal-agent模块进行统一鉴权。鉴权完毕后,如果是Http请求,直接打到下游服务,如果是Grpc和Thrift协议,会通过hal-proxy模块进行协议转换。
- **协议转换模块**:读取ETCD配置,对Traefik分发过来的请求,进行Grpc和Thrift协议转换,并通过服务发现机制,获取服务下游机器,并通过负载均衡,将转换后的数据打到下游服务机器。
![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/microservice/api-wangguan-45943e5f-2cca-45ff-9fb0-908e3cf7c990.png)
### 网关后台
主要由3大模块组成:
- **应用**:主要包括应用名、域名、路径前缀、所属组、状态等,比如印度海外商城、印度社区;
- **服务**:主要包括服务名、注册方式、协议类型、所属组、状态等,比如评论服务、地址服务、搜索服务。
- **插件**:主要包括插件名称、插件类型、插件属性配置等,比如路径前缀替换插件、鉴权插件。
![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/microservice/api-wangguan-0b16b390-77b7-4fba-8529-7be31ba6335b.png)
**一个应用只能绑定一个服务,但是可以绑定多个插件。** 通过后台完成网关配置后,将这些配置信息生成Config文件,发布到ETCD中,Config文件需要遵循严格的数据格式,比如Traefix配置需要遵循官方的文件配置格式,才能被Traefik识别。
![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/microservice/api-wangguan-299f8c95-d6a3-49d8-946d-5efed32faa29.png)
### 协议转换模块
hal-proxy模块是整个微服务网关最复杂,也是技术含量最高的模块,所以给大家详细讲解一下。
#### 问题引入
在讲这个模块前,我们先看下面几个问题:
- 当请求从上游的trafik过来时,需要知道访问下游的机器IP和端口,才能将请求发送给下游,这些机器如何获取呢?
- 有了机器后,我们需要和下游机器建立连接,如果连接用一次就直接释放,肯定对服务会造成很大的压力,这就需要引入Client缓存池,那这个Client缓存池我们又该如何实现呢?
- 最后就是需要对协议进行转换,因为不同的下游服务,支持的协议类型是不一样的,这个网关又是如何动态支持的呢?
![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/microservice/api-wangguan-c2672067-0b21-41ba-807d-f59e0baf5b99.png)
#### 实现原理
![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/microservice/api-wangguan-e7f9c956-cdf2-4238-aeb3-372c61c1532f.png)
我们还是先看一下hal-proxy内部有哪些模块,首先是Resolver模块,这个模块的是什么作用呢?这里我简单介绍一下,目前公司内部通过服务获取到机器列表的方式有多种,比如MIS平台、服务树等,也就是有的是通过平台配置的,有的是直接挂在服务树下,无论哪种方式,我们都通过服务名,通过一定的方式,找到该服务下面所有的主机。
**所以Resolver模块的作用,其实就是通过服务名,找到该服务下的所有机器的IP和服务端口,然后持久化到内存中,并定时更新。**
协议模块就是支持不同的协议转换,每个协议类型的转换,都需要单独实现,**这些协议转换,无非就是先通过机器IP和端口初始化Client,然后再将数据进行转换后,直接发送到下游的机器。**
最后就是连接池,之前我们其实也用到go自带的pool来做,但是当对pool数据进行更新时,需要加锁,所以性能一直起不来,后来**改成了环形队列,然后对数据的操作全部通过原子操作方式,就实现了无锁操作,大大提高的并发性能。**
#### 实现逻辑
这个是hal-proxy的逻辑实现图,画了2天,包含所有核心对象的交互方式,这里就不去细讲,能掌握多少,靠大家自己领悟,如果有任何疑问(或者看不清图片),可以关注我公众号,加我微信沟通。
![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/microservice/api-wangguan-c79970c9-9fce-4164-a299-bb268b058924.png)
----
> 整理:沉默王二,戳[转载链接](https://mp.weixin.qq.com/s/b2xf4GiJksBQqjHVpsh49Q),作者:楼仔,戳[原文链接](https://mp.weixin.qq.com/s/0Qupyl5eCyQGFEAIY6GxoQ)。
![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/xingbiaogongzhonghao.png)
\ No newline at end of file
---
category:
- Java企业级开发
tag:
- MySQL
- Redis
title: 如何保障MySQL和Redis的数据一致性?
---
> 整理:沉默王二,戳[转载链接](https://mp.weixin.qq.com/s/RL4Bt_UkNcnsBGL_9w37Zg),作者:楼仔,戳[原文链接](https://mp.weixin.qq.com/s/l7v4s1VekIPNi7KZuUgwGQ)。
如何保障 MySQL 和 Redis 的数据一致性?这个问题很早之前我就遇到过,但是一直没有仔细去研究,上个月看了极客的课程,有一篇文章专门有过讲解,刚好有粉丝也问我这个问题,所以感觉有必要单独出一篇。
**之前也看了很多相关的文章,但是感觉讲的都不好**,很多文章都会去讲各种策略,比如(旁路缓存)策略、(读穿 / 写穿)策略和(写回)策略等,感觉意义真的不大,然后有的文章也只讲了部分情况,也没有告诉最优解。
我直接先抛一下结论:**在满足实时性的条件下,不存在两者完全保存一致的方案,只有最终一致性方案。** 根据网上的众多解决方案,总结出 6 种,直接看目录:
![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/mysql/redis-shuju-yizhixing-537a505f-1f3f-4f23-b5e3-209c8c8a9281.png)
## 不好的方案
### 1. 先写 MySQL,再写 Redis
![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/mysql/redis-shuju-yizhixing-9ac6e9ab-dd82-40a5-b71b-836c745ed8ac.png)
> 图解说明:
> - 这是一副时序图,描述请求的先后调用顺序;
> - 橘黄色的线是请求 A,黑色的线是请求 B;
> - 橘黄色的文字,是 MySQL 和 Redis 最终不一致的数据;
> - 数据是从 10 更新为 11;
> - 后面所有的图,都是这个含义,不再赘述。
请求 A、B 都是先写 MySQL,然后再写 Redis,在高并发情况下,如果请求 A 在写 Redis 时卡了一会,请求 B 已经依次完成数据的更新,就会出现图中的问题。
这个图已经画的很清晰了,我就不用再去啰嗦了吧,**不过这里有个前提,就是对于读请求,先去读 Redis,如果没有,再去读 DB,但是读请求不会再回写 Redis。** 大白话说一下,就是读请求不会更新 Redis。
### 2. 先写 Redis,再写 MySQL
![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/mysql/redis-shuju-yizhixing-c50e7ef0-40aa-4931-982a-a9aa31faa6f1.png)
同“先写 MySQL,再写 Redis”,看图可秒懂。
### 3. 先删除 Redis,再写 MySQL
这幅图和上面有些不一样,前面的请求 A 和 B 都是更新请求,这里的请求 A 是更新请求,**但是请求 B 是读请求,且请求 B 的读请求会回写 Redis。**
![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/mysql/redis-shuju-yizhixing-0fec5605-5530-4b12-af0e-2b529c41e6e6.png)
请求 A 先删除缓存,可能因为卡顿,数据一直没有更新到 MySQL,导致两者数据不一致。
**这种情况出现的概率比较大,因为请求 A 更新 MySQL 可能耗时会比较长,而请求 B 的前两步都是查询,会非常快。**
## 好的方案
### 4. 先删除 Redis,再写 MySQL,再删除 Redis
对于“先删除 Redis,再写 MySQL”,如果要解决最后的不一致问题,其实再对 Redis 重新删除即可,**这个也是大家常说的“缓存双删”。**
![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/mysql/redis-shuju-yizhixing-1fe439cd-83fe-487f-a7ba-578a84839616.png)
为了便于大家看图,对于蓝色的文字,“删除缓存 10”必须在“回写缓存10”后面,那如何才能保证一定是在后面呢?**网上给出的第一个方案是,让请求 A 的最后一次删除,等待 500ms。**
对于这种方案,看看就行,反正我是不会用,太 Low 了,风险也不可控。
**那有没有更好的方案呢,我建议异步串行化删除,即删除请求入队列**
![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/mysql/redis-shuju-yizhixing-6cb3caf1-c85b-4361-8e29-9e71361fd0c8.png)
异步删除对线上业务无影响,串行化处理保障并发情况下正确删除。
如果双删失败怎么办,网上有给 Redis 加一个缓存过期时间的方案,这个不敢苟同。**个人建议整个重试机制,可以借助消息队列的重试机制,也可以自己整个表,记录重试次数**,方法很多。
> 简单小结一下:
> - “缓存双删”不要用无脑的 sleep 500 ms;
> - 通过消息队列的异步&串行,实现最后一次缓存删除;
> - 缓存删除失败,增加重试机制。
### 5. 先写 MySQL,再删除 Redis
![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/mysql/redis-shuju-yizhixing-1f0e26a8-49c3-469e-a193-08f9766943aa.png)
对于上面这种情况,对于第一次查询,请求 B 查询的数据是 10,但是 MySQL 的数据是 11,**只存在这一次不一致的情况,对于不是强一致性要求的业务,可以容忍。**(那什么情况下不能容忍呢,比如秒杀业务、库存服务等。)
当请求 B 进行第二次查询时,因为没有命中 Redis,会重新查一次 DB,然后再回写到 Reids。
![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/mysql/redis-shuju-yizhixing-268696d3-a7e9-4762-9fe6-283859d5b0ba.png)
这里需要满足 2 个条件:
- 缓存刚好自动失效;
- 请求 B 从数据库查出 10,回写缓存的耗时,比请求 A 写数据库,并且删除缓存的还长。
对于第二个条件,我们都知道更新 DB 肯定比查询耗时要长,所以出现这个情况的概率很小,同时满足上述条件的情况更小。
### 6. 先写 MySQL,通过 Binlog,异步更新 Redis
这种方案,主要是监听 MySQL 的 Binlog,然后通过异步的方式,将数据更新到 Redis,这种方案有个前提,查询的请求,不会回写 Redis。
![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/mysql/redis-shuju-yizhixing-0da55874-8cf7-4c5a-995b-a0e6611bfac2.png)
这个方案,会保证 MySQL 和 Redis 的最终一致性,但是如果中途请求 B 需要查询数据,如果缓存无数据,就直接查 DB;如果缓存有数据,查询的数据也会存在不一致的情况。
**所以这个方案,是实现最终一致性的终极解决方案,但是不能保证实时性。**
## 几种方案比较
我们对比上面讨论的 6 种方案:
1. 先写 Redis,再写 MySQL
- **这种方案,我肯定不会用**,万一 DB 挂了,你把数据写到缓存,DB 无数据,这个是灾难性的;
- 我之前也见同学这么用过,如果写 DB 失败,对 Redis 进行逆操作,那如果逆操作失败呢,是不是还要搞个重试?
2. 先写 MySQL,再写 Redis
- **对于并发量、一致性要求不高的项目,很多就是这么用的**,我之前也经常这么搞,但是不建议这么做;
- 当 Redis 瞬间不可用的情况,需要报警出来,然后线下处理。
3. 先删除 Redis,再写 MySQL
- 这种方式,我还真没用过,**直接忽略吧。**
4. 先删除 Redis,再写 MySQL,再删除 Redis
- 这种方式虽然可行,但是**感觉好复杂**,还要搞个消息队列去异步删除 Redis。
5. 先写 MySQL,再删除 Redis
- **比较推荐这种方式**,删除 Redis 如果失败,可以再多重试几次,否则报警出来;
- 这个方案,是实时性中最好的方案,在一些高并发场景中,推荐这种。
6. 先写 MySQL,通过 Binlog,异步更新 Redis
- **对于异地容灾、数据汇总等,建议会用这种方式**,比如 binlog + kafka,数据的一致性也可以达到秒级;
- 纯粹的高并发场景,不建议用这种方案,比如抢购、秒杀等。
**个人结论:**
- **实时一致性方案**:采用“先写 MySQL,再删除 Redis”的策略,这种情况虽然也会存在两者不一致,但是需要满足的条件有点苛刻,**所以是满足实时性条件下,能尽量满足一致性的最优解。**
- **最终一致性方案**:采用“先写 MySQL,通过 Binlog,异步更新 Redis”,可以通过 Binlog,结合消息队列异步更新 Redis,**是最终一致性的最优解。**
----
> 整理:沉默王二,戳[转载链接](https://mp.weixin.qq.com/s/RL4Bt_UkNcnsBGL_9w37Zg),作者:楼仔,戳[原文链接](https://mp.weixin.qq.com/s/l7v4s1VekIPNi7KZuUgwGQ)。
![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/xingbiaogongzhonghao.png)
\ No newline at end of file
---
category:
- 数据库
- Java企业级开发
tag:
- Spring Boot
- Redis
title: Spring Boot 整合 Redis 缓存
---
# 某意大利小哥,竟靠一个缓存中间件直接封神?
大家好,我是二哥呀!关注我有一段时间的小伙伴都知道了,我最近的业余时间都花在了编程喵🐱这个实战项目上,其中要用到 Redis,于是我就想,索性出一期 Redis 的入门教程吧——主要是整合 Redis 来实现缓存功能,希望能帮助到大家。
作为开发者,相信大家都知道 Redis 的重要性。Redis 是使用 C 语言开发的一个高性能键值对数据库,是互联网技术领域使用最为广泛的存储中间件,它是「Remote Dictionary Service」的首字母缩写,也就是「远程字典服务」。
Redis 以超高的性能、完美的文档、简洁的源码著称,国内外很多大型互联网公司都在用,比如说阿里、腾讯、GitHub、Stack Overflow 等等。当然了,中小型公司也都在用。
![](https://img-blog.csdnimg.cn/img_convert/10872dcbe110f9aa7aaf23a8fed76469.png)
### 安装 Redis
Redis 的作者是一名意大利人,原名 Salvatore Sanfilippo,网名 Antirez。不过,很遗憾的是,网上竟然没有他的维基百科,甚至他自己的博客网站,都在跪的边缘(没有 HTTPS,一些 js 也加载失败了)
Redis 的官网提供了各种平台的安装包,Linux、macOS、Windows 的都有
![](https://img-blog.csdnimg.cn/img_convert/5dba51cff6e4f1c6c05ab893749b0a18.png)
![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/redis/redis-springboot-182f2469-b7f2-4fec-bd41-e5a33dca185a.png)
不过,如果是鄙人造出 Redis 这么酷炫的产品,早就功成身退了。
>官方地址:[https://redis.io/docs/getting-started/](https://redis.io/docs/getting-started/)
### 一、安装 Redis
我目前用的是 macOS,直接执行 `brew install redis` 就可以完成安装了。
Redis 的官网提供了各种平台的安装包,Linux、macOS、Windows 的都有。
![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/redis/redis-springboot-cdf02715-5ed6-44b5-a1ce-db0249107dd7.png)
完成安装后执行 `redis-server` 就可以启动 Redis 服务了。
![](https://img-blog.csdnimg.cn/img_convert/a4ac529b6f2a4ee018e344ba1e65dc5f.png)
![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/redis/redis-springboot-8c272a19-e961-449c-afee-c973fb44a5e0.png)
>官方地址:[https://redis.io/docs/getting-started/](https://redis.io/docs/getting-started/)
Windows 用户可以通过我之前提到的 [chocolatey 命令行软件管理神器](https://mp.weixin.qq.com/s/Hgm3ZAbOeBqpSUsJZBtlNg)安装(可以戳链接了解详情),只需要一行命令 `choco install redis` 就可以完成安装了,非常方便。
我目前用的是 macOS,直接执行 `brew install redis` 就可以完成安装了。
![](https://img-blog.csdnimg.cn/img_convert/7de3dc4db8e8711270a6974aa8e083c5.png)
![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/redis/redis-springboot-13b569ca-e747-4d64-af0d-a9a5d0260f5f.png)
完成安装后执行 `redis-server` 就可以启动 Redis 服务了。
![](https://img-blog.csdnimg.cn/img_convert/d1ea58d82b3dfbd79baef07efef24054.png)
生产环境中,我们通常会在 Linux 上安装 Redis。我的服务器上安装了宝塔面板,可以直接在软件商店里搜「Redis」关键字,然后直接安装(我已经安装过了)。
![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/redis/redis-springboot-df5e600b-1290-447c-b140-6f513c69492c.png)
顺带安装一下 Redis 客户端工具,推荐 GitHub 星标 20k+ 的 AnotherRedisDesktopManager,一款 🚀🚀🚀 更快、更好、更稳定的Redis桌面(GUI)管理客户端,支持 Windows、macOS 和 Linux,性能出众,可以轻松加载海量键值。
>[https://github.com/qishibo/AnotherRedisDesktopManager](https://github.com/qishibo/AnotherRedisDesktopManager)
安装完成后,链接 Redis 服务:
![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/redis/redis-springboot-d36b9022-fe3b-4fb1-80c3-8d23d19d9025.png)
### Redis 数据类型
Redis支持五种数据类型:string(字符串),hash(哈希),list(列表),set(集合)及zset(sorted set:有序集合)。
>Redis 教程:[https://www.redis.net.cn/tutorial/3508.html](https://www.redis.net.cn/tutorial/3508.html)
**1)string**
string 是 Redis 最基本的数据类型,一个key对应一个value。
我们可以通过 AnotherRedisDesktopManager 客户端来练习一下基本的 set、get 命令(参考 Redis 文档,客户端会有提示,所以命令完全不用死记硬背)。
![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/redis/redis-springboot-d7d4043b-b753-484c-bfc1-25533004cca5.png)
对应文本命令:
```
### 增加一个 key 为 name,value 为 沉默王二
> set name '沉默王二'
OK
### 获取
> get name
沉默王二
> set name '沉默王三'
OK
> get name
沉默王三
### 删除
> del name
1
> get name
null
### 测试是否存在 name
> EXISTS key
0
> EXISTS name
0
```
**2)hash**
Redis hash 是一个键值对集合,值可以看成是一个 Map。
```
### 清除数据库
> flushdb
OK
### 创建 hash,key 为 user_hset 字段为 user1,值为 沉默王二
> hset user_hset user1 沉默王二
1
> hset user_hset user2 沉默王三
1
### 字段长度
> hlen user_hset
2
### 所有字段
> HKEYS user_hset
user1
user2
### 所有值
> hvals user_hset
沉默王二
沉默王三
### 字段 user1 的值
> hget user_hset user1
沉默王二
### 获取 key 为 user_hset 的所有字段和值
> hgetall user_hset
user1
沉默王二
user2
沉默王三
### 更新字段
> hset user_hset user1 沉默王四
0
> hgetall user_hset
user1
沉默王四
user2
沉默王三
> hdel user_hset user1
1
> hgetall user_hset
user2
沉默王三
```
**3)list**
list 是一个简单的字符串列表,按照插入顺序排序。
```
### 添加 key 为 user_list value 为 沉默王二、沉默万三的集合
> lpush user_list 沉默王二 沉默王三
2
### 查询
> lrange user_list 0 -1
沉默王三
沉默王二
### 往尾部添加
> rpush user_list 沉默王二是沙比
3
### 头部添加
> lpush user_list 沉默王二是傻叉
4
> lrange user_list 0 -1
沉默王二是傻叉
沉默王三
沉默王二
沉默王二是沙比
### 更新 index 为 0 的值
> lset user_list 0 沉默王四
OK
> lrange user_list 0 -1
沉默王四
沉默王三
沉默王二
沉默王二是沙比
### 删除 index 为 0 的值
> lrem user_list 0 沉默王四
1
> lrange user_list 0 -1
沉默王三
沉默王二
沉默王二是沙比
```
**4)set**
set 是 string 类型的无序集合,不允许有重复的元素。
```
### 添加 key 为 user_set value 为沉默王二 沉默王三 沉默王二的狗腿子的集合
> sadd user_set 沉默王二 沉默王三 沉默王二的狗腿子
3
### 查询
> smembers user_set
沉默王二
沉默王二的狗腿子
沉默王三
### 删除 value 为沉默王二的元素
> srem user_set 沉默王二
1
> smembers user_set
沉默王二的狗腿子
沉默王三
### 添加
> sadd user_set 沉默王二
1
> smembers user_set
沉默王二
沉默王二的狗腿子
沉默王三
```
**5)sorted set**
不过,实际的开发当中,我们通常会选择 Linux 服务器来作为生产环境。我的服务器上安装了宝塔面板,可以直接在软件商店里搜「Redis」关键字,然后直接安装(我的已经安装过了)。
sorted set 是 string 类型的有序集合,不允许有重复的元素。
```
> FLUSHDB
OK
### 添加 key 为 user_zset 分数为 1 值为沉默王二、分数为 2 值为沉默王三、分数为 3 值为沉默王二的狗腿子
> zadd user_zset 1 沉默王二 2 沉默王三 3 沉默王二的狗腿子
3
### 查询
> zrange user_zset 0 -1
沉默王二
沉默王三
沉默王二的狗腿子
### 反转
> zrevrange user_zset 0 -1
沉默王二的狗腿子
沉默王三
沉默王二
### 查询元素沉默王二的分数
> zscore user_zset 沉默王二
1
```
![](https://img-blog.csdnimg.cn/img_convert/753dc67e5ca3fd1090c281ff77c105a2.png)
### 二、整合 Redis
### Spring Boot 整合 Redis
第一步,在 pom.xml 文件中添加 Redis 的 starter。
```
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
```
第二步,在 application.yml 文件中添加 Redis 的配置信息
```
spring:
redis:
host: xxx.xxx.99.232 # Redis服务器地址
database: 0 # Redis数据库索引(默认为0)
port: 6379 # Redis服务器连接端口
password: xxx # Redis服务器连接密码(默认为空)
```
第三步,在测试类中添加以下代码。
```java
@SpringBootTest
class CodingmoreRedisApplicationTests {
@Resource
private RedisTemplate redisTemplate;
@Resource
private StringRedisTemplate stringRedisTemplate;
@Test
public void testRedis() {
// 添加
redisTemplate.opsForValue().set("name","沉默王二");
// 查询
System.out.println(redisTemplate.opsForValue().get("name"));
// 删除
redisTemplate.delete("name");
// 更新
redisTemplate.opsForValue().set("name","沉默王二的狗腿子");
// 查询
System.out.println(redisTemplate.opsForValue().get("name"));
// 添加
stringRedisTemplate.opsForValue().set("name","沉默王二");
// 查询
System.out.println(stringRedisTemplate.opsForValue().get("name"));
// 删除
stringRedisTemplate.delete("name");
// 更新
stringRedisTemplate.opsForValue().set("name","沉默王二的狗腿子");
// 查询
System.out.println(stringRedisTemplate.opsForValue().get("name"));
}
}
```
RedisTemplate 和 StringRedisTemplate 都是 Spring Data Redis 提供的模板类,可以对 Redis 进行操作,后者针对键值对都是 String 类型的数据,前者可以是任何类型的对象。
RedisTemplate 和 StringRedisTemplate 除了提供 opsForValue 方法来操作字符串之外,还提供了以下方法:
- opsForList:操作 list
- opsForSet:操作 set
- opsForZSet:操作有序 set
- opsForHash:操作 hash
运行测试类后可以在控制台看到以下输出信息:
![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/redis/redis-springboot-f4456aea-2e48-4bad-910d-2d963ef0224e.png)
也可以通过 AnotherRedisDesktopManager 客户端查看 Redis 数据库中的数据。
![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/redis/redis-springboot-f7551ebb-0bde-4084-9ab0-4a724d8ad2ec.png)
### 编程喵整合 Redis
编程喵是一个 Spring Boot + Vue 的前后端分离项目,要整合 Redis 的话,最好的方式是使用 Spring Cache,仅仅通过 @Cacheable、@CachePut、@CacheEvict、@EnableCaching 等注解就可以轻松使用 Redis 做缓存了。
![](https://img-blog.csdnimg.cn/img_convert/3855e75d86e5da978a76369384dd8055.png)
![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/redis/redis-springboot-eb6d69d7-9152-4695-87c7-cba138ca93fd.png)
**1)@EnableCaching**,开启缓存功能。
......@@ -63,7 +331,7 @@ Spring Cache 的注解(前面提到的四个)会在调用方法之后,去
这些读写操作不用我们手动再去写代码实现了,直接交给 Spring Cache 来打理就 OK 了,是不是非常贴心?
![](https://img-blog.csdnimg.cn/img_convert/66e9480a7b1e9ecf48953dc97ed882f0.png)
![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/redis/redis-springboot-afed274d-458d-4e6e-9fd0-b421ac811f47.png)
**第一步**,在 pom.xml 文件中追加 Redis 的 starter。
......@@ -195,13 +463,13 @@ public class PostTagController {
**第五步**,启动服务器端,启动客户端,修改标签进行测试。
![](https://img-blog.csdnimg.cn/img_convert/7a3faff80eae3d43031aae38f2fc8a89.png)
![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/redis/redis-springboot-6463fdda-6cc2-43f4-91e6-e0de9f0f1c0c.png)
通过 Red 客户端(一款 macOS 版的 Redis 桌面工具),可以看到刚刚更新的返回值已经添加到 Redis 中了。
![](https://img-blog.csdnimg.cn/img_convert/482888d29652bcf20ec5e1af369cd03b.png)
![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/redis/redis-springboot-468a253d-931a-4e5b-8f7e-36ecc1561dac.png)
### 三、使用 Redis 连接池
### 使用 Redis 连接池
Redis 是基于内存的数据库,本来是为了提高程序性能的,但如果不使用 Redis 连接池的话,建立连接、断开连接就需要消耗大量的时间。
......@@ -214,7 +482,7 @@ Redis 是基于内存的数据库,本来是为了提高程序性能的,但
它俩在 GitHub 上都挺受欢迎的,大家可以按需选用。
![](https://img-blog.csdnimg.cn/img_convert/0304871d4718d0dd9a5d428f14bb21a3.png)
![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/redis/redis-springboot-c94651b5-1e53-42e5-ad5f-162b4bf509a7.png)
我这里把两种客户端的情况都演示一下,方便小伙伴们参考。
......@@ -237,11 +505,11 @@ spring:
```
Caused by: java.lang.ClassNotFoundException: org.apache.commons.pool2.impl.GenericObjectPoolConfig
at java.net.URLClassLoader.findClass(URLClassLoader.java:381)
at java.lang.ClassLoader.loadClass(ClassLoader.java:424)
at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:335)
at java.lang.ClassLoader.loadClass(ClassLoader.java:357)
... 153 common frames omitted
at java.net.URLClassLoader.findClass(URLClassLoader.java:381)
at java.lang.ClassLoader.loadClass(ClassLoader.java:424)
at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:335)
at java.lang.ClassLoader.loadClass(ClassLoader.java:357)
... 153 common frames omitted
```
添加 commons-pool2 依赖:
......@@ -258,11 +526,11 @@ Caused by: java.lang.ClassNotFoundException: org.apache.commons.pool2.impl.Gener
重新启动服务,在 RedisConfig 类的 redisTemplate 方法里对 redisTemplate 打上断点,debug 模式下可以看到连接池的配置信息(`redisConnectionFactory→clientConfiguration→poolConfig`)。如下图所示。
![](https://img-blog.csdnimg.cn/img_convert/1550863dcb1dc7eb24efb265d5b04319.png)
![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/redis/redis-springboot-e4cd346c-07d0-4ee3-9832-4c7a2aa1b7b4.png)
如果在 application-dev.yml 文件中没有添加 Lettuce 连接池配置的话,是不会看到
![](https://img-blog.csdnimg.cn/img_convert/4dd422a75f9e710ea34a1771001b8333.png)
![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/redis/redis-springboot-7e86e208-bf5f-4dc2-a962-2b926adaa524.png)
......@@ -304,22 +572,22 @@ spring:
启动服务后,观察 redisTemplate 的 clientConfiguration 节点,可以看到它的值已经变成 DefaultJedisClientConfiguration 对象了。
![](https://img-blog.csdnimg.cn/img_convert/fab2a70033e339b836802aa15b1327ba.png)
![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/redis/redis-springboot-01aa7dc6-b9f7-46bd-b8a4-0a24e44185bc.png)
当然了,也可以不配置 Jedis 客户端的连接池,走默认的连接池配置。因为 Jedis 客户端默认增加了连接池的依赖包,在 pom.xml 文件中点开 Jedis 客户端依赖可以查看到。
![](https://img-blog.csdnimg.cn/img_convert/dabc473eebaf612a913f740002a6af5f.png)
![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/redis/redis-springboot-c87d8e02-aace-4d86-8011-13e4087956e0.png)
### 四、自由操作 Redis
### 自由操作 Redis
Spring Cache 虽然提供了操作 Redis 的便捷方法,比如我们前面演示的 @CachePut 注解,但注解提供的操作非常有限,比如说它只能保存返回值到缓存中,而返回值并不一定是我们想要保存的结果。
![](https://img-blog.csdnimg.cn/img_convert/3804abcc76359d9f225ac794e68650bc.png)
![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/redis/redis-springboot-f28a3b84-ed0b-4a78-a5e5-5803bae967be.png)
与其保存这个返回给客户端的 JSON 信息,我们更想保存的是更新后的标签。那该怎么自由地操作 Redis 呢?
![](https://img-blog.csdnimg.cn/img_convert/2effe1fc18c93391c51340576a00ddf5.png)
![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/redis/redis-springboot-9b89af51-c2fd-4b2d-ba57-a59efa4cbffd.png)
**第一步**,增加 RedisService 接口:
......@@ -421,27 +689,20 @@ public class PostTagController {
}
```
**第四步**,重启服务,使用 Knife4j 测试该接口 :
**第四步**,重启服务,使用 [Knife4j](https://mp.weixin.qq.com/s/gWPCg6TP3G_-I-eqA6EJmA) 测试该接口 :
![](https://img-blog.csdnimg.cn/img_convert/5a05faf1d698be1004d43459d10c4c41.png)
![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/redis/redis-springboot-b74b8efe-4ef8-4395-9892-692272593b68.png)
然后通过 Red 查看该缓存,OK,确认我们的代码是可以完美执行的。
![](https://img-blog.csdnimg.cn/img_convert/7c5062a79c293b58f279e61588280d18.png)
### 五、小结
赞美 Redis 的彩虹屁我就不再吹了,总之,如果我是 Redis 的作者 Antirez,我就自封为神!
![](https://img-blog.csdnimg.cn/img_convert/dbbeda5f72afd77fbccb2f66560f8ab3.gif)
![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/redis/redis-springboot-6b3979b7-37be-4c0a-b6fa-7701850ea0a1.png)
编程喵实战项目的源码地址我贴下面了,大家可以下载下来搞一波了:
> [https://github.com/itwanger/coding-more](https://github.com/itwanger/coding-more)
我们下期见~
### 项目源码
----
> - 编程喵:[https://github.com/itwanger/coding-more](https://github.com/itwanger/coding-more)
> - 整合 Redis 专用:[https://github.com/itwanger/coding-more](https://github.com/itwanger/codingmore-learning/tree/main/codingmore-redis)
![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/xingbiaogongzhonghao.png)
---
category:
- Java企业级开发
tag:
- Spring Boot
title: Spring Boot 整合 Thymeleaf 模板引擎
---
### 前言
先说作用。
- 过滤器(Filter):当有一堆请求,只希望符合预期的请求进来。
- 拦截器(Interceptor):想要干涉预期的请求。
- 监听器(Listener):想要监听这些请求具体做了什么。
再说区别。
过滤器是在请求进入容器后,但还没有进入 Servlet 之前进行预处理的。如下图所示。
![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/springboot/Filter-Interceptor-Listener-2a28621b-2cc1-4d29-87a1-cf6a01d95443.png)
拦截器是在请求进入控制器(Controller) 之前进行预处理的。
虚线内就是过滤器和拦截器的作用范围:
![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/springboot/Filter-Interceptor-Listener-dd48851b-c123-4fd8-b82d-9ae87b33745d.png)
过滤器依赖于 Servlet 容器,而拦截器依赖于 Spring 的 IoC 容器,因此可以通过注入的方式获取容器当中的对象。
监听器用于监听 Web 应用中某些对象的创建、销毁、增加、修改、删除等动作,然后做出相应的处理。
### 过滤器
- 过滤敏感词汇(防止sql注入)
- 设置字符编码
- URL级别的权限访问控制
- 压缩响应信息
过滤器的创建和销毁都由 Web 服务器负责,Web 应用程序启动的时候,创建过滤器对象,为后续的请求过滤做好准备。
过滤器可以有很多个,一个个过滤器组合起来就成了 FilterChain,也就是过滤器链。
在 Spring 中,过滤器都默认继承了 OncePerRequestFilter,顾名思义,OncePerRequestFilter 的作用就是确保一次请求只通过一次过滤器,而不重复执行。
![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/springboot/Filter-Interceptor-Listener-aaa1c537-c8ed-4c5d-b27f-93c1409f2748.png)
在编程喵实战项目中,我们就是通过继承 OncePerRequestFilter 来实现 JWT 登录授权过滤的。
```java
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain chain) throws ServletException, IOException {
// 从客户端请求中获取 JWT
String authHeader = request.getHeader(this.tokenHeader);
// 该 JWT 是我们规定的格式,以 tokenHead 开头
if (authHeader != null && authHeader.startsWith(this.tokenHead)) {
// The part after "Bearer "
String authToken = authHeader.substring(this.tokenHead.length());
// 从 JWT 中获取用户名
String username = jwtTokenUtil.getUserNameFromToken(authToken);
LOGGER.info("checking username:{}", username);
// SecurityContextHolder 是 SpringSecurity 的一个工具类
// 保存应用程序中当前使用人的安全上下文
if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
// 根据用户名获取登录用户信息
UserDetails userDetails = this.userDetailsService.loadUserByUsername(username);
// 验证 token 是否过期
if (jwtTokenUtil.validateToken(authToken, userDetails)) {
// 将登录用户保存到安全上下文中
UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails,
null, userDetails.getAuthorities());
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(authentication);
}
}
}
chain.doFilter(request, response);
}
}
```
我们利用 Spring Initializr 来新建一个 Web 项目 codingmore-filter-interceptor-listener。
添加一个过滤器 MyFilter :
```java
@WebFilter(urlPatterns = "/*", filterName = "myFilter")
public class MyFilter implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
Filter.super.init(filterConfig);
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
long start = System.currentTimeMillis();
chain.doFilter(request,response);
System.out.println("Execute cost="+(System.currentTimeMillis()-start));
}
@Override
public void destroy() {
Filter.super.destroy();
}
}
```
@WebFilter 注解用于将一个类声明为过滤器,urlPatterns 属性用来指定过滤器的 URL 匹配模式,filterName 用来定义过滤器的名字。
MyFilter 过滤器的逻辑非常简单,重写了 Filter 的三个方法,在 doFilter 方法中加入了时间戳的记录。
然后我们在项目入口类上加上 @ServletComponentScan 注解,这样过滤器就会自动注册。
启动服务器,访问任意的 URL。
![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/springboot/Filter-Interceptor-Listener-c865b6d2-30d3-435b-a930-c732caed17ce.png)
### 拦截器
- 登录验证,判断用户是否登录
- 权限验证,判断用户是否有权限访问资源,如校验token
- 日志记录,记录请求操作日志(用户ip,访问时间等),以便统计请求访问量
- 处理cookie、本地化、国际化、主题等
- 性能监控,监控请求处理时长等
我们来写一个简单的拦截器 LoggerInterceptor:
```java
@Slf4j
public class LoggerInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
log.info("preHandle{}...",request.getRequestURI());
return true;
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
HandlerInterceptor.super.postHandle(request, response, handler, modelAndView);
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
HandlerInterceptor.super.afterCompletion(request, response, handler, ex);
}
}
```
一个拦截器必须实现 HandlerInterceptor 接口,preHandle 方法是 Controller 方法调用前执行,postHandle 是 Controller 方法正常返回后执行,afterCompletion 方法无论 Controller 方法是否抛异常都会执行。
只有 preHandle 返回 true 的话,其他两个方法才会执行。
如果 preHandle 返回 false 的话,表示不需要调用Controller方法继续处理了,通常在认证或者安全检查失败时直接返回错误响应。
再来一个 InterceptorConfig 对拦截器进行配置:
```java
@Configuration
public class InterceptorConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new LoggerInterceptor()).addPathPatterns("/**");
}
}
```
@Configuration 注解用于定义配置类,干掉了以往 Spring 繁琐的 xml 配置文件。
编写一个用于被拦截的控制器 MyInterceptorController:
```java
@RestController
@RequestMapping("/myinterceptor")
public class MyInterceptorController {
@RequestMapping("/hello")
public String hello() {
return "沉默王二是傻X";
}
}
```
@RestController 注解相当于 @Controller + @ResponseBody 注解,@ResponseBody 注解用于将 Controller 方法返回的对象,通过适当的 HttpMessageConverter 转换为指定格式后,写入到 Response 对象的 body 数据区,通常用来返回 JSON 或者 XML 数据,返回 JSON 数据的情况比较多。
![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/springboot/Filter-Interceptor-Listener-27c3f03f-8cca-4cbe-84cb-005075c0b8c9.png)
启动服务器,访问 `http://localhost:8080/myinterceptor/hello`
![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/springboot/Filter-Interceptor-Listener-dcd99eeb-c00e-4a7a-a1c2-f8c5ca952aed.png)
在控制台可以看到拦截器中的日志信息:
![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/springboot/Filter-Interceptor-Listener-20bb0987-77c8-4069-a59f-bbd2d5584d8c.png)
无论是过滤器还是拦截器,都属于AOP(面向切面编程)思想的具体实现。除了这两种实现之外,还有另一种更灵活的AOP实现技术,即 Aspect,在编程喵实战项目里,你可以看到 Aspect 具体实现。
比如说统一日志切面 WebLogAspect,就是用来记录请求信息的。
```java
@Aspect
@Component
@Order(1)
public class WebLogAspect {
private static final Logger LOGGER = LoggerFactory.getLogger(WebLogAspect.class);
@Pointcut("execution(public * com.codingmore.controller.*.*(..))")
public void webLog() {
}
@Before("webLog()")
public void doBefore(JoinPoint joinPoint) throws Throwable {
}
@AfterReturning(value = "webLog()", returning = "ret")
public void doAfterReturning(Object ret) throws Throwable {
}
@Around("webLog()")
public Object doAround(ProceedingJoinPoint joinPoint) throws Throwable {
long startTime = System.currentTimeMillis();
//获取当前请求对象
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
webLog.setStartTime(startTime);
webLog.setUri(request.getRequestURI());
logMap.put("parameter",webLog.getParameter());
logMap.put("spendTime",webLog.getSpendTime());
logMap.put("description",webLog.getDescription());
LOGGER.info("{}", JSONUtil.parse(webLog));
return result;
}
/**
* 根据方法和传入的参数获取请求参数
*/
private Object getParameter(Method method, Object[] args) {
}
}
```
通过拦截后的请求信息大概是这样的:
![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/springboot/Filter-Interceptor-Listener-7a4b219d-bd3e-435e-a2dc-93f4fe4e8cc2.png)
### 监听器
根据监听对象可以把监听器分为 3 类:
- ServletContext:对应应用 application,整个 Web 服务器中只有一个,Web 服务器关闭时销毁。可用于数据缓存,例如结合redis,在Web服务创建时从数据库拉取数据到缓存服务器。
- HttpSession: 对应会话 session,在会话建立时创建,一端会话关闭时销毁。可用于获取在线用户数量。
- ServletRequest:对应 request,客户端发送请求时创建,一同创建的还有 response,用于封装请求数据,在一次请求处理完成时销毁。可用于封装用户信息。
新建一个 MyListener:
```java
@WebListener
public class MyListener implements ServletContextListener {
@Override
public void contextInitialized(ServletContextEvent sce) {
System.out.println("上下文创建");
}
@Override
public void contextDestroyed(ServletContextEvent sce) {
System.out.println("上下文销毁");
}
}
```
@WebListener 注解用于将一个类声明为监听器,同样干掉了 web.xml 文件。
ServletContextListener 能够监听整个 Web 应用程序的生命周期。当 Web 应用启动时触发 contextInitialized 方法,关闭时触发 contextDestroyed 方法。
在 Intellij IDEA 中重启服务的时候,可以在控制台看到如下信息:
![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/springboot/Filter-Interceptor-Listener-6b1904a7-64dc-44fa-8dde-8e986325ec7e.png)
不过需要注意的是,在 Intellij IDEA 中直接关闭进程无法看到 contextDestroyed 被调用的消息。
### 源码路径:
> - 编程喵:[https://github.com/itwanger/coding-more](https://github.com/itwanger/coding-more)
> - 过滤器,拦截器、监听器专用:[https://github.com/itwanger/coding-more](https://github.com/itwanger/codingmore-learning/tree/main/codingmore-filter-interceptor-listener)
![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/xingbiaogongzhonghao.png)
\ No newline at end of file
---
category:
- Java企业级开发
tag:
- Spring Boot
- Redis
title: Spring Boot 整合 Logback 定制日志框架
---
### 关于 Logback
日志系统是一个线上项目必备的素质之一,代表性的日志框架 Log4j、SLF4J、Logback 这哥仨竟然是亲兄弟,他们有一个亲爹,那就是巨佬 Ceki Gulcu。
由于 Spring Boot 的默认日志框架选用的 Logback,再加上 Log4j2 之前爆过严重的漏洞,所以我们这次就只关注 Logback。
1)Logback 非常自然地实现了 SLF4J,不需要像 Log4j 和 JUL 那样加一个适配层。
![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/springboot/logback-67c983bf-101d-48cc-80da-3cb031d7407b.png)
2)Spring Boot 的默认日志框架使用的是 Logback,启动编程喵项目的时候就可以看到 Logback 记录的日志了。
![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/springboot/logback-a2cacfa1-484a-4904-bea3-248d12097387.png)
怎么看出来是 logback 呢?
说实话,看不出来,哈哈哈,不过可以从 Spring Boot 官网找到证据。
![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/springboot/logback-9ac58c2c-e7f9-4df7-aede-ba7d5c69741c.png)
还有,通过源码也可以窥见一二。
![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/springboot/logback-7a10bd7b-598a-4c30-9c83-b80689671f41.png)
3)logback 分为三个模块:
- logback-core,核心模块,提供了关键的通用机制;
- logback-classic,可以看作是 log4j 的改进版,实现了简单的日志门面 SLF4J;
- logback-access,主要用来和 Servlet 容器交互,比如说 Tomcat,提供了一些 HTTP 访问的功能。
如果想研究 logback 源码的话,可以按照这三个模块去研究。
### 直接上手
不废话,直接新建一个空的 Spring Boot 项目,在测试类中加上两行代码。
```java
@SpringBootTest
class CodingmoreLogbackApplicationTests {
static Logger logger = LoggerFactory.getLogger(CodingmoreLogbackApplicationTests.class);
@Test
void contextLoads() {
logger.info("logback testing");
}
}
```
Logger 和 LoggerFactory 都来自 SLF4J,所以如果项目是从 Log4j + SLF4J 切换到 Logback 的话,此时的代码是零改动的。
其他什么也不用做,运行后就可以看到 logback 已经正常工作了。
![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/springboot/logback-07c6b600-3667-4113-bbd5-5ec25990e9dc.png)
在没有配置文件的情况下,一切都是默认的,Logback 的日志信息会输出到控制台。可以通过 StatusPrinter 来打印 Logback 的内部信息:
```java
LoggerContext lc = (LoggerContext)LoggerFactory.getILoggerFactory();
StatusPrinter.print(lc);
```
再次运行测试类,可以在控制台看到以下信息:
![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/springboot/logback-05b134ab-b6e6-4a10-a00c-41b829938618.png)
也就是说,Logback 会在 classpath 路径下先寻找 logback-test.xml 文件,没有找到的话,寻找 logback.xml 文件,都找不到的话,就输出到控制台。
并且,Logger 的默认日志级别是 INFO 级别的,这就意味着如果尝试下面的代码:
```java
logger.debug("沉默王二是傻 X");
```
控制台是看不到这行日志输出的。Logback 的日志级别是这样的:
>TRACE < DEBUG < INFO < WARN < ERROR
也就是说小于 INFO 级别的日志都不会记录,只有大于等于 INFO 级别的日志才会被记录。
### 编程喵实战项目的日志案例分析
尽管默认配置很有用,但它很可能不能满足我们的实际开发需求,于是我们需要找到一种更优雅的解决方案。
`src/main/resources` 目录下有以下名称之一的配置文件时,Spring Boot 将自动加载它来作为 Logback 的配置项:
- logback-spring.xml
- logback.xml
- logback-spring.groovy
- logback.groovy
Spring Boot 建议我们使用 `-spring` 结尾的配置文件,这样可以使用 Spring Boot 的 Profile 功能(针对不同的环境(开发环境、测试环境、正式环境)提供不同的配置项)。
编程喵用的是 logback-spring.xml(在 codingmore-admin 项目下可以找得到):
```
<!--
configuration 有三个属性:
scan:当此属性设置为true时,配置文件如果发生改变,将会被重新加载,默认值为true。
scanPeriod:设置监测配置文件是否有修改的时间间隔,如果没有给出时间单位,默认单位是毫秒当scan为true时,此属性生效。默认的时间间隔为1分钟。
debug:当此属性设置为true时,将打印出logback内部日志信息,实时查看logback运行状态。默认值为false。
-->
<configuration>
<include resource="org/springframework/boot/logging/logback/defaults.xml" />
<!-- 定义日志文件名称 -->
<property name="APP_NAME" value="codingmore-admin" />
<!-- 定义日志文件的路径 -->
<property name="LOG_PATH" value="${user.home}/${APP_NAME}/logs" />
<!-- 定义日志的文件名 -->
<property name="LOG_FILE" value="${LOG_PATH}/codingmore-admin.log" />
<!-- 滚动记录日志,先将日志记录到指定文件,当符合某个条件时,将日志记录到其他文件 -->
<appender name="APPLICATION"
class="ch.qos.logback.core.rolling.RollingFileAppender">
<!-- 指定日志文件的名称 -->
<file>${LOG_FILE}</file>
<!--
当发生滚动时,决定 RollingFileAppender 的行为,涉及文件移动和重命名
TimeBasedRollingPolicy: 最常用的滚动策略,它根据时间来制定滚动策略,既负责滚动也负责触发滚动。
-->
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<!--
滚动时产生的文件的存放位置及文件名称
%d{yyyy-MM-dd}:按天进行日志滚动
%i:当文件大小超过maxFileSize时,按照i进行文件滚动
-->
<fileNamePattern>${LOG_FILE}.%d{yyyy-MM-dd}.%i.log</fileNamePattern>
<!--
可选节点,控制保留的归档文件的最大数量,超出数量就删除旧文件。假设设置每天滚动,
且maxHistory是7,则只保存最近7天的文件,删除之前的旧文件。
注意,删除旧文件时,那些为了归档而创建的目录也会被删除。
-->
<maxHistory>7</maxHistory>
<!--
当日志文件超过maxFileSize指定的大小时,根据上面提到的%i进行日志文件滚动
注意此处配置SizeBasedTriggeringPolicy是无法实现按文件大小进行滚动的,
必须配置timeBasedFileNamingAndTriggeringPolicy
-->
<timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
<maxFileSize>50MB</maxFileSize>
</timeBasedFileNamingAndTriggeringPolicy>
</rollingPolicy>
<!-- 日志输出格式: -->
<layout class="ch.qos.logback.classic.PatternLayout">
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [ %thread ] - [ %-5level ] [ %logger{50} : %line ] - %msg%n</pattern>
</layout>
</appender>
<!-- ch.qos.logback.core.ConsoleAppender 表示控制台输出 -->
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
<!--
日志输出格式:
%d表示日期时间,%green 绿色
%thread表示线程名,%magenta 洋红色
%-5level:级别从左显示5个字符宽度 %highlight 高亮色
%logger{36} 表示logger名字最长36个字符,否则按照句点分割 %yellow 黄色
%msg:日志消息
%n是换行符
-->
<layout class="ch.qos.logback.classic.PatternLayout">
<pattern>%green(%d{yyyy-MM-dd HH:mm:ss.SSS}) [%magenta(%thread)] %highlight(%-5level) %yellow(%logger{36}): %msg%n</pattern>
</layout>
</appender>
<!--
root与logger是父子关系,没有特别定义则默认为root,任何一个类只会和一个logger对应,
要么是定义的logger,要么是root,判断的关键在于找到这个logger,然后判断这个logger的appender和level。
-->
<root level="info">
<appender-ref ref="CONSOLE" />
<appender-ref ref="APPLICATION" />
</root>
</configuration>
```
具体含义都写在了注释里,大家可以细致看一下,了解即可。基本上生产环境下的 Logback 配置就是这样的,可微调。
也可以使用下面这份 logback-spring.xml。
```
<?xml version="1.0" encoding="UTF-8"?>
<configuration >
<include resource="org/springframework/boot/logging/logback/defaults.xml" />
<property name="APP_NAME" value="codingmore-admin" />
<property name="LOG_PATH" value="${user.home}/${APP_NAME}/logs" />
<property name="LOG_FILE" value="${LOG_PATH}/codingmore-admin.log" />
<appender name="APPLICATION"
class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${LOG_FILE}</file>
<encoder>
<pattern>${FILE_LOG_PATTERN}</pattern>
</encoder>
<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
<fileNamePattern>${LOG_FILE}.%d{yyyy-MM-dd}.%i.log</fileNamePattern>
<maxHistory>7</maxHistory>
<maxFileSize>50MB</maxFileSize>
<totalSizeCap>2GB</totalSizeCap>
</rollingPolicy>
</appender>
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>${CONSOLE_LOG_PATTERN}</pattern>
<charset>utf8</charset>
</encoder>
</appender>
<root level="info">
<appender-ref ref="CONSOLE" />
<appender-ref ref="APPLICATION" />
</root>
</configuration>
```
只说几个不同的点。
`FILE_LOG_PATTERN``CONSOLE_LOG_PATTERN` 是在 Spring Boot 中默认定义的。
>[https://github.com/spring-projects/spring-boot/blob/main/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/logback/DefaultLogbackConfiguration.java](https://github.com/spring-projects/spring-boot/blob/main/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/logback/DefaultLogbackConfiguration.java)
![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/springboot/logback-87217069-b756-4c0c-945e-06ecc5785b81.png)
SizeAndTimeBasedRollingPolicy 比 TimeBasedRollingPolicy 多了一个日志文件大小设定的属性:maxFileSize,其他完全一样。
totalSizeCap,所有日志文件的大小(可选项)。超出这个大小时,旧的日志文件将会被异步删除。需要配合 maxHistory 属性一起使用,并且是第二条件。
在Intellij IDEA 中启动项目,我们来查看一下配置后的日志效果(控制台中)。
![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/springboot/logback-1a849206-617e-45d5-b199-50787c12e9bc.png)
由于我们加了颜色配置,所以控制台日志看起来对眼睛更友好了一些。
那配置的日志文件在哪里呢?在 `user.home` 下,如果不确定具体值是什么的话,可以通过 `System.getProperty("user.home")` 获取到。
![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/springboot/logback-bdb8558e-2fd0-488e-9a0a-7c78234eae7a.png)
顺着这个路径就可以找到生成的日志文件了,并且日志的生成策略也是符合我们的预期的。
![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/springboot/logback-3e5c5534-470b-4ec4-b5fa-cb2a6fbbaee4.png)
### 使用 lombok 进行日志记录
在上面的例子中,我们必须在类中声明一个静态的 Logger 对象才能在需要记录日志的地方使用它。
```java
static Logger logger = LoggerFactory.getLogger(CodingmoreLogbackApplicationTests.class);
```
这样的样板代码令人生厌!
我们可以通过 lombok 的方式来解决这个问题,在 pom.xml 文件中加入依赖。
```
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
```
然后在类上加上 `@Slf4j` 注解,之后就可以直接使用 log(添加`@Slf4j` 注解后会自动添加一个 log 字段)来记录日志了。
```
@SpringBootTest
@Slf4j
class CodingmoreLogbackApplicationTests {
@Test
void testSlf4j() {
log.info("沉默王二是个大煞笔");
}
}
```
运行该测试方法,可以在控制台看到如下日志:
![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/springboot/logback-2456652a-63f5-4499-8133-ed9c8a68e0e7.png)
日志文件也有:
![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/springboot/logback-f1baaaa3-e6c4-4abb-8cc7-9862e90f960a.png)
查看字节码反编译后的文件就明白了。lombok 在编译的时候会自动添加一行代码:
```java
private static final Logger log = LoggerFactory.getLogger(CodingmoreLogbackApplicationTests.class);
```
![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/springboot/logback-e5eb93b2-906c-4fbf-8fe4-6e6a3c727625.png)
### logback 中文手册
GitHub 上有一份 Logback 的中文手册,总共有 150 多页,主要包含以下基本的和高级的特性:
- logback 的整体架构
- 讨论 logback 最好的实践以及反模式
- logback 的 xml 配置方式
- appender
- encoder
- layout
- filter
- 上下文诊断
- Joran - logback 的配置系统
>[https://github.com/YLongo/logback-chinese-manual](https://github.com/YLongo/logback-chinese-manual)
![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/springboot/logback-cd491159-e48e-4c74-a67f-7962a45e847f.png)
### 源码路径
> - 编程喵:[https://github.com/itwanger/coding-more](https://github.com/itwanger/coding-more)
> - Logback 详细配置专用:[https://github.com/itwanger/coding-more](https://github.com/itwanger/codingmore-learning/tree/main/codingmore-logback)
![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/xingbiaogongzhonghao.png)
---
category:
- Java企业级开发
tag:
- Spring Boot
title: Spring Boot 整合 Thymeleaf 模板引擎
---
### 关于 Thymeleaf
Thymeleaf 是一个优秀的、面向 Java 的 HTML 页面模板,具有丰富的标签语言和函数。在 JSP 被淘汰之后,Thymeleaf 取而代之成为了 Spring Boot 推荐的模板引擎。
![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/springboot/thymeleaf-d373bf02-a577-4382-89b4-0b29a87ab922.png)
Thymeleaf 在有网和没网的环境下都可以正常工作,既能让美工在浏览器中查看页面的静态效果,也能让程序员在服务器查看带数据的动态页面效果。
这是因为 Thymeleaf 支持 HTML 原型,在 HTML 标签里增加额外的属性来达到模板+数据的展示方式。
浏览器在解释 HTML 的时候会忽略未定义的标签属性,所以 Thymeleaf 可以静态地运行;当有数据返回页面时,Thymeleaf 标签会动态地替换静态内容。
下面列举一些 Thymeleaf 常用的表达式、标签和函数。
1)常用表达式
- `${...}`变量表达式
- `*{...}`选择表达式
- `#{...}`文字表达式
- `@{...}`URL 表达式
- `#maps` 对象表达式
2)常用标签
- th:action 定义服务器端控制器路径。
- th:each 循环语句
- th:field 表单字段
- th:href URL 链接
- th:id div 标签中的 ID
- th:if 条件判断
- th:include 引入文件
- th:fragment 定义代码片段
- th:object 替换对象
- th:src 图片地址
- th:text 文本
- th:value 属性值
3)常用函数
- `#dates` 日期函数
- `#lists` 列表函数
- `#arrays` 数组函数
- `#strings` 字符串函数
- `#numbers` 数字函数
- `#calendars` 日历函数
- `#objects` 对象函数
- `#bools` 布尔函数
想要查看更多 Thymeleaf 表达式、标签、函数等内容,可以到 Thymeleaf 官网:
>[https://www.thymeleaf.org/](https://www.thymeleaf.org/)
### 整合 Thymeleaf
第一步,在 pom.xml 文件中添加 Thymeleaf 的 stater
```
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
```
第二步,在 application.yml 文件中添加 Thymeleaf 的配置
```
spring:
thymeleaf:
cache: false # 开发时关闭缓存,不然看不到实时页面
```
其他配置项采用默认就可以了,想要看有哪些默认项的话,可以全局打开 ThymeleafProperties.java 类。
![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/springboot/thymeleaf-2e0cba5c-89ae-4f1b-8cc8-0c8f86d5f520.png)
Thymeleaf 模板引擎默认会读取 resources 目录下的 templates 目录,这个目录是用来存放 HTML 页面的。
第三步,新建 UserController.java 控制器。
```java
@Controller
@RequestMapping("/user")
public class UserController {
@Resource
private UserService userService;
@RequestMapping("/all")
public String all(Model model) {
model.addAttribute("users", userService.findAll());
return "all";
}
}
```
- @Controller 注解表示该类为一个控制器类。
- @RequestMapping 注解用来处理请求地址映射,可用于类或者方法。
- Model 接口可以承载数据库里查到的数据,前端可以从 model 中取出来。
第四步,在 resources/templates 目录下新建 all.html 文件(文件名对应控制器中 all 方法返回的字符串).
```
<!DOCTYPE html>
<html lang="zh" xmlns:th="http://www.thymeleaf.org">
<head>
<meta http-equiv="content-type" content="text/html;charset=utf-8">
<title>Thymeleaf</title>
</head>
<body>
<table>
<tr>
<td>用户名</td>
<td>密码</td>
</tr>
<tr th:each="user:${users}">
<td th:text="${user.name}"></td>
<td th:text="${user.password}"></td>
</tr>
</table>
</body>
</html>
```
`<html lang="zh" xmlns:th="http://www.thymeleaf.org">` 为 Thymeleaf 的命名空间,通过引入命名空间就可以在 HTML 文件中使用 Thymeleaf 标签语言,用关键字 “th”来标注。
第五步,启动主类,如果看到以下信息表示启动成功。
![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/springboot/thymeleaf-3e636801-32df-4591-9159-fe83f771f68d.png)
第六步,在浏览器地址栏里输入 `http://localhost:8080/user/all` 访问接口。
![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/springboot/thymeleaf-e4b658fd-e30a-4b00-8818-ab00f8a28620.png)
### HTTP Client
Intellij IDEA 中提供了一个非常轻量级的 HTTP 客户端,可用来进行简单的测试。通过 tools 菜单栏选中 HTTP Client 创建一个 HTTP 请求。
![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/springboot/thymeleaf-33cdf614-84d1-40eb-b78d-be57843a3b95.png)
按照格式填写完请求的 URL 后,就点击 Run all requests 就可以在 services 面板中查看到页面输出信息了。
![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/springboot/thymeleaf-28a6bf83-42ef-4f07-9a3c-996b8a222b4a.png)
### 源码地址
> - 编程喵:[https://github.com/itwanger/coding-more](https://github.com/itwanger/coding-more)
> - [https://github.com/itwanger/codingmore-learning](https://github.com/itwanger/codingmore-learning/tree/main/codingmore-thymeleaf)
![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/xingbiaogongzhonghao.png)
\ No newline at end of file
此差异已折叠。
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册