深入探讨5种单例模式

深入探讨5种单例模式

一、对比总览

以下是不同单例模式实现方式的特性对比表格。表格从线程安全性、延迟加载、实现复杂度、反序列化安全性、防反射攻击性等多个方面进行考量。

特性 饿汉式 饱汉式 饱汉式-双检锁 静态内部类 枚举单例(推荐)
线程安全性 ×
延迟加载 × ×
实现复杂度 ×
反序列化安全性 × × × ×
防反射攻击性 × × × ×

详细解释

  1. 线程安全性

    • 饿汉式:类加载时即创建实例(因为instance是static),线程安全。
    • 饱汉式:未使用同步机制,线程不安全。
    • 饱汉式-双检锁:使用同步块和双重检查锁,线程安全。
    • 静态内部类:通过类加载机制,线程安全。
    • 枚举单例:JVM确保线程安全。
  2. 延迟加载

    • 饿汉式:类加载时即创建实例,不具备延迟加载。
    • 饱汉式:实例在首次使用时创建,具备延迟加载。
    • 饱汉式-双检锁:实例在首次使用时创建,具备延迟加载。
    • 静态内部类:实例在首次使用时创建(因为静态内部类只有在使用时才会加载),具备延迟加载。
    • 枚举单例:类加载时即创建实例,不具备延迟加载。
  3. 实现复杂度

    • 饿汉式:实现简单。
    • 饱汉式:实现简单。
    • 饱汉式-双检锁:实现相对复杂。
    • 静态内部类:实现简单。
    • 枚举单例:实现简单。
  4. 反序列化安全性

    • 饿汉式饱汉式饱汉式-双检锁静态内部类 :需要实现 readResolve 方法以防止反序列化创建新实例。
    • 枚举单例:天然防止反序列化创建新实例,JVM保证。
  5. 防反射攻击性

    • 饿汉式饱汉式饱汉式-双检锁静态内部类:可能通过反射创建新实例。
    • 枚举单例 :防止反射攻击,创建新实例时抛出 IllegalArgumentException

二、代码

1. 饿汉式

language-java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
public class OrderManager1 {

@Getter
@Setter
private Map<String, TradeOrder> orders = new HashMap<>();

@Getter
private static final OrderManager1 instance = new OrderManager1();

/**
* 添加订单
*
* @param order 顺序
*/
public void addOrder(TradeOrder order) {

orders.put(order.getId(), order);
}

/**
* 获取订单
*
* @param orderId 订单id
* @return {@link TradeOrder}
*/
public TradeOrder getOrder(String orderId) {

return orders.get(orderId);
}
}

2. 饱汉式

language-java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
public class OrderManager2 {

@Getter
@Setter
private Map<String, TradeOrder> orders = new HashMap<>();

private static OrderManager2 instance;

public static OrderManager2 getInstance(){

if (instance == null){

instance = new OrderManager2();
}
return instance;
}

/**
* 添加订单
*
* @param order 顺序
*/
public void addOrder(TradeOrder order) {

orders.put(order.getId(), order);
}

/**
* 获取订单
*
* @param orderId 订单id
* @return {@link TradeOrder}
*/
public TradeOrder getOrder(String orderId) {

return orders.get(orderId);
}
}

3. 饱汉式-双检锁

language-java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
public class OrderManager3 {

@Getter
@Setter
private Map<String, TradeOrder> orders = new HashMap<>();

private static OrderManager3 instance;

public static OrderManager3 getInstance(){

if (instance == null){

synchronized (OrderManager3.class){

if (instance == null){

instance = new OrderManager3();
}
}
instance = new OrderManager3();
}
return instance;
}

/**
* 添加订单
*
* @param order 顺序
*/
public void addOrder(TradeOrder order) {

orders.put(order.getId(), order);
}

/**
* 获取订单
*
* @param orderId 订单id
* @return {@link TradeOrder}
*/
public TradeOrder getOrder(String orderId) {

return orders.get(orderId);
}
}

4. 静态内部类

language-java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
public class OrderManager4 {

@Getter
@Setter
private Map<String, TradeOrder> orders = new HashMap<>();

private static class SingletonHelper{

private static final OrderManager4 INSTANCE = new OrderManager4();
}

public static OrderManager4 getInstance(){

return SingletonHelper.INSTANCE;
}


/**
* 添加订单
*
* @param order 顺序
*/
public void addOrder(TradeOrder order) {

orders.put(order.getId(), order);
}

/**
* 获取订单
*
* @param orderId 订单id
* @return {@link TradeOrder}
*/
public TradeOrder getOrder(String orderId) {

return orders.get(orderId);
}
}

5. 枚举单例

language-java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
public enum OrderManager5 {

INSTANCE;

@Getter
@Setter
private Map<String, TradeOrder> orders = new HashMap<>();

/**
* 添加订单
*
* @param order 顺序
*/
public void addOrder(TradeOrder order) {

orders.put(order.getId(), order);
}

/**
* 获取订单
*
* @param orderId 订单id
* @return {@link TradeOrder}
*/
public TradeOrder getOrder(String orderId) {

return orders.get(orderId);
}
}

三、性能对比

language-java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
package org.dragon.singleton;

import lombok.extern.slf4j.Slf4j;

import java.util.*;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.function.Supplier;

