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

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


  • 首页

  • 归档

  • 搜索

算法开启队列实现栈 用队列实现栈

发表于 2021-11-29

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

用队列实现栈

题目

image-20211101211340400

栈结构体

1
2
3
4
c复制代码typedef struct {
Queue q1;
Queue q2;//两个队列
} MyStack;

栈初始化

1
2
3
4
5
6
c复制代码MyStack* myStackCreate() {
MyStack* st = (MyStack*)malloc(sizeof(MyStack));
QueueInit(&st->q1);
QueueInit(&st->q2);
return st;
}

入“栈”

1
2
3
4
5
6
7
8
9
10
c复制代码void myStackPush(MyStack* obj, int x) {
if(!QueueErase(&obj->q1))
{
QueuePush(&obj->q1,x);
}
else//两个都为空时push给q2
{
QueuePush(&obj->q2,x);
}
}

出“栈”并取栈顶元素

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
C复制代码int myStackPop(MyStack* obj) {
Queue* emptyQ = &obj->q1;
Queue* nonemptyQ = &obj->q2;//假设q2空,q1非空
//不是我们就互换位置
if(!QueueErase(emptyQ))
{
nonemptyQ = &obj->q1;
emptyQ = &obj->q2;
}
//非空队长大于一时朝空队里面挪动数据
while(QueueSize(nonemptyQ)>1)
{
//把非队空的对头数拿出push给对空的
QueuePush(emptyQ,QueueFront(nonemptyQ));
//然后把非队空的对头数pop掉
QueuePop(nonemptyQ);
}
//因为要返回栈顶数据所以存完再pop
int tmp = QueueFront(nonemptyQ);
//此时非空队就只还有一个数据,pop掉就行
QueuePop(nonemptyQ);
return tmp;
}

取栈顶元素

1
2
3
4
5
6
7
8
9
10
11
c复制代码int myStackTop(MyStack* obj) {
//谁不为空就去谁队尾数据
if(!QueueErase(&obj->q1))
{
return QueueBack(&obj->q1);
}
else
{
return QueueBack(&obj->q2);
}
}

判断栈空

1
2
3
c复制代码bool myStackEmpty(MyStack* obj) {
return QueueErase(&obj->q1)&&QueueErase(&obj->q2);
}

栈销毁

1
2
3
4
5
c复制代码void myStackFree(MyStack* obj) {
QueueDestroy(&obj->q1);
QueueDestroy(&obj->q2);
free(obj);
}

image-20211102002834360

栈代码(接口代码去我上面文章取) 算法开启小码农队列血脉

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
c复制代码
typedef struct {
Queue q1;
Queue q2;//两个队列
} MyStack;


MyStack* myStackCreate() {
MyStack* st = (MyStack*)malloc(sizeof(MyStack));
QueueInit(&st->q1);
QueueInit(&st->q2);
return st;
}

void myStackPush(MyStack* obj, int x) {
if(!QueueErase(&obj->q1))
{
QueuePush(&obj->q1,x);
}
else//两个都为空时push给q2
{
QueuePush(&obj->q2,x);
}
}

int myStackPop(MyStack* obj) {
Queue* emptyQ = &obj->q1;
Queue* nonemptyQ = &obj->q2;//假设q2空,q1非空
//不是我们就互换位置
if(!QueueErase(emptyQ))
{
nonemptyQ = &obj->q1;
emptyQ = &obj->q2;
}
//非空队长大于一时朝空队里面挪动数据
while(QueueSize(nonemptyQ)>1)
{
//把非队空的对头数拿出push给对空的
QueuePush(emptyQ,QueueFront(nonemptyQ));
//然后把非队空的对头数pop掉
QueuePop(nonemptyQ);
}
//因为要返回栈顶数据所以存完再pop
int tmp = QueueFront(nonemptyQ);
//此时非空队就只还有一个数据,pop掉就行
QueuePop(nonemptyQ);
return tmp;
}

int myStackTop(MyStack* obj) {
//谁不为空就去谁队尾数据
if(!QueueErase(&obj->q1))
{
return QueueBack(&obj->q1);
}
else
{
return QueueBack(&obj->q2);
}
}

bool myStackEmpty(MyStack* obj) {
return QueueErase(&obj->q1)&&QueueErase(&obj->q2);
}

void myStackFree(MyStack* obj) {
QueueDestroy(&obj->q1);
QueueDestroy(&obj->q2);
free(obj);
}

本文转载自: 掘金

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

Spring Cloud Gateway源码解析-10-自定

发表于 2021-11-29

自定义Predicate

思路

在SCG初始化解析中我们已经知道了Predicate是怎么根据我们的配置装配的。
以RemoteAddrRoutePredicateFactory为例。

1
2
json复制代码public class RemoteAddrRoutePredicateFactory
extends AbstractRoutePredicateFactory<RemoteAddrRoutePredicateFactory.Config>

RemoteAddrRoutePredicateFactory继承了抽象类AbstractRoutePredicateFactory,泛型为内部类Config。

1
2
3
4
5
6
7
8
9
json复制代码	@Override
public ShortcutType shortcutType() {
return GATHER_LIST;
}

@Override
public List<String> shortcutFieldOrder() {
return Collections.singletonList("sources");
}

重写shortcutType和shortcutFieldOrder方法,这两个方法主要是用来定义Config的配置及生成方式,具体细节不再深叙,个人认为SCG的ShortcutType设计的很不好理解。

1
2
3
4
5
6
7
8
9
10
json复制代码@Override
public Predicate<ServerWebExchange> apply(Config config) {

return new GatewayPredicate() {
@Override
public boolean test(ServerWebExchange exchange) {
return false;
}
};
}

实现apply方法,内部创建GatewayPredicate匿名内部类。

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
json复制代码public static class Config {

@NotEmpty
private List<String> sources = new ArrayList<>();

@NotNull
private RemoteAddressResolver remoteAddressResolver = new RemoteAddressResolver() {
};

public List<String> getSources() {
return sources;
}

public Config setSources(List<String> sources) {
this.sources = sources;
return this;
}

public Config setSources(String... sources) {
this.sources = Arrays.asList(sources);
return this;
}

public Config setRemoteAddressResolver(
RemoteAddressResolver remoteAddressResolver) {
this.remoteAddressResolver = remoteAddressResolver;
return this;
}

}

根据Predicate功能定义内部类Config。

综上所述总结自定义Predicate要做的事情有如下几点:

  1. 类名称,以XXX开头,RoutePredicateFactory结尾。
  2. 定义内部Config类,内部定义Predicate所需配置。
  3. 继承了抽象类AbstractRoutePredicateFactory,泛型为内部类Config
  4. 重写shortcutType和shortcutFieldOrder方法
  5. 实现apply方法,内部创建GatewayPredicate匿名内部类。

自定义黑名单Predicate

实现

实现黑名单可以通过配置的IP或者IP段进行限制。当请求进入时,获取到当前请求的客户端的IP,判断是否与配置的黑名单匹配,匹配返回false即可。

