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

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


  • 首页

  • 归档

  • 搜索

SSIS学习使用十五:SSIS参数概述,项目参数,包参数及使

发表于 2021-11-22

这是我参与11月更文挑战的第22天,活动详情查看:2021最后一次更文挑战

翻译参考

本文主要参考翻译自 The Stairway to Integration Services 系列文章的原文 An Overview of SSIS Parameters - Level 15 of the Stairway to Integration Services,目的在于对 SSIS 有一个全面清晰的认识,所有内容在原文的基础上进行实操,由于版本差异、个人疑问等多种原因,未采用完全翻译的原则,同时也会对文中内容进行适当修改,希望最终可以更有利于学习和了解 SSIS,

感谢支持!


在本文中,我们将探讨 SSIS变量 的第一个表亲:SSIS参数。演示参数配置,通过 包参数 进行动态属性值管理以及在 SSIS包 执行过程中如何配置和使用参数。

首先,打开之前的第一个项目FirstSSIS。

SSIS参数介绍

SSIS参数101(SSIS Parameters 101)

SSIS参数 与 SSIS变量 非常相似。在整个 SSIS 2012 中,变量和参数可以互换使用。但是有一些重要的区别:

  • SSIS包开始执行后,参数为只读。
  • 参数的作用域(或范围)在“项目”和“包”级别(Project and Package level)。
  • 参数具有敏感属性(Sensitive attribute)。
  • 参数具有必须属性(Required attribute)。
  • 由于SSIS包开始执行后,参数是只读的,因此无法使用表达式来控制参数值。

项目参数(Project Parameters)

SSIS项目中包含的任何 SSIS包 都可以使用项目参数(Project parameters)。它们只能在 项目部署模型(Project Deployment Model) 的上下文中使用。

SQL Server 2012 Integration Services 中的项目部署模型是新增的。项目参数是项目部署模型的一项新功能。其他功能包括包参数,项目连接管理器,以及部署到新的 SQL Server 2012 Integration Services 目录。

在解决方案资源管理器中,双击解决方案资源管理器中的 Project.params 对象以打开“项目参数”,如图所示:

“项目参数”窗口提供了一个包含三个按钮的工具栏:

  • 添加参数-Add Parameter
  • 删除参数-Delete Parameter
  • 将参数添加到配置-Add Parameters to Configurations

添加参数(Add Parameter)

单击 “添加参数” 按钮,将新的项目参数添加到 FirstSSIS SSIS项目。

名为 “Parameter” 的新项目参数将添加到SSIS项目。这是默认参数名称(Name),该参数的默认属性设置:

  • 数据类型-Data Type: Int32
  • 值-Value: 0
  • 敏感-Sensitive: False
  • 必需-Required: False
  • 说明-Description: [Empty String]

参数和变量 共享许多数据类型,但不是全部 —— 如下图所示,参数数据类型在左侧,变量数据类型在右侧:

对两个列表进行扫描后发现,参数数据类型是变量数据类型的子集。参数和变量共有以下数据类型:

  • Boolean
  • Byte
  • DateTime
  • Decimal
  • Double
  • Int16
  • Int32
  • Int64
  • SByte
  • Single
  • String
  • UInt32
  • UInt64

变量数据类型(Variable data types)独有的是:

  • Char
  • DBNull
  • Object

一旦 “值”单元格 失去焦点,就检查参数的“值”属性的数据类型一致性。如果选择数据类型“Int32”并在“值”单元格中输入“NaN”,则当导航到Project.params网格中的另一个单元格时,会出现下图所示的错误:

敏感属性是一个布尔值(True或False),默认为False。将此属性设置为True会将参数值标记为敏感。如果为True,则 SQL Server 2012 Integration Services 将使用 “Project ProtectionLevel” 设置中指定的方法对该值进行加密。

可以通过在解决方案资源管理器中,右键单击该项目,然后单击”属性”(Properties),来访问项目(Project)ProtectionLevel属性和其他项目属性。

单击”属性”将显示FirstSSIS属性页。显示属性页后,​​可以通过单击ProtectionLevel属性文本框中的省略号来设置 Project ProtectionLevel 属性。单击省略号将显示“项目保护级别”窗口,如下图:

请注意:当”敏感”(Sensitive)属性设置为True时,Parameter的Value属性将被屏蔽。

“必需”(Required)属性是一个布尔值(True或False),默认为False。设置为False时,SSIS项目执行不需要该参数。当设置为True时,必须为SSIS项目执行提供参数值。

“说明”(Description)属性提供了一种记录参数目的、使用和可选值的描述。

a means of documenting the purpose, usage, and optional values

出于演示目的,将参数配置为:

  • Name: Parameter
  • Data Type: Int32
  • Value: 0
  • Sensitive: True
  • Required: True
  • Description: Test Parameter

删除参数(Delete Parameter)

“删除参数”按钮将从存储在Project.params对象中的“项目参数”集合中删除所选参数。

如图所示,会显示警告,通知数据集成开发人员使用项目参数的程序包可能无法执行,除非通过选中“不再显示此消息”复选框将其覆盖(建议不要选中此复选框)。

包参数(Package Parameters)

配置新SSIS包

创建SSIS包

在开始讨论 SSIS包参数 之前,让我们向项目添加一个新的SSIS包。

在解决方案资源管理器中,右键单击项目名称下的”SSIS包”,然后单击 “新建SSIS包”(New SSIS Package),如图所示:

重命名新包名为 “LoadWeatherData”。打开该包,包参数(Package parameters) 的配置位于 “参数”(Parameters) 选项卡下。

