npm全称Node Package Manager,是node.js的模块依赖管理工具。由于npm的源在国外,所以国内用户使用起来各种不方便。下面整理出了一部分国内优秀的npm镜像资源,国内用户可以选择使用。

国内优秀npm镜像

如何使用

有很多方法来配置npm的registry地址,下面根据不同情境列出几种比较常用的方法。以淘宝npm镜像举例:

1.临时使用

1
npm --registry https://registry.npmmirror.com install express

2.持久使用

1
npm config set registry https://registry.npmmirror.com

// 配置后可通过下面方式来验证是否成功

1
npm config get registry

// 或

1
npm info express

3.通过cnpm使用

1
npm install -g cnpm --registry=https://registry.npmmirror.com

// 使用

1
cnpm install express

Yarn

临时修改

1
yarn save 软件名 --registry https://registry.npmmirror.com

全局修改

1
yarn config set registry https://registry.npmmirror.com

参考: https://www.jianshu.com/p/bd03c4c497c5 https://www.cnblogs.com/zh719588366/p/6784848.html

设计与架构究竟是什么?

尽管标题是 设计与架构究竟是什么?然而书中似乎并没有给出答案。反而对于目标给出了清晰的定义。

1
软件架构的终极目标是,用最小的人力成本来满足构建和维护该系统的需求。

对于架构评估,书中给出了简单的说明。

1
一个软件架构的优劣,可以用它满足用户需求所需要的成本来衡量。如果该成本很低,并且在系统的整个生命周期内一直都能维持这样的低成本,那么这个系统的设计就是优良的。如果该系统的每次发布都会提升下一次的变更的成本,那么这个设计就是不好的。

乱麻系统

书中举了一个乱麻系统的例子,说明一个糟糕的架构有多令人难受。乱麻系统的特点如下:

  1. 没有经过设计,匆匆忙忙被构建起来
  2. 为了加快发布速度,拼命地往团队里加入新人
  3. 决策层对代码质量提升和设计结构优化存在着持续的、长久的忽视

导致的后果就是,对开发者来讲带来了很大的挫败感,团队没有人偷懒,拼命工作,然而不管他们投入了多少个人时间,救了多少次火,产出始终上不去。工程师的大部分时间都消耗在对现有系统的修修补补上,而不完成新功能。

对于公司管理层而且,就是成本不断的上升,功能产出确明显下降。

解决之道

书中举了龟兔赛跑的故事,归纳了以下的主题思想:

  1. 慢但是稳,是成功的秘诀
  2. 该比赛并不是拼谁开始跑的快,也是不拼谁更有力气
  3. 心态越急,反而跑得慢

工程师们普遍用一句话来欺骗自己:“我们可以未来再重构代码,产品上线最重要!”但是结果大家都知道,产品上线后重构工作就再没人提起了。

接下来就举例子说明 测试驱动开发(TDD) 的方法论,说明软件开发的一个核心特点:

1
要想跑得快,先要跑得稳。

接下来是另一个极端,某些工程师可能会认为挽救一个系统的唯一办法是抛弃现有系统,设计一个全新的系统来替代。然而

1
过度自信只会使得重构设计陷入和原项目一样的困局中。

个人观点

  1. 关于架构评估,书中只简单讲了评判标准,而且是结果型的标准。当然这样指明了大的方向,而具体的方法论并没有讲。当然架构评估本身就是一门复杂的学问。

  2. 关于乱麻系统一,有的项目就是开发完成就结束了,或者还没开发完成就结束了,对于这一类项目,似乎是什么样的架构并不重要,因为不存在维护的问题。所以一个项目在立项时,有没有考虑过它的生命周期呢?项目的生命周期越长,平摊成本就越低,而收益越大。

  3. 关于乱麻系统二,业务与技术往往是相反的,业务上的事件的发生往往是突发的,而为了应对突发事件产生的系统,往往没有足够的时间留给技术实现团队。这也变相的要求作为技术人,需要准备好架构预案,针对某类系统有提前想的架构模块,业务需求真的到来的时候,需要我们能花费较少的时间就能出来一个还不错的设计,从而满足业务的需要,所以没事多磨刀。

  4. 关于设计一个全新的系统,往往我们面临的是别人留下来一堆代码,而这一堆里,不知道哪些有用,哪些没用,而且出于新人接手的情况下,如果业务上不特别着急,又需要工程师表现时,就很容易出现通过重构以显示自己能力的想法。这个时候我们一定不要忘记架构的目标,就是前面说的:

    软件架构的终极目标是,用最小的人力成本来满足构建和维护该系统的需求。

我们一定要想想,重构是不是能用最小的人力成本来满足构建和维护该系统的需求。

  1. 关于要快还是要稳,我想架构上是要稳,业务上可能是要快,这个时候要看我们如何取舍了。有时候要快才能占领先机。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

