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

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


  • 首页

  • 归档

  • 搜索

Golang深入浅出之-Go语言标准库net/http:构建

发表于 2024-04-28

Go语言以其简洁的语法和强大的并发模型,成为构建高性能Web服务器的优选语言之一。其标准库中的net/http包提供了构建HTTP服务器和客户端的所有必要工具。本文旨在深入浅出地讲解net/http包的使用,分析在构建Web服务器过程中常见的问题、易错点,并提出避免策略,辅以实用代码示例。

image.png

net/http基础

net/http包提供了两个核心功能:创建HTTP服务器和发起HTTP请求。创建服务器主要通过http.ListenAndServe或更灵活的http.Server结构体来实现,而发起请求则通常使用http.Get、http.Post等函数或自定义http.Client。

常见问题与易错点

易错点1:路由设计不当

初学者往往直接在http.HandleFunc中硬编码路由逻辑,导致代码难以维护和扩展。

避免方法:采用第三方路由库(如gorilla/mux)或自定义路由结构,实现清晰的路由分发逻辑。

易错点2:忽视HTTP中间件

中间件是处理跨多个路由的通用逻辑(如日志记录、认证)的有效方式,但常被忽视。

避免方法:设计可插拔的中间件系统,利用http.HandlerFunc的链式调用来集成中间件逻辑。

易错点3:资源泄露

长连接(Keep-Alive)和HTTP/2可能导致连接或goroutine泄露,影响性能和稳定性。

避免方法:合理配置Server的ReadTimeout、WriteTimeout和MaxHeaderBytes等参数,使用Context管理goroutine生命周期。

实战代码示例

基础服务器示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
go复制代码package main

import (
"fmt"
"net/http"
)

func handler(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Hello, World!")
}

func main() {
http.HandleFunc("/", handler)
if err := http.ListenAndServe(":8080", nil); err != nil {
panic(err)
}
}

中间件示例

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
go复制代码package main

import (
"log"
"net/http"
)

func loggingMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
log.Printf("%s %s\n", r.Method, r.URL.Path)
next.ServeHTTP(w, r)
})
}

func helloHandler(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, "Hello, with middleware!")
}

func main() {
mux := http.NewServeMux()
mux.HandleFunc("/", helloHandler)

server := &http.Server{
Addr: ":8080",
Handler: loggingMiddleware(mux),
}

log.Println("Starting server on :8080...")
if err := server.ListenAndServe(); err != nil {
log.Fatal(err)
}
}

总结

net/http包以其简洁的设计和强大的功能,成为了Go语言构建Web服务器的首选。通过理解并避免上述易错点,开发者可以更高效地利用这一标准库来开发出既强大又易于维护的Web服务。无论是简单的静态文件服务,还是复杂的API服务器,net/http配合良好的架构设计和最佳实践,都能帮助你构建出高性能、高可用的应用。随着对Go语言及其标准库的深入理解,你将能更自信地应对各种Web开发挑战。

本文转载自: 掘金

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

linux下python core分析:查看core的pyt

发表于 2024-04-28

python出的core,栈都是c语言的栈,没办法从core中知道core在了python代码的哪一行,导致无从下手分析定位问题,搜索了一下相关资料,没有特别系统的分析方法,所以整理一下分析python core的方法以及一些可能的原因总结

core是什么?

linux下进程异常崩溃退出时,可以生成一份当时的内存、寄存器等的文件镜像,也就是一个core文件,我们可以用gdb命令分析core文件,查看当时的代码栈、内存等信息

检查是否开启core

  1. 使用 ulimit -a 查看第一行 core file size,如果是0就代表没有开,需要在/etc/profile中加一行ulimit -c unlimited,然后source /etc/profile,当前及后续登录的shell都会开启core
  2. 修改/etc/sysctl.conf中kernel.core_pattern = /home/core.%e.%p.%t,修改core的生成路径,sysctl -p使配置生效

环境准备

实测yum install安装的python不可用,需要编译一个新的python,编译时指定–with-pydebug参数。

加–with-pydebug参数后,编译的时候gcc会带-g参数,有调试信息,该参数同样会启用python的一些检查机制,方便我们编码时提前发现问题。具体的参数内容可见官方文档:
docs.python.org/zh-cn/3.12/…

  • 操作系统: linux centos8
  • python版本: python3.8

下载python解释器源码

下载地址:www.python.org/ftp/python/…

编译python

编译前需要下载一些依赖的工具

1
shell复制代码yum install zlib-devel bzip2-devel ncurses-devel sqlite-devel readline-devel tk-devel gcc make libffi-devel

然后开始编译

1
2
3
4
shell复制代码tar -xvf Python-3.8.8.tar.xz
cd Python-3.8.8
./configure --with-pydebug
make

编译完成后,会在当前目录生成一个python二进制文件,就是我们想要的python解释器了。

查看core的python栈

生成一个core

首先写一个python脚本来生成一个core。脚本通过访问0地址来生成core

1
2
3
generate_core.py复制代码from ctypes import c_int

print(c_int.from_address(0).value)

执行./python generate_core.py来执行脚本,会生成一个core。(生成core的路径可以通过查看操作系统的/proc/sys/kernel/core_pattern查看)

分析core

使用gdb工具来打开一个core文件,然后使用bt命令打印栈信息

1
scss复制代码gdb python_path(编译的python二进制的绝对路径) core_path(core文件的绝对路径)

image.png

可以看到栈信息全是c的栈,然后载入使用Python源码中自带的gdb工具,键入

1
go复制代码python import sys; sys.path.append("/root/python3.8/Python-3.8.8/Tools/gdb"); import libpython

使用py-bt命令打印python栈信息

1
复制代码py-bt

image.png
可以看到对应的core的python栈了,接下来就可以根据业务逻辑代码,去分析为什么会产生core。

常见的产生core的原因

常见的产生core的情况是有以下几种

Segmentation faule(段错误),这时进程的退出码是-11

1、访问非法指针

上面的生成core的示例脚本就是一个访问非法指针的例子,在64位操作系统中,指针的大小是8个字节,常见的指针地址一般前两个字节是0x00, 第三个字节是0x7f, 后面的是随机的。如果访问非法指针,那么就会取不到值引发信号11段错误,导致出core。

image.png
可以看到这个core的原因是memcpy函数访问了空指针导致的core。

2、调用非法函数(也等同于访问非法指针,只是现象不同)

示例代码,该代码定义了一个函数指针,然后创建了一个指向空的函数指针,调用该函数

1
2
3
4
5
6
7
8
test.c复制代码#include<stdio.h>
typedef int(*func)(int, int);
int main()
{
func add_func = NULL;
add_func(1, 2);
return 0;
}

编译运行该文件

1
2
bash复制代码gcc test.c
./a.out

image.png
会生成一个core,可以看到core原因是段错误

然后gdb解析core
image.png
这种的core,栈后面的函数是??,找不到地址对应的函数符号名,前面的栈地址指针也是非法的,这个是一个空指针

abort,系统主动abort,这时进程的退出码是-6

一般是os的检测,多次free同一块内存可以触发

示例代码

1
2
3
4
5
6
7
8
9
test.c复制代码#include<stdio.h>
#include <stdlib.h>
int main()
{
char *name = (char *)malloc(8);
free(name);
free(name);
return 0;
}

编译运行(编译时在使用gcc -g test.c 添加调试信息)

image.png

可以看到core的原因是Aborted,同时前面提示了double free。

image.png
由于添加了调试信息,gdb的时候可以看到哪一行代码调用的free函数引发的abort

主动产生core的方法

示例代码很简单

1
2
3
4
test.py复制代码import time
while True:
print('hello world!!!')
time.sleep(1)

不想影响当前进程的执行,又想保留一个core现场

使用gcore 进程pid 的命令,会生成一个core

1
bash复制代码gcore $pid

image.png

到系统的对应目录下找这个core文件使用gdb解析即可分析

主动core并且停止当前进程的运行

使用kill命令向进程发信号6(abort)或者11(segmentation fault)即可

1
bash复制代码kill -6 $pid

例如 kill -6 1742424

image.png

本文转载自: 掘金

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

Flutter&Flame游戏实践#13 扫雷 - 界面

发表于 2024-04-28

本文为稀土掘金技术社区首发签约文章,30天内禁止转载,30天后未获授权禁止转载,侵权必究!


Flutter&Flame 游戏开发系列前言:

该系列是 [张风捷特烈] 的 Flame 游戏开发教程。Flutter 作为 全平台 的 原生级 渲染框架,兼具 全端 跨平台和高性能的特点。目前官方对休闲游戏的宣传越来越多,以 Flame 游戏引擎为基础,Flutter 有游戏方向发展的前景。本系列教程旨在让更多的开发者了解 Flutter 游戏开发。

第一季:30 篇短文章,快速了解 Flame 基础。[已完结]

第二季:从休闲游戏实践,进阶 Flutter&Flame 游戏开发。

两季知识是独立存在的,第二季 不需要 第一季作为基础。本系列教程源码地址在 【toly1994328/toly_game】,系列文章列表可在《文章总集》 或 【github 项目首页】 查看。


在之前我们实现了两个类型的小游戏:

  • Trex 跳跃碰撞类, 1~4集
  • 打砖块 射击消除类,5~12集

接下来,我们将写一下 益智类 的小游戏。扫雷 作为历史悠久的一款益智游戏。在当年游戏匮乏的时代,想必它承载着很多人童年的宝贵回忆。下面几篇 Flutter&Flame 游戏实践,将像素级复刻最经典版的扫雷游戏:

