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

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


  • 首页

  • 归档

  • 搜索

Kotlin Jetpack 实战 01 从一个膜拜大

发表于 2020-06-15

简介

本文属于《Kotlin Jetpack 实战》系列文章。

这是我用 Java 写的一个“原始架构”的 App,名字叫:KotlinJetpackInAction,它的功能只有一个:膜拜大神!

为了方便大家理解 Kotlin,Coroutines,Jetpack,Functional Programming,MMVM 这些新知识,这个 Demo 简单到了极致。随着文章的更新,我会一步步用 Kotlin,Jetpack 将其重构,然后再往里面加一些新功能。

截图

Android 界无人不知,无人不晓的神级存在:JakeWharton

工程结构

MainActivity:用来膜拜大神

ImagePreviewActivity:用来瞻仰大神的头像

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
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);
}
}
}

ImagePreviewActivity 源码

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

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

Intent intent = getIntent();
String url = intent.getStringExtra(MainActivity.EXTRA_PHOTO);
if (!TextUtils.isEmpty(url)) {
ImageView imageView = findViewById(R.id.imagePreview);
Glide.with(this).load(url).into(imageView);
}
}
}

结尾

我相信,即使是刚入门的小伙伴也能轻松看懂这个 Demo。

这也是本系列文章的不同之处,我会带着各位从这个简单 Demo 开始,一步步学习那些"高端"技术,最终把这个"简单"App变成"高端"App。

这个 Demo 已在 GitHub 开源,欢迎 star,fork,也欢迎提 Issue 说出你的建议: github.com/chaxiu/Kotl…

回目录–>《Kotlin Jetpack 实战》

都看到这了,点个赞呗!

本文转载自: 掘金

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

Kotlin Jetpack 实战|00 写给 Java

发表于 2020-06-14

简介

本文主要讲解 Kotlin 基础语法。

本文是《Kotlin Jetpack 实战》的开篇。

主要内容

每个 Java 开发者都应该学 Kotlin

快速认识 Kotlin

基础语法

扩展函数

委托

结尾

正文

每个 Java 开发者都应该学 Kotlin

推荐学习 Kotlin 的理由有很多,比如:Kotlin 更简洁,Kotlin 有协程,Kotlin 有扩展函数,学了 Kotlin 后学别的语言会很快,比如:Python,Swift,Dart, Ruby…

不过,如果你是 Android 开发者,我劝你别再做无谓的挣扎了,赶紧入坑吧!

快速认识 Kotlin

Kotlin 是著名 IDE 公司 JetBrains 创造出的一门基于 JVM 的语言。Kotlin 有着以下几个特点:

  • 简洁,1行顶5行
  • 安全,主要指“空安全”
  • 兼容,与 Java 兼容
  • 工具友好,IntelliJ 对 Kotlin 简直不要太友好

JetBrains 不仅创造了 Kotlin,还创造了著名的 IntelliJ IDEA。Android 开发者使用的 Android Studio 就是基于 IntelliJ 改造出来的。

基础语法

1. 所有 Kotlin 类都是对象 (Everything in Kotlin is an object)

与 Java 不一样是:Kotlin 没有基本数据类型 (Primitive Types),所有 Kotlin 里面的类都是对象,它们都继承自: Any这个类;与 Java 类似的是,Kotlin 提供了如下的内置类型:

Type Bit width 备注
Double 64 Kotlin 没有 double
Float 32 Kotlin 没有 float
Long 64 Kotlin 没有 long
Int 32 Kotlin 没有 int/Integer
Short 16 Kotlin 没有 short
Byte 8 Kotlin 没有 byte

思考题1:

既然 Kotlin 与 Java 是兼容的,那么 Kotlin Int 与 Java int、Java Integer 之间是什么关系?

思考题2:

Kotlin Any 类型与 Java Object 类型之间有什么关系?

2. 可见性修饰符 (Visibility Modifiers)

修饰符 描述
public 与Java一致
private 与Java一致
protected 与Java一致
internal 同 Module 内可见

3. 变量定义 (Defining Variables)

定义一个 Int 类型的变量:

1
kotlin复制代码var a: Int = 1

定义一个 Int 类型的常量(不可变的变量?只读的变量?)

1
kotlin复制代码val b: Int = 1

类型可推导时,类型申明可省略:

1
kotlin复制代码val c = 1

语句末尾的;可有可无:

1
2
kotlin复制代码val d: Int;
d = 1;

小结:

  • var 定义变量
  • val 定义常量(不可变的变量?只读变量?)
  • Kotlin 支持类型自动推导

思考题3:

Kotlin val 变量与 Java 的 final 有什么关系?

4 空安全 (Null Safety)

定义一个可为空的 String 变量:

1
2
3
4
kotlin复制代码var b: String? = "Kotlin"
b = null
print(b)
// 输出 null

定义一个不可为空的 String 变量:

1
2
3
kotlin复制代码var a: String = "Kotlin"
a = null
// 编译器报错,null 不能被赋给不为空的变量

变量赋值:

1
2
3
4
5
kotlin复制代码var a: String? = "Kotlin"
var b: String = "Kotlin"
b = a // 编译报错,String? 类型不可以赋值给 String 类型

a = b // 编译通过

空安全调用

1
2
3
4
kotlin复制代码var a: String? = "Kotlin"
print(a.length) // 编译器报错,因为 a 是可为空的类型
a = null
print(a?.length) // 使用?. 的方式调用,输出 null

Elvis 操作符

1
2
3
4
5
6
kotlin复制代码// 下面两个语句等价
val l: Int = if (b != null) b.length else -1
val l = b?.length ?: -1

// Elvis 操作符在嵌套属性访问时很有用
val name = userInstance?.user?.baseInfo?.profile?.name?: "Kotlin"

小结:

  • T 代表不可为空类型,编译器会检查,保证不会被 null 赋值
  • T? 代表可能为空类型
  • 不能将 T? 赋值给 T
  • 使用 instance?.fun() 进行空安全调用
  • 使用 Elvis 操作符为可空变量替代值,简化逻辑

5. 类型检查与转换 (Type Checks and Casts)

类型判断、智能类型转换:

1
2
3
4
kotlin复制代码if (x is String) {
print(x.length) // x 被编译自动转换为 String
}
// x is String 类似 Java 里的 instanceOf

不安全的类型转换 as

1
2
3
kotlin复制代码val y = null
val x: String = y as String
//抛异常,null 不能被转换成 String

安全的类型转换 as?

1
2
3
4
kotlin复制代码val y = null
val z: String? = y as? String
print(z)
// 输出 null

小结:

  • 使用 is 关键字进行类型判断
  • 使用 as 进行类型转换,可能会抛异常
  • 使用 as? 进行安全的类型转换

6. if 判断

基础用法跟 Java 一毛一样。它们主要区别在于:Java If is Statement,Kotlin If is Expression。因此它对比 Java 多了些“高级”用法,懒得讲了,咱看后面的实战吧。

7. for 循环

跟 Java 也差不多,随便看代码吧:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
kotlin复制代码// 集合遍历,跟 Java 差不多
for (item in collection) {
print(item)
}

// 辣鸡 Kotlin 语法
for (item in collection) print(item)

// 循环 1,2,3
for (i in 1..3) {
println(i)
}

// 6,4,2,0
for (i in 6 downTo 0 step 2) {
println(i)
}

8. when

when 就相当于高级版的 switch,它的高级之处在于支持模式匹配(Pattern Matching):

1
2
3
4
5
6
7
8
9
10
kotlin复制代码val x = 9
when (x) {
in 1..10 -> print("x is in the range")
in validNumbers -> print("x is valid")
!in 10..20 -> print("x is outside the range")
is String -> print("x is String")
x.isOdd() -> print("x is odd")
else -> print("none of the above")
}
// 输出:x is in the range

9. 相等性 (Equality)

Kotlin 有两种类型的相等性:

  • 结构相等 (Structural Equality)
  • 引用相等 (Referential Equality)

结构相等:

1
2
3
4
5
kotlin复制代码// 下面两句两个语句等价
a == b
a?.equals(b) ?: (b === null)
// 如果 a 不等于 null,则通过 equals 判断 a、b 的结构是否相等
// 如果 a 等于 null,则判断 b 是不是也等于 null

引用相等:

1
2
kotlin复制代码print(a === b)
// 判断 a、b 是不是同一个对象

思考题4:

1
2
3
4
5
6
kotlin复制代码val a: Int = 10000
val boxedA: Int? = a
val anotherBoxedA: Int? = a
print(boxedA == anotherBoxedA)
print(boxedA === anotherBoxedA)
// 输出什么内容?

思考题5:

1
2
3
4
5
6
kotlin复制代码val a: Int = 1
val boxedA: Int? = a
val anotherBoxedA: Int? = a
print(boxedA == anotherBoxedA)
print(boxedA === anotherBoxedA)
// 输出什么内容?

10. 函数 (Functions)

1
2
3
4
5
6
kotlin复制代码fun triple(x: Int): Int {
return 3 * x
}
// 函数名:triple
// 传入参数:不为空的 Int 类型变量
// 返回值:不为空的 Int 类型变量

11. 类 (Classes)

类定义

使用主构造器(Primary Constructor)定义类一个 Person 类,需要一个 String 类型的变量:

1
kotlin复制代码class Person constructor(firstName: String) { ... }

如果主构造函数没有注解或者可见性修饰符,constructor 关键字可省略:

1
kotlin复制代码class Person(firstName: String) { ... }

也可以使用次构造函数(Secondary Constructor)定义类:

1
2
3
4
5
6
kotlin复制代码class Person {
constructor(name: String) { ... }
}

// 创建 person 对象
val instance = Person("Kotlin")

init 代码块

Kotlin 为我们提供了 init 代码块,用于放置初始化代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
kotlin复制代码class Person {
var name = "Kotlin"
init {
name = "I am Kotlin."
println(name)
}

constructor(s: String) {
println(“Constructor”)
}
}

fun main(args: Array<String>) {
Person("Kotlin")
}

以上代码输出结果为:

1
2
css复制代码I am Kotlin.
Constructor

结论:init 代码块执行时机在类构造之后,但又在“次构造器”执行之前。

