diff --git a/2020/08/19/docker-frequently-used-commands/index.html b/2020/08/19/docker-frequently-used-commands/index.html index 768ad943..ea51d2e3 100644 --- a/2020/08/19/docker-frequently-used-commands/index.html +++ b/2020/08/19/docker-frequently-used-commands/index.html @@ -27,7 +27,7 @@ - + @@ -162,7 +162,7 @@
- 16 + 15 标签
@@ -237,7 +237,7 @@

更新于 - + diff --git a/2023/05/27/how-to-install-clash-on-ubuntu/index.html b/2023/05/27/how-to-install-clash-on-ubuntu/index.html index dc89c6be..c8295c28 100644 --- a/2023/05/27/how-to-install-clash-on-ubuntu/index.html +++ b/2023/05/27/how-to-install-clash-on-ubuntu/index.html @@ -27,7 +27,7 @@ - + @@ -163,7 +163,7 @@
- 16 + 15 标签
@@ -238,7 +238,7 @@

更新于 - + diff --git a/2023/06/07/how-to-setup-OpenVPN-connect-client-on-iOS-and-macOS/index.html b/2023/06/07/how-to-setup-OpenVPN-connect-client-on-iOS-and-macOS/index.html index 210cdbae..20b729df 100644 --- a/2023/06/07/how-to-setup-OpenVPN-connect-client-on-iOS-and-macOS/index.html +++ b/2023/06/07/how-to-setup-OpenVPN-connect-client-on-iOS-and-macOS/index.html @@ -27,9 +27,9 @@ - + - + @@ -162,7 +162,7 @@
- 16 + 15 标签
@@ -237,7 +237,7 @@

更新于 - + @@ -300,7 +300,7 @@

- +
diff --git a/2023/06/07/how-to-setup-OpenVPN-server-on-windows-10/index.html b/2023/06/07/how-to-setup-OpenVPN-server-on-windows-10/index.html index 8f3c5449..921fd058 100644 --- a/2023/06/07/how-to-setup-OpenVPN-server-on-windows-10/index.html +++ b/2023/06/07/how-to-setup-OpenVPN-server-on-windows-10/index.html @@ -27,9 +27,9 @@ - + - + @@ -162,7 +162,7 @@
- 16 + 15 标签
@@ -237,7 +237,7 @@

更新于 - + @@ -305,7 +305,7 @@

- +
diff --git a/2023/06/07/how-to-use-OpenVPN-to-access-home-network/index.html b/2023/06/07/how-to-use-OpenVPN-to-access-home-network/index.html index 1fcd3c07..a2d5519b 100644 --- a/2023/06/07/how-to-use-OpenVPN-to-access-home-network/index.html +++ b/2023/06/07/how-to-use-OpenVPN-to-access-home-network/index.html @@ -27,9 +27,9 @@ - + - + @@ -162,7 +162,7 @@
- 16 + 15 标签
@@ -237,7 +237,7 @@

更新于 - + @@ -332,7 +332,7 @@

- +
diff --git a/2023/06/13/how-to-configure-proxy-for-terminal-docker-and-container/index.html b/2023/06/13/how-to-configure-proxy-for-terminal-docker-and-container/index.html index 432006ca..d4b90754 100644 --- a/2023/06/13/how-to-configure-proxy-for-terminal-docker-and-container/index.html +++ b/2023/06/13/how-to-configure-proxy-for-terminal-docker-and-container/index.html @@ -27,7 +27,7 @@ - + @@ -163,7 +163,7 @@
- 16 + 15 标签
@@ -238,7 +238,7 @@

更新于 - + diff --git a/2023/06/23/how-to-install-Minikube-on-Ubuntu-20-04/index.html b/2023/06/23/how-to-install-Minikube-on-Ubuntu-20-04/index.html index 6e59abae..688a7152 100644 --- a/2023/06/23/how-to-install-Minikube-on-Ubuntu-20-04/index.html +++ b/2023/06/23/how-to-install-Minikube-on-Ubuntu-20-04/index.html @@ -27,7 +27,7 @@ - + @@ -162,7 +162,7 @@
- 16 + 15 标签
@@ -237,7 +237,7 @@

更新于 - + diff --git a/2023/06/24/Ubuntu-server-20-04-not-all-disk-space-was-allocated-after-installation/index.html b/2023/06/24/Ubuntu-server-20-04-not-all-disk-space-was-allocated-after-installation/index.html index 05b0cf8a..31195d43 100644 --- a/2023/06/24/Ubuntu-server-20-04-not-all-disk-space-was-allocated-after-installation/index.html +++ b/2023/06/24/Ubuntu-server-20-04-not-all-disk-space-was-allocated-after-installation/index.html @@ -27,9 +27,9 @@ - + - + @@ -162,7 +162,7 @@
- 16 + 15 标签
@@ -237,7 +237,7 @@

更新于 - + @@ -295,7 +295,7 @@

- +
diff --git a/2023/06/28/how-to-use-ssh-to-connect-github-and-server/index.html b/2023/06/28/how-to-use-ssh-to-connect-github-and-server/index.html index 3e1ec60b..c8b96df7 100644 --- a/2023/06/28/how-to-use-ssh-to-connect-github-and-server/index.html +++ b/2023/06/28/how-to-use-ssh-to-connect-github-and-server/index.html @@ -27,7 +27,7 @@ - + @@ -162,7 +162,7 @@
- 16 + 15 标签
@@ -237,7 +237,7 @@

更新于 - + diff --git a/2023/06/29/tmux-frequently-used-commands/index.html b/2023/06/29/tmux-frequently-used-commands/index.html index 641a6409..9226ed65 100644 --- a/2023/06/29/tmux-frequently-used-commands/index.html +++ b/2023/06/29/tmux-frequently-used-commands/index.html @@ -27,7 +27,7 @@ - + @@ -162,7 +162,7 @@
- 16 + 15 标签
@@ -237,7 +237,7 @@

更新于 - + diff --git a/2023/07/13/Java-class-loader-source-code-analysis/index.html b/2023/07/13/Java-class-loader-source-code-analysis/index.html index 270ee6a3..14bb4033 100644 --- a/2023/07/13/Java-class-loader-source-code-analysis/index.html +++ b/2023/07/13/Java-class-loader-source-code-analysis/index.html @@ -27,10 +27,10 @@ - + - - + + @@ -163,7 +163,7 @@
- 16 + 15 标签
@@ -238,7 +238,7 @@

更新于 - + @@ -471,8 +471,8 @@

- - + +
@@ -484,8 +484,8 @@
- diff --git a/2023/11/17/how-does-Spring-load-beans/Pasted image 20231118213120.png b/2023/08/10/how-does-Spring-load-beans/Pasted image 20231118213120.png similarity index 100% rename from 2023/11/17/how-does-Spring-load-beans/Pasted image 20231118213120.png rename to 2023/08/10/how-does-Spring-load-beans/Pasted image 20231118213120.png diff --git a/2023/11/17/how-does-Spring-load-beans/index.html b/2023/08/10/how-does-Spring-load-beans/index.html similarity index 97% rename from 2023/11/17/how-does-Spring-load-beans/index.html rename to 2023/08/10/how-does-Spring-load-beans/index.html index 86a15f40..e15ec9f9 100644 --- a/2023/11/17/how-does-Spring-load-beans/index.html +++ b/2023/08/10/how-does-Spring-load-beans/index.html @@ -22,25 +22,25 @@ - + - - - + + + - + - + - + Spring Bean 加载过程 | Moralok @@ -165,7 +165,7 @@
- 16 + 15 标签
@@ -204,7 +204,7 @@
- +
-
-
- - -
- -
- - -
-
- -
-
- - -
- -
- - -
-
- -
-
- - -
- -
- - -
-
- -
-
- - -
- -
- - -
-
- -
-
- - -
- -
- - -
-
diff --git a/archives/2023/11/index.html b/archives/2023/11/index.html index 38cea205..ece70fec 100644 --- a/archives/2023/11/index.html +++ b/archives/2023/11/index.html @@ -158,7 +158,7 @@

Moralok

- 16 + 15 标签
@@ -203,26 +203,6 @@

Moralok

2023 -
-
- - -
- -
- - -
-
-
@@ -203,26 +203,6 @@

Moralok

2023 - -
+ +
diff --git a/archives/index.html b/archives/index.html index 36a1de18..343dc736 100644 --- a/archives/index.html +++ b/archives/index.html @@ -158,7 +158,7 @@

Moralok

@@ -203,26 +203,6 @@

Moralok