import javax.net.ssl.HttpsURLConnection;
import java.net.URL;
import java.security.cert.Certificate;
import java.security.cert.X509Certificate;

public class Main {

public static void main(String[] args) throws Exception {
URL url = new URL("https://www.baidu.com");
HttpsURLConnection connection = (HttpsURLConnection) url.openConnection();
connection.connect();
for (Certificate certificate : connection.getServerCertificates()) {
//第一个就是服务器本身证书,后续的是证书链上的其他证书
X509Certificate x509Certificate = (X509Certificate) certificate;
System.out.println(x509Certificate.getSubjectDN());
System.out.println(x509Certificate.getNotBefore());//有效期开始时间
System.out.println(x509Certificate.getNotAfter());//有效期结束时间
}
connection.disconnect();
}
}

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
import java.util.Arrays;
import java.util.List;

/**
* @author junfeng
*/
public class CsvLine {
private final String[] items;

CsvLine(List<String> items){
if(items==null){
this.items = new String[0];
}else{
this.items = items.toArray(new String[0]);
}
}
public int length(){
return items.length;
}

/**
* 根据下标获取值,从0开始
* @param off 下标
* @return 下标对应的文本
*/
public String get(int off){
if(off<0){
throw new IllegalArgumentException("off should >= 0");
}
if(off>=this.items.length){
return null;
}
return this.items[off];
}

/**
* 获取所有项的内容列表
* @return 所有项的内容列表
*/
public List<String> getItemList(){
return Arrays.asList(this.items);
}

@Override
public String toString(){
return "[" + String.join(",",items) + "]";
}
}
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
import java.io.*;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;

/**
* CSV内容读取类封装
* 格式约定:
* 列分隔符:默认是逗号”,“
* 封闭符:指定源文件中使用的结束字符,默认为双引号。其中包括:双引号、单引号。
*
* 1)每条记录占一行;
* 2)分隔符 前后的空格会被忽略;
* 3)字段中包含有 分隔符,该字段必须用 封闭符 括起来;
* 4)字段中包含有换行符,该字段必须用 封闭符 括起来;
* 5)字段前后包含有空格,该字段必须用 封闭符 括起来;
* 6)字段中的 封闭符 用两个 封闭符 表示;
* 7)第一条记录,可以是字段名;
*
* 分隔符 和 封闭符 是单一字符。
* @author junfeng
*/
public class CsvReader implements Closeable {
char splitChar = ',';
char limitChar = '"';
BufferedReader br;
File file;
Charset charset = StandardCharsets.UTF_8;

/**
* CSV解析类构造函数
* @param file 要读取的文件
*/
public CsvReader(File file){
this.file = file;
}

/**
* CSV解析类构造函数
* @param file 要读取的文件
* @param charset 文件编码
*/
public CsvReader(File file,Charset charset){
this.file = file;
this.charset = charset;
}

/**
* CSV解析类构造函数
* @param file 要读取的文件
* @param charset 文件编码
* @param splitChar 分隔符
* @param limitChar 限定符
*/
public CsvReader(File file,Charset charset,char splitChar,char limitChar){
this.file = file;
this.charset = charset;
this.splitChar = splitChar;
this.limitChar = limitChar;
}

/**
* 读取一行 CSV 内容,正常返回 CsvLine 对象,读到文件结束,则返回null
* @return CsvLine 封装的内容
* @throws IOException 文件读取异常
*/
public CsvLine readLine() throws IOException{
if(br==null){
br = new BufferedReader(new InputStreamReader(new FileInputStream(this.file),charset));
}
// 标记当前是否位于 限定符内
boolean inLimit = false;
List<String> list = new ArrayList<>();
StringBuilder sbItem = new StringBuilder();
// 标记当前项是否由限定符 包括,如果包括 加入项列表时 不会清空前后的空格
boolean itemInLimit = false;
int nextChar = -1;
while (true){
int c = nextChar;
nextChar = -1;
if(c==-1){
c = br.read();
}
if(inLimit){
if(c==limitChar){
nextChar = br.read();
if(nextChar==limitChar){
sbItem.append((char)c);
nextChar = -1;
}else{
inLimit = false;
}
}else{
sbItem.append((char)c);
}
}else{
if(c==limitChar){
inLimit = true;
sbItem = new StringBuilder();
itemInLimit = true;
}else if(c==splitChar){
if(itemInLimit){
list.add(sbItem.toString());
}else{
list.add(sbItem.toString().trim());
}
itemInLimit = false;
sbItem = new StringBuilder();
}else if(c=='\r'){
// 啥也不干
}else if(c=='\n'){
if(sbItem.length()>0){
if(itemInLimit){
list.add(sbItem.toString());
}else{
list.add(sbItem.toString().trim());
}
}
break;
}else{
sbItem.append((char)c);
}
}

if(c==-1){
if(list.size()==0){
return null;
}
break;
}
}
return new CsvLine(list);
}

@Override
public void close() throws IOException {
if(br!=null){
br.close();
}
}

public static void main(String[] args) throws IOException {
CsvReader reader = new CsvReader(new File("c:/tmp/31.csv"),Charset.forName("gbk"));
CsvLine line;
while ((line=reader.readLine())!=null){
System.out.println(line);
}
}
}

