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

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


  • 首页

  • 归档

  • 搜索

如何在Linux中设置ssh无密码登录

发表于 2021-11-04

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

ssh用于远程登录 Linux 服务器,当我们使用ssh命令登录远程服务器时,必须输入对方的密码,才能登录成功。但是如果我们经常需要远程登录大量的服务器,每次登录输入密码就会非常繁琐,费时又费力。

我们知道ssh除了支持基于密码的身份验证方式,还支持基于公钥的身份验证方式,接下来就介绍一下如何设置基于SSH密钥的身份验证在不输入密码的情况下登录到远程服务器。

设置ssh无密码登录

要在Linux中设置无密码ssh登录,需要生成公共身份验证密钥并将其添加到远程服务器的~/.ssh/authorized_keys文件即可。

检查现有的SSH密钥对

在生成新的SSH密钥对之前,可以通过一下命令检查一下自己的服务器上是否已经存在密钥:

1
shell复制代码ls -al ~/.ssh/id_*.pub

如果显示No such file or directory说明之前没有生成密钥,继续执行下面的命令生成新密钥:

1
shell复制代码ssh-keygen -t rsa -C "tigeriaf"

以下命令将生成一个新的ssh密钥对,并将”tigeriaf”作为注释。

image.png

将会生成密钥文件id_rsa和私钥文件id_rsa.pub(如果用dsa则生成id_dsa和id_dsa.pub)。

关于ssh-keygen的使用方法以及选项参数可以,查看之前的文章:Linux ssh-keygen 命令详解。

复制公钥到远程服务器

上面已经生成了ssh密钥对,要实现在没有密码的情况下登录远程服务器,还需要将公钥复制远程服务器。

使用的命令是ssh-copy-id。在本地机器执行:

1
shell复制代码ssh-copy-id username@ip_address

输入远程服务器用户的密码,通过身份验证后,公钥将附加到远程服务器用户的authorized_keys文件中。

image.png

使用ssh密钥登录服务器

上面的步骤都完成后,就能够通过ssh无密码登录到远程服务器了。

1
shell复制代码ssh username@ip_address

原创不易,如果小伙伴们觉得有帮助,麻烦点个赞再走呗~

最后,感谢女朋友在工作和生活中的包容、理解与支持 !

本文转载自: 掘金

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

每次启动项目的服务,电脑竟然乖乖的帮我打开了浏览器,100行

发表于 2021-11-04
  1. 前言

大家好,我是若川。欢迎关注我的公众号若川视野,最近组织了源码共读活动,感兴趣的可以加我微信 ruochuan12 参与,如今已进行三个月,大家一起交流学习,共同进步,很多人都表示收获颇丰。

想学源码,极力推荐之前我写的《学习源码整体架构系列》 包含jQuery、underscore、lodash、vuex、sentry、axios、redux、koa、vue-devtools、vuex4、koa-compose、vue 3.2 发布、vue-this、create-vue、玩具vite等10余篇源码文章。

本文仓库 open-analysis,求个star^_^

最近组织了源码共读活动,大家一起学习源码。于是搜寻各种值得我们学习,且代码行数不多的源码。

我们经常遇到类似场景:每次启动项目的服务,电脑竟然乖乖的帮我打开了浏览器。当然你也可能没有碰到过,但可能有这样的需求。而源码300行左右,核心源码不到100行。跟我们工作息息相关,非常值得我们学习。

之前写过据说 99% 的人不知道 vue-devtools 还能直接打开对应组件文件?本文原理揭秘,也是跟本文类似原理。

阅读本文,你将学到:

1
2
3
4
js复制代码1. 电脑竟然乖乖的帮我打开了浏览器原理和源码实现
2. 学会使用 Node.js 强大的 child_process 模块
3. 学会调试学习源码
4. 等等
  1. 使用

2.1 在 webpack 中使用

devServer.open

告诉 dev-server 在服务器启动后打开浏览器。 将其设置为 true 以打开您的默认浏览器。

webpack.config.js

1
2
3
4
5
6
bash复制代码module.exports = {
//...
devServer: {
open: true,
},
};

Usage via the CLI:

1
bash复制代码npx webpack serve --open

To disable:

1
bash复制代码npx webpack serve --no-open

现在大多数都不是直接用 webpack 配置了。而是使用脚手架。那么接着来看我们熟悉的脚手架中,打开浏览器的功能是怎么使用的。

2.2 在 vue-cli 使用

1
2
3
4
5
6
7
bash复制代码npx @vue/cli create vue3-project
# 我的 open-analysis 项目中 vue3-project 文件夹
# npm i -g yarn
# yarn serve 不会自动打开浏览器
yarn serve
# --open 参数后会自动打开浏览器
yarn serve --open

2.3 在 create-react-app 使用

1
2
3
4
5
bash复制代码npx create-react-app react-project
# 我的 open-analysis 项目中 react-project 文件夹
# npm i -g yarn
# 默认自动打开了浏览器
yarn start

为此我截了图

终端我用的是 window terminal,推荐我之前的文章:使用 ohmyzsh 打造 windows、ubuntu、mac 系统高效终端命令行工具,用过都说好。

webpack、vue-cli和create-react-app,它们三者都有个特点就是不约而同的使用了open。

引用 open 分别的代码位置是:

  • webpack-dev-server
  • vue-cli
  • create-react-app

接着我们来学习open原理和源码。

  1. 原理

在 npm 之王 @sindresorhus 的 open README文档中,英文描述中写了为什么使用它的几条原因。

为什么推荐使用 open

1
2
3
4
5
6
bash复制代码积极维护。
支持应用参数。
更安全,因为它使用 spawn 而不是 exec。
修复了大多数 node-open 的问题。
包括适用于 Linux 的最新 xdg-open 脚本。
支持 Windows 应用程序的 WSL 路径。

一句话概括open原理则是:针对不同的系统,使用Node.js的子进程 child_process 模块的spawn方法,调用系统的命令打开浏览器。

对应的系统命令简单形式则是:

1
2
3
4
5
6
bash复制代码# mac
open https://lxchuan12.gitee.io
# win
start https://lxchuan12.gitee.io
# linux
xdg-open https://lxchuan12.gitee.io

windows start 文档

open包描述信息:open

在这里可以看到有哪些 npm 包依赖了 open

我们熟悉的很多 npm 包都依赖了open。这里列举几个。

  • webpack-dev-server
  • react-dev-utils
  • @vue/cli-shared-utils
  • patch-package
  • lighthouse
  • release-it
  1. 阅读源码前的准备工作

1
2
3
4
5
6
7
8
9
bash复制代码# 推荐克隆我的项目,保证与文章同步,同时测试文件齐全
git clone https://github.com/lxchuan12/open-analysis.git
# npm i -g yarn
cd open && yarn

# 或者克隆官方项目
git clone https://github.com/sindresorhus/open.git
# npm i -g yarn
cd open && yarn

4.1 写个例子,便于调试源码

由于测试用例相对较为复杂,我们自己动手写个简单的例子,便于我们自己调试。

根据 README,我们在 open-analysis 文件夹下新建一个文件夹 examples ,里面存放一个 index.js。文件内容如下:

1
2
3
4
5
js复制代码// open-analysis/examples/index.js
(async () => {
const open = require('../open/index.js');
await open('https://lxchuan12.gitee.io');
})();

在 await open('https://lxchuan12.gitee.io'); 打上断点。在终端命令行中执行

1
bash复制代码node examples/index.js

会自动唤起调试模式。如果不支持先阅读这个官方文档配置:Node.js debugging in VS Code,如果还是不行,可以升级到最新版VSCode试试。

跟着调试我们可以进入 open 函数。

调试

VSCode 调试 Node.js 说明

4.2 open 打开函数

1
2
3
4
5
6
7
8
9
10
11
js复制代码// open/index.js
const open = (target, options) => {
if (typeof target !== 'string') {
throw new TypeError('Expected a `target`');
}

return baseOpen({
...options,
target
});
};

跟着断点,我们来看最终调用的 baseOpen。
这个函数比较长,重点可以猜到是:const subprocess = childProcess.spawn(command, cliArguments, childProcessOptions);这句,我们可以打算断点调试。

4.3 baseOpen 基础打开函数

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
js复制代码// open/index.js
const childProcess = require('child_process');
const localXdgOpenPath = path.join(__dirname, 'xdg-open');

const {platform, arch} = process;
// 调试时我们可以自行调整修改平台,便于调试各个平台异同,比如 mac、win、linux
// const {arch} = process;
// mac
// const platform = 'darwin';
// win
// const platform = 'win32';
// const platform = '其他';

const baseOpen = async options => {
options = {
wait: false,
background: false,
newInstance: false,
allowNonzeroExitCode: false,
...options
};
// 省略部分代码
// 命令
let command;
// 命令行参数
const cliArguments = [];
// 子进程选项
const childProcessOptions = {};
if (platform === 'darwin') {
command = 'open';
// 省略 mac 部分代码
} else if (platform === 'win32' || (isWsl && !isDocker())) {
// 省略 window 或者 window 子系统代码
const encodedArguments = ['Start'];
} else {
const useSystemXdgOpen = process.versions.electron ||
platform === 'android' || isBundled || !exeLocalXdgOpen;
command = useSystemXdgOpen ? 'xdg-open' : localXdgOpenPath;
// 省略 linux 代码
}
// 省略部分代码
const subprocess = childProcess.spawn(command, cliArguments, childProcessOptions);

// 省略部分代码
subprocess.unref();

return subprocess;
}

由此我们可以看出:

一句话概括open原理则是:针对不同的系统,使用Node.js的子进程 child_process 模块的spawn方法,调用系统的命令打开浏览器。

对应的系统命令简单形式则是:

1
2
3
4
5
6
bash复制代码# mac
open https://lxchuan12.gitee.io
# win
start https://lxchuan12.gitee.io
# linux
xdg-open https://lxchuan12.gitee.io
  1. 总结

一句话概括open原理则是:针对不同的系统,使用Node.js的子进程 child_process 模块的spawn方法,调用系统的命令打开浏览器。

本文从日常常见的场景每次启动服务就能自动打开浏览器出发,先讲述了日常在webpack、vue-cli、create-react-app如何使用该功能,最后从源码层面解读了open的原理和源码实现。工作常用的知识能做到知其然,知其所以然,就比很多人厉害了。

