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

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


  • 首页

  • 归档

  • 搜索

Spring 中 PropertyEditorRegistr

发表于 2021-11-19

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

1.简介

PropertyEditorRegistry : 属性编辑器注册表,顾名思义,主要的作用就是保存 属性编辑器(PropertyEditor),根据需要返回对应的属性编辑器。

PropertyEditor :属性编辑器的接口,它规定了将外部设置值转换为内部JavaBean属性值的转换接口方法

本文主要讲一讲它们两的作用和关系

2.PropertyEditor

2.1 继承关系

图片.png
从上图我们可以发现它的主要的常用的实现类,基本都是 PropertyEditorSupport 下面,我们通常可以继承它,实现自己的自定义属性编辑器,如常见的:String类型属性编辑器,Character类型编辑器….等,都是它的子类。

2.2 主要方法

图片.png

  • void setValue(Object value); :设置更改对象的值,如果是基本类型数据必须要转成包装类,如 int类型数据要转成 Integer
  • Object getValue() :获取属性的值,基本类型数据必须要转成包装类型
  • boolean isPaintable() :确定此属性编辑器是否可绘制。
  • void paintValue(java.awt.Graphics gfx, java.awt.Rectangle box):将值的表示形式绘制到屏幕的给定区域中。
  • String getJavaInitializationString() :返回可用于设置属性的Java代码的片段匹配编辑当前状态。
  • String getAsText() : 以文本的形式获取该属性的值,返回一个String类型的结果
  • void setAsText(String text) throws java.lang.IllegalArgumentException : 通过解析给定字符串来设置属性值,如果解析不了会抛异常。
  • String[] getTags() : 返回表示有效属性值的字符串数组(如boolean属性对应的有效Tag为true和false),以便属性编辑器能以下拉框的方式显示出来。缺省返回null,表示属性没有匹配的字符值有限集合
  • java.awt.Component getCustomEditor() :返回一个组件,可以让人直接编辑当前属性值。如果为空,则可能为空
  • boolean supportsCustomEditor() : 确定该属性是否支持自定义编辑。
  • void addPropertyChangeListener(PropertyChangeListener listener) : 为值的更改添加一个监听器,当值修改后,触发的监听器事件
  • void removePropertyChangeListener(PropertyChangeListener listener) : 移除上面设置的监听器

3.PropertyEditorRegistry

3.1 主要继承关系

这张图是我们后续在讲Spring 容器初始化时,主要用到的,大家可以看到,下面有几个比较重要的实现类,BeanWrapperImpl , TypeConverterSupport 等。
图片.png
下面这张图主要列出了数据绑定相关的继承关系
图片.png

3.2 主要方法

图片.png

  • void registerCustomEditor(Class<?> requiredType, PropertyEditor propertyEditor);
    • 注册一个指定类型的属性编辑器
    • requiredType:指定的属性类型
    • propertyEditor :属性编辑器
  • void registerCustomEditor(@Nullable Class<?> requiredType, @Nullable String propertyPath, PropertyEditor propertyEditor);
    • 为指定类型注册一个自定义的属性编辑器
    • requiredType:指定的属性类型
    • propertyPath:属性的路径(名称或嵌套路径),如果为null给定类型的所有属性注册编辑器
    • propertyEditor :属性编辑器
  • PropertyEditor findCustomEditor(@Nullable Class<?> requiredType, @Nullable String propertyPath)
    • 通过指定的类型和属性,查找自定义属性编辑器
    • requiredType : 指定的类型
    • propertyPath :指定的属性的路径

4.总结

从上面的方法可以很容易的看出 PropertyEditorRegistry 主要就是为了添加和查找 PropertyEditor。在Spring,创建bean时 就会通过 PropertyEditorRegistry 取出对应的 PropertyEditor,给对应的 属性设置或修改它的值。

本文转载自: 掘金

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

记一次EFK采集不到k8s日志数据的解决过程 背景 现象 解

发表于 2021-11-19

趁热记录下,给未来的自己

背景

本地部署了两套 k8s 环境:开发和预生产。为了方便开发同学调试,需要将 k8s 的业务日志通过 EFK 框架输出到 WEB 端查看。

部署架构:

  • elastic search 和 kibana 是部署在 k8s 集群外,
  • filebeat 以 daemonset 的方式部署在 k8s 的 node 节点上。

现象

同一份 filebeat-k8s.yml 文件,在开发集群上可以成功部署且在 Kibana 上能采集到对应的日志。但是在预生产集群上部署成功后,无法在 Kibana 上看到日志。

解决过程

首先,对比了 filebeat 在开发和预生产 node 节点上的日志,发现前者含有 [input.harvester] 和 Collection to backoff established 日志,而后者没有。这说明,后者没有采集到数据,所以没有向 elastic search 发送数据。

定位到了具体问题,那么接下来要解决的是,为什么预生产 node 节点上的 filebeat 无法采集 docker 日志呢?

到这里,我突然想起来,预生产和开发集群最大的区别在于: docker 安装目录不同。

  • 开发集群的 docker 安装在默认位置: /var/lib/docker;
  • 预生产的 docker 安装在了 /home/ops/docker 。

这么做的目的是,将 docker 从较小的系统盘移到了较大的数据盘上,以便放更多的日志。没想到给自己埋下了一个大坑。。。

为了验证猜测,进入预生产的 filebeat 容器, 查看 /var/log/container 目录下的内容,发现全是红色的,表示挂载失败:

用 ls -la 命令查看了下,发现这些文件软链接于 /var/log/pods

于是,又进入 /var/log/pods,ls -la 发现,该目录下的log文件又软链接于 /home/ops/docker 目录

而 /home/ops/docker 目录在容器中并不存在!至此,找到了根本原因。那么可以修改 filebeat-k8s.yml 里 DeamonSet 资源的 volume 和 volumeMounts 如下:

1
2
3
4
5
6
7
8
9
10
yaml复制代码volumeMounts:
- name: varlibdockercontainers
# 容器内的目录
mountPath: /home/ops/docker/containers
readOnly: true
volumes:
- name: varlibdockercontainers
# 宿主机上的目录
hostPath:
path: /home/ops/docker/containers

总结

k8s里 docker 的日志链路关系:

  • 默认docker安装目录:
  • /var/log/container/.log –> /var/log/pods//.log –> /var/lib/docker/containers//\.log**
  • 本文预生产docker安装目录:
  • /var/log/container/.log –> /var/log/pods//.log –> /home/ops/docker/containers//\.log**

Done

本文转载自: 掘金

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

Java中String类的部分方法说明

发表于 2021-11-19

Java中用于处理字符串常量的有三个类:

java.lang.Stirng

java.lang.StringBuilder

java.lang.lang.StringBuilder

本文先介绍String类。

String类表示字符串。Java程序中的所有字符串文字(例如"abc" )都实现为此类的实例。

字符串是不变的; 它们的值在创建后无法更改。 字符串缓冲区支持可变字符串。 因为String对象是不可变的,所以可以共享它们(如果两个字符串的内容相同,则采用同一块内存地址)。