具体的实现逻辑见下方代码即可,与SCG内置的RemoteAddrRoutePredicateFactory类似

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
json复制代码/**
* Description:黑名单Predicate
* @author li.hongjian
* @email lhj502819@163.com
* @Date 2021/3/31
*/
public class BlackRemoteAddrRoutePredicateFactory
extends AbstractRoutePredicateFactory<BlackRemoteAddrRoutePredicateFactory.Config> {

public BlackRemoteAddrRoutePredicateFactory() {
super(Config.class);
}

@NotNull
private List<IpSubnetFilterRule> convert(List<String> values) {
List<IpSubnetFilterRule> sources = new ArrayList<>();
for (String arg : values) {
addSource(sources, arg);
}
return sources;
}

/**
* 此方法需重写,用来创建Config使用
* @return
*/
@Override
public ShortcutType shortcutType() {

/**
* GATHER_LIST类型只能有一个shortcutField
* {@link this#shortcutFieldOrder()}
*/
return GATHER_LIST;
}

/**
* 配置中的value对应的字段
* 比如当前我们的Config中的字段就为sources
* @return
*/
@Override
public List<String> shortcutFieldOrder() {
return Collections.singletonList("sources");
}


@Override
public Predicate<ServerWebExchange> apply(Config config) {
/**
* IpSubnetFilterRule是Netty中定义的IP过滤规则
*/
//根据配置的sources生成对应规则
List<IpSubnetFilterRule> sources = convert(config.sources);

return new GatewayPredicate() {
@Override
public boolean test(ServerWebExchange exchange) {
InetSocketAddress remoteAddress = config.remoteAddressResolver
.resolve(exchange);
if (remoteAddress != null && remoteAddress.getAddress() != null) {
//只要符合任意一个规则就返回false,与RemoteAddrRoutePredicateFactory相反
for (IpSubnetFilterRule source : sources) {
if (source.matches(remoteAddress)) {
return false;
}
}
}
//如果没有匹配所有规则,则通过
return true;
}
};
}

private void addSource(List<IpSubnetFilterRule> sources, String source) {
//判断是否配置了IP段,如果没有则默认为最大为32,如配置172.15.32.15,则被修改为172.15.32.15/32
if (!source.contains("/")) { // no netmask, add default
source = source + "/32";
}
//假设配置的为 172.15.32.15/18
//根据'/'分割 [0]:172.15.32.15 [1]:18
String[] ipAddressCidrPrefix = source.split("/", 2);
String ipAddress = ipAddressCidrPrefix[0];
int cidrPrefix = Integer.parseInt(ipAddressCidrPrefix[1]);

//设置拒绝规则
sources.add(
new IpSubnetFilterRule(ipAddress, cidrPrefix, IpFilterRuleType.REJECT));
}

public static class Config{
/**
* 可配置多个IP/IP段
*/
@NotEmpty
private List<String> sources = new ArrayList<>();
/**
* 用来解析客户端IP
*/
@NotNull
private RemoteAddressResolver remoteAddressResolver = new RemoteAddressResolver() {
};

public List<String> getSources() {
return sources;
}

public Config setSources(List<String> sources) {
this.sources = sources;
return this;
}

public Config setSources(String... sources) {
this.sources = Arrays.asList(sources);
return this;
}

public Config setRemoteAddressResolver(
RemoteAddressResolver remoteAddressResolver) {
this.remoteAddressResolver = remoteAddressResolver;
return this;
}
}
}

使用

1
2
3
4
5
6
7
8
9
json复制代码spring:
cloud:
gateway:
routes:
- id: hello_route
uri: http://localhost:8088/api/hello
predicates:
- BlackRemoteAddr=169.254.183.1/18,172.17.31.1/18 #配置指定IP段限制
- Path=/api/hello

验证

启动SCG和一个本地服务并暴露接口为/api/hello,返回结果为hello world。在自定义的BlackRemoteAddrRoutePredicateFactory#test中断点.

**环境:本机IP ** 169.254.183.16。配置限制IP为169.254.183.1/18。此时我们的请求应该是会被拒绝返回404状态码的。
在这里插入图片描述

可以看到Predicate获取到了我们的IP,并且匹配了我们配置的规则,因此返回false,表示不匹配该路由,因此返回404.
在这里插入图片描述

修改配置为其他IP段,比如169.254.183.18/32,此时我们的IP是不符合该规则的,因此会放行。
重启项目再次测试。
在这里插入图片描述

可以看到我们的IP并没有匹配配置的规则,返回true,表示可以走该路由。
在这里插入图片描述

上边我对“重启”重点标注了,每次修改规则都要重启项目才能生效,那么在实际用的时候,我们肯定想实现不重启就可以动态修改我们配置的规则,那么该怎么做呢?
我们可以通过将配置写到外部一个源中,比如DB中,通过类似Nacos一样定时发送一个刷新配置的事件去实现刷新Predicate配置。明天我们就来基于Redis来实现动态刷新配置的功能。

本文转载自: 掘金

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

LeetCode-133-克隆图

发表于 2021-11-29

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

克隆图

题目描述:给你无向 连通 图中一个节点的引用,请你返回该图的 深拷贝(克隆)。

图中的每个节点都包含它的值 val(int) 和其邻居的列表(list[Node])。

示例说明请见LeetCode官网。

来源:力扣(LeetCode)

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

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

解法一:深度优先遍历

首先,如果当前节点为空,不用处理,直接返回。

否则,使用一个Map即visited来存储已经处理过的图节点和相应的克隆节点,递归处理过程如下:

  • 判断当前节点是否在visited中,如果在,则说明已经处理过了,则直接从visited中取出当前节点的克隆并返回。
  • 否则,根据当前节点克隆出一个新的克隆节点,新克隆的节点的邻居节点初始化为空,然后将当前节点和新的克隆节点添加到visited中;
  • 然后递归处理当前节点的邻居节点。

最后,返回当前节点的克隆节点。

解法二:广度优先遍历

同样的,首先,如果当前节点为空,不用处理,直接返回。

否则,也是需要初始化一个Map即visited来存储已经处理过的图节点和相应的克隆节点,首先将当前节点和由当前节点克隆的节点添加到visited中,然后将当前节点添加到一个队列queue中,然后处理队列中的节点,知道队列不为空,处理过程如下:

  • 取出队列的头结点为curNode;
  • 遍历处理curNode节点的邻居节点;
  • 如果当前邻居节点不在visited,则将其和相应的克隆节点添加到visited中,并将其加到队列中;
  • 然后将当前邻居节点的克隆节点添加到curNode的克隆节点的邻居节点列表中。

最后,返回初始节点的克隆节点。

注:图相关的算法还是了解的不多,得多学学。

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

import java.util.*;