因为文章不宜过长,所以未全面展开讲述源码中所有细节。非常建议读者朋友按照文中方法使用VSCode调试 open 源码。学会调试源码后,源码并没有想象中的那么难。

最后可以持续关注我@若川。欢迎加我微信 ruochuan12 交流,参与 源码共读 活动,大家一起学习源码,共同进步。


关于 && 交流群

最近组织了源码共读活动,感兴趣的可以加我微信 ruochuan12 参与,长期交流学习。

作者:常以若川为名混迹于江湖。欢迎加我微信ruochuan12。前端路上 | 所知甚少,唯善学。

关注公众号若川视野,每周一起学源码,学会看源码,进阶高级前端。

若川的博客

segmentfault若川视野专栏,开通了若川视野专栏,欢迎关注~

掘金专栏,欢迎关注~

知乎若川视野专栏,开通了若川视野专栏,欢迎关注~

github blog,求个star^_^~

本文转载自: 掘金

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

坏了!面试官问我垃圾回收机制

发表于 2021-11-04

面试官:我还记得上次你讲到JVM内存结构(运行时数据区域)提到了「堆」,然后你说是分了几块区域嘛

面试官:当时感觉再讲下去那我可能就得加班了

面试官:今天有点空了,继续聊聊「堆」那块吧

候选者:嗯,前面提到了堆分了「新生代」和「老年代」,「新生代」又分为「Eden」和「Survivor」区,「survivor」区又分为「From Survivor」和「To Survivor」区

候选者:说到这里,我就想聊聊Java的垃圾回收机制了

面试官:那你开始你的表演吧

候选者:我们使用Java的时候,会创建很多对象,但我们未曾「手动」将这些对象进行清除

候选者:而如果用C/C++语言的时候,用完是需要自己free(释放)掉的

候选者:那为什么在写Java的时候不用我们自己手动释放”垃圾”呢?原因很简单,JVM帮我们做了(自动回收垃圾)

面试官:嗯…

候选者:我个人对垃圾的定义:只要对象不再被使用了,那我们就认为该对象就是垃圾,对象所占用的空间就可以被回收

面试官:那是怎么判断对象不再被使用的呢?

候选者:常用的算法有两个「引用计数法」和「可达性分析法」

候选者:引用计数法思路很简单:当对象被引用则+1,但对象引用失败则-1。当计数器为0时,说明对象不再被引用,可以被可回收

候选者:引用计数法最明显的缺点就是:如果对象存在循环依赖,那就无法定位该对象是否应该被回收(A依赖B,B依赖A)

面试官:嗯…

候选者:另一种就是可达性分析法:它从「GC Roots」开始向下搜索,当对象到「GC Roots」都没有任何引用相连时,说明对象是不可用的,可以被回收

候选者:「GC Roots」是一组必须「活跃」的引用。从「GC Root」出发,程序通过直接引用或者间接引用,能够找到可能正在被使用的对象

面试官:还是不太懂,那「GC Roots」一般是什么?你说它是一组活跃的引用,能不能举个例子,太抽象了。

候选者:比如我们上次不是聊到JVM内存结构中的虚拟机栈吗,虚拟机栈里不是有栈帧吗,栈帧不是有局部变量吗?局部变量不就存储着引用嘛。

候选者:那如果栈帧位于虚拟机栈的栈顶,是不是就可以说明这个栈帧是活跃的(换言之,是线程正在被调用的)

候选者:既然是线程正在调用的,那栈帧里的指向「堆」的对象引用,是不是一定是「活跃」的引用?

候选者:所以,当前活跃的栈帧指向堆里的对象引用就可以是「GC Roots」

面试官:嗯…

候选者:当然了,能作为「GC Roots」也不单单只有上面那一小块

候选者:比如类的静态变量引用是「GC Roots」,被「Java本地方法」所引用的对象也是「GC Roots」等等…

候选者:回到理解的重点:「GC Roots」是一组必须「活跃」的「引用」,只要跟「GC Roots」没有直接或者间接引用相连,那就是垃圾

候选者:JVM用的就是「可达性分析算法」来判断对象是否垃圾

面试官:懂了

候选者:垃圾回收的第一步就是「标记」,标记哪些没有被「GC Roots」引用的对象

候选者:标记完之后,我们就可以选择直接「清除」,只要不被「GC Roots」关联的,都可以干掉

候选者:过程非常简单粗暴,但也存在很明显的问题

候选者:直接清除会有「内存碎片」的问题:可能我有10M的空余内存,但程序申请9M内存空间却申请不下来(10M的内存空间是垃圾清除后的,不连续的)

候选者:那解决「内存碎片」的问题也比较简单粗暴,「标记」完,不直接「清除」。

候选者:我把「标记」存活的对象「复制」到另一块空间,复制完了之后,直接把原有的整块空间给干掉!这样就没有内存碎片的问题了

候选者:这种做法缺点又很明显:内存利用率低,得有一块新的区域给我复制(移动)过去

面试官:嗯…

候选者:还有一种「折中」的办法,我未必要有一块「大的完整空间」才能解决内存碎片的问题,我只要能在「当前区域」内进行移动

候选者:把存活的对象移到一边,把垃圾移到一边,那再将垃圾一起删除掉,不就没有内存碎片了嘛

候选者:这种专业的术语就叫做「整理」

候选者:扯了这么久,我们把思维再次回到「堆」中吧

候选者:经过研究表明:大部分对象的生命周期都很短,而只有少部分对象可能会存活很长时间

候选者:又由于「垃圾回收」是会导致「stop the world」(应用停止访问)