举例:

1
2
3
4
5
6
7
ini复制代码public class Test01 {
  public static void main(String[] args) {
      String text1 = "123465";
      String text2 = "123465";
      System.out.println(text1 == text2);
  }
}

结果为:true

==比较的是内存地址。

如果是new出来的 ,那一定是不同的内存地址。

举例:

1
2
3
4
5
6
7
typescript复制代码public class Test01 {
  public static void main(String[] args) {
      String text1 = "123465";
      String text2 = new String("123456");
      System.out.println(text1 == text2);
  }
}

结果为:false

  1. charAt(int index)

返回指定索引处的 char值。

1
2
3
4
5
6
typescript复制代码public class Test01 {
  public static void main(String[] args) {
      String text1 = "123465";
      System.out.println(text1.charAt(2));
  }
}

结果为:3

  1. codePointAt(int index)

返回指定索引处的字符(Unicode代码点)。

1
2
3
4
5
6
typescript复制代码public class Test01 {
  public static void main(String[] args) {
      String text1 = "abcdefg";
      System.out.println(text1.codePointAt(2));
  }
}

结果为:99

  1. compareTo(String anotherString)

简单来说,String类的compareTo()方法是用来比较两个字符串的字典顺序。   用字符串1跟字符串2作比较,如果字符串1的字典顺序在字符串2前面,则返回一个负数。若在后面,则返回一个正数。若两个字符串的字典顺序相同,则返回0。   这里的字典顺序指的是ASCII码表中的字符顺序。ASCII表中每个字符都有对应的下标,从0开始升序排列,共128个字符。

1
2
3
4
5
6
7
8
9
10
typescript复制代码public class Test01 {
  public static void main(String[] args) {
      String text1 = "abcdefg";
      String text2 = "AaBbCcdD";
      //字符串1的第一个字符跟字符串2的第一个字符不相等,
      // 则两个字符串都按照第一个字符的ASCII码顺序进行比较,
      // 其他字符都不用看,并返回一个整型。
      System.out.println(text1.compareTo(text2));
  }
}

结果为:32

a的ASCII码为97,A的ASCII码为65,两者相差32.

  1. equals(Object anObject)

将此字符串与指定的对象进行比较,当且仅当参数不是null且是String对象表示与此对象相同的字符序列时,结果为true。

1
2
3
4
5
6
7
typescript复制代码public class Test01 {
  public static void main(String[] args) {
      String text1 = "abcdefg";
      String text2 = "AaBbCcdD";
      System.out.println(text1.equals(text2));
  }
}

结果为:false

  1. getBytes(Object anObject)

使用给定的charset将此String编码为字节序列,将结果存储到新的字节数组中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
arduino复制代码public class Test01 {
  public static void main(String[] args) {
      String a = "11 ";
      String b = "好";
      byte[] arr;
      arr = a.getBytes();
      for (byte x : arr){
          System.out.print("x= " + x);
      }
      System.out.println();
      for (byte y : arr){
          System.out.print("y= " + y);
      }
  }
}

结果为:x= 49x= 49x= 32 y= -28y= -67y= -96

1的ASCLL码为49,空格是32 。

这里声明了一个byte类型的数组array用来接受getBytes()方法返回类的字节数组。 发现 字符串 “好”转换成字节之后 数组array里面有两个元素 ,一个 -28,一个 -67,这是因为,字符“你”是一个中文字符,占用了两个字节,当将其转换为byte时高8位转换成一个字节显示为-28 , 低8位转换成一个字节显示为 -67 。

  1. indexOf(int ch)

返回指定字符第一次出现的字符串中的索引。

1
2
3
4
5
6
typescript复制代码public class Test01 {
  public static void main(String[] args) {
      String a = "abcdef ";
      System.out.println(a.indexOf("d"));
  }
}

结果为:3

  1. repeat(int count)

返回一个字符串,其值为此字符串的串联重复 count次。

1
2
3
4
5
6
typescript复制代码public class Test01 {
  public static void main(String[] args) {
      String a = "abcdefabddffe ";
      System.out.println(a.repeat(2));
  }
}

结果为:abcdefabddffe abcdefabddffe

  1. replace(char oldChar,char newChar)

返回从替换所有出现的导致一个字符串 oldChar在此字符串 newChar 。

1
2
3
4
5
6
typescript复制代码public class Test01 {
  public static void main(String[] args) {
      String a = "abcdefabddffe ";
      System.out.println(a.replace("a","1"));
  }
}

结果为:1bcdef1bddffe

  1. split(String regex)

将此字符串拆分为给定 regular expression的匹配 项 。

1
2
3
4
5
6
7
8
9
ini复制代码public class Test01 {
  public static void main(String[] args) {
      String a = "ab cde fab dd ffe ";
      String[] arr = a.split(" ");
      for (int i = 0; i < arr.length; i++) {
          System.out.println(arr[i]);
      }
  }
}

结果为:

ab cde fab dd ffe

  1. subSequence(int beginIndex.int endIndex)

返回作为此序列的子序列的字符序列。

1
2
3
4
5
6
typescript复制代码public class Test01 {
public static void main(String[] args) {
String a = "ab cde fab dd ffe ";
System.out.println(a.subSequence(0,5));
}
}

结果为:ab cd

12.toLowerCase( )

使用默认语言环境的规则将此 String所有字符转换为小写。

1
2
3
4
5
6
typescript复制代码public class Test01 {
public static void main(String[] args) {
String a = "ABCdefG ";
System.out.println(a.toLowerCase());
}
}

结果为:abcdefg

13.trim( )

返回一个字符串,其值为此字符串,删除了所有前导和尾随空格,其中space被定义为其代码点小于或等于 'U+0020' (空格字符)的任何字符。

1
2
3
4
5
6
typescript复制代码public class Test01 {
public static void main(String[] args) {
String a = " avdfds ";
System.out.println(a.trim());
}
}

结果为:avdfds

14.valueOf(int i )

image-20211119131548590

1
2
3
4
5
6
arduino复制代码public class Test01 {
public static void main(String[] args) {
byte b = 32;
System.out.println(String.valueOf(b));
}
}

结果为:10

这个10是字符类型的

本文转载自: 掘金

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

ThreadPoolExecutor学习笔记(二)

发表于 2021-11-19

书接上回,在addWorker 方法中,添加成功后会启动 Worker 中的线程

1
2
3
4
5
6
7
ini复制代码w = new Worker(firstTask);
final Thread t = w.thread;
...
if (workerAdded) {
t.start();//启动worker中的线程
workerStarted = true;
}

接下来看看run方法做了啥

1
2
3
csharp复制代码public void run() {
runWorker(this);
}