image.png


一、扫雷玩法介绍

一款益智游戏,首先要明确:

  • [1]. 游戏操作规则。
  • [2]. 游戏胜利和失败的条件。
  • [3]. 游戏交互细节。

1. 游戏操作规则
  • 闭合的单元格中隐藏着 地雷 或 数字。
  • 闭合的单元格可以通过点击打开。
  • 单元格中数字表示九个中含 地雷 的数量。

比如下面的紫框中的 1 单元格,表示它所在的九格中(红框) 存在一个地雷。而红框中只有尾翻开的单元格,那么可以推理出左上角的单元格是雷:

image.png

此时就可以通过右键将该区域标记为 地雷。这就是扫雷的核心玩法:

image.png


2.游戏的胜败条件
  • 当点到地雷时,游戏失败。并展示出地图中的所有地雷:
  • 游戏胜利的条件是排除所有的地雷,将非雷区全部翻开:
游戏失败 游戏胜利
image.png image.png

总的来看,它是一个逻辑推理的益智游戏,规则非常精简,所以很容易上手。复杂的单元格也可以提高游戏的可玩性,是一个非常优秀的游戏玩法设计。


3. 游戏交互细节

下面动态图中展示了扫雷游戏的基本交互,包括:

  • 按下及拖动过程中,对应的单元格处于按下状态。
  • 抬起时,打开单元格。
  • 右键标记、取消旗子。
  • 顶部中间的表情展示当前的游戏交互状态,点击时重新开始。
  • 左侧 LED 展示雷的数量,右侧 LED 展示使用的秒数。

130.gif


二、整体界面布局分析

本篇我们先来解决界面设计和交互的问题,在下一章再实现具体的玩法。为了让单元格的尺寸在任何大小下都不失真,这里资源图片全部采用 svg。也顺便介绍一下 svg 如何在 Flame 中使用。

image.png


1. 游戏界面布局

游戏界面在布局上非常简单,顶部展示游戏状态信息,一般称之为 HUD (Heads-Up Display);下方网格是游戏区域,将作为后期处理的重点部分;除此之外,还有两者之间的边框需要展现:

image.png

整体构件结构如下图所示,SweeperLayout 负责整体布局的展示,包括外部的边线框。其中包含 SweeperHud 和 CellManager 两个主题内容作为孩子:

image.png


2. 尺寸设计

游戏中的构建尺寸如何规定,是一个棘手的问题。它是自适应屏幕宽高进行缩放,还是固定尺寸,不受窗口尺寸影响。扫雷游戏固定尺寸即可,如果自适应窗口缩放,会导致个数少时单元格非常大。我们希望窗口缩放不影响游戏的尺寸表现。

为了便于修改尺寸,游戏界面中所有的尺寸都基于一个标准尺寸作为单位。这里选取 单元格 尺寸 cellSize。当我们确定了网格的行列数 gridXY ,通过 SizeRes 类进行维护:

1
2
3
4
5
6
7
8
9
---->[lib/sweeper/game/config/size_res.dart]----
class SizeRes {
final double cellSize;
final (int, int) gridXY;

SizeRes({
this.cellSize = 18,
this.gridXY = (16, 16),
});

一切尺寸相关的数据够可以通过这两个数据 计算得到。比如网格区的宽高是行列数乘以单元格尺寸; Hud 尺寸高度是两个单元格大小;宽度是网格宽度。表情按钮的大小是 1.5 被的单元格大小。这里通过 get 方法提供计算逻辑:

1
2
3
4
5
Vector2 get gridSize => Vector2(gridXY.$1 * cellSize, gridXY.$2 * cellSize);

Vector2 get hudSize => Vector2(gridXY.$1 * cellSize, cellSize * 2);

Vector2 get faceSize => Vector2(cellSize * 1.5, cellSize * 1.5);

同样,LED 的尺寸、间隔、矩形区域等数据可以通过 get 方法提供。这也是 SizeRes 命名的原因,将其视为尺寸资源的仓库,方便统一管理和维护:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// hud 矩形区域
Rect get hudRect => Rect.fromLTWH(gap, gap, hudSize.x, hudSize.y);

// 网格矩形区域
Rect get gridRect =>
Rect.fromLTWH(gap, hudSize.y + gap + gap, gridSize.x, gridSize.y);

// 边框间隔
double get gap => 0.72 * cellSize;

// 布局总尺寸
Vector2 get layoutSize => Vector2(
gridSize.x + gap * 2,
gridSize.y + hudSize.y + gap * 3,
);

// led 宽度
double get ledWidth => cellSize * 0.64;

// led 间隔
double get ledSpace => cellSize * 0.12;

3. 阴影边线框的实现

俗话说,细节决定成败。仔细观察面板可以发现,其中有很多处阴影边线。包括最外部、单元格外围、HUD 外围、LED 灯外围四个地方。如何展示这些边框呢?

image.png

首先,这种边框存在于多个场合,所以需要封装一下便于复用。边框的展现可以通过绘制 矩形 的四条边线实现。其中可以设置边线的 边线宽度、四边颜色。如下所示,封装一个 BorderDecoration 类承载数据,调用 paint 方法,绘制对应矩形的四个边框:

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
---->[lib/sweeper/painter/decration/border_decroation.dart]----
class BorderDecoration {
final double strokeWidth;
final Color top;
final Color right;
final Color bottom;
final Color left;

BorderDecoration({
required this.strokeWidth,
required this.top,
required this.right,
required this.bottom,
required this.left,
});

void paint(Rect rect, Canvas canvas) {
Paint paint = Paint()..strokeWidth = strokeWidth..strokeCap = StrokeCap.round;
canvas.drawLine(
rect.topRight, rect.topRight + Offset(0, rect.height),
paint..color = right,
);
canvas.drawLine(
rect.bottomLeft, rect.bottomLeft + Offset(rect.width, 0),
paint..color = bottom,
);
canvas.drawLine(
rect.topLeft, rect.topLeft + Offset(rect.width, 0),
paint..color = top,
);
canvas.drawLine(
rect.topLeft, rect.topLeft + Offset(0, rect.height),
paint..color = left,
);
}
}

4. 实现整体布局:SweeperLayout

游戏的外框通过 SweeperLayout 构建展示,其中复写 render 方法,操作 Canvas 进行绘制边线和背景色。一共有三个边线需要绘制,分别封装为三个方法:

image.png

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class SweeperLayout extends PositionComponent with HasGameRef<SweeperGame> {
@override
void onGameResize(Vector2 size) {
x = (size.x - width) / 2;
y = (size.y - height) / 2;
super.onGameResize(size);
}

@override
FutureOr<void> onLoad() {
size = game.sizeRes.layoutSize;
/// TODO... 添加内容
return super.onLoad();
}

@override
void render(Canvas canvas) {
Rect rect = Offset.zero & Size(width, height);
canvas.drawRect(rect, Paint()..color = ColorRes.background);
_pintHudBorder(canvas);
_paintGridBorder(canvas);
_paintOutBorder(canvas,rect);
super.render(canvas);
}

_pintHudBorder : 绘制 Hud 内部的边线,创建上面写的 BorderDecoration 对象,触发 paint 方法完成绘制边线任务。

注: 其中尺寸相关的数据,封装在 game.sizeRes 中,后面会单独介绍。现在只要知道通过它可以获取尺寸数据即可:

image.png

1
2
3
4
5
6
7
8
9
10
11
void _pintHudBorder(Canvas canvas) {
double strokeWidth = game.sizeRes.gap * 0.35;
BorderDecoration decoration = BorderDecoration(
strokeWidth: strokeWidth,
top: ColorRes.gray,
left: ColorRes.gray,
right: ColorRes.white,
bottom: ColorRes.white,
);
decoration.paint(game.sizeRes.hudRect, canvas);
}

_paintGridBorder 用于绘制网格外围的边线:

image.png

1
2
3
4
5
6
7
8
9
10
11
void _paintGridBorder(Canvas canvas) {
double strokeWidth = game.sizeRes.gap * 0.42;
BorderDecoration decoration = BorderDecoration(
strokeWidth: strokeWidth,
top: ColorRes.gray,
left: ColorRes.gray,
right: ColorRes.white,
bottom: ColorRes.white,
);
decoration.paint(game.sizeRes.gridRect, canvas);
}

_paintGridBorder 用于绘制网格外围的边线:

image.png

1
2
3
4
5
6
7
8
9
10
11
void _paintOutBorder(Canvas canvas,Rect rect) {
double strokeWidth = game.sizeRes.gap * 0.25;
BorderDecoration decoration = BorderDecoration(
strokeWidth: strokeWidth,
top: ColorRes.white,
left: ColorRes.white,
right: ColorRes.gray,
bottom: ColorRes.gray,
);
decoration.paint(rect, canvas);
}

三、单元格与其管理器

接下来将要完成如下的单元格布局,以及拖拽时间过程中,对应单元格呈现出按压状态。其中单元格通过 svg 图片展示,这里也正好介绍一下 Flame 对 svg 的支持情况:

131.gif


1. 单元格构件 Cell

这里称单元格为 Cell , 在 Flame 中使用 svg 构件,需要额外添加类库 flame_svg。它是 Flame 官方基于 flutter_svg 封装的构建:

1
flame_svg: ^1.10.1

我们知道 SpriteComponent 是基于 Sprite 渲染呈现内容; 这里 flame_svg 封装了 SvgComponent 构建,基于 Svg 渲染呈现内容。Sprite 基于资源图片得到,同理 Svg 可以通过加载 svg 文件得到。

同样将加载逻辑放在 TextureLoader 类中,通过 loadSvg 方法加载资源列表并放入 _svgMap 映射关系中。提供 findSvg 方法根据文件名,获取 Svg 对象:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
---->[packages/flame_ext/lib/texture_loader.dart]----
final Map<String, Svg> _svgMap = {};

Future<void> loadSvg(List<String> images ,{
LoadProgressCallBack? loadingCallBack,
}) async{
int total = images.length;
int cur = 0;
for (int i = 0; i < images.length; i++) {
String filename = path.basename(images[i]);
_svgMap[filename] = await Svg.load(images[i]);
cur++;
loadingCallBack?.call(total, cur);
}
}

Svg findSvg(String name) {
if (_svgMap.containsKey(name)) {
return _svgMap[name]!;
}
throw AssetsNotFindException(name);
}

Cell 单元格如下所示 ,由于一个坐标表示它的位置;默认加载 closed.svg 文件表示闭合单元格;其中目前提供两个方法 pressed 和 reset 分别让单元格更新为按压和闭合状态。

image.png

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
---->[lib/sweeper/game/heros/cell/cell.dart]----
class Cell extends SvgComponent with HasGameRef<SweeperGame> {
final (int, int) pos;

Cell(this.pos);

@override
FutureOr<void> onLoad() {
double cellSize = game.sizeRes.cellSize;
size = Vector2(cellSize, cellSize);
svg = game.loader.findSvg('closed.svg');
return super.onLoad();
}

void pressed() {
svg = game.loader.findSvg('pressed.svg');
}

void reset() {
svg = game.loader.findSvg('closed.svg');
}
}

2.单元格管理器

单元格的和之前打砖块中的砖块管理类似,都是遍历行列生成单体。如下所示,在 _createCells 方法中遍历行列数,创建 Cell 对象加入列表。就可以依次排列从此网格:

image.png

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
---->[lib/sweeper/game/heros/cell/cell_manager.dart]----
class CellManager extends PositionComponent
with HasGameRef<SweeperGame>, DragCallbacks, GameCellLogic {
@override
FutureOr<void> onLoad() {
size = game.sizeRes.gridSize;
double gap = game.sizeRes.gap;
double height = game.sizeRes.hudSize.y + gap * 2;
position = Vector2(gap, height);
addAll(_createCells());
return super.onLoad();
}

List<Cell> _createCells() {
int rowCount = game.sizeRes.gridXY.$2;
int columnCount = game.sizeRes.gridXY.$1;
double cellSize = game.sizeRes.cellSize;
List<Cell> result = [];
for (int i = 0; i < rowCount; i++) {
for (int j = 0; j < columnCount; j++) {
Cell cell = Cell((j, i));
cell.x = cellSize * j;
cell.y = cellSize * i;
result.add(cell);
}
}
return result;
}
}

3. 交互的逻辑分离: GameCellLogic

这里做了一个有趣的尝试,将 构建构建逻辑 和 交互时数据处理逻辑 通过 mixin 进行分离。如下所示,定义了 GameCellLogic 来处理网格整体的交互逻辑:

image.png

这样就可以让逻辑更为紧凑,之后修改交互逻辑,只需要在 GameCellLogic 中处理即可。它混入 DragCallbacks ,可以复写 onDragUpdate 方法监听拖拽事件。然后根据触点和单元格尺寸计算出落点的坐标。通过 pressed 方法进行处理,将目标点的 Cell 改为 pressed 状态。最近在看 《代码简洁之道》,对我大有裨益。方法应该短小精炼,只处理必要的事:

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
---->[lib/sweeper/game/logic/game_cell_logic.dart]----
mixin GameCellLogic on DragCallbacks, HasGameRef<SweeperGame> {
/// 被按压的单元格
final List<Cell> _pressedCells = [];

@override
void onDragUpdate(DragUpdateEvent event) {
pressed(event.localStartPosition);;
super.onDragUpdate(event);
}

void pressed(Vector2 vector2){
game.activeFace();
double cellSize = game.sizeRes.cellSize;
int x = vector2.x ~/ cellSize;
int y = vector2.y ~/ cellSize;
pressedAt((x, y));
}

void pressedAt((int, int) pos) {
if (!_allowPressAt(pos)) return;
_resetPrevPressed();
_doPressAt(pos);
}

bool _allowPressAt((int, int) pos) {
return _pressedCells.where((e) => e.pos == pos).isEmpty;
}

void _resetPrevPressed() {
if (_pressedCells.isNotEmpty) {
Cell lastActive = _pressedCells.removeLast();
lastActive.reset();
}
}

void _doPressAt((int, int) pos) {
List<Cell> cells = children.whereType<Cell>().toList();
Iterable<Cell> targets = cells.where((e) => e.pos == pos);
if (targets.isNotEmpty) {
Cell cell = targets.first;
cell.pressed();
_pressedCells.add(cell);
}
}
}

到这里就完成了拖拽时按压的交互逻辑。将方法独立封装,可以带来很强的复用性,比如要增加点击的按下的事件时,额外混入 TapCallbacks,复写 onTapDown 方法调用 pressed 即可:

image.png


四、HUD 的处理

HUD 中包含三个部分,左侧是地雷个数,中间是按钮,右侧时游戏开始后的秒数。

image.png

到这里,游戏中组件的整体结构就非常明确了,如下所示:

image.png


1. Led 显示屏的封装:LedScreen

这种 Led 显示屏可能在以后的项目中也能用,可以单独封装起来便于复用。如下所示,我们要封装一个显示屏,可以指定显示屏中数字管的个数,以便更灵活使用:

image.png

显示屏封装为 LedScreen 构建,传入数量、宽度、间隔信息。通过这些信息,可以计算出显示屏幕的尺寸 screenSize。在 onLoad 方法中,遍历 count 次,加入 SvgComponent 展示数字管即可:

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
---->[lib/sweeper/game/heroes/hud/led_screen.dart]----
class LedScreen extends PositionComponent with HasGameRef<SweeperGame> {
final int count;
final double ledWidth;
final double ledSpace;

LedScreen({
this.count = 3,
required this.ledWidth,
required this.ledSpace,
});

Vector2 get screenSize => Vector2(
ledWidth * count + (count-1) * ledSpace+2*ledSpace,
ledWidth * 2 + ledSpace,
);

@override
FutureOr<void> onLoad() {
size = screenSize;
addAll(_createLedLamps());
return super.onLoad();
}

List<Component> _createLedLamps() {
List<Component> ledLamps = [];
Vector2 ledSize = Vector2(ledWidth, ledWidth * 2);
for (int i = 0; i < count; i++) {
SvgComponent led = SvgComponent(
svg: game.loader.findSvg('d0.svg'),
size: ledSize,
position: Vector2(ledSpace + (ledWidth + ledSpace) * i, ledSpace / 2),
);
ledLamps.add(led);
}
return ledLamps;
}

这样在 SweeperHud 中通过 _addLedScreen 方法,创建 LedScreen 添加其中即可。至于显示屏的数字变化,将在下一篇结合具体的场景来完善。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
--->[lib/sweeper/game/heroes/hud/sweeper_hud.dart]----
void _addLedScreen(){
double ledWidth = game.sizeRes.ledWidth;
double ledSpace = game.sizeRes.ledSpace;
LedScreen left = LedScreen(ledSpace: ledSpace,ledWidth: ledWidth);
double ledY = (height - left.screenSize.y) / 2;
double ledX = ledWidth/2;
left.position = Vector2(ledX, ledY);
add(left);
LedScreen right = LedScreen(ledSpace: ledSpace,ledWidth: ledWidth,count: 4);
ledX = width - ledWidth/2 - right.screenSize.x;
right.position = Vector2(ledX, ledY);
add(right);
}

2. 表情按钮构件:FaceButton

表情按钮看起来非常简单,就是展示一个表情 svg 图像。通过 FaceButton 来完成,其中需要处理点击时的按压效果。混入 TapCallbacks 在按下和抬起事件中切换图片资源:

image.png

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
---->[lib/sweeper/game/heroes/hud/face_button.dart]----
class FaceButton extends SvgComponent with HasGameRef<SweeperGame>,TapCallbacks {

@override
FutureOr<void> onLoad() {
size = game.sizeRes.faceSize;
svg = game.loader.findSvg('face.svg');
return super.onLoad();
}

@override
void onTapDown(TapDownEvent event) {
super.onTapDown(event);
pressed();
}

@override
void onTapUp(TapUpEvent event) {
super.onTapUp(event);
reset();
}

void active(){
svg = game.loader.findSvg('face_active.svg');
}

void reset() {
svg = game.loader.findSvg('face.svg');
}

void pressed() {
svg = game.loader.findSvg('face_pressed.svg');
}
}

3. 表情按钮构件:FaceButton

虽然表情按钮非常简单,但是其中蕴含着一个很重要的知识点:如何管理表情。

如下所示,在单元格点击和拓展时,如何改变表情呢?

133.gif

常规来看,想让宫格的事件影响到表情按钮,需要通过世界来一层层找到按钮对象,然后修改其图像。这样无疑非常复杂。按钮是被动地被改变,有没有什么手段能主动让按钮主动监听需要变化的事件呢?

fbb39c4a7030a30465f4548b3b91399.png


任何构件都可以访问 Game,我们可以把它当成一个 大广播,宫格点击时发送通知。表情按钮相当于收音机,可以主动监听广播的喊话。这就是一个很标准的 监听通知机制。我们有很多种手段来完成这件事,这里先采用 Flutter 内置的 Stream 流来完成(当然你可以使用任何状态管理方式来处理)。

image.png


下面定义 GameFaceLogic 就是这个大广播,播报一个 bool 值。通过 activeFace 和 resetFace 可以让广播喊话,发送通知。SweeperGame 只要混入 GameFaceLogic 就具备了大广播的能力:

image.png

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
---->[lib/sweeper/game/logic/game_face_logic.dart]----
mixin GameFaceLogic{
bool _isActive = false;

final StreamController<bool> _faceCtrl = StreamController.broadcast();

Stream<bool> get faceStream => _faceCtrl.stream;

void activeFace(){
if(_isActive) return;
_faceCtrl.add(true);
_isActive = true;
}

void resetFace(){
if(!_isActive) return;
_faceCtrl.add(false);
_isActive = false;
}
}
  • 单元格:广播发送消息

单元格的交互逻辑中,只需要在对应实际触发 activeFace 和 resetFace 方法,就可以发送通知。

1
2
3
4
5
6
7
8
9
---->[lib/sweeper/game/logic/game_cell_logic.dart]----
void pressed(Vector2 vector2){
game.activeFace();
/// 略同...
}

void unpressed(){
game.resetFace();
}
  • 表情按钮:收音机接收消息

此时按钮构建在装载是监听广播,触发 _onFaceChange 方法,修改状态即可。注意在销毁时取消监听。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
 ---->[lib/sweeper/game/heroes/hud/face_button.dart]----
StreamSubscription<bool>? _subscription;

@override
void onMount() {
super.onMount();
_subscription = game.faceStream.listen(_onFaceChange);
}

@override
void onRemove() {
_subscription?.cancel();
super.onRemove();
}

void _onFaceChange(bool value) {
if(value){
active();
}else{
reset();
}
}

到这里我们就完成了扫雷界面交互上的核心需求,下一篇将实现扫雷的具体功能逻辑,敬请期待。感谢大家的支持,喜欢的话,希望可以点个赞 ~

133.gif

本文转载自: 掘金

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

MyBatis初始化基本过程

发表于 2024-04-28

MyBatis初始化方式

MyBatis初始化提供了两种方式:

  • 基于XML配置文件:基于XML配置文件的方式是将MyBatis的所有配置信息放在XML文件中mybatis-config.xml,MyBatis通过加载并XML配置文件,将配置文信息组装成内部的Configuration对象。
  • 基于Java API:基于Java API的方式是手动创建Configuration对象,然后将配置参数set 进入Configuration对象中。

任何框架的初始化,应该都是先加载配置信息,接下来我们将使用基于XML配置文件的方式,来深入讨论MyBatis是如何通过配置文件构建Configuration对象。

基于Xml配置初始化

XML 配置文件中包含了对 MyBatis 系统的核心设置,包括获取数据库连接实例的数据源(DataSource)以及决定事务作用域和控制方式的事务管理器(TransactionManager)。

通过一个简单的例子,分析一下基于Xml配置MyBatis是怎样完成初始化的,都做了些什么?

  • mybatis-config.xml配置
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
xml复制代码<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<settings>
<!-- 延迟加载的全局开关。当开启时,所有关联对象都会延迟加载 -->
<setting name="lazyLoadingEnabled" value="true"/>

<!-- 指定 MyBatis 所用日志的具体实现,未指定时将自动查找。-->
<setting name="logImpl" value="STDOUT_LOGGING"/>

<!--是否开启驼峰命名自动映射,即从经典数据库列名 A_COLUMN 映射到经典 Java 属性名 aColumn -->
<setting name="mapUnderscoreToCamelCase" value="true"/>

</settings>

<!-- 定义数据库的信息,默认使用development数据库构建环境 -->
<environments default="development">
<environment id="development">
<!-- 使用了 JDBC 的提交和回滚功能,它依赖从数据源获得的连接来管理事务作用域 -->
<transactionManager type="JDBC"/>

<!-- 数据库信息替换为自己的环境 -->
<dataSource type="POOLED">
<property name="driver" value="com.mysql.cj.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://localhost:3306/work"/>
<property name="username" value="xxxx"/>
<property name="password" value="xxxx"/>
</dataSource>
</environment>
</environments>


<!-- 定义映射器 -->
<mappers>
<mapper class="org.apache.ibatis.example.mapper.ScheduleSettingMapper"/>
</mappers>
</configuration>

MyBatis配置项提供了很多配置项,这个配置文件中只配置了一些基本的节点,只是用来演示。

如果有对MyBatis配置项不了解的或者不知道MyBatis提供哪些配置,可以去看看MyBatis官网文档,文档上对每一个配置项都已经做出了很详细的说明和示例。

  • 程序入口代码
1
2
3
4
5
6
7
8
9
10
11
java复制代码String resource = "mybatis-config.xml";
InputStream inputStream = null;
try {
inputStream = Resources.getResourceAsStream(resource);
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
SqlSessionFactory sqlSessionFactory = null;
//初始化
sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);

  上述代码的功能是通过Resources工具类,调用ClassLoader读取classpath下的mybatis-config.xml配置文件,得到一个输入流inputStream,SqlSessionFactoryBuilder根据传入的数据流生成Configuration对象,然后根据Configuration对象创建默认的SqlSessionFactory实例。

源码分析

  前面提到,SqlSessionFactoryBuilder根据传入的数据流生成Configuration对象,然后根据Configuration对象创建默认的SqlSessionFactory实例,现在让我们通过源码来一步一步看一看

  • 调用SqlSessionFactoryBuilder对象的build(inputStream)方法
1
java复制代码sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);

  SqlSessionFactoryBuilder是SqlSessionFactory的构造器,用于创建SqlSessionFactory,采用了Builder设计模式。

  • SqlSessionFactoryBuilder会根据输入流inputStream等创建XMLConfigBuilder对象
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复制代码public SqlSessionFactory build(InputStream inputStream, String environment, Properties properties) {
try {
// 创建XMLConfigBuilder对象用来解析XML配置文件生成Document对象,创建Configuration对象
XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties);

// XMLConfigBuilder 对象调用parse()方法,将XML配置文件内的信息解析成Configuration对象
Configuration configuration = parser.parse();

// 根据解析好的Configuration对象,创建DefaultSqlSessionFactory对象
return build(configuration);
} catch (Exception e) {
throw ExceptionFactory.wrapException("Error building SqlSession.", e);
} finally {
ErrorContext.instance().reset();
try {
if (inputStream != null) {
inputStream.close();
}
} catch (IOException e) {
// Intentionally ignore. Prefer previous error.
}
}
}

// 可以通过Configuration创建DefaultSqlSessionFactory对象
public SqlSessionFactory build(Configuration config) {
return new DefaultSqlSessionFactory(config);
}

