0%

概述

双链表实现了List和Deque接口。 实现所有可选列表操作,并允许所有元素(包括null )。

所有的操作都能像双向列表一样预期。 索引到列表中的操作将从开始或结束遍历列表,以更接近指定的索引为准。

请注意,此实现不同步。 如果多个线程同时访问链接列表,并且至少有一个线程在结构上修改列表,则必须在外部进行同步。 (结构修改是添加或删除一个或多个元素的任何操作;仅设置元素的值不是结构修改。)
这通常通过在自然封装列表的对象上进行同步来实现。 如果没有这样的对象存在,列表应该使用 Collections.synchronizedList 方法“包装”。 这最好在创建时完成,以防止意外的不同步访问列表:

List list = Collections.synchronizedList(new LinkedList(...)); 

这个类的 iterator 和 listIterator 方法返回的迭代器是故障快速的 :如果列表在迭代器创建之后的任何时间被结构化地修改,除了通过迭代器自己的remove或add方法之外,
迭代器将会抛出一个ConcurrentModificationException 。 因此,面对并发修改,迭代器将快速而干净地失败,而不是在未来未确定的时间冒着任意的非确定性行为。

请注意,迭代器的故障快速行为无法保证,因为一般来说,在不同步并发修改的情况下,无法做出任何硬性保证。
失败快速迭代器尽力投入ConcurrentModificationException 。 因此,编写依赖于此异常的程序的正确性将是错误的:迭代器的故障快速行为应仅用于检测错误。

(以上来自 Java8 api)

分析

首先看一下 LinkedList 的继承关系:

LinkedListUML.png

定义

1
2
3
public class LinkedList<E>
extends AbstractSequentialList<E>
implements List<E>, Deque<E>, Cloneable, java.io.Serializable{}
  1. LinkedList 是一个继承于 AbstractSequentialList 的双向链表。它也可以被当作堆栈、队列或双端队列进行操作。
    最大限度地减少了实现受“连续访问”数据存储(如链接列表)支持的此接口所需的工作,从而以减少实现 List 接口的复杂度。
  2. LinkedList 实现 List 接口,能对它进行序列(有序集合)操作。
  3. LinkedList 实现 Deque 接口,即能将LinkedList当作双端队列使用。
  4. LinkedList 实现了 Cloneable 接口,即覆盖了函数 clone(),能克隆。
  5. LinkedList 实现 java.io.Serializable 接口,这意味着 LinkedList 支持序列化,能通过序列化去传输。
  6. LinkedList 是非同步的。

属性

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 class LinkedList<E> extends AbstractSequentialList<E> implements List<E>, Deque<E>, Cloneable, java.io.Serializable{

transient int size = 0;// list中的元素个数

/**
* 链表头节点
* 不变式: (first == null && last == null) || (first.prev == null && first.item != null)
*/
transient Node<E> first;

/**
* 链表尾节点
* 不变式: (first == null && last == null) || (last.next == null && last.item != null)
*/
transient Node<E> last;

private static class Node<E> {
E item;// 实际存放的元素
Node<E> next;// 后一个节点
Node<E> prev;// 前一个节点

// 构造函数元素顺序分别为前,自己,后。就像排队一样
Node(Node<E> prev, E element, Node<E> next) {
this.item = element;
this.next = next;
this.prev = prev;
}
}
}

构造方法

由于采用的是链表结构,所以不像 ArrayList 一样,有指定容量的构造方法

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
public class LinkedList<E> extends AbstractSequentialList<E> implements List<E>, Deque<E>, Cloneable, java.io.Serializable{

/**
* 构造一个空列表.
*/
public LinkedList() {
}

/**
* 构造一个包含指定 collection 中的元素的列表,这些元素按其 collection 的迭代器返回的顺序排列
*/
public LinkedList(Collection<? extends E> c) {
this();// 什么都不做
addAll(c);// 将 c 集合里的元素添加进链表
}

/**
* 按照指定集合的迭代器返回的顺序将指定集合中的所有元素追加到此列表的末尾。
*/
public boolean addAll(Collection<? extends E> c) {
return addAll(size, c);
}

private void checkPositionIndex(int index) {
if (!isPositionIndex(index))
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}

/**
* 判断参数是迭代器或添加操作的有效位置的索引。
*/
private boolean isPositionIndex(int index) {
return index >= 0 && index <= size;
}

/**
* 从指定位置开始,将指定集合中的所有元素插入此列表。
* 将当前位置的元素(如果有)和任何后续元素向右移动(增加其索引)。
* 新元素将按照指定集合的迭代器返回的顺序出现在列表中。
*/
public boolean addAll(int index, Collection<? extends E> c) {
checkPositionIndex(index);// 检查索引是否正确,即在 0 <= index <= size

Object[] a = c.toArray();// 将 collection 转为数组
int numNew = a.length;
if (numNew == 0)
return false;

Node<E> pred, succ;// 声明 pred 为"当前要插入节点的前一个节点",succ 为"当前要插入节点的后一个节点"
if (index == size) {// 说明要插入元素的位置就在链表的末尾,后置元素为null,前一个元素就是last
succ = null;
pred = last;
} else { // 说明在链表的中间插入,这时 pred 为原来 index 的 prev,succ 为原来的元素
succ = node(index);// 利用双向链表的特性,进行更快的遍历
pred = succ.prev;
}

for (Object o : a) {// 遍历数组,逐个添加
@SuppressWarnings("unchecked") E e = (E) o;
Node<E> newNode = new Node<>(pred, e, null);
if (pred == null)
first = newNode;
else
pred.next = newNode;
pred = newNode;// 将新节点作为pred,为下一个元素插入做准备

}

if (succ == null) {// 如果后继元素为空,那么插入完后的最后一个元素,就 pred 就是 last
last = pred;
} else {// 否则就维护最后一个元素和之前的元素之间的关系
pred.next = succ;
succ.prev = pred;
}

size += numNew;
modCount++;// 链表结构发生改动
return true;
}

/**
* 返回指定元素索引处的(非空)节点
* 利用双向链表的特性,进行更快的遍历
* 双向链表和索引值联系起来:通过一个计数索引值来实现
* 当我们调用get(int index)时,首先会比较“index”和“双向链表长度的1/2”;
* 若前者大,则从链表头开始往后查找,直到 index 位置;
* 否则,从链表末尾开始先前查找,直到 index 位置.
*/
Node<E> node(int index) {
// assert isElementIndex(index);

if (index < (size >> 1)) {// 如果index在链表的前半部分,则从头部节点开始遍历
Node<E> x = first;
for (int i = 0; i < index; i++)
x = x.next;
return x;
} else {// 如果index在链表的后半部分,则从尾部节点开始遍历
Node<E> x = last;
for (int i = size - 1; i > index; i--)
x = x.prev;
return x;
}
}
}

增加 add(E e)

作为链表,添加新元素就是在链表的末尾插入新元素。

注意,如果末尾元素是 null ,又该如何处理?

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
public class LinkedList<E> extends AbstractSequentialList<E> implements List<E>, Deque<E>, Cloneable, java.io.Serializable{
/**
* 将指定的元素追加到此列表的末尾。
*/
public boolean add(E e) {
linkLast(e);
return true;
}

/**
* 链接 e 作为最后一个元素。
*/
void linkLast(E e) {
final Node<E> l = last;// 记录last节点
final Node<E> newNode = new Node<>(l, e, null);// 初始化新的节点

last = newNode;
if (l == null)// 末尾元素是 null,是个空列表
first = newNode;
else
l.next = newNode;
size++;
modCount++;// 链表结构发生改动
}
}

LinkedList 还有其他的增加方法:

  • add(int index, E element):在此列表中指定的位置插入指定的元素。
  • addAll(Collection<? extends E> c):添加指定 collection 中的所有元素到此列表的结尾,顺序是指定 collection 的迭代器返回这些元素的顺序。
  • addAll(int index, Collection<? extends E> c):将指定 collection 中的所有元素从指定位置开始插入此列表。
  • AddFirst(E e): 将指定元素插入此列表的开头。
  • addLast(E e): 将指定元素添加到此列表的结尾。

移除

处理思路:

  1. 由于插入的元素可能为null,所以要对o进行判断,否则不论是o为null还是遍历的时候元素为null,都会导致报空指针异常
  2. 找到元素后,对前后的元素关系重新维护,要考虑到元素是否在头尾的情况
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
public class LinkedList<E> extends AbstractSequentialList<E> implements List<E>, Deque<E>, Cloneable, java.io.Serializable{

public boolean remove(Object o) {
if (o == null) {// 是否为 null 的判断
// 从头节点遍历链表寻找第一个 x(null) 元素
for (Node<E> x = first; x != null; x = x.next) {
if (x.item == null) {
unlink(x);// 取消链接 x(null) 元素,重新维护删除元素后的前后关系
return true;
}
}
} else {// 与上面的逻辑相同
for (Node<E> x = first; x != null; x = x.next) {
if (o.equals(x.item)) {
unlink(x);
return true;
}
}
}
return false;
}

/**
* Unlinks non-null node x.
*/
E unlink(Node<E> x) {
// assert x != null;
final E element = x.item;
// 局部保存被删除节点的前后节点
final Node<E> next = x.next;
final Node<E> prev = x.prev;

if (prev == null) {// prev 为 null 说明 x 节点为 first 节点,则删除后,next 为 first
first = next;
} else {// 否则 prev的下一个元素为x的next
prev.next = next;
x.prev = null;// 设为 null,方便GC
}

if (next == null) {// next 为null说明x节点为 last 节点,则删除后,next 为 prev
last = prev;
} else {// 否则 next 的上一个元素为x的prev
next.prev = prev;
x.next = null;// 设为 null,方便GC
}

x.item = null;// 设为 null,方便GC
size--;
modCount++;// 链表结构发生改变
return element;//返回被删除节点的数据体
}
}

其他的移除方法:

  • clear(): 从此列表中移除所有元素。
  • remove():获取并移除此列表的头(第一个元素)。
  • remove(int index):移除此列表中指定位置处的元素。
  • remove(Objec o):从此列表中移除首次出现的指定元素(如果存在)。
  • removeFirst():移除并返回此列表的第一个元素。
  • removeFirstOccurrence(Object o):从此列表中移除第一次出现的指定元素(从头部到尾部遍历列表时)。
  • removeLast():移除并返回此列表的最后一个元素。
  • removeLastOccurrence(Object o):从此列表中移除最后一次出现的指定元素(从头部到尾部遍历列表时)。

查询

查询的方法非常简单,

1
2
3
4
5
6
7
public class LinkedList<E> extends AbstractSequentialList<E> implements List<E>, Deque<E>, Cloneable, java.io.Serializable{

public E get(int index) {
checkElementIndex(index);// 检查索引index 是否在 [0,size] 区间内
return node(index).item;//利用双向链表的特性,进行更快的遍历
}
}

其它的查询方法:

  • getFirst():返回此列表的第一个元素。
  • getLast():返回此列表的最后一个元素。
  • indexOf(Object o):返回此列表中首次出现的指定元素的索引,如果此列表中不包含该元素,则返回 -1。
  • lastIndexOf(Object o):返回此列表中最后出现的指定元素的索引,如果此列表中不包含该元素,则返回 -1。

迭代器 listIterator

关于集合的快速失败机制的详细了解可以看这里

iterator() 调用的其实是 listIterator() 方法,对于不同的实现类,都会实现不同的方法,但是其原理是一致的,
都是为了防止多线程操作同一个集合而出现的问题

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
public class LinkedList<E> extends AbstractSequentialList<E> implements List<E>, Deque<E>, Cloneable, java.io.Serializable{

public ListIterator<E> listIterator(int index) {
checkPositionIndex(index);// 检查索引的正确性[0, size]
return new ListItr(index);
}

private class ListItr implements ListIterator<E> {
private Node<E> lastReturned;// 记录上次返回的元素
private Node<E> next;// 记录下一个元素
private int nextIndex;
private int expectedModCount = modCount;// 用来判断迭代过程中,是否有对元素的改动(fail-fast)

ListItr(int index) {
// assert isPositionIndex(index);
next = (index == size) ? null : node(index);//初始化next,以便在next方法中返回
nextIndex = index;
}

public boolean hasNext() {
return nextIndex < size;
}

public E next() {
checkForComodification();// 判断是否有对元素的改动,有则抛出异常
if (!hasNext())
throw new NoSuchElementException();

lastReturned = next;// next()当中的next元素就是要返回的结果
next = next.next;
nextIndex++;
return lastReturned.item;
}

// 省略其它代码。。。

final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
}
}