runWorker

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
ini复制代码final void runWorker(Worker w) {
Thread wt = Thread.currentThread();
Runnable task = w.firstTask;
w.firstTask = null;
w.unlock(); // allow interrupts
boolean completedAbruptly = true;
try {//捕获getTask的异常
//如果第一个任务不为空,跑第一个任务
//如果第一个任务为空,循环从任务队列中获取任务
while (task != null || (task = getTask()) != null) {
w.lock();
//如果线程池停止了,确保线程被中断
//如果没停止,确保线程不被中断
if ((runStateAtLeast(ctl.get(), STOP) ||
(Thread.interrupted() &&
runStateAtLeast(ctl.get(), STOP))) &&
!wt.isInterrupted())
wt.interrupt();
try {//捕获业务的异常
beforeExecute(wt, task);
Throwable thrown = null;
try {
task.run();//执行任务
} catch (RuntimeException x) {
thrown = x; throw x;
} catch (Error x) {
thrown = x; throw x;
} catch (Throwable x) {
thrown = x; throw new Error(x);
} finally {
afterExecute(task, thrown);
}
} finally {
task = null;
w.completedTasks++;//完成的任务计数++
w.unlock();
}
}
completedAbruptly = false;
} finally {
processWorkerExit(w, completedAbruptly);//completedAbruptly 如果是用户业务导致的异常,为true
}
}

启动线程的方法还是挺简单的。

processWorkerExit

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
scss复制代码private void processWorkerExit(Worker w, boolean completedAbruptly) {
if (completedAbruptly) // If abrupt, then workerCount wasn't adjusted
decrementWorkerCount();//工作线程数减一

final ReentrantLock mainLock = this.mainLock;
mainLock.lock();
try {
completedTaskCount += w.completedTasks;
workers.remove(w);//从workers中移除
} finally {
mainLock.unlock();
}

tryTerminate();

int c = ctl.get();
if (runStateLessThan(c, STOP)) {
if (!completedAbruptly) {
//处理worker退出的时候,如果任务队列还有任务,保证至少有一个线程在执行任务
int min = allowCoreThreadTimeOut ? 0 : corePoolSize;
if (min == 0 && ! workQueue.isEmpty())
min = 1;
if (workerCountOf(c) >= min)
return; // replacement not needed
}
addWorker(null, false);//如果是用户线程导致的异常,补充一个worker
}
}

shutdown

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
scss复制代码public void shutdown() {
final ReentrantLock mainLock = this.mainLock;
mainLock.lock();
try {
checkShutdownAccess();
//CAS改变线程池状态为SHUTDOWN
advanceRunState(SHUTDOWN);
//中断所有空闲线程
interruptIdleWorkers();
onShutdown(); // hook for ScheduledThreadPoolExecutor
} finally {
mainLock.unlock();
}
tryTerminate();
}

interruptIdleWorkers中断空闲线程

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复制代码private void interruptIdleWorkers(boolean onlyOne) {
final ReentrantLock mainLock = this.mainLock;
mainLock.lock();
try {
for (Worker w : workers) {
Thread t = w.thread;
//w.tryLock()能拿到锁说明是空闲线程,在runWorker里面有 w.lock(); 和w.unlock();包裹的运行任务的代码,空闲线程阻塞在runWorker的task = getTask()这


if (!t.isInterrupted() && w.tryLock()) {
try {
t.interrupt();
} catch (SecurityException ignore) {
} finally {
w.unlock();
}
}
if (onlyOne)
break;
}
} finally {
mainLock.unlock();
}
}

tryTerminate

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
scss复制代码final void tryTerminate() {
for (;;) {
int c = ctl.get();
//当前线程池状态判断
if (isRunning(c) ||
runStateAtLeast(c, TIDYING) ||
(runStateOf(c) == SHUTDOWN && ! workQueue.isEmpty()))
return;
//当前worker大于零,停止一个空闲线程,保证有一个线程能执行接下来的线程池的状态转换的代码
if (workerCountOf(c) != 0) { // Eligible to terminate
interruptIdleWorkers(ONLY_ONE);
return;
}
// 执行线程池的状态转换
final ReentrantLock mainLock = this.mainLock;
mainLock.lock();
try {
//把线程池状态转换为TIDYING状态
if (ctl.compareAndSet(c, ctlOf(TIDYING, 0))) {
try {
//执行terminated钩子函数
terminated();
} finally {
//把线程池状态转换为 TERMINATED 状态
ctl.set(ctlOf(TERMINATED, 0));
termination.signalAll();
}
return;
}
} finally {
mainLock.unlock();
}
// else retry on failed CAS
}
}

shutdownNow

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
scss复制代码public List<Runnable> shutdownNow() {
List<Runnable> tasks;
final ReentrantLock mainLock = this.mainLock;
mainLock.lock();
try {
checkShutdownAccess();
//CAS改变线程池状态为 STOP
advanceRunState(STOP);
//中断所有线程
interruptWorkers();
//获取所有未执行的任务
tasks = drainQueue();
} finally {
mainLock.unlock();
}
tryTerminate();
//返回所有未执行的任务
return tasks;
}

shutdownNow关闭线程池流程:

  1. 把线程池状态改为 STOP ,相当于把所有从 runWorker 进来的口子给拦住了,因为在runWorker中有如下判断:
1
2
3
4
5
less复制代码if ((runStateAtLeast(ctl.get(), STOP) ||
(Thread.interrupted() &&
runStateAtLeast(ctl.get(), STOP))) &&
!wt.isInterrupted())
wt.interrupt();

如果有线程在关闭线程池的时候,还在task.run();中执行业务代码,当线程被中断后,再次获取任务的时候会抛出异常,举个例子:
被中断的线程再次运行到queue.take();的时候直接抛出异常

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
scss复制代码public static void main(String[] args) {
BlockingQueue queue = new ArrayBlockingQueue(1);
Thread thread = new Thread(() -> {
long count = 0;
for (;;){
if (count < 160) {
count ++;
System.out.println(count);
}
break;
}
try {
queue.take();
} catch (InterruptedException e) {
e.printStackTrace();
}
});
thread.start();
thread.interrupt();
System.out.println(thread.isInterrupted());

}
返回结果:
true
1
java.lang.InterruptedException
at java.util.concurrent.locks.AbstractQueuedSynchronizer.acquireInterruptibly(AbstractQueuedSynchronizer.java:1220)
at java.util.concurrent.locks.ReentrantLock.lockInterruptibly(ReentrantLock.java:335)
at java.util.concurrent.ArrayBlockingQueue.take(ArrayBlockingQueue.java:400)
at com.networkbench.tingyun.demo.thread.Main.lambda$main$0(Main.java:19)
at com.networkbench.tingyun.demo.thread.Main$$Lambda$1/1936628443.run(Unknown Source)
at java.lang.Thread.run(Thread.java:745)
  1. 中断所有已经启动的现场
1
scss复制代码interruptWorkers();
  1. 获取所有未执行的任务
1
ini复制代码tasks = drainQueue();
  1. 更改线程池状态
1
scss复制代码tryTerminate()

本文转载自: 掘金

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

LeetCode-113-路径总和 II

发表于 2021-11-19

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

路径总和 II

题目描述:给你二叉树的根节点 root 和一个整数目标和 targetSum ,找出所有 从根节点到叶子节点 路径总和等于给定目标和的路径。

