2024年2月

HashMap

HashMap 继承自 AbstractMap,实现了 Map 接口,基于哈希表实现,元素以键值对的方式存储,允许键和值为 null。因为 key 不允许重复,因此只能有一个键为 null。HashMap 不能保证放入元素的顺序,它是无序的,和放入的顺序并不相同。HashMap 是线程不安全的。

1. 哈希表

哈希表基于数组实现,当前元素的关键字通过某个哈希函数得到一个哈希值,这个哈希值映射到数组中的某个位置。哈希函数的好坏直接决定该哈希表的性能

当我们对某个元素进行哈希运算,得到一个存储地址,然后要进行插入的时候,发现已经被其他元素占用了,这就是所谓的哈希冲突,也叫哈希碰撞

解决方法如下:

  • 开放定址法:当冲突发生时,使用某种探查技术在散列表中形成一个探查序列,沿此序列逐个单元地查找,直到碰到一个开放的地址(即该地址单元为空),将待插入的新结点存入该地址单元
  • 链地址法:可将散列表定义为一个由 m 个头指针组成的指针数组,将所有关键字为同义词的结点链接在同一个单链表中,初始时数组中各分量的初值应均为 1
  • 再哈希法:同时构造多个不同的哈希函数,发生冲突时再换别的哈希函数

2. JDK1.7 实现原理

HashMap 由数组和链表实现对数据的存储,HashMap 里面实现一个静态内部类 Entry,包含 Key、Value 和对 key 的 hashcode 值进行 hash 运算后得到的 Hash 值,它还具有 Next 指针,可以连接下一个 Entry 实体,以此来解决 Hash 冲突的问题

3. JDK1.7 存储流程

  • 初始化哈希表:真正初始化哈希表(初始化存储数组)是在第一次添加键值对时
    • 数组为空:设置默认阈值与初始容量
    • 设置了传入容量:将传入的容量大小转化为大于自身的最小的二次幂。如果超出最大允许容量,则设置为最大值
  • 判断键是否为空:对 null 作哈希运算,结果为 0,所以以 null 为键的键值对一般放在数组首位,该位置的新值总是会覆盖旧值
  • 计算元素存放位置:首先根据 key 的 hashcode 计算 hash 值,然后根据 hash 值计算 index 下标值
    • 哈希冲突:当发生哈希冲突时,为了保证键的唯一性,哈希表不会马上在链表中插入新数据,而是先遍历链表,查找该键是否已存在,若已存在,替换即可
  • 添加键值对:使用头插法,新添加元素放在链表头部,原始节点作为新节点的后继节点

4. JDK1.7 哈希函数

JDK 1.7 做了 9 次扰动处理 = 4 次位运算 + 5 次异或运算

5. JDK1.7 下标计算

计算元素位置采用的是 & 运算,该方法返回 h & (length - 1),其中 h 为 key 的 hash 值,length 是数组长度

6. JDK1.7 扩容机制

先判断是否需要扩容,再插入

7. JDK1.8 实现原理

1.8 以前 HashMap 采用 数组 + 链表 实现,即使用链表处理冲突,同一 hash 值的节点都存储在一个链表里。但是当同一 hash 值相等的元素较多时,通过 key 值依次查找的效率较低。JDK1.8 中,HashMap 采用 数组 + 链表 + 红黑树 实现,当链表长度超过阈值时,将链表转换为红黑树,大大减少了查找时间

8. JDK1.8 存储流程

  • 初始化哈希表:真正初始化哈希表(初始化存储数组)是在第一次添加键值对时
    • 数组为空:设置默认阈值与初始容量
    • 设置了传入容量:将传入的容量大小转化为大于自身的最小的二次幂。如果超出最大允许容量,则设置为最大值
  • 判断键是否为空:对 null 作哈希运算,结果为 0,所以以 null 为键的键值对一般放在数组首位,该位置的新值总是会覆盖旧值
  • 计算元素存放位置:首先根据 key 的 hashcode 计算 hash 值,然后根据 hash 值计算 index 下标值
    • 哈希冲突:当发生哈希冲突时,为了保证键的唯一性,哈希表不会马上在链表中插入新数据,而是先遍历链表,查找该键是否已存在,若已存在,替换即可;如果不存在,判断当前节点类型是不是树型节点,如果是树型节点,创造树型节点插入红黑树中;如果不是树型节点,创建普通 Node 加入链表中;判断链表长度是否大于 8 并且数组长度大于 64, 大于的话链表转换为红黑树
  • 添加键值对:链表的插入方式从头插法改成了尾插法,简单说就是插入时,如果数组位置上已经有元素,1.7 将新元素放到数组中,原始节点作为新节点的后继节点,1.8 遍历链表,将元素放置到链表的最后

9. JDK1.8 哈希函数

JDK 1.8 简化了扰动函数 = 只做了 2 次扰动 = 1 次位运算 + 1 次异或运算,本质是哈希码的低 16 位异或高 16 位

10. JDK1.8 下标计算

计算元素位置采用的是 & 运算,该方法返回 h & (length - 1),其中 h 为 key 的 hash 值,length 是数组长度

11. JDK1.8 扩容机制

先进行插入,插入完成再判断是否需要扩容。扩容时,1.7 需要对原数组中的元素进行重新 hash 定位,以确定在新数组中的位置,1.8 采用更简单的判断逻辑,位置不变或索引 + 旧容量大小


相关问题

1. 扩容机制?

HashMap 使用懒扩容机制,只有在进行 PUT 操作时才会判断是否扩容,需要用到的属性有两个:

  • 阈值:threshold,初始容量为 16,扩容时需要使用
  • 负载因子:loadFactor,默认是 0.75,用于减缓哈希冲突,如果等到数组满了才扩容,那是某些桶可能就不止一个元素了

阈值 = 数组大小 * 负载因子,容器默认大小为 16,此时 阈值 = 16 * 0.75 = 12,如果当前数组中元素的数量大于阈值,则将数组大小扩大为原来的两倍,并将原来数组中的元素进行重新放到新数组中。需要注意的是,每次扩容之后,都要重新计算元素在数组的位置,因为元素所在位置和数组长度有关,既然扩容后数组长度发生了变化,那么元素位置也会发生变化

2. 针对扩容机制的优化方案?

我们可以自定义数组容量及加载因子的大小。加载因子过大时,HashMap 内的数组使用率高,内部极有可能形成 Entry 链,影响查找速度。加载因子过小时,HashMap 内的数组使用率低,内部不会生成 Entry 链,或者生成的 Entry 链很短,提高了查找速度,不过会占用更多的内存。所以要进行时间和空间的折中考虑

3. 为什么不直接使用 hashcode 作为存储数组的下标位置?

因为 key.hashCode() 函数调用的是 key 键值类型自带的哈希函数,返回 int 型散列值。int 值范围为非常大,前后加起来大概 40 亿的映射空间,一个 40 亿长度的数组,内存是放不下的。而且使用之前还需要对数组的长度取模运算,得到余数才能用来访问数组下标

4. 为什么要作扰动处理?

加大哈希码低位的随机性,使得分布更均匀,从而提高对应数组存储下标位置的随机性 & 均匀性,最终减少哈希冲突

5. 为什么采用(哈希码 & 数组长度减一)这种方式?

这也解释了为什么 HashMap 的数组长度要取 2 的整数幂。因为 数组长度 减一 正好相当于一个低位掩码。与操作的结果就是散列值的高位全部归零,只保留低位值,用来做数组下标访问,其结果与取模运算相同,效率却要高很多

6. 为什么在 1.8 使用尾插法插入新结点?

因为 1.7 扩容时,元素会被重新移动到新的数组,而使用头插法会使链表发生反转,比如原本是 A-B-C 的链表,扩容之后就变成 C-B-A 了,在多线程环境下,会导致链表成环的问题。而尾插法,在扩容时会保持链表原本的顺序不变,就不会出现链表成环的问题

前言

这是一篇很水的文章,没有任何技术含量,在
Github
已经有很多人对
Avalonia

OpenGL ES
支持进行了讨论,但是我还是想写一篇文章来记录一下我是如何在
Avalonia
中使用
OpenGL ES
的。

介绍

在介绍
Avalonia

OpenGL ES
之前,我们先来了解一下什么是
Avalonia

OpenGL ES
以及
Avalonia
是如何支持
OpenGL ES
的。

Angle 和 OpenGL ES