有关队列、栈的方法

  • peek():返回第一个节点,若LinkedList的大小为0,则返回null
  • peekFirst():返回第一个节点,若LinkedList的大小为0,则返回null
  • peekLast():返回最后一个节点,若LinkedList的大小为0,则返回null
  • element():返回第一个节点,若LinkedList的大小为0,则抛出异常
  • poll():删除并返回第一个节点,若LinkedList的大小为0,则返回null
  • pollFirst():删除并返回第一个节点,若LinkedList的大小为0,则返回null
  • pollLast():删除并返回最后一个节点,若LinkedList的大小为0,则返回null
  • offer(E e):将e添加双向链表末尾
  • offerFirst(E e):将e添加双向链表开头
  • offerLast(E e):将e添加双向链表末尾
  • push(E e):将e插入到双向链表开头
  • pop():删除并返回第一个节点

LinkedList 作为 FIFO(先进先出) 的队列, 下表的方法等效:

队列方法 等效方法
add(e) addLast(e)
offer(e) offerLast(e)
remove() removeFirst()
poll() pollFirst()
element() getFirst()
peek() peekFirst()

LinkedList 作为 LIFO(后进先出) 的栈, 下表的方法等效:

栈方法 等效方法
push(e) addFirst(e)
pop() removeFirst()
peek() peekFirst()

LinkedList 的遍历方法和性能比较

使用示例

总结

  1. LinkedList 实际上是通过双向链表去实现的。它包含一个非常重要的内部类:NodeNode 是双向链表节点所对应的数据结构,
    它包括的属性有:当前节点所包含的值,上一个节点,下一个节点。
  2. 从 LinkedList 的实现方式中可以发现,它不存在LinkedList容量不足的问题。
  3. LinkedList 的克隆函数,即是将全部元素克隆到一个新的LinkedList对象中。
  4. LinkedList 实现java.io.Serializable。当写入到输出流时,先写入“容量”,再依次写入“每一个节点保护的值”;
    当读出输入流时,先读取“容量”,再依次读取“每一个元素”。
  5. 由于 LinkedList 实现了Deque,而 Deque 接口定义了在双端队列两端访问元素的方法。提供插入、移除和检查元素的方法。
    每种方法都存在两种形式:一种形式在操作失败时抛出异常,另一种形式返回一个特殊值(null 或 false,具体取决于操作)。

[TOC]

概要

本文主要以 ArrayList 为例,对 Iterator 的快速失败(fail-fast), 也就是 Java 集合的错误检测机制进行学习总结。主要内容有:

  1. 简介
  2. 错误展示
  3. 问题解决
  4. 理解原理
  5. JDK的解决办法

简介

“快速失败”也就是 fail-fast,它是 Java 集合的一种错误检测机制。当多个线程对集合进行结构上的改变的操作时,有可能会产生 fail-fast 机制。
记住是有可能,而不是一定。例如:假设存在两个线程(线程 1、线程 2),线程 1 通过 Iterator 在遍历集合 A 中的元素,
在某个时候线程 2 修改了集合 A 的结构(是结构上面的修改,而不是简单的修改集合元素的内容),
那么这个时候程序就会抛出 ConcurrentModificationException 异常,从而产生 fail-fast 机制。

错误示例

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
package com.littlefxc.examples.base.collections;

import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;

/**
* Java 集合的错误检测机制 fail-fast 的示例
*
* @author fengxuechao
*/
public class FailFastTest {

private static List<Integer> list = new ArrayList<>();
//private static List<String> list = new CopyOnWriteArrayList<String>();

/**
* 线程one迭代list
*/
private static class threadOne extends Thread {

@Override
public void run() {
Iterator<Integer> iterator = list.iterator();
while (iterator.hasNext()) {
int i = iterator.next();
System.out.println("ThreadOne 遍历:" + i);
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}

/**
* 当i == 3时,修改list
*/
private static class threadTwo extends Thread {

@Override
public void run() {
int i = 0;
while (i < 6) {
System.out.println("ThreadTwo run:" + i);
if (i == 3) {
list.remove(i);
}
i++;
}
}
}

public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
list.add(i);
}
new threadOne().start();
new threadTwo().start();
}
}

运行结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
ThreadOne 遍历:0
ThreadTwo run:0
ThreadTwo run:1
ThreadTwo run:2
ThreadTwo run:3
ThreadTwo run:4
ThreadTwo run:5
Exception in thread "Thread-0" java.util.ConcurrentModificationException
at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:901)
at java.util.ArrayList$Itr.next(ArrayList.java:851)
at com.littlefxc.examples.base.collections.FailFastTest$threadOne.run(FailFastTest.java:25)

Process finished with exit code 0

问题解决

先说解决办法:

  1. 在遍历过程中所有涉及到改变 modCount 值得地方全部加上 synchronized 或者直接使用 Collections.synchronizedList,这样就可以解决。但是不推荐,因为增删造成的同步锁可能会阻塞遍历操作。
  2. 使用 CopyOnWriteArrayList 来替换 ArrayList。推荐使用该方案。

理解原理

同过上面的错误示例和问题解决,可以初步了解到产生 fail-fast 的原因就在于
当某一个线程遍历list的过程中,list的内容被另外一个线程所改变了;
就会抛出 ConcurrentModificationException 异常,产生fail-fast事件。

ConcurrentModificationException 的产生:当方法检测到对象的并发修改,但不允许这种修改时就抛出该异常。

也就是说,即便是在单线程环境中,只要违反了规则,同样也可能会抛出异常。

当我对代码运行多次时,发现代码运行有几率不抛出异常,这就说明迭代器的快速失败行为并不能得到保证,所以,不要写依赖这个异常的程序代码。
正确的做法是:ConcurrentModificationException 应该仅用于检测 bug。

AbstractList 抛出 ConcurrentModificationException 的部分代码(Java8):

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
package java.util;

public abstract class AbstractList<E> extends AbstractCollection<E> implements List<E> {

//神略代码...

// AbstractList中唯一的属性
// 用来记录List修改的次数:每修改一次(添加/删除等操作),将modCount+1
protected transient int modCount = 0;

// 返回List对应迭代器。实际上,是返回Itr对象。
public Iterator<E> iterator() {
return new Itr();
}

// Itr是Iterator(迭代器)的实现类
private class Itr implements Iterator<E> {
int cursor = 0;

int lastRet = -1;

// 修改数的记录值。
// 每次新建Itr()对象时,都会保存新建该对象时对应的modCount;
// 以后每次遍历List中的元素的时候,都会比较expectedModCount和modCount是否相等;
// 若不相等,则抛出ConcurrentModificationException异常,产生fail-fast事件。
int expectedModCount = modCount;

public boolean hasNext() {
return cursor != size();
}

public E next() {
// 获取下一个元素之前,都会判断“新建Itr对象时保存的modCount”和“当前的modCount”是否相等;
// 若不相等,则抛出ConcurrentModificationException异常,产生fail-fast事件。
checkForComodification();
try {
E next = get(cursor);
lastRet = cursor++;
return next;
} catch (IndexOutOfBoundsException e) {
checkForComodification();
throw new NoSuchElementException();
}
}

public void remove() {
if (lastRet == -1)
throw new IllegalStateException();
checkForComodification();

try {
AbstractList.this.remove(lastRet);
if (lastRet < cursor)
cursor--;
lastRet = -1;
expectedModCount = modCount;
} catch (IndexOutOfBoundsException e) {
throw new ConcurrentModificationException();
}
}

final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
}

//省略代码...
}

从中,我们可以发现在调用 next() 和 remove()时,都会执行 checkForComodification()。若 “modCount 不等于 expectedModCount”,则抛出ConcurrentModificationException异常,产生fail-fast事件。

要搞明白 fail-fast机制,我们就要需要理解什么时候“modCount 不等于 expectedModCount”!
从Itr类中,我们知道 expectedModCount 在创建Itr对象时,被赋值为 modCount。通过Itr,我们知道:expectedModCount不可能被修改为不等于 modCount。所以,需要考证的就是modCount何时会被修改。

那么它(modCount)在什么时候因为什么原因而发生改变呢?

ArrayList部分源码:

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
public class ArrayList<E> extends AbstractList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable
{
/**
* 最小化列表容量
*/
public void trimToSize() {
modCount++;
if (size < elementData.length) {
elementData = (size == 0)
? EMPTY_ELEMENTDATA
: Arrays.copyOf(elementData, size);
}
}

/**
* 确定动态扩容所需容量
*/
public void ensureCapacity(int minCapacity) {
int minExpand = (elementData != DEFAULTCAPACITY_EMPTY_ELEMENTDATA)
// any size if not default element table
? 0
// larger than default for default empty table. It's already
// supposed to be at default size.
: DEFAULT_CAPACITY;

if (minCapacity > minExpand) {
ensureExplicitCapacity(minCapacity);
}
}

/**
* 确定动态扩容所需容量
*/
private void ensureCapacityInternal(int minCapacity) {
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
}

ensureExplicitCapacity(minCapacity);
}

/**
* 动态扩容
*/
private void ensureExplicitCapacity(int minCapacity) {
modCount++;

// overflow-conscious code
if (minCapacity - elementData.length > 0)
grow(minCapacity);
}

/**
* 1. 将指定元素的索引及后续元素的索引向右移动(索引+1)
* 2. 在指定的索引插入元素
*/
public void add(int index, E element) {
rangeCheckForAdd(index);

ensureCapacityInternal(size + 1); // Increments modCount!!
System.arraycopy(elementData, index, elementData, index + 1,
size - index);
elementData[index] = element;
size++;
}

/**
* 1. 将指定索引及后续元素的索引向左移动
* 2. 数组元素实际数量 - 1
*/
public E remove(int index) {
rangeCheck(index);

modCount++;
E oldValue = elementData(index);

int numMoved = size - index - 1;
if (numMoved > 0)
System.arraycopy(elementData, index+1, elementData, index,
numMoved);
elementData[--size] = null; // clear to let GC do its work

return oldValue;
}

// 循环比较元素,获取要移除元素的索引,然后将该索引及后续元素的索引向左移动
public boolean remove(Object o) {
if (o == null) {
for (int index = 0; index < size; index++)
if (elementData[index] == null) {
fastRemove(index);
return true;
}
} else {
for (int index = 0; index < size; index++)
if (o.equals(elementData[index])) {
fastRemove(index);
return true;
}
}
return false;
}

private void fastRemove(int index) {
modCount++;
int numMoved = size - index - 1;
if (numMoved > 0)
System.arraycopy(elementData, index+1, elementData, index,
numMoved);
elementData[--size] = null; // clear to let GC do its work
}

/**
* 循环设置所有元素值为null, 加快垃圾回收
*/
public void clear() {
modCount++;

// clear to let GC do its work
for (int i = 0; i < size; i++)
elementData[i] = null;

size = 0;
}
}

从上面的源代码我们可以看出,ArrayList 中无论 add、remove、clear 方法只要是涉及了改变 ArrayList 元素的个数的方法都会导致 modCount 的改变。
所以我们这里可以初步判断由于 expectedModCount 得值与 modCount 的改变不同步,导致两者之间不等从而产生 fail-fast 机制。

场景还原:

有两个线程(线程 A,线程 B),其中线程 A 负责遍历 list、线程B修改 list。线程 A 在遍历 list 过程的某个时候(此时 expectedModCount = modCount=N),
线程启动,同时线程B增加一个元素,这是 modCount 的值发生改变(modCount + 1 = N + 1)。
线程 A 继续遍历执行 next 方法时,通告 checkForComodification 方法发现 expectedModCount = N ,而 modCount = N + 1,两者不等,
这时就抛出ConcurrentModificationException 异常,从而产生 fail-fast 机制。

至此,我们就完全了解了fail-fast是如何产生的!

也就是,当多个线程对同一个集合进行操作的时候,某线程访问集合的过程中,该集合的内容被其他线程所改变(即其它线程通过add、remove、clear等方法,改变了modCount的值);
这时,就会抛出ConcurrentModificationException异常,产生fail-fast事件。

JDK的解决办法:CopyOnWriteArrayList

CopyOnWriteArrayList 是 ArrayList 的一个线程安全的变体,其中所有可变操作(add、set 等等)都是通过对底层数组进行一次新的复制来实现的。
该类产生的开销比较大,但是在两种情况下,它非常适合使用。

  1. 在不能或不想进行同步遍历,但又需要从并发线程中排除冲突时。
  2. 当遍历操作的数量大大超过可变操作的数量时。

遇到这两种情况使用 CopyOnWriteArrayList 来替代 ArrayList 再适合不过了。

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
package java.util.concurrent;
import java.util.*;
import java.util.concurrent.locks.*;
import sun.misc.Unsafe;

public class CopyOnWriteArrayList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable {

// 省略代码...

// 返回集合对应的迭代器
public Iterator<E> iterator() {
return new COWIterator<E>(getArray(), 0);
}

// 省略代码...

private static class COWIterator<E> implements ListIterator<E> {
private final Object[] snapshot;

private int cursor;

private COWIterator(Object[] elements, int initialCursor) {
cursor = initialCursor;
// 新建COWIterator时,将集合中的元素保存到一个新的拷贝数组中。
// 这样,当原始集合的数据改变,拷贝数据中的值也不会变化。
snapshot = elements;
}

public boolean hasNext() {
return cursor < snapshot.length;
}

public boolean hasPrevious() {
return cursor > 0;
}

public E next() {
if (! hasNext())
throw new NoSuchElementException();
return (E) snapshot[cursor++];
}

public E previous() {
if (! hasPrevious())
throw new NoSuchElementException();
return (E) snapshot[--cursor];
}

public int nextIndex() {
return cursor;
}

public int previousIndex() {
return cursor-1;
}

public void remove() {
throw new UnsupportedOperationException();
}

public void set(E e) {
throw new UnsupportedOperationException();
}

public void add(E e) {
throw new UnsupportedOperationException();
}
}

// 省略代码...

}

可以从上面的源码中可以看出:

  1. 和 ArrayList 继承于 AbstractList 不同,CopyOnWriteArrayList 没有继承于 AbstractList,它仅仅只是实现了 List 接口。
  2. ArrayList 的 iterator() 函数返回的 Iterator 是在 AbstractList 中实现的;而 CopyOnWriteArrayList 是自己实现 Iterator。
  3. ArrayList 的 Iterator 实现类中调用 next() 时,会“调用 checkForComodification() 比较 expectedModCount modCount 的大小”;但是,CopyOnWriteArrayList 的 Iterator 实现类中,没有所谓的 checkForComodification(),更不会抛出 ConcurrentModificationException 异常!

CopyOnWriterArrayList 的 add 方法与 ArrayList 的 add 方法有一个最大的不同点就在于,下面三句代码:

1
2
3
Object[] newElements = Arrays.copyOf(elements, len + 1);
newElements[len] = e;
setArray(newElements);

就是这三句代码使得 CopyOnWriterArrayList 不会抛 ConcurrentModificationException 异常。
它们就是 copy 原来的 array,再在 copy 数组上进行 add 操作,这样做就完全不会影响 COWIterator 中的 array 了

CopyOnWriterArrayList 的核心概念就是:

任何对 array 在结构上有所改变的操作(add、remove、clear 等),CopyOnWriterArrayList 都会 copy 现有的数据,再在 copy 的数据上修改,
这样就不会影响 COWIterator 中的数据了,修改完成之后改变原有数据的引用即可。同时这样造成的代价就是产生大量的对象,
同时数组的 copy 也是相当有损耗的。

Spring + MyBatis + MySQL主从分离

基于 Docker 的 MySQL 主从复制搭建

基于 Docker 的 MySQL 主从复制搭建

前言

在大型应用程序中,配置主从数据库并使用读写分离是常见的设计模式。而要对现有的代码在不多改变源码的情况下,
可以使用 Spring 的 AbstractRoutingDataSource 和 Mybatis 的 Interceptor 为核心做到感知mysql读写分离

阅读全文 »

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
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
1204
1205
1206
1207
1208
1209
1210
1211
1212
1213
1214
1215
1216
1217
1218
1219
1220
1221
1222
1223
1224
1225
1226
1227
1228
1229
import java.util.List;
import java.util.Map;
import java.util.Set;