叶子节点 是指没有子节点的节点。

示例说明请见LeetCode官网。

来源:力扣(LeetCode)

链接:leetcode-cn.com/problems/pa…

著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。

解法一:层序遍历

用Queue<Pair<TreeNode, Pair<Integer, List<Integer>>>>这种结构来记录当前节点的路径以及路径和,其中:

  • 外层的Pair的key为当前节点;
  • 内层的Pair的key为当前路径的总和,value为当前路径的记录,从根节点到当前节点。

然后使用层序遍历的方式使用队列遍历二叉树的节点,当判断某节点的左右节点为空,即为叶子节点,然后判断当前的路径值是否与targetSum相等,如果相等,将相应的路径添加到结果集中。

最后,返回结果集。

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
java复制代码import com.kaesar.leetcode.TreeNode;
import javafx.util.Pair;

import java.util.*;

public class LeetCode_113 {
// 结果集
private static List<List<Integer>> result = new ArrayList<>();

public static List<List<Integer>> pathSum(TreeNode root, int targetSum) {
if (root == null) {
return new ArrayList<>();
}
/**
* 外层的Pair的key为当前节点
* 内层的Pair的key为当前路径的总和,value为当前路径的记录,从根节点到当前节点
*/
Queue<Pair<TreeNode, Pair<Integer, List<Integer>>>> nodes = new LinkedList<>();
List<Integer> path = new ArrayList<>();
path.add(root.val);
// 初始化,将根节点添加到队列中
nodes.add(new Pair<>(root, new Pair<>(root.val, path)));

// while (!nodes.isEmpty()) {
Pair<TreeNode, Pair<Integer, List<Integer>>> cur = nodes.poll();
TreeNode curNode = cur.getKey();
// 判断当前节点的左右节点为空,即为叶子节点,然后判断当前的路径值是否与targetSum相等
if (curNode.left == null && curNode.right == null) {
if (cur.getValue().getKey() == targetSum) {
result.add(cur.getValue().getValue());
}
continue;
}
// 如果当前节点不是叶子节点,继续往下遍历
if (curNode.left != null) {
List<Integer> leftPath = new ArrayList<>(Arrays.asList(new Integer[cur.getValue().getValue().size()]));
Collections.copy(leftPath, cur.getValue().getValue());
leftPath.add(curNode.left.val);
nodes.add(new Pair<>(curNode.left, new Pair<>(cur.getValue().getKey() + curNode.left.val, leftPath)));
}
if (curNode.right != null) {
List<Integer> rightPath = new ArrayList<>(Arrays.asList(new Integer[cur.getValue().getValue().size()]));
Collections.copy(rightPath, cur.getValue().getValue());
rightPath.add(curNode.right.val);
nodes.add(new Pair<>(curNode.right, new Pair<>(cur.getValue().getKey() + curNode.right.val, rightPath)));
}
}

return result;
}

public static void main(String[] args) {
TreeNode root = new TreeNode(1);
root.left = new TreeNode(2);
root.right = new TreeNode(3);
root.right.left = new TreeNode(4);
root.right.right = new TreeNode(5);

for (List<Integer> integers : pathSum(root, 8)) {
for (Integer integer : integers) {
System.out.print(integer + " ");
}
System.out.println();
}
}
}

【每日寄语】 命运,不过是失败者无聊的自慰,不过是懦怯者的解嘲。人们的前途只能靠自己的意志、自己的努力来决定。

本文转载自: 掘金

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

基金列表获取之Java"伪"爬虫 1 文章的由来 2数据

发表于 2021-11-19
  1. 文章的由来

认识一哥们,颜值与才华兼具,奈何选择靠才华体验生活。算了不说他了,免得自我贬低 ~ . ~

1
2
复制代码      稠密的眉毛哗变地稍稍向上扬起,长而微卷的睫毛下,有着一双像朝露一样清亮的眼睛,英挺的鼻梁,像玫瑰花瓣一样粉嫩的嘴唇,还有白净的皮肤。
      冰冷孤傲的眼睛好像没有焦距,深黯的眼底布满了舒适,乌黑的头发,散在耳边,耳钻发出幽蓝的光线。俊美的不能不令人暗暗赞叹,他的身边围绕着一股冰冷的气味。

作为一个 “年轻”人,很多的朋友都应该对基金、股票有所涉猎,别说话,说话的都是亏损的多,这哥们除外哈!

本篇文章就是比葫芦画瓢,不喜勿喷,仅供学习娱乐使用。

2.数据来源

大佬已经给我们测过小河的深度了,不深 。。。 刚好比你我的身高高一截,所以 … 切勿随意下河游泳,来吧,乘坐这个小木舟也是可以过河的。

小木舟

往下瞅,就这个优秀的请求,告诉了你赚钱的秘密,是不是希望自己的基金/股票列表也是一样大红色呢。

image-20211118202154412

1
2
3
4
ini复制代码 #原始链接: 提供想要的数据信息
 http://fund.eastmoney.com/Data/Fund_JJJZ_Data.aspx?lx=1&sort=zdf,desc&page=2,200&onlySale=0
 # lx分明就是类型的简写。sort 是对某些字段排序可以忽略。分页的话2,200就是第二页,每页200条,onlySale就是可以卖出的条件。
 http://fund.eastmoney.com/Data/Fund_JJJZ_Data.aspx?lx=1&sort=zdf,desc&page=2,100&onlySale=0

别的不说,就为这个链接,你们得去给参考文章点个赞 !!!

3.数据抓取

接下来才是头疼的问题,你想要的都有了,那么我想要地麻烦满足一下吧。

对于一名“脸滚”键盘的你来说或许不是那么的困难,对吧 ( 哈哈哈 )。只需一个 Http 请求即可获取我们想要的数据。关于Java相关的Http请求方式种类有很多,笔者使用的是基于第三方开源工具类 Hutool 的Http请求。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
ini复制代码 // 1. 繁琐的请求方式       
 HttpConnection httpConnection = HttpConnection.create(url, null);
 InputStream inputStream = httpConnection.getInputStream();
 // 读取数据信息
 InputStreamReader inputStreamReader = new InputStreamReader(inputStream,"UTF-8");
 BufferedReader bufferedReader = new BufferedReader(inputStreamReader);
 String temp;
 while ((temp = bufferedReader.readLine()) != null){
     // 打印读取到的信息
     System.out.println(temp);
 }
 ​
 // 2. 便捷的请求方式
    HttpResponse httpResponse =  HttpRequest.get(url).timeout(300 * 1000)
        .setConnectionTimeout(200 * 1000).execute();
    System.out.println(httpResponse.body());

获取如下数据信息截图如下:

2.jpg

