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

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


  • 首页

  • 归档

  • 搜索

Air实现Go程序实时热重载

发表于 2021-10-29

今天我们要介绍一个神器——Air能够实时监听项目的代码文件,在代码发生变更之后自动重新编译并执行,大大提高gin框架项目的开发效率。

为什么要进行实时的加载?

平常在使用Python进行Django和Flask开发的时候,我们编写完代码,程序能够自动重新加载并执行(live-reload),这在日常的开发阶段是十分方便的。

在使用Go语言的gin框架在本地做开发调试的时候,经常需要在变更代码之后频繁的按下Ctrl+C停止程序并重新编译再执行,这样就不是很方便。

Air介绍

怎样才能在基于gin框架开发时实现实时加载功能呢?像这种烦恼肯定不会只是你一个人的烦恼,所以我报着肯定有现成轮子的心态开始了全网大搜索。果不其然就在Github上找到了一个工具:Air。它支持以下特性:

  1. 彩色日志输出
  2. 自定义构建或二进制命令
  3. 支持忽略子目录
  4. 启动后支持监听新目录
  5. 更好的构建过程

安装

Go (通用的安装方式)

1
bash复制代码go get -u github.com/cosmtrek/air

MacOS

1
bash复制代码curl -fLo air https://git.io/darwin_air

Linux

1
bash复制代码curl -fLo air https://git.io/linux_air

Windows

1
bash复制代码curl -fLo air.exe https://git.io/windows_air

Docker

1
2
3
4
5
6
7
bash复制代码docker run -it --rm \
-w "<PROJECT>" \
-e "air_wd=<PROJECT>" \
-v $(pwd):<PROJECT> \
-p <PORT>:<APP SERVER PORT> \
cosmtrek/air
-c <CONF>

然后按照下面的方式在docker中运行你的项目:

1
2
3
4
5
bash复制代码docker run -it --rm \
-w "/go/src/github.com/cosmtrek/hub" \
-v $(pwd):/go/src/github.com/cosmtrek/hub \
-p 9090:9090 \
cosmtrek/air

Air的使用

为了敲命令时更简单更方便,你应该把alias air='~/.air'加到你的.bashrc或.zshrc中。
首先进入项目的根目录:

1
bash复制代码cd /your_project

最简单的用法就是直接执行下面的命令:

1
2
bash复制代码# 首先在当前目录下查找 `.air.conf`配置文件,如果找不到就使用默认的
air -c .air.conf

推荐的使用方法是:

1
2
3
4
5
python复制代码# 1. 在当前目录创建一个新的配置文件.air.conf
touch .air.conf
# 2. 复制 `air.conf.example` 中的内容到这个文件,然后根据你的需要去修改它
# 3. 使用你的配置运行 air, 如果文件名是 `.air.conf`,只需要执行 `air`。
air

air_example.conf示例

完整的air_example.conf示例配置如下,可以根据自己的需要修改。

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
bash复制代码# [Air](https://github.com/cosmtrek/air) TOML 格式的配置文件

# 工作目录
# 使用 . 或绝对路径,请注意 `tmp_dir` 目录必须在 `root` 目录下
root = "."
tmp_dir = "tmp"

[build]
# 只需要写你平常编译使用的shell命令。你也可以使用 `make`
# Windows平台示例: cmd = "go build -o tmp\main.exe ."
cmd = "go build -o ./tmp/main ."
# 由`cmd`命令得到的二进制文件名
# Windows平台示例:bin = "tmp\main.exe"
bin = "tmp/main"
# 自定义执行程序的命令,可以添加额外的编译标识例如添加 GIN_MODE=release
# Windows平台示例:full_bin = "tmp\main.exe"
full_bin = "APP_ENV=dev APP_USER=air ./tmp/main"
# 监听以下文件扩展名的文件.
include_ext = ["go", "tpl", "tmpl", "html"]
# 忽略这些文件扩展名或目录
exclude_dir = ["assets", "tmp", "vendor", "frontend/node_modules"]
# 监听以下指定目录的文件
include_dir = []
# 排除以下文件
exclude_file = []
# 如果文件更改过于频繁,则没有必要在每次更改时都触发构建。可以设置触发构建的延迟时间
delay = 1000 # ms
# 发生构建错误时,停止运行旧的二进制文件。
stop_on_error = true
# air的日志文件名,该日志文件放置在你的`tmp_dir`中
log = "air_errors.log"

[log]
# 显示日志时间
time = true

[color]
# 自定义每个部分显示的颜色。如果找不到颜色,使用原始的应用程序日志。
main = "magenta"
watcher = "cyan"
build = "yellow"
runner = "green"

[misc]
# 退出时删除tmp目录
clean_on_exit = true

博主使用的效果

image.png

image.png

好了,现在就开始在自己的项目中使用Air,感受实时重新加载的便利吧。

本文转载自: 掘金

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

小公司java服务devops的几种方案

发表于 2021-10-29

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

devops这个概念无疑是解决开发运维之间的鸡零狗碎小矛盾的良方,但针对lz所在的小公司而言,还是开发占据了决策的主导地位,也承担了更多的责任。所以在此文中做一下我们小公司的一体化运维的方案

当然不管是什么时期,不管是什么部署方案,都逃不了如下几个步骤

  • 程序打包(分不同运行环境)
1
go复制代码譬如:maven package
  • 上传程序包至运行服务器对应路径下,一般是某个tomcat的webapps下
