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

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


  • 首页

  • 归档

  • 搜索

现在准备好告别Transform了吗? 拥抱AGP70

发表于 2021-10-07

前文提要

之前就和大家介绍过AGP(Android Gradle Plugin) 7.0.0版本之后Transform 已经过期即将废弃的事情。而且也简单的介绍了替换的方式是Transform Action,经过我这一阵子的学习和调研,发现只能说答对了一半吧。下面介绍个新东西AsmClassVisitorFactory。

com.android.build.api.instrumentation.AsmClassVisitorFactory

A factory to create class visitor objects to instrument classes.

The implementation of this interface must be an abstract class where the parameters and instrumentationContext are left unimplemented. The class must have an empty constructor which will be used to construct the factory object.

当前官方推荐使用的应该是这个类,这个类的底层实现就是基于gradle原生的Transform Action,这次的学习过程其实走了一点点弯路,一开始尝试的是Transform Action,但是貌似弯弯绕绕的,最后也没有成功,而且Transform Action的输入产物都是单一文件,修改也是针对单一文件的,所以貌似也不完全是一个很好的替换方案,之前文章介绍的那种复杂的asm操作则无法负荷了。

AsmClassVisitorFactory根据官方说法,编译速度会有提升,大概18%左右,这个下面我们会在使用阶段对其进行介绍的。

image.png

学废了

我们先从AsmClassVisitorFactory这个抽象接口开始介绍起吧。

AsmClassVisitorFactory

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
kotlin复制代码@Incubating
interface AsmClassVisitorFactory<ParametersT : InstrumentationParameters> : Serializable {

/**
* The parameters that will be instantiated, configured using the given config when registering
* the visitor, and injected on instantiation.
*
* This field must be left unimplemented.
*/
@get:Nested
val parameters: Property<ParametersT>

/**
* Contains parameters to help instantiate the visitor objects.
*
* This field must be left unimplemented.
*/
@get:Nested
val instrumentationContext: InstrumentationContext

/**
* Creates a class visitor object that will visit a class with the given [classContext]. The
* returned class visitor must delegate its calls to [nextClassVisitor].
*
* The given [classContext] contains static information about the classes before starting the
* instrumentation process. Any changes in interfaces or superclasses for the class with the
* given [classContext] or for any other class in its classpath by a previous visitor will
* not be reflected in the [classContext] object.
*
* [classContext] can also be used to get the data for classes that are in the runtime classpath
* of the class being visited.
*
* This method must handle asynchronous calls.
*
* @param classContext contains information about the class that will be instrumented by the
* returned class visitor.
* @param nextClassVisitor the [ClassVisitor] to which the created [ClassVisitor] must delegate
* method calls.
*/
fun createClassVisitor(
classContext: ClassContext,
nextClassVisitor: ClassVisitor
): ClassVisitor

/**
* Whether or not the factory wants to instrument the class with the given [classData].
*
* If returned true, [createClassVisitor] will be called and the returned class visitor will
* visit the class.
*
* This method must handle asynchronous calls.
*/
fun isInstrumentable(classData: ClassData): Boolean
}

简单的分析下这个接口,我们要做的就是在createClassVisitor这个方法中返回一个ClassVisitor,正常我们在构造ClassVisitor实例的时候是需要传入下一个ClassVisitor实例的,所以我们之后在new的时候传入nextClassVisitor就行了。

另外就是isInstrumentable,这个方法是判断当前类是否要进行扫描,因为如果所有类都要通过ClassVisitor进行扫描还是太耗时了,我们可以通过这个方法过滤掉很多我们不需要扫描的类。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
kotlin复制代码@Incubating
interface ClassData {
/**
* Fully qualified name of the class.
*/
val className: String

/**
* List of the annotations the class has.
*/
val classAnnotations: List<String>

/**
* List of all the interfaces that this class or a superclass of this class implements.
*/
val interfaces: List<String>

/**
* List of all the super classes that this class or a super class of this class extends.
*/
val superClasses: List<String>
}

ClassData并不是asm的api,所以其中包含的内容相对来说比较少,但是应该也勉强够用了。这部分大家简单看看就行了,就不多做介绍了呢。

新的Extension

AGP版本升级之后,应该是为了区分新旧版的Extension,所以在AppExtension的基础上,新增了一个AndroidComponentsExtension出来。

我们的transformClassesWith就需要注册在这个上面。这个需要考虑到变种,和之前的Transform还是有比较大的区别的,这样我们就可以基于不同的变种增加对应的适配工作了。

1
2
3
4
5
6
kotlin复制代码        val androidComponents = project.extensions.getByType(AndroidComponentsExtension::class.java)
androidComponents.onVariants { variant ->
variant.transformClassesWith(PrivacyClassVisitorFactory::class.java,
InstrumentationScope.ALL) {}
variant.setAsmFramesComputationMode(FramesComputationMode.COPY_FRAMES)
}

实战

这次还是在之前的敏感权限api替换的字节码替换工具的基础上进行测试开发。

ClassVisitor

看看我们正常是如何写一个简单的ClassVisitor的。

1
2
3
4
5
kotlin复制代码ClassWriter classWriter = new ClassWriter(ClassWriter.COMPUTE_MAXS);
ClassVisitor methodFilterCV = new ClassFilterVisitor(classWriter);
ClassReader cr = new ClassReader(srcClass);
cr.accept(methodFilterCV, ClassReader.SKIP_DEBUG);
return classWriter.toByteArray();

首先我们会构造好一个空的ClassWriter,接着会构造一个ClassVisitor实例,然后传入这个ClassWriter。然后我们构造一个ClassReader实例,然后将byte数组传入,之后调用classReader.accept方法,之后我们就能在visitor中逐个访问数据了。

那么其实我们的类信息,方法啥的都是通过ClassReader读入的,然后由当前的ClassVisitor访问完之后交给我们最后一个ClassWriter。

其中ClassWriter也是一个ClassVisitor对象,他复杂重新将修改过的类转化成byte数据。可以看得出来ClassVisitor就有一个非常简单的链表结构,之后逐层向下访问。

介绍完了这个哦,我们做个大胆的假设,如果我们这个ClassVisitor链表前插入几个不同的ClassVisitor,那么我们是不是就可以让asm修改逐个生效,然后也不需要多余的io操作了呢。这就是新的asm api 的设计思路了,也是我们这边大佬的字节码框架大佬的设计。另外bytex内的设计思路也是如此。

tips ClassNode 因为是先生成的语法树,所以和一般的ClassVisitor有点小区别,需要在visitEnd方法内调用accept(next)

实际代码分析

接下来我们上实战咯。我将之前的代码套用到这次的逻辑上来。

demo地址

1
2
3
4
5
6
7
8
9
10
11
kotlin复制代码abstract class PrivacyClassVisitorFactory : AsmClassVisitorFactory<InstrumentationParameters.None> {

override fun createClassVisitor(classContext: ClassContext, nextClassVisitor: ClassVisitor): ClassVisitor {
return PrivacyClassNode(nextClassVisitor)
}

override fun isInstrumentable(classData: ClassData): Boolean {
return true
}

}

我在isInstrumentable都返回的是true,其实我可以将扫描规则限定在特定包名内,这样就可以加快构建速度了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
kotlin复制代码class PrivacyClassNode(private val nextVisitor: ClassVisitor) : ClassNode(Opcodes.ASM5) {
override fun visitEnd() {
super.visitEnd()
PrivacyHelper.whiteList.let {
val result = it.firstOrNull { whiteName ->
name.contains(whiteName, true)
}
result
}.apply {
if (this == null) {
// println("filter: $name")
}
}
PrivacyHelper.whiteList.firstOrNull {
name.contains(it, true)
}?.apply {
val iterator: Iterator<MethodNode> = methods.iterator()
while (iterator.hasNext()) {
val method = iterator.next()
method.instructions?.iterator()?.forEach {
if (it is MethodInsnNode) {
it.isPrivacy()?.apply {
println("privacy transform classNodeName: ${name@this}")
it.opcode = code
it.owner = owner
it.name = name
it.desc = desc
}
}
}
}
}
accept(nextVisitor)
}
}


private fun MethodInsnNode.isPrivacy(): PrivacyAsmEntity? {
val pair = PrivacyHelper.privacyList.firstOrNull {
val first = it.first
first.owner == owner && first.code == opcode && first.name == name && first.desc == desc
}
return pair?.second

}

这部分比较简单,把逻辑抽象定义在类ClassNode内,然后在visitEnd方法的时候调用我之前说的accept(nextVisitor)方法。

另外就是注册逻辑了,和我前面介绍的内容基本都是一样的。

我的观点

AsmClassVisitorFactory相比较于之前的Transform确实简化了非常非常多,我们不需要关心之前的增量更新等等逻辑,只要专注于asm api的操作就行了。

其次就是因为减少了io操作,所以其速度自然也就比之前有所提升。同时因为基于的是Transform Action,所以整体性能还是非常ok的,那部分增量可以说是更简单了。

另外我也和我的同事大佬交流过哦,复杂的这种类似上篇文章介绍的,最好还是使用Gradle Task的形式进行修改。

最后文章内容感谢 2BAB 和 森哥,文章很多内容都参考了几位大佬的文章。

最后我打算放飞自己了,文章字数就随便了吧,不要老盯着文章长短问题。

本文转载自: 掘金

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

Shell编程

发表于 2021-10-06

Shell类型

shell种类特别的多,一般在Linux下默认都是bash,我们可以通过cat /etc/passwd查看每个用户默认的shell。同时我们在工作中可能会用到zsh,其实zsh是兼容bash的。

一般在shell下执行的命令有两种:

  • 内建命令
  • 非内建命令
    内建命令在执行时一般不会fork出子shell,内建命令有cd、alias、exit,一般我们通过which cmd可以查出命令是不是内建命令。不管是内建命令,还是非内建命令,我们都可以通过$?来获取命令的返回码(返回吗一般是0表示成功,非0表示失败)。

Shell如何执行命令

执行命令我们一般是分单条执行还是多条执行。

单条执行

其实就是我们常用的方式,只不过是如果是内建命令就不会fork而已,非内建命令会fork/exec/wait。

多条执行

其实就是我们说写的脚本(批处理),把多条命令放到一个文件中,然后一块执行。
比如有如下一个脚本,文件名为:script.sh:

1
2
3
4
bash复制代码#! /bin/sh

cd ..
ls

一般有两种执行方式:

  1. 以一种可执行文件的方式来执行。

chmod +x script.sh然后./script.sh就可以执行了。这种执行方式的大概流程为:先fork出一个子shell,在子shell执行通过exec来执行该脚本中第一行所指定的/bin/sh的解释器(相当于将sh的代码段加载到这个子进程中来),而当前script.sh是作为一个命令行参数来传递给sh的。当执行到cd ..,由于cd是内建命令,相当于sh内部调用一个函数来改变当前子shell的工作路径;然后再执行ls,由于ls不是内建命令,所以会再fork出一个子进程执行ls,子shell调用wait等待ls进程执行完,当ls的进程执行完之后,子shell的wait会解除,由于脚本中的代码已经执行完了,所以主shell中wait子shell也解除,因此我们就能看到shell提示符了。

  1. 直接通过解释器来执行
    也就是这种sh ./script.sh方式。我们会发现,其实两种方式的本质还是一样的。
    我们应该能发现在脚本中有一句cd ..,但是脚本执行完之后,我们的工作目录本没有任何改变,这是为啥?这是因为修改的是子shell的工作目录,当前主shell的目录并没有修改呀。
  2. 执行脚本时能不能先不要fork出子shell
    答案肯定是可以的。比如我们执行以下的方式source ./script.sh或者. ./script.sh,这种方式就不会先fork出子shell来执行,但是到单个命令时还是会fork。这里可以联想到我们平时在定义环境变量时脚本,我们执行该脚本时都用source script.sh的原因。因为执行不会fork,所以环境变量就是针对当前的shell进程的;如果执行之前的方式执行,仅仅修改的子shell的环境变量,主shell环境变量并没有任何变化。

另外要注意,通过(cd ..; ls -l)这样也是会fork的。

Shell基本语法

变量

在shell中变量分成本地变量、环境变量。本地变量只能在当前的shell进程中使用,而环境变量可以在当前的shell进程的子进程中使用。

    1. 本地变量
      本地变量的定义方式:
1
ini复制代码VALNAME=value

等号两边不能有空格。获取获取变量的值呢:

1
bash复制代码echo ${VALNAME}

在shell中所有变量的类型都是字符串类型,如果变量没有定义就是空字符串。

    1. 环境变量
      将本地变量导出就是环境变量,可以通过命令export VALNAME。也可以一边定义一边导出:
1
ini复制代码export AAA=value

查看环境变量,可以通过env或者printenv。环境变量可以有父进程传递给子进程。

    1. 如何使用变量
      使用变量推荐使用:
1
bash复制代码echo ${SHELL}

也就是大括号的方式。这样的好处就是后边跟一些字符串,变量展开以后也是可以拼接上的。比如echo ${SHELL}abc,那么结果就是/bin/zshabc

    1. 变量的类型
      在shell里边可以通过declare来声明变量,例如如下代码:
1
2
3
4
5
6
7
8
9
10
11
bash复制代码#!/bin/bash 

declare -i mi
declare -i mx=100
declare -i s=0

for((mi=0; mi <= mx; mi=mi+1)); do
s=s+mi
done

echo $s

同时我们也可以通过declare来声明数组

1
2
3
4
5
6
7
8
9
10
11
12
bash复制代码#!/bin/bash 


declare -a colors
colors[0]="yellow"
colors[1]="white"
colors[2]="black"

