开发者博客 – IT技术 尽在开发者博客

开发者博客 – 科技是第一生产力


  • 首页

  • 归档

  • 搜索

二叉树刷题记(二-中序遍历)

发表于 2021-11-07

这是我参与11月更文挑战的第1天,活动详情查看:2021最后一次更文挑战

前言

  • 昨天做了一道二叉树的中序遍历题目, 采用递归方式完成,今天更新第二种方法,使用迭代方法完成本题。在面试的时候,面试让你手写非递归的代码可能性会大一点,因为递归就那么几行代码,看一眼就会了。
  • 什么是迭代呢?
+ 在我的认识里:迭代就是使用**循环嵌套**的方式,然后借助辅助空间,例如数组等其他数据结构。
  • 越是比较难写出来的代码,它的质量一般来说是比较高的
  • 本文目的
+ 1.二叉树的中序遍历过程
+ 2.学会中序遍历的非递归代码,理解并掌握代码的实现过程
+ 3.希望本文章对你学习写代码有一定的帮助
**正文**
  • 预备知识1:
+ 二叉树中**左孩子**、**右孩子**、**父结点**代表的是什么?

image.png

  • 预备知识2:
+ **中序遍历**的顺序是什么呢?
+ 左孩子-》父结点-》右孩子
+ 上图的输出顺序为 [4,2,5,1,3,6]
+ 这个应该很好计算出来的,如果不会,欢迎下方评论区留言,我会专门讲解一下,也就当是给自己复习咯!
  • 啦啦啦,差不多了。当然你还得掌握循环和数组这些基本知识
  • 1.题目如下
    image.png
  • 2.代码实现
+ **代码思想**:首先,我们得用一个栈来保存我们在循环过程中遇到的元素,遇到就把元素压栈。那么这里又有一个问题,我们不能只入栈而不出栈,**那我们什么时候出栈呢?**
+ 我们知道,中序遍历是从左孩子开始的,所以我们应该是左孩子为null,也就是没有左孩子的时候出栈(这只是我的一种解释,不同人对这个解释不一样),知道这个这个问题就差不多了。
+ 树这个数据结构,遇到该类型问题我的理解就是将问题转化成一个结点的问题,这个结点能处理好了,那么其他的也就大致差不多,而且我觉得树的问题有一定的规律可循,这个还是需要慢慢发现的。
+ 直接看代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
ini复制代码var inorderTraversal = function(root) {
let res = [];//最后要返回的数组
let stack = [];//我们用来存储遍历过程中遇到的元素
//root == null 并且 stack.length == 0(循环退出)
while(root||stack.length){
while(root){//当前元素存在就循环
stack.push(root);//把当前元素入栈,不是入的值,而是当前的整个元素
root = root.left;//继续找它的左孩子
}
//该元素没有左孩子,那么我们就应该打印该元素
let target = stack.pop();//返回该元素并出栈
res.push(target.val);//将该元素的值压入要返回的栈里边
root = target.right;//遍历该元素的右孩子,因为左边和中间(父结点)已经遍历过了
}
return res;//返回最终的结果
};
  • 解释
+ 外层循环的判断条件(root||stack.length)
+ 循环退出的条件
+ root == null **并且** stack.length == 0
+ root == null的意思就是继续以该方向往下找已经找不到了,需要退回另走一个方向试试
+ stack.length == 0就是所有元素已经遇见并且都打印了
+ **注**:这里说的是root刚开始不是null它的过程
+ 其他的重点我都写在代码那里了,若有那一部分解释有问题,欢迎提出。
**结尾**
  • 本人笔拙,有的地方写的可能不对,欢迎提出。
  • 希望文章对你有所帮助,若觉得还有点用,欢迎点赞支持。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Docker 搭建常见服务

发表于 2021-11-07

Docker Hub

  1. Docker Hub 是一个由 Docker 公司运行和管理的镜像云存储库
  2. Docker Hub 官方地址:hub.docker.com/
  3. 通过 Dcoker Hub 可以搜索收录的公告镜像,也可以进行镜像仓库的私有化部署
  4. Docker Hub 官网镜像中带有Offical Image标志的,为官方发布,更加安全可靠

image-20211106212051836

MySQL

  1. 打开 Docker Hub 官网,搜索 MySQL 镜像,推荐选择带有Offical Image标志的

image-20211107092359150
2. 在Description中可查看描述文档,在Tags中可查看版本号

image-20211107092753989
3. 拉取镜像

1
shell复制代码docker pull mysql:8.0.27

image-20211107093258892
4. MySQL 容器基本启动命令

1
2
3
4
5
shell复制代码docker run 
--name some-mysql
-p 3306:3306
-e MYSQL_ROOT_PASSWORD=my-secret-pw
-d mysql:tag

使用-e指定环境变量,MYSQL_ROOT_PASSWORD 指定 root 用户密码

使用-p指定端口映射,将容器内 3306 映射到宿主机 3306,以供外部访问
5. MySQL 容器数据持久化

1
2
3
4
5
6
shell复制代码docker run 
--name some-mysql
-p 3306:3306
-v /my/custom/data:/var/lib/mysql
-e MYSQL_ROOT_PASSWORD=my-secret-pw
-d mysql:tag

使用-v指定数据卷映射,MySQL 容器的数据默认存放在/var/lib/mysql,通过数据卷映射到宿主机目录(可自动创建),方便备份

MySQL 容器被持久化到宿主机目录,再次挂载时,会自动加载原库表数据以及数据库密码设置
6. MySQL 容器自定义配置文件

MySQL 容器对配置文件进行了拆分,主配置文件为/etc/mysql/my.cnf,使用!includedir引入了/etc/mysql/conf.d 和 /etc/mysql/mysql.conf.d,mysql.conf.d存放了默认配置文件,conf.d留给用户自定义配置,一般只需要映射 /etc/mysql/conf.d下mysql.cnf即可

1
2
3
4
5
6
7
shell复制代码docker run 
--name some-mysql
-p 3306:3306
-v /my/custom/data:/var/lib/mysql
-v /my/custom/conf:/etc/mysql/conf.d
-e MYSQL_ROOT_PASSWORD=my-secret-pw
-d mysql:tag

image-20211107102440620
7. MySQL 容器常见启动环境变量

MYSQL_DATABASE指定启动时创建的数据库名称

MYSQL_USER、MYSQL_PASSWORD指定MYSQL_DATABASE的用户和密码

可通过在 /docker-entrypoint-initdb.d 下映射 SQL 脚本或 Shell 脚本文件来初始化 MySQL 数据库

Nginx

  1. 拉取 Nginx 镜像
1
shell复制代码docker pull nginx:1.21.3
  1. 运行 Nginx web 服务
1
2
3
4
shell复制代码docker run 
--name some-nginx
-v /some/content:/usr/share/nginx/html:ro # :ro 代表只读,容器内数据改变不影响外部
-d nginx:tag

作为 Web 服务器时,Nginx 容器 Web 资源默认存放在容器内的/usr/share/nginx/html,映射到宿主机目录可用于部署前端系统
3. 配置 Nginx 反向代理

Nginx 容器配置文件默认存放在容器内的/etc/nginx/nginx.conf

若没有该配置文件模板,可启动一个临时容器,用docker cp命令复制到宿主机

1
2
3
shell复制代码docker run --name tmp-nginx -d nginx
docker cp tmp-nginx:/etc/nginx/nginx.conf /host/path/nginx.conf
docker rm -f tmp-nginx

修改nginx.conf,添加反向代理配置如下

1
2
3
4
5
6
7
8
9
10
ini复制代码upstream myservers {
server 192.168.1.8;
server 192.168.1.9;
server 192.168.1.10;
}
server {
location / {
proxy_pass http://myservers/;
}
}

启动 Nginx 容器

1
2
3
4
shell复制代码docker run 
--name my-custom-nginx-container
-v /host/path/nginx.conf:/etc/nginx/nginx.conf
-d nginx:tag

Redis

  1. 拉取 Redis 镜像
1
shell复制代码docker pull redis:6.2.5

同一个版本号有不同版本,如6.2.5-buster、[6.2.5-alpine],区别是基于的 Linux 系统不同,在大小和内置工具上有差异
2. 启动 Redis

1
shell复制代码docker run --name some-redis -p 6379:6379 -d redis:tag
  1. 开启数据持久化

快照方式(默认开启),快照文件是容器内/data目录的dump.rdb文件

1
2
3
4
5
shell复制代码docker run 
--name some-redis
-p 6379:6379
-v /var/docker/redis/data:/data
-d redis:tag

开启 AOF 持久化,减少丢数据的风险,最多丢失一秒内的数据

1
2
3
4
5
6
shell复制代码docker run 
--name some-redis
-p 6379:6379
-v /var/docker/redis/data:/data
-d redis:tag
redis-server --appendonly yes # 可在 run 命令后直接追加启动后容器要执行的命令

AOF 文件名为appendonly.aof,默认生成到/data目录
4. 自定义 Redis 配置文件

新建redis.conf配置文件,添加自定义配置项

1
2
3
4
5
6
7
8
shell复制代码# 设置为 0.0.0.0,即开启远程访问
bind 0.0.0.0
# 开启 AOF
appendonly yes
# 设置端口
port 6379
# 设置访问密码
requirepass xxxxx

启动 Redis 并加载自定义redis.conf

1
2
3
4
5
6
7
shell复制代码docker run 
--name some-redis
-p 6379:6379
-v /var/docker/redis/data:/data
-v /var/docker/redis/redis.conf:/etc/redis.conf # 挂载配置文件
-d redis:tag
redis-server /etc/redis.conf # 启动 server 时使用配置文件

RabbitMQ

  1. 在 Docker Hub 上搜索 RabbitMQ

image-20211108212725487

拉取 RabbitMQ 镜像

1
shell复制代码docker pull rabbitmq:3.8.23-management

版本号中带有 management 的镜像是带有 Web 管理界面

不带 management 的镜像只能通过命令操作
2. 启动 RabbitMQ 基本服务

1
2
3
4
5
6
7
shell复制代码# 15672 Web 管理页面端口
# 5672 通信端口
docker run \
--name rabbitmq \
-p 15672:15672 \
-p 5672:5672 \
-d rabbitmq:3.8.23-management

Web 管理界面默认的账户密码为 guest/guest
3. 启动 RabbitMQ 并指定管理员初始账号密码

1
2
3
4
5
6
7
8
9
shell复制代码 # RABBITMQ_DEFAULT_USER 用户名
# RABBITMQ_DEFAULT_PASS 密码
docker run \
--name rabbitmq \
-p 15672:15672 \
-p 5672:5672 \
-e RABBITMQ_DEFAULT_USER=admin \
-e RABBITMQ_DEFAULT_PASS=xxxx \
-d rabbitmq:3.8.23-management
  1. 启动时创建一个虚拟主机
1
2
3
4
5
6
7
8
9
shell复制代码# RABBITMQ_DEFAULT_VHOST 指定虚拟主机名称
docker run \
--name rabbitmq \
-p 15672:15672 \
-p 5672:5672 \
-e RABBITMQ_DEFAULT_USER=admin \
-e RABBITMQ_DEFAULT_PASS=xxxx \
-e RABBITMQ_DEFAULT_VHOST=demoMQ \
-d rabbitmq:3.8.23-management
  1. 自定义 RabbitMQ 配置

