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

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


  • 首页

  • 归档

  • 搜索

有限状态机的4种Java实现对比

发表于 2019-12-22

在日常工作过程中,我们经常会遇到状态的变化场景,例如订单状态发生变化,商品状态的变化。这些状态的变化,我们称为有限状态机,缩写为FSM( F State Machine).。之所以称其为有限,是因为这些场景中的状态往往是可以枚举出来的有限个的,所以称其为有限状态机。下面我们来看一个具体的场景例子。

简单场景:

地铁进站闸口的状态有两个:已经关闭、已经开启两个状态。刷卡后闸口从已关闭变为已开启,人通过后闸口状态从已开启变为已关闭。

01 遇到这类问题,在编码时我们应该如何处理呢?

  • 基于Switch
  • 基于状态集合
  • 基于State模式
  • 基于枚举的实现

下面我们针对每一种实现方式进行分析。场景分解后会有一下2种状态4种情况出现:

Index State Event NextState Action
1 闸机口 LOCKED 投币 闸机口 UN_LOCKED 闸机口打开闸门
2 闸机口 LOCKED 通过 闸机口 LOCKED 闸机口警告
3 闸机口 UN_LOCKED 投币 闸机口 UN _LOCKED 闸机口退币
4 闸机口 UN_LOCKED 通过 闸机口 LOCKED 闸机口关闭闸门

针对以上4种请求,共拆分了5个Test Case

T01

Given:一个Locked的进站闸口

When: 投入硬币

Then:打开闸口

T02

Given:一个Locked的进站闸口

When: 通过闸口

Then:警告提示

T03

Given:一个Unocked的进站闸口

When: 通过闸口

Then:闸口关闭

T04

Given:一个Unlocked的进站闸口

When: 投入硬币

Then:退还硬币

T05

Given:一个闸机口

When: 非法操作

Then:操作失败

代码地址:https://gitlab.com/tengbai/fsm-java

项目中共有4中状态机的实现方式。

  • 基于Switch语句实现的有限状态机,代码在master分支
  • 基于State模式实现的有限状态机。代码在state-pattern分支
  • 基于状态集合实现的有限状态机。代码在collection-state分支
  • 基于枚举实现的状态机。代码在enum-state分支
01.01 使用Switch来实现有限状态机

这种方式只需要懂得Java语法及可以实现出来。先看代码,然后我们在讨论这种实现方式是否好。

EntranceMachineTest.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
复制代码package com.page.java.fsm;

import com.page.java.fsm.exception.InvalidActionException;
import org.junit.jupiter.api.Test;

import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.assertj.core.api.BDDAssertions.then;

class EntranceMachineTest {

@Test
void should_be_unlocked_when_insert_coin_given_a_entrance_machine_with_locked_state() {
EntranceMachine entranceMachine = new EntranceMachine(EntranceMachineState.LOCKED);

String result = entranceMachine.execute(Action.INSERT_COIN);

then(result).isEqualTo("opened");
then(entranceMachine.getState()).isEqualTo(EntranceMachineState.UNLOCKED);
}

@Test
void should_be_locked_and_alarm_when_pass_given_a_entrance_machine_with_locked_state() {
EntranceMachine entranceMachine = new EntranceMachine(EntranceMachineState.LOCKED);

String result = entranceMachine.execute(Action.PASS);

then(result).isEqualTo("alarm");
then(entranceMachine.getState()).isEqualTo(EntranceMachineState.LOCKED);
}

@Test
void should_fail_when_execute_invalid_action_given_a_entrance_with_locked_state() {
EntranceMachine entranceMachine = new EntranceMachine(EntranceMachineState.LOCKED);

assertThatThrownBy(() -> entranceMachine.execute(null))
.isInstanceOf(InvalidActionException.class);
}

@Test
void should_locked_when_pass_given_a_entrance_machine_with_unlocked_state() {
EntranceMachine entranceMachine = new EntranceMachine(EntranceMachineState.UNLOCKED);

String result = entranceMachine.execute(Action.PASS);

then(result).isEqualTo("closed");
then(entranceMachine.getState()).isEqualTo(EntranceMachineState.LOCKED);
}

@Test
void should_refund_and_unlocked_when_insert_coin_given_a_entrance_machine_with_unlocked_state() {
EntranceMachine entranceMachine = new EntranceMachine(EntranceMachineState.UNLOCKED);

String result = entranceMachine.execute(Action.INSERT_COIN);

then(result).isEqualTo("refund");
then(entranceMachine.getState()).isEqualTo(EntranceMachineState.UNLOCKED);
}
}

Action.java

1
2
3
4
复制代码public enum Action {
INSERT_COIN,
PASS
}

EntranceMachineState.java

1
2
3
4
复制代码public enum EntranceMachineState {
UNLOCKED,
LOCKED
}

InvalidActionException.java

1
2
3
4
复制代码package com.page.java.fsm.exception;

public class InvalidActionException extends RuntimeException {
}

EntranceMachine.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
复制代码package com.page.java.fsm;

import com.page.java.fsm.exception.InvalidActionException;
import lombok.Data;

import java.util.Objects;

@Data
public class EntranceMachine {

private EntranceMachineState state;

public EntranceMachine(EntranceMachineState state) {
this.state = state;
}

public String execute(Action action) {
if (Objects.isNull(action)) {
throw new InvalidActionException();
}

if (EntranceMachineState.LOCKED.equals(state)) {
switch (action) {
case INSERT_COIN:
setState(EntranceMachineState.UNLOCKED);
return open();
case PASS:
return alarm();
}
}

if (EntranceMachineState.UNLOCKED.equals(state)) {
switch (action) {
case PASS:
setState(EntranceMachineState.LOCKED);
return close();
case INSERT_COIN:
return refund();
}
}
return null;
}

private String refund() {
return "refund";
}

private String close() {
return "closed";
}

private String alarm() {
return "alarm";
}

private String open() {
return "opened";
}
}

if(), swich语句都是switch语句,但是Switch是一种Code Bad Smell,因为它本质上一种重复。当代码中有多处相同的switch时,会让系统变得晦涩难懂,脆弱,不易修改。

上面的代码虽然出现了多层嵌套但是还算是结构简单,不过想通过并不能很清楚闸机口的逻辑还是化点时间。如果闸机口的状态等多一些,那就阅读、理解起来也就更加困难。

所以在日常工作,我遵循**“事不过三,三则重构”**的原则:

事不过三:

当只有一两个状态(或者重复)时,那么先用最简单的实现实现。

一旦出现三种以及以上的状态(或者重复),立即重构。

01.02 State模式

EntranceMachineTest.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
复制代码package com.page.java.fsm;

import com.page.java.fsm.exception.InvalidActionException;
import org.junit.jupiter.api.Test;

import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.assertj.core.api.BDDAssertions.then;

class EntranceMachineTest {

@Test
void should_be_unlocked_when_insert_coin_given_a_entrance_machine_with_locked_state() {
EntranceMachine entranceMachine = new EntranceMachine(new LockedEntranceMachineState());

String result = entranceMachine.execute(Action.INSERT_COIN);

then(result).isEqualTo("opened");
then(entranceMachine.isUnlocked()).isTrue();
}

@Test
void should_be_locked_and_alarm_when_pass_given_a_entrance_machine_with_locked_state() {
EntranceMachine entranceMachine = new EntranceMachine(new LockedEntranceMachineState());

String result = entranceMachine.execute(Action.PASS);

then(result).isEqualTo("alarm");
then(entranceMachine.isLocked()).isTrue();
}

@Test
void should_fail_when_execute_invalid_action_given_a_entrance_with_locked_state() {
EntranceMachine entranceMachine = new EntranceMachine(new LockedEntranceMachineState());

assertThatThrownBy(() -> entranceMachine.execute(null))
.isInstanceOf(InvalidActionException.class);
}

@Test
void should_locked_when_pass_given_a_entrance_machine_with_unlocked_state() {
EntranceMachine entranceMachine = new EntranceMachine(new UnlockedEntranceMachineState());

String result = entranceMachine.execute(Action.PASS);

then(result).isEqualTo("closed");
then(entranceMachine.isLocked()).isTrue();
}

@Test
void should_refund_and_unlocked_when_insert_coin_given_a_entrance_machine_with_unlocked_state() {
EntranceMachine entranceMachine = new EntranceMachine(new UnlockedEntranceMachineState());

String result = entranceMachine.execute(Action.INSERT_COIN);

then(result).isEqualTo("refund");
then(entranceMachine.isUnlocked()).isTrue();
}
}

EntranceMachineState.java

1
2
3
4
5
6
7
8
复制代码package com.page.java.fsm;

public interface EntranceMachineState {

String insertCoin(EntranceMachine entranceMachine);

String pass(EntranceMachine entranceMachine);
}

LockedEntranceMachineState.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
复制代码package com.page.java.fsm;

public class LockedEntranceMachineState implements EntranceMachineState {

@Override
public String insertCoin(EntranceMachine entranceMachine) {
return entranceMachine.open();
}

@Override
public String pass(EntranceMachine entranceMachine) {
return entranceMachine.alarm();
}
}

UnlockedEntranceMachineState.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
复制代码package com.page.java.fsm;

public class UnlockedEntranceMachineState implements EntranceMachineState {

@Override
public String insertCoin(EntranceMachine entranceMachine) {
return entranceMachine.refund();
}

@Override
public String pass(EntranceMachine entranceMachine) {
return entranceMachine.close();
}
}

Action.java

1
2
3
4
5
6
复制代码package com.page.java.fsm;

public enum Action {
PASS,
INSERT_COIN
}

EntranceMachine.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
复制代码package com.page.java.fsm;

import com.page.java.fsm.exception.InvalidActionException;

import java.util.Objects;

public class EntranceMachine {

private EntranceMachineState locked = new LockedEntranceMachineState();

private EntranceMachineState unlocked = new UnlockedEntranceMachineState();

private EntranceMachineState state;

public EntranceMachine(EntranceMachineState state) {
this.state = state;
}

public String execute(Action action) {
if (Objects.isNull(action)) {
throw new InvalidActionException();
}

if (Action.PASS.equals(action)) {
return state.pass(this);
}

return state.insertCoin(this);
}

public boolean isUnlocked() {
return state == unlocked;
}

public boolean isLocked() {
return state == locked;
}

public String open() {
setState(unlocked);
return "opened";
}

public String alarm() {
setState(locked);
return "alarm";
}

public String refund() {
setState(unlocked);
return "refund";
}

public String close() {
setState(locked);
return "closed";
}

private void setState(EntranceMachineState state) {
this.state = state;
}
}

State模式和Proxy模式类似,但是在State模式中EntranceMachineState持有EntranceMachine实例的引用。

我们发现EntranceMachine的execute()方法的逻辑变的简单,但是代码复杂度升高了。因为每个state实例都提供了两个动作实现insertCoin()和pass()。这个地方本人认为并不够表意,因为作出的动作被添加到两个状态上,虽然能够实现业务业务,但是并不利于理解清楚业务意思。

State模式,虽然能够将逻辑进行拆分,但是那些状态的顺序,以及有几种状态,都不是很直观的观察到。