ANGLE
是一个开源的项目,它的目标是将
OpenGL ES
2.0、3.0

3.1
应用程序转换为
Direct3D 11

Vulkan

Metal

OpenGL 3.0+
应用程序。

ANGLE
通过将
OpenGL ES
API
调用转换为
Direct3D

Vulkan

Metal
API
调用来实现这一目标。

ANGLE
也提供了一个实现
OpenGL ES
2.0、3.0

3.1
的库,这样就可以在不支持
OpenGL ES
的平台上运行
OpenGL ES
应用程序。

以下是摘自
ANGLE
项目的介绍:

Level of OpenGL ES support via backing renderers
Direct3D 9 Direct3D 11 Desktop GL GL ES Vulkan Metal
OpenGL ES 2.0 complete complete complete complete complete complete
OpenGL ES 3.0 complete complete complete complete complete
OpenGL ES 3.1 [incomplete][ES31OnD3D] complete complete complete
OpenGL ES 3.2 in progress in progress complete
Platform support via backing renderers
Direct3D 9 Direct3D 11 Desktop GL GL ES Vulkan Metal
Windows complete complete complete complete complete
Linux complete complete
Mac OS X complete complete [1]
iOS complete [2]
Chrome OS complete planned
Android complete complete
GGP (Stadia) complete
Fuchsia complete

ANGLE
项目的地址:
https://github.com/google/angle

Avalonia

Avalonia
是一个
.NET
平台的
XAML

C#

UI
框架,它的目标是创建一个跨平台的
UI
框架,支持
Windows

Linux

MacOS
。(摘自
Avalonia
官网)

Avalonia
渲染
API
的实现是基于
SkiaSharp
的,
SkiaSharp

Google

Skia
图形库的
.NET
实现。
为了实现硬件加速
SkiaSharp
是支持
OpenGL

OpenGL ES
渲染的,为了统一平台,
Avalonia
选择了
ANGLE
作为
OpenGL ES
的实现。

Avalonia
项目的地址:
https://github.com/AvaloniaUI/Avalonia

如何使用 OpenGL ES

Avalonia
中的
OpenGL ES
是通过
ANGLE
来实现的,在
Avalonia
项目中已经集成了
ANGLE
,所以我们不需要再去关心
ANGLE
的集成问题。

在该框架中使用
OpenGL ES
的方式是通过
Avalonia
提供的
OpenGlControlBase
控件来实现的,我们只需要集成
OpenGlControlBase
控件并重写
OnOpenGlInit
方法就可以获取到
OpenGL ES

Context
以及函数指针了。


Avalonia
中使用
OpenGL ES
的步骤如下:(我这边使用的是 Silk.NET 来调用 OpenGL ES 的函数)

using Silk.NET.OpenGLES;

namespace GraphicsHostApp.Graphics.OpenGL;

public class Renderer : OpenGlControlBase, IGraphicsHost<GL>
{
	private GL _gl;

	protected override void OnOpenGlInit(GlInterface gl)
	{
		// 获取 OpenGL ES 的 函数指针。
		_gl ??= GL.GetApi(gl.GetProcAddress);

		// 后续初始化操作。
	}

	protected override void OnOpenGlDeinit(GlInterface gl)
	{
		// 释放 OpenGL ES 的资源。
		Code ...

		// 释放函数指针。
		_gl.Dispose();
		_gl = null;
	}

	protected override void OnOpenGlRender(GlInterface gl, int fb)
	{
		// 更新操作。
		Code ...

		// 渲染操作。 注:这里需要注意的是,父类代码并没有更新视口,所以需要手动更新视口。gl.Viewport(0, 0, Width, Height);
		Code ...

		// 提交渲染到主循环中。
		Dispatcher.UIThread.Post(RequestNextFrameRendering, DispatcherPriority.Render);
	}
}

结语

Avalonia
是一个很不错的
UI
框架,它的
OpenGL ES
支持也是很完善的,但是在使用
OpenGL ES
的时候需要注意的是
OpenGL ES

Context
是在
OpenGlControlBase

OnOpenGlInit
方法中创建的,所以在
OnOpenGlRender
方法中使用
OpenGL ES
的函数指针的时候需要注意
Context
是否已经创建了。

其次,
OpenGL ES

Context
是线程相关的,所以在使用
OpenGL ES
的时候需要注意
Context
的线程问题。

在使用
OpenGL ES
扩展的时候要注意
ANGLE
是否支持该扩展,如果不支持的话需要自己去实现。

演示项目

  • GraphicsHostApp

    Avalonia

    OpenGL ES
    的演示项目,介绍了如何使用
    C#

    C++
    来实现
    OpenGL ES
    渲染。)

参考

引言

在日常业务开发中,我们时常需要使用一些其他公司的服务,调用第三方系统的接口,这时就会涉及到网络请求,通常我们可以使用
HttpClient

OkHttp
等框架去完成网络请求。随着RESTful API的普及,一个高效、简洁且易于维护的HTTP客户端库显得尤为关键。而本文主要介绍一款强大的网络客户端库:
Retrofit2

Retrofit2简介

Retrofit2是什么?

Retrofit2
是一个由Square公司精心打造并开源的
Java

Android
双平台适用的
RESTful API
客户端库,其核心构建在性能卓越的
OkHttp
库之上。通过精巧的设计原理,
Retrofit2
将原本复杂的HTTP网络请求过程高度抽象为直观且类型安全的接口调用模式,从而极大地简化了应用程序与后端API之间的交互逻辑。

开发者利用
Retrofit2
能够以注解驱动的方式来声明和定义API接口,轻松指定HTTP方法、URL路径以及请求参数等关键信息,进而自动生成相应的请求实现代码。该框架不仅支持同步及异步两种调用方式,还内置了对JSON数据序列化和反序列化的自动化处理能力,这意味着无论是发送请求还是解析响应,都能无缝转换成或从对应的Java对象进行操作。

此外,
Retrofit2
具备强大的扩展性,允许开发人员根据项目需求定制各种高级功能,如自定义转换器以适应不同数据格式,添加拦截器以实现全局请求/响应处理,以及集成多种认证机制,充分满足现代应用程序中面对复杂网络环境的各种需求。

Retrofit2能做什么?

Retrofit2
的主要功能包括:

  1. 类型安全API设计

    Retrofit2
    赋予开发者以声明式接口定义的方式来确保网络请求的类型安全性。这意味着通过在接口方法上使用注解来精确指定HTTP请求参数和响应数据结构,框架会自动进行类型校验并确保数据在传输过程中严格符合预期类型,从而消除类型不匹配引发的运行时错误。

  2. 网络请求流程精简

    Retrofit2
    极大地简化了发起网络请求的步骤。开发人员仅需专注于设计与后端服务交互的API接口及相应的
    HTTP
    动作,框架会自动生成底层逻辑代码,无需手动编写创建请求、设置
    Header
    或解析响应内容等繁琐环节,极大地提高了开发效率。

  3. 内置数据转换机制
    :为便于数据处理,
    Retrofit2
    集成了多种数据转换器(
    Converter
    ),能够轻松地将从服务器接收到的HTTP原始数据流转换成Java对象,支持常见的数据格式如
    JSON

    XML
    以及其他可通过扩展实现的格式,这使得数据模型与实际业务逻辑之间的映射变得直观且易于管理。

  4. 异步执行与回调集成
    :针对移动应用中避免阻塞UI线程的需求,Retrofit2全面支持异步网络请求。它允许开发者采用回调函数或者结合RxJava等反应式编程库来优雅地处理异步任务,确保即便在网络请求执行期间也能保持流畅的用户体验和应用性能。

Retrofit2的优点

  1. 代码简化与一致性
    :通过提供一种声明式的方式来设计和实现网络请求接口,
    Retrofit2
    极大地减少了开发人员在处理网络通信时所需编写的重复性代码量。开发者仅需关注业务逻辑相关的API描述,无需手动构建和管理复杂的HTTP请求。

  2. 提升可读性和维护性
    :框架强调清晰的结构和注解驱动的配置方式,使得网络请求逻辑更加直观且易于理解,进而提高了代码的可读性和维护性。开发者能够快速识别并定位各个网络操作的意义和行为。

  3. 类型安全保证
    :通过集成类型安全的
    API
    设计,
    Retrofit2
    消除了因参数拼写错误或类型不匹配所引发的运行时异常风险。它确保了数据交换过程中参数类型的正确性,增强了应用的整体健壮性。

  4. 高效稳定集成

    Retrofit2
    无缝集成了高性能的
    OkHttp
    库,充分利用了其在网络连接复用、缓存策略、失败重试等方面的性能优势,从而有效提升了网络请求的执行效率及服务稳定性,为应用程序提供了更强大的网络支持基础架构。