默认配置文件是/etc/rabbitmq/rabbitmq.conf

image-20211108222440762

自定义配置文件启动,并持久化/var/lib/rabbitmq运行目录

1
2
3
4
5
6
7
shell复制代码docker run \
--name rabbitmq \
-p 15672:15672 \
-p 5672:5672 \
-v /var/docker/rabbitmq/rabbitmq.conf:/etc/rabbitmq/rabbitmq.conf \
-v /var/docker/rabbitmq/data:/var/lib/rabbitmq \
-d rabbitmq:3.8.23-management

浏览器访问宿主机的 15672 端口,可进入登录界面,使用配置的账户登录即可

image-20211112220802017

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

spring boot整合rabbitmq

发表于 2021-11-07

这是我参与11月更文挑战的第7天,活动详情查看:2021最后一次更文挑战

1.spring boot整合rabbitmq

代码思路:在配置文件中定义队列(queue),交换机(exchange),然后队列与交换器以路由键名称相对应(路由键和队列名相匹配,既以路由键寻找对列名),然后生产者可以通过交换器和队列名称确定要发送的队列,而消费者选择监控队列,来获取消息。

在整合之前需要安装rabbitmq,然后启动和搭建框架。

1.Direct交换机

1.新建队列与绑定关系
1
2
3
4
5
6
7
8
9
10
js复制代码@Configuration
public class RabbitMQConfig {

// -------------------------topic队列
// 创建队列
@Bean
public Queue topicQueue() {
return new Queue("topic.mess");
}
}
2.生产者

直接配置一个队列,然后调用API发送消息就可以了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
js复制代码public class ProducerController {
@Autowired
private RabbitTemplate rabbitTemplate;

@GetMapping("/sendMessage")
public Object sendMessage() {
new Thread(() -> {
//for (int i = 0; i < 100; i++) {

Date date = new Date();
String value = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss").format(date);
System.out.println("send message {}" + value);
City city= new City();
city.setCityName("aaaa");
city.setDescription("bbb");
city.setProvinceId((long)111);
rabbitTemplate.convertAndSend("topic.mess", city); //使用默认的队列

//}
}).start();
return "ok";
}
}
3.消费者

消费者直接使用就可以了(可以传对象 基本类型)。需要@RabbitListener注解。

1
2
3
4
5
6
7
8
9
js复制代码@Component
@RabbitListener(queues = "topic.mess") //topic交换机
public class Consumer2 {

@RabbitHandler
public void consumeMessage(City city) {
System.out.println("consume message {} 2222222:" + city);
}
}

2.topic交换机

1.新建队列与绑定关系

声名两个队列和一个topic交换器,然后通过路由键绑定他们之间的关系,路由键和队列名相同就能匹配,但是topic可以模糊匹配 #可以代替一段字符。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
js复制代码@Configuration
public class RabbitMQConfig {

// -------------------------topic队列
// 创建队列
@Bean
public Queue topicQueue() {
return new Queue("topic.mess");
}

@Bean
public Queue topicQueue2() {
return new Queue("topic.mess2");
}

// 创建 topic 类型的交换器
@Bean
public TopicExchange topicExchange() {
return new TopicExchange("topic");
}


// 使用路由键(routingKey)把队列(Queue)绑定到交换器(Exchange) Topic交换器通过routingKey与队列绑定
@Bean
public Binding bindingA(Queue topicQueue, TopicExchange topicExchange) {
return BindingBuilder.bind(topicQueue).to(topicExchange).with("topic.mess");
}

@Bean
public Binding bindingB(Queue topicQueue2, TopicExchange topicExchange) {
return BindingBuilder.bind(topicQueue2).to(topicExchange).with("topic.#");
}
}
2.生产者

直接调用API发送消息。消费者发送到队列,因为有模糊匹配的规则,topic.mess可以匹配 topic.mess和topic.mess2队列 而topic.mess2只能匹配到topic.#。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
js复制代码@RestController
public class ProducerController {
@Autowired
private RabbitTemplate rabbitTemplate;

@GetMapping("/sendMessage")
public Object sendMessage() {
new Thread(() -> {
//for (int i = 0; i < 100; i++) {

Date date = new Date();
String value = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss").format(date);
System.out.println("send message {}" + value);
City city= new City();
city.setCityName("aaaa");
city.setDescription("bbb");
city.setProvinceId((long)111);
rabbitTemplate.convertAndSend("topic", "topic.mess", city);
rabbitTemplate.convertAndSend("topic", "topic.mess2", city);
//}
}).start();
return "ok";
}
}
3.消费者

消费者直接接收。

1
2
3
4
5
6
7
8
9
js复制代码@Component
@RabbitListener(queues = "topic.mess") //topic交换机
public class Consumer2 {

@RabbitHandler
public void consumeMessage(City city) {
System.out.println("consume message {} 2222222:" + city);
}
}

3.Fanout Exchange 广播

1.新建队列与绑定关系

在配置文件中声名队列和交换器,然后绑定。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
js复制代码// --------FanoutExchange绑定
// -------------------------Fanout 队列
@Bean
FanoutExchange fanoutExchange() {
return new FanoutExchange("fanoutExchange");
}
@Bean
public Queue fanoutQueue() {
return new Queue("fanoutqueue");
}
@Bean
public Queue fanoutQueue2() {
return new Queue("fanoutqueue2");
}
@Bean
public Binding bindingC(Queue fanoutQueue, FanoutExchange fanoutExchange) {
return BindingBuilder.bind(fanoutQueue).to(fanoutExchange);
}
@Bean
public Binding bindingD(Queue fanoutQueue2, FanoutExchange fanoutExchange) {
return BindingBuilder.bind(fanoutQueue2).to(fanoutExchange);
}
2.生产者

调用API发送消息。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
js复制代码@RestController
public class ProducerController {
@Autowired
private RabbitTemplate rabbitTemplate;

@GetMapping("/sendMessage")
public Object sendMessage() {
new Thread(() -> {
//for (int i = 0; i < 100; i++) {

Date date = new Date();
String value = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss").format(date);
System.out.println("send message {}" + value);
City obj = new City();
obj.setCityName("aaaa");
obj.setDescription("bbb");
obj.setProvinceId((long)111);
rabbitTemplate.convertAndSend("fanoutExchange","", value); //使用默认的队列

//}
}).start();
return "ok";
}
}
3.消费者

然后接收,所有绑定队列的都可以接收到。

1
2
3
4
5
6
7
8
9
js复制代码@Component
@RabbitListener(queues = "fanoutqueue2")
public class Consumer {

@RabbitHandler
public void consumeMessage(String message) {
System.out.println("consume message {} 1111111:" + message);
}
}

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

一份完整的后端学习路线

发表于 2021-11-07

前言

大家好呀,我是捡田螺的小男孩。最近很多读者跟我聊天,想要一份后端学习路线。学习方向和路线很重要,清晰的学习路线,能让你在成功道路上事半功倍。所以趁着这个周末,给大家整理一份完整的后端学习路线(偏Java方向)。相信看完后,不管是还在学校读书的学生还是已经工作的伙伴,都会有帮助的。

后端学习路线

  • 公众号:捡田螺的小男孩

1.数据结构与算法

1.1 为什么数据结构很重要?

我记得当时读大一的时候,我们就有一门专业必修课:《C++数据结构与算法》。毫无疑问,数据结构对于程序员来说,非常基础非常重要。程序界有这么一句话,程序=数据结构+算法。可见数据结构的重要性。

日常业务开发中,几乎不会有从0到1实现个数据结构的需求。最多就是用递归算法解析一下文件,用排序算法排下数据。 但是呢,不仅仅于此。如果回到编程语言这块的话,就拿Java来说的话,底层框架经常见数据结构。

比如常用的集合ArrayList 和LinkedList,底层就是数组和链表的数据结构。再比如我们使用频率超级高的HashMap,JDK8之前,它的底层就是数组+链表。JDK8之后,底层数据结构就是数组+链表+红黑树。只有熟悉数据结构,才能更好掌握这些底层源码知识。

其他编程语言其实也类似,所以学好数据结构真的很重要。

1.2 数据结构与算法相关数据推荐

如何学习数据结构与算法呢?书山有路勤为径,哈哈。所以就是可以多看书!看哪本书的,根据不同开发语言,推荐这几本吧:

《数据结构与算法分析-C语言描述》

《数据结构与算法分析-Java语言描述》

如果精力旺盛的伙伴们,可以啃下这本神书:《算法导论》

电子书已经给你们准备好啦,可以在下面这个GitHub仓库上找到。

github地址

1.3 视频推荐

B站上,浙江大学的一个数据结构的课还挺不错。很经典也比较全,非常适合小白入门。

视频链接:www.bilibili.com/video/BV1JW…

  1. 计算机网络

2.1 为什么计算机网络很重要?

计算机网路,是计算机专业的必修课。我们学校的话,是大二会学这么课,课本是这个《计算机网络: 自顶向下方法》。

计算机网络为什么重要呢?举两个简单例子

  • 一个http请求返回了403状态码,有些小伙子不知道是权限的原因。
  • 再比如,你调一个第三方接口,返回超时,你就需要ping一下或者telnet一下,确认网络是不是通的,等等。

2.2 计算机网络相关书推荐

《计算机网络: 自顶向下方法》

本书是经典的计算机网络教材,采用作者独创的自顶向下方法来讲授计算机网络的原理及其协议,自第1版出版以来已经被数百所大学和学院选作教材,被译为14种语言。

《图解HTTP》

本书对互联网基盘——HTTP协议进行了全面系统的介绍。HTTP协议的发展历史娓娓道来,严谨细致地剖析了HTTP协议的结构,列举诸多常见通信场景及实战案例,最后延伸到Web安全、最新技术动向等方面。

《网络是怎样连接的》

本书以探索之旅的形式,从在浏览器中输入网址开始,一路追踪了到显示出网页内容为止的整个过程,以图配文,讲解了网络的全貌,并重点介绍了实际的网络设备和软件是如何工作的。

2.3 计算机网络视频推荐

B站的计算机网络微课堂,觉得挺不错的,给大家推荐一下

视频地址:www.bilibili.com/video/BV1c4…

3.数据库

3.1 为什么数据库很重要?

后端就是操作和存储数据,所以作为后端开发,数据库是最主要的学习模块。数据库,也是计算机专业的必修课。我们学校的话,是大三会学这门课,课本教材是这个《数据库系统概论》。

如果连基本的SQL都不会写的话,就不能算后台开发工程师。当然,学习数据库技能,会写SQL语句只是基本素养。想成为一名高级后台开发工程师,还需要学会SQL调优、分库分表等等。

3.2 数据库相关书籍推荐

《sql必知必会》

本书是深受世界各地读者欢迎的SQL经典畅销书,内容丰富,文字简洁明快,针对Oracle、SQL Server、MySQL、DB2、PostgreSQL、SQLite等各种主流数据库提供了大量简明的实例。