1
bash复制代码譬如:mv /**.jar /tomcat/webapps/**.jar
  • 运行或重启tomcat
1
2
3
4
5
bash复制代码譬如:
# 先停止服务
kill -9 ***
# 再启动服务
sh /tomcat/bin/startup.sh
  • 查看对应的启动日志,及其启动情况
1
bash复制代码tail -f /tomcat/logs/catalina.out

主要分为以下几个时期:

手动发包

这个时期,完完整整滴对应了上面的几个环节,只不过就是有点麻烦,麻烦,麻烦

具体操作:

  • 手动打包
  • 手动传递
  • 手动操作tomcat服务
    优点:适合新手熟悉linux平台
    缺点:但每个环节都需要自己手动的调整一些参数,而且每次都需要很多重复性的工作,一不小心的话可能就得重来

jenkins脚本及定时任务

服务直接对接了代码管理平台,代码提交/手动触发版本迭代,固定的活动固定的环境

具体操作:

  • 搭建配置jenkins服务
  • 每个服务对应一个活动
  • Maven、Gradle、Ant脚本打包
  • shell脚本远程复制、重启服务
    优点:适合在固定的服务器下,譬如请求数量稳定的生产服务或者小型的测试服务,不同测试环境针对不同分支代码,便于业务测试和专业测试,对于开发人员更加友好
    缺点:扩容或收缩服务时,细节操作过多,工作量大

开发平台流水线+K8S平台

代码管理平台打包,制作镜像,kubernetes编排管理docker容器、负载均衡,这个时期将运维和开中间的问题解决了很大一部分,主要归功于虚拟化容器平台,省去了繁琐的配置,不用双方来回甩锅,也不用为了一个小小的低级问题去排查半天,从某一方面极大的提升了生产力

具体操作:

  • 平台侧程序打包
  • 集成dockerfile构建镜像,上传至镜像仓库
  • 运行docker容器
    优点:开发侧和运维侧的清晰划分,省去细节配置,避免低级问题,平滑扩容
    缺点:维护工作量大,开发侧需考虑更多部署策略

个人看法

没有最好的,只有最合适的

一个服务就部署一次,那就手动发

一个团队做个服务频繁走测试,那就jenkins

一个服务要在不同的时间节点扛住不同的压力,或者要明确划分开发运维体系,那就k8s

本文转载自: 掘金

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

【算法学习】1221 分割平衡字符串(java / c /

发表于 2021-10-29
  • 本文已参与「掘力星计划」,赢取创作大礼包,挑战创作激励金。

非常感谢你阅读本文~
欢迎【👍点赞】【⭐收藏】【📝评论】~
放弃不难,但坚持一定很酷~
希望我们大家都能每天进步一点点~
本文由 二当家的白帽子 https://juejin.cn/user/2771185768884824/posts 博客原创~


  1. 分割平衡字符串:

在一个 平衡字符串 中,’L’ 和 ‘R’ 字符的数量是相同的。

给你一个平衡字符串 s,请你将它分割成尽可能多的平衡字符串。

注意:分割得到的每个字符串都必须是平衡字符串,且分割得到的平衡字符串是原平衡字符串的连续子串。

返回可以通过分割得到的平衡字符串的 最大数量 。

样例 1

1
2
3
4
5
6
arduino复制代码输入:
s = "RLRRLLRLRL"
输出:
4
解释:
s 可以分割为 "RL"、"RRLL"、"RL"、"RL" ,每个子字符串中都包含相同数量的 'L' 和 'R' 。

样例 2

1
2
3
4
5
6
arduino复制代码输入:
s = "RLLLLRRRLR"
输出:
3
解释:
s 可以分割为 "RL"、"LLLRRR"、"LR" ,每个子字符串中都包含相同数量的 'L' 和 'R' 。

样例 3

1
2
3
4
5
6
ini复制代码输入:
s = "LLLLRRRR"
输出:
1
解释:
s 只能保持原样 "LLLLRRRR".

样例 4

1
2
3
4
5
6
arduino复制代码输入:
s = "RLRRRLLRLL"
输出:
2
解释:
s 可以分割为 "RL"、"RRRLLRLL" ,每个子字符串中都包含相同数量的 'L' 和 'R' 。

提示

  • 1 <= s.length <= 1000
  • s[i] = ‘L’ 或 ‘R’
  • s 是一个 平衡 字符串

分析

  • 这道算法题如果说s本身不是平衡字符串就会有点难度。
  • 由于s本身就是平衡字符串,所以从左到右可以使用贪心,越短则可以分的越多,而且剩下的字符串也是平衡字符串。
  • 我们需要记录遍历过程中‘L’和‘R’的数量,如果相等就是一个平衡字符串。
  • 分别记录‘L’和‘R’的数量需要2个变量,但是我们可以记录他们的差值,差为0就说明数量相等,也就是平衡字符串了,这样就只需要一个变量来做记录。

题解

java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
java复制代码class Solution {
public int balancedStringSplit(String s) {
int ans = 0;
// 记录左和右的差值,左减,右加,为0表示左右数量一样
int v = 0;
int n = s.length();
for (int i = 0; i < n; ++i) {
if (s.charAt(i) == 'L') {
--v;
} else {
++v;
}
if (v == 0) {
++ans;
}
}
return ans;
}
}

c

1
2
3
4
5
6
7
8
9
10
11
12
13
c复制代码int balancedStringSplit(char * s){
int ans = 0;
// 记录左和右的差值,左减,右加,为0表示左右数量一样
int v = 0;
while (*s) {
*s == 'L' ? --v : ++v;
if (v == 0) {
++ans;
}
++s;
}
return ans;
}

c++

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
cpp复制代码class Solution {
public:
int balancedStringSplit(string s) {
int ans = 0;
// 记录左和右的差值,左减,右加,为0表示左右数量一样
int v = 0;
for (char c : s) {
c == 'L' ? --v : ++v;
if (v == 0) {
++ans;
}
}
return ans;
}
};

python

1
2
3
4
5
6
7
8
9
10
11
12
13
python复制代码class Solution:
def balancedStringSplit(self, s: str) -> int:
ans = 0
# 记录左和右的差值,左减,右加,为0表示左右数量一样
v = 0
for c in s:
if c == 'L':
v -= 1
else:
v += 1
if v == 0:
ans += 1
return ans

go

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
go复制代码func balancedStringSplit(s string) int {
ans := 0
// 记录左和右的差值,左减,右加,为0表示左右数量一样
v := 0
for _, c := range s {
if c == 'L' {
v--
} else {
v++
}
if v == 0 {
ans++
}
}
return ans
}

rust

1
2
3
4
5
6
7
8
9
10
11
rust复制代码impl Solution {
pub fn balanced_string_split(s: String) -> i32 {
s.chars().scan(0, |v, c| {
match c {
'L' => *v -= 1,
_ => *v += 1
};
Some(*v)
}).filter(|v| { *v == 0 }).count() as i32
}
}

在这里插入图片描述


原题传送门:https://leetcode-cn.com/problems/split-a-string-in-balanced-strings/


欢迎在评论区讨论,掘金官方将在掘力星计划活动结束后,在评论区抽送100份掘金周边,抽奖详情见活动文章


本文转载自: 掘金

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

网络诊断工具Ping详解

发表于 2021-10-29

ping定义:

ping是工作在「TCP/IP」网络体系结构中应用层的一个计算机网络工具,用来测试数据包能否通过IP协议到达特定主机。

ping的运作原理是向目标主机传出一个ICMP的请求回显数据包(ICMP的TYPE为8),并等待接收回显回应数据包(ICMP的TYPE为0)。程序会按时间和成功响应的次数估算丢失数据包率(丢包率)和数据包往返时间(网络时延,Round-trip delay time)。

由于ping是使用ICMP协议来实现的,所以接下来我们就详细了解一下ICMP。

ICMP协议介绍

ICMP就是互联网控制报文协议,网络包在复杂的网络环境传输时,常常遇到各种各样的问题。当遇到问题的时候,会返回具体的原因。

所以ICMP的功能主要有两种,确认IP包是否成功到底目标地址,通知在发送过程中IP被丢弃的原因。

image.png

ICMP报文是封装在IP包里的,一个ICMP报文包括IP报头,ICMP报头和ICMP报文,若IP报头中的协议字段为1时,说明是一个ICMP报文。

  • 类型:占一个字节,标识ICMP报文的类型,不同类型有不同的代码,它有分为两大类,一类为差错报文,一类为查询报文。
  • 代码:与类型字段一起标识ICMP报文的详细类型。
  • 校验和:检验报文在传输过程中是否出现差错,和IP报头中的校验和是一样的。

类型、代码对应关系

type code 说明
0 0 回显应答(ping 应答)
3 0 网络不可达
3 1 主机不可达
3 2 协议不可达
3 3 端口不可达
3 4 需要进行分片,但设置不分片位
3 5 源站选路失败
3 6 目的网络未知
3 7 目的主机未知
3 9 目的网络被强制禁止
3 10 目的主机被强制禁止
3 11 由于服务类型 TOS,网络不可达
3 12 由于服务类型 TOS,主机不可达
3 13 由于过滤,通信被强制禁止
3 14 主机越权
3 15 优先中止失效
4 0 源端被关闭(基本流控制)
5 0 对网络重定向
5 1 对主机重定向
5 2 对服务类型和网络重定向
5 3 对服务类型和主机重定向
8 0 回显请求(ping 请求)
9 0 路由器通告
10 0 路由器请求
11 0 传输期间生存时间为 0
11 1 在数据报组装期间生存时间为 0
12 0 坏的 IP 首部
12 1 缺少必需的选项
13 0 时间戳请求
14 0 时间戳应答
17 0 地址掩码请求
18 0 地址掩码应答

查询报文类型

ping就是查询报文,一种主动的请求,并且获得主动应答的ICMP协议。ping发起请求命令,会发送一个ICMP Echo Request给对端,请求得到回复,就叫ICMP Echo Reply,它对比原生的ICMP,多了两个字段,一个是标识符,一个是序号。并且ping命令会统计响应时间和TTL(生存周期)来计算往还时间,说明路程的长短。

假定我们在一个子网内,主机A的IP为192.168.6.1,主机B的IP为192.168.6.2,在主机A运行ping 192.168.6.2

  1. ping命令执行的时候,源主机会构建一个ICMP请求的数据包。里头有两个重要的字段,就是我们前面说的标识符和序号,对于请求数据包而言该字段的类型为8(回送请求),另一个就是顺序号,区分连续ping的时候发出的多个数据包,每发出一个请求数据包,顺序号会自动加1,并且会在报文的数据部分插入发送时间。
  2. ICMP协议将这个数据包连同192.168.6.2交给IP层,IP层将会以192.168.6.2作为目标地址,本机IP地址作为源地址,还有一些控制信息构建为一个IP数据包
  3. 加入MAC头,先判断一下是不是在一个子网内,如果是就在ARP映射表中查找IP地址192.168.6.2对应的MAC地址,则可以直接使用。如果没有,就要发送ARP协议查询MAC地址,获取MAC地址后。构建一个数据帧在附加上一些控制信息,依据以太网的介质访问规则,将数据包发送出去。
  4. 192.168.6.2收到这个数据帧后,检查下MAC目标地址,和本机的MAC地址对比,符合就接收,不符丢弃。检查该数据帧,将IP数据包从帧中提取出来,交给本机IP层,IP层检查完后,有用信息提取后交给ICMP协议。
  5. 192.168.6.2收到后,会构建一个ICMP应答包,应答的类型字段为0,顺序号为接收到的请求包的序号,发给主机192.168.6.1
  6. 在规定时限内,源主机也就是192.168.6.1没有接到ICMP的应答包,就说明目标主机不可达,如果接收到ICM应答包,则说明目标主机可达。然后检查当前时刻减去该数据最初从源主机发出的时刻,也就是ICMP数据包的时间延迟。

注意:有很多中间设备都是禁止ping的,并不是说网络不通。这个时候我们就要使用其他协议来检查网络是否通畅,如telnet等

差错报文类型

这是ICMP协议的另一个报文种类,差错报文中常用的类型主要有5种。终点不可达、端口不可达,源点抑制,超时,参数问题,改变路由(重定向)

  1. 假定主机A给主机B发送一个邮件,网络传输中要经过很多台路由器,若R1接到邮件后,发生了故障,不知道下一步该发给哪个路由设备或那台主机设备,这封邮件也就没法到达主机B,这时路由R1就会把数据包丢弃并向主机A发回一个终点不可达的数据报文。这时数据报文,类型为「3」,表示是不可达的错误类型。
  2. 目标系统收到一个IP数据报某个服务请求时,本地没有此服务,会向源头返回ICMP端口不可达信息,类型为「3」。
  3. 假定主机A给主机B发送一个视频,主机A传输非常快,主机B接收慢的话,可能导致数据都拥塞在某一台设备上,这台设备解决不了拥塞问题,会给源主机发送一个源点抑制报文反馈给源主机,告诉源主机数据拥塞了,在发送就会丢数据,告诉他放慢发送数据。类型为「4」。
  4. 如果超过网络包生存周期还是没到,就是超时,主机A给主机B发送一个数据报文时,该数据报文在网络会有一个TTL生存时间字段,没经过一台路由,TTL字段就会减1,路由器收到数据报文后TTL字段值减1后为0的话,该路由器就会把数据包丢弃并向主机A发送一个ICMP超时报文,类型为「11」。
  5. 当路由器后目标追收到数据报的首部中有的字段值不正确时,就会丢弃数据报,向源点发送参数问题的报文,类型为「12」
  6. 路由器改变路由报文发送给主机,让主机知道下次应将数据报文发送给另外的路由器。类型为「5」

另外,ping工具使用ICMP协议,Traceroute也是使用ICMP实现的网络检测跟踪工具,主要使用ICMP差错报文及TTL的方式,后续详细说明

本文转载自: 掘金

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

一张图彻底搞懂Spring循环依赖

发表于 2021-10-29

1 什么是循环依赖?

如下图所示:

file

BeanA类依赖了BeanB类,同时BeanB类又依赖了BeanA类。这种依赖关系形成了一个闭环,我们把这种依赖关系就称之为循环依赖。同理,再如下图的情况:

file

上图中,BeanA类依赖了BeanB类,BeanB类依赖了BeanC类,BeanC类依赖了BeanA类,如此,也形成了一个依赖闭环。再比如:

file

上图中,自己引用了自己,自己和自己形成了依赖关系。同样也是一个依赖闭环。那么,如果出现此类循环依赖的情况,会出现什么问题呢?

2 循环依赖问题复现

2.1 定义依赖关系

我们继续扩展前面的内容,给ModifyService增加一个属性,代码如下:

1
2
3
4
5
6
7
8
9
java复制代码
@GPService
public class ModifyService implements IModifyService {

@GPAutowired private QueryService queryService;

...

}

给QueryService增加一个属性,代码如下:

1
2
3
4
5
6
7
8
9
10
java复制代码
@GPService
@Slf4j
public class QueryService implements IQueryService {

@GPAutowired private ModifyService modifyService;

...

}

如此,ModifyService依赖了QueryService,同时QueryService也依赖了ModifyService,形成了依赖闭环。那么这种情况下会出现什么问题呢?

2.2 问题复现

我们来运行调试一下之前的代码,在GPApplicationContext初始化后打上断点,我们来跟踪一下IoC容器里面的情况,如下图:

file

启动项目,我们发现只要是有循环依赖关系的属性并没有自动赋值,而没有循环依赖关系的属性均有自动赋值,如下图所示:

file

这种情况是怎么造成的呢?我们分析原因之后发现,因为,IoC容器对Bean的初始化是根据BeanDefinition循环迭代,有一定的顺序。这样,在执行依赖注入时,需要自动赋值的属性对应的对象有可能还没初始化,没有初始化也就没有对应的实例可以注入。于是,就出现我们看到的情况。

3 使用缓存解决循环依赖问题

file

3.1 定义缓存

具体代码如下:

1
2
3
4
5
6
7
8
9
java复制代码
// 循环依赖的标识---当前正在创建的实例bean
private Set<String> singletonsCurrectlyInCreation = new HashSet<String>();

//一级缓存
private Map<String, Object> singletonObjects = new HashMap<String, Object>();

// 二级缓存: 为了将成熟的bean和纯净的bean分离. 避免读取到不完整的bean.
private Map<String, Object> earlySingletonObjects = new HashMap<String, Object>();

3.2 判断循环依赖

增加getSingleton()方法:

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
java复制代码
/**
* 判断是否是循环引用的出口.
* @param beanName
* @return
*/
private Object getSingleton(String beanName,GPBeanDefinition beanDefinition) {

//先去一级缓存里拿,
Object bean = singletonObjects.get(beanName);
// 一级缓存中没有, 但是正在创建的bean标识中有, 说明是循环依赖
if (bean == null && singletonsCurrentlyInCreation.contains(beanName)) {

bean = earlySingletonObjects.get(beanName);
// 如果二级缓存中没有, 就从三级缓存中拿
if (bean == null) {
// 从三级缓存中取
Object object = instantiateBean(beanName,beanDefinition);

// 然后将其放入到二级缓存中. 因为如果有多次依赖, 就去二级缓存中判断. 已经有了就不在再次创建了
earlySingletonObjects.put(beanName, object);


}
}
return bean;
}