  通过new XMLConfigBuilder()创建对象时,会生成Configuration对象和XPathParser对象

  • Configuration对象主要是用来保存xml文件的配置信息
  • XPathParser对象持有解析mybatis-config.xml文件和Mapper文件生成Document对象和解析mybatis-3-config.dtd文件和mybatis-3-mapper.dtd转换成XMLMapperEntityResolver对象
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
java复制代码public XMLConfigBuilder(Class<? extends Configuration> configClass, InputStream inputStream, String environment,
Properties props) {
this(configClass, new XPathParser(inputStream, true, props, new XMLMapperEntityResolver()), environment, props);
}

private XMLConfigBuilder(Class<? extends Configuration> configClass, XPathParser parser, String environment,
Properties props) {
// Configuration的初始化
super(newConfig(configClass));
ErrorContext.instance().resource("SQL Mapper Configuration");
// 设置自定义配置
this.configuration.setVariables(props);
// 解析标志
this.parsed = false;
// environment初始化
this.environment = environment;
// 包装配置 InputStream 的 XPathParser
this.parser = parser;
}
  • SqlSessionFactoryBuilder调用XMLConfigBuilder对象的parse()方法
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
java复制代码public Configuration parse() {
if (parsed) {
throw new BuilderException("Each XMLConfigBuilder can only be used once.");
}
parsed = true;
// 通过XPathParser获取<configuration>节点对应的Node对象
XNode xNode = parser.evalNode("/configuration");
// 解析/configuration子节点信息
parseConfiguration(xNode);
return configuration;
}

private void parseConfiguration(XNode root) {
try {
// 解析properties节点
propertiesElement(root.evalNode("properties"));
// 解析settings节点
Properties settings = settingsAsProperties(root.evalNode("settings"));
// 配置自定义虚拟文件系统实现
loadCustomVfsImpl(settings);
// 配置自定义日志实现
loadCustomLogImpl(settings);
// 解析typeAliases节点
typeAliasesElement(root.evalNode("typeAliases"));
// 解析plugins节点
pluginsElement(root.evalNode("plugins"));
// 解析objectFactory节点
objectFactoryElement(root.evalNode("objectFactory"));
// 解析objectWrapperFactory节点
objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));
// 解析reflectorFactory节点
reflectorFactoryElement(root.evalNode("reflectorFactory"));
settingsElement(settings);
// 解析environments节点
environmentsElement(root.evalNode("environments"));
// 解析databaseIdProvider节点
databaseIdProviderElement(root.evalNode("databaseIdProvider"));
// 解析typeHandlers节点
typeHandlersElement(root.evalNode("typeHandlers"));
// 解析mappers节点
mappersElement(root.evalNode("mappers"));
} catch (Exception e) {
throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
}
}

  XMLConfigBuilder对象的parse()方法,主要是通过XpathParser根据Xpath表达式获取基本的DOM节点以及子节点Node信息的操作(解析的节点configuration, properties, settings, typeAliases, typeHandlers, objectFactory, objectWrapperFactory, plugins, environments, databaseIdProvider, mappers), 然后将这些值解析出来设置到Configuration对象中,最后返回Configuration对象。