《高性能Mysql》

《高性能mysql(第3版)》不但适合数据库管理员(dba)阅读,也适合开发人员参考学习。不管是数据库新手还是专家,相信都能从本书有所收获。

《MySQL技术内幕:innodb存储引擎》

《MySQL技术内幕:InnoDB存储引擎(第2版)》从源代码的角度深度解析了InnoDB的体系结构、实现原理、工作机制,并给出了大量最佳实践,能帮助你系统而深入地掌握InnoDB,更重要的是,它能为你设计管理高性能、高可用的数据库系统提供绝佳的指导。

3.3 数据库相关文章推荐

之前写过很多MySQL数据库相关的文章,每一篇质量都很不错,推荐给大家,相信大家看完会有收获的。

  • 看一遍就理解:MVCC原理详解
  • 看一遍就理解:order by详解
  • MySQL索引底层:B+树详解
  • 阿里一面,给了几条SQL,问需要执行几次树搜索操作?
  • MySQL中,21个写SQL的好习惯(修正版)
  • 100道MySQL数据库经典面试题解析(收藏版)
  • 一文彻底读懂MySQL事务的四大隔离级别
  • 手把手教你分析Mysql死锁问题
  • 实战!聊聊如何解决MySQL深分页问题
  • 后端程序员必备:书写高质量SQL的30条建议
  • 后端程序员必备:索引失效的十大杂症
  • 生产问题分析!delete in子查询不走索引?!

3.4 课程推荐

  • 极客时间的《MySQL 45讲》

MySQL的45讲,真的挺好,课程讲解深入浅出,生动有趣,不仅仅适合开发看,也适合运维看。

3.5 视频推荐

推荐一个适合初级以及中级工程师看的SQl视频,讲师一个老外,内容很不错的。

视频地址:www.bilibili.com/video/BV1UE…

  1. 操作系统

4.1 为什么要学习操作系统?

引用一个知乎的回答:

比如你要开发一个网络代理软件,不过是从socket上收一个包,然后转发给另一个socket,看起来,跟操作系统没半毛线关系。

实现过程中,如果你只用一个线程处理网络IO,只要CPU顶得住,延迟一般会在几个毫秒内。但是如果你用了多线程分别处理收/发,网络压力稍大,引入的延迟就会增加,额外延迟就可能突破几十个毫秒。

想搞明白这是为什么,就需要对操作系统调度原理、时间片等概念没有足够深刻的理解。

应用层开发的确只需要接触冰山在海面上的可见部分;但这只够你开发一些蹩脚的软件;冰山藏在海面下的9/10,和冰山的可见部分毕竟是一体的:浮于表面的软件同样会影响冰山的不可见部分、并被冰山的不可见部分影响。如果没有基本了解,当冰山的不可见部分透过可见部分坑到你时,你绝没能力为这些蹩脚软件debug。

4.2 操作系统书籍推荐

《现代操作系统》

本书是操作系统领域的经典之作.书中集中讨论了操作系统的基本原理,包括进程、线程、存储管理、文件系统、输入/输出、死锁等,同时还包含了有关计算机安全、多媒体操作系统、掌上计算机操作系统、微内核、多核处理机上的虚拟机以及操作系统设计等方面的内容。

《程序是怎么跑起来的》

本书从计算机的内部结构开始讲起,以图配文的形式详细讲解了二进制、内存、数据压缩、源文件和可执行文件、操作系统和应用程序的关系、汇编语言、硬件控制方法等内容,目的是让读者了解从用户双击程序图标到程序开始运行之间到底发生了什么。同时专设了“如果是你,你会怎样介绍?”专栏,以小学生、老奶奶为对象讲解程序的运行原理,颇为有趣。本书图文并茂,通俗易懂,非常适合计算机爱好者及相关从业人员阅读。

4.3 操作系统视频推荐

清华大学公开课:操作系统,讲得很好,推荐一波

视频地址:open.163.com/newview/mov…

  1. 计算机组成原理

5.1 为什么需要学习计算机组成原理

不管是前端还是后台开发,我们编程的程序,都是在计算机上跑的。日常开发中,可能很少机会接触到计算机底层。但是你知道吗,我们使用的Java、Python、Go等这些高级语言,最终变成计算机的指令,然后操作计算机硬件。当然,这些计算机基础知识点,在学校专业课上学好就可以啦。

如果你是计算机非科班,半路出家的。给你推荐本书,以及一个不错的视频吧

5.2 计算机组成原理书推荐

这本书主要讲了计算机系统体系结构、计算机位运算、指令集体系结构、处理器控制。

5.3 视频推荐

哈工大计算机组成原理:

视频地址:www.bilibili.com/video/BV1WW…

  1. 缓存

6.1 为什么需要学习缓存

如果需要频繁查询数据库,使用缓存的话,就可以减少数据库的压力,提高接口性能。一般缓存用得比较多的话,有Redis,memcache,以及JVM本地缓存。

6.2 缓存相关书籍推荐

《Redis设计与实现》

系统而全面地描述了 Redis 内部运行机制。图示丰富,描述清晰,并给出大量参考信息,是NoSQL数据库开发人员案头必备。包括大部分Redis单机特征,以及所有多机特性。

本书全面讲解Redis基本功能及其应用,并结合线上开发与运维监控中的实际使用案例,深入分析并总结了实际开发运维中遇到的“陷阱”,以及背后的原因, 包含大规模集群开发与管理的场景、应用案例与开发技巧,为高效开发运维提供了大量实际经验和建议。

6.3 Redis相关文章推荐

  • 大厂经典面试题:Redis为什么这么快?
  • 美团二面:Redis与MySQL双写一致性如何保证?
  • 使用Redis,你必须知道的21个注意要
  • 2W字!详解20道Redis经典面试题!(珍藏版)

6.4 视频推荐

Redis入门的话,推荐B站这个视频:《尚硅谷- Redis6 入门到精通》

视频地址:www.bilibili.com/video/BV1Rv…

  1. 后端主流开发语言

当前主流后端开发语言如下:

7.1 Java

笔者目前是Java开发工程师。Java一般用来做应用的,它作为热门服务端语言活跃多年了,很多公司都有找Java程序员,市场有很多需求。所以说,选择学习Java,也许不是最好的,但是一定不会太差。当前那些银行金融机构,后端几乎都是Java语言。

学习Java,看书的话,推荐这两本书吧

《Java编程思想》

Java界的神书!本书赢得了全球程序员的广泛赞誉,即使是最晦涩的概念,作者都会用小而直接的编程示例讲解明白。从Java的基础语法到最高级特性(深入的面向对象概念、多线程、自动项目构建、单元测试和调试等),本书都能逐步指导你轻松掌握。

《深入理解Java虚拟机》

这是一本从工作原理和工程实践两个维度深入剖析JVM的著作,是计算机领域公认的经典。

当然,Java相关的好书还有很多很多:

一份Java程序员的珍藏书单,请您注意查收

至于视频的话,如果你是零基础入门的,推荐看韩顺平的视频:零基础30天学会Java

7.2 C++

如果你还在读大学,你想进腾讯公司,那学好C++还是有挺大机会的。因为腾讯还是找挺多C++后台的。书籍的话,推荐这基本:《C++ Primer》、《Effective C++》、《C++ 标准程序库》

入门视频的话,可以看这个:黑马程序员匠心之作|C++教程从0到1入门编程,学习编程不再难

视频地址:www.bilibili.com/video/BV1et…

7.3 Python

python,可以用来做数据分析、机器学习,还也可以用来做后端开发、Web开发、前端、人工智能、大数据等等。它功能那个非常强大,而且比较简单,所以还是很受广大开发者欢迎的。入门python的话,可以看这本书

《Python编程:入门到实践》

这是一本入门好书,真的是手把手教你的,初学者成就感满满。

《流畅的Python》

很棒的书籍,从最底层去理解Python。适合想要扩充知识的中级和高级Python程序员。

7.4 Go

当前很多大厂,如腾讯、宇宙条、虾皮等等,都有用Go语言。所以学Go语言也是一个不错的选择。推荐几本关于Go语言的不错书籍。

《Go专家编程》

《Go专家编程》深入地讲解了Go语言常见特性的内部机制和实现方式,大部分内容源自对Go语言源码的分析,并从中提炼出实现原理。通过阅读本书,读者可以快速、轻松地了解Go语言的内部运作机制。

《The Go Programming Language》

这本书代码例子相当好,基本上很多概念,文字看不懂的话,多读几遍代码,很容易就能理解了

  1. 消息队列

消息队列有ActiveMQ,RabbitMQ,ZeroMQ,Kafka,MetaMQ,RocketMQ。消息队列本质上就是,生产者把消息发到队列存储,然后消息者消费的过程,它主要解决应用耦合,异步消息,流量削锋等问题。

对于后端程序员来说,消息队列是必学的!可以看这几本书哈

《Kafka权威指南》

本书详细介绍了如何部署Kafka集群、开发可靠的基于事件驱动的微服务,以及基于Kafka平台构建可伸缩的流式应用程序。通过详尽示例,你将会了解到Kafka的设计原则、可靠性保证、关键API,以及复制协议、控制器和存储层等架构细节。

《RabbitMQ实战指南》

《RabbitMQ实战指南》从消息中间件的概念和RabbitMQ的历史切入,主要阐述RabbitMQ的安装、使用、配置、管理、运维、原理、扩展等方面的细节。

学习视频的话,推荐:尚硅谷Kafka教程(消息队列kafka快速入门)

视频地址:www.bilibili.com/video/BV1a4…

  1. Java Web

学好Java web的话,你就可以自己开发个网站啦,想想是不是很开心呀。Java web包括前端基础(如html,css,js等等)、servlet、JSP、Filter、Session、Cookie、springmvc等等。

推荐这个视频:尚硅谷最新版JavaWeb全套教程,java web零基础入门完整版

视频地址:www.bilibili.com/video/BV1Y7…

  1. 设计模式

如果你每天都是流水线式地写代码,如何在写代码中找到乐趣呢?最好的方式就是:使用设计模式优化自己的业务代码。

设计模式代表了最佳的实践,通常被有经验的面向对象的软件开发人员所采用。设计模式是软件开发人员在软件开发过程中面临的一般问题的解决方案。这些解决方案是众多软件开发人员经过相当长的一段时间的试验和错误总结出来的。

学习设计模式,需要知道这六大原则:

  • 开闭原则:对扩展开放,对修改关闭。在程序需要进行拓展的时候,不能去修改原有的代码,实现一个热插拔的效果。
  • 里氏代换原则: 任何基类可以出现的地方,子类一定可以出现。使用抽象类继承,不使用具体类继承。
  • 依赖倒转原则: 针对接口编程,依赖于抽象而不依赖于具体。
  • 接口隔离原则: 使用多个隔离的接口,比使用单个接口要好。它强调降低依赖,降低耦合。
  • 迪米特法则: 一个软件实体应当尽可能少地与其他实体发生相互作用,通过中间类建立联系。
  • 合成复用原则: 尽量使用合成/聚合的方式,而不是使用继承。