public class LeetCode_133 {
private static Map<GraphNode, GraphNode> visited = new HashMap<>();

/**
* 深度优先遍历
*
* @param node 当前节点
* @return
*/
public static GraphNode cloneGraph(GraphNode node) {
if (node == null) {
return node;
}
// 如果该节点已经被访问过了,则直接从哈希表中取出对应的克隆节点返回
if (visited.containsKey(node)) {
return visited.get(node);
}

// 克隆当前节点
GraphNode cloneNode = new GraphNode(node.val, new ArrayList<>());
// 哈希表存储
visited.put(node, cloneNode);

// 递归处理当前节点的邻居节点
for (GraphNode neighbor : node.neighbors) {
cloneNode.neighbors.add(cloneGraph(neighbor));
}

return cloneNode;
}

/**
* 广度优先遍历
*
* @param node 当前节点
* @return
*/
public static GraphNode cloneGraph2(GraphNode node) {
if (node == null) {
return node;
}

HashMap<GraphNode, GraphNode> visited = new HashMap<>();

Queue<GraphNode> queue = new LinkedList<>();
// 将当前节点添加到队列中
queue.add(node);
// 克隆第一个节点并存储到哈希表中
visited.put(node, new GraphNode(node.val, new ArrayList<>()));

// 广度优先遍历
while (!queue.isEmpty()) {
// 取出队列的头节点
GraphNode curNode = queue.remove();
// 遍历当前节点的邻居节点
for (GraphNode neighbor : curNode.neighbors) {
if (!visited.containsKey(neighbor)) {
visited.put(neighbor, new GraphNode(neighbor.val, new ArrayList<>()));
// 将邻居节点添加到队列中
queue.add(neighbor);
}

// 更新当前节点的邻居节点列表
visited.get(curNode).neighbors.add(visited.get(neighbor));
}
}

return visited.get(node);
}

public static void main(String[] args) {

}
}

【每日寄语】 自己动手,丰衣足食!

本文转载自: 掘金

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

实际开发中mybatis的动态排序问题

发表于 2021-11-29

问题产生

上周做报表的时候,有一个动态排序的需求:

根据”create_time”或者”update_time”对数据进行升序或者降序。

初步想法

当时第一反应是,和前端约定传过来两个参数,一个作为排序字段,一个作为升降序,然后在xml文件中用#{}取到。

于是约定好sortingType 传create_Time或者create_time,不传默认create_time,sortingRule传ASC,DESC不传默认ASC。

代码如下:

image.png

以为只要前端传的没问题,就不会出问题,但是在测试中发现并没有进行排序,前端传过来值了。说明是后端问题。

解决

其实解决办法比较简单,这属于mybatis本身的问题,为了防止sql注入,使用#{}取值时会根据具体的数据类型 将其解析,所以会将字符串解析成 “”,这在order by 语句后肯定是不行的,于是使用${}就解决了这个问题,当然,得在代码里控制传入的值不能是别的字符,这样就在解决问题的基础之上也解决了sql注入问题。

本文转载自: 掘金

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

☆打卡算法☆LeetCode 66、加一 算法解析

发表于 2021-11-29

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

推荐阅读

  • CSDN主页
  • GitHub开源地址
  • Unity3D插件分享
  • 简书地址
  • 我的个人博客
  • QQ群:1040082875

大家好,我是小魔龙,Unity3D软件工程师,VR、AR,虚拟仿真方向,不定时更新软件开发技巧,生活感悟,觉得有用记得一键三连哦。

一、题目

1、算法题目

“给定一个由整数组成的数组,在该数的基础上加一。”

题目链接:

来源:力扣(LeetCode)

链接:66. 加一 - 力扣(LeetCode) (leetcode-cn.com)

2、题目描述

给定一个由 整数 组成的 非空 数组所表示的非负整数,在该数的基础上加一。

最高位数字存放在数组的首位, 数组中每个元素只存储单个数字。

你可以假设除了整数 0 之外,这个整数不会以零开头。

1
2
3
4
ini复制代码示例 1:
输入: digits = [1,2,3]
输出: [1,2,4]
解释: 输入数组表示数字 123。
1
2
3
4
ini复制代码示例 2:
输入: digits = [4,3,2,1]
输出: [4,3,2,2]
解释: 输入数组表示数字 4321。

二、解题

1、思路分析

找出最长的后缀9,对于数组digits来说,如果末尾没有9,末尾加1即可。

末尾有若干个9,那么只需要找出从末尾开始第一个不为9的元素,将该元素加1,后面的9全部归零,然后返回。

对于数组元素全部都是9的情况,只需要构造一个比原来数组大于1的新数组,将首元素设1,其余元素为0即可。

接下来,就需要逆序遍历数组,找出9元素。

2、代码实现

代码参考:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
csharp复制代码public class Solution {
public int[] PlusOne(int[] digits) {
int n = digits.Length;
for (int i = n - 1; i >= 0; --i) {
if (digits[i] != 9) {
++digits[i];
for (int j = i + 1; j < n; ++j) {
digits[j] = 0;
}
return digits;
}
}

// digits 中所有的元素均为 9
int[] ans = new int[n + 1];
ans[0] = 1;
return ans;
}
}

image.png

3、时间复杂度

时间复杂度 : O(n)

其中n是数组的长度,只需要遍历一遍数组即可求得答案。

空间复杂度: O(1)

只需要常数级别的空间存放变量。

三、总结

不能用转换成数字然后加了之后再转换成数组的想法哦,会溢出的。

本文转载自: 掘金

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

ThreadLocal详解

发表于 2021-11-29

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

一、什么是ThreadLocal

ThreadLocal是通过线程隔离的方式防止任务在共享资源上产生冲突,线程本地存储是一种自动化机制,可以为使用相同变量的每个不同线程都创建不同的存储。

​

线程安全的解决思路:

1)互斥同步:synchronized 和 ReentrantLock

2) 非阻塞同步:CAS 、AtomicXXX

3) 无同步方案:本地存储(Thread Local)

本地存储(Thread Local)官网解释:

This class provides thread-local variables. These variables differ from their normal counterparts in that each thread that accesses one (via its {@code get} or {@code set} method) has its own, independently initialized copy of the variable. {@code ThreadLocal} instances are typically private static fields in classes that wish to associate state with a thread (e.g., a user ID or Transaction ID) 该类提供了线程局部 (thread-local) 变量。这些变量不同于它们的普通对应物,因为访问某个变量(通过其 get 或 set 方法)的每个线程都有自己的局部变量,它独立于变量的初始化副本。ThreadLocal 实例通常是类中的 private static 字段,它们希望将状态与某一个线程(例如,用户 ID 或事务 ID)相关联。

ThreadLocal提供了线程本地的实例。它与普通变量的区别在于,每个使用该变量的线程都会初始化一个完全独立的实例副本。ThreadLocal变量通常被private static修饰。当一个线程结束时,它所使用的所有ThreadLocal相对的实例副本都可被回收。

总的来说,ThreadLocal适用于每个线程需要自己独立的实例且该实例需要在多个方法中被使用,也即变量在线程间隔离而在方法或类间共享的场景。

​

二、ThreadLocal原理-如何实现线程隔离

每个Thread对象都有一个ThreadLocalMap,当创建一个ThreadLocal时,就会将该Threadlocal对象添加到该Map中,其中键就是ThreadLocal,值可以是任意类似。

向ThreadLocal存入一个值,实际上是向当前线程对象中的ThreadLocalMap存入值,ThreadLocalMap可以简单的理解成一个Map,而向这个Map存值的key就是ThreadLocal实例本身。\

image.png

image.png

也就是说,想要存入的ThreadLocal中的数据实际上并没有存到ThreadLocal对象中去,而是以这个ThreadLocal实例作为key存到当前线程中的一个Map中去了,获取ThreadLocal的值同样也是这个道理,就也就是ThreadLocal可以实现线程之间隔离的原因。

三、ThreadLocalMap对象是什么

image.png

image.png