2023 - -
+ +
diff --git a/css/main.css b/css/main.css index 2e69a1c0..0950d022 100644 --- a/css/main.css +++ b/css/main.css @@ -2641,7 +2641,7 @@ mark.search-keyword { vertical-align: middle; } .links-of-author a::before { - background: #cc0101; + background: #d6c8ff; display: inline-block; margin-right: 3px; transform: translateY(-2px); diff --git a/index.html b/index.html index c183ac63..4c6f4b08 100644 --- a/index.html +++ b/index.html @@ -158,7 +158,7 @@

Moralok

@@ -191,166 +191,6 @@

Moralok

-
- - - -
- - - - - - - -
-

- -

- - -
- - - - -
-

Spring Bean 生命周期

- -

获取 Bean

获取指定 Bean 的入口方法是 getBean,在 Spring 上下文刷新过程中,就依次调用 AbstractBeanFactory#getBean(java.lang.String) 方法获取 non-lazy-init 的 Bean。

-
1
2
3
4
public Object getBean(String name) throws BeansException {
// 具体工作由 doGetBean 完成
return doGetBean(name, null, null, false);
}
- -

deGetBean

作为公共处理逻辑,由 AbstractBeanFactory 自己实现。

-
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
protected <T> T doGetBean(
final String name, final Class<T> requiredType, final Object[] args, boolean typeCheckOnly)
throws BeansException {
// 转换名称:去除 FactoryBean 的前缀 &,将别名转换为规范名称
final String beanName = transformedBeanName(name);
Object bean;

// 检查单例缓存中是否已存在
Object sharedInstance = getSingleton(beanName);
if (sharedInstance != null && args == null) {
// ...
// 如果已存在,直接返回该实例或者使用该实例(FactoryBean)创建并返回对象
bean = getObjectForBeanInstance(sharedInstance, name, beanName, null);
}
else {
// 如果当前 Bean 是一个正在创建中的 prototype 类型,表明可能发生循环引用
// 注意:Spring 并未解决 prototype 类型的循环引用问题,要抛出异常
if (isPrototypeCurrentlyInCreation(beanName)) {
throw new BeanCurrentlyInCreationException(beanName);
}

// 如果当前 beanFactory 没有 bean 定义,去 parent beanFactory 中查找
BeanFactory parentBeanFactory = getParentBeanFactory();
if (parentBeanFactory != null && !containsBeanDefinition(beanName)) {
String nameToLookup = originalBeanName(name);
if (args != null) {
return (T) parentBeanFactory.getBean(nameToLookup, args);
}
else {
return parentBeanFactory.getBean(nameToLookup, requiredType);
}
}

if (!typeCheckOnly) {
// 标记为至少创建过一次
markBeanAsCreated(beanName);
}

try {
final RootBeanDefinition mbd = getMergedLocalBeanDefinition(beanName);
checkMergedBeanDefinition(mbd, beanName, args);

// 确保 bean 依赖的 bean(构造器参数) 都已实例化
String[] dependsOn = mbd.getDependsOn();
if (dependsOn != null) {
for (String dep : dependsOn) {
if (isDependent(beanName, dep)) {
// 注意:Spring 并未解决构造器方法中的循环引用问题,要抛异常
}
// 注册依赖关系,确保先销毁被依赖的 bean
registerDependentBean(dep, beanName);
// 递归,获取依赖的 bean
getBean(dep);
}
}
}

if (mbd.isSingleton()) {
// 如果是单例类型(绝大多数都是此类型)
// 再次从缓存中获取,如果仍不存在,则使用传入的 ObjectFactory 创建
sharedInstance = getSingleton(beanName, new ObjectFactory<Object>(
{
@Override
public Object getObject() throws BeansException {
try {
// 创建 bean
return createBean(beanName, mbd, args);
}
catch (BeansException ex) {
// 由于可能已经提前暴露,需要显示地销毁
destroySingleton(beanName);
throw ex;
}
}
});
bean = getObjectForBeanInstance(sharedInstance, name, beanName, mbd);
}
else if (mbd.isPrototype()) {
// 如果是原型类型,每次都新创建一个
// ...
}
else {
// 如果是其他 scope 类型
// ...
}
}
catch (BeansException ex) {
cleanupAfterBeanCreationFailure(beanName);
throw ex;
}
}
- -

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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
public Object getSingleton(String beanName, ObjectFactory<?> singletonFactory) {
// 加锁
synchronized (this.singletonObjects) {
// 再次从缓存中获取(和调用前从缓存中获取构成双重校验)
Object singletonObject = this.singletonObjects.get(beanName);
if (singletonObject == null) {
if (this.singletonsCurrentlyInDestruction) {
// 如果正在销毁单例,则抛异常
// 注意:不要在销毁方法中调用获取 bean 方法
}
// 创建前,先注册到正在创建中的集合
// 在出现循环引用时,第二次进入 doGetBean,用此作为判断标志
beforeSingletonCreation(beanName);
boolean newSingleton = false;
// ...
try {
// 使用传入的单例工厂创建对象
singletonObject = singletonFactory.getObject();
newSingleton = true;
}
catch (IllegalStateException ex) {
// 如果异常的出现是因为 bean 被创建了,就忽略异常,否则抛出异常
singletonObject = this.singletonObjects.get(beanName);
if (singletonObject == null) {
throw ex;
}
}
catch (BeanCreationException ex) {
// ...
}
finally {
// ...
// 创建后,从正在创建中集合移除
afterSingletonCreation(beanName);
}
if (newSingleton) {
// 添加单例到缓存
addSingleton(beanName, singletonObject);
}
}
return (singletonObject != NULL_OBJECT ? singletonObject : null);
}
}
- -

创建 Bean

createBean 是创建 Bean 的入口方法,由 AbstractBeanFactory 定义,由 AbstractAutowireCapableBeanFactory 实现。

-
1
2
3
4
5
6
7
8
9
10
11
12
13
14
protected Object createBean(String beanName, RootBeanDefinition mbd, Object[] args) throws BeanCreationException {
// ...
try {
// 给 Bean 后置处理器一个返回代理的机会
Object bean = resolveBeforeInstantiation(beanName, mbdToUse);
if (bean != null) {
return bean;
}
}
// ...
// 常规的创建 Bean
Object beanInstance = doCreateBean(beanName, mbdToUse, args);
return beanInstance;
}
- -

doCreateBean

常规的创建 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
protected Object doCreateBean(final String beanName, final RootBeanDefinition mbd, final Object[] args) throws BeanCreationException {
BeanWrapper instanceWrapper = null;
if (mbd.isSingleton()) {
instanceWrapper = this.factoryBeanInstanceCache.remove(beanName);
}
if (instanceWrapper == null) {
// 使用相应的策略创建 bean 实例,例如通过工厂方法或者有参、无参构造器方法
instanceWrapper = createBeanInstance(beanName, mbd, args);
}
final Object bean = (instanceWrapper != null ? instanceWrapper.getWrappedInstance() : null);
Class<?> beanType = (instanceWrapper != null ? instanceWrapper.getWrappedClass() : null);
mbd.resolvedTargetType = beanType;

// ...

// 使用 ObjectFactory 封装实例并缓存,以解决循环引用问题
boolean earlySingletonExposure = (mbd.isSingleton()
&& this.allowCircularReferences
&& isSingletonCurrentlyInCreation(beanName));
if (earlySingletonExposure) {
addSingletonFactory(beanName, new ObjectFactory<Object>() {
@Override
public Object getObject() throws BeansException {
return getEarlyBeanReference(beanName, mbd, bean);
}
});
}

Object exposedObject = bean;
try {
// 填充属性(包括解析依赖的 bean)
populateBean(beanName, mbd, instanceWrapper);
if (exposedObject != null) {
// 初始化 bean
exposedObject = initializeBean(beanName, exposedObject, mbd);
}
}
// ...

// 如有需要,将 bean 注册为一次性的,以供 beanFactory 在关闭时调用销毁方法
try {
registerDisposableBeanIfNecessary(beanName, bean, mbd);
}
// ...

return exposedObject;
}
- -

createBeanInstance

创建 Bean 实例,并使用 BeanWrapper 封装。实例化的方式:

-
    -
  1. 工厂方法
  2. -
  3. 构造器方法
      -
    1. 有参
    2. -
    3. 无参
    4. -
    -
  4. -
-

populateBean

为创建出的实例填充属性,包括解析当前 bean 所依赖的 bean。

-
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
protected void populateBean(String beanName, RootBeanDefinition mbd, BeanWrapper bw) {
PropertyValues pvs = mbd.getPropertyValues();
// ...

// 给 InstantiationAwareBeanPostProcessors 一个机会,
// 在设置 bean 属性前修改 bean 状态,可用于自定义的字段注入
boolean continueWithPropertyPopulation = true;
if (!mbd.isSynthetic() && hasInstantiationAwareBeanPostProcessors()) {
for (BeanPostProcessor bp : getBeanPostProcessors()) {
if (bp instanceof InstantiationAwareBeanPostProcessor) {
InstantiationAwareBeanPostProcessor ibp = (InstantiationAwareBeanPostProcessor) bp;
if (!ibp.postProcessAfterInstantiation(bw.getWrappedInstance(), beanName)) {
continueWithPropertyPopulation = false;
break;
}
}
}
}

// 是否继续填充属性的流程
if (!continueWithPropertyPopulation) {
return;
}

if (mbd.getResolvedAutowireMode() == RootBeanDefinition.AUTOWIRE_BY_NAME
|| mbd.getResolvedAutowireMode() == RootBeanDefinition.AUTOWIRE_BY_TYPE) {
MutablePropertyValues newPvs = new MutablePropertyValues(pvs);
// 根据名称注入
if (mbd.getResolvedAutowireMode() == RootBeanDefinition.AUTOWIRE_BY_NAME) {
autowireByName(beanName, mbd, bw, newPvs);
}

// 根据类型注入
if (mbd.getResolvedAutowireMode() == RootBeanDefinition.AUTOWIRE_BY_TYPE) {
autowireByType(beanName, mbd, bw, newPvs);
}
pvs = newPvs;
}

// 是否存在 InstantiationAwareBeanPostProcessors
boolean hasInstAwareBpps = hasInstantiationAwareBeanPostProcessors();
// 是否需要检查依赖
boolean needsDepCheck = (mbd.getDependencyCheck() != RootBeanDefinition.DEPENDENCY_CHECK_NONE);

if (hasInstAwareBpps || needsDepCheck) {
PropertyDescriptor[] filteredPds = filterPropertyDescriptorsForDependencyCheck(bw, mbd.allowCaching);
if (hasInstAwareBpps) {
// 后置处理 PropertyValues
for (BeanPostProcessor bp : getBeanPostProcessors()) {
if (bp instanceof InstantiationAwareBeanPostProcessor) {
InstantiationAwareBeanPostProcessor ibp = (InstantiationAwareBeanPostProcessor) bp;
pvs = ibp.postProcessPropertyValues(pvs, filteredPds, bw.getWrappedInstance(), beanName);
if (pvs == null) {
return;
}
}
}
}
if (needsDepCheck) {
checkDependencies(beanName, mbd, filteredPds, pvs);
}
}
// 将属性应用到 bean 上(常规情况下,前面的处理都用不上)
applyPropertyValues(beanName, mbd, bw, pvs);
}
- -

initializeBean

在填充完属性后,实例就可以进行初始化工作:

-
    -
  1. invokeAwareMethods,让 Bean 通过 xxxAware 接口感知一些信息
  2. -
  3. 调用 BeanPostProcessor 的 postProcessBeforeInitialization 方法
  4. -
  5. invokeInitMethods,调用初始化方法
  6. -
  7. 调用 BeanPostProcessor 的 postProcessAfterInitialization 方法
  8. -
-
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
protected Object initializeBean(final String beanName, final Object bean, RootBeanDefinition mbd) {
// 处理 Aware 接口的相应方法
if (System.getSecurityManager() != null) {
AccessController.doPrivileged(new PrivilegedAction<Object>() {
@Override
public Object run() {
invokeAwareMethods(beanName, bean);
return null;
}
}, getAccessControlContext());
}
else {
invokeAwareMethods(beanName, bean);
}

// 应用 BeanPostProcessor 的 postProcessBeforeInitialization 方法
Object wrappedBean = bean;
if (mbd == null || !mbd.isSynthetic()) {
wrappedBean = applyBeanPostProcessorsBeforeInitialization(wrappedBean, beanName);
}

try {
// 调用初始化方法
invokeInitMethods(beanName, wrappedBean, mbd);
}
catch (Throwable ex) {
throw new BeanCreationException(
(mbd != null ? mbd.getResourceDescription() : null),
beanName, "Invocation of init method failed", ex);
}

if (mbd == null || !mbd.isSynthetic()) {
// 应用 BeanPostProcessor 的 postProcessAfterInitialization 方法
wrappedBean = applyBeanPostProcessorsAfterInitialization(wrappedBean, beanName);
}
return wrappedBean;
}
- -
处理 Aware 接口的相应方法

让 Bean 在初始化中,感知(获知)和自身相关的资源,如 beanName、beanClassLoader 或者 beanFactory。

-
1
2
3
4
5
6
7
8
9
10
11
12
13
private void invokeAwareMethods(final String beanName, final Object bean) {
if (bean instanceof Aware) {
if (bean instanceof BeanNameAware) {
((BeanNameAware) bean).setBeanName(beanName);
}
if (bean instanceof BeanClassLoaderAware) {
((BeanClassLoaderAware) bean).setBeanClassLoader(getBeanClassLoader());
}
if (bean instanceof BeanFactoryAware) {
((BeanFactoryAware) bean).setBeanFactory(AbstractAutowireCapableBeanFactory.this);
}
}
}
- -
调用初始化方法
    -
  1. 如果 bean 实现 InitializingBean 接口,调用 afterPropertiesSet 方法
  2. -
  3. 如果自定义 init 方法且满足调用条件,同样进行调用
  4. -
-
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
protected void invokeInitMethods(String beanName, final Object bean, RootBeanDefinition mbd) throws Throwable {
// 是否实现 InitializingBean 接口,是的话调用 afterPropertiesSet 方法
// 给 bean 一个感知属性已设置并做出反应的机会
boolean isInitializingBean = (bean instanceof InitializingBean);
if (isInitializingBean
&& (mbd == null || !mbd.isExternallyManagedInitMethod("afterPropertiesSet"))) {
if (System.getSecurityManager() != null) {
try {
AccessController.doPrivileged(new PrivilegedExceptionAction<Object>() {
@Override
public Object run() throws Exception {
((InitializingBean) bean).afterPropertiesSet();
return null;
}
}, getAccessControlContext());
}
catch (PrivilegedActionException pae) {
throw pae.getException();
}
}
else {
((InitializingBean) bean).afterPropertiesSet();
}
}

// 如果存在自定义的 init 方法且方法名称不是 afterPropertiesSet,判断是否调用
if (mbd != null) {
String initMethodName = mbd.getInitMethodName();
if (initMethodName != null
&& !(isInitializingBean && "afterPropertiesSet".equals(initMethodName))
&& !mbd.isExternallyManagedInitMethod(initMethodName)) {
invokeCustomInitMethod(beanName, bean, mbd);
}
}
}
- -
BeanPostProcessor 处理

在调用初始化方法前后,BeanPostProcessor 先后进行两次处理。其实和 BeanPostProcessor 相关的代码都非常相似:

-
    -
  1. 获取 Processor 列表
  2. -
  3. 判断 Processor 类型是否是当前需要的
  4. -
  5. 对 bean 进行处理
  6. -
-
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public Object applyBeanPostProcessorsBeforeInitialization(Object existingBean, String beanName) throws BeansException {

Object result = existingBean;
for (BeanPostProcessor beanProcessor : getBeanPostProcessors()) {
result = beanProcessor.postProcessBeforeInitialization(result, beanName);
if (result == null) {
return result;
}
}
return result;
}

public Object applyBeanPostProcessorsAfterInitialization(Object existingBean, String beanName) throws BeansException {

Object result = existingBean;
for (BeanPostProcessor beanProcessor : getBeanPostProcessors()) {
result = beanProcessor.postProcessAfterInitialization(result, beanName);
if (result == null) {
return result;
}
}
return result;
}
- -

再思 Bean 的初始化

以下代码片段一度让我困惑,从注释看,初始化 Bean 实例的工作包括了 populateBean 和 initializeBean,但是 initializeBean 方法的含义就是初始化 Bean。在 initializeBean 方法中,调用了 invokeInitMethods 方法,其含义仍然是调用初始化方法。
在更熟悉代码后,我有一种微妙的、个人性的体会,在 Spring 源码中,有时候视角的变化是很快的,痕迹是很小的。如果不加以理解和区分,很容易迷失在相似的描述中。以此处为例,“初始化 Bean 和 Bean 的初始化”扩展开来是 “Bean 工厂初始化一个 Bean 和 Bean 自身进行初始化”。

-
1
2
3
4
5
6
7
8
// Initialize the bean instance.
Object exposedObject = bean;
try {
populateBean(beanName, mbd, instanceWrapper);
if (exposedObject != null) {
exposedObject = initializeBean(beanName, exposedObject, mbd);
}
}
- -

在注释这一行,视角是属于 BeanFactory(AbstractAutowireCapableBeanFactory)。从工厂的视角,面对一个刚刚创建出来的 Bean 实例,需要完成两方面的工作:

-
    -
  1. 为 Bean 实例填充属性,包括解析依赖,为 Bean 自身的初始化做好准备。
  2. -
  3. Bean 自身的初始化。
  4. -
-

在万事俱备之后,就是 Bean 自身的初始化工作。由于 Spring 的高度扩展性,这部分并不只是单纯地调用初始化方法,还包含 Aware 接口和 BeanPostProcessor 的相关处理,前者偏属于 Java 对象层面,后者偏属于 Spring Bean 层面。
在认同 BeanPostProcessor 的处理属于 Bean 自身初始化工作的一部分后,@PostConstruct 注解的方法被称为 Bean 的初始化方法也就不那么违和了,因为它的实现原理正是 BeanPostProcessor,这仍然在 initializeBean 的范围内。

- - -
- - - - - -
-
- -
-
-
- - - - - - -
@@ -392,7 +232,7 @@

- + @@ -546,7 +386,7 @@

- + @@ -714,7 +554,7 @@

- + @@ -1147,7 +987,7 @@

- + @@ -1380,7 +1220,7 @@

- + @@ -1580,7 +1420,7 @@

- + @@ -1701,6 +1541,166 @@

+ + + +
+ + + + + + + +
+

+ +

+ + +
+ + + + +
+

Spring Bean 生命周期

+ +

获取 Bean

获取指定 Bean 的入口方法是 getBean,在 Spring 上下文刷新过程中,就依次调用 AbstractBeanFactory#getBean(java.lang.String) 方法获取 non-lazy-init 的 Bean。

+
1
2
3
4
public Object getBean(String name) throws BeansException {
// 具体工作由 doGetBean 完成
return doGetBean(name, null, null, false);
}
+ +

deGetBean

作为公共处理逻辑,由 AbstractBeanFactory 自己实现。

+
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
protected <T> T doGetBean(
final String name, final Class<T> requiredType, final Object[] args, boolean typeCheckOnly)
throws BeansException {
// 转换名称:去除 FactoryBean 的前缀 &,将别名转换为规范名称
final String beanName = transformedBeanName(name);
Object bean;

// 检查单例缓存中是否已存在
Object sharedInstance = getSingleton(beanName);
if (sharedInstance != null && args == null) {
// ...
// 如果已存在,直接返回该实例或者使用该实例(FactoryBean)创建并返回对象
bean = getObjectForBeanInstance(sharedInstance, name, beanName, null);
}
else {
// 如果当前 Bean 是一个正在创建中的 prototype 类型,表明可能发生循环引用
// 注意:Spring 并未解决 prototype 类型的循环引用问题,要抛出异常
if (isPrototypeCurrentlyInCreation(beanName)) {
throw new BeanCurrentlyInCreationException(beanName);
}

// 如果当前 beanFactory 没有 bean 定义,去 parent beanFactory 中查找
BeanFactory parentBeanFactory = getParentBeanFactory();
if (parentBeanFactory != null && !containsBeanDefinition(beanName)) {
String nameToLookup = originalBeanName(name);
if (args != null) {
return (T) parentBeanFactory.getBean(nameToLookup, args);
}
else {
return parentBeanFactory.getBean(nameToLookup, requiredType);
}
}

if (!typeCheckOnly) {
// 标记为至少创建过一次
markBeanAsCreated(beanName);
}

try {
final RootBeanDefinition mbd = getMergedLocalBeanDefinition(beanName);
checkMergedBeanDefinition(mbd, beanName, args);

// 确保 bean 依赖的 bean(构造器参数) 都已实例化
String[] dependsOn = mbd.getDependsOn();
if (dependsOn != null) {
for (String dep : dependsOn) {
if (isDependent(beanName, dep)) {
// 注意:Spring 并未解决构造器方法中的循环引用问题,要抛异常
}
// 注册依赖关系,确保先销毁被依赖的 bean
registerDependentBean(dep, beanName);
// 递归,获取依赖的 bean
getBean(dep);
}
}
}

if (mbd.isSingleton()) {
// 如果是单例类型(绝大多数都是此类型)
// 再次从缓存中获取,如果仍不存在,则使用传入的 ObjectFactory 创建
sharedInstance = getSingleton(beanName, new ObjectFactory<Object>(
{
@Override
public Object getObject() throws BeansException {
try {
// 创建 bean
return createBean(beanName, mbd, args);
}
catch (BeansException ex) {
// 由于可能已经提前暴露,需要显示地销毁
destroySingleton(beanName);
throw ex;
}
}
});
bean = getObjectForBeanInstance(sharedInstance, name, beanName, mbd);
}
else if (mbd.isPrototype()) {
// 如果是原型类型,每次都新创建一个
// ...
}
else {
// 如果是其他 scope 类型
// ...
}
}
catch (BeansException ex) {
cleanupAfterBeanCreationFailure(beanName);
throw ex;
}
}
+ +

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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
public Object getSingleton(String beanName, ObjectFactory<?> singletonFactory) {
// 加锁
synchronized (this.singletonObjects) {
// 再次从缓存中获取(和调用前从缓存中获取构成双重校验)
Object singletonObject = this.singletonObjects.get(beanName);
if (singletonObject == null) {
if (this.singletonsCurrentlyInDestruction) {
// 如果正在销毁单例,则抛异常
// 注意:不要在销毁方法中调用获取 bean 方法
}
// 创建前,先注册到正在创建中的集合
// 在出现循环引用时,第二次进入 doGetBean,用此作为判断标志
beforeSingletonCreation(beanName);
boolean newSingleton = false;
// ...
try {
// 使用传入的单例工厂创建对象
singletonObject = singletonFactory.getObject();
newSingleton = true;
}
catch (IllegalStateException ex) {
// 如果异常的出现是因为 bean 被创建了,就忽略异常,否则抛出异常
singletonObject = this.singletonObjects.get(beanName);
if (singletonObject == null) {
throw ex;
}
}
catch (BeanCreationException ex) {
// ...
}
finally {
// ...
// 创建后,从正在创建中集合移除
afterSingletonCreation(beanName);
}
if (newSingleton) {
// 添加单例到缓存
addSingleton(beanName, singletonObject);
}
}
return (singletonObject != NULL_OBJECT ? singletonObject : null);
}
}
+ +

创建 Bean

createBean 是创建 Bean 的入口方法,由 AbstractBeanFactory 定义,由 AbstractAutowireCapableBeanFactory 实现。

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
protected Object createBean(String beanName, RootBeanDefinition mbd, Object[] args) throws BeanCreationException {
// ...
try {
// 给 Bean 后置处理器一个返回代理的机会
Object bean = resolveBeforeInstantiation(beanName, mbdToUse);
if (bean != null) {
return bean;
}
}
// ...
// 常规的创建 Bean
Object beanInstance = doCreateBean(beanName, mbdToUse, args);
return beanInstance;
}
+ +

doCreateBean

常规的创建 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
protected Object doCreateBean(final String beanName, final RootBeanDefinition mbd, final Object[] args) throws BeanCreationException {
BeanWrapper instanceWrapper = null;
if (mbd.isSingleton()) {
instanceWrapper = this.factoryBeanInstanceCache.remove(beanName);
}
if (instanceWrapper == null) {
// 使用相应的策略创建 bean 实例,例如通过工厂方法或者有参、无参构造器方法
instanceWrapper = createBeanInstance(beanName, mbd, args);
}
final Object bean = (instanceWrapper != null ? instanceWrapper.getWrappedInstance() : null);
Class<?> beanType = (instanceWrapper != null ? instanceWrapper.getWrappedClass() : null);
mbd.resolvedTargetType = beanType;

// ...

// 使用 ObjectFactory 封装实例并缓存,以解决循环引用问题
boolean earlySingletonExposure = (mbd.isSingleton()
&& this.allowCircularReferences
&& isSingletonCurrentlyInCreation(beanName));
if (earlySingletonExposure) {
addSingletonFactory(beanName, new ObjectFactory<Object>() {
@Override
public Object getObject() throws BeansException {
return getEarlyBeanReference(beanName, mbd, bean);
}
});
}

Object exposedObject = bean;
try {
// 填充属性(包括解析依赖的 bean)
populateBean(beanName, mbd, instanceWrapper);
if (exposedObject != null) {
// 初始化 bean
exposedObject = initializeBean(beanName, exposedObject, mbd);
}
}
// ...

// 如有需要,将 bean 注册为一次性的,以供 beanFactory 在关闭时调用销毁方法
try {
registerDisposableBeanIfNecessary(beanName, bean, mbd);
}
// ...

return exposedObject;
}
+ +

createBeanInstance

创建 Bean 实例,并使用 BeanWrapper 封装。实例化的方式:

+
    +
  1. 工厂方法
  2. +
  3. 构造器方法
      +
    1. 有参
    2. +
    3. 无参
    4. +
    +
  4. +
+

populateBean

为创建出的实例填充属性,包括解析当前 bean 所依赖的 bean。

+
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
protected void populateBean(String beanName, RootBeanDefinition mbd, BeanWrapper bw) {
PropertyValues pvs = mbd.getPropertyValues();
// ...

// 给 InstantiationAwareBeanPostProcessors 一个机会,
// 在设置 bean 属性前修改 bean 状态,可用于自定义的字段注入
boolean continueWithPropertyPopulation = true;
if (!mbd.isSynthetic() && hasInstantiationAwareBeanPostProcessors()) {
for (BeanPostProcessor bp : getBeanPostProcessors()) {
if (bp instanceof InstantiationAwareBeanPostProcessor) {
InstantiationAwareBeanPostProcessor ibp = (InstantiationAwareBeanPostProcessor) bp;
if (!ibp.postProcessAfterInstantiation(bw.getWrappedInstance(), beanName)) {
continueWithPropertyPopulation = false;
break;
}
}
}
}

// 是否继续填充属性的流程
if (!continueWithPropertyPopulation) {
return;
}

if (mbd.getResolvedAutowireMode() == RootBeanDefinition.AUTOWIRE_BY_NAME
|| mbd.getResolvedAutowireMode() == RootBeanDefinition.AUTOWIRE_BY_TYPE) {
MutablePropertyValues newPvs = new MutablePropertyValues(pvs);
// 根据名称注入
if (mbd.getResolvedAutowireMode() == RootBeanDefinition.AUTOWIRE_BY_NAME) {
autowireByName(beanName, mbd, bw, newPvs);
}

// 根据类型注入
if (mbd.getResolvedAutowireMode() == RootBeanDefinition.AUTOWIRE_BY_TYPE) {
autowireByType(beanName, mbd, bw, newPvs);
}
pvs = newPvs;
}

// 是否存在 InstantiationAwareBeanPostProcessors
boolean hasInstAwareBpps = hasInstantiationAwareBeanPostProcessors();
// 是否需要检查依赖
boolean needsDepCheck = (mbd.getDependencyCheck() != RootBeanDefinition.DEPENDENCY_CHECK_NONE);

if (hasInstAwareBpps || needsDepCheck) {
PropertyDescriptor[] filteredPds = filterPropertyDescriptorsForDependencyCheck(bw, mbd.allowCaching);
if (hasInstAwareBpps) {
// 后置处理 PropertyValues
for (BeanPostProcessor bp : getBeanPostProcessors()) {
if (bp instanceof InstantiationAwareBeanPostProcessor) {
InstantiationAwareBeanPostProcessor ibp = (InstantiationAwareBeanPostProcessor) bp;
pvs = ibp.postProcessPropertyValues(pvs, filteredPds, bw.getWrappedInstance(), beanName);
if (pvs == null) {
return;
}
}
}
}
if (needsDepCheck) {
checkDependencies(beanName, mbd, filteredPds, pvs);
}
}
// 将属性应用到 bean 上(常规情况下,前面的处理都用不上)
applyPropertyValues(beanName, mbd, bw, pvs);
}
+ +

initializeBean

在填充完属性后,实例就可以进行初始化工作:

+
    +
  1. invokeAwareMethods,让 Bean 通过 xxxAware 接口感知一些信息
  2. +
  3. 调用 BeanPostProcessor 的 postProcessBeforeInitialization 方法
  4. +
  5. invokeInitMethods,调用初始化方法
  6. +
  7. 调用 BeanPostProcessor 的 postProcessAfterInitialization 方法
  8. +
+
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
protected Object initializeBean(final String beanName, final Object bean, RootBeanDefinition mbd) {
// 处理 Aware 接口的相应方法
if (System.getSecurityManager() != null) {
AccessController.doPrivileged(new PrivilegedAction<Object>() {
@Override
public Object run() {
invokeAwareMethods(beanName, bean);
return null;
}
}, getAccessControlContext());
}
else {
invokeAwareMethods(beanName, bean);
}

// 应用 BeanPostProcessor 的 postProcessBeforeInitialization 方法
Object wrappedBean = bean;
if (mbd == null || !mbd.isSynthetic()) {
wrappedBean = applyBeanPostProcessorsBeforeInitialization(wrappedBean, beanName);
}

try {
// 调用初始化方法
invokeInitMethods(beanName, wrappedBean, mbd);
}
catch (Throwable ex) {
throw new BeanCreationException(
(mbd != null ? mbd.getResourceDescription() : null),
beanName, "Invocation of init method failed", ex);
}

if (mbd == null || !mbd.isSynthetic()) {
// 应用 BeanPostProcessor 的 postProcessAfterInitialization 方法
wrappedBean = applyBeanPostProcessorsAfterInitialization(wrappedBean, beanName);
}
return wrappedBean;
}
+ +
处理 Aware 接口的相应方法

让 Bean 在初始化中,感知(获知)和自身相关的资源,如 beanName、beanClassLoader 或者 beanFactory。

+
1
2
3
4
5
6
7
8
9
10
11
12
13
private void invokeAwareMethods(final String beanName, final Object bean) {
if (bean instanceof Aware) {
if (bean instanceof BeanNameAware) {
((BeanNameAware) bean).setBeanName(beanName);
}
if (bean instanceof BeanClassLoaderAware) {
((BeanClassLoaderAware) bean).setBeanClassLoader(getBeanClassLoader());
}
if (bean instanceof BeanFactoryAware) {
((BeanFactoryAware) bean).setBeanFactory(AbstractAutowireCapableBeanFactory.this);
}
}
}
+ +
调用初始化方法
    +
  1. 如果 bean 实现 InitializingBean 接口,调用 afterPropertiesSet 方法
  2. +
  3. 如果自定义 init 方法且满足调用条件,同样进行调用
  4. +
+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
protected void invokeInitMethods(String beanName, final Object bean, RootBeanDefinition mbd) throws Throwable {
// 是否实现 InitializingBean 接口,是的话调用 afterPropertiesSet 方法
// 给 bean 一个感知属性已设置并做出反应的机会
boolean isInitializingBean = (bean instanceof InitializingBean);
if (isInitializingBean
&& (mbd == null || !mbd.isExternallyManagedInitMethod("afterPropertiesSet"))) {
if (System.getSecurityManager() != null) {
try {
AccessController.doPrivileged(new PrivilegedExceptionAction<Object>() {
@Override
public Object run() throws Exception {
((InitializingBean) bean).afterPropertiesSet();
return null;
}
}, getAccessControlContext());
}
catch (PrivilegedActionException pae) {
throw pae.getException();
}
}
else {
((InitializingBean) bean).afterPropertiesSet();
}
}

// 如果存在自定义的 init 方法且方法名称不是 afterPropertiesSet,判断是否调用
if (mbd != null) {
String initMethodName = mbd.getInitMethodName();
if (initMethodName != null
&& !(isInitializingBean && "afterPropertiesSet".equals(initMethodName))
&& !mbd.isExternallyManagedInitMethod(initMethodName)) {
invokeCustomInitMethod(beanName, bean, mbd);
}
}
}
+ +
BeanPostProcessor 处理

在调用初始化方法前后,BeanPostProcessor 先后进行两次处理。其实和 BeanPostProcessor 相关的代码都非常相似:

+
    +
  1. 获取 Processor 列表
  2. +
  3. 判断 Processor 类型是否是当前需要的
  4. +
  5. 对 bean 进行处理
  6. +
+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public Object applyBeanPostProcessorsBeforeInitialization(Object existingBean, String beanName) throws BeansException {

Object result = existingBean;
for (BeanPostProcessor beanProcessor : getBeanPostProcessors()) {
result = beanProcessor.postProcessBeforeInitialization(result, beanName);
if (result == null) {
return result;
}
}
return result;
}

public Object applyBeanPostProcessorsAfterInitialization(Object existingBean, String beanName) throws BeansException {

Object result = existingBean;
for (BeanPostProcessor beanProcessor : getBeanPostProcessors()) {
result = beanProcessor.postProcessAfterInitialization(result, beanName);
if (result == null) {
return result;
}
}
return result;
}
+ +

再思 Bean 的初始化

以下代码片段一度让我困惑,从注释看,初始化 Bean 实例的工作包括了 populateBean 和 initializeBean,但是 initializeBean 方法的含义就是初始化 Bean。在 initializeBean 方法中,调用了 invokeInitMethods 方法,其含义仍然是调用初始化方法。
在更熟悉代码后,我有一种微妙的、个人性的体会,在 Spring 源码中,有时候视角的变化是很快的,痕迹是很小的。如果不加以理解和区分,很容易迷失在相似的描述中。以此处为例,“初始化 Bean 和 Bean 的初始化”扩展开来是 “Bean 工厂初始化一个 Bean 和 Bean 自身进行初始化”。

+
1
2
3
4
5
6
7
8
// Initialize the bean instance.
Object exposedObject = bean;
try {
populateBean(beanName, mbd, instanceWrapper);
if (exposedObject != null) {
exposedObject = initializeBean(beanName, exposedObject, mbd);
}
}
+ +

在注释这一行,视角是属于 BeanFactory(AbstractAutowireCapableBeanFactory)。从工厂的视角,面对一个刚刚创建出来的 Bean 实例,需要完成两方面的工作:

+
    +
  1. 为 Bean 实例填充属性,包括解析依赖,为 Bean 自身的初始化做好准备。
  2. +
  3. Bean 自身的初始化。
  4. +
+

在万事俱备之后,就是 Bean 自身的初始化工作。由于 Spring 的高度扩展性,这部分并不只是单纯地调用初始化方法,还包含 Aware 接口和 BeanPostProcessor 的相关处理,前者偏属于 Java 对象层面,后者偏属于 Spring Bean 层面。
在认同 BeanPostProcessor 的处理属于 Bean 自身初始化工作的一部分后,@PostConstruct 注解的方法被称为 Bean 的初始化方法也就不那么违和了,因为它的实现原理正是 BeanPostProcessor,这仍然在 initializeBean 的范围内。

+ + +
+ + + + + +
+
+ +
+
+

+ + + + + + +
@@ -1742,7 +1742,7 @@

- + @@ -2028,7 +2028,7 @@

- + @@ -2182,7 +2182,7 @@

- + diff --git a/leancloud_counter_security_urls.json b/leancloud_counter_security_urls.json index 881386d7..3857eb32 100644 --- a/leancloud_counter_security_urls.json +++ b/leancloud_counter_security_urls.json @@ -1 +1 @@ -[{"title":"在 Ubuntu 上安装 Clash","url":"/2023/05/27/how-to-install-clash-on-ubuntu/"},{"title":"Docker 常用命令列表","url":"/2020/08/19/docker-frequently-used-commands/"},{"title":"在 iOS 和 macOS 上安装 OpenVPN 客户端","url":"/2023/06/07/how-to-setup-OpenVPN-connect-client-on-iOS-and-macOS/"},{"title":"在 Windows 10 上安装 OpenVPN 服务器","url":"/2023/06/07/how-to-setup-OpenVPN-server-on-windows-10/"},{"title":"使用 OpenVPN 访问家庭内网","url":"/2023/06/07/how-to-use-OpenVPN-to-access-home-network/"},{"title":"Ubuntu server 20.04 安装后没有分配全部磁盘空间","url":"/2023/06/24/Ubuntu-server-20-04-not-all-disk-space-was-allocated-after-installation/"},{"title":"如何使用 SSH 连接 Github 和服务器","url":"/2023/06/28/how-to-use-ssh-to-connect-github-and-server/"},{"title":"Tmux 常用命令和快捷键","url":"/2023/06/29/tmux-frequently-used-commands/"},{"title":"如何为终端、docker 和容器设置代理","url":"/2023/06/13/how-to-configure-proxy-for-terminal-docker-and-container/"},{"title":"如何在 Ubuntu 20.04 上安装 Minikube","url":"/2023/06/23/how-to-install-Minikube-on-Ubuntu-20-04/"},{"title":"JVM GC 的测试和分析","url":"/2023/11/01/testing-and-analysis-of-jvm-gc/"},{"title":"字符串常量池的测试和分析","url":"/2023/11/03/testing-and-analysis-of-StringTable/"},{"title":"Spring Bean 加载过程","url":"/2023/11/17/how-does-Spring-load-beans/"},{"title":"关于 Java 字节码指令的一些例子分析","url":"/2023/11/09/some-examples-of-Java-bytecode-instruction-analysis/"},{"title":"基于 Redis 的分布式锁的简单实现","url":"/2023/11/13/simple-implementation-of-distributed-lock-based-on-Redis/"},{"title":"Java 类加载器源码分析","url":"/2023/07/13/Java-class-loader-source-code-analysis/"},{"title":"JVM 内存区域的测试和分析","url":"/2023/11/04/testing-and-analysis-of-jvm-memory-area/"},{"title":"Java 垃圾收集","url":"/2023/11/07/garbage-collection-in-Java/"}] \ No newline at end of file +[{"title":"在 Ubuntu 上安装 Clash","url":"/2023/05/27/how-to-install-clash-on-ubuntu/"},{"title":"在 iOS 和 macOS 上安装 OpenVPN 客户端","url":"/2023/06/07/how-to-setup-OpenVPN-connect-client-on-iOS-and-macOS/"},{"title":"在 Windows 10 上安装 OpenVPN 服务器","url":"/2023/06/07/how-to-setup-OpenVPN-server-on-windows-10/"},{"title":"使用 OpenVPN 访问家庭内网","url":"/2023/06/07/how-to-use-OpenVPN-to-access-home-network/"},{"title":"如何为终端、docker 和容器设置代理","url":"/2023/06/13/how-to-configure-proxy-for-terminal-docker-and-container/"},{"title":"Docker 常用命令列表","url":"/2020/08/19/docker-frequently-used-commands/"},{"title":"Ubuntu server 20.04 安装后没有分配全部磁盘空间","url":"/2023/06/24/Ubuntu-server-20-04-not-all-disk-space-was-allocated-after-installation/"},{"title":"Tmux 常用命令和快捷键","url":"/2023/06/29/tmux-frequently-used-commands/"},{"title":"如何使用 SSH 连接 Github 和服务器","url":"/2023/06/28/how-to-use-ssh-to-connect-github-and-server/"},{"title":"如何在 Ubuntu 20.04 上安装 Minikube","url":"/2023/06/23/how-to-install-Minikube-on-Ubuntu-20-04/"},{"title":"Spring Bean 加载过程","url":"/2023/08/10/how-does-Spring-load-beans/"},{"title":"JVM GC 的测试和分析","url":"/2023/11/01/testing-and-analysis-of-jvm-gc/"},{"title":"字符串常量池的测试和分析","url":"/2023/11/03/testing-and-analysis-of-StringTable/"},{"title":"Java 类加载器源码分析","url":"/2023/07/13/Java-class-loader-source-code-analysis/"},{"title":"关于 Java 字节码指令的一些例子分析","url":"/2023/11/09/some-examples-of-Java-bytecode-instruction-analysis/"},{"title":"基于 Redis 的分布式锁的简单实现","url":"/2023/11/13/simple-implementation-of-distributed-lock-based-on-Redis/"},{"title":"JVM 内存区域的测试和分析","url":"/2023/11/04/testing-and-analysis-of-jvm-memory-area/"},{"title":"Java 垃圾收集","url":"/2023/11/07/garbage-collection-in-Java/"}] \ No newline at end of file diff --git a/page/2/index.html b/page/2/index.html index 36d9d6ec..ab4a4a4e 100644 --- a/page/2/index.html +++ b/page/2/index.html @@ -158,7 +158,7 @@

Moralok

@@ -232,7 +232,7 @@

- + @@ -343,7 +343,7 @@

- + @@ -493,7 +493,7 @@

- + @@ -633,7 +633,7 @@

- + @@ -781,7 +781,7 @@

- + @@ -897,7 +897,7 @@

- + @@ -1018,7 +1018,7 @@

- + @@ -1151,7 +1151,7 @@

- + diff --git a/search.xml b/search.xml index caf717a6..7947b806 100644 --- a/search.xml +++ b/search.xml @@ -45,6 +45,176 @@ proxy + + 在 iOS 和 macOS 上安装 OpenVPN 客户端 + /2023/06/07/how-to-setup-OpenVPN-connect-client-on-iOS-and-macOS/ + 安装 OpenVPN Connect

macOS 访问官网下载
iOS 访问 AppStore,需要登录外区 Apple ID。

+

配置 OpenVPN Connect

客户端提供了两种方式导入配置文件,一是通过 URL,建议 URL 仅限在私有网络内访问,二是通过其他方式例如邮件,下载为本地文件再导入。

+

配置文件的组织方式又分为两种形式,一种是将 CA 根证书 ca.crt,客户端证书 client.crt,客户端密钥 client.key 的内容复制粘贴到 client.ovpn 中,形成一个联合配置文件;另一种是使用 openssl 将 CA 根证书 ca.crt,客户端证书 client.crt,客户端密钥 client.key 转换为 PKCS#12 文件,先后导入 client.ovpn12 和 client.ovpn。

+

单一 client.ovpn

从目录 C:\Program Files\OpenVPN\sample-config 复制客户端配置文件模板 client.ovpn,修改以下配置:

+
remote your-server 1194

;ca ca.crt
;cert client.crt
;key client.key

;tls-auth ta.key 1

<ca>
-----BEGIN CERTIFICATE-----
paste contents of ca.crt
-----END CERTIFICATE-----
</ca>
<cert>
-----BEGIN CERTIFICATE-----
paste contents of client.crt
-----END CERTIFICATE-----
</cert>
<key>
-----BEGIN PRIVATE KEY-----
paste contents of client.key
-----END PRIVATE KEY-----
</key>
+

remote your-server 1194 中的地址和端口替换成你的 OpenVPN server 的地址和端口。将 ca ca.crtcert client.crtkey client.keytls-auth ta.key 1 注释掉,将各自文件中的内容以上述类 XML 的形式粘贴到 client.ovpn 中。

+

将修改好的客户端配置文件导入到客户端中即可。

+

client.ovpn + client.opvn12

使用 openssl 命令将客户端的证书和私钥文件转换为 PKCS#12 形式的文件。该命令会提示 Enter Export Password,可以为空,但为了安全建议设置密码。

+
openssl pkcs12 -export -in cert -inkey key -certfile ca -name MyClient -out client.ovpn12
+

由于在 iOS 中导入 PKCS#12 文件到 Keychain 中时只导入了客户端证书和密钥,CA 根证书并没有导入,client.ovpn 文件中必须要保留 CA 根证书的配置。
既可以用传统的引用文件的方式:

+
ca ca.crt
+

也可以用类 XML 的形式粘贴 ca.crt 内容到 client.ovpn 中:

+
<ca>
paste contents of ca.crt here
</ca>
+

先导入 client.ovpn12(需要输入转换时的密码),再导入 client.ovpn。

+
+

但是我失败了……导入 client.ovpn12 时密码一直错误,搜索到类似的案例,但是没有找到解决方案。不确定是不是 openssl 版本引起的。

+
+

路由器 NAT

在路由器管理后台的 NAT 设置功能里,配置好对外端口号和 Windows 10 主机上 OpenVPN 端口号的映射关系。

+

参考链接

iOS 使用 OpenVPN 的 FAQ
如何通过 iOS Keychain 使用客户端证书和密钥
如何配置 iOS OpenVPN 客户端的证书认证

+]]> + + openvpn + + + + 在 Windows 10 上安装 OpenVPN 服务器 + /2023/06/07/how-to-setup-OpenVPN-server-on-windows-10/ + 安装 OpenVPN server

OpenVPN 社区 下载 Windows 64-bit MSI installer。本次安装的版本为 OpenVPN 2.6.4。

+
+

注意事项:在选择安装类型时选择 Customize 而不要选择 Install Now。额外勾选 OpenVPN -> OpenVPN Service -> Entire feature will be installed on local hard drive 和 OpenSSL Utilities -> EasyRSA 3 Certificate Management Scripts -> Entire feature will be installed on local hard drive。

+
+

安装完毕后,会弹出一条消息提示未找到可读的连接配置文件,暂时忽略。
此时在 控制面板\网络和 Internet\网络连接 中可以看到创建了两个新的网络适配器 OpenVPN TAP-Windows6 和 OpenVPN Wintun。

+

配置 OpenVPN server

打开 Windows 10 终端程序。
进入 OpenVPN 默认安装目录中的 easy-rsa 目录。

+
cd 'C:\Program Files\OpenVPN\easy-rsa'
+

执行命令进入 Easy-RSA 3 Shell

+
.\EasyRSA-Start.bat
+

初始化公钥基础设施目录 pki

+
./easyrsa init-pki
+

构建证书颁发机构(CA)密钥,CA 根证书文件将在后续用于对其他证书和密钥进行签名。该命令要求输入 Common Name,输入主机名即可。创建的 ca.crt 保存在目录 C:\Program Files\OpenVPN\easy-rsa\pki 中,ca.key 保存在目录 C:\Program Files\OpenVPN\easy-rsa\pki\private 中。

+
./easyrsa build-ca nopass
+

构建服务器证书和密钥。创建的 server.crt 保存在目录 C:\Program Files\OpenVPN\easy-rsa\pki\issued 中,server.key 保存在目录 C:\Program Files\OpenVPN\easy-rsa\pki\private 中。

+
./easyrsa build-server-full server nopass
+

构建客户端证书和密钥。创建的 client.crt 保存在目录 C:\Program Files\OpenVPN\easy-rsa\pki\issued 中,client.key 保存在目录 C:\Program Files\OpenVPN\easy-rsa\pki\private 中。

+
./easyrsa build-client-full client nopass
+

生成 Diffie-Hellman 参数

+
./easyrsa gen-dh
+

从目录 C:\Program Files\OpenVPN\sample-config 复制服务端配置文件模板 server.ovpn 到目录 C:\Program Files\OpenVPN\config 中,修改以下配置:

+
port 1194
dh dh.pem
duplicate-cn
;tls-auth ta.key 0
+

端口号按需修改,默认为1194,需要保证 OpenVPN 的网络流量可以通过防火墙,设置 Windows 10 Defender 允许 OpenVPN 通过即可。dh2048.pem 修改为生成的文件名 dh.pem。取消注释 duplicate-cn,让多个客户端使用同一个客户端证书。注释掉 tls-auth ta.key 0。复制 ca.crt,dh.pem,server.crt 和 server.key 到目录 C:\Program Files\OpenVPN\config 中。

+

启动与连接

启动 OpenVPN,点击连接,系统提示分配 IP 10.8.0.1。按配置,每次 OpenVPN server 都将为自己分配 10.8.0.1。

+

参考链接

openvpn安装配置说明(windows系统)
如何在Windows 10上安装和配置OpenVPN

+]]>
+ + openvpn + +
+ + 使用 OpenVPN 访问家庭内网 + /2023/06/07/how-to-use-OpenVPN-to-access-home-network/ + 网络概况