不过在实际业务中,State模式也是一种很好的实现方式,毕竟他避免了switch的堆积问题。

01.03 使用状态集合

状态集合是将一组描述状态变化的事务元素组成的集合。

集合中的每一个元素包含4个属性:当前的状态,事件,下一个状态,触发的动作。

使用时遍历集合根据动作找到特定的元素,并更具元素上的属性和事件来完成业务逻辑。

具体代码如下:

EntranceMachineTest.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
复制代码package com.page.java.fsm;

import com.page.java.fsm.exception.InvalidActionException;
import org.junit.jupiter.api.Test;

import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.assertj.core.api.BDDAssertions.then;

class EntranceMachineTest {

@Test
void should_be_unlocked_when_insert_coin_given_a_entrance_machine_with_locked_state() {
EntranceMachine entranceMachine = new EntranceMachine(EntranceMachineState.LOCKED);

String result = entranceMachine.execute(Action.INSERT_COIN);

then(result).isEqualTo("opened");
then(entranceMachine.getState()).isEqualTo(EntranceMachineState.UNLOCKED);
}

@Test
void should_be_alarm_when_pass_given_a_entrance_machine_with_locked_state() {
EntranceMachine entranceMachine = new EntranceMachine(EntranceMachineState.LOCKED);

String result = entranceMachine.execute(Action.PASS);

then(result).isEqualTo("alarm");
then(entranceMachine.getState()).isEqualTo(EntranceMachineState.LOCKED);
}

@Test
void should_fail_when_execute_invalid_action_given_a_entrance_machine() {
EntranceMachine entranceMachine = new EntranceMachine(EntranceMachineState.LOCKED);

assertThatThrownBy(() -> entranceMachine.execute(null))
.isInstanceOf(InvalidActionException.class);

}

@Test
void should_closed_when_pass_given_a_entrance_machine_with_unlocked() {
EntranceMachine entranceMachine = new EntranceMachine(EntranceMachineState.UNLOCKED);

String result = entranceMachine.execute(Action.PASS);

then(result).isEqualTo("closed");
then(entranceMachine.getState()).isEqualTo(EntranceMachineState.LOCKED);
}

@Test
void should_refund_when_insert_coin_given_a_entrance_machine_with_unlocked() {
EntranceMachine entranceMachine = new EntranceMachine(EntranceMachineState.UNLOCKED);

String result = entranceMachine.execute(Action.INSERT_COIN);

then(result).isEqualTo("refund");
then(entranceMachine.getState()).isEqualTo(EntranceMachineState.UNLOCKED);
}
}

Action.java

1
2
3
4
5
6
复制代码package com.page.java.fsm;

public enum Action {
PASS,
INSERT_COIN
}

EntranceMachineState.java

1
2
3
4
5
6
复制代码package com.page.java.fsm;

public enum EntranceMachineState {
LOCKED,
UNLOCKED
}

EntranceMachine.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
复制代码package com.page.java.fsm;

import com.page.java.fsm.events.AlarmEvent;
import com.page.java.fsm.events.CloseEvent;
import com.page.java.fsm.events.OpenEvent;
import com.page.java.fsm.events.RefundEvent;
import com.page.java.fsm.exception.InvalidActionException;
import lombok.Data;

import java.util.Arrays;
import java.util.List;
import java.util.Optional;

@Data
public class EntranceMachine {

List<EntranceMachineTransaction> entranceMachineTransactionList = Arrays.asList(
EntranceMachineTransaction.builder()
.currentState(EntranceMachineState.LOCKED)
.action(Action.INSERT_COIN)
.nextState(EntranceMachineState.UNLOCKED)
.event(new OpenEvent())
.build(),
EntranceMachineTransaction.builder()
.currentState(EntranceMachineState.LOCKED)
.action(Action.PASS)
.nextState(EntranceMachineState.LOCKED)
.event(new AlarmEvent())
.build(),
EntranceMachineTransaction.builder()
.currentState(EntranceMachineState.UNLOCKED)
.action(Action.PASS)
.nextState(EntranceMachineState.LOCKED)
.event(new CloseEvent())
.build(),
EntranceMachineTransaction.builder()
.currentState(EntranceMachineState.UNLOCKED)
.action(Action.INSERT_COIN)
.nextState(EntranceMachineState.UNLOCKED)
.event(new RefundEvent())
.build()
);

private EntranceMachineState state;

public EntranceMachine(EntranceMachineState state) {
setState(state);
}

public String execute(Action action) {
Optional<EntranceMachineTransaction> transactionOptional = entranceMachineTransactionList
.stream()
.filter(transaction ->
transaction.getAction().equals(action) && transaction.getCurrentState().equals(state))
.findFirst();

if (!transactionOptional.isPresent()) {
throw new InvalidActionException();
}

EntranceMachineTransaction transaction = transactionOptional.get();
setState(transaction.getNextState());
return transaction.getEvent().execute();
}
}

EntranceMachineTransaction.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
复制代码package com.page.java.fsm;

import com.page.java.fsm.events.Event;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class EntranceMachineTransaction {

private EntranceMachineState currentState;

private Action action;

private EntranceMachineState nextState;

private Event event;
}

Event.java

1
2
3
4
5
6
复制代码package com.page.java.fsm.events;

public interface Event {

String execute();
}

OpenEvent.java

1
2
3
4
5
6
7
8
复制代码package com.page.java.fsm.events;

public class OpenEvent implements Event {
@Override
public String execute() {
return "opened";
}
}

AlarmEvent.java

1
2
3
4
5
6
7
8
复制代码package com.page.java.fsm.events;

public class AlarmEvent implements Event {
@Override
public String execute() {
return "alarm";
}
}

CloseEvent.java

1
2
3
4
5
6
7
8
复制代码package com.page.java.fsm.events;

public class CloseEvent implements Event {
@Override
public String execute() {
return "closed";
}
}

RefundEvent.java

1
2
3
4
5
6
7
8
复制代码package com.page.java.fsm.events;

public class RefundEvent implements Event {
@Override
public String execute() {
return "refund";
}
}

InvalidActionException.java

1
2
3
4
复制代码package com.page.java.fsm.exception;

public class InvalidActionException extends RuntimeException {
}

相比于Switch的实现方式,状态集合的实现方式对状态规则的描述更加直观。且扩展性更强,不需求修改实现路基,只需要添加相关的状态描述即可。

我们知道日常工作中读代码和写代码比例在10:1,有些场景下甚至到了20:1。Switch需要我们每次在脑子中组织一次状态的顺序和规则,而集合能够很直观的表达出这个规则。

01.04 使用Enum的来实现状态机

EntranceMachineTest.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
复制代码package com.page.java.fsm;

import com.page.java.fsm.exception.InvalidActionException;
import org.junit.jupiter.api.Test;

import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.assertj.core.api.BDDAssertions.then;

class EntranceMachineTest {

@Test
void should_unlocked_when_insert_coin_given_a_entrance_machine_with_locked_state() {
EntranceMachine entranceMachine = new EntranceMachine(EntranceMachineState.LOCKED);

String result = entranceMachine.execute(Action.INSERT_COIN);

then(result).isEqualTo("opened");
then(entranceMachine.getState()).isEqualTo(EntranceMachineState.UNLOCKED);
}

@Test
void should_alarm_when_pass_given_a_entrance_machine_with_locked_state() {
EntranceMachine entranceMachine = new EntranceMachine(EntranceMachineState.LOCKED);

String result = entranceMachine.execute(Action.PASS);

then(result).isEqualTo("alarm");
then(entranceMachine.getState()).isEqualTo(EntranceMachineState.LOCKED);
}

@Test
void should_fail_when_execute_invalid_action_given_a_entrance_machine() {
EntranceMachine entranceMachine = new EntranceMachine(EntranceMachineState.LOCKED);

assertThatThrownBy(() -> entranceMachine.execute(null))
.isInstanceOf(InvalidActionException.class);
}

@Test
void should_refund_when_insert_coin_given_a_entrance_machine_with_unlocked_state() {
EntranceMachine entranceMachine = new EntranceMachine(EntranceMachineState.UNLOCKED);

String result = entranceMachine.execute(Action.INSERT_COIN);

then(result).isEqualTo("refund");
then(entranceMachine.getState()).isEqualTo(EntranceMachineState.UNLOCKED);
}

@Test
void should_closed_when_pass_given_a_entrance_machine_with_unlocked_state() {
EntranceMachine entranceMachine = new EntranceMachine(EntranceMachineState.UNLOCKED);

String result = entranceMachine.execute(Action.PASS);

then(result).isEqualTo("closed");
then(entranceMachine.getState()).isEqualTo(EntranceMachineState.LOCKED);

}

}

EntraceMachine.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
复制代码package com.page.java.fsm;


import com.page.java.fsm.exception.InvalidActionException;
import lombok.Data;

import java.util.Objects;

@Data
public class EntranceMachine {

private EntranceMachineState state;

public EntranceMachine(EntranceMachineState state) {
setState(state);
}

public String execute(Action action) {
if (Objects.isNull(action)) {
throw new InvalidActionException();
}

return action.execute(this, state);
}

public String open() {
return "opened";
}

public String alarm() {
return "alarm";
}

public String refund() {
return "refund";
}

public String close() {
return "closed";
}
}

Action.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
复制代码package com.page.java.fsm;

public enum Action {
PASS {
@Override
public String execute(EntranceMachine entranceMachine, EntranceMachineState state) {
return state.pass(entranceMachine);
}
},
INSERT_COIN {
@Override
public String execute(EntranceMachine entranceMachine, EntranceMachineState state) {
return state.insertCoin(entranceMachine);
}
};

public abstract String execute(EntranceMachine entranceMachine, EntranceMachineState state);
}

EntranceMachineState.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
复制代码package com.page.java.fsm;

public enum EntranceMachineState {
LOCKED {
@Override
public String insertCoin(EntranceMachine entranceMachine) {
entranceMachine.setState(UNLOCKED);
return entranceMachine.open();
}

@Override
public String pass(EntranceMachine entranceMachine) {
entranceMachine.setState(this);
return entranceMachine.alarm();
}
},
UNLOCKED {
@Override
public String insertCoin(EntranceMachine entranceMachine) {
entranceMachine.setState(this);
return entranceMachine.refund();
}

@Override
public String pass(EntranceMachine entranceMachine) {
entranceMachine.setState(LOCKED);
return entranceMachine.close();
}
};

public abstract String insertCoin(EntranceMachine entranceMachine);

public abstract String pass(EntranceMachine entranceMachine);
}

InvalidActionException.java

1
2
3
4
复制代码package com.page.java.fsm.exception;

public class InvalidActionException extends RuntimeException {
}

通过上面的代码,可以发现Action、EntranceMachineState两个枚举的复杂度都提升了。不单单是定义了常量那么简单。还提供了相应的逻辑处理。

在EntranceMachineState.java的提交记录中,对进行了一次重构,将具体业务逻辑执行移动到EntranceMachine中,EntranceMachineState内每种状态的方法中只负责调度。这样能够通过EntranceMachineState相对直观的看清楚做了什么,状态变成了什么。

