0%

入职了新公司后,我接触到了 Yapi 这样一款强大的接口管理平台,但是相比于 Swagger,接口的创建和信息的更新在 Yapi 上显得要麻烦许多,还好在 IDEA 上有 EasyYapi 这样一款基于 Java Doc 注释生成接口信息的插件,让 Yapi 管理项目接口文档这一过程高效了很多。

安装插件

我们在 IDEA 上安装 EasyYapi 插件,可以直接搜索名字进行下载,也可以通过链接下载 EasyYapi - IntelliJ IDEs Plugin | Marketplace

导出 API

在安装完插件之后,就可以按步骤进行接口信息导出了。
首先,在项目目录中选定接口所在的文件夹,然后使用快捷键alt shift E(windows)/ctrl E(mac) , 然后选择要导出的接口,点击绿色的 ☑️ 进行导出

如果是初次使用,在导出期间需要按照提示输入 Yapi 服务的地址,以及 Yapi 项目的 token。其中项目的 token 在如下路径可以找到
看到终端出现如下输出,说明导出成功
点击链接,可以看到接口信息已经同步到 Yapi 的项目中

进阶

EasyYapi 是通过解析 Java Doc 注释来生成接口信息,如果按照 Java Doc 格式编写注释就可以在 Yapi 中补充更多的接口信息

类注释

1
2
3
4
5
6
7
8
9
10
11
/**
* 设备控制器
* 对设备进行控制和管理
*
* @author XZN1WX
* @date 2022/05/30
*/
@RestController
@RequestMapping("/api/device")
public class DeviceController {
}

注释的第一行为分类名称,第二行为分类注释

方法注释

1
2
3
4
5
6
7
8
9
10
11
/**
* 添加
* 添加一台新的设备
*
* @param input 输入
* @return {@link String}
*/
@PostMapping()
public Output add(@ModelAttribute Input input) {
return new Output();
}

注释第一行是接口名,第二行是接口注释

属性注释

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class Output {

/**
* 参数1
*/
private String var1;

/**
* 参数2
*/
private Integer var2;

/**
* 参数3
*/
private Date var3;

}

属性注释中的内容对应了返回数据的备注信息

Java Doc

为了更加便捷地生成 Java Doc,可以安装 Easy JavaDoc 插件:IDEA插件系列(29):Easy Javadoc插件–快速生成javadoc文档注释_二木成林的博客-CSDN博客_idea javadoc插件

参考

  1. SpringBoot使用EasyYapi对代码0侵入实现API接口一键发布到YApi - 第422篇
  2. SpringBoot使用EasyYapi对代码0侵入实现API接口一键发布到YApi的进阶使用 - 第423篇_悟纤的博客-CSDN博客_easyyapi

原因

实际原因出在我写的业务方法,在最外层没有包裹一个 try/catch 语句块,导致线程执行业务方法时内部抛出了未捕获的异常,但是线程本身没有写一个默认的异常处理方法,最终 JVM 的默认处理方式是将这个异常以及堆栈信息只会被打印到控制台,而不是写到日志中。

原理分析

  • 当一个线程因未捕获的异常而即将终止时,JVM 将使用 Thread.getUncaughtExceptionHandler() 查询该线程以获得其 UncaughtExceptionHandler
  • 调用该 handler 的 uncaughtException() 方法,将线程和异常作为参数传递。
    • 如果线程没有实现 uncaughtException() 方法,则搜索该线程的 ThreadGroup 的异常处理器。
    • ThreadGroup 中的默认异常处理器实现是将处理工作逐层委托给上层的 ThreadGroup,直到某个 ThreadGroup 的异常处理器能够处理该异常,否则一直传递到顶层的ThreadGroup。
    • 顶层 ThreadGroup 的异常处理器委托给默认的系统处理器(如果默认的处理器存在,默认情况下为空),否则把栈信息输出到 System.err,即输出到控制台