Retrofit2 VS HttpClient

  1. 现代化的 API 设计

    Retrofit2
    使用现代编程风格,通过注解定义
    HTTP
    请求接口,代码简洁易读。相比之下,
    HttpClient
    需要手动构建
    Request
    和处理响应,代码结构更为繁琐。

  2. 自动转换数据

    Retrofit2
    提供了内置或自定义的数据转换器,如
    GsonConverterFactory
    ,可以自动将
    JSON
    或其他格式的数据转换为
    Java
    对象,简化了数据的序列化和反序列化过程。
    HttpClient
    则需要手动处理数据转换,操作相对繁琐。

  3. 异步与同步支持

    Retrofit2
    支持同步和异步两种网络请求方式,提供了基于
    Call

    Observable
    等类型的异步调用方式,方便结合
    RxJava
    等响应式编程框架使用,极大地提升了用户体验和应用程序性能。
    HttpClient
    在异步支持方面较为局限。

  4. 面向接口编程

    Retrofit2
    通过定义服务接口来描述API端点,使得网络层与其他业务逻辑解耦,提高了代码组织性和可测试性。相比之下,
    HttpClient
    直接操作
    HttpRequest

    HttpResponse
    实例,耦合度较高。

  5. 兼容性与性能

    官方不再推荐使用
    Apache HttpClient
    ,而
    OkHttp

    Retrofit2
    底层依赖库)经过持续优化,在性能、连接复用、缓存策略以及对
    HTTP/2
    协议的支持等方面表现更优。

  6. 易于扩展

    Retrofit2
    可以很容易地添加拦截器(
    Interceptor
    )进行诸如身份验证、日志记录和重试机制等功能的扩展。虽然]
    HttpClient
    的扩展性也很强,但需要更多手工编码。

  7. 社区活跃与更新频繁

    Retrofit2

    OkHttp
    社区活跃,更新迭代较快,能快速跟进新的技术和最佳实践,确保开发者能够利用最新的技术改进和安全更新。

Retrofit2
在简化
RESTful API
客户端开发、提高效率、易用性、可维护性以及对现代网络特性的支持上均优于旧版的
HttpClient

Retrofit2 VS OkHttp

  1. API 接口定义简洁明了

    Retrofit2 使用注解(Annotations)来描述 HTTP 请求方法、URL、参数等,开发者只需通过定义 Java 接口就能清晰地表达出网络调用的意图。相比之下,OkHttp 需要开发者直接处理复杂的 HTTP 请求构建逻辑。

  2. 自动序列化与反序列化

    Retrofit2 提供了转换器(Converter)支持,如 GsonConverterFactory、JacksonConverterFactory 等,能够自动将 JSON 或其他格式的数据转换为 Java 对象以及相反的操作,极大地简化了数据处理过程。而 OkHttp 需要开发者手动处理数据转换。

  3. 同步/异步模式统一处理

    Retrofit2 不仅支持同步请求,还对异步请求提供了统一的 Call 或 Observable 返回类型,方便在 Android 中进行非阻塞式编程,并且易于结合 RxJava 等响应式库使用。相比之下,OkHttp 的异步请求处理需要开发者自行管理。

  4. 丰富的注解体系

    Retrofit2 提供了多种注解以支持不同的请求类型(GET、POST、PUT、DELETE 等)、路径参数、查询参数、表单提交、文件上传、多部分请求等,可以灵活配置请求内容。而 OkHttp 的使用需要开发者手动构建请求参数和处理响应。

  5. 强大的扩展性

    Retrofit2 支持自定义拦截器(Interceptor),可以在请求前后添加额外的业务逻辑,如认证、日志记录、缓存策略等。同时,可以自由配置 OkHttpClient 实例,充分利用 OkHttp 的所有特性,如连接池、重试机制、HTTP/2 支持等。相比之下,OkHttp 更专注于网络通信的核心功能。

  6. 代码可读性强

    Retrofit2 将网络请求抽象成一个服务接口的形式,使得代码更易于阅读和维护,提高了整体项目的组织性和整洁度。相比之下,OkHttp 的使用需要开发者更多地关注底层的网络通信细节。

  7. 降低耦合度

    使用 Retrofit2 可以将网络访问层与应用的其他组件更好地解耦,使得业务逻辑代码更加关注于处理业务本身,而不是如何发起网络请求。相比之下,OkHttp 的使用需要开发者更多地处理网络请求的细节,耦合度较高。

虽然 OkHttp 是一个高性能的 HTTP 客户端,专注于网络通信的核心功能,但 Retrofit2 在此基础上封装了一层高级抽象,让开发者能以声明式的方式编写网络请求代码,降低了复杂度并提升了开发效率。

Retrofit2使用

引入依赖

<dependency>
	<groupId>com.squareup.retrofit2</groupId>
	<artifactId>retrofit</artifactId>
	<version>2.9.0</version>
</dependency>

<!-- 示例使用jackson的converter -->
<dependency>
	<groupId>com.squareup.retrofit2</groupId>
	<artifactId>converter-jackson</artifactId>
	<version>2.9.0</version>
</dependency>

定义API接口


Retrofit
框架中,构建与服务器的通信接口是通过定义清晰、结构化的API接口来实现的。这个过程涵盖了详细指定请求方式、路径以及相关参数等关键信息。具体来说,每个接口方法代表了一种特定的HTTP交互模式,明确指示了请求类型(如
GET

POST

PUT

DELETE
)和目标
URL
路径。

请求方法

在接口方法上应用诸如
@GET

@POST

@PUT

@DELETE
等注解是为了精确映射到相应的HTTP动作。

@POST("user/add")

@GET("user/info/{id}")

// 也可以指定查询参数
@GET("user/list?pageSize=50")
URL操作

利用
@Path

@Query

@Body
注解能够进一步细化接口描述,分别用于设定路径中的动态变量、查询字符串参数以及
HTTP
请求体内容。接口方法可以接受不同类型的参数,这些参数会根据注解类型被正确地插入到请求的不同部分。
使用
@Path
注解的参数会在实际调用时将传入值插入到
URL
路径中相应的位置

@GET("group/{id}/users")  
Call<List<UserInfoResponse>> groupList(@Path("id") int groupId);

还可以通过
@Query
参数添加查询参数。

@GET("group/{id}/users")  
Call<List<UserInfoResponse>> groupList(@Path("id") int groupId, @Query("pageSize") Integer pageSize);

对于复杂的查询参数组合,可以使用
Map

@GET("group/{id}/users")  
Call<List<UserInfoResponse>> groupList(@Path("id") int groupId, @QueryMap Map<String, String> options);
请求体

对于请求对象,可以使用
@Body
注解指定对象作为
HTTP
请求体。
@Body
注解通常用于指定将对象作为
JSON
格式的数据传输到服务器。当您在 Retrofit 接口方法中使用
@Body
注解时,
Retrofit
将会使用内部的转换器(如
GsonConverter
或者
JacksonConverter
)将对象转换为
JSON
格式的字符串,并将其作为请求的请求体发送到服务器。
通常情况下,
@Body
注解用于
POST
或者
PUT
请求,其中请求的主体包含了要传输的对象的
JSON
表示形式。

@POST("users/new")  
Call<UserInfoResponse> createUser(@Body UserInfoRequest user);

通常情况下 @Body注解用于指定JSON格式的数据传输,但Retrofit并不会强制要求请求体的格式必须是JSON。您也可以使用其他格式的数据,例如XML或者纯文本,只要在请求体中提供了正确的数据格式,并且服务器能够正确地解析这种格式的数据。

表单数据和Multipart请求

方法还可以声明发送表单数据和多部分请求数据
使用
@FormUrlEncoded

@Field
或者
@FieldMap
将发送表单数据。

  @FormUrlEncoded
  @POST("users/new")
  Call<UserInfoResponse> createUser1(@Field("name") String name, @Field("passowrd") String password);

  @FormUrlEncoded
  @POST("users/new")
  Call<UserInfoResponse> createUser2(@FieldMap Map<String, Object> paramMap);