3.3 添加缓存

修改getBean()方法,在getBean()方法中添加如下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
java复制代码
//Bean的实例化,DI是从而这个方法开始的
public Object getBean(String beanName){

//1、先拿到BeanDefinition配置信息
GPBeanDefinition beanDefinition = regitry.beanDefinitionMap.get(beanName);

// 增加一个出口. 判断实体类是否已经被加载过了
Object singleton = getSingleton(beanName,beanDefinition);
if (singleton != null) { return singleton; }

// 标记bean正在创建
if (!singletonsCurrentlyInCreation.contains(beanName)) {
singletonsCurrentlyInCreation.add(beanName);
}

//2、反射实例化newInstance();
Object instance = instantiateBean(beanName,beanDefinition);

//放入一级缓存
this.singletonObjects.put(beanName, instance);

//3、封装成一个叫做BeanWrapper
GPBeanWrapper beanWrapper = new GPBeanWrapper(instance);
//4、执行依赖注入
populateBean(beanName,beanDefinition,beanWrapper);
//5、保存到IoC容器
factoryBeanInstanceCache.put(beanName,beanWrapper);

return beanWrapper.getWrapperInstance();

}

3.4 添加依赖注入

修改populateBean()方法,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
java复制代码
private void populateBean(String beanName, GPBeanDefinition beanDefinition, GPBeanWrapper beanWrapper) {

...

try {

//ioc.get(beanName) 相当于通过接口的全名拿到接口的实现的实例
field.set(instance,getBean(autowiredBeanName));
} catch (IllegalAccessException e) {
e.printStackTrace();
continue;
}
...

}

