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

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


  • 首页

  • 归档

  • 搜索

一张PDF了解JDK9 GC调优秘籍-附PDF下载 简介 O

发表于 2020-06-30

简介

今天我们讲讲JDK9中的JVM GC调优参数,JDK9中JVM的参数总共有2142个,其中正式的参数有659个。好像比JDK8中的参数要少一点。

为了方便大家的参考,特意将JDK9中的GC参数总结成了一张PDF,这个PDF比之前总结的JDK8的PDF在排版,颜色和内容准确性上面又有了非常大的提升,欢迎大家下载。

Oracle中的文档

今天这篇文章的内容都是从Oracle JDK9的官方文档中提炼出来的。对于里面的内容的真实性,我不能保证是100%正确的。

有人要问了,官网文档也会有错误?

这个问题要从两个方面说起,第一方面,任何人都会犯错误,虽然官网文档经过了编辑,校验核对然后才发布,但是总会有一些遗漏的地方。

第二,Oracle的文档是有专门的写文档的部门来专门编写的,写文档就是他们的工作,所以,这些文档并不是开发JDK的开发人员编写的,而是和开发JDK不相关的文档编写员编写的。

至于文档写完之后有没有JDK开发人员过目,大家可以自行脑补……

所以古人说得好,尽信书不如无书。

JDK9中JVM参数的变化

一代新人换旧人,长江后浪推前浪。由来只有新人笑 有谁听到旧人哭。

JDK9出现了,那么JDK8中的一些参数自然需要退伍了。

我们回想一下JDK9中有些什么变化呢?我总结一下有三个。

  1. 最大的变化就是引入了JPMS(Java Platform Module System)也就是Project Jigsaw。

模块化的本质就是将一个大型的项目拆分成为一个一个的模块,每个模块都是独立的单元,并且不同的模块之间可以互相引用和调用。

在module中会有元数据来描述该模块的信息和该模块与其他模块之间的关系。这些模块组合起来,构成了最后的运行程序。
2. 然后就是引入的Xlog日志服务,通过Xlog日志服务我们可以监控JVM中的事件,比如:GC,class loading,JPMS,heap,thread等等。
3. 最后就是将String中的底层存储从char数组换成了byte数组。

这三个变化中和JVM最相关的就是第二个Xlog日志服务。

废弃的JVM选项

-Xusealtsigs / -XX:+UseAltSigs

这两个选项在JDK9中被废弃了,如果你不知道也没关系,因为这两个选项是在Oracle Solaris中专有的。现在用Solaris服务器的人应该比较少了…..

不推荐(Deprecated)的JVM选项

下面这些选项是JVM已经不再推荐使用了,如果你使用的话也没问题,但是会有报警。

Deprecated表示这些选项会在未来被删除,我们应该尽量避免使用这些选项。

选项有很多,我们挑一些比较常见和重要的来给大家讲解一下。

-d32 / -d64

为什么这两个参数会被不推荐呢?因为现在的服务器性能已经非常非常的好了。

如果你的JDK是64位的,那么默认就启用了-server和-d64模式,32位的JDK现在应该很少见到了。

Oracle官方文档说只有Java HotSpot Server VM才有64位的模式。不知道是真是假,因为其他的VM我也没有用过,没有发言权。

-Xloggc:garbage-collection.log

因为JDK9中引入Xlog框架,所以之前的日志输出的参数都被替换成了新的Xlog格式:

比如上面的命令被替换成为 -Xlog:gc:garbage-collection.log

所以那些以Print开头的GC日志输出参数都是不推荐的。我们需要使用Xlog来替代。

同样的以Trace开头的运行时日志输出参数也是不推荐的,也可以使用Xlog来替代。

-XX:+UseConcMarkSweepGC / -XX:CMS*

CMS在JDK9中是不被推荐的,所以CMS开头的参数都不要用了。

-XX:+UseParNewGC

因为ParNewGC是和CMS一起使用的,所以CMS不推荐之后,ParNewGC也是不推荐使用的。

-XX:MaxPermSize=size / -XX:PermSize=size

JDK8中,Prem区已经被移到了Metaspace,所以上面的参数可以被下面的替代:

-XX:MaxMetaspaceSize=size / -XX:MetaspaceSize=size

被删除的JVM参数

-Xincgc

增量GC在JDK9中被删除了。

-Xmaxjitcodesize=size JIT中最大的code cache大小被替换成 -XX:ReservedCodeCacheSize。

还有其他的一些CMS的参数。

JDK9的新特性Application Class Data Sharing

AppCDS的全称是Application Class-Data Sharing。主要是用来在不同的JVM中共享Class-Data信息,从而提升应用程序的启动速度。

通常来说,如果要执行class字节码,JVM需要执行下面的一些步骤:给定一个类的名字,JVM需要从磁盘上面找到这个文件,加载,并验证字节码,最后将它加载进来。

如果JVM启动的时候需要加载成百上千个class,那么需要的就不是一个小数目了。

对于打包好的jar包来说,只要jar的内容不变,那么jar包中的类的数据始终是相同的。JVM在启动时候每次都会运行相同的加载步骤。

AppCDS的作用就是将这些能够共享的数据归类成一个存储文件,在不同的JVM中共享。

下面是AppCDS的大概工作流程:

  1. 选择要归档的class,并创建一个class的列表,用在归档中。( -XX:DumpLoadedClassList)
  2. 创建归档文件(-Xshare:dump和-XX:SharedArchiveFile)
  3. 使用归档文件(-Xshare:on 和 -XX:SharedArchiveFile)

相应的VM参数如下:

JDK9的新特性Xlog

在java程序中,我们通过日志来定位和发现项目中可能出现的问题。在现代java项目中,我们使用log4j或者slf4j,Logback等日志记录框架来处理日志问题。

JVM是java程序运行的基础,JVM中各种事件比如:GC,class loading,JPMS,heap,thread等等其实都可以有日志来记录。通过这些日志,我们可以监控JVM中的事件,并可以依次来对java应用程序进行调优。

在JDK9中引入的Xlog日志服务就是为这个目的而创建的。

通过xlog,JDK将JVM中的各种事件统一起来,以统一的形式对外输出。通过tag参数来区分子系统,通过log level来区分事件的紧急性,通过logging output来配置输出的地址。

在JDK9之后,之前的Print*参数都被Xlog所代替了。

我们看下常用的Xlog和GC日志参数:

JDK9中的G1参数

作为JDK9中的默认垃圾回收器G1,对G1的调优是必不可少的。下面是G1的参数:

JDK9中的通用VM参数

下面是通用的VM参数:

JDK9中的通用GC参数

下面是JDK9中的通用GC参数:

JDK9中的内存调整参数

下面是JDK9中的内存调整参数:

总结

千言万语不如一张PDF。我把JDK9的GC参数总结成了一张PDF,下面是PDF的下载链接。

JDK9GC-cheatsheet.pdf

欢迎大家下载。

本文作者:flydean程序那些事

本文链接:www.flydean.com/jdk9-gc-che…

本文来源:flydean的博客

欢迎关注我的公众号:程序那些事,更多精彩等着您!

本文转载自: 掘金

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

使用 Prometheus-Operator 监控 Cali

发表于 2020-06-29

原文链接:fuckcloudnative.io/posts/monit…

Calico 中最核心的组件就是 Felix,它负责设置路由表和 ACL 规则等,以便为该主机上的 endpoints 资源正常运行提供所需的网络连接。同时它还负责提供有关网络健康状况的数据(例如,报告配置其主机时发生的错误和问题),这些数据会被写入 etcd,以使其对网络中的其他组件和操作人员可见。

由此可见,对于我们的监控来说,监控 Calico 的核心便是监控 Felix,Felix 就相当于 Calico 的大脑。本文将学习如何使用 Prometheus-Operator 来监控 Calico。

本文不会涉及到 Calico 和 Prometheus-Operator 的部署细节,如果不知道如何部署,请查阅官方文档和相关博客。

  1. 配置 Calico 以启用指标

默认情况下 Felix 的指标是被禁用的,必须通过命令行管理工具 calicoctl 手动更改 Felix 配置才能开启,需要提前配置好命令行管理工具。

本文使用的 Calico 版本是 v3.15.0,其他版本类似。先下载管理工具:

1
2
bash复制代码$ wget https://github.com/projectcalico/calicoctl/releases/download/v3.15.0/calicoctl -O /usr/local/bin/calicoctl
$ chmod +x /usr/local/bin/calicoctl

接下来需要设置 calicoctl 配置文件(默认是 /etc/calico/calicoctl.cfg)。如果你的 Calico 后端存储使用的是 Kubernetes API,那么配置文件内容如下:

1
2
3
4
5
6
yaml复制代码apiVersion: projectcalico.org/v3
kind: CalicoAPIConfig
metadata:
spec:
datastoreType: "kubernetes"
kubeconfig: "/root/.kube/config"

如果 Calico 后端存储使用的是 etcd,那么配置文件内容如下:

1
2
3
4
5
6
7
8
9
yaml复制代码apiVersion: projectcalico.org/v3
kind: CalicoAPIConfig
metadata:
spec:
datastoreType: "etcdv3"
etcdEndpoints: https://192.168.57.51:2379,https://192.168.57.52:2379,https://192.168.57.53:2379
etcdKeyFile: /opt/kubernetes/ssl/server-key.pem
etcdCertFile: /opt/kubernetes/ssl/server.pem
etcdCACertFile: /opt/kubernetes/ssl/ca.pem

你需要将其中的证书路径换成你的 etcd 证书路径。

配置好了 calicoctl 之后就可以查看或修改 Calico 的配置了,先来看一下默认的 Felix 配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
bash复制代码$ calicoctl get felixConfiguration default -o yaml

apiVersion: projectcalico.org/v3
kind: FelixConfiguration
metadata:
creationTimestamp: "2020-06-25T14:37:28Z"
name: default
resourceVersion: "269031"
uid: 52146c95-ff97-40a9-9ba7-7c3b4dd3ba57
spec:
bpfLogLevel: ""
ipipEnabled: true
logSeverityScreen: Info
reportingInterval: 0s

可以看到默认的配置中没有启用指标,需要手动修改配置,命令如下:

1
bash复制代码$ calicoctl patch felixConfiguration default  --patch '{"spec":{"prometheusMetricsEnabled": true}}'

Felix 暴露指标的端口是 9091,可通过检查监听端口来验证是否开启指标:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
bash复制代码$ ss -tulnp|grep 9091
tcp LISTEN 0 4096 [::]:9091 [::]:* users:(("calico-node",pid=13761,fd=9))

$ curl -s http://localhost:9091/metrics
# HELP felix_active_local_endpoints Number of active endpoints on this host.
# TYPE felix_active_local_endpoints gauge
felix_active_local_endpoints 1
# HELP felix_active_local_policies Number of active policies on this host.
# TYPE felix_active_local_policies gauge
felix_active_local_policies 0
# HELP felix_active_local_selectors Number of active selectors on this host.
# TYPE felix_active_local_selectors gauge
felix_active_local_selectors 0
...
  1. Prometheus 采集 Felix 指标

启用了 Felix 的指标后,就可以通过 Prometheus-Operator 来采集指标数据了。Prometheus-Operator 在部署时会创建 Prometheus、PodMonitor、ServiceMonitor、AlertManager 和 PrometheusRule 这 5 个 CRD 资源对象,然后会一直监控并维持这 5 个资源对象的状态。其中 Prometheus 这个资源对象就是对 Prometheus Server 的抽象。而 PodMonitor 和 ServiceMonitor 就是 exporter 的各种抽象,是用来提供专门提供指标数据接口的工具,Prometheus 就是通过 PodMonitor 和 ServiceMonitor 提供的指标数据接口去 pull 数据的。

ServiceMonitor 要求被监控的服务必须有对应的 Service,而 PodMonitor 则不需要,本文选择使用 PodMonitor 来采集 Felix 的指标。

PodMonitor 虽然不需要应用创建相应的 Service,但必须在 Pod 中指定指标的端口和名称,因此需要先修改 DaemonSet calico-node 的配置,指定端口和名称。先用以下命令打开 DaemonSet calico-node 的配置:

1
bash复制代码$ kubectl -n kube-system edit ds calico-node

然后在线修改,在 spec.template.sepc.containers 中加入以下内容:

1
2
3
4
yaml复制代码        ports:
- containerPort: 9091
name: http-metrics
protocol: TCP

创建 Pod 对应的 PodMonitor:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
bash复制代码# prometheus-podMonitorCalico.yaml
apiVersion: monitoring.coreos.com/v1
kind: PodMonitor
metadata:
labels:
k8s-app: calico-node
name: felix
namespace: monitoring
spec:
podMetricsEndpoints:
- interval: 15s
path: /metrics
port: http-metrics
namespaceSelector:
matchNames:
- kube-system
selector:
matchLabels:
k8s-app: calico-node
1
bash复制代码$ kubectl apply -f prometheus-podMonitorCalico.yaml

有几个参数需要注意:

  • PodMonitor 的 name 最终会反应到 Prometheus 的配置中,作为 job_name。
  • podMetricsEndpoints.port 需要和被监控的 Pod 中的 ports.name 相同,此处为 http-metrics。
  • namespaceSelector.matchNames 需要和被监控的 Pod 所在的 namespace 相同,此处为 kube-system。
  • selector.matchLabels 的标签必须和被监控的 Pod 中能唯一标明身份的标签对应。

最终 Prometheus-Operator 会根据 PodMonitor 来修改 Prometheus 的配置文件,以实现对相关的 Pod 进行监控。可以打开 Prometheus 的 UI 查看监控目标:

注意 Labels 中有 pod="calico-node-xxx",表明监控的是 Pod。

  1. 可视化监控指标

采集完指标之后,就可以通过 Grafana 的仪表盘来展示监控指标了。Prometheus-Operator 中部署的 Grafana 无法实时修改仪表盘的配置(必须提前将仪表盘的 json 文件挂载到 Grafana Pod 中),而且也不是最新版(7.0 以上版本),所以我选择删除 Prometheus-Operator 自带的 Grafana,自行部署 helm 仓库中的 Grafana。先进入 kube-prometheus 项目的 manifests 目录,然后将 Grafana 相关的部署清单都移到同一个目录下,再删除 Grafana:

1
2
3
4
bash复制代码$ cd kube-prometheus/manifests
$ mkdir grafana
$ mv grafana-* grafana/
$ kubectl delete -f grafana/

然后通过 helm 部署最新的 Grafana:

1
bash复制代码$ helm install grafana stable/grafana -n monitoring

访问 Grafana 的密码保存在 Secret 中,可以通过以下命令查看:

1
2
3
4
5
6
7
8
9
10
bash复制代码$ kubectl -n monitoring get secret grafana -o yaml

apiVersion: v1
data:
admin-password: MnpoV3VaMGd1b3R3TDY5d3JwOXlIak4yZ3B2cTU1RFNKcVY0RWZsUw==
admin-user: YWRtaW4=
ldap-toml: ""
kind: Secret
metadata:
...

对密码进行解密:

1
bash复制代码$ echo -n "MnpoV3VaMGd1b3R3TDY5d3JwOXlIak4yZ3B2cTU1RFNKcVY0RWZsUw=="|base64 -d

解密出来的信息就是访问密码。用户名是 admin。通过用户名和密码登录 Grafana 的 UI:

添加 Prometheus-Operator 的数据源:

Calico 官方没有单独 dashboard json,而是将其放到了 ConfigMap 中,我们需要从中提取需要的 json,提取出 felix-dashboard.json 的内容,然后将其中的 datasource 值替换为 prometheus。你可以用 sed 替换,也可以用编辑器,大多数编辑器都有全局替换的功能。如果你实在不知道如何提取,可以使用我提取好的 json:

修改完了之后,将 json 内容导入到 Grafana:

最后得到的 Felix 仪表盘如下图所示:

如果你对我截图中 Grafana 的主题配色很感兴趣,可以参考这篇文章:Grafana 自定义主题。


Kubernetes 1.18.2 1.17.5 1.16.9 1.15.12离线安装包发布地址store.lameleg.com ,欢迎体验。 使用了最新的sealos v3.3.6版本。 作了主机名解析配置优化,lvscare 挂载/lib/module解决开机启动ipvs加载问题, 修复lvscare社区netlink与3.10内核不兼容问题,sealos生成百年证书等特性。更多特性 github.com/fanux/sealo… 。欢迎扫描下方的二维码加入钉钉群 ,钉钉群已经集成sealos的机器人实时可以看到sealos的动态。

本文转载自: 掘金

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

阿里技术专家详解DDD系列 第二弹 - 应用架构 案例分析

发表于 2020-06-29

作者|殷浩

出品|阿里巴巴新零售淘系技术

DDD 详解第一弹- Domain Primitive

架构这个词源于英文里的“Architecture“,源头是土木工程里的“建筑”和“结构”,而架构里的”架“同时又包含了”架子“(scaffolding)的含义,意指能快速搭建起来的固定结构。而今天的应用架构,意指软件系统中固定不变的代码结构、设计模式、规范和组件间的通信方式。在应用开发中架构之所以是最重要的第一步,因为一个好的架构能让系统安全、稳定、快速迭代。在一个团队内通过规定一个固定的架构设计,可以让团队内能力参差不齐的同学们都能有一个统一的开发规范,降低沟通成本,提升效率和代码质量。

在做架构设计时,一个好的架构应该需要实现以下几个目标:

1、独立于框架:架构不应该依赖某个外部的库或框架,不应该被框架的结构所束缚。

2、独立于UI:前台展示的样式可能会随时发生变化(今天可能是网页、明天可能变成console、后天是独立app),但是底层架构不应该随之而变化。

3、独立于底层数据源:无论今天你用MySQL、Oracle还是MongoDB、CouchDB,甚至使用文件系统,软件架构不应该因为不同的底层数据储存方式而产生巨大改变。

4、独立于外部依赖:无论外部依赖如何变更、升级,业务的核心逻辑不应该随之而大幅变化。

5、可测试:无论外部依赖了什么数据库、硬件、UI或者服务,业务的逻辑应该都能够快速被验证正确性。

这就好像是建筑中的楼宇,一个好的楼宇,无论内部承载了什么人、有什么样的活动、还是外部有什么风雨,一栋楼都应该屹立不倒,而且可以确保它不会倒。但是今天我们在做业务研发时,更多的会去关注一些宏观的架构,比如SOA架构、微服务架构,而忽略了应用内部的架构设计,很容易导致代码逻辑混乱,很难维护,容易产生bug而且很难发现。今天,我希望能够通过案例的分析和重构,来推演出一套高质量的DDD架构。

案例分析

我们先看一个简单的案例需求如下:

用户可以通过银行网页转账给另一个账号,支持跨币种转账。

同时因为监管和对账需求,需要记录本次转账活动。

拿到这个需求之后,一个开发可能会经历一些技术选型,最终可能拆解需求如下:

1、从MySql数据库中找到转出和转入的账户,选择用 MyBatis 的 mapper 实现 DAO;2、从 Yahoo(或其他渠道)提供的汇率服务获取转账的汇率信息(底层是 http 开放接口);

3、计算需要转出的金额,确保账户有足够余额,并且没超出每日转账上限;

4、实现转入和转出操作,扣除手续费,保存数据库;

5、发送 Kafka 审计消息,以便审计和对账用;

而一个简单的代码实现如下:

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
复制代码public class TransferController {

private TransferService transferService;

public Result<Boolean> transfer(String targetAccountNumber, BigDecimal amount, HttpSession session) {
Long userId = (Long) session.getAttribute("userId");
return transferService.transfer(userId, targetAccountNumber, amount, "CNY");
}
}

public class TransferServiceImpl implements TransferService {

private static final String TOPIC_AUDIT_LOG = "TOPIC_AUDIT_LOG";
private AccountMapper accountDAO;
private KafkaTemplate<String, String> kafkaTemplate;
private YahooForexService yahooForex;

@Override
public Result<Boolean> transfer(Long sourceUserId, String targetAccountNumber, BigDecimal targetAmount, String targetCurrency) {
// 1. 从数据库读取数据,忽略所有校验逻辑如账号是否存在等
AccountDO sourceAccountDO = accountDAO.selectByUserId(sourceUserId);
AccountDO targetAccountDO = accountDAO.selectByAccountNumber(targetAccountNumber);

// 2. 业务参数校验
if (!targetAccountDO.getCurrency().equals(targetCurrency)) {
throw new InvalidCurrencyException();
}

// 3. 获取外部数据,并且包含一定的业务逻辑
// exchange rate = 1 source currency = X target currency
BigDecimal exchangeRate = BigDecimal.ONE;
if (sourceAccountDO.getCurrency().equals(targetCurrency)) {
exchangeRate = yahooForex.getExchangeRate(sourceAccountDO.getCurrency(), targetCurrency);
}
BigDecimal sourceAmount = targetAmount.divide(exchangeRate, RoundingMode.DOWN);

// 4. 业务参数校验
if (sourceAccountDO.getAvailable().compareTo(sourceAmount) < 0) {
throw new InsufficientFundsException();
}

if (sourceAccountDO.getDailyLimit().compareTo(sourceAmount) < 0) {
throw new DailyLimitExceededException();
}

// 5. 计算新值,并且更新字段
BigDecimal newSource = sourceAccountDO.getAvailable().subtract(sourceAmount);
BigDecimal newTarget = targetAccountDO.getAvailable().add(targetAmount);
sourceAccountDO.setAvailable(newSource);
targetAccountDO.setAvailable(newTarget);

// 6. 更新到数据库
accountDAO.update(sourceAccountDO);
accountDAO.update(targetAccountDO);

// 7. 发送审计消息
String message = sourceUserId + "," + targetAccountNumber + "," + targetAmount + "," + targetCurrency;
kafkaTemplate.send(TOPIC_AUDIT_LOG, message);

return Result.success(true);
}

}