同时他还支持发送多部分请求,例如文件上传。在方法上使用
@Multipart
注解用于发送多部分请求,而参数要使用
@Part
注解。在
Retrofit
接口方法中使用
@Multipart
注解时,
Retrofit
将会使用
multipart/form-data
格式来发送请求,这种格式允许同时上传文本数据和二进制文件数据。

@Multipart  
@POST("user/image")  
Call<UserInfoResponse> updateUser(@Part("image") RequestBody userImage, @Part("imageDesc") RequestBody imageDesc);

@Part
注解用于声明每个部分的内容,其中可以是
RequestBody
类型的文本或者二进制数据,也可以是
MultipartBody.Part
类型的文件或者其他二进制数据。这样的话,就可以通过多个
@Part
注解来声明不同类型的部分,以满足不同的上传需求

Header信息

使用
@Headers
注解为方法设置静态头部。

@Headers({  
"Accept: application/json, text/plain, */*",  
"User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36",  
"Cookie:xxxxxx"  
})  
@POST("users/new")  
Call<UserInfoResponse> createUser(@Body UserInfoRequest user);

使用用
@Header
或者
HeaderMap
注解动态更新请求头。必须提供相应的参数给
@Header
。如果值为
null
,则头部将被省略。否则,将对值调用
toString
,并使用结果。

@POST("users/new")  
Call<UserInfoResponse> createUser(@Header("Cookie") String cookie, @Body UserInfoRequest user);  
  
@POST("users/new")  
Call<UserInfoResponse> createUser2(@HeaderMap Map<String, String> headerMap, @Body UserInfoRequest user);

关于Header参数,我们还可以通过OkHttp的拦截器进行操作。

方法返回值

API接口方法通常返回
Call<T>
类型的对象,这里的T代表期望从服务器接收的数据类型。这种方式使得开发者能方便地利用 Retrofit 提供的回调机制或其他响应式编程库(如RxJava)来处理网络请求的结果,从而确保了对异步操作的良好控制和管理。

public interface MyClientService {
    @POST("test/add")
    Call<TestResponse> addTest(@Body TestRequest testRequest);

	@GET("group/{id}/users")
	Call<List<User>> groupList(@Path("id") int groupId);
}

创建Retrofit实例

Retrofit
框架的核心组件是
Retrofit
实例。
Retrofit
实例作为整个框架的心脏,不仅负责搭建网络请求所需的基础设施,还承担起发起请求、转换数据和管理响应生命周期的任务。

Retrofit retrofit = new Retrofit.Builder()
				// 设置 API 的基础 URL
                .baseUrl("http://localhost:8080/coderacademy/") 
                .addConverterFactory(JacksonConverterFactory.create())
                .build();
baseUrl设置

其中
baseUrl
用于指定请求服务器的根地址或者
API
的基础路径。
Retrofit
会自动将
baseUrl
和方法注解中的相对路径结合起来生成实际请求的完整URL。例如对上述示例中:

public interface MyClientService {
    @POST("test/add")
    Call<TestResponse> addTest(@Body TestRequest testRequest);
}

最终的请求url为:
localhost:8080/coderacademy/test/add

关于
baseUrl
的设置有一些注意事项:

  • baseUrl设置必须以
    /
    结尾,否则汇报错。
    image.png

  • 请求方法中的相对路径(不以"/"开头),将会正确附加在以斜杠结尾的 baseUrl 的路径后面。这确保了正确的 URL 结果。如baseUrl
    http://localhost:8080/coderacademy/
    , 方法url为
    test/add
    ,则最终的路径为:
    localhost:8080/coderacademy/test/add

  • 请求方法中的绝对路径(以"/"开头),忽略
    baseUrl
    中的路径组件,只保留host部分,最终的
    URL
    将只包含
    baseUrl
    的主机部分和方法的路径。如baseUrl
    http://localhost:8080/coderacademy/
    , 方法url为
    /test/add
    ,则最终的路径为:
    localhost:8080/test/add

  • 请求方法中的路径可以是完整的
    URL
    ,如果方法路径是完整的URL,则会替换
    baseUrl
    。如baseUrl为
    http://localhost:8080/coderacademy/
    ,而方法url为
    http://localhost:8081/coderacademy/test/add
    ,则最终的url为:
    http://localhost:8081/coderacademy/test/add

Converter设置

Retrofit
默认只能将
HTTP
响应主体反序列化为
OkHttp

ResponseBody
类型,并且只能接受其
RequestBody
类型用于
@Body
注解。为了支持其他类型,可以添加转换器。

官方提供了8种转换器:

转换器 功能 使用依赖
Gson 将 JSON 数据转换为 Java 对象,以及将 Java 对象转换为 JSON 数据。 com.squareup.retrofit2:converter-gson
Jackson 将JSON数据转换为 Java 对象,以及将 Java 对象转换为 JSON 数据。 com.squareup.retrofit2:converter-jackson
Moshi 将 JSON 数据转换为 Java 对象,以及将 Java 对象转换为 JSON 数据。 com.squareup.retrofit2:converter-moshi
Protobuf 将 Protocol Buffers 数据转换为 Java 对象,以及将 Java 对象转换为 Protocol Buffers 数据。 com.squareup.retrofit2:converter-protobuf
Wire 将 Wire 数据转换为 Java 对象,以及将 Java 对象转换为 Wire 数据。 com.squareup.retrofit2:converter-wire
Simple XML 将 XML 数据转换为 Java 对象,以及将 Java 对象转换为 XML 数据。 com.squareup.retrofit2:converter-simplexml
JAXB 将 XML 数据转换为 Java 对象,以及将 Java 对象转换为 XML 数据。 com.squareup.retrofit2:converter-jaxb
Scalars 将原始类型、包装类型和字符串转换为 RequestBody,以及将 ResponseBody 转换为原始类型、包装类型和字符串。 com.squareup.retrofit2:converter-scalars
除了官方提供的这几种转换器以外,如果使用了
Retrofit
默认不支持的内容格式的
API
进行通信(例如
YAML

TXT
、自定义格式),或者使用不同的库来实现现有格式(请求与响应是不同的格式),我们也可以实现自定义转换器。

除此之外
Retrofit
还可以跟
OkHttpClient
搭配使用,实现其高级功能,通过
OkHttpClient
,您可以实现诸如网络连接池、超时设置、重试机制、拦截器等高级功能。而
Retrofit
则提供了简化的
API
,使得使用这些高级功能变得更加方便。

OkHttpClient okHttpClient = new OkHttpClient.Builder()
    .connectTimeout(30, TimeUnit.SECONDS) // 设置连接超时时间
    .readTimeout(30, TimeUnit.SECONDS)    // 设置读取超时时间
    .writeTimeout(30, TimeUnit.SECONDS)   // 设置写入超时时间
    .addInterceptor(new LoggingInterceptor()) // 添加日志拦截器
    .build();

Retrofit retrofit = new Retrofit.Builder()
    .baseUrl("http://localhost:8080/coderacademy/")
    .client(okHttpClient) // 设置自定义的 OkHttpClient
    .addConverterFactory(GsonConverterFactory.create())
    .build();

创建请求接口实例,发起请求

在创建完
Retrofit
实例之后,接下来就需要通过调用
Retrofit
实例的
create()
方法来创建
API
接口的实例。然后就可以使用该实例调用定义在接口中的方法来发起网络请求。

MyClientService myClientService = retrofit.create(MyClientService.class);

TestRequest testRequest = new TestRequest();
testRequest.setName("码农Academy");
testRequest.setPassword("12131");
// 发起请求
Call<TestResponse> call = myClientService.addTest(testRequest);
try {
	Response<TestResponse> response = call.execute();
	System.out.println("是否请求成功:"+response.isSuccessful());
	System.out.println("响应:"+ response.toString());
	TestResponse testResponse = response.body();
	System.out.println("请求结果:"+ testResponse.toString());
}catch (Exception e){
	e.printStackTrace();
}


Retrofit
中,
Call
对象代表了一个待执行的网络请求。它是一个表示单个异步或同步执行的请求的对象。
Call
接口定义了执行网络请求和处理响应的方法。
Call
接口的泛型类型参数表示了该网络请求的响应类型。例如,
Call<TestResponse>
表示该网络请求的响应是一个
TestResponse
对象响应。

execute()
方法用于同步执行网络请求,并返回一个
Response
对象。当调用
execute()
方法时,请求将立即发出,当前线程将被阻塞直到请求完成并返回响应。
Response
对象包含了网络请求的响应数据,可以通过调用
body()
方法来获取响应主体。