echo "len=${colors[@]}"
for co in ${colors[@]}; do
echo $co
done
    1. 变量内容的删除与替换
      变量内容的删除,最基本的语法为${var#模式}/${var##模式}/${var%模式}/${var%%模式}/${var/old/new}/${var//old/new}
  1. #表示从前往后删除,删除满足要求最短的字符串
1
2
3
4
5
bash复制代码file_path="/home/test/workspace/shell/test.cpp"
echo ${file_path#/*/}

## 输出内容为:
test/workspace/shell/test.cpp
  1. ##表示从前往后删除,删除满足要求的最长字符串
1
2
3
4
ini复制代码file_path="/home/test/workspace/shell/test.cpp"
echo ${file_path##/*/}
## 输出内容为:
test.cpp
  1. %表示从后往前删除,删除满足要求的最短字符串
1
2
3
4
5
bash复制代码file_path="/home/test/workspace/shell/test/test.cpp"
echo ${file_path%/test*}

## 输出内容为:
/home/test/workspace/shell/test
  1. %%表示从后往前删除,满足要求的最长字符串
1
2
3
4
5
bash复制代码file_path="/home/test/workspace/shell/test/test.cpp"
echo ${file_path%%/test*}

## 输出结果为:
/home
  1. 使用${var/old/new}替换时,仅仅替换一次。使用${var//old/new}替换时,是替换全部
1
2
3
4
5
6
7
8
bash复制代码file_path="/home/test/workspace/shell/test/test.cpp"
echo ${file_path/test/learn}
# 输出:
/home/learn/workspace/shell/test/test.cpp

echo ${file_path//test/learn}
# 输出
/home/learn/workspace/shell/learn/learn.cpp
    1. 变量的测试
      这里的内容还挺多的,我这里仅仅记录我常用的。
      new_var=${old-content}表示old没有定义时,new_var使用content;如果old定义了并且是个空字符串,那么new_var也是""。另外new_var=${old:-content},也就是说old没有定义还是个空字符串,那么new_var就取content,否则就取old。

文件名代换globbing

其实就是一些通配符。

通配符 作用
* 匹配0个或者多个任何字符
? 匹配1个任意字符
[] 匹配方括号中其中一个字符
比如我们执行ls ch1[0-2].doc,那么shell其实会先展开通配符,比如能找到ch10.doc、ch11.doc,然后将这两个文件传递给ls。也就是说ls并不会不处理这些通配符,是由shell先展开再传递给ls。

命令行代换``、$()

通过``或者$()括起来的也是一条命令,shell会先执行这条命令,将执行结果放到当前所在的命令行中。

1
2
3
4
5
6
bash复制代码DATE=`date`
echo $DATE

或者
DATE=$(date)
echo $DATE

算术代换:$(())

shell中变量默认都是字符串,所以可以涉及到整形变量+、-、*、/可以这样处理:

1
2
bash复制代码val=10
echo $(($val + 10))

$(())只能用于整形运算

转义字符\

有一些特殊的字符如果我们想使用这些特殊字符的字面量时,就可以使用转义。
echo \$SHELL结果就不打印SHELL变量了,而是打印$SHELL。

另外\也可以表示续行的意思。

单引号、双引号

单引号表示字符的字面值。双引号一般情况下都表示字符的字面值,但是在遇到$变量名则会展开变量值;在遇到``则会命令替换。

bash启动脚本

就是bash启动时执行的脚本,这里边的规则还是挺复杂的,但是只需要记住一条,我们可以将环境变量、alias、mask定义到.bashrc文件中即可。这样在bash启动时会自动source启动脚本,这样我们预先定义的变量就自动生效了。

shell脚本语法

条件测试

条件测试语句在shell中有两种表达[]/[[]]/test,条件测试语句可以测试字符串/数值/文件的属性。比如

1
2
3
4
perl复制代码VAR=2
test $VAR -gt 1$

[ $VAR -gt 3 ]

不管用那种方式,最终表示条件是真还是假,是通过$?来判断的,0表示true,1表示false。

AAA_test.png

对于与或非

AAAA_1.png

在shell中[[]]是[]的拓展,并且[[]]是兼容[]的。竟然是拓展,那功能肯定要比之前的[]要强一些的。

  • 替换掉[]中的-a或者-o
1
2
3
lua复制代码[ -f README.md -a -x README.md ] && echo "yes" || echo "no"
# 如果使用[[]]的话,可以这样:
[[ -f README.md && -x README.md ]] && echo "yes" || echo "no"
  • 使用正则表达式
1
2
3
lua复制代码A="hello"; [[ "$A" =~ hell? ]] && echo "yes" || echo "no"

# 这里要注意=~ 右边不能使用""。如果使用""就表示字符串了,不使用""就表示正则的模式

判断语句

在shell编程中是可以使用if语句的,但是跟我们的c语言的if也不太一样。

1
2
3
bash复制代码if [ -f ~/.bashrc ]; then 
. ~/.bashrc
fi

其实涉及三条语句,if [ -f ~/.bashrc ] 如果测试语句$?为0则表示为true,否则为false。then . ~/.bashrc在shell一般一行只有一条语句,如果要有多条语句的话,就用;分离。fi 表示if的结束标记。

特殊if语句,也就是if的条件永远为true。:是空语句,执行结果$?是0。

1
bash复制代码if :; then echo "always true"; fi

常见的if用法为:

1
2
3
4
5
6
7
8
9
bash复制代码num=1

if [ $(($num)) -gt 10 ]; then
echo 'a'
elif [ ${num} -eq 10 ]; then
echo 'b'
else
echo 'c'
fi

&&、||用法,其实就是利用短路特性。

1
bash复制代码test "$(whoami)" != 'root' && (echo you are using a non-privileged account; exit 1)

对于if语句最佳实践,最好对于if [ $var xxxx ]中的$var用""括起来。

case语句

case语句就是c语言中的switch case。大概的用法是这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
bash复制代码case $1 in 
start)
...
;;
stop)
...
;;
reload | force-reload)
...
;;
restart)
...
;;
*)
log_success_msg "Usage: /etc/init.d/apache2 {start|stop|restart|reload|force-reload|start-htcacheclean|stop-htcacheclean}"
exit 1
;;
esac

以case开头,在in里边进行匹配,支持通配符,一旦找到某一个条件就执行对应的语句最终语句是以;;结束。整个语句是以esac结束。

for/do/done

for循环。比如

1
2
3
bash复制代码for FRUIT in apple banana pear; do 
echo "I like $FRUIT"
done

如果想将某一个目录下的文件改名,可以这样:

1
2
3
4
5
6
bash复制代码files=`ls test`
echo ${files}

for f in ${files}; do
mv "test/${f}" "test/${f}.tmp"
done

也可以写类似于c语言的循环形式

1
2
3
4
5
6
7
8
9
ini复制代码declare -i mi
declare -i mx=100
declare -i s=0

for((mi=0; mi <= mx; mi=mi+1)); do
s=s+mi
done

echo $s

while/do/done

1
2
3
4
5
bash复制代码COUNTER=1
while [ "$COUNTER" -lt 10 ]; do
echo "Here we go again"
COUNTER=$(($COUNTER+1))
done

这个应该很好理解。

位置参数与特殊变量

可以参考如下列表:

AAAA-2.png

其中位置参数可以用shift命令左移。比如shift 3表示原来的4现在变成4现在变成4现在变成1,原来的5现在变成5现在变成5现在变成2等等,原来的1、1、1、2、3丢弃,3丢弃,3丢弃,0不移动。不带参数的shift命令相当于shift 1。shift可以使用场景就是在脚本内部可能会调用其他的脚本,但是参数又不想用那么多,这个时候就可以使用shift进行丢弃。

函数

shell中的函数不用写参数列表跟返回值类型的。是可以传递参数跟返回值的,只不过不需要声明而已。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
bash复制代码is_directory()
{
DIR_NAME=$1
if [ ! -d $DIR_NAME ]; then
return 1
else
return 0
fi
}
for DIR in "$@"; do
if is_directory "$DIR"
then :
else
echo "$DIR doesn't exist. Creating it now..."
mkdir $DIR > /dev/null 2>&1
if [ $? -ne 0 ]; then
echo "Cannot create directory $DIR"
exit 1
fi
fi
done

在shell脚本中如果要表示返回true,一般用return 0,这个跟if判断是有关系的。另外在shell空语句用:来表达。

常见技巧积累

如果脚本遇到错误,最好能暴露出来,不要埋雷

这种情况下,我们一般要不使用set -e或者是指定脚本解释器时,指定-e参数,比如这样:

1
2
3
4
5
6
7
8
9
10
bash复制代码#!/bin/bash -e

#set -e
mkdir -p a/b
cd a
touch file
cd -

rmdir a
echo "done"

这样脚本一旦遇到error就会停止运行,并且$?也会返回非0

显性指定脚本的工作目录

也就是说,不管使用者从那个位置启动脚本,该脚本都能一如既往的按照预期的目录工作。一般情况下我们将某个脚本放置在某个目录,那一般工作路径基本上是基于该目录的,所以我们最好再写脚本时,在脚本的最开头这样写:

1
2
bash复制代码dir=$(dirname $(readlink -f "$0"))
cd ${dir}

这种写法也同时考虑了软连接的情况

脚本的命令行参数处理技巧

最简单的处理,应该是一个for再加一个case语句来处理,复杂点的参数可以通过getopt来实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
bash复制代码#!/bin/bash 

for arg in $@; do
case $arg in
-h)
echo "usage:xxxx"
exit 0
;;
-a)
echo "param a handle"
;;
*)
echo "default param handle"
;;
esac
done

其他

|命令的右边必须是能接受标准输入作为参数时才能正常运行。比如echo "helloworld" | echo就不会有任何输出,可以通过xargs命令将标准输入转成命令行参数, echo "helloworld | xargs echo就可以正常打印了

本文转载自: 掘金

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

使用 Prometheus + Grafana + Spri

发表于 2021-10-06

小知识,大挑战!本文正在参与“程序员必备小知识”创作活动。

在企业级的应用中,监控往往至关重要,监控可以帮助我们预防故障,预测变化趋势,在达到阈值的时候报警,为排查生产问题提供更多的信息。如果我们不知道我们程序的运行情况,当线上系统出现了事故再去排查就需要花费更多的时间,如果能提前监控,就能早做准备,以免出了事故之后乱了手脚,当然也避免不了系统不产生一点事故,但是能减少系统事故的产生。同时也能看到系统问题,早做优化,避免更大的事故发生。

  1. Spring Boot Actuator

根据官网介绍,Spring Boot包含了很多附加功能帮助我们监控和管理我们的应用,可以使用HTTP或者JMX等方式通过端点(endpoint)获取应用的健康状态以及其他指标收集。

Spring Boot Actuator模块就是Spring Boot提供的集成了上面所述的监控和管理的功能。像Spring Boot其他模块一样开箱即用,非常方便,通过Actuator就可以使用HTTP或者JMX来监控我们的应用。

JMX(Java Management Extensions):Java平台的管理和监控接口,任何程序只要按JMX规范访问这个接口,就可以获取所有管理与监控信息。

下面简单介绍下Spring Boot Actuator是如何使用的,具体的使用方法见官方文档,官方的文档是永远值得相信的。

1.1 添加依赖

如果使用maven管理,则是:

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

如果使用Gradle,则是下面这样的:

1
2
3
gradle复制代码dependencies {
implementation 'org.springframework.boot:spring-boot-starter-actuator'
}

1.2 开启端点

Spring Boot中监控应用或者与应用交互都是通过端点(endpoint)进行的,Spring Boot提供了非常多的原生端点,比如health,可以帮助我们监控应用的监控状态(是否可用),同时可以添加自定义的端点。每个端点都可以单独设置是否开启,并通过HTTP或者JMX暴露给外部系统。如果选择HTTP方式,则URL的前缀一般是/actuator,比如health的url地址就是/actuator/health。

默认情况下,除了shutdown(让系统优雅的关闭) ,其他端点默认是开启的。我们可以通过management.endpoint.<id>.enabled设置某个端口的状态,比如开启shutdown端口。

1
properties复制代码management.endpoint.shutdown.enabled=true

1.3 暴露端点

开启端点后还必须暴露端点给HTTP或者JMX才能正常使用,但是端口可能包含一些敏感的数据,所以Spring Boot的原生端口默认只支持HTTP或者JMX,比如shutdown端口默认只支持JMX,health端口即支持JMX,也支持HTTP。

ID JMX Web
auditevents Yes No
beans Yes No
caches Yes No
conditions Yes No
configprops Yes No
env Yes No
flyway Yes No
health Yes Yes
heapdump N/A No
httptrace Yes No
info Yes No
integrationgraph Yes No
jolokia N/A No
logfile N/A No
loggers Yes No
liquibase Yes No
metrics Yes No
mappings Yes No
prometheus N/A No
quartz Yes No
scheduledtasks Yes No
sessions Yes No
shutdown Yes No
startup Yes No
threaddump Yes No

如果要改变端口暴露的方式,使用include或者exclude属性,比如:

1
2
properties复制代码management.endpoints.jmx.exposure.include=*
management.endpoints.web.exposure.include=health,info,prometheus

到目前为止,Spring Boot Actuator就配置好了,除了上述所看到的端点的开启和暴露方式,还有HTTP,JMX,日志,指标(Metrics),权限,HTTP追踪,进程监控等功能,如果想了解更多,可以去官网进一步学习。

  1. Prometheus

Prometheus,中文名普罗米修斯,是新一代的监控系统,与其他监控系统相比,具有易于管理,监控服务的内部运行状态,强大的数据模型,强大的查询语言PromQL,高效,可扩展,易于集成,可视化,开放性等众多功能。详细内容可见官网,此处不再详细介绍。

在Spring Boot中,原生支持了prometheus端口,只需要通过如下配置就可集成Prometheus暴露给HTTP。

1
2
3
4
5
yaml复制代码management:
  endpoints:
    web:
      exposure:
        include: "prometheus"

除了上述配置外,还需要配置metrics,因为如果没有这个参数,很多报表不能正常显示。(此处没有深入研究,道歉…)

1
2
3
4
5
6
7
8
yaml复制代码management:
  endpoints:
    web:
      exposure:
        include: "prometheus"
  metrics:
    tags:
      application: ${spring.application.name}

这样就把Prometheus的客户端配置好了,另外就还需要服务端,这里我们使用docker方式,首先需要配置文件prometheus.yml

1
2
3
4
5
6
7
8
9
10
11
12
yaml复制代码scrape_configs:
  # 任意写,建议英文,不要包含特殊字符
  - job_name: 'jobName'
    # 采集的间隔时间
    scrape_interval: 15s
    # 采集时的超时时间
    scrape_timeout: 10s
    # 采集路径
    metrics_path: '/actuator/prometheus'
    # 采集服务的地址,也就是我们应用的地址
    static_configs:
      - targets: ['localhost:8080']

创建docker-compose.yml,注意prometheus.yml与docker-compose.yml的相对路径,如果放在同样的目录下,volumes则为- './prometheus.yml:/etc/prometheus/config.yml'。

1
2
3
4
5
6
7
8
9
yaml复制代码version: '3.3'
services:
  prometheus:
    image: 'prom/prometheus:v2.14.0'
    ports:
      - '9090:9090'
    command: '--config.file=/etc/prometheus/config.yml'
    volumes:
      - './prometheus.yml:/etc/prometheus/config.yml'

然后直接使用命令docker-compose up -d启动即可,docker compose的使用另寻了解,此处不再详细介绍。

启动之后,在浏览器中访问http://localhost:9090

2021-10-06-B7UuV1

然后,可以查看不同指标的监控数据,比如jvm_memory_used_bytes

2021-10-06-caFBAX

这样,我们就通过Prometheus已经可以看到Spring Boot不同指标的监控数据了,那么为什么还需要Grafana呢,不集成Grafana也是可以的,但是通过Grafana,我们可以更加方便快捷的可视化的查看监控数据,最终的成果如下图所述:

2021-10-06-tJAxIH

如果感兴趣的话,继续往下看哟。

  1. Grafana

3.1 介绍

Grafana是一个可视化面板,可以展示非常漂亮的图标和布局,支持Prometheus,SQL(MySQL,PostgreSQL)等作为数据源。

有如下特点:

  1. 可视化:非常精美的各种组件可供选择,比如图表,文本,提醒事项,还有灵活的布局,可以自由配置你的可视化面板。
  2. 数据源:目前支持Prometheus,Graphite,Loki,Elasticsearch,MySQL,PostgreSQL,Stackdriver和TestData DB等多种数据源。
  3. 通知提醒:通过可视化的方式配置通知规则,在数据达到阈值时,将配置好的信息发送给指定管理员。
  4. 混合数据源:在同一个图中混合不同的数据源,基于每个查询指定数据源。

3.2 安装

还是使用docker compose的方式安装,docker-compose.yml如下:

1
2
3
4
5
6
yaml复制代码version: '3.3'
services:
  grafana:
    image: 'grafana/grafana:6.5.0'
    ports:
      - '3000:3000'