1)每个Thread线程内部都有一个ThreadLocalMap;

2) Map里面存储线程本地对象ThreadLocal(key)和线程的变量副本(value);

3) Thread内部的Map是由ThreadLocal维护,ThreadLocal负责向Map获取和设置线程的变量值;

4)一个Thread可以有多个ThreadLocal;

总之,每个线程都有其独有的Map结构,而Map中存有的是ThreadLocal为key变量副本为Vaule的键值对,以此达到变量隔离的目的。

​

当初始化一个线程时其内部去创建一个ThreadLocalMap的Map容器待用

1
2
3
4
5
vbnet复制代码public class Thread implements Runnable {
    /* ThreadLocal values pertaining to this thread. This map is maintained
     * by the ThreadLocal class. */
    ThreadLocal.ThreadLocalMap threadLocals = null;
}

当ThreadLocalMap被创建加载时其静态内部类Entry也随之加载,完成初始化动作。

1
2
3
4
5
6
7
8
ruby复制代码static class Entry extends WeakReference<ThreadLocal<?>> {
        /** The value associated with this ThreadLocal. */
       Object value;
        Entry(ThreadLocal<?> k, Object v) {
            super(k);
            value = v;
        }
}

线程Thread内部的Map容器初始化完毕,那么它又是如何和ThreadLocal有关系,ThreadLocal又是如何管理键值对的关系。

​

ThreadLocal核心方法分析:

​

set()方法用于保存当前线程的副本变量值:

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
typescript复制代码/**
 * Sets the current thread's copy of this thread-local variable
 * to the specified value.  Most subclasses will have no need to
 * override this method, relying solely on the {@link #initialValue}
 * method to set the values of thread-locals.
 *
 * @param value the value to be stored in the current thread's copy of
 *        this thread-local.
 */
public void set(T value) {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null)
        map.set(this, value);
    else
        createMap(t, value);
}

ThreadLocalMap getMap(Thread t) {
    return t.threadLocals;
}

void createMap(Thread t, T firstValue) {
    t.threadLocals = new ThreadLocalMap(this, firstValue);
}

当在Thread内部调用set()方法时:

1)第一步会获取调用当前方法的线程Thread;

2) 获取当前线程内部的ThreadLocalMap容器;

3)把变量副本给set到Map;

ThreadLocal(就是个维护线程内部变量的工具),只是在set时去操作Thread内部的ThreadLocalMap将变量拷贝到Thread内部的Map容器中,key就是当前的ThreadLocal,value就是变量的副本。

get()方法用于获取当前线程的副本变量值:

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
java复制代码/**
 * Returns the value in the current thread's copy of this
 * thread-local variable.  If the variable has no value for the
 * current thread, it is first initialized to the value returned
 * by an invocation of the {@link #initialValue} method.
 *
 * @return the current thread's value of this thread-local
 */
public T get() {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null) {
        ThreadLocalMap.Entry e = map.getEntry(this);
        if (e != null)
            return (T)e.value;
    }
    return setInitialValue();
}

ThreadLocalMap getMap(Thread t) {
    return t.threadLocals;
}

private T setInitialValue() {
    T value = initialValue();
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null)
        map.set(this, value);
    else
        createMap(t, value);
    return value;
}

protected T initialValue() {
    return null;
}

1)获取当前线程的ThreadLocalMap对象;

2) 从map中根据this(当前的threadlocal对象)获取线程存储的Entry节点;

3)从Entry节点获取存储的对应value副本值返回;

4)map为空的话返回初始值null,即线程变量副本为null;

​

remove()方法移除当前线程的副本变量值:

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
ini复制代码/**
 * Removes the current thread's value for this thread-local
 * variable.  If this thread-local variable is subsequently
 * {@linkplain #get read} by the current thread, its value will be
 * reinitialized by invoking its {@link #initialValue} method,
 * unless its value is {@linkplain #set set} by the current thread
 * in the interim.  This may result in multiple invocations of the
 * <tt>initialValue</tt> method in the current thread.
 *
 * @since 1.5
 */
public void remove() {
 ThreadLocalMap m = getMap(Thread.currentThread());
 if (m != null)
     m.remove(this);
}

ThreadLocalMap getMap(Thread t) {
    return t.threadLocals;
}

 /**
  * Remove the entry for key.
  */
    private void remove(ThreadLocal<?> key) {
    Entry[] tab = table;
    int len = tab.length;
    int i = key.threadLocalHashCode & (len-1);
       for (Entry e = tab[i];
            e != null;
            e = tab[i = nextIndex(i, len)]) {
              if (e.get() == key) {
                  e.clear();
                  expungeStaleEntry(i);
                  return;
              }
          }
    }

四、内存泄漏问题

实际上ThreadLocalMap中使用的key为ThreadLocal的弱引用,弱引用的特点是,如果这个对象只存在弱引用,那么在下一次垃圾回收时必然会被清理掉。

​

​

key的弱引用问题:

To help deal with very large and long-lived usages, the hash table entries use WeakReferences for keys.

为了处理非常大和生命周期非常长的线程,哈希表使用弱引用作为 key

生命周期长:线程池中的线程

​

ThreadLocal在没有外部对象引用时如Thread,发生GC时弱引用key会被回收,而value是强引用不会回收,如果创建ThreadLocal的线程一直持续运行如线程池中的线程,那么这个Entry对象中的value就有可能一直得不到回收,发生内存泄漏。

​

1)key如果使用强引用:引用的ThreadLocal的对象被回收了,但是ThreadLocalMap还持有ThreadLocal的强引用,如果没有手动删除,ThreadLocal不会被回收,导致Entry内存泄漏。

​

2)key使用弱引用:引用的ThreadLocal的对象被回收了,由于ThreadLocalMap持有ThreadLocal的弱引用,即使没有手动删除,ThreadLocal也会被回收。value在下一次ThreadLocalMap调用set,get,remove时会被清除。

​

Java8中已经做了一些优化如,在ThreadLocal的get()、set()、remove()方法调用时会清除掉线程ThreadLocalMap中所有Entry中key为null的value,并将整个Entry设置为null,利于下次内存回收。

​

建议回收自定义的ThreadLocal变量,尤其在线程池场景下,线程经常会被复用,如果不清理自定义的ThreadLocal变量,可能会影响后续业务逻辑和造成内存泄漏等问题,尽量在代理中使用try-finally块进行回收。

本文转载自: 掘金

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

