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

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


  • 首页

  • 归档

  • 搜索

怎么使用Swoft 搭建微服务(TCP RPC)PHP中高级

发表于 2019-11-05

Swoft 框架是首个基于Swoole 原生协程的新时代 PHP高性能协程全栈框架,内置协程网络服务器及常用的协程客户端,常驻内存,不依赖传统的 PHP-FPM。

如何搭建微服务?

首先确保已经可以正确搭建Swoft

不清楚的可以查看Swoft 官方文档

鉴于每个人的开发环境都不同

这里选用官方Docker 作为开发环境

Docker下载地址>>>

拉Docker 镜像

1
复制代码docker pull swoft/swoft

非常的简单


这样就是成功了

为了方便理解

我们把swoft 复制两份

命名为swoft-rpc和swoft-http

swoft-rpc只开启TCP 服务
swoft-http只开启Http 服务

修改配置文件

把根目录的.env.example复制一份为.env

.env 文件为swoft 配置文件, 最高优先级(覆盖config 下配置)

http 用到的配置

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
复制代码# Server
PFILE=/tmp/swoft.pid
PNAME=php-swoft
TCPABLE=false //是否同时启动TCP 服务器,这里用不到改为false
CRONABLE=false
AUTO_RELOAD=true
AUTO_REGISTER=false
...
# HTTP
HTTP_HOST=0.0.0.0 //监听的网卡
HTTP_PORT=80 //监听的端口
HTTP_MODE=SWOOLE_PROCESS //不用管
HTTP_TYPE=SWOOLE_SOCK_TCP //不用管
...//mysql 和redis 略过
# User service (demo service)
USER_POOL_NAME=user //别名
USER_POOL_URI=192.168.1.214:8099,192.168.1.214:8099 //负载均衡,URI填写为RPC 的地址,注意Docker和宿主之间的关系
USER_POOL_MIN_ACTIVE=5 //下面都不用管
USER_POOL_MAX_ACTIVE=10
USER_POOL_MAX_WAIT=20
USER_POOL_TIMEOUT=200
USER_POOL_MAX_WAIT_TIME=3
USER_POOL_MAX_IDLE_TIME=60
USER_POOL_USE_PROVIDER=false
USER_POOL_BALANCER=random
USER_POOL_PROVIDER=consul

RPC 用到的配置

1
2
3
4
5
6
7
复制代码# TCP
TCP_HOST=0.0.0.0 //监听的网卡
TCP_PORT=8099 //监听的端口
TCP_MODE=SWOOLE_PROCESS //不用管
TCP_TYPE=SWOOLE_SOCK_TCP //不用管
TCP_PACKAGE_MAX_LENGTH=2048 //最大链接数
TCP_OPEN_EOF_CHECK=false //不用管

启动Docker 容器

1
复制代码docker run -it --rm -p 8099:8099 -v E:\WWW\swoft-rpc:/var/www/swoft  swoft/swoft /bin/bash

这里用-it和-v方便调试


这样就是成功启动了

启动RPC 服务

1
复制代码php bin/swoft rpc:start

RPC 服务只需要单独启动TCP 服务器

有的同学RPC 和TCP 的关系可能还没弄清楚

这里RPC 服务和TCP 服务器可以类比为Web 服务和HTTP 服务器

监听HTTP 来实现Web 服务
监听TCP 来实现RPC 服务

就这样理解吧


这样就是成功启动了

启动Web服务

也就是启动HTTP 服务器XD

新开一个终端来创建新容器

1
复制代码docker run -it --rm -p 9501:80 -v E:\WWW\swoft-http:/var/www/swoft swoft/swoft /bin/bash

这里端口改成9501, 因为本地开发环境已经用了80了:b

1
复制代码php bin/swoft server:start


因为在之前把自动开启TCP 服务器禁用了

所以显示Disabled

这样也就是成功了!

访问一下http://127.0.0.1:9501/看下有没有问题

没问题的话, 可以看下官方提供的RPC demo http://127.0.0.1:9501/rpc/call

phper在进阶的时候总会遇到一些问题和瓶颈,业务代码写多了没有方向感,不知道该从那里入手去提升,对此我整理了一些资料,包括但不限于:分布式架构、高可扩展、高性能、高并发、服务器性能调优、TP6,laravel,YII2,Redis,Swoole、Swoft、Kafka、Mysql优化、shell脚本、Docker、微服务、Nginx等多个知识点高级进阶干货需要的可以免费分享给大家,需要请戳这里

)))最后,祝所有大家在面试中过关斩将,拿到心仪offer。如果想与一群3-8年资深开发者一起交流学习的话,需要点击这里

本文转载自: 掘金

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

赶快看看Java11,不然你就out了!

发表于 2019-11-05

前言

赶快看看Java11,不然你就out了!
这个是个人博客原文的链接。Java更新的太快了,都学不过来了,最近了解一些Java8以后的一些特性,写下来希望对大家有帮助。

为什么选择Java11

  • 容器环境支持,GC等领域的增强。
  • 进行了瘦身,更轻量级,安装包体积小。
  • JDK11 是一个长期支持版。

特性介绍

由于直接从Java8跨越到Java11,所以特性介绍就把Java9-Java11的部分特性一起介绍一下。想要了解Java8特性的朋友可以去我的博客找「Java8系列」。

Jshell @since 9

Jshell在Java9中就被提出来了,可以直接在终端写Java程序,回车就可以执行。Jshell默认会导入下面的一些包,所以在Jshell环境中这些包的内容都是可以使用的。

1
2
3
4
5
6
7
8
9
10
11
复制代码import java.lang.*;
import java.io.*;
import java.math.*;
import java.net.*;
import java.nio.file.*;
import java.util.*;
import java.util.concurrent.*;
import java.util.function.*;
import java.util.prefs.*;
import java.util.regex.*;
import java.util.stream.*;
1.什么是Jshell?

Jshell是在 Java 9 中引入的。它提供了一个交互式 shell,用于快速原型、调试、学习 Java 及 Java API,所有这些都不需要 public static void main 方法,也不需要在执行之前编译代码。

2.Jshell的使用

打开终端,键入jshell进入jshell环境,然后输入/help intro可以查看Jshell的介绍。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
复制代码 lixiaoshuang@localhost  ~  jshell
| 欢迎使用 JShell -- 版本 11.0.2
| 要大致了解该版本, 请键入: /help intro

jshell> /help intro
|
| intro
| =====
|
| 使用 jshell 工具可以执行 Java 代码,从而立即获取结果。
| 您可以输入 Java 定义(变量、方法、类等等),例如:int x = 8
| 或 Java 表达式,例如:x + x
| 或 Java 语句或导入。
| 这些小块的 Java 代码称为“片段”。
|
| 这些 jshell 工具命令还可以让您了解和
| 控制您正在执行的操作,例如:/list
|
| 有关命令的列表,请执行:/help

jshell>

Jshell确实是一个好用的小工具,这里不做过多介绍,我就举一个例子,剩下的大家自己体会。比如我们现在就想随机生成一个UUID,以前需要这么做:

  • 创建一个类。
  • 创建一个main方法。
  • 然后写一个生成UUID的逻辑,执行。

现在只需要,进入打开终端键入jshell,然后直接输入var uuid = UUID.randomUUID()回车。就可以看到uuid的回显,这样我们就得到了一个uuid。并不需要public static void main(String[] args);

1
2
3
4
5
6
7
复制代码 lixiaoshuang@localhost  ~  jshell
| 欢迎使用 JShell -- 版本 11.0.2
| 要大致了解该版本, 请键入: /help intro

jshell> var uuid = UUID.randomUUID();
uuid ==> 9dac239e-c572-494f-b06d-84576212e012
jshell>
3.怎么退出Jshell?

在Jshell环境中键入/exit就可以退出。

1
2
3
4
5
6
7
8
9
10
11
复制代码 lixiaoshuang@localhost  ~ 
lixiaoshuang@localhost  ~  jshell
| 欢迎使用 JShell -- 版本 11.0.2
| 要大致了解该版本, 请键入: /help intro

jshell> var uuid = UUID.randomUUID();
uuid ==> 9dac239e-c572-494f-b06d-84576212e012

jshell> /exit
| 再见
lixiaoshuang@localhost  ~ 

模块化(Module)@since 9

1.什么是模块化?

模块化就是增加了更高级别的聚合,是Package的封装体。Package是一些类路径名字的约定,而模块是一个或多个Package组成的封装体。

java9以前 :package => class/interface。

java9以后 :module => package => class/interface。

那么JDK被拆为了哪些模块呢?打开终端执行java --list-modules查看。

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
复制代码lixiaoshuang@localhost  ~ java --list-modules
java.base@11.0.2
java.compiler@11.0.2
java.datatransfer@11.0.2
java.desktop@11.0.2
java.instrument@11.0.2
java.logging@11.0.2
java.management@11.0.2
java.management.rmi@11.0.2
java.naming@11.0.2
java.net.http@11.0.2
java.prefs@11.0.2
java.rmi@11.0.2
java.scripting@11.0.2
java.se@11.0.2
java.security.jgss@11.0.2
java.security.sasl@11.0.2
java.smartcardio@11.0.2
java.sql@11.0.2
java.sql.rowset@11.0.2
java.transaction.xa@11.0.2
java.xml@11.0.2
java.xml.crypto@11.0.2
jdk.accessibility@11.0.2
jdk.aot@11.0.2
jdk.attach@11.0.2
jdk.charsets@11.0.2
jdk.compiler@11.0.2
jdk.crypto.cryptoki@11.0.2
jdk.crypto.ec@11.0.2
jdk.dynalink@11.0.2
jdk.editpad@11.0.2
jdk.hotspot.agent@11.0.2
jdk.httpserver@11.0.2
jdk.internal.ed@11.0.2
jdk.internal.jvmstat@11.0.2
jdk.internal.le@11.0.2
jdk.internal.opt@11.0.2
jdk.internal.vm.ci@11.0.2
jdk.internal.vm.compiler@11.0.2
jdk.internal.vm.compiler.management@11.0.2
jdk.jartool@11.0.2
jdk.javadoc@11.0.2
jdk.jcmd@11.0.2
jdk.jconsole@11.0.2
jdk.jdeps@11.0.2
jdk.jdi@11.0.2
jdk.jdwp.agent@11.0.2
jdk.jfr@11.0.2
jdk.jlink@11.0.2
jdk.jshell@11.0.2
jdk.jsobject@11.0.2
jdk.jstatd@11.0.2
jdk.localedata@11.0.2
jdk.management@11.0.2
jdk.management.agent@11.0.2
jdk.management.jfr@11.0.2
jdk.naming.dns@11.0.2
jdk.naming.rmi@11.0.2
jdk.net@11.0.2
jdk.pack@11.0.2
jdk.rmic@11.0.2
jdk.scripting.nashorn@11.0.2
jdk.scripting.nashorn.shell@11.0.2
jdk.sctp@11.0.2
jdk.security.auth@11.0.2
jdk.security.jgss@11.0.2
jdk.unsupported@11.0.2
jdk.unsupported.desktop@11.0.2
jdk.xml.dom@11.0.2
jdk.zipfs@11.0.2
2.为什么这么做?

大家都知道JRE中有一个超级大的rt.jar(60多M),tools.jar也有几十兆,以前运行一个hello world也需要上百兆的环境。

  • 让Java SE程序更加容易轻量级部署。
  • 强大的封装能力。
  • 改进组件间的依赖管理,引入比jar粒度更大的Module。
  • 改进性能和安全性。
3.怎么定义模块?

模块的是通过module-info.java进行定义,编译后打包后,就成为一个模块的实体。下面来看下最简单的模块定义。

4.模块的关键字
  • open

用来指定开放模块,开放模块的所有包都是公开的,public的可以直接引用使用,其他类型可以通过反射得到。

1
2
3
4
5
复制代码open module module.one {
//导入日志包
requires java.logging;

}
  • opens

opens 用来指定开放的包,其中public类型是可以直接访问的,其他类型可以通过反射得到。

1
2
3
4
复制代码module module.one {

opens <package>;
}
  • exports

exports用于指定模块下的哪些包可以被其他模块访问。

1
2
3
4
5
6
复制代码module module.one {

exports <package>;

exports <package> to <module1>, <module2>...;
}
  • requires

该关键字声明当前模块与另一个模块的依赖关系。

1
2
3
4
5
复制代码module module.one {

requires <package>;

}
  • uses、provides…with…

uses语句使用服务接口的名字,当前模块就会发现它,使用java.util.ServiceLoader类进行加载,必须是本模块中的,不能是其他模块中的.其实现类可以由其他模块提供。

1
2
3
4
5
6
7
8
复制代码module module.one {

//对外提供的接口服务 ,下面指定的接口以及提供服务的impl,如果有多个实现类,用用逗号隔开
uses <接口名>;

provides <接口名> with <接口实现类>,<接口实现类>;

}

var关键字 @since 10

1.var是什么?

var是Java10中新增的局部类型变量推断。它会根据后面的值来推断变量的类型,所以var必须要初始化。

例:

1
2
复制代码var a;       ❌
var a = 1; ✅
2.var使用示例
  • var定义局部变量
1
2
3
复制代码var a = 1; 
等于
int a = 1;
  • var接收方法返回时
1
2
3
复制代码var result = this.getResult();
等于
String result = this.getResult();
  • var循环中定义局部变量
1
2
3
4
5
6
7
复制代码for (var i = 0; i < 5; i++) {
System.out.println(i);
}
等于
for (int i = 0; i < 5; i++) {
System.out.println(i);
}
  • var结合泛型
1
2
3
4
5
复制代码var list1 = new ArrayList<String>();  //在<>中指定了list类型为String
等于
List<String> list1 = new ArrayList<>();

var list2 = new ArrayList<>(); //<>里默认会是Object
  • var在Lambda中使用(java11才可以使用)