此处可以与Prometheus合并到一个docker compose中,如果合并则是:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
yaml复制代码version: '3.3'
services:
  prometheus:
    image: 'prom/prometheus:v2.14.0'
    ports:
      - '9090:9090'
    command: '--config.file=/etc/prometheus/config.yml'
    volumes:
      - './prometheus.yml:/etc/prometheus/config.yml'

  grafana:
    image: 'grafana/grafana:6.5.0'
    ports:
      - '3000:3000'

然后再通过命令docker-compose up -d启动,在浏览器中访问http://localhost:3000,并使用初始账号admin:admin登录。

2021-10-06-VLU67p

3.3 配置

3.3.1 添加数据源

登录之后,选择添加数据源,选择Prometheus。

2021-10-06-GIx6OD

输入数据源名称(任意),Prometheus的url地址,然后点击添加保存。

2021-10-06-tksEgE

3.3.2 创建仪表盘

下一步就是创建仪表盘,在这里有两个选择,其一是创建新的仪表盘,选择不同的组件,设置布局,还有一种方式是选择grafana官方或者社区提供的仪表盘,而且他们的样式都十分精美,可以直接导入。这里我们选择导入仪表盘,因为我们是Java应用,重点关注的肯定是JVM相关的指标,所以我们搜索JVM,通过安装量进行排序。

2021-10-06-gy4E1A

点击第一个,这个仪表盘包含了JVM,线程,CPU等指标,我们就导入这个,当然你也可以选择其他的仪表盘或者自建。

2021-10-06-l8RyBT

在Grafana.com Dashboard处输入4701。

2021-10-06-rQfZIA

选择数据源,点击导入。

2021-10-06-wv5tLx

最终呈现的仪表盘就如下图:

2021-10-06-QqsmVO

  1. 总结

这篇主要从Spring Boot Actuator入手,介绍了Spring Boot应用监控的端点和暴露方式,接着就以端点Prometheus为例,介绍了Prometheus的基本概念和如何使用的,Spring Boot Actuator + Prometheus就已经能完成可视化监控应用了,但是Prometheus的可视化还是比较粗糙,这个时候Grafana就出场了,通过Grafana和Prometheus就可以实现完美的可视化仪表盘。

另外除了监控应用外,我们平时使用的还有数据库,那么我们如果通过Grafana和Prometheus监控数据库实例的相关指标呢,下一篇文章见。

本文转载自: 掘金

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

实现二维码(链接)分享

发表于 2021-10-06

链接(二维码)分享需求

  • 功能模块添加分享链接(二维码)功能,通过分享出去的链接,可查看功能模块的详情。
  • 分享出去的链接在1天/3天/7天/30天/永不过期,打开过期的链接,弹出提示页面链接已过期。
  • 3某些模块分享的链接只能由系统内的指定用户打开,在其他系统外或者非指定的用户打开提示无权限。
  • 支持Android/Ios/Web端分享,在Android/Ios端内扫描二维码直接跳转至相应功能模块

程序设计方案

二维码分享 (1)

数据库脚本
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
sql复制代码CREATE TABLE `url_share` (
`id` varchar(32) NOT NULL COMMENT '主键',
`userSn` varchar(10) NOT NULL COMMENT '发起分享人',
`expire` bigint(20) NOT NULL COMMENT '过期时间,-1代表永久',
`shareParam` longtext NOT NULL COMMENT '分享参数',
`shareModule` varchar(20) NOT NULL COMMENT '所属模块',
`shareToken` varchar(32) NOT NULL COMMENT '分享token',
`shareUrl` varchar(512) NOT NULL COMMENT '分享的链接',
`shareTime` datetime NOT NULL COMMENT '分享时间',
PRIMARY KEY (`id`),
UNIQUE KEY `ix_shareToken` (`shareToken`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='链接分享';


CREATE TABLE `url_share_userauth` (
`id` varchar(32) NOT NULL,
`userSn` varchar(10) NOT NULL COMMENT '用户通行证',
`shareId` varchar(32) NOT NULL COMMENT '分享id',
PRIMARY KEY (`id`),
KEY `ix_shareId` (`shareId`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='链接分享用户授权';
接口新开Or复用已有接口

事先与客户端约定所有用于分享链接(二维码)的接口request uri前添加/share前缀作为分享接口的标识

1.新增/share/xxx的接口

比如现有业务依赖/doBiz接口,需要实现分享功能

1
2
3
4
java复制代码@PostMapping("/doBiz")
public void doBiz(@RequestParam String param) throws Exception {
helloService.doBiz(param);
}

新增一个用于分享的接口,定义为/share/doBiz,然后复用service的方法

1
2
3
4
java复制代码@PostMapping("/share/doBiz")
public void shareDoBiz(@RequestParam String param) throws Exception {
helloService.doBiz(param);
}

每有一个新模块需要分享功能,在控制层controller要增加一个或者多个/share/xxx的接口,造成代码重复。试想在不同的版本迭代过程中,都会存在模块添加分享功能的需求,到时候再去增加一个或者多个/share/xxx的接口,这是很难接受的。

2.篡改请求复用已有接口

Servlet的Filter或者Spring Cloud的ZuulFilter允许我们在收到请求真正转发给ServerletDispatcher之前修改HttpServerletRequest的request uri和request param,下面是一个通过Spring Cloud的ZuulFilter篡改请求的例子。

2.1与前端约定所有分享页面调用业务接口的格式为:

1
java复制代码Get(Post)  /share/doBiz...

2.2配置可用于通过/share/xxx访问的接口uri

如果将所有接口都允许通过/share/xxx的形式暴露出去,这是非常严重的系统漏洞,对于业务数据敏感的业务可能会带来无法挽回的损失,我们可以通过配置文件给每个模块配置允许通过/share/xxx访问的接口,这样在每次需要给新模块添加分享功能时,仅仅需要添加配置文件,对于不符合配置模块请求uri的接口,跳过篡改请求参数(地址)的Filter继续执即可。

配置文件urshare.json

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
json复制代码[
{
"htmlUrl":"http://172.16.1.133:9529/#/share",
"reqUrls":[
"/xxxx/task/findTaskType/**",
"/xxx/task/taskDetail/**"
],
"module":"taskDetail"
},
{
"htmlUrl":"http://172.16.1.133:9529/#/share",
"reqUrls":[
"/xxx/user/info/**"
],
"module":"userInfo"
}
]
  • htmlUrl:生成的分享的链接地址前缀,最终生成的分享链接形式一般为http://172.16.1.133:9529/#/share?shareToken=xxx
  • reqUrls:分享链接的页面需要请求的后台uri地址
  • module:分享模块

2.3匹配uri是否符合规则的方法

1
2
3
4
5
6
7
8
9
10
11
12
java复制代码public static boolean pathMatchPattern(String path, List<String> patterns) {
boolean result = false;
for (String pattern : patterns) {
//Spring提供的用于匹配uri正则的工具类
AntPathMatcher matcher = new AntPathMatcher();
if (matcher.match(pattern, path)) {
result = true;
break;
}
}
return result;
}
篡改HttpServletRequest请求和校验/share/doBiz请求网关ZuulFilter
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
java复制代码@Component
public class UrlShareFilter extends ZuulFilter implements ApplicationRunner {

private Logger logger = LoggerFactory.getLogger(UrlShareFilter.class);

@Resource
private RedisUtil redisUtil;

@Resource
private UrlShareFeignService urlShareFeignService;

@Override
public String filterType() {
return PRE_TYPE;
}

@Override
public int filterOrder() {
return 0;
}

@Override
public boolean shouldFilter() {
RequestContext ctx = RequestContext.getCurrentContext();
String reqUri = ctx.getRequest().getRequestURI();
if (reqUri.indexOf("/share/") != -1) {
try {
return PathUtils.pathMatchPattern(reqUri.replaceFirst("/share", EmptyUtils.EMPTY_STR), urlShareFeignService.shareReqUrls());
} catch (Exception e) {
logger.error("urlShareFeignService.shareReqUrls error", e);
return false;
}
}
return false;
}

@Override
public Object run() throws ZuulException {
//解析并验证shareToken
RequestContext ctx = RequestContext.getCurrentContext();
HttpServletRequest request = ctx.getRequest();
String shareToken = null;
Map<String, String[]> queryParamMap = request.getParameterMap();
if (EmptyUtils.isNotEmpty(queryParamMap)) {
String[] queryParam = queryParamMap.get(Constants.SHARE_TOKEN_HEADER);
if (EmptyUtils.isNotEmpty(queryParam)) {
shareToken = queryParam[0];
}
}
String usrToken = null;
if (EmptyUtils.isEmpty(shareToken)) {
sendResp(ctx, HttpStatus.UNAUTHORIZED.value(), "分享链接不合法");
return false;
} else {
UrlShareInfo urlShareInfo = null;
try {
urlShareInfo = urlShareFeignService.getUrlShareInfo(shareToken);
} catch (Exception ex) {
sendResp(ctx, HttpStatus.INTERNAL_SERVER_ERROR.value(), "服务内部错误");
return false;
}
if (urlShareInfo == null) {
sendResp(ctx, HttpStatus.UNAUTHORIZED.value(), "分享链接无效");
return false;
}
if (urlShareInfo.getExpire() > 0&& urlShareInfo.getExpire() <= System.currentTimeMillis()) {
sendResp(ctx, HttpStatus.UNAUTHORIZED.value(), "分享链接已过期");
return false;
}
if (EmptyUtils.isNotEmpty(ctx.getRequest().getHeader(Constants.TOKEN_HEADER))) {
usrToken = ctx.getRequest().getHeader(Constants.TOKEN_HEADER);
}
if (EmptyUtils.isNotEmpty(urlShareInfo.getAuthUserSns())) {
if (usrToken == null) {
sendResp(ctx, HttpStatus.UNAUTHORIZED.value(), "用户无权限");
return false;
} else {
//分享链接需要用户权限打开
if (redisUtil.get(usrToken) != null) {
UserSession userSession = JSONObject.parseObject(redisUtil.get(usrToken).toString(), UserSession.class);
if (userSession != null) {
if (!urlShareInfo.getAuthUserSns().contains(userSession.getUserSn())) {
sendResp(ctx, HttpStatus.UNAUTHORIZED.value(), "用户无权限");
return false;
}
} else {
sendResp(ctx, HttpStatus.UNAUTHORIZED.value(), "用户无权限");
return false;
}
} else {
sendResp(ctx, HttpStatus.UNAUTHORIZED.value(), "用户无权限");
return false;
}
}
}
}
//share请求重定向到正常请求
final String realToken = (usrToken == null ? Constants.QRCODE_SHARE_REDIS_KEY : usrToken);
String url = request.getRequestURI().replaceFirst("/share", EmptyUtils.EMPTY_STR);
ctx.setRequest(new HttpServletRequestWrapper(request) {
@Override
public String getRequestURI() {
return url;
}

@Override
public Map<String, String[]> getParameterMap() {
return queryParamMap;
}

@Override
//设置用于分享的Cookie(Token)参数,访问后台接口使用
public String getHeader(String name) {
if (name.equals(Constants.TOKEN_HEADER) || name.equals(WpsConst.HEAD_TOKEN)) {
return realToken;
}
return super.getHeader(name);
}
});
Map<String, List<String>> requestQueryParams = ctx.getRequestQueryParams();
if (requestQueryParams == null) {
requestQueryParams = new HashMap<>();
}
requestQueryParams.remove(Constants.SHARE_TOKEN_HEADER);
ctx.setRequestQueryParams(requestQueryParams);
ctx.put(FilterConstants.REQUEST_URI_KEY, url);
ctx.addZuulRequestHeader(Constants.TOKEN_HEADER, realToken);
return true;
}

private void sendResp(RequestContext ctx, Integer code, String errorMsg) {
ctx.setSendZuulResponse(false);
try {
ctx.setResponseStatusCode(HttpStatus.INTERNAL_SERVER_ERROR.value());
ctx.getResponse().setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value());
ctx.getResponse().getWriter().write(JSONObject.toJSONString(ResponseResult.fail(code, errorMsg)));
} catch (Exception e) {
logger.info(logger.toString());
}
}

@Override
public void run(ApplicationArguments args) throws Exception {
//初始化用于分享用到的Session数据
redisUtil.set(Constants.QRCODE_SHARE_REDIS_KEY, Constants.QRCODE_SHARE_REDIS_VAL);
}

}
  • 由于部分接口访问需要获取用户信息,先通过ApplicationRunner.run初始化分享Cookie(Token)的Session数据
  • shouldFilter方法用于判断/share/doBiz请求,是否允许经过UrlShareFilter篡改请求uri和参数,判断逻辑:匹配请求是否符合urlshare.json配置的reqUrls其中的一条uri规则,是的话就需要通过run方法篡改请求。
  • run方法根据shareToken查找此次分享的参数信息,如链接时效性 有效性 授权人并校验,其次篡改RequestUri去除/share前缀和requestParam,添加用于分享用的Cookie(Token)信息
  • 注意UrlShareFilter的优先级应该配置最高优先级
生成二维码

使用hutool工具包的QrCodeUtil类创建二维码并返回给客户端

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
java复制代码 @PostMapping(value = "/shareUrlQrcode", produces = "application/octet-stream;charset=UTF-8")
public void shareUrlQrcode(@RequestBody GetShareUrlParam getShareUrlParam) throws Exception {
try {
HttpServletResponse response = getResponse();
response.setHeader("Pragma", "No-cache");
response.setHeader("Cache-Control", "no-cache");
response.setDateHeader("Expires", 0);
response.setContentType("image/png");
String shareUrl = urlShareService.shareUrl(getShareUrlParam, getUserSn());
QrConfig qrConfig = QrConfig.create().setWidth(500).setHeight(500).setMargin(0).setImg(ImageIO.read(ResourceUtil.getStream("qrcodelog.png")));
QrCodeUtil.generate(shareUrl, qrConfig,"png", response.getOutputStream());
}catch (Exception e){
logger.error("shareUrlQrcode error,getShareUrlParam={}", getShareUrlParam, e);
throw e;
}
}

总结

生成二维码最好不要将过期时间/授权用户信息直接加密放到requestParam参数传递,因为参数大小的不确定性将会导致二维码非常密集,相机在扫描密集二维码的效果会变得很差很差。

通过shareToken,后端交由UrlShareFilter根据shareToken获取校验过期时间/授权用户;前端可以通过shareToken参数调用接口得到分享页面所需的参数信息。并且二维码链接的长度确定,二维码的扫描性能得到了保证。

本文转载自: 掘金

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

Spring Data JPA的使用

发表于 2021-10-06

JPA顾名思义就是Java Persistence API的意思,是JDK 5.0注解或XML描述对象-关系表的映射关系,并将运行期的实体对象持久化到数据库中。

  • SpringBoot使用SpringDataJPA完成CRUD操作. 数据的存储以及访问都是最为核心的关键部分,现在有很多企业采用主流的数据库,如关系型数据库:MySQL,Oracle,SQLServer。非关系型数据库:redis,mongodb等.
  • Spring Data JPA 是Spring Data 的一个子项目,它通过提供基于JPA的Repository极大了减少了操作JPA的代码。

1、导入相关依赖并配置文件

1
2
3
4
5
6
7
8
9
10
xml复制代码<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>

<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
1
2
3
4
5
6
7
8
9
10
11
12
13
yaml复制代码spring:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url:
username:
password:
jpa:
database: mysql
# 日志中显示sql语句
show-sql: true
hibernate:
naming:
physical-strategy: org.hibernate.boot.model.naming.PhysicalNamingStrategyStandardImpl

2、JPA使用

步骤一:新建实体类并添加JPA注解

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
java复制代码@Data
@AllArgsConstructor
@NoArgsConstructor

@Entity
@Table(name = "article")
public class Article implements Serializable {

@Id
@GeneratedValue
@Column(name = "a_id")
private Integer aId;
@Column(name = "article_title")
private String articleTitle;
@Column(name = "article_content")
private String articleContent;
@Column(name = "head_image")
private String headImage;
@Column(name = "article_author")
private String articleAuthor;
@Column(name = "type_number")
private Integer typeNumber;
@Column(name = "pageviews")
private Integer pageViews;
@Column(name = "create_time")
private String createTime;
@Column(name = "is_state")
private Integer isState;

}

步骤二:新建接口ArticleDao

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
java复制代码/**
* JpaRepository<T,ID> 提供简单的数据操作接口
* Article 实体类类型
* Integer 主键类型
*
* JpaSpecificationExecutor<T> 提供复杂查询接口
* Article 实体类类型
*
* Serializable 序列化
*/
@Repository
public interface ArticleDao extends JpaRepository<Article,Integer>,JpaSpecificationExecutor<Article>,Serializable{

//这里没有代码,注意没有代码..........

}

步骤三:测试

1
2
3
4
5
6
7
8
9
10
11
12
13
java复制代码@SpringBootTest
class Springboot07JpaApplicationTests {

@Autowired
private ArticleDao articleDao;

@Test
void contextLoads() {
List<Article> articleList = articleDao.findAll();
articleList.forEach(System.out::println);
}

}

3、JPA查询方法命令规范

关键字 方法命名 sql where字句
And findByNameAndPwd where name= ? and pwd =?
Or findByNameOrSex where name= ? or sex=?
Is,Equals findById,findByIdEquals where id= ?
Between findByIdBetween where id between ? and ?
LessThan findByIdLessThan where id < ?
LessThanEquals findByIdLessThanEquals where id <= ?
GreaterThan findByIdGreaterThan where id > ?
GreaterThanEquals findByIdGreaterThanEquals where id > = ?
After findByIdAfter where id > ?
Before findByIdBefore where id < ?
IsNull findByNameIsNull where name is null
isNotNull,NotNull findByNameNotNull where name is not null
Like findByNameLike where name like ?
NotLike findByNameNotLike where name not like ?
StartingWith findByNameStartingWith where name like ‘?%’
EndingWith findByNameEndingWith where name like ‘%?’
Containing findByNameContaining where name like ‘%?%’
OrderBy findByIdOrderByXDesc where id=? order by x desc
Not findByNameNot where name <> ?
In findByIdIn(Collection<?> c) where id in (?)
NotIn findByIdNotIn(Collection<?> c) where id not in (?)
True findByAaaTue where aaa = true
False findByAaaFalse where aaa = false
IgnoreCase findByNameIgnoreCase where UPPER(name)=UPPER(?)

4、JPQL语法生成

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
java复制代码public interface StandardRepository extends JpaRepository<Standard, Long> {

// JPA的命名规范
List<Standard> findByName(String name);

// 自定义查询,没有遵循命名规范
@Query("from Standard where name = ?")
Standard findByNamexxxx(String name);

// 遵循命名规范,执行多条件查询
Standard findByNameAndMaxLength(String name, Integer maxLength);

// 自定义多条件查询
@Query("from Standard where name = ?2 and maxLength = ?1")
Standard findByNameAndMaxLengthxxx(Integer maxLength, String name);

// 使用”标准”SQL查询,以前mysql是怎么写,这里继续
@Query(value = "select * from T_STANDARD where C_NAME = ? and C_MAX_LENGTH = ?",
nativeQuery = true)
Standard findByNameAndMaxLengthxx(String name, Integer maxLength);

// 模糊查询
Standard findByNameLike(String name);

@Modifying // 代表本操作是更新操作
@Transactional // 事务注解
@Query("delete from Standard where name = ?")
void deleteByName(String name);

@Modifying // 代表本操作是更新操作
@Transactional // 事务注解
@Query("update Standard set maxLength = ? where name = ?")
void updateByName(Integer maxLength, String name);
}

5、JPA URUD示例

modle:Article.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
java复制代码@Data
@AllArgsConstructor
@NoArgsConstructor

@Entity
@Table(name = "article")
public class Article implements Serializable {

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "a_id")
private int aId;
@Column(name = "article_title")
private String articleTitle;
@Column(name = "article_content")
private String articleContent;
@Column(name = "head_image")
private String headImage;
@Column(name = "article_author")
private String articleAuthor;
@Column(name = "type_number")
private int typeNumber;

private int pageviews;

@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
@Column(name = "create_time")
private Date createTime;
@Column(name = "is_state")
private int isState;
}

dao:ArticleDao.java

1
2
3
4
5
6
7
8
java复制代码public interface ArticleDao extends JpaRepository<Article, Integer>, JpaSpecificationExecutor<Article>, Serializable {

List<Article> findByArticleTitleContaining(String keywords);

//自定义方法
@Query("select art from Article art where art.articleTitle like %?1% or art.articleContent like %?1%")
Page<Article> findByLike(String keywords, Pageable pageable);
}

service:ArticleService.java

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

Page<Article> findByLike(String keywords, int page, int pageSize);

public void delArticle(int aId);

public void updateArticle(Article article);

public void addArticle(Article article);
}

serviceImpl:ArticleServiceImpl.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
java复制代码@Service
public class ArticleServiceImpl implements ArticleService {

@Autowired
private ArticleDao articleDao;

@Override
public Page<Article> findByLike(String keywords, int page, int pageSize) {
Sort sort = Sort.by(Sort.Direction.DESC, "createTime");
PageRequest pageable = PageRequest.of(page - 1, pageSize, sort);
Page<Article> pageResult = articleDao.findByLike(keywords, pageable);
return pageResult;
}

@Override
public void delArticle(int aId) {
articleDao.deleteById(aId);
}

@Override
public void updateArticle(Article article) {
articleDao.save(article);
}

@Override
public void addArticle(Article article) {
articleDao.save(article);
}
}

controller:ArticleController.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
java复制代码@RestController
@Api(value = "文章的接口", description = "文章的接口")
public class ArticleController {

@Autowired
private ArticleService articleService;

@GetMapping("Article")
public ResponseData<Article> selAllArticle(
@RequestParam(value = "keywords", required = true, defaultValue = "") String keywords,
@RequestParam(value = "page", required = true, defaultValue = "1") Integer page,
@RequestParam(value = "pageSize", required = true, defaultValue = "10") Integer pageSize) {

Page<Article> pageResult = articleService.findByLike(keywords, page, pageSize);
ResponseData<Article> rd = new ResponseData<Article>(200, "success", pageResult.getTotalElements(), pageResult.getContent());
return rd;
}

@DeleteMapping("Article/{aId}")
public void delArticleById(@PathVariable int aId) {
articleService.delArticle(aId);
}

@PutMapping("Article")
public void updateArticleById(@RequestBody Article article) {
articleService.updateArticle(article);
}

@PostMapping("Article")
public void addArticleById(@RequestBody Article article) {
articleService.addArticle(article);
}
}

6、JPA实现分页和模糊查询

modle:Article.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
java复制代码@Data
@AllArgsConstructor
@NoArgsConstructor

@Entity
@Table(name = "article")
public class Article implements Serializable {

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "a_id")
private int aId;
@Column(name = "article_title")
private String articleTitle;
@Column(name = "article_content")
private String articleContent;
@Column(name = "head_image")
private String headImage;
@Column(name = "article_author")
private String articleAuthor;
@Column(name = "type_number")
private int typeNumber;

private int pageviews;

@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
@Column(name = "create_time")
private Date createTime;
@Column(name = "is_state")
private int isState;
}

