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

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


  • 首页

  • 归档

  • 搜索

Microsoft 开源 eBPF for Windows

发表于 2021-05-12

Microsoft 发布了新的开源项目 eBPF for Windows,以使 eBPF 在 Windows 10 和 Windows Server 2016 及更高版本上工作。eBPF 是kernel 3.15 中引入的新设计,可以在不改变内核源代码或加载内核模块的情况下,在Linux内核中运行沙盒程序。

下文来自 微软官方公告

eBPF 是一项众所周知的革命性技术–提供可编程性、可扩展性和敏捷性。eBPF已被应用于拒绝服务保护和可观察性等用例。随着时间的推移,围绕eBPF已经建立了一个重要的工具、产品和经验的生态系统。尽管对eBPF的支持首先是在Linux内核中实现的,但人们对允许eBPF在其他操作系统上使用的兴趣越来越大,而且除了内核之外,还可以扩展用户模式服务和守护程序。

今天,我们很高兴地宣布了一个新的微软开源项目,以使eBPF在Windows 10和Windows Server 2016及更高版本上工作。ebpf-for-windows项目旨在让开发者在现有版本的Windows之上使用熟悉的eBPF工具链和应用编程接口(API)。在其他人的工作基础上,该项目采用了几个现有的eBPF开源项目,并添加了 “胶水”,使其在Windows上运行。

我们现在宣布这个项目,因为我们的目标是与强大的eBPF社区合作,以确保eBPF在Windows和其他地方都能正常运行。

架构概述

下图说明了本项目的架构和相关组件。

image.png

如图所示,现有的eBPF工具链,如clang,可用于从各种语言的源代码生成eBPF字节码。然后,生成的字节码可以被任何应用程序使用,或者通过Windows netsh命令行工具手动使用,这两个工具都使用一个暴露Libbpf API的共享库,尽管这项工作仍在进行中。

该库将eBPF字节码发送到一个静态验证器(PREVAIL验证器),该验证器托管在一个用户模式保护进程中,这是一个Windows安全环境,允许内核组件信任一个由其信任的密钥签署的用户模式守护程序。如果字节码通过了验证器的所有安全检查,该字节码可以被加载到运行在Windows内核模式执行上下文中的uBPF解释器中,或者由uBPF即时编译器(JIT)进行编译,并将本地代码加载到内核模式执行上下文中。

安装到内核模式执行上下文的eBPF程序可以附加到各种钩子上,以处理事件和调用eBPF shim暴露的各种辅助API,它在内部包装了公共的Windows内核API,允许在现有版本的Windows上使用eBPF。到目前为止,已经添加了两个钩子(XDP和socket bind),虽然这些是网络专用的钩子,但我们希望随着时间的推移,将添加更多的钩子和助手,而不仅仅是与网络有关的_。_

这是eBPF的一个分支吗?

简而言之,不是。

eBPF for Windows项目利用了现有的开源项目,包括IOVisor uBPF项目和PREVAIL验证器,通过为该代码添加Windows特定的托管环境,在Windows之上运行它们。

这是否提供了与为Linux编写的eBPF程序的应用兼容性?

其目的是为使用通用钩子和助手的代码提供源代码兼容性,这些钩子和助手适用于整个操作系统的生态系统。

Linux提供了许多钩子和助手,其中一些是非常具体的Linux(使用Linux内部数据结构,例如),将不适用于其他平台。其他的钩子和助手是普遍适用的,目的是为了支持他们的eBPF程序。

同样,eBPF for Windows项目暴露了Libbpf APIs,为与eBPF程序交互的应用程序提供源代码兼容性。

了解更多和贡献

ebpf-for-windows项目为Windows用户带来了eBPF的力量,并打算最终驻扎在eBPF生态系统中一个社区管理的基础。有了你的投入和帮助,我们可以达到这个目标。

请联系我们或在GitHub上创建一个问题。我们很高兴能继续完善和扩展ebpf-for-windows,使每个人都能从这个项目中受益。我们渴望看到你对这个项目的发现以及它的发展。

本文转载自: 掘金

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

推荐一个 Nginx 可视化配置神器

发表于 2021-05-12

Nginx 是前后端开发工程师必须掌握的神器。该神器有很多使用场景,比如反向代理、负载均衡、动静分离、跨域等等。

把 Nginx 下载下来,打开 conf 文件夹的 nginx.conf 文件,Nginx 服务器的基础配置和默认的配置都存放于此。

配置是让程序员非常头疼的事,比如 Java 后端框架 SSM ,大量配置文件让不少人头皮发麻,所以才涌现了 Spring Boot 这样能简化配置的框架。

如果能够采用可视化的方式对 Nginx 进行配置,那该多好。老逛在 GitHub 上发现了一款可以一键生成 Nginx 配置的神器,相当给力。

先来看看它都支持什么功能的配置:反向代理、HTTPS、HTTP/2、IPv6, 缓存、WordPress、CDN、Node.js 支持、 Python (Django) 服务器等等。

关于Java项目整理了100+Java项目教程+源码+笔记,地址:100+个Java项目教程+源码+笔记

如果你想在线进行配置,只需要打开网站:nginxconfig.io/,按照自己的需求进行操…

选择你的场景,填写好参数,系统就会自动生成配置文件。

项目地址:github.com/digitalocea…

网站:www.digitalocean.com/community/t…

本文转载自: 掘金

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

git提交忽略不必要的文件或文件夹 Java Debug

发表于 2021-05-12

本文正在参加「Java主题月 - Java Debug笔记活动」,详情查看<活动链接>

Git 作为开发者而言,是再熟悉不过的工具了,但意向的困扰你肯定遇到过。

在使用 Git 用来管理代码后,如果采用 eclipse 中 git 插件,可以更加便利的在eclipse 中进行代码的提交及更新操作。对于创建的 maven 项目而言,本地工程项目中往往会有一些本地项目配置文件,而这些配置文件却是不需要提交至版本库的,如 maven 项目的 target 文件夹、bin 文件夹等。如下图,实际开发中我们只需提交:src、pom.xml、.gitignore等。

(.gitignore是用来配置忽略文件的配置文件,建议一同提交至版本库,便于其他开发人员使用)

面对上述这种情况,如何做到只提交需要关注提交的文件,而忽略不必要的文件或文件夹,防止本地不必要的文件提交至版本库呢?

Git 忽略不必要文件有2种方法,一种是命令方法,一种是 eclipse 安装 git 插件设置。

方法一:配置文件方法

  1. 首先在仓库中创建隐藏文件“.gitignore”,选中本地仓库,右击“Git Bash Here”,然后执行如下命令:
1
bash复制代码touch .gitignore

※项目中一般会自动生成该配置文件

强烈推荐使用配置文件方式,便于其他人使用。

  1. 用文本编辑器如editplus或notepad++输入需要忽略的文件或文件名,如下所示:
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
markdown复制代码##ignore this file##
/target/
.classpath
.project
.settings
##filter databfile、sln file##
*.mdb
*.ldb
*.sln
##class file##
*.com
*.class
*.dll
*.exe
*.o
*.so
# compression file
*.7z
*.dmg
*.gz
*.iso
*.jar
*.rar
*.tar
*.zip
*.via
*.tmp
*.err
# OS generated files #
.DS_Store
.DS_Store?
._*
.Spotlight-V100
.Trashes
Icon?
ehthumbs.db
Thumbs.db

备注:

/target/:过滤文件设置,表示过滤这个文件夹

*.mdb ,*.ldb ,*.sln:表示过滤某种类型的文件

/mtk/do.c ,/mtk/if.h:表示指定过滤某个文件下具体文件

!*.c , !/dir/subdir/:! 开头表示不过滤

*.[oa] 支持通配符:过滤repo中所有以.o或者.a为扩展名的文件

该方法保证任何人都提交不了这类文件。

方法二:eclipse配置

eclipse 安装 Git 插件设置:点击“Add Pattern”添加你要过滤的文件,该方法只能保证本地提交过滤,但是提交远程仓库则不会。

本文转载自: 掘金

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

Spark-Spark Streaming(1)-- 入门案

发表于 2021-05-11

1、概览

Spark Streaming 是核心 Spark API 的扩展,它支持对实时数据流进行可伸缩的、高吞吐量的、容错的流处理。数据可以从 Kafka、 Kinesis 或 TCP sockets 等许多来源获取,也可以使用 map、 reduce、 join 和 window 等高级函数表示的复杂算法进行处理。最后,可以将处理过的数据推送到文件系统、数据库和实时仪表板。事实上,您可以将 Spark 的机器学习和图形处理算法应用于数据流。

image

在内部,它的工作原理如下。Spark Streaming 接收实时输入数据流并将数据分批,然后由 Spark 引擎处理以批量生成最终结果流。
image

Spark Streaming 提供了一种称为离散化流(discreated stream)或 DStream 的高级抽象,它表示一个连续的数据流。可以从 Kafka 和 Kinesis 等源的输入数据流创建 DStreams,也可以对其他 DStreams 应用高级操作。在内部,DStream 表示为 rdd 序列。

本指南向您展示如何开始使用 DStreams 编写 Spark Streaming 程序。您可以用 Scala、 Java 或 Python (在 Spark 1.2中引入)编写 Spark Streaming 程序,所有这些在本指南中都有介绍。您将在本指南中找到可以在不同语言的代码片段之间进行选择的选项卡。

注意: 在 Python 中有一些 api 是不同的或者不可用的。在本指南中,您将发现 pythonapi 标记突出显示了这些差异。

2、一个简单的例子

在我们详细介绍如何编写自己的 Spark Streaming 程序之前,让我们快速了解一下简单的 Spark Streaming 程序是什么样的。假设我们要计算从侦听 TCP 套接字的数据服务器接收的文本数据中的字数。你所需要做的就是:。

首先,我们将 Spark Streaming 类的名称和 StreamingContext 中的一些隐式转换导入到环境中,以便将有用的方法添加到我们需要的其他类中(如 DStream)。 Streamingcontext 是所有流功能的主要入口点。 我们用两个执行线程创建一个本地 StreamingContext,批处理间隔为1秒。

1
2
3
4
5
6
7
8
9
10
scala复制代码import org.apache.spark._
import org.apache.spark.streaming._
import org.apache.spark.streaming.StreamingContext._ // 从Spark 1.3开始就没有必要了

// a. 创建SparkConf对象,设置应用配置信息
val sparkConf: SparkConf = new SparkConf()
.setMaster("local[2]")
.setAppName(this.getClass.getSimpleName.stripSuffix("$"))
// b. 传递SparkConf对象,构建流式上下文对象, TODO: 时间间隔 -> 用于划分流式数据为很多批次Batch
val context: StreamingContext = new StreamingContext(sparkConf, Seconds(BATCH_INTERVAL))