Go语言搬砖 resty http包(调jenkins ap

发表于 2021-11-29

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

前言

在前两篇文章中都使用HttpRequest这个http包来做api的请求

image.png
然后github上面还有一个更有名,星星更多,社区也更活跃的http包Resty

最近11月3号又发布了一个新版本,项目参与者多达75个,标星有5.2k

Resty特色

  • 支持GET, POST, PUT, DELETE, HEAD, PATCH, OPTIONS等方法
  • 设置和请求的方法简单
  • 请求体支持多种数据类型(string,[]byte,struct,map,slice,io.Reader)
  • 返回支持[]byte和string
  • 对json和xml内容自动编码和解码
  • 支持上传一个多个文件和下载指定路径或打包
  • 请求参数支持QueryParams,QueryString,FormData
  • 支持重试,代理,证书
  • 支持简单认证和认证token

Resty官网: github.com/go-resty/re…

演示例子

演示部分例子get,post等例子,其它put,delete,patch都差不多

简单get请求

该示例显示请求状态和响应耗时

1
2
3
4
5
6
js复制代码func simpleGet() {
client := resty.New()
resp, _ := client.R().EnableTrace().Get("https://httpbin.org/get")
fmt.Println("状态码:", resp.StatusCode())
fmt.Println("总耗时:", resp.Time())
}

增强get请求

该示例支持map类型和路径参数,支持设置请求头和认证token

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
js复制代码func enhancedGet() {
client := resty.New()
resp, _ := client.R().
SetQueryParams(map[string]string{
"page_no": "1",
"limit": "20",
"sort": "name",
"order": "asc",
"random": strconv.FormatInt(time.Now().Unix(), 10),
}).
//SetQueryString("productId=232&template=fresh-sample&cat=resty&source=google&kw=buy a lot more").
SetHeader("Accept", "application/json").
SetAuthToken("xxxxxxxxxxxxxxxxxxxxxxx").
Get("/search_result")
fmt.Println(resp)
}

灵活post请求

该示例支持设置body内容为map支持简单认证和token认证

1
2
3
4
5
6
7
8
js复制代码func variousPost() {
client := resty.New()
resp, err := client.R().
SetBody(map[string]interface{}{"username": "testuser", "password": "testpass"}).
SetAuthToken("<your-auth-token>").
Post("https://myapp.com/login")
fmt.Println(resp, err)
}

多文件上传

该示例支持多文件上传,支持定义本地文件路径,支持设置FormData数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
js复制代码func multipartFileUpload() {
profileImgBytes, _ := ioutil.ReadFile("/Users/jeeva/test-img.png")
notesBytes, _ := ioutil.ReadFile("/Users/jeeva/text-file.txt")

client := resty.New()

resp, _ := client.R().
SetFileReader("profile_img", "test-img.png", bytes.NewReader(profileImgBytes)).
SetFileReader("notes", "text-file.txt", bytes.NewReader(notesBytes)).
SetFormData(map[string]string{
"first_name": "dddd",
"last_name": "cccc",
}).
Post("http://myapp.com/upload")
fmt.Println(resp)
}

文件下载

定义下载保存路径,直接下载

1
2
3
4
5
6
js复制代码func downFile() {
client := resty.New()
_, _ = client.R().
SetOutput("plugin/test-v5.1-beta.zip").
Get("http://my.app/test")
}

实战例子

通过上面的演示例子,可以看到resty这个包功能非常强大

接下来我们使用该包来封装jenkins api,来进行二次开发

构造一个jenkins客户端

编写一个方法创建jenkins客户端,后续所有动作只需调用既可

该客户端集成了重试,json头,以及简单认证

1
2
3
4
5
6
7
8
9
10
11
12
js复制代码func jenkinsClient() *resty.Request {
c := resty.New()
a := c.SetRetryCount(3).SetRetryWaitTime(5 * time.Second).
SetRetryMaxWaitTime(20 * time.Second).
R().SetHeader("Accept", "application/json").
SetBasicAuth("username", "password")
return a
}

var (
url = "http://ip:port"
)

获取jenkins job信息

创建好jobinfo的结构体,方便接下来的数据接收

1
2
3
4
5
6
7
8
9
js复制代码type JobInfo struct {
DisplayName string
FullName string
Buildable bool
NextBuildNumber int
InQueue bool
Color string //blue成功 red失败
Url string
}

resty会将返回数据解码,只需要提供结构体接收

1
2
3
4
5
6
7
8
9
10
js复制代码func jenkinsJobInfo(job string) {
a := jenkinsClient()
jobInfo := &JobInfo{}
resp, err := a.SetResult(jobInfo).Get(url + "/job/" + job + "/api/json")
if err != nil {
fmt.Println(err)
}
fmt.Println("resp\n", resp.StatusCode(), resp.Time())
fmt.Println(jobInfo.Color)
}

无参构建job

简单post请求,调用无参数构建

1
2
3
4
5
6
7
js复制代码func jenkinsJobBuild(job string)  {
a := jenkinsClient()
resp, _ := a.Post(url+"/job/"+job+"/build")
if resp.StatusCode() == 201 {
fmt.Println("build 成功")
}
}

查看构建日志

查看日志,需要先获取job的最后一次的buildID

所以这里发请2次请求,第1次获取buildID,第2次获取日志内容

1
2
3
4
5
6
7
8
9
js复制代码func jenkinsJobLog(job string)  {
a := jenkinsClient()
resp, _ := a.Get(url + "/job/" + job + "/lastBuild/buildNumber")
if resp.StatusCode() == 200 {
lastBuild := resp.String()
resp2, _ := a.Get(url + "/job/" + job + "/" + lastBuild + "/logText/progressiveText")
fmt.Println(resp2.String())
}
}

job开关(启用禁用job)

第一个参数为job名称,第二个参数设定开关值

1
2
3
4
5
6
7
8
9
10
js复制代码func jenkinsJobSwich(job string,swi bool) {
a := jenkinsClient()
if swi {
resp, _ := a.Post(url + "/job/" + job + "/enable")
fmt.Println(resp.Status())
} else {
resp, _ := a.Post(url + "/job/" + job + "/disable")
fmt.Println(resp.Status())
}
}

小结

通过实战可以发现,在没有第三方sdk的时候,完全是可以自已通过使用http包,来进行http api项目的二次开发或封装

而resty则是Go http包中的王者

本文转载自: 掘金

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

服务API版本控制设计与实践 一、前言 二、通用解决方案 三

发表于 2021-11-29

一、前言

笔者曾负责vivo应用商店服务器开发,有幸见证应用商店从百万日活到几千万日活的发展历程。应用商店客户端经历了大大小小上百个版本迭代后,服务端也在架构上完成了单体到服务集群、微服务升级。

下面主要聊一聊在业务快速发展过程中,产品不断迭代,服务端在兼容不同版本客户端的API遇到的问题的一些经验和心得。一方面让团队内童鞋对已有的一些设计思想有一个更彻底的理解,另一方面也是希望能引起一些遇到类似场景同行的共鸣,提供解决思路。

二、通用解决方案

应用商店客户端迭代非常频繁,发布新的APP版本的时候,势必导致出现多版本,这样服务端就会导致多个不同的客户端请求。强制用户升级APP,可能会导致用户流失,因此采用多版本共存就是必须的。以下是业界讨论过的的一些SOA服务API版本控制方法参考[1]。在实际开发中原则上离不开以下三个方案。

方案一:The Knot 无版本——即平台的API永远只有一个版本,所有的用户都必须使用最新的API,任何API的修改都会影响到平台所有的用户。(如下图1)

方案二:Point-to-Point——点对点,即平台的API版本自带版本号,用户根据自己的需求选择使用对应的API,需要使用新的API特性,用户必须自己升级。(如下图2)

方案三:Compatible Versioning——兼容性版本控制,和The Knot一样,平台只有一个版本,但是最新版本需要兼容以前版本的API行为。(如下图3)

(引用自:The Costs of Versioning an API)

简单分析,The Knot只维护最新版本,对服务端而言维护有一定简化了,但是要求服务使用者及时适配最新的版本,这种做法不太适用用户产品,目前内部服务比较适用。Point-Point针对不同客户的版本提供独立的服务,当随着版本的增加开发和运维的维护成本会增加,这种在后面我们面对“协议升级”的时候有使用。

方案三应该是最常用的情况,服务端向后兼容。后面案例也主要采用这种思想,具体的做法也是有很多种,会结合具体的业务场景使用不同策略,这个会是接下来讨论的重点。

三、具体业务场景面临的挑战和探索

3.1 The Knot 无版本和Point-to-Point模式的应用场景

上图是我们应用商店迭代变化的一个缩影,业务发展到一定阶段面临以下挑战:

1)业务发展前期,作为服务提供方,服务端不仅要支撑多个版本应用商店客户端,同时服务于软件侧的PC助手;