dao:ArticleDao.java

1
2
3
4
5
java复制代码public interface ArticleDao extends JpaRepository<Article, Integer>, JpaSpecificationExecutor<Article>, Serializable {

@Query("select art from Article art where art.articleTitle like %?1% or art.articleContent like %?1%")
Page<Article> findByLike(String keywords, Pageable pageable);
}

service:ArticleService.java

1
2
3
4
java复制代码public interface ArticleService {

Page<Article> findByLike(String keywords, int page, int pageSize);
}

serviceImpl:ArticleServiceImpl.java

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

@Autowired
private ArticleDao articleDao;

@Override
public Page<Article> findByLike(String keywords, int page, int pageSize) {
Sort sort = Sort.by(Sort.Direction.DESC, "createTime");
PageRequest pageable = PageRequest.of(page - 1, pageSize, sort);
Page<Article> pageResult = articleDao.findByLike(keywords, pageable);
return pageResult;
}
}

controller:ArticleController.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
java复制代码@RestController
@Api(value = "文章的接口", description = "文章的接口")
public class ArticleController {

@Autowired
private ArticleService articleService;

@GetMapping("Article")
public ResponseData<Article> selAllArticle(
@RequestParam(value = "keywords", required = true, defaultValue = "") String keywords,
@RequestParam(value = "page", required = true, defaultValue = "1") Integer page,
@RequestParam(value = "pageSize", required = true, defaultValue = "10") Integer pageSize) {

Page<Article> pageResult = articleService.findByLike(keywords, page, pageSize);
ResponseData<Article> rd = new ResponseData<Article>(200, "success", pageResult.getTotalElements(), pageResult.getContent());
return rd;
}
}

本文转载自: 掘金

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

第十三章:Redis高性能设计之epoll和IO多路复用深度

发表于 2021-10-06

常见面试题:
Redis单线程如何处理那么多并发客户端连接,为什么单线程,为什么快?

Redis的IO多路复用,Redis利用epoll来实现IO多路复用,将连接信息和事件放到队列中,一次放到文件事件分派器,事件分派器将事件分发给事件处理器。
在这里插入图片描述
Redis 是跑在单线程中的,所有的操作都是按照顺序线性执行的,但是由于读写操作等待用户输入或输出都是阻塞的,所以 I/O 操作在一般情况下往往不能直接返回,这会导致某一文件的 I/O 阻塞导致整个进程无法对其它客户提供服务,而 I/O 多路复用就是为了解决这个问题而出现

所谓 I/O 多路复用机制,就是说通过一种机制,可以监视多个描述符,一旦某个描述符就绪(一般是读就绪或写就绪),能够通知程序进行相应的读写操作。这种机制的使用需要 select 、 poll 、 epoll 来配合。多个连接共用一个阻塞对象,应用程序只需要在一个阻塞对象上等待,无需阻塞等待所有连接。当某条连接有新的数据可以处理时,操作系统通知应用程序,线程从阻塞状态返回,开始进行业务处理。

Redis 服务采用 Reactor 的方式来实现文件事件处理器(每一个网络连接其实都对应一个文件描述符) ,Redis基于Reactor模式开发了网络事件处理器,这个处理器被称为文件事件处理器。它的组成结构为4部分:

  • 多个套接字、
  • IO多路复用程序、
  • 文件事件分派器、
  • 事件处理器。

因为文件事件分派器队列的消费是单线程的,所以Redis才叫单线程模型

参考《Redis 设计与实现》书中,如下图所示
在这里插入图片描述
在这里插入图片描述

I/O多路复用模型

1.I/O :网络 I/O
2.多路:多个客户端连接(连接就是套接字描述符,即 socket 或者 channel)
3.复用:复用一个或几个线程。也就是说一个或一组线程处理多个 TCP 连接,使用单进程就能够实现同时处理多个客户端的连接

一句话:一个服务端进程可以同时处理多个套接字描述符。其发展可以分select->poll->epoll三个阶段来描述。

小文章:
上午开会,错过了公司食堂的饭点, 中午就和公司的首席架构师一起去楼下的米线店去吃米线。我们到了一看,果然很多人在排队。

架构师马上发话了:嚯,请求排队啊!你看这位收银点菜的,像不像nginx的反向代理?只收请求,不处理,把请求都发给后厨去处理。
我们交了钱,拿着号离开了点餐收银台,找了个座位坐下等餐。
架构师:你看,这就是异步处理,我们下了单就可以离开等待,米线做好了会通过小喇叭**“回调**”我们去取餐;
如果同步处理,我们就得在收银台站着等餐,后面的请求无法处理,客户等不及肯定会离开了。

接下里架构师盯着手中的纸质号牌。

架构师:你看,这个纸质号牌在后厨“服务器”那里也有,这不就是表示会话的ID吗?
有了它就可以把大家给区分开,就不会把我的排骨米线送给别人了。过了一会, 排队的人越来越多,已经有人表示不满了,可是收银员已经满头大汗,忙到极致了。

架构师:你看他这个系统缺乏弹性扩容, 现在这么多人,应该增加收银台,可以没有其他收银设备,老板再着急也没用。
老板看到在收银这里帮不了忙,后厨的订单也累积得越来越多, 赶紧跑到后厨亲自去做米线去了。

架构师又发话了:幸亏这个系统的后台有并行处理能力,可以随意地增加资源来处理请求(做米线)。
我说:他就这点儿资源了,除了老板没人再会做米线了。
不知不觉,我们等了20分钟, 但是米线还没上来。
架构师:你看,系统的处理能力达到极限,超时了吧。
这时候收银台前排队的人已经不多了,但是还有很多人在等米线。

老板跑过来让这个打扫卫生的去收银,让收银小妹也到后厨帮忙。打扫卫生的做收银也磕磕绊绊的,没有原来的小妹灵活。

架构师:这就叫服务降级,为了保证米线的服务,把别的服务都给关闭了。又过了20分钟,后厨的厨师叫道:237号, 您点的排骨米线没有排骨了,能换成番茄的吗?

架构师低声对我说:瞧瞧, 人太多, 系统异常了。然后他站了起来:不行,系统得进行补偿操作:退费。

说完,他拉着我,饿着肚子,头也不回地走了。

重要概念如下:

同步:调用者要一直等待调用结果的通知后才能进行后续的执行,
现在就要,我可以等,等出结果为止。

异步:指被调用方先返回应答让调用者先回去,然后再计算调用结果,计算完最终结果后再通知并返回给调用方,异步调用要想获得结果一般通过回调。

同步与异步的理解:同步、异步的讨论对象是被调用者(服务提供者),重点在于获得调用结果的消息通知方式上。

阻塞:调用方一直在等待而且别的事情什么都不做,当前进/线程会被挂起,啥都不干。

非阻塞:调用在发出去后,调用方先去忙别的事情,不会阻塞当前进/线程,而会立即返回。

阻塞与非阻塞的理解:阻塞、非阻塞的讨论对象是调用者(服务请求者),重点在于等消息时候的行为,调用者是否能干其它事

总结:

  • 同步阻塞:服务员说快到你了,先别离开我后台看一眼马上通知你。客户在海底捞火锅前台干等着,啥都不干。
  • 同步非阻塞:服务员说快到你了,先别离开。客户在海底捞火锅前台边刷抖音边等着叫号。
  • 异步阻塞:服务员说还要再等等,你先去逛逛,一会儿通知你。客户怕过号在海底捞火锅前台拿着排号小票啥都不干,一直等着店员通知。
  • 异步非阻塞:服务员说还要再等等,你先去逛逛,一会儿通知你。拿着排号小票+刷着抖音,等着店员通知。

Unix网络编程中的五种IO模型

  • Blocking IO - 阻塞IO
  • NoneBlocking IO - 非阻塞IO
  • IO multiplexing - IO多路复用
  • signal driven IO - 信号驱动IO
  • asynchronous IO - 异步IO