我们可以看到,一段业务代码里经常包含了参数校验、数据读取存储、业务计算、调用外部服务、发送消息等多种逻辑。在这个案例里虽然是写在了同一个方法里,在真实代码中经常会被拆分成多个子方法,但实际效果是一样的,而在我们日常的工作中,绝大部分代码都或多或少的接近于此类结构。在Martin Fowler的 P of EAA书中,这种很常见的代码样式被叫做Transaction Script(事务脚本)。虽然这种类似于脚本的写法在功能上没有什么问题,但是长久来看,他有以下几个很大的问题:可维护性差、可扩展性差、可测试性差。

问题1-可维护性能差

一个应用最大的成本一般都不是来自于开发阶段,而是应用整个生命周期的总维护成本,所以代码的可维护性代表了最终成本。

可维护性 = 当依赖变化时,有多少代码需要随之改变

参考以上的案例代码,事务脚本类的代码很难维护因为以下几点:

1、数据结构的不稳定性:AccountDO类是一个纯数据结构,映射了数据库中的一个表。这里的问题是数据库的表结构和设计是应用的外部依赖,长远来看都有可能会改变,比如数据库要做Sharding,或者换一个表设计,或者改变字段名。

2、依赖库的升级:AccountMapper依赖MyBatis的实现,如果MyBatis未来升级版本,可能会造成用法的不同(可以参考iBatis升级到基于注解的MyBatis的迁移成本)。同样的,如果未来换一个ORM体系,迁移成本也是巨大的。

3、第三方服务依赖的不确定性:第三方服务,比如Yahoo的汇率服务未来很有可能会有变化:轻则API签名变化,重则服务不可用需要寻找其他可替代的服务。在这些情况下改造和迁移成本都是巨大的。同时,外部依赖的兜底、限流、熔断等方案都需要随之改变。

4、第三方服务API的接口变化:YahooForexService.getExchangeRate返回的结果是小数点还是百分比?入参是(source, target)还是(target, source)?谁能保证未来接口不会改变?如果改变了,核心的金额计算逻辑必须跟着改,否则会造成资损。

5、中间件更换:今天我们用Kafka发消息,明天如果要上阿里云用RocketMQ该怎么办?后天如果消息的序列化方式从String改为Binary该怎么办?如果需要消息分片该怎么改?

我们发现案例里的代码对于任何外部依赖的改变都会有比较大的影响。如果你的应用里有大量的此类代码,你每一天的时间基本上会被各种库升级、依赖服务升级、中间件升级、jar包冲突占满,最终这个应用变成了一个不敢升级、不敢部署、不敢写新功能、并且随时会爆发的炸弹,终有一天会给你带来惊喜。

问题2-可拓展性差

事务脚本式代码的第二大缺陷是:虽然写单个用例的代码非常高效简单,但是当用例多起来时,其扩展性会变得越来越差。

可扩展性 = 做新需求或改逻辑时,需要新增/修改多少代码

参考以上的代码,如果今天需要增加一个跨行转账的能力,你会发现基本上需要重新开发,基本上没有任何的可复用性:

1、数据来源被固定、数据格式不兼容:原有的AccountDO是从本地获取的,而跨行转账的数据可能需要从一个第三方服务获取,而服务之间数据格式不太可能是兼容的,导致从数据校验、数据读写、到异常处理、金额计算等逻辑都要重写。

2、业务逻辑无法复用:数据格式不兼容的问题会导致核心业务逻辑无法复用。每个用例都是特殊逻辑的后果是最终会造成大量的if-else语句,而这种分支多的逻辑会让分析代码非常困难,容易错过边界情况,造成bug。

3、逻辑和数据存储的相互依赖:当业务逻辑增加变得越来越复杂时,新加入的逻辑很有可能需要对数据库schema或消息格式做变更。而变更了数据格式后会导致原有的其他逻辑需要一起跟着动。在最极端的场景下,一个新功能的增加会导致所有原有功能的重构,成本巨大。

在事务脚本式的架构下,一般做第一个需求都非常的快,但是做第N个需求时需要的时间很有可能是呈指数级上升的,绝大部分时间花费在老功能的重构和兼容上,最终你的创新速度会跌为0,促使老应用被推翻重构。

问题3-可测试性能差

除了部分工具类、框架类和中间件类的代码有比较高的测试覆盖之外,我们在日常工作中很难看到业务代码有比较好的测试覆盖,而绝大部分的上线前的测试属于人肉的“集成测试”。低测试率导致我们对代码质量很难有把控,容易错过边界条件,异常case只有线上爆发了才被动发现。而低测试覆盖率的主要原因是业务代码的可测试性比较差。

可测试性 = 运行每个测试用例所花费的时间 * 每个需求所需要增加的测试用例数量

参考以上的一段代码,这种代码有极低的可测试性:

1、设施搭建困难:当代码中强依赖了数据库、第三方服务、中间件等外部依赖之后,想要完整跑通一个测试用例需要确保所有依赖都能跑起来,这个在项目早期是及其困难的。在项目后期也会由于各种系统的不稳定性而导致测试无法通过。

2、运行耗时长:大多数的外部依赖调用都是I/O密集型,如跨网络调用、磁盘调用等,而这种I/O调用在测试时需要耗时很久。另一个经常依赖的是笨重的框架如Spring,启动Spring容器通常需要很久。当一个测试用例需要花超过10秒钟才能跑通时,绝大部分开发都不会很频繁的测试。

3、耦合度高:假如一段脚本中有A、B、C三个子步骤,而每个步骤有N个可能的状态,当多个子步骤耦合度高时,为了完整覆盖所有用例,最多需要有N * N * N个测试用例。当耦合的子步骤越多时,需要的测试用例呈指数级增长。

在事务脚本模式下,当测试用例复杂度远大于真实代码复杂度,当运行测试用例的耗时超出人肉测试时,绝大部分人会选择不写完整的测试覆盖,而这种情况通常就是bug很难被早点发现的原因。

总结分析

我们重新来分析一下为什么以上的问题会出现?因为以上的代码违背了至少以下几个软件设计的原则:

1、单一性原则(Single Responsibility Principle):单一性原则要求一个对象/类应该只有一个变更的原因。但是在这个案例里,代码可能会因为任意一个外部依赖或计算逻辑的改变而改变。

2、依赖反转原则(Dependency Inversion Principle):依赖反转原则要求在代码中依赖抽象,而不是具体的实现。在这个案例里外部依赖都是具体的实现,比如YahooForexService虽然是一个接口类,但是它对应的是依赖了Yahoo提供的具体服务,所以也算是依赖了实现。同样的KafkaTemplate、MyBatis的DAO实现都属于具体实现。

3、开放封闭原则(Open Closed Principle):开放封闭原则指开放扩展,但是封闭修改。在这个案例里的金额计算属于可能会被修改的代码,这个时候该逻辑应该需要被包装成为不可修改的计算类,新功能通过计算类的拓展实现。

我们需要对代码重构才能解决这些问题。

重构方案

在重构之前,我们先画一张流程图,描述当前代码在做的每个步骤:

这是一个传统的三层分层结构:UI层、业务层、和基础设施层。上层对于下层有直接的依赖关系,导致耦合度过高。在业务层中对于下层的基础设施有强依赖,耦合度高。我们需要对这张图上的每个节点做抽象和整理,来降低对外部依赖的耦合度。

抽象数据存储层

第一步常见的操作是将Data Access层做抽象,降低系统对数据库的直接依赖。具体的方法如下:

1、新建Account实体对象:一个实体(Entity)是拥有ID的域对象,除了拥有数据之外,同时拥有行为。Entity和数据库储存格式无关,在设计中要以该领域的通用严谨语言(Ubiquitous Language)为依据。

2、新建对象储存接口类AccountRepository:Repository只负责Entity对象的存储和读取,而Repository的实现类完成数据库存储的细节。通过加入Repository接口,底层的数据库连接可以通过不同的实现类而替换。

具体的简单代码实现如下:

Account实体类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
复制代码@Data
public class Account {
private AccountId id;
private AccountNumber accountNumber;
private UserId userId;
private Money available;
private Money dailyLimit;

public void withdraw(Money money) {
// 转出
}

public void deposit(Money money) {
// 转入
}
}

和AccountRepository及MyBatis实现类:

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
复制代码public interface AccountRepository {
Account find(AccountId id);
Account find(AccountNumber accountNumber);
Account find(UserId userId);
Account save(Account account);
}

public class AccountRepositoryImpl implements AccountRepository {

@Autowired
private AccountMapper accountDAO;

@Autowired
private AccountBuilder accountBuilder;

@Override
public Account find(AccountId id) {
AccountDO accountDO = accountDAO.selectById(id.getValue());
return accountBuilder.toAccount(accountDO);
}

@Override
public Account find(AccountNumber accountNumber) {
AccountDO accountDO = accountDAO.selectByAccountNumber(accountNumber.getValue());
return accountBuilder.toAccount(accountDO);
}

@Override
public Account find(UserId userId) {
AccountDO accountDO = accountDAO.selectByUserId(userId.getId());
return accountBuilder.toAccount(accountDO);
}

@Override
public Account save(Account account) {
AccountDO accountDO = accountBuilder.fromAccount(account);
if (accountDO.getId() == null) {
accountDAO.insert(accountDO);
} else {
accountDAO.update(accountDO);
}
return accountBuilder.toAccount(accountDO);
}

}

Account实体类和AccountDO数据类的对比如下:

1、Data Object数据类:AccountDO是单纯的和数据库表的映射关系,每个字段对应数据库表的一个column,这种对象叫Data Object。DO只有数据,没有行为。AccountDO的作用是对数据库做快速映射,避免直接在代码里写SQL。无论你用的是MyBatis还是Hibernate这种ORM,从数据库来的都应该先直接映射到DO上,但是代码里应该完全避免直接操作 DO。

2、Entity实体类:Account 是基于领域逻辑的实体类,它的字段和数据库储存不需要有必然的联系。Entity包含数据,同时也应该包含行为。在 Account 里,字段也不仅仅是String等基础类型,而应该尽可能用上一讲的 Domain Primitive 代替,可以避免大量的校验代码。

DAO 和 Repository 类的对比如下:

1、DAO对应的是一个特定的数据库类型的操作,相当于SQL的封装。所有操作的对象都是DO类,所有接口都可以根据数据库实现的不同而改变。比如,insert 和 update 属于数据库专属的操作。

2、Repository对应的是Entity对象读取储存的抽象,在接口层面做统一,不关注底层实现。比如,通过 save 保存一个Entity对象,但至于具体是 insert 还是 update 并不关心。Repository的具体实现类通过调用DAO来实现各种操作,通过Builder/Factory对象实现AccountDO 到 Account之间的转化

Repository和Entity

1、通过Account对象,避免了其他业务逻辑代码和数据库的直接耦合,避免了当数据库字段变化时,大量业务逻辑也跟着变的问题。

2、通过Repository,改变业务代码的思维方式,让业务逻辑不再面向数据库编程,而是面向领域模型编程。

Account属于一个完整的内存中对象,可以比较容易的做完整的测试覆盖,包含其行为。

Repository作为一个接口类,可以比较容易的实现Mock或Stub,可以很容易测试。

AccountRepositoryImpl实现类,由于其职责被单一出来,只需要关注Account到AccountDO的映射关系和Repository方法到DAO方法之间的映射关系,相对于来说更容易测试。

抽象第三方服务

类似对于数据库的抽象,所有第三方服务也需要通过抽象解决第三方服务不可控,入参出参强耦合的问题。在这个例子里我们抽象出 ExchangeRateService 的服务,和一个ExchangeRate的Domain Primitive类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
复制代码public interface ExchangeRateService {
ExchangeRate getExchangeRate(Currency source, Currency target);
}

