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

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


  • 首页

  • 归档

  • 搜索

吊炸天的 Kafka 图形化工具 Eagle,必须推荐给你!

发表于 2021-06-08

SpringBoot实战电商项目mall(40k+star)地址:github.com/macrozheng/…

摘要

Kafka是当下非常流行的消息中间件,据官网透露,已有成千上万的公司在使用它。最近实践了一波Kafka,确实很好很强大。今天我们来从三个方面学习下Kafka:Kafaka在Linux下的安装,Kafka的可视化工具,Kafka和SpringBoot结合使用。希望大家看完后能快速入门Kafka,掌握这个流行的消息中间件!

Kafka简介

Kafka是由LinkedIn公司开发的一款开源分布式消息流平台,由Scala和Java编写。主要作用是为处理实时数据提供一个统一、高吞吐、低延迟的平台,其本质是基于发布订阅模式的消息引擎系统。

Kafka具有以下特性:

  • 高吞吐、低延迟:Kafka收发消息非常快,使用集群处理消息延迟可低至2ms。
  • 高扩展性:Kafka可以弹性地扩展和收缩,可以扩展到上千个broker,数十万个partition,每天处理数万亿条消息。
  • 永久存储:Kafka可以将数据安全地存储在分布式的,持久的,容错的群集中。
  • 高可用性:Kafka在可用区上可以有效地扩展群集,某个节点宕机,集群照样能够正常工作。

Kafka安装

我们将采用Linux下的安装方式,安装环境为CentOS 7.6。此处没有采用Docker来安装部署,个人感觉直接安装更简单(主要是官方没提供Docker镜像)!

  • 首先我们需要下载Kafka的安装包,下载地址:mirrors.bfsu.edu.cn/apache/kafk…

  • 下载完成后将Kafka解压到指定目录:
1
2
bash复制代码cd /mydata/kafka/
tar -xzf kafka_2.13-2.8.0.tgz
  • 解压完成后进入到解压目录:
1
bash复制代码cd kafka_2.13-2.8.0
  • 虽然有消息称Kafka即将移除Zookeeper,但是在Kafka最新版本中尚未移除,所以启动Kafka前还是需要先启动Zookeeper;

  • 启动Zookeeper服务,服务将运行在2181端口;
1
2
bash复制代码# 后台运行服务,并把日志输出到当前文件夹下的zookeeper-out.file文件中
nohup bin/zookeeper-server-start.sh config/zookeeper.properties > zookeeper-out.file 2>&1 &
  • 由于目前Kafka是部署在Linux服务器上的,外网如果想要访问,需要修改Kafka的配置文件config/server.properties,修改下Kafka的监听地址,否则会无法连接;
1
2
3
4
5
6
7
8
9
properties复制代码############################# Socket Server Settings #############################

# The address the socket server listens on. It will get the value returned from
# java.net.InetAddress.getCanonicalHostName() if not configured.
# FORMAT:
# listeners = listener_name://host_name:port
# EXAMPLE:
# listeners = PLAINTEXT://your.host.name:9092
listeners=PLAINTEXT://192.168.5.78:9092
  • 最后启动Kafka服务,服务将运行在9092端口。
1
2
bash复制代码# 后台运行服务,并把日志输出到当前文件夹下的kafka-out.file文件中
nohup bin/kafka-server-start.sh config/server.properties > kafka-out.file 2>&1 &

Kafka命令行操作

接下来我们使用命令行来操作下Kafka,熟悉下Kafka的使用。

  • 首先创建一个叫consoleTopic的Topic;
1
bash复制代码bin/kafka-topics.sh --create --topic consoleTopic --bootstrap-server 192.168.5.78:9092
  • 接下来查看Topic;
1
bash复制代码bin/kafka-topics.sh --describe --topic consoleTopic --bootstrap-server 192.168.5.78:9092
  • 会显示如下Topic信息;
1
2
bash复制代码Topic: consoleTopic	TopicId: tJmxUQ8QRJGlhCSf2ojuGw	PartitionCount: 1	ReplicationFactor: 1	Configs: segment.bytes=1073741824
Topic: consoleTopic Partition: 0 Leader: 0 Replicas: 0 Isr: 0
  • 向Topic中发送消息:
1
bash复制代码bin/kafka-console-producer.sh --topic consoleTopic --bootstrap-server 192.168.5.78:9092
  • 直接在命令行中输入信息即可发送;

  • 重新打开一个窗口,通过如下命令可以从Topic中获取消息:
1
bash复制代码bin/kafka-console-consumer.sh --topic consoleTopic --from-beginning --bootstrap-server 192.168.5.78:9092

Kafka可视化

使用命令行操作Kafka确实有点麻烦,接下来我们试试可视化工具kafka-eagle。

安装JDK

如果你使用的是CentOS的话,默认没有安装完整版的JDK,需要自行安装!

  • 下载JDK 8,下载地址:mirrors.tuna.tsinghua.edu.cn/AdoptOpenJD…

  • 下载完成后将JDK解压到指定目录;
1
2
3
bash复制代码cd /mydata/java
tar -zxvf OpenJDK8U-jdk_x64_linux_xxx.tar.gz
mv OpenJDK8U-jdk_x64_linux_xxx.tar.gz jdk1.8
  • 在/etc/profile文件中添加环境变量JAVA_HOME。
1
2
3
4
5
6
bash复制代码vi /etc/profile
# 在profile文件中添加
export JAVA_HOME=/mydata/java/jdk1.8
export PATH=$PATH:$JAVA_HOME/bin
# 使修改后的profile文件生效
. /etc/profile

安装kafka-eagle

  • 下载kafka-eagle的安装包,下载地址:github.com/smartloli/k…

  • 下载完成后将kafka-eagle解压到指定目录;
1
2
bash复制代码cd /mydata/kafka/
tar -zxvf kafka-eagle-web-2.0.5-bin.tar.gz
  • 在/etc/profile文件中添加环境变量KE_HOME;
1
2
3
4
5
6
bash复制代码vi /etc/profile
# 在profile文件中添加
export KE_HOME=/mydata/kafka/kafka-eagle-web-2.0.5
export PATH=$PATH:$KE_HOME/bin
# 使修改后的profile文件生效
. /etc/profile
  • 安装MySQL并添加数据库ke,kafka-eagle之后会用到它;
  • 修改配置文件$KE_HOME/conf/system-config.properties,主要是修改Zookeeper的配置和数据库配置,注释掉sqlite配置,改为使用MySQL;
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
properties复制代码######################################
# multi zookeeper & kafka cluster list
######################################
kafka.eagle.zk.cluster.alias=cluster1
cluster1.zk.list=localhost:2181

######################################
# kafka eagle webui port
######################################
kafka.eagle.webui.port=8048

######################################
# kafka sqlite jdbc driver address
######################################
# kafka.eagle.driver=org.sqlite.JDBC
# kafka.eagle.url=jdbc:sqlite:/hadoop/kafka-eagle/db/ke.db
# kafka.eagle.username=root
# kafka.eagle.password=www.kafka-eagle.org

######################################
# kafka mysql jdbc driver address
######################################
kafka.eagle.driver=com.mysql.cj.jdbc.Driver
kafka.eagle.url=jdbc:mysql://localhost:3306/ke?useUnicode=true&characterEncoding=UTF-8&zeroDateTimeBehavior=convertToNull
kafka.eagle.username=root
kafka.eagle.password=root
  • 使用如下命令启动kafka-eagle;
1
bash复制代码$KE_HOME/bin/ke.sh start
  • 命令执行完成后会显示如下信息,但并不代表服务已经启动成功,还需要等待一会;

  • 再介绍几个有用的kafka-eagle命令:
1
2
3
4
5
6
7
8
9
10
bash复制代码# 停止服务
$KE_HOME/bin/ke.sh stop
# 重启服务
$KE_HOME/bin/ke.sh restart
# 查看服务运行状态
$KE_HOME/bin/ke.sh status
# 查看服务状态
$KE_HOME/bin/ke.sh stats
# 动态查看服务输出日志
tail -f $KE_HOME/logs/ke_console.out
  • 启动成功可以直接访问,输入账号密码admin:123456,访问地址:http://192.168.5.78:8048/

  • 登录成功后可以访问到Dashboard,界面还是很棒的!

可视化工具使用

  • 之前我们使用命令行创建了Topic,这里可以直接通过界面来创建;

  • 我们还可以直接通过kafka-eagle来发送消息;

  • 我们可以通过命令行来消费Topic中的消息;
1
bash复制代码bin/kafka-console-consumer.sh --topic testTopic --from-beginning --bootstrap-server 192.168.5.78:9092
  • 控制台获取到信息显示如下;

  • 还有一个很有意思的功能叫KSQL,可以通过SQL语句来查询Topic中的消息;

  • 可视化工具自然少不了监控,如果你想开启kafka-eagle对Kafka的监控功能的话,需要修改Kafka的启动脚本,暴露JMX的端口;
1
2
3
4
5
6
bash复制代码vi kafka-server-start.sh
# 暴露JMX端口
if [ "x$KAFKA_HEAP_OPTS" = "x" ]; then
export KAFKA_HEAP_OPTS="-server -Xms2G -Xmx2G -XX:PermSize=128m -XX:+UseG1GC -XX:MaxGCPauseMillis=200 -XX:ParallelGCThreads=8 -XX:ConcGCThreads=5 -XX:InitiatingHeapOccupancyPercent=70"
export JMX_PORT="9999"
fi
  • 来看下监控图表界面;

  • 还有一个很骚气的监控大屏功能;

  • 还有Zookeeper的命令行功能,总之功能很全,很强大!

SpringBoot整合Kafka

在SpringBoot中操作Kafka也是非常简单的,比如Kafka的消息模式很简单,没有队列,只有Topic。

  • 首先在应用的pom.xml中添加Spring Kafka依赖;
1
2
3
4
5
6
xml复制代码<!--Spring整合Kafka-->
<dependency>
<groupId>org.springframework.kafka</groupId>
<artifactId>spring-kafka</artifactId>
<version>2.7.1</version>
</dependency>
  • 修改应用配置文件application.yml,配置Kafka服务地址及consumer的group-id;
1
2
3
4
5
6
7
yaml复制代码server:
port: 8088
spring:
kafka:
bootstrap-servers: '192.168.5.78:9092'
consumer:
group-id: "bootGroup"
  • 创建一个生产者,用于向Kafka的Topic中发送消息;
1
2
3
4
5
6
7
8
9
10
11
12
13
java复制代码/**
* Kafka消息生产者
* Created by macro on 2021/5/19.
*/
@Component
public class KafkaProducer {
@Autowired
private KafkaTemplate kafkaTemplate;

public void send(String message){
kafkaTemplate.send("bootTopic",message);
}
}
  • 创建一个消费者,用于从Kafka中获取消息并消费;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
java复制代码/**
* Kafka消息消费者
* Created by macro on 2021/5/19.
*/
@Slf4j
@Component
public class KafkaConsumer {

@KafkaListener(topics = "bootTopic")
public void processMessage(String content) {
log.info("consumer processMessage : {}",content);
}

}
  • 创建一个发送消息的接口,调用生产者去发送消息;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
java复制代码/**
* Kafka功能测试
* Created by macro on 2021/5/19.
*/
@Api(tags = "KafkaController", description = "Kafka功能测试")
@Controller
@RequestMapping("/kafka")
public class KafkaController {

@Autowired
private KafkaProducer kafkaProducer;

@ApiOperation("发送消息")
@RequestMapping(value = "/sendMessage", method = RequestMethod.GET)
@ResponseBody
public CommonResult sendMessage(@RequestParam String message) {
kafkaProducer.send(message);
return CommonResult.success(null);
}
}
  • 直接在Swagger中调用接口进行测试;

  • 项目控制台会输出如下信息,表明消息已经被接收并消费掉了。