/**
* Common interface for sharded and non-sharded Jedis
*/
public interface JedisCommands {

/**
* 存储数据到缓存中,若key已存在则覆盖 value的长度不能超过1073741824 bytes (1 GB)
*
* @param key
* @param value
* @return
*/
String set(String key, String value);

/**
* 存储数据到缓存中,并制定过期时间和当Key存在时是否覆盖。
*
* @param key
* @param value
* @param nxxx
* nxxx的值只能取NX或者XX,如果取NX,则只有当key不存在是才进行set,如果取XX,则只有当key已经存在时才进行set
*
* @param expx expx的值只能取EX或者PX,代表数据过期时间的单位,EX代表秒,PX代表毫秒。
* @param time 过期时间,单位是expx所代表的单位。
* @return
*/
String set(String key, String value, String nxxx, String expx, long time);

/**
* 从缓存中根据key取得其String类型的值,如果key不存在则返回null,如果key存在但value不是string类型的,
* 则返回一个error。这个方法只能从缓存中取得value为string类型的值。
*
* @param key
* @return
*/
String get(String key);

/**
* 检查某个key是否在缓存中存在,如果存在返回true,否则返回false;需要注意的是,即使该key所对应的value是一个空字符串,
* 也依然会返回true。
*
* @param key
* @return
*/
Boolean exists(String key);

/**
*
* 如果一个key设置了过期时间,则取消其过期时间,使其永久存在。
*
* @param key
* @return 返回1或者0,1代表取消过期时间成功,0代表不成功(只有当key不存在时这种情况才会发生)
*/
Long persist(String key);

/**
* 返回某个key所存储的数据类型,返回的数据类型有可能是"none", "string", "list", "set", "zset",
* "hash". "none"代表key不存在。
*
* @param key
* @return
*/
String type(String key);

/**
* 为key设置一个特定的过期时间,单位为秒。过期时间一到,redis将会从缓存中删除掉该key。
* 即使是有过期时间的key,redis也会在持久化时将其写到硬盘中,并把相对过期时间改为绝对的Unix过期时间。
* 在一个有设置过期时间的key上重复设置过期时间将会覆盖原先设置的过期时间。
*
* @param key
* @param seconds
* @return 返回1表示成功设置过期时间,返回0表示key不存在。
*/
Long expire(String key, int seconds);

/**
* 机制同{@link expire}一样,只是时间单位改为毫秒。
*
* @param key
* @param milliseconds
* @return 返回值同 {@link expire}一样。
*/
Long pexpire(String key, long milliseconds);

/**
* 与{@link expire}不一样,expireAt设置的时间不是能存活多久,而是固定的UNIX时间(从1970年开始算起),单位为秒。
*
* @param key
* @param unixTime
* @return
*/
Long expireAt(String key, long unixTime);

/**
* 同{@link expireAt}机制相同,但单位为毫秒。
*
* @param key
* @param millisecondsTimestamp
* @return
*/
Long pexpireAt(String key, long millisecondsTimestamp);

/**
* 返回一个key还能活多久,单位为秒
*
* @param key
* @return 如果该key本来并没有设置过期时间,则返回-1,如果该key不存在,则返回-2
*/
Long ttl(String key);

/**
* 设置或者清除指定key的value上的某个位置的比特位,如果该key原先不存在,则新创建一个key,其value将会自动分配内存,
* 直到可以放下指定位置的bit值。
*
* @param key
* @param offset
* @param value true代表1,false代表0
* @return 返回原来位置的bit值是否是1,如果是1,则返回true,否则返回false。
*/
Boolean setbit(String key, long offset, boolean value);

/**
* 设置或者清除指定key的value上的某个位置的比特位,如果该key原先不存在,则新创建一个key,其value将会自动分配内存,
* 直到可以放下指定位置的bit值。
*
* @param key
* @param offset
* @param value 只能是"1"或者"0"
* @return 返回原来位置的bit值是否是1,如果是1,则返回true,否则返回false。
*/
Boolean setbit(String key, long offset, String value);

/**
* 取得偏移量为offset的bit值。
*
* @param key
* @param offset
* @return true代表1,false代表0
*/
Boolean getbit(String key, long offset);

/**
* 这个命令的作用是覆盖key对应的string的一部分,从指定的offset处开始,覆盖value的长度。
* 如果offset比当前key对应string还要长,
* 那这个string后面就补0以达到offset。不存在的keys被认为是空字符串,所以这个命令可以确保key有一个足够大的字符串
* 能在offset处设置value。
*
* @param key
* @param offset
* @param value
* @return 该命令修改后的字符串长度
*/
Long setrange(String key, long offset, String value);

/**
* 获得start - end之间的子字符串,若偏移量为负数,代表从末尾开始计算,例如-1代表倒数第一个,-2代表倒数第二个
*
* @param key
* @param startOffset
* @param endOffset
* @return
*/
String getrange(String key, long startOffset, long endOffset);

/**
* 自动将key对应到value并且返回原来key对应的value。如果key存在但是对应的value不是字符串,就返回错误。
*
* @param key
* @param value
* @return
*/
String getSet(String key, String value);

/**
* 参考 {@link set(String key, String value, String nxxx, String expx, long
* time)}
*
* @param key
* @param value
* @return
*/
Long setnx(String key, String value);

/**
* 参考 {@link set(String key, String value, String nxxx, String expx, long
* time)}
*
* @param key
* @param seconds
* @param value
* @return
*/
String setex(String key, int seconds, String value);

/**
* 将指定key的值减少某个值
*
* @param key
* @param integer
* @return 返回减少后的新值
*/
Long decrBy(String key, long integer);

/**
* 将指定Key的值减少1
*
* @param key
* @return 返回减少后的新值
*/
Long decr(String key);

/**
* 将指定的key的值增加指定的值
*
* @param key
* @param integer
* @return 返回增加后的新值
*/
Long incrBy(String key, long integer);

/**
* 将指定的key的值增加指定的值(浮点数)
*
* @param key
* @param value
* @return 返回增加后的新值
*/
Double incrByFloat(String key, double value);

/**
* 将指定的key的值增加1
*
* @param key
* @return 返回增加后的新值
*/
Long incr(String key);

/**
* 若key存在,将value追加到原有字符串的末尾。若key不存在,则创建一个新的空字符串。
*
* @param key
* @param value
* @return 返回字符串的总长度
*/
Long append(String key, String value);

/**
* 返回start - end 之间的子字符串(start 和 end处的字符也包括在内)
*
* @param key
* @param start
* @param end
* @return 返回子字符串
*/
String substr(String key, int start, int end);

/**
* 设置hash表里field字段的值为value。如果key不存在,则创建一个新的hash表
*
* @param key
* @param field
* @param value
* @return 如果该字段已经存在,那么将会更新该字段的值,返回0.如果字段不存在,则新创建一个并且返回1.
*/
Long hset(String key, String field, String value);

/**
* 如果该key对应的值是一个Hash表,则返回对应字段的值。 如果不存在该字段,或者key不存在,则返回一个"nil"值。
*
* @param key
* @param field
* @return
*/
String hget(String key, String field);

/**
* 当字段不存在时,才进行set。
*
* @param key
* @param field
* @param value
* @return 如果该字段已经存在,则返回0.若字段不存在,则创建后set,返回1.
*/
Long hsetnx(String key, String field, String value);

/**
* 设置多个字段和值,如果字段存在,则覆盖。
*
* @param key
* @param hash
* @return 设置成功返回OK,设置不成功则返回EXCEPTION
*/
String hmset(String key, Map<String, String> hash);

/**
* 在hash中获取多个字段的值,若字段不存在,则其值为nil。
*
* @param key
* @param fields
* @return 按顺序返回多个字段的值。
*/
List<String> hmget(String key, String... fields);

/**
* 对hash中指定字段的值增加指定的值
*
* @param key
* @param field
* @param value
* @return 返回增加后的新值
*/
Long hincrBy(String key, String field, long value);

/**
* 判断hash中指定字段是否存在
*
* @param key
* @param field
* @return 若存在返回1,若不存在返回0
*/
Boolean hexists(String key, String field);

/**
* 删除hash中指定字段
*
* @param key
* @param field
* @return 删除成功返回1, 删除不成功返回0
*/
Long hdel(String key, String... field);

/**
* 返回 key 指定的哈希集包含的字段的数量
*
* @param key
* @return 哈希集中字段的数量,当 key 指定的哈希集不存在时返回 0
*/
Long hlen(String key);

/**
* 返回 key 指定的哈希集中所有字段的名字。
*
* @param key
* @return 哈希集中的字段列表,当 key 指定的哈希集不存在时返回空列表。
*/
Set<String> hkeys(String key);

/**
* 返回 key 指定的哈希集中所有字段的值。
*
* @param key
* @return 哈希集中的值的列表,当 key 指定的哈希集不存在时返回空列表。
*/
List<String> hvals(String key);

/**
* 返回 key 指定的哈希集中所有的字段和值
*
* @param key
* @return 返回 key 指定的哈希集中所有的字段和值,若key不存在返回空map。
*/
Map<String, String> hgetAll(String key);

/**
* 向存于 key 的列表的尾部插入所有指定的值。如果 key 不存在,那么会创建一个空的列表然后再进行 push 操作。 当 key
* 保存的不是一个列表,那么会返回一个错误。
*
* 可以使用一个命令把多个元素打入队列,只需要在命令后面指定多个参数。元素是从左到右一个接一个从列表尾部插入。 比如命令 RPUSH mylist a
* b c 会返回一个列表,其第一个元素是 a ,第二个元素是 b ,第三个元素是 c。
*
* @param key
* @param string
* @return 在 push 操作后的列表长度。
*/
Long rpush(String key, String... string);

/**
* 将所有指定的值插入到存于 key 的列表的头部。如果 key 不存在,那么在进行 push 操作前会创建一个空列表。 如果 key
* 对应的值不是一个 list 的话,那么会返回一个错误。
*
* 可以使用一个命令把多个元素 push 进入列表,只需在命令末尾加上多个指定的参数。元素是从最左端的到最右端的、一个接一个被插入到 list
* 的头部。 所以对于这个命令例子 LPUSH mylist a b c,返回的列表是 c 为第一个元素, b 为第二个元素, a 为第三个元素。
*
* @param key
* @param string
* @return 在 push 操作后的列表长度。
*/
Long lpush(String key, String... string);

/**
* 返回存储在 key 里的list的长度。 如果 key 不存在,那么就被看作是空list,并且返回长度为 0。 当存储在 key
* 里的值不是一个list的话,会返回error。
*
* @param key
* @return key对应的list的长度。
*/
Long llen(String key);

/**
* 返回存储在 key 的列表里指定范围内的元素。 start 和 end
* 偏移量都是基于0的下标,即list的第一个元素下标是0(list的表头),第二个元素下标是1,以此类推。
*
* 偏移量也可以是负数,表示偏移量是从list尾部开始计数。 例如, -1 表示列表的最后一个元素,-2 是倒数第二个,以此类推。
*
* @param key
* @param start
* @param end
* @return 指定范围里的列表元素。
*/
List<String> lrange(String key, long start, long end);

/**
* 修剪(trim)一个已存在的 list,这样 list 就会只包含指定范围的指定元素。start 和 stop 都是由0开始计数的, 这里的 0
* 是列表里的第一个元素(表头),1 是第二个元素,以此类推。
*
* @param key
* @param start
* @param end
* @return
*/
String ltrim(String key, long start, long end);

/**
* 返回列表里的元素的索引 index 存储在 key 里面。 下标是从0开始索引的,所以 0 是表示第一个元素, 1 表示第二个元素,并以此类推。
* 负数索引用于指定从列表尾部开始索引的元素。在这种方法下,-1 表示最后一个元素,-2 表示倒数第二个元素,并以此往前推。
*
* 当 key 位置的值不是一个列表的时候,会返回一个error。
*
* @param key
* @param index
* @return 请求的对应元素,或者当 index 超过范围的时候返回 nil。
*/
String lindex(String key, long index);

/**
* 设置 index 位置的list元素的值为 value。
*
* 当index超出范围时会返回一个error。
*
* @param key
* @param index
* @param value
* @return 状态恢复
*/
String lset(String key, long index, String value);

/**
* 从存于 key 的列表里移除前 count 次出现的值为 value 的元素。 这个 count 参数通过下面几种方式影响这个操作:
*
* count > 0: 从头往尾移除值为 value 的元素。 count < 0: 从尾往头移除值为 value 的元素。 count = 0:
* 移除所有值为 value 的元素。
*
* 比如, LREM list -2 "hello" 会从存于 list 的列表里移除最后两个出现的 "hello"。
*
* 需要注意的是,如果list里没有存在key就会被当作空list处理,所以当 key 不存在的时候,这个命令会返回 0。
*
* @param key
* @param count
* @param value
* @return 返回删除的个数
*/
Long lrem(String key, long count, String value);

/**
* 移除并且返回 key 对应的 list 的第一个元素。
*
* @param key
* @return 返回第一个元素的值,或者当 key 不存在时返回 nil。
*/
String lpop(String key);

/**
* 移除并返回存于 key 的 list 的最后一个元素。
*
* @param key
* @return 最后一个元素的值,或者当 key 不存在的时候返回 nil。
*/
String rpop(String key);

/**
* 添加一个或多个指定的member元素到集合的 key中.指定的一个或者多个元素member 如果已经在集合key中存在则忽略.如果集合key
* 不存在,则新建集合key,并添加member元素到集合key中.
*
* 如果key 的类型不是集合则返回错误.
*
* @param key
* @param member
* @return 返回新成功添加到集合里元素的数量,不包括已经存在于集合中的元素.
*/
Long sadd(String key, String... member);

/**
* 返回key集合所有的元素.
*
* 该命令的作用与使用一个参数的SINTER 命令作用相同.
*
* @param key
* @return 集合中的所有元素.
*/
Set<String> smembers(String key);

/**
* 在key集合中移除指定的元素. 如果指定的元素不是key集合中的元素则忽略 如果key集合不存在则被视为一个空的集合,该命令返回0.
*
* 如果key的类型不是一个集合,则返回错误.
*
* @param key
* @param member
* @return 从集合中移除元素的个数,不包括不存在的成员.
*/
Long srem(String key, String... member);

/**
* 移除并返回一个集合中的随机元素
*
* 该命令与 SRANDMEMBER相似,不同的是srandmember命令返回一个随机元素但是不移除.
*
* @param key
* @return 被移除的元素, 当key不存在的时候返回 nil .
*/
String spop(String key);

/**
* 移除并返回多个集合中的随机元素
*
* @param key
* @param count
* @return 被移除的元素, 当key不存在的时候值为 nil .
*/
Set<String> spop(String key, long count);

/**
* 返回集合存储的key的基数 (集合元素的数量).
*
* @param key
* @return 集合的基数(元素的数量),如果key不存在,则返回 0.
*/
Long scard(String key);

/**
* 返回成员 member 是否是存储的集合 key的成员.
*
* @param key
* @param member
* @return 如果member元素是集合key的成员,则返回1.如果member元素不是key的成员,或者集合key不存在,则返回0
*/
Boolean sismember(String key, String member);

/**
* 仅提供key参数,那么随机返回key集合中的一个元素.该命令作用类似于SPOP命令, 不同的是SPOP命令会将被选择的随机元素从集合中移除,
* 而SRANDMEMBER仅仅是返回该随记元素,而不做任何操作.
*
* @param key
* @return 返回随机的元素,如果key不存在则返回nil
*/
String srandmember(String key);

/**
* 如果count是整数且小于元素的个数,返回含有 count
* 个不同的元素的数组,如果count是个整数且大于集合中元素的个数时,仅返回整个集合的所有元素
* ,当count是负数,则会返回一个包含count的绝对值的个数元素的数组
* ,如果count的绝对值大于元素的个数,则返回的结果集里会出现一个元素出现多次的情况.
*
* @param key
* @param count
* @return 返回一个随机的元素数组,如果key不存在则返回一个空的数组.
*/
List<String> srandmember(String key, int count);

/**
* 返回key的string类型value的长度。如果key对应的非string类型,就返回错误。
*
* @param key
* @return key对应的字符串value的长度,或者0(key不存在)
*/
Long strlen(String key);

/**
* 该命令添加指定的成员到key对应的有序集合中,每个成员都有一个分数。你可以指定多个分数/成员组合。如果一个指定的成员已经在对应的有序集合中了,
* 那么其分数就会被更新成最新的
* ,并且该成员会重新调整到正确的位置,以确保集合有序。如果key不存在,就会创建一个含有这些成员的有序集合,就好像往一个空的集合中添加一样
* 。如果key存在,但是它并不是一个有序集合,那么就返回一个错误。
*
* 分数的值必须是一个表示数字的字符串,并且可以是double类型的浮点数。
*
* @param key
* @param score
* @param member
* @return 返回添加到有序集合中元素的个数,不包括那种已经存在只是更新分数的元素。
*/
Long zadd(String key, double score, String member);

/**
* 该命令添加指定的成员到key对应的有序集合中,每个成员都有一个分数。你可以指定多个分数/成员组合。如果一个指定的成员已经在对应的有序集合中了,
* 那么其分数就会被更新成最新的
* ,并且该成员会重新调整到正确的位置,以确保集合有序。如果key不存在,就会创建一个含有这些成员的有序集合,就好像往一个空的集合中添加一样
* 。如果key存在,但是它并不是一个有序集合,那么就返回一个错误。
*
* 分数的值必须是一个表示数字的字符串,并且可以是double类型的浮点数。
*
* @param key
* @param scoreMembers
* @return 返回添加到有序集合中元素的个数,不包括那种已经存在只是更新分数的元素。
*/
Long zadd(String key, Map<String, Double> scoreMembers);

/**
* 返回有序集key中,指定区间内的成员。其中成员按score值递增(从小到大)来排序。具有相同score值的成员按字典序来排列。
*
* 如果你需要成员按score值递减(score相等时按字典序递减)来排列,请使用ZREVRANGE命令。
* 下标参数start和stop都以0为底,也就是说,以0表示有序集第一个成员,以1表示有序集第二个成员,以此类推。
* 你也可以使用负数下标,以-1表示最后一个成员,-2表示倒数第二个成员,以此类推。
*
* 超出范围的下标并不会引起错误。如果start的值比有序集的最大下标还要大,或是start >
* stop时,ZRANGE命令只是简单地返回一个空列表。
* 另一方面,假如stop参数的值比有序集的最大下标还要大,那么Redis将stop当作最大下标来处理。
*
* @param key
* @param start
* @param end
* @return 指定范围的元素列表
*/
Set<String> zrange(String key, long start, long end);

/**
* 从集合中删除指定member元素,当key存在,但是其不是有序集合类型,就返回一个错误。
*
* @param key
* @param member
* @return 返回的是从有序集合中删除的成员个数,不包括不存在的成员。
*/
Long zrem(String key, String... member);

/**
* 为有序集key的成员member的score值加上增量increment。如果key中不存在member,就在key中添加一个member,
* score是increment(就好像它之前的score是0.0)。如果key不存在,就创建一个只含有指定member成员的有序集合。
*
* 当key不是有序集类型时,返回一个错误。
*
* score值必须整数值或双精度浮点数。也有可能给一个负数来减少score的值。
*
* @param key
* @param score
* @param member
* @return member成员的新score值.
*/
Double zincrby(String key, double score, String member);

/**
* 返回有序集key中成员member的排名。其中有序集成员按score值递增(从小到大)顺序排列。排名以0为底,也就是说,
* score值最小的成员排名为0。
*
* 使用ZREVRANK命令可以获得成员按score值递减(从大到小)排列的排名。
*
* @param key
* @param member
* @return 如果member是有序集key的成员,返回member的排名的整数。 如果member不是有序集key的成员,返回 nil。
*/
Long zrank(String key, String member);

/**
* 返回有序集key中成员member的排名,其中有序集成员按score值从大到小排列。排名以0为底,也就是说,score值最大的成员排名为0。
*
* 使用ZRANK命令可以获得成员按score值递增(从小到大)排列的排名。
*
* @param key
* @param member
* @return 如果member是有序集key的成员,返回member的排名。整型数字。 如果member不是有序集key的成员,返回Bulk
* reply: nil.
*/
Long zrevrank(String key, String member);

/**
* 返回有序集key中,指定区间内的成员。其中成员的位置按score值递减(从大到小)来排列。具有相同score值的成员按字典序的反序排列。
* 除了成员按score值递减的次序排列这一点外,ZREVRANGE命令的其他方面和ZRANGE命令一样。
*
* @param key
* @param start
* @param end
* @return 指定范围的元素列表(可选是否含有分数)。
*/
Set<String> zrevrange(String key, long start, long end);

/**
* 返回有序集key中,指定区间内的成员。其中成员按score值递增(从小到大)来排序。具有相同score值的成员按字典序来排列。
*
* 如果你需要成员按score值递减(score相等时按字典序递减)来排列,请使用ZREVRANGE命令。
* 下标参数start和stop都以0为底,也就是说,以0表示有序集第一个成员,以1表示有序集第二个成员,以此类推。
* 你也可以使用负数下标,以-1表示最后一个成员,-2表示倒数第二个成员,以此类推。
*
* 超出范围的下标并不会引起错误。如果start的值比有序集的最大下标还要大,或是start >
* stop时,ZRANGE命令只是简单地返回一个空列表。
* 另一方面,假如stop参数的值比有序集的最大下标还要大,那么Redis将stop当作最大下标来处理。
*
* 使用WITHSCORES选项,来让成员和它的score值一并返回,返回列表以value1,score1, ...,
* valueN,scoreN的格式表示,而不是value1,...,valueN。客户端库可能会返回一些更复杂的数据类型,比如数组、元组等。
*
* @param key
* @param start
* @param end
* @return 指定范围的元素列表(以元组集合的形式)。
*/
Set<Tuple> zrangeWithScores(String key, long start, long end);

/**
* 返回有序集key中,指定区间内的成员。其中成员的位置按score值递减(从大到小)来排列。具有相同score值的成员按字典序的反序排列。
* 除了成员按score值递减的次序排列这一点外,ZREVRANGE命令的其他方面和ZRANGE命令一样。
*
* @param key
* @param start
* @param end
* @return 指定范围的元素列表(可选是否含有分数)。
*/
Set<Tuple> zrevrangeWithScores(String key, long start, long end);

/**
* 返回key的有序集元素个数。
*
* @param key
* @return key存在的时候,返回有序集的元素个数,否则返回0。
*/
Long zcard(String key);

/**
* 返回有序集key中,成员member的score值。
*
* 如果member元素不是有序集key的成员,或key不存在,返回nil。
*
* @param key
* @param member
* @return member成员的score值(double型浮点数)
*/
Double zscore(String key, String member);

/**
* 对一个集合或者一个列表排序
*
* 对集合,有序集合,或者列表的value进行排序。默认情况下排序只对数字排序,双精度浮点数。
*
* @see #sort(String, String)
* @see #sort(String, SortingParams)
* @see #sort(String, SortingParams, String)
* @param key
* @return 假设集合或列表包含的是数字元素,那么返回的将会是从小到大排列的一个列表。
*/
List<String> sort(String key);

/**
* 根据指定参数来对列表或集合进行排序.
* <p>
* <b>examples:</b>
* <p>
* 一下是一些例子列表或者key-value:
*
* <pre>
* x = [1, 2, 3]
* y = [a, b, c]
*
* k1 = z
* k2 = y
* k3 = x
*
* w1 = 9
* w2 = 8
* w3 = 7
* </pre>
*
* 排序:
*
* <pre>
* sort(x) or sort(x, sp.asc())
* -> [1, 2, 3]
*
* sort(x, sp.desc())
* -> [3, 2, 1]
*
* sort(y)
* -> [c, a, b]
*
* sort(y, sp.alpha())
* -> [a, b, c]
*
* sort(y, sp.alpha().desc())
* -> [c, b, a]
* </pre>
*
* Limit (e.g. for Pagination):
*
* <pre>
* sort(x, sp.limit(0, 2))
* -> [1, 2]
*
* sort(y, sp.alpha().desc().limit(1, 2))
* -> [b, a]
* </pre>
*
* 使用外部键来排序:
*
* <pre>
* sort(x, sb.by(w*))
* -> [3, 2, 1]
*
* sort(x, sb.by(w*).desc())
* -> [1, 2, 3]
* </pre>
*
* Getting external keys:
*
* <pre>
* sort(x, sp.by(w*).get(k*))
* -> [x, y, z]
*
* sort(x, sp.by(w*).get(#).get(k*))
* -> [3, x, 2, y, 1, z]
* </pre>
*
* @see #sort(String)
* @see #sort(String, SortingParams, String)
* @param key
* @param sortingParameters
* @return a list of sorted elements.
*/
List<String> sort(String key, SortingParams sortingParameters);

/**
* 返回有序集key中,score值在min和max之间(默认包括score值等于min或max)的成员。
*
* @param key
* @param min
* @param max
* @return 指定分数范围的元素个数。
*/
Long zcount(String key, double min, double max);

/**
* 返回有序集key中,score值在min和max之间(默认包括score值等于min或max)的成员。
*
* @param key
* @param min
* @param max
* @return 指定分数范围的元素个数。
*/
Long zcount(String key, String min, String max);

/**
* 返回key的有序集合中的分数在min和max之间的所有元素(包括分数等于max或者min的元素)。元素被认为是从低分到高分排序的。
* 具有相同分数的元素按字典序排列
*
* @param key
* @param min
* @param max
* @return 指定分数范围的元素列表
*/
Set<String> zrangeByScore(String key, double min, double max);

/**
* 返回key的有序集合中的分数在min和max之间的所有元素(包括分数等于max或者min的元素)。元素被认为是从低分到高分排序的。
* 具有相同分数的元素按字典序排列
*
* @param key
* @param min
* @param max
* @return 指定分数范围的元素列表
*/
Set<String> zrangeByScore(String key, String min, String max);

/**
* 返回key的有序集合中的分数在min和max之间的所有元素(包括分数等于max或者min的元素)。元素被认为是从低分到高分排序的。
* 具有相同分数的元素按字典序排列, 指定返回结果的数量及区间。
*
* @param key
* @param min
* @param max
* @param offset
* @param count
* @return 指定分数范围的元素列表
*/
Set<String> zrangeByScore(String key, double min, double max, int offset, int count);

/**
* 返回key的有序集合中的分数在min和max之间的所有元素(包括分数等于max或者min的元素)。元素被认为是从低分到高分排序的。
* 具有相同分数的元素按字典序排列, 指定返回结果的数量及区间。
*
* @param key
* @param min
* @param max
* @param offset
* @param count
* @return 指定分数范围的元素列表
*/
Set<String> zrangeByScore(String key, String min, String max, int offset, int count);

/**
* 返回key的有序集合中的分数在min和max之间的所有元素(包括分数等于max或者min的元素)。元素被认为是从低分到高分排序的。
* 具有相同分数的元素按字典序排列。返回元素和其分数,而不只是元素。
*
* @param key
* @param min
* @param max
* @return
*/
Set<Tuple> zrangeByScoreWithScores(String key, double min, double max);

/**
* 返回key的有序集合中的分数在min和max之间的所有元素(包括分数等于max或者min的元素)。元素被认为是从低分到高分排序的。
* 具有相同分数的元素按字典序排列, 指定返回结果的数量及区间。 返回元素和其分数,而不只是元素。
*
* @param key
* @param min
* @param max
* @param offset
* @param count
* @return
*/
Set<Tuple> zrangeByScoreWithScores(String key, double min, double max, int offset,
int count);

/**
* 返回key的有序集合中的分数在min和max之间的所有元素(包括分数等于max或者min的元素)。元素被认为是从低分到高分排序的。
* 具有相同分数的元素按字典序排列。返回元素和其分数,而不只是元素。
*
* @param key
* @param min
* @param max
* @return
*/
Set<Tuple> zrangeByScoreWithScores(String key, String min, String max);

/**
* 返回key的有序集合中的分数在min和max之间的所有元素(包括分数等于max或者min的元素)。元素被认为是从低分到高分排序的。
* 具有相同分数的元素按字典序排列, 指定返回结果的数量及区间。 返回元素和其分数,而不只是元素。
*
* @param key
* @param min
* @param max
* @param offset
* @param count
* @return
*/
Set<Tuple> zrangeByScoreWithScores(String key, String min, String max, int offset,
int count);

/**
* 机制与zrangeByScore一样,只是返回结果为降序排序。
*
* @param key
* @param max
* @param min
* @return
*/
Set<String> zrevrangeByScore(String key, double max, double min);

/**
* 机制与zrangeByScore一样,只是返回结果为降序排序。
*
* @param key
* @param max
* @param min
* @return
*/
Set<String> zrevrangeByScore(String key, String max, String min);

/**
* 机制与zrangeByScore一样,只是返回结果为降序排序。
*
* @param key
* @param max
* @param min
* @param offset
* @param count
* @return
*/
Set<String> zrevrangeByScore(String key, double max, double min, int offset, int count);

/**
* 机制与zrangeByScoreWithScores一样,只是返回结果为降序排序。
*
* @param key
* @param max
* @param min
* @return
*/
Set<Tuple> zrevrangeByScoreWithScores(String key, double max, double min);

/**
* 机制与zrangeByScore一样,只是返回结果为降序排序。
*
* @param key
* @param max
* @param min
* @param offset
* @param count
* @return
*/
Set<String> zrevrangeByScore(String key, String max, String min, int offset, int count);

/**
* 机制与zrangeByScoreWithScores一样,只是返回结果为降序排序。
*
* @param key
* @param max
* @param min
* @return
*/
Set<Tuple> zrevrangeByScoreWithScores(String key, String max, String min);

/**
* 机制与zrangeByScoreWithScores一样,只是返回结果为降序排序。
*
* @param key
* @param max
* @param min
* @param offset
* @param count
* @return
*/
Set<Tuple> zrevrangeByScoreWithScores(String key, double max, double min, int offset,
int count);

/**
* 机制与zrangeByScoreWithScores一样,只是返回结果为降序排序。
*
* @param key
* @param max
* @param min
* @param offset
* @param count
* @return
*/
Set<Tuple> zrevrangeByScoreWithScores(String key, String max, String min, int offset,
int count);

/**
* 移除有序集key中,指定排名(rank)区间内的所有成员。下标参数start和stop都以0为底,0处是分数最小的那个元素。这些索引也可是负数,
* 表示位移从最高分处开始数。例如,-1是分数最高的元素,-2是分数第二高的,依次类推。
*
* @param key
* @param start
* @param end
* @return 被移除成员的数量。
*/
Long zremrangeByRank(String key, long start, long end);

/**
* 移除有序集key中,所有score值介于min和max之间(包括等于min或max)的成员。
*
* 自版本2.1.6开始,score值等于min或max的成员也可以不包括在内,语法请参见ZRANGEBYSCORE命令。
*
* @param key
* @param start
* @param end
* @return 删除的元素的个数
*/
Long zremrangeByScore(String key, double start, double end);

/**
* 移除有序集key中,所有score值介于min和max之间(包括等于min或max)的成员。
*
* 自版本2.1.6开始,score值等于min或max的成员也可以不包括在内,语法请参见ZRANGEBYSCORE命令。
*
* @param key
* @param start
* @param end
* @return 删除的元素的个数
*/
Long zremrangeByScore(String key, String start, String end);

/**
* 当插入到有序集合中的元素都具有相同的分数时,这个命令可以返回min和max指定范围内的元素的数量。
*
* @param key
* @param min
* @param max
* @return
*/
Long zlexcount(final String key, final String min, final String max);

/**
* 把 value 插入存于 key 的列表中在基准值 pivot 的前面或后面。
*
* 当 key 不存在时,这个list会被看作是空list,任何操作都不会发生。
*
* 当 key 存在,但保存的不是一个list的时候,会返回error。
*
* @param key
* @param where
* @param pivot 前或后
* @param value
* @return 在 insert 操作后的 list 长度。
*/
Long linsert(String key, Client.LIST_POSITION where, String pivot, String value);

/**
* 只有当 key 已经存在并且存着一个 list 的时候,在这个 key 下面的 list 的头部插入 value。 与 LPUSH 相反,当
* key 不存在的时候不会进行任何操作。
*
* @param key
* @param string
* @return 在 push 操作后的 list 长度。
*/
Long lpushx(String key, String... string);

/**
* 将值 value 插入到列表 key 的表尾, 当且仅当 key 存在并且是一个列表。 和 RPUSH 命令相反, 当 key
* 不存在时,RPUSHX 命令什么也不做。
*
* @param key
* @param string
* @return 在Push操作后List的长度
*/
Long rpushx(String key, String... string);

/**
* @deprecated unusable command, this will be removed in 3.0.0.
*/
@Deprecated
List<String> blpop(String arg);

/**
* BLPOP 是阻塞式列表的弹出原语。 它是命令 LPOP 的阻塞版本,这是因为当给定列表内没有任何元素可供弹出的时候, 连接将被 BLPOP
* 命令阻塞。 当给定多个 key 参数时,按参数 key 的先后顺序依次检查各个列表,弹出第一个非空列表的头元素。 {@link http
* ://www.redis.cn/commands/blpop.html}
*
* @param timeout
* @param key
* @return
*/
List<String> blpop(int timeout, String key);

/**
* @deprecated unusable command, this will be removed in 3.0.0.
*/
@Deprecated
List<String> brpop(String arg);

/**
* BRPOP 是一个阻塞的列表弹出原语。 它是 RPOP 的阻塞版本,因为这个命令会在给定list无法弹出任何元素的时候阻塞连接。
* 该命令会按照给出的 key 顺序查看 list,并在找到的第一个非空 list 的尾部弹出一个元素。
*
* 请在 BLPOP 文档 中查看该命令的准确语义,因为 BRPOP 和 BLPOP
* 基本是完全一样的,除了它们一个是从尾部弹出元素,而另一个是从头部弹出元素。 {@link http
* ://www.redis.cn/commands/brpop.html}
*
*
* @param timeout
* @param key
* @return
*/
List<String> brpop(int timeout, String key);

/**
* 删除一个Key,如果删除的key不存在,则直接忽略。
*
* @param key
* @return 被删除的keys的数量
*/
Long del(String key);

/**
* 回显
*
* @param string
* @return 回显输入的字符串
*/
String echo(String string);

/**
* 将当前数据库的 key 移动到给定的数据库 db 当中。
*
* 如果当前数据库(源数据库)和给定数据库(目标数据库)有相同名字的给定 key ,或者 key 不存在于当前数据库,那么 MOVE 没有任何效果。
*
* 因此,也可以利用这一特性,将 MOVE 当作锁(locking)原语(primitive)。
*
* @param key
* @param dbIndex
* @return 移动成功返回 1 失败则返回 0
*/
Long move(String key, int dbIndex);

/**
* 统计字符串的字节数
*
* @param key
* @return 字节数
*/
Long bitcount(final String key);

/**
* 统计字符串指定起始位置的字节数
*
* @param key
* @param start
* @param end
* @return
*/
Long bitcount(final String key, long start, long end);

/**
* 迭代hash里面的元素
*
* @param key
* @param cursor
* @return
*/
ScanResult<Map.Entry<String, String>> hscan(final String key, final String cursor);

/**
* 迭代set里面的元素
*
* @param key
* @param cursor
* @return
*/
ScanResult<String> sscan(final String key, final String cursor);

/**
* 迭代zset里面的元素
*
* @param key
* @param cursor
* @return
*/
ScanResult<Tuple> zscan(final String key, final String cursor);

}