public class ExchangeRateServiceImpl implements ExchangeRateService {

@Autowired
private YahooForexService yahooForexService;

@Override
public ExchangeRate getExchangeRate(Currency source, Currency target) {
if (source.equals(target)) {
return new ExchangeRate(BigDecimal.ONE, source, target);
}
BigDecimal forex = yahooForexService.getExchangeRate(source.getValue(), target.getValue());
return new ExchangeRate(forex, source, target);
}

防腐层(ACL)

这种常见的设计模式叫做Anti-Corruption Layer(防腐层或ACL)。很多时候我们的系统会去依赖其他的系统,而被依赖的系统可能包含不合理的数据结构、API、协议或技术实现,如果对外部系统强依赖,会导致我们的系统被”腐蚀“。这个时候,通过在系统间加入一个防腐层,能够有效的隔离外部依赖和内部逻辑,无论外部如何变更,内部代码可以尽可能的保持不变。

ACL 不仅仅只是多了一层调用,在实际开发中ACL能够提供更多强大的功能:

1、适配器:很多时候外部依赖的数据、接口和协议并不符合内部规范,通过适配器模式,可以将数据转化逻辑封装到ACL内部,降低对业务代码的侵入。在这个案例里,我们通过封装了ExchangeRate和Currency对象,转化了对方的入参和出参,让入参出参更符合我们的标准。

2、缓存:对于频繁调用且数据变更不频繁的外部依赖,通过在ACL里嵌入缓存逻辑,能够有效的降低对于外部依赖的请求压力。同时,很多时候缓存逻辑是写在业务代码里的,通过将缓存逻辑嵌入ACL,能够降低业务代码的复杂度。

3、兜底:如果外部依赖的稳定性较差,一个能够有效提升我们系统稳定性的策略是通过ACL起到兜底的作用,比如当外部依赖出问题后,返回最近一次成功的缓存或业务兜底数据。这种兜底逻辑一般都比较复杂,如果散落在核心业务代码中会很难维护,通过集中在ACL中,更加容易被测试和修改。

4、易于测试:类似于之前的Repository,ACL的接口类能够很容易的实现Mock或Stub,以便于单元测试。

5、功能开关:有些时候我们希望能在某些场景下开放或关闭某个接口的功能,或者让某个接口返回一个特定的值,我们可以在ACL配置功能开关来实现,而不会对真实业务代码造成影响。同时,使用功能开关也能让我们容易的实现Monkey测试,而不需要真正物理性的关闭外部依赖。

抽象中间件

类似于2.2的第三方服务的抽象,对各种中间件的抽象的目的是让业务代码不再依赖中间件的实现逻辑。因为中间件通常需要有通用型,中间件的接口通常是String或Byte[] 类型的,导致序列化/反序列化逻辑通常和业务逻辑混杂在一起,造成胶水代码。通过中间件的ACL抽象,减少重复胶水代码。

在这个案例里,我们通过封装一个抽象的AuditMessageProducer和AuditMessage DP对象,实现对底层kafka实现的隔离:

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
复制代码@Value
@AllArgsConstructor
public class AuditMessage {

private UserId userId;
private AccountNumber source;
private AccountNumber target;
private Money money;
private Date date;

public String serialize() {
return userId + "," + source + "," + target + "," + money + "," + date;
}

public static AuditMessage deserialize(String value) {
// todo
return null;
}
}

public interface AuditMessageProducer {
SendResult send(AuditMessage message);
}

public class AuditMessageProducerImpl implements AuditMessageProducer {

private static final String TOPIC_AUDIT_LOG = "TOPIC_AUDIT_LOG";

@Autowired
private KafkaTemplate<String, String> kafkaTemplate;

@Override
public SendResult send(AuditMessage message) {
String messageBody = message.serialize();
kafkaTemplate.send(TOPIC_AUDIT_LOG, messageBody);
return SendResult.success();
}
}

具体的分析和2.2类似,在此略过。

封装业务逻辑

在这个案例里,有很多业务逻辑是跟外部依赖的代码混合的,包括金额计算、账户余额的校验、转账限制、金额增减等。这种逻辑混淆导致了核心计算逻辑无法被有效的测试和复用。在这里,我们的解法是通过Entity、Domain Primitive和Domain Service封装所有的业务逻辑:

用Domain Primitive封装跟实体无关的无状态计算逻辑

在这个案例里使用ExchangeRate来封装汇率计算逻辑:

1
2
3
4
5
6
7
8
9
复制代码BigDecimal exchangeRate = BigDecimal.ONE;
if (sourceAccountDO.getCurrency().equals(targetCurrency)) {
exchangeRate = yahooForex.getExchangeRate(sourceAccountDO.getCurrency(), targetCurrency);
}
BigDecimal sourceAmount = targetAmount.divide(exchangeRate, RoundingMode.DOWN);
变为:

ExchangeRate exchangeRate = exchangeRateService.getExchangeRate(sourceAccount.getCurrency(), targetMoney.getCurrency());
Money sourceMoney = exchangeRate.exchangeTo(targetMoney);

用Entity封装单对象的有状态的行为,包括业务校验

用Account实体类封装所有Account的行为,包括业务校验如下:

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
复制代码@Data
public class Account {

private AccountId id;
private AccountNumber accountNumber;
private UserId userId;
private Money available;
private Money dailyLimit;

public Currency getCurrency() {
return this.available.getCurrency();
}

// 转入
public void deposit(Money money) {
if (!this.getCurrency().equals(money.getCurrency())) {
throw new InvalidCurrencyException();
}
this.available = this.available.add(money);
}

// 转出
public void withdraw(Money money) {
if (this.available.compareTo(money) < 0) {
throw new InsufficientFundsException();
}
if (this.dailyLimit.compareTo(money) < 0) {
throw new DailyLimitExceededException();
}
this.available = this.available.subtract(money);
}
}

原有的业务代码则可以简化为:

1
2
复制代码sourceAccount.deposit(sourceMoney);
targetAccount.withdraw(targetMoney);

用Domain Service封装多对象逻辑

在这个案例里,我们发现这两个账号的转出和转入实际上是一体的,也就是说这种行为应该被封装到一个对象中去。特别是考虑到未来这个逻辑可能会产生变化:比如增加一个扣手续费的逻辑。这个时候在原有的TransferService中做并不合适,在任何一个Entity或者Domain Primitive里也不合适,需要有一个新的类去包含跨域对象的行为。这种对象叫做Domain Service。

我们创建一个AccountTransferService的类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
复制代码public interface AccountTransferService {
void transfer(Account sourceAccount, Account targetAccount, Money targetMoney, ExchangeRate exchangeRate);
}

public class AccountTransferServiceImpl implements AccountTransferService {
private ExchangeRateService exchangeRateService;

@Override
public void transfer(Account sourceAccount, Account targetAccount, Money targetMoney, ExchangeRate exchangeRate) {
Money sourceMoney = exchangeRate.exchangeTo(targetMoney);
sourceAccount.deposit(sourceMoney);
targetAccount.withdraw(targetMoney);
}
}

而原始代码则简化为一行:

1
复制代码accountTransferService.transfer(sourceAccount, targetAccount, targetMoney, exchangeRate);

重构后结果分析

这个案例重构后的代码如下:

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
复制代码public class TransferServiceImplNew implements TransferService {

private AccountRepository accountRepository;
private AuditMessageProducer auditMessageProducer;
private ExchangeRateService exchangeRateService;
private AccountTransferService accountTransferService;

@Override
public Result<Boolean> transfer(Long sourceUserId, String targetAccountNumber, BigDecimal targetAmount, String targetCurrency) {
// 参数校验
Money targetMoney = new Money(targetAmount, new Currency(targetCurrency));

// 读数据
Account sourceAccount = accountRepository.find(new UserId(sourceUserId));
Account targetAccount = accountRepository.find(new AccountNumber(targetAccountNumber));
ExchangeRate exchangeRate = exchangeRateService.getExchangeRate(sourceAccount.getCurrency(), targetMoney.getCurrency());

// 业务逻辑
accountTransferService.transfer(sourceAccount, targetAccount, targetMoney, exchangeRate);

// 保存数据
accountRepository.save(sourceAccount);
accountRepository.save(targetAccount);

// 发送审计消息
AuditMessage message = new AuditMessage(sourceAccount, targetAccount, targetMoney);
auditMessageProducer.send(message);

return Result.success(true);
}
}

可以看出来,经过重构后的代码有以下几个特征:

1、业务逻辑清晰,数据存储和业务逻辑完全分隔。

2、Entity、Domain Primitive、Domain Service都是独立的对象,没有任何外部依赖,但是却包含了所有核心业务逻辑,可以单独完整测试。

3、原有的TransferService不再包括任何计算逻辑,仅仅作为组件编排,所有逻辑均delegate到其他组件。这种仅包含Orchestration(编排)的服务叫做Application Service(应用服务)。

我们可以根据新的结构重新画一张图:

然后通过重新编排后该图变为:

我们可以发现,通过对外部依赖的抽象和内部逻辑的封装重构,应用整体的依赖关系变了:

1、最底层不再是数据库,而是Entity、Domain Primitive和Domain Service。这些对象不依赖任何外部服务和框架,而是纯内存中的数据和操作。这些对象我们打包为Domain Layer(领域层)。领域层没有任何外部依赖关系。

2、再其次的是负责组件编排的Application Service,但是这些服务仅仅依赖了一些抽象出来的ACL类和Repository类,而其具体实现类是通过依赖注入注进来的。Application Service、Repository、ACL等我们统称为Application Layer(应用层)。应用层 依赖 领域层,但不依赖具体实现。

3、最后是ACL,Repository等的具体实现,这些实现通常依赖外部具体的技术实现和框架,所以统称为Infrastructure Layer(基础设施层)。Web框架里的对象如Controller之类的通常也属于基础设施层。

如果今天能够重新写这段代码,考虑到最终的依赖关系,我们可能先写Domain层的业务逻辑,然后再写Application层的组件编排,最后才写每个外部依赖的具体实现。这种架构思路和代码组织结构就叫做Domain-Driven Design(领域驱动设计,或DDD)。所以DDD不是一个特殊的架构设计,而是所有Transction Script代码经过合理重构后一定会抵达的终点。

DDD的六边形架构

在我们传统的代码里,我们一般都很注重每个外部依赖的实现细节和规范,但是今天我们需要敢于抛弃掉原有的理念,重新审视代码结构。在上面重构的代码里,如果抛弃掉所有Repository、ACL、Producer等的具体实现细节,我们会发现每一个对外部的抽象类其实就是输入或输出,类似于计算机系统中的I/O节点。这个观点在CQRS架构中也同样适用,将所有接口分为Command(输入)和Query(输出)两种。除了I/O之外其他的内部逻辑,就是应用业务的核心逻辑。基于这个基础,Alistair Cockburn在2005年提出了Hexagonal Architecture(六边形架构),又被称之为Ports and Adapters(端口和适配器架构)。

在这张图中:

1、I/O的具体实现在模型的最外层

2、每个I/O的适配器在灰色地带

3、每个Hex的边是一个端口

4、Hex的中央是应用的核心领域模型

在Hex中,架构的组织关系第一次变成了一个二维的内外关系,而不是传统一维的上下关系。同时在Hex架构中我们第一次发现UI层、DB层、和各种中间件层实际上是没有本质上区别的,都只是数据的输入和输出,而不是在传统架构中的最上层和最下层。

除了2005年的Hex架构,2008年 Jeffery Palermo的Onion Architecture(洋葱架构)和2017年 Robert Martin的Clean Architecture(干净架构),都是极为类似的思想。除了命名不一样、切入点不一样之外,其他的整体架构都是基于一个二维的内外关系。这也说明了基于DDD的架构最终的形态都是类似的。Herberto Graca有一个很全面的图包含了绝大部分现实中的端口类,值得借鉴。

代码组织结构

为了有效的组织代码结构,避免下层代码依赖到上层实现的情况,在Java中我们可以通过POM Module和POM依赖来处理相互的关系。通过Spring/SpringBoot的容器来解决运行时动态注入具体实现的依赖的问题。一个简单的依赖关系图如下:

Types 模块

Types模块是保存可以对外暴露的Domain Primitives的地方。Domain Primitives因为是无状态的逻辑,可以对外暴露,所以经常被包含在对外的API接口中,需要单独成为模块。Types模块不依赖任何类库,纯 POJO 。

Domain 模块

Domain 模块是核心业务逻辑的集中地,包含有状态的Entity、领域服务Domain Service、以及各种外部依赖的接口类(如Repository、ACL、中间件等。Domain模块仅依赖Types模块,也是纯 POJO 。

Application模块

Application模块主要包含Application Service和一些相关的类。Application模块依赖Domain模块。还是不依赖任何框架,纯POJO。

Infrastructure模块

Infrastructure模块包含了Persistence、Messaging、External等模块。比如:Persistence模块包含数据库DAO的实现,包含Data Object、ORM Mapper、Entity到DO的转化类等。Persistence模块要依赖具体的ORM类库,比如MyBatis。如果需要用Spring-Mybatis提供的注解方案,则需要依赖Spring。

Web模块

Web模块包含Controller等相关代码。如果用SpringMVC则需要依赖Spring。

Start模块

Start模块是SpringBoot的启动类。

测试

1、Types,Domain模块都属于无外部依赖的纯POJO,基本上都可以100%的被单元测试覆盖。

2、Application模块的代码依赖外部抽象类,需要通过测试框架去Mock所有外部依赖,但仍然可以100%被单元测试。

3、Infrastructure的每个模块的代码相对独立,接口数量比较少,相对比较容易写单测。但是由于依赖了外部I/O,速度上不可能很快,但好在模块的变动不会很频繁,属于一劳永逸。

4、Web模块有两种测试方法:通过Spring的MockMVC测试,或者通过HttpClient调用接口测试。但是在测试时最好把Controller依赖的服务类都Mock掉。一般来说当你把Controller的逻辑都后置到Application Service中时,Controller的逻辑变得极为简单,很容易100%覆盖。

5、Start模块:通常应用的集成测试写在start里。当其他模块的单元测试都能100%覆盖后,集成测试用来验证整体链路的真实性。

代码的演进/变化速度

在传统架构中,代码从上到下的变化速度基本上是一致的,改个需求需要从接口、到业务逻辑、到数据库全量变更,而第三方变更可能会导致整个代码的重写。但是在DDD中不同模块的代码的演进速度是不一样的:

1、Domain层属于核心业务逻辑,属于经常被修改的地方。比如:原来不需要扣手续费,现在需要了之类的。通过Entity能够解决基于单个对象的逻辑变更,通过Domain Service解决多个对象间的业务逻辑变更。

2、Application层属于Use Case(业务用例)。业务用例一般都是描述比较大方向的需求,接口相对稳定,特别是对外的接口一般不会频繁变更。添加业务用例可以通过新增Application Service或者新增接口实现功能的扩展。

3、Infrastructure层属于最低频变更的。一般这个层的模块只有在外部依赖变更了之后才会跟着升级,而外部依赖的变更频率一般远低于业务逻辑的变更频率。

所以在DDD架构中,能明显看出越外层的代码越稳定,越内层的代码演进越快,真正体现了领域“驱动”的核心思想。

总结

DDD不是一个什么特殊的架构,而是任何传统代码经过合理的重构之后最终一定会抵达的终点。DDD的架构能够有效的解决传统架构中的问题:

1、高可维护性:当外部依赖变更时,内部代码只用变更跟外部对接的模块,其他业务逻辑不变。

2、高可扩展性:做新功能时,绝大部分的代码都能复用,仅需要增加核心业务逻辑即可。

3、高可测试性:每个拆分出来的模块都符合单一性原则,绝大部分不依赖框架,可以快速的单元测试,做到100%覆盖。

4、代码结构清晰:通过POM module可以解决模块间的依赖关系,所有外接模块都可以单独独立成Jar包被复用。当团队形成规范后,可以快速的定位到相关代码。

本文转载自: 掘金

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

Kotlin Jetpack 实战 03 Kotlin

发表于 2020-06-29

往期文章

《Kotlin Jetpack 实战:开篇》

《00. 写给 Java 开发者的 Kotlin 入坑指南》

《01. 从一个膜拜大神的 Demo 开始》

《02. 用 Kotlin 写 Gradle 脚本是一种什么体验?》

简介

本文假设各位已经有了 Kotlin 基础,对 Kotlin 还不熟悉的小伙伴可以去看我之前发的文章。

本文将带领各位用 Kotlin 一步步重构我们的 Demo 工程,顺便一窥Kotlin 编程的三重境界。

说明:本系列文章都只探讨 Kotlin JVM,Kotlin JS/Native 都不在探讨范围内。

主要内容

前期准备

第一重境界:用 Java 视角写 Kotlin

第二重境界:用 Kotlin 视角写 Kotlin

第三重境界:用 Bytecode 视角写 Kotlin

结尾

前期准备

  • 将 Android Studio 版本升级到最新
  • 将我们的 Demo 工程 clone 到本地,用 Android Studio 打开:
    github.com/chaxiu/Kotl…
  • 切换到分支:chapter_03_kotlin_refactor_training
  • 强烈建议各位小伙伴小伙伴跟着本文一起实战,实战才是本文的精髓

为工程添加 Kotlin 支持

上一章我们已经将 Groovy 改成了 Kotlin DSL,但工程本身还不支持我们用 Kotlin 写 Android App。所以我们还需要做一些配置:

Libs.kt 增加以下依赖常量:

1
2
kotlin复制代码const val kotlinStdLib = "org.jetbrains.kotlin:kotlin-stdlib-jdk8:${Versions.kotlinVersion}"
const val ktxCore = "androidx.core:core-ktx:${Versions.ktxCore}"

根目录下的 build.gradle.kt 新增:

1
2
3
4
kotlin复制代码dependencies {
...
classpath(kotlin("gradle-plugin", version = Versions.kotlinVersion))
}

app/build.gradle.kt 新增:

1
2
3
4
5
6
7
8
9
10
11
kotlin复制代码plugins {
...
kotlin("android")
kotlin("android.extensions")
}

dependencies {
...
implementation(Libs.kotlinStdLib)
implementation(Libs.ktxCore)
}

注意事项:纯 Kotlin 开发的话做以上配置就够,但如果有 Java 混合开发的话,最好加上以下编译器参数配置,防止出现兼容性问题:
app/build.gradle.kt 新增:

1
2
3
4
5
6
7
8
9
10
11
12
13
kotlin复制代码android {
...
// Configure Java compiler compatible with Java 1.8
compileOptions {
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
}
// Configure Kotlin compiler target Java 1.8 when compile Kotlin to bytecode
kotlinOptions {
this as KotlinJvmOptions
jvmTarget = "1.8"
}
}

以上配置的作用,分别是:

  • 配置 Java 编译器兼容 Java 1.8
  • 配置 Kotlin 编译器以 Java 1.8 的规范生成字节码

以上修改的具体细节可以看我这个 GitHub Commit。

接下来我们进入正题,用 Kotlin 重构 Java 代码。

正文

我一直认为 Kotlin 是一门易学难精的语言:入门易,精通难。如果要为 Kotlin 程序员划分境界,我觉得可以划分三重境界。

1. 第一重境界:用 Java 视角写 Kotlin

这几乎是每个 Kotlin 程序员都会经历的境界(包括曾经的我)。我曾以为学会 Kotlin 的语法就能写好 Kotlin 代码,然而我只是把脑子里的 Java/C 代码用 Kotlin 语法翻译一遍写出来了而已。

接下来我就以第一重境界的”功力”,来重构我们的 Demo 工程。大家看看热闹就行,千万别学进脑子里啊。[狗头]

我现在假装自己是个新手,刚学会 Kotlin 语法。正所谓,柿子要挑软的捏,咱们重构代码当然也从最简单的开始。于是我找到 Demo 工程里的 User.java,一咬牙,就你了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
java复制代码public class User {
// 工程简单到没有数据库,所以将 API 请求写死缓存到这里
public static final String CACHE_RESPONSE = "{"login":"JakeWharton","id":66577,"node_id":"MDQ6VXNlcjY2NTc3","avatar_url":"https://avatars0.githubusercontent.com/u/66577?v=4","gravatar_id":"","url":"https://api.github.com/users/JakeWharton","html_url":"https://github.com/JakeWharton","followers_url":"https://api.github.com/users/JakeWharton/followers",小伙伴"following_url":"https://api.github.com/users/JakeWharton/following{/other_user}","gists_url":"https://api.github.com/users/JakeWharton/gists{/gist_id}","starred_url":"https://api.github.com/users/JakeWharton/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/JakeWharton/subscriptions","organizations_url":"https://api.github.com/users/JakeWharton/orgs","repos_url":"https://api.github.com/users/JakeWharton/repos","events_url":"https://api.github.com/users/JakeWharton/events{/privacy}","received_events_url":"https://api.github.com/users/JakeWharton/received_events","type":"User","site_admin":false,"name":"Jake Wharton","company":"Square","blog":"https://jakewharton.com","location":"Pittsburgh, PA, USA","email":null,"hireable":null,"bio":null,"twitter_username":null,"public_repos":104,"public_gists":54,"followers":57849,"following":12,"created_at":"2009-03-24T16:09:53Z","updated_at":"2020-05-28T00:07:20Z"}";

private String id;
private String login;
private String avatar_url;
private String name;
private String company;
private String blog;
private Date lastRefresh;

public User() { }

public User(@NonNull String id, String login, String avatar_url, String name, String company, String blog, Date lastRefresh) {
this.id = id;
this.login = login;
this.avatar_url = avatar_url;
this.name = name;
this.company = company;
this.blog = blog;
this.lastRefresh = lastRefresh;
}

public String getId() { return id; }
public String getAvatar_url() { return avatar_url; }
public Date getLastRefresh() { return lastRefresh; }
public String getLogin() { return login; }
public String getName() { return name; }
public String getCompany() { return company; }
public String getBlog() { return blog; }

public void setId(String id) { this.id = id; }
public void setAvatar_url(String avatar_url) { this.avatar_url = avatar_url; }
public void setLastRefresh(Date lastRefresh) { this.lastRefresh = lastRefresh; }
public void setLogin(String login) { this.login = login; }
public void setName(String name) { this.name = name; }
public void setCompany(String company) { this.company = company; }
public void setBlog(String blog) { this.blog = blog; }

一顿操作,我把这个 Java Bean 用 Kotlin 语法翻译成了这样:

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
kotlin复制代码class User {

companion object {
val CACHE_RESPONSE = "..."
}

private var id: String? = null
private var login: String? = null
private var avatar_url: String? = null
private var name: String? = null
private var company: String? = null
private var blog: String? = null
private var lastRefresh: Date? = null

constructor() {}
constructor(id: String, login: String?, avatar_url: String?, name: String?, company: String?, blog: String?, lastRefresh: Date?) {
this.id = id
this.login = login
this.avatar_url = avatar_url
this.name = name
this.company = company
this.blog = blog
this.lastRefresh = lastRefresh
}

fun getId(): String? { return id }
fun getAvatar_url(): String? { return avatar_url }
fun getLastRefresh(): Date? { return lastRefresh }
fun getLogin(): String? { return login }
fun getName(): String? { return name }
fun getCompany(): String? { return company }
fun getBlog(): String? { return blog }

fun setId(id: String?) { this.id = id }
fun setAvatar_url(avatar_url: String?) { this.avatar_url = avatar_url }
fun setLastRefresh(lastRefresh: Date?) { this.lastRefresh = lastRefresh }
fun setLogin(login: String?) { this.login = login }
fun setName(name: String?) { this.name = name }
fun setCompany(company: String?) { this.company = company }
fun setBlog(blog: String?) { this.blog = blog }
}

我看着自己一行一行写出来的 Kotlin 代码,心里成就感满满。So easy![狗头]

为了让工程能够模拟 Kotlin/Java 混编,我们让 ImagePreviewActivity 继续维持 Java 状态,所以接下来就剩下 MainActivity.java 的重构了。我们先看 MainActivity 的 Java 代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
java复制代码public class MainActivity extends AppCompatActivity {
public static final String TAG = "Main";
public static final String EXTRA_PHOTO = "photo";

StringRequest stringRequest;
RequestQueue requestQueue;

private ImageView image;
private ImageView gif;
private TextView username;
private TextView company;
private TextView website;

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
init();
}

private void init() {
image = findViewById(R.id.image);
gif = findViewById(R.id.gif);
username = findViewById(R.id.username);
company = findViewById(R.id.company);
website = findViewById(R.id.website);

display(User.CACHE_RESPONSE);
requestOnlineInfo();
}

private void requestOnlineInfo() {
requestQueue = Volley.newRequestQueue(this);
String url ="https://api.github.com/users/JakeWharton";
stringRequest = new StringRequest(Request.Method.GET, url,
new Response.Listener<String>() {
@Override
public void onResponse(String response) {
display(response);
}
}, new Response.ErrorListener() {
@Override
public void onErrorResponse(VolleyError error) {
Toast.makeText(MainActivity.this, error.getMessage(), Toast.LENGTH_SHORT).show();
}
});
stringRequest.setTag(TAG);
requestQueue.add(stringRequest);
}

private void display(@Nullable String response) {
if (TextUtils.isEmpty(response)) { return; }

Gson gson = new Gson();
final User user = gson.fromJson(response, User.class);
if (user != null){
Glide.with(this).load("file:///android_asset/bless.gif").into(gif);
Glide.with(this).load(user.getAvatar_url()).apply(RequestOptions.circleCropTransform()).into(image);
this.username.setText(user.getName());
this.company.setText(user.getCompany());
this.website.setText(user.getBlog());

image.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
gotoImagePreviewActivity(user);
}
});
}
}

private void gotoImagePreviewActivity(User user) {
Intent intent = new Intent(this, ImagePreviewActivity.class);
intent.putExtra(EXTRA_PHOTO, user.getAvatar_url());
startActivity(intent);
}

@Override
protected void onStop () {
super.onStop();
if (requestQueue != null) {
requestQueue.cancelAll(TAG);
}
}
}

一通操作,我把 MainActivity 重构成了这样:

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
kotlin复制代码class MainActivity : AppCompatActivity() {
companion object {
val TAG = "Main"
val EXTRA_PHOTO = "photo"
}

var stringRequest: StringRequest? = null
var requestQueue: RequestQueue? = null

private var image: ImageView? = null
private var gif: ImageView? = null
private var username: TextView? = null
private var company: TextView? = null
private var website: TextView? = null

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
init()
}

private fun init() {
image = findViewById(R.id.image)
gif = findViewById(R.id.gif)
username = findViewById(R.id.username)
company = findViewById(R.id.company)
website = findViewById(R.id.website)
display(User.CACHE_RESPONSE)
requestOnlineInfo()
}

private fun requestOnlineInfo() {
requestQueue = Volley.newRequestQueue(this)
val url = "https://api.github.com/users/JakeWharton"
stringRequest = StringRequest(Request.Method.GET, url,
object: Response.Listener<String> {
override fun onResponse(response: String?) {
display(response)
}
}, object: Response.ErrorListener {
override fun onErrorResponse(error: VolleyError?) {
Toast.makeText(this@MainActivity, error?.message, Toast.LENGTH_SHORT).show()
}
})
stringRequest!!.tag = TAG
requestQueue!!.add(stringRequest)
}

private fun display(response: String?) {
if (TextUtils.isEmpty(response)) {
return
}
val gson = Gson()
val user = gson.fromJson(response, User::class.java)
if (user != null) {
Glide.with(this).load("file:///android_asset/bless.gif").into(gif!!)
Glide.with(this).load(user.getAvatar_url()).apply(RequestOptions.circleCropTransform()).into(image!!)
username!!.text = user.getName()
company!!.text = user.getCompany()
website!!.text = user.getBlog()
image!!.setOnClickListener(object: View.OnClickListener{
override fun onClick(v: View?) {
gotoImagePreviewActivity(user)
}
})
}
}

private fun gotoImagePreviewActivity(user: User) {
val intent = Intent(this, ImagePreviewActivity::class.java)
intent.putExtra(EXTRA_PHOTO, user.getAvatar_url())
startActivity(intent)
}

override fun onStop() {
super.onStop()
if (requestQueue != null) {
requestQueue!!.cancelAll(TAG)
}
}
}

由于 MainActivity 重构成了 Kotlin,ImagePreviewActivity.java 需要对应做一些调整。原因是 Java 还不能很好的识别伴生对象。

修改前:

1
2
3
4
5
6
7
8
9
10
java复制代码public class ImagePreviewActivity extends AppCompatActivity {

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
...
String url = intent.getStringExtra(MainActivity.EXTRA_PHOTO);
...
}
}

修改后:

1
2
3
4
5
6
7
8
9
10
java复制代码public class ImagePreviewActivity extends AppCompatActivity {

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
...
String url = intent.getStringExtra(MainActivity.Companion.getEXTRA_PHOTO());
...
}
}

小结

这个境界的特点是:一行 Kotlin 对应一行 Java,还不会运用 Kotlin 独有的特性。

以上修改的具体细节可以看我这个 GitHub Commit。

各位小伙伴千万别看到这里就走了啊,请看我下一个境界是怎么写(演)的。

2. 第二重境界:用 Kotlin 视角写 Kotlin

到第二重境界,我就是个成熟的 Kotlin 程序员了。我会用一些 Kotlin 独有特性去改善 Java 代码里的逻辑。

2-1 Data Class

我们还是从最简单的 User.kt 开始,看过《写给 Java 开发者的 Kotlin 入坑指南》的小伙伴一定知道 Data Class,我们来将 User.kt 重构成 Data Class,真的会省不少代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
kotlin复制代码data class User(
var id: String? = null,
var login: String? = null,
var avatar_url: String? = null,
var name: String? = null,
var company: String? = null,
var blog: String? = null,
var lastRefresh: Date? = null
) {
companion object {
val CACHE_RESPONSE = "..."
}
}
小结

Data Class 可以节省我们编写 Java Bean 的时间。

2-2 lateinit

接下来看 MainActivity.kt,我们从最上面的变量开始。之前我们定义的变量都是可为空的(Nullable),导致这些变量在使用的时候都需要判空,或者使用非空断言!!。这很不Kotlin。解决这个问题的办法很多,这里我先用 lateinit 来解决网络请求的两个变量。

修改前:

1
2
3
4
5
6
7
8
9
10
11
kotlin复制代码class MainActivity : AppCompatActivity() {
...
var stringRequest: StringRequest? = null
var requestQueue: RequestQueue? = null

private fun requestOnlineInfo() {
...
stringRequest!!.tag = TAG
requestQueue!!.add(stringRequest)
}
}

修改后:

1
2
3
4
5
6
7
8
9
10
11
kotlin复制代码class MainActivity : AppCompatActivity() {
...
private lateinit var stringRequest: StringRequest
private lateinit var requestQueue: RequestQueue

private fun requestOnlineInfo() {
...
stringRequest.tag = TAG
requestQueue.add(stringRequest)
}
}

小结

一般来说,我们定义不为空的变量需要在构造函数或者 init 代码块里赋值,这样编译器才不会报错。但很多时候我们的变量赋值并不能在以上情况下完成赋值,比如:findViewById。

lateinit 的作用是告诉编译器,我定义的这个不为空的变量,虽然目前没有对它赋值,但我在使用它之前,一定会对它赋值,肯定不为空,你不必报错。

2-3 Kotlin-Android-Extensions