1.集合工厂方法

从Java 9开始, List,Set 和 Map 接口中,新的静态工厂方法可以创建这些集合的不可变实例。方便创建简单列表

代码示例:

1
2
3
4
5
6
7
8
public static void main(String[] args){
List<String> list = List.of("a","b","c");
System.out.println(list);
Set<String> set = Set.of("a", "b", "c");
System.out.println(set);
Map<String, String> map = Map.of("k1","v1","k2","v2","k3","v3");
System.out.println(map);
}

输出结果

1
2
3
[a, b, c]
[a, b, c]
{k1=v1, k2=v2, k3=v3}

2.私有接口方法

在Java 8,接口可以有常量变量和抽象方法。我们不能在接口中提供私有方法的实现。现在接口越来越象抽象类了,意味着java通过这种方式变相的实现多继承。

示例代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public interface MyInterface {
private void test(){
System.out.println("private test method");
}
default void doTest(){
test();
}
public static void main(String[] args){
MyInterface myinterface = new MyInterface() {

};
myinterface.doTest();
}
}

关键点:可以在 接口中使用 private 来定义私有接口

3.改进的进程 API

在 Java 9 之前,Process API 仍然缺乏对使用本地进程的基本支持,例如获取进程的 PID 和所有者,进程的开始时间,进程使用了多少 CPU 时间,多少本地进程正在运行等。

Java 9 向 Process API 添加了一个名为 ProcessHandle 的接口来增强 java.lang.Process 类。

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
public static void main(String[] args) throws IOException {
ProcessBuilder pb = new ProcessBuilder("cmd.exe");
String Null = "Null";
Process p = pb.start();
ProcessHandle.Info info = p.info();
System.out.println("进程ID " + p.pid());
System.out.println("可执行文件 " + info.command().orElse(Null));
System.out.println("启动时间 " + info.startInstant().map(i -> i.atZone(ZoneId.systemDefault())
.toLocalDateTime().toString()).orElse(Null));
System.out.println("参数 " + info.arguments().map(a -> Stream.of(a).collect(
Collectors.joining(" "))).orElse(Null));
System.out.println("用户 " + info.user().orElse(Null));
}

输出:

1
2
3
4
5
进程ID 25420
可执行文件 C:\Windows\System32\cmd.exe
启动时间 2022-10-27T10:35:42.124
参数 Null
用户 admin

4. 改进的 Stream API

Java 9 为 Stream 新增了:dropWhile、takeWhile、ofNullable 方法,iterate 方法新增了一个重载方法。

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
public static void main(String[] args){
Stream.of("a","b","c","","e","f").takeWhile(s->!s.isEmpty())
.forEach(System.out::print);
System.out.println("\n===");
Stream.of("a","b","c","","e","f").dropWhile(s-> !s.isEmpty())
.forEach(System.out::print);
System.out.println("\n===");
IntStream.iterate(3, x -> x < 10, x -> x+ 3).forEach(System.out::println);
System.out.println("===");
List list = Stream.ofNullable(null).collect(Collectors.toList());
System.out.println(list.size());
}

输出结果:

1
2
3
4
5
6
7
8
9
abc
===
ef
===
3
6
9
===
0

说明:

  • takeWhile : 满足条件才执行,不满足则退出循环
  • dropWhile : 第一次不满足条件才执行
  • iterate : 方便创建流,可用于测试
  • ofNullable: 防止空指针

5. 改进的 try-with-resources

从java7开始,支持了try-with-resources, 但在java 7 的时候,变量必须定义在try后的括号里。现在可以放出来了,使用更简单 示例代码

1
2
3
4
5
6
7
8
 public static void main(String[] args) throws IOException {
BufferedReader br = new BufferedReader(new FileReader("c:/tmp/b.txt"));
BufferedWriter bw = new BufferedWriter(new FileWriter("c:/tmp/c.txt"));
try(br;bw){
String line = br.readLine();
bw.write(line);
}
}

6. 改进的 Optional 类

在 Java 9 中, Optional 添加了以下方法:

  • stream()

  • ifPresentOrElse()

  • orElse()

    stream()

    从Optional 中获取一个stream, 如果有值则返回该值的stream, 否则返回一个空的流,Stream.empty()。

示例代码:

1
2
3
4
5
6
7
8
9
10
11
public static void main(String[] args){
Optional op = Optional.empty();
System.out.println(op.stream().toList().size());
Optional op2 = Optional.of("1");
System.out.println(op2.stream().toList().size());

System.out.println("===");
Optional<List<String>> op3 = Optional.of(List.of("1","2"));
op3.stream().forEach(t-> System.out.println(t));
System.out.println(op3.stream().toList().size());
}