@Slf4j
public class SingletonPerformanceTest {

static long timeout = 20; // 超时时间,单位为秒
static int testIterations = 10_000_000; // 测试次数
static int threadCount = 1000; // 并发线程数
static Map<String, HashMap<String, Long>> result = new HashMap<>();

public static void main(String[] args) {

/*
* 多次调用,结果是最后一次调用存入。为什么多次调用,因为单次test不准确,总是靠前的OrderManager跑的快,可能是因为Java某些机制导致的
* 所以多次调用,逐渐平稳。
* */
firstCreationTest();
mulAccessTest();
mulAccessTest();
mulAccessTest();
ConcurrentAccessTest();
ConcurrentAccessTest();
printRes();
ConcurrentAccessTest();
printRes();
ConcurrentAccessTest();
printRes();
}

/**
* 打印结果
*/
private static void printRes(){

ArrayList<String> names = new ArrayList<>();
names.add(OrderManager1.class.getSimpleName());
names.add(OrderManager2.class.getSimpleName());
names.add(OrderManager3.class.getSimpleName());
names.add(OrderManager4.class.getSimpleName());
names.add(OrderManager5.class.getSimpleName());
// 表头
System.out.printf("%-20s%-20s%-25s%-25s%-20s%n", "Singleton Type", "First Creation (ms)", "Multiple Access (ms)", "Concurrent Access (ms)", "Memory Used (MB)");
System.out.println("---------------------------------------------------------------------------------------------------------------");

for (String name : names) {

// 打印结果,转换时间为毫秒
System.out.printf("%-20s%-20.3f%-25.3f%-25.3f%-20.3f%n", name, result.get(name).get("firstCreation") / 1_000_000.0, result.get(name).get("mulAccess") / 1_000_000.0, result.get(name).get("ConcurrentAccess") / 1_000_000.0, 0 / (1024.0 * 1024.0));
}
}

/**
* 首次创建测试
*/
private static void firstCreationTest(){

List<Runnable> tests = new ArrayList<>();
tests.add(()->firstCreation(OrderManager1::getInstance));
tests.add(()->firstCreation(OrderManager2::getInstance));
tests.add(()->firstCreation(OrderManager3::getInstance));
tests.add(()->firstCreation(OrderManager4::getInstance));
tests.add(()->firstCreation(() -> OrderManager5.INSTANCE));
// 随机化测试顺序
Collections.shuffle(tests);
//run
for (Runnable test : tests) {

test.run();
log.info("Complete one test");
try {

Thread.sleep(200);
} catch (InterruptedException e) {

throw new RuntimeException(e);
}
}
}

/**
* 多次访问测试
*/
private static void mulAccessTest(){

List<Runnable> tests = new ArrayList<>();
tests.add(()->mulAccess(OrderManager1::getInstance, testIterations));
tests.add(()->mulAccess(OrderManager2::getInstance, testIterations));
tests.add(()->mulAccess(OrderManager3::getInstance, testIterations));
tests.add(()->mulAccess(OrderManager4::getInstance, testIterations));
tests.add(()->mulAccess(() -> OrderManager5.INSTANCE, testIterations));
// 随机化测试顺序
Collections.shuffle(tests);
//run
for (Runnable test : tests) {

test.run();
log.info("Complete one test");
try {

Thread.sleep(200);
} catch (InterruptedException e) {

throw new RuntimeException(e);
}
}
}

/**
* 多线程访问测试
*/
private static void ConcurrentAccessTest(){

List<Runnable> tests = new ArrayList<>();
tests.add(()->ConcurrentAccess(OrderManager1::getInstance, testIterations, threadCount));
tests.add(()->ConcurrentAccess(OrderManager2::getInstance, testIterations, threadCount));
tests.add(()->ConcurrentAccess(OrderManager3::getInstance, testIterations, threadCount));
tests.add(()->ConcurrentAccess(OrderManager4::getInstance, testIterations, threadCount));
tests.add(()->ConcurrentAccess(() -> OrderManager5.INSTANCE, testIterations, threadCount));
// 随机化测试顺序
Collections.shuffle(tests);
//run
for (Runnable test : tests) {

test.run();
log.info("Complete one test");
try {

Thread.sleep(200);
} catch (InterruptedException e) {

throw new RuntimeException(e);
}
}
}

/**
* 首次创建
*
* @param singletonSupplier 单一供应商
* @return long ns
*/
private static <T> long firstCreation(Supplier<T> singletonSupplier){

// 测试首次创建时间
long startTime = System.nanoTime();
T instance = singletonSupplier.get();
long endTime = System.nanoTime();
long resTime = endTime - startTime;
//save res
String simpleName = instance.getClass().getSimpleName();
HashMap<String, Long> resMap = result.computeIfAbsent(simpleName, k->new HashMap<>());
resMap.put("firstCreation", resTime);
return resTime;
}

/**
* 多次访问
*
* @param singletonSupplier 单一供应商
* @param iterations 迭代
* @return long ns
*/
private static <T> long mulAccess(Supplier<T> singletonSupplier, int iterations){

//预热
for (int i = 0; i < 100_000; i++) {

T instance = singletonSupplier.get();
}
//计算
long startTime = System.nanoTime();
for (int i = 0; i < iterations; i++) {

T instance = singletonSupplier.get();
}
long endTime = System.nanoTime();
long resTime = endTime - startTime;
//save res
String simpleName = singletonSupplier.get().getClass().getSimpleName();
HashMap<String, Long> resMap = result.computeIfAbsent(simpleName, k->new HashMap<>());
resMap.put("mulAccess", resTime);
return resTime;
}

/**
* 并发访问
*
* @param singletonSupplier 单一供应商
* @param iterations 迭代
* @param threadCount 线程数
* @return long ns
*/
private static <T> long ConcurrentAccess(Supplier<T> singletonSupplier, int iterations, int threadCount){

ExecutorService executorService = Executors.newFixedThreadPool(100);
//预热
CountDownLatch latch1 = new CountDownLatch(100);
for (int i = 0; i < threadCount; i++) {

executorService.submit(() -> {

for (int j = 0; j < 100_000; j++) {

T instance = singletonSupplier.get();
}
latch1.countDown();
});
}
try {

boolean completed = latch1.await(timeout, TimeUnit.SECONDS);
if (!completed) {

System.out.println("Concurrent access test for 预热" + singletonSupplier.get().getClass().getSimpleName() + " timed out!");
}
} catch (InterruptedException e) {

e.printStackTrace();
}
//计算
CountDownLatch latch2 = new CountDownLatch(threadCount);
long startTime = System.nanoTime();
for (int i = 0; i < threadCount; i++) {

executorService.submit(() -> {

for (int j = 0; j < iterations; j++) {

T instance = singletonSupplier.get();
}
latch2.countDown();
});
}
try {

boolean completed = latch2.await(timeout, TimeUnit.SECONDS);
if (!completed) {

System.out.println("Concurrent access test for " + singletonSupplier.getClass().getSimpleName() + " timed out!");
}
} catch (InterruptedException e) {

e.printStackTrace();
}
long endTime = System.nanoTime();
long concurrentAccessTime = endTime - startTime;

executorService.shutdown();
//save res
String simpleName = singletonSupplier.get().getClass().getSimpleName();
HashMap<String, Long> resMap = result.computeIfAbsent(simpleName, k->new HashMap<>());
resMap.put("ConcurrentAccess", concurrentAccessTime);
return concurrentAccessTime;
}
}

结果输出如下