另外,还可以使用
Call
对象来发起异步网络请求。异步请求允许您在发出请求后继续执行其他代码,而不必等待网络请求完成。当请求完成后,
Retrofit
将在后台线程上调用您提供的回调方法,以处理响应数据。

Call<TestResponse> call = myClientService.addTest(testRequest);

try {
	call.enqueue(new Callback<TestResponse>() {
		@Override
		public void onResponse(Call<TestResponse> call, Response<TestResponse> response) {
			System.out.println("是否请求成功:"+response.isSuccessful());
			System.out.println("响应:"+ response.toString());
			TestResponse testResponse = response.body();
			System.out.println("请求结果:"+ testResponse.toString());
		}

		@Override
		public void onFailure(Call<TestResponse> call, Throwable t) {
			// 请求失败结果
		}
	});
   
}catch (Exception e){
	e.printStackTrace();
}

异步请求时,需要实现
Callback
接口,该接口定义了处理成功和失败响应的方法。在
onResponse
方法中处理成功响应,在
onFailure
方法中处理失败响应。

然后使用
Call
对象的
enqueue()
方法来执行异步网络请求,并传入
Callback

Retrofit
将在后台线程上执行网络请求,并在请求完成后调用相应的回调方法。

到此一个使用
Retrofit2
发起请求的功能就完成了。接下来我们看一下
Retrofit2
的一些高级功能。

Retrofit2的高级功能

拦截器

Retrofit
的高级功能通常需要与
OkHttpClient
结合使用才能实现。
OkHttpClient
是一个强大的
HTTP
客户端库,
Retrofit
是基于它构建的,并且
Retrofit
默认使用
OkHttpClient
作为其底层的网络请求库。

通过
OkHttpClient
,您可以实现诸如网络连接池、超时设置、重试机制、拦截器等高级功能。而
Retrofit
则提供了简化的
API
,使得使用这些高级功能变得更加方便。

OkHttpClient okHttpClient = new OkHttpClient.Builder()
    .connectTimeout(30, TimeUnit.SECONDS) // 设置连接超时时间
    .readTimeout(30, TimeUnit.SECONDS)    // 设置读取超时时间
    .writeTimeout(30, TimeUnit.SECONDS)   // 设置写入超时时间
    .addInterceptor(new LoggingInterceptor()) // 添加日志拦截器
    .build();

Retrofit retrofit = new Retrofit.Builder()
    .baseUrl("http://localhost:8080/coderacademy/")
    .client(okHttpClient) // 设置自定义的 OkHttpClient
    .addConverterFactory(GsonConverterFactory.create())
    .build();

对于拦截器,在实际开发中有较多需要使用的场景,比如第三方服务需要使用一些签名验证手段,请求数据进行加密等,我们都可以统一在拦截器中进行处理。自定义拦截器,我们需要实现
Interceptor
接口,实现
intercept()
方法。

@Slf4j
public class MyAuthInterceptor implements Interceptor {
    
    @NotNull
    @Override
    public Response intercept(@NotNull Chain chain) throws IOException {

        String appKey = "MyKey";
        String appToken = "MyToken";

        Request request = chain.request();
        Request.Builder builder = request.newBuilder();
        builder.addHeader("Api-Key", appKey).addHeader("Api-Secret", appToken);
        request = builder.build();
        return chain.proceed(request);
    }
}

传入拦截器:

OkHttpClient okHttpClient = new OkHttpClient.Builder()
    .connectTimeout(30, TimeUnit.SECONDS) // 设置连接超时时间
    .readTimeout(30, TimeUnit.SECONDS)    // 设置读取超时时间
    .writeTimeout(30, TimeUnit.SECONDS)   // 设置写入超时时间
    .addInterceptor(new LoggingInterceptor()) // 添加日志拦截器
    .addInterceptor(new MyAuthInterceptor()) 
    .build();

转换器

前面内容已经提到对于转换器,出了
Retrofit2
提供的8种转换器以外,有些特别的请求体这几种转换器不能满足,此时,我们可以自定义转换器。需要继承
Converter.Factory
类,重写
requestBodyConverter

reponseBodyConverter
方法即可。

public class CustomBodyConverterFactory extends Converter.Factory {

    @Nullable
    @Override
    public Converter<ResponseBody, ?> responseBodyConverter(Type type, Annotation[] annotations, Retrofit retrofit) {
        return new CustomResponseBodyConverter(type);
    }

    @Nullable
    @Override
    public Converter<?, RequestBody> requestBodyConverter(Type type, Annotation[] parameterAnnotations, Annotation[] methodAnnotations, Retrofit retrofit) {
        return new CustomRequestBodyConverter(type);
    }
}

然后我们在分别实现
CustomResponseBodyConverter
以及
CustomRequestBodyConverter
,实现请求与响应不同的转换器。

@Slf4j
public class CustomRequestBodyConverter implements Converter<CustomRequest, RequestBody> {

    private final ObjectMapper objectMapper;

    public CustomRequestBodyConverter() {
    
      this.objectMapper = new ObjectMapper(new JsonFactoryBuilder().build());
      this.objectMapper.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
        this.objectMapper.setPropertyNamingStrategy(PropertyNamingStrategy.SNAKE_CASE);
        this.objectMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
    }

    @Nullable
    @Override
    public RequestBody convert(CustomRequest CustomRequest) throws IOException {
	    // 具体转换逻辑
    }
}    

/**
*响应转换器
*/
public class CustomResponseBodyConverter implements Converter<ResponseBody, Object> {

    private final Type type;
    /**
     * 对象映射器
     */
    private final Gson gson;

    public CustomResponseBodyConverter(Type type) {
        this.type = type;
        GsonBuilder gsonBuilder = new GsonBuilder();
        this.gson = gsonBuilder.create();
    }

    @Override
    public Object convert(ResponseBody value) throws IOException {
	    // 具体处理逻辑
    }
}    

使用自定义转换器

Retrofit retrofit = new Retrofit.Builder()
    .baseUrl("http://localhost:8080/coderacademy/")
    .client(okHttpClient) // 设置自定义的 OkHttpClient
    .addConverterFactory(new CustomBodyConverterFactory())
    .build();

总结

本文深入介绍了
Retrofit2
,这是由
Square
公司开源的一款面向
Java

Android
平台的
RESTful API
客户端库。基于强大的
OkHttp
网络库构建,
Retrofit2
通过优雅的设计理念,将复杂的
HTTP
请求抽象为类型安全且易于理解的接口调用。

在使用
Retrofit2
时,开发者可以利用注解来定义
API
接口以及配置请求方法、
URL
路径、参数等信息,大大简化了网络通信实现过程,提高了代码可读性和维护性。同时,
Retrofit2
内置了多种数据转换器(如
GsonConverterFactory
),支持
JSON
以及其他格式的数据自动序列化与反序列化,极大地降低了开发成本。

Retrofit2
不仅支持同步和异步两种请求模式,还提供了丰富的扩展机制,包括自定义转换器以适应不同数据格式,添加拦截器处理全局请求/响应逻辑,以及集成各种认证方式,满足复杂网络环境下的各类需求。

此外,本文还阐述了如何创建和配置
Retrofit
实例,给出了具体的使用示例,并深入探讨了如何利用高级功能如自定义转换器、拦截器以及进行身份验证等,进一步展示了
Retrofit2
在实际项目中的强大灵活性和实用性。通过本文的学习,读者将能够更加熟练地使用
Retrofit2
开发出高效、可靠的网络请求功能。

本文已收录于我的个人博客:
码农Academy的博客,专注分享Java技术干货,包括Java基础、Spring Boot、Spring Cloud、Mysql、Redis、Elasticsearch、中间件、架构设计、面试题、程序员攻略等

Redisson分布式锁

来自Githup官方文档的介绍:

Redisson是一个在Redis的基础上实现的Java驻内存数据网格(In-Memory Data Grid)。它不仅提供了一系列的分布式的Java常用对象,还提供了许多分布式服务。其中包括(
BitSet
,
Set
,
Multimap
,
SortedSet
,
Map
,
List
,
Queue
,
BlockingQueue
,
Deque
,
BlockingDeque
,
Semaphore
,
Lock
,
AtomicLong
,
CountDownLatch
,
Publish / Subscribe
,
Bloom filter
,
Remote service
,
Spring cache
,
Executor service
,
Live Object service
,
Scheduler service
) Redisson提供了使用Redis的最简单和最便捷的方法。Redisson的宗旨是促进使用者对Redis的关注分离(Separation of Concern),从而让使用者能够将精力更集中地放在处理业务逻辑上。