4 循环依赖对AOP创建代理对象的影响

4.1 循环依赖下的代理对象创建过程

我们都知道Spring AOP、事务等都是通过代理对象来实现的,而事务的代理对象是由自动代理创建器来自动完成的。也就是说Spring最终给我们放进容器里面的是一个代理对象,而非原始对象。

这里我们结合循环依赖,再分析一下AOP代理对象的创建过程和最终放进容器内的动作,看如下代码:

1
2
3
4
5
6
7
8
9
10
11
12
java复制代码
@Service
public class MyServiceImpl implements MyService {
@Autowired
private MyService myService;

@Transactional
@Override
public Object hello(Integer id) {
return "service hello";
}
}

此Service类使用到了事务,所以最终会生成一个JDK动态代理对象Proxy。刚好它又存在自己引用自己的循环依赖的情况。跟进到Spring创建Bean的源码部分,来看doCreateBean()方法:

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
java复制代码
protected Object doCreateBean( ... ){

...

// 如果允许循环依赖,此处会添加一个ObjectFactory到三级缓存里面,以备创建对象并且提前暴露引用
// 此处Tips:getEarlyBeanReference是后置处理器SmartInstantiationAwareBeanPostProcessor的一个方法,
// 主要是保证自己被循环依赖的时候,即使被别的Bean @Autowire进去的也是代理对象
// AOP自动代理创建器此方法里会创建的代理对象

// Eagerly cache singletons to be able to resolve circular references
// even when triggered by lifecycle interfaces like BeanFactoryAware.
boolean earlySingletonExposure = (mbd.isSingleton() &&
this.allowCircularReferences &&
isSingletonCurrentlyInCreation(beanName));
if (earlySingletonExposure) { // 需要提前暴露(支持循环依赖),注册一个ObjectFactory到三级缓存
addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, bean));
}

// 如果发现自己被循环依赖,会执行上面的getEarlyBeanReference()方法,从而创建一个代理对象从三级缓存转移到二级缓存里
// 注意此时候对象还在二级缓存里,并没有在一级缓存。并且此时可以知道exposedObject仍旧是原始对象 populateBean(beanName, mbd, instanceWrapper);
exposedObject = initializeBean(beanName, exposedObject, mbd);

// 经过这两大步后,exposedObject还是原始对象
// 注意:此处是以事务的AOP为例
// 因为事务的AOP自动代理创建器在getEarlyBeanReference()创建代理后,
// initializeBean() 就不会再重复创建了,二选一,下面会有详细描述)

...

// 循环依赖校验(非常重要)
if (earlySingletonExposure) {
// 前面讲到因为自己被循环依赖了,所以此时候代理对象还存放在二级缓存中
// 因此,此处getSingleton(),就会把代理对象拿出来
// 然后赋值给exposedObject对象并返回,最终被addSingleton()添加进一级缓存中
// 这样就保证了我们容器里缓存的对象实际上是代理对象,而非原始对象

Object earlySingletonReference = getSingleton(beanName, false);
if (earlySingletonReference != null) {

// 这个判断不可少(因为initializeBean()方法中给exposedObject对象重新赋过值,否则就是是两个不同的对象实例)
if (exposedObject == bean) {
exposedObject = earlySingletonReference;
}
}
...
}

}

以上代码分析的是代理对象有自己存在循环依赖的情况,Spring用三级缓存很巧妙的进行解决了这个问题。

4.2 非循环依赖下的代理对象创建过程

如果自己并不存在循环依赖的情况,Spring的处理过程就稍微不同,继续跟进源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
java复制代码
protected Object doCreateBean( ... ) {
...

addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, bean));

...

// 此处注意,因为没有循环引用,所以上面getEarlyBeanReference()方法不会执行
// 也就是说此时二级缓存里并不会存在
populateBean(beanName, mbd, instanceWrapper);

// 重点在此
//AnnotationAwareAspectJAutoProxyCreator自动代理创建器此处的postProcessAfterInitialization()方法里,会给创建一个代理对象返回
// 所以此部分执行完成后,exposedObject() 容器中缓存的已经是代理对象,不再是原始对象
// 此时二级缓存里依旧无它,更别提一级缓存了
exposedObject = initializeBean(beanName, exposedObject, mbd);

...

// 循环依赖校验
if (earlySingletonExposure) {
// 前面讲到一级、二级缓存里都没有缓存,然后这里传参数是false,表示不从三级缓存中取值
// 因此,此时earlySingletonReference = null ,并直接返回

// 然后执行addSingleton()方法,由此可知,容器里最终存在的也还是代理对象

Object earlySingletonReference = getSingleton(beanName, false);
if (earlySingletonReference != null) {
if (exposedObject == bean) {
exposedObject = earlySingletonReference;
}
}
...
}

根据以上代码分析可知,只要用到代理,没有被循环引用的,最终存在Spring容器里缓存的仍旧是代理对象。如果我们关闭Spring容器的循环依赖,也就是把allowCircularReferences设值为false,那么会不会出现问题呢?先关闭循环依赖开关。

1
2
3
4
5
6
7
8
9
10
11
12
java复制代码
// 它用于关闭循环引用(关闭后只要有循环引用现象将报错)
@Component
public class MyBeanFactoryPostProcessor implements BeanFactoryPostProcessor {

@Override
public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {

((AbstractAutowireCapableBeanFactory) beanFactory).setAllowCircularReferences(false);

}
}

关闭循环依赖后,上面代码中存在A、B循环依赖的情况,运行程序会出现如下异常:

1
2
3
4
java复制代码
Caused by: org.springframework.beans.factory.BeanCurrentlyInCreationException: Error creating bean with name 'a': Requested bean is currently in creation: Is there an unresolvable circular reference?
at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.beforeSingletonCreation(DefaultSingletonBeanRegistry.java:339)
at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:215)

此处异常类型也是BeanCurrentlyInCreationException异常,但报错位置在DefaultSingletonBeanRegistry.beforeSingletonCreation

我们来分析一下,在实例化A后给其属性赋值时,Spring会去实例化B。B实例化完成后会继续给B属性赋值,由于我们关闭了循环依赖,所以不存在提前暴露引用。因此B无法直接拿到A的引用地址,只能又去创建A的实例。而此时我们知道A其实已经正在创建中了,不能再创建了。所有就出现了异常。对照演示代码,来分析一下程序运行过程:

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

// 因为关闭了循环依赖,所以此处不能再依赖自己
// 但是MyService需要创建AOP代理对象
//@Autowired
//private MyService myService;

@Transactional
@Override
public Object hello(Integer id) {
return "service hello";
}
}

其大致运行步骤如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
java复制代码
protected Object doCreateBean( ... ) {

// earlySingletonExposure = false 也就是Bean都不会提前暴露引用,因此不能被循环依赖

boolean earlySingletonExposure = (mbd.isSingleton() &&
this.allowCircularReferences &&
isSingletonCurrentlyInCreation(beanName));
...

populateBean(beanName, mbd, instanceWrapper);

// 若是开启事务,此处会为原生Bean创建代理对象
exposedObject = initializeBean(beanName, exposedObject, mbd);

if (earlySingletonExposure) {
...

// 因为上面没有提前暴露代理对象,所以上面的代理对象exposedObject直接返回。

}
}