language-txt
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
[17:15:54.519] [INFO ] org.dragon.singleton.SingletonPerformanceTest 73 firstCreationTest - Complete one test
[17:15:54.730] [INFO ] org.dragon.singleton.SingletonPerformanceTest 73 firstCreationTest - Complete one test
[17:15:54.936] [INFO ] org.dragon.singleton.SingletonPerformanceTest 73 firstCreationTest - Complete one test
[17:15:55.141] [INFO ] org.dragon.singleton.SingletonPerformanceTest 73 firstCreationTest - Complete one test
[17:15:55.347] [INFO ] org.dragon.singleton.SingletonPerformanceTest 73 firstCreationTest - Complete one test
[17:15:55.554] [INFO ] org.dragon.singleton.SingletonPerformanceTest 97 mulAccessTest - Complete one test
[17:15:55.782] [INFO ] org.dragon.singleton.SingletonPerformanceTest 97 mulAccessTest - Complete one test
[17:15:56.007] [INFO ] org.dragon.singleton.SingletonPerformanceTest 97 mulAccessTest - Complete one test
[17:15:56.227] [INFO ] org.dragon.singleton.SingletonPerformanceTest 97 mulAccessTest - Complete one test
[17:15:56.445] [INFO ] org.dragon.singleton.SingletonPerformanceTest 97 mulAccessTest - Complete one test
[17:15:56.669] [INFO ] org.dragon.singleton.SingletonPerformanceTest 97 mulAccessTest - Complete one test
[17:15:56.906] [INFO ] org.dragon.singleton.SingletonPerformanceTest 97 mulAccessTest - Complete one test
[17:15:57.146] [INFO ] org.dragon.singleton.SingletonPerformanceTest 97 mulAccessTest - Complete one test
[17:15:57.376] [INFO ] org.dragon.singleton.SingletonPerformanceTest 97 mulAccessTest - Complete one test
[17:15:57.598] [INFO ] org.dragon.singleton.SingletonPerformanceTest 97 mulAccessTest - Complete one test
[17:15:57.818] [INFO ] org.dragon.singleton.SingletonPerformanceTest 97 mulAccessTest - Complete one test
[17:15:58.054] [INFO ] org.dragon.singleton.SingletonPerformanceTest 97 mulAccessTest - Complete one test
[17:15:58.276] [INFO ] org.dragon.singleton.SingletonPerformanceTest 97 mulAccessTest - Complete one test
[17:15:58.495] [INFO ] org.dragon.singleton.SingletonPerformanceTest 97 mulAccessTest - Complete one test
[17:15:58.716] [INFO ] org.dragon.singleton.SingletonPerformanceTest 97 mulAccessTest - Complete one test
[17:15:59.430] [INFO ] org.dragon.singleton.SingletonPerformanceTest 121 ConcurrentAccessTest - Complete one test
[17:15:59.658] [INFO ] org.dragon.singleton.SingletonPerformanceTest 121 ConcurrentAccessTest - Complete one test
[17:16:02.737] [INFO ] org.dragon.singleton.SingletonPerformanceTest 121 ConcurrentAccessTest - Complete one test
[17:16:08.533] [INFO ] org.dragon.singleton.SingletonPerformanceTest 121 ConcurrentAccessTest - Complete one test
[17:16:13.700] [INFO ] org.dragon.singleton.SingletonPerformanceTest 121 ConcurrentAccessTest - Complete one test
[17:16:19.432] [INFO ] org.dragon.singleton.SingletonPerformanceTest 121 ConcurrentAccessTest - Complete one test
[17:16:24.632] [INFO ] org.dragon.singleton.SingletonPerformanceTest 121 ConcurrentAccessTest - Complete one test
[17:16:30.366] [INFO ] org.dragon.singleton.SingletonPerformanceTest 121 ConcurrentAccessTest - Complete one test
[17:16:35.516] [INFO ] org.dragon.singleton.SingletonPerformanceTest 121 ConcurrentAccessTest - Complete one test
[17:16:40.431] [INFO ] org.dragon.singleton.SingletonPerformanceTest 121 ConcurrentAccessTest - Complete one test
Singleton Type First Creation (ms) Multiple Access (ms) Concurrent Access (ms) Memory Used (MB)
---------------------------------------------------------------------------------------------------------------
OrderManager1 0.010 16.848 4928.236 0.000
OrderManager2 0.010 18.592 5504.781 0.000
OrderManager3 0.009 19.127 5513.309 0.000
OrderManager4 0.241 18.772 4920.940 0.000
OrderManager5 0.117 16.637 4704.835 0.000
[17:16:45.002] [INFO ] org.dragon.singleton.SingletonPerformanceTest 121 ConcurrentAccessTest - Complete one test
[17:16:48.982] [INFO ] org.dragon.singleton.SingletonPerformanceTest 121 ConcurrentAccessTest - Complete one test
[17:16:52.778] [INFO ] org.dragon.singleton.SingletonPerformanceTest 121 ConcurrentAccessTest - Complete one test
[17:16:57.834] [INFO ] org.dragon.singleton.SingletonPerformanceTest 121 ConcurrentAccessTest - Complete one test
[17:17:02.298] [INFO ] org.dragon.singleton.SingletonPerformanceTest 121 ConcurrentAccessTest - Complete one test
Singleton Type First Creation (ms) Multiple Access (ms) Concurrent Access (ms) Memory Used (MB)
---------------------------------------------------------------------------------------------------------------
OrderManager1 0.010 16.848 4217.333 0.000
OrderManager2 0.010 18.592 4340.869 0.000
OrderManager3 0.009 19.127 4824.581 0.000
OrderManager4 0.241 18.772 3743.032 0.000
OrderManager5 0.117 16.637 3558.995 0.000
[17:17:06.778] [INFO ] org.dragon.singleton.SingletonPerformanceTest 121 ConcurrentAccessTest - Complete one test
[17:17:11.231] [INFO ] org.dragon.singleton.SingletonPerformanceTest 121 ConcurrentAccessTest - Complete one test
[17:17:15.733] [INFO ] org.dragon.singleton.SingletonPerformanceTest 121 ConcurrentAccessTest - Complete one test
[17:17:20.812] [INFO ] org.dragon.singleton.SingletonPerformanceTest 121 ConcurrentAccessTest - Complete one test
[17:17:25.837] [INFO ] org.dragon.singleton.SingletonPerformanceTest 121 ConcurrentAccessTest - Complete one test
Singleton Type First Creation (ms) Multiple Access (ms) Concurrent Access (ms) Memory Used (MB)
---------------------------------------------------------------------------------------------------------------
OrderManager1 0.010 16.848 4281.649 0.000
OrderManager2 0.010 18.592 4849.279 0.000
OrderManager3 0.009 19.127 4782.224 0.000
OrderManager4 0.241 18.772 4267.228 0.000
OrderManager5 0.117 16.637 4233.907 0.000

进程已结束,退出代码为 0

可以去多跑几次,基本上最后一种枚举单例,性能属于最好的一批。并且也最安全。

Java - JVM

Java - JVM

一、JVM

1. JVM的作用

Java代码编译成java字节码后,运行在JVM中,只要针对不同的系统都开发JVM后,java就实现了跨平台。

2. JVM、JRE、JDK的关系

3. JVM的组成

  1. 类加载器(ClassLoader)
  2. 运行时数据区(Runtime Data Area)
  3. 执行引擎(Execution Engine)
  4. 本地库接口(Native Interface)

4. JVM工作流程

ClassLoader负责加载字节码文件,至于是否可以运行,由Execution Engine决定。
Execution Engine把指令和数据信息加载到内存中,并且负责把命令解释到操作系统,将JVM指令集翻译成操作系统指令集。
Execution Engine执行指令时,可能会调用一些本地的接口,这时就需要用到Native Interface,主要负责调用本地的接口给java程序使用,会在本地方法栈中记录对应的本地方法。

5. 运行时方法区Runtime Data Area

是JVM最重要的部分,运行时数据区的组成主要包含以下

  1. PC Register(程序计数器)
    程序计数器是程序控制流的指示器,循环,跳转,异常处理,线程的恢复等工作都需要依赖程序计数器去完成。程序计数器是线程私有的,它的生命周期是和线程保持一致的,我们知道,N 个核数的 CPU 在同一时刻,最多有 N个线程同时运行,在我们真实的使用过程中可能会创建很多线程,JVM 的多线程其实是通过线程轮流切换,分配处理器执行时间来实现的。既然涉及的线程切换,所以每条线程必须有一个独立的程序计数器。
  2. Stack (Java虚拟机栈)
    虚拟机栈,其描述的就是线程内存模型,也可以称作线程栈,也是每个线程私有的,生命周期与线程保持一致。在每个方法执行的时候,jvm 都会同步创建一个栈帧去存储局部变量表,操作数栈,动态连接,方法出口等信息。一个方法的生命周期就贯彻了一个栈帧从入栈到出栈的全部过程。
  3. Native Method Stack (本地方法栈)
    本地方法栈的概念很好理解,我们知道,java底层用了很多c的代码去实现,而其调用c端的方法上都会有native,代表本地方法服务,而本地方法栈就是为其服务的。
  4. Heap(堆)
    可以说是 JVM 中最大的一块儿内存区域了,它是所有线程共享的,不管你是初学者还是资深开发,多少都会听说过堆,毕竟几乎所有的对象都会在堆中分配。
  5. Method Area(方法区)
    方法区也是所有线程共享的区域,它存储了被 jvm 加载的类型信息、常量、静态变量等数据。运行时常量池就是方法区的一部分,编译期生成的各种字面量与符号引用就存储在其中。

