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

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


  • 首页

  • 归档

  • 搜索

git commit 你的提交真的规范吗?

发表于 2021-04-29

代码提交描述内容随意、质量参差不齐,会降低提交log的可读性和可维护性能。受到Angular提交准则启发,提出了约定式提交规范这种基于提交消息的轻量级约定。

约定提交规范格式

1
2
3
4
5
xml复制代码<type>(<scope>): <subject>
<BLANK LINE>
<body>
<BLANK LINE>
<footer>

规约每一行的含义

为了更好、更规范的提交代码,我们需要了解约定提交规范格式,然后再实战中规范自己的提交。自信满满,哈哈。

type
type 描述
feat 一个新的功能
fix bug修复
docs 文档,README.md等
style 重构在代码,不修改代码含义包括空白行、代码格式化、标注等
perf 提高代码性能
refactor 代码修改,既不不是修改bug也不是新增feature
test 新增缺失的单元测试或纠正已经存在的测试
build 构建修改和额外依赖npm,docker等
ci 与CI有关的变动(Travis,Circle,BrowserStack)等
revert 回退一次提交
chore 不修改test和src的其余变动其他
deps 升级依赖
scope

必填项,格式为项目名/模块名,多个模块拆成多个Commit。例如:
spider/xiguadaili

subject

提交的简短说明

body

详细描述,小要求不做修改,重大需求更新需要把添加body说明

footer(affect issues)

指明影响的问题,一般是一个issure或JIRA或需求文档

break changes

指明破坏性修改,比如版本升级,接口参数减少、接口删除、迁移

查看并筛选Commit的目的

查看
1
shell复制代码git log <last tag> HEAD --pretty=format:%s
筛选
1
shell复制代码git log <last release> HEAD --grep feature

(Git Commit Template)一款idea插件

点击Ok之后,commit信息结构如下:

1
2
3
4
5
6
yaml复制代码fix(project_name/module_name): xxxxxxxxxxxxxx

1.日志链路跟踪traceId
2.xxxxxxxxxxxxxx

Closes www.github.com/wencaixu

提交之后Gitlab格式

git commit记录

欢迎关注我的公众号:看相声也要敲代码

本文转载自: 掘金

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

我在GitHub上找到了这些游戏项目,刺激!

发表于 2021-04-29

作者:JackTian

来源:公众号「杰哥的IT之旅」

ID:Jake_Internet

原文链接:我在GitHub上找到了这些游戏项目,刺激!

大家好,我是JackTian。

当你学习、工作累了,趁休息时间娱乐放松一下,通过用游戏的方式来学习技术,那么 GitHub 上这些好玩的开源项目小游戏是最适合不过了。

首先,我们来看一下这张图,可快速了解这篇文章中所涉及到的 23 个关于游戏方面的开源项目。

图片

Games on GitHub

这个开源项目作者收集了托管在 GitHub 上的开源游戏以及跟游戏相关的项目列表,所含的项目类型众多,游戏种类包括教育类、浏览器类等,还包括一些开源的游戏引擎等等;

图片

该项目的目录:

图片

具体细节就不逐一点开给大家演示了,感兴趣的朋友去看看。

GitHub 地址:github.com/leereilly/g…

vim-game-code-break

这个项目是 Vim 插件版本的打豆豆。

将其放在您的.vimrc中:

1
rust复制代码Plug 'johngrib/vim-game-code-break'

然后在Vim中运行以下命令:

1
2
ruby复制代码:source %
:PlugInstall

装上这个插件输入:VimGameCodeBreak,即可开始游戏。

图片