缺陷就是,EntranceMachine 对外提供了public的setState方法,这也就意味着调用者在将来维护是,很有可能滥用setState方法。

02 总结

通过上面4中对FSM的实现,我们看到每一种是实现都有优点和它的不足。那么在日常工作中,如何选择呢,我个人认为可以遵循一下两个建议:

  1. 遵循Simple Design。如果没有一个外部参考,那么用哪一种都不为过。所以引入一个原则作为参考,可以更好的帮助我们做决定。这里日常工作中我们经常使用Simple Design:通过测试、揭示意图、消除重复、最少元素。并在实现过程中不断重构,代码是重构出来的,而不是一次性的设计出来的。
  2. 在状态机的实现上多做尝试。例子只是一个简单的场景,所以只能看到简单场景下的实现效果,实际业务线上的状态会非常丰富,而且每种状态中可真行的动作也是不同的。所以针对特定场景遇到的问题,多尝试练习思考,练习思考后的经验才是最重要的。

参考

  • 《敏捷开发实践》

本文转载自: 掘金

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

18道kafka高频面试题哪些你还不会?(含答案和思维导图)

发表于 2019-12-22

原文:https://www.cnblogs.com/starcrm/p/10552183.html

前言

Kafka是最初由Linkedin公司开发,是一个分布式、支持分区的(partition)、多副本的(replica),基于zookeeper协调的分布式消息系统,它的最大的特性就是可以实时的处理大量数据以满足各种需求场景:比如基于hadoop的批处理系统、低延迟的实时系统、storm/Spark流式处理引擎,web/nginx日志、访问日志,消息服务等等,用scala语言编写,Linkedin于2010年贡献给了Apache基金会并成为顶级开源 项目。

关于Kafka的知识总结了个思维导图

kafka 面试题

1、如何获取 topic 主题的列表2、生产者和消费者的命令行是什么?3、consumer 是推还是拉?4、讲讲 kafka 维护消费状态跟踪的方法5、讲一下主从同步6、为什么需要消息系统,mysql 不能满足需求吗?7、Zookeeper 对于 Kafka 的作用是什么?8、数据传输的事务定义有哪三种?9、Kafka 判断一个节点是否还活着有那两个条件?10、Kafka 与传统 MQ 消息系统之间有三个关键区别11、讲一讲 kafka 的 ack 的三种机制
12、消费者如何不自动提交偏移量,由应用提交?
13、消费者故障,出现活锁问题如何解决?14、如何控制消费的位置15、kafka 分布式(不是单机)的情况下,如何保证消息的顺序消费?16、kafka 的高可用机制是什么?17、kafka 如何减少数据丢失18、kafka 如何不消费重复数据?比如扣款,我们不能重复的扣。### 1、如何获取 topic 主题的列表

bin/kafka-topics.sh –list –zookeeper localhost:2181### 2、生产者和消费者的命令行是什么?

生产者在主题上发布消息:bin/kafka-console-producer.sh –broker-list 192.168.43.49:9092 –topicHello-Kafka注意这里的 IP 是 server.properties 中的 listeners 的配置。接下来每个新行就是输入一条新消息。消费者接受消息:bin/kafka-console-consumer.sh –zookeeper localhost:2181 –topicHello-Kafka –from-beginning### 3、consumer 是推还是拉?

Kafka 最初考虑的问题是,customer 应该从 brokes 拉取消息还是 brokers 将消息推送到 consumer,也就是 pull 还 push。在这方面,Kafka 遵循了一种大部分消息系统共同的传统的设计:producer 将消息推送到 broker,consumer 从broker 拉取消息。一些消息系统比如 Scribe 和 Apache Flume 采用了 push 模式,将消息推送到下游的 consumer。这样做有好处也有坏处:由 broker 决定消息推送的速率,对于不同消费速率的 consumer 就不太好处理了。消息系统都致力于让 consumer 以最大的速率最快速的消费消息,但不幸的是,push 模式下,当 broker 推送的速率远大于 consumer 消费的速率时,consumer 恐怕就要崩溃了。最终 Kafka 还是选取了传统的 pull 模式。Pull 模式的另外一个好处是 consumer 可以自主决定是否批量的从 broker 拉取数据 。Push 模式必须在不知道下游 consumer 消费能力和消费策略的情况下决定是立即推送每条消息还是缓存之后批量推送。如果为了避免 consumer 崩溃而采用较低的推送速率,将可能导致一次只推送较少的消息而造成浪费。Pull 模式下,consumer 就可以根据自己的消费能力去决定这些策略。Pull 有个缺点是,如果 broker 没有可供消费的消息,将导致 consumer 不断在循环中轮询,直到新消息到 t 达。为了避免这点,Kafka 有个参数可以让 consumer阻塞知道新消息到达(当然也可以阻塞知道消息的数量达到某个特定的量这样就可以批量发送)。### 4、讲讲 kafka 维护消费状态跟踪的方法

大部分消息系统在 broker 端的维护消息被消费的记录:一个消息被分发到consumer 后 broker 就马上进行标记或者等待 customer 的通知后进行标记。这样也可以在消息在消费后立马就删除以减少空间占用。但是这样会不会有什么问题呢?如果一条消息发送出去之后就立即被标记为消费过的,旦 consumer 处理消息时失败了(比如程序崩溃)消息就丢失了。为了解决这个问题,很多消息系统提供了另外一个个功能:当消息被发送出去之后仅仅被标记为已发送状态,当接到 consumer 已经消费成功的通知后才标记为已被消费的状态。这虽然解决了消息丢失的问题,但产生了新问题,首先如果 consumer处理消息成功了但是向 broker 发送响应时失败了,这条消息将被消费两次。第二个问题时,broker 必须维护每条消息的状态,并且每次都要先锁住消息然后更改状态然后释放锁。这样麻烦又来了,且不说要维护大量的状态数据,比如如果消息发送出去但没有收到消费成功的通知,这条消息将一直处于被锁定的状态,Kafka 采用了不同的策略。Topic 被分成了若干分区,每个分区在同一时间只被一个 consumer 消费。这意味着每个分区被消费的消息在日志中的位置仅仅是一个简单的整数:offset。这样就很容易标记每个分区消费状态就很容易了,仅仅需要一个整数而已。这样消费状态的跟踪就很简单了。这带来了另外一个好处:consumer 可以把 offset 调成一个较老的值,去重新消费老的消息。这对传统的消息系统来说看起来有些不可思议,但确实是非常有用的,谁规定了一条消息只能被消费一次呢?### 5、讲一下主从同步

Kafka允许topic的分区拥有若干副本,这个数量是可以配置的,你可以为每个topci配置副本的数量。Kafka会自动在每个个副本上备份数据,所以当一个节点down掉时数据依然是可用的。Kafka的副本功能不是必须的,你可以配置只有一个副本,这样其实就相当于只有一份数据。### 6、为什么需要消息系统,mysql 不能满足需求吗?

(1)解耦:允许你独立的扩展或修改两边的处理过程,只要确保它们遵守同样的接口约束。(2)冗余:消息队列把数据进行持久化直到它们已经被完全处理,通过这一方式规避了数据丢失风险。许多消息队列所采用的”插入-获取-删除”范式中,在把一个消息从队列中删除之前,需要你的处理系统明确的指出该消息已经被处理完毕,从而确保你的数据被安全的保存直到你使用完毕。(3)扩展性:因为消息队列解耦了你的处理过程,所以增大消息入队和处理的频率是很容易的,只要另外增加处理过程即可。(4)灵活性 & 峰值处理能力:在访问量剧增的情况下,应用仍然需要继续发挥作用,但是这样的突发流量并不常见。如果为以能处理这类峰值访问为标准来投入资源随时待命无疑是巨大的浪费。使用消息队列能够使关键组件顶住突发的访问压力,而不会因为突发的超负荷的请求而完全崩溃。(5)可恢复性:系统的一部分组件失效时,不会影响到整个系统。消息队列降低了进程间的耦合度,所以即使一个处理消息的进程挂掉,加入队列中的消息仍然可以在系统恢复后被处理。(6)顺序保证:在大多使用场景下,数据处理的顺序都很重要。大部分消息队列本来就是排序的,并且能保证数据会按照特定的顺序来处理。(Kafka 保证一个 Partition 内的消息的有序性)(7)缓冲:有助于控制和优化数据流经过系统的速度,解决生产消息和消费消息的处理速度不一致的情况。(8)异步通信:很多时候,用户不想也不需要立即处理消息。消息队列提供了异步处理机制,允许用户把一个消息放入队列,但并不立即处理它。想向队列中放入多少消息就放多少,然后在需要的时候再去处理它们。### 7、Zookeeper 对于 Kafka 的作用是什么?

Zookeeper 是一个开放源码的、高性能的协调服务,它用于 Kafka 的分布式应用。Zookeeper 主要用于在集群中不同节点之间进行通信在 Kafka 中,它被用于提交偏移量,因此如果节点在任何情况下都失败了,它都可以从之前提交的偏移量中获取除此之外,它还执行其他活动,如: leader 检测、分布式同步、配置管理、识别新节点何时离开或连接、集群、节点实时状态等等。8、数据传输的事务定义有哪三种?和 MQTT 的事务定义一样都是 3 种。(1)最多一次: 消息不会被重复发送,最多被传输一次,但也有可能一次不传输(2)最少一次: 消息不会被漏发送,最少被传输一次,但也有可能被重复传输.(3)精确的一次(Exactly once): 不会漏传输也不会重复传输,每个消息都传输被一次而且仅仅被传输一次,这是大家所期望的### 9、Kafka 判断一个节点是否还活着有那两个条件?

(1)节点必须可以维护和 ZooKeeper 的连接,Zookeeper 通过心跳机制检查每个节点的连接(2)如果节点是个 follower,他必须能及时的同步 leader 的写操作,延时不能太久### 10、Kafka 与传统 MQ 消息系统之间有三个关键区别

(1).Kafka 持久化日志,这些日志可以被重复读取和无限期保留(2).Kafka 是一个分布式系统:它以集群的方式运行,可以灵活伸缩,在内部通过复制数据提升容错能力和高可用性(3).Kafka 支持实时的流式处理### 11、讲一讲 kafka 的 ack 的三种机制

request.required.acks 有三个值 0 1 -1(all)0:生产者不会等待 broker 的 ack,这个延迟最低但是存储的保证最弱当 server 挂掉的时候就会丢数据。1:服务端会等待 ack 值 leader 副本确认接收到消息后发送 ack 但是如果 leader挂掉后他不确保是否复制完成新 leader 也会导致数据丢失。-1(all):服务端会等所有的 follower 的副本受到数据后才会受到 leader 发出的ack,这样数据不会丢失### 12、消费者如何不自动提交偏移量,由应用提交?

将 auto.commit.offset 设为 false,然后在处理一批消息后 commitSync() 或者异步提交 commitAsync()即:

1
2
3
4
5
6
7
8
复制代码ConsumerRecords<> records = consumer.poll();
for (ConsumerRecord<> record : records){
。。。
tyr{
consumer.commitSync()
}
。。。
}

13、消费者故障,出现活锁问题如何解决?