💡 随着Java 7及以后版本的发布,虽然字符串常量池被移至堆内存,运行时常量池仍然是方法区(或Java 8中的元空间)的一部分。
💡 在Java 8之前,方法区的实现被称为永久代(PermGen),并且是堆的一部分。所以,当我们说”方法区”时,从概念上讲,它是JVM的一个独立逻辑部分,但在HotSpot JVM的具体实现中,直到Java 7为止,它是作为堆内存结构的一个部分(即永久代)来实现的,永久代是堆的一个物理部分。
💡从Java 8开始,HotSpot JVM去除了永久代的概念,引入了元空间(Metaspace),并且元空间是在本地内存中,而不是在堆内存中。因此,在Java 8及以后的版本中,方法区的实现从永久代变为了元空间,方法区(现在通常指的是元空间)与堆内存是完全分开的。

💡 元空间在本地内存中,只要内存足够,就不会出现OOM(Out of Memory)。元空间的概念仍然在JVM内存模型中。

二、深入JVM内存模型(JMM)

。。。待续

JavaSE-多线程

JavaSE-多线程

1.创建线程

创建线程有三种方式,都需要子类,然后在主程序中使用它。

其一

继承Thread类,重写run方法,这种方式简单,但是没法继承其他类。

language-java
1
2
3
4
5
6
7
8
9
10
11
public class Test01_01 extends Thread{

@Override
public void run() {

for (int i = 0; i < 5; i++) {

logger.info("thread sout:"+i);
}
}
}

运行方式如下

language-java
1
2
Thread t = new Test01_01();
t.start();

其二

实现Runnable接口,重写run方法,这种方式避免了无法继承别的类的缺陷。

language-java
1
2
3
4
5
6
7
8
9
10
11
12
public class Test02_01 implements Runnable{

@Override
public void run() {

Thread cut = Thread.currentThread();
for (int i = 0; i < 5; i++) {

System.out.println("thread ["+cut.getName()+"]"+i);
}
}
}

运行方式如下

language-java
1
2
Runnable target = new Test02_01();
new Thread(target).start();

或者使用lambda表达式

language-java
1
2
3
4
5
6
7
8
new Thread(()->{

Thread cut = Thread.currentThread();
for (int i = 0; i < 5; i++) {

System.out.println("thread ["+cut.getName()+"]"+i);
}
}).start();

其三

实现Callable接口,重写call方法,这种方式可以取得线程的返回值。

language-java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
private static class Test04_01 implements Callable<Long>{

@Override
public Long call() throws Exception {

Thread cut = Thread.currentThread();
System.out.println("当前线程:"+cut.getName());
long res = 0;
for (int i = 0; i < 100; i++) {

for (int j = 0; j < 100; j++) {

res += j;
}
}
return res;
}
};

运行方式如下

language-java
1
2
3
4
5
Callable<Long> call = new Test04_01();
FutureTask<Long> task = new FutureTask<>(call);
new Thread(task).start();
Long l = task.get();
System.out.println(l);

2.线程常用方式

3.线程同步

三种办法

其一

同步代码块,在类中使用

language-java
1
2
3
4
synchronized (LockObject){

//your code
}

对于示例方法,使用this作为LockObject,
对于静态方法,使用类名.class作为LockObject

其二

同步方法,在方法前加上synchronized 关键字
同步方法其实底层也是有隐式锁对象的,只是锁的范围是整个方法代码。
如果方法是实例方法:同步方法默认用this作为的锁对象。
如果方法是静态方法:同步方法默认用类名.class作为的锁对象。

其三

Lock锁,定义一把锁,需要加锁时lock,结束时unlock,

language-java
1
2
3
4
5
6
7
8
9
10
11
12
private final Lock lk = new ReentrantLock();
try {

lk.lock();
money -= 1;
} catch (Exception e) {

throw new RuntimeException(e);
} finally {

lk.unlock();
}

4.线程池

创建线程的开销是很大的。使用线程池可以复用线程,迭代任务不迭代线程,这样意味着,我们只能只用Runnable和Callable的方式开多线程。

创建线程池

language-java
1
2
3
4
ThreadPoolExecutor threadPool = new ThreadPoolExecutor(3, 5, 8, TimeUnit.SECONDS,
new ArrayBlockingQueue<>(4),
Executors.defaultThreadFactory(),
new ThreadPoolExecutor.CallerRunsPolicy());

七个参数的意思分别是:

  1. 核心线程数(一直存活的线程数)
  2. 最大线程数(最大-核心=允许存在的临时线程数)
  3. 临时线程存活时间(如果临时线程超过这个时间没有被分配任务就消亡)
  4. 存活时间单位
  5. 等待队列(ArrayBlockingQueue对象,需指定长度,被线程拿走的任务不算在等待队列)
  6. 线程工厂(使用Executors工具类即可)
  7. 拒绝策略(线程都在忙,等待队列满了,新的任务怎么处理)

临时线程的开启条件为,一个新任务来了,核心线程都在忙,同时等待队列已满,同时线程数还没达到最大线程数时,临时线程被创建,拿走新任务。

线程池常用方法

执行Runnable任务如下

language-java
1
2
Runnable target1 = new MyRunnable();
threadPool.execute(target1);// core 1

执行Callable任务如下

language-java
1
2
Callable<Integer> callableTask = new MyCallable();
Future<Integer> future = threadPool.submit(callableTask);

拒绝策略

线程池工具类

这样就不用自己去设计ThreadPoolExecutor了,Executors底层是封装ThreadPoolExecutor的。

一个经验之谈
计算密集型任务:核心线程数量=CPU核数 + 1
IO密集型任务:核心线程数量=CPU核数 * 2

5.并行并发

并发 并行
CPU能同时处理线程的数量有限,为了保证全部线程都能往前执行,CPU会轮巡为系统的每个线程服务,由于CPU切换的速度很快,给我们的感觉这些线程在同时执行,这就是并发。 在同一个时刻上,同时有多个线程在被CPU调度执行。

多线程是并发和并行同时进行的!

6.悲观乐观锁

悲观锁 乐观锁
一上来就加锁,没有安全感。每次只能一个线程进入访问完毕后,再解锁。线程安全,性能较差! 一开始不上锁,认为是没有问题的,大家一起跑,等要出现线程安全问题的时候才开始控制。线程安全,性能较好。

上述三种线程同步默认都是悲观锁。以下是一个悲观锁和乐观锁比较代码

language-java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
public class PostLock01 {

private static class MyRunnable01 implements Runnable{

private int number;
@Override
public void run() {

for (int i = 0; i < 1000; i++) {

synchronized (this){

++number;
}
}
}
};
private static class MyRunnable02 implements Runnable{

// private int number;
private AtomicInteger number = new AtomicInteger();
@Override
public void run() {

for (int i = 0; i < 1000; i++) {

number.incrementAndGet();
}
}
};

public static void main(String[] args) {

long timeStart, timeEnd;
timeStart = System.currentTimeMillis();
pessLock();
timeEnd = System.currentTimeMillis();
System.out.println("pessimism : "+(timeEnd - timeStart)+" ms");

timeStart = System.currentTimeMillis();
optiLock();
timeEnd = System.currentTimeMillis();
System.out.println("optimism : "+(timeEnd - timeStart)+" ms");
}

public static void pessLock(){

Runnable target = new MyRunnable01();
for (int i = 0; i < 32; i++) {

new Thread(target).start();
}
}

public static void optiLock(){

Runnable target = new MyRunnable02();
for (int i = 0; i < 32; i++) {

new Thread(target).start();
}
}

}
Java - 锁