由上面代码可知,即使关闭循环依赖开关,最终缓存到容器中的对象仍旧是代理对象,显然@Autowired给属性赋值的也一定是代理对象。

最后,以AbstractAutoProxyCreator为例看看自动代理创建器实现循环依赖代理对象的细节。

AbstractAutoProxyCreator是抽象类,它的三大实现子类InfrastructureAdvisorAutoProxyCreator、AspectJAwareAdvisorAutoProxyCreator、AnnotationAwareAspectJAutoProxyCreator小伙伴们应该比较熟悉,该抽象类实现了创建代理的动作:

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
java复制代码
// 该类实现了SmartInstantiationAwareBeanPostProcessor接口 ,通过getEarlyBeanReference()方法解决循环引用问题

public abstract class AbstractAutoProxyCreator extends ProxyProcessorSupport implements SmartInstantiationAwareBeanPostProcessor, BeanFactoryAware {

...

// 下面两个方法是自动代理创建器创建代理对象的唯二的两个节点:

// 提前暴露代理对象的引用,在postProcessAfterInitialization之前执行
// 创建好后放进缓存earlyProxyReferences中,注意此处value是原始Bean

@Override
public Object getEarlyBeanReference(Object bean, String beanName) {

Object cacheKey = getCacheKey(bean.getClass(), beanName);
this.earlyProxyReferences.put(cacheKey, bean);
return wrapIfNecessary(bean, beanName, cacheKey);

}

// 因为它会在getEarlyBeanReference之后执行,这个方法最重要的是下面的逻辑判断
@Override
public Object postProcessAfterInitialization(@Nullable Object bean, String beanName) {

if (bean != null) {

Object cacheKey = getCacheKey(bean.getClass(), beanName);

// 下面的remove()方法返回被移除的value,也就是原始Bean
// 判断如果存在循环引用,也就是执行了上面的getEarlyBeanReference()方法,
// 此时remove() 返回值肯定是原始对象

// 若没有被循环引用,getEarlyBeanReference()不执行
// 所以remove() 方法返回null,此时进入if执行逻辑,调用创建代理对象方法
if (this.earlyProxyReferences.remove(cacheKey) != bean) {
return wrapIfNecessary(bean, beanName, cacheKey);
}
}

return bean;

}
...
}

根据以上分析可得知,自动代理创建器它保证了代理对象只会被创建一次,而且支持循环依赖的自动注入的依旧是代理对象。由上面分析得出结论,在Spring容器中,不论是否存在循环依赖的情况,甚至关闭Spring容器的循环依赖功能,它对Spring AOP代理的创建流程有影响,但对结果是无影响的。也就是说Spring很好地屏蔽了容器中对象的创建细节,让使用者完全无感知。

本文为“Tom弹架构”原创,转载请注明出处。技术在于分享,我分享我快乐!如果本文对您有帮助,欢迎关注和点赞;如果您有任何建议也可留言评论或私信,您的支持是我坚持创作的动力。关注『 Tom弹架构 』可获取更多技术干货!

本文转载自: 掘金

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

保姆级教程,从概念到实践帮你快速上手 Apache APIS

发表于 2021-10-29

Apache APISIX Ingress 概览

Apache APISIX Ingress 定义

在 K8s 生态中,Ingress 作为表示 K8s 流量入口的一种资源,想要让其生效,就需要有一个 Ingress Controller 去监听 K8s 中的 Ingress 资源,并对这些资源进行相应规则的解析和实际承载流量。在当下趋势中,像 Kubernetes Ingress Nginx 就是使用最广泛的 Ingress Controller 实现。

而 APISIX Ingress 则是另一种 Ingress Controller 的实现。跟 Kubernetes Ingress Nginx 的区别主要在于 APISIX Ingress 是以 Apache APISIX 作为实际承载业务流量的数据面。如下图所示,当用户请求到具体的某一个服务/API/网页时,通过外部代理将整个业务流量/用户请求传输到 K8s 集群,然后经过 APISIX Ingress 进行后续处理。

图片

从上图可以看到,APISIX Ingress 分成了两部分。一部分是 APISIX Ingress Controller,作为控制面它将完成配置管理与分发。另一部分 APISIX Proxy Pod 负责承载业务流量,它是通过 CRD(Custom Resource Definitions) 的方式实现的。Apache APISIX Ingress 除了支持自定义资源外,还支持原生的 K8s Ingress 资源。

Apache APISIX 简述

前边我们提到了 APISIX Ingress 是采用 Apache APISIX 作为实际承载业务流量的数据面,那么 Apache APISIX 项目又是做什么的呢?

图片

Apache APISIX 是 Apache 基金会旗下的顶级开源项目,也是当前最活跃的开源网关项目。作为一个动态、实时、高性能的开源 API 网关,Apache APISIX 提供了负载均衡、动态上游、灰度发布、服务熔断、身份认证、可观测性等丰富的流量管理功能。

Apache APISIX 可以帮助企业快速、安全地处理 API 和微服务流量,比如限流认证、日志安全功能,以及支持丰富的自定义插件。目前也与很多开源项目如 Apache SkyWalking、Prometheus 等之类的组件进行了相关集成。

APISIX Ingress vs K8s Ingress Nginx

因为本人同时参与到了 APISIX Ingress 与 K8s Ingress Nginx 两个项目的开发和维护,所以很多人也会问我,这两个项目做比较的话,到底该如何选择?或者说为什么有了 K8s Ingress Nginx 还要再做 APISIX Ingress。

配置层面

在 APISIX Ingress 中,我们增加了一些丰富且灵活的配置,比如通过单个配置文件去实现灰度部署。但在 K8s Ingress Nginx 中去实现如上效果的话,最少也需要有两个 Ingress 资源文件才可以完成。

丰富度

在丰富度上,由于 Apache APISIX 本身的自带功能丰富且允许多种插件扩展使用,所以使用 APISIX Ingress 就可以省去自己额外配置功能的繁琐步骤,可以将更多的时间投入到实际开发中。

架构分离

APISIX Ingress 采用了数据面与控制面的分离架构,所以用户可以选择将数据面部署在 K8s 集群内部/外部。但 K8s Ingress Nginx 是将控制面和数据面放在了同一个 Pod 中,如果 Pod 或控制面出现一点闪失,整个 Pod 就会挂掉,进而影响到业务流量。

这种架构分离,给用户提供了比较方便的部署选择,同时在业务架构调整场景下,也方便进行相关数据的迁移与使用。

APISIX Ingress 特性详解

由于 Apache APISIX 是一个全动态的高性能网关,所以在 APISIX Ingress 自身就支持了全动态,包括路由、SSL 证书、上游以及插件等等。

同时 APISIX Ingress 还具有以下特性:

  • 支持 CRD,更容易理解声明式配置;同时状态检查可保证快速掌握声明配置的同步状态
  • 支持高级路由匹配规则以及自定义资源,可与 Apache APISIX 官方 50 多个插件 & 客户自定义插件进行扩展使用
  • 支持 K8s 原生 Ingress 配置
  • 支持流量切分
  • 支持 gRPC plaintext 与 TCP 4 层代理
  • 服务自动注册发现,无惧扩缩容
  • 更灵活的负载均衡策略,自带健康检查功能

以下我们将从 CRD 与自定义资源层面进行详细的介绍。

CRD 扩展

在前面的介绍中我们提到了 CRD,那么 APISIX Ingress 是如何使用 CRD 扩展的?

图片