Spring-Security-Oauth2第二篇之配置客户端

@[toc]
第一篇中,描述的都是授权服务器和资源服务器。本篇要描述的是关于如何配置客户端的示例。

首先,需要考虑在OAuth2中有四种不同的角色:

  • 资源所有者 - 能够授予对其受保护资源的访问权限的实体
  • 授权服务器 -在成功验证资源所有者 并获得其授权后, 向客户端授予访问令牌
  • 资源服务器 - 需要访问令牌以允许或至少考虑访问其资源的组件
  • 客户端 - 能够从授权服务器获取访问令牌的实体

使用 @EnableResourceServer 表示资源服务器

使用 @EnableOAuth2Sso 表示授权码类型的客户端

使用 @EnableOAuth2Client 表示客户端凭据类型的客户端

1. 项目结构

实在抱歉,之前的关于客户端的项目结构图片贴错了(是第一篇的项目结构图),下面换上正确的图片
在这里插入图片描述

2. maven 依赖

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
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.5.19.RELEASE</version>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security.oauth</groupId>
<artifactId>spring-security-oauth2</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<dependency>
<groupId>org.thymeleaf.extras</groupId>
<artifactId>thymeleaf-extras-springsecurity4</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
</dependencies>

3. 使用 @EnableOAuth2Sso 注解安全配置