Java - 锁

乐观锁

分为三个阶段:数据读取、写入校验、数据写入。

假设数据一般情况下不会造成冲突,只有在数据进行提交更新时,才会正式对数据的冲突与否进行检测,如果发现冲突了,则返回错误信息,让用户决定如何去做。fail-fast机制。

悲观锁

正如其名,它指对数据被外界(可能是本机的其他事务,也可能是来自其它服务器的事务处理)的修改持保守态度。在整个数据处理过程中,将数据处于锁定状态。悲观锁大多数情况下依靠数据库的锁机制实现,以保证操作最大程度的独占性。如果加锁的时间过长,其他用户长时间无法访问,影响程序的并发访问性,同时这样对数据库性能开销影响也很大,特别是长事务而言,这样的开销往往无法承受。

分布式锁

分布式集群中,对锁接口QPS性能要求很高,单台服务器满足不了要求,可以考虑将锁服务部署在独立的分布式系统中,比如借助分布式缓存来实现。

可重入锁

可重入锁,也叫做递归锁,是指在同一个线程在调外层方法获取锁的时候,再进入内层方法会自动获取锁。ReentrantLocksynchronized 都是 可重入锁。可重入锁的一个好处是可一定程度避免死锁。

这对于设计复杂的程序或库来说是非常重要的。考虑下面这种情况:

language-java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
class SomeClass {

private final Lock lock = new ReentrantLock();

public void methodA() {

lock.lock();
try {

// 一些代码
methodB(); // 在 methodA 中调用 methodB
// 更多代码
} finally {

lock.unlock();
}
}

public void methodB() {

lock.lock();
try {

// 一些代码
} finally {

lock.unlock();
}
}
}

在这个例子中,methodA()methodB() 都使用了同一把锁。如果 ReentrantLock 不是可重入的,那么当线程在 methodA() 中已经获取了锁后,再次尝试在 methodB() 中获取锁时,就会导致死锁,因为锁已经被同一个线程持有,而不是其他线程。

因此,”可重入”锁允许在同一线程中多次获取同一把锁,这样就能避免死锁,并且简化了程序设计,因为无需担心方法之间的调用顺序是否会导致死锁。

尽管在您提出的例子中看起来似乎并没有太大意义,但在实际的程序设计中,”可重入”锁确实是一个非常重要的概念。

自旋锁

自旋锁是一种基于忙等待(busy-waiting)的锁,它在获取锁时会不断地循环尝试获取锁,而不是让线程进入睡眠状态。自旋锁的主要特点是在锁被其他线程占用时,当前线程会不断地尝试获取锁,直到获取到锁为止,而不会释放 CPU 控制权。

自旋锁适用于以下情况:

  1. 锁被持有的时间短:如果锁被持有的时间很短,那么等待锁的线程不需要进入睡眠状态,使用自旋锁可以避免线程切换的开销

  2. 多核处理器:在多核处理器上,一个线程在自旋等待锁的同时,其他线程可以继续执行,因此自旋锁在多核处理器上能够充分利用 CPU 时间,提高并发性能。

  3. 高并发场景:在高并发的情况下,锁的竞争可能会很激烈,自旋锁可以减少线程的阻塞时间,提高系统的响应速度。

然而,自旋锁也有一些缺点:

  1. 等待时间过长可能会浪费 CPU 资源:如果锁的竞争很激烈,导致线程不断自旋等待锁的释放,可能会浪费大量的 CPU 时间。

  2. 不适用于长时间持有锁的情况:如果锁被持有的时间较长,自旋锁会导致其他线程长时间等待,影响系统的响应性能。

因此,在使用自旋锁时需要权衡利弊,根据具体的场景来决定是否使用自旋锁。通常情况下,自旋锁适用于锁被持有时间短、锁的竞争不激烈的情况下,能够有效提高并发性能。

当然可以。以下是一个简单的示例,演示了如何使用自旋锁来保护一个共享资源:

language-java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import java.util.concurrent.atomic.AtomicBoolean;

public class SpinLock {

private AtomicBoolean locked = new AtomicBoolean(false);

public void lock() {

// 不断尝试获取锁,直到成功为止
while (!locked.compareAndSet(false, true)) {

// 自旋等待,不做其他事情,持续尝试获取锁
}
}

public void unlock() {

// 释放锁
locked.set(false);
}
}

在这个示例中,SpinLock 类实现了一个自旋锁。AtomicBoolean 类被用作锁状态的标记,初始时为 false 表示未锁定状态。lock() 方法使用了自旋等待的方式来尝试获取锁,它会不断地尝试将 locked 的值从 false 设置为 true,直到成功获取到锁为止。在 unlock() 方法中,锁会被释放,将 locked 的值重新设置为 false

以下是一个使用 SpinLock 的示例:

language-java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
public class SpinLockExample {

private static int counter = 0;
private static SpinLock spinLock = new SpinLock();

public static void main(String[] args) throws InterruptedException {

Thread t1 = new Thread(() -> {

for (int i = 0; i < 1000; i++) {

spinLock.lock();
counter++;
spinLock.unlock();
}
});

Thread t2 = new Thread(() -> {

for (int i = 0; i < 1000; i++) {

spinLock.lock();
counter++;
spinLock.unlock();
}
});

t1.start();
t2.start();
t1.join();
t2.join();

System.out.println("Counter: " + counter);
}
}

在这个示例中,两个线程 t1t2 分别对 counter 执行了 1000 次增加操作,每次增加操作都在获取自旋锁后执行,并在执行完毕后释放自旋锁。最后输出 counter 的值,由于自旋锁的保护,counter 的增加操作是线程安全的。

独享锁

独享锁是指该锁一次只能被一个线程所持有。

共享锁

共享锁是指该锁可被多个线程所持有。ReentrantReadWriteLock,其读锁是共享锁,其写锁是独享锁。读锁的共享锁可保证并发读是非常高效的,读写、写读、写写的过程是互斥的。独享锁与共享锁也是通过AQSAbstractQueuedSynchronizer)来实现的,通过实现不同的方法,来实现独享或者共享。

互斥锁

独享锁/共享锁就是一种广义的说法,互斥锁/读写锁指具体的实现。

读写锁

读写锁在Java中的具体实现就是ReentrantReadWriteLock

阻塞锁

阻塞锁,可以说是让线程进入阻塞状态进行等待,当获得相应的信号(唤醒,时间) 时,才可以进入线程的准备就绪状态,准备就绪状态的所有线程,通过竞争进入运行状态。

JAVA中,能够进入\退出、阻塞状态或包含阻塞锁的方法有 ,synchronized 关键字(其中的重量锁),ReentrantLock,Object.wait()/notify(),LockSupport.park()/unpark()

公平锁

公平锁是指多个线程按照申请锁的顺序来获取锁

非公平锁

非公平锁是指多个线程获取锁的顺序并不是按照申请锁的顺序,有可能后申请的线程比先申请的线程优先获取锁。

可能造成优先级反转或者饥饿现象。对于Java ReentrantLock而言,通过构造函数 ReentrantLock(boolean fair) 指定该锁是否是公平锁,默认是非公平锁。

非公平锁的优点在于吞吐量比公平锁大。对于Synchronized而言,也是一种非公平锁。

分段锁

分段锁其实是一种锁的设计,目的是细化锁的粒度,并不是具体的一种锁,对于ConcurrentHashMap而言,其并发的实现就是通过分段锁的形式来实现高效的并发操作。