学习设计模式,书籍的话,推荐这几本书

《Head First 设计模式》

本书涵盖了23个设计模式,例子简单易懂,抛砖引玉,读起来很有意思的。真的是一本非常赞的设计模式入门书籍

《设计模式》

这本书结合设计实作例从面向对象的设计中精选出23个设计模式,总结了面向对象设计中最有价值的经验,并且用简洁可复用的形式表达出来。适合大学计算机专业的学生、研究生及相关人员参考。

《图解设计模式》

194张图表 + Java示例代码 = 轻松理解GoF的23种设计模式

学习视频的话,推荐B站这个:尚硅谷Java设计模式(图解+框架源码剖析)

视频地址:www.bilibili.com/video/BV1G4…

  1. 代码优化

想成为一位高级开发工程甚至资深技术专家,不掌握常规的代码优化技巧,是说不过去的。我们日常开发中即使是增删改查,也是有很多地方值得优化代码的。

推荐几本优化代码的书:

《重构:改善既有代码的设计》

本书作者给出了一系列行之有效的整洁代码操作实践,些实践在本书中体现为一条条规则,并辅以来自现实项目的正、反两面的范例。只要遵循这些规则,就能编写出干净的代码,从而有效提升代码质量。

《代码整洁之道》

本书作者给出了一系列行之有效的整洁代码操作实践,些实践在本书中体现为一条条规则,并辅以来自现实项目的正、反两面的范例。只要遵循这些规则,就能编写出干净的代码,从而有效提升代码质量。

《Effective java》

本书一共包含90个条目,每个条目讨论Java程序设计中的一条规则。这些规则反映了最有经验的优秀程序员在实践中常用的一些有益的做法。

  1. 分布式

当前系统架构,都是分布式部署的。有关于分布式,我们主要有这些知识点需要学习:

12.1 分布式锁

作为分布式锁一般有数据库锁、redis分布式锁、还有zookeeper分布式锁。

之前写过一篇有关于redis分布式锁的文章:

七种方案!探讨Redis分布式锁的正确使用姿势

12.2 分布式一致性算法

分布式一致性算法有PAXOS、Raft、Zab。

给大家推荐一本书吧:

《从Paxos到Zookeeper 分布式一致性原理与实践》

《Paxos到Zookeeper:分布式一致性原理与实践》从分布式一致性的理论出发,向读者简要介绍几种典型的分布式一致性协议,以及解决分布式一致性问题的思路,其中重点讲解了Paxos和ZAB协议。

12.3 分布式事务

面试的时候,大厂特别喜欢问分布式事务。有关于分布式事务,需要知道数据一致性、CAP理论、BASE理论、分布式事务解决方案(如TCC、2PC、本地消息等等)。

推荐下我之前写得一篇文章。

后端程序员必备:分布式事务基础篇

12.4 一致性哈希算法

一鼓作气学会“一致性哈希”,就靠这 18 张图了

12.5 微服务

Dubbo、Spring Cloud、Zookeeper、RPC、 Eureka、Gateway、Sentinel这些名字是不是都比较熟悉了呢?

如果是新手的话,建议先从Dubbo开始学。对RPC、分布式概念基本了解后,就开始啃 Spring Cloud 全家桶。

推荐下B站,尚硅谷的Dubbo教程

视频地址:www.bilibili.com/video/BV1ns…

还有尚硅谷 SpringCloud的视频教程

视频地址:www.bilibili.com/video/BV18E…

顺便推荐下这本书:《微服务架构设计模式》

本书将教会你如何开发和部署生产级别的微服务架构应用。这套宝贵的架构设计模式建立在数十年的分布式系统经验之上,Chris 还为开发服务添加了新的模式,并将它们组合成可在真实条件下可靠地扩展和执行的系统。本书不仅仅是一个模式目录,还提供了经验驱动的建议,以帮助你设计、实现、测试和部署基于微服务的应用程序。

  1. Spring、 SpringMVC、MyBatis、SpringBoot、SpringSecurity、netty

Java开发,日常都需要用到的框架。推荐B站相关学习视频:

  • 尚硅谷 - Spring 5 框架最新版教程(idea版):www.bilibili.com/video/BV1Vf…
  • 尚硅谷 - SpringMVC 2021 最新教程:www.bilibili.com/video/BV1Ry…
  • 尚硅谷 - MyBatis 实战教程全套完整版:www.bilibili.com/video/BV1mW…
  • 雷丰阳 2021 版 SpringBoot2 零基础入门:www.bilibili.com/video/BV19K…
  • 尚硅谷 - SpringSecurity 框架教程:www.bilibili.com/video/BV15a…
  • 尚硅谷Netty教程:www.bilibili.com/video/BV1DJ…

14.开发规范

写代码一定要遵循规范,因为不规范的代码维护起来成本很高,又容易出bug。

推荐 一本书:

《阿里巴巴 Java 开发手册》

它结合作者的开发经验和架构历程,提炼阿里巴巴集团技术团队的集体编程经验和软件设计智慧,浓缩成为立体的编程规范和最佳实践。

视频的话,推荐这个:华山版《Java开发手册》独家讲解

视频地址:developer.aliyun.com/live/1201

  1. 版本管里工具

版本管理系统有SVn和Git,一般我们都是用Git。

熟悉和使用Git,是每位开发必备的技能。你需要知道怎么提交代码、推送代码到远程、从远程拉取代码、回退代码、合并代码以解决代码冲突。

推荐学习Git的视频:【尚硅谷】5h打通Git全套教程IDEA版

菜鸟教程:www.runoob.com/git/git-tut…

之前写过一年关于Git的文章,写得挺好的:程序员必备基础:Git 命令全方位学习

16.安全相关

安全这一块,还是挺重要的。作为后台开发工程师,需要掌握加密解密过程、加签验签、web安全常见问题、服务器安全漏洞问题、基本授权认证实现。

之前写过一篇有关安全漏洞问题的,感兴趣可以看下。

程序员必备基础:10种常见安全漏洞浅析

  1. 搜索引擎

我们日常开发的,经常有搜索的功能实现,数据量比较大时,搜索功能一般是基于Elasticsearch实现的。

  • Lucene 是apache软件基金会 jakarta项目组的一个子项目,是一个开放源代码的全文检索引擎工具包,但它不是一个完整的全文检索引擎,而是一个全文检索引擎的架构,提供了完整的查询引擎和索引引擎
  • Elasticsearch 是一个基于Lucene的搜索服务器。它提供了一个分布式多用户能力的全文搜索引擎,基于RESTful web接口。
  • Solr是一个独立的企业级搜索应用服务器,它对外提供类似于Web-service的API接口。用户可以通过http请求,向搜索引擎服务器提交一定格式的XML文件,生成索引;也可以通过Http Get操作提出查找请求,并得到XML格式的返回结果。

Elasticsearch入门的话,看这本书的《Elasticsearch实战》,虽然比较老了,新手还是可以的

当然,最好还是看官方文档吧。

  1. linux

绝大多数企业,他们的项目都是不是在Linux服务器上的。因此,有必要学习Linux基本命令以及shell脚本的编写。

如果有条件的话,建议大家自己购买一台云服务器,并且在本地搭建 Linux 虚拟机环境,搞起来!

推荐这本书:《鸟哥的 Linux 私房菜 —— 基础篇》

如果想要入门,这本就是最好的选择了.讲得非常非常细致。

学习视频:【小白入门 通俗易懂】2021韩顺平 一周学会Linux

视频地址:www.bilibili.com/video/BV1Sv…

19.Java 练手项目

推荐几个练手的Java项目:

  • 硅谷尚筹网Java项目实战开发教程(含SSM框架,微服务架构,封捷主讲):www.bilibili.com/video/BV1bE…
  • 黑马程序员Java项目《好客租房》,Java企业级解决方案(Spring全家桶+分布式解决方案+微信授权+爬虫解决方案等):www.bilibili.com/video/BV1sZ…
  • mall-learning:github.com/macrozheng/…
  • SpringBoot 电商商城系统 Mall4j:github.com/gz-yami/mal…
  • 黑马程序员Java项目SaaS移动办公完整版《iHRM 人力资源管理系统》,跨行业SaaS办公整合性解决方案:www.bilibili.com/video/BV18A…
  1. leetcode

当前互联网环境,如果想要进大厂的话,必须要要刷leetcode。因为每个大厂都会安排1~2到leetcode算法题。如果你打算换工作,那还是建议提前刷下leetcode题目。把leetcode题目哪些中等难度和简单的题目,都刷一遍就差不多啦。

如果你看书的话,推荐《剑指offer》以及《labuladong算法小抄》

在线刷题地址:leetcode-cn.com/

如果你看视频的话,推荐这个:这可能是B站讲的最好的数据结构算法-leetcode真题解析(2021年最新版)

视频地址:www.bilibili.com/video/BV1a5…

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Java之String

发表于 2021-11-07

1. 常用的创建字符串的两种方式:

  1. 通过直接赋值创建字符串
1
java复制代码String testA = "一生清澈";
  1. 通过调用构造方法创建字符串
1
java复制代码String testB = new String("一生清澈");

2. 通过直接赋值创建字符串

2.1 创建过程

  1. 首先检查字符串常量池中是否已经存在了该字符串对象,如果已经存在,那么指针直接指向已经存在的字符串的地址
  2. 如果字符串常量池中不存在该字符串,那么在字符串常量池中创建此 字符串对象,然后指针指向新创建的字符串的地址

2.2 代码验证

1
2
3
4
5
java复制代码String testA = "一生清澈";
String testB = "一生清澈";

// 判断字符串的内存地址是否相等
System.out.println(testA == testB);

输出结果:true

3. 通过调用构造方法创建字符串

3.1 创建过程

  1. 首先在堆中为新的字符串对象分配内存
  2. 然后检查字符串常量池中是否存在相等的字符串,如果存在,那么将新创建的字符串对象的字符数组指针指向常量池中字符串对象的字符数组。如果不存在,那么先在字符串常量池中创建新的字符串对象,然后让堆中新创建的字符串对象的字符数组指针指向常量池中字符串对象的字符数组

3.2 代码验证

1
2
3
4
5
6
7
java复制代码// 通过直接赋值的方式创建字符串
String testA = "一生清澈";
// 通过调用构造方法的方式创建字符串
String testB = new String("一生清澈");

// 判断字符串的内存地址是否相等
System.out.println(testA == testB);

输出结果:false

1
2
3
4
5
6
7
8
9
10
11
12
java复制代码// 通过反射获取字符串类的字符数组
Field value = String.class.getDeclaredField("value");
// 设置私有变量的访问权限
value.setAccessible(true);

// 通过反射获取testA对象的字符数组
char[] testACharArray = (char[]) value.get(testA);
// 通过反射获取testB对象的字符数组
char[] testBCharArray = (char[]) value.get(testB);

// 判断字符数组的地址是否相同
System.out.println(testACharArray == testBCharArray);

输出结果:true

输出结果证明:这虽然是两个不同的字符串对象,但是他们内部的字符数组是同一个字符数组

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

认识「泛型 & Trait」

发表于 2021-11-07