候选者:理解「stop the world」应该很简单吧:回收垃圾的时候,程序是有短暂的时间不能正常继续运作啊。不然JVM在回收的时候,用户线程还继续分配修改引用,JVM怎么搞(:

候选者:为了使「stop the world」持续的时间尽可能短以及提高并发式GC所能应付的内存分配速率

候选者:在很多的垃圾收集器上都会在「物理」或者「逻辑」上,把这两类对象进行区分,死得快的对象所占的区域叫做「年轻代」,活得久的对象所占的区域叫做「老年代」

候选者:但也不是所有的「垃圾收集器」都会有,只不过我们现在线上用的可能都是JDK8,JDK8及以下所使用到的垃圾收集器都是有「分代」概念的。

候选者:所以,你可以看到我的「堆」是画了「年轻代」和「老年代」

候选者:要值得注意的是,高版本所使用的垃圾收集器的ZGC是没有分代的概念的(:

候选者:只不过我为了好说明现状,ZGC的话有空我们再聊

面试官:嗯…好吧

候选者:在前面更前面提到了垃圾回收的过程,其实就对应着几种「垃圾回收算法」,分别是:

候选者:标记清除算法、标记复制算法和标记整理算法【「标记」「清除」「复制」「整理」】

候选者:经过上面的铺垫之后,这几种算法应该还是比较好理解的

候选者:「分代」和「垃圾回收算法」都搞明白了之后,我们就可以看下在JDK8生产环境及以下常见的垃圾回收器了

候选者:「年轻代」的垃圾收集器有:Seria、Parallel Scavenge、ParNew

候选者:「老年代」的垃圾收集器有:Serial Old、Parallel Old、CMS

候选者:看着垃圾收集器有很多,其实还是非常好理解的。Serial是单线程的,Parallel是多线程

候选者:这些垃圾收集器实际上就是「实现了」垃圾回收算法(标记复制、标记整理以及标记清除算法)

候选者:CMS是「JDK8之前」是比较新的垃圾收集器,它的特点是能够尽可能减少「stop the world」时间。在垃圾回收时让用户线程和 GC 线程能够并发执行!

候选者:又可以发现的是,「年轻代」的垃圾收集器使用的都是「标记复制算法」

候选者:所以在「堆内存」划分中,将年轻代划分出Survivor区(Survivor From 和Survivor To),目的就是为了有一块完整的内存空间供垃圾回收器进行拷贝(移动)

候选者:而新的对象则放入Eden区

候选者:我下面重新画下「堆内存」的图,因为它们的大小是有默认的比例的

候选者:图我已经画好了,应该就不用我再说明了

面试官:我还想问问,就是,新创建的对象一般是在「新生代」嘛,那在什么时候会到「老年代」中呢?

候选者:嗯,我认为简单可以分为两种情况:

候选者:1. 如果对象太大了,就会直接进入老年代(对象创建时就很大 || Survivor区没办法存下该对象)

候选者:2. 如果对象太老了,那就会晋升至老年代(每发生一次Minor GC ,存活的对象年龄+1,达到默认值15则晋升老年代 || 动态对象年龄判定 可以进入老年代)

面试官:既然你又提到了Minor GC,那Minor GC 什么时候会触发呢?

候选者:当Eden区空间不足时,就会触发Minor GC

面试官:Minor GC 在我的理解就是「年轻代」的GC,你前面又提到了「GC Roots」嘛

面试官:那在「年轻代」GC的时候,从GC Roots出发,那不也会扫描到「老年代」的对象吗?那那那..不就相当于全堆扫描吗?

候选者:这JVM里也有解决办法的。

候选者:HotSpot 虚拟机「老的GC」(G1以下)是要求整个GC堆在连续的地址空间上。

候选者:所以会有一条分界线(一侧是老年代,另一侧是年轻代),所以可以通过「地址」就可以判断对象在哪个分代上

候选者:当做Minor GC的时候,从GC Roots出发,如果发现「老年代」的对象,那就不往下走了(Minor GC对老年代的区域毫无兴趣)

面试官:但又有个问题,那如果「年轻代」的对象被「老年代」引用了呢?(老年代对象持有年轻代对象的引用),那时候肯定是不能回收掉「年轻代」的对象的。

候选者:HotSpot虚拟机下 有「card table」(卡表)来避免全局扫描「老年代」对象

候选者:「堆内存」的每一小块区域形成「卡页」,卡表实际上就是卡页的集合。当判断一个卡页中有存在对象的跨代引用时,将这个页标记为「脏页」

候选者:那知道了「卡表」之后,就很好办了。每次Minor GC 的时候只需要去「卡表」找到「脏页」,找到后加入至GC Root,而不用去遍历整个「老年代」的对象了。

面试官:嗯嗯嗯,还可以的啊,要不继续聊聊CMS?

候选者:这面试快一个小时了吧,我图也画了这么多了。下次?下次吧?有点儿累了

本文总结:

  • 什么是垃圾:只要对象不再被使用,那即是垃圾
  • 如何判断为垃圾:可达性分析算法和引用计算算法,JVM使用的是可达性分析算法
  • 什么是GC Roots:GC Roots是一组必须活跃的引用,跟GC Roots无关联的引用即是垃圾,可被回收
  • 常见的垃圾回收算法:标记清除、标记复制、标记整理
  • 为什么需要分代:大部分对象都死得早,只有少部分对象会存活很长时间。在堆内存上都会在物理或逻辑上进行分代,为了使「stop the world」持续的时间尽可能短以及提高并发式GC所能应付的内存分配速率。
  • Minor GC:当Eden区满了则触发,从GC Roots往下遍历,年轻代GC不关心老年代对象
  • 什么是card table【卡表】:空间换时间(类似bitmap),能够避免扫描老年代的所有对应进而顺利进行Minor GC (案例:老年代对象持有年轻代对象引用)
  • 堆内存占比:年轻代占堆内存1/3,老年代占堆内存2/3。Eden区占年轻代8/10,Survivor区占年轻代2/10(其中From 和To 各站1/10)

欢迎关注我的微信公众号【Java3y】来聊聊Java面试,对线面试官系列持续更新中!

【对线面试官-移动端】系列 一周两篇持续更新中!

【对线面试官-电脑端】系列 一周两篇持续更新中!

原创不易!!求三连!!

本文转载自: 掘金

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

近期业务大量突增微服务性能优化总结-4增加对于同步微服务的

发表于 2021-11-04

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

最近,业务增长的很迅猛,对于我们后台这块也是一个不小的挑战,这次遇到的核心业务接口的性能瓶颈,并不是单独的一个问题导致的,而是几个问题揉在一起:我们解决一个之后,发上线,之后发现还有另一个的性能瓶颈问题。这也是我经验不足,导致没能一下子定位解决;而我又对我们后台整个团队有着固执的自尊,不想通过大量水平扩容这种方式挺过压力高峰,导致线上连续几晚都出现了不同程度的问题,肯定对于我们的业务增长是有影响的。这也是我不成熟和要反思的地方。这系列文章主要记录下我们针对这次业务增长,对于我们后台微服务系统做的通用技术优化,针对业务流程和缓存的优化由于只适用于我们的业务,这里就不再赘述了。本系列会分为如下几篇:

  1. 改进客户端负载均衡算法
  2. 开发日志输出异常堆栈的过滤插件
  3. 针对 x86 云环境改进异步日志等待策略
  4. 增加对于同步微服务的 HTTP 请求等待队列的监控以及云上部署,需要小心达到实例网络流量上限导致的请求响应缓慢
  5. 针对系统关键业务增加必要的侵入式监控

增加对于同步微服务的 HTTP 请求等待队列的监控

同步微服务对于请求超时存在的问题

相对于基于 spring-webflux 的异步微服务,基于 spring-webmvc 的同步微服务没有很好的处理客户端有请求超时配置的情况。当客户端请求超时时,客户端会直接返回超时异常,但是调用的服务端任务,在基于 spring-webmvc 的同步微服务并没有被取消,基于 spring-webflux 的异步微服务是会被取消的。目前,还没有很好的办法在同步环境中可以取消这些已经超时的任务。

我们的基于 spring-webmvc 的同步微服务,HTTP 容器使用的是 Undertow。在 spring-boot 环境下,我们可以配置处理 HTTP 请求的线程池大小:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
yaml复制代码server:
undertow:
# 以下的配置会影响buffer,这些buffer会用于服务器连接的IO操作
# 如果每次需要 ByteBuffer 的时候都去申请,对于堆内存的 ByteBuffer 需要走 JVM 内存分配流程(TLAB -> 堆),对于直接内存则需要走系统调用,这样效率是很低下的。
# 所以,一般都会引入内存池。在这里就是 `BufferPool`。
# 目前,UnderTow 中只有一种 `DefaultByteBufferPool`,其他的实现目前没有用。
# 这个 DefaultByteBufferPool 相对于 netty 的 ByteBufArena 来说,非常简单,类似于 JVM TLAB 的机制
# 对于 bufferSize,最好和你系统的 TCP Socket Buffer 配置一样
# `/proc/sys/net/ipv4/tcp_rmem` (对于读取)
# `/proc/sys/net/ipv4/tcp_wmem` (对于写入)
# 在内存大于 128 MB 时,bufferSize 为 16 KB 减去 20 字节,这 20 字节用于协议头
buffer-size: 16364
# 是否分配的直接内存(NIO直接分配的堆外内存),这里开启,所以java启动参数需要配置下直接内存大小,减少不必要的GC
# 在内存大于 128 MB 时,默认就是使用直接内存的
directBuffers: true
threads:
# 设置IO线程数, 它主要执行非阻塞的任务,它们会负责多个连接, 默认设置每个CPU核心一个读线程和一个写线程
io: 4
# 阻塞任务线程池, 当执行类似servlet请求阻塞IO操作, undertow会从这个线程池中取得线程
# 它的值设置取决于系统线程执行任务的阻塞系数,默认值是IO线程数*8
worker: 128

其背后的线程池,是 jboss 的线程池:org.jboss.threads.EnhancedQueueExecutor,spring-boot 目前不能通过配置修改这个线程池的队列大小,默认队列大小是 Integer.MAX

我们需要监控这个线程池的队列大小,并针对这个指标做一些操作:

  • 当这个任务持续增多的时候,就代表这时候请求处理跟不上请求到来的速率了,需要报警。
  • 当累积到一定数量时,需要将这个实例暂时从注册中心取下,并扩容。
  • 待这个队列消费完之后,重新上线。
  • 当超过一定时间还是没有消费完的话,将这个实例重启。

添加同步微服务 HTTP 请求等待队列监控

幸运的是,org.jboss.threads.EnhancedQueueExecutor 本身通过 JMX 暴露了 HTTP servlet 请求的线程池的各项指标:

image

我们的项目中,使用两种监控:

  • prometheus + grafana 微服务指标监控,这个主要用于报警以及快速定位问题根源
  • JFR 监控,这个主要用于详细定位单实例问题

对于 HTTP 请求等待队列监控,我们应该通过 prometheus 接口向 grafana 暴露,采集指标并完善响应操作。

暴露 prometheus 接口指标的代码是:

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
less复制代码@Log4j2
@Configuration(proxyBeanMethods = false)
//需要在引入了 prometheus 并且 actuator 暴露了 prometheus 端口的情况下才加载
@ConditionalOnEnabledMetricsExport("prometheus")
public class UndertowXNIOConfiguration {
@Autowired
private ObjectProvider<PrometheusMeterRegistry> meterRegistry;
//只初始化一次
private volatile boolean isInitialized = false;

//需要在 ApplicationContext 刷新之后进行注册
//在加载 ApplicationContext 之前,日志配置就已经初始化好了
//但是 prometheus 的相关 Bean 加载比较复杂,并且随着版本更迭改动比较多,所以就直接偷懒,在整个 ApplicationContext 刷新之后再注册
// ApplicationContext 可能 refresh 多次,例如调用 /actuator/refresh,还有就是多 ApplicationContext 的场景
// 这里为了简单,通过一个简单的 isInitialized 判断是否是第一次初始化,保证只初始化一次
@EventListener(ContextRefreshedEvent.class)
public synchronized void init() {
if (!isInitialized) {
Gauge.builder("http_servlet_queue_size", () ->
{
try {
return (Integer) ManagementFactory.getPlatformMBeanServer()
.getAttribute(new ObjectName("org.xnio:type=Xnio,provider=\"nio\",worker=\"XNIO-2\""), "WorkerQueueSize");
} catch (Exception e) {
log.error("get http_servlet_queue_size error", e);
}
return -1;
}).register(meterRegistry.getIfAvailable());
isInitialized = true;
}
}
}

之后,调用 /actuator/prometheus 我们就能看到对应的指标:

1
2
3
bash复制代码# HELP http_servlet_queue_size  
# TYPE http_servlet_queue_size gauge
http_servlet_queue_size 0.0

当发生队列堆积时,我们能快速的报警,并且直观地从 grafana 监控上发现:

image

对于公有云部署,关注网络限制的监控

现在的公有云,都会针对物理机资源进行虚拟化,对于网络网卡资源,也是会虚拟化的。以 AWS 为例,其网络资源的虚拟化实现即 ENA(Elastic Network Adapter)。它会对以下几个指标进行监控并限制:

  • 带宽:每个虚拟机实例(AWS 中为每个 EC2 实例),都具有流量出的最大带宽以及流量入的最大带宽。这个统计使用一种网络 I/O 积分机制,根据平均带宽使用率分配网络带宽,最后的效果是允许短时间内超过额定带宽,但是不能持续超过。
  • 每秒数据包 (PPS,Packet Per Second) 个数:每个虚拟机实例(AWS 中为每个 EC2 实例)都限制 PPS 大小
  • 连接数:建立连接的个数是有限的
  • 链接本地服务访问流量:一般在公有云,每个虚拟机实例 (AWS 中为每个 EC2 实例)访问 DNS,元数据服务器等,都会限制流量

同时,成熟的公有云,这些指标一般都会对用户提供展示分析界面,例如 AWS 的 CloudWatch 中,就提供了以下几个指标的监控:

image

在业务流量突增时,我们通过 JFR 发现访问 Redis 有性能瓶颈,但是 Redis 本身的监控显示他并没有遇到性能瓶颈。这时候就需要查看是否因为网络流量限制导致其除了问题,在我们出问题的时间段,我们发现 NetworkBandwidthOutAllowanceExceeded 事件显著提高了很多:

image

对于这种问题,就得需要考虑垂直扩容(提升实例配置)与水平扩容(多实例负载均衡)了,或者减少网络流量(增加压缩等)

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

本文转载自: 掘金

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

SpringBoot 实战:一招实现结果的优雅响应

发表于 2021-11-04

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

你好,我是看山。

今天说一下 Spring Boot 如何实现优雅的数据响应:统一的结果响应格式、简单的数据封装。

前提

无论系统规模大小,大部分 Spring Boot 项目是提供 Restful + json 接口,供前端或其他服务调用,格式统一规范,是程序猿彼此善待彼此的象征,也是减少联调挨骂的基本保障。

通常响应结果中需要包含业务状态码、响应描述、响应时间戳、响应内容,比如:

1
2
3
4
5
6
7
8
9
js复制代码{
  "code": 200,
  "desc": "查询成功",
  "timestamp": "2020-08-12 14:37:11",
  "data": {
    "uid": "1597242780874",
    "name": "测试 1"
  }
}

对于业务状态码分为两个派系:一个是推荐使用 HTTP 响应码作为接口业务返回;另一种是 HTTP 响应码全部返回 200,在响应体中通过单独的字段表示响应状态。两种方式各有优劣,个人推荐使用第二种,因为很多 Web 服务器对 HTTP 状态码有拦截处理功能,而且状态码数量有限,不够灵活。比如返回 200 表示接口处理成功且正常响应,现在需要有一个状态码表示接口处理成功且正常响应,但是请求数据状态不对,可以返回 2001 表示。

自定义响应体

定义一个数据响应体是返回统一响应格式的第一步,无论接口正常返回,还是发生异常,返回给调用方的结构格式都应该不变。给出一个示例:

1
2
3
4
5
6
7
8
9
10
11
12
java复制代码@ApiModel
@Data
public class Response<T> {
    @ApiModelProperty(value = "返回码", example = "200")
    private Integer code;
    @ApiModelProperty(value = "返回码描述", example = "ok")
    private String desc;
    @ApiModelProperty(value = "响应时间戳", example = "2020-08-12 14:37:11")
    private Date timestamp = new Date();
    @ApiModelProperty(value = "返回结果")
    private T data;
}

这样,只要在 Controller 的方法返回Response就可以了,接口响应就一致了,但是这样会形成很多格式固定的代码模板,比如下面这种写法:

1
2
3
4
5
6
7
8
java复制代码@RequestMapping("hello1")
public Response<String> hello1() {
    final Response<String> response = new Response<>();
    response.setCode(200);
    response.setDesc("返回成功");
    response.setData("Hello, World!");
    return response;
}

调用接口响应结果为:

1
2
3
4
5
6
js复制代码{
  "code": 200,
  "desc": "返回成功",
  "timestamp": "2020-08-12 14:37:11",
  "data": "Hello, World!"
}

这种重复且没有技术含量的代码,怎么能配得上程序猿这种优(lan)雅(duo)的生物呢?最好能在返回响应结果的前提下,减去那些重复的代码,比如:

1
2
3
4
java复制代码@RequestMapping("hello2")
public String hello2() {
    return "Hello, World!";
}

这就需要借助 Spring 提供的ResponseBodyAdvice来实现了。

全局处理响应数据

先上代码:

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
java复制代码/**
 * <br>created at 2020/8/12
 *
 * @author www.howardliu.cn
 * @since 1.0.0
 */
@RestControllerAdvice
public class ResultResponseAdvice implements ResponseBodyAdvice<Object> {
    @Override
    public boolean supports(final MethodParameter returnType, final Class<? extends HttpMessageConverter<?>> converterType) {
        return !returnType.getGenericParameterType().equals(Response.class);// 1
    }

    @Override
    public Object beforeBodyWrite(final Object body, final MethodParameter returnType, final MediaType selectedContentType,
                                  final Class<? extends HttpMessageConverter<?>> selectedConverterType,
                                  final ServerHttpRequest request, final ServerHttpResponse response) {
        if (body == null || body instanceof Response) {
            return body;
        }
        final Response<Object> result = new Response<>();
        result.setCode(200);
        result.setDesc("查询成功");
        result.setData(body);
        if (returnType.getGenericParameterType().equals(String.class)) {// 2
            ObjectMapper objectMapper = new ObjectMapper();
            try {
                return objectMapper.writeValueAsString(result);
            } catch (JsonProcessingException e) {
                throw new RuntimeException("将 Response 对象序列化为 json 字符串时发生异常", e);
            }
        }
        return result;
    }
}

/**
 * <br>created at 2020/8/12
 *
 * @author www.howardliu.cn
 * @since 1.0.0
 */
@RestController
public class HelloWorldController {
    @RequestMapping("hello2")
    public String hello2() {
        return "Hello, World!";
    }

    @RequestMapping("user1")
    public User user1() {
        User u = new User();
        u.setUid(System.currentTimeMillis() + "");
        u.setName("测试1");
        return u;
    }
}

上面代码是实现了 Spring ResponseBodyAdvice类的模板方式,按照 Spring 的要求实现就行。只有两个需要特别注意的地方,也就是代码中标注 1 和 2 的地方。

首先说 1 这一行,也就是supports方法,这个方法是校验是否需要调用beforeBodyWrite方法的前置判断,返回true则执行beforeBodyWrite方法,这里根据 Controller 方法返回类型来判断是否需要执行beforeBodyWrite,也可以一律返回true,在后面判断是否需要进行类型转换。

然后重点说下 2 这一行,这行是坑,是大坑,如果对 Spring 结构不熟悉的,绝对会在这徘徊许久,不得妙法。

代码 2 这一行是判断Controller的方法是否返回的是String类型的结果,如果是,将返回的对象序列化之后返回。

这是因为Spring对String类型的响应类型单独处理了,使用StringHttpMessageConverter类进行数据转换。在处理响应结果的时候,会在方法getContentLength中计算响应体大小,其父类方法定义是protected Long getContentLength(T t, @Nullable MediaType contentType),而StringHttpMessageConverter将方法重写为protected Long getContentLength(String str, @Nullable MediaType contentType),第一个参数是响应对象,固定写死是String类型,如果我们强制返回Response对象,就会报ClassCastException。

当然,直接返回String的场景不多,这个坑可能会在某天特殊接口中突然出现。

补充说明

上面只是展示了ResponseBodyAdvice类最简单的应用,我们还可以实现更多的扩展使用。比如:

  1. 返回请求ID:这个需要与与RequestBodyAdvice联动,获取到请求ID后,在响应是放在响应体中;
  2. 结果数据加密:通过ResponseBodyAdvice实现响应数据加密,不会侵入业务代码,而且可以通过注解方式灵活处理接口的加密等级;
  3. 有选择的包装响应体:比如定义注解IgnoreResponseWrap,在不需要包装响应体的接口上定义,然后在supports方法上判断方法的注解即可,比如:
1
2
3
4
5
java复制代码@Override
public boolean supports(final MethodParameter returnType, final Class<? extends HttpMessageConverter<?>> converterType) {
    final IgnoreResponseWrap[] declaredAnnotationsByType = returnType.getExecutable().getDeclaredAnnotationsByType(IgnoreResponseWrap.class);
    return !(declaredAnnotationsByType.length > 0 || returnType.getGenericParameterType().equals(Response.class));
}

很多其他玩法就不一一列举了。

总结

上面说了正常响应的数据,只做到了一点优雅,想要完整,还需要考虑接口异常情况,总不能来个大大的try/catch/finally包住业务逻辑吧,那也太丑了。后面会再来一篇,重点说说接口如何在出现异常时,也能返回统一的结果响应。

推荐阅读

  • SpringBoot 实战:一招实现结果的优雅响应

你好,我是看山。游于码界,戏享人生。如果文章对您有帮助,请点赞、收藏、关注。欢迎关注公众号「看山的小屋」,发现不一样的世界。

本文转载自: 掘金

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

工厂模式——猫粮公司的演进

发表于 2021-11-04

一起用代码吸猫!本文正在参与【喵星人征文活动】

猫粮公司的诞生

陀螺是个程序喵,另起炉灶自己开了公司,为了纪念曾经码梦为生的岁月,公司起名为“跑码场”,主要业务是生产猫粮。

一个喵兼顾着研发和运营,终究不是长久之计。于是雇了一个菜喵做学徒,技术怎么样并不在意,陀螺最看重的是菜喵的名字—招财。

很快,第一款产品「鱼香猫粮」上线,陀螺让招财写个线上订单系统,方便顾客网上下单

招财很快写出了代码

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复制代码/**
* 鱼香味猫粮
*
* @author 蝉沐风
*/
public class FishCatFood {

//猫粮口味
private String flavor;

//制作猫粮的工艺过程
public void make() {
System.out.println("正在制作【" + flavor + "】口味的猫粮");
}

public String getFlavor() {
return flavor;
}

public void setFlavor(String flavor) {
this.flavor = flavor;
}

public FishCatFood(String flavor) {
this.flavor = flavor;
}
}
1
2
3
4
5
6
7
8
java复制代码public class PaoMaChang {

public FishCatFood order() {
FishCatFood fishCatFood = new FishCatFood("fish");
fishCatFood.make();
return fishCatFood;
}
}

测试之后上线,一直运行正常。

过了一段时间,陀螺对招财说:“公司目前正在研发一款牛肉猫粮,并且预计在接下来一段时间会上线「薄荷猫粮」、「鸡肉猫粮」等多款新品,你升级一下订单系统应对一下未来可能发生的改变。”

招财接到任务,重构了原来的代码,首先创建了抽象的CatFood,之后所有具体口味的猫粮必须继承该类

1
2
3
4
5
6
7
8
9
10
11
java复制代码/**
* 猫粮的抽象类,所有具体口味的猫粮必须继承自该接口
*
* @author 蝉沐风
*/
public abstract class CatFood {
//产品风味
String flavor;

public abstract void make();
}

接下来依次是各种口味的猫粮对象

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
java复制代码/**
* 牛肉猫粮
*/
public class BeefCatFood extends CatFood {
public BeefCatFood() {
this.flavor = "beef";
}

@Override
public void make() {
System.out.println("正在制作【beef】口味猫粮");
}
}

/**
* 鸡肉猫粮
*/
public class ChickenCatFood extends CatFood {
public ChickenCatFood() {
this.flavor = "chicken";
}

@Override
public void make() {
System.out.println("正在制作【chicken】口味猫粮");
}
}

/**
* 鱼香猫粮
*/
public class FishCatFood extends CatFood {
public FishCatFood() {
this.flavor = "fish";
}

@Override
public void make() {
System.out.println("正在制作【fish】口味猫粮");
}
}

/**
* 薄荷猫粮
*/
public class MintCatFood extends CatFood {
public MintCatFood() {
this.flavor = "mint";
}

@Override
public void make() {
System.out.println("正在制作【mint】口味猫粮");
}
}

最后是下单的逻辑

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

public CatFood order(String flavor) {

CatFood catFood;

if ("fish".equals(flavor)) {
catFood = new FishCatFood();
} else if ("beef".equals(flavor)) {
catFood = new BeefCatFood();
} else if ("mint".equals(flavor)) {
catFood = new MintCatFood();
} else if ("chicken".equals(flavor)) {
catFood = new ChickenCatFood();
} else {
throw new RuntimeException("找不到该口味的猫粮");
}

catFood.make();

return catFood;
}
}

招财迫不及待地向陀螺展示自己的代码,并介绍到:“老板,我的代码已经能够满足未来的动态变化了,如果再有新口味的产品,只需要创建该产品的对象,然后修改一下order()方法就好了!”

陀螺赞赏地点点头,“看得出来你经过了自己认真的思考,这一点非常好!但是别着急,你有没有听说过开闭原则?”

“开闭原则?听说过,但是仅仅停留在概念上,我记得好像是‘对修改关闭,对扩展开放’,当时为了面试背的还是挺熟的,哈哈哈”

“那你对照开闭原则再看一下你的代码,你觉得你的代码有什么问题?”,陀螺问道。

招财赶紧仔细审视了一下自己的代码,”我知道了,现在的问题是一旦有新产品上线,就需要改动orde()方法,这就是所谓的没有对修改关闭吧,但是有了新的产品你总得有个地方把他new出来啊,这一步是无论如何都无法省略的,我觉得目前的代码是能够满足需求的。”

“你说的没错,设计原则并不是金科玉律,比如未来如果只有零星几个的新口味产品上线的话,你确实没有必要改变现在的代码结构,简单的修改一下order()就可以了,根本不用在意对修改关闭的这种约束。但是你有必要思考一下,如果后期我们研发了数十种乃至上百种产品,这种情况下你该怎么做?”

“除了修改order()方法,我实在没有想出其他的办法…”,招财挠着脑袋回答道。

陀螺不急不慢地解释说:“这种时候,我们可以先识别出代码中哪些是经常变化的部分,然后考虑使用封装,很明显,order()方法中创建对象的部分就是经常需要变化的,我们可以将封装,使其专门用于创造对象。”

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复制代码/**
* 猫粮类的简单工厂
* @author 蝉沐风
*/
public class SimpleCatFoodFactory {
public static CatFood createCatFood(String flavor) {
CatFood catFood;

if ("fish".equals(flavor)) {
catFood = new FishCatFood();
} else if ("beef".equals(flavor)) {
catFood = new BeefCatFood();
} else if ("mint".equals(flavor)) {
catFood = new MintCatFood();
} else if ("chicken".equals(flavor)) {
catFood = new ChickenCatFood();
} else {
throw new RuntimeException("找不到该口味的猫粮");
}

return catFood;

}
}


/**
* 重构之后的order代码
*/
public class PaoMaChangV2 {

public CatFood order(String flavor) {

CatFood catFood = SimpleCatFoodFactory.createCatFood(flavor);

catFood.make();

return catFood;
}
}

陀螺解释说:“如此一来,我们完成了封装的操作,把生成对象的操作集中在了SimpleCatFoodFactory中。”

招财立即提出了自己的疑问:“我不理解这样做有什么好处,在我看来这只是把一个问题搬到了一个对象里罢了,问题本身依然存在!”

“就创建的过程而言,你说的确实没错。”,陀螺点点头,“但是,我们仍然得到了很多益处,现在我们的SimpleCatFoodFactory不仅仅可以被order()方法使用了,之后的任何相关逻辑都可以调用我们写的这个类,而且如果后续需要改变,我们也仅仅需要改变这个单独的类就可以了”。

招财无奈地回应说,“好吧,你的话确实很有道理,把经常变动的部分提取出来是个不错的代码优化习惯。对了,刚才这种优化技巧有名字吗?”

“这种叫简单工厂,很多开发人员都误以为它是一种设计模式了,但是它其实并不属于GoF23种设计模式,但是由于用的人太多,经常把它和工厂模式一起介绍。至于是不是设计模式,对我们而言并不重要。”

简单工厂并不是一种设计模式,更像是一种编程的优化习惯,用来将对象的创建过程和客户端程序进行解耦

招财并不放弃,继续追问,“那能不能有个办法再优化一下创建对象的过程呢,它现在依然没有满足开闭原则!而且客户端的调用方式非常不优雅,万一参数不小心拼错了,直接就崩了,这种麻烦不应该转嫁到客户端不是吗?”

陀螺愣了愣,久久盯着招财,仿佛看到了当年自己刚学习编程的样子,对一切充满好奇,对代码又有点洁癖,欣慰地说道:“说得好啊,那我们尝试利用反射继续优化一下吧。”

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
java复制代码/**
* 反射优化后的猫粮类的简单工厂
*
* @author 蝉沐风
*/
public class SimpleCatFoodFactoryV2 {
public static CatFood createCatFood(Class<? extends CatFood> clazz) {
if (clazz != null) {
try {
return clazz.newInstance();
} catch (Exception e) {
throw new RuntimeException("对象不存在");
}
}
return null;

}
}

客户端的代码优化如下

1
2
3
4
5
6
7
8
java复制代码public CatFood order(Class<? extends CatFood> clazz) {

CatFood catFood = SimpleCatFoodFactoryV2.createCatFood(clazz);

catFood.make();

return catFood;
}

“到此SimpleCatFoodFactoryV2就符合了开闭原则,但是这里利用反射的一个基本原则是所有对象的构造方法必须保持一致,如果对象创建的过程比较复杂而且各有特点,那么优化到这一步或许并不是最好的选择,记住优化的原则——合适就好”,陀螺补充道。

招财对陀螺的这一番优化和解说佩服不已,心想实习遇到这么个好老板好师傅,平时还能试吃自己最爱的猫粮,这简直就是在天堂啊。

猫粮公司的扩张

日子一天天过去,公司在陀螺的运营下经营有成,计划在全国各地建立分公司。为了保证服务质量,陀螺希望各个分公司能够使用他们经过时间考验的代码。

但是不同的分公司需要根据当地特色生产不同口味的产品,比如山东生产「葱香猫粮」、「大酱猫粮」,湖南生产「辣子猫粮」、「剁椒猫粮」…

招财心想,这不简单嘛!继续利用SimpleCatFoodFactoryV2,让各个公司的新款猫粮继承CatFood不就可以了嘛!

但是转念一想,随着每个分公司的产品链的丰富,获取产品的创建过程会有差异,那么SimpleCatFoodFactoryV2的职责会变得越来越多,像一个万能的类,不方便维护。

招财想到可以为每个分公司创建独立的简单工厂,然后将具体的简单工厂对象绑定到PaoMaChang对象中,顾客下单的时候只要指定对应的分公司的工厂和口味就可以进行下单了。

PaoMaChangV3重构如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
java复制代码/**
* 跑码场对象-版本3
* @author 蝉沐风
*/
public class PaoMaChangV3 {

private ICatFoodFactory factory;

public PaoMaChangV3(ICatFoodFactory factory) {
this.factory = factory;
}

public CatFood order(String flavor) {

CatFood catFood = factory.create(flavor);

catFood.make();

return catFood;
}
}

将工厂本身也做了个抽象,创建ICatFoodFactory接口

1
2
3
java复制代码public interface ICatFoodFactory {
CatFood create(String flavor);
}

各分公司的工厂代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
java复制代码/**
* 山东分公司简单工厂
*
* @author 蝉沐风
*/
public class ShanDongSimpleCatFoodFactory implements ICatFoodFactory {
CatFood catFood;

@Override
public CatFood create(String flavor) {
if ("congxiang".equals(flavor)) {
catFood = new CongXiangCatFood();
} else if ("dajiang".equals(flavor)) {
catFood = new DaJiangCatFood();
} else {
throw new RuntimeException("找不到该口味的猫粮");
}

return catFood;
}
}

/**
* 湖南分公司简单工厂
*
* @author 蝉沐风
*/
public class HuNanSimpleCatFoodFactory implements ICatFoodFactory {
CatFood catFood;

@Override
public CatFood create(String flavor) {
if ("duojiao".equals(flavor)) {
catFood = new DuoJiaoCatFood();
} else if ("mala".equals(flavor)) {
catFood = new MaLaCatFood();
} else {
throw new RuntimeException("找不到该口味的猫粮");
}

return catFood;
}
}

各种口味的猫粮代码如下

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
java复制代码/**
* 大酱猫粮
*/
public class DaJiangCatFood extends CatFood {
public DaJiangCatFood() {
this.flavor = "dajiang";
}

@Override
public void make() {
System.out.println("正在制作【大酱】口味猫粮");
}
}

/**
* 葱香猫粮
*/
public class CongXiangCatFood extends CatFood {
public CongXiangCatFood() {
this.flavor = "congxiang";
}

@Override
public void make() {
System.out.println("正在制作【葱香】口味猫粮");
}
}

/**
* 剁椒猫粮
*/
public class DuoJiaoCatFood extends CatFood {
public DuoJiaoCatFood() {
this.flavor = "duojiao";
}

@Override
public void make() {
System.out.println("正在制作【剁椒】口味猫粮");
}
}

/**
* 麻辣猫粮
*/
public class MaLaCatFood extends CatFood {
public MaLaCatFood() {
this.flavor = "mala";
}

@Override
public void make() {
System.out.println("正在制作【麻辣】口味猫粮");
}
}

产品类对应的UML图为

CatFood继承关系

顾客下单「湖南分公司」的「剁椒猫粮」的代码就变成了这样

1
2
3
4
5
6
java复制代码public static void main(String[] args) {
HuNanSimpleCatFoodFactory huNanSimpleCatFoodFactory = new HuNanSimpleCatFoodFactory();
PaoMaChangV3 paoMaChang = new PaoMaChangV3(huNanSimpleCatFoodFactory);
//下单剁椒猫粮
paoMaChang.order("duojiao");
}

到此,招财重构完了代码,经过细心检查系统终于上线了,各地分公司使用这套系统有条不紊地开展起自己的业务,形势一片大好!

之后的某一天,招财接到陀螺的电话,让他火速前往陀螺的办公室,招财一路战战兢兢,一直在想是不是自己的代码出了问题。来到办公室,陀螺招呼招财来到他旁边坐着,指着满屏的代码说道:“别害怕,你的代码到目前为止没有出什么bug。你为每一个分公司单独创建自己的简单工厂,又把简单工厂对象作为参数注入到了PaoMaChang类中,能看得出来你最近没少在代码上下功夫。只是我在审查各分公司代码的时候发现一个潜在的隐患。”说罢,打开了某分公司的代码给招财看。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
java复制代码/**
* 湖南跑码场分公司
* @author 蝉沐风
*/
public class HuNanPaoMaChangV3 {

private ICatFoodFactory factory;

public HuNanPaoMaChangV3(ICatFoodFactory factory) {
this.factory = factory;
}

public CatFood order(String flavor) {

CatFood catFood = factory.create(flavor);

catFood.make();

//湖南分公司自己添加了一个「包装」逻辑
catFood.pack();

return catFood;
}

}

招才看到,湖南分公司的技术人员在order()方法中擅自添加了一个pack()打包的方法,陀螺继续说道:“先不管这个逻辑加的对不对,光是分公司能够改动我们的核心代码这一点就是有风险的,你需要想个办法,既能让每个分公司自由创建产品,又能保证我们的核心功能不被改变,核心逻辑只能由我们来定。”

“确实是个问题,目前各个分公司的下单逻辑都是自己定义的,我们需要提供一个真正的“框架”,让他们按照我们的标准来进行业务逻辑。”

“没错!”,陀螺欣慰地看着招财。

“既然如此,我可以把我们的PaoMaChangV3改成抽象的,命名为PaoMaChangV4吧,让各个子公司继承这个类,然后为order()添加final关键字,禁止子类进行覆写,这样他们便只能用我们的下单逻辑了”,招财一遍思考一边说。

“那你打算怎么让子公司能自由控制各种产品呢?”,陀螺问道。

招财不慌不忙地回答:“我最近又研究了一下多态和继承,order()方法中的create()方法不做具体操作,将该方法延迟到子类中进行执行。”说罢,招财立刻写了如下代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
java复制代码/**
* 跑码场对象-版本4
* @author 蝉沐风
*/
public abstract class PaoMaChangV4 {

public final CatFood order(String flavor) {

CatFood catFood = create(flavor);

catFood.make();

return catFood;
}

//该方法需要子类继承
public abstract CatFood create(String flavor);

}

“order()方法只是调用了create()方法而已,是由子公司创建的子类负责具体实现create()方法,湖南分公司和山东分公司对应的代码如下”,招财接着解释道。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
java复制代码/**
* 湖南跑码场分公司V4
*
* @author 蝉沐风
*/
public class HuNanPaoMaChangV4 extends PaoMaChangV4 {

@Override
public CatFood create(String flavor) {
CatFood catFood;
if ("duojiao".equals(flavor)) {
catFood = new DuoJiaoCatFood();
} else if ("mala".equals(flavor)) {
catFood = new MaLaCatFood();
} else {
throw new RuntimeException("找不到该口味的猫粮");
}

return catFood;
}

}

/**
* 山东跑码场分公司V4
*
* @author 蝉沐风
*/
public class ShanDongPaoMaChangV4 extends PaoMaChangV4 {

@Override
public CatFood create(String flavor) {
CatFood catFood;
if ("congxiang".equals(flavor)) {
catFood = new CongXiangCatFood();
} else if ("dajiang".equals(flavor)) {
catFood = new DaJiangCatFood();
} else {
throw new RuntimeException("找不到该口味的猫粮");
}

return catFood;
}
}

对应的UML图为

工厂继承关系

最终顾客的下单方式变成了

1
2
3
4
5
6
7
java复制代码//下单剁椒猫粮
public static void main(String[] args) {
//顾客首先需要一个湖南分公司的对象
PaoMaChangV4 huNanPaoMaChangV4 = new HuNanPaoMaChangV4();
//然后下单
huNanPaoMaChangV4.order("duojiao");
}

“看来真是要对你刮目相看了,你刚刚总结出来的这种思想其实就是大名鼎鼎的工厂方法模式”,陀螺满意地笑了,“工厂方法模式通过让子类决定该创建的对象是什么,来达到将对象创建的过程封装的目的。”

工厂方法模式:定义一个创建对象的接口,担忧子类决定要实例化的类是哪一个,将类的实例化推迟到了子类。

“啊!”,招财大惊,没想到自己误打误撞研究出了工厂方法模式,“我其实并没有想这么多,只是单纯想解决当下的问题,适应未来的变化而已。”

“我知道,恐怕现在让你总结什么时候该用简单工厂模式,什么时候该用工厂方法模式你也未必说的准确。设计模式也不过是前人不断优化自己的代码总结出来的方法论。不必拘泥于你的优化方式叫什么名字,或者干脆忘掉我刚才说的术语吧,在合适的时机运用合适的方法来解决问题才是最重要的!不要学习了设计模式,就觉得自己手上握着锤子,然后看什么都是钉子。”

“我明白了师傅!但是我听说还有一种关于工厂的设计模式,你要不顺便给我讲讲吧。”

猫粮原材料的工厂

“还有一种叫抽象工厂模式,如果你明白了我们系统的一步步优化,这个模式对你来说就太简单了。还是用我们公司的场景给你举例子吧。”

“假如我们想进一步控制分公司生产猫粮的原料,避免每个分公司的原料质量参差不齐。制作猫粮的主要原料都是一样的,都需要肉、燕麦、果蔬、牛磺酸等,但是不同的分公司又有不同的原料生产工艺,抽象工厂就适合于这种场景。”

“那该怎么进行设计呢?”

“这个简单啊,我们可以为每一个分公司创建一个原料工厂,这个原料工厂必须符合我们制定的标准,像这样”,招财写下了伪代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
java复制代码public interface CatFoodIngredientAbstractFactory {
// 肉类生产
Meat createMeat();

// 燕麦生产
Oats createOats();

// 果蔬生产
FruitsAndVegetables createFruitsAndVegetables();

// 牛磺酸生产
Taurine createTaurine();
}

“各分公司自己的原料厂必须实现CatFoodIngredientFactory来实现每一个创造方法,以山东分公司为例。”

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
java复制代码/**
* 山东分公司猫粮原料厂
*
* @author 蝉沐风
*/
public class ShanDongCatFoodIngredientFactory implements CatFoodIngredientAbstractFactory {
@Override
public Meat createMeat() {
return new ShanDongMeat();
}

@Override
public Oats createOats() {
return new ShanDongOats();
}

@Override
public FruitsAndVegetables createFruitsAndVegetables() {
return new ShanDongFruitsAndVegetables();
}

@Override
public Taurine createTaurine() {
return new ShanDongTaurine();
}
}

注:代码中有很多类未给出实现,大家只需理解其中的含义即可

招财继续问道:“现在怎么把各个分公司的原料工厂和猫粮联系起来呢?”

“别急,为了更好的解释抽象工厂,我们需要先改变一下我们的CatFood类。这里只是为了单纯讲解抽象工厂模式而进行的更改,和我们自身的业务逻辑已经没有关系了。”

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
java复制代码/**
* 猫粮的抽象类,所有具体口味的猫粮必须继承自该接口
*
* @author 蝉沐风
*/
public abstract class CatFoodV2 {
public String flavor;

public Meat meat;
public Oats oats;
public FruitsAndVegetables fruitsAndVegetables;
public Taurine taurine;


public abstract void make();
}

“接下来的重点就是如何创建具体口味的猫粮了。你觉得怎么让猫粮和原料厂关联起来呢?”

“可以在子类中添加一个原料工厂的对象,猫粮产品对象的时候可以选择某个原料厂进行初始化,这样就实现了猫粮和具体原料之间的解耦,猫粮类只需要知道怎么制作就可以了,比如像这个样子。”

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
java复制代码/**
* 大酱猫粮
*/
public class DaJiangCatFoodV2 extends CatFoodV2 {

private CatFoodIngredientFactory catFoodIngredientFactory;

public DaJiangCatFoodV2(CatFoodIngredientFactory catFoodIngredientFactory) {
this.flavor = "dajiang";
this.catFoodIngredientFactory = catFoodIngredientFactory;
}

@Override
public void make() {
System.out.println("正在使用原料:");
System.out.println("肉:" + catFoodIngredientFactory.createMeat());
System.out.println("燕麦:" + catFoodIngredientFactory.createOats());
System.out.println("果蔬:" + catFoodIngredientFactory.createFruitsAndVegetables());
System.out.println("牛磺酸:" + catFoodIngredientFactory.createTaurine());
System.out.println("制作【大酱】口味猫粮");
}
}

“孺子可教”,陀螺欣慰地说道,“你已经掌握的面向对象的精髓了,那么分公司的代码你也可以写出来了,试试看吧。”

招财很快写出了代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
java复制代码/**
* 跑码场对象-版本5
*
* @author 蝉沐风
*/
public abstract class PaoMaChangV5 {

public final CatFoodV2 order(String flavor) {

CatFoodV2 catFood = create(flavor);

catFood.make();

return catFood;
}

//该方法需要子类继承
public abstract CatFoodV2 create(String flavor);

}

/**
* 山东跑码场分公司V5
*
* @author 蝉沐风
*/
public class ShanDongPaoMaChangV5 extends PaoMaChangV5 {
//山东分公司采用山东原料厂的原料
CatFoodIngredientFactory catFoodIngredientFactory = new ShanDongCatFoodIngredientFactory();

@Override
public CatFoodV2 create(String flavor) {
CatFoodV2 catFood;
if ("congxiang".equals(flavor)) {
catFood = new CongXiangCatFoodV2(catFoodIngredientFactory);
} else if ("dajiang".equals(flavor)) {
catFood = new DaJiangCatFoodV2(catFoodIngredientFactory);
} else {
throw new RuntimeException("找不到该口味的猫粮");
}

return catFood;
}
}

“到此为止,我们就用抽象工厂模式完成了业务的改造,顾客下单的逻辑并没有发生变化。为了完整性,我们给出抽象工厂的定义”,陀螺说道。

抽象工厂模式:提供接口,用来创建相关或依赖对象的家族,而不需要明确制定具体类。

招财郁闷地说:“你让我自己写我觉得自己能写出来,你解释这么多,我反而头大了!”

“哈哈哈哈哈哈,学习有三种境界,第一种:看山是山,看水是水;第二种:看山不是山,看水不是水;第三种:看山依然山,看水依然水。你现在就处于第一种向第二种过度的阶段”,陀螺打趣道。

“我们从头捋一遍我们系统升级的过程,帮助你理解。”

总结

“刚开始我们公司只生产一种产品——鱼香猫粮,这时你直接针对该产品创建类FishCatFood进行业务逻辑编写即可,不需要进行任何优化。”

“后来公司相继生产了其他两种产品,鉴于每种产品产品的相关性,你创建了CatFood抽象类,之后生产的每种产品都需要继承这个类,然后在order()方法中根据用户传入的口味制作相应的产品。但是随着公司的发展,产品可能会一改再改(急剧增加或下架),order()方法不再满足开闭原则,因此我们将创建对象的代码抽离到SimpleCatFoodFactory中进行统一管理,这就是简单工厂。”

简单工厂类图

“后来公司相继在其他省份创建了子公司,每个子公司都有自己的产品,为了避免SimpleCatFoodFactory成为万能工厂,我们为每个分公司创建了独立的简单工厂,按照我们的要求来创建产品对象。”

“我们并不想让子公司能够修改order()的中的逻辑,因此我们试图创建一个‘框架’,强制让子公司使用我们的下单逻辑,同时又保证子公司自由创建产品的灵活性。于是我们在PaoMaChangV4抽象类中使用了抽象的create()方法,我们将实现create()的行为延迟到子类中,父类中制定了基本框架。这一步使得order()不依赖于具体类,换句话说,这就是解耦。当order()方法调用create()方法是,PaoMaChangV4的子类(子公司对象)将负责创建真正的产品。这就是工厂方法模式。”

“最后我们想确保对每个子公司每个产品原料的控制,定义了原料族。这里有一个隐含的假设,每个产品所使用的原料都是相同的,区别是生产方式不同。”

原料家族

“我们创建了原料工厂CatFoodIngredientAbstractFactory接口,该接口定义了创建所有原料的接口,再看一下代码。”

1
2
3
4
5
6
7
8
9
10
11
12
13
java复制代码public interface CatFoodIngredientAbstractFactory {
// 肉类生产
Meat createMeat();

// 燕麦生产
Oats createOats();

// 果蔬生产
FruitsAndVegetables createFruitsAndVegetables();

// 牛磺酸生产
Taurine createTaurine();
}

“接下来我们为每个分公司创建了实现了CatFoodIngredientAbstractFactory接口的子类来实现每一个创建方法。为了更恰当地解释抽象工厂模式,我们又稍微改造了一下猫粮类,得到了CatFoodV2,所有的具体产品依然继承自CatFoodV2,不同的每个产品都需要从构造器中得到一个原料工厂,注入到对象中的catFoodIngredientFactory变量,CatFoodV2中的make()方法会使用到该工厂创建的原料。”

“最后总结一下抽象工厂模式的使用场景,当你需要使用原料家族来创建想要制造的产品的时候,你就可以考虑使用抽象工厂模式了。”


我是蝉沐风,一个让你沉迷于技术的讲述者,欢迎大家留言!

本文转载自: 掘金

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

Spring Cloud / Alibaba 微服务架构

发表于 2021-11-03

这是我参与11月更文挑战的第3天,活动详情查看:2021最后一次更文挑战。
从这篇开始,我们将一起来搭建电商项目~

创建微服务

1、创建一个maven项目

PNG图像.png
选择maven项目,我使用的是jdk8,next。

PNG图像.png
为自己的项目取个名称就完成创建了!

pom文件就不需要一个个字去敲了,可以直接复制我上传的文件。注意groupId和artifactId,如果项目名和我取不同的记得自行修改。贴进去后记得刷新一下maven。

Tips:

1)groupId标签是项目组织唯一的标识符,比如项目叫test,那么groupId应该是com.xxx.test,即域名.公司名.项目名。

