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

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


  • 首页

  • 归档

  • 搜索

Java8 判空新写法 Java8 判断空指针新写法

发表于 2021-09-26

Java8 判断空指针新写法

  • 小知识,大挑战!本文正在参与“程序员必备小知识”创作活动。

引言

在开发过程中很多时候会遇到判空校验,如果不做判空校验则会产生NullPointerException异常。如下代码直接使用可能会有问题。

1
java复制代码user.getAddress().getProvince();

当user为null时,会产生空指针异常,常规解决办法如下:

1
2
3
4
5
6
java复制代码if(user!=null){
Address address = user.getAddress();
if(address!=null){
String province = address.getProvince();
}
}

这种写法是比较丑陋的,为了避免上述丑陋的写法,让丑陋的设计变得优雅。Java8提供了Optional类来优化这种写法。

API介绍

先介绍一下API,与其他文章不同的是,本文采取类比的方式来讲,同时结合源码。而不像其他文章一样,一个个API罗列出来,让人找不到重点。

1、Optional(),empty(),of(),ofNullable()

这四个函数之间具有相关性,因此放在一组进行记忆。

先说明一下,Optional(T value),即构造函数,它是private权限的,不能由外部调用的。其余三个函数是public权限,供我们所调用。那么,Optional的本质,就是内部储存了一个真实的值,在构造的时候,就直接判断其值是否为空。好吧,这么说还是比较抽象。直接上Optional(T value)构造函数的源码,如下图所示

微信图片_20210926140757.jpg
那么,of(T value)的源码如下

1
2
3
java复制代码public static <T> Optional<T> of(T value) {
return new Optional<>(value);
}

也就是说of(T value)函数内部调用了构造函数。根据构造函数的源码我们可以得出两个结论:

  • 通过of(T value)函数所构造出的Optional对象,当Value值为空时,依然会报NullPointerException
  • 通过of(T value)函数所构造出的Optional对象,当Value值不为空时,能正常构造Optional对象。

除此之外呢,Optional类内部还维护一个value为null的对象,大概就是长下面这样的

1
2
3
4
5
6
7
8
9
10
11
12
13
java复制代码public final class Optional<T> {
//省略....
private static final Optional<?> EMPTY = new Optional<>();
private Optional() {
this.value = null;
}
//省略...
public static<T> Optional<T> empty() {
@SuppressWarnings("unchecked")
Optional<T> t = (Optional<T>) EMPTY;
return t;
}
}

那么,empty()的作用就是返回EMPTY对象。
好了铺垫了这么多,可以说ofNullable(T value)的作用了,上源码

1
2
3
java复制代码public static <T> Optional<T> ofNullable(T value) {
return value == null ? empty() : of(value);
}

好吧,大家应该都看得懂什么意思了。相比较of(T value)的区别就是:

  • 当value值为null时,of(T value)会报NullPointerException异常;
  • ofNullable(T value)不会throw Exception,ofNullable(T value)直接返回一个EMPTY对象。

那是不是意味着,我们在项目中只用ofNullable函数而不用of函数呢?

不是的,一个东西存在那么自然有存在的价值。当在运行过程中,不想隐藏NullPointerException。而是要立即报告,这种情况下就用Of函数。但是不得不承认,这样的场景真的很少。博主也仅在写junit测试用例中用到过此函数。

2、orElse(),orElseGet()和orElseThrow()

这三个函数放一组进行记忆,都是在构造函数传入的value值为null时,进行调用的。orElse和orElseGet的用法如下所示,相当于value值为null时,给予一个默认值:

1
2
3
4
5
6
7
8
9
10
11
12
java复制代码@Test
public void test() {
User user = null;
user = Optional.ofNullable(user).orElse(createUser());
user = Optional.ofNullable(user).orElseGet(() -> createUser());

}
public User createUser(){
User user = new User();
user.setName("zhangsan");
return user;
}

这两个函数的区别:当user值不为null时,orElse函数依然会执行createUser()方法,而orElseGet函数并不会执行createUser()方法,大家可自行测试。

至于orElseThrow,就是value值为null时,直接抛一个异常出去,用法如下所示:

1
2
java复制代码User user = null;
Optional.ofNullable(user).orElseThrow(()->new Exception("用户不存在"));

3、map()和flatMap()

这两个函数放在一组记忆,这两个函数做的是转换值的操作。

直接上源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
java复制代码public final class Optional<T> {
//省略....
public<U> Optional<U> map(Function<? super T, ? extends U> mapper) {
Objects.requireNonNull(mapper);
if (!isPresent())
return empty();
else {
return Optional.ofNullable(mapper.apply(value));
}
}
//省略...
public<U> Optional<U> flatMap(Function<? super T, Optional<U>> mapper) {
Objects.requireNonNull(mapper);
if (!isPresent())
return empty();
else {
return Objects.requireNonNull(mapper.apply(value));
}
}
}

这两个函数,在函数体上没什么区别。唯一区别的就是入参,map函数所接受的入参类型为Function<? super T, ? extends U>,而flapMap的入参类型为Function<? super T, Optional>。

在具体用法上,对于map而言:

如果User结构是下面这样的

1
2
3
4
5
6
java复制代码public class User {
private String name;
public String getName() {
return name;
}
}

这时候取name的写法如下所示

1
java复制代码String city = Optional.ofNullable(user).map(u-> u.getName()).get();

对于flatMap而言:

如果User结构是下面这样的

1
2
3
4
5
6
java复制代码public class User {
private String name;
public Optional<String> getName() {
return Optional.ofNullable(name);
}
}

这时候取name的写法如下所示

1
java复制代码String city = Optional.ofNullable(user).flatMap(u-> u.getName()).get();

4、isPresent()和ifPresent(Consumer<? super T> consumer)

这两个函数放在一起记忆,isPresent即判断value值是否为空,而ifPresent就是在value值不为空时,做一些操作。这两个函数的源码如下

1
2
3
4
5
6
7
8
9
10
11
java复制代码public final class Optional<T> {
//省略....
public boolean isPresent() {
return value != null;
}
//省略...
public void ifPresent(Consumer<? super T> consumer) {
if (value != null)
consumer.accept(value);
}
}

5、filter(Predicate<? super T> predicate)

源码:

1
2
3
4
5
6
7
8
java复制代码public final class Optional<T> {
//省略....
Objects.requireNonNull(predicate);
if (!isPresent())
return this;
else
return predicate.test(value) ? this : empty();
}

filter 方法接受一个 Predicate 来对 Optional 中包含的值进行过滤,如果包含的值满足条件,那么还是返回这个 Optional;否则返回 Optional.empty。

用法如下

1
java复制代码Optional<User> user1 = Optional.ofNullable(user).filter(u -> u.getName().length()<6);

如上所示,如果user的name的长度是小于6的,则返回。如果是大于6的,则返回一个EMPTY对象。

实战

例一

在函数方法中,以前写法

1
2
3
4
5
6
7
8
9
10
11
java复制代码public String getCity(User user)  throws Exception{
if(user!=null){
if(user.getAddress()!=null){
Address address = user.getAddress();
if(address.getCity()!=null){
return address.getCity();
}
}
}
throw new Excpetion("取值错误");
}

JAVA8写法

1
2
3
4
5
6
java复制代码public String getCity(User user) throws Exception{
return Optional.ofNullable(user)
.map(u-> u.getAddress())
.map(a->a.getCity())
.orElseThrow(()->new Exception("取指错误"));
}

例二

比如,在主程序中,以前写法

1
2
3
java复制代码if(user!=null){
dosomething(user);
}

JAVA8写法

1
2
3
4
java复制代码Optional.ofNullable(user)
.ifPresent(u->{
dosomething(u);
});

例三

以前写法

1
2
3
4
5
6
7
8
9
10
11
12
java复制代码public User getUser(User user) throws Exception{
if(user!=null){
String name = user.getName();
if("zhangsan".equals(name)){
return user;
}
}else{
user = new User();
user.setName("zhangsan");
return user;
}
}

java8写法

1
2
3
4
5
6
7
8
9
java复制代码public User getUser(User user) {
return Optional.ofNullable(user)
.filter(u->"zhangsan".equals(u.getName()))
.orElseGet(()-> {
User user1 = new User();
user1.setName("zhangsan");
return user1;
});
}

其他的例子,不一一列举了。不过采用这种链式编程,虽然代码优雅了。但是,逻辑性没那么明显,可读性有所降低,大家项目中看情况酌情使用。

本文转载自: 掘金

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

聊聊日常开发中,你会选择哪款Log框架?

发表于 2021-09-26

小知识,大挑战!本文正在参与“程序员必备小知识”创作活动

本篇文章主要讲解我在工作中是使用什么日志框架,因为大家都知道:适合的才是最好的。springboot 相信大家都用过吧,是的,关于日志的选择,springboot 都帮我们做好了,它的日志门面选用的就是 SLF4J,而日志实现选用的是Logback。

至于它为什么会这样选择?我觉得还是有一定的道理的。至于门面为什么选择SLF4J,不作过多赘述,还是来聊聊实现为什么选择Logback吧。

Logback 作为流行的 log4j 项目的继承者。这两个框架都是同一个人写的,性能比 log4j 要好。至于 log4j2 ,它是log4j 1.x 的升级版,参考了 logback 的一些优秀的设计,并且修复了一些问题,因此带来了一些重大的提升。为什么没有选择它呢?我认为还是因为其他的一些框架没有更好的和log4j2 进行适配起来。导致现在流行的日志框架都还是选择Logback。

日志的用途