输出结果:

1
2
3
4
5
0
1
===
[1, 2]
1

ifPresentOrElse

定义

1
public void ifPresentOrElse(Consumer<? super T> action, Runnable emptyAction) 

示例代码:

1
2
3
4
5
6
7
public static void main(String[] args){
Optional<String> op = Optional.of("A");
op.ifPresentOrElse(t->System.out.println(t),()->System.out.println("Empty"));

Optional<String> op1 = Optional.empty();
op1.ifPresentOrElse(t->System.out.println(t),()->System.out.println("Empty"));
}

输出结果

1
2
A
Empty

orElse

如果optional有值,返回对应的值此,否则返回orElse里的内容 定义:

1
public T orElse(T other)

示例代码:

1
2
3
4
5
6
7
public static void main(String[] args){
Optional<String> op = Optional.of("A");
System.out.println(op.orElse("B"));

Optional<String> op1 = Optional.empty();
System.out.println(op1.orElse("B"));
}

输出结果

1
2
A
B

7. 改进的 CompletableFuture API

新增了

  • newIncompleteFuture
  • defaultExecutor
  • copy
  • minimalCompletionStage
  • completeAsync
  • orTimeout
  • completeOnTimeout
  • delayedExecutor
  • completedStage
  • failedFuture
  • failedStage

8. 支持使用var

从 java 10开始,引入了var 来声明局部变量 示例代码:

1
2
3
4
5
6
var str = "Hi";
var v = 0;
for(var i=0;i<10;i++){

}
var list = new ArrayList<String>();

9. 删除的内容

Java 10 删除了很多工具:

  • 命令行工具 javah,可以使用 javac -h 代替。
  • 命令行选项 -X:prof,尽管可以使用 jmap 工具来访问分析信息。

一些从 Java1.2 开始标记的为已弃用的 API 也被永久删除了。包括 java.lang.SecurityManager.inCheck 字段和以下方法:

1
2
3
4
5
6
7
8
9
10
java.lang.SecurityManager.classDepth(java.lang.String)
java.lang.SecurityManager.classLoaderDepth()
java.lang.SecurityManager.currentClassLoader()
java.lang.SecurityManager.currentLoadedClass()
java.lang.SecurityManager.getInCheck()
java.lang.SecurityManager.inClass(java.lang.String)
java.lang.SecurityManager.inClassLoader()
java.lang.Runtime.getLocalizedInputStream(java.io.InputStream)
java.lang.Runtime.getLocalizedOutputStream(java.io.OutputStream)
弃用

整个 SecurityManager 类在 java 17 被标识为 弃用。在将来可能会删除。

JDK 10 也弃用了一些 API。

  • java.security.acl 包已标记为已弃用,也包括 java.security 包中包含各种相关的类(Certificate,Identity,IdentityScope,Singer,auth.Policy)。
  • javax.management.remote.rmi.RMIConnectorServer 类中的 CREDENTIAL_TYPES 被标记为不建议使用。
    • java.io.FileInputStream 和 java.io.FileOutputStream 中的 finalize()方法已被标记为已弃用。所以在 java.util.zip.Deflater / Inflater / ZipFileclasses 中的 finalize()方法也被弃用。

10. 字符串API加强

isBlank()

从java 11 起开始支持,用来判断字符串里是否只包含空字符

定义:

1
public boolean isBlank()

示例代码:

1
2
3
4
public static void main(String[] args){
String s = " ";
System.out.println(s.isBlank());
}

输出结果

1
true

常见的空格,回车,换行,制表符 都会被识别为空白字符。

lines()

定义

1
public Stream<String> lines()

从java 11 起开始支持,按 \r \n 把符串转成stream.

示例代码:

1
2
String str = "a\n b\r\rc";
System.out.println(str.lines().count());

输出结果

1
4

\r\n会只作为一个换行符。连续的 \r\n\r\n 就会识别为两行

strip()

定义

1
public String strip()

从java 11 起开始支持,去掉字符串前后的半角和全角空格,trim()只能去掉半角空格。stripLeading()是去掉左边的空格,stripTrailing()是去掉右边的空格

1
2
3
4
5
6
7
8
9
String str = " Hello\u3000";
// str = 7
System.out.println("str = " + str.length());
// trim = 6
System.out.println("trim = " + str.trim().length());
// strip = 5
System.out.println("strip = " + str.strip().length());
System.out.println("stripLeading[" + str.stripLeading() + "]");
System.out.println("stripTrailing[" + str.stripTrailing() + "]");

输出

1
2
3
4
5
str = 7
trim = 6
strip = 5
stripLeading[HELLO ]
stripTrailing[ HELLO]

repeat

定义

1
public String repeat(int count)

从java 11开始支持,重复当前字符串多次。可以用于日志输出时,输出多个分隔符,比如:======================