3.1. 客户端安全配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/**
* 配置最核心的部分是用于启动单点登陆 @EnableOAuth2Sso 注解。
* 这里要注意,我们需要重写WebSecurityConfigurerAdapter 否则所有的路径都会受到SSO的保护,
* 这样无论用户访问哪个页面都会被重定向到登录页面,在这个例子里,index和login页面是唯一不需要被防护的。
*
* @author fengxuechao
* @date 2019/3/27
*/
@EnableOAuth2Sso
@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.antMatcher("/**")
.authorizeRequests()
.antMatchers("/", "/login**")
.permitAll()
.anyRequest()
.authenticated();
http.csrf().disable();
}
}

同时必须在授权服务器中的授权类型中添加授权码类型,同时添加回调链接(核心代码见 3.2. 授权服务器核心代码)。

3.2. 授权服务器核心代码

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
@Configuration
@EnableAuthorizationServer
public class AuthServerConfig extends AuthorizationServerConfigurerAdapter {
/**
* 第三方用户客户端详情
* Grant Type代表当前授权的类型:
* <p>
* authorization_code:传统的授权码模式<br>
* implicit:隐式授权模式<br>
* password:资源所有者(即用户)密码模式<br>
* client_credentials:客户端凭据(客户端ID以及Key)模式<br>
* refresh_token:获取access token时附带的用于刷新新的token模式
* </p>
*
* @param clients
* @throws Exception
*/
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.jdbc(dataSource)
.withClient("client_1")
.secret("123456")
.resourceIds(DEMO_RESOURCE_ID)
.redirectUris("https://www.baidu.com", "http://localhost:8081/product/1", "http://localhost:8083/login")
.accessTokenValiditySeconds(1200)
.refreshTokenValiditySeconds(50000)
.authorizedGrantTypes("client_credentials", "refresh_token", "password", "authorization_code")
.scopes("all")
.authorities("client")
.autoApprove(true)
.and().build();
}
}

redirectUris() 中的链接表示回调接口,其中 http://localhost:8083/login 是本次需要添加的

authorizedGrantTypes() 表示授权服务器支持的授权类型,本次添加了 authorization_code

autoApprove(true) 表示自动授权

3.3. 配置文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
server.port=8083
server.session.cookie.name=CLIENTSESSION
security.oauth2.client.client-id=client_1
security.oauth2.client.client-secret=123456
security.oauth2.client.access-token-uri=http://localhost:8081/oauth/token
security.oauth2.client.user-authorization-uri=http://localhost:8081/oauth/authorize
security.oauth2.client.scope=all
# userInfoUri用户端点的URI,用于获取当前用户详细信息
security.oauth2.resource.user-info-uri=http://localhost:8081/user/me
# 解析令牌的地址
security.oauth2.authorization.check-token-access=http://localhost:8001/oauth/check_token

security.basic.enabled=false

spring.thymeleaf.cache=false

注意:在配置文件中要注意 server.session.cookie.name 的配置,
因为 cookie 不会保存端口,所以要注意客户端的 cookie 名和授权服务器的 cookie 名的不同。

4. MVC 配置

4.1. 客户端 API

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
/**
* @author fengxuechao
* @date 2019/3/27
*/
@RestController
@SpringBootApplication
public class ClientApp {
public static void main(String[] args) {
SpringApplication.run(ClientApp.class);
}

@Autowired
OAuth2RestTemplate restTemplate;

@GetMapping("/securedPage")
public ModelAndView securedPage(OAuth2Authentication authentication) {
return new ModelAndView("securedPage").addObject("authentication", authentication);
}

@GetMapping("/remoteCall")
public Map remoteCall() {
ResponseEntity<Map> responseEntity = restTemplate.getForEntity("http://127.0.0.1:8082/api/userinfo", Map.class);
return responseEntity.getBody();
}

@Bean
public OAuth2RestTemplate oauth2RestTemplate(
OAuth2ClientContext oAuth2ClientContext, OAuth2ProtectedResourceDetails details) {
return new OAuth2RestTemplate(details, oAuth2ClientContext);
}
}

4.2. 客户端 MVC 映射

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/**
* @author fengxuechao
* @date 2019/3/27
*/
@Configuration
@EnableWebMvc
public class WebMvcConfig extends WebMvcConfigurerAdapter {

@Bean
public RequestContextListener requestContextListener() {
return new RequestContextListener();
}

@Override
public void addViewControllers(ViewControllerRegistry registry) {
registry.addViewController("/").setViewName("forward:/index");
registry.addViewController("/index");
}

}

4.3. 前端

index.html

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<!DOCTYPE html>
<html lang="en">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
<title>Spring Security SSO Client</title>
<link rel="stylesheet"
href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.2/css/bootstrap.min.css"/>
</head>

<body>
<div class="container">
<div class="col-sm-12">
<h1>Spring Security SSO Client</h1>
<a class="btn btn-primary" href="securedPage">Login</a>
</div>
</div>
</body>
</html>

securedPage.html

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
<title>Spring Security SSO Client</title>
<link rel="stylesheet"
href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.2/css/bootstrap.min.css"/>
</head>

<body>
<div class="container">
<div class="col-sm-12">
<h1>Secured Page</h1>
Welcome, <span th:text="${authentication.name}">Name</span>
<br/>
Your authorities are <span th:text="${authentication.authorities}">authorities</span>
</div>
</div>
</body>
</html>

5. 测试

启动授权服务器,资源服务器和客户端,进入客户端首页:

在这里插入图片描述

点击登陆,重定向到授权服务器的登陆页面,输入授权服务器信任的用户名(user_1)和密码(123456):

在这里插入图片描述

点击登陆,重定向到安全页面:

在这里插入图片描述

调用资源服务器资源:

在这里插入图片描述

6. 使用 @EnableOAuth2Client 注解安全配置

如果使用客户端凭据模式就足够的话,那么对上文中的代码只需很少的配置即可完成客户端凭据模式的客户端。