日志的用途大致可以归纳成以下三种:

  • 问题追踪:通过日志不仅仅包括我们程序的一些bug,也可以在安装配置时,通过日志可以发现问题。
  • 状态监控:通过实时分析日志,可以监控系统的运行状态,做到早发现问题、早处理问题。
  • 安全审计:审计主要体现在安全上,通过对日志进行分析,可以发现是否存在非授权的操作。

日志的级别

数字越大,级别越高,框架只会输出大于等于当前日志级别的信息。

  • ERROR 40
  • WARN 30
  • INFO 20
  • DEBUG 10
  • TRACE 0

Slf4j的使用

springboot默认帮我们配置好了日志。spring-boot-starter这个依赖里面已经为我们集成好了,自己去看看。

第一种方式

1
2
3
4
5
java复制代码private static final Logger logger = LoggerFactory.getLogger(Test.class);

logger.info();

...

默认的级别上info,按上面的排名只会输出 info、warn、error级别以上的日志。

在获取logger对象时,一般都是将本类的class传递进去,在默认的格式在日志输出时会把每条日志信息所在的class名输出出来。

第二种方式

使用 Lombok

Lombok不仅仅提供了强大的@Data注解,同时支持日志相关。

@Slf4j添加在类上,我们就不用再手动的获取Logger对象了,而是直接使用log。

1
2
3
java复制代码log.debug("dubug..."); 
log.info("info...");
log.error("error...");

输出格式

1
2
java复制代码log.info("name = " + name + " ,age = " + age);
log.info("name:{},age: {}", name, age);

配置文件

我们可以在配置文件中修改日志的默认配置。

image-20210926113424811

logback-spring.xml

下面是我经常用到的一些配置,你们可以根据自己的需要进行一些修改,文件默认放在resources目录下。

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
xml复制代码<?xml version="1.0" encoding="UTF-8"?>
<configuration debug="false">
<!--设置存储路径变量-->
<property name="LOG_HOME" value="./logs"/>

<!--控制台输出appender-->
<appender name="console" class="ch.qos.logback.core.ConsoleAppender">
<!--设置输出格式-->
<encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
<!--格式化输出:%d表示日期,%thread表示线程名,%-5level:级别从左显示5个字符宽度%msg:日志消息,%n是换行符-->
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n</pattern>
<!--设置编码-->
<charset>UTF-8</charset>
</encoder>
</appender>

<!--打印所有日志-->
<appender name="logFile" class="ch.qos.logback.core.rolling.RollingFileAppender">
<!--日志名,指定最新的文件名,其他文件名使用FileNamePattern -->
<File>${LOG_HOME}/log.log</File>
<!--文件滚动模式 按照时间,每天产生一个-->
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<!--日志文件输出的文件名,使用gz压缩-->
<FileNamePattern>${LOG_HOME}/log.%d{yyyy-MM-dd}.log.gz</FileNamePattern>
<!--日志文件保留天数-->
<MaxHistory>30</MaxHistory>
<!--按大小分割同一天的-->
<!-- <timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">-->
<!-- <maxFileSize>100MB</maxFileSize>-->
<!-- </timeBasedFileNamingAndTriggeringPolicy>-->
</rollingPolicy>

<!--输出格式-->
<encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
<!--格式化输出:%d表示日期,%thread表示线程名,%-5level:级别从左显示5个字符宽度%msg:日志消息,%n是换行符-->
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n</pattern>
<!--设置编码-->
<charset>UTF-8</charset>
</encoder>
<!--在root指定的级别之上再次进行过滤,输出大于等于level,可通过onMatch和onMisMatch来确定只输出某个级别的 -->
<filter class="ch.qos.logback.classic.filter.LevelFilter">
<level>INFO</level>
</filter>
</appender>

<!--打印Error日志-->
<appender name="errorLogFile" class="ch.qos.logback.core.rolling.RollingFileAppender">
<!--日志名,指定最新的文件名,其他文件名使用FileNamePattern -->
<File>${LOG_HOME}/error.log</File>
<!--文件滚动模式 按照时间,每天产生一个-->
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<!--日志文件输出的文件名,使用gz压缩-->
<FileNamePattern>${LOG_HOME}/error.%d{yyyy-MM-dd}.log.gz</FileNamePattern>
<!--日志文件保留天数-->
<MaxHistory>30</MaxHistory>
</rollingPolicy>

<!--输出格式-->
<encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
<!--格式化输出:%d表示日期,%thread表示线程名,%-5level:级别从左显示5个字符宽度%msg:日志消息,%n是换行符-->
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n</pattern>
<!--设置编码-->
<charset>UTF-8</charset>
</encoder>

<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
<!--设置日志级别,过滤掉info日志,只输入error日志-->
<level>ERROR</level>
</filter>
</appender>


<!--指定基础的日志输出级别-->
<root level="INFO">
<!--appender将会添加到这个loger-->
<appender-ref ref="console"/>
<appender-ref ref="logFile"/>
<appender-ref ref="errorLogFile"/>
</root>
</configuration>

使用示例

1
2
3
4
5
6
7
8
9
10
11
12
13
java复制代码import lombok.extern.slf4j.Slf4j;

@Slf4j
public class TestService
{

public static void main(String[] args)
{
log.info("info信息");
log.warn("warn信息");
log.error("error信息");
}
}

小结

以上就是我平时在工作中用到日志框架的简单的使用。同时也提醒大家,在输出日志的时候要谨慎,由于输出log过程需要进行磁盘操作,所以log信息一定要言简意赅,不要输出一些无用的log。

本文转载自: 掘金

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

这个 MySQL bug 让我大开眼界!

发表于 2021-09-26

这周收到一个 sentry 报警,如下 SQL 查询超时了。

1
sql复制代码select * from order_info where uid = 5837661 order by id asc limit 1

执行show create table order_info 发现这个表其实是有加索引的