宽带是电信宽带,分配了动态的公网 IP。
光猫使用桥接模式,通过路由器拨号上网(PPPoE),路由器局域网为 192.168.3.0/24。
一台 Windows 10 主机,在路由器局域网上的 IP 192.168.3.120。。
Windows 10 主机上运行 Vmware 虚拟机,网络采用 NAT 模式。子网为 192.168.46.0/24。运行了 Linux 主机,IP 192.168.46.128。Windows 10 主机在子网中的 IP 为 192.168.3.1。

+ + + + + + + + + + + + + + + + + + + + + + + + + + + +
子网Windows 10 主机Linux 主机客户端
路由器 192.168.3.0/24192.168.3.120--
Vmware NAT 192.168.46.0/24192.168.46.1192.168.46.128-
VPN 10.8.0.0/2410.8.0.1-10.8.0.6
+

目标和考虑因素

    +
  1. 能从公网访问家庭内网,包括 Windows 10 主机和虚拟机上的 Linux 主机。
  2. +
  3. 不想通过路由器的 NAT 功能直接将路由器局域网上的设备映射到公网。一是为了安全,二是为了避免运营商审查。
  4. +
  5. 已经尝试过使用 ZeroTier,将所需设备组建在一个局域网当中,可以作为备选方案。同时不想每次新增设备都安装 ZeroTier。
  6. +
  7. 想利用公网 IP 以及上行带宽尝尝鲜。
  8. +
  9. 想要能直连虚拟机上的 Linux 主机,而不是通过 Vmware 的 NAT 映射。不想每次新增服务都要设置 NAT,修改 Windows Defender 的规则。
  10. +
+

实现过程

在 Windows 10 上安装 OpenVPN 服务器

在 Windows 10 上安装 OpenVPN 服务器

+

在 iOS 和 macOS 上安装 OpenVPN 客户端

在 iOS 和 macOS 上安装 OpenVPN 客户端

+

客户端访问服务端其他的私有子网

如果在所需的每一个设备上都安装 OpenVPN,将它们连接在 VPN 的子网 10.8.0.0/24 中,也是可以满足需求的,但是这和每个设备都安装 ZeroTier 差不多。

+

server.ovpn 新增配置

server.ovpn 配置文件中新增一行配置,这个配置的意思是将该路由配置统一推送给客户端,让它们可以访问服务端的其他私有子网。相当于将服务端的其他私有子网的情况告知客户端,这样客户端就知道发往 192.168.46.128 的 Packet 是发向哪里的。

+
push "route 192.168.46.0 255.255.255.0"
+ +

打开 Windows 10 主机的路由转发功能

    +
  1. Win + R 输入 regedit 打开注册表。
  2. +
  3. 找到 HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\Tcpip\Parameters,修改 IPEnableRouter 为 1。
  4. +
  5. 重启
  6. +
+

为虚拟机上的 Linux 主机新增路由

在终端中输入命令:

+
route add -net 10.8.0.0/24 gw 192.168.46.1
+

这是为了让 OpenVPN 服务端的其他私有子网上的设备知道来自 10.8.0.0/24 的 IP Packet 应该路由回 OpenVPN 服务端。

+

参考链接

透过openvpn来访问内网资源
OpenVPN 路由详解
OpenVPN中的另一个路由问题 - 在VPN上无法访问本地计算机
openvpn添加本地路由表
windows开启路由转发
linux route命令的使用详解
Windows命令行route命令使用图解

+]]>
+ + openvpn + +
+ + 如何为终端、docker 和容器设置代理 + /2023/06/13/how-to-configure-proxy-for-terminal-docker-and-container/ + Ubuntu 上安装 Clash 后,Clash 通过监听本地的 7890 端口,提供代理服务。但是不同程序设置代理的方式不尽相同,并不是启动了 Clash 以及在某一处设置后,整个系统发出的 HTTP 请求都能经过代理。本文将介绍如何为终端、docker 和容器添加代理。

+

为终端设置代理

有时候,我们需要在终端通过执行命令的方式访问网络和下载资源,比如使用 wgetcurl

+

设置 Shell 环境变量

这一类软件都是可以通过为 Shell 设置环境变量的方式来设置代理,涉及到的环境变量有 http_proxyhttps_proxyno_proxy
仅为当前会话设置,执行命令:

+
export http_proxy=http://proxyAddress:port
export https_proxy=http://proxyAddress:port
export no_proxy=localhost,127.0.0.1
+

永久设置代理,在设置 Shell 环境变量的脚本中(不同 Shell 的配置文件不同,比如 ~/.bashrc~/.zshrc)添加:

+
export http_proxy=http://proxyAddress:port
export https_proxy=http://proxyAddress:port
export no_proxy=localhost,127.0.0.1
+

重新启动一个会话或者执行命令 source ~/.bashrc 使其在当前会话立即生效。

+

修改 wget 配置文件

在搜索过程中发现还可以在 wget 的配置文件 ~/.wgetrc 中添加:

+
use_proxy = on

http_proxy = http://proxyAddress:port
https_proxy = http://proxyAddress:port
+ +

为 docker 设置代理

如果你以为为终端设置代理后 docker 就会使用代理,那你就错了。在从官方的镜像仓库 pull 镜像反复出错后并收到类似 Error response from daemon: Get "https://registry-1.docker.io/v2/": read tcp 192.168.3.140:59460->44.205.64.79:443: read: connection reset by peer 这样的报错信息后,我才开始怀疑我并没有真正给 docker 设置好代理。
在执行 docker pull 命令时,实际上命令是由守护进程 docker daemon 执行的。

+

通过 systemd 设置

如果你的 docker daemon 是通过 systemd 管理的,那么你可以通过设置 docker.service 服务的环境变量来设置代理。
执行命令查看 docker.service 信息,得知配置文件位置 /lib/systemd/system/docker.service

+
~$ systemctl status docker.service 
● docker.service - Docker Application Container Engine
Loaded: loaded (/lib/systemd/system/docker.service; enabled; vendor preset: enabled)
Active: active (running) since Tue 2023-06-13 00:52:54 CST; 22h ago
TriggeredBy: ● docker.socket
Docs: https://docs.docker.com
Main PID: 387690 (dockerd)
Tasks: 139
Memory: 89.6M
CPU: 1min 26.512s
CGroup: /system.slice/docker.service
+

docker.service[Service] 模块添加:

+
Environment=HTTP_PROXY=http://proxyAddress:port
Environment=HTTPS_PROXY=http://proxyAddress:port
Environment=NO_PROXY=localhost,127.0.0.1
+

重新加载配置文件并重启服务:

+
systemctl daemon-reload
systemctl restart docker.service
+ +

修改 dockerd 配置文件

还可以修改 dockerd 配置文件,添加:

+
export http_proxy="http://proxyAddress:port"
+

然后重启 docker daemon 即可。

+
+

国内的镜像仓库在绝大多数时候都可以满足条件,但是存在个别镜像同步不及时的情况,如果使用 latest 标签拉取到的镜像并非近期的镜像,因此有时候需要直接从官方镜像仓库拉取镜像。

+
+

为 docker 容器设置代理

docker daemon 进程设置代理和为 docker 容器设置代理是有区别的。比如使用 docker 启动媒体服务器 jellyfin 后,jellyfin 的刮削功能就需要代理才能正常使用,这时候不要因为在很多地方设置过代理就以为容器内部已经在使用代理了。

+

修改配置文件

创建或修改 ~/.docker/config.json,添加:

+
{
"proxies":
{
"default":
{
"httpProxy": "http://proxyAddress:port",
"httpsProxy": "http://proxyAddress:port",
"noProxy": "localhost,127.0.0.1"
}
}
}
+

此后创建的新容器,会自动设置环境变量来使用代理。

+

为指定容器添加环境变量

在启动容器时使用 -e 手动注入环境变量 http_proxy。这意味着进入容器使用 export 设置环境变量的方式也是可行的。

+
+

注意:如果代理是使用宿主机的代理,当网络为 bridge 模式,proxyAddress 需要填写宿主机的 IP;如果使用 host 模式,proxyAddress 可以填写 127.0.0.1。

+
+

总结

不要因为在很多地方设置过代理,就想当然地以为当前的访问也是经过代理的。每个软件设置代理的方式不尽相同,但是大体上可以归结为:

+
    +
  1. 使用系统的环境变量
  2. +
  3. 修改软件的配置文件
  4. +
  5. 执行时注入参数
  6. +
+

举一反三,像 aptgit 这类软件也是有其设置代理的方法。当你的代理稳定但是相应的访问失败时,大胆假设你的代理没有设置成功。要理清楚,当前的访问是谁发起的,才能正确地使用关键词搜索到正确的设置方式。

+
+

原本我在 docker 相关的使用中,有关代理的设置方式是通过修改配置文件,实现永久、全局的代理配置。但是在后续的使用中,发现代理在一些场景(比如使用 cloudflare tunnel)中会引起不易排查的问题,决定采用临时、局部的配置方式。

+
+

参考链接

Linux 让终端走代理的几种方法
Linux ❀ wget设置代理
配置Docker使用代理
Docker的三种网络代理配置
docker 设置代理,以及国内加速镜像设置

+]]>
+ + proxy + docker + +
Docker 常用命令列表 /2020/08/19/docker-frequently-used-commands/ @@ -498,188 +668,25 @@ - 在 iOS 和 macOS 上安装 OpenVPN 客户端 - /2023/06/07/how-to-setup-OpenVPN-connect-client-on-iOS-and-macOS/ - 安装 OpenVPN Connect

macOS 访问官网下载
iOS 访问 AppStore,需要登录外区 Apple ID。

-

配置 OpenVPN Connect

客户端提供了两种方式导入配置文件,一是通过 URL,建议 URL 仅限在私有网络内访问,二是通过其他方式例如邮件,下载为本地文件再导入。

-

配置文件的组织方式又分为两种形式,一种是将 CA 根证书 ca.crt,客户端证书 client.crt,客户端密钥 client.key 的内容复制粘贴到 client.ovpn 中,形成一个联合配置文件;另一种是使用 openssl 将 CA 根证书 ca.crt,客户端证书 client.crt,客户端密钥 client.key 转换为 PKCS#12 文件,先后导入 client.ovpn12 和 client.ovpn。

-

单一 client.ovpn

从目录 C:\Program Files\OpenVPN\sample-config 复制客户端配置文件模板 client.ovpn,修改以下配置:

-
remote your-server 1194

;ca ca.crt
;cert client.crt
;key client.key

;tls-auth ta.key 1

<ca>
-----BEGIN CERTIFICATE-----
paste contents of ca.crt
-----END CERTIFICATE-----
</ca>
<cert>
-----BEGIN CERTIFICATE-----
paste contents of client.crt
-----END CERTIFICATE-----
</cert>
<key>
-----BEGIN PRIVATE KEY-----
paste contents of client.key
-----END PRIVATE KEY-----
</key>
-

remote your-server 1194 中的地址和端口替换成你的 OpenVPN server 的地址和端口。将 ca ca.crtcert client.crtkey client.keytls-auth ta.key 1 注释掉,将各自文件中的内容以上述类 XML 的形式粘贴到 client.ovpn 中。

-

将修改好的客户端配置文件导入到客户端中即可。

-

client.ovpn + client.opvn12

使用 openssl 命令将客户端的证书和私钥文件转换为 PKCS#12 形式的文件。该命令会提示 Enter Export Password,可以为空,但为了安全建议设置密码。

-
openssl pkcs12 -export -in cert -inkey key -certfile ca -name MyClient -out client.ovpn12
-

由于在 iOS 中导入 PKCS#12 文件到 Keychain 中时只导入了客户端证书和密钥,CA 根证书并没有导入,client.ovpn 文件中必须要保留 CA 根证书的配置。
既可以用传统的引用文件的方式:

-
ca ca.crt
-

也可以用类 XML 的形式粘贴 ca.crt 内容到 client.ovpn 中:

-
<ca>
paste contents of ca.crt here
</ca>
-

先导入 client.ovpn12(需要输入转换时的密码),再导入 client.ovpn。

-
-

但是我失败了……导入 client.ovpn12 时密码一直错误,搜索到类似的案例,但是没有找到解决方案。不确定是不是 openssl 版本引起的。

-
-

路由器 NAT

在路由器管理后台的 NAT 设置功能里,配置好对外端口号和 Windows 10 主机上 OpenVPN 端口号的映射关系。

-

参考链接

iOS 使用 OpenVPN 的 FAQ
如何通过 iOS Keychain 使用客户端证书和密钥
如何配置 iOS OpenVPN 客户端的证书认证

-]]>
- - OpenVPN - -
- - 在 Windows 10 上安装 OpenVPN 服务器 - /2023/06/07/how-to-setup-OpenVPN-server-on-windows-10/ - 安装 OpenVPN server

OpenVPN 社区 下载 Windows 64-bit MSI installer。本次安装的版本为 OpenVPN 2.6.4。

-
-

注意事项:在选择安装类型时选择 Customize 而不要选择 Install Now。额外勾选 OpenVPN -> OpenVPN Service -> Entire feature will be installed on local hard drive 和 OpenSSL Utilities -> EasyRSA 3 Certificate Management Scripts -> Entire feature will be installed on local hard drive。

-
-

安装完毕后,会弹出一条消息提示未找到可读的连接配置文件,暂时忽略。
此时在 控制面板\网络和 Internet\网络连接 中可以看到创建了两个新的网络适配器 OpenVPN TAP-Windows6 和 OpenVPN Wintun。

-

配置 OpenVPN server

打开 Windows 10 终端程序。
进入 OpenVPN 默认安装目录中的 easy-rsa 目录。

-
cd 'C:\Program Files\OpenVPN\easy-rsa'
-

执行命令进入 Easy-RSA 3 Shell

-
.\EasyRSA-Start.bat
-

初始化公钥基础设施目录 pki

-
./easyrsa init-pki
-

构建证书颁发机构(CA)密钥,CA 根证书文件将在后续用于对其他证书和密钥进行签名。该命令要求输入 Common Name,输入主机名即可。创建的 ca.crt 保存在目录 C:\Program Files\OpenVPN\easy-rsa\pki 中,ca.key 保存在目录 C:\Program Files\OpenVPN\easy-rsa\pki\private 中。

-
./easyrsa build-ca nopass
-

构建服务器证书和密钥。创建的 server.crt 保存在目录 C:\Program Files\OpenVPN\easy-rsa\pki\issued 中,server.key 保存在目录 C:\Program Files\OpenVPN\easy-rsa\pki\private 中。

-
./easyrsa build-server-full server nopass
-

构建客户端证书和密钥。创建的 client.crt 保存在目录 C:\Program Files\OpenVPN\easy-rsa\pki\issued 中,client.key 保存在目录 C:\Program Files\OpenVPN\easy-rsa\pki\private 中。

-
./easyrsa build-client-full client nopass
-

生成 Diffie-Hellman 参数

-
./easyrsa gen-dh
-

从目录 C:\Program Files\OpenVPN\sample-config 复制服务端配置文件模板 server.ovpn 到目录 C:\Program Files\OpenVPN\config 中,修改以下配置:

-
port 1194
dh dh.pem
duplicate-cn
;tls-auth ta.key 0
-

端口号按需修改,默认为1194,需要保证 OpenVPN 的网络流量可以通过防火墙,设置 Windows 10 Defender 允许 OpenVPN 通过即可。dh2048.pem 修改为生成的文件名 dh.pem。取消注释 duplicate-cn,让多个客户端使用同一个客户端证书。注释掉 tls-auth ta.key 0。复制 ca.crt,dh.pem,server.crt 和 server.key 到目录 C:\Program Files\OpenVPN\config 中。

-

启动与连接

启动 OpenVPN,点击连接,系统提示分配 IP 10.8.0.1。按配置,每次 OpenVPN server 都将为自己分配 10.8.0.1。

-

参考链接

openvpn安装配置说明(windows系统)
如何在Windows 10上安装和配置OpenVPN

+ Ubuntu server 20.04 安装后没有分配全部磁盘空间 + /2023/06/24/Ubuntu-server-20-04-not-all-disk-space-was-allocated-after-installation/ + 最近在本地测试 Kubesphere 和 Minikube,使用 Ubuntu server 20.04 搭建了多个虚拟机,磁盘空间紧张。注意到安装后,磁盘空间仅占据分配的一半左右。
如果 Ubuntu server 20.04 安装时使用默认的 lvm 选项,就会出现这种情况。