1
2
3
复制代码Consumer<String> Consumer = (var i) -> System.out.println(i);
等于
Consumer<String> Consumer = (String i) -> System.out.println(i);
3.var不能再哪里使用?
  • 类成员变量类型。
  • 方法返回值类型。
  • Java10中Lambda不能使用var,Java11中可以使用。

增强api

1.字符串增强 @since 11
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
复制代码// 判断字符串是否为空白
" ".isBlank(); // true

// 去除首尾空格
" Hello Java11 ".strip(); // "Hello Java11"

// 去除尾部空格
" Hello Java11 ".stripTrailing(); // " Hello Java11"

// 去除首部空格
" Hello Java11 ".stripLeading(); // "Hello Java11 "

// 复制字符串
"Java11".repeat(3); // "Java11Java11Java11"

// 行数统计
"A\nB\nC".lines().count(); // 3
2.集合增强

从Java 9 开始,jdk里面就为集合(List、Set、Map)增加了of和copyOf方法。它们用来创建不可变集合。

  • of() @since 9
  • copyOf() @since 10

示例一:

1
2
3
4
5
6
7
复制代码        var list = List.of("Java", "Python", "C"); //不可变集合
var copy = List.copyOf(list); //copyOf判断是否是不可变集合类型,如果是直接返回
System.out.println(list == copy); // true

var list = new ArrayList<String>(); // 这里返回正常的集合
var copy = List.copyOf(list); // 这里返回一个不可变集合
System.out.println(list == copy); // false

示例二:

1
2
3
4
5
6
7
复制代码        var set = Set.of("Java", "Python", "C");
var copy = Set.copyOf(set);
System.out.println(set == copy); // true

var set1 = new HashSet<String>();
var copy1 = List.copyOf(set1);
System.out.println(set1 == copy1); // false

示例三:

1
2
3
4
5
6
7
复制代码        var map = Map.of("Java", 1, "Python", 2, "C", 3);
var copy = Map.copyOf(map);
System.out.println(map == copy); // true

var map1 = new HashMap<String, Integer>();
var copy1 = Map.copyOf(map1);
System.out.println(map1 == copy1); // false

注意:使用 of 和 copyOf 创建的集合为不可变集合,不能进行添加、删除、替换、排序等操作,不然会报java.lang.UnsupportedOperationException异常,使用Set.of()不能出现重复元素、Map.of()不能出现重复key,否则回报java.lang.IllegalArgumentException。。

3.Stream增强 @since 9

Stream是Java 8 中的特性,在Java 9 中为其新增了4个方法:

  • ofNullable(T t)

此方法可以接收null来创建一个空流

1
2
3
4
复制代码以前
Stream.of(null); //报错
现在
Stream.ofNullable(null);
  • takeWhile(Predicate<? super T> predicate)

此方法根据Predicate接口来判断如果为true就 取出 来生成一个新的流,只要碰到false就终止,不管后边的元素是否符合条件。

1
2
3
复制代码        Stream<Integer> integerStream = Stream.of(6, 10, 11, 15, 20);
Stream<Integer> takeWhile = integerStream.takeWhile(t -> t % 2 == 0);
takeWhile.forEach(System.out::println); // 6,10
  • dropWhile(Predicate<? super T> predicate)

此方法根据Predicate接口来判断如果为true就 丢弃 来生成一个新的流,只要碰到false就终止,不管后边的元素是否符合条件。

1
2
3
复制代码        Stream<Integer> integerStream = Stream.of(6, 10, 11, 15, 20);
Stream<Integer> takeWhile = integerStream.dropWhile(t -> t % 2 == 0);
takeWhile.forEach(System.out::println); //11,15,20
  • iterate重载

以前使用iterate方法生成无限流需要配合limit进行截断

1
2
复制代码        Stream<Integer> limit = Stream.iterate(1, i -> i + 1).limit(5);
limit.forEach(System.out::println); //1,2,3,4,5

现在重载后这个方法增加了个判断参数

1
2
复制代码        Stream<Integer> iterate = Stream.iterate(1, i -> i <= 5, i -> i + 1);
iterate.forEach(System.out::println); //1,2,3,4,5
4.Optional增强 @since 9
  • stream()

如果为空返回一个空流,如果不为空将Optional的值转成一个流。

1
2
3
4
5
6
7
复制代码        //返回Optional值的流
Stream<String> stream = Optional.of("Java 11").stream();
stream.forEach(System.out::println); // Java 11

//返回空流
Stream<Object> stream = Optional.ofNullable(null).stream();
stream.forEach(System.out::println); //
  • ifPresentOrElse(Consumer<? super T> action, Runnable emptyAction)

个人感觉这个方法就是结合isPresent()对Else的增强,ifPresentOrElse 方法的用途是,如果一个 Optional 包含值,则对其包含的值调用函数 action,即 action.accept(value),这与 ifPresent 一致;与 ifPresent 方法的区别在于,ifPresentOrElse 还有第二个参数 emptyAction —— 如果 Optional 不包含值,那么 ifPresentOrElse 便会调用 emptyAction,即 emptyAction.run()。

1
2
3
4
5
6
7
复制代码        Optional<Integer> optional = Optional.of(1);
optional.ifPresentOrElse( x -> System.out.println("Value: " + x),() ->
System.out.println("Not Present.")); //Value: 1

optional = Optional.empty();
optional.ifPresentOrElse( x -> System.out.println("Value: " + x),() ->
System.out.println("Not Present.")); //Not Present.
  • or(Supplier<? extends Optional<? extends T>> supplier)
1
2
3
4
5
6
7
8
复制代码        Optional<String> optional1 = Optional.of("Java");
Supplier<Optional<String>> supplierString = () -> Optional.of("Not Present");
optional1 = optional1.or( supplierString);
optional1.ifPresent( x -> System.out.println("Value: " + x)); //Value: Java

optional1 = Optional.empty();
optional1 = optional1.or( supplierString);
optional1.ifPresent( x -> System.out.println("Value: " + x)); //Value: Not Present
5.InputStream增强 @since 9
1
2
3
4
5
6
复制代码        String lxs = "java";
try (var inputStream = new ByteArrayInputStream(lxs.getBytes());
var outputStream = new ByteArrayOutputStream()) {
inputStream.transferTo(outputStream);
System.out.println(outputStream); //java
}

HTTP Client API

改api支持同步和异步两种方式,下面是两种方式的示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
复制代码        var request = HttpRequest.newBuilder()
.uri(URI.create("https://www.baidu.com/"))
.build();
var client = HttpClient.newHttpClient();
// 同步
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
System.out.println(response.body());

// 异步
CompletableFuture<HttpResponse<String>> sendAsync = client.sendAsync(request, HttpResponse.BodyHandlers.ofString());
//这里会阻塞
HttpResponse<String> response1 = sendAsync.get();
System.out.println(response1.body());

直接运行java文件

我们都知道以前要运行一个.java文件,首先要javac编译成.class文件,然后在java执行:

1
2
3
4
复制代码//编译
javac Java11.java
//运行
java Java11

在java11中,只需要通过java一个命令就可以搞定

1
复制代码java Java11.java

移除内容

  • com.sun.awt.AWTUtilities。
  • sun.misc.Unsafe.defineClass 使用java.lang.invoke.MethodHandles.Lookup.defineClass来替代。
  • Thread.destroy() 以及 Thread.stop(Throwable) 方法。
  • sun.nio.ch.disableSystemWideOverlappingFileLockCheck 属性。
  • sun.locale.formatasdefault 属性。
  • jdk snmp 模块。
  • javafx,openjdk 是从java10版本就移除了,oracle java10还尚未移除javafx ,而java11版本将javafx也移除了。
  • Java Mission Control,从JDK中移除之后,需要自己单独下载。
  • Root Certificates :Baltimore Cybertrust Code Signing CA,SECOM ,AOL and Swisscom。
  • 在java11中将java9标记废弃的Java EE及CORBA模块移除掉。

完全支持Linux容器(包括docker)

许多运行在Java虚拟机中的应用程序(包括Apache Spark和Kafka等数据服务以及传统的企业应用程序)都可以在Docker容器中运行。但是在Docker容器中运行Java应用程序一直存在一个问题,那就是在容器中运行JVM程序在设置内存大小和CPU使用率后,会导致应用程序的性能下降。这是因为Java应用程序没有意识到它正在容器中运行。随着Java 10的发布,这个问题总算得以解决,JVM现在可以识别由容器控制组(cgroups)设置的约束。可以在容器中使用内存和CPU约束来直接管理Java应用程序,其中包括:

  • 遵守容器中设置的内存限制
  • 在容器中设置可用的CPU
  • 在容器中设置CPU约束

Java 10的这个改进在Docker for Mac、Docker for Windows以及Docker Enterprise Edition等环境均有效。

总结

Java版本特性.png

参考

segmentfault.com/a/119000001…

感谢大家的观看,希望多多关注哦。 如有错误,烦请指正。

本文转载自: 掘金

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

java类加载之初始化过程(附面试题)

发表于 2019-11-05

类或接口的初始化过程就是执行它们的初始化方法<clinit>。这个方法是由编译器在编译的时候生成到class文件中的,包含类静态field赋值指令和静态语句块(static{})中的代码指令两部分,顺序和源码中的顺序相同。

以下情况下,会触发类(用C表示)的初始化:

  • new(创建对象), getstatic(获取类field), putstatic(给类field赋值), 或 invokestatic(调用类方法) 指令执行,创建C的实例,获取/设置C的静态字段,调用C的静态方法。

