数据专栏

智能大数据搬运工,你想要的我们都有

科技资讯:

科技学院:

科技百科:

科技书籍:

网站大全:

软件大全:

value值为Map集合和对象 当YAML配置文件中配置的属性值为Map集合或对象类型时,YAML配置文件格式同样可以分为两种书写方式:缩进式写法和行内式写法。 其中,缩进式写法的示例代码如下 ```yaml person: map: k1: v1 k2: v2 ``` 对应的行内式写法示例代码如下 ```yaml person: map: {k1: v1,k2: v2} ``` 在YAML配置文件中,配置的属性值为Map集合或对象类型时,缩进式写法的形式按照YAML文件格式编写即可,而行内式写法的属性值要用大括号“{}”包含。 学习让人快乐,学习更让人觉得无知!学了1个多月的《Java工程师高薪训练营》,才发现自己对每个技术点的认知都很肤浅,根本深不下去,立个Flag:每天坚持学习一小时,一周回答网上3个技术问题,把自己知道都分享出来。
软件开发
2020-06-17 15:37:00
reStructuredText ( RST 、 ReST 或 reST )是一种用于文本数据的文件格式,主要用于 Python 编程语言社区的技术文档。
它是Python Doc-SIG(Documentation Special Interest Group)的 Docutils 项目的一部分,旨在为 Python 创建一组类似于 Java 的 Javadoc 或 Perl 的 Plain Old Documentation(pod)的工具。Docutils 可以从 Python 程序中提取注释和信息,并将它们格式化为各种形式的程序文档。
从这个意义上说,reStructuredText 是一种轻量级标记语言,其设计目的是(a)文档处理软件(如Docutils)可以处理它,(b)读和写 Python 源代码的程序员很容易读它。
reST 解析器的引用实现是 Python 编程语言中的 Docutils 文本处理框架的一个组件,但是还可以使用其他解析器。
没有正式的 mime 类型注册为 reStructuredText,但非官方的是 text/x-rst
可以将 RST 文件理解为 Python 使用的 Markup 文件就可以了。
官方的使用手册,请参考链接: https://docutils.sourceforge.io/docs/user/rst/quickstart.html
目前还有一个在线的编辑环境,请参考 http://rst.ninjs.org/#Kml0YWxpY3Mq 上面的内。
你可以在上面的链接中对标记文件进行编辑。
在编辑的过程中,你可能还需要对 Markup 的语法有所了解。