1
bash复制代码2021-05-19 16:59:21.016  INFO 2344 --- [ntainer#0-0-C-1] c.m.mall.tiny.component.KafkaConsumer    : consumer processMessage : Spring Boot message!

总结

通过本文的一波实践,大家基本就能入门Kafka了。安装、可视化工具、结合SpringBoot,这些基本都是和开发者相关的操作,也是学习Kafka的必经之路。

参考资料

  • Kafka官方文档:kafka.apache.org/quickstart
  • kafka-eagle官方文档:www.kafka-eagle.org/articles/do…
  • Kafka相关概念:juejin.cn/post/684490…

项目源码地址

github.com/macrozheng/…

本文 GitHub github.com/macrozheng/… 已经收录,欢迎大家Star!

本文转载自: 掘金

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

SpringBoot整合Dubbo+Nacos2 1前言

发表于 2021-06-08

1.前言

这是一个基于SpringBoot整合Apache Dubbo+Nacos的极简教程,笔者使用到的技术及版本如下:

SpringBoot 2.4.5

Dubbo 2.7.11

Nacos 2.0.1(自行安装)

Dubbo官网:dubbo.apache.org/zh/

Nacos官网:nacos.io/zh-cn/docs/…

2.目录结构

dubbo-client:生产者消费者共有接口

dubbo-consumer:消费者

dubbo-provider:生产者
在这里插入图片描述

3.配置文件

pom核心依赖

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
xml复制代码<dependency>
<groupId>org.apache.dubbo</groupId>
<artifactId>dubbo-spring-boot-starter</artifactId>
<version>2.7.11</version>
</dependency>
<dependency>
<groupId>com.alibaba.boot</groupId>
<artifactId>nacos-discovery-spring-boot-starter</artifactId>
<version>0.2.7</version>
<exclusions>
<exclusion>
<groupId>com.alibaba.spring</groupId>
<artifactId>spring-context-support</artifactId>
</exclusion>
</exclusions>
</dependency>

pom配置文件全文

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
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
xml复制代码<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<packaging>pom</packaging>
<modules>
<module>dubbo-provider</module>
<module>dubbo-client</module>
<module>dubbo-consumer</module>
</modules>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.4.5</version>
<relativePath/>
</parent>
<groupId>xyz.hcworld</groupId>
<artifactId>rpcscaffold</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>rpcscaffold</name>
<description>SpringBoot整合Apache Dubbo+Nacos的极简教程</description>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.apache.dubbo</groupId>
<artifactId>dubbo-spring-boot-starter</artifactId>
<version>2.7.11</version>
</dependency>
<dependency>
<groupId>com.alibaba.boot</groupId>
<artifactId>nacos-discovery-spring-boot-starter</artifactId>
<version>0.2.7</version>
<exclusions>
<exclusion>
<groupId>com.alibaba.spring</groupId>
<artifactId>spring-context-support</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</build>
</project>

注释:需要去除spring-context-support依赖,否则运行不起来,但是去除后消费者会报错却不影响使用。

4.接口模块

定义一个消费者和生产者共同拥有的接口子模块。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
java复制代码package xyz.hcworld.service;
​
/**
* 注入接口
* @ClassName: DemoService
* @Author: 张红尘
* @Date: 2021-05-24
* @Version:1.0
*/
public interface DemoService {
/**
* 测试方法获取service参数
* @param name
* @return
*/
String sayName(String name);
}

5.生产者模块

pom核心依赖

依赖加入接口模块。

1
2
3
4
5
6
7
xml复制代码<dependencies>
<dependency>
<groupId>xyz.hcworld</groupId>
<artifactId>dubbo-client</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
</dependencies>

yml配置文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
yaml复制代码# 设置nacos的地址(配置中心)
nacos:
discovery:
server-addr: 192.168.2.142:8848
# 设置dubbo的参数
dubbo:
application:
# 当前消费者的配置名
name: dubbo-provider-demo
registry:
address: nacos://192.168.2.142:8848
# 协议(缺省值)
protocol:
name: dubbo
port: -1

# 当前接口的名字与版本
demo:
service:
version: 1.0.0
name: demoService

类

新版Dubbo将 @Service标注为过时,所以最新的注解应当使用 @DubboService ,如果使用 @Service容易与Spring的 @Service搞混。

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复制代码package xyz.hcworld.service.impl;
​
import org.apache.dubbo.config.annotation.DubboService;
import org.apache.dubbo.rpc.RpcContext;
import org.springframework.beans.factory.annotation.Value;
import xyz.hcworld.service.DemoService;
​
/**
* @ClassName: DemoServiceImpl
* @Author: 张红尘
* @Date: 2021-05-24
* @Version:1.0
*/
@DubboService(interfaceClass = DemoService.class,interfaceName = "${demo.service.name}",version = "${demo.service.version}")
public class DemoServiceImpl implements DemoService {
​
@Value("${demo.service.name}")
private String serviceName;
​
@Override
public String sayName(String name) {
RpcContext rpcContext = RpcContext.getContext();
return String.format("Service [name :%s , port : %d] %s(\"%s\") : Hello,%s",
serviceName,
rpcContext.getLocalPort(),
rpcContext.getMethodName(),
name,
name);
}
}

6.消费者模块

pom文件与同生产者模块一致,yml文件修改name其余也与生产者模块一致。

1
2
3
4
yml复制代码dubbo:
application:
# 当前消费者的配置名
name: dubbo-consumer-demo

类

生产者消费者的接口名与接口版本必须一致。使用@DubboReference注入远程接口。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
java复制代码package xyz.hcworld.controller;
​
import org.apache.dubbo.config.annotation.DubboReference;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import xyz.hcworld.service.DemoService;
​
/**
* @ClassName: DubboController
* @Author: 张红尘
* @Date: 2021-05-24
* @Version:1.0
*/
@RestController
public class DubboController {
​
@DubboReference(interfaceClass = DemoService.class,interfaceName = "${demo.service.name}",version = "${demo.service.version}")
private DemoService demoService;
​
@RequestMapping("/hello")
public String sayHello(){
return demoService.sayName("张");
}
}

7.结果

生产者启动结果

在这里插入图片描述

消费者启动结果在这里插入图片描述

注释:报错是因为去除了com.alibaba.spring下的spring-context-support,但并不影响使用,不去除将无法启动,这是因为Dubbo不兼容太新的SpringBoot版本导致。

浏览器请求结果

在这里插入图片描述

Nacos可视化

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

8.项目下载地址

GitHub:github.com/z875479694h…

笔者联系方式

公众号:青山有录

Blog:www.hcworld.xyz

Github:github.com/z875479694h

本文转载自: 掘金

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

盘点 Cloud Feign 负载均衡策略

发表于 2021-06-07

这是我参与更文挑战的第2天,活动详情查看: 更文挑战

总文档 :文章目录

Github : github.com/black-ant

一 . 前言

这是Feign 系列文章中的一篇 , 主要说明 Feign 关于负载均衡的相关知识点 , 文章包含如下内容 :

  • Feign 使用 Ribbon 时的负载均衡处理

二 . Feign 的轮询策略

Feign 其本身有一套轮询的逻辑处理 Server 的调用 , 其主要流程如下 :

  • Step 1 : 调用 submit 获取 server 信息
  • Step 2 : selectServer 选择 Server
  • Step 3 : 回调执行 ServerOperation 中的请求发起

前期调用逻辑

为了方便理解 , 我们来看一下其之前的调用逻辑 , Feign 主要是通过 Invoke 的方式发起方法的代理 :

  • Step 1 : 触发 InvoKe (FeignInvocationHandler)
  • Step 2 : SynchronousMethodHandler 的调用 , 处理对于方法逻辑
  • Step 3 : LoadBalancerFeignClient 调用主逻辑
  • Step 4 : AbstractLoadBalancerAwareClient 中间过渡 , 处理 URL , 生成真正的URL
  • Step 5 : 最终调用的是 Client 的内部类 (Default)

2.1 获取 Server 入口

可以看到 , 在如上第四步中 , 开始负载均衡的相关处理 :

executeWithLoadBalancer 是 AbstractLoadBalancerAwareClient 中的负载均衡方法 , 该类位于 com.netflix.loadbalancer 包中

PS : OpenFeign 本身就携带了 Ribbon 依赖 !

首先 , 我们来看一下 executeWithLoadBalancer 主逻辑 , 这里 Submit 是一个Function 语法糖调用 :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
java复制代码C- AbstractLoadBalancerAwareClient
public T executeWithLoadBalancer(final S request, final IClientConfig requestConfig) throws ClientException {
LoadBalancerCommand<T> command = buildLoadBalancerCommand(request, requestConfig);

try {
// Step 1 : submit 中获取 Server
// Step 2 : 通过 ServerOperation 语法糖 , 执行具体的操作
return command.submit(// 暂时省略 Function:003 )
.toBlocking()
.single();
} catch (Exception e) {
Throwable t = e.getCause();
if (t instanceof ClientException) {
throw (ClientException) t;
} else {
throw new ClientException(e);
}
}
}

接下来看一下 Submit 方法中做了什么事情 :

  • Step 1 : 容器准备
  • Step 2 : 负载均衡监听器开启
  • Step 3 : 重试次数设置
    • maxRetrysSame : 在一台服务器上执行的最大重试次数
    • maxRetrysNext : 要重试的最大不同服务器的数量
  • Step 4 : 使用负载均衡器获得 Server ]
  • Step 5 : 重试策略的处理
  • Step 6 : 流程异常处理
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
java复制代码// C- LoadBalancerCommand : 做过魔改 , 仅展示核心的代码
public Observable<T> submit(final ServerOperation<T> operation) {

// Step 1 : 容器准备
final ExecutionInfoContext context = new ExecutionInfoContext();

// Step 2 : ExecutionContextListenerInvoker 负载均衡器在执行的不同阶段调用的侦听器
if (listenerInvoker != null) {
try {
listenerInvoker.onExecutionStart();
} catch (AbortExecutionException e) {
return Observable.error(e);
}
}

// Step 3 : 重试次数设置
final int maxRetrysSame = retryHandler.getMaxRetriesOnSameServer();
final int maxRetrysNext = retryHandler.getMaxRetriesOnNextServer();

// Step 4 : 使用负载均衡器获得 Server
// 选择 Server , 此处已经通过 Rule 选择完成
Observable<Server> servers = server == null ? selectServer() : Observable.just(server);
Observable<T> o = servers.concatMap(// PS : Function:001 详见);

// Step 5 : 重试策略的处理
if (maxRetrysNext > 0 && server == null){
o = o.retry(retryPolicy(maxRetrysNext, false));
}

// Step 6 : 流程异常处理
return o.onErrorResumeNext( // PS : Function:002 详见);
}

重点 : 在Step 4 中 , selectServer 就已经完成负载均衡的选择处理了 , 其后在 concatMap (Function 1)中进行了进一步的操作 :

[Pro] : Function:001 中干了什么 ?

总结: 简单来说 , 就是数据审计 , 监听器触发和方法扩展 , 用于后续的回调

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
54
55
56
57
58
java复制代码C- LoadBalancerCommand

// 观察者对象编号 : Observable:001
new Func1<Server, Observable<T>>() {
@Override
// Called for each server being selected
public Observable<T> call(Server server) {
// 容器设置 Server
context.setServer(server);

// 捕获LoadBalancer中的每个服务器(节点)的各种统计数据
final ServerStats stats = loadBalancerContext.getServerStats(server);

// 尝试调用和重试操作 (Observable:002)
Observable<T> o = Observable.just(server).concatMap( new Func1<Server, Observable<T>>(){
@Override
public Observable<T> call(final Server server) {
context.incAttemptCount();
loadBalancerContext.noteOpenConnection(stats);

if (listenerInvoker != null) {
try {
// 当选择服务器并且请求将在服务器上执行时调用
listenerInvoker.onStartWithServer(context.toExecutionInfo());
} catch (AbortExecutionException e) {
return Observable.error(e);
}
}

// 计时器开始
final Stopwatch tracer = loadBalancerContext.getExecuteTracer().start();

// 提供接收基于推送的通知的机制 -> UNDO:001 / Observable:003
return operation.call(server).doOnEach(new Observer<T>() {

private T entity;

// 省略其中代码 , 其中实现了 四个方法 :

// - onCompleted : 完成后调用
recordStats(tracer, stats, entity, null)

// - onError : 异常时调用 , 区别是传入了 exception
recordStats(tracer, stats, null, e)
listenerInvoker.onExceptionWithServer(e, context.toExecutionInfo())

// - onNext : 为观察者提供一个要观察的新项目 , 设置 entity + 触发监听器
this.entity = entity
listenerInvoker.onExecutionSuccess(entity, context.toExecutionInfo())


// - recordStats : 停止计时 + 更新统计数据
tracer.stop()
oadBalancerContext.noteRequestCompletion(
stats, entity, exception, tracer.getDuration(TimeUnit.MILLISECONDS), retryHandler)
});
}
}

[Pro] : Function:002 中干了什么 ?

Function 2 是异常处理流程 , 其中主要干了三件事 :

  • 通过不同的重试配置 , 构建不同的 ClientException
  • listenerInvoker 存在 , 触发事件
  • 返回一个观察者对象 , 当观察者订阅了这个Observable时,它会调用Observer的Observer#onError方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
java复制代码C- LoadBalancerCommand
new Func1<Throwable, Observable<T>>() {
@Override
public Observable<T> call(Throwable e) {
// 建不同的 ClientException
if (context.getAttemptCount() > 0) {
if (maxRetrysNext > 0 && context.getServerAttemptCount() == (maxRetrysNext + 1)) {
e = new ClientException(......);
}
else if (maxRetrysSame > 0 && context.getAttemptCount() == (maxRetrysSame + 1)) {
e = new ClientException(......);
}
}

if (listenerInvoker != null) {
listenerInvoker.onExecutionFailed(e, context.toFinalExecutionInfo());
}
return Observable.error(e);
}
}

2.2 selectServer 选择 Server

前面说了 , 负载均衡的核心就是 SelectServer :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
java复制代码C- LoadBalancerCommand
private Observable<Server> selectServer() {
return Observable.create(new OnSubscribe<Server>() {
@Override
public void call(Subscriber<? super Server> next) {
// 通过 loadBalancerContext 进行负载均衡处理
Server server = loadBalancerContext.getServerFromLoadBalancer(loadBalancerURI, loadBalancerKey);

// Observable 模式的其他调用
next.onNext(server);
next.onCompleted();
}
});
}

2.3 getServerFromLoadBalancer 主逻辑

这一段逻辑比较长 , 但是核心代码不多 , 我们仅保留其中最核心的几个部分

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
java复制代码C- LoadBalancerContext
public Server getServerFromLoadBalancer(@Nullable URI original, @Nullable Object loadBalancerKey) throws ClientException {

// 属性准备
String host = null;
int port = -1;

// 省略点一 : 会尝试从 URI original (http:///template/get?desc=order-server 中获取 Host 和 Post
// 当然 ,正常模式下是获取不到的 , 所以这里省略 host != null 的情况 >>>

// 获取 ILoadBalancer 对象 , 此处主要为 ZoneAwareLoadBalancer , 会调用父类 BaseLoadBalancer
ILoadBalancer lb = getLoadBalancer();
if (host == null) {
if (lb != null){
// 核心逻辑 : 选择 Server 对象 --> 详见 2.4
Server svc = lb.chooseServer(loadBalancerKey);
host = svc.getHost();
return svc;
} else {
// 省略负载均衡器不存在的逻辑
}
} else {
// 省略 host 不为 null
}

// 最终构建一个 新 Server 对象返回
return new Server(host, port);
}



// PS : 其中省略了很多判空抛出异常的逻辑 , 通常结构如下所示
if (host == null){
throw new ClientException(ClientException.ErrorType.GENERAL,"....");
}

Feign_ILoadBalancer.png

2.4 chooseServer 中通过 Rule 选择 Server

此处就是最终算法的使用点 , 通过 Rule 调用最终的策略进行处理 :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
java复制代码C- BaseLoadBalancer
public Server chooseServer(Object key) {
if (counter == null) {
// 用于跟踪某个事件发生频率的监视器类型
counter = createCounter();
}
counter.increment();

if (rule == null) {
return null;
} else {
// 调用 Rule 返回具体的 Server
return rule.choose(key);

// 忽略 try-catch
}
}

Rule 体系中提供了完善的体系 , 来看一下 Rule 体系的完整结构 :

Feign_IRule.png

PS : 这里主要使用 PredicateBasedRule

2.5 PredicateBasedRule 案例分析

其中有几个主要的逻辑 :

  • lb.getAllServers() : 获取所有的Server
  • getEligibleServers(servers, loadBalancerKey) :
  • incrementAndGetModulo(eligible.size()) :

Step 1 : choose 选择 Server 可以看到 , 这里是通过 Predicate 进行具体的选择

1
2
3
4
5
6
7
8
9
10
11
12
java复制代码C- PredicateBasedRule 
@Override
public Server choose(Object key) {
ILoadBalancer lb = getLoadBalancer();
// 注意此处的 lb.getAllServers() , 已经获取了所有的 Server 列表
Optional<Server> server = getPredicate().chooseRoundRobinAfterFiltering(lb.getAllServers(), key);
if (server.isPresent()) {
return server.get();
} else {
return null;
}
}

Step 2 : Server 选择主方法

在该方法中首先获取一个正确的 Server List (Step 3), 在使用算法选择具体的 Server (Step 4)

1
2
3
4
5
6
7
8
9
10
java复制代码C- PredicateBasedRule 
// 选择合适的 Service
public Optional<Server> chooseRoundRobinAfterFiltering(List<Server> servers, Object loadBalancerKey) {
List<Server> eligible = getEligibleServers(servers, loadBalancerKey);
if (eligible.size() == 0) {
return Optional.absent();
}
// 核心语句 : 此处从 Server 中获取 Server
return Optional.of(eligible.get(incrementAndGetModulo(eligible.size())));
}

Step 3 : 获取正确的 Server List

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
java复制代码C- PredicateBasedRule 
public List<Server> getEligibleServers(List<Server> servers, Object loadBalancerKey) {
if (loadBalancerKey == null) {
return ImmutableList.copyOf(Iterables.filter(servers, this.getServerOnlyPredicate()));
} else {
List<Server> results = Lists.newArrayList();
for (Server server: servers) {
//
if (this.apply(new PredicateKey(loadBalancerKey, server))) {
results.add(server);
}
}
return results;
}
}

Step 4 : 使用算法选择具体的 Server

1
2
3
4
5
6
7
8
9
10
java复制代码C- PredicateBasedRule 
private int incrementAndGetModulo(int modulo) {
for (;;) {
int current = nextIndex.get();
int next = (current + 1) % modulo;
if (nextIndex.compareAndSet(current, next) && current < modulo){
return current;
}
}
}

2.6 执行 ServerOperation (Function:003)

当Submit 执行Server 处理时(详见 -> UNDO:001) , 就会回调之前准备的观察者对象 , 此处通过 Server 生成自己需要的 URL , 完成负载均衡的处理

这里也是一种观察者的使用 , 在所有的处理完成后 , 通过订阅的方式执行后续逻辑代码 >>

1
2
3
4
5
6
7
8
9
10
11
12
13
14
java复制代码C- AbstractLoadBalancerAwareClient
new ServerOperation<T>() {
@Override
public Observable<T> call(Server server) {
// 获取实际的 URL , 指向具体的 Server
URI finalUri = reconstructURIWithServer(server, request.getUri());
S requestForServer = (S) request.replaceUri(finalUri);
try {
return Observable.just(AbstractLoadBalancerAwareClient.this.execute(requestForServer, requestConfig));
} catch (Exception e) {
return Observable.error(e);
}
}
}

其他

问题一 : 观察者及 Function 的调用逻辑

Feign 负载均衡中一大麻烦就是观察者的使用及Function 语法糖 , 这种回调模式很容易混淆整个逻辑 :

  • Step 1 : LoadBalancerCommand 中取得 Server 对象
  • Step 2 : Function:002 中进行 Server 的二次处理
  • Step 3 : Observable:002 中执行监听器和计时
  • Step 4 : 回到 AbstractLoadBalancerAwareClient 中的 Function:003 生成 Url 及后续逻辑
  • Step 5 : Observable:003 中 onNext 触发监听器和设置 entity 实体
  • Step 6 : Observable:003 中 onCompleted 对请求完成进行订阅处理

问题二 : ExecutionListener 的使用

  • 作用 : 负载均衡器在执行的不同阶段调用的监听器
  • 使用 : 提供了如下几种方法
    • onExecutionStart : 启动
    • onStartWithServer : 当选择服务器并且请求将在服务器上执行时调用
    • onExceptionWithServer : Server 出现异常
    • onExecutionSuccess : 执行成功
    • onExecutionFailed : 执行失败
1
2
3
4
5
6
7
java复制代码public void onExecutionStart(ExecutionContext<I> context) {
for (ExecutionListener<I, O> listener : listeners) {
if (!isListenerDisabled(listener)) {
listener.onExecutionStart(context.getChildContext(listener));
}
}
}

总结

这篇文章力求把负载均衡的逻辑理清楚 , 现在看基本上满足了要求 , 其实整个过程中还有很多可以思考的地方

例如 :

  • Feign 中多观察者对象的使用 , 虽然可读性变差了 ,但是业务能力提升了不少 , 如何更好的设计这套逻辑
  • Feign Ribbon Rule 的使用 ,是否可以自行定制一套或者通过扩展的方式来外界算法

这些东西都会在后续的文章中 , 进行分析 , 拭目以待.

本文转载自: 掘金

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

MySQL中case when用法以及注意事项

发表于 2021-06-07

前言

在MySQL中有两个地方用到了关键字case:

  • Flow Control Functions - CASE Operator
  • Flow Control Statements - CASE Statement

在CASE Statement中不能有ELSE NULL子句,并且以END CASE结尾,而不是END。

CASE Statement主要用在复合语句中,比如存储过程;而CASE Operator则是在单条语句中用作函数。

本文介绍的主要是CASE Operator的用法。

case when的基本语法

第一种用法:

1
2
3
4
5
sql复制代码CASE value
WHEN compare_value THEN result
[WHEN compare_value THEN result ...]
[ELSE result]
END

第二种用法:

1
2
3
4
5
sql复制代码CASE
WHEN condition THEN result
[WHEN condition THEN result ...]
[ELSE result]
END

两种用法的区别:

第一种CASE语法返回的是第一个value=compare_value为true的分支的结果。

第二种CASE语法返回的是第一个condition为true的分支的结果。

如果没有一个value=compare_value或者condition为true,那么就会返回ELSE对应的结果,如果没有ELSE分支,那么返回NULL。

case when的注意事项

分支之间不能有交集

这个函数是顺序执行的,每个条件之间不能有交集;倒不是MySQL的语法上不允许有交集,而是因为一旦成功匹配一条之后其他分支不会再执行了。如果没有理顺逻辑关系,查询的结果可能和预期不符。

NULL的判断

在CASE的第一种用法中如果要判断某则字段或者表达式的是否为NULL的写法。

错误的写法:

1
2
3
4
5
6
sql复制代码SELECT
CASE (`字段`|`表达式`)
WHEN NULL THEN '结果为假'
ELSE '结果为真'
END
FROM `table_name`

正确的写法为:

1
2
3
4
5
6
sql复制代码SELECT
CASE (`字段`|`表达式`) IS NULL
WHEN TRUE THEN '结果为真'
ELSE '结果为假'
END
FROM `table_name`

必须这么写的原因是:MySQL对于是否为NULL的判断不能直接用等于号=,而是用IS NULL或者IS NOT NULL。

默认值的问题

在mysql case when 的坑的这篇博客中看到这种用法,还挺有意思的。

语句1:

1
2
3
4
5
6
7
sql复制代码UPDATE categories
SET
display_order = CASE id
WHEN 1 THEN 3
WHEN 2 THEN 4
WHEN 3 THEN 5
END;

语句2:

1
2
3
4
5
6
7
8
9
sql复制代码UPDATE categories
SET
display_order = CASE id
WHEN 1 THEN 3
WHEN 2 THEN 4
WHEN 3 THEN 5
END
WHERE
id IN (1,2,3);

如果不用where语句对id进行限制,那么语句1会将id不为1, 2, 3的所有记录的display_order字段都设置为NULL。

分支返回的值类型可以不一致

1
2
3
4
5
6
sql复制代码SELECT
CASE
WHEN 5 % 3 = 0 THEN "情况1"
WHEN 5 % 3 = 1 THEN "情况2"
ELSE 12
END AS result;

在DataGrip上执行这个SQL语句没有报错,并且后面接表名查询也不报错。看到这个结果我裂开了,分支的返回类型不一致,不应该报错吗?

接着我又用JdbcTemplate去执行了这个SQL语句,发现居然也没有报错,ELSE分支的值被转化为了字符串。

果然MySQL不严谨啊!

此时,我回过头又仔细看了一眼MySQL的文档,发现其实文档上对这种情况说的十分详细。

这里简单翻译一下:

CASE函数的返回值是所有结果值类型的聚合(aggregated type):

  • 如果所有值的类型都是数值的,那么聚合类型也是数值的:
    • 如果其中至少有一个值是双精度的,那么结果类型就是双精度的。
    • 否则,如果至少有一个值是DECIMAL,那么结果的类型就是DECIMAL。
    • ……
  • ……
  • 对于所有其他类型的组合,结果是VARCHAR类型。
  • 类型合并时,会忽略NULL值所属的类型。

中间的类型合并情况太多了,限于篇幅原因就不一一列举了,感兴趣的请移步:operate_case。

case when的使用场景

  • 根据条件转换字段含义
  • 行转列

字段转换

1
2
3
4
5
6
7
8
9
10
11
sql复制代码SELECT
name '姓名',
age '年龄',
CASE
WHEN age < 18 THEN '少年'
WHEN age < 30 THEN '青年'
WHEN age >= 30 AND age < 50 THEN '中年'
ELSE '老年'
END '年龄段'
FROM
user_info;

一条语句输出多个指标

有多少男同学,多少女同学,并统计男同学中有几人及格,女同学中有几人及格

表结构如下:其中STU_SEX字段,0表示男生,1表示女生。

STU_CODE STU_NAME STU_SEX STU_SCORE
XM 小明 0 88
XL 小磊 0 55
XF 小峰 0 45
XH 小红 1 66
XN 晓妮 1 77
XY 小伊 1 99
1
2
3
4
5
6
7
sql复制代码SELECT 
SUM (CASE WHEN STU_SEX = 0 THEN 1 ELSE 0 END) AS MALE_COUNT,
SUM (CASE WHEN STU_SEX = 1 THEN 1 ELSE 0 END) AS FEMALE_COUNT,
SUM (CASE WHEN STU_SCORE >= 60 AND STU_SEX = 0 THEN 1 ELSE 0 END) AS MALE_PASS,
SUM (CASE WHEN STU_SCORE >= 60 AND STU_SEX = 1 THEN 1 ELSE 0 END) AS FEMALE_PASS
FROM
THTF_STUDENTS

这个例子中表结构不是很合理:姓名,性别,分数放在同一个表中;但是sum和case一起使用我还见的比较少,sum不都一般和group一起使用吗?

行转列

  • 按月份横向显示销售额
  • 按科目横向显示成绩

单纯的CASE WHEN并不能实现行转列,还需要配合SUM和GROUP BY等子句的使用。

统计各科成绩

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
sql复制代码SELECT
st.stu_id '学号',
st.stu_name '姓名',
sum(CASE co.course_name WHEN '大学语文' THEN sc.scores ELSE 0 END ) '大学语文',
sum(CASE co.course_name WHEN '新视野英语' THEN sc.scores ELSE 0 END ) '新视野英语',
sum(CASE co.course_name WHEN '离散数学' THEN sc.scores ELSE 0 END ) '离散数学',
sum(CASE co.course_name WHEN '概率论' THEN sc.scores ELSE 0 END ) '概率论',
sum(CASE co.course_name WHEN '线性代数' THEN sc.scores ELSE 0 END ) '线性代数',
sum(CASE co.course_name WHEN '高等数学' THEN sc.scores ELSE 0 END ) '高等数学'
FROM
edu_student st
LEFT JOIN edu_score sc ON st.stu_id = sc.stu_id
LEFT JOIN edu_courses co ON co.course_no = sc.course_no
GROUP BY
st.stu_id
ORDER BY
NULL;

各个部门每个月的绩效总和

1
2
3
4
5
6
7
8
9
10
11
sql复制代码SELECT
t1.dep,
t2.depname,
SUM(CASE mon WHEN '一月' THEN yj ELSE 0 END) AS 一月,
SUM(CASE mon WHEN '一月' THEN yj ELSE 0 END) AS 一月,
SUM(CASE mon WHEN '一月' THEN yj ELSE 0 END) AS 一月,
FROM
table_1 t1
LEFT JOIN table_2 t2 ON t1.dep = t2.dep
GROUP BY
t1.dep;

SQL优化

优化统计分析的例子

使用sum case when之前的SQL:

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
sql复制代码SELECT 
(
SELECT SUM(total_fee)
FROM mall_order SS
WHERE SS.create_time = S.create_time AND SS.payment_method = 1
) AS 'zhifubaoTotalOrderAmount',
(
SELECT COUNT(*)
FROM mall_order SS
WHERE SS.create_time = S.create_time AND SS.payment_method = 1
) AS 'zhifubaoTotalOrderNum',
(
SELECT SUM(total_fee)
FROM mall_order SS
WHERE SS.create_time = S.create_time AND SS.payment_method = 2
) AS 'weixinTotalOrderAmount',
(
SELECT COUNT(*)
FROM mall_order SS
WHERE SS.create_time = S.create_time AND SS.payment_method = 2
) AS 'weixinTotalOrderNum'
FROM mall_order S
WHERE S.create_time > '2016-05-01' AND S.create_time < '2016-08-01'
GROUP BY
S.create_time
ORDER BY
S.create_time ASC;

执行情况:50w条数据,10s左右;全表扫描,4个子查询DEPENDENT SUBQUERY,依赖于外部查询。

使用sum case when优化之后的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
sql复制代码SELECT
S.create_time,
sum(case when S.payment_method =1 then 1 else 0 end) as 'zhifubaoOrderNum',
sum(case when S.payment_method =1 then total_fee else 0 end) as 'zhifubaoOrderAmount',
sum(case when S.payment_method =2 then 1 else 0 end) as 'weixinOrderNum',
sum(case when S.payment_method =2 then total_fee else 0 end) as 'weixinOrderAmount'
FROM
mall_order S
WHERE
S.create_time > '2015-05-01' and S.create_time < '2016-08-01'
GROUP BY
S.create_time
ORDER BY
S.create_time asc;

执行情况:全表扫描50w条数据,1s左右;遍历全表一次就可以得到结果了。

另一个优化的例子

原来的SQL:

1
2
3
4
5
6
7
8
9
10
11
sql复制代码SELECT
uid,
sum(power) powerup
FROM t1
WHERE
date>='2017-03-31' AND
UNIX_TIMESTAMP(STR_TO_DATE(concat(date,' ',hour),'%Y-%m-%d %H'))>=1490965200 AND
UNIX_TIMESTAMP(STR_TO_DATE(concat(date,' ',hour),'%Y-%m-%d %H'))<1492174801 AND
aType in (1,6,9)
GROUP BY
uid;

情况描述:表设计时将日期时间中的date和hour给独立出来成两列,查询时再合并成一个新的条件;导致了这个SQL效率非常低,全表扫描、没有索引、有临时表、需要额外排序。

优化后的SQL:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
sql复制代码SELECT
uid,
sum(powerup+powerup1)
FROM
(
SELECT uid,
CASE
WHEN concat(date,' ',hour) >='2017-03-24 13:00' THEN power ELSE '0'
END AS powerup,

CASE
WHEN concat(date,' ',hour) < '2017-03-25 13:00' THEN power ELSE '0'
END AS powerup1

FROM t1
WHERE date >= '2017-03-24' AND date AND aType in (1,6,9)
) a
GROUP BY
uid;

使用case when优化之后,原来的在date上的索引就可以用上了。

总结

个人不太喜欢在业务代码的SQL语句中用case when,原因有两点:

  • 可读性不高
  • 可维护性不好

不过在做统计分析的时候,使用这类函数会感叹:真香!

参考文献

  • dev.mysql.com/doc/refman/…
  • dev.mysql.com/doc/refman/…
  • www.cnblogs.com/echojson/p/…
  • blog.csdn.net/qq_16142851…
  • blog.csdn.net/u013514928/…
  • www.cnblogs.com/chenduzizho…
  • www.cnblogs.com/echojson/p/…
  • blog.csdn.net/qq_16142851…
  • my.oschina.net/u/1187675/b…
  • blog.csdn.net/weixin_3246…
  • blog.csdn.net/rongtaoup/a…

本文转载自: 掘金

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

python协程(超详细) 1、迭代 2、迭代器 3、生成器

发表于 2021-06-07

1、迭代

这是我参与更文挑战的第7天,活动详情查看: 更文挑战

1.1 迭代的概念

使用for循环遍历取值的过程叫做迭代,比如:使用for循环遍历列表获取值的过程

1
2
3
python复制代码# Python 中的迭代
for value in [2, 3, 4]:
print(value)

1.2 可迭代对象

标准概念:在类里面定义__iter__方法,并使用该类创建的对象就是可迭代对象

简单记忆:使用for循环遍历取值的对象叫做可迭代对象, 比如:列表、元组、字典、集合、range、字符串

1.3 判断对象是否是可迭代对象

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
python复制代码# 元组,列表,字典,字符串,集合,range都是可迭代对象
from collections import Iterable
# 如果解释器提示警告,就是用下面的导入方式
# from collections.abc import Iterable

# 判断对象是否是指定类型
result = isinstance((3, 5), Iterable)
print("元组是否是可迭代对象:", result)

result = isinstance([3, 5], Iterable)
print("列表是否是可迭代对象:", result)

result = isinstance({"name": "张三"}, Iterable)
print("字典是否是可迭代对象:", result)

result = isinstance("hello", Iterable)
print("字符串是否是可迭代对象:", result)

result = isinstance({3, 5}, Iterable)
print("集合是否是可迭代对象:", result)

result = isinstance(range(5), Iterable)
print("range是否是可迭代对象:", result)

result = isinstance(5, Iterable)
print("整数是否是可迭代对象:", result)

# 提示: 以后还根据对象判断是否是其它类型,比如以后可以判断函数里面的参数是否是自己想要的类型
result = isinstance(5, int)
print("整数是否是int类型对象:", result)

class Student(object):
pass

stu = Student()
result = isinstance(stu, Iterable)

print("stu是否是可迭代对象:", result)

result = isinstance(stu, Student)

print("stu是否是Student类型的对象:", result)

1.4 自定义可迭代对象

在类中实现__iter__方法

自定义可迭代类型代码

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
python复制代码from collections import Iterable
# 如果解释器提示警告,就是用下面的导入方式
# from collections.abc import Iterable

# 自定义可迭代对象: 在类里面定义__iter__方法创建的对象就是可迭代对象
class MyList(object):

def __init__(self):
self.my_list = list()

# 添加指定元素
def append_item(self, item):
self.my_list.append(item)

def __iter__(self):
# 可迭代对象的本质:遍历可迭代对象的时候其实获取的是可迭代对象的迭代器, 然后通过迭代器获取对象中的数据
pass

my_list = MyList()
my_list.append_item(1)
my_list.append_item(2)
result = isinstance(my_list, Iterable)

print(result)

for value in my_list:
print(value)

执行结果:

1
2
3
4
5
python复制代码Traceback (most recent call last):
True
File "/Users/hbin/Desktop/untitled/aa.py", line 24, in <module>
for value in my_list:
TypeError: iter() returned non-iterator of type 'NoneType'

通过执行结果可以看出来,遍历可迭代对象依次获取数据需要迭代器

总结

在类里面提供一个__iter__创建的对象是可迭代对象,可迭代对象是需要迭代器完成数据迭代的

2、迭代器

2.1 自定义迭代器对象

自定义迭代器对象: 在类里面定义__iter__和__next__方法创建的对象就是迭代器对象

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
54
python复制代码from collections import Iterable
from collections import Iterator

# 自定义可迭代对象: 在类里面定义__iter__方法创建的对象就是可迭代对象
class MyList(object):

def __init__(self):
self.my_list = list()

# 添加指定元素
def append_item(self, item):
self.my_list.append(item)

def __iter__(self):
# 可迭代对象的本质:遍历可迭代对象的时候其实获取的是可迭代对象的迭代器, 然后通过迭代器获取对象中的数据
my_iterator = MyIterator(self.my_list)
return my_iterator


# 自定义迭代器对象: 在类里面定义__iter__和__next__方法创建的对象就是迭代器对象
class MyIterator(object):

def __init__(self, my_list):
self.my_list = my_list

# 记录当前获取数据的下标
self.current_index = 0

# 判断当前对象是否是迭代器
result = isinstance(self, Iterator)
print("MyIterator创建的对象是否是迭代器:", result)

def __iter__(self):
return self

# 获取迭代器中下一个值
def __next__(self):
if self.current_index < len(self.my_list):
self.current_index += 1
return self.my_list[self.current_index - 1]
else:
# 数据取完了,需要抛出一个停止迭代的异常
raise StopIteration


my_list = MyList()
my_list.append_item(1)
my_list.append_item(2)
result = isinstance(my_list, Iterable)

print(result)

for value in my_list:
print(value)

运行结果:

1
2
3
4
python复制代码True
MyIterator创建的对象是否是迭代器: True
1
2

2.2 iter()函数与next()函数

  1. iter函数: 获取可迭代对象的迭代器,会调用可迭代对象身上的__iter__方法
  2. next函数: 获取迭代器中下一个值,会调用迭代器对象身上的__next__方法
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
54
55
56
57
python复制代码# 自定义可迭代对象: 在类里面定义__iter__方法创建的对象就是可迭代对象
class MyList(object):

def __init__(self):
self.my_list = list()

# 添加指定元素
def append_item(self, item):
self.my_list.append(item)

def __iter__(self):
# 可迭代对象的本质:遍历可迭代对象的时候其实获取的是可迭代对象的迭代器, 然后通过迭代器获取对象中的数据
my_iterator = MyIterator(self.my_list)
return my_iterator


# 自定义迭代器对象: 在类里面定义__iter__和__next__方法创建的对象就是迭代器对象
# 迭代器是记录当前数据的位置以便获取下一个位置的值
class MyIterator(object):

def __init__(self, my_list):
self.my_list = my_list

# 记录当前获取数据的下标
self.current_index = 0

def __iter__(self):
return self

# 获取迭代器中下一个值
def __next__(self):
if self.current_index < len(self.my_list):
self.current_index += 1
return self.my_list[self.current_index - 1]
else:
# 数据取完了,需要抛出一个停止迭代的异常
raise StopIteration

# 创建了一个自定义的可迭代对象
my_list = MyList()
my_list.append_item(1)
my_list.append_item(2)

# 获取可迭代对象的迭代器
my_iterator = iter(my_list)
print(my_iterator)
# 获取迭代器中下一个值
# value = next(my_iterator)
# print(value)

# 循环通过迭代器获取数据
while True:
try:
value = next(my_iterator)
print(value)
except StopIteration as e:
break

2.3 for循环的本质

遍历的是可迭代对象

  • for item in Iterable 循环的本质就是先通过iter()函数获取可迭代对象Iterable的迭代器,然后对获取到的迭代器不断调用next()方法来获取下一个值并将其赋值给item,当遇到StopIteration的异常后循环结束。

遍历的是迭代器

  • for item in Iterator 循环的迭代器,不断调用next()方法来获取下一个值并将其赋值给item,当遇到StopIteration的异常后循环结束。

2.4 迭代器的应用场景

我们发现迭代器最核心的功能就是可以通过next()函数的调用来返回下一个数据值。如果每次返回的数据值不是在一个已有的数据集合中读取的,而是通过程序按照一定的规律计算生成的,那么也就意味着可以不用再依赖一个已有的数据集合,也就是说不用再将所有要迭代的数据都一次性缓存下来供后续依次读取,这样可以节省大量的存储(内存)空间。

举个例子,比如,数学中有个著名的斐波拉契数列(Fibonacci),数列中第一个数为0,第二个数为1,其后的每一个数都可由前两个数相加得到:

0, 1, 1, 2, 3, 5, 8, 13, 21, 34, …

现在我们想要通过for…in…循环来遍历迭代斐波那契数列中的前n个数。那么这个斐波那契数列我们就可以用迭代器来实现,每次迭代都通过数学计算来生成下一个数。

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
python复制代码class Fibonacci(object):

def __init__(self, num):
# num:表示生成多少fibonacci数字
self.num = num
# 记录fibonacci前两个值
self.a = 0
self.b = 1
# 记录当前生成数字的索引
self.current_index = 0

def __iter__(self):
return self

def __next__(self):
if self.current_index < self.num:
result = self.a
self.a, self.b = self.b, self.a + self.b
self.current_index += 1
return result
else:
raise StopIteration


fib = Fibonacci(5)
# value = next(fib)
# print(value)

for value in fib:
print(value)

执行结果:

1
2
3
4
5
python复制代码0
1
1
2
3

小结

迭代器的作用就是是记录当前数据的位置以便获取下一个位置的值

3、生成器

3.1 生成器的概念

生成器是一类特殊的迭代器,它不需要再像上面的类一样写__iter__()和__next__()方法了, 使用更加方便,它依然可以使用next函数和for循环取值

3.2 创建生成器方法1

  • 第一种方法很简单,只要把一个列表生成式的 [ ] 改成 ( )
1
2
3
4
5
6
7
8
9
10
11
12
13
python复制代码my_list = [i * 2 for i in range(5)]
print(my_list)

# 创建生成器
my_generator = (i * 2 for i in range(5))
print(my_generator)

# next获取生成器下一个值
# value = next(my_generator)
#
# print(value)
for value in my_generator:
print(value)

执行结果:

1
2
3
4
5
6
7
python复制代码[0, 2, 4, 6, 8]
<generator object <genexpr> at 0x101367048>
0
2
4
6
8

3.3 创建生成器方法2

在def函数里面看到有yield关键字那么就是生成器

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
python复制代码def fibonacci(num):
a = 0
b = 1
# 记录生成fibonacci数字的下标
current_index = 0
print("--11---")
while current_index < num:
result = a
a, b = b, a + b
current_index += 1
print("--22---")
# 代码执行到yield会暂停,然后把结果返回出去,下次启动生成器会在暂停的位置继续往下执行
yield result
print("--33---")


fib = fibonacci(5)
value = next(fib)
print(value)
value = next(fib)
print(value)

value = next(fib)
print(value)

# for value in fib:
# print(value)

在使用生成器实现的方式中,我们将原本在迭代器__next__方法中实现的基本逻辑放到一个函数中来实现,但是将每次迭代返回数值的return换成了yield,此时新定义的函数便不再是函数,而是一个生成器了。

简单来说:只要在def中有yield关键字的 就称为 生成器

3.4 生成器使用return关键字

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
python复制代码def fibonacci(num):
a = 0
b = 1
# 记录生成fibonacci数字的下标
current_index = 0
print("--11---")
while current_index < num:
result = a
a, b = b, a + b
current_index += 1
print("--22---")
# 代码执行到yield会暂停,然后把结果返回出去,下次启动生成器会在暂停的位置继续往下执行
yield result
print("--33---")
return "嘻嘻"

fib = fibonacci(5)
value = next(fib)
print(value)
# 提示: 生成器里面使用return关键字语法上没有问题,但是代码执行到return语句会停止迭代,抛出停止迭代异常

# return 和 yield的区别
# yield: 每次启动生成器都会返回一个值,多次启动可以返回多个值,也就是yield可以返回多个值
# return: 只能返回一次值,代码执行到return语句就停止迭代

try:
value = next(fib)
print(value)
except StopIteration as e:
# 获取return的返回值
print(e.value)

提示:

  • 生成器里面使用return关键字语法上没有问题,但是代码执行到return语句会停止迭代,抛出停止迭代异常

3.5 yield和return的对比

  • 使用了yield关键字的函数不再是函数,而是生成器。(使用了yield的函数就是生成器)
  • 代码执行到yield会暂停,然后把结果返回出去,下次启动生成器会在暂停的位置继续往下执行
  • 每次启动生成器都会返回一个值,多次启动可以返回多个值,也就是yield可以返回多个值
  • return只能返回一次值,代码执行到return语句就停止迭代,抛出停止迭代异常

3.6 使用send方法启动生成器并传参

send方法启动生成器的时候可以传参数

1
2
3
4
5
6
python复制代码def gen():
i = 0
while i<5:
temp = yield i
print(temp)
i+=1

执行结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
python复制代码In [43]: f = gen()

In [44]: next(f)
Out[44]: 0

In [45]: f.send('haha')
haha
Out[45]: 1

In [46]: next(f)
None
Out[46]: 2

In [47]: f.send('haha')
haha
Out[47]: 3

In [48]:

**注意:如果第一次启动生成器使用send方法,那么参数只能传入None,一般第一次启动生成器使用next函数

小结

  • 生成器创建有两种方式,一般都使用yield关键字方法创建生成器
  • yield特点是代码执行到yield会暂停,把结果返回出去,再次启动生成器在暂停的位置继续往下执行

4、协程

4.1 协程的概念

协程,又称微线程,纤程,也称为用户级线程,在不开辟线程的基础上完成多任务,也就是在单线程的情况下完成多任务,多个任务按照一定顺序交替执行 通俗理解只要在def里面只看到一个yield关键字表示就是协程

协程是也是实现多任务的一种方式

协程yield的代码实现

简单实现协程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
python复制代码import time

def work1():
while True:
print("----work1---")
yield
time.sleep(0.5)

def work2():
while True:
print("----work2---")
yield
time.sleep(0.5)

def main():
w1 = work1()
w2 = work2()
while True:
next(w1)
next(w2)

if __name__ == "__main__":
main()

运行结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
python复制代码----work1---
----work2---
----work1---
----work2---
----work1---
----work2---
----work1---
----work2---
----work1---
----work2---
----work1---
----work2---
...省略...

小结

协程之间执行任务按照一定顺序交替执行

5、greenlet

5.1 greentlet的介绍

为了更好使用协程来完成多任务,python中的greenlet模块对其封装,从而使得切换任务变的更加简单

使用如下命令安装greenlet模块:

1
复制代码pip3 install greenlet

使用协程完成多任务

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
python复制代码import time
import greenlet


# 任务1
def work1():
for i in range(5):
print("work1...")
time.sleep(0.2)
# 切换到协程2里面执行对应的任务
g2.switch()


# 任务2
def work2():
for i in range(5):
print("work2...")
time.sleep(0.2)
# 切换到第一个协程执行对应的任务
g1.switch()


if __name__ == '__main__':
# 创建协程指定对应的任务
g1 = greenlet.greenlet(work1)
g2 = greenlet.greenlet(work2)

# 切换到第一个协程执行对应的任务
g1.switch()

运行效果

1
2
3
4
5
6
7
8
9
10
python复制代码work1...
work2...
work1...
work2...
work1...
work2...
work1...
work2...
work1...
work2...

6、gevent

6.1 gevent的介绍

greenlet已经实现了协程,但是这个还要人工切换,这里介绍一个比greenlet更强大而且能够自动切换任务的第三方库,那就是gevent。

gevent内部封装的greenlet,其原理是当一个greenlet遇到IO(指的是input output 输入输出,比如网络、文件操作等)操作时,比如访问网络,就自动切换到其他的greenlet,等到IO操作完成,再在适当的时候切换回来继续执行。

由于IO操作非常耗时,经常使程序处于等待状态,有了gevent为我们自动切换协程,就保证总有greenlet在运行,而不是等待IO

安装

1
python复制代码pip3 install gevent

6.2 gevent的使用

1
2
3
4
5
6
7
8
9
10
11
12
13
python复制代码import gevent

def work(n):
for i in range(n):
# 获取当前协程
print(gevent.getcurrent(), i)

g1 = gevent.spawn(work, 5)
g2 = gevent.spawn(work, 5)
g3 = gevent.spawn(work, 5)
g1.join()
g2.join()
g3.join()

运行结果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
python复制代码<Greenlet "Greenlet-0" at 0x26d8c970488: work(5)> 0
<Greenlet "Greenlet-1" at 0x26d8c970598: work(5)> 0
<Greenlet "Greenlet-2" at 0x26d8c9706a8: work(5)> 0
<Greenlet "Greenlet-0" at 0x26d8c970488: work(5)> 1
<Greenlet "Greenlet-1" at 0x26d8c970598: work(5)> 1
<Greenlet "Greenlet-2" at 0x26d8c9706a8: work(5)> 1
<Greenlet "Greenlet-0" at 0x26d8c970488: work(5)> 2
<Greenlet "Greenlet-1" at 0x26d8c970598: work(5)> 2
<Greenlet "Greenlet-2" at 0x26d8c9706a8: work(5)> 2
<Greenlet "Greenlet-0" at 0x26d8c970488: work(5)> 3
<Greenlet "Greenlet-1" at 0x26d8c970598: work(5)> 3
<Greenlet "Greenlet-2" at 0x26d8c9706a8: work(5)> 3
<Greenlet "Greenlet-0" at 0x26d8c970488: work(5)> 4
<Greenlet "Greenlet-1" at 0x26d8c970598: work(5)> 4
<Greenlet "Greenlet-2" at 0x26d8c9706a8: work(5)> 4

可以看到,3个greenlet是依次运行而不是交替运行

6.3 gevent切换执行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
python复制代码import gevent

def work(n):
for i in range(n):
# 获取当前协程
print(gevent.getcurrent(), i)
#用来模拟一个耗时操作,注意不是time模块中的sleep
gevent.sleep(1)

g1 = gevent.spawn(work, 5)
g2 = gevent.spawn(work, 5)
g3 = gevent.spawn(work, 5)
g1.join()
g2.join()
g3.join()

运行结果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
python复制代码<Greenlet at 0x7fa70ffa1c30: f(5)> 0
<Greenlet at 0x7fa70ffa1870: f(5)> 0
<Greenlet at 0x7fa70ffa1eb0: f(5)> 0
<Greenlet at 0x7fa70ffa1c30: f(5)> 1
<Greenlet at 0x7fa70ffa1870: f(5)> 1
<Greenlet at 0x7fa70ffa1eb0: f(5)> 1
<Greenlet at 0x7fa70ffa1c30: f(5)> 2
<Greenlet at 0x7fa70ffa1870: f(5)> 2
<Greenlet at 0x7fa70ffa1eb0: f(5)> 2
<Greenlet at 0x7fa70ffa1c30: f(5)> 3
<Greenlet at 0x7fa70ffa1870: f(5)> 3
<Greenlet at 0x7fa70ffa1eb0: f(5)> 3
<Greenlet at 0x7fa70ffa1c30: f(5)> 4
<Greenlet at 0x7fa70ffa1870: f(5)> 4
<Greenlet at 0x7fa70ffa1eb0: f(5)> 4

6.4 给程序打补丁

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
python复制代码import gevent
import time
from gevent import monkey

# 打补丁,让gevent框架识别耗时操作,比如:time.sleep,网络请求延时
monkey.patch_all()


# 任务1
def work1(num):
for i in range(num):
print("work1....")
time.sleep(0.2)
# gevent.sleep(0.2)

# 任务1
def work2(num):
for i in range(num):
print("work2....")
time.sleep(0.2)
# gevent.sleep(0.2)



if __name__ == '__main__':
# 创建协程指定对应的任务
g1 = gevent.spawn(work1, 3)
g2 = gevent.spawn(work2, 3)

# 主线程等待协程执行完成以后程序再退出
g1.join()
g2.join()

运行结果

1
2
3
4
5
6
python复制代码work1....
work2....
work1....
work2....
work1....
work2....

6.5 注意

  • 当前程序是一个死循环并且还能有耗时操作,就不需要加上join方法了,因为程序需要一直运行不会退出

示例代码

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
python复制代码import gevent
import time
from gevent import monkey

# 打补丁,让gevent框架识别耗时操作,比如:time.sleep,网络请求延时
monkey.patch_all()


# 任务1
def work1(num):
for i in range(num):
print("work1....")
time.sleep(0.2)
# gevent.sleep(0.2)

# 任务1
def work2(num):
for i in range(num):
print("work2....")
time.sleep(0.2)
# gevent.sleep(0.2)



if __name__ == '__main__':
# 创建协程指定对应的任务
g1 = gevent.spawn(work1, 3)
g2 = gevent.spawn(work2, 3)

while True:
print("主线程中执行")
time.sleep(0.5)

执行结果:

1
erlang复制代码主线程中执行work1....work2....work1....work2....work1....work2....主线程中执行主线程中执行主线程中执行..省略..
  • 如果使用的协程过多,如果想启动它们就需要一个一个的去使用join()方法去阻塞主线程,这样代码会过于冗余,可以使用gevent.joinall()方法启动需要使用的协程

实例代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
python复制代码 import time
import gevent

def work1():
for i in range(5):
print("work1工作了{}".format(i))
gevent.sleep(1)

def work2():
for i in range(5):
print("work2工作了{}".format(i))
gevent.sleep(1)


if __name__ == '__main__':
w1 = gevent.spawn(work1)
w2 = gevent.spawn(work2)
gevent.joinall([w1, w2]) # 参数可以为list,set或者tuple

7、进程、线程、协程对比

7.1 进程、线程、协程之间的关系

  • 一个进程至少有一个线程,进程里面可以有多个线程
  • 一个线程里面可以有多个协程

关系图.png

7.2 进程、线程、线程的对比

  1. 进程是资源分配的单位
  2. 线程是操作系统调度的单位
  3. 进程切换需要的资源最大,效率很低
  4. 线程切换需要的资源一般,效率一般(当然了在不考虑GIL的情况下)
  5. 协程切换任务资源很小,效率高
  6. 多进程、多线程根据cpu核数不一样可能是并行的,但是协程是在一个线程中 所以是并发

小结

  • 进程、线程、协程都是可以完成多任务的,可以根据自己实际开发的需要选择使用
  • 由于线程、协程需要的资源很少,所以使用线程和协程的几率最大
  • 开辟协程需要的资源最少

本文转载自: 掘金

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

Android Jetpack 开发套件

发表于 2021-06-07

⭐️ 本文已收录到 AndroidFamily,技术和职场问题,请关注公众号 [彭旭锐] 和 [BaguTree Pro] 知识星球提问。

Android Jetpack 开发套件是 Google 推出的 Android 应用开发编程范式,为开发者提供了解决应用开发场景中通用的模式化问题的最佳实践,让开发者可将时间精力集中于真正重要的业务编码工作上。

这篇文章是 Android Jetpack 系列文章的第 7 篇文章,完整目录可以移步至文章末尾~


前言

  • Fragment 是一个历史悠久的组件,从 API 11 引入至今,已经成为 Android 开发中最常用的组件之一;
  • 在这个专题里,我们将从「使用 & 核心原理 & 面试」三个层面来讨论 Fragment。如果能帮上忙,请务必点赞加关注,这真的对我非常重要。

目录


  1. Fragment 的过去、现在和未来

1.1 Fragment 解决了什么问题?(过去)

Fragment 可以将 Activity 视图拆分为多个区块进行模块化地管理 ,避免了 Activity 视图代码过度臃肿混乱。虽然自定义 View 或 Window 也可以在一定程度拆分 Activity 界面,但事实上它们的职责不同:View / Window 的职责是封装某种视图功能,而 Fragment 是在更高的层次使用控制自定义 View。此外,Fragment 还可以更方便地管理生命周期和事务(虽然我们会通过 MVP 或 MVVM 模式分离业务逻辑,但是对于复杂页面,我们还是无法避免 Activity 视图代码演化的非常臃肿混乱)。

需要注意的是,Fragment 不能脱离 Activity 独立存在,必须由 Activity 或另一个 Fragment 托管,Fragment#onCreateView 实例化的视图最终会被嵌入到宿主的视图树中。

类 角色 MVVM 分层 生命周期感知
Activity 视图控制器 View 层 感知
Fragment 视图控制器 View 层 感知
View 视图 View 层 不感知
Window 视图 View 层 不感知

1.2 Fragment 存在什么问题?(现在)

Fragment 的最初设计理念是 “一个微型 Activity” 的角色,正所谓 “欲戴王冠,必受其重”,很多专门为 Activity 设计 的 API 也需要添加到 Fragment 中,比如运行时权限,多窗口模式切换等新 API。这无疑是在无限制地扩充 Fragment 的职责边界,也在增大 Fragment 设计的复杂度,要知道 Fragment 的本质思想是界面模块化而已。

1.3 Fragment 2.0(未来)

Google 正在重新构思 Fragment 的定位,随着 AndroidX Fragment 版本 陆续更新,新版 Fragment 正在渐渐走进我们的视野,我们称新版 Fragment 为 Fragment 2.0,已知的新特性包括:

  • FragmentScenario:Fragment 的测试框架;
  • FragmentFactory:统一的 Fragment 实例化组件;
  • FragmentContainerView:Fragment 专属视图容器;
  • OnBackPressedDispatcher:在 Fragment 或其他组件中处理返回按钮事件。

具体分析见:Android | 上车!AndroidX Fragment 新姿势!


  1. 说一下 Fragment 的整体结构

现在,我们正式开始讨论 Fragment 的核心工作原理,分析的过程中我会结合读源码,帮助你更清晰地、全面地理解 Fragment 的工作原理。不过,在开始之前我们有必要先梳理 Fragment 源码的整体结构设计,为下文深入阅读源码打下基础。

2.1 代码框架

FragmentActivity.java

1
2
3
4
5
6
7
8
9
10
less复制代码final FragmentController mFragments = FragmentController.createController(new HostCallbacks());

protected void onCreate(@Nullable Bundle savedInstanceState) {
mFragments.attachHost(null /*parent*/);
super.onCreate(savedInstanceState);

...
// FragmentController 中定义了很多 dispatchXX() 方法
mFragments.dispatchCreate();
}

如下 UML 类图描述了 Framgent 整体的代码框架:

要点如下:

  • FragmentActivity 是 Activity 支持 Fragment 的基础,其中持有一个 FragmentController 中间类,它是 FragmentActivity 和 FragmentManager 的中间桥接者,对 Fragment 的操作最终是分发到 FragmentManager 来处理;
  • FragmentManager 承载了 Fragment 的核心逻辑,负责对 Fragment 执行添加、移除或替换等操作,以及添加到返回堆栈。它的实现类 FragmentManagerImpl 是我们主要的分析对象;
  • FragmentHostCallback 是 FragmentManager 向 Fragment 宿主的回调接口,Activity 和 Fragment 中都有内部类实现该接口,所以 Activity 和 Fragment 都可以作为另一个 Fragment 的宿主(Fragment 宿主和 FragmentManager 是 1 : 1 的关系);
  • FragmentTransaction 是 Fragment 事务抽象类,它的实现类 BackStackRecord 是事务管理的主要分析对象。

下图描述了每个宿主与关联的 FragmentManager 的关系:

提示(必看):在后面的讨论中,我们不会再感知 FragmentActivity 与 Fragment 中间的 FragmentController,因为这属于软件设计模式的实现细节,而不是 Fragment 的核心源码。阅读源码我们一定要拨开表面看本质,抓流程,不要拘泥细枝末节。

2.2 说一下 Fragment 生命周期?

生命周期感知是 Fragment 的最基础的功能,也是面试的重灾区,我认为在 “背诵” 生命周期之前,如果你先向面试官阐述你对以下问题的理解,或许是更棒的回答:

  • 问题 1:什么是生命周期,生命周期回调方法(比如 onCreateView())是生命周期的本质吗?
    答:不然。状态转移才是生命周期的本质(Activity 同理)。生命周期方法的本质是 Fragment 状态转移,当生命周期方法被调用,说明 Fragment 从一个状态转移到另一个状态,而所谓的 “生命周期回调” 只是 Framework 提供给开发者使用的 Hook 点,用于在状态转移时执行自定义逻辑。
  • 问题 2:你提到状态转移,那你说下 Fragment 有哪几种状态?
    答:从源码看,AndroidX Fragment 定义了以下五种状态,相对于早期的 Support Fragment 版本少了 STOPPED 等状态,这是因为 Google 认为这些状态是可以对称使用的,例如 STOPPED 状态和 STARTED 状态其实没有本质区别。

Fragment.java

1
2
3
4
5
arduino复制代码static final int INITIALIZING = 0;     初始状态,Fragment 未创建
static final int CREATED = 1; 已创建状态,Fragment 视图未创建
static final int ACTIVITY_CREATED = 2; 已视图创建状态,Fragment 不可见
static final int STARTED = 3; 可见状态,Fragment 不处于前台
static final int RESUMED = 4; 前台状态,可接受用户交互
  • 问题 3:Fragment 生命周期与宿主同步的吗,如果不是,是独立的吗?
    答:不然。Fragment 的生命周期主要受「宿主」、「事务」、「setRetainInstance() API」三个因素影响:当宿主生命周期发生变化时,会触发 Fragment 状态转移到 宿主的最新状态。不过,使用事务和 setRetainInstance() API 也可以使 Fragment 在一定程度上与宿主状态不同步(需要注意:宿主依然在一定程度上形成约束)。
    • 宿主:【见第 3 节】
    • 事务:【见第 4 节】
    • setRetainInstance() API : 【见第 6 节】

下面这张图完整描绘了 Fragment 生命周期:

—— 图片引用自网络

内容很多,别怕。跟着我的节奏一步步分析下去,也就那样了:“就这?🙇”


  1. 宿主如何改变 Fragment 状态

前面提到,Fragment 的生命周期受到三个因素影响,我们暂且讨论宿主的因素。

3.1 Activity 与 Fragment 生命周期的同步关系

当宿主生命周期发生变化时,Fragment 的状态会同步到宿主的状态。从源码看,体现在宿主生命周期回调中会调用 FragmentManager 中一系列 dispatchXXX() 方法来触发 Fragment 状态转移。

FragmentActivity

1
2
3
4
5
6
less复制代码@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
mFragments.attachHost(null /*parent*/);
...
mFragments.dispatchCreate(); // 最终调用 FragmentManager#dispatchCreate()
}

下表总结了 Activity 生命周期与 Fragment 生命周期的关系:

Activity 生命周期 FragmentManager Fragment 状态转移 Fragment 生命周期回调
onCreate dispatchCreate INITIALIZING-> CREATE -> onAttach-> onCreate
onStart(首次) dispatchActivityCreateddispatchStart CREATE-> ACTIVITY_CREATED-> STARTED -> onCreateView-> onViewCreated-> onActivityCreated-> onStart
onStart(非首次) dispatchStart ACTIVITY_CREATED-> STARTED -> onStart
onResume dispatchResume STARTED-> RESUMED(Fragment 可交互) -> onResume
onPause dispatchPause RESUMED-> STARTED -> onPause
onStop dispatchStop STARTED-> ACTIVITY_CREATED -> onStop
onDestroy dispatchDestroy ACTIVITY_CREATED-> CREATED-> INITIALIZING -> onDestroyView-> onDestroy-> onDetach

3.2 状态转移核心源码分析

FragmentManager 中一系列 dispatchXXX() 方法会触发 Fragment 状态转移,我们点进去看看:

提示: 源码方法跳转太多,不利于理解核心流程。我直接帮你梳理出核心流程,跟你直接看源码会不同,但逻辑是相同的。

FragmentManager.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
29
30
31
32
33
34
ini复制代码(源码方法跳转太多,我直接帮你梳理出核心流程,跟你直接看源码会不同,但逻辑是相同的)
public void dispatchCreate() {
mStateSaved = false;
mStopped = false;
moveToState(Fragment.CREATED, false);
4、处理未执行的事务(见第 4 节)
execPendingActions();
}

void moveToState(int newState, boolean always) {
1、状态判断
if (nextState == mCurState) {
return;
}
mCurState = nextState;

2、执行添加的 Fragment
// Must add them in the proper order. mActive fragments may be out of order
for (int i = 0; i < mAdded.size(); i++) {
Fragment f = mAdded.get(i);
// 更新 Fragment 到当前状态
moveFragmentToExpectedState(f);
}

3、执行未添加,但是准备移除的 Fragment
// Now iterate through all active fragments. These will include those that are removed and detached.
for (int i = 0; i < mActive.size(); i++) {
Fragment f = mActive.valueAt(i);
if (f != null && (f.mRemoving || f.mDetached) && !f.mIsNewlyAdded) {
// 更新 Fragment 到当前状态
moveFragmentToExpectedState(f);
}
}
}

其中,moveFragmentToExpectedState() 最终调用到 moveToState(Fragment, int) :

FragmentManager.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
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
54
55
javascript复制代码-> moveFragmentToExpectedState 最终调用到 
-> 更新 Fragment 到当前状态
void moveToState(Fragment f, int newState) {
1、准备 Detatch Fragment 的情况,不再与宿主同步,进入 CREATED 状态
if ((!f.mAdded || f.mDetached) && newState > Fragment.CREATED) {
newState = Fragment.CREATED;
}

2、移除 Fragment 的情况,Fragment 不再与宿主同步
if (f.mRemoving && newState > f.mState) {
if (f.isInBackStack()) {
2.1 移除动作添加到返回栈,则进入 CREATED 状态
newState = Math.min(nextState, Fragment.CREATED);
} else {
2.1 移除动作添加到返回栈,则进入 DESTROY 状态
newState = Math.min(nextState, Fragment.INITIALIZING);
}
}

3、真正执行状态转移
if (f.mState <= newState ) {
switch (f.mState) {
case Fragment.INITIALIZING:
if (nextState> Fragment.INITIALIZING) {
...
}
// fall through
case Fragment.CREATED:
...
// fall through
case Fragment.ACTIVITY_CREATED:
...
// fall through
case Fragment.STARTED:
...
}
} else {
switch (f.mState) {
case Fragment.RESUMED:
if (newState < Fragment.RESUMED) {
...
}
// fall through
case Fragment.STARTED:
...
// fall through
case Fragment.ACTIVITY_CREATED:
...
// fall through
case Fragment.CREATED:
...
}
}
...
}

以上代码已经非常简化了,总结一下:

触发状态转移时,首先会判断 Fragment,如果已经处于目标状态 newState,则会跳过状态转移。然而,并不是 FragmentManager 里所有的 Fragment 都会执行状态转移,只有 「mAdded 为真 && mDetached 为假」 的 Fragment 才会更新到目标状态,其他 Fragment 会脱离宿主状态。最后,状态转移完成后会处理未执行的事务execPendingActions();,可见每次 dispatchXXX() 都会提供一次事务执行的窗口。

不同 Fragment 标志位(Detach / Remove / 返回栈)与最终状态的关系总结如下表:

情况 判断 描述 最终状态
1 f.mRemoving 移除 Fragment.INITIALIZING
2 f.mRemoving && f.isInBackStack() 移除,但添加进返回栈 Fragment.CREATED
3 !f.mAdded f.mDetached
4 f.mAdded 已添加 newState(同步到宿主状态)

提示: 这些标志位可以通过事务进行干涉。

3.3 典型场景生命周期

基本规律:Activity 状态转移触发 Fragment 状态转移。

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
markdown复制代码首次启动:
Activity - onCreate
Fragment - onAttach
Fragment - onCreate
Fragment - onCreateView
Fragment - onViewCreated
Activity - onStart
Fragment - onActivityCreated
Fragment - onStart
Activity - onResume
Fragment - onResume
-------------------------------------------------
退出:
Activity - onPause
Fragment - onPause
Activity - onStop
Fragment - onStop
Activity - onDestroy
Fragment - onDestroyView
Fragment - onDestroy
Fragment - onDetach
-------------------------------------------------
回到桌面:
Activity - onPause
Fragment - onPause
Activity - onStop
Fragment - onStop
-------------------------------------------------
返回:
Activity - onStart
Fragment - onStart
Activity - onResume
Fragment - onResume

  1. Fragment 事务管理

现在,我们来讨论影响 Fragment 状态转移的第二个因素:事务。

4.1 事务概述

  • 问题 1:事务的特性是什么? 事务是恢复和并发的基本单位,具备 4 个基本特性:
+ 原子性:事务不可分割,要么全部完成,要么全部失败回滚;
+ 一致性:事务执行前后数据都具有一致性;
+ 隔离性:事务执行过程中,不受其他事务干扰;
+ 持久性:事务一旦完成,对数据的改变就是永久的。在 Android 中体现为 Fragment 状态保存后,commit() 提交事务会抛异常,因为这部分新提交的事务影响的状态无法保存。
  • Fragment 事务的作用: 使用事务 FragmentTransaction 可以动态改变 Fragment 状态,使得 Fragment 在一定程度脱离宿主的状态。不过,事务依然受到宿主状态约束,例如:当前 Activity 处于 STARTED 状态,那么 addFragment 不会使得 Fragment 进入 RESUME 状态。只有将来 Activity 进入 RESUME 状态时,才会同步 Fragment 到最新状态。

4.2 你知道不同事务操作的区别吗?

  • add & remove:Fragment 状态在 INITIALIZING 与 RESUMED 之间转移;
  • detach & attach: Fragment 状态在 CREATE 与 RESUMED 之间转移;
  • replace: 先移除所有 containerId 中的实例,再 add 一个 Fragment;
  • show & hide: 只控制 Fragment 隐藏或显示,不会触发状态转移,也不会销毁 Fragment 视图或实例;
  • hide & detach & remove 的区别: hide 不会销毁视图和实例、detach 只销毁视图不销毁实例、remove 会销毁实例(自然也销毁视图)。不过,如果 remove 的时候将事务添加到回退栈,那么 Fragment 实例就不会被销毁,只会销毁视图。

下图描述了 Fragment 状态转移与宿主和事务的简单关系:

这里有一个让人摸不着头脑的问题,detach Fragment 并不会回调 onDetach(),因为 detach 只会转移到 CREATE 状态,而回调 onDetach() 需要转移到 INITIALIZING。不知道 Google 为什么要采用这么有歧义的命名。

1
2
3
4
arduino复制代码detach Fragment:
Fragment - onPause
Fragment - onStop
Fragment - onDestroyView

4.3 说说看不同事务提交方式的区别?

FragmentTransaction 定义了 5 种提交方式:

API 描述 是否同步
commit() 异步提交事务,不允许状态丢失 异步
commitAllowingStateLoss() 异步提交事务,允许状态丢失 异步
commitNow() 同步提交事务,不允许状态丢失 同步
commitNowAllowingStateLoss() 同步提交事务,允许状态丢失 同步
executePendingTransactions() 同步执行事务队列中的全部事务 同步

需要注意的地方:

  • onSaveInstanceState() 保存状态后,事务形成的新状态是不会被保存的。在状态保存之后调用 commit() 或 commitNow() 会抛异常

FragmentManagerImpl.java

1
2
3
4
5
csharp复制代码private void checkStateLoss() {
if (mStateSaved || mStopped) {
throw new IllegalStateException("Can not perform this action after onSaveInstanceState");
}
}
  • 使用 commitNow() 或 commitNowAllowingStateLoss() 提交的事务不允许加入回退栈
    为什么有这个设计呢?可能是 Google 考虑到同时存在同步提交和异步提交的事务,并且两个事务都要加入回退栈时,无法确定哪个在上哪个在下是符合预期的,所以干脆禁止 commitNow() 加入回退栈。如果确实有需要同步执行+回退栈的应用场景,可以采用commit() + executePendingTransactions()的取巧方法。相关源码体现如下:

BackStackRecord.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
typescript复制代码@Override
public void commitNow() {
disallowAddToBackStack();
mManager.execSingleAction(this, false);
}

@Override
public void commitNowAllowingStateLoss() {
disallowAddToBackStack();
mManager.execSingleAction(this, true);
}
@NonNull
public FragmentTransaction disallowAddToBackStack() {
if (mAddToBackStack) {
throw new IllegalStateException("This transaction is already being added to the back stack");
}
mAllowAddToBackStack = false;
return this;
}
  • commitNow() 和 executePendingTransactions() 都是同步执行,有区别吗?
    commitNow() 是同步执行当前事务,而 executePendingTransactions() 是同步执行事务队列中的全部事务。

  1. 如何把 Fragment 加载到界面上?

5.1 添加方法

有两种方式可以将 Fragment 添加到 Activity 视图上:静态加载 + 动态加载

  • 静态加载: 静态加载是指在布局文件中使用 标签添加 Fragment 的方式,要点总结如下:
属性 描述
class Fragment 的全限定类名
android:name Fragment 的全限定类名(与 class 没有差别,但 class 优先)
android:id Fragment 唯一标识
android:tag Fragment 唯一标识(id 和 tag 至少设置一个)

举例:

1
2
3
4
5
6
7
8
9
10
11
ini复制代码<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="match_parent">
<fragment android:name="com.example.TestFragmentFragment"
android:id="@+id/list"
android:layout_weight="1"
android:layout_width="0dp"
android:layout_height="match_parent" />
</LinearLayout>
  • 动态加载: 动态加载是指在代码中使用事务 FragmentTransaction 添加 Fragment 的方式。例如:
1
2
3
ini复制代码TextFragment fragment = new TextFragment();
fragmentTransaction.add(R.id.containerId, fragment);
fragmentTransaction.commit();

5.2 Fragment 静态加载源码分析

从布局文件添加 Fragment 本质上是 xml 解析为视图树的过程,这个过程由 LayoutInflater 完成。最终, 标签的解析工作最终是交给 FragmentManager#onCreateView(…) 处理的,让我们来看看具体是如何处理的,源码如下:

FragmentManagerImpl.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
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
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
ini复制代码(已简化)
public View onCreateView(@Nullable View parent, @NonNull String name, @NonNull Context context, @NonNull AttributeSet attrs) {
1、解析属性
String fname = 解析 class 属性
if (fname == null) {
fname = 解析 android:name 属性
}
int id = 解析 android:id 属性
String tag = 解析 android:tag 属性

2、根据 id 或 tag 重用已经创建的 Fragment
Fragment fragment = id != View.NO_ID ? findFragmentById(id) : null;
if (fragment == null && tag != null) {
fragment = findFragmentByTag(tag);
}
if (fragment == null && containerId != View.NO_ID) {
fragment = findFragmentById(containerId);
}

3、新建 Fragment
if (fragment == null) {
3.1 反射创建 Fragment 实例
fragment = getFragmentFactory().instantiate(context.getClassLoader(), fname);
3.2 mFromLayout 设置为 true
fragment.mFromLayout = true;
fragment.mFragmentId = id != 0 ? id : containerId;
fragment.mContainerId = containerId;
fragment.mTag = tag;
fragment.mInLayout = true;
fragment.mFragmentManager = this;
fragment.mHost = mHost;
fragment.onInflate(mHost.getContext(), attrs, fragment.mSavedFragmentState);
3.3 添加 Fragment,立即状态转移
addFragment(fragment, true);
} else if (fragment.mInLayout) {
...
} else {
...
}

4.1 将 id 设置给 Fragment 根布局
if (id != 0) {
fragment.mView.setId(id);
}
4.2 将 tag 设置给 Fragment 根布局
if (fragment.mView.getTag() == null) {
fragment.mView.setTag(tag);
}

5、返回 Fragment 根布局
return fragment.mView;
}

-> 3.3 添加 Fragment,立即状态转移
public void addFragment(Fragment fragment, boolean moveToStateNow) {
...
if (moveToStateNow) {
moveToState(fragment); // 状态转移
}
}
-> 状态转移
void moveToState(Fragment f, int newState, ...) {
...
if (f.mState <= newState ) {
switch (f.mState) {
case Fragment.INITIALIZING:
if (nextState> Fragment.INITIALIZING) {
...
}
// fall through
case Fragment.CREATED:
if (f.mFromLayout && !f.mPerformedCreateView) { // 如果来自布局,并且未执行过 onCreateView
最终调用:
mView = onCreateView(inflater, container, savedInstanceState);
f.onViewCreated(f.mView, f.mSavedFragmentState);
提示:最终在 LayoutInflater 中执行 viewGroup.addView(view, params);
}
if (!f.mFromLayout) { // 不是来自布局
最终调用:
mView = onCreateView(inflater, container, savedInstanceState);
f.onViewCreated(f.mView, f.mSavedFragmentState);
if (container != null) {
container.addView(f.mView);
}
}
...
// fall through
case Fragment.ACTIVITY_CREATED:
...
// fall through
case Fragment.STARTED:
...
}
} else {
...
}
}

以上代码已经非常简化了,代码虽然长但是流程很清楚:

  • 1、FragmentManager 根据布局中的 id 属性或 tag 属性来重用 Fragment,如果不存在则通过反射来创建 Fragment 实例。
  • 2、设置 mFromLayout 为 true,并立即执行状态转移。在 moveToState() 的 CREATE 分支会根据 mFromLayout 判断:如果来自布局,并且未执行过 onCreateView,才会回调 Fragment#onCreateView 创建 View 实例。
  • 3、最终回溯到 LayoutInflater 中,执行 ViewGroup#addView(mView),将 Fragment 根布局添加到父布局中(所以,我们不用在 Fragment 里创建的视图时调用 addView() )。

在我之前写的一篇文章里已经详细讨论过布局解析的全过程:Android | 带你探究 LayoutInflater 布局解析原理,关于 的部分在第 4.2 节,记得去看看。

5.3 Fragment 动态加载源码分析

Fragment 事务的源码在 第 4.4 节 已经讨论过了,我们知道了通过事务添加 / 移除的 Fragment 最终还是会走到 moveToState(…) 来执行状态转移。在创建 View 实例后,mView 也会直接添加到 containerId 容器上。

5.4 静态加载和动态加载的区别体现在哪里?

静态加载和动态加载的主要区别体现在 执行加载操作的消息周期不同:静态加载和布局解析是在同一个 Handler 消息周期中,而动态加载和事务提交不一定在一个 Handler 消息周期中(取决于调用 commit() 还是 commitNow())。

5.5 静态加载和动态加载的优缺点?

这个问题看似合理,但其实经不起推敲,是个伪命题。较常见的说法是静态加载简单直接,而动态加载灵活性跟高。 提出这个说法的人其实忽略了一点:从布局文件中静态加载的 Fragment 也可以使用事务进行动态操作,静态加载也是具有灵活性的。

比较合理的问法是:静态加载和动态加载各适合什么场景?静态加载适合于界面初始化时就确定显示位置和时机的 Fragment,从布局文件中加载可以方便预览。相反地,动态加载适用于初始化时无法确定显示位置和时机的 Fragment,需要依赖代码中的判断条件动态判断。

1
2
3
4
5
6
7
8
scss复制代码演示:使用事务操作从布局文件中静态加载的 Fragment
with(supportFragmentManager.beginTransaction()) {
val fragmentA = supportFragmentManager.findFragmentById(R.id.FragmentA)
if (null != fragmentA) {
hide(fragmentA)
commit()
}
}

  1. setRetainInstance() 到底做了什么?

事实上,从 androidx.fragment 1.3.0 开始,setRetainInstance() 这个 API 已经废弃了。不过,考虑到这个 API 的重要性,我们还是花费一点时间来回顾一下。

6.1 概述

  • 问题 1:什么时候应该使用 setRetainInstance(true)?
    答:在配置变更时(例如屏幕旋转),整个 Activity 需要销毁重建,顺带着 Activity 中的 Fragment 也需要销毁重建。而设置 setRetainInstance(true) 的 Fragment 对象在 Activity 销毁重建的过程中不会被销毁。
  • 问题 2:setRetainInstance(true) 对 Fragment 生命周期的影响?
    答:在 Activity 销毁时,Fragment 不会回调 onDestroy(),而是直接回调 onDestroyView() + onDetach();在 Activity 重建时,Fragment 不会回调 onCreate(),而是直接回调 onCreateView()。
  • 问题 3:为什么废弃 setRetainInstance()?
    答:引入 ViewModel 后,setRetainInstance() API 开始变得鸡肋。ViewModel 已经提供了在 Activity 重建等场景下保持数据的能力,虽然 setRetainInstance() 也具备相同功能,但需要利用 Fragment 来间接存储数据,使用起来不方便,存储粒度也过大。

6.2 setRetainInstance() 核心源码分析

Fragment.java

1
2
3
4
5
6
7
8
9
10
11
12
13
typescript复制代码@Deprecated
public void setRetainInstance(boolean retain) {
mRetainInstance = retain;
if (mFragmentManager != null) {
if (retain) {
mFragmentManager.addRetainedFragment(this);
} else {
mFragmentManager.removeRetainedFragment(this);
}
} else {
mRetainInstanceChangedWhileDetached = true;
}
}

FragmentManager.java

1
2
3
less复制代码void addRetainedFragment(@NonNull Fragment f) {
mNonConfig.addRetainedFragment(f);
}

FragmentManagerViewModel.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
less复制代码void addRetainedFragment(@NonNull Fragment fragment) {
if (mIsStateSaved) {
if (FragmentManager.isLoggingEnabled(Log.VERBOSE)) {
Log.v(TAG, "Ignoring addRetainedFragment as the state is already saved");
}
return;
}
if (mRetainedFragments.containsKey(fragment.mWho)) {
return;
}
mRetainedFragments.put(fragment.mWho, fragment);
if (FragmentManager.isLoggingEnabled(Log.VERBOSE)) {
Log.v(TAG, "Updating retained Fragments: Added " + fragment);
}
}

这段代码并不复杂,当我们调用 Fragment#setRetainInstance(true)时,最终会将 Fragment 添加到一个 ViewModel 中。ViewModel 是具备在 Activity 重建是恢复数据的能力的,现在的问题转换为 ViewModel 为什么可以恢复数据?

简单来说,在 Activity 销毁时,最终会调用 Activity#retainNonConfigurationInstances() 保存 ActivityClientRecord,并托管给 ActivityManagerService。这个过程就相当于把 Fragment 保存到更长的生命周期了。具体见:Android Jetpack 开发套件 #3 为什么 Activity 都重建了 ViewModel 还存在?


  1. 总结

我们前面讲了 Fragment 一些历史问题的由来,以及它的一些核心特性,包括生命周期、事务、加载方式和已过时的 setRetainInstance(true)。关于 Fragment 的话题还有很多,今天我们只讨论了其中最核心的部分,更多内容我后续会继续发布更多文章来讨论。


参考资料

  • Fragment 的过去、现在和将来 —— 谷歌开发者
  • Fragment 的过去、现在和将来(Youtube 视频版) —— Android Dev Summit ‘19
  • Fragment 指南 —— 官方文档
  • Activity 都重建了,你 Fragment 凭什么活着? —— Wan Android
  • 今天考察下 Fragment 相关两个不常见 API —— Wan Android
  • Fragment 是如何被存储与恢复的? —— Wan Android
  • Fragment 生命周期 —— 更木小八 著
  • Fragment 不为人知的细节 —— 三雒 著

推荐阅读

Android Jetpack 系列文章目录如下(2023/07/08 更新):

  • #1 Lifecycle:生命周期感知型组件的基础
  • #2 为什么 LiveData 会重放数据,怎么解决?
  • #3 为什么 Activity 都重建了 ViewModel 还存在?
  • #4 有小伙伴说看不懂 LiveData、Flow、Channel,跟我走
  • #5 Android UI 架构演进:从 MVC 到 MVP、MVVM、MVI
  • #6 ViewBinding 与 Kotlin 委托双剑合璧
  • #7 AndroidX Fragment 核心原理分析
  • #8 OnBackPressedDispatcher:Jetpack 处理回退事件的新姿势
  • #9 食之无味!App Startup 可能比你想象中要简单
  • #10 从 Dagger2 到 Hilt 玩转依赖注入(一)

⭐️ 永远相信美好的事情即将发生,欢迎加入小彭的 Android 交流社群~

本文转载自: 掘金

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

不需要jre运行Java?你没看错!

发表于 2021-06-07

原创:小姐姐味道(微信公众号ID:xjjdog),欢迎分享,转载请保留出处。

今天我们要介绍的是spring-native,它可以让你的spring boot程序,体验graalvm编译器的特性,把你的应用直接编译成native的!

不需要再安装jre,你的应用程序将和exe一样,直接在目标机器上运行!而且启动时间不到1秒钟。

要体验这个功能,我们从spring boot拿一个demo。

1
bash复制代码https://start.spring.io/

在这里选择这个实验性的功能SpringNative。下载下来之后,就可以使用maven进行打包测试。

image.png

1
2
bash复制代码mvn spring-boot:build-image
gradle bootBuildImage

看一下这无敌的启动速度…. 0.038秒… 几乎是瞬时的!

image.png

神奇!

这一切,都得益于graalvm编译器。不过,你至少要把JDK升级到11才能用,也算是堆Java8用户的一种别样的驱动吧。

当然,只有在2.4.5以后的SpringBoot版本中,才支持Spring Native。

GraalVM是什么?

graalvm也是oracle的项目,它的代码地址是:

1
bash复制代码https://github.com/oracle/graal

项目地址是:

1
bash复制代码www.graalvm.org/docs/

graalvm是一个想要统一天下的虚拟机。因为它相比较与HotSpotVM,还能够运行其他语言比如ruby,python,php等。

它是一个新的JVM,不同的是由于做了适配,它能够让不同的语言跑在同一个vm下面。

看看下面这张图,就知道graalvm的野心有多大。

image.png

这还没完,它最吸引人的地方就在于,它能够将应用代码,直接打包成native的二进制可执行代码,运行时连JVM都不需要了!

大家都知道,native和跑在vm里完全是两个档次,否则也不会有jit这么牛x的技术存在了。连android和ios都知道,native的应用流畅性比跑在monotouch上或者hybrid上高很多很多,对于追求性能的企业级应用来说,这个功能就更加实用一些。

让人惊讶的是,它为各个语言实现了一个可以沟通的桥梁。比如我看好js中的某个库,不需要重新开发一个了,在Java中直接就可以用。这是因为,graalVM开发了跨语言互操作协议,能保证跨语言的互操作性。

现在这个功能,大多数平台已经支持了。

image.png

什么叫做native呢?考虑下面这份代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
java复制代码public class Example {
public static void main(String[] args) {
String str = "Native Image is awesome";
String reversed = reverseString(str);
System.out.println("The reversed string is: " + reversed);
}

public static String reverseString(String str) {
if (str.isEmpty())
return str;
return reverseString(str.substring(1)) + str.charAt(0);
}
}

通常情况下,我们直接这样运行,或者打包成jar包。

1
2
bash复制代码javac Example.java
java Example

但我们还可以多一步,就是把class文件native化。

1
arduino复制代码native-image Example

执行的时候,只需要输入 ./Example 就可以了。

有什么好处?

使用native编译的应用,可以实现秒级别的启动,运行更快,占用内存更小。它与主流的部署方式如微服务、k8s等,更加的切合。

但它与传统的JVM也有很多不同,主要体现在:

  1. 系统的性能分析会在编译阶段就给出
  2. 没用的部分和代码将不会编译,直接会被移除,这得益于前些java版本的模块化
  3. 需要提前对反射、资源和动态代理进行转换,没有类加载的延迟
  4. classpath在编译阶段固定
  5. class将不会被懒加载,回在启动的时候一股脑放到内存

虽然native有很多好处,但它的编译时间却很长,因为要做大量的代码静态分析,这也是所有native程序的通病吧。

End

这种thin jar的思路,是不是感觉Java的发展越来越像golang了呢?docker镜像也会因为这种改变便得更小更纯粹,而脱离jre的Java应用也越来越像一个真正的程序了。

但可惜的是,这种编译成native的思路虽然好,现阶段还是无法和golang相抗衡,主要还是在于编译器的差异上。

但愿graalvm能够继续发力,带java继续飞上几十年,养我三代子孙!

作者简介:小姐姐味道 (xjjdog),一个不允许程序员走弯路的公众号。聚焦基础架构和Linux。十年架构,日百亿流量,与你探讨高并发世界,给你不一样的味道。我的个人微信xjjdog0,欢迎添加好友,​进一步交流。​

本文转载自: 掘金

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

我去!爬虫遇到字体反爬,哭了 01 网页分析 02 获取字体

发表于 2021-06-07

这是我参与更文挑战的第5天,活动详情查看: 更文挑战

今天准备爬取某某点评店铺信息时,遇到了『字体』反爬。比如这样的:

还有这样的:

可以看到这些字体已经被加密(反爬)

竟然遇到这种情况,那辰哥就带大家如何去解决这类反爬(字体反爬类)

01 网页分析

在开始分析反爬之前,先简单的介绍一下背景(爬取的网页)

辰哥爬取的某某点评的店铺信息。一开始查看网页源码是这样的

这种什么也看不到,咱们换另一种方式:通过程序直接把整个网页源代码保存下来

获取到的网页源码如下:

比如这里看到评论数(4位数)都有对应着一个编号(相同的数字编号相同),应该是对应着网站的字体库。

下一步,我们需要找到这个网站的字体库。

02 获取字体库

这里的字体库建议在目标网站里面去获取,因为不同的网站的字体库是不一样,导致解码还原的字体也会不一样。

1、抓包获取字体库

在浏览器network里面可以看到一共有三种字体库。(三种字体库各有不同的妙用,后面会有解释)

把字体库链接复制在浏览器里面打开,就可以把字体库下载到本地。

2、查看字体库

这里使用FontCreator的工具查看字体库。

下载地址:

1
javascript复制代码https://www.high-logic.com/font-editor/fontcreator/download

这里需要注册,邮箱验证才能下载,不过辰哥已经下载了,可以在公众号回复:FC,获取安装包。

安装之后,把刚刚下载的字体库在FontCreator中打开

可以看到字体的内容以及对应的编号。

比如数字7对应F399、**数字8对应F572 ,**咱们在原网页和源码对比,是否如此???

可以看到,真是一模一样对应着解码就可以还原字体。

3、为什么会有三个字体库

在查看加密字体的CSS样式时,方式有css内容是这样的

字体库1:d35c3812.woff 对应解码class为 shopNum

字体库2:084c9fff.woff 对应解码class为 reviewTag和address

字体库3:73f5e6f3.woff 对应解码class为 tagName

也就是说,字体所属的不同class标签,对应的解密字体库是不一样的,辰哥这里不得不说一句:太鸡贼了

咱们这里获取的评论数,clas为shopNum,需要用到字体库d35c3812.woff

03 代码实现解密

1、加载字体库

既然我们已经知道了字体反爬的原理,那么我们就可以开始编程实现解密还原。

加载字体库的Python库包是:fontTools ,安装命令如下:

1
nginx复制代码pip install fontTools

将字体库的内容对应关系保存为xml格式

code和name是一一对应关系

可以看到网页源码中的编号后四位对应着字体库的编号。

因此我们可以建立应该字体对应集合

建立好映射关系好,到网页源码中去进行替换

这样我们就成功的将字体反爬处理完毕。后面提取内容大家基本都没问题。

2、完整代码

输出结果:

可以看到加密的数字全部都还原了。

04 小结

辰哥在本文中主要讲解了如此处理字体反爬问题,并以某某点评为例去实战演示分析。辰哥在文中处理的数字类型,大家可以尝试去试试中文如何解决。

为了大家方便学习,辰哥已经把本文的完整源码上传,需要的在公众后台回复:字体反爬

不明白的地方可以在下方留言,一起交流。

本文转载自: 掘金

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

使用SpringBoot的线程池处理异步任务

发表于 2021-06-07

这是我参与更文挑战的第3天,活动详情查看: 更文挑战

介绍

最近在做项目时了解了最好不要直接使用 new Thread(...).start() ,用线程池来隐式的维护所有线程,具体为什么可以看这篇文章。

其实 SpringBoot 已经为我们创建并配置好了这个东西,这里就来学习一下如何来使用 SpringBoot 为我们设置的线程池。

如有错误欢迎联系我指正!

使用

创建配置类

首先我们需要创建一个配置类来让 SpringBoot 加载,并且在里面设置一些自己需要的参数。

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
java复制代码@Configuration
@EnableAsync
public class ExecutorConfig {

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

@Value("${async.executor.thread.core_pool_size}")
private int corePoolSize;
@Value("${async.executor.thread.max_pool_size}")
private int maxPoolSize;
@Value("${async.executor.thread.queue_capacity}")
private int queueCapacity;
@Value("${async.executor.thread.keep_alive_seconds}")
private int keepAliveSeconds;
@Value("${async.executor.thread.name.prefix}")
private String namePrefix;

@Bean(name = "asyncServiceExecutor")
public Executor asyncServiceExecutor() {
logger.info("开启SpringBoot的线程池!");

ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();

// 设置核心线程数
executor.setCorePoolSize(corePoolSize);
// 设置最大线程数
executor.setMaxPoolSize(maxPoolSize);
// 设置缓冲队列大小
executor.setQueueCapacity(queueCapacity);
// 设置线程的最大空闲时间
executor.setKeepAliveSeconds(keepAliveSeconds);
// 设置线程名字的前缀
executor.setThreadNamePrefix(namePrefix);
// 设置拒绝策略:当线程池达到最大线程数时,如何处理新任务
// CALLER_RUNS:在添加到线程池失败时会由主线程自己来执行这个任务
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());

// 线程池初始化
executor.initialize();

return executor;
}
}

首先,

  • @Configuration 的作用是表明这是一个配置类。
  • @EnableAsync 的作用是启用 SpringBoot 的异步执行

其次,关于线程池的设置有

  • corePoolSize: 核心线程数,当向线程池提交一个任务时池里的线程数小于核心线程数,那么它会创建一个线程来执行这个任务,一直直到池内的线程数等于核心线程数
  • maxPoolSize: 最大线程数,线程池中允许的最大线程数量。关于这两个数量的区别我会在下面解释
  • queueCapacity: 缓冲队列大小,用来保存阻塞的任务队列(注意这里的队列放的是任务而不是线程)
  • keepAliveSeconds: 允许线程存活时间(空闲状态下),单位为秒,默认60s
  • namePrefix: 线程名前缀
  • RejectedExecutionHandler: 拒绝策略,当线程池达到最大线程数时,如何处理新任务。线程池为我们提供的策略有
    • AbortPolicy:默认策略。直接抛出 RejectedExecutionException
    • DiscardPolicy:直接丢弃掉被拒绝的任务,且不会抛出任何异常
    • DiscardOldestPolicy:丢弃掉队列中的队头元素(也就是最早在队列里的任务),然后重新执行 提交该任务 的操作
    • CallerRunsPolicy:由主线程自己来执行这个任务,该机制将减慢新任务的提交

关于 corePoolSize 与 maxPoolSize 的区别也是困惑了我很久,官方文档上的解释说的很清楚。我的理解如下:

这个线程池其实是有点“弹性的”。当向线程池提交任务时:

  • 若 当前运行的线程数 < corePoolSize

则 即使其它的工作线程处于空闲状态,线程池也会创建一个新线程来执行任务

  • 若 corePoolSize < 当前运行的线程数 < maxPoolSize
+ 若 **队列已满**


则 创建新线程来执行任务
+ 若 **队列未满**


则 加入队列中
  • 若 当前运行的线程数 > maxPoolSize
+ 若 **队列已满**


则 拒绝任务
+ 若 **队列未满**


则 加入队列中

所以当想要创建固定大小的线程池时,将 corePoolSize 和 maxPoolSize 设置成一样就行了。

最后,别忘了给方法加上 @Bean 注解,否则 SpringBoot 不会加载。

这里因为我加了 @Value 注解,可以在 application.properties 中配置相关数据,如

1
2
3
4
5
6
7
8
9
10
java复制代码# 配置核心线程数
async.executor.thread.core_pool_size = 5
# 配置最大线程数
async.executor.thread.max_pool_size = 5
# 配置队列大小
async.executor.thread.queue_capacity = 999
# 配置线程池中的线程的名称前缀
async.executor.thread.name.prefix = test-async-
# 配置线程最大空闲时间
async.executor.thread.keep_alive_seconds = 30

在具体的方法中使用

配置完上面那些使用起来就轻松了,只需在业务方法前加上 @Async 注解,它就会异步执行了。

如在 Service 中添加如下方法。

1
2
3
4
5
6
7
8
9
10
11
java复制代码@Async("asyncServiceExecutor")
// 注:@Async所修饰的函数不能定义为static类型,这样异步调用不会生效
public void asyncTest() throws InterruptedException {
logger.info("任务开始!");

System.out.println("异步执行某耗时的事...");
System.out.println("如休眠5秒");
Thread.sleep(5000);

logger.info("任务结束!");
}

然后在 Controller 里调用一下这个方法,在网页上连续发送请求做一个测试。
我这里连续发起了5次请求,可以看到这5个任务确实是成功地异步执行了。

res1.png

我设置的线程池大小为 5,所以当超过 5 个任务被提交时,会放入阻塞队列中。

res2.png

到这里,基本的异步执行任务就实现了。

自定义

虽然它提供给我们的线程池已经很强大了,但是有时候我们还需要一些额外信息,比如说我们想知道这个线程池已经执行了多少任务了、当前有多少线程在运行、阻塞队列里还有多少任务等等。那么这个时候我们就可以自定义我们的线程池。

自定义很简单,自己写一个类继承 Spring 提供的 ThreadPoolTaskExecutor,在此之上做修改就好了。如

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
54
java复制代码public class VisibleThreadPoolTaskExecutor extends ThreadPoolTaskExecutor {

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

public void info() {
ThreadPoolExecutor executor = getThreadPoolExecutor();

if (executor == null) return;

String info = "线程池" + this.getThreadNamePrefix() +
"中,总任务数为 " + executor.getTaskCount() +
" ,已处理完的任务数为 " + executor.getCompletedTaskCount() +
" ,目前正在处理的任务数为 " + executor.getActiveCount() +
" ,缓冲队列中任务数为 " + executor.getQueue().size();

logger.info(info);
}

@Override
public void execute(Runnable task) {
info();
super.execute(task);
}

@Override
public void execute(Runnable task, long startTimeout) {
info();
super.execute(task, startTimeout);
}

@Override
public Future<?> submit(Runnable task) {
info();
return super.submit(task);
}

@Override
public <T> Future<T> submit(Callable<T> task) {
info();
return super.submit(task);
}

@Override
public ListenableFuture<?> submitListenable(Runnable task) {
info();
return super.submitListenable(task);
}

@Override
public <T> ListenableFuture<T> submitListenable(Callable<T> task) {
info();
return super.submitListenable(task);
}
}

然后在我们的配置类 ExecutorConfig 中将

ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
改为
ThreadPoolTaskExecutor executor = new VisibleThreadPoolTaskExecutor();,
也就是使用我们自己定义的线程池,然后会在相应的任务执行(execute())、任务提交(submit())时打印我们需要的信息了。

打印结果,在此之前已处理了5个任务:

res3.png

查询线程池信息

上面自定义线程池后想查询信息只能在线程池中的方法查询,那如果我想在任意地方查询线程池的信息呢?那也是可以的,而且非常简单。我这里写一个接口来查询线程池的任务信息以做示例。

首先修改一下线程池里的 Info() 方法,让它返回我们需要的信息。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
java复制代码public String info() {
ThreadPoolExecutor executor = getThreadPoolExecutor();
if (executor == null) return "线程池不存在";

String info = "线程池" + this.getThreadNamePrefix() +
"中,总任务数为 " + executor.getTaskCount() +
" ,已处理完的任务数为 " + executor.getCompletedTaskCount() +
" ,目前正在处理的任务数为 " + executor.getActiveCount() +
" ,缓冲队列中任务数为 " + executor.getQueue().size();

logger.info(info);

return info;
}

然后修改一下配置类 ExecutorConfig 里注册线程池的方法,让它注册的是我们自定义的线程池类型。

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
java复制代码@Bean(name = "asyncServiceExecutor")
public VisibleThreadPoolTaskExecutor asyncServiceExecutor() {
logger.info("开启SpringBoot的线程池!");

// 修改这里,要返回我们自己定义的类 VisibleThreadPoolTaskExecutor
VisibleThreadPoolTaskExecutor executor = new VisibleThreadPoolTaskExecutor();
// ThreadPoolTaskExecutor executor = new VisibleThreadPoolTaskExecutor();

// 设置核心线程数
executor.setCorePoolSize(corePoolSize);
// 设置最大线程数
executor.setMaxPoolSize(maxPoolSize);
// 设置缓冲队列大小
executor.setQueueCapacity(queueCapacity);
// 设置线程的最大空闲时间
executor.setKeepAliveSeconds(keepAliveSeconds);
// 设置线程名字的前缀
executor.setThreadNamePrefix(namePrefix);
// 设置拒绝策略:当线程池达到最大线程数时,如何处理新任务
// CALLER_RUNS:不在新线程中执行任务,而是有调用者所在的线程来执行
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());

// 线程池初始化
executor.initialize();

return executor;
}

再在我们需要信息的地方自动注入这个线程池,然后调用一下 info() 方法就能得到信息了,我这里以在 Service 层中获取信息为例。

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复制代码
@Service
public class DemoService {

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

// 别忘了这里要用 SpringBoot 的自动注入
@Autowired
private VisibleThreadPoolTaskExecutor executor;

// @SneakyThrows 这个注解是Lombok带的,我为了代码简洁使用的。你也可以使用 try catch 的方法。
@SneakyThrows
@Async("asyncServiceExecutor")
public void asyncTest() {
logger.info("任务开始!");

System.out.println("异步执行某耗时的事...");
System.out.println("如休眠5秒");
Thread.sleep(5000);

logger.info("任务结束!");

// 你甚至可以在任务结束时再打印一下线程池信息
executor.info();
}

public String getExecutorInfo() {
return executor.info();
}
}

最后在 Controller 层中调用一下,就大功告成了!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
java复制代码@RestController
public class DemoController {

@Autowired
private DemoService demoService;

@GetMapping("/async")
public void async() {
demoService.asyncTest();
}

@GetMapping("/info")
public String info() {
return demoService.getExecutorInfo();
}
}

来看一下测试的结果吧,我这里调用 /async 一口气开启了 15 个任务,然后在不同时间使用 /info 来看看信息。

刚开始时的结果:

res4.png

一口气提交了15个任务后的中间结果:

res5.png

所有任务都执行完了的最终结果:

res6.png

总结

本篇到这里就结束了,篇幅略长。总结一下,要想在SpringBoot中使用它提供的线程池其实很简单,只要两步:

  1. 注册线程池(使用 @Bean 来注册),设置一些自己想要的参数;
  2. 在想要异步调用的方法上加上 @Async 注解。

当然你也可以不使用 @Async 注解,直接在想开线程的地方自动注入你注册的线程池,然后像普通线程池一样使用就行了。

其实关于这一方面的知识也讲得并不够详尽,比如线程池里还有哪些方法、SpringBoot是如何为我们弄得这么方便的等等,还需要多多补充知识。

参考

  • stackoverflow.com/questions/1…
  • docs.oracle.com/javase/8/do…
  • blog.csdn.net/m0_37701381…

本文转载自: 掘金

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

给你一台服务器,你能把你写的代码部署到线上吗?

发表于 2021-06-07

作者:小傅哥

博客:bugstack.cn

沉淀、分享、成长,让自己和他人都能有所收获!😄

一、前言

给你一台服务器,你能把你写的代码部署到线上吗?

我们常常会听到这样一句话:“为了让研发只关心业务开发,我们做了某某某!”做了啥呢,做了让你不用关心,系统搭建、技术框架、核心组件、通用模块以及上线应用时也只是点点点就可以了,也根本了解不到一台应用服务器是如何,部署环境、开通端口、申请域名、配置SSL的。所以呢,大多数人的你变得越来越像车间中单一岗位的工具人,想在公司走到更高的岗位或者出了公司想做点事情,都会成为你的瓶颈!

一套完整的能力范围,要涵盖哪些方面?

  • 当我们以一个需求的诞生从承接到上线,这个过程中大概会经历的角色包括:业务、运营、产品、数据、研发(UI)、测试和运维,产品运用数据和模型,量化业务提出的需求,该如何迭代实现,满足运营使用完成业务目标,再由UI、前后端研发、测试完成项目的开发和验证以及部署到运维配置的线上环境中。
  • 站在程序员的角色上以这一整套流程来看,其实很大一部分研发人员只能在编程开发的范围内互动,从技术角色上离的最近的是测试和上线部署,但如果让研发自己去部署测试环境,搭建线上环境就会非常困难,不是说技术层面有多难,而是这个事情几乎就没有经历过,也没想过要去做一做试试。
  • 业务、运营、产品、数据中的模型、算法、量化,可能这一部分里研发人员就更远了,压根就不清楚因为什么场景、提出了什么目的、做了怎么的评估、提出了那些手段以及该如何落地,而到研发这能看到的可能仅仅是等待执行的 PRD,正因为总总是这样,所以才有那句话:“你离开公司可能就什么也干不了了!”

接下来,给小伙伴讲讲,我对热爱事情的折腾,不只是技术视野范围的拓宽,也可能让你有些意外收获!

二、在服务器上花出去的钱!

这种事你们可以花点钱嘛,花点,哪怕要呢,要不了多少钱!

汤师爷说,花点钱,我听进去了!

其实我一直从不会吝啬于技术学习上的消费,也不会把时间浪费到非个人能长期成长的其他做兼职的事上。从13年毕业工作开始,因为赚钱少,合租的几个伙伴们也有人会出去找点兼职赚钱,我是属于那种不但没周末去赚钱,还把额外省下的钱都买了域名和服务器,从最早的主机屋到百度开始有BCH云服务,也看过七牛云还用过百度云存储,一路折腾下来服务器上也花费了上万块。

这些钱都买啥了?仅域名就买过一堆,包括:itstack.org、yuyueqianli.com、fuzhengwe.cn、linuxjar.org、iteuds.cn、bugstack.cn等等,那服务器呢?服务器除了正常消费的,还买终身的!!!

虽然,花了不少钱,但也正是因为这些消费和不断的倒腾,让我学会了域名注册、域名备案、域名配置(A记录、CNAME记录、TXT)、证书申请、服务搭建、宝塔应用、配置环境、Linux命令等等。当你有一条具体要做的事情时,你会以这条路径为指导,不断的搜索相应的资料并实践造作!

造作,出第一个能看得过去的论坛,拥有稍许的流量!

  • 不过后来由于干不过一些流氓似的攻击以及 org 域名备案的影响,最终这个小论坛也挂在了奔走的路上。
  • 不过好在网站没白死,从这里面还是学到了很多东西,包括:部署、上线,运维,在运维过程中发现的一些流量峰值、缓存处理、防刷处理、防盗链处理、用户注册与QQ关联、改造原有php代码,支持一些功能等等,挂的只是网站,但留下的是技术经验!

三、把花出去的钱赚回来了!

我这钱是怎么赚回来的?

在我搭建论坛、博客、贴吧似的功能并逐步有些许后,就开始有人联系我能不能给他们做一个这样的网站或者企业门户网站。正好当时还在传统行业的我,也有不少业余时间,每天五点半就能下班,当然有时间搞了。好!说干就干,一顿操作下,2年内接了不少私活,也赚了几万块,算是把服务器、域名的钱都赚回来了,也算没白折腾!

1. 企业门户网站(5000元)

企业门户网站(5000元)

  • 指数:⭐⭐⭐⭐
  • 背景:刚上班一年左右,高中同学问我学计算机能帮他们公司做个网站吗,就模仿老罗那个锤子公司的样式就行,5000块钱。
  • 结果:我接了,可能也是初生牛犊不怕虎,人家需要用PHP语言写!我一个学Java的,写了快一年的C#,之后用PHP给人家做一个企业门户网站,该说不说胆子挺大!
  • 收获:项目顺利部署完成,5000块钱如约到手,买了我第一个苹果手机 iPhone 4s,仍然在我身边。

我第一个苹果手机 iPhone 4s

2. 卖家具宣传网站(2000元)

卖家具宣传网站(2000元)

  • 指数:⭐⭐⭐
  • 背景:14年年初,亲戚家开了一个制作水族箱的小作坊,也是得知我是学计算的。锣鼓喧天的找到我说做一个宣传他们公司商品的网站外面找人做太贵了!。
  • 结果:💰钱咱也不好意思要,只是把服务器和域名等费用的钱要了,不过后来给了我个大红包 2000 元,嘿嘿,手一抖,收了!
  • 收获:得益于我已经接过一个项目,所以PHP开发起来也是很容易,按照他们当时喜欢的样式,做了一个仿照点点网的风格网站布局。这次赚的钱交房租了!

3. Netty通信框架(2000元)

Netty通信框架(2000元)

  • 指数:⭐⭐⭐
  • 背景:14年左右,开始喜欢搞Netty。可能也是当时网上的资料并不多,很多人因为我写了一整套的Netty案例找到我。也就有了这么一次问我能给写个Netty的通信框架不,2000元。
  • 结果:这也是当时头一次不用PHP,而是用Java语言赚到的钱。对我来说还是蛮简单的,1个5:30下班回家就写完了,第二天就给过去了。
  • 收获:知识真的可以变成钱,尤其是那些稍微有点难度,又搞的人不多的时候。

4. 讲课、数据采集(11000元)

除了上面接到的私活,还接到了不少七七八八的小活。

  • 本科生设计指导,1000元。来自猪八戒网。
  • 研究生加密算法,2000元。一个研究生伙伴跟我一起设计出来的,给我从他们学院申请的费用。
  • 在线给一个学生讲课,好像一天是50元,将来快1个月,1000元。
  • 一个物流数据综合平台,其实功能不算多,有点像记录外贸订单的,5000元。
  • 协助一个自己接项目的老板,写了一周Netty编解码部分代码,对接下位机。2000元。

就这样,七七八八的在那两年,赚了2万多块钱。当然还有一部分小的收入,不足1000的。也有被骗过,比如人家拿到项目了就不给钱了或是拿到截图了「我没加水印」,人家够演示的了,也不给钱了。

四、搞一台服务器咋用起来?

接下来,教教你怎么把一个服务器用起来!

对于一个在校的学校来说,或者是已经工作了,但从没有了解或者接触过服务器的配置,以及如何把自己的代码运行到服务器上。那么你可以参考下面的教程介绍,按照这样一个入门的指导把自己的代码也部署到服务器上试试。

可能还有很多小伙伴都不知道服务器能干嘛,简单来说,这就是不在家里,你的一台虚拟电脑,而且是 24小时运行不宕机的,你可以在上面练习网络编程(有公网IP)、中转服务器,以及如下:

  1. 搭博客:mp.weixin.qq.com/s/ZoQ0xAphJ…
  2. 搞论坛:phpwind、Discuz、wordpress(有博客和论坛等模板)
  3. 弄网盘:mp.weixin.qq.com/s/gzUrFexHc…
  4. 聊天室:mp.weixin.qq.com/s/OmXCY4fTf…
  5. 其他的:练习下自己的项目、搞个集群、玩玩ES、弄弄实战、留着接私活给别人部署演示

而这些内容的练习,都能让你把一整套从研发到运维的内容玩透,彻底的了解域名、备案、ssl、宝塔、Linux常用命令等等。

1. 先neng个服务器

首先,无论你是否有服务器,你都可以跟小傅哥一起学习关于服务器的使用,我们建了个群专门学习服务器,添加我的微信:fustack,备注:服务器学习加群。

如果你还是一个新用户小白,那么可以跟着我的流程一起来,先neng一个便宜的服务器,学习使用即可。这里小傅哥给新人弄了个活动,79元即可买一台一年有效期的服务器,还是比我以前买的便宜多了!

  • 当你购买服务器的时候会看到,地域、实例、操作系统等,地域北京、上海、杭州的网速比较好,张家口的便宜但是网速会比较慢。操作系统默认即可,停机后可以更换。

2. 服务器介绍

在购买完服务器后,等待云平台数分钟初始化服务,完事就可以直接使用配置。如下:

  • 重置密码:点击你的实例,蓝色的这个字母,进入后再右侧有一个,重置实例密码,操作。
  • 远程链接:点击远程链接即可链接到你服务上,它是一个在线的操作。你可以通过本地的软件 xshell 链接到服务上去。
  • 更换系统:如果你对自己默认选择的系统不是很满意或者有其他需求,都可以点击停止系统,之后开始操作系统更换。

3. 系统更换成宝塔镜像

对于服务器系统来说你可以使用Linux命令安装各项服务组件,例如k8s、docker、jdk、tomcat、mysql或者php需要的内容等,但对于实际使用的运维来说,我们更希望运维成本越低越好,所以这里我们选择了宝塔,这样一个服务器运维面板来管理我们的服务器。

在各类的云平台上,包括:百度云、华为云、阿里云、腾讯云,都可以安装宝塔的,有的云平台还会有自己的已经准备好的宝塔镜像,这里我们以阿里云服务为例,把系统停机更好为宝塔。

停机

  • 位置:点击云服务的实例,就可以进入到这个页面
  • 操作:更换系统之前我们需要先进行停机操作,停机后就可以点击更换操作系统了

换系统

选镜像

  • 更换完系统进行确认订单,接下来会跳转到管理后台,这时稍等会,服务器会进行启动。

4. 配置并登录宝塔

远程登录

  • 这一步我们直接在网页上登录了,你也可以使用 xshell 登录公网IP

初始化宝塔

命令:[root@CodeGuide ~]# bt default

配置安全组

  • 宝塔的访问要配置 8888 端口,否则是不能访问到的,这个在服务器的安全组中开放即可。
  • 这里我们为了方便就直接开启全部的了,如果你是实际使用,可不能这样操作,否则很不安全!

登录宝塔

  • 地址:39.96.73.xxx:8888/ - 换成你的地址
  • 账号:用户名和密码已经在控制台打印,你可以复制自己的,登录宝塔后可以修改这个默认的密码

5. 安装阿帕奇和FTP

接下来我们在宝塔中安装一个阿帕奇服务器和FTP,这样就可以部署和访问我们的静态博客了,也就是一个html,如下:

安装 Apache

  • 安装过程中会自动的执行一些命令,这个你不用管,只要默默看着就行了。

安装 FTP

  • 安装 FTP 主要是为了通过本地可以把文件传送到服务器上,比如你的一个静态博客是 html,就可以通过 FTP 传到服务器上。

6. 网站配置

安装了阿帕奇和FTP我们就可以简单的配置一个站点了,有了这个站点就可以访问到我们自己的博客!

创建站点

  • 创建站点的适合如果你还没有申请域名,或者域名还没有备案呢,那么就可以直接把公网IP填写进来。

访问站点

  • 地址:http://39.96.73.167/ 在访问的时候,你换成自己的IP即可

7. 网站内容

  • 在宝塔的文件里,你可以选择第6步骤中添加的站点,在里面找到你的文件,做一些修改动作。这个时候在访问网站,就会发现内容已经是你新的内容了。

8. 域名配置

如果你有域名并已经备案好了,那么在创建站点的时候就可以直接把域名配置上,在访问你的网站的时候就可以通过域名访问了。

  • 添加域名:这个里面小傅哥配置的是已经申请好并备案了的域名,你配置成你的就可以。记得配置好域名后,需要在你的域名服务里,通过A记录把服务器IP映射配置上去。
  • FTP 配置:为了更加方便的上传你的文件,你可以把FTP打开,这样就可以通过FTP传输配置了。
  • 访问地址:blog.itedus.cn - 由于域名不是在阿里云,可能http会监测为未备案,拒绝访问

9. SSL 配置

关于 SSL 的申请可以有很多免费网站提供,也可以在宝塔中申请,如果你是用阿里云服务,可以免费申请20个 SSL 证书,另外如果你的域名和服务都是在阿里云,那么在申请 SSL 可以直接走 DNS 认证,否则你需要把 DNS 信息手动配置到你自己的域名上去。放心这个在申请的时候都有提示,按照说明配置即可

下载证书

因为我们需要把 ssl 配置到宝塔上,所以这里需要把 SSL 下载下来,选择 Apache 格式下载。

配置证书

  • 配置后点击保存即可,另外需要强制开启 HTTPS,否则你的网站访问 http 也能继续访问,就没有意义了。
  • 现在你就可以通过 https,访问自己的博客或者网站了,是不看上去高大上了不少!

10. 其他说明

可能你还希望配置 jdk、tomcat,没关系,在宝塔里你都可以安装,也可以安装 mysql,有了这些入门的内容,剩下的就可以搜索一些通用配置的内容,也可以在阿里云中搜索。

五、总结

  • 本文主要介绍了关于一些技术成长有哪些知识点和内容可以扩充你的知识面,以及关于运维服务器的一些操作知识的入门学习。有了这样一个基础的操作领你进门,接下来就可以扩展的搜索很多其他的内容,来完善你要做的一些部署了。
  • 另外本文没有介绍域名的注册和备案,这些内容还是很容易的,你只需要在云平台搜索域名或者百度搜索域名注册,都可以找到一个注册的入口。一般.cn的域名是比较便宜的,其他很多域名续费比较贵,另外像.org这样是不能备案的,所以不要选择太格鲁的域名。
  • 像是这样的知识一定是动手操作起来才能学到东西,可能在这个过程中你会遇到各种各样的问题,没关系,这些问题都是可以搜到的。

六、系列推荐

  • Cloudreve 自建云盘搭建,我说了没人能限得了我的容量和速度!
  • 20年3月27日,Github被攻击。我的GitPage博客也挂了,紧急修复之路,也教会你搭建 Jekyll 博客!
  • 为了把Github博客粉丝转移到公众号,我做了公众号开发并部署到云服务
  • hexo、docsify、jekyll、vuepress,博客搭建指导
  • 13年毕业,用两年时间从外包走进互联网大厂

本文转载自: 掘金

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

1…649650651…956

开发者博客

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