使用这个context,我们可以创建一个 DStream,它表示来自 TCP 源的流数据,指定为主机名(如 localhost)和端口(如9999)。

1
2
scala复制代码// Create a DStream that will connect to hostname:port, like localhost:9999
val lines = ssc.socketTextStream("localhost", 9999)

这个DStream类型的 lines 表示将从数据服务器接收的数据流。此 DStream 中的每个记录都是一行文本。接下来,我们要将行按空格字符拆分为单词。

1
2
scala复制代码// Split each line into words
val words = lines.flatMap(_.split(" "))

flatMap 是一个一对多的 DStream 操作,它通过从源 DStream 中的每个记录生成多个新记录来创建一个新的 DStream。在这种情况下,每一行将被分割为多个单词,单词流将表示为单词 DStream。接下来,我们要统计这些单词。

1
2
3
4
5
6
7
scala复制代码import org.apache.spark.streaming.StreamingContext._ // not necessary since Spark 1.3
// Count each word in each batch
val pairs = words.map(word => (word, 1))
val wordCounts = pairs.reduceByKey(_ + _)

// Print the first ten elements of each RDD generated in this DStream to the console
wordCounts.print()

words DStream 进一步被映射(一对一的转换)到一个(word,1)的 pairs DStream,然后将其简化以获得每批数据中单词的频率。最后,wordCounts.print () 将每秒打印一些生成的计数。

请注意,当这些行被执行时,Spark Streaming 只设置它在启动时将执行的计算,而且还没有真正的处理开始。要在设置完所有转换之后启动处理,我们最后调用

1
2
scala复制代码ssc.start()             // Start the computation
ssc.awaitTermination() // Wait for the computation to terminate

如下是完整的代码

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
scala复制代码/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

// scalastyle:off println
package org.apache.spark.examples.streaming

import org.apache.spark.SparkConf
import org.apache.spark.storage.StorageLevel
import org.apache.spark.streaming.{Seconds, StreamingContext}

/**
* Counts words in UTF8 encoded, '\n' delimited text received from the network every second.
*
* Usage: NetworkWordCount <hostname> <port>
* <hostname> and <port> describe the TCP server that Spark Streaming would connect to receive data.
*
* To run this on your local machine, you need to first run a Netcat server
* `$ nc -lk 9999`
* and then run the example
* `$ bin/run-example org.apache.spark.examples.streaming.NetworkWordCount localhost 9999`
*/
object NetworkWordCount {
def main(args: Array[String]): Unit = {
if (args.length < 2) {
System.err.println("Usage: NetworkWordCount <hostname> <port>")
System.exit(1)
}

StreamingExamples.setStreamingLogLevels()

// Create the context with a 1 second batch size
val sparkConf = new SparkConf().setAppName("NetworkWordCount")
val ssc = new StreamingContext(sparkConf, Seconds(1))

// Create a socket stream on target ip:port and count the
// words in input stream of \n delimited text (e.g. generated by 'nc')
// Note that no duplication in storage level only for running locally.
// Replication necessary in distributed scenario for fault tolerance.
val lines = ssc.socketTextStream(args(0), args(1).toInt, StorageLevel.MEMORY_AND_DISK_SER)
val words = lines.flatMap(_.split(" "))
val wordCounts = words.map(x => (x, 1)).reduceByKey(_ + _)
wordCounts.print()
ssc.start()
ssc.awaitTermination()
}
}
// scalastyle:on println

如果您已经下载并构建了 Spark,则可以按照以下方式运行此示例。首先需要使用以下命令将 Netcat (大多数类 unix 系统中的一个小实用程序)作为数据服务器运行

$ nc -lk 9999
然后,在另一个终端中,您可以使用

$ ./bin/run-example streaming.NetworkWordCount localhost 9999
然后,在运行 netcat 服务器的终端中键入的任何行都将计数并每秒钟在屏幕上打印一次。它看起来像下面这样。
image

本文转载自: 掘金

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

深入讲解Spring循环依赖以及三级缓存,结合实战案例讲的明

发表于 2021-05-11

三级缓存

注意,三级缓存这个叫法是国内的叫法,Spring官方没有任何三级缓存相关的叫法,这个三级缓存的叫法来源于三个成员变量:
org.springframework.beans.factory.support.DefaultSingletonBeanRegistry

1
2
3
4
5
6
7
8
swift复制代码/** Cache of singleton objects: bean name to bean instance. 一级缓存*/
private final Map<String, Object> singletonObjects = new ConcurrentHashMap<>(256);

/** Cache of early singleton objects: bean name to bean instance. 二级缓存*/
private final Map<String, Object> earlySingletonObjects = new HashMap<>(16);

/** Cache of singleton factories: bean name to ObjectFactory. 三级缓存*/
private final Map<String, ObjectFactory<?>> singletonFactories = new HashMap<>(16);

个人整理了一些资料,有需要的朋友可以直接点击领取。

Java基础知识大全

百本Java架构师核心书籍

对标阿里P8的Java学习路线和资料

2021年最新java面试题合集

所以其实我们也应该从实际使用意义上去理解这3个cache的意义,而不是只在名字上做文章,毕竟这3个名字很不合理。

Spring生成Bean时如何利用三级缓存

Spring在创建Bean的过程我们简化成我们关心的步骤:

  • 实例化Bean
  • 判断作用域是否为单例,允许循环依赖,并且当前bean正在创建,还没有创建完成。如果都满足条件,则调用addSingletonFactory将bean实例放入三级缓存中,同时删除二级缓存中相同的beanName元素,这里我们发现了一个秘密,单例对象只可能存在于三级缓存中的某一个,所以在添加到某一缓存中时,一定会删除其他2级缓存中的相同对象
  • 调用populateBean方法进行依赖注入
  • 调用initializeBean方法完成对象初始化和AOP增强

org.springframework.beans.factory.support.DefaultSingletonBeanRegistry#addSingletonFactory,添加到三级缓存方法

1
2
3
4
5
6
7
8
9
10
kotlin复制代码protected void addSingletonFactory(String beanName, ObjectFactory<?> singletonFactory) {
Assert.notNull(singletonFactory, "Singleton factory must not be null");
synchronized (this.singletonObjects) {
if (!this.singletonObjects.containsKey(beanName)) {
this.singletonFactories.put(beanName, singletonFactory);
this.earlySingletonObjects.remove(beanName);
this.registeredSingletons.add(beanName);
}
}
}

其中涉及的流程可以从
org.springframework.beans.factory.support.AbstractBeanFactory#doGetBean(final String name, @Nullable final Class requiredType, @Nullable final Object[] args, boolean typeCheckOnly)
这个方法开始,这个方法是个关键的方法,因为里面包含了创建Bean的流程,Spring在设计时,将createBean放到了getBean的流程里
说实话,这个方法太长,而且很多逻辑除非作者来讲,否则很难自己将每一句代码的意思都读懂,这里我们只挑感兴趣的流程来看,
第一步我们看到第8行代码,
急切的检查手动注册单例的单例缓存,eager我们通常解释为饿汉式,其实也有点乐观的意思,这里就直接到单例缓存中去取,如果取到那肯定就是单例的,没取到就需要创建。这种套路我们进行实际编程时应该学到,是个很常见的编程模式。
这里的逻辑是直接从单例池中获取,如果获取到了就可以直接返回bean,如果没有获取到就需要创建,在创建的逻辑里再去判断是singleton还是prototype。

这里可以看到26行抛出了一个异常,原因是这个bean是一个prototype的,而且发现这个bean已经被加入到了正在创建的bean队列中,只有一种可能就是,在创建他的时候,他被加入到正在创建队列中,他引用了别人,在创建别人时,别人引用了他,这种prototype的循环引用,Spring不提供解决方案,直接抛出异常,Spring希望这种情况下的循环引用由应用自己修改逻辑。

下面我们可以看到82、100、118行就是真正的实例化bean的地方,org.springframework.beans.factory.support.AbstractBeanFactory#createBean(),这个方法在AbstractBeanFactory中是抽象的空方法,交给了子类去实现,由于createBean中涉及了三级缓存相关的逻辑所以我们不得不在看看相关的源码。

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
scss复制代码protected <T> T doGetBean(final String name, @Nullable final Class<T> requiredType,
@Nullable final Object[] args, boolean typeCheckOnly) throws BeansException {

final String beanName = transformedBeanName(name);
Object bean;

// Eagerly check singleton cache for manually registered singletons.
Object sharedInstance = getSingleton(beanName);
if (sharedInstance != null && args == null) {
if (logger.isTraceEnabled()) {
if (isSingletonCurrentlyInCreation(beanName)) {
logger.trace("Returning eagerly cached instance of singleton bean '" + beanName +
"' that is not fully initialized yet - a consequence of a circular reference");
}
else {
logger.trace("Returning cached instance of singleton bean '" + beanName + "'");
}
}
bean = getObjectForBeanInstance(sharedInstance, name, beanName, null);
}

else {
// Fail if we're already creating this bean instance:
// We're assumably within a circular reference.
if (isPrototypeCurrentlyInCreation(beanName)) {
throw new BeanCurrentlyInCreationException(beanName);
}

// Check if bean definition exists in this factory.
BeanFactory parentBeanFactory = getParentBeanFactory();
if (parentBeanFactory != null && !containsBeanDefinition(beanName)) {
// Not found -> check parent.
String nameToLookup = originalBeanName(name);
if (parentBeanFactory instanceof AbstractBeanFactory) {
return ((AbstractBeanFactory) parentBeanFactory).doGetBean(
nameToLookup, requiredType, args, typeCheckOnly);
}
else if (args != null) {
// Delegation to parent with explicit args.
return (T) parentBeanFactory.getBean(nameToLookup, args);
}
else if (requiredType != null) {
// No args -> delegate to standard getBean method.
return parentBeanFactory.getBean(nameToLookup, requiredType);
}
else {
return (T) parentBeanFactory.getBean(nameToLookup);
}
}

if (!typeCheckOnly) {
markBeanAsCreated(beanName);
}

try {
final RootBeanDefinition mbd = getMergedLocalBeanDefinition(beanName);
checkMergedBeanDefinition(mbd, beanName, args);

// Guarantee initialization of beans that the current bean depends on.
String[] dependsOn = mbd.getDependsOn();
if (dependsOn != null) {
for (String dep : dependsOn) {
if (isDependent(beanName, dep)) {
throw new BeanCreationException(mbd.getResourceDescription(), beanName,
"Circular depends-on relationship between '" + beanName + "' and '" + dep + "'");
}
registerDependentBean(dep, beanName);
try {
getBean(dep);
}
catch (NoSuchBeanDefinitionException ex) {
throw new BeanCreationException(mbd.getResourceDescription(), beanName,
"'" + beanName + "' depends on missing bean '" + dep + "'", ex);
}
}
}

// Create bean instance.
if (mbd.isSingleton()) {
sharedInstance = getSingleton(beanName, () -> {
try {
return createBean(beanName, mbd, args);
}
catch (BeansException ex) {
// Explicitly remove instance from singleton cache: It might have been put there
// eagerly by the creation process, to allow for circular reference resolution.
// Also remove any beans that received a temporary reference to the bean.
destroySingleton(beanName);
throw ex;
}
});
bean = getObjectForBeanInstance(sharedInstance, name, beanName, mbd);
}

else if (mbd.isPrototype()) {
// It's a prototype -> create a new instance.
Object prototypeInstance = null;
try {
beforePrototypeCreation(beanName);
prototypeInstance = createBean(beanName, mbd, args);
}
finally {
afterPrototypeCreation(beanName);
}
bean = getObjectForBeanInstance(prototypeInstance, name, beanName, mbd);
}

else {
String scopeName = mbd.getScope();
final Scope scope = this.scopes.get(scopeName);
if (scope == null) {
throw new IllegalStateException("No Scope registered for scope name '" + scopeName + "'");
}
try {
Object scopedInstance = scope.get(beanName, () -> {
beforePrototypeCreation(beanName);
try {
return createBean(beanName, mbd, args);
}
finally {
afterPrototypeCreation(beanName);
}
});
bean = getObjectForBeanInstance(scopedInstance, name, beanName, mbd);
}
catch (IllegalStateException ex) {
throw new BeanCreationException(beanName,
"Scope '" + scopeName + "' is not active for the current thread; consider " +
"defining a scoped proxy for this bean if you intend to refer to it from a singleton",
ex);
}
}
}
catch (BeansException ex) {
cleanupAfterBeanCreationFailure(beanName);
throw ex;
}
}

// Check if required type matches the type of the actual bean instance.
if (requiredType != null && !requiredType.isInstance(bean)) {
try {
T convertedBean = getTypeConverter().convertIfNecessary(bean, requiredType);
if (convertedBean == null) {
throw new BeanNotOfRequiredTypeException(name, requiredType, bean.getClass());
}
return convertedBean;
}
catch (TypeMismatchException ex) {
if (logger.isTraceEnabled()) {
logger.trace("Failed to convert bean '" + name + "' to required type '" +
ClassUtils.getQualifiedName(requiredType) + "'", ex);
}
throw new BeanNotOfRequiredTypeException(name, requiredType, bean.getClass());
}
}
return (T) bean;
}