+

分配了 40GB 磁盘空间,可用仅 19GB。

+
$ df -h
Filesystem Size Used Avail Use% Mounted on
udev 3.9G 0 3.9G 0% /dev
tmpfs 792M 7.5M 785M 1% /run
/dev/mapper/ubuntu--vg-ubuntu--lv 19G 17G 995M 95% /
tmpfs 3.9G 0 3.9G 0% /dev/shm
tmpfs 5.0M 0 5.0M 0% /run/lock
tmpfs 3.9G 0 3.9G 0% /sys/fs/cgroup
/dev/sda2 2.0G 108M 1.7G 6% /boot
/dev/loop0 64M 64M 0 100% /snap/core20/1828
/dev/loop2 50M 50M 0 100% /snap/snapd/18357
/dev/loop1 92M 92M 0 100% /snap/lxd/24061
tmpfs 792M 0 792M 0% /run/user/1000
/dev/loop3 54M 54M 0 100% /snap/snapd/19457
+ +

查看发现 Free PE / Size 还有 19GB。

+
$ sudo vgdisplay
--- Volume group ---
VG Name ubuntu-vg
System ID
Format lvm2
Metadata Areas 1
Metadata Sequence No 2
VG Access read/write
VG Status resizable
MAX LV 0
Cur LV 1
Open LV 1
Max PV 0
Cur PV 1
Act PV 1
VG Size <38.00 GiB
PE Size 4.00 MiB
Total PE 9727
Alloc PE / Size 4863 / <19.00 GiB
Free PE / Size 4864 / 19.00 GiB
VG UUID NuEjzH-CKXm-W6lA-gqzj-4bds-IR1Y-dTZ8IP
+ +

重新分配空间。

+
$ sudo lvextend -l +100%FREE /dev/mapper/ubuntu--vg-ubuntu--lv
Size of logical volume ubuntu-vg/ubuntu-lv changed from <19.00 GiB (4863 extents) to <38.00 GiB (9727 extents).
Logical volume ubuntu-vg/ubuntu-lv successfully resized.

$ sudo resize2fs /dev/mapper/ubuntu--vg-ubuntu--lv
resize2fs 1.45.5 (07-Jan-2020)
Filesystem at /dev/mapper/ubuntu--vg-ubuntu--lv is mounted on /; on-line resizing required
old_desc_blocks = 3, new_desc_blocks = 5
The filesystem on /dev/mapper/ubuntu--vg-ubuntu--lv is now 9960448 (4k) blocks long.
+ +

再次查看。

+
df -h
Filesystem Size Used Avail Use% Mounted on
udev 3.9G 0 3.9G 0% /dev
tmpfs 792M 7.5M 785M 1% /run
/dev/mapper/ubuntu--vg-ubuntu--lv 38G 17G 19G 47% /
tmpfs 3.9G 0 3.9G 0% /dev/shm
tmpfs 5.0M 0 5.0M 0% /run/lock
tmpfs 3.9G 0 3.9G 0% /sys/fs/cgroup
/dev/sda2 2.0G 108M 1.7G 6% /boot
/dev/loop0 64M 64M 0 100% /snap/core20/1828
/dev/loop2 50M 50M 0 100% /snap/snapd/18357
/dev/loop1 92M 92M 0 100% /snap/lxd/24061
tmpfs 792M 0 792M 0% /run/user/1000
/dev/loop3 54M 54M 0 100% /snap/snapd/19457
/dev/loop4 64M 64M 0 100% /snap/core20/1950
+ +

参考链接

ubuntu20.04 server 安装后磁盘空间只有一半的处理
Ubuntu Server 20.04.1 LTS, not all disk space was allocated during installation?

]]>
- OpenVPN - -
- - 使用 OpenVPN 访问家庭内网 - /2023/06/07/how-to-use-OpenVPN-to-access-home-network/ - 网络概况

宽带是电信宽带,分配了动态的公网 IP。
光猫使用桥接模式,通过路由器拨号上网(PPPoE),路由器局域网为 192.168.3.0/24。
一台 Windows 10 主机,在路由器局域网上的 IP 192.168.3.120。。
Windows 10 主机上运行 Vmware 虚拟机,网络采用 NAT 模式。子网为 192.168.46.0/24。运行了 Linux 主机,IP 192.168.46.128。Windows 10 主机在子网中的 IP 为 192.168.3.1。

- - - - - - - - - - - - - - - - - - - - - - - - - - - -
子网Windows 10 主机Linux 主机客户端
路由器 192.168.3.0/24192.168.3.120--
Vmware NAT 192.168.46.0/24192.168.46.1192.168.46.128-
VPN 10.8.0.0/2410.8.0.1-10.8.0.6
-

目标和考虑因素

    -
  1. 能从公网访问家庭内网,包括 Windows 10 主机和虚拟机上的 Linux 主机。
  2. -
  3. 不想通过路由器的 NAT 功能直接将路由器局域网上的设备映射到公网。一是为了安全,二是为了避免运营商审查。
  4. -
  5. 已经尝试过使用 ZeroTier,将所需设备组建在一个局域网当中,可以作为备选方案。同时不想每次新增设备都安装 ZeroTier。
  6. -
  7. 想利用公网 IP 以及上行带宽尝尝鲜。
  8. -
  9. 想要能直连虚拟机上的 Linux 主机,而不是通过 Vmware 的 NAT 映射。不想每次新增服务都要设置 NAT,修改 Windows Defender 的规则。
  10. -
-

实现过程

在 Windows 10 上安装 OpenVPN 服务器

在 Windows 10 上安装 OpenVPN 服务器

-

在 iOS 和 macOS 上安装 OpenVPN 客户端

在 iOS 和 macOS 上安装 OpenVPN 客户端

-

客户端访问服务端其他的私有子网

如果在所需的每一个设备上都安装 OpenVPN,将它们连接在 VPN 的子网 10.8.0.0/24 中,也是可以满足需求的,但是这和每个设备都安装 ZeroTier 差不多。

-

server.ovpn 新增配置

server.ovpn 配置文件中新增一行配置,这个配置的意思是将该路由配置统一推送给客户端,让它们可以访问服务端的其他私有子网。相当于将服务端的其他私有子网的情况告知客户端,这样客户端就知道发往 192.168.46.128 的 Packet 是发向哪里的。

-
push "route 192.168.46.0 255.255.255.0"
- -

打开 Windows 10 主机的路由转发功能

    -
  1. Win + R 输入 regedit 打开注册表。
  2. -
  3. 找到 HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\Tcpip\Parameters,修改 IPEnableRouter 为 1。
  4. -
  5. 重启
  6. -
-

为虚拟机上的 Linux 主机新增路由

在终端中输入命令:

-
route add -net 10.8.0.0/24 gw 192.168.46.1
-

这是为了让 OpenVPN 服务端的其他私有子网上的设备知道来自 10.8.0.0/24 的 IP Packet 应该路由回 OpenVPN 服务端。

-

参考链接

透过openvpn来访问内网资源
OpenVPN 路由详解
OpenVPN中的另一个路由问题 - 在VPN上无法访问本地计算机
openvpn添加本地路由表
windows开启路由转发
linux route命令的使用详解
Windows命令行route命令使用图解

-]]>
- - OpenVPN - -
- - Ubuntu server 20.04 安装后没有分配全部磁盘空间 - /2023/06/24/Ubuntu-server-20-04-not-all-disk-space-was-allocated-after-installation/ - 最近在本地测试 Kubesphere 和 Minikube,使用 Ubuntu server 20.04 搭建了多个虚拟机,磁盘空间紧张。注意到安装后,磁盘空间仅占据分配的一半左右。
如果 Ubuntu server 20.04 安装时使用默认的 lvm 选项,就会出现这种情况。

-

分配了 40GB 磁盘空间,可用仅 19GB。

-
$ df -h
Filesystem Size Used Avail Use% Mounted on
udev 3.9G 0 3.9G 0% /dev
tmpfs 792M 7.5M 785M 1% /run
/dev/mapper/ubuntu--vg-ubuntu--lv 19G 17G 995M 95% /
tmpfs 3.9G 0 3.9G 0% /dev/shm
tmpfs 5.0M 0 5.0M 0% /run/lock
tmpfs 3.9G 0 3.9G 0% /sys/fs/cgroup
/dev/sda2 2.0G 108M 1.7G 6% /boot
/dev/loop0 64M 64M 0 100% /snap/core20/1828
/dev/loop2 50M 50M 0 100% /snap/snapd/18357
/dev/loop1 92M 92M 0 100% /snap/lxd/24061
tmpfs 792M 0 792M 0% /run/user/1000
/dev/loop3 54M 54M 0 100% /snap/snapd/19457
- -

查看发现 Free PE / Size 还有 19GB。

-
$ sudo vgdisplay
--- Volume group ---
VG Name ubuntu-vg
System ID
Format lvm2
Metadata Areas 1
Metadata Sequence No 2
VG Access read/write
VG Status resizable
MAX LV 0
Cur LV 1
Open LV 1
Max PV 0
Cur PV 1
Act PV 1
VG Size <38.00 GiB
PE Size 4.00 MiB
Total PE 9727
Alloc PE / Size 4863 / <19.00 GiB
Free PE / Size 4864 / 19.00 GiB
VG UUID NuEjzH-CKXm-W6lA-gqzj-4bds-IR1Y-dTZ8IP
- -

重新分配空间。

-
$ sudo lvextend -l +100%FREE /dev/mapper/ubuntu--vg-ubuntu--lv
Size of logical volume ubuntu-vg/ubuntu-lv changed from <19.00 GiB (4863 extents) to <38.00 GiB (9727 extents).
Logical volume ubuntu-vg/ubuntu-lv successfully resized.

$ sudo resize2fs /dev/mapper/ubuntu--vg-ubuntu--lv
resize2fs 1.45.5 (07-Jan-2020)
Filesystem at /dev/mapper/ubuntu--vg-ubuntu--lv is mounted on /; on-line resizing required
old_desc_blocks = 3, new_desc_blocks = 5
The filesystem on /dev/mapper/ubuntu--vg-ubuntu--lv is now 9960448 (4k) blocks long.
- -

再次查看。

-
df -h
Filesystem Size Used Avail Use% Mounted on
udev 3.9G 0 3.9G 0% /dev
tmpfs 792M 7.5M 785M 1% /run
/dev/mapper/ubuntu--vg-ubuntu--lv 38G 17G 19G 47% /
tmpfs 3.9G 0 3.9G 0% /dev/shm
tmpfs 5.0M 0 5.0M 0% /run/lock
tmpfs 3.9G 0 3.9G 0% /sys/fs/cgroup
/dev/sda2 2.0G 108M 1.7G 6% /boot
/dev/loop0 64M 64M 0 100% /snap/core20/1828
/dev/loop2 50M 50M 0 100% /snap/snapd/18357
/dev/loop1 92M 92M 0 100% /snap/lxd/24061
tmpfs 792M 0 792M 0% /run/user/1000
/dev/loop3 54M 54M 0 100% /snap/snapd/19457
/dev/loop4 64M 64M 0 100% /snap/core20/1950
- -

参考链接

ubuntu20.04 server 安装后磁盘空间只有一半的处理
Ubuntu Server 20.04.1 LTS, not all disk space was allocated during installation?

-]]>
- - Ubuntu - -
- - 如何使用 SSH 连接 Github 和服务器 - /2023/06/28/how-to-use-ssh-to-connect-github-and-server/ - 使用 SSH 连接 Github

检查现有 SSH 密钥

打开终端,输入 ls -al ~/.ssh 以查看是否存在现有的 SSH 密钥。

-
$ ls -al ~/.ssh
total 16
drwx------ 2 wrmao wrmao 4096 Jun 28 20:19 .
drwxr-xr-x 6 wrmao wrmao 4096 Jun 28 20:13 ..
-rw------- 1 wrmao wrmao 106 Jun 28 20:07 authorized_keys
-rw-r--r-- 1 wrmao wrmao 444 Jun 28 20:19 known_hosts
-

检查目录列表以查看是否已经有 SSH 公钥。 默认情况下,GitHub 的一个支持的公钥的文件名是以下之一。

-
    -
  • id_rsa.pub
  • -
  • id_ecdsa.pub
  • -
  • id_ed25519.pub
  • -
-

生成 SSH 密钥

如果没有密钥,就需要生成新的 SSH 密钥;如果已有,跳到上传已有密钥环节。
打开终端,粘贴下面的文本(替换为你的 GitHub 电子邮件地址),这将以提供的电子邮件地址为标签创建新 SSH 密钥。
一直 yes 确定选择默认即可。

-
$ ssh-keygen -t ed25519 -C "your_email@example.com"
Generating public/private ed25519 key pair.
Enter file in which to save the key (/home/your-user/.ssh/id_ed25519):
- -

上传 SSH 密钥

将 SSH 公钥复制到剪贴板,在 Github 上的 Settings - Access - SSH and GPG keys - New SSH key,粘贴即可。

-
$ cat ~/.ssh/id_ed25519.pub
- -

配置 Git

$ git config --global user.name "moralok"
$ git config --global user.email "wrmao.public@outlook.com"
- - -

SSH 的原理

关于 SSH

使用 SSH 协议可以连接远程服务器和服务并向它们验证,而无需在每次访问时都提供用户名和密码,Github 还可以使用 SSH 密钥对提交进行签名。

-

公钥和私钥

SSH 的使用(非对称加密)需要生成公钥 public key 和私钥 private key。常用的算法有 rsaecdsaed25519,相对应的公钥默认文件名即id_XXX.pub。ed25519 的安全性介于 rsa 2048rsa 4096 之间,但性能却提升数十倍。
在生成密钥时,会要求你 Enter passphrase (empty for no passphrase):,可以输入一个口令保护私钥的使用。不为空的情况下,正常使用是需要输入这个口令的,很多人认为麻烦,因此留空。
公钥的权限必须是 644,私钥的权限必须是 600,否则 SSH 认为其不可靠。
私钥是要安全保管在客户端不能泄露的,公钥则要提供给远程服务器或服务。服务端的 ~/.ssh/authorized_keys 里面存储着可以登录的客户端的公钥。我们将公钥粘贴到 Github 的过程就是对应于此。

-
$ ssh-keygen -t rsa -b 4096 -f my_id -C "email@example.com"
-
    -
  • -t 表示算法,如 rsa
  • -
  • -b 表示 rsa 密钥长度,默认 2048 bit,ed25519 不需要指定。
  • -
  • -f 表示文件名。
  • -
  • -C 表示在公钥文件中添加注释,可修改
  • -
-

SSH 公钥登录过程

    -
  1. Client 将自己的公钥存放到服务端,追加到 authorized_keys 文件。
  2. -
  3. Server 收到 Client 的连接请求后,会在 authorized_keys 文件中匹配到 Client 传过来的公钥,并生成随机数 R,用公钥对随机数加密得到 pubKey(R)。
  4. -
  5. Client 收到后通过私钥解密得到随机数 R,然后对随机数 R 和本次会话的 sessionKey 使用 MD5 生成摘要 Digest1,发送给服务端。
  6. -
  7. Server 会对随机数 R 和会话的 sessionKey 同样使用 MD5 生成摘要 Digest2,对比相同即完成认证过程。
  8. -
-

避免中间人攻击

SSH 通过口令确认避免中间人攻击,如果用户第一次登录 Server,系统会提示:

-
$ ssh -T git@github.com
The authenticity of host 'github.com (20.205.243.166)' can't be established.
ECDSA key fingerprint is SHA256:p2QAMXNIC1TJYWeIOttrVc98/R1BUFWu3/LiyKgUfQM.
Are you sure you want to continue connecting (yes/no/[fingerprint])? yes
Warning: Permanently added 'github.com,20.205.243.166' (ECDSA) to the list of known hosts.
Hi ${username}! You've successfully authenticated, but GitHub does not provide shell access.
-

Server 需要在其网站上公示其公钥的指纹,Github 的公钥指纹在这里
确认匹配后,客户端会在 ~/.ssh/known_hosts 中记录,下次登录不再警告。

-

使用 SSH 免密登录服务器

使用现成的密钥,将 ~/.ssh/id_ed25519.pub 的内容追加到自己服务端的 ~/.ssh/authorized_keys 中,使用 ssh user@host 成功免密登录。
这样一来,使用 VScode 远程连接服务器和使用 Remote Explorer 打开文件夹时,不用每次都输入密码了。

-

参考链接

使用 SSH 进行连接 Github
Git 多台电脑共用SSH Key
SSH协议登录过程详解
GitHub 的 SSH 密钥指纹
使用 Ed25519 算法生成你的 SSH 密钥

-]]>
- - ssh + ubuntu
@@ -748,55 +755,48 @@ - 如何为终端、docker 和容器设置代理 - /2023/06/13/how-to-configure-proxy-for-terminal-docker-and-container/ - Ubuntu 上安装 Clash 后,Clash 通过监听本地的 7890 端口,提供代理服务。但是不同程序设置代理的方式不尽相同,并不是启动了 Clash 以及在某一处设置后,整个系统发出的 HTTP 请求都能经过代理。本文将介绍如何为终端、docker 和容器添加代理。

-

为终端设置代理

有时候,我们需要在终端通过执行命令的方式访问网络和下载资源,比如使用 wgetcurl

-

设置 Shell 环境变量

这一类软件都是可以通过为 Shell 设置环境变量的方式来设置代理,涉及到的环境变量有 http_proxyhttps_proxyno_proxy
仅为当前会话设置,执行命令:

-
export http_proxy=http://proxyAddress:port
export https_proxy=http://proxyAddress:port
export no_proxy=localhost,127.0.0.1
-

永久设置代理,在设置 Shell 环境变量的脚本中(不同 Shell 的配置文件不同,比如 ~/.bashrc~/.zshrc)添加:

-
export http_proxy=http://proxyAddress:port
export https_proxy=http://proxyAddress:port
export no_proxy=localhost,127.0.0.1
-

重新启动一个会话或者执行命令 source ~/.bashrc 使其在当前会话立即生效。

-

修改 wget 配置文件

在搜索过程中发现还可以在 wget 的配置文件 ~/.wgetrc 中添加:

-
use_proxy = on

http_proxy = http://proxyAddress:port
https_proxy = http://proxyAddress:port
+ 如何使用 SSH 连接 Github 和服务器 + /2023/06/28/how-to-use-ssh-to-connect-github-and-server/ + 使用 SSH 连接 Github

检查现有 SSH 密钥

打开终端,输入 ls -al ~/.ssh 以查看是否存在现有的 SSH 密钥。

+
$ ls -al ~/.ssh
total 16
drwx------ 2 wrmao wrmao 4096 Jun 28 20:19 .
drwxr-xr-x 6 wrmao wrmao 4096 Jun 28 20:13 ..
-rw------- 1 wrmao wrmao 106 Jun 28 20:07 authorized_keys
-rw-r--r-- 1 wrmao wrmao 444 Jun 28 20:19 known_hosts
+

检查目录列表以查看是否已经有 SSH 公钥。 默认情况下,GitHub 的一个支持的公钥的文件名是以下之一。

+
    +
  • id_rsa.pub
  • +
  • id_ecdsa.pub
  • +
  • id_ed25519.pub
  • +
+

生成 SSH 密钥

如果没有密钥,就需要生成新的 SSH 密钥;如果已有,跳到上传已有密钥环节。
打开终端,粘贴下面的文本(替换为你的 GitHub 电子邮件地址),这将以提供的电子邮件地址为标签创建新 SSH 密钥。
一直 yes 确定选择默认即可。

+
$ ssh-keygen -t ed25519 -C "your_email@example.com"
Generating public/private ed25519 key pair.
Enter file in which to save the key (/home/your-user/.ssh/id_ed25519):
-

为 docker 设置代理

如果你以为为终端设置代理后 docker 就会使用代理,那你就错了。在从官方的镜像仓库 pull 镜像反复出错后并收到类似 Error response from daemon: Get "https://registry-1.docker.io/v2/": read tcp 192.168.3.140:59460->44.205.64.79:443: read: connection reset by peer 这样的报错信息后,我才开始怀疑我并没有真正给 docker 设置好代理。
在执行 docker pull 命令时,实际上命令是由守护进程 docker daemon 执行的。

-

通过 systemd 设置

如果你的 docker daemon 是通过 systemd 管理的,那么你可以通过设置 docker.service 服务的环境变量来设置代理。
执行命令查看 docker.service 信息,得知配置文件位置 /lib/systemd/system/docker.service

-
~$ systemctl status docker.service 
● docker.service - Docker Application Container Engine
Loaded: loaded (/lib/systemd/system/docker.service; enabled; vendor preset: enabled)
Active: active (running) since Tue 2023-06-13 00:52:54 CST; 22h ago
TriggeredBy: ● docker.socket
Docs: https://docs.docker.com
Main PID: 387690 (dockerd)
Tasks: 139
Memory: 89.6M
CPU: 1min 26.512s
CGroup: /system.slice/docker.service
-

docker.service[Service] 模块添加:

-
Environment=HTTP_PROXY=http://proxyAddress:port
Environment=HTTPS_PROXY=http://proxyAddress:port
Environment=NO_PROXY=localhost,127.0.0.1
-

重新加载配置文件并重启服务:

-
systemctl daemon-reload
systemctl restart docker.service
+

上传 SSH 密钥

将 SSH 公钥复制到剪贴板,在 Github 上的 Settings - Access - SSH and GPG keys - New SSH key,粘贴即可。

+
$ cat ~/.ssh/id_ed25519.pub
-

修改 dockerd 配置文件

还可以修改 dockerd 配置文件,添加:

-
export http_proxy="http://proxyAddress:port"
-

然后重启 docker daemon 即可。

-
-

国内的镜像仓库在绝大多数时候都可以满足条件,但是存在个别镜像同步不及时的情况,如果使用 latest 标签拉取到的镜像并非近期的镜像,因此有时候需要直接从官方镜像仓库拉取镜像。

-
-

为 docker 容器设置代理

docker daemon 进程设置代理和为 docker 容器设置代理是有区别的。比如使用 docker 启动媒体服务器 jellyfin 后,jellyfin 的刮削功能就需要代理才能正常使用,这时候不要因为在很多地方设置过代理就以为容器内部已经在使用代理了。

-

修改配置文件

创建或修改 ~/.docker/config.json,添加:

-
{
"proxies":
{
"default":
{
"httpProxy": "http://proxyAddress:port",
"httpsProxy": "http://proxyAddress:port",
"noProxy": "localhost,127.0.0.1"
}
}
}
-

此后创建的新容器,会自动设置环境变量来使用代理。

-

为指定容器添加环境变量

在启动容器时使用 -e 手动注入环境变量 http_proxy。这意味着进入容器使用 export 设置环境变量的方式也是可行的。

-
-

注意:如果代理是使用宿主机的代理,当网络为 bridge 模式,proxyAddress 需要填写宿主机的 IP;如果使用 host 模式,proxyAddress 可以填写 127.0.0.1。

-
-

总结

不要因为在很多地方设置过代理,就想当然地以为当前的访问也是经过代理的。每个软件设置代理的方式不尽相同,但是大体上可以归结为:

-
    -
  1. 使用系统的环境变量
  2. -
  3. 修改软件的配置文件
  4. -
  5. 执行时注入参数
  6. +

    配置 Git

    $ git config --global user.name "moralok"
    $ git config --global user.email "wrmao.public@outlook.com"
    + + +

    SSH 的原理

    关于 SSH

    使用 SSH 协议可以连接远程服务器和服务并向它们验证,而无需在每次访问时都提供用户名和密码,Github 还可以使用 SSH 密钥对提交进行签名。

    +

    公钥和私钥

    SSH 的使用(非对称加密)需要生成公钥 public key 和私钥 private key。常用的算法有 rsaecdsaed25519,相对应的公钥默认文件名即id_XXX.pub。ed25519 的安全性介于 rsa 2048rsa 4096 之间,但性能却提升数十倍。
    在生成密钥时,会要求你 Enter passphrase (empty for no passphrase):,可以输入一个口令保护私钥的使用。不为空的情况下,正常使用是需要输入这个口令的,很多人认为麻烦,因此留空。
    公钥的权限必须是 644,私钥的权限必须是 600,否则 SSH 认为其不可靠。
    私钥是要安全保管在客户端不能泄露的,公钥则要提供给远程服务器或服务。服务端的 ~/.ssh/authorized_keys 里面存储着可以登录的客户端的公钥。我们将公钥粘贴到 Github 的过程就是对应于此。

    +
    $ ssh-keygen -t rsa -b 4096 -f my_id -C "email@example.com"
    +
      +
    • -t 表示算法,如 rsa
    • +
    • -b 表示 rsa 密钥长度,默认 2048 bit,ed25519 不需要指定。
    • +
    • -f 表示文件名。
    • +
    • -C 表示在公钥文件中添加注释,可修改
    • +
    +

    SSH 公钥登录过程

      +
    1. Client 将自己的公钥存放到服务端,追加到 authorized_keys 文件。
    2. +
    3. Server 收到 Client 的连接请求后,会在 authorized_keys 文件中匹配到 Client 传过来的公钥,并生成随机数 R,用公钥对随机数加密得到 pubKey(R)。
    4. +
    5. Client 收到后通过私钥解密得到随机数 R,然后对随机数 R 和本次会话的 sessionKey 使用 MD5 生成摘要 Digest1,发送给服务端。
    6. +
    7. Server 会对随机数 R 和会话的 sessionKey 同样使用 MD5 生成摘要 Digest2,对比相同即完成认证过程。
    -

    举一反三,像 aptgit 这类软件也是有其设置代理的方法。当你的代理稳定但是相应的访问失败时,大胆假设你的代理没有设置成功。要理清楚,当前的访问是谁发起的,才能正确地使用关键词搜索到正确的设置方式。

    -
    -

    原本我在 docker 相关的使用中,有关代理的设置方式是通过修改配置文件,实现永久、全局的代理配置。但是在后续的使用中,发现代理在一些场景(比如使用 cloudflare tunnel)中会引起不易排查的问题,决定采用临时、局部的配置方式。

    -
    -

    参考链接

    Linux 让终端走代理的几种方法
    Linux ❀ wget设置代理
    配置Docker使用代理
    Docker的三种网络代理配置
    docker 设置代理,以及国内加速镜像设置

    +

    避免中间人攻击

    SSH 通过口令确认避免中间人攻击,如果用户第一次登录 Server,系统会提示:

    +
    $ ssh -T git@github.com
    The authenticity of host 'github.com (20.205.243.166)' can't be established.
    ECDSA key fingerprint is SHA256:p2QAMXNIC1TJYWeIOttrVc98/R1BUFWu3/LiyKgUfQM.
    Are you sure you want to continue connecting (yes/no/[fingerprint])? yes
    Warning: Permanently added 'github.com,20.205.243.166' (ECDSA) to the list of known hosts.
    Hi ${username}! You've successfully authenticated, but GitHub does not provide shell access.
    +

    Server 需要在其网站上公示其公钥的指纹,Github 的公钥指纹在这里
    确认匹配后,客户端会在 ~/.ssh/known_hosts 中记录,下次登录不再警告。

    +

    使用 SSH 免密登录服务器

    使用现成的密钥,将 ~/.ssh/id_ed25519.pub 的内容追加到自己服务端的 ~/.ssh/authorized_keys 中,使用 ssh user@host 成功免密登录。
    这样一来,使用 VScode 远程连接服务器和使用 Remote Explorer 打开文件夹时,不用每次都输入密码了。

    +

    参考链接

    使用 SSH 进行连接 Github
    Git 多台电脑共用SSH Key
    SSH协议登录过程详解
    GitHub 的 SSH 密钥指纹
    使用 Ed25519 算法生成你的 SSH 密钥

    ]]> - proxy - docker + ssh @@ -860,6 +860,78 @@ minikube + + Spring Bean 加载过程 + /2023/08/10/how-does-Spring-load-beans/ + Spring Bean 生命周期 + +

    获取 Bean

    获取指定 Bean 的入口方法是 getBean,在 Spring 上下文刷新过程中,就依次调用 AbstractBeanFactory#getBean(java.lang.String) 方法获取 non-lazy-init 的 Bean。

    +
    public Object getBean(String name) throws BeansException {
    // 具体工作由 doGetBean 完成
    return doGetBean(name, null, null, false);
    }
    + +

    deGetBean

    作为公共处理逻辑,由 AbstractBeanFactory 自己实现。

    +
    protected <T> T doGetBean(
    final String name, final Class<T> requiredType, final Object[] args, boolean typeCheckOnly)
    throws BeansException {
    // 转换名称:去除 FactoryBean 的前缀 &,将别名转换为规范名称
    final String beanName = transformedBeanName(name);
    Object bean;

    // 检查单例缓存中是否已存在
    Object sharedInstance = getSingleton(beanName);
    if (sharedInstance != null && args == null) {
    // ...
    // 如果已存在,直接返回该实例或者使用该实例(FactoryBean)创建并返回对象
    bean = getObjectForBeanInstance(sharedInstance, name, beanName, null);
    }
    else {
    // 如果当前 Bean 是一个正在创建中的 prototype 类型,表明可能发生循环引用
    // 注意:Spring 并未解决 prototype 类型的循环引用问题,要抛出异常
    if (isPrototypeCurrentlyInCreation(beanName)) {
    throw new BeanCurrentlyInCreationException(beanName);
    }

    // 如果当前 beanFactory 没有 bean 定义,去 parent beanFactory 中查找
    BeanFactory parentBeanFactory = getParentBeanFactory();
    if (parentBeanFactory != null && !containsBeanDefinition(beanName)) {
    String nameToLookup = originalBeanName(name);
    if (args != null) {
    return (T) parentBeanFactory.getBean(nameToLookup, args);
    }
    else {
    return parentBeanFactory.getBean(nameToLookup, requiredType);
    }
    }

    if (!typeCheckOnly) {
    // 标记为至少创建过一次
    markBeanAsCreated(beanName);
    }

    try {
    final RootBeanDefinition mbd = getMergedLocalBeanDefinition(beanName);
    checkMergedBeanDefinition(mbd, beanName, args);

    // 确保 bean 依赖的 bean(构造器参数) 都已实例化
    String[] dependsOn = mbd.getDependsOn();
    if (dependsOn != null) {
    for (String dep : dependsOn) {
    if (isDependent(beanName, dep)) {
    // 注意:Spring 并未解决构造器方法中的循环引用问题,要抛异常
    }
    // 注册依赖关系,确保先销毁被依赖的 bean
    registerDependentBean(dep, beanName);
    // 递归,获取依赖的 bean
    getBean(dep);
    }
    }
    }

    if (mbd.isSingleton()) {
    // 如果是单例类型(绝大多数都是此类型)
    // 再次从缓存中获取,如果仍不存在,则使用传入的 ObjectFactory 创建
    sharedInstance = getSingleton(beanName, new ObjectFactory<Object>(
    {
    @Override
    public Object getObject() throws BeansException {
    try {
    // 创建 bean
    return createBean(beanName, mbd, args);
    }
    catch (BeansException ex) {
    // 由于可能已经提前暴露,需要显示地销毁
    destroySingleton(beanName);
    throw ex;
    }
    }
    });
    bean = getObjectForBeanInstance(sharedInstance, name, beanName, mbd);
    }
    else if (mbd.isPrototype()) {
    // 如果是原型类型,每次都新创建一个
    // ...
    }
    else {
    // 如果是其他 scope 类型
    // ...
    }
    }
    catch (BeansException ex) {
    cleanupAfterBeanCreationFailure(beanName);
    throw ex;
    }
    }
    + +

    getSingleton

    public Object getSingleton(String beanName, ObjectFactory<?> singletonFactory) {
    // 加锁
    synchronized (this.singletonObjects) {
    // 再次从缓存中获取(和调用前从缓存中获取构成双重校验)
    Object singletonObject = this.singletonObjects.get(beanName);
    if (singletonObject == null) {
    if (this.singletonsCurrentlyInDestruction) {
    // 如果正在销毁单例,则抛异常
    // 注意:不要在销毁方法中调用获取 bean 方法
    }
    // 创建前,先注册到正在创建中的集合
    // 在出现循环引用时,第二次进入 doGetBean,用此作为判断标志
    beforeSingletonCreation(beanName);
    boolean newSingleton = false;
    // ...
    try {
    // 使用传入的单例工厂创建对象
    singletonObject = singletonFactory.getObject();
    newSingleton = true;
    }
    catch (IllegalStateException ex) {
    // 如果异常的出现是因为 bean 被创建了,就忽略异常,否则抛出异常
    singletonObject = this.singletonObjects.get(beanName);
    if (singletonObject == null) {
    throw ex;
    }
    }
    catch (BeanCreationException ex) {
    // ...
    }
    finally {
    // ...
    // 创建后,从正在创建中集合移除
    afterSingletonCreation(beanName);
    }
    if (newSingleton) {
    // 添加单例到缓存
    addSingleton(beanName, singletonObject);
    }
    }
    return (singletonObject != NULL_OBJECT ? singletonObject : null);
    }
    }
    + +

    创建 Bean

    createBean 是创建 Bean 的入口方法,由 AbstractBeanFactory 定义,由 AbstractAutowireCapableBeanFactory 实现。

    +
    protected Object createBean(String beanName, RootBeanDefinition mbd, Object[] args) throws BeanCreationException {
    // ...
    try {
    // 给 Bean 后置处理器一个返回代理的机会
    Object bean = resolveBeforeInstantiation(beanName, mbdToUse);
    if (bean != null) {
    return bean;
    }
    }
    // ...
    // 常规的创建 Bean
    Object beanInstance = doCreateBean(beanName, mbdToUse, args);
    return beanInstance;
    }
    + +

    doCreateBean

    常规的创建 Bean 的具体工作是由 doCreateBean 完成的。

    +
    protected Object doCreateBean(final String beanName, final RootBeanDefinition mbd, final Object[] args) throws BeanCreationException {
    BeanWrapper instanceWrapper = null;
    if (mbd.isSingleton()) {
    instanceWrapper = this.factoryBeanInstanceCache.remove(beanName);
    }
    if (instanceWrapper == null) {
    // 使用相应的策略创建 bean 实例,例如通过工厂方法或者有参、无参构造器方法
    instanceWrapper = createBeanInstance(beanName, mbd, args);
    }
    final Object bean = (instanceWrapper != null ? instanceWrapper.getWrappedInstance() : null);
    Class<?> beanType = (instanceWrapper != null ? instanceWrapper.getWrappedClass() : null);
    mbd.resolvedTargetType = beanType;

    // ...

    // 使用 ObjectFactory 封装实例并缓存,以解决循环引用问题
    boolean earlySingletonExposure = (mbd.isSingleton()
    && this.allowCircularReferences
    && isSingletonCurrentlyInCreation(beanName));
    if (earlySingletonExposure) {
    addSingletonFactory(beanName, new ObjectFactory<Object>() {
    @Override
    public Object getObject() throws BeansException {
    return getEarlyBeanReference(beanName, mbd, bean);
    }
    });
    }

    Object exposedObject = bean;
    try {
    // 填充属性(包括解析依赖的 bean)
    populateBean(beanName, mbd, instanceWrapper);
    if (exposedObject != null) {
    // 初始化 bean
    exposedObject = initializeBean(beanName, exposedObject, mbd);
    }
    }
    // ...

    // 如有需要,将 bean 注册为一次性的,以供 beanFactory 在关闭时调用销毁方法
    try {
    registerDisposableBeanIfNecessary(beanName, bean, mbd);
    }
    // ...

    return exposedObject;
    }
    + +

    createBeanInstance

    创建 Bean 实例,并使用 BeanWrapper 封装。实例化的方式:

    +
      +
    1. 工厂方法
    2. +
    3. 构造器方法
        +
      1. 有参
      2. +
      3. 无参
      4. +
      +
    4. +
    +

    populateBean

    为创建出的实例填充属性,包括解析当前 bean 所依赖的 bean。

    +
    protected void populateBean(String beanName, RootBeanDefinition mbd, BeanWrapper bw) {
    PropertyValues pvs = mbd.getPropertyValues();
    // ...

    // 给 InstantiationAwareBeanPostProcessors 一个机会,
    // 在设置 bean 属性前修改 bean 状态,可用于自定义的字段注入
    boolean continueWithPropertyPopulation = true;
    if (!mbd.isSynthetic() && hasInstantiationAwareBeanPostProcessors()) {
    for (BeanPostProcessor bp : getBeanPostProcessors()) {
    if (bp instanceof InstantiationAwareBeanPostProcessor) {
    InstantiationAwareBeanPostProcessor ibp = (InstantiationAwareBeanPostProcessor) bp;
    if (!ibp.postProcessAfterInstantiation(bw.getWrappedInstance(), beanName)) {
    continueWithPropertyPopulation = false;
    break;
    }
    }
    }
    }

    // 是否继续填充属性的流程
    if (!continueWithPropertyPopulation) {
    return;
    }

    if (mbd.getResolvedAutowireMode() == RootBeanDefinition.AUTOWIRE_BY_NAME
    || mbd.getResolvedAutowireMode() == RootBeanDefinition.AUTOWIRE_BY_TYPE) {
    MutablePropertyValues newPvs = new MutablePropertyValues(pvs);
    // 根据名称注入
    if (mbd.getResolvedAutowireMode() == RootBeanDefinition.AUTOWIRE_BY_NAME) {
    autowireByName(beanName, mbd, bw, newPvs);
    }

    // 根据类型注入
    if (mbd.getResolvedAutowireMode() == RootBeanDefinition.AUTOWIRE_BY_TYPE) {
    autowireByType(beanName, mbd, bw, newPvs);
    }
    pvs = newPvs;
    }

    // 是否存在 InstantiationAwareBeanPostProcessors
    boolean hasInstAwareBpps = hasInstantiationAwareBeanPostProcessors();
    // 是否需要检查依赖
    boolean needsDepCheck = (mbd.getDependencyCheck() != RootBeanDefinition.DEPENDENCY_CHECK_NONE);

    if (hasInstAwareBpps || needsDepCheck) {
    PropertyDescriptor[] filteredPds = filterPropertyDescriptorsForDependencyCheck(bw, mbd.allowCaching);
    if (hasInstAwareBpps) {
    // 后置处理 PropertyValues
    for (BeanPostProcessor bp : getBeanPostProcessors()) {
    if (bp instanceof InstantiationAwareBeanPostProcessor) {
    InstantiationAwareBeanPostProcessor ibp = (InstantiationAwareBeanPostProcessor) bp;
    pvs = ibp.postProcessPropertyValues(pvs, filteredPds, bw.getWrappedInstance(), beanName);
    if (pvs == null) {
    return;
    }
    }
    }
    }
    if (needsDepCheck) {
    checkDependencies(beanName, mbd, filteredPds, pvs);
    }
    }
    // 将属性应用到 bean 上(常规情况下,前面的处理都用不上)
    applyPropertyValues(beanName, mbd, bw, pvs);
    }
    + +

    initializeBean

    在填充完属性后,实例就可以进行初始化工作:

    +
      +
    1. invokeAwareMethods,让 Bean 通过 xxxAware 接口感知一些信息
    2. +
    3. 调用 BeanPostProcessor 的 postProcessBeforeInitialization 方法
    4. +
    5. invokeInitMethods,调用初始化方法
    6. +
    7. 调用 BeanPostProcessor 的 postProcessAfterInitialization 方法
    8. +
    +
    protected Object initializeBean(final String beanName, final Object bean, RootBeanDefinition mbd) {
    // 处理 Aware 接口的相应方法
    if (System.getSecurityManager() != null) {
    AccessController.doPrivileged(new PrivilegedAction<Object>() {
    @Override
    public Object run() {
    invokeAwareMethods(beanName, bean);
    return null;
    }
    }, getAccessControlContext());
    }
    else {
    invokeAwareMethods(beanName, bean);
    }

    // 应用 BeanPostProcessor 的 postProcessBeforeInitialization 方法
    Object wrappedBean = bean;
    if (mbd == null || !mbd.isSynthetic()) {
    wrappedBean = applyBeanPostProcessorsBeforeInitialization(wrappedBean, beanName);
    }

    try {
    // 调用初始化方法
    invokeInitMethods(beanName, wrappedBean, mbd);
    }
    catch (Throwable ex) {
    throw new BeanCreationException(
    (mbd != null ? mbd.getResourceDescription() : null),
    beanName, "Invocation of init method failed", ex);
    }

    if (mbd == null || !mbd.isSynthetic()) {
    // 应用 BeanPostProcessor 的 postProcessAfterInitialization 方法
    wrappedBean = applyBeanPostProcessorsAfterInitialization(wrappedBean, beanName);
    }
    return wrappedBean;
    }
    + +
    处理 Aware 接口的相应方法

    让 Bean 在初始化中,感知(获知)和自身相关的资源,如 beanName、beanClassLoader 或者 beanFactory。

    +
    private void invokeAwareMethods(final String beanName, final Object bean) {
    if (bean instanceof Aware) {
    if (bean instanceof BeanNameAware) {
    ((BeanNameAware) bean).setBeanName(beanName);
    }
    if (bean instanceof BeanClassLoaderAware) {
    ((BeanClassLoaderAware) bean).setBeanClassLoader(getBeanClassLoader());
    }
    if (bean instanceof BeanFactoryAware) {
    ((BeanFactoryAware) bean).setBeanFactory(AbstractAutowireCapableBeanFactory.this);
    }
    }
    }
    + +
    调用初始化方法
      +
    1. 如果 bean 实现 InitializingBean 接口,调用 afterPropertiesSet 方法
    2. +
    3. 如果自定义 init 方法且满足调用条件,同样进行调用
    4. +
    +
    protected void invokeInitMethods(String beanName, final Object bean, RootBeanDefinition mbd) throws Throwable {
    // 是否实现 InitializingBean 接口,是的话调用 afterPropertiesSet 方法
    // 给 bean 一个感知属性已设置并做出反应的机会
    boolean isInitializingBean = (bean instanceof InitializingBean);
    if (isInitializingBean
    && (mbd == null || !mbd.isExternallyManagedInitMethod("afterPropertiesSet"))) {
    if (System.getSecurityManager() != null) {
    try {
    AccessController.doPrivileged(new PrivilegedExceptionAction<Object>() {
    @Override
    public Object run() throws Exception {
    ((InitializingBean) bean).afterPropertiesSet();
    return null;
    }
    }, getAccessControlContext());
    }
    catch (PrivilegedActionException pae) {
    throw pae.getException();
    }
    }
    else {
    ((InitializingBean) bean).afterPropertiesSet();
    }
    }

    // 如果存在自定义的 init 方法且方法名称不是 afterPropertiesSet,判断是否调用
    if (mbd != null) {
    String initMethodName = mbd.getInitMethodName();
    if (initMethodName != null
    && !(isInitializingBean && "afterPropertiesSet".equals(initMethodName))
    && !mbd.isExternallyManagedInitMethod(initMethodName)) {
    invokeCustomInitMethod(beanName, bean, mbd);
    }
    }
    }
    + +
    BeanPostProcessor 处理

    在调用初始化方法前后,BeanPostProcessor 先后进行两次处理。其实和 BeanPostProcessor 相关的代码都非常相似:

    +
      +
    1. 获取 Processor 列表
    2. +
    3. 判断 Processor 类型是否是当前需要的
    4. +
    5. 对 bean 进行处理
    6. +
    +
    public Object applyBeanPostProcessorsBeforeInitialization(Object existingBean, String beanName) throws BeansException {

    Object result = existingBean;
    for (BeanPostProcessor beanProcessor : getBeanPostProcessors()) {
    result = beanProcessor.postProcessBeforeInitialization(result, beanName);
    if (result == null) {
    return result;
    }
    }
    return result;
    }

    public Object applyBeanPostProcessorsAfterInitialization(Object existingBean, String beanName) throws BeansException {

    Object result = existingBean;
    for (BeanPostProcessor beanProcessor : getBeanPostProcessors()) {
    result = beanProcessor.postProcessAfterInitialization(result, beanName);
    if (result == null) {
    return result;
    }
    }
    return result;
    }
    + +

    再思 Bean 的初始化

    以下代码片段一度让我困惑,从注释看,初始化 Bean 实例的工作包括了 populateBean 和 initializeBean,但是 initializeBean 方法的含义就是初始化 Bean。在 initializeBean 方法中,调用了 invokeInitMethods 方法,其含义仍然是调用初始化方法。
    在更熟悉代码后,我有一种微妙的、个人性的体会,在 Spring 源码中,有时候视角的变化是很快的,痕迹是很小的。如果不加以理解和区分,很容易迷失在相似的描述中。以此处为例,“初始化 Bean 和 Bean 的初始化”扩展开来是 “Bean 工厂初始化一个 Bean 和 Bean 自身进行初始化”。

    +
    // Initialize the bean instance.
    Object exposedObject = bean;
    try {
    populateBean(beanName, mbd, instanceWrapper);
    if (exposedObject != null) {
    exposedObject = initializeBean(beanName, exposedObject, mbd);
    }
    }
    + +

    在注释这一行,视角是属于 BeanFactory(AbstractAutowireCapableBeanFactory)。从工厂的视角,面对一个刚刚创建出来的 Bean 实例,需要完成两方面的工作:

    +
      +
    1. 为 Bean 实例填充属性,包括解析依赖,为 Bean 自身的初始化做好准备。
    2. +
    3. Bean 自身的初始化。
    4. +
    +

    在万事俱备之后,就是 Bean 自身的初始化工作。由于 Spring 的高度扩展性,这部分并不只是单纯地调用初始化方法,还包含 Aware 接口和 BeanPostProcessor 的相关处理,前者偏属于 Java 对象层面,后者偏属于 Spring Bean 层面。
    在认同 BeanPostProcessor 的处理属于 Bean 自身初始化工作的一部分后,@PostConstruct 注解的方法被称为 Bean 的初始化方法也就不那么违和了,因为它的实现原理正是 BeanPostProcessor,这仍然在 initializeBean 的范围内。

    +]]>
    + + java + spring + +
    JVM GC 的测试和分析 /2023/11/01/testing-and-analysis-of-jvm-gc/ @@ -930,7 +1002,7 @@

    尽管总体上有迹可循,但是 GC 的具体情况,仍然需要具体分析,有很多分支情况未一一确认。

    ]]> - Java + java jvm
    @@ -962,306 +1034,88 @@

    使用 javap -v .\StringTableTest_1.class 进行反编译,摘取重要部分:

    Constant pool:
    #1 = Methodref #6.#24 // java/lang/Object."<init>":()V
    #2 = String #25 // a
    #3 = String #26 // b
    #4 = String #27 // ab

    #25 = Utf8 a
    #26 = Utf8 b
    #27 = Utf8 ab



    0: ldc #2 // String a
    2: astore_1
    3: ldc #3 // String b
    5: astore_2
    6: ldc #4 // String ab
    8: astore_3
    9: return
      -
    • Class 文件中的常量池 Constant pool 会记录代码中出现的字面量(文本文件)。
    • -
    • 运行时常量池是方法区的一部分,Class 文件中的常量池的内容,在类加载后,就进入了运行时常量池中(内存中的数据)。
    • -
    • 字符串常量池,记录 interned string 的一个全局表,JDK 6 前在方法区,后移到堆中。
    • -
    -

    字符串常量池的位置和形式

    在《深入理解Java虚拟机》提到:字符串常量池的位置从 JDK 7 开始,从永久代中移到了堆中。在这句话中,字符串常量池像是一个特定的内存区域,存储了 interned string 的实例。

    - - -

    验证字符串常量池的位置

    书中使用了以下方式来验证字符串常量池的位置。

    -
    public class StringTableTest_8 {  

    // JDK 1.8 设置 -Xmx10m -XX:-UseGCOverheadLimit
    // JDK 1.6 设置 -XX:MaxPerSize=10m
    public static void main(String[] args) {
    List<String> list = new ArrayList<>();
    int i = 0;
    try {
    for (int j = 0; j < 260000; j++) {
    list.add(String.valueOf(j).intern());
    i++;
    }
    } catch (Exception e) {
    e.printStackTrace();
    } finally {
    System.out.println(i);
    }
    }
    }
    -

    在 JDK 8 中异常如下:

    -
    Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
    -

    在 JDK 6 中异常如下:

    -
    java.lang.OutOfMemoryError: PermGen space
    - -

    同时书中也提到了,在字符串常量池的位置改变后,它只用保存第一次出现时字符串对象的引用。JDK 8 中的 intern 方法可以印证该说法,方法注释中提到:如果字符串常量池中已存在相等(equals)的字符串,那就返回已存在的对象(这样原先准备加入的对象就可以释放);否则,将字符串对象加入字符串常量池中,直接返回对该对象的引用(不用像 JDK 6 时,复制一个对象加入常量池,返回该复制对象的引用)。

    -

    关于 intern 的实验

    public class StringTableTest_5 {  

    public static void main(String[] args) {
    // "a"、"b" 作为字符串字面量,会解析得到字符串对象放入字符串常量池
    // 但是 new String("a") 创建出来的字符串对象,不会进入字符串常量池
    String s1 = new String("a") + new String("b");
    // intern 方法尝试将 s1 放入 StringTable,无则放入,返回该对象引用,有则返回已存在对象的引用
    String s2 = s1.intern();

    String x = "ab";

    System.out.println(s2 == x);
    System.out.println(s1 == x);
    }
    }

    public class StringTableTest_6 {

    public static void main(String[] args) {
    // 将 "ab" 的赋值语句提前到最开始,"ab" 生成的字符串对象进入字符串常量池
    String x = "ab";
    String s1 = new String("a") + new String("b");
    // intern 方法尝试将 s1 放入 StringTable,无则放入,返回该对象引用,有则返回已存在对象的引用
    String s2 = s1.intern();

    System.out.println(s2 == x);
    System.out.println(s1 == x);
    }
    }
    -

    实验结果证实了上述说法。

    -

    字符串常量池到底是什么?

    但是 xinxi 提及:字符串常量池,也称为 StringTable,本质上是一个惰性维护的哈希表,是一个纯运行时的结构,只存储对 java.lang.String 实例的引用,而不存储 String 对象的内容。当我们提到一个字符串进入字符串常量池其实是说在这个 StringTable 中保存了对它的引用,反之,如果说没有在其中就是说 StringTable 中没有对它的引用。
    zyplanke 分析 StringTable 在内存中的形式时,也表达了类似的观点。

    - - -

    尽管这个疑问似乎不妨碍我们理解很多东西,但是深究之后,真的让人困惑,网上也没有搜集到更多的信息。字符串常量池和 StringTable 是否等价?字符串常量池更准确的说法是否是“一个保存引用的 StringTable 加上分布在堆(JDK 6 以前的永久代)中的字符串实例”?
    已经好几次打开 jvm 的源码,却看不懂它到底什么意思啊!!!!!难道是时候开始学 C++ 了吗。

    -

    进入字符串常量池的时机

    前面提到了第一次遇到的字符串字面量会在某一个时刻,生成对应的字符串对象进入字符串常量池,同时也提到了,字符串常量池(StringTable)的维护是懒惰的,那么这些究竟是什么时候发生的呢?

    -
    public class StringTableTest_12 {

    public static void main(String[] args) throws IOException {
    new String("ab");
    }
    }
    - -
     0: new           #2                  // class java/lang/String
    3: dup
    4: ldc #3 // String ab
    6: invokespecial #4 // Method java/lang/String."<init>":(Ljava/lang/String;)V
    9: pop
    10: return
    - -

    RednaxelaFX 的文章提到:

    -
    -

    在类加载阶段,JVM 会在堆中创建对应这些 class 文件常量池中的字符串对象实例,并在字符串常量池中驻留其引用。具体在 resolve 阶段执行。这些常量全局共享。

    -
    -

    xinxi 的文章中补充到:

    -
    -

    这里说的比较笼统,没错,是 resolve 阶段,但是并不是大家想的那样,立即就创建对象并且在字符串常量池中驻留了引用。 JVM规范里明确指定resolve阶段可以是lazy的。
    ……
    就 HotSpot VM 的实现来说,加载类的时候,那些字符串字面量会进入到当前类的运行时常量池,不会进入全局的字符串常量池(即在 StringTable 中并没有相应的引用,在堆中也没有对应的对象产生)。

    -
    -

    《深入理解Java虚拟机》中提到:

    -
    -

    《Java虚拟机规范》之中并未规定解析阶段发生的具体时间,只要求了在执行ane-warray、checkcast、getfield、getstatic、instanceof、invokedynamic、invokeinterface、invoke-special、invokestatic、invokevirtual、ldc、ldc_w、ldc2_w、multianewarray、new、putfield和putstatic这17个用于操作符号引用的字节码指令之前,先对它们所使用的符号引用进行解析。所以虚拟机实现可以根据需要来自行判断,到底是在类被加载器加载时就对常量池中的符号引用进行解析,还是等到一个符号引用将要被使用前才去解析它。

    -
    -

    综上可知,字符串字面量的解析是属于类加载的解析阶段,但是《Java虚拟机规范》并未规定解析发生的具体时间,只要求在执行一些字节码指令前进行,其中包括了 ldc 指令。虚拟机的具体实现,比如 Hotspot 就在执行 ldc #indexNumber 前触发解析,根据字符串常量池中是否已存在字符串对象决定是否创建对象,并将对象推送到栈顶。
    这也证实了前文中提到的字符串字面量生成字符串对象和 new 关键字无关。

    -

    验证延迟实例化

    使用 IDEA memory 功能,观察字符串对象的个数逐个变化。

    -
      -
    1. 直到第一次运行到字符串字面量时,才会创建对应的字符串对象。
    2. -
    3. 相同的字符串常量,不会重复创建字符串对象。
    4. -
    -
    public class StringTableTest_4 {  

    public static void main(String[] args) {
    System.out.println();

    System.out.println("1");
    System.out.println("2");
    System.out.println("3");
    System.out.println("4");
    System.out.println("5");
    System.out.println("6");
    System.out.println("7");
    System.out.println("8");
    System.out.println("9");
    System.out.println("0");
    System.out.println("1");
    System.out.println("2");
    System.out.println("3");
    System.out.println("4");
    System.out.println("5");
    System.out.println("6");
    System.out.println("7");
    System.out.println("8");
    System.out.println("9");
    System.out.println("0");
    }
    }
    - - - -

    字符串常量池的垃圾回收和性能优化

    垃圾回收

    前文提到字符串常量池在 JDK 7 开始移到堆中,是因为考虑在方法区中的垃圾回收是比较困难的,同时随着字节码技术的发展,CGLib 等会大量动态生成类的技术的运用使得方法区的内存紧张,将字符串常量池移到堆中,可以有效提高其垃圾回收效率。

    -
    public class StringTableTest_9 {  

    // -Xmx10m -XX:+PrintStringTableStatistics -XX:+PrintGCDetails -verbose:gc
    public static void main(String[] args) {
    int i = 0;
    try {
    // 0->100->10000,观察统计信息中数量的变化以及垃圾回收记录
    for (int j = 0; j < 10000; j++) {
    String.valueOf(j).intern();
    i++;
    }
    } catch (Exception e) {
    e.printStackTrace();
    } finally {
    System.out.println(i);
    }
    }
    }
    - - -
    [GC (Allocation Failure) [PSYoungGen: 2048K->488K(2560K)] 2048K->856K(9728K), 0.0007745 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 


    StringTable statistics:
    Number of buckets : 60013 = 480104 bytes, avg 8.000
    Number of entries : 7277 = 174648 bytes, avg 24.000
    Number of literals : 7277 = 421560 bytes, avg 57.930
    Total footprint : = 1076312 bytes
    Average bucket size : 0.121
    Variance of bucket size : 0.125
    Std. dev. of bucket size: 0.354
    Maximum bucket size : 3
    - -

    性能优化

    调整 buckets size

    当 size 过小,哈希碰撞增加,链表变长,效率会变低,需要增大 buckets size。

    -
    public class StringTableTest_10 {  

    // -XX:StringTableSize=200000 -XX:+PrintStringTableStatistics
    // 默认->200000->1009(最小值),观察耗时
    public static void main(String[] args) {
    try (BufferedReader br = new BufferedReader(new InputStreamReader(new FileInputStream("src/main/resources/linux.words"), StandardCharsets.UTF_8))) {
    String line = null;
    long start = System.nanoTime();
    while (true) {
    line = br.readLine();
    if (line == null) {
    break;
    }
    line.intern();
    }
    System.out.println("cost: " + (System.nanoTime() - start) / 1000000) ;
    } catch (IOException e) {
    throw new RuntimeException(e);
    }
    }
    }
    - -

    主动运用 intern 的场景

    当你需要大量缓存重复的字符串时,使用 intern 可以大大减少内存占用。

    -
    public class StringTableTest_11 {  

    // -Xms500m -Xmx500m -XX:StringTableSize=200000 -XX:+PrintStringTableStatistics
    public static void main(String[] args) throws IOException {
    List<String> words = new ArrayList<>();
    System.in.read();
    for (int i = 0; i < 10; i++) {
    try (BufferedReader br = new BufferedReader(new InputStreamReader(new FileInputStream("src/main/resources/linux.words"), StandardCharsets.UTF_8))) {
    String line = null;
    long start = System.nanoTime();
    while (true) {
    line = br.readLine();
    if (line == null) {
    break;
    }
    // words.add(line);
    words.add(line.intern());
    }
    System.out.println("cost: " + (System.nanoTime() - start) / 1000000) ;
    }
    }
    System.in.read();
    }
    }
    - -

    使用 VisualVM 观察字符串和 char[] 内存占用情况,可以发现提升显著。

    - - -

    字符串拼接

    变量的拼接

    字符串变量的拼接,底层是使用 StringBuilder 实现:new StringBuilder().append("a").append("b").toString(),而 toString 方法使用拼接得到的 char 数组创建一个新的 String 对象,因此 s3 和 s4 是不相同的两个对象。

    -
    public class StringTableTest_2 {  

    public static void main(String[] args) {
    String s1 = "a";
    String s2 = "b";
    String s3 = "ab";
    String s4 = s1 + s2;
    }
    }
    - -
     0: ldc           #2                  // String a
    2: astore_1
    3: ldc #3 // String b
    5: astore_2
    6: ldc #4 // String ab
    8: astore_3
    9: new #5 // class java/lang/StringBuilder
    12: dup
    13: invokespecial #6 // Method java/lang/StringBuilder."<init>":()V
    16: aload_1
    17: invokevirtual #7 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
    20: aload_2
    21: invokevirtual #7 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
    24: invokevirtual #8 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
    27: astore 4
    29: return
    - -

    常量的拼接

    字符串常量的拼接是在编译期间,因为已知结果而被优化为一个字符串常量。又因为 “ab” 字符串在 StringTable 中是已存在的,所以不会重新创建新对象。

    -
    public class StringTableTest_3 {

    public static void main(String[] args) {
    String s1 = "a";
    String s2 = "b";
    String s3 = "ab";
    String s4 = s1 + s2;
    String s5 = "a" + "b";
    }
    }
    - -
     0: ldc           #2                  // String a
    2: astore_1
    3: ldc #3 // String b
    5: astore_2
    6: ldc #4 // String ab
    8: astore_3
    9: new #5 // class java/lang/StringBuilder
    12: dup
    13: invokespecial #6 // Method java/lang/StringBuilder."<init>":()V
    16: aload_1
    17: invokevirtual #7 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
    20: aload_2
    21: invokevirtual #7 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
    24: invokevirtual #8 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
    27: astore 4
    29: ldc #4 // String ab
    31: astore 5
    33: return
    - -

    参考文章

      -
    1. Java 中new String(“字面量”) 中 “字面量” 是何时进入字符串常量池的? - xinxi的回答 - 知乎
    2. -
    3. 请别再拿“String s = new String(“xyz”);创建了多少个String实例”来面试了吧
    4. -
    5. JVM中字符串常量池StringTable在内存中形式分析
    6. -
    -]]> - - Java - jvm - - - - Spring Bean 加载过程 - /2023/11/17/how-does-Spring-load-beans/ - Spring Bean 生命周期 - -

    获取 Bean

    获取指定 Bean 的入口方法是 getBean,在 Spring 上下文刷新过程中,就依次调用 AbstractBeanFactory#getBean(java.lang.String) 方法获取 non-lazy-init 的 Bean。

    -
    public Object getBean(String name) throws BeansException {
    // 具体工作由 doGetBean 完成
    return doGetBean(name, null, null, false);
    }
    - -

    deGetBean

    作为公共处理逻辑,由 AbstractBeanFactory 自己实现。

    -
    protected <T> T doGetBean(
    final String name, final Class<T> requiredType, final Object[] args, boolean typeCheckOnly)
    throws BeansException {
    // 转换名称:去除 FactoryBean 的前缀 &,将别名转换为规范名称
    final String beanName = transformedBeanName(name);
    Object bean;

    // 检查单例缓存中是否已存在
    Object sharedInstance = getSingleton(beanName);
    if (sharedInstance != null && args == null) {
    // ...
    // 如果已存在,直接返回该实例或者使用该实例(FactoryBean)创建并返回对象
    bean = getObjectForBeanInstance(sharedInstance, name, beanName, null);
    }
    else {
    // 如果当前 Bean 是一个正在创建中的 prototype 类型,表明可能发生循环引用
    // 注意:Spring 并未解决 prototype 类型的循环引用问题,要抛出异常
    if (isPrototypeCurrentlyInCreation(beanName)) {
    throw new BeanCurrentlyInCreationException(beanName);
    }

    // 如果当前 beanFactory 没有 bean 定义,去 parent beanFactory 中查找
    BeanFactory parentBeanFactory = getParentBeanFactory();
    if (parentBeanFactory != null && !containsBeanDefinition(beanName)) {
    String nameToLookup = originalBeanName(name);
    if (args != null) {
    return (T) parentBeanFactory.getBean(nameToLookup, args);
    }
    else {
    return parentBeanFactory.getBean(nameToLookup, requiredType);
    }
    }

    if (!typeCheckOnly) {
    // 标记为至少创建过一次
    markBeanAsCreated(beanName);
    }

    try {
    final RootBeanDefinition mbd = getMergedLocalBeanDefinition(beanName);
    checkMergedBeanDefinition(mbd, beanName, args);

    // 确保 bean 依赖的 bean(构造器参数) 都已实例化
    String[] dependsOn = mbd.getDependsOn();
    if (dependsOn != null) {
    for (String dep : dependsOn) {
    if (isDependent(beanName, dep)) {
    // 注意:Spring 并未解决构造器方法中的循环引用问题,要抛异常
    }
    // 注册依赖关系,确保先销毁被依赖的 bean
    registerDependentBean(dep, beanName);
    // 递归,获取依赖的 bean
    getBean(dep);
    }
    }
    }

    if (mbd.isSingleton()) {
    // 如果是单例类型(绝大多数都是此类型)
    // 再次从缓存中获取,如果仍不存在,则使用传入的 ObjectFactory 创建
    sharedInstance = getSingleton(beanName, new ObjectFactory<Object>(
    {
    @Override
    public Object getObject() throws BeansException {
    try {
    // 创建 bean
    return createBean(beanName, mbd, args);
    }
    catch (BeansException ex) {
    // 由于可能已经提前暴露,需要显示地销毁
    destroySingleton(beanName);
    throw ex;
    }
    }
    });
    bean = getObjectForBeanInstance(sharedInstance, name, beanName, mbd);
    }
    else if (mbd.isPrototype()) {
    // 如果是原型类型,每次都新创建一个
    // ...
    }
    else {
    // 如果是其他 scope 类型
    // ...
    }
    }
    catch (BeansException ex) {
    cleanupAfterBeanCreationFailure(beanName);
    throw ex;
    }
    }
    - -

    getSingleton

    public Object getSingleton(String beanName, ObjectFactory<?> singletonFactory) {
    // 加锁
    synchronized (this.singletonObjects) {
    // 再次从缓存中获取(和调用前从缓存中获取构成双重校验)
    Object singletonObject = this.singletonObjects.get(beanName);
    if (singletonObject == null) {
    if (this.singletonsCurrentlyInDestruction) {
    // 如果正在销毁单例,则抛异常
    // 注意:不要在销毁方法中调用获取 bean 方法
    }
    // 创建前,先注册到正在创建中的集合
    // 在出现循环引用时,第二次进入 doGetBean,用此作为判断标志
    beforeSingletonCreation(beanName);
    boolean newSingleton = false;
    // ...
    try {
    // 使用传入的单例工厂创建对象
    singletonObject = singletonFactory.getObject();
    newSingleton = true;
    }
    catch (IllegalStateException ex) {
    // 如果异常的出现是因为 bean 被创建了,就忽略异常,否则抛出异常
    singletonObject = this.singletonObjects.get(beanName);
    if (singletonObject == null) {
    throw ex;
    }
    }
    catch (BeanCreationException ex) {
    // ...
    }
    finally {
    // ...
    // 创建后,从正在创建中集合移除
    afterSingletonCreation(beanName);
    }
    if (newSingleton) {
    // 添加单例到缓存
    addSingleton(beanName, singletonObject);
    }
    }
    return (singletonObject != NULL_OBJECT ? singletonObject : null);
    }
    }
    - -

    创建 Bean

    createBean 是创建 Bean 的入口方法,由 AbstractBeanFactory 定义,由 AbstractAutowireCapableBeanFactory 实现。

    -
    protected Object createBean(String beanName, RootBeanDefinition mbd, Object[] args) throws BeanCreationException {
    // ...
    try {
    // 给 Bean 后置处理器一个返回代理的机会
    Object bean = resolveBeforeInstantiation(beanName, mbdToUse);
    if (bean != null) {
    return bean;
    }
    }
    // ...
    // 常规的创建 Bean
    Object beanInstance = doCreateBean(beanName, mbdToUse, args);
    return beanInstance;
    }
    - -

    doCreateBean

    常规的创建 Bean 的具体工作是由 doCreateBean 完成的。

    -
    protected Object doCreateBean(final String beanName, final RootBeanDefinition mbd, final Object[] args) throws BeanCreationException {
    BeanWrapper instanceWrapper = null;
    if (mbd.isSingleton()) {
    instanceWrapper = this.factoryBeanInstanceCache.remove(beanName);
    }
    if (instanceWrapper == null) {
    // 使用相应的策略创建 bean 实例,例如通过工厂方法或者有参、无参构造器方法
    instanceWrapper = createBeanInstance(beanName, mbd, args);
    }
    final Object bean = (instanceWrapper != null ? instanceWrapper.getWrappedInstance() : null);
    Class<?> beanType = (instanceWrapper != null ? instanceWrapper.getWrappedClass() : null);
    mbd.resolvedTargetType = beanType;

    // ...

    // 使用 ObjectFactory 封装实例并缓存,以解决循环引用问题
    boolean earlySingletonExposure = (mbd.isSingleton()
    && this.allowCircularReferences
    && isSingletonCurrentlyInCreation(beanName));
    if (earlySingletonExposure) {
    addSingletonFactory(beanName, new ObjectFactory<Object>() {
    @Override
    public Object getObject() throws BeansException {
    return getEarlyBeanReference(beanName, mbd, bean);
    }
    });
    }

    Object exposedObject = bean;
    try {
    // 填充属性(包括解析依赖的 bean)
    populateBean(beanName, mbd, instanceWrapper);
    if (exposedObject != null) {
    // 初始化 bean
    exposedObject = initializeBean(beanName, exposedObject, mbd);
    }
    }
    // ...

    // 如有需要,将 bean 注册为一次性的,以供 beanFactory 在关闭时调用销毁方法
    try {
    registerDisposableBeanIfNecessary(beanName, bean, mbd);
    }
    // ...

    return exposedObject;
    }
    - -

    createBeanInstance

    创建 Bean 实例,并使用 BeanWrapper 封装。实例化的方式:

    -
      -
    1. 工厂方法
    2. -
    3. 构造器方法
        -
      1. 有参
      2. -
      3. 无参
      4. -
      -
    4. -
    -

    populateBean

    为创建出的实例填充属性,包括解析当前 bean 所依赖的 bean。

    -
    protected void populateBean(String beanName, RootBeanDefinition mbd, BeanWrapper bw) {
    PropertyValues pvs = mbd.getPropertyValues();
    // ...

    // 给 InstantiationAwareBeanPostProcessors 一个机会,
    // 在设置 bean 属性前修改 bean 状态,可用于自定义的字段注入
    boolean continueWithPropertyPopulation = true;
    if (!mbd.isSynthetic() && hasInstantiationAwareBeanPostProcessors()) {
    for (BeanPostProcessor bp : getBeanPostProcessors()) {
    if (bp instanceof InstantiationAwareBeanPostProcessor) {
    InstantiationAwareBeanPostProcessor ibp = (InstantiationAwareBeanPostProcessor) bp;
    if (!ibp.postProcessAfterInstantiation(bw.getWrappedInstance(), beanName)) {
    continueWithPropertyPopulation = false;
    break;
    }
    }
    }
    }

    // 是否继续填充属性的流程
    if (!continueWithPropertyPopulation) {
    return;
    }

    if (mbd.getResolvedAutowireMode() == RootBeanDefinition.AUTOWIRE_BY_NAME
    || mbd.getResolvedAutowireMode() == RootBeanDefinition.AUTOWIRE_BY_TYPE) {
    MutablePropertyValues newPvs = new MutablePropertyValues(pvs);
    // 根据名称注入
    if (mbd.getResolvedAutowireMode() == RootBeanDefinition.AUTOWIRE_BY_NAME) {
    autowireByName(beanName, mbd, bw, newPvs);
    }

    // 根据类型注入
    if (mbd.getResolvedAutowireMode() == RootBeanDefinition.AUTOWIRE_BY_TYPE) {
    autowireByType(beanName, mbd, bw, newPvs);
    }
    pvs = newPvs;
    }

    // 是否存在 InstantiationAwareBeanPostProcessors
    boolean hasInstAwareBpps = hasInstantiationAwareBeanPostProcessors();
    // 是否需要检查依赖
    boolean needsDepCheck = (mbd.getDependencyCheck() != RootBeanDefinition.DEPENDENCY_CHECK_NONE);

    if (hasInstAwareBpps || needsDepCheck) {
    PropertyDescriptor[] filteredPds = filterPropertyDescriptorsForDependencyCheck(bw, mbd.allowCaching);
    if (hasInstAwareBpps) {
    // 后置处理 PropertyValues
    for (BeanPostProcessor bp : getBeanPostProcessors()) {
    if (bp instanceof InstantiationAwareBeanPostProcessor) {
    InstantiationAwareBeanPostProcessor ibp = (InstantiationAwareBeanPostProcessor) bp;
    pvs = ibp.postProcessPropertyValues(pvs, filteredPds, bw.getWrappedInstance(), beanName);
    if (pvs == null) {
    return;
    }
    }
    }
    }
    if (needsDepCheck) {
    checkDependencies(beanName, mbd, filteredPds, pvs);
    }
    }
    // 将属性应用到 bean 上(常规情况下,前面的处理都用不上)
    applyPropertyValues(beanName, mbd, bw, pvs);
    }
    - -

    initializeBean

    在填充完属性后,实例就可以进行初始化工作:

    -
      -
    1. invokeAwareMethods,让 Bean 通过 xxxAware 接口感知一些信息
    2. -
    3. 调用 BeanPostProcessor 的 postProcessBeforeInitialization 方法
    4. -
    5. invokeInitMethods,调用初始化方法
    6. -
    7. 调用 BeanPostProcessor 的 postProcessAfterInitialization 方法
    8. -
    -
    protected Object initializeBean(final String beanName, final Object bean, RootBeanDefinition mbd) {
    // 处理 Aware 接口的相应方法
    if (System.getSecurityManager() != null) {
    AccessController.doPrivileged(new PrivilegedAction<Object>() {
    @Override
    public Object run() {
    invokeAwareMethods(beanName, bean);
    return null;
    }
    }, getAccessControlContext());
    }
    else {
    invokeAwareMethods(beanName, bean);
    }

    // 应用 BeanPostProcessor 的 postProcessBeforeInitialization 方法
    Object wrappedBean = bean;
    if (mbd == null || !mbd.isSynthetic()) {
    wrappedBean = applyBeanPostProcessorsBeforeInitialization(wrappedBean, beanName);
    }

    try {
    // 调用初始化方法
    invokeInitMethods(beanName, wrappedBean, mbd);
    }
    catch (Throwable ex) {
    throw new BeanCreationException(
    (mbd != null ? mbd.getResourceDescription() : null),
    beanName, "Invocation of init method failed", ex);
    }

    if (mbd == null || !mbd.isSynthetic()) {
    // 应用 BeanPostProcessor 的 postProcessAfterInitialization 方法
    wrappedBean = applyBeanPostProcessorsAfterInitialization(wrappedBean, beanName);
    }
    return wrappedBean;
    }
    - -
    处理 Aware 接口的相应方法

    让 Bean 在初始化中,感知(获知)和自身相关的资源,如 beanName、beanClassLoader 或者 beanFactory。

    -
    private void invokeAwareMethods(final String beanName, final Object bean) {
    if (bean instanceof Aware) {
    if (bean instanceof BeanNameAware) {
    ((BeanNameAware) bean).setBeanName(beanName);
    }
    if (bean instanceof BeanClassLoaderAware) {
    ((BeanClassLoaderAware) bean).setBeanClassLoader(getBeanClassLoader());
    }
    if (bean instanceof BeanFactoryAware) {
    ((BeanFactoryAware) bean).setBeanFactory(AbstractAutowireCapableBeanFactory.this);
    }
    }
    }
    - -
    调用初始化方法
      -
    1. 如果 bean 实现 InitializingBean 接口,调用 afterPropertiesSet 方法
    2. -
    3. 如果自定义 init 方法且满足调用条件,同样进行调用
    4. -
    -
    protected void invokeInitMethods(String beanName, final Object bean, RootBeanDefinition mbd) throws Throwable {
    // 是否实现 InitializingBean 接口,是的话调用 afterPropertiesSet 方法
    // 给 bean 一个感知属性已设置并做出反应的机会
    boolean isInitializingBean = (bean instanceof InitializingBean);
    if (isInitializingBean
    && (mbd == null || !mbd.isExternallyManagedInitMethod("afterPropertiesSet"))) {
    if (System.getSecurityManager() != null) {
    try {
    AccessController.doPrivileged(new PrivilegedExceptionAction<Object>() {
    @Override
    public Object run() throws Exception {
    ((InitializingBean) bean).afterPropertiesSet();
    return null;
    }
    }, getAccessControlContext());
    }
    catch (PrivilegedActionException pae) {
    throw pae.getException();
    }
    }
    else {
    ((InitializingBean) bean).afterPropertiesSet();
    }
    }

    // 如果存在自定义的 init 方法且方法名称不是 afterPropertiesSet,判断是否调用
    if (mbd != null) {
    String initMethodName = mbd.getInitMethodName();
    if (initMethodName != null
    && !(isInitializingBean && "afterPropertiesSet".equals(initMethodName))
    && !mbd.isExternallyManagedInitMethod(initMethodName)) {
    invokeCustomInitMethod(beanName, bean, mbd);
    }
    }
    }
    - -
    BeanPostProcessor 处理

    在调用初始化方法前后,BeanPostProcessor 先后进行两次处理。其实和 BeanPostProcessor 相关的代码都非常相似:

    -
      -
    1. 获取 Processor 列表
    2. -
    3. 判断 Processor 类型是否是当前需要的
    4. -
    5. 对 bean 进行处理
    6. -
    -
    public Object applyBeanPostProcessorsBeforeInitialization(Object existingBean, String beanName) throws BeansException {

    Object result = existingBean;
    for (BeanPostProcessor beanProcessor : getBeanPostProcessors()) {
    result = beanProcessor.postProcessBeforeInitialization(result, beanName);
    if (result == null) {
    return result;
    }
    }
    return result;
    }

    public Object applyBeanPostProcessorsAfterInitialization(Object existingBean, String beanName) throws BeansException {

    Object result = existingBean;
    for (BeanPostProcessor beanProcessor : getBeanPostProcessors()) {
    result = beanProcessor.postProcessAfterInitialization(result, beanName);
    if (result == null) {
    return result;
    }
    }
    return result;
    }
    - -

    再思 Bean 的初始化

    以下代码片段一度让我困惑,从注释看,初始化 Bean 实例的工作包括了 populateBean 和 initializeBean,但是 initializeBean 方法的含义就是初始化 Bean。在 initializeBean 方法中,调用了 invokeInitMethods 方法,其含义仍然是调用初始化方法。
    在更熟悉代码后,我有一种微妙的、个人性的体会,在 Spring 源码中,有时候视角的变化是很快的,痕迹是很小的。如果不加以理解和区分,很容易迷失在相似的描述中。以此处为例,“初始化 Bean 和 Bean 的初始化”扩展开来是 “Bean 工厂初始化一个 Bean 和 Bean 自身进行初始化”。

    -
    // Initialize the bean instance.
    Object exposedObject = bean;
    try {
    populateBean(beanName, mbd, instanceWrapper);
    if (exposedObject != null) {
    exposedObject = initializeBean(beanName, exposedObject, mbd);
    }
    }
    - -

    在注释这一行,视角是属于 BeanFactory(AbstractAutowireCapableBeanFactory)。从工厂的视角,面对一个刚刚创建出来的 Bean 实例,需要完成两方面的工作:

    -
      -
    1. 为 Bean 实例填充属性,包括解析依赖,为 Bean 自身的初始化做好准备。
    2. -
    3. Bean 自身的初始化。
    4. -
    -

    在万事俱备之后,就是 Bean 自身的初始化工作。由于 Spring 的高度扩展性,这部分并不只是单纯地调用初始化方法,还包含 Aware 接口和 BeanPostProcessor 的相关处理,前者偏属于 Java 对象层面,后者偏属于 Spring Bean 层面。
    在认同 BeanPostProcessor 的处理属于 Bean 自身初始化工作的一部分后,@PostConstruct 注解的方法被称为 Bean 的初始化方法也就不那么违和了,因为它的实现原理正是 BeanPostProcessor,这仍然在 initializeBean 的范围内。

    -]]>
    - - java - spring - -
    - - 关于 Java 字节码指令的一些例子分析 - /2023/11/09/some-examples-of-Java-bytecode-instruction-analysis/ - 演示字节码指令的执行
    public class ByteCodeTest_2 {

    public static void main(String[] args) {
    int a = 10;
    int b = Short.MAX_VALUE + 1;
    int c = a + b;
    System.out.println(c);
    }
    }
    - -

    操作数栈和本地变量表的大小

    在编译期间就可计算得到操作数栈和本地变量表的大小。

    -
    stack=2, locals=4, args_size=1
    - -

    本地变量表

    Slot,即槽位,可理解为索引。

    -
    Start  Length  Slot  Name   Signature
    0 18 0 args [Ljava/lang/String;
    3 15 1 a I
    6 12 2 b I
    10 8 3 c I
    - -

    运行时常量池

    #3 = Integer            32768
    #4 = Fieldref #27.#28 // java/lang/System.out:Ljava/io/PrintStream;
    #5 = Methodref #29.#30 // java/io/PrintStream.println:(I)V
    - - - -

    字节码指令

     0: bipush        10
    2: istore_1
    3: ldc #3 // int 32768
    5: istore_2
    6: iload_1
    7: iload_2
    8: iadd
    9: istore_3
    10: getstatic #4 // Field java/lang/System.out:Ljava/io/PrintStream;
    13: iload_3
    14: invokevirtual #5 // Method java/io/PrintStream.println:(I)V
    17: return
    -
      -
    • bipush,将一个 byte,推入操作数栈。
        -
      • short 范围内的数是和字节码指令一起存储的,范围外的数是存储在运行时常量池中的。
      • -
      • 操作数栈的宽度是 4 个字节,short 范围内的数在推入操作数栈前会经过符号扩展成为 int。
      • -
      -
    • -
    • istore_1,将栈顶的 int,存入局部变量表,槽位 1。
    • -
    • ldc,从运行时常量池中将指定常量推入操作数栈。
    • -
    • istore_2,将栈顶的 int,存入局部变量表,槽位 2。
    • -
    • iload_1 iload_2,依次从局部变量表将两个 int 推入操作数栈,槽位分别是 1 和 2。
    • -
    • iadd,将栈顶的两个 int 弹出并相加,将结果推入操作数栈。
    • -
    • istore_3,将栈顶的 int,存入局部变量表,槽位 3。
    • -
    • getstatic,获取类的静态属性,推入操作数栈。
    • -
    • iload_3,从局部变量表将 int 推入操作数栈,槽位 3。
    • -
    • invokevirtual,将栈顶的参数依次弹出,调用实例方法。
    • -
    • return,返回 void
    • -
    -

    分析 a++ 和 ++a

    public class ByteCodeTest_3 {

    public static void main(String[] args) {
    int a = 10;
    int b = a++ + ++a + a--;
    System.out.println(a);
    System.out.println(b);
    }
    }
    - -

    字节码指令

     0: bipush        10
    2: istore_1
    3: iload_1
    4: iinc 1, 1
    7: iinc 1, 1
    10: iload_1
    11: iadd
    12: iload_1
    13: iinc 1, -1
    16: iadd
    17: istore_2
    - -
      -
    • a++ 和 ++a 的区别是先 load 还是先 iinc。
    • -
    • iinc,将局部变量表指定槽位的数加上一个常数。
    • -
    • 注意 a 只 load 到操作数栈并没有 store 回局部变量表。
    • -
    • b = 10 + 12 + 12 = 34
    • -
    • a = 10 + 1 + 1 - 1 = 11
    • -
    -

    分析判断条件

    public class ByteCodeTest_4 {

    public static void main(String[] args) {
    int a = 0;
    // ifeq, goto
    if (a == 0) {
    a = 10;
    } else {
    a = 20;
    }
    }
    }
    - -

    字节码指令

     0: iconst_0
    1: istore_1
    2: iload_1
    3: ifne 12
    6: bipush 10
    8: istore_1
    9: goto 15
    12: bipush 20
    14: istore_1
    15: return
    - -
      -
    • iconst,将一个 int 常量推入操作数栈。
    • -
    • if<cond>,一个 int 和 0 的比较成立时进入分支,跳转到指定行号。
    • -
    • goto,总是进入的分支,跳转到指定行号。
    • -
    -

    涉及的字节码指令

      -
    • bipush,将一个 byte 符号扩展为一个 int,推入操作数栈。
    • -
    • istore,将栈顶的 int,存入局部变量表的指定槽位。
    • -
    • iload,将局部变量表指定槽位的 int,推入操作数栈。
    • -
    • ldc,从运行时常量池将指定常量推入操作数栈。
    • -
    • iadd,将栈顶的两个 int 弹出并相加,将结果推入操作数栈。
    • -
    • getstatic,获取类的静态属性,推入操作数栈。
    • -
    • invokevirtual,将栈顶的参数依次弹出,调用实例方法。
    • -
    • return,返回 void。
    • -
    • iinc,将局部变量表中指定槽位的数加一个常量。
    • -
    • if<cond>,一个 int 和 0 的比较成立时进入分支,跳转到指定行号。
        -
      • ifeq,equals
      • -
      • ifne,not equals
      • -
      • iflt,less than
      • -
      • ifge,greater than or equals
      • -
      • ifgt,great than
      • -
      • ifle,less than or equals
      • -
      -
    • -
    • goto,总是进入的分支,跳转到指定行号。
    • -
    -]]>
    - - Java - bytecode - -
    - - 基于 Redis 的分布式锁的简单实现 - /2023/11/13/simple-implementation-of-distributed-lock-based-on-Redis/ - 在分布式应用中,并发访问资源需要谨慎考虑。比如读取和修改保存并不是一个原子操作,在并发时,就可能发生修改的结果被覆盖的问题。

    - +
  7. Class 文件中的常量池 Constant pool 会记录代码中出现的字面量(文本文件)。
  8. +
  9. 运行时常量池是方法区的一部分,Class 文件中的常量池的内容,在类加载后,就进入了运行时常量池中(内存中的数据)。
  10. +
  11. 字符串常量池,记录 interned string 的一个全局表,JDK 6 前在方法区,后移到堆中。
  12. + +

    字符串常量池的位置和形式

    在《深入理解Java虚拟机》提到:字符串常量池的位置从 JDK 7 开始,从永久代中移到了堆中。在这句话中,字符串常量池像是一个特定的内存区域,存储了 interned string 的实例。

    + -

    很多人都了解在必要的时候需要使用分布式锁来限制程序的并发执行,但是在具体的细节上,往往并不正确。

    -

    基于 Redis 的分布式锁简单实现

    本质上要实现的目标就是在 Redis 中占坑,告诉后来者资源已经被锁定,放弃或者稍后重试。Redis 原生支持 set if not exists 的语义。

    -
    > setnx lock:user1 true
    OK

    ... do something

    > del lock:user1
    (integer) 1
    +

    验证字符串常量池的位置

    书中使用了以下方式来验证字符串常量池的位置。

    +
    public class StringTableTest_8 {  

    // JDK 1.8 设置 -Xmx10m -XX:-UseGCOverheadLimit
    // JDK 1.6 设置 -XX:MaxPerSize=10m
    public static void main(String[] args) {
    List<String> list = new ArrayList<>();
    int i = 0;
    try {
    for (int j = 0; j < 260000; j++) {
    list.add(String.valueOf(j).intern());
    i++;
    }
    } catch (Exception e) {
    e.printStackTrace();
    } finally {
    System.out.println(i);
    }
    }
    }
    +

    在 JDK 8 中异常如下:

    +
    Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
    +

    在 JDK 6 中异常如下:

    +
    java.lang.OutOfMemoryError: PermGen space
    -

    死锁问题

    问题一:异常引发死锁 1

    如果在处理过程中,程序出现异常,将导致 del 指令没有执行成功。锁无法释放,其他线程将无法再获取锁。

    - +

    同时书中也提到了,在字符串常量池的位置改变后,它只用保存第一次出现时字符串对象的引用。JDK 8 中的 intern 方法可以印证该说法,方法注释中提到:如果字符串常量池中已存在相等(equals)的字符串,那就返回已存在的对象(这样原先准备加入的对象就可以释放);否则,将字符串对象加入字符串常量池中,直接返回对该对象的引用(不用像 JDK 6 时,复制一个对象加入常量池,返回该复制对象的引用)。

    +

    关于 intern 的实验

    public class StringTableTest_5 {  

    public static void main(String[] args) {
    // "a"、"b" 作为字符串字面量,会解析得到字符串对象放入字符串常量池
    // 但是 new String("a") 创建出来的字符串对象,不会进入字符串常量池
    String s1 = new String("a") + new String("b");
    // intern 方法尝试将 s1 放入 StringTable,无则放入,返回该对象引用,有则返回已存在对象的引用
    String s2 = s1.intern();

    String x = "ab";

    System.out.println(s2 == x);
    System.out.println(s1 == x);
    }
    }

    public class StringTableTest_6 {

    public static void main(String[] args) {
    // 将 "ab" 的赋值语句提前到最开始,"ab" 生成的字符串对象进入字符串常量池
    String x = "ab";
    String s1 = new String("a") + new String("b");
    // intern 方法尝试将 s1 放入 StringTable,无则放入,返回该对象引用,有则返回已存在对象的引用
    String s2 = s1.intern();

    System.out.println(s2 == x);
    System.out.println(s1 == x);
    }
    }
    +

    实验结果证实了上述说法。

    +

    字符串常量池到底是什么?

    但是 xinxi 提及:字符串常量池,也称为 StringTable,本质上是一个惰性维护的哈希表,是一个纯运行时的结构,只存储对 java.lang.String 实例的引用,而不存储 String 对象的内容。当我们提到一个字符串进入字符串常量池其实是说在这个 StringTable 中保存了对它的引用,反之,如果说没有在其中就是说 StringTable 中没有对它的引用。
    zyplanke 分析 StringTable 在内存中的形式时,也表达了类似的观点。

    + -

    改进一:设置超时时间

    对 key 设置过期时间,如果在处理过程中,程序出现异常,导致 del 指令没有执行成功,设置的过期时间一到,key 将自动被删除,锁也就等于被释放了。

    -
    > setnx lock:user1 true
    OK
    > expire lock:user1 5

    ... do something

    > del lock:user1
    (integer) 1
    +

    尽管这个疑问似乎不妨碍我们理解很多东西,但是深究之后,真的让人困惑,网上也没有搜集到更多的信息。字符串常量池和 StringTable 是否等价?字符串常量池更准确的说法是否是“一个保存引用的 StringTable 加上分布在堆(JDK 6 以前的永久代)中的字符串实例”?
    已经好几次打开 jvm 的源码,却看不懂它到底什么意思啊!!!!!难道是时候开始学 C++ 了吗。

    +

    进入字符串常量池的时机

    前面提到了第一次遇到的字符串字面量会在某一个时刻,生成对应的字符串对象进入字符串常量池,同时也提到了,字符串常量池(StringTable)的维护是懒惰的,那么这些究竟是什么时候发生的呢?

    +
    public class StringTableTest_12 {

    public static void main(String[] args) throws IOException {
    new String("ab");
    }
    }
    -

    问题二:异常引发死锁 2

    事实上,上述措施并没有彻底解决问题。如果在设置 key 的超时时间之前,程序出现异常,一切仍旧会发生。

    - +
     0: new           #2                  // class java/lang/String
    3: dup
    4: ldc #3 // String ab
    6: invokespecial #4 // Method java/lang/String."<init>":(Ljava/lang/String;)V
    9: pop
    10: return
    -

    本质原因是 setnx 和 expire 两个指令不是一个原子操作。那么是否可以使用 Redis 的事务解决呢?不行。因为 expire 依赖于 setnx 的执行结果,如果 setnx 没有成功,expire 就不应该执行。

    -

    改进二:setnx + expire 的原子指令

    如果 setnx 和 expire 可以用一个原子指令实现就好了。

    - +

    RednaxelaFX 的文章提到:

    +
    +

    在类加载阶段,JVM 会在堆中创建对应这些 class 文件常量池中的字符串对象实例,并在字符串常量池中驻留其引用。具体在 resolve 阶段执行。这些常量全局共享。

    +
    +

    xinxi 的文章中补充到:

    +
    +

    这里说的比较笼统,没错,是 resolve 阶段,但是并不是大家想的那样,立即就创建对象并且在字符串常量池中驻留了引用。 JVM规范里明确指定resolve阶段可以是lazy的。
    ……
    就 HotSpot VM 的实现来说,加载类的时候,那些字符串字面量会进入到当前类的运行时常量池,不会进入全局的字符串常量池(即在 StringTable 中并没有相应的引用,在堆中也没有对应的对象产生)。

    +
    +

    《深入理解Java虚拟机》中提到:

    +
    +

    《Java虚拟机规范》之中并未规定解析阶段发生的具体时间,只要求了在执行ane-warray、checkcast、getfield、getstatic、instanceof、invokedynamic、invokeinterface、invoke-special、invokestatic、invokevirtual、ldc、ldc_w、ldc2_w、multianewarray、new、putfield和putstatic这17个用于操作符号引用的字节码指令之前,先对它们所使用的符号引用进行解析。所以虚拟机实现可以根据需要来自行判断,到底是在类被加载器加载时就对常量池中的符号引用进行解析,还是等到一个符号引用将要被使用前才去解析它。

    +
    +

    综上可知,字符串字面量的解析是属于类加载的解析阶段,但是《Java虚拟机规范》并未规定解析发生的具体时间,只要求在执行一些字节码指令前进行,其中包括了 ldc 指令。虚拟机的具体实现,比如 Hotspot 就在执行 ldc #indexNumber 前触发解析,根据字符串常量池中是否已存在字符串对象决定是否创建对象,并将对象推送到栈顶。
    这也证实了前文中提到的字符串字面量生成字符串对象和 new 关键字无关。

    +

    验证延迟实例化

    使用 IDEA memory 功能,观察字符串对象的个数逐个变化。

    +
      +
    1. 直到第一次运行到字符串字面量时,才会创建对应的字符串对象。
    2. +
    3. 相同的字符串常量,不会重复创建字符串对象。
    4. +
    +
    public class StringTableTest_4 {  

    public static void main(String[] args) {
    System.out.println();

    System.out.println("1");
    System.out.println("2");
    System.out.println("3");
    System.out.println("4");
    System.out.println("5");
    System.out.println("6");
    System.out.println("7");
    System.out.println("8");
    System.out.println("9");
    System.out.println("0");
    System.out.println("1");
    System.out.println("2");
    System.out.println("3");
    System.out.println("4");
    System.out.println("5");
    System.out.println("6");
    System.out.println("7");
    System.out.println("8");
    System.out.println("9");
    System.out.println("0");
    }
    }
    -
    基于原生指令的实现

    在 Redis 2.8 版本中,Redis 的作者加入 set 指令扩展参数,允许 setnx 和 expire 组合成一个原子指令。

    -
    > set lock:user1 true ex 5 nx
    OK

    ... do something

    > del lock:user1
    (integer) 1
    + -
    基于 Lua 脚本的实现

    除了使用原生的指令外,还可以使用 Lua 脚本,将多个 Redis 指令组合成一个原子指令。

    -
    if redis.call('setnx', KEYS[1], ARGV[1]) == 1 then
    redis.call('expire', KEYS[1], ARGV[2])
    return true
    else
    return false
    end
    +

    字符串常量池的垃圾回收和性能优化

    垃圾回收

    前文提到字符串常量池在 JDK 7 开始移到堆中,是因为考虑在方法区中的垃圾回收是比较困难的,同时随着字节码技术的发展,CGLib 等会大量动态生成类的技术的运用使得方法区的内存紧张,将字符串常量池移到堆中,可以有效提高其垃圾回收效率。

    +
    public class StringTableTest_9 {  

    // -Xmx10m -XX:+PrintStringTableStatistics -XX:+PrintGCDetails -verbose:gc
    public static void main(String[] args) {
    int i = 0;
    try {
    // 0->100->10000,观察统计信息中数量的变化以及垃圾回收记录
    for (int j = 0; j < 10000; j++) {
    String.valueOf(j).intern();
    i++;
    }
    } catch (Exception e) {
    e.printStackTrace();
    } finally {
    System.out.println(i);
    }
    }
    }
    -

    超时问题

    基于 Redis 的分布式锁还会面临超时问题。如果在加锁和释放之间的处理逻辑过于耗时,以至于超出了 key 的过期时间,锁将在处理结束前被释放,就可能发生问题。

    - -

    问题一:其他线程提前进入临界区

    如果第一个线程因为处理逻辑过于耗时导致在处理结束前锁已经被释放,其他线程将可以提前获得锁,临界区的代码将不能保证严格串行执行。

    -

    问题二:错误释放其他线程的锁

    如果在第二个线程获得锁后,第一个线程刚好处理逻辑结束去释放锁,将导致第二个线程的锁提前被释放,引发连锁问题。

    -

    改进一:不要用于较长时间的任务

    与其说是改进,不如说是注意事项。如果真的出现问题,造成的数据错误可能需要人工介入解决。

    -

    如果真的存在这样的业务场景,应考虑使用其他解决方案加以优化。

    -

    改进二:使用 watchdog 实现锁续期

    为 Redis 的 key 设置过期时间,其实是为了解决死锁问题而做出的兜底措施。可以为获得的锁设置定时任务定期地为锁续期,以避免锁被提前释放。

    -
    private void scheduleRenewal() {
    String value = lockValue.get();
    ScheduledFuture<?> scheduledFuture = sScheduler.scheduleAtFixedRate(
    () -> this.renewal(value), RENEWAL_INTERVAL, RENEWAL_INTERVAL, TimeUnit.MILLISECONDS
    );
    renewalTask.set(scheduledFuture);
    }
    +
    [GC (Allocation Failure) [PSYoungGen: 2048K->488K(2560K)] 2048K->856K(9728K), 0.0007745 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 


    StringTable statistics:
    Number of buckets : 60013 = 480104 bytes, avg 8.000
    Number of entries : 7277 = 174648 bytes, avg 24.000
    Number of literals : 7277 = 421560 bytes, avg 57.930
    Total footprint : = 1076312 bytes
    Average bucket size : 0.121
    Variance of bucket size : 0.125
    Std. dev. of bucket size: 0.354
    Maximum bucket size : 3
    -

    但是这个方式仍然不能避免解锁失败时的其他线程的等待时间。

    -

    改进三:加锁时指定 tag

    可以将 set 指令的 value 参数设置为一个随机数,释放锁时先匹配持有的 tag 是否和 value 一致,如果一致再删除 key,以此避免锁被其他线程错误释放。

    -
    基于原生指令的实现
    tag = random.nextint()
    if redis.set(key, tag, nx= True, ex=5):
    do_something()
    redis.delifequals(key, tag)
    +

    性能优化

    调整 buckets size

    当 size 过小,哈希碰撞增加,链表变长,效率会变低,需要增大 buckets size。

    +
    public class StringTableTest_10 {  

    // -XX:StringTableSize=200000 -XX:+PrintStringTableStatistics
    // 默认->200000->1009(最小值),观察耗时
    public static void main(String[] args) {
    try (BufferedReader br = new BufferedReader(new InputStreamReader(new FileInputStream("src/main/resources/linux.words"), StandardCharsets.UTF_8))) {
    String line = null;
    long start = System.nanoTime();
    while (true) {
    line = br.readLine();
    if (line == null) {
    break;
    }
    line.intern();
    }
    System.out.println("cost: " + (System.nanoTime() - start) / 1000000) ;
    } catch (IOException e) {
    throw new RuntimeException(e);
    }
    }
    }
    -

    但是注意,Redis 并没有提供语义为 delete if equals 的原子指令,这样的话问题并不能被彻底解决。如果在第一个线程判断 tag 是否和 value 相等之后,第二个线程刚好获得了锁,然后第一个线程因为匹配成功执行删除 key 操作,仍然将导致第二个线程获得的锁被第一个线程错误释放。

    - +

    主动运用 intern 的场景

    当你需要大量缓存重复的字符串时,使用 intern 可以大大减少内存占用。

    +
    public class StringTableTest_11 {  

    // -Xms500m -Xmx500m -XX:StringTableSize=200000 -XX:+PrintStringTableStatistics
    public static void main(String[] args) throws IOException {
    List<String> words = new ArrayList<>();
    System.in.read();
    for (int i = 0; i < 10; i++) {
    try (BufferedReader br = new BufferedReader(new InputStreamReader(new FileInputStream("src/main/resources/linux.words"), StandardCharsets.UTF_8))) {
    String line = null;
    long start = System.nanoTime();
    while (true) {
    line = br.readLine();
    if (line == null) {
    break;
    }
    // words.add(line);
    words.add(line.intern());
    }
    System.out.println("cost: " + (System.nanoTime() - start) / 1000000) ;
    }
    }
    System.in.read();
    }
    }
    -
    基于 Lua 脚本的实现
    if redis.call("get", KEYS[1]) == ARGV[1] then
    return redis.call("del", KEYS[1])
    else
    return 0
    end
    +

    使用 VisualVM 观察字符串和 char[] 内存占用情况,可以发现提升显著。

    + -

    可重入性

    可重入性是指线程在已经持有锁的情况下再次请求加锁,如果一个锁支持同一个线程多次加锁,那么就称这个锁是可重入的,类似 Java 的 ReentrantLock。

    -

    使用 ThreadLocal 实现锁计数

    Redis 分布式锁如果要支持可重入,可以使用线程的 ThreadLocal 变量存储当前持有的锁计数。但是在多次获得锁后,过期时间并没有得到延长,后续获得锁后持有锁的时间其实比设置的时间更短。

    -
    private ThreadLocal<Integer> lockCount = ThreadLocal.withInitial(() -> 0);

    public boolean tryLock() {
    Integer count = lockCount.get();
    if (count != null && count > 0) {
    lockCount.set(count + 1);
    return true;
    }
    String result = commands.set(lockKey, lockValue.get(), SetArgs.Builder.nx().px(RedisLockManager.LOCK_EXPIRE));
    if ("OK".equals(result)) {
    lockCount.set(1);
    scheduleRenewal();
    return true;
    }
    return false;
    }
    +

    字符串拼接

    变量的拼接

    字符串变量的拼接,底层是使用 StringBuilder 实现:new StringBuilder().append("a").append("b").toString(),而 toString 方法使用拼接得到的 char 数组创建一个新的 String 对象,因此 s3 和 s4 是不相同的两个对象。

    +
    public class StringTableTest_2 {  

    public static void main(String[] args) {
    String s1 = "a";
    String s2 = "b";
    String s3 = "ab";
    String s4 = s1 + s2;
    }
    }
    -

    使用 Redis hash 实现锁计数

    还可以使用 Redis 的 hash 数据结构实现锁计数,支持重新获取锁后重置过期时间。

    -
    if (redis.call('exists', KEYS[1]) == 0) then 
    redis.call('hset', KEYS[1], ARGV[2], 1);
    redis.call('pexpire', KEYS[1], ARGV[1]);
    return nil;
    end;
    if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then
    redis.call('hincrby', KEYS[1], ARGV[2], 1);
    redis.call('pexpire', KEYS[1], ARGV[1]);
    return nil;
    return redis.call('pttl', KEYS[1]);
    +
     0: ldc           #2                  // String a
    2: astore_1
    3: ldc #3 // String b
    5: astore_2
    6: ldc #4 // String ab
    8: astore_3
    9: new #5 // class java/lang/StringBuilder
    12: dup
    13: invokespecial #6 // Method java/lang/StringBuilder."<init>":()V
    16: aload_1
    17: invokevirtual #7 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
    20: aload_2
    21: invokevirtual #7 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
    24: invokevirtual #8 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
    27: astore 4
    29: return
    -

    书的作者不推荐使用可重入锁,他提出可重入锁会加重客户端的复杂度,如果在编写代码时注意在逻辑结构上进行调整,完全可以避免使用可重入锁。

    -

    代码实现

    redis-lock

    -

    参考文章

      -
    • 《Redis 深度历险,核心原理与应用实践》
    • -
    +

    常量的拼接

    字符串常量的拼接是在编译期间,因为已知结果而被优化为一个字符串常量。又因为 “ab” 字符串在 StringTable 中是已存在的,所以不会重新创建新对象。

    +
    public class StringTableTest_3 {

    public static void main(String[] args) {
    String s1 = "a";
    String s2 = "b";
    String s3 = "ab";
    String s4 = s1 + s2;
    String s5 = "a" + "b";
    }
    }
    + +
     0: ldc           #2                  // String a
    2: astore_1
    3: ldc #3 // String b
    5: astore_2
    6: ldc #4 // String ab
    8: astore_3
    9: new #5 // class java/lang/StringBuilder
    12: dup
    13: invokespecial #6 // Method java/lang/StringBuilder."<init>":()V
    16: aload_1
    17: invokevirtual #7 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
    20: aload_2
    21: invokevirtual #7 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
    24: invokevirtual #8 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
    27: astore 4
    29: ldc #4 // String ab
    31: astore 5
    33: return
    + +

    参考文章

      +
    1. Java 中new String(“字面量”) 中 “字面量” 是何时进入字符串常量池的? - xinxi的回答 - 知乎
    2. +
    3. 请别再拿“String s = new String(“xyz”);创建了多少个String实例”来面试了吧
    4. +
    5. JVM中字符串常量池StringTable在内存中形式分析
    6. +
    ]]>
    - distributed lock - redis + java + jvm
    @@ -1458,8 +1312,154 @@
    postDefineClass

    在定义类后使用 ProtectionDomain 中的 certs 补充 Class 实例的 signer 信息,猜测在 native 方法 defineClassX 方法中,对 ProtectionDomain 做了一些修改。事实上,从代码上看,将 CodeSource 包装为 ProtectionDomain 传入后,除了 defineClassX 方法外,其他地方都是取出 CodeSource 使用。

    private void postDefineClass(Class<?> c, ProtectionDomain pd)
    {
    if (pd.getCodeSource() != null) {
    // 获取证书
    Certificate certs[] = pd.getCodeSource().getCertificates();
    if (certs != null)
    setSigners(c, certs);
    }
    }
    ]]> - Java - ClassLoader + java + class loader + +
    + + 关于 Java 字节码指令的一些例子分析 + /2023/11/09/some-examples-of-Java-bytecode-instruction-analysis/ + 演示字节码指令的执行
    public class ByteCodeTest_2 {

    public static void main(String[] args) {
    int a = 10;
    int b = Short.MAX_VALUE + 1;
    int c = a + b;
    System.out.println(c);
    }
    }
    + +

    操作数栈和本地变量表的大小

    在编译期间就可计算得到操作数栈和本地变量表的大小。

    +
    stack=2, locals=4, args_size=1
    + +

    本地变量表

    Slot,即槽位,可理解为索引。

    +
    Start  Length  Slot  Name   Signature
    0 18 0 args [Ljava/lang/String;
    3 15 1 a I
    6 12 2 b I
    10 8 3 c I
    + +

    运行时常量池

    #3 = Integer            32768
    #4 = Fieldref #27.#28 // java/lang/System.out:Ljava/io/PrintStream;
    #5 = Methodref #29.#30 // java/io/PrintStream.println:(I)V
    + + + +

    字节码指令

     0: bipush        10
    2: istore_1
    3: ldc #3 // int 32768
    5: istore_2
    6: iload_1
    7: iload_2
    8: iadd
    9: istore_3
    10: getstatic #4 // Field java/lang/System.out:Ljava/io/PrintStream;
    13: iload_3
    14: invokevirtual #5 // Method java/io/PrintStream.println:(I)V
    17: return
    +
      +
    • bipush,将一个 byte,推入操作数栈。
        +
      • short 范围内的数是和字节码指令一起存储的,范围外的数是存储在运行时常量池中的。
      • +
      • 操作数栈的宽度是 4 个字节,short 范围内的数在推入操作数栈前会经过符号扩展成为 int。
      • +
      +
    • +
    • istore_1,将栈顶的 int,存入局部变量表,槽位 1。
    • +
    • ldc,从运行时常量池中将指定常量推入操作数栈。
    • +
    • istore_2,将栈顶的 int,存入局部变量表,槽位 2。
    • +
    • iload_1 iload_2,依次从局部变量表将两个 int 推入操作数栈,槽位分别是 1 和 2。
    • +
    • iadd,将栈顶的两个 int 弹出并相加,将结果推入操作数栈。
    • +
    • istore_3,将栈顶的 int,存入局部变量表,槽位 3。
    • +
    • getstatic,获取类的静态属性,推入操作数栈。
    • +
    • iload_3,从局部变量表将 int 推入操作数栈,槽位 3。
    • +
    • invokevirtual,将栈顶的参数依次弹出,调用实例方法。
    • +
    • return,返回 void
    • +
    +

    分析 a++ 和 ++a

    public class ByteCodeTest_3 {

    public static void main(String[] args) {
    int a = 10;
    int b = a++ + ++a + a--;
    System.out.println(a);
    System.out.println(b);
    }
    }
    + +

    字节码指令

     0: bipush        10
    2: istore_1
    3: iload_1
    4: iinc 1, 1
    7: iinc 1, 1
    10: iload_1
    11: iadd
    12: iload_1
    13: iinc 1, -1
    16: iadd
    17: istore_2
    + +
      +
    • a++ 和 ++a 的区别是先 load 还是先 iinc。
    • +
    • iinc,将局部变量表指定槽位的数加上一个常数。
    • +
    • 注意 a 只 load 到操作数栈并没有 store 回局部变量表。
    • +
    • b = 10 + 12 + 12 = 34
    • +
    • a = 10 + 1 + 1 - 1 = 11
    • +
    +

    分析判断条件

    public class ByteCodeTest_4 {

    public static void main(String[] args) {
    int a = 0;
    // ifeq, goto
    if (a == 0) {
    a = 10;
    } else {
    a = 20;
    }
    }
    }
    + +

    字节码指令

     0: iconst_0
    1: istore_1
    2: iload_1
    3: ifne 12
    6: bipush 10
    8: istore_1
    9: goto 15
    12: bipush 20
    14: istore_1
    15: return
    + +
      +
    • iconst,将一个 int 常量推入操作数栈。
    • +
    • if<cond>,一个 int 和 0 的比较成立时进入分支,跳转到指定行号。
    • +
    • goto,总是进入的分支,跳转到指定行号。
    • +
    +

    涉及的字节码指令

      +
    • bipush,将一个 byte 符号扩展为一个 int,推入操作数栈。
    • +
    • istore,将栈顶的 int,存入局部变量表的指定槽位。
    • +
    • iload,将局部变量表指定槽位的 int,推入操作数栈。
    • +
    • ldc,从运行时常量池将指定常量推入操作数栈。
    • +
    • iadd,将栈顶的两个 int 弹出并相加,将结果推入操作数栈。
    • +
    • getstatic,获取类的静态属性,推入操作数栈。
    • +
    • invokevirtual,将栈顶的参数依次弹出,调用实例方法。
    • +
    • return,返回 void。
    • +
    • iinc,将局部变量表中指定槽位的数加一个常量。
    • +
    • if<cond>,一个 int 和 0 的比较成立时进入分支,跳转到指定行号。
        +
      • ifeq,equals
      • +
      • ifne,not equals
      • +
      • iflt,less than
      • +
      • ifge,greater than or equals
      • +
      • ifgt,great than
      • +
      • ifle,less than or equals
      • +
      +
    • +
    • goto,总是进入的分支,跳转到指定行号。
    • +
    +]]>
    + + java + bytecode + +
    + + 基于 Redis 的分布式锁的简单实现 + /2023/11/13/simple-implementation-of-distributed-lock-based-on-Redis/ + 在分布式应用中,并发访问资源需要谨慎考虑。比如读取和修改保存并不是一个原子操作,在并发时,就可能发生修改的结果被覆盖的问题。

    + + +

    很多人都了解在必要的时候需要使用分布式锁来限制程序的并发执行,但是在具体的细节上,往往并不正确。

    +

    基于 Redis 的分布式锁简单实现

    本质上要实现的目标就是在 Redis 中占坑,告诉后来者资源已经被锁定,放弃或者稍后重试。Redis 原生支持 set if not exists 的语义。

    +
    > setnx lock:user1 true
    OK

    ... do something

    > del lock:user1
    (integer) 1
    + +

    死锁问题

    问题一:异常引发死锁 1

    如果在处理过程中,程序出现异常,将导致 del 指令没有执行成功。锁无法释放,其他线程将无法再获取锁。

    + + +

    改进一:设置超时时间

    对 key 设置过期时间,如果在处理过程中,程序出现异常,导致 del 指令没有执行成功,设置的过期时间一到,key 将自动被删除,锁也就等于被释放了。

    +
    > setnx lock:user1 true
    OK
    > expire lock:user1 5

    ... do something

    > del lock:user1
    (integer) 1
    + +

    问题二:异常引发死锁 2

    事实上,上述措施并没有彻底解决问题。如果在设置 key 的超时时间之前,程序出现异常,一切仍旧会发生。

    + + +

    本质原因是 setnx 和 expire 两个指令不是一个原子操作。那么是否可以使用 Redis 的事务解决呢?不行。因为 expire 依赖于 setnx 的执行结果,如果 setnx 没有成功,expire 就不应该执行。

    +

    改进二:setnx + expire 的原子指令

    如果 setnx 和 expire 可以用一个原子指令实现就好了。

    + + +
    基于原生指令的实现

    在 Redis 2.8 版本中,Redis 的作者加入 set 指令扩展参数,允许 setnx 和 expire 组合成一个原子指令。

    +
    > set lock:user1 true ex 5 nx
    OK

    ... do something

    > del lock:user1
    (integer) 1
    + +
    基于 Lua 脚本的实现

    除了使用原生的指令外,还可以使用 Lua 脚本,将多个 Redis 指令组合成一个原子指令。

    +
    if redis.call('setnx', KEYS[1], ARGV[1]) == 1 then
    redis.call('expire', KEYS[1], ARGV[2])
    return true
    else
    return false
    end
    + +

    超时问题

    基于 Redis 的分布式锁还会面临超时问题。如果在加锁和释放之间的处理逻辑过于耗时,以至于超出了 key 的过期时间,锁将在处理结束前被释放,就可能发生问题。

    + + +

    问题一:其他线程提前进入临界区

    如果第一个线程因为处理逻辑过于耗时导致在处理结束前锁已经被释放,其他线程将可以提前获得锁,临界区的代码将不能保证严格串行执行。

    +

    问题二:错误释放其他线程的锁

    如果在第二个线程获得锁后,第一个线程刚好处理逻辑结束去释放锁,将导致第二个线程的锁提前被释放,引发连锁问题。

    +

    改进一:不要用于较长时间的任务

    与其说是改进,不如说是注意事项。如果真的出现问题,造成的数据错误可能需要人工介入解决。

    +

    如果真的存在这样的业务场景,应考虑使用其他解决方案加以优化。

    +

    改进二:使用 watchdog 实现锁续期

    为 Redis 的 key 设置过期时间,其实是为了解决死锁问题而做出的兜底措施。可以为获得的锁设置定时任务定期地为锁续期,以避免锁被提前释放。

    +
    private void scheduleRenewal() {
    String value = lockValue.get();
    ScheduledFuture<?> scheduledFuture = sScheduler.scheduleAtFixedRate(
    () -> this.renewal(value), RENEWAL_INTERVAL, RENEWAL_INTERVAL, TimeUnit.MILLISECONDS
    );
    renewalTask.set(scheduledFuture);
    }
    + +

    但是这个方式仍然不能避免解锁失败时的其他线程的等待时间。

    +

    改进三:加锁时指定 tag

    可以将 set 指令的 value 参数设置为一个随机数,释放锁时先匹配持有的 tag 是否和 value 一致,如果一致再删除 key,以此避免锁被其他线程错误释放。

    +
    基于原生指令的实现
    tag = random.nextint()
    if redis.set(key, tag, nx= True, ex=5):
    do_something()
    redis.delifequals(key, tag)
    + +

    但是注意,Redis 并没有提供语义为 delete if equals 的原子指令,这样的话问题并不能被彻底解决。如果在第一个线程判断 tag 是否和 value 相等之后,第二个线程刚好获得了锁,然后第一个线程因为匹配成功执行删除 key 操作,仍然将导致第二个线程获得的锁被第一个线程错误释放。

    + + +
    基于 Lua 脚本的实现
    if redis.call("get", KEYS[1]) == ARGV[1] then
    return redis.call("del", KEYS[1])
    else
    return 0
    end
    + +

    可重入性

    可重入性是指线程在已经持有锁的情况下再次请求加锁,如果一个锁支持同一个线程多次加锁,那么就称这个锁是可重入的,类似 Java 的 ReentrantLock。

    +

    使用 ThreadLocal 实现锁计数

    Redis 分布式锁如果要支持可重入,可以使用线程的 ThreadLocal 变量存储当前持有的锁计数。但是在多次获得锁后,过期时间并没有得到延长,后续获得锁后持有锁的时间其实比设置的时间更短。

    +
    private ThreadLocal<Integer> lockCount = ThreadLocal.withInitial(() -> 0);

    public boolean tryLock() {
    Integer count = lockCount.get();
    if (count != null && count > 0) {
    lockCount.set(count + 1);
    return true;
    }
    String result = commands.set(lockKey, lockValue.get(), SetArgs.Builder.nx().px(RedisLockManager.LOCK_EXPIRE));
    if ("OK".equals(result)) {
    lockCount.set(1);
    scheduleRenewal();
    return true;
    }
    return false;
    }
    + +

    使用 Redis hash 实现锁计数

    还可以使用 Redis 的 hash 数据结构实现锁计数,支持重新获取锁后重置过期时间。

    +
    if (redis.call('exists', KEYS[1]) == 0) then 
    redis.call('hset', KEYS[1], ARGV[2], 1);
    redis.call('pexpire', KEYS[1], ARGV[1]);
    return nil;
    end;
    if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then
    redis.call('hincrby', KEYS[1], ARGV[2], 1);
    redis.call('pexpire', KEYS[1], ARGV[1]);
    return nil;
    return redis.call('pttl', KEYS[1]);
    + +

    书的作者不推荐使用可重入锁,他提出可重入锁会加重客户端的复杂度,如果在编写代码时注意在逻辑结构上进行调整,完全可以避免使用可重入锁。

    +

    代码实现

    redis-lock

    +

    参考文章

      +
    • 《Redis 深度历险,核心原理与应用实践》
    • +
    +]]>
    + + distributed lock + redis
    @@ -1603,7 +1603,7 @@
    public void run() {
    if (address == 0) {
    // Paranoia
    return;
    }
    unsafe.freeMemory(address);
    address = 0;
    Bits.unreserveMemory(size, capacity);
    }
    ]]> - Java + java jvm
    @@ -1948,7 +1948,7 @@
]]>
- Java + java jvm
diff --git a/tags/bytecode/index.html b/tags/bytecode/index.html index a7e0bb21..73a265e6 100644 --- a/tags/bytecode/index.html +++ b/tags/bytecode/index.html @@ -158,7 +158,7 @@ diff --git a/tags/clash/index.html b/tags/clash/index.html index 7eca15ee..11a88075 100644 --- a/tags/clash/index.html +++ b/tags/clash/index.html @@ -158,7 +158,7 @@ diff --git a/tags/ClassLoader/index.html b/tags/class-loader/index.html similarity index 95% rename from tags/ClassLoader/index.html rename to tags/class-loader/index.html index 51bb0805..edd642aa 100644 --- a/tags/ClassLoader/index.html +++ b/tags/class-loader/index.html @@ -22,7 +22,7 @@ - + @@ -30,14 +30,14 @@ - + - + -标签: ClassLoader | Moralok +标签: class loader | Moralok @@ -158,7 +158,7 @@ @@ -195,7 +195,7 @@
-

