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

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


  • 首页

  • 归档

  • 搜索

Kotlin Jetpack 实战|06 Kotlin 扩

发表于 2020-08-06

往期文章

《Kotlin Jetpack 实战:开篇》

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

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

《04. Kotlin 高阶函数》

《05. Kotlin 泛型》

前言

扩展(Extension),可以说是 Kotlin 里最有意思的特性,没有之一。

本文会系统的讲解 Kotlin 扩展函数和扩展属性以及比较难懂的扩展作用域和扩展静态解析,最后再搭配一个实战环节,将扩展函数跟前面讲的高阶函数结合到一起。

前期准备

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

正文

1. 扩展是什么?

Kotlin 的扩展,用起来就像是:能给一个类新增功能,这个新增的功能:可以是函数,也可以是属性。

借助 Kotlin 扩展,我们能轻易的写出这样的代码:

1
2
3
4
5
arduino复制代码// 扩展函数
"KotlinJetpackInAction".log()

// 扩展属性
val isBlank = String.isNullOrBlank

以上的代码,看起来就像是我们修改了原本 String 并且往里面加了方法和属性: log(), isNullOrBlank。

初次见到扩展这个特性的时候,我真的被惊艳到了。虽然扩展不是 Kotlin 独有的特性(别的现代语言也有),但是,Kotlin 能在兼容 Java 的同时引入这样的特性,那就真的很了不起了。

2. 顶层扩展 (Top Level Extension)

顶层扩展,是最常用的扩展方式,它的定义方式也很简单,以上面的两行代码为例,我们看看它们分别应该怎么定义吧。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
kotlin复制代码// BaseExt.kt
package com.boycoder.kotlinjetpackinaction

// 注意!!
// 顶层扩展不能定义在任何 Class 里,不然它就变成“类内扩展”了!

// 为 String 定义扩展函数
fun String.log() {
println(this)
}

// 为 String 定义扩展属性
val String?.isNullOrBlank: Boolean
get() = this == null || this.isBlank()

3. 顶层扩展的原理是什么?

要理解顶层扩展的实现原理,直接看字节码对应的 Java 即可,前面的文章已经讲过如何将 Kotlin 代码反编译成 Java,我们直接看结果:

1
2
3
4
5
6
7
java复制代码public static final void log(String $this$log) {
System.out.println($this$log);
}

public static final boolean isNullOrBlank(String $this$isNullOrBlank) {
return $this$isNullOrBlank == null || StringsKt.isBlank((CharSequence)$this$isNullOrBlank);
}

顶层扩展的本质,其实就是 Java 的静态方法,这跟我们在 Java 中经常写的 Utils 类其实是一个原理。Kotlin 的顶层扩展用着感觉很神奇,但它的原理异常简单。这一切都是因为 Kotlin 编译器帮我们做了一层封装和转换。

有的人可能会嗤之以鼻的说“这不就是语法糖嘛”,但我从中看到的是 Kotlin 这种追求简洁和生产力的设计思想。

4. 类内扩展 (Declaring extensions as members)

Package 级别的顶层扩展理解起来很简单,类内扩展会稍微复杂些。

类内扩展(Declaring extensions as members) 在官方中文站的翻译是:扩展声明为成员,这个翻译虽然更接近本质,但太僵硬了,因此我在这里用 类内扩展 指代它。

类内扩展的写法跟顶层扩展是一模一样的,区别在于它在其他类的里面。让我们来看一个例子:

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
kotlin复制代码// 被扩展的类
class Host(val hostname: String) {
fun printHostname() { print(hostname) }
}

class Test(val host: Host, val port: Int) {
fun printPort() { print(port) }

// 在 Test 类内给 Host 增加了一个扩展函数
// ↓
fun Host.printConnectionString() {
printHostname() // Host.printHostname()
print(":")
printPort() // Test.printPort()
}

// 在 Test 类内给 Host 增加了一个扩展属性
// ↓
val Host.isHomeEmpty: Boolean
get() = hostname.isEmpty()

fun test() {
host.printConnectionString()
}
}

fun main() {
// 报错,Host 的类内扩展,在外面无法访问,这是与顶层扩展的不同
Host("").isHomeEmpty
Host("").printConnectionString()
}

5. 扩展小结:

  • 顶层扩展 它不能定义在类内,它的作用域是 Package 级别的,能导包就能用
  • 类内扩展 它定义在其他类内,它的作用与局限在该类内
  • 类内扩展 的优势在于,它既能访问被扩展类(Host),也能访问它所在的类(Test)
  • 扩展 并没有实际修改被扩展的类,因此我们仍然只能访问类里的public方法和属性

6. 类内扩展的原理是什么?

我们直接看反编译后的 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
java复制代码// Host 类并没有新增任何属性和方法
// ↓
public final class Host {
...
public final void printHostname() {
String var1 = this.hostname;
System.out.print(var1);
}
}

public final class Test {
public final void printPort() {
System.out.print(var1);
}
// Host 的扩展函数,变成了 Test 的成员函数,Host 变成了参数
// ↓ ↓
public final void printConnectionString(Host $this$printConnectionString) {
$this$printConnectionString.printHostname();
String var2 = ":";
System.out.print(var2);
this.printPort();
}
// Host 的扩展属性,也变成了 Test 的成员函数,Host 变成了参数
// ↓ ↓
public final boolean isHomeEmpty(Host $this$isHomeEmpty) {
CharSequence var2 = (CharSequence)$this$isHomeEmpty.getHostname();
return var2.length() == 0;
}
}

我们回过头来看 类内扩展 的英文:(Declaring extensions as members),这非常接近它的本质。看到这里,各位应该明白这两个名字的差别:类内扩展描述的是表象;扩展声明为成员描述的是原理。

另外,在上面这个案例中,Test 叫做分发接收者(Dispatch Receiver),Host 叫做扩展接受者(Extension Receiver)。这……是不是好像在哪听过类似的名字?对!这里跟上一章节:高阶函数带接收者的函数类型相呼应了。

7. 扩展函数的类型是什么?

上一章节讲带接收者的函数类型的时候,我讲过这样一句话:

从外表上看,带接收者的函数类型,就等价于成员函数(也等价于扩展函数)。但从本质上讲,它仍是通过编译器注入 this 来实现的。

一个表格来总结:

所以说,带接收者的函数类型和扩展函数的语法设计也是一样的。

下面是我在 Demo 里写的验证代码,感兴趣的小伙伴可以去 TestExt.kt 实际运行一下:

1
2
3
4
5
6
kotlin复制代码fun testFunctionType() {
var lambda: A.(B, C) -> D? = A::test
lambda = A::testExt
lambda = ::testReceiver
var lambdaX: (A, B, C) -> D? = lambda
}

8. 扩展是静态的

扩展是静态的。

这句话的潜台词是:扩展不支持多态。看这个代码案例很容易就能理解:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
kotlin复制代码open class Shape

class Rectangle: Shape()

fun Shape.getName() = "Shape"

fun Rectangle.getName() = "Rectangle"

fun printClassName(s: Shape) {
println(s.getName())
}

printClassName(Rectangle())
// 输出: Shape

这个特性虽然反直觉,但是很容易理解,以后我们使用过程当中注意一下就好。

以上代码的具体细节可以看我这个 GitHub Commit。


9. 类内扩展 override,扩展函数冲突

这部分是扩展函数相对难理解的部分,文字不容易解释,只有实际运行代码通过反编译才能弄清楚,请到 Demo 工程中找到 TestExtAsMember.kt 运行代码,然后反编译思考一下。相关解释我已经写到注释里了。代码案例也是直接用的官方文档里的,这个例子设计的很巧妙。

TestExtAsMember.kt 的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
kotlin复制代码open class Base { }

class Derived : Base() { }

open class BaseCaller {
open fun Base.printFunctionInfo() {
println("Base extension function in BaseCaller")
}

open fun Derived.printFunctionInfo() {
println("Derived extension function in BaseCaller")
}

val Derived.test: Int
get() = 1

fun call(b: Base) {
b.printFunctionInfo() // 调用扩展函数
}
}

class DerivedCaller: BaseCaller() {
override fun Base.printFunctionInfo() {
println("Base extension function in DerivedCaller")
}

override fun Derived.printFunctionInfo() {
println("Derived extension function in DerivedCaller")
}
}

/**
* 步骤:先运行代码,然后调试代码,最后反编译代码。
*
* 理解这个例子的关键在于:
*
* BaseCaller().call(), DerivedCaller().call() 是多态的。
*
* 而 call 函数里的 base.printFunctionInfo() 是静态的。
*
* 这段话一定要结合反编译后的代码看
*
*/
fun main() {
BaseCaller().call(Base())
BaseCaller().call(Derived())
DerivedCaller().call(Base())
DerivedCaller().call(Derived())
}

以上代码的具体细节可以看我这个 GitHub Commit。


6. 实战

学了这么多理论,终于到我们的实战环节了。

7. 扩展函数 + SharedPreferences

还记得 Java 的 SharedPreferences 有多麻烦吗?这种模版代码我们是否写过很多?

1
2
3
4
5
6
java复制代码SharedPreferences sharedPreferences= getSharedPreferences("data",Context.MODE_PRIVATE);
SharedPreferences.Editor editor = sharedPreferences.edit();
editor.putString(SP_KEY_RESPONSE, response);

editor.commit();
editor.apply();

Java 时代我们可以封装类似 PreferencesUtils 来避免模版代码。而 Kotlin 的扩展函数能让我们的代码看起来更加的简洁。接下来,我们为 SharedPreferences 增加一个扩展函数:

1
2
3
4
5
6
7
8
9
10
11
12
kotlin复制代码fun SharedPreferences.edit(
commit: Boolean = false,
action: SharedPreferences.Editor.() -> Unit
) {
val editor = edit()
action(editor)
if (commit) {
editor.commit()
} else {
editor.apply()
}
}

这个扩展函数很简单,我们直接看怎么用它吧。

1
2
3
4
5
6
7
8
9
kotlin复制代码// MainActivity.kt
private val preference: SharedPreferences by lazy(LazyThreadSafetyMode.NONE) {
getSharedPreferences(SP_NAME, MODE_PRIVATE)
}

private fun display(response: String?) {
...
preference.edit { putString(SP_KEY_RESPONSE, response) }
}

是不是清爽很多?我们终于有地方缓存 API 请求了。😂

注:另外,我们还可以结合 Kotlin 的其他特性将 SharedPreferences 封装的更加彻底,这个我们下一篇文章会讲哈。

8. 扩展函数 + Spannable

Java 里要写一个复杂的 SpannableString,是件很痛苦的事情,我随手搜一段老代码,不知能否唤起你的痛苦记忆:

1
2
3
4
5
6
7
8
9
10
11
12
java复制代码SpannableString spannableString = new SpannableString("设置各种不同的字体风格:叶应是叶");
TextView tv_styleSpan = (TextView) findViewById(R.id.tv_styleSpan);

StyleSpan bold = new StyleSpan(Typeface.BOLD);
StyleSpan italic = new StyleSpan(Typeface.ITALIC);
StyleSpan boldItalic = new StyleSpan(Typeface.BOLD_ITALIC);

spannableString.setSpan(bold, 12, 13, Spanned.SPAN_INCLUSIVE_EXCLUSIVE);
spannableString.setSpan(italic, 13, 14, Spanned.SPAN_INCLUSIVE_EXCLUSIVE);
spannableString.setSpan(boldItalic, 14, 16, Spanned.SPAN_INCLUSIVE_EXCLUSIVE);

tv_styleSpan.setText(spannableString);

让我们看看借助 Kotlin 的扩展函数能做出什么样的事情吧:

这是我们接下来要实现的效果,虽然它看着是4行文字,但它却是在一个 TextView 里展示的:

在 Java 里要实现这样一个效果得费不少力气,但借助 Kotlin 扩展函数,我们写一个这样的效果简直是不费吹灰之力:

1
2
3
4
5
6
7
8
9
10
11
12
kotlin复制代码// MainActivity.kt
username.text = ktxSpan {
name!!.bold().italic().size(1.3F).background(Color.YELLOW)
.append("\n")
.append("\n")
.append("Google".strike().italic().size(0.8F).color(Color.GRAY))
.append("\n")
.append(company!!.color(Color.BLUE).underline())
.append("\n")
.append("\n")
.append(url(blog!!, blog))
}

对应的 Kotlin 扩展函数是怎么实现的?其实也不难,前后不过 20 行代码:

这是入口函数,它接收一个初始值,还有一个 Lambda 表达式。注释写的很详细,我就不多解释了:

1
2
3
4
5
6
kotlin复制代码/**
* 顶层函数,作为 Span DSL 的入口类
*
* 这里用到一个重要知识点:CharSequence.() -> SpannableString 与 (CharSequence) -> SpannableString 等价
*/
fun ktxSpan(s: CharSequence = SpannableString(""), func: CharSequence.() -> SpannableString) = func(s)

这是整个 ktxSpan 的核心代码:

1
2
3
4
5
6
7
kotlin复制代码/**
* 核心代码 setSpan(o, 0, length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
*/
private fun span(s: CharSequence, o: Any): SpannableString = when (s) {
is SpannableString -> s
else -> SpannableString(s)
}.apply { setSpan(o, 0, length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) }

这里用扩展函数封装了各种 Span 的 Api:

1
2
3
4
5
6
7
8
kotlin复制代码/**
* 借助扩展函数,实现方便的 Api
*/
fun CharSequence.bold(s: CharSequence = this) = span(s, StyleSpan(android.graphics.Typeface.BOLD))
fun CharSequence.italic(s: CharSequence = this) = span(s, StyleSpan(android.graphics.Typeface.ITALIC))
fun CharSequence.underline(s: CharSequence = this) = span(s, UnderlineSpan())
fun CharSequence.strike(s: CharSequence = this) = span(s, StrikethroughSpan())
/*部分代码省略*/

各位小伙伴可以去下载 Demo 调试运行一下: github.com/chaxiu/Kotl…,欢迎 Star Fork。

思考题1:

这个 ktxSpan 还有优化的空间,你知道该怎么优化吗?

思考题2:

我们在前面高阶函数里写的 HTML DSL,是否也能用扩展来优化?

思考题3:

Kotlin 顶层扩展解决了 Java 的哪些问题?

思考题4:

Kotlin 类内扩展有哪些实际使用场景?

9. 结尾

  • Kotlin 顶层扩展解决了 Java 各种 Utils 的问题,它不仅提高了代码的可读性,还增强了易用性
  • 可读性:response.isNullOrBlank() 比 TextUtils.isEmpty(response) 可读性更好
  • 易用性:当我们在 IDE 里输入:response. IDE 就会提示我们 response.isNullOrBlank(),而 TextUtils 则无法自动提示。

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

回目录–>【Kotlin Jetpack 实战】

本文转载自: 掘金

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

Web API系列(一):初识Web API及手动搭建基本框

发表于 2020-08-06

1.导言

随着Web技术的发展,现在各种框架,前端的,后端的,数不胜数。全栈工程师的压力越来越大。 PC端,pad端,移动端App(安卓/IOS)的发展,使得前后端一体的开发模式十分笨重。因此,前后端分离是web发展的趋势,其中,RESTful API是目前前后端分离的最佳实践,ASP.NET Web API是在.NET Framework上构建RESTful应用程序的理想平台。Web API应用如下图所示。

)​

2.ASP.NET Web API介绍

ASP.NET Web API是一个框架,可以轻松构建HTTP服务,覆盖广泛的客户端,包括浏览器和移动设备。 ASP.NET Web API是在.NET Framework上构建RESTful应用程序的理想平台。其中,RESTful属于一种设计风格,REST中的GET,POST,PUT DELETE来进行数据的增删改查,如果开发人员的应用程序符合RESTful原则,则它的服务称为”RESTful风格应用服务”。

ASP.NET Web API核心的消息处理管道独立于ASP.NET平台,比Asp.NET MVC设计的管道更为复杂,功能也更为强大,支持Web Host和Self Host(任意类型的应用程序,如控制台、Windows Form应用程序、WPF应用甚至Windows Service)两种寄宿方式,本文主要介绍第一种方式。Web API整个生命周期如下图所示。

​

3.手动搭建基本框架

Visual Studio为我们提供了专门用于创建ASP.NET Web API应用的项目模板,借助于此项目模板提供的向导,我们可以“一键式”创建一个完整的ASP.NET Web API项目。在项目创建过程中,Visual studio会自动为我们添加必要的程序集引用和配置,甚至会为我们自动生成相关的代码,总之—句话:这种通过向导生成的项目在被创建之后其本身就是—个可执行的应用。笔者在此就不在演示自动创建的过程,重点讲解手动搭建,这样可以让我们更深入的了解Web API的运行原理。

(1)创建空的ASP.NET Web 应用程序

在VS2017中,选择ASP.NET Web 应用程序(.NET Framework),框架选择.NET Framework4.5,如下图所示。

​

选择空项目,同时去掉MVC及Web API选项,如下图所示。

)

(2)通过NuGet下载安装Microsoft.Asp.Net.Api

右键项目,选择【管理 NuGet 程序包】,搜索WebAPI,选择Microsoft.Asp.Net.Api,点击右侧【安装】按钮,完成安装,如下图所示。

​

(3)添加全局应用程序类Global.asax

右键项目,添加新建项,选择全局应用程序类,如下图所示。

global.asax是一个文本文件,它提供全局可用代码。这些代码包括应用程序的事件处理程序以及会话事件、方法和静态变量。有时该文件也被称为应用程序文件。打开文件,代码如下所示,发现该文件包含了Web应用程序入口Application_Start,这和WinForm应用程序的main函数类似。

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
csharp复制代码namespace MyWebAPI
{
public class Global : System.Web.HttpApplication
{

protected void Application_Start(object sender, EventArgs e)
{

}

protected void Session_Start(object sender, EventArgs e)
{

}

protected void Application_BeginRequest(object sender, EventArgs e)
{

}

protected void Application_AuthenticateRequest(object sender, EventArgs e)
{

}

protected void Application_Error(object sender, EventArgs e)
{

}

protected void Session_End(object sender, EventArgs e)
{

}

protected void Application_End(object sender, EventArgs e)
{

}
}
}