如下就是createBean()方法的源码,老长了

org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory#createBean(java.lang.String, org.springframework.beans.factory.support.RootBeanDefinition, java.lang.Object[])
很不幸,方法虽长,核心却被封装到了另一个方法doCreateBean()中,看看41行,我们又要去粘贴源码了…….

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
java复制代码@Override
protected Object createBean(String beanName, RootBeanDefinition mbd, @Nullable Object[] args)
throws BeanCreationException {

if (logger.isTraceEnabled()) {
logger.trace("Creating instance of bean '" + beanName + "'");
}
RootBeanDefinition mbdToUse = mbd;

// Make sure bean class is actually resolved at this point, and
// clone the bean definition in case of a dynamically resolved Class
// which cannot be stored in the shared merged bean definition.
Class<?> resolvedClass = resolveBeanClass(mbd, beanName);
if (resolvedClass != null && !mbd.hasBeanClass() && mbd.getBeanClassName() != null) {
mbdToUse = new RootBeanDefinition(mbd);
mbdToUse.setBeanClass(resolvedClass);
}

// Prepare method overrides.
try {
mbdToUse.prepareMethodOverrides();
}
catch (BeanDefinitionValidationException ex) {
throw new BeanDefinitionStoreException(mbdToUse.getResourceDescription(),
beanName, "Validation of method overrides failed", ex);
}

try {
// Give BeanPostProcessors a chance to return a proxy instead of the target bean instance.
Object bean = resolveBeforeInstantiation(beanName, mbdToUse);
if (bean != null) {
return bean;
}
}
catch (Throwable ex) {
throw new BeanCreationException(mbdToUse.getResourceDescription(), beanName,
"BeanPostProcessor before instantiation of bean failed", ex);
}

try {
Object beanInstance = doCreateBean(beanName, mbdToUse, args);
if (logger.isTraceEnabled()) {
logger.trace("Finished creating instance of bean '" + beanName + "'");
}
return beanInstance;
}
catch (BeanCreationException | ImplicitlyAppearedSingletonException ex) {
// A previously detected exception with proper bean creation context already,
// or illegal singleton state to be communicated up to DefaultSingletonBeanRegistry.
throw ex;
}
catch (Throwable ex) {
throw new BeanCreationException(
mbdToUse.getResourceDescription(), beanName, "Unexpected exception during bean creation", ex);
}
}

org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory#doCreateBean()
看第10行,是实例化对象,可以理解为给对象分配内存
看第41行,就是加入第三级缓存,终于找到这个关键点了,注意这里有个非常重要的点,就是传递的是一个lambda表达式,也就是一个函数,也就是说三级缓存里面直接存储的是并不一个bean,想要从三级缓存里拿出bean,需要调用一下这个函数,调用时机是在getSingleton()函数触发的,也就是说这里只是关联上了方法,不会去调用。
看第47行,是填充属性的方法,这个方法里会触发引用对象的创建,包括循环依赖的对象的创建
看第48行,是后置处理逻辑的地方

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
scss复制代码protected Object doCreateBean(final String beanName, final RootBeanDefinition mbd, final @Nullable Object[] args)
throws BeanCreationException {

// Instantiate the bean.
BeanWrapper instanceWrapper = null;
if (mbd.isSingleton()) {
instanceWrapper = this.factoryBeanInstanceCache.remove(beanName);
}
if (instanceWrapper == null) {
instanceWrapper = createBeanInstance(beanName, mbd, args);
}
final Object bean = instanceWrapper.getWrappedInstance();
Class<?> beanType = instanceWrapper.getWrappedClass();
if (beanType != NullBean.class) {
mbd.resolvedTargetType = beanType;
}

// Allow post-processors to modify the merged bean definition.
synchronized (mbd.postProcessingLock) {
if (!mbd.postProcessed) {
try {
applyMergedBeanDefinitionPostProcessors(mbd, beanType, beanName);
}
catch (Throwable ex) {
throw new BeanCreationException(mbd.getResourceDescription(), beanName,
"Post-processing of merged bean definition failed", ex);
}
mbd.postProcessed = true;
}
}

// Eagerly cache singletons to be able to resolve circular references
// even when triggered by lifecycle interfaces like BeanFactoryAware.
boolean earlySingletonExposure = (mbd.isSingleton() && this.allowCircularReferences &&
isSingletonCurrentlyInCreation(beanName));
if (earlySingletonExposure) {
if (logger.isTraceEnabled()) {
logger.trace("Eagerly caching bean '" + beanName +
"' to allow for resolving potential circular references");
}
addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, bean));
}

// Initialize the bean instance.
Object exposedObject = bean;
try {
populateBean(beanName, mbd, instanceWrapper);
exposedObject = initializeBean(beanName, exposedObject, mbd);
}
catch (Throwable ex) {
if (ex instanceof BeanCreationException && beanName.equals(((BeanCreationException) ex).getBeanName())) {
throw (BeanCreationException) ex;
}
else {
throw new BeanCreationException(
mbd.getResourceDescription(), beanName, "Initialization of bean failed", ex);
}
}

if (earlySingletonExposure) {
Object earlySingletonReference = getSingleton(beanName, false);
if (earlySingletonReference != null) {
if (exposedObject == bean) {
exposedObject = earlySingletonReference;
}
else if (!this.allowRawInjectionDespiteWrapping && hasDependentBean(beanName)) {
String[] dependentBeans = getDependentBeans(beanName);
Set<String> actualDependentBeans = new LinkedHashSet<>(dependentBeans.length);
for (String dependentBean : dependentBeans) {
if (!removeSingletonIfCreatedForTypeCheckOnly(dependentBean)) {
actualDependentBeans.add(dependentBean);
}
}
if (!actualDependentBeans.isEmpty()) {
throw new BeanCurrentlyInCreationException(beanName,
"Bean with name '" + beanName + "' has been injected into other beans [" +
StringUtils.collectionToCommaDelimitedString(actualDependentBeans) +
"] in its raw version as part of a circular reference, but has eventually been " +
"wrapped. This means that said other beans do not use the final version of the " +
"bean. This is often the result of over-eager type matching - consider using " +
"'getBeanNamesForType' with the 'allowEagerInit' flag turned off, for example.");
}
}
}
}

// Register bean as disposable.
try {
registerDisposableBeanIfNecessary(beanName, bean, mbd);
}
catch (BeanDefinitionValidationException ex) {
throw new BeanCreationException(
mbd.getResourceDescription(), beanName, "Invalid destruction signature", ex);
}

return exposedObject;
}

这个就是属性装配的方法,里面涉及了引用对象的创建
org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory#populateBean()
看org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory#applyPropertyValues()这个方法里面就知道。

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
ini复制代码protected void populateBean(String beanName, RootBeanDefinition mbd, @Nullable BeanWrapper bw) {
if (bw == null) {
if (mbd.hasPropertyValues()) {
throw new BeanCreationException(
mbd.getResourceDescription(), beanName, "Cannot apply property values to null instance");
}
else {
// Skip property population phase for null instance.
return;
}
}

// Give any InstantiationAwareBeanPostProcessors the opportunity to modify the
// state of the bean before properties are set. This can be used, for example,
// to support styles of field injection.
if (!mbd.isSynthetic() && hasInstantiationAwareBeanPostProcessors()) {
for (BeanPostProcessor bp : getBeanPostProcessors()) {
if (bp instanceof InstantiationAwareBeanPostProcessor) {
InstantiationAwareBeanPostProcessor ibp = (InstantiationAwareBeanPostProcessor) bp;
if (!ibp.postProcessAfterInstantiation(bw.getWrappedInstance(), beanName)) {
return;
}
}
}
}

PropertyValues pvs = (mbd.hasPropertyValues() ? mbd.getPropertyValues() : null);

int resolvedAutowireMode = mbd.getResolvedAutowireMode();
if (resolvedAutowireMode == AUTOWIRE_BY_NAME || resolvedAutowireMode == AUTOWIRE_BY_TYPE) {
MutablePropertyValues newPvs = new MutablePropertyValues(pvs);
// Add property values based on autowire by name if applicable.
if (resolvedAutowireMode == AUTOWIRE_BY_NAME) {
autowireByName(beanName, mbd, bw, newPvs);
}
// Add property values based on autowire by type if applicable.
if (resolvedAutowireMode == AUTOWIRE_BY_TYPE) {
autowireByType(beanName, mbd, bw, newPvs);
}
pvs = newPvs;
}

boolean hasInstAwareBpps = hasInstantiationAwareBeanPostProcessors();
boolean needsDepCheck = (mbd.getDependencyCheck() != AbstractBeanDefinition.DEPENDENCY_CHECK_NONE);

PropertyDescriptor[] filteredPds = null;
if (hasInstAwareBpps) {
if (pvs == null) {
pvs = mbd.getPropertyValues();
}
for (BeanPostProcessor bp : getBeanPostProcessors()) {
if (bp instanceof InstantiationAwareBeanPostProcessor) {
InstantiationAwareBeanPostProcessor ibp = (InstantiationAwareBeanPostProcessor) bp;
PropertyValues pvsToUse = ibp.postProcessProperties(pvs, bw.getWrappedInstance(), beanName);
if (pvsToUse == null) {
if (filteredPds == null) {
filteredPds = filterPropertyDescriptorsForDependencyCheck(bw, mbd.allowCaching);
}
pvsToUse = ibp.postProcessPropertyValues(pvs, filteredPds, bw.getWrappedInstance(), beanName);
if (pvsToUse == null) {
return;
}
}
pvs = pvsToUse;
}
}
}
if (needsDepCheck) {
if (filteredPds == null) {
filteredPds = filterPropertyDescriptorsForDependencyCheck(bw, mbd.allowCaching);
}
checkDependencies(beanName, mbd, filteredPds, pvs);
}

if (pvs != null) {
applyPropertyValues(beanName, mbd, bw, pvs);
}
}