ClassLoader +

class loader 标签

diff --git a/tags/distributed-lock/index.html b/tags/distributed-lock/index.html index 2f362423..1e5c3b4f 100644 --- a/tags/distributed-lock/index.html +++ b/tags/distributed-lock/index.html @@ -158,7 +158,7 @@
diff --git a/tags/docker/index.html b/tags/docker/index.html index d368487c..3268f4d5 100644 --- a/tags/docker/index.html +++ b/tags/docker/index.html @@ -158,7 +158,7 @@
diff --git a/tags/index.html b/tags/index.html index a0284859..610838e1 100644 --- a/tags/index.html +++ b/tags/index.html @@ -27,7 +27,7 @@ - + @@ -161,7 +161,7 @@ @@ -209,10 +209,10 @@

tags
diff --git a/tags/java/index.html b/tags/java/index.html index 6da14daa..38e0e196 100644 --- a/tags/java/index.html +++ b/tags/java/index.html @@ -158,7 +158,7 @@
@@ -209,14 +209,114 @@

java
- +
+ + +
+

+ + + + + + + + + + + + diff --git a/tags/jvm/index.html b/tags/jvm/index.html index 8cba0ef1..3705df14 100644 --- a/tags/jvm/index.html +++ b/tags/jvm/index.html @@ -158,7 +158,7 @@ diff --git a/tags/minikube/index.html b/tags/minikube/index.html index d29c310b..bc43d402 100644 --- a/tags/minikube/index.html +++ b/tags/minikube/index.html @@ -158,7 +158,7 @@ diff --git a/tags/OpenVPN/index.html b/tags/openvpn/index.html similarity index 96% rename from tags/OpenVPN/index.html rename to tags/openvpn/index.html index 8c2c6fd4..1a164e02 100644 --- a/tags/OpenVPN/index.html +++ b/tags/openvpn/index.html @@ -22,7 +22,7 @@ - + @@ -30,14 +30,14 @@ - + - + -标签: OpenVPN | Moralok +标签: openvpn | Moralok @@ -158,7 +158,7 @@ @@ -195,7 +195,7 @@
-

