项目中,可能会遇到虽然知道发生了异常,但是不知道异常是什么,由于调用栈信息不全,从日志上看不出具体问题;主要原因是异常调用栈太深,导致异常信息被截断。

下面这个方法可以获取根异常,将中间的非主要异常过滤掉。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/**  
* 获取根异常,防止调用链太长,导致有效信息被截断
*/
public static Throwable getRootThrowable(Throwable e) {
if (Objects.isNull(e)) {
return null;
}

Throwable lastCause = e;
Throwable currentCause = e.getCause();
while (Objects.nonNull(currentCause)) {
lastCause = currentCause;
currentCause = currentCause.getCause();
}

return lastCause;
}

示例

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
@Slf4j  
public class ThrowableTestDemo {
public static void main(String[] args) {
test();
}

private static void test() {
try {
try {
try {
try {
try {
int i = 1 / 0;
} catch (Exception e) {
throw new RuntimeException(e);
}
} catch (Exception e) {
throw new RuntimeException(e);
}
} catch (Exception e) {
throw new RuntimeException(e);
}
} catch (Exception e) {
throw new RuntimeException(e);
}
} catch (Exception e) {
log.warn("处理前:", e);
System.out.println("=======>");
log.warn("处理后:", getRootThrowable(e));
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
10:38:13.122 [main] WARN com.coocaa.media.sync.ThrowableTestDemo - 处理前:
java.lang.RuntimeException: java.lang.RuntimeException: java.lang.RuntimeException: java.lang.RuntimeException: java.lang.ArithmeticException: / by zero
at com.coocaa.media.sync.ThrowableTestDemo.test(ThrowableTestDemo.java:36)
at com.coocaa.media.sync.ThrowableTestDemo.main(ThrowableTestDemo.java:15)
Caused by: java.lang.RuntimeException: java.lang.RuntimeException: java.lang.RuntimeException: java.lang.ArithmeticException: / by zero
at com.coocaa.media.sync.ThrowableTestDemo.test(ThrowableTestDemo.java:33)
... 1 common frames omitted
Caused by: java.lang.RuntimeException: java.lang.RuntimeException: java.lang.ArithmeticException: / by zero
at com.coocaa.media.sync.ThrowableTestDemo.test(ThrowableTestDemo.java:30)
... 1 common frames omitted
Caused by: java.lang.RuntimeException: java.lang.ArithmeticException: / by zero
at com.coocaa.media.sync.ThrowableTestDemo.test(ThrowableTestDemo.java:27)
... 1 common frames omitted
Caused by: java.lang.ArithmeticException: / by zero
at com.coocaa.media.sync.ThrowableTestDemo.test(ThrowableTestDemo.java:25)
... 1 common frames omitted

=======>
10:38:13.124 [main] WARN com.coocaa.media.sync.ThrowableTestDemo - 处理后:
java.lang.ArithmeticException: / by zero
at com.coocaa.media.sync.ThrowableTestDemo.test(ThrowableTestDemo.java:25)
at com.coocaa.media.sync.ThrowableTestDemo.main(ThrowableTestDemo.java:15)

思考

封装一个 set 对象构建器,利用类的 set 引用方法(不局限 set 开头的方法)来设值,便于点式调用。

测试

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

import cn.hutool.core.bean.BeanUtil;
import lombok.Data;

@Data
public class User {
private String name;
private Integer age;
private String sex;
private String email;

private String other;

public void appendOther(String other) {
this.other = other;
}

public static void main(String[] args) {
// 方法1,普通语法(推荐)
User user1 = new User();
user1.setName("Jojo");
user1.setAge(18);
user1.setSex("女");
user1.setEmail("JoJo@example.com");
user1.appendOther("其他数据");
System.out.println(user1);

// 方法2,双括号语法(本质是匿名函数。不推荐使用,可能引起内层泄露)
User user2 = new User() {{
setName("Jojo");
setAge(18);
setSex("女");
setEmail("JoJo@example.com");
appendOther("其他数据");
}};
System.out.println(user2);

// 方法3:set 构建器(推荐)
final User user3 = SetBuilder.of(new User())
.set(User::setName, "JoJo")
.set(User::setAge, 18)
.set(User::setSex, "女")
.set(User::setEmail, "JoJo@example.com")
.set(User::appendOther, "其他数据")
.build();
System.out.println(user3);

// 方法4:反射设值
final User user4 = new User();
BeanUtil.setFieldValue(user4, "name", "JoJo");
BeanUtil.setFieldValue(user4, "age", 18);
BeanUtil.setFieldValue(user4, "sex", "女");
BeanUtil.setFieldValue(user4, "email", "JoJo@example.com");
BeanUtil.setFieldValue(user4, "other", "其他数据");
System.out.println(user4);
}
/**
* 打印:
User(name=Jojo, age=18, sex=女, email=JoJo@example.com, other=其他数据)
User(name=Jojo, age=18, sex=女, email=JoJo@example.com, other=其他数据)
User(name=JoJo, age=18, sex=女, email=JoJo@example.com, other=其他数据)
User(name=JoJo, age=18, sex=女, email=JoJo@example.com, other=其他数据)
*/
}

源码

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
/**
* 封装一个set构建器,基于对象的 set 引用方法来设值
* <p>
* @param <T> 指定对象泛型
*/
public class SetBuilder<T> {
private final T object;

public static <T> SetBuilder<T> of(T object) {
return new SetBuilder<>(object);
}

/**
* 构造函数
*
* @param object 对象
*/
public SetBuilder(T object) {
this.object = object;
}

/**
* 链式调用
*
* @param setter set引用方法
* @param value 要设置的属性值
* @param <V> 对象泛型
* @return builder,可链式调用
*/
public <V> SetBuilder<T> set(Setter<T, V> setter, V value) {
setter.set(object, value);
return this;
}


/**
* 构建
*
* @return 构建后对象
*/
public T build() {
return object;
}

@FunctionalInterface
public interface Setter<T, V> {

/**
* 调用set方法
*
* @param t 实体对象
* @param value 实体对象属性值
*/
void set(T t, V value);
}

}

目的:

对于多个独立的任务,可以以并发的方式执行任务,以提高 CPU 利用率,提高处理效率。

思路

在一个线程池中,开启指定数量的线程,每个线程从任务队列中获取任务执行。

执行的过程中,判断当前线程是否在执行任务的状态,如果没有执行任务,取一条任务执行,如果正在执行,则跳过,下轮再判断。

在所有任务执行完后,关闭线程池。

需要注意的是数据结构的选择,须选择并发类的数据结构,不然可能出现阻塞,死锁等情况。

(具体逻辑参考源码)

示例

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
/**
* 并发执行器示例
*/
public class ConcurrentExecutorTest {

/**
* 测试
*/
public static void main(String[] args) {
for (int i = 0; i < 100; i++) {
test();
}
}

private static void test() {
Map<String, String> paramMap = new LinkedHashMap<>();
for (int i = 0; i < 10; i++) {
paramMap.put("key:" + i, "value:" + i);
}

final ConcurrentExecutor<String, String, Integer> executor = new ConcurrentExecutor<>(5, paramMap,
(k, v) -> {
ThreadUtil.sleep(10);
System.out.println(Thread.currentThread().getName() + "-" + v);
final int abs = Math.abs(Objects.hash(v));
if (abs % 3 == 0) {
int i = 1 / 0;
}
return abs;
});
executor.execute();
System.out.println("success result: " + executor.getSuccessResultMap());
System.out.println("error result: " + executor.getErrorResultMap());
}
}

测试结果

1
2
3
4
5
6
7
8
9
10
11
12
pool-1-thread-1-value:0
pool-1-thread-2-value:1
pool-1-thread-4-value:3
pool-1-thread-3-value:2
pool-1-thread-3-value:8
pool-1-thread-1-value:5
pool-1-thread-5-value:4
pool-1-thread-2-value:6
pool-1-thread-4-value:7
pool-1-thread-3-value:9
success result: {key:2=231604360, key:0=231604358, key:6=231604364, key:5=231604363, key:3=231604361, key:9=231604367, key:8=231604366}
error result: {key:1=java.lang.ArithmeticException: / by zero, key:4=java.lang.ArithmeticException: / by zero, key:7=java.lang.ArithmeticException: / by zero}

源码(方案 1)

基于自定义的 queue 实现

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

import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.collection.ConcurrentHashSet;
import cn.hutool.core.lang.Assert;
import cn.hutool.core.thread.ThreadUtil;
import cn.hutool.core.util.BooleanUtil;

import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.ExecutorService;
import java.util.function.BiFunction;

/**
* 并发执行器
* <p>
* 适用场景:每个任务是独立的,不耦合的
*
* @author lilou
* @since 2022/6/9 9:05
*/
public class ConcurrentExecutor<K, V, R> {
/**
* 任务参数映射(K:key的类型,V:值的类型)
*/
private final Map<K, V> paramMap;
/**
* 成功的任务结果映射(R:结果类型)
*/
private final Map<K, R> successResultMap;

/**
* 失败的任务结果映射
*/
private final Map<K, Throwable> errorResultMap;

/**
* 当前运行中的key集合
*/
private final Set<K> runningKeySet;

/**
* 候选任务key队列
*/
private final Queue<K> candidateKeyQueue;

/**
* 同时运行的最大线程数量
*/
private final int maxThreadNum;

/**
* 执行器
*/
private final ExecutorService executorService;

/**
* 具体任务策略
*/
private final BiFunction<K, V, R> biFunction;

/**
* 当前index线程的运行状态,可依据此状态,判断是否立刻从任务参数中获取任务执行
*/
private final Map<Integer, Boolean> currentIndexThreadRunningStatusMap;

public ConcurrentExecutor(int maxThreadNum, Map<K, V> paramMap, BiFunction<K, V, R> biFunction) {
Assert.notNull(paramMap, "paramMap不可为空");
Assert.isTrue(maxThreadNum > 0, "maxThreadNum不可小于1");

final int paramSize = paramMap.size();
this.maxThreadNum = Math.min(maxThreadNum, paramSize);
// tips: 须转换成同步类的map数据结构,如果错误地使用 this.paramMap = paramMap; 且外部使用了HashMap 或 LinkedHashMap,多测试几遍会发现,偶尔会陷入了阻塞
this.paramMap = Collections.synchronizedMap(paramMap);
this.candidateKeyQueue = new ConcurrentLinkedQueue<>(paramMap.keySet());
this.runningKeySet = new ConcurrentHashSet<>(paramSize);
this.biFunction = biFunction;
this.executorService = ThreadUtil.newExecutor(this.maxThreadNum, this.maxThreadNum, Integer.MAX_VALUE);
this.currentIndexThreadRunningStatusMap = new ConcurrentHashMap<>(this.maxThreadNum);
this.successResultMap = new ConcurrentHashMap<>(this.paramMap.size());
this.errorResultMap = new ConcurrentHashMap<>();
}


public void execute() {
while (CollUtil.isNotEmpty(paramMap)) {

// 最多同时有 maxRunningThreadNumber 同时消费 taskMap 中的数据
for (int i = 0; i < this.maxThreadNum; i++) {
int currentIndex = i;

// 当前线程上次还未执行完,暂时跳过
final Boolean isRunning = currentIndexThreadRunningStatusMap.getOrDefault(currentIndex, false);
if (BooleanUtil.isTrue(isRunning)) {
continue;
}

// 选择一个候选key
final K candidateKey = pickCandidateKey();
// 当前没有对应key的任务
if (Objects.isNull(candidateKey)) {
continue;
}

// 在线程池中运行任务
executorService.submit(() -> {
try {
currentIndexThreadRunningStatusMap.put(currentIndex, true);
final V data = paramMap.get(candidateKey);

// 开始执行任务
final R result = biFunction.apply(candidateKey, data);

// 存入正常结果
successResultMap.put(candidateKey, result);
} catch (Exception e) {
// 存入异常结果
errorResultMap.put(candidateKey, e);
} finally {
paramMap.remove(candidateKey);
candidateKeyQueue.remove(candidateKey);
currentIndexThreadRunningStatusMap.remove(currentIndex);
}
});
}
}
executorService.shutdown();
}


/**
* 从候选任务key队列中选择一个任务key
*/
private K pickCandidateKey() {
for (K candidateKey : candidateKeyQueue) {
if (!runningKeySet.contains(candidateKey)) {
runningKeySet.add(candidateKey);
return candidateKey;
}
}
return null;
}

public Map<K, R> getSuccessResultMap() {
return successResultMap;
}

public Map<K, Throwable> getErrorResultMap() {
return errorResultMap;
}
}

源码(方案 2)

基于 executorService.invokeAll

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
package com.lyloou.component.util.concurrent;

import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.date.TimeInterval;
import cn.hutool.core.lang.Assert;
import cn.hutool.core.thread.ThreadUtil;
import lombok.SneakyThrows;

import java.util.*;
import java.util.concurrent.Callable;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutorService;
import java.util.function.BiFunction;

/**
* 并发执行器
* <p>
* 适用场景:每个任务是独立的,不耦合的
*
* @author lilou
* @since 2022/6/9 9:05
*/
public class ConcurrentExecutor2<K, V, R> {
/**
* 任务参数映射(K:key的类型,V:值的类型)
*/
private final Map<K, V> paramMap;
/**
* 成功的任务结果映射
*/
private final Map<K, R> successResultMap;

/**
* 失败的任务结果映射
*/
private final Map<K, Throwable> errorResultMap;

/**
* 执行器
*/
private final ExecutorService executorService;

/**
* 具体任务策略
*/
private final BiFunction<K, V, R> biFunction;


public ConcurrentExecutor2(int maxThreadNum, Map<K, V> paramMap, BiFunction<K, V, R> biFunction) {
Assert.notNull(paramMap, "paramMap不可为空");
Assert.isTrue(maxThreadNum > 0, "maxThreadNum不可小于1");

// 同时运行的最大线程数量
int maxThreadNum1 = Math.min(maxThreadNum, paramMap.size());
// tips: 如果错误地使用 this.paramMap = paramMap; 多测试几遍会发现,偶尔会陷入了阻塞
this.paramMap = Collections.synchronizedMap(paramMap);
this.biFunction = biFunction;
this.executorService = ThreadUtil.newExecutor(maxThreadNum1, maxThreadNum1, Integer.MAX_VALUE);
this.successResultMap = new ConcurrentHashMap<>(this.paramMap.size());
this.errorResultMap = new ConcurrentHashMap<>();
}


public void execute() throws InterruptedException {
while (CollUtil.isNotEmpty(paramMap)) {
List<Callable<R>> callableList = new ArrayList<>();
paramMap.forEach((k, v) -> callableList.add(() -> {
try {
final R result = biFunction.apply(k, v);
successResultMap.put(k, result);
} catch (Exception e) {
errorResultMap.put(k, e);
} finally {
paramMap.remove(k);
}
return null;
}));
// 在线程池中运行任务
executorService.invokeAll(callableList);
executorService.shutdown();
}
}

public Map<K, R> getSuccessResultMap() {
return successResultMap;
}

public Map<K, Throwable> getErrorResultMap() {
return errorResultMap;
}

/**
* 测试
*/
public static void main(String[] args) throws InterruptedException {
final TimeInterval timer = DateUtil.timer();
for (int i = 0; i < 100; i++) {
test();
}
System.out.println(timer.intervalMs());
}

private static void test() throws InterruptedException {
Map<String, String> paramMap = new LinkedHashMap<>();
for (int i = 0; i < 10; i++) {
paramMap.put("key:" + i, "value:" + i);
}

final ConcurrentExecutor2<String, String, Integer> executor = new ConcurrentExecutor2<>(5, paramMap,
(k, v) -> {
ThreadUtil.sleep(10);
System.out.println(Thread.currentThread().getName() + "-" + v);
final int abs = Math.abs(Objects.hash(v));
if (abs % 3 == 0) {
int i = 1 / 0;
}
return abs;
});
executor.execute();
System.out.println("success result: " + executor.getSuccessResultMap());
System.out.println("error result: " + executor.getErrorResultMap());
}
}

背景

编写代码时,会经常需要编写两个对象是否相等的逻辑,一般会有如下做法

  1. 直接写在业务代码中;
  2. 单独写个方法,业务代码中调用;
  3. 重写 equals 方法;

上面这些做法,都比较复杂,如果属性太多或复杂点(如果是 list 和 map 就更复杂了),就需要编写更多的判断逻辑代码了。

想法

如果能只需要提供比较的方法引用列表,有个地方能自动方法引用取值,并比较就好了。

思路 1

  1. 在 java8 中可以使用方法引用,如:People::getName;
  2. 可以将所有要比较的 Getter 保存到列表中;
  3. 在 比较的时候,根据 方法引用获取具体的值进行比较;
  4. 全部比较都相等了,就认为是相等的。

思路 2

  1. 思路 1 适用于需要比较的字段少时,有一种情况是,需要比较的字段多,只想排除掉少量字段
  2. 通过注解来忽略指定的字段
  3. 获取全部字段,过滤掉忽略的字段,通过反射比较字段对应的值。

举个例子 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
@Getter
@Setter
public class EqualDemo implements Equable {

private Integer id;
private Integer age;
private String username;
private Date createTime;
private Date updateTime;
private List<EqualDemo> list;
private Map<String, EqualDemo> map;

@Override
public boolean equals(Object obj) {
if (obj instanceof EqualDemo) {
if (!Objects.equals(((EqualDemo) obj).getId(), this.getId())) {
return false;
}

if (!Objects.equals(((EqualDemo) obj).getAge(), this.getAge())) {
return false;
}

if (!Objects.equals(((EqualDemo) obj).getUsername(), this.getUsername())) {
return false;
}
final List<EqualDemo> list1 = ((EqualDemo) obj).getList();
final List<EqualDemo> list2 = this.getList();
// todo 比较 list1 和 list2

final Map<String, EqualDemo> map1 = ((EqualDemo) obj).getMap();
final Map<String, EqualDemo> map2 = this.getMap();
// todo 比较 map1 和 map2


return true;
}
return Objects.equals(this, obj);
}
}

// 使用
public class EqualDemoTest {
@Test
public void testSimpleTrue() {
final EqualDemo s1 = new EqualDemo();
s1.setId(1);
s1.setUsername("bob");

final EqualDemo s2 = new EqualDemo();
s2.setId(1);
s2.setUsername("bob");

// // s1 和 s2 相等
assert s1.equals(s2);
}
}

举个例子 2(改造后-指定需要比较的字段的 Getter 方法引用)

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
@Getter
@Setter
public class OnlyEqualDemo implements Equable {

private Integer id;

private Integer age;

private String username;
private Date createTime;
private Date updateTime;
private List<OnlyEqualDemo> list;
private Map<String, OnlyEqualDemo> map;


@SuppressWarnings("unchecked")
@Override
public List<EqualGetter<OnlyEqualDemo>> listOnlyEqualsToGetter() {
final EqualGetter<OnlyEqualDemo> getId = OnlyEqualDemo::getId;
return Arrays.asList(
getId,
OnlyEqualDemo::getAge,
OnlyEqualDemo::getUsername,
OnlyEqualDemo::getList,
OnlyEqualDemo::getMap
);
}
}


// 使用
public class OnlyEqualDemoTest {


@Test
public void testSimpleTrue() {
final OnlyEqualDemo s1 = new OnlyEqualDemo();
s1.setId(1);
s1.setUsername("bob");

final OnlyEqualDemo s2 = new OnlyEqualDemo();
s2.setId(1);
s2.setUsername("bob");

// // s1 和 s2 相等
assert s1.onlyEqualsTo(s2);
}
}

举个例子 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
33
34
35
36
37
38
39
40
@Getter
@Setter
public class IgnoreEqualDemo implements Equable {

private Integer id;

@EqualIgnored
private Integer age;

private String username;

@EqualIgnored
private Date createTime;

@EqualIgnored
private Date updateTime;

private List<IgnoreEqualDemo> list;
private Map<String, IgnoreEqualDemo> map;
}

// 使用
public class IgnoreEqualDemoTest {


@Test
public void testSimpleTrue() {
final IgnoreEqualDemo s1 = new IgnoreEqualDemo();
s1.setId(1);
s1.setUsername("bob");
s1.setCreateTime(new Date());

final IgnoreEqualDemo s2 = new IgnoreEqualDemo();
s2.setId(1);
s2.setUsername("bob");

// // s1 和 s2 相等
assert s1.ignoreEqualsTo(s2);
}
}

这样就简洁很多了,由于 ignoreEqualsTo 和 onlyEqualsTo 方法是 Equable 接口中的默认方法,具体逻辑全封装起来了(包括 object, collection 和 map 的处理),不需要重写就可以直接使用,具体实现看下面的源码。

源码

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

import lombok.SneakyThrows;

import java.beans.PropertyDescriptor;
import java.io.Serializable;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.*;
import java.util.function.BiFunction;
import java.util.stream.Collectors;

/**
* 比较两个对象是否相等
*
* @author lilou
* @date 2022/4/9 9:30
* <p>
* @see Equable#onlyEqualsTo(Equable) 方法1:通过 Getter方法引用 来指定需要比较的字段(场景:字段总量多,但是只需要比较少量字段时)
* @see Equable#ignoreEqualsTo(Equable) 方法2:通过忽略注解({@code EqualIgnored})来指定忽略比较的字段,未标识的字段会全部参与比较(场景:字段总量多,但是只需要排除少量字段时)
* 注意:onlyEqualsTo 和 ignoreEqualsTo 不要混用
*/
public interface Equable extends Serializable {
long serialVersionUID = 1L;

/**
* 使用方法引用来比较,只比较指定的 Getter 方法引用。
* 使用此方法:重写 listOnlyEqualsToGetter 方法,指定需要比较的字段的Getter方法引用
*
* @param other 另一个 model
* @return 结果
*/
default boolean onlyEqualsTo(Equable other) {
final List<EqualGetter<Equable>> getterList = listOnlyEqualsToGetter();

// getter方法引用列表中没有需要比较的 getter,直接比较对象
if (getterList == null || getterList.isEmpty()) {
return Objects.equals(other, this);
}

// 列表有值,计算两个对象在列表中 getter 值,并比较是否相等
for (EqualGetter<Equable> equalGetter : getterList) {
Object o1 = equalGetter.apply(this);
Object o2 = equalGetter.apply(other);

// 对于属性中也有实现了 Equable 接口的,递归调用
if (o1 instanceof Equable && o2 instanceof Equable) return ((Equable) o1).onlyEqualsTo((Equable) o2);

// equalTo collection
if (o1 instanceof Collection && o2 instanceof Collection) {
// 判断不相等
if (notEqualsCollection((Collection<?>) o1, (Collection<?>) o2, Equable::onlyEqualsTo)) return false;

// 列表已经判断完了,开始下一个属性
continue;
}

// equalTo map
if (o1 instanceof Map && o2 instanceof Map) {
if (notEqualsMap((Map<?, ?>) o1, (Map<?, ?>) o2, Equable::onlyEqualsTo)) return false;
// 列表已经判断完了,开始下一个属性
continue;
}

if (!Objects.equals(o1, o2)) {
return false;
}
}
return true;
}

default boolean notEqualsMap(Map<?, ?> o1, Map<?, ?> o2, BiFunction<Equable, Equable, Boolean> biFunc) {
// 比较 key
final Collection<?> set1 = o1.keySet();
final Collection<?> set2 = o2.keySet();
if (notEqualsCollection(set1, set2, biFunc)) return true;

// 比较 value
final Collection<?> values1 = o1.values();
final Collection<?> values2 = o2.values();
return notEqualsCollection(values1, values2, biFunc);
}

/**
* 判断 collection 是否不相等
*
* @param o1 第一个 collection
* @param o2 第二个 collection
* @return 是否不相等
*/
default boolean notEqualsCollection(Collection<?> o1, Collection<?> o2, BiFunction<Equable, Equable, Boolean> biFunc) {
if (Objects.isNull(o1) && Objects.isNull(o2)) {
return false;
}

if (Objects.isNull(o1) || Objects.isNull(o2)) {
return true;
}

if (o1.isEmpty() && o2.isEmpty()) {
return false;
}

if (o1.size() != o2.size()) {
return true;
}

// 根据 hashCode 来排序
o1 = o1.stream().sorted(Comparator.comparingInt(Object::hashCode)).collect(Collectors.toList());
o2 = o2.stream().sorted(Comparator.comparingInt(Object::hashCode)).collect(Collectors.toList());


// 逐个比较
final Iterator<?> iterator1 = o1.iterator();
final Iterator<?> iterator2 = o2.iterator();
while (iterator1.hasNext() && iterator2.hasNext()) {
final Object next1 = iterator1.next();
final Object next2 = iterator2.next();
if (next1 instanceof Equable && next2 instanceof Equable) {
if (!biFunc.apply((Equable) next1, (Equable) next2)) {
return true;
}
} else {
return !Objects.equals(next1, next2);
}
}
return false;
}

/**
* 需重写
* <p>
* 需要比较的 Getter 方法列表(原理是通过Getter 的 apply 方法得到实际的值,再进行比较)
*
* @param <T> 泛型
* @return getter 方法列表
*/
default <T extends Equable> List<EqualGetter<T>> listOnlyEqualsToGetter() {
return Collections.emptyList();
}

/**
* getter方法接口定义
*/
@FunctionalInterface
interface EqualGetter<T extends Equable> extends Serializable {
Object apply(T source);
}


/**
* IgnoreEqual 注解+反射来实现 equal 逻辑
*
* @param other 另一个 model
* @return 结果
*/
@SneakyThrows
default boolean ignoreEqualsTo(Equable other) {
if (Objects.equals(this, other)) {
return true;
}

final Class<? extends Equable> aClass1 = this.getClass();
final Class<? extends Equable> aClass2 = other.getClass();
if (!Objects.equals(aClass1, aClass2)) {
return false;
}

final Field[] declaredFields = aClass1.getDeclaredFields();
for (Field declaredField : declaredFields) {

// 字段是否被标记忽略
final EqualIgnored isIgnored = declaredField.getAnnotation(EqualIgnored.class);
if (Objects.nonNull(isIgnored)) {
continue;
}

final PropertyDescriptor propertyDescriptor = new PropertyDescriptor(declaredField.getName(), aClass1);
final Method readMethod = propertyDescriptor.getReadMethod();

final Object o1 = readMethod.invoke(this);
final Object o2 = readMethod.invoke(other);

// 对于属性中也有实现了 Equable 接口的,递归调用
if (o1 instanceof Equable && o2 instanceof Equable) return ((Equable) o1).ignoreEqualsTo((Equable) o2);

// equalTo collection
if (o1 instanceof Collection && o2 instanceof Collection) {
// 判断不相等
if (notEqualsCollection((Collection<?>) o1, (Collection<?>) o2, Equable::ignoreEqualsTo)) return false;

// 列表已经判断完了,开始下一个属性
continue;
}

// equalTo map
if (o1 instanceof Map && o2 instanceof Map) {

if (notEqualsMap((Map<?, ?>) o1, (Map<?, ?>) o2, Equable::ignoreEqualsTo)) return false;

// 列表已经判断完了,开始下一个属性
continue;
}

if (!Objects.equals(o1, o2)) {
return false;
}
}

return true;
}

}


@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface EqualIgnored {
}

单元测试

方法 1:

https://github.com/lyloou/component/blob/master/component-dto/src/test/java/com/lyloou/component/dto/OnlyEqualDemoTest.java
Equable_20220410103323_2022-04-10-10-33-24

方法 2:

https://github.com/lyloou/component/blob/master/component-dto/src/test/java/com/lyloou/component/dto/IgnoreEqualDemoTest.java
Equable_20220410103408_2022-04-10-10-34-09

参考资源

frps 是一个优秀的内网穿透软件。

frps 下载安装:https://github.com/fatedier/frp/releases

服务端: ./frps -c ./frps.ini

frps.ini

1
2
3
4
5
6
7
8
9
10
11
12
[common]
bind_addr = 0.0.0.0
bind_port = 7000

bind_udp_port = 7001

dashboard_port = 7500
dashboard_user = admin
dashboard_pwd = admin

vhost_http_port = 2780
subdomain_host = frps.example.com

客户端: ./frpc -c ./frpc.ini

frpc.ini

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
[common]
server_addr = xx.xx.xx.xx
server_port = 7000
admin_addr = 127.0.0.1
admin_port = 7400

[ssh]
type = tcp
local_ip = 127.0.0.1
local_port = 22
remote_port = 7022

[web]
type = http
local_port = 2780
subdomain = office

访问服务器端的 dashboard 查看统计信息: http://frps.example.com:7500/

访问内网 ssh: ssh -oPort=7022 test@x.x.x.x
通过浏览器访问内网机器: http://office.frps.example.com:2780/

配置自动启动

安装 supervisor

1
2
3
4
apt install supervisor

# 查看 supervisor 状态
service supervisor status

添加 frps 配置:vi /etc/supervisor/conf.d/frps.conf

1
2
3
4
[program:frps]
command=/root/c/frp/frps -c /root/c/frp/frps.ini
autostart=true
autorestart=true

可以依次运行下面操作加载 新加入的 frps

1
2
3
4
5
6
# 告诉 supervisor 新加了配置
supervisorctl reread

# 让 supervisor 启动 frps
supervisorctl update

输入 supervisorctl 查看 frps 是否已运行

202202082113352

start 和 stop frps
202202082114908

设置 supervisor 开机启动

1
2
systemctl enable supervisor.service
systemctl daemon-reload

为 supervisor 添加 dashboard,通过 9001 来访问:

vi /etc/supervisor/supervisord.conf 添加下面代码

1
2
3
4
[inet_http_server]
port=*:9001
username=admin
password=admin

202202082135628

参考资料

https://github.com/fatedier/frp

通过 frp 实现访问内网 ssh 与 http 简明教程 - 简书

How to Install and Configure Supervisor on Ubuntu 20.04 | Atlantic.Net

1
2
3
4
5
6
7
8
9
10
11
12
[program:nginx]
command=/usr/sbin/nginx -g "daemon off;"
autostart=true
autorestart=true
startretries=5
numprocs=1
startsecs=0
process_name=%(program_name)s_%(process_num)02d
stderr_logfile=/var/log/supervisor/%(program_name)s_stderr.log
stderr_logfile_maxbytes=10MB
stdout_logfile=/var/log/supervisor/%(program_name)s_stdout.log
stdout_logfile_maxbytes=10MB

使用 supervisor 设置服务端 frp 开机启动 - 一只猿 - 前端攻城尸 | 安全研究员 | 硬件控 | 业余极客 | 开源拥护者

frp - 《frp 中文文档》 - 书栈网 · BookStack

桌面

关闭弹性滚动

进入:edge://flags/

disable Microsoft Edge scrolling personality

恢复 win10 桌面右键菜单

Win11 恢复 Win10 经典右键菜单 亲测有效_admans 的专栏-CSDN 博客_win11 换回 win10 右键

管理员权限运行

1
reg.exe add "HKCU\Software\Classes\CLSID\{86ca1aa0-34aa-4e8b-a509-50c905bae2a2}\InprocServer32" /f /ve

重启资源管理器:
taskkill /f /im explorer.exe&&explorer.exe

cmd /c taskkill /f /im explorer.exe&&explorer.exe 这命令什么意思_百度知道

恢复 win11

1
reg.exe delete "HKCU\Software\Classes\CLSID\{86ca1aa0-34aa-4e8b-a509-50c905bae2a2}\InprocServer32" /va /f

其它方案:startAllback 软件

剑指 Offer 第 32 题-从上到下打印二叉树

描述

输入一个整数数组,判断该数组是不是某二叉搜索树的后序遍历的结果。如果是则返回 true ,否则返回 false 。假设输入的数组的任意两个数字都互不相同。
数据范围: 节点数量 0≤n≤1000 ,节点上的值满足 1≤val≤10^5,保证节点上的值各不相同

要求:空间复杂度 O(n) ,时间时间复杂度 O(n^2)

提示:

1.二叉搜索树是指父亲节点大于左子树中的全部节点,但是小于右子树中的全部节点的树。

2.该题我们约定空树不是二叉搜索树

3.后序遍历是指按照 “左子树-右子树-根节点” 的顺序遍历

4.参考下面的二叉搜索树,示例 1
jz32-从上到下打印二叉树_20220124175723_2022-01-24-17-57-24

方法 1

jz32-从上到下打印二叉树_20220124175628_2022-01-24-17-56-29

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
/**
* 从上往下
*
* @author lilou
*/
public class Jz32 {

public static void main(String[] args) {
final Jz32 jz = new Jz32();
final TreeNode root = new TreeNode(8);
final TreeNode node10 = new TreeNode(10);
root.left = new TreeNode(6);
root.right = node10;
node10.left = new TreeNode(1);
node10.right = new TreeNode(2);
System.out.println(jz.PrintFromTopToBottom(root));
}

public ArrayList<Integer> PrintFromTopToBottom(TreeNode root) {
if (root == null) {
return new ArrayList<>();
}
final ArrayList<Integer> list = new ArrayList<>();
Queue<TreeNode> queue = new LinkedList<>();
queue.offer(root);

while (!queue.isEmpty()) {
final TreeNode node = queue.poll();
list.add(node.val);
if (node.left != null) {
queue.offer(node.left);
}
if (node.right != null) {
queue.offer(node.right);
}
}
return list;
}

public static class TreeNode {
int val = 0;
TreeNode left = null;
TreeNode right = null;

public TreeNode(int val) {
this.val = val;

}

}
}

扩展-按层次打印

jz32-从上到下打印二叉树_20220124175928_2022-01-24-17-59-28

jz32-从上到下打印二叉树_20220124175801_2022-01-24-17-58-01

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 algorithm.jzoffer;

import java.util.LinkedList;
import java.util.Queue;

/**
* 按层次打印
*
* @author lilou
*/
public class Jz32_3 {

public static void main(String[] args) {
final Jz32_3 jz = new Jz32_3();
final TreeNode root = new TreeNode(8);
final TreeNode node10 = new TreeNode(10);
final TreeNode node6 = new TreeNode(6);
root.left = node6;
root.right = node10;
node10.left = new TreeNode(1);
node10.right = new TreeNode(2);
node6.left = new TreeNode(7);
node6.right = new TreeNode(11);

jz.PrintFromTopToBottom(root);
}

public void PrintFromTopToBottom(TreeNode root) {
if (root == null) {
return;
}
Queue<TreeNode> queue = new LinkedList<>();
queue.offer(root);
int toBePrint = 1;
int nextLevel = 0;

while (!queue.isEmpty()) {
final TreeNode node = queue.poll();
System.out.printf("%s\t", node.val);
if (node.left != null) {
queue.offer(node.left);
nextLevel++;
}
if (node.right != null) {
queue.offer(node.right);
nextLevel++;
}
toBePrint--;
if (toBePrint == 0) {
System.out.println();
toBePrint = nextLevel;
nextLevel = 0;
}
}
}

public static class TreeNode {
int val = 0;
TreeNode left = null;
TreeNode right = null;

public TreeNode(int val) {
this.val = val;

}

}
}

扩展-按「之」字打印

jz32-从上到下打印二叉树_20220124175912_2022-01-24-17-59-13
jz32-从上到下打印二叉树_20220124175849_2022-01-24-17-58-49

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
package algorithm.jzoffer;

import java.util.Stack;

/**
* 按之字打印
*
* @author lilou
*/
public class Jz32_4 {

public static void main(String[] args) {
final Jz32_4 jz = new Jz32_4();
final TreeNode node1 = new TreeNode(1);
final TreeNode node2 = new TreeNode(2);
final TreeNode node3 = new TreeNode(3);
final TreeNode node4 = new TreeNode(4);
final TreeNode node5 = new TreeNode(5);
final TreeNode node6 = new TreeNode(6);
final TreeNode node7 = new TreeNode(7);
final TreeNode node8 = new TreeNode(8);
final TreeNode node9 = new TreeNode(9);
final TreeNode node10 = new TreeNode(10);
final TreeNode node11 = new TreeNode(11);
final TreeNode node12 = new TreeNode(12);
final TreeNode node13 = new TreeNode(13);
final TreeNode node14 = new TreeNode(14);
final TreeNode node15 = new TreeNode(15);
node1.left = node2;
node1.right = node3;
node2.left = node4;
node2.right = node5;
node3.left = node6;
node3.right = node7;
node4.left = node8;
node4.right = node9;
node5.left = node10;
node5.right = node11;
node6.left = node12;
node6.right = node13;
node7.left = node14;
node7.right = node15;

jz.PrintFromTopToBottom(node1);
}

public void PrintFromTopToBottom(TreeNode root) {
if (root == null) {
return;
}
Stack<TreeNode> stack1 = new Stack<>();
Stack<TreeNode> stack2 = new Stack<>();
stack1.push(root);

while (!stack1.isEmpty() || !stack2.isEmpty()) {
while (!stack1.isEmpty()) {
final TreeNode node = stack1.pop();
System.out.printf("%s\t", node.val);
if (node.left != null) {
stack2.push(node.left);
}
if (node.right != null) {
stack2.push(node.right);
}
}

// 下一层
System.out.println();
while (!stack2.isEmpty()) {
final TreeNode node = stack2.pop();
System.out.printf("%s\t", node.val);
if (node.right != null) {
stack1.push(node.right);
}
if (node.left != null) {
stack1.push(node.left);
}
}

System.out.println();
}
}

public static class TreeNode {
int val = 0;
TreeNode left = null;
TreeNode right = null;

public TreeNode(int val) {
this.val = val;

}

}
}

剑指 Offer 第 23 题-链表中环的入口结点

描述: 输入一个矩阵,按照从外向里以顺时针的顺序依次打印出每一个数字,例如,如果输入如下 4 X 4 矩阵:

1
2
3
4
[[1,2,3,4],
[5,6,7,8],
[9,10,11,12],
[13,14,15,16]]

则依次打印出数字

[1,2,3,4,8,12,16,15,14,13,9,5,6,7,11,10]

数据范围:

0 <= matrix.length <= 100

0 <= matrix[i].length <= 100

例如:

jz29-顺时针打印矩阵_20220121170748_2022-01-21-17-07-49

方法 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
public class Jz29_2 {
public static void main(String[] args) {
final Jz29_3_1 jz = new Jz29_3_1();
System.out.println(jz.printMatrix(new int[][]{
{1, 2, 3, 1},
{4, 5, 6, 1},
{4, 5, 6, 1},
}));

System.out.println(jz.printMatrix(new int[][]{
{1}, {2}, {3}, {4}, {5}
}));

System.out.println(jz.printMatrix(new int[][]{
{1, 2, 3, 4, 5}
}));

System.out.println(jz.printMatrix(new int[][]{
{1, 2, 3, 4},
{5, 6, 7, 8},
{9, 10, 11, 12},
{13, 14, 15, 16}
}));
}

/**
* 对于只有一行或只有一列时,会重复
*/
public ArrayList<Integer> printMatrix(int[][] matrix) {
// 为空的情况
if (matrix == null || matrix.length == 0) {
return new ArrayList<>();
}

ArrayList<Integer> list = new ArrayList<>(matrix.length * matrix[0].length);
int minX = 0;
int maxX = matrix.length - 1;
int minY = 0;
int maxY = matrix[0].length - 1;

int x, y;
while (minX <= maxX && minY <= maxY) {
// 用两个标识来判断本轮是否有下移或左移
boolean down = false;
boolean left = false;

// 从左上角开始
// 向右
for (x = minX, y = minY; y <= maxY; y++) {
list.add(matrix[x][y]);
}

// 向下
for (x = minX + 1, y = maxY; x <= maxX; x++) {
list.add(matrix[x][y]);
down = true;
}

// 向左,左移之前确认是否有下移,防止重复
for (x = maxX, y = maxY - 1; down && y >= minY; y--) {
list.add(matrix[x][y]);
left = true;
}

// 向上,上移之前确认是否有左移,防止重复
for (x = maxX - 1, y = minY; left && x > minX; x--) {
list.add(matrix[x][y]);
}

// 瘦身一圈
minX++;
maxX--;
minY++;
maxY--;
}

return list;
}
}

方法 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
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
public class Jz29_4 {
public static void main(String[] args) {
final Jz29_4 jz = new Jz29_4();
System.out.println(jz.printMatrix(new int[][]{
{1, 2, 3, 1},
{4, 5, 6, 1},
{4, 5, 6, 1},
}));

System.out.println(jz.printMatrix(new int[][]{
{1}, {2}, {3}, {4}, {5}
}));

System.out.println(jz.printMatrix(new int[][]{
{1, 2, 3, 4, 5}
}));

System.out.println(jz.printMatrix(new int[][]{
{1, 2, 3, 4},
{5, 6, 7, 8},
{9, 10, 11, 12},
{13, 14, 15, 16}
}));
}

/**
* 起始点从左上角,沿对角线往中间靠
*/
public ArrayList<Integer> printMatrix(int[][] matrix) {
// 为空的情况
if (matrix == null || matrix.length == 0) {
return new ArrayList<>();
}

ArrayList<Integer> list = new ArrayList<>(matrix.length * matrix[0].length);
int start = 0;
while (matrix.length > start * 2 && matrix[0].length > start * 2) {
addToList(list, matrix, start);
start++;
}

return list;
}

private void addToList(ArrayList<Integer> list, int[][] matrix, int start) {

// max row index
int maxX = matrix.length - 1 - start;
// max column index
int maxY = matrix[0].length - 1 - start;

boolean down = false;
boolean left = false;

// 从左向右
for (int i = start; i <= maxY; ++i) {
list.add(matrix[start][i]);
}

// 从上向下
for (int i = start + 1; i <= maxX; i++) {
list.add(matrix[i][maxY]);
down = true;
}

// 从右向左
for (int i = maxY - 1; down && i >= start; i--) {
list.add(matrix[maxX][i]);
left = true;
}

// 从下向上
for (int i = maxX - 1; left && i > start + 1; i--) {
list.add(matrix[i][start]);
}

}
}

0%