execute() 和 submit() 遇到未捕获异常

  • 如果使用 execute() 方法执行任务,在线程内部,对于未捕获的异常,并不会像在主线程中一样打印错误日志。线程 UncaughtExceptionHandler 接口中有一个 uncaughtException() 方法,如果没有实现该处理方法,JVM 最后的默认策略是使用 System.err.print("Exception in thread \"" + t.getName() + "\" ") 输出日志到控制台,但是不会写到项目的日志文件中。如果在线程外加上 try/catch 语句也不能捕获到线程内部的异常,因为实际上线程内部的异常最后已经被 JVM 处理了,实际有效的方法是在线程所执行的业务方法外部加一层 try/catch,或者实现 uncaughtException() 方法打印日志,来保证业务方法中抛出的异常最终能被我们捕获到并在日志中被看见。
  • 如果使用 submit() 方法来执行任务,内部抛出异常不做任何处理,异常既不会打印到日志中,也不会输出到控制台,看起来就好像异常被线程吞了。实际上异常信息被存储到了线程的结果信息中,通过 Future#get 方法就能获取到内部抛出的异常,然后对异常进行捕获和处理。

总结

  • 出现异常的线程会被线程池移除,线程池会新建一个线程放入
  • 在线程因未捕获的异常而面临死亡时会调用 Thread.UncaughtExceptionHandler.uncaughtException() 方法,默认会将异常信息打印到控制台
  • execute() 内部出现未捕获异常时,默认只会输出错误信息到控制台,不会出现在日志中
  • submit() 方法内部出现未捕获异常时,默认控制台和日志都不会有相关信息,需要通过 Future#get 方法来获取异常信息
  • 使用 exectue() 方法执行任务时,推荐挑选其中一种处理方式
    • 业务代码最外层加上 try/catch 语句块,输出错误日志,便于问题排查
    • 实现 UncaughtExceptionHandler 接口并为线程重写一个默认的 uncaughtException 方法,在 uncaughtException 方法中打印日志

参考

【Java技术指南】「技术盲区」看看线程以及线程池的异常处理机制都有哪些?

Kafka

组件

  • Producer:生产者,负责生产消息并发送到 kafka。
  • Consumer:消费者,连接到 kafka 上并接收消息
  • Consumer group:消费者组,包含多个消费者实例。不同 group 下的消费者可以重复消费数据,同一 group 下的消费者,消息只能被其中一个消费者消费。
  • Broker:kafka 的服务节点,即 kafka 的服务器
  • Topic:kafka 中的消息按照 topic 进行区分,生产者将消息发送到特定 topic,消费者订阅 topic 的消息并进行消费
  • Partition:一个 topic 可被分为多个 partition,同一个主题下不同 partition 包含的信息是不同的。(存储层面上可看作一个可追加的 log 文件,消息被追加到文件后会被分配一个 offset 偏移量)
  • Offset:消息在分区中的唯一标识,不跨分区。每个 consumer 自己管理 offset,控制消费的信息
  • Replication:副本,kafka 中一个 partition 会在多 broker 上存在多个副本,只有主副本提供对外读写,从副本实时同步主副本的数据。主副本所在 broker 出现故障时,controller 会选举新的主副本对外提供读写服务,保证高可用。
  • Record:实际写入 kafka 的消息记录,每个 record 包含 key,value 和 timestamp

Zookeeper

Kafka 时使用 Zk 构建的分布式系统,Zk 负责了

  • Kafka Controller 的 Leader 选举
  • Kafka 集群成员管理
  • Topic 配置管理
  • 分区副本管理

Controller

从 broker 中选举出来,负责分区 leader 和 follower 的管理
选举过程:
Broker 启动的时候尝试去读取 /controller 节点的 brokerid 的值,如果 brokerid 的值不等于-1,则表明已经有其他的 Broker 成功成为 Controller 节点,当前 Broker 主动放弃竞选;如果不存在 /controller 节点,或者 brokerid 数值异常,当前 Broker 尝试去创建 /controller 这个节点,此时也有可能其他 broker 同时去尝试创建这个节点,只有创建成功的那个 broker 才会成为控制器,而创建失败的 broker 则表示竞选失败。每个 broker 都会在内存中保存当前控制器的 brokerid 值,这个值可以标识为 activeControllerId。
实现:
Controller 内部也采用生产者-消费者实现模式,Controller 将 zookeeper 的变动通过事件的方式发送给事件队列,队列就是一个LinkedBlockingQueue,事件消费者线程组消费事件,将相应的事件同步到各 Broker 节点。这种队列 FIFO 的模式保证了消息的有序性。
职责:

  1. 处理 broker 节点的上线、下线,更新集群元数据,并将集群变化通知到所有 broker 节点
  2. 负责分区副本的分配工作,主导 topic 分区主副本的选取,创建 topic 或者 topic 扩容分区
  3. 管理分区和副本的状态机