出现“活锁”的情况,是它持续的发送心跳,但是没有处理。为了预防消费者在这种情况下一直持有分区,我们使用 max.poll.interval.ms 活跃检测机制。 在此基础上,如果你调用的 poll 的频率大于最大间隔,则客户端将主动地离开组,以便其他消费者接管该分区。 发生这种情况时,你会看到 offset 提交失败(调用commitSync()引发的 CommitFailedException)。这是一种安全机制,保障只有活动成员能够提交 offset。所以要留在组中,你必须持续调用 poll。消费者提供两个配置设置来控制 poll 循环:max.poll.interval.ms:增大 poll 的间隔,可以为消费者提供更多的时间去处理返回的消息(调用 poll(long)返回的消息,通常返回的消息都是一批)。缺点是此值越大将会延迟组重新平衡。max.poll.records:此设置限制每次调用 poll 返回的消息数,这样可以更容易的预测每次 poll 间隔要处理的最大值。通过调整此值,可以减少 poll 间隔,减少重新平衡分组的对于消息处理时间不可预测地的情况,这些选项是不够的。 处理这种情况的推荐方法是将消息处理移到另一个线程中,让消费者继续调用 poll。 但是必须注意确保已提交的 offset 不超过实际位置。另外,你必须禁用自动提交,并只有在线程完成处理后才为记录手动提交偏移量(取决于你)。 还要注意,你需要 pause 暂停分区,不会从 poll 接收到新消息,让线程处理完之前返回的消息(如果你的处理能力比拉取消息的慢,那创建新线程将导致你机器内存溢出)。### 14、如何控制消费的位置

kafka 使用 seek(TopicPartition, long)指定新的消费位置。用于查找服务器保留的最早和最新的 offset 的特殊的方法也可用(seekToBeginning(Collection) 和seekToEnd(Collection))### 15、kafka 分布式(不是单机)的情况下,如何保证消息的顺序消费?

Kafka 分布式的单位是 partition,同一个 partition 用一个 write ahead log 组织,所以可以保证 FIFO 的顺序。不同 partition 之间不能保证顺序。但是绝大多数用户都可以通过 message key 来定义,因为同一个 key 的 message 可以保证只发送到同一个 partition。Kafka 中发送 1 条消息的时候,可以指定(topic, partition, key) 3 个参数。partiton 和 key 是可选的。如果你指定了 partition,那就是所有消息发往同 1个 partition,就是有序的。并且在消费端,Kafka 保证,1 个 partition 只能被1 个 consumer 消费。或者你指定 key( 比如 order id),具有同 1 个 key 的所有消息,会发往同 1 个 partition。### 16、kafka 的高可用机制是什么?

这个问题比较系统,回答出 kafka 的系统特点,leader 和 follower 的关系,消息读写的顺序即可。### 17、kafka 如何减少数据丢失

Kafka到底会不会丢数据(data loss)? 通常不会,但有些情况下的确有可能会发生。下面的参数配置及Best practice列表可以较好地保证数据的持久性(当然是trade-off,牺牲了吞吐量)。* block.on.buffer.full = true

  • acks = all
  • retries = MAX_VALUE
  • max.in.flight.requests.per.connection = 1
  • 使用KafkaProducer.send(record, callback)
  • callback逻辑中显式关闭producer:close(0)
  • unclean.leader.election.enable=false
  • replication.factor = 3
  • min.insync.replicas = 2
  • replication.factor > min.insync.replicas
  • enable.auto.commit=false
  • 消息处理完成之后再提交位移

18、kafka 如何不消费重复数据?比如扣款,我们不能重复的扣。

其实还是得结合业务来思考,我这里给几个思路:比如你拿个数据要写库,你先根据主键查一下,如果这数据都有了,你就别插入了,update 一下好吧。比如你是写 Redis,那没问题了,反正每次都是 set,天然幂等性。比如你不是上面两个场景,那做的稍微复杂一点,你需要让生产者发送每条数据的时候,里面加一个全局唯一的 id,类似订单 id 之类的东西,然后你这里消费到了之后,先根据这个 id 去比如 Redis 里查一下,之前消费过吗?如果没有消费过,你就处理,然后这个 id 写 Redis。如果消费过了,那你就别处理了,保证别重复处理相同的消息即可。比如基于数据库的唯一键来保证重复数据不会重复插入多条。因为有唯一键约束了,重复数据插入只会报错,不会导致数据库中出现脏数据。欢迎大家关注我的公众号【程序员追风】,2019年多家公司java面试题整理了1000多道400多页pdf文档,文章都会在里面更新,整理的资料也会放在里面。

最后

欢迎大家一起交流,喜欢文章记得关注我点个赞哟,感谢支持!

本文转载自: 掘金

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

日常工作中最容易犯的几个并发错误

发表于 2019-12-22

前言

列举大家平时在工作中最容易犯的几个并发错误,都是在实际项目代码中看到的鲜活例子,希望对大家有帮助。

First Blood

线上总是出现:ERROR 1062 (23000) Duplicate entry ‘xxx’ for key ‘yyy’,我们来看一下有问题的这段代码:

1
2
3
4
5
6
7
8
复制代码UserBindInfo info = selectFromDB(userId);
if(info == null){
info = new UserBindInfo(userId,deviceId);
insertIntoDB(info);
}else{
info.setDeviceId(deviceId);
updateDB(info);
}

在并发情况下,第一步判断都为空,就会有2个或者多个线程进入插入数据库操作,
这时候就出现了同一个ID插入多次。

正确处理姿势:

1
复制代码insert into UserBindInfo values(#{userId},#{deviceId}) on duplicate key update deviceId=#{deviceId}多次的情况,导致插入失败。

一般情况下,可以用insert…on duplicate key update… 解决这个问题。

注意: 如果UserBindInfo表存在主键以及一个以上的唯一索引,在并发情况下,使用insert…on duplicate key,可能会产生死锁(Mysql5.7),可以这样处理:

1
2
3
4
5
复制代码try{
UserBindInfoMapper.insertIntoDB(userBindInfo);
}catch(DuplicateKeyException ex){
UserBindInfoMapper.update(userBindInfo);
}

Double Kill

小心你的全局变量,如下面这段代码:

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

private static final SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");

public static void main(String[] args) throws InterruptedException {
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(10, 100, 1, TimeUnit.MINUTES, new LinkedBlockingQueue<>(1000));

while (true){
threadPoolExecutor.execute(()->{
String dateString = sdf.format(new Date());
try {
Date parseDate = sdf.parse(dateString);
String dateString2 = sdf.format(parseDate);
System.out.println(dateString.equals(dateString2));
} catch (ParseException e) {
e.printStackTrace();
}
});
}

}

}

可以看到有异常抛出

全局变量的SimpleDateFormat,在并发情况下,存在安全性问题,阿里Java规约明确要求谨慎使用它。

除了SimpleDateFormat,其实很多时候,面对全局变量,我们都需要考虑并发情况是否存在问题,如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
复制代码@Component
public class Test {

public static List<String> desc = new ArrayList<>();

public List<String> getDescByUserType(int userType) {
if (userType == 1) {
desc.add("普通会员不可以发送和查看邮件,请购买会员");
return desc;
} else if (userType == 2) {
desc.add("恭喜你已经是VIP会员,尽情的发邮件吧");
return desc;
}else {
desc.add("你的身份未知");
return desc;
}
}
}

因为desc是全局变量,在并发情况下,请求getDescByUserType方法,得到的可能并不是你想要的结果。

Trible Kill

假设现在有如下业务:控制同一个用户访问某个接口的频率不能小于5秒。一般很容易想到使用redis的 setnx操作来控制并发访问,于是有以下代码:

1
2
3
4
5
6
复制代码if(RedisOperation.setnx(userId, 1)){
RedisOperation.expire(userId,5,TimeUnit.SECONDS));
//执行正常业务逻辑
}else{
return “访问过于频繁”;
}

假设执行完setnx操作,还没来得及设置expireTime,机器重启或者突然崩溃,将会发生死锁。该用户id,后面执行setnx永远将为false,这可能让你永远损失那个用户。

那么怎么解决这个问题呢,可以考虑用SET key value NX EX max-lock-time ,它是一种在 Redis 中实现锁的方法,是原子性操作,不会像以上代码分两步执行,先set再expire,它是一步到位。

客户端执行以上的命令:

  • 如果服务器返回 OK ,那么这个客户端获得锁。
  • 如果服务器返回 NIL ,那么客户端获取锁失败,可以在稍后再重试。
  • 设置的过期时间到达之后,锁将自动释放

Quadra Kill

我们看一下有关ConcurrentHashMap的一段代码,如下:

1
2
3
4
5
6
7
8
9
复制代码//全局变量
Map<String, Integer> map = new ConcurrentHashMap();

Integer value = count.get(k);
if(value == null){
map.put(k,1);
}else{
map.put(k,value+1);
}