下面就是将bean对应的ObjectFactory添加到三级缓存的代码:

1
2
3
4
5
6
7
8
9
10
kotlin复制代码protected void addSingletonFactory(String beanName, ObjectFactory<?> singletonFactory) {
Assert.notNull(singletonFactory, "Singleton factory must not be null");
synchronized (this.singletonObjects) {
if (!this.singletonObjects.containsKey(beanName)) {
this.singletonFactories.put(beanName, singletonFactory);
this.earlySingletonObjects.remove(beanName);
this.registeredSingletons.add(beanName);
}
}
}

这里需要注意的是,调用加入三级缓存的方法时,传递的实参是什么下面这个函数,相当于存入三级缓存保留了一段后置逻辑,等到调用getSingleton()是就会触发,为什么要这么设计呢?为什么不在存入的时候直接就执行了这段逻辑?

1
2
3
4
5
6
7
8
9
10
11
12
ini复制代码protected Object getEarlyBeanReference(String beanName, RootBeanDefinition mbd, Object bean) {
Object exposedObject = bean;
if (!mbd.isSynthetic() && hasInstantiationAwareBeanPostProcessors()) {
for (BeanPostProcessor bp : getBeanPostProcessors()) {
if (bp instanceof SmartInstantiationAwareBeanPostProcessor) {
SmartInstantiationAwareBeanPostProcessor ibp = (SmartInstantiationAwareBeanPostProcessor) bp;
exposedObject = ibp.getEarlyBeanReference(exposedObject, beanName);
}
}
}
return exposedObject;
}

我们关心的是什么时候取出来,取出来的时机是getSingleton()方法,这里有2个方法:
org.springframework.beans.factory.support.DefaultSingletonBeanRegistry#getSingleton(java.lang.String, boolean)
org.springframework.beans.factory.support.DefaultSingletonBeanRegistry#getSingleton(java.lang.String, org.springframework.beans.factory.ObjectFactory<?>)
前者是用于检查单例池中是否有bean,同时会提升三级缓存到二级缓存,这么做的原因要回头看doGetBean()方法的逻辑,每一个doGetBean()的方法最前面都会调用第一个getSingleton()方法,一般而言,就2种情况,一级缓存中找到,或者一级缓存中没找到,三级缓存中找到,此时是循环依赖的情况,那么此时Spring就要提升依赖bean到二级缓存。后者用于createBean,因为第二个参数传递的是createBean()函数,更重要的是他在里面调用了,看第39行的singletonFactory.getObject()会触发createBean动作。然后后面有一个加入一级缓存的动作。

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
kotlin复制代码protected Object getSingleton(String beanName, boolean allowEarlyReference) {
Object singletonObject = this.singletonObjects.get(beanName);
if (singletonObject == null && isSingletonCurrentlyInCreation(beanName)) {
synchronized (this.singletonObjects) {
singletonObject = this.earlySingletonObjects.get(beanName);
if (singletonObject == null && allowEarlyReference) {
ObjectFactory<?> singletonFactory = this.singletonFactories.get(beanName);
if (singletonFactory != null) {
singletonObject = singletonFactory.getObject();
this.earlySingletonObjects.put(beanName, singletonObject);
this.singletonFactories.remove(beanName);
}
}
}
}
return singletonObject;
}

public Object getSingleton(String beanName, ObjectFactory<?> singletonFactory) {
Assert.notNull(beanName, "Bean name must not be null");
synchronized (this.singletonObjects) {
Object singletonObject = this.singletonObjects.get(beanName);
if (singletonObject == null) {
if (this.singletonsCurrentlyInDestruction) {
throw new BeanCreationNotAllowedException(beanName,
"Singleton bean creation not allowed while singletons of this factory are in destruction " +
"(Do not request a bean from a BeanFactory in a destroy method implementation!)");
}
if (logger.isDebugEnabled()) {
logger.debug("Creating shared instance of singleton bean '" + beanName + "'");
}
beforeSingletonCreation(beanName);
boolean newSingleton = false;
boolean recordSuppressedExceptions = (this.suppressedExceptions == null);
if (recordSuppressedExceptions) {
this.suppressedExceptions = new LinkedHashSet<>();
}
try {
singletonObject = singletonFactory.getObject();
newSingleton = true;
}
catch (IllegalStateException ex) {
// Has the singleton object implicitly appeared in the meantime ->
// if yes, proceed with it since the exception indicates that state.
singletonObject = this.singletonObjects.get(beanName);
if (singletonObject == null) {
throw ex;
}
}
catch (BeanCreationException ex) {
if (recordSuppressedExceptions) {
for (Exception suppressedException : this.suppressedExceptions) {
ex.addRelatedCause(suppressedException);
}
}
throw ex;
}
finally {
if (recordSuppressedExceptions) {
this.suppressedExceptions = null;
}
afterSingletonCreation(beanName);
}
if (newSingleton) {
addSingleton(beanName, singletonObject);
}
}
return singletonObject;
}
}

流程总结:

前提:我们将2个重载的getSingleton()方法依次命名为getSingleton1()和getSingleton2(),具体区别看上面讲解
在Spring中,将createBean的动作封装在了doGetBean()动作里,doGetBean()每次获取bean都要先从单例池中去拿单例:getSingleton1(),注意,getSingleton1()方法是有一些逻辑的,不仅仅是一个get动作,他有一个执行三级缓存中ObjectFactory生产对象的动作,这里有需要的话,会产生代理对象,并将三级缓存提升至二级缓存。当然,如果getSingleton1()方法返回的是null,doGetBean()将会调用getSingleton2(),那么将会触发doCreateBean()方法,doCreateBean()方法是singleton和prototype类型的bean通用的方法,doCreateBean()首先实例化bean,然后判断是否是singleton,是的话就加入到三级缓存,然后进行属性填充和初始化回调,然后根据条件调用getSintleton1(),同时接着getSingleton2()方法将会把创建的对象加入到1级缓存,注意这个顺序,就这样,完成对象的创建了。

动态代理+循环依赖

这里还有个更加重要场景,就是循环依赖+动态代理。这种场景在实际使用中是会产生的,如果仅仅是循环依赖或是仅仅是动态代理,这个很好理解。假设这里有A、B2个对象互相依赖且A需要生成代理对象,假设先实例化A,那么他们实例化的流程是:
实例化A,将A加入三级缓存,填充A属性,发现B
实例化B,将B加入三级缓存,填充B属性,发现A
从三级缓存中取到A,然后产生A的代理对象,并将A的代理对象升级到2级缓存,将B升级到1级缓存等,完成B的创建
将A升级到一级缓存等操作,完成A的创建
这段代码是上面org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory#doCreateBean()中的一段,他存入三级缓存的这个函数,就包含了获取代理对象的逻辑。

1
2
3
4
5
6
7
erlang复制代码if (earlySingletonExposure) {
if (logger.isTraceEnabled()) {
logger.trace("Eagerly caching bean '" + beanName +
"' to allow for resolving potential circular references");
}
addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, bean));
}

事实是动态代理+循环依赖一般情况下,Spring会报一个错误:
Error creating bean with name ‘classA’: Bean with name ‘classA’ has been injected into other beans [classB] in its raw version as part of a circular reference, but has eventually been wrapped. This means that said other beans do not use the final version of the bean. This is often the result of over-eager type matching - consider using ‘getBeanNamesOfType’ with the ‘allowEagerInit’ flag turned off, for example.
如果加上一个懒加载就能解决问题。

单例懒加载

一般说懒加载都是说单例的,单纯的一个没有引用其他对象,也没有被其他对象引用的对象似乎比较好理解,就是扫描到对象了,判断一下是否是延迟加载,是的话就不进行实例化后后续初始化。这个好理解,主要是bean都是互相引用的,如果懒加载bean和非懒加载bean之间互相引用了,我们需要搞清楚现象。这里有个大胆的猜测:
1、懒加载bean引用非懒加载bean时,非懒加载bean照常进行初始化
2、非懒加载bean引用懒加载bean时,懒加载bean也要照常进行初始化

最后

感谢你看到这里,文章有什么不足还请指正,觉得文章对你有帮助的话记得给我点个赞,每天都会分享java相关技术文章或行业资讯,欢迎大家关注和转发文章!

本文转载自: 掘金

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

去哪儿网领域驱动设计(DDD)实践之路

发表于 2021-05-11

众所周知,领域驱动设计(DDD)的概念出自Evic Evans的《领域驱动设计:软件核心复杂性应对之道》。它是指通过统一语言、业务抽象、领域划分和领域建模等一系列手段来控制软件复杂度的方法论。

伴随着微服务的兴起,领域驱动设计(DDD)正在焕发青春,其价值被越来越多的业内人士重新发掘。那么,DDD在国内的发展现状如何?业务团队要了解DDD,应该如何上手?带着这些疑问,我们采访了去哪儿网技术总监王植萌。

▲去哪儿网技术总监 王植萌

国内DDD的实施现状

关于DDD的业务重塑,企业最关心的因素有三个方面:一、是否可以提升人效,降低整体成本;二、是否能够指导系统架构快速适应组织架构与业务架构的变化,保证技术与业务不脱节;三、是否能够促进产研融合,为业务的共同愿景发力。