从用户层面来看,当 Client 发起请求,到达 Apache APISIX 后,会直接把相应的业务流量传输到后端(如 Service Pod),从而完成转发过程。此过程不需要经过 Ingress Controller,这样做可以保证一旦有问题出现,或者是进行变更、扩缩容或者迁移处理等,都不会影响到用户和业务流量。

同时在配置端,用户通过 kubectl apply,可将自定义 CRD 配置应用到 K8s 集群。Ingress Controller 会持续 watch 这些资源变更,来将相应配置应用到 Apache APISIX。

自定义资源

APISIX Ingress 目前已经支持的自定义资源主要是以下 5 类,涉及到路由、上游、消费者、证书相关和集群公共配置的相关类别。

APISIX Route(路由)

自定义资源 APISIX Route 中 spec 属性的顶级配置是 http。但其实 spec 是同时支持两种配置的,一种是下图示例的 spec.http,主要用于 7 层代理;另一种是 spec.stream,用于 4 层代理。在配置文件中,我们首先为其自定义了一项规则,即 match 下的相关参数。

图片

如上图后端配置示例使用了同一个 Service,实际使用中大家根据场景进行调整即可。需要注意的是,weight 属性是用来配置相关 Service 权重。通过以上配置,从而实现一套完整的路由自定义资源。

APISIX Upstream(上游)

在配置 APISIX Upstream 时,需要注意 name 的内容要与 K8s 集群的 Service 保持一致,这样可以保证后续 APISIX Ingress Controller 准确匹配其相应流量。

图片

在配置文件中,sspec.loadbalancer 主要负责负载均衡策略的设置,有多种策略模式可供选择。spec.scheme 则是协议类型的配置,目前只支持 HTTP 和 gRPC 协议。spec.healthCheck 主要是对健康检查功能进行设置,比如设置其活跃状态、生效协议与路径和最终反馈等参数配置。

APISIX Consumer(消费者)

在 APISIX Consumer 配置中,主要是增加了认证相关的功能,比如 spec.authParameter,目前该配置参数支持 BasicAuth 与 KeyAuth 这两种比较常见的认证类型。

图片

通过 value 可直接去配置相关的 username 和 password,或者直接使用 secret 进行配置,相比前者的明文配置会更安全一些。

APISIX TLS(证书)

APISIX TLS 主要是为了进行证书的管理。如示例所示,用户可以通过 hosts 来配置多个域名,secret 下的参数就是对应的配置证书。

图片

同时 APISIX TLS 还配有 spec.client,用于进行 mTLS 双向认证的配置。

APISIX Config 相关

关于自定义资源支持的 Config 类型我们会从两个方面进行描述。

一种是 APISIX Cluster Config,它主要用于一些通用配置。目前支持在 K8s 或者 Apache APISIX 中全局使用 Prometheus 插件/全局配置 SkyWalking,后续开发中也会去增加一些其他的通用配置。

图片

另一种就是我们现在正在 PR 中的 APISIX Plugin Config。大家如果感兴趣的话,也可以点击链接来一起参与讨论。Plugin Config 主要是将通用的插件配置统一集合在一起,比如一些同样的配置,用户就可以通过 APISIX Plugin Config 同时应用在多个路由当中,省去了额外多项独立配置的繁琐步骤。

APISIX Ingress 上手实践

目前大家可以通过 Helm Charts 的方式来进行 APISIX Ingress 的部署。通过一条命令,就可以同时把 Apache APISIX 以及 APISIX Ingress,包括 Apache APISIX 所需要用到的 etcd 全部部署好,步骤非常简单。

图片

实践场景一:流量切分

通过使用 APISIX Ingress 可以实现按比例进行流量切分的效果,具体操作如下:

步骤一:配置 APISIX Upstream

图片

步骤二:配置 APISIX Route

通过在 backends 中去配置 subset 和 weight,来实现用户请求流量进入时的分流。如下图示例就是 90% 的流量会进入到 v1 中,10% 的流量进入到 v2 中。

图片

通过以上两步,就可以十分方便地按比例进行流量切分,实现类似灰度发布等场景需求。

实践场景二:配置认证

如果想在 APISIX Ingress 中为某些路由配置 Basic Auth,可以参考如下操作:

步骤一:创建 APISIX Consumer 资源

如前文所提到的,可以在 APISIX Consumer 配置中增加 basicAuth,并为其指定用户名和密码。

图片

步骤二:配置 APISIX Route,增加相关参数

在自定义资源 APISIX Route 中,通过在后端添加 authentication,将其开启并指定认证类型即可。

图片

通过以上步骤,就可以实现使用 Consumer 去完成相关配置认证。

实践场景三:K8s 资源扩展

正如我们在开头提到过的,APISIX Ingress 不仅支持自定义资源,还同时支持 K8s 原生的 Ingress 资源。

图片

如上图是 K8s Ingress 资源。通常情况下如果想要在资源上做 rewrite,可以通过增加 annotation 配置属性。这样当用户携带 httpbin.org 请求时,就可以通过路径 /sample 将它重定向到 /ip。

当上述需求使用 APISIX Ingress 时,只需在 Ingress 增加一个 kubernetes.io/ingress.class:apisix,去指定 APISIX Ingress Controller 去监听这个资源,同时通过配置 k8s.apisix.apache.org/rewrite-target:"/ip",就可以完成重定向到 /ip 路径。

图片

以上示例只是目前 APISIX Ingress 对于原生 K8s Ingress 支持的一种方式,更多示例大家可以查看具体文档

(apisix.apache.org/docs/ingres…) 进行参考使用。

未来规划

之后 APISIX Ingress 将会继续在功能与生态上进行更新,目前阶段已经完成了 APISIX Ingress 与 Cert-manager 集成,后续将逐步实现以下目标:

  1. 完成 Kubernetes V1.22+ 与 CRD V1 版本的适配支持(已经完成,即将在 APISIX Ingress V1.3 版本 中发布)
  2. 支持 Gateway API(预计在 Q4 阶段实现)
  3. 扩展新架构,以便于用户在不需要使用 etcd 的情况下,可以正常使用 APISIX Ingress
  4. 丰富产品生态,扩展 APISIX Ingress 社区

最后也希望大家能够多多地参与到项目中来,比如每两周的周三下午 2 点都会有一次 APISIX Ingress 社区会议,会跟大家同步一下当前的项目进展或者遇到的问题。大家可以持续关注 Apache APISIX 视频号,届时可以直接参与社区会议直播。

关于作者

张晋涛,Apache APISIX Committer、Kubernetes Ingress Nginx Reviewer,多个云原生开源项目的贡献者。

关于 Apache APISIX

Apache APISIX 是一个动态、实时、高性能的开源 API 网关,提供负载均衡、动态上游、灰度发布、服务熔断、身份认证、可观测性等丰富的流量管理功能。Apache APISIX 可以帮忙企业快速、安全的处理 API 和微服务流量,包括网关、Kubernetes Ingress 和服务网格等。

Apache APISIX 落地用户(仅部分)
图片

  • Apache APISIX GitHub:github.com/apache/apis…
  • Apache APISIX 官网:apisix.apache.org/
  • Apache APISIX 文档:apisix.apache.org/zh/docs/api…

本文转载自: 掘金

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

为什么Spring不推荐使用Autowired注解?

发表于 2021-10-29

引言

在实际工作中,使用IDEA开发时,很多码友都喜欢使用@Autowired注解进行依赖注入,这个时候 IDEA 就会报黄色警告,代码一片warning,代码洁癖的我不允许这么一个不明不白的警告在这里。@Autowired作为Spring的亲儿子,为啥在IDEA中提示了这么一个警告?所以,带着我的洁癖,和我的好奇心,开始研究起了这个警告。

我们简单翻译一下自动提示的是啥意思:

不建议直接在字段上进行依赖注入。Spring开发团队建议:在Java Bean中永远使用构造方法进行依赖注入。

带着上面的疑问,我们接着往下看

依赖注入的方式