image.png

2)artifactId标签是项目的唯一标识符,一般是项目名-xxx,比如test-model。

3)项目的打包类型:pom、jar、war,packaging标签用来指定打包类型,默认是jar类型。由于我们搭建的是一个多模块工程,里面包含多个模块,是给其它子模块提供pom依赖的,所以我们的打包类型应该是pom,而不是jar。

4)由于我们是一个Spring Cloud工程,它是从Spring Boot开始去开发的,所以parent标签是Spring Boot,版本是目前企业级开发最流行的2.3.1.RELEASE版本。

image.png

5)properties标签里配置了spring-cloud的版本以及spring-cloud-alibab的版本,alibaba需要适配于spring cloud,两者的版本有相互之间的依赖关系,如果你不确定自己选择的两个版本是否适配,可以使用我项目中的版本或者查阅官方文档,否则可能会出现一些意想不到的错误且很难排查。

image.png

6)dependencies标签代表依赖,对于我们这个父工程来说,继承的子工程都会自动去用父工程这边定义的依赖,所以我们把通用的依赖都放在我们这个父工程下。(下一篇文章内容中会来逐个介绍pom文件中的各个依赖分别是什么)

7)dependencyManagement标签是项目依赖管理,父项目只是声明依赖,子项目需要写明需要的依赖(可以省略版本信息)。此处的version就是前面我们在properties标签中配置的版本。