这里的节点解析就不一一去看了,后续会有单独的文章挑几个核心节点做详细介绍。

  • 调用SqlSessionFactoryBuilder对象的build(configuration)方法

  通过赋值的Configuration对象,调用build方法创建DefaultSqlSessionFactory对象。基于Java API方式,手动创建XMLConfigBuilder,并解析创建Configuration对象,最后调用此方法生成SqlSessionFactoryBuilder对象。

至此,我们就知道了MyBatis是如何通过配置文件构建Configuration对象,并使用它创建SqlSessionFactory对象。

总结

  我们通过一个时序图,把整个myBatis初始化过程串起来,方便小伙伴更加直观的把整个流程串起来,从而对整个初始化过程了解的更加清晰

MyBatis初始化基本过程:

image.png

每个基于 MyBatis 的应用都是以一个 SqlSessionFactory 的实例为核心的。SqlSessionFactory 的实例可以通过 SqlSessionFactoryBuilder 从 XML 配置文件或一个预先配置的 Configuration 实例来构建出来。

screenshot-20240417-103455.png

本文转载自: 掘金

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

微信小程序生态9—微信开放平台unionId机制介绍 一、机

发表于 2024-04-28

一、机制说明

参考文档:developers.weixin.qq.com/minigame/de…

如果开发者拥有多个移动应用、网站应用、和公众帐号(包括小程序),可通过 unionID 来区分用户的唯一性,因为只要是同一个微信开放平台帐号下的移动应用、网站应用和公众帐号(包括小程序),用户的 UnionID 是唯一的。换句话说,同一用户,对同一个微信开放平台下的不同应用,unionID是相同的。

image.png

二、获取微信小程序的unionId

1、微信开放平台绑定小程序

2、调用wx.login()方法获取code

3、使用code调用

api.weixin.qq.com/sns/jscode2…

返回值如下

1
2
3
4
5
json复制代码{
"unionid":"oQ19D6OLhLKzJTOFJyx5nj2Yd-_g",
  "openid":"oKeNO44zXYi_sp9WqKZaHQwWvdmU",
  "session_key":"dFTHtJhhZcNvHvsvR1KnYg=="
 }

三、获取微信公众号的unionId

1、微信开放平台绑定公众号

2、配置服务回调域名和接口

3、当有事件(如关注公众号、在公众号里发消息),微信就会回调服务器地址并且将openId传过来

可以使用如下接口进行接收

1
2
3
4
5
6
7
8
9
10
11
java复制代码/**
  * 公众号回调接口
  */