「这是我参与11月更文挑战的第 6 天,活动详情查看:2021最后一次更文挑战」


泛型和trait是Rust类型系统中最重要的两个概念。

泛型并不是Rust特有的概念,在很多强类型编程语言中也支持泛型。泛型允许开发者编写一些在使用时才指定类型的代码。

Rust 中的 trait,它借鉴了Haskell的Typeclass。同时也是:Rust 实现零成本抽象的基石。

  1. trait是Rust唯一的接口抽象方式
  2. 可以静态分发,也可以动态调用
  3. 可以当作标记类型拥有某些特定行为的”标签”来使用

最关键的一句话:trait 是对类型行为的抽象。

泛型

在泛型的类型签名中,通常使用字母T来代表一个泛型。也就是说这个Option<T>枚举类型对于任何类型都适用。

这样的话,我们就没必要给每个类型都定义一遍Option枚举,比如 Option<u32>或 Option<String>等。标准库提供的 Option<T> 类型已经通过 use std::prelude::v1::* 自动引入了每个Rust包中,所以可以直接使用 Some(T)/None 来表示一个 Option<T> 类型,而不需要写 Option::Some(T) 或 Option::None。

Trait

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
rust复制代码pub trait Fly {
fn fly(&self) -> bool;
}
struct Duck;
struct Pig;
// impl block
impl Fly for Duck {
fn fly(&self) -> bool {
return true;
}
}
impl Fly for Pig {
fn fly(&self) -> bool {
return false;
}
}
// use Triat
fn fly_static<T: Fly>(s: T) -> bool {
s.fly()
}
fn fly_dyn(s: &Fly) -> bool {
s.fly()
}

fn main() {
let pig = Pig;
assert_eq!(fly_static::<Pig>(pig), false);
let duck = Duck;
assert_eq!(fly_static::<Duck>(duck), true);
assert_eq!(fly_dyn(&Pig), false);
assert_eq!(fly_dyn(&Duck), true);
}

我们需要关注两个点:

fly_static()

我们可以看到:fly_static<T: Fly>(s: T) 。其中:T:Fly 这种语法形式使用 Fly Trait 对泛型T进行行为上的限定 → 代表实现 Fly Trait 的类型,或者是拥有 fly 这种行为的类型。

这种限制在Rust中被成为:Trait bound。

通过Trait bound,限制了fy_static泛型函数参数的类型范围。如果有不满足该限定的类型传入,编译器就会识别并报错。

fy_static::

使用了assert!断言,用于判断 fy_static::<Pig>(pig) 的调用结果是否将会返回 false。其中 ::<Pig> 这样的语法形式用于给泛型函数指定具体的类型,这里调用的是 Pig实现的fly 方法。

上面这种调用方式在 Rust 中叫静态分发。

Rust 编译器会为 fy_static::<Pig>(pig)和 fy_static::<Duck>(duck)这两个具体类型的调用生成特殊化的代码。也就是说,对于编译器来说,这种抽象并不存在,因为在编译阶段,泛型已经被展开为具体类型的代码。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Ribbon 源码分析

发表于 2021-11-07

配置

引用eureka依赖的时候内部就已经带人了ribbon,不用单独引用
image.png

image.png

image.png
调用

image.png

流程图

image.png

入口

image.png

image.png
相关代码

image.png

image.png

image.png
感兴趣的可以看看这个:www.codingsky.com/doc/2020/6/…

image.png

debug

image.png

image.png

image.png
一路追踪

image.png
每隔10秒比对一次
image.png

image.png

image.png
定时任务ping服务状态的相关逻辑

1
bash复制代码com.netflix.loadbalancer.BaseLoadBalancer.Pinger#runPinger

从eureka获取服务注册的信息

image.png

image.png

image.png

image.png

image.png

image.png

image.png

那ribbon是怎么更新eureka的服务呢?看下去

image.png
处理逻辑 每隔30秒更新一次

image.png

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
java复制代码public synchronized void start(final UpdateAction updateAction) {
if (isActive.compareAndSet(false, true)) {
final Runnable wrapperRunnable = new Runnable() {
@Override
public void run() {
if (!isActive.get()) {
if (scheduledFuture != null) {
scheduledFuture.cancel(true);
}
return;
}
try {
updateAction.doUpdate();
lastUpdated = System.currentTimeMillis();
} catch (Exception e) {
logger.warn("Failed one update cycle", e);
}
}
};

scheduledFuture = getRefreshExecutor().scheduleWithFixedDelay(
wrapperRunnable,
initialDelayMs,
refreshIntervalMs,
TimeUnit.MILLISECONDS
);
} else {
logger.info("Already active, no-op");
}
}

image.png

image.png

后记

到这里就主要流程分析完成了,可能有不足,哈哈哈.下期Feign,敬请期待!

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

【高并发】深入解析Callable接口

发表于 2021-11-07

大家好,我是冰河~~

本文纯干货,从源码角度深入解析Callable接口,希望大家踏下心来,打开你的IDE,跟着文章看源码,相信你一定收获不小。

1.Callable接口介绍

Callable接口是JDK1.5新增的泛型接口,在JDK1.8中,被声明为函数式接口,如下所示。

1
2
3
4
java复制代码@FunctionalInterface
public interface Callable<V> {
V call() throws Exception;
}

在JDK 1.8中只声明有一个方法的接口为函数式接口,函数式接口可以使用@FunctionalInterface注解修饰,也可以不使用@FunctionalInterface注解修饰。只要一个接口中只包含有一个方法,那么,这个接口就是函数式接口。

在JDK中,实现Callable接口的子类如下图所示。

默认的子类层级关系图看不清,这里,可以通过IDEA右键Callable接口,选择“Layout”来指定Callable接口的实现类图的不同结构,如下所示。

这里,可以选择“Organic Layout”选项,选择后的Callable接口的子类的结构如下图所示。

在实现Callable接口的子类中,有几个比较重要的类,如下图所示。

分别是:Executors类中的静态内部类:PrivilegedCallable、PrivilegedCallableUsingCurrentClassLoader、RunnableAdapter和Task类下的TaskCallable。

2.实现Callable接口的重要类分析

接下来,分析的类主要有:PrivilegedCallable、PrivilegedCallableUsingCurrentClassLoader、RunnableAdapter和Task类下的TaskCallable。虽然这些类在实际工作中很少被直接用到,但是作为一名合格的开发工程师,设置是秃顶的资深专家来说,了解并掌握这些类的实现有助你进一步理解Callable接口,并提高专业技能(头发再掉一批,哇哈哈哈。。。)。

  • PrivilegedCallable

PrivilegedCallable类是Callable接口的一个特殊实现类,它表明Callable对象有某种特权来访问系统的某种资源,PrivilegedCallable类的源代码如下所示。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
java复制代码/**
* A callable that runs under established access control settings
*/
static final class PrivilegedCallable<T> implements Callable<T> {
private final Callable<T> task;
private final AccessControlContext acc;

PrivilegedCallable(Callable<T> task) {
this.task = task;
this.acc = AccessController.getContext();
}

public T call() throws Exception {
try {
return AccessController.doPrivileged(
new PrivilegedExceptionAction<T>() {
public T run() throws Exception {
return task.call();
}
}, acc);
} catch (PrivilegedActionException e) {
throw e.getException();
}
}
}

从PrivilegedCallable类的源代码来看,可以将PrivilegedCallable看成是对Callable接口的封装,并且这个类也继承了Callable接口。

在PrivilegedCallable类中有两个成员变量,分别是Callable接口的实例对象和AccessControlContext类的实例对象,如下所示。

1
2
java复制代码private final Callable<T> task;
private final AccessControlContext acc;

其中,AccessControlContext类可以理解为一个具有系统资源访问决策的上下文类,通过这个类可以访问系统的特定资源。通过类的构造方法可以看出,在实例化AccessControlContext类的对象时,只需要传递Callable接口子类的对象即可,如下所示。

1
2
3
4
java复制代码PrivilegedCallable(Callable<T> task) {
this.task = task;
this.acc = AccessController.getContext();
}

AccessControlContext类的对象是通过AccessController类的getContext()方法获取的,这里,查看AccessController类的getContext()方法,如下所示。

1
2
3
4
5
6
7
8
java复制代码public static AccessControlContext getContext(){
AccessControlContext acc = getStackAccessControlContext();
if (acc == null) {
return new AccessControlContext(null, true);
} else {
return acc.optimize();
}
}

通过AccessController的getContext()方法可以看出,首先通过getStackAccessControlContext()方法来获取AccessControlContext对象实例。如果获取的AccessControlContext对象实例为空,则通过调用AccessControlContext类的构造方法实例化,否则,调用AccessControlContext对象实例的optimize()方法返回AccessControlContext对象实例。

这里,我们先看下getStackAccessControlContext()方法是个什么鬼。

1
java复制代码private static native AccessControlContext getStackAccessControlContext();

原来是个本地方法,方法的字面意思就是获取能够访问系统栈的决策上下文对象。

接下来,我们回到PrivilegedCallable类的call()方法,如下所示。

1
2
3
4
5
6
7
8
9
10
11
12
java复制代码public T call() throws Exception {
try {
return AccessController.doPrivileged(
new PrivilegedExceptionAction<T>() {
public T run() throws Exception {
return task.call();
}
}, acc);
} catch (PrivilegedActionException e) {
throw e.getException();
}
}

通过调用AccessController.doPrivileged()方法,传递PrivilegedExceptionAction。接口对象和AccessControlContext对象,并最终返回泛型的实例对象。

首先,看下AccessController.doPrivileged()方法,如下所示。

1
2
3
4
5
java复制代码@CallerSensitive
public static native <T> T
doPrivileged(PrivilegedExceptionAction<T> action,
AccessControlContext context)
throws PrivilegedActionException;

可以看到,又是一个本地方法。也就是说,最终的执行情况是将PrivilegedExceptionAction接口对象和AccessControlContext对象实例传递给这个本地方法执行。并且在PrivilegedExceptionAction接口对象的run()方法中调用Callable接口的call()方法来执行最终的业务逻辑,并且返回泛型对象。

  • PrivilegedCallableUsingCurrentClassLoader

此类表示为在已经建立的特定访问控制和当前的类加载器下运行的Callable类,源代码如下所示。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
java复制代码/**
* A callable that runs under established access control settings and
* current ClassLoader
*/
static final class PrivilegedCallableUsingCurrentClassLoader<T> implements Callable<T> {
private final Callable<T> task;
private final AccessControlContext acc;
private final ClassLoader ccl;

PrivilegedCallableUsingCurrentClassLoader(Callable<T> task) {
SecurityManager sm = System.getSecurityManager();
if (sm != null) {
sm.checkPermission(SecurityConstants.GET_CLASSLOADER_PERMISSION);
sm.checkPermission(new RuntimePermission("setContextClassLoader"));
}
this.task = task;
this.acc = AccessController.getContext();
this.ccl = Thread.currentThread().getContextClassLoader();
}

public T call() throws Exception {
try {
return AccessController.doPrivileged(
new PrivilegedExceptionAction<T>() {
public T run() throws Exception {
Thread t = Thread.currentThread();
ClassLoader cl = t.getContextClassLoader();
if (ccl == cl) {
return task.call();
} else {
t.setContextClassLoader(ccl);
try {
return task.call();
} finally {
t.setContextClassLoader(cl);
}
}
}
}, acc);
} catch (PrivilegedActionException e) {
throw e.getException();
}
}
}