image.png

8)最后配置了一个远程仓库,repositories标签。虽然在maven中可能已经配置了仓库,但可能里面不包含有我们需要的一些依赖,所以我们需要在这里做个声明,配置一个远程仓库,如果从maven配置仓库中找不到,则从我们工程中配置的这个远程仓库中去寻找依赖。

image.png

至此我们就完成了对pom文件的创建和初步编写。

剩下的就下一篇再见吧!如果有写的不对的地方也请大家指出,谢谢!

本文转载自: 掘金

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

RuoYi-Vue 前后端分离版代码浅析-Configur

发表于 2021-11-03

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

前言

本节介绍RuoYi-Vue的ruoyi-admin模块中的系统主页模块SysIndexController 部分的代码,这个接口中有意思的是RuoYiConfig 这个类,这是一个系统配置类,里面的一个注解ConfigurationProperties很有意思。

获取系统配置

我们平时使用application.yml中的属性都是使用的@Value,通过这种方式来拿到对应的配置文件中储存的属性

1
2
java复制代码    @Value("${ruoyi.name}")
private String name ;

它是在org.springframework.beans.factory.annotation这里的,在@Value注解下,我们需要将整个的属性名都写出来才可以使用这个属性,而我们接下来介绍的ConfigurationProperties则省了我们一部分工作。