Spring有三种依赖注入的方式

基于属性(filed)注入

这种注入方式就是在 bean 的变量上使用注解进行依赖注入。本质上是通过反射的方式直接注入到 field 。这是我平常开发中看的最多也是最熟悉的一种方式。比如:

1
2
java复制代码@Autowired
UserService userService;

基于set方法注入

通过对应变量的setXXX()方法以及在方法上面使用注解,来完成依赖注入。比如:

1
2
3
4
5
6
java复制代码private UserService userService;

@Autowired
public void setUserService(UserService userService) {
this.userService = userService;
}

说明:在 Spring 4.5 及更高的版本中,setXXX 上面的 @Autowired 注解是可以不写的。

基于构造器注入

将各个必需的依赖全部放在带有注解构造方法的参数中,并在构造方法中完成对应变量的初始化,这种方式,就是基于构造方法的注入。比如:

1
2
3
4
5
6
java复制代码private final UserService userService;

@Autowired
public UserController(UserService userService) {
this.userService = userService;
}

属性注入的问题

如你所见,变量(filed)注入的方式是如此的简洁。但实际上他是有一些问题的,具体问题如下:

问题一

1
2
3
4
5
6
7
8
java复制代码@Autowired
private UserService userService;

private String company;

public UserServiceImpl() {
this.company = userService.getCompany();
}

编译过程不会报错,但是运行之后报NullPointerException

1
java复制代码Instantiation of bean failed; nested exception is org.springframework.beans.BeanInstantiationException: Failed to instantiate [...]: Constructor threw exception; nested exception is java.lang.NullPointerException

Java 在初始化一个类时,是按照静态变量或静态语句块 –> 实例变量或初始化语句块 –> 构造方法 -> @Autowired 的顺序。所以在执行这个类的构造方法时,user对象尚未被注入,它的值还是 null。

问题二

不能有效的指明依赖。相信很多人都遇见过一个 bug,依赖注入的对象为 null,在启动依赖容器时遇到这个问题都是配置的依赖注入少了一个注解什么的。这种方式就过于依赖注入容器了,当没有启动整个依赖容器时,这个类就不能运转,在反射时无法提供这个类需要的依赖。

问题三

依赖注入的核心思想之一就是被容器管理的类不应该依赖被容器管理的依赖,换成白话来说就是如果这个类使用了依赖注入的类,那么这个类摆脱了这几个依赖必须也能正常运行。然而使用变量注入的方式是不能保证这点的。

怎么解决?

我们一般开发需要注入属性的时候都会使用到这三个注解@Autowired、@Inject、@Resource,这三个注解在 Spring 中也是支持只用的。我们先来看一下这三个注解有啥区别?

@Autowired

@Autowired为 Spring 框架提供的注解,可以理解是 Spring 的亲儿子。这里先给出一个示例代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
java复制代码public interface IndexService {

void sayHello();
}

@Service
public class IndexServiceImpl implements IndexService {

@Override
public void sayHello() {
System.out.println("hello, this is IndexServiceImpl");
}
}

@Service
public class IndexServiceImpl2 implements IndexService {

@Override
public void sayHello() {
System.out.println("hello, this is IndexServiceImpl2");
}
}

测试方法

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

@Autowired
// @Qualifier("indexServiceImpl2")
IndexService indexService;

@Test
void gooo() {
Assertions.assertNotNull(indexService);
indexService.sayHello();
}

}

这里说一下匹配 bean 的过程:

  1. 按照type在上下文中查找匹配,查找type为IndexService的 bean。
  2. 如果有多个 bean,则按照name进行匹配
* 如果有`@Qualifier`注解,则按照`@Qualifier`指定的`name`进行匹配,查找`name`为`indexServiceImpl2`的 bean。
* 如果没有,则按照变量名进行匹配。查找`name`为`indexService`的 bean 。
  1. 匹配不到,则报错。@Autowired(required=false),如果设置required为 false (默认为 true ),则注入失败时不会抛出异常。

@Inject

在 Spring 的环境下,@Inject 和 @Autowired 是相同的,因为它们的依赖注入都是使用AutowiredAnnotationBeanPostProcessor这个后置处理器来处理的。

这两个的区别,首先 @Inject 是 Java EE 包里的,在SE环境需要单独引入。另一个区别在于 @Autowired 可以设置 required=false 而 @Inject 并没有这个属性。也有的说 @Inject 是 spring 的干儿子。

@Resource

@Resource 是JSR-250定义的注解。Spring 在 CommonAnnotationBeanPostProcessor 实现了对JSR-250的注解的处理,其中就包括 @Resource。

这个@Resource有 2 个属性name和type。在 spring 中name属性定义为 bean 的名字,type这是 bean 的类型。如果属性上加 @Resource 注解那么他的注入流程是:

  1. 如果同时指定了name和type,则从 Spring 上下文中找到唯一匹配的 bean 进行装配,找不到则抛出异常。
  2. 如果指定了name,则从上下文中查找名称匹配的 bean 进行装配,找不到则抛出异常。
  3. 如果指定了type,则从上下文中找到类型匹配的唯一 bean 进行装配,找不到或是找到多个,都会抛出异常。
  4. 如果既没有指定name,又没有指定type,则默认按照byName方式进行装配;如果没有匹配,按照byType进行装配。

所以我们可以使用@Resource替代@Autowired,当然也可以使用@RequiredArgsConstructor构造器方式注入,这种形式就是Spring推荐使用的构造器方式注入,此种方式是lombok包下的注解,如果使用此种方式,需要项目中引入lombok,例如:

1
2
3
4
java复制代码@RequiredArgsConstructor
public class UserDaoImpl {
private final User user;
}

本文转载自: 掘金

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

蓄水池抽样

发表于 2021-10-29

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

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

关注公众号:高性能架构探索。后台回复【pdf】,可以免费领取计算机经典书籍

问题

1、给定一个数据流,数据流长度N很大,且N直到处理完所有数据之前都不可知,请问如何在只遍历一遍数据(O(N))的情况下,能够随机选取出m个不重复的数据

2、在不知道文件行数的情况下,如何在只遍历一遍文件的情况下,随机选取出m行

分析

看到此种问题,我们的第一想法是,把数据流中的数据保存起来,然后通过把数据流中的数据存储起来,然后进行随机获取,我们以leetcode中的某个题目为例,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
arduino复制代码class Solution {
public:
  /** @param head The linked list's head.
      Note that the head is guaranteed to be not null, so it contains at least one node. */
  Solution(ListNode* head) {
      auto h = head;
      while (h) {
          v.push_back(h->val);
          h = h->next;
      }
  }
   
  int getRandom() {
      return v[random() % v.size()];
  }
  private:
    std::vector<int> v;
};

此解法基本上满足了随机获取链表节点值的要求,但是如果再加一个条件,即链表很长,乃至内存中存储不了,此时应该怎么做呢?

解决

这就涉及到蓄水池算法。

蓄水池抽样

蓄水池抽样是一系列随机算法,用于在不替换的情况下,从一个未知大小n的总体中选择一个简单的随机样本(k个项目),只需对这些项目进行一次遍历。总体n的大小对于算法来说是未知的,并且通常对于所有n个项来说都太大而无法放入主内存。随着时间的推移,总体将显示给算法,并且算法不能回顾以前的项目。在任何时候,算法的当前状态必须允许提取一个简单的随机样本,而不替换迄今为止看到的部分总体的大小k。

算法思路大致如下:

  • 如果接收的数据量小于m,则依次放入蓄水池。
  • 当接收到第i个数据时,i >= m,在[0, i]范围内取以随机数d,若d的落在[0, m-1]范围内,则用接收到的第i个数据替换蓄水池中的第d个数据。
  • 重复上一个步骤

证明

为了证明这个解是完全有效的,我们必须证明0<=i<n的任何项流[i]在最终储层[]中的概率是k/n。让我们把证据分为两种情况,因为前k项的处理方式不同。