KTX 是 Android 官方提供的一个 Gradle 插件,能够为开发者提供便利,它最著名的功能就是能够省掉 findViewById。之前我们在工程里已经添加了这个插件,接下来直接使用就可以了。

直接将控件的申明和赋值都删掉,然后在调用的地方我们按 option + return 选择 import:

修改前:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
kotlin复制代码private var image: ImageView? = null
private var gif: ImageView? = null
private var username: TextView? = null
private var company: TextView? = null
private var website: TextView? = null

image = findViewById(R.id.image)
gif = findViewById(R.id.gif)
username = findViewById(R.id.username)
company = findViewById(R.id.company)
website = findViewById(R.id.website)

...
username!!.text = user.name
company!!.text = user.company
website!!.text = user.blog

修改后:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
kotlin复制代码// 注意这里
import kotlinx.android.synthetic.main.activity_main.*

// private var image: ImageView? = null
// private var gif: ImageView? = null
// private var username: TextView? = null
// private var company: TextView? = null
// private var website: TextView? = null

// image = findViewById(R.id.image)
// gif = findViewById(R.id.gif)
// username = findViewById(R.id.username)
// company = findViewById(R.id.company)
// website = findViewById(R.id.website)

...
username.text = user.name
company.text = user.company
website.text = user.blog

小结

  • KTX 提供的便利当然不止是替代 findViewById,后面我们慢慢讲
  • KTX 提供便利的同时其实有一定隐患,我们后面再讲

2-4 Lambda

以下代码 Android Studio 会提示 Convert to lambda 我们只需要按 option + return,Android Studio 就会帮我们重构。

修改前:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
kotlin复制代码...
stringRequest = StringRequest(Request.Method.GET, url,
object : Response.Listener<String> {
override fun onResponse(response: String?) {
display(response)
}
}, object : Response.ErrorListener {
override fun onErrorResponse(error: VolleyError?) {
Toast.makeText(this@MainActivity, error?.message, Toast.LENGTH_SHORT).show()
}
})
...
image.setOnClickListener(object : View.OnClickListener {
override fun onClick(v: View?) {
gotoImagePreviewActivity(user)
}
})

修改后:

1
2
3
4
5
6
7
8
9
10
11
12
kotlin复制代码...
stringRequest = StringRequest(Request.Method.GET,
url,
Response.Listener { response ->
display(response)
},
Response.ErrorListener { error ->
Toast.makeText(this@MainActivity, error?.message, Toast.LENGTH_SHORT).show()
})
...
image.setOnClickListener { gotoImagePreviewActivity(user) }
...

小结

  • Kotlin Lambda 要讲清楚能专门写一本书,本文暂时只管怎么用
  • 在这里使用 Lambda 作为接口实现,最大的好处其实是提高了代码的可读性

2-5 扩展函数

使用 Kotlin 的扩展函数能消灭一切 xxUtils.java。Kotlin 标准函数就已经为我们提供了相关扩展函数,帮助我们消灭 TextUtils。

修改前:

1
2
3
4
kotlin复制代码...
if (TextUtils.isEmpty(response)) {
return
}

修改后:

1
2
3
4
kotlin复制代码...
if (response.isNullOrBlank()) {
return
}

上面修改后的代码看起来像是 response 有一个成员方法: isNullOrBlank(),这样做有很多好处:

  • 写代码更流畅,一个类有哪些可以调用的方法,IDE 会自动提示,而不用去找 xxUtils
  • 代码可读性更好

2-6 标准函数 apply

Kotlin 提供了一系列标准函数,比如: let, also, with, apply 帮助开发者简化逻辑。这里我们使用 apply,它的作用解释起来很麻烦,看代码更明了:

修改前:

1
2
3
4
5
6
kotlin复制代码if (user != null) {
...
username.text = user.name
website.text = user.blog
image.setOnClickListener { gotoImagePreviewActivity(user) }
}

修改后:

1
2
3
4
5
6
kotlin复制代码user?.apply {
...
username.text = name
website.text = blog
image.setOnClickListener { gotoImagePreviewActivity(this) }
}

小结

这个境界的特点是:

  • 一行 Kotlin 代码能对应多行 Java 代码
  • 代码可读性增强
  • 代码健壮性更好

具体细节可以看这个 Github Commit。

第三重境界:用 Bytecode 视角写 Kotlin

Kotlin 号称 Java 100% 兼容,就是因为 Kotlin 最终会被编译成字节码(Bytecode)。通过查看 Kotlin 编译后的字节码,我们既能了解 Kotlin 的原理,也能探索出一些 Kotlin 编程的 Tips。

受限于本文的篇幅,我们暂且不谈 Kotlin 的实现原理,也不去详细探讨 Kotlin 编程的 Tips。我们继续专注于实战。现阶段的项目中,我们已经尝试加入了一些 Kotlin 的特性,我们只研究现阶段用到的这些 Kotlin 特性。

3-1 如何查看 Kotlin 对应的 字节码?

Tools -> Kotlin -> Show Kotlin Bytecode
一般我们情况下我们只需要查看 Kotlin 等价的 Java 代码即可,因此我们可以在字节码弹窗的左上角找到 Decompile 按钮,这样就能看到 Kotlin 等价的 Java 代码了。

3-2 尽可能消灭可变性(Mutability)

Java 中被 final 修饰的变量一旦赋值后就无法被修改。这在 Java 中也是很好的习惯,我们在 Kotlin 中也应该沿用。Kotlin 没有 final,但是有 val。

我们还是先从 User.kt 开始。

1
2
3
4
5
6
7
8
9
10
11
12
13
kotlin复制代码data class User(
var id: String? = null,
var login: String? = null,
var avatar_url: String? = null,
var name: String? = null,
var company: String? = null,
var blog: String? = null,
var lastRefresh: Date? = null
) {
companion object {
val CACHE_RESPONSE = "..."
}
}

User.kt 反编译成 Java 后:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
java复制代码...
public final class User {
@Nullable
private String id;
...
@NotNull
private static final String CACHE_RESPONSE = "...";
public static final User.Companion Companion = new User.Companion((DefaultConstructorMarker)null);

@Nullable
public final String getId() {
return this.id;
}

public final void setId(@Nullable String var1) {
this.id = var1;
}
...
public static final class Companion {
@NotNull
public final String getCACHE_RESPONSE() {
return User.CACHE_RESPONSE;
}

private Companion() {
}

// $FF: synthetic method
public Companion(DefaultConstructorMarker $constructor_marker) {
this();
}
}
}

我们将 User.kt 里面的 var 都替换成 val:

1
2
3
4
5
6
7
8
9
10
11
12
13
kotlin复制代码data class User(
val id: String? = null,
val login: String? = null,
val avatar_url: String? = null,
val name: String? = null,
val company: String? = null,
val blog: String? = null,
val lastRefresh: Date? = null
) {
companion object {
val CACHE_RESPONSE = "..."
}
}

它反编译成 Java 代码变成了这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
java复制代码public final class User {
@Nullable
private final String id; // 多了 final
...
@NotNull
private static final String CACHE_RESPONSE = "...";
public static final User.Companion Companion = new User.Companion((DefaultConstructorMarker)null);

@Nullable
public final String getId() {
return this.id;
}
// setId() 没有了
...

public static final class Companion {
@NotNull
public final String getCACHE_RESPONSE() {
return User.CACHE_RESPONSE;
}

private Companion() {
}

// $FF: synthetic method
public Companion(DefaultConstructorMarker $constructor_marker) {
this();
}
}
}

小结:

  • Kotlin 基于 JVM,所以从前 Java 的编程经验也是有用的
  • 将 Data Class 的 var 改成 val 后,它的成员变量就有 final 修饰了,同时set 方法也没了,一个 Data Class 在被实例化后,就无法再被修改了
  • 如果要修改 Data Class 的成员变量怎么办?用 copy 方法

3-3 尽可能缩小变量的作用域(Scope)

这一点在 Java 和 Kotlin 中同样有用。MainActivity.kt 中有两个成员变量,其中的 stringRequest 其实是可以改为局部变量的。

修改前:

1
2
3
4
5
kotlin复制代码class MainActivity : AppCompatActivity() {
...
private lateinit var stringRequest: StringRequest
private lateinit var requestQueue: RequestQueue
}

修改后:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
kotlin复制代码class MainActivity : AppCompatActivity() {
...
// private lateinit var stringRequest: StringRequest
private lateinit var requestQueue: RequestQueue

private fun requestOnlineInfo() {
...
val stringRequest = StringRequest(Request.Method.GET,
url,
Response.Listener { response ->
display(response)
},
Response.ErrorListener { error ->
Toast.makeText(this@MainActivity, error?.message, Toast.LENGTH_SHORT).show()
})
...
}
}

3-4 巧用 by lazy

MainActivity 只剩下一个成员变量 requestQueue,它还是用的 var 修饰的,我们能不能把它改为 val 呢?当然可以,但我们需要借助 by lazy,委托。

修改后:

1
2
3
4
5
6
kotlin复制代码class MainActivity : AppCompatActivity() {
...
private val requestQueue: RequestQueue by lazy {
Volley.newRequestQueue(this)
}
}

让我们看看它等价的 Java 代码,它的初始化交给了 LazyKt.lazy:

1
2
3
4
5
6
7
8
9
10
11
java复制代码private final Lazy requestQueue$delegate = LazyKt.lazy((Function0)(new Function0() {
// $FF: synthetic method
// $FF: bridge method
public Object invoke() {
return this.invoke();
}

public final RequestQueue invoke() {
return Volley.newRequestQueue((Context)MainActivity.this);
}
}));

再看看 LazyKt.lazy 的实现,实际上是 SynchronizedLazyImpl:

1
ktolin复制代码public actual fun <T> lazy(initializer: () -> T): Lazy<T> = SynchronizedLazyImpl(initializer)

再看看 SynchronizedLazyImpl:

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
kotlin复制代码private class SynchronizedLazyImpl<out T>(initializer: () -> T, lock: Any? = null) : Lazy<T>, Serializable {
private var initializer: (() -> T)? = initializer
@Volatile private var _value: Any? = UNINITIALIZED_VALUE
// final field is required to enable safe publication of constructed instance
private val lock = lock ?: this

override val value: T
get() {
val _v1 = _value
if (_v1 !== UNINITIALIZED_VALUE) {
@Suppress("UNCHECKED_CAST")
return _v1 as T
}

return synchronized(lock) {
val _v2 = _value
if (_v2 !== UNINITIALIZED_VALUE) {
@Suppress("UNCHECKED_CAST") (_v2 as T)
} else {
val typedValue = initializer!!()
_value = typedValue
initializer = null
typedValue
}
}
}

override fun isInitialized(): Boolean = _value !== UNINITIALIZED_VALUE

override fun toString(): String = if (isInitialized()) value.toString() else "Lazy value not initialized yet."

private fun writeReplace(): Any = InitializedLazyImpl(value)
}

果然,和我们之前文章提到的一样,by lazy 默认情况下会使用同步的方式进行初始化。但我们当前项目并不需要,毕竟多线程同步也是有开销的。

修改后:

1
2
3
kotlin复制代码private val requestQueue: RequestQueue by lazy(LazyThreadSafetyMode.NONE) {
Volley.newRequestQueue(this)
}

3-5 不要用错伴生对象

由于 Java 无法识别 Kotlin 里面的伴生对象,所以我们在 Java 里访问的时候比较别扭。

1
2
3
4
5
6
kotlin复制代码class MainActivity : AppCompatActivity() {
companion object {
...
val EXTRA_PHOTO = "photo"
}
}

在 Java 中访问:

1
2
3
4
5
6
7
8
java复制代码public class ImagePreviewActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
...
String url = intent.getStringExtra(MainActivity.Companion.getEXTRA_PHOTO());
...
}
}

反编译后:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
java复制代码...
@NotNull
private static final String EXTRA_PHOTO = "photo";
public static final MainActivity.Companion Companion = new MainActivity.Companion((DefaultConstructorMarker)null);

...
public static final class Companion {
@NotNull
public final String getEXTRA_PHOTO() {
return MainActivity.EXTRA_PHOTO;
}

private Companion() {
}

// $FF: synthetic method
public Companion(DefaultConstructorMarker $constructor_marker) {
this();
}
}

我们可以看到,默认情况下,Kotlin 为伴生对象里的变量生成了 get 方法,Java 代码里要访问这个变量必须这样: MainActivity.Companion.getEXTRA_PHOTO(),这很不友好。

为了让 Java 能够更好的识别伴生对象里的变量和方法,我们可以这么做:

使用 const:

1
2
3
4
5
6
kotlin复制代码class MainActivity : AppCompatActivity() {
companion object {
...
const val EXTRA_PHOTO = "photo"
}
}

或者使用 @JvmField 注解:

1
2
3
4
5
6
7
kotlin复制代码class MainActivity : AppCompatActivity() {
companion object {
...
@JvmField
val EXTRA_PHOTO = "photo"
}
}

在 Java 中访问:

1
2
3
4
5
6
7
8
java复制代码public class ImagePreviewActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
...
String url = intent.getStringExtra(MainActivity.EXTRA_PHOTO);
...
}
}

以上两种情况反编译成 Java 的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
java复制代码...
@NotNull
public static final String EXTRA_PHOTO = "photo";
public static final MainActivity.Companion Companion = new MainActivity.Companion((DefaultConstructorMarker)null);

...
public static final class Companion {
@NotNull
public final String getTAG() {
return MainActivity.TAG;
}

private Companion() {
}

// $FF: synthetic method
public Companion(DefaultConstructorMarker $constructor_marker) {
this();
}
}

不少博客讲伴生对象到这里就结束了。@JvmField,const,@JvmStatic,这些确实是使用伴生对象需要注意的。

可是,咱们的代码到这里是不是就完美了?并不。

我们可以看到,即使我们加上了 @JvmField 或者 const,伴生对象仍然为常量生成了 get 方法,同时也定义了一个 Companion 的类,还有一个 instance。然而我们最初的需求只是要定义一个 public static final String 的常量而已。

这个小结的标题是不要用错伴生对象。它的前提是什么?它的前提是:该不该用。在这里我不禁要问一句:这种情况下,真的需要伴生对象吗?答案是:不需要。

MainActivity 中的 TAG 不需要在类以外被访问,因此可以直接定义为成员变量:

1
2
3
kotlin复制代码class MainActivity : AppCompatActivity() {
private val TAG = "Main"
}

现在只剩下 EXTRA_PHOTO,我们应该怎么处理?在 Java 中,我们经常会定义一个类来专门存放常量,Kotlin 中我们同样可以借鉴:

让我们创建一个 Constant.kt:

1
2
3
4
5
6
7
8
kotlin复制代码//注意这里,它要放到 package 的前面
@file:JvmName("Constant")

package com.boycoder.kotlinjetpackinaction

const val EXTRA_PHOTO = "photo"

const val CACHE_RESPONSE = "..."

在 Kotlin 中可以直接这样使用:

1
2
kotlin复制代码// Kotlin 中甚至可以省略掉 Constant,因为 CACHE_RESPONSE 是顶层常量。
display(CACHE_RESPONSE)

在 Java 中要这样使用:

1
2
java复制代码// 由于 @file:JvmName("Constant") 的存在,Java 中也能很好的访问 Constant.EXTRA_PHOTO
String url = intent.getStringExtra(Constant.EXTRA_PHOTO);

Constant.kt 反编译成 Java 后是这样的:

1
2
3
4
5
6
java复制代码public final class Constant {
@NotNull
public static final String EXTRA_PHOTO = "photo";
@NotNull
public static final String CACHE_RESPONSE = "...";
}

所以说,如果只是需要定义静态常量,哪用得上 Kotlin 的伴生对象?

以上修改的具体细节可以看我这个 Github Commit。

总结:

  • Java 的编程经验在 Kotlin 中也是有用的,但我们又不能被 Java 里的经验禁锢
  • Kotlin 中引入了 Java 所没有的特性和概念,我们在使用前最好能用清楚底层实现
  • 网上博客写的最佳实践不一定对(包括本文),要独立思考

4. 结尾

本文只是借助我们的 Demo 一窥 Kotlin 编程的三重境界,让大家对 Kotlin 编程整体有个了解。后面我也许会写专题文章来讲《Kotlin 编译器漫游指南》,《Kotlin 最佳实践指北》,也许吧。

文章写到这已经接近尾声了,那我们的 Demo 工程改到这个程度是不是已经完美了呢?当然没有。但我不想写了,欢迎各位小伙伴留言一起讨论还有哪些地方能改进。

我们下一篇文章再见。

回目录–>《Kotlin Jetpack 实战》

本文转载自: 掘金

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

一个好用的Apollo配置中心 Client 是多重要

发表于 2020-06-29

背景

我们有部分服务,从JAVA 切换到 Node.Js,也因此要有一个好用的配置中心,携程的Apollo非常棒,JAVA语言有官方维护的Client,很棒。切换到Node.js虽然有几个开源的实现,但是对我们来说还是不不够完美,简单的几个要求:

  • 支持热更新,我可不想每次修改配置都重启服务器
  • 最好像Java那样 @value(“mysql.port:3306”)一个简单的装饰器就可以注入配置
  • 故障容错能力,比如哪天配置中心挂了,服务还是可以正常启动的,要求配置中心不可用的情况下,主动恢复最近一份配置启动项目。
  • 支持Typescript

因此诞生了本项目,

Introduction

  1. 本项目为携程配置中心框架 Apollo 提供的Node.js版本客户端;
  2. 客户端连接成功后,会拉取所有配置到本地存储一份,主要是用于 apollo 服务不可用的情况下容错和降级;
  3. 通过Http long polling 机制实现热更新,客户端会自动修改被注入的属性值,实现无重启修改配置;
  4. 可以通过getConfigs() 函数获取最新的配置;
  5. 故障容错机制,apollo 服务不可用的情况下,客户端会自动恢复最近一份配置启动。

apollo 服务端测试环境:

  • host: http://106.54.227.205
  • 账号: apollo
  • 密码: admin

Features

  • 配置热更新
  • 支持装饰器 @value(“mysql.port:3306”)
  • 缓存配置到本地
  • 灰度发布
  • 支持 TypeScript

Install

1
复制代码npm i ctrip-apollo-client

Usage

  • 本demo 已经在测试环境创建项目 apolloclient,大家可以直接在本地测试;
  • 在配置中心修改user.name值后,无需重启,再次请求,会自动取到最新的值;
  • demo 源码。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
复制代码import { CtripApplloClient, value, hotValue } from 'ctrip-apollo-client';
import Koa from 'koa';

const apollo = new CtripApplloClient({
configServerUrl: 'http://106.54.227.205:8080',
appId: 'apolloclient',
configPath: './config/apolloConfig.json',
namespaceList: ['application', 'development.qa']
});
const app = new Koa();

const run = async () => {
// 初始化配置
await apollo.init();

// 获取的配置,不会热更新
const port = apollo.getValue('app.port:3000');
// 获取配置,支持热更新,需要通过 appName.value 获取最终值
const appName = hotValue('app.name:apollo-demo');

class User {
// 通过装饰器注入,支持热更新
// 只能注入类的属性
@value("user.name:liuwei")
public name: string
}
const user = new User();

app.use(async (ctx, next) => {
ctx.body = {
appName: appName.value,
userName: user.name
}
await next();
})
app.listen(port);
console.log('listening on port:', port);
console.info(`curl --location --request GET \'http://localhost:${port}\' `);
}
run();

javascript demo 请点击链接

API

ApolloClient(options) 构造函数

  • returns: apolloClient
  • options
    • configServerUrl string required Apollo配置服务的地址
    • appId string required 应用的appId
    • clusterName string 集群名,默认值:default
    • namespaceList array Namespace的名字,默认值:[application]
    • configPath string 本地配置文件路径 默认值./config/apolloConfig.json
    • logger object 日志类 必须实现 logger.info(),logger.error() 两个方法

init(timeoutMs)
初始化配置中心,拉取远端配置到端上,并且缓存一份到文件中,同时开启配置变更监听器来实时同步配置,如果拉取超过 timeoutMs ,或者发生异常,则读取本地缓存的配置文件。如果本地没有缓存配置文件,抛出异常。

  • return: Promise
  • timeoutMs 超时时间

getConfigs() 获取最新的配置文件

  • returns: object
1
复制代码const config = apollo.getConfigs();

getValue (namespace = ‘application’) 获取具体的配置字段

  • returns: string
  • namespace 默认值: application
  • field string eg: mysql.port:3306 分号前面key,如果未配置 3306 作为默认值
1
2
3
4
5
复制代码class User {
get userName () {
return apollo.getValue({ field: 'user.name:liuwei' });
}
}

hotValue (namespace = ‘application’) 获取具体的配置字段,封装 getter(热更新)

  • returns: {value}
  • namespace 默认值: application
  • field string 属性位置 eg: mysql.port:3306 分号前面key,如果未配置 3306 作为默认值
1
2
复制代码const userName = apollo.hotValue({ field: 'user.name:liuwei' });
console.log(userName.value);

withValue(target, key, field, namespace)

  • returns: void
  • target 目标对象
  • key 需要注入对象的属性
  • field string 属性位置 eg: mysql.port:3306 分号前面key,如果未配置 3306 作为默认值
  • namespace string 默认值:application
1
2
3
4
5
6
7
复制代码class User {
constructor () {
withValue(this, 'userId', { field: 'user.id:10071' });
}
}
// userId 属性会跟随配置更新
new User().userId

onChange (callback(object)) 配置变更回调通知

  • returns: void

value(field, namespace) 注入器,只能注入类的属性

  • field string 字段属性
  • namespace string
1
2
3
4
5
复制代码import { value } from 'ctrip-apollo-client';
class User {
@value("user.name:liuwei")
public name: string
}