分区状态机

PartitionStateChange,管理 Topic 的分区,它有以下 4 种状态:

  1. NonExistentPartition:该状态表示分区没有被创建过或创建后被删除了。
  2. NewPartition:分区刚创建后,处于这个状态。此状态下分区已经分配了副本,但是还没有选举 leader,也没有 ISR 列表。
  3. OnlinePartition:一旦这个分区的 leader 被选举出来,将处于这个状态。
  4. OfflinePartition:当分区的 leader 宕机,转移到这个状态。

副本状态机

  1. NewReplica: 创建 topic 和分区分配后创建 replicas,此时,replica 只能获取到成为 follower 状态变化请求。
  2. OnlineReplica: 当 replica 成为 parition 的 assingned replicas 时,其状态变为 OnlineReplica, 即一个有效的 OnlineReplica。
  3. OfflineReplica: 当一个 replica 下线,进入此状态,这一般发生在 broker 宕机的情况下;
  4. NonExistentReplica: Replica 成功删除后,replica 进入 NonExistentReplica 状态。

Kafka 性能

IO
Kafka 文件采取顺序写的方式,减少了磁盘寻道和旋转的次数,减少了磁盘 IO 的时间
零拷贝
传统数据从磁盘到网卡需要经过四次拷贝和两次系统调用:

  1. 操作系统从磁盘读取数据到内核空间的 pagecache
  2. 应用程序读取内核空间的数据到用户空间的缓冲区
  3. 应用程序将数据(用户空间的缓冲区)写回内核空间到套接字缓冲区(内核空间)
  4. 操作系统将数据从套接字缓冲区(内核空间)复制到通过网络发送的 NIC 缓冲区(网卡缓冲区)
    零拷贝省去中间步骤,只用将磁盘文件的数据复制到页面缓存中一次,然后将数据从页面缓存直接发送到网络中(发送给不同的订阅者时,都可以使用同一个页面缓存),避免了重复复制操作。
    如果有10个消费者,传统方式下,数据复制次数为4*10=40次, 而使用“零拷贝技术”只需要1+10=11次,一次为从磁盘复制到页面缓存, 10次表示10个消费者各自读取一次页面缓存。
    生产者生产的消息会先写入 page cache,之后会写入到磁盘。leader 和 follower 的同步,与 consumer 消费信息的原理是一样的。因此如果 Kafka producer 的生产速率与 consumer 的消费速率相差不大,那么就能几乎只靠对 broker page cache 的读写完成整个生产 - 消费过程,磁盘访问非常少。

Kafka 的 Consumer 客户端是线程不安全的,在 Consumer 客户端采用 Reactor 线程模型可以保证线程安全,提升消费性能

参考

一文读懂Kafka零拷贝_gaofeng的博客-CSDN博客
Kafka性能篇:为何Kafka这么"快"?