2)产品形态变化多样,服务端接口变更和维护面临多版本客户端兼容的挑战;

3)架构逻辑上,服务端采用早期传统架构,开发和维护成本比较高;服务端与客户端进行交互的协议优化升级;以及服务拆分势在必行。

所以服务端协议、框架升级以及公共服务拆分是首要解决的方向。改造经历了两个过程:

  • 阶段一新版本新的接口一律采用新的JSON协议;已有功能接口进行兼容处理,根据客户端版本进行区分,返回不同协议的格式内容。
  • 阶段二随着业务迭代,新的版本商店依赖的所有接口都完成了协议升级后,为了提升服务的稳定性,旧的协议性能无法明显提升,一方面升级后端架构和框架,提升开发效率和可维护性。同时拆分和独立新的工程,实现历史工程只提供给历史版本使用。我们针对大流量高并发、以及基础服务场景比如首页、详情、下载进行独立服务独立拆分。同时也提取一些公共的内部RPC服务,比如获取应用详情、过滤服务等。

经过改造,服务端架构如上图所示。

1)至此Old-Service后续只用进行相应的维护工作即可,对应Point-to-Point版本。

2)内部的RPC服务由于只提供内部服务,服务端和客户端可以随时同步升级,只要维护最新的版本就可以,采用The Knot模式。这里需要注意的是服务的升级需要注意保持向下兼容,在扩展字段或者修改字段的时候需要特别小心,不然可能在服务升级的时候会引起客户端调用的异常。

3.2 Compatible Versioning:兼容性版本控制

兼容性版本控制应该是最常见的版本控制方式,特别是在C/S架构当中,具体的兼容性版本不同的策略总结有API版本、客户端版本号、功能参数标志等。

场景一:API版本号控制

随着互联网发展的,用户体验要求也是越来越高,产品形式也会随之每年有不一样的变化。除了避免审美疲劳外,也是在不断探索如何提升屏效、点击率和转化。就拿应用商店首页列表举例。

应用列表在形态上经历过单一的应用双排 -> 单排 -> 单排+穿插的布局。内容上也经历了不同商业化模式、人工排期到算法等演进。

每个版本接口内部逻辑变化是十分大的,有明显差异。如果只是简单在service层根据版本进行判断处理,会导致处理逻辑会变得异常复杂,并且还可能导致对低版本产生影响。同时商店首页是十分重要的业务场景,结合风险考虑,类似这样对场景,在接口URL上新增版本字段,不同对版本使用不同的值,在控制层根据不同的版本进行不同的处理逻辑会更加合理,简单有效。具体策略也有比如在URL上新增接口版本字段/{version}/index、请求头携带版本参数等。

场景二:客户端版本号控制

类似首页列表,商店的穿插Banner也经历了多个版本的迭代。如下图所示。这些穿插样式都是在不同版本下出现的,在样式布局,支持跳转能力等方面各个版本的支持程度不一样,接口返回时需要进行相应的处理适配、过滤等处理。

这类场景如果采用场景一的方案升级新的接口也能够解决,但是会存在大量重复代码,而且新增接口对于客户端接口改造、特别是一些接口路径会影响到大数据埋点统计,也是有比较高的沟通和维护成本在里面。

为了提升代码复用性。使用客户端版本号控制是首选考虑策略。但是需要注意,如果只是简单的在代码层面根据客户端版本号进行判断,会存在以下问题需要考虑:

1)代码层面会存在各种判断,造成的代码可读性差,有没有更加优雅的方法;

2)存在一个客观情况。那就是客户端的版本号是存在不确定性的。由于客户端采用火车发布模式 参考[2],多版本并行开发,导致版本号存在变动、版本跳跃不连续的情况时有发生,也给服务端开发带来了不少困扰。

如何思考解决这些问题呢?其实对于不同的产品形态涉及的一些资源或者产品模块本身出现在不同的迭代周期,可以认为他们具备了版本或者时间的属性。站在程序员视角,把某个资源支持对应的客户端版本作为这个资源对象的一个成员属性。每种资源具有这种属性后,也有相应的逻辑行为来对应成员方法—根据属性进行过滤。这样的设计赋予资源了属性和行为后,资源具备了统一的、灵活的过滤能力,而不再是简单的硬编码根据版本进行if-else判断。

有了方案后,实施起来就比较容易了。开发分配资源ID,并且设置对应支持客户端版本范围。过滤逻辑统一到资源对象。

代码层面可以将过滤逻辑统一封装到一个工具类(示例代码),在各个业务接口返回进行过滤。更加优雅的方案是建立统一的资源上层类,封装资源过滤方法,所有资源位的资源对象实现该上层类,统一在获取资源逻辑完成过滤能力。

场景三:新增功能标识参数

应用商店业务主要提供用户发现和下载新应用、更新手机已安装的应用。商店有增量更新可以减小更新包体积,因此也叫省流量更新,有效提升用户体验。前期我们使用开源的增量算法,但是发现该算法在部分机器合成拆分包会耗时很长,甚至引起crash。

于是项目组寻求更加高效拆分算法。类似在这些已有接口的进行功能增强的场景,除了提供新的API或者内部简单通过客户端版本判断进行扩展外,有没有更好的方案呢?因为除了这些方案已知的弊端外,需要从长远考虑,比如前面提到的算法,后续还会不会存在升级的可能,下载接口会不会有更多能力的增强。

结合上面思考,在原来接口基础上新增标志参数字段,表示该请求发出的客户端支持的能力。为了后续扩展,字段类型为整数值,不只是简单的boolean,服务端通过位运算完成判断逻辑。客户端也摆脱某个功能与版本的强一致性,不用去记录某个版本具有某种能力。

四、关于接口设计的更多思考

最后补充一些踩过的坑和反思。服务端在提供接口时,不能仅仅关注接口的实现,更多的时候需要关注接口的使用方,他们使用的场景、调用时机等等。否则开发在对接口问题排查、维护花费的时间会比实际开发的耗时要多上好几倍。

1)场景化:具体到什么是场景化呢,拿商店客户端的帮助用户检测手机安装的应用版本是否最新的服务举例,检测时机是存在不同的场景的,比如用户启动、用户切换wlan环境、定时检测等。当需要进行精细化分析,哪些请求是有效的,哪些会引起集中请求时,这个时候如果请求上没有场景区分,那么分析将无从下手。所以在与客户端沟通接口设计时,请带上场景这个因素。接口设计上可参考如/app/{scene}/upgrade,定义好各个场景名称,在路径上带上具体的场景,这样对线上不同来源请求量级、问题分析都会有很大好处。