官方文档地址:
https://github.com/redisson/redisson/wiki

中文文档地址:
https://github.com/redisson/redisson/wiki/目录

先讲一下为什么使用分布式锁:

在传统的单体应用中,我们可以使用Java并发处理相关的API(如ReentrantLock或synchronized)来实现对共享资源的互斥控制,确保在高并发情况下同一时间只有一个线程能够执行特定方法。然而,随着业务的发展,单体应用逐渐演化为分布式系统,多线程、多进程分布在不同机器上,这导致了原有的单机部署下的并发控制策略失效。为了解决这一问题,我们需要引入一种跨JVM的互斥机制来管理共享资源的访问,这就是分布式锁所要解决的核心问题。

SpringBoot框架整合

1.引入依赖

        <dependency>
            <groupId>org.redisson</groupId>
            <artifactId>redisson-spring-boot-starter</artifactId>
            <version>3.15.5</version>
        </dependency> 

2.创建配置类

@Configuration
public class RedissonConfig {

    @Value("${spring.redis.host}")
    private String redisHost;

    @Value("${spring.redis.port}")
    private int redisPort;

    @Value("${spring.redis.password}")
    private String password;

    @Value("${spring.redis.database}")
    private int database;

    @Bean
    public RedissonClient redissonClient() {
        Config config = new Config();
        config.useSingleServer()
                .setAddress("redis://" + redisHost + ":" + redisPort)
                .setPassword(password)
                .setDatabase(database)
                .setConnectionMinimumIdleSize(10) // 连接池最小空闲连接数
                .setConnectionPoolSize(50) // 连接池最大连接数
                .setIdleConnectionTimeout(60000) // 线程超时时间
                .setConnectTimeout(60000) // 客户端程序获取redis连接超时时间
                .setTimeout(60000); // 响应超时时间
        return Redisson.create(config);
    }
}

集群的配置参考:
https://github.com/redisson/redisson/wiki/2.-配置方法#24-集群模式

3.实际使用

    public  void checkAndLock5() {
        RLock redisson_lock = redissonClient.getLock("redisson_lock");
        try{
            redisson_lock.lock();
            // 1. 业务代码
            
        }finally {
            redisson_lock.unlock();
        }
    }

可重入锁(Reentrant Lock)

基于Redis的Redisson分布式可重入锁
RLock
Java对象实现了
java.util.concurrent.locks.Lock
接口。同时还提供了
异步(Async)

反射式(Reactive)

RxJava2标准
的接口

RLock lock = redisson.getLock("anyLock");
// 最常见的使用方法
lock.lock();

大家都知道,如果负责储存这个分布式锁的Redisson节点宕机以后,而且这个锁正好处于锁住的状态时,这个锁会出现锁死的状态。为了避免这种情况的发生,Redisson内部提供了一个监控锁的看门狗,它的作用是在Redisson实例被关闭前,不断的延长锁的有效期。默认情况下,看门狗的检查锁的超时时间是30秒钟,也可以通过修改
Config.lockWatchdogTimeout
来另行指定。

另外Redisson还通过加锁的方法提供了
leaseTime
的参数来指定加锁的时间。超过这个时间后锁便自动解开了。

// 加锁以后10秒钟自动解锁
// 无需调用unlock方法手动解锁
lock.lock(10, TimeUnit.SECONDS);

// 尝试加锁,最多等待100秒,上锁以后10秒自动解锁
boolean res = lock.tryLock(100, 10, TimeUnit.SECONDS);
if (res) {
   try {
     ...
   } finally {
       lock.unlock();
   }
}

公平锁(Fair Lock)

基于Redis的Redisson分布式可重入公平锁也是实现了
java.util.concurrent.locks.Lock
接口的一种
RLock
对象。同时还提供了
异步(Async)

反射式(Reactive)

RxJava2标准
的接口。它保证了当多个Redisson客户端线程同时请求加锁时,优先分配给先发出请求的线程。
所有请求线程会在一个队列中排队
,当某个线程出现宕机时,Redisson会等待5秒后继续下一个线程,也就是说如果前面有5个线程都处于等待状态,那么后面的线程会等待至少25秒。

RLock fairLock = redisson.getFairLock("anyLock");
// 最常见的使用方法
fairLock.lock();

大家都知道,如果负责储存这个分布式锁的Redis节点宕机以后,而且这个锁正好处于锁住的状态时,这个锁会出现锁死的状态。为了避免这种情况的发生,Redisson内部提供了一个监控锁的看门狗,它的作用是在Redisson实例被关闭前,不断的延长锁的有效期。默认情况下,看门狗的检查锁的超时时间是30秒钟,也可以通过修改
Config.lockWatchdogTimeout
来另行指定。

另外Redisson还通过加锁的方法提供了
leaseTime
的参数来指定加锁的时间。超过这个时间后锁便自动解开了。

// 10秒钟以后自动解锁
// 无需调用unlock方法手动解锁
fairLock.lock(10, TimeUnit.SECONDS);

// 尝试加锁,最多等待100秒,上锁以后10秒自动解锁
boolean res = fairLock.tryLock(100, 10, TimeUnit.SECONDS);
...
fairLock.unlock();

联锁(MultiLock)

基于Redis的Redisson分布式联锁
RedissonMultiLock
对象可以将多个
RLock
对象关联为一个联锁,每个
RLock
对象实例可以来自于不同的Redisson实例。

RLock lock1 = redissonInstance1.getLock("lock1");
RLock lock2 = redissonInstance2.getLock("lock2");
RLock lock3 = redissonInstance3.getLock("lock3");

RedissonMultiLock lock = new RedissonMultiLock(lock1, lock2, lock3);
// 同时加锁:lock1 lock2 lock3
// 所有的锁都上锁成功才算成功。
lock.lock();
...
lock.unlock();

大家都知道,如果负责储存某些分布式锁的某些Redis节点宕机以后,而且这些锁正好处于锁住的状态时,这些锁会出现锁死的状态。为了避免这种情况的发生,Redisson内部提供了一个监控锁的看门狗,它的作用是在Redisson实例被关闭前,不断的延长锁的有效期。默认情况下,看门狗的检查锁的超时时间是30秒钟,也可以通过修改
Config.lockWatchdogTimeout
来另行指定。

另外Redisson还通过加锁的方法提供了
leaseTime
的参数来指定加锁的时间。超过这个时间后锁便自动解开了。

RedissonMultiLock lock = new RedissonMultiLock(lock1, lock2, lock3);
// 给lock1,lock2,lock3加锁,如果没有手动解开的话,10秒钟后将会自动解开
lock.lock(10, TimeUnit.SECONDS);

// 为加锁等待100秒时间,并在加锁成功10秒钟后自动解开
boolean res = lock.tryLock(100, 10, TimeUnit.SECONDS);
...
lock.unlock();

红锁(RedLock)

基于Redis的Redisson红锁
RedissonRedLock
对象实现了
Redlock
介绍的加锁算法。该对象也可以用来将多个
RLock
对象关联为一个红锁,每个
RLock
对象实例可以来自于不同的Redisson实例。

RLock lock1 = redissonInstance1.getLock("lock1");
RLock lock2 = redissonInstance2.getLock("lock2");
RLock lock3 = redissonInstance3.getLock("lock3");

RedissonRedLock lock = new RedissonRedLock(lock1, lock2, lock3);
// 同时加锁:lock1 lock2 lock3
// 红锁在大部分节点上加锁成功就算成功。
lock.lock();
...
lock.unlock();

大家都知道,如果负责储存某些分布式锁的某些Redis节点宕机以后,而且这些锁正好处于锁住的状态时,这些锁会出现锁死的状态。为了避免这种情况的发生,Redisson内部提供了一个监控锁的看门狗,它的作用是在Redisson实例被关闭前,不断的延长锁的有效期。默认情况下,看门狗的检查锁的超时时间是30秒钟,也可以通过修改
Config.lockWatchdogTimeout
来另行指定。

另外Redisson还通过加锁的方法提供了
leaseTime
的参数来指定加锁的时间。超过这个时间后锁便自动解开了。