BIO

当用户进程调用了recvfrom这个系统调用,kernel就开始了IO的第一个阶段:准备数据(对于网络IO来说,很多时候数据在一开始还没有到达。比如,还没有收到一个完整的UDP包。这个时候kernel就要等待足够的数据到来)。这个过程需要等待,也就是说数据被拷贝到操作系统内核的缓冲区中是需要一个过程的。而在用户进程这边,整个进程会被阻塞(当然,是进程自己选择的阻塞)。当kernel一直等到数据准备好了,它就会将数据从kernel中拷贝到用户内存,然后kernel返回结果,用户进程才解除block的状态,重新运行起来。所以,BIO的特点就是在IO执行的两个阶段都被block了。
在这里插入图片描述
在这里插入图片描述
先演示accept监听,即socket服务端监听客户端的连接,代码演示如下:
RedisServer

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
java复制代码package com.zzyy.study.iomultiplex.one;

import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;

/**
* @auther zzyy
* @create 2020-12-06 10:14
*/
public class RedisServer
{
public static void main(String[] args) throws IOException
{
byte[] bytes = new byte[1024];

ServerSocket serverSocket = new ServerSocket(6379);

while(true)
{
System.out.println("-----111 等待连接");
Socket socket = serverSocket.accept();
System.out.println("-----222 成功连接");
}
}
}

RedisClient01如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
java复制代码package com.zzyy.study.iomultiplex.one;

import java.io.IOException;
import java.net.Socket;
import java.util.Scanner;

/**
* @auther zzyy
* @create 2020-12-06 10:20
*/
public class RedisClient01
{
public static void main(String[] args) throws IOException
{
System.out.println("------RedisClient01 start");
Socket socket = new Socket("127.0.0.1", 6379);
}
}

RedisClient02如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
java复制代码 
package com.zzyy.study.iomultiplex.one;

import java.io.IOException;
import java.net.Socket;

/**
* @auther zzyy
* @create 2020-12-06 10:20
*/
public class RedisClient02
{
public static void main(String[] args) throws IOException
{
System.out.println("------RedisClient02 start");
Socket socket = new Socket("127.0.0.1", 6379);
}
}

再演示read,即读取socket客户端发送过来的信息。
RedisServerBIO如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
java复制代码 
package com.zzyy.study.iomultiplex.bio;

import java.io.IOException;
import java.io.InputStream;
import java.net.ServerSocket;
import java.net.Socket;

/**
* @auther zzyy
* @create 2020-12-08 15:14
*/
public class RedisServerBIO
{
public static void main(String[] args) throws IOException
{

ServerSocket serverSocket = new ServerSocket(6379);

while(true)
{
System.out.println("-----111 等待连接");
Socket socket = serverSocket.accept();//阻塞1 ,等待客户端连接
System.out.println("-----222 成功连接");

InputStream inputStream = socket.getInputStream();
int length = -1;
byte[] bytes = new byte[1024];
System.out.println("-----333 等待读取");
while((length = inputStream.read(bytes)) != -1)//阻塞2 ,等待客户端发送数据
{
System.out.println("-----444 成功读取"+new String(bytes,0,length));
System.out.println("====================");
System.out.println();
}
inputStream.close();
socket.close();
}
}
}

RedisClient01如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
java复制代码 
package com.zzyy.study.iomultiplex.bio;

import java.io.IOException;
import java.io.OutputStream;
import java.net.Socket;
import java.util.Scanner;

/**
* @auther zzyy
* @create 2020-12-08 15:21
*/
public class RedisClient01
{
public static void main(String[] args) throws IOException
{
Socket socket = new Socket("127.0.0.1",6379);
OutputStream outputStream = socket.getOutputStream();

//socket.getOutputStream().write("RedisClient01".getBytes());

while(true)
{
Scanner scanner = new Scanner(System.in);
String string = scanner.next();
if (string.equalsIgnoreCase("quit")) {
break;
}
socket.getOutputStream().write(string.getBytes());
System.out.println("------input quit keyword to finish......");
}
outputStream.close();
socket.close();
}
}

RedisClient02如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
java复制代码 
package com.zzyy.study.iomultiplex.bio;

import java.io.IOException;
import java.io.OutputStream;
import java.net.Socket;
import java.util.Scanner;

/**
* @auther zzyy
* @create 2020-12-08 15:21
*/
public class RedisClient02
{
public static void main(String[] args) throws IOException
{
Socket socket = new Socket("127.0.0.1",6379);
OutputStream outputStream = socket.getOutputStream();

//socket.getOutputStream().write("RedisClient01".getBytes());

while(true)
{
Scanner scanner = new Scanner(System.in);
String string = scanner.next();
if (string.equalsIgnoreCase("quit")) {
break;
}
socket.getOutputStream().write(string.getBytes());
System.out.println("------input quit keyword to finish......");
}
outputStream.close();
socket.close();
}
}

上面的模型存在很大的问题,如果客户端与服务端建立了连接,如果这个连接的客户端迟迟不发数据,程就会一直堵塞在read()方法上,这样其他客户端也不能进行连接,也就是一次只能处理一个客户端,对客户很不友好

知道问题所在了,请问如何解决??

多线程模式

只要连接了一个socket,操作系统分配一个线程来处理,这样read()方法堵塞在每个具体线程上而不堵塞主线程,就能操作多个socket了,哪个线程中的socket有数据,就读哪个socket,各取所需,灵活统一。

程序服务端只负责监听是否有客户端连接,使用 accept() 阻塞
客户端1连接服务端,就开辟一个线程(thread1)来执行 read() 方法,程序服务端继续监听

客户端2连接服务端,也开辟一个线程(thread2)来执行 read() 方法,程序服务端继续监听

客户端3连接服务端,也开辟一个线程(thread3)来执行 read() 方法,程序服务端继续监听
。。。。。。

任何一个线程上的socket有数据发送过来,read()就能立马读到,cpu就能进行处理。

代码修改如下:
RedisServerBIOMultiThread

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
java复制代码package com.zzyy.study.iomultiplex.bio;

import java.io.IOException;
import java.io.InputStream;
import java.net.ServerSocket;
import java.net.Socket;

/**
* @auther zzyy
* @create 2020-12-08 16:13
*
*/
public class RedisServerBIOMultiThread
{
public static void main(String[] args) throws IOException
{
ServerSocket serverSocket = new ServerSocket(6379);

while(true)
{
//System.out.println("-----111 等待连接");
Socket socket = serverSocket.accept();//阻塞1 ,等待客户端连接
//System.out.println("-----222 成功连接");

new Thread(() -> {
try {
InputStream inputStream = socket.getInputStream();
int length = -1;
byte[] bytes = new byte[1024];
System.out.println("-----333 等待读取");
while((length = inputStream.read(bytes)) != -1)//阻塞2 ,等待客户端发送数据
{
System.out.println("-----444 成功读取"+new String(bytes,0,length));
System.out.println("====================");
System.out.println();
}
inputStream.close();
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
},Thread.currentThread().getName()).start();

System.out.println(Thread.currentThread().getName());

}
}
}

RedisClient01如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
java复制代码 
package com.zzyy.study.iomultiplex.bio;

import java.io.IOException;
import java.io.OutputStream;
import java.net.Socket;
import java.util.Scanner;

/**
* @auther zzyy
* @create 2020-12-08 15:21
*/
public class RedisClient01
{
public static void main(String[] args) throws IOException
{
Socket socket = new Socket("127.0.0.1",6379);
OutputStream outputStream = socket.getOutputStream();

//socket.getOutputStream().write("RedisClient01".getBytes());

while(true)
{
Scanner scanner = new Scanner(System.in);
String string = scanner.next();
if (string.equalsIgnoreCase("quit")) {
break;
}
socket.getOutputStream().write(string.getBytes());
System.out.println("------input quit keyword to finish......");
}
outputStream.close();
socket.close();
}
}

RedisClient02如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
java复制代码 
package com.zzyy.study.iomultiplex.bio;

import java.io.IOException;
import java.io.OutputStream;
import java.net.Socket;
import java.util.Scanner;

/**
* @auther zzyy
* @create 2020-12-08 15:21
*/
public class RedisClient02
{
public static void main(String[] args) throws IOException
{
Socket socket = new Socket("127.0.0.1",6379);
OutputStream outputStream = socket.getOutputStream();

//socket.getOutputStream().write("RedisClient01".getBytes());

while(true)
{
Scanner scanner = new Scanner(System.in);
String string = scanner.next();
if (string.equalsIgnoreCase("quit")) {
break;
}
socket.getOutputStream().write(string.getBytes());
System.out.println("------input quit keyword to finish......");
}
outputStream.close();
socket.close();
}
}

存在的问题

多线程模型:每来一个客户端,就要开辟一个线程,如果来1万个客户端,那就要开辟1万个线程。在操作系统中用户态不能直接开辟线程,需要调用内核来创建的一个线程,这其中还涉及到用户状态的切换(上下文的切换),十分耗资源。

知道问题所在了,请问如何解决??

解决

第一个办法:使用线程池

这个在客户端连接少的情况下可以使用,但是用户量大的情况下,你不知道线程池要多大,太大了内存可能不够,也不可行。

第二个办法:NIO(非阻塞式IO)方式
因为read()方法堵塞了,所有要开辟多个线程,如果什么方法能使read()方法不堵塞,这样就不用开辟多个线程了,这就用到了另一个IO模型,NIO(非阻塞式IO)

tomcat7之前就是用BIO多线程来解决多连接

NIO

当用户进程发出read操作时,如果kernel中的数据还没有准备好,那么它并不会block用户进程,而是立刻返回一个error。从用户进程角度讲 ,它发起一个read操作后,并不需要等待,而是马上就得到了一个结果。用户进程判断结果是一个error时,它就知道数据还没有准备好,于是它可以再次发送read操作。一旦kernel中的数据准备好了,并且又再次收到了用户进程的system call,那么它马上就将数据拷贝到了用户内存,然后返回。所以,NIO特点是用户进程需要不断的主动询问内核数据准备好了吗?
在这里插入图片描述
在非阻塞式 I/O 模型中,应用程序把一个套接口设置为非阻塞,就是告诉内核,当所请求的I/O操作无法完成时,不要将进程睡眠而是返回一个“错误”,应用程序基于 I/O 操作函数将不断的轮询数据是否已经准备好,如果没有准备好,继续轮询,直到数据准备好为止。

面试总结回答

在NIO模式中,一切都是非阻塞的:

  • accept()方法是非阻塞的,如果没有客户端连接,就返回error
  • read()方法是非阻塞的,如果read()方法读取不到数据就返回error,如果读取到数据时只阻塞read()方法读数据的时间

在NIO模式中,只有一个线程:
当一个客户端与服务端进行连接,这个socket就会加入到一个数组中,隔一段时间遍历一次,看这个socket的read()方法能否读到数据,这样一个线程就能处理多个客户端的连接和读取了

上述以前BIO的socket是阻塞的,另外开发一套API——ServerSocketChannel
在这里插入图片描述
RedisServerNIO代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
java复制代码package com.zzyy.study.iomultiplex.nio;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.ArrayList;

/**
* @auther zzyy
* @create 2020-12-06 11:40
*/
public class RedisServerNIO
{
static ArrayList<SocketChannel> socketList = new ArrayList<>();
static ByteBuffer byteBuffer = ByteBuffer.allocate(1024);

public static void main(String[] args) throws IOException
{
System.out.println("---------RedisServerNIO 启动等待中......");
ServerSocketChannel serverSocket = ServerSocketChannel.open();
serverSocket.bind(new InetSocketAddress("127.0.0.1",6379));
serverSocket.configureBlocking(false);//设置为非阻塞模式

while (true)
{
for (SocketChannel element : socketList)
{
int read = element.read(byteBuffer);
if(read > 0)
{
System.out.println("-----读取数据: "+read);
byteBuffer.flip();
byte[] bytes = new byte[read];
byteBuffer.get(bytes);
System.out.println(new String(bytes));
byteBuffer.clear();
}
}

SocketChannel socketChannel = serverSocket.accept();
if(socketChannel != null)
{
System.out.println("-----成功连接: ");
socketChannel.configureBlocking(false);//设置为非阻塞模式
socketList.add(socketChannel);
System.out.println("-----socketList size: "+socketList.size());
}
}
}
}

RedisClient01代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
java复制代码package com.zzyy.study.iomultiplex.nio;

import java.io.IOException;
import java.io.OutputStream;
import java.net.Socket;
import java.util.Scanner;

/**
* @auther zzyy
* @create 2020-12-06 10:20
*/
public class RedisClient01
{
public static void main(String[] args) throws IOException
{
System.out.println("------RedisClient01 start");
Socket socket = new Socket("127.0.0.1",6379);
OutputStream outputStream = socket.getOutputStream();
while(true)
{
Scanner scanner = new Scanner(System.in);
String string = scanner.next();
if (string.equalsIgnoreCase("quit")) {
break;
}
socket.getOutputStream().write(string.getBytes());
System.out.println("------input quit keyword to finish......");
}
outputStream.close();
socket.close();
}
}

RedisClient02代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
java复制代码package com.zzyy.study.iomultiplex.nio;

import java.io.IOException;
import java.io.OutputStream;
import java.net.Socket;
import java.util.Scanner;

/**
* @auther zzyy
* @create 2020-12-06 10:2asds7
*/
public class RedisClient02
{
public static void main(String[] args) throws IOException
{
System.out.println("------RedisClient02 start");


Socket socket = new Socket("127.0.0.1",6379);
OutputStream outputStream = socket.getOutputStream();

while(true)
{
Scanner scanner = new Scanner(System.in);
String string = scanner.next();
if (string.equalsIgnoreCase("quit")) {
break;
}
socket.getOutputStream().write(string.getBytes());
System.out.println("------input quit keyword to finish......");
}
outputStream.close();
socket.close();
}
}

存在的问题和优缺点

NIO成功的解决了BIO需要开启多线程的问题,NIO中一个线程就能解决多个socket,但是还存在2个问题。

问题一:
这个模型在客户端少的时候十分好用,但是客户端如果很多,
比如有1万个客户端进行连接,那么每次循环就要遍历1万个socket,如果一万个socket中只有10个socket有数据,也会遍历一万个socket,就会做很多无用功,每次遍历遇到 read 返回 -1 时仍然是一次浪费资源的系统调用。

问题二:
而且这个遍历过程是在用户态进行的,用户态判断socket是否有数据还是调用内核的read()方法实现的,这就涉及到用户态和内核态的切换,每遍历一个就要切换一次,开销很大因为这些问题的存在。

优点:不会阻塞在内核的等待数据过程,每次发起的 I/O 请求可以立即返回,不用阻塞等待,实时性较好。

缺点:轮询将会不断地询问内核,这将占用大量的 CPU 时间,系统资源利用率较低,所以一般 Web 服务器不使用这种 I/O 模型。

结论:让Linux内核搞定上述需求,我们将一批文件描述符通过一次系统调用传给内核由内核层去遍历,才能真正解决这个问题。IO多路复用应运而生,也即将上述工作直接放进Linux内核,不再两态转换而是直接从内核获得结果,因为内核是非阻塞的。

问题升级:如何用单线程处理大量的链接?

IO Multiplexing(IO多路复用)

IO multiplexing就是我们说的select,poll,epoll,有些地方也称这种IO方式为event driven IO事件驱动IO。就是通过一种机制,一个进程可以监视多个描述符,一旦某个描述符就绪(一般是读就绪或者写就绪),能够通知程序进行相应的读写操作。可以基于一个阻塞对象,同时在多个描述符上等待就绪,而不是使用多个线程(每个文件描述符一个线程,每次new一个线程),这样可以大大节省系统资源。所以,I/O 多路复用的特点是通过一种机制一个进程能同时等待多个文件描述符而这些文件描述符(套接字描述符)其中的任意一个进入读就绪状态,select()函数就可以返回。

在这里插入图片描述
I/O多路复用在英文中其实叫 I/O multiplexing
在这里插入图片描述
I/O multiplexing 这里面的 multiplexing 指的其实是在单个线程通过记录跟踪每一个Sock(I/O流)的状态来同时管理多个I/O流. 目的是尽量多的提高服务器的吞吐能力。
在这里插入图片描述
大家都用过nginx,nginx使用epoll接收请求,ngnix会有很多链接进来, epoll会把他们都监视起来,然后像拨开关一样,谁有数据就拨向谁,然后调用相应的代码处理。redis类似同理

文件描述符(File descriptor)是计算机科学中的一个术语,是一个用于表述指向文件的引用的抽象化概念。文件描述符在形式上是一个非负整数。实际上,它是一个索引值,指向内核为每一个进程所维护的该进程打开文件的记录表。当程序打开一个现有文件或者创建一个新文件时,内核向进程返回一个文件描述符。在程序设计中,一些涉及底层的程序编写往往会围绕着文件描述符展开。但是文件描述符这一概念往往只适用于UNIX、Linux这样的操作系统。
在这里插入图片描述
说人话:

模拟一个tcp服务器处理30个客户socket。
假设你是一个监考老师,让30个学生解答一道竞赛考题,然后负责验收学生答卷,你有下面几个选择:

第一种选择:按顺序逐个验收,先验收A,然后是B,之后是C、D。。。这中间如果有一个学生卡住,全班都会被耽误,你用循环挨个处理socket,根本不具有并发能力。

第二种选择:你创建30个分身线程,每个分身线程检查一个学生的答案是否正确。 这种类似于为每一个用户创建一个进程或者线程处理连接。

第三种选择,你站在讲台上等,谁解答完谁举手。这时C、D举手,表示他们解答问题完毕,你下去依次检查C、D的答案,然后继续回到讲台上等。此时E、A又举手,然后去处理E和A。。。这种就是IO复用模型。Linux下的select、poll和epoll就是干这个的。

将用户socket对应的fd注册进epoll,然后epoll帮你监听哪些socket上有消息到达,这样就避免了大量的无用操作。此时的socket应该采用非阻塞模式。这样,整个过程只在调用select、poll、epoll这些调用的时候才会阻塞,收发客户消息是不会阻塞的,整个进程或者线程就被充分利用起来,这就是事件驱动,所谓的reactor反应模式。

Reactor设计模式
基于 I/O 复用模型:多个连接共用一个阻塞对象,应用程序只需要在一个阻塞对象上等待,无需阻塞等待所有连接。当某条连接有新的数据可以处理时,操作系统通知应用程序,线程从阻塞状态返回,开始进行业务处理。

Reactor 模式,是指通过一个或多个输入同时传递给服务处理器的服务请求的事件驱动处理模式。服务端程序处理传入多路请求,并将它们同步分派给请求对应的处理线程,Reactor 模式也叫 Dispatcher 模式。即 I/O 多了复用统一监听事件,收到事件后分发(Dispatch 给某进程),是编写高性能网络服务器的必备技术。
在这里插入图片描述
Reactor 模式中有 2 个关键组成:
1)Reactor:Reactor 在一个单独的线程中运行,负责监听和分发事件,分发给适当的处理程序来对 IO 事件做出反应。 它就像公司的电话接线员,它接听来自客户的电话并将线路转移到适当的联系人;
2)Handlers:处理程序执行 I/O 事件要完成的实际事件,类似于客户想要与之交谈的公司中的实际办理人。Reactor 通过调度适当的处理程序来响应 I/O 事件,处理程序执行非阻塞操作。