2)鉴权和服务隔离:除了场景需要考虑外,接口调用在分配时做好记录和鉴权以及服务隔离。比如商店的部分接口服务不仅提供给客户端,同时也会提供给手机系统应用调用。目前vivo上亿的存量用户体量,这里要十分小心,系统应用的调用量控制不当,并发可比商店本身要大的多。首先前期与服务调用方评估沟通、做好设计,避免出问题。即使在出问题时,也要有机制能够快速发现问题、能够分析出问题的来源,降低问题带来的损失。

至此上面解决问题的思路,都与具体业务以及背景有一定关系。随着技术不断迭代和发展,在移动端APP页面动态性,目前业界也有了更多高效的技术方案,比如谷歌的Flutter、Weex等。这些技术能够实现灵活扩展,多端统一,性能也能够接近native。不仅减少了客户端发版频次,也减少了服务端兼容性处理成本。目前我们vivo也有团队在使用和实践。

技术不断更迭,没有最好的方案,只有最适合的方案。开发过程中不仅满足当前实现,更多的是考虑到后续扩展性和可维护性。开发不能一味追求高端技术,技术最终服务于业务,坚持长期主义,效率至上。

五、参考资料

1、The Costs of Versioning an API

2、敏捷开发,火车发布模式

本文转载自: 掘金

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

SpringCloud升级之路20200x版-44避免

发表于 2021-11-29

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

本系列代码地址:github.com/JoJoTec/spr…

我们在这一节首先分析下 Spring Cloud Gateway 一些其他可能丢失链路信息的点,之后来做一些可以避免链路信息丢失的设计,之后基于这个设计去实现我们需要的一些定制化的 GlobalFilter

Spring Cloud Gateway 其他的可能丢失链路信息的点

经过前面的分析,我们可以看出,不止这里,还有其他地方会导致 Spring Cloud Sleuth 的链路追踪信息消失,这里举几个大家常见的例子:

1.在 GatewayFilter 中指定了异步执行某些任务,由于线程切换了,并且这时候可能 Span 已经结束了,所以没有链路信息,例如:

1
2
3
4
5
6
7
kotlin复制代码@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
return chain.filter(exchange).publishOn(Schedulers.parallel()).doOnSuccess(o -> {
//这里就没有链路信息了
log.info("success");
});
}

2.将 GatewayFilter 中继续链路的 chain.filter(exchange) 放到了异步任务中执行,上面的 AdaptCachedBodyGlobalFilter 就属于这种情况,这样会导致之后的 GatewayFilter 都没有链路信息,例如:

1
2
3
4
kotlin复制代码@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
return Mono.delay(Duration.ofSeconds(1)).then(chain.filter(exchange));
}

Java 并发编程模型与 Project Reactor 编程模型的冲突思考

Java 中的很多框架,都用到了 ThreadLocal,或者通过 Thread 来标识唯一性。例如:

  • 日志框架中的 MDC,一般都是 ThreadLocal 实现。
  • 所有的锁、基于 AQS 的数据结构,都是通过 Thread 的属性来唯一标识谁获取到了锁的。
  • 分布式锁等数据结构,也是通过 Thread 的属性来唯一标识谁获取到了锁的,例如 Redisson 中分布式 Redis 锁的实现。

但是放到 Project Reactor 编程模型,这就显得格格不入了,因为 Project Reactor 异步响应式编程就是不固定线程,没法保证提交任务和回调能在同一个线程,所以 ThreadLocal 的语义在这里很难成立。Project Reactor 虽然提供了对标 ThreadLocal 的 Context,但是主流框架还没有兼容这个 Context,所以给 Spring Cloud Sleuth 粘合这些链路追踪带来了很大困难,因为 MDC 是一个 ThreadLocal 的 Map 实现,而不是基于 Context 的 Map。这就需要 Spring Cloud Sleuth 在订阅一开始,就需要将链路信息放入 MDC,同时还需要保证运行时不切换线程。

运行不切换线程,这样其实限制了 Project Reactor 的灵活调度,是有一些性能损失的。我们其实想尽量就算加入了链路追踪信息,也不用强制运行不切换线程。但是 Spring Cloud Sleuth 是非侵入式设计,很难实现这一点。但是对于我们自己业务的使用,我们可以定制一些编程规范,来保证大家写的代码不丢失链路信息。

可以从哪里获取当前请求的 Span

Spring Cloud Sleuth 的链路信息核心即 Span,在之前的源码分析中,我们知道,在入口的 WebFilter 中,TraceWebFilter 生成 Span 并将其放入本次 HTTP 请求响应抽象的 ServerWebExchange 的 attributes 中:

TraceWebFilter.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
kotlin复制代码protected static final String TRACE_REQUEST_ATTR = Span.class.getName();
private Span findOrCreateSpan(Context c) {
Span span;
AssertingSpan assertingSpan = null;
//如果当前 Reactor 的上下文中有 Span,就用这个 Span
if (c.hasKey(Span.class)) {
Span parent = c.get(Span.class);
try (Tracer.SpanInScope spanInScope = this.tracer.withSpan(parent)) {
span = this.tracer.nextSpan();
}
if (log.isDebugEnabled()) {
log.debug("Found span in reactor context" + span);
}
}
else {
//如果当前请求中本身包含 span 信息,就用这个 span 启动一个新的子 span
if (this.span != null) {
try (Tracer.SpanInScope spanInScope = this.tracer.withSpan(this.span)) {
span = this.tracer.nextSpan();
}
if (log.isDebugEnabled()) {
log.debug("Found span in attribute " + span);
}
}
//从当前所处的上下文中获取 span
span = this.spanFromContextRetriever.findSpan(c);
//没获取到就新生成一个
if (this.span == null && span == null) {
span = this.handler.handleReceive(new WrappedRequest(this.exchange.getRequest()));
if (log.isDebugEnabled()) {
log.debug("Handled receive of span " + span);
}
}
else if (log.isDebugEnabled()) {
log.debug("Found tracer specific span in reactor context [" + span + "]");
}
assertingSpan = SleuthWebSpan.WEB_FILTER_SPAN.wrap(span);
//将 span 放入 `ServerWebExchange` 的 attributes 中
this.exchange.getAttributes().put(TRACE_REQUEST_ATTR, assertingSpan);
}
if (assertingSpan == null) {
assertingSpan = SleuthWebSpan.WEB_FILTER_SPAN.wrap(span);
}
return assertingSpan;
}

这样可以看出,我们在编写 GlobalFilter 的时候可以通过读取 ServerWebExchange 的 attributes 获取当前链路信息的 Span。但是 TRACE_REQUEST_ATTR 是 protected 的,我们可以下面这个工具类将其暴露出来。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
scala复制代码package com.github.jojotech.spring.cloud.apigateway.common;

import org.springframework.cloud.sleuth.CurrentTraceContext;
import org.springframework.cloud.sleuth.Tracer;
import org.springframework.cloud.sleuth.http.HttpServerHandler;
import org.springframework.cloud.sleuth.instrument.web.TraceWebFilter;