Benchmark

一次性注入 [localValue] x 736,896,802 ops/sec ±1.49% (82 runs sampled)

支持热更新 [hotValue] x 2,021,310 ops/sec ±1.28% (87 runs sampled)

热更新默认值 [hotValue default] x 1,581,645 ops/sec ±0.89% (87 runs sampled)

装饰器注入 [decorator] x 2,161,312 ops/sec ±0.96% (87 runs sampled)

原生访问 [dot] x 704,644,395 ops/sec ±1.45% (82 runs sampled)

Fastest is [localValue]

License

MIT

项目源码

github.com/lvgithub/ct…

本文转载自: 掘金

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

JFR定位由于可能的JDK11的bug导致Log4j2 CP

发表于 2020-06-28

本文基于OpenJDK 11

最近使用Spring Cloud Gateway的时候,遇到了一个奇怪的问题:

线上有3个 API 网关实例,压力均衡,平稳运行3天后,突然有一个实例,CPU飚高,并且响应时间增加很多,从几十毫秒涨到了几分钟。

线上是 k8s 管理容器,立刻停掉了这个 pod,重建,恢复正常。

线上我们开启了 JFR 记录(可以参考我的另外系列文章:Java 监控 JFR),通过 JMC 查看下出问题的 JFR 记录。

首先我们来看 GC,我们的 GC 算法是 G1,主要通过 G1 Garbage Collection这个事件查看:

image

发现 GC 全部为 Young GC,且耗时比较正常,频率上也没有什么明显异常。

接下来来看,CPU 占用相关。直接看 Thread CPU Load 这个事件,看每个线程的 CPU 占用情况。发现reactor-http-epoll线程池的线程,CPU 占用很高,加在一起,接近了 100%。

image

这些线程是 reactor-netty 处理业务的线程,观察其他实例,发现正常情况下,并不会有这么高的 CPU 负载。那么为啥会有这么高的负载呢?通过 Thread Dump 来看一下线程堆栈有何发现.

通过查看多个线程堆栈 dump,发现这些线程基本都处于 Runnable,并且执行的方法是原生方法,和StackWalker相关,例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
复制代码"reactor-http-epoll-2" #75 daemon prio=5 os_prio=0 cpu=25100145.64ms elapsed=306507.26s tid=0x0000556eddcbd000 nid=0x61 runnable  [0x00007f8605443000]
java.lang.Thread.State: RUNNABLE
at java.lang.StackStreamFactory$AbstractStackWalker.callStackWalk(java.base@11.0.6/Native Method)
at java.lang.StackStreamFactory$AbstractStackWalker.beginStackWalk(java.base@11.0.6/StackStreamFactory.java:370)
at java.lang.StackStreamFactory$AbstractStackWalker.walk(java.base@11.0.6/StackStreamFactory.java:243)
at java.lang.StackWalker.walk(java.base@11.0.6/StackWalker.java:498)
at org.apache.logging.log4j.util.StackLocator.calcLocation(StackLocator.java:81)
at org.apache.logging.log4j.util.StackLocatorUtil.calcLocation(StackLocatorUtil.java:76)
at org.apache.logging.log4j.spi.AbstractLogger.getLocation(AbstractLogger.java:2201)
at org.apache.logging.log4j.spi.AbstractLogger.logMessageTrackRecursion(AbstractLogger.java:2144)
at org.apache.logging.log4j.spi.AbstractLogger.logMessageSafely(AbstractLogger.java:2127)
at org.apache.logging.log4j.spi.AbstractLogger.logMessage(AbstractLogger.java:2020)
at org.apache.logging.log4j.spi.AbstractLogger.logIfEnabled(AbstractLogger.java:1891)
at org.apache.logging.log4j.spi.AbstractLogger.info(AbstractLogger.java:1436)
at com.xxx.apigateway.filter.AccessCheckFilter.filter(AccessCheckFilter.java:144)
at org.springframework.cloud.gateway.handler.FilteringWebHandler$GatewayFilterAdapter.filter(FilteringWebHandler.java:138)
at org.springframework.cloud.gateway.filter.OrderedGatewayFilter.filter(OrderedGatewayFilter.java:44)
at org.springframework.cloud.gateway.handler.FilteringWebHandler$DefaultGatewayFilterChain.lambda$filter$0(FilteringWebHandler.java:118)
at org.springframework.cloud.gateway.handler.FilteringWebHandler$DefaultGatewayFilterChain$$Lambda$1265/0x0000000800b83440.get(Unknown Source)
at reactor.core.publisher.MonoDefer.subscribe(MonoDefer.java:44)
at reactor.core.publisher.InternalMonoOperator.subscribe(InternalMonoOperator.java:55)
at reactor.core.publisher.MonoDefer.subscribe(MonoDefer.java:52)
at reactor.core.publisher.InternalMonoOperator.subscribe(InternalMonoOperator.java:55)
at reactor.core.publisher.MonoDefer.subscribe(MonoDefer.java:52)
at reactor.core.publisher.InternalMonoOperator.subscribe(InternalMonoOperator.java:55)
at reactor.core.publisher.MonoDefer.subscribe(MonoDefer.java:52)
at reactor.core.publisher.InternalMonoOperator.subscribe(InternalMonoOperator.java:55)
at reactor.core.publisher.MonoDefer.subscribe(MonoDefer.java:52)
at reactor.core.publisher.InternalMonoOperator.subscribe(InternalMonoOperator.java:55)
at reactor.core.publisher.MonoDefer.subscribe(MonoDefer.java:52)
at reactor.core.publisher.InternalMonoOperator.subscribe(InternalMonoOperator.java:55)
at reactor.core.publisher.MonoDefer.subscribe(MonoDefer.java:52)
at reactor.core.publisher.InternalMonoOperator.subscribe(InternalMonoOperator.java:55)
at reactor.core.publisher.MonoDefer.subscribe(MonoDefer.java:52)
at reactor.core.publisher.InternalMonoOperator.subscribe(InternalMonoOperator.java:55)
at reactor.core.publisher.MonoDefer.subscribe(MonoDefer.java:52)
at reactor.core.publisher.Mono.subscribe(Mono.java:4105)
at reactor.core.publisher.MonoIgnoreThen$ThenIgnoreMain.drain(MonoIgnoreThen.java:172)
at reactor.core.publisher.MonoIgnoreThen.subscribe(MonoIgnoreThen.java:56)
at reactor.core.publisher.InternalMonoOperator.subscribe(InternalMonoOperator.java:55)
at reactor.core.publisher.MonoFlatMap$FlatMapMain.onNext(MonoFlatMap.java:150)
at org.springframework.cloud.sleuth.instrument.reactor.ScopePassingSpanSubscriber.onNext(ScopePassingSpanSubscriber.java:90)
at reactor.core.publisher.FluxSwitchIfEmpty$SwitchIfEmptySubscriber.onNext(FluxSwitchIfEmpty.java:67)
at org.springframework.cloud.sleuth.instrument.reactor.ScopePassingSpanSubscriber.onNext(ScopePassingSpanSubscriber.java:90)
at reactor.core.publisher.MonoNext$NextSubscriber.onNext(MonoNext.java:76)
at org.springframework.cloud.sleuth.instrument.reactor.ScopePassingSpanSubscriber.onNext(ScopePassingSpanSubscriber.java:90)
at reactor.core.publisher.FluxConcatMap$ConcatMapImmediate.innerNext(FluxConcatMap.java:274)
at reactor.core.publisher.FluxConcatMap$ConcatMapInner.onNext(FluxConcatMap.java:851)
at org.springframework.cloud.sleuth.instrument.reactor.ScopePassingSpanSubscriber.onNext(ScopePassingSpanSubscriber.java:90)
at reactor.core.publisher.FluxMap$MapSubscriber.onNext(FluxMap.java:114)
at org.springframework.cloud.sleuth.instrument.reactor.ScopePassingSpanSubscriber.onNext(ScopePassingSpanSubscriber.java:90)
at reactor.core.publisher.FluxSwitchIfEmpty$SwitchIfEmptySubscriber.onNext(FluxSwitchIfEmpty.java:67)
at org.springframework.cloud.sleuth.instrument.reactor.ScopePassingSpanSubscriber.onNext(ScopePassingSpanSubscriber.java:90)
at reactor.core.publisher.Operators$MonoSubscriber.complete(Operators.java:1637)
at reactor.core.publisher.MonoFlatMap$FlatMapInner.onNext(MonoFlatMap.java:241)
at reactor.core.publisher.Operators$ScalarSubscription.request(Operators.java:2199)
at reactor.core.publisher.MonoFlatMap$FlatMapInner.onSubscribe(MonoFlatMap.java:230)
at reactor.core.publisher.MonoJust.subscribe(MonoJust.java:54)
at reactor.core.publisher.InternalMonoOperator.subscribe(InternalMonoOperator.java:55)
at reactor.core.publisher.MonoFlatMap$FlatMapMain.onNext(MonoFlatMap.java:150)
at org.springframework.cloud.sleuth.instrument.reactor.ScopePassingSpanSubscriber.onNext(ScopePassingSpanSubscriber.java:90)
at reactor.core.publisher.FluxMap$MapSubscriber.onNext(FluxMap.java:114)
at org.springframework.cloud.sleuth.instrument.reactor.ScopePassingSpanSubscriber.onNext(ScopePassingSpanSubscriber.java:90)
at reactor.core.publisher.MonoNext$NextSubscriber.onNext(MonoNext.java:76)
at org.springframework.cloud.sleuth.instrument.reactor.ScopePassingSpanSubscriber.onNext(ScopePassingSpanSubscriber.java:90)
at reactor.core.publisher.FluxConcatMap$ConcatMapImmediate.innerNext(FluxConcatMap.java:274)
at reactor.core.publisher.FluxConcatMap$ConcatMapInner.onNext(FluxConcatMap.java:851)
at org.springframework.cloud.sleuth.instrument.reactor.ScopePassingSpanSubscriber.onNext(ScopePassingSpanSubscriber.java:90)
at reactor.core.publisher.FluxOnErrorResume$ResumeSubscriber.onNext(FluxOnErrorResume.java:73)
at org.springframework.cloud.sleuth.instrument.reactor.ScopePassingSpanSubscriber.onNext(ScopePassingSpanSubscriber.java:90)
at reactor.core.publisher.MonoPeekTerminal$MonoTerminalPeekSubscriber.onNext(MonoPeekTerminal.java:173)
at org.springframework.cloud.sleuth.instrument.reactor.ScopePassingSpanSubscriber.onNext(ScopePassingSpanSubscriber.java:90)
at reactor.core.publisher.Operators$MonoSubscriber.complete(Operators.java:1637)
at reactor.core.publisher.MonoFilterWhen$MonoFilterWhenMain.innerResult(MonoFilterWhen.java:193)
at reactor.core.publisher.MonoFilterWhen$FilterWhenInner.onNext(MonoFilterWhen.java:260)
at reactor.core.publisher.MonoFilterWhen$FilterWhenInner.onNext(MonoFilterWhen.java:228)
at reactor.core.publisher.Operators$ScalarSubscription.request(Operators.java:2199)
at reactor.core.publisher.MonoFilterWhen$FilterWhenInner.onSubscribe(MonoFilterWhen.java:249)
at reactor.core.publisher.MonoJust.subscribe(MonoJust.java:54)
at reactor.core.publisher.Mono.subscribe(Mono.java:4105)
at reactor.core.publisher.MonoFilterWhen$MonoFilterWhenMain.onNext(MonoFilterWhen.java:150)
at reactor.core.publisher.Operators$ScalarSubscription.request(Operators.java:2199)
at reactor.core.publisher.MonoFilterWhen$MonoFilterWhenMain.onSubscribe(MonoFilterWhen.java:103)
at reactor.core.publisher.MonoJust.subscribe(MonoJust.java:54)
at reactor.core.publisher.Mono.subscribe(Mono.java:4105)
at reactor.core.publisher.FluxConcatMap$ConcatMapImmediate.drain(FluxConcatMap.java:441)
at reactor.core.publisher.FluxConcatMap$ConcatMapImmediate.onNext(FluxConcatMap.java:243)
at org.springframework.cloud.sleuth.instrument.reactor.ScopePassingSpanSubscriber.onNext(ScopePassingSpanSubscriber.java:90)
at org.springframework.cloud.sleuth.instrument.reactor.ScopePassingSpanSubscriber.onNext(ScopePassingSpanSubscriber.java:90)
at reactor.core.publisher.FluxDematerialize$DematerializeSubscriber.onNext(FluxDematerialize.java:91)
at reactor.core.publisher.FluxDematerialize$DematerializeSubscriber.onNext(FluxDematerialize.java:38)
at org.springframework.cloud.sleuth.instrument.reactor.ScopePassingSpanSubscriber.onNext(ScopePassingSpanSubscriber.java:90)
at reactor.core.publisher.FluxIterable$IterableSubscription.slowPath(FluxIterable.java:243)
at reactor.core.publisher.FluxIterable$IterableSubscription.request(FluxIterable.java:201)
at org.springframework.cloud.sleuth.instrument.reactor.ScopePassingSpanSubscriber.request(ScopePassingSpanSubscriber.java:76)
at reactor.core.publisher.FluxDematerialize$DematerializeSubscriber.request(FluxDematerialize.java:120)
at org.springframework.cloud.sleuth.instrument.reactor.ScopePassingSpanSubscriber.request(ScopePassingSpanSubscriber.java:76)
at org.springframework.cloud.sleuth.instrument.reactor.ScopePassingSpanSubscriber.request(ScopePassingSpanSubscriber.java:76)
at reactor.core.publisher.FluxConcatMap$ConcatMapImmediate.onSubscribe(FluxConcatMap.java:228)
at org.springframework.cloud.sleuth.instrument.reactor.ScopePassingSpanSubscriber.onSubscribe(ScopePassingSpanSubscriber.java:69)
at org.springframework.cloud.sleuth.instrument.reactor.ScopePassingSpanSubscriber.onSubscribe(ScopePassingSpanSubscriber.java:69)
at reactor.core.publisher.FluxDematerialize$DematerializeSubscriber.onSubscribe(FluxDematerialize.java:70)
at org.springframework.cloud.sleuth.instrument.reactor.ScopePassingSpanSubscriber.onSubscribe(ScopePassingSpanSubscriber.java:69)
at reactor.core.publisher.FluxIterable.subscribe(FluxIterable.java:139)
at reactor.core.publisher.FluxIterable.subscribe(FluxIterable.java:63)
at reactor.core.publisher.InternalFluxOperator.subscribe(InternalFluxOperator.java:53)
at reactor.core.publisher.FluxDefer.subscribe(FluxDefer.java:54)
at reactor.core.publisher.Mono.subscribe(Mono.java:4105)
at reactor.core.publisher.FluxConcatMap$ConcatMapImmediate.drain(FluxConcatMap.java:441)
at reactor.core.publisher.FluxConcatMap$ConcatMapImmediate.onNext(FluxConcatMap.java:243)
at org.springframework.cloud.sleuth.instrument.reactor.ScopePassingSpanSubscriber.onNext(ScopePassingSpanSubscriber.java:90)
at reactor.core.publisher.FluxIterable$IterableSubscription.slowPath(FluxIterable.java:243)
at reactor.core.publisher.FluxIterable$IterableSubscription.request(FluxIterable.java:201)
at org.springframework.cloud.sleuth.instrument.reactor.ScopePassingSpanSubscriber.request(ScopePassingSpanSubscriber.java:76)
at reactor.core.publisher.FluxConcatMap$ConcatMapImmediate.onSubscribe(FluxConcatMap.java:228)
at org.springframework.cloud.sleuth.instrument.reactor.ScopePassingSpanSubscriber.onSubscribe(ScopePassingSpanSubscriber.java:69)
at reactor.core.publisher.FluxIterable.subscribe(FluxIterable.java:139)
at reactor.core.publisher.FluxIterable.subscribe(FluxIterable.java:63)
at reactor.core.publisher.InternalMonoOperator.subscribe(InternalMonoOperator.java:55)
at reactor.core.publisher.MonoDefer.subscribe(MonoDefer.java:52)
at reactor.core.publisher.InternalMonoOperator.subscribe(InternalMonoOperator.java:55)
at reactor.core.publisher.MonoDefer.subscribe(MonoDefer.java:52)
at reactor.core.publisher.InternalMonoOperator.subscribe(InternalMonoOperator.java:55)
at org.springframework.cloud.sleuth.instrument.web.TraceWebFilter$MonoWebFilterTrace.subscribe(TraceWebFilter.java:162)
at reactor.core.publisher.InternalMonoOperator.subscribe(InternalMonoOperator.java:55)
at reactor.core.publisher.MonoDefer.subscribe(MonoDefer.java:52)
at reactor.core.publisher.InternalMonoOperator.subscribe(InternalMonoOperator.java:55)
at reactor.core.publisher.MonoDefer.subscribe(MonoDefer.java:52)
at reactor.core.publisher.InternalMonoOperator.subscribe(InternalMonoOperator.java:55)
at reactor.core.publisher.MonoDefer.subscribe(MonoDefer.java:52)
at reactor.core.publisher.Mono.subscribe(Mono.java:4105)
at reactor.core.publisher.MonoIgnoreThen$ThenIgnoreMain.drain(MonoIgnoreThen.java:172)
at reactor.core.publisher.MonoIgnoreThen.subscribe(MonoIgnoreThen.java:56)
at reactor.core.publisher.InternalMonoOperator.subscribe(InternalMonoOperator.java:55)
at reactor.netty.http.server.HttpServerHandle.onStateChange(HttpServerHandle.java:64)
at reactor.netty.tcp.TcpServerBind$ChildObserver.onStateChange(TcpServerBind.java:228)
at reactor.netty.http.server.HttpServerOperations.onInboundNext(HttpServerOperations.java:465)
at reactor.netty.channel.ChannelOperationsHandler.channelRead(ChannelOperationsHandler.java:90)
at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:377)
at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:363)
at io.netty.channel.AbstractChannelHandlerContext.fireChannelRead(AbstractChannelHandlerContext.java:355)
at reactor.netty.http.server.HttpTrafficHandler.channelRead(HttpTrafficHandler.java:167)
at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:377)
at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:363)
at io.netty.channel.AbstractChannelHandlerContext.fireChannelRead(AbstractChannelHandlerContext.java:355)
at io.netty.channel.CombinedChannelDuplexHandler$DelegatingChannelHandlerContext.fireChannelRead(CombinedChannelDuplexHandler.java:436)
at io.netty.handler.codec.ByteToMessageDecoder.fireChannelRead(ByteToMessageDecoder.java:321)
at io.netty.handler.codec.ByteToMessageDecoder.fireChannelRead(ByteToMessageDecoder.java:308)
at io.netty.handler.codec.ByteToMessageDecoder.callDecode(ByteToMessageDecoder.java:422)
at io.netty.handler.codec.ByteToMessageDecoder.channelRead(ByteToMessageDecoder.java:276)
at io.netty.channel.CombinedChannelDuplexHandler.channelRead(CombinedChannelDuplexHandler.java:251)
at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:377)
at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:363)
at io.netty.channel.AbstractChannelHandlerContext.fireChannelRead(AbstractChannelHandlerContext.java:355)
at io.netty.channel.DefaultChannelPipeline$HeadContext.channelRead(DefaultChannelPipeline.java:1410)
at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:377)
at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:363)
at io.netty.channel.DefaultChannelPipeline.fireChannelRead(DefaultChannelPipeline.java:919)
at io.netty.channel.epoll.AbstractEpollStreamChannel$EpollStreamUnsafe.epollInReady(AbstractEpollStreamChannel.java:792)
at io.netty.channel.epoll.AbstractEpollChannel$AbstractEpollUnsafe$1.run(AbstractEpollChannel.java:387)
at io.netty.util.concurrent.AbstractEventExecutor.safeExecute(AbstractEventExecutor.java:164)
at io.netty.util.concurrent.SingleThreadEventExecutor.runAllTasks(SingleThreadEventExecutor.java:472)
at io.netty.channel.epoll.EpollEventLoop.run(EpollEventLoop.java:384)
at io.netty.util.concurrent.SingleThreadEventExecutor$4.run(SingleThreadEventExecutor.java:989)
at io.netty.util.internal.ThreadExecutorMap$2.run(ThreadExecutorMap.java:74)
at io.netty.util.concurrent.FastThreadLocalRunnable.run(FastThreadLocalRunnable.java:30)
at java.lang.Thread.run(java.base@11.0.6/Thread.java:834)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
复制代码"reactor-http-epoll-4" #79 daemon prio=5 os_prio=0 cpu=25266372.53ms elapsed=306415.58s tid=0x0000556eddcc4800 nid=0x65 runnable  [0x00007f85e2ff9000]
java.lang.Thread.State: RUNNABLE
at java.lang.StackStreamFactory$AbstractStackWalker.fetchStackFrames(java.base@11.0.6/Native Method)
at java.lang.StackStreamFactory$AbstractStackWalker.fetchStackFrames(java.base@11.0.6/StackStreamFactory.java:386)
at java.lang.StackStreamFactory$AbstractStackWalker.getNextBatch(java.base@11.0.6/StackStreamFactory.java:322)
at java.lang.StackStreamFactory$AbstractStackWalker.peekFrame(java.base@11.0.6/StackStreamFactory.java:263)
at java.lang.StackStreamFactory$AbstractStackWalker.hasNext(java.base@11.0.6/StackStreamFactory.java:351)
at java.lang.StackStreamFactory$StackFrameTraverser.nextStackFrame(java.base@11.0.6/StackStreamFactory.java:520)
at java.lang.StackStreamFactory$StackFrameTraverser.forEachRemaining(java.base@11.0.6/StackStreamFactory.java:581)
at java.util.stream.AbstractPipeline.copyInto(java.base@11.0.6/AbstractPipeline.java:484)
at java.util.stream.AbstractPipeline.wrapAndCopyInto(java.base@11.0.6/AbstractPipeline.java:474)
at java.util.stream.AbstractPipeline.evaluate(java.base@11.0.6/AbstractPipeline.java:550)
at java.util.stream.AbstractPipeline.evaluateToArrayNode(java.base@11.0.6/AbstractPipeline.java:260)
at java.util.stream.ReferencePipeline.toArray(java.base@11.0.6/ReferencePipeline.java:517)
at java.util.stream.ReferencePipeline.toArray(java.base@11.0.6/ReferencePipeline.java:523)
at org.apache.logging.log4j.util.StackLocator$FqcnCallerLocator.apply(StackLocator.java:96)
at org.apache.logging.log4j.util.StackLocator$FqcnCallerLocator.apply(StackLocator.java:90)
at java.lang.StackStreamFactory$StackFrameTraverser.consumeFrames(java.base@11.0.6/StackStreamFactory.java:534)
at java.lang.StackStreamFactory$AbstractStackWalker.doStackWalk(java.base@11.0.6/StackStreamFactory.java:306)
at java.lang.StackStreamFactory$AbstractStackWalker.callStackWalk(java.base@11.0.6/Native Method)
at java.lang.StackStreamFactory$AbstractStackWalker.beginStackWalk(java.base@11.0.6/StackStreamFactory.java:370)
at java.lang.StackStreamFactory$AbstractStackWalker.walk(java.base@11.0.6/StackStreamFactory.java:243)
at java.lang.StackWalker.walk(java.base@11.0.6/StackWalker.java:498)
at org.apache.logging.log4j.util.StackLocator.calcLocation(StackLocator.java:81)
at org.apache.logging.log4j.util.StackLocatorUtil.calcLocation(StackLocatorUtil.java:76)
at org.apache.logging.log4j.spi.AbstractLogger.getLocation(AbstractLogger.java:2201)
at org.apache.logging.log4j.spi.AbstractLogger.logMessageTrackRecursion(AbstractLogger.java:2144)
at org.apache.logging.log4j.spi.AbstractLogger.logMessageSafely(AbstractLogger.java:2127)
at org.apache.logging.log4j.spi.AbstractLogger.logMessage(AbstractLogger.java:2038)
at org.apache.logging.log4j.spi.AbstractLogger.logIfEnabled(AbstractLogger.java:1915)
at org.apache.logging.log4j.spi.AbstractLogger.info(AbstractLogger.java:1451)
at com.xxx.apigateway.filter.CommonLogFilter.filter(CommonLogFilter.java:42)
at org.springframework.cloud.gateway.handler.FilteringWebHandler$GatewayFilterAdapter.filter(FilteringWebHandler.java:138)
at org.springframework.cloud.gateway.filter.OrderedGatewayFilter.filter(OrderedGatewayFilter.java:44)
at org.springframework.cloud.gateway.handler.FilteringWebHandler$DefaultGatewayFilterChain.lambda$filter$0(FilteringWebHandler.java:118)
at org.springframework.cloud.gateway.handler.FilteringWebHandler$DefaultGatewayFilterChain$$Lambda$1265/0x0000000800b83440.get(Unknown Source)
at reactor.core.publisher.MonoDefer.subscribe(MonoDefer.java:44)
at reactor.core.publisher.InternalMonoOperator.subscribe(InternalMonoOperator.java:55)
at reactor.core.publisher.MonoDefer.subscribe(MonoDefer.java:52)
at reactor.core.publisher.InternalMonoOperator.subscribe(InternalMonoOperator.java:55)
at reactor.core.publisher.MonoDefer.subscribe(MonoDefer.java:52)
at reactor.core.publisher.InternalMonoOperator.subscribe(InternalMonoOperator.java:55)
at reactor.core.publisher.MonoDefer.subscribe(MonoDefer.java:52)
at reactor.core.publisher.InternalMonoOperator.subscribe(InternalMonoOperator.java:55)
at reactor.core.publisher.MonoDefer.subscribe(MonoDefer.java:52)
at reactor.core.publisher.InternalMonoOperator.subscribe(InternalMonoOperator.java:55)
at reactor.core.publisher.MonoDefer.subscribe(MonoDefer.java:52)
at reactor.core.publisher.Mono.subscribe(Mono.java:4105)
at reactor.core.publisher.MonoIgnoreThen$ThenIgnoreMain.drain(MonoIgnoreThen.java:172)
at reactor.core.publisher.MonoIgnoreThen.subscribe(MonoIgnoreThen.java:56)
at reactor.core.publisher.InternalMonoOperator.subscribe(InternalMonoOperator.java:55)
at reactor.core.publisher.MonoFlatMap$FlatMapMain.onNext(MonoFlatMap.java:150)
at org.springframework.cloud.sleuth.instrument.reactor.ScopePassingSpanSubscriber.onNext(ScopePassingSpanSubscriber.java:90)
at reactor.core.publisher.FluxSwitchIfEmpty$SwitchIfEmptySubscriber.onNext(FluxSwitchIfEmpty.java:67)
at org.springframework.cloud.sleuth.instrument.reactor.ScopePassingSpanSubscriber.onNext(ScopePassingSpanSubscriber.java:90)
at reactor.core.publisher.MonoNext$NextSubscriber.onNext(MonoNext.java:76)
at org.springframework.cloud.sleuth.instrument.reactor.ScopePassingSpanSubscriber.onNext(ScopePassingSpanSubscriber.java:90)
at reactor.core.publisher.FluxConcatMap$ConcatMapImmediate.innerNext(FluxConcatMap.java:274)
at reactor.core.publisher.FluxConcatMap$ConcatMapInner.onNext(FluxConcatMap.java:851)
at org.springframework.cloud.sleuth.instrument.reactor.ScopePassingSpanSubscriber.onNext(ScopePassingSpanSubscriber.java:90)
at reactor.core.publisher.FluxMap$MapSubscriber.onNext(FluxMap.java:114)
at org.springframework.cloud.sleuth.instrument.reactor.ScopePassingSpanSubscriber.onNext(ScopePassingSpanSubscriber.java:90)
at reactor.core.publisher.FluxSwitchIfEmpty$SwitchIfEmptySubscriber.onNext(FluxSwitchIfEmpty.java:67)
at org.springframework.cloud.sleuth.instrument.reactor.ScopePassingSpanSubscriber.onNext(ScopePassingSpanSubscriber.java:90)
at reactor.core.publisher.Operators$MonoSubscriber.complete(Operators.java:1637)
at reactor.core.publisher.MonoFlatMap$FlatMapInner.onNext(MonoFlatMap.java:241)
at reactor.core.publisher.Operators$ScalarSubscription.request(Operators.java:2199)
at reactor.core.publisher.MonoFlatMap$FlatMapInner.onSubscribe(MonoFlatMap.java:230)
at reactor.core.publisher.MonoJust.subscribe(MonoJust.java:54)
at reactor.core.publisher.InternalMonoOperator.subscribe(InternalMonoOperator.java:55)
at reactor.core.publisher.MonoFlatMap$FlatMapMain.onNext(MonoFlatMap.java:150)
at org.springframework.cloud.sleuth.instrument.reactor.ScopePassingSpanSubscriber.onNext(ScopePassingSpanSubscriber.java:90)
at reactor.core.publisher.FluxMap$MapSubscriber.onNext(FluxMap.java:114)
at org.springframework.cloud.sleuth.instrument.reactor.ScopePassingSpanSubscriber.onNext(ScopePassingSpanSubscriber.java:90)
at reactor.core.publisher.MonoNext$NextSubscriber.onNext(MonoNext.java:76)
at org.springframework.cloud.sleuth.instrument.reactor.ScopePassingSpanSubscriber.onNext(ScopePassingSpanSubscriber.java:90)
at reactor.core.publisher.FluxConcatMap$ConcatMapImmediate.innerNext(FluxConcatMap.java:274)
at reactor.core.publisher.FluxConcatMap$ConcatMapInner.onNext(FluxConcatMap.java:851)
at org.springframework.cloud.sleuth.instrument.reactor.ScopePassingSpanSubscriber.onNext(ScopePassingSpanSubscriber.java:90)
at reactor.core.publisher.FluxOnErrorResume$ResumeSubscriber.onNext(FluxOnErrorResume.java:73)
at org.springframework.cloud.sleuth.instrument.reactor.ScopePassingSpanSubscriber.onNext(ScopePassingSpanSubscriber.java:90)
at reactor.core.publisher.MonoPeekTerminal$MonoTerminalPeekSubscriber.onNext(MonoPeekTerminal.java:173)
at org.springframework.cloud.sleuth.instrument.reactor.ScopePassingSpanSubscriber.onNext(ScopePassingSpanSubscriber.java:90)
at reactor.core.publisher.Operators$MonoSubscriber.complete(Operators.java:1637)
at reactor.core.publisher.MonoFilterWhen$MonoFilterWhenMain.innerResult(MonoFilterWhen.java:193)
at reactor.core.publisher.MonoFilterWhen$FilterWhenInner.onNext(MonoFilterWhen.java:260)
at reactor.core.publisher.MonoFilterWhen$FilterWhenInner.onNext(MonoFilterWhen.java:228)
at reactor.core.publisher.Operators$ScalarSubscription.request(Operators.java:2199)
at reactor.core.publisher.MonoFilterWhen$FilterWhenInner.onSubscribe(MonoFilterWhen.java:249)
at reactor.core.publisher.MonoJust.subscribe(MonoJust.java:54)
at reactor.core.publisher.Mono.subscribe(Mono.java:4105)
at reactor.core.publisher.MonoFilterWhen$MonoFilterWhenMain.onNext(MonoFilterWhen.java:150)
at reactor.core.publisher.Operators$ScalarSubscription.request(Operators.java:2199)
at reactor.core.publisher.MonoFilterWhen$MonoFilterWhenMain.onSubscribe(MonoFilterWhen.java:103)
at reactor.core.publisher.MonoJust.subscribe(MonoJust.java:54)
at reactor.core.publisher.Mono.subscribe(Mono.java:4105)
at reactor.core.publisher.FluxConcatMap$ConcatMapImmediate.drain(FluxConcatMap.java:441)
at reactor.core.publisher.FluxConcatMap$ConcatMapImmediate.onNext(FluxConcatMap.java:243)
at org.springframework.cloud.sleuth.instrument.reactor.ScopePassingSpanSubscriber.onNext(ScopePassingSpanSubscriber.java:90)
at org.springframework.cloud.sleuth.instrument.reactor.ScopePassingSpanSubscriber.onNext(ScopePassingSpanSubscriber.java:90)
at reactor.core.publisher.FluxDematerialize$DematerializeSubscriber.onNext(FluxDematerialize.java:91)
at reactor.core.publisher.FluxDematerialize$DematerializeSubscriber.onNext(FluxDematerialize.java:38)
at org.springframework.cloud.sleuth.instrument.reactor.ScopePassingSpanSubscriber.onNext(ScopePassingSpanSubscriber.java:90)
at reactor.core.publisher.FluxIterable$IterableSubscription.slowPath(FluxIterable.java:243)
at reactor.core.publisher.FluxIterable$IterableSubscription.request(FluxIterable.java:201)
at org.springframework.cloud.sleuth.instrument.reactor.ScopePassingSpanSubscriber.request(ScopePassingSpanSubscriber.java:76)
at reactor.core.publisher.FluxDematerialize$DematerializeSubscriber.request(FluxDematerialize.java:120)
at org.springframework.cloud.sleuth.instrument.reactor.ScopePassingSpanSubscriber.request(ScopePassingSpanSubscriber.java:76)
at org.springframework.cloud.sleuth.instrument.reactor.ScopePassingSpanSubscriber.request(ScopePassingSpanSubscriber.java:76)
at reactor.core.publisher.FluxConcatMap$ConcatMapImmediate.onSubscribe(FluxConcatMap.java:228)
at org.springframework.cloud.sleuth.instrument.reactor.ScopePassingSpanSubscriber.onSubscribe(ScopePassingSpanSubscriber.java:69)
at org.springframework.cloud.sleuth.instrument.reactor.ScopePassingSpanSubscriber.onSubscribe(ScopePassingSpanSubscriber.java:69)
at reactor.core.publisher.FluxDematerialize$DematerializeSubscriber.onSubscribe(FluxDematerialize.java:70)
at org.springframework.cloud.sleuth.instrument.reactor.ScopePassingSpanSubscriber.onSubscribe(ScopePassingSpanSubscriber.java:69)
at reactor.core.publisher.FluxIterable.subscribe(FluxIterable.java:139)
at reactor.core.publisher.FluxIterable.subscribe(FluxIterable.java:63)
at reactor.core.publisher.InternalFluxOperator.subscribe(InternalFluxOperator.java:53)
at reactor.core.publisher.FluxDefer.subscribe(FluxDefer.java:54)
at reactor.core.publisher.Mono.subscribe(Mono.java:4105)
at reactor.core.publisher.FluxConcatMap$ConcatMapImmediate.drain(FluxConcatMap.java:441)
at reactor.core.publisher.FluxConcatMap$ConcatMapImmediate.onNext(FluxConcatMap.java:243)
at org.springframework.cloud.sleuth.instrument.reactor.ScopePassingSpanSubscriber.onNext(ScopePassingSpanSubscriber.java:90)
at reactor.core.publisher.FluxIterable$IterableSubscription.slowPath(FluxIterable.java:243)
at reactor.core.publisher.FluxIterable$IterableSubscription.request(FluxIterable.java:201)
at org.springframework.cloud.sleuth.instrument.reactor.ScopePassingSpanSubscriber.request(ScopePassingSpanSubscriber.java:76)
at reactor.core.publisher.FluxConcatMap$ConcatMapImmediate.onSubscribe(FluxConcatMap.java:228)
at org.springframework.cloud.sleuth.instrument.reactor.ScopePassingSpanSubscriber.onSubscribe(ScopePassingSpanSubscriber.java:69)
at reactor.core.publisher.FluxIterable.subscribe(FluxIterable.java:139)
at reactor.core.publisher.FluxIterable.subscribe(FluxIterable.java:63)
at reactor.core.publisher.InternalMonoOperator.subscribe(InternalMonoOperator.java:55)
at reactor.core.publisher.MonoDefer.subscribe(MonoDefer.java:52)
at reactor.core.publisher.InternalMonoOperator.subscribe(InternalMonoOperator.java:55)
at reactor.core.publisher.MonoDefer.subscribe(MonoDefer.java:52)
at reactor.core.publisher.InternalMonoOperator.subscribe(InternalMonoOperator.java:55)
at org.springframework.cloud.sleuth.instrument.web.TraceWebFilter$MonoWebFilterTrace.subscribe(TraceWebFilter.java:162)
at reactor.core.publisher.InternalMonoOperator.subscribe(InternalMonoOperator.java:55)
at reactor.core.publisher.MonoDefer.subscribe(MonoDefer.java:52)
at reactor.core.publisher.InternalMonoOperator.subscribe(InternalMonoOperator.java:55)
at reactor.core.publisher.MonoDefer.subscribe(MonoDefer.java:52)
at reactor.core.publisher.InternalMonoOperator.subscribe(InternalMonoOperator.java:55)
at reactor.core.publisher.MonoDefer.subscribe(MonoDefer.java:52)
at reactor.core.publisher.Mono.subscribe(Mono.java:4105)
at reactor.core.publisher.MonoIgnoreThen$ThenIgnoreMain.drain(MonoIgnoreThen.java:172)
at reactor.core.publisher.MonoIgnoreThen.subscribe(MonoIgnoreThen.java:56)
at reactor.core.publisher.InternalMonoOperator.subscribe(InternalMonoOperator.java:55)
at reactor.netty.http.server.HttpServerHandle.onStateChange(HttpServerHandle.java:64)
at reactor.netty.tcp.TcpServerBind$ChildObserver.onStateChange(TcpServerBind.java:228)
at reactor.netty.http.server.HttpServerOperations.onInboundNext(HttpServerOperations.java:465)
at reactor.netty.channel.ChannelOperationsHandler.channelRead(ChannelOperationsHandler.java:90)
at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:377)
at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:363)
at io.netty.channel.AbstractChannelHandlerContext.fireChannelRead(AbstractChannelHandlerContext.java:355)
at reactor.netty.http.server.HttpTrafficHandler.channelRead(HttpTrafficHandler.java:167)
at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:377)
at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:363)
at io.netty.channel.AbstractChannelHandlerContext.fireChannelRead(AbstractChannelHandlerContext.java:355)
at io.netty.channel.CombinedChannelDuplexHandler$DelegatingChannelHandlerContext.fireChannelRead(CombinedChannelDuplexHandler.java:436)
at io.netty.handler.codec.ByteToMessageDecoder.fireChannelRead(ByteToMessageDecoder.java:321)
at io.netty.handler.codec.ByteToMessageDecoder.channelRead(ByteToMessageDecoder.java:295)
at io.netty.channel.CombinedChannelDuplexHandler.channelRead(CombinedChannelDuplexHandler.java:251)
at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:377)
at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:363)
at io.netty.channel.AbstractChannelHandlerContext.fireChannelRead(AbstractChannelHandlerContext.java:355)
at io.netty.channel.DefaultChannelPipeline$HeadContext.channelRead(DefaultChannelPipeline.java:1410)
at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:377)
at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:363)
at io.netty.channel.DefaultChannelPipeline.fireChannelRead(DefaultChannelPipeline.java:919)
at io.netty.channel.epoll.AbstractEpollStreamChannel$EpollStreamUnsafe.epollInReady(AbstractEpollStreamChannel.java:792)
at io.netty.channel.epoll.EpollEventLoop.processReady(EpollEventLoop.java:475)
at io.netty.channel.epoll.EpollEventLoop.run(EpollEventLoop.java:378)
at io.netty.util.concurrent.SingleThreadEventExecutor$4.run(SingleThreadEventExecutor.java:989)
at io.netty.util.internal.ThreadExecutorMap$2.run(ThreadExecutorMap.java:74)
at io.netty.util.concurrent.FastThreadLocalRunnable.run(FastThreadLocalRunnable.java:30)
at java.lang.Thread.run(java.base@11.0.6/Thread.java:834)