情况1:对于最后n-k个流项,即,对于流[i],其中k<=i<n

对于每一个这样的流项流[i],我们从0到i选取一个随机索引,如果选取的索引是前k个索引之一,我们将选取索引处的元素替换为流[i]

为了简化证明,让我们先考虑最后一个项目。最后一个项目在最终库中的概率=为最后一个项目选取前k个索引之一的概率=k/n(从大小为n的列表中选取k个项目之一的概率)

现在让我们考虑第二个最后一个项目。最后第二项在最终储层中的概率[]=[在流[n-2]的迭代中选取前k个索引之一的概率]X[在流[n-1]的迭代中选取的索引与在流[n-2]中选取的索引不同的概率]=[k/(n-1)]*[(n-1)/n]=k/n。

类似地,我们可以从流[n-1 ]到流[k]中考虑所有流项的其他项,并推广证明。

情况2:对于前k个流项,即,对于流[i],其中0<=i<k

第一k个项目最初被复制到库[],并且可以在稍后的流[k]到流[n]的迭代中被移除。

来自流[0..k-1]的项目在最终数组中的概率=当项目流[k]、流[k+1]、….时项目未被拾取的概率…。考虑流[n-1]=[k/(k+1)]x[(k+1)/(k+2)]x[(k+2)/(k+3)]x…x[(n-1)/n]=k/n

实现

仍然以leetcode中此题为例,随机获取一个链表中的一个节点值,注意,此处k = 1:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
ini复制代码class Solution {
public:
  Solution(ListNode* head) {
      this->head = head;
  }
   
  int getRandom() {
      ListNode* phead = this->head;
      int val = phead->val;
      int count = 1;
      while (phead){
          if (rand() % count++ == 0)
              val = phead->val;
          phead = phead->next;
      }
      return val;
  }
  ListNode* head;
};

那么 如果要获取链表中的k(k != 1)个值,时候,又该怎么实现呢? 此时,需要遍历链表的前k个节点,将前k个节点的值存储在数组中,然后从第k + 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
ini复制代码class Solution {
public:
  Solution(ListNode* head) {
      this->head = head;
  }
   
  std::vector<int> getRandom(int count) {
      ListNode* phead = this->head;
      int i = 0;
      int k = count;
      std::vector<int> res;
      while (phead && k--) {
        res.push_back(phead->val);
      }
 
      k = count;
      while (phead) {
        int rd = rand() % k++;
        if (rd < k) {
          res[rd] = phead->val;        
        }
 
        phead = phead->next;
      }
      return res;
  }
  ListNode* head;
};

本文转载自: 掘金

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

ZLMediaKit教程(五)支持webrtc

发表于 2021-10-29

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

环境

  • ubuntu 18.04 64bit
  • ZLMediaKit 20210924
  • ffmpeg 3.4.8

webrtc是什么

WebRTC 是 Web Real Time Communication 的缩写,也被称为网络实时通信,是由 Google 和其他一些大公司推动的一个开源项目,它通过 Javascript API 来实现无插件的实时通信。WebRTC 促进了浏览器到浏览器的语音呼叫、视频聊天和文件共享应用的大力发展。目前大多数浏览器已经支持了 webRTC 。

下图是 WebRTC 的架构图

zlmediakit webrtc

  • 最上层的 web app,是 Web 开发者自己开发的程序,主要利用的是 javascript 的 web api
  • web api,WebRTC 标准的 API (javascript 版),由 w3c 来维护
  • 媒体层,这里就笼统来讲,它是 WebRTC 的底层核心部分,包括了音频和视频数据的采集、编码、传输,除此之外,还有会话的管理、点对点的链接等,采用的 C++ 语言编写

实践

安装 openssl,ubuntu 18.04 对应的 openssl 版本是 1.1.1

1
arduino复制代码sudo apt-get install libssl-dev

如果你的操作系统版本比较老的话,也可以从源码开始安装 openssl,命令如下

1
2
3
4
5
bash复制代码git clone https://github.com/openssl/openssl.git
cd openssl
./config
make
sudo make install

然后是安装 libsrtp,即 Secure Realtime Transport Protocol,方法还是熟悉的老三样 configure、make 和 make install

1
2
3
4
5
bash复制代码git clone https://github.com/cisco/libsrtp.git
cd libsrtp
./configure --enable-openssl
make
sudo make install

最后来编译 ZLMediaKit

1
2
3
4
5
6
7
bash复制代码cd ZLMediaKit
mkdir build
cd build

# 加入webrtc的编译选项
cmake .. -DENABLE_WEBRTC=on
make

在 cmake 的时候注意看看输出信息,确保 webrtc 功能已开启

zlmediakit webrtc

编译成功后,生成的可执行文件在 release/linux/MediaServer

接下来,就可以启动服务了

1
复制代码sudo ./MediaServer -d

然后,使用 ffmpeg 推送一个本地视频文件到服务器

1
arduino复制代码ffmpeg -re -i test.mp4 -vcodec h264 -acodec aac -f flv rtmp://127.0.0.1/live/test

完成后,我们打开 chrome 浏览器,输入 http://192.168.1.140,这个 ip 地址是 MediaServer 服务器的地址

zlmediakit webrtc

选择 6 webrtc,这里我们没有证书,所以忽略掉 https 的警告

zlmediakit webrtc

设置下分辨率,点击开始,上方的播放器就开始播放推送的视频了

zlmediakit webrtc

zlmediakit webrtc

关联阅读

  1. ZLMediaKit教程(一)编译安装
  2. ZLMediaKit教程(二)主程序和配置文件
  3. ZLMediaKit教程(三)URL规则
  4. ZLMediaKit教程(四)HTTP API

本文转载自: 掘金

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

CentOS7 yum方式安装MySQL57(简单 好用)

发表于 2021-10-29

CentOS7 yum方式安装MySQL5.7

  1. 下载并安装MySQL官方的 Yum Repository
1
2
3
java复制代码wget http://dev.mysql.com/get/mysql57-community-release-el7-8.noarch.rpm
yum localinstall mysql57-community-release-el7-8.noarch.rpm -y
yum install mysql-community-server mysql -y
  1. MySql初始化及配置
1. 启动: `systemctl start mysqld`
2. 查看状态: `systemctl status mysqld`


![image.png](https://gitee.com/songjianzaina/juejin_p11/raw/master/img/27bbfd7a5edf8baeb08b9b324a85041a3eaf93aeaf0ff4047a5ef4d0a765b2b9)
3. 查看初始密码: `grep "password" /var/log/mysqld.log`
![在这里插入图片描述](https://gitee.com/songjianzaina/juejin_p11/raw/master/img/36db3f1ff44d43a825bbfb41cc32b3fad09cdcd41fbd4206112e472c9d64e898)
4. 登录: mysql -uroot -p'查到的密码'
5. 这个两个设置以后 密码很简单不会报错



1
2
java复制代码set global validate_password_policy=0;        
set global validate_password_length=1;
6. 修改密码: `ALTER USER 'root'@'localhost' IDENTIFIED BY '123456';` 7. 启用远程连接: `grant all privileges on *.* to 'root'@'%' identified by '123456' with grant option;` 8. 刷新权限: flush privileges; 9. 修改默认字符集: vi /etc/my.cnf
1
2
3
4
5
6
7
8
9
java复制代码[mysqld]
default-storage-engine=INNODB
character-set-server = utf8mb4
collation-server = utf8mb4_general_ci

[client]
default-character-set=utf8mb4
[mysql]
default-character-set=utf8mb4
10. 重启mysql: `systemctl restart mysqld` 11. 开机启动: `systemctl enable mysqld` 12. 取消使用yum时每次都加载mysql: `yum -y remove mysql57-community-release-el7-8.noarch`

本文转载自: 掘金

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

1…455456457…956

开发者博客

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