RedissonRedLock lock = new RedissonRedLock(lock1, lock2, lock3);
// 给lock1,lock2,lock3加锁,如果没有手动解开的话,10秒钟后将会自动解开
lock.lock(10, TimeUnit.SECONDS);

// 为加锁等待100秒时间,并在加锁成功10秒钟后自动解开
boolean res = lock.tryLock(100, 10, TimeUnit.SECONDS);
...
lock.unlock();

读写锁(ReadWriteLock)

基于Redis的Redisson分布式可重入读写锁
RReadWriteLock
Java对象实现了
java.util.concurrent.locks.ReadWriteLock
接口。其中读锁和写锁都继承了
RLock
接口。

分布式可重入读写锁允许同时有多个读锁和一个写锁处于加锁状态。

RReadWriteLock rwlock = redisson.getReadWriteLock("anyRWLock");
// 最常见的使用方法
rwlock.readLock().lock();
// 或
rwlock.writeLock().lock();

大家都知道,如果负责储存这个分布式锁的Redis节点宕机以后,而且这个锁正好处于锁住的状态时,这个锁会出现锁死的状态。为了避免这种情况的发生,Redisson内部提供了一个监控锁的看门狗,它的作用是在Redisson实例被关闭前,不断的延长锁的有效期。默认情况下,看门狗的检查锁的超时时间是30秒钟,也可以通过修改
Config.lockWatchdogTimeout
来另行指定。

另外Redisson还通过加锁的方法提供了
leaseTime
的参数来指定加锁的时间。超过这个时间后锁便自动解开了。

// 10秒钟以后自动解锁
// 无需调用unlock方法手动解锁
rwlock.readLock().lock(10, TimeUnit.SECONDS);
// 或
rwlock.writeLock().lock(10, TimeUnit.SECONDS);

// 尝试加锁,最多等待100秒,上锁以后10秒自动解锁
boolean res = rwlock.readLock().tryLock(100, 10, TimeUnit.SECONDS);
// 或
boolean res = rwlock.writeLock().tryLock(100, 10, TimeUnit.SECONDS);
...
lock.unlock();

信号量(Semaphore)

基于Redis的Redisson的分布式信号量(
Semaphore
)Java对象
RSemaphore
采用了与
java.util.concurrent.Semaphore
相似的接口和用法。同时还提供了
异步(Async)

反射式(Reactive)

RxJava2标准
的接口。

RSemaphore semaphore = redisson.getSemaphore("semaphore");
semaphore.acquire();
//或
semaphore.acquireAsync();
semaphore.acquire(23);
semaphore.tryAcquire();
//或
semaphore.tryAcquireAsync();
semaphore.tryAcquire(23, TimeUnit.SECONDS);
//或
semaphore.tryAcquireAsync(23, TimeUnit.SECONDS);
semaphore.release(10);
semaphore.release();
//或
semaphore.releaseAsync();

闭锁(CountDownLatch)

基于Redisson的Redisson分布式闭锁(
CountDownLatch
)Java对象
RCountDownLatch
采用了与
java.util.concurrent.CountDownLatch
相似的接口和用法。

RCountDownLatch latch = redisson.getCountDownLatch("anyCountDownLatch");
latch.trySetCount(1);
latch.await();

// 在其他线程或其他JVM里
RCountDownLatch latch = redisson.getCountDownLatch("anyCountDownLatch");
latch.countDown();

技术背景

分子动力学模拟(Molecule Dynamics Simulation,MD),本质上是一门采样技术。通过配置力场参数、拓扑结构和积分器,对一个给定的体系不断的采样,最终得到一系列的轨迹。那么得到分子动力学模拟的轨迹之后,如何使用后分析工具进行轨迹分析,也是一项很重要的工作。目前来说,基于Python的开源工具MDAnalysis(简称mda)是一个比较常用的MD后分析工具。本文主要介绍基于MindSponge分子动力学模拟框架生成了相应的轨迹之后,如何使用MDAnalysis工具进行分析。

环境配置

需要说明的是,MindSponge当前主要有两个版本,一个是华为MindSpore下的官方仓库
MindScience
,这里面包含了多个工具的正式发布版本,其中也有MindSponge,相对而言功能比较稳定,但是需要编译构建和安装使用。另外一个仓库是
MindSponge
,是MindSponge开发团队维护的一个develop版本,这个仓库只要
git clone
下来就可以测试和使用。本文章中的相关代码是基于
后者
来实现的,暂时没上正式版仓库。关于MindSponge的安装和基本使用方法,可以参考下
之前的文章
,所有的内容都是开源免费的。

然后MDAnalysis可以用pip直接安装(这里我们使用的是pip清华源):

$ python3 -m pip install mdanalysis --upgrade
Looking in indexes: https://pypi.tuna.tsinghua.edu.cn/simple
Requirement already satisfied: mdanalysis in /home/dechin/anaconda3/envs/mindsponge/lib/python3.9/site-packages (2.7.0)
Requirement already satisfied: numpy<2.0,>=1.22.3 in /home/dechin/anaconda3/envs/mindsponge/lib/python3.9/site-packages (from mdanalysis) (1.24.0)
Requirement already satisfied: GridDataFormats>=0.4.0 in /home/dechin/anaconda3/envs/mindsponge/lib/python3.9/site-packages (from mdanalysis) (1.0.2)
Requirement already satisfied: mmtf-python>=1.0.0 in /home/dechin/anaconda3/envs/mindsponge/lib/python3.9/site-packages (from mdanalysis) (1.1.3)
Requirement already satisfied: joblib>=0.12 in /home/dechin/.local/lib/python3.9/site-packages (from mdanalysis) (1.2.0)
Requirement already satisfied: scipy>=1.5.0 in /home/dechin/anaconda3/envs/mindsponge/lib/python3.9/site-packages (from mdanalysis) (1.10.1)
Requirement already satisfied: matplotlib>=1.5.1 in /home/dechin/anaconda3/envs/mindsponge/lib/python3.9/site-packages (from mdanalysis) (3.7.1)
Requirement already satisfied: tqdm>=4.43.0 in /home/dechin/anaconda3/envs/mindsponge/lib/python3.9/site-packages (from mdanalysis) (4.65.0)
Requirement already satisfied: threadpoolctl in /home/dechin/.local/lib/python3.9/site-packages (from mdanalysis) (3.1.0)
Requirement already satisfied: packaging in /home/dechin/anaconda3/envs/mindsponge/lib/python3.9/site-packages (from mdanalysis) (23.0)
Requirement already satisfied: fasteners in /home/dechin/anaconda3/envs/mindsponge/lib/python3.9/site-packages (from mdanalysis) (0.19)
Requirement already satisfied: mda-xdrlib in /home/dechin/anaconda3/envs/mindsponge/lib/python3.9/site-packages (from mdanalysis) (0.2.0)
Requirement already satisfied: mrcfile in /home/dechin/anaconda3/envs/mindsponge/lib/python3.9/site-packages (from GridDataFormats>=0.4.0->mdanalysis) (1.5.0)
Requirement already satisfied: contourpy>=1.0.1 in /home/dechin/anaconda3/envs/mindsponge/lib/python3.9/site-packages (from matplotlib>=1.5.1->mdanalysis) (1.0.7)
Requirement already satisfied: cycler>=0.10 in /home/dechin/anaconda3/envs/mindsponge/lib/python3.9/site-packages (from matplotlib>=1.5.1->mdanalysis) (0.11.0)
Requirement already satisfied: fonttools>=4.22.0 in /home/dechin/anaconda3/envs/mindsponge/lib/python3.9/site-packages (from matplotlib>=1.5.1->mdanalysis) (4.38.0)
Requirement already satisfied: kiwisolver>=1.0.1 in /home/dechin/anaconda3/envs/mindsponge/lib/python3.9/site-packages (from matplotlib>=1.5.1->mdanalysis) (1.4.4)
Requirement already satisfied: pillow>=6.2.0 in /home/dechin/anaconda3/envs/mindsponge/lib/python3.9/site-packages (from matplotlib>=1.5.1->mdanalysis) (9.4.0)
Requirement already satisfied: pyparsing>=2.3.1 in /home/dechin/anaconda3/envs/mindsponge/lib/python3.9/site-packages (from matplotlib>=1.5.1->mdanalysis) (3.0.9)
Requirement already satisfied: python-dateutil>=2.7 in /home/dechin/anaconda3/envs/mindsponge/lib/python3.9/site-packages (from matplotlib>=1.5.1->mdanalysis) (2.8.2)
Requirement already satisfied: importlib-resources>=3.2.0 in /home/dechin/anaconda3/envs/mindsponge/lib/python3.9/site-packages (from matplotlib>=1.5.1->mdanalysis) (5.12.0)
Requirement already satisfied: msgpack>=1.0.0 in /home/dechin/anaconda3/envs/mindsponge/lib/python3.9/site-packages (from mmtf-python>=1.0.0->mdanalysis) (1.0.5)
Requirement already satisfied: zipp>=3.1.0 in /home/dechin/anaconda3/envs/mindsponge/lib/python3.9/site-packages (from importlib-resources>=3.2.0->matplotlib>=1.5.1->mdanalysis) (3.11.0)
Requirement already satisfied: six>=1.5 in /home/dechin/anaconda3/envs/mindsponge/lib/python3.9/site-packages (from python-dateutil>=2.7->matplotlib>=1.5.1->mdanalysis) (1.16.0)