6.1. 客户端安全配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/**
* 使用 @EnableOAuth2Client 注解来开启 client_credentials。
* 这里要注意的是要明确在配置文件中配置 security.oauth2.client.grant-type=client_credentials 。
* 同时允许要调用的接口,注意对比与 WebSecurityConfig 类的不同点。
*
* @author fengxuechao
* @date 2019/3/27
*/
@EnableOAuth2Client
@Configuration
public class WebSecurityConfig2 extends WebSecurityConfigurerAdapter {

@Override
protected void configure(HttpSecurity http) throws Exception {
http.antMatcher("/**")
.authorizeRequests()
.antMatchers("/", "/login**", "/remoteCall")
.permitAll()
.anyRequest()
.authenticated();
http.csrf().disable();
}
}

注意,因为客户端凭据模式信任持有客户端凭证的客户端发出的任何请求,将远程调用资源服务器的请求 “/remoteCall” 允许访问。

配置文件 application.properties 中添加 security.oauth2.client.grant-type=client_credentials

6.2. 测试

在这里插入图片描述

7. 参考资源

Simple Single Sign-On with Spring Security OAuth2

https://spring.io/guides/tutorials/spring-boot-oauth2/

Spring-Security-Oauth2第一篇

@[toc]

1. Oauth 介绍

OAuth 是一个关于授权(authorization)的开放网络标准,在全世界得到广泛应用,目前的版本是2.0版。

OAuth 是一个开放标准,允许用户让第三方应用访问该用户在某一网站上存储的私密的资源(如照片,视频,联系人列表),
而不需要将用户名和密码提供给第三方应用。OAuth允许用户提供一个令牌,而不是用户名和密码来访问他们存放在特定服务提供者的数据。
每一个令牌授权一个特定的网站在特定的时段内访问特定的资源。这样,OAuth让用户可以授权第三方网站访问他们存储在另外服务提供者的某些特定信息。
更多OAuth2请参考理解OAuth 2.0

2. Spring Security Oauth2 的使用

2.1. 使用MySQL存储 access_token 和 client 信息

在学习过程中,很多示例中,所有的token信息都是保存在内存中的,这显然无法在生产环境中使用(进程结束后所有token丢失, 用户需要重新授权),
也不利于我们的学习,因此需要将这些信息进行持久化操作。

授权服务器中的数据存储到数据库中并不难 spring-security-oauth2 已经为我们设计好了一套Schema和对应的DAO对象。
但在使用之前,我们需要先对相关的类有一定的了解。

2.2. 数据结构脚本

spring-security-oauth2 为我们提供了 Schema:

https://github.com/spring-projects/spring-security-oauth/blob/master/spring-security-oauth2/src/test/resources/schema.sql

注意: 框架所提供的数据脚本适用于 HSQL,语句中会有某些字段为 LONGVARBINARY 类型,它对应 MYSQL 的 BLOB 类型。

2.3. 相关的接口

spring-security-oauth2 通过 DefaultTokenServices 类来完成 token 生成、过期等 OAuth2 标准规定的业务逻辑,
DefaultTokenServices 又是通过 TokenStore 接口完成对生成数据的持久化。

对于 Token 信息,本篇文章使用 JdbcTokenStore,在生产环境中更喜爱使用 RedisTokenStore

对于 Client 信息,本篇文章使用 JdbcClientDetailsService

2.4. 服务类型

OAuth2 在服务提供者上可分为两类:

  • 授权认证服务:AuthenticationServer

    1
    2
    3
    @Configuration
    @EnableAuthorizationServer
    public class AuthServerConfig extends AuthorizationServerConfigurerAdapter {}
  • 资源获取服务:ResourceServer

    1
    2
    3
    @Configuration
    @EnableResourceServer
    public class ResourceServerConfiguration extends ResourceServerConfigurerAdapter {}

注意:这两者有时候可能存在同一个应用程序中(即SOA架构)。在Spring OAuth中可以简便的将其分配到两个应用中(即微服务),而且可多个资源获取服务共享一个授权认证服务。

2.5. 项目结构和 maven 依赖

前面浅尝辄止的讲述了一些原理,下面的内容是示例展示。

在这里插入图片描述

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
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<!-- oauth2 核心依赖 -->
<dependency>
<groupId>org.springframework.security.oauth</groupId>
<artifactId>spring-security-oauth2</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- 将token存储在redis中 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.1.10</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
</dependencies>

2.5.1. 配置授权认证服务

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
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
package com.fengxuechao.examples.sso.server.configuration;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.oauth2.config.annotation.configurers.ClientDetailsServiceConfigurer;
import org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerConfigurerAdapter;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableAuthorizationServer;
import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerEndpointsConfigurer;
import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerSecurityConfigurer;
import org.springframework.security.oauth2.provider.client.JdbcClientDetailsService;
import org.springframework.security.oauth2.provider.token.TokenStore;
import org.springframework.security.oauth2.provider.token.store.JdbcTokenStore;

import javax.sql.DataSource;

/**
* @author fengxuechao
* @date 2019/3/26
*/
@Configuration
@EnableAuthorizationServer
public class AuthServerConfig extends AuthorizationServerConfigurerAdapter {

private static final String DEMO_RESOURCE_ID = "*";

@Autowired
AuthenticationManager authenticationManager;

@Autowired
private DataSource dataSource;

/**
* 声明TokenStore实现
*
* @return
*/
@Bean
public TokenStore tokenStore() {
return new JdbcTokenStore(dataSource);
}

/**
* 声明 ClientDetails实现
*
* @return
*/
@Bean
public JdbcClientDetailsService clientDetailsService() {
return new JdbcClientDetailsService(dataSource);
}

/**
* 第三方用户客户端详情
* Grant Type代表当前授权的类型:
* <p>
* authorization_code:传统的授权码模式<br>
* implicit:隐式授权模式<br>
* password:资源所有者(即用户)密码模式<br>
* client_credentials:客户端凭据(客户端ID以及Key)模式<br>
* refresh_token:获取access token时附带的用于刷新新的token模式
* </p>
*
* @param clients
* @throws Exception
*/
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.jdbc(dataSource)
.withClient("client_1")
.secret("123456")
.resourceIds(DEMO_RESOURCE_ID)
.redirectUris("https://www.baidu.com", "http://localhost:8081/product/1")
.accessTokenValiditySeconds(1200)
.refreshTokenValiditySeconds(50000)
.authorizedGrantTypes("client_credentials", "refresh_token", "password", "authorization_code")
.scopes("all")
.authorities("client").and().build();
}

@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
// redis保存token
// endpoints.tokenStore(new RedisTokenStore(redisConnectionFactory))
// JDBC 保存 token
endpoints.tokenStore(new JdbcTokenStore(dataSource));
endpoints.setClientDetailsService(clientDetailsService());
endpoints.authenticationManager(authenticationManager);

}

@Override
public void configure(AuthorizationServerSecurityConfigurer oauthServer) throws Exception {
// 允许表单认证
oauthServer.allowFormAuthenticationForClients();
// 授权认证服务需要把 /oauth/check_toke 暴露出来,并且附带上权限访问。
oauthServer.checkTokenAccess("isAuthenticated()");
}
}

2.5.2. 配置用户权限|拦截保护的请求

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
package com.fengxuechao.examples.sso.server.configuration;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;

/**
* @author fengxuechao
* @date 2019/3/26
*/
@Order(2)
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

/**
* 具体的用户权限控制实现类
*
* @return
*/
@Bean
@Override
protected UserDetailsService userDetailsService() {
InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();
manager.createUser(User.withUsername("user_1").password("123456").authorities("USER").build());
manager.createUser(User.withUsername("user_2").password("123456").authorities("USER").build());
return manager;
}

/**
* 用来配置拦截保护的请求
*
* @param http
* @throws Exception
*/
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.csrf().disable()
.requestMatchers().antMatchers("/oauth/**", "/login/**", "/logout/**")
.and().authorizeRequests().antMatchers("/oauth/*").authenticated()
.and().formLogin().permitAll();
}
}

2.5.3. 配置资源获取服务

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
package com.fengxuechao.examples.sso.server.configuration;

import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer;
import org.springframework.security.oauth2.config.annotation.web.configuration.ResourceServerConfigurerAdapter;
import org.springframework.security.oauth2.config.annotation.web.configurers.ResourceServerSecurityConfigurer;

@Order(6)
@Configuration
@EnableResourceServer
public class ResourceServerConfiguration extends ResourceServerConfigurerAdapter {

private static final String DEMO_RESOURCE_ID = "*";

@Override
public void configure(ResourceServerSecurityConfigurer resources) {
resources.resourceId(DEMO_RESOURCE_ID).stateless(true);
}

@Override
public void configure(HttpSecurity http) throws Exception {
http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED)
.and().requestMatchers().anyRequest()
.and().anonymous()
.and().authorizeRequests()
// .antMatchers("/product/**").access("#oauth2.hasScope('select') and hasRole('ROLE_USER')")
.antMatchers("/**").authenticated(); //配置访问权限控制,必须认证过后才可以访问
}
}

注意:ResourceServerConfiguration 和 SecurityConfiguration上配置的顺序
SecurityConfiguration 一定要在 ResourceServerConfiguration 之前,因为 spring 实现安全是通过添加过滤器(Filter)来实现的,
基本的安全过滤应该在oauth过滤之前, 所以在 SecurityConfiguration 设置 @Order(2) , 在 ResourceServerConfiguration 上设置 @Order(6)

2.5.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
package com.fengxuechao.examples.sso.server.web;

import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;

import java.security.Principal;

/**
* @author fengxuechao
* @date 2019/3/26
*/
@RestController
public class AuthEndpoints {

@GetMapping("/product/{id}")
public String getProduct(@PathVariable String id) {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
return "product id : " + id;
}

@GetMapping("/order/{id}")
public String getOrder(@PathVariable String id) {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
return "order id : " + id;
}

@GetMapping("/user/me")
public Principal user(Principal principal) {
return principal;
}
}

2.5.5. 测试

  1. 客户端凭据(客户端ID以及Key)模式

    发送 POST 请求获取 access_token

    request
    1
    POST http://localhost:8081/oauth/token?grant_type=client_credentials&scope=all&client_id=client_1&client_secret=123456

    请求结果:

    1
    2
    3
    4
    5
    6
    {
    "access_token": "d3025813-fd1f-4ccb-9faa-495cad16deff",
    "token_type": "bearer",
    "expires_in": 1199,
    "scope": "all"
    }

    将请求结果中的 access_token 取出并作为请求受保护资源 api 的请求参数

    request
    1
    GET http://localhost:8081/order/1?access_token=d3025813-fd1f-4ccb-9faa-495cad16deff
  2. 授权码模式

    授权链接

    1
    http://localhost:8081/oauth/authorize?response_type=code&client_id=client_1&scope=all&redirect_uri=http://localhost:8081/product/1

    在这里插入图片描述

    登陆后,同意授权

    在这里插入图片描述

    在这里插入图片描述

    将请求连接中的 code 作为请求令牌的请求参数

    1
    POST http://localhost:8081/oauth/token?client_id=client_1&grant_type=authorization_code&redirect_uri=http://localhost:8081/product/1&client_secret=123456&code=7fTmqZ

    请求结果:

    1
    2
    3
    4
    5
    6
    7
    {
    "access_token": "b485ed7c-3c92-43b0-97f2-0dc54da61d80",
    "token_type": "bearer",
    "refresh_token": "02b204ea-31f5-45c0-809e-ef2693117d31",
    "expires_in": 1199,
    "scope": "all"
    }

    取出 access_token 作为受保护的请求资源的令牌

    request
    1
    GET http://localhost:8081/product/1?access_token=b485ed7c-3c92-43b0-97f2-0dc54da61d80

2.5.6. 如何分离授权服务和资源服务

在上文 2.4. 服务类型 章节中,提过 在Spring OAuth中可以简便的将其分配到两个应用中(即微服务),而且可多个资源获取服务共享一个授权认证服务