主要和这两个原生方法有关:

  • java.lang.StackStreamFactory$AbstractStackWalker.callStackWalk
  • java.lang.StackStreamFactory$AbstractStackWalker.fetchStackFrames

虽然一直有OpenJDK 11 之后, StackWalker性能有问题,不如new Throwable()获取堆栈快的问题。但是考虑到压力是均衡的,其他两个实例并没有这个问题,应该不是本身对于这个类的应用,导致的CPU消耗突然变大。查看 JDK 相关的 JIRA,发现一个有意思的 BUG:Application on JDK11 consume 100% CPU after a few hours of uptime

这里面说,java.lang.StackStreamFactory$AbstractStackWalker.callStackWalk这个原生方法,在 JVM 运行几小时后,突然 CPU 就会飚高到 100%。看来可能确实有些问题。针对这个问题,我也提了个 Issue 到 Log4j 的 JIRA:High CPU consumption using StackWalker

那么为什么会调用这个类呢?Log4j2 打异步日志的时候,如果需要保留日志产生的类还有行号,需要缓存堆栈,那么需要配置includeLocation = true。这个堆栈,如果环境是 java 9 之前,那么通过 new Throwable() 实现, 参考:StackLocator.java 如果环境是 java 9 之后,那么通过 StackWalker 实现:StackLocator.java 这个如果你配置了includeLocation = true,就会缓存堆栈。

对于网关,我们可以不用打印类和行号,配置includeLocation = false可以避免再出现类似的问题。

以后在应用中,

本文转载自: 掘金

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

手把手教你 GitLab 的安装及使用

发表于 2020-06-28

新入职公司,发现公司还在使用落后生产工具 svn,由于重度使用过 svn 和 git ,知道这两个工具之间的差异,已经在使用 git 的路上越走越远。
于是,跟上级强烈建议让我在公司推行 git 和他的私有仓库 gitlab,多次安利“磨刀不误砍柴工”的理念,终于被我说服。
以下是我边安装和边记录的详细笔记,务求安装好之后分享给同事直接就能看懂,降低团队的学习成本。

git的优点

  1. git是分布式的,svn不是
    git分布式本地就可以用,可以随便保存各种历史痕迹,不用担心污染服务器,连不上服务器也能提交代码、查看log。
  2. GIT分支和SVN的分支不同
    分支在SVN中实际上是版本库中的一份copy,而git一个仓库是一个快照,所以git 切换、合并分支等操作更快速。
  3. git有一个强大的代码仓库管理系统 - gitlab
    可以很方便的管理权限、代码review,创建、管理project

GitLab介绍

GitLab:是一个基于Git实现的在线代码仓库托管软件,你可以用gitlab自己搭建一个类似于Github一样的系统,一般用于在企业、学校等内部网络搭建git私服。
功能:Gitlab 是一个提供代码托管、提交审核和问题跟踪的代码管理平台。对于软件工程质量管理非常重要。
版本:GitLab 分为社区版(CE) 和企业版(EE)。
配置:建议CPU2核,内存2G以上。

Gitlab的服务构成:

Nginx:静态web服务器。
gitlab-shell:用于处理Git命令和修改authorized keys列表。(Ruby)
gitlab-workhorse: 轻量级的反向代理服务器。(go)