redis为什么是单线程
在这里插入图片描述
Redis 服务采用 Reactor 的方式来实现文件事件处理器(每一个网络连接其实都对应一个文件描述符)

Redis基于Reactor模式开发了网络事件处理器,这个处理器被称为文件事件处理器。它的组成结构为4部分:

  • 多个套接字、
  • IO多路复用程序、
  • 文件事件分派器、
  • 事件处理器。因为文件事件分派器队列的消费是单线程的,所以Redis才叫单线程模型

select, poll, epoll 都是I/O多路复用的具体的实现

所谓 I/O 多路复用机制指内核一旦发现进程指定的一个或者多个IO条件准备读取,它就通知该进程,就是说通过一种机制,可以监视多个描述符,一旦某个描述符就绪(一般是读就绪或写就绪),能够通知程序进行相应的读写操作。这种机制的使用需要 select 、 poll 、 epoll 来配合。

多个连接共用一个阻塞对象,应用程序只需要在一个阻塞对象上等待,无需阻塞等待所有连接。

当某条连接有新的数据可以处理时,操作系统通知应用程序,线程从阻塞状态返回,开始进行业务处理。

select方法

在这里插入图片描述
在这里插入图片描述
select是第一个实现 (1983 左右在BSD里面实现)

C语言代码如下:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
优点
select 其实就是把NIO中用户态要遍历的fd数组(我们的每一个socket链接,安装进ArrayList里面的那个)拷贝到了内核态,让内核态来遍历,因为用户态判断socket是否有数据还是要调用内核态的,所有拷贝到内核态后,这样遍历判断的时候就不用一直用户态和内核态频繁切换了

从代码中可以看出,select系统调用后,返回了一个置位后的&rset,这样用户态只需进行很简单的二进制比较,就能很快知道哪些socket需要read数据,有效提高了效率。
在这里插入图片描述
问题

1、bitmap最大1024位,一个进程最多只能处理1024个客户端

2、&rset不可重用,每次socket有数据就相应的位会被置位

3、文件描述符数组拷贝到了内核态(只不过无系统调用切换上下文的开销。(内核层可优化为异步事件通知)),仍然有开销。select 调用需要传入 fd 数组,需要拷贝一份到内核,高并发场景下这样的拷贝消耗的资源是惊人的。(可优化为不复制)

4、select并没有通知用户态哪一个socket有数据,仍然需要O(n)的遍历。select 仅仅返回可读文件描述符的个数,具体哪个可读还是要用户自己遍历。(可优化为只返回给用户就绪的文件描述符,无需用户做无效的遍历)

我们自己模拟写的是,RedisServerNIO.java,只不过将它内核化了。

select小结论