重塑DDD可以让企业获得一个非常有用的领域模型和更好的用户体验,清晰的模型边界,以及更好的企业架构;企业业务得到更准确的定义和理解;领域专家可以为软件设计做出贡献;做出敏捷、迭代式和持续建模等。

即便DDD可以满足企业的诸多需求,但是仍有许多朋友对其价值收益感受不明显。其原因主要是DDD门槛高、概念多,体系庞大又抽象,再加上缺少实践经验和案例指导,很多开发人员对DDD望洋兴叹,面临DDD落地难的困境。

王植萌认为,“经过一年多的项目实践发现,DDD并不存在最佳实践。它是一套体系与原则,针对不同类型的业务领域,DDD实践方法和理念,以及资源投入的方式是不一样的。很多书本上都没有描述,需要通过实践来摸索总结。”

去哪儿网DDD的实践

在去年疫情时期,去哪儿网的高层提出“练内功”的理念。所谓,练内功的要义就是要让技术架构适应业务与组织结构的变化。当组织结构发生巨大的变化时,产研之间需要迅速融合产生竞争力。当发生较大的人员变动时,产研双方都有重新理解业务、梳理业务达成对业务领域一致认知的愿望。

早在2013年,去哪儿网就落地了敏捷开发。而DDD的理念与敏捷开发、极限编程的理念一脉相承。可以说,在DDD之前,敏捷开发是去哪儿网实现产研融合的重要方法,只是DDD体系要比敏捷开发更为完善和厚重。所以,去哪儿网选择在项目中落地DDD。

在落地DDD的过程中,去哪儿网也面临着重重困境。没有与产品树立共同愿景,共同做战略设计的习惯,导致产研双方无法实现共同愿景,从而导致DDD做成了一次消耗更多人力的技术重构。此外,事件风暴需要反复做,但是用纸来保持事件风暴信息容易丢失且成本高昂。

为了解决这些问题,去哪儿网采用BeeArt的工具,来做事件风暴结果储存的工具,保证了事件风暴结果可分版本的保存与使用,大幅度提升了事件风暴的质量,提高了DDD实践的成功率。

对于DDD业务重塑完成后,如何保持领域不被外部入侵?王植萌表示,“我们通过标准化API的方式,去哪儿网实现了一套标准的API接入规范和工具,有效保证了API对于限界上下文边界的隔离性,防止限界上下文被入侵。”

DDD的成功落地给去哪儿网带来最直观的价值是:去哪儿网从整体上开始重视设计工作,细化设计的流程。增进了产研间的互信,双方对需求的工时预期趋于一致,对于需求的错误理解大大减少。在解决复杂度高的问题时,DDD给予了非常必要的理论指导,能够解决复杂领域中不易厘清的不合理架构,并加以改进。

总的来看,去哪儿网DDD实践成功后,整体上增加了领域专家,提高了产研沟通效率,响应速度也逐渐变快。在技术侧,去哪儿网掌握了业务的核心玩法,计算过程趋于标准化。在产品侧,去哪儿网专注业务策略,解决了已有的痛点。在运营侧,整个业务过程可视化,提升问题的处理效率,减少需要反馈的问题。

如何快速上手DDD?

业务团队可以先从工作坊,包括Event Storming或Domain StoryTelling着手,感受DDD带来的好处。然后,再研读DDD战略的部分信息,并且优化和整合到自己的业务知识库里,理解如何更好地将自身业务知识传递给团队,从而降低团队间的沟通成本,提高协作效率。

关于上手DDD的书籍,王植萌推荐了欧创新老师的《中台架构与实现-基于DDD和微服务》,并作出很高的评价,“这本书是近些年来,DDD领域的一部佳作。它将DDD所涉及的疆域由与产品的共同愿景,一直延伸到具体的代码落地实现,是一部纵贯全局的划时代的作品。”

王植萌指出,有兴趣在DDD领域有所发展的架构师可以花7~8个小时阅读《领域驱动设计精粹》一书,然后再上B站去哪儿网的Qunar技术大本营,找到去哪儿网的系列教学视频进行学习,其中有手把手的挂盘讲解事件风暴过程的环节,下一步就是要亲自做一次DDD思想指导的业务重塑。

写在最后

通过去哪儿网的实践证明,DDD完全可以高成功率的落地。随着教材质量的提升、工具的逐步完善、实践经验的不断积累,DDD一定可以在国内加速发展。

在即将到来的第十三届中国系统架构师大会(SACC2021)上,王植萌将作为“DDD领域驱动设计专场”的出品人,为大家带来去哪儿网实际的DDD业务重塑案例,包括实践中和实践后的思考,期待能给听众朋友们带来启发与帮助。

本文转载自: 掘金

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

Android Jetpack 开发套件

发表于 2021-05-11

⭐️ 本文已收录到 AndroidFamily,技术和职场问题,请关注公众号 [彭旭锐] 和 [BaguTree Pro] 知识星球提问。

Android Jetpack 开发套件是 Google 推出的 Android 应用开发编程范式,为开发者提供了解决应用开发场景中通用的模式化问题的最佳实践,让开发者可将时间精力集中于真正重要的业务编码工作上。

这篇文章是 Android Jetpack 系列文章的第 6 篇文章,完整目录可以移步至文章末尾~

前言

大家好,我是小彭。

过去两年,我们在掘金平台上发表过一些文章,小彭也收到了大家的意见和鼓励。最近,我会陆续搬运到公众号上。

ViewBinding 是 Android Gradle Plugin 3.6 中新增的特性,用于更加轻量地实现视图绑定(即视图与变量的绑定),可以理解为轻量版本的 DataBinding。 在这篇文章里,我将总结 ViewBinding 使用方法 & 原理,示例程序 AndroidFamilyDemo · KotlinDelegate 有用请记得给 Star ,给小彭一点创作的动力。

记录:2022 年 9 月 8 日修订:优化文章结构

前置知识:

  • Kotlin 编程 #2 委托机制 & 原理 & 应用
  • Kotlin 编程 #3 扩展函数(终于知道为什么 with 用 this,let 用 it)
  • Java | 关于泛型能问的都在这里了(含Kotlin)
  • Android Jetpack 开发套件 #7 AndroidX Fragment 核心原理分析

学习路线图


  1. 认识 ViewBinding

1.1 ViewBinding 用于解决什么问题?

ViewBinding 是 Android Gradle Plugin 3.6 中新增的特性,用于更加轻量地实现视图绑定(即视图与变量的绑定),可以理解为轻量版本的 DataBinding。

1.2 ViewBinding 与其他视图绑定方案对比

在 ViewBinding 之前,业界已经有过几种视图绑定方案了,想必你也用过。那么,ViewBinding 作为后起之秀就一定比前者香吗?我从多个维度对比它们的区别:

角度 findViewById ButterKnife Kotlin Synthetics DataBinding ViewBinding ❓
简洁性 ✖ ✖ ✔ ✔ ✔ ❓
编译期检查 ✖ ✖ ✖ ✔ ✔ ❓
编译速度 ✔ ✖ ✔ ✖ ✔ ❓
支持 Kotlin & Java ✔ ✔ ✖ ✔ ✔ ❓
收敛模板代码 ✖ ✖ ✔ ✖ ✖ ❓
  • 1、简洁性: findViewById 和 ButterKnife 需要在代码中声明很多变量,其他几种方案代码简洁度较好;
  • 2、编译检查: 编译期间主要有两个方面的检查:类型检查 + 只能访问当前布局中的 id。findViewById、ButterKnife 和 Kotlin Synthetics 在这方面表现较差;
  • 3、编译速度: findViewById 的编译速度是最快的,而 ButterKnife 和 DataBinding 中存在注解处理,编译速度略逊色于 Kotlin Synthetics 和 ViewBinding;
  • 4、支持 Kotlin & Java: Kotlin Synthetics 只支持 Kotlin 语言;
  • 5、收敛模板代码: 基本上每种方案都带有一定量的模板代码,只有 Kotlin Synthetics 的模板代码是较少的。

可以看到,并没有一种绝对优势的方法,但越往后整体的效果是有提升的。另外,❓是什么呢?

1.3 ViewBinding 的实现原理

AGP 插件会为每个 XML 布局文件创建一个绑定类文件 xxxBinding ,绑定类中会持有布局文件中所有带 android:id 属性的 View 引用。例如,有布局文件为 fragment_test.xml ,则插件会生成绑定类 FragmentTestBinding.java 。

那么,所有 XML 布局文件都生成 Java 类,会不会导致包体积瞬间增大?不会的, 未使用的类会在混淆时被压缩。


  1. ViewBinding 的基本用法

这一节我们来介绍 ViewBinding 的使用方法,内容不多。

提示: ViewBinding 要求在 Android Gradle Plugin 版本在至少在 3.6 以上。

2.1 添加配置

视图绑定功能按模块级别启用,启用的模块需要在模块级 build.gralde 中添加配置。例如:

build.gradle

1
2
3
4
5
6
groovy复制代码android {
...
viewBinding {
enabled = true
}
}

对于不需要生成绑定类的布局文件,可以在根节点声明 tools:viewBindingIgnore="true" 。例如:

1
2
3
4
5
xml复制代码<LinearLayout
...
tools:viewBindingIgnore="true" >
...
</LinearLayout>

2.2 视图绑定

绑定类中提供了 3 个视图绑定 API:

1
2
3
4
5
6
7
8
kotlin复制代码// 绑定到视图 view 上
fun <T> bind(view : View) : T

// 使用 inflater 解析布局,再绑定到 View 上
fun <T> inflate(inflater : LayoutInflater) : T

// 使用 inflater 解析布局,再绑定到 View 上
fun <T> inflate(inflater : LayoutInflater, parent : ViewGroup?, attachToParent : Boolean) : T
  • 1、在 Activity 中使用

MainActivity.kt

1
2
3
4
5
6
7
8
9
10
11
12
kotlin复制代码class TestActivity: AppCompatActivity(R.layout.activity_test) {

private lateinit var binding: ActivityTestBinding

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)

binding = ActivityTestBinding.inflate(layoutInflater)
setContentView(binding.root)
binding.tvDisplay.text = "Hello World."
}
}
  • 2、在 Fragment 中使用

TestFragment.kt

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
kotlin复制代码class TestFragment : Fragment(R.layout.fragment_test) {

private var _binding: FragmentTestBinding? = null
private val binding get() = _binding!!

override fun onViewCreated(root: View, savedInstanceState: Bundle?) {
_binding = FragmentTestBinding.bind(root)

binding.tvDisplay.text = "Hello World."
}

override fun onDestroyView() {
super.onDestroyView()

// 置空
_binding = null
}
}

2.3 避免内存泄露

这里有一个隐藏的内存泄露问题,你需要理解清楚(严格来说这并不是 ViewBinding 的问题,即使你采用其它视图绑定方案也要考虑这个问题)。