功能介绍

  • h、l 键是控制左右方向;
  • space 键是发球;
  • ` 键是作弊键;
  • ] 键是神模式,[ 键是人工模式;
  • q 键是结束游戏,Q 键是退出并关闭游戏;

GitHub 地址:

github.com/johngrib/vi…

javascript-tetris

一个简单的 JavaScript 俄罗斯方块游戏,这个开源项目是通过 HTML5 实现的,功能简陋齐全,作者还记录这个游戏的实现细节以及工作原理。

图片

地址:

codeincomplete.com/articles/ja…

GitHub 地址:

github.com/jakesgordon…

游戏在线地址:

codeincomplete.com/games/tetri…

react-tetris

除了上述俄罗斯方块游戏外,还有一款不错的,我也是非常喜欢,这个项目是通过 React 实现的,游戏框架使用的是 React + Redux,其中再加入了 Immutable,用它的实例来做来 Redux 的 state,跟上述实现的方式还是有所不同。

图片

这款游戏不仅指屏幕的自适应,而是在 PC 使用键盘、在手机使用手指的响应式操作,都很方便。

图片

GitHub 地址:github.com/chvin/react…

游戏在线地址:chvin.github.io/react-tetri…

sshtron

SSHTron 是一款通过 SSH 运行的多人 Lightcycle 游戏,只需运行如下命令即可开始游戏:

1
arduino复制代码# ssh sshtron.zachlatta.com

图片

功能介绍

  • W A S D 或 vim 键绑定移动(不要使用箭头键);
  • 退出或按 Ctrl + C 退出;

GitHub 地址:github.com/zachlatta/s…

battle-city

基于 React 的经典坦克大战,这个 GitHub 仓库的版本是经典坦克大战的复刻版本,基于原版素材,使用 React 将各类素材封装为对应的组件。素材使用 SVG 进行渲染以展现游戏的像素风,可以先调整浏览器缩放再进行游戏,1080P 屏幕下使用 200% 缩放为最佳。

此游戏使用网页前端技术进行开发,主要通过 React 进行页面展现,使用 Immutable.js 作为数据结构工具库,使用 redux 管理游戏状态,以及使用 redux-saga/little-saga 处理复杂的游戏逻辑。

图片

GitHub 地址:github.com/shinima/bat…

游戏在线地址:shinima.pw/battle-city…

pacman

Pacman 是基于 HTML5 的吃豆人游戏。该项目在 GitHub 上的核心代码就两个文件,代码有注释、整洁。对于新手来说是个很好的实践项目。

图片

GitHub 地址:github.com/mumuy/pacma…

游戏在线地址:passer-by.com/pacman/

ratel

Ratel 这个项目是基于 Netty 实现的一款命令行斗地主游戏。

Ratel 分客户端和服务端,你可以让小伙伴们的客户端都连接你的服务器进行游戏,也可以直接连接作者的公网服务器进行游戏。

图片

详情可参考:《摸鱼神器:在 Linux 命令行下玩斗地主!》这篇文章介绍了如何安装 Ratel?以及一些 Ratel 的玩法。

GitHub 地址:github.com/ainilili/ra…

lila

lila 是一款基于 Scala 语言,完全免费、开源、没有广告、支持多语言的在线国际象棋游戏。

图片

GitHub 地址:github.com/ornicar/lil…

游戏在线地址:lichess.org/

star-battle

star-battle 是一个使用 JavaScript ES6、Canvas 开发的飞船射击类游戏。

图片

功能介绍

  • 使用 W、A、S、D 键控制飞船,按下 Space 发射;
  • 燃料初始值为 15,每秒递减 1 点,当燃料值为 0 时,游戏结束;
  • 触碰掉下的燃料瓶可增加 15 点,最大值为 30 点;
  • 击中敌方飞船增加 5 分。行星需击中两次,增加 10 分。击中友方减 10 分;
  • 撞击敌方损失 15 点燃料,撞击友方扣除 10 分;
  • 游戏允许负分;
  • 按下 P 暂停游戏,按下 M 静音;

GitHub 地址:github.com/gd4Ark/star…

游戏在线地址:4ark.me/star-battle…

PythonPlantsVsZombies

PythonPlantsVsZombies 是用 Python 语言编写的植物大战僵尸。

图片

功能介绍

  • PlantsVsZombies 所支持的植物类型:向日葵、豌豆射手、寒冰射手、坚果、樱桃炸弹等;
  • PlantsVsZombies 所支持的僵尸类型:普通僵尸、鞭打僵尸,锥头僵尸,水桶头僵尸,报纸僵尸等;
  • 该项目可使用 json 文件来存储关卡数据信息(例如僵尸的位置和时间,背景信息);
  • 支持在关卡开始前选择植物卡;
  • 支持白天/夜间模式,移动卡选择水平和 Wallnut 保龄球水平;

运行 main.py 文件即可运行游戏:

1
2
3
4
5
6
python复制代码# python main.py
import pygame as pg
from source.main import main
if __name__=='__main__':
main()
pg.quit()

GitHub 地址:
github.com/marblexu/Py…

HueJumper2k

这个项目是用 JS 实现 2KB 大小的 3D 赛车游戏。

图片

图片

控制项

  • 鼠标 = 转向
  • 点击 = 刹车
  • 双击 = 跳转
  • R = 重新启动
  • 1 = 屏幕图

GitHub 地址:

github.com/KilledByAPi…

游戏在线地址:

killedbyapixel.itch.io/hue-jumper

free-python-games

免费的入门级 Python 游戏集合库,都是一些简单的小游戏:贪吃蛇、迷宫、Pong、猜字等,运行方便、代码简单易懂。以游戏的方式开启你的 Python 学习之旅,玩完再学源码,真是其乐无穷。

图片

GitHub 地址:github.com/grantjenks/…

css-sweeper

一个只用 HTML 和 CSS 实现的扫雷游戏。

图片

GitHub 地址:github.com/propjockey/…

游戏在线地址:propjockey.github.io/css-sweeper…

emoji-minesweeper

Emoji-minesweeper 是一款表情符号的扫雷游戏。

图片

功能介绍

  • 左键单击可开启一个地点;
  • 右键单击可将一个点标记为炸弹;
  • 双击以打开目标附近的所有 8 个点(使用右键单击已标记为炸弹的点除外)

GitHub 地址:

github.com/muan/emoji-…

游戏在线地址:

muan.github.io/emoji-mines…

MazeBattles.com

使用 Node.js 和 Socket.io 实现的在线迷宫游戏,通过 [a][w][s][d] 按键移动位置,支持多人和单人两种模式。

图片

GitHub 地址:

github.com/HenryDavidZ…

游戏在线地址:

www.mazebattles.com/

flexboxfroggy

一个帮助学习 CSS flexbox 知识的在线游戏。

游戏一共 24 关,通俗易懂的解释了 flex 布局,适合初学者,支持中文,可以在 settings 中选择语言。

图片

GitHub 地址:

github.com/thomaspark/…

游戏在线地址:

flexboxfroggy.com/

gorched

Go 语言写的终端游戏 Scorched Earth。

图片

控制项

  • ← → 改变大炮角度
  • SPACE 开始加载(第一击)并射击(第二击)
  • Ctrl+C 退出游戏
  • Ctrl+R 重新开始当前回合
  • Ctrl+N 开始下一轮
  • S 显示分数
  • A 显示玩家的属性
  • H 显示帮助

GitHub 地址:github.com/zladovan/go…

游戏在线地址:repl.it/@zladovan/g…

Mindustry

一款 Java 编写的免费沙盒塔防游戏。支持多平台:Windows、Linux、macOS、Android

图片

GitHub 地址:github.com/Anuken/Mind…

AIDungeon

AIDungeon 是一个基于机器学习的地下城文字游戏。

图片

GitHub 地址:

github.com/Latitude-Ar…

游戏在线地址:

play.aidungeon.io/main/landin…

OpenEmu

OpenEmu 是一个可以玩各种复古游戏的游戏机,支持任天堂、索尼 PSP、世嘉 32X 等 30 多种游戏引擎,以及支持外接游戏手柄、投屏等操作;

图片

GitHub 地址:github.com/OpenEmu/Ope…

gameboy.live

gameboy.live 是一个具有终端 “云游戏” 支持的基本 Gameboy 模拟器,可通过 Socket 远程玩像素游戏。

图片

功能介绍

  • CPU指令仿真
  • 计时器和中断
  • 支持仅ROM,MBC1,MBC2,MBC3磁带
  • 声音模拟
  • 图形仿真
  • 云游戏
  • ROM调试器
  • 游戏保存和恢复卡带级别

GitHub 地址:github.com/HFO4/gamebo…

cxk-ball

这是一款用 Javascript 实现的 CXK 打篮球游戏。

图片

该游戏有多种模式可选择:

  • 简单
  • 普通
  • 困难
  • 极限
  • 非人类

图片

GitHub 地址:github.com/kasuganosor…

游戏在线地址:cxk.ssrr.one/

最后

以上就是今天所要分享的全部内容了。

如果你觉得这篇文章对你有点用的话,就请为本文留个言,点个赞,或者转发一下,让更多的朋友看到,因为这将是我持续输出更多优质文章的最强动力!

当然了,如果你有挖掘到 GitHub 上关于游戏项目或其他类型的开源项目,也欢迎大家留言分享。

本文转载自: 掘金

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

Mac系统安装多版本 PHP,低版本 PHP

发表于 2021-04-28

本文主要介绍如何在 Mac 电脑, 使用 HomeBrew 安装各版本的 PHP。主要解决低版本不好安装,以及多版本使用问题。同样支持 M1 Mac (Apple Silicon)。

现在 PHP 的版本已经到 8.0,但是在各公司的生产环境中,大量使用的仍然是 7.x 版本,甚至 5.6 版本。对后端开发工程师来说,在自己的电脑上开发,最好安装与公司生产环境相同的版本,来保证开发过程中代码的适配性。

安装低版本的 PHP,对于不会编译安装的同学来说,有些困难。甚至对于一些中高级工程师,也并不容易。主要原因在于 PHP 不同版本编译安装依赖的一些底层包的版本问题,以及 Mac OS 本身系统的一些包并不适配。大部分人往往安装 PHP 要耗费一到两个小时时间,甚至多半天。本文主要目的在于又快又好的搞定开发环境,并不讨论原生编译安装,以及怎么样的方式更好的问题。

如果你是”老工程师”

1
2
3
bash复制代码brew tap shivammathur/php
brew search php
brew install shivammathur/php/php@5.6 //可选择其他版本

图片

如果你是”新手”

首先声明,Mac OS 本身是自带了PHP的,PHP 的版本根据Mac OS 的不同而不同。如果自带的够你用了,那么就用自带的吧。仍然建议你安装新的,之后好管理。

第一步,安装 HomeBrew,该工具可以让你以后安装其他软件更快更整洁。**网址为:brew.sh/index\_zh-c… 。可自行搜索安装。也可以使用下面的命令:

1
bash复制代码/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"

HomeBrew 相当于一个存放技术软件相关的“软件管家”,包含常用技术工具,如Mysql、NodeJS、Redis 等等。**建议学习此工具的使用,也可以安装其他软件。有事半功倍的效果。

安装完成后。会显示相关信息。==> Installation successful!

很可能你会在这一步走不下去,没关系,搜索一下换成国内的清华源或者中科大源。

第二步,搜索可用 PHP 版本。如果有你所需的版本,直接安装即可。

1
sql复制代码brew search php

图片

如果安装失败了,或者提示带有关键词 icu4c 的错误,那么,先放弃吧,试试下面的。icu4c 错误可以解决,但是非常麻烦。

第三步,如果没有你所需的版本,可添加其他”仓库”。

稍微解释一下,你可以认为是这个“软件管家”有一个主要仓库,这个仓库里面只有最新最常用的 货物。而你想要一些旧的货物,是需要从其他地方调货的。所以需要添加其他分仓库。

添加旧的PHP仓库:

1
bash复制代码brew tap shivammathur/php

安装完成后,再次搜索

1
sql复制代码brew search php

图片

可以看到最右侧,有很多版本。

选择你喜欢的版本。

1
bash复制代码brew install shivammathur/php/php@8.0

安装多版本

只需要选择另一个版本,进行安装即可。比如此时我已经安装了 8.0 我再安装一个 7.3。

1
bash复制代码brew install shivammathur/php/php@7.3

安装过程是这样的

图片

安装完成后提示:请按照提示添加相关环境变量。类似下述代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
perl复制代码php@7.1 is keg-only, which means it was not symlinked into /usr/local,
because this is an alternate version of another formula.

If you need to have php@7.1 first in your PATH run:
echo 'export PATH="/usr/local/opt/php@7.1/bin:$PATH"' >> ~/.zshrc
echo 'export PATH="/usr/local/opt/php@7.1/sbin:$PATH"' >> ~/.zshrc

For compilers to find php@7.1 you may need to set:
export LDFLAGS="-L/usr/local/opt/php@7.1/lib"
export CPPFLAGS="-I/usr/local/opt/php@7.1/include"

To have launchd start exolnet/deprecated/php@7.1 now and restart at login:
brew services start exolnet/deprecated/php@7.1
Or, if you don't want/need a background service you can just run:
php-fpm

php -v 看看版本吧。如果版本没变,重启下你的terminal。

如何切换版本

1
复制代码brew install brew-php-switcher

安装好后,我现在是 7.1 我要切换到 7.3

1
复制代码brew-php-switcher 7.3

图片

切换完记得按照提示添加相关环境变量。重启terminal。

图片

其他:

phpini 文件在哪里?

1
css复制代码php --ini

或者

1
2
3
javascript复制代码➜  php -i | grep 'php.ini'
Configuration File (php.ini) Path => /usr/local/etc/php/7.0
Loaded Configuration File => /usr/local/etc/php/7.0/php.ini

怎么重启php?

brew services restart php@7.1 //start stop

报错提示:

图片

解决:按照提示,安装xcode-select 工具。注意,这里不是安装xcode,不用害怕。

打开terminal 执行 xcode-select –install

结束语:在该研究技术的时候研究技术,该追求效率的时候追求效率。

文章结束,仅供参考,欢迎讨论。


程序员阿菜 搞懂技术 | 看清生活

PHP、Go 后端程序员学习之路。主要分享后端技术,立志于说清楚真实工作中的编程。一起爬上开悟之坡。

本文转载自: 掘金

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

Spring Cloud Alibaba(四):Nacos服

发表于 2021-04-28

微服务的起点就是服务注册与发现,Spring Cloud Alibaba系列将使用Nacos作为服务注册与发现。

本节主要以nacos-server 1.4.1版本进行演示。

1 服务端

1.1 下载 nacos-server

download.csdn.net/download/qq…

1.2 启动服务

以Mac为例,演示。

进入到 nacos/bin 目录。

启动命令:

1
shell复制代码sh startup.sh -m standalone

1.3 管理平台

地址:http://localhost:8848/nacos

默认账号:nacos

默认密码:nacos

1.4 停止服务

以Mac为例,演示。

进入到 nacos/bin 目录。

启动命令:

1
shell复制代码sh shutdown.sh

1.5 界面截图

image-20210428161706748.png

image-20210428162309386.png

2 客户端

2.1 依赖

1
2
3
4
5
xml复制代码<!-- nacos 服务注册 -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>

2.2 自动配置

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
java复制代码/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.fengwenyi.demouserservicecore.config;

import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.context.annotation.Configuration;

/**
* @author <a href="mailto:chenxilzx1@gmail.com">theonefx</a>
*/
@EnableDiscoveryClient
@Configuration
public class NacosDiscoveryConfiguration {
}

2.3 配置

1
2
properties复制代码spring.cloud.nacos.discovery.server-addr=127.0.0.1:8848
spring.cloud.nacos.discovery.group=springcloud-alibaba-demo

2.4 界面截图

image-20210428163952606.png

3 资料

  • nacos-server:github.com/alibaba/nac…
  • springcloud-alibaba-demo:gitee.com/fengwenyi/s…

本文转载自: 掘金

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

盘点 SpringBoot Application 主流

发表于 2021-04-28

首先分享之前的所有文章 , 欢迎点赞收藏转发三连下次一定 >>>> 😜😜😜

文章合集 : 🎁 juejin.cn/post/694164…

Github : 👉 github.com/black-ant

CASE 备份 : 👉 gitee.com/antblack/ca…

一 . 前言

这一篇来说一说 SpringBoot Application 的主流程.

SpringAppliation 的主流程入口很简单 :

1
2
3
4
5
6
7
8
9
java复制代码@SpringBootApplication
public class BaseApplication {
public static void main(String[] args) {
SpringApplication.run(BaseApplication.class, args);
}
}

1 > 使用 @SpringBootApplication 注解,标明是 Spring Boot 应用。通过它,可以开启自动配置的功能。
2 > main 方法 : 调用 SpringApplication#run(Class<?>... primarySources) 方法,启动 Spring Boot 应用

我们从这个入口一步步看看 , 这个流程里面到底做了什么 , 其中主要涉及这几件事:

  • 创建且启动 listener
  • 通过 args 生成 environment
  • 通过 environment 创建 context
  • 刷新 context
  • 当然 , 还会打印 Banner

流程图

SpringBoot 流程.png

二 . 流程解析

2.1 SpringApplication 核心流程

核心流程主要在SpringApplication.class 中 ,我们从这个流程看看:

SpringApplication 属性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
java复制代码F- resourceLoader
?- 资源加载器
F- primarySources 
?- 主要的 Java Config 类的数组
F- webApplicationType 
?- 调用 WebApplicationType#deduceFromClasspath() 方法,通过 classpath ,判断 Web 应用类型。
F- listeners 
?- ApplicationListener 数组。
F- mainApplicationClass 
?- 调用 #deduceMainApplicationClass() 方法,获得是调用了哪个 #main(String[] args) 方法


// 提供了三种 ApplicationContext 加载类 , 这个后续会用上
String DEFAULT_CONTEXT_CLASS = "org.springframework.context.annotation.AnnotationConfigApplicationContext";
String DEFAULT_SERVLET_WEB_CONTEXT_CLASS = "org.springframework.boot.web.servlet.context.AnnotationConfigServletWebServerApplicationContext";
String DEFAULT_REACTIVE_WEB_CONTEXT_CLASS = "org.springframework.boot.web.reactive.context.AnnotationConfigReactiveWebServerApplicationContext";


// 默认Banner 地址 -> "banner.txt"
String BANNER_LOCATION_PROPERTY_VALUE = SpringApplicationBannerPrinter.DEFAULT_BANNER_LOCATION;

SpringApplication run 方法主流程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
java复制代码M1_01- run()
T-> 计时 ,创建 StopWatch 并且启动 , 用于记录启动的时长
-> configureHeadlessProperty() : 配置 headless 属性
?- Headless模式是系统的一种配置模式。在该模式下,系统缺少了显示设备、键盘或鼠标
?- 该模式下可以创建轻量级组件 , 收集字体等前置工作
- getRunListeners : 获取 SpringApplicationRunListeners ,并且开启监听 listeners.starting()
- 1 创建  ApplicationArguments 对象
- 2 prepareEnvironment 加载属性配置(传入 listener + arguments ) -> M20_01
?- 执行完成后,所有的 environment 的属性都会加载进来 (application.properties等)
- 3 打印banner(printBanner)
- 4 创建Spring 容器(createApplicationContext)
- 准备异常对象(getSpringFactoriesInstances.SpringBootExceptionReporter )
- 5 调用所有初始化类的 initialize 方法(prepareContext) , 初始化Spring 容器
- 6 刷新容器(refreshContext) , 执行 Spring 容器的初始化的后置逻辑(afterRefresh)
T-> 计时完成
- 7 通知 SpringApplicationRunListener , 执行异常处理等收尾

SpringApplication 主流程伪代码

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
java复制代码public ConfigurableApplicationContext run(String... args) throws Exception {
StopWatch stopWatch = new StopWatch();
stopWatch.start();
ConfigurableApplicationContext context = null;
Collection<SpringBootExceptionReporter> exceptionReporters = new ArrayList<>();
configureHeadlessProperty();
SpringApplicationRunListeners listeners = getRunListeners(args);
listeners.starting();
try {
ApplicationArguments applicationArguments = new DefaultApplicationArguments(args);
// 调用 M1_21 获取 ConfigurableEnvironment
ConfigurableEnvironment environment = prepareEnvironment(listeners, applicationArguments);
configureIgnoreBeanInfo(environment);
Banner printedBanner = printBanner(environment);
// PS:M1_01_03
context = createApplicationContext();
// 从 factories 中获取 Exception M1_11
exceptionReporters = getSpringFactoriesInstances(SpringBootExceptionReporter.class,
new Class[] { ConfigurableApplicationContext.class }, context);
// M1_35 : 为 Context 添加属性
prepareContext(context, environment, listeners, applicationArguments, printedBanner);
// M1_50 : 刷新容器Bean
refreshContext(context);
afterRefresh(context, applicationArguments);
// 计时结束
stopWatch.stop();
if (this.logStartupInfo) {
new StartupInfoLogger(this.mainApplicationClass).logStarted(getApplicationLog(), stopWatch);
}
listeners.started(context);

// 执行 implements ApplicationRunne 对象
callRunners(context, applicationArguments);
}catch (Throwable ex) {
handleRunFailure(context, ex, exceptionReporters, listeners);
throw new IllegalStateException(ex);
}

try {
listeners.running(context);
}catch (Throwable ex) {
handleRunFailure(context, ex, exceptionReporters, null);
throw new IllegalStateException(ex);
}
return context;
}

2.2 子模块 : Enviroment 处理

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
java复制代码M1_21- prepareEnvironment
- getOrCreateEnvironment -> M21_01
-

private ConfigurableEnvironment prepareEnvironment(SpringApplicationRunListeners listeners,
ApplicationArguments applicationArguments) {
// 内部通过 WebApplicationType 生成不同的 Environment (可以set 自己的 Environment) -> M1_23
ConfigurableEnvironment environment = getOrCreateEnvironment();
// 重写此方法以完全控制环境自定义,或者重写上述方法之一以分别对属性源或概要文件进行细粒度控制。 -> M1_24
configureEnvironment(environment, applicationArguments.getSourceArgs());
// 对 configurationProperties 属性进行处理 -> M2_01
ConfigurationPropertySources.attach(environment);
// listener 处理
listeners.environmentPrepared(environment);
// 将 environment 绑定到 SpringApplication
bindToSpringApplication(environment); -> M1_25
if (!this.isCustomEnvironment) {
environment = new EnvironmentConverter(getClassLoader()).convertEnvironmentIfNecessary(environment,deduceEnvironmentClass());
}
ConfigurationPropertySources.attach(environment);
return environment;
}

M1_23- getOrCreateEnvironment
- 通过 webApplicationType , 创建三种不同的 Environment

// M1_23 伪代码
private ConfigurableEnvironment getOrCreateEnvironment() {
if (this.environment != null) {
return this.environment;
}
switch (this.webApplicationType) {
case SERVLET:
return new StandardServletEnvironment();
case REACTIVE:
return new StandardReactiveWebEnvironment();
default:
return new StandardEnvironment();
}
}


M1_24- configureEnvironment
- 获取 ApplicationConversionService 的实现类
- 调用 configurePropertySources(environment, args) , 在此应用程序环境中添加、删除或重新排序任何PropertySources。
- MutablePropertySources sources = environment.getPropertySources();
?- 获取前面的 MutablePropertySources
- sources.addLast : 添加 defaultProperties
- 如果 args > 0 , 且可以添加 commonLine , 则添加CommandLineProperties
?- 其中会判断属性 commandLineArgs 是否会存在 ,存在则常用替换方式

// M1_24 伪代码
protected void configureEnvironment(ConfigurableEnvironment environment, String[] args) {
if (this.addConversionService) {
ConversionService conversionService = ApplicationConversionService.getSharedInstance();
environment.setConversionService((ConfigurableConversionService) conversionService);
}
configurePropertySources(environment, args);
configureProfiles(environment, args);
}



C2- ConfigurationPropertySources.
M2_01- attach(environment);
- sources = ((ConfigurableEnvironment) environment).getPropertySources() : 获取 MutablePropertySources
- attached = sources.get(ATTACHED_PROPERTY_SOURCE_NAME) : 获取 PropertySource
- 如果 attached 为null 或者 不等于 sources , 则将 sources 替换原先的 attached

// Binder : 从一个或多个容器绑定对象的容器对象
M1_25- bindToSpringApplication
C3- Binder
M3_01- bind(ConfigurationPropertyName name, Bindable<T> target, BindHandler handler, Context context,boolean allowRecursiveBinding, boolean create)
- context.clearConfigurationProperty() : 清空原属性
- handler.onStart(name, target, context) : BindHandler 开始绑定
- bindObject : 将 属性绑定到对象
- handleBindResult : 返回 绑定结果


private <T> T bind(ConfigurationPropertyName name, Bindable<T> target, BindHandler handler, Context context,boolean allowRecursiveBinding, boolean create) {
try {
Bindable<T> replacementTarget = handler.onStart(name, target, context);
if (replacementTarget == null) {
return handleBindResult(name, target, handler, context, null, create);
}
target = replacementTarget;
Object bound = bindObject(name, target, handler, context, allowRecursiveBinding);
return handleBindResult(name, target, handler, context, bound, create);
}catch (Exception ex) {
return handleBindError(name, target, handler, context, ex);
}
}

configureIgnoreBeanInfo 配置

1
2
3
4
5
6
7
8
9
10
11
12
13
java复制代码// 其中仅设置了2个属性 : 

// 属性一 : spring.beaninfo.ignore , 用于
environment.getProperty("spring.beaninfo.ignore", Boolean.class, Boolean.TRUE)

// 设置二 : 设置到 System 中
System.setProperty(CachedIntrospectionResults.IGNORE_BEANINFO_PROPERTY_NAME, ignore.toString())

//那么该属性是为了干什么 ? -> spring.beaninfo.ignore
当值为 true 时 , 意味着跳过对 BeanInfo 类的搜索 .
如果经历了对不存在的 BeanInfo 类的重复 ClassLoader 访问,可以考虑将这个标志切换为“ true”,以防在启动或延迟加载时这种访问开销很大

但是现阶段所有 BeanInfo 元数据类,默认值是"false"

2.3 Banner 的打印

纯粹是好奇 , 过来看一眼

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
java复制代码
M1_28- printBanner : 准备 Banner 类


// 代码详情
private Banner printBanner(ConfigurableEnvironment environment) {
if (this.bannerMode == Banner.Mode.OFF) {
return null;
}
ResourceLoader resourceLoader = (this.resourceLoader != null)
? this.resourceLoader
: new DefaultResourceLoader(getClassLoader());

SpringApplicationBannerPrinter bannerPrinter = new SpringApplicationBannerPrinter(resourceLoader, this.banner);
if (this.bannerMode == Mode.LOG) {
return bannerPrinter.print(environment, this.mainApplicationClass, logger);
}
// 核心代码 -> 调用类 SpringBootBanner
return bannerPrinter.print(environment, this.mainApplicationClass, System.out);
}

// 核心类 SpringBootBanner
C- SpringBootBanner
M- printBanner
- printStream.println(line) : 逐行打印那个 Spring
- 打印版本号
?- :: Spring Boot :: (v2.3.1.RELEASE)

// ps : 点进去就知道了 , 逐行打印
  • 提供了一个开关
  • 构建一个 SpringApplicationBannerPrinter ,
  • 调用 print 生成了一个 Banner 对象

2.4 createApplicationContext 逻辑

该逻辑为创建 ApplicationContext 的相关逻辑 , 这里先简单过一下 :

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
java复制代码
C- SpringApplication
M1_30- createApplicationContext
- Class<?> contextClass = this.applicationContextClass;
IF- contextClass为null
- 根据 webApplicationType 类型,获得 ApplicationContext 类型
- AnnotationConfigServletWebServerApplicationContext 
- AnnotationConfigApplicationContext
- AnnotationConfigReactiveWebServerApplicationContext
- 根据 contextClass 创建 ApplicationContext 对象


// M1_30 伪代码
protected ConfigurableApplicationContext createApplicationContext() {
Class<?> contextClass = this.applicationContextClass;
if (contextClass == null) {
try {
// 根据不同 webApplicationType 准备不同的 ContextClass
switch (this.webApplicationType) {
case SERVLET:
contextClass = Class.forName(DEFAULT_SERVLET_WEB_CONTEXT_CLASS);
break;
case REACTIVE:
contextClass = Class.forName(DEFAULT_REACTIVE_WEB_CONTEXT_CLASS);
break;
default:
contextClass = Class.forName(DEFAULT_CONTEXT_CLASS);
}
}
catch (ClassNotFoundException ex) {
throw new IllegalStateException(
"Unable create a default ApplicationContext, please specify an ApplicationContextClass", ex);
}
}
// 反射构造器方法获得 context 实现类
return (ConfigurableApplicationContext) BeanUtils.instantiateClass(contextClass);
}


// ApplicationContext 核心流程值得深入 , 后面开一个单章来详细是说

PS:M1_01_03 生成一个 ConfigurableApplicationContext
data-applicationContext.jpg

PS : ApplicationContext 体系结构

ApplicationContext.png

这里补充一下 Reactive 的区别

SpringBoot-Stack.jpg

如图所示 , Reactive 是 Spring 中一个重要的技术栈 , Reactive 可以用于构建响应性、弹性、弹性和消息驱动的企业级反应系统.

WebFlux 并不是 Spring MVC 替代,它主要应用还是在异步非阻塞编程模型上 , 使用了 WebFlux 的应用,其整体响应时间更短,启动的线程数更少,使用的内存资源更少。同时,延迟越大,WebFlux 的优势越明显。

具体可以参考这篇文档 @ blog.csdn.net/u010862794/…

2.5 中间操作

M1_11 获取 SpringBootExceptionReporter 的处理类

1
2
3
4
5
6
7
java复制代码getSpringFactoriesInstances(SpringBootExceptionReporter.class,new Class[] { ConfigurableApplicationContext.class }, context);


// Spring.factories
# Error Reporters
org.springframework.boot.SpringBootExceptionReporter=\
org.springframework.boot.diagnostics.FailureAnalyzers

M1_12 callRunners : 干什么

该方法主要是运行 ApplicationRunner

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
java复制代码// callRunners 主流程
private void callRunners(ApplicationContext context, ApplicationArguments args) {
List<Object> runners = new ArrayList<>();
runners.addAll(context.getBeansOfType(ApplicationRunner.class).values());
runners.addAll(context.getBeansOfType(CommandLineRunner.class).values());
AnnotationAwareOrderComparator.sort(runners);
for (Object runner : new LinkedHashSet<>(runners)) {
if (runner instanceof ApplicationRunner) {
callRunner((ApplicationRunner) runner, args);
}
if (runner instanceof CommandLineRunner) {
callRunner((CommandLineRunner) runner, args);
}
}
}


@Component
public class SourceTestLogic implements ApplicationRunner {

private Logger logger = LoggerFactory.getLogger(this.getClass());

@Override
public void run(ApplicationArguments args) throws Exception {
logger.info("------> run <-------");
}
}

// 简单点说 , ApplicationRunner 初始化启动就是这里做的

2.6 Context 的三次处理

Context 的二次处理分为 三步 :

  • prepareContext(context, environment, listeners, applicationArguments, printedBanner);
    • 设置 Context 的前置属性
  • refreshContext(context);
    • 注入 Bean , 注册监听器 , 初始化并发布事件
  • afterRefresh(context, applicationArguments);
    • 现阶段为空实现

prepareContext 的主流程

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
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
java复制代码// prepareContext(context, environment, listeners, applicationArguments, printedBanner);
M1_35- prepareContext : 准备 ApplicationContext 对象,主要是初始化它的一些属性
-> 1 设置 context 的 environment 属性
-> 2 调用 #postProcessApplicationContext(ConfigurableApplicationContext context) 方法,
?- 设置 context 的一些属性 -> M1_36
-> 3 调用 #applyInitializers(ConfigurableApplicationContext context) 方法,
?- 初始化 ApplicationContextInitializer -> applyInitializers
-> 4 调用 SpringApplicationRunListeners#contextPrepared
?- 通知 SpringApplicationRunListener 的数组,Spring 容器准备完成
-> 5 设置 beanFactory 的属性
-> 6 调用 #load(ApplicationContext context, Object[] sources) 方法,加载 BeanDefinition 们
?-
-> 创建 BeanDefinitionRegistry 对象
-> 设置 loader 属性
-> 执行BeanDefine 加载

// M1_35 伪代码
private void prepareContext(ConfigurableApplicationContext context, ConfigurableEnvironment environment,
SpringApplicationRunListeners listeners, ApplicationArguments applicationArguments, Banner printedBanner) {
// 为容器设置 environment
context.setEnvironment(environment);
// 设置容器 classloader 和 conversionService , 即容器中类的加载工具
postProcessApplicationContext(context);
// 在刷新上下文之前,将任何ApplicationContextInitializers应用于该上下文
applyInitializers(context);
// listeners 执行 , 在创建并准备好ApplicationContext之后调用,但在加载源之前调用。
listeners.contextPrepared(context);
if (this.logStartupInfo) {
// 打印启动日志配置
logStartupInfo(context.getParent() == null);
// 打印 active profile 信息
logStartupProfileInfo(context);
}
// Add boot specific singleton beans
// beanFactory 注入相关Bean
ConfigurableListableBeanFactory beanFactory = context.getBeanFactory();
beanFactory.registerSingleton("springApplicationArguments", applicationArguments);
if (printedBanner != null) {
beanFactory.registerSingleton("springBootBanner", printedBanner);
}

if (beanFactory instanceof DefaultListableBeanFactory) {
((DefaultListableBeanFactory) beanFactory)
.setAllowBeanDefinitionOverriding(this.allowBeanDefinitionOverriding);
}
if (this.lazyInitialization) {
// 添加一个新的BeanFactoryPostProcessor
// 该新的BeanFactoryPostProcessor将在刷新之前应用于此应用程序上下文的内部Bean工厂,然后再评估任何Bean定义。
// 在上下文配置期间调用。
context.addBeanFactoryPostProcessor(new LazyInitializationBeanFactoryPostProcessor());
}
// Load the sources
Set<Object> sources = getAllSources();
Assert.notEmpty(sources, "Sources must not be empty");
// -> M1_38
load(context, sources.toArray(new Object[0]));
listeners.contextLoaded(context);
}

M1_36- postProcessApplicationContext : 在ApplicationContext中应用任何相关的后处理
- beanNameGenerator 存在则设置到 context.getBeanFactory().registerSingleton 中
- resourceLoader 存在且 为 GenericApplicationContext类型 , 则 setResourceLoader
- resourceLoader 存在且 为 resourceLoader 则 setClassLoader
- addConversionService 为 true 则设置到 BeanFactory 中

// M1_36 伪代码
protected void postProcessApplicationContext(ConfigurableApplicationContext context) {
if (this.beanNameGenerator != null) {
context.getBeanFactory().registerSingleton(AnnotationConfigUtils.CONFIGURATION_BEAN_NAME_GENERATOR,this.beanNameGenerator);
}
if (this.resourceLoader != null) {
if (context instanceof GenericApplicationContext) {
((GenericApplicationContext) context).setResourceLoader(this.resourceLoader);
}
if (context instanceof DefaultResourceLoader) {
((DefaultResourceLoader) context).setClassLoader(this.resourceLoader.getClassLoader());
}
}
if (this.addConversionService) {
context.getBeanFactory().setConversionService(ApplicationConversionService.getSharedInstance());
}
}


M1_37- applyInitializers
FOR- (ApplicationContextInitializer initializer : getInitializers()) : for 循环处理 Initializers
- Assert判断 是否为 ApplicationContextInitializer 的实例
- initializer.initialize(context) 初始化对象


M1_38- load
- 这里主要是创建 BeanDefinitionLoader

protected void load(ApplicationContext context, Object[] sources) {
// 创建 BeanDefinitionLoader , 直接 new 的
BeanDefinitionLoader loader = createBeanDefinitionLoader(getBeanDefinitionRegistry(context), sources);
if (this.beanNameGenerator != null) {
// beanName 生成类
loader.setBeanNameGenerator(this.beanNameGenerator);
}
if (this.resourceLoader != null) {
loader.setResourceLoader(this.resourceLoader);
}
if (this.environment != null) {
loader.setEnvironment(this.environment);
}
// -> M5_01
loader.load();
}

C5- BeanDefinitionLoader
M5_01- loader
?- 这里会循环调用 load source

// M5_01 伪代码
for (Object source : this.sources) {
count += load(source);
}

refresh 流程

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
java复制代码
C- AbstractApplicationContext
M1_50- refreshContext(context);
- refresh(context);
- 如果有 ShutdownHook (关闭钩子) , 则注册 registerShutdownHook


@Override
public void refresh() throws BeansException, IllegalStateException {
synchronized (this.startupShutdownMonitor) {
// 准备此上下文以进行刷新。
prepareRefresh();

// Tell the subclass to refresh the internal bean factory.
ConfigurableListableBeanFactory beanFactory = obtainFreshBeanFactory();

// 告诉子类刷新内部bean工厂
prepareBeanFactory(beanFactory);

try {
// 允许在上下文子类中对bean工厂进行后处理
postProcessBeanFactory(beanFactory);

// 调用在上下文中注册为bean的工厂处理器
// IOC 的主要逻辑就在其中
invokeBeanFactoryPostProcessors(beanFactory);

// 注册拦截Bean创建的Bean处理器
registerBeanPostProcessors(beanFactory);

// 初始化此上下文的消息源
initMessageSource();

// 初始化此上下文的事件多主控器.
initApplicationEventMulticaster();

// 初始化特定上下文子类中的其他特殊bean.
onRefresh();

// 检查侦听器bean并注册它们.
registerListeners();

// 实例化所有剩余的(非lazy-init)单例.
finishBeanFactoryInitialization(beanFactory);

//最后一步:发布相应的事件
finishRefresh();
}catch (BeansException ex) {

// 销毁已创建的单件以避免资源悬空
destroyBeans();

// 重置“活动”标志.
cancelRefresh(ex);

// 将异常传播到调用方.
throw ex;
}

finally {
// Reset common introspection caches in Spring's core, since we
// might not ever need metadata for singleton beans anymore...
resetCommonCaches();
}
}
}

// 这一部分也是 AbstractApplicationContext 的主要流程 , 放在后续 ApplicationContext 单章说

afterRefresh 流程

PS : 这是一个空实现

1
java复制代码M1_60- afterRefresh

2.7 Listener 处理

Application 启动时 , Listener 总共涉及到四个操作 :

  • getRunListeners
  • listener.starting
  • listener.started
  • listener.running

以我这微薄的英语水平 , 这里面怕是有个进行时和一个过去时 , 哈哈哈哈哈

可以看到 , 第一步和第四步目的都比较明确 , 主要来看看第二 . 三步

getRunListeners

1
2
3
4
5
6
7
8
java复制代码C- SpringApplication
M- getRunListeners
- getSpringFactoriesInstances(SpringApplicationRunListener.class, types, this, args)

// 可以看到 , 从 Factories 中获取的 , 先阶段只有一个 EventPublishingRunListener
# Run Listeners
org.springframework.boot.SpringApplicationRunListener=\
org.springframework.boot.context.event.EventPublishingRunListener

listener.starting

1
2
3
4
5
6
7
8
9
java复制代码//这里的 starting 是 EventPublishingRunListener 运行
// 具体事件的处理逻辑 , 我们后续文档继续深入

C- EventPublishingRunListener
M- starting()
- this.initialMulticaster.multicastEvent(new ApplicationStartingEvent(this.application, this.args));

C- AbstractApplicationEventMulticaster
?- 将所有事件多播给所有注册的侦听器,并在调用线程中调用它们 , 简单点说就是事件群发

listeners.started(context)

1
2
3
4
5
6
7
8
9
10
11
12
13
java复制代码// 这里的事件就更多了

for (SpringApplicationRunListener listener : this.listeners) {
listener.started(context);
}

这里还是 EventPublishingRunListener

@Override
public void started(ConfigurableApplicationContext context) {
context.publishEvent(new ApplicationStartedEvent(this.application, this.args, context));
AvailabilityChangeEvent.publish(context, LivenessState.CORRECT);
}

listener.running

1
2
3
java复制代码// 发布 ReadyEvent
context.publishEvent(new ApplicationReadyEvent(this.application, this.args, context));
AvailabilityChangeEvent.publish(context, ReadinessState.ACCEPTING_TRAFFIC);

篇幅有限 , 就不详细看看哪些执行了

2.8 其他处理

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


private void handleRunFailure(ConfigurableApplicationContext context, Throwable exception,
Collection<SpringBootExceptionReporter> exceptionReporters, SpringApplicationRunListeners listeners) {
try {
try {
// 处理退出码
handleExitCode(context, exception);
if (listeners != null) {
listeners.failed(context, exception);
}
}finally {
reportFailure(exceptionReporters, exception);
if (context != null) {
context.close();
}
}
}catch (Exception ex) {
logger.warn("Unable to close ApplicationContext", ex);
}
ReflectionUtils.rethrowRuntimeException(exception);
}

三 . 总结

这一篇对 SpringBoot 的大概流程简单过了一遍 , 篇幅有限有几个点暂时先没纳入 , 后续补充

  • @SpringApplication 注解
  • SpringApplicationContext 的始末
  • Listener 的加载逻辑

更新记录

  • v20210804 : 更新布局 , 优化结构

本文转载自: 掘金

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

Spring Cloud Alibaba 微服务进阶实战 网

发表于 2021-04-28

下载地址:Spring Cloud Alibaba 微服务进阶实战 网盘下载

Spring Cloud Alibaba介绍

它为微服务提供了流量控制和融合降解的功能。它具有与hystrix相同的功能,可以有效地解决微服务调用的“雪崩”效应,为微服务系统提供稳定的解决方案。随着hytrxi进入维护期,不再提供新功能,sentinel是一个很好的选择。通常,hystrix使用一个线程池来隔离服务调用,Sentinel使用用户线程来隔离接口。与两者相比,hystrxi是服务级隔离,sentinel提供接口级隔离,sentinel隔离级别更精细,此外sentinel直接使用用户线程来限制,与hystrx的线程池隔离相比,减少了线程切换的开销。此外,sentinel的仪表板提供了更改当前限制规则的在线配置。也更优化。
从官方文献介绍来看,sentinel具有以下特点:
丰富的应用场景:近10年,sentinel一直承担着阿里巴巴双十一大流量推广的核心场景。例如spike(即系统容量范围内的突发流量控制)、消息峰值削峰填谷、实时断开下游不可用应用。完整的实时监控:sentinel还提供实时监控功能。您可以在控制台中看到连接到应用程序的单个计算机的二级数据。聚集运行的集群少于500个单位。广泛的开源生态系统:sentinel提供了与其他开源框架/库的开箱可用的集成模块,例如与spring cloud、dubbo、grpc的集成。您只需要引入相应的依赖项并执行简单的配置就可以快速访问sentinel。完整的spi扩展点:sentinel提供易于使用和完整的spi扩展点。您可以通过实现扩展点、Quick自定义逻辑来实现这一点。例如,自定义规则管理、自适应数据源等。

Spring Cloud Alibaba 微服务进阶实战

如何在spring cloud Alibaba中使用sentinel

sentinel作为阿里巴巴的组成部分之一,将其用于spring cloud项目非常简单。现在让我们以案例的形式解释如何在spring cloud项目中使用sentinel。这个项目是基于之前的nacos教程案例。将sentinel的spring cloud初始依赖项添加到项目的pom文件中,代码如下:

1
2
3
4
5
xml复制代码<dependency>
<groupid>org.springframework.cloud</groupid>
<artifactid>spring-cloud-starter-alibaba-sentinel</artifactid>
<version>0.9.0.release</version>
</dependency>
1
2
3
4
5
6
7
8
9
10
11
12
13
yaml复制代码server:
port:8762
spring:
application:
name:nacos-provider
cloud:
nacos:
discovery:
server-addr:127.0.0.1:8848
sentinel:
transport:
port:8719
dashboard:localhost:8080
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
less复制代码@restcontroller
public class providercontroller {
@getmapping ("/hi")
@sentinelresource (value="hi")
public string hi (@requestparam (value="name", defaultvalue="forezp", required=false) string name) {
return "hi" + name;
}
}
@restcontroller
public class providercontroller {
@getmapping ("/hi")
@sentinelresource (value="hi")
public string hi (@requestparam (value="name", defaultvalue="forezp", required=false) string name) {
return "hi" + name;
}
}@restcontroller
public class providercontroller {
@getmapping ("/hi")
@sentinelresource (value="hi")
public string hi (@requestparam (value="name", defaultvalue="forezp", required=false) string name) {
return "hi" + name;
}
}
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
typescript复制代码<dependency>
<groupid>org.springframework.cloud</groupid>
<artifactid>spring-cloud-starter-openfeign</artifactid>
</dependency>
<dependency>
<groupid>org.springframework.cloud</groupid>
<artifactid>spring-cloud-starter-alibaba-sentinel</artifactid>
<version>0.9.0.release</version>
</dependency>
In the configuration file, in addition to the sentinel.transport. Dashboard configuration, you also need to add the feign.sentinel.enabled configuration. The code is as follows:

server:
port:8763
spring:
application:
name:nacos-consumer
cloud:
nacos:
discovery:
server-addr:127.0.0.1:8848
sentinel:
transport:
port:8719
dashboard:localhost:8080
feign.sentinel.enabled:true
Write a feignclient and call the/hi interface of nacos-provider:

@feignclient ("nacos-provider")
public interface providerclient {
@getmapping ("/hi")
string hi (@requestparam (value="name", defaultvalue="forezp", required=false) string name);
}
Write a restcontroller to call providerclient, the code is as follows:

@restcontroller
public class consumercontroller {
@autowired
providerclient providerclient;
@getmapping ("/hi-feign")
public string hifeign () {
return providerclient.hi ("feign");
}
}

Spring Cloud Alibaba 微服务进阶实战

Spring Cloud Alibaba 微服务进阶实战: 基于Nacos实现Gateway动态网关

动态路由是指路由器可以自动建立自己的路由表,并根据实际情况的变化及时进行调整。动态路由器上的路由表项通过互联路由器相互交换信息,并按照一定的算法进行优化;路由信息在一定的时间间隔内不断更新,以适应不断变化的网络,获得最优的寻路效果。
最初的操作是:
动态路由机制的运行依赖于两个基本功能

  1. Nacos环境准备
    不引入特定的Nacos配置。你可以参考阿里巴巴的官方介绍。在这里,您可以通过windows直接启动单机模式,并登录到Nacos Console。创建名称空间dev,并在dev下默认分组下创建网关-路由器的dataId
    Spring Cloud Alibaba 微服务进阶实战:Nacos环境准备
1
2
3
4
5
6
7
8
9
10
11
12
13
14
css复制代码
[{ "id": "consumer-router", "order": 0, "predicates": [{ "args": { "pattern": "/consume/**" }, "name": "Path" }],
"uri": "lb://nacos-consumer"
},{
"id": "provider-router",
"order": 2,
"predicates": [{
"args": {
"pattern": "/provide/**"
},
"name": "Path"
}],
"uri": "lb://nacos-provider"
}]
  1. 连接到Nacos配置中心
    通常,项目中“配置中心”的配置通常是在引导中配置的。propertis (yaml),以确保从Nacos Config读取项目中的路由配置。
1
2
3
4
5
ini复制代码# nacosConfiguration Center configuration is recommended to be configured in bootstrap.properties
spring.cloud.nacos.config.server-addr=127.0.0.1:8848
#spring.cloud.nacos.config.file-extension=properties
# Configuration center namespace: dev namespace (environment)
spring.cloud.nacos.config.namespace=08ecd1e5-c042-410a-84d5-b0a8fbeed8ea
1
2
3
4
5
6
7
8
9
less复制代码@SpringBootApplication
@EnableDiscoveryClient
public class GatewayApplication
{
public static void main( String[] args )
{
SpringApplication.run(GatewayApplication.class, args);
}
}

本文转载自: 掘金

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

Redis 大数据量(百亿级)Key存储需求及解决方案 一

发表于 2021-04-28

最近我在思考实时数仓问题的时候,想到了巨量的redis的存储的问题,然后翻阅到这篇文章,与各位分享

一 需求背景

该应用场景为DMP缓存存储需求,DMP需要管理非常多的第三方id数据,其中包括各媒体cookie与自身cookie(以下统称supperid)的mapping关系,还包括了supperid的人口标签、移动端id(主要是idfa和imei)的人口标签,以及一些黑名单id、ip等数据。

在hdfs的帮助下离线存储千亿记录并不困难,然而DMP还需要提供毫秒级的实时查询。由于cookie这种id本身具有不稳定性,所以很多的真实用户的浏览行为会导致大量的新cookie生成,只有及时同步mapping的数据才能命中DMP的人口标签,无法通过预热来获取较高的命中,这就跟缓存存储带来了极大的挑战。

经过实际测试,对于上述数据,常规存储超过五十亿的kv记录就需要1T多的内存,如果需要做高可用多副本那带来的消耗是巨大的,另外kv的长短不齐也会带来很多内存碎片,这就需要超大规模的存储方案来解决上述问题。

二 存储何种数据

人⼝标签主要是cookie、imei、idfa以及其对应的gender(性别)、age(年龄段)、geo(地域)等;mapping关系主要是媒体cookie对supperid的映射。以下是数据存储⽰示例:

  1. PC端的ID:
1
2
3
js复制代码媒体编号-媒体cookie=>supperid

supperid => { age=>年龄段编码,gender=>性别编码,geo=>地理位置编码 }
  1. Device端的ID:
1
js复制代码imei or idfa => { age=>年龄段编码,gender=>性别编码,geo=>地理位置编码 }

显然PC数据需要存储两种key=>value还有key=>hashmap,⽽而Device数据需要存储⼀一种

key=>hashmap即可。

三 数据特点

  • 短key短value:
1
2
3
js复制代码其中superid为21位数字:比如1605242015141689522;
imei为小写md5:比如2d131005dc0f37d362a5d97094103633;
idfa为大写带”-”md5:比如:51DFFC83-9541-4411-FA4F-356927E39D04;
  • 媒体自身的cookie长短不一;
  • 需要为全量数据提供服务,supperid是百亿级、媒体映射是千亿级、移动id是几十亿级;
  • 每天有十亿级别的mapping关系产生;
  • 对于较大时间窗口内可以预判热数据(有一些存留的稳定cookie);
  • 对于当前mapping数据无法预判热数据,有很多是新生成的cookie;

4 存在的技术挑战

1)长短不一容易造成内存碎片;

2)由于指针大量存在,内存膨胀率比较高,一般在7倍,纯内存存储通病;

3)虽然可以通过cookie的行为预判其热度,但每天新生成的id依然很多(百分比比较敏感,暂不透露);

4)由于服务要求在公网环境(国内公网延迟60ms以下)下100ms以内,所以原则上当天新更新的mapping和人口标签需要全部in memory,而不会让请求落到后端的冷数据;

5)业务方面,所有数据原则上至少保留35天甚至更久;

6)内存至今也比较昂贵,百亿级Key乃至千亿级存储方案势在必行!

5 解决方案

5.1 淘汰策略

存储吃紧的一个重要原因在于每天会有很多新数据入库,所以及时清理数据尤为重要。主要方法就是发现和保留热数据淘汰冷数据。

网民的量级远远达不到几十亿的规模,id有一定的生命周期,会不断的变化。所以很大程度上我们存储的id实际上是无效的。而查询其实前端的逻辑就是广告曝光,跟人的行为有关,所以一个id在某个时间窗口的(可能是一个campaign,半个月、几个月)访问行为上会有一定的重复性。

数据初始化之前,我们先利用hbase将日志的id聚合去重,划定TTL的范围,一般是35天,这样可以砍掉近35天未出现的id。另外在Redis中设置过期时间是35天,当有访问并命中时,对key进行续命,延长过期时间,未在35天出现的自然淘汰。这样可以针对稳定cookie或id有效,实际证明,续命的方法对idfa和imei比较实用,长期积累可达到非常理想的命中。

5.2 减少膨胀

Hash表空间大小和Key的个数决定了冲突率(或者用负载因子衡量),再合理的范围内,key越多自然hash表空间越大,消耗的内存自然也会很大。再加上大量指针本身是长整型,所以内存存储的膨胀十分可观。先来谈谈如何把key的个数减少。

大家先来了解一种存储结构。我们期望将key1=>value1存储在redis中,那么可以按照如下过程去存储。先用固定长度的随机散列md5(key)值作为redis的key,我们称之为BucketId,而将key1=>value1存储在hashmap结构中,这样在查询的时候就可以让client按照上面的过程计算出散列,从而查询到value1。

过程变化简单描述为:get(key1) -> hget(md5(key1), key1) 从而得到value1。

如果我们通过预先计算,让很多key可以在BucketId空间里碰撞,那么可以认为一个BucketId下面挂了多个key。比如平均每个BucketId下面挂10个key,那么理论上我们将会减少超过90%的redis key的个数。

具体实现起来有一些麻烦,而且用这个方法之前你要想好容量规模。我们通常使用的md5是32位的hexString(16进制字符),它的空间是128bit,这个量级太大了,我们需要存储的是百亿级,大约是33bit(2的33次方),所以我们需要有一种机制计算出合适位数的散列,而且为了节约内存,我们需要利用全部字符类型(ASCII码在0~127之间)来填充,而不用HexString,这样Key的长度可以缩短到一半。

下面是具体的实现方式

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
js复制代码
public static byte [] getBucketId(byte [] key, Integer bit) {

MessageDigest mdInst = MessageDigest.getInstance("MD5");

mdInst.update(key);

byte [] md = mdInst.digest();

byte [] r = new byte[(bit-1)/7 + 1];// 因为一个字节中只有7位能够表示成单字符,ascii码是7位

int a = (int) Math.pow(2, bit%7)-2;

md[r.length-1] = (byte) (md[r.length-1] & a);

System.arraycopy(md, 0, r, 0, r.length);

for(int i=0;i<r.length;i++) {

if(r[i]<0) r[i] &= 127;

}

return r;

}

参数bit决定了最终BucketId空间的大小,空间大小集合是2的整数幂次的离散值。这里解释一下为何一个字节中只有7位可用,是因为redis存储key时需要是ASCII(0~127),而不是byte array。如果规划百亿级存储,计划每个桶分担10个kv,那么我们只需2^30=1073741824的桶个数即可,也就是最终key的个数。

5.3 减少碎片

碎片主要原因在于内存无法对齐、过期删除后,内存无法重新分配。通过上文描述的方式,我们可以将人口标签和mapping数据按照上面的方式去存储,这样的好处就是redis key是等长的。另外对于hashmap中的key我们也做了相关优化,截取cookie或者deviceid的后六位作为key,这样也可以保证内存对齐,理论上会有冲突的可能性,但在同一个桶内后缀相同的概率极低(试想id几乎是随机的字符串,随意10个由较长字符组成的id后缀相同的概率*桶样本数=发生冲突的期望值<<0.05,也就是说出现一个冲突样本则是极小概率事件,而且这个概率可以通过调整后缀保留长度控制期望值)。而value只存储age、gender、geo的编码,用三个字节去存储。

另外提一下,减少碎片还有个很low但是有效的方法,将slave重启,然后强制的failover切换主从,这样相当于给master整理的内存的碎片。

推荐Google-tcmalloc, facebook-jemalloc内存分配,可以在value不大时减少内存碎片和内存消耗。有人测过大value情况下反而libc更节约。

本文转载自: 掘金

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

帅呆了!Kafka移除了Zookeeper!

发表于 2021-04-28

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

普天同庆!最新版的Kafka 2.8.0,移除了对Zookeeper的依赖,通过KRaft进行自己的集群管理。很好很好,终于有点质的改变了。

一听到KRaft,我们就想到了Raft协议。Raft协议是当今最流行的分布式协调算法,Etcd、Consul等系统的基础,就来自于此。现在Kafka也有了。

由于这个功能太新了,所以2.8.0版本默认还是要用ZooKeeper的,但并不妨碍我们尝尝鲜。另外,不要太激动了,据官方声称有些功能还不是太完善,所以不要把它用在线上。

开源一套以教学为目的系统,欢迎star:github.com/xjjdog/bcma…。它包含ToB复杂业务、互联网高并发业务、缓存应用;DDD、微服务指导。模型驱动、数据驱动。了解大型服务进化路线,编码技巧、学习Linux,性能调优。Docker/k8s助力、监控、日志收集、中间件学习。前端技术、后端实践等。主要技术:SpringBoot+JPA+Mybatis-plus+Antd+Vue3。

  1. 如何开始KRaft?

Kafka使用内嵌的KRaft替代了ZooKeeper,是一个非常大的进步,因为像ES之类的分布式系统,这种集群meta信息的同步,都是自循环的。

但如何使用KRaft启动呢?很多同学直接晕菜了,这方面的资料也比较少,但使用起来非常简单。

我们注意到,在config目录下,多了一个叫做kraft的目录,里面包含着一套新的配置文件,可以直接摒弃对ZK的依赖。

image-20210428164657222.png

通过下面三行命令,即可开启一个单机的broker,从始至终没有ZK的参与。

1
2
3
bash复制代码# ./bin/kafka-storage.sh random-uuid
# ./bin/kafka-storage.sh format -t TBYU7WMiREexuZqrjKG60g -c ./config/kraft/server.properties
# ./bin/kafka-server-start.sh ./config/kraft/server.properties

经过一阵噼里啪啦的运行,No ZK的Kafka已经启动起来了。

image-20210428165304381.png

就是这么简单。

  1. 如何配置的?

kafka又加了一个内部主题,叫做@metadata,用来存这些元信息。

接下来我们就要看一些关键的配置信息。你可以使用vimdiff config/server.properties config/kraft/server.properties看一下这些主要的区别。

首先,kraft多了一个叫做process.roles的配置。在我们的配置文件里它是这样的。

1
properties复制代码process.roles=broker,controller

它其实有三个取值。

  • broker: 这台机器将仅仅当作一个broker
  • controller: 作为 Raft quorum的控制器之一进行启动
  • broker,controller: 包含两者的功能

熟悉ES的同学可以看出,这些划分就像是es的master和node,所以分布式的概念其实在一定程度上是相通的。

接下来是监听地址的变化,因为我们的server有了两个功能,所以也就需要开启两个端口。

1
properties复制代码listeners=PLAINTEXT://:9092,CONTROLLER://:9093

另外,还有一个叫做node.id的东西。不同于原来的broker.id,这个nodeid是用来投票用的。

1
properties复制代码node.id=1

因为raft协议的特性,我们的投票配置就要使用上面的node.id。写起来比较怪异是不是?但总比Zk的好看多了。所以这些配置在后面的版本是有可能改动的。

1
properties复制代码controller.quorum.voters=1@localhost:9093

这就是配置文件的主要区别。我们来看看它的集合。

1
2
3
4
properties复制代码process.roles=broker,controller 
listeners=PLAINTEXT://:9092,CONTROLLER://:9093
node.id=1
controller.quorum.voters=1@localhost:9093
  1. 为什么要干掉ZK?

Kafka作为一个消息队列,竟然要依赖一个重量级的协调系统ZooKeeper,不得不说是一个笑话。同样作为消息队列,人家RabbitMQ早早的就实现了自我管理。

Zookeeper非常笨重,还要求奇数个节点的集群配置,扩容和缩容也不方便。Zk的配置方式,也和kafka的完全不一样,要按照调优Kafka,竟然还要兼顾另外一个系统,这真是日了狗了。

Kafka要想往轻量级,开箱即用的方向发展,就不得不干掉Zk。

另外,由于Zk和Kafka毕竟不是在一个存储体系里面,当Topic和Partition的数量上了规模,数据同步问题就变的显著起来。Zk是可靠,但是它慢啊,完全不如放在Kafka的日志存储体系里面,这对标榜速度的Kafka来说,是不得不绕过的一环。

使用过Kafka-admin的同学,应该都对缓慢的监控数据同步历历在目。它需要先从zk上转一圈,获取一些元数据信息,然后再从Kafka的JMX接口中拉取数据。这么一转悠,就几乎让大型集群死翘翘。

  1. 会有哪些改变?

部署更简单。

首先,部署变的更加简单。对于一些不太追求高可用的系统,甚至一个进程就能把可爱的kafka跑起来。我们也不需要再申请对zookeeper友好的SSD磁盘,也不用再关注zk的容量是不是够用了。

监控更便捷。

其次,由于信息的集中,从Kafka获取监控信息,就变得轻而易举,不用再到zk里转一圈了。与grafana/kibana/promethus等系统的集成,指日可待。

速度更快捷。

最重要的当然是速度了。Raft比ZK的ZAB协议更加易懂,也更加高效,partition的主选举将变得更快捷,controller 的调度速度将上一个档次。

以后,再也不会有这样的连接方式。

1
properties复制代码zookeeper.connect=zookeeper:2181

取而代之的,只会剩下bootstrap的连接方式。Kafka的节点,越来越像对等节点。

1
properties复制代码bootstrap.servers=broker:9092

kafka还提供了一个叫做kafka-metadata-shell.sh的工具,能够看到topic和partion的分布,这些信息原来是可以通过zk获取的,现在可以使用这个命令行获取。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
bash复制代码$ ./bin/kafka-metadata-shell.sh  --snapshot /tmp/kraft-combined-logs/\@metadata-0/00000000000000000000.log
>> ls /
brokers local metadataQuorum topicIds topics
>> ls /topics
foo
>> cat /topics/foo/0/data
{
"partitionId" : 0,
"topicId" : "5zoAlv-xEh9xRANKXt1Lbg",
"replicas" : [ 1 ],
"isr" : [ 1 ],
"removingReplicas" : null,
"addingReplicas" : null,
"leader" : 1,
"leaderEpoch" : 0,
"partitionEpoch" : 0
}
>> exit

最后,还是要提醒一下,目前不要在线上环境开启这个功能,还是老老实实用ZK吧。功能就是原因,因为这些功能的配套设施还没有到位,代码也没有达到让人放心的程度。你要是用了,很可能会因为工具不全或者难缠的bug痛不欲生。

开源一套以教学为目的系统,欢迎star:github.com/xjjdog/bcma…。它包含ToB复杂业务、互联网高并发业务、缓存应用;DDD、微服务指导。模型驱动、数据驱动。了解大型服务进化路线,编码技巧、学习Linux,性能调优。Docker/k8s助力、监控、日志收集、中间件学习。前端技术、后端实践等。主要技术:SpringBoot+JPA+Mybatis-plus+Antd+Vue3。

不过,这勇敢的第一步已经卖出,方向也已经指明,我们剩下的就是等待了。无论如何,干掉Zk,是件大好事。

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

本文转载自: 掘金

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

史上最详Android版kotlin协程入门进阶实战(四)

发表于 2021-04-28

banners_twitter.png
由于文章涉及到的只是点比较多、内容可能过长,可以根据自己的能力水平和熟悉程度分阶段跳着看。如有讲述的不正确的地方劳烦各位私信给笔者,万分感谢image.png

由于时间原因,笔者白天工作只有晚上空闲时间才能写作,所以更新频率应该在一周一篇,当然我也会尽量的利用时间,争取能够提前发布。为了方便阅读将本文章拆分个多个章节,根据自己需要选择对应的章节,现在也只是目前笔者心里的一个大概目录,最终以更新为准:

Kotlin协程基础及原理系列

  • 史上最详Android版kotlin协程入门进阶实战(一) -> kotlin协程的基础用法
  • 史上最详Android版kotlin协程入门进阶实战(二) -> kotlin协程的关键知识点初步讲解
  • 史上最详Android版kotlin协程入门进阶实战(三) -> kotlin协程的异常处理
  • 史上最详Android版kotlin协程入门进阶实战(四) -> 使用kotlin协程开发Android的应用
  • 史上最详Android版kotlin协程入门进阶实战(五) -> kotlin协程的网络请求封装

Flow系列

  • Kotlin协程之Flow使用(一)
  • Kotlin协程之Flow使用(二)
  • Kotlin协程之Flow使用(三)

扩展系列

  • 封装DataBinding让你少写万行代码
  • ViewModel的日常使用封装

kotlin协程在Android中的基础应用

通过前面的三个章节,现在我们已经了解了kotlin协程的基本使用和相关基础知识点。如:

  1. 协程的基础使用方式和基本原理。
  2. CoroutineContext:协程上下文中包含的Element以及下上文的作用,传递。
  3. CoroutineDispatcher:协程调度器的使用
  4. CoroutineStart:协程启动模式在不同模式下的区别
  5. CoroutineScope:协程作用域的分类,以及不同作用域下的异常处理。
  6. 挂起函数以及suspend关键字的作用,以及Continuation的挂起恢复流程。
  7. CoroutineExceptionHandler:协程异常处理,结合supervisorScope和SupervisorJob的使用。

这一章节中,我们将主要讲解kotlin协程在Android中的基础使用。我们先引入相关扩展库组件库:

1
2
kotlin复制代码    implementation "androidx.activity:activity-ktx:1.2.2"
implementation "androidx.fragment:fragment-ktx:1.3.3"

Android使用kotlin协程

我们在之前的章节中使用协程的方式都是通过runBlocking或者使用GlobalScope的launch、async方式启动,当然也可以通过创建一个新的CoroutineScope,然后通过launch或者async方式启动一个新的协程。我们在讲解协程异常处理的篇章中就提到,通过SupervisorJob和CoroutineExceptionHandler实现了一个和supervisorScope相同的作用域。

1
2
3
4
5
6
7
8
9
10
11
kotlin复制代码private fun testException(){
val exceptionHandler = CoroutineExceptionHandler { coroutineContext, throwable ->
Log.d("exceptionHandler", "${coroutineContext[CoroutineName].toString()} 处理异常 :$throwable")
}
val supervisorScope = CoroutineScope(SupervisorJob() + exceptionHandler)
with(supervisorScope) {
launch{
}
//省略...
}
}

在第一节中我们提到runBlocking它会将常规的阻塞代码连接到一起,主要用于main函数和测试中。而GlobalScope又是一个全局顶级协程,我们在之前的案例中基本都使用这种方式。但是这个协程是在整个应用程序生命周期内运行的,如果我们用GlobalScope启动协程,我们启动一个将会变得极其繁琐,而且需要对于各种引用的处理以及管控异常取消操作。

我们可以先忽略CoroutineExceptionHandler协程异常处理。因为不管是任何方式启动协程,如果不在程上下文中添加CoroutineExceptionHandler,当产生未捕获的异常时都会导致应用崩溃。

那么下面代码会出现什么问题?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
kotlin复制代码private fun start() {
GlobalScope.launch{
launch {
//网络请求1...
throw NullPointerException("空指针")
}
val result = withContext(Dispatchers.IO) {
//网络请求2...
requestData()
"请求结果"
}
btn.text = result
launch {
//网络请求3...
}
}
}
  • 因为我们的GlobalScope默认使用的是Dispatchers.Default,这会导致我们在非主线程上刷新UI。
  • 子协程产生异常会产生相互干扰。子协程异常取消会导致父协程取消,同时其他子协程也将会被取消。
  • 如果我们这个时候activity或者framgent退出,因为协程是在GlobalScope中运行,所以即使activity或者framgent退出,这个协程还是在运行,这个时候会产生各种泄露问题。同时此协程当执行到刷新操作时,因为我们的界面已经销毁,这个时候执行UI刷新将会产生崩溃。

如果我们要解决上面的问题。我们得这么做:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
kotlin复制代码var job:Job? = null
private fun start() {
job = GlobalScope.launch(Dispatchers.Main + SupervisorJob()) {
launch {
throw NullPointerException("空指针")
}
val result = withContext(Dispatchers.IO) {
//网络请求...
"请求结果"
}
launch {
//网络请求3...
}
btn.text = result
}
}

override fun onDestroy() {
super.onDestroy()
job?.cancel()
}

我们先需要通过launch启动时加入Dispatchers.Main来保证我们是在主线程刷新UI,同时还需要再GlobalScope.launch的协程上下文中加入SupervisorJob来避免子协程的异常取消会导致整个协程树被终结。
最后我们还得把每次通过GlobalScope启动的Job保存下来,在activity或者framgent退出时调用job.cancel取消整个协程树。这么来一遍感觉还行,但是我们不是写一次啊,每次写的时候会不会感觉超麻烦,甚至怀疑人生。

image.png

所以官方在kotlin协程中提供了一个默认在主线程运行的协程:MainScope,我们可以通过它来启动协。

1
kotlin复制代码public fun MainScope(): CoroutineScope = ContextScope(SupervisorJob() + Dispatchers.Main)

我们可以看到MainScope的创建默认就使用了SupervisorJob和 Dispatchers.Main。说明我们可以通过MainScope来处理UI组件刷新。同时由于MainScope采用的是SupervisorJob,所以我们各个子协程中的异常导致的取消操作并不会导致MainScope的取消。这就很好的简化了我们通过GlobalScope去启动一个协程的过程。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
kotlin复制代码private val mainScope = MainScope()
private fun start() {
mainScope.launch {
launch {
throw NullPointerException("空指针")
}
val result = withContext(Dispatchers.IO) {
//网络请求...
"请求结果"
}
launch {
//网络请求3...
}
btn.text = result
}
}
override fun onDestroy() {
super.onDestroy()
mainScope.cancel()
}

通过使用MainScope我们是不是省略了很多操作。同时我们也不需要保存每一个通过MainScope启动的Job了,直接在最后销毁的时候调用mainScope.cancel()就能取消所有通过mainScope启动的协程。

这里多提一点:可能这里有的人会想,我使用GlobalScope也不保存启动的Job,直接GlobalScope.cancel不行吗?如果是这样的话,那么恭喜你喜提超级崩溃BUG一个。这里就不扩展了。可以自己动手去试试,毕竟实践出真理。

那可能还有人想,我连创建MainScope都懒得写,而且脑子经常不好使,容易忘记调用mainScope进行cancel操作怎么办。

image.png

官方早就为我们这些懒人想好了解决方案,这个时候我们只需要再集成一个ktx运行库就可以了。

在Activity与Framgent中使用协程

1
kotlin复制代码    implementation "androidx.lifecycle:lifecycle-runtime-ktx:2.3.1"

这个时候我们就可以在activity或者framgent直接使用lifecycleScope进行启动协程。我们看来看看activity中的lifecycleScope实现

1
2
kotlin复制代码public val LifecycleOwner.lifecycleScope: LifecycleCoroutineScope
get() = lifecycle.coroutineScope

我们可以到lifecycleScope它是通过lifecycle得到一个coroutineScope,是一个LifecycleCoroutineScope对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
kotlin复制代码public val Lifecycle.coroutineScope: LifecycleCoroutineScope
get() {
while (true) {
val existing = mInternalScopeRef.get() as LifecycleCoroutineScopeImpl?
if (existing != null) {
return existing
}
val newScope = LifecycleCoroutineScopeImpl(
this,
SupervisorJob() + Dispatchers.Main.immediate
)
if (mInternalScopeRef.compareAndSet(null, newScope)) {
newScope.register()
return newScope
}
}
}

我们可以看到lifecycleScope采用的和MainScope一样的创建CoroutineScope,同时它又通过结合lifecycle来实现当lifecycle状态处于DESTROYED状态的时候自动关闭所有的协程。

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
kotlin复制代码public abstract class LifecycleCoroutineScope internal constructor() : CoroutineScope {
internal abstract val lifecycle: Lifecycle
public fun launchWhenCreated(block: suspend CoroutineScope.() -> Unit): Job = launch {
lifecycle.whenCreated(block)
}
public fun launchWhenStarted(block: suspend CoroutineScope.() -> Unit): Job = launch {
lifecycle.whenStarted(block)
}
public fun launchWhenResumed(block: suspend CoroutineScope.() -> Unit): Job = launch {
lifecycle.whenResumed(block)
}
}


internal class LifecycleCoroutineScopeImpl(
override val lifecycle: Lifecycle,
override val coroutineContext: CoroutineContext
) : LifecycleCoroutineScope(), LifecycleEventObserver {
init {
if (lifecycle.currentState == Lifecycle.State.DESTROYED) {
coroutineContext.cancel()
}
}

fun register() {
launch(Dispatchers.Main.immediate) {
if (lifecycle.currentState >= Lifecycle.State.INITIALIZED) {
lifecycle.addObserver(this@LifecycleCoroutineScopeImpl)
} else {
coroutineContext.cancel()
}
}
}

override fun onStateChanged(source: LifecycleOwner, event: Lifecycle.Event) {
if (lifecycle.currentState <= Lifecycle.State.DESTROYED) {
lifecycle.removeObserver(this)
coroutineContext.cancel()
}
}
}

同时我们也可以通过launchWhenCreated、launchWhenStarted、launchWhenResumed来启动协程,等到lifecycle处于对应状态时自动触发此处创建的协程。

比如我们可以这么操作:

1
2
3
4
5
6
7
8
9
10
11
12
kotlin复制代码class MainTestActivity : AppCompatActivity() {
init {
lifecycleScope.launchWhenResumed {
Log.d("init", "在类初始化位置启动协程")
}
}

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
}
}
1
2
kotlin复制代码D/onResume: onResume
D/init: 在类初始化位置启动协程

按照我们正常情况加载顺序,是不是应该init先执行输出?然而在实际情况中它是在等待Activity进入onResume状态以后才执行接着看launchWhenResumed中调用的whenResumed实现。

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
kotlin复制代码public suspend fun <T> Lifecycle.whenResumed(block: suspend CoroutineScope.() -> T): T {
return whenStateAtLeast(Lifecycle.State.RESUMED, block)
}

public suspend fun <T> Lifecycle.whenStateAtLeast(
minState: Lifecycle.State,
block: suspend CoroutineScope.() -> T
): T = withContext(Dispatchers.Main.immediate) {
val job = coroutineContext[Job] ?: error("when[State] methods should have a parent job")
val dispatcher = PausingDispatcher()
val controller =
LifecycleController(this@whenStateAtLeast, minState, dispatcher.dispatchQueue, job)
try {
withContext(dispatcher, block)
} finally {
controller.finish()
}
}

@MainThread
internal class LifecycleController(
private val lifecycle: Lifecycle,
private val minState: Lifecycle.State,
private val dispatchQueue: DispatchQueue,
parentJob: Job
) {
private val observer = LifecycleEventObserver { source, _ ->
if (source.lifecycle.currentState == Lifecycle.State.DESTROYED) {
handleDestroy(parentJob)
} else if (source.lifecycle.currentState < minState) {
dispatchQueue.pause()
} else {
dispatchQueue.resume()
}
}

init {
if (lifecycle.currentState == Lifecycle.State.DESTROYED) {
handleDestroy(parentJob)
} else {
lifecycle.addObserver(observer)
}
}

private inline fun handleDestroy(parentJob: Job) {
parentJob.cancel()
finish()
}

@MainThread
fun finish() {
lifecycle.removeObserver(observer)
dispatchQueue.finish()
}
}

我们可以看到,实际上是调用了whenStateAtLeast,同时使用了withContext进行了一个同步操作。然后在LifecycleController中通过添加LifecycleObserver来监听状态,通过lifecycle当前状态来对比我们设定的触发状态,最终决定是否恢复执行。

现在我们对于Activity中的lifecycleScope的创建以及销毁流程有了一个大概的了解。同理Fragment中的lifecycleScope实现原理也是和Activity是一样的,这里我们就不再重复讲解。我们做个简单的实验:

1
2
3
4
5
6
7
8
9
10
kotlin复制代码class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
lifecycleScope.launch {
delay(2000)
Toast.makeText(this@MainActivity,"haha",Toast.LENGTH_SHORT).show()
}
}
}

这个时候是不是比之前的使用方式简单多了,我们既不用关心创建过程,也不用关心销毁的过程。

这个时候我们就需要提到CoroutineExceptionHandler协程异常处理。通过之前的章节我们知道,启动一个协程以后,如果未在协程上下文中添加CoroutineExceptionHandler情况下,一旦产生了未捕获的异常,那么我们的程序将会崩溃退出。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
kotlin复制代码class MainActivity : AppCompatActivity() {
val exceptionHandler = CoroutineExceptionHandler { coroutineContext, throwable ->
Log.d("exceptionHandler", "${coroutineContext[CoroutineName]} $throwable")
}
fun load() {
lifecycleScope.launch(exceptionHandler) {
//省略...
}
lifecycleScope.launch(exceptionHandler) {
//省略...
}
lifecycleScope.launch(exceptionHandler) {
//省略...
}
}
}

当出现这种情况的时候,像笔者这种有严重偷懒情结的人就开始抓狂了。为什么要写这么多遍 lifecycleScope.launch,同时每一次启动都要手动添加CoroutineExceptionHandler。难道就不能再简便一点吗?

image.png

当然可以,首先我们自定义一个异常处理,,我们在实现上只做一个简单的异常日志输出:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
kotlin复制代码/**
* @param errCode 错误码
* @param errMsg 简要错误信息
* @param report 是否需要上报
*/
class GlobalCoroutineExceptionHandler(private val errCode: Int, private val errMsg: String = "", private val report: Boolean = false) : CoroutineExceptionHandler {
override val key: CoroutineContext.Key<*>
get() = CoroutineExceptionHandler

override fun handleException(context: CoroutineContext, exception: Throwable) {
val msg = exception.stackTraceToString()
Log.e("$errCode","GlobalCoroutineExceptionHandler:${msg}")
}
}

然后我们在通过kotlin的扩展函数来简化我们的使用,去掉重复写lifecycleScope.launch和exceptionHandler的过程,我们就定义三个常用方法。

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
kotlin复制代码inline fun AppCompatActivity.requestMain(
errCode: Int = -1, errMsg: String = "", report: Boolean = false,
noinline block: suspend CoroutineScope.() -> Unit) {
lifecycleScope.launch(GlobalCoroutineExceptionHandler(errCode, errMsg, report)) {
block.invoke(this)
}
}

inline fun AppCompatActivity.requestIO(
errCode: Int = -1, errMsg: String = "", report: Boolean = false,
noinline block: suspend CoroutineScope.() -> Unit): Job {
return lifecycleScope.launch(Dispatchers.IO + GlobalCoroutineExceptionHandler(errCode, errMsg, report)) {
block.invoke(this)
}
}

inline fun AppCompatActivity.delayMain(
errCode: Int = -1, errMsg: String = "", report: Boolean = false,
delayTime: Long, noinline block: suspend CoroutineScope.() -> Unit) {
lifecycleScope.launch(GlobalCoroutineExceptionHandler(errCode, errMsg, report)) {
withContext(Dispatchers.IO) {
delay(delayTime)
}
block.invoke(this)
}
}

这个时候我们就可以愉快的在Activity中使用了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
kotlin复制代码class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
requestMain {
delay(2000)
Toast.makeText(this@MainActivity,"haha",Toast.LENGTH_SHORT).show()
}
requestIO {
loadNetData()
}
delayMain(100){
Toast.makeText(this@MainActivity,"haha",Toast.LENGTH_SHORT).show()
}
}

private suspend fun loadNetData(){
//网络加载
}
}

同样的我们再扩展一套基于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
kotlin复制代码inline fun Fragment.requestMain(
errCode: Int = -1, errMsg: String = "", report: Boolean = false,
noinline block: suspend CoroutineScope.() -> Unit) {
lifecycleScope.launch(GlobalCoroutineExceptionHandler(errCode, errMsg, report)) {
block.invoke(this)
}
}

inline fun Fragment.requestIO(
errCode: Int = -1, errMsg: String = "", report: Boolean = false,
noinline block: suspend CoroutineScope.() -> Unit) {
lifecycleScope.launch(Dispatchers.IO + GlobalCoroutineExceptionHandler(errCode, errMsg, report)) {
block.invoke(this)
}
}

inline fun Fragment.delayMain(
errCode: Int = -1, errMsg: String = "", report: Boolean = false, delayTime: Long,
noinline block: suspend CoroutineScope.() -> Unit) {
lifecycleScope.launch(GlobalCoroutineExceptionHandler(errCode, errMsg, report)) {
withContext(Dispatchers.IO) {
delay(delayTime)
}
block.invoke(this)
}
}

然后也可以愉快的在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
kotlin复制代码class HomeFragment:Fragment() {

init {
lifecycleScope.launchWhenCreated {
Toast.makeText(context,"Fragment创建了", Toast.LENGTH_SHORT).show()
}
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
return inflater.inflate(R.layout.fragment_main,container,false)
}

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
requestMain {
//...
}
requestIO {
//...
}
delayMain(100){
//...
}
}
}

这里需要提一下,可能有的人不太明白,为什么要把Activity和Fragment都分开写,他们都是使用的lifecycleScope,我们直接通过lifecycleScope扩展就不可以了吗。假如我们这么扩展:

1
2
3
4
5
6
7
kotlin复制代码inline fun LifecycleCoroutineScope.requestMain(
errCode: Int = -1, errMsg: String = "", report: Boolean = false,
noinline block: suspend CoroutineScope.() -> Unit) {
launch(GlobalCoroutineExceptionHandler(errCode, errMsg, report)) {
block.invoke(this)
}
}

我们以Dailog为例,来启动一个协程:

1
2
3
4
5
6
7
8
9
kotlin复制代码val dialog = Dialog(this)
dialog.show()
(dialog.context as LifecycleOwner).lifecycleScope.requestMain {
withContext(Dispatchers.IO){
//网络加载
}
// 刷新UI
}
dialog.cancel()

那么可能会出现一个什么问题?是的,内存泄露的问题以及错误的引用问题。虽然我的dialog被销毁了,但是我们lifecycleScope并不处于DESTROYED状态,所以我们的协程依然会执行,这个时候我们就会出现内存泄露和崩溃问题。

通过上面的学习,我们已经基本掌握了协程在Activity和Fragment中的使用方式。接下来我们讲解在Viewmodel中使用协程。

ViewModel中使用协程

如果我们想和在Activity和Fragment中一样的简便、快速的在ViewModel使用协程。那么我们就需要集成下面这个官方的ViewModel扩展库。

1
kotlin复制代码implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.3.1"

与Activity和Fragment不同的是,在ViewModel我们使用的不是lifecycleScope,而是使用viewModelScope,使用viewModelScope,使用viewModelScope。重要的事情说三遍。

这里一定要注意噢,之前就有好几个人问我为什么在viewmodel里面用不了协程,我开始纳闷半天咋就用不了呢。最后一问结果是在ViewModel使用lifecycleScope,这样做是不对滴。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
kotlin复制代码public val ViewModel.viewModelScope: CoroutineScope
get() {
val scope: CoroutineScope? = this.getTag(JOB_KEY)
if (scope != null) {
return scope
}
return setTagIfAbsent(
JOB_KEY,
CloseableCoroutineScope(SupervisorJob() + Dispatchers.Main.immediate)
)
}

internal class CloseableCoroutineScope(context: CoroutineContext) : Closeable, CoroutineScope {
override val coroutineContext: CoroutineContext = context

override fun close() {
coroutineContext.cancel()
}
}

viewModelScope相比较lifecycleScope实现会稍微简单一点。都是使用的SupervisorJob() + Dispatchers.Main上下文,同时最终的取消操作也类似lifecycleScope,只不过viewModelScope取消是在ViewModel的销毁的时候取消。

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
java复制代码final void clear() {
mCleared = true;
if (mBagOfTags != null) {
synchronized (mBagOfTags) {
for (Object value : mBagOfTags.values()) {
closeWithRuntimeException(value);
}
}
}
onCleared();
}


<T> T setTagIfAbsent(String key, T newValue) {
T previous;
synchronized (mBagOfTags) {
previous = (T) mBagOfTags.get(key);
if (previous == null) {
mBagOfTags.put(key, newValue);
}
}
T result = previous == null ? newValue : previous;
if (mCleared) {
closeWithRuntimeException(result);
}
return result;
}

private static void closeWithRuntimeException(Object obj) {
if (obj instanceof Closeable) {
try {
((Closeable) obj).close();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}

同样的通过上面的总结,我们也为ViewModel扩展一套常用的方法

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
kotlin复制代码inline fun ViewModel.requestMain(
errCode: Int = -1, errMsg: String = "", report: Boolean = false,
noinline block: suspend CoroutineScope.() -> Unit) {
viewModelScope.launch(GlobalCoroutineExceptionHandler(errCode, errMsg, report)) {
block.invoke(this)
}
}

inline fun ViewModel.requestIO(
errCode: Int = -1, errMsg: String = "", report: Boolean = false,
noinline block: suspend CoroutineScope.() -> Unit) {
viewModelScope.launch(Dispatchers.IO + GlobalCoroutineExceptionHandler(errCode, errMsg, report)) {
block.invoke(this)
}
}

inline fun ViewModel.delayMain(
errCode: Int = -1, errMsg: String = "", report: Boolean = false, delayTime: Long,
noinline block: suspend CoroutineScope.() -> Unit) {
viewModelScope.launch(GlobalCoroutineExceptionHandler(errCode, errMsg, report)) {
withContext(Dispatchers.IO) {
delay(delayTime)
}
block.invoke(this)
}
}

然后我们就可以愉快的在ViewModel进行使用协程了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
kotlin复制代码class MainViewModel:ViewModel() {

init {
requestMain {
Log.d("MainViewModel", "主线程中启动协程")
}
requestIO {
Log.d("MainViewModel", "IO线程中启动协程进行网络加载")
}
delayMain(100){
Log.d("MainViewModel", "主线程中启动协程并延时一定时间")
}
}
}

好了,常规使用协程的方式我都已经学会。但是我们在一些环境下如法使用使用lifecycleScope和viewModelScope的时候我们又该怎么办。比如:在Service、Dialog、PopWindow以及一些其他的环境中又该如何使用。

image.png

其他环境下使用协程

在这些环境中我们可以采用通用的方式进行处理,其实还是根据协程作用域的差异分为两类:

  • 协同作用域:这一类我们就模仿MainScope自定义一个CoroutineScope。
  • 主从(监督)作用域:这一类我们直接使用MainScope,然后在此基础上做一些扩展即可。

如果对这两个概念还不理解的,麻烦移步到第二章节里面仔细阅读一遍,这里就不再解释。

image.png

我们接下来模仿MainScope创建一个CoroutineScope,它是在主线程下执行,并且它的Job不是SupervisorJob。

1
2
kotlin复制代码@Suppress("FunctionName")
public fun NormalScope(): CoroutineScope = CoroutineScope(Dispatchers.Main)

然后我再基于NormalScope和MainScope进行使用。我们就以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
31
32
33
34
35
36
kotlin复制代码abstract class BaseService :Service(){
private val normalScope = NormalScope()

override fun onDestroy() {
normalScope.cancel()
super.onDestroy()
}

protected fun requestMain(
errCode: Int = -1, errMsg: String = "", report: Boolean = false,
block: suspend CoroutineScope.() -> Unit) {
normalScope.launch(GlobalCoroutineExceptionHandler(errCode, errMsg, report)) {
block.invoke(this)
}
}


protected fun requestIO(
errCode: Int = -1, errMsg: String = "", report: Boolean = false,
block: suspend CoroutineScope.() -> Unit): Job {
return normalScope.launch(Dispatchers.IO + GlobalCoroutineExceptionHandler(errCode, errMsg, report)) {
block.invoke(this)
}
}

protected fun delayMain(
delayTime: Long,errCode: Int = -1, errMsg: String = "", report: Boolean = false,
block: suspend CoroutineScope.() -> Unit) {
normalScope.launch(GlobalCoroutineExceptionHandler(errCode, errMsg, report)) {
withContext(Dispatchers.IO) {
delay(delayTime)
}
block.invoke(this)
}
}
}

我们创建一个抽象类BaseService类,然后再定义一些基础使用方法后,我就可以快速的使用了

1
2
3
4
5
6
7
8
9
10
11
kotlin复制代码class MainService : BaseService() {

override fun onBind(intent: Intent): IBinder? = null

override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
requestIO {
//网络加载
}
return super.onStartCommand(intent, flags, startId)
}
}

同理在Dialog、PopWindow以及一些其他的环境中可以依照此方法,定义符合我们自己需求的CoroutineScope。一定要记得不要跨域使用,以及及时的关闭协程。

又到了文章末尾,在此章节中我们已经了解了协程结合Activity、Fragment、Lifecycle、Viewmodel的基础使用,以及如何简单的自定义一个协程,如果还有不清楚的地方,可在下方留言。

image.png

需要源码的看这里:demo源码

预告

下一篇我们将结合Android Jetpack组件进行使用,如:DataBinding,LiveData,Room等。

原创不易。如果您喜欢这篇文章,您可以动动小手点赞收藏image.png。

微信截图_20211227104733.jpg

Android技术交流群,有兴趣的可以私聊加入

关联文章
Kotlin协程基础及原理系列

  • 史上最详Android版kotlin协程入门进阶实战(一) -> kotlin协程的基础用法
  • 史上最详Android版kotlin协程入门进阶实战(二) -> kotlin协程的关键知识点初步讲解
  • 史上最详Android版kotlin协程入门进阶实战(三) -> kotlin协程的异常处理
  • 史上最详Android版kotlin协程入门进阶实战(四) -> 使用kotlin协程开发Android的应用
  • 史上最详Android版kotlin协程入门进阶实战(五) -> kotlin协程的网络请求封装

Flow系列

  • Kotlin协程之Flow使用(一)
  • Kotlin协程之Flow使用(二)
  • Kotlin协程之Flow使用(三)

扩展系列

  • 封装DataBinding让你少写万行代码
  • ViewModel的日常使用封装

本文转载自: 掘金

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

Spring Cloud Stream 体系及原理介绍 Sp

发表于 2021-04-28

头图.png

作者 | 洛夜
来源 | 阿里巴巴云原生公众号

Spring Cloud Stream在 Spring Cloud 体系内用于构建高度可扩展的基于事件驱动的微服务,其目的是为了简化消息在 Spring Cloud 应用程序中的开发。

Spring Cloud Stream (后面以 SCS 代替 Spring Cloud Stream) 本身内容很多,而且它还有很多外部的依赖,想要熟悉 SCS,必须要先了解 Spring Messaging 和 Spring Integration 这两个项目,接下来,文章将围绕以下三点进行展开:

  • 什么是 Spring Messaging
  • 什么是 Spring Integration
  • 什么是 SCS 体系及其原理

1.png

本文配套可交互教程已登录阿里云知行动手实验室,PC 端登录 start.aliyun.com_ _在浏览器中立即体验。

Spring Messaging

Spring Messaging 是 Spring Framework 中的一个模块,其作用就是统一消息的编程模型。

  • 比如消息 Messaging 对应的模型就包括一个消息体 Payload 和消息头 Header:

2.png

1
2
3
4
5
csharp复制代码package org.springframework.messaging;
public interface Message<T> {
T getPayload();
MessageHeaders getHeaders();
}
  • 消息通道 MessageChannel 用于接收消息,调用send方法可以将消息发送至该消息通道中:

3.png

1
2
3
4
5
6
7
8
9
10
java复制代码@FunctionalInterface
public interface MessageChannel {
long INDEFINITE_TIMEOUT = -1;
default boolean send(Message<?> message) {

return send(message, INDEFINITE_TIMEOUT);

}
boolean send(Message<?> message, long timeout);
}

消息通道里的消息如何被消费呢?

  • 由消息通道的子接口可订阅的消息通道SubscribableChannel实现,被MessageHandler消息处理器所订阅:
1
2
3
4
java复制代码public interface SubscribableChannel extends MessageChannel {
boolean subscribe(MessageHandler handler);
boolean unsubscribe(MessageHandler handler);
}
  • 由MessageHandler真正地消费/处理消息:
1
2
3
4
java复制代码@FunctionalInterface
public interface MessageHandler {
void handleMessage(Message<?> message) throws MessagingException;
}

Spring Messaging 内部在消息模型的基础上衍生出了其它的一些功能,如:

  • 消息接收参数及返回值处理:消息接收参数处理器HandlerMethodArgumentResolver配合@Header, @Payload等注解使用;消息接收后的返回值处理器HandlerMethodReturnValueHandler配合@SendTo注解使用;
  • 消息体内容转换器MessageConverter;
  • 统一抽象的消息发送模板AbstractMessageSendingTemplate;
  • 消息通道拦截器ChannelInterceptor;

Spring Integration

Spring Integration 提供了 Spring 编程模型的扩展用来支持企业集成模式(Enterprise Integration Patterns),是对 Spring Messaging 的扩展。

它提出了不少新的概念,包括消息路由MessageRoute、消息分发MessageDispatcher、消息过滤Filter、消息转换Transformer、消息聚合Aggregator、消息分割Splitter等等。同时还提供了MessageChannel和MessageHandler的实现,分别包括 DirectChannel、ExecutorChannel、PublishSubscribeChannel和MessageFilter、ServiceActivatingHandler、MethodInvokingSplitter 等内容。

这里为大家介绍几种消息的处理方式:

  • 消息的分割:

4.png

  • 消息的聚合:

5.png

  • 消息的过滤:

6.png

  • 消息的分发:

7.png

接下来,我们以一个最简单的例子来尝试一下 Spring Integration。

这段代码解释为:

1
2
3
4
5
6
7
less复制代码SubscribableChannel messageChannel =new DirectChannel(); // 1

messageChannel.subscribe(msg-> { // 2
System.out.println("receive: " +msg.getPayload());
});

messageChannel.send(MessageBuilder.withPayload("msgfrom alibaba").build()); // 3
  • 构造一个可订阅的消息通道messageChannel。
  • 使用MessageHandler去消费这个消息通道里的消息。
  • 发送一条消息到这个消息通道,消息最终被消息通道里的MessageHandler所消费。
  • 最后控制台打印出:receive: msg from alibaba。

DirectChannel内部有个UnicastingDispatcher类型的消息分发器,会分发到对应的消息通道MessageChannel中,从名字也可以看出来,UnicastingDispatcher是个单播的分发器,只能选择一个消息通道。那么如何选择呢? 内部提供了LoadBalancingStrategy负载均衡策略,默认只有轮询的实现,可以进行扩展。

我们对上段代码做一点修改,使用多个 MessageHandler 去处理消息:

1
2
3
4
5
6
7
8
9
10
11
12
less复制代码SubscribableChannel messageChannel = new DirectChannel();

messageChannel.subscribe(msg -> {
System.out.println("receive1: " + msg.getPayload());
});

messageChannel.subscribe(msg -> {
System.out.println("receive2: " + msg.getPayload());
});

messageChannel.send(MessageBuilder.withPayload("msg from alibaba").build());
messageChannel.send(MessageBuilder.withPayload("msg from alibaba").build());

由于DirectChannel内部的消息分发器是UnicastingDispatcher单播的方式,并且采用轮询的负载均衡策略,所以这里两次的消费分别对应这两个MessageHandler。控制台打印出:

1
2
vbnet复制代码receive1: msg from alibaba
receive2: msg from alibaba

既然存在单播的消息分发器UnicastingDispatcher,必然也会存在广播的消息分发器,那就是BroadcastingDispatcher,它被 PublishSubscribeChannel 这个消息通道所使用。广播消息分发器会把消息分发给所有的 MessageHandler:

1
2
3
4
5
6
7
8
9
10
11
12
less复制代码SubscribableChannel messageChannel = new PublishSubscribeChannel();

messageChannel.subscribe(msg -> {
System.out.println("receive1: " + msg.getPayload());
});

messageChannel.subscribe(msg -> {
System.out.println("receive2: " + msg.getPayload());
});

messageChannel.send(MessageBuilder.withPayload("msg from alibaba").build());
messageChannel.send(MessageBuilder.withPayload("msg from alibaba").build());

Spring Cloud Stream

SCS 与各模块之间的关系是:

  • SCS 在 Spring Integration 的基础上进行了封装,提出了Binder, Binding, @EnableBinding, @StreamListener等概念。
  • SCS 与 Spring Boot Actuator 整合,提供了/bindings, /channelsendpoint。
  • SCS 与 Spring Boot Externalized Configuration 整合,提供了BindingProperties, BinderProperties等外部化配置类。
  • SCS 增强了消息发送失败的和消费失败情况下的处理逻辑等功能。
  • SCS 是 Spring Integration 的加强,同时与 Spring Boot 体系进行了融合,也是 Spring Cloud Bus 的基础。它屏蔽了底层消息中间件的实现细节,希望以统一的一套 API 来进行消息的发送/消费,底层消息中间件的实现细节由各消息中间件的 Binder 完成。

Binder是提供与外部消息中间件集成的组件,为构造Binding提供了 2 个方法,分别是bindConsumer和bindProducer,它们分别用于构造生产者和消费者。目前官方的实现有 Rabbit Binder 和 Kafka Binder, Spring Cloud Alibaba 内部已经实现了 RocketMQ Binder。

8.png

从图中可以看出,Binding是连接应用程序跟消息中间件的桥梁,用于消息的消费和生产。我们来看一个最简单的使用 RocketMQ Binder 的例子,然后分析一下它的底层处理原理:

  • 启动类及消息的发送:
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复制代码@SpringBootApplication
@EnableBinding({ Source.class, Sink.class }) // 1
public class SendAndReceiveApplication {

public static void main(String[] args) {
SpringApplication.run(SendAndReceiveApplication.class, args);
}

@Bean // 2
public CustomRunner customRunner() {
return new CustomRunner();
}

public static class CustomRunner implements CommandLineRunner {

@Autowired
private Source source;

@Override
public void run(String... args) throws Exception {
int count = 5;
for (int index = 1; index <= count; index++) {
source.output().send(MessageBuilder.withPayload("msg-" + index).build()); // 3
}
}
}
}
  • 消息的接收:
1
2
3
4
5
6
7
8
9
typescript复制代码@Service
public class StreamListenerReceiveService {

@StreamListener(Sink.INPUT) // 4
public void receiveByStreamListener1(String receiveMsg) {
System.out.println("receiveByStreamListener: " + receiveMsg);
}

}

这段代码很简单,没有涉及到 RocketMQ 相关的代码,消息的发送和接收都是基于 SCS 体系完成的。如果想切换成 RabbitMQ 或 Kafka,只需修改配置文件即可,代码无需修改。

我们来分析下这段代码的原理:

1.@EnableBinding对应的两个接口属性Source和Sink是 SCS 内部提供的。SCS 内部会基于Source和Sink构造BindableProxyFactory,且对应的 output 和 input 方法返回的 MessageChannel 是DirectChannel。output 和 input 方法修饰的注解对应的 value 是配置文件中 binding 的 name。

1
2
3
4
5
6
7
8
9
10
java复制代码public interface Source {
String OUTPUT = "output";
@Output(Source.OUTPUT)
MessageChannel output();
}
public interface Sink {
String INPUT = "input";
@Input(Sink.INPUT)
SubscribableChannel input();
}

配置文件里 bindings 的 name 为 output 和 input,对应Source和Sink接口的方法上的注解里的 value:

1
2
3
4
5
6
7
ini复制代码spring.cloud.stream.bindings.output.destination=test-topic
spring.cloud.stream.bindings.output.content-type=text/plain
spring.cloud.stream.rocketmq.bindings.output.producer.group=demo-group

spring.cloud.stream.bindings.input.destination=test-topic
spring.cloud.stream.bindings.input.content-type=text/plain
spring.cloud.stream.bindings.input.group=test-group1
  1. 构造CommandLineRunner,程序启动的时候会执行CustomRunner的run方法。
  2. 调用Source接口里的 output 方法获取DirectChannel,并发送消息到这个消息通道中。这里跟之前 Spring Integration 章节里的代码一致。
  • Source 里的 output 发送消息到DirectChannel消息通道之后会被AbstractMessageChannelBinder#SendingHandler这个MessageHandler处理,然后它会委托给AbstractMessageChannelBinder#createProducerMessageHandler创建的 MessageHandler 处理(该方法由不同的消息中间件实现)。
  • 不同的消息中间件对应的AbstractMessageChannelBinder#createProducerMessageHandler方法返回的 MessageHandler 内部会把 Spring Message 转换成对应中间件的 Message 模型并发送到对应中间件的 broker。
  1. 使用@StreamListener进行消息的订阅。请注意,注解里的Sink.input对应的值是 “input”,会根据配置文件里 binding 对应的 name 为 input 的值进行配置:
  • 不同的消息中间件对应的AbstractMessageChannelBinder#createConsumerEndpoint方法会使用 Consumer 订阅消息,订阅到消息后内部会把中间件对应的 Message 模型转换成 Spring Message。
  • 消息转换之后会把 Spring Message 发送至 name 为 input 的消息通道中。
  • @StreamListener对应的StreamListenerMessageHandler订阅了 name 为 input 的消息通道,进行了消息的消费。

这个过程文字描述有点啰嗦,用一张图总结一下(黄色部分涉及到各消息中间件的 Binder 实现以及 MQ 基本的订阅发布功能):

9.png

SCS 章节的最后,我们来看一段 SCS 关于消息的处理方式的一段代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
typescript复制代码@StreamListener(value = Sink.INPUT, condition = "headers['index']=='1'")
public void receiveByHeader(Message msg) {
System.out.println("receive by headers['index']=='1': " + msg);
}

@StreamListener(value = Sink.INPUT, condition = "headers['index']=='9999'")
public void receivePerson(@Payload Person person) {
System.out.println("receive Person: " + person);
}

@StreamListener(value = Sink.INPUT)
public void receiveAllMsg(String msg) {
System.out.println("receive allMsg by StreamListener. content: " + msg);
}

@StreamListener(value = Sink.INPUT)
public void receiveHeaderAndMsg(@Header("index") String index, Message msg) {
System.out.println("receive by HeaderAndMsg by StreamListener. content: " + msg);
}

有没有发现这段代码跟 Spring MVC Controller 中接收请求的代码很像? 实际上他们的架构都是类似的,Spring MVC 对于 Controller 中参数和返回值的处理类分别是org.springframework.web.method.support.HandlerMethodArgumentResolver、org.springframework.web.method.support.HandlerMethodReturnValueHandler。

Spring Messaging 中对于参数和返回值的处理类之前也提到过,分别是org.springframework.messaging.handler.invocation.HandlerMethodArgumentResolver、org.springframework.messaging.handler.invocation.HandlerMethodReturnValueHandler。

它们的类名一模一样,甚至内部的方法名也一样。

总结

10.png

上图是 SCS 体系相关类说明的总结,关于 SCS 以及 RocketMQ Binder 更多相关的示例,可以参考 RocketMQ Binder Demos,包含了消息的聚合、分割、过滤;消息异常处理;消息标签、SQL 过滤;同步、异步消费等等。

本文转载自: 掘金

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

1…678679680…956

开发者博客

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