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

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


  • 首页

  • 归档

  • 搜索

java流太太太好用了

发表于 2021-10-13

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

  • 情景:一个集合对象list,现在想获取这个集合中每个对象的id,并将这些id值存放在另一个集合中,方便我去查询数据。如果是你来实现这个需求,你会用什么方法去实现呢。

我猜会有许多人会选择循环变量这个集合对象,取出id存放在集合里面,代码是这样的:

1
2
3
4
5
ini复制代码List<Clazz> list = clazzes;
List<Long> ids = new ArrayList();
for (Clazz clazz : list) {
ids.add(clazz.getId());
}

但是!实际上,这个需求可以只用一行代码就可以解决,那是用的什么呢?“流”请看代码:

1
2
ini复制代码List<Clazz> list = clazzes;
List<Long> collect = list.stream().map(Clazz::getId).collect(Collectors.toList());

使用流一行代码就可以解决关键看着清晰明了。
上面list.stream().map(Clazz::getId).collect(Collectors.toList())这一行代码用了JAVA8 的两个新特性

  • 双冒号 双冒号就是把方法当作参数传递给需要的方法,或者是传递到stream()中去。在这里就是将其传到stream中去其语法格式 类名::方法名
  • stream 流 通过Collectors 类将流转换成集合元素 流的操作还有许多,可以参考搜索网络

再分享一下 最近根据echart图来查询数据,我在写查询语句筛选条件使用了大量的stream流,发现使用stream流是真的舒服。

我先描述我最近的一个接口:这个接口需要展示四个饼图。而四个饼图是:1.男女教师占比;2.各年龄段占比 3.学历占比,4.职称统计
我想在一个接口中完成这个四个的查询 我的思路有几个:

1.是写多个查询语句 需要一个查询一个(但是各种筛选条件下来 很麻烦)

2.利用视图 可以用来多次调用(但是在查询中会存在in操作 觉得麻烦)

3.利用stream流 根据筛选条件查出符合的教师信息 对每一个操作进行筛选

1
2
3
scss复制代码 通过各种筛选条件查出的结果: teacherList (集合类型)
Long count1 = teacherList.stream().filter(e -> e.getGender().equals(0)).count(); //男生数量
Long count2 = teacherList.stream().filter(e -> e.getGender().equals(1)).count(); //女生数量

通过这样可以直接算出数量 而不用去便利算数据

而更多详细的stream流的信息可以去网上搜索学习

我对stream流的学习还在表面 还有许多灵活的用法我还需要继续学习 欢迎大佬指导!

本文转载自: 掘金

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

Java并发编程基础——线程

发表于 2021-10-13

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

本文已参与 「掘力星计划」 ,赢取创作大礼包,挑战创作激励金。

简介:

线程是操作系统调度的最小单元,在多核环境中,多个线程能同时执行,如果运用得当,能显著的提升程序的性能。

一、线程初步认识

1、什么是线程

操作系统运行一个程序会为其启动一个进程。例如,启动一个Java程序会创建一个Java进程。现代操作系统调度的最小单元是线程,线程也称为轻量级进程(Light Weight Process),一个进程中可以创建一个到多个线程,线程拥有自己的计数器、堆栈和局部变量等属性,并且能访问共享的内存变量。处理器会通过快速切换这些线程,来执行程序。

2、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
java复制代码package com.lizba.p2;

import java.lang.management.ManagementFactory;
import java.lang.management.ThreadInfo;
import java.lang.management.ThreadMXBean;
import java.util.Arrays;

/**
* <p>
*
* </p>
*
* @Author: Liziba
* @Date: 2021/6/13 23:03
*/
public class MultiThread {

public static void main(String[] args) {
// 获取Java线程管理MXBean
ThreadMXBean threadMXBean = ManagementFactory.getThreadMXBean();
// 获取线程和线程堆栈信息;
// boolean lockedMonitors = false 不需要获取同步的monitor信息;
// boolean lockedSynchronizers = false 不需要获取同步的synchronizer信息
ThreadInfo[] threadInfos = threadMXBean.dumpAllThreads(false, false);
// 打印线程ID和线程name
Arrays.stream(threadInfos).forEach(threadInfo -> {
System.out.println("[" + threadInfo.getThreadId() + "]" + threadInfo.getThreadName());
});
}

}

输出结果(不一定一致):

[6]Monitor Ctrl-Break // idea中特有的线程(不用管)

[5]Attach Listener // JVM进程间的通信线程

[4]Signal Dispatcher // 分发处理发送给JVM信号的线程

[3]Finalizer // 调用对象的finalizer线程

[2]Reference Handler // 清楚Reference的线程

[1]main // main线程,用户程序入口


总结:

从输出结果不难看出,Java程序本身就是多线程的。它不仅仅只有一个main线程在运行,而是main线程和其他多个线程在同时运行。

3、为什么要使用多线程

使用多线程的好处如下:

  1. 更多处理器核心

计算机处理器核心数增多,由以前的高主频向多核心技术发展,现在的计算机更擅长于并行计算,因此如何充分利用多核心处理器是现在的主要问题。线程是操作系统调度的最小单元,一个程序作为一个进程来运行,它会创建多个线程,而一个线程在同一时刻只能运行在一个处理器上。因此一个进程如果能使用多线程计算,将其计算逻辑分配到多个处理器核心上,那么相比单线程运行将会有更显著的性能提升。

  1. 更快响应时间

在复杂业务场景中,我们可以将非强一致性关联的业务派发给其他线程处理(或者使用消息队列)。这样可以减少应用响应用户请求的时间

  1. 更好的编程模型

合理使用Java的提供的多线程编程模型,能使得程序员更好的解决问题,而不需要过于复杂的考虑如何将其多线程化。

4、线程的优先级

现代操作系统基本采用的是时间片分配的方式来调度线程,也就是操作系统将CPU的运行分为一个个时间片,线程会分配的若干时间片,当线程时间片用完了,就会发生线程调度等待下次时间片的分配。线程在一次CPU调度中能执行多久,取决于所分时间片的多少,而线程优先级就是决定线程需要多或者少分配一些处理器资源的线程属性。

在Java线程中,线程的优先级的可设置范围是1-10,默认优先级是5,理论上优先级高的线程分配时间片数量要优先于低的线程(部分操作系统这个设置是不生效的);


示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
java复制代码package com.lizba.p2;

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;

/**
* <p>
* 线程优先级设置
* </p>
*
* @Author: Liziba
* @Date: 2021/6/14 12:03
*/
public class Priority {

/** 线程执行流程控制开关 */
private static volatile boolean notStart = true;
/** 线程执行流程控制开关 */
private static volatile boolean notEnd = true;

public static void main(String[] args) throws InterruptedException {
List<Job> jobs = new ArrayList<>();
// 设置5个优先级为1的线程,设置5个优先级为10的线程
for (int i = 0; i < 10; i++) {
int priority = i < 5 ? Thread.MIN_PRIORITY : Thread.MAX_PRIORITY;
Job job = new Job(priority);
jobs.add(job);
Thread thread = new Thread(job, "Thread:" + i);
thread.setPriority(priority);
thread.start();
}
notStart = false;
TimeUnit.SECONDS.sleep(10);
notEnd = false;
jobs.forEach(
job -> System.out.println("Job priority : " + job.priority + ", Count : " + job.jobCount)
);
}


/**
* 通过Job来记录线程的执行次数和优先级
*/
static class Job implements Runnable {

private int priority;
private long jobCount;

public Job(int priority) {
this.priority = priority;
}

@Override
public void run() {
while (notStart) {
// 让出CPU时间片,等待下次调度
Thread.yield();
}
while (notEnd) {
// 让出CPU时间片,等待下次调度
Thread.yield();
jobCount++;
}
}
}

}

执行结果:

从输出结果上来看,优先级为1的线程和优先级为10的线程执行的次数非常相近,因此这表明程序正确性是不能依赖线程的优先级高低的。

\

5、线程的状态

线程的生命周期如下:

状态名称 说明
NEW 初始状态,线程被构建,并未调用start()方法
RUNNABLE 运行状态,Java线程将操作系统中的就绪和运行两种状态统称为“运行中”
BLOCKED 阻塞状态,线程阻塞于锁
WAITING 等待状态,线程进入等待状态,进入该状态表示当前线程需要等待其他线程作出一些特定动作(通知或中断)
TIME_WAITING 超时等待,先比WAITING可以在指定的时间内自行返回
TERMINATED 终止状态,表示当前线程已经执行完毕

通过代码来查看Java线程的状态

代码示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
java复制代码package com.lizba.p2;

import java.util.concurrent.TimeUnit;