ConcurrentHashMap中的分段锁称为Segment,它即类似于HashMapJDK7HashMap的实现)的结构,即内部拥有一个Entry数组,数组中的每个元素又是一个链表;同时又是一个ReentrantLockSegment继承了ReentrantLock)

当需要put元素的时候,并不是对整个HashMap加锁,而是先通过hashcode知道要放在哪一个分段中,然后对这个分段加锁,所以当多线程put时,只要不是放在同一个分段中,可支持并行插入。

对象锁

一个线程可以多次对同一个对象上锁。对于每一个对象,java虚拟机维护一个加锁计数器,线程每获得一次该对象,计数器就加1,每释放一次,计数器就减 1,当计数器值为0时,锁就被完全释放了。

synchronized修饰非静态方法、同步代码块的synchronized (this)synchronized (非this对象),锁的是对象,线程想要执行对应同步代码,需要获得对象锁。

使用 synchronized 加锁 this 时,只有同一个对象会使用同一把锁,不同对象之间的锁是不同的。 ​

当需要同步访问对象的实例方法或实例变量时,应该使用 this 作为 synchronized 的参数。
例如,在一个多线程环境中,多个线程需要同时访问对象的实例方法或实例变量时,可以使用 synchronized(this) 来确保线程安全。

language-java
1
2
3
4
5
6
7
public void synchronizedBlock() {

synchronized (this) {

// 同步代码块
}
}

类锁

synchronized修饰静态方法或者同步代码块的synchronized (类.class),线程想要执行对应同步代码,需要获得类锁。
使用 synchronized 加锁 class 时,无论共享一个对象还是创建多个对象,它们用的都是同一把锁
当需要同步访问类的静态方法或静态变量时,应该使用 MyClass.class 作为 synchronized 的参数。
例如,在一个多线程环境中,多个线程需要同时访问类的静态方法或静态变量时,可以使用 synchronized(MyClass.class) 来确保线程安全。

language-java
1
2
3
4
5
6
7
public static void synchronizedStaticBlock() {

synchronized (MyClass.class) {

// 同步代码块
}
}

锁升级

锁升级是Java虚拟机中的一种优化策略,它是针对 synchronized 关键字进行的优化,并不是像 ReentrantLock 这样的锁类库,可以在代码逻辑中直接使用的锁。

(无锁、偏向锁、轻量级锁、重量级锁)是指在Java虚拟机中对锁的状态进行优化和调整的过程。这些状态反映了对象的锁定状态以及锁的竞争情况。这种锁升级的目的是为了在不同情况下提供不同的锁定机制,以减少锁的竞争、提高性能。

下面是对这几种锁状态的简要介绍:

  1. 无锁状态

    • 当线程尝试获取锁时,对象的锁状态为无锁状态,表示该对象没有被任何线程锁定。
    • 在无锁状态下,线程会通过CAS(Compare and Swap)等原子操作尝试直接修改对象的指针或标记位,来尝试获取锁。
  2. 偏向锁

    • 当只有一个线程访问同步块时,对象的锁状态会升级为偏向锁状态。
    • 偏向锁会将线程的ID记录在对象头中,表示该线程拥有对象的偏向锁。当其他线程尝试获取锁时,会检查偏向锁的线程ID是否与当前线程ID相同,如果相同则表示获取成功。
  3. 轻量级锁

    • 当有多个线程竞争同步块时,对象的锁状态会升级为轻量级锁状态。
    • 轻量级锁使用CAS操作来避免传统的互斥量操作,尝试在用户态下通过自旋来获取锁。如果自旋获取锁失败,则升级为重量级锁。
  4. 重量级锁

    • 当轻量级锁竞争失败时,对象的锁状态会升级为重量级锁状态。
    • 重量级锁会使得竞争失败的线程进入阻塞状态,从而让出CPU资源,减少竞争。

锁升级是Java虚拟机对锁状态的动态调整过程,旨在根据实际的锁竞争情况和线程行为来选择最适合的锁策略,以提高程序的并发性能。

Spring AMQP(3.1.1)设置ConfirmCallback和ReturnsCallback

Spring AMQP(3.1.1)设置ConfirmCallback和ReturnsCallback

环境如下

Version
SpringBoot 3.2.1
spring-amqp 3.1.1
RabbitMq 3-management

一、起因

老版本的spring-amqpCorrelationData上设置ConfirmCallback。但是今天却突然发现correlationData.getFuture()没有addCallback函数了。

查询文档和帖子后,发现ConfirmCallbackReturnsCallback都需要在RabbitTemplate中设置,同时ConfirmCallback中默认无法得到消息内容,如果想在ConfirmCallback中把消息内容存到数据库等地方进行记录,怎么办呢?

参考手册

二、代码

1. 定义exchange和queue

language-java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
@Slf4j
@Configuration
public class PayNotifyConfig{


//交换机
public static final String PAYNOTIFY_EXCHANGE_FANOUT = "paynotify_exchange_fanout";
//支付通知队列
public static final String PAYNOTIFY_QUEUE = "paynotify_queue";
//支付结果通知消息类型
public static final String MESSAGE_TYPE = "payresult_notify";


//声明交换机,且持久化
@Bean(PAYNOTIFY_EXCHANGE_FANOUT)
public FanoutExchange paynotify_exchange_fanout() {

// 三个参数:交换机名称、是否持久化、当没有queue与其绑定时是否自动删除
return new FanoutExchange(PAYNOTIFY_EXCHANGE_FANOUT, true, false);
}
//支付通知队列,且持久化
@Bean(PAYNOTIFY_QUEUE)
public Queue paynotify_queue() {

return QueueBuilder.durable(PAYNOTIFY_QUEUE).build();
}

//交换机和支付通知队列绑定
@Bean
public Binding binding_paynotify_queue(@Qualifier(PAYNOTIFY_QUEUE) Queue queue, @Qualifier(PAYNOTIFY_EXCHANGE_FANOUT) FanoutExchange exchange) {

return BindingBuilder.bind(queue).to(exchange);
}
}

2. RabbitTemplate

在上面的类中继续添加RabbitTemplate ,并设置ConfirmCallbackReturnsCallback

language-java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
@Bean
public RabbitTemplate rabbitTemplate(final ConnectionFactory connectionFactory) {

final RabbitTemplate rabbitTemplate = new RabbitTemplate(connectionFactory);
//设置confirm callback
rabbitTemplate.setConfirmCallback((correlationData, ack, cause) -> {

String body = "1";
if (correlationData instanceof EnhancedCorrelationData) {

body = ((EnhancedCorrelationData) correlationData).getBody();
}
if (ack) {

//消息投递到exchange
log.debug("消息发送到exchange成功:correlationData={},message_id={} ", correlationData, body);
System.out.println("消息发送到exchange成功:correlationData={},message_id={}"+correlationData+body);
} else {

log.debug("消息发送到exchange失败:cause={},message_id={}",cause, body);
System.out.println("消息发送到exchange失败:cause={},message_id={}"+cause+body);
}
});

//设置return callback
rabbitTemplate.setReturnsCallback(returned -> {

Message message = returned.getMessage();
int replyCode = returned.getReplyCode();
String replyText = returned.getReplyText();
String exchange = returned.getExchange();
String routingKey = returned.getRoutingKey();
// 投递失败,记录日志
log.error("消息发送失败,应答码{},原因{},交换机{},路由键{},消息{}",
replyCode, replyText, exchange, routingKey, message.toString());
});
return rabbitTemplate;
}

3. EnhancedCorrelationData