https://www.ossez.com/t/python-rst/177
软件开发
2020-06-17 01:30:00
>作者 谢恩铭,公众号「程序员联盟」(微信号:coderhub)。 转载请注明出处。 原文: https://www.jianshu.com/p/4adb95073745
> 《C语言探索之旅》 全系列
内容简介 前言 文件的打开和关闭 读写文件的不同方法 在文件中移动 文件的重命名和删除 第二部分第八课预告
1. 前言
上一课 C语言探索之旅 | 第二部分第六课:创建你自己的变量类型 之后,我们来学习很常用的文件读写。
我们学过了这么多变量的知识,已经知道变量实在是很强大的,可以帮助我们实现很多事情。
变量固然强大,还是有缺陷的,最大的缺陷就是: 不能永久保存 。
因为 C语言的变量储存在内存中,在你的程序退出时就被清除了,下次程序启动时就不能找回那个值了。
>“蓦然回首,那人不在灯火阑珊处...”
>“今天的你我, 怎样重复昨天的故事? 这一张旧船票, 还能否登上你的破船?”
>不能够啊,“涛声不能依旧”啊...
如果这样的话,我们如何在 C语言编写的游戏中保存游戏的最高分呢?怎么用 C语言写一个退出时依然保存文本的文本编辑器呢?
幸好,在 C语言中我们可以读写文件。这些文件会储存在我们电脑的硬盘上,就不会在程序退出或电脑关闭时被清除了。
为了实现文件读写,我们就要用到迄今为止我们所学过的知识:
指针,结构体,字符串,等等。
也算是复习吧。
2. 文件的打开和关闭
为了读写文件,我们需要用到定义在 stdio.h 这个标准库头文件中的一些函数,结构,等。
是的,就是我们所熟知的 stdio.h,我们的“老朋友” printf 和 scanf 函数也是定义在这个头文件里。
下面按顺序列出我们打开一个文件,进行读或写操作所必须遵循的一个流程: 调用“文件打开”函数 fopen(f 是 file(表示“文件”)的首字母;open 表示“打开”),返回一个指向该文件的指针。 检测文件打开是否成功,通过第 1 步中 fopen 的返回值(文件指针)来判断。如果指针为 NULL,则表示打开失败,我们需要停止操作,并且返回一个错误。 如果文件打开成功(指针不为 NULL),那么我们就可以接着用 stdio.h 中的函数来读写文件了。 一旦我们完成了读写操作,我们就要关闭文件,用 fclose(close 表示“关闭”)函数。
首先我们来学习如何使用 fopen 和 fclose 函数,之后我们再学习如何读写文件。
fopen:打开文件
函数 fopen 的原型是这样的: FILE* fopen(const char* fileName, const char* openMode);
不难看出,这个函数接收两个参数: fileName:文件名(name 表示“名字”)。是一个字符串类型,而且是 const,意味着不能改变其值。 openMode:打开方式(open 表示“打开”,mode 表示“方式”)。表明我们打开文件之后要干什么的一个指标。只读、只写、读写,等等。
这个函数的返回值,是 FILE * ,也就是一个 FILE(file 表示“文件”)指针。
FILE 定义在 stdio.h 中。有兴趣的读者可以自己去找一下 FILE 的定义。
我们给出 FILE 的一般定义: typedef struct { char *fpos; /* Current position of file pointer (absolute address) */ void *base; /* Pointer to the base of the file */ unsigned short handle; /* File handle */ short flags; /* Flags (see FileFlags) */ short unget; /* 1-byte buffer for ungetc (b15=1 if non-empty) */ unsigned long alloc; /* Number of currently allocated bytes for the file */ unsigned short buffincrement; /* Number of bytes allocated at once */ } FILE;
可以看到 FILE 是一个结构体(struct),里面有 7 个变量。当然我们不必深究 FILE 的定义,只要会使用 FILE 就好了,而且不同操作系统对于 FILE 的定义不尽相同。
细心的读者也许会问:“之前不是说结构体的名称最好是首字母大写么,为什么 FILE 这个结构体每一个字母都是大写呢?怎么和常量的命名方式一样呢?”
好问题。其实我们之前建议的命名方式(对于结构体,首字母大写,例如:StructName)只是一个“规范”(虽然大多数程序员都喜欢遵循),并不是一个强制要求。
这只能说明编写 stdio.h 的前辈并不一定遵循这个“规范”而已。当然,这对我们并没什么影响。
以下列出几种可供使用的 openMode : r :只读。r 是 read(表示“读”)的首字母。这个模式下,我们只能读文件,而不能对文件写入。文件必须已经存在。 w :只写。w 是 write(表示“写”)的首字母。这个模式下,只能写入,不能读出文件的内容。如果文件不存在,将会被创建。 a :追加。a 是 append(表示“追加”)的首字母。这个模式下,从文件的末尾开始写入。如果文件不存在,将会被创建。 r+ :读和写。这个模式下,可以读和写文件,但文件也必须已经存在。 w+ :读和写。预先会删除文件内容。这个模式下,如果文件存在且内容不为空,则内容首先会被清空。如果文件不存在,将会被创建。 a+ :读写追加。这个模式下,读写文件都是从文件末尾开始。如果文件不存在,将会被创建。
上面所列的模式,其实还可以组合上 b 这个模式。b 是 binary 的缩写,表示“二进制”。 对于上面的每一个模式,如果你添加 b 后,会变成 rb , wb , ab , rb+ , wb+ , ab+ ),该文件就会以二进制模式打开。不过二进制的模式一般不是那么常用。
一般来说, r , w 和 r+ 用得比较多。 w+ 模式要慎用,因为它会首先清空文件内容。当你需要往文件中添加内容时, a 模式会很有用。
下面的例子程序就以 r+ (读写)的模式打开文件: #include int main(int argc, char *argv[]) { FILE* file = NULL; file = fopen("test.txt", "r+"); return 0; }
于是,file 成为了指向 test.txt 文件的一个指针。
你会问:“我们的 test.txt 文件位于哪里呢?”
text.txt 文件和可执行文件位于同一目录下。
“文件一定要是 .txt 结尾的吗?”
不是,完全由你决定文件的后缀名。你大可以创建一个文件叫做 xxx.level,用于记录游戏的关卡信息。
“文件一定要和可执行文件在同一个文件夹下么?”
也不是。理论上可以位于当前系统的任意文件夹里,只要在 fopen 函数的文件名参数里指定文件的路径就好了,例如: file = fopen("folder/test.txt", "w");
这样,文件 test.txt 就是位于当前目录的文件夹 folder 里。这里的 folder/test.txt 称为“相对路径”。
我们也可以这样: file = fopen("/home/user/folder/test.txt", "w");
这里的 /home/user/folder/test.txt 是“绝对路径”。
测试打开文件
在调用 fopen 函数尝试打开文件后,我们需要检测 fopen 的返回值,以判断打开是否成功。
检测方法也很简单:如果 fopen 的返回值为 NULL,那么打开失败;如果不为 NULL,那么表示打开成功。示例如下: #include int main(int argc, char *argv[]) { FILE* file = NULL; file = fopen("test.txt", "r+"); if (file != NULL) { // 读写文件 } else { // 显示一个错误提示信息 printf("无法打开 test.txt 文件\n"); } return 0; }
记得每次使用 fopen 函数时都要对返回值作判断,因为如果文件不存在或者正被其他程序占用,那可能会使当前程序运行失败。
fclose:关闭文件
close 表示“关闭”。
如果我们成功地打开了一个文件,那么我们就可以对文件进行读写了(读写的操作我们下一节再详述)。
如果我们对文件的操作已经结束,那么我们应该关闭这个文件,这样做是为了释放占用的文件指针。
我们需要调用 fclose 函数来实现文件的关闭,这个函数可以释放内存,也就是从内存中删除你的文件(指针)。
函数原型: int fclose(FILE* pointerOnFile);
这个函数只有一个参数:指向文件的指针。
函数的返回值(int)有两种情况: 0 :当关闭操作成功时。 EOF(是 End Of File 的缩写,表示“文件结束”。一般等于 -1):如果关闭失败。
示例如下: #include int main(int argc, char *argv[]) { FILE* file = NULL; file = fopen("test.txt", "r+"); if (file != NULL) { // 读写文件 // ... fclose(file); // 关闭我们之前打开的文件 } return 0; }
3. 读写文件的不同方法
现在,我们既然已经知道怎么打开和关闭文件了,接下来我们就学习如何对文件进行读出和写入吧。
我们首先学习如何写入文件(相比读出要简单一些),之后我们再看如何从文件读出。
对文件写入
用于写入文件的函数有好几个,我们可以根据情况选择最适合的函数来使用。
我们来学习三个用于文件写入的函数: fputc:在文件中写入一个字符(一次只写一个)。是 file put character 的缩写。put 表示“放入”,character 表示“字符”。 fputs:在文件中写入一个字符串。是 file put string 的缩写。string 表示“字符串”。 fprintf:在文件中写入一个格式化过的字符串,用法与 printf 是几乎相同的,只是多了一个文件指针。
fputc
此函数用于在文件中一次写入一个字符。
函数原型: int fputc(int character, FILE* pointerOnFile);
这个函数包含两个参数: character:int 型变量,表示要写入的字符。我们也可以直接写 'A' 这样的形式,之前 ASCII 那节的知识点没有忘吧。 pointerOnFile:指向文件的指针。
函数返回 int 值。如果写入失败,则为 EOF;否则,会是另一个值。
示例: #include int main(int argc, char *argv[]) { FILE* file = NULL; file = fopen("test.txt", "w"); if (file != NULL) { fputc('A', file); // 写入字符 A fclose(file); } return 0; }
上面的程序用于向 test.txt 文件写入字符 'A'。
fputs
这个函数和 fputc 类似,区别是 fputc 每次是写入一个字符,而 fputs 每次写入一个字符串。
函数原型: int fputs(const char* string, FILE* pointerOnFile);
类似地,这个函数也接受两个参数: string:要写入的字符串。 pointerOnFile:指向文件的指针。
如果出错,函数返回 EOF;否则,返回不同于 EOF 的值。
示例: #include int main(int argc, char *argv[]) { FILE* file = NULL; file = fopen("test.txt", "w"); if (file != NULL) { fputs("你好,朋友。\n最近怎么样?", file); fclose(file); } return 0; }
fprintf
这个函数很有用,因为它不仅可以向文件写入字符串,而且这个字符串是可以由我们来格式化的。用法其实和 printf 函数类似,就是多了一个文件指针。
函数原型: int fprintf(FILE *stream, const char *format, ...)
示例: #include int main(int argc, char *argv[]) { FILE* file = NULL; int age = 0; file = fopen("test.txt", "w"); if (file != NULL) { // 询问用户的年龄 printf("您几岁了 ? "); scanf("%d", &age); // 写入文件 fprintf(file, "使用者年龄是 %d 岁\n", age); fclose(file); } return 0; }
从文件中读出
我们可以用与写入文件时类似名字的函数,只是略微修改了一些,也有三个: fgetc:读出一个字符。是file get character 的缩写。get 表示“获取,取得”。 fgets:读出一个字符串。是 file get string 的缩写。 fscanf:与 scanf 的用法类似,只是多了一个文件指针。scanf 是从用户输入读取,而 fscanf 是从文件读取。
这次介绍这三个函数我们会简略一些,因为如果大家掌握好了前面那三个写入的函数,那这三个读出的函数是类似的。只是操作相反了。
fgetc
首先给出函数原型: int fgetc(FILE* pointerOnFile);
函数返回值是读到的字符。如果不能读到字符,那会返回 EOF。
但是如何知道我们从文件的哪个位置读取呢?是第三个字符处,还是第十个字符处呢?
其实,在我们读取文件时,有一个“游标”(cursor),会跟随移动。
这当然是虚拟的游标,你不会在屏幕上看到它。你可以想象这个游标和你用记事本编辑文件时的闪动的光标类似。这个游标指示你当前在文件中的位置。
之后的小节,我们会学习如何移动这个游标,使其位于文件中特定的位置。可以是开头,也可以是第 7 个字符处。
fgetc 函数每读入一个字符,这个游标就移动一个字符长度。我们就可以用一个循环来读出文件所有的字符。例如: #include int main(int argc, char *argv[]) { FILE* file = NULL; int currentCharacter = 0; file = fopen("test.txt", "r"); if (file != NULL) { // 循环读取,每次一个字符 do { currentCharacter = fgetc(file); // 读取一个字符 printf("%c", currentCharacter); // 显示读取到的字符 } while (currentCharacter != EOF); // 我们继续,直到 fgetc 返回 EOF(表示“文件结束”)为止 fclose(file); } return 0; }
fgets
此函数每次读出一个字符串,这样可以不必每次读一个字符(有时候效率太低)。
这个函数每次最多读取一行,因为它遇到第一个 '\n'(换行符)会结束读取。所以如果我们想要读取多行,需要用循环。
>插入一点回车符和换行符的知识: 关于“回车”(carriage return)和“换行”(line feed)这两个概念的来历和区别。 在计算机还没有出现之前,有一种叫做电传打字机(Teletype Model 33)的玩意,每秒钟可以打 10 个字符。 但是它有一个问题,就是打完一行换行的时候,要用去 0.2 秒,正好可以打两个字符。要是在这 0.2 秒里面,又有新的字符传过来,那么这个字符将丢失。 于是,研制人员想了个办法解决这个问题,就是在每行后面加两个表示结束的字符。一个叫做“回车”,告诉打字机把打印头定位在左边界;另一个叫做“换行”,告诉打字机把纸向下移一行。这就是“换行”和“回车”的来历,从它们的英语名字上也可以看出一二。 后来,计算机被发明了,这两个概念也就被搬到了计算机上。那时,存储器很贵,一些科学家认为在每行结尾加两个字符太浪费了,加一个就可以。于是,就出现了分歧。在 Unix/Linux 系统里,每行结尾只有“<换行>”,即 "\n";在 Windows 系统里面,每行结尾是“<换行><回车>”,即 "\n\r";在 macOS 系统里,每行结尾是“<回车>”,即 "\r"。 一个直接后果是,Unix/Linux/macOS 系统下的文件在Windows里打开的话,所有文字会变成一行;而 Windows 里的文件在 Unix/Linux/macOS 下打开的话,在每行的结尾可能会多出一个 ^M 符号。 Linux 中遇到换行符会进行“回车 + 换行”的操作,回车符反而只会作为控制字符显示,不发生回车的操作。 而 Windows 中要“回车符 + 换行符”才会实现“回车+换行",缺少一个控制符或者顺序不对都不能正确的另起一行。
函数原型: char* fgets(char* string, int characterNumberToRead, FILE* pointerOnFile);
示例: #include #define MAX_SIZE 1000 // 数组的最大尺寸 1000 int main(int argc, char *argv[]) { FILE* file = NULL; char string[MAX_SIZE] = ""; // 尺寸为 MAX_SIZE 的数组,初始为空 file = fopen("test.txt", "r"); if (file != NULL) { fgets(string, MAX_SIZE, file); // 我们读取最多 MAX_SIZE 个字符的字符串,将其存储在 string 中 printf("%s\n", string); // 显示字符串 fclose(file); } return 0; }
这里,我们的 MAX_SIZE 足够大(1000),保证可以容纳下一行的字符数。所以遇到 '\n' 我们就停止读取,因此以上代码的作用就是读取文件中的一行字符,并将其输出。
那我们如何能够读取整个文件的内容呢?很简单,加一个循环。
如下: #include #define MAX_SIZE 1000 // 数组的最大尺寸 1000 int main(int argc, char *argv[]) { FILE* file = NULL; char string[MAX_SIZE] = ""; // 尺寸为 MAX_SIZE 的数组,初始为空 file = fopen("test.txt", "r"); if (file != NULL) { while (fgets(string, MAX_SIZE, file) != NULL) // 我们一行一行地读取文件内容,只要不遇到文件结尾 printf("%s\n", string); // 显示字符串 fclose(file); } return 0; }
fscanf
此函数的原理和 scanf 是一样的。负责从文件中读取规定样式的内容。
函数原型: int fscanf(FILE *stream, const char *format, ...)
示例:
例如我们创建一个 test.txt 文件,在里面输入三个数:23, 45, 67。
输入的形式可以是类似下面这样: 每个数之间有空格
每个数之间换一行
#include int main(int argc, char *argv[]) { FILE* file = NULL; int score[3] = {0}; // 包含 3 个最佳得分的数组 file = fopen("test.txt", "r"); if (file != NULL) { fscanf(file, "%d %d %d", &score[0], &score[1], &score[2]); printf("最佳得分是 : %d, %d 和 %d\n", score[0], score[1], score[2]); fclose(file); } return 0; }
运行输出: 最佳得分是:23, 45, 67
4. 在文件中移动
前面我们提到了虚拟的“游标”,现在我们仔细地来学习一下。
每当我们打开一个文件的时候,实际上都存在一个“游标”,标识你当前在文件中所处的位置。
你可以类比我们的文本编辑器,每次你在文本编辑器(例如记事本)里面输入文字的时候,不是有一个游标(光标)可以到处移动么?它指示了你在文件中的位置,也就是你下一次输入会从哪里开始。
总结来说,游标系统使得我们可以在文件中指定位置进行读写操作。
我们介绍三个与文件中游标移动有关的函数: ftell:告知目前在文件中哪个位置。tell 表示“告诉”。 fseek:移动文件中的游标到指定位置。seek 表示“探寻”。 rewind:将游标重置到文件的开始位置(这和用 fseek 函数来使游标回到文件开始位置是一个效果)。rewind 表示“转回”。
ftell:指示目前在文件中的游标位置
这个函数使用起来非常简单,它返回一个 long 型的整数值,标明目前游标所在位置。函数原型是: long ftell(FILE* pointerOnFile);
其中,pointerOnFile 这个指针就是文件指针,指向当前文件。
相信不必用例子就知道如何使用了吧。
fseek:使游标移动到指定位置
函数原型为: int fseek(FILE* pointerOnFile, long move, int origin);
此函数能使游标在文件(pointerOnFile 指针所指)中从位置(origin 所指。origin 表示“初始”)开始移动一定距离(move 所指。move 表示“移动”)。 move 参数:可以是一个正整数,表明向前移动;0,表明不移动;或者负整数,表明回退。 origin 参数:它的取值可以是以下三个值( #define 所定义的常量)中的任意: SEEK_SET :文件开始处。SET 表示“设置”。 SEEK_CUR :游标当前所在位置。CUR 是 current(表示“当前”)的缩写。 SEEK_END :文件末尾。END 表示“结尾”。
来看几个具体使用实例吧: // 这行代码将游标放置到距离文件开始处 5 个位置的地方 fseek(file, 5, SEEK_SET); // 这行代码将游标放置到距离当前位置往后 3 个位置的地方 fseek(file, -3, SEEK_CUR); // 这行代码将游标放置到文件末尾 fseek(file, 0, SEEK_END);
rewind:使游标回到文件开始位置
这个函数的作用就相当于使用 fseek 来使游标回到 0 的位置 void rewind(FILE* pointerOnFile);
相信使用难不倒大家吧,看函数原型就一目了然了。和 fseek(file, 0, SEEK_SET); 是一个效果。
5. 文件的重命名和删除
我们来学习两个简单的函数,以结束这次的课程: rename 函数:重命名一个文件(rename 表示“重命名”)。 remove 函数:删除一个文件(remove 表示“移除”)。
这两个函数的特殊之处就在于,不同于之前的一些文件操作函数,它们不需要文件指针作为参数,只需要把文件的名字传给这两个函数就够了。
rename:重命名文件
函数原型: int rename(const char* oldName, const char* newName);
oldName 就是文件的“旧名字”,而 newName 是文件的“新名字”。
如果函数执行成功,则返回 0;否则,返回非零的 int 型值。
以下是一个使用的例子: int main(int argc, char *argv[]) { rename("test.txt", "renamed_test.txt"); return 0; }
很简单吧。
remove:删除一个文件
函数原型: int remove(const char* fileToRemove);
fileToRemove 就是要删除的文件名。
>注意:remove 函数要慎用,因为它不会提示你是否确认删除文件。 文件是直接从硬盘被永久删除了,也不会先移动至垃圾箱。 想要再找回被删除的文件就只能借助一些特殊的软件了,但是恢复过程可能没那么容易,也不一定能够成功。
实例: int main(int argc, char *argv[]) { remove("test.txt"); return 0; }
6. 第二部分第八课预告
今天的课就到这里,一起加油吧!
下一课: C语言探索之旅 | 第二部分第八课:动态分配
>我是 谢恩铭 ,公众号「程序员联盟」(微信号:coderhub)运营者,慕课网精英讲师 Oscar 老师 ,终生学习者。 热爱生活,喜欢游泳,略懂烹饪。 人生格言:「向着标杆直跑」
软件开发
2020-06-15 11:53:00
value值为普通数据类型(例如数字、字符串、布尔等) 当YAML配置文件中配置的属性值为普通数据类型时,可以直接配置对应的属性值,同时对于字符串类型的属性值,不需要额外添加引号,示例代码如下 ```yaml server: port: 8081 path: /hello ``` 上述代码用于配置server的port和path属性,port和path属于一个级别 刚学了拉勾教育的《Java工程师高薪训练营》,看到刚学到的点。希望拉勾能给我推到想去的公司,目标:字节!
软件开发
2020-06-15 10:37:00
它们都是位于传输层的协议,两者的大致区别有: 1.TCP是需要先 连接 在传输,而UDP无需连接直接进行传输。
2.正由于一个需连接一个不需,故TCP提供的是 可靠 的数据传输,而UDP不保证数据的完整、正确性。
3.同时,由于TCP需要先连接,难免增加了不少 开销 ,使得它的首部字节增大很多、所需资源也更多、数据传输的速度更慢,而UDP的首部字节和资源都要小很多,速度也快一些。
4.TCP的 传输形式 是字节流,而UDP直接就是数据报文段。
综上,TCP一般用于文件传输、邮件发送、远程登录等场景,而UDP常用于即时通信如QQ信息、直播等。
软件开发
2020-06-14 16:07:00
SQL--多表查询(mysql)
博客说明 文章所涉及的资料来自互联网整理和个人总结,意在于个人学习和经验汇总,如有什么地方侵权,请联系本人删除,谢谢!
笛卡尔积 有两个集合A,B .取这两个集合的所有组成情况。 要完成多表查询,需要消除无用的数据
分类
内连接查询 1. 从哪些表中查询数据 2. 条件是什么 3. 查询哪些字段 隐式内连接:
使用where条件消除无用数据 例子: * -- 查询所有员工信息和对应的部门信息 SELECT * FROM emp,dept WHERE emp.`dept_id` = dept.`id`; -- 查询员工表的名称,性别。部门表的名称 SELECT emp.name,emp.gender,dept.name FROM emp,dept WHERE emp.`dept_id` = dept.`id`; SELECT t1.name, -- 员工表的姓名 t1.gender,-- 员工表的性别 t2.name -- 部门表的名称 FROM emp t1, dept t2 WHERE t1.`dept_id` = t2.`id`; 显式内连接: 语法: select 字段列表 from 表名1 [inner] join 表名2 on 条件 SELECT * FROM emp INNER JOIN dept ON emp.`dept_id` = dept.`id`; SELECT * FROM emp JOIN dept ON emp.`dept_id` = dept.`id`;
外链接查询 左外连接:
语法:select 字段列表 from 表1 left [outer] join 表2 on 条件;
查询的是左表所有数据以及其交集部分。 -- 查询所有员工信息,如果员工有部门,则查询部门名称,没有部门,则不显示部门名称 SELECT t1.*,t2.`name` FROM emp t1 LEFT JOIN dept t2 ON t1.`dept_id` = t2.`id`; 右外连接:
语法:select 字段列表 from 表1 right [outer] join 表2 on 条件;
查询的是右表所有数据以及其交集部分。 SELECT * FROM dept t2 RIGHT JOIN emp t1 ON t1.`dept_id` = t2.`id`;
子查询 概念:
查询中嵌套查询,称嵌套查询为子查询 -- 查询工资最高的员工信息 -- 1 查询最高的工资是多少 9000 SELECT MAX(salary) FROM emp; -- 2 查询员工信息,并且工资等于9000的 SELECT * FROM emp WHERE emp.`salary` = 9000; -- 一条sql就完成这个操作。子查询 SELECT * FROM emp WHERE emp.`salary` = (SELECT MAX(salary) FROM emp); 子查询不同情况 子查询的结果是单行单列的: 子查询可以作为条件,使用运算符去判断。 运算符: > >= < <= = -- 查询员工工资小于平均工资的人 SELECT * FROM emp WHERE emp.salary < (SELECT AVG(salary) FROM emp); 子查询的结果是多行单列的: 子查询可以作为条件,使用运算符in来判断 -- 查询'财务部'和'市场部'所有的员工信息 SELECT id FROM dept WHERE NAME = '财务部' OR NAME = '市场部'; SELECT * FROM emp WHERE dept_id = 3 OR dept_id = 2; -- 子查询 SELECT * FROM emp WHERE dept_id IN (SELECT id FROM dept WHERE NAME = '财务部' OR NAME = '市场部'); 子查询的结果是多行多列的: 子查询可以作为一张虚拟表参与查询 -- 查询员工入职日期是2011-11-11日之后的员工信息和部门信息 -- 子查询 SELECT * FROM dept t1 ,(SELECT * FROM emp WHERE emp.`join_date` > '2011-11-11') t2 WHERE t1.id = t2.dept_id; -- 普通内连接 SELECT * FROM emp t1,dept t2 WHERE t1.`dept_id` = t2.`id` AND t1.`join_date` > '2011-11-11'
感谢 黑马程序员
以及勤劳的自己
软件开发
2020-06-13 18:30:00
今天给大家分享如何在VMware中创建CentOS虚拟机,CentOS6.7为例进行说明,CentOS7版本亦可以参考该教程,具体的教程如下。
1、之后打开VMware,主页面如下图所示。点击第一个框框,“创建新的虚拟机”。
2、弹出下图界面,选择第二个“自定义(高级)”,之后“下一步”。
3、这一步默认即可,直接点击“下一步”。
4、这里点击“稍后安装操作系统”,之后选择“下一步”。
5、这一步是选择版本。客户机操作系统默认选择为Linux系统,而版本则根据电脑的位数来进行选择,如果电脑是64位系统的话,就选择CentOS64位,如果是32位的话,就直接选择CentOS即可。这里如果选择不正确的话,后面在安装Ubuntu系统的时候重启会报系统不兼容的错误。选择好版本之后,点击选择“下一步”。
6、弹出虚拟机的名称和安装位置。
7、虚拟机名称可以自定义,不多赘述,这里将其命名为master,后期作为集群的管理节点用的。虚拟机蛮占内存的,所以安装位置不建议放到C盘,建议放到其他的盘内,且要保证该磁盘的空间至少要大于虚拟机的大小。这里,小编将虚拟机放到E盘。
8、处理器的配置。如果只是想拿虚拟机练练手,要求不高的话,就直接默认,选择“下一步”即可。
9、如果对处理要求较高,则需要按需进行分配。这里小编将配置给的比较高,因为后期有其他的用途,如下图所示。
10、虚拟机内存默认为2G。同上一步一致,要求不高的话直接默认即可。在这里小编选择了20G,大家可以根据自己的要求进行选择,要保证磁盘大小够用噢,之后选择“下一步”。
11、网络连接部分,直接默认即可,选择NAT模式,之后选择“下一步”。
12、选择I/O控制器类型,这一步默认即可,选择推荐的类型,之后选择“下一步”。
13、磁盘类型一般选择推荐的就可以,SCSI和SATA都是比较受欢迎的磁盘类型,之后选择“下一步”。
14、选择磁盘。建议选择第一项,“创建新虚拟磁盘”,之后选择“下一步”。下一步最大磁盘大小默认设为20G,默认即可。这里选择“将虚拟磁盘存储为单个文件”,也有的伙伴选择“将虚拟磁盘拆分为多个文件”,也是可以的。
15、指定磁盘文件。这一步默认即可,之后选择“下一步”。
16、之后弹出下图的界面,之后再选择“完成”。
17、至此,CentOS虚拟机创建完成,如下图所示。
接下来你就可以在VMware虚拟机中安装CentOS系统了,Linux搞起来。
看完本文有收获?请转发分享给更多的人
IT共享之家
入群请在微信后台回复【入群】 想学习更多Python网络爬虫与数据挖掘知识,可前往专业网站: http://pdcfighting.com/
软件开发
2020-06-13 09:52:00
熔断 限流 重试
1. 限速路由器
限速在高并发场景中比较常用的手段之一,可以有效的保障服务的整体稳定性,Spring Cloud Gateway 提供了基于 Redis 的限流方案。所以我们首先需要添加对应的依赖包spring-boot-starter-data-redis-reactive org.springframework.boot spring-boot-starter-data-redis-reactive
配置文件中需要添加 Redis 地址和限流的相关配置 server: port: 8080 spring: application: name: spring-cloud-gateway redis: host: localhost password: password port: 6379 cloud: gateway: discovery: locator: enabled: true routes: - id: requestratelimiter_route uri: http://example.org filters: - name: RequestRateLimiter args: redis-rate-limiter.replenishRate: 10 redis-rate-limiter.burstCapacity: 20 key-resolver: "#{@userKeyResolver}" predicates: - Method=GET filter 名称必须是 RequestRateLimiter redis-rate-limiter.replenishRate:允许用户每秒处理多少个请求 redis-rate-limiter.burstCapacity:令牌桶的容量,允许在一秒钟内完成的最大请求数 key-resolver:使用 SpEL 按名称引用 bean
项目中设置限流的策略,创建 Config 类。(了解源码可+求求: 1791743380) package com.springcloud.gateway.config; import org.springframework.cloud.gateway.filter.ratelimit.KeyResolver; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import reactor.core.publisher.Mono; /** * Created with IntelliJ IDEA. * * @Date: 2019/7/11 * @Time: 23:45 * @email: inwsy@hotmail.com * Description: */ @Configuration public class Config { @Bean KeyResolver userKeyResolver() { return exchange -> Mono.just(exchange.getRequest().getQueryParams().getFirst("user")); } }
Config类需要加@Configuration注解。
根据请求参数中的 user 字段来限流,也可以设置根据请求 IP 地址来限流,设置如下: @Bean public KeyResolver ipKeyResolver() { return exchange -> Mono.just(exchange.getRequest().getRemoteAddress().getHostName()); }
这样网关就可以根据不同策略来对请求进行限流了。
2. 熔断路由器
Spring Cloud Gateway 也可以利用 Hystrix 的熔断特性,在流量过大时进行服务降级,同样我们还是首先给项目添加上依赖。 这样网关就可以根据不同策略来对请求进行限流了。 2. 熔断路由器 在之前的 Spring Cloud 系列文章中,大家对熔断应该有了一定的了解,如过不了解可以先读这篇文章:《跟我学SpringCloud | 第四篇:熔断器Hystrix》 Spring Cloud Gateway 也可以利用 Hystrix 的熔断特性,在流量过大时进行服务降级,同样我们还是首先给项目添加上依赖。
配置示例 spring: cloud: gateway: routes: - id: hystrix_route uri: http://example.org filters: - Hystrix=myCommandName
配置后,gateway 将使用 myCommandName 作为名称生成 HystrixCommand 对象来进行熔断管理。如果想添加熔断后的回调内容,需要在添加一些配置。 spring: cloud: gateway: routes: - id: hystrix_route uri: lb://spring-cloud-producer predicates: - Path=/consumingserviceendpoint filters: - name: Hystrix args: name: fallbackcmd fallbackUri: forward:/incaseoffailureusethis
fallbackUri: forward:/incaseoffailureusethis配置了 fallback 时要会调的路径,当调用 Hystrix 的 fallback 被调用时,请求将转发到/incaseoffailureuset这个 URI。
3. 重试路由器
RetryGatewayFilter 是 Spring Cloud Gateway 对请求重试提供的一个 GatewayFilter Factory。
配置示例 spring: cloud: gateway: routes: - id: retry_test uri: lb://spring-cloud-producer predicates: - Path=/retry filters: - name: Retry args: retries: 3 statuses: BAD_GATEWAY
Retry GatewayFilter 通过这四个参数来控制重试机制: retries, statuses, methods, 和 series。 retries:重试次数,默认值是 3 次 statuses:HTTP 的状态返回码,取值请参考:org.springframework.http.HttpStatus methods:指定哪些方法的请求需要进行重试逻辑,默认值是 GET 方法,取值参考:org.springframework.http.HttpMethod series:一些列的状态码配置,取值参考:org.springframework.http.HttpStatus.Series。符合的某段状态码才会进行重试逻辑,默认值是 SERVER_ERROR,值是 5,也就是 5XX(5 开头的状态码),共有5 个值。
软件开发
2020-06-12 11:50:00
常规的创建文件方式有: touch vi(m) tee > 或 >>
但是这几种都只适合创建小的文本文件,某些情况下出于测试的需要,你需要快速创建一个超大的文件,可能要 上百G。这时候要使用上面几个命令,你可能要等一天的时间,效率非常低。
接下来介绍几种我常用的方法
1. dd
dd命令,可以从标准输入或指定的文件中读取数据,拷贝至新文件。 $ dd if=/dev/zero of=big_file count=10 bs=1G
使用 time 命令,可以算出创建一个 10G的文件需要耗时多久? $ time dd if=/dev/zero of=big_file count=10 bs=1G 10+0 records in 10+0 records out 10737418240 bytes (11 GB) copied, 7.93627 s, 1.4 GB/s real 0m7.941s user 0m0.000s sys 0m7.935s
花了将近8秒的时间 $ ls -lh big_file -rw-r--r-- 1 root root 10G Jun 2 10:57 big_file
2. fallocate
fallocate命令可以为文件预分配物理空间。 -l 后接空间大小,默认单位为字节。也可后跟k、m、g、t、p、e来指定单位,分别代表KB、MB、GB、TB、PB、EB。 $ fallocate -l 10G big_file
使用 time 命令,可以算出创建一个 10G的文件需要耗时多久? $ time fallocate -l 10G big_file real 0m0.002s user 0m0.000s sys 0m0.001s
居然只有 0.001秒。 $ ls -lh big_file -rw-r--r-- 1 root root 10G Jun 2 11:01 big_file
使用 du 命令,查看是否是真正创建了一个 10G大小的文件?从结果来看,是的。 $ du -sh big_file 10G big_file
3. truncate
truncate命令可以将文件缩减或扩展为指定大小,使用 -s 参数设置大小。 $ truncate -s 10G big_file
使用 time 命令,可以算出创建一个 10G的文件需要耗时多久? $ time truncate -s 10G big_file real 0m0.001s user 0m0.000s sys 0m0.001s
也是只有 0.001 秒。
不过与 fallocate 不同的是,allocate 创建的文件是真实大小,而 truncate 创建的并不是。
使用 ls 和 du 命令,查看的结果是不一样的。 $ ls -lh big_file -rw-r--r-- 1 root root 10G Jun 2 11:11 big_file $ du -sh big_file 0 big_file
由此可见 truncate 的作用是指定一个文件的大小,如果该文件不存在,就会创建该文件。如果指定文件的大小小于原先的大小,会丢失内容。
这个命令指定的文件大小其实是虚拟的。只是显示出来的大小。如果你指定一个非常大的文件。其实服务器剩余空间并不会减少。
以上,是我常用的几种创建大文件的方法,可以根据不同的使用场景进行选择,我一般使用的是 fallocate,速度够快,也是也会真实地占用磁盘空间,符合真实的测试场景。
软件开发
2020-06-12 08:42:00
6.39(几何:点的位置)编程练习题3.32显示如何测试一个点是否在一个有向直线的左侧、右侧,或在该直线上。使用下面的方法头编写该方法:
public static boolean leftOfTheLine(double x0, double y0, double x1, double y1, double x2, double y2)
public static boolean onTheSameLine(double x0, double y0, double x1, double y1, double x2, double y2)
public static boolean onTheLineSegment(double x0, double y0, double x1, double y1, double x2, double y2)
编写一个程序,提示用户输入三个点赋给p0、p1和p2,显示p2是否在从p0到p1的直线的左侧、右侧、直线上,或者线段上。
下面是一些运行示例:
Enter three points for p0, p1, and p2: 1 1 2 2 1.5 1.5
(1.5, 1.5) is on the line segment from (1.0, 1.0) to (2.0, 2.0)
Enter three points for p0, p1, and p2: 1 1 2 2 3 3
(3.0, 3.0) is on the same line from (1.0, 1.0) to (2.0, 2.0)
Enter three points for p0, p1, and p2: 1 1 2 2 1 1.5
(1.0, 1.5) is on the left side of the line from (1.0, 1.0) to (2.0, 2.0)
Enter three points for p0, p1, and p2: 1 1 2 2 1 –1
(1.0, −1.0) is on the right side of the line from (1.0, 1.0) to (2.0, 2.0)
6.39(Geometry: point position) Programming Exercise 3.32 shows how to test whether a point is on the left side of a directed line, on the right, or on the same line. Write the methods with the following headers:
public static boolean leftOfTheLine(double x0, double y0, double x1, double y1, double x2, double y2)
public static boolean onTheSameLine(double x0, double y0, double x1, double y1, double x2, double y2)
public static boolean onTheLineSegment(double x0, double y0, double x1, double y1, double x2, double y2)
Write a program that prompts the user to enter the three points for p0, p1, and p2and displays whether p2 is on the left side of the line from p0 to p1, right side, the same line, or on the line segment.
Here are some sample runs:
Enter three points for p0, p1, and p2: 1 1 2 2 1.5 1.5
(1.5, 1.5) is on the line segment from (1.0, 1.0) to (2.0, 2.0)
Enter three points for p0, p1, and p2: 1 1 2 2 3 3
(3.0, 3.0) is on the same line from (1.0, 1.0) to (2.0, 2.0)
Enter three points for p0, p1, and p2: 1 1 2 2 1 1.5
(1.0, 1.5) is on the left side of the line from (1.0, 1.0) to (2.0, 2.0)
Enter three points for p0, p1, and p2: 1 1 2 2 1 –1
(1.0, −1.0) is on the right side of the line from (1.0, 1.0) to (2.0, 2.0)
下面是参考答案代码: // https://cn.fankuiba.com import java.util.Scanner; public class Ans6_39_page205 { public static void main(String[] args) { Scanner input = new Scanner(System.in); System.out.print("Enter three points for p0, p1, and p2:"); double x0 = input.nextDouble(); double y0 = input.nextDouble(); double x1 = input.nextDouble(); double y1 = input.nextDouble(); double x2 = input.nextDouble(); double y2 = input.nextDouble(); if (onTheSameLine(x0, y0, x1, y1, x2, y2)) { if (onTheLineSegment(x0, y0, x1, y1, x2, y2)) { System.out.printf("(%.1f, %.1f) is on the line segment from " + "(%.1f, %.1f) to (%.1f, %.1f)", x2, y2, x0, y0, x1, y1); } else { System.out.printf("(%.1f, %.1f) is on the same line from " + "(%.1f, %.1f) to (%.1f, %.1f)", x2, y2, x0, y0, x1, y1); } } else { if (leftOfTheLine(x0, y0, x1, y1, x2, y2)) { System.out.printf("(%.1f, %.1f) is on the left side of the line from " + "(%.1f, %.1f) to (%.1f, %.1f)", x2, y2, x0, y0, x1, y1); } else { System.out.printf("(%.1f, %.1f) is on the right side of the line from " + "(%.1f, %.1f) to (%.1f, %.1f)", x2, y2, x0, y0, x1, y1); } } } public static boolean leftOfTheLine(double x0, double y0, double x1, double y1, double x2, double y2) { if ((x1 - x0) * (y2 - y0) - (x2 - x0) * (y1 - y0) > 0) return true; else return false; } public static boolean onTheSameLine(double x0, double y0, double x1, double y1, double x2, double y2) { return (x1 - x0) * (y2 - y0) - (x2 - x0) * (y1 - y0) == 0; } public static boolean onTheLineSegment(double x0, double y0, double x1, double y1, double x2, double y2) { if ((x1 - x0) * (y2 - y0) - (x2 - x0) * (y1 - y0) == 0 && x2 <= (Math.max(x0, x1)) && x2 >= (Math.min(x1, x0))) return true; else return false; } }
适用Java语言程序设计与数据结构(基础篇)(原书第11版)Java语言程序设计(基础篇)(原书第10/11版)
发布在博客:( https://cn.fankuiba.com )
软件开发
2020-06-12 03:42:00
Java--注解
博客说明 文章所涉及的资料来自互联网整理和个人总结,意在于个人学习和经验汇总,如有什么地方侵权,请联系本人删除,谢谢!
说明
注解:说明程序的。给计算机看的
注释:用文字描述程序的。给程序员看的
注解(Annotation),也叫元数据。一种代码级别的说明。它是JDK1.5及以后版本引入的一个特性,与类、接口、枚举是在同一个层次。它可以声明在包、类、字段、方法、局部变量、方法参数等的前面,用来对这些元素进行说明,注释
作用
①编写文档:通过代码里标识的注解生成文档【生成文档doc文档】
②代码分析:通过代码里标识的注解对代码进行分析【使用反射】
③编译检查:通过代码里标识的注解让编译器能够实现基本的编译检查【Override】
JDK中预定义的一些注解
@Override :检测被该注解标注的方法是否是继承自父类(接口)的
@Deprecated:该注解标注的内容,表示已过时
@SuppressWarnings:压制警告, 一般传递参数all @SuppressWarnings("all")
自定义注解
格式 元注解 public @interface 注解名称{ 属性列表; }
本质
注解本质上就是一个接口,该接口默认继承Annotation接口 public interface MyAnno extends java.lang.annotation.Annotation {}
属性
接口中的抽象方法 属性的返回值类型有下列取值 基本数据类型 String 枚举 注解 定义了属性,在使用时需要给属性赋值 如果定义属性时,使用default关键字给属性默认初始化值,则使用注解时,可以不进行属性的赋值。 如果只有一个属性需要赋值,并且属性的名称是value,则value可以省略,直接定义值即可。 数组赋值时,值使用{}包裹。如果数组中只有一个值,则{}可以省略
元注解
用于描述注解的注解 @Target:描述注解能够作用的位置 ElementType取值: TYPE:可以作用于类上 METHOD:可以作用于方法上 FIELD:可以作用于成员变量上 @Retention:描述注解被保留的阶段 @Retention(RetentionPolicy.RUNTIME):当前被描述的注解,会保留到class字节码文件中,并被JVM读取到 @Documented:描述注解是否被抽取到api文档中 @Inherited:描述注解是否被子类继承
解析注解
获取注解中定义的属性值 获取注解定义的位置的对象 (Class,Method,Field) 获取指定的注解 getAnnotation(Class) //其实就是在内存中生成了一个该注解接口的子类实现对象 public class ProImpl implements Pro{ public String className(){ return "cn.itcast.annotation.Demo1"; } public String methodName(){ return "show"; } } 调用注解中的抽象方法获取配置的属性值
感谢 黑马程序员
以及勤劳的自己
软件开发
2020-06-11 19:45:00
前端自学大概的路线图
因为刚开始学习,没有完整的前端知识体系,所以是大概的路线图,希望大家可以帮我一起修改完善。
大致的思路是:1. 先打好基础,在学习框架 。
基础方面主要涉及到的语言是Html、CSS、JavaScript,如果在这三个里面在挑基础的话应该就是Html、CSS了。
所以,2. 基础知识里面先学Html,CSS 。
Html\CSS学习计划
之前有过一个月的断断续续的学习,W3C的基础知识也看了,但是一到自己写页面还是不会。我就问自己为什么?我觉得可能的原因是,会HTML\CSS基础知识还不足以写出完整的静态页面,一定是我还缺少了某些知识和能力。
我觉得一个最重要的能力是: 拿到设计图,首先是分析它,而不是直接写代码 。
分析是一种能力,而HTML\CSS只是实现我们目标的工具,而掌握了HTML\CSS基础知识仅仅是掌握了如何使用这些工具,就像是我会搬砖,但并不知道怎么盖房子。
我认为这种分析能力是需要通过做大量 完整的页面 来锻炼的。
所以我大致的学习思路是:以最快的速度了解Html\CSS基础知识,然后至今开始做大量的完整的静态页面。下面会展开说我的学习计划以及一些资料。
1.快速掌握基本的HTML、CSS知识
网上有很多免费的HTML\CSS教程,但是为了 快速 ,我推荐[marksheet],这个教程的好处是它先介绍了web方面的基础知识,让大家对web有个宏观上的了解,然后再开始介绍HTML\CSS的 必要最少知识 ,大概四十分钟就可以看完。
2.准备好随时可查阅的资料
我觉得以下几个资料准备好就可以直接开始做页面了
cheat sheet

准备这个的好处是他们是图片,而且高度概括了HTML\CSS的基础知识,自己做页面的时候把这个图片打开,随时查阅,会比上网查更快
搜索XX mdn
当然如何需要知道某个html标签或者css属性的详细用法,只需要google一下: 你要找的知识 mdn
code guide
为了写出很健壮的代码,还是要吸取前辈们的一些最佳实践,我推荐的是 Code Guide ,也不是要强行的遵守他,但是要有意识注意他,边实践边改自己的代码。
3.做大量完整的静态页面
首先我需要找到这样的静态页面去临摹,下面是哪里寻找这些代码来临摹
[freecodecamp]这是一个学习前端的很好的网站,里面有一些实战的项目,值得去做,但是没有视频指导,会比上一种稍微难一点的。
[Dribbble]上有很多设计很漂亮的设计图,我们可以去上面找到自己喜欢的设计图,然后实现它。这种方式最难,但是也最锻炼人。
我的一点建议:我会在实现每一个完整页面前,会写一个简单的 分析报告 ,里面的内容就是分析一下这个页面的结构,我大概会怎么实现它,有哪些结构我没见过的需要学习的。因为时间花了,肯定要把每个页面研究透,这也是我之前强调的认真的态度吧。
4.以自己的语言梳理学到的知识
我需要总结自己的cheet sheet,这样才能真正掌握html\css的基础知识轮廓;
需要总结自己的“设计图到HTML”的能力,因为我是产品经理,觉得语义化的html完全可以作为网站的UE来用;
需要自己总结的常用“设计样板”,类似各种布局的总结,各种导航栏的总结之类的,以后碰到类似的就直接拿来用了。某种程度上,我认为一个好的前端就是有自己的一个“设计样板”库吧,而且不断的更新扩充它。
the last but not the least
专门建立的学习Q-q-u-n ⑦⑧④-⑦⑧③-零①② 分享学习方法和需要注意的小细节,互相交流学习,不停更新最新的教程和学习技巧(网页制作,网站开发,web开发,从0基础开始的的HTML+CSS+JavaScript。jQuery,Ajax,node,angular框架等到移动端HTML5的项目实战【视频+工具+系统路线图】全栈工程师学习路线以及规划都有整理,分享给小伙伴)点: 学习前端,我们是认真的
软件开发
2020-06-11 18:29:00
作者:frank909 https://blog.csdn.net/briblue/article/details/76736356
泛型,一个孤独的守门者。
大家可能会有疑问,我为什么叫做泛型是一个守门者。这其实是我个人的看法而已,我的意思是说泛型没有其看起来那么深不可测,它并不神秘与神奇。泛型是 Java 中一个很小巧的概念,但同时也是一个很容易让人迷惑的知识点,它让人迷惑的地方在于它的许多表现有点违反直觉。
文章开始的地方,先给大家奉上一道经典的测试题。 List l1 = new ArrayList(); List l2 = new ArrayList(); System.out.println(l1.getClass() == l2.getClass());
请问,上面代码最终结果输出的是什么?不了解泛型的和很熟悉泛型的同学应该能够答出来,而对泛型有所了解,但是了解不深入的同学可能会答错。
正确答案是 true。
上面的代码中涉及到了泛型,而输出的结果缘由是类型擦除。先好好说说泛型。
泛型是什么?
泛型的英文是 generics,generic 的意思是通用,而翻译成中文,泛应该意为广泛,型是类型。所以泛型就是能广泛适用的类型。
但泛型还有一种较为准确的说法就是为了参数化类型,或者说可以将类型当作参数传递给一个类或者是方法。
那么,如何解释类型参数化呢? public class Cache {    Object value;    public Object getValue() {        return value;    }    public void setValue(Object value) {        this.value = value;    } }
假设 Cache 能够存取任何类型的值,于是,我们可以这样使用它。 Cache cache = new Cache(); cache.setValue(134); int value = (int) cache.getValue(); cache.setValue("hello"); String value1 = (String) cache.getValue();
使用的方法也很简单,只要我们做正确的强制转换就好了。
但是,泛型却给我们带来了不一样的编程体验。 public class Cache {    T value;    public Object getValue() {        return value;    }    public void setValue(T value) {        this.value = value;    } }
这就是泛型,它将 value 这个属性的类型也参数化了,这就是所谓的参数化类型。再看它的使用方法。 Cache cache1 = new Cache(); cache1.setValue("123"); String value2 = cache1.getValue(); Cache cache2 = new Cache(); cache2.setValue(456); int value3 = cache2.getValue();
最显而易见的好处就是它不再需要对取出来的结果进行强制转换了。但,还有另外一点不同。
泛型除了可以将类型参数化外,而参数一旦确定好,如果类似不匹配,编译器就不通过。 上面代码显示,无法将一个 String 对象设置到 cache2 中,因为泛型让它只接受 Integer 的类型。
所以,综合上面信息,我们可以得到下面的结论。 与普通的 Object 代替一切类型这样简单粗暴而言,泛型使得数据的类别可以像参数一样由外部传递进来。它提供了一种扩展能力。它更符合面向抽象开发的软件编程宗旨。 当具体的类型确定后,泛型又提供了一种类型检测的机制,只有相匹配的数据才能正常的赋值,否则编译器就不通过。所以说,它是一种类型安全检测机制,一定程度上提高了软件的安全性防止出现低级的失误。 泛型提高了程序代码的可读性,不必要等到运行的时候才去强制转换,在定义或者实例化阶段,因为  Cache  这个类型显化的效果,程序员能够一目了然猜测出代码要操作的数据类型。
下面的文章,我们正常介绍泛型的相关知识。
泛型的定义和使用
泛型按照使用情况可以分为 3 种。 1. 泛型类。 2. 泛型方法。 3. 泛型接口。
泛型类
我们可以这样定义一个泛型类。 public class Test {    T field1; }
尖括号  <>  中的 T 被称作是类型参数,用于指代任何类型。事实上,T 只是一种习惯性写法,如果你愿意。你可以这样写。 public class Test {    Hello field1; }
但出于规范的目的,Java 还是建议我们用单个大写字母来代表类型参数。常见的如: 1. T 代表一般的任何类。 2. E 代表 Element 的意思,或者 Exception 异常的意思。 3. K 代表 Key 的意思。 4. V 代表 Value 的意思,通常与 K 一起配合使用。 5. S 代表 Subtype 的意思,文章后面部分会讲解示意。
如果一个类被   的形式定义,那么它就被称为是泛型类。
那么对于泛型类怎么样使用呢? Test test1 = new Test<>(); Test test2 = new Test<>();
只要在对泛型类创建实例的时候,在尖括号中赋值相应的类型便是。T 就会被替换成对应的类型,如 String 或者是 Integer。你可以相像一下,当一个泛型类被创建时,内部自动扩展成下面的代码。 public class Test {    String field1; }
当然,泛型类不至接受一个类型参数,它还可以这样接受多个类型参数。 public class MultiType {    E value1;    T value2;    public E getValue1(){        return value1;    }    public T getValue2(){        return value2;    } }
泛型方法 public class Test1 {    public  void testMethod(T t){    } }
泛型方法与泛型类稍有不同的地方是,类型参数也就是尖括号那一部分是写在返回值前面的。  中的 T 被称为类型参数,而方法中的 T 被称为参数化类型,它不是运行时真正的参数。
当然,声明的类型参数,其实也是可以当作返回值的类型的。 public   T testMethod1(T t){        return null; }
泛型类与泛型方法的共存现象 public class Test1{    public  void testMethod(T t){        System.out.println(t.getClass().getName());    }    public   T testMethod1(T t){        return t;    } }
上面代码中, Test1  是泛型类,testMethod 是泛型类中的普通方法,而 testMethod1 是一个泛型方法。而泛型类中的类型参数与泛型方法中的类型参数是没有相应的联系的,泛型方法始终以自己定义的类型参数为准。
所以,针对上面的代码,我们可以这样编写测试代码。 Test1 t = new Test1(); t.testMethod("generic"); Integer i = t.testMethod1(new Integer(1));
泛型类的实际类型参数是 String,而传递给泛型方法的类型参数是 Integer,两者不想干。
但是,为了避免混淆,如果在一个泛型类中存在泛型方法,那么两者的类型参数最好不要同名。比如, Test1  代码可以更改为这样 public class Test1{    public  void testMethod(T t){        System.out.println(t.getClass().getName());    }    public   E testMethod1(E e){        return e;    } }
泛型接口
泛型接口和泛型类差不多,所以一笔带过。 public interface Iterable { }
通配符 ?
除了用   表示泛型外,还有   这种形式。? 被称为通配符。
可能有同学会想,已经有了   的形式了,为什么还要引进   这样的概念呢? class Base{} class Sub extends Base{} Sub sub = new Sub(); Base base = sub;
上面代码显示,Base 是 Sub 的父类,它们之间是继承关系,所以 Sub 的实例可以给一个 Base 引用赋值,那么 List lsub = new ArrayList<>(); List lbase = lsub;
最后一行代码成立吗?编译会通过吗?
答案是否定的。
编译器不会让它通过的。Sub 是 Base 的子类,不代表  List  和  List  有继承关系。
但是,在现实编码中,确实有这样的需求,希望泛型能够处理某一范围内的数据类型,比如某个类和它的子类,对此 Java 引入了通配符这个概念。
所以,通配符的出现是为了指定泛型中的类型范围。
通配符有 3 种形式。  被称作无限定的通配符。  被称作有上限的通配符。  被称作有下限的通配符。
无限定通配符 public void testWildCards(Collection collection){ }
上面的代码中,方法内的参数是被无限定通配符修饰的 Collection 对象,它隐略地表达了一个意图或者可以说是限定,那就是 testWidlCards() 这个方法内部无需关注 Collection 中的真实类型,因为它是未知的。所以,你只能调用 Collection 中与类型无关的方法。
我们可以看到,当   存在时,Collection 对象丧失了 add() 方法的功能,编译器不通过。 我们再看代码。 List wildlist = new ArrayList(); wildlist.add(123);// 编译不通过
有人说,  提供了只读的功能,也就是它删减了增加具体类型元素的能力,只保留与具体类型无关的功能。它不管装载在这个容器内的元素是什么类型,它只关心元素的数量、容器是否为空?我想这种需求还是很常见的吧。
有同学可能会想,  既然作用这么渺小,那么为什么还要引用它呢?
个人认为,提高了代码的可读性,程序员看到这段代码时,就能够迅速对此建立极简洁的印象,能够快速推断源码作者的意图。

 代表着类型未知,但是我们的确需要对于类型的描述再精确一点,我们希望在一个范围内确定类别,比如类型 A 及 类型 A 的子类都可以。 public void testSub(Collection para){ }
上面代码中,para 这个 Collection 接受 Base 及 Base 的子类的类型。
但是,它仍然丧失了写操作的能力。也就是说 para.add(new Sub()); para.add(new Base());
仍然编译不通过。
没有关系,我们不知道具体类型,但是我们至少清楚了类型的范围。

这个和   相对应,代表 T 及 T 的超类。 public void testSuper(Collection para){ }
 神奇的地方在于,它拥有一定程度的写操作的能力。 public void testSuper(Collection para){    para.add(new Sub());//编译通过    para.add(new Base());//编译不通过 }
通配符与类型参数的区别
一般而言,通配符能干的事情都可以用类型参数替换。 比如 public void testWildCards(Collection collection){}
可以被 public  void test(Collection collection){}
取代。
值得注意的是,如果用泛型方法来取代通配符,那么上面代码中 collection 是能够进行写操作的。只不过要进行强制转换。 public  void test(Collection collection){    collection.add((T)new Integer(12));    collection.add((T)"123"); }
需要特别注意的是,类型参数适用于参数之间的类别依赖关系,举例说明。 public class Test2 {    T value1;    E value2; } public  void test(D d,S s){    }
E 类型是 T 类型的子类,显然这种情况类型参数更适合。 有一种情况是,通配符和类型参数一起使用。 public  void test(T t,Collection collection){ }
如果一个方法的返回类型依赖于参数的类型,那么通配符也无能为力。 public T test1(T t){    return value1; }
类型擦除
泛型是 Java 1.5 版本才引进的概念,在这之前是没有泛型的概念的,但显然,泛型代码能够很好地和之前版本的代码很好地兼容。
这是因为,泛型信息只存在于代码编译阶段,在进入 JVM 之前,与泛型相关的信息会被擦除掉,专业术语叫做类型擦除。
通俗地讲,泛型类和普通类在 java 虚拟机内是没有什么特别的地方。回顾文章开始时的那段代码 List l1 = new ArrayList(); List l2 = new ArrayList(); System.out.println(l1.getClass() == l2.getClass());
打印的结果为 true 是因为  List  和  List  在 jvm 中的 Class 都是 List.class。
泛型信息被擦除了。
可能同学会问,那么类型 String 和 Integer 怎么办?
答案是泛型转译。 public class Erasure {    T object;    public Erasure(T object) {        this.object = object;    } }
Erasure 是一个泛型类,我们查看它在运行时的状态信息可以通过反射。 Erasure erasure = new Erasure("hello"); Class eclz = erasure.getClass(); System.out.println("erasure class is:"+eclz.getName());
打印的结果是 erasure class is:com.frank.test.Erasure
Class 的类型仍然是 Erasure 并不是  Erasure  这种形式,那我们再看看泛型类中 T 的类型在 jvm 中是什么具体类型。 Field[] fs = eclz.getDeclaredFields(); for ( Field f:fs) {    System.out.println("Field name "+f.getName()+" type:"+f.getType().getName()); }
打印结果是 Field name object type:java.lang.Object
那我们可不可以说,泛型类被类型擦除后,相应的类型就被替换成 Object 类型呢?
这种说法,不完全正确。
我们更改一下代码。 public class Erasure { //  public class Erasure {    T object;    public Erasure(T object) {        this.object = object;    } }
现在再看测试结果: Field name object type:java.lang.String
我们现在可以下结论了,在泛型类被类型擦除的时候,之前泛型类中的类型参数部分如果没有指定上限,如   则会被转译成普通的 Object 类型,如果指定了上限如   则类型参数就被替换成类型上限。
所以,在反射中。 public class Erasure {    T object;    public Erasure(T object) {        this.object = object;    }    public void add(T object){    } }
add() 这个方法对应的 Method 的签名应该是 Object.class。 Erasure erasure = new Erasure("hello"); Class eclz = erasure.getClass(); System.out.println("erasure class is:"+eclz.getName()); Method[] methods = eclz.getDeclaredMethods(); for ( Method m:methods ){    System.out.println(" method:"+m.toString()); }
打印结果是 method:public void com.frank.test.Erasure.add(java.lang.Object)
也就是说,如果你要在反射中找到 add 对应的 Method,你应该调用  getDeclaredMethod("add",Object.class)  否则程序会报错,提示没有这么一个方法,原因就是类型擦除的时候,T 被替换成 Object 类型了。
类型擦除带来的局限性
类型擦除,是泛型能够与之前的 java 版本代码兼容共存的原因。但也因为类型擦除,它会抹掉很多继承相关的特性,这是它带来的局限性。
理解类型擦除有利于我们绕过开发当中可能遇到的雷区,同样理解类型擦除也能让我们绕过泛型本身的一些限制。比如
正常情况下,因为泛型的限制,编译器不让最后一行代码编译通过,因为类似不匹配,但是,基于对类型擦除的了解,利用反射,我们可以绕过这个限制。 public interface List extends Collection{     boolean add(E e); }
上面是 List 和其中的 add() 方法的源码定义。
因为 E 代表任意的类型,所以类型擦除时,add 方法其实等同于 boolean add(Object obj);
那么,利用反射,我们绕过编译器去调用 add 方法。 public class ToolTest {    public static void main(String[] args) {        List ls = new ArrayList<>();        ls.add(23); //      ls.add("text");        try {            Method method = ls.getClass().getDeclaredMethod("add",Object.class);            method.invoke(ls,"test");            method.invoke(ls,42.9f);        } catch (NoSuchMethodException e) {            // TODO Auto-generated catch block            e.printStackTrace();        } catch (SecurityException e) {            // TODO Auto-generated catch block            e.printStackTrace();        } catch (IllegalAccessException e) {            // TODO Auto-generated catch block            e.printStackTrace();        } catch (IllegalArgumentException e) {            // TODO Auto-generated catch block            e.printStackTrace();        } catch (InvocationTargetException e) {            // TODO Auto-generated catch block            e.printStackTrace();        }        for ( Object o: ls){            System.out.println(o);        }    } }
打印结果是: 23 test 42.9
可以看到,利用类型擦除的原理,用反射的手段就绕过了正常开发中编译器不允许的操作限制。
泛型中值得注意的地方
泛型类或者泛型方法中,不接受 8 种基本数据类型。
所以,你没有办法进行这样的编码。 List li = new ArrayList<>(); List li = new ArrayList<>();
需要使用它们对应的包装类。 List li = new ArrayList<>(); List li1 = new ArrayList<>();
对泛型方法的困惑 public  T test(T t){    return null; }
有的同学可能对于连续的两个 T 感到困惑,其实   是为了说明类型参数,是声明,而后面的不带尖括号的 T 是方法的返回值类型。 你可以相像一下,如果 test() 这样被调用 test("123");
那么实际上相当于 public String test(String t);
Java 不能创建具体类型的泛型数组
这句话可能难以理解,代码说明。 List[] li2 = new ArrayList[]; List li3 = new ArrayList[];
这两行代码是无法在编译器中编译通过的。原因还是类型擦除带来的影响。
List  和  List  在 jvm 中等同于 List  ,所有的类型信息都被擦除,程序也无法分辨一个数组中的元素类型具体是  List 类型还是  List  类型。
但是, List[] li3 = new ArrayList[10]; li3[1] = new ArrayList(); List v = li3[1];
借助于无限定通配符却可以,前面讲过  ?  代表未知类型,所以它涉及的操作都基本上与类型无关,因此 jvm 不需要针对它对类型作判断,因此它能编译通过,但是,只提供了数组中的元素因为通配符原因,它只能读,不能写。比如,上面的 v 这个局部变量,它只能进行 get() 操作,不能进行 add() 操作,这个在前面通配符的内容小节中已经讲过。
泛型,并不神奇
我们可以看到,泛型其实并没有什么神奇的地方,泛型代码能做的非泛型代码也能做。
而类型擦除,是泛型能够与之前的 java 版本代码兼容共存的原因。
可量也正因为类型擦除导致了一些隐患与局限。
但,我还是要建议大家使用泛型,如官方文档所说的,如果可以使用泛型的地方,尽量使用泛型。
毕竟它抽离了数据类型与代码逻辑,本意是提高程序代码的简洁性和可读性,并提供可能的编译时类型转换安全检测功能。
类型擦除不是泛型的全部,但是它却能很好地检测我们对于泛型这个概念的理解程度。
我在文章开头将泛型比作是一个守门人,原因就是他本意是好的,守护我们的代码安全,然后在门牌上写着出入的各项规定,及“xxx 禁止出入”的提醒。但是同我们日常所遇到的那些门卫一般,他们古怪偏执,死板守旧,我们可以利用反射基于类型擦除的认识,来绕过泛型中某些限制,现实生活中,也总会有调皮捣蛋者能够基于对门卫们生活作息的规律,选择性地绕开他们的监视,另辟蹊径溜进或者溜出大门,然后扬长而去,剩下守卫者一个孤独的身影。
所以,我说泛型,并不神秘,也不神奇。
推荐去我的博客阅读更多:
1. Java JVM、集合、多线程、新特性系列教程
2. Spring MVC、Spring Boot、Spring Cloud 系列教程
3. Maven、Git、Eclipse、Intellij IDEA 系列工具教程
4. Java、后端、架构、阿里巴巴等大厂最新面试题
觉得不错,别忘了点赞+转发哦!
软件开发
2020-06-11 17:13:00
package test import ( "log" "os" ) var size = int64(1024 * 1e10) func ExampleTruncate() { f, err := os.Create("foobar1.bin") if err != nil { log.Fatal(err) } defer f.Close() if err := f.Truncate(size); err != nil { log.Fatal(err) } // Output: // } func ExampleSeek() { f, err := os.Create("foobar2.bin") if err != nil { log.Fatal(err) } defer f.Close() _, err = f.Seek(size-1, 0) if err != nil { log.Fatal(err) } _, err = f.Write([]byte{0}) if err != nil { log.Fatal(err) } // Output: // }
软件开发
2020-06-11 00:03:00
查询代码如下:
报如下的错 Cause: java.lang.IllegalArgumentException: invalid comparison: java.util.ArrayList and java.lang.String
原来if test的表达式写的不对,传入ids是ArrayList,不能当成字串比较,直接用 ids!=null 判断即可。
软件开发
2020-06-10 23:40:00
\r\n", "250", "MAIL FROM: "), //6.发送收件人邮箱 250 OK array("RCPT TO: <". $mailto .">\r\n", "250", "RCPT TO: "), //7.开始发送邮件数据 354 OK array("DATA\r\n", "354", "DATA Start: "), //8.0 发件人 array("From: ". $smtp_from ."\r\n", "", ""), //8.1 收件人 array("To: ". $mailto ."\r\n", "", ""), //8.2 发件代理客户端 array("X-Mailer: LANSEYUJIE WebMailer 1.0\r\n", "", ""), //8.3 邮件重要级别 1(Highest) 3(Normal) 5(Lowest) array("X-Priority: 1 (Highest)\r\n", "", ""), //8.4 标题 array("Subject: ". $subject ."\r\n", "", ""), //8.5 编码 array("Content-Type: text/html; charset=\"utf-8\"\r\n", "", ""), //8.6 内容加密方式 array("Content-Transfer-Encoding: base64\r\n\r\n", "", ""), //8.7 Base64 Encode Body array(base64_encode($body) ."\r\n", "", ""), //9.结束发送邮件数据 250 array("\r\n.\r\n", "250", "DATA End: "), //10.结束会话 array("QUIT\r\n", "221", "QUIT: ") ); $info = ''; //建立socket通信 if (1 == $smtp_openssl) { //SSL加密通信 $fp = @fsockopen('ssl://'. $smtp_host, $smtp_port); } else { //普通无加密通信 $fp = @fsockopen($smtp_host, $smtp_port); } if (!$fp) { $info .= "FSOCKOPEN Error: Cannot conect to ". $smtp_host ."\r\n
"; } //发送smtp数组中的命令/数据 foreach ($smtp as $request) { //发送请求信息 @fputs($fp, $request[0]); //接收响应信息 if (1 == $smtp_debug && '' != $request[1]) { //接收信息 $response = @fgets($fp, 128); $info .= $request[2] . $response ."\r\n
"; } } //关闭连接 @fclose($fp); return $info; } // 测试 echo smtp_mailer('test@lanseyujie.com', 'Mailer Test', 'This is a test mail to verify the Mailer run or not!');
软件开发
2020-06-10 23:24:00
遇到的错误解决:====================================== bad major version at offset=6
原因:使用高版本的JDK编译的Java class文件试图在较低版本的JVM上运行
解决方法:确保编译的JDK和运行时JVM的JRE版本一致

解决:最终追查客户的容器打印日志,客户使用的为j2re 1.5版本,最终用jdk1.5编译项目通过。
软件开发
2020-06-10 18:15:00

Laravel 中间件
代码展示
protected function sendRequestThroughRouter($request) { # $this->app->instance('request', $request); # Facade::clearResolvedInstance('request'); # $this->bootstrap(); return (new Pipeline($this->app)) ->send($request) ->through($this->app->shouldSkipMiddleware() ? [] : $this->middleware) ->then($this->dispatchToRouter()); // $this->dispatchToRouter(),后期继续 } new \Illuminate\Routing\Pipeline($this->app): public function __construct(Container $container = null) { $this->container = $container; } public function send($passable) { $this->passable = $passable; return $this; } public function through($pipes) { #################################################################################### # $this->middleware = [ # # \Illuminate\Foundation\Http\Middleware\CheckForMaintenanceMode::class, # # \Illuminate\Foundation\Http\Middleware\ValidatePostSize::class, # # \App\Http\Middleware\TrimStrings::class, # # \Illuminate\Foundation\Http\Middleware\ConvertEmptyStringsToNull::class, # # ]; # #################################################################################### $this->pipes = is_array($pipes) ? $pipes : func_get_args(); return $this; } // 中间件的本质 public function then(Closure $destination) { $pipeline = array_reduce( array_reverse($this->pipes), $this->carry(), $this->prepareDestination($destination) ); // 上面操作完之后返回一个匿名函数,接受一个参数 return $pipeline($this->passable); } // 注意这个是子类里面会调用的父carry,区别在于子类加入了异常处理 protected function carry() { // 接受匿名函数$stack参数和$pipe参数,返回匿名函数,再将此匿名函数作为第一个参数$stack迭代传入。 return function ($stack, $pipe) { return function ($passable) use ($stack, $pipe) { if ($pipe instanceof Closure) { return $pipe($passable, $stack); } elseif (! is_object($pipe)) { list($name, $parameters) = $this->parsePipeString($pipe); $pipe = $this->getContainer()->make($name); $parameters = array_merge([$passable, $stack], $parameters); } else { $parameters = [$passable, $stack]; } // 中间件作用是提供了一种方便的机制来过滤进入应用的 HTTP 请求 // $this->method默认为handle,可通过via方法进行设置 return $pipe->{$this->method}(...$parameters); }; }; } protected function parsePipeString($pipe) { // 不带参数的pipe(class)或带参数(参数以,分割)的pipe(class:param1,param2,param3...) list($name, $parameters) = array_pad(explode(':', $pipe, 2), 2, []); if (is_string($parameters)) { $parameters = explode(',', $parameters); } // $parameters为[]数组或[param1,param2,param3...] return [$name, $parameters]; } protected function prepareDestination(Closure $destination) { return function ($passable) use ($destination) { return $destination($passable); }; }
前置条件 array_reduce接受三个参数,第一个参数接收数组,第二个参数函数名(也可以是匿名函数,函数有两个参数,分别代表$result和$item),第三个参数(可选),该参数将被当成是数组中的第一个值来处理,或者如果数组为空的话就作为最终返回值。 匿名函数也叫闭包函数(closures),允许临时创建一个没有指定名称的函数,通过 Closure 类来实现的。 当对闭包函数进行赋值时,PHP 便会自动将此种表达式转换成内置类 Closure 的对象对象在进行赋值。 当闭包函数使用到 use 使用外部数据时,会在 Closure 对象生成一个 static 属性数组进行存放。 当闭包函数使用到参数时,会在 Closure 对象生成一个 parameter 属性数组进行存放。
public function then(Closure $destination) { $pipeline = array_reduce( array_reverse($this->pipes), $this->carry(), $this->prepareDestination($destination) ); return $pipeline($this->passable); }
生成最终匿名函数的过程:
array_reduce执行第一次时得到如下简化的匿名函数返回,将会继续作为第一个参数进行迭代: object(Closure)#id (1) { ["static"]=> array(2) { ["stack"]=> object(Closure)#1 (0) { // $this->prepareDestination($destination) } ["pipe"]=> string(15) "Illuminate\Foundation\Http\Middleware\ConvertEmptyStringsToNull" } } 第二次: object(Closure)#id (1) { ["static"]=> array(2) { ["stack"]=> object(Closure)#id (1) { ["static"]=> array(2) { ["stack"]=> object(Closure)#1 (0) { // $this->prepareDestination($destination) } ["pipe"]=> string(15) "Illuminate\Foundation\Http\Middleware\ConvertEmptyStringsToNull" } } ["pipe"]=> string(15) "App\Http\Middleware\TrimStrings" } } 第三次: object(Closure)#id (1) { ["static"]=> array(2) { ["stack"]=> object(Closure)#id (1) { ["static"]=> array(2) { ["stack"]=> object(Closure)#id (1) { ["static"]=> array(2) { ["stack"]=> object(Closure)#1 (0) { // $this->prepareDestination($destination) } ["pipe"]=> string(15) "Illuminate\Foundation\Http\Middleware\ConvertEmptyStringsToNull" } } ["pipe"]=> string(15) "App\Http\Middleware\TrimStrings" } } ["pipe"]=> string(15) "Illuminate\Foundation\Http\Middleware\ValidatePostSize" } }
依次类推,最终得到一个匿名函数如下(接受一个参数, 此匿名函数内部使用上面递归形式的$pipe和$stack )。
function ($passable) { if ($pipe instanceof Closure) { return $pipe($passable, $stack); } elseif (! is_object($pipe)) { list($name, $parameters) = $this->parsePipeString($pipe); $pipe = $this->getContainer()->make($name); // 实例化middleware $parameters = array_merge([$passable, $stack], $parameters); } else { $parameters = [$passable, $stack]; } return $pipe->{$this->method}(...$parameters); // 语法糖模式,因为middleware参数可有可无 };
最终匿名函数的调用过程(从最外层开始,这就是前面为什么要array_reverse,一层一层往里拨,整体上的处理是:$pipe若是匿名函数,直接调用。若是字符串,则解析成对应的类和参数,make类,再组成参数数组。最后调用$pipe->handle)
最外层($pipe=Illuminate\Foundation\Http\Middleware\CheckForMaintenanceMode): (new \Illuminate\Foundation\Http\Middleware\CheckForMaintenanceMode)->handle($passable, $stack) 执行完前置操作后,调用$stack($passable),继续进行下一层 下一层($pipe=Illuminate\Foundation\Http\Middleware\ValidatePostSize): (new \Illuminate\Foundation\Http\Middleware\ValidatePostSize)->handle($passable, $stack) 执行完前置操作后,调用$stack($passable),继续进行下一层 **以此类推,当每层中间件的前置任务全部完成,即递归执行到最里面一层(路由分发,解析请求,返回响 应),再由最内层一层一层往回走,执行每层中间件的后置任务。至此,返回本次请求的响应。**
最内层(即路由分发,解析请求,返回响应)的操作代码展示,后续分析:即执行$destination($passable)匿名函数,位于下面方法中。
protected function prepareDestination(Closure $destination) { return function ($passable) use ($destination) { return $destination($passable); }; } $destination: protected function dispatchToRouter() { return function ($request) { $this->app->instance('request', $request); return $this->router->dispatch($request); }; }
说明:所有的后置操作,都是执行到最内层,递归往回走时才会执行逐层执行。

软件开发
2020-06-10 17:32:00
一朝入IT,帽子头上带.
这一路走来过程多艰难心酸,多少次有过想退却的想法,还有哪些迷茫,怀疑自己的阶段
相信你,也一定有过 .
阶段1:只会增删改查:
时间:大学期间(2008年-2009年)
学习的方式:看视频、看书。(学会了使用控件的增删改查,和写SQL语句)。
自我感觉:XX管理系统已不在话下,反正网站弄来弄去也是增删改查,这大概就是无知者无畏。
如何破:(走出社会去就好了)。
阶段2:能快速实现功能,却不会思考:
时间:(2009年-20012年)
学习的方式:看博客、大量的编码(第1年,封闭式开发(1天敲15个小时代码),第二年后:每周六天,每天到夜里3点)。
感觉:大量的编码,做了大量的不同类型的系统,已练就成一个高级代码工:(CodeSmith代码生成器+批量生成增删改查)。
如何破:(让自己静下来)。
阶段3:学会了分享、安全、性能、底层原理
时间:(20012年-2016年)
学习的方式:写博客、看博客、写框架(CYQ.Data V-V4系列、QBlog)。
感觉:为某个框架代码而自我得意。
如何破:不断重复的将自己归零(把自己的认为得意的东西开源出去,如果要保留一点感觉,可阶段性开源)。
阶段4:领悟了抽象思维、面向对象、写框架、用户体验
时间:(2016年-至今)
感觉:也许我懂的很多,但是我不懂的更多。
如何破:(转行卖烧烤。。。)
总结一下:
上面的内容,其实只是个大概的节点情况,太多细节点的过程,来不及细说,全面汇总成以下几点:
1:大量的编码实战。
2:开发自己的作品。
3:学会分享。
4:学会低调,知识归零。
关于看书:
这么多年,我很少看技术类的书,因为人的特征本各不相同,我不善于记忆,只能通过大量的实战来形成条件反射。
另外,从实战到=》互联网的文章,有目的性的补充知识,更加容易被吸引。
所以,看书和看文章,其实本质都是在学习,只是我是通过实战驱动去补充知识,而不是单纯的看书,然后选择被遗忘。
以下内容随意聊聊::
这些年,曾高调过,曾低调过,也不高不低过,有人看我很谦虚,有人看我屌,其实,都是看心情,看心情的。。。
我想汇总一下我的知识体系,身体又懒的动,只因大脑感觉要写完天都要亮好几回,若不写,这随谈又要谈什么呢?
听说Java界的架构师很少,其实是Java的技术总监或CTO太少,毕竟是上层决定了下层的技术使用。
在我这几年当总监的岁月里,拿到的Offer最高月薪也只有4W,这在Python界轻松就能突破的数字,在Java界却不那么容易。
因此,从现实出发,混到总监的,有些也不在乎技术了,也没有技术情节和忠诚感,转个技术方向突破薪资才是硬道理。
生态圈的良性循环,还是得靠占据总监位置的同志们,坚持。。。
而且,大量的Java,必须突破自己,靠架构师上靠,才能进一步稳住市场。
目前Java大多数都在1W左右2W以下的薪资,勉强占领高级程序员或技术经理这么个角色。
追期根源,是因为技术上没有亮点,技术还停留在能做功能的地步,技术和思想也没形成体系。
怎么成长才能突破?
忽略其它综合因素,比较明显的是以下两个层面:
1:技术上的成长。
2:岗位上的成长。
明显决定薪水的,是岗位,要记得:上层给你一个Title让你带队,也好过给你涨薪,这是你跳槽的最有谈判的资本。
然后说说技术,感觉技术成长不上去的时候,怎么办?
先说说现在年轻人最大的感觉,很多人在用我的开源框架,但是他们遇到问题时,很多都是直接上问题,不愿调试代码。
这样的方式,只是完成了任务,就算最终学完整学会了用框架,也只是在众多框架中学会了一个,成长性是很小的。
过程是很重要的,哪怕是弯路,哪怕浪费时间,记得那都是思维的成长,比你完成一个当前的任务更值钱。
都说人生苦短,技术无涯,底层原理下面还有底层原理,无限的底层下,究竟学到哪层,才算底层?
在这框架百出的年代,若每样都要掌握,深度必无,人生必毁,所以,怎么学习,已经成为了一个值的思考的问题了。
其实,这些年,对于自己的成长,无非就是一样,将已有所有知识点形成体系。
当然,我并不是有意而为之,而是大量的实战和知识补充,最终自然而形成。
所以,学习应该围绕着体系而进行,适当的学习底层,是为了建立体系的连结点,而不是为追底层而底层。
今天跟大家分享一份Java 工程师进阶知识点扫盲清单,中间涵盖了redis/mongodb/dubbo/zookeeper/kafka 高并发、高可用、分布式、微服务等众多技术。
当然还有Java 工程师软实力方面的内容,比如说:面试技巧、技术文档编写、中英文档阅读、数据结构与算法等方面的内容。
这里先给大家列举一些分布式的内容:
Redis 从入门到精通,Redis的数据结构分析 Redis主从复制原理及无磁盘复制分析 Redis管道模式详解 Redis缓存与数据库一致性问题解决方案 基于Redis实现分布式锁实战 图解Redis中的AOF和RDB持久化策略的原理 Redis读写分离架构实践 Redis哨兵架构及数据丢失问题分析 Redis luster数据分布算法之 Hash Slot Redis使用常见问题及性能优化思路 Redis高可用及高伸缩架构实战 缓存击穿,缓存雪崩预防策略 Redis批量查询优化 Redis高性能集群之Twemproxy or codis
Dubbo Dubbo管理中心及监控平台安装部署 Dubbo分布式服务模块划分(领域驱动) 基于Dubbo的分布式系统架构实战 Dubbo负载均衡策略分析 Dubbo服务调试之服务只订阅及服务只注册配置 Dubbo服务接口的设计原则(实战经验分享) Dubbo设计原理及源码分析 基于Dubbo构建大型分布式电商平台实战雏形 Dubbo容错机制及高扩展性分析
Zookeeper 从0开始搭建3个节点的Zookeeper集群 深入分析Zookeeper在disconf配置中心的应用 基于Zookeeper的分布式锁解决方案 Zookeeper Watcher核心机制深入源码分析 Zookeeper集群升级,迁移 基于Zookeeper实现分布式服务器动态上下线感知 深入分析Zookeeper Zab协议及选举机制源码解读
Kafka Kafka基于Zookeeper搭建高可用集群实战 Kafka消息处理过程剖析 Java客服端实现Kafka生产者与消费者实例 Kafka的副本机制及选举原理剖析 基于Kafka实现应用日记实时上报统计分析
mongoDB mongoDB应用场景及实现原理 mongoDB常用命令及配置 基于mongoDB手写ORM框架 基于mongoDB GirdFS实现一个分布式网盘实战 mongoDB高可用实战及V4.0新提醒
关于这份Java工程师进阶清单的问题
上述的内容只是整个清单中的一小部分,这里只是列举一些,供大家参考,更加详细的内容,这边有一份高清的技术脑图,可以分享给大家,领取的话,看文末就好了
关于软实力方面的内容,这里也列举一下内容:
面试技巧 如何写出一份漂亮的简历 如何高效准备面试 如何优雅谈薪 如果精准跳槽 辞职的正确姿势 如何规划自己的职业发展
技术文档编写 需求文档编写技巧 Java开发规范文档编写技巧 数据库设计文档编写技巧
中英文档阅读 一小时掌握英语学习技巧 专业英语单词记忆技巧
个人认为,作为技术人就要保持终生学习的态度,让学习力成为核心竞争力,才能不被时代所淘汰,而高效的时间支配能让你变得更加优秀,所以,这里也送给大家一份学习路线图。
上面的这些面试题都整理成了PDF文档,希望能帮助到你面试前的复习并找到一个好的工作,相对来说也节省了你在网上搜索资料的时间来学习!!!
附欢迎关注我的公种号:it资源之家 ,扫描下面二维码即可领取更多一线大厂Java面试题资料!
最后
欢迎大家评论区一起交流,相互提升;整理资料不易,如果喜欢文章记得点个赞哈,感谢大家支持!!!
软件开发
2020-06-10 17:05:00
1 FreeRTOS任务基础
1.1 多任务系统
在51单片机、AVR、STM32裸机编程时,我们通常都是用一个main函数里面加一个while(1)做一个大循环来完成所有的事务处理,同时在加上中断处理一些较为紧急的事务。相对于多任务系统而言,这个就是单任务系统,也称作前后台系统,中断服务函数
作为前台程序,大循环while(1)作为后台程序,如图 1.1.1 所示:

图1.1.1 前后台系统
前后台系统的实时性较大(尤其是调度任务较多),每个任务都是轮流执行,没有轮到该任务运行的时候,不管该任务有多么的紧急,就只能等着,大家拥有一样的优先级。但是该类系统简单,所消耗的资源较少。
多任务系统可以将一个大问题分成很多个具有共性的小问题,逐一的将这些小问题解决,进而大问题将得到全面的解决,我们可将每一个小问题都视为一个任务。这些小任务是并发处理的,由于他们的执行时间很短,我们所能感觉到的是所有的任务都是同时进行的。那么多任务运行的问题就来了,这就涉及到任务执行的先后顺序及什么任务该执行不该执行了。该模块的功能将由任务调度器来完成,具体如何实现,各类系统是有很大差别的,通常来说我们可分为抢占式(UCos、FreeRTOS)和非抢占式(Linux)。FreeRTOS是一个支持抢占式的实时操作系统,其任务运行如图1.1.2所示:
图1.1.2 抢占式多任务系统 1.2 FreeRTOS任务

在使用RTOS的时候一个实时应用可以一个独立的任务,每个任务都有自己的运行环境,CPU在任一时间点只能运行一个任务,具体运行哪一个任务将由任务调度器决定。因此,任务调度器将会不断的开启和关闭任务,任务无需了解RTOS调度器的行为,RTOS调度器的功能是确保一个任务在开始执行的时候与上一次退出的时候的运行环境相同(寄存器值、堆栈内容等),这就是上下文切换(Linux中将该过程称为:进程上下文切换)。学过Linux的就会很清楚的知道每一个进程都拥有自己的堆栈,RTOS也是一样,每一个任务都拥有自己独立的堆栈,当任务发生切换时任务调度器就会将其上下文环境保存在堆栈中,等到该任务拿到CPU的使用权时再从其堆栈中取出所保存的上下文环境,继续运行该任务。
RTOS的任务特性:
任务简单,相对于Linux来说该类任务简单很多;
没有使用限制,任务可以运行无数次;
支持抢占和优先级;
每个任务都拥有独立的堆栈,导致RAM必须较大;
1.3 FreeRTOS任务状态
FreeRTOS中的任务状态有运行态、就绪态、阻塞态、挂起态,但任一任务只能处于这几种状态中的一个。
1.3.1 运行态
当一个任务正在运行时(这一时刻该任务的代码在CPU中执行),那么该任务就处于运行态。如果所使用的是单核CPU,那么不管任何时刻只有一个任务处于运行态,这就证明了CPU在某一时刻只能被一个任务拿到使用权。
1.3.2 就绪态
就绪态是指那些已经准备好了的任务,可以随时拿到CPU的使用权,进而进入运行态,但是此时此刻该状态的任务还没有执行,主要是因为当前有一个同优先级或者更高优先级的任务正在运行。
1.3.3 阻塞态
如果一个任务当前正在等待某一外部事件的发生所处于的状态,如:任务正在等待队列、信号量、事件组等都会进入阻塞态。任务进入阻塞态是有一定的时间限制,当超时等待时,该任务将退出阻塞态,进入就绪态,等待拿到CPU的使用权,进入运行态。
1.3.4 挂起态
任务进入挂起的状态和阻塞态一样,将不会被任务调度器所调用,但是出于挂起态的任务是没有超时的问题。在FreeRTOS中,任务进入和退出挂起态只能通过调用VTaskSuspend()和xTaskReume()。
图1.3.1 任务状态间的转换
1.3.5 任务优先级
FreeRTOS中每一个任务都可以都可以分配从 0~(configMAX_PRIORITIES-1) 的 优 先 级 ,configMAX_PRIORITIES 在文件 FreeRTOSConfig.h 中有定义。如果所使用的硬件平台支持类似计算前导零这样的指令(可以通过该指令选 择 下 一 个 要 运 行 的 任 务 , Cortex-M 处 理 器 是 支 持 该 指 令 的 ) , 并 且 宏configUSE_PORT_OPTIMISED_TASK_SELECTION 也 设 置 为 了 1 , 那 么 宏configMAX_PRIORITIES 不能超过 32!也就是优先级不能超过 32 级。其他情况下宏configMAX_PRIORITIES 可以为任意值,但是考虑到 RAM 的消耗,宏 configMAX_PRIORITIES最好设置为一个满足应用的最小值。
优先级的数字越低表示任务的优先级越低,0的任务优先级最低,configMAX_PRIORITIES-1的优先级最高。空闲任务的优先级最低,为0。
1.3.6 任务控制块
任务控制块(Task Contrl Block,简称TCB),FreeRTOS的每一个任务都有一些属性需要存储,所有的信息将存储在一个结构体中,该结构体叫做任务控制块:TCB_t,在使用xTaskCreate()创建任务时将会自动给每一个任务分配一个任务控制块,此结构体在文件task.c中有定义,具体成员如下:
typedef struct tskTaskControlBlock
{
volatile StackType_t *pxTopOfStack; //任务堆栈栈顶
#if ( portUSING_MPU_WRAPPERS == 1 )
xMPU_SETTINGS xMPUSettings; //MPU 相关设置
#endif

ListItem_t xStateListItem; //状态列表项
ListItem_t xEventListItem; //事件列表项
UBaseType_t uxPriority; //任务优先级
StackType_t *pxStack; //任务堆栈起始地址
char pcTaskName[ configMAX_TASK_NAME_LEN ]; //任务名字

#if ( portSTACK_GROWTH > 0 )
StackType_t *pxEndOfStack; //任务堆栈栈底
#endif

#if ( portCRITICAL_NESTING_IN_TCB == 1 )
UBaseType_t uxCriticalNesting; //临界区嵌套深度
#endif

#if ( configUSE_TRACE_FACILITY == 1 ) //trace 或到 debug 的时候用到
UBaseType_t uxTCBNumber;
UBaseType_t uxTaskNumber;
#endif
#if ( configUSE_MUTEXES == 1 )
UBaseType_t uxBasePriority; //任务基础优先级,优先级反转的时候用到
UBaseType_t uxMutexesHeld; //任务获取到的互斥信号量个数
#endif

#if ( configUSE_APPLICATION_TASK_TAG == 1 )
TaskHookFunction_t pxTaskTag;
#endif

#if( configNUM_THREAD_LOCAL_STORAGE_POINTERS > 0 ) //与本地存储有关
Void *pvThreadLocalStoragePointers[ configNUM_THREAD_LOCAL_STORAGE_POINTERS ];
#endif

#if( configGENERATE_RUN_TIME_STATS == 1 )
uint32_t ulRunTimeCounter; //用来记录任务运行总时间
#endif

#if ( configUSE_NEWLIB_REENTRANT == 1 )
struct _reent xNewLib_reent; //定义一个 newlib 结构体变量
#endif

#if( configUSE_TASK_NOTIFICATIONS == 1 ) //任务通知相关变量
volatile uint32_t ulNotifiedValue; //任务通知值
volatile uint8_t ucNotifyState; //任务通知状态
#endif

#if( tskSTATIC_AND_DYNAMIC_ALLOCATION_POSSIBLE != 0 )
// 用来标记任务是动态创建的还是静态创建的,
// 如果是静态创建的此变量就为 pdTURE,
// 如果是动态创建的就为 pdFALSE
uint8_t ucStaticallyAllocated;
#endif

#if( INCLUDE_xTaskAbortDelay == 1 )
uint8_t ucDelayAborted;
#endif
} tskTCB;
————————————————
版权声明:本文为CSDN博主「楓潇潇」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/u013836909/java/article/details/90259801
软件开发
2020-06-09 16:40:00
作者:coolblog https://segmentfault.com/a/1190000010895869
1. 背景
最近在学习 Zookeeper,在刚开始接触 Zookeeper 的时候,完全不知道 Zookeeper 有什么用。且很多资料都是将 Zookeeper 描述成一个“类 Unix/Linux 文件系统”的中间件,导致我很难将类 Unix/Linux 文件系统的 Zookeeper 和分布式应用联系在一起。
后来在粗读了《ZooKeeper 分布式过程协同技术详解》和《从Paxos到Zookeeper 分布式一致性原理与实践》两本书,并动手写了一些 CURD demo 后,初步对 Zookeeper 有了一定的了解。
不过比较肤浅,为了进一步加深对 Zookeeper 的认识,我利用空闲时间编写了本篇文章对应的 demo – 基于 Zookeeper 的分布式锁实现。通过编写这个分布式锁 demo,使我对 Zookeeper 的 watcher 机制、Zookeeper 的用途等有了更进一步的认识。
不过我所编写的分布式锁还是比较简陋的,实现的也不够优美,仅仅是个练习,仅供参考使用。好了,题外话就说到这里,接下来我们就来聊聊基于 Zookeeper 的分布式锁实现。
2. 独占锁和读写锁的实现
在本章,我将分别说明独占锁和读写锁详细的实现过程,并配以相应的流程图帮助大家了解实现的过程。这里先说说独占锁的实现。 Java中的锁原理、锁优化、CAS、AQS ,这篇文章推荐大家看下。
2.1 独占锁的实现
独占锁又称排它锁,从字面意思上很容易理解他们的用途。即如果某个操作 O1 对访问资源 R1 的过程加锁,在操作 O1 结束对资源 R1 访问前,其他操作不允许访问资源 R1。以上算是对独占锁的简单定义了,那么这段定义在 Zookeeper 的“类 Unix/Linux 文件系统”的结构中是怎样实现的呢?在锁答案前,我们先看张图:
图1 独占锁的 Zookeeper 节点结构
如上图,对于独占锁,我们可以将资源 R1 看做是 lock 节点,操作 O1 访问资源 R1 看做创建 lock 节点,释放资源 R1 看做删除 lock 节点。这样我们就将独占锁的定义对应于具体的 Zookeeper 节点结构,通过创建 lock 节点获取锁,删除节点释放锁。详细的过程如下:
1、多个客户端竞争创建 lock 临时节点 2、其中某个客户端成功创建 lock 节点,其他客户端对 lock 节点设置 watcher 3、持有锁的客户端删除 lock 节点或该客户端崩溃,由 Zookeeper 删除 lock 节点 4、其他客户端获得 lock 节点被删除的通知 5、重复上述4个步骤,直至无客户端在等待获取锁了
上面即独占锁具体的实现步骤,理解起来并不复杂,这里不再赘述。
图2 获取独占锁流程图
2.2 读写锁的实现
说完独占锁的实现,这节来说说读写锁的实现。读写锁包含一个读锁和写锁,操作 O1 对资源 R1 加读锁,且获得了锁,其他操作可同时对资源 R1 设置读锁,进行共享读操作。
如果操作 O1 对资源 R1 加写锁,且获得了锁,其他操作再对资源 R1 设置不同类型的锁都会被阻塞。总结来说,读锁具有共享性,而写锁具有排他性。那么在 Zookeeper 中,我们可以用怎样的节点结构实现上面的操作呢?
图3 读写锁的 Zookeeper 节点结构
在 Zookeeper 中,由于读写锁和独占锁的节点结构不同,读写锁的客户端不用再去竞争创建 lock 节点。所以在一开始,所有的客户端都会创建自己的锁节点。如果不出意外,所有的锁节点都能被创建成功,此时锁节点结构如图3所示。之后,客户端从 Zookeeper 端获取 /share_lock 下所有的子节点,并判断自己能否获取锁。如果客户端创建的是读锁节点,获取锁的条件(满足其中一个即可)如下:
1、自己创建的节点序号排在所有其他子节点前面 2、自己创建的节点前面无写锁节点
如果客户端创建的是写锁节点,由于写锁具有排他性。所以获取锁的条件要简单一些,只需确定自己创建的锁节点是否排在其他子节点前面即可。
不同于独占锁,读写锁的实现稍微复杂一下。读写锁有两种实现方式,各有异同,接下来就来说说这两种实现方式。
读写锁的第一种实现
第一种实现是对 /sharelock 节点设置 watcher,当 /sharelock 下的子节点被删除时,未获取锁的客户端收到 /share_lock 子节点变动的通知。在收到通知后,客户端重新判断自己创建的子节点是否可以获取锁,如果失败,再次等待通知。详细流程如下:
1、所有客户端创建自己的锁节点 2、从 Zookeeper 端获取 /sharelock 下所有的子节点,并对 /sharelock 节点设置 watcher 3、判断自己创建的锁节点是否可以获取锁,如果可以,持有锁。否则继续等待 4、持有锁的客户端删除自己的锁节点,其他客户端收到 /share_lock 子节点变动的通知 5、重复步骤2、3、4,直至无客户端在等待获取锁了
上述步骤对于的流程图如下:
图4 获取读写锁实现1流程图
上面获取读写锁流程并不复杂,但却存在性能问题。以图3所示锁节点结构为例,第一个锁节点 host1-W-0000000001 被移除后,Zookeeper 会将 /share_lock 子节点变动的通知分发给所有的客户端。
但实际上,该子节点变动通知除了能影响 host2-R-0000000002 节点对应的客户端外,分发给其他客户端则是在做无用功,因为其他客户端即使获取了通知也无法获取锁。所以这里需要做一些优化,优化措施是让客户端只在自己关心的节点被删除时,再去获取锁。
读写锁的第二种实现
在了解读写锁第一种实现的弊端后,我们针对这一实现进行优化。这里客户端不再对 /share_lock 节点进行监视,而只对自己关心的节点进行监视。还是以图3的锁节点结构进行举例说明,host2-R-0000000002 对应的客户端 C2 只需监视 host1-W-0000000001 节点是否被删除即可。
而 host3-W-0000000003 对应的客户端 C3 只需监视 host2-R-0000000002 节点是否被删除即可,只有 host2-R-0000000002 节点被删除,客户端 C3 才能获取锁。而 host1-W-0000000001 节点被删除时,产生的通知对于客户端 C3 来说是无用的,即使客户端 C3 响应了通知也没法获取锁。 Zookeeper 集群安装配置,超详细,速度收藏 ,推荐阅读。
这里总结一下,不同客户端关心的锁节点是不同的。如果客户端创建的是读锁节点,那么客户端只需找出比读锁节点序号小的最后一个的写锁节点,并设置 watcher 即可。而如果是写锁节点,则更简单,客户端仅需对该节点的上一个节点设置 watcher 即可。详细的流程如下:
1、所有客户端创建自己的锁节点 2、从 Zookeeper 端获取 /share_lock 下所有的子节点 3、判断自己创建的锁节点是否可以获取锁,如果可以,持有锁。否则对自己关心的锁节点设置 watcher 4、持有锁的客户端删除自己的锁节点,某个客户端收到该节点被删除的通知,并获取锁 5、重复步骤4,直至无客户端在等待获取锁了
上述步骤对于的流程图如下:
图5 获取读写锁实现2流程图
3. 写在最后
本文较为详细的描述了基于 Zookeeper 分布式锁的实现过程,并根据上面描述的两种锁原理实现了较为简单的分布式锁 demo,代码放在了 github 上,需要的朋友自取。
因为这只是一个简单的 demo,代码实现的并不优美,仅供参考。最后,如果你觉得文章还不错的话,欢迎点赞。如果有不妥的地方,也请提出来,我会虚心改之。
推荐去我的博客阅读更多:
1. Java JVM、集合、多线程、新特性系列教程
2. Spring MVC、Spring Boot、Spring Cloud 系列教程
3. Maven、Git、Eclipse、Intellij IDEA 系列工具教程
4. Java、后端、架构、阿里巴巴等大厂最新面试题
觉得不错,别忘了点赞+转发哦!
软件开发
2020-06-09 15:29:00
1. ActiveMQ 高可用集群方案介绍
http://activemq.apache.org/clustering.html 基于jdbc 基于kahaDB http://activemq.apache.org/replicated-message-store.html http://activemq.apache.org/pure-master-slave.html http://activemq.apache.org/networks-of-brokers.html
2. Master-Slave 部署方式
1. Shared filesystem Master-Slave 部署方式
2. Shared database Master-Slave 部署方式
因 ActiveMQ 默认不支持数据库存储,需要手动添加数据库驱动。在 ${ACTIVEMQ_HOME}/lib/extra/ 下放入 mysql-connector-java-xxx.jar 驱动。 节点,设置 persistent="true" ,开启持久化。 设置数据库适配,指定 mysql 数据源。 想要多台,直接复制 activemq 目录到其他服务器即可。 代码连接 mq 地址配置。 // 非failover的公共参数配置通过nested.*,例如 failover:(...)?nested.wireFormat.maxInactivityDuration=1000 // randomize=false 随机选择,默认是顺序 // 指定优先切换 failover:(tcp://host1:61616,tcp://host2:61616,tcp://host3:61616)?priorityBackup=true&priorityURIs=tcp://local1:61616,tcp://local2:61616 // maxReconnectDelay重连的最大间隔时间(毫秒) String brokerUrl = "failover:(tcp://activemq.tony.com:61616,tcp://activemq-slave.tony.com:61616)?initialReconnectDelay=100";
3. Replicated LevelDB Store 方式(弃用)
3. Broker-Cluster 部署方式 官方地址: http://activemq.apache.org/networks-of-brokers.html
该方式有一个问题,假如某一个数据库宕机,数据将变得不可用。 通过网络连接,将多个 broker 组合,对外构成一个整体,集群间共享队列和主题列表。
Static Broker-Cluster 部署配置 在 ${ACTIVEMQ_HOME}/conf/activemq.xml 中静态指定 Broker 需要桥连接的其他 Broker。 在所有节点中添加 networkConnector 节点,uri 地址为集群内其他节点的信息。 所有的 broker 都启动后,在 web 管理界面的 Network 页面可以看到连接信息。
Dynamic Broker-Cluster 部署配置 在 ${ACTIVEMQ_HOME}/conf/activemq.xml 中静态指定 Broker 需要桥连接的其他 Broker。由 activemq 启动后动态查找。(基于 multicast 技术,确保网络畅通) 首先在 Broker 节点中添加 networkConnector 节点。 修改 transportConnector,增加 discoveryUri 属性,并添加 publishedAddressPolicy。
4. Master-Slave 和 Broker—Cluster 相结合的部署方式
部署配置 修改 ${ACTIVEMQ_HOME}/conf/activemq.xml ,在 Broker 节点中添加多个 networkConnector,连接到多个 broker 集群。 ......
5. networkConnector 网络连接器配置说明
1. networkConnector 配置属性说明
属性 默认值 描述 name bridge 名称
dynamicOnly false 如果为 true,持久订阅被激活时才创建对应的持久订阅。
decreaseNetworkConsumerPriority false 如果为 true,网络的消费者优先级降低为 -5。
如果为 false,则默认跟本地消费者一样为 0。
networkTTL 1 消费和订阅在网络上通过的 broker 数量。
conduitSubscription true 多个网络消费者是否被当做一个消费者来对待。
excludedDestinations empty 不通过网络转发的 destination。
dynamicallyIncludedDestinations empty 通过网络转发的 destination,注意空列表代表所有的都转发。
staticallyIncludedDestinations empty 匹配的都将通过网络转发-即使没有对应的消费者。
duplex false 如果为 true,则既可以消费有课生产消息到网络的 broker。
prefetchSize 1000 设置网络消费者的 prefetch size 参数。
必须大于 0,因为网络消费者不能自己轮询消息。
suppressDuplicateQueueSubscriptions false 如果为 true,重复的订阅关系一产生即被阻止。(注意:从 5.3 版本开始)
bridgeTempDestinations true 是否广播 advisoty messages 来创建临时的 destination。
alwaysSyncSend
staticBridge
false
false
如果为 true,非持久化消息也将使用 request/reply 方式替代 oneway 方式发送到远程 broker。(注意:从 5.6 版本开始)
如果为 true,只有 staticallyIncludedDestinations 中配置的 destination 可以被处理。(注意:从 5.6 版本开始)
软件开发
2020-06-09 07:15:00
类加载器
简单讲,类加载器ClassLoader的功能就是负责将class文件加载到jvm内存。 类加载器分类
从虚拟机层面讲分为两大类型的类加载器,一是Bootstrap Classloader即启动类加载器(C++实现),它是虚拟机的一部分,二是其他类型类加载器(JAVA实现),在虚拟机外部,并全部继承ClassLoader类。
从细分的角度讲会分为以下三类类加载器:
1、Bootsrap ClassLoader
启动类加载器,完全由jvm控制加载,外面访问不到这个类加载器,即不能被java程序引用。它主要负责加载jvm自身的工作类,即java/lib目录和-Xbootclasspath参数指定的目录的类库。
2、Extension ClassLoader
扩展类加载器,由java实现,即ExtClassLoader实现类。它主要负责加载java/lib/ext目录和系统环境变量java.ext.dirs指定目录所有类库。
3、Application ClassLoader
应用程序类加载器,由java实现,即AppClassLoader实现类。它的父类是ExtClassLoader,它主要负责加载classpath目录上的类库。如果没有自定义ClassLoader,它就是程序中默认的ClassLoader,即可以通过ClassLoader.getSystemClassLoader()获取当前系统的类加载器。
从上图看虽然Bootstrap ClassLoader是最顶层的类加载器,但是不能被程序引用,它也不是ExtClassLoader的父类加载器,ExtClassLoader没有父类载器,我们不防来看下面简单的例子。
程序首先输出了程序默认的类加载器AppClassLoader,然后再输出了其父类加载器ExtClassLoader,然后就完了,这就证实了上面的理论。 类加载机制
虽然定义了上面这几个类加载器,但在加载时类加载器会审查一个class类应该由哪个类型的加载器负责加载,它使用的是等级加载机制,是一种双亲委派模型。
双亲委派模式要求所有类加载器,除了顶层的Bootstrap类加载器之外都要有自己的父类加载器。在收到一个类加载请求时,当前默认的类加载器它不会首先自己来加载这个类,它会委托给自己的父类加载器去加载,父类加载器再委托给父父类加载器,以此类推,直到顶层类加载器,由上到下加载,除非上面的类加载器都无法加载时自己才去加载。
来看看ClassLoader.loadClass方法源码
再回到之前文章中的有一道关于是否可以自定义类java.lang.String并使用的面试题,它在java/lib目录下,所以当应用类加载器去classpath加载时会去委托父类加载器,这时最顶层类加载器会发现自己之前已经加载过,所以这次不再加载,所以自定义的这个java.lang.String虽然可以正常编译,但不能被类加载器加载并使用。
所以,这也是双亲委派模式的好处,同一个路径的类保证不能加载两次,保证了类与类之间的正常行为和正常运行。
推荐去我的博客阅读更多:
1. Java JVM、集合、多线程、新特性系列教程
2. Spring MVC、Spring Boot、Spring Cloud 系列教程
3. Maven、Git、Eclipse、Intellij IDEA 系列工具教程
4. Java、后端、架构、阿里巴巴等大厂最新面试题
觉得不错,别忘了点赞+转发哦!
软件开发
2020-06-08 17:29:00
写此文的起因是看到一个面试题:默认hashCode()返回的值是什么?GC后hashCode会改变吗?
在以前的认知里,默认hashCode()的返回值是java对象的内存地址没错。然而这又是一个经不起推敲的结论。
首先把一个对象作为key存入hashmap,如果hashCode返回内存地址,那么GC后地址必然改变,那么用这个对象就取不到值了,这显然是不合理的。
不知为何这样的谬误会存在脑子里这么久,从来没有怀疑过它。
既然想到这里,就再简单剖析一下hashCode原理吧。
1、预备知识
1.1 safepoint
GC相关。VM线程触发GC时,需要先 stop-the-world ,说人话就是设置一个safepoint。工作线程会时不时检查safepoint状态,一旦发现safepoint被设置就将自己挂起。VM线程发现工作线程都挂起以后就执行GC,完事后再唤醒工作线程。
用现实情况举例就是:leader在群里发了一条"一会儿开会"的消息(safepoint),每个成员时不时查看消息,看到消息第一时间放下手中工作前往会议室(线程挂起),然而有个人沉浸编码无法自拔,十分钟后才看到信息,结果导致会议延迟(GC期间程序暂停时间过长)
至于底层具体如何实现,呃,我没看懂,感兴趣的同学自行了解 https://www.jianshu.com/p/c79c5e02ebe6
1.2 Mark Word
就是Java对象头里面的 Mark Word 转载,下面给出原文地址 |-------------------------------------------------------|--------------------| | Mark Word (32 bits) | State | |-------------------------------------------------------|--------------------| | identity_hashcode:25 | age:4 | biased_lock:1 | lock:2 | Normal | |-------------------------------------------------------|--------------------| | thread:23 | epoch:2 | age:4 | biased_lock:1 | lock:2 | Biased | |-------------------------------------------------------|--------------------| | ptr_to_lock_record:30 | lock:2 | Lightweight Locked | |-------------------------------------------------------|--------------------| | ptr_to_heavyweight_monitor:30 | lock:2 | Heavyweight Locked | |-------------------------------------------------------|--------------------| | | lock:2 | Marked for GC | |-------------------------------------------------------|--------------------|
需要了解的几个点: Mark Word 存储的数据会随着状态(右列)的变化而改变 identity_hashcode 是延迟加载的 当对象被锁定时(非Normal状态), identity_hashcode 会移动到管程 Monitor 中 https://www.jianshu.com/p/3d38cba67f8b
2、源码分析
2.1 FastHashCode
从hashCode源码一路往下找,很容易就找到了 FastHashCode
openjdk11\src\hotspot\share\runtime\synchronizer.cpp intptr_t ObjectSynchronizer::FastHashCode(Thread * Self, oop obj) { if (UseBiasedLocking) { // == safepoint相关,英文不好就不翻译了 == // NOTE: many places throughout the JVM do not expect a safepoint // to be taken here, in particular most operations on perm gen // objects. However, we only ever bias Java instances and all of // the call sites of identity_hash that might revoke biases have // been checked to make sure they can handle a safepoint. The // added check of the bias pattern is to avoid useless calls to // thread-local storage. if (obj->mark()->has_bias_pattern()) { // Handle for oop obj in case of STW safepoint Handle hobj(Self, obj); // Relaxing assertion for bug 6320749. assert(Universe::verify_in_progress() || !SafepointSynchronize::is_at_safepoint(), "biases should not be seen by VM thread here"); BiasedLocking::revoke_and_rebias(hobj, false, JavaThread::current()); obj = hobj(); assert(!obj->mark()->has_bias_pattern(), "biases should be revoked by now"); } } ObjectMonitor* monitor = NULL; markOop temp, test; intptr_t hash; markOop mark = ReadStableMark(obj); // == 读对象头MarkWord == if (mark->is_neutral()) { // == 判断是否normal状态 == hash = mark->hash(); // == 从mark里取hash == if (hash) { // == 已经存在,直接返回 == return hash; } hash = get_next_hash(Self, obj); // == 真正生成hash的方法 == temp = mark->copy_set_hash(hash); // == 复制mark串并将新的hash写入 == // use (machine word version) atomic operation to install the hash // 下面这段是CAS常规操作,Java源码里也随处可见,一定要熟悉 // 结合ConcurrentHashMap可以给某些业务加分段锁,非常好用 test = obj->cas_set_mark(temp, mark); // == 通过CAS将新Mark设置给obj,返回设置前的值 == if (test == mark) { // == 如果设置前的值和刚开始读到的值相等,表明期间没被其他线程修改过 == return hash; } // If atomic operation failed, we must inflate the header // into heavy weight monitor. We could add more code here // for fast path, but it does not worth the complexity. } else if (mark->has_monitor()) { // == 判断加锁(重量级锁) == // == 前文提到加锁的对象`identity_hashcode`会移动到管程`Monitor`中 == monitor = mark->monitor(); // == 从monitor中获取hashcode == temp = monitor->header(); assert(temp->is_neutral(), "invariant"); hash = temp->hash(); if (hash) { return hash; } // Skip to the following code to reduce code size } else if (Self->is_lock_owned((address)mark->locker())) { // == 判断是否锁重入 == temp = mark->displaced_mark_helper(); // this is a lightweight monitor owned assert(temp->is_neutral(), "invariant"); hash = temp->hash(); // by current thread, check if the displaced if (hash) { // header contains hash code return hash; } // WARNING: // The displaced header is strictly immutable. // It can NOT be changed in ANY cases. So we have // to inflate the header into heavyweight monitor // even the current thread owns the lock. The reason // is the BasicLock (stack slot) will be asynchronously // read by other threads during the inflate() function. // Any change to stack may not propagate to other threads // correctly. } // == 无锁或轻量级锁无法获得hash,则升级为重量级锁 == // == 基本逻辑和上面相似,不再赘述 == // Inflate the monitor to set hash code monitor = ObjectSynchronizer::inflate(Self, obj, inflate_cause_hash_code); // Load displaced header and check it has hash code mark = monitor->header(); assert(mark->is_neutral(), "invariant"); hash = mark->hash(); if (hash == 0) { hash = get_next_hash(Self, obj); temp = mark->copy_set_hash(hash); // merge hash code into header assert(temp->is_neutral(), "invariant"); test = Atomic::cmpxchg(temp, monitor->header_addr(), mark); if (test != mark) { // The only update to the header in the monitor (outside GC) // is install the hash code. If someone add new usage of // displaced header, please update this code hash = test->hash(); assert(test->is_neutral(), "invariant"); assert(hash != 0, "Trivial unexpected object/monitor header usage."); } } // We finally get the hash return hash; }
2.2 get_next_hash
核心逻辑。根据全局hashCode的值决定使用哪种算法。可以通过配置 -XX:hashCode=[0-5] 修改。
openjdk11\src\hotspot\share\runtime\synchronizer.cpp static inline intptr_t get_next_hash(Thread * Self, oop obj) { intptr_t value = 0; // == hashCode是一个全局变量,指明要使用的hashCode算法 == if (hashCode == 0) { // This form uses global Park-Miller RNG. // On MP system we'll have lots of RW access to a global, so the // mechanism induces lots of coherency traffic. value = os::random(); // == 0 使用系统随机数 == } else if (hashCode == 1) { // This variation has the property of being stable (idempotent) // between STW operations. This can be useful in some of the 1-0 // synchronization schemes. // == 1 在内存地址基础上进行二次运算 == intptr_t addrBits = cast_from_oop(obj) >> 3; value = addrBits ^ (addrBits >> 5) ^ GVars.stwRandom; } else if (hashCode == 2) { // == 2 测试用途 == value = 1; // for sensitivity testing } else if (hashCode == 3) { value = ++GVars.hcSequence; // == 3 自增序列 == } else if (hashCode == 4) { value = cast_from_oop(obj); // == 4 使用内存地址 == } else { // == 5 某个"名字说了你也记不住"的算法,默认使用这个算法 == // Marsaglia's xor-shift scheme with thread-specific state // This is probably the best overall implementation -- we'll // likely make this the default in future releases. unsigned t = Self->_hashStateX; t ^= (t << 11); Self->_hashStateX = Self->_hashStateY; Self->_hashStateY = Self->_hashStateZ; Self->_hashStateZ = Self->_hashStateW; unsigned v = Self->_hashStateW; v = (v ^ (v >> 19)) ^ (t ^ (t >> 8)); Self->_hashStateW = v; value = v; } value &= markOopDesc::hash_mask; if (value == 0) value = 0xBAD; assert(value != markOopDesc::no_hash, "invariant"); TEVENT(hashCode: GENERATE); return value; }
2.3 MarkWord
openjdk11\src\hotspot\share\oops\markOop.cpp // 通过源码可以简单看出32位与64位的区别 // 本次分析以32位为例 class markOopDesc: public oopDesc { private: // Conversion uintptr_t value() const { return (uintptr_t) this; } public: // 字段大小 // Constants enum { age_bits = 4, // age lock_bits = 2, // lock biased_lock_bits = 1, // biased_lock max_hash_bits = BitsPerWord - age_bits - lock_bits - biased_lock_bits, // 32-4-2-1=25 hash_bits = max_hash_bits > 31 ? 31 : max_hash_bits, // identity_hashcode:25 cms_bits = LP64_ONLY(1) NOT_LP64(0), // 64位包含1位unused,32位没有 epoch_bits = 2 // Biased状态下的epoch }; // 字段所在位置偏移 // The biased locking code currently requires that the age bits be // contiguous to the lock bits. enum { lock_shift = 0, biased_lock_shift = lock_bits, // << 2 age_shift = lock_bits + biased_lock_bits, // << 3 cms_shift = age_shift + age_bits, // << 7 hash_shift = cms_shift + cms_bits, // << 7 (32位) epoch_shift = hash_shift }; // 计算掩码 enum { lock_mask = right_n_bits(lock_bits), // b11 lock_mask_in_place = lock_mask << lock_shift, // b11 biased_lock_mask = right_n_bits(lock_bits + biased_lock_bits), // b111 biased_lock_mask_in_place= biased_lock_mask << lock_shift, // b111 biased_lock_bit_in_place = 1 << biased_lock_shift, // b100 age_mask = right_n_bits(age_bits), // b1111 age_mask_in_place = age_mask << age_shift, // b111_1000 epoch_mask = right_n_bits(epoch_bits), // b11 epoch_mask_in_place = epoch_mask << epoch_shift, // b1_1000_0000 cms_mask = right_n_bits(cms_bits), // b1 cms_mask_in_place = cms_mask << cms_shift // b1000_0000 #ifndef _WIN64 // 32位 ,hash_mask = right_n_bits(hash_bits), // b1_11111111_11111111_11111111 hash_mask_in_place = (address_word)hash_mask << hash_shift // b11111111_11111111_11111111_10000000 #endif }; .... } // 没找到right_n_bits源码,随便写了个Java版的 public static int right_n_bits(int n) { int x = 1; for (int i = 1; i < n; i++) { x = (x << 1) + 1; } return x; }
2.4 copy_set_hash
openjdk11\src\hotspot\share\oops\markOop.cpp // 拷贝Mark值的副本,并将hash填入 // (不确定,从逻辑上看应该是拷贝了,但看tmp变量名和value()的定义又像指针操作) markOop copy_set_hash(intptr_t hash) const { // 假设传入hash值为 b01010101 /* hash_mask_in_place b11111111_11111111_11111111_10000000 ~ b00000000_00000000_00000000_01111111 value() b00010010_00010001_00011000_00001001 假定值 & b00000000_00000000_00000000_00001001 => tmp */ intptr_t tmp = value() & (~hash_mask_in_place); /* hash b0_00000000_00000000_01010101 hash_mast b1_11111111_11111111_11111111 & b0_00000000_00000000_01010101 截掉超出的位数 << 7 b00000000_00000000_00101010_10000000 tmp b00000000_00000000_00000000_00001001 |= b00000000_00000000_00101010_10001001 > tmp */ tmp |= ((hash & hash_mask) << hash_shift); return (markOop)tmp; }
2.5 cas_set_mark markOop oopDesc::cas_set_mark(markOop new_mark, markOop old_mark) { // atomic_cmpxchg_at 未找到源码 // 推测返回值为修改前的值 return HeapAccess<>::atomic_cmpxchg_at(new_mark, as_oop(), mark_offset_in_bytes(), old_mark); }
这段代码没什么好说的,和我们常用的 AtomicXXX 很相似 AtomicReference ref = new AtomicReference<>("A"); String old = ref.compareAndExchange("A", "B"); // V compareAndExchange(V expectedValue, V newValue) System.out.println(old); // A System.out.println(ref.get()); // B
3、总结 默认情况下,hashCode跟内存地址无关,是通过某种(叫不上名字的)算法计算得来的 hashCode是延迟计算的,一旦设置,不会改变 可以通过配置 -XX:hashCode=[0-5] 更改算法
4、Q&A
Q:我了解这个有用吗?
A: 初/中级工作中:没啥用 高级工作中:不知道,没达到 面试中:能把不了解的面试官说懵逼。当然他不懂也不会问 其他:把程序启动命令改成 -XX:hashCode=2 来坑队友(误。要用在正当用途比如学习)
软件开发
2020-06-08 15:17:00
前天去面试,被问到golang是如何实现高并发的,之前在 GO并发编程实战 这本书看到过介绍,但是没有引起重视。
传统的并发形式:多线程共享内存,这也是Java、C#或者C++等语言中的多线程开发的常规方法,其实golang语言也支持这种传统模式,另外一种是Go语言特有的,也是Go语言推荐的:CSP(communicating sequential processes)并发模型。不同于传统的多线程通过共享内存来通信,CSP讲究的是“以通信的方式来共享内存”。
“不要以共享内存的方式来通信,相反,要通过通信来共享内存。”
go语言使用MPG模式来实现CSP
在传统的并发中起很多线程只会加大CPU和内存的开销,太多的线程会大量的消耗计算机硬件资源,造成并发量的瓶颈。
M指的是Machine,一个M直接关联了一个内核线程。
P指的是”processor”,代表了M所需的上下文环境,也是处理用户级代码逻辑的处理器。
G指的是Goroutine,其实本质上也是一种轻量级的线程。
我个人的理解:M关联了一个内核线程,通过调度器P(上下文)的调度,可以连接1个或者多个G,相当于把一个内核线程切分成了了N个用户线程,M和P是一对一关系(但是实际调度中关系多变),通过P调度N个G(P和G是一对多关系),实现内核线程和G的多对多关系(M:N),通过这个方式,一个内核线程就可以起N个Goroutine,同样硬件配置的机器可用的用户线程就成几何级增长,并发性大幅提高。
这篇文章详细介绍了相关原理:
https://i6448038.github.io/2017/12/04/golang-concurrency-principle/
————————————————
版权声明:本文为CSDN博主「zzr881」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/zzr881/java/article/details/82533661
软件开发
2020-06-08 14:50:00
Eureka设置服务上下线
被动下线(默认)
等待若干秒后,eureka server自动剔除失效的服务地址。(见Eureka配置优化)
直接执行http请求 shell命令执行(成功:200;失败:404,500) # 如果服务还存活着,那么在一定的时间内,服务会重新注册(非强制) curl -X DELETE "http://[ip]:[port]/eureka/apps/[your_app_name]/[your_instance_id]" -I # 强制上线/下线/移除服务(强制) curl -X PUT "http://192.168.1.248:8761/eureka/apps/CONFIG-SERVER/192.168.1.248:8088/status?value=UP" -I curl -X PUT "http://192.168.1.248:8761/eureka/apps/CONFIG-SERVER/192.168.1.248:8088/status?value=DOWN" -I curl -X PUT "http://192.168.1.248:8761/eureka/apps/CONFIG-SERVER/192.168.1.248:8088/status?value=OUT_OF_SERVICE" -I # 恢复以上强制操作为原始状态(解除强制覆盖:如果不加参数status的话,先初始化为UNKOWN状态,随即更新其真实状态) curl -X DELETE "http://192.168.1.248:8761/eureka/apps/CONFIG-SERVER/192.168.1.248:8088/status?value=[status(,or you can ignore this param)]" -I POSTMAN执行(具体功能同上)
Java程序执行(强制) @RestController public class OfflineController { @GetMapping("/offline") public String offline() { DiscoveryManager.getInstance().shutdownComponent(); return "offline OK"; } }
OR @RestController public class OfflineController { @Autowired private EurekaClient discoveryClient; @GetMapping("/offline2") public String offline2() { discoveryClient.shutdown(); discoveryClient = null; return "offline OK2"; } }
Python程序执行(非强制) pip install py_eureka_client #!/usr/bin/env python3 import eureka_client eureka_client.cancel('http://192.168.1.248:8761/eureka/', app_name, id)
参考: https://pypi.org/project/py-eureka-client/ https://github.com/Netflix/eureka/wiki/Eureka-REST-operations
通过Spring Boot Actuator控制服务上下线
参考: ENDPOINT: shutdown、pause、service-registry
Spring Cloud应用优雅上下线(集成发布系统) 从注册中心强制移除服务, curl -X PUT "http://192.168.1.248:8761/eureka/apps/CONFIG-SERVER/192.168.1.248:8088/status?value=OUT_OF_SERVICE" -I ; 等待若干时间之后(等待各个端和组件对服务列表的缓存过期),发布重启应用(先用kill -15 [pid],若干时间后再按需kill -9 [pid]); 从注册中心恢复服务,覆盖之前的强制状态(修改method即可), curl -X DELETE "http://192.168.1.248:8761/eureka/apps/CONFIG-SERVER/192.168.1.248:8088/status?value=OUT_OF_SERVICE" -I ,等待若干时间即可生效; 按照以上步骤1-3发布其他应用节点(灰度发布)
Eureka配置优化
https://docs.qq.com/doc/DTUJSUW9jUFFpZXp0?appid=101520907&od_appid=101520907&from=weiyun
软件开发
2020-06-08 14:22:00
python 是一门简单直观的语言并与主要竞争者一样强大 开源,以便任何人都可以为它做贡献 代码像纯英语那样容易理解 适用于短期开发的日常任务
软件开发
2020-06-07 20:38:09
1:冒泡算法
思路:相邻两个数字依次比较, /** * @param arr 待排序数组 * @brief 冒泡排序 */ protected static void maoPao(int[] arr) { //外层循环控制需要比较的次数,需要比较的次数,需要减去自身,自己不用跟自己比较了 for (int i = 0; i < arr.length - 1; i++) { //内层循环控制每两个相邻的数需要比较的次数,外层循环一次,就会得到一个最大的数,内层就需要减少一次 for (int i1 = 0; i1 < arr.length - 1 - i; i1++) { int temp; if (arr[i1] >= arr[i1 + 1]) {//假如左边的数比右边的数大 //交换两数的位置 temp = arr[i1]; arr[i1] = arr[i1 + 1]; arr[i1 + 1] = temp; } } } }
2:快速排序,
思路:两边同时像中间靠拢,二分思想 /** * @param arr 待排序数组 * @param left 数组最小索引 * @param right 数组最大索引 * @brief 快速排序(二分思想) */ protected static void fastSort(int[] arr, int left, int right) { //定义左右哨兵位和中间数以及临时变量 int l = left, r = right, temp, t; if (l >= r) {//递归出口 return; } //假设最左边的为中间数,可以随机指定 temp = arr[left]; /** * 从最右边的开始走,一直走到当前位置的数比中间数小的时候,此时右边的开始走,当右边的有比中间数大的时候,左右交换位置,然后第一次交换结束 * 右边开始的继续走,当碰到比中间数小的时候,重复第一次的情况, * 所以可以使用递归加while来解决 * */ //当左哨兵位l小于右边r哨兵位时,说明一次都没有遍历完,因为当l=r时,说明大的已经全部交换到右边,小的全部交换到左边了,除了这种情况以外,都是需要继续遍历,直到l=r while (l < r) { //只要满足右边遍历的数都大于中间数,并且l= temp && l < r) { r--;//r下标一直递减,模拟依次向左遍历 } //只要满足从最左边开始遍历的数都小于中间数,并且l 其他算法,以后再更新
软件开发
2020-06-07 20:38:00
在PHP的预定义接口和方法中, 有一个ArrayAccess(数组式访问)接口。
定义: 像访问数组一样访问对象的能力的接口 interface ArrayAccess { public function offsetExists ( mixed $offset ) : bool public function offsetGet ( mixed $offset ) : mixed public function offsetSet ( mixed $offset , mixed $value ) : void public function offsetUnset ( mixed $offset ) : void }
举一个简单的例子, 用Foo类去实现该接口中的4个方法: class Foo implements ArrayAccess{ public function offsetExists($offset){//isset判断时调用 echo "这里是 offsetExists() 方法 你输入的参数是 {$offset}".PHP_EOL; } public function offsetGet($offset){//获取值时调用 echo "这里是 offsetGet() 方法 你输入的参数是 $offset".PHP_EOL; } public function offsetSet($offset, $value){//设置值时调用 echo "这里是 offsetSet() 方法 你输入的 {$offset}={$value}".PHP_EOL; } public function offsetUnset($offset){//unset时调用 echo "这里是 offsetUnset() 方法 你输入的参数是 {$offset}".PHP_EOL; } }
接口就是一个类的模板, 在其实现类中必须实现所有的方法,否则直接"Fatal error"。我们上面就给类Foo实现了ArrayAccess的4种方法,现在来调用下试试。 $foo = new Foo(); var_dump(isset($foo['name'])); echo $foo['name']; $foo['name'] = 'aben'; unset($foo['name'] );
运行结果是: 这里是 offsetExists() 方法 你输入的参数是 name bool(false) 这里是 offsetGet() 方法 你输入的参数是 name 这里是 offsetSet() 方法 你输入的 name=aben 这里是 offsetUnset() 方法 你输入的参数是 name
在实际的工作中,最常用到ArrayAccess的就属ORM获取数据操作了。 下面是tp5.1中数据集Collection类的代码摘要 /** * 数据集管理类 */ class Collection implements ArrayAccess, Countable, IteratorAggregate, JsonSerializable, Arrayable, Jsonable { .... // ArrayAccess public function offsetExists($offset) { return array_key_exists($offset, $this->items); } public function offsetGet($offset) { return $this->items[$offset]; } public function offsetSet($offset, $value) { if (is_null($offset)) { $this->items[] = $value; } else { $this->items[$offset] = $value; } } public function offsetUnset($offset) { unset($this->items[$offset]); } ... }
该类中实现了ArrayAccess的4种操作, 以及Countable实现数组的count方法。
软件开发
2020-06-07 11:19:00
最近开发过程中遇到了JNI的Reference相关问题,了解到Local Reference和Global Reference的相关知识点,整理如下:
背景 :项目需求,在Native C/C++层调用上层Android Camera Java接口,把所有的操作包括Camera都沉到Native层去实现。但在JNI调试过程中遇到了android JNI ERROR (app bug): accessed stale local reference的报错。
现象 :在Native层创建Java的Camera对象,其对象的指针保存到本地,函数返回到Java层,之后再进入Native层,想通过Native层的Camera对象指针调用相应的方法,但是发现每次都是重新调用Java对象方法后报错。
分析 :在Native层创建的Java对象,对象创建后会有一个局部引用指向该对象,当从Native环境返回到Java环境,该局部引用失效,此对象就没有引用计数,Java的内存回收机制会自动回收该对象,第二次再进入Native层访问其之前保存的地址时就会报错。
解决 :使用全局引用始终持有该对象的引用使其不被自动回收,请看下面的知识点。
Local Reference
局部引用,看如下精简代码: env->NewStringUTF("0");
在JNI中,每次调用NewObject方法创建一个新的对象都会返回一个对该对象的局部引用(Local Reference),该局部引用只在线程当前的Native环境中有效,返回到Java环境后该引用与对象之间的联系就会被断掉,引用失效,所以我们不能在Native方法中把局部引用缓存用于下一次调用时使用。
局部引用可以无限创建吗
如图:
这里引入局部引用表的概念,每当线程从Java环境进入到Native环境后,JVM就会创建该线程Native环境的局部引用表,用来保存本次Native环境所创建的所有局部引用,每当Native中引用或者新创建一个Java对象,JVM就会局部引用表创建一个局部引用,局部引用表是有大小限制的,最大是512,如果超过限制会报OOM内存泄漏。
Q:那如何才能更好的避免由于局部引用过多造成Native环境中的OOM呢?
A:控制局部引用的生命周期,如果需要创建过多的局部引用,可以在Java对象的操作结束后,手动调用DeleteLocalRef函数删除局部引用,该局部引用就会在局部引用表中被移除,避免触发局部引用表的大小限制。
注意 :局部引用不是我们平时所理解的代码中的局部变量,局部变量在当前生命周期(例如函数退出)结束后就会失效,而局部引用在函数退出后可能不会失效,它的生命周期是和整个Native上下文环境相关联,只有从Native环境返回到Java环境后局部引用才会失效。
Global Reference
全局引用,终于到了最上面讨论的问题了,因为局部引用在Native环境返回到Java环境后就会失效,导致下次进入Native环境后再次使用相对应的Java对象就会出错,所以可以使用全局引用来解决这个问题,全局引用可以始终与Java对象保持联系,使得此对象不会被JVM回收掉,见如下代码: JNIEXPORT jint JNICALL JNI_OnLoad (JavaVM * vm, void * reserved) { jclass tmp = env->FindClass("com/example/company/MyClass"); jclass class = env->NewGlobalRef(tmp); return JNI_VERSION_1_6; }
这里需要注意,在不需要使用Java对象后尽量手动调用DeleteGlobalRef()函数来使得引用失效,避免对象始终存在,产生潜在的内存泄漏。
Weak Global Reference
虚全局引用与全局引用的区别在于该类型的引用可能随时被JVM回收掉,这里涉及到几个函数: NewWeakGlobalRef(); DeleteWeakGlobalRef(); isSameObject();
在使用虚引用前需要通过isSameObject将其和NULL比较,如果返回TRUE返回true表示已经被JVM回收掉就不能使用了,这里有可能前一行代码判断还是可用,后一行代码时就被JVM回收,解决办法时通过NewLocalRef()获取虚全局引用,避免当时被JVM回收。
参考资料
https://www.cnblogs.com/zhongshujunqia/p/4638077.html?utm_source=tuicool&utm_medium=referral https://www.cnblogs.com/younghome/p/4609044.html https://stackoverflow.com/questions/14765776/jni-error-app-bug-accessed-stale-local-reference-0xbc00021-index-8-in-a-tabl https://www.ibm.com/developerworks/cn/java/j-lo-jnileak/index.html https://juejin.im/post/5c19bfa0f265da6133568545
更多文章请关注
更多文章,请关注我的V X 公 主 号:程序喵大人,欢迎交流~
软件开发
2020-06-06 21:30:00
2种方式实现:
注意:如果发现运行时java.lang.NoClassDefFoundError:异常就是缺少了jar包。添加对应的jar包就可以。
一、emoji-java-4.0.0.jar实现 com.vdurmont emoji-java 4.0.0 org.json json 20180813 代码: public static void main(String args[]){ String str = "苍天厚土�� �� �� �� ��" ; System.out.println( "原始字符为:\n" + str); System.out.println( "to aliases 之后:" ); System.out.println(EmojiParser.parseToAliases(str)); System.out.println(EmojiParser.parseToAliases(str, EmojiParser.FitzpatrickAction.PARSE)); System.out.println(EmojiParser.parseToAliases(str, EmojiParser.FitzpatrickAction.REMOVE)); System.out.println(EmojiParser.parseToAliases(str, EmojiParser.FitzpatrickAction.IGNORE)); System.out.println(EmojiParser.parseToAliases(str, EmojiParser.FitzpatrickAction.IGNORE)); System.out.println(EmojiParser.parseToUnicode(str)); }
运行结果:
原始字符为:
苍天厚土😁 🐶 🤔 👻 🚒
to aliases 之后:
苍天厚土:grin: :dog: :thinking: :ghost: :fire_engine:
苍天厚土:grin: :dog: :thinking: :ghost: :fire_engine:
苍天厚土:grin: :dog: :thinking: :ghost: :fire_engine:
苍天厚土:grin: :dog: :thinking: :ghost: :fire_engine:
苍天厚土:grin: :dog: :thinking: :ghost: :fire_engine:
苍天厚土😁 🐶 🤔 👻 🚒

二、java-emoji-converter-0.1.1.jar方式 com.thoughtworks.xstream xstream com.github.binarywang java-emoji-converter 0.1.1 代码: public static void main(String args[]){ String str = "苍天厚土�� �� �� �� ��" ; EmojiConverter emojiConverter = EmojiConverter.getInstance(); System.out.println( "原始字符为:\n" + str); System.out.println( "to aliases 之后:" ); System.out.println(emojiConverter.toAlias(str)); System.out.println(emojiConverter.toUnicode(str)); }

运行结果:
原始字符为:
苍天厚土😁 🐶 🤔 👻 🚒
to aliases 之后:
苍天厚土:grin: :dog: :thinking: :ghost: :fire_engine:
苍天厚土😁 🐶 🤔 👻 🚒

备注:
经过测试,神奇的事情发生了,使用方法一加密,方法二可以正常解密。使用方法二加密,方法一可以正常解密。应该是2个jar包底层数据一致吧。

将一些逻辑问题使用代码实现
软件开发
2020-06-29 15:51:00
最近一直在写序列化类,希望把一些游戏过程中的场景数据信息存储到硬盘。因为需要处理的数据很多,所以我写了一个工具类,其中包括字符串的序列化,问题来了:
公司原先的代码参差不齐,有使用AString的(使用ANSI char作为字符单元,相当于std::string),也有考虑到unicode问题而采用AWString的(使用wchar_t作为字符单元,相当于std::wstring),同时考虑到根据编译环境自动视别的问题,也定义有一个宏ACString,即:如果定义有UNICODE环境变量,则自动替换为AWString,否则使用AString。好吧,自作聪明的我使用了ACString来写工具函数,测试时没有任何问题,但一旦加入到真正的运行环境中就会出现“把const char*转化为bool,性能警告”的错误,几经检查,发现在游戏环境下,传入的参数是AString类型,而我用的ACString则被自动转化成了AWString,找不到参数匹配的,所以只有默认转化了。
看来,解决办法只能是同时提供AString与AWString两种类型的工具函数,因为你不知道你的客户传给你的将是哪一种,除非大家都铁了心的一致使用ACString,然而在现实的编码中,这却是不切实际的。
这让我想起前两天为STL写的格式化函数(STL没有,只好自己写了-_-),当时我也想自作聪明的把std::string与std::wstring合二为一,现在想来恐怕是庸人自扰罢了,于是早上起来把原来的代码作下修改,如下:
#include < stdarg.h >
#include < string >
#include < vector >
#include < cassert >
#ifndef __STR_STR_HPP_INCLUDED_
#define __STR_STR_HPP_INCLUDED_
namespace str
{
// 字符串格式化函数
std:: string format( const char * fmt, )
{
std:: string strResult = "" ;
if (NULL != fmt)
{
va_list marker = NULL;
va_start(marker, fmt); // 初始化变量参数
size_t nLength = _vscprintf(fmt, marker) + 1 ; // 获取格式化字符串长度
std::vector < char > vBuffer(nLength, ' \0 ' ); // 创建用于存储格式化字符串的字符数组
int nWritten = _vsnprintf_s( & vBuffer[ 0 ], vBuffer.size(), nLength, fmt, marker);
if (nWritten > 0 )
{
strResult = & vBuffer[ 0 ];
}
va_end(marker); // 重置变量参数
}
return strResult;
}
// 字符串格式化函数
std::wstring format( const wchar_t * fmt, )
{
std::wstring strResult = L "" ;
if (NULL != fmt)
{
va_list marker = NULL;
va_start(marker, fmt); // 初始化变量参数
size_t nLength = _vscwprintf(fmt, marker) + 1 ; // 获取格式化字符串长度
std::vector < wchar_t > vBuffer(nLength, L ' \0 ' ); // 创建用于存储格式化字符串的字符数组
int nWritten = _vsnwprintf_s( & vBuffer[ 0 ], vBuffer.size(), nLength, fmt, marker);
if (nWritten > 0 )
{
strResult = & vBuffer[ 0 ];
}
va_end(marker); // 重置变量参数
}
return strResult;
}
}
#endif
测试全码(其中str.hpp就是上面代码的文件名了):
#include < iostream >
#include < cassert >
#include " str.hpp "
int main( int argc, char * argv[])
{
const double PI = 3.1415926 ;
std:: string s = str::format( " ANSI%s---字符:%c;整数:%d;双精度:%f " , " 测试 " , ' a ' , 38 , PI);
std::cout << s << std::endl;
std::wstring ws = str::format(L " UNICODE%s---字符:%c;整数:%d;双精度:%f " , L " 测试 " , ' u ' , 83 , PI);
std::wcout << ws << std::endl;
system( " pause " );
return 0 ;
}
输出结果:
问题一:前面的format函数只能在vs2005下通过编译,在vc6.0下就不可以,原因是 用于“获取格式化字符串长度”的_vscprintf和_vscwprintf在vc6.0中找不到替代品;
问题二:相信大家已经看到了,ANSI字符串的测试没有任何问题,但UNICODE字符串在输出时中断了,确切的说是在输出到中文时中断了,我用断点查看过,ws的构造没有问题,即wchar_t版的format函数没有问题,那么问题就应该出在std::wcout上面,但问题在哪呢?
诸位看官,欢迎提供任何指导意见。
软件开发
2020-06-29 15:38:00
https://blog.csdn.net/Mote123/article/details/106488538
npm config set registry https://registry.npm.taobao.org
首先需要将npm的包下载地址改为淘宝镜像地址:
npm config set registry https://npm.taobao.org/mirrors/node
下载node源码加速:
npm config set disturl https://npm.taobao.org/mirrors/node
然后将electron包下载地址注册位淘宝的镜像:
npm config set ELECTRON_MIRROR https://npm.taobao.org/mirrors/electron/
软件开发
2020-06-29 14:17:00

将markdown文件中的信息提取为json格式
代码 const fs = require('fs') const content = fs.readFileSync('./t.md', 'utf8') let re = /---(.*?)---/sg const yaml = require('js-yaml'); let s = re.exec(content)[1] console.log(yaml.safeLoad(s))

提取结果 { date: '2018-11-7', tag: [ 'frontmatter', 'vuepress' ], author: 'ULIVZ', location: 'Hangzhou' }
软件开发
2020-06-28 17:54:00
[TOC]
名词解释 领域驱动设计 核心是不断提炼通用语言并用于与领域专家等团队所有成员交流,并用代码来表达出一个与通用语言一致的领域模型。 通用语言 通过团队交流达成共识的能够简单清晰准确传递业务规则的语言(可以是文字、图片等)。 领域 软件系统要解决的问题域,是有边界的。领域一般包含多个子域,子域根据其功能划分为核心域、通用域、支撑域。 限界上下文 描述领域边界,一个限界上下文可能包含多个子域,但一般实践上都以一对一为好。应用单元和部署单元一般也与限界上下文一致。
领域模型
对我们软件系统中要解决问题的抽象表达(解决方案)。模型一般在一个限界上下文中有效。 模块(Module) 聚合根(Aggregate Root) 实体(Entity)区别于 值对象 的地方在于 实体 有着 唯一的标示 值对象(Value Object)它不应该成为领域内的某一个东西,而 仅仅是 描述领域内的一个 概念 工厂(Factory) 领域服务(Domain Service)无状态的操作,用于实现某个特定领域的任务。当操作不适合放在聚合中或者 值对象 中的时候,此时就应该放置在领域服务中。 仓储定义(Repository) 领域事件(Domain Event) 限界上下文(Bounded Context)
对于值对象,它不应该成为领域内的某一个东西,而仅仅是描述领域内的一个概念 。
首先针对 实体 以及 值对象 ,我们应该 尽量使用 的是 值对象 而非实体。 实体 和 值对象 才是最重要的DDD建模对象,如果反而首先使用领域服务,容易导致“贫血领域模型”。
当值对象创建出来之后,就不应该再去修改,也就是 值对象的不变性 。 值对象可以在创建对象化之后直接处理掉,不用担心客户端对值对象的修改问题。 领域实现: 用户界面 应用服务 领域模型 基础设施 服务暴露 仓储实现 反腐层实现 实践步骤为: 找到子域 识别核心域、通用域、支撑域 确定限界上下文映射 在每个子域内设计领域模型 实现领域模型和应用
参考: Value Object & Entity & Domain Service
软件开发
2020-06-28 16:56:00
Go+ Playground 分为两个版本,一个 基于 Docker ,在服务端编译执行 Go+ 代码;另一版本 基于 GopherJS ,其直接在前端编译执行 Go+ 代码。 https://qiniu.github.io/ https://play.goplus.org/
软件开发
2020-06-28 21:16:00
(点击图片进入关卡)
用那只最聪明的美洲狮宠物给农民们留下深刻印象
简介
给小村子里的人带来一场精彩表演吧。
使用 pet.say() 让它回答听到的问题。
别忘了用 pet.on() 指派 sayTwo 给 "hear" 事件作为处理函数。
默认代码
# 教你的宠物回答问题!
# 很幸运,所有的答案都是"2"
def sayTwo(event):
# 使用pet.say()来回答"2"

pass
# 使用pet:on(),通过sayTwo来处理"hear"事件
# 现在休息并观看表演吧!
hero.say("一加一等于…?")
hero.say("x^3 - 6x^2 + 12x - 8 = 0. x等于多少?")
hero.say("火星有多少卫星?")
概览
教猫咪执行各种特殊技能吧!使用正确答案重新为猫咪编程,通过提出各种问题让观众叹为观止。
def sayApplesauce(event):
pet.say("Applesauce")
pet.on("hear", sayApplesauce)
hero.say("什么由苹果做成?")
hero.say("什么被捣碎做成酱汁?")
hero.say("一点肉桂能改善什么?")
博士猫咪 解法
# 教你的宠物回答问题!
# 很幸运,所有的答案都是"2"
def sayTwo(event):
# 使用pet.say()来回答"2"
pet.say("2")
# 使用pet:on(),通过sayTwo来处理"hear"事件
pet.on("hear", sayTwo)
# 现在休息并观看表演吧!
hero.say("一加一等于…?")
hero.say("x^3 - 6x^2 + 12x - 8 = 0. x等于多少?")
hero.say("火星有多少卫星?")

本攻略发于极客战记官方教学栏目,原文地址为: https://codecombat.163.com/news/jikezhanji-senlinboshimaomi 极客战记——学编程,用玩的
软件开发
2020-06-28 10:18:00
1:SpringBootApplication注解
简介 在启动类上有一个非常重要的注解,就是SpringBootApplication注解,理解它对于理解SpringBoot的启动过程很有帮助。 可以查看一下SpringBootApplication的源码,里面有很多注解,其中最重要的有: 1: @Configuration ,这个是在@SpringBootConfiguration里面用的 2:@EnableAutoConfiguration 3:@ComponentScan
@EnableAutoConfiguration
2:SpringApplication执行流程
可以配着看源码
3:起步依赖
什么是起步依赖
Spring Boot通过提供众多起步依赖来降低项目依赖的复杂度。所谓起步依赖,就是一个Maven项目对象模型(Project Object Model,POM),定义了对其他库的传递依赖,这些东西加在一起即支持某项功能 好处 不用管究竟需要哪些依赖:添加相应的起步依赖,就相当于把一堆需要的依赖都加入了。 也不用管具体要依赖什么版本:起步依赖本身的版本是由正在使用的Spring Boot的版本来决定 的,而起步依赖则会决定它们引入的传递依赖的版本。 Springboot目前有哪些起步依赖 可以参看官方文档: https://docs.spring.io/spring-boot/docs/2.0.0.M4/reference/htmlsingle/#using-boot-starter
调整起步依赖
1:需要特定版本的依赖 直接在pom里面添加相应的依赖,并指定需要的版本 2:排除部分依赖 使用exclusion,例如: org.springframework.boot spring-boot-starter-web org.slf4j jcl-over-slf4j cc老师2020年高级架构师系统培训课程7月6号正式开课,现在618活动,限时折扣中。至少300学时录播课程,60小时直播课,内容包含最新架构体系,现主流的架构,以及即时更新最新的技术,从理论到实践,以及如何解决问题。面试常用的 Netty,Redis,Kafka,Zookeeper,Dubbo, Nginx + openResty + kong + Lua, ElasticSearch等技术都深入源码层级,轻松掌握这些技术,从容应对面试。对于有想跟我一样有着像提高自身技术的同学,课程的详情都在私塾的首页详细说明了,感兴趣的小伙伴,可以一起去 【 私塾在线  】 学习
软件开发
2020-06-28 01:08:00
 1 基础工程创建
1:创建一个maven工程
2:加入parent        org.springframework.boot      spring-boot-starter-parent      1.5.6.RELEASE  
3:加入启动依赖        org.springframework.boot      spring-boot-starter-web  
4:设置properties        1.8      UTF-8      com.cc.Application  
5:配置springboot 插件        springbootstudy                            org.springframework.boot            spring-boot-maven-plugin                  
6:开发Controller @Controller @RequestMapping("/hello") public class FirstController {     @RequestMapping("/abc")     @ResponseBody     public String abc() {        System.out.println("now in FirstController.abc");        return "Hello World!";     } }
7:启动类 @SpringBootApplication public class App {    public static void main(String\[\] args) {      //负责启动引导应 用程序      SpringApplication.run(App.class, args);    } }
8:启动运行
先运行启动类,然后在浏览器输入:[ http://localhost:8080/hello/abc ]( http://localhost:8080/hello/abc )
@SpringBootApplication:开启组件扫描和自动配置,
实际 上,@SpringBootApplication将三个有用的注解组合在了一起:
 Spring的 @Configuration :标明该类使用Spring基于Java的配置
 Spring的@ComponentScan:启用组件扫描
 Spring Boot的@EnableAutoConfiguration:开启Spring Boot自动配置
![111.png]( https://upload-images.jianshu.io/upload_images/23729306-2314e8608a24c1e0.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240 )
![222.png]( https://upload-images.jianshu.io/upload_images/23729306-22a9aa19261ad690.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240 )
2 加入数据层
1:环境设置
(1)加入依赖        org.springframework.boot      spring-boot-starter-data-jpa          mysql      mysql-connector-java      5.1.43  
(2)在数据库中创建一个库springbootstudy,同时建一个表tbl_user,有如下字段:uuid、name、age
(3)配置数据源、配置jpa对hibernate的支持,在resources文件夹中建立一个application.properties文件,基本配置如下: spring.datasource.driverClassName=com.mysql.jdbc.Driver spring.datasource.url=jdbc:mysql://127.0.0.1:3306/springbootstudy?useUnicode=true&characterEncoding=UTF-8 spring.datasource.username=root spring.datasource.password=root spring.datasource.max-active=20 spring.datasource.max-idle=8 spring.datasource.min-idle=8 spring.datasource.initial-size=10 spring.jpa.database=MySQL spring.jpa.show-sql=true spring.jpa.generate-ddl=false spring.jpa.hibernate.ddl-auto=update spring.jpa.properties.hibernate.current\_session\_context_class=org.springframework.orm.hibernate4.SpringSessionContext spring.jpa.properties.hibernate.format_sql=true spring.jpa.properties.hibernate.use\_sql\_comments=false spring.hibernate.packageScan=com.cc spring.jta.transaction-manager-id=transactionManager spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQL5Dialect
2:添加user模块,先写vo
3:添加dao及实现,DAO接口示例如下: public interface UserDAO {    public String create(M m);    public void update(M m);    public void delete(String uuid);    public M getByUuid(String uuid);    public List getAll(); }
4:添加java配置
为了支持直接注入Hibernate的SessionFactory,建立一个H4Conf的类,采用java配置的方式: @Configuration public class H4Conf {     @Bean     public SessionFactory sessionFactory(HibernateEntityManagerFactory hemf) {        return hemf.getSessionFactory();     } }
添加Service及实现
添加Controller实现
3 添加页面,使用ThymeLeaf
1:环境构建
(1)在application.properties中添加:  spring.thymeleaf.cache=false  spring.thymeleaf.prefix=classpath:/templates
(2)添加依赖        org.springframework.boot      spring-boot-starter-thymeleaf  
2:在resources下面添加templates,在templates下面添加user文件夹,下面用来存放页面
3:使用ThymeLeaf开发的页面,比如userAdd.html,示例如下:     hello    
                                                                                                               
uuid
name
age
cc老师2020年高级架构师系统培训课程7月6号正式开课,现在618活动,限时折扣中。至少300学时录播课程,60小时直播课,内容包含最新架构体系,现主流的架构,以及即时更新最新的技术,从理论到实践,以及如何解决问题。面试常用的 Netty,Redis,Kafka,Zookeeper,Dubbo, Nginx + openResty + kong + Lua, ElasticSearch等技术都深入源码层级,轻松掌握这些技术,从容应对面试。对于有想跟我一样有着像提高自身技术的同学,课程的详情都在私塾的首页详细说明了,感兴趣的小伙伴,可以一起去 【 私塾在线  】 学习
软件开发
2020-06-28 01:02:00
题目

解题思路:
思路:动态规划
暂时不是很懂
https://leetcode-cn.com/problems/interleaving-string/solution/xiang-xi-tong-su-de-si-lu-fen-xi-duo-jie-fa-by-2-9/
代码: import java.util.*; public class Solution { public boolean isInterleave(String s1, String s2, String s3) { if (s1.length() + s2.length() != s3.length()) { return false; } boolean[][] dp = new boolean[s1.length() + 1][s2.length() + 1]; for (int i = 0; i <= s1.length(); i++) { for (int j = 0; j <= s2.length(); j++) { if (i == 0 && j == 0) { dp[i][j] = true; } else if (i == 0) { dp[i][j] = dp[i][j - 1] && s2.charAt(j - 1) == s3.charAt(j - 1); } else if (j == 0) { dp[i][j] = dp[i - 1][j] && s1.charAt(i - 1) == s3.charAt(i - 1); } else { dp[i][j] = dp[i - 1][j] && s1.charAt(i - 1) == s3.charAt(i + j - 1) || dp[i][j - 1] && s2.charAt(j - 1) == s3.charAt(i + j - 1); } } } return dp[s1.length()][s2.length()]; } }
软件开发
2020-06-27 17:32:00