1
2
3
String s = "=";
// output ===
System.out.println(s.repeat(3));

indent

定义

1
public String indent(int n) 

从java12开始支持,对字符串的每一行前面加n个空格。当n小于0时,就是去掉n个空格。并且在字符串的最后,会加上换行符。

代码示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
String a = "hi";
System.out.println("[" + a.indent(2) + "]");
System.out.println("[" + a.indent(-2) + "]");
System.out.println("-".repeat(10));

String b = " ha";
System.out.println("[" + b.indent(2) + "]");
System.out.println("[" + b.indent(-2) + "]");
System.out.println("-".repeat(10));

String c = " ho";
System.out.println("[" + c.indent(2) + "]");
System.out.println("[" + c.indent(-2) + "]");
System.out.println("-".repeat(10));

String t = "ta\ntb";
System.out.println("[" + t.indent(2) + "]");
System.out.println("[" + t.indent(-2) + "]");

输出结果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
[  hi
]
[hi
]
----------
[ ha
]
[ha
]
----------
[ ho
]
[ ho
]
----------
[ ta
tb
]
[ta
tb
]

formatted

定义:

1
public String formatted(Object... args)

从java 15开始支持。便于字符串本身就是格式化串时的使用,等价于String.format(this, args)

示例代码:

1
2
String str = "hi %s";
System.out.println(str.formatted("小明"));

输出结果

1
hi 小明

11. 多行字符串

从java 15开始支持多行字符串,String类也增加了配套的方法。

1
2
3
4
5
6
7
8
9
10
11
12
public static void main(String[] args){
String str = """
<div>
<h1>多行文本</h1>
<p>java 支持多行文本语法</p>
</div>
""";
System.out.println(str);
System.out.println("_".repeat(30));
System.out.println(str.stripIndent());
System.out.println("_".repeat(30));
}

输出结果

1
2
3
4
5
6
7
8
9
10
11
12
<div>
<h1>多行文本</h1>
<p>java 支持多行文本语法</p>
</div>

______________________________
<div>
<h1>多行文本</h1>
<p>java 支持多行文本语法</p>
</div>

______________________________

java17会自动把多行文本前面的缩进空格去掉。没有必要再用stripIndent()方法重复处理。

12. 文本转义

定义

1
public String translateEscapes()

从java 15开始支持,把字符串里的\n变成换行符,其它的\t \r 也同样支持。

示例代码:

1
2
3
String str = "ha\\noo\\tkk";
System.out.println(str);
System.out.println(str.translateEscapes());

输出结果

1
2
3
ha\noo\tkk
ha
oo kk

13. 文件读写更方便

java从1.7开始提供了Files工具类,java 11 增加了 writeString 和 readString,可以更方便的读写文本文件。

定义

1
2
3
4
public static String readString(Path path) throws IOException
public static String readString(Path path, Charset cs) throws IOException
public static Path writeString(Path path, CharSequence csq, OpenOption... options)throws IOException
public static Path writeString(Path path, CharSequence csq, Charset cs, OpenOption... options)throws IOException

默认使用的是UTF-8的编码,示例代码如下

1
2
3
String dir= "C://tmp/hello.txt";
Path path = Files.writeString(Path.of(dir), "java 学习");
String str = Files.readString(path);

java 12 还增加了 mismatch 方法。对比两个文件,如果内容一致,会返回 -1 ,如果内容不同,会返回不同的字节开始位置。

14. HttpClient

java 11 支持了HttpClient, Http请求可以不用再引第三方包了。

1
2
3
4
5
6
7
8
9
 public static void main(String[] args) throws IOException, InterruptedException {
var request = HttpRequest.newBuilder()
.uri(URI.create("https://baidu.com"))
.GET()
.build();
var client = HttpClient.newHttpClient();
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
System.out.println(response.body());
}

15. switch表达式改进

可以把多个case写在一行了,通过 -> 来执行相关动作。

1
2
3
4
5
6
7
8
String month = "june";
switch (month) {
case "march", "april", "may" -> System.out.println("春天");
case "june", "july", "august" -> System.out.println("夏天");
case "september", "october", "november" -> System.out.println("秋天");
case "december", "january", "february" -> System.out.println("冬天");
default -> System.out.println("不知道什么季节");
}

16. NumberFormat

方便输出数字格式化后的显示

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Locale locale = new Locale("zh", "CN");
NumberFormat numberFormat = NumberFormat.getCompactNumberInstance(locale, NumberFormat.Style.SHORT);

System.out.println(numberFormat.format(100));
System.out.println(numberFormat.format(1000));
System.out.println(numberFormat.format(10000));
System.out.println(numberFormat.format(100000));
System.out.println(numberFormat.format(1000000));

// 设置小数位数
numberFormat.setMaximumFractionDigits(2);
System.out.println(numberFormat.format(1234));
System.out.println(numberFormat.format(123456));
System.out.println(numberFormat.format(12345678));
System.out.println(numberFormat.format(1234567890));