ConfigurationProperties

在class上使用

在这里这个注解是这么使用的

1
2
3
4
5
6
7
8
9
10
11
java复制代码@Component
@ConfigurationProperties(prefix = "ruoyi")
public class RuoYiConfig {
/**
* 项目名称
*/
private String name;
/**
* 版本
*/
private String version;

和@Value不同,它是在org.springframework.boot.context.properties中的,通过指定前缀,我们就可以直接使用ruoyi下的各种属性。

1
2
3
4
5
6
yml复制代码# 项目相关配置
ruoyi:
# 名称
name: RuoYi
# 版本
version: 3.7.0

甚至在类中都不需要指定对应的属性二级名称,只要保证名称一一对应即可,相较于使用@Value只能注入单值,@ConfigurationProperties 非常适合这种批量属性注入的情况,不过@ConfigurationProperties不支持SpEL表达式,这里需要注意下。

在方法上使用

比较常见的就是在数据库主从或者读写分离时使用,Ruoyi中的数据库主从这里就用到了@ConfigurationProperties这个注解。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
java复制代码
@Configuration
public class DruidConfig
{
@Bean
@ConfigurationProperties("spring.datasource.druid.master")
public DataSource masterDataSource(DruidProperties druidProperties)
{
DruidDataSource dataSource = DruidDataSourceBuilder.create().build();
return druidProperties.dataSource(dataSource);
}

@Bean
@ConfigurationProperties("spring.datasource.druid.slave")
@ConditionalOnProperty(prefix = "spring.datasource.druid.slave", name = "enabled", havingValue = "true")
public DataSource slaveDataSource(DruidProperties druidProperties)
{
DruidDataSource dataSource = DruidDataSourceBuilder.create().build();
return druidProperties.dataSource(dataSource);
}

image.png
需要注意的是,如上图红线所示,当将该注解作用于方法上时,如果想要有效的绑定配置,那么该方法需要有@Bean注解且所属的Class需要有@Configuration注解。

本文转载自: 掘金

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

ZooKeeper核心概念精讲,通俗易懂,加薪必备

发表于 2021-11-03

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

前言

通过前面两个章节的学习,相信小伙伴们对Zookeeper有了一个初步的认识,今天就和大家一起学习Zookeeper的核心部分,比如我们能够创建的节点类型有哪些?znode中的参数都表示什么?监听机制是什么?zk有哪些特性?

这一系列的问题在今天的文章中,花哥都会逐个讲解,还没有学习前两章内容的小伙伴,点击下面这个直通车可以学习一下哦。

Zookeeper大白话入门,真的很通俗易懂

Zookeeper单机部署、必备命令、场景实战

会话(session)

我们先来看下面这个Zookeeper的系统架构图,可以看到服务器(Server)可以接受多个客户端(Client)的连接,而每一个客户端连接到服务器,就会生成一个会话,zk会生成一个唯一会话id。

有一点需要注意,leader是不允许客户端连接的,除非leaderServes 参数被显示设置。

为了保持客户端与服务器的会话有效,客户端会以特定的时间频率发送心跳,如果服务器超过会话超时时长(默认为2倍tickTime)还没有收到客户端的心跳,就会将该服务器判断为挂了。

image.png