但是上面的数据不是我们想要的怎么办呢,这时就要瞅瞅小木舟上有其他什么可用的工具,比如Json格式解析,具体实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
ini复制代码 // 使用上面便捷的方法对获取的结果进行解析
  // 根据打印的结果信息进行分析,需要将结果转换为标准的json格式
  JSONObject jsonObject = new JSONObject(httpResponse.body().replace("var db=",""));
  // 获取基金详情列表Json数组
  JSONArray datas = jsonObject.getJSONArray("datas");
  List<Fund> funds = new ArrayList<>();
  for (int i = 0;  i < datas.length(); i++) {
     JSONArray jsonArray = datas.getJSONArray(i);
     Fund fund = new Fund();
     fund.setCode(jsonArray.getString(0));
     fund.setName(jsonArray.getString(1));
     fund.setValue(jsonArray.getString(3));
     funds.add(fund);
  }

通过上面的简单操作,小木舟就可以在河中畅快的左右摇摆,可以浏览各页的基金详情列表。

3.jpg

后续可以将获取的数据自行存放数据库中,根据自己的需要进行相关的展示,也可以深入的分析个人收藏的基金动态变化信息。

基金代码 基金名称 最新净值
005477 长安鑫禧灵活配置混合A 0.8040
005478 长安鑫禧灵活配置混合C 0.7982
005343 长安裕盛灵活配置混合A 1.1877
1
复制代码 万事开头难,但是现如今如同站在巨人的肩膀上摘苹果,相对轻松些,沿着优秀的方向学习、钻研,跟随洪流前进,小木舟不会停止不前,哪怕我们没有了船桨,也会在洪流中勇往直前。

【参考文章】 juejin.cn/post/703083…

本文转载自: 掘金

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

【弄nèng - 化繁为简】Transactional同一

发表于 2021-11-19

@Transactional,在一个事务中更新数据,在查询能查询到新数据

同一个类中

在一个事务中更新之后再查询能查询到最新的数据,毋庸置疑。

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
java复制代码    @Autowired
private TestMapper testMapper;

@Transactional
public void testTransactional() {
System.out.println("1.====:" + testMapper.selectById(1).toString());
updateTestById("司马缸5");
System.out.println("2.====:" + testMapper.selectById(1).toString());
}

public void updateTestById(String name) {
TestEntity entity = new TestEntity();
entity.setId(1);
entity.setName(name);
testMapper.updateById(entity);
}

执行testTransactional()

输出

在这里插入图片描述

不同的类中

A更新,B查询,也能查询到新数据,因为B加入到A的事务中了。此时如果B中出现异常,AB中的操作都会回滚

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
java复制代码@Service
public class TestServiceImpl extends ServiceImpl<TestMapper, TestEntity> implements ITestService {

private static Map<String, String> map = Maps.newHashMap();
@Autowired
private TestMapper testMapper;

@Autowired
private TestServiceImpl2 testServiceImpl2;

@Transactional
public void testTransactional() {
System.out.println("1.====:" + testMapper.selectById(2));

updateTestById("司马缸2");

System.out.println("2.====:" + testMapper.selectById(2));

System.out.println("3.====:" + testServiceImpl2.get());
}

public void updateTestById(String name) {
TestEntity entity = new TestEntity();
entity.setId(1);
entity.setName(name);
testMapper.updateById(entity);
}
}



@Service
public class TestServiceImpl2 extends ServiceImpl<TestMapper, TestEntity> implements ITestService {

@Autowired
private TestMapper testMapper;

// @Transactional(propagation = Propagation.REQUIRES_NEW)
public TestEntity get() {
TestEntity entity = testMapper.selectById(2);
return entity;
}
}

执行testTransactional()

输出

image.png

本文转载自: 掘金

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

Python实现天气查询功能

发表于 2021-11-19

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

一、介绍

我们先来看一下实现的程序有什么功能:
在这里插入图片描述
功能也是非常简单的,输入城市,显示当前城市、当前日期时间、温度和天气。

API使用的是国家气象局的接口,完全免费的:
t.weather.sojson.com/api/weather…
这个是以北京为例的接口,其中最后的数字101010100就是北京的城市代码。查询其它城市只需要把城市代码修改成其它城市的就可以了。

因为这个API的访问只能通过城市代码,在这方面还是有点麻烦的,不过我把城市代码整理出了一个json文件,稍后会为大家讲解这个过程。

二、返回数据

这里以南昌为例,因为返回的数据比较多,就不完整的列举了。请求成功时,大致数据如下:
在这里插入图片描述
里面还有很多没有拉开的地方,然后我们尝试访问一个错误的城市代码:
t.weather.sojson.com/api/weather…
其中1是非法的城市代码,我们看一下数据:

1
2
3
4
python复制代码{
"message": "Request resource not found.",
"status": 404
}

两者都有一个status,也就是状态码。当状态码为200时,说明请求正常完成。404就是我们经常看见的,意思就是没找到。

二、代码讲解

代码是非常简单的,主要就是使用了两个模块:requests、json。一个网络请求,一个json解析。

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
python复制代码import requests, json

#api地址
url = 'http://t.weather.sojson.com/api/weather/city/'

#输入城市中文
city = input("请输入你要查询的城市:")

#读取json文件
f = open('city.json', 'rb')

#使用json模块的load方法加载json数据,返回一个字典
cities = json.load(f)

#通过城市的中文获取城市代码
city = cities.get(city)

#网络请求,传入请求api+城市代码
response = requests.get(url + city)

#将数据以json形式返回,这个d就是返回的json数据
d = response.json()

#当返回状态码为200,输出天气状况
if(d['status'] == 200):
print("城市:", d["cityInfo"]["parent"], d["cityInfo"]["city"])
print("时间:", d["time"], d["data"]["forecast"][0]["week"])
print("温度:", d["data"]["forecast"][0]["high"], d["data"]["forecast"][0]["low"])
print("天气:", d["data"]["forecast"][0]["type"])

代码方面没有特别难的地方,就是在输出的时候需要自己对json数据有一些了解,然后根据自己需要的内容,输出即可。自己可以尝试请求,然后用HiJson格式化一下,或者也有许多在线格式化的网址,然后分析自己需要的数据。

三、将城市代码转为json数据(Excel技巧)

接下来讲讲我是如下和城市代码斗争的,下面这段大家可以不用看。代码中使用到的city.json文件我会在文末上传。

因为我没有找到获取城市代码的接口,只找到了下面这段文字:

1
python复制代码北京:101010100朝阳:101010300顺义:101010400怀柔:101010500通州:101010600昌平:101010700延庆:101010800丰台:101010900石景山:101011000大兴:101011100房山:101011200密云:101011300门头沟:101011400平谷:101011500八达岭:101011600佛爷顶:101011700汤河口:101011800密云上甸子:101011900斋堂:101012000霞云岭:101012100北京城区:101012200海淀:101010200天津:101030100宝坻:101030300东丽:101030400西青:101030500北辰:101030600蓟县:101031400汉沽:101030800静海:101030900津南:101031000塘沽:101031100大港:101031200武清:101030200宁河:101030700上海:101020100宝山:101020300嘉定:101020500南汇:101020600浦东:101021300青浦:101020800松江:101020900奉贤:101021000崇明:101021100徐家汇:101021200闵行:101020200金山:101020700石家庄:101090101张家口:101090301承德:101090402唐山:101090501秦皇岛:101091101沧州:101090701衡水:101090801邢台:101090901邯郸:101091001保定:101090201廊坊:101090601郑州:101180101新乡:101180301许昌:101180401平顶山:101180501信阳:101180601南阳:101180701开封:101180801洛阳:101180901商丘:101181001焦作:101181101鹤壁:101181201濮阳:101181301周口:101181401漯河:101181501驻马店:101181601三门峡:101181701济源:101181801安阳:101180201合肥:101220101芜湖:101220301淮南:101220401马鞍山:101220501安庆:101220601宿州:101220701阜阳:101220801亳州:101220901黄山:101221001滁州:101221101淮北:101221201铜陵:101221301宣城:101221401六安:101221501巢湖:101221601池州:101221701蚌埠:101220201杭州:101210101舟山:101211101湖州:101210201嘉兴:101210301金华:101210901绍兴:101210501台州:101210601温州:101210701丽水:101210801衢州:101211001宁波:101210401重庆:101040100合川:101040300南川:101040400江津:101040500万盛:101040600渝北:101040700北碚:101040800巴南:101040900长寿:101041000黔江:101041100万州天城:101041200万州龙宝:101041300涪陵:101041400开县:101041500城口:101041600云阳:101041700巫溪:101041800奉节:101041900巫山:101042000潼南:101042100垫江:101042200梁平:101042300忠县:101042400石柱:101042500大足:101042600荣昌:101042700铜梁:101042800璧山:101042900丰都:101043000武隆:101043100彭水:101043200綦江:101043300酉阳:101043400秀山:101043600沙坪坝:101043700永川:101040200福州:101230101泉州:101230501漳州:101230601龙岩:101230701晋江:101230509南平:101230901厦门:101230201宁德:101230301莆田:101230401三明:101230801兰州:101160101平凉:101160301庆阳:101160401武威:101160501金昌:101160601嘉峪关:101161401酒泉:101160801天水:101160901武都:101161001临夏:101161101合作:101161201白银:101161301定西:101160201张掖:101160701广州:101280101惠州:101280301梅州:101280401汕头:101280501深圳:101280601珠海:101280701佛山:101280800肇庆:101280901湛江:101281001江门:101281101河源:101281201清远:101281301云浮:101281401潮州:101281501东莞:101281601中山:101281701阳江:101281801揭阳:101281901茂名:101282001汕尾:101282101韶关:101280201南宁:101300101柳州:101300301来宾:101300401桂林:101300501梧州:101300601防城港:101301401贵港:101300801玉林:101300901百色:101301001钦州:101301101河池:101301201北海:101301301崇左:101300201贺州:101300701贵阳:101260101安顺:101260301都匀:101260401兴义:101260906铜仁:101260601毕节:101260701六盘水:101260801遵义:101260201凯里:101260501昆明:101290101红河:101290301文山:101290601玉溪:101290701楚雄:101290801普洱:101290901昭通:101291001临沧:101291101怒江:101291201香格里拉:101291301丽江:101291401德宏:101291501景洪:101291601大理:101290201曲靖:101290401保山:101290501呼和浩特:101080101乌海:101080301集宁:101080401通辽:101080501阿拉善左旗:101081201鄂尔多斯:101080701临河:101080801锡林浩特:101080901呼伦贝尔:101081000乌兰浩特:101081101包头:101080201赤峰:101080601南昌:101240101上饶:101240301抚州:101240401宜春:101240501鹰潭:101241101赣州:101240701景德镇:101240801萍乡:101240901新余:101241001九江:101240201吉安:101240601武汉:101200101黄冈:101200501荆州:101200801宜昌:101200901恩施:101201001十堰:101201101神农架:101201201随州:101201301荆门:101201401天门:101201501仙桃:101201601潜江:101201701襄樊:101200201鄂州:101200301孝感:101200401黄石:101200601咸宁:101200701成都:101270101自贡:101270301绵阳:101270401南充:101270501达州:101270601遂宁:101270701广安:101270801巴中:101270901泸州:101271001宜宾:101271101内江:101271201资阳:101271301乐山:101271401眉山:101271501凉山:101271601雅安:101271701甘孜:101271801阿坝:101271901德阳:101272001广元:101272101攀枝花:101270201银川:101170101中卫:101170501固原:101170401石嘴山:101170201吴忠:101170301西宁:101150101黄南:101150301海北:101150801果洛:101150501玉树:101150601海西:101150701海东:101150201海南:101150401济南:101120101潍坊:101120601临沂:101120901菏泽:101121001滨州:101121101东营:101121201威海:101121301枣庄:101121401日照:101121501莱芜:101121601聊城:101121701青岛:101120201淄博:101120301德州:101120401烟台:101120501济宁:101120701泰安:101120801西安:101110101延安:101110300榆林:101110401铜川:101111001商洛:101110601安康:101110701汉中:101110801宝鸡:101110901咸阳:101110200渭南:101110501太原:101100101临汾:101100701运城:101100801朔州:101100901忻州:101101001长治:101100501大同:101100201阳泉:101100301晋中:101100401晋城:101100601吕梁:101101100乌鲁木齐:101130101石河子:101130301昌吉:101130401吐鲁番:101130501库尔勒:101130601阿拉尔:101130701阿克苏:101130801喀什:101130901伊宁:101131001塔城:101131101哈密:101131201和田:101131301阿勒泰:101131401阿图什:101131501博乐:101131601克拉玛依:101130201拉萨:101140101山南:101140301阿里:101140701昌都:101140501那曲:101140601日喀则:101140201林芝:101140401台北县:101340101高雄:101340201台中:101340401海口:101310101三亚:101310201东方:101310202临高:101310203澄迈:101310204儋州:101310205昌江:101310206白沙:101310207琼中:101310208定安:101310209屯昌:101310210琼海:101310211文昌:101310212保亭:101310214万宁:101310215陵水:101310216西沙:101310217南沙岛:101310220乐东:101310221五指山:101310222琼山:101310102长沙:101250101株洲:101250301衡阳:101250401郴州:101250501常德:101250601益阳:101250700娄底:101250801邵阳:101250901岳阳:101251001张家界:101251101怀化:101251201黔阳:101251301永州:101251401吉首:101251501湘潭:101250201南京:101190101镇江:101190301苏州:101190401南通:101190501扬州:101190601宿迁:101191301徐州:101190801淮安:101190901连云港:101191001常州:101191101泰州:101191201无锡:101190201盐城:101190701哈尔滨:101050101牡丹江:101050301佳木斯:101050401绥化:101050501黑河:101050601双鸭山:101051301伊春:101050801大庆:101050901七台河:101051002鸡西:101051101鹤岗:101051201齐齐哈尔:101050201大兴安岭:101050701长春:101060101延吉:101060301四平:101060401白山:101060901白城:101060601辽源:101060701松原:101060801吉林:101060201通化:101060501沈阳:101070101鞍山:101070301抚顺:101070401本溪:101070501丹东:101070601葫芦岛:101071401营口:101070801阜新:101070901辽阳:101071001铁岭:101071101朝阳:101071201盘锦:101071301大连:101070201锦州:101070701