@RequestMapping(value = "/gzh/callback", method = RequestMethod.POST, produces = "application/xml; charset=UTF-8")
public String callback(@RequestBody String requestBody,
                       @RequestParam("signature") String signature,
                       @RequestParam("timestamp") String timestamp,
                       @RequestParam("nonce") String nonce,
                       @RequestParam("openid") String openid,
                       @RequestParam(name = "encrypt_type", required = false) String encType,
                       @RequestParam(name = "msg_signature", required = false) String msgSignatur

4、调用api.weixin.qq.com/cgi-bin/use…

参考文档:developers.weixin.qq.com/doc/offiacc…

返回值如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
json复制代码{
    "subscribe": 1, 
    "openid": "o6_bmjrPTlm6_2sgVt7hMZOPfL2M", 
    "language": "zh_CN", 
    "subscribe_time": 1382694957,
    "unionid": " o6_bmasdasdsad6_2sgVt7hMZOPfL",
    "remark": "",
    "groupid": 0,
    "tagid_list":[128,2],
    "subscribe_scene": "ADD_SCENE_QR_CODE",
    "qr_scene": 98765,
    "qr_scene_str": ""
}

四、小结一下

把小程序和公众号绑定到微信开放平台后,微信的用户认证接口就会自动带上unionId,这个我已经试验过了。如果一个服务商开通了很多一样的小程序(只是认证主体不一样),那么可以尝试使用这一机制做到多个小程序的用户信息同步。

文末小彩蛋,自己花一个星期做的小网站,放出来给大家看看,网址如下:http://47.120.49.119:8080

本文转载自: 掘金

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

GitHub介绍,GitHub如何订阅充值?

发表于 2024-04-28

一、GitHub介绍

GitHub是一个面向开源及私有软件项目的托管平台,因为只支持git 作为唯一的版本库格式进行托管,故名Github。 GitHub于2008年4月10日正式上线,除了git代码仓库托管及基本的Web管理界面以外,还提供了订阅、讨论组、文本渲染、在线文件编辑器、协作图谱(报表)、代码片段分享(Gist)等功能

二、GitHub发展历程

2008年4月10日,GitHub正式上线。

2014年1月23日,联合创始人汤姆·普雷斯顿-维尔纳(Tom Preston-Werner)从另一位联合创始人克里斯·万斯特拉斯(Chris Wanstrath)手中接过总裁职位,后者也将接过普雷斯顿-维尔纳留下的CEO位置。

2018年6月4日晚,微软宣布,通过75亿美元的股票交易收购GitHub。 10月26日,微软以75亿美元收购GitHub交易已完成。10月29日,微软开发者服务副总裁奈特·弗里德曼(Nat Friedman)将成为GitHub的新一任CEO。

2020年3月17日,Github宣布收购npm,GitHub现在已经保证npm将永远免费。

2、GitHub特点

一个更好的合作方式。GitHub将团队聚集在一起,一起解决问题,推进想法,并在这个过程中互相学习。

编写更好的代码。合作能生巧。在拉请求中发生的对话和代码审查可以帮助您的团队分担工作的负担,并改进您构建的软件。了解代码评审。

管理混乱的代码。深呼吸。在GitHub上,项目管理发生在问题和项目板上,就在你的代码旁边。你所要做的就是提到一个队友,让他们参与进来。学习项目管理。

找到合适的工具。用你的GitHub账户从GitHub Marketplace上浏览和购买应用程序。找到你喜欢的工具或者发现新的爱好,然后在几分钟内开始使用它们。了解集成。

三、GitHub六大基本功能

管理软件开发:作为开源代码库以及版本控制系统,Github拥有超过900万开发者用户。随着越来越多的应用程序转移到了云上,Github已经成为了管理软件开发以及发现已有代码的首选方法。

分布式控制:如前所述,作为一个分布式的版本控制系统,在Git中并不存在主库这样的概念,每一份复制出的库都可以独立使用,任何两个库之间的不一致之处都可以进行合并。在GitHub进行分支就像在Myspace(或Facebook…)进行交友一样,在社会关系图的节点中不断的连线。

托管各种git库:GitHub可以托管各种git库,并提供一个web界面,但它与外国的SourceForge、Google Code或中国的coding的服务不同。GitHub的独特卖点在于从另外一个项目进行分支的简易性。为一个项目贡献代码非常简单:首先点击项目站点的“fork”的按钮,然后将代码检出并将修改加入到刚才分出的代码库中,最后通过内建的“pull request”机制向项目负责人申请代码合并。已经有人将GitHub称为代码玩家的MySpace。

开源项目免费托管:GitHub项目本身自然而然的也在GitHub上进行托管,只不过在一个私有的,公共视图不可见的库中。开源项目可以免费托管,但私有库则并不如此。Chris Wanstrath,GitHub的开发者之一,肯定了通过付费的私有库来在财务上支持免费库的托管这一计划。

方便团队开发:通过与客户的接洽,开发FamSpam,甚至是开发GitHub本身,GitHub的私有库已经被证明了物有所值。任何希望节省时间并希望和团队其它成员一样远离页面频繁转换之苦的人士都会从GitHub中获得他们真正想要的价值。

帮助初学者寻找开源代码:在GitHub,用户可以十分轻易地找到海量的开源代码。

四、GitHub常见术语/常用命令

Add a bio 类似签名的意思

Overview 概述 Repositories 库 Stars 星标

Followers 追随者 Following 我追随的人

Edit profile 编辑配置文件

Popular repositories 流行的库

Customize your pinned repositories 自定义固定存储库

Contribution setting 贡献的设置

Contribution activity 捐助活动

Code 代码

issues 问题

pull request 拉请求 projects 项目

wiki 维基

insights 视角

settings 设置 watch 浏览

star 标星

fork 叉

Your repositories 你的库

Repositories you contribute to 你贡献的仓库

Add files via upload 通过上传添加文件

Commit directly to the master branch 直接提交给主分支

Create a new branch for this commit and start a pull request. 为这个提交创建一个新的分支并启动一个拉请求。

五、GitHub如何订阅充值?

这里我是使用了Fomepay的556150的卡订阅的,点击获取

image.png

开卡步骤很简单,按图片步骤即可,此卡0年费0月费,比较划算

本文转载自: 掘金

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

当下时代我们该如何学习

发表于 2024-04-28

一.内容总览


学习总是伴随着我们的一生,无论是前20年的学习生涯,还是工作中的技术相关的学习,所以我们可以说学习是重要的,当然可能很多人都会像我一样曾经也为某些技术学了就忘,忘了再学,永远在追新,永远赶不上的怪圈中折腾,今天我将根据我的真实经历和对一些学习方法的研究,全面的阐述下学习,从世界观,方法论,以及理论支持三个方面来概述一下,希望对也有这种苦恼的人有所引导和帮助。

二.聊聊学习观


😊在聊这些之前,我觉的大家需要先摒弃一种心态,学会一种学习方法,就能不费力的学好 学好可能是可以的,但是不费力是不符合科学规律的,除非你真的对这个东西极其感兴趣,但是在现实中我们学习的东西我们不一定感兴趣,但是我们又不得不学。

😁首先我想先聊聊自己的心路历程,看看你是否有相似的经历,在读高中的时候我是一个普普通通的学生,但是成绩并不优秀,有时候也很苦恼,自己为什么学不好,然后我会觉得那些学习好的人一定是非常的聪明,他们本身就是脑子好,方法啥的都是骗资质平平的人的,全是鸡汤,然后因为这个心态的影响,我总是幻想着有没有什么能让自己变聪明,或者什么绝世方法,让自己随便听听课就能成绩优异,但是很遗憾我在当时并没有找到方法或者提升智力,在当时也没有现在的认知和高度,不过现在回想起来我成绩也是优秀过的,记得在小学的时候一直也是名列前茅,成绩经常是第一,我在思考过后我发现了一个不同的地方,这个东西就是状态,当我成绩优秀的阶段,我的心是非常静的,我并没有觉得某些人很厉害,或者某些方法很牛,或者某些人比我聪明,在那个状态,我的眼中的人和知识都是平等的,并不会因为一些知识比较难就不学习,我对知识和人都是一视同仁的,每天也不会思考要各科考多少分,能上什么样的大学,我的心仅仅是放在单纯的知识上,我甚至不会计较某人说你风凉话,或者对你态度不好,你的目标和经历仅仅是和知识进行玩耍和对话,所以我在思考后我发现想要有一个好的学习结果首先在心态上就要不一样,就比如我现在在做程序员,市场上主要的方向有前端 Java服务端 C/C++ 等等,但是总会听到一些声音,前端不行,后端不行,在这个时候总会因为一些个人不服气的点想要去据理力争,久而久之甚至觉得这些东西是对的,但是仔细想想这种心态是不对的,这些说法首先是不客观的,其实对个人而言根本不重要,我见过各个方向的大神,真的就是行行出状元,甚至如果你想你甚至可以跨领域学习很多东西,我们应该做的应该是静下心来把心思放在具体技术上去,和具体的技术知识玩耍,对话,应该从内心里就觉得那种论根本就不重要,要完全不在意,心静是好的学习的前提,谦虚不骄躁是学习的前提。


🧙我们为什么要学习? 难道就是这个世界上有很多技术和知识要让我们去学习吗? 我觉得肯定不是这样的,知识和技术的出现是为了解决某些问题的,而不是它的出现是为了让我们学习某些知识的,就比如我们在学习某些技术的时候,总想按照某某系统体系把整个体系的知识学完,然后到最后各种坚持,PUA自己,勉勉强强学完了,或者根本坚持不下来,但是当我们使用的时候一脸懵逼然后这个时候很多人并不是想着解决问题,而是怀着一种心态,再次重头开始,抱着系统学习就能解决项目中的所有问题的心态,如果我们学习的目标是为了解决我们实际中遇到的问题那么系统学习就更像是让我们把全部知识学习一遍后,然后再根据现实问题去发挥我们所学的知识,其实这种思想并不是没有原因的,这种思想就像我们上学的时候,把所有知识学习一遍再去考试,是一种非常低效的行为,因为读书的时候有范围,但是技术中是可以无限扩展的,全面的学习往往也是学了忘,忘了学,不过当我们完全对一个行业或者技术不了解的时候系统学习可以帮助我们建立一个很好的认识,拥有一个更加清晰的技术发展路线,这个阶段的系统学习很有必要,但是当我们在工作中已经上手了,就不应该再去秉持这种学习心态了,这种做法简直就是舍近求远,仅仅是对自己内心的一个感觉欺骗,让自己有一种伪自信,系统学习的东西是很难完全吸收和掌握的,并且也都是皮毛,当技术更新了,学习的也都白费了,当一个东西你学习永远也用不到,那也等于白学,我们需要明白一个东西,当你对一个技术理解越深,更新对你的影响越小所以我们应该结合工作中所用到的,把一个技术学精,把一个技术学透,相信我,当你有了深度,往往也就有了广度,只有当我们知道了为什么要学习后,我们才能静下心来学习,才不容易被各种复杂的说法和认知所干扰,努力达到自己的目标。


🎯当我们明白了我们为什么要学习这个问题之后,下面的一个问题就是我们要学习哪些东西,这个世界问题很多,所以就造成知识也很多,技术也很多,所以造成需要学的也有很多,但是我们要学习哪些知识哪?英语?数学?Java? Python?JavaScript? TypeScript? 我觉得在学习之前,先理解一句话永远不要高估自己这句话我也是在技术的学习中的感悟的,有的时候在进行技术点和理论的学习的时候,感觉自己学习的还不错,但是在实践的时候却一塌糊涂,有的技术在感官上觉得很简单,但是在真的去研究的时候是一个及其复杂的系统,所以真的不要高估自己觉得自己啥都能学完,啥都能学好,对自己高估会导致在学习时候的挫败感,进而导致对某些技术的“恐惧”“厌恶”结果本来不是很难的技术硬生生变成了自己内心的真老虎,所以我们应该学习哪些哪?

  1. 自己会在工作和生活中用到的,并且不学不行的。
  2. 要结合自己的能力,能够学会和掌握的。
  3. 要对自己未来和当前有促进的,对自己的发展有利好的。

结合这三点我们来详细的聊一聊,举一个实际工作中的例子,我在工作中用到过Vue也用到过React但是框架的前提是JavaScript/TypeScript HTML CSS 前端工程化 Node所以说这些东西就是我们必须要学的,Vue React是在工作中用到的,但是基本上只会用其中一个,虽然我都会,但是我自认为,两个框架用的都不精,那么我就应该把使用频率更高的学精,学透,另一个暂时会用就好,以后遇到了再深入学习,当然也能整个职业生涯都用不到,就比如我,目前工作使用的是Vue那我就把Vue学精学透,给自己带来更大的提升。

其次要结合自己的能力,可能有很多前端在工作的时候总有去学Java Python C/C++的冲动,可能是内心的不满,也可能是内心觉得那些技术厉害,前端“不行”,但是我觉得大多数时候是因为对自己不自信,当然我也有过这种心态,但是研究过发现其他技术并不是像自己想的那样,也并没有那么的神,并且理性的思考过后,我觉得自己前端学的都不算很好,舍近求远的学习其他内容是不理智的,所以我的能力不足以让我做到全知全会,那么我就应该放弃那部分的知识学习,转而把自己应该学习的学好,

最后是对自己未来有帮助的,比如在前端领域,很多前端程序员都被称之为“切图仔”这是为什么哪?因为前端一般都只写前端页面,很少涉及数据的开发,但是前端领域 Node可以让我们进行服务器的开发,并且SSR服务端渲染,工程化都要依赖这个技术,可能我现在用不到,但是它是进阶必备,对我的技术发展非常必要,所以这个也是我应该学习的,因为它对我的的发展有利好,再比如英语的学习,我们很多时候在看文档的时候,一脸懵逼,全是英文,完全看不懂,翻译软件又翻译的狗屁不是,英语的学习不论是对工作还是对“进入外企”工作都是有帮助的,所以符合自己能力要求的同学,可以学习一下。

二.学习方法


在了解个人学习观念后,我们来聊聊学习方法,因为学习对我们很重要,所以学习方法也很多,但是我个人觉得比较有效的方法有两个,一个是费曼学习法,另一个是四步法,首先我们来看下什么是费曼学习法。

  1. 确立学习目标,可以是一门技术,可以是一本书,任意事物。
  2. 理解你要学习的对象,筛选相关资料,从多个角度归纳这个对象。
  3. 用输出代替输入,模拟一个教学场景,用自己的话让对方听懂。
  4. 进行回顾和反思,如果有没模糊不清的概念需要着重学习,如有必要需要完整的输出一次。
  5. 简化知识并吸收,让知识讲出来更加通俗易懂,简洁有效也方便自己知识内化。

费曼学习法是一种公认有效的方法,可能在刚开始使用这种方法的时候会有些不适应,毕竟国内应试教育更强调记忆和固定模式的多一点,然而在现实中解决问题的角度不计其数,我们完全可以从各个角度和方式去了解问题,解决问题,费曼学习法中的模拟教学环境,用自己的话让对方听懂,如果有这种条件当然是好的,但是如果没有这种条件,写文章,录制视频也都是可以的,当然这个方法也仅供参考,实际执行还是要根据自身灵活更改。

除了费曼学习法还有另外一个方法就是四步法,出处已经无从知晓,它主要包含以下几点。

  1. 听懂/看懂
  2. 记住
  3. 学会
  4. 掌握

它主要强调的是学习的程度,思考一下,我们平时可能仅仅停留在第一个步骤就不往下走了,当然并不是所有的知识和技术都要学习的精熟,还是要根据上面的学习观制定符合自己的学习计划。

四.理论支持


👽虽然列举了学习方法,谁知道你的学习方法是不是科学的哪?那么这些学习方法的主要原理支持如下:

  1. 金字塔理论:只有教授给他人才能真正的学会知识,我们可以看到在金字塔原理中,听讲的效率是最低的,仅仅有5%,听讲,阅读,声音图片,示范演示等等都是被动学习,效率都比较低下,主动学习的留存率比较高,讨论,实践,教授给他人都是属于主动学习,其中教授给他人的留存率是最高的达到了90%,这和费曼学习法中的教授给他人如出一辙。

  1. Effort Reinforces Learning(努力加强学习):努力加强学习简单来讲就是在学习一个技术和知识的时候,耗费自己的精力的学习往往效果比较好,以下是论文的出处:
    www.jneurosci.org/content/42/…

六.总结


这篇文章从学习观,学习方法,学习方法的理论支持,来分享了我对学习的理解和学习方法的理解,但是这个世界上的每个人都是一个比较特殊的个体,所以,在借鉴的同时还是要总结出来适合自己的方式和方法,不要人云亦云,这个分享到这里就结束了,希望能够帮到和我曾经一样迷茫的你。

本文转载自: 掘金

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

Spring Boot 自定义 Banner,让你的 Ban

发表于 2024-04-28

🙏废话不多说系列,直接开整🙏

cat009.png


下面是 Spring Boot 自定义的 启动 Bannner。

image.png

自定义Banner步骤:

  1. 在resources目录下,新建文件 banner.txt;
  2. 使用自动生成Banner图标的网址生成,复制到 banner.txt 文件中即可。
* [patorjk.com/software/ta…](http://patorjk.com/software/taag)
* [www.network-science.de/ascii/](http://www.network-science.de/ascii/)
1
2
3
# 自定义spring boot的banner
1. 网址1: http://patorjk.com/software/taag
2. 网址2: http://www.network-science.de/ascii/

image.png
3. Spring Boot 的配置文件 application.yml 或者 bootstrap.yml;

1
2
3
4
5
spring:
main:
banner-mode: LOG # (CONSOLE/LOG/OFF)关闭 SpringBoot 启动的 banner 标签
banner:
location: banner.txt # 自定义 SpringBoot 启动的 banner 标签
  1. 输出结果:
1
2
3
4
5
6
___________                        _________                      __  .__    .__                 
__ ___/__.__.______ ____ / _____/ ____ _____ _____/ |_| |__ |__| ____ ____
| | < | |____ _/ __ \ _____ \ / _ \ / _/ __ \ __\ | | |/ \ / ___\
| | ___ || |_> > ___/ / ( <_> ) Y Y \ ___/| | | Y \ | | / /_/ >
|____| / ____|| __/ ___ > /_______ /____/|__|_| /___ >__| |___| /__|___| /___ /
/ |__| / / / / / //_____/

来源: patorjk.com/software/ta…


🙏至此,非常感谢阅读🙏

cat009.png

本文转载自: 掘金

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

「Java 开发」SpringBoot Junit5 测试用

发表于 2024-04-28

🙏废话不多说系列,直接开整🙏

女18.jpg

一、常用注解

  • @BeforeEach:在每个单元测试方法执行前都执行一遍
  • @BeforeAll:在每个单元测试方法执行前执行一遍(只执行一次)
  • @DisplayName(“商品入库测试”):用于指定单元测试的名称
  • @Disabled:当前单元测试置为无效,即单元测试时跳过该测试
  • @RepeatedTest(n):重复性测试,即执行n次
  • @ParameterizedTest:参数化测试,
  • @ValueSource(ints = {1, 2, 3}):参数化测试提供数据

二、引入依赖使用

spring-boot 2.4.3

1
2
3
4
5
xml复制代码<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>

(1)重复测试

1
2
3
4
5
java复制代码@RepeatedTest(value = 4)
@DisplayName("重复测试")
void repeatedTest(){
System.out.println("重复测试");
}

结果展示:

image.png

(2)参数化测试

1
2
3
4
5
6
java复制代码@ParameterizedTest
@ValueSource(ints = {1, 2, 3})
@DisplayName("参数化测试")
void paramTest(int a) {
assert(a > 0 && a < 4);
}

image.png

(3)内嵌测试

junit5 提供了嵌套单元测试的功能,可以更好的展示测试类之间的业务逻辑关系,我们通常是一个业务对应一个测试类,有业务关系的类其实可以写在一起。这样有利于进行测试。而且内联的写法可以大大减少不必要的类,精简项目,防止类爆炸等一系列问题。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
java复制代码@SpringBootTest
@AutoConfigureMockMvc
@DisplayName("Junit5单元测试")
public class MockTest {
//....
@Nested
@DisplayName("内嵌订单测试")
class OrderTestClas {
@Test
@DisplayName("取消订单")
void cancelOrder() {
int status = -1;
System.out.println("取消订单成功,订单状态为:"+status);
}
}
}

image.png

(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
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
java复制代码    // assertNull与assertNotNull用来判断条件是否为·null
@Test
@DisplayName("测试断言 NotNull")
void testNotNull() {
Assertions.assertNotNull(new Object());
}

// assertThrows用来判断执行抛出的异常是否符合预期,并可以使用异常类型接收返回值进行其他操作
@Test
@DisplayName("测试断言 抛出异常")
void testThrow() {
ArithmeticException arithmeticException = Assertions.assertThrows(ArithmeticException.class, () -> {
int n = 5 / 0;
});
Assertions.assertEquals("/ by zero", arithmeticException.getMessage());
}

@Test
@DisplayName("测试断言 超时")
// assertTimeout用来判断执行过程是否超时
void testTimeout() {
String actualResult = Assertions.assertTimeout(Duration.ofSeconds(2), () -> {
Thread.sleep(1000);
return "a result";
});
System.out.println(actualResult);
}


@Test
@DisplayName("测试组合断言")
// assertAll是组合断言,当它内部所有断言正确执行完才算通过
void testAll() {
Assertions.assertAll("测试 item 商品下单",
() -> {
//模拟用户余额扣减
Assertions.assertTrue(1 < 2, "余额不足");
},
() -> {
//模拟item数据库扣减库存
Assertions.assertTrue(3 < 4);
},
() -> {
//模拟交易流水落库
Assertions.assertNotNull(new Object());
}
);
}

最后,全部示例

献上本人全部的测试用例

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
Java复制代码package edu.study.module.up;

import org.junit.jupiter.api.*;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;
import org.springframework.boot.test.context.SpringBootTest;

import java.time.Duration;

@SpringBootTest
class DemoApplicationTests {

@Test
void contextLoads() {
}

@BeforeAll // 在执行这个测试类(即以下执行所有测试方法时,才会执行此方法,此方法为 static 静态方法)
static void testBeforeAll(){
System.out.println("test before All.");
}

@BeforeEach // 在每次执行单独的一个测试方法时,都会先执行此方法
void testBeforeEach(){
System.out.println("test before each.");
}

@Disabled
@DisplayName("当前单元测试置为无效,即单元测试时跳过该测试")
@Test
void testDisabled(){
System.out.println("此方法在执行时,不会输出");
}

// assertNull与assertNotNull用来判断条件是否为·null
@Test
@DisplayName("测试断言 NotNull")
void testNotNull() {
Assertions.assertNotNull(new Object());
}

// assertThrows用来判断执行抛出的异常是否符合预期,并可以使用异常类型接收返回值进行其他操作
@Test
@DisplayName("测试断言 抛出异常")
void testThrow() {
ArithmeticException arithmeticException = Assertions.assertThrows(ArithmeticException.class, () -> {
int n = 5 / 0;
});
Assertions.assertEquals("/ by zero", arithmeticException.getMessage());
}

@Test
@DisplayName("测试断言 超时")
// assertTimeout用来判断执行过程是否超时
void testTimeout() {
String actualResult = Assertions.assertTimeout(Duration.ofSeconds(2), () -> {
Thread.sleep(1000);
return "a result";
});
System.out.println(actualResult);
}


@Test
@DisplayName("测试组合断言")
// assertAll是组合断言,当它内部所有断言正确执行完才算通过
void testAll() {
Assertions.assertAll("测试 item 商品下单",
() -> {
//模拟用户余额扣减
Assertions.assertTrue(1 < 2, "余额不足");
},
() -> {
//模拟item数据库扣减库存
Assertions.assertTrue(3 < 4);
},
() -> {
//模拟交易流水落库
Assertions.assertNotNull(new Object());
}
);
}

@RepeatedTest(value = 4)
@DisplayName("重复测试")
void repeatedTest() {
System.out.println("重复测试");
}


@ParameterizedTest
@ValueSource(ints = {1, 2, 3})
@DisplayName("参数化测试")
void paramTest(int a) {
assert (a > 0 && a < 4);
}

@Nested
@DisplayName("内嵌订单测试")
class OrderTestClas {
@Test
@DisplayName("取消订单")
void cancelOrder() {
int status = -1;
System.out.println("取消订单成功,订单状态为:" + status);
}

@RepeatedTest(3)
@DisplayName("取消订单")
void cancelOrder2() {
int status = -1;
System.out.println("取消订单成功,订单状态为:" + status);
}
}

}

结果展示

image.png


🙏至此,非常感谢阅读🙏

cat009.png

本文转载自: 掘金

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

大数据小白的测试成长之路 引言 1初入职场:站在迷茫的十字

发表于 2024-04-28

引言

22年校招入职京东后,我一直在数据中台测试部从事测试开发的工作。毕业后,写的最多的文档是测试计划和测试报告,鲜有机会就自己的成长码字进行回顾和总结。借“up技术人”栏目,也终于是在工作之余回头望,对自己这近两年时光进行一个小总结。

本文是一个大数据测试小白初入职场后的成长总结,有新人入职的迷茫,也有点滴积累后的经验之谈。希望此文能够对正在迷茫的新人朋友以及对大数据测试有兴趣的同学有些帮助。

1.初入职场:站在迷茫的十字路口

我在本科和硕士阶段的专业都是计算机科学与技术,研究生时期的研究方向是网络嵌入,虽有过短暂的实习经历,但也是偏向业务测试,可以说对大数据是知之甚少。leader 只对我说:“刚开始不会很正常,在学校大概率接触不到这些,都是一点一点入门的,没关系,慢慢来。”

刚开始接触工作,只能说是一个“看不懂”。

大数据领域有很多的专有名词和英文缩写,“集群”、“队列”、“RSS”、“NN”、“DN”、“NS”等等,面对这些陌生的概念,我实在有点慌,于是我选择了最“学生”的办法–看书。

不可否认看书是有用的,但是效率太低了,而且在实际工作中,大数据存算引擎的改造很多是自研而且具有一定实际背景的,依靠专业书并不能切实地帮助我们开展测试工作。

比起专业书,团队文档和大胆提问才是我们初入职场的出路。

团队文档不仅记录了历史的需求测试记录,帮助我们了解产品背景,而且通过善用搜索功能,我们也能很快地理解陌生的名词和概念,提高沟通效率。其中,产研测团队有一些自命名的微服务和小工具,若不是通过团队搜索,或直接询问,我们可能需要花费大量不必要的精力去了解。所以一定要主动些,多问多沟通,才能更快的融入团体开展工作。

现在回头看,还是很想念“新人期”的。每天你不找 mentor,mentor也会找你问问“今天有啥问题吗”,再简单的问题也会耐心地为你解答。此外,新人月报、部门1v1沟通都是很好的机会,借着“初生牛犊”的身份,有任何疑惑和建议都可以直接和 leader 沟通。虽然那段时光有困难有挑战,但正是因为那段时间的点滴积累让我逐步走上大数据测试领域的职业道路。

2.进阶之路:打怪升级要一步步来

2.1 第一步:提交大数据计算任务

相较于传统软件测试,大数据测试的核心在于验证数据分析处理的准确性和可靠性,确保大数据系统能够高效、稳定地处理海量数据。大数据测试存在一定的门槛,要求我们不仅要具备基本的软件测试技能,还需熟悉大数据平台的使用。所以,迈出的第一步不妨是在大数据平台上提交一个计算任务。

说来简单,其实准备工作有很多:

1.通过大数据平台考试:考试提供了一定的培训课程能够作为平台入门指引,能够很好地帮助我们对大数据平台有一个整体的初步认识

2.申请权限:包括数据权限,以及对数据进行操作的账号权限

3.新建一个大数据任务:权限申请通过后,可使用账号进行读数据的操作,这就是一个简单的大数据任务

至此,从用户视角已成功提交了一个任务,而对于大数据平台和作为测试人员对你来说要做的工作还有许多。

2.2 第二步:点亮大数据产品地图

从提交一个大数据任务的过程中不难看出,大数据平台提供的服务众多,不仅包括直接面向用户的数据权限管理、账号管理、流程中心等,还有任务提交后任务计算有关的计算引擎、调度引擎、存储等。

前期跟进需求时,总是会有层出不穷的问题。

“任务找不到计算环境?”

“读表怎么没权限?”

“表的元数据在哪里看?”

……

测试主体服务只有一个,但涉及到的相关服务有许多,需要了解的背景知识最初可能都不知道去哪里查、怎么查。大数据存算引擎的需求来源往往是研发,类型往往是技术改造,虽然改造的仅是数据处理长链路中的某一环节,但测试场景的梳理离不开对全链路的熟悉。 若连大数据平台的基础服务及其功能特性都不清楚的话,是无法完成质量保障工作的。

除了日常需求中的积累,我们也需主动去探索大数据平台。作为一名大数据测试开发工程师,探索和点亮自己的大数据产品地图是我们的必修课。 大数据平台的产品离不开数据和对数据处理的任务,不如从这两点出发思考这个问题。





熟悉大数据平台的各服务是作为大数据测试的基本要求,借此我们能够更好地帮助产研团队进行风险评估。此外,大数据平台本身面向用户提供大一系列的数据管理工具也能成为我们工作的助力。例如元数据查询,比之自己写脚本去看表的相关信息,直接在平台上就能便捷查询到表的结构、访问、存储等详细信息。

2.3 第三步:走进大促备战工作

大促活动通常会引发流量的显著增长和数据处理需求的激增。为保障大促活动期间服务的稳定运行,大数据平台会有一些关键的备战措施,例如压测、应急演练、应急预案等。

我刚入职第一次经历双十一备战时,当时主负责的是一个新的大数据服务,所以其大促备战方案许多是从零开始的挑战。由于缺乏经验,我也体验了第一次在京东的跨夜加班。

•核心时段不能压

现有的压力测试工具虽然能够支持接口级别的压力测试,但如何安排压测时间、确定压测时长和流量大小、以及压测数据的来源等问题仍然存在。由于缺乏经验前期准备时间较长,最终一直到封板日当天才开始实操。且准备开始操作的我并不知道核心时段的问题,还是经研发同学提醒当前时段有风险才及时停止了动作。压测环境通常无法完全独立于线上环境,更何况我们是一次操作新服务的压测,因此必须避免在核心时段进行压测。

•读接口也会产生脏数据

在梳理压测接口时,我们区分读接口和写接口的目的是为了更好地理解和控制压测过程中可能产生的数据一致性问题。但这也会对我们产生一定的误导:由于压测接口被标识为读接口,且压测数据是独立构造的,我们没有考虑到该接口可能包含审计相关的写操作。直到压测快接近结束时,我们接到下游服务的告警电话,通知我们的压测对他们的服务产生了影响,这才意识到该读接口也会产生脏数据。

•应急预案不能只有预案没有动作

应急预案是对线上问题的一种应急手段,其操作存在一定的风险。我记得在预案评审阶段,由于涉及高危操作,我们原本打算在预发环境中申请资源进行演练操作。然而,ldr 提出了一个关键的问题:如果在备战期间都不进行实际操作,大促期间真遇到问题了怎么办?他强调,只有尽早发现并解决问题,才能确保线上服务的稳定。

至今,我已经参与了三次大促的备战工作,明显感受到了大促备战方案和执行流程的日益成熟。即便在这样的背景下,我们仍然需要严格遵循备战方案,确保关键操作步骤与产品研发团队协调一致,并且提前公告相关信息,以保证上下游服务和平台用户能够预判风险。

此外,基于公司现有的平台,大促备战正逐步转变为常态化工作,备战任务已经逐渐机制化和自动化,形成了可靠的解决方案。这一系列的线上服务保障措施,不仅能够为大促提供坚实的支持,还能够对每次服务上线进行风险评估,确保问题能够被及时发现并更早解决。

3.能力回顾:给新手的几点建议

对大数据测试有兴趣的同学,以下四点是值得关注的准备方向:

1.掌握大数据基础:熟悉如Hadoop、Spark等大数据处理框架的核心概念及其在实际场景中的应用

2.编程与脚本技能:精通至少一种编程语言(例如Java或Python),并熟练运用基本的Shell命令

3.测试专业能力:具备扎实的软件测试基础知识,了解基本的质量保障手段

4.学习与解决问题的能力:拥有快速学习新技术的能力,并能以问题解决为导向,高效分析并简化复杂问题

ps. 这几点非常像招聘要求,所以大家也可以多多关注有兴趣的岗位的招聘信息,从岗位需求出发培养自己的专业能力~

4.未来在握:风起云涌的技术浪潮

在不断涌现的各种新兴应用中,应用层的测试工具和质量保障方法正在经历一个成熟和进步的过程。随着众多应用的实践检验,一个新 APP 和 Web 应用在上市前所需的基准测试,以及上线、监控和应急自愈等手段已经变得日益标准化和系统化。然而,与应用层的测试相比,大数据相关产品的测试则更加依赖于个人的专业能力,并且通常需要更高的专业门槛。因此,大数据测试的覆盖率往往低于应用层的测试。这就为我们提供了许多潜在的探索机会。

1.功能测试 -> 质量保障:测试工作已逐渐从功能测试向质量保障的方向转变。这要求测试工作不仅要关注产品本身,还要涵盖全平台的质量和稳定性。没有接触这份工作之前,我们可能会用“点点点”来描述测试的工作内容,但在质量保障的大环境下,测试工作还包括效能工具建设、安全合规保障、流程规范制定等多个维度。

2.技术效能:测试开发工程师的主要职责之一是维护和完善效能工具。对于大数据平台来说,虽然自动化测试工具至关重要,但数据生成、全程监控等环节目前也仍主要依赖人工操作。如何将这些环节自动化,是我们面临的一项挑战。

每年都有像我一样的 JD star 加入京东,加入数据中台,也许你能比我多些大数据相关的知识储备,也许你也像我一样从零开始,但我相信这里不会让你失望。不管是遇到难关,还是想要大展拳脚,都有团队站在你的身边为你助力,都有前辈们高瞻远瞩地为你指路。我们在京东等你。

本文转载自: 掘金

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

123…399

开发者博客

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