(4)注册Web API路由

路由系统是请求消息进入ASP.NET Web API 消息处理管道的第一道屏障,其根本目的用于解析URL请求,在后续的系列文章中会详细讲解,这里就不深入讲解。

在Application_Start函数中注册Web API路由,代码如下:

1
2
3
4
5
6
7
php复制代码 protected void Application_Start(object sender, EventArgs e)
{
GlobalConfiguration.Configuration.Routes.MapHttpRoute(
name: "DefaultApi",
routeTemplate: "api/{controller}/{id}",
defaults: new { id = RouteParameter.Optional });
}

(5)添加Web API控制器

右键项目,添加新建项,选择Web API控制器类,如下图所示。

​

打开ValuesController.cs文件,发现该类直接继承与ApiController,且包含了GET,POST,PUT DELETE等Action,代码如下所示,控制器在后续系列文章中会详细讲解,这里也不过讲解。

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
csharp复制代码public class ValuesController : ApiController
{
// GET api/<controller>
public IEnumerable<string> Get()
{
return new string[] { "value1", "value2" };
}

// GET api/<controller>/5
public string Get(int id)
{
return "value";
}

// POST api/<controller>
public void Post([FromBody]string value)
{
}

// PUT api/<controller>/5
public void Put(int id, [FromBody]string value)
{
}

// DELETE api/<controller>/5
public void Delete(int id)
{
}
}

(6)调用Web API

运行程序(自动创建IIS服务),在地址栏中输入http://localhost:52317/api/Values调用了ValuesController中的Get()方法,Google浏览显示的调用结果如下所示。

​

4.总结

至此,完成了ASP.NET Web API的基本介绍和手动构建Web API基本框架的详细步骤。通过此博客读者可以更加深入的认识和了解Web API,文中若有不足之处,还望海涵,博文写作不易希望多多支持,后续会更新更多内容,感兴趣的朋友可以加关注,欢迎留言交流!同时,欢迎扫描下方的微信公众号,获取更多干货!!

本文转载自: 掘金

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

面经手册 · 第2篇《数据结构,HashCode为什么使用3

发表于 2020-08-05

作者:小傅哥

博客:bugstack.cn

沉淀、分享、成长,让自己和他人都能有所收获!😄

一、前言

在面经手册的前两篇介绍了《面试官都问我啥》和《认知自己的技术栈盲区》,这两篇内容主要为了说明面试过程的考查范围,包括个人的自我介绍、技术栈积累、项目经验等,以及在技术栈盲区篇章中介绍了一个整套技术栈在系统架构用的应用,以此全方面的扫描自己有哪些盲区还需要补充。而接下来的章节会以各个系列的技术栈中遇到的面试题作为切入点,讲解技术要点,了解技术原理,包括;数据结构、数据算法、技术栈、框架等进行逐步展开学习。

在进入数据结构章节讲解之前可以先了解下,数据结构都有哪些,基本可以包括;数组(Array)、栈(Stack)、队列(Queue)、链表(LinkList)、树(Tree)、散列表(Hash)、堆(Heap)、图(Graph)。

而本文主要讲解的就是与散列表相关的HashCode,本来想先讲HashMap,但随着整理资料发现与HashMap的实现中,HashCode的散列占了很重要的一设计思路,所以最好把这部分知识补全,再往下讲解。

二、面试题

说到HashCode的面试题,可能这是一个非常核心的了。其他考点;怎么实现散列、计算逻辑等,都可以通过这道题的学习了解相关知识。

Why does Java’s hashCode() in String use 31 as a multiplier?

这个问题其实☞指的就是,hashCode的计算逻辑中,为什么是31作为乘数。

三、资源下载

本文讲解的过程中涉及部分源码等资源,可以通过关注公众号:bugstack虫洞栈,回复下载进行获取{回复下载后打开获得的链接,找到编号ID:19},包括;

  1. HashCode 源码测试验证工程,interview-03
  2. 103976个英语单词库.txt,验证HashCode值
  3. HashCode散列分布.xlsx,散列和碰撞图表

四、源码讲解

1. 固定乘积31在这用到了

1
2
3
4
5
6
7
8
9
10
11
12
java复制代码// 获取hashCode "abc".hashCode();
public int hashCode() {
int h = hash;
if (h == 0 && value.length > 0) {
char val[] = value;
for (int i = 0; i < value.length; i++) {
h = 31 * h + val[i];
}
hash = h;
}
return h;
}

在获取hashCode的源码中可以看到,有一个固定值31,在for循环每次执行时进行乘积计算,循环后的公式如下;
s[0]*31^(n-1) + s[1]*31^(n-2) + ... + s[n-1]

那么这里为什么选择31作为乘积值呢?

2. 来自stackoverflow的回答

在stackoverflow关于为什么选择31作为固定乘积值,有一篇讨论文章,Why does Java’s hashCode() in String use 31 as a multiplier? 这是一个时间比较久的问题了,摘取两个回答点赞最多的;

413个赞👍的回答

最多的这个回答是来自《Effective Java》的内容;

1
java复制代码The value 31 was chosen because it is an odd prime. If it were even and the multiplication overflowed, information would be lost, as multiplication by 2 is equivalent to shifting. The advantage of using a prime is less clear, but it is traditional. A nice property of 31 is that the multiplication can be replaced by a shift and a subtraction for better performance: 31 * i == (i << 5) - i. Modern VMs do this sort of optimization automatically.

这段内容主要阐述的观点包括;

  1. 31 是一个奇质数,如果选择偶数会导致乘积运算时数据溢出。
  2. 另外在二进制中,2个5次方是32,那么也就是 31 * i == (i << 5) - i。这主要是说乘积运算可以使用位移提升性能,同时目前的JVM虚拟机也会自动支持此类的优化。

80个赞👍的回答

1
java复制代码As Goodrich and Tamassia point out, If you take over 50,000 English words (formed as the union of the word lists provided in two variants of Unix), using the constants 31, 33, 37, 39, and 41 will produce less than 7 collisions in each case. Knowing this, it should come as no surprise that many Java implementations choose one of these constants.
  • 这个回答就很有实战意义了,告诉你用超过5千个单词计算hashCode,这个hashCode的运算使用31、33、37、39和41作为乘积,得到的碰撞结果,31被使用就很正常了。
  • 他这句话就就可以作为我们实践的指向了。

3. Hash值碰撞概率统计

接下来要做的事情并不难,只是根据stackoverflow的回答,统计出不同的乘积数对10万个单词的hash计算结果。10个单词表已提供,可以通过关注公众号:bugstack虫洞栈进行下载

3.1 读取单词字典表

1
2
3
4
5
6
7
java复制代码1	a	"n.(A)As 或 A's  安(ampere(a) art.一;n.字母A /[军] Analog.Digital,模拟/数字 /(=account of) 帐上"
2 aaal American Academy of Arts and Letters 美国艺术和文学学会
3 aachen 亚琛[德意志联邦共和国西部城市]
4 aacs Airways and Air Communications Service (美国)航路与航空通讯联络处
5 aah " [军]Armored Artillery Howitzer,装甲榴弹炮;[军]Advanced Attack Helicopter,先进攻击直升机"
6 aal "ATM Adaptation Layer,ATM适应层"
7 aapamoor "n.[生]丘泽,高低位镶嵌沼泽"
  • 单词表的文件格式如上,可以自行解析
  • 读取文件的代码比较简单,这里不展示了,可以通过资源下载进行获取

3.2 Hash计算函数

1
2
3
4
5
6
7
java复制代码public static Integer hashCode(String str, Integer multiplier) {
int hash = 0;
for (int i = 0; i < str.length(); i++) {
hash = multiplier * hash + str.charAt(i);
}
return hash;
}
  • 这个过程比较简单,与原hash函数对比只是替换了可变参数,用于我们统计不同乘积数的计算结果。

3.3 Hash碰撞概率计算

想计算碰撞很简单,也就是计算那些出现相同哈希值的数量,计算出碰撞总量即可。这里的实现方式有很多,可以使用set、map也可以使用java8的stream流统计distinct。

1
2
3
4
5
6
7
java复制代码private static RateInfo hashCollisionRate(Integer multiplier, List<Integer> hashCodeList) {
int maxHash = hashCodeList.stream().max(Integer::compareTo).get();
int minHash = hashCodeList.stream().min(Integer::compareTo).get();
int collisionCount = (int) (hashCodeList.size() - hashCodeList.stream().distinct().count());
double collisionRate = (collisionCount * 1.0) / hashCodeList.size();
return new RateInfo(maxHash, minHash, multiplier, collisionCount, collisionRate);
}
  • 这里记录了最大hash和最小hash值,以及最终返回碰撞数量的统计结果。

3.4 单元测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
java复制代码@Before
public void before() {
"abc".hashCode();
// 读取文件,103976个英语单词库.txt
words = FileUtil.readWordList("E:/itstack/git/github.com/interview/interview-01/103976个英语单词库.txt");
}

@Test
public void test_collisionRate() {
List<RateInfo> rateInfoList = HashCode.collisionRateList(words, 2, 3, 5, 7, 17, 31, 32, 33, 39, 41, 199);
for (RateInfo rate : rateInfoList) {
System.out.println(String.format("乘数 = %4d, 最小Hash = %11d, 最大Hash = %10d, 碰撞数量 =%6d, 碰撞概率 = %.4f%%", rate.getMultiplier(), rate.getMinHash(), rate.getMaxHash(), rate.getCollisionCount(), rate.getCollisionRate() * 100));
}
}
  • 以上先设定读取英文单词表中的10个单词,之后做hash计算。
  • 在hash计算中把单词表传递进去,同时还有乘积数;2, 3, 5, 7, 17, 31, 32, 33, 39, 41, 199,最终返回一个list结果并输出。
  • 这里主要验证同一批单词,对于不同乘积数会有怎么样的hash碰撞结果。

测试结果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
java复制代码单词数量:103976
乘数 = 2, 最小Hash = 97, 最大Hash = 1842581979, 碰撞数量 = 60382, 碰撞概率 = 58.0730%
乘数 = 3, 最小Hash = -2147308825, 最大Hash = 2146995420, 碰撞数量 = 24300, 碰撞概率 = 23.3708%
乘数 = 5, 最小Hash = -2147091606, 最大Hash = 2147227581, 碰撞数量 = 7994, 碰撞概率 = 7.6883%
乘数 = 7, 最小Hash = -2147431389, 最大Hash = 2147226363, 碰撞数量 = 3826, 碰撞概率 = 3.6797%
乘数 = 17, 最小Hash = -2147238638, 最大Hash = 2147101452, 碰撞数量 = 576, 碰撞概率 = 0.5540%
乘数 = 31, 最小Hash = -2147461248, 最大Hash = 2147444544, 碰撞数量 = 2, 碰撞概率 = 0.0019%
乘数 = 32, 最小Hash = -2007883634, 最大Hash = 2074238226, 碰撞数量 = 34947, 碰撞概率 = 33.6106%
乘数 = 33, 最小Hash = -2147469046, 最大Hash = 2147378587, 碰撞数量 = 1, 碰撞概率 = 0.0010%
乘数 = 39, 最小Hash = -2147463635, 最大Hash = 2147443239, 碰撞数量 = 0, 碰撞概率 = 0.0000%
乘数 = 41, 最小Hash = -2147423916, 最大Hash = 2147441721, 碰撞数量 = 1, 碰撞概率 = 0.0010%
乘数 = 199, 最小Hash = -2147459902, 最大Hash = 2147480320, 碰撞数量 = 0, 碰撞概率 = 0.0000%

Process finished with exit code 0

公众号:bugstack虫洞栈,hash碰撞图表

以上就是不同的乘数下的hash碰撞结果图标展示,从这里可以看出如下信息;

  1. 乘数是2时,hash的取值范围比较小,基本是堆积到一个范围内了,后面内容会看到这块的展示。
  2. 乘数是3、5、7、17等,都有较大的碰撞概率
  3. 乘数是31的时候,碰撞的概率已经很小了,基本稳定。
  4. 顺着往下看,你会发现199的碰撞概率更小,这就相当于一排奇数的茅坑量多,自然会减少碰撞。但这个范围值已经远超过int的取值范围了,如果用此数作为乘数,又返回int值,就会丢失数据信息。

4. Hash值散列分布

除了以上看到哈希值在不同乘数的一个碰撞概率后,关于散列表也就是hash,还有一个非常重要的点,那就是要尽可能的让数据散列分布。只有这样才能减少hash碰撞次数,也就是后面章节要讲到的hashMap源码。

那么怎么看散列分布呢?如果我们能把10万个hash值铺到图表上,形成的一张图,就可以看出整个散列分布。但是这样的图会比较大,当我们缩小看后,就成一个了大黑点。所以这里我们采取分段统计,把2 ^ 32方分64个格子进行存放,每个格子都会有对应的数量的hash值,最终把这些数据展示在图表上。

4.1 哈希值分段存放