安装完成后,就可以先用MindSponge生成一个用于后分析的轨迹,再调用MDAnalysis进行分析。

生成轨迹

这里我们使用的案例轨迹,还是
前一篇文章
中所用到的能量极小化的一个案例。模拟的分子是这个样子的:

分子动力学模拟的相关代码如下:

from mindspore import nn, context
import numpy as np
import sys
# 添加sponge所在的路径,这样就不需要安装即可直接使用
sys.path.insert(0, '../..')
from sponge import ForceField, Sponge, set_global_units, Protein
from sponge.callback import RunInfo, WriteH5MD, SaveLastPdb
from sponge.colvar import Distance, Angle, Torsion

# 配置MindSpore的执行环境
context.set_context(mode=context.GRAPH_MODE, device_target='GPU', device_id=1)
# 配置全局单位
set_global_units('A', 'kcal/mol')

# 定义一个基于case1.pdb的分子系统
system = Protein('../pdb/case1.pdb', template=['protein0.yaml'], rebuild_hydrogen=True)
# 定义一个amber.ff99sb的力场
energy = ForceField(system, parameters=['AMBER.FF99SB'])
# 定义一个学习率为1e-03的Adam优化器
min_opt = nn.Adam(system.trainable_params(), 1e-03)

cv_bond = Distance([0, 1])
cv_angle = Angle([0, 1, 2])
cv_dihedral = Torsion([0, 1, 2, 3])
# 定义一个用于执行分子模拟的Sponge实例
md = Sponge(system, potential=energy, optimizer=min_opt, metrics={'bond': cv_bond, 'angle': cv_angle,
                                                                  'dihedral': cv_dihedral})

# RunInfo这个回调函数可以在屏幕上根据指定频次输出能量参数
run_info = RunInfo(20)
# WriteH5MD回调函数,可以将轨迹、能量、力和速度等参数保留到一个hdf5文件中,文件后缀为h5md
cb_h5md = WriteH5MD(system, 'test.h5md', save_freq=10, write_image=False, save_last_pdb='last_pdb.pdb')
# 保存PDB文件
bonds = np.array([[0, 56], [6, 10]], np.int32)
# 开始执行分子动力学模拟,运行2000次迭代
md.run(200, callbacks=[run_info, cb_h5md])

运行结束后,会在当前路径下生成一个名为
last_pdb.pdb
的构象文件和一个
test.h5md
的轨迹文件。关于h5md格式的轨迹文件,可以用
silx
这个工具来进行直观的可视化:

这是体系能量极小化过程中的能量变化曲线:

并且保存了轨迹数据:

MDAnalysis分析

使用MDAnalysis进行分析的主要流程,就是用拓扑结构文件和轨迹文件构建两个
MDAnalysis.Universe
对象。这里拓扑结构文件可以使用pdb文件,但要求pdb文件中包含有CONECT成键相互关系,否则跟成键相互作用相关的内容使用mda无法分析,MindSponge所生成的pdb文件中是包含了成键关系信息的。再者就是h5md也是mda所支持的轨迹文件扩展名,使用MindSponge生成的轨迹可以直接用mda加载:

import MDAnalysis as mda
u = mda.Universe('last_pdb.pdb', 'test.h5md')

加载完之后,我们可以打印其中的一些关键信息,比如原子类型和残基类型等:

print('Atom Types List:\n', u.atoms)
# Atom Types List:
# <AtomGroup [<Atom 1: N of type N of resname ALA, resid 1 and segid A and altLoc >, <Atom 2: CA of type C of resname ALA, resid 1 and segid A and altLoc >, <Atom 3: CB of type C of resname ALA, resid 1 and segid A and altLoc >, ..., <Atom 55: HB1 of type H of resname ALA, resid 4 and segid A and altLoc >, <Atom 56: HB2 of type H of resname ALA, resid 4 and segid A and altLoc >, <Atom 57: HB3 of type H of resname ALA, resid 4 and segid A and altLoc >]>
print('Residue Types List:\n', u.residues)
# Residue Types List:
# <ResidueGroup [<Residue ALA, 1>, <Residue ARG, 2>, <Residue ALA, 3>, <Residue ALA, 4>]>
print('Step 0 Coordinates Shape:\n', np.array(u.coord).shape)
# Step 0 Coordinates Shape:
# (57, 3)
print('C Atoms:\n', u.select_atoms('name C'))
# C Atoms:
# <AtomGroup [<Atom 4: C of type C of resname ALA, resid 1 and segid A and altLoc >, <Atom 22: C of type C of resname ARG, resid 2 and segid A and altLoc >, <Atom 40: C of type C of resname ALA, resid 3 and segid A and altLoc >, <Atom 50: C of type C of resname ALA, resid 4 and segid A and altLoc >]>
print('Contact Map:\n', contact_matrix(np.array(u.coord)))
# Contact Map:
# [[ True  True  True ...  True  True  True]
# [ True  True  True ...  True  True  True]
# [ True  True  True ...  True  True  True]
# ...
# [ True  True  True ...  True  True  True]
# [ True  True  True ...  True  True  True]
# [ True  True  True ...  True  True  True]]
print(u.bonds)
# <TopologyGroup containing 56 bonds>

然后是一些跟轨迹相关的条目:

print('Number of Frames in Trajectory:\n', u.trajectory.n_frames)
# Number of Frames in Trajectory:
# 20
print('Number of Atoms:\n', u.trajectory.n_atoms)
# Number of Atoms:
# 57
print(u.trajectory.has_positions)
# True
print(u.trajectory.ts.positions.shape)
# (57, 3)
print(u.trajectory.ts.has_velocities)
# False
print(u.trajectory.ts.has_forces)
# False
print(u.trajectory[0].data)
# {'trajectory': 10, 'time': 0.010000000707805157, 'step': 10}
print(u.trajectory[1].positions[0])
# [ -0.11355944 -11.455442    -0.79421705]

因为我们在定义CallBack的时候没有在轨迹中保存速度参量和力参量,因此这里
has_velocities

has_forces
两个的值都是
False
,但实际上我们是可以支持在中间轨迹把这两个参量写入到h5md文件中的。由于轨迹有很多帧,在mda里面我们可以直接对
u.trajectory
使用索引,来定位到特定的某一帧,再导出自己所需要的参量。除了单点分析,我们还可以定义一个reference trajectory来计算RMSD等参数:

ref = mda.Universe('last_pdb.pdb', 'test.h5md')
R = RMSD(u, ref, select="backbone", groupselections=['backbone and resid 1-4'])
R.run()
rmsd = R.results.rmsd.T
print (rmsd[2])

更多的MDAnalysis工具的使用方法和函数接口,可以参考
MDAnalysis官方文档
或者是这个
中文翻译版文档

总结概要

这篇文章我们主要介绍了MindSponge分子动力学模拟软件如何跟后分析工具MDAnalysis相配合的方法,其主要操作流程就是调用MindSponge自带的CallBack来输出拓扑文件和轨迹文件给MDAnalysis,然后就可以调用MDAnalysis的相关分析函数接口,十分的方便。

版权声明

本文首发链接为:
https://www.cnblogs.com/dechinphy/p/mda-mds.html

作者ID:DechinPhy

更多原著文章:
https://www.cnblogs.com/dechinphy/

请博主喝咖啡:
https://www.cnblogs.com/dechinphy/gallery/image/379634.html