先将这段文字复制下来,然后打开Excel,选中一个单元,粘贴。就会有如下效果:
在这里插入图片描述
然后我们点击北京,拖动横向滚动条,按住shift,点击最后一个数据:
在这里插入图片描述
这样就把整个数据全选了。然后复制,点击北京下面一个单元格。右击->选择性复制->勾选转置:
在这里插入图片描述
转置后效果如下,在这里插入图片描述
但是这样有一个明显的问题,北京对应的是101010100。而上面是朝阳和101010100在一起,而且在同一单元格。我们先把数据拆成两单元格。在“101010100朝阳”单元格,选中“101010100”。复制到右边(注意要先将格式设置为文本):单元格1,再选择“朝阳”,复制到下一个单元格:单元格2,然后分别点击单元格1、单元格2,按ctrl+E,就有如下效果:
在这里插入图片描述

数据都被分开了,然后我们把多余数据删除。我们选中左边列的数据(点开头,按shift,点结尾),剪切,然后点击上一个单元格,复制:
在这里插入图片描述
然后就对齐了,现在我们再转置回去。点击左上角、按住shift、点击右下角(有数据的区域),然后复制->点击一个空白单元格->选择性粘贴->转置:在这里插入图片描述
我把两行对换位置,然后转置的。转置之后,我们将有效数据(横向的数据)复制。然后打开浏览器:
www.bejson.com/json/col2js…
使用这个网址,将内容复制进去,点转换就好了。效果如下,我们只需要将json文本保存一个文件就好了。在这里插入图片描述
虽然API的请求确实是非常方便,但是把这个城市代码确实麻烦。

代码和文件还用HiJson我都上传了百度云:
链接:pan.baidu.com/s/1_sGBFm0N…
提取码:o5ay

前段时间,有位朋友在评论中提供了一个城市代码的接口。在使用过程中还是遇到了一些问题,然后我又找了一下,发现了另一个接口:
wthrcdn.etouch.cn/weather_min…
这个接口不需要传城市代码,直接传入城市名称就可以了。然后代码就可以改成:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
python复制代码import requests, json

weatherUrl = "http://wthrcdn.etouch.cn/WeatherApi?city=" #返回xml数据
weatherUrl = "http://wthrcdn.etouch.cn/weather_mini?city=" #返回json数据

cityName = input("请输入你要查询的城市:")

weatherResp = requests.get(weatherUrl + cityName)
d = weatherResp.json()

if(d['status'] >= 1000):
print("城市:", d["data"]["city"])
print("时间:", d["data"]["forecast"][0]["date"])
print("温度:", d["data"]["forecast"][0]["high"], d["data"]["forecast"][0]["low"])
print("天气:", d["data"]["forecast"][0]["type"])

因为这个结构返回数据正常是1000,所以判断改成了==1000。一下就方便多了~

本文转载自: 掘金

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

【设计模式系列】抽象工厂模式 前言 抽象工厂模式的结构 抽象

发表于 2021-11-19

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

前言

抽象工厂模式也是创建型设计模式之一,它和工厂模式很相似,抽象工厂是所有形态的工厂模式中最为抽象和最具一般性的一种形态,当有多个抽象角色时可以使用种工厂模式。

关联阅读:工厂模式

比如在我们上一期工厂模式中只有一个抽象产品Computer,如果说现在又有一个新的抽象产品耳机也要创建,耳机包括蓝牙耳机和有线耳机,那么就可以使用抽象工厂模式,对于客户端来说,只需要指定工厂类型,就可以创建具体的工厂了,无需指定产品的具体情况。

抽象工厂可以理解为是工厂的工厂,用于针对不同的产品分支创建出不同的工厂。

Talk is cheap,Show me the code.

抽象工厂模式的结构

同样需要有Computer抽象类和子类PC,Laptop。

然后我们增加耳机的抽象类Headset,然后有两个子类BluetoothHeadset和有线耳机WiredHeadset。

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 abstract class HeadSet {

public abstract void play();

}
/**
* 蓝牙耳机
**/
public class BlueToothHeadSet extends HeadSet{
@Override
public void play() {
System.out.println("蓝牙耳机播放音乐");
}
}
/**
* 有线耳机
**/
public class WiredHeadset extends HeadSet{
@Override
public void play() {
System.out.println("有线耳机播放音乐");
}
}

创建耳机工厂类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
java复制代码/**
* @author 小黑说Java
* @ClassName HeadSetFactory
* @Description
* @date 2021/11/17
**/
public class HeadSetFactory implements AbstractFactory {
@Override
public Computer createComputer(String type, String ram, String hdd, String cpu) {
return null;
}

@Override
public HeadSet createHeadSet(String type) {
if ("Bluetooth".equals(type)) {
return new BlueToothHeadSet();
}
if ("Wired".equals(type)) {
return new WiredHeadset();
}
return null;
}
}

然后需要有一个可以创建工厂的服务,在这个服务中可以按照工厂的类型创建出具体的工厂实例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
java复制代码/**
* @author 小黑说Java
* @ClassName FactoryProducer
* @Description
* @date 2021/11/18
**/
public class FactoryProducer {
public static AbstractFactory getFactory(String factoryType) {
if ("Computer".equals(factoryType)) {
return new ComputerFactory();
}
if ("Headset".equals(factoryType)) {
return new HeadSetFactory();
}
return null;
}
}

接下来通过一个简单的测试代码来模拟对抽象工厂的使用:

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

public static void main(String[] args) {
AbstractFactory computerFactory = FactoryProducer.getFactory("Computer");
AbstractFactory headsetFactory = FactoryProducer.getFactory("Headset");
// 通过computerFactory创建PC
Computer pc = computerFactory.createComputer("PC", "16GB", "500GB", "2.4GHz");
// 通过headsetFactory创建有线耳机
HeadSet wired = headsetFactory.createHeadSet("Wired");

Computer laptop = computerFactory.createComputer("Laptop", "16GB", "500GB", "2.4GHz");
HeadSet bluetooth = headsetFactory.createHeadSet("Bluetooth");
}
}

抽象工厂的类图

抽象工厂模式的优势

抽象工厂模式主要有以下优势:

  • 分离了具体的类。客户通过抽象接口操纵实例,产品的类名也在具体工厂的实现中被分离,它们不出现在客户代码中。
  • 易于更换产品系列。一个具体工厂类只在初始化时出现一次,这使得改变一个应用的具体工厂变得很容易,只需改变具体的工厂即可使用不同的产品配置
  • 有利于产品的一致性。当一个系列的产品对象被设计成一起工作时,一个应用一次只能使用同一个系列中的对象,这一点很重要,而抽象工厂很容易实现这一点