12. 继承 (Inheritance)

  • 使用 open 关键字修饰的类,可以被继承
  • 使用 open 关键字修饰的方法,可以被重写
  • 没有 open 关键字修饰的类,不可被继承
  • 没有 open 关键字修饰的方法,不可被重写
  • 以 Java 的思想来理解,Kotlin 的类和方法,默认情况下是 final 的

定义一个可被继承的 Base 类,其中的 add() 方法可以被重写,test() 方法不可被重写:

1
2
3
4
kotlin复制代码open class Base {
open fun add() { ... }
fun test() { ... }
}

定义 Foo 继承 Base 类,重写 add() 方法

1
2
3
kotlin复制代码class Foo() : Base() {
override fun add() { ... }
}
  • 使用 : 符号来表示继承
  • 使用 override 重写方法

13. This 表达式 (Expression)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
kotlin复制代码class A {

fun testA(){ }

inner class B { // 在 class A 定义内部类 B

fun testB(){ }

fun foo() {
this.testB() // ok
this.testA() // 编译错误
this@A.testA() // ok
this@B.testB() // ok
}
}
}

小结:

  • inner 关键字定义内部类
  • 在内部类当中访问外部类,需要显示使用 this@OutterClass.fun() 的语法

14. 数据类 (Data Class)

假设我们有个这样一个 Java Bean:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
java复制代码public class Developer {
private String name;

public Developer(String name) {
this.name = name;
}

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Developer developer = (Developer) o;
return name != null ? name.equals(developer.name) : developer.name == null;
}

@Override
public int hashCode() {
int result = name != null ? name.hashCode() : 0;
return result;
}

@Override
public String toString() {
return "Developer{" + "name='" + name + '}';
}
}

如果我们将其翻译成 Kotlin 代码,大约会是这样的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
kotlin复制代码class Developer(var name: String?) {

override fun equals(o: Any?): Boolean {
if (this === o) return true
if (o == null || javaClass != o.javaClass) return false
val developer = o as Developer?
return if (name != null) name == developer!!.name else developer!!.name == null
}

override fun hashCode(): Int {
return if (name != null) name!!.hashCode() else 0
}

override fun toString(): String {
return "Developer{" + "name='" + name + '}'.toString()
}
}

然而,Kotlin 为我们提供了另外一种选择,它叫做数据类:

1
kotlin复制代码data class Developer(var name: String)

上面这一行简单的代码,完全能替代前面我们的写的那一大堆模板 Java 代码,甚至额外多出了一些功能。如果将上面的数据类翻译成等价的 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 Developer {
@NotNull
private String name;

public Developer(@NotNull String name) {
super();
this.name = name;
}

@NotNull
public final String getName() { return this.name; }

public final void setName(@NotNull String var1) { this.name = var1; }

@NotNull
public final Developer copy(@NotNull String name) { return new Developer(name); }

public String toString() { return "Developer(name=" + this.name + ")"; }

public int hashCode() { return this.name != null ? this.name.hashCode() : 0; }

public boolean equals(Object var1) {
if (this != var1) {
if (var1 instanceof Developer) {
Developer var2 = (Developer)var1;
if (Intrinsics.areEqual(this.name, var2.name)) {
return true;
}
}
return false;
} else {
return true;
}
}
}

可以看到,Kotlin 的数据类不仅为我们提供了 getter、setter、equals、hashCode、toString,还额外的帮我们实现了 copy 方法!这也体现了 Kotlin 的简洁特性。

序列化的坑

如果是旧工程迁移到 Kotlin,那么可能需要注意这个坑:

1
2
3
4
kotlin复制代码// 定义一个数据类,其中成员变量 name 是不可为空的 String 类型,默认值是 MOMO
data class Person(val age: Int, val name: String = "Kotlin")
val person = gson.fromJson("""{"age":42}""", Person::class.java)
print(person.name) // 输出 null

对于上面的情况,由于 Gson 最初是为 Java 语言设计的序列化框架,并不支持 Kotlin 不可为空、默认值这些特性,从而导致原本不可为空的属性变成null,原本应该有默认值的变量没有默认值。

对于这种情,市面上已经有了解决方案:

  • kotlinx.serialization
  • moshi

15. 扩展 (Extensions)

如何才能在不修改源码的情况下给一个类新增一个方法?比如我想给 Context 类增加一个 toast 类,怎么做?

如果使用 Java,上面的需求是无法被满足的。然而 Kotlin 为我们提供了扩展语法,让我们可以轻松实现以上的需求。

扩展函数

为 Context 类定义一个 toast 方法:

1
2
3
kotlin复制代码fun Context.toast(msg: String, length: Int = Toast.LENGTH_SHORT){
Toast.makeText(this, msg, length).show()
}

扩展函数的使用:

1
2
3
kotlin复制代码val activity: Context? = getActivity()
activity?.toast("Hello world!")
activity?.toast("Hello world!", Toast.LENGTH_LONG)

属性扩展

除了扩展函数,Kotlin 还支持扩展属性,用法基本一致。

思考题6:

上面的例子中,我们给不可为空的 Context 类增加了扩展函数,因此我们在使用这个方法的时候需要判空。实际上,Kotlin 还支持我们为 可为空的 类增加扩展函数:

1
2
3
4
5
kotlin复制代码// 为 Context? 添加扩展函数
fun Context?.toast(msg: String, length: Int = Toast.LENGTH_SHORT){
if (this == null) { //do something }
Toast.makeText(this, msg, length).show()
}

扩展函数使用:

1
2
3
kotlin复制代码val activity: Context? = getActivity()
activity.toast("Hello world!")
activity.toast("Hello world!", Toast.LENGTH_LONG)

请问这两种定义扩展函数的方式,哪种更好?分别适用于什么情景?为什么?

16. 委托 (Delegation)

Kotlin 中,使用by关键字表示委托:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
kotlin复制代码interface Animal {
fun bark()
}

// 定义 Cat 类,实现 Animal 接口
class Cat : Animal {
override fun bark() {
println("喵喵")
}
}

// 将 Zoo 委托给它的参数 animal
class Zoo(animal: Animal) : Animal by animal

fun main(args: Array<String>) {
val cat = Cat()
Zoo(cat).bark()
}
// 输出结果:喵喵

属性委托 (Property Delegation)

其实,从上面类委托的例子中,我们就能知道,Kotlin 之所以提供委托这个语法,主要是为了方便我们使用者,让我们可以很方便的实现代理这样的模式。这一点在 Kotlin 的委托属性这一特性上体现得更是淋漓尽致。

Kotlin 为我们提供的标准委托非常有用。

by lazy 实现”懒加载“

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
kotlin复制代码// 通过 by 关键字,将 lazyValue 属性委托给 lazy {} 里面的实现
val lazyValue: String by lazy {
val result = compute()
println("computed!")
result
}

// 模拟计算返回的变量
fun compute():String{
return "Hello"
}

fun main(args: Array<String>) {
println(lazyValue)
println("=======")
println(lazyValue)
}

以上代码输出的结果:

1
2
3
4
markdown复制代码computed!
Hello
=======
Hello

由此可见,by lazy 这种委托的方式,可以让我们轻松实现懒加载。
其内部实现,大致是这样的:
by lazy 执行流程

lazy 求值的线程模式: LazyThreadSafetyMode

Kotlin 为lazy 委托提供三种线程模式,他们分别是:

  • LazyThreadSafetyMode.SYNCHRONIZED
  • LazyThreadSafetyMode.NONE
  • LazyThreadSafetyMode.PUBLICATION

上面这三种模式,前面两种很好理解:

  1. LazyThreadSafetyMode.SYNCHRONIZED 通过加锁实现多线程同步,这也是默认的模式。
  2. LazyThreadSafetyMode.NONE 则没有任何线程安全代码,线程不安全。

我们详细看看LazyThreadSafetyMode.PUBLICATION,官方文档的解释是这样的:

Initializer function can be called several times on concurrent access to uninitialized [Lazy] instance value, but only the first returned value will be used as the value of [Lazy] instance.

意思就是,用LazyThreadSafetyMode.PUBLICATION模式的 lazy 委托变量,它的初始化方法是可能会被多个线程执行多次的,但最后这个变量的取值是仅以第一次算出的值为准的。即,哪个线程最先算出这个值,就以这个值为准。

by Delegates.observable 实现”观察者模式”的变量

观察者模式,又被称为订阅模式。最常见的场景就是:比如读者们订阅了MOMO公众号,每次MOMO更新的时候,读者们就会收到推送。而观察者模式应用到变量层面,就延伸成了:如果这个的值改变了,就通知我。

1
2
3
4
5
6
7
8
9
10
11
12
13
kotlin复制代码class User {
// 为 name 这个变量添加观察者,每次 name 改变的时候,都会执行括号内的代码
var name: String by Delegates.observable("<no name>") {
prop, old, new ->
println("name 改变了:$old -> $new")
}
}

fun main(args: Array<String>) {
val user = User()
user.name = "first: Tom"
user.name = "second: Jack"
}

以上代码的输出为:

1
2
sql复制代码name 改变了:<no name> -> first: Tom
name 改变了:first: Tom -> second: Jack

思考题7:

lazy 委托的LazyThreadSafetyMode.PUBLICATION适用于什么样的场景?

结尾

都看到这了,点个赞呗!

下一章–>从一个膜拜大神的 Demo 开始

回目录–>《Kotlin Jetpack 实战》

本文转载自: 掘金

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

Kotlin Jetpack 实战|目录

发表于 2020-06-14

简介

Kotlin,协程,Jetpack ,Compose,沉思录。

推荐

我辞职期间,全身心投入,写了一个技术专栏《Kotlin 编程第一课》 ,写完专栏才入职新公司。

现在已有几千人学过,推荐给你:

屏幕快照 2022-06-15 上午9.44.02.png

第一部分:快速入门 Kotlin(已完结)

我为什么要写 《Kotlin Jetpack 实战》?

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

第二部分:Kotlin 编程的艺术(已完结)

将那些最复杂的技术,应用到这个最简单的 App 里!

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

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

《03. Kotlin 编程的三重境界》

《04. Kotlin 高阶函数》

《05. Kotlin 泛型》

《06. Kotlin 扩展》

《07. Kotlin 委托》

《08. 协程不为人知的调试技巧》

《09. 图解协程:suspend》

第三部分:Kotlin 思维篇(已完结)

《Kotlin编程第一课》更新完毕。

《什么是“函数式思维”?》

《什么是“表达式思维”?》

《什么是“不变性思维”?》

《什么是“空安全思维”?》

《什么是“协程思维模型”?》

第四部分:协程篇(已完结)

《Kotlin编程第一课》更新完毕。

《如何启动协程?》

《挂起函数:Kotlin协程的核心》

《Job:协程也有生命周期吗?》

《context:万物皆为Context?》

《实战:让KtHttp支持“挂起函数”》

《Channel:为什么说Channel是“热”的?》

《Flow:为什么说Flow是“冷”的?》

《Select:到底是在选择什么?》

《并发:协程不需要处理同步吗?》

《异常:try catch为什么会不起作用?坑!》

《实战:让KtHttp支持Flow》

第五部分:协程源码篇(已完结)

《Kotlin编程第一课》更新完毕。

《协程源码的地图:如何读源码才不会迷失?》

《图解“挂起函数”:原来你就是个状态机?》

《深入理解协程基础元素》

《launch的背后到底发生了什么?》

《Dispatchers是如何工作的?》

《CoroutineScope是如何管理协程的?》

《图解Channel:如何理解它的CSP通信模型?》

《图解Flow:原来你是只纸老虎?》

第六部分:Jetpack 篇

更新中……

《Android 强推的 Baseline Profiles 国内能用吗?我找 Google 工程师求证了!》

第七部分:Jetpack Compose 篇

Kotlin 写 UI 真的太爽了!

《2小时入门 Compose(上)》

《图解 Compose 原理:揭秘 Composable 的本质》

联系方式

如果你想第一时间看到更新,可关注我的公众号:朱涛的自习室

Day1.003.jpeg

如果你对《Kotlin Jetpack 实战》有什么建议,或者希望我往里面加哪些内容,可以通过公众号加我的个人微信。

都看到这了,给点个赞呗!

本文转载自: 掘金

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

Kafka面试:基础27问,应该都会?

发表于 2020-06-14

❝
消息队列也叫 MQ(Message Queue)。Kafka作为消息队列中的优秀平台,被很多公司使用,是一种高吞吐量的分布式发布订阅消息系统,本篇给大家总结了27道Kafka知识点或者说面试题,持续更新中。。。

❞

1.什么是kafka?

Apache Kafka是由Apache开发的一种发布订阅消息系统。

2.kafka的3个关键功能?

  • 发布和订阅记录流,类似于消息队列或企业消息传递系统。
  • 以容错的持久方式存储记录流。
  • 处理记录流。

3.kafka通常用于两大类应用?

  • 建立实时流数据管道,以可靠地在系统或应用程序之间获取数据
  • 构建实时流应用程序,以转换或响应数据流

4.kafka特性?

  1. 消息持久化
  2. 高吞吐量
  3. 扩展性
  4. 多客户端支持
  5. Kafka Streams
  6. 安全机制
  7. 数据备份
  8. 轻量级
  9. 消息压缩

5.kafka的5个核心Api?

  • Producer API
  • Consumer API
  • Streams API
  • Connector API
  • Admin API

6.什么是Broker(代理)?

Kafka集群中,一个kafka实例被称为一个代理(Broker)节点。

7.什么是Producer(生产者)?

消息的生产者被称为Producer。

Producer将消息发送到集群指定的主题中存储,同时也自定义算法决定将消息记录发送到哪个分区?

8.什么是Consumer(消费者)?

消息的消费者,从kafka集群中指定的主题读取消息。

9.什么是Topic(主题)?

主题,kafka通过不同的主题却分不同的业务类型的消息记录。

10.什么是Partition(分区)?

每一个Topic可以有一个或者多个分区(Partition)。

11.分区和代理节点的关系?

一个分区只对应一个Broker,一个Broker可以管理多个分区。

12.什么是副本(Replication)?

每个主题在创建时会要求制定它的副本数(默认1)。

13.什么是记录(Record)?

实际写入到kafka集群并且可以被消费者读取的数据。

每条记录包含一个键、值和时间戳。

14.kafka适合哪些场景?

日志收集、消息系统、活动追踪、运营指标、流式处理、时间源等。

15.kafka磁盘选用上?

SSD的性能比普通的磁盘好,这个大家都知道,实际中我们用普通磁盘即可。它使用的方式多是顺序读写操作,一定程度上规避了机械磁盘最大的劣势,即随机读写操作慢,因此SSD的没有太大优势。

16.使用RAID的优势?

  • 提供冗余的磁盘存储空间
  • 提供负载均衡

17.磁盘容量规划需要考虑到几个因素?

  • 新增消息数
  • 消息留存时间
  • 平均消息大小
  • 备份数
  • 是否启用压缩

18.Broker使用单个?多个文件目录路径参数?

log.dirs 多个

log.dir 单个

19.一般来说选择哪个参数配置路径?好处?

log.dirs

好处:

提升读写性能,多块物理磁盘同时读写高吞吐。

故障转移。一块磁盘挂了转移到另一个上。

20.自动创建主题的相关参数是?

auto.create.topics.enable

21.解决kafka消息丢失问题?

  • 不要使用 producer.send(msg),而要使用 producer.send(msg, callback)。
  • 设置 acks = all。
  • 设置 retries 为一个较大的值。
  • 设置 unclean.leader.election.enable = false。
  • 设置 replication.factor >= 3。
  • 设置 min.insync.replicas > 1。
  • 确保 replication.factor > min.insync.replicas。
  • 确保消息消费完成再提交。

22.如何自定分区策略?

显式地配置生产者端的参数partitioner.class

参数为你实现类的 全限定类名,一般来说实现partition方法即可。

23.kafka压缩消息可能发生的地方?

Producer 、Broker。

24.kafka消息重复问题?

做好幂等。

数据库方面可以(唯一键和主键)避免重复。

在业务上做控制。

25.你知道的kafka监控工具?

  • JMXTool 工具
  • Kafka Manager
  • Burrow
  • JMXTrans + InfluxDB + Grafana
  • Confluent Control Center

26.kafka系统支持两种不同发送方式?

异步模式

同步模式

27.消费者和消费者组区别?

一个消费者组,可以有一个或者多个消费者程序。

消费者组名(GroupID)一般由具有唯一性字符串表示。

如果一个消费者组订阅了主题,则该主题每个分区只能分配给某一个消费者组中的某一个消费者程序。

参考:

  • 《Kafka并不难学》
  • 《kafka入门与实践》
  • 极客时间:Kafka核心技术与实战
  • http://kafka.apache.org/

本文使用 mdnice 排版

本文转载自: 掘金

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

Jetpack 最新成员 AndroidX App Star

发表于 2020-06-13

前言

前几天 Google 更新了几个 Jetpack 新成员 Hilt、Paging 3、App Startup 等等,周末空闲时间实践了一下 App Startup 可以前去查看 GitHub 上的项目 AndroidX-Jetpack-Practice ,接下来一起来分析一下 AndroidX App Startup。

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

  • App Startup 是什么?
  • App Startup 为我们解决了什么问题?
  • 为什么无论是 Google 还是第三方库,初始化时都会在 ContentProvider 里面进行初始化?
  • 在 ContentProvider 里初始化会带来什么性能问题?
  • ContentProvider 启动顺序源码分析?
  • 如何正确使用 App Startup?
    • 自动初始化。
    • 手动初始化(也是延迟初始化)。

App Startup 是什么?

来自 Google 文档: App Startup 是 Android Jetpack 最新成员,提供了在 App 启动时初始化组件简单、高效的方法,无论是 library 开发人员还是 App 开发人员都可以使用 App Startup 显示的设置初始化顺序。

简单的说就是 App Startup 提供了一个 ContentProvider 来运行所有依赖项的初始化,避免每个第三方库单独使用 ContentProvider 进行初始化,从而提高了应用的程序的启动速度。

无论是 Google 提供的库还是第三方库,启动时运行一些初始化逻辑并不少见,例如 WorkManager 在应用启动时使用 ContentProvider 进行初始化,来看一下 Google 工程师 Husayn Hakeem 分享的一张的图。

LibraryA, LibraryB, and LibraryC initialized using their own ContentProviders

上图表示现在我们有三个库分别 LibraryA、LibraryB、和 LibraryC 它们使用自己的 ContentProviders 进行初始化。

而 App Startup 提供了一个 ContentProvider 来运行所有依赖项的初始化(LibraryA、LibraryB、和 LibraryC),如下图所示。

LibraryA, LibraryB, and LibraryC initialized by AndroidX Startup

AndroidX App Startup 为我们解决了什么问题?

刚才我们说到无论是 Google 提供的库还是第三方库,App 启动运行时会初始化一些逻辑,它们为了方便开发者使用,避免开发者手动调用,使用 ContentProvider 进行初始化,例如 WorkManager 在应用启动时使用 ContentProvider 进行初始化,我们来看一下 WorkManager 的源码,先来看一下 AndroidManifest.xml 文件内容。

如上所见,我们可以看到在 AndroidManifest.xml 文件内定义了一个名为 WorkManagerInitializer 的 ContentProvider,我来看看 WorkManagerInitializer 里面都做了什么。

1
2
3
4
5
6
7
8
9
10
11
scala复制代码public class WorkManagerInitializer extends ContentProvider {
@Override
public boolean onCreate() {
// Initialize WorkManager with the default configuration.
WorkManager.initialize(getContext(), new Configuration.Builder().build());
return true;
}

......
// 省略了没用的代码
}

如上所见其实就是在 WorkManagerInitializer 的 onCreate() 方法里面,使用默认配置初始化 WorkManager。

我们也来模仿 WorkManager 写一个 Demo,这里只贴出部分代码,更多信息查看 GitHub 上的 AppStartupSimple 下面的 ContentProvider 模块。

  • 定义一个 WorkContentProvider 并在 onCreate 方法中打印一行日志。
1
2
3
4
5
6
7
8
9
kotlin复制代码class WorkContentProvider : ContentProvider() {

override fun onCreate(): Boolean {
Log.d(TAG, "WorkContentProvider create()")
return true
}

.....
}
  • 在 AndroidManifest.xml 文件中注册 WorkContentProvider。
1
2
3
4
5
6
ini复制代码<application>
<provider
android:name=".WorkContentProvider"
android:authorities="${applicationId}.provider"
android:exported="false" />
</application>
  • 运行 App 日志如下所示。
1
csharp复制代码com.hi.dhl.startup.simple D/WorkContentProvider: WorkContentProvider create()

假设你的 App 有很多类似于 WorkManager 这样的库,都在 ContentProvider 里面进行一些初始化工作,在 App 启动时运行多个 ContentProvider,这样会带来一些问题:

  • 多个 ContentProvider 会增加了 App 启动运行的时间。
  • ContentProvider 的 onCreate 方法会先于 Application 的 OnCreate 方法执行,这是在冷启动阶段自动运行初始化的,来看一下 Android 10 系统源码。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
scss复制代码private void handleBindApplication(AppBindData data) {
......

if (!data.restrictedBackupMode) {
if (!ArrayUtils.isEmpty(data.providers)) {
// 创建ContentProvider
installContentProviders(app, data.providers);
}
}

......

try {
// 调用调用 Application 的 OnCreate 方法
mInstrumentation.callApplicationOnCreate(app);
} catch (Exception e) {
......
}

......
}

这是在 App 冷启动时自动运行初始化的,这样只会增加 App 的加载时间,用户希望 App 加载得快,启动慢会带来糟糕的用户体验,AndroidX App Startup 正是为了解决这个问题而出现的。

如何正确使用 AndroidX App Startup?

使用 AndroidX App Startup 来运行所有依赖项的初始化有两种方式:

  • 自动初始化。
  • 手动初始化(也是延迟初始化)。

具体可以查看 GitHub 上的 AppStartupSimple 下面的 Startup-Library 模块相关代码。

自动初始化

  • 在 build.gradle 文件内添加依赖。
1
arduino复制代码implementation "androidx.startup:startup-runtime:1.0.0-alpha01"
  • 实现 Initializer 接口,并重写两个方法,来初始化组件。
1
2
3
4
5
6
7
8
9
10
11
12
kotlin复制代码class LibaryC : Initializer<LibaryC.Dependency> {
override fun create(context: Context): Dependency {
// 初始化工作
Log.e(TAG, "init LibaryC ")
return Dependency()
}

override fun dependencies(): MutableList<Class<out Initializer<*>>> {
return mutableListOf(LibaryB::class.java)
}
......
}
  • create(Context): 这里进行组件初始化工作。
  • dependencies(): 返回需要初始化的列表,同时设置 App 启动时依赖库运行的顺序,假设
    LibaryC 依赖于 LibaryB,LibaryB 依赖于 LibaryA,App 启动运行时,会先运行 LibaryA 然后运行 LibaryB 最后运行 LibaryC。

正如 GitHub 上的 AppStartupSimple 示例项目,它依赖结构就是 LibaryC 依赖于 LibaryB,LibaryB 依赖于 LibaryA,输出结果如下所示:

1
2
3
csharp复制代码com.hi.dhl.startup.simple E/LibaryA: init LibaryA 
com.hi.dhl.startup.simple E/LibaryB: init LibaryB
com.hi.dhl.startup.simple E/LibaryC: init LibaryC
  • 在 AndroidManifest.xml 文件中注册 InitializationProvider。
1
2
3
4
5
6
7
8
9
10
11
12
13
xml复制代码<application>
<provider
android:name="androidx.startup.InitializationProvider"
android:authorities="${applicationId}.androidx-startup"
android:exported="false"
tools:node="merge">

<!-- 自动初始化 -->
<meta-data
android:name="com.hi.dhl.startup.library.LibaryC"
android:value="androidx.startup" />
</provider>
</application>

App 启动的时 App Startup 会读取 AndroidManifest.xml 文件里面的 InitializationProvider 下面的 <meta-data> 声明要初始化的组件,完成自动初始化工作。

手动初始化(也是延迟初始化)

  • 在 build.gradle 文件内添加依赖,和上文一样。
  • 创建一个类 LibaryD 实现 Initializer 接口,并重写两个方法,来初始化组件,和上文一样。
  • 在 AndroidManifest.xml 文件中注册 InitializationProvider。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
ini复制代码<application>
<provider
android:name="androidx.startup.InitializationProvider"
android:authorities="${applicationId}.androidx-startup"
android:exported="false"
tools:node="merge">
<!--
手动初始化(也是延迟初始化)
在 `<meta-data>` 标签内添加 `tools:node="remove"`
-->
<meta-data
android:name="com.hi.dhl.startup.library.LibaryD"
android:value="androidx.startup"
tools:node="remove" />
</provider>
</application>
  • 禁用单个组件的自动初始化,需要在 <meta-data> 标签内添加 tools:node="remove" 清单合并工具会将它从清单文件中删除。
  • 禁用所有组件初始化,需要在 provider 标签内添加 tools:node="remove" 清单合并工具会将它从清单文件中删除。
1
2
3
4
5
6
7
8
xml复制代码<!-- 禁用所有组件初始化 -->
<provider
android:name="androidx.startup.InitializationProvider"
android:authorities="${applicationId}.androidx-startup"
android:exported="false"
tools:node="remove">
......
</provider>
  • 在需要的地方进行初始化,调用以下代码进行初始化。
1
scss复制代码AppInitializer.getInstance(context).initializeComponent(MyInitializer::class.java)

如果组件初始化之后,再次调用 AppInitializer.initializeComponent() 方法不会再次初始化。

手动初始化(也是延迟初始化)是非常有用的,组件不需要在 App 启动时运行,只需要在需要它地方运行,可以减少 App 的启动时间,提高启动速度。

全文到这里就结束了,App Startup 和 ContentProvider 相关示例已经上传到 GitHub 上了
AndroidX-Jetpack-Practice:https://github.com/hi-dhl/AndroidX-Jetpack-Practice

正在建立一个最全、最新的 AndroidX Jetpack 相关组件的实战项目 以及 相关组件原理分析文章,仓库持续更新中,可以前去查看:AndroidX-Jetpack-Practice

总结

这篇文章主要介绍了以下内容:

  • ContentProvider 启动顺序源码分析。
  • App Startup 是 Jetpack 的新成员,是为了解决因 App 启动时运行多个 ContentProvider 会增加 App 的启动时间的问题。
  • 使用了一个 InitializationProvider 管理多个依赖项,消除了每个库单独使用 ContentProvider 成本,减少初始化时间。
  • App Startup 允许你自定义组件初始化顺序。
  • App Startup 可以自动初始化 AndroidManifest.xml 文件中 InitializationProvider 下面的 <meta-data> 声明要初始化的组件。
  • App Startup 提供了一种延迟初始化组件的方法,减少 App 初始化时间。

在 AndroidManifest.xml 文件中声明 node="remove" 打包的时候会删除?这样做的目的是什么?

  • 便于管理所有的初始化项
  • 禁用组件自动初始化也将禁用该组件依赖项的自动初始化
  • 确保合并工具从所有其他合并清单文件中删除

参考文献

  • developer.android.com/topic/libra…
  • proandroiddev.com/androidx……

结语

关注公众号:ByteCode,查看一系列 Android 系统源码、逆向分析、算法、译文、Kotlin、Jetpack 源码相关的文章,如果这篇文章对你有帮助,请帮我点个 star,感谢!!!,欢迎一起来学习,在技术的道路上一起前进。


最后推荐我一直在更新维护的项目和网站:

  • 计划建立一个最全、最新的 AndroidX Jetpack 相关组件的实战项目 以及 相关组件原理分析文章,正在逐渐增加 Jetpack 新成员,仓库持续更新,欢迎前去查看:AndroidX-Jetpack-Practice
  • LeetCode / 剑指 offer / 国内外大厂面试题 / 多线程 题解,语言 Java 和 kotlin,包含多种解法、解题思路、时间复杂度、空间复杂度分析


+ 剑指 offer 及国内外大厂面试题解:在线阅读
+ LeetCode 系列题解:在线阅读

  • 最新 Android 10 源码分析系列文章,了解系统源码,不仅有助于分析问题,在面试过程中,对我们也是非常有帮助的,仓库持续更新,欢迎前去查看 Android10-Source-Analysis
  • 整理和翻译一系列精选国外的技术文章,每篇文章都会有译者思考部分,对原文的更加深入的解读,仓库持续更新,欢迎前去查看 Technical-Article-Translation
  • 「为互联网人而设计,国内国外名站导航」涵括新闻、体育、生活、娱乐、设计、产品、运营、前端开发、Android 开发等等网址,欢迎前去查看 为互联网人而设计导航网站

历史文章

  • 为数不多的人知道的 Kotlin 技巧以及 原理解析(一)
  • 为数不多的人知道的 Kotlin 技巧以及 原理解析(二)
  • 为数不多的人知道的 AndroidStudio 快捷键(一)
  • 为数不多的人知道的 AndroidStudio 快捷键(二)
  • 再见吧 buildSrc, 拥抱 Composing builds 提升 Android 编译速度
  • Jetpack 最新成员 AndroidX App Startup 实践以及原理分析
  • Jetpack 成员 Paging3 实践以及源码分析(一)
  • Jetpack 新成员 Paging3 网络实践及原理分析(二)
  • Jetpack 新成员 Hilt 实践(一)启程过坑记
  • Jetpack 新成员 Hilt 实践之 App Startup(二)进阶篇
  • Jetpack 新成员 Hilt 与 Dagger 大不同(三)落地篇
  • 全方面分析 Hilt 和 Koin 性能
  • 神奇宝贝(PokemonGo) 眼前一亮的 Jetpack + MVVM 极简实战
  • Google 推荐在项目中使用 sealed 和 RemoteMediator
  • Kotlin Sealed 是什么?为什么 Google 都用
  • Kotlin StateFlow 搜索功能的实践 DB + NetWork

本文转载自: 掘金

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

面试官:这7张图要是都学会,我还问什么mysql调优(建议收

发表于 2020-06-13

前言

MySQL 为关系型数据库(Relational Database Management System),一个关系型数据库由一个或数个表格组成, 表格一般包括以下:

  • 表头(header): 每一列的名称;
  • 列(col): 具有相同数据类型的数据的集合;
  • 行(row): 每一行用来描述某个人/物的具体信息;
  • 值(value): 行的具体信息, 每个值必须与该列的数据类型相同;
  • 键(key): 表中用来识别某个特定的人物的方法, 键的值在当前列中具有唯一性。

但是就是这些简简单单的表格,却衍生出了相当多的问题,尤其是随着互联网时代的发展,没得办法,为了用户体验更加流程,对于数据库的优化等问题成为了在面试的过程中被经常问起的话题,但是,道高一尺魔高一丈,程序猿会被屈服吗?不存在的

7张图(2xmind+5张知识),总结mysql从架构一直到使用和调优的相关知识点

平台原因,上传图片有点模糊,需要高清图的老铁,关注+转发,私信“资料”即可

mysql架构

mysql数据结构

mysql索引系统

B+树添加和删除数据图解

红黑树

mysql的xmind图

学习怎么能没有系统的梳理,程序猿梳理知识点靠什么,就是知识脑图,反正我是很喜欢这种方式,上面的图里面的知识,看起来没什么顺序的话,没关系,xmind图我已经为大家准备好了

因为展开后这张图实在太大了,所以就关闭了

mysql优化xmind图

在一开始的时候,我也说了,常规的sql编写,其实在笔试中考察比较多,这个就是个人的一个技术能力的考察了,不是说能够取巧的,但是对于优化,更多的是理论的考察,那这些怎么办呢?关于mysql索引优化的相关知识图谱奉上

同样的。因为展开后这张图实在太大了,所以就截取了一部分

同样的,害怕有些老铁可能刚接触这些东西,看的不是特别清晰,那么针对这些图片中的知识,我也录制了这样的一套视频,我不信还会有人说学不明白

需要上面视频和知识图的老铁,关注公众号:Java架构师联盟,后台回复mysql即可

本文转载自: 掘金

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

分享一个springboot脚手架

发表于 2020-06-13

项目介绍

在我们开发项目的时候各个项目之间总有一些可共用的代码或者配置,如果我们每新建一个项目就把代码复制粘贴再修改就显得很没有必要。于是我就做了一个 poseidon-boot-starter 该项目是基于 spring-boot的 starter 功能开发的,因此只适用于 spring-boot 项目。该项目集成了如下功能:

  • 异常通知
  • 权限配置
  • 幂等锁
  • 日志配置
  • 用户操作日志记录
  • 查询接口通用化

项目地址:github.com/muggle0/pos…

下面介绍该组件如何在我们的 spring-boot 项目中使用。

首先我们需要下载下来这个项目:

1
awk复制代码git clone https://github.com/muggle0/poseidon-boot-starter.git

然后安装到我们的本地仓库或者私有云:

1
2
3
stata复制代码cd poseidon-boot-starter

mvn install

安装完成之后在spring boot 项目中引入依赖:

1
2
3
4
xml复制代码<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-autoconfigure</artifactId>
</dependency>

然后进行一些基础的配置:

1
2
3
4
5
6
7
ini复制代码poseidon.auto=true
poseidon.static-path=/**/*.*
poseidon.ignore-path=/**
logging.config=classpath:poseidon-logback.xml
log.dir=logs
logging.level.com.muggle=debug
spring.profiles.include=refresh

自动化配置默认是不开启的,我们需要使用 poseidon.auto=true 来启用相关功能,当开启自动化配置之后,我们必须要实现两个接口并注入到spring容器—— com.muggle.poseidon.store.SecurityStore 和 com.muggle.poseidon.service.TokenService 。poseidon.static-path 是 ant 匹配的静态资源路径,符合该规则的url不会被权限过滤器拦截,poseidon.ignore-path 是鉴权忽略规则,符合该规则的url不会参与鉴权,直接放行。logging.config=classpath:poseidon-logback.xml 则是采用 poseidon-boot-starter 中的logback配置策略(五彩斑斓的黑),如果采用该配置则必须指定 log.dir 日志文件输出路径。logging.level.com.muggle=debug 是指定包名以debug的级别输出,方便看一些日志调试。spring.profiles.include=refresh 当指定这个 profile 的时候,会去获取当前项目的所有url并交给 tokenService去处理。还有其他默认不开启的功能,在源码解读中介绍。

源码解读

前文我们提到过,该项目是基于 springboot 的 starter 功能开发的,其原理就是一个 springboot 定制版的 spi 这里不做太多介绍,这里我主要介绍如何在项目中使用的。

首先在 META-INF/spring.factories,中指定了要注入的类有哪些:

1
2
3
4
5
stylus复制代码org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.muggle.poseidon.auto.ExpansibilityConfig,\
com.muggle.poseidon.auto.SecurityAutoConfig,\
com.muggle.poseidon.handler.web.WebUrlHandler,\
com.muggle.poseidon.handler.web.WebResultHandler

ExpansibilityConfig 是预留的配置类,实际未使用,SecurityAutoConfig 是整合 spring-security 相关的配置。WebUrlHandler 是处理一些特殊的url的。WebResultHandler 是统一异常处理配置。这几个类都通过 @ConditionalOnProperty(prefix = "poseidon", name = "auto", havingValue = "true", matchIfMissing = false) 来控制是否自动配置。配置类具体的源码细节这里就不介绍了。下面对各个功能的源码进行解读。

security

项目集成了security,并重写了处理器和鉴权相关的类,改造成了纯返回json,并从请求头中获取token的方式。首先我们看重写了哪些处理器:

  • com.muggle.poseidon.handler.security.PoseidonAccessDeniedHandler 鉴权失败处理器;
  • com.muggle.poseidon.handler.security.PoseidonAuthenticationFailureHandler 登录失败处理器;
  • com.muggle.poseidon.handler.security.PoseidonAuthenticationSuccessHandler 登录成功处理器;
  • com.muggle.poseidon.handler.security.PoseidonLoginUrlAuthenticationEntryPoint 未登录处理器;
  • com.muggle.poseidon.handler.security.PoseidonLogoutSuccessHandler 登出成功处理器。

以上几个处理器都是返回json的数据,如果需要修改json格式或者需要改成重定向的方式,需要手动去找到相关处理器去修改;因为这部分相关工作(比如重定向或者提示信息)都可以在前端解决,所以这里未做扩展处理。

然后是 token过滤器 com.muggle.poseidon.filter.SecurityTokenFilter,该过滤器会首先从请求头中获取token,如果获取失败则会从cookie 中获取token,key都是 token,获取到token后调用 securityStore.getUserdetail(String token) 得到一个 UserDetails ,因此,怎么通过token获取用户信息需要使用者自己去扩展,你可以直接从数据库中读,或者从缓存中读,或者直接就像jwt那样,通过解析token生成。在接下来的鉴权流程中。会从该 UserDetails 中获取 GrantedAuthority 集合 和 url 一并传递给 rooleMatch(Collection<? extends GrantedAuthority> authorities, String path) 去鉴权(如果匹配为 IgnorePath 则不鉴权直接通过)。这里的鉴权方案也是需要使用者去自己实现,鉴权方案肯定是通过匹配url来实现,那么怎么去匹配设计方案就很多了,这里提供几个思路:

  1. 当配置 spring.profiles.include=refresh 的时候会去获取项目中的所有url和相关的swagger注释。交给 TokenService.processUrl(List<AuthUrlPathDO> list) 去处理,你可以保存到数据库,为后续鉴权提供依据。
  2. 你可以制定一套url的命名规则,当鉴权的时候和 GrantedAuthority 进行直接匹配,通过规则我们就能直接判断哪些用户是有权限访问的了。
  3. 前端发请求的时候,在url末尾带上一个参数来指定哪些角色可访问(不安全,可通过伪造请求跳过鉴权)。

在 TokenService 和 SecurityStore 中还有其他相关的方法,如登入登出等,这里不做介绍了,请参看源码注释。

统一异常处理

统一异常处理相关的类是 WebResultHandler 它定义了一些异常信息的处理策略。如果你不想要这些策略可以直接删掉它,或者自己重新注入一个异常处理器,如果你想扩展它,那么你可以参考项目中readme.md文档中的案例:

1
2
3
4
5
6
7
8
9
10
11
scala复制代码@RestControllerAdvice
@Configuration
public class MyWebResultHandler extends WebResultHandler {
private static final Logger log = LoggerFactory.getLogger(OAwebResultHandler.class);
@ExceptionHandler({ConstraintViolationException.class})
public ResultBean methodArgumentNotValidException(ConstraintViolationException e, HttpServletRequest req) {
log.error("参数未通过校验", e);
ResultBean error = ResultBean.error(e.getConstraintViolations().iterator().next().getMessage());
return error;
}
}

需要注意的一个地方,如果我们项目中出现了未知的异常,应该要引起重视,因此当发生未知异常的时候会抛出一个事件。使用者可以注册监听器来监听这个事件,当发生未知的异常的时候可以及时的通知到开发人员,示例:

1
2
3
4
5
6
7
8
9
typescript复制代码@Component
public class ExceptionListener implements ApplicationListener<ExceptionEvent> {

@Override
public void onApplicationEvent(ExceptionEvent event) {
String message = event.getMessage();
// TODO 将异常信息投递到邮箱等,通知开发人员系统异常,尽快处理。
}
}

请求日志及幂等锁

想要使用请求日志的功能需要实现 DistributedLocker 接口并注册到spring容器中以激活日志切面。然后再需要拦截的方法上加上 @InterfaceAction 当我们请求这个方法时就会以info级别将请求参数输入到日志中,目前日志格式是写死的,格式形如:

1
routeros复制代码INFO  com.muggle.poseidon.aop.RequestAspect - 》》》》》》 请求日志   用户名:用户未登录 url=/user/regester.jsonmethod=POSTip=127.0.0.1host=127.0.0.1port=57180classMethod=com.muggle.poseidon.oa.controller.UserController.regesterparamters [  (OaUserVO(gender=1, username=muggle, password=xxxxxx, email=null, imgUrl=null))  ]

如果想做幂等拦截 则需要在注解上添加参数 @InterfaceAction(Idempotent = true,message = "请求太频繁,请稍后再试") ,Idempotent 是否开启幂等拦截,
message 是 被拦截后的提示信息,expertime 是幂等锁时长 。开启拦截后会 拼接一个 key String key = "lock:idempotent:" + request.getRequestURI() + ":" + username + ":" + RequestUtils.getIP(request); 然后调用 DistributedLocker.trylock(String key, Long express) 方法进行上锁,express 参数就是注解上配置 expertime,上锁方式需要使用者自己实现,你可以用redis,zookeeper,或者缓存来上锁。

部分使用者可能希望能把请求相关的信息存储到数据库,我也提供了扩展接口:RequestLogProcessor 只要实现该接口并注册到 spring 你就能在recordBefore 方法中拿到 请求相关信息 ,在recordAfterReturning 方法中拿到返回值,注意如果方法抛出异常,是不会拿到返回值的,需要自己去修改源码添加异常切面方法,异常切面方法的注解是 @AfterThrowing。

日志配置

日志配置主要是两个地方,一个是 banner.txt另外一个是 poseidon-logback.xml 如果小伙伴不喜欢这个banner想去掉,只需要在自己的项目中添加一个 banner.txt 进行覆盖就行了。

poseidon-logback.xml 是对日志格式等的配置,通过 logging.config=classpath:poseidon-logback.xml 来启用该配置,同时需要指定日志文件输出路径 log.dir=/temp/xxx,启用该配置后你就可以在控制台上看到五彩斑斓的黑,如果小伙伴不喜欢这个配色,可以根据配置文件中的注释去修改。

查询配置

做出查询配置这个功能是为了减少平时开发写查询接口的开发成本,这个功能本身是结合 mybatis 的 pagehelper 插件使用的,如果你没有用这个插件,那就享受不到这个福利了。

由于各个公司或者的查询要求不尽相同,所以这里我也只做了一个顶层抽象。具体查询策略还是需要开发者去实现,将扩展性预留了出来。下面介绍这个功能的思路。

查询bean的 顶层抽象为 com.muggle.poseidon.base.BaseQuery,这里面定义查询的一些通用属性。然后在 com.muggle.poseidon.aop.QueryAspect 中拦截查询方法,拦截规则是类名必须要以 Controller 结尾,入参必须是 BaseQuery 的子类。

这个切面是没有注册的,需要手动注册一下:

1
2
3
4
java复制代码    @Bean
QueryAspect getQueryAspect(){
return new QueryAspect();
}

在切面的 doBefore(JoinPoint joinPoint) 中 对查询参数进行转化,在doAfterReturning(JoinPoint joinPoint, Object result)
对查询的返回值进行再次处理。实际使用中小伙伴就根据项目需求进行扩展吧。

一些基础类的封装

com.muggle.poseidon.util 收集了一些工具类,小伙伴们请按需增删。com.muggle.poseidon.base包下的 com.muggle.poseidon.base.ResultBean是对 controller 层的返回值的bean的封装。exception 包下是自定义异常的顶层抽象类。

结语

目前项目只发布了 BETA 版,后续不会再在这个版本上加新功能,当版本稳定后,我会在这个版本基础上发布一个 REALSE 版本。如果小伙伴发现bug,或者有改进意见,或者对这个项目有新的需求请务必联系我,撸码不易,点个star支持一下吧,球球了。

点击关注我的博客

本文转载自: 掘金

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

SpringBoot整合SpringTask实现定时任务

发表于 2020-06-11

前因

温馨提醒:阅读本文需要6分钟

半藏商城中会有一些用户提交了订单但是一直没有支付的情况,之前我是通过quartz定时任务每天的5点扫描未支付订单然后读取用户的邮箱地址发送邮件提醒用户尽快支付。这次我是采用Spring中自带的SpringTask来进行定时任务。

Cron表达式

Cron表达式是一个字符串,包括6~7个时间元素,在SpringTask中可以用于指定任务的执行时间。

Cron的语法格式

Seconds Minutes Hours DayofMonth Month DayofWeek

Cron格式中每个时间元素的说明

1
2
3
4
5
6
7
复制代码时间元素  可出现的字符   有效数值范围
Seconds , - * / 0-59
Minutes , - * / 0-59
Hours , - * / 0-23
DayofMonth, - * / ? L W 0-31
Month , - * / 1-12
DayofWeek , - * / ? L # 1-7或SUN-SAT

Cron格式中特殊字符说明

1
2
3
4
5
6
7
8
9
复制代码字符	作用		举例
, 列出枚举值 在Minutes域使用5,10,表示在5分和10分各触发一次
- 表示触发范围 在Minutes域使用5-10,表示从5分到10分钟每分钟触发一次
* 匹配任意值 在Minutes域使用*, 表示每分钟都会触发一次
/ 起始时间开始触发,每隔固定时间触发一次 在Minutes域使用5/10,表示5分时触发一次,每10分钟再触发一次
? 在DayofMonth和DayofWeek中,用于匹配任意值 在DayofMonth域使用?,表示每天都触发一次
# 在DayofMonth中,确定第几个星期几 1#3表示第三个星期日
L 表示最后 在DayofWeek中使用5L,表示在最后一个星期四触发
W 表示有效工作日(周一到周五) 在DayofMonth使用5W,如果5日是星期六,则将在最近的工作日4日触发一次

整合SpringTask

由于SpringTask已经存在于Spring框架中,所以无需添加依赖。

配置SpringTaskConfig类

只需要在配置类中添加一个@EnableScheduling注解即可开启SpringTask的定时任务能力。

1
2
3
4
复制代码@Configuration
@EnableScheduling
public class SpringTaskConfig {
}

添加CallPaySpringTask类来执行定时任务

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
复制代码package ltd.hanzo.mall.task;

import lombok.extern.slf4j.Slf4j;
import ltd.hanzo.mall.service.TaskService;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;

/**
* @Author 皓宇QAQ
* @email 2469653218@qq.com
* @Date 2020/5/19 23:33
* @link https://github.com/Tianhaoy/hanzomall
* @Description: 每天定时扫描订单 未支付状态的单子发送邮件提醒支付
*/
@Slf4j
@Component
public class CallPaySpringTask {

@Resource
private TaskService taskService;

/**
* cron表达式:Seconds Minutes Hours DayOfMonth Month DayOfWeek [Year]
*/
@Scheduled(cron = "0 0 5 * * ?")
private void callPay() {
log.info("通过SpringTask开始批量发送待支付订单邮件提醒");
//这里调用自己的定时任务接口--我这里调用的是发送待支付订单邮件的接口
taskService.callPayOrders();
}
}

还有一些查找订单信息的service层 mapper层代码就不贴出了,根据自己的业务进行开发就可以。主要是分享流程,代码实现并不难。

小结

到此为止,整个通过SpringTask定时任务发送邮件信息的流程就介绍完毕了,知识只有分享出来才有价值。如果有问题的话,可以在关于我的页面,通过我的邮箱联系我进行探讨。

本文转载自: 掘金

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

33 如何手动实现一个协程池?

发表于 2020-06-11

Hi,大家好,我是明哥。

在自己学习 Golang 的这段时间里,我写了详细的学习笔记放在我的个人微信公众号 《Go编程时光》,对于 Go 语言,我也算是个初学者,因此写的东西应该会比较适合刚接触的同学,如果你也是刚学习 Go 语言,不防关注一下,一起学习,一起成长。

我的在线博客:golang.iswbm.com
我的 Github:github.com/iswbm/GolangCodingTime


在 Golang 中要创建一个协程是一件无比简单的事情,你只要定义一个函数,并使用 go 关键字去执行它就行了。

如果你接触过其他语言,会发现你在使用使用线程时,为了减少线程频繁创建销毁还来的开销,通常我们会使用线程池来复用线程。

池化技术就是利用复用来提升性能的,那在 Golang 中需要协程池吗?

在 Golang 中,goroutine 是一个轻量级的线程,他的创建、调度都是在用户态进行,并不需要进入内核,这意味着创建销毁协程带来的开销是非常小的。

因此,我认为大多数情况下,开发人员是不太需要使用协程池的。

但也不排除有某些场景下是需要这样做,因为我还没有遇到就不说了。

抛开是否必要这个问题,单纯从技术的角度来看,我们可以怎样实现一个通用的协程池呢?

下面就来一起学习一下我的写法

首先定义一个协程池(Pool)结构体,包含两个属性,都是 chan 类型的。

一个是 work,用于接收 task 任务

一个是 sem,用于设置协程池大小,即可同时执行的协程数量

1
2
3
4
go复制代码type Pool struct {
work chan func() // 任务
sem chan struct{} // 数量
}

然后定义一个 New 函数,用于创建一个协程池对象,有一个细节需要注意

work 是一个无缓冲通道

而 sem 是一个缓冲通道,size 大小即为协程池大小

1
2
3
4
5
6
go复制代码func New(size int) *Pool {
return &Pool{
work: make(chan func()),
sem: make(chan struct{}, size),
}
}

最后给协程池对象绑定两个函数

1、NewTask:往协程池中添加任务

当第一次调用 NewTask 添加任务的时候,由于 work 是无缓冲通道,所以会一定会走第二个 case 的分支:使用 go worker 开启一个协程。

1
2
3
4
5
6
7
go复制代码func (p *Pool) NewTask(task func()) { 
select {
case p.work <- task:
case p.sem <- struct{}{}:
go p.worker(task)
}
}

2、worker:用于执行任务

为了能够实现协程的复用,这个使用了 for 无限循环,使这个协程在执行完任务后,也不退出,而是一直在接收新的任务。

1
2
3
4
5
6
7
go复制代码func (p *Pool) worker(task func()) { 
defer func() { <-p.sem }()
for {
task()
task = <-p.work
}
}

这两个函数是协程池实现的关键函数,里面的逻辑很值得推敲:

1、如果设定的协程池数大于 2,此时第二次传入往 NewTask 传入task,select case 的时候,如果第一个协程还在运行中,就一定会走第二个case,重新创建一个协程执行task

2、如果传入的任务数大于设定的协程池数,并且此时所有的任务都还在运行中,那此时再调用 NewTask 传入 task ,这两个 case 都不会命中,会一直阻塞直到有任务执行完成,worker 函数里的 work 通道才能接收到新的任务,继续执行。

以上便是协程池的实现过程。

使用它也很简单,看下面的代码你就明白了

1
2
3
4
5
6
go复制代码func main()  {
pool := New(128)
pool.NewTask(func(){
fmt.Println("run task")
})
}

为了让你看到效果,我设置协程池数为 2,开启四个任务,都是 sleep 2 秒后,打印当前时间。

1
2
3
4
5
6
7
8
9
10
11
12
13
go复制代码func main()  {
pool := New(2)

for i := 1; i <5; i++{
pool.NewTask(func(){
time.Sleep(2 * time.Second)
fmt.Println(time.Now())
})
}

// 保证所有的协程都执行完毕
time.Sleep(5 * time.Second)
}

执行结果如下,可以看到总共 4 个任务,由于协程池大小为 2,所以 4 个任务分两批执行(从打印的时间可以看出)

1
2
3
4
复制代码2020-05-24 23:18:02.014487 +0800 CST m=+2.005207182
2020-05-24 23:18:02.014524 +0800 CST m=+2.005243650
2020-05-24 23:18:04.019755 +0800 CST m=+4.010435443
2020-05-24 23:18:04.019819 +0800 CST m=+4.010499440

本文转载自: 掘金

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

为数不多的人知道的 Kotlin 技巧以及 原理解析

发表于 2020-06-10

Google 引入 Kotlin 的目的就是为了让 Android 开发更加方便,自从官宣 Kotlin 成为了 Android 开发的首选语言之后,已经有越来越多的人开始使用 Kotlin。

结合着 Kotlin 的高级函数的特性可以让代码可读性更强,更加简洁,但是呢简洁的背后是有代价的,使用不当对性能可能会有损耗,这块往往很容易被我们忽略,这就需要我们去研究 kotlin 语法糖背后的魔法,当我们在开发的时候,选择合适的语法糖,尽量避免这些错误,关于 Kotlin 性能损失那些事,可以看一下我另外两篇文章。

  • [译][2.4K Start] 放弃 Dagger 拥抱 Koin
  • [译][5k+] Kotlin 的性能优化那些事

这两篇文章都分析了 Kotlin 使用不当对性能的影响,不仅如此 Kotlin 当中还有很多让人傻傻分不清楚的语法糖例如 run, with, let, also, apply 等等,这篇文章将介绍一种简单的方法来区分它们以及如何选择使用。

通过这篇文章你将学习到以下内容,文中会给出相应的答案

  • 如何使用 plus 操作符对集合进行操作?
  • 当获取 Map 值为空时,如何设置默认值?
  • require 或者 check 函数做什么用的?
  • 如何区分 run, with, let, also and apply 以及如何使用?
  • 如何巧妙的使用 in 和 when 关键字?
  • Kotlin 的单例有几种形式?
  • 为什么 by lazy 声明的变量只能用 val?

plus 操作符

在 Java 中算术运算符只能用于基本数据类型,+ 运算符可以与 String 值一起使用,但是不能在集合中使用,在 Kotlin 中可以应用在任何类型,我们来看一个例子,利用 plus (+) 和 minus (-) 对 Map 集合做运算,如下所示。

1
2
3
4
5
6
7
8
9
10
11
12
kotlin复制代码fun main() {
val numbersMap = mapOf("one" to 1, "two" to 2, "three" to 3)

// plus (+)
println(numbersMap + Pair("four", 4)) // {one=1, two=2, three=3, four=4}
println(numbersMap + Pair("one", 10)) // {one=10, two=2, three=3}
println(numbersMap + Pair("five", 5) + Pair("one", 11)) // {one=11, two=2, three=3, five=5}

// minus (-)
println(numbersMap - "one") // {two=2, three=3}
println(numbersMap - listOf("two", "four")) // {one=1, three=3}
}

其实这里用到了运算符重载,Kotlin 在 Maps.kt 文件里面,定义了一系列用关键字 operator 声明的 Map 的扩展函数。

用 operator 关键字声明 plus 函数,可以直接使用 + 号来做运算,使用 operator 修饰符声明 minus 函数,可以直接使用 - 号来做运算,其实我们也可以在自定义类里面实现 plus (+) 和 minus (-) 做运算。

1
2
3
4
5
6
7
8
9
10
11
12
kotlin复制代码
data class Salary(var base: Int = 100){
override fun toString(): String = base.toString()
}

operator fun Salary.plus(other: Salary): Salary = Salary(base + other.base)
operator fun Salary.minus(other: Salary): Salary = Salary(base - other.base)

val s1 = Salary(10)
val s2 = Salary(20)
println(s1 + s2) // 30
println(s1 - s2) // -10

Map 集合的默认值

在 Map 集合中,可以使用 withDefault 设置一个默认值,当键不在 Map 集合中,通过 getValue 返回默认值。

1
2
3
4
5
6
7
8
9
go复制代码val map = mapOf(
"java" to 1,
"kotlin" to 2,
"python" to 3
).withDefault { "?" }

println(map.getValue("java")) // 1
println(map.getValue("kotlin")) // 2
println(map.getValue("c++")) // ?

源码实现也非常简单,当返回值为 null 时,返回设置的默认值。

1
2
3
4
5
6
7
8
9
kotlin复制代码internal inline fun <K, V> Map<K, V>.getOrElseNullable(key: K, defaultValue: () -> V): V {
val value = get(key)
if (value == null && !containsKey(key)) {
return defaultValue()
} else {
@Suppress("UNCHECKED_CAST")
return value as V
}
}

但是这种写法和 plus 操作符在一起用,有一个 bug ,看一下下面这个例子。

1
2
go复制代码val newMap = map + mapOf("python" to 3)
println(newMap.getValue("c++")) // 调用 getValue 时抛出异常,异常信息:Key c++ is missing in the map.

这段代码的意思就是,通过 plus(+) 操作符合并两个 map,返回一个新的 map, 但是忽略了默认值,所以看到上面的错误信息,我们在开发的时候需要注意这点。

使用 require 或者 check 函数作为条件检查

1
2
3
4
5
6
7
8
9
10
11
12
13
14
javascript复制代码// 传统的做法
val age = -1;
if (age <= 0) {
throw IllegalArgumentException("age must not be negative")
}

// 使用 require 去检查
require(age > 0) { "age must be negative" }

// 使用 checkNotNull 检查
val name: String? = null
checkNotNull(name){
"name must not be null"
}

那么我们如何在项目中使用呢,具体的用法可以查看我 GitHub 上的项目 DataBindingDialog.kt 当中的用法。

如何区分和使用 run, with, let, also, apply

感谢大神 Elye 的这篇文章提供的思路 Mastering Kotlin standard functions。

run, with, let, also, apply 都是作用域函数,这些作用域函数如何使用,以及如何区分呢,我们将从以下三个方面来区分它们。

  • 是否是扩展函数。
  • 作用域函数的参数(this、it)。
  • 作用域函数的返回值(调用本身、其他类型即最后一行)。

是否是扩展函数

首先我们来看一下 with 和 T.run,这两个函数非常的相似,他们的区别在于 with 是个普通函数,T.run 是个扩展函数,来看一下下面的例子。

1
2
3
4
5
6
7
scala复制代码val name: String? = null
with(name){
val subName = name!!.substring(1,2)
}

// 使用之前可以检查它的可空性
name?.run { val subName = name.substring(1,2) }?:throw IllegalArgumentException("name must not be null")

在这个例子当中,name?.run 会更好一些,因为在使用之前可以检查它的可空性。

作用域函数的参数(this、it)

我们在来看一下 T.run 和 T.let,它们都是扩展函数,但是他们的参数不一样 T.run 的参数是 this, T.let 的参数是 it。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
dart复制代码val name: String? = "hi-dhl.com"

// 参数是 this,可以省略不写
name?.run {
println("The length is ${this.length} this 是可以省略的 ${length}")
}

// 参数 it
name?.let {
println("The length is ${it.length}")
}

// 自定义参数名字
name?.let { str ->
println("The length is ${str.length}")
}

在上面的例子中看似 T.run 会更好,因为 this 可以省略,调用更加的简洁,但是 T.let 允许我们自定义参数名字,使可读性更强,如果倾向可读性可以选择 T.let。

作用域函数的返回值(调用本身、其他类型)

接下里我们来看一下 T.let 和 T.also 它们接受的参数都是 it, 但是它们的返回值是不同的 T.let 返回最后一行,T.also 返回调用本身。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
kotlin复制代码
var name = "hi-dhl"

// 返回调用本身
name = name.also {
val result = 1 * 1
"juejin"
}
println("name = ${name}") // name = hi-dhl

// 返回的最后一行
name = name.let {
val result = 1 * 1
"hi-dhl.com"
}
println("name = ${name}") // name = hi-dhl.com

从上面的例子来看 T.also 似乎没有什么意义,细想一下其实是非常有意义的,在使用之前可以进行自我操作,结合其他的函数,功能会更强大。

1
kotlin复制代码fun makeDir(path: String) = path.let{ File(it) }.also{ it.mkdirs() }

当然 T.also 还可以做其他事情,比如利用 T.also 在使用之前可以进行自我操作特点,可以实现一行代码交换两个变量,在后面会有详细介绍

T.apply 函数

通过上面三个方面,大致了解函数的行为,接下来看一下 T.apply 函数,T.apply 函数是一个扩展函数,返回值是它本身,并且接受的参数是 this。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
kotlin复制代码// 普通方法
fun createInstance(args: Bundle) : MyFragment {
val fragment = MyFragment()
fragment.arguments = args
return fragment
}
// 改进方法
fun createInstance(args: Bundle)
= MyFragment().apply { arguments = args }


// 普通方法
fun createIntent(intentData: String, intentAction: String): Intent {
val intent = Intent()
intent.action = intentAction
intent.data=Uri.parse(intentData)
return intent
}
// 改进方法,链式调用
fun createIntent(intentData: String, intentAction: String) =
Intent().apply { action = intentAction }
.apply { data = Uri.parse(intentData) }

汇总

以表格的形式汇总,更方便去理解

函数 是否是扩展函数 函数参数(this、it) 返回值(调用本身、最后一行)
with 不是 this 最后一行
T.run 是 this 最后一行
T.let 是 it 最后一行
T.also 是 it 调用本身
T.apply 是 this 调用本身

使用 T.also 函数交换两个变量

接下来演示的是使用 T.also 函数,实现一行代码交换两个变量?我们先来回顾一下 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
css复制代码int a = 1;
int b = 2;

// Java - 中间变量
int temp = a;
a = b;
b = temp;
System.out.println("a = "+a +" b = "+b); // a = 2 b = 1

// Java - 加减运算
a = a + b;
b = a - b;
a = a - b;
System.out.println("a = " + a + " b = " + b); // a = 2 b = 1

// Java - 位运算
a = a ^ b;
b = a ^ b;
a = a ^ b;
System.out.println("a = " + a + " b = " + b); // a = 2 b = 1

// Kotlin
a = b.also { b = a }
println("a = ${a} b = ${b}") // a = 2 b = 1

来一起分析 T.also 是如何做到的,其实这里用到了 T.also 函数的两个特点。

  • 调用 T.also 函数返回的是调用者本身。
  • 在使用之前可以进行自我操作。

也就是说 b.also { b = a } 会先将 a 的值 (1) 赋值给 b,此时 b 的值为 1,然后将 b 原始的值(2)赋值给 a,此时 a 的值为 2,实现交换两个变量的目的。

in 和 when 关键字

使用 in 和 when 关键字结合正则表达式,验证用户的输入,这是一个很酷的技巧。

1
2
3
4
5
6
7
8
9
10
kotlin复制代码// 使用扩展函数重写 contains 操作符
operator fun Regex.contains(text: CharSequence) : Boolean {
return this.containsMatchIn(text)
}

// 结合着 in 和 when 一起使用
when (input) {
in Regex("[0–9]") -> println("contains a number")
in Regex("[a-zA-Z]") -> println("contains a letter")
}

in 关键字其实是 contains 操作符的简写,它不是一个接口,也不是一个类型,仅仅是一个操作符,也就是说任意一个类只要重写了 contains 操作符,都可以使用 in 关键字,如果我们想要在自定义类型中检查一个值是否在列表中,只需要重写 contains() 方法即可,Collections 集合也重写了 contains 操作符。

1
2
3
4
5
6
7
kotlin复制代码val input = "kotlin"

when (input) {
in listOf("java", "kotlin") -> println("found ${input}")
in setOf("python", "c++") -> println("found ${input}")
else -> println(" not found ${input}")
}

Kotlin 的单例三种写法

我汇总了一下目前 Kotlin 单例总共有三种写法:

  • 使用 Object 实现单例。
  • 使用 by lazy 实现单例。
  • 可接受参数的单例(来自大神 Christophe Beyls)。

使用 Object 实现单例

代码:

1
csharp复制代码object WorkSingleton

Kotlin 当中 Object 关键字就是一个单例,比 Java 的一坨代码看起来舒服了很多,来看一下编译后的 Java 文件。

1
2
3
4
5
6
7
8
java复制代码public final class WorkSingleton {
public static final WorkSingleton INSTANCE;

static {
WorkSingleton var0 = new WorkSingleton();
INSTANCE = var0;
}
}

通过 static 代码块实现的单例,优点:饿汉式且是线程安全的,缺点:类加载时就初始化,浪费内存。

使用 by lazy 实现单例

利用伴生对象 和 by lazy 也可以实现单例,代码如下所示。

1
2
3
4
5
6
7
8
9
10
11
kotlin复制代码class WorkSingleton private constructor() {

companion object {

// 方式一
val INSTANCE1 by lazy(mode = LazyThreadSafetyMode.SYNCHRONIZED) { WorkSingleton() }

// 方式二 默认就是 LazyThreadSafetyMode.SYNCHRONIZED,可以省略不写,如下所示
val INSTANCE2 by lazy { WorkSingleton() }
}
}

lazy 的延迟模式有三种:

  • 上面代码所示 mode = LazyThreadSafetyMode.SYNCHRONIZED,lazy 默认的模式,可以省掉,这个模式的意思是:如果有多个线程访问,只有一条线程可以去初始化 lazy 对象。
  • 当 mode = LazyThreadSafetyMode.PUBLICATION 表达的意思是:对于还没有被初始化的 lazy 对象,可以被不同的线程调用,如果 lazy 对象初始化完成,其他的线程使用的是初始化完成的值。
  • mode = LazyThreadSafetyMode.NONE 表达的意思是:只能在单线程下使用,不能在多线程下使用,不会有锁的限制,也就是说它不会有任何线程安全的保证以及相关的开销。

通过上面三种模式,这就可以理解为什么 by lazy 声明的变量只能用 val,因为初始化完成之后它的值是不会变的。

可接受参数的单例

但是有的时候,希望在单例实例化的时候传递参数,例如:

1
scss复制代码Singleton.getInstance(context).doSome()

上面这两种形式都不能满足,来看看大神 Christophe Beyls 在这篇文章给出的方法 Kotlin singletons with argument 代码如下。

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复制代码class WorkSingleton private constructor(context: Context) {
init {
// Init using context argument
}

companion object : SingletonHolder<WorkSingleton, Context>(::WorkSingleton)
}


open class SingletonHolder<out T : Any, in A>(creator: (A) -> T) {
private var creator: ((A) -> T)? = creator
@Volatile
private var instance: T? = null

fun getInstance(arg: A): T {
val i = instance
if (i != null) {
return i
}

return synchronized(this) {
val i2 = instance
if (i2 != null) {
i2
} else {
val created = creator!!(arg)
instance = created
creator = null
created
}
}
}
}

有没有感觉这和 Java 中双重校验锁的机制很像,在 SingletonHolder 类中如果已经初始化了直接返回,如果没有初始化进入 synchronized 代码块创建对象,利用了 Kotlin 伴生对象提供的非常强大功能,它能够像其他任何对象一样从基类继承,从而实现了与静态继承相当的功能。 所以我们将 SingletonHolder 作为单例类伴随对象的基类,在单例类上重用并公开 getInstance()函数。

参数传递给 SingletonHolder 构造函数的 creator,creator 是一个 lambda 表达式,将 WorkSingleton 传递给 SingletonHolder 类构造函数。

并且不限制传入参数的类型,凡是需要传递参数的单例模式,只需将单例类的伴随对象继承于 SingletonHolder,然后传入当前的单例类和参数类型即可,例如:

1
2
3
4
5
typescript复制代码class FileSingleton private constructor(path: String) {

companion object : SingletonHolder<FileSingleton, String>(::FileSingleton)

}

总结

到这里就结束了,Kotlin 的强大不止于此,后面还会分享更多的技巧,在 Kotlin 的道路上还有很多实用的技巧等着我们一起来探索。

例如利用 Kotlin 的 inline、reified、DSL 等等语法, 结合着 DataBinding、LiveData 等等可以设计出更加简洁并利于维护的代码,更多技巧可以查看我 GitHub 上的项目 JDataBinding。

参考链接

  • Mastering Kotlin standard functions: run, with, let, also and apply
  • Kotlin: fun with “in”

结语

关注公众号:ByteCode,查看一系列 Android 系统源码、逆向分析、算法、译文、Kotlin、Jetpack 源码相关的文章,如果这篇文章对你有帮助,请帮我点个 star,感谢!!!,欢迎一起来学习,在技术的道路上一起前进。


最后推荐我一直在更新维护的项目和网站:

  • 计划建立一个最全、最新的 AndroidX Jetpack 相关组件的实战项目 以及 相关组件原理分析文章,正在逐渐增加 Jetpack 新成员,仓库持续更新,欢迎前去查看:AndroidX-Jetpack-Practice
  • LeetCode / 剑指 offer / 国内外大厂面试题 / 多线程 题解,语言 Java 和 kotlin,包含多种解法、解题思路、时间复杂度、空间复杂度分析


+ 剑指 offer 及国内外大厂面试题解:在线阅读
+ LeetCode 系列题解:在线阅读

  • 最新 Android 10 源码分析系列文章,了解系统源码,不仅有助于分析问题,在面试过程中,对我们也是非常有帮助的,仓库持续更新,欢迎前去查看 Android10-Source-Analysis
  • 整理和翻译一系列精选国外的技术文章,每篇文章都会有译者思考部分,对原文的更加深入的解读,仓库持续更新,欢迎前去查看 Technical-Article-Translation
  • 「为互联网人而设计,国内国外名站导航」涵括新闻、体育、生活、娱乐、设计、产品、运营、前端开发、Android 开发等等网址,欢迎前去查看 为互联网人而设计导航网站

历史文章

  • 为数不多的人知道的 Kotlin 技巧以及 原理解析(一)
  • 为数不多的人知道的 Kotlin 技巧以及 原理解析(二)
  • 为数不多的人知道的 AndroidStudio 快捷键(一)
  • 为数不多的人知道的 AndroidStudio 快捷键(二)
  • 再见吧 buildSrc, 拥抱 Composing builds 提升 Android 编译速度
  • Jetpack 最新成员 AndroidX App Startup 实践以及原理分析
  • Jetpack 成员 Paging3 实践以及源码分析(一)
  • Jetpack 新成员 Paging3 网络实践及原理分析(二)
  • Jetpack 新成员 Hilt 实践(一)启程过坑记
  • Jetpack 新成员 Hilt 实践之 App Startup(二)进阶篇
  • Jetpack 新成员 Hilt 与 Dagger 大不同(三)落地篇
  • 全方面分析 Hilt 和 Koin 性能
  • 神奇宝贝(PokemonGo) 眼前一亮的 Jetpack + MVVM 极简实战
  • Google 推荐在项目中使用 sealed 和 RemoteMediator
  • Kotlin Sealed 是什么?为什么 Google 都用
  • Kotlin StateFlow 搜索功能的实践 DB + NetWork

本文转载自: 掘金

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

1…804805806…956

开发者博客

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