原始的CorrelationData,目前已经无法从中获取消息内容,也就是说现在的ConfirmCallback无法获取到消息的内容,因为设计上只关注是否投递到exchange成功。如果需要在ConfirmCallback中获取消息的内容,需要扩展这个类,并在发消息的时候,放入自定义数据。

language-java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class EnhancedCorrelationData extends CorrelationData {

private final String body;

public EnhancedCorrelationData(String id, String body) {

super(id);
this.body = body;
}

public String getBody() {

return body;
}
}

4. 发送消息

EnhancedCorrelationData把消息本身放进去,或者如果你有表记录消息,你可以只放入其id。这样触发ConfirmCallback的时候,就可以获取消息内容。

language-java
1
2
3
4
5
6
7
8
9
public void notifyPayResult() {

String message = "TEST Message";
Message message1 = MessageBuilder.withBody(message.getBytes(StandardCharsets.UTF_8))
.setDeliveryMode(MessageDeliveryMode.PERSISTENT)
.build();
CorrelationData correlationData = new EnhancedCorrelationData(UUID.randomUUID().toString(), message.toString());
rabbitTemplate.convertAndSend(PayNotifyConfig.PAYNOTIFY_EXCHANGE_FANOUT,"", message1, correlationData);
}
Gateway中Spring Security6统一处理CORS

Gateway中Spring Security6统一处理CORS

一、起因

使用了gateway微服务作为整体的网关,并且整合了Spring Security6;还有一个system微服务,作为被请求的资源,当浏览器向gateway发送请求,请求system资源时,遇到CORS问题。

于是我在system对应的controller上加了@CrossOrigin,无效;配置WebMvcConfigurer,也无效。
后来发现,会不会是gatewayspring security6在一开始就拦截了CORS跨域请求,导致根本走不到后面的system配置。

查询了一波,果然如此。这里记录解决方法。

二、解决方法

这是依赖

language-xml
1
2
3
4
5
6
7
8
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>

这是配置

language-java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
@EnableWebFluxSecurity
@Configuration
public class SecurityConfig {


//安全拦截配置
@Bean
public SecurityWebFilterChain webFluxSecurityFilterChain(ServerHttpSecurity http) throws Exception {

return http
.cors(cors -> cors.configurationSource(corsConfigurationSource()))
.authorizeExchange(exchanges ->
exchanges
.pathMatchers("/**").permitAll()
.anyExchange().authenticated()
)
.oauth2ResourceServer(oauth2 -> oauth2.jwt(Customizer.withDefaults()))
.csrf(ServerHttpSecurity.CsrfSpec::disable)
.build();
}

@Bean
public CorsConfigurationSource corsConfigurationSource() {

CorsConfiguration corsConfig = new CorsConfiguration();
corsConfig.addAllowedOriginPattern("*"); // 允许任何源
corsConfig.addAllowedMethod("*"); // 允许任何HTTP方法
corsConfig.addAllowedHeader("*"); // 允许任何HTTP头
corsConfig.setAllowCredentials(true); // 允许证书(cookies)
corsConfig.setMaxAge(3600L); // 预检请求的缓存时间(秒)

UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", corsConfig); // 对所有路径应用这个配置
return source;
}
}

需要注意的是,在gatewayspring security中处理了CORS问题后,后续的system什么的,就不需要再二次处理了。因为CORS是一个浏览器的策略,只要处理一次,告诉浏览器我允许跨域,浏览器收到后就不再阻拦请求了。

Spring Authorization Server Spring Security密码加密

Spring Authorization Server Spring Security密码加密

一、修改密码编码器

BCryptPasswordEncoder举例。
直接将其注册成PasswordEncoderBean即可。

language-java
1
2
3
4
5
6
7
8
    @Bean
public PasswordEncoder passwordEncoder() {

// 密码为明文方式
// return NoOpPasswordEncoder.getInstance();
// 或使用 BCryptPasswordEncoder
return new BCryptPasswordEncoder();
}

二、效果

使用了加密算法后,无论是RegisteredClient的密码还是UserDetailsService的密码,都会以密文的方式存储在服务器上。

但是前端输入的密码仍然是以明文的方式出现并传到服务器,之后服务器会对明文进行相同的手段(指对同样的明文,密文相同)加密,比较两个密文是否一致。

三、注意点

1. RegisteredClient

language-java
1
2
3
4
5
6
7
8
9
   @Bean
public RegisteredClientRepository registeredClientRepository() {

RegisteredClient registeredClient = RegisteredClient.withId(UUID.randomUUID().toString())
.clientId("XcWebApp")
.clientSecret(passwordEncoder().encode("XcWebApp"))
.build();
return new InMemoryRegisteredClientRepository(registeredClient);
}

RegisteredClientclientSecret仍然需要提供密文。

是因为,加密这个行为,只在服务器校验前端发送的明文时使用,至于对照物,则是代码中提供好的密文,所以这个需要提供密文。

2. UserDetailsService

对于UserDetailsService也是,密码也需要提供现成的密文形式。
下面的代码中数据库保存的是password密文。

language-java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
@Component
@Slf4j
public class UserServiceImpl implements UserDetailsService {

@Autowired
XcUserMapper xcUserMapper;

@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {

//根据username查询数据库
XcUser xcUser = xcUserMapper.selectOne(new LambdaQueryWrapper<XcUser>()
.eq(XcUser::getUsername, username));
//用户不存在,返回null
if (xcUser == null){

return null;
}
//用户存在,拿到密码,封装成UserDetails,密码对比由框架进行
String password = xcUser.getPassword();

UserDetails userDetails = User.withUsername(username).password(password).authorities("read").build();
return userDetails;
}
}

SpringBoot使用当前类代理类(内部事务)解决方案

SpringBoot使用当前类代理类(内部事务)解决方案

Spring Boot 开发中,我们时常遇到需要在一个类的内部调用自己的其他方法,并且这些方法可能需要事务支持。这种场景通常发生在业务逻辑较为复杂的服务类中,其中一些操作需要确保数据的一致性和完整性。本文将以 MediaFileServiceImpl 类为例,探讨如何在 Spring Boot 中有效地使用当前类的代理类来处理内部事务。

一、场景描述

考虑一个典型的例子:在 MediaFileServiceImpl 服务类中,upload 方法需要调用 upload2Mysql 方法。这里,upload2Mysql 方法是事务性的,意味着它涉及到数据库操作,这些操作需要在一个事务中被处理。如果直接在 upload 方法中调用 upload2Mysql,由于 Spring 的代理方式,事务管理可能不会被正确应用,因为实际上是在同一个实例内部进行方法调用,绕过了 Spring 的代理。

language-java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@Service
@Slf4j
public class MediaFileServiceImpl implements MediaFileService {


@Lazy
@Autowired
private MediaFileService mediaFileService;

@Override
public UploadFileResultDto upload() {

// ... 一些业务逻辑 ...
mediaFileService.upload2Mysql();
// ... 其他业务逻辑 ...
}

@Transactional
@Override
public MediaFiles upload2Mysql() {

// ... 事务性操作 ...
}
}

二、解决方案

1. 使用 @Lazy(推荐)

MediaFileServiceImpl 类中使用 @Lazy 注解解决循环依赖问题。

language-java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@Service
@Slf4j
public class MediaFileServiceImpl implements MediaFileService {


@Lazy
@Autowired
private MediaFileService mediaFileService;

@Override
public UploadFileResultDto upload() {

// ... 一些业务逻辑 ...
mediaFileService.upload2Mysql();
// ... 其他业务逻辑 ...
}

@Transactional
@Override
public MediaFiles upload2Mysql() {

// ... 事务性操作 ...
}
}