ResourceServerTokenServices 是组成授权服务的另一半。

  1. 若是资源服务器和授权服务在同一个应用,可以使用 DefaultTokenServices
  2. 若是分离的。ResourceServerTokenServices 必须知道令牌的如何解码。

ResourceServerTokenServices 解析令牌的方法:

  • 使用 RemoteTokenServices,资源服务器通过HTTP请求来解码令牌。每次都请求授权服务器的端点 /oauth/check_toke,以此来解码令牌
  • 若是访问量大,则通过http获取之后,换成令牌的结果
  • 若是 jwt 令牌,需请求授权服务的 /oauth/token_key,来获取 key 进行解码

注意:授权认证服务需要把/oauth/check_toke暴露出来,并且附带上权限访问。

  1. 项目结构

    在这里插入图片描述

  2. 独立资源服务器配置

    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
    package com.fengxuechao.examples.sso.res.configuration;

    import org.springframework.context.annotation.Configuration;
    import org.springframework.security.config.annotation.web.builders.HttpSecurity;
    import org.springframework.security.config.http.SessionCreationPolicy;
    import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer;
    import org.springframework.security.oauth2.config.annotation.web.configuration.ResourceServerConfigurerAdapter;
    import org.springframework.security.oauth2.config.annotation.web.configurers.ResourceServerSecurityConfigurer;

    @Configuration
    @EnableResourceServer
    public class ResourceServerConfig extends ResourceServerConfigurerAdapter {
    private static final String DEMO_RESOURCE_ID = "*";

    @Override
    public void configure(ResourceServerSecurityConfigurer resources) {
    resources.resourceId(DEMO_RESOURCE_ID).stateless(true);
    }

    @Override
    public void configure(HttpSecurity http) throws Exception {
    http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED)
    .and().requestMatchers().anyRequest()
    .and().anonymous()
    .and().authorizeRequests().antMatchers("/**").authenticated();
    }

    /*@Primary
    @Bean
    public RemoteTokenServices tokenServices() {
    RemoteTokenServices tokenServices = new RemoteTokenServices();
    tokenServices.setCheckTokenEndpointUrl("http://localhost:8081/oauth/check_token");
    tokenServices.setClientId("client_1");
    tokenServices.setClientSecret("123456");
    return tokenServices;
    }*/
    }
  3. 配置文件

    application.properties

    1
    2
    3
    4
    5
    6
    7
    server.port=8082
    security.oauth2.client.client-id=client_1
    security.oauth2.client.client-secret=123456
    # userInfoUri用户端点的URI,用于获取当前用户详细信息
    security.oauth2.resource.user-info-uri=http://localhost:8081/user/me
    # 解析令牌的地址
    security.oauth2.authorization.check-token-access=http://localhost:8001/oauth/check_token
  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
    package com.fengxuechao.examples.sso.res;

    import org.springframework.boot.SpringApplication;
    import org.springframework.boot.autoconfigure.SpringBootApplication;
    import org.springframework.http.ResponseEntity;
    import org.springframework.security.core.context.SecurityContextHolder;
    import org.springframework.web.bind.annotation.RequestMapping;
    import org.springframework.web.bind.annotation.RestController;

    import java.util.HashMap;
    import java.util.Map;

    /**
    * @author fengxuechao
    * @date 2019/3/26
    */
    @SpringBootApplication
    @RestController
    public class ClientApp {
    public static void main(String[] args) {
    SpringApplication.run(ClientApp.class, args);
    }

    // 资源API
    @RequestMapping("/api/userinfo")
    public ResponseEntity<Map> getUserInfo() {

    String user = (String) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
    String email = user + "@test.com";
    Map<String, String> map = new HashMap<>();
    map.put("name", user);
    map.put("email", email);
    return ResponseEntity.ok(map);
    }
    }

参考资源

SpringBoot配置文件的优先级

项目结构

在这里插入图片描述

配置文件的优先级

application.properties 和 application.yml 文件按照优先级从大到小顺序排列在以下四个位置:

  1. file:./config/ (当前项目路径config目录下);
  2. file:./ (当前项目路径下);
  3. classpath:/config/ (类路径config目录下);
  4. classpath:/ (类路径config下).

在这里插入图片描述

源代码展示:

1
2
3
4
5
6
7
public class ConfigFileApplicationListener
implements EnvironmentPostProcessor, SmartApplicationListener, Ordered {
// Note the order is from least to most specific (last one wins)
private static final String DEFAULT_SEARCH_LOCATIONS = "classpath:/,classpath:/config/,file:./,file:./config/";

// 省略其它代码
}

以端口配置为例

  • 在resources/config目录下配置文件设置端口为8888;
  • 在resources/目录下配置文件设置端口为8080;
  • 在类路径config目录下配置文件设置端口为6666;
  • 在类路径下配置文件设置端口为5555;

运行结果:

在这里插入图片描述

自定义配置文件的绑定

  1. CustomizedFile 类

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    /**
    * 自定义配置文件, 需要配合使用后@Configuration@PropertySource("classpath:customized-file.properties")来指定
    * @author fengxuechao
    */
    @Configuration
    @ConfigurationProperties(prefix = "customizedFile")
    @PropertySource("classpath:customized-file-${spring.profiles.active}.properties")
    public class CustomizedFile {
    private String name;
    private String author;
    private String path;
    private String description;
    // 省略 setter/getter
    }

    看到 ${spring.profiles.active},聪明的你一定知道这是 spring boot多环境自定义配置文件的实现方式。
    生效的配置文件是 ${spring.profiles.active} 所指定的配置文件,本文案例中生效的是 customized-file-dev.properties
    接下来继续创建配置文件验证

  2. customized-file.properties

    1
    2
    3
    4
    customizedFile.name=自定义配置文件名
    customizedFile.author=作者名
    customizedFile.path=路径地址
    customizedFile.description=看到这个就表明自定义配置文件成功了
  3. customized-file-dev.properties

    1
    customizedFile.description=DEV:看到这个就表明自定义配置文件成功了
  4. 运行结果:

    在这里插入图片描述

    结论:只有 customized-file-dev.properties 中配置的属性生效

Java源码学习之ArrayList

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
package java.util;

import java.util.function.Consumer;
import java.util.function.Predicate;
import java.util.function.UnaryOperator;

public class ArrayList<E> extends AbstractList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable
{
private static final long serialVersionUID = 8683452581122892189L;

/**
* 默认容量
*/
private static final int DEFAULT_CAPACITY = 10;

/**
* 空数组, new ArrayList(0)的时候默认数组构建一个空数组
*/
private static final Object[] EMPTY_ELEMENTDATA = {};

/**
* 空数组, 调用无参构造函数的时候默认给一个空数组
*/
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};

/**
* 保存数据的数组
*/
transient Object[] elementData; // non-private to simplify nested class access

/**
* ArrayList的实际元素数量
*
* @serial
*/
private int size;

/**
* 给定一个初始容量来构造一个空数组
*/
public ArrayList(int initialCapacity) {
if (initialCapacity > 0) {
this.elementData = new Object[initialCapacity];
} else if (initialCapacity == 0) {
this.elementData = EMPTY_ELEMENTDATA;
} else {
throw new IllegalArgumentException("Illegal Capacity: "+
initialCapacity);
}
}

/**
* 无参数构造方法默认为空数组
*/
public ArrayList() {
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}

/**
* 构造方法传入一个Collection, 则将Collection里面的值copy到arrayList
*/
public ArrayList(Collection<? extends E> c) {
elementData = c.toArray();
if ((size = elementData.length) != 0) {
// c.toArray might (incorrectly) not return Object[] (see 6260652)
if (elementData.getClass() != Object[].class)
elementData = Arrays.copyOf(elementData, size, Object[].class);
} else {
// replace with empty array.
this.elementData = EMPTY_ELEMENTDATA;
}
}

/**
* 调整当前实例的容量为实际数组的大小,用于最小化实例的内存空间。
* 可以解决平时新增、删除元素后elementData过大的问题。
*/
public void trimToSize() {
modCount++;
if (size < elementData.length) {
elementData = (size == 0)
? EMPTY_ELEMENTDATA
: Arrays.copyOf(elementData, size);
}
}

/**
* 确定动态扩容所需容量
*/
private void ensureCapacityInternal(int minCapacity) {
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
}

ensureExplicitCapacity(minCapacity);
}

private void ensureExplicitCapacity(int minCapacity) {
modCount++;

// 超出了数组可容纳的长度,需要进行动态扩展
if (minCapacity - elementData.length > 0)
grow(minCapacity);
}

/**
* 1. 一些JVM可能存储Headerwords
* 2. 避免一些机器内存溢出,减少出错几率
*/
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;

/**
* 动态扩容的核心方法。
*
* @param minCapacity the desired minimum capacity
*/
private void grow(int minCapacity) {
// overflow-conscious code
int oldCapacity = elementData.length;
// 先对容量扩大1.5倍, 这里 oldCapacity >> 1 是二进制操作右移,相当于除以2, 我称之为期望容量
int newCapacity = oldCapacity + (oldCapacity >> 1);
// minCapacity 我称之为最小容量
// 比较期望容量与最小容量
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
// 判断期望容量是否超过 Integer.MAX_VALUE - 8. 一般很少用到,那么多数据也不会用ArrayList来做容器了吧
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
// minCapacity is usually close to size, so this is a win:
elementData = Arrays.copyOf(elementData, newCapacity);
}

// 这辈子都不太有机会用到吧
private static int hugeCapacity(int minCapacity) {
if (minCapacity < 0) // overflow
throw new OutOfMemoryError();
return (minCapacity > MAX_ARRAY_SIZE) ?
Integer.MAX_VALUE :
MAX_ARRAY_SIZE;
}

public boolean add(E e) {
ensureCapacityInternal(size + 1); // Increments modCount!!
elementData[size++] = e;
return true;
}

/**
* 1. 将指定元素的索引及后续元素的索引向右移动(索引+1)
* 2. 在指定的索引插入元素
*/
public void add(int index, E element) {
rangeCheckForAdd(index);

ensureCapacityInternal(size + 1); // Increments modCount!!
System.arraycopy(elementData, index, elementData, index + 1,
size - index);
elementData[index] = element;
size++;
}

/**
* 1. 将指定索引及后续元素的索引向左移动
* 2. 数组元素实际数量 - 1
*/
public E remove(int index) {
rangeCheck(index);

modCount++;
E oldValue = elementData(index);

int numMoved = size - index - 1;
if (numMoved > 0)
System.arraycopy(elementData, index+1, elementData, index,
numMoved);
elementData[--size] = null; // clear to let GC do its work

return oldValue;
}

// 循环比较元素,获取要移除元素的索引,然后将该索引及后续元素的索引向左移动
public boolean remove(Object o) {
if (o == null) {
for (int index = 0; index < size; index++)
if (elementData[index] == null) {
fastRemove(index);
return true;
}
} else {
for (int index = 0; index < size; index++)
if (o.equals(elementData[index])) {
fastRemove(index);
return true;
}
}
return false;
}

private void fastRemove(int index) {
modCount++;
int numMoved = size - index - 1;
if (numMoved > 0)
System.arraycopy(elementData, index+1, elementData, index,
numMoved);
elementData[--size] = null; // clear to let GC do its work
}

/**
* 循环设置所有元素值为null, 加快垃圾回收
*/
public void clear() {
modCount++;

// clear to let GC do its work
for (int i = 0; i < size; i++)
elementData[i] = null;

size = 0;
}

// 只要将前面的源码读懂,后面的都是类似的

}

从上面的源码分析中就可以看出 ArrayList 的本质就是数组。ArrayList 的一些特性都来源于数组:有序、元素可重复、插入慢、 索引快。
而所谓的动态扩容不就是复制原数组到扩容后的数组。

锁是用于通过多个线程控制对共享资源的访问的工具。一般来说,锁提供对共享资源的独占访问:一次只能有一个线程可以获取锁,并且对共享资源的所有访问都要求首先获取锁。 但是,一些锁可能允许并发访问共享资源,如ReadWriteLock的读写锁。

阅读全文 »