/**
* <p>
* 睡眠指定时间工工具类
* </p>
*
* @Author: Liziba
* @Date: 2021/6/14 13:27
*/
public class SleepUtil {

public static final void sleepSecond(long seconds) {
try {
TimeUnit.SECONDS.sleep(seconds);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
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
typescript复制代码package com.lizba.p2;

/**
* <p>
* 线程状态示例代码
* </p>
*
* @Author: Liziba
* @Date: 2021/6/14 13:25
*/
public class ThreadStateDemo {

public static void main(String[] args) {
// TimeWaiting
new Thread(new TimeWaiting(), "TimeWaitingThread").start();
// Waiting
new Thread(new Waiting(), "WaitingThread").start();
// Blocked1和Blocked2一个获取锁成功,一个获取失败
new Thread(new Blocked(), "Blocked1Thread").start();
new Thread(new Blocked(), "Blocked2Thread").start();
}

// 线程不断的进行睡眠
static class TimeWaiting implements Runnable {

@Override
public void run() {
while (true) {
SleepUtil.sleepSecond(100);
}
}
}

// 线程等待在Waiting.class实例上
static class Waiting implements Runnable {

@Override
public void run() {
while (true) {
synchronized (Waiting.class) {
try {
Waiting.class.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}

// 该线程Blocked.class实例上加锁,不会释放该锁
static class Blocked implements Runnable {

@Override
public void run() {
synchronized (Blocked.class) {
while (true) {
SleepUtil.sleepSecond(100);
}
}
}
}

}

使用JPS查看Java进程:

查看示例代码ThreadStateDemo进程ID是2576,键入jstack 2576查看输出:

整理输出结果:

线程名称 线程状态
Blocked2Thread BLOCKED (on object monitor),阻塞在获取Blocked.class的锁上
Blocked1Thread TIMED_WAITING (sleeping)
WaitingThread WAITING (on object monitor)
TimeWaitingThread TIMED_WAITING (sleeping)

总结:

线程在自身生命周期中不是规定处于某一个状态,而是随着代码的执行在不同的状态之间进行切换。

Java线程的状态变化图如下:

Java线程状态变迁图

总结:

  • 线程创建后,调用start()方法开始运行
  • 线程执行wait()方法后,线程进入等待状态,进入等待的线程需要依靠其他线程才能够返回到运行状态
  • 超时等待相当于在等待朱姑娘太的基础上增加了超时限制,达到设置的超时时间后返回到运行状态
  • 线程执行同步方法或代码块时,未获取到锁的线程,将会进入到阻塞状态。
  • 线程执行完Runnable的run()方法之后进入到终止状态
  • 阻塞在Java的concurrent包中Lock接口的线程是等待状态,因为Lock接口阻塞的实现使用的是Daemon线程

6、Daemon线程

简介:

Daemon线程是一种支持型线程,它的主要作用是程序中后台调度和支持性工作。当一个Java虚拟机中不存在非Daemon线程的时候,Java虚拟机将会退出。Daemon线程需要在启动之前设置,不能在启动之后设置。

设置方式:

1
arduino复制代码Thread.setDaemon(true)

需要特别注意的点:

Daemon线程被用作支持性工作的完成,但是在Java虚拟机退出时Daemon线程的finally代码块不一定执行。

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
java复制代码package com.lizba.p2;

/**
* <p>
* DaemonRunner线程
* </p>
*
* @Author: Liziba
* @Date: 2021/6/14 19:50
*/
public class DaemonRunner implements Runnable{
@Override
public void run() {
try {
SleepUtil.sleepSecond(100);
} finally {
System.out.println("DaemonRunner finally run ...");
}
}
}

测试:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
java复制代码package com.lizba.p2;

/**
* <p>
*
* </p>
*
* @Author: Liziba
* @Date: 2021/6/14 19:59
*/
public class DaemonTest {

public static void main(String[] args) {
Thread t = new Thread(new DaemonRunner(), "DaemonRunner");
t.setDaemon(true);
t.start();
}

}

输出结果:

总结:

不难发现,DaemonRunner的run方法的finally代码块并没有执行,这是因为,当Java虚拟机种已经没有非Daemon线程时,虚拟机会立即退出,虚拟机中的所以daemon线程需要立即终止,所以线程DaemonRunner会被立即终止,finally并未执行。

二、线程启动和终止

1、构造线程

运行线程之前需要构造一个线程对象,线程对象在构造的时候需要设置一些线程的属性,这些属性包括线程组、线程的优先级、是否时daemon线程、线程名称等信息。

代码示例:

来自java.lang.Thread

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
ini复制代码  /**
* Initializes a Thread.
*
* @param g the Thread group
* @param target the object whose run() method gets called
* @param name the name of the new Thread
* @param stackSize the desired stack size for the new thread, or
* zero to indicate that this parameter is to be ignored.
* @param acc the AccessControlContext to inherit, or
* AccessController.getContext() if null
* @param inheritThreadLocals if {@code true}, inherit initial values for
* inheritable thread-locals from the constructing thread
*/
private void init(ThreadGroup g, Runnable target, String name, long stackSize, AccessControlContext acc, boolean inheritThreadLocals) {
if (name == null) {
throw new NullPointerException("name cannot be null");
}
// 设置线程名称
this.name = name;
// 当前线程设置为该线程的父线程
Thread parent = currentThread();
SecurityManager security = System.getSecurityManager();
if (g == null) {
if (security != null) {
g = security.getThreadGroup();
}
if (g == null) {
g = parent.getThreadGroup();
}
}
g.checkAccess();
if (security != null) {
if (isCCLOverridden(getClass())) {
security.checkPermission(SUBCLASS_IMPLEMENTATION_PERMISSION);
}
}
g.addUnstarted();
// 设置线程组
this.group = g;
// 将daemon属性设置为父线程的对应的属性
this.daemon = parent.isDaemon();
// 将prority属性设置为父线程的对应的属性
this.priority = parent.getPriority();
if (security == null || isCCLOverridden(parent.getClass()))
this.contextClassLoader = parent.getContextClassLoader();
else
this.contextClassLoader = parent.contextClassLoader;
this.inheritedAccessControlContext = acc != null ? acc : AccessController.getContext();
this.target = target;
setPriority(priority);
// 复制父线程的InheritableThreadLocals属性
if (inheritThreadLocals && parent.inheritableThreadLocals != null)
this.inheritableThreadLocals =
ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
/* Stash the specified stack size in case the VM cares */
this.stackSize = stackSize;

// 设置一个线程id
tid = nextThreadID();
}

总结:

在上述代码中,一个新构建的线程对象时由其parent线程来分配空间的,而child继承了parent是否为Daemon、优先级和加载资源的contextClassLoader以及可继承的ThreadLocal,同时会分配一个唯一的ID来标志线程。此时一个完整的能够运行的线程对象就初始化好了,在堆内存中等待运行。

2、什么是线程中断

中断可以理解为线程的一个标识位属性,它表示一个运行中的线程是否被其他线程进行了中断操作。线程通过检查自身是否被中断来进行响应,线程通过方法isInterrupted()来进行判断是否被中断,也可以通过调用静态方法Thread.interrupted()对当前线程的中断标志位进行复位。

如下情况不能准确判断线程是否被中断过:

  1. 线程已经终止运行,即使被中断过,isInterrupted()方法也会返回false
  2. 方法抛出InterruptedException异常,即使被中断过,调用isInterrupted()方法将会返回false,这是因为抛出InterruptedException之前会清除中断标志。

示例代码:

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
typescript复制代码package com.lizba.p2;

/**
* <p>
* 线程中断示例代码
* </p>
*
* @Author: Liziba
* @Date: 2021/6/14 20:36
*/
public class Interrupted {

public static void main(String[] args) {
// sleepThread不停的尝试睡眠
Thread sleepThread = new Thread(new SleepRunner(), "sleepThread");
sleepThread.setDaemon(true);

// busyThread
Thread busyThread = new Thread(new BusyRunner(), "busyThread");
busyThread.setDaemon(true);

// 启动两个线程
sleepThread.start();
busyThread.start();

// 休眠5秒,让sleepThread和busyThread运行充分
SleepUtil.sleepSecond(5);

// 中断两个线程
sleepThread.interrupt();
busyThread.interrupt();

System.out.println("SleepThread interrupted is " + sleepThread.isInterrupted());
System.out.println("BusyThread interrupted is " + busyThread.isInterrupted());

// 睡眠主线程,防止daemon线程退出
SleepUtil.sleepSecond(2);
}

static class SleepRunner implements Runnable {

@Override
public void run() {
while (true) {
SleepUtil.sleepSecond(10);
}
}
}

static class BusyRunner implements Runnable {

@Override
public void run() {
while (true) {}
}
}

}

查看运行结果:

总结:

抛出InterruptedException的是sleepThread线程,虽然两者都被中断过,但是sleepThread线程的中断标志返回的是false,这是因为TimeUnit.SECONDS.sleep(seconds)会抛出InterruptedException异常,抛出异常之前,sleepThread线程的中断标志被清除了。但是,busyThread一直在运行没有抛出异常,中断位没有被清除。

3、suspend()、resume()和stop()

举例:

线程这三个方法,相当于QQ音乐播放音乐时的暂停、恢复和停止操作。(注意这些方法已经过期了,不建议使用。)

示例代码:

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

import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.concurrent.TimeUnit;

/**
* <p>
* 线程过期方法示例
* </p>
*
* @Author: Liziba
* @Date: 2021/6/14 20:57
*/
public class Deprecated {


static DateFormat format = new SimpleDateFormat("HH:mm:ss");

public static void main(String[] args) {
Thread printThread = new Thread(new PrintThread(), "PrintThread");
printThread.start();
SleepUtil.sleepSecond(3);

// 暂停printThread输出
printThread.suspend();
System.out.println("main suspend PrintThread at " + format.format(new Date()));
SleepUtil.sleepSecond(3);

// 恢复printThread输出
printThread.resume();
System.out.println("main resume PrintThread at " + format.format(new Date()));
SleepUtil.sleepSecond(3);

// 终止printThread输出
printThread.stop();
System.out.println("main stop PrintThread at " + format.format(new Date()));
SleepUtil.sleepSecond(3);
}


static class PrintThread implements Runnable {

@Override
public void run() {
while (true) {
System.out.println(Thread.currentThread().getName() + "Run at "
+ format.format(new Date()));
SleepUtil.sleepSecond(1);
}
}
}

}

输出结果:

总结:

上述代码执行输出的结果,与API说明和我们的预期完成一致,但是看似正确的代码却隐藏这很多问题。

存在问题:

  • suspend()方法调用后不会释放已占有的资源(比如锁),可能会导致死锁
  • stop()方法在终结一个线程时不能保证资源的正常释放,可能会导致程序处于不确定的工作状态

4、正确的终止线程

  1. 调用线程的interrupt()方法
  2. 使用一个Boolean类型的变量来控制是否停止任务并终止线程

示例代码:

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
java复制代码package com.lizba.p2;

/**
* <p>
* 标志位终止线程示例代码
* </p>
*
* @Author: Liziba
* @Date: 2021/6/14 21:17
*/
public class ShutDown {

public static void main(String[] args) {
Runner one = new Runner();
Thread t = new Thread(one, "CountThread");
t.start();
SleepUtil.sleepSecond(1);
t.interrupt();

Runner two = new Runner();
t = new Thread(two, "CountThread");
t.start();
SleepUtil.sleepSecond(1);
two.cancel();
}



private static class Runner implements Runnable {

private long i;
private volatile boolean on = true;

@Override
public void run() {
while (on && !Thread.currentThread().isInterrupted()) {
i++;
}
System.out.println("Count i = " +i);
}

/**
* 关闭
*/
public void cancel() {
on = false;
}
}

}

输出结果:

总结:

main线程通过中断操作和cancel()方法均可使CountThread得以终止。这两种方法终止线程的好处是能让线程在终止时有机会去清理资源。做法更加安全和优雅。

文章总结至《Java并发编程的艺术》,下文总结-线程间通信。

\

本文转载自: 掘金

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

这款开源测试神器,圆了我玩游戏不用动手的梦想

发表于 2021-10-13

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

本文已参与「掘力星计划」,赢取创作大礼包,挑战创作激励金。

一天我在公司用手机看游戏直播,同事问我在玩什么游戏?我和他说在看直播,他恍然大悟:原来如此,我还纳闷你玩游戏,咋不用动手呢。。。。一语惊醒梦中人:玩游戏不用动手,怎样才能做到玩游戏不用手呢?我要去 GitHub 上找找~

本期介绍的是 GitHub 上标星 5.4k 由网易开源和维护的开源项目:Airtest 一款基于 Python 的跨平台 UI 自动化测试框架,释放双手“玩”游戏、“耍”应用!

项目地址:github.com/AirtestProj…

所谓 UI 自动化就是让游戏和应用,按照预先设定的步骤自己动。可用来实现自动测试应用,也可以用来实现应用自动化。。。就像升级版按键精灵?

一、应用场景

首先这个项目适合所有读者,只要跟着本教程仅需会一点 Python 语法,就可以学会用 Airtest 解决一些重复性的工作,释放你的双手!比如:检测僵尸好友、批量点赞、刷游戏副本、自动化测试等等。

如果测试同学上手了 Airtest 就会赞不绝口,毕竟测试界面的每个按钮是否有效是一个机械性的任务,不应该让机械的操作浪费宝贵的时间,有了它可以轻松搞定“点点点”的测试需求。

另外 Airtest 对于普通使用者能做到最有意思的事情就是做 游戏自动化。例如,在游戏中经常会有许多固定模式的日常任务,通过 Airtest 可以让这些枯燥的日常任务,自动执行从而节省时间。对于阴阳师等游戏配合游戏自带的自动战斗经过调试我们甚至可以做到全自动游玩,让玩家彻底解放双手!

看到这里是不是跃跃欲试了呢?下面就跟着小编一起上手 Airtest 吧~

二、上手

安装:

1
shell复制代码$ pip install -U airtest

对于新手来讲,我个人推荐使用官方推出可视化编辑器 AirtestIDE,使用起来非常方便,一键快速连接安卓、iOS 等设备,同时支持图像识别与 UI 元素识别。

下载地址:airtest.netease.com/

后面的教程也是采用 AirtestIDE 讲解,手机为 Android 系统。

1. 打开手机开发者模式

打开手机的设置界面,找到 关于手机 界面:

连续点击 版本号 栏目直到提示开发者模式已经打开:

然后根据机型不同,找到开发人员选项中,打开开发人员选项 -> USB 调试 -> “仅充电”模式下允许ADB调试,关闭 监控 ADB 安装应用

之后将我们的手机使用数据线与电脑连接起来。

注意:为了防止发生安全问题,不连接 AirtestIDE 时请关闭 USB 调试功能!

2. 连接安卓手机

启动 AirtestIDE 后会提示需要登录,这里可以直接点击 Skip 跳过。然后我们就能看到主界面啦:

当手机连接到电脑后,点击设备连接窗口的刷新 ADB按钮后等待一会儿就能看见我们的手机:

点击 操作 栏的 connect 后即可连接手机。

第一次连接时在手机上会弹出认证提示,无特殊情况点击同意即可

之后就能看到我们手机画面:

同时鼠标可代替手指进行点击操作。

3. 实现自动访问 HG 官网

实现手机自动访问网站仅需要简单的 3 步:

1、在 Airtest 辅助窗 找到 touch 与 text:

2、点击 touch 后在手机画面选择浏览器图标截图,生成如下代码:

3、选择 text 输入:www.hellogithub.com

最后点击 运行 按钮即可。

注意,此部分由于需要安装 yosemite 调试工具可能需要先运行几次才可成功,而且图像识别有一定概率会失败

同样,IDE 也支持录制动作,点击 Airtest 辅助窗 里的录像按钮即可:

之后使用鼠标操作手机,IDE 就会自动为我们生成脚本了。

4. 生成报告(测试人员最爱)

Airtest 可以非常方便的生成测试报告,只需要点击 IDE 上方的生成报告按钮:

即可自动生成全面的测试报告:

三、自动识别

在实际应用中,有些时候是图片识别无法完成或者存在困难的,这时我们可以利用 Poco 工具直接识别 UI 框架定位元素。

一个很简单的例子,自动打开 HG 的微信小程序并分享机器学习的内容:

1
2
3
4
5
6
7
8
9
10
11
python复制代码from airtest.core.api import *
from poco.drivers.android.uiautomation import AndroidUiautomationPoco

poco = AndroidUiautomationPoco()

auto_setup(__file__)

poco(text="HelloGitHub").click()
poco(text="#机器学习").click()
poco(text="tutorials").click()
poco("android.widget.Button").click()

利用 IDE 的 Poco 录制功能可以很方便的自动生成上述代码,实际效果如下:

在应用当中巧妙地利用 Poco 可以节省很多图片识别时间以及应用背景不同时程序的稳定性。

四、结语

到这里 Airtest 的演示就结束啦!本文仅仅演示了 Airtest 最基本的功能。游戏自动化只是其中很小的一个功能,它主要还是用来做自动化测试。支持不同设备同时进行测试,但对于使用者而言有一定的上手门槛,感兴趣去的小伙伴可以去深入研究研究,但要注意方向呀~

《讲解开源项目》:github.com/HelloGitHub…

最后,感谢您的阅读!这里是 HelloGitHub 分享 GitHub 上有趣、入门级的开源项目。您的每个点赞、留言、分享都是对我们最大的鼓励!


关注 HelloGitHub 公众号 第一时间收到更新。

还有更多开源项目的介绍和宝藏项目等待你的发现。

本文转载自: 掘金

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

Vue 团队公开快如闪电的全新脚手架工具 create-vu

发表于 2021-10-13

本文已参与「掘力星计划」,赢取创作大礼包,挑战创作激励金。

  1. 前言

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

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

美国时间 2021 年 10 月 7 日早晨,Vue 团队等主要贡献者举办了一个 Vue Contributor Days 在线会议,蒋豪群(知乎胖茶,Vue.js 官方团队成员,Vue-CLI 核心开发),在会上公开了create-vue,一个全新的脚手架工具。

create-vue使用npm init vue@next一行命令,就能快如闪电般初始化好基于vite的Vue3项目。

本文就是通过调试和大家一起学习这个300余行的源码。

阅读本文,你将学到:

1
2
3
4
5
sh复制代码1. 学会全新的官方脚手架工具 create-vue 的使用和原理
2. 学会使用 VSCode 直接打开 github 项目
3. 学会使用测试用例调试源码
4. 学以致用,为公司初始化项目写脚手架工具。
5. 等等
  1. 使用 npm init vue@next 初始化 vue3 项目

create-vue github README上写着,An easy way to start a Vue project。一种简单的初始化vue项目的方式。

1
sh复制代码npm init vue@next

估计大多数读者,第一反应是这样竟然也可以,这么简单快捷?

忍不住想动手在控制台输出命令,我在终端试过,见下图。

npm-init-vue@next

最终cd vue3-project、npm install 、npm run dev打开页面http://localhost:3000。

初始化页面

2.1 npm init && npx

为啥 npm init 也可以直接初始化一个项目,带着疑问,我们翻看 npm 文档。

npm init

npm init 用法:

1
2
3
sh复制代码npm init [--force|-f|--yes|-y|--scope]
npm init <@scope> (same as `npx <@scope>/create`)
npm init [<@scope>/]<name> (same as `npx [<@scope>/]create-<name>`)

npm init <initializer> 时转换成npx命令:

  • npm init foo -> npx create-foo
  • npm init @usr/foo -> npx @usr/create-foo
  • npm init @usr -> npx @usr/create

看完文档,我们也就理解了:

1
2
3
4
sh复制代码# 运行
npm init vue@next
# 相当于
npx create-vue@next

我们可以在这里create-vue,找到一些信息。或者在npm create-vue找到版本等信息。

其中@next是指定版本,通过npm dist-tag ls create-vue命令可以看出,next版本目前对应的是3.0.0-beta.6。

1
2
3
sh复制代码npm dist-tag ls create-vue
- latest: 3.0.0-beta.6
- next: 3.0.0-beta.6

发布时 npm publish --tag next 这种写法指定 tag。默认标签是latest。

可能有读者对 npx 不熟悉,这时找到阮一峰老师博客 npx 介绍、nodejs.cn npx

npx 是一个非常强大的命令,从 npm 的 5.2 版本(发布于 2017 年 7 月)开始可用。

简单说下容易忽略且常用的场景,npx有点类似小程序提出的随用随走。

轻松地运行本地命令

1
2
3
4
5
6
7
8
9
sh复制代码node_modules/.bin/vite -v
# vite/2.6.5 linux-x64 node-v14.16.0

# 等同于
# package.json script: "vite -v"
# npm run vite

npx vite -v
# vite/2.6.5 linux-x64 node-v14.16.0

使用不同的 Node.js 版本运行代码
某些场景下可以临时切换 node 版本,有时比 nvm 包管理方便些。

1
2
3
4
5
sh复制代码npx node@14 -v
# v14.18.0

npx -p node@14 node -v
# v14.18.0

无需安装的命令执行

1
2
sh复制代码# 启动本地静态服务
npx http-server
1
2
3
4
5
6
7
sh复制代码# 无需全局安装
npx @vue/cli create vue-project
# @vue/cli 相比 npm init vue@next npx create-vue@next 很慢。

# 全局安装
npm i -g @vue/cli
vue create vue-project

npx-vue-cli

npm init vue@next (npx create-vue@next) 快的原因,主要在于依赖少(能不依赖包就不依赖),源码行数少,目前index.js只有300余行。

  1. 配置环境调试源码

3.1 克隆 create-vue 项目

本文仓库地址 create-vue-analysis,求个star~

1
2
3
4
sh复制代码# 可以直接克隆我的仓库,我的仓库保留的 create-vue 仓库的 git 记录
git clone https://github.com/lxchuan12/create-vue-analysis.git
cd create-vue-analysis/create-vue
npm i

当然不克隆也可以直接用 VSCode 打开我的仓库
Open in Visual Studio Code

顺带说下:我是怎么保留 create-vue 仓库的 git 记录的。

1
2
3
4
5
sh复制代码# 在 github 上新建一个仓库 `create-vue-analysis` 克隆下来
git clone https://github.com/lxchuan12/create-vue-analysis.git
cd create-vue-analysis
git subtree add --prefix=create-vue https://github.com/vuejs/create-vue.git main
# 这样就把 create-vue 文件夹克隆到自己的 git 仓库了。且保留的 git 记录

关于更多 git subtree,可以看Git Subtree 简明使用手册

3.2 package.json 分析

1
2
3
4
5
6
7
8
9
10
js复制代码// create-vue/package.json
{
"name": "create-vue",
"version": "3.0.0-beta.6",
"description": "An easy way to start a Vue project",
"type": "module",
"bin": {
"create-vue": "outfile.cjs"
},
}

bin指定可执行脚本。也就是我们可以使用 npx create-vue 的原因。

outfile.cjs 是打包输出的JS文件

1
2
3
4
5
6
7
8
js复制代码{
"scripts": {
"build": "esbuild --bundle index.js --format=cjs --platform=node --outfile=outfile.cjs",
"snapshot": "node snapshot.js",
"pretest": "run-s build snapshot",
"test": "node test.js"
},
}

执行 npm run test 时,会先执行钩子函数 pretest。run-s 是 npm-run-all 提供的命令。run-s build snapshot 命令相当于 npm run build && npm run snapshot。

根据脚本提示,我们来看 snapshot.js 文件。

3.3 生成快照 snapshot.js

这个文件主要作用是根据const featureFlags = ['typescript', 'jsx', 'router', 'vuex', 'with-tests'] 组合生成31种加上 default 共计 32种 组合,生成快照在 playground目录。

因为打包生成的 outfile.cjs 代码有做一些处理,不方便调试,我们可以修改为index.js便于调试。

1
2
3
4
js复制代码// 路径 create-vue/snapshot.js
const bin = path.resolve(__dirname, './outfile.cjs')
// 改成 index.js 便于调试
const bin = path.resolve(__dirname, './index.js')

我们可以在for和 createProjectWithFeatureFlags 打上断点。

createProjectWithFeatureFlags其实类似在终端输入如下执行这样的命令

1
sh复制代码node ./index.js --xxx --xxx --force
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
js复制代码function createProjectWithFeatureFlags(flags) {
const projectName = flags.join('-')
console.log(`Creating project ${projectName}`)
const { status } = spawnSync(
'node',
[bin, projectName, ...flags.map((flag) => `--${flag}`), '--force'],
{
cwd: playgroundDir,
stdio: ['pipe', 'pipe', 'inherit']
}
)

if (status !== 0) {
process.exit(status)
}
}

// 路径 create-vue/snapshot.js
for (const flags of flagCombinations) {
createProjectWithFeatureFlags(flags)
}

调试:VSCode打开项目,VSCode高版本(1.50+)可以在 create-vue/package.json => scripts => "test": "node test.js"。鼠标悬停在test上会有调试脚本提示,选择调试脚本。如果对调试不熟悉,可以看我之前的文章koa-compose,写的很详细。

调试时,大概率你会遇到:create-vue/index.js 文件中,__dirname 报错问题。可以按照如下方法解决。在 import 的语句后,添加如下语句,就能愉快的调试了。

1
2
3
4
5
6
7
8
9
10
js复制代码// 路径 create-vue/index.js
// 解决办法和nodejs issues
// https://stackoverflow.com/questions/64383909/dirname-is-not-defined-in-node-14-version
// https://github.com/nodejs/help/issues/2907

import { fileURLToPath } from 'url';
import { dirname } from 'path';

const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);

接着我们调试 index.js 文件,来学习。

  1. 调试 index.js 主流程

回顾下上文 npm init vue@next 初始化项目的。

npm-init-vue@next

单从初始化项目输出图来看。主要是三个步骤。

1
2
3
sh复制代码1. 输入项目名称,默认值是 vue-project
2. 询问一些配置 渲染模板等
3. 完成创建项目,输出运行提示
1
2
3
4
5
6
7
8
js复制代码async function init() {
// 省略放在后文详细讲述
}

// async 函数返回的是Promise 可以用 catch 报错
init().catch((e) => {
console.error(e)
})

4.1 解析命令行参数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
js复制代码// 返回运行当前脚本的工作目录的路径。
const cwd = process.cwd()
// possible options:
// --default
// --typescript / --ts
// --jsx
// --router / --vue-router
// --vuex
// --with-tests / --tests / --cypress
// --force (for force overwriting)
const argv = minimist(process.argv.slice(2), {
alias: {
typescript: ['ts'],
'with-tests': ['tests', 'cypress'],
router: ['vue-router']
},
// all arguments are treated as booleans
boolean: true
})

minimist

简单说,这个库,就是解析命令行参数的。看例子,我们比较容易看懂传参和解析结果。

1
2
3
4
5
6
7
8
9
10
11
12
bash复制代码$ node example/parse.js -a beep -b boop
{ _: [], a: 'beep', b: 'boop' }

$ node example/parse.js -x 3 -y 4 -n5 -abc --beep=boop foo bar baz
{ _: [ 'foo', 'bar', 'baz' ],
x: 3,
y: 4,
n: 5,
a: true,
b: true,
c: true,
beep: 'boop' }

比如

1
sh复制代码npm init vue@next --vuex --force

4.2 如果设置了 feature flags 跳过 prompts 询问

这种写法方便代码测试等。直接跳过交互式询问,同时也可以省时间。

1
2
3
4
5
6
7
8
9
10
11
12
js复制代码// if any of the feature flags is set, we would skip the feature prompts
// use `??` instead of `||` once we drop Node.js 12 support
const isFeatureFlagsUsed =
typeof (argv.default || argv.ts || argv.jsx || argv.router || argv.vuex || argv.tests) ===
'boolean'

// 生成目录
let targetDir = argv._[0]
// 默认 vue-projects
const defaultProjectName = !targetDir ? 'vue-project' : targetDir
// 强制重写文件夹,当同名文件夹存在时
const forceOverwrite = argv.force

4.3 交互式询问一些配置

如上文npm init vue@next 初始化的图示

  • 输入项目名称
  • 还有是否删除已经存在的同名目录
  • 询问使用需要 JSX Router vuex cypress 等。
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
js复制代码let result = {}

try {
// Prompts:
// - Project name:
// - whether to overwrite the existing directory or not?
// - enter a valid package name for package.json
// - Project language: JavaScript / TypeScript
// - Add JSX Support?
// - Install Vue Router for SPA development?
// - Install Vuex for state management? (TODO)
// - Add Cypress for testing?
result = await prompts(
[
{
name: 'projectName',
type: targetDir ? null : 'text',
message: 'Project name:',
initial: defaultProjectName,
onState: (state) => (targetDir = String(state.value).trim() || defaultProjectName)
},
// 省略若干配置
{
name: 'needsTests',
type: () => (isFeatureFlagsUsed ? null : 'toggle'),
message: 'Add Cypress for testing?',
initial: false,
active: 'Yes',
inactive: 'No'
}
],
{
onCancel: () => {
throw new Error(red('✖') + ' Operation cancelled')
}
}
]
)
} catch (cancelled) {
console.log(cancelled.message)
// 退出当前进程。
process.exit(1)
}

4.4 初始化询问用户给到的参数,同时也会给到默认值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
js复制代码// `initial` won't take effect if the prompt type is null
// so we still have to assign the default values here
const {
packageName = toValidPackageName(defaultProjectName),
shouldOverwrite,
needsJsx = argv.jsx,
needsTypeScript = argv.typescript,
needsRouter = argv.router,
needsVuex = argv.vuex,
needsTests = argv.tests
} = result
const root = path.join(cwd, targetDir)

// 如果需要强制重写,清空文件夹

if (shouldOverwrite) {
emptyDir(root)
// 如果不存在文件夹,则创建
} else if (!fs.existsSync(root)) {
fs.mkdirSync(root)
}

// 脚手架项目目录
console.log(`\nScaffolding project in ${root}...`)

// 生成 package.json 文件
const pkg = { name: packageName, version: '0.0.0' }
fs.writeFileSync(path.resolve(root, 'package.json'), JSON.stringify(pkg, null, 2))

4.5 根据模板文件生成初始化项目所需文件

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
js复制代码  // todo:
// work around the esbuild issue that `import.meta.url` cannot be correctly transpiled
// when bundling for node and the format is cjs
// const templateRoot = new URL('./template', import.meta.url).pathname
const templateRoot = path.resolve(__dirname, 'template')
const render = function render(templateName) {
const templateDir = path.resolve(templateRoot, templateName)
renderTemplate(templateDir, root)
}

// Render base template
render('base')

// 添加配置
// Add configs.
if (needsJsx) {
render('config/jsx')
}
if (needsRouter) {
render('config/router')
}
if (needsVuex) {
render('config/vuex')
}
if (needsTests) {
render('config/cypress')
}
if (needsTypeScript) {
render('config/typescript')
}

4.6 渲染生成代码模板

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
js复制代码// Render code template.
// prettier-ignore
const codeTemplate =
(needsTypeScript ? 'typescript-' : '') +
(needsRouter ? 'router' : 'default')
render(`code/${codeTemplate}`)

// Render entry file (main.js/ts).
if (needsVuex && needsRouter) {
render('entry/vuex-and-router')
} else if (needsVuex) {
render('entry/vuex')
} else if (needsRouter) {
render('entry/router')
} else {
render('entry/default')
}

4.7 如果配置了需要 ts

重命名所有的 .js 文件改成 .ts。
重命名 jsconfig.json 文件为 tsconfig.json 文件。

jsconfig.json 是VSCode的配置文件,可用于配置跳转等。

把index.html 文件里的 main.js 重命名为 main.ts。

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

if (needsTypeScript) {
// rename all `.js` files to `.ts`
// rename jsconfig.json to tsconfig.json
preOrderDirectoryTraverse(
root,
() => {},
(filepath) => {
if (filepath.endsWith('.js')) {
fs.renameSync(filepath, filepath.replace(/\.js$/, '.ts'))
} else if (path.basename(filepath) === 'jsconfig.json') {
fs.renameSync(filepath, filepath.replace(/jsconfig\.json$/, 'tsconfig.json'))
}
}
)

// Rename entry in `index.html`
const indexHtmlPath = path.resolve(root, 'index.html')
const indexHtmlContent = fs.readFileSync(indexHtmlPath, 'utf8')
fs.writeFileSync(indexHtmlPath, indexHtmlContent.replace('src/main.js', 'src/main.ts'))
}

4.8 配置了不需要测试

因为所有的模板都有测试文件,所以不需要测试时,执行删除 cypress、/__tests__/ 文件夹

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
js复制代码  if (!needsTests) {
// All templates assumes the need of tests.
// If the user doesn't need it:
// rm -rf cypress **/__tests__/
preOrderDirectoryTraverse(
root,
(dirpath) => {
const dirname = path.basename(dirpath)

if (dirname === 'cypress' || dirname === '__tests__') {
emptyDir(dirpath)
fs.rmdirSync(dirpath)
}
},
() => {}
)
}

4.9 根据使用的 npm / yarn / pnpm 生成README.md 文件,给出运行项目的提示

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
js复制代码// Instructions:
// Supported package managers: pnpm > yarn > npm
// Note: until <https://github.com/pnpm/pnpm/issues/3505> is resolved,
// it is not possible to tell if the command is called by `pnpm init`.
const packageManager = /pnpm/.test(process.env.npm_execpath)
? 'pnpm'
: /yarn/.test(process.env.npm_execpath)
? 'yarn'
: 'npm'

// README generation
fs.writeFileSync(
path.resolve(root, 'README.md'),
generateReadme({
projectName: result.projectName || defaultProjectName,
packageManager,
needsTypeScript,
needsTests
})
)

console.log(`\nDone. Now run:\n`)
if (root !== cwd) {
console.log(` ${bold(green(`cd ${path.relative(cwd, root)}`))}`)
}
console.log(` ${bold(green(getCommand(packageManager, 'install')))}`)
console.log(` ${bold(green(getCommand(packageManager, 'dev')))}`)
console.log()
  1. npm run test => node test.js 测试

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
js复制代码// create-vue/test.js
import fs from 'fs'
import path from 'path'
import { fileURLToPath } from 'url'

import { spawnSync } from 'child_process'

const __dirname = path.dirname(fileURLToPath(import.meta.url))
const playgroundDir = path.resolve(__dirname, './playground/')

for (const projectName of fs.readdirSync(playgroundDir)) {
if (projectName.endsWith('with-tests')) {
console.log(`Running unit tests in ${projectName}`)
const unitTestResult = spawnSync('pnpm', ['test:unit:ci'], {
cwd: path.resolve(playgroundDir, projectName),
stdio: 'inherit',
shell: true
})
if (unitTestResult.status !== 0) {
throw new Error(`Unit tests failed in ${projectName}`)
}

console.log(`Running e2e tests in ${projectName}`)
const e2eTestResult = spawnSync('pnpm', ['test:e2e:ci'], {
cwd: path.resolve(playgroundDir, projectName),
stdio: 'inherit',
shell: true
})
if (e2eTestResult.status !== 0) {
throw new Error(`E2E tests failed in ${projectName}`)
}
}
}

主要对生成快照时生成的在 playground 32个文件夹,进行如下测试。

1
2
3
sh复制代码pnpm test:unit:ci

pnpm test:e2e:ci
  1. 总结

我们使用了快如闪电般的npm init vue@next,学习npx命令了。学会了其原理。

1
sh复制代码npm init vue@next => npx create-vue@next

快如闪电的原因在于依赖的很少。很多都是自己来实现。如:Vue-CLI中 vue create vue-project 命令是用官方的npm包validate-npm-package-name,删除文件夹一般都是使用 rimraf。而 create-vue 是自己实现emptyDir和isValidPackageName。

非常建议读者朋友按照文中方法使用VSCode调试 create-vue 源码。源码中还有很多细节文中由于篇幅有限,未全面展开讲述。

学完本文,可以为自己或者公司创建类似初始化脚手架。

目前版本是3.0.0-beta.6。我们持续关注学习它。

最后欢迎加我微信 ruochuan12 交流,参与 源码共读 活动,大家一起学习源码,共同进步。

  1. 参考资料

发现 create-vue 时打算写文章加入到源码共读计划中,大家一起学习。而源码共读群里小伙伴upupming比我先写完文章。

@upupming vue-cli 将被 create-vue 替代?初始化基于 vite 的 vue3 项目为何如此简单?


关于 && 交流群

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

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

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

若川的博客

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

掘金专栏,欢迎关注~

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

github blog,求个star^_^~

本文转载自: 掘金

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

Redis该怎么学?其实很简单,这份学习路线+资料+书单,送

发表于 2021-10-13

本文已参与 「掘力星计划」 ,赢取创作大礼包,挑战创作激励金。

前言

这绝对不是一篇水文,进来的兄弟们 千万不要白嫖,真香警告⚠️。 (点赞!!!)

这篇文章很早前就打算写了,特地留到现在是因为我想把Redis系列的文章全部更完,能让需要的小伙伴们有个系统的栏目学习,而不是一些零落的知识点(其实是这期间我打球把腿摔断了)。

为了总结这个专栏我看了很多Redis 的书籍、博客、各种资料,仔细研究了两个月后总结下来了三十多篇文章,总共十几万字,它们都赤身白条,静静地躺在我的专栏那里,等着你们去光顾光顾!!!

我相信我的文章读者大部分都是学生,或者应届毕业生,也有一些初中级开发,但是无论怎么样接下来的资源都非常非常适合你们,接下来就跟着小捌(拐杖)的脚步,走进Redis的世界吧!

学习路线

其实关于Redis的学习线路网上特别多,但是我把一些重要的或者很经典的东西都列了出来,并且有对应的文章去详细的介绍(如果需要原图和我的手稿的可以在文章末尾关注我的微信公众号私聊我! )

上述线路思维导图无法点击,我将每个知识点对应的文章连接总结在这里:

Redis扫盲+自己搭建Redis

《Redis扫描》

《CentOS安装Redis》

《Redis一主二从Sentinel监控配置》

《CentOS 7单机安装Redis Cluster(3主3从伪集群)》

Redis五种基本数据类型

《高级程序员必须精通的Redis,第一篇之——string(字符串)》

《高级程序员必须精通的Redis,第二篇之——list(列表)》

《高级程序员必须精通的Redis,第三篇之——hash(散列)》

《高级程序员必须精通的Redis,第四篇之——set(集合)》

《高级程序员必须精通的Redis,第五篇之——zset(有序集合)》

《你真的懂Redis的5种基本数据结构吗?这些知识点或许你还需要看看!》

Redis三种特殊数据类型

《Bitmaps-位图》

《HyperLogLog 使用及其算法原理详细讲解》

《Geospatial-地理空间》

Redis之Stream

《Redis精通系列——Stream》

Redis之Pub/Sub

《Redis精通系列——Pub/Sub(发布订阅)》

Redis之Pipeline

《Redis精通系列——Pipeline(管道)》

Redis事务

《Redis事务详述》

Redis持久化

《Redis持久化AOF与RDB全面解析》

Redis过期策略

《Redis精通系列——过期策略》

Redis淘汰策略

《Redis精通系列——LRU算法详述(Least Recently Used - 最近最少使用)》

《Redis精通系列——LFU算法详述(Least Frequently Used - 最不经常使用)》

Redis限流

《Redis之zset实现滑动窗口限流》

《漏斗限流详述》

《Redis-Cell令牌桶限流详述》

布隆过滤器

《Redis安装布隆(Bloom Filter)过滤器》

《布隆(Bloom Filter)过滤器——全面讲解,建议收藏》

分布式

《如果有人问你CAP理论是什么,你直接把这篇文章发给他!》

《Redis分布式基石——主从复制技术详述》

《假如面试官让你聊聊Sentinel(哨兵),看完这篇文章足矣!》

《Redis集群详述(从服务内部讲解,这次看完真的懂了,面试官再怎么问也能轻轻松松!)》

《⭐Redis分布式——主从复制、Sentinel、集群彻底吃透⭐(看完这篇万字长文,你的Redis水平将会上升一个层次)》

《详解Redis分布式锁(图文并茂,手把手搭建服务,代码详解,建议收藏)》

《Redis精通系列——info 指令用法》

客户端

《Jedis》

《Lettuce》

《超级强大的Redission》

《三者对比-来自腾讯云技术圈子》

学习资源

关于学习资源我主要推荐一些比较权威的网站和一些好的课程,这些我都踩过坑,送给一头雾水到处摸索的你!如果你实在不想看文章,也不想看书,我也会在这里推荐一些网课(我不是卖课的哈,我也没有收钱,嘤嘤嘤!!!所以点个赞不过分吧!!! )

网站资源

首当其冲的肯定是官网,但是英语要求比较高,而且网比较卡你懂的!

https://redis.io/

如果英语和捌哥水平差不多,我建议还是尽量别完全吊死在官网上,也别用那些翻译的奇奇怪怪的翻译软件直接翻译官网学习,建议如下网站,网速快,翻译的也比较准确(但是还是有很多知识点很拗口) :

http://redis.cn/

如果命令不记得了怎么办,收藏这个网站,超级好用:

http://redisdoc.com/

学技术怎么都是从菜鸟一步步走向大神的,那么菜鸟网站怎么会错过呢?我觉得也是入门的好选择之一:

https://www.runoob.com/

https://www.runoob.com/redis/redis-tutorial.html

如果是学Java的弟兄们,建议研究下Redission:

https://github.com/redisson/redisson/wiki/%E7%9B%AE%E5%BD%95

最后推荐一个宝藏博主,捌哥也是跟着一步步走过来的:

https://blog.csdn.net/qq_41125219/category_11179094.html

视频资源

关于Redis视频,其实网上讲的很深入的我并没有发现,但是入门的确实有几个值得去看看,不过捌哥认为学好技术还是得自己亲历亲为,不要只看不练哈……

基础视频:

尚硅谷的Redis 6,版本新,资料全

www.bilibili.com/video/BV1Rv…

狂神的Redis讲解,幽默风趣,通俗易懂

www.bilibili.com/video/BV1S5…

进阶视频:

黑马程序员的Redis入门到精通

www.bilibili.com/video/BV1CJ…

图灵学院这里面有将Redis部分还不错,比较深入:

www.bilibili.com/video/BV1Ry…

书籍推荐

这是我第一次推荐书,当然我仅仅是推荐个人书单,这都是我认真读过的书,觉得比较有帮助才敢推荐出来,也欢迎大家在评论区提建议,如果文章阅读不错,粉丝长得快,我就送出我推荐的两本全新书(需要的可以后台给我留言,或者直接关注我文末的微信公众号)。

捌哥是穷屌丝,但是为了表示诚意,必须送!!!

注意,我推荐书,我可没收钱哈,单纯就是推荐给有需要的兄弟们!!!!!!

《Redis深度历险》

首先是一本掌阅技术专家,钱文品(老钱)的书 —— 《Redis深度历险》

推荐理由:

  1. 知识点全面,Redis版本也比较新
  2. 举例丰富,文中都是Java和Python一起举例
  1. 彩色打印,图文并茂
  2. 由浅及深,通俗易懂

适合人群:

Redis入门的小伙伴,在校生、应届毕业找工作的,初中高级开发工程师我觉得都可以,书不厚,看起来不容易毛躁。

展示我自己买的那本书的一些内容:

))

《Redis设计与实现》

这本书是由黄健宏老师编写的,我买的是第二版,但是这个第二版也是Redis3.0版本,但是买了保证不亏(铁赚不亏!!!)

推荐这本书的主要原因是:

  • 讲的真的细致,简直就是细致入微,但是你又不觉得是废话,保证看完之后让你受益匪浅,真心吊打面试官系列。
  • 源码丰富,原理讲的十分透彻
  • 作者多年经验,有非常多不错的思维方式输出,对于一个真心探求揭开Redis面纱的人很有版主

当然部分知识点确实有些落后,但是也不碍事,大家在阅读的时候自己关注官方版本升级时,改动了哪些地方明白就好。

))

白嫖不好,创作不易, 各位的点赞就是李子捌创作的最大动力,我们下篇文章见,文末公众号关注有福利!

持续更新,未完待续……

本文转载自: 掘金

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

mongodb野史

发表于 2021-10-13

最近因为新游戏业务的需要,打算抛弃mysql,拥抱mongodb,于是打算找mongodb本人聊聊,看看是否真的有值得被用的价值。

mongodb从办公区走了出来,高高瘦瘦的一个年轻小伙子啊,本以为应该是一个小肥仔呢,毕竟芒果不就是肥肥胖胖的吗。

image-20211010163332919

你好,请问你是芒果db吗?我率先打起了招呼。

mongodb回复,是呀,不过我不叫芒果db,我叫mongodb啦,我的原名取自英文单词”Humongous”,寓意庞大,人如其名,意味着我是具备处理大规模数据能力的。

嗯,志存高远啊,不错,之前问过mysql阿姨,她说你和她不一样,她比较擅长处理关系型的数据,你比较理性,比较擅长处理非关系型的数据,还自称文档型数据库,想问问看你们两个的职责有什么区别吗?

image-20211010163502838

首先我先解释下什么是文档型数据库哈,文档是数据的基本单元,将多个键值对有序地放置在一起称为文档。可能这么说你也不大理解,这样吧,你看下这个表格就明白了

image-20211010163932109

简单的解释便是文档对应了数据库内的行。

至于职责上其实就和mysql阿姨描述的一样,我们一个是处理关系型,一个是处理非关系型,算是互补,对你们使用者来说,如果是因为业务需求或者是项目初始阶段,而导致数据的具体格式无法明确定义的话,使用mongodb最合适啦,相比关系型数据库来说,我们比较非常容易被扩展,这也为写代码带来了极大的方便。

哦,明白,那找你算是找对啦,我们游戏数据本身没什么关系,不过我们项目组这边和mysql阿姨比较熟悉,毕竟经常合作的,mongodb是第一次了解,想问问小哥哥,新手入门会不会比较困难呀?

看出来你们是新手了哈哈,放心啦,不会的,因为mysql阿姨的设计很优秀,并且使用广泛,因而我的爸爸Dwight Merriman在设计之初,也是严格参考了mysql的概念模型的,毕竟向前辈学习也是很重要的嘛,因而很多人都说,mongodb是 NoSQL中最像mysql的数据库的,你看看这个表格就知道啦

image-20211010122416995

基本上就是换了个叫法而已,至于有什么区别,我让他们本人出来和你解释吧。

mongodb说完,便跑出了几个小弟,原来mongodb还是一个小团队啊,刚刚和我聊的原来是队长,主要负责销售,各司其职,精妙啊。

image-20211010163551743

首先是名叫database数据库的小伙子开口了,他说他与mysql阿姨的数据库(database)概念相同,一个数据库包含多个集合(表),说完便笑嘻嘻的走开了。

接着是名叫collection集合的壮汉,哇,这身肌肉实在是绝了,他说他相当于mysql阿姨的表(table),一个集合可以存放多个文档(行),不过他的不同之处就在于集合的结构(schema)是动态的,不需要预先声明一个严格的表结构。

后面是名叫document 文档的小老妹和叫field字段小老弟开口,两个人都比较害羞,娇滴滴的说一个相当于SQL中的行(row),一个文档由多个字段(列)组成,并采用bson(json)格式表示,另一个相当于SQL中的列(column),相比普通column的差别在于field的类型可以更加灵活,比如支持嵌套的文档、数组。

filed字段小老弟说完,也给我画了一个表格,说是字段的对照。

image-20211010123907226

听完几个小伙子的发言,我感叹到,设计真精妙,对新手太友好了。不过刚刚听到了一个比较新的东西,bson是什么呢?

mongodb队长大叫了一声,bson出来下,说说你是个什么东西。

mongodb队长说完,在场的都笑了哈哈哈。

bson听到有人叫他,便跑了出来。

嗯,长得很帅气的一个小伙子。

image-20211010163648567

bson说,MongoDB 文档从格式上讲,是基于 JSON 的,典型的格式是这样的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
json复制代码{
"_id": 1,
"name" : { "first" : "John", "last" : "Backus" },
"contribs" : [ "Fortran", "ALGOL", "Backus-Naur Form", "FP" ],
"awards" : [
{
"award" : "W.W. McDowell Award",
"year" : 1967,
"by" : "IEEE Computer Society"
}, {
"award" : "Draper Prize",
"year" : 1993,
"by" : "National Academy of Engineering"
}
]
}

曾经,JSON 的出现及流行让 Web 2.0 的数据传输变得非常简单,所以使用 JSON 语法是非常容易让开发者接受的。但是 JSON 也有自己的短板,比如无法支持像日期这样的特定数据类型,因此 MongoDB 实际上使用的是一种扩展式的JSON,叫 BSON(Binary JSON),可以支持多种结构类型,简言之,我就是JSON老哥的升级版,给你看看我支持的数据类型

image-20211010163948172

哦,了解了,刚刚听field字段小老弟说到_id,想了解下在分布式情况下,mongodb在唯一id这块怎么做到对抗高并发的?我们之前在调研mysql分布式应用的时候,发现mysql这块没做处理,还是挺蛋疼的。

field字段小老弟听到我在问他问题,于是就很害羞的走了出来,奶声奶气的说,基本上使用mysql等关系型数据库时,主键都是设置成自增的,不过在分布式环境下很容易冲突,为此我们采用了一个称之为ObjectId的类型来做主键,ObjectId是一个12字节的 BSON 类型字符串,每一个主键由4部分构成:

  • 4字节:UNIX时间戳
  • 3字节:表示运行MongoDB的机器
  • 2字节:表示生成此_id的进程
  • 3字节:由一个随机数开始的计数器生成的值

前三个部分可以保证在每一秒每一个mongo进程产生的文档id是不同的,然后每一个mongo进程会自己维护一个计数器,那么同一进程每一秒内的文档也会不同,但是会有一个上限值,跟计数器的位数有关。通过这种方式保证了在同一个集合中的唯一性。

嗯,确实,通过这四部分,基本可以说是唯一的了。

看我表现的比较满意,mongodb队长继续补充,在操作语法这块想必你也应该考虑过是否用起来比较丝滑的问题,放心,我们对数据的操作命令也基于JSON/BSON 格式,基本上没什么学习成本,给你举几个例子

比如插入文档的操作:

image-20211010153351527

执行文档查找:

image-20211010153404721

删除文档:

image-20211010153418791

在传统的SQL语法中,可以限定返回的字段,MongoDB可以使用Projection来表示:

image-20211010153431191

你应该也发现啦,这种基于BSON/JSON 的语法格式并不复杂,它的表达能力甚至要比SQL更加强大。

嗯,确实丝滑,我们项目组那边的小伙伴应该会很喜欢,突然想皮下,那如果我们想使用SQL语句进行查询什么的,可以嘛?

mongodb队长微微一笑,当然是可以的,不过需要借由第三方工具平台实现,你是服务端,可以看看 presto 之类的一些平台,如果是客户端的话,可以考虑使用mongobooster、studio3t 这样的工具。

对了,索引这块呢,在mysql中我们可是在这块折腾了许久,会不会换成mongodb,又得从头再来一次呢?

嘿嘿嘿,mongodb再次发出了笑声,仿佛是猜到了我想问什么一样,不用。

首先索引的技术实现依赖底层的存储引擎,目前我们使用 wiredTiger 作为默认的引擎。该引擎在在索引的实现上使用了 B+树的结构,这与其他的传统数据库并没有什么不同,这意味着大部分SQL数据库的一些索引调优技巧在 MongoDB 上仍然是可行的。

哇,这就太好啦,那怎么创建索引呢?

别急,看我给举几个例子,mongodb这边使用 ensureIndexes 为集合声明一个普通的索引

1
text复制代码db.book.ensureIndex({author: 1})

author后面的数字是啥意思呢? 1 代表升序,如果的话是降序则是 -1,这点应该是很好理解的。

那么如何实现复合式(compound)的索引呢?我们可以看看

1
text复制代码db.book.ensureIndex({type: 1, published: 1})

不过只有对于复合式索引时,索引键的顺序才变得有意义。

看吧,是不是很简单,基本上都是很清晰的。

除此之外,mongodb还支持以下几种索引:

  • 哈希(HASH)索引,哈希是另一种快速检索的数据结构,MongoDB 的 HASH 类型分片键会使用哈希索引。
  • 地理空间索引,用于支持快速的地理空间查询,如寻找附近1公里的商家。
  • 文本索引,用于支持快速的全文检索
  • 模糊索引(Wildcard Index),一种基于匹配规则的灵活式索引,在4.2版本开始引入。

这几种索引类型,貌似mysql也是没有的。

确实,地理空间索引这些貌似没听说过,当然了,也可能是我没用过,那么索引的特性也都有嘛?

那必须的啊,可以通过设置参数来赋予索引特性呀,比如

  • unique=true,表示一个唯一性索引
  • expireAfterSeconds=3600,表示这是一个TTL索引,并且数据将在1小时后老化
  • sparse=true,表示稀疏的索引,仅索引非空(non-null)字段的文档
  • partialFilterExpression: { rating: { $gt: 5 },条件式索引,即满足计算条件的文档才进行索引

挺好啊,基本一致,学习成本确实低啊,那是不是索引评估这块也是使用explain啊。

对头,可以使用 explain() 命令可以用于查询计划分析,进一步评估索引的效果,比如

image-20211010155541852

可以从结果看出执行计划是否高效,比如未能命中索引的结果,会显示COLLSCAN,命中索引的结果,使用IXSCAN,出现了内存排序,显示为 SORT,开发人员便可以通过结果进行调优啦。

啊啊啊,挺好挺好,对于我们这群mysql转mongodb的来说,基本没什么学习成本啊,另外,你给我推荐基本书吧,你推荐的,我们也比较放心。

没问题呀,诺,刚好我们这边也在收集相关资料

image-20211010161225895

还不错,有需要自取啦,点击链接

好啊,这样,你来我们项目组这边做个分享吧,我们小伙伴这边想听听高可用这块的设计呢。

好呀,我们晚点再约个时间,到时候再细聊。

关于mysql相关的,有兴趣的朋友也可以看看 zhuanlan.zhihu.com/p/404685504

这篇文章写的比较认真,有帮助的话,求各位朋友们点赞 + 喜欢 + 收藏支持下啦!❤️

我是稀饭下雪,持续分享编程干货,后面高可用这块等我和mongodb队长约好时间,我们不见不散。

本文转载自: 掘金

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

谈谈java中的引用类型 引用类型简介 强引用 软引用 弱引

发表于 2021-10-13

本文已参与「掘力星计划」,赢取创作大礼包,挑战创作激励金。

引用类型简介

java中的引用其实就像是一个对象的名字或者别名,一个对象在内存中会请求一块空间来保存数据,根据对象的大小,它可能需要占用的空间大小也不等。访问对象的时候,不会直接是访问对象在内存中的数据,而是通过引用去访问。引用也是一种数据类型,可以把它想象为类似C++语言中指针的东西,它指示了对象在内存中的地址,只不过我们不能够观察到这个地址究竟是什么。

如果我们定义了不止一个引用指向同一个对象,那么这些引用是不相同的,因为引用也是一种数据类型,需要一定的内存空间(stack,栈空间)来保存。但是它们的值是相同的,都指示同一个对象在内存(heap,堆空间)的中位置。在java中引用类型分为两类:值类型和引用类型,其中值类型就是基本数据类型,如int,double类型,而引用类型就是除了基本数据类型之外的所有类型。在JDK.1.2之后Java对引用的概念进行了扩充,将引用分为了:强引用(Strong Reference)、软引用(Soft Reference)、弱引用(Weak Reference)、虚引用(Phantom Reference)4种,这4种引用的强度依次减弱。

强引用

强引用是使用最普遍的引用。如果一个对象具有强引用,那垃圾回收器绝不会回收它。当内存空间不足,Java虚拟机宁愿抛出OutOfMemoryError错误,使程序异常终止,也不会靠随意回收具有强引用的对象来解决内存不足的问题。比如:A a = new A()。强引用有一下三个特性:

强引用可以直接访问目标对象。

强引用所指向的对象在任何时候都不会被系统回收。

强引用可能导致内存泄漏。

源码如下:

1
2
3
4
5
6
7
8
9
scala复制代码/**
* Final references, used to implement finalization
*/
class FinalReference<T> extends Reference<T> {

public FinalReference(T referent, ReferenceQueue<? super T> q) {
super(referent, q);
}
}

对象新建后默认为强引用类型的,是普遍对象引用的类型。FinalReference的源码只有一个空实现,这也说明强引用是“默认引用类型”

GC 回收问题

对象因为Finalizer的引用而变成了一个临时的强引用,即使没有其他的强引用,还是无法立即被回收;
对象至少经历两次GC才能被回收,因为只有在FinalizerThread执行完了对象的finalize方法的情况下才有可能被下次GC回收,而有可能期间已经经历过多次GC了,但是一直还没执行对象的finalize方法;
CPU资源比较稀缺的情况下FinalizerThread线程有可能因为优先级比较低而延迟执行对象的finalize方法;
因为对象的finalize方法迟迟没有执行,有可能会导致大部分f对象进入到old分代,此时容易引发old分代的GC,甚至Full GC,GC暂停时间明显变长,甚至导致OOM;

软引用

软引用是用来描述一些“还有用但是非必须”的对象。软引用的回收策略在不同的JVM实现会略有不同,JVM不仅仅只会考虑当前内存情况,还会考虑软引用所指向的referent最近的使用情况和创建时间来综合决定是否回收该referent。软引用保存了两个变量:

timestamp:每次调用get方法都会更新时间戳。JVM可以利用该字段来选择要清除的软引用,但不是必须要这样做。

clock:时间锁,由垃圾收集器更新。

因此,任何GC都可以使用这些字段并定义清除软引用的策略,例如:最后清除最近创建的或最近使用的软引用。在JDK 1.2之后,提供了SoftReference类来实现软引用。软引用可用来实现内存敏感的高速缓存。软引用可以和一个引用队列(ReferenceQueue)联合使用,如果软引用所引用的对象被垃圾回收器回收,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
csharp复制代码/**
* 软引用对象由垃圾收集器根据内存需要决定是否清除。软引用经常用于实现内存敏感的缓存。
*
* 假如垃圾收集器在某个时间确定对象是软可达的,此时它可以选择原子地清除
* 指向该对象的所有软引用,以及从该对象通过强引用链连接的其他软可达对象的所有软引用。
* 与时同时或者之后的某个时间,它会将注册了reference queues的新清除的软引用加入队列。
*
* 在虚拟机抛出OutOfMemoryError异常之前,将保证清除对软可达对象的所有软引用。
* 不过,并没有对清除软引用的时间以及清除顺序施加强制约束。
* 但是,鼓励虚拟机实现偏向不清除最近创建或最近使用的软引用。
*
* 该类的直接实例可用于实现简单的缓存。
* 该类或其派生子类也可用于更大的数据结构以实现更复杂的高速缓存。
* 只要软引用的引用对象还是强可达的,即还在实际使用中,软引用就不会被清除。
* 因此,复杂的高速缓存可以通过持有对最近使用缓存对象的强引用来防止其被清除,
* 而不常使用的剩余缓存对象由垃圾收集器决定是否清除。
*/
public class SoftReference&lt;T&gt; extends Reference&lt;T&gt; {
static private long clock;
private long timestamp;

// 返回对象的引用。如果该引用对象已经被程序或者垃圾收集器清除,则返回null。
// 把最近一次垃圾回收时间赋值给timestamp
public T get() {
T o = super.get();
if (o != null &amp;&amp; this.timestamp != clock)
this.timestamp = clock;
return o;
}

}

下面看一个例子:

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
csharp复制代码/**
* 软引用在pending状态时,referent就已经是null了。
*
* 启动参数:-Xmx5m
*
*/
public class SoftReference {

private static ReferenceQueue<MyObject> queue = new ReferenceQueue<>();

public static void main(String[] args) throws InterruptedException {
Thread.sleep(3000);
MyObject object = new MyObject();
SoftReference<MyObject> softRef = new SoftReference(object, queue);
new Thread(new CheckRefQueue()).start();

object = null;
System.gc();
System.out.println("After GC : Soft Get = " + softRef.get());
System.out.println("分配大块内存");

/**
* 总共触发了 3 次 full gc。第一次有System.gc();触发;第二次在在分配new byte[5*1024*740]时触发,然后发现内存不够,于是将softRef列入回收返回,接着进行了第三次full gc。
*/
//byte[] b = new byte[5*1024*740];

/**
* 也是触发了 3 次 full gc。第一次有System.gc();触发;第二次在在分配new byte[5*1024*740]时触发,然后发现内存不够,于是将softRef列入回收返回,接着进行了第三次full gc。当第三次 full gc 后发现内存依旧不够用于分配new byte[5*1024*740],则就抛出了OutOfMemoryError异常。
*/
byte[] b = new byte[5*1024*790];

System.out.println("After new byte[] : Soft Get = " + softRef.get());
}

public static class CheckRefQueue implements Runnable {

Reference<MyObject> obj = null;

@Override
public void run() {
try {
obj = (Reference<MyObject>) queue.remove();
} catch (InterruptedException e) {
e.printStackTrace();
}

if (obj != null) {
System.out.println("Object for softReference is " + obj.get());
}

}
}

public static class MyObject {

@Override
protected void finalize() throws Throwable {
System.out.println("MyObject's finalize called");
super.finalize();
}

@Override
public String toString() {
return "I am MyObject.";
}
}
}

弱引用

用来描述非必须的对象,强度比软引用更弱一些,被弱引用关联的对象只能生存到下一次垃圾收集发送之前。开始回收是,无论当前内存是否足够,都会回收掉只被弱引用关联的对象。Java垃圾回收器准备对WeakReference所指向的对象进行回收时,调用对象的finalize()方法之前,WeakReference对象自身会被加入到这个ReferenceQueue对象中,此时可以通过ReferenceQueue的poll()方法取到它们。源码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
java复制代码/**
* 弱引用对象不能阻止自身的引用被回收。
* 弱引用常用于实现规范化映射(对象实例可以在程序的多个地方同时使用)。
*
* 假如垃圾收集器在某个时间点确定对象是弱可达的。那时它将原子地清除对该对象的所有弱引用
* 以及该引用通过强引用或者软引用连接的所有其他弱可达对象的所有弱引用。
* 同时,它将表明前面所指的所有弱可达对象都可以执行finalize方法。
* 与此同时或之后某一个时间,它将注册了reference queues的那些新清除弱引用加入队列。
*/
public class WeakReference&lt;T&gt; extends Reference&lt;T&gt; {

// 创建没有注册ReferenceQueue的弱引用
public WeakReference(T referent) {
super(referent);
}

// 创建注册了ReferenceQueue的弱引用
public WeakReference(T referent, ReferenceQueue&lt;? super T&gt; q) {
super(referent, q);
}
}

虚引用

虚引用是所有引用类型中最弱的一种。一个对象是否关联到虚引用,完全不会影响该对象的生命周期,也无法通过虚引用来获取一个对象的实例。为对象设置一个虚引用的唯一目的是:能在此对象被垃圾收集器回收的时候收到一个系统通知,它就是利用ReferenceQueue实现的。当referent被gc回收时,JVM自动把虚引用对象本身加入到ReferenceQueue中,表明该reference指向的referent被回收。然后可以通过去queue中取到reference,可以通过这个来做额外的清理工作。可以用虚引用代替对象finalize方法来实现资源释放,这样更加灵活和安全。

PhantomReference只有当Java垃圾回收器对其所指向的对象真正进行回收时,会将其加入到这个ReferenceQueue对象中,这样就可以追踪对象的销毁情况。这里referent对象的finalize()方法已经调用过了。
所以具体用法和之前两个有所不同,它必须传入一个ReferenceQueue对象。当虚引用所引用对象准备被垃圾回收时,虚引用会被添加到这个队列中。源码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
java复制代码/**
* 虚引用对象在被垃圾收集器检查到后加入reference queues队列,否则会被回收。
* 虚引用最常用于实现比Java finalization机制更灵活的安排额外的清理工作。
*
* 如果垃圾收集器在某个时间点确定虚引用对象是虚可达的,那么在那个时间或之后某个时间它会将引用加入reference queues队列。
*
* 为了确保可回收对象保持不变,虚引用的引用无法使用:虚引用对象的get方法始终返回null。
*
* 与软引用和弱引用不同,当虚引用加入reference queues队列后垃圾收集器不会被自动清除。
* 只通过虚引用可达的对象将保持不变,直到所有此类引用都被清除或自已变为不可达。
*/
public class PhantomReference&lt;T&gt; extends Reference&lt;T&gt; {

// 由于不能通过虚引用访问对象,因此此方法始终返回null。
public T get() {
return null;
}

// 使用空ReferenceQueue队列创建一个虚引用没有意义:它的get方法总是返回null,
// 并且由于它没有注册队列,所以也不会被加入队列有任何清理前的预处理操作。
public PhantomReference(T referent, ReferenceQueue&lt;? super T&gt; q) {
super(referent, q);
}
}

下面看一个例子:

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
csharp复制代码public class PhantomReferenceTest {

private static ReferenceQueue<MyObject> queue = new ReferenceQueue<>();

public static void main(String[] args) throws InterruptedException {
MyObject object = new MyObject();
Reference<MyObject> phanRef = new PhantomReference<>(object, queue);
System.out.println("创建的虚拟引用为: " + phanRef);
new Thread(new CheckRefQueue()).start();

object = null;

int i = 1;
while (true) {
System.out.println("第" + i++ + "次GC");
System.gc();
TimeUnit.SECONDS.sleep(1);
}

/**
* 在经过一次GC之后,系统找到了垃圾对象,并调用finalize()方法回收内存,但没有立即加入PhantomReference Queue中。因为MyObject对象重写了finalize()方法,并且该方法是一个非空实现,所以这里MyObject也是一个Final Reference。所以第一次GC完成的是Final Reference的事情。
* 第二次GC时,该对象(即,MyObject)对象会真正被垃圾回收器进行回收,此时,将PhantomReference加入虚引用队列( PhantomReference Queue )。
* 而且每次gc之间需要停顿一些时间,已给JVM足够的处理时间;如果这里没有TimeUnit.SECONDS.sleep(1); 可能需要gc到第5、6次才会成功。
*/

}

public static class MyObject {

@Override
protected void finalize() throws Throwable {
System.out.println("MyObject's finalize called");
super.finalize();
}

@Override
public String toString() {
return "I am MyObject";
}
}

public static class CheckRefQueue implements Runnable {

Reference<MyObject> obj = null;

@Override
public void run() {
try {
obj = (Reference<MyObject>)queue.remove();
System.out.println("删除的虚引用: " + obj + " , 虚引用的对象: " + obj.get());
System.exit(0);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}

总结

Java的4种引用的级别由高到低依次为:

强引用 > 软引用 > 弱引用 > 虚引用。

引用.PNG

抽奖说明

1.本活动由掘金官方支持 详情可见juejin.cn/post/701221…

2.通过评论和文章有关的内容即可参加,要和文章内容有关哦!

3.本月的文章都会参与抽奖活动,欢迎大家多多互动!

4.除掘金官方抽奖外本人也将送出周边礼物(马克杯一个和掘金徽章若干,马克杯将送给走心评论,徽章随机抽取,数量视评论人数增加)。

本文转载自: 掘金

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

springboot + dockerfile-maven-

发表于 2021-10-12

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

整合步骤 (maven + springboot)

  1. 在maven的pom.xml文件中增加配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
xml复制代码	<properties>
<docker.image.prefix>xd</docker.image.prefix>
</properties>

<build>
<finalName>docker-demo</finalName>
<plugins>
<plugin>
<groupId>com.spotify</groupId>
<artifactId>dockerfile-maven-plugin</artifactId>
<version>1.3.6</version>
<configuration>
<repository>${docker.image.prefix}/${project.artifactId}</repository>
<buildArgs>
<JAR_FILE>target/${project.build.finalName}.jar</JAR_FILE>
</buildArgs>
</configuration>
</plugin>
</plugins>
</build>

配置讲解

​ Spotify 的 docker-maven-plugin 插件是用maven插件方式构建docker镜像的。project.build.finalName∗∗产出物名称,缺省为∗∗{project.build.finalName}** 产出物名称,缺省为**project.build.finalName∗∗产出物名称,缺省为∗∗{project.artifactId}-${project.version}

  1. 打包SpringCloud镜像并上传私有仓库并部署

什么是dockerFile

​ 什么是Dockerfile : 由一系列命令和参数构成的脚本,这些命令应用于基础镜像, 最终创建一个新的镜像

​ 创建 Dockerfile 添加内容 (默认是根目录, 可以修改为src/main/docker/Dockerfile,如果修则需要制定路径)

​ 下面是dockfile的内容:

1
2
3
4
5
dockerfile复制代码FROM openjdk:8-jdk-alpine
VOLUME /tmp
ARG JAR_FILE
COPY ${JAR_FILE} app.jar
ENTRYPOINT ["java","-jar","/app.jar"]

参数讲解:

  • FROM : 需要一个基础镜像,可以是公共的或者是私有的, 后续构建会基于此镜像,如果同一个Dockerfile中建立多个镜像时,可以使用多个FROM指令
  • VOLUME 配置一个具有持久化功能的目录,主机 /var/lib/docker 目录下创建了一个临时文件,并链接到容器的/tmp。改步骤是可选的,如果涉及到文件系统的应用就很有必要了。/tmp目录用来持久化到 Docker 数据文件夹,因为 Spring Boot 使用的内嵌 Tomcat 容器默认使用/tmp作为工作目录
  • ARG:设置编译镜像时加入的参数, ENV 是设置容器的环境变量
  • COPY : 只支持将本地文件复制到容器 ,还有个ADD更强大但复杂点
  • ENTRYPOINT:容器启动时执行的命令
  • EXPOSE 8080:暴露镜像端口
  1. 构建镜像

1
2
ini复制代码mvn install dockerfile:build
mvn install dockerfile:build -Dmaven.test.skip=true

​ 补充: 如何在阿里云上的linux如何打包上去

  1. 在maven 安装目录的setting文件中加入配置

1
2
3
4
5
xml复制代码<server>
<id>docker-registry</id>
<username>*******</username>
<password>******</password>
</server>
  1. 修改阿里云centeros7 Docker配置开放远程访问

1
bash复制代码vi /usr/lib/systemd/system/docker.service

​ ExecStart这一行后面加上 -H tcp://0.0.0.0:2375 -H unix:///var/run/docker.sock

最终效果:

1
ruby复制代码ExecStart=/usr/bin/dockerd -H tcp://0.0.0.0:2375 -H unix:///var/run/docker.sock
  1. 重启

1
2
3
sql复制代码systemctl daemon-reload

systemctl start docker
  1. 服务器验证是否开始监听

1
2
3
bash复制代码netstat -anp|grep 2375

curl 127.0.0.1:2375/info
  1. windows系统环境变量中新建DOCKER_HOST, 值为 tcp://[修改成你的远程服务器端口号]:2375

  1. 打标签

1
bash复制代码docker tag a1b9fc71720d registry.cn-shenzhen.aliyuncs.com/xd/xd_images:docker-demo-v201808
  1. 推送到镜像仓库

1
bash复制代码docker push registry.cn-shenzhen.aliyuncs.com/xd/xd_images:docker-demo-v201808
  1. 应用服务器拉取镜像

1
bash复制代码docker pull registry.cn-shenzhen.aliyuncs.com/xd/xd_images:docker-demo-v201808
  1. 启动

1
css复制代码docker run -d --name xd_docker_demo1 -p 8099:8080  a1b9fc71720d
  1. 查看启动日志

1
linux复制代码docker logs -f  containerid
  1. 访问

1
arduino复制代码http://[IP]:8099/find/user

docker整合注册中心

  1. 新增maven插件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
xml复制代码<properties>
<docker.image.prefix>xd</docker.image.prefix>
</properties>
<build>
<finalName>docker-demo</finalName>
<plugins>
<plugin>
<groupId>com.spotify</groupId>
<artifactId>dockerfile-maven-plugin</artifactId>
<version>1.3.6</version>
<configuration>
<repository>${docker.image.prefix}/${project.artifactId}</repository>
<buildArgs>
<JAR_FILE>target/${project.build.finalName}.jar</JAR_FILE>
</buildArgs>
</configuration>
</plugin>
</plugins>
</build>
  1. 新建Dockerfile

1
2
3
4
5
css复制代码FROM openjdk:8-jdk-alpine
VOLUME /tmp
ARG JAR_FILE
COPY ${JAR_FILE} app.jar
ENTRYPOINT ["java","-jar","/app.jar"]
  1. 打包

1
2
ini复制代码mvn install dockerfile:build
mvn install -Dmaven.test.skip=true dockerfile:build
  1. 推送阿里云镜像仓库

阿里云镜像仓库:dev.aliyun.com/search.html

1
2
3
bash复制代码docker tag 062d2ddf272a registry.cn-shenzhen.aliyuncs.com/xd/xd_images:eureka-v20180825
docker push registry.cn-shenzhen.aliyuncs.com/xd/xd_images:eureka-v20180825
docker pull registry.cn-shenzhen.aliyuncs.com/xd/xd_images:eureka-v20180825

5、查看日志

1
复制代码docker logs -f  containerid
  1. 运行

docker run -d --name docker-eureka-server -p 8761:8761 e7f687f101a7

Docker Redis安装

  1. 搜索镜像

1
sql复制代码docker search redis
  1. 拉取

1
bash复制代码docker pull docker.io/redis
  1. 启动

1
arduino复制代码docker run --name "xd_redis" -p 6379:6379 -d 4e8db158f18d

参考:

1
bash复制代码docker run --name "xd_redis" -p 6379:6379 -d 4e8db158f18d --requirepass "123456" -v $PWD/data:/data
  1. 访问redis容器里面,进行操作

1
bash复制代码docker exec -it 295058d2b92e redis-cli

​

本文转载自: 掘金

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

一起来打造属于自己的服务器:Linux服务器及web环境搭建

发表于 2021-10-12

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

想拥有自己的服务器,就需要搭建服务器,服务器搭建可以在Linux或者windows,主流的服务器都是搭建在Linux,这里我们就以Linux服务器搭建进行讲解。

Linux环境安装教程

服务器搭建,首先要拥有Linux环境,VMware虚拟机和centos镜像文件是必不可少的

VMware下载地址 密码:77yb

centos下载地址

安装 VMware

下载后,傻瓜式一直下一步即可安装成功,如图所示:

图片

创建虚拟机

安装好VMware后,现在就要创建新的虚拟机

图片

这里可以选择典型,或者自定义安装,我们这里为了方便就选择典型安装

图片

然后将下载的centos镜像文件导入其中

图片

图片

这里需要选择磁盘大小,默认20g就可以了

图片

进入centos安装页面,我们选择第一个,Install Centos 7

图片

图片

设置用户名和密码

图片

安装完成,就可以进入linux的世界了

搭建web环境

如果需要将自己的应用部署到
Linux安裝好后就要开始搭建web环境了搭建web环境需要jdk,mysql,tomcat这三个软件

这三个软件下载地址: pan.baidu.com/s/1twEJsEVz… 密码:63kw

软件建议安装在 cd /usr/local目录下

1
2
3
4
5
java复制代码mkdir jdk1.8

mkdir tomcat

mkdir mysql

创好的文件夹如图所示

图片

j d k安装

首先安装jdk,解压jdk安装包然后输入命令去编辑profile文件

1
2
3
4
5
6
7
8
9
10
11
12
13
ini复制代码  vim /etc/profile
#set java environment


JAVA\_HOME=/usr/local/jdk1.7.0\_71

CLASSPATH=.:$JAVA\_HOME/lib.tools.jar

PATH=$JAVA\_HOME/bin:$PATH

这段代码拷贝到profile最后一行中

:wq保存退出

接着进入 cd /usr/local/jdk1.7.0_71/bin目录中

输入命令

1
2
3
4
5
java复制代码sudo yum install glibc.i686

source /etc/profile

java -version

图片

如果如上图显示,则代表jdk配置成功

安装Tomcat

先解压,解压完成后

然后进入tomcat的bin目录

cd /usr/local/tomcat/bin

启动Tomcat

./startup.sh

开放8080端口

/sbin/iptables -I INPUT -p tcp –dport 8080 -j ACCEPT

在页面访问

图片

tomcat就安装成功了

安装mysql

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
css复制代码解压命令

tar -xvf mysql-5.7.17-1.el5.x86\_64.rpm-bundle

第三步:解压后进入mysql目录开始执行安装操作,按顺序依次执行

rpm -ivh --force mysql-community-common-5.7.16-1.el6.x86\_64.rpm --nodeps (依赖包)

rpm -ivh --force mysql-community-libs-5.7.16-1.el6.x86\_64.rpm --nodeps (jar包)

rpm -ivh --force mysql-community-client-5.7.16-1.el6.x86\_64.rpm --nodeps (客户端)

rpm -ivh --force mysql-community-server-5.7.16-1.el6.x86\_64.rpm --nodeps (服务器端)

第四步启动mysql以及获取初始密码并更改

service mysqld start(启动)

在.log文件获取初始密码

vim /var/log/mysqld.log

图片

图中红框内就是默认的密码,如果想修改密码,就操作如下命令:

1
2
3
ini复制代码mysql -u root -p

set password = password('root');

设置密码提示密码太简单

原来MySQL5.6.6版本之后增加了密码强度验证插件validate_password,相关参数设置的较为严格。

使用了该插件会检查设置的密码是否符合当前设置的强度规则,若不满足则拒绝设置。影响的语句和函数有:create user,grant,set password,password(),old password。

解决方案

  1. 查看mysql全局参数配置

该问题其实与mysql的validate_password_policy的值有关。

查看一下msyql密码相关的几个全局参数

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
go复制代码 `mysql> select @@validate_password_policy;`

`+----------------------------+`

`| @@validate_password_policy |`

`+----------------------------+`

`| MEDIUM |`

`+----------------------------+`

`1 row in set (0.00 sec)`

`mysql> SHOW VARIABLES LIKE 'validate_password%';`

`+--------------------------------------+--------+`

`| Variable_name | Value |`

`+--------------------------------------+--------+`

`| validate_password_dictionary_file | |`

`| validate_password_length | 8 |`

`| validate_password_mixed_case_count | 1 |`

`| validate_password_number_count | 1 |`

`| validate_password_policy | MEDIUM |`

`| validate_password_special_char_count | 1 |`

`+--------------------------------------+--------+`

`6 rows in set (0.08 sec)`
  1. 参数解释

validate_password_dictionary_file

插件用于验证密码强度的字典文件路径。

validate_password_length

密码最小长度,参数默认为8,它有最小值的限制,最小值为:validate_password_number_count + validate_password_special_char_count + (2 * validate_password_mixed_case_count)

validate_password_mixed_case_count

密码至少要包含的小写字母个数和大写字母个数。

validate_password_number_count

密码至少要包含的数字个数。

validate_password_policy

密码强度检查等级,0/LOW、1/MEDIUM、2/STRONG。有以下取值:

Policy Tests Performed

0 or LOW Length

1 or MEDIUM Length; numeric, lowercase/uppercase, and special characters

2 or STRONG Length; numeric, lowercase/uppercase, and special characters; dictionary file

默认是1,即MEDIUM,所以刚开始设置的密码必须符合长度,且必须含有数字,小写或大写字母,特殊字符。

validate_password_special_char_count

密码至少要包含的特殊字符数。
3. 执行操作

图片

此时再设置简单密码就可以通过了

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
java复制代码set password=password('123456');

开放Linux的对外访问的端口3306

/sbin/iptables -I INPUT -p tcp --dport 3306 -j ACCEPT

/etc/rc.d/init.d/iptables save ---将修改永久保存到防火墙

MySQL字符编码设置

输入命令 vi /etc/my.cnf

\[client\]

default-character-set = utf8

\[mysqld\]

character-set-server = utf8

collation-server = utf8\_general\_ci

默认情况下mysql为安全起见,不支持远程登录mysql,所以需要设置开启 远程登录mysql的权限

登录mysql后输 入如下命令:

grant all privileges on \*.\* to 'root' @'%' identified by '123456';

flush privileges;

这样,使用navicat就可以连接上了

图片

tomcat,mysql,jdk都已安装好,在本地就可以作为自己的服务器进行使用,当然,如果你不想本地安装linux环境,也可以自己买云服务器,根据教程进行web环境的搭建。

满满的干货,写了这么多,给个赞吧,我是不学就难受的卧龙小蛋,欢迎关注我,一起学习,一起成长

本文转载自: 掘金

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

实战 Cloud Nacos 配置加载流程和优先级

发表于 2021-10-12

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

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

Github : 👉 github.com/black-ant

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

一 . 前言

这一篇来看一下 Nacos Client 对配置的请求流程以及相关的配置

二 . 配置类及用法

2.1 基本使用案例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
yml复制代码# 这也是最常见的案例模板
spring:
application:
name: nacos-multi-config
cloud:
nacos:
config:
extension-configs:
# 对应 Nacos DataId
- data-id: nacos-multi-config-A.yaml
group: DEFAULT_RSM
- data-id: nacos-multi-config-B.yaml
group: DEFAULT_RSM
- data-id: nacos-multi-config-C.yaml
group: DEFAULT_RSM
# 文件后缀
file-extension: yaml
server-addr: 127.0.0.1:8848
discovery:
server-addr: 127.0.0.1:8848

2.2 配置类解析

Nacos 对应的配置类为 NacosConfigProperties , 这里来看一下所有的参数:

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
java复制代码// Nacos 地址
private String serverAddr;

// 用户名 及密码
private String username;
private String password;

// 内容编码
private String encode;

// 所属组
private String group = "DEFAULT_GROUP";

// Config DataId 前缀
private String prefix;

// Config 后缀文件名
private String fileExtension = "properties";

// 获取配置超时时间
private int timeout = 3000;

// 最大重连次数
private String maxRetry;

// 获取配置长轮询超时时间
private String configLongPollTimeout;

// 配置获取错误的重试次数
private String configRetryTime;

// 自动获取及注册 Listener 监听 , 会有网络开销
private boolean enableRemoteSyncConfig = false;

// 服务域名,可动态获取服务器地址
private String endpoint;

// 命名空间,不同环境的分离配置
private String namespace;

// 访问键和访问密钥
private String accessKey;
private String secretKey;

// 容器地址
private String contextPath;

// 分片名
private String clusterName;

// naco配置dataId名称
private String name;

/**
* 共享配置集
* spring.cloud.nacos.config.shared-configs[0]=xxx .
*/
private List<Config> sharedConfigs;

/**
* 扩展配置集
* spring.cloud.nacos.config.extension-configs[0]=xxx .
*/
private List<Config> extensionConfigs;

// 是否刷新配置
private boolean refreshEnabled = true;

三 . 配置流程

下面看一下配置的加载和获取流程

3.1 开启配置的入口

配置得开启是基于 PropertySourceBootstrapConfiguration 开启的 ,

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复制代码public void initialize(ConfigurableApplicationContext applicationContext) {
List<PropertySource<?>> composite = new ArrayList<>();
AnnotationAwareOrderComparator.sort(this.propertySourceLocators);
boolean empty = true;
ConfigurableEnvironment environment = applicationContext.getEnvironment();

// 从不同的 locators 获取资源
for (PropertySourceLocator locator : this.propertySourceLocators) {

// 对于 Nacos , 此处会调用 NacosPropertySourceLocator
Collection<PropertySource<?>> source = locator.locateCollection(environment);

if (source == null || source.size() == 0) {
continue;
}
List<PropertySource<?>> sourceList = new ArrayList<>();
for (PropertySource<?> p : source) {

// 分别生成了 BootstrapPropertySource 和 SimpleBootstrapPropertySource
if (p instanceof EnumerablePropertySource) {
EnumerablePropertySource<?> enumerable = (EnumerablePropertySource<?>) p;
sourceList.add(new BootstrapPropertySource<>(enumerable));
}
else {
sourceList.add(new SimpleBootstrapPropertySource(p));
}
}
composite.addAll(sourceList);
empty = false;
}

// 省略资源的处理 , 后文再说
}

3.2 Nacos 加载入口

期间会经过接口类的 PropertySourceLocator 来发起对应

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
java复制代码// C- NacosPropertySourceLocator
public PropertySource<?> locate(Environment env) {

nacosConfigProperties.setEnvironment(env);

// ConfigService 是主要的查询
ConfigService configService = nacosConfigManager.getConfigService();

if (null == configService) {
log.warn("no instance of config service found, can't load config from nacos");
return null;
}

// 超时时间可以通过配置文件配置
long timeout = nacosConfigProperties.getTimeout();

// 为该类的局部变量 , 意味着通用该变量
nacosPropertySourceBuilder = new NacosPropertySourceBuilder(configService,timeout);

// 准备前缀和 name
String name = nacosConfigProperties.getName();
String dataIdPrefix = nacosConfigProperties.getPrefix();
if (StringUtils.isEmpty(dataIdPrefix)) {
dataIdPrefix = name;
}

// 注意 , 默认前缀
if (StringUtils.isEmpty(dataIdPrefix)) {
dataIdPrefix = env.getProperty("spring.application.name");
}

CompositePropertySource composite = new CompositePropertySource(
NACOS_PROPERTY_SOURCE_NAME);

// private List<Config> sharedConfigs 的处理
loadSharedConfiguration(composite);

// private List<Config> extensionConfigs 的处理
loadExtConfiguration(composite);

// 主配置文件获取
loadApplicationConfiguration(composite, dataIdPrefix, nacosConfigProperties, env);
return composite;
}

补充一 : loadSharedConfiguration 进行共享数据的处理

持多个共享 Data Id 的配置,优先级小于extension-configs , 适合于共享配置文件与项目默认配置文件处于相同Group时 (PS:只能在一个 Group 中)

主要流程为获取配置 , 校验准确性 , 调用 loadNacosConfiguration 发起配置的调用

PS: 主要流程看补充二 , 这也是为什么优先级没有 loadExtConfiguration 高的原因

补充二 : loadExtConfiguration 处理配置

主要流程为 nacosConfigProperties.getExtensionConfigs() , 判断是否存在 , 存在会先 checkConfiguration , 再调用 loadNacosConfiguration 进行主流程处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
java复制代码private void loadNacosConfiguration(final CompositePropertySource composite,List<NacosConfigProperties.Config> configs) {

// 此处会循环调用 , 所以后面的实际上会覆盖前面的
for (NacosConfigProperties.Config config : configs) {
// getFileExtension 为文件后缀
loadNacosDataIfPresent(composite, config.getDataId(), config.getGroup(),
NacosDataParserHandler.getInstance().getFileExtension(config.getDataId()),
config.isRefresh());
}
}

// 后续逻辑就是调用 NacosPropertySourceBuilder 进行正在的发起查询和解析

NacosPropertySource build(String dataId, String group, String fileExtension,boolean isRefreshable) {
// Step 1 : 此处查询到对象并且完成解析 -> 3.3
List<PropertySource<?>> propertySources = loadNacosData(dataId, group,fileExtension);

// Step 2 : 将解析的对象封装为 NacosPropertySource
NacosPropertySource nacosPropertySource = new NacosPropertySource(propertySources,
group, dataId, new Date(), isRefreshable);

// Step 3: NacosPropertySourceRepository 中有个 ConcurrentHashMap<String, NacosPropertySource>
NacosPropertySourceRepository.collectNacosPropertySource(nacosPropertySource);
return nacosPropertySource;
}

补充三 : 主配置文件获取

该流程的优先级最高 , 是通过常规方式获取配置的流程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
java复制代码private void loadApplicationConfiguration(
CompositePropertySource compositePropertySource, String dataIdPrefix,
NacosConfigProperties properties, Environment environment) {

// 获取文件后缀和组
String fileExtension = properties.getFileExtension();
String nacosGroup = properties.getGroup();

// 第一次加载 : 默认情况下直接加载一次
loadNacosDataIfPresent(compositePropertySource, dataIdPrefix, nacosGroup,
fileExtension, true);

// 第二次加载 : 加载带有后缀,具有比默认值更高的优先级
loadNacosDataIfPresent(compositePropertySource,
dataIdPrefix + DOT + fileExtension, nacosGroup, fileExtension, true);

// 第三次加载 :用配置文件 Profile 加载,它比后缀具有更高的优先级
// 此处循环所有的 Profiles , 分别进行处理
for (String profile : environment.getActiveProfiles()) {
String dataId = dataIdPrefix + SEP1 + profile + DOT + fileExtension;
loadNacosDataIfPresent(compositePropertySource, dataId, nacosGroup,
fileExtension, true);
}

}

可以看到 , 这里加载了多次 , 同时通过这种方式确定优先级

3.3 Nacos 获取配置主流程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
java复制代码private List<PropertySource<?>> loadNacosData(String dataId, String group,String fileExtension) {
String data = null;

// Step 1 : 获取远程配置信息
data = configService.getConfig(dataId, group, timeout);
if (StringUtils.isEmpty(data)) {
return Collections.emptyList();
}

// Step 2 : 解析远程配置信息
return NacosDataParserHandler.getInstance().parseNacosData(dataId, data,fileExtension);

// 此处省略了异常处理逻辑 , 出现异常不会抛出 ,而是返回空集合
// PS : 这里也导致部分错误不好从日志判断原因
// return Collections.emptyList();
}

PS : 这里不止是个字符串 , 复制到文本里面可以看出就是一个 yaml 格式的数据
image.png

3.3.1 远程配置的获取

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
java复制代码private String getConfigInner(String tenant, String dataId, String group, long timeoutMs) throws NacosException {
group = null2defaultGroup(group);
ParamUtils.checkKeyParam(dataId, group);
ConfigResponse cr = new ConfigResponse();

// 准备查询对象
// {dataId=nacos-multi-config-B.yaml, tenant=, group=DEFAULT_RSM}
cr.setDataId(dataId);
cr.setTenant(tenant);
cr.setGroup(group);

// 优先使用本地配置
String content = LocalConfigInfoProcessor.getFailover(agent.getName(), dataId, group, tenant);
// 如果此处有本地配置 , 则直接返回 , 此处省略

try {
// 发起远程调用
// HttpAgent.httpGet(Constants.CONFIG_CONTROLLER_PATH, null, params, agent.getEncode(), readTimeout)
String[] ct = worker.getServerConfig(dataId, group, tenant, timeoutMs);
cr.setContent(ct[0]);

configFilterChainManager.doFilter(null, cr);
content = cr.getContent();

return content;
} catch (NacosException ioe) {
if (NacosException.NO_RIGHT == ioe.getErrCode()) {
throw ioe;
}

}

// 如果不是 403 , 则会通过快照获取
content = LocalConfigInfoProcessor.getSnapshot(agent.getName(), dataId, group, tenant);
cr.setContent(content);
configFilterChainManager.doFilter(null, cr);
content = cr.getContent();
return content;
}

3.3.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
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
java复制代码// C- NacosDataParserHandler
public List<PropertySource<?>> parseNacosData(String configName, String configValue,
String extension) throws IOException {


if (StringUtils.isEmpty(configValue)) {
return Collections.emptyList();
}

// 文件后缀
if (StringUtils.isEmpty(extension)) {
extension = this.getFileExtension(configName);
}

// 此处的 PropertySourceLoader 主要有四种 :
// - NacosXmlPropertySourceLoader : XML 文件格式
// - PropertiesPropertySourceLoader : Properties 文件格式
// - YamlPropertySourceLoader : YAML 文件格式
// - NacosJsonPropertySourceLoader : JSON 文件格式
for (PropertySourceLoader propertySourceLoader : propertySourceLoaders) {

if (!canLoadFileExtension(propertySourceLoader, extension)) {
// 如果不能加载 , 直接退出
continue;
}
NacosByteArrayResource nacosByteArrayResource;
// 省略转换为Byte逻辑 : new NacosByteArrayResource

nacosByteArrayResource.setFilename(getFileName(configName, extension));

// 核心逻辑 , 此处将 yaml 格式文本转换为了 OriginTrackedMapPropertySource
List<PropertySource<?>> propertySourceList = propertySourceLoader
.load(configName, nacosByteArrayResource);
if (CollectionUtils.isEmpty(propertySourceList)) {
return Collections.emptyList();
}

return propertySourceList.stream().filter(Objects::nonNull)
.map(propertySource -> {
if (propertySource instanceof EnumerablePropertySource) {
// 获得 Name 名称
String[] propertyNames = ((EnumerablePropertySource) propertySource)
.getPropertyNames();
if (propertyNames != null && propertyNames.length > 0) {
Map<String, Object> map = new LinkedHashMap<>();

// 此处是将 OriginTrackedValue 转换为对应类型的值
Arrays.stream(propertyNames).forEach(name -> {
map.put(name, propertySource.getProperty(name));
});

// 最终构建一个 PropertySource 的 List 集合
return new OriginTrackedMapPropertySource(
propertySource.getName(), map, true);
}
}
return propertySource;
}).collect(Collectors.toList());
}
return Collections.emptyList();
}

image.png

image.png

补充 : YamlPropertySourceLoader 流程

1
2
3
4
5
6
7
8
java复制代码public List<PropertySource<?>> load(String name, Resource resource) throws IOException {

// 核心 : 通过 OriginTrackedYamlLoader 进行 Loader 加载 , 此处就不深入了 , 属于工具类的功能
List<Map<String, Object>> loaded = new OriginTrackedYamlLoader(resource).load();

// 省略判空和添加逻辑 -> propertySources.add(new OriginTrackedMapPropertySource
return propertySources;
}

四 . 总结

篇幅有限 , 所以 本地配置的覆盖与集成 以及 配置的顺序加载及优先级 准备放在下一篇文档里面梳理 , 下面分享一张流程图

image.png

补充 : 配置的优先级

  • application 主配置 > extensionConfigs > sharedConfigs
  • extensionConfigs/sharedConfigs 排在后面的数组比前面的优先级高
  • application 主逻辑 profile > 带后缀

本文转载自: 掘金

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

1…494495496…956

开发者博客

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