这个类理解起来比较简单,首先,在类中定义了三个成员变量,如下所示。

1
2
3
java复制代码private final Callable<T> task;
private final AccessControlContext acc;
private final ClassLoader ccl;

接下来,通过构造方法注入Callable对象,在构造方法中,首先获取系统安全管理器对象实例,通过系统安全管理器对象实例检查是否具有获取ClassLoader和设置ContextClassLoader的权限。并在构造方法中为三个成员变量赋值,如下所示。

1
2
3
4
5
6
7
8
9
10
java复制代码PrivilegedCallableUsingCurrentClassLoader(Callable<T> task) {
SecurityManager sm = System.getSecurityManager();
if (sm != null) {
sm.checkPermission(SecurityConstants.GET_CLASSLOADER_PERMISSION);
sm.checkPermission(new RuntimePermission("setContextClassLoader"));
}
this.task = task;
this.acc = AccessController.getContext();
this.ccl = Thread.currentThread().getContextClassLoader();
}

接下来,通过调用call()方法来执行具体的业务逻辑,如下所示。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
java复制代码public T call() throws Exception {
try {
return AccessController.doPrivileged(
new PrivilegedExceptionAction<T>() {
public T run() throws Exception {
Thread t = Thread.currentThread();
ClassLoader cl = t.getContextClassLoader();
if (ccl == cl) {
return task.call();
} else {
t.setContextClassLoader(ccl);
try {
return task.call();
} finally {
t.setContextClassLoader(cl);
}
}
}
}, acc);
} catch (PrivilegedActionException e) {
throw e.getException();
}
}

在call()方法中同样是通过调用AccessController类的本地方法doPrivileged,传递PrivilegedExceptionAction接口的实例对象和AccessControlContext类的对象实例。

具体执行逻辑为:在PrivilegedExceptionAction对象的run()方法中获取当前线程的ContextClassLoader对象,如果在构造方法中获取的ClassLoader对象与此处的ContextClassLoader对象是同一个对象(不止对象实例相同,而且内存地址也相同),则直接调用Callable对象的call()方法返回结果。否则,将PrivilegedExceptionAction对象的run()方法中的当前线程的ContextClassLoader设置为在构造方法中获取的类加载器对象,接下来,再调用Callable对象的call()方法返回结果。最终将当前线程的ContextClassLoader重置为之前的ContextClassLoader。

  • RunnableAdapter

RunnableAdapter类比较简单,给定运行的任务和结果,运行给定的任务并返回给定的结果,源代码如下所示。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
java复制代码/**
* A callable that runs given task and returns given result
*/
static final class RunnableAdapter<T> implements Callable<T> {
final Runnable task;
final T result;
RunnableAdapter(Runnable task, T result) {
this.task = task;
this.result = result;
}
public T call() {
task.run();
return result;
}
}
  • TaskCallable

TaskCallable类是javafx.concurrent.Task类的静态内部类,TaskCallable类主要是实现了Callable接口并且被定义为FutureTask的类,并且在这个类中允许我们拦截call()方法来更新task任务的状态。源代码如下所示。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
java复制代码private static final class TaskCallable<V> implements Callable<V> {

private Task<V> task;
private TaskCallable() { }

@Override
public V call() throws Exception {
task.started = true;
task.runLater(() -> {
task.setState(State.SCHEDULED);
task.setState(State.RUNNING);
});
try {
final V result = task.call();
if (!task.isCancelled()) {
task.runLater(() -> {
task.updateValue(result);
task.setState(State.SUCCEEDED);
});
return result;
} else {
return null;
}
} catch (final Throwable th) {
task.runLater(() -> {
task._setException(th);
task.setState(State.FAILED);
});
if (th instanceof Exception) {
throw (Exception) th;
} else {
throw new Exception(th);
}
}
}
}

从TaskCallable类的源代码可以看出,只定义了一个Task类型的成员变量。下面主要分析TaskCallable类的call()方法。

当程序的执行进入到call()方法时,首先将task对象的started属性设置为true,表示任务已经开始,并且将任务的状态依次设置为State.SCHEDULED和State.RUNNING,依次触发任务的调度事件和运行事件。如下所示。

1
2
3
4
5
java复制代码task.started = true;
task.runLater(() -> {
task.setState(State.SCHEDULED);
task.setState(State.RUNNING);
});

接下来,在try代码块中执行Task对象的call()方法,返回泛型对象。如果任务没有被取消,则更新任务的缓存,将调用call()方法返回的泛型对象绑定到Task对象中的ObjectProperty对象中,其中,ObjectProperty在Task类中的定义如下。

1
java复制代码private final ObjectProperty<V> value = new SimpleObjectProperty<>(this, "value");

接下来,将任务的状态设置为成功状态。如下所示。

1
2
3
4
5
6
7
8
9
10
11
12
java复制代码try {
final V result = task.call();
if (!task.isCancelled()) {
task.runLater(() -> {
task.updateValue(result);
task.setState(State.SUCCEEDED);
});
return result;
} else {
return null;
}
}

如果程序抛出了异常或者错误,会进入catch()代码块,设置Task对象的Exception信息并将状态设置为State.FAILED,也就是将任务标记为失败。接下来,判断异常或错误的类型,如果是Exception类型的异常,则直接强转为Exception类型的异常并抛出。否则,将异常或者错误封装为Exception对象并抛出,如下所示。

1
2
3
4
5
6
7
8
9
10
11
java复制代码catch (final Throwable th) {
task.runLater(() -> {
task._setException(th);
task.setState(State.FAILED);
});
if (th instanceof Exception) {
throw (Exception) th;
} else {
throw new Exception(th);
}
}

记住:你比别人强的地方,不是你做过多少年的CRUD工作,而是你比别人掌握了更多深入的技能。不要总停留在CRUD的表面工作,理解并掌握底层原理并熟悉源码实现,并形成自己的抽象思维能力,做到灵活运用,才是你突破瓶颈,脱颖而出的重要方向!

最后,作为一名合格(发际线比较高)的开发人员或者资深(秃顶)的工程师和架构师来说,理解原理和掌握源码,并形成自己的抽象思维能力,灵活运用是你必须掌握的技能。

好了,今天就到这儿吧,我是冰河,我们下期见~~

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Java的类加载器和类加载机制详解 1 类加载机制 2 类与

发表于 2021-11-07

「这是我参与11月更文挑战的第7天,活动详情查看:2021最后一次更文挑战」。

Java的Class(类)加载器,主要工作在Class加载的“加载阶段”,其主要作用是从系统外部获取Class的二进制数据流。

关于Class(类)的文件结构:Java的 Class(类)文件结构详解;关于Class的加载过程:Java的Class(类)加载过程详解。

1 类加载机制

类加载机制主要有如下3种:

  1. 全盘负责:所谓全盘负责,就是当一个类加载器负责加载某个Class时,该Class所依赖和引用其他Class也将由该类加载器负责载入,除非显示使用另外一个类加载器来载入。
  2. 双亲委派:所谓的双亲委派,则是先让父类加载器试图加载该Class,只有在父类加载器无法加载该类时才尝试从自己的类路径中加载该类。通俗的讲,就是某个特定的类加载器在接到加载类的请求时,首先将加载任务委托给父加载器,依次递归,如果父加载器可以完成类加载任务,就成功返回;只有父加载器无法完成此加载任务时,才自己去加载。
  3. 缓存机制:缓存机制将会保证所有加载过的Class都会被缓存,当程序中需要使用某个Class时,类加载器先从缓存区中搜寻该Class,只有当缓存区中不存在该Class对象时,系统才会读取该类对应的二进制数据,并将其转换成Class对象,存入缓冲区中。这就是为很么修改了Class后,必须重新启动JVM,程序所做的修改才会生效的原因。

Java的JVM在加载类时默认采用的是双亲委派机制和缓存机制。

2 类与类加载器

对于任意一个类,都需要由加载它的类加载器和这个类本身一同确立其在 Java 虚拟机的唯一性。每个类加载器都拥有一个独立的类名称空间。也就是说:比较两个类是否「相等」,只要在这两个类是由同一个类加载器加载的前提下才有意义,否则,即使这两个类来源于同一个 Class 文件,被同一个虚拟机加载,只要加载它们的类加载器不同,那这两个类就必定不相等。

这里所指的”相等”,包括代表类的Class对象的equals()方法、isAssignableFrom()方法、islnstance()方法的返回结果,也包括使用instanceof关键字做对象所属关系判定等情况。如果没有注意到类加载器的影响,在某些情况下可能会产生具有迷惑性的结果,下面代码演示了不同的类加载器对instanceof关键字运算的结果的影响。

如下案例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
java复制代码public class ClassLoaderTest {
public static void main(String[] args) throws Exception {
//自定义类加载器
ClassLoader myLoader = new ClassLoader() {
@Override
public Class<?> loadClass(String name)
throws ClassNotFoundException {
try {
String fileName = name.substring(name.lastIndexOf(".") + 1)
+ ".class";
InputStream is = getClass().getResourceAsStream(fileName);
if (is == null) {
return super.loadClass(name);
}
byte[] b = new byte[is.available()];
is.read(b);
return defineClass(name, b, 0, b.length);
} catch (IOException e) {
throw new ClassNotFoundException(name);
}
}
};
//使用自定义类加载器来加载类ClassLoaderTest
Class newClass = myLoader.loadClass("com.ikang.JVM.classloader.ClassLoaderTest");
//反射获取实例
Object obj1 = newClass.newInstance();
//获取实例所属的类全路径名
System.out.println(obj1.getClass());


//原生的类加载器来加载类ClassLoaderTest
Class<?> oldClass = ClassLoaderTest.class.getClassLoader().loadClass("com.ikang.JVM.classloader.ClassLoaderTest");
//反射获取实例
Object obj2 = oldClass.newInstance();
//获取实利所属的类全路径名
System.out.println(obj2.getClass());


//直接获取ClassLoaderTest类的类全路径名
System.out.println(ClassLoaderTest.class);


/*从上面的上个输出中,开起来它们属于同一个类,但是实际上并不是,如下判断*/
System.out.println(obj1 instanceof ClassLoaderTest);
System.out.println(obj2 instanceof ClassLoaderTest);
}
}

3 类加载器种类

3.1 虚拟机规范的角度

根据《Java虚拟机规范 javaSE8》,Java虚拟机支持两种不同的类加载器:

  1. 引导类加载器(Bootstrap ClassLoader):它使用C++实现(这里仅限于HotSpot,也就是JDK1.5之后默认的虚拟机,有很多其他的虚拟机是用Java语言实现的),是虚拟机自身的一部分。
  2. 所有其他的类加载器:这些类加载器都由Java语言实现,独立于虚拟机之外,并且全部继承自抽象类java.lang.ClassLoader,这些类加载器需要由引导类加载器加载到内存中之后才能去加载其他的类。