1
2
3
4
5
6
7
8
less复制代码CREATE TABLE `order_info` (
 `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
 `uid` int(11) unsigned,
 `order_status` tinyint(3) DEFAULT NULL,
... 省略其它字段和索引
 PRIMARY KEY (`id`),
 KEY `idx_uid_stat` (`uid`,`order_status`),
) ENGINE=InnoDB DEFAULT CHARSET=utf8

理论上执行上述 SQL 会命中 idx_uid_stat 这个索引,但实际执行 explain 查看

1
sql复制代码explain select * from order_info where uid = 5837661 order by id asc limit 1

可以看到它的 possible_keys(此 SQL 可能涉及到的索引) 是 idx_uid_stat,但实际上(key)用的却是全表扫描

我们知道 MySQL 是基于成本来选择是基于全表扫描还是选择某个索引来执行最终的执行计划的,所以看起来是全表扫描的成本小于基于 idx_uid_stat 索引执行的成本,不过我的第一感觉很奇怪,这条 SQL 虽然是回表,但它的 limit 是 1,也就是说只选择了满足 uid = 5837661 中的其中一条语句,就算回表也只回一条记录,这种成本几乎可以忽略不计,优化器怎么会选择全表扫描呢。

当然怀疑归怀疑,为了查看 MySQL 优化器为啥选择了全表扫描,我打开了 optimizer_trace 来一探究竟

画外音:在MySQL 5.6 及之后的版本中,我们可以使用 optimizer trace 功能查看优化器生成执行计划的整个过程

使用 optimizer_trace 的具体过程如下

1
2
3
4
ini复制代码SET optimizer_trace="enabled=on";   // 打开 optimizer_trace
SELECT * FROM order_info where uid = 5837661 order by id asc limit 1
SELECT * FROM information_schema.OPTIMIZER_TRACE; // 查看执行计划表
SET optimizer_trace="enabled=off"; // 关闭 optimizer_trace

MySQL 优化器首先会计算出全表扫描的成本,然后选出该 SQL 可能涉及到到的所有索引并且计算索引的成本,然后选出所有成本最小的那个来执行,来看下 optimizer trace 给出的关键信息

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
json复制代码{
 "rows_estimation": [
  {
     "table": "`rebate_order_info`",
     "range_analysis": {
       "table_scan": {
         "rows": 21155996,
         "cost": 4.45e6   // 全表扫描成本
      }
    },
    ...
     "analyzing_range_alternatives": {
         "range_scan_alternatives": [
        {
           "index": "idx_uid_stat",
           "ranges": [
           "5837661 <= uid <= 5837661"
          ],
           "index_dives_for_eq_ranges": true,
           "rowid_ordered": false,
           "using_mrr": false,
           "index_only": false,
           "rows": 255918,
           "cost": 307103, // 使用idx_uid_stat索引的成本
           "chosen": true
          }
        ],
      "chosen_range_access_summary": { // 经过上面的各个成本比较后选择的最终结果
        "range_access_plan": {
            "type": "range_scan",
            "index": "idx_uid_stat", // 可以看到最终选择了idx_uid_stat这个索引来执行
            "rows": 255918,
            "ranges": [
            "58376617 <= uid <= 58376617"
            ]
        },
        "rows_for_plan": 255918,
        "cost_for_plan": 307103,
        "chosen": true
        }
        }  
  ...

可以看到全表扫描的成本是 4.45e6,而选择索引 idx_uid_stat 的成本是 307103,远小于全表扫描的成本,而且从最终的选择结果(chosen_range_access_summary)来看,确实也是选择了 idx_uid_stat 这个索引,但为啥从 explain 看到的选择是执行 PRIMARY 也就是全表扫描呢,难道这个执行计划有误?

仔细再看了一下这个执行计划,果然发现了猫腻,执行计划中有一个 reconsidering_access_paths_for_index_ordering 选择引起了我的注意

1
2
3
4
5
6
7
8
9
10
11
12
13
json复制代码{
"reconsidering_access_paths_for_index_ordering": {
   "clause": "ORDER BY",
   "index_order_summary": {
     "table": "`rebate_order_info`",
     "index_provides_order": true,
     "order_direction": "asc",
     "index": "PRIMARY", // 可以看到选择了主键索引
     "plan_changed": true,
     "access_type": "index_scan"
}
}
}

这个选择表示由于排序的原因再进行了一次索引选择优化,由于我们的 SQL 使用了 id 排序(order by id asc limit 1),优化器最终选择了 PRIMARY 也就是全表扫描来执行,也就是说这个选择会无视之前的基于索引成本的选择,为什么会有这样的一个选项呢,主要原因如下:

The short explanation is that the optimizer thinks — or should I say hopes — that scanning the whole table (which is already sorted by the id field) will find the limited rows quick enough, and that this will avoid a sort operation. So by trying to avoid a sort, the optimizer ends-up losing time scanning the table.

从这段解释可以看出主要原因是由于我们使用了 order by id asc 这种基于 id 的排序写法,优化器认为排序是个昂贵的操作,所以为了避免排序,并且它认为 limit n 的 n 如果很小的话即使使用全表扫描也能很快执行完,这样使用全表扫描也就避免了 id 的排序(全表扫描其实也就是基于 id 主键的聚簇索引的扫描,本身就是基于 id 排好序的)

如果这个选择是对的那也罢了,然而实际上这个优化却是有 bug 的!实际选择 idx_uid_stat 执行会快得多(只要 28 ms)!网上有不少人反馈这个问题,而且出现这个问题基本只与 SQL 中出现 order by id asc limit n这种写法有关,如果 n 比较小很大概率会走全表扫描,如果 n 比较大则会选择正确的索引。

这个 bug 最早追溯到 2014 年,不少人都呼吁官方及时修正这个bug,可能是实现比较困难,直到 MySQL 5.7,8.0 都还没解决,所以在官方修复前我们要尽量避免这种写法,那么怎么避免呢,主要有两种方案

  1. 使用 force index 来强制使用指定的索引,如下:
1
csharp复制代码select * from order_info force index(idx_uid_stat) where uid = 5837661 order by id asc limit 1

这种写法虽然可以,但不够优雅,如果这个索引被废弃了咋办?于是有了第二种比较优雅的方案

  1. 使用 order by (id+0) 方案,如下
1
sql复制代码select * from order_info where uid = 5837661 order by (id+0) asc limit 1

这种方案也可以让优化器选择正确的索引,更推荐!

巨人的肩膀

  • mysql 优化器 bug 4zsw5.cn/L1zEi

最后,求关注,原创不易,需要一些正反馈,欢迎大家关注我的公众号「码海」,一起进阶,一起牛逼

本文转载自: 掘金

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

5种微服务注册中心如何选型?这几个维度告诉你!

发表于 2021-09-26

1、前言

微服务的注册中心目前主流的有以下四种:

  • Zookeeper
  • Eureka
  • Consul
  • Kubernetes

那么实际开发中到底如何选择呢?这是一个值得深入研究的事情,别着急,今天陈某就带大家深入了解一下这四类注册中心以及如何选型的问题。

这是《Spring Cloud 进阶》专栏第四篇文章,往期文章如下:

  • 五十五张图告诉你微服务的灵魂摆渡者Nacos究竟有多强?
  • openFeign夺命连环9问,这谁受得了?
  • 阿里面试这样问:Nacos、Apollo、Config配置中心如何选型?这10个维度告诉你!

2、为什么需要注册中心?

随着单体应用拆分,首当面临的第一份挑战就是服务实例的数量较多,并且服务自身对外暴露的访问地址也具有动态性。可能因为服务扩容、服务的失败和更新等因素,导致服务实例的运行时状态经常变化,如下图:

商品详情需要调用营销、订单、库存三个服务,存在问题有:

  • 营销、订单、库存这三个服务的地址都可能动态的发生改变,单纯只使用配置的形式需要频繁的变更,如果是写到配置文件里面还需要重启系统,这对生产来说太不友好了
  • 服务是集群部署的形式调用方负载均衡如何去实现

解决第一个问题办法就是用我们用伟人说过一句话,没有什么是加一个中间层解决不了的,这个中间层就是我们的注册中心。

解决第二问题就是关于负载均衡的实现,这个需要结合我们中间层老大哥来实现。

3、如何实现一个注册中心?

对于如何实现注册中心这个问题,首先将服务之间是如何交互的模型抽象出来,我们结合实际的案例来说明这个问题,以商品服务为例:

  1. 当我们搜索商品的时候商品服务就是提供者;
  2. 当我们查询商品详情的时候即服务的提供者又是服务的消费者,消费是订单、库存等服务;
    由此我们需要引入的三个角色就是:中间层(注册中心)、生产者、消费者,如下图:


整体的执行流程如下:

  1. 在服务启动时,服务提供者通过内部的注册中心客户端应用自动将自身服务注册到注册中心,包含主机地址、服务名称等等信息;
  2. 在服务启动或者发生变更的时候,服务消费者的注册中心客户端程序则可以从注册中心中获取那些已经注册的服务实例信息或者移除已下线的服务;

上图还多一个设计缓存本地路由,缓存本地路由是为了提高服务路由的效率和容错性,服务消费者可以配备缓存机制以加速服务路由。更重要的是,当服务注册中心不可用时,服务消费者可以利用本地缓存路由实现对现有服务的可靠调用。

在整个执行的过程中,其中有点有一点是比较难的,就是服务消费者如何及时知道服务的生产者如何及时变更的,这个问题也是经典的生产者消费者的问题,解决的方式有两种:

  1. 发布-订阅模式:服务消费者能够实时监控服务更新状态,通常采用监听器以及回调机制,经典的案例就是Zookeeper;
  2. 主动拉取策略:服务的消费者定期调用注册中心提供的服务获取接口获取最新的服务列表并更新本地缓存,经典案例就是Eureka;

对于如何选择这两种方式,其实还有一个数据一致性问题可以聊聊,比如选择定时器肯定就抛弃了一致性,最求的是最终一致,这里就不深入展开了,另外你可能还会说服务的移除等等这些功能都没介绍,在我看来那只是一个附加功能,注册中心重点还是在于服务注册和发现,其他都是锦上添花罢了。

4、如何解决负载均衡的问题?

负载均衡的实现有两种方式:

  1. 服务端的负载均衡;
  2. 客户端的负载均衡;
    对于实现的方案来说本质上是差不多的,只是说承接的载体不一样,一个是服务端,一个客户端,如下图:

服务端的负载均衡,给服务提供者更强的流量控制权,但是无法满足不同的消费者希望使用不同负载均衡策略的需求。

客户端的负载均衡则提供了这种灵活性,并对用户扩展提供更加友好的支持。但是客户端负载均衡策略如果配置不当,可能会导致服务提供者出现热点,或者压根就拿不到任何服务提供者。

服务端负载均衡典型的代表就是Nginx,客户端负载均衡典型代表是Ribbon,每种方式都有经典的代表,我们都是可以深入学习的。

常见的负载均衡器的算法的实现,常见的算法有以下六种:

1、轮询法

将请求按顺序轮流地分配到后端服务器上,它均衡地对待后端的每一台服务器,而不关心服务器实际的连接数和当前的系统负载。

2、随机法

通过系统的随机算法,根据后端服务器的列表大小值来随机选取其中的一台服务器进行访问。由概率统计理论可以得知,随着客户端调用服务端的次数增多;其实际效果越来越接近于平均分配调用量到后端的每一台服务器,也就是轮询的结果。

3、哈希算法

哈希的思想是根据获取客户端的IP地址,通过哈希函数计算得到的一个数值,用该数值对服务器列表的大小进行取模运算,得到的结果便是客服端要访问服务器的序号。采用源地址哈希法进行负载均衡,同一IP地址的客户端,当后端服务器列表不变时,它每次都会映射到同一台后端服务器进行访问。

4、加权轮询法

不同的后端服务器可能机器的配置和当前系统的负载并不相同,因此它们的抗压能力也不相同。给配置高、负载低的机器配置更高的权重,让其处理更多的请;而配置低、负载高的机器,给其分配较低的权重,降低其系统负载,加权轮询能很好地处理这一问题,并将请求顺序且按照权重分配到后端。

5.加权随机法

与加权轮询法一样,加权随机法也根据后端机器的配置,系统的负载分配不同的权重。不同的是,它是按照权重随机请求后端服务器,而非顺序。

6.最小连接数法

最小连接数算法比较灵活和智能,由于后端服务器的配置不尽相同,对于请求的处理有快有慢,它是根据后端服务器当前的连接情况,动态地选取其中当前
积压连接数最少的一台服务器来处理当前的请求,尽可能地提高后端服务的利用效率,将负责合理地分流到每一台服务器。

5、注册中心如何选型?

现在注册中心的选择也是五花八门,现阶段比较流行有以下几种:

在介绍这个之前大家有些需要了解的知识有CAP、Paxos、Raft算法这里我就不进行过多介绍了。开始介绍以上5种实现注册中心的方式。

1、Zookeeper

这个说起来有点意思的是官方并没有说他是一个注册中心,但是国内Dubbo场景下很多都是使用Zookeeper来完成了注册中心的功能。

当然这有很多历史原因,这里我们就不追溯了,我还是来聊聊作为注册中心使用的情况下,Zookeeper有哪些表现吧。

Zookeeper基础概念

1、三种角色

Leader 角色:一个Zookeeper集群同一时间只会有一个实际工作的Leader,它会发起并维护与各Follwer及Observer间的心跳。所有的写操作必须要通过Leader完成再由Leader将写操作广播给其它服务器。

Follower角色:一个Zookeeper集群可能同时存在多个Follower,它会响应Leader的心跳。Follower可直接处理并返回客户端的读请求,同时会将写请求转发给Leader处理,并且负责在Leader处理写请求时对请求进行投票。

Observer角色:与Follower类似,但是无投票权。

2、四种节点

PERSISTENT-持久节点:除非手动删除,否则节点一直存在于Zookeeper上

EPHEMERAL-临时节点:临时节点的生命周期与客户端会话绑定,一旦客户端会话失效,那么这个客户端创建的所有临时节点都会被移除。

PERSISTENT_SEQUENTIAL-持久顺序节点:基本特性同持久节点,只是增加了顺序属性,节点名后边会追加一个由父节点维护的自增整型数字。

EPHEMERAL_SEQUENTIAL-临时顺序节点:基本特性同临时节点,增加了顺序属性,节点名后边会追加一个由父节点维护的自增整型数字。

3、一种机制

Zookeeper的Watch机制,是一个轻量级的设计。因为它采用了一种推拉结合的模式。一旦服务端感知主题变了,那么只会发送一个事件类型和节点信息给关注的客户端,而不会包括具体的变更内容,所以事件本身是轻量级的,这就是推的部分。然后,收到变更通知的客户端需要自己去拉变更的数据,这就是拉的部分。

Zookeeper如何实现注册中心?

简单来讲,Zookeeper可以充当一个服务注册表(Service Registry),让多个服务提供者形成一个集群,让服务消费者通过服务注册表获取具体的服务访问地址(ip+端口)去访问具体的服务提供者。如下图所示:

每当一个服务提供者部署后都要将自己的服务注册到zookeeper的某一路径上: /{service}/{version}/{ip:port} 。

比如我们的HelloWorldService部署到两台机器,那么Zookeeper上就会创建两条目录:

  • /HelloWorldService/1.0.0/100.19.20.01:16888
  • HelloWorldService/1.0.0/100.19.20.02:16888。

这么描述有点不好理解,下图更直观,

在Zookeeper中,进行服务注册,实际上就是在Zookeeper中创建了一个Znode节点,该节点存储了该服务的IP、端口、调用方式(协议、序列化方式)等。

该节点承担着最重要的职责,它由服务提供者(发布服务时)创建,以供服务消费者获取节点中的信息,从而定位到服务提供者真正IP,发起调用。通过IP设置为临时节点,那么该节点数据不会一直存储在 ZooKeeper 服务器上。

当创建该临时节点的客户端会话因超时或发生异常而关闭时,该节点也相应在 ZooKeeper 服务器上被删除,剔除或者上线的时候会触发Zookeeper的Watch机制,会发送消息给消费者,因此就做到消费者信息的及时更新。

Zookeeper从设计上来说的话整体遵循的CP的原则,在任何时候对 Zookeeper 的访问请求能得到一致的数据结果,同时系统对网络分区具备容错性,在使用 Zookeeper 获取服务列表时,如果此时的 Zookeeper 集群中的 Leader 宕机了,该集群就要进行 Leader 的选举,又或者 Zookeeper 集群中半数以上服务器节点不可用(例如有三个节点,如果节点一检测到节点三挂了 ,节点二也检测到节点三挂了,那这个节点才算是真的挂了),那么将无法处理该请求。

所以说,Zookeeper 不能保证服务可用性。

2、Eureka

Netflix我感觉应该是在酝酿更好的东西的,下面我们重点还是来介绍Ereka 1.x相关的设计。

Eureka由两个组件组成:Eureka服务端和Eureka客户端。Eureka服务器用作服务注册服务器。Eureka客户端是一个java客户端,用来简化与服务器的交互、作为轮询负载均衡器,并提供服务的故障切换支持。

Eureka的基本架构,由3个角色组成:
1、Eureka Server

提供服务注册和发现功能;

2、Service Provider
服务提供方,将自身服务注册到Eureka,从而使服务消费方能够找到;

3、Service Consumer
服务消费方,从Eureka获取注册服务列表,从而能够消费服务

Eureka 在设计时就紧遵AP原则,Eureka Server 可以运行多个实例来构建集群,解决单点问题,实例之间通过彼此互相注册来提高可用性,是一种去中心化的架构,无 master/slave 之分,每一个实例 都是对等的,每个节点都可被视为其他节点的副本。

在集群环境中如果某台 Eureka Server 宕机,Eureka Client 的请求会自动切换到新的 Eureka Server 节点上,当宕机的服务器重新恢复后,Eureka 会再次将其纳入到服务器集群管理之中。

当节点开始接受客户端请求时,所有的操作都会在节点间进行复制操作,将请求复制到该 Eureka Server 当前所知的其它所有节点中。

当一个新的 Eureka Server 节点启动后,会首先尝试从邻近节点获取所有注册列表信息,并完成初始化。Eureka Server 通过 getEurekaServiceUrls() 方法获取所有的节点,并且会通过心跳契约的方式定期更新。

默认情况下,如果 Eureka Server 在一定时间内没有接收到某个服务实例的心跳(默认周期为30秒),Eureka Server 将会注销该实例(默认为90秒, eureka.instance.lease-expiration-duration-in-seconds 进行自定义配置)。

当 Eureka Server 节点在短时间内丢失过多的心跳时,那么这个节点就会进入自我保护模式,这个测试环境的时候需要注意一下。

Eureka的集群中,只要有一台Eureka还在,就能保证注册服务可用,只不过查到的信息可能不是最新的(不保证强一致性)。

除此之外,Eureka还有一种自我保护机制,如果在15分钟内超过**85%**的节点都没有正常的心跳,那么Eureka就认为客户端与注册中心出现了网络故障,此时会出现以下几种情况:

  • Eureka不再从注册表中移除因为长时间没有收到心跳而过期的服务;
  • Eureka仍然能够接受新服务注册和查询请求,但是不会被同步到其它节点上(即保证当前节点依然可用)
  • 当网络稳定时,当前实例新注册的信息会被同步到其它节点中。

3、Nacos

Nacos 无缝支持一些主流的开源生态,如下图:

Nacos 致力于帮助您发现、配置和管理微服务。Nacos 提供了一组简单易用的特性集,帮助您快速实现动态服务发现、服务配置、服务元数据及流量管理。

Nacos 帮助您更敏捷和容易地构建、交付和管理微服务平台。 Nacos 是构建以“服务”为中心的现代应用架构 (例如微服务范式、云原生范式) 的服务基础设施。

Nacos除了服务的注册发现之外,还支持动态配置服务。动态配置服务可以让您以中心化、外部化和动态化的方式管理所有环境的应用配置和服务配置。动态配置消除了配置变更时重新部署应用和服务的需要,让配置管理变得更加高效和敏捷。配置中心化管理让实现无状态服务变得更简单,让服务按需弹性扩展变得更容易。

Nacos特点

服务发现和服务健康监测

Nacos 支持基于 DNS 和基于 RPC 的服务发现。服务提供者使用 原生SDK、OpenAPI、或一个独立的Agent TODO注册 Service 后,服务消费者可以使用DNS TODO 或HTTP&API查找和发现服务。

Nacos 提供对服务的实时的健康检查,阻止向不健康的主机或服务实例发送请求。Nacos 支持传输层 (PING 或 TCP)和应用层 (如 HTTP、MySQL、用户自定义)的健康检查。 对于复杂的云环境和网络拓扑环境中(如 VPC、边缘网络等)服务的健康检查,Nacos 提供了 agent 上报模式和服务端主动检测2种健康检查模式。Nacos 还提供了统一的健康检查仪表盘,帮助您根据健康状态管理服务的可用性及流量。

动态配置服务

动态配置服务可以让您以中心化、外部化和动态化的方式管理所有环境的应用配置和服务配置。

动态配置消除了配置变更时重新部署应用和服务的需要,让配置管理变得更加高效和敏捷。

配置中心化管理让实现无状态服务变得更简单,让服务按需弹性扩展变得更容易。

Nacos 提供了一个简洁易用的UI (控制台样例 Demo) 帮助您管理所有的服务和应用的配置。Nacos 还提供包括配置版本跟踪、金丝雀发布、一键回滚配置以及客户端配置更新状态跟踪在内的一系列开箱即用的配置管理特性,帮助您更安全地在生产环境中管理配置变更和降低配置变更带来的风险。

动态 DNS 服务

动态 DNS 服务支持权重路由,让您更容易地实现中间层负载均衡、更灵活的路由策略、流量控制以及数据中心内网的简单DNS解析服务。动态DNS服务还能让您更容易地实现以 DNS 协议为基础的服务发现,以帮助您消除耦合到厂商私有服务发现 API 上的风险。

Nacos 提供了一些简单的 DNS APIs TODO 帮助您管理服务的关联域名和可用的 IP:PORT 列表.

服务及其元数据管理

Nacos 能让您从微服务平台建设的视角管理数据中心的所有服务及元数据,包括管理服务的描述、生命周期、服务的静态依赖分析、服务的健康状态、服务的流量管理、路由及安全策略、服务的 SLA 以及最首要的 metrics 统计数据。

Nacos支持插件管理

关于Nacos数据的存储来说,支持临时也支持持久化。

关于设计来说支持CP也支持AP,对他来说只是一个命令的切换,随你玩,还支持各种注册中心迁移到Nacos,反正一句话,只要你想要的他就有。

4、Consul

Consul是HashiCorp公司推出的开源工具,Consul由Go语言开发,部署起来非常容易,只需要极少的可执行程序和配置文件,具有绿色、轻量级的特点。Consul是分布式的、高可用的、 可横向扩展的用于实现分布式系统的服务发现与配置。

Consul的特点

服务发现(Service Discovery)

Consul提供了通过DNS或者HTTP接口的方式来注册服务和发现服务。一些外部的服务通过Consul很容易的找到它所依赖的服务。

健康检查(Health Checking)

Consul的Client可以提供任意数量的健康检查,既可以与给定的服务相关联(“webserver是否返回200 OK”),也可以与本地节点相关联(“内存利用率是否低于90%”)。操作员可以使用这些信息来监视集群的健康状况,服务发现组件可以使用这些信息将流量从不健康的主机路由出去。

Key/Value存储

应用程序可以根据自己的需要使用Consul提供的Key/Value存储。 Consul提供了简单易用的HTTP接口,结合其他工具可以实现动态配置、功能标记、领袖选举等等功能。

安全服务通信

Consul可以为服务生成和分发TLS证书,以建立相互的TLS连接。意图可用于定义允许哪些服务通信。服务分割可以很容易地进行管理,其目的是可以实时更改的,而不是使用复杂的网络拓扑和静态防火墙规则。

多数据中心

Consul支持开箱即用的多数据中心. 这意味着用户不需要担心需要建立额外的抽象层让业务扩展到多个区域。

Consul支持多数据中心,在上图中有两个DataCenter,他们通过Internet互联,同时请注意为了提高通信效率,只有Server节点才加入跨数据中心的通信。

在单个数据中心中,Consul分为Client和Server两种节点(所有的节点也被称为Agent),Server节点保存数据,Client负责健康检查及转发数据请求到Server;Server节点有一个Leader和多个Follower,Leader节点会将数据同步到Follower,Server的数量推荐是3个或者5个,在Leader挂掉的时候会启动选举机制产生一个新的Leader。

集群内的Consul节点通过gossip协议(流言协议)维护成员关系,也就是说某个节点了解集群内现在还有哪些节点,这些节点是Client还是Server。单个数据中心的流言协议同时使用TCP和UDP通信,并且都使用8301端口。跨数据中心的流言协议也同时使用TCP和UDP通信,端口使用8302。

集群内数据的读写请求既可以直接发到Server,也可以通过Client使用RPC转发到Server,请求最终会到达Leader节点,在允许数据延时的情况下,读请求也可以在普通的Server节点完成,集群内数据的读写和复制都是通过TCP的8300端口完成。

Consul其实也可以在应用内进行注册,后续采用Spring Cloud全家桶这套做负载

我们这里聊聊关于Consul的应用外的注册:

上图主要多出来两个组件,分别是Registrator和Consul Template,接下来我们介绍下这两个组件如何结合可以实现在应用发进行服务发现和注册。

Registrator:一个开源的第三方服务管理器项目,它通过监听服务部署的 Docker 实例是否存活,来负责服务提供者的注册和销毁。

Consul Template:定时从注册中心服务端获取最新的服务提供者节点列表并刷新 LB 配置(比如 Nginx 的 upstream),这样服务消费者就通过访问 Nginx 就可以获取最新的服务提供者信息,达到动态调节负载均衡的目的。

整体架构图可能是这样:

我们用Registrator来监控每个Server的状态。当有新的Server启动的时候,Registrator会把它注册到Consul这个注册中心上。

由于Consul Template已经订阅了该注册中心上的服务消息,此时Consul注册中心会将新的Server信息推送给Consul Template,Consul Template则会去修改nginx.conf的配置文件,然后让Nginx重新载入配置以达到自动修改负载均衡的目的。

5、Kubernetes

Kubernetes是一个轻便的和可扩展的开源平台,用于管理容器化应用和服务。通过Kubernetes能够进行应用的自动化部署和扩缩容。

在Kubernetes中,会将组成应用的容器组合成一个逻辑单元以更易管理和发现。Kubernetes积累了作为Google生产环境运行工作负载15年的经验,并吸收了来自于社区的最佳想法和实践。

Kubernetes经过这几年的快速发展,形成了一个大的生态环境,Google在2014年将Kubernetes作为开源项目。Kubernetes的关键特性包括:

  • 自动化装箱:在不牺牲可用性的条件下,基于容器对资源的要求和约束自动部署容器。同时,为了提高利用率和节省更多资源,将关键和最佳工作量结合在一起。
  • 自愈能力:当容器失败时,会对容器进行重启;当所部署的Node节点有问题时,会对容器进行重新部署和重新调度;当容器未通过监控检查时,会关闭此容器;直到容器正常运行时,才会对外提供服务。
  • 水平扩容:通过简单的命令、用户界面或基于CPU的使用情况,能够对应用进行扩容和缩容。
  • 服务发现和负载均衡:开发者不需要使用额外的服务发现机制,就能够基于Kubernetes进行服务发现和负载均衡。
  • 自动发布和回滚:Kubernetes能够程序化的发布应用和相关的配置。如果发布有问题,Kubernetes将能够回归发生的变更。
  • 保密和配置管理:在不需要重新构建镜像的情况下,可以部署和更新保密和应用配置。
  • 存储编排:自动挂接存储系统,这些存储系统可以来自于本地、公共云提供商(例如:GCP和AWS)、网络存储(例如:NFS、iSCSI、Gluster、Ceph、Cinder和Floker等)。

Kubernetes属于主从分布式架构,主要由Master Node和Worker Node组成,以及包括客户端命令行工具Kubectl和其它附加项。

Master Node:作为控制节点,对集群进行调度管理,Master主要由三部分构成:

  1. Api Server相当于 K8S 的网关,所有的指令请求都必须经过 Api Server;
  2. Kubernetes调度器,使用调度算法,把请求资源调度到某个 Node 节点;
  3. Controller控制器,维护 K8S 资源对象(CRUD:添加、删除、更新、修改);
  4. ETCD存储资源对象(可以服务注册、发现等等);

Worker Node:作为真正的工作节点,运行业务应用的容器;Worker Node主要包含五部分:

  1. Docker是运行容器的基础环境,容器引擎;
  2. Kuberlet 执行在 Node 节点上的资源操作,Scheduler 把请求交给Api ,然后 Api Sever 再把信息指令数据存储在 ETCD 里,于是 Kuberlet 会扫描 ETCD 并获取指令请求,然后去执行;
  3. Kube-proxy是代理服务,起到负载均衡作用;
  4. Fluentd采集日志;
  5. Pod:Kubernetes 管理的基本单元(最小单元),Pod 内部是容器。Kubernetes 不直接管理容器,而是管理 Pod;

6、总结

1、高可用

这几款开源产品都已经考虑如何搭建高可用集群,有些差别而已;

2、关于CP还是AP的选择

对于服务发现来说,针对同一个服务,即使注册中心的不同节点保存的服务提供者信息不尽相同,也并不会造成灾难性的后果。

但是对于服务消费者来说,如果因为注册中心的异常导致消费不能正常进行,对于系统来说是灾难性,因此我觉得对于注册中心选型应该关注可用性,而非一致性,所以我选择AP。

3、技术体系

对于语言来说我们都是Java技术栈,从这点来说我们更倾向于Eureka、Nacos。

如果公司内部有专门的中间件或者运维团队的可以Consul、Kubernetes,毕竟Kubernetes才是未来,我们追求的就是框架内解决这些问题,不要涉及到应用内的业务开发,我们其实后者是有的,只是可能不能达到能自主研发程度,这样只能要求自己走的远一些。

应用内的解决方案一般适用于服务提供者和服务消费者同属于一个技术体系;应用外的解决方案一般适合服务提供者和服务消费者采用了不同技术体系的业务场景。

关于Eureka、Nacos如何选择,这个选择就比较容易做了,那个让我做的事少,我就选择那个,显然Nacos帮我们做了更多的事。

4、产品的活跃度

这几款开源产品整体上都比较活跃

7、最后说一句

陈某码字整理不易,觉得文章不错的朋友欢迎点赞、转发、在看,谢谢大家的支持!

感兴趣的可以关注陈某公号:码猿技术专栏

本文转载自: 掘金

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

validate-npm-package-name 源码阅读

发表于 2021-09-26
  1. 前言

首先, 很感谢「 若川 」 大佬 提供的 一个「 源码共读 」 平台, 其实我很早就进入了,但是因为做为一个奶爸, 下班后马不停蹄地回家带娃, 所以很少有时间参与 源码共读 的活动来。

今天正好抽出时间,参与一下大佬的新一期的活动, 这一期项目是 validate-npm-package-name, 它是检验一个字符串是否是一个 有效的 包命名

  1. 学习目标

  • 了解 validate-npm-package-name 的作用和使用场景
  1. 工具介绍

Give me a string and I’ll tell you if it’s a valid npm package name.
This package exports a single synchronous function that takes a string as input and returns an object with two properties:
validForNewPackages :: Boolean
validForOldPackages :: Boolean

接受一个字符串参数, 检验该字符串是否是一个有效的包命名,
该工具 提供一个 接受字符串的函数, 并且返回 一个拥有2个属性的对象
validForNewPackages :: Boolean
validForOldPackages :: Boolean

  1. 包命名规则

  1. 包名不能是空字符串;
  2. 所有的字符串必须小写;
  3. 可以包含 连字符 - ;
  4. 包名不得包含任何非 url 安全字符;
  5. 包名不得以 . 或者 _ 开头;
  6. 包名首尾不得包含空格;
  7. 包名不得包含 ~)(‘!* 任意一个字符串;
  8. 包名不得与node.js/io.js 的核心模块 或者 保留名 以及 黑名单相同;
  9. 包名的长度不得超过 214;
  1. 示例

5.1 有效的包名

1
2
3
4
5
6
7
js复制代码var validate = require("validate-npm-package-name")
validate("some-package")
validate("example.com")
validate("under_score")
validate("123numeric")
validate("@npm/thingy")
validate("@jane/foo.js")

5.2 无效的包名

1
2
3
js复制代码// 包含 不符合 第6条 规则
validate("excited!")
validate(" leading-space:and:weirdchars")
  1. 源码阅读

6.1 代码结构

1
2
3
4
5
6
7
8
9
10
js复制代码var scopedPackagePattern = new RegExp('^(?:@([^/]+?)[/])?([^/]+?)$') 
var builtins = require('builtins')
var blacklist = ['node_modules','favicon.ico']
var validate = module.exports = function(name) {
// .... 检验 参数 name 是否 规范
}
validate.scopedPackagePattern = scopedPackagePattern
var done = function (warning, erros) {
// .... 返回 处理结果
}

6.1.1 scopedPackagePattern 正则表达式

var scopedPackagePattern = new RegExp('^(?:@([^/]+?)[/])?([^/]+?)$'

正则表达式可视化, 链接地址:jex.im/regulex/

下载.png
匹配以下字符串

  • @user 以@开头
  • @user/test
  • 非‘/’的字符串

image.png

builtins:列出了 node 所有的内置模块

6.1.2 validate函数, 结合 [包命名规则]

(1)检测传入的参数是否是字符串;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
js复制代码if (name === null) {
errors.push('name cannot be null')
return done(warnings, errors)
}

if (name === undefined) {
errors.push('name cannot be undefined')
return done(warnings, errors)
}

if (typeof name !== 'string') {
errors.push('name must be a string')
return done(warnings, errors)
}
(2)包名不能是空字符串
1
2
3
js复制代码if (!name.length) {
errors.push('name length must be greater than zero')
}
(3)包名不得以 . 或者 _ 开头
1
2
3
4
5
6
js复制代码if (name.match(/^\./)) {
errors.push('name cannot start with a period')
}
if (name.match(/^_/)) {
errors.push('name cannot start with an underscore')
}
(4) 包名首尾不得包含空格
1
2
3
4
js复制代码// trim 方法 可以去掉 字符串 两边的 空白
if (name.trim() !== name) {
errors.push('name cannot contain leading or trailing spaces')
}
(5) 包名不得与node.js/io.js 的核心模块 或者 保留名 以及 黑名单相同
1
2
3
4
5
6
7
8
9
10
11
12
13
js复制代码// No funny business
blacklist.forEach(function (blacklistedName) {
if (name.toLowerCase() === blacklistedName) {
errors.push(blacklistedName + ' is a blacklisted name')
}
})

// core module names like http, events, util, etc
builtins.forEach(function (builtin) {
if (name.toLowerCase() === builtin) {
warnings.push(builtin + ' is a core module name')
}
})
(6)包名的长度不得超过 214
1
2
3
4
5
js复制代码// really-long-package-names-------------------------------such--length-----many---wow
// the thisisareallyreallylongpackagenameitshouldpublishdowenowhavealimittothelengthofpackagenames-poch.
if (name.length > 214) {
warnings.push('name can no longer contain more than 214 characters')
}
(7)所有的字符串必须小写
1
2
3
4
js复制代码// mIxeD CaSe nAMEs
if (name.toLowerCase() !== name) {
warnings.push('name can no longer contain capital letters')
}
(8)包名不得包含 ~)(‘!* 任意一个字符串
1
2
3
js复制代码if (/[~'!()*]/.test(name.split('/').slice(-1)[0])) {
warnings.push('name can no longer contain special characters ("~\'!()*")')
}
(9)包名不得包含任何非 url 安全字符
1
2
3
4
js复制代码const regx = new RegExp('^(?:@([^/]+?)[/])?([^/]+?)$') 
const name = '@user/package'
// ?: 会忽略分组
// ['@user/package', 'user', 'package', index: 0, input: '@user/package', groups: undefined]
1
2
3
4
5
6
7
8
9
10
11
12
13
js复制代码if (encodeURIComponent(name) !== name) {
// Maybe it's a scoped package name, like @user/package
var nameMatch = name.match(scopedPackagePattern)
if (nameMatch) {
var user = nameMatch[1]
var pkg = nameMatch[2]
if (encodeURIComponent(user) === user && encodeURIComponent(pkg) === pkg) {
return done(warnings, errors)
}
}

errors.push('name can only contain URL-friendly characters')
}

6.2 done 函数

1
2
3
4
5
6
7
8
9
10
11
js复制代码var done = function (warnings, errors) {
var result = {
validForNewPackages: errors.length === 0 && warnings.length === 0,
validForOldPackages: errors.length === 0,
warnings: warnings,
errors: errors
}
if (!result.warnings.length) delete result.warnings
if (!result.errors.length) delete result.errors
return result
}
  1. 总结

整个项目并不是太难, 都是一些基本的字符串判断,以及正则匹配,但是值得每个人去学习, 在编写一个工具的时候, 保证代码的逻辑清晰, 代码的书写规范。

本文转载自: 掘金

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

实战!聊聊如何解决MySQL深分页问题

发表于 2021-09-26

前言

大家好,我是捡田螺的小男孩。

我们日常做分页需求时,一般会用limit实现,但是当偏移量特别大的时候,查询效率就变得低下。本文将分4个方案,讨论如何优化MySQL百万数据的深分页问题,并附上最近优化生产慢SQL的实战案例。

  • 公众号:捡田螺的小男孩

limit深分页为什么会变慢?

先看下表结构哈:

1
2
3
4
5
6
7
8
9
10
sql复制代码CREATE TABLE account (
  id int(11) NOT NULL AUTO_INCREMENT COMMENT '主键Id',
  name varchar(255) DEFAULT NULL COMMENT '账户名',
  balance int(11) DEFAULT NULL COMMENT '余额',
  create_time datetime NOT NULL COMMENT '创建时间',
  update_time datetime NOT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
  PRIMARY KEY (id),
  KEY idx_name (name),
  KEY idx_update_time (update_time) //索引
) ENGINE=InnoDB AUTO_INCREMENT=1570068 DEFAULT CHARSET=utf8 ROW_FORMAT=REDUNDANT COMMENT='账户表';

假设深分页的执行SQL如下:

1
bash复制代码select id,name,balance from account where update_time> '2020-09-19' limit 100000,10;

这个SQL的执行时间如下:

执行完需要0.742秒,深分页为什么会变慢呢?如果换成 limit 0,10,只需要0.006秒哦

我们先来看下这个SQL的执行流程:

  1. 通过普通二级索引树idx_update_time,过滤update_time条件,找到满足条件的记录ID。
  2. 通过ID,回到主键索引树,找到满足记录的行,然后取出展示的列(回表)
  3. 扫描满足条件的100010行,然后扔掉前100000行,返回。

SQL的执行流程

SQL的执行流程

执行计划如下:

SQL变慢原因有两个:

  1. limit语句会先扫描offset+n行,然后再丢弃掉前offset行,返回后n行数据。也就是说limit 100000,10,就会扫描100010行,而limit 0,10,只扫描10行。
  2. limit 100000,10 扫描更多的行数,也意味着回表更多的次数。

通过子查询优化

因为以上的SQL,回表了100010次,实际上,我们只需要10条数据,也就是我们只需要10次回表其实就够了。因此,我们可以通过减少回表次数来优化。

回顾B+ 树结构

那么,如何减少回表次数呢?我们先来复习下B+树索引结构哈~

InnoDB中,索引分主键索引(聚簇索引)和二级索引

  • 主键索引,叶子节点存放的是整行数据
  • 二级索引,叶子节点存放的是主键的值。

把条件转移到主键索引树

如果我们把查询条件,转移回到主键索引树,那就不就可以减少回表次数啦。转移到主键索引树查询的话,查询条件得改为主键id了,之前SQL的update_time这些条件咋办呢?抽到子查询那里嘛~

子查询那里怎么抽的呢?因为二级索引叶子节点是有主键ID的,所以我们直接根据update_time来查主键ID即可,同时我们把 limit 100000的条件,也转移到子查询,完整SQL如下:

1
sql复制代码select id,name,balance FROM account where id >= (select a.id from account a where a.update_time >= '2020-09-19' limit 100000, 1) LIMIT 10;(可以加下时间条件到外面的主查询)

查询效果一样的,执行时间只需要0.038秒!

我们来看下执行计划

由执行计划得知,子查询 table a查询是用到了idx_update_time索引。首先在索引上拿到了聚集索引的主键ID,省去了回表操作,然后第二查询直接根据第一个查询的 ID往后再去查10个就可以了!

因此,这个方案是可以的~

INNER JOIN 延迟关联

延迟关联的优化思路,跟子查询的优化思路其实是一样的:都是把条件转移到主键索引树,然后减少回表。不同点是,延迟关联使用了inner join代替子查询。

优化后的SQL如下:

1
sql复制代码SELECT  acct1.id,acct1.name,acct1.balance FROM account acct1 INNER JOIN (SELECT a.id FROM account a WHERE a.update_time >= '2020-09-19' ORDER BY a.update_time LIMIT 100000, 10) AS  acct2 on acct1.id= acct2.id;

查询效果也是杠杆的,只需要0.034秒

执行计划如下:

查询思路就是,先通过idx_update_time二级索引树查询到满足条件的主键ID,再与原表通过主键ID内连接,这样后面直接走了主键索引了,同时也减少了回表。

标签记录法

limit 深分页问题的本质原因就是:偏移量(offset)越大,mysql就会扫描越多的行,然后再抛弃掉。这样就导致查询性能的下降。

其实我们可以采用标签记录法,就是标记一下上次查询到哪一条了,下次再来查的时候,从该条开始往下扫描。就好像看书一样,上次看到哪里了,你就折叠一下或者夹个书签,下次来看的时候,直接就翻到啦。

假设上一次记录到100000,则SQL可以修改为:

1
bash复制代码select  id,name,balance FROM account where id > 100000 order by id limit 10;

这样的话,后面无论翻多少页,性能都会不错的,因为命中了id索引。但是你,这种方式有局限性:需要一种类似连续自增的字段。

使用between…and…

很多时候,可以将limit查询转换为已知位置的查询,这样MySQL通过范围扫描between...and,就能获得到对应的结果。

如果知道边界值为100000,100010后,就可以这样优化:

1
sql复制代码select  id,name,balance FROM account where id between 100000 and 100010 order by id desc;

手把手实战案例

我们一起来看一个实战案例哈。假设现在有表结构如下,并且有200万数据.

1
2
3
4
5
6
7
8
9
10
11
sql复制代码CREATE TABLE account (
 id varchar(32) COLLATE utf8_bin NOT NULL COMMENT '主键',
 account_no varchar(64) COLLATE utf8_bin NOT NULL DEFAULT '' COMMENT '账号'
 amount decimal(20,2) DEFAULT NULL COMMENT '金额'
 type varchar(10) COLLATE utf8_bin DEFAULT NULL COMMENT '类型A,B'
 create_time datetime DEFAULT NULL COMMENT '创建时间',
 update_time datetime DEFAULT NULL COMMENT '更新时间',
 PRIMARY KEY (id),
 KEY `idx_account_no` (account_no),
 KEY `idx_create_time` (create_time)
 ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin COMMENT='账户表'

业务需求是这样:获取最2021年的A类型账户数据,上报到大数据平台。

一般思路的实现方式

很多伙伴接到这么一个需求,会直接这么实现了:

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
ini复制代码//查询上报总数量
Integer total = accountDAO.countAccount();

//查询上报总数量对应的SQL
<select id ='countAccount' resultType="java.lang.Integer">
  seelct count(1) 
  from account
  where create_time >='2021-01-01 00:00:00'
  and  type ='A'
</select>

//计算页数
int pageNo = total % pageSize == 0 ? total / pageSize : (total / pageSize + 1);

//分页查询,上报
for(int i = 0; i < pageNo; i++){
 List<AcctountPO> list = accountDAO.listAccountByPage(startRow,pageSize);
 startRow = (pageNo-1)*pageSize;
 //上报大数据
 postBigData(list);
}
 
//分页查询SQL(可能存在limit深分页问题,因为account表数据量几百万)
<select id ='listAccountByPage' >
  seelct * 
  from account
  where create_time >='2021-01-01 00:00:00'
  and  type ='A'
  limit #{startRow},#{pageSize}
</select>

实战优化方案

以上的实现方案,会存在limit深分页问题,因为account表数据量几百万。那怎么优化呢?

其实可以使用标签记录法,有些伙伴可能会有疑惑,id主键不是连续的呀,真的可以使用标签记录?

当然可以,id不是连续,我们可以通过order by让它连续嘛。优化方案如下:

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
ini复制代码//查询最小ID
String  lastId = accountDAO.queryMinId();

//查询最小ID对应的SQL
<select id="queryMinId" returnType=“java.lang.String”>
select MIN(id) 
from account
where create_time >='2021-01-01 00:00:00'
and type ='A'
</select>

//一页的条数
Integer pageSize = 100;

List<AcctountPO> list ;
do{
   list = listAccountByPage(lastId,pageSize);
   //标签记录法,记录上次查询过的Id
   lastId = list.get(list,size()-1).getId();
    //上报大数据
    postBigData(list);
}while(CollectionUtils.isNotEmpty(list));

<select id ="listAccountByPage">
  select * 
  from account 
  where create_time >='2021-01-01 00:00:00'
  and id > #{lastId}
  and type ='A'
  order by id asc  
  limit #{pageSize}
</select>

本文转载自: 掘金

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

扒开外衣仔细分析:String为什么不可变

发表于 2021-09-25

小知识,大挑战!本文正在参与“程序员必备小知识”创作活动

说到Java源码,我相信小伙伴们或多或少都有接触,比如util包中常见的ArrayList、HashMap、LinkedHashMap,还有前面我们学过的多线程相关类,如:Executors、CountDownLatch、CyclicBarrier,又或者是lang包中的Object、Integer、String、Thread等。

这些都是我们比较常见的类,不过对于他们的实现原理,我们有时候并不能说出个所以然来,甚至有些人写了四五年代码,连最最常见的String如何实现都没有看过,还总是抱怨每天总是搬砖、前途迷茫。说实话,学习源码并不仅仅是为了应对面试,更重要的是我们通过阅读源码的过程,学习它的设计思想,这种思想在我们项目中完全可以实践出来。

总结来说,阅读源码有以下几点好处:

  1. 学习优秀的设计思想;
  2. 增加面试实力,横扫一切;
  3. 代码日常评审时大秀肌肉。

二、String源码分析

首先看一个小例子:

1
2
ini复制代码String name = "huage";
name="huasao";

相信面试中我们也会遇到类似【这个过程中有几个对象】的问题,其实当name重新赋值后,并不会在原有内存地址中修改,而是会创建一个新对象,如下图所示。

image.png

2.1 不可继承原因一:类定义

1
2
3
4
vbnet复制代码public final class String
   implements java.io.Serializable, Comparable<String>, CharSequence {
...
}

有了上面这个例子,我们就要从代码中来分析,String为什么不可变,在String.java类定义中可以看到:String类由final关键字修饰,这也意为着String类不可被继承,创建后不能被修改。

String类同时实现了三个接口:

  • Serializable:实现序列化,标记接口,用于标识序列化,未实现该接口无法被序列化。
  • Comparable:对两个实例化对象比较大小
  • CharSequence:String本质是个char数组,该接口为只读的字符序列。

2.2 不可继承原因二:成员变量

1
2
arduino复制代码/** The value is used for character storage. */
private final char value[];

String 中保存数据的是一个 char 的数组 value, value 同样也是被 final 修饰,这就意外着该数组无法被修改。

但是这时候有小伙伴就会反问道:被final修饰的变量只是引用不可变,堆内存中的值完全可以被修改啊。

1
2
3
arduino复制代码final char value[] = {'h','u','a','g','e'};
value[4] = 's';
System.out.println(value); //最终输出:huags

这个时候我们就要看一下,value 的访问权限是 private ,说明外部访问不到该变量,并且String 也没有提供 value 的相关操作方法,所以 value 一旦生成就无法再被被修改。

2.3 拓展:常见方法

接下来看String类中几个常见的方法是如何实现的

  • equals
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
arduino复制代码public boolean equals(Object anObject) {
   //内存地址是否相同
   if (this == anObject) {
       return true;
  }
   //目标对象是否为String类,不是的话直接返回false
   if (anObject instanceof String) {
       String anotherString = (String)anObject;
       int n = value.length;
       //判断两者长度是否相等
       if (n == anotherString.value.length) {
           char v1[] = value;
           char v2[] = anotherString.value;
           int i = 0;
           //比较两者中每一个字符是否相等
           while (n-- != 0) {
               if (v1[i] != v2[i])
                   return false;
               i++;
          }
           return true;
      }
  }
   return false;
}
  • 截取字符串:substring
1
2
3
4
5
6
7
8
9
10
11
12
13
14
arduino复制代码public String substring(int beginIndex, int endIndex) {
   if (beginIndex < 0) {
       throw new StringIndexOutOfBoundsException(beginIndex);
  }
   if (endIndex > value.length) {
       throw new StringIndexOutOfBoundsException(endIndex);
  }
   int subLen = endIndex - beginIndex;
   if (subLen < 0) {
       throw new StringIndexOutOfBoundsException(subLen);
  }
   return ((beginIndex == 0) && (endIndex == value.length)) ? this
          : new String(value, beginIndex, subLen);
}

通过上述源码可以得出:对于满足条件的起始/终止下标,该方法会调用new String()构成器,而该构造器最终通过System.arraycopy生成一个新的字符串返回,这也就意为着该方法并不会在原有字符串基础上进行修改。

同样的,我们可以查看replace、concat等方法源码,他们都会不会修改原有字符串,而是会生成一个新的字符串对象返回,这也是String不可变的表现。

三、String不可变的好处

  • 节省内存

如果定义了多个对象,并且每个对象的值是相同的,那么在实际中,堆内存只会存储一份数据,从而节省内存开销

image.png

\

本文转载自: 掘金

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

使用 FIO 对 Kubernetes 持久卷进行 Benc

发表于 2021-09-25

工具

Dbench

  • github.com/leeliu/dben…

用法

  1. 编辑 dbench.yaml 文件中的 storageClassName 以匹配你自己的 Storage Class。
1
sh复制代码kubectl get storageclasses
  1. 部署
1
sh复制代码kubectl apply -f dbench.yaml
  1. 部署后,Dbench Job 将:
* 使用 `storageClassName: ssd`(默认)提供 `1000Gi`(默认)的持久卷。
* 在新配置的磁盘上运行一系列 `fio` 测试。
* 目前有 `9` 个测试,每个测试 `15` 秒 - 总运行时间约为 `2.5` 分钟。
  1. 使用以下方法跟踪基准测试进度:
1
sh复制代码kubectl logs -f job/dbench

空输出表示 job 尚未创建,或 storageClassName 无效,请参阅下面的故障排除。
5. 在所有测试结束时,您将看到类似于以下内容的摘要:

1
2
3
4
5
6
7
sh复制代码==================
= Dbench Summary =
==================
Random Read/Write IOPS: 75.7k/59.7k. BW: 523MiB/s / 500MiB/s
Average Latency (usec) Read/Write: 183.07/76.91
Sequential Read/Write: 536MiB/s / 512MiB/s
Mixed Random Read/Write IOPS: 43.1k/14.4k

Dbench 摘要结果

* `Random Read/Write IOPS`(随机读写)
* `BW`(带宽)
* `Average Latency (usec) Read/Write`(读/写平均延迟)
* `Sequential Read/Write`(顺序读/写)
* `Mixed Random Read/Write IOPS`(混合随机读/写)
  1. 测试完成后,进行清理:
1
sh复制代码kubectl delete -f dbench.yaml

注意事项/故障排除

  • 如果持久化卷声明(Persistent Volume Claim)卡在 Pending 上,很可能您没有指定有效的存储类(Storage Class)。使用 kubectl get storageclasses 进行双重检查。还要检查用于配置的卷大小是否为 1000Gi(默认值)。
  • 绑定持久性卷可能需要一些时间,Kubernetes Dashboard UI 将 Dbench Job 显示为红色,直到卷完成配置。
  • 测试多种磁盘大小很有用,因为大多数云提供商按每 GB 配置的 IOPS 定价。 因此,4000Gi 卷的性能可能将优于 1000Gi 卷。重新测试,只需编辑 yaml,kubectl delete -f dbench.yaml 并在 deprovision/delete 完成后再次运行 kubectl apply -f dbench.yaml。
  • 所有 fio 测试的项都在 docker-entrypoint.sh 中。
    • Testing Read IOPS…
    • Testing Write IOPS…
    • Testing Read Bandwidth…
    • Testing Write Bandwidth…
    • Testing Read Latency…
    • Testing Write Latency…
    • Testing Read Sequential Speed…
    • Testing Write Sequential Speed…
    • Testing Read/Write Mixed…

腾讯云 K8S 集群生产实战

  1. kubectl get storageclass

1.png
2. vi dbench.yaml

2.png
3. kubectl apply -f dbench.yaml
4. kubectl logs -f job/dbench

3.png
5. kubectl delete -f dbench.yaml

本文转载自: 掘金

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

我写了一个脚本,可在“任意”服务器上执行命令!

发表于 2021-09-25

大家好,我是冰河~~

冰河之前维护着上千台服务器组成的服务器集群,如果每次需要在服务器上执行命令的时候,都要手动登录每台服务器进行操作的话,那也太麻烦了。你想想,如果在上千台服务器的集群中,每台服务器中只需要简单的执行一个相同的命令,那别说执行命令了,就是让你依次手动登录上千台服务器,那也够你受的了。估计依次登录上千台服务器,给你三天时间你可能都登不完,那怎么办呢?有没有什么好的方法来解决这个问题呢?

别急,我们今天就是来解决这个问题的。

说实话,我在维护上千台服务器集群的时候,并没有去依次手动登录每台服务器,为啥?没错,就是因为我懒啊!我懒的去登录,并且依次登录那么多台服务器,整个人都会崩溃的。

于是,我就想办法能不能写个脚本,让这个脚本接收我要执行的命令,然后将命令依次分发到集群上所有的服务器中执行,这不就解决问题了吗?说干就干。

不过,这里,有个需要注意的地方:那就是:需要提前配置好集群中每台服务器的主机名和IP地址的对应关系,能够互相使用主机名进行通信,并配置了SSH免密码登录。这一点不行担心,只要让运维在规划和分配服务器的时候,规划好就行了,无需后面再依次登录服务器处理。

为了方便小伙伴们理解,这里我们就假设集群中存在1024台服务器,每台服务器的主机名为binghe1~binghe1024。每台服务器可以通过主机名进行通信,接下来,我写了一个名称为distribute_command.sh的脚本,内容如下所示。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
bash复制代码#!/bin/bash
pcount=$#
if (( pcount<1 )) ; then
echo no args;
exit;
fi
#先在本机上执行命令
echo ------------binghe$host-----------------
$@
#循环在集群中的远程节点上执行命令
for (( host=1 ; host<=1024; host=host+1)) ; do
echo ------------binghe$host-----------------
ssh binghe$host $@
done;

这个脚本的含义为:接收传递进来的命令,将命令分发到主机名为binghe1~binghe1024的服务器上执行,也就是说,使用这个脚本我们能够做到:同时在集群的服务器上执行相同的命令。

接下来,为distribute_command.sh脚本赋予可执行权限,如下所示。

1
bash复制代码chmod a+x ./distribute_command.sh

使用格式如下:

1
bash复制代码./distribute_command.sh 在服务器上执行的完整命令

使用示例

  • 在集群中的每台服务器的/home目录下创建hello.txt文,内容为hello world
1
bash复制代码./distribute_command.sh echo "hello world" >> /home/hello.txt
  • 查看集群中每台服务器上hello.txt文件的内容
1
bash复制代码./distribute_command.sh cat /home/hello.txt
  • 删除集群中每台服务器上的hello.txt文件
1
bash复制代码./distribute_command.sh rm -rf /home/hello.txt

是不是很简单啊?所以说,有时候,不要盲目的去执行。很多时候,在做事情之前,要先思考下有没有更好的解决方案,有没有效率更加高效的解决方案。就比如这篇文章上说的,在上千台服务器上执行一条命令,如果依次手动登录每台服务器执行命令,估计花三天时间都搞不定;如果我们写了一个脚本的话,估计也就1分钟之内就搞定了。所以,效率和质量才是做事情需要追求的目标。

好了,今天就到这儿吧,我是冰河,我们下期见~~

\

本文转载自: 掘金

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

Spring 实操 目录总结

发表于 2021-09-25

首先分享之前的所有文章 , 欢迎点赞收藏转发三连下次一定 >>>> 😜😜😜

文章合集 : 🎁 juejin.cn/post/694164…

Github : 👉 github.com/black-ant

CASE 备份 : 👉 gitee.com/antblack/ca…

一 . 前言

Spring 中有很多节点可以用于复杂的定制 , 这个系列就是盘点这些核心定制点 , 以及能给我们带来什么好处

文章目的

  • 梳理主流程 ,并且提供流程图快速理解
  • 梳理实际操作的使用

👉👉👉👉总结文档会不断更新以及修正 , 欢迎收藏点赞

二 . IOC 整体流程

!!!! 这个被压缩有点厉害呀 @ 掘金!!!

Spring-全流程 (2).png

Spring IOC 原理

盘点 SpringIOC : Resource 及 Document 体系

盘点 SpringIOC : ApplicationContext 一览

盘点 SpringIOC : Bean 创建之 InitializingBean

盘点 SpringIOC : Bean 创建之属性注入

盘点 SpringIOC : Bean 创建主流程

三 . 要点解析

3.1 IOC 要点

循环依赖部分

盘点 SpringIOC : 循环依赖

image.png

1
java复制代码// 简述 : TODO 后期补充 , 详见上方地址

3.2 AOP 部分

AOP 的发起流程

盘点 AOP : AOP 的初始化

image.png

Aop 的创建过程

盘点 AOP : AOP 代理类的创建
image.png

Aop 拦截类进行拦截

盘点 AOP : AOP 的拦截对象的创建

image.png

AOP 拦截主流程

盘点 AOP : AOP 的拦截与方法调用

image.png

1
java复制代码// 简述 : TODO 后期补充 , 详见上方地址

四 . SpingMVC 部分

盘点 SpringMVC : MVC 主流程

4.1 注解扫描及请求拦截

image.png

1
java复制代码// 简述 : TODO 后期补充 , 详见上方地址

4.2 方法映射

image.png

1
java复制代码// 简述 : TODO 后期补充 , 详见上方地址

4.3 属性转换

image.png

1
java复制代码// 简述 : TODO 后期补充 , 详见上方地址

五. SpringBoot 部分

TODO : 流程图后期补充

盘点 SpringBoot : 自动装配

盘点 SpringBoot : Factories 处理流程

盘点 SpringBoot : Application 主流程

盘点 SpringBoot : Listener

盘点 SpringBoot : Application配置的读取流程

总结

这是一个长期更新的文档 ,后续将会补充其中的细节和修正理解的错误 , 以及优化排版 , 建议插眼!!!

附录-文档地址

二阶段 : 定制点系列 (撰写中)

这个系列主要设计以下几个部分 :

  • BeanDefinition 的定制处理
  • BeanFactory 的定制处理
  • FactoryBean 的定制处理
  • BeanPostProcessor 深度使用
  • BeanAware 能干什么
  • 自定义Spring 容器
  • Spring 启动时能做什么
  • InitializingBean 流程
  • DisposableBean 流程

本文转载自: 掘金

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

1…519520521…956

开发者博客

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