2. 使用方法注入

方法注入是一种允许在运行时动态注入方法实现的技术。这里,我们通过一个简单的例子来说明如何应用方法注入。

language-java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@Service
@Slf4j
public abstract class MediaFileServiceImpl implements MediaFileService {


@Override
public UploadFileResultDto upload() {

// ... 一些业务逻辑 ...
getMediaFileService().upload2Mysql();
// ... 其他业务逻辑 ...
}

@Transactional
@Override
public MediaFiles upload2Mysql() {

// ... 事务性操作 ...
}

@Lookup
protected abstract MediaFileService getMediaFileService();
}

3. 使用 ApplicationContext

这种方法通过 ApplicationContext 获取当前类的代理。

language-java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
@Service
@Slf4j
public class MediaFileServiceImpl implements MediaFileService {


@Autowired
private ApplicationContext context;

private MediaFileService getMediaFileService() {

return context.getBean(MediaFileService.class);
}

@Override
public UploadFileResultDto upload() {

// ... 一些业务逻辑 ...
getMediaFileService().upload2Mysql();
// ... 其他业务逻辑 ...
}

@Transactional
@Override
public MediaFiles upload2Mysql() {

// ... 事务性操作 ...
}
}

4. 分离服务层

将事务性方法移至另一个服务类中。

language-java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
@Service
@Slf4j
public class MediaFileServiceImpl implements MediaFileService {


@Autowired
private MediaFileTransactionalService transactionalService;

@Override
public UploadFileResultDto upload() {

// ... 一些业务逻辑 ...
transactionalService.upload2Mysql();
// ... 其他业务逻辑 ...
}
}

@Service
@Transactional
class MediaFileTransactionalService {


public MediaFiles upload2Mysql() {

// ... 事务性操作 ...
}
}

5. AspectJ 代理模式

使用 AspectJ 代理模式而不是默认的 JDK 动态代理。

language-java
1
2
3
4
5
6
@EnableAspectJAutoProxy(proxyTargetClass = true)
@Configuration
public class AppConfig {

// ... 其他配置 ...
}

然后,您的 MediaFileServiceImpl 类保持不变。

SpringCloud Gateway(4.1.0) 返回503:原因分析与解决方案

SpringCloud Gateway(4.1.0) 返回503:原因分析与解决方案

一、环境版本

Version
spring-cloud-dependencies 2023.0.0
spring-cloud-starter-gateway 4.1.0
Nacos v2.3.0

二、原因分析

Spring Cloud Gateway 的早期版本中,Ribbon 被用作默认的负载均衡器。随着Spring Cloud的发展,RibbonSpring Cloud LoadBalancer 替代。在过渡期间,为了兼容,Spring Cloud 同时支持了 RibbonSpring Cloud LoadBalancer。然而,从 Spring Cloud 2020.0.0 版本开始,Ribbon 被废弃,Spring Cloud LoadBalancer 成为了推荐的负载均衡方案。

在这个版本变动中,为了提供更大的灵活性,spring-cloud-starter-loadbalancer 被标记为了可选依赖,不再默认包含在 Spring Cloud Gateway 中。因此,在使用 4.1.0 版本的 Spring Cloud Gateway 并需要服务发现和负载均衡功能时,如果没有显式包含这个依赖,就会导致无法处理 lb://URI,从而返回503错误。

三、解决方案

要解决这个问题,您需要在您的项目的 POM 文件中显式添加 spring-cloud-starter-loadbalancer 依赖:

language-xml
1
2
3
4
5
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-loadbalancer</artifactId>
<version>4.1.0</version>
</dependency>

添加后,确保重启应用程序以使配置生效。这样,Spring Cloud Gateway 就能够正确处理基于服务发现的负载均衡,从而避免503错误。

通过理解 Spring Cloud 的历史演变和适应其依赖管理的变化,我们可以更好地维护和优化我们的微服务架构。

SpringCloud + Nacos配置文件加载顺序和优先级详解

SpringCloud + Nacos配置文件加载顺序和优先级详解

在微服务架构中,合理地管理和理解配置文件的加载顺序与优先级对于确保应用的稳定性和灵活性至关重要。特别是在使用 Spring Cloud Alibaba Nacos 作为配置中心的场景下,这一点显得尤为重要。本文将基于一个具体的 bootstrap.yml 配置示例,深入探讨这些概念,并介绍如何通过 Nacos 配置实现本地配置的优先级设置。

一、加载顺序与优先级

1. 示例配置

首先,我们看一下示例的 bootstrap.yml 配置:

language-yaml
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
spring:
application:
name: content-api
cloud:
nacos:
server-addr: 192.168.101.65:8848
discovery:
namespace: dev
group: xuecheng-plus-project
config:
namespace: dev
group: xuecheng-plus-project
file-extension: yaml
refresh-enabled: true
extension-configs:
- data-id: content-service-${
spring.profiles.active}.yaml
group: xuecheng-plus-project
refresh: true
shared-configs:
- data-id: swagger-${
spring.profiles.active}.yaml
group: xuecheng-plus-common
refresh: true
- data-id: logging-${
spring.profiles.active}.yaml
group: xuecheng-plus-common
refresh: true
profiles:
active: dev

2. 配置文件分类

Spring Cloud Alibaba Nacos 环境中,我们主要遇到以下类型的配置文件:

  1. 本地配置文件

    • bootstrap.yml / bootstrap.yaml
    • application.yml / application.yaml
  2. Nacos 配置中心的配置文件

    • 共享配置文件 (shared-configs)
    • 扩展配置文件 (extension-configs)
    • 项目应用名配置文件 (${spring.application.name}.yaml / .properties)

3. 加载顺序

  1. **bootstrap.yml / bootstrap.yaml**:首先加载,用于配置应用的启动环境。
  2. Nacos 配置中心的配置文件
    • 先加载 共享配置文件 (shared-configs)
    • 然后是 扩展配置文件 (extension-configs)
    • 最后是 项目应用名配置文件 (${spring.application.name}.yaml / .properties)
  3. **application.yml / application.yaml**:在 Nacos 配置加载之后。

4. 优先级

  1. 项目应用名配置文件:具有最高优先级。
  2. 扩展配置文件:次之,覆盖共享配置。
  3. 共享配置文件:优先级低于扩展配置。
  4. **本地 application.yml / application.yaml**:优先级低于所有从 Nacos 加载的配置。
  5. **本地 bootstrap.yml / bootstrap.yaml**:优先级最低。

二、本地配置优先的设置

Nacos 中,可以通过特定的配置来设置本地配置优先。这可以在 bootstrap.ymlapplication.yml 文件中设置:

language-yaml
1
2
3
4
spring:
cloud:
config:
override-none: true

override-none 设置为 true 时,本地配置文件 (application.yml / application.yaml) 将具有最高的优先级,即使这些配置在 Nacos 中也有定义。这种设置适用于需要在不同环境中覆盖远程配置中心配置的场景。

结论

了解和正确应用 Spring Cloud Alibaba Nacos 中配置文件的加载顺序和优先级,对于确保微服务的正确运行至关重要。此外,通过配置 override-nonetrue,可以灵活地实现本地配置优先的需求,进一步增强了配置管理的灵活性。这些特性使得 Spring Cloud Alibaba Nacos 成为管理微服务配置的强大工具。