  • 客户端连接时生成的会话id

image.png

客户端会维护一个TCP连接,后续通过这个连接能够发送请求,接收服务器响应、获取监听事件等。

Znode节点类型

前面文章我们学到,Zookeeper是由一个个节点(Znode)组成,而Znode又包含多种类型,下面花哥一一列出。

节点类型 举例 描述
持久节点 create /testData 100 一旦创建,就会持久化
顺序节点 ①:create -s /testData/name huage ②:create -s /testData/ name zk会在路径后追加10位的数字,如name40000000007,数字由1开始,逐步递增,最大值为2^32-1
临时节点 create -e /testData 100 客户端断开后,会被删除
临时顺序节点 create -e -s /testData/age 18 临时节点+顺序节点的组合

每个节点最多可以存放1M的数据。

Znode数据构成

知道了有几种节点类型,接下来我们就来看看每一个节点的组成。

每一个Znode由四个部分组成:存储数据(data)、访问权限(acl)、子节点引用(children)、状态信息(stat)

名称 描述
data 存储的业务数据
acl 客户端对Znode的访问权限
children 当前节点的子节点引用
stat 包含Znode节点的状态信息,如事务id、版本号等

上面四种较为复杂的是节点元数据(stat),先通过get命令查看节点信息,可以看到除了该节点的值以外,还包含了很多其他参数。

image.png

上面这些参数就是元数据,下面直接贴出表格,大家务必要一一了解。

名称 描述
cZxid 创建该节点的事务id
mZxid 最后修改该节点的事务id
pZxid znode最后更新的子节点事务id
ctime 该节点创建时间
mtime 该节点最后修改时间
cversion 该节点的子节点版本号,初始值为-1,每次子节点修改,该值会自动增加
dataVersion 该节点被修改的次数,初始版本为0,每对该节点的数据进行操作,都会自动增加
aclVersion 当前节点的acl权限版本号
ephemeraOwner 临时节点的拥有者会话id,如果不是临时节点,默认为0
dataLength 该节点数据长度
numChildren 子节点数

如果觉得看表格太恶心…….,花哥贴心的演示一波。

首先新建一个节点,使用get查看节点的数据。

image.png

我们直接打开另一个客户端,将这个节点修改一下,是不是发现最后修改的事务id(mZxid)、最后修改时间(mtime)、修改次数(dataVersion)都已经发生了变化。

image.png

如果对子节点操作的话,会有什么变化呢,一个小问题希望小伙伴么自己动手试一下。

watch监听机制

昨天的文章中,我们也用实例演示了watch的用法,今天拿出来再重点讲解一下。

下面这个是通过【get -w 节点路径】来监听该节点,只要该节点被修改,在监听端的客户端就会收到消息通知。

执行结果8.gif

同样也可以使用ls -w来监听节点的新增或删除变化。

执行结果9.gif

在命令行我们可以通过上面这种方式来监听节点,那在java中我们也可以用getData() 、getChildren() 、exists() 来实现监听事件。

Watch有以下两个特性:

  1. 一次性触发:watch在触发之后,就会被删除,如果想要持续收到消息通知,就需要持续设置watch;
  2. 有序性:客户端先收到watch通知,然后才会查看到节点的变化。

有一点需要提出,由于watch是一次性触发,在接收到一次变更通知后,当再次发起watch请求时,会有一定延迟,如果此时节点再次变化,那就会丢失这次的变更。

ZooKeeper的特性

最后,我们来看一下ZooKeeper有哪些特性呢

  • 顺序一致性:Leader服务器会根据请求顺序生成事务id,保证客户端操作是按顺序生效的;
  • 原子性:所有事务请求的处理结果要么成功、要么失败,没有部分结果;
  • 单一系统映像:无论客户端连到哪一个服务器,看到的数据都是一致的;
  • 可靠性:数据一旦变更就不会丢失;
  • 实时性:保证客户端当时读取的数据是最新的。

写到最后

今天讲了不少内容,是时候展现真的技术了,明天我们就可以看一下Zookeeper的实际应用场景,了解学这个玩意到底有什么用,用在哪里。文中如果有哪些错误,希望小伙伴们指出,共同学习哦。

本文转载自: 掘金

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

Spring 与策略模式双剑合壁,让你彻底的消灭冗余的if

发表于 2021-11-03

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

实际业务场景

最近在项目中需要开发消息发送平台,需要根据 type 值判断来进行相应的业务处理以及消息发送方式,这样就涉及到重复的 if-else 的问题,为了解决这样重复的代码来造成的代码冗余的问题,所以考虑使用策略模式,根据具体的场景执行对应的策略,来解决问题。本文简单写一个测试 Demo,实际开发需要根据具体业务,具体实现对应的业务逻辑。

具体实现

当我们遇到这样的逻辑处理时,第一反应是针对 type 进行 if…else… 或者是 switch 的逻辑判断,从而区分不同业务逻辑处理。

基于 if…else… 的伪代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
java复制代码public int sendMessage(int type, Message message) {
if (type == 1) {
// 省略业务处理
return mail.sendMessage(message); // 进行邮件发送
} else if (type == 2) {
// 省略业务处理
return mobile.sendMessage(message); // 进行手机短信发送
} else if (type == 3) {
// 省略业务处理
return app.sendSendMessage(message); // 进行 app 消息推送
} else if (type == 4){
// ...
}
//...
}

假设上面的代码是用来对消息进行多渠道的发送,根据不同 type 来进行发送方式的选择。当然真实的业务场景不可能是这么简单的判断。

首先对照一下设计模式的开闭原则:面对扩展开放,面对修改关闭。

上述代码,如果某个发送方式改变了,那么这段代码就要进行修改,或者如果新增了一个发送方式,这段代码同样需要修改。一旦修改必然会影响到其他方式的业务逻辑。完全不符合开闭原则,同时代码中还充斥着大量的 if…else…,如果业务复杂,代码会急速膨胀。

那么,下面我们就针对以上实例,用策略模式来进行重新设计。

基于策略模式的伪代码

首先定义一个发送消息的接口 ISendMessageStrategy。实战过程中可根据具体情况采用接口或抽象类。

1
2
3
4
5
6
7
8
9
java复制代码public interface ISendMessageStrategy {

/**
* 发送消息
*
* @param message 发送的消息内容
*/
void sendMessage(Message message);
}

在接口中提供一个方法,也就是发送消息的方法。这里因为是接口,所以定义的方法就需要子类必须实现。下面便是针对此接口的具体实现,不同的发送方式有不同的实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
java复制代码/**
* 邮件发送的实现
**/
@Slf4j
public class EmailSendMessageStrategy implemets ISendMessageStrategy {
@Override
public void sendMessage(Message message) {
// 省略业务处理
mail.sendMessage(message);
}
}

/**
* 短信发送的实现
**/
public class MobileSendMessageStrategy implemets ISendMessageStrategy {
@Override
public void sendMessage(Message message) {
// 省略业务处理
phone.sendMessage(message);
}
}

我们来实现一个持有接口 ISendMessageStrategy 的角色类 SendMessageHandler

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

/**
* 持有策略抽象类
*/
private ISendMessageStrategy sendMessageStrategy;

// 通过构造方法注入,也可以通过其他方式注入
public SendMessageHandler(ISendMessageStrategy sendMessageStrategy) {
this.sendMessageStrategy = sendMessageStrategy;
}

public void sendMessage(Message message) {
return sendMessageStrategy.sendMessage(message);
}

}

最后,我们来看一下如何调用该策略类

1
2
3
4
5
6
java复制代码public class Test {
public static void main(String[] args) {
SendMessageHandler handler = new SendMessageHandler(new MobileSendMessageStrategy());
handler.sendMessage(new Message());
}
}

使用 Spring 来管理对象

上面的改进已经避免了大量的 if… else…,此时如果项目使用的是 Spring 项目,我们再进一步改进, 此时主要利用 Spring 的 @Autowired 注解来将实例化的策略实现类注入到一个 Map 当中,然后通过 key 可以方便的拿到服务。

首先将策略实现类通过@Service 注解进行实例化,并指定实例化的名称。以短信发送的实现为例:

1
2
3
4
kotlin复制代码@Component("mobileSendMessageStrategy")
public class MobileSendMessageStrategy implemets ISendMessageStrategy {
// ...
}

其他策略实现类与上相同,依次实例化。最后改造环境角色类为 SendMessageHandler:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
java复制代码@Component
public class SendMessageHandler implements ApplicationContextAware {

private final Map<String, ISendMessageStrategy> strategyMap = new ConcurrentHashMap<>();

public ISendMessageStrategy get(String beanName) {
return strategyMap.get(beanName);
}

@Override
public void setApplicationContext(ApplicationContext context) throws Exception {
strategyMap = applicationContext.getBeansOfType(ISendMessageStrategy.class);
}
}

applicationContext.getBeansOfType(ISendMessageStrategy.class) 会将容器中 ISendMessageStrategy 的实现类(注解了 @Component)放到该 map 中。其中 key 就是 @Component 中指定的实例化服务的名称,value 便是对应的对象。

1
2
3
4
5
6
7
8
java复制代码public class Test {
@Resource
private SendMessageHandler messageHandler;

public void test() {
messageHandler.get("mobileSendMessageStrategy").sendMessage(new Message());
}
}

借助枚举类进一步优化

我们再进一步改进。我们不再在策略角色类中调用策略类的方法了,只让策略角色类作为工厂的角色,返回对应的服务。而相关服务方法的调用由客户端直接调用实现类的方法。

同时,针对服务的名称和类型我们通过枚举进行映射。先来定义一个枚举类:

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
java复制代码public enum TypeEnum {

MAIL(0, "mailSendMessageStrategy", "邮件"),
MOBILE(1, "mobileSendMessageStrategy", "短信"),
APP(2, "appSendMessageStrategy", "app");

TypeEnum(int type, String serviceName, String desc) {
this.type = type;
this.serviceName = serviceName;
this.desc = desc;
}

public static TypeEnum valueOf(int type) {
for (TypeEnum typeEnum : TypeEnum.values()) {
if (typeEnum.getType() == type) {
return typeEnum;
}
}
return null;
}

private int type;

private String serviceName;

private String desc;

public int getType() {
return type;
}

public String getServiceName() {
return serviceName;
}

public String getDesc() {
return desc;
}
}

然后改造环境角色类为 SendMessageHandler:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
java复制代码@Component
public class SendMessageHandler implements ApplicationContextAware {

private final Map<String, ISendMessageStrategy> strategyMap = new ConcurrentHashMap<>();

public ISendMessageStrategy get(int type) {
TypeEnum typeEnum = TypeEnum.valueOf(type);
return strategyMap.get(typeEnum.getServiceName());
}

@Override
public void setApplicationContext(ApplicationContext context) throws Exception {
strategyMap = applicationContext.getBeansOfType(ISendMessageStrategy.class);
}
}

测试一下

1
2
3
4
5
6
7
8
9
java复制代码public class Test {
@Resource
private SendMessageHandler messageHandler;

public void test() {
int type = 1;
messageHandler.get(type).sendMessage(new Message());
}
}

此时,如果新添加算法,只用创建对应算法的服务,然后在枚举类中映射一下关系,便可在不影响客户端调用的情况进行扩展。当然,根据具体的业务场景还可以进行进一步的改造。

总结

设计模式可以可以更好的扩展代码,但是一定程度上也加大了代码的阅读成本。所以在实际的项目中,不要一味的追求设计模式,要结合实际的业务情况进行合理的选择。当判断项固定且较少时,if…else… 也是一种更高效且便于维护的方式。

本文转载自: 掘金

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

1…424425426…956

开发者博客

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