问题:为什么 Fragment#onDestroyView() 里需要置空绑定类对象,而 Activity 里不需要?
答:Activity 实例和 Activity 视图的生命周期是同步的,而 Fragment 实例和 Fragment 视图的生命周期并不是完全同步的,因此需要在 Fragment 视图销毁时,手动回收绑定类对象,否则造成内存泄露。例如:detach Fragment,或者 remove Fragment 并且事务进入返回栈,此时 Fragment 视图销毁但 Fragment 实例存在。关于 Fragment 生命周期和事务在我之前的一篇文章里讨论过:Android Jetpack 开发套件 #7 AndroidX Fragment 核心原理分析

总之,在视图销毁但是控制类对象实例还存活的时机,你就需要手动回收绑定类对象,否则造成内存泄露。

2.4 ViewBinding 绑定类源码

反编译如下:

ActivityTestBinding.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
java复制代码public final class ActivityTestBinding implements ViewBinding {
private final ConstraintLayout rootView;
public final TextView tvDisplay;

private ActivityTestBinding (ConstraintLayout paramConstraintLayout1, TextView paramTextView)
this.rootView = paramConstraintLayout1;
this.tvDisplay = paramTextView;
}

public static ActivityTestBinding bind(View paramView) {
TextView localTextView = (TextView)paramView.findViewById(2131165363);
if (localTextView != null) {
return new ActivityMainBinding((ConstraintLayout)paramView, localTextView);
}else {
paramView = "tvDisplay";
}
throw new NullPointerException("Missing required view with ID: ".concat(paramView));
}

public static ActivityMainBinding inflate(LayoutInflater paramLayoutInflater) {
return inflate(paramLayoutInflater, null, false);
}

public static ActivityMainBinding inflate(LayoutInflater paramLayoutInflater, ViewGroup paramViewGroup, boolean paramBoolean) {
paramLayoutInflater = paramLayoutInflater.inflate(2131361821, paramViewGroup, false);
if (paramBoolean) {
paramViewGroup.addView(paramLayoutInflater);
}
return bind(paramLayoutInflater);
}

public ConstraintLayout getRoot() {
return this.rootView;
}
}

  1. ViewBinding 与 Kotlin 委托双剑合璧

到这里,ViewBinding 的使用教程已经说完了。但是回过头看,有没有发现一些局限性呢?

  • 1、创建和回收 ViewBinding 对象需要重复编写样板代码,特别是在 Fragment 中使用的案例;
  • 2、binding 属性是可空的,也是可变的,使用起来不方便。

那么,有没有可优化的方案呢?我们想起了 Kotlin 属性委托,关于 Kotlin 委托机制在我之前的一篇文章里讨论过:Kotlin | 委托机制 & 原理。如果你还不太了解 Kotlin 委托,下面的内容对你会有些难度。下面,我将带你一步步封装 ViewBinding 属性委托工具。首先,我们梳理一下我们要委托的内容与需求,以及相应的解决办法:

需求 解决办法
需要委托 ViewBinding#bind() 的调用 反射
需要委托 binding = null 的调用 监听 Fragment 视图生命周期
期望 binding 属性声明为非空不可变变量 ReadOnlyProperty<F, V>

3.1 ViewBinding + Kotlin 委托 1.0

我们现在较复杂的 Fragment 中尝试使用 Kotlin 委托优化:

FragmentViewBindingPropertyV1.kt

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
kotlin复制代码private const val TAG = "ViewBindingProperty"

public inline fun <reified V : ViewBinding> viewBindingV1() = viewBindingV1(V::class.java)

public inline fun <reified T : ViewBinding> viewBindingV1(clazz: Class<T>): FragmentViewBindingPropertyV1<Fragment, T> {
val bindMethod = clazz.getMethod("bind", View::class.java)
return FragmentViewBindingPropertyV1 {
bindMethod(null, it.requireView()) as T
}
}

/**
* @param viewBinder 创建绑定类对象
*/
class FragmentViewBindingPropertyV1<in F : Fragment, out V : ViewBinding>(
private val viewBinder: (F) -> V
) : ReadOnlyProperty<F, V> {

private var viewBinding: V? = null

@MainThread
override fun getValue(thisRef: F, property: KProperty<*>): V {
// 已经绑定,直接返回
viewBinding?.let { return it }

// Use viewLifecycleOwner.lifecycle other than lifecycle
val lifecycle = thisRef.viewLifecycleOwner.lifecycle
val viewBinding = viewBinder(thisRef)
if (lifecycle.currentState == Lifecycle.State.DESTROYED) {
Log.w(
TAG, "Access to viewBinding after Lifecycle is destroyed or hasn't created yet. " +
"The instance of viewBinding will be not cached."
)
// We can access to ViewBinding after Fragment.onDestroyView(), but don't save it to prevent memory leak
} else {
lifecycle.addObserver(ClearOnDestroyLifecycleObserver())
this.viewBinding = viewBinding
}
return viewBinding
}

@MainThread
fun clear() {
viewBinding = null
}

private inner class ClearOnDestroyLifecycleObserver : LifecycleObserver {

private val mainHandler = Handler(Looper.getMainLooper())

@MainThread
@OnLifecycleEvent(Lifecycle.Event.ON_DESTROY)
fun onDestroy(owner: LifecycleOwner) {
owner.lifecycle.removeObserver(this)
mainHandler.post { clear() }
}
}
}

使用示例:

1
2
3
4
5
6
7
8
kotlin复制代码class TestFragment : Fragment(R.layout.fragment_test) {

private val binding : FragmentTestBinding by viewBindingV1()

override fun onViewCreated(root: View, savedInstanceState: Bundle?) {
binding.tvDisplay.text = "Hello World."
}
}

干净清爽!前面提出的三个需求也都实现了,现在我为你解答细节:

  • 问题 1、为什么可以使用 V::class.java,不是泛型擦除了吗? 利用了 Kotlin 内敛函数 + 实化类型参数,编译后函数体整体被复制到调用处,V::class.java 其实是 FragmentTestBinding::class.java。具体分析见:Java | 关于泛型能问的都在这里了(含Kotlin)
  • 问题 2、ReadOnlyProperty<F, V> 是什么? ReadOnlyProperty 是不可变属性代理,通过 getValue(…) 方法实现委托行为。第一个类型参数 F 是属性所有者,第二个参数 V 是属性类型,因为我们在 Fragment 中定义属性,属性类型为 ViewBinding,所谓定义类型参数为 <in F : Fragment, out V : ViewBinding>;
  • 问题 3、解释下 getValue(…) 方法? 直接看注释:

FragmentViewBindingPropertyV1.kt

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
kotlin复制代码@MainThread
override fun getValue(thisRef: F, property: KProperty<*>): V {
// 1、viewBinding 不为空说明已经绑定,直接返回
viewBinding?.let { return it }

// 2、Fragment 视图的生命周期
val lifecycle = thisRef.viewLifecycleOwner.lifecycle

// 3、实例化绑定类对象
val viewBinding = viewBinder(thisRef)

if (lifecycle.currentState == Lifecycle.State.DESTROYED) {
// 4.1 如果视图生命周期为 DESTROYED,说明视图被销毁,此时不缓存绑定类对象(避免内存泄漏)
} else {
// 4.2 定义视图生命周期监听者
lifecycle.addObserver(ClearOnDestroyLifecycleObserver())
// 4.3 缓存绑定类对象
this.viewBinding = viewBinding
}
return viewBinding
}
  • 问题 4、为什么 onDestroy() 要采用 Handler#post(Message) 完成? 因为 Fragment#viewLifecycleOwner 通知生命周期事件 ON_DESTROY 的时机在 Fragment#onDestroyView 之前。如果不使用 post 的方式,那么业务方要是在 onDestroyView 中访问了 binding,则会二次执行 getValue() 这是不必要的。

3.2 ViewBinding + Kotlin 委托 2.0

V1.0 版本使用了反射,真的一定要反射吗?反射调用 bind 函数的目的就是获得一个 ViewBinding 绑定类对象,或许我们可以试试把创建对象的行为交给外部去定义,类似这样用一个 lambda 表达式实现工厂函数:

FragmentViewBindingPropertyV2.kt

1
2
3
4
5
6
7
8
9
10
11
12
13
kotlin复制代码inline fun <F : Fragment, V : ViewBinding> viewBindingV2(
crossinline viewBinder: (View) -> V,
// 类似于创建工厂
crossinline viewProvider: (F) -> View = Fragment::requireView
) = FragmentViewBindingPropertyV2 { fragment: F ->
viewBinder(viewProvider(fragment))
}

class FragmentViewBindingPropertyV2<in F : Fragment, out V : ViewBinding>(
private val viewBinder: (F) -> V
) : ReadOnlyProperty<F, V> {
// 以下源码相同 ...
}

使用示例:

1
2
3
4
5
6
7
8
kotlin复制代码class TestFragment : Fragment(R.layout.fragment_test) {

private val binding by viewBindingV2(FragmentTestBinding::bind)

override fun onViewCreated(root: View, savedInstanceState: Bundle?) {
binding.tvDisplay.text = "Hello World."
}
}

干净清爽!不使用反射也可以实现,现在我为你解答细节:

  • 问题 5、(View) -> V 是什么? Kotlin 高阶函数,可以把 lambda 表达式直接作为参数传递,其中 View 是函数参数,而 T 是函数返回值。lambda 表达式本质上是 「可以作为值传递的代码块」。在老版本 Java 中,传递代码块需要使用匿名内部类实现,而使用 lambda 表达式甚至连函数声明都不需要,可以直接传递代码块作为函数值;
  • 问题 6、Fragment::requireView 是什么? 把函数 requireView() 作为参数传递。Fragment#requireView() 会返回 Fragment 的根节点,但要注意在 onCreateView() 之前调用 requireView() 会抛出异常;
  • 问题 7、FragmentTestBinding::bind 是什么? 把函数 bind() 作为参数传递,bind 函数的参数为 View,返回值为 ViewBinding,与函数声明 (View) -> V 匹配。

3.3 ViewBinding + Kotlin 委托最终版

V2.0 版本已经完成了针对 Fragment 的属性代理,但是实际场景中只会在 Fragment 中使用 ViewBinding 吗?显然并不是,我们还有其他一些场景:

  • Activity
  • Fragment
  • DialogFragment
  • ViewGroup
  • RecyclerView.ViewHolder

所以,我们有必要将委托工具适当封装得更通用些,完整代码和演示工程你可以直接下载查看: AndroidFamilyDemo · KotlinDelegate

ViewBindingProperty.kt

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
kotlin复制代码// -------------------------------------------------------
// ViewBindingProperty for Activity
// -------------------------------------------------------