如果获取的类field是带有ConstantValue属性的常量,不会触发初始化

  • 第一次调用 java.lang.invoke.MethodHandle 实例返回了REF_getStatic,REF_putStatic, REF_invokeStatic, REF_newInvokeSpecial类型的方法句柄。
  • 反射调用,如Class或java.lang.reflect`包中的类
  • 如果C是一个类,它的子类<clinit>方法调用前,先调用C的<clinit>方法
  • 如果C是一个接口,并且定义了一个非abstract, 非static 的方法, 它的实现类(直接或间接)执行初始化方法<clinit>时会先初始化C.
  • C作为主类(包含main方法)时

可以看出在static{}中执行一些耗时的操作会导致类初始化阻塞甚至失败

在类初始化之前,会先进行链接操作

为了加快初始化效率,jvm是多线程执行初始化操作的,可能会有多个线程同一时刻尝试初始化类,也可能一个类初始化过程中又触发递归初始化该类,所以jvm需要保证只有一个线程去进行初始化动作,jvm通过为已验证过的类保持一个状态和一个互斥锁来保证初始化过程是线程安全的。

虚拟机中类的状态:

  • 类已验证和准备,但未初始化
  • 类正在被一个线程初始化
  • 类已经完成初始化,可以使用了
  • 类初始化失败

实际上,虚拟机为类定义的状态可能不止上面4种,如hotspot,见前文

除了状态,在初始化一个类之前,先要获得与这个类相关联的锁对象(监视器),记作LC。

类或接口C的初始化流程如下(jvm1.8规范):

  1. 等待获取C的锁LC.
  2. 如果C正在被其他线程初始化, 释放LC,并阻塞当前线程直到C初始化完成.

线程中断对初始化过程没有影响
3. 如果C正在被当前线程初始化, 则肯定是在递归初始化时又触发C初始化. 释放LC并正常返回.
4. 如果C的状态为已经初始化,释放LC并正常返回.
5. 如果C的状态为初始化失败,释放LC并抛出一个 NoClassDefFoundError异常.
6. 否则记录当前类C的状态为初始化中,并设置当前线程为初始化线程, 然后释放LC.

然后, 按照字节码文件中的顺序初始化C中每个带有ConstantValue属性的 final static 字段.

**注意:**jvm规范把常量的赋值定义在初始化阶段,<clinit>执行之前,具体实现未必严格遵守。如hotspot虚拟机在解析字节码过程创建_java_mirror镜像类时已为每个常量字段赋值。
7. 下一步, 如果C是一个类, 而且它的父类还未初始化, SC记作它的父类, SI1, ..., SIn 记作C实现的至少包含一个非抽象,非静态方法的接口(直接或间接的) 。 先初始化SC,所有父接口的顺序按照递归的顺序而不是继承层次的顺序确定, 对于一个被C直接实现的接口I (按照C的接口列表 interfaces 的顺序), 在I初始化之前,先循环遍历初始化I的父接口 (按照I的接口列表 interfaces 的顺序) .
8. 下一步, 查看定义类加载器是否开启了断言(用于调试).

1
2
3
4
5
复制代码// ClassLoader

// 查询类是否开启了断言
// 通过#setClassAssertionStatus(String, boolean)/#setPackageAssertionStatus(String, boolean)/#setDefaultAssertionStatus(boolean)设置断言
boolean desiredAssertionStatus(String className);
  1. 下一步,执行C的初始化方法<clinit>.
  2. 如果C的初始化正常完成, 获取LC并将C的状态标记为已完成初始化, 唤醒所有等待线程,释放锁LC,初始化过程完成.
  3. 否则, 初始化方法必须抛出一个异常E. 如果E不是 Error 或其子类, 创建一个 ExceptionInInitializerError 实例(以E作为参数), 在接下来的步骤中,以这个实例替换E,如果因为内存溢出无法创建 ExceptionInInitializerError 实例,用一个 OutOfMemoryError 替换E.
  4. 获取 LC, 标记C的初始化状态为发生错误, 通知所有等待线程, 释放 LC, 并通过E或其他替代(见前一步)异常返回.

虚拟机的实现可能优化这个过程,在它可以判断初始化已经完成时, 取消在第1步获取锁 (和在第 4/5释放锁) , 前提是, 根据java内存模型, 所有的 happens-before 关系在加锁和优化锁时都存在.

接下来看一个例子:

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
复制代码interface IA {
Object o = new Object();
}

abstract class Base {

static {
System.out.println("Base <clinit> invoked");
}

public Base() {
System.out.println("Base <init> invoked");
}

{
System.out.println("Base normal block invoked");
}
}

class Sub extends Base implements IA {
static {
System.out.println("Sub <clinit> invoked");
}

{
System.out.println("Sub normal block invoked");
}

public Sub() {
System.out.println("Sub <init> invoked");
}
}

public class TestInitialization {

public static void main(String[] args) {
new Sub();
}
}

在hotspot虚拟机上运行:

1
复制代码javac TestInitialization.java && java TestInitialization

可以看出初始化顺序为:父类静态构造器 -> 子类静态构造块 -> 父类普通构造块 -> 父类构造器 -> 子类普通构造快 -> 子类构造器,且普通构造快在实例构造器之前调用,与顺序无关。

关于接口由于没法添加static{},可以通过反编译看下也生成了<clinit>方法:

如果没有为类定义实例构造器,编译器会生成一个不带参数的默认构造器,里边调用父类的默认构造器

如果类中没有静态变量的赋值语句或静态代码块,则不必生成<clinit>

最后,介绍几个相关面试题:

  1. 下面代码输出什么?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
复制代码public class InitializationQuestion1 {

private static InitializationQuestion1 q = new InitializationQuestion1();
private static int a;
private static int b = 0;

public InitializationQuestion1() {
a++;
b++;
}

public static void main(String[] args) {
System.out.println(InitializationQuestion1.a);
System.out.println(InitializationQuestion1.b);
}
}

把q声明放到b后面呢?输出什么?
2. 下面代码输出什么?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
复制代码abstract class Parent {
static int a = 10;

static {
System.out.println("Parent init");
}
}

class Child extends Parent {
static {
System.out.println("Child init");
}
}

public class InitializationQuestion2 {
public static void main(String[] args) {
System.out.println(Child.a);
}
}

改成下面试试:

1
2
3
4
5
6
7
复制代码abstract class Parent {
static final int a = 10;

static {
System.out.println("Parent init");
}
}

再改成下面这样试试:

1
2
3
4
5
6
7
8
9
10
11
复制代码abstract class Parent {
static final int a = value();

static {
System.out.println("Parent init");
}

static int value(){
return 10;
}
}

本文转载自: 掘金

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

《我们一起进大厂》系列-缓存雪崩、击穿、穿透

发表于 2019-11-04

你知道的越多,你不知道的越多

点赞再看,养成习惯

GitHub上已经开源github.com/JavaFamily,有一线大厂面试点脑图,欢迎Star和完善

一点感慨

本来都把稿子放到公众号保存了,洗澡的时候想了一下晚上的比赛,觉得还是打开电脑写点东西,跟文章内容没关系,只是一点个人的感慨,不知道多少小伙伴看了昨天SKT VS G2的比赛,又不知道多少小伙伴还记得Faker手抖的那一幕。

不知道你们看了是什么感受,我看到他手抖的时候我内心也抖了,世界赛我支持的都是LPL的队伍,但是我喜欢李哥这个人,那种对胜利的执著,这么多年了那种坚持自己的坚持,这么多利益诱惑在面前却只想要胜利,这样的人我好喜欢啊,我想很多人也喜欢。

可能就像很多网友说的那样,英雄迟暮,但是我觉得他还是有点东西,就像很多人说我们程序员只能吃年轻饭一样,但是如果你坚持自己的坚持,做个腹有诗书气自华的仔,我想最后肯定会得到自己的得到。

好了我也不煽情了,我们开始讲技术吧。

正文

上一期吊打系列我们提到了Redis的基础知识,还没看的小伙伴可以回顾一下

《吊打面试官》系列-Redis基础

那提到Redis我相信各位在面试,或者实际开发过程中对缓存雪崩,穿透,击穿也不陌生吧,就算没遇到过但是你肯定听过,那三者到底有什么区别,我们又应该怎么去防止这样的情况发生呢,我们有请下一位受害者。

面试开始

一个大腹便便,穿着格子衬衣的中年男子,拿着一个满是划痕的mac向你走来,看着快秃顶的头发,心想着肯定是尼玛顶级架构师吧!但是我们腹有诗书气自华,虚都不虚。

小伙子我看你的简历上写到了Redis,那么我们直接开门见山,直接怼常见的几个大问题,Redis雪崩了解么?

帅气迷人的面试官您好,我了解的,目前电商首页以及热点数据都会去做缓存 ,一般缓存都是定时任务去刷新,或者是查不到之后去更新的,定时任务刷新就有一个问题。

举个简单的例子:如果所有首页的Key失效时间都是12小时,中午12点刷新的,我零点有个秒杀活动大量用户涌入,假设当时每秒 6000 个请求,本来缓存在可以扛住每秒 5000 个请求,但是缓存当时所有的Key都失效了。此时 1 秒 6000 个请求全部落数据库,数据库必然扛不住,它会报一下警,真实情况可能DBA都没反应过来就直接挂了。此时,如果没用什么特别的方案来处理这个故障,DBA 很着急,重启数据库,但是数据库立马又被新的流量给打死了。这就是我理解的缓存雪崩。

我刻意看了下我做过的项目感觉再吊的都不允许这么大的QPS直接打DB去,不过没慢SQL加上分库,大表分表可能还还算能顶,但是跟用了Redis的差距还是很大

同一时间大面积失效,那一瞬间Redis跟没有一样,那这个数量级别的请求直接打到数据库几乎是灾难性的,你想想如果打挂的是一个用户服务的库,那其他依赖他的库所有的接口几乎都会报错,如果没做熔断等策略基本上就是瞬间挂一片的节奏,你怎么重启用户都会把你打挂,等你能重启的时候,用户早就睡觉去了,并且对你的产品失去了信心,什么垃圾产品。

面试官摸了摸自己的头发,嗯还不错,那这种情况咋整?你都是怎么去应对的?

处理缓存雪崩简单,在批量往Redis存数据的时候,把每个Key的失效时间都加个随机值就好了,这样可以保证数据不会在同一时间大面积失效,我相信,Redis这点流量还是顶得住的。

1
java复制代码setRedis(Key,value,time + Math.random() * 10000);

如果Redis是集群部署,将热点数据均匀分布在不同的Redis库中也能避免全部失效的问题,不过本渣我在生产环境中操作集群的时候,单个服务都是对应的单个Redis分片,是为了方便数据的管理,但是也同样有了可能会失效这样的弊端,失效时间随机是个好策略。

或者设置热点数据永远不过期,有更新操作就更新缓存就好了(比如运维更新了首页商品,那你刷下缓存就完事了,不要设置过期时间),电商首页的数据也可以用这个操作,保险。

那你了解缓存穿透和击穿么,可以说说他们跟雪崩的区别么?

嗯,了解,我先说一下缓存穿透吧,缓存穿透是指缓存和数据库中都没有的数据,而用户不断发起请求,我们数据库的 id 都是1开始自增上去的,如发起为id值为 -1 的数据或 id 为特别大不存在的数据。这时的用户很可能是攻击者,攻击会导致数据库压力过大,严重会击垮数据库。

小点的单机系统,基本上用postman就能搞死,比如我自己买的阿里服务

像这种你如果不对参数做校验,数据库id都是大于0的,我一直用小于0的参数去请求你,每次都能绕开Redis直接打到数据库,数据库也查不到,每次都这样,并发高点就容易崩掉了。

至于缓存击穿嘛,这个跟缓存雪崩有点像,但是又有一点不一样,缓存雪崩是因为大面积的缓存失效,打崩了DB,而缓存击穿不同的是缓存击穿是指一个Key非常热点,在不停的扛着大并发,大并发集中对这一个点进行访问,当这个Key在失效的瞬间,持续的大并发就穿破缓存,直接请求数据库,就像在一个完好无损的桶上凿开了一个洞。

面试官露出欣慰的眼光,那他们分别怎么解决

缓存穿透我会在接口层增加校验,比如用户鉴权校验,参数做校验,不合法的参数直接代码Return,比如:id 做基础校验,id <=0的直接拦截等。

这里我想提的一点就是,我们在开发程序的时候都要有一颗“不信任”的心,就是不要相信任何调用方,比如你提供了API接口出去,你有这几个参数,那我觉得作为被调用方,任何可能的参数情况都应该被考虑到,做校验,因为你不相信调用你的人,你不知道他会传什么参数给你。

举个简单的例子,你这个接口是分页查询的,但是你没对分页参数的大小做限制,调用的人万一一口气查 Integer.MAX_VALUE 一次请求就要你几秒,多几个并发你不就挂了么?是公司同事调用还好大不了发现了改掉,但是如果是黑客或者竞争对手呢?在你双十一当天就调你这个接口会发生什么,就不用我说了吧。这是之前的Leader跟我说的,我觉得大家也都应该了解下。

从缓存取不到的数据,在数据库中也没有取到,这时也可以将对应Key的Value对写为null、位置错误、稍后重试这样的值具体取啥问产品,或者看具体的场景,缓存有效时间可以设置短点,如30秒(设置太长会导致正常情况也没法使用)。

这样可以防止攻击用户反复用同一个id暴力攻击,但是我们要知道正常用户是不会在单秒内发起这么多次请求的,那网关层Nginx本渣我也记得有配置项,可以让运维大大对单个IP每秒访问次数超出阈值的IP都拉黑。

那你还有别的办法么?

还有我记得Redis还有一个高级用法布隆过滤器(Bloom Filter)这个也能很好的防止缓存穿透的发生,他的原理也很简单就是利用高效的数据结构和算法快速判断出你这个Key是否在数据库中存在,不存在你return就好了,存在你就去查了DB刷新KV再return。

那又有小伙伴说了如果黑客有很多个IP同时发起攻击呢?这点我一直也不是很想得通,但是一般级别的黑客没这么多肉鸡,再者正常级别的Redis集群都能抗住这种级别的访问的,小公司我想他们不会感兴趣的。把系统的高可用做好了,集群还是很能顶的。

缓存击穿的话,设置热点数据永远不过期。或者加上互斥锁就能搞定了
作为暖男,代码我肯定帮你们准备好了

面试结束

嗯嗯还不错,三个点都回答得很好,今天也不早了,面试就先到这里,明天你再过来二面我继续问一下你关于Redis集群高可用,主从同步,哨兵等知识点的问题。

晕居然还有下一轮面试!(强行下一期的伏笔哈哈)但是为了offer还是得舔,嗯嗯,好的帅气面试官。

能回答得这么全面这么细节还是忍不住点赞

(暗示点赞,每次都看了不点赞,你们想白嫖我么?你们好坏喲,不过我喜欢)

总结

我们玩归玩,闹归闹,别拿面试开玩笑。

本文简单的介绍了,Redis的雪崩,击穿,穿透,三者其实都差不多,但是又有一些区别,在面试中其实这是问到缓存必问的,大家不要把三者搞混了,因为缓存雪崩、穿透和击穿,是缓存最大的问题,要么不出现,一旦出现就是致命性的问题,所以面试官一定会问你。

大家一定要理解是怎么发生的,以及是怎么去避免的,发生之后又怎么去抢救,你可以不是知道很深入,但是你不能一点都不去想,面试有时候不一定是对知识面的拷问,或许是对你的态度的拷问,如果你思路清晰,然后知其然还知其所以然那就很赞,还知道怎么预防那来上班吧。

最后暖男我继续给你们做个小的技术总结:

一般避免以上情况发生我们从三个时间段去分析下:

  • 事前:Redis 高可用,主从+哨兵,Redis cluster,避免全盘崩溃。
  • 事中:本地 ehcache 缓存 + Hystrix 限流+降级,避免** MySQL** 被打死。
  • 事后:Redis 持久化 RDB+AOF,一旦重启,自动从磁盘上加载数据,快速恢复缓存数据。

上面的几点我会在吊打系列Redis篇全部讲一下这个月应该可以吧Redis更完,限流组件,可以设置每秒的请求,有多少能通过组件,剩余的未通过的请求,怎么办?走降级!可以返回一些默认的值,或者友情提示,或者空白的值。

好处:

数据库绝对不会死,限流组件确保了每秒只有多少个请求能通过。 只要数据库不死,就是说,对用户来说,3/5 的请求都是可以被处理的。 只要有 3/5 的请求可以被处理,就意味着你的系统没死,对用户来说,可能就是点击几次刷不出来页面,但是多点几次,就可以刷出来一次。

这个在目前主流的互联网大厂里面是最常见的,你是不是好奇,某明星爆出什么事情,你发现你去微博怎么刷都空白界面,但是有的人又直接进了,你多刷几次也出来了,现在知道了吧,那是做了降级,牺牲部分用户的体验换来服务的安全,可还行?

点关注,不迷路

好了各位,以上就是这篇文章的全部内容了,能看到这里的人呀,都是人才。

我后面会每周都更新几篇一线互联网大厂面试和常用技术栈相关的文章,非常感谢人才们能看到这里,如果这个文章写得还不错,觉得「敖丙」我有点东西的话 求点赞👍 求关注❤️ 求分享👥 对暖男我来说真的 非常有用!!!

白嫖不好,创作不易,各位的支持和认可,就是我创作的最大动力,我们下篇文章见!

敖丙 | 文 【原创】

如果本篇博客有任何错误,请批评指教,不胜感激 !


文章每周持续更新,可以微信搜索「 三太子敖丙 」第一时间阅读和催更(比博客早一到两篇哟),本文 GitHub github.com/JavaFamily 已经收录,有一线大厂面试点思维导图,也整理了很多我的文档,欢迎Star和完善,大家面试可以参照考点复习,希望我们一起有点东西。

本文转载自: 掘金

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

回归Java基础:LinkedBlockingQueue阻塞

发表于 2019-11-03

前言

整理了阻塞队列LinkedBlockingQueue的学习笔记,希望对大家有帮助。有哪里不正确,欢迎指出,感谢。

LinkedBlockingQueue的概述

LinkedBlockingQueue的继承体系图

我们先来看看LinkedBlockingQueue的继承体系。使用IntelliJ IDEA查看类的继承关系图形

  • 蓝色实线箭头是指类继承关系
  • 绿色箭头实线箭头是指接口继承关系
  • 绿色虚线箭头是指接口实现关系。

LinkedBlockingQueue实现了序列化接口 Serializable,因此它有序列化的特性。
LinkedBlockingQueue实现了BlockingQueue接口,BlockingQueue继承了Queue接口,因此它拥有了队列Queue相关方法的操作。

LinkedBlockingQueue的类图

类图来自Java并发编程之美

LinkedBlockingQueue主要特性:

  1. LinkedBlockingQueue底层数据结构为单向链表。
  2. LinkedBlockingQueue 有两个Node节点,一个head节点,一个tail节点,只能从head取元素,从tail添加元素。
  3. LinkedBlockingQueue 容量是一个原子变量count,它的初始值为0。
  4. LinkedBlockingQueue有两把ReentrantLock的锁,一把控制元素入队,一把控制出队,保证在并发情况下的线程安全。
  5. LinkedBlockingQueue 有两个条件变量,notEmpty 和 notFull。它们内部均有一个条件队列,存放着出入队列被阻塞的线程,这其实是生产者-消费者模型。

LinkedBlockingQueue的重要成员变量

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
复制代码//容量范围,默认值为 Integer.MAX_VALUE
private final int capacity;

//当前队列元素个数
private final AtomicInteger count = new AtomicInteger();

//头结点
transient Node<E> head;

//尾节点
private transient Node<E> last;

//take, poll等方法的可重入锁
private final ReentrantLock takeLock = new ReentrantLock();

//当队列为空时,执行出队操作(比如take )的线程会被放入这个条件队列进行等待
private final Condition notEmpty = takeLock.newCondition();

//put, offer等方法的可重入锁
private final ReentrantLock putLock = new ReentrantLock();

//当队列满时, 执行进队操作( 比如put)的线程会被放入这个条件队列进行等待
private final Condition notFull = putLock.newCondition();

LinkedBlockingQueue的构造函数

LinkedBlockingQueue有三个构造函数:

  1. 无参构造函数,容量为Integer.MAX
1
2
3
复制代码public LinkedBlockingQueue() {
this(Integer.MAX_VALUE);
}
  1. 设置指定容量的构造器
1
2
3
4
5
6
7
复制代码public LinkedBlockingQueue(int capacity) {
if (capacity <= 0) throw new IllegalArgumentException();
//设置队列大小
this.capacity = capacity;
//new一个null节点,head、tail节点指向该节点
last = head = new Node<E>(null);
}
  1. 传入集合,如果调用该构造器,容量默认也是Integer.MAX_VALUE
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
复制代码 public LinkedBlockingQueue(Collection<? extends E> c) {
//调用指定容量的构造器
this(Integer.MAX_VALUE);
//获取put, offer的可重入锁
final ReentrantLock putLock = this.putLock;
putLock.lock();
try {
int n = 0;
//循环向队列中添加集合中的元素
for (E e : c) {
if (e == null)
throw new NullPointerException();
if (n == capacity)
throw new IllegalStateException("Queue full");
//将队列的last节点指向该节点
enqueue(new Node<E>(e));
++n;
}
//更新容量值
count.set(n);
} finally {
//释放锁
putLock.unlock();
}
}

LinkedBlockingQueue底层Node类

Node源码

1
2
3
4
5
6
7
8
9
10
复制代码static class Node<E> {
// 当前节点的元素值
E item;
// 下一个节点的索引
Node<E> next;
//节点构造器
Node(E x) {
item = x;
}
}

LinkedBlockingQueue的节点符合单向链表的数据结构要求:

  • 一个成员变量为当前节点的元素值
  • 一个成员变量是下一节点的索引
  • 构造方法的唯一参数节点元素值。

Node节点图

item表示当前节点的元素值,next表示指向下一节点的指针

LinkedBlockingQueue常用操作

offer操作

入队方法,其实就是向队列的尾部插入一个元素。如果元素为空,抛出空指针异常。如果队列已满,则丢弃当前元素,返回false,它是非阻塞的。如果队列空闲则插入成功返回true。

offer源代码

offer方法源码如下:

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
复制代码   public boolean offer(E e) {
//为空直接抛空指针
if (e == null) throw new NullPointerException();
final AtomicInteger count = this.count;
//如果当前队列满了的话,直接返回false
if (count.get() == capacity)
return false;
int c = -1;
//构造新节点
Node<E> node = new Node<E>(e);
获取put独占锁
final ReentrantLock putLock = this.putLock;
putLock.lock();
try {
//判断队列是否已满
if (count.get() < capacity) {
//进队列
enqueue(node);
//递增元素计数
c = count.getAndIncrement();
//如果元素入队,还有空闲,则唤醒notFull条件队列里被阻塞的线程
if (c + 1 < capacity)
notFull.signal();
}
} finally {
//释放锁
putLock.unlock();
}
//如果容量为0,则
if (c == 0)
//激活 notEmpty 的条件队列,唤醒被阻塞的线程
signalNotEmpty();
return c >= 0;
}

enqueue方法源码如下:

1
2
3
4
复制代码private void enqueue(Node<E> node) {
//从尾节点加进去
last = last.next = node;
}

为了形象生动,我们用一张图来看看往队列里依次放入元素A和元素B。图片参考来源【细谈Java并发】谈谈LinkedBlockingQueue

signalNotEmpty方法源码如下

1
2
3
4
5
6
7
8
9
10
11
12
复制代码private void signalNotEmpty() {
//获取take独占锁
final ReentrantLock takeLock = this.takeLock;
takeLock.lock();
try {
//唤醒notEmpty条件队列里被阻塞的线程
notEmpty.signal();
} finally {
//释放锁
takeLock.unlock();
}
}

offer执行流程图

基本流程:

  • 判断元素是否为空,如果是,就抛出空指针异常。
  • 判读队列是否已满,如果是,添加失败,返回false。
  • 如果队列没满,构造Node节点,上锁。
  • 判断队列是否已满,如果队列没满,Node节点在队尾加入队列待。
  • 加入队列后,判断队列是否还有空闲,如果是,唤醒notFull的阻塞线程。
  • 释放完锁后,判断容量是否为空,如果是,唤醒notEmpty的阻塞线程。

put操作

put方法也是向队列尾部插入一个元素。如果元素为null,抛出空指针异常。如果队列己满则阻塞当前线程,直到队列有空闲插入成功为止。如果队列空闲则插入成功,直接返回。如果在阻塞时被其他线程设置了中断标志, 则被阻塞线程会抛出 InterruptedException 异常而返回。

put源代码

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
复制代码  public void put(E e) throws InterruptedException {
////为空直接抛空指针异常
if (e == null) throw new NullPointerException();
int c = -1;
// 构造新节点
Node<E> node = new Node<E>(e);
//获取putLock独占锁
final ReentrantLock putLock = this.putLock;
final AtomicInteger count = this.count;
//获取独占锁,它跟lock的区别,是可以被中断
putLock.lockInterruptibly();
try {
//队列已满线程挂起等待
while (count.get() == capacity) {
notFull.await();
}
//进队列
enqueue(node);
//递增元素计数
c = count.getAndIncrement();
//如果元素入队,还有空闲,则唤醒notFull条件队列里被阻塞的线程
if (c + 1 < capacity)
notFull.signal();
} finally {
//释放锁
putLock.unlock();
}
//如果容量为0,则
if (c == 0)
//激活 notEmpty 的条件队列,唤醒被阻塞的线程
signalNotEmpty();
}

put流程图

基本流程:

  • 判断元素是否为空,如果是就抛出空指针异常。
  • 构造Node节点,上锁(可中断锁)
  • 判断队列是否已满,如果是,阻塞当前线程,一直等待。
  • 如果队列没满,Node节点在队尾加入队列。
  • 加入队列后,判断队列是否还有空闲,如果是,唤醒notFull的阻塞线程。
  • 释放完锁后,判断容量是否为空,如果是,唤醒notEmpty的阻塞线程。

poll操作

从队列头部获取并移除一个元素, 如果队列为空则返回 null, 该方法是不阻塞的。

poll源代码

poll方法源代码

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
复制代码 public E poll() {
final AtomicInteger count = this.count;
//如果队列为空,返回null
if (count.get() == 0)
return null;
E x = null;
int c = -1;
//获取takeLock独占锁
final ReentrantLock takeLock = this.takeLock;
takeLock.lock();
try {
//如果队列不为空,则出队,并递减计数
if (count.get() > 0) {
x = dequeue();
c = count.getAndDecrement();
////容量大于1,则激活 notEmpty 的条件队列,唤醒被阻塞的线程
if (c > 1)
notEmpty.signal();
}
} finally {
//释放锁
takeLock.unlock();
}
if (c == capacity)
//唤醒notFull条件队列里被阻塞的线程
signalNotFull();
return x;
}

dequeue方法源代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
复制代码  //出队列
private E dequeue() {
//获取head节点
Node<E> h = head;
//获取到head节点指向的下一个节点
Node<E> first = h.next;
//head节点原来指向的节点的next指向自己,等待下次gc回收
h.next = h; // help GC
// head节点指向新的节点
head = first;
// 获取到新的head节点的item值
E x = first.item;
// 新head节点的item值设置为null
first.item = null;
return x;
}

为了形象生动,我们用一张图来描述出队过程。图片参考来源【细谈Java并发】谈谈LinkedBlockingQueue

signalNotFull方法源码

1
2
3
4
5
6
7
8
9
10
11
12
复制代码 private void signalNotFull() {
//获取put独占锁
final ReentrantLock putLock = this.putLock;
putLock.lock();
try {
////唤醒notFull条件队列里被阻塞的线程
notFull.signal();
} finally {
//释放锁
putLock.unlock();
}
}

poll流程图

基本流程:

  • 判断元素是否为空,如果是,就返回null。
  • 加锁
  • 判断队列是否有元素,如果没有,释放锁
  • 如果队列有元素,则出队列,获取数据,容量计数器减一。
  • 判断此时容量是否大于1,如果是,唤醒notEmpty的阻塞线程。
  • 释放完锁后,判断容量是否满,如果是,唤醒notFull的阻塞线程。

peek操作

获取队列头部元素但是不从队列里面移除它,如果队列为空则返回 null。 该方法是不 阻塞的。

peek源代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
复制代码  public E peek() {
//队列容量为0,返回null
if (count.get() == 0)
return null;
//获取takeLock独占锁
final ReentrantLock takeLock = this.takeLock;
takeLock.lock();
try {
Node<E> first = head.next;
//判断first是否为null,如果是直接返回
if (first == null)
return null;
else
return first.item;
} finally {
//释放锁
takeLock.unlock();
}
}

peek流程图

基本流程:

  • 判断队列容量大小是否为0,如果是,就返回null。
  • 加锁
  • 获取队列头部节点first
  • 判断节点first是否为null,是的话,返回null。
  • 如果fist不为null,返回节点first的元素。
  • 释放锁。

take操作

获取当前队列头部元素并从队列里面移除它。 如果队列为空则阻塞当前线程直到队列 不为空然后返回元素,如果在阻塞时被其他线程设置了中断标志, 则被阻塞线程会抛出 InterruptedException 异常而返回。

take源代码

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
复制代码 public E take() throws InterruptedException {
E x;
int c = -1;
final AtomicInteger count = this.count;
//获取takeLock独占锁
final ReentrantLock takeLock = this.takeLock;
//获取独占锁,它跟lock的区别,是可以被中断
takeLock.lockInterruptibly();
try {
//当前队列为空,则阻塞挂起
while (count.get() == 0) {
notEmpty.await();
}
//)出队并递减计数
x = dequeue();
c = count.getAndDecrement();
if (c > 1)
//激活 notEmpty 的条件队列,唤醒被阻塞的线程
notEmpty.signal();
} finally {
//释放锁
takeLock.unlock();
}
if (c == capacity)
//激活 notFull 的条件队列,唤醒被阻塞的线程
signalNotFull();
return x;
}

take流程图

基本流程:

  • 加锁
  • 判断队列容量大小是否为0,如果是,阻塞当前线程,直到队列不为空。
  • 如果队列容量大小大于0,节点出队列,获取元素x,计数器减一。
  • 判断队列容量大小是否大于1,如果是,唤醒notEmpty的阻塞线程。
  • 释放锁。
  • 判断队列容量是否已满,如果是,唤醒notFull的阻塞线程。
  • 返回出队元素x

remove操作

删除队列里面指定的元素,有则删除并返回 true,没有则返回 false。

remove方法源代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
复制代码 public boolean remove(Object o) {
//为空直接返回false
if (o == null) return false;
//双重加锁
fullyLock();
try {
//边历队列,找到元素则删除并返回true
for (Node<E> trail = head, p = trail.next;
p != null;
trail = p, p = p.next) {
if (o.equals(p.item)) {
//执行unlink操作
unlink(p, trail);
return true;
}
}
return false;
} finally {
//解锁
fullyUnlock();
}
}

双重加锁,fullyLock方法源代码

1
2
3
4
5
6
复制代码void fullyLock() {
//putLock独占锁加锁
putLock.lock();
//takeLock独占锁加锁
takeLock.lock();
}

unlink方法源代码

1
2
3
4
5
6
7
8
9
复制代码  void unlink(Node<E> p, Node<E> trail) {
p.item = null;
trail.next = p.next;
if (last == p)
last = trail;
//如果当前队列满 ,则删除后,也不忘记唤醒等待的线程
if (count.getAndDecrement() == capacity)
notFull.signal();
}

fullyUnlock方法源代码

1
2
3
4
5
复制代码  void fullyUnlock() {
//与双重加锁顺序相反,先解takeLock独占锁
takeLock.unlock();
putLock.unlock();
}

remove流程图

基本流程

  • 判断要删除的元素是否为空,是就返回false。
  • 如果要删除的元素不为空,加双重锁
  • 遍历队列,找到要删除的元素,如果找不到,返回false。
  • 如果找到,删除该节点,返回true。
  • 释放锁

size操作

获取当前队列元素个数。

1
2
3
复制代码 public int size() {
return count.get();
}

由于进行出队、入队操作时的 count是加了锁的,所以结果相比ConcurrentLinkedQueue 的 size 方法比较准确。

总结

  • LinkedBlockingQueue底层通过单向链表实现。
  • 它有头尾两个节点,入队操作是从尾节点添加元素,出队操作是对头节点进行操作。
  • 它的容量是原子变量count,保证szie获取的准确性。
  • 它有两把独占锁,保证了队列操作原子性。
  • 它的两把锁都配备了一个条件队列,用来存放阻塞线程,结合入队、出队操作实现了一个生产消费模型。

Java并发编程之美中,有一张图惟妙惟肖描述了它,如下图:

参看与感谢

  • 《Java并发编程之美》
  • 阻塞队列之LinkedBlockingQueue
  • Java并发之LinkedBlockingQueue
  • 【细谈Java并发】谈谈LinkedBlockingQueue

个人公众号

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

本文转载自: 掘金

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

串的两种模式匹配方式(BF/KMP算法)

发表于 2019-11-03

前言

串,又称作字符串,它是由0个或者多个字符所组成的有限序列,串同样可以采用顺序存储和链式存储两种方式进行存储,在主串中查找定位子串问题(模式匹配)是串中最重要的操作之一,而不同的算法实现有着不同的效率,我们今天就来对比学习串的两种模式匹配方式:

  • 朴素的模式匹配算法(Brute-Force算法,简称BF算法)
  • KMP模式匹配算法

朴素的模式匹配算法(BF算法)

BF算法是模式匹配中的一种常规算法,它的思想就是:

  • 第一轮:子串中的第一个字符与主串中的第一个字符进行比较
    • 若相等,则继续比较主串与子串的第二个字符
    • 若不相等,进行第二轮比较
  • 第二轮:子串中的第一个字符与主串中第二个字符进行比较……
  • 第N轮:依次比较下去,直到全部匹配

图示说明:

第一轮:

第二轮:

…… 原理一致,省略中间步骤

第五轮:

第六轮:

代码实现:

看完文字与图例讲解,我们来动手实现一个这样的算法

简单归纳上面的步骤就是:

主串的每一个字符与子串的开头进行匹配,匹配成功则比较子串与主串的下一位是否匹配,匹配失败则比较子串与主串的下一位,很显然,我们可以使用两个指针来分别指向主串和子串的某个字符,来实现这样一种算法

匹配成功,返回子串在主串中第一次出现的位置,匹配失败返回 -1,子串是空串返回 0

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
复制代码int String::bfFind(const String &s, int pos) const {
//主串和子串的指针,i主串,j子串
int i, j;
//主串比子串小,匹配失败,curLenght为串的长度
if (curLength < s.curLenght)
return -1;

while (i < curLength && j < s.curLength) {
//对应字符相等,指针后移
if (data[i] == s.data[j])
i+, j++;
else { //对应字符不相等
i = i -j + 1; //主串指针移动
j = 0; //子串从头开始
}
//返回子串在主串的位置
if (j >= s.curLength)
return (i - s.curLength);
else return -1;
}
}

注:代码只为体现算法思路,具体定义未给出

这种算法简单易懂,却存在着一个很大的缺点,那就是需要多次回溯,效率低下,若主串为 000000000001 子串为00001,这就意味着每一轮都要比较到子串的最后一个字符才会匹配失败,有没有更好的办法呢?下面的KMP模式匹配算法就很好的解决了这一问题

KMP模式匹配算法

如果仅仅进行一些少量数据的运算,可能你甚至觉得BF算法也还行,起码是很容易写出来的,毕竟能跑的就是好程序,但是一旦数据量增大,你就会发现有一些 “无用功” 真的会大大的拖慢你的速度

KMP模式配算法是由 D.E.Knuth,J.H.Morris,V.R.Pratt 三位前辈提出的,它是一种对朴素模式匹配算法的改进,核心就是利用匹配失败后的信息,尽量减少子主串的匹配次数,其体现就是 主串指针一直往后移动,子串指针回溯

图示说明:

下面所表示的是朴素模式匹配算法的过程,我们看看如果使用KMP算法的思想,哪些步骤是可以省略掉的

① 中前五个元素,均互相匹配,知道第六个元素才匹配失败,按照BF算法来说,就直接进行 ② ③ 操作,但是,我们可以发现,子串中的前三个元素 a b c 均不是相同的,但是在 ① 中已经与 主串相匹配,所以 子串分别与主串中的第二 第三个元素匹配 一定是不匹配的,所以图中的 ② ③ 均可以省略

在 ① 中 子串中的 第一第二个元素 ab 和第四第五个元素 ab 是相同的,且 第四第五个元素 ab 已经与主串中的 第四第五个元素匹配成功,这意味着,子串中第一第二个元素 ab 一定与 主串中 第四第五个元素相匹配,所以 ④ ⑤ 步骤可以省略

如果按照这种思路,上面的例子只需要执行 ① 和 ⑥ 就可以了

next 数组值推导

(一) 主串指针是否需要回溯

我们观察上面的两种过程 ,BF算法-①②③④⑤⑥,KMP算法-①⑥,如果我们现在假定有两个指针,i 和 j,分别指向主串和子串中的所处位置,从上图我们可以知道,主串指针,也就是 i 的值在 ① 的状态下, 指针指向6的位置,而在 ②③④⑤ 中却分别指向了2345,而在 ⑥ 中仍指向6的位置

这说明,朴素模式匹配算法,主串的 i 值会不断的进行回溯,但是 KMP模式匹配算法将这种没必要的回溯省略掉了,所以减少了执行次数

(二) 子串指针优化总结

既然主串指针不进行回溯,那么还可以优化的就是 子串指针了,一般会遇到两种情况 我们举两个例子:

  • 如果子串为 abcdef,主串为abcdexabcdef,当第一轮匹配到第六个字符f和x的时候,匹配失败了,这个时候如果按照朴素模式匹配,就需要拿子串的首元素a去分别和主串的bcde进行比较,但是由于子串f元素前的元素中没有相同的元素,并且与主串匹配,所以a与主串中的2-5号元素 即 bcde 都是不可能相匹配的,所有这几部都可以省略,直接让a和主串中的x去匹配
  • 如果子串为abcabx,主串为abcababcax,在第一轮中,前五个元素子主串分别相匹配,第六个元素位置出错,按照朴素模式匹配,我们需要拿子串首元素a,依次与主串中的a后面的元素匹配,但是子串前面三个字符abc是不相等的,按照我们第一种情况的经验,就直接跳过这些步骤了,所有我们直接拿 子串a与 主串第四个元素a进行比较就可以了,但是我们发现,子串中出错的位置x前的串 abcab 的前缀和后缀都是 ab,既然第一轮的时候,已经匹配成功,那就意味着,子串中的 第一第二个元素ab一定与 主串中 第四第五个元素 ab相等,所以这个步骤也可以省略,也就直接可以拿子串前缀ab后面的c开始于a进行比对,这也就是我们上面图中例子的详细思路

总结:所以我们得出规律,子串指针的值取决于,子串前后缀元素的相似程度

想要应用到具体代码中,我们可以把子串位置变化 的 j 值定义成一个next数组,且长度与子串长度相同

next[j]=
\begin{cases}
-1 && 当j = 0\\
max & {k|0<k<j 且 "T_0 T_1...T_k-_1" = "T_j-_k T_j-_k+_1 ...T_j-_1"} & 当集合不为空时\\
0 &&其他情况 \\
\end{cases}

  • 情况1:当 j = 0 时,next[j] = -1, 表示子串指针指向下标为0的元素的时候匹配失败,子串无法回溯,(j不能赋值-1) ,此时将主串指针后移一位,子串不,进行下一轮比较
  • 情况2:在已经匹配的子串中,存在相同的前缀串 T0 T1 … Tk-1 和后缀串 Tj-k Tj-k+1 … Tj-1,子串指针则回溯到next[j] = k的位置,然后进行下一趟比较,例如:子串 abcabc 有相同前缀和后缀ab 所以子串指针回溯到 c的位置
  • 情况3:在已经匹配的子串,若不存在相等的前缀和后缀,则主串指针不动,子串指针回溯到 j = 0 的位置,然后进行下一趟比较

例:主串 S = “abc520abc520abcd”, 子串 T = “abc520abcd” ,利用 KMP算法匹配过程

子串 next 数组

j 0 1 2 3 4 5 6 7 8 9
子串 a b c 5 2 0 a b c d
next[j] -1 0 0 0 0 0 0 1 2 3

可以看到,在 指针 i = 9 且 j = 9 的时候,匹配失败, 此时 next[9] = 3 ,所以子串指针回溯到 下标 j = 3 的位置也就是元素 5 的位置,进行第二轮比较,然后正好全部匹配成功

(三) 求next数组算法实现

1
2
3
4
5
6
7
8
9
10
11
12
复制代码void Stirng::getNext(const String &t, int *next) {
int i = 0, j = -1;
next[0] = -1;
while (i < t.curLength - 1) {
if ((j == -1) || t[i] == t[j]) {
++i, ++j;
next[i] = j;
}else{
j = next[j];
}
}
}

KMP算法代码实现

有了 next 数组的铺垫,我们就可以来实现KMP算法了

匹配成功返回子串在主串中第一次出现的位置,失败返回-1,子串为空串返回0

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
复制代码int String::kmpFind(const String &t, int pos) {
//不允许申请大小为0的数组
if (t,curLength == 0) return 0;
//如果主串比子串小,匹配失败
if(t.curLength < t.curLength) return -1;
//主串指针i,子串指针j
int i = 0, j = 0;
int *next = new int[t.curLrngth];
getNext(t,next);
while (i < curLength && j < t,curLength) {
if (j == -1 || data[i] == t.data[j]) //情况12
i++, j++;
else //情况3
j = next[j];
}
delete []next;
if (j > t.curLength)
return (i - t.curLength)
else
return -1;
}

KMP模式匹配算法改进

有一种特殊情况的出现,使得我们不得不考虑KMP算法的改进

那就是子串中有多个连续重复的元素,例如主串 S=“aaabcde” 子串T=“aaaaax” 在主串指针不动,移动子串指针比较这些值,其实有很多无用功,因为子串中前5个元素都是相同的a,所以我们可以省略掉这些重复的步骤

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
复制代码void String::getNextVal(const String &t, int *nextVal) {
int i = 0, j = -1;
nextVal[0] = -1;
while (i < t.curLength -1) {
if ((k == -1) || (t[i] == t[j])) {
++i, ++j;
if (t[i] != t[j])
nextVal[i] = j;
else
nextVal[i] = nextVal[j];
}
else
j = nextVal[j];
}
}

这种改进的核心就在于 增加了对子串中 t[i] 和 t[j] 是否相等的判断,相等则直接将 nextVal[j] 的值赋给 nextVal[i]

总结

在BF算法中,当主串和子串不匹配的时候,主串和子串你的指针都需要回溯,所以导致了该算法时间复杂度比较高为 O(nm) ,空间复杂度为 O(1) 注:虽然其时间复杂度为 O(nm) 但是在一般应用下执行,其执行时间近似 O(n+m) 所以仍被使用

KMP算法,利用子串的结构相似性,设计next数组,在此之上达到了主串不回溯的效果,大大减少了比较次数,但是相对应的却牺牲了存储空间,KMP算法 时间复杂度为 O(n+m) 空间复杂度为 O(n)

结尾:

如果文章中有什么不足,或者错误的地方,欢迎大家留言分享想法,感谢朋友们的支持!

如果能帮到你的话,那就来关注我吧!如果您更喜欢微信文章的阅读方式,可以关注我的公众号

在这里的我们素不相识,却都在为了自己的梦而努力 ❤

一个坚持推送原创开发技术文章的公众号:理想二旬不止

本文转载自: 掘金

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

基于AOP和Redis实现对接口调用情况的接口及IP限流

发表于 2019-11-01

[toc]

需求描述

  1. 项目中有许多接口,现在我们需要实现一个功能对接口调用情况进行统计,主要功能如下:
    • 需求一:实现对每个接口,每天的调用次数做记录;
    • 需求二:如果某次调用抛出了异常信息,则记录下异常信息;
    • 需求三:限流,限制单个IP一天内对一个接口的调用次数。

概要设计

  1. 因为需要对每个接口的调用情况进行统计,所以选择AOP来实现,将Controller层抽象为一个切面
* @Before 执行业务操作前进行限流判断;
* @AfterReturn 如果正常返回则调用次数加1;
* @AfterThrowing 如果抛出异常则记录异常信息。如果将这些信息写入数据库的话会对每个接口带来额外的操作数据库的开销,影响接口响应时间,且此类记录信息较多,所以此处选择Redis将这些信息缓存下来。
  1. Redis设计
* 对于需求一,我们需要记录三个信息:1、调用的接口名;2、调用的日期(精确到天);3、调用次数。所以此处Redis的key使用Hash结构,数据结构如下:key = 接口URI、key = 调用日期(到天)、value = 调用次数(初始值为1,没一次调用后自增1)。
* 对于需求二,需要记录的信息有:1、调用的接口名;2、异常发生时间(精确到毫秒);3、异常信息。因为需求一的key已经设置成了接口URI,所以此处选择使用URI + 后缀“\_exception”的形式来代表异常信息的key。所以此需求Redis的数据结构设计如下(仍然使用Hash结构):key = URI + “\_exception”、key = 异常发生时间(精确到毫秒)、value = 异常信息。
* 对于需求三,我们需要记录的信息有:1、调用的接口名;2、ip地址;3、调用时间;4、调用次数。此需求需要记录的信息较多,但是我们可以将信息1、信息2、信息3组合起来拼接成一个唯一的key即可,将调用时间的维度精确到天且设置key的过期时间为一天,这样的一个key即可代表单个IP一天时间内访问了哪些接口。所以Redis的数据结构设计如下:key = URI + ip +date(精确到天)、value = 调用次数。

代码实现

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
复制代码/**
* 接口调用情况监控
* 1、监控单个接口一天内的调用次数
* 2、如果抛出异常,则记录异常信息及发生时间
* 3、对单个IP进行限流,每天对每个接口的调用次数有限
*
* @author csh
* @date 2019/10/30
*/
@Aspect
@Component
public class ApiCallAdvice {

@Resource
private RedisTemplate redisTemplate;
@Resource
private StringRedisTemplate stringRedisTemplate;

private static final String FORMAT_PATTERN_DAY = "yyyy-MM-dd";
private static final String FORMAT_PATTERN_MILLS = "yyyy-MM-dd HH:mm:ss:SSS";

/**
* 真正执行业务操作前先进行限流的验证
* 限制维度为:一天内单个IP的访问次数
* key = URI + IP + date(精确到天)
* value = 调用次数
*/
@Before("execution(* com.pagoda.erp.platform.controller.*.*(..))")
public void before() {
// 接收到请求,记录请求内容
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
//获取请求的request
HttpServletRequest request = attributes.getRequest();

String uri = request.getRequestURI();
String date = dateFormat(FORMAT_PATTERN_DAY);
String ip = getRequestIp(request);

if (StringUtils.isEmpty(ip)) {
throw new BusinessException("IP不能为空。");
}
// URI+IP+日期 构成以天为维度的key
String ipKey = uri + "_" + ip + "_" + date;
if (redisTemplate.hasKey(ipKey)) {
if (Integer.parseInt(redisTemplate.opsForValue().get(ipKey).toString()) > 10000) {
throw new BusinessException("访问失败,已超过访问次数。");
}
redisTemplate.opsForValue().increment(ipKey, 1);
} else {
stringRedisTemplate.opsForValue().set(ipKey, "1", 1L, TimeUnit.DAYS);
}
}

/**
* 如果有返回结果,代表一次调用,则对应接口的调用次数加一,统计维度为天
* (Redis使用Hash结构)
* key = URI
* key = date (精确到天)
* value = 调用次数
*/
@AfterReturning("execution(* com.pagoda.erp.platform.controller.*.*(..))")
public void afterReturning() {
// 接收到请求,记录请求内容
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
//获取请求的request
HttpServletRequest request = attributes.getRequest();

String uri = request.getRequestURI();
String date = dateFormat(FORMAT_PATTERN_DAY);
if (redisTemplate.hasKey(uri)) {
redisTemplate.boundHashOps(uri).increment(date, 1);
} else {
redisTemplate.boundHashOps(uri).put(date, 1);
}
}

/**
* 如果调用抛出异常,则缓存异常信息(Redis使用Hash结构)
* key = URI + “_exception”
* key = time (精确到毫秒的时间)
* value = exception 异常信息
*
* @param ex 异常信息
*/
@AfterThrowing(value = "execution(* com.pagoda.erp.platform.controller.*.*(..))", throwing = "ex")
public void afterThrowing(Exception ex) {
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest request = attributes.getRequest();

String uri = request.getRequestURI() + "_exception";
String time = dateFormat(FORMAT_PATTERN_MILLS);
String exception = ex.getMessage();

redisTemplate.boundHashOps(uri).put(time, exception);
}

private String getRequestIp(HttpServletRequest request) {
// 获取请求IP
String ip = request.getHeader("x-forwarded-for");
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip) || "null".equals(ip)) {
ip = "" + request.getHeader("Proxy-Client-IP");
}
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip) || "null".equals(ip)) {
ip = "" + request.getHeader("WL-Proxy-Client-IP");
}
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip) || "null".equals(ip)) {
ip = "" + request.getRemoteAddr();
}
return ip;
}

private String dateFormat(String pattern) {
SimpleDateFormat dateFormat = new SimpleDateFormat(pattern);
return dateFormat.format(new Date());
}
}

参考资料

  • 使用aop+redis+注解 实现 限制单位时间内访问接口的次数
  • Spring AOP实例

本文转载自: 掘金

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

SpringBoot系列教程之Bean之指定初始化顺序的若干

发表于 2019-10-31

上一篇博文介绍了@Order注解的常见错误理解,它并不能指定 bean 的加载顺序,那么问题来了,如果我需要指定 bean 的加载顺序,那应该怎么办呢?

本文将介绍几种可行的方式来控制 bean 之间的加载顺序

  • 构造方法依赖
  • @DependOn 注解
  • BeanPostProcessor 扩展

原文: SpringBoot系列教程之Bean之指定初始化顺序的若干姿势

I. 环境搭建

我们的测试项目和上一篇博文公用一个项目环境,当然也可以建一个全新的测试项目,对应的配置如下:(文末有源码地址)

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
xml复制代码<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.7</version>
<relativePath/> <!-- lookup parent from update -->
</parent>

<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<spring-cloud.version>Finchley.RELEASE</spring-cloud.version>
<java.version>1.8</java.version>
</properties>

<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
</dependencies>

<build>
<pluginManagement>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</pluginManagement>
</build>
<repositories>
<repository>
<id>spring-milestones</id>
<name>Spring Milestones</name>
<url>https://repo.spring.io/milestone</url>
<snapshots>
<enabled>false</enabled>
</snapshots>
</repository>
</repositories>

II. 初始化顺序指定

1. 构造方法依赖

这种可以说是最简单也是最常见的使用姿势,但是在使用时,需要注意循环依赖等问题

我们知道 bean 的注入方式之中,有一个就是通过构造方法来注入,借助这种方式,我们可以解决有优先级要求的 bean 之间的初始化顺序

比如我们创建两个 Bean,要求 CDemo2 在 CDemo1 之前被初始化,那么我们的可用方式

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

private String name = "cdemo 1";

public CDemo1(CDemo2 cDemo2) {
System.out.println(name);
}
}

@Component
public class CDemo2 {

private String name = "cdemo 1";

public CDemo2() {
System.out.println(name);
}
}

实测输出结果如下,和我们预期一致

虽然这种方式比较直观简单,但是有几个限制

  • 需要有注入关系,如 CDemo2 通过构造方法注入到 CDemo1 中,如果需要指定两个没有注入关系的 bean 之间优先级,则不太合适(比如我希望某个 bean 在所有其他的 Bean 初始化之前执行)
  • 循环依赖问题,如过上面的 CDemo2 的构造方法有一个 CDemo1 参数,那么循环依赖产生,应用无法启动

另外一个需要注意的点是,在构造方法中,不应有复杂耗时的逻辑,会拖慢应用的启动时间

2. @DependOn 注解

这是一个专用于解决 bean 的依赖问题,当一个 bean 需要在另一个 bean 初始化之后再初始化时,可以使用这个注解

使用方式也比较简单了,下面是一个简单的实例 case

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
java复制代码@DependsOn("rightDemo2")
@Component
public class RightDemo1 {
private String name = "right demo 1";

public RightDemo1() {
System.out.println(name);
}
}

@Component
public class RightDemo2 {
private String name = "right demo 2";

public RightDemo2() {
System.out.println(name);
}
}

上面的注解放在 RightDemo1 上,表示RightDemo1的初始化依赖于rightDemo2这个 bean

在使用这个注解的时候,有一点需要特别注意,它能控制 bean 的实例化顺序,但是 bean 的初始化操作(如构造 bean 实例之后,调用@PostConstruct注解的初始化方法)顺序则不能保证,比如我们下面的一个实例,可以说明这个问题

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
java复制代码@DependsOn("rightDemo2")
@Component
public class RightDemo1 {
private String name = "right demo 1";

@Autowired
private RightDemo2 rightDemo2;

public RightDemo1() {
System.out.println(name);
}

@PostConstruct
public void init() {
System.out.println(name + " _init");
}
}

@Component
public class RightDemo2 {
private String name = "right demo 2";

@Autowired
private RightDemo1 rightDemo1;

public RightDemo2() {
System.out.println(name);
}

@PostConstruct
public void init() {
System.out.println(name + " _init");
}
}

注意上面的代码,虽然说有循环依赖,但是通过@Autowired注解方式注入的,所以不会导致应用启动失败,我们先看一下输出结果

有意思的地方来了,我们通过@DependsOn注解来确保在创建RightDemo1之前,先得创建RightDemo2;

所以从构造方法的输出可以知道,先实例 RightDemo2, 然后实例 RightDemo1;

然后从初始化方法的输出可以知道,在上面这个场景中,虽然 RightDemo2 这个 bean 创建了,但是它的初始化代码在后面执行

题外话:
有兴趣的同学可以试一下把上面测试代码中的@Autowired的依赖注入删除,即两个 bean 没有相互注入依赖,再执行时,会发现输出顺序又不一样

3. BeanPostProcessor

最后再介绍一种非典型的使用方式,如非必要,请不要用这种方式来控制 bean 的加载顺序

先创建两个测试 bean

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
java复制代码@Component
public class HDemo1 {
private String name = "h demo 1";

public HDemo1() {
System.out.println(name);
}
}

@Component
public class HDemo2 {
private String name = "h demo 2";

public HDemo2() {
System.out.println(name);
}
}

我们希望 HDemo2 在 HDemo1 之前被加载,借助 BeanPostProcessor,我们可以按照下面的方式来实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
java复制代码@Component
public class DemoBeanPostProcessor extends InstantiationAwareBeanPostProcessorAdapter implements BeanFactoryAware {
private ConfigurableListableBeanFactory beanFactory;
@Override
public void setBeanFactory(BeanFactory beanFactory) {
if (!(beanFactory instanceof ConfigurableListableBeanFactory)) {
throw new IllegalArgumentException(
"AutowiredAnnotationBeanPostProcessor requires a ConfigurableListableBeanFactory: " + beanFactory);
}
this.beanFactory = (ConfigurableListableBeanFactory) beanFactory;
}

@Override
@Nullable
public Object postProcessBeforeInstantiation(Class<?> beanClass, String beanName) throws BeansException {
// 在bean实例化之前做某些操作
if ("HDemo1".equals(beanName)) {
HDemo2 demo2 = beanFactory.getBean(HDemo2.class);
}
return null;
}
}

请将目标集中在postProcessBeforeInstantiation,这个方法在某个 bean 的实例化之前,会被调用,这就给了我们控制 bean 加载顺序的机会

看到这种骚操作,是不是有点蠢蠢欲动,比如我有个 bean,希望在应用启动之后,其他的 bean 实例化之前就被加载,用这种方式是不是也可以实现呢?

下面是一个简单的实例 demo,重写DemoBeanPostProcessor的postProcessAfterInstantiation方法,在 application 创建之后,就加载我们的 FDemo 这个 bean

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
java复制代码@Override
public boolean postProcessAfterInstantiation(Object bean, String beanName) throws BeansException {
if ("application".equals(beanName)) {
beanFactory.getBean(FDemo.class);
}

return true;
}


@DependsOn("HDemo")
@Component
public class FDemo {
private String name = "F demo";

public FDemo() {
System.out.println(name);
}
}

@Component
public class HDemo {
private String name = "H demo";

public HDemo() {
System.out.println(name);
}
}

从下图输出可以看出,HDemo, FDemo的实例化顺序放在了最前面了

4. 小结

在小结之前,先指出一下,一个完整的 bean 创建,在本文中区分了两块顺序

  • 实例化 (调用构造方法)
  • 初始化 (注入依赖属性,调用@PostConstruct方法)

本文主要介绍了三种方式来控制 bean 的加载顺序,分别是

  • 通过构造方法依赖的方式,来控制有依赖关系的 bean 之间初始化顺序,但是需要注意循环依赖的问题
  • @DependsOn注解,来控制 bean 之间的实例顺序,需要注意的是 bean 的初始化方法调用顺序无法保证
  • BeanPostProcessor 方式,来手动控制 bean 的加载顺序

II. 其他

0. 项目

  • SpringBoot系列教程之Bean加载顺序之错误使用姿势辟谣
  • 工程:github.com/liuyueyi/sp…
  • 项目: github.com/liuyueyi/sp…

1. 一灰灰 Blog

尽信书则不如,以上内容,纯属一家之言,因个人能力有限,难免有疏漏和错误之处,如发现 bug 或者有更好的建议,欢迎批评指正,不吝感激

下面一灰灰的个人博客,记录所有学习和工作中的博文,欢迎大家前去逛逛

  • 一灰灰 Blog 个人博客 blog.hhui.top
  • 一灰灰 Blog-Spring 专题博客 spring.hhui.top

本文转载自: 掘金

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

Docker搭建Portainer可视化界面

发表于 2019-10-31
为了解决上回说到的问题,在网上找了找 找到了一个 非常有好的可视化界面管理工具.

Portainer 是什么东西 (开源轻量级)

Portainer是Docker的图形化管理工具,提供状态显示面板、应用模板快速部署、容器镜像网络数据卷的基本操作(包括上传下载镜像,创建容器等操作)、事件日志显示、容器控制台操作、Swarm集群和服务等集中管理和操作、登录用户管理和控制等功能。功能十分全面,基本能满足中小型单位对容器管理的全部需求。
我用到的图形化管理,应用快速部署,时间日志显示,容器控制台操作 (应为我用的比较简单没有太多深入了解)

Docker和窗口下载镜像

搜索 docker search portainer
拉取 docker pull portainer/portainer
启动 docker run -d -p 9000:9000 –restart=always -v /var/run/docker.sock:/var/run/docker.sock –name prtainer-test portainer/portainer
ps: docker 运行命令讲解

-d :后台运行-p:前边是外访问端口:后边是对内访问端口–restart=always: 当 docker 重启时,容器自动启动。-v:磁盘目录映射–name: 为容器指定一个名称

见识一下 浏览器访问(127.0.0.1:9000) 就可以访问页面

新建用戶

file

我选的是本机 还有很多模式 欢迎深究

file

咣当

file

一些基本的功能

file

1
复制代码

blockchain
公众号即可获得”Spring Cloud”教程

本文转载自: 掘金

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

学习 sentry 源码整体架构,打造属于自己的前端异常监控

发表于 2019-10-31

前言

你好,我是若川。这是学习源码整体架构系列第四篇。整体架构这词语好像有点大,姑且就算是源码整体结构吧,主要就是学习是代码整体结构,不深究其他不是主线的具体函数的实现。文章学习的是打包整合后的代码,不是实际仓库中的拆分的代码。

学习源码整体架构系列文章如下:

1.学习 jQuery 源码整体架构,打造属于自己的 js 类库

2.学习 underscore 源码整体架构,打造属于自己的函数式编程类库

3.学习 lodash 源码整体架构,打造属于自己的函数式编程类库

4.学习 sentry 源码整体架构,打造属于自己的前端异常监控SDK

5.学习 vuex 源码整体架构,打造属于自己的状态管理库

6.学习 axios 源码整体架构,打造属于自己的请求库

7.学习 koa 源码的整体架构,浅析koa洋葱模型原理和co原理

8.学习 redux 源码整体架构,深入理解 redux 及其中间件原理

感兴趣的读者可以点击阅读。

导读

本文通过梳理前端错误监控知识、介绍sentry错误监控原理、sentry初始化、Ajax上报、window.onerror、window.onunhandledrejection几个方面来学习sentry的源码。

开发微信小程序,想着搭建小程序错误监控方案。最近用了丁香园 开源的Sentry 小程序 SDKsentry-miniapp。
顺便研究下sentry-javascript仓库 的源码整体架构,于是有了这篇文章。

本文分析的是打包后未压缩的源码,源码总行数五千余行,链接地址是:browser.sentry-cdn.com/5.7.1/bundl…, 版本是v5.7.1。

本文示例等源代码在这我的github博客中github blog sentry,需要的读者可以点击查看,如果觉得不错,可以顺便star一下。

看源码前先来梳理下前端错误监控的知识。

前端错误监控知识

摘抄自 慕课网视频教程:前端跳槽面试必备技巧

别人做的笔记:前端跳槽面试必备技巧-4-4 错误监控类

前端错误的分类

1.即时运行错误:代码错误

try...catch

window.onerror (也可以用DOM2事件监听)

2.资源加载错误

object.onerror: dom对象的onerror事件

performance.getEntries()

Error事件捕获

3.使用performance.getEntries()获取网页图片加载错误

var allImgs = document.getElementsByTagName('image')

var loadedImgs = performance.getEntries().filter(i => i.initiatorType === 'img')

最后allIms和loadedImgs对比即可找出图片资源未加载项目

Error事件捕获代码示例

1
2
3
复制代码window.addEventListener('error', function(e) {
console.log('捕获', e)
}, true) // 这里只有捕获才能触发事件,冒泡是不能触发

上报错误的基本原理

1.采用Ajax通信的方式上报

2.利用Image对象上报 (主流方式)

Image上报错误方式:
(new Image()).src = 'https://lxchuan12.cn/error?name=若川'

Sentry 前端异常监控基本原理

1.重写 window.onerror 方法、重写 window.onunhandledrejection 方法

如果不了解onerror和onunhandledrejection方法的读者,可以看相关的MDN文档。这里简要介绍一下:

MDN GlobalEventHandlers.onerror

1
2
3
复制代码window.onerror = function (message, source, lineno, colno, error) {
console.log('message, source, lineno, colno, error', message, source, lineno, colno, error);
}

参数:

message:错误信息(字符串)。可用于HTML onerror=""处理程序中的event。

source:发生错误的脚本URL(字符串)

lineno:发生错误的行号(数字)

colno:发生错误的列号(数字)

error:Error对象(对象)

MDN unhandledrejection

当 Promise 被 reject 且没有 reject 处理器的时候,会触发 unhandledrejection 事件;这可能发生在 window 下,但也可能发生在 Worker 中。 这对于调试回退错误处理非常有用。

Sentry 源码可以搜索 global.onerror 定位到具体位置

1
2
3
4
5
6
7
复制代码 GlobalHandlers.prototype._installGlobalOnErrorHandler = function () {
// 代码有删减
// 这里的 this._global 在浏览器中就是 window
this._oldOnErrorHandler = this._global.onerror;
this._global.onerror = function (msg, url, line, column, error) {}
// code ...
}

同样,可以搜索global.onunhandledrejection 定位到具体位置

1
2
3
4
5
复制代码GlobalHandlers.prototype._installGlobalOnUnhandledRejectionHandler = function () {
// 代码有删减
this._oldOnUnhandledRejectionHandler = this._global.onunhandledrejection;
this._global.onunhandledrejection = function (e) {}
}

2.采用Ajax上传

支持 fetch 使用 fetch,否则使用 XHR。

1
2
3
4
5
6
7
复制代码BrowserBackend.prototype._setupTransport = function () {
// 代码有删减
if (supportsFetch()) {
return new FetchTransport(transportOptions);
}
return new XHRTransport(transportOptions);
};

2.1 fetch

1
2
3
4
5
6
7
8
9
10
复制代码FetchTransport.prototype.sendEvent = function (event) {
var defaultOptions = {
body: JSON.stringify(event),
method: 'POST',
referrerPolicy: (supportsReferrerPolicy() ? 'origin' : ''),
};
return this._buffer.add(global$2.fetch(this.url, defaultOptions).then(function (response) { return ({
status: exports.Status.fromHttpCode(response.status),
}); }));
};

2.2 XMLHttpRequest

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
复制代码XHRTransport.prototype.sendEvent = function (event) {
var _this = this;
return this._buffer.add(new SyncPromise(function (resolve, reject) {
// 熟悉的 XMLHttpRequest
var request = new XMLHttpRequest();
request.onreadystatechange = function () {
if (request.readyState !== 4) {
return;
}
if (request.status === 200) {
resolve({
status: exports.Status.fromHttpCode(request.status),
});
}
reject(request);
};
request.open('POST', _this.url);
request.send(JSON.stringify(event));
}));
}

接下来主要通过Sentry初始化、如何Ajax上报和window.onerror、window.onunhandledrejection三条主线来学习源码。

如果看到这里,暂时不想关注后面的源码细节,直接看后文小结1和2的两张图。或者可以点赞或收藏这篇文章,后续想看了再看。

Sentry 源码入口和出口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
复制代码  var Sentry = (function(exports){
// code ...

var SDK_NAME = 'sentry.javascript.browser';
var SDK_VERSION = '5.7.1';

// code ...
// 省略了导出的Sentry的若干个方法和属性
// 只列出了如下几个
exports.SDK_NAME = SDK_NAME;
exports.SDK_VERSION = SDK_VERSION;
// 重点关注 captureMessage
exports.captureMessage = captureMessage;
// 重点关注 init
exports.init = init;

return exports;
}({}));

Sentry.init 初始化 之 init 函数

初始化

1
2
复制代码// 这里的dsn,是sentry.io网站会生成的。
Sentry.init({ dsn: 'xxx' });
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
复制代码// options 是 {dsn: '...'}
function init(options) {
// 如果options 是undefined,则赋值为 空对象
if (options === void 0) { options = {}; }
// 如果没传 defaultIntegrations 则赋值默认的
if (options.defaultIntegrations === undefined) {
options.defaultIntegrations = defaultIntegrations;
}
// 初始化语句
if (options.release === undefined) {
var window_1 = getGlobalObject();
// 这是给 sentry-webpack-plugin 插件提供的,webpack插件注入的变量。这里没用这个插件,所以这里不深究。
// This supports the variable that sentry-webpack-plugin injects
if (window_1.SENTRY_RELEASE && window_1.SENTRY_RELEASE.id) {
options.release = window_1.SENTRY_RELEASE.id;
}
}
// 初始化并且绑定
initAndBind(BrowserClient, options);
}

getGlobalObject、inNodeEnv 函数

很多地方用到这个函数getGlobalObject。其实做的事情也比较简单,就是获取全局对象。浏览器中是window。

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
复制代码/**
* 判断是否是node环境
* Checks whether we're in the Node.js or Browser environment
*
* @returns Answer to given question
*/
function isNodeEnv() {
// tslint:disable:strict-type-predicates
return Object.prototype.toString.call(typeof process !== 'undefined' ? process : 0) === '[object process]';
}
var fallbackGlobalObject = {};
/**
* Safely get global scope object
*
* @returns Global scope object
*/
function getGlobalObject() {
return (isNodeEnv()
// 是 node 环境 赋值给 global
? global
: typeof window !== 'undefined'
? window
// 不是 window self 不是undefined 说明是 Web Worker 环境
: typeof self !== 'undefined'
? self
// 都不是,赋值给空对象。
: fallbackGlobalObject);

继续看 initAndBind 函数

initAndBind 函数之 new BrowserClient(options)

1
2
3
4
5
6
7
复制代码function initAndBind(clientClass, options) {
// 这里没有开启debug模式,logger.enable() 这句不会执行
if (options.debug === true) {
logger.enable();
}
getCurrentHub().bindClient(new clientClass(options));
}

可以看出 initAndBind(),第一个参数是 BrowserClient 构造函数,第二个参数是初始化后的options。
接着先看 构造函数 BrowserClient。
另一条线 getCurrentHub().bindClient() 先不看。

BrowserClient 构造函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
复制代码var BrowserClient = /** @class */ (function (_super) {
// `BrowserClient` 继承自`BaseClient`
__extends(BrowserClient, _super);
/**
* Creates a new Browser SDK instance.
*
* @param options Configuration options for this SDK.
*/
function BrowserClient(options) {
if (options === void 0) { options = {}; }
// 把`BrowserBackend`,`options`传参给`BaseClient`调用。
return _super.call(this, BrowserBackend, options) || this;
}
return BrowserClient;
}(BaseClient));

从代码中可以看出:
BrowserClient 继承自BaseClient,并且把BrowserBackend,options传参给BaseClient调用。

先看 BrowserBackend,这里的BaseClient,暂时不看。

看BrowserBackend之前,先提一下继承、继承静态属性和方法。

__extends、extendStatics 打包代码实现的继承

未打包的源码是使用ES6 extends实现的。这是打包后的对ES6的extends的一种实现。

如果对继承还不是很熟悉的读者,可以参考我之前写的文章。面试官问:JS的继承

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
复制代码// 继承静态方法和属性
var extendStatics = function(d, b) {
// 如果支持 Object.setPrototypeOf 这个函数,直接使用
// 不支持,则使用原型__proto__ 属性,
// 如何还不支持(但有可能__proto__也不支持,毕竟是浏览器特有的方法。)
// 则使用for in 遍历原型链上的属性,从而达到继承的目的。
extendStatics = Object.setPrototypeOf ||
({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) ||
function (d, b) { for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; };
return extendStatics(d, b);
};

function __extends(d, b) {
extendStatics(d, b);
// 申明构造函数__ 并且把 d 赋值给 constructor
function __() { this.constructor = d; }
// (__.prototype = b.prototype, new __()) 这种逗号形式的代码,最终返回是后者,也就是 new __()
// 比如 (typeof null, 1) 返回的是1
// 如果 b === null 用Object.create(b) 创建 ,也就是一个不含原型链等信息的空对象 {}
// 否则使用 new __() 返回
d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());
}

不得不说这打包后的代码十分严谨,上面说的我的文章《面试官问:JS的继承》中没有提到不支持__proto__的情况。看来这文章可以进一步严谨修正了。
让我想起Vue源码中对数组检测代理判断是否支持__proto__的判断。

1
2
3
复制代码// vuejs 源码:https://github.com/vuejs/vue/blob/dev/dist/vue.js#L526-L527
// can we use __proto__?
var hasProto = '__proto__' in {};

看完打包代码实现的继承,继续看 BrowserBackend 构造函数

BrowserBackend 构造函数 (浏览器后端)

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
复制代码var BrowserBackend = /** @class */ (function (_super) {
__extends(BrowserBackend, _super);
function BrowserBackend() {
return _super !== null && _super.apply(this, arguments) || this;
}
/**
* 设置请求
*/
BrowserBackend.prototype._setupTransport = function () {
if (!this._options.dsn) {
// We return the noop transport here in case there is no Dsn.
// 没有设置dsn,调用BaseBackend.prototype._setupTransport 返回空函数
return _super.prototype._setupTransport.call(this);
}
var transportOptions = __assign({}, this._options.transportOptions, { dsn: this._options.dsn });
if (this._options.transport) {
return new this._options.transport(transportOptions);
}
// 支持Fetch则返回 FetchTransport 实例,否则返回 XHRTransport实例,
// 这两个构造函数具体代码在开头已有提到。
if (supportsFetch()) {
return new FetchTransport(transportOptions);
}
return new XHRTransport(transportOptions);
};
// code ...
return BrowserBackend;
}(BaseBackend));

BrowserBackend 又继承自 BaseBackend。

BaseBackend 构造函数 (基础后端)

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
复制代码/**
* This is the base implemention of a Backend.
* @hidden
*/
var BaseBackend = /** @class */ (function () {
/** Creates a new backend instance. */
function BaseBackend(options) {
this._options = options;
if (!this._options.dsn) {
logger.warn('No DSN provided, backend will not do anything.');
}
// 调用设置请求函数
this._transport = this._setupTransport();
}
/**
* Sets up the transport so it can be used later to send requests.
* 设置发送请求空函数
*/
BaseBackend.prototype._setupTransport = function () {
return new NoopTransport();
};
// code ...
BaseBackend.prototype.sendEvent = function (event) {
this._transport.sendEvent(event).then(null, function (reason) {
logger.error("Error while sending event: " + reason);
});
};
BaseBackend.prototype.getTransport = function () {
return this._transport;
};
return BaseBackend;
}());

通过一系列的继承后,回过头来看 BaseClient 构造函数。

BaseClient 构造函数(基础客户端)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
复制代码var BaseClient = /** @class */ (function () {
/**
* Initializes this client instance.
*
* @param backendClass A constructor function to create the backend.
* @param options Options for the client.
*/
function BaseClient(backendClass, options) {
/** Array of used integrations. */
this._integrations = {};
/** Is the client still processing a call? */
this._processing = false;
this._backend = new backendClass(options);
this._options = options;
if (options.dsn) {
this._dsn = new Dsn(options.dsn);
}
if (this._isEnabled()) {
this._integrations = setupIntegrations(this._options);
}
}
// code ...
return BaseClient;
}());

小结1. new BrowerClient 经过一系列的继承和初始化

可以输出下具体new clientClass(options)之后的结果:

1
2
3
4
5
6
7
8
9
10
复制代码function initAndBind(clientClass, options) {
if (options.debug === true) {
logger.enable();
}
var client = new clientClass(options);
console.log('new clientClass(options)', client);
getCurrentHub().bindClient(client);
// 原来的代码
// getCurrentHub().bindClient(new clientClass(options));
}

最终输出得到这样的数据。我画了一张图表示。重点关注的原型链用颜色标注了,其他部分收缩了。

sentry new BrowserClient 实例图 By@若川

sentry new BrowserClient 实例图 By@若川

initAndBind 函数之 getCurrentHub().bindClient()

继续看 initAndBind 的另一条线。

1
2
3
4
5
6
复制代码function initAndBind(clientClass, options) {
if (options.debug === true) {
logger.enable();
}
getCurrentHub().bindClient(new clientClass(options));
}

获取当前的控制中心 Hub,再把new BrowserClient() 的实例对象绑定在Hub上。

getCurrentHub 函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
复制代码// 获取当前Hub 控制中心
function getCurrentHub() {
// Get main carrier (global for every environment)
var registry = getMainCarrier();
// 如果没有控制中心在载体上,或者它的版本是老版本,就设置新的。
// If there's no hub, or its an old API, assign a new one
if (!hasHubOnCarrier(registry) || getHubFromCarrier(registry).isOlderThan(API_VERSION)) {
setHubOnCarrier(registry, new Hub());
}
// node 才执行
// Prefer domains over global if they are there (applicable only to Node environment)
if (isNodeEnv()) {
return getHubFromActiveDomain(registry);
}
// 返回当前控制中心来自载体上。
// Return hub that lives on a global object
return getHubFromCarrier(registry);
}

衍生的函数 getMainCarrier、getHubFromCarrier

1
2
3
4
5
6
7
8
9
10
复制代码function getMainCarrier() {
// 载体 这里是window
// 通过一系列new BrowerClient() 一系列的初始化
// 挂载在 carrier.__SENTRY__ 已经有了三个属性,globalEventProcessors, hub, logger
var carrier = getGlobalObject();
carrier.__SENTRY__ = carrier.__SENTRY__ || {
hub: undefined,
};
return carrier;
}
1
2
3
4
5
6
7
8
9
10
复制代码// 获取控制中心 hub 从载体上
function getHubFromCarrier(carrier) {
// 已经有了则返回,没有则new Hub
if (carrier && carrier.__SENTRY__ && carrier.__SENTRY__.hub) {
return carrier.__SENTRY__.hub;
}
carrier.__SENTRY__ = carrier.__SENTRY__ || {};
carrier.__SENTRY__.hub = new Hub();
return carrier.__SENTRY__.hub;
}

bindClient 绑定客户端在当前控制中心上

1
2
3
4
5
6
复制代码Hub.prototype.bindClient = function (client) {
// 获取最后一个
var top = this.getStackTop();
// 把 new BrowerClient() 实例 绑定到top上
top.client = client;
};
1
2
3
4
复制代码Hub.prototype.getStackTop = function () {
// 获取最后一个
return this._stack[this._stack.length - 1];
};

小结2. 经过一系列的继承和初始化

再回过头来看 initAndBind函数

1
2
3
4
5
6
7
8
9
10
11
12
复制代码function initAndBind(clientClass, options) {
if (options.debug === true) {
logger.enable();
}
var client = new clientClass(options);
console.log(client, options, 'client, options');
var currentHub = getCurrentHub();
currentHub.bindClient(client);
console.log('currentHub', currentHub);
// 源代码
// getCurrentHub().bindClient(new clientClass(options));
}

最终会得到这样的Hub实例对象。笔者画了一张图表示,便于查看理解。

Hub 实例关系图

Hub 实例关系图

初始化完成后,再来看具体例子。
具体 captureMessage 函数的实现。

1
复制代码Sentry.captureMessage('Hello, 若川!');

captureMessage 函数

通过之前的阅读代码,知道会最终会调用Fetch接口,所以直接断点调试即可,得出如下调用栈。
接下来描述调用栈的主要流程。

captureMessage 断点调试图

captureMessage 断点调试图

调用栈主要流程:

captureMessage

1
2
3
4
5
6
7
8
9
10
11
12
13
14
复制代码function captureMessage(message, level) {
var syntheticException;
try {
throw new Error(message);
}
catch (exception) {
syntheticException = exception;
}
// 调用 callOnHub 方法
return callOnHub('captureMessage', message, level, {
originalException: message,
syntheticException: syntheticException,
});
}

=> callOnHub

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
复制代码/**
* This calls a function on the current hub.
* @param method function to call on hub.
* @param args to pass to function.
*/
function callOnHub(method) {
// 这里method 传进来的是 'captureMessage'
// 把method除外的其他参数放到args数组中
var args = [];
for (var _i = 1; _i < arguments.length; _i++) {
args[_i - 1] = arguments[_i];
}
// 获取当前控制中心 hub
var hub = getCurrentHub();
// 有这个方法 把args 数组展开,传递给 hub[method] 执行
if (hub && hub[method]) {
// tslint:disable-next-line:no-unsafe-any
return hub[method].apply(hub, __spread(args));
}
throw new Error("No hub defined or " + method + " was not found on the hub, please open a bug report.");
}

=> Hub.prototype.captureMessage

接着看Hub.prototype 上定义的 captureMessage 方法

1
2
3
4
5
6
7
复制代码Hub.prototype.captureMessage = function (message, level, hint) {
var eventId = (this._lastEventId = uuid4());
var finalHint = hint;
// 代码有删减
this._invokeClient('captureMessage', message, level, __assign({}, finalHint, { event_id: eventId }));
return eventId;
};

=> Hub.prototype._invokeClient

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
复制代码/**
* Internal helper function to call a method on the top client if it exists.
*
* @param method The method to call on the client.
* @param args Arguments to pass to the client function.
*/
Hub.prototype._invokeClient = function (method) {
// 同样:这里method 传进来的是 'captureMessage'
// 把method除外的其他参数放到args数组中
var _a;
var args = [];
for (var _i = 1; _i < arguments.length; _i++) {
args[_i - 1] = arguments[_i];
}
var top = this.getStackTop();
// 获取控制中心的 hub,调用客户端也就是new BrowerClient () 实例中继承自 BaseClient 的 captureMessage 方法
// 有这个方法 把args 数组展开,传递给 hub[method] 执行
if (top && top.client && top.client[method]) {
(_a = top.client)[method].apply(_a, __spread(args, [top.scope]));
}
};

=> BaseClient.prototype.captureMessage

1
2
3
4
5
6
7
8
9
10
11
12
13
复制代码BaseClient.prototype.captureMessage = function (message, level, hint, scope) {
var _this = this;
var eventId = hint && hint.event_id;
this._processing = true;
var promisedEvent = isPrimitive(message)
? this._getBackend().eventFromMessage("" + message, level, hint)
: this._getBackend().eventFromException(message, hint);
// 代码有删减
promisedEvent
.then(function (event) { return _this._processEvent(event, hint, scope); })
// 代码有删减
return eventId;
};

最后会调用 _processEvent 也就是

=> BaseClient.prototype._processEvent

这个函数最终会调用

1
复制代码_this._getBackend().sendEvent(finalEvent);

也就是

=> BaseBackend.prototype.sendEvent

1
2
3
4
5
复制代码BaseBackend.prototype.sendEvent = function (event) {
this._transport.sendEvent(event).then(null, function (reason) {
logger.error("Error while sending event: " + reason);
});
};

=> FetchTransport.prototype.sendEvent 最终发送了请求

FetchTransport.prototype.sendEvent

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
复制代码FetchTransport.prototype.sendEvent = function (event) {
var defaultOptions = {
body: JSON.stringify(event),
method: 'POST',
// Despite all stars in the sky saying that Edge supports old draft syntax, aka 'never', 'always', 'origin' and 'default
// https://caniuse.com/#feat=referrer-policy
// It doesn't. And it throw exception instead of ignoring this parameter...
// REF: https://github.com/getsentry/raven-js/issues/1233
referrerPolicy: (supportsReferrerPolicy() ? 'origin' : ''),
};
// global$2.fetch(this.url, defaultOptions) 使用fetch发送请求
return this._buffer.add(global$2.fetch(this.url, defaultOptions).then(function (response) { return ({
status: exports.Status.fromHttpCode(response.status),
}); }));
};

看完 Ajax 上报 主线,再看本文的另外一条主线 window.onerror 捕获。

window.onerror 和 window.onunhandledrejection 捕获 错误

例子:调用一个未申明的变量。

1
复制代码func();

Promise 不捕获错误

1
2
3
4
5
6
复制代码new Promise(() => {
fun();
})
.then(res => {
console.log('then');
})

captureEvent

调用栈主要流程:

window.onerror

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
复制代码GlobalHandlers.prototype._installGlobalOnErrorHandler = function () {
if (this._onErrorHandlerInstalled) {
return;
}
var self = this; // tslint:disable-line:no-this-assignment
// 浏览器中这里的 this._global. 就是window
this._oldOnErrorHandler = this._global.onerror;
this._global.onerror = function (msg, url, line, column, error) {
var currentHub = getCurrentHub();
// 代码有删减
currentHub.captureEvent(event, {
originalException: error,
});
if (self._oldOnErrorHandler) {
return self._oldOnErrorHandler.apply(this, arguments);
}
return false;
};
this._onErrorHandlerInstalled = true;
};

window.onunhandledrejection

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
复制代码GlobalHandlers.prototype._installGlobalOnUnhandledRejectionHandler = function () {
if (this._onUnhandledRejectionHandlerInstalled) {
return;
}
var self = this; // tslint:disable-line:no-this-assignment
this._oldOnUnhandledRejectionHandler = this._global.onunhandledrejection;
this._global.onunhandledrejection = function (e) {
// 代码有删减
var currentHub = getCurrentHub();
currentHub.captureEvent(event, {
originalException: error,
});
if (self._oldOnUnhandledRejectionHandler) {
return self._oldOnUnhandledRejectionHandler.apply(this, arguments);
}
return false;
};
this._onUnhandledRejectionHandlerInstalled = true;
};

共同点:都会调用currentHub.captureEvent

1
2
3
复制代码currentHub.captureEvent(event, {
originalException: error,
});

=> Hub.prototype.captureEvent

最终又是调用 _invokeClient ,调用流程跟 captureMessage 类似,这里就不再赘述。

1
复制代码this._invokeClient('captureEvent')

=> Hub.prototype._invokeClient

=> BaseClient.prototype.captureEvent

=> BaseClient.prototype._processEvent

=> BaseBackend.prototype.sendEvent

=> FetchTransport.prototype.sendEvent

最终同样是调用了这个函数发送了请求。

可谓是殊途同归,行文至此就基本已经结束,最后总结一下。

总结

Sentry-JavaScript源码高效利用了JS的原型链机制。可谓是惊艳,值得学习。

本文通过梳理前端错误监控知识、介绍sentry错误监控原理、sentry初始化、Ajax上报、window.onerror、window.onunhandledrejection几个方面来学习sentry的源码。还有很多细节和构造函数没有分析。

总共的构造函数(类)有25个,提到的主要有9个,分别是:Hub、BaseClient、BaseBackend、BaseTransport、FetchTransport、XHRTransport、BrowserBackend、BrowserClient、GlobalHandlers。

其他没有提到的分别是 SentryError、Logger、Memo、SyncPromise、PromiseBuffer、Span、Scope、Dsn、API、NoopTransport、FunctionToString、InboundFilters、TryCatch、Breadcrumbs、LinkedErrors、UserAgent。

这些构造函数(类)中还有很多值得学习,比如同步的Promise(SyncPromise)。
有兴趣的读者,可以看这一块官方仓库中采用typescript写的源码SyncPromise,也可以看打包后出来未压缩的代码。

读源码比较耗费时间,写文章记录下来更加费时间(比如写这篇文章跨度十几天…),但收获一般都比较大。

如果读者发现有不妥或可改善之处,再或者哪里没写明白的地方,欢迎评论指出。另外觉得写得不错,对您有些许帮助,可以点赞、评论、转发分享,也是对笔者的一种支持。万分感谢。

推荐阅读

知乎滴滴云:超详细!搭建一个前端错误监控系统

掘金BlackHole1:JavaScript集成Sentry

丁香园 开源的Sentry 小程序 SDKsentry-miniapp

sentry官网

sentry-javascript仓库

笔者往期文章

面试官问:JS的继承

面试官问:JS的this指向

面试官问:能否模拟实现JS的call和apply方法

面试官问:能否模拟实现JS的bind方法

面试官问:能否模拟实现JS的new操作符

前端使用puppeteer 爬虫生成《React.js 小书》PDF并合并

关于

作者:常以若川为名混迹于江湖。前端路上 | PPT爱好者 | 所知甚少,唯善学。

个人博客-若川,使用vuepress重构了,阅读体验可能更好些

掘金专栏,欢迎关注~

segmentfault前端视野专栏,欢迎关注~

知乎前端视野专栏,欢迎关注~

github blog,相关源码和资源都放在这里,求个star^_^~

欢迎加微信交流 微信公众号

可能比较有趣的微信公众号,长按扫码关注。欢迎加笔者微信ruochuan12(注明来源,基本来者不拒),拉您进【前端视野交流群】,长期交流学习~

若川视野

若川视野

本文使用 mdnice 排版

本文转载自: 掘金

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

1…850851852…956

开发者博客

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