GitLab Workhorse是一个敏捷的反向代理。它会处理一些大的HTTP请求,比如文件上传、文件下载、Git push/pull和Git包下载。其它请求会反向代理到GitLab Rails应用,即反向代理给后端的unicorn。

logrotate:日志文件管理工具。
postgresql:数据库。
redis:缓存数据库。
sidekiq:用于在后台执行队列任务(异步执行)。(Ruby)
unicorn:An HTTP server for Rack applications,GitLab Rails应用是托管在这个服务器上面的。(Ruby Web Server,主要使用Ruby编写)

GitLab安装

1.源码安装

2.yum安装

官方源地址:about.gitlab.com/downloads/#…
清华大学镜像源:mirror.tuna.tsinghua.edu.cn/help/gitlab…

新建 /etc/yum.repos.d/gitlab_gitlab-ce.repo,内容为:

1
2
3
4
5
复制代码    [gitlab-ce]
name=Gitlab CE Repository
baseurl=https://mirrors.tuna.tsinghua.edu.cn/gitlab-ce/yum/el$releasever/
gpgcheck=0
enabled=1

安装依赖

1
2
3
4
5
复制代码    sudo yum install curl openssh-server openssh-clients postfix cronie
sudo service postfix start
sudo chkconfig postfix on
#这句是用来做防火墙的,避免用户通过ssh方式和http来访问。
sudo lokkit -s http -s ssh

再执行

1
2
3
复制代码    sudo yum makecache
sudo yum install gitlab-ce
sudo gitlab-ctl reconfigure #Configure and start GitLab

配置域名: vim /var/opt/gitlab/nginx/conf/gitlab-http.conf

1
2
3
4
5
复制代码    # 外网访问的端口,如果服务器已经有服务器占用了80,那么这里可以改成其它
listen *:8888;
server_name gitlab.test.domain.com;

set $http_host_with_default "gitlab.test.domain.com:8888";

补充说明:因为编译gitlab的配置 /etc/gitlab/gitlab.rb 时会重新生成这个自定义nginx 配置,所以只要 gitlab 的配置配得好,上面的nginx其实不需要自定义的。

修改密码

1
2
3
4
复制代码    gitlab-rails console production
user = User.where(id:1).first
user.password='123456'
user.save!

GitLab备份和恢复

备份

1
2
复制代码    # 可以将此命令写入crontab,以实现定时备份
/usr/bin/gitlab-rake gitlab:backup:create

备份的数据会存储在/var/opt/gitlab/backups,用户通过自定义参数 gitlab_rails[‘backup_path’],改变默认值。

恢复

1
2
3
4
5
6
7
8
9
10
11
12
复制代码    # 停止unicorn和sidekiq,保证数据库没有新的连接,不会有写数据情况
sudo gitlab-ctl stop unicorn
sudo gitlab-ctl stop sidekiq

# 进入备份目录进行恢复,1476900742为备份文件的时间戳
cd /var/opt/gitlab/backups
gitlab-rake gitlab:backup:restore BACKUP=1476900742
cd -

# 启动unicorn和sidekiq
sudo gitlab-ctl start unicorn
sudo gitlab-ctl start sidekiq

GitLab配置文件修改

1
复制代码    vim /etc/gitlab/gitlab.rb

gitlab基本配置:

1
2
3
4
复制代码    #外部访问url(经过编译后,自动将这个配置编译到nginx配置,nginx就无需配置了)
external_url 'http://gitlab.test.domain.com:8888'
#默认值就是8080。如果端口被占用,可将8080修改为其它(例如:9090)
unicorn['port'] = 8080

gitlab发送邮件配置

1
2
3
4
5
6
7
8
9
复制代码    gitlab_rails['smtp_enable'] = true  
gitlab_rails['smtp_address'] = “smtp.exmail.qq.com”
gitlab_rails['smtp_port'] = 25
gitlab_rails['smtp_user_name'] = “huangdc@domain.com“
gitlab_rails['smtp_password'] = "smtp password"
gitlab_rails['smtp_authentication']= “plain"
gitlab_rails['smtp_enable_starttls_auto']= true
gitlab_rails['gitlab_email_from']= 'huangdc@domain.com'
gitlab_rails['gitlab_email_reply_to']= ‘noreply@domain.com'

服务器修改过ssh端口的坑(需要修改配置ssh端口)

1
2
复制代码    #修改过ssh端口,gitlab中项目的的ssh地址,会在前面加上协议头和端口号“ssh://git@gitlab.domain.com:55725/huangdc/test.git”
gitlab_rails['gitlab_shell_ssh_port'] = 55725

配置生效

1
2
3
4
复制代码    #使配置生效
gitlab-ctl reconfigure
#重新启动GitLab
gitlab-ctl restart

GitLab常用命令

1
2
3
4
5
6
7
8
9
复制代码    gitlab-ctl start    # 启动所有 gitlab 组件;
gitlab-ctl stop # 停止所有 gitlab 组件;
gitlab-ctl restart # 重启所有 gitlab 组件;
gitlab-ctl status # 查看服务状态;
vim /etc/gitlab/gitlab.rb # 修改gitlab配置文件;
gitlab-ctl reconfigure # 重新编译gitlab的配置;
gitlab-rake gitlab:check SANITIZE=true --trace # 检查gitlab;
gitlab-ctl tail # 查看日志;
gitlab-ctl tail nginx/gitlab_access.log

注意:执行 reconfigure 命令会把gitlab的nginx组件的配置还原,导致自定义修改的端口以及域名等都没有了。

常用目录

1
2
复制代码    日志地址:/var/log/gitlab/   # 对应各服务的打印日志 
服务地址:/var/opt/gitlab/ # 对应各服务的主目录

查看gitlab版本

cat /opt/gitlab/embedded/service/gitlab-rails/VERSION

新建项目

使用root用户登录进gitlab会后,点击“new project“创建一个项目,比如项目命名为“kuaijiFirstProject”。
然后会发现,硬盘上已经生成了一个git文件:

1
复制代码    /var/opt/gitlab/git-data/repositories/root/kuaijiFirstProject.git

汉化

gitlab.com/xhang/gitla…

gitlab的使用

1
复制代码    ssh -T -p 55725 git@gitlab.domain.com

注意:以上这条 ssh 命令测试通过,未必代表就能 git clone 代码,git clone 代码需要执行命令的账户有写权限,如果是普通用户用 sudo git clone 那么 git 就会使用的 root 账号的 Private Key。

1.登录

管理员会为使用者开通账号并设置权限。

2.使用者在客户端生成ssh key

参考文章: www.jianshu.com/p/142b3dc8a…

1
复制代码    ssh-keygen -t rsa -C "huangdc@domain.com"

3.将公钥的内容copy到gitlab用户设置里面的“SSH Keys”

Windows: clip < ~/.ssh/id_rsa.pub
Mac: pbcopy < ~/.ssh/id_rsa.pub
GNU/Linux (requires xclip): xclip -sel clip < ~/.ssh/id_rsa.pub

4.测试ssh连接

1
2
复制代码#标准测试代码:ssh -T git@gitlab.com
ssh -T -p 55725 git@gitlab.domain.com #修改过端口号的测试代码

如果连接成功的话,会出现以下信息:

1
复制代码    Welcome to GitLab, huangdc!

说明:实际上执行这条ssh命令,所使用的远程服务器的用户是git,这个用户是在安装gitlab的时候生成的,所有使用gitlab服务器的ssh客户端,都是使用git这个用户。在这里的用户“huangdc”是通过gitlab创建的,是用于gitlab的权限管理,也用作标识提交代码的开发者信息,不要跟ssh的用户混淆了。

如何使用多个SSH公钥(自己电脑在使用多个代码仓库)

原理其实是:因为每个仓库都需要 ssh 连接,而 ssh 命令默认是使用 .ssh 目录下面的私钥去连接代码仓库,所以我们可以在 .ssh/config 目录里面针对不同的仓库域名重定义它的私钥。

例子如下:

编辑文件: vim /Users/david/.ssh/config

1
2
复制代码    Host            gitlab.domain.com    
IdentityFile /Users/david/.ssh/id_rsa

命令行环境下初始化项目

  1. 首先在 gitlab 上面创建一个空的代码仓库,得到仓库地址如下:
1
复制代码    ssh://git@gitlab.domain.com:55725/huangdc/test.git
  1. 在本地初始化仓库、提交代码、推送到远程 master 分支。
1
2
3
4
5
6
复制代码    git clone ssh://git@gitlab.domain.com:55725/huangdc/test.git  
cd test
touch README.md
git add README.md
git commit -m "add README"
git push -u origin master

命令行环境下迁移旧的项目

  1. 首先在 gitlab 上面创建一个空的代码仓库,得到仓库地址如下:
1
2
复制代码    #注意:已存在代码的旧项目只能推送到空的远程代码仓库
ssh://git@gitlab.domain.com:55725/dev/memberApi.git
  1. 本地初始化项目、关联远程仓库、推送到远程仓库
1
2
3
4
复制代码 cd /Users/david/work_www/memberApi
git init
git remote add origin ssh://git@gitlab.domain.com:55725/dev/memberApi.git
git push -u origin master

SourceTree的安装和打开

  1. 官网下载链接:www.sourcetreeapp.com/
  2. 打开SourceTree之后,需要登录Atlassian账号来激活SourceTree。可以使用Google账号直接关联登录。
  3. 登录后还需要一些设置,以最简单的方式跳过就行

##502报错排查
1.权限

1
复制代码chmod -R 755 /var/log/gitlab                //增加权限

2.端口被占用

1
2
复制代码vim  /etc/gitlab/gitlab.rb                  //编辑配置文件
external_url '*****:*****' //更改端口

3.内存不足
安装GitLab需要至少4G的内存

www.jianshu.com/p/b04356e01…

本文转载自: 掘金

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

宝贝,来,满足你,二哥告诉你学 Java 应该买什么书?

发表于 2020-06-28

(这次的标题是不是有点皮,对模仿好朋友 guide 哥的,我也要皮一皮)

高尔基说过,对吧?宝贝们,“书籍是人类进步的阶梯”,不管学什么,买几本心仪的书读一读,帮助还是非常大的。尽管坏书比好书多得多,但只要有幸读到几本好书,就全值了。

Java 要学的知识点非常非常多,但经典的书籍就那么几本,不算多。所以,这里我推荐给你的,能买就赶紧买,别害怕花钱,尤其是遇到屯书的日子,便宜的时候多买点,香啊。

这次推荐的书单也是最近好几个宝贝咨询我的一个问题,“二哥,学 Java 应该买什么书啊?好纠结,你能不能把你私藏的书单全部贡献出来?”

好吧,宝贝,二哥(二叔)这次满足你们,好吧?

1)《Head First Java》

这本书之所以作为首推,就是因为看起来不枯燥,真的。里面插画非常多,有意思的小话题很多,如果对技术图书望而生畏的话,这本书很值得一看。

骚话很多的一本书,只能这么悄悄地告诉你。不只是读死书,你还会玩游戏、拼图、解谜题以及以意想不到的方式与 Java 交互。

2)《Java 核心技术卷 1》

这本书涉及的知识点非常全面,入门的话,只看卷 1 就足够了,卷 2 可以暂时不看。

上面这两本书里面还是会讲到 Swing、AWT、Applet 的内容,都可以略过,感觉这些内容真的没必要讲了,不知道是不是出版社为了凑数,《Java核心技术》都出到第十版了,还有这些内容,真的是醉了。

3)《Java编程思想》

很多老师喜欢把这本书作为入门书推荐,但我觉得实在是不应该,因为新手根本驾驭不了这本书。这本书还是需要一定编程基础的人读的,否则就很容易劝退了。

毕竟讲的是思想,既然是思想,肯定就是在用的基础上进行升华总结,去探讨为什么的层面。那也就是说,只要你靠着前面两本书入了门,那么就需要这本书进行锤炼一下了。

4)《Effective Java》

就说一句,“我很希望 10 年前就拥有这本书。可能有人认为我不需要任何 Java 方面的书籍,但我需要这本”——Java 之父詹姆斯高司令鼎力推荐的书,你就知道要不要读了?

这本书的所有条目都具有指导性建议,对提高你的 Java 编程艺术很有帮助。

5)《阿里巴巴 Java 开发手册》

目前最新版是泰山版,《阿里巴巴 Java 开发手册》属于代码规范级别的开源手册,网上都可以找到下载地址,也可以在「沉默王二」公众号后台回复「手册」获取下载地址。

《Java 开发手册》是每个 Java 程序员都值得拥有的一本参考指南。该手册涵盖了编程规约、异常日志、单元测试、安全规约、MySQL 数据库、工程结构、设计规约灯 7 个部分,参考价值极大。

6)《Java网络编程》

《Java网络编程》是一本 API 书,主要就是对网络编程中的一些类和方法的介绍,但网络编程是 Java 开发中很重要的一块,所以这本书对一个 Java 程序员来说还是很重要的。值得一看。

7)《Java 并发编程实战》

这本书可以称得上是 Java 并发编程方面的圣经了,虽然看起来比较枯燥,但核心知识点都讲到了。从并发和线程安全性的基本概念出发,介绍了如何使用类库来提供基本的并发方案,包括如何利用线程来提高并发应用程序的吞吐量、如何识别可并发执行的任务、如何提高单线程子系统的响应性、如何确保并发程序执行预期的任务,如何提高并发代码的性能和可伸缩性等等内容。

8)《深入浅出 Java 多线程》

这本书是几个阿里朋友合伙重写了一本书,关键是开源的,直接通过链接就可以在线阅读了。

github.com/RedSpider1/…

另外,也可以在「沉默王二」公众号后台回复「并发」获取下载地址。

9)《深入理解 Java 虚拟机》

目前已经出到第三版,我自己在家最近也在重新读,《深入理解 Java 虚拟机》是唯一一本我们国内程序员写的经典书,可以媲美《Java 编程思想》。

必须承认,这本书确实是提升 Java 功力的良药。这本书让我对于虚拟机的运行机理与底层知识真正来了一次近距离接触,许多知识不再只是符号或文字的堆叠,而是一种真正地理解。值得大力推荐。

10)《Java 性能权威指南》

通过前面 9 本书,基本上就把 Java 所有的知识点都学完了,那么就差如何对性能做出优化了,这本书值得拥有。

11)《代码整洁之道》

你现在是不是已经做好了提升编程艺术的准备了,那么这本《代码整洁之道》就可以让你的代码看起来更优雅,更整洁,更像大师写的。软件的质量,不仅依赖于架构,更与代码质量息息相关。而代码的质量与其整洁度成正比关系,越整洁的代码,其质量毫无疑问的就会越高。由于本书中的例子是由 Java 实现的,因此 Java 程序员在读这本书的时候有着天然的优势。

12)《设计模式之禅》

设计模式之禅(第 2 版)》是设计模式领域公认的 3 本经典著作之一,也是我们国内程序员写的一本书,趣味化十足,读起来也非常容易理解。这本书值得所有的程序员读一读,但 Java 程序员读起来更容易上手,因为源码是 Java 完成的。作者名叫秦小波,和我最喜欢的作家王小波同名。

大家都听说过,学习设计模式非常的重要,那么为什么这么重要呢,设计模式到底是什么?打个比喻学编程就像学武功一样。

武功要练得很牛逼,有两样东西不能丢。第一,是内功;第二,是武功秘籍。内功对应到编程就是我们编程基础能力,那编程的设计模式就可以想象成武术中的武功秘籍。

设计模式就是根据不同类型场景,设计优雅的(编码)解决方案。学好设计模式有很多好处,比如,容易看懂经典代码中的逻辑(很多优秀的开源框架大量使用了设计模式);应对面试时对答如流(设计模是面试重点);可以编写出优雅的解决方案(或者代码)。

13)《Spring 实战》

好了,既然要学 Java,想要成为一名称职的 Java 工程师,Spring 就没法忽视,对吧?这本书既可以被刚开始学习 Spring 的读者当作学习指南,也可以被那些想深入了解 Spring 某方面功能的专业用户作为参考用书。

当然,如果你想在 Web 开发方面更上一层楼的话,我再厚着脸皮推荐一下我自己写的《Web 全栈开发进阶之路》,里面也涉及到了一些 Spring 的常用知识点。

14)《Spring Boot+Vue全栈开发实战》

Spring Boot 方面就必须推荐一下我的好朋友江南一点雨的书,关键是他本人录制了很多免费的视频,这些视频配套着他的书看,绝对可以对 Spring Boot 有着充分的掌握。顺带把前端最火的 Vue 入门了,不香吗?

好了好了,书籍整体就先推荐到这吧,足够宝贝你看上一段时间了,加油哦。

如果觉得文章对你有点帮助,请微信搜索「 沉默王二 」第一时间阅读。

本文已收录 GitHub,传送门~ ,里面更有大厂面试完整考点,欢迎 Star。

我是沉默王二,一枚有颜值却靠才华苟且的程序员。关注即可提升学习效率,别忘了三连啊,点赞、收藏、留言,我不挑,嘻嘻。

本文转载自: 掘金

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

Jetpack 新成员 Hilt 实践之 App Start

发表于 2020-06-28

在上一篇文章 Jetpack 新成员 Hilt 实践(一)启程过坑记 分别介绍了 Hilt 的常用注解、以及在实践过程中遇到的一些坑,Hilt 如何 Android 框架类进行绑定,以及他们的生命周期,这篇文章继续讲解 Hilt 的用法,代码已经全部上传到 GitHub:HiltWithAppStartupSimple 如果对你有帮助,请在仓库右上角帮我点个赞。

Hilt 涉及的知识点有点多而且比较难理解,在看本篇文章之前一定要先看一下之前的文章 Jetpack 新成员 Hilt 实践(一),为了节省篇幅,这篇文章将会忽略 Hilt 环境配置的过程等等之前文章已经介绍过的内容。

另外如果想了解 Google 新推出的另外两个 Jetpack 新成员 App Startup 和 Paging3 的实践与原理,可以点击下方链接前去查看。

  • Jetpack 成员 AndroidX App Startup 实践以及原理分析
  • Jetpack 成员 Paging3 数据实践以及源码分析(一)
  • Jetpack 成员 Paging3 网络实践及原理分析(二)
  • Jetpack 成员 Paging3 使用 RemoteMediator 实现加载网络分页数据并更新到数据库中(三)
  • 代码地址:https://github.com/hi-dhl/AndroidX-Jetpack-Practice

通过这篇文章你将学习到以下内容:

  • 什么是注解?
  • @assist 注解和 SavedStateHandle 如何使用?
  • 如何使用 @Binds 注解实现接口注入?
  • @Binds 和 @Provides 的区别?
  • 限定符 @Qualifier 的使用?
+ 自定义限定符 `@qualifers`
+ 预定义的限定符 `@qualifers`
  • 组件作用域 @scopes 如何使用?
  • 如何在 Hilt 不支持的类中执行依赖注入?
+ `Hilt` 如何和 `ContentProvider` 一起使用?
+ `Hilt` 如何和 `App Startup` 一起使用?

Hilt 是基于 Dagger 基础上进行开发的,如果了解 Dagger 朋友们,应该会感觉它们很像,但是与 Dagger 不同的是, Hilt 集成了 Jetpack 库和 Android 框架类,并删除了大部分模板代码,让开发者只需要关注如何进行绑定,而不需要管理所有 Dagger 配置的问题。

在上篇文章已经介绍过, Hilt 如何 Android 框架类进行绑定,以及他们的生命周期,这篇文章将介绍 Hilt 如何和 Jetpack 组件(ViewModel、App Startup)一起绑定,在开始介绍之前我们先来了解一下什么是注解。

什么是注解

之前有小伙伴在 WX 上问过我,对注解不太了解,所以想在这里想简单的提一下。

注解是放在 Java 源码的类、方法、字段、参数前的一种特殊“注释”,注解则可以被编译器打包进入 class 文件,可以在编译,类加载,运行时被读取。

常见的三个注解 @Override、@Deprecated、@SuppressWarnings

  • @Override: 确保子类重写了父类的方法,编译器会检查该方法是否正确地实现。
  • @Deprecated:表示某个类、方法已经过时,编译器会检查,如果使用了过时的方法,会给出提示。
  • @SuppressWarnings:编译器会忽略产生的警告。

Hilt 如何和 ViewModel 一起使用?

在上一篇文章只是简单的介绍了 Hilt 如何和 ViewModel 一起使用,我们继续介绍 ViewModel 的另外一个重要的参数 SavedStateHandle,首先需要添加依赖。

在 App 模块中的 build.gradle 文件中添加以下代码。

1
2
arduino复制代码implementation 'androidx.hilt:hilt-lifecycle-viewmodel:1.0.0-alpha01'
kapt 'androidx.hilt:hilt-compiler:1.0.0-alpha01'

koltin 使用 kapt, Java 使用 annotationProcessor。

注意: 这个是在 Google 文档上没有提到的,如果使用的是 kotlin 的话需要额外在 App 模块中的 build.gradle 文件中添加以下代码,否则调用 by viewModels() 会编译不过。

1
2
3
4
ini复制代码// For Kotlin projects
kotlinOptions {
jvmTarget = "1.8"
}

在 ViewModel 的构造函数中使用 @ViewModelInject 注解提供一个 ViewModel,如果需要用到 SavedStateHandle,需要使用 @assist 注解添加 SavedStateHandle 依赖项,代码如下所示。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
kotlin复制代码class HiltViewModel @ViewModelInject constructor(
private val tasksRepository: Repository,
//SavedStateHandle 用于进程被终止时,保存和恢复数据
@Assisted private val savedStateHandle: SavedStateHandle
) : ViewModel() {

// getLiveData 方法会取得一个与 key 相关联的 MutableLiveData
// 当与 key 相对应的 value 改变时 MutableLiveData 也会更新。
private val _userId: MutableLiveData<String> = savedStateHandle.getLiveData(USER_KEY)

// 对外暴露不可变的 LiveData
val userId: LiveData<String> = _userId

companion object {
private val USER_KEY = "userId"
}
}