3.1 开发人员的角度

站在Java开发人员的角度来看,类加载器可以大致划分为以下三类:

3.1.1 启动类加载器

Bootstrap ClassLoader,也称根类加载器/引导类加载器。它是在Java虚拟机启动后初始化的,用来加载 Java 的核心类库, 例如< JAVA_HOME >/jre/lib/rt.jar里的所有class,比如java.time.、java.util.、java.nio.、java.lang.、java.text.、java.sql.、java.math、JUC包等各种自带的类库。

启动类加载器无法被Java程序直接引用,用户在编写自定义类加载器时,如果需要把加载请求委派给启动类加载器,那直接使用null代替即可。

下面程序可以获得启动加载器所加载的核心类库,并会看到本机安装的Java环境变量指定的JDK中提供的核心jar包路径:

1
2
3
4
5
java复制代码URL[] urls = Launcher.getBootstrapClassPath().getURLs();
Arrays.stream(urls).forEach(System.out::println);
/*for (URL url : urls) {
System.out.println(url.toExternalForm());
}*/

本人机器输出如下:

1
2
3
4
5
6
7
8
java复制代码file:/C:/Program%20Files/Java/jdk1.8.0_144/jre/lib/resources.jar
file:/C:/Program%20Files/Java/jdk1.8.0_144/jre/lib/rt.jar
file:/C:/Program%20Files/Java/jdk1.8.0_144/jre/lib/sunrsasign.jar
file:/C:/Program%20Files/Java/jdk1.8.0_144/jre/lib/jsse.jar
file:/C:/Program%20Files/Java/jdk1.8.0_144/jre/lib/jce.jar
file:/C:/Program%20Files/Java/jdk1.8.0_144/jre/lib/charsets.jar
file:/C:/Program%20Files/Java/jdk1.8.0_144/jre/lib/jfr.jar
file:/C:/Program%20Files/Java/jdk1.8.0_144/jre/classes

3.1.2 扩展类加载器

Extension ClassLoader,它用来加载 Java 的扩展库。该加载器由sun.misc.Launcher$ExtClassLoader实现,它负责加载< JAVA_HOME >\jre\lib\ext目录中,或者由java.ext.dirs系统变量指定的路径中的所有类库(如javax.*开头的类),开发者可以直接使用扩展类加载器。它的父加载器为null,并且是使用java语言实现的。

获取扩展类加载器加载的jar包:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
java复制代码public static void main(String[] args) {
File[] dirs = getExtClassLoaderFile();
//递归展示jar包
for (File dir : dirs) {
showFile(dir.getPath());
}
}

/**
* ExtClassLoader类中获取路径的代码
* 加载<JAVA_HOME>/lib/ext目录中的类库目录
*/
static File[] getExtClassLoaderFile() {
String s = System.getProperty("java.ext.dirs");
File[] dirs;
if (s != null) {
StringTokenizer st =
new StringTokenizer(s, File.pathSeparator);
int count = st.countTokens();
dirs = new File[count];
for (int i = 0; i < count; i++) {
dirs[i] = new File(st.nextToken());
}
} else {
dirs = new File[0];
}
return dirs;
}

/**
* 递归展示jar包
*/
static void showFile(String path) {
File file = new File(path);
if (file.exists()) {
if (file.isFile()) {
if (file.getName().endsWith(".jar")) {
System.out.println(file.getAbsolutePath());
return;
}
}
File[] files = file.listFiles();
for (File file1 : files) {
if (file1.isFile()) {
if (file1.getName().endsWith(".jar")) {
System.out.println(file1.getAbsolutePath());
}
} else {
showFile(file1.getPath());
}
}
}
}

3.1.3 应用类加载器

Application ClassLoader,也称系统类加载器。这个类加载器由 sun.misc.Launcher$AppClassLoader 实现。

这个类加载器是 ClassLoader 中的getSystemClassLoader() 方法的返回值,所以一般称它为系统类加载器。它负责加载用户路径(ClassPath)上所指定的类库,开发者可以使用这个类加载器,如果应用程序没有自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器。它的父加载器是 ExtClassLoader。

1
2
java复制代码ClassLoader systemClassLoader = ClassLoader.getSystemClassLoader();
System.out.println(systemClassLoader);

3.1.4 自定义类加载器

我们的应用程序都是由这3中类加载器互相配合进行加载的,如果有必要,还可以加入自己定义的类加载器。开发人员可以通过继承 java.lang.ClassLoader类,重写findClass()方法,调用defineClass()方法的方式实现自己的类加载器,以满足一些特殊的需求。

为什么会有自定义类加载器?

  1. 一方面是由于java代码很容易被反编译,如果需要对自己的代码加密的话,可以对编译后的代码进行加密,然后再通过实现自己的自定义类加载器进行解密,最后再加载。
  2. 另一方面也有可能从非标准的来源加载代码,比如从网络来源,那就需要自己实现一个类加载器,从指定源进行加载。

4 双亲委派模型

上面这些类加载器之间的关系一般如下图所示:

在这里插入图片描述

上图中的类加载器之间的这种层次关系,称为类加载器的双亲委派模型。双亲委派模型要求除了顶层的启动类加载器,其余的类加载器都应该有自己的父类加载器。这里类加载器之间的父子关系一般不会以继承关系来实现,而是使用组合关系来复用父加载器的代码。

双亲委托模型的工作过程是:如果一个类加载器收到了类加载器的请求,它首先不会自己尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的加载请求最终都应该传送到顶层的启动类加载器中,只有当父类加载器反馈自己无法完成这个加载请求(它的搜索范围中没有找到所需的类时,子加载类才会尝试自己去加载)。

4.1 双亲委派机制的优势

采用双亲委派模式的是好处是Java类随着它的类加载器一起具备了一种带有优先级的层次关系,通过这种层级关可以避免类的重复加载,当父亲已经加载了该类时,就没有必要子ClassLoader再加载一次。

其次是考虑到安全因素,java核心api中定义类型不会被随意替换,假设通过网络传递一个名为java.lang.Integer的类,通过双亲委托模式传递到启动类加载器,而启动类加载器在核心Java API发现这个名字的类,发现该类已被加载,并不会重新加载网络传递的过来的java.lang.Integer,而直接返回已加载过的Integer.class,这样便可以防止核心API库被随意篡改。

4.2 双亲委派模型的实现

双亲委派模型对于保证Java 程序的稳定性很重要,但它的实现却非常简单,实现双亲委派的代码都集中在java.lang.ClassLoader 的 loadClass() 方法中:先检查类是否被加载过,若没有则调用父加载器的loadClass() 方法,若父加载器为空则默认使用启动类加载器作为父加载器。如果父加载器失败,抛出 ClassNotFoundException 异常后,再调用自己的 finClass() 方法进行加载。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
java复制代码protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// 先检查是否被加载过
Class<?> c = findLoadedClass(name);
//如果没被加载过
if (c == null) {
long t0 = System.nanoTime();
try {
//如果父加载器不为null,则首先让父加载器尝试加载该类
if (parent != null) {
c = parent.loadClass(name, false);
} else {
//否则,使用dBootstrapClassloader来加载
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
}
//父类没有加载成功
if (c == null) {
// If still not found, then invoke findClass in order
// to find the class.
long t1 = System.nanoTime();
//尝试子类加载器加载
c = findClass(name);

// this is the defining class loader; record the stats
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
}
//根据条件判断是否进行链接操作
if (resolve) {
resolveClass(c);
}
return c;
}
}

5 破坏双亲委派模型

双亲委派模型并不是一个强制性的约束模型,而是Java设计者推荐给开发者的类加载器实现方式。在Java的世界中大部分的类加载器都遵循这个模型,但也有例外,到目前为止,双亲委派模型主要出现过3较大规模的“被破坏”情况。

双亲委派模型的第一次“被破坏” 其实发生在双亲委派模型出现之前–即JDK1.2发布之前。由于双亲委派模型是在JDK1.2之后才被引入的,而类加载器和抽象类java.lang.ClassLoader则是JDK1.0时候就已经存在,面对已经存在 的用户自定义类加载器的实现代码,Java设计者引入双亲委派模型时不得不做出一些妥协。为了向前兼容,JDK1.2之后的java.lang.ClassLoader添加了一个新的proceted方法findClass(),在此之前,用户去继承java.lang.ClassLoader的唯一目的就是重写loadClass()方法,因为虚拟在进行类加载的时候会调用加载器的私有方法loadClassInternal(),而这个方法的唯一逻辑就是去调用自己的loadClass()。JDK1.2之后已不再提倡用户再去覆盖loadClass()方法,应当把自己的类加载逻辑写到findClass()方法中,在loadClass()方法的逻辑里,如果父类加载器加载失败,则会调用自己的findClass()方法来完成加载,这样就可以保证新写出来的类加载器是符合双亲委派模型的。

双亲委派模型的第二次“被破坏” 是这个模型自身的缺陷所导致的,双亲委派模型很好地解决了各个类加载器的基础类统一问题(越基础的类由越上层的加载器进行加载),基础类之所以被称为“基础”,是因为它们总是作为被调用代码调用的API。但是,如果基础类又要调用用户的代码,那该怎么办呢。

这并非是不可能的事情,一个典型的例子便是JNDI服务,它的代码由启动类加载器去加载(在JDK1.3时放进rt.jar),但JNDI的目的就是对资源进行集中管理和查找,它需要调用独立厂商实现部部署在应用程序的classpath下的JNDI接口提供者(SPI, Service Provider Interface)的代码,但启动类加载器不可能“认识”之些代码,该怎么办?

为了解决这个困境,Java设计团队只好引入了一个不太优雅的设计:线程上下文件类加载器(Thread Context ClassLoader)。这个类加载器可以通过java.lang.Thread类的setContextClassLoader()方法进行设置,如果创建线程时还未设置,它将会从父线程中继承一个;如果在应用程序的全局范围内都没有设置过,那么这个类加载器默认就是应用程序类加载器。有了线程上下文类加载器,JNDI服务使用这个线程上下文类加载器去加载所需要的SPI代码,也就是父类加载器请求子类加载器去完成类加载动作,这种行为实际上就是打通了双亲委派模型的层次结构来逆向使用类加载器,已经违背了双亲委派模型,但这也是无可奈何的事情。Java中所有涉及SPI的加载动作基本上都采用这种方式,例如JNDI,JDBC,JCE,JAXB和JBI等。

双亲委派模型的第三次“被破坏” 是由于用户对程序的动态性的追求导致的,例如OSGi的出现。在OSGi环境下,类加载器不再是双亲委派模型中的树状结构,而是进一步发展为网状结构。

相关文章:

  1. 《Java虚拟机规范》
  2. 《深入理解Java虚拟机》

如果有什么不懂或者需要交流,可以留言。另外希望点赞、收藏、关注,我将不间断更新各种Java学习博客!

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

SpringCloud升级之路20200x版-29Sp

发表于 2021-11-07

这是我参与11月更文挑战的第7天,活动详情查看:2021最后一次更文挑战