1
2
3
4
5
6
7
8
9
10
11
java复制代码public static Map<Integer, Integer> hashArea(List<Integer> hashCodeList) {
Map<Integer, Integer> statistics = new LinkedHashMap<>();
int start = 0;
for (long i = 0x80000000; i <= 0x7fffffff; i += 67108864) {
long min = i;
long max = min + 67108864;
// 筛选出每个格子里的哈希值数量,java8流统计;https://bugstack.cn/itstack-demo-any/2019/12/10/%E6%9C%89%E7%82%B9%E5%B9%B2%E8%B4%A7-Jdk1.8%E6%96%B0%E7%89%B9%E6%80%A7%E5%AE%9E%E6%88%98%E7%AF%87(41%E4%B8%AA%E6%A1%88%E4%BE%8B).html
int num = (int) hashCodeList.parallelStream().filter(x -> x >= min && x < max).count();
statistics.put(start++, num);
}
return statistics;
  • 这个过程主要统计int取值范围内,每个哈希值存放到不同格子里的数量。
  • 这里也是使用了java8的新特性语法,统计起来还是比较方便的。

4.2 单元测试

1
2
3
4
5
6
7
8
java复制代码@Test
public void test_hashArea() {
System.out.println(HashCode.hashArea(words, 2).values());
System.out.println(HashCode.hashArea(words, 7).values());
System.out.println(HashCode.hashArea(words, 31).values());
System.out.println(HashCode.hashArea(words, 32).values());
System.out.println(HashCode.hashArea(words, 199).values());
}
  • 这里列出我们要统计的乘数值,每一个乘数下都会有对应的哈希值数量汇总,也就是64个格子里的数量。
  • 最终把这些统计值放入到excel中进行图表化展示。

统计图表

公众号:bugstack虫洞栈,hash散列表

  • 以上是一个堆积百分比统计图,可以看到下方是不同乘数下的,每个格子里的数据统计。
  • 除了199不能用以外,31的散列结果相对来说比较均匀。
4.2.1 乘数2散列

  • 乘数是2的时候,散列的结果基本都堆积在中间,没有很好的散列。
4.2.2 乘数31散列

  • 乘数是31的时候,散列的效果就非常明显了,基本在每个范围都有数据存放。
4.2.3 乘数199散列

  • 乘数是199是不能用的散列结果,但是它的数据是更加分散的,从图上能看到有两个小山包。但因为数据区间问题会有数据丢失问题,所以不能选择。

文中引用

  • www.tianxiaobo.com/2018/01/18/…
  • stackoverflow.com/questions/2…

五、总结

  • 以上主要介绍了hashCode选择31作为乘数的主要原因和实验数据验证,算是一个散列的数据结构的案例讲解,在后续的类似技术中,就可以解释其他的源码设计思路了。
  • 看过本文至少应该让你可以从根本上解释了hashCode的设计,关于他的所有问题也就不需要死记硬背了,学习编程内容除了最开始的模仿到深入以后就需要不断的研究数学逻辑和数据结构。
  • 文中参考了优秀的hashCode资料和stackoverflow,并亲自做实验验证结果,大家也可以下载本文中资源内容;英文字典、源码、excel图表等内容。

六、推荐阅读

  • 面经手册 · 开篇《面试官都问我啥》
  • 工作两年简历写成这样,谁要你呀!
  • 讲道理,只要你是一个爱折腾的程序员,毕业找工作真的不需要再花钱培训!
  • 大学四年到毕业工作5年的学习路线资源汇总
  • 手写mybait-spring核心功能(干货好文一次学会工厂bean、类代理、bean注册的使用)
  • 源码分析 | Mybatis接口没有实现类为什么可以执行增删改查

本文转载自: 掘金

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

你的 Git 还在用小乌龟吗?

发表于 2020-08-04

作者爱讲话

八月伊始,杭州也越来越热的,但是看着明媚的天气,心情还是很好的。


最近看了《乐队的夏天》,被 Muma木马 乐队,疯狂圈粉,乐队主唱木玛说:“木马就像一个被我们弄坏的玩具,现在我们把它找回来了,是一个新的木马”(背景:Muma木马乐队解散过)

一下子,就想起自己大学和几个学长组过的乐队,地下室排练的时光还是很简单很快乐的,但是可能大学最大的遗憾也就是没有把这个乐队坚持到底吧。

说回 Muma木马,听了 Muma木马的《旧城之王》,感觉的歌词写的很有意思,非常的抽象,上了知乎有很多说歌词是故意拼凑,但是我还是觉得应该是作者有自己的想法,以一种真诚又戏谑的方式和时代对峙

所有繁华落尽剩一人为王,在时代的漩涡中丢盔卸甲,但无论何时也不曾忘记,我们常常幻想的英雄其实住在自己心里 -Muma木马


Git 引入

王经理🤓:郝美丽,你上周写的代码怎么不提交到分支上?

小美:经理,小乌龟被扔来扔去,我感觉他太可怜了。呜呜

小美的乌龟

小美的乌龟

王经理:???

小林:😍😍😍😍😍

王经理看着小美可怜的眼神,感觉小美应该是真的心疼乌龟 :“咳咳,为了照顾小美,那我们今天来说下如何在 idea 里使用 git ,大家打开我们的 idea,来 clone 一下这个项目,项目地址是这个
https://github.com/maerduduqi/test

郝美丽 :经理,你人太好了,不虐待乌龟,从我们做起

王经理:emmm

王经理:林步动,叫你打开 idea ,你开谷歌浏览器干嘛?

小林:经理,你不是叫我 clone 项目嘛,我登上去下载这个项目下来,然后解压,然后在 idea 里打开啊

1,在 Idea 里 clone 项目

王经理:你傻的在我的想象之中,又在我的意料之外

我们可以在 Idea 里直接打开我们这个项目

点击 File -> New -> Project From Version Control


ok,现在这个项目已经被我们 clone 下来了

王经理:我问你们个问题,我们使用 Git 的最大目的是什么?

郝美丽 & 林步动:没有蛀牙! (这个梗,能 Get 到么 😂)

2,在 Idea 里 commit and push

王经理:对,就是提交我们写的代码与其他人一起协作,就是 commit and push,在 Idea 里使用这个操作也很简单


小林:经理,我打断下你,是不是文件只有蓝色和红色啊?有没有绿色,我最喜欢绿色了,小美和我说,要想生活过得去,身上总得带点绿,我觉得小美说的很有道理啊。

经理向小林投来疑问的目光


的确有绿色,还有灰色和白色

  • 绿色,代表代码文件已经加入版本控制但是暂未提交
  • 白色,代表代码文件加入版本控制,已提交,无改动
  • 灰色:代表代码文件是已经被版本控制已忽略文件

说回提交文件


林步动:哈哈哈,好简单啊,是不是这样就行了

王经理:还差最后一步,就 push 成功了


王经理:ok,我们现在已经 push 成功了

3,在 Idea 里查看提交记录

林步动:经理,我想看看小美提交了啥,我应该怎么看啊

王经理:这个也很简单,看我操作


小林:what ,小美说我笨,我能不能撤回这个提交记录?

王经理:可以的。只要这样就能撤回了



4,在 Idea 里解决冲突


小林:经理这一手可以。经理你能不能给我展示下 idea 如何解决冲突模拟场景

王经理:当然可以

假设有另个开发人员开发同一个项目,并且编写同一个文件,工作流程如下:

1.01号程序员先上传文件conflict.txt,并继续在conflict.txt上写代码;


2.02号程序员更新项目代码,并在conflict.txt上写代码,写完后,在提交到远程服务端;


3.当01号程序员把写完后,准备提交代码了,这时的正规操作手法,先更新在提交,但是在更新的时候必然会冲突,因为这时候更新的代码conflict.txt与本地仓库代码conflict.txt不一致


提交前,我要更新,冲突了:


解决方案如下:

  • accept yours:代表以自己的为准;
  • accept theris:代表以更新下来的文件为准;
  • merge:代表手动合并
  • 一般解决冲突我们都是选择merge


将需要的内容点击:”>>”既可以合并内容到result中,不需要的内容点击“x”即可,合并完成后点击apply即可。

值得注意的是,最将所有的“x >>”符号都要处理完,不需要的点击“x”,需要的点击“>>”

小林:厉害了啊,经理 🤤


小美:经理,我也想问一个问题

王经理:问问问,就没有我不会的


5,在 Idea 里使用分支


小美:能不能教教我 在 Idea 里使用 git 分支的技巧啊?

王经理:小菜一碟,接下来给大家模拟下 分支的新建与合并使用 场景 (摘自作者姿势帝文章,文末附原文链接)

1,开发某个网站。

2,为实现某个新的需求、问题(#53问题),创建一个分支(名为:iss53)。

3,在这个分支上开展工作。

正在此时,你突然接到一个电话说有个很严重的问题需要紧急修补。 你将按照如下方式来处理:

1,切换到你的线上分支(production branch)。

2,为这个紧急任务新建一个分支(名为:hotfix),并在其中修复它。

3,在测试通过之后,切换回线上分支(名为:master),然后合并这个修补分支,最后将改动推送到线上分支,并删除hotfix分支。

4,切换回你最初工作的分支(iss53)上,继续工作。

5,iss53问题处理完后,合并到master主干上,删除iss53分支。

当我们决定去为实现某个新的需求,如何新建分支呢?


填写分支名称


在iss53分支上开发,如下


将分支推送到远程仓库


点击push推送到远程仓库


分支随着工作的进展向前推进

现在你接到那个电话,有个紧急问题等待你来解决。

有了 Git 的帮助,你不必把这个紧急问题和 iss53 的修改混在一起,

你也不需要花大力气来还原关于 53# 问题的修改,然后再添加关于这个紧急问题的修改,最后将这个修改提交到线上分支。 你所要做的仅仅是切换回 master 分支。

idea上操作如下:


特别注意:在你这么做之前,要留意你的工作目录和暂存区里那些还没有被提交的修改,它可能会和你即将检出的分支产生冲突从而阻止 Git 切换到该分支。 最好的方法是,在你切换分支之前,保持好一个干净的状态。

有一些方法可以绕过这个问题(即,保存进度(stashing) 和 修补提交(commit amending))。

这个时候,你的工作目录和你在开始 #53 问题之前一模一样,现在你可以专心修复紧急问题了。

请牢记:当你切换分支的时候,Git 会重置你的工作目录,使其看起来像回到了你在那个分支上最后一次提交的样子。

Git 会自动添加、删除、修改文件以确保此时你的工作目录和这个分支最后一次提交时的样子一模一样。

接下来,你要修复这个紧急问题。 让我们建立一个针对该紧急问题的分支(hotfix branch),在该分支上工作直到问题解决:


基于 master 分支的紧急问题分支 hotfix上进行代码开发,模拟如下:


你可以运行你的测试,确保你的修改是正确的,然后提交代码到远程仓库,提交到远程仓库的操作与刚才提交iss53操作一样。

当hotfix这个紧急问题的分支开发完成后,将其合并回你的 master 分支来部署到线上。 你可以使用idea的 git merge来达到上述目的:

首先切换到master

然后,以master为主线合并hotfix,这个很重要,因为是以master为主,将hotfix的的代码合并到master上,不要把顺序弄反


然后,以master为主线合并hotfix,这个很重要,因为是以master为主,将hotfix的的代码合并到master上,不要把顺序弄返


现在,最新的修改已经在 master 分支所指向的提交快照中,这是你只需要提交master到远程仓库(非常重要,千万别忘记),你可以着手发布该修复了。

当我们顺利的合并到了master主线上,接下来我们就应该删除分支hotfix

删除本地仓库分支hotfix


删除远程仓库分支hotfix


删完回到我们的分支上


继续在分支iss53上写代码


总结一下


王经理:尖叫声在哪里?


小林:都学完谁还理你,下班时间到啦


关于 Idea 里使用 Git 的技巧肯定不止这么一点点,希望大家触类旁通,多试一试,每个按钮都点一点。

毕竟鲁迅说过:git 这东西,多用用就知道了


参考文章:
https://www.cnblogs.com/newAndHui/p/10846276.html


本文转载自: 掘金

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

厉害啊!第一次见到把Shiro运行流程写的这么清楚的,建议收

发表于 2020-08-04

前言

shiro是apache的一个开源框架,是一个权限管理的框架,实现 用户认证、用户授权。
spring中有spring security (原名Acegi),是一个权限框架,它和spring依赖过于紧密,没有shiro使用简单。
shiro不依赖于spring,shiro不仅可以实现 web应用的权限管理,还可以实现c/s系统,分布式系统权限管理,shiro属于轻量框架,越来越多企业项目开始使用shiro。

Shiro运行流程学习笔记

项目中使用到了shiro,所以对shiro做一些比较深的了解。

也不知从何了解起,先从shiro的运行流程开始。

运行流程

  1. 首先调用 Subject.login(token) 进行登录,其会自动委托给 Security Manager,调用之前必须通过 SecurityUtils.setSecurityManager() 设置;
  2. SecurityManager 负责真正的身份验证逻辑;它会委托给 Authenticator 进行身份验证;
  3. Authenticator 才是真正的身份验证者,Shiro API 中核心的身份认证入口点,此处可以自定义插入自己的实现;
  4. Authenticator 可能会委托给相应的 AuthenticationStrategy 进行多 Realm 身份验证,默认 ModularRealmAuthenticator 会调用 AuthenticationStrategy 进行多 Realm 身份验证;
  5. Authenticator 会把相应的 token 传入 Realm,从 Realm 获取身份验证信息,如果没有返回 / 抛出异常表示身份验证失败了。此处可以配置多个 Realm,将按照相应的顺序及策略进行访问。

绑定线程

这里从看项目源码开始。

看第一步,Subject.login(token)方法。

1
2
3
ini复制代码UsernamePasswordToken token = new UsernamePasswordToken(username, password, rememberMe);
Subject subject = SecurityUtils.getSubject();
subject.login(token);

出现了一个UsernamePasswordToken对象,它在这里会调用它的一个构造函数。

1
2
3
java复制代码public UsernamePasswordToken(final String username, final String password, final boolean rememberMe) {
this(username, password != null ? password.toCharArray() : null, rememberMe, null);
}

据笔者自己了解,这是shiro的一个验证对象,只是用来存储用户名密码,以及一个记住我属性的。

之后会调用shiro的一个工具类得到一个subject对象。

1
2
3
4
5
6
7
8
ini复制代码public static Subject getSubject() {
Subject subject = ThreadContext.getSubject();
if (subject == null) {
subject = (new Subject.Builder()).buildSubject();
ThreadContext.bind(subject);
}
return subject;
}

通过getSubject方法来得到一个Subject对象。

这里不得不提到shiro的内置线程类ThreadContext,通过bind方法会将subject对象绑定在线程上。

1
2
3
4
5
scss复制代码public static void bind(Subject subject) {
if (subject != null) {
put(SUBJECT_KEY, subject);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
scss复制代码public static void put(Object key, Object value) {
if (key == null) {
throw new IllegalArgumentException("key cannot be null");
}

if (value == null) {
remove(key);
return;
}

ensureResourcesInitialized();
resources.get().put(key, value);

if (log.isTraceEnabled()) {
String msg = "Bound value of type [" + value.getClass().getName() + "] for key [" +
key + "] to thread " + "[" + Thread.currentThread().getName() + "]";
log.trace(msg);
}
}

且shiro的key都是遵循一个固定的格式。

1
arduino复制代码public static final String SUBJECT_KEY = ThreadContext.class.getName() + "_SUBJECT_KEY";

经过非空判断后会将值以KV的形式put进去。

当你想拿到subject对象时,也可以通过getSubject方法得到subject对象。

在绑定subject对象时,也会将securityManager对象进行一个绑定。

而绑定securityManager对象的地方是在Subject类的一个静态内部类里(可让我好一顿找)。

在getSubject方法中的一句代码调用了内部类的buildSubject方法。

1
ini复制代码subject = (new Subject.Builder()).buildSubject();

PS:此处运用到了建造者设计模式,可以去菜鸟教程仔细了解,

进去观看源码后可以看见。

首先调用无参构造,在无参构造里调用有参构造函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
csharp复制代码public Builder() {
this(SecurityUtils.getSecurityManager());
}

public Builder(SecurityManager securityManager) {
if (securityManager == null) {
throw new NullPointerException("SecurityManager method argument cannot be null.");
}
this.securityManager = securityManager;
this.subjectContext = newSubjectContextInstance();
if (this.subjectContext == null) {
throw new IllegalStateException("Subject instance returned from 'newSubjectContextInstance' " +
"cannot be null.");
}
this.subjectContext.setSecurityManager(securityManager);
}

在此处绑定了securityManager对象。

当然,他也对securityManager对象的空状况进行了处理,在getSecurityManager方法里。

1
2
3
4
5
6
7
8
9
10
11
12
13
ini复制代码public static SecurityManager getSecurityManager() throws UnavailableSecurityManagerException {
SecurityManager securityManager = ThreadContext.getSecurityManager();
if (securityManager == null) {
securityManager = SecurityUtils.securityManager;
}
if (securityManager == null) {
String msg = "No SecurityManager accessible to the calling code, either bound to the " +
ThreadContext.class.getName() + " or as a vm static singleton. This is an invalid application " +
"configuration.";
throw new UnavailableSecurityManagerException(msg);
}
return securityManager;
}

真正的核心就在于securityManager这个对象。

SecurityManager

SecurityManager是一个接口,他继承了步骤里所谈到的Authenticator,Authorizer类以及用于Session管理的SessionManager。

1
2
3
4
5
6
7
8
java复制代码public interface SecurityManager extends Authenticator, Authorizer, SessionManager {

Subject login(Subject subject, AuthenticationToken authenticationToken) throws AuthenticationException;

void logout(Subject subject);

Subject createSubject(SubjectContext context);
}

看一下它的实现。

且这些类和接口都有依次继承的关系。

Relam

接下来了解一下另一个重要的概念Relam。

Realm充当了Shiro与应用安全数据间的“桥梁”或者“连接器”。也就是说,当与像用户帐户这类安全相关数据进行交互,执行认证(登录)和授权(访问控制)时,Shiro会从应用配置的Realm中查找很多内容。

从这个意义上讲,Realm实质上是一个安全相关的DAO:它封装了数据源的连接细节,并在需要时将相关数据提供给Shiro。当配置Shiro时,你必须至少指定一个Realm,用于认证和(或)授权。配置多个Realm是可以的,但是至少需要一个。

Shiro内置了可以连接大量安全数据源(又名目录)的Realm,如LDAP、关系数据库(JDBC)、类似INI的文本配置资源以及属性文件 等。如果缺省的Realm不能满足需求,你还可以插入代表自定义数据源的自己的Realm实现。

一般情况下,都会自定义Relam来使用。

先看一下实现。

以及自定义的一个UserRelam。

看一下类图。

每个抽象类继承后所需要实现的方法都不一样。

1
scala复制代码public class UserRealm extends AuthorizingRealm

这里继承AuthorizingRealm,需要实现它的两个方法。

1
2
3
4
5
java复制代码//给登录用户授权
protected abstract AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals);

//这个抽象方法属于AuthorizingRealm抽象类的父类AuthenticatingRealm类 登录认证,也是登录的DAO操作所在的方法
protected abstract AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException;

之后再来看看这个验证方法,在之前的步骤里提到了,验证用到了Authenticator ,也就是第五步。

Authenticator

Authenticator 会把相应的 token 传入 Realm,从 Realm 获取身份验证信息,如果没有返回 / 抛出异常表示身份验证失败了。此处可以配置多个 Realm,将按照相应的顺序及策略进行访问。

再回到之前登录方法上来看看。

subject.login(token)在第一步中调用了Subject的login方法,找到它的最终实现DelegatingSubject类。

里面有调用了securityManager的login方法,而最终实现就在DefaultSecurityManager这个类里。

1
ini复制代码Subject subject = securityManager.login(this, token);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
java复制代码public Subject login(Subject subject, AuthenticationToken token) throws AuthenticationException {
AuthenticationInfo info;
try {
info = authenticate(token);
} catch (AuthenticationException ae) {
try {
onFailedLogin(token, ae, subject);
} catch (Exception e) {
if (log.isInfoEnabled()) {
log.info("onFailedLogin method threw an " +
"exception. Logging and propagating original AuthenticationException.", e);
}
}
throw ae; //propagate
}

之后就是验证流程,这里我们会看到第四步,点进去会到抽象类AuthenticatingSecurityManager。再看看它的仔细调用。

1
2
3
java复制代码public AuthenticationInfo authenticate(AuthenticationToken token) throws AuthenticationException {
return this.authenticator.authenticate(token);
}

真正的调用Relam进行验证并不在这,而是在ModularRealmAuthenticator。

他们之间是一个从左到右的过程。

1
2
3
4
5
6
7
8
9
scss复制代码protected AuthenticationInfo doAuthenticate(AuthenticationToken authenticationToken) throws AuthenticationException {
assertRealmsConfigured();
Collection<Realm> realms = getRealms();
if (realms.size() == 1) {
return doSingleRealmAuthentication(realms.iterator().next(), authenticationToken);
} else {
return doMultiRealmAuthentication(realms, authenticationToken);
}
}

在这里咱们就看这个doSingleRealmAuthentication方法。

单Relam验证。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
java复制代码protected AuthenticationInfo doSingleRealmAuthentication(Realm realm, AuthenticationToken token) {
if (!realm.supports(token)) {
String msg = "Realm [" + realm + "] does not support authentication token [" +
token + "]. Please ensure that the appropriate Realm implementation is " +
"configured correctly or that the realm accepts AuthenticationTokens of this type.";
throw new UnsupportedTokenException(msg);
}
//在此处调用你自定义的Relam的方法来验证。
AuthenticationInfo info = realm.getAuthenticationInfo(token);
if (info == null) {
String msg = "Realm [" + realm + "] was unable to find account data for the " +
"submitted AuthenticationToken [" + token + "].";
throw new UnknownAccountException(msg);
}
return info;
}

再看看多Relam的。

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
ini复制代码protected AuthenticationInfo doMultiRealmAuthentication(Collection<Realm> realms, AuthenticationToken token) {

AuthenticationStrategy strategy = getAuthenticationStrategy();

AuthenticationInfo aggregate = strategy.beforeAllAttempts(realms, token);

for (Realm realm : realms) {

aggregate = strategy.beforeAttempt(realm, token, aggregate);

if (realm.supports(token)) {

AuthenticationInfo info = null;
Throwable t = null;
try {
//调用自定义的Relam的方法来验证。
info = realm.getAuthenticationInfo(token);
} catch (Throwable throwable) {
t = throwable;
}

aggregate = strategy.afterAttempt(realm, token, info, aggregate, t);

} else {
log.debug("Realm [{}] does not support token {}. Skipping realm.", realm, token);
}
}

aggregate = strategy.afterAllAttempts(token, aggregate);

return aggregate;
}

会发现调用的都是Relam的getAuthenticationInfo方法。

看到了熟悉的UserRelam,此致,闭环了。

但是也只是了解了大概的流程,对每个类的具体作用并不是很了解,所以笔者还是有很多地方要去学习,不,应该说我本来就是菜鸡,就要学才能变带佬。

最后

大家看完有什么不懂的可以在下方留言讨论,也可以关注我私信问我,我看到后都会回答的。也欢迎大家关注我的公众号:前程有光,马上金九银十跳槽面试季,整理了1000多道将近500多页pdf文档的Java面试题资料放在里面,助你圆梦BAT!文章都会在里面更新,整理的资料也会放在里面。谢谢你的观看,觉得文章对你有帮助的话记得关注我点个赞支持一下!

本文转载自: 掘金

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

「总结篇」别再说自己不会JVM了,看完这篇能和面试官扯上半小

发表于 2020-08-04

前言

如果本篇文章有错,欢迎各路大神疯狂diss~~当然喽,如果你看了这篇文章有所收获,那就疯狂点赞吧,你的点赞就是对我的最大鼓励。可以顺便加个关注哦,回家不迷路,不定期更新博客~~

周志明那本《深入理解 JAVA 虚拟机》翻了一遍又一遍,终于鼓起勇气在这里写下关于 JVM 的博客!!!现在,我要开始把我所理解到的记录在这里,和各位朋友一起分享!!!

我相信点开这篇文章的小伙伴一定知道JVM是啥了吧?What,还不知道?好吧,看看维基我想你应该就会明白了:Java虚拟机- 维基百科,自由的百科全书

不过,作为一个爱思考的在校大学生,我也总结了以下三点:

  • 一个能够运行字节码的虚拟机。
  • 屏蔽了具体的操作系统的信息。
  • 正是以上两点,使得Java程序具有一次编译,到处执行的特性。

关于JVM是什么的介绍就到这里,还是老样子,先来看看这篇文章的结构:

运行时数据区域

什么是运行时数据区域?

Java程序在运行时,会为JVM单独划出一块内存区域,而这块内存区域又可以再次划分出一块运行时数据区,运行时数据区域大致可以分为五个部分:
在这里插入图片描述

从上面的图中,有两种颜色不同的区域,红色的是线程共享区域,绿色的是线程私有区域。下面我们一个一个讲清楚,不过在学习这部分的时候,最好先思考为什么会有这些区域。难道是因为存在即合理?

堆(Heap)

很多做开发的同学,会格外关注堆和栈,这是不是就从另一个角度说明了堆和栈的重要性?既然如此,我们就从同学们关注的点开始说。(贴心吧,是不是感觉眼角再一次被打湿?)

先把干货放上来,首先,Java堆区具有下面几个特点:

  • 存储的是我们new来的对象,不存放基本类型和对象引用。
  • 由于创建了大量的对象,垃圾回收器主要工作在这块区域。
  • 线程共享区域,因此是线程不安全的。
  • 能够发生OutOfMemoryError。

其实,Java堆区还可以划分为新生代和老年代,新生代又可以进一步划分为Eden区、Survivor 1区、Survivor 2区。具体比例参数的话,可以看一下下面这张图。

在这里插入图片描述

我想图中已经解释相当清楚了,就没有必要文字说明了吧?关于Java堆对象的创建,以及何时会发生内存泄漏,我后面应该会专门写一篇文章,这里的话就只是一些理论介绍。

虚拟机栈(VM Stack)

Java虚拟机栈也是一块被开发者重点关注的地方,同样,先把干货放上来:

  • 线程私有区域,每一个线程都有独享一个虚拟机栈,因此这是线程安全的区域。
  • 存放基本数据类型以及对象的引用。
  • 每一个方法执行的时候会在虚拟机栈中创建一个相应栈帧,方法执行完毕后该栈帧就会被销毁。方法栈帧是以先进后出的方式虚拟机栈的。
  • 每一个栈帧又可以划分为局部变量表、操作数栈、动态链接、方法出口以及额外的附加信息。
  • 这个区域可能有两种异常:如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError异常(通常是递归导致的);JVM动态扩展时无法申请到足够内存则抛出OutOfMemoryError异常。

同样,这篇文章反响好的话,实战练习后面单独出文章。

本地方法栈(Native Method Stack)

本地方法栈其实可以和Java虚拟机栈进行对比理解,唯一不同的是本地方法栈是Java程序在调用本地方法的时候创建栈帧的地方。和JVM栈一样,这个区域也会抛出StackOverflowError和OutOfMemoryError。

方法区(Method Area)

方法区,也应该是以一块被重点关注的区域。同样,方法区的主要特点如下:

  • 线程共享区域,因此这是线程不安全的区域。
  • 方法区也是一个可能会发生OutOfMemoryError的区域。
  • 方法区存储的是从Class文件加载进来的静态变量、类信息、常量池以及编译器编译后的代码。

对于方法区,我觉得重点应该说一下常量池。常量池可以分为Class文件常量池以及运行时常量池,Java程序运行后,Class文件中的信息被字节码执行引擎加载到了方法区,从而形成了运行时常量池。

另外,说起方法区,可能还有人会把它与永久代、元空间混为一谈。那么他们之间的区别到底是什么?方法区是Java虚拟机规范中的定义,是一种规范,而永久代是一种实现,一个是标准一个是实现。不过Java 8以后就没有永久代这个说法了,元空间取代了永久代。

程序计数器(Program Counter Register)

程序计数器非常简单,想必大家都不是Java的初学者了,也都应该明白一点线程与进程的概念?(灵魂拷问,你明白么?)不明白没关系,我一句话给你讲清楚。

进程是资源分配的最小单位,线程是CPU调度的最小单位,一个进程可以包含多个线程, Java线程通过抢占的方法获得CPU的执行权。现在可以思考下面这个场景。

某一次,线程A获得CPU的执行权,开始执行内部程序。但是线程A的程序还没有执行完,在某一时刻CPU的执行权被另一个线程B抢走了。后来经过线程A的不懈努力,又抢回了CPU的执行权,那么线程A的程序又要从头开始执行?

这个时候程序计数器就粉墨登场了,它的作用就是记录当前线程所执行的位置。 这样,当线程重新获得CPU的执行权的时候,就直接从记录的位置开始执行,分支、循环、跳转、异常处理也都依赖这个程序计数器来完成。此外,程序计数器还具有以下特点:

  • 线程私有,每一个线程都有一个程序计数器,因此它是线程安全的。
  • 唯一一块不存在OutOfMemoryError的区域,可能是设计者觉得没必要。

对象的创建与访问

对象的创建

前面我们已经说过,对象是在堆中创建的,通常只需要new一个就行了。难道就是这么简单?确实没有这么简单,就单单是这样的new关键字,Java虚拟内部进行了一系列的sao操作。

当虚拟机遇到字节码new指令时,就会去运行时常量池寻找该实例化对象相对应的类是否被加载、解析和初始化。如果没有被加载,就会先加载该类的信息,否则就为新生对象分配内存。

分配内存无非有两种方法:

  • 指针碰撞:通过一个类似于指针的东西为对象分配内存,前提是堆空间是相对规整的。
  • 空闲列表:堆空间不规整,使用一个列表记录了哪些空间是空闲的,分配内存的时候会更新列表。

以上是两种不同的方法,至于虚拟机使用哪一种方法,这个就取决虚拟机的类型了。

对象的内存布局

对象在堆中的存储布局可以分为三个部分:

  • 对象头
+ 第一类信息:存储对象自身的运行时数据,例如哈希码、GC分代年龄、锁状态标志等等。
+ 第二类信息:指针类型,Java虚拟机通过这个指针来确定该对象是那个类的实例。
  • 实例数据:对象真正存储的有效信息。
  • 对齐填充:没有实际的意义,起着占位符的作用。

对象的访问定位

我们前面说到过,Java虚拟机栈中存储的是基本数据类型和对象引用。基本数据类型我们已经很清楚了,那么,这个对象引用又是什么鬼?

是这样的,对象实例存储在Java堆中,通过这个对象引用我们就可以找到对象在堆中的位置。但是,对于如何定位到这个对象,不同的Java虚拟机又有不同的方法。

通常情况下,有下面两种方法:

  • 使用句柄访问,通常会在Java堆中划分一块句柄池。
  • 使用直接指针,这样Java虚拟机栈中存储的就是该对象在堆中的地址。

使用句柄访问对象
使用直接指针访问对象

这两种访问对象的方法各有优势。使用直接指针进行访问,就可以直接定位到对象,减小了一次指针定位的时间开销(使用句柄的话会通过句柄池的指针二次定位对象),最大的好处就是速度更快。但是使用句柄的话,就是当对象发生移动的时候,可以不用改变栈中存储的reference,只需要改变句柄池中实例数据的指针。

垃圾收集算法

论对象已死?

前面一部分我们都在讲对象,一个对象能够被创建,那么这个对象在什么时候被销毁了?通常,判断一个对象是否被销毁有两种方法:

  • 引用计数算法: 为对象添加一个引用计数器,每当对象在一个地方被引用,则该计数器加1;每当对象引用失效时,计数器减1。但计数器为0的时候,就表白该对象没有被引用。
  • 可达性分析算法: 通过一系列被称之为“GC Roots”的根节点开始,沿着引用链进行搜索,凡是在引用链上的对象都不会被回收。

就像上图的那样,绿色部分的对象都在GC Roots的引用链上,就不会被垃圾回收器回收,灰色部分的对象没有在引用链上,自然就被判定为可回收对象。

那么,问题来了,这个GC Roots又是什么?下面列举可以作为GC Roots的对象:

  • Java虚拟机栈中被引用的对象,各个线程调用的参数、局部变量、临时变量等。
  • 方法区中类静态属性引用的对象,比如引用类型的静态变量。
  • 方法区中常量引用的对象。
  • 本地方法栈中所引用的对象。
  • Java虚拟机内部的引用,基本数据类型对应的Class对象,一些常驻的异常对象。
  • 被同步锁(synchronized)持有的对象。

现在,我们已经知道哪些对像是可以回收的。那么又要采取什么方式对对象进行回收呢?垃圾回收算法主要有三种,依次是标记-清除算法、标记-复制算法、标记-整理算法。这三种垃圾收集算法其实也比较容易理解,下面我先介绍概念,然后在依次总结一下。

标记–清除算法

见名知义,标记–清除算法就是对无效的对象进行标记,然后清除。如下图:

对于标记–清除算法,你一定会清楚看到,在进行垃圾回收之后,堆空间有大量的碎片,出现了不规整的情况。在给大对象分配内存的时候,由于无法找到足够的连续的内存空间,就不得不再一次触发垃圾收集。另外,如果Java堆中存在大量的垃圾对象,那么垃圾回收的就必然进行大量的标记和清除动作,这个势必造成回收效率的降低。

复制算法

标记–复制算法就是把Java堆分成两块,每次垃圾回收时只使用其中一块,然后把存活的对象全部移动到另一块区域。如下图:

标记–复制算法有一个很明显的缺点,那就是每次只使用堆空间的一半,造成了Java堆空间使用率的的下降。

现在大部分Java虚拟机的垃圾回收器使用的就是标记–复制算法,但是,对于Java堆空间的划分,并不是简单地一分为二。

还记得这张图么?

前面讲Java内存结构的时候,提到过Java堆的具体划分,那现在就来好好的说一说。

首先得从两个分代收集理论说起:

  • 弱分代假说:大多数对象的生命存活时间很短。
  • 强分代假说:经过越多次垃圾收集的对象,存活的时间就越久。

正是这两个分代假说,使得设计者对Java堆的划分更加合理。下面,来说一下GC的分类:

  • Minor GC/Young GC:针对新生代的垃圾收集。
  • Major GC/Old GC:针对老年代的垃圾收集。
  • Full GC:针对整个Java堆以及方法区的垃圾收集。

好了,知道了GC的分类,是时候知道GC的流程了。

通常情况下,初次被创建的对象存放在新生代的Eden区,当第一次触发Minor GC,Eden区存活的对象被转移到Survivor区的某一块区域。以后再次触发Minor GC的时候,Eden区的对象连同一块Survivor区的对象一起,被转移到了另一块Survivor区。可以看到,这两块Survivor区我们每一次只使用其中的一块,这样也仅仅是浪费了一块Survivor区。

每经历过一次垃圾回收的对象,它的分代年龄就加1,当分代年龄达到15以后,就直接被存放到老年代中。

还有一种情况,给大对象分配内存的时候,Eden区已经没有足够的内存空间了,这时候该怎么办?对于这种情况,大对象就会直接进入老年代。

标记–整理算法

标记–整理算法算是一种折中的垃圾收集算法,在对象标记的过程,和前面两个执行的是一样步骤。但是,进行标记之后,存活的对象会移动到堆的一端,然后直接清理存活对象以外的区域就可以了。这样,既避免了内存碎片,也不存在堆空间浪费的说法了。但是,每次进行垃圾回收的时候,都要暂停所有的用户线程,特别是对老年代的对象回收,则需要更长的回收时间,这对用户体验是非常不好的。如下图:

HotSpot的算法细节

根节点枚举

根节点枚举,其实就是找出可以作为GC Roots的对象,在这个过程中,所有的用户线程都必须停下。到目前为止,几乎还没有虚拟机可以做到GC Roots遍历与用户线程并发执行。当然,可达性分析算法中最耗时的寻找引用链的过程已经可以做到和用户线程并发执行了。那么,为什么需要在根节点枚举的时候停止用户线程?

其实也不难考虑,如果进行GC Roots遍历的时候,用户线程没有暂停,根节点集合的对象引用关系还在不断发生变化,这样遍历到的结果是不准确的。那么,Java虚拟机在查找GC Roots的时候,是真的需要进行全局遍历?

其实不是这样的,HotSpot虚拟机通过一个叫做OopMap的数据结构,可以知道哪些地方存储了对象引用。这样,大大减小了GC Roots的遍历时间。

安全点

安全点,是线程能够中断的点。我们在GC Roots遍历的时候,是一定要让用户线程停下来的。问题来了,线程是可以在任意位置停下来吗?为了使得线程到达最近的安全点停下来,有两种思路:

  • 抢先式中断: 暂停所有的用户线程,如果哪条线程没有在安全点,就恢复这条线程执行,直到它跑到安全点上在中断。不过没有Java虚拟机采用这种思路。
  • 主动式中断: 不对线程进行操作,仅仅设置一个简单的标志位,线程执行的时候不断区轮询这个标志位,当这个标志位为真的时候,线程就在离自己最近的安全点挂起。

安全区域

安全区域是安全点的拉伸和扩展,安全点解决了如何让线程停下,却没有解决如何让虚拟机进入垃圾回收状态。

安全区域是指能能够确保在某一代码片段中,引用关系不会发生变化的区域。因此,一旦线程进入了安全区域,就可以不去理会这些处于安全区域的线程。当线程离开安全区域的时候,虚拟机就会检查是否完成了根节点枚举。

记忆集与卡表

不知道大家是否考虑过这样的一个问题?既然Java堆有新生代老年代的划分,那么对象引用是否会存在跨代?如果存在跨代,又该如何解决老年代的GC Roots遍历问题?

首先,跨代引用是存在的。因此,垃圾收集器在新生代建立了一个叫做记忆集的数据结构,用来避免把整个老年代假如GC Roots的扫描范围。

记忆集是抽象的数据结构,而卡表是记忆集的具体实现,这种关系就类似与方法区与元空间。

写屏障

写屏障的作用很简单,就是对卡表进行维护和更新。

并发的可达性分析

前面我们说到过为什么要暂停所有的用户线程(这个动作也被称之为Stop The World)?这其实是为了不让用户线程改变GC Roots对象的引用。试想,如果用户线程能够随便把死亡的对象重新标记为存活,或者把存活的对象标记为死亡,这岂不是会使的程序发生意想不到的错误。

经典的垃圾收集器

知道了不少的垃圾收集理论,但是具体到某一类的垃圾收集器,其实现方式又不全然相同。下面就介绍一些常见的垃圾收集器。

Serial 收集器

Serial 收集器是最基础、历史最悠久的收集器,它在进行垃圾收集的时候会暂停所有的工作线程,直到完成垃圾收集过程。下面是Serial垃圾收集器的运行示意图:

ParNew 收集器

ParNew 垃圾收集器实则是Serial 垃圾收集器的多线程版本,这个多线程在于ParNew垃圾收集器可以使用多条线程进行垃圾回收。

Parallel Scavenge 收集器

也是一款新生代垃圾收集器,同样的基于标记–复制算法实现的。它最大的特点是可以控制吞吐量。

那什么是吞吐量呢?

Serial Old 收集器

Serial Old 收集器是Serial 收集器的老年代版本。其垃圾收集器的运行原理和Serial 收集器是一样的。

Parallel Old 收集器

Parallel Old 收集器同样是Parallel Scavenge 收集器的老年代版本,支持多线程并发收集。下面就是它的运行示意图:

CMS 收集器

前面说到过Parallel Scavenge 收集器,它是一个可以控制吞吐量的垃圾收集器。现在要说的CMS 收集器,它是一个追求最短停顿时间的垃圾收集器,基于标记–清除算法实现的。CMS 垃圾收集器的运作过程相对前面几个垃圾收集器来说比较复杂,整个过程可以分为四个部分:

  • 初始标记: 需要Stop The World,这里仅仅标记GC Roots能够直接关联的对象,所以速度很快。
  • 并发标记: 从关联对象遍历整个GC Roots的引用链,这个过程耗时最长,但是却可以和用户线程并发运行。
  • 重新标记: 修正并发时间,因为用户线程可能会导致标记产生变动,同样需要Stop The World。
  • 并发清除: 清除已经死亡的对象。

Garbage First 收集器

Garbage First(简称G 1)收集器是垃圾收集器发展史上里程碑式的成果,主要面向服务端应用程序。另外G 1收集器虽然还保留新生代和老年代的概念,但是新生代和老年代不在固定,它们都是一系列区域的动态集合。

好了,关于垃圾收集器就介绍到这里,至于G 1收集器还是有很多地方需要关注的,朋友们可以查阅相关资料。下一篇,我们讲类加载机制,你们说好么?

现在是凌晨两点半,庆幸我没看到凌晨四点的太阳。上帝会带走他最喜欢的儿子么?还是那句话,原创不易,点赞再看,最后加个关注,定期推送原创高质量文章。

最后的最后,上点干货。(但凡有个这样的女朋友,我会熬夜么?)

本文转载自: 掘金

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

详细讲解!RabbitMQ如何防止数据丢失,看这篇就够了!

发表于 2020-08-03

思维导图

在这里插入图片描述

一、分析数据丢失的原因

分析RabbitMQ消息丢失的情况,不妨先看看一条消息从生产者发送到消费者消费的过程:

可以看出,一条消息整个过程要经历两次的网络传输:从生产者发送到RabbitMQ服务器,从RabbitMQ服务器发送到消费者。

在消费者未消费前存储在队列(Queue)中。

所以可以知道,有三个场景下是会发生消息丢失的:

  • 存储在队列中,如果队列没有对消息持久化,RabbitMQ服务器宕机重启会丢失数据。
  • 生产者发送消息到RabbitMQ服务器过程中,RabbitMQ服务器如果宕机停止服务,消息会丢失。
  • 消费者从RabbitMQ服务器获取队列中存储的数据消费,但是消费者程序出错或者宕机而没有正确消费,导致数据丢失。

针对以上三种场景,RabbitMQ提供了三种解决的方式,分别是消息持久化,confirm机制,ACK事务机制。

二、消息持久化

RabbitMQ是支持消息持久化的,消息持久化需要设置:Exchange为持久化和Queue持久化,这样当消息发送到RabbitMQ服务器时,消息就会持久化。

首先看Exchange交换机的类图:

看这个类图其实是要说明上一篇文章介绍的四种交换机都是AbstractExchange抽象类的子类,所以根据java的特性,创建子类的实例会先调用父类的构造器,父类也就是AbstractExchange的构造器是怎么样的呢?

从上面的注释可以看到durable参数表示是否持久化。默认是持久化(true)。创建持久化的Exchange可以这样写:

1
2
3
4
5
java复制代码	@Bean
public DirectExchange rabbitmqDemoDirectExchange() {
//Direct交换机
return new DirectExchange(RabbitMQConfig.RABBITMQ_DEMO_DIRECT_EXCHANGE, true, false);
}

接着是Queue队列,我们先看看Queue的构造器是怎么样的:

也是通过durable参数设置是否持久化,默认是true。所以创建时可以不指定:

1
2
3
4
5
java复制代码	@Bean
public Queue fanoutExchangeQueueA() {
//只需要指定名称,默认是持久化的
return new Queue(RabbitMQConfig.FANOUT_EXCHANGE_QUEUE_TOPIC_A);
}

这就完成了消息持久化的设置,接下来启动项目,发送几条消息,我们可以看到:


怎么证明是已经持久化了呢,实际上可以找到对应的文件:
在这里插入图片描述
找到对应磁盘中的目录:

消息持久化可以防止消息在RabbitMQ Server中不会因为宕机重启而丢失。

三、消息确认机制

3.1 confirm机制

在生产者发送到RabbitMQ Server时有可能因为网络问题导致投递失败,从而丢失数据。我们可以使用confirm模式防止数据丢失。工作流程是怎么样的呢,看以下图解:
在这里插入图片描述
从上图中可以看到是通过两个回调函数**confirm()、returnedMessage()**进行通知。

一条消息从生产者发送到RabbitMQ,首先会发送到Exchange,对应回调函数confirm()。第二步从Exchange路由分配到Queue中,对应回调函数则是returnedMessage()。

代码怎么实现呢,请看演示:

首先在application.yml配置文件中加上如下配置:

1
2
3
4
5
6
7
8
9
yml复制代码spring:
rabbitmq:
publisher-confirms: true
# publisher-returns: true
template:
mandatory: true
# publisher-confirms:设置为true时。当消息投递到Exchange后,会回调confirm()方法进行通知生产者
# publisher-returns:设置为true时。当消息匹配到Queue并且失败时,会通过回调returnedMessage()方法返回消息
# spring.rabbitmq.template.mandatory: 设置为true时。指定消息在没有被队列接收时会通过回调returnedMessage()方法退回。

有个小细节,publisher-returns和mandatory如果都设置的话,优先级是以mandatory优先。可以看源码:
在这里插入图片描述
接着我们需要定义回调方法:

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
java复制代码@Component
public class RabbitmqConfirmCallback implements RabbitTemplate.ConfirmCallback, RabbitTemplate.ReturnCallback {
private Logger logger = LoggerFactory.getLogger(RabbitmqConfirmCallback.class);

/**
* 监听消息是否到达Exchange
*
* @param correlationData 包含消息的唯一标识的对象
* @param ack true 标识 ack,false 标识 nack
* @param cause nack 投递失败的原因
*/
@Override
public void confirm(CorrelationData correlationData, boolean ack, String cause) {
if (ack) {
logger.info("消息投递成功~消息Id:{}", correlationData.getId());
} else {
logger.error("消息投递失败,Id:{},错误提示:{}", correlationData.getId(), cause);
}
}

@Override
public void returnedMessage(Message message, int replyCode, String replyText, String exchange, String routingKey) {
logger.info("消息没有路由到队列,获得返回的消息");
Map map = byteToObject(message.getBody(), Map.class);
logger.info("message body: {}", map == null ? "" : map.toString());
logger.info("replyCode: {}", replyCode);
logger.info("replyText: {}", replyText);
logger.info("exchange: {}", exchange);
logger.info("routingKey: {}", exchange);
logger.info("------------> end <------------");
}

@SuppressWarnings("unchecked")
private <T> T byteToObject(byte[] bytes, Class<T> clazz) {
T t;
try (ByteArrayInputStream bis = new ByteArrayInputStream(bytes);
ObjectInputStream ois = new ObjectInputStream(bis)) {
t = (T) ois.readObject();
} catch (Exception e) {
e.printStackTrace();
return null;
}
return t;
}
}

我这里就简单地打印回调方法返回的消息,在实际项目中,可以把返回的消息存储到日志表中,使用定时任务进行进一步的处理。

我这里是使用RabbitTemplate进行发送,所以在Service层的RabbitTemplate需要设置一下:

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
java复制代码@Service
public class RabbitMQServiceImpl implements RabbitMQService {
@Resource
private RabbitmqConfirmCallback rabbitmqConfirmCallback;

@Resource
private RabbitTemplate rabbitTemplate;

@PostConstruct
public void init() {
//指定 ConfirmCallback
rabbitTemplate.setConfirmCallback(rabbitmqConfirmCallback);
//指定 ReturnCallback
rabbitTemplate.setReturnCallback(rabbitmqConfirmCallback);
}

@Override
public String sendMsg(String msg) throws Exception {
Map<String, Object> message = getMessage(msg);
try {
CorrelationData correlationData = (CorrelationData) message.remove("correlationData");
rabbitTemplate.convertAndSend(RabbitMQConfig.RABBITMQ_DEMO_DIRECT_EXCHANGE, RabbitMQConfig.RABBITMQ_DEMO_DIRECT_ROUTING, message, correlationData);
return "ok";
} catch (Exception e) {
e.printStackTrace();
return "error";
}
}

private Map<String, Object> getMessage(String msg) {
String msgId = UUID.randomUUID().toString().replace("-", "").substring(0, 32);
CorrelationData correlationData = new CorrelationData(msgId);
String sendTime = sdf.format(new Date());
Map<String, Object> map = new HashMap<>();
map.put("msgId", msgId);
map.put("sendTime", sendTime);
map.put("msg", msg);
map.put("correlationData", correlationData);
return map;
}
}

大功告成!接下来我们进行测试,发送一条消息,我们可以控制台:
在这里插入图片描述
假设发送一条信息没有路由匹配到队列,可以看到如下信息:
在这里插入图片描述
这就是confirm模式。它的作用是为了保障生产者投递消息到RabbitMQ不会出现消息丢失。

3.2 事务机制(ACK)

最开始的那张图已经讲过,消费者从队列中获取到消息后,会直接确认签收,假设消费者宕机或者程序出现异常,数据没有正常消费,这种情况就会出现数据丢失。

所以关键在于把自动签收改成手动签收,正常消费则返回确认签收,如果出现异常,则返回拒绝签收重回队列。
在这里插入图片描述
代码怎么实现呢,请看演示:

首先在消费者的application.yml文件中设置事务提交为manual手动模式:

1
2
3
4
5
6
7
yml复制代码spring:
rabbitmq:
listener:
simple:
acknowledge-mode: manual # 手动ack模式
concurrency: 1 # 最少消费者数量
max-concurrency: 10 # 最大消费者数量

然后编写消费者的监听器:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
java复制代码@Component
public class RabbitDemoConsumer {

enum Action {
//处理成功
SUCCESS,
//可以重试的错误,消息重回队列
RETRY,
//无需重试的错误,拒绝消息,并从队列中删除
REJECT
}

@RabbitHandler
@RabbitListener(queuesToDeclare = @Queue(RabbitMQConfig.RABBITMQ_DEMO_TOPIC))
public void process(String msg, Message message, Channel channel) {
long tag = message.getMessageProperties().getDeliveryTag();
Action action = Action.SUCCESS;
try {
System.out.println("消费者RabbitDemoConsumer从RabbitMQ服务端消费消息:" + msg);
if ("bad".equals(msg)) {
throw new IllegalArgumentException("测试:抛出可重回队列的异常");
}
if ("error".equals(msg)) {
throw new Exception("测试:抛出无需重回队列的异常");
}
} catch (IllegalArgumentException e1) {
e1.printStackTrace();
//根据异常的类型判断,设置action是可重试的,还是无需重试的
action = Action.RETRY;
} catch (Exception e2) {
//打印异常
e2.printStackTrace();
//根据异常的类型判断,设置action是可重试的,还是无需重试的
action = Action.REJECT;
} finally {
try {
if (action == Action.SUCCESS) {
//multiple 表示是否批量处理。true表示批量ack处理小于tag的所有消息。false则处理当前消息
channel.basicAck(tag, false);
} else if (action == Action.RETRY) {
//Nack,拒绝策略,消息重回队列
channel.basicNack(tag, false, true);
} else {
//Nack,拒绝策略,并且从队列中删除
channel.basicNack(tag, false, false);
}
channel.close();
} catch (Exception e) {
e.printStackTrace();
}
}
}
}

解释一下上面的代码,如果没有异常,则手动确认回复RabbitMQ服务端basicAck(消费成功)。

如果抛出某些可以重回队列的异常,我们就回复basicNack并且设置重回队列。

如果是抛出不可重回队列的异常,就回复basicNack并且设置从RabbitMQ的队列中删除。

接下来进行测试,发送一条普通的消息”hello”:
在这里插入图片描述
解释一下ack返回的三个方法的意思。

①成功确认

1
java复制代码void basicAck(long deliveryTag, boolean multiple) throws IOException;

消费者成功处理后调用此方法对消息进行确认。

  • deliveryTag:该消息的index
  • multiple:是否批量.。true:将一次性ack所有小于deliveryTag的消息。

②失败确认

1
java复制代码void basicNack(long deliveryTag, boolean multiple, boolean requeue) throws IOException;
  • deliveryTag:该消息的index。
  • multiple:是否批量。true:将一次性拒绝所有小于deliveryTag的消息。
  • requeue:被拒绝的是否重新入队列。

③失败确认

1
java复制代码void basicReject(long deliveryTag, boolean requeue) throws IOException;
  • deliveryTag:该消息的index。
  • requeue:被拒绝的是否重新入队列。

basicNack()和basicReject()的区别在于:basicNack()可以批量拒绝,basicReject()一次只能拒接一条消息。

四、遇到的坑

4.1 启用nack机制后,导致的死循环

上面的代码我故意写了一个bug。测试发送一条”bad”,然后会抛出重回队列的异常。这就有个问题:重回队列后消费者又消费,消费抛出异常又重回队列,就造成了死循环。
在这里插入图片描述
那怎么避免这种情况呢?

既然nack会造成死循环的话,我提供的一个思路是不使用basicNack(),把抛出异常的消息落库到一张表中,记录抛出的异常,消息体,消息Id。通过定时任务去处理。

如果你有什么好的解决方案,也可以留言讨论~

4.2 double ack

有的时候比较粗心,不小心开启了自动Ack模式,又手动回复了Ack。那就会报这个错误:

1
2
3
java复制代码消费者RabbitDemoConsumer从RabbitMQ服务端消费消息:java技术爱好者
2020-08-02 22:52:42.148 ERROR 4880 --- [ 127.0.0.1:5672] o.s.a.r.c.CachingConnectionFactory : Channel shutdown: channel error; protocol method: #method<channel.close>(reply-code=406, reply-text=PRECONDITION_FAILED - unknown delivery tag 1, class-id=60, method-id=80)
2020-08-02 22:52:43.102 INFO 4880 --- [cTaskExecutor-1] o.s.a.r.l.SimpleMessageListenerContainer : Restarting Consumer@f4a3a8d: tags=[{amq.ctag-8MJeQ7el_PNbVJxGOOw7Rw=rabbitmq.demo.topic}], channel=Cached Rabbit Channel: AMQChannel(amqp://guest@127.0.0.1:5672/,5), conn: Proxy@782a1679 Shared Rabbit Connection: SimpleConnection@67c5b175 [delegate=amqp://guest@127.0.0.1:5672/, localPort= 56938], acknowledgeMode=AUTO local queue size=0

出现这个错误,可以检查一下yml文件是否添加了以下配置:

1
2
3
4
5
6
7
yml复制代码spring:
rabbitmq:
listener:
simple:
acknowledge-mode: manual
concurrency: 1
max-concurrency: 10

如果上面这个配置已经添加了,还是报错,有可能你使用@Configuration配置了SimpleRabbitListenerContainerFactory,根据SpringBoot的特性,代码优于配置,代码的配置覆盖了yml的配置,并且忘记设置手动manual模式:

1
2
3
4
5
6
7
8
java复制代码@Bean
public SimpleRabbitListenerContainerFactory rabbitListenerContainerFactory(ConnectionFactory connectionFactory) {
SimpleRabbitListenerContainerFactory factory = new SimpleRabbitListenerContainerFactory();
factory.setConnectionFactory(connectionFactory);
//设置手动ack模式
factory.setAcknowledgeMode(AcknowledgeMode.MANUAL);
return factory;
}

如果你还是有报错,那可能是写错地方了,写在生产者的项目了。以上的配置应该配置在消费者的项目。因为ack模式是针对消费者而言的。我就是写错了,写在生产者,折腾了几个小时,泪目~

4.3 性能问题

其实手动ACK相对于自动ACK肯定是会慢很多,我在网上查了一些资料,性能相差大概有10倍。所以一般在实际应用中不太建议开手动ACK模式。不过也不是绝对不可以开,具体情况具体分析,看并发量,还有数据的重要性等等。

所以在实际项目中还需要权衡一下并发量和数据的重要性,再决定具体的方案。

4.4 启用手动ack模式,如果没有及时回复,会造成队列异常

如果开启了手动ACK模式,但是由于代码有bug的原因,没有回复RabbitMQ服务端,那么这条消息就会放到Unacked状态的消息堆里,只有等到消费者的连接断开才会转到Ready消息。如果消费者一直没有断开连接,那Unacked的消息就会越来越多,占用内存就越来越大,最后就会出现异常。

这个问题,我没法用我的电脑演示,我的电脑太卡了。

五、总结

通过上面的学习后,总结了RabbitMQ防止数据丢失有三种方式:

  • 消息持久化
  • 生产者消息确认机制(confirm模式)
  • 消费者消息确认模式(ack模式)

上面所有例子的代码都上传github了:

github.com/yehongzhi/m…

如果你觉得这篇文章对你有用,点个赞吧~

你的点赞是我创作的最大动力~

想第一时间看到我更新的文章,可以微信搜索公众号「java技术爱好者」,拒绝做一条咸鱼,我是一个努力让大家记住的程序员。我们下期再见!!!
在这里插入图片描述

能力有限,如果有什么错误或者不当之处,请大家批评指正,一起学习交流!

本文转载自: 掘金

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

Kotlin Jetpack 实战|05 Kotlin 泛

发表于 2020-08-03

往期文章

《Kotlin Jetpack 实战:开篇》

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

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

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

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

《04. Kotlin 高阶函数》

前言

这是一篇文科生都能读泛型入门教程。(亲测,我女朋友都能看懂。)

本文以故事的形式介绍 Kotlin 泛型及其不变性,声明处型变,使用处型变,最后再搭配一个实战环节,将泛型应用到我们的 Demo 当中来。

前期准备

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

正文

1. 遥控器的故事:泛型

女朋友:好想要一个万能遥控器啊。

我:要不我教你用 Kotlin 的泛型实现一个吧!

女朋友:切,又想忽悠我学 Kotlin。[白眼]

我:真的很简单,保证你一看就会。

1-1 泛型类

我:这是一个万能遥控器,它带有一个泛型参数

1
2
3
4
5
6
kotlin复制代码//          类的泛型参数(形参)
// ↓
class Controller<T>() {
fun turnOn(obj: T){ ... }
fun turnOff(obj: T){ ... }
}

我:它用起来也简单,想控制什么,把对应的泛型传进去就行,就跟选模式一样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
kotlin复制代码//                    电视机作为泛型实参
// ↓
val tvController: Controller<TV> = Controller<TV>()
val tv = TV()
// 控制电视机
tvController.turnOn(tv)
tvController.turnOff(tv)

// 电风扇作为泛型实参
// ↓
val fanController: Controller<Fan> = Controller<Fan>()
val fan = Fan()
// 控制电风扇
fanController.turnOn(fan)
fanController.turnOff(fan)

借助 Kotlin 的顶层函数,Controller 类甚至都可以省掉,直接用泛型函数:

1-2 泛型函数

1
2
3
4
kotlin复制代码//     函数的泛型参数
// ↓ ↓
fun <T> turnOn(obj: T){ ... }
fun <T> turnOff(obj: T){ ... }

泛型函数用起来也简单:

1
2
3
4
5
6
7
8
9
kotlin复制代码// 控制电视
val tv = TV()
turnOn<TV>(tv)
turnOff<TV>(tv)

// 控制风扇
val fan = Fan()
turnOn<Fan>(fan)
turnOff<Fan>(fan)

女朋友:我知道怎么用啦!是不是这样?

1
2
kotlin复制代码val boyFriend = BoyFriend()
turnOff<BoyFriend>(boyFriend)

我:……


2. 招聘的故事:泛型的不变性(Invariant)

女朋友:我想招几个大学生做兼职,你推荐几个大学吧。

我:好嘞,不过我要通过 Kotlin 泛型来给你推荐。

女朋友:呃……刚才你讲的泛型还挺简单,这次有什么新花样吗?

我:你看下去就知道了。

我:先来点准备工作:

1
2
3
4
5
6
7
8
9
10
11
kotlin复制代码// 学生
open class Student()
// 女学生
class FemaleStudent: Student()

// 大学
class University<T>(val name: String) {
// 往外取,代表招聘
fun get(): T { ... }
fun put(student: T){ ... }
}

我:你的招聘需求可以用这样的代码描述:

1
2
3
4
5
6
7
8
kotlin复制代码//                                  注意这里
// 女朋友需要一个大学(变量声明) ↓
lateinit var university: University<Student>

// 注意这里
// 我随便推荐一个大学 ↓
university = University<Student>("某大学")
val student: Student = university.get()// 招聘

女朋友:原来 Kotlin 也没那么难……

女朋友:能赋值一个”女子大学”吗?

我:不行,会报错。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
kotlin复制代码//                                  注意这里
// ↓
lateinit var university: University<Student>
// 这是报错的原因
// ↓
university = University<FemaleStudent>("女子大学")
val student: Student = university.get()

// 编译器报错!!
/*
Type mismatch.
Required: University<Student>
Found: University<FemaleStudent>
*/

女朋友:什么鬼。。。

我:虽然 Student 和 FemaleStudent 之间是父子关系,但是 University 和 University 之间没有任何关系。这叫泛型的不变性。

女朋友:这不合理!女子大学招聘出来的学生,难道就不是学生?

我:招聘当然符合逻辑,但别忘了 University 还有一个 put 方法。

我:你怎么防止别人把一个男学生放到女子大学里去?

我:让我们看看如果可以将“女子大学”当作“普通大学”用,会出现什么问题:

1
2
3
4
5
6
7
kotlin复制代码//              声明的类型是:普通大学,然而,实际类型是:女子大学。
// ↓ ↓
var university: University<Student> = University<FemaleStudent>("女子大学")

val maleStudent: Student = Student()
// 男学生被放进女子大学!不合理。
university.put(maleStudent)

女朋友:明白了,原来这就是泛型不变性的原因,确实能避免不少麻烦。

1
2
3
4
5
6
7
kotlin复制代码// 默认情况下,编译器只允许这么做
// 声明的泛型参数与实际的要一致
↓ ↓
var normalUniversity: University<Student> = University<Student>

↓ ↓
var wUniversity: University<FemaleStudent> = University<FemaleStudent>

3. 搞定招聘:泛型的协变(Covariant)

女朋友:如果我把 University 类里面的 put 方法删掉,是不是就可以用“女子大学”赋值了?这样就不用担心把男学生放到女子大学的问题了。

我:这还不够,还需要加一个关键字 out 告诉编译器:我们只会从 University 类往外取,不会往里面放。这时候,University 就可以当作 University 的子类。

我:这叫做泛型的协变。

1
2
3
4
5
6
7
8
kotlin复制代码open class Student()
class FemaleStudent: Student()

// 看这里
// ↓
class University<out T>(val name: String) {
fun get(): T { ... }
}

女朋友:我试试,果然好了!

1
2
3
kotlin复制代码// 不再报错
var university: University<Student> = University<FemaleStudent>("女子大学")
val student: Student = university.get()

我:你不来写代码真浪费了。


4. 填志愿的故事:泛型的逆变(Contravariant)

女朋友:我妹妹刚高考完,马上要填志愿了,你给推荐个大学吧。

我:咱刚看过泛型协变,要不你试试自己解决这个填志愿的问题?正好 University 里有个 put 方法,你就把 put 当作填志愿就行了。

女朋友:那我依葫芦画瓢试试…… 给我妹妹报一个女子大学。

1
2
3
4
5
6
7
8
9
10
11
12
kotlin复制代码open class Student()
class FemaleStudent: Student()

class University<T>(val name: String) {
fun get(): T { ... }
// 往里放,代表填志愿
fun put(student: T){ ... }
}

val sister: FemaleStudent = FemaleStudent()
val university: University<FemaleStudent> = University<FemaleStudent>("女子大学")
university.put(sister)//填报女子大学

女朋友:完美!

我:厉害。

女朋友:能不能再报一个普通综合大学?

我:不行,你忘记泛型不变性了吗?

1
2
3
4
5
6
7
8
9
10
11
12
kotlin复制代码val sister: FemaleStudent = FemaleStudent()
// 报错原因:声明类型是:女子大学 赋值的类型是:普通大学
// ↓ ↓
val university: University<FemaleStudent> = University<Student>("普通大学")
university.put(sister)

// 报错
/*
Type mismatch.
Required: University<FemaleStudent>
Found: University<Student>
*/

女朋友:我妹能报女子大学,居然不能报普通的综合大学?这不合理吧!

我:你别忘了 University 还有一个 get 方法吗?普通综合大学 get 出来的可不一定是女学生。

女朋友:哦。那我把 get 方法删了,再加个关键字?

我:对。删掉 get 方法,再加一个关键字:in 就行了。它的作用是告诉编译器:我们只会往 University 类里放,不会往外取。这时候,University 就可以当作 University 的子类。

我:这其实就叫做泛型的逆变,它们的继承关系反过来了。

1
2
3
4
5
6
7
8
9
10
kotlin复制代码//              看这里
// ↓
class University<in T>(val name: String) {
fun put(student: T){ ... }
}

val sister: FemaleStudent = FemaleStudent()
// 编译通过
val university: University<FemaleStudent> = University<Student>("普通大学")
university.put(sister)

女朋友:泛型还挺有意思。

我:上面提到的协变和逆变。它们都是通过修改 University 类的泛型声明实现的,所以它们统称为:声明处型变,这是 Kotlin 才有的概念,Java 中没有。


5. 使用处型变(Use-site Variance)

女朋友:万一 University 是第三方提供的,我们无法修改,怎么办?能不能在不修改 University 类的前提下实现同样的目的?

我:可以,这就要用到使用处型变了。他们也分为:使用处协变,使用处逆变。

1
2
3
4
5
6
7
8
kotlin复制代码open class Student()
class FemaleStudent: Student()

// 假设 University 无法修改
class University<T>(val name: String) {
fun get(): T { ... }
fun put(student: T){ ... }
}

5-1 使用处协变

我:在泛型的实参前面增加一个 out 关键字,代表我们只会从 University 往外取,不会往里放。这么做就实现了 使用处协变。

1
2
3
4
5
6
7
8
kotlin复制代码//                                         看这里
// ↓
fun useSiteCovariant(university: University<out Student>) {
val femaleStudent: Student? = university.get()

// 报错: Require Nothing? found Student?
// university.put(femaleStudent)
}

女朋友:这也挺容易理解的。那使用处逆变呢?加个 in?

5-2 使用处逆变

我:对。在泛型的实参前面增加一个 in 关键字,代表我们只会从 University 往里放,不会往外取。这么做就实现了 使用处逆变。

1
2
3
4
5
6
7
8
kotlin复制代码//                                               看这里
// ↓
fun useSiteContravariant(universityIn: University<in FemaleStudent>) {
universityIn.put(FemaleStudent())

// 报错: Require FemaleStudent? found Any?
// val femaleStudent: FemaleStudent? = universityIn.get()
}

女朋友:思想是一样的。

女朋友:如果是从 University 招聘学生,就是往外取,这种情况下就是协变,可以用 University 替代 University,因为女子大学取出来的女学生,和普通大学取出来的学生,都是学生。

女朋友:如果是 University 要招生,就是往里放,这种情况下,就只能用 University 替代 University,因为普通大学的招生范围更广,女子大学能接收的学生,普通大学也接收。

我:你总结的真好。顺便提一句:Kotlin 的使用处型变,还有个名字叫:类型投影(Type Projections),这名字真烂。

以上代码的具体细节可以看我这个 GitHub Commit。

5-3 Kotlin 和 Java 对比

我:既然你 Kotlin 泛型理解起来毫无压力,那我再给你给加个餐,对比一下 Java 的使用处型变。

女朋友:呃…… Java 是啥玩意?

我:没事,你就当看个乐呵。

使用处协变 使用处逆变
Kotlin University University
Java University<? extends Student> University<? super FemaleStudent>

我:是不是简单明了?

女朋友:还是 Kotlin 的容易理解:out 代表只能往外取(get),in代表只能往里放(put)。

我:没错。

女朋友:对比起来,Java 的表达方式真是无力吐槽。(-_-)

1
2
3
4
5
6
7
8
9
10
11
12
13
java复制代码//     Java 这辣鸡协变语法
// ↓
University<? extends Student> covariant = new University<FemaleStudent>("女子大学");
Student student = covariant.get();
// 报错
covariant.put(student);

// Java 这辣鸡逆变语法
// ↓
University<? super FemaleStudent> contravariant = new University<Student>("普通大学");
contravariant.put(new FemaleStudent())
// 报错
Student s = contravariant.get();

以上代码的具体细节可以看我这个 GitHub Commit。


6. Kotlin 泛型实战

我:这里有一个 Kotlin 的 Demo,要不你来看看有哪些地方能用泛型优化的?

女朋友:过分了啊!你让我学 Kotlin 就算了,还想让我帮你写代码?

女朋友:你来写,我来看。

我:呃……听领导的。

6-1 泛型版本的 apply 函数

我:这是上一个章节里的代码,这个 apply 函数其实可以用泛型来简化,让所有的类都能使用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
kotlin复制代码
// 替代 替代 替代
// ↓ ↓ ↓
fun User.apply(block: User.() -> Unit): User{
block()
return this
}

user?.apply { this: User ->
...
username.text = this.name
website.text = this.blog
image.setOnClickListener { gotoImagePreviewActivity(this) }
}

我:使用泛型替代以后的 apply 函数就是这样:

1
2
3
4
5
6
7
kotlin复制代码
// 泛型 泛型 泛型
// ↓ ↓ ↓ ↓
fun <T> T.apply(block: T.() -> Unit): T{
block()
return this
}

女朋友:Kotlin 官方的 apply 函数也是这么实现的吗?

我:几乎一样,它只是多了个 contract,你暂时还不懂。

女朋友:呃……还有其他例子吗?

6-2 泛型版本的 HTML 构建器

我:在上一个章节里,我实现了一个简单的 类型安全的 HTML 构建器,其中有不少重复的代码。

女朋友:咱们可以利用泛型消灭重复代码,对吧?

我:没错。

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
kotlin复制代码class Body : BaseElement("body") {
fun h1(block: () -> String): H1 {
val content = block()
val h1 = H1(content)
this.children += h1
return h1
}
// ↑
// 看看这重复的模板代码
// ↓
fun p(block: () -> String): P {
val content = block()
val p = P(content)
this.children += p
return p
}
}
// ↑
// 看看这重复的模板代码
// ↓
class Head : BaseElement("head") {
fun title(block: () -> String): Title {
val content = block()
val title = Title(content)
this.children += title
return title
}
}

我:让我们用泛型来优化:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
kotlin复制代码
open class BaseElement(var name: String, var content: String = "") : Element {
// 在父类增加一个共有的泛型方法
protected fun <T : BaseElement> initString(element: T, init: T.() -> String): T {
val content = element.init()
element.content = content
children.add(element)
return element
}
}

class Body : BaseElement("body") {
fun h1(block: H1.() -> String) = initString(H1("h1"), block)
fun p(block: P.() -> String) = initString(P("p"), block)
}

class Head : BaseElement("head") {
fun title(block: Title.() -> String) = initString(Title(), block)
}

我:还有一个地方有重复代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
kotlin复制代码class HTML : BaseElement("html") {
fun head(block: Head.() -> Unit): Head {
val head = Head()
head.block()
this.children += head
return head
}
// ↑
// 看看这重复的模板代码
// ↓
fun body(block: Body.() -> Unit): Body {
val body = Body()
body.block()
this.children += body
return body
}
}

我:优化后:

1
2
3
4
5
6
7
8
9
10
11
12
13
kotlin复制代码open class BaseElement(var name: String, var content: String = "") : Element {
// 在父类增加一个共有的泛型方法
protected fun <T : Element> init(element: T, init: T.() -> Unit): T {
element.init()
children.add(element)
return element
}
}

class HTML : BaseElement("html") {
fun head(block: Head.() -> Unit) = init(Head(), block)
fun body(block: Body.() -> Unit) = init(Body(), block)
}

女朋友:嗯,顺眼了很多!

以上代码的具体细节可以看我这个 GitHub Commit。

7. 总结

  • 受限于篇幅,Kotlin 泛型剩余知识点留到以后再讲,本文作为入门暂时够用了。泛型要讲透彻得写一本书,这不是本文的目的。
  • 泛型的思想是一样的,理解了 Kotlin 型变,迁移到 Java 也是一样的。
  • Kotlin 的泛型,由于借鉴了别的语言(C#),所以理解起来其实要比 Java 简单很多。
  • 文章看完了,快去敲代码吧:github.com/chaxiu/Kotl…
  • 找这个分支:chapter_05_generics

8. 思考题:

Kotlin 的声明处型变和使用处型变它们分别有哪些优势和劣势?


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

回目录–>【Kotlin Jetpack 实战】

本文转载自: 掘金

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

实战:一键生成前后端代码,Mybatis-Plus代码生成器

发表于 2020-07-31

前言

在日常的软件开发中,程序员往往需要花费大量的时间写CRUD,不仅枯燥效率低,而且每个人的代码风格不统一。MyBatis-Plus 代码生成器,通过 AutoGenerator 可以快速生成 Entity、Mapper、Mapper XML、Service、Controller 等各个模块及前端页面的代码,极大的提升了开发效率。

项目介绍

本项目将以springboot用演示,前端使用freemaker,数据库持久层用mybatis(考虑到mybatis的使用还是最普遍的,就没有用jpa和mybatisplus),通过Velocity模板引擎配置各模块的文件模板,通过mybatis-plus代码生成器连接mysql,用商品表为例生成各模块的代码和前端页面。(本项目只演示分页查询和导出功能)。

本项目所有代码和脚本都能都文末找到地址。

实战

数据库脚本

创建一张商品表test_goods

1
2
3
4
5
6
7
8
9
10
11
sql复制代码CREATE TABLE `test_goods` (
`id` bigint(20) DEFAULT NULL COMMENT 'id',
`goods_sn` varchar(45) DEFAULT NULL COMMENT '商品编码',
`name` varchar(255) DEFAULT NULL COMMENT '商品名称',
`title` varchar(80) DEFAULT NULL COMMENT '标题',
`price` decimal(10,2) DEFAULT NULL COMMENT '售价',
`status` int(2) DEFAULT NULL COMMENT '商品状态',
`sale_count` int(11) DEFAULT NULL COMMENT '销量',
`create_date` datetime DEFAULT NULL COMMENT '创建时间',
`modify_date` datetime DEFAULT NULL COMMENT '修改时间'
) ENGINE=InnoDB DEFAULT CHARSET=utf8

maven依赖

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
xml复制代码  <dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.1.2</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus</artifactId>
<version>2.1.4</version>
</dependency>

<!-- aspectj -->
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<scope>provided</scope>
</dependency>
<!--es-->

<!-- lombok 简化get/set 方法 -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.16.10</version>
<scope>provided</scope>
</dependency>

<dependency>
<groupId>org.apache.velocity</groupId>
<artifactId>velocity-engine-core</artifactId>
<version>2.0</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<!--csv导出用到-->
<dependency>
<groupId>com.opencsv</groupId>
<artifactId>opencsv</artifactId>
<version>3.8</version>
</dependency>

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

配置文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
yml复制代码mybatis:
mapper-locations: classpath:mybatis/*Mapper.xml
type-aliases-package: com.lzn.mybatisplus.codegenerator.entity

spring:
datasource:
username: root
password: 123qwe
url: jdbc:mysql://192.168.0.1:3306/myProject?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=UTC
driver-class-name: com.mysql.jdbc.Driver
redis:
host: 192.168.0.1
password: 1234qwer
port: 6379
freemarker:
template-loader-path: classpath:/templates/pages/
cache: false
charset: UTF-8
check-template-location: true
content-type: text/html
expose-request-attributes: true
expose-session-attributes: true
suffix: .ftl

模板文件

本项目中,所有模块的文件都是用Velocity模板引擎生成,这里简单介绍下Velocity的语法,在Velocity中用表示变量,例如:{}表示变量,例如:表示变量,例如:{table.entityName} 表示实体名,field.name表示字段名,我们在AutoGenerator代码生成器里定义的全局变量{field.name} 表示字段名,我们在AutoGenerator代码生成器里定义的全局变量 field.name表示字段名,我们在AutoGenerator代码生成器里定义的全局变量{author}、{date} 表示作者,日期等。在Velocity中用#表示语法,例如 #foreach(field in ${table.fields}) #end遍历表字段。下面演示几个类、前端文件、xml文件的模板文件

实体类模板(entity.java.vm)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
java复制代码package ${package.Entity};

import java.math.BigDecimal;
import java.util.Date;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;

/**
* 数据库表名 ${table.name}
*
* @author ${author}
* @date ${date}
*/
@Getter
@Setter
@ToString
public class ${table.entityName} {

#foreach($field in ${table.fields})
/**
* 数据库字段名 ${field.name} 类型 ${field.type}
*/
private ${field.propertyType} ${field.propertyName};

#end

}

Controller模板(controller.java.vm)

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
java复制代码package ${package.Controller};

import ${package.Entity}.${entity};
import ${package.Service}.${table.serviceName};
import com.lzn.mybatisplus.codegenerator.export.${table.entityName}VO;
import com.lzn.mybatisplus.codegenerator.export.${table.entityName}ExportService;
import com.lzn.mybatisplus.codegenerator.utils.entity.*;
import com.lzn.mybatisplus.codegenerator.utils.export.*;
import org.apache.commons.beanutils.ConvertUtils;
import com.lzn.mybatisplus.codegenerator.utils.ParameterUtil;
import com.lzn.mybatisplus.codegenerator.utils.entity.GridDataModel;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.*;
import javax.annotation.Resource;
import javax.servlet.ServletRequest;
import javax.servlet.http.HttpServletRequest;
import java.util.List;
import java.util.Map;

/**
* <p>
* ${tablecomment} 前端控制器
* </p>
*
* @author ${author}
* @since ${date}
*/
@Controller
@RequestMapping(value="/admin/${table.entityPath}")
public class ${table.controllerName}{
private static Logger logger = LoggerFactory.getLogger(${table.controllerName}.class);

@Resource
private ${entity}Service ${table.entityPath}Service;



@RequestMapping(value = "list", method = RequestMethod.GET)
public String list(Model model){
return "/admin/${cfg.pageDirName}/list";
}

@RequestMapping(value = "searchList", method = RequestMethod.POST)
@ResponseBody
@ExportMethod(serviceClass = ${entity}ExportService.class, memo = "明细导出")
public String searchList(ServletRequest request,@ModelAttribute("page") OmuiPage page){
try {
Map<String,Object> searchParam = ParameterUtil.getParametersStartingWith(request, "filter_");
GridDataModel<${entity}VO> gd =${table.entityPath}Service.findByPage(searchParam, page);
return JsonMapper.nonDefaultMapper().toJson(gd);
} catch (Exception e) {
logger.error("查询出错了",e);
return JsonMapper.nonDefaultMapper().toJson(new Resp("false", e.getMessage()));
}
}


}

Service类模板(service.java.vm)

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
java复制代码package ${package.Service};



import org.springframework.stereotype.Service;
import com.lzn.mybatisplus.codegenerator.dao.${table.mapperName};
import com.lzn.mybatisplus.codegenerator.utils.entity.GridDataModel;
import com.lzn.mybatisplus.codegenerator.utils.entity.OmuiPage;
import com.lzn.mybatisplus.codegenerator.export.${table.entityName}VO;

import javax.annotation.Resource;
import java.math.BigDecimal;
import java.text.DateFormat;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.*;


/**
* <p>
* $!{tablecomment} 服务类
* </p>
*
* @author ${author}
* @since ${date}
*/
@Service
public class ${table.serviceName} {

@Resource
private ${table.mapperName} ${table.entityPath}Dao;

/**
* 分页查询
* */
public GridDataModel<${table.entityName}VO> findByPage(Map<String, Object> searchParams, OmuiPage page){
GridDataModel<${table.entityName}VO> gm = new GridDataModel<${table.entityName}VO>();
searchParams.put("start", page.getStart());
searchParams.put("limit", page.getLimit());
long count = ${table.entityPath}Dao.countForPage(searchParams);
List<${table.entityName}VO> list = ${table.entityPath}Dao.listForPage(searchParams);
gm.setTotal(count);
gm.setRows(list);
return gm;
}



}

Dao类模板(dao.java.vm)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
java复制代码package ${package.Mapper};

import com.lzn.mybatisplus.codegenerator.entity.${table.entityName};
import com.lzn.mybatisplus.codegenerator.export.${table.entityName}VO;
import java.util.List;
import java.util.Map;

public interface ${table.mapperName} {
/**
* 根据主键删除数据库的记录, ${table.name}
*/
int deleteByPrimaryKey(Long id);

/**
* 新写入数据库记录, ${table.name}
*/
int insert(${table.entityName} record);

/**
* 根据指定主键获取一条数据库记录, ${table.name}
*/
${table.entityName} selectByPrimaryKey(Long id);

/**
* 根据主键来更新符合条件的数据库记录, ${table.name}
*/
int updateByPrimaryKey(${table.entityName} record);

/**
* 根据条件分页查询
* */
List<${table.entityName}VO> listForPage(Map<String,Object> searchMap);

/**
* 根据条件分页查询(计数)
* */
long countForPage(Map<String,Object> searchMap);

}

Mapper.xml模板(mapper.xml.vm)

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
xml复制代码<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="${package.Mapper}.${table.mapperName}">
#if(${baseResultMap})
<!-- 通用查询映射结果 -->
<resultMap id="BaseResultMap" type="${package.Entity}.${entity}">
#foreach($field in ${table.fields})
#if(${field.keyFlag})##生成主键排在第一位
<id column="${field.name}" property="${field.propertyName}" />
#end
#end
#foreach($field in ${table.commonFields})##生成公共字段
<result column="${field.name}" property="${field.propertyName}" />
#end
#foreach($field in ${table.fields})
#if(!${field.keyFlag})##生成普通字段
<result column="${field.name}" property="${field.propertyName}" />
#end
#end
</resultMap>
#end

#if(${baseColumnList})
<!-- 通用查询结果列 -->
<sql id="Base_Column_List">
#foreach($field in ${table.commonFields})
#if(${field.name} == ${field.propertyName})${field.name}#else${field.name} AS ${field.propertyName}#end,
#end
${table.fieldNames}
</sql>
#end

<delete id="deleteByPrimaryKey" parameterType="java.lang.Long">
<!-- -->
delete from ${table.name}
where
#foreach($field in ${table.fields})
#if(${field.keyFlag})## 主键
${field.name} = #{ ${field.propertyName} }
#end
#end
</delete>

<insert id="insert" parameterType="${package.Entity}.${entity}">
<!-- -->
<selectKey keyProperty="id" order="AFTER" resultType="java.lang.Long">
SELECT LAST_INSERT_ID()
</selectKey>
insert into ${table.name} (
#foreach($field in ${table.fields})
#if(!${field.keyFlag})##生成普通字段
${field.name}#if($foreach.hasNext),#end
#end
#end
)
values (
#foreach($field in ${table.fields})
#if(!${field.keyFlag})##生成普通字段
#{ ${field.propertyName}}#if($foreach.hasNext),#end
#end
#end
)
</insert>

<update id="updateByPrimaryKey" parameterType="${package.Entity}.${entity}">
<!-- -->
update ${table.name}
set
#foreach($field in ${table.fields})
#if(!${field.keyFlag})##生成普通字段
${field.name} = #{ ${field.propertyName}} #if($foreach.hasNext),#end
#end
#end
where
#foreach($field in ${table.fields})
#if(${field.keyFlag})
id = #{ ${field.name} }
#end
#end
</update>


<select id="selectByPrimaryKey" parameterType="java.lang.Long" resultMap="BaseResultMap">
<!-- -->
select
<include refid="Base_Column_List" />
from ${table.name}
where id = #{ id }
</select>

<select id="countForPage" parameterType="map" resultType="Long">
<!-- -->
select
count(*)
from
${table.name}
where 1=1
<if test="beginDate != null and beginDate != ''">
and create_date <![CDATA[>=]]> #{beginDate}
</if>
<if test="endDate != null and endDate != ''">
and create_date <![CDATA[<=]]> #{endDate}
</if>
</select>

<select id="listForPage" parameterType="map" resultType="com.lzn.mybatisplus.codegenerator.export.${table.entityName}VO">
<!-- -->
select
<include refid="Base_Column_List" />
from
${table.name}
where 1=1
<if test="beginDate != null and beginDate != ''">
and create_date <![CDATA[>=]]> #{beginDate}
</if>
<if test="endDate != null and endDate != ''">
and create_date <![CDATA[<=]]> #{endDate}
</if>
limit #{start}, #{limit}
</select>
</mapper>

前端页面list.ftl模板(list.ftl.vm)

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
html复制代码<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<#assign base=request.contextPath>
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta http-equiv="content-type" content="text/html; charset=utf-8" />
<title>$!{tablecomment}</title>
<link href="${base}/static/omui/css/elegant/om-all.css" rel="stylesheet" type="text/css" />
<link href="${base}/static/admin/css/admin.css" rel="stylesheet" type="text/css" />

<script type="text/javascript" src="${base}/static/js/jquery-1.7.1.js"></script>
<script type="text/javascript" src="${base}/static/js/HForm.js"></script>
<script type="text/javascript" src="${base}/static/My97DatePicker/WdatePicker.js"></script>
<script type="text/javascript" src="${base}/static/omui/js/operamasks-ui.min.js"></script>
<script type="text/javascript" src="${base}/static/omui/js/common.js"></script>
<script type="text/javascript" src="${base}/static/bui/js/common.js"></script>
<script type="text/javascript" src="${base}/static/admin/js/export.js"></script>

<script type="text/javascript">


$().ready(function(){

//初始化控件
$("#search-panel").omPanel({
title : "条件搜索",collapsible:false
});

//搜索
$('#searchButton').bind('click', function(e) {
var data = $("#listForm").HForm('form2json');
$('#listGrid').omGrid({extraData:data});
});

$("#start-time").omCalendar();
$("#time-end").omCalendar();

$('#searchButton').omButton({
icons : {left : '${base}/static/omui/images/search.png'},width : 70
});

$(".input-select").change(function(){
$('#searchButton').click();
});

$('#buttonbar').omButtonbar({
btns : [{label:"导出Excel",
id:"addbutton" ,
icons : {left : '${base}/static/omui/images/export.png'},
onClick:function()
{
exportUtil({
title : "列表导出",
exportUrl : "${base}/admin/${table.entityPath}/searchList",
extraParam : $("#listForm").HForm('form2json')
});
}
}
]
});


//初始化列表
var height=$(document).height() -$('#search-panel').outerHeight()-$('#buttonbar').outerHeight()-40;
$('#listGrid').omGrid({
height:height,
limit:20,
method:'post',
singleSelect:false,
extraData:$("#listForm").HForm('form2json'),
dataSource : '${base}/admin/${table.entityPath}/searchList',
colModel : [
{header : 'ID', name : 'id', width : 30, align : 'left',sort:'serverSide'},
{header : '创建时间', name : 'createDate', width : 150, align : 'left',sort:'serverSide',renderer :dataFormat1},
{header : '修改时间', name : 'modifyDate', width : 150, align : 'left',sort:'serverSide',renderer :dataFormat1},
#foreach($field in ${table.fields})

#set($comment = "")
#set($type = "")
#set($isNullAble = true)
#set($defaultValue = false)
#set($listIsShow = true)
#set($listIsSearch = false)

#foreach( $e in $field.comment.split(","))
#if( $foreach.count == 1 )
#set($comment = $e)
#elseif( $foreach.count == 2 )
#set($type = $e)
#elseif( $foreach.count == 3)
#if($e == "YES")
#set($isNullAble = true)
#else
#set($isNullAble = false)
#end
#elseif( $foreach.count == 4)
#if($e == "true")
#set($defaultValue = true)
#else
#set($defaultValue = false)
#end
#elseif( $foreach.count == 5)
#if($e == "true")
#set($listIsShow = true)
#else
#set($listIsShow = false)
#end
#elseif( $foreach.count == 6)
#if($e == "true")
#set($listIsSearch = true)
#else
#set($listIsSearch = false)
#end
#end
#end
{header : '#if("$!comment" != "")${comment}#end', name : '${field.propertyName}',width : 90, align : 'left',sort:'serverSide'#if($type == "timer"),renderer :dataFormat1 #end},
#end
],
rowDetailsProvider:function(rowData){
}
});

//初始化控件 end
function getIds(datas) {
var str = "";
for (var i = 0; i < datas.length; i++) {
str += datas[i].id + ",";
}
//去掉最后一个逗号(如果不需要去掉,就不用写)
if (str.length > 0) {
str = str.substr(0, str.length - 1);
}
return str;
}

$('#searchButton').click();
});



</script>
</head>
<body >

<div id="search-panel">
<form id="listForm">
<div>
<span class="label">状态:</span>
<select class="js-example-basic-single input-select" name="filter_EQS_status">
<option value="0" selected>待处理</option>
<option value="1">已处理</option>
<option value="">全部</option>
</select>
<span class="label">手机号:</span>
<input type="text" class="input-text" name="filter_LIKES_mobile" />
<span class="label">联系人:</span>
<input type="text" class="input-text" name="filter_LIKES_name" />
<span class="label">创建时间:</span>
<input id="start-time" style="width: 118px" name="filter_GTED_createDate"/>
-
<input id="time-end" style="width: 118px" name="filter_LTED_createDate"/>
<span id="searchButton">查询</span>
</div>
</form>
</div>

<div id="buttonbar"></div><!-- 工具栏位置 -->
<table id="listGrid"></table> <!-- 主列表位置 -->

</body>
</html>

代码生成器

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

import com.baomidou.mybatisplus.generator.AutoGenerator;
import com.baomidou.mybatisplus.generator.InjectionConfig;
import com.baomidou.mybatisplus.generator.config.*;
import com.baomidou.mybatisplus.generator.config.converts.MySqlTypeConvert;
import com.baomidou.mybatisplus.generator.config.po.TableInfo;
import com.baomidou.mybatisplus.generator.config.rules.DbColumnType;
import com.baomidou.mybatisplus.generator.config.rules.DbType;
import com.baomidou.mybatisplus.generator.config.rules.NamingStrategy;

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


/**
* 辅助生产后台开发相关代码 开发时只在自己本地代码修改,不要提交
* 生成ddao service controller entity java代码 和前端 flt文件。
* 只演示list场景
*/
public class MpGenerator {

//注意:开发时只在自己本地代码修改,不要提交、不要提交 不要提交
//第一步修改 javaSrcDir 修改成自己项目存放java源代码的根路径
static String javaSrcDir = "D:/Git_space/lunzijihua/codegenerator/src/main/java";
static String resourceDir = "D:/Git_space/lunzijihua/codegenerator/src/main/resources";
//第二步修改 pageRootDir 修改成你要开发的模块的名称 存放ftl文件的文件夹的根路径
static String pageRootDir ="D:/Git_space/lunzijihua/codegenerator/src/main/resources/templates/pages/";


//第三步修改 packageName 修改成你要开发的模块的名称 包名 要小写 生产的entity service dao action文件夹和java代码会在下面
static String packageName = "user";//模块文件夹包名称
//第四步修改 pageDirName 修改成你要开发的模块的名称 存放ftl文件的文件夹 要小写
static String pageDirName = "user";//模块页面文件夹名
//第五步骤 表的前缀 填写了 生成文件时会去除掉
static String tablePrefix="test_";
//第六步 数据库里面对应的表的全名
static String tableName="test_goods";

/**
* <p>
* 代码自动生成
* </p>
*/
public static void main(String[] args) {

AutoGenerator mpg = new AutoGenerator();
// 全局配置
GlobalConfig gc = new GlobalConfig();
gc.setOutputDir(javaSrcDir);
gc.setFileOverride(true);
gc.setActiveRecord(true);// 不需要ActiveRecord特性的请改为false
gc.setEnableCache(false);// XML 二级缓存
gc.setBaseResultMap(true);// XML ResultMap
gc.setBaseColumnList(true);// XML columList
// .setKotlin(true) 是否生成 kotlin 代码
gc.setAuthor("liuzhinan");

// 自定义文件命名,注意 %s 会自动填充表实体属性!
gc.setMapperName("%sMybatisDao");
// gc.setXmlName("%sDao");
gc.setServiceName("%sService");
// gc.setServiceImplName("%sService");
// gc.setControllerName("%sAction");
mpg.setGlobalConfig(gc);

// 数据源配置
DataSourceConfig dsc = new DataSourceConfig();
dsc.setDbType(DbType.MYSQL);


dsc.setTypeConvert(new MySqlTypeConvert(){
// 自定义数据库表字段类型转换【可选】
@Override
public DbColumnType processTypeConvert(String fieldType) {
System.out.println("转换类型:" + fieldType);
// 注意!!processTypeConvert 存在默认类型转换,如果不是你要的效果请自定义返回、非如下直接返回。
return super.processTypeConvert(fieldType);
}
});
dsc.setDriverName("com.mysql.jdbc.Driver");
dsc.setUsername("test");
dsc.setPassword("123456");
dsc.setUrl("jdbc:mysql://192.168.0.1:3306/myProject?useSSL=false");
mpg.setDataSource(dsc);

// 策略配置
StrategyConfig strategy = new StrategyConfig();
// strategy.setCapitalMode(true);// 全局大写命名 ORACLE 注意
strategy.setTablePrefix(new String[] { tablePrefix });// 此处可以修改为您的表前缀
strategy.setNaming(NamingStrategy.underline_to_camel);// 表名生成策略
strategy.setInclude(new String[] { tableName }); // 需要生成的表
// strategy.setExclude(new String[]{"test"}); // 排除生成的表
// 自定义实体父类
strategy.setSuperEntityClass("com.lzn.mybatisplus.codegenerator.entity.IdEntity");
// 自定义实体,公共字段
// strategy.setSuperEntityColumns(new String[] { "id", "create_date","modify_date" });
// 自定义 mapper 父类
// strategy.setSuperMapperClass("com.baomidou.demo.TestMapper");
// 自定义 service 父类
// strategy.setSuperServiceClass("com.baomidou.demo.TestService");
// 自定义 service 实现类父类
// strategy.setSuperServiceImplClass("com.baomidou.demo.TestServiceImpl");
// 自定义 controller 父类
// strategy.setSuperControllerClass("com.baomidou.demo.TestController");
// 【实体】是否生成字段常量(默认 false)
// public static final String ID = "test_id";
// strategy.setEntityColumnConstant(true);
// 【实体】是否为构建者模型(默认 false)
// public User setName(String name) {this.name = name; return this;}
// strategy.setEntityBuilderModel(true);
mpg.setStrategy(strategy);

// 包配置
PackageConfig pc = new PackageConfig();
pc.setParent("com.lzn.mybatisplus.codegenerator");
pc.setModuleName(null);
pc.setMapper("dao");
pc.setEntity("entity");
pc.setService("service");
pc.setServiceImpl("service.impl");
pc.setController("controller");
mpg.setPackageInfo(pc);

// 注入自定义配置,可以在 VM 中使用 cfg.abc 【可无】
InjectionConfig cfg = new InjectionConfig() {
@Override
public void initMap() {
Map<String, Object> map = new HashMap<String, Object>();
map.put("abc", this.getConfig().getGlobalConfig().getAuthor() + "-mp");
map.put("pageDirName",pageDirName);
map.put("packageName",packageName);
this.setMap(map);
}
};

List<FileOutConfig> focList = new ArrayList<FileOutConfig>();

// cfg.setFileOutConfigList(focList);
// mpg.setCfg(cfg);

//生成导出视图对象
focList.add(new FileOutConfig("/templates/vm/vo.java.vm") {
@Override
public String outputFile(TableInfo tableInfo) {
return javaSrcDir+"/com/lzn/mybatisplus/codegenerator/export/"+tableInfo.getEntityName()+"VO.java";
}
});
//生成excel导出的服务类,
focList.add(new FileOutConfig("/templates/vm/exportservice.java.vm") {
@Override
public String outputFile(TableInfo tableInfo) {
return javaSrcDir+"/com/lzn/mybatisplus/codegenerator/export/"+tableInfo.getEntityName()+"ExportService.java";
}
});
//生成mybatisDao文件到指定目录
focList.add(new FileOutConfig("/templates/vm/mybatisdao.java.vm") {
@Override
public String outputFile(TableInfo tableInfo) {
return javaSrcDir+"/com/lzn/mybatisplus/codegenerator/dao/"+tableInfo.getEntityName()+"MybatisDao.java";
}
});

//生成mapper文件到指定目录
focList.add(new FileOutConfig("/templates/vm/mapper.xml.vm") {
@Override
public String outputFile(TableInfo tableInfo) {
return resourceDir+"/mybatis/"+tableInfo.getEntityName()+"Mapper.xml";
}
});

// 自定义 xxList.ftl 生成
focList.add(new FileOutConfig("/templates/vm/list.ftl.vm") {
@Override
public String outputFile(TableInfo tableInfo) {
// 自定义输入文件名称
return pageRootDir+pageDirName+"/list.ftl";
}
});
cfg.setFileOutConfigList(focList);
mpg.setCfg(cfg);

// 关闭默认 xml 生成,调整生成 至 根目录
TemplateConfig tc = new TemplateConfig();
tc.setEntity("/templates/vm/entity.java.vm");
tc.setService("/templates/vm/service.java.vm");
tc.setServiceImpl(null);//设成null才会不生产
tc.setController("/templates/vm/controller.java.vm");
tc.setMapper(null);
tc.setXml(null);
mpg.setTemplate(tc);

// 自定义模板配置,可以 copy 源码 mybatis-plus/src/main/resources/templates 下面内容修改,
// 放置自己项目的 src/main/resources/templates 目录下, 默认名称一下可以不配置,也可以自定义模板名称
// TemplateConfig tc = new TemplateConfig();
// tc.setController("...");
// tc.setEntity("...");
// tc.setMapper("...");
// tc.setXml("...");
// tc.setService("...");
// tc.setServiceImpl("...");
// 如上任何一个模块如果设置 空 OR Null 将不生成该模块。
// mpg.setTemplate(tc);

// 执行生成
mpg.execute();

// 打印注入设置【可无】
System.err.println(mpg.getCfg().getMap().get("abc"));
}

}

执行代码生成器的Main方法

执行代码后,在对应的目录自动生成了文件

启动项目

并访问列表页路径 http://localhost:8080/admin/goods/list

点击导出按钮(由于篇幅有限,导出的视图对象,导出service类和aop切面实现本文没有阐述,各位可自行下载代码查看)

总结

本文为项目自动生成前后端代码提供了思路:我们可以为项目的增删改查业务编写一套规范的代码,以此编写代码模板,后续通过代码生成器,通过数据库的一张表可快速生成前后端代码,提高项目组的开发效率。

代码

github.com/pengziliu/G…

本文转载自: 掘金

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

因为不知道Java的CopyOnWriteArrayList

发表于 2020-07-31

先看再点赞,给自己一点思考的时间,微信搜索【沉默王二】关注这个靠才华苟且的程序员。
本文 GitHub github.com/itwanger 已收录,里面还有一线大厂整理的面试题,以及我的系列文章。

hello,同学们,大家好,我是沉默王二,在我为数不多的面试经历中,有一位姓马的面试官令我印象深刻,九年过去了,我还能记得他为数不多的发量。

老马:“兄弟,ArrayList 是线程安全的吗?”
王二:“不是啊。”
老马:“那有没有线程安全的 List?”
王二:“有啊,Vector。”
老马:“还有别的吗?”
王二:“Vector 不就够用了吗?”
老马看了一下左手腕上的表,说道:“今天差不多就到这里吧,你回去等通知。”

(不是,我特么不是刚进来,就回答了三个问题而已,就到这了?)

现在回想起来当时一脸懵逼的样子,脸上情不自禁地泛起了红晕,老马的意思是让我说说 Java 的 CopyOnWriteArrayList,可惜我当时几乎没怎么用过这个类,也不知道它就是个线程安全的 List,惭愧啊惭愧。

(地上有坑吗?我想跳进去。)

真正的勇士敢于直面过去的惨淡,经过这么多年的努力,我的技术功底已经大有长进了,是时候输出一波伤害了。希望这篇文章能够给不太了解 CopyOnWriteArrayList 的同学一点点帮助,到时候给面试官一个好看。

注:我用的是 OpenJDK 14。

01、Vector

Vector 的源码文档上直截了当地说了,“如果不需要线程安全,推荐使用 ArrayList 替代 Vector。”说实话,在我十多年的编程生涯中,的确很少使用 Vector,因为它的线程安全是建立在每个方法上都加了 synchronized 关键字的基础上,锁的粒度很高,意味着性能就不咋滴。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
java复制代码public synchronized boolean add(E e) {
modCount++;
add(e, elementData, elementCount);
return true;
}

public synchronized E remove(int index) {
modCount++;
if (index >= elementCount)
throw new ArrayIndexOutOfBoundsException(index);
E oldValue = elementData(index);

int numMoved = elementCount - index - 1;
if (numMoved > 0)
System.arraycopy(elementData, index+1, elementData, index,
numMoved);
elementData[--elementCount] = null; // Let gc do its work

return oldValue;
}

就连 size() 这样的方法上都加了 synchronized,可想而知,Vector 有多铺张浪费,有多锦衣玉食。

如果对 synchronized 关键字不太了解的话,可以点击下面的链接查看我之前写的一篇文章。

我去,你竟然还不会用 synchronized

高并发的情况下,一般都要求性能要给力,Vector 显然不够格,所以被遗忘在角落也是“罪有应得”啊。

02、SynchronizedList

那有些同学可能会说,可以使用 Collections.synchronizedList() 让 ArrayList 变成线程安全啊。

1
2
3
4
5
java复制代码public static <T> List<T> synchronizedList(List<T> list) {
return (list instanceof RandomAccess ?
new Collections.SynchronizedRandomAccessList<>(list) :
new Collections.SynchronizedList<>(list));
}

无论是 SynchronizedRandomAccessList 还是 SynchronizedList,它们都没有在方法级别上使用 synchronized 关键字,而是在方法体内使用了 synchronized(this) 块。

1
2
3
4
5
6
java复制代码public void add(int index, E element) {
synchronized (mutex) {list.add(index, element);}
}
public E remove(int index) {
synchronized (mutex) {return list.remove(index);}
}

其中 mutex 为 this 关键字,也就是当前对象。

1
2
3
4
5
6
java复制代码final Object mutex;     // Object on which to synchronize

SynchronizedCollection(Collection<E> c) {
this.c = Objects.requireNonNull(c);
mutex = this;
}

03、ConcurrentModificationException

ConcurrentModificationException 这个异常不知道同学们有没有遇到过?我先来敲段代码让它发生一次,让同学们认识一下。

1
2
3
4
5
6
7
8
9
10
11
12
java复制代码List<String> list = new ArrayList<>();
list.add("沉默王二");
list.add("沉默王三");
list.add("一个文章真特么有趣的程序员");

for (String str : list) {
if ("沉默王二".equals(str)) {
list.remove(str);
}
}

System.out.println(list);

运行这段代码就会抛出 ConcurrentModificationException:

1
2
3
php复制代码Exception in thread "main" java.util.ConcurrentModificationException
at java.base/java.util.ArrayList$Itr.checkForComodification(ArrayList.java:1012)
at java.base/java.util.ArrayList$Itr.next(ArrayList.java:966)

通过异常的堆栈信息可以查找到,异常发生在 ArrayList 的内部类 Itr 的 checkForComodification() 方法中。

1
2
3
4
java复制代码final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}

也就是说,在执行 checkForComodification() 方法的时候,发现 modCount 和 expectedModCount 不等,就抛出了 ConcurrentModificationException 异常。

为什么会这样呢?之前的代码也没有调用 checkForComodification() 方法啊!

那就只能来看一下反编译后的字节码了,原来 for-each 这个语法糖是通过 Iterator 实现的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
java复制代码List<String> list = new ArrayList();
list.add("沉默王二");
list.add("沉默王三");
list.add("一个文章真特么有趣的程序员");
Iterator var3 = list.iterator();

while (var3.hasNext()) {
String str = (String) var3.next();
if ("沉默王二".equals(str)) {
list.remove(str);
}
}

System.out.println(list);

在执行 list.iterator() 的时候,其实返回的就是 ArrayList 的内部类 Itr。

1
2
3
java复制代码public Iterator<E> iterator() {
return new ArrayList.Itr();
}

迭代器 Iterator 是 fail-fast 的,如果以任何方式(包括 remove 和
add)对迭代器进行修改的话,就会抛出 ConcurrentModificationException。

迭代器在执行 remove() 方法的时候,会对 modCount 加 1。remove() 方法内部会调用 fastRemove() 方法。

1
2
3
4
5
6
7
java复制代码private void fastRemove(Object[] es, int i) {
modCount++;
final int newSize;
if ((newSize = size - 1) > i)
System.arraycopy(es, i + 1, es, i, newSize - i);
es[size = newSize] = null;
}

当在进行下一次 next() 会执行 checkForComodification() 方法,结果发现 modCount 为 4,而 expectedModCount 为 3,于是就抛出了异常。

之所以在单线程的情况下就抛出 ConcurrentModificationException,就是为了在多线程并发的情况下,不冒任何的危险,提前规避掉其他线程对 List 修改的可能性。

ArrayList 返回的迭代器是 fail-fast 的,Vector 的也是,SynchronizedList 的也是。这就意味着它们在多线程环境下通过 for-each 遍历进行增删操作的时候会出问题。

04、CopyOnWriteArrayList

瞧,为了引出 CopyOnWriteArrayList,我花了多少心思。

1
2
3
4
5
6
7
8
9
10
11
12
java复制代码List<String> list = new CopyOnWriteArrayList();
list.add("沉默王二");
list.add("沉默王三");
list.add("一个文章真特么有趣的程序员");

for (String str : list) {
if ("沉默王二".equals(str)) {
list.remove(str);
}
}

System.out.println(list);

把 ArrayList 换成 CopyOnWriteArrayList,程序就能够正常执行了,输出结果如下所示。

1
css复制代码[沉默王三, 一个文章真特么有趣的程序员]

之所以不抛出 ConcurrentModificationException 异常,是因为 CopyOnWriteArrayList 是 fail-safe 的,迭代器遍历的是原有的数组,remove 的时候 remove 的是复制后的新数组,然后再将新数组赋值给原有的数组。

不过,任何在获取迭代器之后对 CopyOnWriteArrayList 的修改将不会及时反映迭代器里。

1
2
3
4
5
6
7
java复制代码CopyOnWriteArrayList<String> list1 =
new CopyOnWriteArrayList<>(new String[] {"沉默王二", "沉默王三"});
Iterator itr = list1.iterator();
list1.add("沉默王四");
while(itr.hasNext()) {
System.out.print(itr.next() + " ");
}

沉默王四并不会出现在输出结果中。

1
复制代码沉默王二 沉默王三

ArrayList 的迭代器 Itr 是支持 remove 的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
java复制代码List<String> list = new ArrayList();
list.add("沉默王二");
list.add("沉默王三");
list.add("一个文章真特么有趣的程序员");
Iterator var3 = list.iterator();

while (var3.hasNext()) {
String str = (String) var3.next();
if ("沉默王二".equals(str)) {
var3.remove();
}
}

System.out.println(list);

程序输出的结果如下所示:

1
css复制代码[沉默王三, 一个文章真特么有趣的程序员]

而 CopyOnWriteArrayList 的迭代器 COWIterator 是不支持 remove 的。

1
2
3
java复制代码public void remove() {
throw new UnsupportedOperationException();
}

CopyOnWriteArrayList 实现了 List 接口,不过,它不在 java.util 包下,而在 java.util.concurrent 包下,算作是 ArrayList 的增强版,线程安全的。

顾名思义,CopyOnWriteArrayList 在进行写操作(add、set、remove)的时候会先进行拷贝,底层是通过数组复制来实现的。

Java 8 的时候,CopyOnWriteArrayList 的增删改操作方法使用的是 ReentrantLock(可重入锁,一个线程获得了锁之后仍然可以反复的加锁,不会出现自己阻塞自己的情况)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
java复制代码public boolean add(E e) {
final ReentrantLock lock = this.lock;
lock.lock();
try {
Object[] elements = getArray();
int len = elements.length;
Object[] newElements = Arrays.copyOf(elements, len + 1);
newElements[len] = e;
setArray(newElements);
return true;
} finally {
lock.unlock();
}
}

Java 14 的时候,已经改成 synchronized 块了。

1
2
3
4
5
6
7
8
9
10
java复制代码public boolean add(E e) {
synchronized (lock) {
Object[] es = getArray();
int len = es.length;
es = Arrays.copyOf(es, len + 1);
es[len] = e;
setArray(es);
return true;
}
}

其中的 lock 是一个 Object 对象(注释上说和 ReentrantLock 有一点关系)。

1
2
3
4
5
java复制代码/**
* The lock protecting all mutators. (We have a mild preference
* for builtin monitors over ReentrantLock when either will do.)
*/
final transient Object lock = new Object();

使用 ReentrantLock 性能更好,还是 synchronized 块性能更好,同学们可以试验一下。不过,从另外一些细节上看,Java 14 的写法比 Java 8 更简洁一些,其中就少了一个 newElements 变量的创建。

再来看 set() 方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
java复制代码public E set(int index, E element) {
synchronized (lock) {
Object[] es = getArray();
E oldValue = elementAt(es, index);

if (oldValue != element) {
es = es.clone();
es[index] = element;
}
// Ensure volatile write semantics even when oldvalue == element
setArray(es);
return oldValue;
}
}

同样使用了 synchronized 块,并且调用了封装好的 clone() 方法进行了复制。

然后来看 remove() 方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
java复制代码public E remove(int index) {
synchronized (lock) {
Object[] es = getArray();
int len = es.length;
E oldValue = elementAt(es, index);
int numMoved = len - index - 1;
Object[] newElements;
if (numMoved == 0)
newElements = Arrays.copyOf(es, len - 1);
else {
newElements = new Object[len - 1];
System.arraycopy(es, 0, newElements, 0, index);
System.arraycopy(es, index + 1, newElements, index,
numMoved);
}
setArray(newElements);
return oldValue;
}
}

synchronized 块是必须的,数组复制(System.arraycopy())也是必须的。

和 Vector 不同的是,CopyOnWriteArrayList 的 get()、size() 方法不再加锁。

1
2
3
4
5
6
7
java复制代码public int size() {
return getArray().length;
}

public E get(int index) {
return elementAt(getArray(), index);
}

简单总结一下就是:第一,CopyOnWriteArrayList 在修改时,复制出一个新数组,修改的操作在新数组中完成,最后将新数组赋值给原有的数组引用。第二,CopyOnWriteArrayList 的写加锁,读不加锁。

CopyOnWriteArrayList 有很多优势,但数组复制是沉重的,如果写的操作比较多,而读的操作比较少,内存就会被占用得比较多;另外,CopyOnWriteArrayList 无法保证数据是实时同步的,因为读写操作是分离的,写的操作都建立在复制的新数组上,而读的是原有的数组。

05、最后

如果九年前,我就看到了这样一篇文章,一定就不会被老马刁难呢,保不准还能再拖延半个小时,让他多问二十个问题。但我想同学们一定是比我幸运的,至少现在看到了,不晚,对不对?


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

注:如果文章有任何问题,欢迎毫不留情地指正。

如果你觉得文章对你有些帮助欢迎微信搜索「沉默王二」第一时间阅读,回复「小白」更有我肝了 4 万+字的 Java 小白手册 2.0 版,本文 GitHub github.com/itwanger 已收录,欢迎 star。

本文转载自: 掘金

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

1…789790791…956

开发者博客

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