输出内容:

1
2
3
4
5
6
7
8
9
100
1,000
1万
10万
100万
1,234
12.35万
1234.57万
12.35亿

如果把第一行改成美国的Locale

1
Locale locale = new Locale("en", "US");

那么就会输出:

1
2
3
4
5
6
7
8
9
100
1K
10K
100K
1M
1.23K
123.46K
12.35M
1.23B

17. instanceof

java 14 简化了 instanceof 关键字的用法,现在你可以这样写

1
2
3
4
Object obj = "abc";
if(obj instanceof String str){
System.out.println(str);
}

18. record

record关键字,在java 17转正。它是和class, interface, enum 同类的关键字。 我们可以创建一个Point.java文件,里面就写:

1
public record Point(int x,int y) {}

然后就可以这样来使用了

1
2
3
4
5
6
7
8
public class RecordTest {
public static void main(String[] args){
Point p = new Point(1,2);
System.out.println(p.x());
System.out.println(p.y());
System.out.println(p.toString());
}
}

它适用于,简单不可变对象的封装。在某些场景下,可以替代lombok

19. sealed class 密封类

先看代码

1
2
3
4
5
6
public sealed class Person permits Female, Male {
}
public final class Male extends Person{
}
public final class Female extends Person {
}

上面我们定义了三个类,Person, Male 和 Female,即 人,男人,女人。 密封类就是增加了一种可能,明确定义某个类只能被指定的类继承。其它类无法对其进行继承。

从上面的例子我们看到 Person类前面使用了 sealed class 对其进行声明,在后面通过 permits 关键字指定了它只能被Female, Male这两个类进行继承。

Female, Male 这两个类在声明的时候必须指定它是final,还是 sealed ,还是non-sealed。如果指定为non-sealed,那么它就以可以被随意继承了。

看下面的代码:

1
2
3
4
5
6
7
8
9
10
public sealed class Person permits Female, Male {
}
public final class Female extends Person {
}
public sealed class Male extends Person permits OldMale {
}
public non-sealed class OldMale extends Male{
}
public class CityOldMale extends OldMale{
}

这里我们把Male改为sealed修饰的类,指定其唯一子类 OldMale。但在声明 OldMale 指定为 non-sealed, 这么一来 OldMale 就又可以被随意的继承了。比如上面的 CityOldMale。

注意 上面的代码是5个类,我自己测试时是用了5个文件,大家可以自己试试以加深印象。

其它更新

在 Java 11 支持了 Unicode 10 之后, Java 12 支持了 Unicode 11,支持操作更多的表情、符号。

垃圾回收优化,引入 ZGC回收

java 14 移除了CMS(Concurrent Mark Sweep)垃圾收集器

官方参考

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

function messageError(message,title){
console.log(message,title)
}
/**
* 状态检查
*/
export function checkStatus(response) {
if (response && response.status >= 200 && response.status < 400) {
return response;
}
if (response && response.status === 401) {
messageError('会话超时,请重新登录','会话超时,请重新登录')
if(typeof window !== "undefined") {
sessionStorage.clear();
}
}
if (!response) {
messageError('您的网络发生异常,无法连接服务器','网络异常')
} else {
const { status, url, statusText } = response;
messageError(statusText,`请求错误 ${status}: ${url}`)
}
}

var access_token = '';
if(typeof window !== "undefined") {
access_token = sessionStorage.getItem('token') ''
}

const headerParams = {
token: access_token,
}

/**
* 请求接口封装
*/
export async function request(url,headers) {
try{
const fetchResponse = await fetch(url, headers);
const response = checkStatus(fetchResponse);
if(response){
return response.json();
}
}catch(err){
messageError(url,'请求失败')
}
}

/**
* post
* @param url
* @param params
* @param headers
*/
export function post(url,body,headers){
return request(url, {
method: 'POST',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
...headerParams,
...headers,
},
body: JSON.stringify(body),
});
}

/**
* get
* @param url 请求URL
* @param params 请求参数
* @param headers 额外HTTP头
*/
export function get(url, params, headers){
if (params) {
const paramsArray = [];
//拼接参数
Object.keys(params).forEach(key =>
paramsArray.push(key + '=' + params[key]),
);
if (url.search(/\?/) === -1) {
url += '?' + paramsArray.join('&');
} else {
url += '&' + paramsArray.join('&');
}
}

return request(url, {
method: 'GET',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
...headerParams,
...headers,
},
});
}

/**
* put
* @param url 请求URL
* @param body 正文内容
* @param headers 额外请求头
*/
export function put(url,body,headers){
return request(url, {
method: 'PUT',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
...headerParams,
...headers,
},
body: JSON.stringify(body),
});
}