2021年12月10日,Log4j2 被爆出存在严重安全漏洞,攻击者可以通过 JNDI 注入的方式远程在目标机器上执行任意代码来发动攻击,一种场景就是,如果有一个接口会将入参用log4j2打印出来,攻击者就可以传入形如${jndi:rmi//xxxxxx}的命令,在目标机器上执行远程代码。

在排查最近在开发的一个 Java 项目是否存在该漏洞时,发现这个项目打印的日志并不会触发漏洞,但是项目中又确实存在 log4j2 的依赖。仔细看了一下项目的日志方法,原来这个项目使用的日志框架是 logback 而非 log4j2。这也刚好勾起了我的疑问,平时在使用日志时知道有 slf4j,log4j 这类字眼,logback 很少有见到,对于日志这一块,也是只需要能打印出来供线上问题排查即可。那么 Java 中的日志框架组成是什么样的,又有哪些日志框架呢?

日志框架

Java 日志框架采用门面设计模式,主要由日志门面、日志适配器和日志库三大部分构成,其中:

  • 日志门面:本身不负责日志功能的实现,只是约定了一套接口规范,让使用者无需关注底层日志库以及使用细节。目前最广泛使用的日志门面是 slf4j 和 commons-logging
  • 日志库:具体实现日志的相关功能,目前主流的三个日志库分别是 log2j、log-jdk 和 logback
  • 日志适配器:主要完成日志库到日志门面的适配,例如 log4j 没有实现 slf4j 接口,所以如果要实现 log4j + slf4j 的模式,就需要额外引入适配器来解决接口不兼容的问题

Slf4j

很长一段时间,我都以为 slf4j 是和 log4j 一样的一种日志框架,但其实它的全称(Simple Logging Facade for Java)告诉我们,这是专门针对 Java 提供的一种简单日志门面。它本身不提供日志服务,而是统一了接口,供其他日志框架来接入。优点就是,如果后续项目升级时需要更换日志框架,省去了对代码的改动,只需要更换日志框架的 jar 包即可。

Log4j

Log4j 一款比较早的日志框架,log4j2 是在此基础上的升级版,有更强大的性能。两者在获取 Logger 的路径上有差异,配置方式也有区别,log4j 本身没有实现 slf4j 接口,如果想要适配,需要引入额外的 jar 包。

Logback

Logback 是 Log4j 的升级版,由 Slf4j 的作者开发,使用 xml 进行配置。该框架默认实现了 Slf4j 标准,所使用的 Logger 和 LoggerFactory 都是 Slf4j 的实现,使用 slf4j + logback 的组合不需要引入额外的 jar 包。

参考

【安全通报】Apache Log4j2 远程代码执行漏洞
log4j、log4j2、slf4j、logback什么关系?到底该使用哪些jar??
Slf4j与log4j及log4j2的关系及使用方法

Thrift IDL

Thrift 是一个 RPC 框架,是典型的 C/S 架构,采用 IDL 来定义接口

基本数据类型

  • bool: 布尔值 true or false
  • byte: 有符号字节
  • i16: 16位有符号整数
  • i32: 32位有符号整数
  • i64: 64位有符号整数
  • double: 64位浮点数
  • string: 编码无关字符串
  • void: 返回为空

复合类型

struct
相当于 C 语言中的结构体和面向对象思想中的类,将多种数据类型整合起来

1
2
3
4
5
6
7
8
struct User {
1: i32 id,
2: string user_name,
3: required bool is_admin,
4: optional string nick_name,
5: optional string sex = "male",
20: optional string city,
}

注:

  • optional 表示该字段可选
  • requeired 表示该字段必填
  • 序号不可以重复,但不是必须连续
    enum
    枚举类型
1
2
3
4
5
enum UserType {
BLOCKED = 0,
NORMAL = 1,
ADMIN = 2,
}

容器

Thrift 提供了三种容器类型,list、map 和 set

  • list 相当于 Java 中的 ArrayList
  • map<T1, T2> 相当于 Java 中的 HashMap
  • set 相当于 Java 中的 HashSet

异常

Thrift 支持自定义异常

1
2
3
exception NotFound {
1: string message,
}

注:编写服务端代码时,除了在代码中抛出异常,还要在定义接口时,指明该接口可能抛出的异常

Service

类似于 Java 中的 interface

1
2
3
4
5
service UserService {
User query_user(1: i32 user_id) throws (
1: NotFound not_found),
User add_user(1: User user);
}

命名空间

1
namespace [java/cpp/py] com.example.project

参考

Thrift 教程

两台机器之间同步文件常用的方式有 scprsync 两种,两者的主要区别在于:

  • scp 会将整个文件进行传输;rsync 只对差异文件做更新(通过比较最后修改时间和文件的大小),支持分块传输,可以只传输修改的部分
  • scp 是加密传输;rsync 不是加密传输

在项目中我使用的是 rsync --daemon 的方式进行文件同步,这种方式需要在目标同步机器上的 rsyncd.conf 配置文件中配置模块名,存储路径等信息。具体配置方法参考:参考

我的测试用例中使用的配置项是

1
2
3
4
5
6
7
8
9
10
11
uid = 0 
gid = 0
pid file = /var/run/rsyncd.pid
log file = /var/log/rsyncd.log

[user]
path=/data
ignore errors = yes # 忽略IO问题
read only = no
use chroot = no # 连接时可以不需要root权限
list = yes #当客户请求列出可以使用的模块列表时,该模块可以被列出

模块名称为 user ,文件的默认保存路径为 /data
服务器通过 rsync --daemon 启动守护进程

在另一台机器上通过指令

1
rsync -avz [local_path] [server_address]::user

可以将local_path目录下的文件增量写入到目标服务器的/data目录下

参考

【Linux】rsync 守护进程的配置
SCP和Rsync远程拷贝的几个技巧

最近有一个新项目需要写一个定时任务,因为没有涉及到复杂的功能需求,所以考虑仅使用 Spring 自带的 @Scheduled 注解实现

@Scheduled

基于注解实现定时任务的方式很简单,只需要在启动类前加上 @EnableScheduling 注解

1
2
3
4
5
6
@EnableScheduling
public class DemoApplication {
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
}

然后在需要执行定时任务的方法前加上 @Scheduled() 注解,然后在括号内添加规则即可

1
2
3
4
5
6
7
8
9
每隔5秒执行一次:*/5 * * * * ?
每隔1分钟执行一次:0 */1 * * * ?
每天23点执行一次:0 0 23 * * ?
每天凌晨1点执行一次:0 0 1 * * ?
每月1号凌晨1点执行一次:0 0 1 1 * ?
每天的0点、13点、18点、21点都执行一次:0 0 0,13,18,21 * * ?

@Scheduled(cron = "0 0 2 * * ?")
public void func(){}

需要注意的是,通过 @Scheduled 实现的定时任务都是在一个线程中执行的,如果出现下一个定时任务开始时前一个还没有执行完的情况,下一个定时任务会阻塞直到前面的定时任务执行完。
此外, @Scheduled 不支持分布式的情况,如果部署到集群上,多台机器都会执行定时任务。所以如果在分布式的情况下,需要通过分布式锁来保证只有一台机器执行定时任务。

@Retryable

考虑到定时任务可能有执行失败的情况,执行失败后需要有重试机制。重试的最简单执行方式就是代码层面使用循环,为了保证业务逻辑的整洁,考虑在框架层面实现重试功能,所以选择使用 @Retryable 注解。
首先需要添加相关依赖

1
2
3
4
5
6
7
8
<dependency>
<groupId>org.springframework.retry</groupId>
<artifactId>spring-retry</artifactId>
</dependency>
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
</dependency>

然后在项目启动类前添加 @EnableRetry 注解

1
2
3
4
5
6
7
@EnableScheduling
@EnableRetry
public class DemoApplication {
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
}

然后在方法前加上 @Retryable 注解声明重试条件,当方法抛出对应异常后会触发重试

1
2
3
4
5
6
7
8
9
@Retryable参数的意思说明
value:抛出指定异常才会重试
include:和value一样,默认为空,当exclude也为空时,默认所以异常
exclude:指定不处理的异常
maxAttempts:最大重试次数,默认3
backoff:重试等待策略,默认使用@Backoff@Backoff的value默认为1000L,我们设置为2000L;multiplier(指定延迟倍数)默认为0,表示固定暂停1秒后进行重试,如果把multiplier设置为1.5,则第一次重试为2秒,第二次为3秒,第三次为4.5秒。

@Retryable(value = Exception.class, maxAttempts = 3, backoff = @Backoff(delay = 2000L, multiplier = 1.5))
public void func(){}

如果达到最大重试次数依然抛出异常,可以通过 @Recover 注解来配置回调方法,可以基于此实现报警等功能

1
2
@Recover
public void alert(){}

注意:重试方法和回调方法需要写在同一个 Java 文件中

TODO

基于分布式锁让 @Schedule 兼容分布式
基于线程池让定时任务并发执行

参考

011 @Retryable的使用 - 曹军 - 博客园

高性能的5大设计思想

扩展

垂直扩展-针对单台机器

  • 扩容机器,增加配置
  • 分表,减少数据量
  • 无锁,减少锁竞争

水平扩展-多机器均衡

  • nginx 负载均衡
  • 无状态
  • 分布式
缓存

数据前置,开辟新的数据交换区,解决原始数据获取代价太大的问题

常用缓存策略:CDN 静态数据、热点数据缓存、服务缓存 Redis、内存缓存、数据库缓存、分布式缓存

缓存不一致:捕捉更新结果,如果失败就重试;

分离

动静分离、读写分离、主从主备、冷热分离

异步

消息队列,削峰填谷,解耦

复用

线程池、连接池复用,减少资源创建销毁的时间

高可用的5大设计思想

隔离
  • 核心/非核心服务隔离部署
  • 机房隔离
  • 读写隔离
限流

识别恶意流量

  • 接入层限流:nginx
  • 应用层限流:队列满后丢弃后续数据
  • 分布式限流
降级
  • 有损服务:系统繁忙页面
  • 熔断
  • 保底:出现异常时的保底策略
超时和失败重试
可回滚

事务、版本、数据的可回滚,保证及时解决数据问题

*保障手段

压测、应急预案演练(多机房容灾等)、线上监控报警

磁盘 I/O

读取和写入文件 I/O 操作都需要调用操作系统的接口。因为磁盘是由操作系统来管理的,应用程序想要访问物理设备就只能通过系统调用的方式来工作
在这期间内核空间和用户空间是隔离的,所以存在数据在两处来回复制的问题
操作系统会引入缓存来加速 I/O 操作

标准访问文件方式

磁盘-高速缓存-应用程序缓存-应用程序

直接 I/O 方式

磁盘-应用程序缓存-应用程序 (例如数据库系统明确知道需要缓存、失效哪些数据)

编码相关

尽量将所有编码都设置为 UTF-8,是理想的中文编码方式
UTF-8 对单字节范围内的字符采用1个字节表示,对汉字采用3个字节表示

JVM内存分析

内存泄漏

发生内存泄露时,首先查看 Heap 的内存使用情况,查看 Major GC,Full GC 的情况,是否有对象一直没有被回收
除此之外,内存泄露还可能出现在其他需要用到内存的区域,包括 JVM 本身的 JIT 编译,JVM 栈,JNI 调用本地代码,NIO 用 Direct Buffer 申请内存等

Session 与 Cookie 都是为了保持用户与后端服务器的交互状态
缺点:随着 Cookie 数和访问量的增加,占用的网络带宽会增加,浏览器存储的 Cookie 也有限;Session 不容易在多台服务器之间共享

分布式 Session

有一台服务订阅服务器,为应用提供 Session 和Cookie 的配置项,精确控制哪些应用可以操作哪些 Session 和 Cookie,可以有效控制 Session 的安全性和 Cookie 的数量,简化 Cookie 的管理
应用 订阅 服务订阅服务器 使用的 Session 配置项, Session 存储到分布式缓存中

多域名的 Session 同步

需要一个跳转应用,这个应用可以被一个或者多个域名访问。这个应用从一个域名下取得 sessionID,然后将这个 sessionID 同步到另一个域名下
所以要实现多域名的 Session 同步,就要将同一个 sessionID 作为 Cookie 写到多个域名下

ClassLoader

有一个 defineClass 方法,可以将接收到的字节流解析成 JVM 能识别的 Class 对象
所以在 Java 中可以通过 1. class 文件实例化对象 2. 通过类的字节码流创建类的 Class 对象,然后进行实例化

JVM 平台的 ClassLoader 共分 3 层,BootstarpClassLoader,ExtClassLoader,AppClassLoader
其中

  • BootstarpClassLoader 完全 JVM 自己控制,不能被别的类访问到。它也不是 ExtClassLoader 的父类
  • ExtClassLoader 和 AppClassLoader 是 Launcher 的内部类,都继承了 URLClassLoader,URLClassLoader 实现了 ClassLoader 抽象类
  • 创建 Launcher 对象会先创建 ExtClassLoader 对象,然后其作为父类加载器创建 AppClassLoader 对抗。最后由 Launcher 对象获取到的加载器是 AppClassLoader 对象

Tomcat

总体架构

  • Server:Server 对应的就是一个 Tomcat 实例
  • Service:一个 Service 由一个 Container 和多个 Connector 构成。Service 默认只有一个,但一个 Server 可对应多个 Service 服务。
  • Connector:一个 Service 可以有多个 Connector 来实现支持多种 I/O 模型和应用层协议
  • Container:一个 Container 对应多个 Connector,顶层容器其实就是 Engine

Connecter 负责对外交流,Container 负责处理 Connector 接受的请求

Connector

Connector 对 Servlet 容器屏蔽了网络协议和 I/O 模型的区别,无论是什么协议,在容器中获取到的都是一个 ServletRequest 对象
连接器需要完成 3 个高聚合功能:

  • 网络通信
  • 应用层协议解析
  • Tomcat Request/Response 与 ServletRequest/ServletResponse 的转化
    与此对应的三个组件分别为 EndPoint、Processor 和 Adapter
  • Endpoint 负责底层 Socket 通信,提供字节流给 Processor
  • Processor 负责应用层协议解析,提供 Tomcat Request 对象给 Adapter
  • Adapter 负责调用容器,提供 ServletRequest 对象给容器
    其中 Endpoint 和 Processor 一起抽象成了 ProtocolHandler,实现封装通信协议和 I/O 模型的差异

Container

加载并管理 Servlet,处理具体的 Request 请求
由 Engine、Host、Context 和 Wrapper 4个子容器组件构成,4个组件是父子关系:Engine 包含 Host,Host 包含 Context,Context 包含 Wrapper

用到的设计模式

  • 组合模式管理容器(俄罗斯套娃)
  • 观察者模式发布启动事件达到解耦、开闭原则
  • 骨架抽象类和模版方法抽象变与不变,实现代码复用和灵活扩展
  • 责任链模式(Pipeline-Valve)实现一个请求处理过程中有很多处理者一次对请求进行处理

Tomcat 的类加载器

对于 Tomcat 来说,它的类加载器是 StandardClassLoader,但它只是一个代理类,没有覆盖 loadClass() 方法,所以按照双亲委派机制,它会调用父类加载器去加载,所以加载 Tomcat 本身的依然是 AppClassLoader
Tomcat的自定义类加载器 WebappClassLoader 覆盖了 loadClass() 方法,打破了双亲委托机制,有自己的加载机制

  • 先在本地 cache 查找是否加载过该类,即查看 Tomcat 的类加载器是否已经加载过这个类
  • 如果 Tomcat 没有加载过这个类,就调用 findLoadedClass() 从系统类加载器的 cache 中查找是否已经加载过
  • 如果都没有,尝试用 ExtClassLoader 类加载器加载。这一步目的是防止 Web 应用自己的类覆盖 JRE 的核心类,因为 ExtClassLoader 会委托给 BootstrapClassLoader 去加载
  • 如果 ExtClassLoader 加载器加载失败,说明 JRE 核心类中没有该类,就在本地 Web 应用目录下查找并加载
  • 如果本地目录下没有该类,说明不是 Web 应用自己定义的类,交给系统类加载器 AppClassLoader 加载
  • 如果加载过程全部失败,抛出 ClassNotFound 异常

Servlet

在 Tomcat 的容器等级中,由 Context 容器直接管理 Servlet 在容器中的包装类 Wrapper

为什么要将 Servlet 包装成 StandardWrapper 而不包装成 Servlet 对象? - 为了不将 Servlet 强耦合在Tomcat中。StandardWrapper 是 Tomcat 容器的一部分,具有容器的特征。

Servlet 体系结构

Servlet 中主要包含四个类,其中

  • ServletConfig 获取 Servlet 的一些配置属性
  • ServletContext 负责描述 Servlet 运行的交易场景,这个交易场景是用来让两个模块进行数据交换
  • ServletRequest 和 ServletResponse 是交互的具体对象

Servlet 工作方式

用户从浏览器发出一个请求,其中带有 hostname port 和 URL
hostname port 用来建立 TCP 连接,服务器根据 URL 来到达正确的 Servlet 容器
这个映射关系由 Mapper 组件来完成,这个类保存了 Tomcat 的 Container 中所有子容器的信息。在 Request 类进入 Container 之前,mapper 就已经确定了它要访问的子容器 (有一个 MapperListener 类会作为监听者被添加到 Container 的所有子容器中,容器的变化会反应到对应 MapperListener 的 mapper 属性的修改中)

容器的先后顺序为:Engine -> Host -> Context -> Wrapper 找到对应的 Servlet 后执行 Servlet 的 service 方法 **门面设计模式** Servlet 中的 StandardWrapperFacade、ApplicationContextFacade 都是门面对象。门面设计模式起到对数据的封装作用,别的系统通过这个门面来访问数据。

Spring

三大核心组件是 Bean、Core 和 Context

  • Bean 包装的是 Object 对象,这些对象的依赖关系由 IoC 容器来统一管理
  • Context 本身是 Bean 之间关系的集合,充当 IoC 容器的角色。Bean 之间的关系主要是依赖注入的注入关系
  • Core 是发现、建立和维护 Bean 之间关系所需要的一系列工具

参考

Tomcat 架构原理解析到架构设计借鉴

问题1

在配置文件中 datasource 的数据源名称设置为 yudao_orders_0 时出现如下错误

1
Caused by: org.springframework.boot.context.properties.source.InvalidConfigurationPropertyNameException: Configuration property name 'spring.shardingsphere.datasource.yudao_orders_0' is not valid

原因是配置文件中的 datasource 的命名中不能带下划线,改为 yudao-orders-0 即可

问题2

在 application.yaml 中配置 type 字段,即连接池类型时,如果选择的是 Druid,要注意 maven 依赖
pom.xml 文件中 druid-spring-boot-starter 和 sharding-jdbc-spring-boot-starter 不能同时出现

  1. 因为数据连接池的starter(比如druid)可能会先加载并且其创建一个默认数据源,这将会使得ShardingSphere-JDBC创建数据源时发生冲突。
    解决方法有2种:
  2. 在启动类中排除 Druid 自动配置类@SpringBootApplication(exclude = {DruidDataSourceAutoConfigure.class})
  3. 将 druid-spring-boot-starter 依赖替换为纯 Druid,如下
1
2
3
4
5
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.1.22</version>
</dependency>

问题3

读写分离配置时注意配置的名称,其中的 slave-data-source-names最后有个s

1
2
3
4
5
6
spring:
shardingsphere:
masterslave:
name: ms # 名字,任意,需要保证唯一
master-data-source-name: ds-master # 主库数据源
slave-data-source-names: ds-slave-1, ds-slave-2 # 从库数据源

问题4

Shardingsphere 4.1.1 和 Swagger 2.9.2 存在冲突
Swagger 2.9.2 依赖于 Guava 20.0 以上的版本,但是 Shardingsphere 4.1.1 中引入的是 Guava 18.0 版本,所以启动项目时会导致以下错误

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
Caused by: org.springframework.context.ApplicationContextException: Failed to start bean 'documentationPluginsBootstrapper'; nested exception is com.google.common.util.concurrent.ExecutionError: java.lang.NoSuchMethodError: com.google.common.collect.FluentIterable.concat(Ljava/lang/Iterable;Ljava/lang/Iterable;)Lcom/google/common/collect/FluentIterable;
at org.springframework.context.support.DefaultLifecycleProcessor.doStart(DefaultLifecycleProcessor.java:176)
at org.springframework.context.support.DefaultLifecycleProcessor.access$200(DefaultLifecycleProcessor.java:50)
at org.springframework.context.support.DefaultLifecycleProcessor$LifecycleGroup.start(DefaultLifecycleProcessor.java:346)
at org.springframework.context.support.DefaultLifecycleProcessor.startBeans(DefaultLifecycleProcessor.java:149)
at org.springframework.context.support.DefaultLifecycleProcessor.onRefresh(DefaultLifecycleProcessor.java:112)
at org.springframework.context.support.AbstractApplicationContext.finishRefresh(AbstractApplicationContext.java:880)
at org.springframework.boot.context.embedded.EmbeddedWebApplicationContext.finishRefresh(EmbeddedWebApplicationContext.java:144)
at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:546)
at org.springframework.boot.context.embedded.EmbeddedWebApplicationContext.refresh(EmbeddedWebApplicationContext.java:122)
at org.springframework.boot.SpringApplication.refresh(SpringApplication.java:693)
at org.springframework.boot.SpringApplication.refreshContext(SpringApplication.java:360)
at org.springframework.boot.SpringApplication.run(SpringApplication.java:303)
at org.springframework.boot.test.context.SpringBootContextLoader.loadContext(SpringBootContextLoader.java:120)
at org.springframework.test.context.cache.DefaultCacheAwareContextLoaderDelegate.loadContextInternal(DefaultCacheAwareContextLoaderDelegate.java:98)
at org.springframework.test.context.cache.DefaultCacheAwareContextLoaderDelegate.loadContext(DefaultCacheAwareContextLoaderDelegate.java:116)
... 25 more

手动引入 20.0 版本的 Guava 后可以解决该问题

1
2
3
4
5
6
<!-- 主要因为 shardingsphere 中默认的 guava 依赖版本是 18.0 和 swagger 2.9.2 冲突,所以手动引入 20.0 版本的 -->
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>20.0</version>
</dependency>

参考

FAQ :: ShardingSphere
【ShardingSphere】2. Spring Boot整合Sharding-JDBC实现读写分离 - 贺小康的个人空间 - OSCHINA - 中文开源技术交流社区
SpringBoot 2.x ShardingSphere读写分离实战