本系列代码地址:github.com/JoJoTec/spr…

在使用云原生的很多微服务中,比较小规模的可能直接依靠云服务中的负载均衡器进行内部域名与服务映射,通过健康检查接口判断实例健康状态,然后直接使用 OpenFeign 生成对应域名的 Feign Client。Spring Cloud 生态中,对 OpenFeign 进行了封装,其中的 Feign Client 的各个组件,也是做了一定的定制化,可以实现在 OpenFeign Client 中集成服务发现与负载均衡。在此基础上,我们还结合了 Resilience4J 组件,实现了微服务实例级别的线程隔离,微服务方法级别的断路器以及重试。

我们先来分析下 Spring Cloud OpenFeign

Spring Cloud OpenFeign 解析

从 NamedContextFactory 入手

Spring Cloud OpenFeign 的 github 地址:github.com/spring-clou…

首先,根据我们之前分析 spring-cloud-loadbalancer 的流程,我们先从继承 NamedContextFactory 的类入手,这里是 FeignContext,通过其构造函数,得到其中的默认配置类:

FeignContext.java

1
2
3
csharp复制代码public FeignContext() {
super(FeignClientsConfiguration.class, "feign", "feign.client.name");
}

从构造方法可以看出,默认的配置类是:FeignClientsConfiguration。我们接下来详细分析这个配置类中的元素,并与我们之前分析的 OpenFeign 的组件结合起来。

负责解析类元数据的 Contract,与 spring-web 的 HTTP 注解相结合

为了开发人员更好上手使用和理解,最好能实现使用 spring-web 的 HTTP 注解(例如 @RequestMapping,@GetMapping 等等)去定义 FeignClient 接口。在 FeignClientsConfiguration 中就是这么做的:

FeignClientsConfiguration.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
typescript复制代码@Autowired(required = false)
private FeignClientProperties feignClientProperties;

@Autowired(required = false)
private List<AnnotatedParameterProcessor> parameterProcessors = new ArrayList<>();

@Autowired(required = false)
private List<FeignFormatterRegistrar> feignFormatterRegistrars = new ArrayList<>();

@Bean
@ConditionalOnMissingBean
public Contract feignContract(ConversionService feignConversionService) {
boolean decodeSlash = feignClientProperties == null || feignClientProperties.isDecodeSlash();
return new SpringMvcContract(this.parameterProcessors, feignConversionService, decodeSlash);
}

@Bean
public FormattingConversionService feignConversionService() {
FormattingConversionService conversionService = new DefaultFormattingConversionService();
for (FeignFormatterRegistrar feignFormatterRegistrar : this.feignFormatterRegistrars) {
feignFormatterRegistrar.registerFormatters(conversionService);
}
return conversionService;
}

其核心提供的 Feign 的 Contract 就是 SpringMvcContract,SpringMvcContract 主要包含两部分核心逻辑:

  • 定义 Feign Client 专用的 Formatter 与 Converter 注册
  • 使用 AnnotatedParameterProcessor 来解析 SpringMVC 注解以及我们自定义的注解

定义 Feign Client 专用的 Formatter 与 Converter 注册

首先,Spring 提供了类型转换机制,其中单向的类型转换为实现 Converter 接口;在 web 应用中,我们经常需要将前端传入的字符串类型的数据转换成指定格式或者指定数据类型来满足我们调用需求,同样的,后端开发也需要将返回数据调整成指定格式或者指定类型返回到前端页面(在 Spring Boot 中已经帮我们做了从 json 解析和返回对象转化为 json,但是某些特殊情况下,比如兼容老项目接口,我们还可能使用到),这个是通过实现 Formatter 接口实现。举一个简单的例子:

定义一个类型:

1
2
3
4
5
6
less复制代码@Data
@AllArgsConstructor
public class Student {
private final Long id;
private final String name;
}

我们定义可以通过字符串解析出这个类的对象的 Converter,例如 “1,zhx” 就代表 id = 1 并且 name = zhx:

1
2
3
4
5
6
7
8
9
typescript复制代码public class StringToStudentConverter implements Converter<String, Student> {
@Override
public Student convert(String from) {
String[] split = from.split(",");
return new Student(
Long.parseLong(split[0]),
split[1]);
}
}

然后将这个 Converter 注册:

1
2
3
4
5
6
7
typescript复制代码@Configuration(proxyBeanMethods = false)
public class TestConfig implements WebMvcConfigurer {
@Override
public void addFormatters(FormatterRegistry registry) {
registry.addConverter(new StringToStudentConverter());
}
}

编写一个测试接口:

1
2
3
4
5
6
7
8
less复制代码@RestController
@RequestMapping("/test")
public class TestController {
@GetMapping("/string-to-student")
public Student stringToStudent(@RequestParam("student") Student student) {
return student;
}
}

调用 /test/string-to-student?student=1,zhx,可以看到返回:

1
2
3
4
json复制代码{
"id": 1,
"name": "zhx"
}

同样的,我们也可以通过 Formatter 实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
typescript复制代码public class StudentFormatter implements Formatter<Student> {
@Override
public Student parse(String text, Locale locale) throws ParseException {
String[] split = text.split(",");
return new Student(
Long.parseLong(split[0]),
split[1]);
}

@Override
public String print(Student object, Locale locale) {
return object.getId() + "," + object.getName();
}
}

然后将这个 Formatter 注册:

1
2
3
4
5
6
7
typescript复制代码@Configuration(proxyBeanMethods = false)
public class TestConfig implements WebMvcConfigurer {
@Override
public void addFormatters(FormatterRegistry registry) {
registry.addFormatter(new StudentFormatter());
}
}

Feign 也提供了这个注册机制,为了和 spring-webmvc 的注册机制区分开,使用了 FeignFormatterRegistrar 继承了 FormatterRegistrar 接口。然后通过定义 FormattingConversionService 这个 Bean 实现 Formatter 和 Converter 的注册。例如:

假设我们有另一个微服务需要通过 FeignClient 调用上面这个接口,那么就需要定义一个 FeignFormatterRegistrar 将 Formatter 注册进去:

1
2
3
4
5
6
typescript复制代码@Bean
public FeignFormatterRegistrar getFeignFormatterRegistrar() {
return registry -> {
registry.addFormatter(new StudentFormatter());
};
}

之后我们定义 FeignClient:

1
2
3
4
5
less复制代码@FeignClient(name = "test-server", contextId = "test-server")
public interface TestClient {
@GetMapping("/test/string-to-student")
Student get(@RequestParam("student") Student student);
}

在调用 get 方法时,会调用 StudentFormatter 的 print 将 Student 对象输出为格式化的字符串,例如 {"id": 1,"name": "zhx"} 会变成 1,zhx。

AnnotatedParameterProcessor 来解析 SpringMVC 注解以及我们自定义的注解

AnnotatedParameterProcessor 是用来将注解解析成 AnnotatedParameterContext 的 Bean,AnnotatedParameterContext 包含了 Feign 的请求定义,包括例如前面提到的 Feign 的 MethodMetadata 即方法元数据。默认的 AnnotatedParameterProcessor 包括所有 SpringMVC 对于 HTTP 方法定义的注解对应的解析,例如 @RequestParam 注解对应的 RequestParamParameterProcessor:

RequestParamParameterProcessor.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
scss复制代码public boolean processArgument(AnnotatedParameterContext context, Annotation annotation, Method method) {
//获取当前参数属于方法的第几个
int parameterIndex = context.getParameterIndex();
//获取参数类型
Class<?> parameterType = method.getParameterTypes()[parameterIndex];
//要保存的解析的方法元数据 MethodMetadata
MethodMetadata data = context.getMethodMetadata();

//如果是 Map,则指定 queryMap 下标,直接返回
//这代表一旦使用 Map 作为 RequestParam,则其他的 RequestParam 就会被忽略,直接解析 Map 中的参数作为 RequestParam
if (Map.class.isAssignableFrom(parameterType)) {
checkState(data.queryMapIndex() == null, "Query map can only be present once.");
data.queryMapIndex(parameterIndex);
//返回解析成功
return true;
}
RequestParam requestParam = ANNOTATION.cast(annotation);
String name = requestParam.value();
//RequestParam 的名字不能是空
checkState(emptyToNull(name) != null, "RequestParam.value() was empty on parameter %s", parameterIndex);
context.setParameterName(name);

Collection<String> query = context.setTemplateParameter(name, data.template().queries().get(name));
//将 RequestParam 放入 方法元数据 MethodMetadata
data.template().query(name, query);
//返回解析成功
return true;
}

我们也可以实现 AnnotatedParameterProcessor 来自定义我们的注解,配合 SpringMVC 的注解一起使用去定义 FeignClient

HTTP 编码解码器,与 spring-boot 中的编码解码器相结合

Spring Cloud 中的任何组件,都是基于 Spring Boot 而实现的。由于 Spring Boot 中已经有了 HTTP 编码解码器,就可以不用单独给 OpenFeign 单独再实现 HTTP 编码解码器了,而是考虑将 OpenFeign 的编码解码器接口用 Spring Boot 的 HTTP 编码解码器实现。

在 FeignClientsConfiguration 中,提供了默认的实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
typescript复制代码//由于初始化顺序以及 NamedContextFactory 的 Configuration 初始化的原因,这里需要注入 ObjectFactory 而不是直接注入 HttpMessageConverters 防止找不到 Bean
@Autowired
private ObjectFactory<HttpMessageConverters> messageConverters;

@Bean
@ConditionalOnMissingBean
public Decoder feignDecoder() {
return new OptionalDecoder(new ResponseEntityDecoder(new SpringDecoder(this.messageConverters)));
}

@Bean
@ConditionalOnMissingBean
@ConditionalOnMissingClass("org.springframework.data.domain.Pageable")
public Encoder feignEncoder(ObjectProvider<AbstractFormWriter> formWriterProvider) {
return springEncoder(formWriterProvider, encoderProperties);
}

@Bean
@ConditionalOnClass(name = "org.springframework.data.domain.Pageable")
@ConditionalOnMissingBean
public Encoder feignEncoderPageable(ObjectProvider<AbstractFormWriter> formWriterProvider) {
PageableSpringEncoder encoder = new PageableSpringEncoder(springEncoder(formWriterProvider, encoderProperties));

if (springDataWebProperties != null) {
encoder.setPageParameter(springDataWebProperties.getPageable().getPageParameter());
encoder.setSizeParameter(springDataWebProperties.getPageable().getSizeParameter());
encoder.setSortParameter(springDataWebProperties.getSort().getSortParameter());
}
return encoder;
}

@Bean
@ConditionalOnClass(name = "org.springframework.data.domain.Pageable")
@ConditionalOnMissingBean
public QueryMapEncoder feignQueryMapEncoderPageable() {
return new PageableSpringQueryMapEncoder();
}

微信搜索“我的编程喵”关注公众号,每日一刷,轻松提升技术,斩获各种offer

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

1…404405406…956

开发者博客

9558 日志
1953 标签
RSS
© 2025 开发者博客
本站总访问量次
由 Hexo 强力驱动
|
主题 — NexT.Muse v5.1.4
0%