声明:本人从未参与任何健康码系统的开发,此文是我基于自己对健康码的使用,相关新闻的阅读,按我自己的理解,整理出来的文章。如果跟某些系统有技术上的相似,纯属巧合。

摘要: 本文尝试从一个旁观者的身份,设计一个省的健康码系统。讲述一个健康码系统包含的主要功能,主要流程。技术上讲述 技术要求,技术架构,流程设计,安全处理,并发处理。 希望能通过一个相对完整的技术文档,给自己一个纸上谈兵的机会,给各位一个参考。

正文: 随着新冠疫情的发展,各省市都建设了配套的健康码系统和APP。一开始只是能查看健康码(绿码,黄码,红码),后来再支持了核酸码,同时健康码上能查看核酸信息。 尽管亮码看起来很简单,但还是发生过不少事故;2022年9月1日开始,成都开始进入“静默模式”,市民按要求开始有序做核酸检测,目标是全市2000万人每天核酸检测一次。但9月2日、3日,成都核酸检测系统出现崩溃的情况,导致一天一检并不顺利。2021年12月20日,西安“一码通”崩溃;2021年12月25日,天津健康码出现异常。2022年1月粤康码公告显示,10日上午8:31平台检测到粤康码流量异常增大,最高达每分钟140万次,超出承载极限,触发系统保护机制,导致部分用户访问粤康码缓慢或异常,运行保障团队紧急处置,于9:04部分缓解,9:56完全恢复顺畅运行。 上面是基于部分新闻报道的整理,说明健康码系统容易出故障,而且一旦出现故障对民众的生活都会造成较大影响,很容易就民怨沸腾。坦白讲又有多少人真的处理过1分钟140万次请求呢,按秒来算就是2.3万次每秒。

昨天晚上,沙瑞金书记托梦给我,通知我已经中标汉东省健康码系统的项目,所以我赶紧开始写文档。

一、主要功能

健康码系统使用人分为五类:民众用户,采样员,检测员,客服,系统管理员。

  1. 民众用户指普通民众,即生活在汉东省的所有人,需要通过健康码系统完成查看健康码,查看核酸码。
  2. 采样员指为普通民众进行核酸采样的工作人员,需要通过扫描 普通用户出示的健康码,登记录采样信息。
  3. 检测员指对核酸样本进行检查的工作人员,需要通过扫描采样管上的二维码登记核酸检测结果。
  4. 客服指对健康码异常情况进行处理的工作人员,负责接听民众的申诉,核实信息,按需要调整健康码状态。
  5. 系统管理员指能创建各种工作人员账号,分配想着权限的工作人员。

接入终端包括:

  1. 健康码APP,给民众用户使用, 民众可以使用健康码APP查看自己的健康码,核酸码,核酸记录。
  2. 健康码微信小程序,给民众用户使用,功能同健康码APP。
  3. 健康码支付宝小程序,给民众用户使用,功能同健康码APP。
  4. 采样APP,给采样员使用,采样员通过采样APP完成扫码登记采样。
  5. 健康码web管理系统,给检测员,客服,系统管理员使用。

民众用户

  1. 民众用户能通过手机号验证码或用户名密码登录健康码APP。
  2. 民众用户能通过健康码APP查看健康码
  3. 民众用户能通过健康码APP查看核酸码,提供给采样员扫码完成核酸采样登记
  4. 民众用户能通过健康码APP查看核酸检测记录,检测结果

采样员

  1. 采样员能通过手机号验证码或用户名密码登录采样APP。

  2. 采样员能通过采样APP完成检测试管条形码扫码登录,完成民众用户的核酸码扫描登记

    检测员

  3. 检测员能通过用户名密码登录健康码web管理系统

  4. 检测员能通过扫描检测试管条形码登记检测结果

  5. 检测员能批量登记检测结果

客服

  1. 客服能通过用户名密码登录健康码web管理系统
  2. 客服能通过姓名,身份证号码查询民众用户,查看其核酸记录,异常记录
  3. 客服能给特定民众用户设定标记,标记为红码或黄码,需要记录异常原因
  4. 客服能给特定民众用户解除标记,标记为绿码,需要记录异常解除原因

系统管理员

  1. 客服能通过用户名密码登录健康码web管理系统
  2. 系统管理员能添加 采样员, 检测员,客服 和系统管理员

更多详细功能描述请参考《汉东省健康码系统需求规格说明书》。