@JvmName("viewBindingActivity")
inline fun <V : ViewBinding> ComponentActivity.viewBinding(
crossinline viewBinder: (View) -> V,
crossinline viewProvider: (ComponentActivity) -> View = ::findRootView
): ViewBindingProperty<ComponentActivity, V> = ActivityViewBindingProperty { activity: ComponentActivity ->
viewBinder(viewProvider(activity))
}

@JvmName("viewBindingActivity")
inline fun <V : ViewBinding> ComponentActivity.viewBinding(
crossinline viewBinder: (View) -> V,
@IdRes viewBindingRootId: Int
): ViewBindingProperty<ComponentActivity, V> = ActivityViewBindingProperty { activity: ComponentActivity ->
viewBinder(activity.requireViewByIdCompat(viewBindingRootId))
}

// -------------------------------------------------------
// ViewBindingProperty for Fragment / DialogFragment
// -------------------------------------------------------

@Suppress("UNCHECKED_CAST")
@JvmName("viewBindingFragment")
inline fun <F : Fragment, V : ViewBinding> Fragment.viewBinding(
crossinline viewBinder: (View) -> V,
crossinline viewProvider: (F) -> View = Fragment::requireView
): ViewBindingProperty<F, V> = when (this) {
is DialogFragment -> DialogFragmentViewBindingProperty { fragment: F ->
viewBinder(viewProvider(fragment))
} as ViewBindingProperty<F, V>
else -> FragmentViewBindingProperty { fragment: F ->
viewBinder(viewProvider(fragment))
}
}

@Suppress("UNCHECKED_CAST")
@JvmName("viewBindingFragment")
inline fun <F : Fragment, V : ViewBinding> Fragment.viewBinding(
crossinline viewBinder: (View) -> V,
@IdRes viewBindingRootId: Int
): ViewBindingProperty<F, V> = when (this) {
is DialogFragment -> viewBinding(viewBinder) { fragment: DialogFragment ->
fragment.getRootView(viewBindingRootId)
} as ViewBindingProperty<F, V>
else -> viewBinding(viewBinder) { fragment: F ->
fragment.requireView().requireViewByIdCompat(viewBindingRootId)
}
}

// -------------------------------------------------------
// ViewBindingProperty for ViewGroup
// -------------------------------------------------------

@JvmName("viewBindingViewGroup")
inline fun <V : ViewBinding> ViewGroup.viewBinding(
crossinline viewBinder: (View) -> V,
crossinline viewProvider: (ViewGroup) -> View = { this }
): ViewBindingProperty<ViewGroup, V> = LazyViewBindingProperty { viewGroup: ViewGroup ->
viewBinder(viewProvider(viewGroup))
}

@JvmName("viewBindingViewGroup")
inline fun <V : ViewBinding> ViewGroup.viewBinding(
crossinline viewBinder: (View) -> V,
@IdRes viewBindingRootId: Int
): ViewBindingProperty<ViewGroup, V> = LazyViewBindingProperty { viewGroup: ViewGroup ->
viewBinder(viewGroup.requireViewByIdCompat(viewBindingRootId))
}

// -------------------------------------------------------
// ViewBindingProperty for RecyclerView#ViewHolder
// -------------------------------------------------------

@JvmName("viewBindingViewHolder")
inline fun <V : ViewBinding> RecyclerView.ViewHolder.viewBinding(
crossinline viewBinder: (View) -> V,
crossinline viewProvider: (RecyclerView.ViewHolder) -> View = RecyclerView.ViewHolder::itemView
): ViewBindingProperty<RecyclerView.ViewHolder, V> = LazyViewBindingProperty { holder: RecyclerView.ViewHolder ->
viewBinder(viewProvider(holder))
}

@JvmName("viewBindingViewHolder")
inline fun <V : ViewBinding> RecyclerView.ViewHolder.viewBinding(
crossinline viewBinder: (View) -> V,
@IdRes viewBindingRootId: Int
): ViewBindingProperty<RecyclerView.ViewHolder, V> = LazyViewBindingProperty { holder: RecyclerView.ViewHolder ->
viewBinder(holder.itemView.requireViewByIdCompat(viewBindingRootId))
}

// -------------------------------------------------------
// ViewBindingProperty
// -------------------------------------------------------

private const val TAG = "ViewBindingProperty"

interface ViewBindingProperty<in R : Any, out V : ViewBinding> : ReadOnlyProperty<R, V> {
@MainThread
fun clear()
}

class LazyViewBindingProperty<in R : Any, out V : ViewBinding>(
private val viewBinder: (R) -> V
) : ViewBindingProperty<R, V> {

private var viewBinding: V? = null

@Suppress("UNCHECKED_CAST")
@MainThread
override fun getValue(thisRef: R, property: KProperty<*>): V {
// Already bound
viewBinding?.let { return it }

return viewBinder(thisRef).also {
this.viewBinding = it
}
}

@MainThread
override fun clear() {
viewBinding = null
}
}

abstract class LifecycleViewBindingProperty<in R : Any, out V : ViewBinding>(
private val viewBinder: (R) -> V
) : ViewBindingProperty<R, V> {

private var viewBinding: V? = null

protected abstract fun getLifecycleOwner(thisRef: R): LifecycleOwner

@MainThread
override fun getValue(thisRef: R, property: KProperty<*>): V {
// Already bound
viewBinding?.let { return it }

val lifecycle = getLifecycleOwner(thisRef).lifecycle
val viewBinding = viewBinder(thisRef)
if (lifecycle.currentState == Lifecycle.State.DESTROYED) {
Log.w(
TAG, "Access to viewBinding after Lifecycle is destroyed or hasn't created yet. " +
"The instance of viewBinding will be not cached."
)
// We can access to ViewBinding after Fragment.onDestroyView(), but don't save it to prevent memory leak
} else {
lifecycle.addObserver(ClearOnDestroyLifecycleObserver(this))
this.viewBinding = viewBinding
}
return viewBinding
}

@MainThread
override fun clear() {
viewBinding = null
}

private class ClearOnDestroyLifecycleObserver(
private val property: LifecycleViewBindingProperty<*, *>
) : LifecycleObserver {

private companion object {
private val mainHandler = Handler(Looper.getMainLooper())
}

@MainThread
@OnLifecycleEvent(Lifecycle.Event.ON_DESTROY)
fun onDestroy(owner: LifecycleOwner) {
mainHandler.post { property.clear() }
}
}
}

class FragmentViewBindingProperty<in F : Fragment, out V : ViewBinding>(
viewBinder: (F) -> V
) : LifecycleViewBindingProperty<F, V>(viewBinder) {

override fun getLifecycleOwner(thisRef: F): LifecycleOwner {
try {
return thisRef.viewLifecycleOwner
} catch (ignored: IllegalStateException) {
error("Fragment doesn't have view associated with it or the view has been destroyed")
}
}
}

class DialogFragmentViewBindingProperty<in F : DialogFragment, out V : ViewBinding>(
viewBinder: (F) -> V
) : LifecycleViewBindingProperty<F, V>(viewBinder) {

override fun getLifecycleOwner(thisRef: F): LifecycleOwner {
return if (thisRef.showsDialog) {
thisRef
} else {
try {
thisRef.viewLifecycleOwner
} catch (ignored: IllegalStateException) {
error("Fragment doesn't have view associated with it or the view has been destroyed")
}
}
}
}

// -------------------------------------------------------
// Utils
// -------------------------------------------------------

@RestrictTo(RestrictTo.Scope.LIBRARY)
class ActivityViewBindingProperty<in A : ComponentActivity, out V : ViewBinding>(
viewBinder: (A) -> V
) : LifecycleViewBindingProperty<A, V>(viewBinder) {

override fun getLifecycleOwner(thisRef: A): LifecycleOwner {
return thisRef
}
}

fun <V : View> View.requireViewByIdCompat(@IdRes id: Int): V {
return ViewCompat.requireViewById(this, id)
}

fun <V : View> Activity.requireViewByIdCompat(@IdRes id: Int): V {
return ActivityCompat.requireViewById(this, id)
}

/**
* Utility to find root view for ViewBinding in Activity
*/
fun findRootView(activity: Activity): View {
val contentView = activity.findViewById<ViewGroup>(android.R.id.content)
checkNotNull(contentView) { "Activity has no content view" }
return when (contentView.childCount) {
1 -> contentView.getChildAt(0)
0 -> error("Content view has no children. Provide root view explicitly")
else -> error("More than one child view found in Activity content view")
}
}

fun DialogFragment.getRootView(viewBindingRootId: Int): View {
val dialog = checkNotNull(dialog) {
"DialogFragment doesn't have dialog. Use viewBinding delegate after onCreateDialog"
}
val window = checkNotNull(dialog.window) { "Fragment's Dialog has no window" }
return with(window.decorView) {
if (viewBindingRootId != 0) requireViewByIdCompat(
viewBindingRootId
) else this
}
}

  1. 总结

ViewBinding 是一个轻量级的视图绑定方案,Android Gradle 插件会为每个 XML 布局文件创建一个绑定类。在 Fragment 中使用 ViewBinding 需要注意在 Fragment#onDestroyView() 里置空绑定类对象避免内存泄漏。但这会带来很多重复编写样板代码,使用属性委托可以收敛模板代码,保证调用方代码干净清爽。

角度 findViewById ButterKnife Kotlin Synthetics DataBinding ViewBinding ViewBindingProperty
简洁性 ✖ ✖ ✔ ✔ ✔ ✔
编译期检查 ✖ ✖ ✖ ✔ ✔ ✔
编译速度 ✔ ✖ ✔ ✖ ✔ ✔
支持 Kotlin & Java ✔ ✔ ✖ ✔ ✔ ✔
收敛模板代码 ✖ ✖ ✔ ✖ ✖ ✔

参考资料

  • View Binding 视图绑定 —— 官方文档
  • View Binding 与 Kotlin 委托属性的巧妙结合,告别垃圾代码! —— Kirill Rozov 著,依然范特稀西 译
  • 谁才是 ButterKnife 的终结者? —— fundroid 著
  • 深入研究 ViewBinding 在 include, merge, adapter, fragment, activity 中使用 —— Flywith24 著

推荐阅读

Android Jetpack 系列文章目录如下(2023/07/08 更新):

  • #1 Lifecycle:生命周期感知型组件的基础
  • #2 为什么 LiveData 会重放数据,怎么解决?
  • #3 为什么 Activity 都重建了 ViewModel 还存在?
  • #4 有小伙伴说看不懂 LiveData、Flow、Channel,跟我走
  • #5 Android UI 架构演进:从 MVC 到 MVP、MVVM、MVI
  • #6 ViewBinding 与 Kotlin 委托双剑合璧
  • #7 AndroidX Fragment 核心原理分析
  • #8 OnBackPressedDispatcher:Jetpack 处理回退事件的新姿势
  • #9 食之无味!App Startup 可能比你想象中要简单
  • #10 从 Dagger2 到 Hilt 玩转依赖注入(一)

