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

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


  • 首页

  • 归档

  • 搜索

Python 的十万个为什么?

发表于 2020-05-17

随着 Python 在近些年的火爆,网上出现了很多这个方向的公众号和博客,文章也层出不穷。

受到此风气的影响,我也把自己“培养”成了一名技术博主,写作近两年来,陆陆续续写过不少的系列文章(shimo.im/docs/6ggy3r…)。

近期看到短视频火热起来,看到某些短视频平台的发展数据,看到我国 5G 建设的落地加速。这新的风气慢慢就感染了我,我也想在视频方向上做点尝试。

目前主要的想法是录制一个“Python 为什么”系列节目,主要关注 Python 的语法、设计和发展等话题,以一个个“为什么”式的问题为切入点,试着展现 Python 的迷人魅力。

标题中的“十万个”是种夸张说法,也许最终只能做到几百个。我对于自己的问题意识和持久耐力是比较自信的,所以不管数量多少,我会保证它的质量,并一直发展下去。

我会认真准备和制作短视频,同时也会把文稿材料整理发布出来。视频时长将控制在 3 分钟内,主要发布的平台暂定在 B 站(space.bilibili.com/97566624)。

前面已经发布了两期:为什么使用 len 函数、为什么使用缩进 ,关于后续的内容,我已经有了很多的选题想法。

其中一个方向是 Python 官网列出的 27 个“为什么”问题:

我会用自己的方式,慢慢把这些都阐述出来。同时,我还想到了下面这些选题:

  • 为什么不用分号“;”作结尾?
  • 为什么不支持“复制”字符串?
  • 为什么没有“else if”写法?
  • 为什么不支持三目运算符?
  • 为什么不支持“i ++”自增语法?
  • 为什么没有元组生成式?
  • 为什么会存在 GIL ?
  • 为什么 range() 不是迭代器?
  • 为什么不推荐使用匿名函数?
  • 为什么 Python 3 会不兼容 Python 2?
  • 为什么把 print 语句变为 print() 内置函数?
  • ……

之所以写这篇文章,我一方面想告知大家这个计划,但更主要的是希望能得到一些反馈:

  • 关于前面列出的那些话题,大家最关心哪些呢?
  • 除了所列举的问题,大家还比较关心哪些话题呢?

我把这两个问题提交成了一份问卷:www.wjx.cn/m/77821760.… ,恳请读到这里的爱学习的同学们,花 1 分钟填一下。谢谢大家的支持,无限感激!

本文转载自: 掘金

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

【译】【5k+】 Kotlin 的性能优化那些事

发表于 2020-05-17

前言

  • 原标题: Item: Consider aggregating elements to a map
  • 原文地址: blog.kotlin-academy.com/item……
  • 原文作者:Marcin Moskala
  • 介绍:作者 Marcin Moskala 是大神级别的人物,在 Medium 上至少有 5K+ 的关注者,在 Twitter 上至少有 4K+ 的关注者,是 「Effective Kotlin」一书的作者之一。「Effective Kotlin」总结了 Kotlin 社区的最佳实践和经验,很多来自 Google 工程师的案例,揭露了很多不为人知的 Kotlin 背后的魔法。

这篇文章应该可以说是 [译][2.4K Start] 放弃 Dagger 拥抱 Koin 文章的续集,在 “放弃 Dagger 拥抱 Koin” 文章中介绍了过渡使用 Inline 修饰符所带来的后果,以及 Koin 团队在为修复 1x 版本所做的性能优化,这边文章将继续学习如何提升 Kotlin 的查询速度。

通过这篇文章你将学习到以下内容,将在译者思考部分会给出相应的答案

  • 如何提升 Kotlin 的查询速度?
  • 性能和代码可读性该做如何选择?
  • Kotlin 内存泄露那些事, 消除过期的对象引用?
  • 如何提高 Kotlin 代码的可读性?
  • Kotlin 算法:一行代码实现杨辉三角?

这篇文章涉及很多重要的知识点,带着自己理解,请耐心读下去,应该可以从中学到很多技巧

译文

我们需要多次访问大量的数据情况,这其实并不少见,例如:

  • cache:从服务上下载的数据,然后保存在本地内存中以更快地访问它们
  • repository:从一些文件中加载数据
  • in-memory repository:用于不同类型的内存测试

这些数据可能表示一些用户、id、配置等等,它们通常以 list 形式返给我们,它们可能以相同的方式存储在内存中:

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
kotlin复制代码class NetworkUserRepo(val userService: UserService): UserRepo {
private var users: List<User>? = null
override fun getUser(id: UserId): User? {
if(users == null) {
users = userService.getUsers()
}
return users?.firstOrNull { it.id == id }
}
}

class ConfigurationsRepository(
val configurations: List<Configuration>
) {
fun getByName(name: String) = configurations
.firstOrNull { it.name == name }
}
class InMemoryUserRepo: UserRepo {
private val users: MutableList<User> = mutableListOf()
override fun getUser(id: UserId): User?
= users.firstOrNull { it.id == id }

fun addUser(user: User) {
user.add(user)
}
}

这可能是存储这些元素的最好方式,注意我们是如何加载数据如何使用的,我们通过某个标识符或者名字访问这些元素(它们与我们设计数据库时唯一值有关),当 n 等于 list 的大小时,在 list 中查找元素的复杂度为 O(n),更准确的说,平均需要 n / 2 次比较才能找到一个元素,如果是一个比较的大的 list,查找效率极其低效,解决这个问题的一个好办法是使用 Map 代替 list, Kotlin 默认使用的是 hash map, 更具体的说是 LinkedHashMap,当我们使用 hash map 查找元素的性能要好得多, 实际上 JVM 使用的 hash map 的大小根据映射本身的大小进行了调整, 如果实现 hashCode 方式正确,查找一个元素只需要进行一次比较。

这是 InMemoryRepo 中使用 map 代替 list

1
2
3
4
5
6
7
8
kotlin复制代码class InMemoryUserRepo: UserRepo {
private val users: MutableMap<UserId, User> = mutableMapOf()
override fun getUser(id: UserId): User? = users[id]

fun addUser(user: User) {
user.put(user.id, user)
}
}

大多是其他操作,比如修改或者迭代这些数据(可能使用集合方法 filter, map, flatMap, sorted, sum 等等)对于 list 和 map 性能差不多的。

那么我们如何从 list 转换到 map,或者从 map 转换到 list,使用 associate 方法来完成 list 转换到 map,最常见的方法是 associateBy,它构建一个映射,其中的值是列表中的元素,键是通过一个 lambda 表达式提供。

1
2
3
4
5
6
7
kotlin复制代码data class User(val id: Int, val name: String)
val users = listOf(User(1, "Michal"), User(2, "Marek"))
val byId = users.associateBy { it.id }
byId == mapOf(1 to User(1, "Michal"), 2 to User(2, "Marek"))
val byName = users.associateBy { it.name }
byName == mapOf("Michal" to User(1, "Michal"),
"Marek" to User(2, "Marek"))

注意,映射中的键必须是唯一的,否则相同键值的元素会被删掉,这就是为什么我们应该根据唯一标识符进行关联(对于键值不是唯一的,应该使用 groupBy 方法)

1
2
3
ini复制代码val users = listOf(User(1, "Michal"), User(2, "Michal"))
val byName = users.associateBy { it.name }
byName == mapOf("Michal" to User(2, "Michal"))

从 map 转换到 list 使用 values 方法

1
2
3
ini复制代码val users = listOf(User(1, "Michal"), User(2, "Michal"))
val byId = users.associateBy { it.id }
users == byId.values

如何在 repositories 中用 Map 提高元素访问的性能

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
kotlin复制代码class NetworkUserRepo(val userService: UserService): UserRepo {
private var users: Map<UserId, User>? = null
override fun getUser(id: UserId): User? {
if(users == null) {
users = userService.getUsers().associateBy { it.id }
}
return users?.get(id)
}
}

class ConfigurationsRepository(
configurations: List<Configuration>
) {
val configurations: Map<String, Configuration> =
configurations.associateBy { it.name }

fun getByName(name: String) = configurations[name]
}

这个技巧是非常重要的,但是并不适合所有的 cases,当我们需要访问比较大的 list 的时候是非常有用的,这在后台访问是非常重要的,这些 list 可能在后台每秒被访问很多次,但是在前台并不重要(这里说的是 Android 或者 iOS)用户最多只会访问几次 repository,需要注意的是从 list 转换到 map 是需要时间的,如果过渡使用,可能会对性能有不好的影响。

译者思考

作者总共从三个方面 Network、Configurations、InMemory 告诉我们应该如何从 list 转 map, 或者从 map 转 list, 以及应该在后台需要多次访问很大的数据集合中使用 map,过渡的使用只会对性能产生负面的影响。

  • list 转 map 调用用 associateBy 方法,接受一个 lambda 表达式
1
2
3
ini复制代码val users = listOf(User(1, "Michal"), User(2, "Michal"))
val byName = users.associateBy { it.name }
byName == mapOf("Michal" to User(2, "Michal"))
  • 从 map 转 list 调用 values 方法
1
2
3
ini复制代码val users = listOf(User(1, "Michal"), User(2, "Michal"))
val byId = users.associateBy { it.id }
users == byId.values

这是一个非常重要的优化的手段(使用空间换取时间),在 [译][2.4K Start] 放弃 Dagger 拥抱 Koin 文章中介绍了当我们引入 Koin 1x 的时候冷启动时间变长了,而且在有大量依赖的时候,查找的时间会有点长,用过这个版本的朋友,应该都会有这个感觉,Koin 团队的解决方案中用到了 HashMap,使用空间换取时间,查找一个 Definition 时间复杂度变成了 O(1),从提高的访问速度。

其实我们应该在头脑中,保持内存管理的意识,在每次优化、修改代码之前,不要急于写代码,先整理一下思路,在头脑中过一遍自己的方案,我们应该为项目找到一个折衷方案,不仅要考虑内存和性能,还要考虑代码的可读性。当我们做一个应用程序,在大多数情况下可读性更重要。当我们开发一个库时,通常性能和内存更重要。

性能和代码可读性该做如何选择

如果用 Java 和 Kotlin 语言刷过 LeetCode,使用相同的思路实现同一个算法,在正常的 Case 中,Kotlin 和 Java 执行时间差值很小,数据量越大的情况下 Kotlin 和 Java 差距会越来越大,Kotlin 执行时间会越来越慢,但是为什么 Kotlin 语言还会成为 Android 开发的首选语言呢?来看一下作者 Marcin Moskala 另外一篇文章 My favorite examples of functional programming in Kotlin 展示的快排算法。

在之前的文章中分享了过这个算法,现在我们来分析一下这个算法。

1
2
3
4
5
6
7
8
9
10
scss复制代码fun <T : Comparable<T>> List<T>.quickSort(): List<T> = 
if(size < 2) this
else {
val pivot = first()
val (smaller, greater) = drop(1).partition { it <= pivot}
smaller.quickSort() + pivot + greater.quickSort()
}

// 使用 [2,5,1] -> [1,2,5]
listOf(2,5,1).quickSort() // [1,2,5]

这是一个非常酷的函数式编程的例子,当看到这个算法的第一感觉,它非常的简洁,可读性很强,其次我们来看一下这个算法执行时间,其实它根本没有针对性能进行优化。

如果你需要使用高性能的算法,你可以使用 Java 标准库当中的函数,Kotlin 扩展函数 sorted() 就是用 Java 标准库中的函数,Java 标准库中的函数效率会更高的,但是实际执行时间怎么样呢?生成一个随机数数组,使用使用 quickSort() 和 sorted() 方法进行排序,比较它们的执行时间,代码如下所示:

1
2
3
4
5
6
7
8
scss复制代码val r = Random()
listOf(100_000, 1_000_000, 10_000_000)
.asSequence()
.map { (1..it).map { r.nextInt(1000000000) } }
.forEach { list: List<Int> ->
println("Java stdlib sorting of ${list.size} elements took ${measureTimeMillis { list.sorted() }}")
println("quickSort sorting of ${list.size} elements took ${measureTimeMillis { list.quickSort() }}")
}

执行结果如下所示:

1
2
3
4
5
6
yaml复制代码Java stdlib sorting of 100000 elements took 83
quickSort sorting of 100000 elements took 163
Java stdlib sorting of 1000000 elements took 558
quickSort sorting of 1000000 elements took 859
Java stdlib sorting of 10000000 elements took 6182
quickSort sorting of 10000000 elements took 12133`

正如你所见,quickSort() 比 sorted() 排序算法要慢两倍,在正常情况下,差值通常在 0.1ms 和 0.2ms 之间,基本上可以忽略不计,但是它更简洁,可读性更强。这解释了在某些情况下,我们可以考虑使用一个优化程度稍低,但可读性强且简洁的函数,你同意作者这种观点吗?

Kotlin 内存泄露那些事, 消除过期的对象引用

我看过很多文章都说 Kotlin 简洁和高效,Kotlin 确实很简洁,在 “如何提高 Kotlin 代码的可读性” 部分我会列举一些例子,但是高效的背后是有代价的,这块往往很容易被我们忽略,这就需要我们去研究 kotlin 语法糖背后的魔法,当我们在开发的时候,选择合适的语法糖,尽量避免这些错误,例如带有 lnmba 表达式高阶函数,不使用 Inline 修饰符,会被编译成匿名内部类等等,更详细的内容参考 [译][2.4K Start] 放弃 Dagger 拥抱 Koin Inline 修饰符带来的性能损失部分。

内存管理最重要的一条规则是,不使用的对象应该被释放

这篇文章 Effective Java in Kotlin, item 7: Eliminate obsolete object references 作者也列举了 Kotlin 的一些例子,例如我们需要使用 mutableLazy 属性委托,像 lazy 一样工作,我们来看一下实现代码:

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复制代码fun <T> mutableLazy(initializer: () -> T): ReadWriteProperty<Any?, T> = MutableLazy(initializer)

private class MutableLazy<T>(
val initializer: () -> T
) : ReadWriteProperty<Any?, T> {

private var value: T? = null
private var initialized = false

override fun getValue(
thisRef: Any?,
property: KProperty<*>
): T {
synchronized(this) {
if (!initialized) {
value = initializer()
initialized = true
}
return value as T
}
}

override fun setValue(
thisRef: Any?,
property: KProperty<*>,
value: T
) {
synchronized(this) {
this.value = value
initialized = true
}
}
}

如何使用:

1
2
3
4
5
6
7
8
9
10
11
scss复制代码var game: Game? by mutableLazy { readGameFromSave() }

fun setUpActions() {
startNewGameButton.setOnClickListener {
game = makeNewGame()
startGame()
}
resumeGameButton.setOnClickListener {
startGame()
}
}

思考一下 mutableLazy 实现正确吗? 它有一个地方不对,lnmba 表达式 initializer 在使用后没有被删除。这意味着只要对 MutableLazy 实例的引用存在,它就会被保持,即使它不再有用,如何改进 MutableLazy 实现的方法,优化代码如下所示:

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复制代码fun <T> mutableLazy(initializer: () -> T): ReadWriteProperty<Any?, T> = MutableLazy(initializer)

private class MutableLazy<T>(
var initializer: (() -> T)?
) : ReadWriteProperty<Any?, T> {

private var value: T? = null

override fun getValue(
thisRef: Any?,
property: KProperty<*>
): T {
synchronized(this) {
val initializer = initializer
if (initializer != null) {
value = initializer()
this.initializer = null
}
return value as T
}
}

override fun setValue(
thisRef: Any?,
property: KProperty<*>,
value: T
) {
synchronized(this) {
this.value = value
this.initializer = null
}
}
}

在使用完之后将 initializer 设置为 null,它将会被 GC 回收。特别要注意当一个高阶函数会被编译成匿名类时或者它是一个未知类(任何或泛型类型)时,这个优化显得非常重要,我们来看一下 Kotlin stdlib 库中的类 SynchronizedLazyImpl 代码如下所示:

kotlin-stdlib……/kotlin/util/LazyJVM.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
kotlin复制代码private class SynchronizedLazyImpl<out T>(
initializer: () -> T, lock: Any? = null
) : Lazy<T>, Serializable {
private var initializer: (() -> T)? = initializer
private var _value: Any? = UNINITIALIZED_VALUE
private val lock = lock ?: this

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

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

请注意,在使用完之后 initializers 设置为 null,将会被 GC 回收

如何提高 Kotlin 代码的可读性

上文提到了 Kotlin 简洁可读性很强,但是呢通过 AndroidStudio 提供了 convert our Java code to Kotlin 插件,将 Java 代码转换为 Kotlin 代码,Java-Style Kotlin 的代码明显很难看,那么如何提升 Kotlin 代码的可读性,我想分享几个很酷的例子 Improve Java to Kotlin code review,用到了 Elvis 表达式、run, with 等等函数

消除!!

1
matlab复制代码myList!!.length

change to

1
matlab复制代码myList?.length

空检查

1
2
3
scss复制代码if (callback != null) {              
callback!!.response()
}

change to

1
scss复制代码callback?.response()

使用 Elvis 表达式

1
2
3
4
5
6
7
javascript复制代码if (toolbar != null) {
if (arguments != null) {
toolbar!!.title = arguments!!.getString(TITLE)
} else {
toolbar!!.title = ""
}
}

change to

1
ini复制代码toolbar?.title = arguments?.getString(TITLE) ?: “”

使用 scope 函数

1
2
3
scss复制代码val intent = intentUtil.createIntent(activity!!.applicationContext) 
activity!!.startActivity(intent)
dismiss()

change to

1
2
3
4
5
scss复制代码activity?.run { 
val intent = intentUtil.createIntent(this)
startActivity(intent)
dismiss()
}

ps: scope 函数还有 run, with, let, also and apply,它们的区别是什么,如何正确使用它们,后面的文章会详细的介绍。

使用 takeIf if 函数

1
2
ini复制代码if (something != null && something == preference) {   
something.doThing()

change to

1
ini复制代码something?.takeIf { it == preference }?.let { something.doThing() }

Android TextUtil

1
2
ini复制代码if (TextUtils.isEmpty(someString)) {...}
val joinedString = TextUtils.join(COMMA, separateList)

change to

1
2
ini复制代码if (someString.isEmpty()) {...}
val joinedString = separateList.joinToString(separator = COMMA)

Java Util

1
ini复制代码val strList = Arrays.asList("someString")

change to

1
ini复制代码val strList = listOf("someString")

Empty and null

1
ini复制代码if (myList == null || myList.isEmpty()) {...}

change to

1
javascript复制代码if (myList.isNullOrEmpty() {...}

避免对对象进行重复操作

1
2
3
scss复制代码recyclerView.setLayoutManager(layoutManager)
recyclerView.setAdapter(adapter)
recyclerView.setItemAnimator(animator)

change to

1
2
3
4
5
scss复制代码with(recyclerView) {
setLayoutManager(layoutManager)
setAdapter(adapter)
setItemAnimator(animator)
}

避免列表循环

1
2
3
scss复制代码for (str in stringList) {
println(str)
}

change to

1
scss复制代码stringList.forEach { println(it) }

避免使用 mutable 集合

1
2
3
4
css复制代码val stringList: List<String> = mutableListOf()
for (other in otherList) {
stringList.add(dosSomething(other))
}

change to

1
ini复制代码val stringList = otherList.map { dosSomething(it) }

使用 when 代替 if

1
2
3
4
5
6
7
scss复制代码if (requestCode == REQUEST_1) {            
doThis()
} else if (requestCode == REQUEST_2) {
doThat()
} else {
doSomething()
}

change to

1
2
3
4
5
scss复制代码when (requestCode) { 
REQUEST_1 -> doThis()
REQUEST_1 -> doThat()
else -> doSomething()
}

使用 const

1
2
3
4
ini复制代码companion object {        
val EXTRA_STRING = "EXTRA_EMAIL"
val EXTRA_NUMBER = 12345
}

change to

1
2
3
4
kotlin复制代码companion object {        
const val EXTRA_STRING = "EXTRA_EMAIL"
const val EXTRA_NUMBER = 12345
}

如果有更好的例子,欢迎留言

Kotlin 算法:一行代码实现杨辉三角

我想分享一个很酷的算法,用一行代码实现杨辉三角,代码来自 Marcin Moskala 大神的 Twitter

1
2
3
4
5
6
7
scss复制代码fun pascal() = generateSequence(listOf(1)) { prev ->
listOf(1) + (1..prev.lastIndex).map { prev[it - 1] + prev[it] } + listOf(1)
}

fun main() {
pascal().take(10).forEach(::println)
}

20200517-124137

在这里有个小建议,可以关注一些你感兴趣的官方、大牛的 Twitter 账号,还有,他们不定时就会分享一些新的技术、新的文章等等。

安利一个译者自己撸的导航网站

基于 Python + Material Design 开发的 “为互联网人而设计 国内国外名站导航“ ,收集了国内外热门网址,涵括新闻、体育、生活、娱乐、设计、产品、运营、前端开发、Android开发等等导航网站 地址

参考文献

  • Item: Consider aggregating elements to a map
  • Effective Java in Kotlin, item 7: Eliminate obsolete object references
  • Improve Java to Kotlin code review

结语

致力于分享一系列 Android 系统源码、逆向分析、算法、翻译相关的文章,目前正在翻译一系列欧美精选文章,请持续关注,除了翻译还有对每篇欧美文章思考,如果对你有帮助,请帮我点个赞,感谢!!!期待与你一起成长。

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

算法

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

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

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

Android 10 源码系列

因为掘金没有文章分类和文章排序功能,所以在 GitHub 建立了一个 Android10-Source-Analysis 仓库便于查看,文章都会同步到这个仓库

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

Android 应用系列

  • 如何高效获取视频截图
  • 如何在项目中封装 Kotlin + Android Databinding
  • [译][Google工程师] 刚刚发布了 Fragment 的新特性 “Fragment 间传递数据的新方式” 以及源码分析
  • [译][2.4K Start] 放弃 Dagger 拥抱 Koin

工具系列

  • 为数不多的人知道的 AndroidStudio 快捷键(一)
  • 为数不多的人知道的 AndroidStudio 快捷键(二)
  • 关于 adb 命令你所需要知道的
  • 10分钟入门 Shell 脚本编程

逆向系列

  • 基于 Smali 文件 Android Studio 动态调试 APP
  • 解决在 Android Studio 3.2 找不到 Android Device Monitor 工具

本文转载自: 掘金

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

【漫画】读写锁ReadWriteLock还是不够快?再试试S

发表于 2020-05-17

本文来源于公众号【胖滚猪学编程】 转载请注明出处!

在互斥锁ReentrantLock不好用?试试读写锁ReadWriteLock一文中,我们对比了互斥锁ReentrantLock和读写锁ReadWriteLock的区别,说明了读写锁在读多写少的场景下具有明显的性能优势,但是人的欲望是无穷的,还是不能被满足。。

StampedLock1

数据库中的锁

由于大部分码农接触锁都是从数据库中的锁开始的,所以这里不妨先聊聊数据库中的锁。

我们以火车票售票的例子,假设如下场景,两处火车票售票点同时读取某一趟列车车票数据库中的余票数量,然后两处售票点同时卖出一张车票,同时修改余票为 X -1,写回数据库,这样就造成了实际卖出两张火车票而数据库中的记录却只减少了一张。

如果你阅读了公众号【胖滚猪学编程】的并发系列文章,包括:如何解决原子性问题、ReentrantLock互斥锁、读写锁ReadWriteLock,那么你一定知道出现原因和解决方案,对了,可以使用锁。

锁可以分为两大类,乐观锁和悲观锁:

  • 悲观锁:顾名思义,就是很悲观,总是假设最坏的情况,每次去拿数据的时候都认为别人会修改, 所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁。
  • 乐观锁:乐观锁,每次去拿数据的时候想法都是“没事,肯定没被改过”,于是就开心地获取到数据,不放心吗?那就在更新的时候判断一下在此期间别人有没有去更新过这个数据,可以使用版本号等机制。

一般情况下,数据库都会有读共享写独占的锁并发的方案,也就是说读读并发是没问题的,但在读写并发时,则有可能出现读取不一致情况,也就是常说的脏读,所以在悲观锁的模式下,在有写线程的时候,是不允许有任何其他的读和写线程的,也就是说写是独占的,这样会导致系统的吞吐明显下降。我们所说的ReadWriteLock的写锁就属于悲观锁。

如何避免这一情况,答案是使用乐观锁。每个线程都不会修改原始数据,而是从原始数据上拷贝上一份数据,同时记录版本号,不同的线程更新自己的数据,在最终写会时会判断版本号是否变更,如果变更则意味有人已经更改过了,那么当前线程需要做的就是自旋重试,如果重试指定的次数依然失败,那么就应该放弃更新,这种策略仅仅适合写并发并不强烈的场景,如果写竞争严重,那么多次自旋重试的开销也是非常耗性能的,如果竞争激烈,那么写锁独占的方式则更加适合。

那么具体怎么使用版本号机制呢?

很简单,对数据库表添加了一个version字段,设置为bigint类型。查询的时候我们需要查出版本信息,更新的时候,需要将版本信息+1。

1
2
3
4
复制代码1.查询数据信息
select xxx,version from xxx where id= #{id}
2.根据数据信息是判断当前数据库中的version是否还是刚才查出来的那个version
update xxx set xxx=xxx ,version = version+1 where id=#{id} and version= #{version};

由于update指定了where条件,可根据返回修改记录条数来判断当前更新是否生效,如果成功改动0条数据,说明version发生了变更,这时候可以根据自己业务逻辑来决定是否需要回滚事务。

数据库里的乐观锁,查询的时候需要把 version 字段查出来,更新的时候要利用 version 字段做验证。这个 version 字段就类似于今天我们要说的 StampedLock 里面的 stamp。基于上面谈到的这些内容,我们再来分析StampedLock类,就会非常比较容易理解。

本文来源于公众号【胖滚猪学编程】 以漫画形式让编程so easy and interesting !转载请注明出处!

StampedLock

Java 在 1.8 这个版本里,提供了一种叫 StampedLock 的锁,它的性能比读写锁还要好。

对比ReadWriteLock

我们先来看看StampedLock 和上一篇文章讲的 ReadWriteLock 有哪些区别。

ReadWriteLock 支持两种模式:一种是读锁,一种是写锁。而 StampedLock 支持三种模式,分别是:写锁、悲观读锁和乐观读。

其中,写锁、悲观读锁的语义和 ReadWriteLock 的写锁、读锁的语义非常类似,允许多个线程同时获取悲观读锁,但是只允许一个线程获取写锁,写锁和悲观读锁是互斥的。

不同的是:StampedLock 里的写锁和悲观读锁加锁成功之后,都会返回一个 stamp;然后解锁的时候,需要传入这个 stamp,这里的stamp就类似刚刚我们说的数据库version,相信你已经明白了。

我们通过代码演示一下写锁、悲观读锁是如何使用的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
复制代码    // 锁实例
private final StampedLock sl = new StampedLock();

// 排它锁-写锁
void writeLock() {
long stamp = sl.writeLock();//获取写锁
try {
// 业务逻辑
} finally {
sl.unlockWrite(stamp);//释放写锁
}
}

// 悲观读锁
void readLock() {
long stamp = sl.readLock();
try {
// 业务逻辑
} finally {
sl.unlockRead(stamp);
}
}

乐观读

StampedLock 的性能之所以比 ReadWriteLock 还要好,其关键是 StampedLock 支持乐观读的方式。所谓乐观读,即读的时候也能允许一个线程获取写锁,也就是说不是所有的写操作都被阻塞,自然而然的会比所有写都阻塞性能要强。

还是通过代码来说明一下乐观读是如何使用的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
复制代码    // 乐观读
double distanceFromOrigin() {
long stamp = sl.tryOptimisticRead();//(1)
double currentX = x, currentY = y;

// 检查在(1)获取到读锁票据后,锁有没被其他写线程排它性抢占
if (!sl.validate(stamp)) {
// 如果被抢占则获取一个共享读锁(悲观读锁)
stamp = sl.readLock();
try {
currentX = x;
currentY = y;
} finally {
sl.unlockRead(stamp);
}
}
return Math.sqrt(currentX*currentX + currentY*currentY);
}

tryOptimisticRead() 就是我们前面提到的乐观读。不过需要注意的是,由于 tryOptimisticRead() 是无锁的,所以共享变量 x 和 y 读入方法局部变量时,x 和 y 有可能被其他线程修改了。因此最后读完之后,还需要再次验证一下是否存在写操作,这个验证操作是通过调用 validate(stamp) 来实现的。

还有一个巧妙的地方:如果执行乐观读操作的期间,存在写操作,会把乐观读升级为悲观读锁。这个做法挺合理的,否则你就需要在一个循环里反复执行乐观读,直到执行乐观读操作的期间没有写操作(只有这样才能保证 x 和 y 的正确性和一致性),而循环读会浪费大量的 CPU。升级为悲观读锁,代码简练且不易出错。

StampedLock2

锁的升级

在上一篇读写锁文章中,我们说到锁的升级和降级,ReadWriteLock是只允许降级而不允许升级的,而StampedLock 支持锁的降级(通过 tryConvertToReadLock() 方法实现)和升级(通过 tryConvertToWriteLock() 方法实现),读锁居然也可以升级为写锁,这也是它区别于读写锁的一大特性!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
复制代码    // 读锁升级成写锁
void moveIfAtOrigin(double newX, double newY) { // upgrade
// Could instead start with optimistic, not read mode
long stamp = sl.readLock();
try {
while (x == 0.0 && y == 0.0) {
// 尝试将获取的读锁升级为写锁
long ws = sl.tryConvertToWriteLock(stamp);
if (ws != 0L) {
stamp = ws;
x = newX;
y = newY;
break;
} else {
// 读锁升级写锁失败则释放读锁,显示获取独占写锁,然后循环重试
sl.unlockRead(stamp);
stamp = sl.writeLock();
}
}
} finally {
sl.unlock(stamp);
}
}

StampedLock 使用注意事项

StampedLock真有这么完美吗?挑刺时间又来咯!

1、StampedLock 在命名上并没有增加 Reentrant,显然,StampedLock 不支持重入。这个是在使用中必须要特别注意的。

2、StampedLock 的悲观读锁、写锁都不支持条件变量(Condition),这个也需要你注意。

3、使用 StampedLock 一定不要调用中断操作,即不要调用interrupt() 方法,因为内部实现里while循环里面对中断的处理有点问题。如果需要支持中断功能,一定使用可中断的悲观读锁 readLockInterruptibly() 和写锁 writeLockInterruptibly()。

总结

以如何解决原子性问题为起点,我们初始了锁的概念,了解了synchronized锁模型,之后又走进了J.U.C Lock包,首先接触到了ReentrantLock互斥锁,由于互斥锁在读多写少场景的效率不高,因此接触了读写锁ReadWriteLock,而今天,又学习了一种比读写锁还要快的锁StampedLock。说明JAVA真是博大精深,连锁都有那么多种,需要根据实际情况合理选择才是!

关于StampedLock,重点应该了解它独特的思想:乐观的思想。就像人一样,不能总是悲观思想,乐观思想积极面对生活效率才更高!StampedLock通过一个叫做stamp的类似于数据库版本号的字段,实现了乐观读。当然永远乐观也是不行的,StampedLock也有它的缺陷,对于这些,你也需要特别注意。

本文来源于公众号【胖滚猪学编程】 以漫画形式让编程so easy and interesting !转载请注明出处!

本文转载自公众号【胖滚猪学编程】 用漫画让编程so easy and interesting!欢迎关注!

本文转载自: 掘金

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

工作流引擎原理-打造一款适合自己的工作流引擎

发表于 2020-05-17

前言

作为开发人员或多或少都会接触过工作流引擎,如 activiti、 jbpm等,这些工作流引擎都会比较重。在小项目中引入就会显得不是很优雅。本文主要简单介绍工作流引擎的原理并讲述如何慢慢打造一款适合自己的工作流引擎。

流程节点

当工程需若干个不同程序(流程)或分若干个阶段来完成时,某一程序或某一阶段结束,另一程序或某一阶段开始时的转接点(类别点或时间点),称流程节点。 常见的流程节点有:开始结束、任务节点、条件节点、分支节点、合并节点、子流程节点、结束节点等。为了简单起见,这里只讲解最简单的流程(开始->任务->结束)。

开始节点

流程的开始,做一些初始化操作的节点。

  • 类型 START
  • 模型类 StartModel
  • 属性说明
属性名 类型 说明
id String 节点id,流程节点的唯一标识
name String 节点名称,说明是节点做什么的
nextNodeId String 下一个节点id
ext Map 扩展属性

任务节点

该节点会产生任务,任务完成后才能进行下一步

  • 类型 TASK
  • 模型类 TaskModel
  • 属性说明
属性名 类型 说明
id String 节点id,流程节点的唯一标识
name String 节点名称,说明是节点做什么的
nextNodeId String 下一个节点id
performType String 参与方式(ANY->任何一个参与者处理完即可执行下一步,ALL->所有参与者都完成,才可执行下一步)
expireTime String 期望完成时间(产生的任务期望啥时间完成)
reminderTime String 提醒时间(如任务未处理,啥时候提醒)
reminderRepeat Integer 提醒间隔(分钟)(如任务未处理,提醒规则是什么)
autoExecute Boolean 是否自动执行(如任务未处理且到期,是否自动执行)
ext Map 扩展属性

结束节点

该节点说明流程已走完,在这里触发流程完成事件

  • 类型 END
  • 模型类 EndModel
  • 属性说明
属性名 类型 说明
id String 节点id,流程节点的唯一标识
name String 节点名称,说明是节点做什么的
ext Map 扩展属性

流程定义样例

  • 新闻中心稿件发布流程
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
复制代码{
"flowId":"cmsNews",
"flowName":"新闻中心稿件发布",
"nodeList": [
{
"id":"start",
"nodeType": "START",
"name":"开始",
"nextNodeId": "collect"
},
{
"id":"collect",
"nodeType": "TASK",
"name":"采编新闻稿件",
"performType":"ANY",
"nextNodeId": "firstReview",
"ext":{
"userIds":"1,2,3"
}
},
{
"id":"firstReview",
"nodeType": "TASK",
"name":"党群部宣传科初审",
"performType":"ANY",
"nextNodeId": "secondReview",
"ext":{
"userIds":"1,2,3"
}
},
{
"id":"secondReview",
"nodeType": "TASK",
"name":"党群部二审",
"performType":"ANY",
"nextNodeId": "end",
"ext":{
"userIds":"1,2,3"
}
},
{
"id":"end",
"nodeType": "END",
"name":"结束"
}
]
}

表结构设计

  • flow_define(流程定义)
字段 类型 空 默认 注释
id char(36) False 主键
flow_define_code varchar(16) True 流程定义编码
flow_name varchar(100) True 流程名称
description varchar(255) True 流程描述
flow_define mediumtext True 流程定义(json字符串)
create_time datetime True 创建时间
update_time datetime True 更新时间
is_deleted tinyint(1) unsigned True 0 是否删除(1->删除YES,0->未删除NO)
  • flow_work(流程实例)
字段 类型 空 默认 注释
id char(36) False 主键
flow_name varchar(100) True 流程名称
flow_define_code varchar(32) True 流程定义编码
last_operator char(36) True 最后一个操作者id
current_node_id char(36) True 当前节点id
next_node_id char(36) True 下一步节点id
flow_define mediumtext True 流程定义(json字符串)
status int(6) True 流程状态(10->开始START,30->结束END,40->取消CANCEL)
flow_param mediumtext True 流程参数json
create_time datetime True 创建时间
update_time datetime True 更新时间
is_deleted tinyint(1) unsigned True 0 是否删除(1->删除YES,0->未删除NO)
  • flow_task(流程任务)
字段 类型 空 默认 注释
id char(36) False 主键
flow_work_id char(36) True 流程id
flow_node_id char(36) True 流程节点id
task_name varchar(100) True 任务名称
operator char(36) True 操作者用户id
actor_user_id char(36) True 执行用户的id
status varchar(6) True 10 任务状态(10->新建CREATED,20->已完成FINISHED,30->过期EXPIRED,40->取消CANCEL,50->驳回REJECT)
service_type varchar(32) True 业务类型
service_id varchar(36) True 关联的业务id
finish_time datetime True 完成时间
flow_param mediumtext True 流程参数json
create_time datetime True 创建时间
update_time datetime True 更新时间
is_deleted tinyint(1) unsigned True 0 是否删除(1->删除YES,0->未删除NO)

处理流程关键字描述

  • 关于流程定义文件

流程定义文件即描述流程各节点关系的文件,在这里使用json文件来描述,存储在flow_define表的flow_define字段上。

  • 关于流程定义文件解析

流程定义文件解析即将流程定义文件中的各个流程节点解析出来,并将其组装成流程模型。

  • 关于流程实例

流程实例即使用流程定义生成的一个流程实例,生成的流程实例会存储在flow_work表。(该操作是由业务系统发起,即发起一个流程。)

  • 关于任务

任务会在任务节点上产生,即当执行到任务节点时,会使用该节点的处理器,给参与的人员分派任务,分派的任务存储在flow_task表中。业务系统中可通过查询该表,拿到登录用户的待办任务。

  • 关于执行完成任务

当参与人员把待办任务处理后,业务系统会调用完成任务方法,流程引擎会修改任务完成状态,并驱动流程往下一个节点行进。

  • 关于驳回任务

当参与人员驳回任务后,业务系统会调用任务驳回方法,流程引擎会将流程进度调回上一个节点。

核心处理流程描述

FlowWorkCtl.java

  • 启动流程实例
  1. 加载流程定义
  2. 解析流程定义文件
  3. 节点处理器控制器调用执行节点处理
    1. 如果是开始节点,只需要发布开始事件
    2. 如果是任务节点,则根据节点配置,分派任务,发布开始事件并更新流程实例节点状态(非必须,只是简单记录)
    3. 如果是结束节点,则修改流程实例为完成,并发布结束事件。
  • 执行任务
  1. 获取任务实例
  2. 获取流程实例
  3. 解析流程定义文件
  4. 将任务设置为已完成
  5. 判断节点任务是否完成
    1. 如果已完成,则拿到下一个节点,调用节点处理控制器执行
    2. 如未完成,则无需处理
  • 驳回任务
  1. 获取任务实例
  2. 获取流程实例
  3. 解析流程定义文件
  4. 将任务设置为已驳回
  5. 修改节点的其他参与人员的任务为已取消
  6. 拿到上一个节点模型,调用节点处理控制器执行

开始编码

工程目录

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
复制代码com.mole.modules.flow
├── entity # 实体类,与表结构对应
├── FlowDefine.java # 流程定义实体
├── FlowTask.java # 流程任务实体
└── FlowWork.java # 流程实例实体
├── enums # 错误码枚举
└── FlowErrEnum.java
├── event # 事件
├── FlowEndNodeEvent.java # 流程结束事件
├── FlowStartNodeEvent.java # 流程开始事件
├── FlowTaskCreatedEvent.java # 流程任务创建事件
└── FlowTaskNode.Event.java # 任务开始执行事件
├── mapper # 持久层
├── FlowDefineMapper.java
├── FlowTaskMapper.java
└── FlowWorkMapper.java
├── param # 参数实体
└── FlowParam.java # 流程参数实体
├── model # 模型层
├── BaseModel.java # 基础节点模型
├── EndModel.java # 结束节点模型
├── FlowModel.java # 流程模型
├── StartModel.java # 开始节点模型
└── TaskModel.java # 任务节点模型
├── parser
└── FlowNodeParser.java # 节点解析器
├── processor
├── impl
├── EndNodeProcessorImpl.java # 结束节点处理实现类
├── StartNodeProcessorImpl.java # 开始节点处理实现类
└── TaskNodeProcessorImpl.java # 任务节点处理实现类
├── FlowNodeProcessor.java # 节点处理接口
└── FlowNodeProcessorCtl.java # 节点处理器控制器
├── service
└── FlowNodeUserService.java # 自定义节点参与人接口
└── FlowWorkCtl.java # 流程控制类-引擎

核心类

模型类

  • FlowModel.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
复制代码package com.mole.modules.flow.model;

import java.io.Serializable;
import java.util.List;
/**
* 流程模型
* @author MLD
*
*/
public class FlowModel implements Serializable{

/**
*
*/
private static final long serialVersionUID = -3978388377128544781L;
/**
* 流程id
*/
private String flowId;
/**
* 流程名称
*/
private String flowName;
/**
* 流程节点
*/
private List<BaseModel> nodeList;
// ....get .....set 略
/**
* 通过id获取节点
* @param nodeId
* @return
*/
public BaseModel getNodeModel(String nodeId) {
if(null == nodeList || nodeList.isEmpty()) {
return null;
}
for(BaseModel node : nodeList) {
if(node.getId().equals(nodeId)){
return node;
}
}
return null;
}
/**
* 获取开始节点模型
* @return
*/
public StartModel getStartModel() {
if(null == nodeList || nodeList.isEmpty()) {
return null;
}
for(BaseModel node : nodeList) {
if(node instanceof StartModel){
return (StartModel)node;
}
}
return null;
}
/**
* 通过当前节点的上一个节点
* @param nodeId
* @return
*/
public BaseModel getPreNodeModel(String nodeId) {
if(null == nodeList || nodeList.isEmpty()) {
return null;
}
for(BaseModel node : nodeList) {
if(nodeId.equals(node.getNextNodeId())){
return node;
}
}
return null;
}
/**
* 获取结束节点模型
* @return
*/
public EndModel getEndModel() {
if(null == nodeList || nodeList.isEmpty()) {
return null;
}
for(BaseModel node : nodeList) {
if(node instanceof EndModel){
return (EndModel)node;
}
}
return null;
}

}
  • BaseModel.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
复制代码package com.mole.modules.flow.model;
import java.io.Serializable;
import java.util.Map
public class BaseModel implements Serializable {
/**
* 节点id
*/
private String id;
/**
* 节点名称
*/
private String name;
/**
* 节点类型
*/
private String nodeType;
/**
* 下一个节点id
*/
private String nextNodeId;
/**
* 扩展属性
*/
private Map<String,Object> ext;
}
  • StartModel.java
1
2
3
4
5
复制代码package com.mole.modules.flow.model;
import java.io.Serializable;
public class StartModel extends BaseModel implements Serializable {

}
  • TaskModel.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
复制代码package com.mole.modules.flow.model;
import java.io.Serializable;
public class TaskModel extends BaseModel implements Serializable {
/**
* 参与方式(
* ANY->任何一个参与者处理完即可执行下一步,
* ALL->所有参与者都完成,才可执行下一步
* )
*/
private String performType;
/**
* 期望完成时间(产生的任务期望啥时间完成)
*/
private String expireTime;
/**
* 提醒时间(如任务未处理,啥时候提醒)
*/
private String reminderTime;
/**
* 提醒间隔(分钟)(如任务未处理,提醒规则是什么)
*/
private Integer reminderRepeat;
/**
* 是否自动执行(如任务未处理且到期,是否自动执行)
*/
private Boolean autoExecute;
}
  • EndModel
1
2
3
4
5
复制代码package com.mole.modules.flow.model;
import java.io.Serializable;
public class EndModel extends BaseModel implements Serializable {

}

流程节点解析类

将流程定义json转成对应的流程模型

  • FlowNodeParser.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
复制代码package com.mole.modules.flow.parser;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Iterator;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.mole.modules.framework.util.JsonUtil;
import com.mole.modules.flow.model.BaseModel;
import com.mole.modules.flow.model.EndModel;
import com.mole.modules.flow.model.FlowModel;
import com.mole.modules.flow.model.StartModel;
import com.mole.modules.flow.model.TaskModel;
/**
* 节点解析器
* @author MLD
*
*/
public class FlowNodeParser {
private ObjectMapper objectMapper = new ObjectMapper();
private final static String FLOW_ID="flowId";
private final static String FLOW_NAME="flowName";
private final static String NODE_LIST="nodeList";
private final static String NODE_TYPE="nodeType";
/**
* 解析流程定义
* @param flowDefine
* @return
* @throws IOException
*/
public FlowModel parser(String flowDefine) throws IOException {
JsonNode root = objectMapper.readTree(flowDefine);
if(!root.has(FLOW_ID)||!root.has(FLOW_NAME)||!root.has(NODE_LIST)) {

}
String flowId = root.get(FLOW_ID).asText();
String flowName = root.get(FLOW_NAME).asText();
JsonNode nodeList = root.get("nodeList");
if(!nodeList.isArray()) {
return null;
}
FlowModel flowModel = new FlowModel();
flowModel.setFlowId(flowId);
flowModel.setFlowName(flowName);
flowModel.setNodeList(new ArrayList<BaseModel>());
Iterator<JsonNode> nodes = nodeList.elements();
while (nodes.hasNext()) {
JsonNode node = nodes.next();
if(node.has(NODE_TYPE)) {
String nodeType = node.get(NODE_TYPE).asText();
if("START".equals(nodeType)) {
// 开始节点
StartModel model = objectMapper.readValue(node.toString(), StartModel.class);
flowModel.getNodeList().add(model);
} else if("TASK".equals(nodeType)) {
// 任务节点
TaskModel model = objectMapper.readValue(node.toString(), TaskModel.class);
flowModel.getNodeList().add(model);
} else if("END".equals(nodeType)) {
// 结束节点
EndModel model = objectMapper.readValue(node.toString(), EndModel.class);
flowModel.getNodeList().add(model);
}
}
}
return flowModel;
}
}

节点处理器类

节点处理器主要是对不同的节点进行不一样调度处理,如开始节点会驱动流程往下一步走,任务节点会产生任务,结束节点会将流程结束等。

  • FlowNodeProcessor.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
复制代码package com.mole.modules.flow.processor;

import com.mole.modules.flow.entity.FlowWork;
import com.mole.modules.flow.model.BaseModel;
import com.mole.modules.flow.model.FlowModel;
import com.mole.modules.flow.param.FlowParam;

/**
* 节点处理器接口
* @author MLD
*
*/
public interface FlowNodeProcessor {
/**
* 要处理的节点类型
* @return
*/
public String getNodeType();
/**
* 流程节点处理方法
* @param flowWork 流程实例
* @param flowModel 当前流程模型
* @param currentNodeModel 当前节点模型
* @param flowParam 流程参数
*/
public void process(FlowWork flowWork,FlowModel flowModel,BaseModel currentNddeModel,FlowParam flowParam);
}
  • StartNodeProcessorImpl.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
复制代码package com.mole.modules.flow.processor.impl;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import com.mole.modules.flow.entity.FlowWork;
import com.mole.modules.flow.event.FlowStartNodeEvent;
import com.mole.modules.flow.model.BaseModel;
import com.mole.modules.flow.model.FlowModel;
import com.mole.modules.flow.model.StartModel;
import com.mole.modules.flow.param.FlowParam;
import com.mole.modules.flow.processor.FlowNodeProcessor;
/**
* 开始节点处理器
* @author MLD
*
*/
@Component
public class StartNodeProcessorImpl implements FlowNodeProcessor {
@Autowired(required=false)
private FlowStartNodeEvent flowStartEvent;
@Override
public void process(FlowWork flowWork, FlowModel flowModel, BaseModel currentNddeModel,FlowParam flowParam) {
// 开始节点事件--
if(null != flowStartEvent) {
StartModel startModel = flowModel.getStartModel();
flowStartEvent.onEvent(flowWork, startModel, flowParam);
}
}
@Override
public String getNodeType() {
return "START";
}

}
  • TaskNodeProcessorImpl.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
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
复制代码package com.mole.modules.flow.processor.impl;

import java.util.ArrayList;
import java.util.Date;
import java.util.List;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import com.mole.framework.base.ServiceException;
import com.mole.framework.base.YesNoEnum;
import com.mole.framework.util.JsonUtil;
import com.mole.framework.util.StringUtil;
import com.mole.modules.flow.entity.FlowTask;
import com.mole.modules.flow.entity.FlowWork;
import com.mole.modules.flow.enums.FlowErrEnum;
import com.mole.modules.flow.event.FlowTaskCreatedEvent;
import com.mole.modules.flow.event.FlowTaskNodeEvent;
import com.mole.modules.flow.mapper.FlowTaskMapper;
import com.mole.modules.flow.mapper.FlowWorkMapper;
import com.mole.modules.flow.model.BaseModel;
import com.mole.modules.flow.model.FlowModel;
import com.mole.modules.flow.model.TaskModel;
import com.mole.modules.flow.param.FlowParam;
import com.mole.modules.flow.processor.FlowNodeProcessor;
import com.mole.modules.flow.service.FlowNodeUserService;
/**
* 任务节点处理器
* @author MLD
*
*/
@Component
public class TaskNodeProcessorImpl implements FlowNodeProcessor {
@Autowired
private FlowWorkMapper flowWorkMapper;
@Autowired
private FlowTaskMapper flowTaskMapper;
@Autowired(required=false)
private FlowNodeUserService flowNodeUserService;
@Autowired(required=false)
private FlowTaskNodeEvent flowTaskStartEvent;
@Autowired(required=false)
private FlowTaskCreatedEvent flowTaskCreatedEvent;
@Override
public void process(FlowWork flowWork, FlowModel flowModel, BaseModel currentNddeModel,FlowParam flowParam) {
// 下一个节点
// BaseModel nextNodeModel = flowModel.getNodeModel(flowWork.getNextNodeId());
TaskModel taskModel = (TaskModel)currentNddeModel;
// 1. 创建任务
createTask(flowWork, flowParam, taskModel);
// 2. 修改当前流程实例状态
if(null != flowTaskStartEvent) {
flowTaskStartEvent.OnEvent(flowWork, taskModel, flowParam);
}
}
/**
* 创建任务
* @param flowWork
* @param param
* @param taskModel
*/
private int createTask(FlowWork flowWork,FlowParam flowParam,TaskModel taskModel){
Date now = new Date();
String flowWorkId = flowWork.getId();
List<String> userIds = flowParam.getUserIds();
if(null==userIds) {
userIds = new ArrayList<>();
}
if(null!=flowNodeUserService) {
List<String> list = flowNodeUserService.loadUser(taskModel, flowParam);
if(null!=list) {
for(String id:list) {
if(!userIds.contains(id)) {
userIds.add(id);
}
}

}
}
if(userIds.isEmpty()) {
throw new ServiceException(FlowErrEnum.FLOW86000009);
}
for(String actorUserId:userIds) {
// 2. 创建一个任务
FlowTask flowTask = new FlowTask();
flowTask.setCreateTime(now);
flowTask.setFlowWorkId(flowWorkId);
flowTask.setIsDeleted(YesNoEnum.NO);
flowTask.setServiceId(flowParam.getServiceId());
flowTask.setServiceType(flowParam.getServiceType());
flowTask.setTaskName(taskModel.getName());
flowTask.setUpdateTime(now);
flowTask.setOperator(flowParam.getOperator());
flowTask.setStatus(FlowTask.StatusEnum.CREATED);
flowTask.setFlowNodeId(taskModel.getId());
flowTask.setActorUserId(actorUserId);
flowTask.setFlowParam(JsonUtil.toJson(flowParam));
if(StringUtil.isNotEmpty(flowParam.getTitle())) {
flowTask.setTaskName(flowParam.getTitle());
}
flowTaskMapper.insertSelective(flowTask);
if(null != flowTaskCreatedEvent) {
flowTaskCreatedEvent.onEvent(flowWork, flowTask, flowParam);
}
}
return userIds.size();
}

@Override
public String getNodeType() {
return "TASK";
}

}
  • EndNodeProcessorImpl.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
复制代码package com.mole.modules.flow.processor.impl;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import com.mole.modules.flow.entity.FlowWork;
import com.mole.modules.flow.event.FlowEndNodeEvent;
import com.mole.modules.flow.model.BaseModel;
import com.mole.modules.flow.model.EndModel;
import com.mole.modules.flow.model.FlowModel;
import com.mole.modules.flow.param.FlowParam;
import com.mole.modules.flow.processor.FlowNodeProcessor;
@Component
public class EndNodeProcessorImpl implements FlowNodeProcessor{
@Autowired(required=false)
private FlowEndNodeEvent flowEndEvent;
@Override
public void process(FlowWork flowWork, FlowModel flowModel, BaseModel currentNddeModel,FlowParam flowParam) {
if(null == flowEndEvent) {
EndModel endModel = flowModel.getEndModel();
flowEndEvent.onEvent(flowWork, endModel, flowParam);;
}
}

@Override
public String getNodeType() {
return "END";
}

}

节点处理器控制类

由节点处理器控制类统一调用处理器

  • FlowNodeProcesstorCtl.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
复制代码package com.mole.modules.flow.processor;

import java.util.Date;
import java.util.List;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import com.mole.modules.flow.entity.FlowWork;
import com.mole.modules.flow.mapper.FlowWorkMapper;
import com.mole.modules.flow.model.BaseModel;
import com.mole.modules.flow.model.EndModel;
import com.mole.modules.flow.model.FlowModel;
import com.mole.modules.flow.model.StartModel;
import com.mole.modules.flow.param.FlowParam;
/**
* 节点处理器控制器
* @author MLD
*
*/
@Component
public class FlowNodeProcesstorCtl {
@Autowired(required=false)
private List<FlowNodeProcessor> flowNodeProcessorList;
@Autowired
private FlowWorkMapper flowWorkMapper;
/**
* 节点流程处理器控制
* @param flowWork 当前流程实例
* @param flowModel 当前流程实例模型
* @param model 当前节点模型
* @param param 流程参数
*/
public void process(FlowWork flowWork,FlowModel flowModel, BaseModel model,FlowParam param){
FlowWork upFlowWork = new FlowWork();
if(model == null) {
return;
}
// 执行开始节点
for(FlowNodeProcessor processor:flowNodeProcessorList) {
if(processor.getNodeType().equals(model.getNodeType())){
BaseModel nextNodeModel = flowModel.getNodeModel(model.getNextNodeId());
if(model instanceof StartModel) { //开始节点
// 执行开始模型
processor.process(flowWork, flowModel, model,param);
// 执行下一个模型
process(flowWork,flowModel,nextNodeModel, param);
} else if(model instanceof EndModel) {
// 结束节点
upFlowWork.setStatus(FlowWork.StatusEnum.END);
Date now = new Date();
upFlowWork.setId(flowWork.getId());
upFlowWork.setCurrentNodeId(model.getId());
upFlowWork.setNextNodeId("");
upFlowWork.setLastOperator(param.getOperator());
upFlowWork.setUpdateTime(now);
flowWorkMapper.updateByPrimaryKeySelective(upFlowWork);
} else { // 非开始和结束节点
processor.process(flowWork, flowModel, model,param);
Date now = new Date();
upFlowWork.setId(flowWork.getId());
upFlowWork.setCurrentNodeId(model.getId());
upFlowWork.setNextNodeId(model.getNextNodeId());
upFlowWork.setLastOperator(param.getOperator());
upFlowWork.setUpdateTime(now);
flowWorkMapper.updateByPrimaryKeySelective(upFlowWork);
}
// 存在一个满足条件的,即可中断,其实设计上也仅会有一个相同类型的。
break;
}
}
}
}

节点事件接口类

节点事件接口,由业务系统实现

  • FlowStartNodeEvent
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
复制代码package com.mole.modules.flow.event;

import com.mole.modules.flow.entity.FlowWork;
import com.mole.modules.flow.model.StartModel;
import com.mole.modules.flow.param.FlowParam;
/**
* 流程实例开始事件-由业务系统实现
* @author MLD
*
*/
public interface FlowStartNodeEvent {
/**
* @param flowWork 当前流程实例
* @param startModel 开始节点模型
* @param param 流程参数
*/
public void onEvent(FlowWork flowWork, StartModel startModel,FlowParam flowParam);
}
  • FlowTaskNodeEvent.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
复制代码package com.mole.modules.flow.event;

import com.mole.modules.flow.entity.FlowWork;
import com.mole.modules.flow.model.TaskModel;
import com.mole.modules.flow.param.FlowParam;

/**
* 任务开始事件-由业务系统实现
* @author MLD
*
*/
public interface FlowTaskNodeEvent {
/**
* 任务开始事件
* @param flowWork 当前流程实例
* @param taskModel 任务节点模型
* @param flowParam 流程参数
*/
public void OnEvent(FlowWork flowWork,TaskModel taskModel,FlowParam flowParam);
}
  • FlowTaskCreatedEvent.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
复制代码package com.mole.modules.flow.event;

import com.mole.modules.flow.entity.FlowTask;
import com.mole.modules.flow.entity.FlowWork;
import com.mole.modules.flow.param.FlowParam;

/**
* 创建任务事件-由业务系统实现
* @author MLD
*
*/
public interface FlowTaskCreatedEvent {
/**
*
* @param flowWork 当前流程实例
* @param flowTask 当前任务实例
* @param flowParam 流程参数
*/
public void onEvent(FlowWork flowWork,FlowTask flowTask,FlowParam flowParam);
}
  • FlowEndNodeEvent.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
复制代码package com.mole.modules.flow.event;

import com.mole.modules.flow.entity.FlowWork;
import com.mole.modules.flow.model.EndModel;
import com.mole.modules.flow.param.FlowParam;
/**
* 流程实例结束事件-由业务系统实现
* @author MLD
*
*/
public interface FlowEndNodeEvent {
/**
*
* @param flowWork 当前流程实例
* @param endModel 结束节点
* @param flowParam 流程参数
*/
public void onEvent(FlowWork flowWork,EndModel endModel, FlowParam flowParam);
}

自定义任务节点参与人接口类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
复制代码package com.mole.modules.flow.service;

import java.util.List;

import com.mole.modules.flow.model.BaseModel;
import com.mole.modules.flow.param.FlowParam;
/**
* 自定义节点参与人接口-由业务系统实现
* @author MLD
*
*/
public interface FlowNodeUserService {
/**
* 加载节点参与人
* @param nodeModel 节点模型
* @param param 流程参数
* @return
*/
public List<String> loadUser(BaseModel nodeModel,FlowParam param);
}

流程控制类

提供给业务操作流程的类

  • FlowWorkCtl.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
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
复制代码package com.mole.modules.flow;

import java.io.IOException;
import java.util.Date;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import tk.mybatis.mapper.entity.Condition;

import com.mole.framework.base.ServiceException;
import com.mole.framework.base.YesNoEnum;
import com.mole.framework.util.JsonUtil;
import com.mole.modules.flow.entity.FlowDefine;
import com.mole.modules.flow.entity.FlowTask;
import com.mole.modules.flow.entity.FlowWork;
import com.mole.modules.flow.enums.FlowErrEnum;
import com.mole.modules.flow.mapper.FlowDefineMapper;
import com.mole.modules.flow.mapper.FlowTaskMapper;
import com.mole.modules.flow.mapper.FlowWorkMapper;
import com.mole.modules.flow.model.BaseModel;
import com.mole.modules.flow.model.FlowModel;
import com.mole.modules.flow.model.StartModel;
import com.mole.modules.flow.param.FlowParam;
import com.mole.modules.flow.parser.FlowNodeParser;
import com.mole.modules.flow.processor.FlowNodeProcesstorCtl;

/**
* 流程控制类-引擎
* @author MLD
*
*/
@Component
public class FlowWorkCtl {
/**
* 通过流程定义编码启动流程
* @param flowDefineCode 流程定义编码
* @param param 流程启动入参
*/
@Autowired
private FlowDefineMapper flowDefineMapper;
@Autowired
private FlowNodeParser flowNodeParser;
@Autowired
private FlowWorkMapper flowWorkMapper;
@Autowired
private FlowNodeProcesstorCtl flowNodeProcesstorCtl;
@Autowired
private FlowTaskMapper flowTaskMapper;
/**
* 启动一个流程
* @param flowDefineCode
* @param operator 操作者用户id
* @param param
*/
public FlowWork startProcess(String flowDefineCode,String operator,FlowParam param){
// TODO 加载流程配置
FlowDefine q = new FlowDefine();
q.setFlowDefineCode(flowDefineCode);
q.setIsDeleted(YesNoEnum.NO);
if(null==param.getOperator()) {
param.setOperator(operator);
}
FlowDefine flowDefine = flowDefineMapper.selectOne(q);
if(null == flowDefine) {
// 流程定义不存在
throw new ServiceException(FlowErrEnum.FLOW86000001);
}
FlowModel flowModel = null;
try {
flowModel = flowNodeParser.parser(flowDefine.getFlowDefine());
} catch (IOException e) {
// 流程解析异常
throw new ServiceException(FlowErrEnum.FLOW86000002);
}
StartModel startModel = flowModel.getStartModel();
Date now = new Date();
// 创建一个新流程
FlowWork flowWork = new FlowWork();
flowWork.setCreateTime(now);
flowWork.setCurrentNodeId(startModel.getId());
flowWork.setFlowDefine(flowDefine.getFlowDefine());
flowWork.setFlowName(flowDefine.getFlowName());
flowWork.setIsDeleted(YesNoEnum.NO);
flowWork.setNextNodeId(startModel.getNextNodeId());
flowWork.setStatus(FlowWork.StatusEnum.START);
flowWork.setUpdateTime(now);
flowWork.setFlowParam(JsonUtil.toJson(param));
flowWork.setFlowDefineCode(flowDefineCode);
flowWork.setLastOperator(operator);
flowWorkMapper.insertSelective(flowWork);
// 执行开始模型
flowNodeProcesstorCtl.process(flowWork, flowModel, startModel, param);
return flowWork;
}
/**
* 完成一个任务
* @param flowTaskId 流程任务id
* @param operator 操作者用户id
* @param param 流程参数
*/
public void completeTask(String flowTaskId,String operator,FlowParam param) {
if(null==param.getOperator()) {
param.setOperator(operator);
}
FlowTask task = flowTaskMapper.selectByPrimaryKey(flowTaskId);
if(null == task || YesNoEnum.YES.equals(task.getIsDeleted())) {
// 流程任务不存在
throw new ServiceException(FlowErrEnum.FLOW86000007);
}
// 判断操作者是否在分派的用户里
if(!task.getActorUserId().equals(operator)) {
// 该用户无流程任务
throw new ServiceException(FlowErrEnum.FLOW86000008);
}
if(FlowTask.StatusEnum.FINISHED.equals(task.getStatus())) {
// 已完成,就不处理了
return;
}
FlowWork flowWork = flowWorkMapper.selectByPrimaryKey(task.getFlowWorkId());
if(null==flowWork) {
// 流程实例不存在
throw new ServiceException(FlowErrEnum.FLOW86000005);
}
FlowModel flowModel = null;
try {
flowModel = flowNodeParser.parser(flowWork.getFlowDefine());
} catch (IOException e) {
// 流程解析异常
throw new ServiceException(FlowErrEnum.FLOW86000002);
}
BaseModel currentNodeModel = flowModel.getNodeModel(task.getFlowNodeId());
BaseModel nextNodeModel = flowModel.getNodeModel(currentNodeModel.getNextNodeId());
Date now = new Date();
FlowTask upTask = new FlowTask();
upTask.setId(task.getId());
upTask.setUpdateTime(now);
upTask.setStatus(FlowTask.StatusEnum.FINISHED);
upTask.setFinishTime(now);
// 修改当前任务为已完成
flowTaskMapper.updateByPrimaryKeySelective(upTask);
int count = 0;
if("ALL".equals(currentNodeModel.getNodeType())) {
// 所有人都完成,才能走下一步流程
Condition qTaskCondition = new Condition(FlowTask.class);
qTaskCondition.createCriteria().andEqualTo("flowWorkId", task.getFlowWorkId())
.andEqualTo("flowNodeId", task.getFlowNodeId())
.andEqualTo("status", FlowTask.StatusEnum.CREATED)
.andEqualTo("isDeleted", YesNoEnum.NO)
.andNotEqualTo("actorUserId", operator);
count = flowTaskMapper.selectCountByCondition(qTaskCondition);
} else {
// ANY 任务一个节点完成都可以走下一步,别的任务要修改为已取消
Condition upTaskCondition = new Condition(FlowTask.class);
upTaskCondition.createCriteria().andEqualTo("flowWorkId", task.getFlowWorkId())
.andEqualTo("flowNodeId", task.getFlowNodeId())
.andEqualTo("status", FlowTask.StatusEnum.CREATED)
.andEqualTo("isDeleted", YesNoEnum.NO)
.andNotEqualTo("actorUserId", operator);
FlowTask upTaskToCancel = new FlowTask();
upTaskToCancel.setUpdateTime(now);
upTaskToCancel.setStatus(FlowTask.StatusEnum.CANCEL);
flowTaskMapper.updateByPrimaryKeySelective(upTaskToCancel);
}
if (count==0) {
// 3. 执行下一步流程
flowNodeProcesstorCtl.process(flowWork, flowModel, nextNodeModel, param);
}
}
/**
* 驳回一个任务
* @param flowTaskId 流程任务id
* @param operator 操作者用户id
* @param flowParam 流程参数
*/
public void rejectTask(String flowTaskId,String operator,FlowParam flowParam) {
if(null==flowParam.getOperator()) {
flowParam.setOperator(operator);
}
FlowTask task = flowTaskMapper.selectByPrimaryKey(flowTaskId);
if(null == task || YesNoEnum.YES.equals(task.getIsDeleted())) {
// 流程任务不存在
throw new ServiceException(FlowErrEnum.FLOW86000007);
}
// 判断人是否在里面
if(!task.getActorUserId().equals(operator)) {
// 该用户无流程任务
throw new ServiceException(FlowErrEnum.FLOW86000008);
}
if(FlowTask.StatusEnum.FINISHED.equals(task.getStatus())) {
// 已完成,就不处理了
return;
}
FlowWork flowWork = flowWorkMapper.selectByPrimaryKey(task.getFlowWorkId());
if(null==flowWork) {
// 流程实例不存在
throw new ServiceException(FlowErrEnum.FLOW86000005);
}
FlowModel flowModel = null;
try {
flowModel = flowNodeParser.parser(flowWork.getFlowDefine());
} catch (IOException e) {
// 流程解析异常
throw new ServiceException(FlowErrEnum.FLOW86000002);
}
Date now = new Date();
FlowTask upTask = new FlowTask();
upTask.setId(task.getId());
upTask.setUpdateTime(now);
upTask.setStatus(FlowTask.StatusEnum.REJECT);
upTask.setFinishTime(now);
// 修改当前任务为驳回
flowTaskMapper.updateByPrimaryKeySelective(upTask);
// 中断当前节点任务
Condition upTaskCondition = new Condition(FlowTask.class);
upTaskCondition.createCriteria().andEqualTo("flowWorkId", task.getFlowWorkId())
.andEqualTo("flowNodeId", task.getFlowNodeId())
.andEqualTo("status", FlowTask.StatusEnum.CREATED)
.andEqualTo("isDeleted", YesNoEnum.NO)
.andNotEqualTo("actorUserId", operator);
FlowTask upTaskToCancel = new FlowTask();
upTaskToCancel.setUpdateTime(now);
upTaskToCancel.setStatus(FlowTask.StatusEnum.CANCEL);
flowTaskMapper.updateByPrimaryKeySelective(upTaskToCancel);

// 获取上一个节点
BaseModel preNodeModel = flowModel.getPreNodeModel(task.getFlowNodeId());
// 执行上一个节点处理器
flowNodeProcesstorCtl.process(flowWork, flowModel, preNodeModel, flowParam);
}
}

调用样例

  • 启动一个流程实例
1
2
3
4
5
6
7
8
复制代码String flowDefineCode = "yzq_invite_bid";
FlowParam flowParam = new FlowParam();
flowParam.setServiceId(UUID.randomUUID().toString());
flowParam.setServiceType("yzq_invite_bid");
flowParam.setUserIds(new ArrayList<>());
flowParam.getUserIds().add("1");
Sting operator = "1";
flowWorkCtl.startProcess(flowDefineCode,operator, flowParam);
  • 完成一个任务
1
2
3
4
5
6
7
8
复制代码String flowTaskId = "flowTaskId";
FlowParam flowParam = new FlowParam();
flowParam.setServiceId(UUID.randomUUID().toString());
flowParam.setServiceType("yzq_invite_bid");
flowParam.setUserIds(new ArrayList<>());
flowParam.getUserIds().add("1");
Sting operator = "1";
flowWorkCtl.completeTask(flowTaskId, operator, flowParam);
  • 驳回任务
1
2
3
4
5
6
7
8
复制代码String flowTaskId = "flowTaskId";
FlowParam flowParam = new FlowParam();
flowParam.setServiceId(UUID.randomUUID().toString());
flowParam.setServiceType("yzq_invite_bid");
flowParam.setUserIds(new ArrayList<>());
flowParam.getUserIds().add("1");
Sting operator = "1";
flowWorkCtl.rejectTask(flowTaskId, operator, flowParam);

本工程使用到的框架

  • springboot 2.0
  • tk.mybatis

其他

本想开源的,但是该项目和公司的框架耦合的太厉害,不太方便抽离,等哪天有空,会考虑把这部分代码单独抽离开源。还有一点就是,目前考虑的只是最简单的顺序流程,涉及到条件、分支、合并、子流程等复杂的流程暂时还没考虑。不过本意只是想一步步来,慢慢剖析原理。

本文转载自: 掘金

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

es 在数十亿级别数量下如何提高查询效率?

发表于 2020-05-17

面试题

es 在数据量很大的情况下(数十亿级别)如何提高查询效率啊?

面试官心理分析

这个问题是肯定要问的,说白了,就是看你有没有实际干过 es,因为啥?其实 es 性能并没有你想象中那么好的。很多时候数据量大了,特别是有几亿条数据的时候,可能你会懵逼的发现,跑个搜索怎么一下 5~10s,坑爹了。第一次搜索的时候,是 5~10s,后面反而就快了,可能就几百毫秒。

你就很懵,每个用户第一次访问都会比较慢,比较卡么?所以你要是没玩儿过 es,或者就是自己玩玩儿 demo,被问到这个问题容易懵逼,显示出你对 es 确实玩儿的不怎么样?

面试题剖析

说实话,es 性能优化是没有什么银弹的,啥意思呢?就是不要期待着随手调一个参数,就可以万能的应对所有的性能慢的场景。也许有的场景是你换个参数,或者调整一下语法,就可以搞定,但是绝对不是所有场景都可以这样。

性能优化的杀手锏——filesystem cache

你往 es 里写的数据,实际上都写到磁盘文件里去了,查询的时候,操作系统会将磁盘文件里的数据自动缓存到 filesystem cache 里面去。

es-search-process.png

es 的搜索引擎严重依赖于底层的 filesystem cache,你如果给 filesystem cache 更多的内存,尽量让内存可以容纳所有的 idx segment file 索引数据文件,那么你搜索的时候就基本都是走内存的,性能会非常高。

性能差距究竟可以有多大?我们之前很多的测试和压测,如果走磁盘一般肯定上秒,搜索性能绝对是秒级别的,1秒、5秒、10秒。但如果是走 filesystem cache,是走纯内存的,那么一般来说性能比走磁盘要高一个数量级,基本上就是毫秒级的,从几毫秒到几百毫秒不等。

这里有个真实的案例。某个公司 es 节点有 3 台机器,每台机器看起来内存很多,64G,总内存就是 64 * 3 = 192G。每台机器给 es jvm heap 是 32G,那么剩下来留给 filesystem cache 的就是每台机器才 32G,总共集群里给 filesystem cache 的就是 32 * 3 = 96G 内存。而此时,整个磁盘上索引数据文件,在 3 台机器上一共占用了 1T 的磁盘容量,es 数据量是 1T,那么每台机器的数据量是 300G。这样性能好吗? filesystem cache 的内存才 100G,十分之一的数据可以放内存,其他的都在磁盘,然后你执行搜索操作,大部分操作都是走磁盘,性能肯定差。

归根结底,你要让 es 性能要好,最佳的情况下,就是你的机器的内存,至少可以容纳你的总数据量的一半。

根据我们自己的生产环境实践经验,最佳的情况下,是仅仅在 es 中就存少量的数据,就是你要用来搜索的那些索引,如果内存留给 filesystem cache 的是 100G,那么你就将索引数据控制在 100G 以内,这样的话,你的数据几乎全部走内存来搜索,性能非常之高,一般可以在 1 秒以内。

比如说你现在有一行数据。id,name,age .... 30 个字段。但是你现在搜索,只需要根据 id,name,age 三个字段来搜索。如果你傻乎乎往 es 里写入一行数据所有的字段,就会导致说 90% 的数据是不用来搜索的,结果硬是占据了 es 机器上的 filesystem cache 的空间,单条数据的数据量越大,就会导致 filesystem cahce 能缓存的数据就越少。其实,仅仅写入 es 中要用来检索的少数几个字段就可以了,比如说就写入 es id,name,age 三个字段,然后你可以把其他的字段数据存在 mysql/hbase 里,我们一般是建议用 es + hbase 这么一个架构。

hbase 的特点是适用于海量数据的在线存储,就是对 hbase 可以写入海量数据,但是不要做复杂的搜索,做很简单的一些根据 id 或者范围进行查询的这么一个操作就可以了。从 es 中根据 name 和 age 去搜索,拿到的结果可能就 20 个 doc id,然后根据 doc id 到 hbase 里去查询每个 doc id 对应的完整的数据,给查出来,再返回给前端。

写入 es 的数据最好小于等于,或者是略微大于 es 的 filesystem cache 的内存容量。然后你从 es 检索可能就花费 20ms,然后再根据 es 返回的 id 去 hbase 里查询,查 20 条数据,可能也就耗费个 30ms,可能你原来那么玩儿,1T 数据都放 es,会每次查询都是 5~10s,现在可能性能就会很高,每次查询就是 50ms。

数据预热

假如说,哪怕是你就按照上述的方案去做了,es 集群中每个机器写入的数据量还是超过了 filesystem cache 一倍,比如说你写入一台机器 60G 数据,结果 filesystem cache 就 30G,还是有 30G 数据留在了磁盘上。

其实可以做数据预热。

举个例子,拿微博来说,你可以把一些大V,平时看的人很多的数据,你自己提前后台搞个系统,每隔一会儿,自己的后台系统去搜索一下热数据,刷到 filesystem cache 里去,后面用户实际上来看这个热数据的时候,他们就是直接从内存里搜索了,很快。

或者是电商,你可以将平时查看最多的一些商品,比如说 iphone 8,热数据提前后台搞个程序,每隔 1 分钟自己主动访问一次,刷到 filesystem cache 里去。

对于那些你觉得比较热的、经常会有人访问的数据,最好做一个专门的缓存预热子系统,就是对热数据每隔一段时间,就提前访问一下,让数据进入 filesystem cache 里面去。这样下次别人访问的时候,性能一定会好很多。

冷热分离

es 可以做类似于 mysql 的水平拆分,就是说将大量的访问很少、频率很低的数据,单独写一个索引,然后将访问很频繁的热数据单独写一个索引。最好是将冷数据写入一个索引中,然后热数据写入另外一个索引中,这样可以确保热数据在被预热之后,尽量都让他们留在 filesystem os cache 里,别让冷数据给冲刷掉。

你看,假设你有 6 台机器,2 个索引,一个放冷数据,一个放热数据,每个索引 3 个 shard。3 台机器放热数据 index,另外 3 台机器放冷数据 index。然后这样的话,你大量的时间是在访问热数据 index,热数据可能就占总数据量的 10%,此时数据量很少,几乎全都保留在 filesystem cache 里面了,就可以确保热数据的访问性能是很高的。但是对于冷数据而言,是在别的 index 里的,跟热数据 index 不在相同的机器上,大家互相之间都没什么联系了。如果有人访问冷数据,可能大量数据是在磁盘上的,此时性能差点,就 10% 的人去访问冷数据,90% 的人在访问热数据,也无所谓了。

document 模型设计

对于 MySQL,我们经常有一些复杂的关联查询。在 es 里该怎么玩儿,es 里面的复杂的关联查询尽量别用,一旦用了性能一般都不太好。

最好是先在 Java 系统里就完成关联,将关联好的数据直接写入 es 中。搜索的时候,就不需要利用 es 的搜索语法来完成 join 之类的关联搜索了。

document 模型设计是非常重要的,很多操作,不要在搜索的时候才想去执行各种复杂的乱七八糟的操作。es 能支持的操作就那么多,不要考虑用 es 做一些它不好操作的事情。如果真的有那种操作,尽量在 document 模型设计的时候,写入的时候就完成。另外对于一些太复杂的操作,比如 join/nested/parent-child 搜索都要尽量避免,性能都很差的。

分页性能优化

es 的分页是较坑的,为啥呢?举个例子吧,假如你每页是 10 条数据,你现在要查询第 100 页,实际上是会把每个 shard 上存储的前 1000 条数据都查到一个协调节点上,如果你有个 5 个 shard,那么就有 5000 条数据,接着协调节点对这 5000 条数据进行一些合并、处理,再获取到最终第 100 页的 10 条数据。

分布式的,你要查第 100 页的 10 条数据,不可能说从 5 个 shard,每个 shard 就查 2 条数据,最后到协调节点合并成 10 条数据吧?你必须得从每个 shard 都查 1000 条数据过来,然后根据你的需求进行排序、筛选等等操作,最后再次分页,拿到里面第 100 页的数据。你翻页的时候,翻的越深,每个 shard 返回的数据就越多,而且协调节点处理的时间越长,非常坑爹。所以用 es 做分页的时候,你会发现越翻到后面,就越是慢。

我们之前也是遇到过这个问题,用 es 作分页,前几页就几十毫秒,翻到 10 页或者几十页的时候,基本上就要 5~10 秒才能查出来一页数据了。

有什么解决方案吗?

不允许深度分页(默认深度分页性能很差)

跟产品经理说,你系统不允许翻那么深的页,默认翻的越深,性能就越差。

类似于 app 里的推荐商品不断下拉出来一页一页的

类似于微博中,下拉刷微博,刷出来一页一页的,你可以用 scroll api,关于如何使用,自行上网搜索。

scroll 会一次性给你生成所有数据的一个快照,然后每次滑动向后翻页就是通过游标 scroll_id 移动,获取下一页下一页这样子,性能会比上面说的那种分页性能要高很多很多,基本上都是毫秒级的。

但是,唯一的一点就是,这个适合于那种类似微博下拉翻页的,不能随意跳到任何一页的场景。也就是说,你不能先进入第 10 页,然后去第 120 页,然后又回到第 58 页,不能随意乱跳页。所以现在很多产品,都是不允许你随意翻页的,app,也有一些网站,做的就是你只能往下拉,一页一页的翻。

初始化时必须指定 scroll 参数,告诉 es 要保存此次搜索的上下文多长时间。你需要确保用户不会持续不断翻页翻几个小时,否则可能因为超时而失败。

除了用 scroll api,你也可以用 search_after 来做,search_after 的思想是使用前一页的结果来帮助检索下一页的数据,显然,这种方式也不允许你随意翻页,你只能一页页往后翻。初始化时,需要使用一个唯一值的字段作为 sort 字段。

关注我的微信公众号,第一时间获得我的博客的更新提醒,更有惊喜等着你哟~

扫一扫下方二维码或搜索微信号shenshan_laoyuan关注

深山老猿

本篇文章由一文多发平台ArtiPub自动发布

本文转载自: 掘金

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

面技术岗位实习生,这些你得了解啊!

发表于 2020-05-16

最近是实习生招聘季节,作为团队的面试官之一,也面试了有接近20个后端的实习生。举一些例子,谈一些个人体会,提一些小建议,望君笑纳。

1.遇到过好多学弟学妹,实习招聘一开始就投递了简历,但迟迟未看到他们进入面试环节,一问才知道,这是打算参加最后一轮笔试,希望可以准备充分一些,说不定网上还有些资料可以放出来呢?

小岑看法: offer名额是有限的,越早参加笔试的,那就肯定是越早能获得被面试的机会,也就可以更早拿到offer名额,越往后的话,不但是offer数量会少,而且随着数量减少,待定选手增多,只能优中再选优,无形当中把自己的游戏难度调到了Hard模式。

小岑建议: 在实习招聘季到来前,就做好面试的准备,全力备战前几轮的笔试,越早肯定是越好,然后尽量走内推通道,增加自己的简历被看到的机会。

2.遇到过几个实习生,意向工作地没有填上海,也不愿接受调配,抱着试一试的态度,给同学打了电话,结果回答是,我这个其实是随便选的,我对上海也是OK的。

小岑看法: 如果面试官没有很留心你的话,可能因为这个城市,就不会关注你的简历了,因为地域不符,也没法把你招过来,可能你无形中就错过了一个机会。

小岑建议: 填写公司的招聘系统时,尽量把必填的,选填的都填写完整,自己的意向工作地,把自己愿意去实习工作的城市都选上,另外通常系统里都会有是否愿意接受调配,一定要考虑清楚选择。

3.看到一些不错的简历,很兴奋,打电话过去,结果是只考虑C++,Java不考虑 或者是 主要做C++,可以转Java,结果Java这块都没怎么复习,自身的计算机基础也不是很扎实,面试结果也不是很理想。

小岑看法: 捞简历阶段,其实是面试官根据自身团队的属性,挑选合适的简历,捞到简历后,通常是根据你简历投递的岗位展开面试。

小岑建议: 标注清楚自己找的是哪种语言的实习生,因为捞简历的部门不一定和你的技术栈是契合的,会增加你和面试官前期的沟通成本。另外提前考虑下有没有跨语言的可能,通常来说一些大厂的主要技术栈,网上都是可以查到相关资料的,如果可以接受跨语言,建议需要提前准备一下,会进行基本的考察。

4.看到一些学生的简历,动辄是精通Java,精通Redis,精通算法,可能是因为确实不知道自己不知道,也可能是因为想展示下自己的竞争力。那么我看到这些精通的,我会认为学生这块掌握的一定很好了,然后可能会针对精通技能问一些深入的问题,但往往大多数同学在涉及到这类问题时,才会表达下,自己其实目前主要在应用阶段。

小岑看法: 针对自己比较熟悉的技能,在简历上增加标注是可行的,但切勿夸大,如果面试官对这块技能考察后,你的回答不是很理想,会非常减分。

小岑建议: 针对自己熟悉的技能,根据真实的情况写熟悉XXX技能及其原理,也可以写目前是学会应用XXX技能,这都是比较合理的写法,不要过度夸大,反而容易给自己的面试阶段埋坑。

5.有个印象比较深刻的沟通例子,就是我和学生沟通可以面试的时间,电话打通的那一刻,语气非常不好,嗓门也很大,背景音也很嘈杂,我都怀疑是平时是不是被骚扰电话打多了,接到陌生电话就很烦躁,知道我是面试的之后,稍微缓和了一些,但感觉还是比较急躁,沟通完时间后,匆忙挂断了电话。

小岑看法: 面试其实从你接听面试官电话的那一刻就开始了。保持一个职业化的沟通,可以给面试官留下比较好的第一印象,相信大家也知道,第一印象在任何场景中都是很重要的。

小岑建议: 不论平时接电话的语气是怎么样的,这段期间接电话,礼貌平静一些。通常面试官会和你沟通一下可以面试的时间,建议报一些可以选择的时段给面试官,然后最好也反问下面试官,这个时间段他是否也方便,多一些换位思考。

6.面试当天准时到场,仪表整洁一些,提前调试好网络和视频清晰度,手机保持免打扰状态。

7.面试过程中 一定会遇到不会的,不会不要紧,但你要根据目前学习到的知识提出你的一些设想,切记不要冷场以及胡乱回答。

8.现在没有听明白的问题,多和面试官确认下问题想问的是什么,回答时不要流水账,思考后,概括一下,有一些层次结构回答,可以看一下金字塔原理这本书。

9.代码环节的话,不要一听完问题就一股脑去写,尽可能多和面试官交流一下题意,明确边界范围,边写的时候也可以边交流。代码的话,不要在主方法里就蒙头写,有一些结构拆分,封装,现场写代码往往不是要你一定写出,更看重的是你写代码过程的习惯和基本功。在远程online coding时,在没有得到面试官的许可时,不要切出屏幕,面试官都会看到的,如果你想ide优化下你的代码,提申请,但最好不要。

10.面试结束后,往往会给同学一些提问的机会,遇到过一些同学准备很充分,问了和个人,公司都相关的一些问题,也有部分同学什么问题也没有就匆匆结束了 面试。

小岑看法: 面试完的提问环节,也很重要,尽可能表达下你对这个目标岗位的疑问和实习的热诚,是你可以再次展示自我,吸引面试官的机会,也可以增加一些印象分数。

小岑建议: 事先准备好一些问题,思考下自己想从实习中获得什么,想在这家公司获得些什么,会更有准备一些。

如果你有一些关于实习的问题,想要咨询,可以加公众号 程序员小岑成长记,我会尽快回复你的。

本文转载自: 掘金

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

SpringDataJpa中的复杂查询和动态查询,多表查询。

发表于 2020-05-15

在前面的章节已经讲述了SpringDataJpa的CRUD操作以及其底层代理实现的分析,下面介绍SpringDataJpa中的复杂查询和动态查询,多表查询。(保姆级教程)

文章字数较多,请各位按需阅读。

不清楚JPA的小伙伴可以参考这篇文章:JPA简介;

不清楚SpringDataJPA环境搭建的小伙伴可以参考这篇文章:SpringDataJPA入门案例;

想了解SpringDataJPA代理类实现过程可以参考这篇文章:SpringDadaJPA底层实现原理

如需转载,请注明出处。

1.复杂查询

i.方法名称规则查询

方法名查询:只需要按照SpringDataJpa提供的方法名称规则去定义方法,在dao接口中定义方法即可。

其中对于方法的名称有一套约定。

KeyWord Sample JPQL
And findByLastnameAndFirstname where x.lastname = ?1 and x.firstname = ?2
Or findByLastnameOrFirstname where x.lastname = ?1 or x.firstname = ?2
Between findByAgeBetween where x.Age between ?1 and ?2
LessThan findByAgeLessThan where x.age < ?1
GreaterThan findByAgeGreaterThan where x.age > ?1
Like findByFirstnameLike where x.firstname like ?1
NotLike findByFirstnameNotLike where x.firstname not like ?1
TRUE findByActiveTrue() where x.active = true
FALSE findByActiveFalse() where x.active = false
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
复制代码public interface CustomerDao extends JpaRepository<Customer,Long>, JpaSpecificationExecutor<Customer> {
/**
* 方法名的约定:
* findBy:查询
* 对象中的属性名(首字母大写):查询条件
* *默认情况:使用 =的方式查询
* 特殊的查询方式,比如模糊查询
* findByCustName-----根据客户名称查询 findBy表示要查询 CustName属性名
* springDataJpa在运行阶段
* 会根据方法名称进行解析 findBy from XXX(实体类)
* 属性名称 where custName
* 1. findBy+属性名称(根据属性名称进行完成匹配任务)
* 2. findBy+属性名称+查询方式(Like|isnull)
* 3. 多条件查询
* findBy+属性名称+查询方式+多条件连接符(and|or)+属性名+查询方式
*/
public List<Customer> findByCustName(String name);
//查询id为3且name中含有大学的用户
public Customer findByCustId(Long id);
public Customer findByCustIdAndCustNameLike(Long id,String name);
}

ii.JPQL查询

使用 Spring Data JPA 提供的查询方法已经可以解决大部分的应用场景,但是对于某些业务来
说,我们还需要灵活的构造查询条件,这时就可以使用@Query 注解,结合 JPQL 的语句方式完成
查询 。

@Query 注解的使用非常简单,只需在方法上面标注该注解,同时提供一个 JPQL 查询语句即可

注意:

通过使用 @Query 来执行一个更新操作,为此,我们需要在使用 @Query 的同时,用 @Modifying 来将该操作标识为修改查询,这样框架最终会生成一个更新的操作,而非查询 。

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
复制代码public interface CustomerDao extends JpaRepository<Customer,Long>, JpaSpecificationExecutor<Customer> {
/**
* 1.根据客户名称查询客户
* jpql:from Customer where custName=?
*/
@Query(value="from Customer where custName =?")
public List<Customer> findCustomerJpql(String name);
/**
* 2.根据客户名称和客户id查询
* 对多个占位符参数
* 默认情况下,占位符的位置需要和方法参数中的位置保持一致
* 也可以指定占位符参数的位置(注意:中间不要有空格)
* ? 索引的方式,指定此占位符的取值来源 eg ?2表示此占位符对应第二个参数
*/
@Query(value="from Customer where custName=?2 and custId=?1")
public Customer findByNameAndId(Long id,String name);
/**
* 3.根据id更新客户的name
* sql:update cst_customer set cust_name=? where cust_id=?
* jpql:update Customer set custName=? where custId=?
*
* @query:代表的是进行查询
* 需要声明此方法是执行更新操作
* 使用 @Modifying
*/
@Query(value = "update Customer set custName=? where custId=?")
@Modifying
public void updateCustomerName(String name,Long id);
}

注意:在执行springDataJpa中使用jpql完成更新,删除操作时,需要手动添加事务的支持 必须的;因为默认会执行结束后,回滚事务。

1
2
3
4
5
6
复制代码 @Test
@Transactional//添加事务的支持
@Rollback(value = false)
public void updateCustomerName(){
customerDao.updateCustomerName("学生公寓",4L);
}

iii.SQL查询

Spring Data JPA 同样也支持 sql 语句的查询,如下:

1
2
3
4
5
6
7
8
9
10
复制代码/**
* 查询所有用户:使用sql查询
* Sql:select * from cst_customer
* nativeQuery = true配置查询方式,true表示Sql查询,false表示Jpql查询
* 注意:返回值是一个Object[]类型的list
*/
// @Query(value = "select * from cst_customer",nativeQuery = true)
// public List<Object []>findSql();
@Query(value = "select * from cst_customer where cust_name like ?",nativeQuery = true)
public List<Object []>findSql(String name);

2.动态查询

springdatajpa的接口规范:

  • JpaRepository<操作的实体类型,实体类型中的 主键 属性的类型>

封装了基本的CRUD的操作,分页等;

  • JpaSpecificationExecutor<操作的实体类类型>

封装了复杂查询。

上述查询方法使用到的是接口JpaRepository中的方法,下面分析JpaSpecificationExecutor中的方法。

i.为什么需要动态查询

可能有些许疑惑,为什么还需要动态查询呢?有时候我们在查询某个实体的时候哦,给定的查询条件不是固定的,这个时候就需要动态构建相应的查询语句,可以理解为上述的查询条件是定义在dao接口中的,而动态查询条件定义在实现类中。

ii.JpaSpecificationExecutor中定义的方法

1
2
3
4
5
6
7
8
9
10
11
复制代码public interface JpaSpecificationExecutor<T> {
T findOne(Specification<T> var1);

List<T> findAll(Specification<T> var1);

Page<T> findAll(Specification<T> var1, Pageable var2);

List<T> findAll(Specification<T> var1, Sort var2);

long count(Specification<T> var1);
}

在上述方法中,我们可以看到接口Specification。可以简单理解为,Specification构造的就是查询条件。我们看看Specification中定义的方法。

1
2
3
4
5
6
7
8
复制代码/*
* root :T表示查询对象的类型,代表查询的根对象,可以通过root获取实体中的属性
* query :代表一个顶层查询对象,用来自定义查询
* cb :用来构建查询,此对象里有很多条件方法
**/
public interface Specification<T> {
Predicate toPredicate(Root<T> root, CriteriaQuery<?> query, CriteriaBuilder cb);
}

与上述查询方法不同,复杂查询定义在dao接口中,而动态查询定义在实现类中。

1)单条件查询

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
复制代码@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration("classpath:applicationContext.xml")
public class SpecTest {
@Autowired
private CustomerDao customerDao;
@Test
public void conditionTest(){
/**
* 自定义查询条件
* 1.实现Specification接口(提供泛型:查询对象类型,需要那个对象就写哪个泛型)
* 2.实现toPredicate方法(构造查询条件)
* 3.需要借书方法参数中的两个形参
* root:用于获取查询的对象属性
* CriteriaBuilder:构造查询条件,内部封装了很多的查询条件(例如:模糊匹配,精准匹配)
* 需求:根据客户名称查询,查询客户名称为大学
* 查询条件
* 1.查询方法 (精准匹配,是否为空...)
* CriteriaBuilder对象
* 2.比较的属性名称(与哪个字段去以什么方式去比较)
* root对象
*/

Specification<Customer> spec=new Specification<Customer>() {
@Override
public Predicate toPredicate(Root<Customer> root, CriteriaQuery<?> criteriaQuery, CriteriaBuilder cb) {
//1.获取比较的属性(不是字段名)
Path<Object> custName = root.get("custName");
//2.构造查询条件
/**
* 第一个参数:需要比较的属性(Path)
* 第二个参数:当前比较的取值
*/
Predicate predicate = cb.equal(custName, "三峡大学");//进行精准匹配 (比较的属性,比较的属性的取值)
return predicate;
}
};
//根据返回的对象个数选择findOne或者findAll
Customer customer = customerDao.findOne(spec);
System.out.println(customer);
}
}

2)多条件查询

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
复制代码@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration("classpath:applicationContext.xml")
public class SpecTest {
@Autowired
private CustomerDao customerDao;
/**
* 多条件查询:根据用户名和所属行业进行查询
* root:获取属性
* 用户名
* 所属行业
* cb:构造查询
* 1.构造客户名的精准匹配查询
* 2.构造所属行业的精准匹配查询
* 3,将以上两个查询联系起来
*/
@Test
public void findByNmaeAndIndustray(){
Specification<Customer> spec=new Specification<Customer>() {
@Override
public Predicate toPredicate(Root<Customer> root, CriteriaQuery<?> criteriaQuery, CriteriaBuilder cb) {
//1.获取属性
Path<Object> custName = root.get("custName");
Path<Object> industry = root.get("custIndustry");
//2.构造查询
Predicate p1 = cb.equal(custName, "6测试数据-coderxz");
Predicate p2 = cb.equal(industry, "6测试数据-java工程师");
//3。将多个查询条件组合到一起(and/or)
Predicate predicate = cb.and(p1, p2);
return predicate;
}
};
Customer customer = customerDao.findOne(spec);
System.out.println(customer);
}
}

3)模糊查询

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
复制代码@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration("classpath:applicationContext.xml")
public class SpecTest {
@Autowired
private CustomerDao customerDao;
/**
* 案例:根据客户名称进行模糊配置,返回客户列表
*
* equal:直接的path对象(属性),然后直接进行比较即可
*
* 对于gt,lt,le,like:得到path对象,根据path对象指定比较参数的类型(字符串or数字...),再进行比较
* 指定参数类型 path.as(类型的字节码对象)
*/
@Test
public void findVagueCustomer(){
Specification<Customer>spec=new Specification<Customer>() {
@Override
public Predicate toPredicate(Root<Customer> root, CriteriaQuery<?> criteriaQuery, CriteriaBuilder criteriaBuilder) {
Path<Object> custName = root.get("custName");
Predicate predicate = criteriaBuilder.like(custName.as(String.class), "%大学%");
return predicate;
}
};
List<Customer> customers = customerDao.findAll(spec);
for(Customer c:customers){
System.out.println(c);
}
}
}

4)分页查询

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
复制代码@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration("classpath:applicationContext.xml")
public class SpecTest {
@Autowired
private CustomerDao customerDao;
/**
* 分页查询
* findAll(Pageable) 没有条件的分页查询
* findAll(Specification,Pageable)
* Specification查询条件
* Pageable分页参数 查询的页码,每页查询的条件
* 返回:Pahe(StringDataJpa)为我们封装好的pageBean对象,数据列表,
*/
@Test
public void pageCustomer(){
Specification<Customer> spec=new Specification<Customer>() {
@Override
public Predicate toPredicate(Root<Customer> root, CriteriaQuery<?> criteriaQuery, CriteriaBuilder criteriaBuilder) {
return null;
}
};
/**
* Pageable 接口
* PageRequest是其实现类
* 第一个参数:当前查询的页数(从0开始)
* 第二个参数:每页查询的数量
* 注意:在新版本的jpa中,此方法已过时,新方法是PageRequest.of(page,size)
*/
Pageable pageable = new PageRequest(0,1);
//分页查询 page是SpringDataJpa为我们封装的一个JavaBean
Page<Customer> page = customerDao.findAll(spec, pageable);
//获得总页数(这些数据需要分几页)
System.out.println("查询总页数:"+page.getTotalPages());
//获得总记录数(数据库的总记录数)
System.out.println("查询总记录数:"+page.getTotalElements());
//得到数据集合列表
System.out.println("数据集合列表:"+page.getContent());
}
}

5)对查询结果进行排序

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
复制代码@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration("classpath:applicationContext.xml")
public class SpecTest {
@Autowired
private CustomerDao customerDao;
/**
* 对查询结果进行排序
*/
@Test
public void findSortCustomer(){
Specification<Customer>spec=new Specification<Customer>() {
@Override
public Predicate toPredicate(Root<Customer> root, CriteriaQuery<?> criteriaQuery, CriteriaBuilder criteriaBuilder) {
Path<Object> custName = root.get("custName");
Predicate predicate = criteriaBuilder.like(custName.as(String.class), "%大学%");
return predicate;
}
};
/**
*创建排序对象,需要调用构造方法实例化对象
* 第一个参数:排序的顺序(正序,倒序)
* sort.Direction.DESC:倒序
* sort.Direction.ASC:升序
* 第二个参数:排序的属性名称
*/
Sort sort = new Sort(Sort.Direction.DESC, "custId");
List<Customer> customers = customerDao.findAll(spec,sort);
for(Customer c:customers){
System.out.println(c);
}
}
}

3.多表查询

上述复杂查询和动态查询都是基于单表查询,只需要指定实体类与数据库表中一对一的映射。而多表查询需要修改实体类之间的映射关系。

在数据库中表与表之间,存在三种关系:多对多、一对多、一对一。

多表查询01

那么与之对应的实体映射也应该有三种关系。那么在JPA中表的关系如何分析呢?

1.建立表与表之间的关系

  • 第一步:首先确定两张表之间的关系。
    如果关系确定错了,后面做的所有操作就都不可能正确。
  • 第二步:在数据库中实现两张表的关系
  • 第三步:在实体类中描述出两个实体的关系
  • 第四步:配置出实体类和数据库表的关系映射(重点)

4.JPA中的一对多

案例分析:

采用两个实体对象:公司与员工

在不考虑兼职的情况下,每名员工对应一家公司,每家公司有多名员工。

在一对多关系中,我们习惯把一的一方称之为主表,把多的一方称之为从表。在数据库中建立一对
多的关系,需要使用数据库的外键约束。

**什么是外键?**指的是从表中有一列,取值参照主表中的主键,这一列就是外键。

springdatajpa进阶01

数据库表:

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
复制代码CREATE TABLE `cst_customer` (
`cust_id` bigint(20) NOT NULL AUTO_INCREMENT,
`cust_address` varchar(255) DEFAULT NULL,
`cust_industry` varchar(255) DEFAULT NULL,
`cust_level` varchar(255) DEFAULT NULL,
`cust_name` varchar(255) DEFAULT NULL,
`cust_phone` varchar(255) DEFAULT NULL,
`cust_source` varchar(255) DEFAULT NULL,
PRIMARY KEY (`cust_id`)
) ENGINE=InnoDB AUTO_INCREMENT=26 DEFAULT CHARSET=utf8;

CREATE TABLE `cst_linkman` (
`lkm_id` bigint(20) NOT NULL AUTO_INCREMENT,
`lkm_email` varchar(255) DEFAULT NULL,
`lkm_gender` varchar(255) DEFAULT NULL,
`lkm_memo` varchar(255) DEFAULT NULL,
`lkm_mobile` varchar(255) DEFAULT NULL,
`lkm_name` varchar(255) DEFAULT NULL,
`lkm_phone` varchar(255) DEFAULT NULL,
`lkm_position` varchar(255) DEFAULT NULL,
`lkm_cust_id` bigint(20) DEFAULT NULL,
PRIMARY KEY (`lkm_id`),
KEY `FKh9yp1nql5227xxcopuxqx2e7q` (`lkm_cust_id`),
CONSTRAINT `FKh9yp1nql5227xxcopuxqx2e7q` FOREIGN KEY (`lkm_cust_id`) REFERENCES `cst_customer` (`cust_id`)
) ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8;

1.建立实体与表之间的映射关系

注意:使用的注解都是JPA规范的,导包需要导入javac.persistence下的包

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
复制代码package ctgu.pojo;
import javax.persistence.*;
import java.util.HashSet;
import java.util.Set;

/**
*我们需要配置:
* 1.实体类与表的映射关系(此pojo与数据库中的那一张表关系映射)
* @ Entity
* @ Table(name="cst_customer")name表示数据库中表的名称
* 2.实体类中属性与表中字段的映射关系
* @ Id声明主键的设置
* @ GeneratedValue配置主键是生成策略(自动增长)
* strategy=
* GenerationType.IDENTITY:自增 Mysql(底层数据库支持的自增长方式对id自增)
* GenerationType.SEQUENCE:序列 Oracle(底层数据库必须支持序列)
* GenerationType.TABLE:jpa提供的一种机制,通过一张数据库表的形式帮助我们完成自增
* GenerationType.AUTO:有程序自动的帮助我们选择主键生成策略
* @ Column(name = "cust_id")数据库中表中字段的名字
*/
@Entity
@Table(name = "cst_customer")
public class Customer {
/**
* @ Id声明主键的设置
* @ GeneratedValue配置主键是生成策略(自动增长)
* GenerationType.IDENTITY
* @ Column(name = "cust_id")数据库中表中字段的名字
*/
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "cust_id")
private Long custId;
@Column(name = "cust_name")
private String custName;
@Column(name = "cust_source")
private String custSource;
@Column(name = "cust_industry")
private String custIndustry;
@Column(name = "cust_level")
private String custLevel;
@Column(name = "cust_address")
private String custAddress;
@Column(name = "cust_phone")
private String custPhone;
/**
* 配置客户与联系人之间的关系(一个客户对应多个联系人)
* 使用注解的形式配置多表关系
* 1 声明关系
* @ OnetoMany:配置一对多关系
* targetEntity:对方对象的字节码对象
* 2.配置外键(中间表)
* @ JoinColumn
* name:外键的在从表的字段名称(不是属性,是数据库的字段名称)
* referencedColumnName:参照的主表的字段名称
*/
@OneToMany(targetEntity = LinkMan.class)
@JoinColumn(name = "lkm_cust_id",referencedColumnName = "cust_id")
private Set<LinkMan> linkMans=new HashSet<>();
/*
get/set/toString()方法略......
*/
}
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
复制代码package ctgu.pojo;
import javax.persistence.*;
@Entity
@Table(name="cst_linkman")
public class LinkMan {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name="lkm_id")
private Long lkmId;
@Column(name="lkm_name")
private String lkmName;
@Column(name="lkm_gender")
private String lkmGender;
@Column(name="lkm_phone")
private String lkmPhone;
@Column(name="lkm_mobile")
private String lkmMobile;
@Column(name="lkm_email")
private String lkmEmail;
@Column(name="lkm_position")
private String lkmPosition;
@Column(name="lkm_memo")
private String lkmMemo;
/**
* 配置联系人到客户的多对一关系
* 外键字段是设置在从表中的,且该字段并未作为对象的属性去配置,而实作为外键去配置
*
* 使用注解的形式配置多对一关系
* 1.配置表关系
* @ManyToOne : 配置多对一关系
* targetEntity:对方的实体类字节码
* 2.配置外键(中间表)
*
* * 配置外键的过程,配置到了多的一方,就会在多的一方维护外键
*
*/
@ManyToOne(targetEntity = Customer.class,fetch = FetchType.LAZY)
@JoinColumn(name = "lkm_cust_id",referencedColumnName = "cust_id")
private Customer customer;
/*
get/set/toString略...
*/
}

注意:在上述实体中,均对外键进行了维护。

2.映射的注解说明

i.@OneToMany

作用:建立一对多的关系映射
属性:

  • targetEntityClass:指定多的多方的类的字节码(常用)
  • mappedBy:指定从表实体类中引用主表对象的名称。(常用)
  • cascade:指定要使用的级联操作
  • fetch:指定是否采用延迟加载
  • orphanRemoval:是否使用孤儿删除

ii.@ManyToOne

作用:建立多对一的关系
属性:

  • targetEntityClass:指定一的一方实体类字节码(常用)
  • cascade:指定要使用的级联操作
  • fetch:指定是否采用延迟加载
  • optional:关联是否可选。如果设置为 false,则必须始终存在非空关系。

iii.@JoinColumn

作用:用于定义主键字段和外键字段的对应关系。
属性:

  • name:指定外键字段的名称(常用)
  • referencedColumnName:指定引用主表的主键字段名称(常用)
  • unique:是否唯一。默认值不唯一
  • nullable:是否允许为空。默认值允许。
  • insertable:是否允许插入。默认值允许。
  • updatable:是否允许更新。默认值允许。
  • columnDefinition:列的定义信息。

3.一对多测试

i.保存公司和联系人

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
复制代码package ctgu.OntoMany;

import ctgu.dao.CustomerDao;
import ctgu.dao.LinkManDao;
import ctgu.pojo.Customer;
import ctgu.pojo.LinkMan;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.annotation.Rollback;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.transaction.annotation.Transactional;

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration("classpath:applicationContext.xml")
public class OntoManyTest {
@Autowired
private CustomerDao customerDao;
@Autowired
private LinkManDao linkManDao;
/**
* 保存一个客户,保存一个联系人
* 现象:从表(联系人)的外键为空
* 原因:
* 主表中没有配置关系
*/
@Test
@Transactional
@Rollback(value = false)
public void addTest(){
Customer customer = new Customer();
LinkMan linkMan = new LinkMan();
customer.setCustName("TBD云集中心");
customer.setCustLevel("VIP客户");
customer.setCustSource("网络");
customer.setCustIndustry("商业办公");
customer.setCustAddress("昌平区北七家镇");
customer.setCustPhone("010-84389340");

linkMan.setLkmName("小明");
linkMan.setLkmGender("male");
linkMan.setLkmMobile("13811111111");
linkMan.setLkmPhone("010-34785348");
linkMan.setLkmEmail("123456@qq.com");
linkMan.setLkmPosition("老师");
linkMan.setLkmMemo("还行吧");
/**
* 配置了客户到联系人的关系
* 从客户的角度上,发送了两条insert语句,发送一条更新语句更新数据库(更新从表中的外键值)
* 由于我们配置了客户到联系人的关系,客户可以对外键进行维护
*/

linkMan.setCustomer(customer);
//此添加可以不写会
customer.getLinkMans().add(linkMan);
customerDao.save(customer);
linkManDao.save(linkMan);
}
}

运行结果:

1
2
3
复制代码Hibernate: insert into cst_customer (cust_address, cust_industry, cust_level, cust_name, cust_phone, cust_source) values (?, ?, ?, ?, ?, ?)
Hibernate: insert into cst_linkman (lkm_cust_id, lkm_email, lkm_gender, lkm_memo, lkm_mobile, lkm_name, lkm_phone, lkm_position) values (?, ?, ?, ?, ?, ?, ?, ?)
Hibernate: update cst_linkman set lkm_cust_id=? where lkm_id=?

分析:

执行了两条insert语句以及一条update语句,有一条update的语句是多余的。产生这种现象的原因是:我们在两个实体类中均对外键进行了维护,相当于维护了两次,解决的办法是放弃一方的维权。

修改:将主表中的关系映射修改为:

1
2
复制代码 @OneToMany(mappedBy = "customer",cascade = CascadeType.ALL,fetch = FetchType.EAGER)
private Set<LinkMan> linkMans=new HashSet<>();

ii.级联添加

级联操作:操作一个对象同时操作它的关联对象

使用方法:只需要在操作主体的注解上配置casade

1
2
3
4
5
6
7
8
9
10
11
复制代码 /**
* 放弃外键维护权:我的一对多映射参照对方的属性就可以了
* mappedBy:对方维护关系的属性名称
* cascade = CascadeType.ALL 进行级联操作,all表示级联所有(insert,delete,update)
* .merge 更新
* .persist保存
* .remove 删除
* fetch 配置延迟加载
*/
@OneToMany(mappedBy = "customer",cascade = CascadeType.ALL,fetch = FetchType.EAGER)
private Set<LinkMan> linkMans=new HashSet<>()

一般是对配置在主表中,但是:注意:慎用CascadeType.ALL

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
复制代码 package ctgu.OntoMany;

import ctgu.dao.CustomerDao;
import ctgu.dao.LinkManDao;
import ctgu.pojo.Customer;
import ctgu.pojo.LinkMan;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.annotation.Rollback;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.transaction.annotation.Transactional;

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration("classpath:applicationContext.xml")
public class OntoManyTest {
@Autowired
private CustomerDao customerDao;
@Autowired
private LinkManDao linkManDao;
/**
* 级联添加:
* 保存一个客户的同时,保存客户的所有联系人
* 需要在操作主题的实体类上,配置casache属性
*/
@Test
@Transactional
@Rollback(value = false)
public void cascadeAdd(){
Customer customer = new Customer();
LinkMan linkMan = new LinkMan();
customer.setCustName("测试公司1");
linkMan.setLkmName("测试员工张三1");
//注意此处添加
linkMan.setCustomer(customer);
customer.getLinkMans().add(linkMan);

customerDao.save(customer);
}
}

测试结果:

1
2
复制代码Hibernate: insert into cst_customer (cust_address, cust_industry, cust_level, cust_name, cust_phone, cust_source) values (?, ?, ?, ?, ?, ?)
Hibernate: insert into cst_linkman (lkm_cust_id, lkm_email, lkm_gender, lkm_memo, lkm_mobile, lkm_name, lkm_phone, lkm_position) values (?, ?, ?, ?, ?, ?, ?, ?)

iii.级联删除

删除公司的同时,删除对应公司的所有员工。

JPA中删除是先执行查询再执行删除。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
复制代码 /**
* 级联删除:删除1号客户的同时,删除1号客户的所有联系人
* 1.需要区分操作主体(你对那个对象进行操作)
* 2.需要在操作主体的实体类上,添加级联属性(需要添加到多表映射关系的注解上)
* 3.cascade(配置级联)
*/
@Test
@Transactional
@Rollback(value = false)
public void cascadeDelete(){

// Customer customer = customerDao.findOne(1L);
customerDao.delete(40L);
}

测试结果:

1
2
3
4
复制代码Hibernate: select customer0_.cust_id as cust_id1_0_0_, customer0_.cust_address as cust_add2_0_0_, customer0_.cust_industry as cust_ind3_0_0_, customer0_.cust_level as cust_lev4_0_0_, customer0_.cust_name as cust_nam5_0_0_, customer0_.cust_phone as cust_pho6_0_0_, customer0_.cust_source as cust_sou7_0_0_, linkmans1_.lkm_cust_id as lkm_cust9_1_1_, linkmans1_.lkm_id as lkm_id1_1_1_, linkmans1_.lkm_id as lkm_id1_1_2_, linkmans1_.lkm_cust_id as lkm_cust9_1_2_, linkmans1_.lkm_email as lkm_emai2_1_2_, linkmans1_.lkm_gender as lkm_gend3_1_2_, linkmans1_.lkm_memo as lkm_memo4_1_2_, linkmans1_.lkm_mobile as lkm_mobi5_1_2_, linkmans1_.lkm_name as lkm_name6_1_2_, linkmans1_.lkm_phone as lkm_phon7_1_2_, linkmans1_.lkm_position as lkm_posi8_1_2_ from cst_customer customer0_ left outer join cst_linkman linkmans1_ on customer0_.cust_id=linkmans1_.lkm_cust_id where customer0_.cust_id=?
Hibernate: delete from cst_linkman where lkm_id=?
Hibernate: delete from cst_linkman where lkm_id=?
Hibernate: delete from cst_customer where cust_id=?

注意:一般使用级联删除是比较危险的,在一对多的情况下。如果没有使用级联操作,应该如何删除数据?

只删除从表数据:可以任意删除。

删除主表数据:

  • 有从表数据
    1. 在默认情况下,会将外键字段置为null,然后再执行删除。此时如果从表的结构上,外键字段存在非空约束将会报错。
    2. 使用级联删除。
    3. 应该先根据外键值,删除从表中的数据,再删除主表中的数据。
  • 没有从表数据:随便删

iv.一对多删除(非级联删除)

创建方法:根据customer删除员工。(使用复杂查询中的自定义方法)

1
2
3
4
5
6
7
8
9
10
11
复制代码package ctgu.dao;

import ctgu.pojo.Customer;
import ctgu.pojo.LinkMan;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;

public interface LinkManDao extends JpaRepository<LinkMan,Long>, JpaSpecificationExecutor<LinkMan> {
//根据外键值进行删除
public void deleteByCustomer(Customer customer);
}

此时的主表的关键映射为设置级联操作:

1
2
复制代码    @OneToMany(mappedBy = "customer",fetch = FetchType.EAGER)
private Set<LinkMan> linkMans=new HashSet<>();

测试:

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
复制代码 package ctgu.OntoMany;

import ctgu.dao.CustomerDao;
import ctgu.dao.LinkManDao;
import ctgu.pojo.Customer;
import ctgu.pojo.LinkMan;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.annotation.Rollback;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.transaction.annotation.Transactional;

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration("classpath:applicationContext.xml")
public class OntoManyTest {
@Autowired
private CustomerDao customerDao;
@Autowired
private LinkManDao linkManDao;
@Test
@Transactional
@Rollback(value = false)
public void cascadeDelete(){
Customer customer = customerDao.findOne(47L);
linkManDao.deleteByCustomer(customer);
customerDao.delete(47L);
}
}

测试结果:

1
2
3
4
复制代码Hibernate: select linkman0_.lkm_id as lkm_id1_1_, linkman0_.lkm_cust_id as lkm_cust9_1_, linkman0_.lkm_email as lkm_emai2_1_, linkman0_.lkm_gender as lkm_gend3_1_, linkman0_.lkm_memo as lkm_memo4_1_, linkman0_.lkm_mobile as lkm_mobi5_1_, linkman0_.lkm_name as lkm_name6_1_, linkman0_.lkm_phone as lkm_phon7_1_, linkman0_.lkm_position as lkm_posi8_1_ from cst_linkman linkman0_ left outer join cst_customer customer1_ on linkman0_.lkm_cust_id=customer1_.cust_id where customer1_.cust_id=?
Hibernate: delete from cst_linkman where lkm_id=?
Hibernate: delete from cst_linkman where lkm_id=?
Hibernate: delete from cst_customer where cust_id=?

5.JPA中的多对多

案例:用户和角色。

用户:指社会上的某个人。

角色:指人们可能有多种身份信息

比如说:小明有多种身份,即使java工程师,还是后端攻城狮,也是CEO;而Java工程师除了小明,还有张三、李四等等。

所以我们说,用户和角色之间的关系是多对多。

springdatajpa进阶02

1.建立实体类与表直接的关系映射

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
复制代码package ctgu.pojo;

import javax.persistence.*;
import java.util.HashSet;
import java.util.Set;

@Entity
@Table(name = "sys_user")
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name="user_id")
private Long userId;
@Column(name="user_name")
private String userName;
@Column(name="age")
private Integer age;
/**
* 配置用户到角色的 多对多 关系
* 配置多对多的映射关系
* 1.声明表关系的配置
* @ManyToMany()
* targetEntity = Role.class声明对方的实体类字节码
* 2.配置中间表(两个外键)
* @JoinTable
* name :中间表的名称
* joinColumns,当前对象在中间表的位置
* @JoinColumn
* name:外键在中间表的字段名称
* referencedColumnName:参照的主表的主键名称
* inverseJoinColumns,对方对象在中间表的位置
*/
// @ManyToMany(targetEntity = Role.class,cascade = CascadeType.ALL)
@ManyToMany(targetEntity = Role.class)
@JoinTable(name = "sys_user_role",
//joinColumns,当前对象在中间表的位置
joinColumns = {@JoinColumn(name = "sys_user_id",referencedColumnName = "user_id")},
//inverseJoinColumns,对方对象在中间表的位置
inverseJoinColumns = {@JoinColumn(name = "sys_role_id",referencedColumnName = "role_id")}
)
private Set<Role> roles = new HashSet<>();

public Long getUserId() {
return userId;
}
public void setUserId(Long userId) {
this.userId = userId;
}
public String getUserName() {
return userName;
}
public void setUserName(String userName) {
this.userName = userName;
}
public Integer getAge() {
return age;
}
public void setAge(Integer age) {
this.age = age;
}
public Set<Role> getRoles() {
return roles;
}
public void setRoles(Set<Role> roles) {
this.roles = roles;
}
}
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
复制代码package ctgu.pojo;

import javax.persistence.*;
import java.util.HashSet;
import java.util.Set;

@Entity
@Table(name = "sys_role")
public class Role {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "role_id")
private Long roleId;
@Column(name = "role_name")
private String roleName;
@ManyToMany(targetEntity = User.class)
@JoinTable(name = "sys_user_role",
//joinColumns,当前对象在中间表的位置
joinColumns = {@JoinColumn(name = "sys_role_id",referencedColumnName = "role_id")},
//inverseJoinColumns,对方对象在中间表的位置
inverseJoinColumns ={@JoinColumn(name = "sys_user_id",referencedColumnName = "user_id")}
)
//@ManyToMany(mappedBy="roles")应该有一方放弃维护
private Set<User> users = new HashSet<>();
public Long getRoleId() {
return roleId;
}
public void setRoleId(Long roleId) {
this.roleId = roleId;
}
public String getRoleName() {
return roleName;
}
public void setRoleName(String roleName) {
this.roleName = roleName;
}
public Set<User> getUsers() {
return users;
}
public void setUsers(Set<User> users) {
this.users = users;
}
}

2,映射注解说明

i.@ManyToMany

作用:用于映射多对多关系
属性:

  • cascade:配置级联操作。
  • fetch:配置是否采用延迟加载。
  • targetEntity:配置目标的实体类。映射多对多的时候不用写。
  • mappedBy:指定从表实体类中引用主表对象的名称。(常用)

ii.@JoinTable

作用:针对中间表的配置
属性:

  • nam:配置中间表的名称
  • joinColumns:中间表的外键字段关联当前实体类所对应表的主键字段
  • inverseJoinColumn:中间表的外键字段关联对方表的主键字段

iii.@JoinColumn

作用:用于定义主键字段和外键字段的对应关系。
属性:

  • name:指定外键字段的名称
  • referencedColumnName:指定引用主表的主键字段名称
  • unique:是否唯一。默认值不唯一
  • nullable:是否允许为空。默认值允许。
  • insertable:是否允许插入。默认值允许。
  • updatable:是否允许更新。默认值允许。
  • columnDefinition:列的定义信息。

3.多对多测试

i.保存用户和角色

数据库表:(其实可以直接由springdataJPA自动生成)

1
2
3
4
5
6
7
8
9
10
11
12
复制代码CREATE TABLE `sys_user` (
`user_id` bigint(20) NOT NULL AUTO_INCREMENT,
`age` int(11) DEFAULT NULL,
`user_name` varchar(255) DEFAULT NULL,
PRIMARY KEY (`user_id`)
) ENGINE=InnoDB AUTO_INCREMENT=16 DEFAULT CHARSET=utf8;

CREATE TABLE `sys_role` (
`role_id` bigint(20) NOT NULL AUTO_INCREMENT,
`role_name` varchar(255) DEFAULT NULL,
PRIMARY KEY (`role_id`)
) ENGINE=InnoDB AUTO_INCREMENT=16 DEFAULT CHARSET=utf8;

dao接口:

1
2
3
4
5
6
7
8
复制代码package ctgu.dao;

import ctgu.pojo.Role;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;

public interface RoleDao extends JpaRepository<Role,Long>, JpaSpecificationExecutor<Role> {
}
1
2
3
4
5
6
7
8
复制代码package ctgu.dao;

import ctgu.pojo.User;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;

public interface UserDao extends JpaRepository<User,Long>, JpaSpecificationExecutor<User> {
}

测试案例:

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
复制代码package ctgu;
import ctgu.dao.RoleDao;
import ctgu.dao.UserDao;
import ctgu.pojo.Role;
import ctgu.pojo.User;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.annotation.Rollback;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.transaction.annotation.Transactional;

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration("classpath:applicationContext.xml")
public class ManyToMany {
@Autowired
private UserDao userDao;
@Autowired
private RoleDao roleDao;

/**
* 保存一个用户,保存一个角色
* 多对多放弃维护权:
* 被动的一方放弃,谁被选择谁放弃
*/
@Test
@Transactional
@Rollback(false)
public void addUserAndRole(){
User user = new User();
Role role1 = new Role();
Role role2 = new Role();
Role role3 = new Role();
user.setUserName("李大明");
role1.setRoleName("后端攻城狮");
role2.setRoleName("java程序员");
role3.setRoleName("CEO");
//用户和角色都可以对中间表进行维护,添加两次就重复了
//配置角色到用户的关系,可以对中间表中的数据进行维护
role1.getUsers().add(user);
role2.getUsers().add(user);
role3.getUsers().add(user);
//配置用户到角色的关系,
user.getRoles().add(role1);
user.getRoles().add(role2);
user.getRoles().add(role3);
userDao.save(user);
roleDao.save(role1);
roleDao.save(role2);
roleDao.save(role3);
}
}

测试结果:

1
复制代码org.springframework.dao.DataIntegrityViolationException: could not execute statement; SQL [n/a]; constraint [null]; nested exception is org.hibernate.exception.ConstraintViolationException: could not execute statement

原因:

在多对多(保存)中,如果双向都设置关系,意味着双方都维护中间表,都会往中间表插入数据,
中间表的 2 个字段又作为联合主键,所以报错,主键重复,解决保存失败的问题:只需要在任意一
方放弃对中间表的维护权即可,推荐在被动的一方放弃,配置如下:

1
2
3
复制代码//放弃对中间表的维护权,解决保存中主键冲突的问题
@ManyToMany(mappedBy="roles")
private Set<SysUser> users = new HashSet<SysUser>(0);

正确结果:

1
2
3
4
5
6
7
8
复制代码Hibernate: insert into sys_user (age, user_name) values (?, ?)
Hibernate: insert into sys_role (role_name) values (?)
Hibernate: insert into sys_role (role_name) values (?)
Hibernate: insert into sys_role (role_name) values (?)
Hibernate: insert into sys_user_role (sys_user_id, sys_role_id) values (?, ?)
Hibernate: insert into sys_user_role (sys_user_id, sys_role_id) values (?, ?)
Hibernate: insert into sys_user_role (sys_user_id, sys_role_id) values (?, ?)
Hibernate: insert into sys_user_role (sys_role_id, sys_user_id) values (?, ?)

系统会自动创建表sys_user_role并添加数据。

ii.级联保存

保存用户的同时,保存其关联角色。

只需要在操作对象的注解上配置cascade

1
2
复制代码@ManyToMany(mappedBy = "roles",cascade = CascadeType.ALL)
private Set<User> users = new HashSet<>();
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
复制代码package ctgu;

import ctgu.dao.RoleDao;
import ctgu.dao.UserDao;
import ctgu.pojo.Role;
import ctgu.pojo.User;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.annotation.Rollback;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.transaction.annotation.Transactional;

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration("classpath:applicationContext.xml")
public class ManyToMany {
@Autowired
private UserDao userDao;
@Autowired
private RoleDao roleDao;
/**
* 级联操作:保存一个用户的同时,保存用户的关联角色
* 只需要在操作对象的注解上配置cascade
*/
@Test
@Transactional
@Rollback(false)
public void addCasecade() {
User user = new User();
Role role = new Role();
user.setUserName("张三");
role.setRoleName("java程序员");
//用户和角色都可以对中间表进行维护,添加两次就重复了
//配置角色到用户的关系,可以对中间表中的数据进行维护
role.getUsers().add(user);
//配置用户到角色的关系,
user.getRoles().add(role);
roleDao.save(role);
}
}

测试结果:

1
2
3
复制代码Hibernate: insert into sys_role (role_name) values (?)
Hibernate: insert into sys_user (age, user_name) values (?, ?)
Hibernate: insert into sys_user_role (sys_user_id, sys_role_id) values (?, ?)

iii.级联删除

1
2
3
4
5
6
7
8
9
复制代码   /**
* 级联操作:删除id为1的用户,同时删除他的关联对象
*/
@Test
@Transactional
@Rollback(false)
public void deleteCasecade() {
roleDao.delete(23L);
}

测试结果:

1
2
3
4
5
复制代码Hibernate: select role0_.role_id as role_id1_0_0_, role0_.role_name as role_nam2_0_0_ from sys_role role0_ where role0_.role_id=?
Hibernate: select users0_.sys_role_id as sys_role2_2_0_, users0_.sys_user_id as sys_user1_2_0_, user1_.user_id as user_id1_1_1_, user1_.age as age2_1_1_, user1_.user_name as user_nam3_1_1_ from sys_user_role users0_ inner join sys_user user1_ on users0_.sys_user_id=user1_.user_id where users0_.sys_role_id=?
Hibernate: delete from sys_user_role where sys_user_id=?
Hibernate: delete from sys_user where user_id=?
Hibernate: delete from sys_role where role_id=?

注意:

  • 调用的对象是role,所有需要在role对象中配置级联cascade = CascadeType.ALL;
  • 慎用!可能会清空相关联的数据;

6.SpringDataJPA中的多表查询

以下例子采用一对多的案例实现。

i.对象导航查询

对象导航查询的方式就是根据已加载的对象,导航到他的关联对象。利用实体与实体之间的关系来检索对象。例如:通过ID查询出一个Customer,可以调用Customer对象中的getLinkMans()方法来获取该客户的所有联系人。

对象导航查询使用的要求是:两个对象之间必须存在关联联系。

案例:查询公司,获取公司下所有的员工

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
复制代码package ctgu.QueryTest;

import ctgu.dao.CustomerDao;
import ctgu.dao.LinkManDao;
import ctgu.pojo.Customer;
import ctgu.pojo.LinkMan;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.transaction.annotation.Transactional;

import java.util.Set;

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration("classpath:applicationContext.xml")
public class ObjectQuery {
@Autowired
private CustomerDao customerDao;
@Autowired
private LinkManDao linkManDao;
/**
* 测试导航查询(查询一个对象的时候,通过此查询他的关联对象)
* 对于对象导航查询,默认使用的是延迟加载的形式来查询的,(需要才去查询)
* 调用get方法并不会立即发送查询,而实在关联对象使用的时候才会查询
* 修改配置,将延迟加载改为立即加载
* fetch 需要配置多表映射关系发注解上
*
*/
@Test
@Transactional//解决在java代码中的no Session问题
public void QueryTest01(){
Customer customer = customerDao.findOne(26L);
Set<LinkMan> linkMans = customer.getLinkMans();
for(LinkMan man:linkMans){
System.out.println(man);
}
}
}

问题:我们在查询Customer时,一定要把LinkMan查出来吗?

分析:如果我们不查的话,在需要的时候需要重新写代码,调用方法查询;但是每次都查出来又会浪费服务器的内存。

解决:查询主表对象时,采用延迟加载的思想,通过配置的方式,当我们需要使用的时候才查询。

延迟加载

由于上述调用的对象为Customer,故而在Customer对象中需要配置延迟加载。Customer对象

1
2
复制代码@OneToMany(mappedBy = "customer",fetch = FetchType.LAZY)
private Set<LinkMan> linkMans=new HashSet<>();

测试结果:

1
2
3
4
复制代码Hibernate: select customer0_.cust_id as cust_id1_0_0_, customer0_.cust_address as cust_add2_0_0_, customer0_.cust_industry as cust_ind3_0_0_, customer0_.cust_level as cust_lev4_0_0_, customer0_.cust_name as cust_nam5_0_0_, customer0_.cust_phone as cust_pho6_0_0_, customer0_.cust_source as cust_sou7_0_0_ from cst_customer customer0_ where customer0_.cust_id=?
Hibernate: select linkmans0_.lkm_cust_id as lkm_cust9_1_0_, linkmans0_.lkm_id as lkm_id1_1_0_, linkmans0_.lkm_id as lkm_id1_1_1_, linkmans0_.lkm_cust_id as lkm_cust9_1_1_, linkmans0_.lkm_email as lkm_emai2_1_1_, linkmans0_.lkm_gender as lkm_gend3_1_1_, linkmans0_.lkm_memo as lkm_memo4_1_1_, linkmans0_.lkm_mobile as lkm_mobi5_1_1_, linkmans0_.lkm_name as lkm_name6_1_1_, linkmans0_.lkm_phone as lkm_phon7_1_1_, linkmans0_.lkm_position as lkm_posi8_1_1_ from cst_linkman linkmans0_ where linkmans0_.lkm_cust_id=?
LinkMan{lkmId=31, lkmName='李四', lkmGenger='null', lkmPhone='null', lkmMobile='null', lkmEmail='null', lkmPosition='null', lkmMemo='null'}
LinkMan{lkmId=30, lkmName='张三', lkmGenger='null', lkmPhone='null', lkmMobile='null', lkmEmail='null', lkmPosition='null', lkmMemo='null'}

分析:我们发现其执行了两条select语句。

问题:在我们查LinkMan时,是否需要把Customer查出来?

分析:由于一个用户只属于一家公司,及每个LinkMan都有唯一的Customer与之对应。如果我们不查,在使用的时候需要额外代码查询。且查询出的是单个对象,对内存消耗较小。

解决:在从表中采用立即加载的思想,只要查询从表实体,就把主表对象同时查出来。

立即加载

1
2
复制代码    @OneToMany(mappedBy = "customer",fetch = FetchType.EAGER)
private Set<LinkMan> linkMans=new HashSet<>();

测试结果:

1
2
3
复制代码Hibernate: select customer0_.cust_id as cust_id1_0_0_, customer0_.cust_address as cust_add2_0_0_, customer0_.cust_industry as cust_ind3_0_0_, customer0_.cust_level as cust_lev4_0_0_, customer0_.cust_name as cust_nam5_0_0_, customer0_.cust_phone as cust_pho6_0_0_, customer0_.cust_source as cust_sou7_0_0_, linkmans1_.lkm_cust_id as lkm_cust9_1_1_, linkmans1_.lkm_id as lkm_id1_1_1_, linkmans1_.lkm_id as lkm_id1_1_2_, linkmans1_.lkm_cust_id as lkm_cust9_1_2_, linkmans1_.lkm_email as lkm_emai2_1_2_, linkmans1_.lkm_gender as lkm_gend3_1_2_, linkmans1_.lkm_memo as lkm_memo4_1_2_, linkmans1_.lkm_mobile as lkm_mobi5_1_2_, linkmans1_.lkm_name as lkm_name6_1_2_, linkmans1_.lkm_phone as lkm_phon7_1_2_, linkmans1_.lkm_position as lkm_posi8_1_2_ from cst_customer customer0_ left outer join cst_linkman linkmans1_ on customer0_.cust_id=linkmans1_.lkm_cust_id where customer0_.cust_id=?
LinkMan{lkmId=30, lkmName='张三', lkmGenger='null', lkmPhone='null', lkmMobile='null', lkmEmail='null', lkmPosition='null', lkmMemo='null'}
LinkMan{lkmId=31, lkmName='李四', lkmGenger='null', lkmPhone='null', lkmMobile='null', lkmEmail='null', lkmPosition='null', lkmMemo='null'}

分析结果:我们发现其只执行了一条select语句。

对比可以发现,立即加载是一次性将查询对象以及关联对象查出来,而延迟加载是先查询目标对象,如果未调用Set<LinkMan> linkMans = customer.getLinkMans();方法,则将不会执行关联对象的查询。

ii.使用 Specification 查询

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
复制代码/**
* Specification的多表查询
*/
@Test
public void testFind() {
Specification<LinkMan> spec = new Specification<LinkMan>() {
public Predicate toPredicate(Root<LinkMan> root, CriteriaQuery<?> query, CriteriaBuilder cb) {
//Join代表链接查询,通过root对象获取
//创建的过程中,第一个参数为关联对象的属性名称,第二个参数为连接查询的方式(left,inner,right)
//JoinType.LEFT : 左外连接,JoinType.INNER:内连接,JoinType.RIGHT:右外连接
Join<LinkMan, Customer> join = root.join("customer",JoinType.INNER);
return cb.like(join.get("custName").as(String.class),"传智播客1");
}
};
List<LinkMan> list = linkManDao.findAll(spec);
for (LinkMan linkMan : list) {
System.out.println(linkMan);
}
}

本文转载自: 掘金

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

【奇技淫巧】新的图片加载库?基于Kotlin协程的图片加载库

发表于 2020-05-15

新的图片加载库——Coil

Coil 是 Instacart 团队研发的新的的图片加载库,它使用了很多高级功能,例如协程,Okhttp,androidx.lifecycle。Coil 还包括一些高级功能,例如图像采样,有效的内存使用以及请求的自动取消/暂停

默认情况下 Coil 与 R8 完全兼容,开箱即用,不需要添加额外的规则。如果使用 Proguard ,您可能需要为 Coroutines, OkHttp 和 Okio 添加规则

Coil 的优势

  • 快速:Coil 进行了很多优化,包括内存和磁盘缓存,对内存中的图像进行采样,重新使用位图,自动暂停/取消请求等等
  • 轻量:Coil 在您的APK中添加了约 2000 种方法(对于已经使用 OkHttp 和 Coroutines 的应用程序),与 Picasso 相当,远少于 Glide 和 Fresco
  • 易用:Coil 的 API 利用 Kotlin 的特性简化了样板代码
  • 现代:Coil 是 Kotlin-first,使用现代化的库,例如 Coroutines, OkHttp, Okio, 以及 AndroidX Lifecycles

Coil 是以下名称的缩写:Coroutine Image Loader

Artifacts

Coil 拥有 5 个 artifact 并发布在 mavenCentral()

  • io.coil-kt:coil:依赖于 io.coil-kt:coil-base 并且包含了 Coil 的单例和 ImageView.load 的扩展函数
  • io.coil-kt:coil-base:base 库,不包含 Coil 的单例和 ImageView.load 的扩展函数,如果使用依赖注入,则可以使用该库
  • io.coil-kt:coil-gif:引入一系列解码器以支持解码 gif
  • io.coil-kt:coil-svg:引入一系列解码器以支持 svg
  • io.coil-kt:coil-video:包括两个 fetchers ,以支持从 Android 支持的任何视频格式中提取和解码帧
1
2
3
4
groovy复制代码// 普通使用引用
implementation "io.coil-kt:coil:0.11.0"
// 使用依赖注入时或者制作基于 coil 的库引用
implementation "io.coil-kt:coil-base:0.11.0"

Java 8

Coil 要求 Java 8,要通过 D8 启用 Java 8 调试,请将以下内容添加到 Gradle 脚本

Gradle (.gradle)

1
2
3
4
5
6
7
8
9
10
11
12
groovy复制代码android {
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
}

tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).all {
kotlinOptions {
jvmTarget = "1.8"
}
}

Gradle Kotlin DSL (.gradle.kts)

1
2
3
4
5
6
7
8
9
10
11
12
kotlin复制代码android {
compileOptions {
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
}
}

tasks.withType<KotlinCompile> {
kotlinOptions {
jvmTarget = "1.8"
}
}

使用

ImageView 扩展函数

io.coil-kt:coil 提供了 类型安全的 ImageView 扩展函数

在 ImageView 中加载图片,只需调用 load 扩展函数

1
2
3
4
5
6
7
8
9
10
kotlin复制代码// URL
imageView.load("https://www.example.com/image.jpg")

// Resource
imageView.load(R.drawable.image)

// File
imageView.load(File("/path/to/image.jpg"))

// And more...

上面的请求等价于:

1
2
3
4
5
6
kotlin复制代码val imageLoader = Coil.imageLoader(context)
val request = LoadRequest.Builder(imageView.context)
.data("https://www.example.com/image.jpg")
.target(imageView)
.build()
imageLoader.execute(request)

可选的请求配置可以通过 lambda 来操作

1
2
3
4
5
kotlin复制代码imageView.load("https://www.example.com/image.jpg") {
crossfade(true)
placeholder(R.drawable.image)
transformations(CircleCropTransformation())
}

Image Loaders¶

ImageLoader 是执行请求的服务类。 他们处理缓存,数据获取,图像解码,请求管理,bitmap pool,内存管理等。 可以使用 builder 来创建和配置新实例:

1
2
3
4
kotlin复制代码val imageLoader = ImageLoader.Builder(context)
.availableMemoryPercentage(0.25)
.crossfade(true)
.build()

imageView.load 使用单例 ImageLoader 执行 LoadRequest 。 可以使用以下方式访问单例 ImageLoader:

1
kotlin复制代码val imageLoader = Coil.imageLoader(context)

(可选)您可以创建自己的ImageLoader实例,并通过依赖项注入将它们注入:

1
kotlin复制代码val imageLoader = ImageLoader(context)

当您创建单个 ImageLoader 并在整个应用程序中共享时,Coil 的性能最佳。 这是因为每个 ImageLoader 都有自己的内存缓存,bitmap pool 和网络监听

Requests¶

有两种 Request 类型

  • LoadRequest 是一个生命周期范围的 request,支持 Target,Transition 等等
  • GetRequest 挂起并返回 RequestResult

如果要加载到自定义 target 中,可以执行 LoadRequest

1
2
3
4
5
6
7
kotlin复制代码val request = LoadRequest.Builder(context)
.data("https://www.example.com/image.jpg")
.target { drawable ->
// Handle the result.
}
.build()
imageLoader.execute(request)

要强制获取图像,请执行GetRequest:

1
2
3
4
kotlin复制代码val request = GetRequest.Builder(context)
.data("https://www.example.com/image.jpg")
.build()
val drawable = imageLoader.execute(request).drawable

单例

如果您使用的是 io.coil-kt:coil ,您可以使用以下任意方式设置 ImageLoader 的实例

在 Application 中实现 ImageLoaderFactory(推荐)

1
2
3
4
5
6
7
8
9
10
11
12
13
kotlin复制代码class MyApplication : Application(), ImageLoaderFactory {

override fun newImageLoader(): ImageLoader {
return ImageLoader.Builder(context)
.crossfade(true)
.okHttpClient {
OkHttpClient.Builder()
.cache(CoilUtils.createDefaultCache(context))
.build()
}
.build()
}
}

调用 Coil.setImageLoader

1
2
3
4
5
6
7
8
9
kotlin复制代码val imageLoader = ImageLoader.Builder(context)
.crossfade(true)
.okHttpClient {
OkHttpClient.Builder()
.cache(CoilUtils.createDefaultCache(context))
.build()
}
.build()
Coil.setImageLoader(imageLoader)

默认的 ImageLoader 可以通过这样取回

1
kotlin复制代码val imageLoader = Coil.imageLoader(context)

设置默认的 ImageLoader 是可选的。 如果未设置,则 Coil 会延迟创建具有默认值的 ImageLoader

如果您使用的是 io.coil-kt:coil-base,您应创建自己的 ImageLoader 实例并通过依赖注入将它注入到 app 中

注意:如果设置自定义OkHttpClient,则必须设置缓存实现,否则ImageLoader将没有磁盘缓存。 可以使用CoilUtils.createDefaultCache 创建默认的 Coil 缓存实例

支持的数据类型

ImageLoader 支持的数据类型为

  • String (mapped to a Uri)
  • HttpUrl
  • Uri (android.resource, content, file, http, and https schemes only)
  • File
  • @DrawableRes Int
  • Drawable
  • Bitmap

预加载

如果要预加载到内存中,执行一个不带 target 的 LoadRequest

1
2
3
4
5
6
kotlin复制代码val request = LoadRequest.Builder(context)
.data("https://www.example.com/image.jpg")
// 可选的,但是设置 ViewSizeResolver 可以通过限制预加载的大小来节省内存
.size(ViewSizeResolver(imageView))
.build()
imageLoader.execute(request)

如果只想将网络图片预加载到磁盘中,可以为 request 关闭内存缓存

1
2
3
4
5
kotlin复制代码val request = LoadRequest.Builder(context)
.data("https://www.example.com/image.jpg")
.memoryCachePolicy(CachePolicy.DISABLED)
.build()
imageLoader.execute(request)

取消请求

LoadRequest 会自动取消在以下几种情况下

  • 关联的 view detached,
  • 关联的 lifecycle destroyed
  • 另一个 request 在相同的 view 中开启

此外,每个 LoadRequest 返回一个 RequestDisposable,可用于检查请求是否在运行中或处理该请求(有效地取消请求并释放其关联资源)

1
2
3
4
kotlin复制代码val disposable = imageView.load("https://www.example.com/image.jpg")

// Cancel the request.
disposable.dispose()

GetRequest 仅当协程的上下文被取消时才会取消

图片采样

假设磁盘上有一个 500x500 的映像,但是只需要以 100x100 的大小将其加载到内存中即可在视图中显示。 Coil 会将图像加载到内存中,但是如果您需要 500x500 的图像会怎样呢? 从磁盘读取还有更好的「质量」,但是图像已经以 100x100 加载到内存中。 理想情况下,当我们从磁盘以 500x500 读取图像时,我们将使用 100x100 图像作为占位符。

这正是 Coil 所做的,并且 Coil 自动为所有 BitmapDrawables 处理此过程。 与 crossfade(true) 搭配使用时,可以创建视觉效果,使图像细节看起来像淡入淡出,类似于渐进式 JPEG

使用要求

  • AndroidX
  • Min SDK 14+
  • Compile SDK: 29+
  • Java 8+

详细内容移步 官方文档

关于我

我是 Flywith24,我的博客内容已经分类整理 在这里,点击右上角的 Watch 可以及时获取我的文章更新哦 😉

  • 掘金
  • 小专栏
  • Github

本文转载自: 掘金

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

看完这篇操作系统,和面试官扯皮就没问题了

发表于 2020-05-15

解释一下什么是操作系统

操作系统是运行在计算机上最重要的一种软件,它管理计算机的资源和进程以及所有的硬件和软件。它为计算机硬件和软件提供了一种中间层

通常情况下,计算机上会运行着许多应用程序,它们都需要对内存和 CPU 进行交互,操作系统的目的就是为了保证这些访问和交互能够准确无误的进行。

解释一下操作系统的主要目的是什么

操作系统是一种软件,它的主要目的有三种

  • 管理计算机资源,这些资源包括 CPU、内存、磁盘驱动器、打印机等。
  • 提供一种图形界面,就像我们前面描述的那样,它提供了用户和计算机之间的桥梁。
  • 为其他软件提供服务,操作系统与软件进行交互,以便为其分配运行所需的任何必要资源。

操作系统的种类有哪些

操作系统通常预装在你购买计算机之前。大部分用户都会使用默认的操作系统,但是你也可以升级甚至更改操作系统。但是一般常见的操作系统只有三种:Windows、macOS 和 Linux。

操作系统结构

单体系统

在大多数系统中,整个系统在内核态以单一程序的方式运行。整个操作系统是以程序集合来编写的,链接在一块形成一个大的二进制可执行程序,这种系统称为单体系统。

在单体系统中构造实际目标程序时,会首先编译所有单个过程(或包含这些过程的文件),然后使用系统链接器将它们全部绑定到一个可执行文件中

在单体系统中,对于每个系统调用都会有一个服务程序来保障和运行。需要一组实用程序来弥补服务程序需要的功能,例如从用户程序中获取数据。可将各种过程划分为一个三层模型

除了在计算机初启动时所装载的核心操作系统外,许多操作系统还支持额外的扩展。比如 I/O 设备驱动和文件系统。这些部件可以按需装载。在 UNIX 中把它们叫做 共享库(shared library),在 Windows 中则被称为 动态链接库(Dynamic Link Library,DLL)。他们的扩展名为 .dll,在 C:\Windows\system32 目录下存在 1000 多个 DLL 文件,所以不要轻易删除 C 盘文件,否则可能就炸了哦。

分层系统

分层系统使用层来分隔不同的功能单元。每一层只与该层的上层和下层通信。每一层都使用下面的层来执行其功能。层之间的通信通过预定义的固定接口通信。

微内核

为了实现高可靠性,将操作系统划分成小的、层级之间能够更好定义的模块是很有必要的,只有一个模块 — 微内核 — 运行在内核态,其余模块可以作为普通用户进程运行。由于把每个设备驱动和文件系统分别作为普通用户进程,这些模块中的错误虽然会使这些模块崩溃,但是不会使整个系统死机。

MINIX 3 是微内核的代表作,它的具体结构如下

在内核的外部,系统的构造有三层,它们都在用户态下运行,最底层是设备驱动器。由于它们都在用户态下运行,所以不能物理的访问 I/O 端口空间,也不能直接发出 I/O 命令。相反,为了能够对 I/O 设备编程,驱动器构建一个结构,指明哪个参数值写到哪个 I/O 端口,并声称一个内核调用,这样就完成了一次调用过程。

客户-服务器模式

微内核思想的策略是把进程划分为两类:服务器,每个服务器用来提供服务;客户端,使用这些服务。这个模式就是所谓的 客户-服务器模式。

客户-服务器模式会有两种载体,一种情况是一台计算机既是客户又是服务器,在这种方式下,操作系统会有某种优化;但是普遍情况下是客户端和服务器在不同的机器上,它们通过局域网或广域网连接。

客户通过发送消息与服务器通信,客户端并不需要知道这些消息是在本地机器上处理,还是通过网络被送到远程机器上处理。对于客户端而言,这两种情形是一样的:都是发送请求并得到回应。

什么是按需分页

在操作系统中,进程是以页为单位加载到内存中的,按需分页是一种虚拟内存的管理方式。在使用请求分页的系统中,只有在尝试访问页面所在的磁盘并且该页面尚未在内存中时,也就发生了缺页异常,操作系统才会将磁盘页面复制到内存中。

多处理系统的优势

随着处理器的不断增加,我们的计算机系统由单机系统变为了多处理系统,多处理系统的吞吐量比较高,多处理系统拥有多个并行的处理器,这些处理器共享时钟、内存、总线、外围设备等。

多处理系统由于可以共享资源,因此可以开源节流,省钱。整个系统的可靠性也随之提高。

什么是内核

在计算机中,内核是一个计算机程序,它是操作系统的核心,可以控制操作系统中所有的内容。内核通常是在 boot loader 装载程序之前加载的第一个程序。

这里还需要了解一下什么是 boot loader。

boot loader 又被称为引导加载程序,它是一个程序,能够将计算机的操作系统放入内存中。在电源通电或者计算机重启时,BIOS 会执行一些初始测试,然后将控制权转移到引导加载程序所在的主引导记录(MBR) 。

什么是实时系统

实时操作系统对时间做出了严格的要求,实时操作系统分为两种:硬实时和软实时

硬实时操作系统规定某个动作必须在规定的时刻内完成或发生,比如汽车生产车间,焊接机器必须在某一时刻内完成焊接,焊接的太早或者太晚都会对汽车造成永久性伤害。

软实时操作系统虽然不希望偶尔违反最终的时限要求,但是仍然可以接受。并且不会引起任何永久性伤害。比如数字音频、多媒体、手机都是属于软实时操作系统。

你可以简单理解硬实时和软实时的两个指标:是否在时刻内必须完成以及是否造成严重损害。

什么是虚拟内存

虚拟内存是一种内存分配方案,是一项可以用来辅助内存分配的机制。我们知道,应用程序是按页装载进内存中的。但并不是所有的页都会装载到内存中,计算机中的硬件和软件会将数据从 RAM 临时传输到磁盘中来弥补内存的不足。如果没有虚拟内存的话,一旦你将计算机内存填满后,计算机会对你说

呃,不,对不起,您无法再加载任何应用程序,请关闭另一个应用程序以加载新的应用程序。对于虚拟内存,计算机可以执行操作是查看内存中最近未使用过的区域,然后将其复制到硬盘上。虚拟内存通过复制技术实现了 妹子,你快来看哥哥能装这么多程序 的资本。复制是自动进行的,你无法感知到它的存在。

什么是进程和进程表

进程就是正在执行程序的实例,比如说 Web 程序就是一个进程,shell 也是一个进程,文章编辑器 typora 也是一个进程。

操作系统负责管理所有正在运行的进程,操作系统会为每个进程分配特定的时间来占用 CPU,操作系统还会为每个进程分配特定的资源。

操作系统为了跟踪每个进程的活动状态,维护了一个进程表。在进程表的内部,列出了每个进程的状态以及每个进程使用的资源等。

courses.cs.vt.edu/csonline/OS… 这个网站上面有一个关于进程状态轮转的动画,做的真是太好了。

什么是线程,线程和进程的区别

这又是一道老生常谈的问题了,从操作系统的角度来回答一下吧。

我们上面说到进程是正在运行的程序的实例,而线程其实就是进程中的单条流向,因为线程具有进程中的某些属性,所以线程又被称为轻量级的进程。浏览器如果是一个进程的话,那么浏览器下面的每个 tab 页可以看作是一个个的线程。

下面是线程和进程持有资源的区别

线程不像进程那样具有很强的独立性,线程之间会共享数据

创建线程的开销要比进程小很多,因为创建线程仅仅需要堆栈指针和程序计数器就可以了,而创建进程需要操作系统分配新的地址空间,数据资源等,这个开销比较大。

使用多线程的好处是什么

多线程是程序员不得不知的基本素养之一,所以,下面我们给出一些多线程编程的好处

  • 能够提高对用户的响应顺序
  • 在流程中的资源共享
  • 比较经济适用
  • 能够对多线程架构有深入的理解

什么是 RR 调度算法

RR(round-robin) 调度算法主要针对分时系统,RR 的调度算法会把时间片以相同的部分并循环的分配给每个进程,RR 调度算法没有优先级的概念。这种算法的实现比较简单,而且每个线程都会占有时间片,并不存在线程饥饿的问题。

导致系统出现死锁的情况

死锁的出现需要同时满足下面四个条件

  • 互斥(Mutual Exclusion):一次只能有一个进程使用资源。如果另一个进程请求该资源,则必须延迟请求进程,直到释放该资源为止。
  • 保持并等待(Hold and Wait):必须存在一个进程,该进程至少持有一个资源,并且正在等待获取其他进程当前所持有的资源。
  • 无抢占(No Preemption):资源不能被抢占,也就是说,在进程完成其任务之后,只能由拥有它的进程自动释放资源。
  • 循环等待(Circular Wait) :必须存在一组 {p0,p1,….. pn} 的等待进程,使 p0 等待 p1 持有的资源,p1 等待由 p2 持有的资源, pn-1 正在等待由 pn 持有的资源,而 pn 正在等待由 p0 持有的资源。

RAID 的不同级别

RAID 称为 磁盘冗余阵列,简称 磁盘阵列。利用虚拟化技术把多个硬盘结合在一起,成为一个或多个磁盘阵列组,目的是提升性能或数据冗余。

RAID 有不同的级别

  • RAID 0 - 无容错的条带化磁盘阵列
  • RAID 1 - 镜像和双工
  • RAID 2 - 内存式纠错码
  • RAID 3 - 比特交错奇偶校验
  • RAID 4 - 块交错奇偶校验
  • RAID 5 - 块交错分布式奇偶校验
  • RAID 6 - P + Q冗余

什么是 DMA

DMA 的中文名称是直接内存访问,它意味着 CPU 授予 I/O 模块权限在不涉及 CPU 的情况下读取或写入内存。也就是 DMA 可以不需要 CPU 的参与。这个过程由称为 DMA 控制器(DMAC)的芯片管理。由于 DMA 设备可以直接在内存之间传输数据,而不是使用 CPU 作为中介,因此可以缓解总线上的拥塞。DMA 通过允许 CPU 执行任务,同时 DMA 系统通过系统和内存总线传输数据来提高系统并发性。

多线程编程的好处是什么

对不起,我忍不住想偷笑

说直白点,为什么单线程能够处理的却要用多线程来处理?当然是为了提高程序的装逼并行能力了。多线程在某些情况下能够使你程序运行的更快,这也是为什么多核 CPU 会出现,但是多核 CPU 的出现会导致数据的一致性问题,不过这些问题程序员就能解决。另一个角度来说,多线程编程能够提高程序员的编程能力和编程思维。同时也能提高程序员的管理能力,你如果把每条线程流当作罗老师时间管理的女主一样,能够及时协调好所有P友的关系,那你也是超神程序员了,所以,是谁说程序员不会做管理的?Doug Lea 大佬牛逼!!!

ps:Doug Lea 大佬开发的 JUC 工具包,此处不加狗头。

什么是设备驱动程序

在计算机中,设备驱动程序是一种计算机程序,它能够控制或者操作连接到计算机的特定设备。驱动程序提供了与硬件进行交互的软件接口,使操作系统和其他计算机程序能够访问特定设备,不用需要了解其硬件的具体构造。

进程间的通信方式

通信概念

进程间的通信方式比较多,首先你需要理解下面这几个概念

  • 竞态条件:即两个或多个线程同时对一共享数据进行修改,从而影响程序运行的正确性时,这种就被称为竞态条件(race condition)。
  • 临界区:不仅共享资源会造成竞态条件,事实上共享文件、共享内存也会造成竞态条件、那么该如何避免呢?或许一句话可以概括说明:禁止一个或多个进程在同一时刻对共享资源(包括共享内存、共享文件等)进行读写。换句话说,我们需要一种 互斥(mutual exclusion) 条件,这也就是说,如果一个进程在某种方式下使用共享变量和文件的话,除该进程之外的其他进程就禁止做这种事(访问统一资源)。

一个好的解决方案,应该包含下面四种条件

1. 任何时候两个进程不能同时处于临界区
2. 不应对 CPU 的速度和数量做任何假设
3. 位于临界区外的进程不得阻塞其他进程
4. 不能使任何进程无限等待进入临界区

  • 忙等互斥:当一个进程在对资源进行修改时,其他进程必须进行等待,进程之间要具有互斥性,我们讨论的解决方案其实都是基于忙等互斥提出的。

解决方案

进程间的通信用专业一点的术语来表示就是 Inter Process Communication,IPC,它主要有下面几种通信方式

  • 消息传递:消息传递是进程间实现通信和同步等待的机制,使用消息传递,进程间的交流不需要共享变量,直接就可以进行通信;消息传递分为发送方和接收方
  • 先进先出队列:先进先出队列指的是两个不相关联进程间的通信,两个进程之间可以彼此相互进程通信,这是一种全双工通信方式
  • 管道:管道用于两个相关进程之间的通信,这是一种半双工的通信方式,如果需要全双工,需要另外一个管道。
  • 直接通信:在这种进程通信的方式中,进程与进程之间只存在一条链接,进程间要明确通信双方的命名。
  • 间接通信:间接通信是通信双方不会直接建立连接,而是找到一个中介者,这个中介者可能是个对象等等,进程可以在其中放置消息,并且可以从中删除消息,以此达到进程间通信的目的。
  • 消息队列:消息队列是内核中存储消息的链表,它由消息队列标识符进行标识,这种方式能够在不同的进程之间提供全双工的通信连接。
  • 共享内存:共享内存是使用所有进程之间的内存来建立连接,这种类型需要同步进程访问来相互保护。

进程间状态模型

1
复制代码cat chapter1 chapter2 chapter3 | grep tree

第一个进程是 cat,将三个文件级联并输出。第二个进程是 grep,它从输入中选择具有包含关键字 tree 的内容,根据这两个进程的相对速度(这取决于两个程序的相对复杂度和各自所分配到的 CPU 时间片),可能会发生下面这种情况,grep 准备就绪开始运行,但是输入进程还没有完成,于是必须阻塞 grep 进程,直到输入完毕。

当一个进程开始运行时,它可能会经历下面这几种状态

图中会涉及三种状态

  1. 运行态,运行态指的就是进程实际占用 CPU 时间片运行时
  2. 就绪态,就绪态指的是可运行,但因为其他进程正在运行而处于就绪状态
  3. 阻塞态,除非某种外部事件发生,否则进程不能运行

逻辑上来说,运行态和就绪态是很相似的。这两种情况下都表示进程可运行,但是第二种情况没有获得 CPU 时间分片。第三种状态与前两种状态不同的原因是这个进程不能运行,CPU 空闲时也不能运行。

三种状态会涉及四种状态间的切换,在操作系统发现进程不能继续执行时会发生状态1的轮转,在某些系统中进程执行系统调用,例如 pause,来获取一个阻塞的状态。在其他系统中包括 UNIX,当进程从管道或特殊文件(例如终端)中读取没有可用的输入时,该进程会被自动终止。

转换 2 和转换 3 都是由进程调度程序(操作系统的一部分)引起的,进程本身不知道调度程序的存在。转换 2 的出现说明进程调度器认定当前进程已经运行了足够长的时间,是时候让其他进程运行 CPU 时间片了。当所有其他进程都运行过后,这时候该是让第一个进程重新获得 CPU 时间片的时候了,就会发生转换 3。

程序调度指的是,决定哪个进程优先被运行和运行多久,这是很重要的一点。已经设计出许多算法来尝试平衡系统整体效率与各个流程之间的竞争需求。

当进程等待的一个外部事件发生时(如从外部输入一些数据后),则发生转换 4。如果此时没有其他进程在运行,则立刻触发转换 3,该进程便开始运行,否则该进程会处于就绪阶段,等待 CPU 空闲后再轮到它运行。

调度算法都有哪些

调度算法分为三大类:批处理中的调度、交互系统中的调度、实时系统中的调度

批处理中的调度

先来先服务

很像是先到先得。。。可能最简单的非抢占式调度算法的设计就是 先来先服务(first-come,first-serverd)。使用此算法,将按照请求顺序为进程分配 CPU。最基本的,会有一个就绪进程的等待队列。当第一个任务从外部进入系统时,将会立即启动并允许运行任意长的时间。它不会因为运行时间太长而中断。当其他作业进入时,它们排到就绪队列尾部。当正在运行的进程阻塞,处于等待队列的第一个进程就开始运行。当一个阻塞的进程重新处于就绪态时,它会像一个新到达的任务,会排在队列的末尾,即排在所有进程最后。

这个算法的强大之处在于易于理解和编程,在这个算法中,一个单链表记录了所有就绪进程。要选取一个进程运行,只要从该队列的头部移走一个进程即可;要添加一个新的作业或者阻塞一个进程,只要把这个作业或进程附加在队列的末尾即可。这是很简单的一种实现。

不过,先来先服务也是有缺点的,那就是没有优先级的关系,试想一下,如果有 100 个 I/O 进程正在排队,第 101 个是一个 CPU 密集型进程,那岂不是需要等 100 个 I/O 进程运行完毕才会等到一个 CPU 密集型进程运行,这在实际情况下根本不可能,所以需要优先级或者抢占式进程的出现来优先选择重要的进程运行。

最短作业优先

批处理中,第二种调度算法是 最短作业优先(Shortest Job First),我们假设运行时间已知。例如,一家保险公司,因为每天要做类似的工作,所以人们可以相当精确地预测处理 1000 个索赔的一批作业需要多长时间。当输入队列中有若干个同等重要的作业被启动时,调度程序应使用最短优先作业算法

如上图 a 所示,这里有 4 个作业 A、B、C、D ,运行时间分别为 8、4、4、4 分钟。若按图中的次序运行,则 A 的周转时间为 8 分钟,B 为 12 分钟,C 为 16 分钟,D 为 20 分钟,平均时间内为 14 分钟。

现在考虑使用最短作业优先算法运行 4 个作业,如上图 b 所示,目前的周转时间分别为 4、8、12、20,平均为 11 分钟,可以证明最短作业优先是最优的。考虑有 4 个作业的情况,其运行时间分别为 a、b、c、d。第一个作业在时间 a 结束,第二个在时间 a + b 结束,以此类推。平均周转时间为 (4a + 3b + 2c + d) / 4 。显然 a 对平均值的影响最大,所以 a 应该是最短优先作业,其次是 b,然后是 c ,最后是 d 它就只能影响自己的周转时间了。

需要注意的是,在所有的进程都可以运行的情况下,最短作业优先的算法才是最优的。

最短剩余时间优先

最短作业优先的抢占式版本被称作为 最短剩余时间优先(Shortest Remaining Time Next) 算法。使用这个算法,调度程序总是选择剩余运行时间最短的那个进程运行。当一个新作业到达时,其整个时间同当前进程的剩余时间做比较。如果新的进程比当前运行进程需要更少的时间,当前进程就被挂起,而运行新的进程。这种方式能够使短期作业获得良好的服务。

交互式系统中的调度

交互式系统中在个人计算机、服务器和其他系统中都是很常用的,所以有必要来探讨一下交互式调度

轮询调度

一种最古老、最简单、最公平并且最广泛使用的算法就是 轮询算法(round-robin)。每个进程都会被分配一个时间段,称为时间片(quantum),在这个时间片内允许进程运行。如果时间片结束时进程还在运行的话,则抢占一个 CPU 并将其分配给另一个进程。如果进程在时间片结束前阻塞或结束,则 CPU 立即进行切换。轮询算法比较容易实现。调度程序所做的就是维护一个可运行进程的列表,就像下图中的 a,当一个进程用完时间片后就被移到队列的末尾,就像下图的 b。

优先级调度

事实情况是不是所有的进程都是优先级相等的。例如,在一所大学中的等级制度,首先是院长,然后是教授、秘书、后勤人员,最后是学生。这种将外部情况考虑在内就实现了优先级调度(priority scheduling)

它的基本思想很明确,每个进程都被赋予一个优先级,优先级高的进程优先运行。

但是也不意味着高优先级的进程能够永远一直运行下去,调度程序会在每个时钟中断期间降低当前运行进程的优先级。如果此操作导致其优先级降低到下一个最高进程的优先级以下,则会发生进程切换。或者,可以为每个进程分配允许运行的最大时间间隔。当时间间隔用完后,下一个高优先级的进程会得到运行的机会。

最短进程优先

对于批处理系统而言,由于最短作业优先常常伴随着最短响应时间,一种方式是根据进程过去的行为进行推测,并执行估计运行时间最短的那一个。假设每个终端上每条命令的预估运行时间为 T0,现在假设测量到其下一次运行时间为 T1,可以用两个值的加权来改进估计时间,即aT0+ (1- 1)T1。通过选择 a 的值,可以决定是尽快忘掉老的运行时间,还是在一段长时间内始终记住它们。当 a = 1/2 时,可以得到下面这个序列

可以看到,在三轮过后,T0 在新的估计值中所占比重下降至 1/8。

有时把这种通过当前测量值和先前估计值进行加权平均从而得到下一个估计值的技术称作 老化(aging)。这种方法会使用很多预测值基于当前值的情况。

彩票调度

有一种既可以给出预测结果而又有一种比较简单的实现方式的算法,就是 彩票调度(lottery scheduling)算法。他的基本思想为进程提供各种系统资源的彩票。当做出一个调度决策的时候,就随机抽出一张彩票,拥有彩票的进程将获得资源。比如在 CPU 进行调度时,系统可以每秒持有 50 次抽奖,每个中奖进程会获得额外运行时间的奖励。

可以把彩票理解为 buff,这个 buff 有 15% 的几率能让你产生 速度之靴 的效果。

公平分享调度

如果用户 1 启动了 9 个进程,而用户 2 启动了一个进程,使用轮转或相同优先级调度算法,那么用户 1 将得到 90 % 的 CPU 时间,而用户 2 将之得到 10 % 的 CPU 时间。

为了阻止这种情况的出现,一些系统在调度前会把进程的拥有者考虑在内。在这种模型下,每个用户都会分配一些CPU 时间,而调度程序会选择进程并强制执行。因此如果两个用户每个都会有 50% 的 CPU 时间片保证,那么无论一个用户有多少个进程,都将获得相同的 CPU 份额。

页面置换算法都有哪些

算法 注释
最优算法 不可实现,但可以用作基准
NRU(最近未使用) 算法 和 LRU 算法很相似
FIFO(先进先出) 算法 有可能会抛弃重要的页面
第二次机会算法 比 FIFO 有较大的改善
时钟算法 实际使用
LRU(最近最少)算法 比较优秀,但是很难实现
NFU(最不经常食用)算法 和 LRU 很类似
老化算法 近似 LRU 的高效算法
工作集算法 实施起来开销很大
工作集时钟算法 比较有效的算法
  • 最优算法在当前页面中置换最后要访问的页面。不幸的是,没有办法来判定哪个页面是最后一个要访问的,因此实际上该算法不能使用。然而,它可以作为衡量其他算法的标准。
  • NRU 算法根据 R 位和 M 位的状态将页面氛围四类。从编号最小的类别中随机选择一个页面。NRU 算法易于实现,但是性能不是很好。存在更好的算法。
  • FIFO 会跟踪页面加载进入内存中的顺序,并把页面放入一个链表中。有可能删除存在时间最长但是还在使用的页面,因此这个算法也不是一个很好的选择。
  • 第二次机会算法是对 FIFO 的一个修改,它会在删除页面之前检查这个页面是否仍在使用。如果页面正在使用,就会进行保留。这个改进大大提高了性能。
  • 时钟 算法是第二次机会算法的另外一种实现形式,时钟算法和第二次算法的性能差不多,但是会花费更少的时间来执行算法。
  • LRU 算法是一个非常优秀的算法,但是没有特殊的硬件(TLB)很难实现。如果没有硬件,就不能使用 LRU 算法。
  • NFU 算法是一种近似于 LRU 的算法,它的性能不是非常好。
  • 老化 算法是一种更接近 LRU 算法的实现,并且可以更好的实现,因此是一个很好的选择
  • 最后两种算法都使用了工作集算法。工作集算法提供了合理的性能开销,但是它的实现比较复杂。WSClock 是另外一种变体,它不仅能够提供良好的性能,而且可以高效地实现。

最好的算法是老化算法和WSClock算法。他们分别是基于 LRU 和工作集算法。他们都具有良好的性能并且能够被有效的实现。还存在其他一些好的算法,但实际上这两个可能是最重要的。

影响调度程序的指标是什么

会有下面几个因素决定调度程序的好坏

  • CPU 使用率:

CPU 正在执行任务(即不处于空闲状态)的时间百分比。

  • 等待时间

这是进程轮流执行的时间,也就是进程切换的时间

  • 吞吐量

单位时间内完成进程的数量

  • 响应时间

这是从提交流程到获得有用输出所经过的时间。

  • 周转时间

从提交流程到完成流程所经过的时间。

什么是僵尸进程

僵尸进程是已完成且处于终止状态,但在进程表中却仍然存在的进程。僵尸进程通常发生在父子关系的进程中,由于父进程仍需要读取其子进程的退出状态所造成的。

本文转载自: 掘金

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

Java中IO和NIO的本质和区别 简介 IO的本质 DMA

发表于 2020-05-15

简介

终于要写到java中最最让人激动的部分了IO和NIO。IO的全称是input output,是java程序跟外部世界交流的桥梁,IO指的是java.io包中的所有类,他们是从java1.0开始就存在的。NIO叫做new IO,是在java1.4中引入的新一代IO。

IO的本质是什么呢?它和NIO有什么区别呢?我们该怎么学习IO和NIO呢?

别急,看完这篇文章一切都有答案。

更多内容请访问<www.flydean.com>

IO的本质

IO的作用就是从外部系统读取数据到java程序中,或者把java程序中输出的数据写回到外部系统。这里的外部系统可能是磁盘,网络流等等。

因为对所有的外部数据的处理都是由操作系统内核来实现的,对于java应用程序来说,只是调用操作系统中相应的接口方法,从而和外部数据进行交互。

所有IO的本质就是对Buffer的处理,我们把数据放入Buffer供系统写入外部数据,或者从系统Buffer中读取从外部系统中读取的数据。如下图所示:

用户空间也就是我们自己的java程序有一个Buffer,系统空间也有一个buffer。所以会出现系统空间缓存数据的情况,这种情况下系统空间将会直接返回Buffer中的数据,提升读取速度。

DMA和虚拟地址空间

在继续讲解之前,我们先讲解两个操作系统中的基本概念,方便后面我们对IO的理解。

现代操作系统都有一个叫做DMA(Direct memory access)的组件。这个组件是做什么的呢?

一般来说对内存的读写都是要交给CPU来完成的,在没有DMA的情况下,如果程序进行IO操作,那么所有的CPU时间都会被占用,CPU没法去响应其他的任务,只能等待IO执行完成。这在现代应用程序中是无法想象的。

如果使用DMA,则CPU可以把IO操作转交给其他的操作系统组件,比如数据管理器来操作,只有当数据管理器操作完毕之后,才会通知CPU该IO操作完成。现代操作系统基本上都实现了DMA。

虚拟地址空间也叫做(Virtual address space),为了不同程序的互相隔离和保证程序中地址的确定性,现代计算机系统引入了虚拟地址空间的概念。简单点讲可以看做是跟实际物理地址的映射,通过使用分段或者分页的技术,将实际的物理地址映射到虚拟地址空间。

对于上面的IO的基本流程图中,我们可以将系统空间的buffer和用户空间的buffer同时映射到虚拟地址空间的同一个地方。这样就省略了从系统空间拷贝到用户空间的步骤。速度会更快。

同时为了解决虚拟空间比物理内存空间大的问题,现代计算机技术一般都是用了分页技术。

分页技术就是将虚拟空间分为很多个page,只有在需要用到的时候才为该page分配到物理内存的映射,这样物理内存实际上可以看做虚拟空间地址的缓存。

虚拟空间地址分页对IO的影响就在于,IO的操作也是基于page来的。

比较常用的page大小有:1,024, 2,048, 和 4,096 bytes。

IO的分类

IO可以分为File/Block IO和Stream I/O两类。

对于File/Block IO来说,数据是存储在disk中,而disk是由filesystem来进行管理的。我们可以通过filesystem来定义file的名字,路径,文件属性等内容。

filesystem通过把数据划分成为一个个的data blocks来进行管理。有些blocks存储着文件的元数据,有些block存储着真正的数据。

最后filesystem在处理数据的过程中,也进行了分页。filesystem的分页大小可以跟内存分页的大小一致,或者是它的倍数,比如 2,048 或者 8,192 bytes等。

并不是所有的数据都是以block的形式存在的,我们还有一类IO叫做stream IO。

stream IO就像是管道流,里面的数据是序列被消费的。

IO和NIO的区别

java1.0中的IO是流式IO,它只能一个字节一个字节的处理数据,所以IO也叫做Stream IO。

而NIO是为了提升IO的效率而生的,它是以Block的方式来读取数据的。

Stream IO中,input输入一个字节,output就输出一个字节,因为是Stream,所以可以加上过滤器或者过滤器链,可以想想一下web框架中的filter chain。在Stream IO中,数据只能处理一次,你不能在Stream中回退数据。

在Block IO中,数据是以block的形式来被处理的,因此其处理速度要比Stream IO快,同时可以回退处理数据。但是你需要自己处理buffer,所以复杂程度要比Stream IO高。

一般来说Stream IO是阻塞型IO,当线程进行读或者写操作的时候,线程会被阻塞。

而NIO一般来说是非阻塞的,也就是说在进行读或者写的过程中可以去做其他的操作,而读或者写操作执行完毕之后会通知NIO操作的完成。

在IO中,主要分为DataOutPut和DataInput,分别对应IO的out和in。

DataOutPut有三大类,分别是Writer,OutputStream和ObjectOutput。

看下他们中的继承关系:

DataInput也有三大类,分别是ObjectInput,InputStream和Reader。

看看他们的继承关系:

ObjectOutput和ObjectInput类比较少,这里就不列出来了。

统计一下大概20个类左右,搞清楚这20个类的用处,恭喜你java IO你就懂了!

对于NIO来说比较复杂一点,首先,为了处理block的信息,需要将数据读取到buffer中,所以在NIO中Buffer是一个非常中要的概念,我们看下NIO中的Buffer:

从上图我们可以看到NIO中为我们准备了各种各样的buffer类型使用。

另外一个非常重要的概念是channel,channel是NIO获取数据的通道:

NIO需要掌握的类的个数比IO要稍稍多一点,毕竟NIO要复杂一点。

就这么几十个类,我们就掌握了IO和NIO,想想都觉得兴奋。

总结

后面的文章中,我们会介绍小师妹给你们认识,刚好她也在学java IO,后面的学习就跟她一起进行吧,敬请期待。

本文作者:flydean程序那些事

本文链接:www.flydean.com/io-nio-over…

本文来源:flydean的博客

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

本文转载自: 掘金

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

1…813814815…956

开发者博客

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