JDK中的抽象工厂模式

  • javax.xml.parsers.DocumentBuilderFactory.newInstance()
  • javax.xml.transform.TransformerFactory.newInstance()
  • javax.xml.xpath.XPathFactory.newInstance()

抽象工厂模式和工厂模式的区别

  • 工厂模式针对的是多个产品结构;
  • 抽象工厂模式针对的是多个产品族结构。

小结

抽象工厂模式的一个主要功能是它能够隔离要生成的具体产品类, 由于这些类的实际类名部被隐藏在工厂内部,因此客户端根本不需要关心如何对它们进行实例化的细节。每种设计模式都是针对特定问题的解决方案,而抽象工厂模式面临的问题则是当涉及到有多个产品等级结构时,如何更好地进行软件体系结构的设计。


本文转载自: 掘金

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

【下】net core 下的PostgreSQL 异常排查

发表于 2021-11-19

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

书接上回…

5.幽灵再现

由于测试环境未能真正的和线上环境保持一致,因此运维又开了一台AWS的云数据库(postgresql)环境,分配了域名供测试调用。

为了谨慎起见,再交付产品组前,我使用多线程进行了测试,并未发现问题,变交给了产品组。

而产品组在使用后,发现问题依然存在。

这该死的BUG,像幽灵一样,怎么搞?

感觉陷于了死循环里。

6.静心分析问题

从头来过,不抛弃,不放弃!

为了解决该问题,我查阅了云服务的数据库日志,终于找到了有用的线索。

1
2
3
4
5
kotlin复制代码数据库日志 
2021-11-10 05:28:03 UTC:218.76.52.112(26828):xxx@test:[26849]:LOG: could not receive data from client: Connection reset by peer
2021-11-10 05:28:16 UTC:218.76.52.112(26346):xxx@postgres:[24160]:LOG: could not receive data from client: Connection reset by peer
2021-11-10 05:28:16 UTC:218.76.52.112(26504):xxx@test:[25374]:LOG: could not receive data from client: Connection reset by peer
2021-11-10 05:28:16 UTC:218.76.52.112(26361):xxx@test:[24280]:LOG: could not receive data from client: Connection reset by peer

重要的线索来了,Connection reset by peer,连接被重置! 被谁重置了?

7 连接被重置

TCP连接被重置的情景如下:

  1. 如果一端的Socket被关闭(或主动关闭,或因为异常退出而 引起的关闭),另一端仍发送数据,发送的第一个数据包引发该异常(Connect reset by peer)。

Socket默认连接60秒,60秒之内没有进行心跳交互,即读写数据,就会自动关闭连接。

  1. 如果一端退出,但退出时并未关闭该连接,另一端如果在从连接中读数据则抛出该异常(Connection reset)。

简单的说就是在连接断开后的读和写操作引起的。

情景出现的常见原因:

  • 服务器的并发连接数超过了其承载量,服务器会将其中一些连接关闭;
    如果知道实际连接服务器的并发客户数没有超过服务器的承载量,则有可能是中了病毒或者木马,引起网络流量异常。
  • 客户关掉了连接,而服务器还在给客户端发送数据;
  • 防火墙的问题

如果网络连接通过防火墙,而防火墙一般都会有超时的机制,在网络连接长时间不传输数据时,会关闭这个TCP的会话。

关闭后再读写,就会导致异常。 如果关闭防火墙,解决了问题,需要重新配置防火墙,或者自己编写程序实现TCP的长连接。

实现TCP的长连接,需要自己定义心跳协议,每隔一段时间,发送一次心跳协议,双方维持连接。

8 .解决问题

是的,我本地测试AWS服务就没有问题,而测试环境连接AWS服务器就会频繁发生问题,我们之间最大的不同就是网络硬件通道不一样。

那可以断定数据库的连接被缓存在连接池内,而当再次取出来用时,这个连接可能早被网络某个代理/防火墙/路由器等关闭,因此就出现了异常。

不得不说,连接字符串参数很重要,再次阅读文档,即可找到:
Npgsql .net 版本的PostgreSQL数据库连接字符串及参数

  1. 设置 ConnectionLifetime 设置最大生命周期,防止过长时间重用,设置为60s
  2. 设置 Keepalive 心跳,防止被回收。设置为30s

我把这2项配置强有力的推给了产品组。经过半个小时后,产品组反馈,问题得到了解决!

  1. 阅读源码,找找根

我们找到 ConnectorPool类,查看其代码,发现其从池子中获取空闲链接时,会检查是否断开状态。

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
csharp复制代码bool CheckIdleConnector([NotNullWhen(true)] NpgsqlConnector? connector)
{
if (connector is null)
return false;

// Only decrement when the connector has a value.
Interlocked.Decrement(ref _idleCount);

// An connector could be broken because of a keepalive that occurred while it was
// idling in the pool
// TODO: Consider removing the pool from the keepalive code. The following branch is simply irrelevant
// if keepalive isn't turned on.
if (connector.IsBroken)
{
CloseConnector(connector);
return false;
}

if (_connectionLifetime != TimeSpan.Zero && DateTime.UtcNow > connector.OpenTimestamp + _connectionLifetime)
{
Log.Debug("Connection has exceeded its maximum lifetime and will be closed.", connector.Id);
CloseConnector(connector);
return false;
}

return true;
}

当然,注释也表明了,如果设置了KeepAlive参数,那么断开检查是有效的,如果没有设置那么该参数是无效的。是啊,没有心跳,它应该也无法检查链接是否断开吧!

从代码中也能看出来,如果设置了链接的生命周期,那么超时的链接也会被销毁而不是返回给客户端。

继续查看NpgsqlConnector的实现代码,发现了心跳检查的奥妙。

1
csharp复制代码_keepAliveTimer = new Timer(PerformKeepAlive, null, Timeout.Infinite, Timeout.Infinite);

原来是利用定时器进行检查,那么也就是说如果设置了KeepAlive,那么每个链接多占用了1个线程。

心跳检查的核心代码如下:

1
2
3
4
5
6
7
8
9
csharp复制代码Log.Trace("Performing keepalive", Id);
AttemptPostgresCancellation = false;
var timeout = InternalCommandTimeout;
WriteBuffer.Timeout = TimeSpan.FromSeconds(timeout);
UserTimeout = timeout;
WriteSync(async: false);
Flush();
SkipUntil(BackendMessageCode.ReadyForQuery);
Log.Trace("Performed keepalive", Id);

一个简单的写 SYNC 就搞定了心跳保持和检查。

从上述代码来看,我们的推断是又道理的,如果没有心跳检查,那么放在链接池的链接的确状态是未知的,如果被其他网关断开了,那么再次访问,异常必然会被抛出。

嗯,好开心!

  1. 小结

本次故障排查,总跨度有2周时间,共分析2次故障,每次大约占用2-3天时间,确实不易啊,各位看到的给点支持!

👓都看到这了,还在乎点个赞吗?

👓都点赞了,还在乎一个收藏吗?

👓都收藏了,还在乎一个评论吗?

本文转载自: 掘金

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

1…281282283…956

开发者博客

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