在配置SSIS包参数之前,我们先获取一些真实数据。你可以通过访问该链接访问包含实际数据的文件(andyweather.com/data/Weathe…

将文件解压缩到选择的位置。它包含一个名为 sensor1-all 的逗号分隔值(comma-separated values——CSV)文件。如下,在 FirstSSIS 的SSIS解决方案目录中创建了一个名为 data 的新文件夹:

Sensor1-all.csv 包含从我在弗吉尼亚州法姆维尔的个人气象站收集的测量值。每当我训练人们使用SSIS开发数据集成解决方案时,我都会使用这些数据。我喜欢它,因为它是真实数据,而不是示例数据。它是混乱的。没有人能像大多数样本数据库那样干净地收集数据。

包参数

包参数的定义与项目参数几乎相同。设置以下参数属性,创建一个包含 sensor1-all.csv 的存储路径的包参数,如图所示:

  • Name: SourceFilePath
  • Data Type: String
  • Value: <the location of sensor1-all.csv on your system>
  • Sensitive: False
  • Required: True
  • Description: Weather data source file

配置数据流

返回控制流,添加一个”Data Flow Task”,重命名数据流任务 “DFT Stage Temperature and Humidity”,如下:

双击 “DFT Stage Temperature and Humidity” 以打开编辑器。在SSIS工具箱中,展开 “其他源” 类别,然后将 “平面文件源”适配器 拖到 “数据流任务” 画布上。重命名平面文件源 “FFSrc Temperature and Humidity”。

双击 “FFSrc Temperature and Humidity” 平面文件源适配器以打开编辑器,如图所示。

在此SSIS包中没有配置平面文件连接管理器。单击”新建”按钮以创建和配置新的平面文件连接管理器。

请注意,单击“新建”按钮会发生两件事:

  1. 新的平面文件连接管理器已添加到“连接管理器”选项卡中(在图中指示的);
  2. 此新的平面文件连接管理器的编辑器已打开。

重命名连接管理器“FFCM Temperature and Humidity”。单击浏览按钮,导航到存储 sensor1-all.csv 文件的文件夹,然后在 “打开文件” 对话框中将文件类型过滤器从“*.txt”(默认)更改为“*.csv”。然后 sensor1-all.csv 文件将在文件列表框中显示以供选择,如图所示:

选择 sensor1-all.csv 文件,然后单击“打开”按钮。

现在,“平面文件连接管理器的常规”页面在 “文件名” 文本框中显示sensor1-all.csv文件的完整路径。确保选中 “在第一个数据行中显示列名称” 复选框。

不要在 “列”(Columns) 页面上进行任何更改。注意,可以设置行和列分割符,下方提供了更改的预览,如图所示:

下面显示了 Flat File Connection Manager 编辑器的 “高级”(Advanced) 页面。我们不会在此页面上进行任何更改,但是会进行一些观察。

你可以在此处更改列名称,为各个列指定列定界符(Column Delimiter,分隔符)。还可以更改数据类型属性。默认情况下,所有”平面文件连接管理器”列均设置为长度为50的字符串数据类型:

平面文件连接管理器的“预览”页面允许你配置连接管理器以跳过一些数据行。它还提供了一个网格,可预览前100行数据,如图所示:

单击“确定”按钮,以完成 “FFCM Temperature and Humidity” 平面文件连接管理器配置,并返回到 “DFT Stage Temperature and Humidity” 数据流任务中的平面文件源编辑器。

单击”确定”按钮,以关闭平面文件源适配器编辑器。

创建数据库

在继续之前,请打开 SQL Server Management Studio(SSMS) 并连接到 SQL Server 实例。

在SSMS中打开一个新的查询窗口,然后输入如下 Listing 1 所示的文本:

1
2
3
4
5
6
7
8
9
10
11
12
13
sql复制代码-- Listing 1
Use master
go
If Not Exists(Select name
From sys.databases
Where name = 'WeatherData')
begin
print 'Creating database WeatherData'
Create Database WeatherData
print 'WeatherData database created'
end
Else
print 'WeatherData database already exists.'

Listing 1中包含的 Transact-SQL(T-SQL)检查 SQL Server 实例上是否存在名为 WeatherData 的数据库。如果 WeatherData 存在,则会返回一条存在的消息。如果 SQL Server 实例中不存在 WeatherData 数据库,则会出现“正在创建数据库”的信息,然后创建数据库,完成后返回一条消息,通知已经创建了数据库。

为什么我用这种方式编写T-SQL?这是一个很好的问题。

这是幂等脚本(idempotent script)的示例。幂等意味着可以重复操作而不会改变结果。在数学中,,将 0 加到任何数字,都将永远不会改变总和的值。它是幂等的。

Listing 1中的 Transact-SQL 脚本可以执行和重新执行。无论是第一次还是第十次执行, WeatherData 数据库都将在执行完成时存在。它是幂等的。

在SSMS执行此脚本以确保数据库存在。

现在我们可以将 “temperature and humidity” 数据导入 WeatherData 数据库。

添加OLE DB目标

返回到SSDT-BI。将 “OLE DB目标”(OLE DB Destination)适配器 拖到 Data Flow 页面上,从 “FFSrc Temperature and Humidity” 平面文件源适配器连接一个数据流路径到 “OLE DB目标适配器”,并重命名 “OLE DB目标” 为 “OLEDest Stage Temperature”。如图所示:

双击OLE DB Destination适配器 “OLEDest Stage Temperature”,以打开编辑器。

在此SSIS包中没有配置 “OLE DB连接管理器”(OLE DB Connection Managers)。

单击 “OLE DB连接管理器” 下拉菜单旁边的 “新建” 按钮,打开 “配置OLE DB连接管理器” 窗口,如图所示。

“配置OLE DB连接管理器”中列出了当前项目下其他SSIS包中的OLE DB连接管理器。

单击“新建”按钮,创建新的连接管理器配置。在“服务器名称”下拉列表中,选择或输入包含 WeatherData 数据库的 SQL Server 实例的名称。在“选择或输入数据库名称”下拉列表中,选择或输入WeatherData,如图所示。

单击”确定”按钮,关闭连接管理器编辑器。单击”确定”按钮,关闭“配置OLE DB连接管理器”窗口。现在应显示 “OLEDest Stage Temperature” OLE DB目标编辑器,如图所示:

我们将保留 “数据访问模式” 下拉列表的“表或视图”设置。 “表或视图的名称”(Name of the table or the view)下拉列表为空,点击右侧的”新建,打开 “创建表”(Create Table) 窗口:

编辑该语句,从表名中删除“OLEDest”和空格,并从列名中删除空格,如下面的清单2所示。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
sql复制代码-- Listing 2
CREATE TABLE [StageTemperature] (
[Date] varchar(50),
[Time] varchar(50),
[MinT] varchar(50),
[MaxT] varchar(50),
[AverageT] varchar(50),
[MinH] varchar(50),
[MaxH] varchar(50),
[AverageH] varchar(50),
[ComfortZone] varchar(50),
[MinDP] varchar(50),
[MaxDP] varchar(50),
[AverageDP] varchar(50),
[MinHI] varchar(50),
[MaxHI] varchar(50),
[AverageHI] varchar(50)
)

CREATE TABLE语句是使用将平面文件源适配器连接到OLE DB目标适配器的数据流路径发送到OLE DB目标适配器的元数据构建的。

当单击“确定”按钮以关闭“创建表”窗口时,将对数据库执行数据定义语言(DDL)CREATE TABLE语句,从而创建 StageTemperature 表。

单击 OLE DB Destination Editor 中的 Mappings 页面,如图所示。

请注意,自动映射发生在 “Date和Time” 输入列和目标列之间。为什么?因为这些列分别具有相同的名称和数据类型。

暂时不确定自动映射的规则。如果输入和目标列之间的列名和数据类型匹配,则会发生自动映射。如果没有,则有时会发生自动映射,有时不会。为什么?

我对规则的最佳猜测是:如果输入列名称与目标列名称匹配,并且SSIS可以将输入数据类型强制为目标数据类型,则会发生自动映射。我根据八年的观察得出了这个假设。我相信,什么是强制性的,什么不是强制性的,这些年来已经发生了变化。我唯一确定的是:我不知道。

目标表是根据从 “OLE DB目标” 适配器的输入中收集的元数据创建的。如果双击从 “FFSrc Temperature and Humidity” 平面文件源适配器和 “OLEDest Stage Temperature” OLE DB目标适配器连接的数据流路径,你将找到一个”元数据”页面。

但是,我通过删除空格来更改了列名。因此,输入列名称包含空格,而大多数目标列名称不包含空格。

有两种方法可以在“映射”页面上映射列。一种方法是将列名从“可用输入列”网格拖放到“可用目标列”网格上(或从“可用目标列”网格拖放到“可用输入列”网格上-拖放映射双向起作用)。下图显示了另一种方法。可以通过下拉列表将各个 Input 列分配给 Destination 列。

映射完成后,单击“确定”按钮,关闭OLE DB目标编辑器。

使用包参数

我们几乎可以执行了。最后一步是将 SourceFilePath包参数 映射到 “FFCM Temperature and Humidity” 平面文件连接管理器的 ConnectionString 属性中。

为此,请单击 “FFCM Temperature and Humidity” 平面文件连接管理器,然后按F4键以显示连接管理器属性。请注意,ConnectionString属性当前被硬编码为我们之前选择的路径。要将此属性动态绑定到SourceFilePath包参数,请在 Expressions 属性值文本框中单击省略号,如图所示:

“属性表达式编辑器”将会显示。表达式(Expressions)使开发人员可以在运行时将表达式动态映射到属性值。

“属性”列先选择 ConnectionString 属性,将 SourceFilePath 包参数的值映射到 “FFCM Temperature and Humidity” 平面文件连接管理器的 ConnectionString 属性中。单击“表达式”文本框中的ConnectionString属性的省略号以显示“表达式生成器”窗口:

展开“变量和参数”虚拟文件夹,然后将包参数 $ Package::SourceFilePath 拖到“表达式”文本框中。并点击”确定”,关闭生成器窗口。

也可以单击“计算结果值”(Evaluated value)按钮以查看$Package::SourceFilePath参数中包含的值。

单击”确定”按钮,关闭属性表达式编辑器。

现在,“FFCM Temperature and Humidity”平面文件连接管理器的ConnectionString属性动态地与SourceFilePath包参数($Package::SourceFilePath)的值耦合。

按F5键执行SSIS包并测试加载程序,如果一切按计划进行,那么你的数据流应该看起来和如下图所示类似:

停止调试器,然后移动源文件。在文件的当前位置,创建一个名为Dec08的新文件夹,并将sensor1-all.csv文件移动到新的Dec08文件夹中。

返回到SSIS包,然后按F5键重新执行该包。它应该会失败,并显示与图中所示类似的错误。

停止调试器。返回”包参数”(Package Parameters)选项卡,并更改 SourceFilePath 参数的值以包括Dec08文件夹,如图所示。

现在重新执行该包。结果应如图所示。

测试成功!我们已经将源文件的路径设置为动态,并使用 package parameter 来做到这一点。

总结

在本文中,我们探讨了SSIS参数,参数配置,通过包参数进行动态属性值管理以及在SSIS包执行过程中如何配置和使用参数。

本文转载自: 掘金

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

YARN源码不知从何入手,那是你不知道Service

发表于 2021-11-22

「这是我参与11月更文挑战的第1天,活动详情查看:2021最后一次更文挑战」

推荐阅读时间:10分钟

字数:1357字

YARN Server工作机制深度剖析

YARN的核心设计理念是服务化(Service) 和事件驱动AsyncDispatcher ( Event + EventHandler/StateMachine) 。服务化和事件驱动软件设计思想的引入,使得YARN具有低耦合、高内聚的特点,各个模块只需完成各自功能,而模块之间则采用事件联系起来,系统设计简单且维护方便。这种编程方式具有异步、并发等特点,更高效,更适合大型分布式系统。

Service在Yarn源码中处处可见,想要阅读源码就得选了解YARN中的Service机制

下面就详细介绍下什么是Service

对于生命周期较长的对象,YARN 采用了基于服务的对象管理模型对其进行管理,该模型主要有以下几个特点。

  • 将每个被服务化的对象分为 4 个状态:NOTINITED(被创建)、INITED(已初始化)、STARTED(已启动)、STOPPED(已停止)。
  • 任何服务状态变化都可以触发另外一些动作。
  • 可通过组合的方式对任意服务进行组合,以便进行统一管理。通俗的解释:一个大 Service 可能会有一堆子 Service

使用场景化驱动的方式来了解YARN Service

我们比较熟悉的ResourceManager其实就是一个Service,它本身就是Service的子类,下面是ResourceManager和NodeManager类的继承类图:

根据类的继承关系,直接看接口Service,接口的具体信息如下:

  • 印证了Service都有四个状态:NOTINITED、INITED、STARTED、STOPPED
  • 于此同时还有五个重要的方法:构造方法、init()、start()、stop()、close()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
java复制代码public interface Service extends Closeable { 
public enum STATE {
NOTINITED(0, "NOTINITED"),
INITED(1, "INITED"),
STARTED(2, "STARTED"),
STOPPED(3, "STOPPED");
}
// 服务初始化
void init(Configuration config);
// 服务启动
void start();
// 服务停止
void stop();
// 服务关闭
void close() throws IOException;
}

这四个状态和五个重要的方法之间有不可分割的关系:

了解了Service的主要信息之后,下面从上往下查看各类的具体信息

对于AbstractService类,它实现了结构Service,那么它一定会对上述的四个方法进行实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
java复制代码public abstract class AbstractService implements Service { 
private final String name;
private final ServiceStateModel stateModel;

@Override public void init(Configuration conf) { serviceInit(config); }

@Override public void start() { serviceStart(); }

@Override public void stop() { serviceStop(); }

// 都是空实现!
protected void serviceInit() throws Exception {}
protected void serviceStart() throws Exception { }
protected void serviceStop() throws Exception { }
}

通过源码可以看到,虽然对四个方法都已经进行了实现但是都是空实现,这是为什么呢?

使用一个中间类去对父类的方法进行空实现,然后真正的子类去继承中间类,这样可以避免子类去实现父类的一些不必要的方法,此处AbstractService就是中间类,这样的代码设计模式是可以借鉴的

既然AbstractService对这四个方法没有进行实现,下面就来看它的子类CompositeService,来看CompositeService类的具体定义:

  1. 一个ArrayList,它是一个Service类型的集合,印证了上面的特点:一个大 Service 可能会有一堆子 Service

此处是ArrayList说明Service加入的顺序是保留了下来的,是有顺序的

1
java复制代码private final List<Service> serviceList = new ArrayList<Service>();
  1. 从ArrayList中获取Service,上述已经说明列表保留了服务的顺序,由此此处获取Service也是有顺序的
1
2
3
4
5
java复制代码public List<Service> getServices() { 
synchronized(serviceList) {
return new ArrayList<Service>(serviceList);
}
}
  1. 添加子 service 的方法
1
2
3
4
5
6
7
8
9
10
11
12
13
java复制代码protected void addService(Service service) { 
synchronized(serviceList) {
serviceList.add(service);
}
}
protected boolean addIfService(Object object) {
if(object instanceof Service) {
addService((Service) object);
return true;
} else {
return false;
}
}
  1. 针对所有 Service 执行 init 初始化:最终调用 service 的 () 方法

针对所有 Service 执行 start 启动:最终调用 service 的 serviceStart() 方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
java复制代码protected void serviceInit(Configuration conf) throws Exception { 
// 获取当前 service 组件的所有子 service
List<Service> services = getServices();
// 遍历每个 子 service 调用 init() 执行初始化
for(Service service : services) {
service.init(conf);
}
}

protected void serviceStart() throws Exception {
List<Service> services = getServices();
for(Service service : services) {
service.start();
}
}

下面看到ResourceManager,它是CompositeService的子类,它实现了方法serviceInit()、serviceStart()、serviceStop()

当ResourceManager进行初始化、开启的时候会调用父类的init、start方法,然后这两个方法再调用本部类重写了的serviceInit()、serviceStart()方法,从而完成具体业务逻辑。

1
2
3
4
5
6
7
8
9
10
java复制代码ResourceManager.main(){
// ResourceManager 初始化
Resourcemanager resourceManager = new Resourcemanager();

// 各种服务创建和初始化
resourceManage.init(conf);

//各种服务启动
resourcemanager.start();
}

总而言之,在 YARN 中,会有非常非常多的 Service,每个 service 都要初始化和启动,如果Service本身有serviceInit、serviceStart方法,就各自做自己的实现,否则使用父类的实现。

如果阅读 Service 组件的源码,从下面三方面入手:

  • 构造方法
  • serviceInit() 方法
  • serviceStart() 方法

本人在学习的路上,上述文章如有错误还请指教批评。

本文转载自: 掘金

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

Linux下获取文件或目录的状态信息(属性、大小、创建时间等

发表于 2021-11-22

这是我参与11月更文挑战的第22天,活动详情查看:2021最后一次更文挑战」

一、前言

在Linux下进行文件、目录编程时经常需要获取指定文件的属性信息,比如: 文件类型、大小、创建日期、修改日期等属性信息。

获取这些属性信息非常有用,比如: 打开一个设备文件时,为了防止未知错误,可以提前判断打开的设备文件是否是正确的类型。 拷贝文件时、读取文件时,mmap映射文件时,可以根据文件本身的大小,判断文件是否读写完毕。

Linux下有3种函数可以快速得到文件、目录的属性信息:

1
cpp复制代码stat 、fstat、lstat

详细的介绍如下:

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
cpp复制代码#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>

int stat(const char *path, struct stat *buf);
函数功能: 获取文件或者目录的状态
函数参数:
const char *path 文件或者目录的路径.
struct stat *buf 保存获取的状态信息

int fstat(int fd, struct stat *buf);
函数功能: 获取文件的状态信息.
函数形参:
int fd 文件描述符
struct stat *buf 保存获取的状态信息

int lstat(const char *path, struct stat *buf);
函数功能: 获取文件的状态信息. 不区分链接文件.

保存状态信息的结构体:
struct stat {
dev_t st_dev; /* ID of device containing file */
ino_t st_ino; /* inode number */
mode_t st_mode; /* protection 文件的类型*/
nlink_t st_nlink; /* number of hard links */
uid_t st_uid; /* user ID of owner */
gid_t st_gid; /* group ID of owner */
dev_t st_rdev; /* device ID (if special file) */
off_t st_size; /* total size, in bytes 文件的字节大小*/
blksize_t st_blksize; /* blocksize for file system I/O */
blkcnt_t st_blocks; /* number of 512B blocks allocated */
time_t st_atime; /* time of last access */
time_t st_mtime; /* time of last modification */
time_t st_ctime; /* time of last status change */
};

S_ISREG(m) is it a regular file? 普通文件
S_ISDIR(m) directory? 目录
S_ISCHR(m) character device? 字符设备文件
S_ISBLK(m) block device? 块设备文件
S_ISFIFO(m) FIFO (named pipe)? 管道文件
S_ISLNK(m) symbolic link? (Not in POSIX.1-1996.) 链接文件
S_ISSOCK(m) socket? (Not in POSIX.1-1996.) socket文件

在使用stat函数获取目录的信息时,st_size属性是没有的,目录的大小需要自己计算,里面可能包含了很多子目录等。

二、案例代码

下面使用state函数编写示例: 程序运行时,从命令行传入路径,调用opendir 函数打开目录,循环遍历目录,读取目录下的所有文件,并判断每个文件的类型,空间大小等信息打印到终端显示。

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
cpp复制代码#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <dirent.h>
#include <string.h>
#include <stdlib.h>

int main(int argc,char **argv)
{
if(argc!=2)
{
printf("参数: ./a.out <目录的路径>\n");
return 0;
}
/*1. 打开目录*/
DIR *dir=opendir(argv[1]);
if(dir==NULL)
{
printf("%s 目录打开失败.\n",argv[1]);
return -1;
}
/*2. 遍历目录*/
struct dirent *dir_info;
struct stat s_buf; //存放状态信息的
char *abs_path=NULL;
while(dir_info=readdir(dir))
{
//1. 申请空间
abs_path=malloc(strlen(argv[1])+strlen(dir_info->d_name)+1);
//2. 拼接路径
sprintf(abs_path,"%s%s",argv[1],dir_info->d_name);
//3. 获取文件的状态信息
stat(abs_path,&s_buf);
//4. 打印文件的状态
if(S_ISREG(s_buf.st_mode))
{
printf("%s 是普通文件.文件大小:%d Byte\n",abs_path,s_buf.st_size);
}
else if(S_ISDIR(s_buf.st_mode))
{
printf("%s 是目录.\n",abs_path);
}
else if(S_ISCHR(s_buf.st_mode))
{
printf("%s 字符设备文件.\n",abs_path);
}
else if(S_ISBLK(s_buf.st_mode))
{
printf("%s 块设备文件.\n",abs_path);
}
else if(S_ISFIFO(s_buf.st_mode))
{
printf("%s 是管道文件.\n",abs_path);
}
else if(S_ISLNK(s_buf.st_mode))
{
printf("%s 是链接文件.\n",abs_path);
}

//5. 释放空间
free(abs_path);
}
/*3. 关闭目录*/
closedir(dir);
return 0;
}

本文转载自: 掘金

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

【算法系列】Array算法题详细解析(续)

发表于 2021-11-22

这是我参与11月更文挑战的第22天,活动详情查看:2021最后一次更文挑战

我是小黑,一个在互联网“苟且”的程序员。

流水不争先,贵在滔滔不绝

本期内容是对上一期【算法系列】Array算法题详细解析的一个延伸,主要包含三道数组相关算法题目的解题思路和代码实现。

将数组中的奇数和偶数分开

题目描述:

给定一个整数数组,将数组中的奇数和偶数分开。元素的顺序可以改变。

思路:

  • 假设数组是arr[]
  • 初始化两个索引变量,left=0,right=arr.length-1
  • 递增left变量知道数值为奇数
  • 递减right变量直到数值为偶数
  • 如果left<right,将arr[left]和arr[right]交换
  • 最后的结果左边是偶数,右边是奇数

代码实现:

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

/**
* @author 小黑说Java
* @ClassName SeparateOddEvenMain
* @Description
* @date 2021/11/21
**/
public class SeparateOddEvenMain {

public static void main(String[] args) {

int[] arr = {12, 17, 70, 15, 22, 65, 21, 90};
System.out.println("原始数组: ");
for (int j : arr) {
System.out.print(j + " ");
}
separateEvenOddNumbers(arr);
System.out.println("\n奇数和偶数分开后数组: ");
for (int j : arr) {
System.out.print(j + " ");
}
}

public static void separateEvenOddNumbers(int[] arr) {
int left = 0;
int right = arr.length - 1;
for (int i = 0; i < arr.length; i++) {

while (arr[left] % 2 == 0) {
left++;
}
while (arr[right] % 2 == 1) {
right--;
}

if (left < right) {
int temp = arr[left];
arr[left] = arr[right];
arr[right] = temp;
}
}
}
}

运行结果:

对一个只有012的数组用O(n)时间复杂度排序

题目描述:

现有整数数组,其内部元素只有0,1,2,例如:int[] arr = [1, 2, 2, 0, 0, 1, 2, 2, 1],对其进行排序,要求为O(1)时间复杂度。

思路:

因为数组中只有数字012,所以先遍历一遍数组,记录下0,1,2出现的次数,然后重新遍历一遍数组,按照012出现的次数重新对数组中的元素赋值。

步骤:

  • 遍历给定数组一次,并不断增加所遇到的数字的计数。
  • 再次遍历数组,从索引0开始,不断改变当前索引上元素的值,首先耗尽所有的0,然后是1,最后是所有的2

这种方式必须对数组遍历两次,第一次为统计出现次数,第二次重新赋值,时间复杂度为O(n)。

代码实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
java复制代码package com.heiz.algorithm.array;

/**
* @author 小黑说Java
* @ClassName Sort012
* @Description
* @date 2021/11/21
**/
public class Sort012 {

public static void main(String[] args) {

int[] a = {1, 0, 2, 0, 0, 1, 2, 2, 1};
sort(a);
for (int val : a) {
System.out.print(val + " ");
}
}

public static void sort(int[] a) {
// 保存0,1,2出现的次数
int[] freq = new int[3];
for (int val : a) {
freq[val]++;
}
int pointer = 0;
for (int i = 0; i < freq.length; i++) {
while (freq[i]-- > 0) {
a[pointer] = i;
pointer++;
}
}
}
}

查找滑动窗口最大值

题目描述:

给定一个整数数组和一个整数k,从所有大小为k的连续子数组中找出最大元素。

比如:

1
2
shell复制代码Input : int[] arr = {2,6,-1,2,4,1,-6,5}
int k = 3

则对应的输出结果应该是:

1
shell复制代码output : 6,6,4,4,4,5

也就是对每一个大小为3的子数组,打印其中的最大值。

思路:

将数组按照K生成所有的子数组,然后遍历找出他们的最大值。

这种方式对于每个点基本都是取下一个K个元素,然后遍历,找到最大值,这个算法的时间复杂度为O(n*k)。

代码实现:

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 com.heiz.algorithm.array;

/**
* @author 小黑说Java
* @ClassName SlidingWindowMax
* @Description
* @date 2021/11/21
**/

import java.util.Scanner;

public class SlidingWindowMax {

public static void main(String[] args) {
System.out.println("请输入数组大小:");
Scanner scn = new Scanner(System.in);
int[] arr = new int[scn.nextInt()];
System.out.println("请输入数组元素:");
for (int i = 0; i < arr.length; i++) {
arr[i] = scn.nextInt();
}
System.out.println("请输入窗口大小:");
int windowSize = scn.nextInt();
solve(arr, windowSize);

}

public static void solve(int[] arr, int k) {
for (int i = k; i <= arr.length; i++) {
int max = Integer.MIN_VALUE;

for (int j = i - k; j < i; j++) {
max = Math.max(max, arr[j]);
}

System.out.print(max +"\t");
}
}
}

运行结果:

思路二:

使用Deque来帮助我们找到O(n)中滑动窗口的最大值。

Deque是一个双端队列,也就是说,你可以从前面或后面添加或删除元素。

我们解决这个问题的方法是:

我们保留子数组的k个元素以相反的顺序,我们不需要保留所有的k个元素,我们将在后面的代码中看到。

为前k个元素生成Deque,保持它们的倒序,以便最大的元素在前面。

如果Deque是空的,直接添加元素,否则检查传入的元素是否大于最后一个元素,如果是,继续从最后一个元素弹出元素,直到剩余Deque的最后一个元素大于传入的元素。

我们还需要删除属于不同子数组的元素。即Deque的下标必须在[i, i+k]范围内。

代码实现:

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

import java.util.LinkedList;
import java.util.Scanner;

/**
* @author 小黑说Java
* @ClassName SlidingWindowMax
* @Description
* @date 2021/11/21
**/

public class SlidingWindowMax {

public static void main(String[] args) {
System.out.println("请输入数组大小:");
Scanner scn = new Scanner(System.in);
int[] arr = new int[scn.nextInt()];
System.out.println("请输入数组元素:");
for (int i = 0; i < arr.length; i++) {
arr[i] = scn.nextInt();
}

System.out.print("arr[]: {");
for (int j : arr) {
System.out.print(" " + j);
}
System.out.println(" }");
System.out.println("请输入窗口大小:");
int windowSize = scn.nextInt();

solveEfficient(arr, windowSize);

}

public static void solveEfficient(int[] arr, int k) {
LinkedList<Integer> deque = new LinkedList<>();

for (int i = 0; i < arr.length; i++) {

while (!deque.isEmpty() && arr[deque.getLast()] <= arr[i]) {
deque.removeLast();
}

while (!deque.isEmpty() && deque.getFirst() <= i - k) {
deque.removeFirst();
}

deque.addLast(i);

if (i >= k - 1) {
System.out.print(" " + arr[deque.getFirst()]);
}

}
}
}

运行结果:


本文转载自: 掘金

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

深入浅出: Java回调机制(异步) 一、什么是回调 二、C

发表于 2021-11-22

这是我参与11月更文挑战的第22天,活动详情查看:2021最后一次更文挑战

我们先来看看回调的英文定义:

A callback is a function that is passed as an argument to another function and is executed after its parent function has completed。

字面上的理解,回调函数就是一个参数,将这个函数作为参数传到另一个函数里面,当那个函数执行完之后,再执行传进去的这个函数。这个过程就叫做回调。

其实也很好理解对吧,回调,回调,就是回头调用的意思。主函数的事先干完,回头再调用传进来的那个函数。但是以前看过很多博客,他们总是将回调函数解释的云里雾里,很高深的样子。

  举一个别人举过的例子:约会结束后你送你女朋友回家,离别时,你肯定会说:“到家了给我发条信息,我很担心你。” 对不,然后你女朋友回家以后还真给你发了条信息。小伙子,你有戏了。其实这就是一个回调的过程。你留了个参数(回调)函数(要求女朋友给你发条信息)给你女朋友,然后你女朋友回家,回家的动作是主函数。她必须先回到家以后,主函数执行完了,再执行传进去的函数,然后你就收到一条信息了。

一、什么是回调

回调,回调。要先有调用,才有调用者和被调用者之间的回调。所以在百度百科中是这样的:

软件模块之间总是存在着一定的接口,从调用方式上,可以把他们分为三类:同步调用、回调和异步调用。

回调是一种特殊的调用,至于三种方式也有点不同。

  1. 同步回调,即阻塞,单向。
  2. 回调,即双向(类似自行车的两个齿轮)。
  3. 异步调用,即通过异步消息进行通知。

二、CS中的异步回调(java案例)

比如这里模拟个场景:客户端发送msg给服务端,服务端处理后(5秒),回调给客户端,告知处理成功。代码如下:

回调接口类:

1
2
3
4
5
6
7
8
java复制代码/**
* @author Jeff Lee
* @since 2015-10-21 21:34:21
* 回调模式-回调接口类
*/
public interface CSCallBack {
public void process(String status);
}

模拟客户端:

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复制代码/**
* @author Jeff Lee
* @since 2015-10-21 21:25:14
* 回调模式-模拟客户端类
*/
public class Client implements CSCallBack {

private Server server;

public Client(Server server) {
this.server = server;
}

public void sendMsg(final String msg){
System.out.println("客户端:发送的消息为:" + msg);
new Thread(new Runnable() {
@Override
public void run() {
server.getClientMsg(Client.this,msg);
}
}).start();
System.out.println("客户端:异步发送成功");
}

@Override
public void process(String status) {
System.out.println("客户端:服务端回调状态为:" + status);
}
}

模拟服务端:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
java复制代码/**
* @author Jeff Lee
* @since 2015-10-21 21:24:15
* 回调模式-模拟服务端类
*/
public class Server {

public void getClientMsg(CSCallBack csCallBack , String msg) {
System.out.println("服务端:服务端接收到客户端发送的消息为:" + msg);

// 模拟服务端需要对数据处理
try {
Thread.sleep(5 * 1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("服务端:数据处理成功,返回成功状态 200");
String status = "200";
csCallBack.process(status);
}
}

测试类:

1
2
3
4
5
6
7
8
9
10
11
12
13
java复制代码/**
* @author Jeff Lee
* @since 2015-10-21 21:24:15
* 回调模式-测试类
*/
public class CallBackTest {
public static void main(String[] args) {
Server server = new Server();
Client client = new Client(server);

client.sendMsg("Server,Hello~");
}
}

运行下测试类 — 打印结果如下:

1
2
3
4
5
6
7
java复制代码客户端:发送的消息为:Server,Hello~
客户端:异步发送成功
服务端:服务端接收到客户端发送的消息为:Server,Hello~

(这里模拟服务端对数据处理时间,等待5秒)
服务端:数据处理成功,返回成功状态 200
客户端:服务端回调状态为:200

一步一步分析下代码,核心总结如下

  1. 接口作为方法参数,其实际传入引用指向的是实现类
  2. Client的sendMsg方法中,参数为final,因为要被内部类一个新的线程可以使用。这里就体现了异步。
  3. 调用server的getClientMsg(),参数传入了Client本身(对应第一点)。

三、回调的应用场景

回调目前运用在什么场景比较多呢?从操作系统到开发者调用:

  1. Windows平台的消息机制
  2. 异步调用微信接口,根据微信返回状态对出业务逻辑响应。
  3. Servlet中的Filter(过滤器)是基于回调函数,需容器支持。

补充:其中 Filter(过滤器)和Interceptor(拦截器)的区别,拦截器基于是Java的反射机制,和容器无关。但与回调机制有异曲同工之妙。

总之,这设计让底层代码调用高层定义(实现层)的子程序,增强了程序的灵活性。

四、模式对比

上面讲了Filter和Intercepter有着异曲同工之妙。其实接口回调机制和一种设计模式—观察者模式也有相似之处:

观察者模式:

GOF说道 — “定义对象的一种一对多的依赖关系,当一个对象的状态发送改变的时候,所有对他依赖的对象都被通知到并更新。”它是一种模式,是通过接口回调的方法实现的,即它是一种回调的体现。

接口回调:

与观察者模式的区别是,它是种原理,而非具体实现。

五、心得

总结四步走:

  1. 机制,即是原理。
  2. 模式,即是体现。
  3. 记住具体场景,常见模式。
  4. 然后深入理解原理。

本文转载自: 掘金

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

咱们从头到尾说一次优雅关闭

发表于 2021-11-22

优雅关闭(Graceful Shutdown/Graceful Exit),这个词好像并没有什么官方的定义,也没找到权威的来源,不过在Bing里搜索 Graceful Exit,出现的第二条却是个专门为女性处理离婚的网站……image.png
好家伙,女性离婚一站式解决方案,这也太专业了。看来不光是程序需要优雅关闭,就连离婚也得Graceful!
​

在计算机里呢,优雅关闭指的其实就是程序的一种关闭方案。那既然有优雅关闭,肯定也有不优雅的关闭了。

Windows 的优雅关闭

就拿 Windows 电脑开关机这事来说,长按电源键强制关机,或者直接断电关机,这种就属于硬关闭(hard shutdown),操作系统接收不到任何信号就直接没了,多不优雅!
​

此时系统内,或者一些软件还没有进行关闭前的处理,比如你加班写了4个小时的PPT来没来得及保存……
​

但一般除了死机之外,很少会有人强制关机,大多数人的操作还是通过电源选项->关机操作,让操作系统自己处理关机。比如 Windows 在关机前,会主动的关闭所有应用程序,可是很多应用会捕获进程的关闭事件,导致自己无法正常关闭,从而导致系统无法正常关机。比如 office 套件里,在关闭之前如果没保存会弹框让你保存,这个机制就会干扰操作系统的正常关机。
​

或者你用的是 Win10,动不动就自己更新系统的那种,如果你在更新系统的时候断电强制关机,再次开机的时候可能就会有惊喜了……更新文件写了一半,你猜猜会出现什么问题?

网络中的优雅关闭

网络是不可靠的!

TCP 的八股文相信大家都背过,四次挥手后才能断开连接,但四次挥手也是建立在正常关闭的前提下。如果你强行拔网线,或者强制断电,对端不可能及时的检测到你的断开,此时对端如果继续发送报文,就会收到错误了。
​

你看除了优雅的四次挥手,还有 TCP KeepAlive 做心跳,光有这个还不够,应用层还得再做一层心跳,同时还得正确优雅的处理连接断开,Connection Reset 之类的错误。
​

所以,如果我们在写一个网络程序时,一定要提供关闭机制,在关闭事件中正常关闭 socket/server,从而减少因为关闭导致的更多异常问题。

怎么监听关闭事件?

各种语言都会提供这个关闭事件的监听机制,只是用法不同。借助这个关闭监听,实现优雅关闭就很轻松了。

JAVA 监听关闭

JAVA 提供了一个简单的关闭事件的监听机制,可以接收到正常关闭信号的事件,比如命令行程序下的 Ctrl+C 退出信号。

1
2
3
4
5
6
java复制代码Runtime.getRuntime().addShutdownHook(new Thread(new Runnable() {
@Override
public void run() {
System.out.println("Before shutdown...");
}
}));

在这段配置完成后,正常关闭前,ShutdownHook的线程就会被启动执行,输出 Before shutdown。当然你要是直接强制关闭,比如Windows下的结束进程,Linux 下的 Kill -9……神仙都监听不到

C++ 里监听关闭

C++ 里也有类似的实现,只要将函数注册到atexit函数中,在程序正常关闭前就可以执行注册的fnExit函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
cpp复制代码void fnExit1 (void)
{
puts ("Exit function 1.");
}

void fnExit2 (void)
{
puts ("Exit function 2.");
}

int main ()
{
atexit (fnExit1);
atexit (fnExit2);
puts ("Main function.");
return 0;
}

关闭过程中可能会遇到的问题

设想这么一个场景,一个消息消费逻辑,事务提交成功后推送周边系统。早就收到了关闭信号,但是由于有大量消息堆积,一部分已经堆积在内存队列了,可是并行消费处理的逻辑一直没执行完。
​

此时有部分消费线程提交事务,还没有推送周边系统时,就收到了 Force Kill 信号,那么就会出现数据不一致的问题,本服务数据已经落库,但没有推送三方……
graceful_shutdown_04.drawio.png
再举一个数据库的例子,存储引擎有聚集索引和非聚集索引的概念,如果一条 Insert 语句执行后,刚写了聚集索引,还没来得及写非聚集索引,进程就被干掉了,那么这俩索引数据直接就不一致了!
​

不过作为存储引擎,一定会处理这个不一致的问题。但如果可以正常关闭,让存储引擎安全的执行完,这种不一致的风险就会大大降低。

进程停止

JAVA 进程停止的机制是,所有非守护线程都已经停止后,进程才会退出。那么直接给JAVA进程发一个关闭信号,进程就能关闭吗?肯定不行!
​

JAVA 里的线程默认都是非阻塞线程,非守护线程会只要不停,JVM 进程是不会停止的。所以收到关闭信号后,得自行关闭所有的线程,比如线程池……

线程中断

线程怎么主动关闭?抱歉,这个真关不了(stop 方法从JAVA 1.1就被废弃了),只能等线程自己执行完成,或者通过软状态加 interrupt 来实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
cpp复制代码private volatile boolean stopped = false;

@Override
public void run() {
while (!stopped && Thread.interrupted()){
// do sth...
}
}

public void stop(){
stopped = true;
interrupt();
}

当线程处于 WAITTING 状态时,interrupt 方法会中断这个 WAITTING 的状态,强制返回并抛出 InterruptedException 。比如我们的线程正在卡在 Socket Read 操作上,或者 Object.wait/JUC 下的一些锁等待状态时,调用 interrupt 方法就会中断这个等待状态,直接抛出异常。
​

但如果线程没卡在 WAITING 状态,而且还是在线程池中创建的,没有软状态,那上面这个关闭策略可就不太适用了。

线程池的关闭策略

ThreadPoolExecutor 提供了两个关闭方法:

  1. shutdown - interrupt 空闲的 Worker线程,等待所有任务(线程)执行完成。因为空闲 Worker 线程会处于 WAITING 状态,所以interrupt 方法会直接中断 WAITING 状态,停止这些空闲线程。
  2. shutdownNow - interrupt 所有的 Worker 线程,不管是不是空闲。对于空闲线程来说,和 shutdown 方法一样,直接就被停止了,可以对于正在工作中的 Worker 线程,不一定处于 WAITING状态,所以 interrupt 就不能保证关闭了。

注意:大多数的线程池,或者调用线程池的框架,他们的默认关闭策略是调用 shutdown,而不是 shutdownNow,所以正在执行的线程并不一定会被 Interrupt
​

但作为业务线程,一定要处理 **`InterruptedException`。不然万一有shutdownAll,或者是手动创建线程的中断,业务线程没有及时响应,可能就会导致线程彻底无法关闭了**

三方框架的关闭策略

除了 JDK 的线程池之外,一些三方框架/库,也会提供一些正常关闭的方法。

  • Netty 里的 EventLoopGroup.shutdownGracefully/shutdown - 关闭线程池等资源
  • Reddsion 里的 Redisson.shutdown - 关闭连接池的连接,销毁各种资源
  • Apache HTTPClient 里的 CloseableHttpClient.close - 关闭连接池的连接,关闭 Evictor 线程等

这些主流的成熟框架,都会给你提供一个优雅关闭的方法,保证你在调用关闭之后,它可以销毁资源,关闭它自己创建的线程/池。
​

尤其是这种涉及到创建线程的三方框架,必须要提供正常关闭的方法,不然可能会出现线程无法关闭,导致最终 JVM 进程不能正常退出的情况。

Tomcat 里的优雅关闭

Tomcat 的关闭脚本(sh 版本)设计的很不错,直接手摸手的告诉你应该怎么关:

1
2
3
4
5
bash复制代码commands:
stop Stop Catalina, waiting up to 5 seconds for the process to end
stop n Stop Catalina, waiting up to n seconds for the process to end
stop -force Stop Catalina, wait up to 5 seconds and then use kill -KILL if still running
stop n -force Stop Catalina, wait up to n seconds and then use kill -KILL if still running

这个设计很灵活,直接提供 4 种关闭方式,任你随便选择。

在 force 模式下,会给进程发送一个 SIGTERM Signal(kill -15),这个信号是可以被 JVM 捕获到的,会执行注册的 ShutdownHook 线程。等待5秒后如果进程还在,就 Force Kill,流程如下图所示:
graceful_shutdown_02.drawio.png
​

接着 Tomcat 里注册的 ShutdownHook 线程会被执行,手动的关闭各种资源,比如 Tomcat 自己的连接,线程池等等。
​

当然还有最重要的一步,关闭所有的 APP:

1
2
3
4
5
6
java复制代码// org.apache.catalina.core.StandardContext#stopInternal

// 关闭所有应用下的所有 Filter - filter.destroy();
filterStop();
// 关闭所有应用下的所有 Listener - listener.contextDestroyed(event);
listenerStop();

借助这俩关闭前的 Hook,应用程序就可以自行处理关闭了,比如在 XML 时代时使用的Servlet Context Listener:

1
2
3
xml复制代码<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>

Spring 在这个 Listener 内,自行调用 Application Context 的关闭方法:

1
2
3
4
5
java复制代码public void contextDestroyed(ServletContextEvent event) {
// 关闭 Spring Application Context
this.closeWebApplicationContext(event.getServletContext());
ContextCleanupListener.cleanupAttributes(event.getServletContext());
}

Spring 的优雅关闭

在 Spring ApplicationContext 执行 close 后,Spring 会对所有的 Bean 执行销毁动作,只要你的 Bean 配置了 destroy 策略,或者实现了 AutoCloseable 接口 ,那么 Spring 在销毁 Bean 时就可以调用 destroy 了,比如 Spring 包装的线程池 - ThreadPoolTaskExecutor,它就实现了 DisposableBean 接口:

1
2
3
4
java复制代码// ThreadPoolTaskExecutor
public void destroy() {
shutdown();
}

在 destroy Bean 时,这个线程池就会执行 shutdown,不需要你手动控制线程池的 shutdown。
​

这里需要注意一下,Spring 创建 Bean 和销毁 Bean的顺序是相反的:
​

spring_bean_priority.drawio.png
销毁时使用相反的顺序,就可以保证依赖 Bean 可以正常被销毁,而不会提前销毁。比如 A->B->C这个依赖关系中,我们一定会保证C先加载;那么在如果先销毁 C 的话 ,B可能还在运行,此时B可能就报错了。
​

所以在处理复杂依赖关系的 Bean 时,应该让前置 Bean 先加载,线程池等基础 Bean 最后加载,销毁时就会先销毁线程池这种基础 Bean了。


大多数需要正常关闭的框架/库在集成 Spring 时,都会集成 Spring Bean 的销毁入口。
​

比如 Redis 客户端 - Lettuce,spring-data-redis 里提供了 lettuce 的集成,集成类 LettuceConnectionFactory 是直接实现 DisposableBean 接口的,在 destroy 方法内部进行关闭

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
java复制代码// LettuceConnectionFactory 

public void destroy() {
this.resetConnection();
this.dispose(this.connectionProvider);
this.dispose(this.reactiveConnectionProvider);

try {
Duration quietPeriod = this.clientConfiguration.getShutdownQuietPeriod();
Duration timeout = this.clientConfiguration.getShutdownTimeout();
this.client.shutdown(quietPeriod.toMillis(), timeout.toMillis(), TimeUnit.MILLISECONDS);
} catch (Exception var4) {
if (this.log.isWarnEnabled()) {
this.log.warn((this.client != null ? ClassUtils.getShortName(this.client.getClass()) : "LettuceClient") + " did not shut down gracefully.", var4);
}
}

if (this.clusterCommandExecutor != null) {
try {
this.clusterCommandExecutor.destroy();
} catch (Exception var3) {
this.log.warn("Cannot properly close cluster command executor", var3);
}
}

this.destroyed = true;
}

其他框架也是一样,集成 Spring 时,都会基于Spring 的 destroy 机制来进行资源的销毁。

Spring 销毁机制的问题

现在有这样一个场景,我们创建了某个 MQ 消费的客户端对象,就叫 XMQConsumer 吧。在这个消费客户端中,内置了一个线程池,当 pull 到消息时会丢到线程池中执行。
​

在消息 MQ 消费的代码中,需要数据库连接池 - DataSource,还需要发送 HTTP 请求 - HttpClient,这俩对象都是被 Spring 托管的。不过 DataSource 和 HttpClient 这俩 Bean 的加载顺序比较靠前,在 XMQConsumer 启动时,这俩 Bean 一定时初始化完成可以使用的。
​

不过这里没有给这个 XMQConsumer 指定 destroy-method,所以 Spring 容器在关闭时,并不会关闭这个消费客户端,消费客户端会继续 pull 消息,消费消息。
​

此时当 Tomcat 收到关闭信号后,按照上面的关闭流程,Spring 会按照 Bean 的加载顺序逆序的依次销毁:

spring_bean_destroy_order.drawio.png

由于 XMQConsumer 没有指定 destroy ,所以 Spring 只会销毁 #2 和 #3 两个 Bean。但 XMQConsumer 线程池里的线程和主线程可是异步的,在销毁前两个对象时,消费线程仍然在运行,运行过程里需要操作数据库,还需要通过 HttpClient 发送请求,此时就会出现:XXX is Closed 之类的错误。

Spring Boot 优雅关闭

到了 Spring Boot 之后,这个关闭机制发生了一点点变化。因为之前是 Spring 项目部署在 Tomcat 里运行,由Tomcat 来启动 Spring。
​

可在 Spring Boot(Executeable Jar 方式)中,顺序反过来了,因为是直接启动 Spring ,然后在 Spring 中来启动 Tomcat(Embedded)。启动方式变了,那么关闭方式肯定也变了,shutdownHook 由 Spring 来负责,最后 Spring 去关闭 Tomcat。
​

如下图所示,这是两种方式的启动/停止顺序:
Untitled Diagram.drawio.png

K8S 优雅关闭

这里说的是 K8S 优雅关闭 POD 的机制,和前面介绍的 Tomcat 关闭脚本类似,都是先发送 SIGTERM Signal ,N秒后如果进程还在,就 Force Kill。
​

只是 Kill 的发起者变成了 K8S/Runtime,容器运行时会给 Pod 内所有容器的主进程发送 Kill(TERM) 信号:
graceful_shutdown_03.drawio.png
同样的,如果在宽限期内(terminationGracePeriodSeconds,默认30秒) ,容器内的进程没有处理完成关闭逻辑,进程会被强制杀死。

当K8S遇到 SpringBoot(Executeable Jar)

没什么特殊的,由 K8S 对 Spring Boot 进程发送 TERM 信号,然后执行 Spring Boot 的 ShutdownHook

当K8S遇到 Tomcat

和 Tomcat 的 catalina.sh 关闭方式完全一样,只是这个关闭的发起者变成了 K8S

总结

说了这么多的优雅关闭,到底怎么算优雅呢?这里简单总结 3 点:

  1. 作为框架/库,一定要提供正常关闭的方法,手动的关闭线程/线程池,销毁连接资源,FD资源等
  2. 作为应用程序,一定要处理好 InterruptedException,千万不要忽略这个异常,不然有进程无法正常退出的风险
  3. 在关闭时,一定要注意顺序,尤其是线程池类的资源,一定要保证线程池先关闭。最安全的做法是不要 interrupt 线程,等待线程自己执行完成,然后再关闭。

参考

  • kubernetes.io/zh/docs/con…
  • github.com/apache/tomc…
  • whatis.techtarget.com/definition/…
  • www.wikiwand.com/en/Graceful…
  • docs.spring.io/spring-boot…

​

本文转载自: 掘金

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

如何备份恢复ORACLE_HOME? 一、前言 三、还原OR

发表于 2021-11-22

「这是我参与11月更文挑战的第22天,活动详情查看:2021最后一次更文挑战」

一、前言

通常在安装补丁集或者补丁之前,强烈建议备份 oracle_home 和 oraInventory 目录。

1
2
3
4
bash复制代码The inventory is organized as follows:

Central Inventory (oraInventory)
Local Inventory (Oracle Home inventory)

可以分为 冷备 和 热备,一般建议是将数据库的所有进程(实例,监听等)全部关闭,进行冷备。

备份方式可以有很多种,比如 zip,tar,cpio等方式来压缩oracle_home。

\*参考MOS文档:How to Perform ORACLE_HOME Backup? (Doc ID 565017.1) ***

二、备份ORACLE_HOME

下面使用 tar 压缩方式来进行备份:

本文使用的是单实例11204数据库。

1、关闭数据库所有进程(可选)

1
2
3
4
5
6
7
8
bash复制代码##Non-rac
shutdown immediate
lsnrctl stop

##rac
srvctl stop database -d orcl
srvctl stop listener
srvctl stop scan_listner

2、进入ORACLE_HOME目录所在位置

1
2
bash复制代码echo $ORACLE_HOME
cd /u01/app/oracle/product/11.2.0

3、备份ORACLE_HOME(建议使用root用户)

1
2
3
bash复制代码mkdir /backup
cd /u01/app/oracle/product/11.2.0
tar -pcf /backup/oracle_home_backup210421.tar db

三、还原ORACLE_HOME

1、关闭数据库所有进程(必须)

1
2
3
4
5
6
7
8
bash复制代码##Non-rac
shutdown immediate
lsnrctl stop

##rac
srvctl stop database -d orcl
srvctl stop listener
srvctl stop scan_listner

2、进入ORACLE_HOME目录所在位置

1
2
bash复制代码echo $ORACLE_HOME
cd /u01/app/oracle/product/11.2.0

3、重命名或移走当前ORACLE_HOME

1
2
3
4
5
bash复制代码mv db db_backup210421

or

mv db /backup/db_backup210421

4、还原ORACLE_HOME(oracle用户下)

1
2
3
bash复制代码chown -R oracle:oinstall /backup
cd /u01/app/oracle/product/11.2.0
tar -pxf /backup/oracle_home_backup210421.tar

四、开启数据库

1
2
3
4
5
6
7
8
9
bash复制代码##开启数据库实例和监听
##Non-rac
startup
lsnrctl start

--rac
srvctl start database -d orcl
srvctl start listener
srvctl start scan_listener

本文转载自: 掘金

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

如何让 a == 1 && a == 2 && a == 3

发表于 2021-11-22

「这是我参与11月更文挑战的第22天,活动详情查看:2021最后一次更文挑战」

简述

看到这个问题是不是觉得很神奇,其实只要利用一点Java的语法特性就能解决问题。

1、解决思路:

Java专家组发现,Integer对象初始化的大部分数据介于-128127之间,因此Integer对小数据(-128127)预置了缓存,以此来提升大部分情况下实例化一个Integer的性能。其解决办法是,在jvm初始化的时候,数据-128127之间的数字便被缓存到了本地内存中,如果初始化-128127之间的数字,会直接从内存中取出,不需要新建一个对象,而这个数据被缓存在Integer的内部类IntegerCache中。所以我们通过获取Integer内部类IntegerCache,也就是缓存-128~127的类,并将索引为129(也就是1)的对象赋值给130(2)、131(3),此时当我们从Integer中去取值的时候就会发现1==2==3。

2、Integer内部类IntegerCache源码

源码示例:

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
ini复制代码/**
* IntegerCache缓存了-128~127
*/
private static class IntegerCache {
static final int low = -128;
static final int high;
static final Integer cache[];

static {
// high value may be configured by property
int h = 127;
String integerCacheHighPropValue =
sun.misc.VM.getSavedProperty("java.lang.Integer.IntegerCache.high");
if (integerCacheHighPropValue != null) {
try {
int i = parseInt(integerCacheHighPropValue);
i = Math.max(i, 127);
// Maximum array size is Integer.MAX_VALUE
h = Math.min(i, Integer.MAX_VALUE - (-low) -1);
} catch( NumberFormatException nfe) {
// If the property cannot be parsed into an int, ignore it.
}
}
high = h;

cache = new Integer[(high - low) + 1];
int j = low;
for(int k = 0; k < cache.length; k++)
cache[k] = new Integer(j++);

// range [-128, 127] must be interned (JLS7 5.1.7)
assert IntegerCache.high >= 127;
}

private IntegerCache() {}
}

3、具体实现

代码示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
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
ini复制代码package com.lizba.p3;

import java.lang.reflect.Field;

/**
* <p>
* a == 1 && a == 2 && a == 3示例代码
* </p >
*
* @Author: Liziba
* @Date: 2021/6/3 17:19
*/
public class IntegerTest {


public static void main(String[] args){

try {
Class<?>[] declaredClasses = Integer.class.getDeclaredClasses();
Class<?> integerCache = declaredClasses[0];
Field f = integerCache.getDeclaredField("cache");
f.setAccessible(true);
Integer[] cache = (Integer[]) f.get(integerCache);

System.out.println(cache[129]);
System.out.println(cache[130]);
System.out.println(cache[131]);

cache[130] = cache[129];
cache[131] = cache[129];
Integer a = 1;

// true
if (a == (Integer) 1 && a == (Integer) 2 && a == (Integer) 3) {
System.out.println(true);
}

// true
if (a == Integer.valueOf(1) && a == Integer.valueOf(2) && a == Integer.valueOf(3)) {
System.out.println(true);
}

// 无输出
if (a == new Integer(1) && a == new Integer(2) && a == new Integer(3)) {
System.out.println(true);
}
System.out.println(cache[129]);
System.out.println(cache[130]);
System.out.println(cache[131]);
} catch (Exception e) {
e.printStackTrace();
}

}
}

查看输出结果

4、注意点

如下三个为什么前两个为true,最后一个为false呢?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
scss复制代码// true
if (a == (Integer) 1 && a == (Integer) 2 && a == (Integer) 3) {
System.out.println(true);
}

// true
if (a == Integer.valueOf(1) && a == Integer.valueOf(2) && a == Integer.valueOf(3)) {
System.out.println(true);
}

// 无输出
if (a == new Integer(1) && a == new Integer(2) && a == new Integer(3)) {
System.out.println(true);
}

这个答案我们通过源码来揭晓:

当我们通过new关键字来构造一个Integer时,此时是重新在JVM中实例化的一个对象,因此对象地址是不相等的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
java复制代码 /**
* The value of the {@code Integer}.
*
* @serial
*/
private final int value;

/**
* Constructs a newly allocated {@code Integer} object that
* represents the specified {@code int} value.
*
* @param value the value to be represented by the
* {@code Integer} object.
*/
public Integer(int value) {
this.value = value;
}

而当我们使用Integer.valueOf(int),-128~127的值会从IntegerCache中获取,此时返回的是同一个对象,地址是相等的。

1
2
3
4
5
6
arduino复制代码// -128~127从IntegerCache中获取
public static Integer valueOf(int i) {
if (i >= IntegerCache.low && i <= IntegerCache.high)
return IntegerCache.cache[i + (-IntegerCache.low)];
return new Integer(i);
}

5、总结

这些知识点都是用来考验一个开发者对JDK特性的熟练程度和平时的积累,有时候要是问上了也是有用的,或者也给我们在实际开发中做数据预热来提升服务的性能,带来了一定的思考价值。

本文转载自: 掘金

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

猿猿有责,维持整洁的 Git 提交记录,三个锦囊送给你

发表于 2021-11-22

背景

大家都有学习如何规范简洁的编写代码,但却很少学习如何规范简洁的提交代码。现在大家基本上都用 Git 作为源码管理的工具,Git 提供了极大的灵活性,我们按照各种 workflow 来提交/合并 code,这种灵活性把控不好,也会带来很多问题

最常见的问题就是乱成一团的 git log history,那真的是老太太的裹脚布, 又臭又长, 个人极其不喜欢这种 log

造成这个问题的根本原因就是随意提交代码。

代码都提交了,那还有什么办法拯救吗?三个锦囊,就可以完美解决了

善用 git commit –amend

这个命令的帮助文档是这样描述的:

1
shell复制代码--amend               amend previous commit

也就是说,它可以帮助我们修改最后一次提交

既可以修改我们提交的 message,又可以修改我们提交的文件,最后还会替换最后一个 commit-id

我们可能会在某次提交的时候遗漏了某个文件,当我们再次提交就可能会多处一个无用的 commit-id,大家都这样做,git log 慢慢就会乱的无法追踪完整功能了

假设我们有这样一段 log 信息

1
2
3
4
shell复制代码* 98a75af (HEAD -> feature/JIRA123-amend-test) feat: [JIRA123] add feature 1.2
* 119f86e feat: [JIRA123] add feature 1.1
* 5dd0ad3 feat: [JIRA123] add feature 1
* c69f53d (origin/main, origin/feature/JIRA123-amend-test, origin/HEAD, main) Initial commit

假设我们要修改最后一个 log message,就可以使用下面命令:

1
shell复制代码git commit --amend -m "feat: [JIRA123] add feature 1.2 and 1.3"

我们再来看一下 log 信息, 可以发现,我们用新的 commit-id 5e354d1 替换了旧的 commit-id 98a75af, 修改了 message,并没有增加节点

1
2
3
4
shell复制代码* 5e354d1 (HEAD -> feature/JIRA123-amend-test) feat: [JIRA123] add feature 1.2 and 1.3
* 119f86e feat: [JIRA123] add feature 1.1
* 5dd0ad3 feat: [JIRA123] add feature 1
* c69f53d (origin/main, origin/feature/JIRA123-amend-test, origin/HEAD, main) Initial commit

现在我们的 repo 中文件是这样的:

1
2
3
4
5
shell复制代码.
├── README.md
└── feat1.txt

0 directories, 2 files

假设我们提交 feature 1.3 的时候,忘记了一个配置文件 config.yaml, 不想修改 log,不想添加新的 commit-id,那下面的这个命令就非常好用了

1
2
3
shell复制代码echo "feature 1.3 config info" > config.yaml
git add .
git commit --amend --no-edit

git commit --amend --no-edit 就是灵魂所在了,来看一下当前的 repo 文件:

1
2
3
4
5
6
shell复制代码.
├── README.md
├── config.yaml
└── feat1.txt

0 directories, 3 files

再来看一下 git log

1
2
3
4
shell复制代码* 247572e (HEAD -> feature/JIRA123-amend-test) feat: [JIRA123] add feature 1.2 and 1.3
* 119f86e feat: [JIRA123] add feature 1.1
* 5dd0ad3 feat: [JIRA123] add feature 1
* c69f53d (origin/main, origin/feature/JIRA123-amend-test, origin/HEAD, main) Initial commit

知道这个技巧,就可以确保我们的每次提交都包含有效的信息了。一张图描述这个过程就是这个样子了:

有了 --no-edit 的 buff 加成,威力更大一些

善用 git rebase -i

可以看着,上面的 log 都是在开发 feature1,我们在把 feature 分支 merge 到 main 分支之前,还是应该继续合并 log commit 节点的,这就用到了

1
shell复制代码git rebase -i HEAD~n

其中 n 代表最后几个提交,上面我们针对 feature 1 有三个提交,所以就可以使用:

1
shell复制代码git rebase -i HEAD~3

运行后,会显示一个 vim 编辑器,内容如下:

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
shell复制代码  1 pick 5dd0ad3 feat: [JIRA123] add feature 1
2 pick 119f86e feat: [JIRA123] add feature 1.1
3 pick 247572e feat: [JIRA123] add feature 1.2 and 1.3
4
5 # Rebase c69f53d..247572e onto c69f53d (3 commands)
6 #
7 # Commands:
8 # p, pick <commit> = use commit
9 # r, reword <commit> = use commit, but edit the commit message
10 # e, edit <commit> = use commit, but stop for amending
11 # s, squash <commit> = use commit, but meld into previous commit
12 # f, fixup <commit> = like "squash", but discard this commit's log message
13 # x, exec <command> = run command (the rest of the line) using shell
14 # d, drop <commit> = remove commit
15 # l, label <label> = label current HEAD with a name
16 # t, reset <label> = reset HEAD to a label
17 # m, merge [-C <commit> | -c <commit>] <label> [# <oneline>]
18 # . create a merge commit using the original merge commit's
19 # . message (or the oneline, if no original merge commit was
20 # . specified). Use -c <commit> to reword the commit message.
21 #
22 # These lines can be re-ordered; they are executed from top to bottom.
23 #
24 # If you remove a line here THAT COMMIT WILL BE LOST.
25 #
26 # However, if you remove everything, the rebase will be aborted.
27 #
28 #
29 # Note that empty commits are commented out

合并 commit-id 最常用的是 squash 和 fixup, 前者包含 commit message,后者不包含,这里使用 fixup, 然后 :wq 退出

1
2
3
shell复制代码  1 pick 5dd0ad3 feat: [JIRA123] add feature 1
2 fixup 119f86e feat: [JIRA123] add feature 1.1
3 fixup 247572e feat: [JIRA123] add feature 1.2 and 1.3

我们再来看一下 log, 这就非常清晰了

1
2
shell复制代码* 41cd711 (HEAD -> feature/JIRA123-amend-test) feat: [JIRA123] add feature 1
* c69f53d (origin/main, origin/feature/JIRA123-amend-test, origin/HEAD, main) Initial commit

善用 rebase

上面的 feature1 已经完整的开发完了,main 分支也有了其他人的更新,在将 feature merge 回 main 分支之前,以防代码有冲突,需要先将 main 分支的内容合并到 feature 中,如果用 merge 命令,就会多处一个 merge 节点,log history 中也会出现拐点,并不是线性的,所以这里我们可以在 feature 分支上使用 rebase 命令

1
shell复制代码git pull origin main --rebase

pull 命令的背后是自动帮我们做 merge 的,但是这里以 rebase 的形式,再来看一下 log

1
2
3
shell复制代码* d40daa6 (HEAD -> feature/JIRA123-amend-test) feat: [JIRA123] add feature 1
* 446f463 (origin/main, origin/HEAD) Create main.properties
* c69f53d (origin/feature/JIRA123-amend-test, main) Initial commit

我们的 feature1 功能 on top of main 的提交节点,还是保持线性,接下来就可以 push 代码,然后提 PR,将你的 feature merge 到 main 分支了

简单描述 merge 和 rebase 的区别就是这样的:

我这里使用 git pull origin main --rebase 省略了切换 main 并拉取最新内容再切回来的过程,一步到位,背后的原理都是上图展示的这样

使用 rebase 是要遵守一个黄金法则的,这个之前有说过,就不再是赘述了

总结

有了这三个锦囊,相信大家的 git log 都无比的清晰,如果你还不知道,完全可以用起来,如果你的组内成员不知道,你完全可以推广起来,这样的 repo 看起来才更健康

接下来会介绍一个多分支切换互不影响的锦囊
个人博客:https://dayarch.top

日拱一兵 | 原创

本文转载自: 掘金

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

Java基础学习21之反射 Java基础学习21之反射 反射

发表于 2021-11-22

Java基础学习21之反射

「这是我参与11月更文挑战的第21天,活动详情查看:2021最后一次更文挑战」。

关于作者

  • 作者介绍

🍓 博客主页:作者主页

🍓 简介:JAVA领域优质创作者🥇、一名在校大三学生🎓、在校期间参加各种省赛、国赛,斩获一系列荣誉🏆

🍓 关注我:关注我学习资料、文档下载统统都有,每日定时更新文章,励志做一名JAVA资深程序猿👨‍💻


反射机制

反射机制如果只是针对普通开发者而言意义不大,一般都是作为一些系统的构架设计去使用的,包括以后学习的开源框架,那么几乎都是反射机制。

认识反射

反射指的是对象的反向处理操作,就首先观察以下“正”的操作,在默认情况下,必须先导入一个包才能产生类的实例化对象。

所谓的“反”根据对象来取得对象的来源信息,“反”的操作的本来来源就是Object的一个方法。

  • 取得class对象:public final 类<?> getClass() 该方法返回的一个Class类的对象,这个Class描述的就是类。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
java复制代码package com.day16.demo;

import java.util.Date;

import javafx.scene.chart.PieChart.Data;

public class FanShedemo {
public static void main(String[] args) {
Date date = new Date();
//java.util.Date
System.out.println(date.getClass().getName());

}
}

此时通过对象的确对象的来源,就是“反”的本质。在反射的背后不在是一个对象,而是对象身后的来源。

而这个getClass()方法返回的对象是Class类对象,所以这个Class就是所有反射操作的源头,但是在讲解其真正使用之前还有一个需要先解释的问题,既然Class是所有反射操作的源头,那么这个类肯定是最为重要的,而如果要想取得这个类的实例化对象,java中定义了三种方式:

方式一:通过Object类的getClass()方法取得

1
2
3
4
5
6
7
8
9
10
11
java复制代码package com.day16.demo;

import java.util.Date;

public class FanShedemo {
public static void main(String[] args) {
Date de = new Date();//正着操作
Class<?> cla =de.getClass();//取得class对象
System.out.println(cla.getName());//反着来
}
}

方式二:通过“类.Class”取得

1
2
3
4
5
6
7
8
9
10
11
12
java复制代码package com.day16.demo;

import java.util.Date;


public class FanShedemo {
public static void main(String[] args) {
Date de = new Date();//正着操作
Class<?> cla =Date.class;//取得class对象
System.out.println(cla.getName());//反着来
}
}

方式三:使用Class内部定义的一个static方法

1
2
3
4
5
6
7
8
9
10
11
12
java复制代码package com.day16.demo;

import java.util.Date;


public class FanShedemo {
public static void main(String[] args) throws Exception{
Date de = new Date();//正着操作
Class<?> cla =Class.forName("java.util.Date");//取得class对象
System.out.println(cla.getName());//反着来
}
}

在以上给出的三个方法会发现一个神奇的地方,除了第一种形式会产生Date实例化对象,而第二种和第三种没有

实例化取得对象。于是取得Class类对象有一个最直接的好处:可以直接通过反射实例化对象,在Class类中有一个方法:

  • 通过反射实例化对象:public T newInstance() throws InstantiationException IllegalAccessException

反射实例化对象

1
2
3
4
5
6
7
8
9
10
11
12
java复制代码package com.day16.demo;

import java.util.Date;


public class FanShedemo {
public static void main(String[] args) throws Exception{
Class<?> cla =Class.forName("java.util.Date");//取得class对象
Object o = cla.newInstance();//取得Date对象
System.out.println(o);
}
}

image-20210821155437468

现在可以发现,对于对象的实例化操作,除了使用关键字new之外又多了一个反射机制操作,而且这个操作要比之前使用的new复杂一些,可是有什么用呢?

对于程序的开发模式之前一直强点:尽量减少耦合,而减少耦合的最好的做法是使用接口,但是就算使用了接口也逃不出关键字new,多以实际上new是耦合的关键元凶。

取得父类信息

反射可以做出一个对象所具备的所有操作行为,而且最关键的是这一切的操作都可以基于Object类型进行。

在Java里面任何的程序类实际上都一定会有一个父类,在Class类里面就可以通过此类方式来取得父类或者是实现的父接口,有如下两个方法提供:

public 软件包 getPackage() 取得类的包名称
public 类<? super T> getSuperclass() 取得父类的Class对象
public 类<?>[] getInterfaces() 取得父接口

取得类的相关信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
java复制代码package com.day16.demo;

import java.util.Arrays;
interface IFruit{}
interface IMessage{}
class Person implements IFruit,IMessage{

}
public class FanShedemo {
public static void main(String[] args) throws Exception{
Class<?> cls = Person.class;
System.out.println(cls.getPackage().getName());
System.out.println(cls.getSuperclass().getName());
Class<?> itf[] = cls.getInterfaces();
System.out.println(Arrays.toString(itf));
}
}

通过我们反射可以取得类结构上的所有关键信息。

反射调用构造

一个类可以存在多个构造方法,如果我们要想取得类中构造的调用,我们就可以使用Class类提供的两个方法

public Constructor getConstructor(类<?>… parameterTypes) throws NoSuchMethodException,SecurityException 取得指定参数类型的构造方法
public Constructor<?>[] getConstructors()throws SecurityException 取得类中的所有构造

以上两个方法的返回类型都是java.lang.reflect.Constructor类的实例化对象,这个类里面关注一个方法,实例化对象:public T newInstance(Object… initargs) throws InstantiationException,
IllegalAccessException, IllegalArgumentException, InvocationTargetException

取得类中所有构造方法的信息—利用Constructor类中的toString()方法取得了构造方法的完整信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
java复制代码package com.day16.demo;

import java.lang.reflect.Constructor;
import java.util.Arrays;

class Person {
public Person(){}
public Person(String name){}
public Person(String name , int age){}
}
public class FanShedemo {
public static void main(String[] args) throws Exception{
Class<?> cls = Person.class;
Constructor<?> cst [] = cls.getConstructors();
for (int i = 0; i < cst.length; i++) {
System.out.println(cst[i]);
}
}
}

如果使用getName()方法就比较麻烦

自己拼凑构造方法操作

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

import java.lang.reflect.Constructor;
import java.lang.reflect.Modifier;
import java.util.Arrays;

class Person {
public Person() throws Exception,RuntimeException{}
public Person (String name) throws Exception,RuntimeException{}
public Person(String name , int age) throws Exception,RuntimeException{}
}
public class FanShedemo {
public static void main(String[] args) throws Exception{
Class<?> cls = Person.class;
Constructor<?> cst [] = cls.getConstructors();
for (int i = 0; i < cst.length; i++) {
System.out.print(Modifier.toString(cst[i].getModifiers()) + " ");
System.out.print(cst[i].getName() + "(");
Class <?> params [] = cst[i].getParameterTypes();
for (int j = 0; j < params.length; j++) {
System.out.print(params[j].getName());
if(j < params.length - 1){
System.out.print(",");
}
}
System.out.print(")");
Class<?> exps [] = cst[i].getExceptionTypes();
if(exps.length > 0){
System.out.print(" throws ");
for (int j = 0; j < exps.length; j++) {
System.out.print(exps[j].getName());
if(j < exps.length - 1){
System.out.print(",");
}
}
}
System.out.println();
}
}
}

学习Constructor类目的并不是分析方法的组成,最需要的关注就是问题的结论:在定义简单的java类一定要保留一个无参构造。

观察没有无参构造的方法

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

import java.lang.reflect.Constructor;
class Per{
private String name;
private int age;
public Per(String name,int age){
this.name=name;
this.age=age;
}
@Override
public String toString() {
return "Per [name=" + name + ", age=" + age + "]";
}
}
public class FanShedemo {
public static void main(String[] args) throws Exception{
Class<?> cls = Per.class;//取得class对象
Object obj=cls.newInstance();
}
}
1
2
3
4
5
6
7
java复制代码Exception in thread "main" java.lang.InstantiationException: com.day16.demo.Person
at java.lang.Class.newInstance(Class.java:427)
at com.day16.demo.FanShedemo.main(FanShedemo.java:19)
Caused by: java.lang.NoSuchMethodException: com.day16.demo.Person.<init>()
at java.lang.Class.getConstructor0(Class.java:3082)
at java.lang.Class.newInstance(Class.java:412)
... 1 more

此时运行的时候出现了错误提示“java.lang.InstancetiationException”因为以上的方式使用反射实例化对象时需要的是类之中提供无参构造方法,但是现在既然没有了无参构造方法,那么就必须明确的找到一个构造方法。

通过Constructor类实例化对象

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

import java.lang.reflect.Constructor;
class Per{
private String name;
private int age;
public Per(String name,int age){
this.name=name;
this.age=age;
}
@Override
public String toString() {
return "Per [name=" + name + ", age=" + age + "]";
}
}
public class FanShedemo2 {
public static void main(String[] args) throws Exception{
Class<?> cls = Per.class;//取得class对象
//现在明确表示取得指定参数类型的构造方法对象
Constructor<?> cont = cls.getConstructor(String.class,int.class);
System.out.println(cont.newInstance("张三",19));

}
}

一行些简单Java类要写无参构造,以上内容就只需要了解就可以了。

反射调用方法

当取得了一个类之中的实例化对象之后,下面最需要调用的肯定是类之中的方法,所以可以继续使用Class类取得一个类中所定义的方法定义:

public Method[] getMethods() throws SecurityException 取得全部方法
public Method getMethod(String name,Class<?>… parameterTypes) throws NoSuchMethodException, SecurityException 取得指定方法

发现以上的方法返回的都是java.lang.Method类的对象。

取得一个类之中全部定义的方法

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

import java.lang.reflect.Method;

class Student{
private String name;

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}
}
public class FanShedemo3 {
public static void main(String[] args) throws Exception {
Class<?> cls = Class.forName("com.day16.demo.Student");
Method met [] = cls.getMethods();
for (int i = 0; i < met.length; i++) {
System.out.println(met[i]);
}
}
}

但是取得类Method类对象最大的作用不在于方法的列出(方法的列出都在开发工具上使用了),但是对于取得了Method类对象之后还有一个最大的功能,就是可以利用反射调用类的方法:
调用方法:public Object invoke(Object obj,Object… args) throws IllegalAccessException,IllegalArgumentException,InvocationTargetException
之前调用类中的方法的时候使用的都是“对象.方法”,但是现在有了反射之后,可以直接利用Object类调用指定子类的操作方法。(同事解释一下,为什么setter,和getter方法的命名要求如此严格)。

利用反射调用Student类之中的setName(),getName()方法。

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

import java.lang.reflect.Method;

class Student{
private String name;

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}
}
public class FanShedemo3 {
public static void main(String[] args) throws Exception {
Class<?> cls = Class.forName("com.day16.demo.Student");
Object obj = cls.newInstance();//实例化对象
Method met [] = cls.getMethods();
Method setM=cls.getMethod("setName",String.class);//通过反射构建方法
Method getM=cls.getMethod("getName");
setM.invoke(obj, "小张同学");//键值对的形式传递参数此过程相当于setM
Object result = getM.invoke(obj);
System.out.println(result);
}
}

在日后所有的技术开发中,简单Java类都是如此应用,多以必须按照标准进行。

反射调用成员

个组成部分就是成员(Field,也可以称为属性),如果要通过反射取得类的成员可以使用方法如下:
取得本类的全部成员:public Field[] getDeclaredFields() throws SecurityException
取得指定成员:public Field getDeclaredField(String name)throws NoSuchFieldException, SecurityException

取得本类全部成员

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

import java.lang.reflect.Field;

class Person4{
private String name;
}
public class FanShedemo4 {
public static void main(String[] args) throws Exception{
Class<?> cls = Class.forName("com.day16.demo.Person4");
Object obj = cls.newInstance();
Field[] fields = cls.getDeclaredFields();
for (int i = 0; i < fields.length; i++) {
//private java.lang.String com.day16.demo.Person4.name
System.out.println(fields[i]);
}
}
}

但是找到了Field实际上就找到了一个很有意思的操作,在Field类之中提供了两个方法:

设置属性内容(类似于:对象.属性=内容):public void set(Object obj,Object value) throws IllegalArgumentException,IllegalAccessException
取得属性内容(类似于:对象.属性):public Object get(Object obj) throws IllegalArgumentException,IllegalAccessException

可是从类的开发要求而言,一直都强调类之中的属性必须封装,所以现在调用之前要想办法解除封装。

  • 解除封装(重点):public void setAccessible(boolean flag)throws SecurityException

利用反射操作类中的属性

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

import java.lang.reflect.Field;

class Person4{
private String name;
}
public class FanShedemo4 {
public static void main(String[] args) throws Exception{
Class<?> cls = Class.forName("com.day16.demo.Person4");
Object obj = cls.newInstance();
Field field = cls.getDeclaredField("name");//得到Person4成员变量
//由于Person4设置的属性是私有属性,所以是无法操作的
field.setAccessible(true);///解决封装
field.set(obj, "小张");
System.out.println(field.get(obj));
}
}

虽然反射机制运行直接操作类之中的属性,可是不会有任何一种程序直接操作属性,都会通过setter,getter方法。

image-20210821202523981

反射与简单Java类—单级VO操作原理

如果现在又一个简单Java类,那么这个简单Java类中的属性按照原始的做法一定要通过setter才可以设置,取得肯定继续使用getter(不关注此处)。

Emp.java

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

public class Emp {
private String ename;
private String job;
public String getEname() {
return ename;
}
public void setEname(String ename) {
this.ename = ename;
}
public String getJob() {
return job;
}
public void setJob(String job) {
this.job = job;
}
@Override
public String toString() {
return "Emp [ename=" + ename + ", job=" + job + "]";
}
}

EmpAction.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
java复制代码package com.day16.action;

import com.day16.vo.Emp;

public class EmpAction {
private Emp emp = new Emp();
public void setValue(String val ){//设置属性内容
this.emp.setEname("SMI");
this.emp.setJob("STRACK");
}
public Emp getEmp(){
return emp;
}
}

EmpDemo.java

1
2
3
4
5
6
7
8
9
10
11
12
java复制代码package com.day16.demo;

import com.day16.action.EmpAction;

public class EmpDemo {
public static void main(String[] args) {
String value="emp.ename:smi|emp.job:strack";
EmpAction action = new EmpAction();
action.setValue(value);
System.out.println(action.getEmp());
}
}

image-20210821211538647

单极自动VO设置实现

现在所有操作都是通过TestDemo类调用EmpAciton类实现的,而EmpAciton类的主要作用在于定位要操作属性的类型。同时该程序应该符合所有的简单Java类开发形式,也就意味着我们的设计必须有一个单独的类来实现。

由于Bean的处理操作肯定需要重复出去对象信息,所以我们还需要准备两个程序类:StringUtils,负责字符串的操作,毕竟属性的首字母需要大写处理,而后在写一个对象的具体操作(取得对象、设置对象内容)。

工具类—BeanOperation.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
java复制代码package com.day16.util;

/**
* @author 张晟睿
* 本类主要负责实现自动VO匹配处理操作,本身不需要通过实例化对象完成,所以构造方法私有化
*/

public class BeanOperation {
private BeanOperation(){}

/**
* @param actionObject 表示当前发出设置请求的程序类的当前对象
* @param msg 所有属性的具体内容,格式“属性名称:内容|属性名称:内容”
*
*/
public static void setBeanValue(Object actionObject,String msg) throws Exception{
String result [] = msg.split("\\|");
for (int i = 0; i < result.length; i++) {
//需要针对于给定的属性名称和内容进行一次拆分
String temp[] = result[i].split(":");
String attribute = temp[0];//属性名称,包括“XxxAction属性和具体的简单AJava类的属性”
String value = temp[1];//接收具体的内容属性
String fields [] = attribute.split("\\.");
Object currentObject = ObjectUtils.getObject(actionObject,fields[0]);
ObjectUtils.setObjectValue(currentObject, fields[1], value);
}
}
}

工具类—StringUtils.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
java复制代码package com.day16.util;

/**
* @author 张晟睿
* 针对于字符串进行处理操作
*/
public class StringUtils {
private StringUtils(){}
/**
* @param str
* @return 返回首字母大写
* 首字母大写
*/
public static String initcap(String str){
return str.substring(0,1).toUpperCase() + str.substring(1);
}
}

工具类—ObjectUtils.java

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

import java.lang.reflect.Field;
import java.lang.reflect.Method;

/**
* @author 张晟睿
* 本类的主要功能是根据属性名称调用响应类中getter、setter方法
*/
public class ObjectUtils {
private ObjectUtils(){}
/**
* 根据指定的类对象,设置类中的属性
* @param wrapo 属性所在类的实例化对象
* @param attribute 属性名称
* @param value 属性内容
*/
public static void setObjectValue(Object wrapo,String attribute, String value) throws Exception{

//调用指定属性的Field对象,母的是取得对象类型,如果没有属性也就是说该操作无法继续
Field field = wrapo.getClass().getDeclaredField(attribute);//判断属性是否存在
if(field == null){
field = wrapo.getClass().getField(attribute);
}
if(field == null){//两次操作都无法取得对应的成员变量
return ;//该属性一定不存在
}
String methodName = "set" + StringUtils.initcap(attribute);
Method method = wrapo.getClass().getMethod(methodName,field.getType());
method.invoke(wrapo, value);
}
/**
* 负责调用指定类中getter方法
* @param wrapo 表示要调用方法的所在对象
* @param attribute 表示属性名称
* @return 调用对象的结果
*/
public static Object getObject(Object wrapo,String attribute) throws Exception{
String methodName = "get" + StringUtils.initcap(attribute);//定义getter方法
//调用指定属性的Field对象,母的是取得对象类型,如果没有属性也就是说该操作无法继续
Field field = wrapo.getClass().getDeclaredField(attribute);
if(field == null){
field = wrapo.getClass().getField(attribute);
}
if(field == null){//两次操作都无法取得对应的成员变量
return null;//该属性一定不存在
}
Method method = wrapo.getClass().getMethod(methodName);
return method.invoke(wrapo);
}
}

EmpAction.java

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

import com.day16.util.BeanOperation;
import com.day16.vo.Emp;

public class EmpAction {
private Emp emp = new Emp();
public void setValue(String val ){//设置属性内容
//之所以传递this,主要将EmpAction的类对象传递方法里面
//因为给定的标记:emp.ename:smith,而emp应该对象的是getEmp()方法
try {
BeanOperation.setBeanValue(this, val);
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
public Emp getEmp(){
return emp;
}
}

image-20210822083531155

反射与简单Java类—多级VO设置实现

image-20210822084050435

现在假设一个雇员属于一个部门,一个部门属于一个公司,一个公司属于一个城市,一个城市属于一个省份,一个省份属于一个国家,这种类似关系都可以通过字符串实现多级配置。

修改Dept.java类

1
2
3
4
5
java复制代码public class Dept {
private String dname;
private String loc;
private Company company = new Company();
}

修改Emp.java类

1
2
3
4
5
java复制代码public class Emp {
private String ename;
private String job;
private Dept dept = new Dept();
}

此时所有的引用关系上都自动进行了对象实例化。而现在程序希望可以满足于单级和多级。

image-20210822122022038

修改BeanOperation.java

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

/**
* @author 张晟睿
* 本类主要负责实现自动VO匹配处理操作,本身不需要通过实例化对象完成,所以构造方法私有化
*/

public class BeanOperation {
private BeanOperation(){}

/**
* @param actionObject 表示当前发出设置请求的程序类的当前对象
* @param msg 所有属性的具体内容,格式“属性名称:内容|属性名称:内容”
*
*/
public static void setBeanValue(Object actionObject,String msg) throws Exception{
String result [] = msg.split("\\|");
for (int i = 0; i < result.length; i++) {
//需要针对于给定的属性名称和内容进行一次拆分
String temp[] = result[i].split(":");
String attribute = temp[0];//属性名称,包括“XxxAction属性和具体的简单AJava类的属性”
String value = temp[1];//接收具体的内容属性
String fields [] = attribute.split("\\.");//拆分出属性信息
if(fields.length > 2){//多级配置
//如果要想多级确定出属性的操作对象,那么应该一层找出每一个getter方法返回的内容
Object currentObject = actionObject;//确定当前要操作的对象
for (int j = 0; j < fields.length - 1; j++) {//对应getter返回对象
currentObject = ObjectUtils.getObject(currentObject, fields[j]);
}
ObjectUtils.setObjectValue(currentObject, fields[fields.length - 1], value);
}else{//单级配置
Object currentObject = ObjectUtils.getObject(actionObject,fields[0]);
ObjectUtils.setObjectValue(currentObject, fields[1], value);
}

}
}
}

定义TestEmpDemo.java

1
2
3
4
5
6
7
8
9
10
11
12
java复制代码package com.day16.demo;

import com.day16.action.EmpAction;

public class TestEmpDemo {
public static void main(String[] args) {
String value="emp.ename:smi|emp.job:strack|emp.dept.dname:财务部|emp.dept.company.name:zsr|emp.dept.company.address:北京";
EmpAction action = new EmpAction();
action.setValue(value);
System.out.println(action.getEmp());
}
}

这样的程序才可以正常使用,属于无限级配置。

本文转载自: 掘金

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

1…243244245…956

开发者博客

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