OpenVPN +

openvpn 标签

diff --git a/tags/proxy/index.html b/tags/proxy/index.html index 719c0875..a299bbb5 100644 --- a/tags/proxy/index.html +++ b/tags/proxy/index.html @@ -158,7 +158,7 @@
diff --git a/tags/redis/index.html b/tags/redis/index.html index 9805da19..c64a50cf 100644 --- a/tags/redis/index.html +++ b/tags/redis/index.html @@ -158,7 +158,7 @@
diff --git a/tags/spring/index.html b/tags/spring/index.html index 727db9cb..be7096b4 100644 --- a/tags/spring/index.html +++ b/tags/spring/index.html @@ -158,7 +158,7 @@ @@ -209,14 +209,14 @@

spring
-
diff --git a/tags/ssh/index.html b/tags/ssh/index.html index 71a9240d..ffb21899 100644 --- a/tags/ssh/index.html +++ b/tags/ssh/index.html @@ -158,7 +158,7 @@ diff --git a/tags/tmux/index.html b/tags/tmux/index.html index 7d69b22e..b879790b 100644 --- a/tags/tmux/index.html +++ b/tags/tmux/index.html @@ -158,7 +158,7 @@ diff --git a/tags/Ubuntu/index.html b/tags/ubuntu/index.html similarity index 96% rename from tags/Ubuntu/index.html rename to tags/ubuntu/index.html index aec15820..4dcc6a15 100644 --- a/tags/Ubuntu/index.html +++ b/tags/ubuntu/index.html @@ -22,7 +22,7 @@ - + @@ -30,14 +30,14 @@ - + - + -标签: Ubuntu | Moralok +标签: ubuntu | Moralok @@ -158,7 +158,7 @@ @@ -195,7 +195,7 @@
-

Ubuntu +

ubuntu 标签