select方式,既做到了一个线程处理多个客户端连接(文件描述符),又减少了系统调用的开销(多个文件描述符只有一次 select 的系统调用 + N次就绪状态的文件描述符的 read 系统调用

poll方法

在这里插入图片描述
1997年实现了poll

C语言代码
在这里插入图片描述
在这里插入图片描述
优点
1、poll使用pollfd数组来代替select中的bitmap,数组没有1024的限制,可以一次管理更多的client。它和 select 的主要区别就是,去掉了 select 只能监听 1024 个文件描述符的限制。

2、当pollfds数组中有事件发生,相应的revents置位为1,遍历的时候又置位回零,实现了pollfd数组的重用

问题

poll 解决了select缺点中的前两条,其本质原理还是select的方法,还存在select中原来的问题

1、pollfds数组拷贝到了内核态,仍然有开销
2、poll并没有通知用户态哪一个socket有数据,仍然需要O(n)的遍历

epoll方法

在这里插入图片描述
在这里插入图片描述
在2002年被大神 Davide Libenzi (戴维德·利本兹)发明出来了

三步调用

  • epoll_create:创建一个 epoll 句柄
  • epoll_ctl:向内核添加、修改或删除要监控的文件描述符
  • epoll_wait:类似发起了select() 调用

C语言代码
在这里插入图片描述
在这里插入图片描述
事件通知机制

1、当有网卡上有数据到达了,首先会放到DMA(内存中的一个buffer,网卡可以直接访问这个数据区域)中

2、网卡向cpu发起中断,让cpu先处理网卡的事

3、中断号在内存中会绑定一个回调,哪个socket中有数据,回调函数就把哪个socket放入就绪链表中

结论

多路复用快的原因在于,操作系统提供了这样的系统调用,使得原来的 while 循环里多次系统调用,
变成了一次系统调用 + 内核层遍历这些文件描述符。
epoll是现在最先进的IO多路复用器,Redis、Nginx,linux中的Java NIO都使用的是epoll。
这里“多路”指的是多个网络连接,“复用”指的是复用同一个线程。
1、一个socket的生命周期中只有一次从用户态拷贝到内核态的过程,开销小
2、使用event事件通知机制,每次socket中有数据会主动通知内核,并加入到就绪链表中,不需要遍历所有的socket

在多路复用IO模型中,会有一个内核线程不断地去轮询多个 socket 的状态,只有当真正读写事件发送时,才真正调用实际的IO读写操作。因为在多路复用IO模型中,只需要使用一个线程就可以管理多个socket,系统不需要建立新的进程或者线程,也不必维护这些线程和进程,并且只有真正有读写事件进行时,才会使用IO资源,所以它大大减少来资源占用。多路I/O复用模型是利用 select、poll、epoll 可以同时监察多个流的 I/O 事件的能力,在空闲的时候,会把当前线程阻塞掉,当有一个或多个流有 I/O 事件时,就从阻塞态中唤醒,于是程序就会轮询一遍所有的流(epoll 是只轮询那些真正发出了事件的流),并且只依次顺序的处理就绪的流,这种做法就避免了大量的无用操作。 采用多路 I/O 复用技术可以让单个线程高效的处理多个连接请求(尽量减少网络 IO 的时间消耗),且 Redis 在内存中操作数据的速度非常快,也就是说内存内的操作不会成为影响Redis性能的瓶颈

三个方法对比如下:
在这里插入图片描述

总结

多路复用快的原因在于,操作系统提供了这样的系统调用,使得原来的 while 循环里多次系统调用,变成了一次系统调用 + 内核层遍历这些文件描述符。

在这里插入图片描述
在这里插入图片描述
为什么3个都保有?
在这里插入图片描述
在这里插入图片描述

本文转载自: 掘金

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

第五章:布隆过滤器BloomFilter

发表于 2021-10-06

粉丝反馈回来的考题如下:

现有50亿个电话号码,现有10万个电话号码,如何要快速准确的判断这些电话号码是否已经存在?

1、通过数据库查询——-实现快速有点难。

2、数据预放到内存集合中:50亿*8字节大约40G,内存太大了

基本介绍

布隆过滤器(英语:Bloom Filter)是 1970 年由布隆提出的。它实际上是一个很长的二进制数组+一系列随机hash算法映射函数,主要用于判断一个元素是否在集合中。

通常我们会遇到很多要判断一个元素是否在某个集合中的业务场景,一般想到的是将集合中所有元素保存起来,然后通过比较确定。链表、树、散列表(又叫哈希表,Hash table)等等数据结构都是这种思路。

但是随着集合中元素的增加,我们需要的存储空间也会呈现线性增长,最终达到瓶颈。同时检索速度也越来越慢,上述三种结构的检索时间复杂度分别为O(n),O(logn),O(1)。这个时候,布隆过滤器(Bloom Filter)就应运而生。
在这里插入图片描述
一句话:由一个初值都为零的bit数组和多个哈希函数构成,用来快速判断某个数据是否存在。
在这里插入图片描述
本质就是判断具体数据存不存在一个大的集合中,布隆过滤器是一种类似set的数据结构,只是统计结果不太准确。

布隆过滤器特点

  • 高效地插入和查询,占用空间少,返回的结果是不确定性的。
  • 一个元素如果判断结果为存在的时候元素不一定存在,但是判断结果为不存在的时候则一定不存在。
  • 布隆过滤器可以添加元素,但是不能删除元素。因为删掉元素会导致误判率增加。
  • 误判只会发生在过滤器没有添加过的元素,对于添加过的元素不会发生误判。

可以保证的是:如果布隆过滤器判断一个元素不在一个集合中,那这个元素一定不会在集合中,即,有,是可能有,无,是肯定无

使用场景

1.解决缓存穿透的问题
缓存穿透是一般情况下,先查询缓存redis是否有该条数据,缓存中没有时,再查询数据库。当数据库也不存在该条数据时,每次查询都要访问数据库,这就是缓存穿透。缓存透带来的问题是,当有大量请求查询数据库不存在的数据时,就会给数据库带来压力,甚至会拖垮数据库。

可以使用布隆过滤器解决缓存穿透的问题,把已存在数据的key存在布隆过滤器中,相当于redis前面挡着一个布隆过滤器。当有新的请求时,先到布隆过滤器中查询是否存在:

  • 如果布隆过滤器中不存在该条数据则直接返回;
  • 如果布隆过滤器中已存在,才去查询缓存redis,如果redis里没查询到则穿透到Mysql数据库

2.黑名单校验

发现存在黑名单中的,就执行特定操作。比如:识别垃圾邮件,只要是邮箱在黑名单中的邮件,就识别为垃圾邮件。

假设黑名单的数量是数以亿计的,存放起来就是非常耗费存储空间的,布隆过滤器则是一个较好的解决方案。

把所有黑名单都放在布隆过滤器中,在收到邮件时,判断邮件地址是否在布隆过滤器中即可。

布隆过滤器原理

在Java中传统hash中,哈希函数的概念是:将任意大小的输入数据转换成特定大小的输出数据的函数,转换后的数据称为哈希值或哈希编码,也叫散列值
在这里插入图片描述
如果两个散列值是不相同的(根据同一函数)那么这两个散列值的原始输入也是不相同的。

这个特性是散列函数具有确定性的结果,具有这种性质的散列函数称为单向散列函数。

散列函数的输入和输出不是唯一对应关系的,如果两个散列值相同,两个输入值很可能是相同的,但也可能不同,这种情况称为“散列碰(collision)”。

用 hash表存储大数据量时,空间效率还是很低,当只有一个 hash 函数时,还很容易发生哈希碰撞。

Java中hash冲突java案例,代码演示如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
java复制代码 
public class HashCodeConflictDemo
{
public static void main(String[] args)
{
Set<Integer> hashCodeSet = new HashSet<>();

for (int i = 0; i <200000; i++) {
int hashCode = new Object().hashCode();
if(hashCodeSet.contains(hashCode)) {
System.out.println("出现了重复的hashcode: "+hashCode+"\t 运行到"+i);
break;
}
hashCodeSet.add(hashCode);
}

System.out.println("Aa".hashCode());
System.out.println("BB".hashCode());
System.out.println("柳柴".hashCode());
System.out.println("柴柕".hashCode());

}
}

布隆过滤器(Bloom Filter) 是一种专门用来解决去重问题的高级数据结构。
实质就是一个大型位数组和几个不同的无偏hash函数(无偏表示分布均匀)。由一个初值都为零的bit数组和多个个哈希函数构成,用来快速判断某个数据是否存在。但是跟 HyperLogLog 一样,它也一样有那么一点点不精确,也存在一定的误判概率。
在这里插入图片描述
添加key时,使用多个hash函数对key进行hash运算得到一个整数索引值,对位数组长度进行取模运算得到一个位置,每个hash函数都会得到一个不同的位置,将这几个位置都置1就完成了add操作。

查询key时,只要有其中一位是零就表示这个key不存在,但如果都是1,则不一定存在对应的key。

结论:有,是可能有,无,是肯定无

当有变量被加入集合时,通过N个映射函数将这个变量映射成位图中的N个点,把它们置为 1(假定有两个变量都通过 3 个映射函数)。
在这里插入图片描述
查询某个变量的时候我们只要看看这些点是不是都是 1, 就可以大概率知道集合中有没有它了

如果这些点,有任何一个为零则被查询变量一定不在,如果都是 1,则被查询变量很可能存在,为什么说是可能存在,而不是一定存在呢?那是因为映射函数本身就是散列函数,散列函数是会有碰撞的。

在这里插入图片描述
布隆过滤器的三个步骤:
1.初始化
布隆过滤器 本质上 是由长度为 m 的位向量或位列表(仅包含 0 或 1 位值的列表)组成,最初所有的值均设置为 0。
在这里插入图片描述
2.添加
当我们向布隆过滤器中添加数据时,为了尽量地址不冲突,会使用多个 hash 函数对 key 进行运算,算得一个下标索引值,然后对位数组长度进行取模运算得到一个位置,每个 hash 函数都会算得一个不同的位置。再把位数组的这几个位置都置为 1 就完成了 add 操作。例如,我们添加一个字符串wmyskxz。
在这里插入图片描述
3.判断是否存在
向布隆过滤器查询某个key是否存在时,先把这个 key 通过相同的多个 hash 函数进行运算,查看对应的位置是否都为 1,只要有一个位为 0,那么说明布隆过滤器中这个 key 不存在;如果这几个位置全都是 1,那么说明极有可能存在;因为这些位置的 1 可能是因为其他的 key 存在导致的,也就是前面说过的hash冲突。。。。。

就比如我们在 add 了字符串wmyskxz数据之后,很明显下面1/3/5 这几个位置的 1 是因为第一次添加的 wmyskxz 而导致的;此时我们查询一个没添加过的不存在的字符串inexistent-key,它有可能计算后坑位也是1/3/5 ,这就是误判了……
在这里插入图片描述
在这里插入图片描述

布隆过滤器的误判是指多个输入经过哈希之后在相同的bit位置1了,这样就无法判断究竟是哪个输入产生的,因此误判的根源在于相同的 bit 位被多次映射且置 1。这种情况也造成了布隆过滤器的删除问题,因为布隆过滤器的每一个 bit 并不是独占的,很有可能多个元素共享了某一位。如果我们直接删除这一位的话,会影响其他的元素

特性

  • 一个元素判断结果为没有时则一定没有,
  • 如果判断结果为存在的时候元素不一定存在。

布隆过滤器可以添加元素,但是不能删除元素。因为删掉元素会导致误判率增加。

小总结

  • 可以保证的是,如果布隆过滤器判断一个元素不在一个集合中,那这个元素一定不会在集合中
  • 使用时最好不要让实际元素数量远大于初始化数量
  • 当实际元素数量超过初始化数量时,应该对布隆过滤器进行重建,重新分配一个 size 更大的过滤器,再将所有的历史元素批量 add 进行

布隆过滤器优缺点
1.优点:高效地插入和查询,占用空间少

2.缺点:

  • 不能删除元素。因为删掉元素会导致误判率增加,因为hash冲突同一个位置可能存的东西是多个共有的,你删除一个元素的同时可能也把其它的删除了。
  • 存在误判不同的数据可能出来相同的hash值。

布谷鸟过滤器(了解)

为了解决布隆过滤器不能删除元素的问题,布谷鸟过滤器横空出世。论文《Cuckoo Filter:Better Than Bloom》

作者将布谷鸟过滤器和布隆过滤器进行了深入的对比。相比布谷鸟过滤器而言布隆过滤器有以下不足:查询性能弱、空间利用效率低、不支持反向操作(删除)以及不支持计数

本文转载自: 掘金

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

pip 常用命令与国内源配置

发表于 2021-10-06
  • 小知识,大挑战!本文正在参与“程序员必备小知识”创作活动。

pip默认源存在速度慢的问题,本文介绍pip命令添加国内源的方法。

pip常用命令

安装包

1
2
复制代码pip install Package
pip install -r requirements.txt

更新包

1
复制代码pip install -U Package

卸载包

1
复制代码pip uninstall Package

列出已安装软件

1
2
3
复制代码pip list
pip freeze
pip freeze -r requirements.txt

某个包详细信息

1
sql复制代码pip show -f Package

国内源配置

常用的国内镜像

  • 阿里云 mirrors.aliyun.com/pypi/simple…
  • 豆瓣pypi.douban.com/simple/
  • 清华大学 pypi.tuna.tsinghua.edu.cn/simple/
  • 中国科学技术大学 pypi.mirrors.ustc.edu.cn/simple/
  • 华中科技大学pypi.hustunique.com/

新版ubuntu要求使用https源,要注意。

就我的经验来说阿里云是目前用下来最稳定可靠的。

临时使用

在使用pip的时候,加上参数 -i 和镜像地址

1
shell复制代码pip install -i https://mirrors.aliyun.com/pypi/simple/ tensorboard

从清华源安装 tensorboard

命令配置

在终端输入命令:

1
2
shell复制代码pip install pip -U    # 升级pip到最新版本
pip config set global.index-url https://mirrors.aliyun.com/pypi/simple/

此方法配置后安装包时可能会提示下载源不可信,需要手动加上--trusted-host mirrors.aliyun.com

1
kotlin复制代码The repository located at mirrors.aliyun.com is not a trusted or secure host and is being ignored. If this repository is available via HTTPS we recommend you use HTTPS instead, otherwise you may silence this warning and allow it anyway with '--trusted-host mirrors.aliyun.com'.

这样就不那么香了,一劳永逸的解决方案是修改pip配置文件

修改配置

Linux

配置文件位置:

  • ~/.pip/pip.conf
  • ~/.config/pip/pip.conf
1
2
3
ini复制代码[global]
index-url = https://mirrors.aliyun.com/pypi/simple/
trusted-host = mirrors.aliyun.com
Windows

配置文件位置:

  • %HOMEPATH%\pip\pip.ini,即 C:\Users\pip\pip.ini
  • %APPDATA%\pip\pip.ini,即C:\Users\AppData\Roaming\pip\pip.ini

可以通过在cmd中输入 echo %APPDATA% 或 echo %HOMEPATH% 查看自己的相关路径

1
2
3
4
yml复制代码[global]
index-url = https://mirrors.aliyun.com/pypi/simple/
[install]
trusted-host = mirrors.aliyun.com

调整源地址

  • http不可信,将源地址修改为 https 也可以避免信任危机
1
2
ini复制代码[global]
index-url = https://mirrors.aliyun.com/pypi/simple/

本文转载自: 掘金

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

linux系统查看命令(必看)

发表于 2021-10-06

SSH连接工具

1
2
3
4
5
6
bash复制代码# 工具一:xshell
这是个熟悉的软件啦,目前我正在使用Xshell_7
# 工具二:FinalShell
国产软件,有windows和MAC版本;使用方便而且免费,但是软件比较占用内存。但是都2021年了,笔记本电脑内存都16G起步,问题不大的。
# 工具三:SecureCRT
软件比较专业,一般是英文界面;经常使用linux,使用这款软件是不错的选择。

查看系统版本信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
bash复制代码# lsb_release -a 有的linux系统里面没有这个命令
# 可以使用 cat /etc/centos-release查看

[root@ncayu618 ~]# lsb_release -a
LSB Version: :core-4.1-amd64:core-4.1-noarch
Distributor ID: CentOS
Description: CentOS Linux release 7.9.2009 (Core)
Release: 7.9.2009
Codename: Core

#查看 Linux 版本名称.

[root@ncayu8847 ~]# cat /etc/centos-release
CentOS Linux release 7.5.1804 (Core)

#显示正在运行的内核版本。

[root@ncayu8847 ~]# cat /proc/version
Linux version 3.10.0-862.14.4.el7.x86_64 (mockbuild@kbuilder.bsys.centos.org) (gcc version 4.8.5 20150623 (Red Hat 4.8.5-28) (GCC) ) #1 SMP Wed Sep 26 15:12:11 UTC 2018

#显示电脑以及操作系统的相关信息。

[root@ncayu8847 ~]# uname -a
Linux ncayu8847 3.10.0-862.14.4.el7.x86_64 #1 SMP Wed Sep 26 15:12:11 UTC 2018 x86_64 x86_64 x86_64 GNU/Linux

#查看Linux系统架构,这样我们就可以下载对应的软件包进行安装。

[root@ncayu8847 ~]# arch
x86_64

查看IP地址

1
2
3
4
5
bash复制代码# linux中查看IP地址
ifconfig (通常使用)
ip addr (可以代替ifconfig)可以简写成ip a
# 过滤出IP地址,可用于写shell脚本。
ifconfig -a | grep inet | grep -v 127.0.0.1 | grep -v inet6 | awk '{print $2}'|tr -d "addr:"|awk 'BEGIN{RS="\n";ORS=" ";}{print $0}'
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
bash复制代码[root@ncayu8847 ~]# ip addr
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
inet 127.0.0.1/8 scope host lo
valid_lft forever preferred_lft forever
2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UP group default qlen 1000
link/ether 00:16:3e:16:3c:2b brd ff:ff:ff:ff:ff:ff
inet 172.18.3.0/20 brd 172.18.15.255 scope global dynamic eth0
valid_lft 310954450sec preferred_lft 310954450sec
[root@ncayu8847 ~]# ip a
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
inet 127.0.0.1/8 scope host lo
valid_lft forever preferred_lft forever
2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UP group default qlen 1000
link/ether 00:16:3e:16:3c:2b brd ff:ff:ff:ff:ff:ff
inet 172.18.3.0/20 brd 172.18.15.255 scope global dynamic eth0
valid_lft 310954378sec preferred_lft 310954378sec

查看cpu信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
bash复制代码# 查看CPU信息
cat /proc/cpuinfo

# 查看内存信息
cat /proc/meminfo

#显示系统时间和平均负载
uptime w who top 命令

top 命令
* 使用top命令后可以按键盘数字“1”;可以查看单个CPU的使用情况
* 使用top命令后可以按键盘数字“2”;可以查看单个内存的使用情况

# 日志管理:分析工具:ELK
# 出现问题一定要先查看日志,/var/log message日志,如果系统出现问题,首先检查的就是这个日志文件。

# 查看内存,磁盘
df -h
free -m

查看内存磁盘信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
bash复制代码# 磁盘信息

[root@ncayu8847 ~]# df -h
文件系统 容量 已用 可用 已用% 挂载点
/dev/vda1 40G 32G 5.4G 86% /
devtmpfs 1.9G 0 1.9G 0% /dev
tmpfs 1.9G 0 1.9G 0% /dev/shm
tmpfs 1.9G 720K 1.9G 1% /run
tmpfs 1.9G 0 1.9G 0% /sys/fs/cgroup
tmpfs 379M 0 379M 0% /run/user/0
/dev/vdb 100G 63G 38G 63% /ncayu

# 内存信息

[root@ncayu8847 ~]# free -m
total used free shared buff/cache available
Mem: 3789 1768 118 0 1902 1736
Swap: 0 0 0

文件上传下载

1
2
3
4
5
6
7
8
9
##复制代码
yum install lrzsz

# rz 上传
# sz 下载

还可以设置一下上传和下载的目录

option----session options ---- files transfer 下可以设置上传和下载的目录

windows 查看CPU核心数,线程数

1
2
3
4
5
6
7
8
bash复制代码# 1.cmd窗口输入命令“wmic”

# 2.然后在出现的窗口输入
cpu get Name #查看物理CPU名

cpu get NumberOfCores #查看CPU核心数

cpu get NumberOfLogicalProcessors #查看CPU线程数

linux中查找文件(find)

1
2
3
4
5
6
bash复制代码linux 查找某文件所在路径
find 路径 -name 文件名
例如:find / -name logo_web.png  查找/路径下logo_web.png文件路径

[root@dsjpt07 data]# find / -name demo-springboot-starter-0.0.1-SNAPSHOT.jar
/usr/local/tools/demo-springboot-starter-0.0.1-SNAPSHOT.jar

查看用户组

1
2
3
4
bash复制代码linux如何查看所有的用户和组信息的方法:
1、cat /etc/passwd
2、cat /etc/group
修改用户组时,需要把cat 换成 vim ;比如vim /etc/group

修改root账号密码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
bash复制代码linux下修改root密码方法

以root身份登陆,执行:

passwd 用户名
然后根据提示,输入新密码,再次输入新密码,系统会提示成功修改密码。

具体示例如下:

[root@ncayu618 ~]# passwd root
Changing password for user root.
New UNIX password:
BAD PASSWORD: it is based on a dictionary word
Retype new UNIX password:
passwd: all authentication tokens updated successfully.

查看linux主机的IP地址

1
2
3
4
bash复制代码ifconfig  #查看所有的IP地址

## 过滤出IP地址,可用于写shell脚本。
ifconfig -a | grep inet | grep -v 127.0.0.1 | grep -v inet6 | awk '{print $2}'|tr -d "addr:"|awk 'BEGIN{RS="\n";ORS=" ";}{print $0}'

通过进程号查看端口

1
2
bash复制代码netstat -nap | grep 21587
###通过进程id查看端口号

在linux上校验MD5值

在windows上校验MD5的方式比较繁琐,在linux上会更加简单,首先打开虚拟机上的Center OS7并用 Xshell进行远程连接,新建一个文件11.txt,用md5sum给出11.txt的MD5值,结果如下图所示。touch 11.txt的意思是创建一个名称为11.txt的文件,md5sum 后接路径可以得到文件的MD5值

1
2
3
4
5
6
7
8
bash复制代码[root@Pengfei test02]# md5sum cions.txt #获取cions.txt 的md5值
472a616feeac128d47c058af07001e2d cions.txt
[root@Pengfei test02]# md5sum data.txt #获取data.txt 的md5值
37e07e96f2ad41760cd30ba15146be0b data.txt
##应用场景
#验证Percona mysql的MD5值
[root@ncayu8847 software]# md5sum Percona-Server-5.7.33-36-Linux.x86_64.glibc2.12.tar.gz
6992b38f1085b6b0b30c8df833f043dc Percona-Server-5.7.33-36-Linux.x86_64.glibc2.12.tar.gz

解压命令

1
2
3
4
5
bash复制代码gzip  -d  abcsql.gz

unzip abcsql.zip

tar xzvf abcsql.tar.gz

top命令

1
2
3
bash复制代码和top类似的工具:
# glances
# htop

这两个工具需要下载安装,体验还是蛮不错的。

glances 是一款用于 Linux、BSD 的开源命令行系统监视工具,它使用 Python 语言开发,能够监视 CPU、负载、内存、磁盘 I/O、网络流量、文件系统、系统温度等信息。

glances 可以为 Unix 和 Linux 性能专家提供监视和分析性能数据的功能,其中包括:

  • CPU 使用率
  • 内存使用情况
  • 内核统计信息和运行队列信息
  • 磁盘 I/O 速度、传输和读/写比率
  • 文件系统中的可用空间
  • 磁盘适配器
  • 网络 I/O 速度、传输和读/写比率
  • 页面空间和页面速度
  • 消耗资源最多的进程
  • 计算机信息和系统资源

glances 工具可以在用户的终端上实时显示重要的系统信息,并动态地对其进行更新。这个高效的工具可以工作于任何终端屏幕。另外它并不会消耗大量的 CPU 资源,通常低于百分之二。glances 在屏幕上对数据进行显示,并且每隔两秒钟对其进行更新。您也可以自己将这个时间间隔更改为更长或更短的数值。glances 工具还可以将相同的数据捕获到一个文件,便于以后对报告进行分析和绘制图形。输出文件可以是电子表格的格式 (.csv) 或者 html 格式。

通过pid查看端口

1
2
3
4
5
bash复制代码[root@ncayu618 ncayu618]# netstat -antup|grep 2150
tcp 0 52 172.18.55.8:22 116.237.140.20:36130 ESTABLISHED 2150/sshd: root@pts
[root@ncayu618 ncayu618]#
#通过应用查询端口和pid
$ ss -naltp|grep prometheus

查看防火墙是否开启

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
bash复制代码1、查看firewall服务状态

systemctl status firewalld

出现Active: active (running)切高亮显示则表示是启动状态。

2、查看firewall的状态

firewall-cmd --state
3、开启、重启、关闭、firewalld.service服务

开启
service firewalld start
重启
service firewalld restart
关闭
service firewalld stop
4、查看防火墙规则

firewall-cmd --list-all
5、查询、开放、关闭端口

查询端口是否开放
firewall-cmd --query-port=8080/tcp
开放80端口
firewall-cmd --permanent --add-port=80/tcp
移除端口
firewall-cmd --permanent --remove-port=8080/tcp
#重启防火墙(修改配置后要重启防火墙)
firewall-cmd --reload

参数解释
1、firwall-cmd:是Linux提供的操作firewall的一个工具;
2、–permanent:表示设置为持久;
3、–add-port:标识添加的端口;

一、防火墙的开启、关闭、禁用命令

(1)设置开机启用防火墙:systemctl enable firewalld.service

(2)设置开机禁用防火墙:systemctl disable firewalld.service

(3)启动防火墙:systemctl start firewalld

(4)关闭防火墙:systemctl stop firewalld

(5)检查防火墙状态:systemctl status firewalld

#centos6.x查看防火墙
[root@centos6 ~]# service iptables status
iptables:未运行防火墙。
开启防火墙:
[root@centos6 ~]# service iptables start
关闭防火墙:
[root@centos6 ~]# service iptables stop
重启防火墙
[root@centos6 ~]# service iptables restart

#centos6.x 添加防火墙端口
1.开放80,22,8080 端口

/sbin/iptables -I INPUT -p tcp --dport 80 -j ACCEPT
/sbin/iptables -I INPUT -p tcp --dport 61616 -j ACCEPT
/sbin/iptables -I INPUT -p tcp --dport 9100 -j ACCEPT

2.保存
/etc/rc.d/init.d/iptables save

3.查看打开的端口
/etc/init.d/iptables status

4、关闭端口(以7777端口为例)

vi /etc/sysconfig/iptables 打开配置文件加入如下语句:

-A INPUT -p tcp -m state --state NEW -m tcp --dport 7777 -j DROP

查询所有被占用的端口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
bash复制代码netstat -tulnp

-t(tcp)只显示tcp相关的

-u(udp)只显示udp相关的

-l(listening)只显示监听服务的端口

-n(numeric)不解析名称,能用数字表示的就不用别名(例如:localhost会转成127.0.0.1)

-p(programs)显示端口的PID和程序名称

查询单个端口是否被占用。
可以通过netstat -tulnp | grep 端口号查看当前端口号是否被占用

例如:

netstat -tulnp|grep 3306

检查端口开放情况

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
bash复制代码netstat 工具检测开放端口

[root@DB-Server Server]# netstat -anlp | grep 3306


###nmap是一款网络扫描和主机检测的工具
关于nmap的使用,都可以长篇大写特写,这里不做展开。如下所示,nmap 127.0.0.1 查看本机开放的端口,会扫描所有端口。 当然也可以扫描其它服务器端口。

yum install nmap;

[root@ncayu618 ~]# nmap 127.0.0.1

Starting Nmap 6.40 ( http://nmap.org ) at 2021-05-19 11:14 CST
Nmap scan report for localhost (127.0.0.1)
Host is up (0.0000070s latency).
Not shown: 995 closed ports
PORT STATE SERVICE
22/tcp open ssh
25/tcp open smtp
3000/tcp open ppp
9090/tcp open zeus-admin
9100/tcp open jetdirect

Nmap done: 1 IP address (1 host up) scanned in 1.58 seconds

linux创建新的用户

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
bash复制代码#1、添加用户,首先用adduser命令添加一个普通用户,命令如下:

adduser ncayu
#添加一个名为ncayu的用户
passwd ncayu
#修改密码
Changing password for user ncayu.
New UNIX password: #在这里输入新密码
Retype new UNIX password: #再次输入新密码
passwd: all authentication tokens updated successfully.

#2、赋予root权限
#修改 /etc/sudoers 文件,找到下面一行,在root下面添加一行,如下所示:
## Allow root to run any commands anywhere
root ALL=(ALL) ALL
ncayu ALL=(ALL) ALL
#修改完毕,现在可以用ncayu帐号登录,然后用命令 sudo – ,即可获得root权限进行操作。

更改文件的用户组

1
2
3
4
5
6
7
8
9
10
11
12
13
bash复制代码二、使用 chown命令 更改文件拥有者
在 shell 中,可以使用 chown命令 来改变文件所有者。 chown命令 是change owner(改变拥有者)的缩写。需要要注意的是, 用户必须是已经存在系统中的,也就是只能改变为在 /etc/passwd这个文件中有记录的用户名称才可以 。
chown命令 的用途很多,还可以顺便直接修改用户组的名称。此外,如果要连目录下的所有子目录或文件同时更改文件拥有者的话,直接加上 -R 的参数即可。
基本语法:
chown [ -R] 账号名称 文件或 目录
chown [ -R] 账号名称: 用户组名称 文件或 目录
参数:
-R : 进行递归( recursive )的持续更改,即连同子目录下的所有文件、目录
都更新成为这个用户组。常常用在更改某一目录的情况。
# 例如:
chown ncayu:ncayu /prometheus

chown -R ncayu:ncayu /prometheus

本文转载自: 掘金

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

随手记 AOP 如何避开 BeanNotOfRequir

发表于 2021-10-06

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

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

Github : 👉 github.com/black-ant

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

一 . 前言

今天对 Spring 进行深度使用的时候 , 想仿照 AOP 去实现对应的代理 , 但是却触发了 BeanNotOfRequiredTypeException 异常 , 原因是因为 Spring 会进行类的校验

于是突然产生了好奇 , 决定研究一下 , AOP 是通过什么方式避开这个校验过程

二 . 前置知识

  • AOP 通过 AopProxy 进行代理
  • SpringBoot 1.5 默认使用 JDK Proxy , SpringBoot 2.0 基于自动装配(AopAutoConfiguration)的配置 , 默认使用 CGlib

JDK Proxy 和 CGLib 的区别

老生常谈的问题 , 问了完整性(凑字数) , 还是简单列一下 :

  • JDK Proxy : 利用拦截器(拦截器必须实现InvocationHanlder)加上反射机制生成一个实现代理接口的匿名类,在调用具体方法前调用InvokeHandler来处理。
  • CGLIB动态代理:利用ASM开源包,对代理对象类的class文件加载进来,通过修改其字节码生成子类来处理。

PS : 通过 proxy-target-class 可以进行配置

三 . 原理探索

常规方式是 CGLIB , 所以主流程还是通过这种方式分析 , 有了前置知识的补充 , 实现猜测是由于 CGLIB 的特性 , 实际上被校验出来.

  • 源头 :autowired 导入得时候会校验注入的类是否正确

3.1 拦截的入口

  • Step 1 : AbstractAutowireCapableBeanFactory # populateBean
  • Step 2 : AutowiredAnnotationBeanPostProcessor # postProcessProperties
  • Step 3 : InjectionMetadata # inject
  • Step 4 : AutowiredAnnotationBeanPostProcessor # inject
  • Step 5 : DefaultListableBeanFactory # doResolveDependency
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
java复制代码public Object doResolveDependency(DependencyDescriptor descriptor, @Nullable String beanName,
@Nullable Set<String> autowiredBeanNames, @Nullable TypeConverter typeConverter) throws BeansException {

//............ 以下是主要逻辑

if (autowiredBeanNames != null) {
autowiredBeanNames.add(autowiredBeanName);
}

// 获取 Autowired 的实际对象或者代理对象
if (instanceCandidate instanceof Class) {
instanceCandidate = descriptor.resolveCandidate(autowiredBeanName, type, this);
}

// 判断该对象是否为null
Object result = instanceCandidate;
if (result instanceof NullBean) {
if (isRequired(descriptor)) {
raiseNoMatchingBeanFound(type, descriptor.getResolvableType(), descriptor);
}
result = null;
}

// 核心拦截逻辑
if (!ClassUtils.isAssignableValue(type, result)) {
throw new BeanNotOfRequiredTypeException(autowiredBeanName, type, instanceCandidate.getClass());
}
return result;
}
}

3.2 拦截的判断

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
java复制代码public static boolean isAssignable(Class<?> lhsType, Class<?> rhsType) {

// 类型判断
if (lhsType.isAssignableFrom(rhsType)) {
return true;
} else {
Class resolvedWrapper;

// 基本类型特殊处理
if (lhsType.isPrimitive()) {
resolvedWrapper = (Class)primitiveWrapperTypeMap.get(rhsType);
return lhsType == resolvedWrapper;
} else {
resolvedWrapper = (Class)primitiveTypeToWrapperMap.get(rhsType);
return resolvedWrapper != null && lhsType.isAssignableFrom(resolvedWrapper);
}
}
}

3.3 AOP 的使用

看到了拦截的入口 , 那就得看看 AOP 中是如何通过 PostProcessor 进行处理的了 , 首先看一下 PostProcessor 链表

image.png

Step 1 : 当对象 A 中字段是 @Autowired 注入的 AOP 代理类时

此时我们可以发现 , 在 DefaultListableBeanFactory # doResolveDependency 环节会去获取该代理类的对象 ,
拿到的对象如下图所示 :

1
2
3
4
java复制代码// doResolveDependency 中获取对象环节
if (instanceCandidate instanceof Class) {
// 此时拿到的对象就是一个 cglib 代理类
instanceCandidate = descriptor.resolveCandidate(autowiredBeanName, type, this);

image.png

Step 2 : 判断类的关系入口

1
2
3
4
5
6
7
java复制代码// doResolveDependency 中判断类的关系 -> true
if (!ClassUtils.isAssignableValue(type, result)) {
throw new BeanNotOfRequiredTypeException(autowiredBeanName, type, instanceCandidate.getClass());
}

// result.getClass()
- name=com.gang.aop.demo.service.StartService$$EnhancerBySpringCGLIB$$d673b902

Step 3 : 判断类的关系逻辑

1
2
3
4
5
6
7
8
9
10
11
12
13
java复制代码C- ClassUtils
public static boolean isAssignable(Class<?> lhsType, Class<?> rhsType) {

// 核心语句 , native 方法 -> public native boolean isAssignableFrom(Class<?> cls);
if (lhsType.isAssignableFrom(rhsType)) {
return true;
}
//.........
}

// 这里简单做了一个继承类 , ChildService extends ChildService
: ------> ChildService By ParentService :false <-------
: ------> ParentService By ChildService:true <-------

由此可见 cglib 创建的对象满足该条件 : 相同 , 或者是超类或者超接口

这里回过头看之前的问题 , 就很简单了 :

1
2
3
4
5
6
7
8
9
10
11
12
13
java复制代码// 问题原因 : 
我通过实现 postProcessor 去做了一个代理
public class AopProxyImpl extends Sourceable {
private Sourceable source;
}

// 修改后 :
public class AopProxyImpl extends Source {
private Sourceable source;
}

// 通过继承即可解决 BeanNotOfRequiredTypeException ,弄懂了就没什么难度了
//

四 . 深入原理

那么继续回顾下 CGLIB 的创建过程 , 实际上在编译的结果上是可以很直观的看到代理的对象的 :

image.png

关于 CGLIB 的基础 , 可以看看菜鸟的文档 CGLIB(Code Generation Library) 介绍与原理 , 写的很详细

4.1 CGLIB 的创建过程

FastClass 的作用

FastClass 就是给每个方法编号,通过编号找到方法,这样可以避免频繁使用反射导致效率比较低

CGLIB 会生成2个 fastClass :

  • xxxx$$FastClassByCGLIB$$xxxx :为生成的代理类中的每个方法建立了索引
  • xxxx$$EnhancerByCGLIB$$xxxx$$FastClassByCGLIB$$xxxx : 为我们被代理类的所有方法包含其父类的方法建立了索引

原因 : cglib代理基于继承实现,父类中非public、final的方法无法被继承,所以需要一个父类的fastclass来调用代理不到的方法

FastClass 中有2个主要的方法 :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
java复制代码// 代理方法
public Object invoke(final int n, final Object o, final Object[] array) throws InvocationTargetException {
final CglibService cglibService = (CglibService)o;
switch (n) {
case 0: {
// 代理对应的业务方法
cglibService.run();
return null;
}
case 1: {
// 代理 equeals 方法
return new Boolean(cglibService.equals(array[0]));
}
case 2: {
// 代理 toString 方法
return cglibService.toString();
}
case 3: {
// 代理 hashCode 方法
return new Integer(cglibService.hashCode());
}
}
throw new IllegalArgumentException("Cannot find matching method/constructor");
}


// 实例化对象
public Object newInstance(final int n, final Object[] array) throws InvocationTargetException {
switch (n) {
case 0: {
// 此处总结通过 new 进行了实例化
return new CglibService();
}
}
throw new IllegalArgumentException("Cannot find matching method/constructor");
}

enchance 对象

之前了解到 , cglib 通过重写字节码生成主类达到代理的目的 , 这里来看一下 , 原方法被改写成什么样了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
java复制代码final void CGLIB$run$0() {
super.run();
}

public final void run() {
MethodInterceptor cglib$CALLBACK_2;
MethodInterceptor cglib$CALLBACK_0;
if ((cglib$CALLBACK_0 = (cglib$CALLBACK_2 = this.CGLIB$CALLBACK_0)) == null) {
CGLIB$BIND_CALLBACKS(this);
cglib$CALLBACK_2 = (cglib$CALLBACK_0 = this.CGLIB$CALLBACK_0);
}
if (cglib$CALLBACK_0 != null) {
// 调用拦截器对象
cglib$CALLBACK_2.intercept((Object)this, CglibService$$EnhancerByCGLIB$$7aba7860.CGLIB$run$0$Method, CglibService$$EnhancerByCGLIB$$7aba7860.CGLIB$emptyArgs, CglibService$$EnhancerByCGLIB$$7aba7860.CGLIB$run$0$Proxy);
return;
}
// 没有拦截器对象 , 则直接调用
super.run();
}


// 实际被调用的拦截器
public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {

// 这里会调用关联类
// 最终通过 super.run 调用
Object result = proxy.invokeSuper(obj, args);

return result;
}

此处也可以看到映射关系
image.png

总结

cglib.jpg

本文转载自: 掘金

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

1…507508509…956

开发者博客

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