假设两条线程都进入 value==null,这一步,得出的结果是不是会变小?OK,客官先稍作休息,闭目养神一会,我们验证一下,请看一个demo:

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
复制代码  public static void main(String[] args)  {
for (int i = 0; i < 1000; i++) {
testConcurrentMap();
}
}
private static void testConcurrentMap() {
final Map<String, Integer> count = new ConcurrentHashMap<>();
ExecutorService executorService = Executors.newFixedThreadPool(2);
final CountDownLatch endLatch = new CountDownLatch(2);
Runnable task = ()-> {
for (int i = 0; i < 5; i++) {
Integer value = count.get("k");
if (null == value) {
System.out.println(Thread.currentThread().getName());
count.put("k", 1);
} else {
count.put("k", value + 1);
}
}
endLatch.countDown();
};

executorService.execute(task);
executorService.execute(task);

try {
endLatch.await();
if (count.get("k") < 10) {
System.out.println(count);
}
} catch (Exception e) {
e.printStackTrace();
}

表面看,运行结果应该都是10对吧,好的,我们再看运行结果
:

运行结果出现了5,所以这样实现是有并发问题的,那么正确的实现姿势是啥呢?

1
2
3
4
5
6
7
8
9
复制代码Map<K,V> map = new ConcurrentHashMap(); 
V v = map.get(k);
if(v == null){
v = new V();
V old = map. putIfAbsent(k,v);
if(old != null){
v = old;
}
}

可以考虑使用putIfAbsent解决这个问题

(1)如果key是新的记录,那么会向map中添加该键值对,并返回null。

(2)如果key已经存在,那么不会覆盖已有的值,返回已经存在的值

我们再来看看以下代码以及运行结果:

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
复制代码 public static void main(String[] args)  {
for (int i = 0; i < 1000; i++) {
testConcurrentMap();
}
}

private static void testConcurrentMap() {
ExecutorService executorService = Executors.newFixedThreadPool(2);
final Map<String, AtomicInteger> map = Maps.newConcurrentMap();
final CountDownLatch countDownLatch = new CountDownLatch(2);

Runnable task = ()-> {
AtomicInteger oldValue;
for (int i = 0; i < 5; i++) {
oldValue = map.get("k");
if (null == oldValue) {
AtomicInteger initValue = new AtomicInteger(0);
oldValue = map.putIfAbsent("k", initValue);
if (oldValue == null) {
oldValue = initValue;
}
}
oldValue.incrementAndGet();
}
countDownLatch.countDown();
};

executorService.execute(task);
executorService.execute(task);

try {
countDownLatch.await();
System.out.println(map);
} catch (Exception e) {
e.printStackTrace();
}
}

Penta Kill

现有如下业务场景:用户手上有一张现金券,可以兑换相应的现金,

错误示范一

1
2
3
4
5
6
复制代码if(isAvailable(ticketId){
1、给现金增加操作
2、deleteTicketById(ticketId)
}else{
return “没有可用现金券”
}

解析: 假设有两条线程A,B兑换现金,执行顺序如下:

  • 1.线程A加现金
  • 2.线程B加现金
  • 3.线程A删除票标志
  • 4.线程B删除票标志

显然,这样有问题了,已经给用户加了两次现金了。

错误示范2

1
2
3
4
5
6
复制代码if(isAvailable(ticketId){
1、deleteTicketById(ticketId)
2、给现金增加操作
}else{
return “没有可用现金券”
}

并发情况下,如果一条线程,第一步deleteTicketById删除失败了,也会多添加现金。

正确处理方案

1
2
3
4
5
复制代码if(deleteAvailableTicketById(ticketId) == 1){
1、给现金增加操作
}else{
return “没有可用现金券”
}

个人公众号

  • 如果你是个爱学习的好孩子,可以关注我公众号,一起学习讨论。
  • 如果你觉得本文有哪些不正确的地方,可以评论,也可以关注我公众号,私聊我,大家一起学习进步哈。

本文转载自: 掘金

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

谈谈Java反射:从入门到实践,再到原理

发表于 2019-12-21

前言

反射是Java底层框架的灵魂技术,学习反射非常有必要,本文将从入门概念,到实践,再到原理讲解反射,希望对大家有帮助。

反射理解

官方解析

Oracle 官方对反射的解释是:

1
2
3
4
5
6
复制代码Reflection is commonly used by programs which require the ability to examine or
modify the runtime behavior of applications running in the Java virtual machine.
This is a relatively advanced feature and should be used only by developers who
have a strong grasp of the fundamentals of the language. With that caveat in
mind, reflection is a powerful technique and can enable applications to perform
operations which would otherwise be impossible.

Java 的反射机制是指在运行状态中,对于任意一个类都能够知道这个类所有的属性和方法; 并且对于任意一个对象,都能够调用它的任意一个方法;这种动态获取信息以及动态调用对象方法的功能成为Java语言的反射机制。

白话理解

正射

万物有阴必有阳,有正必有反。既然有反射,就必有“正射”。

那么正射是什么呢?

我们在编写代码时,当需要使用到某一个类的时候,都会先了解这个类是做什么的。然后实例化这个类,接着用实例化好的对象进行操作,这就是正射。

1
2
复制代码Student student = new Student();
student.doHomework("数学");

反射

反射就是,一开始并不知道我们要初始化的类对象是什么,自然也无法使用 new 关键字来创建对象了。

1
2
3
4
5
复制代码 Class clazz = Class.forName("reflection.Student");
Method method = clazz.getMethod("doHomework", String.class);
Constructor constructor = clazz.getConstructor();
Object object = constructor.newInstance();
method.invoke(object, "语文");

正射与反射对比

以上两段代码,执行效果是一样的,如图

但是,其实现的过程还是有很大的差别的:

  • 第一段代码在未运行前就已经知道了要运行的类是Student;
  • 第二段代码则是到整个程序运行的时候,从字符串reflection.Student,才知道要操作的类是Student。

结论

反射就是在运行时才知道要操作的类是什么,并且可以在运行时获取类的完整构造,并调用对应的方法。

Class 对象理解

要理解Class对象,我们先来了解一下RTTI吧。
RTTI(Run-Time Type Identification)运行时类型识别,其作用是在运行时识别一个对象的类型和类的信息。

Java是如何让我们在运行时识别对象和类的信息的?主要有两种方式:
一种是传统的RRTI,它假定我们在编译期已知道了所有类型。
另一种是反射机制,它允许我们在运行时发现和使用类的信息。

每个类都有一个Class对象,每当编译一个新类就产生一个Class对象(更恰当地说,是被保存在一个同名的.class文件中)。比如创建一个Student类,那么,JVM就会创建一个Student对应Class类的Class对象,该Class对象保存了Student类相关的类型信息。

Class类的对象作用是运行时提供或获得某个对象的类型信息

反射的基本使用

获取 Class 类对象

获取反射中的Class对象有三种方法。

第一种,使用 Class.forName 静态方法。

1
复制代码Class class1 = Class.forName("reflection.TestReflection");

第二种,使用类的.class 方法

1
复制代码Class class2 = TestReflection.class;

第三种,使用实例对象的 getClass() 方法。

1
2
复制代码TestReflection testReflection = new TestReflection();
Class class3 = testReflection.getClass();

反射创造对象,获取方法,成员变量,构造器

本小节学习反射的基本API用法,如获取方法,成员变量等。

反射创造对象

通过反射创建类对象主要有两种方式:

实例代码:

1
2
3
4
5
6
7
8
9
复制代码//方式一
Class class1 = Class.forName("reflection.Student");
Student student = (Student) class1.newInstance();
System.out.println(student);

//方式二
Constructor constructor = class1.getConstructor();
Student student1 = (Student) constructor.newInstance();
System.out.println(student1);

运行结果:

反射获取类的构造器

看一个例子吧:

1
2
3
4
5
复制代码Class class1 = Class.forName("reflection.Student");
Constructor[] constructors = class1.getDeclaredConstructors();
for (int i = 0; i < constructors.length; i++) {
System.out.println(constructors[i]);
}

反射获取类的成员变量

看demo:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
复制代码// student 一个私有属性age,一个公有属性email
public class Student {

private Integer age;

public String email;
}

public class TestReflection {
public static void main(String[] args) throws ClassNotFoundException, NoSuchFieldException {
Class class1 = Class.forName("reflection.Student");
Field email = class1.getField("email");
System.out.println(email);
Field age = class1.getField("age");
System.out.println(age);
}
}

运行结果:

即getField(String name) 根据参数变量名,返回一个具体的具有public属性的成员变量,如果该变量不是public属性,则报异常。

反射获取类的方法

demo

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
复制代码public class Student {

private void testPrivateMethod() {

}
public void testPublicMethod() {

}
}

public class TestReflection {
public static void main(String[] args) throws ClassNotFoundException, NoSuchFieldException {
Class class1 = Class.forName("reflection.Student");

Method[] methods = class1.getMethods();
for (int i = 0; i < methods.length; i++) {
System.out.println(methods[i]);
}
}
}

运行结果:

反射的实现原理

通过上一小节学习,我们已经知道反射的基本API用法了。接下来,跟着一个例子,学习反射方法的执行链路。

1
2
3
4
5
6
7
8
9
10
11
12
13
复制代码public class TestReflection {
public static void main(String[] args) throws Exception {
Class clazz = Class.forName("reflection.TestReflection");
Method method = clazz.getMethod("target", String.class);
method.invoke(null, "666");
}

public static void target(String str) {
//打印堆栈信息
new Exception("#" +str).printStackTrace();
System.out.println("invoke target method");
}
}

堆栈信息反映出反射调用链路:

1
2
3
4
5
6
7
8
复制代码java.lang.Exception: #666
invoke target method
at reflection.TestReflection.target(TestReflection.java:17)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:498)
at reflection.TestReflection.main(TestReflection.java:11)

invoke方法执行时序图

我们跟着反射链路去看一下源码,先看Method的invoke方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
复制代码public Object invoke(Object obj, Object... args)
throws IllegalAccessException, IllegalArgumentException,
InvocationTargetException
{
//校验权限
if (!override) {
if (!Reflection.quickCheckMemberAccess(clazz, modifiers)) {
Class<?> caller = Reflection.getCallerClass();
checkAccess(caller, clazz, obj, modifiers);
}
}
MethodAccessor ma = methodAccessor; // read volatile
if (ma == null) {
ma = acquireMethodAccessor(); //获取MethodAccessor
}
//返回MethodAccessor.invoke
return ma.invoke(obj, args);
}

由上可知道,Method 的 invoke 方法,其实是返回接口MethodAccessor的invoke方法。MethodAccessor接口有三个实现类,到底调用的是哪个类的 invoke 方法呢?

进入acquireMethodAccessor方法,可以看到MethodAccessor由ReflectionFactory 的 newMethodAccessor方法决定。

再进ReflectionFactory的newMethodAccessor方法,我们可以看到返回的是DelegatingMethodAccessorImpl对象,也就是说调用的是它的invoke方法。

再看DelegatingMethodAccessorImpl的invoke方法

DelegatingMethodAccessorImpl的invoke方法返回的是MethodAccessorImpl的invoke方法,而MethodAccessorImpl的invoke方法,由它的子类NativeMethodAccessorImpl重写,这时候返回的是本地方法invoke0,如下

因此,Method的invoke方法,是由本地方法invoke0决定的,再底层就是c++相关了,有兴趣的朋友可以继续往下研究。

反射的一些应用以及问题

反射应用

反射是Java框架的灵魂技术,很多框架都使用了反射技术,如spring,Mybatis,Hibernate等。

JDBC 的数据库的连接

在JDBC连接数据库中,一般包括加载驱动,获得数据库连接等步骤。而加载驱动,就是引入相关Jar包后,通过Class.forName() 即反射技术,加载数据库的驱动程序。

Spring 框架的使用

Spring 通过 XML 配置模式装载 Bean,也是反射的一个典型例子。

装载过程:

  • 将程序内XML 配置文件加载入内存中
  • Java类解析xml里面的内容,得到相关字节码信息
  • 使用反射机制,得到Class实例
  • 动态配置实例的属性,使用

这样做当然是有好处的:

不用每次都去new实例了,并且可以修改配置文件,比较灵活。

反射存在的问题

性能问题

java反射的性能并不好,原因主要是编译器没法对反射相关的代码做优化。
有兴趣的朋友,可以看一下这个文章java-reflection-why-is-it-so-slow

安全问题

我们知道单例模式的设计过程中,会强调将构造器设计为私有,因为这样可以防止从外部构造对象。但是反射可以获取类中的域、方法、构造器,修改访问权限。所以这样并不一定是安全的。

看个例子吧,通过反射使用私有构造器实例化。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
复制代码public class Student {
private String name;
private Student(String name) {
System.out.println("我是私有构造器,我被实例化了");
this.name = name;
}
public void doHomework(String subject) {
System.out.println("我的名字是" + name);
System.out.println("我在做"+subject+"作业");
}
}
public class TestReflection {
public static void main(String[] args) throws Exception {
Class clazz = Class.forName("reflection.Student");
// 获取私有构造方法对象
Constructor constructor = clazz.getDeclaredConstructor(String.class);
// true指示反射的对象在使用时应该取消Java语言访问检查。
constructor.setAccessible(true);
Student student = (Student) constructor.newInstance("jay@huaxiao");
student.doHomework("数学");
}
}

运行结果:

显然,反射不管你是不是私有,一样可以调用。
所以,使用反射通常需要程序的运行没有安全限制。如果一个程序对安全性有强制要求,最好不要使用反射啦。
参考与感谢


  • 反射的实现原理
  • 通过反射获取私有构造方法并使用
  • 大白话说Java反射:入门、使用、原理
  • 设计模式之单例模式六(防反射攻击)
  • Reflection:Java反射机制的应用场景
  • 深入理解Java类型信息(Class对象)与反射机制
  • 《Java编程思想》

个人公众号

  • 如果你是个爱学习的好孩子,可以关注我公众号,一起学习讨论。
  • 如果你觉得本文有哪些不正确的地方,可以评论,也可以关注我公众号,私聊我,大家一起学习进步哈。

本文转载自: 掘金

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

一文带你看懂什么是RESTful API

发表于 2019-12-21

一、什么是API

要想知道什么是RESTful API,我们得先知道什么是API.

API(Application Programming Interface,应用程序接口)是一些预先定义的函数,或指软件系统不同组成部分衔接的约定。 [1] 目的是提供应用程序与开发人员基于某软件或硬件得以访问一组例程的能力,而又无需访问原码,或理解内部工作机制的细节。(引用自百度百科)

举个例子:比如我们去肯德基买个汉堡,不需要知道这个汉堡是怎么做出来的,你付过钱之后就能得到一个汉堡。

肯德基就是一个服务器,你就是一个客户端,钱就是你向肯德基请求获得汉堡的参数。

比如,你如果要用水,你自己不必生产水,你只需要调用水厂生产好的水就行了,水厂会把水送到你家的管道里面,你只要打开水龙头就行了,水就会出来。你打开水龙的过程就是在调用水厂的API.

调用API就是一手交钱一手交货的过程,制作汉堡则是API背后的工作。

ps:上面是说给不懂计算机的人解释API是什么。

用计算机的术语来说就是,程序使用写代码的方式访问其他人写的代码的一种渠道就是API.

比如你的程序想要支付宝的支付功能 ,你如果想要自己实现支付功能,你得去找银行对接啊,你得申请支付牌照,然后再写程序,一连串下来,你自己都可以成立一个金融公司了,但是现在你不想成立一个金融公司你只想用支付宝的支付功能,怎么办呢,你可以使用支付宝的接口,在你的程序里面调用支付功能的时候,实际上就是请求支付宝的支付功能 ,你不必考虑支付宝的支付功能是怎么做的,是用什么语言实现的,你只要按照规范请求支付宝的支付请求就行了。这就是调用支付宝的API.

二、什么是REST

REST即表述性状态传递(英文:Representational State Transfer,简称REST)是Roy Fielding博士在2000年他的博士论文中提出来的一种软件架构风格。它是一种针对网络应用的设计和开发方式,可以降低开发的复杂性,提高系统的可伸缩性。(引用自百度百科)

表述性状态传递,这是个啥,谁能听得懂啊,这个中英文直译我吐了。

  • REST描述的是在网络中client和server的一种交互的形式,REST不是一种协议本身没有太大的作用,实用的是如何设计 RESTful API(REST风格的接口)

为什么要使用RESTful结构呢

以前的网页都是前端和后端融在一起的,比如之前的JSP等。在之前的PC时代问题没有啥问题,但是近年来移动互联网快速发展,各种前端框架特别多,如果我们还是前后端不分离的话,一个是学习成本特别高,一个是代码的重复率太高导致我们做了很多重复的东西,代码的复用率不高,用接口的方式可以让代码的复用率变的高些,如下图所示

1576638176196.png

为什么不用普通的接口方式,比如你要从你的网站上面得到一个城市的天气,以前有可能你得这么设计

https://xxx/getWeather?city=深圳

你这样就得传两个参数给后台,这样看起来就很臃肿。

用RESTful方式设计接口你可能会这么设计

GET https://xxx/weathers/深圳

这样只需要传一个参数给后台就可以了,这样看起来很简洁,而且我们的URI中使用的是名词,而不是动词。通过HTTP动词来实现资源的跳转。具体实现方式下面说。

以上就是我们为什么要使用RESTful API结构的原因。

三、如何设计一个RESTful API结构的系统呢。

  1. URI里面使用的是名词而且不是动词,推荐使用复数,通过HTTP动词来实现资源的跳转。
* 错误的


`/getOrders`


`listCitys`


`/getWeathers?city=深圳`
* 正确的


`GET /orders/1` :返回订单编号为1的订单


`POST /orders` :增加一个订单


`Delete /orders/1` :删除一个订单编号为1的订单


`PUT /orders/1` :更新订单编号为1的订单
  1. 保证方法里面只做一件事,不会对资源状态有所改变。比如下面这样是不允许的

GET /updateOrder?id=1

  1. 使用正确的HTTP Status Code 表示返回的请求状态。比如

{"code":"200"}

以上就是如何设计一个简单的RESTful API结构系统的示例。

Tips:

前后台的数据传输可以用json,可以用xml传输,我还是比较倾向于json传输比较方便

比如请求一个城市的温度,用xml返回结果是这样

1
2
3
4
5
复制代码<city>   
<name>深圳</name>
<temperature>26</temperature>
<code>200</code>
</city>

用json返回就是这样

1
2
3
4
5
6
7
8
复制代码{
"city":
{
"name":"深圳",
"temperature":"26",
"code":200
}
}

本文转载自: 掘金

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

图解golang里面的读写锁实现与核心原理分析了解编程语言背

发表于 2019-12-20

基础筑基

读写锁的特点

读写锁区别与互斥锁的主要区别就是读锁之间是共享的,多个goroutine可以同时加读锁,但是写锁与写锁、写锁与读锁之间则是互斥的

写锁饥饿问题

因为读锁是共享的,所以如果当前已经有读锁,那后续goroutine继续加读锁正常情况下是可以加锁成功,但是如果一直有读锁进行加锁,那尝试加写锁的goroutine则可能会长期获取不到锁,这就是因为读锁而导致的写锁饥饿问题

基于高低位与等待队列的实现

image.png

在说golang之前介绍一种JAVA里面的实现,在JAVA中ReentrantReadWriteLock实现采用一个state的高低位来进行读写锁的计数,其中高16位存储读的计数,低16位存储写的计数,并配合一个AQS来实现排队等待机制,同时AQS中的每个waiter都会有一个status,用来标识自己的状态
golang的读写锁的实现
=============

成员变量

image.png

结构体

1
2
3
4
5
6
7
复制代码type RWMutex struct {
w Mutex // held if there are pending writers
writerSem uint32 // 用于writer等待读完成排队的信号量
readerSem uint32 // 用于reader等待写完成排队的信号量
readerCount int32 // 读锁的计数器
readerWait int32 // 等待读锁释放的数量
}

写锁计数

读写锁中允许加读锁的最大数量是4294967296,在go里面对写锁的计数采用了负值进行,通过递减最大允许加读锁的数量从而进行写锁对读锁的抢占

1
复制代码const rwmutexMaxReaders = 1 << 30

读锁实现

读锁加锁逻辑

image.png

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
复制代码func (rw *RWMutex) RLock() {
if race.Enabled {
_ = rw.w.state
race.Disable()
}
// 累加reader计数器,如果小于0则表明有writer正在等待
if atomic.AddInt32(&rw.readerCount, 1) < 0 {
// 当前有writer正在等待读锁,读锁就加入排队
runtime_SemacquireMutex(&rw.readerSem, false)
}
if race.Enabled {
race.Enable()
race.Acquire(unsafe.Pointer(&rw.readerSem))
}
}

读锁释放逻辑

image.png

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
复制代码func (rw *RWMutex) RUnlock() {
if race.Enabled {
_ = rw.w.state
race.ReleaseMerge(unsafe.Pointer(&rw.writerSem))
race.Disable()
}
// 如果小于0,则表明当前有writer正在等待
if r := atomic.AddInt32(&rw.readerCount, -1); r < 0 {
if r+1 == 0 || r+1 == -rwmutexMaxReaders {
race.Enable()
throw("sync: RUnlock of unlocked RWMutex")
}
// 将等待reader的计数减1,证明当前是已经有一个读的,如果值==0,则进行唤醒等待的
if atomic.AddInt32(&rw.readerWait, -1) == 0 {
// The last reader unblocks the writer.
runtime_Semrelease(&rw.writerSem, false)
}
}
if race.Enabled {
race.Enable()
}
}

写锁实现

加写锁实现

image.png

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
复制代码func (rw *RWMutex) Lock() {
if race.Enabled {
_ = rw.w.state
race.Disable()
}
// 首先获取mutex锁,同时多个goroutine只有一个可以进入到下面的逻辑
rw.w.Lock()
// 对readerCounter进行进行抢占,通过递减rwmutexMaxReaders允许最大读的数量
// 来实现写锁对读锁的抢占
r := atomic.AddInt32(&rw.readerCount, -rwmutexMaxReaders) + rwmutexMaxReaders
// 记录需要等待多少个reader完成,如果发现不为0,则表明当前有reader正在读取,当前goroutine
// 需要进行排队等待
if r != 0 && atomic.AddInt32(&rw.readerWait, r) != 0 {
runtime_SemacquireMutex(&rw.writerSem, false)
}
if race.Enabled {
race.Enable()
race.Acquire(unsafe.Pointer(&rw.readerSem))
race.Acquire(unsafe.Pointer(&rw.writerSem))
}
}

释放写锁

image.png

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
复制代码func (rw *RWMutex) Unlock() {
if race.Enabled {
_ = rw.w.state
race.Release(unsafe.Pointer(&rw.readerSem))
race.Disable()
}

// 将reader计数器复位,上面减去了一个rwmutexMaxReaders现在再重新加回去即可复位
r := atomic.AddInt32(&rw.readerCount, rwmutexMaxReaders)
if r >= rwmutexMaxReaders {
race.Enable()
throw("sync: Unlock of unlocked RWMutex")
}
// 唤醒所有的读锁
for i := 0; i < int(r); i++ {
runtime_Semrelease(&rw.readerSem, false)
}
// 释放mutex
rw.w.Unlock()
if race.Enabled {
race.Enable()
}
}

关键核心机制

写锁对读锁的抢占

加写锁的抢占

1
2
复制代码	// 在加写锁的时候通过将readerCount递减最大允许加读锁的数量,来实现对加读锁的抢占
r := atomic.AddInt32(&rw.readerCount, -rwmutexMaxReaders) + rwmutexMaxReaders

加读锁的抢占检测

1
2
3
4
5
6
复制代码// 如果没有写锁的情况下读锁的readerCount进行Add后一定是一个>0的数字,这里通过检测值为负数
//就实现了读锁对写锁抢占的检测
if atomic.AddInt32(&rw.readerCount, 1) < 0 {
// A writer is pending, wait for it.
runtime_SemacquireMutex(&rw.readerSem, false)
}

写锁抢占读锁后后续的读锁就会加锁失败,但是如果想加写锁成功还要继续对已经加读锁成功的进行等待

1
2
3
4
复制代码	if r != 0 && atomic.AddInt32(&rw.readerWait, r) != 0 {
// 写锁发现需要等待的读锁释放的数量不为0,就自己自己去休眠了
runtime_SemacquireMutex(&rw.writerSem, false)
}

写锁既然休眠了,则必定要有一种唤醒机制其实就是每次释放锁的时候,当检查到有加写锁的情况下,就递减readerWait,并由最后一个释放reader lock的goroutine来实现唤醒写锁

1
2
3
4
复制代码		if atomic.AddInt32(&rw.readerWait, -1) == 0 {
// The last reader unblocks the writer.
runtime_Semrelease(&rw.writerSem, false)
}

写锁的公平性

在加写锁的时候必须先进行mutex的加锁,而mutex本身在普通模式下是非公平的,只有在饥饿模式下才是公平的

1
复制代码	rw.w.Lock()

写锁与读锁的公平性

在加读锁和写锁的工程中都使用atomic.AddInt32来进行递增,而该指令在底层是会通过LOCK来进行CPU总线加锁的,因此多个CPU同时执行readerCount其实只会有一个成功,从这上面看其实是写锁与读锁之间是相对公平的,谁先达到谁先被CPU调度执行,进行LOCK锁cache line成功,谁就加成功锁

可见性与原子性问题

在并发场景中特别是JAVA中通常会提到并发里面的两个问题:可见性与内存屏障、原子性, 其中可见性通常是指在cpu多级缓存下如何保证缓存的一致性,即在一个CPU上修改了了某个数据在其他的CPU上不会继续读取旧的数据,内存屏障通常是为了CPU为了提高流水线性能,而对指令进行重排序而来,而原子性则是指的执行某个操作的过程的不可分割

底层实现的CPU指令

go里面并没有volatile这种关键字,那如何能保证上面的AddInt32这个操作可以满足上面的两个问题呢, 其实关键就在于底层的2条指令,通过LOCK指令配合CPU的MESI协议,实现可见性和内存屏障,同时通过XADDL则用来保证原子性,从而解决上面提到的可见性与原子性问题

1
2
3
复制代码	// atomic/asm_amd64.s TEXT runtime∕internal∕atomic·Xadd(SB)
LOCK
XADDL AX, 0(BX)

更多文章可以访问www.sreguide.com

本篇文章由一文多发平台ArtiPub自动发布

本文转载自: 掘金

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

Excel工具-SXSSFWorkbook 研究与低内存占用

发表于 2019-12-20

今天项目中有个需要 Excel 导出的需求。看了下同事用了 SXSSFWorkbook 。之前没怎么用到这个组件。既然这次用了就简单分析一下。
POI提供了HSSF、XSSF以及SXSSF三种方式操作Excel。他们的区别如下:

1
2
3
复制代码HSSF:是操作Excel97-2003版本,扩展名为.xls。
XSSF:是操作Excel2007版本开始,扩展名为.xlsx。
SXSSF:是在XSSF基础上,POI3.8版本开始提供的一种支持低内存占用的操作方式,扩展名为.xlsx。

本文的重点就是分析一下,SXSSF如何支持低内存占用的。
先说结论:
SXSSF 指定了rowAccessWindowSize ,每个sheet 对应一个临时文件,当行数大于rowAccessWindowSize 时,就会向临时文件中flush, 这样就保证了内存的低占用率。当行创建完,直接从临时文件中写入到Excel中。
有一点需要注意:
像单元格合并类似的操作是纯内存操作,如果项目中想一次合并多行时,要注意随时观察自己机器内容的使用情况,避免出现OOM。

1 来个demo

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
复制代码        // 内存中保持100条数据, 超出的部分刷新到磁盘上
SXSSFWorkbook wb = new SXSSFWorkbook(100);

Sheet sh = wb.createSheet();
for(int rownum = 0; rownum < 1000; rownum++){
Row row = sh.createRow(rownum);
for(int cellnum = 0; cellnum < 10; cellnum++){
// 创建行,在这儿根据当前行数跟rowAccessWindowSize 比较,来决定从内存写入文件中。
Cell cell = row.createCell(cellnum);
String address = new CellReference(cell).formatAsString();
cell.setCellValue(address);
}

}

// rownum < 900 的数据被刷新到磁盘,不能被随机访问
for(int rownum = 0; rownum < 900; rownum++){
Assert.assertNull(sh.getRow(rownum));
}

// 最后的100条数据仍然在内存中,可以随机访问
for(int rownum = 900; rownum < 1000; rownum++){
Assert.assertNotNull(sh.getRow(rownum));
}
// 从临时文件写入Excel 文件
FileOutputStream out = new FileOutputStream("d:\\sxssf.xlsx");
wb.write(out);
out.close();

// 从磁盘上释放临时文件
wb.dispose();

2 主要分析点

2.1 创建SXSSFWorkbook

如demo所示, SXSSFWorkbook wb = new SXSSFWorkbook(100); 中指定了rowAccessWindowSize 为100 ,也就是说
会在内存中缓存 rowAccessWindowSize 行数据。当行数超过 rowAccessWindowSize ,则会从内存输入到临时文件中。

临时文件的生成在 2.2 创建Sheet部分 讲一下。超过阈值刷入临时文件在2.3 创建row 部分讲解。

2.2 创建Sheet

如demo所示,Sheet sh = wb.createSheet(); 创建了Sheet. 那么创建过程中,主要的功能是创建了一个临时文件。每个sheet 一个临时文件。废话不多说,我们来看下createSheet的实现。

1
2
3
复制代码public SXSSFSheet createSheet() {
return this.createAndRegisterSXSSFSheet(this._wb.createSheet());
}

createAndRegisterSXSSFSheet 中最核心的就是 sxSheet = new SXSSFSheet(this, xSheet);。那我们来看下这个函数:

1
2
3
4
5
6
7
复制代码public SXSSFSheet(SXSSFWorkbook workbook, XSSFSheet xSheet) throws IOException {
this._workbook = workbook;
this._sh = xSheet;
this._writer = workbook.createSheetDataWriter(); // 这儿创建了临时文件。
this.setRandomAccessWindowSize(this._workbook.getRandomAccessWindowSize());
this._autoSizeColumnTracker = new AutoSizeColumnTracker(this);
}

createSheetDataWriter 中核心的逻辑是 SheetDataWriter。看到createTempFile ,这儿就创建了临时文件。

1
2
3
4
5
复制代码public SheetDataWriter() throws IOException {
this._numberLastFlushedRow = -1;
this._fd = this.createTempFile();
this._out = this.createWriter(this._fd);
}

关于临时文件:

前缀:poi-sxssf-sheet 后缀:.xml
存储路径:代码如下

1
2
3
4
5
6
7
8
9
10
11
12
复制代码private void createPOIFilesDirectory() throws IOException {
if (this.dir == null) {
String tmpDir = System.getProperty("java.io.tmpdir");
if (tmpDir == null) {
throw new IOException("Systems temporary directory not defined - set the -Djava.io.tmpdir jvm property!");
}

this.dir = new File(tmpDir, "poifiles");
}

this.createTempDirectory(this.dir);
}

2.3 创建row

什么时间从内存写入文件?就是在创建row时。那我们看下代码:

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
复制代码public SXSSFRow createRow(int rownum) {
int maxrow = SpreadsheetVersion.EXCEL2007.getLastRowIndex();
if (rownum >= 0 && rownum <= maxrow) {
if (rownum <= this._writer.getLastFlushedRow()) {
throw new IllegalArgumentException("Attempting to write a row[" + rownum + "] in the range [0," + this._writer.getLastFlushedRow() + "] that is already written to disk.");
} else if (this._sh.getPhysicalNumberOfRows() > 0 && rownum <= this._sh.getLastRowNum()) {
throw new IllegalArgumentException("Attempting to write a row[" + rownum + "] in the range [0," + this._sh.getLastRowNum() + "] that is already written to disk.");
} else {
SXSSFRow newRow = new SXSSFRow(this);
this._rows.put(rownum, newRow);
this.allFlushed = false;
// 这儿进行了判断,如果当前行数大于randomAccessWindowSize ,则flushRows
if (this._randomAccessWindowSize >= 0 && this._rows.size() > this._randomAccessWindowSize) {
try {
this.flushRows(this._randomAccessWindowSize);
} catch (IOException var5) {
throw new RuntimeException(var5);
}
}

return newRow;
}
} else {
throw new IllegalArgumentException("Invalid row number (" + rownum + ") outside allowable range (0.." + maxrow + ")");
}
}

判断逻辑就在这儿if (this._randomAccessWindowSize >= 0 && this._rows.size() > this._randomAccessWindowSize)。

下面几部分跟低内存占用没有关系了,只是分析一下在项目中实际用的几个步骤。

2.4 从临时文件写入最终Excel

写入Excel 主要是在workbook.write(out)。看下代码:

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 void write(OutputStream stream) throws IOException {
this.flushSheets(); // 把最后不足randomAccessWindowSize 的行数 写入sheet临时文件。
File tmplFile = TempFile.createTempFile("poi-sxssf-template", ".xlsx"); // 创建了一个tmplFile临时文件,不是sheet的临时文件哈

boolean deleted;
try {
FileOutputStream os = new FileOutputStream(tmplFile);
Throwable var5 = null;

try {
// 这儿将workbook 中所有的数据都写入刚刚创建的tmplFile临时文件中。
this._wb.write(os);
}
...
ZipSecureFile zf = new ZipSecureFile(tmplFile);
var5 = null;

try {
ZipFileZipEntrySource source = new ZipFileZipEntrySource(zf);
Throwable var7 = null;

try {
// 将tmplFile 临时文件写入到目标Excel中。
this.injectData(source, stream);
}
...
} finally {
// 删除tmplFile临时文件。 注意不是sheet的临时文件哈。
deleted = tmplFile.delete();
}

if (!deleted) {
throw new IOException("Could not delete temporary file after processing: " + tmplFile);
}
}

基本逻辑很简洁:

(1)将内存中剩余不足randomAccessSize 数目的数据,先写入sheet 临时文件。

(2)将workbook 中所有的数据(就是多个sheet临时文件)写入一个 刚刚创建的tmpl临时文件

(3)将tmpl临时文件 的数据写入目标文件即可。

2.5 删除临时文件

workbook.dispose(); 这儿的逻辑。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
复制代码public boolean dispose() {
boolean success = true;
Iterator var2 = this._sxFromXHash.keySet().iterator();
// 逐个遍历多个sheet
while(var2.hasNext()) {
SXSSFSheet sheet = (SXSSFSheet)var2.next();

try {
// 这儿的核心是dispose.
success = sheet.dispose() && success;
} catch (IOException var5) {
logger.log(5, new Object[]{var5});
success = false;
}
}

return success;
}

基本的逻辑是,遍历多个sheet, 然后对每个sheet执行dispose . dispose 的逻辑就是如果还没文件没有从sheet中输出,则先flush,然后删除sheet临时文件。

2.6 关于合并单元格的操作

单元格合并的用法:

1
2
复制代码  CellRangeAddress region0 = new CellRangeAddress(rowNum, rowNum+1, column, column);
sheet.addMergedRegion(region0);

这儿就是根据 rowNum 跟column进行合并而已。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
复制代码private int addMergedRegion(CellRangeAddress region, boolean validate) {
if (region.getNumberOfCells() < 2) {
throw new IllegalArgumentException("Merged region " + region.formatAsString() + " must contain 2 or more cells");
} else {
region.validate(SpreadsheetVersion.EXCEL2007);
if (validate) {
this.validateArrayFormulas(region);
this.validateMergedRegions(region);
}

CTMergeCells ctMergeCells = this.worksheet.isSetMergeCells() ? this.worksheet.getMergeCells() : this.worksheet.addNewMergeCells();
CTMergeCell ctMergeCell = ctMergeCells.addNewMergeCell();
ctMergeCell.setRef(region.formatAsString());
return ctMergeCells.sizeOfMergeCellArray();
}
}

3 总结

本文结合项目中用到的Excel 工具- SXSSFWorkbook ,进行了简单讲解。并针对SXSSFWorkbook 低内存占用部分进行了分析。希望对你有所帮助~
本文同步发布在简书 www.jianshu.com/p/18046332b…

4 参考文献

HSSF、XSSF和SXSSF区别以及Excel导出优化
www.cnblogs.com/pcheng/p/74…

基于流的EXCEL文件导出,SXSSFWorkbook源码解析
www.jianshu.com/p/b80a20b81…

#5 其他

(1)读取excel 数字时,默认会带一个 “.0” ,怎么解决?
my.oschina.net/henglaixuex…

本文转载自: 掘金

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

简单实用的状态机设计与实现

发表于 2019-12-18

finite-state machine 有限状态机 是表示有限个状态以及在这些状态之间的转移和动作等行为的数学计算模型,被广泛用于建模应用行为、硬件电路系统设计、软件工程,编译器、网络协议、和计算与语言的研究。

状态机的出现是为了将复杂对象的状态变化进行建模,采取工程化的方式来处理,方便理解与沟通

状态机引入

在实际开发中,也会经常遇到状态机的问题。举个简单的例子,考虑用户支付场景下订单的状态,订单状态可能经过这样的变化:待付款 –> 处理中 –> 失败/成功
在这个过程中,业务系统需要向第三方支付系统(微信、支付宝)查询订单状态,更新本地库的订单状态。

类似的场景很多,特别是在对象的状态比较多的时候,就变得越来越难以维护,同时还伴随这并发问题,一不留心就会出错。所以设计一个通用的状态机管理器就很有必要了。

状态机设计


一个良好的状态机设计,首先要有很好的通用性,能够适应很多场景,其次对于开发者友好,不用开发者关心内部实现。

由状态机的定义可知,状态机由状态转移组成,每一个状态转移包含前置状态、行为操作、后置状态组成,通常状态改变时还伴随着事件通知。

状态机实现


状态机的实现也不难,由一下几个类组成

  • StateManager :状态管理器,状态机主要逻辑的实现者,本身是一个抽象类,维护一组自动状态转换(非终态 –> 终态)和 一组状态改变监听器
  • StateTransition : 状态转换,定义了前置状态(preState)、行为操作(operation)、后置状态(operation返回值)
  • StateListener :状态变化监听器
  • StateAutoSync :状态自动同步器,内部注册所有的状态管理器,负责找到对应的状态管理器,进行状态的同步

StateManager子类实现主要有两类 LocalStateManager (内存维护状态) 和 GlobalStateManager (全局存储维护状态,如mysql/redis)

异步事件驱动

增加事件驱动,支持异步(线程池 并发)和同步两种模式


整个处理过程大致为:处理请求作为事件进入系统,由异步调度器(AsyncDispatcher) 负责调用相应的事件调度器(Event Handler)。该事件调度器处理完毕后可能会产生一个新的事件交由调度器进行分发,也可能该调度器本身就是一个有限状态机,内部处理状态的转移。

具体代码详见:github.com/VectorJin/S…

本文转载自: 掘金

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

spring-boot swagger2 设置全局token

发表于 2019-12-18

spring-boot swagger2 设置全局token,解决接口需要token验证的问题

问题背景

  • 项目中需要在header中进行校验传入token,导致swagger不能调试

具体实现

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
java复制代码@Configuration  
@EnableSwagger2
public class SwaggerConfig implements WebMvcConfigurer {

    @Bean
    public Docket createRestApi() {
        return new Docket(DocumentationType.SWAGGER_2)
            .apiInfo(apiInfo())
            .select()
            //加了ApiOperation注解的类,才生成接口文档
            .apis(RequestHandlerSelectors.withMethodAnnotation(ApiOperation.class))
            //包下的类,才生成接口文档
            .paths(PathSelectors.any())
            .build()
            .securitySchemes(security());
    }

    private ApiInfo apiInfo() {
        return new ApiInfoBuilder()
            .title("XXXX后台管理")
            .description("接口文档")
            .build();
    }

    private List<ApiKey> security() {
        return newArrayList(
            new ApiKey("token", "token", "header")
        );
    }

}

配置完成后刷新页面则会显示

image-20191218182812183

title

点击Authorize

image-20191218182838162

认证页面

填入你生成的token并按authorize则完成了全局的token校验问题,又能够愉快的联调了。

本文转载自: 掘金

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

使用Jenkins自动化部署,节省你的时间

发表于 2019-12-18

引言

我们在开发过程中经常需要修改本地代码,并提交到git或svn。要运行程序则需要将svn或git上的代码编译并部署到web服务器。这个部署如果是手动的则每次都需要执行编译命令,再手动把war文件之类拷贝到服务器,最后重启tomcat,次数多了就会显得很繁琐。Jenkins就是为了解决自动化部署而生。

我的环境:
本地windows10,服务器centOS 7。两方都安装了jdk跟tomcat。

1. 本地机器下载安装Jenkins

jenkins下载地址:jenkins.io/

安装前提是你的windows已经装好了jdk跟tomcat。在这里我用的是jdk8跟tomcat9。

1.1 以上地址点击download后最后有个war文件下载。点击下载到本地。

然后放到tomcat的webapps目录下,注意文件超过了50M,需要修改Tomcat安装目录下的/manager/WEB-INF/web.xml。把以下配置注释掉。

1
复制代码<!-- <max-file-size>52428800</max-file-size> -->

1.2 启动tomcat,浏览器访问localhost:8080/jenkins

出现以下界面,提示你输入密码。密码在红色字表示的文件中。

选择安装推荐的插件。这里需要一些时间。

创建管理员账户。

这样就是安装完成了。

2. Web服务器端安装Maven、git

下载Maven,用来编译工程。下载git用来自动获取git服务器最新代码。

2.1 Maven官网找到下载地址,通过Wget获取

如果你的服务器没安装wget,通过以下命令安装

1
复制代码yum -y install wget

访问Maven官网找到下载地址 maven.apache.org/download.cg…
执行以下命令

1
2
3
4
5
6
复制代码我们一般下载到这里
cd /usr/local/
下载
wget http://us.mirrors.quenda.co/apache/maven/maven-3/3.6.3/binaries/apache-maven-3.6.3-bin.tar.gz
解压缩
tar xvzf apache-maven-3.6.3-bin.tar.gz

2.2 设置Maven环境变量,方便随时随地使用mvn命令

编辑/etc/profile,末尾添加以下两行。

1
2
复制代码M2_HOME=/etc/local/apache-maven-3.6.3
export PATH=${M2_HOME}/bin:${PATH}

重载设置

1
复制代码source /etc/profile

验证是否安装成功

1
复制代码mvn -v

2.3 安装git

1
复制代码yum install git

3. 将Web服务器注册到Jenkins节点中

如果你的Jenkins安装到了Web服务器中,可以省略这个步骤,因为Jenkins默认创建了一个叫做master的节点代表Jenkins安装的机器,但是这样的话后面重启tomcat时,由于Jenkins运行在tomcat中,会随tomcat一起被关闭掉从而失去链接。相反,像我这样将Jenkins装到了本地机器,为了能够部署到远程的Web服务器中,你需要本步骤把Web服务器作为节点注册到Jenkins中。如下图,就是已经系统建好的了master,跟我创建的测试节点TestEnv。

节点的创建步骤如下。首先选择Build Executor Status。

点击新建节点。

输入节点名,选择Permanet Agent,点击OK。

接下来填写配置。

  • Remote root directory 是你的在web服务器上的工作根目录。我这里用root登录,把工作目录放在/root/jenkins下。
  • Launch agent agents via SSH。这个代表用SSH登录。
  • Host 填写你的Web服务器地址
  • Credentials 填写你的登录账户密码
  • Host Key Verification Strategy 选择Known hosts file Verification Strategy

最后点击保存就可以完成节点的创建。

4. Jenkins创建任务

点击 创建新任务 进入项目创建界面

填写任务名,选择自由风格的工程。

这里要选择节点,也就是刚才我们注册的TestEnv节点。

如下图,我使用git,所以需要填写以下内容。

  • Repository URL: git的http仓库地址
  • Credentials: 登录需要的用户名密码
  • Branch Specifer : git的分支
  • Check out to s sub-directory: 任意填写一个子目录名用于下载代码

最后,添加要执行的shell命令,并且保存设置。

代码如下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
复制代码BUILD_ID=DONOTKILLME

. /etc/profile

export PROJ_PATH=`pwd`
export TOMCAT_APP_PATH=/usr/local/apache-tomcat-9.0.30

#编译代码(注意siled是任意名称,是之前步骤输入的目录名)
cd $PROJ_PATH/siled
mvn clean install

#删除原有的war文件
rm -rf $TOMCAT_APP_PATH/webapps/*.war

#关闭tomcat
$TOMCAT_APP_PATH/bin/shutdown.sh

#拷贝新的war到tomcat下
cp $PROJ_PATH/siled/target/*.war $TOMCAT_APP_PATH/webapps/

#重启tomcat
$TOMCAT_APP_PATH/bin/startup.sh

5. 尝试部署

点击Build now,立即构建。这样Jenkins就会自动链接服务器,并从git获取最新代码,然后执行上一步中你添加的shell命令。

构建完成后会有如下的结果。蓝色表示构建成功,红色代表失败,灰色代表中途与节点失去联系。数字表示次数。点击数字。我这里是第12次构建,所以点击了#12。

点击控制台输出。

可以确认结果是成功的。

6. 验证部署应用

访问应用地址,可以看到自动部署成功。

7. 定时部署

上面的步骤基本上就没问题了,但是有一点,我们想要每天都部署最新的代码怎么办?当然每次手动点击部署就太麻烦了。Jenkins为我们提供了定时部署功能。

以上配置,表示在6月13日23点触发。

如果配置成 00 06 * * * ,表示每天早上6点触发。如果不是很熟悉这块,可以百度cron表达式。

官方配置说明

总结

初次配置Jenkins有些繁琐,但并不难。中间如果有什么问题网上都有答案,这里我尽量写的详细一点,希望能够帮助到读者。正所谓一时Jenkins一时爽,一直Jenkins一直爽。尤其是越大的项目,通过灵活的自动化命令,你就越省心省力。

本文转载自: 掘金

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

1…840841842…956

开发者博客

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