二、技术要求

  1. 高可用性要求,时间可用性需要达到99.95%, 即年累积不可用时间不得超过285分钟,月累积不可用时间不得超过24分钟。
  2. 高并发要求,对于民众用户的查看健康码和核酸码的请求,需要支持 4.6万次/秒的高峰请求,1分钟280万次。按已知的峰值的2倍设计。
  3. 低延迟要求,对于民众用户的查看健康码和核酸码的请求,99.9%的请求都应当于2秒内响应。即通常情况下用户点击查看健康码,应当在2秒内能正常显示健康码。
  4. 数据防篡改要求,核酸记录一旦录入,不能被随意更改。
  5. 数据备份要求,应当建立完备的数据备份机制,保证数据得到完整快速的备份,防止数据丢失。
  6. 审计要求,系统应该详细记录核酸采样过程,包括但不限于哪位采样员,在什么时间,什么地点对哪位民众进行了采样;哪位检测员,在什么时间,会么地点对哪些样本进行了检测,检测结果是什么。
  7. 审计要求,系统应该详细记录健康码变化过程,包括但不限于哪位客服,在什么时间,对哪位民众进行了健康码变更;记录变更结果及变更原因。
  8. 实名认证要求,系统的服务要求对姓名和身份证号码做实名认证。因为认证工作量巨大,需要支持人脸识别,完成自动实名认证。
  9. 离线支持,由于采样时可能遇到的网络问题或者并发问题,核酸码需要支持离线截图使用。保证采样过程顺畅。
  10. 离线支持,出于保证采样顺畅的保证,允许采样APP短暂离线,即采样APP能够在无网络的情况下完成扫码登记,联网后能自动上传采样记录。保证在网络不稳定的情况下也能正常采样。界面上未同步的数据应当有明显的标识提示。

三、技术架构

分层架构

终端层 终端层的说明请参考本文 一、主要功能中的 接入终端 接入层 接入层是指网络及接入中间件,终端通过接入层访问服务层。

  1. CDN,CDN主要用于静态文件的缓存,缓解源站压力,同时提高终端的访问速度
  2. K8S容器,K8S容器用于装载服务层的微服务,使得维护与扩容变得更可靠便利。
  3. VPN,通过VPN建立虚拟专用网络,防止外部人员非法访问
  4. 堡垒机,堡垒机保证系统维护过程可追踪,可审计

服务层 服务层是指业务的代码实现层,主要业务逻辑代码均在服务层实现。服务层通过微服务技术实现。

  1. CodeQueryService 提供查询核酸码,健康码,核酸记录功能
  2. SampleService 提供采样登记功能
  3. UserService 提供用户登录,实名认证功能
  4. ManageService 提供审计,用户管理,异常申诉功能

存储层 存储层是指数据存储实现层,主要通过使用开源中间件,实现数据的持久化存储,及缓存实现。

  1. Redis 用于缓存用户健康码及核酸码内容
  2. MySQL 用于存储用户信息,健康码状态
  3. Clickhouse 用于存储核酸记录,健康码变更记录
  4. Elastic Search 用于存储系统日志信息
  5. OSS 用于存储用户上传的图片或文件。

外部服务层 外部服务层指系统依赖的第三方服务

  1. 支付宝实名认证,支付宝提供的实名认证服务
  2. 微信实名认证,微信提供的实名认证服务

数据流图

逻辑架构

未完待续

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
SELECT col.name                        AS name,
t.name AS type,
Cast(col.length AS VARCHAR(80)) AS length,
Cast(col.xprec AS VARCHAR(80)) AS precision,
CASE
WHEN EXISTS (SELECT 1
FROM dbo.sysindexes si
INNER JOIN dbo.sysindexkeys sik
ON si.id = sik.id
AND si.indid = sik.indid
INNER JOIN dbo.syscolumns sc
ON sc.id = sik.id
AND sc.colid = sik.colid
INNER JOIN dbo.sysobjects so
ON so.name = si.name
AND so.xtype = 'PK'
WHERE sc.id = col.id
AND sc.colid = col.colid) THEN 'YES'
ELSE 'NO'
END AS 'key',
CASE
WHEN col.isnullable = 1 THEN 'NO'
ELSE 'YES'
END AS isnullable,
Isnull(ep.[value], NULL) AS comment
FROM dbo.syscolumns col
LEFT JOIN dbo.systypes t
ON col.xtype = t.xusertype
LEFT JOIN sys.extended_properties ep
ON col.id = ep.major_id
AND col.colid = ep.minor_id
INNER JOIN dbo.sysobjects obj
ON col.id = obj.id
AND obj.status >= 0
LEFT JOIN sys.schemas s
ON obj.uid = s.schema_id
WHERE s.name = 'schema_name'
AND obj.name = ( 'table_name' );

JAVA开发工程师 任职要求: 1、本科及以上学历,计算机相关专业,3年以上工作经验 2、具有扎实的java技术功底,熟悉SpringBoot、MyBatis等开源框架的应用了解其运行基本原理 3、熟悉数据库设计,如MySQL,能熟练的编写SQL及调优; 熟悉常见的NoSQL,如:Redis 4、熟悉微服务架构,对Spring Cloud 有一定了解优先,了解K8S优先 5、热爱技术研发、注重代码质量,有良好的软件工程和编码规范意识 6、有较好的沟通能力,逻辑清晰,有独立分析和解决问题的能力;有强烈的责任心和团队合作精神,能快速主动融入团队