⭐️ 永远相信美好的事情即将发生,欢迎加入小彭的 Android 交流社群~

本文转载自: 掘金

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

吊炸天的 Docker 图形化工具 Portainer,必须

发表于 2021-05-11

SpringBoot实战电商项目mall(40k+star)地址:github.com/macrozheng/…

摘要

之前操作Docker的时候,一直使用的是命令行的形式。命令行虽说看起来挺炫酷,但有时候还是挺麻烦的。今天给大家推荐一个Docker图形化工具Portainer,轻量级又好用,希望对大家有所帮助!

简介

Portainer 是一款轻量级的应用,它提供了图形化界面,用于方便地管理Docker环境,包括单机环境和集群环境。

安装

直接使用Docker来安装Portainer是非常方便的,仅需要两步即可完成。

  • 首先下载Portainer的Docker镜像;
1
bash复制代码docker pull portainer/portainer
  • 然后再使用如下命令运行Portainer容器;
1
2
3
4
5
bash复制代码docker run -p 9000:9000 -p 8000:8000 --name portainer \
--restart=always \
-v /var/run/docker.sock:/var/run/docker.sock \
-v /mydata/portainer/data:/data \
-d portainer/portainer
  • 第一次登录的时候需要创建管理员账号,访问地址:http://192.168.5.78:9000/

  • 之后我们选择连接到本地的Docker环境,连接完成后我们就可以愉快地使用Portainer进行可视化管理了!

使用

  • 登录成功后,可以发现有一个本地的Docker环境;

  • 打开Dashboard菜单可以看到Docker环境的概览信息,比如运行了几个容器,有多少个镜像等;

  • 打开App Templates菜单可以看到很多创建容器的模板,通过模板设置下即可轻松创建容器,支持的应用还是挺多的;

  • 打开Containers菜单,可以看到当前创建的容器,我们可以对容器进行运行、暂停、删除等操作;

  • 选择一个容器,点击Logs按钮,可以直接查看容器运行日志,可以和docker logs命令说再见了;

  • 点击Inspect按钮,可以查看容器信息,比如看看容器运行的IP地址;

  • 点击Stats按钮,可以查看容器的内存、CPU及网络的使用情况,性能分析不愁了;

  • 点击Console按钮,可以进入到容器中去执行命令,比如我们可以进入到MySQL容器中去执行登录命令;

  • 打开Images菜单,我们可以查看所有的本地镜像,对镜像进行管理;

  • 打开Networks菜单,可以查看Docker环境中的网络情况;

  • 打开Users菜单,我们可以创建Portainer的用户,并给他们赋予相应的角色;

  • 打开Registries菜单,我们可以配置自己的镜像仓库,这样在拉取镜像的时候,就可以选择从自己的镜像仓库拉取了。

总结

Portainer作为一款轻量级Docker图形化管理工具,功能强大且实用,要是有个私有镜像仓库管理功能就更好了,这样我们就不用安装重量级的镜像仓库Harbor了。

官网地址

github.com/portainer/p…

本文 GitHub github.com/macrozheng/… 已经收录,欢迎大家Star!

本文转载自: 掘金

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

优雅的进行全局参数验证拦截 0x01:pomxml 引入依

发表于 2021-05-10

参数验证如果没有做全局验证,就会导致代码非常臃肿。存在大量的 if 判断非空语句。今天介绍一种优雅的方案。先介绍一个待会用到的注解@InitBinder,它的作用:

从字面上可以看出 @InitBinder 的作用是给 Binder 做初始化的,被此注解的方法可以对 WebDataBinder 初始化。WebDataBinder 是用于表单到方法的数据绑定的。

@InitBinder 只在 @Controller 中注解方法来为这个控制器注册一个绑定器初始化方法,方法只对本控制器有效。

@InitBinder
public void initBinder(WebDataBinder webDataBinder){
//TODO

}

0x01:pom.xml 引入依赖库

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
xml复制代码<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>

<groupId>com.olive</groupId>
<artifactId>valid-demo</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>jar</packaging>

<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.8.RELEASE</version>
<relativePath />
</parent>
<name>valid-demo</name>
<url>http://maven.apache.org</url>

<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>

<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>javax.validation</groupId>
<artifactId>validation-api</artifactId>
</dependency>
</dependencies>
</project>

虽然是引用 @InitBinder 注解,但是底层框架还是使用 SpringBoot 的验证框架

#0x02:定义 Vo 对象

先定义一个 BaseVo 类,方便进行全局参数判断。这个类很简单,代码如下

1
2
3
4
5
6
7
java复制代码package com.olive.vo;

import java.io.Serializable;

public class BaseVo implements Serializable{

}

定义查询参数类 UserQueryVo,继承 BaseVo

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
scala复制代码package com.olive.vo;

import javax.validation.constraints.NotEmpty;

public class UserQueryVo extends BaseVo {

@NotEmpty(message="不能为空")
private String query;

public String getQuery() {
return query;
}

public void setQuery(String query) {
this.query = query;
}

}

关键就是在字段里使用注解,标识参数不能为空

0x03:定义控制器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
java复制代码package com.olive.controller;

import java.util.HashMap;
import java.util.Map;

import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import com.olive.vo.UserQueryVo;

@RestController
public class UserController {

@RequestMapping("/user/queryUser")
public Map queryUser(@RequestBody @Validated UserQueryVo queryVo){
Map result = new HashMap();
result.put("code", 200);
result.put("msg", "success");
return result;
}

}

在参数里使用 @Validated 注解,对 Vo 类进行校验标识。其实,正常做到这一步就可以完全进行参数校验了,但是没有一个统一拦截的入口。

0x04:添加参数校验统一拦截入口

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
java复制代码package com.olive;

import java.util.HashMap;
import java.util.List;
import java.util.Map;

import javax.servlet.http.HttpServletRequest;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.validation.BeanPropertyBindingResult;
import org.springframework.validation.ObjectError;
import org.springframework.validation.Validator;
import org.springframework.web.bind.WebDataBinder;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.InitBinder;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestControllerAdvice;

import com.olive.vo.BaseVo;

@RestControllerAdvice
public class UserControllerAdvice {

@Autowired
private Validator validator;

@InitBinder
public void initValid(WebDataBinder webDataBinder){
Object target= webDataBinder.getTarget();
if(target==null){
return;
}
if(target instanceof BaseVo){
BeanPropertyBindingResult result = new BeanPropertyBindingResult(target, target.getClass().getSimpleName());
validator.validate(target, result);
List<ObjectError> errors = result.getAllErrors();
if(errors==null || errors.isEmpty()){
return;
}
throw new RuntimeException("参数错误");
}
}

@ExceptionHandler(value = Exception.class)
@ResponseBody
public Map defaultErrorHandler(HttpServletRequest req, Exception e) {
Map result = new HashMap();
result.put("code", 500);
result.put("msg", e.getMessage());
return result;
}

}

该段代码分析如下

  • 使用 @RestControllerAdvice 进行标识,该注解可以对标注了 @RestController 的控制器进行拦截
  • 在 initValid 方法中,使用 @InitBinder标识;同时该方法传入 WebDataBinder 对象,在方法里编写参数校验代码。如果验证不过直接抛出异常。这里抛出运行时异常 RuntimeException;实际项目中可以自定义继承 RuntimeException 的参数异常类ParamValidExecpiton
  • 在方法 defaultErrorHandler 中, 使用 @ExceptionHandler 标识;进行全局异常处理,这里直接拦截Exception;实际项目中可以直接拦截自己定义的参数异常类ParamValidExecpiton。

0x05:编写引导类

1
2
3
4
5
6
7
8
9
10
11
typescript复制代码import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class Application
{
public static void main( String[] args )
{
SpringApplication.run(Application.class, args);
}
}

启动并进行验证

mmbiz.qpic.cn/mmbiz_png/g…

调试模式可以看到

image.png

这样就达到了统一控制参数校验,不需要分散到不同的代码块中了。

本文转载自: 掘金

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

从头搭建我的博客网站

发表于 2021-05-10

不断的摄入也需要输出,才能看到相对成本,便于更好进步,搭建一个博客更好的输出

1、选择静态博客模板框架

因为我没有太多的功能需求,目前选择使用静态博客框架,目前市面上框架也很多,比较主流的如:Jekyll(Ruby语言) Octopress(基于Jekyll) Hexo(Node语言) Hugo(GO语言)等,详细信息大家可以搜索引擎,本文选择Hexo来搭建博客网站。

2、安装本地开发环境

  • 安装Node.js 地址:nodejs.org/zh-cn/ 默认会安装NPM包管理器
  • 查看Node版本和npm版本:node -v/npm -v 安装最新版本就行

3、安装Hexo

1
2
3
4
5
6
7
8
bash复制代码npm install -g cnpm --registry=https://registry.npm.taobao.org 全局安装cnpm
cnpm install -g hexo-cli 安装Hexo框架生成器
mkdir blog 创建博客目录
cd blog 进入目录
hexo init 初始化博客生成器
hexo s/hexo server 启动本地博客
hexo n "新建博客" 新建博客文件
hexo g 打包成静态文件,默认在public目录下

4、添加主题全局配置

将以下配置添加至博客根目录下的_config.yml文件最后

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
yaml复制代码jsonContent:
meta: false
pages: false
posts:
title: true
date: true
path: true
text: false
raw: false
content: false
slug: false
updated: false
comments: false
link: false
permalink: false
excerpt: false
categories: false
tags: true

5、接入评论功能

Hexo默认支持的评论接口在themes/yilia/_config.yml配置文件中,多说、网易云跟帖、畅言、Disqus、Gitment,本文使用的是畅言接入

6、上传静态文件到云服务器

如果有云服务器(腾讯云,阿里云,百度云,京东云等)将静态博客文件上传到指定目录,使用nginx代理静态文件,就大功告成啦!

7、个性化修改

  • 所有的个性化主题配置基本都在themes/yilia/_config.yml下,大家可以自行摸索,具体可以参考:github.com/litten/hexo…
  • 去除畅言评论发表下方的广告:
    themes/yilia/layout/_partial/post/changyan.ejs文件结尾添加:
1
2
3
4
5
6
css复制代码<style>
.section-service-w {
margin-top: -250px !important;
transform: scale(0) !important;
}
</style>

最终效果如下:
WeChat58f6a40eff4f8a78d0ab262a4d9c5770.png

  • 截取文章部分内容作为摘要
    添加<!-- more --> 至需要截取的Markdown文章相应的位置即可
  • 分页设置如果当前博客数小于每页大小是不会展示分页组件的,分页配置通过Hexo配置文件进行设置

8、服务器部署,自动化部署

下一篇 博客自动化部署 详细记录下来

本文转载自: 掘金

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

1…671672673…956

开发者博客

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