将用户的 userId 存储在 SavedStateHandle 中,当进程被终止时保存和恢复对应的数据。

SavedStateHandle 是什么?SavedStateHandle 为了解决什么问题?

Activity 和 Fragment 通常会在下面三种情况下被销毁(以下内容来自 Google):

  • 从当前界面永久离开: 用户导航至其他界面或直接关闭 Activity (通过点击返回按钮或执行的操作调用了 finish() 方法)。对应 Activity 实例被永久关闭。
  • Activity 配置 (configuration) 被改变: 例如旋转屏幕等操作,会使 Activity 需要立即重建。
  • 应用在后台时,其进程被系统杀死: 这种情况发生在设备剩余运行内存不足,系统又需要释放一些内存的时候,当进程在后台被杀死后,用户又返回该应用时 Activity 需要被重建。

ViewModel 会帮您处理第二种情况,因为在这种情况下 ViewModel 没有被销毁,而在第三种情况下,ViewModel 被销毁了, 当进程在后台被杀死后,则需要使用 onSaveInstanceState() 作为备用保存数据的方式。

SavedStateHandle 的出现就是为了解决 App 进程终止保存和恢复数据问题,ViewModel 不需要向 Activity 发送和接收状态。相反的,现在可以在 ViewModel 中处理保存和恢复数据。

SavedStateHandle 类似于一个 Bundle,它是数据的键-值映射,这个 SavedStateHandle 包含在 ViewModel 中,它在后台进程终止时仍然存在,以前保存在 onSaveInstanceState() 中的任何数据现在都可以保存在 SavedStateHandle 中。

使用 @Binds 注解实现接口注入?

注入接口实例有两种方式分别使用注解 @Binds 和 @Provides,@Provides 的方式在上一篇文章 Jetpack 新成员 Hilt 实践(一)启程过坑记 Hilt 如何和 Room 一起使用 和 Hilt 如何和第三方组件一起使用 都有介绍,这里我们来介绍如何使用注解 @Binds。

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
kotlin复制代码interface WorkService {
fun init()
}

/**
* 注入构造函数,因为 Hilt 需要知道如何提供 WorkServiceImpl 的实例
*/
class WorkServiceImpl @Inject constructor() :
WorkService {

override fun init() {
Log.e(TAG, " I am an WorkServiceImpl")
}

}

@Module
@InstallIn(ApplicationComponent::class)
// 这里使用了 ActivityComponent,因此 WorkServiceModule 绑定到 ActivityComponent 的生命周期。
abstract class WorkServiceModule {

/**
* @Binds 注解告诉 Hilt 需要提供接口实例时使用哪个实现
*
* bindAnalyticsService 函数需要为 Hilt 提供了以下信息
* 1. 函数返回类型告诉 Hilt 提供了哪个接口的实例
* 2. 函数参数告诉 Hilt 提供哪个实现
*/
@Binds
abstract fun bindAnalyticsService(
workServiceImpl: WorkServiceImpl
): WorkService
}

使用注解 @Binds 时,需要提供以下两个信息:

  • 函数参数告诉 Hilt 接口的实现类,例如参数 WorkServiceImpl 是接口 WorkService 的实现类。
  • 函数返回类型告诉 Hilt 提供了哪个接口的实例。

注解 @Binds 和 注解 @Provides 的区别?

  • @Binds:需要在方法参数里面明确指明接口的实现类。
  • @Provides:不需要在方法参数里面明确指明接口的实现类,由第三方框架实现,通常用于和第三方框架进行绑定(Retrofit、Room 等等)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
kotlin复制代码// 有自己的接口实现
@Binds
abstract fun bindAnalyticsService(
workServiceImpl: WorkServiceImpl
): WorkService

// 没有自己的接口实现
@Provides
fun providePersonDao(application: Application): PersonDao {
return Room
.databaseBuilder(application, AppDataBase::class.java, "dhl.db")
.fallbackToDestructiveMigration()
.allowMainThreadQueries()
.build().personDao()
}

@Provides
fun provideGitHubService(): GitHubService {
return Retrofit.Builder()
.baseUrl("https://api.github.com/")
.addConverterFactory(GsonConverterFactory.create())
.build().create(GitHubService::class.java)
}

限定符 @Qualifier 注解的使用

来自 Google:@Qualifier 是一种注解,当类型定义了多个绑定时,使用它来标识该类型的特定绑定。

换句话说 @Qualifier 声明同一个类型,可以在多处进行绑定,我将限定符分为两种。

  1. 自定义限定符
  2. 预定义限定符

自定义限定符的使用

我们先用注解 @Qualifier 声明两个不同的实现。

1
2
3
4
5
6
7
8
9
less复制代码// 为每个声明的限定符,提供对应的类型实例,和 @Binds 或者 @Provides 一起使用
@Qualifier
// @Retention 定义了注解的生命周期,对应三个值(SOURCE、BINARY、RUNTIME)
@Retention(AnnotationRetention.BINARY)
annotation class RemoteTasksDataSource // 注解的名字,后面直接使用它

@Qualifier
@Retention(AnnotationRetention.RUNTIME)
annotation class LocalTasksDataSource
  • @Qualifier :为每个声明的限定符,提供对应的类型实例,和 @Binds 或者 @Provides 一起使用
  • @Retention:定义了注解的生命周期,对应三个值(SOURCE、BINARY、RUNTIME)
+ `AnnotationRetention.SOURCE`:仅编译期,不存储在二进制输出中。
+ `AnnotationRetention.BINARY`:存储在二进制输出中,但对反射不可见。
+ `AnnotationRetention.RUNTIME`:存储在二进制输出中,对反射可见。

通常我们自定义的注解都是 RUNTIME,所以务必要加上@Retention(RetentionPolicy.RUNTIME) 这个注解

来看一下 @Qualifier 和 @Provides 一起使用的例子,定义了两个方法,具有相同的返回类型,但是实现不同,限定符将它们标记为两个不同的绑定。

1
2
3
4
5
6
7
8
9
10
11
12
13
less复制代码@Singleton
@RemoteTasksDataSource
@Provides
fun provideTasksRemoteDataSource(): DataSource { // 返回值相同
return RemoteDataSource() // 不同的实现
}

@Singleton
@LocalTasksDataSource
@Provides
fun provideTaskLocalDataSource(appDatabase: AppDataBase): DataSource { // 返回值相同
return LocalDataSource(appDatabase.personDao()) // 不同的实现
}

当我们声明完 @Qualifier 注解之后,就可以使用声明的两个 @Qualifier,来看个例子,定义一个 Repository 构造方法里面传入用 @Qualifier 注解声明的两个不同实现。

1
2
3
4
5
6
7
8
9
10
11
less复制代码@Singleton
@Provides
fun provideTasksRepository(
@LocalTasksDataSource localDataSource: DataSource,
@RemoteTasksDataSource remoteDataSource: DataSource
): Repository {
return TasksRepository(
localDataSource,
remoteDataSource
)
}

provideTasksRepository 方法内,传入的参数都是 DataSource,但是前面用 @Qualifier 注解声明了它们不同的实现。

预定义限定符

Hilt 提供了一些预定义限定符,例如你可能在不同的情况下需要不同的 Context(Appliction、Activity)Hilt 提供了 @ApplicationContext 和 @ActivityContext 两种限定符。

1
2
3
4
5
6
less复制代码class HiltViewModel @ViewModelInject constructor(
@ApplicationContext appContext: Context,
@ActivityContext actContext: Context,
private val tasksRepository: Repository,
@Assisted private val savedStateHandle: SavedStateHandle
)

组件作用域 @scopes 的使用

默认情况下,Hilt 中的所有绑定都是无作用域的,这意味着每次应用程序请求绑定时,Hilt 都会创建一个所需类型的新实例。

@scopes 的作用在指定作用域范围内(Application、Activity 等等) 提供相同的实例。

Hilt 还允许将绑定的作用域限定到特定组件,Hilt 只为绑定作用域到的组件的每个实例创建一次范围绑定,所有绑定请求共享同一个实例,我们来看一例子。

1
2
3
less复制代码@Singleton
class HiltSimple @Inject constructor() {
}

HiltSimple 用 @Singleton 声明了其作用域,那么在 Application 范围内提供相同的实例,代码如下所示,大家可以运行 Demo 看一下输出结果。

1
2
kotlin复制代码MainActivity: com.hi.dhl.hilt.appstartup.di.HiltSimple@8f75417
HitAppCompatActivity: com.hi.dhl.hilt.appstartup.di.HiltSimple@8f75417

注意:绑定组件范围可能非常的昂贵,因为提供的对象会保留在内存中,直到该组件被销毁,应该尽量减少在应用程序中使用绑定组件范围,对于要求在一定范围内使用同一实例的绑定,或者对于创建成本高昂的绑定,使用组件范围的绑定是合适的。

下表列出了每个生成组件的 scope 注解对应的范围。

Android class Generated component Scope
Application ApplicationComponent @Singleton
View Model ActivityRetainedComponent @ActivityRetainedScope
Activity ActivityComponent @ActivityScoped
Fragment FragmentComponent @FragmentScoped
View ViewComponent @ViewScoped
View annotated with @WithFragmentBindings ViewWithFragmentComponent @ViewScoped
Service ServiceComponent @ServiceScoped

在 Hilt 不支持的类中执行依赖注入

Hilt 支持最常见的 Android 类 Application、Activity、Fragment、View、Service、BroadcastReceiver 等等,但是您可能需要在 Hilt 不支持的类中执行依赖注入,在这种情况下可以使用 @EntryPoint 注解进行创建,Hilt 会提供相应的依赖。

@EntryPoint:可以使用 @EntryPoint 注解创建入口点,@EntryPoint 允许 Hilt 使用 Hilt 无法在依赖中提供依赖的对象。

例如 Hilt 不支持 ContentProvider,如果你在想在 ContentProvider 中获取 Hilt 提供的依赖,你可以定义一个接口,并添加 @EntryPoint 注解,然后添加 @InstallIn 注解指定 module 的范围,代码如下所示。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
kotlin复制代码@EntryPoint
@InstallIn(ApplicationComponent::class)
interface InitializerEntryPoint {

fun injectWorkService(): WorkService

companion object {
fun resolve(context: Context): InitializerEntryPoint {

val appContext = context.applicationContext ?: throw IllegalStateException()
return EntryPointAccessors.fromApplication(
appContext,
InitializerEntryPoint::class.java
)
}
}
}

使用 EntryPointAccessors 提供四个静态方法进行访问,分别是 fromActivity、fromApplication、fromFragment、fromView 等等

EntryPointAccessors 提供四个静态方法,第一个参数是 @EntryPoint 接口上 @InstallIn 注解指定 module 的范围,我们在接口 InitializerEntryPoint 用 @InstallIn 注解指定 module 的范围是 ApplicationComponent,所以我们应该使用 EntryPointAccessors 提供的静态方法 fromApplication。

1
2
3
4
5
6
7
8
9
10
11
kotlin复制代码class WorkContentProvider : ContentProvider() {

override fun onCreate(): Boolean {
context?.run {
val service = InitializerEntryPoint.resolve(this).injectWorkService()
Log.e(TAG, "WorkContentProvider ${service.init()}")
}
return true
}
......
}

在 ContentProvider 中调用 EntryPointAccessors 类中的 fromApplication 方法就可以获取到 Hit 提供的依赖。

Hilt 如何和 App Startup 一起使用

App Startup 会默认提供一个 InitializationProvider,InitializationProvider 继承 ContentProvider,那么 Hilt 在 App Startup 中使用的方式和 ContentProvider 一样。

1
2
3
4
5
6
7
8
9
10
11
12
13
kotlin复制代码class AppInitializer : Initializer<Unit> {


override fun create(context: Context): Unit {
val service = InitializerEntryPoint.resolve(context).injectWorkService()
Log.e(TAG, "AppInitializer ${service.init()}")
return Unit
}

override fun dependencies(): MutableList<Class<out Initializer<*>>> =
mutableListOf()

}

通过调用 EntryPointAccessors 的静态方法,获取到 Hit 提供的依赖,关于 App Startup 如何使用可以查看这篇文章 Jetpack 最新成员 AndroidX App Startup 实践以及原理分析

总结

到这里关于 Hilt 的注解使用都介绍完了,代码已经全部上传到了 GitHub:HiltWithAppStartupSimple。

HiltWithAppStartupSimple 包含了本篇文章和 Jetpack 新成员 Hilt 实践(一)启程过坑记 文章中使用的案例,如果之前没有看过可以先去了解一下,之后看代码会更加的清楚。

Hilt 是基于 Dagger 基础上进行开发的,入门要比 Dagger 简单很多,不需要去管理所有的 Dagger 的配置问题,但是其入门的门槛还是很高的,尤其是 Hilt 的注解,需要了解其每个注解的含义才能正确的使用,避免资源的浪费。

这篇文章和之前 Jetpack 新成员 Hilt 实践(一)启程过坑记 的文章其中很多案例都重新去设计了,因为 Google 的提供的案例,确实很难让人理解,希望这两篇文章可以帮助小伙伴们快速入门 Hilt,后面还会有更多实战案例。

计划建立一个最全、最新的 AndroidX Jetpack 相关组件的实战项目 以及 相关组件原理分析文章,正在逐渐增加 Jetpack 新成员,仓库持续更新,可以前去查看:AndroidX-Jetpack-Practice, 如果这个仓库对你有帮助,请在仓库右上角帮我点个赞,后面我会陆续完成更多 Jetpack 新成员的项目实践。

结语

致力于分享一系列 Android 系统源码、逆向分析、算法、翻译、Jetpack 源码相关的文章,正在努力写出更好的文章,如果这篇文章对你有帮助给个 star,一起来学习,期待与你一起成长。

算法

由于 LeetCode 的题库庞大,每个分类都能筛选出数百道题,由于每个人的精力有限,不可能刷完所有题目,因此我按照经典类型题目去分类、和题目的难易程度去排序。

  • 数据结构: 数组、栈、队列、字符串、链表、树……
  • 算法: 查找算法、搜索算法、位运算、排序、数学、……

每道题目都会用 Java 和 kotlin 去实现,并且每道题目都有解题思路、时间复杂度和空间复杂度,如果你同我一样喜欢算法、LeetCode,可以关注我 GitHub 上的 LeetCode 题解:Leetcode-Solutions-with-Java-And-Kotlin,一起来学习,期待与你一起成长。

Android 10 源码系列

正在写一系列的 Android 10 源码分析的文章,了解系统源码,不仅有助于分析问题,在面试过程中,对我们也是非常有帮助的,如果你同我一样喜欢研究 Android 源码,可以关注我 GitHub 上的 Android10-Source-Analysis,文章都会同步到这个仓库。

  • 0xA01 Android 10 源码分析:APK 是如何生成的
  • 0xA02 Android 10 源码分析:APK 的安装流程
  • 0xA03 Android 10 源码分析:APK 加载流程之资源加载
  • 0xA04 Android 10 源码分析:APK 加载流程之资源加载(二)
  • 0xA05 Android 10 源码分析:Dialog 加载绘制流程以及在 Kotlin、DataBinding 中的使用
  • 0xA06 Android 10 源码分析:WindowManager 视图绑定以及体系结构
  • 0xA07 Android 10 源码分析:Window 的类型 以及 三维视图层级分析
  • 更多……

Android 应用系列

  • 如何在项目中封装 Kotlin + Android Databinding
  • 再见吧 buildSrc, 拥抱 Composing builds 提升 Android 编译速度
  • 为数不多的人知道的 Kotlin 技巧以及 原理解析
  • Jetpack 最新成员 AndroidX App Startup 实践以及原理分析
  • Jetpack 成员 Paging3 实践以及源码分析(一)
  • Jetpack 新成员 Paging3 网络实践及原理分析(二)
  • Jetpack 新成员 Hilt 实践(一)启程过坑记

精选译文

目前正在整理和翻译一系列精选国外的技术文章,不仅仅是翻译,很多优秀的英文技术文章提供了很好思路和方法,每篇文章都会有译者思考部分,对原文的更加深入的解读,可以关注我 GitHub 上的 Technical-Article-Translation,文章都会同步到这个仓库。

  • [译][Google工程师] 刚刚发布了 Fragment 的新特性 “Fragment 间传递数据的新方式” 以及源码分析
  • [译][Google工程师] 详解 FragmentFactory 如何优雅使用 Koin 以及部分源码分析
  • [译][2.4K Start] 放弃 Dagger 拥抱 Koin
  • [译][5k+] Kotlin 的性能优化那些事
  • [译] 解密 RxJava 的异常处理机制
  • [译][1.4K+ Star] Kotlin 新秀 Coil VS Glide and Picasso
  • 更多……

工具系列

  • 为数不多的人知道的 AndroidStudio 快捷键(一)
  • 为数不多的人知道的 AndroidStudio 快捷键(二)
  • 关于 adb 命令你所需要知道的
  • 10分钟入门 Shell 脚本编程
  • 基于 Smali 文件 Android Studio 动态调试 APP
  • 解决在 Android Studio 3.2 找不到 Android Device Monitor 工具

本文转载自: 掘金

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

分布式会话 分布式会话

发表于 2020-06-27

分布式会话

一 什么是会话

会话Session代表的是客户端与服务器的一次交互过程,这个过程可以是连续的也可以是时断时续的,曾经的Servlet时代,一旦用户与服务端交互,服务器tomcat就会为用户创建一个session,同时前端会有一个jsessionid,每次交互都会携带该id,如此一来,服务器只要在接到用户请求后,就可以拿到jsessionid,并根据这个ID在内存中找到相应的会话session,当拿到session后就可以操作会话了,会话存活期间,我们就认为用户一致处于正在使用网站的状态,一旦session超期过时,那么就可以认为用户已经离开了王好赞,停止交互了。用户的身份信息,我们也是通过session来判断的,在session中可以保存不同用户的信息。

session的使用,代码如下:

1
2
3
4
5
6
7
8
复制代码@GetMapping("/setSession")
public Object setSession(HttpServletRequest request) {
HttpSession session = request.getSession();
session.setAttribute("userInfo", "new user");
session.setMaxInactiveInterval(3600);
session,getAttribute("userInfo");
session.removeAttribute("userInfo");
}

二 无状态会话

HTTP请求是无状态的,用户向服务端发起多个请求,服务端并不会知道这多次请求都来自同一个用户,这就是无状态。cookie的出现就是为了有状态的记录用户。

常见的,iOS与服务端交互,安卓与服务端交互,前后端分离,小程序与服务端交互,它们都是通过发起http来调用接口数据的,每次交互服务端都不会拿到客户端的状态,但是我们可以通过手段去处理,比如每次请求的时候携带一个userid或者user-token,如此一来,就能让服务端根据用户id或token来获取相应的数据,每个用户的下一次请求都能被服务端识别来自哪个用户。

三 有状态会话

Tomcat的会话就是有状态的,一旦用户和服务端交互,就有会话,会话保存了用户的信息,这样用户就有状态了,服务端会和每个客户端都保持这样的一层关系,这个由容器来管理(也就是tomcat),这个session会话是保存到内存空间里的,如此一来,当不同的用户访问服务端,服务端就能通过会话知道谁是谁了。如果用户不再和服务端交互,那么会话则消失,结束了他的生命周期,如此一来,每个用户其实都会有一个会话被维护。这就是有状态会话

场景: 在传统项目或者jsp项目中使用最多的session都是有状态的,session的存在就是为了弥补http的无状态。


⚠️ tomcat会话可以通过手段实现多系统之间的状态同步,但是会损耗一定的时间,一旦发生同步那么用户请求就会等待,这种做法不可取

”

四 为何使用无状态会话

有状态会话都是放在服务器的内存中的,一旦用户会话量多,那么内存就会出现瓶颈,而无状态会话可以采用介质,前端可以使用cookie(app可以使用缓存)保存用户的id或token, 后端比如redis,相应的会话都能放入redis中进行管理,如此,多应用部署的服务就不会造成内存压力。用户在前端发起http请求,携带id或token,如此,服务端能够根据前端提供的id或token来识别用户,可伸缩性就更强了。

五 单Tomcat会话

先来看一下单个tomcat会话,这个就是有状态的,用户首次访问服务端,这个时候会话产生,并且会设置jsessionid放入cookie中,后续每次请求都会携带jsessionid以保持用户状态。

image-20200627225850524

image-20200627225850524

六 前后端分离会话

用户请求服务端,由于前后端分离,前端发起http请求,不会携带任何状态,当用户第一次请求以后,我们手动设置一个token,作为用户会话,放入redis中, 如此作为redis-session,并且这个token设置后放入前端cookie中(app或者小程序可以放入本地缓存Local Storage中),如此后续交互过程中,前端只需要传递token给后端,后端就能识别这个用户请求来自谁了。

image-20200627230632018

image-20200627230632018

七 集群分布式系统会话

集群或分布式系统的本质是多个系统,假设这里有两个服务器节点,分别是A和B, 它们可以是集群,也可以是分布式系统,一开始用户和A系统交互,那么这个时候的用户状态,我们可以保存到redis中,作为A系统的会话信息,随后用户请求进入B系统,那么B系统中的会话我们也同样和redis关联,如此AB系统的session就统一了。当然cookie是会随着用户的访问携带过来的,那么这个其实就是分布式会话,通过redis来保存用户的状态。

image-20200627231048686

image-20200627231048686

本文转载自: 掘金

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

1…799800801…956

开发者博客

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