public class TraceWebFilterUtil extends TraceWebFilter {

public static final String TRACE_REQUEST_ATTR = TraceWebFilter.TRACE_REQUEST_ATTR;

//仅仅为了暴露 TraceWebFilter 的 TRACE_REQUEST_ATTR 使用的工具类
private TraceWebFilterUtil(Tracer tracer, HttpServerHandler handler, CurrentTraceContext currentTraceContext) {
super(tracer, handler, currentTraceContext);
}
}

下一节,我们将继续讲解避免链路信息丢失做的设计,主要针对获取到现有 Span 之后,如何保证每个 GlobalFilter 都能保持链路信息。

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

本文转载自: 掘金

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

JavaWeb——ServletContext对象及使用

发表于 2021-11-29

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

什么是ServletContext

Web容器在启动时,它会为每个Web应用程序都创建一个对应的ServletContext对象,这个对象代表当前Web应用。并且它被所有客户端共享(比如你在淘宝登陆了,那你的登陆信息会被带到很多页面,就是跳到其他页面发现也是登陆上的)。

ServletContext的应用

共享数据

多个Servlet通过ServletContext对象实现数据共享

在这里插入图片描述

  • 添加属性:setAttribute(String name, Object obj);
  • 得到值:getAttribute(String name),这个方法返回Object
  • 删除属性:removeAttribute(String name)

测试:

首先创建一个放置数据的类

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.cheng.servlet;

import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;

public class HelloServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
ServletContext context = this.getServletContext();
String username = "万里顾一程";//数据
context.setAttribute("username","万里顾一程");//将一个数据保存在ServletContext中,username=万里顾一程
}
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
doGet(req, resp);
}
}

注册Servlet

1
2
3
4
5
6
7
8
xml复制代码<servlet>
<servlet-name>hello</servlet-name>
<servlet-class>com.cheng.servlet.HelloServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>hello</servlet-name>
<url-pattern>/hello</url-pattern>
</servlet-mapping>

然后创建一个接收数据的类

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复制代码package com.cheng.servlet;

import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

public class GetServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
ServletContext context = this.getServletContext();
String attribute = (String) context.getAttribute("username");//返回值,并类型强转为String
resp.setContentType("text/html");
resp.setCharacterEncoding("utf-8");
resp.getWriter().print("名字="+attribute);
}

@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
doGet(req, resp);
}
}

注册Servlet

1
2
3
4
5
6
7
8
xml复制代码<servlet>
<servlet-name>hello1</servlet-name>
<servlet-class>com.cheng.servlet.GetServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>hello1</servlet-name>
<url-pattern>/hello1</url-pattern>
</servlet-mapping>

启动Tomcat开始测试:

1.启动HelloServlet放置数据到ServletContext中

2.启动GetServlet从ServletContext中获得数据

在这里插入图片描述

获取配置的初始化参数

在web.xml里配置的初始化参数可以用getInitParameter()方法获取

1
2
3
4
xml复制代码    <context-param>
<param-name>url</param-name>
<param-value>jdbc:mysql://localhost:3306/mybatis</param-value>
</context-param>

写一个实现类

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.cheng.servlet;

import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

public class ServletDemon03 extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
ServletContext context = this.getServletContext();
String url = context.getInitParameter("url");
resp.getWriter().print(url);
}

@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
doGet(req, resp);
}
}

注册Servlet

1
2
3
4
5
6
7
8
xml复制代码 <servlet>
<servlet-name>hello3</servlet-name>
<servlet-class>com.cheng.servlet.ServletDemon03</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>hello3</servlet-name>
<url-pattern>/hello3</url-pattern>
</servlet-mapping>

运行Tomcat,查看结果

在这里插入图片描述

请求转发

RequestDispatcher 代表请求的派发者。

Forward是指内部转发。当一个Servlet处理请求的时候,它可以决定自己不继续处理,而是转发给另一个Servlet处理。

例如,我们已经编写了一个能处理/hello3的ServletDemon03,继续编写一个能处理/hello4的ServletDemon04,ServletDemon04在收到浏览器的请求后,它并不自己发送响应,而是把请求和响应都转发给路径为/hello3的ServletDemon03,后续请求的处理实际上是由ServletDemon03完成的。这种处理方式称为转发(Forward)

流程图如下所示:

在这里插入图片描述

编些一个实现请求转发的实现类

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复制代码package com.cheng.servlet;

import javax.servlet.RequestDispatcher;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

/**
* @User len
* @Classname com.cheng.servlet.ServletDemon04
* @Project javaweb-01-servlet
* @Description TODO
* @Author wpcheng
* @Create 2021-02-06-18:36
* @Version 1.0
*/
public class ServletDemon04 extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
ServletContext context = this.getServletContext();
//RequestDispatcher 代表请求的派发者, 只能使用绝对路径
RequestDispatcher dispatcher = context.getRequestDispatcher("/hello3");//请求的是地址为/hello3的资源,因此该请求是由路径为/hello3的servlet实现的
dispatcher.forward(req,resp);//内部转发
}

@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
doGet(req, resp);
}
}

注册路径为/hello4的servlet

1
2
3
4
5
6
7
8
xml复制代码    <servlet>
<servlet-name>hello4</servlet-name>
<servlet-class>com.cheng.servlet.ServletDemon04</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>hello4</servlet-name>
<url-pattern>/hello4</url-pattern>
</servlet-mapping>

注册路径为/hello3的servlet

1
2
3
4
5
6
7
8
xml复制代码<servlet>
<servlet-name>hello3</servlet-name>
<servlet-class>com.cheng.servlet.ServletDemon03</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>hello3</servlet-name>
<url-pattern>/hello3</url-pattern>
</servlet-mapping>

启动Tomcat查看转发结果

在这里插入图片描述

==注:转发是在Web服务器内部完成的,对浏览器来说,它只发出了一个HTTP请求,浏览器的地址栏路径仍然是/hello4,浏览器并不知道该请求在Web服务器内部实际上做了一次转发。==

读取资源文件

类路径:即发布到Tomcat服务器后的ClassPath路径,在classes目录下

Properties可以用来保存属性集(类似Map, 以键值的形式保存数据,不同的是Properties都是String类型的)。这个类的优势是可以从流中获得属性集,或者把属性集报错到流中。

获取配置文件的信息

1
2
properties复制代码username=root
password=123456

实现类

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

import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.InputStream;
import java.util.Properties;

public class ServletDemon05 extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
// “ / ”代表了当前工程的根目录
InputStream is = this.getServletContext().getResourceAsStream("/WEB-INF/classes/jdbc.properties");
Properties prop = new Properties();
prop.load(is);//下载文件流
String user = prop.getProperty("username");//通过键获取属性
String pwd = prop.getProperty("password");
resp.getWriter().println(user);
resp.getWriter().println(pwd);
}

@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
doGet(req, resp);
}
}

注册servlet

1
2
3
4
5
6
7
8
xml复制代码<servlet>
<servlet-name>hello5</servlet-name>
<servlet-class>com.cheng.servlet.ServletDemon05</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>hello5</servlet-name>
<url-pattern>/hello5</url-pattern>
</servlet-mapping>

实现

在这里插入图片描述

本文转载自: 掘金

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

1…126127128…956

开发者博客

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